Compare commits

...

72 Commits

Author SHA1 Message Date
Thomas cac21689cf Rootless docker (#716)
Go CI / Lint (push) Has been cancelled
Go CI / Check (push) Has been cancelled
Go CI / Test (mysql, 1.26, mysql:8, ubuntu-latest, 3306:3306) (push) Has been cancelled
Go CI / Test (postgres, 1.26, postgres:16, ubuntu-latest, 5432:5432) (push) Has been cancelled
Go CI / Test (sqlite, 1.26, macOS-latest) (push) Has been cancelled
Go CI / Test (sqlite, 1.26, ubuntu-latest) (push) Has been cancelled
Go CI / Build (1.26, macOS-latest) (push) Has been cancelled
Go CI / Build (1.26, ubuntu-latest) (push) Has been cancelled
Go CI / Build (1.26, windows-latest) (push) Has been cancelled
Signed-off-by: Thomas Miceli <tho.miceli@gmail.com>
2026-06-20 01:54:30 +08:00
Thomas 28736d6b66 Gist expiration + scheduled actions (#726)
Signed-off-by: Thomas Miceli <tho.miceli@gmail.com>
2026-06-20 01:28:48 +08:00
Marcel Herrguth 499f9c67b9 Fixes #723 - Add no-store header for embedded js (#724) 2026-06-20 01:28:32 +08:00
Marcel Herrguth 9d9b54a5e1 Embedding light dark and auto (#718) 2026-06-20 01:07:22 +08:00
Marcel Herrguth 1ba588c90e Closes #479 - Support Syntax highlighting for Salesforce Apex language (#725)
Go CI / Lint (push) Has been cancelled
Go CI / Check (push) Has been cancelled
Go CI / Test (mysql, 1.26, mysql:8, ubuntu-latest, 3306:3306) (push) Has been cancelled
Go CI / Test (postgres, 1.26, postgres:16, ubuntu-latest, 5432:5432) (push) Has been cancelled
Go CI / Test (sqlite, 1.26, macOS-latest) (push) Has been cancelled
Go CI / Test (sqlite, 1.26, ubuntu-latest) (push) Has been cancelled
Go CI / Build (1.26, macOS-latest) (push) Has been cancelled
Go CI / Build (1.26, ubuntu-latest) (push) Has been cancelled
Go CI / Build (1.26, windows-latest) (push) Has been cancelled
* Closes #479 - Support Syntax highlighting for Salesforce Apex language

* Linter
2026-06-19 04:27:34 +08:00
Thomas a2e4734e36 GitHub alerts in Markdown (#721)
Go CI / Lint (push) Has been cancelled
Go CI / Check (push) Has been cancelled
Go CI / Test (mysql, 1.26, mysql:8, ubuntu-latest, 3306:3306) (push) Has been cancelled
Go CI / Test (postgres, 1.26, postgres:16, ubuntu-latest, 5432:5432) (push) Has been cancelled
Go CI / Test (sqlite, 1.26, macOS-latest) (push) Has been cancelled
Go CI / Test (sqlite, 1.26, ubuntu-latest) (push) Has been cancelled
Go CI / Build (1.26, macOS-latest) (push) Has been cancelled
Go CI / Build (1.26, ubuntu-latest) (push) Has been cancelled
Go CI / Build (1.26, windows-latest) (push) Has been cancelled
Signed-off-by: Thomas Miceli <tho.miceli@gmail.com>
2026-06-19 00:07:06 +08:00
Thomas 66f2793f8b Add PKCE for OAuth providers (#720)
Go CI / Lint (push) Has been cancelled
Go CI / Check (push) Has been cancelled
Go CI / Test (mysql, 1.26, mysql:8, ubuntu-latest, 3306:3306) (push) Has been cancelled
Go CI / Test (postgres, 1.26, postgres:16, ubuntu-latest, 5432:5432) (push) Has been cancelled
Go CI / Test (sqlite, 1.26, macOS-latest) (push) Has been cancelled
Go CI / Test (sqlite, 1.26, ubuntu-latest) (push) Has been cancelled
Go CI / Build (1.26, macOS-latest) (push) Has been cancelled
Go CI / Build (1.26, ubuntu-latest) (push) Has been cancelled
Go CI / Build (1.26, windows-latest) (push) Has been cancelled
* PKCE

Signed-off-by: Thomas Miceli <tho.miceli@gmail.com>
2026-06-11 02:18:33 +08:00
Thomas Miceli 2bba402787 v1.13.1
Go CI / Lint (push) Has been cancelled
Go CI / Check (push) Has been cancelled
Go CI / Test (mysql, 1.26, mysql:8, ubuntu-latest, 3306:3306) (push) Has been cancelled
Go CI / Test (postgres, 1.26, postgres:16, ubuntu-latest, 5432:5432) (push) Has been cancelled
Go CI / Test (sqlite, 1.26, macOS-latest) (push) Has been cancelled
Go CI / Test (sqlite, 1.26, ubuntu-latest) (push) Has been cancelled
Go CI / Build (1.26, macOS-latest) (push) Has been cancelled
Go CI / Build (1.26, ubuntu-latest) (push) Has been cancelled
Go CI / Build (1.26, windows-latest) (push) Has been cancelled
Signed-off-by: Thomas Miceli <tho.miceli@gmail.com>
2026-06-10 02:25:21 +07:00
Thomas daeeed3dc5 Fix CSS url for json embed url (#715)
Signed-off-by: Thomas Miceli <tho.miceli@gmail.com>
2026-06-10 02:14:42 +07:00
Marcel Herrguth 06708eb351 Embedding fix vertical scrolling and improve padding (#714)
* Maintenance: Only scroll code vertically

* Improve code padding
2026-06-10 01:02:54 +08:00
Thomas Miceli b41a80a335 v1.13.0
Go CI / Lint (push) Has been cancelled
Go CI / Check (push) Has been cancelled
Go CI / Test (mysql, 1.26, mysql:8, ubuntu-latest, 3306:3306) (push) Has been cancelled
Go CI / Test (postgres, 1.26, postgres:16, ubuntu-latest, 5432:5432) (push) Has been cancelled
Go CI / Test (sqlite, 1.26, macOS-latest) (push) Has been cancelled
Go CI / Test (sqlite, 1.26, ubuntu-latest) (push) Has been cancelled
Go CI / Build (1.26, macOS-latest) (push) Has been cancelled
Go CI / Build (1.26, ubuntu-latest) (push) Has been cancelled
Go CI / Build (1.26, windows-latest) (push) Has been cancelled
Signed-off-by: Thomas Miceli <tho.miceli@gmail.com>
2026-06-09 04:19:30 +07:00
Thomas b43789943a Upgrade deps (#713)
Signed-off-by: Thomas Miceli <tho.miceli@gmail.com>
2026-06-09 04:57:30 +08:00
Marcel Herrguth bf3257faa8 Feature: Allow embedding Gists for a certain file only (#709)
* Feature: Allow embedding Gists for a certain file only

* Move from URL to param approach

* Switch gist.Files to gist.File

* Satisfy linting
2026-06-09 03:44:39 +08:00
Peter Barr 2946de2505 fix: apply topics push option in post-receive hook (#698)
Go CI / Lint (push) Has been cancelled
Go CI / Check (push) Has been cancelled
Go CI / Test (mysql, 1.25, mysql:8, ubuntu-latest, 3306:3306) (push) Has been cancelled
Go CI / Test (postgres, 1.25, postgres:16, ubuntu-latest, 5432:5432) (push) Has been cancelled
Go CI / Test (sqlite, 1.25, macOS-latest) (push) Has been cancelled
Go CI / Test (sqlite, 1.25, ubuntu-latest) (push) Has been cancelled
Go CI / Build (1.25, macOS-latest) (push) Has been cancelled
Go CI / Build (1.25, ubuntu-latest) (push) Has been cancelled
Go CI / Build (1.25, windows-latest) (push) Has been cancelled
* Apply `topics` push option in post-receive hook

The `topics` value was parsed into the push-options map but never read
by the hook, silently discarding any topics passed via `git push -o
topics=...`. Mirror the existing `title`/`description` handling and
reuse the `gisttopics` validator.

* Docs

Signed-off-by: Thomas Miceli <tho.miceli@gmail.com>

---------

Signed-off-by: Thomas Miceli <tho.miceli@gmail.com>
Co-authored-by: Thomas Miceli <tho.miceli@gmail.com>
2026-06-08 04:20:50 +08:00
dependabot[bot] c1ca19aec9 Bump azure/setup-helm from 4.3.1 to 5.0.0 (#688)
Bumps [azure/setup-helm](https://github.com/azure/setup-helm) from 4.3.1 to 5.0.0.
- [Release notes](https://github.com/azure/setup-helm/releases)
- [Changelog](https://github.com/Azure/setup-helm/blob/main/CHANGELOG.md)
- [Commits](https://github.com/azure/setup-helm/compare/v4.3.1...v5.0.0)

---
updated-dependencies:
- dependency-name: azure/setup-helm
  dependency-version: 5.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-08 04:05:34 +08:00
Thomas 34e5a16a26 Require login + api (#711)
Signed-off-by: Thomas Miceli <tho.miceli@gmail.com>
2026-06-08 03:28:49 +08:00
Thomas 31bc25e569 Beautiful docs website (#710)
Signed-off-by: Thomas Miceli <tho.miceli@gmail.com>
2026-06-08 03:15:24 +08:00
Thomas 3b8d947ad8 Fix SSH key generation (#708)
Go CI / Test (postgres, 1.25, postgres:16, ubuntu-latest, 5432:5432) (push) Has been cancelled
Go CI / Test (sqlite, 1.25, macOS-latest) (push) Has been cancelled
Go CI / Test (sqlite, 1.25, ubuntu-latest) (push) Has been cancelled
Go CI / Build (1.25, macOS-latest) (push) Has been cancelled
Go CI / Build (1.25, ubuntu-latest) (push) Has been cancelled
Go CI / Build (1.25, windows-latest) (push) Has been cancelled
Go CI / Lint (push) Has been cancelled
Go CI / Check (push) Has been cancelled
Go CI / Test (mysql, 1.25, mysql:8, ubuntu-latest, 3306:3306) (push) Has been cancelled
Signed-off-by: Thomas Miceli <tho.miceli@gmail.com>
2026-06-04 02:09:26 +08:00
Thomas 8e462397f4 API for gists and users (#707)
Go CI / Lint (push) Has been cancelled
Go CI / Check (push) Has been cancelled
Go CI / Test (mysql, 1.25, mysql:8, ubuntu-latest, 3306:3306) (push) Has been cancelled
Go CI / Test (postgres, 1.25, postgres:16, ubuntu-latest, 5432:5432) (push) Has been cancelled
Go CI / Test (sqlite, 1.25, macOS-latest) (push) Has been cancelled
Go CI / Test (sqlite, 1.25, ubuntu-latest) (push) Has been cancelled
Go CI / Build (1.25, macOS-latest) (push) Has been cancelled
Go CI / Build (1.25, ubuntu-latest) (push) Has been cancelled
Go CI / Build (1.25, windows-latest) (push) Has been cancelled
Signed-off-by: Thomas Miceli <tho.miceli@gmail.com>
2026-06-03 02:26:17 +08:00
KhaledMahfouz5 5c23d7feed Adding Arabic Translation (#706) 2026-06-03 02:21:29 +08:00
awkj 690f151592 feat: add REST API v1 with admin toggle and OpenAPI spec (#702)
Go CI / Lint (push) Has been cancelled
Go CI / Check (push) Has been cancelled
Go CI / Test (mysql, 1.25, mysql:8, ubuntu-latest, 3306:3306) (push) Has been cancelled
Go CI / Test (postgres, 1.25, postgres:16, ubuntu-latest, 5432:5432) (push) Has been cancelled
Go CI / Test (sqlite, 1.25, macOS-latest) (push) Has been cancelled
Go CI / Test (sqlite, 1.25, ubuntu-latest) (push) Has been cancelled
Go CI / Build (1.25, macOS-latest) (push) Has been cancelled
Go CI / Build (1.25, ubuntu-latest) (push) Has been cancelled
Go CI / Build (1.25, windows-latest) (push) Has been cancelled
Adds an HTTP REST API under /api/v1 with bearer-token auth, served
alongside the existing web UI. The API exposes seven endpoints:

  GET    /api/v1/user
  GET    /api/v1/gists
  POST   /api/v1/gists
  GET    /api/v1/gists/:uuid
  PATCH  /api/v1/gists/:uuid
  DELETE /api/v1/gists/:uuid
  GET    /api/v1/gists/:uuid/files/:filename/raw

Auth reuses the existing AccessToken model (extended with a new
ScopeUser permission for /user) and accepts both 'Authorization: Bearer
og_...' and the legacy 'Token og_...' header.

The API is gated by a new 'api-enabled' admin setting (default off) so
deployments must explicitly opt in from Admin Panel → Configuration.
When disabled, requests get a 503 with an actionable hint, and the
access-token settings page shows a banner pointing admins straight to
the toggle.

An embedded OpenAPI 3.1 spec is served at GET /api/v1/openapi.yaml;
import it into Postman / Insomnia / Bruno / openapi-generator for
interactive testing or client generation.

Other UX polish bundled in:
- Settings access-token form now exposes the new User scope and uses
  the existing locale.Tr lookups (translations backfilled for zh-CN).
- Language switcher distinguishes 简体中文 vs 繁體中文 instead of both
  appearing as '中文'.
- Settings page header tabs translated for zh-CN.
- POST /gists cleans up the on-disk repo when commit/create fails
  (previously orphaned).
- visibility=public list excludes private/unlisted gists (regression
  guard added).
- Content-Disposition filename in raw responses is RFC-escaped.

Docs: docs/usage/api.md walks through enabling the API, creating a
token, the seven endpoints with curl examples, error codes and v1
limitations.
2026-05-22 04:12:33 +08:00
Thomas Miceli 8da72b9545 Limit display if there is too much files in one gist (#701)
Go CI / Lint (push) Has been cancelled
Go CI / Check (push) Has been cancelled
Go CI / Test (mysql, 1.25, mysql:8, ubuntu-latest, 3306:3306) (push) Has been cancelled
Go CI / Test (postgres, 1.25, postgres:16, ubuntu-latest, 5432:5432) (push) Has been cancelled
Go CI / Test (sqlite, 1.25, macOS-latest) (push) Has been cancelled
Go CI / Test (sqlite, 1.25, ubuntu-latest) (push) Has been cancelled
Go CI / Build (1.25, macOS-latest) (push) Has been cancelled
Go CI / Build (1.25, ubuntu-latest) (push) Has been cancelled
Go CI / Build (1.25, windows-latest) (push) Has been cancelled
Signed-off-by: Thomas Miceli <tho.miceli@gmail.com>
2026-05-10 03:28:02 +08:00
Lukas Gierth f3c38ddbbb chore: bump helm chart version to 0.7.0 (#696)
Go CI / Lint (push) Has been cancelled
Go CI / Check (push) Has been cancelled
Go CI / Test (mysql, 1.25, mysql:8, ubuntu-latest, 3306:3306) (push) Has been cancelled
Go CI / Test (postgres, 1.25, postgres:16, ubuntu-latest, 5432:5432) (push) Has been cancelled
Go CI / Test (sqlite, 1.25, macOS-latest) (push) Has been cancelled
Go CI / Test (sqlite, 1.25, ubuntu-latest) (push) Has been cancelled
Go CI / Build (1.25, macOS-latest) (push) Has been cancelled
Go CI / Build (1.25, ubuntu-latest) (push) Has been cancelled
Go CI / Build (1.25, windows-latest) (push) Has been cancelled
Co-authored-by: Lukas Gierth <lukas.gierth@ta-systeme.com>
2026-04-30 22:47:18 +08:00
Thomas Miceli 8a6f2d82ff v1.12.2
Go CI / Lint (push) Has been cancelled
Go CI / Check (push) Has been cancelled
Go CI / Test (mysql, 1.25, mysql:8, ubuntu-latest, 3306:3306) (push) Has been cancelled
Go CI / Test (postgres, 1.25, postgres:16, ubuntu-latest, 5432:5432) (push) Has been cancelled
Go CI / Test (sqlite, 1.25, macOS-latest) (push) Has been cancelled
Go CI / Test (sqlite, 1.25, ubuntu-latest) (push) Has been cancelled
Go CI / Build (1.25, macOS-latest) (push) Has been cancelled
Go CI / Build (1.25, ubuntu-latest) (push) Has been cancelled
Go CI / Build (1.25, windows-latest) (push) Has been cancelled
Signed-off-by: Thomas Miceli <tho.miceli@gmail.com>
2026-04-28 16:37:03 +07:00
John71 bfd75a9d58 fix(Gitea): Field avatar_url not found in Gitea JSON response by using gothic provided user data and removing our API call (#674)
Go CI / Lint (push) Has been cancelled
Go CI / Check (push) Has been cancelled
Go CI / Test (mysql, 1.25, mysql:8, ubuntu-latest, 3306:3306) (push) Has been cancelled
Go CI / Test (postgres, 1.25, postgres:16, ubuntu-latest, 5432:5432) (push) Has been cancelled
Go CI / Test (sqlite, 1.25, macOS-latest) (push) Has been cancelled
Go CI / Test (sqlite, 1.25, ubuntu-latest) (push) Has been cancelled
Go CI / Build (1.25, macOS-latest) (push) Has been cancelled
Go CI / Build (1.25, ubuntu-latest) (push) Has been cancelled
Go CI / Build (1.25, windows-latest) (push) Has been cancelled
Co-authored-by: John71 <john71@noreply.git.john71.fr>
2026-04-26 19:45:26 +08:00
dependabot[bot] eef6029c95 Bump softprops/action-gh-release from 2 to 3 (#691)
Bumps [softprops/action-gh-release](https://github.com/softprops/action-gh-release) from 2 to 3.
- [Release notes](https://github.com/softprops/action-gh-release/releases)
- [Changelog](https://github.com/softprops/action-gh-release/blob/master/CHANGELOG.md)
- [Commits](https://github.com/softprops/action-gh-release/compare/v2...v3)

---
updated-dependencies:
- dependency-name: softprops/action-gh-release
  dependency-version: '3'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-26 19:32:59 +08:00
Thomas Miceli c60094c778 Update Go & JS deps / Remove Dependabot for those deps (#694)
Signed-off-by: Thomas Miceli <tho.miceli@gmail.com>
2026-04-26 19:28:17 +08:00
Johannes Kirchner f67bff59c3 feat: add environment variables and secrets to statefulset and deployment (#644)
* feat: add environment variables and secrets to statefulset

* feat: add env and envFromSecrets to deplyoment container
2026-04-26 18:52:21 +08:00
dependabot[bot] ec26888487 Bump github.com/labstack/echo-contrib from 0.17.4 to 0.18.0 (#667)
Go CI / Lint (push) Has been cancelled
Go CI / Check (push) Has been cancelled
Go CI / Test (mysql, 1.25, mysql:8, ubuntu-latest, 3306:3306) (push) Has been cancelled
Go CI / Test (postgres, 1.25, postgres:16, ubuntu-latest, 5432:5432) (push) Has been cancelled
Go CI / Test (sqlite, 1.25, macOS-latest) (push) Has been cancelled
Go CI / Test (sqlite, 1.25, ubuntu-latest) (push) Has been cancelled
Go CI / Build (1.25, macOS-latest) (push) Has been cancelled
Go CI / Build (1.25, ubuntu-latest) (push) Has been cancelled
Go CI / Build (1.25, windows-latest) (push) Has been cancelled
Bumps [github.com/labstack/echo-contrib](https://github.com/labstack/echo-contrib) from 0.17.4 to 0.18.0.
- [Release notes](https://github.com/labstack/echo-contrib/releases)
- [Commits](https://github.com/labstack/echo-contrib/compare/v0.17.4...v0.18.0)

---
updated-dependencies:
- dependency-name: github.com/labstack/echo-contrib
  dependency-version: 0.18.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-13 11:36:13 +08:00
dependabot[bot] 57d76151fd Bump golang.org/x/crypto from 0.48.0 to 0.49.0 (#669)
Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.48.0 to 0.49.0.
- [Commits](https://github.com/golang/crypto/compare/v0.48.0...v0.49.0)

---
updated-dependencies:
- dependency-name: golang.org/x/crypto
  dependency-version: 0.49.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-13 11:33:38 +08:00
dependabot[bot] c2ee390841 Bump github.com/go-webauthn/webauthn from 0.16.0 to 0.16.1 (#671)
Bumps [github.com/go-webauthn/webauthn](https://github.com/go-webauthn/webauthn) from 0.16.0 to 0.16.1.
- [Release notes](https://github.com/go-webauthn/webauthn/releases)
- [Commits](https://github.com/go-webauthn/webauthn/compare/v0.16.0...v0.16.1)

---
updated-dependencies:
- dependency-name: github.com/go-webauthn/webauthn
  dependency-version: 0.16.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-13 11:33:26 +08:00
dependabot[bot] d6fc346e70 Bump @codemirror/view from 6.39.16 to 6.40.0 (#670)
Bumps [@codemirror/view](https://github.com/codemirror/view) from 6.39.16 to 6.40.0.
- [Changelog](https://github.com/codemirror/view/blob/main/CHANGELOG.md)
- [Commits](https://github.com/codemirror/view/compare/6.39.16...6.40.0)

---
updated-dependencies:
- dependency-name: "@codemirror/view"
  dependency-version: 6.40.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-13 11:33:09 +08:00
dependabot[bot] 4e977077ba Bump nodemon from 3.1.11 to 3.1.14 (#672)
Bumps [nodemon](https://github.com/remy/nodemon) from 3.1.11 to 3.1.14.
- [Release notes](https://github.com/remy/nodemon/releases)
- [Commits](https://github.com/remy/nodemon/compare/v3.1.11...v3.1.14)

---
updated-dependencies:
- dependency-name: nodemon
  dependency-version: 3.1.14
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-13 11:32:51 +08:00
dependabot[bot] f865b2b099 Bump @codemirror/commands from 6.10.1 to 6.10.3 (#664)
Bumps [@codemirror/commands](https://github.com/codemirror/commands) from 6.10.1 to 6.10.3.
- [Changelog](https://github.com/codemirror/commands/blob/main/CHANGELOG.md)
- [Commits](https://github.com/codemirror/commands/compare/6.10.1...6.10.3)

---
updated-dependencies:
- dependency-name: "@codemirror/commands"
  dependency-version: 6.10.3
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-13 11:32:24 +08:00
Thomas Miceli d26221de54 Translated using Weblate (Ukrainian) (#659)
Currently translated at 69.4% (243 of 350 strings)

Translation: Opengist/Opengist
Translate-URL: http://tr.opengist.io/projects/_/opengist/uk/

Co-authored-by: Anonymous <noreply@weblate.org>
2026-03-13 11:17:23 +08:00
Thomas Miceli e91139d3ec Improve code search + tests (#663)
Signed-off-by: Thomas Miceli <tho.miceli@gmail.com>
Co-authored-by: Qiang Zhou <zhouqiang.loaded@bytedance.com>
Co-authored-by: theodoruszq <theodoruszq@gmail.com>
2026-03-13 11:16:10 +08:00
Webysther Sperandio 279da52899 feat: search all fields (#622)
Go CI / Lint (push) Has been cancelled
Go CI / Check (push) Has been cancelled
Go CI / Test (mysql, 1.25, mysql:8, ubuntu-latest, 3306:3306) (push) Has been cancelled
Go CI / Test (postgres, 1.25, postgres:16, ubuntu-latest, 5432:5432) (push) Has been cancelled
Go CI / Test (sqlite, 1.25, macOS-latest) (push) Has been cancelled
Go CI / Test (sqlite, 1.25, ubuntu-latest) (push) Has been cancelled
Go CI / Build (1.25, macOS-latest) (push) Has been cancelled
Go CI / Build (1.25, ubuntu-latest) (push) Has been cancelled
Go CI / Build (1.25, windows-latest) (push) Has been cancelled
*  feat(search): search all feature

- add Description field to Gist struct and index it
- extend SearchGistMetadata with Description and Content
- update Bleve and Meilisearch to index and search Description
- modify ParseSearchQueryStr to parse description: and content: keywords
- update templates and i18n for new search options

* Fix test

* Set content by default

Signed-off-by: Thomas Miceli <tho.miceli@gmail.com>

* Config to define default searchable fields

Signed-off-by: Thomas Miceli <tho.miceli@gmail.com>

---------

Signed-off-by: Thomas Miceli <tho.miceli@gmail.com>
Co-authored-by: Thomas Miceli <tho.miceli@gmail.com>
2026-03-12 00:55:23 +08:00
dependabot[bot] 5ad01a3304 Bump @codemirror/view from 6.39.11 to 6.39.16 (#653)
Bumps [@codemirror/view](https://github.com/codemirror/view) from 6.39.11 to 6.39.16.
- [Changelog](https://github.com/codemirror/view/blob/main/CHANGELOG.md)
- [Commits](https://github.com/codemirror/view/compare/6.39.11...6.39.16)

---
updated-dependencies:
- dependency-name: "@codemirror/view"
  dependency-version: 6.39.16
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-12 00:52:53 +08:00
dependabot[bot] 1944502d14 Bump marked from 17.0.3 to 17.0.4 (#652)
Bumps [marked](https://github.com/markedjs/marked) from 17.0.3 to 17.0.4.
- [Release notes](https://github.com/markedjs/marked/releases)
- [Commits](https://github.com/markedjs/marked/compare/v17.0.3...v17.0.4)

---
updated-dependencies:
- dependency-name: marked
  dependency-version: 17.0.4
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-12 00:52:39 +08:00
dependabot[bot] 2d7261ac83 Bump docker/setup-qemu-action from 3 to 4 (#654)
Bumps [docker/setup-qemu-action](https://github.com/docker/setup-qemu-action) from 3 to 4.
- [Release notes](https://github.com/docker/setup-qemu-action/releases)
- [Commits](https://github.com/docker/setup-qemu-action/compare/v3...v4)

---
updated-dependencies:
- dependency-name: docker/setup-qemu-action
  dependency-version: '4'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-12 00:52:12 +08:00
dependabot[bot] 50f2980c10 Bump docker/login-action from 3 to 4 (#655)
Bumps [docker/login-action](https://github.com/docker/login-action) from 3 to 4.
- [Release notes](https://github.com/docker/login-action/releases)
- [Commits](https://github.com/docker/login-action/compare/v3...v4)

---
updated-dependencies:
- dependency-name: docker/login-action
  dependency-version: '4'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-12 00:52:03 +08:00
dependabot[bot] 2e68b6893b Bump docker/build-push-action from 6 to 7 (#656)
Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 6 to 7.
- [Release notes](https://github.com/docker/build-push-action/releases)
- [Commits](https://github.com/docker/build-push-action/compare/v6...v7)

---
updated-dependencies:
- dependency-name: docker/build-push-action
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-12 00:51:52 +08:00
dependabot[bot] 9b68f08c62 Bump docker/metadata-action from 5 to 6 (#657)
Bumps [docker/metadata-action](https://github.com/docker/metadata-action) from 5 to 6.
- [Release notes](https://github.com/docker/metadata-action/releases)
- [Commits](https://github.com/docker/metadata-action/compare/v5...v6)

---
updated-dependencies:
- dependency-name: docker/metadata-action
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-12 00:51:21 +08:00
dependabot[bot] dfabdb403a Bump docker/setup-buildx-action from 3 to 4 (#658)
Bumps [docker/setup-buildx-action](https://github.com/docker/setup-buildx-action) from 3 to 4.
- [Release notes](https://github.com/docker/setup-buildx-action/releases)
- [Commits](https://github.com/docker/setup-buildx-action/compare/v3...v4)

---
updated-dependencies:
- dependency-name: docker/setup-buildx-action
  dependency-version: '4'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-12 00:51:09 +08:00
Thomas Miceli 4da067ab60 Rebuild search index in admin options (#661)
Signed-off-by: Thomas Miceli <tho.miceli@gmail.com>
2026-03-12 00:50:44 +08:00
dependabot[bot] a8339ff6bd Bump github-markdown-css from 5.8.1 to 5.9.0 (#651) 2026-03-11 23:07:34 +07:00
dependabot[bot] 7a5cdd1565 Bump @codemirror/lang-javascript from 6.2.4 to 6.2.5 (#650) 2026-03-11 23:07:02 +07:00
dependabot[bot] 00dcb53e3a Bump katex from 0.16.33 to 0.16.38 (#649) 2026-03-11 23:06:40 +07:00
Thomas Miceli f8b3bbce6a Rebuild search index in admin options (#647)
Go CI / Lint (push) Has been cancelled
Go CI / Check (push) Has been cancelled
Go CI / Test (mysql, 1.25, mysql:8, ubuntu-latest, 3306:3306) (push) Has been cancelled
Go CI / Test (postgres, 1.25, postgres:16, ubuntu-latest, 5432:5432) (push) Has been cancelled
Go CI / Test (sqlite, 1.25, macOS-latest) (push) Has been cancelled
Go CI / Test (sqlite, 1.25, ubuntu-latest) (push) Has been cancelled
Go CI / Build (1.25, macOS-latest) (push) Has been cancelled
Go CI / Build (1.25, ubuntu-latest) (push) Has been cancelled
Go CI / Build (1.25, windows-latest) (push) Has been cancelled
Signed-off-by: Thomas Miceli <tho.miceli@gmail.com>
2026-03-09 07:30:28 +08:00
Johannes Kirchner a697b0f273 fix: port template string and updateStrategy indentation (#643) 2026-03-09 05:50:07 +08:00
Thomas Miceli 33cbfb0904 Bump meili version (#646)
Signed-off-by: Thomas Miceli <tho.miceli@gmail.com>
2026-03-09 05:40:16 +08:00
dependabot[bot] dfea4eb435 Bump github.com/meilisearch/meilisearch-go from 0.36.0 to 0.36.1 (#634)
Go CI / Lint (push) Has been cancelled
Go CI / Check (push) Has been cancelled
Go CI / Test (mysql, 1.25, mysql:8, ubuntu-latest, 3306:3306) (push) Has been cancelled
Go CI / Test (postgres, 1.25, postgres:16, ubuntu-latest, 5432:5432) (push) Has been cancelled
Go CI / Test (sqlite, 1.25, macOS-latest) (push) Has been cancelled
Go CI / Test (sqlite, 1.25, ubuntu-latest) (push) Has been cancelled
Go CI / Build (1.25, macOS-latest) (push) Has been cancelled
Go CI / Build (1.25, ubuntu-latest) (push) Has been cancelled
Go CI / Build (1.25, windows-latest) (push) Has been cancelled
Bumps [github.com/meilisearch/meilisearch-go](https://github.com/meilisearch/meilisearch-go) from 0.36.0 to 0.36.1.
- [Release notes](https://github.com/meilisearch/meilisearch-go/releases)
- [Commits](https://github.com/meilisearch/meilisearch-go/compare/v0.36.0...v0.36.1)

---
updated-dependencies:
- dependency-name: github.com/meilisearch/meilisearch-go
  dependency-version: 0.36.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-03 16:25:22 +08:00
Thomas Miceli d796eeba98 Make gists username/urls case insensitive in URLS (#641)
Signed-off-by: Thomas Miceli <tho.miceli@gmail.com>
2026-03-03 15:28:49 +08:00
dependabot[bot] 4ab38f24c8 Bump marked from 17.0.1 to 17.0.3 (#635)
Bumps [marked](https://github.com/markedjs/marked) from 17.0.1 to 17.0.3.
- [Release notes](https://github.com/markedjs/marked/releases)
- [Commits](https://github.com/markedjs/marked/compare/v17.0.1...v17.0.3)

---
updated-dependencies:
- dependency-name: marked
  dependency-version: 17.0.3
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-03 15:25:30 +08:00
dependabot[bot] e1d1b01d40 Bump github.com/go-webauthn/webauthn from 0.15.0 to 0.16.0 (#636)
Bumps [github.com/go-webauthn/webauthn](https://github.com/go-webauthn/webauthn) from 0.15.0 to 0.16.0.
- [Release notes](https://github.com/go-webauthn/webauthn/releases)
- [Commits](https://github.com/go-webauthn/webauthn/compare/v0.15.0...v0.16.0)

---
updated-dependencies:
- dependency-name: github.com/go-webauthn/webauthn
  dependency-version: 0.16.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-03 15:25:00 +08:00
dependabot[bot] 3c967729cc Bump github.com/gabriel-vasile/mimetype from 1.4.12 to 1.4.13 (#637)
Bumps [github.com/gabriel-vasile/mimetype](https://github.com/gabriel-vasile/mimetype) from 1.4.12 to 1.4.13.
- [Release notes](https://github.com/gabriel-vasile/mimetype/releases)
- [Commits](https://github.com/gabriel-vasile/mimetype/compare/v1.4.12...v1.4.13)

---
updated-dependencies:
- dependency-name: github.com/gabriel-vasile/mimetype
  dependency-version: 1.4.13
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-03 15:20:40 +08:00
dependabot[bot] 36bc576893 Bump @codemirror/language from 6.12.1 to 6.12.2 (#638)
Bumps [@codemirror/language](https://github.com/codemirror/language) from 6.12.1 to 6.12.2.
- [Changelog](https://github.com/codemirror/language/blob/main/CHANGELOG.md)
- [Commits](https://github.com/codemirror/language/compare/6.12.1...6.12.2)

---
updated-dependencies:
- dependency-name: "@codemirror/language"
  dependency-version: 6.12.2
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-03 15:20:28 +08:00
dependabot[bot] c074d60d1d Bump @tailwindcss/vite from 4.1.18 to 4.2.1 (#640)
Bumps [@tailwindcss/vite](https://github.com/tailwindlabs/tailwindcss/tree/HEAD/packages/@tailwindcss-vite) from 4.1.18 to 4.2.1.
- [Release notes](https://github.com/tailwindlabs/tailwindcss/releases)
- [Changelog](https://github.com/tailwindlabs/tailwindcss/blob/main/CHANGELOG.md)
- [Commits](https://github.com/tailwindlabs/tailwindcss/commits/v4.2.1/packages/@tailwindcss-vite)

---
updated-dependencies:
- dependency-name: "@tailwindcss/vite"
  dependency-version: 4.2.1
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-03 15:20:16 +08:00
dependabot[bot] 840a852ed2 Bump tailwindcss from 4.1.18 to 4.2.1 (#639)
Bumps [tailwindcss](https://github.com/tailwindlabs/tailwindcss/tree/HEAD/packages/tailwindcss) from 4.1.18 to 4.2.1.
- [Release notes](https://github.com/tailwindlabs/tailwindcss/releases)
- [Changelog](https://github.com/tailwindlabs/tailwindcss/blob/main/CHANGELOG.md)
- [Commits](https://github.com/tailwindlabs/tailwindcss/commits/v4.2.1/packages/tailwindcss)

---
updated-dependencies:
- dependency-name: tailwindcss
  dependency-version: 4.2.1
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-03 15:20:04 +08:00
dependabot[bot] 34c0b0b3e2 Bump katex from 0.16.28 to 0.16.33 (#633)
Bumps [katex](https://github.com/KaTeX/KaTeX) from 0.16.28 to 0.16.33.
- [Release notes](https://github.com/KaTeX/KaTeX/releases)
- [Changelog](https://github.com/KaTeX/KaTeX/blob/main/CHANGELOG.md)
- [Commits](https://github.com/KaTeX/KaTeX/compare/v0.16.28...v0.16.33)

---
updated-dependencies:
- dependency-name: katex
  dependency-version: 0.16.33
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-03 15:19:23 +08:00
dependabot[bot] 093a4cb4a8 Bump github.com/labstack/echo/v4 from 4.15.0 to 4.15.1 (#632)
Bumps [github.com/labstack/echo/v4](https://github.com/labstack/echo) from 4.15.0 to 4.15.1.
- [Release notes](https://github.com/labstack/echo/releases)
- [Changelog](https://github.com/labstack/echo/blob/master/CHANGELOG.md)
- [Commits](https://github.com/labstack/echo/compare/v4.15.0...v4.15.1)

---
updated-dependencies:
- dependency-name: github.com/labstack/echo/v4
  dependency-version: 4.15.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-03 15:18:29 +08:00
dependabot[bot] f037206f41 Bump golang.org/x/text from 0.33.0 to 0.34.0 (#631)
Bumps [golang.org/x/text](https://github.com/golang/text) from 0.33.0 to 0.34.0.
- [Release notes](https://github.com/golang/text/releases)
- [Commits](https://github.com/golang/text/compare/v0.33.0...v0.34.0)

---
updated-dependencies:
- dependency-name: golang.org/x/text
  dependency-version: 0.34.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-03 15:17:37 +08:00
Thomas Miceli 6c22adba4e Fix async-loaded gist embed scripts (#630)
Go CI / Lint (push) Has been cancelled
Go CI / Check (push) Has been cancelled
Go CI / Test (mysql, 1.25, mysql:8, ubuntu-latest, 3306:3306) (push) Has been cancelled
Go CI / Test (postgres, 1.25, postgres:16, ubuntu-latest, 5432:5432) (push) Has been cancelled
Go CI / Test (sqlite, 1.25, macOS-latest) (push) Has been cancelled
Go CI / Test (sqlite, 1.25, ubuntu-latest) (push) Has been cancelled
Go CI / Build (1.25, macOS-latest) (push) Has been cancelled
Go CI / Build (1.25, ubuntu-latest) (push) Has been cancelled
Go CI / Build (1.25, windows-latest) (push) Has been cancelled
Signed-off-by: Thomas Miceli <tho.miceli@gmail.com>
2026-03-03 00:10:28 +08:00
Thomas Miceli bb63ecd048 Remove windows tests in CI for now (#629)
Go CI / Lint (push) Has been cancelled
Go CI / Check (push) Has been cancelled
Go CI / Test (mysql, 1.25, mysql:8, ubuntu-latest, 3306:3306) (push) Has been cancelled
Go CI / Test (postgres, 1.25, postgres:16, ubuntu-latest, 5432:5432) (push) Has been cancelled
Go CI / Test (sqlite, 1.25, macOS-latest) (push) Has been cancelled
Go CI / Test (sqlite, 1.25, ubuntu-latest) (push) Has been cancelled
Go CI / Build (1.25, macOS-latest) (push) Has been cancelled
Go CI / Build (1.25, ubuntu-latest) (push) Has been cancelled
Go CI / Build (1.25, windows-latest) (push) Has been cancelled
2026-03-02 16:59:43 +08:00
Thomas Miceli 6a61b720ab Improve test suite (#628)
Signed-off-by: Thomas Miceli <tho.miceli@gmail.com>
2026-03-02 15:43:24 +08:00
Thomas Miceli 829cd68879 CSRF skipper only for GET *.js request (#627)
Signed-off-by: Thomas Miceli <tho.miceli@gmail.com>
2026-03-02 15:05:45 +08:00
Thomas Miceli 42490f2995 fix uuid
Go CI / Lint (push) Has been cancelled
Go CI / Check (push) Has been cancelled
Go CI / Test (mysql, 1.25, mysql:8, ubuntu-latest, 3306:3306) (push) Has been cancelled
Go CI / Test (postgres, 1.25, postgres:16, ubuntu-latest, 5432:5432) (push) Has been cancelled
Go CI / Test (sqlite, 1.25, macOS-latest) (push) Has been cancelled
Go CI / Test (sqlite, 1.25, ubuntu-latest) (push) Has been cancelled
Go CI / Test (sqlite, 1.25, windows-latest) (push) Has been cancelled
Go CI / Build (1.25, macOS-latest) (push) Has been cancelled
Go CI / Build (1.25, ubuntu-latest) (push) Has been cancelled
Go CI / Build (1.25, windows-latest) (push) Has been cancelled
Signed-off-by: Thomas Miceli <tho.miceli@gmail.com>
2026-02-26 06:42:00 +07:00
awkj f83018ebf2 support UTF-8, show no English Text (#625)
Go CI / Lint (push) Has been cancelled
Go CI / Check (push) Has been cancelled
Go CI / Test (mysql, 1.25, mysql:8, ubuntu-latest, 3306:3306) (push) Has been cancelled
Go CI / Test (postgres, 1.25, postgres:16, ubuntu-latest, 5432:5432) (push) Has been cancelled
Go CI / Test (sqlite, 1.25, macOS-latest) (push) Has been cancelled
Go CI / Test (sqlite, 1.25, ubuntu-latest) (push) Has been cancelled
Go CI / Test (sqlite, 1.25, windows-latest) (push) Has been cancelled
Go CI / Build (1.25, macOS-latest) (push) Has been cancelled
Go CI / Build (1.25, ubuntu-latest) (push) Has been cancelled
Go CI / Build (1.25, windows-latest) (push) Has been cancelled
2026-02-26 02:13:09 +08:00
Thomas Miceli b097cfcbc0 Clean file path names on file creation (#624) 2026-02-25 23:30:26 +08:00
Thomas Miceli 7b1048ec30 Display a form to create an Opengist account coming from a OAuth provider (#623)
Go CI / Lint (push) Has been cancelled
Go CI / Check (push) Has been cancelled
Go CI / Test (mysql, 1.25, mysql:8, ubuntu-latest, 3306:3306) (push) Has been cancelled
Go CI / Test (postgres, 1.25, postgres:16, ubuntu-latest, 5432:5432) (push) Has been cancelled
Go CI / Test (sqlite, 1.25, macOS-latest) (push) Has been cancelled
Go CI / Test (sqlite, 1.25, ubuntu-latest) (push) Has been cancelled
Go CI / Test (sqlite, 1.25, windows-latest) (push) Has been cancelled
Go CI / Build (1.25, macOS-latest) (push) Has been cancelled
Go CI / Build (1.25, ubuntu-latest) (push) Has been cancelled
Go CI / Build (1.25, windows-latest) (push) Has been cancelled
2026-02-08 16:32:24 +08:00
Joel Godfrey ce39df1030 Update cheat-sheet.md with missing OIDC group configs (#616)
oidc.group-claim-name and oidc.admin-group are missing from the cheat-sheet
2026-02-05 02:23:59 +08:00
Thomas Miceli 07ba04244b Update CI helm 2026-02-03 16:12:07 +07:00
199 changed files with 19072 additions and 5456 deletions
-8
View File
@@ -1,17 +1,9 @@
version: 2
updates:
- package-ecosystem: "npm"
directory: "/"
schedule:
interval: "weekly"
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
- package-ecosystem: "gomod"
directory: "/"
schedule:
interval: "weekly"
- package-ecosystem: "docker"
directory: "/"
schedule:
+6 -8
View File
@@ -16,17 +16,15 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: '20'
node-version: '26'
- name: Install JS dependencies
run: |
npm install vitepress@1.3.4 tailwindcss@3.4.10
- name: Install dependencies
run: npm install
working-directory: docs
- name: Build docs
run: |
cd docs
npx tailwindcss -i .vitepress/theme/style.css -o .vitepress/theme/theme.css -c .vitepress/tailwind.config.js
npm run docs:build
run: npm run build
working-directory: docs
- name: Push to docs repository
run: |
+30 -10
View File
@@ -9,6 +9,7 @@ on:
pull_request:
paths-ignore:
- '**.yml'
- '**.yaml'
- '**.md'
jobs:
@@ -19,15 +20,15 @@ jobs:
- name: Checkout
uses: actions/checkout@v6
- name: Set up Go 1.25
- name: Set up Go 1.26
uses: actions/setup-go@v6
with:
go-version: "1.25"
go-version: "1.26"
- name: Lint
uses: golangci/golangci-lint-action@v9
with:
version: v2.5
version: v2.12
args: --timeout=20m --disable=errcheck
- name: Format
@@ -40,10 +41,10 @@ jobs:
- name: Checkout
uses: actions/checkout@v6
- name: Set up Go 1.25
- name: Set up Go 1.26
uses: actions/setup-go@v6
with:
go-version: "1.25"
go-version: "1.26"
- name: Check Go modules
run: make go_mod check_changes
@@ -57,7 +58,7 @@ jobs:
fail-fast: false
matrix:
os: ["ubuntu-latest"]
go: ["1.25"]
go: ["1.26"]
database: [postgres, mysql]
include:
- database: postgres
@@ -83,6 +84,18 @@ jobs:
--health-interval 10s
--health-timeout 5s
--health-retries 5
meilisearch:
image: getmeili/meilisearch:latest
ports:
- 47700:7700
env:
MEILI_NO_ANALYTICS: true
MEILI_ENV: development
options: >-
--health-cmd "curl -sf http://localhost:7700/health"
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- name: Checkout
uses: actions/checkout@v6
@@ -94,14 +107,16 @@ jobs:
- name: Run tests
run: make test TEST_DB_TYPE=${{ matrix.database }}
env:
OG_TEST_MEILI_HOST: http://localhost:47700
test:
name: Test
strategy:
fail-fast: false
matrix:
os: ["ubuntu-latest", "macOS-latest", "windows-latest"]
go: ["1.25"]
os: ["ubuntu-latest", "macOS-latest"]
go: ["1.26"]
database: ["sqlite"]
runs-on: ${{ matrix.os }}
steps:
@@ -122,17 +137,22 @@ jobs:
fail-fast: false
matrix:
os: ["ubuntu-latest", "macOS-latest", "windows-latest"]
go: ["1.25"]
go: ["1.26"]
runs-on: ${{ matrix.os }}
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Set up Go 1.25
- name: Set up Go 1.26
uses: actions/setup-go@v6
with:
go-version: ${{ matrix.go }}
- name: Set up Node.js 26
uses: actions/setup-node@v6
with:
node-version: '26'
- name: Build
shell: bash
run: make
+6 -5
View File
@@ -2,6 +2,7 @@ name: Build / Deploy Helm Chart
on:
workflow_dispatch:
workflow_call:
jobs:
build-and-deploy:
@@ -11,7 +12,7 @@ jobs:
uses: actions/checkout@v6
- name: Set up Helm
uses: azure/setup-helm@v4.3.1
uses: azure/setup-helm@v5.0.0
with:
version: 'latest'
@@ -36,14 +37,14 @@ jobs:
- name: Push to docs repository
run: |
git clone https://${{ secrets.DOCS_REPO_TOKEN }}@github.com/${{ secrets.DOCS_REPO }}.git target-repo
git clone https://${{ secrets.STATIC_REPO_TOKEN }}@github.com/${{ secrets.STATIC_REPO }}.git target-repo
mkdir -p target-repo/helm
cp helm/*.tgz target-repo/helm/
cp helm/index.yaml target-repo/helm/
cp helm/*.tgz target-repo/srv/helm/
cp helm/index.yaml target-repo/srv/helm/
cd target-repo
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add .
git commit -m "Deploy helm chart from ${{ github.repository }}@${{ github.sha }}" || echo "No changes to commit"
git pull --rebase
git push
git push
+20 -10
View File
@@ -13,16 +13,21 @@ jobs:
- name: Checkout
uses: actions/checkout@v6
- name: Set up Go 1.25
- name: Set up Go 1.26
uses: actions/setup-go@v6
with:
go-version: "1.25"
go-version: "1.26"
- name: Set up Node.js 26
uses: actions/setup-node@v6
with:
node-version: '26'
- name: Cross compile build
run: make all_crosscompile
- name: Upload Release Assets
uses: softprops/action-gh-release@v2
uses: softprops/action-gh-release@v3
with:
files: |
build/*.tar.gz
@@ -42,7 +47,7 @@ jobs:
- name: Docker meta
id: meta
uses: docker/metadata-action@v5
uses: docker/metadata-action@v6
with:
images: |
ghcr.io/thomiceli/opengist
@@ -54,26 +59,26 @@ jobs:
type=semver,pattern={{version}}
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
uses: docker/setup-qemu-action@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
uses: docker/setup-buildx-action@v4
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
uses: docker/login-action@v4
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Login to Docker Hub
uses: docker/login-action@v3
uses: docker/login-action@v4
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v6
uses: docker/build-push-action@v7
with:
context: .
platforms: linux/amd64,linux/arm64
@@ -81,4 +86,9 @@ jobs:
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
cache-to: type=gha,mode=max
helm-build-release:
needs: docker-build-release
uses: ./.github/workflows/helm.yml
secrets: inherit
+47
View File
@@ -1,5 +1,52 @@
# Changelog
## [1.13.1](https://github.com/thomiceli/opengist/compare/v1.13.0...v1.13.1) - 2026-06-10
See here how to [update](https://opengist.io/docs/update) Opengist.
### Fixed
- Embedding fix vertical scrolling and improve padding (#714)
- Fix CSS url for json embed url (#715)
## [1.13.0](https://github.com/thomiceli/opengist/compare/v1.12.2...v1.13.0) - 2026-06-09
See here how to [update](https://opengist.io/docs/update) Opengist.
### Added
- REST API (#707) (#711) (#702)
- Limit display if there is too much files in one gist (#701)
- Topics git push option in post-receive hook (#698)
- Allow embedding Gists for a certain file only (#709)
- Arabic Translation (#706)
### Fixed
- Server SSH key generation (#708)
### Other
- Update deps Golang, JS, Docker deps (#713)
- New docs website (#710)
## [1.12.2](https://github.com/thomiceli/opengist/compare/v1.12.1...v1.12.2) - 2026-03-14
See here how to [update](https://opengist.io/docs/update) Opengist.
### Added
- Search all fields (#622)
- Display a form to create an Opengist account coming from a OAuth provider (#623)
- Rebuild search index in admin options (#647)
### Fixed
- Clean file path names on file creation (#624)
- Support UTF-8 on gist download (#625)
- CSRF skipper only for GET *.js request (#627)
- Async-loaded gist embed scripts (#630)
- Make gists username/urls case insensitive in URLS (#641)
- Improve code search and index tests (#663)
- Translation strings (#659)
- Gitea avatar URL on OAuth (#674)
### [Helm Chart](helm/opengist)
- Add environment variables and secrets to statefulset (#644)
> Admins of Opengist instances may want to run "Rebuild search index" in the admin panel.
## [1.12.1](https://github.com/thomiceli/opengist/compare/v1.12.0...v1.12.1) - 2026-02-03
See here how to [update](https://opengist.io/docs/update) Opengist.
+4 -5
View File
@@ -1,4 +1,4 @@
FROM alpine:3.22 AS base
FROM alpine:3.23 AS base
RUN apk update && \
apk add --no-cache \
@@ -8,11 +8,11 @@ RUN apk update && \
musl-dev \
libstdc++
COPY --from=golang:1.25.6-alpine3.22 /usr/local/go/ /usr/local/go/
COPY --from=golang:1.26.4-alpine3.23 /usr/local/go/ /usr/local/go/
ENV PATH="/usr/local/go/bin:${PATH}"
ENV CGO_ENABLED=0
COPY --from=node:24.13.0-alpine3.22 /usr/local/ /usr/local/
COPY --from=node:26.3.0-alpine3.23 /usr/local/ /usr/local/
ENV NODE_PATH="/usr/local/lib/node_modules"
ENV PATH="/usr/local/bin:${PATH}"
@@ -46,12 +46,11 @@ FROM base AS build
RUN make
FROM alpine:3.22 AS prod
FROM alpine:3.23 AS prod
RUN apk update && \
apk add --no-cache \
shadow \
openssh-server \
curl \
git
+10 -2
View File
@@ -1,4 +1,4 @@
.PHONY: all all_crosscompile install build_frontend build_backend build build_crosscompile build_docker build_dev_docker run_dev_docker watch_frontend watch_backend watch clean clean_docker check_changes go_mod fmt test check-tr
.PHONY: all all_crosscompile install build_frontend build_backend build build_crosscompile build_docker build_dev_docker run_dev_docker watch_frontend watch_backend watch clean clean_docker check_changes go_mod fmt test check-tr update_js_deps update_go_deps
# Specify the name of your Go binary output
BINARY_NAME := opengist
@@ -75,4 +75,12 @@ test:
@OPENGIST_TEST_DB=$(TEST_DB_TYPE) go test ./... -p 1
check-tr:
@bash ./scripts/check-translations.sh
@bash ./scripts/check-translations.sh
update_js_deps:
@echo "Updating NPM dependencies..."
@npx npm-check-updates -u && npm install
update_go_deps:
@echo "Updating Go dependencies..."
@go get -u ./... && go mod tidy
+4 -4
View File
@@ -38,7 +38,7 @@ It is similar to [GitHub Gist](https://gist.github.com/), but open-source and co
Docker [images](https://github.com/thomiceli/opengist/pkgs/container/opengist) are available for each release :
```shell
docker pull ghcr.io/thomiceli/opengist:1.12
docker pull ghcr.io/thomiceli/opengist:1.13
```
It can be used in a `docker-compose.yml` file :
@@ -50,7 +50,7 @@ It can be used in a `docker-compose.yml` file :
```yml
services:
opengist:
image: ghcr.io/thomiceli/opengist:1.12
image: ghcr.io/thomiceli/opengist:1.13
container_name: opengist
restart: unless-stopped
ports:
@@ -77,9 +77,9 @@ Download the archive for your system from the release page [here](https://github
```shell
# example for linux amd64
wget https://github.com/thomiceli/opengist/releases/download/v1.12.1/opengist1.12.1-linux-amd64.tar.gz
wget https://github.com/thomiceli/opengist/releases/download/v1.13.1/opengist1.13.1-linux-amd64.tar.gz
tar xzvf opengist1.12.1-linux-amd64.tar.gz
tar xzvf opengist1.13.1-linux-amd64.tar.gz
cd opengist
chmod +x opengist
./opengist # with or without `--config config.yml`
+7 -3
View File
@@ -32,6 +32,10 @@ index.meili.host:
# Set the API key for the Meiliseach server
index.meili.api-key:
# Set the default search fields. Can contain multiple fields (e.g., `content,username`).
# Fields: content,user,title,description,filename,extension,language,topic. Default: content
search.default: content
# Default branch name used by Opengist when initializing Git repositories.
# If not set, uses the Git default branch name. See https://git-scm.com/book/en/v2/Getting-Started-First-Time-Git-Setup#_new_default_branch
git.default-branch:
@@ -52,6 +56,9 @@ http.port: 6157
# Enable or disable git operations (clone, pull, push) via HTTP (either `true` or `false`). Default: true
http.git-enabled: true
# Enable or disable the REST API (either `true` or `false`). Default: true
api.enabled: true
# File permissions for Unix socket (octal format). Default: 0666
unix-socket-permissions: 0666
@@ -84,9 +91,6 @@ ssh.port: 2222
# If not set, uses the URL from the request
ssh.external-domain:
# Path or alias to ssh-keygen executable. Default: ssh-keygen
ssh.keygen-executable: ssh-keygen
# OAuth2 configuration
# The callback/redirect URL must be http://opengist.url/oauth/<github|gitlab|gitea|openid-connect>/callback
+14 -5
View File
@@ -1,5 +1,18 @@
#!/bin/sh
load_secrets() {
if [ -f "/run/secrets/opengist_secrets" ]; then
set -a
. /run/secrets/opengist_secrets
set +a
fi
}
if [ "$(id -u)" -ne 0 ]; then
load_secrets
exec env HOME=/opengist OG_OPENGIST_HOME=/opengist /app/opengist/opengist --config /config.yml
fi
export USER=opengist
UID=${UID:-1000}
GID=${GID:-1000}
@@ -9,10 +22,6 @@ usermod -o -u "$UID" $USER
chown -R "$USER:$USER" /opengist
chown -R "$USER:$USER" /config.yml
if [ -f "/run/secrets/opengist_secrets" ]; then
set -a
. /run/secrets/opengist_secrets
set +a
fi
load_secrets
exec su $USER -c "OG_OPENGIST_HOME=/opengist /app/opengist/opengist --config /config.yml"
+3
View File
@@ -0,0 +1,3 @@
node_modules
.vitepress/dist
.vitepress/cache
+228 -66
View File
@@ -1,12 +1,176 @@
import {defineConfig} from 'vitepress'
import tailwindcss from '@tailwindcss/vite'
import {listOperations, listSchemas, readSpecRaw} from './openapi'
// Serve the raw OpenAPI spec verbatim at /docs/api/openapi.yaml — read from the
// Go source tree so it never drifts. Dev via middleware, build via asset emit.
let isSsrBuild = false
const openapiRawPlugin = {
name: 'opengist:openapi-raw',
configResolved(c: any) {
isSsrBuild = !!c.build?.ssr
},
configureServer(server: any) {
server.middlewares.use((req: any, res: any, next: any) => {
if (req.url === '/docs/api/openapi.yaml') {
res.setHeader('Content-Type', 'text/yaml; charset=utf-8')
res.end(readSpecRaw())
} else next()
})
},
generateBundle(this: any) {
if (isSsrBuild) return
this.emitFile({type: 'asset', fileName: 'docs/api/openapi.yaml', source: readSpecRaw()})
}
}
// Build the API Reference sidebar from the OpenAPI spec: one entry per
// operation (grouped by tag), then one entry per schema, plus the Overview.
const apiOps = listOperations()
const apiTags = [...new Set(apiOps.map(op => op.tag))]
const apiSidebar = [
{text: 'Overview', link: '/docs/api'},
...apiTags.map(tag => ({
text: tag,
collapsed: false,
items: apiOps
.filter(op => op.tag === tag)
.map(op => ({text: op.summary, link: `/docs/api/${op.id}`})),
})),
{
text: 'Schemas',
collapsed: true,
items: listSchemas().map(name => ({text: name, link: `/docs/api/schemas/${name}`})),
},
]
// Main docs sidebar, shared by the /docs/ pages and the standalone /changelog page.
const docsSidebar = [
{
text: '', items: [
{text: 'Introduction', link: '/docs'},
{text: 'Installation', link: '/docs/installation', items: [
{text: 'Docker', link: '/docs/installation/docker'},
{text: 'Kubernetes', link: '/docs/installation/kubernetes'},
{text: 'Binary', link: '/docs/installation/binary'},
{text: 'Source', link: '/docs/installation/source'},
],
collapsed: true
},
{text: 'Update', link: '/docs/update'},
], collapsed: false
},
{
text: 'Configuration', base: '/docs/configuration', items: [
{text: 'Configure Opengist', link: '/configure'},
{text: 'Databases', items: [
{text: 'SQLite', link: '/databases/sqlite'},
{text: 'PostgreSQL', link: '/databases/postgresql'},
{text: 'MySQL', link: '/databases/mysql'},
], collapsed: true
},
{text: 'OAuth Providers', link: '/oauth-providers'},
{text: 'Custom assets', link: '/custom-assets'},
{text: 'Custom links', link: '/custom-links'},
{text: 'Cheat Sheet', link: '/cheat-sheet'},
{text: 'Metrics', link: '/metrics'},
{text: 'Admin panel', link: '/admin-panel'},
], collapsed: false
},
{
text: 'Usage', base: '/docs/usage', items: [
{text: 'Init via Git', link: '/init-via-git'},
{text: 'Embed Gist', link: '/embed'},
{text: 'Access Tokens', link: '/access-tokens'},
{text: 'Gist as JSON', link: '/gist-json'},
{text: 'Import Gists from Github', link: '/import-from-github-gist'},
{text: 'Git push options', link: '/git-push-options'},
], collapsed: false
},
{
text: 'Administration', base: '/docs/administration', items: [
{text: 'Run with systemd', link: '/run-with-systemd'},
{text: 'Reverse proxy', items: [
{text: 'Nginx', link: '/nginx-reverse-proxy'},
{text: 'Traefik', link: '/traefik-reverse-proxy'},
], collapsed: true},
{text: 'Fail2ban', link: '/fail2ban-setup'},
{text: 'Healthcheck', link: '/healthcheck'},
], collapsed: false
},
{
text: 'Contributing', base: '/docs/contributing', items: [
{text: 'Community', link: '/community'},
{text: 'Development', link: '/development'},
], collapsed: false
},
]
// https://vitepress.dev/reference/site-config
const hostname = 'https://opengist.io'
const ogImage = `${hostname}/opengist-demo.png`
export default defineConfig({
title: "Opengist",
description: "Documention for Opengist",
description: "Documentation for Opengist — a self-hosted pastebin powered by Git.",
lang: 'en-US',
sitemap: {
hostname,
},
markdown: {
config(md) {
// Strip the "See here how to update Opengist." note that appears
// under each version in the embedded CHANGELOG (source stays intact).
md.core.ruler.push('strip_changelog_update_note', (state) => {
const t = state.tokens
for (let i = t.length - 1; i >= 0; i--) {
if (
t[i].type === 'inline' &&
/See here how to .*update.* Opengist\./i.test(t[i].content) &&
t[i - 1]?.type === 'paragraph_open'
) {
t.splice(i - 1, 3)
}
}
})
// Turn "#123" references into links to the matching GitHub PR/issue.
md.inline.ruler.before('text', 'github_ref', (state, silent) => {
const start = state.pos
if (state.src.charCodeAt(start) !== 0x23 /* # */) return false
// Require a boundary before '#' (avoid URL fragments like page#1).
const prev = start > 0 ? state.src[start - 1] : ''
if (prev && /[0-9A-Za-z]/.test(prev)) return false
let pos = start + 1
while (pos < state.posMax && /[0-9]/.test(state.src[pos])) pos++
if (pos === start + 1) return false // no digits
// Reject things like a hex color "#1a2" (digit run followed by a letter).
if (pos < state.posMax && /[A-Za-z]/.test(state.src[pos])) return false
const num = state.src.slice(start + 1, pos)
if (!silent) {
const open = state.push('link_open', 'a', 1)
open.attrs = [
['href', `https://github.com/thomiceli/opengist/pull/${num}`],
['target', '_blank'],
['rel', 'noreferrer'],
]
state.push('text', '', 0).content = `#${num}`
state.push('link_close', 'a', -1)
}
state.pos = pos
return true
})
},
},
vite: {
plugins: [tailwindcss(), openapiRawPlugin]
},
rewrites: {
'index.md': 'index.md',
'introduction.md': 'docs/index.md',
'changelog.md': 'changelog.md',
':path(.*)': 'docs/:path'
},
themeConfig: {
@@ -14,78 +178,39 @@ export default defineConfig({
logo: 'https://raw.githubusercontent.com/thomiceli/opengist/master/public/img/opengist.svg',
logoLink: '/',
nav: [
{ text: 'Demo', link: 'https://demo.opengist.io' },
{ text: 'Translate', link: 'https://tr.opengist.io' }
{ text: 'Docs', link: '/docs', activeMatch: '^/docs' },
{
text: 'Resources',
items: [
{ text: 'Demo', link: 'https://demo.opengist.io' },
{ text: 'Translate', link: 'https://tr.opengist.io' },
]
},
{
text: 'v1.13.1',
items: [
{ text: 'Changelog', link: '/changelog' },
{ text: 'Releases', link: 'https://github.com/thomiceli/opengist/releases' },
]
}
],
sidebar: {
'/docs/': [
{
text: '', items: [
{text: 'Introduction', link: '/docs'},
{text: 'Installation', link: '/docs/installation', items: [
{text: 'Docker', link: '/docs/installation/docker'},
{text: 'Kubernetes', link: '/docs/installation/kubernetes'},
{text: 'Binary', link: '/docs/installation/binary'},
{text: 'Source', link: '/docs/installation/source'},
],
collapsed: true
},
{text: 'Update', link: '/docs/update'},
], collapsed: false
},
{
text: 'Configuration', base: '/docs/configuration', items: [
{text: 'Configure Opengist', link: '/configure'},
{text: 'Databases', items: [
{text: 'SQLite', link: '/databases/sqlite'},
{text: 'PostgreSQL', link: '/databases/postgresql'},
{text: 'MySQL', link: '/databases/mysql'},
], collapsed: true
},
{text: 'OAuth Providers', link: '/oauth-providers'},
{text: 'Custom assets', link: '/custom-assets'},
{text: 'Custom links', link: '/custom-links'},
{text: 'Cheat Sheet', link: '/cheat-sheet'},
{text: 'Metrics', link: '/metrics'},
{text: 'Admin panel', link: '/admin-panel'},
], collapsed: false
},
{
text: 'Usage', base: '/docs/usage', items: [
{text: 'Init via Git', link: '/init-via-git'},
{text: 'Embed Gist', link: '/embed'},
{text: 'Access Tokens', link: '/access-tokens'},
{text: 'Gist as JSON', link: '/gist-json'},
{text: 'Import Gists from Github', link: '/import-from-github-gist'},
{text: 'Git push options', link: '/git-push-options'},
], collapsed: false
},
{
text: 'Administration', base: '/docs/administration', items: [
{text: 'Run with systemd', link: '/run-with-systemd'},
{text: 'Reverse proxy', items: [
{text: 'Nginx', link: '/nginx-reverse-proxy'},
{text: 'Traefik', link: '/traefik-reverse-proxy'},
], collapsed: true},
{text: 'Fail2ban', link: '/fail2ban-setup'},
{text: 'Healthcheck', link: '/healthcheck'},
], collapsed: false
},
{
text: 'Contributing', base: '/docs/contributing', items: [
{text: 'Community', link: '/community'},
{text: 'Development', link: '/development'},
], collapsed: false
},
]},
// Standalone API Reference section: its own sidebar, separate from
// the main docs navigation. Longest-prefix match means /docs/api
// uses this instead of the '/docs/' sidebar below.
'/docs/api': apiSidebar,
'/docs/': docsSidebar,
// Standalone /changelog page reuses the main docs sidebar.
'/changelog': docsSidebar,
},
socialLinks: [
{icon: 'github', link: 'https://github.com/thomiceli/opengist'}
{icon: 'github', link: 'https://github.com/thomiceli/opengist'},
{icon: 'discord', link: 'https://discord.gg/9Pm3X5scZT'}
],
editLink: {
pattern: 'https://github.com/thomiceli/opengist/edit/stable/docs/:path'
pattern: 'https://github.com/thomiceli/opengist/edit/master/docs/:path'
},
// @ts-ignore
lastUpdated: true,
@@ -93,6 +218,43 @@ export default defineConfig({
},
head: [
['link', {rel: 'icon', href: '/favicon.svg'}],
['meta', {name: 'theme-color', content: '#3c79e2'}],
// Site-wide Open Graph / Twitter Card defaults (per-page values are
// refined in transformPageData below).
['meta', {property: 'og:type', content: 'website'}],
['meta', {property: 'og:site_name', content: 'Opengist'}],
['meta', {property: 'og:image', content: ogImage}],
['meta', {name: 'twitter:card', content: 'summary_large_image'}],
['meta', {name: 'twitter:image', content: ogImage}],
],
// Per-page meta: canonical URL, description, and Open Graph / Twitter tags
// built from each page's title + description.
transformPageData(pageData) {
// Mirror the `rewrites` above to compute the deployed path.
let out
if (pageData.relativePath === 'index.md') out = 'index.md'
else if (pageData.relativePath === 'introduction.md') out = 'docs/index.md'
else if (pageData.relativePath === 'changelog.md') out = 'changelog.md'
else out = `docs/${pageData.relativePath}`
const path = out.replace(/(^|\/)index\.md$/, '$1').replace(/\.md$/, '.html')
const url = `${hostname}/${path}`
const base = pageData.title || 'Opengist'
const title = base.includes('Opengist') ? base : `${base} | Opengist`
const description =
pageData.description ||
pageData.frontmatter.description ||
'Documentation for Opengist — a self-hosted pastebin powered by Git.'
pageData.frontmatter.head ??= []
pageData.frontmatter.head.push(
['link', {rel: 'canonical', href: url}],
['meta', {property: 'og:title', content: title}],
['meta', {property: 'og:description', content: description}],
['meta', {property: 'og:url', content: url}],
['meta', {name: 'twitter:title', content: title}],
['meta', {name: 'twitter:description', content: description}],
)
},
ignoreDeadLinks: true
})
+13
View File
@@ -0,0 +1,13 @@
import { defineLoader } from 'vitepress'
import { buildApiData, SPEC_FILE, type ApiData } from './openapi'
// `data` is populated at build time and importable from any component.
declare const data: ApiData
export { data }
export type { ApiData }
export default defineLoader({
// Rebuild whenever the spec changes (path is relative to this file).
watch: [SPEC_FILE],
load: (): Promise<ApiData> => buildApiData()
})
+441
View File
@@ -0,0 +1,441 @@
import { readFileSync } from 'node:fs'
import { fileURLToPath } from 'node:url'
import { parse } from 'yaml'
// The authoritative spec lives in the Go source tree.
export const SPEC_FILE = '../../internal/web/handlers/api/openapi.yaml'
const SPEC_URL = new URL(SPEC_FILE, import.meta.url)
const METHOD_ORDER = ['get', 'post', 'put', 'patch', 'delete']
// ---- shapes consumed by the renderer components (all JSON-serializable) ----
export interface ApiData {
info: { title: string; version: string; descriptionHtml: string }
authHtml: string
servers: { url: string; description: string }[]
groups: Group[]
schemas: SchemaDef[]
}
export interface Group {
name: string
descriptionHtml: string
operations: Operation[]
}
export interface Operation {
id: string
method: string
path: string
summary: string
descriptionHtml: string
auth: { anonymous: boolean; anyToken: boolean; scopes: string[] }
parameters: Param[]
requestBody: (BodyTable & { required: boolean }) | null
responses: ResponseDef[]
// Synthesized samples for the right-hand code panel (pre-highlighted HTML).
sample: { curlHtml: string; responseStatus: string; responseHtml: string | null }
}
interface Param {
name: string
in: string
required: boolean
typeHtml: string
descriptionHtml: string
}
interface Field {
name: string
typeHtml: string
required: boolean
descriptionHtml: string
depth: number
}
// A body schema flattened into a field table (the inlined replacement for a
// schema link): its type label, whether it's an array, and its fields.
interface BodyTable {
label: string
arrayOf: boolean
note: string
rows: Field[]
}
interface ResponseDef {
status: string
descriptionHtml: string
headers: { name: string; typeHtml: string; descriptionHtml: string }[]
typeLabel: string | null
}
// A named component schema, rendered one-per-page.
export interface SchemaDef {
name: string
descriptionHtml: string
note: string
rows: Field[]
}
// Lightweight index used by config.mts (sidebar) and the dynamic-route
// generator. Synchronous and markdown-free so it's cheap to call.
export interface OpIndex {
id: string
method: string
path: string
summary: string
tag: string
}
function readSpec(): any {
return parse(readSpecRaw())
}
// Raw spec contents, for serving the file verbatim at /docs/api/openapi.yaml.
export function readSpecRaw(): string {
return readFileSync(fileURLToPath(SPEC_URL), 'utf-8')
}
// Walk paths × methods in a stable order, yielding each operation.
function eachOperation(spec: any, cb: (op: any, method: string, path: string, item: any) => void) {
for (const [path, item] of Object.entries<any>(spec.paths)) {
for (const method of METHOD_ORDER) {
const op = item[method]
if (op) cb(op, method, path, item)
}
}
}
export function listOperations(): OpIndex[] {
const spec = readSpec()
const ops: OpIndex[] = []
eachOperation(spec, (op, method, path) => {
ops.push({
id: op.operationId,
method: method.toUpperCase(),
path,
summary: op.summary || op.operationId,
tag: op.tags?.[0] || (spec.tags?.[0]?.name ?? '')
})
})
return ops
}
// Names of the component schemas, for the sidebar and per-schema routes.
export function listSchemas(): string[] {
return Object.keys(readSpec().components?.schemas || {})
}
export async function buildApiData(): Promise<ApiData> {
// Lazily pulled in so this module can be imported from config.mts without
// dragging the markdown renderer into the config load path.
const { createMarkdownRenderer } = await import('vitepress')
const spec = readSpec()
const md = await createMarkdownRenderer(fileURLToPath(new URL('..', import.meta.url)))
// Block rendering is async because Shiki highlighting in VitePress 2 is
// resolved via markdown-it-async; renderInline carries no code fences.
const html = (s?: string) => (s ? md.renderAsync(s) : Promise.resolve(''))
const inline = (s?: string) => (s ? md.renderInline(s) : '')
// Drop the "**Required ...**" scope line from operation descriptions — the
// Authorization header row already documents the required scopes.
const stripRequired = (s?: string) =>
s
? s
.split('\n')
.filter((l) => !l.trimStart().startsWith('**Required'))
.join('\n')
: s
const refName = (ref: string) => ref.split('/').pop() as string
const resolve = (ref: string): any =>
ref
.replace(/^#\//, '')
.split('/')
.reduce((acc, key) => acc?.[key], spec)
const deref = (obj: any): any => (obj?.$ref ? resolve(obj.$ref) : obj)
// Named schema references link to their per-schema page.
const link = (name: string) => `<a href="/docs/api/schemas/${name}"><code>${name}</code></a>`
const typeHtml = (s: any): string => {
if (!s) return '<code>any</code>'
if (s.$ref) return link(refName(s.$ref))
if (s.oneOf) return s.oneOf.map(typeHtml).join(' | ')
if (s.allOf) return s.allOf.map(typeHtml).join(' &amp; ')
if (s.type === 'array') return typeHtml(s.items) + '[]'
if (s.type === 'null') return '<code>null</code>'
if (s.type === 'object' && s.additionalProperties)
return `map&lt;string, ${typeHtml(s.additionalProperties)}&gt;`
let t = s.type || 'object'
if (s.format) t += ` &lt;${s.format}&gt;`
let out = `<code>${t}</code>`
if (Array.isArray(s.enum))
out += `: ${s.enum.map((e: any) => `<code>${e}</code>`).join(' | ')}`
return out
}
const constraints = (s: any): string => {
if (!s) return ''
const bits: string[] = []
if (s.default !== undefined) bits.push(`default: ${s.default}`)
if (s.minimum !== undefined) bits.push(`min: ${s.minimum}`)
if (s.maximum !== undefined) bits.push(`max: ${s.maximum}`)
if (s.pattern) bits.push(`pattern: <code>${s.pattern}</code>`)
return bits.length ? ` <em>(${bits.join(', ')})</em>` : ''
}
// Flatten a schema to its full property set, resolving $ref and allOf
// (including inherited fields) so each body can render a complete table.
// `seen` guards against recursive schemas.
const flatten = (
s: any,
seen: string[] = []
): { props: Record<string, any>; required: string[]; addl: any } => {
const acc = { props: {} as Record<string, any>, required: [] as string[], addl: null as any }
if (!s) return acc
if (s.$ref) {
const name = refName(s.$ref)
return seen.includes(name) ? acc : flatten(resolve(s.$ref), [...seen, name])
}
for (const part of s.allOf || []) {
const f = flatten(part, seen)
Object.assign(acc.props, f.props)
acc.required.push(...f.required)
if (f.addl) acc.addl = f.addl
}
if (s.properties) Object.assign(acc.props, s.properties)
if (s.required) acc.required.push(...s.required)
if (s.additionalProperties) acc.addl = s.additionalProperties
return acc
}
// Turn a request/response body schema into a renderable field table.
const bodyTable = (schema: any): BodyTable => {
let s = schema
let arrayOf = false
while (s?.$ref) s = resolve(s.$ref)
if (s?.type === 'array') {
arrayOf = true
s = s.items
while (s?.$ref) s = resolve(s.$ref)
}
const label = schema ? typeHtml(schema) : '<code>any</code>'
const f = flatten(s)
const rows: Field[] = Object.entries<any>(f.props).map(([pname, ps]) => ({
name: pname,
typeHtml: typeHtml(ps),
required: f.required.includes(pname),
descriptionHtml: inline(ps.description),
depth: 0
}))
const note = f.addl ? `Additional properties: map&lt;string, ${typeHtml(f.addl)}&gt;` : ''
return { label, arrayOf, note, rows }
}
// Recursively expand an object/array/map schema into indented field rows,
// descending into nested objects (used for the request body table). `seen`
// tracks $ref names on the current branch to break recursive schemas.
const childrenOf = (schema: any, depth: number, seen: string[]): Field[] => {
let s = schema
const branch = [...seen]
while (s) {
if (s.$ref) {
const n = refName(s.$ref)
if (branch.includes(n)) return []
branch.push(n)
s = resolve(s.$ref)
} else if (s.type === 'array') s = s.items
else if (s.oneOf) s = s.oneOf.find((o: any) => o.type !== 'null') ?? s.oneOf[0]
else break
}
if (!s) return []
const f = flatten(s, branch)
const entries = Object.entries<any>(f.props)
// A pure map (additionalProperties only): descend into the value's fields.
if (entries.length === 0 && f.addl) return childrenOf(f.addl, depth, branch)
const out: Field[] = []
for (const [name, ps] of entries) {
out.push({
name,
typeHtml: typeHtml(ps),
required: f.required.includes(name),
descriptionHtml: inline(ps.description),
depth
})
out.push(...childrenOf(ps, depth + 1, branch))
}
return out
}
// Synthesize a representative value for a schema (no examples in the spec).
// `seen` tracks $ref names on the current branch to break recursive schemas.
const sample = (schema: any, seen: string[] = []): any => {
if (!schema) return null
if (schema.$ref) {
const name = refName(schema.$ref)
if (seen.includes(name)) return null
return sample(resolve(schema.$ref), [...seen, name])
}
if (schema.example !== undefined) return schema.example
if (schema.default !== undefined) return schema.default
if (Array.isArray(schema.enum)) return schema.enum[0]
if (schema.allOf)
return schema.allOf.reduce((acc: any, s: any) => Object.assign(acc, sample(s, seen)), {})
if (schema.oneOf) {
const pick = schema.oneOf.find((s: any) => s.type !== 'null') ?? schema.oneOf[0]
return sample(pick, seen)
}
switch (schema.type) {
case 'array':
return [sample(schema.items, seen)]
case 'object': {
const obj: Record<string, any> = {}
for (const [k, v] of Object.entries<any>(schema.properties || {})) obj[k] = sample(v, seen)
if (schema.additionalProperties && typeof schema.additionalProperties === 'object')
obj['<key>'] = sample(schema.additionalProperties, seen)
return obj
}
case 'integer':
case 'number':
return 1
case 'boolean':
return true
case 'null':
return null
default:
if (schema.format === 'date-time') return '2024-01-01T00:00:00Z'
if (schema.format === 'uri') return 'https://opengist.example/...'
if (schema.format === 'binary') return '<binary data>'
return 'string'
}
}
const HOST = 'https://opengist.example.com'
const base = spec.servers?.[0]?.url ?? ''
const fence = (lang: string, code: string) => md.renderAsync(`\`\`\`${lang}\n${code}\n\`\`\``)
const tagDefs: any[] = spec.tags || []
const groups: Group[] = []
for (const t of tagDefs)
groups.push({ name: t.name, descriptionHtml: await html(t.description), operations: [] })
const groupByName = new Map(groups.map((g) => [g.name, g]))
for (const [path, item] of Object.entries<any>(spec.paths)) {
const pathParams: any[] = (item.parameters || []).map(deref)
for (const method of METHOD_ORDER) {
const op = item[method]
if (!op) continue
const allParams: any[] = [...pathParams, ...(op.parameters || []).map(deref)]
const sec: any[] = op.security ?? spec.security ?? []
const auth = { anonymous: false, anyToken: false, scopes: [] as string[] }
const scopeSet = new Set<string>()
for (const req of sec) {
if (Object.keys(req).length === 0) auth.anonymous = true
else {
const arr: string[] = req.bearerAuth || []
if (arr.length === 0) auth.anyToken = true
else arr.forEach((s) => scopeSet.add(s))
}
}
auth.scopes = [...scopeSet]
const parameters: Param[] = allParams.map((p) => ({
name: p.name,
in: p.in,
required: !!p.required,
typeHtml: typeHtml(p.schema),
descriptionHtml: inline(p.description) + constraints(p.schema)
}))
let requestBody: Operation['requestBody'] = null
let requestSchema: any = null
if (op.requestBody) {
const rb = deref(op.requestBody)
requestSchema = rb.content?.['application/json']?.schema
// Expand nested object fields inline so the whole body shape is visible.
requestBody = {
required: !!rb.required,
...bodyTable(requestSchema),
rows: childrenOf(requestSchema, 0, [])
}
}
// ---- curl request sample: -X, then --url, then headers / body ----
const query = allParams
.filter((p) => p.in === 'query' && (p.required || p.schema?.default !== undefined))
.map((p) => `${p.name}=${p.schema?.default ?? sample(p.schema)}`)
.join('&')
const url = `${HOST}${base}${path}${query ? '?' + query : ''}`
const curlLines = [
`curl -X ${method.toUpperCase()}`,
` --url "${url}"`,
` -H "Authorization: Bearer og_xxxxxxxx"`
]
if (requestSchema) {
curlLines.push(` -H "Content-Type: application/json"`)
curlLines.push(` -d '${JSON.stringify(sample(requestSchema), null, 2)}'`)
}
const curlHtml = await fence('bash', curlLines.join(' \\\n'))
// ---- example response (first 2xx with a JSON body) for the code panel ----
let responseStatus = ''
let responseHtml: string | null = null
for (const [status, resRaw] of Object.entries<any>(op.responses)) {
if (!status.startsWith('2')) continue
responseStatus = status
const schema = deref(resRaw).content?.['application/json']?.schema
if (schema) responseHtml = await fence('json', JSON.stringify(sample(schema), null, 2))
break
}
const responses: ResponseDef[] = Object.entries<any>(op.responses).map(([status, resRaw]) => {
const res = deref(resRaw)
const content = res.content || {}
const ct = content['application/json'] || Object.values<any>(content)[0]
return {
status,
descriptionHtml: inline(res.description),
headers: Object.entries<any>(res.headers || {}).map(([name, hRaw]) => {
const h = deref(hRaw)
return { name, typeHtml: typeHtml(h.schema), descriptionHtml: inline(h.description) }
}),
typeLabel: ct?.schema ? typeHtml(ct.schema) : null
}
})
const built: Operation = {
id: op.operationId,
method: method.toUpperCase(),
path,
summary: op.summary || '',
descriptionHtml: await html(stripRequired(op.description)),
auth,
parameters,
requestBody,
responses,
sample: { curlHtml, responseStatus, responseHtml }
}
;(groupByName.get(op.tags?.[0]) ?? groups[0])?.operations.push(built)
}
}
// ---- component schemas, each rendered on its own page ----
const schemas: SchemaDef[] = []
for (const [name, s] of Object.entries<any>(spec.components?.schemas || {})) {
const t = bodyTable(s)
schemas.push({
name,
descriptionHtml: await html(s.description),
note: t.note,
rows: t.rows
})
}
return {
info: {
title: spec.info.title,
version: spec.info.version,
descriptionHtml: await html(spec.info.description)
},
authHtml: await html(spec.components?.securitySchemes?.bearerAuth?.description),
servers: (spec.servers || []).map((s: any) => ({
url: s.url,
description: s.description || ''
})),
groups,
schemas
}
}
-37
View File
@@ -1,37 +0,0 @@
const colors = require('tailwindcss/colors')
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
"./.vitepress/theme/*.vue",
],
theme: {
colors: {
transparent: 'transparent',
current: 'currentColor',
white: colors.white,
black: colors.black,
gray: {
50: "#EEEFF1",
100: "#DEDFE3",
200: "#BABCC5",
300: "#999CA8",
400: "#75798A",
500: "#585B68",
600: "#464853",
700: "#363840",
800: "#232429",
900: "#131316"
},
indigo: colors.indigo,
},
extend: {
borderWidth: {
'1': '1px',
}
},
},
plugins: [],
darkMode: 'class',
}
-101
View File
@@ -1,101 +0,0 @@
<script>
import { withBase } from 'vitepress';
import './theme.css'
export default {
setup() {
return { withBase };
},
};
</script>
<template>
<main class="home">
<header class="hero">
<div class="mx-auto max-w-7xl px-6 lg:px-8">
<div class="mx-auto lg:text-center">
<img class="rotating h-36 mx-auto my-8 " src="https://raw.githubusercontent.com/thomiceli/opengist/master/public/img/opengist.svg" alt="" >
<a target="_blank" href="https://github.com/thomiceli/opengist/releases" class="inline-flex items-center rounded-full bg-indigo-100 hover:bg-indigo-200 px-4 py-1.5 text-lg font-medium text-indigo-700">
<span class="pr-1">Released 1.12</span>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-4">
<path stroke-linecap="round" stroke-linejoin="round" d="m4.5 19.5 15-15m0 0H8.25m11.25 0v11.25" />
</svg>
</a>
<h1 class="mt-5 text-4xl font-bold tracking-tight sm:text-5xl">Opengist</h1>
<h2 class="mt-4 text-xl">Self-hosted pastebin powered by Git, open-source alternative to Github Gist.</h2>
</div>
<div class="space-x-2 my-12">
<a href="/docs" class="rounded-md bg-indigo-600 mt-6 px-5 py-3 text-xl font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600">Docs</a>
<a target="_blank" href="https://demo.opengist.io" class="rounded-md bg-indigo-400 mt-6 px-5 py-3 text-xl border-white font-semibold text-white shadow-sm hover:bg-indigo-300 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600">Live demo</a>
<a target="_blank" href="https://github.com/thomiceli/opengist" class="rounded-md bg-gray-800 mt-6 px-3 py-3 text-xl dark:border dark:border-1 dark:border-gray-400 font-semibold text-white shadow-sm hover:bg-gray-700 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600">
<svg stroke="currentColor" fill="currentColor" stroke-width="0" viewBox="0 0 16 16" class="w-7 h-auto inline" height="1em" width="1em" xmlns="http://www.w3.org/2000/svg"><path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27s1.36.09 2 .27c1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.01 8.01 0 0 0 16 8c0-4.42-3.58-8-8-8"></path></svg>
</a>
</div>
<div class="border border-1 mt-6 px-5 py-3 rounded-md shadow-sm ">
<code class="select-all ">docker run --name <span class="text-indigo-700 dark:text-indigo-300 font-bold">opengist</span> -p <span class="text-indigo-700 dark:text-indigo-300 font-bold">6157</span>:6157 -v "<span class="text-indigo-700 dark:text-indigo-300 font-bold">$HOME/.opengist</span>:/opengist" ghcr.io/thomiceli/opengist:1</code>
</div>
</div>
</header>
<div class="relative w-full sm:max-w-7xl mx-auto overflow-auto">
<img class="block w-[200vw] max-w-none sm:w-full h-auto" :src="withBase('/opengist-demo.png')" alt="demo-opengist-screenshot" />
</div>
</main>
</template>
<style>
@-webkit-keyframes rotating /* Safari and Chrome */ {
from {
-webkit-transform: rotate(0deg);
-o-transform: rotate(0deg);
transform: rotate(0deg);
}
to {
-webkit-transform: rotate(360deg);
-o-transform: rotate(360deg);
transform: rotate(360deg);
}
}
@keyframes rotating {
from {
-ms-transform: rotate(0deg);
-moz-transform: rotate(0deg);
-webkit-transform: rotate(0deg);
-o-transform: rotate(0deg);
transform: rotate(0deg);
}
to {
-ms-transform: rotate(360deg);
-moz-transform: rotate(360deg);
-webkit-transform: rotate(360deg);
-o-transform: rotate(360deg);
transform: rotate(360deg);
}
}
.home {
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
display: flex;
flex-direction: column;
gap: 1rem;
text-align: center;
}
.rotating {
-webkit-animation: rotating 8s linear infinite;
-moz-animation: rotating 4s linear infinite;
-ms-animation: rotating 4s linear infinite;
-o-animation: rotating 4s linear infinite;
animation: rotating 12s linear infinite;
}
</style>
+8 -3
View File
@@ -1,16 +1,21 @@
<script setup>
import { computed } from 'vue'
import { useData } from 'vitepress'
import Home from './Home.vue'
import Home from './layouts/Home.vue'
import SectionNav from './components/SectionNav.vue'
import DefaultTheme from 'vitepress/theme'
const { Layout } = DefaultTheme
const { frontmatter } = useData()
const isHome = computed(() => frontmatter.value.layout === 'home')
</script>
<template>
<SectionNav v-if="!isHome" />
<Layout>
<template v-if="frontmatter.layout === 'home'" #home-hero-after>
<template v-if="isHome" #home-hero-after>
<Home />
</template>
</Layout>
</template>
</template>
@@ -0,0 +1,161 @@
<script setup lang="ts">
import { computed } from 'vue'
import { data } from '../../openapi.data'
const props = defineProps<{ id: string }>()
const op = computed(() =>
data.groups.flatMap((g) => g.operations).find((o) => o.id === props.id)
)
// Split the path so `{param}` segments can be highlighted.
const pathParts = computed(() =>
(op.value?.path ?? '')
.split(/(\{[^}]+\})/g)
.filter(Boolean)
.map((text) => ({ text, param: text.startsWith('{') && text.endsWith('}') }))
)
// The Authorization header is implied by the spec's bearerAuth security scheme
// rather than declared as a parameter, so synthesize a row from `auth`.
const authHeader = computed(() => {
const a = op.value?.auth
if (!a || (!a.scopes.length && !a.anyToken && !a.anonymous)) return null
const desc = a.scopes.map((s) => `<code>${s}</code>`).join(', ')
return {
name: 'Authorization',
typeHtml: '<code>string</code>',
required: !a.anonymous,
descriptionHtml: desc
}
})
// Request parameters grouped by location, in display order, empty groups dropped.
const paramGroups = computed(() => {
const o = op.value
if (!o) return []
const headers = [
...(authHeader.value ? [authHeader.value] : []),
...o.parameters.filter((p) => p.in === 'header')
]
const groups = [
{ label: 'Headers', descLabel: 'Scopes', optional: true, items: headers },
{ label: 'Path parameters', descLabel: 'Description', optional: false, items: o.parameters.filter((p) => p.in === 'path') },
{ label: 'Query parameters', descLabel: 'Description', optional: false, items: o.parameters.filter((p) => p.in === 'query') }
]
return groups.filter((g) => g.items.length)
})
const hasRequest = computed(() => paramGroups.value.length > 0 || !!op.value?.requestBody)
</script>
<template>
<div v-if="op" class="oas-op-grid">
<!-- left / center: description, request, responses -->
<div class="oas-main">
<h1>{{ op.summary }}</h1>
<div class="oas-endpoint">
<span class="oas-method" :class="`m-${op.method.toLowerCase()}`">{{ op.method }}</span>
<code class="oas-path"><span
v-for="(part, i) in pathParts"
:key="i"
:class="{ 'oas-param': part.param }"
>{{ part.text }}</span></code>
</div>
<div class="oas-desc" v-html="op.descriptionHtml" />
<template v-if="hasRequest">
<h2>Request</h2>
<template v-for="g in paramGroups" :key="g.label">
<h3>{{ g.label }}</h3>
<table class="oas-table">
<thead>
<tr><th>Name</th><th>Type</th><th>{{ g.descLabel }}</th></tr>
</thead>
<tbody>
<tr v-for="p in g.items" :key="p.name">
<td>
<code>{{ p.name }}</code>
<span v-if="p.required" class="oas-req">required</span>
<span v-else-if="g.optional" class="oas-opt">optional</span>
</td>
<td v-html="p.typeHtml" />
<td v-html="p.descriptionHtml" />
</tr>
</tbody>
</table>
</template>
<template v-if="op.requestBody">
<h3>Body parameters</h3>
<p class="oas-bodyhead">
<span v-html="op.requestBody.label" />
<span v-if="op.requestBody.required" class="oas-req">required</span>
<span class="oas-ct">application/json</span>
</p>
<table v-if="op.requestBody.rows.length" class="oas-table">
<thead>
<tr><th>Field</th><th>Type</th><th>Description</th></tr>
</thead>
<tbody>
<tr v-for="(row, i) in op.requestBody.rows" :key="i" :class="{ 'oas-nested': row.depth }">
<td>
<span class="oas-field" :style="{ paddingLeft: row.depth * 1.25 + 'rem' }">
<span v-if="row.depth" class="oas-nest"></span>
<code>{{ row.name }}</code>
<span v-if="row.required" class="oas-req">required</span>
<span v-else class="oas-opt">optional</span>
</span>
</td>
<td v-html="row.typeHtml" />
<td v-html="row.descriptionHtml" />
</tr>
</tbody>
</table>
<p v-if="op.requestBody.note" class="oas-note" v-html="op.requestBody.note" />
</template>
</template>
<h2>Responses</h2>
<table class="oas-table">
<thead>
<tr><th>Status</th><th>Body</th><th>Description</th></tr>
</thead>
<tbody>
<template v-for="r in op.responses" :key="r.status">
<tr>
<td><code class="oas-status" :class="`s-${r.status[0]}`">{{ r.status }}</code></td>
<td><span v-if="r.typeLabel" v-html="r.typeLabel" /><span v-else></span></td>
<td v-html="r.descriptionHtml" />
</tr>
<tr v-if="r.headers.length" class="oas-headers">
<td></td>
<td colspan="2">
<span class="oas-hlabel">Headers:</span>
<span v-for="h in r.headers" :key="h.name" class="oas-header"><code>{{ h.name }}</code></span>
</td>
</tr>
</template>
</tbody>
</table>
</div>
<!-- right: code samples -->
<aside class="oas-code">
<div class="oas-code-sticky">
<div class="oas-code-label">Request</div>
<div class="oas-code-block" v-html="op.sample.curlHtml" />
<template v-if="op.sample.responseHtml">
<div class="oas-code-label">
Response
<code class="oas-status" :class="`s-${op.sample.responseStatus[0]}`">{{ op.sample.responseStatus }}</code>
</div>
<div class="oas-code-block" v-html="op.sample.responseHtml" />
</template>
</div>
</aside>
</div>
<div v-else><p>Unknown operation: <code>{{ id }}</code></p></div>
</template>
@@ -0,0 +1,31 @@
<script setup lang="ts">
import { computed } from 'vue'
import { data } from '../../openapi.data'
const props = defineProps<{ name: string }>()
const schema = computed(() => data.schemas.find((s) => s.name === props.name))
</script>
<template>
<div v-if="schema" class="oas-schema">
<h1>{{ schema.name }}</h1>
<div v-html="schema.descriptionHtml" />
<table v-if="schema.rows.length" class="oas-table">
<thead>
<tr><th>Field</th><th>Type</th><th>Description</th></tr>
</thead>
<tbody>
<tr v-for="row in schema.rows" :key="row.name">
<td>
<code>{{ row.name }}</code>
<span v-if="row.required" class="oas-req">required</span>
</td>
<td v-html="row.typeHtml" />
<td v-html="row.descriptionHtml" />
</tr>
</tbody>
</table>
<p v-if="schema.note" class="oas-note" v-html="schema.note" />
</div>
<div v-else><p>Unknown schema: <code>{{ name }}</code></p></div>
</template>
@@ -0,0 +1,88 @@
<script setup>
import { computed } from 'vue'
import { useRoute, withBase } from 'vitepress'
const route = useRoute()
const isApi = computed(() => route.path.startsWith(withBase('/docs/api')))
const items = computed(() => [
{ text: 'Guide', link: '/docs', active: !isApi.value },
{ text: 'API', link: '/docs/api', active: isApi.value },
])
</script>
<template>
<div class="og-sectionbar">
<div class="og-sectionbar__inner">
<a
v-for="it in items"
:key="it.link"
:href="withBase(it.link)"
class="og-sectionbar__link"
:class="{ 'is-active': it.active }"
>
{{ it.text }}
</a>
</div>
</div>
</template>
<style scoped>
.og-sectionbar {
display: none;
}
@media (min-width: 960px) {
.og-sectionbar {
position: fixed;
top: var(--vp-nav-height);
left: 0;
right: 0;
z-index: 28;
display: block;
height: var(--og-bar-height);
background-color: var(--vp-nav-bg-color);
}
}
/* Same container + frame borders as the top navbar so the bar lines up: full
--vp-layout-max-width centered, 32px inner padding, and 2px vertical + bottom
borders matching the rest of the frame. */
.og-sectionbar__inner {
display: flex;
align-items: center;
gap: 1.5rem;
height: 100%;
max-width: var(--vp-layout-max-width);
margin: 0 auto;
padding-inline: 32px;
border-inline: var(--og-frame-border) solid var(--vp-c-divider);
border-bottom: var(--og-frame-border) solid var(--vp-c-divider);
}
.og-sectionbar__link {
position: relative;
display: inline-flex;
align-items: center;
height: 100%;
font-size: 14px;
font-weight: 600;
color: var(--vp-c-text-2);
transition: color 0.2s ease;
}
.og-sectionbar__link:hover {
color: var(--vp-c-text-1);
}
.og-sectionbar__link.is-active {
color: var(--vp-c-brand-1);
}
.og-sectionbar__link.is-active::after {
content: "";
position: absolute;
left: 0;
right: 0;
bottom: -1px;
height: 2px;
background: var(--vp-c-brand-1);
}
</style>
@@ -0,0 +1,135 @@
<script setup>
const features = [
{
title: 'Public, unlisted & private',
desc: 'Set the visibility of every snippet - public, unlisted, or fully private.',
},
{
title: 'Powered by Git',
desc: 'Each snippet is a Git repository. Clone and push over HTTP or SSH, or create one with a plain git push.',
},
{
title: 'Git CLI & API',
desc: 'Manage your snippets straight from the git command line, or via the API.',
},
{
title: 'Revisions & diffs',
desc: 'Browse the full history of a snippet with diffs between every revision.',
},
{
title: 'Rich rendering',
desc: 'Syntax highlighting for hundreds of languages, plus Markdown with Mermaid, LaTeX, CSV and media files.',
},
{
title: 'Files & uploads',
desc: 'Multiple files per snippet, including binary files and images, with a download as ZIP.',
},
{
title: 'Likes, forks & topics',
desc: 'Star and fork snippets, and organize them with topics for easy discovery.',
},
{
title: 'Embed & JSON',
desc: 'Drop a snippet into any webpage with the embed widget, or fetch it as JSON.',
},
{
title: 'Full-text & code search',
desc: 'Search across snippets via code or metadata.',
},
{
title: 'OAuth, LDAP & passkeys',
desc: 'Log in with GitHub, GitLab, Gitea or OIDC, LDAP, and secure accounts with TOTP or WebAuthn.',
},
{
title: 'Admin & instance controls',
desc: 'Require login for a private instance, allow anonymous snippets, manage users, and expose Prometheus metrics.',
},
{
title: 'Self-hosted your way',
desc: 'A single binary, Docker image or K8s deployment, backed by SQLite, PostgreSQL or MySQL.',
},
]
</script>
<template>
<section class="og-features">
<div class="og-features__head">
<h2 class="og-features__title">Features</h2>
<p class="og-features__desc">
All you need to self-host, manage and share your snippets.
</p>
</div>
<ul class="og-features__grid">
<li v-for="f in features" :key="f.title" class="og-feature">
<h3 class="og-feature__title">{{ f.title }}</h3>
<p class="og-feature__desc">{{ f.desc }}</p>
</li>
</ul>
</section>
</template>
<style scoped>
.og-features {
border-bottom: var(--og-frame-border) solid var(--vp-c-divider);
}
.og-features__head {
padding: 3rem 1.5rem 2.5rem;
text-align: center;
}
.og-features__title {
margin: 0;
border: 0;
font-size: 1.8rem;
line-height: 1.2;
font-weight: 700;
letter-spacing: -0.01em;
}
.og-features__desc {
margin: 0.6rem 0 0;
font-size: 1rem;
color: var(--vp-c-text-2);
}
/* Flush grid: cells separated by the same frame border (no radius, no margin). */
.og-features__grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: var(--og-frame-border);
margin: 0;
padding: 0;
list-style: none;
background: var(--vp-c-divider);
border-top: var(--og-frame-border) solid var(--vp-c-divider);
}
.og-feature {
padding: 1.75rem;
background: var(--vp-c-bg);
}
.og-feature__title {
margin: 0;
border: 0;
font-size: 1.05rem;
font-weight: 600;
letter-spacing: -0.01em;
}
.og-feature__desc {
margin: 0.4rem 0 0;
font-size: 0.9rem;
line-height: 1.55;
color: var(--vp-c-text-2);
}
@media (max-width: 900px) {
.og-features__grid {
grid-template-columns: repeat(2, 1fr);
}
}
@media (max-width: 600px) {
.og-features__grid {
grid-template-columns: 1fr;
}
}
</style>
@@ -0,0 +1,338 @@
<script setup>
import { ref, computed, onMounted, onBeforeUnmount } from 'vue'
const version = 'v1.13'
const dockerCommand = 'docker run -p 6157:6157 -v "$HOME/.opengist:/opengist" ghcr.io/thomiceli/opengist:1.13'
const stars = ref(null)
const formattedStars = computed(() => {
const n = stars.value
if (n == null) return ''
// if (n >= 1000) return (n / 1000).toFixed(1).replace(/\.0$/, '') + 'k'
return String(n)
})
// Cache the star count in localStorage so we don't hit the GitHub API on every
// page load / navigation (unauthenticated API is rate-limited to 60/hour/IP).
const STARS_KEY = 'og-gh-stars'
const STARS_TTL = 6 * 60 * 60 * 1000 // 6 hours
onMounted(async () => {
try {
const cached = JSON.parse(localStorage.getItem(STARS_KEY) || 'null')
if (cached && Date.now() - cached.ts < STARS_TTL) {
stars.value = cached.value
return
}
} catch {}
try {
const res = await fetch('https://api.github.com/repos/thomiceli/opengist')
if (res.ok) {
stars.value = (await res.json()).stargazers_count
try {
localStorage.setItem(STARS_KEY, JSON.stringify({ value: stars.value, ts: Date.now() }))
} catch {}
}
} catch {}
})
const copied = ref(false)
let timer
const copy = async () => {
try {
await navigator.clipboard.writeText(dockerCommand)
copied.value = true
clearTimeout(timer)
timer = setTimeout(() => (copied.value = false), 1600)
} catch {}
}
onBeforeUnmount(() => clearTimeout(timer))
</script>
<template>
<section class="og-hero">
<a
class="og-badge"
href="https://github.com/thomiceli/opengist/releases"
target="_blank"
rel="noopener noreferrer"
>
<span class="og-badge__dot" />
{{ version }} released: Restful API + brand new docs page
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="og-badge__arrow">
<path stroke-linecap="round" stroke-linejoin="round" d="m9 6 6 6-6 6" />
</svg>
</a>
<img
class="og-logo"
src="https://raw.githubusercontent.com/thomiceli/opengist/master/public/img/opengist.svg"
alt="Opengist"
/>
<h1 class="og-title">
<span class="og-title__shine">Opengist</span>
</h1>
<p class="og-tagline">
Self-hosted and open-source pastebin powered by Git
</p>
<div class="og-actions">
<a class="og-btn og-btn--primary" href="/docs">Get started</a>
<a class="og-btn" href="https://demo.opengist.io" target="_blank" rel="noopener noreferrer">
Live demo
</a>
<a
class="og-btn og-btn--github"
href="https://github.com/thomiceli/opengist"
target="_blank"
rel="noopener noreferrer"
aria-label="Opengist on GitHub"
>
<svg class="og-btn__gh" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true">
<path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27s1.36.09 2 .27c1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.01 8.01 0 0 0 16 8c0-4.42-3.58-8-8-8" />
</svg>
<span v-if="formattedStars" class="og-btn__stars">
<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
<path d="m12 17.27 6.18 3.73-1.64-7.03L22 9.24l-7.19-.61L12 2 9.19 8.63 2 9.24l5.46 4.73L5.82 21z" />
</svg>
{{ formattedStars }}
</span>
</a>
</div>
<div class="og-install">
<span class="og-install__prompt">$</span>
<code class="og-install__cmd">{{ dockerCommand }}</code>
<button class="og-install__copy" type="button" aria-label="Copy command" @click="copy">
<svg v-if="copied" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="m5 13 4 4L19 7" />
</svg>
<svg v-else viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="9" y="9" width="11" height="11" rx="2" />
<path d="M5 15V5a2 2 0 0 1 2-2h10" />
</svg>
</button>
</div>
</section>
</template>
<style scoped>
.og-hero {
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
padding: 2.5rem 1.5rem 2rem;
border-bottom: 1px solid var(--vp-c-divider);
}
.og-badge {
display: inline-flex;
align-items: center;
gap: 0.5rem;
padding: 0.35rem 0.9rem;
border-radius: 999px;
font-size: 0.825rem;
font-weight: 500;
color: var(--vp-c-brand-1);
background: var(--vp-c-brand-soft);
transition: filter 0.2s ease;
}
.og-badge:hover {
filter: brightness(1.05);
}
.og-badge__dot {
width: 0.45rem;
height: 0.45rem;
border-radius: 999px;
background: var(--vp-c-brand-1);
}
.og-badge__arrow {
width: 0.85rem;
height: 0.85rem;
}
.og-logo {
width: 4.5rem;
height: 4.5rem;
margin: 2rem 0 1.25rem;
}
.og-title {
margin: 0;
font-size: clamp(1.25rem, 6vw, 2.75rem);
line-height: 1.18;
font-weight: 800;
letter-spacing: -0.02em;
}
.og-title__shine {
display: block;
background: linear-gradient(
110deg,
var(--vp-c-text-1) 0%,
var(--vp-c-text-1) 40%,
var(--vp-c-brand-1) 50%,
var(--vp-c-text-1) 60%,
var(--vp-c-text-1) 100%
);
background-size: 250% 100%;
background-position: 100% 0;
background-clip: text;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
animation: og-shine 6s ease-in-out 0.3s 1 forwards;
}
@keyframes og-shine {
to {
background-position: 0 0;
}
}
.og-tagline {
max-width: 34rem;
margin: 1.2rem 0 0;
font-size: 1.35rem;
line-height: 1;
color: var(--vp-c-text-1);
}
.og-license {
margin: 0.85rem 0 0;
font-size: 0.85rem;
color: var(--vp-c-text-3);
}
.og-actions {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: center;
gap: 0.75rem;
margin-top: 2rem;
}
.og-btn {
display: inline-flex;
align-items: center;
justify-content: center;
height: 2.875rem;
padding: 0 1.5rem;
border-radius: 0.625rem;
border: 1px solid var(--vp-c-divider);
background: var(--vp-c-bg-soft);
color: var(--vp-c-text-1);
font-size: 0.95rem;
font-weight: 600;
transition: border-color 0.2s ease, background-color 0.2s ease, color 0.2s ease;
}
.og-btn:hover {
border-color: var(--vp-c-brand-1);
color: var(--vp-c-brand-1);
}
.og-btn--primary {
border-color: transparent;
background: var(--vp-c-brand-3);
color: #fff;
}
.og-btn--primary:hover {
background: var(--vp-c-brand-2);
color: #fff;
}
.og-btn--icon {
width: 2.875rem;
padding: 0;
}
.og-btn--icon svg {
width: 1.35rem;
height: 1.35rem;
}
/* GitHub button with star count. */
.og-btn--github {
gap: 0.6rem;
padding: 0 1.1rem;
}
.og-btn__gh {
width: 1.35rem;
height: 1.35rem;
}
.og-btn__stars {
display: inline-flex;
align-items: center;
gap: 0.3rem;
padding-left: 0.6rem;
border-left: 1px solid var(--vp-c-divider);
font-variant-numeric: tabular-nums;
}
.og-btn__stars svg {
width: 0.95rem;
height: 0.95rem;
color: #e3b341;
}
.og-install {
display: flex;
align-items: center;
gap: 0.75rem;
max-width: 100%;
margin-top: 2rem;
padding: 0.6rem 0.6rem 0.6rem 1rem;
border-radius: 0.75rem;
border: 1px solid var(--vp-c-divider);
background: var(--vp-c-bg-alt);
font-family: var(--vp-font-family-mono);
}
.og-install__prompt {
color: var(--vp-c-brand-1);
user-select: none;
}
.og-install__cmd {
flex: 1;
min-width: 0; /* allow the long command to shrink + scroll instead of overflowing */
overflow-x: auto;
white-space: nowrap;
font-size: 0.85rem;
color: var(--vp-c-text-1);
background: transparent;
padding: 0;
}
.og-install__copy {
flex-shrink: 0;
display: inline-flex;
align-items: center;
justify-content: center;
width: 2.1rem;
height: 2.1rem;
border-radius: 0.5rem;
color: var(--vp-c-text-2);
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
transition: color 0.2s ease, border-color 0.2s ease;
}
.og-install__copy:hover {
color: var(--vp-c-brand-1);
border-color: var(--vp-c-brand-1);
}
.og-install__copy svg {
width: 1.05rem;
height: 1.05rem;
}
@media (max-width: 640px) {
.og-hero {
padding: 2.5rem 1.25rem 2.5rem;
}
.og-install {
width: 100%;
}
}
@media (min-width: 640px) {
.og-hero {
padding-top: 3.5rem;
}
}
</style>
@@ -0,0 +1,75 @@
<script setup>
import { useData, withBase } from 'vitepress'
const { isDark } = useData()
</script>
<template>
<section class="og-showcase">
<div class="og-showcase__text">
<h2 class="og-showcase__title">A clean, familiar interface</h2>
<p class="og-showcase__desc">
Create, browse and share snippets with syntax highlighting, revisions and more.
</p>
</div>
<div class="og-showcase__frame">
<img
v-show="!isDark"
class="og-showcase__img"
:src="withBase('/opengist-demo.png')"
alt="Opengist web interface"
/>
<img
v-show="isDark"
class="og-showcase__img"
:src="withBase('/opengist-demo-dark.png')"
alt="Opengist web interface"
/>
</div>
</section>
</template>
<style scoped>
.og-showcase {
padding: 3rem 1.5rem;
text-align: center;
}
@media (max-width: 640px) {
.og-showcase {
padding: 2rem 1.25rem;
}
}
.og-showcase__text {
max-width: 34rem;
margin: 0 auto 2rem;
}
.og-showcase__title {
margin: 0;
border: 0;
font-size: 1.6rem;
line-height: 1.2;
font-weight: 700;
letter-spacing: -0.01em;
}
.og-showcase__desc {
margin: 0.5rem 0 0;
font-size: 0.95rem;
color: var(--vp-c-text-2);
}
/* Smaller, centered screenshot frame. */
.og-showcase__frame {
max-width: 72rem;
margin: 0 auto;
border-radius: 0.75rem;
overflow: hidden;
}
.og-showcase__img {
display: block;
width: 100%;
height: auto;
padding: 0;
}
</style>
+6 -1
View File
@@ -2,11 +2,16 @@ import { h } from 'vue'
import type { Theme } from 'vitepress'
import DefaultTheme from 'vitepress/theme'
import Layout from "./Layout.vue";
import OpenApiOperation from "./components/OpenApiOperation.vue";
import OpenApiSchema from "./components/OpenApiSchema.vue";
import './style.css'
import './openapi.css'
export default {
...DefaultTheme,
Layout,
enhanceApp({ app, router, siteData }) {
// ...
app.component('OpenApiOperation', OpenApiOperation)
app.component('OpenApiSchema', OpenApiSchema)
}
} satisfies Theme
+45
View File
@@ -0,0 +1,45 @@
<script setup>
import Hero from '../components/home/Hero.vue'
import Showcase from '../components/home/Showcase.vue'
import Features from '../components/home/Features.vue'
// Add further sections here as the landing page grows (e.g. Features, FAQ).
</script>
<template>
<main class="og-home">
<div class="og-wrapper">
<Hero />
<Features />
<Showcase />
<!-- Empty growing section so the side borders run to the bottom. -->
<div class="og-fill" />
</div>
</main>
</template>
<style scoped>
.og-home {
width: 100%;
}
/* Centered content column framed with vertical borders (vite-plus style).
Sections inside add their own border-bottom to act as horizontal rules.
Width matches the navbar container so the home content aligns with the header. */
.og-wrapper {
display: flex;
flex-direction: column;
max-width: var(--vp-layout-max-width);
margin: 0 auto;
/* Fill the viewport below the navbar so the side borders run to the bottom
even when the content is shorter than the screen. */
min-height: calc(100vh - var(--vp-nav-height));
/* Gap inside the frame, so the left/right borders stay visible through it. */
padding-bottom: 2rem;
border-inline: var(--og-frame-border) solid var(--vp-c-divider);
}
/* Empty section that absorbs the remaining height, extending the side borders. */
.og-fill {
flex: 1;
}
</style>
+138
View File
@@ -0,0 +1,138 @@
/* Shared styling for the generated API reference components. */
.oas-op h3,
.oas-schema h3 {
display: flex;
align-items: center;
gap: 0.6rem;
}
.oas-method {
font-size: 0.72rem;
font-weight: 700;
letter-spacing: 0.04em;
padding: 0.15rem 0.5rem;
border-radius: 5px;
color: #fff;
}
.oas-method.m-get { background: #1f7a4d; }
.oas-method.m-post { background: #1860c2; }
.oas-method.m-put { background: #b06800; }
.oas-method.m-patch { background: #8754c9; }
.oas-method.m-delete { background: #c0392b; }
/* Endpoint shown as a code block: method badge on the left, then the path. */
.oas-endpoint {
display: flex;
align-items: center;
gap: 0.75rem;
margin: 0.75rem 0 1.5rem;
padding: 0.6rem 0.9rem;
background: var(--vp-c-bg-alt);
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
overflow-x: auto;
}
/* Plain path text (no inline-code bg/color); only `.oas-param` is highlighted. */
.vp-doc code.oas-path {
font-size: 0.95rem;
background: none;
color: var(--vp-c-text-1);
padding: 0;
white-space: nowrap;
}
/* Highlight `{param}` segments within the path. */
.oas-param {
color: var(--vp-c-brand-1);
background: var(--vp-c-default-soft);
padding: 0 0.25rem;
border-radius: 4px;
}
.oas-req {
margin-left: 0.4rem;
font-size: 0.7rem;
font-weight: 600;
color: var(--vp-c-red-1);
text-transform: uppercase;
}
.oas-opt {
margin-left: 0.4rem;
font-size: 0.7rem;
font-weight: 600;
color: var(--vp-c-text-3);
text-transform: uppercase;
}
.oas-ct {
margin-left: 0.5rem;
font-size: 0.8rem;
color: var(--vp-c-text-2);
}
/* `.vp-doc table` is display:block / fit-content, so force full-width here. */
.vp-doc table.oas-table {
display: table;
width: 100%;
margin: 0.5rem 0 1rem;
}
.oas-table th { text-align: left; }
.oas-status { font-weight: 700; }
.oas-status.s-2 { color: #1f7a4d; }
.oas-status.s-4,
.oas-status.s-5 { color: #c0392b; }
.oas-headers td {
padding-top: 0;
font-size: 0.85rem;
color: var(--vp-c-text-2);
}
.oas-hlabel { margin-right: 0.4rem; }
.oas-header { margin-right: 0.4rem; }
.oas-includes a { margin-right: 0.5rem; }
.oas-note { font-size: 0.85rem; color: var(--vp-c-text-2); }
.oas-bodyhead { display: flex; align-items: center; gap: 0.5rem; }
.oas-field { display: inline-flex; align-items: center; gap: 0.35rem; }
.oas-nest { color: var(--vp-c-text-3); }
.oas-nested { background: var(--vp-c-bg-soft); }
/* ---- three-pane endpoint layout (pages with pageClass: api-page) ---- */
.api-page .VPDoc .container { max-width: 1180px; }
.api-page .VPDoc.has-sidebar .content { max-width: none; }
.api-page .VPDoc .content-container { max-width: none; }
.oas-op-grid {
display: grid;
grid-template-columns: minmax(0, 1fr) 26rem;
gap: 2.5rem;
align-items: start;
}
.oas-main { min-width: 0; }
.oas-main h1 { margin-top: 0; }
.oas-code-sticky {
position: sticky;
top: calc(var(--vp-nav-height) + 1.5rem);
}
.oas-code-label {
display: flex;
align-items: center;
gap: 0.5rem;
margin: 1rem 0 0.4rem;
font-size: 0.75rem;
font-weight: 600;
letter-spacing: 0.04em;
text-transform: uppercase;
color: var(--vp-c-text-2);
}
.oas-code-label:first-child { margin-top: 0; }
/* Smaller, denser code in the side panel. VitePress applies
`font-size: var(--vp-code-font-size)` to the <code>, so override the var. */
.oas-code {
--vp-code-font-size: 0.75rem;
--vp-code-line-height: 1.5;
}
.oas-code-block [class*='language-'] { margin: 0; }
.oas-code-block pre {
white-space: pre;
padding: 14px 16px;
}
/* Collapse to a single column when there isn't room for the side panel. */
@media (max-width: 1280px) {
.oas-op-grid { grid-template-columns: 1fr; }
.oas-code-sticky { position: static; }
}
+184 -7
View File
@@ -1,3 +1,22 @@
@import "tailwindcss";
/* Map Tailwind's `dark:` variant onto VitePress's `.dark` class. */
@custom-variant dark (&:where(.dark, .dark *));
/* Opengist brand gray palette (replaces Tailwind's default gray). */
@theme {
--color-gray-50: #EEEFF1;
--color-gray-100: #DEDFE3;
--color-gray-200: #BABCC5;
--color-gray-300: #999CA8;
--color-gray-400: #75798A;
--color-gray-500: #585B68;
--color-gray-600: #464853;
--color-gray-700: #363840;
--color-gray-800: #232429;
--color-gray-900: #131316;
}
/**
* Customize default theme styling by overriding CSS variables:
* https://github.com/vuejs/vitepress/blob/main/src/client/theme-default/styles/vars.css
@@ -49,10 +68,11 @@
--vp-c-default-3: var(--vp-c-gray-3);
--vp-c-default-soft: var(--vp-c-gray-soft);
--vp-c-brand-1: var(--vp-c-indigo-1);
--vp-c-brand-2: var(--vp-c-indigo-2);
--vp-c-brand-3: var(--vp-c-indigo-3);
--vp-c-brand-soft: var(--vp-c-indigo-soft);
/* Opengist brand blue (matches --color-primary-* in the app). */
--vp-c-brand-1: #3c79e2;
--vp-c-brand-2: #356fc0;
--vp-c-brand-3: #3c79e2;
--vp-c-brand-soft: rgba(60, 121, 226, 0.14);
--vp-c-tip-1: var(--vp-c-brand-1);
--vp-c-tip-2: var(--vp-c-brand-2);
@@ -70,6 +90,19 @@
--vp-c-danger-soft: var(--vp-c-red-soft);
}
/* Dark theme background (brand gray-900). */
.dark {
--vp-c-bg: #131316;
--vp-c-bg-alt: #0f0f12;
--vp-c-bg-elv: #1a1a1e;
/* Lighter brand blue for contrast on dark backgrounds. */
--vp-c-brand-1: #74a4f6;
--vp-c-brand-2: #588fee;
--vp-c-brand-3: #3c79e2;
--vp-c-brand-soft: rgba(116, 164, 246, 0.16);
}
/**
* Component: Button
* -------------------------------------------------------------------------- */
@@ -142,6 +175,150 @@
height: 108px;
}
@tailwind base;
@tailwind components;
@tailwind utilities;
/* Home: drop VitePress's bottom margin on the home container so our framed
wrapper (and its side borders) reaches the very bottom of the page. */
.VPHome {
margin-bottom: 0 !important;
}
/**
* Navbar: left-align the menu (Docs / Resources) next to the logo,
* keeping the appearance toggle and social links on the right.
* -------------------------------------------------------------------------- */
@media (min-width: 768px) {
/* Drop VitePress's `margin-right: -100vw; padding-right: 100vw` background
bleed: it makes the flex free space negative, which neutralises the auto
margin below. The bar's own background covers the width anyway. */
.VPNavBar .content-body {
margin-right: 0 !important;
padding-right: 0 !important;
}
/* The (empty) search slot has flex-grow:1 and would eat all the free space,
leaving none for the menu's auto margin. Stop it from growing. */
.VPNavBar .VPNavBarSearch {
flex-grow: 0 !important;
}
.VPNavBar .VPNavBarMenu {
margin-right: auto !important;
}
}
/* Keep the navbar identical on the landing page and the docs pages:
VitePress otherwise makes it transparent and borderless on the home page. */
.VPNavBar {
background-color: var(--vp-nav-bg-color) !important;
}
/* Bottom border: match the side borders' style (2px, --vp-c-divider) and keep
it the same width as the container instead of spanning the whole page. */
.VPNavBar .divider {
max-width: var(--vp-layout-max-width);
margin-inline: auto;
}
.VPNavBar .divider-line {
height: var(--og-frame-border);
background-color: var(--vp-c-divider) !important;
}
/* Drop the bottom border under the logo/title (shown on docs pages). */
.VPNavBarTitle.has-sidebar .title {
border-bottom-color: transparent !important;
}
/* Remove the small vertical divider lines between the menu, theme toggle
(.appearance) and social links in the navbar. */
.VPNavBar .menu + .translations::before,
.VPNavBar .menu + .appearance::before,
.VPNavBar .menu + .social-links::before,
.VPNavBar .translations + .appearance::before,
.VPNavBar .appearance + .social-links::before {
display: none !important;
}
/* The dividers also provided the spacing between modules; add it back. */
.VPNavBar .appearance,
.VPNavBar .social-links {
margin-left: 12px;
}
/* Match the home content layout: full --vp-layout-max-width container centered
(native margin: 0 auto), with no side padding on the wrapper. The container
carries the same vertical borders as the home .og-wrapper, with 32px of
inner padding so the nav content sits inside the borders. */
.VPNavBar .wrapper {
padding-inline: 0 !important;
}
.VPNavBar .container {
max-width: var(--vp-layout-max-width) !important;
padding-inline: 32px;
border-inline: var(--og-frame-border) solid var(--vp-c-divider);
}
/* On docs pages the navbar gets `.has-sidebar`, which offsets the title to the
sidebar width and changes the container. Undo it so the navbar lays out the
same as on the landing page (centered, max-width container). */
@media (min-width: 960px) {
.VPNavBar.has-sidebar .wrapper {
padding: 0 !important;
}
.VPNavBar.has-sidebar .container {
max-width: var(--vp-layout-max-width) !important;
}
.VPNavBar.has-sidebar .title {
position: static !important;
width: auto !important;
padding: 0 !important;
}
.VPNavBar.has-sidebar .content {
padding-left: 0 !important;
padding-right: 0 !important;
}
.VPNavBar.has-sidebar .divider {
padding-left: 0 !important;
}
}
/* Horizontal section bar (Guide / API) below the top navbar. Desktop only;
on mobile the top-nav menu provides section switching. */
:root {
--og-bar-height: 48px;
/* Frame border width, between 1px and 2px without using px (0.1rem ≈ 1.6px). */
--og-frame-border: 0.09rem;
}
@media (min-width: 960px) {
/* Push the sidebar, main content and outline down by the bar height. */
.VPSidebar {
padding-top: calc(var(--vp-nav-height) + var(--og-bar-height)) !important;
}
.VPContent.has-sidebar {
padding-top: calc(var(--vp-nav-height) + var(--og-bar-height)) !important;
}
.VPDoc {
--vp-doc-top-height: var(--og-bar-height);
}
}
/* Continue the navbar/home frame down the docs pages: the sidebar gets the
outer-left border and the sidebar/content divider, and the content (.VPDoc)
gets the outer-right border. Same style as the rest of the frame. */
@media (min-width: 960px) {
.VPSidebar {
border-left: var(--og-frame-border) solid var(--vp-c-divider);
border-right: var(--og-frame-border) solid var(--vp-c-divider);
}
.VPDoc {
border-right: var(--og-frame-border) solid var(--vp-c-divider);
}
}
/* At >=1440px the layout is centered, so pull the sidebar box in to start at the
frame's left edge (same X as the navbar's left border) instead of the viewport
edge. Content/nav positions are unchanged; only the box origin and width move. */
@media (min-width: 1440px) {
.VPSidebar {
left: calc((100% - var(--vp-layout-max-width)) / 2) !important;
width: var(--vp-sidebar-width) !important;
padding-left: 32px !important;
}
}
+114
View File
@@ -0,0 +1,114 @@
---
aside: true
---
# Opengist API Reference
Opengist exposes a REST API authenticated with Personal Access Tokens, intended
for programmatic access to gist and user resources.
The base URL for the API is
```
https://opengist.example.com/api/
```
> OpenAPI 3.1 spec is available at
> [`openapi.yaml`](api/openapi.yaml).
>
> A running instance also serves the raw spec at `GET /api/openapi.yaml`.
## Getting an access token
The API authenticates with a Personal Access Token. To create one:
1. Go to **Settings**
2. Select the **Access Tokens** menu
3. Choose a name, select the scopes the token should grant and an optional
expiration date, then click **Create Access Token**
4. Copy the token (starting with `og_`). It is shown only once.
Tokens carry per-resource scopes, each at read or read/write level:
| Scope | Grants |
|-------|--------|
| `gist:read` | Read gists, including the caller's private and unlisted ones |
| `gist:write` | Create, update, delete and fork gists |
| `user:read` | Read the authenticated user's account |
| `user:write` | Update the authenticated user and toggle likes |
## Authentication
Send the token in the `Authorization` header using the `Bearer` scheme:
```
Authorization: Bearer og_xxxxxxxx
```
Each endpoint documents the scope it requires in its **Headers** section.
Note that every endpoint requires authentication when an admin enables the "Require login" setting, which both works for the API and the web interface.
The single gist endpoints are available without authentication when an admin enables "Allow individual gists without login" setting.
## Schema
The API lives under the `/api/` prefix on the same host as your Opengist
instance. All data is sent and received as JSON.
Every endpoint responds with JSON unless specified otherwise
```
Content-Type: application/json
```
All timestamps are returned in ISO 8601 / RFC 3339 format, in UTC:
```
2024-01-01T00:00:00Z
```
## Pagination
List endpoints (such as `GET /gists`) return a JSON array and page the results.
Tune the window with these query parameters:
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `page` | integer | `1` | Page number, 1-based. |
| `per_page` | integer | `30` | Items per page (maximum `100`). |
| `since` | string (date-time) | — | Gist lists only: return only gists updated at or after this RFC 3339 timestamp. |
Pagination metadata is returned in the response headers:
| Header | Description |
|--------|-------------|
| `Link` | RFC 5988 links to other pages: `rel="next"` (when more pages exist) and `rel="prev"` (when `page > 1`). |
| `X-Page` | The current page number (1-based). |
| `X-Per-Page` | Items per page. |
| `X-Total` | Total number of items across all pages. |
| `X-Total-Pages` | Total number of pages, i.e. `ceil(total / per_page)`. |
The `Link` header is formatted as follows:
```
Link: <https://opengist.example/api/gists?page=2&per_page=30>; rel="next",
<https://opengist.example/api/gists?page=1&per_page=30>; rel="prev"
```
## Disabling the API
The API is **enabled by default**. To disable it, define it in the config as follows
### YAML
```yaml
api.enabled: false
```
### Environment variable
```shell
OG_API_ENABLED=false
```
While disabled, the routing layer returns `403` for every endpoint until it is
enabled again. Disabling does not revoke issued tokens — they resume working
once the API is turned back on.
+10
View File
@@ -0,0 +1,10 @@
---
aside: false
pageClass: api-page
---
<script setup>
import { useData } from 'vitepress'
const { params } = useData()
</script>
<OpenApiOperation :id="params.id" />
+10
View File
@@ -0,0 +1,10 @@
import { listOperations } from '../.vitepress/openapi'
// One generated page per operation, keyed by operationId.
export default {
paths() {
return listOperations().map((op) => ({
params: { operation: op.id, id: op.id, title: `${op.method} ${op.path}` }
}))
}
}
+9
View File
@@ -0,0 +1,9 @@
---
aside: false
---
<script setup>
import { useData } from 'vitepress'
const { params } = useData()
</script>
<OpenApiSchema :name="params.name" />
+8
View File
@@ -0,0 +1,8 @@
import { listSchemas } from '../../.vitepress/openapi'
// One generated page per component schema, keyed by its name.
export default {
paths() {
return listSchemas().map((name) => ({ params: { schema: name, name } }))
}
}
+6
View File
@@ -0,0 +1,6 @@
---
description: Release notes and version history for Opengist — new features, fixes and changes in every release.
outline: [2, 2]
---
<!--@include: ../CHANGELOG.md-->
+4 -1
View File
@@ -15,11 +15,13 @@ aside: false
| index | OG_INDEX | `bleve` | Define the code indexer (either `bleve`, `meilisearch`, or empty for no index). |
| index.meili.host | OG_MEILI_HOST | none | Set the host for the Meiliseach server. |
| index.meili.api-key | OG_MEILI_API_KEY | none | Set the API key for the Meiliseach server. |
| search.default | OG_SEARCH_DEFAULT | `content` | Set the default search fields. Can contain multiple fields (e.g., `content,username`). Fields: `content,user,title,description,filename,extension,language,topic`. |
| git.default-branch | OG_GIT_DEFAULT_BRANCH | none | Default branch name used by Opengist when initializing Git repositories. If not set, uses the Git default branch name. More info [here](https://git-scm.com/book/en/v2/Getting-Started-First-Time-Git-Setup#_new_default_branch) |
| sqlite.journal-mode | OG_SQLITE_JOURNAL_MODE | `WAL` | Set the journal mode for SQLite. More info [here](https://www.sqlite.org/pragma.html#pragma_journal_mode) |
| http.host | OG_HTTP_HOST | `0.0.0.0` | The host on which the HTTP server should bind. Use an IP address for network binding. Use a path for Unix socket binding (e.g. /run/opengist.sock) |
| http.port | OG_HTTP_PORT | `6157` | The port on which the HTTP server should listen. |
| http.git-enabled | OG_HTTP_GIT_ENABLED | `true` | Enable or disable git operations (clone, pull, push) via HTTP. (`true` or `false`) |
| api.enabled | OG_API_ENABLED | `true` | Enable or disable the REST API. (`true` or `false`) |
| unix-socket-permissions | OG_UNIX_SOCKET_PERMISSIONS | `0666` | File permissions for Unix socket (octal format). |
| metrics.enabled | OG_METRICS_ENABLED | `false` | Enable or disable Prometheus metrics server (`true` or `false`) |
| metrics.host | OG_METRICS_HOST | `0.0.0.0` | The host on which the metrics server should bind. |
@@ -28,7 +30,6 @@ aside: false
| ssh.host | OG_SSH_HOST | `0.0.0.0` | The host on which the SSH server should bind. |
| ssh.port | OG_SSH_PORT | `2222` | The port on which the SSH server should listen. |
| ssh.external-domain | OG_SSH_EXTERNAL_DOMAIN | none | Public domain for the Git SSH connection, if it has to be different from the HTTP one. If not set, uses the URL from the request. |
| ssh.keygen-executable | OG_SSH_KEYGEN_EXECUTABLE | `ssh-keygen` | Path to the SSH key generation executable. |
| github.client-key | OG_GITHUB_CLIENT_KEY | none | The client key for the GitHub OAuth application. |
| github.secret | OG_GITHUB_SECRET | none | The secret for the GitHub OAuth application. |
| gitlab.client-key | OG_GITLAB_CLIENT_KEY | none | The client key for the GitLab OAuth application. |
@@ -43,6 +44,8 @@ aside: false
| oidc.client-key | OG_OIDC_CLIENT_KEY | none | The client key for the OpenID application. |
| oidc.secret | OG_OIDC_SECRET | none | The secret for the OpenID application. |
| oidc.discovery-url | OG_OIDC_DISCOVERY_URL | none | Discovery endpoint of the OpenID provider. |
| oidc.group-claim-name | OG_OIDC_GROUP_CLAIM_NAME | none | Name of the claim containing the groups. |
| oidc.admin-group | OG_OIDC_ADMIN_GROUP | none | Name of the group that should receive admin rights. |
| ldap.url | OG_LDAP_URL | none | URL of the LDAP instance; if not set, LDAP authentication is disabled |
| ldap.bind-dn | OG_LDAP_BIND_DN | none | Bind DN to authenticate against the LDAP. e.g: cn=read-only-admin,dc=example,dc=com |
| ldap.bind-credentials | OG_LDAP_BIND_CREDENTIALS | none | The password for the Bind DN. |
+2 -2
View File
@@ -27,7 +27,7 @@ Usage via command line :
./opengist --config /path/to/config.yml
```
You can start by copying and/or modifying the provided [config.yml](https://github.com/thomiceli/opengist/blob/stable/config.yml) file.
You can start by copying and/or modifying the provided [config.yml](https://github.com/thomiceli/opengist/blob/master/config.yml) file.
## Configuration via Environment Variables
@@ -69,4 +69,4 @@ services:
secrets:
opengist_secrets:
file: ./secrets.env
```
```
+3 -1
View File
@@ -1,4 +1,6 @@
---
layout: home
navbar: false
title: Opengist — self-hosted pastebin powered by Git
titleTemplate: false
description: Opengist is an open-source, self-hosted pastebin powered by Git — a lightweight alternative to GitHub Gist that you host and fully control.
---
+5
View File
@@ -1,7 +1,12 @@
---
description: Install Opengist with Docker, Kubernetes, a prebuilt binary, or from source — pick the method that fits your environment.
---
# Install Opengist
There are several ways to install Opengist, depending on your preferences and your environment.
- [Docker](installation/docker.md)
- [Kubernetes](installation/kubernetes.md)
- [Source](installation/source.md)
- [Binary](installation/binary.md)
+2 -2
View File
@@ -4,9 +4,9 @@ Download the archive for your system from the release page [here](https://github
```shell
# example for linux amd64
wget https://github.com/thomiceli/opengist/releases/download/v1.12.1/opengist1.12.1-linux-amd64.tar.gz
wget https://github.com/thomiceli/opengist/releases/download/v1.13.1/opengist1.13.1-linux-amd64.tar.gz
tar xzvf opengist1.12.1-linux-amd64.tar.gz
tar xzvf opengist1.13.1-linux-amd64.tar.gz
cd opengist
chmod +x opengist
./opengist # with or without `--config config.yml`
+25
View File
@@ -39,3 +39,28 @@ services:
UID: 1001
GID: 1001
```
## Rootless
By default the container starts as `root` and the entrypoint drops privileges to the
user defined by `UID`/`GID` (see above).
If you'd rather have the container run as a
non-root user from the start — for example with `user:` in Compose, or under rootless
Docker/Podman — set the `user` key instead:
```yml
services:
opengist:
# ...
user: "1001:1001"
volumes:
- "./opengist-data:/opengist"
```
In this mode the entrypoint runs Opengist directly as that user.
Create the Opengist data directory and own it on the host first:
```shell
mkdir -p ./opengist-data && sudo chown -R 1001:1001 ./opengist-data
```
+1 -1
View File
@@ -10,7 +10,7 @@ Requirements:
git clone https://github.com/thomiceli/opengist
cd opengist
git checkout v1.12.1 # optional, to checkout the latest release
git checkout v1.13.1
make
./opengist
+4
View File
@@ -1,3 +1,7 @@
---
description: Opengist is a self-hosted pastebin powered by Git. Snippets are stored in Git repositories and managed via standard Git commands or the web interface.
---
# Opengist
<img height="108px" src="https://raw.githubusercontent.com/thomiceli/opengist/master/public/img/opengist.svg" alt="Opengist" align="right" />
+3367
View File
File diff suppressed because it is too large Load Diff
+19
View File
@@ -0,0 +1,19 @@
{
"name": "opengist-docs",
"version": "0.0.0",
"private": true,
"description": "Documentation site for Opengist, built with VitePress.",
"type": "module",
"scripts": {
"dev": "vitepress dev",
"build": "vitepress build",
"preview": "vitepress preview"
},
"devDependencies": {
"@tailwindcss/vite": "^4.2.4",
"tailwindcss": "^4.2.4",
"vitepress": "^2.0.0-alpha.17",
"vue": "^3.5.27",
"yaml": "^2.9.0"
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 702 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 MiB

After

Width:  |  Height:  |  Size: 666 KiB

+4
View File
@@ -0,0 +1,4 @@
User-agent: *
Allow: /
Sitemap: https://opengist.io/sitemap.xml
+2 -2
View File
@@ -27,9 +27,9 @@ Stop the running instance; then like your first installation of Opengist, downlo
```shell
# example for linux amd64
wget https://github.com/thomiceli/opengist/releases/download/v1.12.1/opengist1.12.1-linux-amd64.tar.gz
wget https://github.com/thomiceli/opengist/releases/download/v1.13.1/opengist1.13.1-linux-amd64.tar.gz
tar xzvf opengist1.12.1-linux-amd64.tar.gz
tar xzvf opengist1.13.1-linux-amd64.tar.gz
cd opengist
chmod +x opengist
./opengist # with or without `--config config.yml`
+17
View File
@@ -1,5 +1,10 @@
# Embed a Gist to your webpage
> [!Tip]
> Fancy to enforce light or dark mode on the embedded Gist?
> Just append `?light` or `?dark` to the Gist-URL.
> Omitting this parameter will cause OpenGist to fallback to `auto`, thus the Browser deciding on the users preference.
To embed a Gist to your webpage, you can add a script tag with the URL of your gist followed by `.js` to your HTML page:
```html
@@ -7,5 +12,17 @@ To embed a Gist to your webpage, you can add a script tag with the URL of your g
<!-- Dark mode: -->
<script src="http://opengist.url/user/gist-url.js?dark"></script>
<!-- Light mode: -->
<script src="http://opengist.url/user/gist-url.js?light"></script>
```
If you have a Gist that holds several different files, you can also explicitely call a specific file by its filename:
```html
<script src="http://opengist.url/user/gist-url.js?file=filename"></script>
<!-- Dark mode: -->
<script src="http://opengist.url/user/gist-url.js?file=filename&dark"></script>
<!-- Light mode: -->
<script src="http://opengist.url/user/gist-url.js?file=filename&light"></script>
```
+17
View File
@@ -30,3 +30,20 @@ git push -o visibility=public
git push -o visibility=unlisted
git push -o visibility=private
```
## Change topics
```shell
git push -o topics="golang devops"
```
## Set expiration
Only applies when creating a gist. The value is either a preset
(`1hour`, `12hours`, `1day`, `7days`, `15days`) or a custom date
(RFC3339, e.g. `2026-01-02T15:04:05Z`).
```shell
git push -o expire=1day
git push -o expire=2026-01-02T15:04:05Z
```
+58 -55
View File
@@ -1,36 +1,39 @@
module github.com/thomiceli/opengist
go 1.25.5
go 1.26.4
require (
github.com/Kunde21/markdownfmt/v3 v3.1.0
github.com/alecthomas/chroma/v2 v2.23.1
github.com/blevesearch/bleve/v2 v2.5.7
github.com/alecthomas/chroma/v2 v2.26.1
github.com/blevesearch/bleve/v2 v2.6.0
github.com/dustin/go-humanize v1.0.1
github.com/gabriel-vasile/mimetype v1.4.12
github.com/gabriel-vasile/mimetype v1.4.13
github.com/glebarez/sqlite v1.11.0
github.com/go-ldap/ldap/v3 v3.4.12
github.com/go-playground/validator/v10 v10.30.1
github.com/go-webauthn/webauthn v0.15.0
github.com/go-ldap/ldap/v3 v3.4.13
github.com/go-playground/validator/v10 v10.30.3
github.com/go-webauthn/webauthn v0.17.4
github.com/google/uuid v1.6.0
github.com/gorilla/schema v1.4.1
github.com/gorilla/securecookie v1.1.2
github.com/gorilla/sessions v1.4.0
github.com/labstack/echo-contrib v0.17.4
github.com/labstack/echo/v4 v4.15.0
github.com/labstack/echo-contrib v0.50.1
github.com/labstack/echo/v4 v4.15.2
github.com/markbates/goth v1.82.0
github.com/meilisearch/meilisearch-go v0.36.0
github.com/meilisearch/meilisearch-go v0.36.3
github.com/oauth2-proxy/mockoidc v0.0.0-20240214162133-caebfff84d25
github.com/pquerna/otp v1.5.0
github.com/prometheus/client_golang v1.23.2
github.com/rs/zerolog v1.34.0
github.com/robfig/cron/v3 v3.0.1
github.com/rs/zerolog v1.35.1
github.com/stretchr/testify v1.11.1
github.com/urfave/cli/v2 v2.27.7
github.com/yuin/goldmark v1.7.16
github.com/yuin/goldmark v1.8.2
github.com/yuin/goldmark-emoji v1.0.6
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc
go.abhg.dev/goldmark/mermaid v0.6.0
golang.org/x/crypto v0.47.0
golang.org/x/text v0.33.0
golang.org/x/crypto v0.53.0
golang.org/x/oauth2 v0.36.0
golang.org/x/text v0.38.0
gopkg.in/yaml.v3 v3.0.1
gorm.io/driver/mysql v1.6.0
gorm.io/driver/postgres v1.6.0
@@ -38,88 +41,88 @@ require (
)
require (
filippo.io/edwards25519 v1.1.0 // indirect
github.com/Azure/go-ntlmssp v0.1.0 // indirect
github.com/RoaringBitmap/roaring/v2 v2.14.4 // indirect
github.com/andybalholm/brotli v1.2.0 // indirect
filippo.io/edwards25519 v1.2.0 // indirect
github.com/Azure/go-ntlmssp v0.1.1 // indirect
github.com/RoaringBitmap/roaring/v2 v2.18.2 // indirect
github.com/andybalholm/brotli v1.2.1 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/bits-and-blooms/bitset v1.24.4 // indirect
github.com/blevesearch/bleve_index_api v1.3.1 // indirect
github.com/blevesearch/geo v0.2.4 // indirect
github.com/blevesearch/go-faiss v1.0.27 // indirect
github.com/bits-and-blooms/bitset v1.24.5 // indirect
github.com/blevesearch/bleve_index_api v1.3.12 // indirect
github.com/blevesearch/geo v0.2.5 // indirect
github.com/blevesearch/go-faiss v1.1.4 // indirect
github.com/blevesearch/go-porterstemmer v1.0.3 // indirect
github.com/blevesearch/gtreap v0.1.1 // indirect
github.com/blevesearch/mmap-go v1.2.0 // indirect
github.com/blevesearch/scorch_segment_api/v2 v2.4.1 // indirect
github.com/blevesearch/scorch_segment_api/v2 v2.4.7 // indirect
github.com/blevesearch/segment v0.9.1 // indirect
github.com/blevesearch/snowballstem v0.9.0 // indirect
github.com/blevesearch/upsidedown_store_api v1.0.2 // indirect
github.com/blevesearch/vellum v1.2.0 // indirect
github.com/blevesearch/zapx/v11 v11.4.2 // indirect
github.com/blevesearch/zapx/v12 v12.4.2 // indirect
github.com/blevesearch/zapx/v13 v13.4.2 // indirect
github.com/blevesearch/zapx/v14 v14.4.2 // indirect
github.com/blevesearch/zapx/v15 v15.4.2 // indirect
github.com/blevesearch/zapx/v16 v16.3.0 // indirect
github.com/blevesearch/zapx/v11 v11.4.3 // indirect
github.com/blevesearch/zapx/v12 v12.4.3 // indirect
github.com/blevesearch/zapx/v13 v13.4.3 // indirect
github.com/blevesearch/zapx/v14 v14.4.3 // indirect
github.com/blevesearch/zapx/v15 v15.4.3 // indirect
github.com/blevesearch/zapx/v16 v16.3.4 // indirect
github.com/blevesearch/zapx/v17 v17.1.6 // indirect
github.com/boombuler/barcode v1.1.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/clipperhouse/stringish v0.1.1 // indirect
github.com/clipperhouse/uax29/v2 v2.4.0 // indirect
github.com/clipperhouse/uax29/v2 v2.7.0 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/dlclark/regexp2 v1.11.5 // indirect
github.com/fxamacker/cbor/v2 v2.9.0 // indirect
github.com/dlclark/regexp2/v2 v2.2.1 // indirect
github.com/fxamacker/cbor/v2 v2.9.2 // indirect
github.com/glebarez/go-sqlite v1.22.0 // indirect
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 // indirect
github.com/go-chi/chi/v5 v5.2.3 // indirect
github.com/go-chi/chi/v5 v5.3.0 // indirect
github.com/go-jose/go-jose/v3 v3.0.1 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-sql-driver/mysql v1.9.3 // indirect
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
github.com/go-webauthn/x v0.1.26 // indirect
github.com/golang-jwt/jwt/v5 v5.3.0 // indirect
github.com/go-sql-driver/mysql v1.10.0 // indirect
github.com/go-viper/mapstructure/v2 v2.5.0 // indirect
github.com/go-webauthn/x v0.2.6 // indirect
github.com/golang-jwt/jwt/v5 v5.3.1 // indirect
github.com/golang/snappy v1.0.0 // indirect
github.com/google/go-tpm v0.9.6 // indirect
github.com/google/go-tpm v0.9.8 // indirect
github.com/gorilla/mux v1.8.1 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
github.com/jackc/pgx/v5 v5.8.0 // indirect
github.com/jackc/pgx/v5 v5.10.0 // indirect
github.com/jackc/puddle/v2 v2.2.2 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/labstack/gommon v0.4.2 // indirect
github.com/labstack/gommon v0.5.0 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.19 // indirect
github.com/mattn/go-colorable v0.1.15 // indirect
github.com/mattn/go-isatty v0.0.22 // indirect
github.com/mattn/go-runewidth v0.0.24 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/mschoch/smat v0.2.0 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/ncruces/go-strftime v1.0.0 // indirect
github.com/philhofer/fwd v1.2.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/prometheus/client_model v0.6.2 // indirect
github.com/prometheus/common v0.67.5 // indirect
github.com/prometheus/procfs v0.19.2 // indirect
github.com/prometheus/common v0.68.1 // indirect
github.com/prometheus/procfs v0.20.1 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/tinylib/msgp v1.6.4 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasttemplate v1.2.2 // indirect
github.com/x448/float16 v0.8.4 // indirect
github.com/xrash/smetrics v0.0.0-20250705151800-55b8f293f342 // indirect
go.etcd.io/bbolt v1.4.3 // indirect
go.yaml.in/yaml/v2 v2.4.3 // indirect
golang.org/x/exp v0.0.0-20260112195511-716be5621a96 // indirect
golang.org/x/net v0.49.0 // indirect
golang.org/x/oauth2 v0.34.0 // indirect
golang.org/x/sync v0.19.0 // indirect
golang.org/x/sys v0.40.0 // indirect
golang.org/x/time v0.14.0 // indirect
golang.org/x/net v0.55.0 // indirect
golang.org/x/sync v0.21.0 // indirect
golang.org/x/sys v0.46.0 // indirect
golang.org/x/time v0.15.0 // indirect
google.golang.org/protobuf v1.36.11 // indirect
modernc.org/libc v1.67.7 // indirect
modernc.org/libc v1.73.0 // indirect
modernc.org/mathutil v1.7.1 // indirect
modernc.org/memory v1.11.0 // indirect
modernc.org/sqlite v1.44.3 // indirect
modernc.org/sqlite v1.52.0 // indirect
)
+143 -134
View File
@@ -1,43 +1,43 @@
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
github.com/Azure/go-ntlmssp v0.1.0 h1:DjFo6YtWzNqNvQdrwEyr/e4nhU3vRiwenz5QX7sFz+A=
github.com/Azure/go-ntlmssp v0.1.0/go.mod h1:NYqdhxd/8aAct/s4qSYZEerdPuH1liG2/X9DiVTbhpk=
filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo=
filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc=
github.com/Azure/go-ntlmssp v0.1.1 h1:l+FM/EEMb0U9QZE7mKNEDw5Mu3mFiaa2GKOoTSsNDPw=
github.com/Azure/go-ntlmssp v0.1.1/go.mod h1:NYqdhxd/8aAct/s4qSYZEerdPuH1liG2/X9DiVTbhpk=
github.com/Kunde21/markdownfmt/v3 v3.1.0 h1:KiZu9LKs+wFFBQKhrZJrFZwtLnCCWJahL+S+E/3VnM0=
github.com/Kunde21/markdownfmt/v3 v3.1.0/go.mod h1:tPXN1RTyOzJwhfHoon9wUr4HGYmWgVxSQN6VBJDkrVc=
github.com/RoaringBitmap/roaring/v2 v2.14.4 h1:4aKySrrg9G/5oRtJ3TrZLObVqxgQ9f1znCRBwEwjuVw=
github.com/RoaringBitmap/roaring/v2 v2.14.4/go.mod h1:oMvV6omPWr+2ifRdeZvVJyaz+aoEUopyv5iH0u/+wbY=
github.com/RoaringBitmap/roaring/v2 v2.18.2 h1:oPq3Cgx//iDuJQVp6xSInAKW34J9CEwE5GmLI2z+Eic=
github.com/RoaringBitmap/roaring/v2 v2.18.2/go.mod h1:eq4wdNXxtJIS/oikeCzdX1rBzek7ANzbth041hrU8Q4=
github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0=
github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
github.com/alecthomas/chroma/v2 v2.2.0/go.mod h1:vf4zrexSH54oEjJ7EdB65tGNHmH3pGZmVkgTP5RHvAs=
github.com/alecthomas/chroma/v2 v2.23.1 h1:nv2AVZdTyClGbVQkIzlDm/rnhk1E9bU9nXwmZ/Vk/iY=
github.com/alecthomas/chroma/v2 v2.23.1/go.mod h1:NqVhfBR0lte5Ouh3DcthuUCTUpDC9cxBOfyMbMQPs3o=
github.com/alecthomas/chroma/v2 v2.26.1 h1:2X21EdxGZNv5GF9mG5u+uzc02GCFyGxbcBm3Grd9A78=
github.com/alecthomas/chroma/v2 v2.26.1/go.mod h1:lxhRRa9H4hPmRLOOdYga4zkQIQjq3dtrrdwQeCfu78Y=
github.com/alecthomas/repr v0.0.0-20220113201626-b1b626ac65ae/go.mod h1:2kn6fqh/zIyPLmm3ugklbEi5hg5wS435eygvNfaDQL8=
github.com/alecthomas/repr v0.5.2 h1:SU73FTI9D1P5UNtvseffFSGmdNci/O6RsqzeXJtP0Qs=
github.com/alecthomas/repr v0.5.2/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
github.com/alexbrainman/sspi v0.0.0-20250919150558-7d374ff0d59e h1:4dAU9FXIyQktpoUAgOJK3OTFc/xug0PCXYCqU0FgDKI=
github.com/alexbrainman/sspi v0.0.0-20250919150558-7d374ff0d59e/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4=
github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ=
github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
github.com/andybalholm/brotli v1.2.1 h1:R+f5xP285VArJDRgowrfb9DqL18yVK0gKAW/F+eTWro=
github.com/andybalholm/brotli v1.2.1/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bits-and-blooms/bitset v1.24.4 h1:95H15Og1clikBrKr/DuzMXkQzECs1M6hhoGXLwLQOZE=
github.com/bits-and-blooms/bitset v1.24.4/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
github.com/blevesearch/bleve/v2 v2.5.7 h1:2d9YrL5zrX5EBBW++GOaEKjE+NPWeZGaX77IM26m1Z8=
github.com/blevesearch/bleve/v2 v2.5.7/go.mod h1:yj0NlS7ocGC4VOSAedqDDMktdh2935v2CSWOCDMHdSA=
github.com/blevesearch/bleve_index_api v1.3.1 h1:LdH3CQgBbIZ5UI/5Pykz87e0jfeQtVnrdZ2WUBrHHwU=
github.com/blevesearch/bleve_index_api v1.3.1/go.mod h1:xvd48t5XMeeioWQ5/jZvgLrV98flT2rdvEJ3l/ki4Ko=
github.com/blevesearch/geo v0.2.4 h1:ECIGQhw+QALCZaDcogRTNSJYQXRtC8/m8IKiA706cqk=
github.com/blevesearch/geo v0.2.4/go.mod h1:K56Q33AzXt2YExVHGObtmRSFYZKYGv0JEN5mdacJJR8=
github.com/blevesearch/go-faiss v1.0.27 h1:7cBImYDDQ82WJd5RUZ1ie6zXztCsC73W94ZzwOjkatk=
github.com/blevesearch/go-faiss v1.0.27/go.mod h1:OMGQwOaRRYxrmeNdMrXJPvVx8gBnvE5RYrr0BahNnkk=
github.com/bits-and-blooms/bitset v1.24.5 h1:654xBVHc23gJMAgOTkPNoCVfiRxuIOAUnAZFtopqJ4w=
github.com/bits-and-blooms/bitset v1.24.5/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
github.com/blevesearch/bleve/v2 v2.6.0 h1:Cyd3dd4q5tCbOV8MnKUVRUDYMHOir9xn12NZzXVSEd4=
github.com/blevesearch/bleve/v2 v2.6.0/go.mod h1:gLmI8lWgHgrIYf7UpUX7JISI1CaqC6VScu46mHThuAY=
github.com/blevesearch/bleve_index_api v1.3.12 h1:MirVNltwGq8z0PhOgiQp+bKL5qq8OvCxEwOOC7NnHNE=
github.com/blevesearch/bleve_index_api v1.3.12/go.mod h1:xvd48t5XMeeioWQ5/jZvgLrV98flT2rdvEJ3l/ki4Ko=
github.com/blevesearch/geo v0.2.5 h1:yJg9FX1oRwLnjXSXF+ECHfXFTF4diF02Ca/qUGVjJhE=
github.com/blevesearch/geo v0.2.5/go.mod h1:Jhq7WE2K6mJTx1xS44M2pUO6Io+wjCSHh1+co3YOgH4=
github.com/blevesearch/go-faiss v1.1.4 h1:wGHK+yiOSIvBAQMr4LcTaHBFf9v1dBebs3WpFqT93Rg=
github.com/blevesearch/go-faiss v1.1.4/go.mod h1:w3W9AiWsFRGVaMG+/cmJi7iHEAuGyC6blsgO1EzCK/M=
github.com/blevesearch/go-porterstemmer v1.0.3 h1:GtmsqID0aZdCSNiY8SkuPJ12pD4jI+DdXTAn4YRcHCo=
github.com/blevesearch/go-porterstemmer v1.0.3/go.mod h1:angGc5Ht+k2xhJdZi511LtmxuEf0OVpvUUNrwmM1P7M=
github.com/blevesearch/gtreap v0.1.1 h1:2JWigFrzDMR+42WGIN/V2p0cUvn4UP3C4Q5nmaZGW8Y=
github.com/blevesearch/gtreap v0.1.1/go.mod h1:QaQyDRAT51sotthUWAH4Sj08awFSSWzgYICSZ3w0tYk=
github.com/blevesearch/mmap-go v1.2.0 h1:l33nNKPFcBjJUMwem6sAYJPUzhUCABoK9FxZDGiFNBI=
github.com/blevesearch/mmap-go v1.2.0/go.mod h1:Vd6+20GBhEdwJnU1Xohgt88XCD/CTWcqbCNxkZpyBo0=
github.com/blevesearch/scorch_segment_api/v2 v2.4.1 h1:os52/JeCSLZ0YUkOuLk/Z7pu0SKUMofDPUg+VnbrRD0=
github.com/blevesearch/scorch_segment_api/v2 v2.4.1/go.mod h1:zvilBm4BNfbnTRLW7KgCTNgk2R31JaWzwRc2BEcD7Is=
github.com/blevesearch/scorch_segment_api/v2 v2.4.7 h1:GlMzW08hcsM3DnLUxhyF/1PcDal1qtvvIuytuph5djw=
github.com/blevesearch/scorch_segment_api/v2 v2.4.7/go.mod h1://IJ7tG3QCf0cWW/aVSXqy77tc1AvLu3fcJLYEvOAFs=
github.com/blevesearch/segment v0.9.1 h1:+dThDy+Lvgj5JMxhmOVlgFfkUtZV2kw49xax4+jTfSU=
github.com/blevesearch/segment v0.9.1/go.mod h1:zN21iLm7+GnBHWTao9I+Au/7MBiL8pPFtJBJTsk6kQw=
github.com/blevesearch/snowballstem v0.9.0 h1:lMQ189YspGP6sXvZQ4WZ+MLawfV8wOmPoD/iWeNXm8s=
@@ -46,18 +46,20 @@ github.com/blevesearch/upsidedown_store_api v1.0.2 h1:U53Q6YoWEARVLd1OYNc9kvhBMG
github.com/blevesearch/upsidedown_store_api v1.0.2/go.mod h1:M01mh3Gpfy56Ps/UXHjEO/knbqyQ1Oamg8If49gRwrQ=
github.com/blevesearch/vellum v1.2.0 h1:xkDiOEsHc2t3Cp0NsNZZ36pvc130sCzcGKOPMzXe+e0=
github.com/blevesearch/vellum v1.2.0/go.mod h1:uEcfBJz7mAOf0Kvq6qoEKQQkLODBF46SINYNkZNae4k=
github.com/blevesearch/zapx/v11 v11.4.2 h1:l46SV+b0gFN+Rw3wUI1YdMWdSAVhskYuvxlcgpQFljs=
github.com/blevesearch/zapx/v11 v11.4.2/go.mod h1:4gdeyy9oGa/lLa6D34R9daXNUvfMPZqUYjPwiLmekwc=
github.com/blevesearch/zapx/v12 v12.4.2 h1:fzRbhllQmEMUuAQ7zBuMvKRlcPA5ESTgWlDEoB9uQNE=
github.com/blevesearch/zapx/v12 v12.4.2/go.mod h1:TdFmr7afSz1hFh/SIBCCZvcLfzYvievIH6aEISCte58=
github.com/blevesearch/zapx/v13 v13.4.2 h1:46PIZCO/ZuKZYgxI8Y7lOJqX3Irkc3N8W82QTK3MVks=
github.com/blevesearch/zapx/v13 v13.4.2/go.mod h1:knK8z2NdQHlb5ot/uj8wuvOq5PhDGjNYQQy0QDnopZk=
github.com/blevesearch/zapx/v14 v14.4.2 h1:2SGHakVKd+TrtEqpfeq8X+So5PShQ5nW6GNxT7fWYz0=
github.com/blevesearch/zapx/v14 v14.4.2/go.mod h1:rz0XNb/OZSMjNorufDGSpFpjoFKhXmppH9Hi7a877D8=
github.com/blevesearch/zapx/v15 v15.4.2 h1:sWxpDE0QQOTjyxYbAVjt3+0ieu8NCE0fDRaFxEsp31k=
github.com/blevesearch/zapx/v15 v15.4.2/go.mod h1:1pssev/59FsuWcgSnTa0OeEpOzmhtmr/0/11H0Z8+Nw=
github.com/blevesearch/zapx/v16 v16.3.0 h1:hF6VlN15E9CB40RMPyqOIhlDw1OOo9RItumhKMQktxw=
github.com/blevesearch/zapx/v16 v16.3.0/go.mod h1:zCFjv7McXWm1C8rROL+3mUoD5WYe2RKsZP3ufqcYpLY=
github.com/blevesearch/zapx/v11 v11.4.3 h1:PTZOO5loKpHC/x/GzmPZNa9cw7GZIQxd5qRjwij9tHY=
github.com/blevesearch/zapx/v11 v11.4.3/go.mod h1:4gdeyy9oGa/lLa6D34R9daXNUvfMPZqUYjPwiLmekwc=
github.com/blevesearch/zapx/v12 v12.4.3 h1:eElXvAaAX4m04t//CGBQAtHNPA+Q6A1hHZVrN3LSFYo=
github.com/blevesearch/zapx/v12 v12.4.3/go.mod h1:TdFmr7afSz1hFh/SIBCCZvcLfzYvievIH6aEISCte58=
github.com/blevesearch/zapx/v13 v13.4.3 h1:qsdhRhaSpVnqDFlRiH9vG5+KJ+dE7KAW9WyZz/KXAiE=
github.com/blevesearch/zapx/v13 v13.4.3/go.mod h1:knK8z2NdQHlb5ot/uj8wuvOq5PhDGjNYQQy0QDnopZk=
github.com/blevesearch/zapx/v14 v14.4.3 h1:GY4Hecx0C6UTmiNC2pKdeA2rOKiLR5/rwpU9WR51dgM=
github.com/blevesearch/zapx/v14 v14.4.3/go.mod h1:rz0XNb/OZSMjNorufDGSpFpjoFKhXmppH9Hi7a877D8=
github.com/blevesearch/zapx/v15 v15.4.3 h1:iJiMJOHrz216jyO6lS0m9RTCEkprUnzvqAI2lc/0/CU=
github.com/blevesearch/zapx/v15 v15.4.3/go.mod h1:1pssev/59FsuWcgSnTa0OeEpOzmhtmr/0/11H0Z8+Nw=
github.com/blevesearch/zapx/v16 v16.3.4 h1:hDAqA8qusZTNbPEL7//w5P65UZ2de6yhSeUaTbp0Po0=
github.com/blevesearch/zapx/v16 v16.3.4/go.mod h1:zqkPPqs9GS9FzVWzCO3Wf1X044yWAV17+4zb+FTiEHg=
github.com/blevesearch/zapx/v17 v17.1.6 h1:rVGeyH0EPElBXM4PvjrCdt8LDdRLpa4GC1gMRQkCWUE=
github.com/blevesearch/zapx/v17 v17.1.6/go.mod h1:c+mPvbZgZnDPOUS5Z9EXhntMcJnpIVjQTM9TF5yEGJM=
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
github.com/boombuler/barcode v1.1.0 h1:ChaYjBR63fr4LFyGn8E8nt7dBSt3MiU3zMOZqFvVkHo=
github.com/boombuler/barcode v1.1.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
@@ -69,11 +71,8 @@ github.com/chromedp/chromedp v0.14.0 h1:/xE5m6wEBwivhalHwlCOyYfBcAJNwg4nLw96QiCf
github.com/chromedp/chromedp v0.14.0/go.mod h1:rHzAv60xDE7VNy/MYtTUrYreSc0ujt2O1/C3bzctYBo=
github.com/chromedp/sysutil v1.1.0 h1:PUFNv5EcprjqXZD9nJb9b/c9ibAbxiYo4exNWZyipwM=
github.com/chromedp/sysutil v1.1.0/go.mod h1:WiThHUdltqCNKGc4gaU50XgYjwjYIhKWoHGPTUfWTJ8=
github.com/clipperhouse/stringish v0.1.1 h1:+NSqMOr3GR6k1FdRhhnXrLfztGzuG+VuFDfatpWHKCs=
github.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEXNWYXQgCt4hdOzA=
github.com/clipperhouse/uax29/v2 v2.4.0 h1:RXqE/l5EiAbA4u97giimKNlmpvkmz+GrBVTelsoXy9g=
github.com/clipperhouse/uax29/v2 v2.4.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk=
github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM=
github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo=
github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
@@ -82,57 +81,61 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
github.com/dlclark/regexp2 v1.7.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ=
github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/dlclark/regexp2/v2 v2.2.1 h1:mf4KkFUj0gJuarK8P+LgiS+Lit7m9N1yAwEfPbee7R0=
github.com/dlclark/regexp2/v2 v2.2.1/go.mod h1:avUrQvPaLz2DrFNHJF0taWAFFX2C1GMSSoeiqFjcBmU=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM=
github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
github.com/gabriel-vasile/mimetype v1.4.12 h1:e9hWvmLYvtp846tLHam2o++qitpguFiYCKbn0w9jyqw=
github.com/gabriel-vasile/mimetype v1.4.12/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
github.com/fxamacker/cbor/v2 v2.9.2 h1:X4Ksno9+x3cz0TZv69ec1hxP/+tymuR8PXQJyDwfh78=
github.com/fxamacker/cbor/v2 v2.9.2/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
github.com/gabriel-vasile/mimetype v1.4.13 h1:46nXokslUBsAJE/wMsp5gtO500a4F3Nkz9Ufpk2AcUM=
github.com/gabriel-vasile/mimetype v1.4.13/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
github.com/glebarez/go-sqlite v1.22.0 h1:uAcMJhaA6r3LHMTFgP0SifzgXg46yJkgxqyuyec+ruQ=
github.com/glebarez/go-sqlite v1.22.0/go.mod h1:PlBIdHe0+aUEFn+r2/uthrWq4FxbzugL0L8Li6yQJbc=
github.com/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GMw=
github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ=
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 h1:BP4M0CvQ4S3TGls2FvczZtj5Re/2ZzkV9VwqPHH/3Bo=
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
github.com/go-chi/chi/v5 v5.2.3 h1:WQIt9uxdsAbgIYgid+BpYc+liqQZGMHRaUwp0JUcvdE=
github.com/go-chi/chi/v5 v5.2.3/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=
github.com/go-chi/chi/v5 v5.3.0 h1:halUjDxhshgXHMrao5bB8eNBXo/rnzwr8m5m36glehM=
github.com/go-chi/chi/v5 v5.3.0/go.mod h1:R+tYY2hNuVUUjxoPtqUdgBqevM9s9njzkTLutVsOCto=
github.com/go-jose/go-jose/v3 v3.0.1 h1:pWmKFVtt+Jl0vBZTIpz/eAKwsm6LkIxDVVbFHKkchhA=
github.com/go-jose/go-jose/v3 v3.0.1/go.mod h1:RNkWWRld676jZEYoV3+XK8L2ZnNSvIsxFMht0mSX+u8=
github.com/go-json-experiment/json v0.0.0-20250725192818-e39067aee2d2 h1:iizUGZ9pEquQS5jTGkh4AqeeHCMbfbjeb0zMt0aEFzs=
github.com/go-json-experiment/json v0.0.0-20250725192818-e39067aee2d2/go.mod h1:TiCD2a1pcmjd7YnhGH0f/zKNcCD06B029pHhzV23c2M=
github.com/go-ldap/ldap/v3 v3.4.12 h1:1b81mv7MagXZ7+1r7cLTWmyuTqVqdwbtJSjC0DAp9s4=
github.com/go-ldap/ldap/v3 v3.4.12/go.mod h1:+SPAGcTtOfmGsCb3h1RFiq4xpp4N636G75OEace8lNo=
github.com/go-ldap/ldap/v3 v3.4.13 h1:+x1nG9h+MZN7h/lUi5Q3UZ0fJ1GyDQYbPvbuH38baDQ=
github.com/go-ldap/ldap/v3 v3.4.13/go.mod h1:LxsGZV6vbaK0sIvYfsv47rfh4ca0JXokCoKjZxsszv0=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.30.1 h1:f3zDSN/zOma+w6+1Wswgd9fLkdwy06ntQJp0BBvFG0w=
github.com/go-playground/validator/v10 v10.30.1/go.mod h1:oSuBIQzuJxL//3MelwSLD5hc2Tu889bF0Idm9Dg26cM=
github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo=
github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/go-webauthn/webauthn v0.15.0 h1:LR1vPv62E0/6+sTenX35QrCmpMCzLeVAcnXeH4MrbJY=
github.com/go-webauthn/webauthn v0.15.0/go.mod h1:hcAOhVChPRG7oqG7Xj6XKN1mb+8eXTGP/B7zBLzkX5A=
github.com/go-webauthn/x v0.1.26 h1:eNzreFKnwNLDFoywGh9FA8YOMebBWTUNlNSdolQRebs=
github.com/go-webauthn/x v0.1.26/go.mod h1:jmf/phPV6oIsF6hmdVre+ovHkxjDOmNH0t6fekWUxvg=
github.com/go-playground/validator/v10 v10.30.3 h1:4MU6YkEwx7GbcPJOZxrtbu+QfF3pJLJuaYTeAH0DYy8=
github.com/go-playground/validator/v10 v10.30.3/go.mod h1:4Axh7oCNGcoGkqLoE4YWt6n20mcEIsPRlB7vPk3lpyc=
github.com/go-sql-driver/mysql v1.10.0 h1:Q+1LV8DkHJvSYAdR83XzuhDaTykuDx0l6fkXxoWCWfw=
github.com/go-sql-driver/mysql v1.10.0/go.mod h1:M+cqaI7+xxXGG9swrdeUIoPG3Y3KCkF0pZej+SK+nWk=
github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro=
github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/go-webauthn/webauthn v0.17.4 h1:KFTSz3R2RYDiUn/0cDi3XTJgFenSG74eKTTHlqWhlxk=
github.com/go-webauthn/webauthn v0.17.4/go.mod h1:pZk63EE/BdztlmyS4Yc+9H5g4a8blNlbtGmdHQHbZX8=
github.com/go-webauthn/x v0.2.6 h1:TEyDuQAIiEgYpx60nKiBJIX/5nSUC8LxNbH+uf5U9uk=
github.com/go-webauthn/x v0.2.6/go.mod h1:45bA7YEqyQhRcQJ/TiBb46Ww8yqHBGvgEhQ3WWF0aDo=
github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU=
github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM=
github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og=
github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
github.com/gobwas/ws v1.4.0 h1:CTaoG1tojrh4ucGPcoJFiAQUAsEWekEWvLy7GsVNqGs=
github.com/gobwas/ws v1.4.0/go.mod h1:G3gNqMNtPppf5XUz7O4shetPpcZ1VJ7zt18dlUeakrc=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY=
github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs=
github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/go-tpm v0.9.6 h1:Ku42PT4LmjDu1H5C5ISWLlpI1mj+Zq7sPGKoRw2XROA=
github.com/google/go-tpm v0.9.6/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY=
github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo=
github.com/google/go-tpm v0.9.8/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY=
github.com/google/go-tpm-tools v0.3.13-0.20230620182252-4639ecce2aba h1:qJEJcuLzH5KDR0gKc0zcktin6KSAwL7+jWKBYceddTc=
github.com/google/go-tpm-tools v0.3.13-0.20230620182252-4639ecce2aba/go.mod h1:EFYHy8/1y2KfgTAsx7Luu7NGhoxtuVHnNo8jE7FikKc=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
@@ -158,8 +161,8 @@ github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsI
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jackc/pgx/v5 v5.8.0 h1:TYPDoleBBme0xGSAX3/+NujXXtpZn9HBONkQC7IEZSo=
github.com/jackc/pgx/v5 v5.8.0/go.mod h1:QVeDInX2m9VyzvNeiCJVjCkNFqzsNb43204HshNSZKw=
github.com/jackc/pgx/v5 v5.10.0 h1:VhSvgU2jSli8o3AqIEOTJr7rZwAEUVo4E4XhR94Zfr0=
github.com/jackc/pgx/v5 v5.10.0/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4=
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/jcmturner/aescts/v2 v2.0.0 h1:9YKLH6ey7H4eDBXW8khjYslgyqG2xZikXP0EQFKrle8=
@@ -188,27 +191,24 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/labstack/echo-contrib v0.17.4 h1:g5mfsrJfJTKv+F5uNKCyrjLK7js+ZW6HTjg4FnDxxgk=
github.com/labstack/echo-contrib v0.17.4/go.mod h1:9O7ZPAHUeMGTOAfg80YqQduHzt0CzLak36PZRldYrZ0=
github.com/labstack/echo/v4 v4.15.0 h1:hoRTKWcnR5STXZFe9BmYun9AMTNeSbjHi2vtDuADJ24=
github.com/labstack/echo/v4 v4.15.0/go.mod h1:xmw1clThob0BSVRX1CRQkGQ/vjwcpOMjQZSZa9fKA/c=
github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0=
github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU=
github.com/labstack/echo-contrib v0.50.1 h1:W9cZZ9viA4TDdFtm8cuA+XGFwOcnfbjJpl7VgfsRLHE=
github.com/labstack/echo-contrib v0.50.1/go.mod h1:8r/++U/Fw/QniApFnzunLanKaviPfBX7fX7/2QX0qOk=
github.com/labstack/echo/v4 v4.15.2 h1:nnh2sCzGCVYnU+wCisMPiYapEg/QVo/gcI9ePKg5/T4=
github.com/labstack/echo/v4 v4.15.2/go.mod h1:Xzp1Ns1RA2c9fY7nSgUJkpkUZGNbEIVHZbtbOMPktBI=
github.com/labstack/gommon v0.5.0 h1:6VSQ2NOzsnEJ5W6+84E0RbcaDDmgB6NIAzWCczTEe6c=
github.com/labstack/gommon v0.5.0/go.mod h1:Rzlg7HHy1maLfzBYGg9NZcVuz1sA68HHhLjhcEllYE0=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/markbates/goth v1.82.0 h1:8j/c34AjBSTNzO7zTsOyP5IYCQCMBTRBHAbBt/PI0bQ=
github.com/markbates/goth v1.82.0/go.mod h1:/DRlcq0pyqkKToyZjsL2KgiA1zbF1HIjE7u2uC79rUk=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw=
github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
github.com/meilisearch/meilisearch-go v0.36.0 h1:N1etykTektXt5KPcSbhBO0d5Xx5NaKj4pJWEM7WA5dI=
github.com/meilisearch/meilisearch-go v0.36.0/go.mod h1:HBfHzKMxcSbTOvqdfuRA/yf6Vk9IivcwKocWRuW7W78=
github.com/mattn/go-colorable v0.1.15 h1:+u9SLTRGnXv73cEsnsmoZBom+dMU88B2M0aDcWy0/jY=
github.com/mattn/go-colorable v0.1.15/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.22 h1:j8l17JJ9i6VGPUFUYoTUKPSgKe/83EYU2zBC7YNKMw4=
github.com/mattn/go-isatty v0.0.22/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4=
github.com/mattn/go-runewidth v0.0.24 h1:cpokDiIn0MGnhdHwuWnJBITySJ20QyNGnY2kR/ay2DU=
github.com/mattn/go-runewidth v0.0.24/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
github.com/meilisearch/meilisearch-go v0.36.3 h1:Yx1aTY5jDgtbStPVkhJTDoLnZTy5sejQSPyjfNMy6e4=
github.com/meilisearch/meilisearch-go v0.36.3/go.mod h1:hWcR0MuWLSzHfbz9GGzIr3s9rnXLm1jqkmHkJPbUSvM=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
@@ -220,7 +220,10 @@ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/oauth2-proxy/mockoidc v0.0.0-20240214162133-caebfff84d25 h1:9bCMuD3TcnjeqjPT2gSlha4asp8NvgcFRYExCaikCxk=
github.com/oauth2-proxy/mockoidc v0.0.0-20240214162133-caebfff84d25/go.mod h1:eDjgYHYDJbPLBLsyZ6qRaugP0mX8vePOhZ5id1fdzJw=
github.com/philhofer/fwd v1.2.0 h1:e6DnBTl7vGY+Gz322/ASL4Gyp1FspeMvx1RNDoToZuM=
github.com/philhofer/fwd v1.2.0/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pquerna/otp v1.5.0 h1:NMMR+WrmaqXU4EzdGJEE1aUUI0AMRzsp96fFFWNPwxs=
@@ -229,24 +232,28 @@ github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h
github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4=
github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw=
github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws=
github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw=
github.com/prometheus/common v0.68.1 h1:omjRRl4QP4komogpXuhfeOiisQg7xdy8VM1UY+pStaY=
github.com/prometheus/common v0.68.1/go.mod h1:ZzL3f6u94qUxh9p+tJTrF+FvBS1XXbbRAZCQkytAL0Y=
github.com/prometheus/procfs v0.20.1 h1:XwbrGOIplXW/AU3YhIhLODXMJYyC1isLFfYCsTEycfc=
github.com/prometheus/procfs v0.20.1/go.mod h1:o9EMBZGRyvDrSPH1RqdxhojkuXstoe4UlK79eF5TGGo=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY=
github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ=
github.com/rs/zerolog v1.35.1 h1:m7xQeoiLIiV0BCEY4Hs+j2NG4Gp2o2KPKmhnnLiazKI=
github.com/rs/zerolog v1.35.1/go.mod h1:EjML9kdfa/RMA7h/6z6pYmq1ykOuA8/mjWaEvGI+jcw=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/tinylib/msgp v1.6.4 h1:mOwYbyYDLPj35mkA2BjjYejgJk9BuHxDdvRnb6v2ZcQ=
github.com/tinylib/msgp v1.6.4/go.mod h1:RSp0LW9oSxFut3KzESt5Voq4GVWyS+PSulT77roAqEA=
github.com/urfave/cli/v2 v2.27.7 h1:bH59vdhbjLv3LAvIu6gd0usJHgoTTPhCFib8qqOwXYU=
github.com/urfave/cli/v2 v2.27.7/go.mod h1:CyNAG/xg+iAOg0N4MPGZqVmv2rCoP267496AOXUZjA4=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
@@ -260,8 +267,8 @@ github.com/xrash/smetrics v0.0.0-20250705151800-55b8f293f342/go.mod h1:Ohn+xnUBi
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
github.com/yuin/goldmark v1.4.15/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yuin/goldmark v1.7.16 h1:n+CJdUxaFMiDUNnWC3dMWCIQJSkxH4uz3ZwQBkAlVNE=
github.com/yuin/goldmark v1.7.16/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
github.com/yuin/goldmark v1.8.2 h1:kEGpgqJXdgbkhcOgBxkC0X0PmoPG1ZyoZ117rDVp4zE=
github.com/yuin/goldmark v1.8.2/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
github.com/yuin/goldmark-emoji v1.0.6 h1:QWfF2FYaXwL74tfGOW5izeiZepUDroDJfWubQI9HTHs=
github.com/yuin/goldmark-emoji v1.0.6/go.mod h1:ukxJDKFpdFb5x0a5HqbdlcKtebh086iJpI31LTKmWuA=
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc h1:+IAOyRda+RLrxa1WC7umKOZRsGq4QrFFMYApOeHzQwQ=
@@ -274,33 +281,35 @@ go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0=
go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8=
golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8=
golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A=
golang.org/x/exp v0.0.0-20260112195511-716be5621a96 h1:Z/6YuSHTLOHfNFdb8zVZomZr7cqNgTJvA8+Qz75D8gU=
golang.org/x/exp v0.0.0-20260112195511-716be5621a96/go.mod h1:nzimsREAkjBCIEFtHiYkrJyT+2uy9YZJB7H1k68CXZU=
golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c=
golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU=
golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o=
golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8=
golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw=
golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.39.0 h1:RclSuaJf32jOqZz74CkPA9qFuVTX7vhLlpfj/IGWlqY=
golang.org/x/term v0.39.0/go.mod h1:yxzUCTP/U+FzoxfdKmLaA0RV1WgE0VY7hXBwKtY/4ww=
golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE=
golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8=
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc=
golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg=
go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ=
go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto=
golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio=
golang.org/x/mod v0.36.0 h1:JJjpVx6myfUsUdAzZuOSTTmRE0PfZeNWzzvKrP7amb4=
golang.org/x/mod v0.36.0/go.mod h1:moc6ELqsWcOw5Ef3xVprK5ul/MvtVvkIXLziUOICjUQ=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8=
golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww=
golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=
golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM=
golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw=
golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/term v0.44.0 h1:0rLvDRCtNj0gZkyIXhCyOb2OAzEhLVqc4B+hrsBhrmc=
golang.org/x/term v0.44.0/go.mod h1:7ze4MdzUzLXpSAoFP1H0bOI9aXDqveSvatT5vKcFh2Y=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE=
golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4=
golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
golang.org/x/tools v0.45.0 h1:18qN3FAooORvApf5XjCXgsuayZOEtXf6JK18I3+ONa8=
golang.org/x/tools v0.45.0/go.mod h1:LuUGqqaXcXMEFEruIVJVm5mgDD8vww/z/SR1gQ4uE/0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
@@ -315,30 +324,30 @@ gorm.io/driver/postgres v1.6.0 h1:2dxzU8xJ+ivvqTRph34QX+WrRaJlmfyPqXmoGVjMBa4=
gorm.io/driver/postgres v1.6.0/go.mod h1:vUw0mrGgrTK+uPHEhAdV4sfFELrByKVGnaVRkXDhtWo=
gorm.io/gorm v1.31.1 h1:7CA8FTFz/gRfgqgpeKIBcervUn3xSyPUmr6B2WXJ7kg=
gorm.io/gorm v1.31.1/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs=
modernc.org/cc/v4 v4.27.1 h1:9W30zRlYrefrDV2JE2O8VDtJ1yPGownxciz5rrbQZis=
modernc.org/cc/v4 v4.27.1/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
modernc.org/ccgo/v4 v4.30.1 h1:4r4U1J6Fhj98NKfSjnPUN7Ze2c6MnAdL0hWw6+LrJpc=
modernc.org/ccgo/v4 v4.30.1/go.mod h1:bIOeI1JL54Utlxn+LwrFyjCx2n2RDiYEaJVSrgdrRfM=
modernc.org/fileutil v1.3.40 h1:ZGMswMNc9JOCrcrakF1HrvmergNLAmxOPjizirpfqBA=
modernc.org/fileutil v1.3.40/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc=
modernc.org/cc/v4 v4.28.4 h1:Hd/4Es+MBj+/7hSdZaisNyu6bv3V0Dp2MdllyfqaH+c=
modernc.org/cc/v4 v4.28.4/go.mod h1:OnovgIhbbMXMu1aISnJ0wvVD1KnW+cAUJkIrAWh+kVI=
modernc.org/ccgo/v4 v4.34.4 h1:OVnSOWQjVKOYkFxoHYB+qQmSHK5gqMqARM+K9DpR/Ws=
modernc.org/ccgo/v4 v4.34.4/go.mod h1:qdKqE8FNIYyysougB1RX9MxCzp5oJOcQXSobANJ4TuE=
modernc.org/fileutil v1.4.0 h1:j6ZzNTftVS054gi281TyLjHPp6CPHr2KCxEXjEbD6SM=
modernc.org/fileutil v1.4.0/go.mod h1:EqdKFDxiByqxLk8ozOxObDSfcVOv/54xDs/DUHdvCUU=
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
modernc.org/gc/v3 v3.1.1 h1:k8T3gkXWY9sEiytKhcgyiZ2L0DTyCQ/nvX+LoCljoRE=
modernc.org/gc/v3 v3.1.1/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY=
modernc.org/gc/v3 v3.1.3 h1:6QAplYyVO+KdPW3pGnqmJDUxtkec8ooEWvks/hhU3lc=
modernc.org/gc/v3 v3.1.3/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY=
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
modernc.org/libc v1.67.7 h1:H+gYQw2PyidyxwxQsGTwQw6+6H+xUk+plvOKW7+d3TI=
modernc.org/libc v1.67.7/go.mod h1:UjCSJFl2sYbJbReVQeVpq/MgzlbmDM4cRHIYFelnaDk=
modernc.org/libc v1.73.0 h1:Y/KmTxbIN5T3x+NFjYOzV/+Ha7wKClfIecmTCTuYlqQ=
modernc.org/libc v1.73.0/go.mod h1:DXZ3eO8qMCNn2SnmTNCiC71nJ9Rcq3PsnpU6Vc4rWK8=
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
modernc.org/opt v0.2.0 h1:tGyef5ApycA7FSEOMraay9SaTk5zmbx7Tu+cJs4QKZg=
modernc.org/opt v0.2.0/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
modernc.org/sqlite v1.44.3 h1:+39JvV/HWMcYslAwRxHb8067w+2zowvFOUrOWIy9PjY=
modernc.org/sqlite v1.44.3/go.mod h1:CzbrU2lSB1DKUusvwGz7rqEKIq+NUd8GWuBBZDs9/nA=
modernc.org/sqlite v1.52.0 h1:p4dhYh2tXZCiyaqHwRVJDjIGKWyXayiQpThxgDzJaxo=
modernc.org/sqlite v1.52.0/go.mod h1:tcNzv5p84E0skkmJn038y+hWJbLQXQqEnQfeh5r2JLM=
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
+13
View File
@@ -1,5 +1,18 @@
# Helm Chart Changelog
# 0.9.0 - 2026-06-10
- Bump Opengist image to 1.13.1
# 0.8.0 - 2026-06-09
- Bump Opengist image to 1.13.0
# 0.7.0 - 2026-03-14
- Bump Opengist image to 1.12.2
- Add environment variables and secrets to statefulset
## 0.6.0 - 2026-02-03
- Bump Opengist image to 1.12.1
+3 -3
View File
@@ -4,6 +4,6 @@ dependencies:
version: 16.7.27
- name: meilisearch
repository: https://meilisearch.github.io/meilisearch-kubernetes
version: 0.17.1
digest: sha256:ad702e35f258fed1f804d3e48b071767499f5730e099a8c461610950e5182368
generated: "2025-09-21T04:49:08.679554149+02:00"
version: 0.26.0
digest: sha256:7182bad3df032b3cb21a793ea6b027eaa96e142ff207b607b62df974bc82de90
generated: "2026-03-09T03:39:04.820136+07:00"
+3 -3
View File
@@ -2,8 +2,8 @@ apiVersion: v2
name: opengist
description: Opengist Helm chart for Kubernetes
type: application
version: 0.6.0
appVersion: 1.12.1
version: 0.9.0
appVersion: 1.13.1
home: https://opengist.io
icon: https://raw.githubusercontent.com/thomiceli/opengist/master/public/img/opengist.svg
sources:
@@ -15,5 +15,5 @@ dependencies:
condition: postgresql.enabled
- name: meilisearch
repository: https://meilisearch.github.io/meilisearch-kubernetes
version: 0.17.1
version: 0.26.0
condition: meilisearch.enabled
+1 -1
View File
@@ -1,6 +1,6 @@
# Opengist Helm Chart
![Version: 0.6.0](https://img.shields.io/badge/Version-0.6.0-informational?style=flat-square) ![AppVersion: 1.12.1](https://img.shields.io/badge/AppVersion-1.12.1-informational?style=flat-square)
![Version: 0.8.0](https://img.shields.io/badge/Version-0.8.0-informational?style=flat-square) ![AppVersion: 1.13.1](https://img.shields.io/badge/AppVersion-1.13.1-informational?style=flat-square)
Opengist Helm chart for Kubernetes. Check [CHANGELOG.md](CHANGELOG.md) for release notes.
+13
View File
@@ -63,6 +63,19 @@ spec:
{{- toYaml .Values.securityContext | nindent 12 }}
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
{{- if or .Values.deployment.env .Values.deployment.envFromSecrets }}
env:
{{- if .Values.deployment.env }}
{{- toYaml .Values.deployment.env | nindent 12 }}
{{- end }}
{{- range .Values.deployment.envFromSecrets }}
- name: {{ .name }}
valueFrom:
secretKeyRef:
name: {{ .secretName }}
key: {{ .secretKey }}
{{- end }}
{{- end }}
ports:
- name: http
containerPort: {{ .Values.service.http.port }}
+1 -1
View File
@@ -25,7 +25,7 @@
{{- $user := default "" .Values.postgresql.global.postgresql.auth.username }}
{{- $pass := default "" .Values.postgresql.global.postgresql.auth.password }}
{{- $db := default "" .Values.postgresql.global.postgresql.auth.database }}
{{- $port := default 5432 .Values.postgresql.global.postgresql.service.ports.postgresql }}
{{- $port := int (default 5432 .Values.postgresql.global.postgresql.service.ports.postgresql) }}
{{- if or (eq $user "") (eq $pass "") (eq $db "") }}
{{- fail "postgresql.enabled=true requires username/password/database (postgresql.global.postgresql.auth.*) or set config.db-uri manually" }}
{{- end }}
+14 -1
View File
@@ -84,7 +84,7 @@ spec:
serviceName: {{ include "opengist.fullname" . }}-http
podManagementPolicy: {{ .Values.statefulSet.podManagementPolicy }}
updateStrategy:
{{- toYaml .Values.statefulSet.updateStrategy | nindent 2 }}
{{- toYaml .Values.statefulSet.updateStrategy | nindent 4 }}
selector:
matchLabels:
{{- include "opengist.selectorLabels" . | nindent 6 }}
@@ -131,6 +131,19 @@ spec:
{{- toYaml .Values.securityContext | nindent 12 }}
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
{{- if or .Values.deployment.env .Values.deployment.envFromSecrets }}
env:
{{- if .Values.deployment.env }}
{{- toYaml .Values.deployment.env | nindent 12 }}
{{- end }}
{{- range .Values.deployment.envFromSecrets }}
- name: {{ .name }}
valueFrom:
secretKeyRef:
name: {{ .secretName }}
key: {{ .secretKey }}
{{- end }}
{{- end }}
ports:
- name: http
containerPort: {{ .Values.service.http.port }}
+13 -2
View File
@@ -18,7 +18,7 @@ configExistingSecret: ""
image:
repository: ghcr.io/thomiceli/opengist
pullPolicy: Always
tag: "1.12.1"
tag: "1.13.1"
digest: ""
imagePullSecrets: []
# - name: "image-pull-secret"
@@ -66,7 +66,11 @@ statefulSet:
podSecurityContext:
fsGroup: 1000
securityContext: {}
# allowPrivilegeEscalation: false
# runAsUser: 1000
# runAsGroup: 1000
# runAsNonRoot: true
# allowPrivilegeEscalation: false
# readOnlyRootFilesystem: true
## Pod Disruption Budget settings
## ref: https://kubernetes.io/docs/tasks/run-application/configure-pdb/
@@ -258,6 +262,13 @@ autoscaling:
## Additional deployment configuration
deployment:
env: []
## Load environment variables from specific secret keys
## Each entry creates an env.valueFrom.secretKeyRef in the container spec
## ref: https://kubernetes.io/docs/concepts/configuration/secret/#using-secrets-as-environment-variables
envFromSecrets: []
# - name: OG_OIDC_SECRET
# secretName: opengist-oidc-client-secret
# secretKey: client_secret
terminationGracePeriodSeconds: 60
labels: {}
annotations: {}
+76 -51
View File
@@ -1,21 +1,20 @@
package actions
import (
"fmt"
"os"
"path/filepath"
"strings"
"sync/atomic"
"time"
"github.com/rs/zerolog/log"
"github.com/thomiceli/opengist/internal/config"
"github.com/thomiceli/opengist/internal/db"
"github.com/thomiceli/opengist/internal/git"
"github.com/thomiceli/opengist/internal/index"
"os"
"path/filepath"
"strings"
"sync"
)
type ActionStatus struct {
Running bool
}
const (
SyncReposFromFS = iota
SyncReposFromDB
@@ -24,63 +23,71 @@ const (
ResetHooks
IndexGists
SyncGistLanguages
DeleteExpiredGists
numActions // keep last — sizes the `running` array
)
var (
mutex sync.Mutex
actions = make(map[int]ActionStatus)
)
// running tracks which actions are in progress in this instance, one slot per
// action type. It dedupes concurrent runs (e.g. a double-clicked admin button)
// and backs IsRunning for the admin panel; cross-instance single-flighting is
// handled separately by the DB action lock.
var running [numActions]atomic.Bool
func updateActionStatus(actionType int, running bool) {
actions[actionType] = ActionStatus{
Running: running,
}
const lockLease = time.Hour
type action struct {
run func()
spec string
}
var registry = map[int]action{
SyncReposFromFS: {run: syncReposFromFS},
SyncReposFromDB: {run: syncReposFromDB},
GitGcRepos: {run: gitGcRepos},
SyncGistPreviews: {run: syncGistPreviews},
ResetHooks: {run: resetHooks},
IndexGists: {run: indexGists},
SyncGistLanguages: {run: syncGistLanguages},
DeleteExpiredGists: {run: deleteExpiredGists, spec: "@every 1m"},
}
func IsRunning(actionType int) bool {
mutex.Lock()
defer mutex.Unlock()
return actions[actionType].Running
return actionType >= 0 && actionType < numActions && running[actionType].Load()
}
func Run(actionType int) {
mutex.Lock()
if actions[actionType].Running {
mutex.Unlock()
func RunOnce(actionType int) {
a, ok := registry[actionType]
if !ok {
log.Error().Msgf("Unknown action type %d", actionType)
return
}
updateActionStatus(actionType, true)
mutex.Unlock()
if !running[actionType].CompareAndSwap(false, true) {
return // already running in this instance
}
defer running[actionType].Store(false)
// Single-flight the action across instances sharing the database so only
// one replica runs it at a time, whether triggered by the scheduler or
// manually.
acquired, err := db.AcquireLock(actionType, lockLease)
if err != nil {
log.Error().Err(err).Msgf("Could not acquire lock for action %d", actionType)
return
}
if !acquired {
return
}
defer func() {
mutex.Lock()
updateActionStatus(actionType, false)
mutex.Unlock()
if err := db.ReleaseLock(actionType); err != nil {
log.Error().Err(err).Msgf("Could not release lock for action %d", actionType)
}
}()
var functionToRun func()
switch actionType {
case SyncReposFromFS:
functionToRun = syncReposFromFS
case SyncReposFromDB:
functionToRun = syncReposFromDB
case GitGcRepos:
functionToRun = gitGcRepos
case SyncGistPreviews:
functionToRun = syncGistPreviews
case ResetHooks:
functionToRun = resetHooks
case IndexGists:
functionToRun = indexGists
case SyncGistLanguages:
functionToRun = syncGistLanguages
default:
log.Error().Msg("Unknown action type")
}
functionToRun()
log.Info().Msgf("Starting running action %d", actionType)
a.run()
log.Info().Msgf("Finished running action %d", actionType)
}
func syncReposFromFS() {
@@ -136,6 +143,7 @@ func syncGistPreviews() {
return
}
for _, gist := range gists {
fmt.Println("Syncing preview for gist", gist.ID)
if err = gist.UpdatePreviewAndCount(false); err != nil {
log.Error().Err(err).Msgf("Cannot update preview and count for gist %d", gist.ID)
}
@@ -150,7 +158,12 @@ func resetHooks() {
}
func indexGists() {
log.Info().Msg("Indexing all Gists...")
log.Info().Msg("Rebuilding index from scratch...")
if err := index.ResetIndex(); err != nil {
log.Error().Err(err).Msg("Cannot reset index")
return
}
gists, err := db.GetAllGistsRows()
if err != nil {
log.Error().Err(err).Msg("Cannot get gists")
@@ -183,3 +196,15 @@ func syncGistLanguages() {
gist.UpdateLanguages()
}
}
func deleteExpiredGists() {
gists, err := db.DeleteExpiredGists()
if err != nil {
log.Error().Err(err).Msg("Cannot delete expired gists")
return
}
if len(gists) > 0 {
log.Info().Msgf("Deleted %d expired gist(s)", len(gists))
}
}
+49
View File
@@ -0,0 +1,49 @@
package actions
import (
"time"
"github.com/robfig/cron/v3"
"github.com/rs/zerolog/log"
)
const cronDrainTimeout = 10 * time.Second
// StartCron registers every scheduled action in `registry` (those with a spec)
// and starts the scheduler. It returns a stop function that halts the scheduler
// and waits (up to cronDrainTimeout) for any in-flight job to finish — call it
// before tearing down the DB so a running action can release its lock cleanly.
// Panicking jobs are recovered so a single failed run can't take down the server.
func StartCron() (stop func()) {
c := cron.New(cron.WithChain(cron.Recover(cronLogger{})))
for actionType, a := range registry {
if a.spec == "" {
continue
}
if _, err := c.AddFunc(a.spec, func() { RunOnce(actionType) }); err != nil {
log.Error().Err(err).Msgf("Invalid cron spec %q for action %d", a.spec, actionType)
}
}
c.Start()
return func() {
log.Info().Msg("Stopping crons...")
select {
case <-c.Stop().Done():
case <-time.After(cronDrainTimeout):
log.Warn().Msg("cron: timed out waiting for jobs to finish")
}
}
}
type cronLogger struct{}
func (cronLogger) Info(msg string, _ ...interface{}) {
log.Info().Msgf("cron: %s", msg)
}
func (cronLogger) Error(err error, msg string, _ ...interface{}) {
log.Error().Err(err).Msgf("cron: %s", msg)
}
+7 -31
View File
@@ -2,16 +2,15 @@ package oauth
import (
gocontext "context"
gojson "encoding/json"
"net/http"
"github.com/markbates/goth"
"github.com/markbates/goth/gothic"
"github.com/markbates/goth/providers/gitea"
"github.com/rs/zerolog/log"
"github.com/thomiceli/opengist/internal/config"
"github.com/thomiceli/opengist/internal/db"
"github.com/thomiceli/opengist/internal/web/context"
"io"
"net/http"
)
type GiteaProvider struct {
@@ -80,34 +79,11 @@ func (p *GiteaCallbackProvider) GetProviderUserSSHKeys() ([]string, error) {
func (p *GiteaCallbackProvider) UpdateUserDB(user *db.User) {
user.GiteaID = p.User.UserID
user.AvatarURL = p.User.AvatarURL
}
resp, err := http.Get(urlJoin(config.C.GiteaUrl, "/api/v1/users/", p.User.UserID))
if err != nil {
log.Error().Err(err).Msg("Cannot get user from Gitea")
return
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
log.Error().Err(err).Msg("Cannot read Gitea response body")
return
}
var result map[string]interface{}
err = gojson.Unmarshal(body, &result)
if err != nil {
log.Error().Err(err).Msg("Cannot unmarshal Gitea response body")
return
}
field, ok := result["avatar_url"]
if !ok {
log.Error().Msg("Field 'avatar_url' not found in Gitea JSON response")
return
}
user.AvatarURL = field.(string)
func (p *GiteaCallbackProvider) IsAdmin() bool {
return false
}
func NewGiteaCallbackProvider(user *goth.User) CallbackProvider {
+4
View File
@@ -77,6 +77,10 @@ func (p *GitHubCallbackProvider) UpdateUserDB(user *db.User) {
user.AvatarURL = "https://avatars.githubusercontent.com/u/" + p.User.UserID + "?v=4"
}
func (p *GitHubCallbackProvider) IsAdmin() bool {
return false
}
func NewGitHubCallbackProvider(user *goth.User) CallbackProvider {
return &GitHubCallbackProvider{
User: user,
+4
View File
@@ -111,6 +111,10 @@ func (p *GitLabCallbackProvider) UpdateUserDB(user *db.User) {
user.AvatarURL = field.(string)
}
func (p *GitLabCallbackProvider) IsAdmin() bool {
return false
}
func NewGitLabCallbackProvider(user *goth.User) CallbackProvider {
return &GitLabCallbackProvider{
User: user,
+32
View File
@@ -3,9 +3,12 @@ package oauth
import (
gocontext "context"
"errors"
"slices"
"github.com/markbates/goth"
"github.com/markbates/goth/gothic"
"github.com/markbates/goth/providers/openidConnect"
"github.com/rs/zerolog/log"
"github.com/thomiceli/opengist/internal/config"
"github.com/thomiceli/opengist/internal/db"
"github.com/thomiceli/opengist/internal/web/context"
@@ -37,6 +40,10 @@ func (p *OIDCProvider) RegisterProvider() error {
}
func (p *OIDCProvider) BeginAuthHandler(ctx *context.Context) {
if err := enablePKCE(ctx, OpenIDConnectString); err != nil {
log.Error().Err(err).Msg("Cannot enable PKCE for OIDC provider")
}
ctxValue := gocontext.WithValue(ctx.Request().Context(), gothic.ProviderParamKey, OpenIDConnectString)
ctx.SetRequest(ctx.Request().WithContext(ctxValue))
@@ -79,6 +86,31 @@ func (p *OIDCCallbackProvider) UpdateUserDB(user *db.User) {
user.AvatarURL = p.User.AvatarURL
}
func (p *OIDCCallbackProvider) IsAdmin() bool {
if config.C.OIDCAdminGroup == "" {
return false
}
groupClaimName := config.C.OIDCGroupClaimName
if groupClaimName == "" {
return false
}
groups, ok := p.User.RawData[groupClaimName].([]interface{})
if !ok {
return false
}
var groupNames []string
for _, group := range groups {
if groupName, ok := group.(string); ok {
groupNames = append(groupNames, groupName)
}
}
return slices.Contains(groupNames, config.C.OIDCAdminGroup)
}
func NewOIDCCallbackProvider(user *goth.User) CallbackProvider {
return &OIDCCallbackProvider{
User: user,
+51
View File
@@ -0,0 +1,51 @@
package oauth
import (
"github.com/markbates/goth"
"github.com/markbates/goth/providers/openidConnect"
"github.com/thomiceli/opengist/internal/web/context"
"golang.org/x/oauth2"
)
const codeVerifierSessionKey = "oauth_code_verifier"
func enablePKCE(ctx *context.Context, providerName string) error {
gothProvider, err := goth.GetProvider(providerName)
if err != nil {
return err
}
oidcProvider, ok := gothProvider.(*openidConnect.Provider)
if !ok {
return nil
}
verifier := oauth2.GenerateVerifier()
sess := ctx.GetSession()
sess.Values[codeVerifierSessionKey] = verifier
ctx.SaveSession(sess)
oidcProvider.SetAuthCodeOptions(map[string]string{
"code_challenge": oauth2.S256ChallengeFromVerifier(verifier),
"code_challenge_method": "S256",
})
return nil
}
func injectCodeVerifier(ctx *context.Context) {
sess := ctx.GetSession()
verifier, ok := sess.Values[codeVerifierSessionKey].(string)
if !ok || verifier == "" {
return
}
req := ctx.Request()
q := req.URL.Query()
q.Set("code_verifier", verifier)
req.URL.RawQuery = q.Encode()
delete(sess.Values, codeVerifierSessionKey)
ctx.SaveSession(sess)
}
+31 -4
View File
@@ -2,15 +2,16 @@ package oauth
import (
"fmt"
"io"
"net/http"
"net/url"
"strings"
"github.com/markbates/goth"
"github.com/markbates/goth/gothic"
"github.com/rs/zerolog/log"
"github.com/thomiceli/opengist/internal/db"
"github.com/thomiceli/opengist/internal/web/context"
"io"
"net/http"
"net/url"
"strings"
)
const (
@@ -32,6 +33,7 @@ type CallbackProvider interface {
GetProviderUserID(user *db.User) bool
GetProviderUserSSHKeys() ([]string, error)
UpdateUserDB(user *db.User)
IsAdmin() bool
}
func DefineProvider(provider string, url string) (Provider, error) {
@@ -50,6 +52,8 @@ func DefineProvider(provider string, url string) (Provider, error) {
}
func CompleteUserAuth(ctx *context.Context) (CallbackProvider, error) {
injectCodeVerifier(ctx)
user, err := gothic.CompleteUserAuth(ctx.Response(), ctx.Request())
if err != nil {
return nil, err
@@ -69,6 +73,29 @@ func CompleteUserAuth(ctx *context.Context) (CallbackProvider, error) {
return nil, fmt.Errorf("unsupported provider %s", user.Provider)
}
func NewCallbackProviderFromSession(provider string, userID string, nickname string, email string, avatarURL string) (CallbackProvider, error) {
user := &goth.User{
Provider: provider,
UserID: userID,
NickName: nickname,
Email: email,
AvatarURL: avatarURL,
}
switch provider {
case GitHubProviderString:
return NewGitHubCallbackProvider(user), nil
case GitLabProviderString:
return NewGitLabCallbackProvider(user), nil
case GiteaProviderString:
return NewGiteaCallbackProvider(user), nil
case OpenIDConnectString:
return NewOIDCCallbackProvider(user), nil
}
return nil, fmt.Errorf("unsupported provider %s", provider)
}
func urlJoin(base string, elem ...string) string {
joined, err := url.JoinPath(base, elem...)
if err != nil {
+10 -6
View File
@@ -2,7 +2,14 @@ package cli
import (
"fmt"
"os"
"os/signal"
"path"
"path/filepath"
"syscall"
"github.com/rs/zerolog/log"
"github.com/thomiceli/opengist/internal/actions"
"github.com/thomiceli/opengist/internal/auth/webauthn"
"github.com/thomiceli/opengist/internal/config"
"github.com/thomiceli/opengist/internal/db"
@@ -12,11 +19,6 @@ import (
"github.com/thomiceli/opengist/internal/web/handlers/metrics"
"github.com/thomiceli/opengist/internal/web/server"
"github.com/urfave/cli/v2"
"os"
"os/signal"
"path"
"path/filepath"
"syscall"
)
var CmdVersion = cli.Command{
@@ -37,9 +39,10 @@ var CmdStart = cli.Command{
Initialize(ctx)
httpServer := server.NewServer(os.Getenv("OG_DEV") == "1", path.Join(config.GetHomeDir(), "sessions"), false)
httpServer := server.NewServer(os.Getenv("OG_DEV") == "1")
go httpServer.Start()
go ssh.Start()
stopCron := actions.StartCron()
var metricsServer *metrics.Server
if config.C.MetricsEnabled {
@@ -48,6 +51,7 @@ var CmdStart = cli.Command{
}
<-stopCtx.Done()
stopCron()
shutdown(httpServer, metricsServer)
return nil
},
+13 -8
View File
@@ -2,7 +2,6 @@ package config
import (
"fmt"
"github.com/thomiceli/opengist/internal/session"
"io"
"net/url"
"os"
@@ -13,6 +12,8 @@ import (
"strings"
"time"
"github.com/thomiceli/opengist/internal/session"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
"gopkg.in/yaml.v3"
@@ -37,11 +38,12 @@ type config struct {
DBUri string `yaml:"db-uri" env:"OG_DB_URI"`
DBFilename string `yaml:"db-filename" env:"OG_DB_FILENAME"` // deprecated
IndexEnabled bool `yaml:"index.enabled" env:"OG_INDEX_ENABLED"` // deprecated
Index string `yaml:"index" env:"OG_INDEX"`
BleveDirname string `yaml:"index.dirname" env:"OG_INDEX_DIRNAME"` // deprecated
MeiliHost string `yaml:"index.meili.host" env:"OG_MEILI_HOST"`
MeiliAPIKey string `yaml:"index.meili.api-key" env:"OG_MEILI_API_KEY"`
IndexEnabled bool `yaml:"index.enabled" env:"OG_INDEX_ENABLED"` // deprecated
Index string `yaml:"index" env:"OG_INDEX"`
BleveDirname string `yaml:"index.dirname" env:"OG_INDEX_DIRNAME"` // deprecated
MeiliHost string `yaml:"index.meili.host" env:"OG_MEILI_HOST"`
MeiliAPIKey string `yaml:"index.meili.api-key" env:"OG_MEILI_API_KEY"`
SearchDefault string `yaml:"search.default" env:"OG_SEARCH_DEFAULT"`
GitDefaultBranch string `yaml:"git.default-branch" env:"OG_GIT_DEFAULT_BRANCH"`
@@ -51,13 +53,14 @@ type config struct {
HttpPort string `yaml:"http.port" env:"OG_HTTP_PORT"`
HttpGit bool `yaml:"http.git-enabled" env:"OG_HTTP_GIT_ENABLED"`
ApiEnabled bool `yaml:"api.enabled" env:"OG_API_ENABLED"`
UnixSocketPermissions string `yaml:"unix-socket-permissions" env:"OG_UNIX_SOCKET_PERMISSIONS"`
SshGit bool `yaml:"ssh.git-enabled" env:"OG_SSH_GIT_ENABLED"`
SshHost string `yaml:"ssh.host" env:"OG_SSH_HOST"`
SshPort string `yaml:"ssh.port" env:"OG_SSH_PORT"`
SshExternalDomain string `yaml:"ssh.external-domain" env:"OG_SSH_EXTERNAL_DOMAIN"`
SshKeygen string `yaml:"ssh.keygen-executable" env:"OG_SSH_KEYGEN_EXECUTABLE"`
GithubClientKey string `yaml:"github.client-key" env:"OG_GITHUB_CLIENT_KEY"`
GithubSecret string `yaml:"github.secret" env:"OG_GITHUB_SECRET"`
@@ -110,6 +113,7 @@ func configWithDefaults() (*config, error) {
c.OpengistHome = ""
c.DBUri = "opengist.db"
c.Index = "bleve"
c.SearchDefault = "content"
c.SqliteJournalMode = "WAL"
@@ -117,12 +121,13 @@ func configWithDefaults() (*config, error) {
c.HttpPort = "6157"
c.HttpGit = true
c.ApiEnabled = true
c.UnixSocketPermissions = "0666"
c.SshGit = true
c.SshHost = "0.0.0.0"
c.SshPort = "2222"
c.SshKeygen = "ssh-keygen"
c.GitlabName = "GitLab"
+34 -3
View File
@@ -8,9 +8,14 @@ import (
)
const (
NoPermission = 0
ReadPermission = 1
ReadWritePermission = 2
ScopeGist = iota
ScopeUser
)
const (
NoPermission = iota
ReadPermission
ReadWritePermission
)
type AccessToken struct {
@@ -24,6 +29,7 @@ type AccessToken struct {
User User `validate:"-"`
ScopeGist uint // 0 = none, 1 = read, 2 = read+write
ScopeUser uint // 0 = none, 1 = read, 2 = read+write
}
// GenerateToken creates a new random token and returns the plain text token.
@@ -100,11 +106,30 @@ func (t *AccessToken) HasGistWritePermission() bool {
return t.ScopeGist >= ReadWritePermission
}
func (t *AccessToken) HasUserReadPermission() bool {
return t.ScopeUser >= ReadPermission
}
func (t *AccessToken) HasUserWritePermission() bool {
return t.ScopeUser >= ReadWritePermission
}
func (t *AccessToken) CheckForPermission(scope, permission uint) bool {
if scope == ScopeGist {
return t.ScopeGist >= permission
}
if scope == ScopeUser {
return t.ScopeUser >= permission
}
return false
}
// -- DTO -- //
type AccessTokenDTO struct {
Name string `form:"name" validate:"required,max=255"`
ScopeGist uint `form:"scope_gist" validate:"min=0,max=2"`
ScopeUser uint `form:"scope_user" validate:"min=0,max=2"`
ExpiresAt string `form:"expires_at"` // empty means no expiration, otherwise date format (YYYY-MM-DD)
}
@@ -120,6 +145,12 @@ func (dto *AccessTokenDTO) ToAccessToken() *AccessToken {
return &AccessToken{
Name: dto.Name,
ScopeGist: dto.ScopeGist,
ScopeUser: dto.ScopeUser,
ExpiresAt: expiresAt,
}
}
// SaveAccessTokenForTest is exported for tests only; saves the entire AccessToken row.
func SaveAccessTokenForTest(t *AccessToken) error {
return db.Save(t).Error
}
+23
View File
@@ -0,0 +1,23 @@
package db_test
import (
"testing"
"github.com/stretchr/testify/require"
"github.com/thomiceli/opengist/internal/db"
)
func TestAccessTokenScopeUser(t *testing.T) {
tok := &db.AccessToken{ScopeUser: db.ReadPermission}
require.True(t, tok.HasUserReadPermission(), "ScopeUser=1 should grant user read")
tokNone := &db.AccessToken{ScopeUser: 0}
require.False(t, tokNone.HasUserReadPermission(), "ScopeUser=0 should not grant user read")
}
func TestAccessTokenDTOScopeUser(t *testing.T) {
dto := &db.AccessTokenDTO{Name: "t", ScopeGist: 1, ScopeUser: 1}
tok := dto.ToAccessToken()
require.Equal(t, uint(1), tok.ScopeUser)
require.Equal(t, uint(1), tok.ScopeGist)
}
+52
View File
@@ -0,0 +1,52 @@
package db
import (
"time"
"gorm.io/gorm/clause"
)
// ActionLock is a DB-backed lease used to single-flight an action across
// multiple Opengist instances sharing the same database. Each lock is one row
// keyed by Action (the action's identifier); LockedUntil holds the Unix
// timestamp the current lease expires at (0 = free).
type ActionLock struct {
Action int `gorm:"primaryKey"`
LockedUntil int64
}
func (ActionLock) TableName() string {
return "action_lock"
}
// AcquireLock atomically grabs the lock for action when it is free or its lease
// has expired, extending the lease by leaseTTL. It returns true only for the
// single caller that won the row. The conditional UPDATE is what makes this
// safe across SQLite/PostgreSQL/MySQL: concurrent writers serialize on the row
// (SQLite serializes all writes), so at most one re-evaluates the
// `locked_until < now` predicate to true. leaseTTL only needs to outlast a
// normal run; it's a safety net so a crashed holder doesn't block future runs.
func AcquireLock(action int, leaseTTL time.Duration) (bool, error) {
now := time.Now().Unix()
if err := db.Clauses(clause.OnConflict{DoNothing: true}).
Create(&ActionLock{Action: action, LockedUntil: 0}).Error; err != nil {
return false, err
}
res := db.Model(&ActionLock{}).
Where("action = ? AND locked_until < ?", action, now).
Update("locked_until", time.Now().Add(leaseTTL).Unix())
if res.Error != nil {
return false, res.Error
}
return res.RowsAffected == 1, nil
}
// ReleaseLock frees the lock for action so the next run can acquire it
// immediately instead of waiting for the lease to expire.
func ReleaseLock(action int) error {
return db.Model(&ActionLock{}).
Where("action = ?", action).
Update("locked_until", 0).Error
}
+1 -1
View File
@@ -155,7 +155,7 @@ func Setup(dbUri string) error {
return err
}
if err = db.AutoMigrate(&User{}, &Gist{}, &SSHKey{}, &AdminSetting{}, &Invitation{}, &WebAuthnCredential{}, &TOTP{}, &GistTopic{}, &GistLanguage{}, &GistInitQueue{}, &AccessToken{}); err != nil {
if err = db.AutoMigrate(&User{}, &Gist{}, &SSHKey{}, &AdminSetting{}, &Invitation{}, &WebAuthnCredential{}, &TOTP{}, &GistTopic{}, &GistLanguage{}, &GistInitQueue{}, &AccessToken{}, &ActionLock{}); err != nil {
return err
}
+309 -51
View File
@@ -12,6 +12,7 @@ import (
"github.com/alecthomas/chroma/v2/lexers"
"github.com/dustin/go-humanize"
"github.com/rs/zerolog/log"
"github.com/thomiceli/opengist/internal/config"
"github.com/thomiceli/opengist/internal/git"
"github.com/thomiceli/opengist/internal/index"
"gorm.io/gorm"
@@ -71,6 +72,7 @@ type Gist struct {
Uuid string
Title string
URL string
URLNormalized string
Preview string
PreviewFilename string
PreviewMimeType string
@@ -83,6 +85,7 @@ type Gist struct {
NbForks int
CreatedAt int64
UpdatedAt int64
ExpiresAt int64 // 0: never expires
Likes []User `gorm:"many2many:likes;constraint:OnUpdate:CASCADE,OnDelete:CASCADE"`
Forked *Gist `gorm:"foreignKey:ForkedID;constraint:OnUpdate:CASCADE,OnDelete:SET NULL"`
@@ -98,7 +101,14 @@ type Like struct {
CreatedAt int64
}
func (gist *Gist) BeforeSave(_ *gorm.DB) error {
gist.URLNormalized = strings.ToLower(gist.URL)
return nil
}
func (gist *Gist) BeforeDelete(tx *gorm.DB) error {
gist.DeleteRepository()
gist.RemoveFromIndex()
// Decrement fork counter if the gist was forked
err := tx.Model(&Gist{}).
Omit("updated_at").
@@ -110,13 +120,20 @@ func (gist *Gist) BeforeDelete(tx *gorm.DB) error {
func GetGist(user string, gistUuid string) (*Gist, error) {
gist := new(Gist)
err := db.Preload("User").Preload("Forked.User").Preload("Topics").
Where("(gists.uuid like ? OR gists.url = ?) AND users.username like ?", gistUuid+"%", gistUuid, user).
Where("(gists.uuid LIKE ? OR gists.url_normalized = ?) AND users.username_normalized = ?",
strings.ToLower(gistUuid)+"%", strings.ToLower(gistUuid), strings.ToLower(user)).
Joins("join users on gists.user_id = users.id").
First(&gist).Error
return gist, err
}
func GetGistByUUID(uuid string) (*Gist, error) {
gist := new(Gist)
err := db.Preload("User").Preload("Forked.User").Where("uuid = ?", uuid).First(gist).Error
return gist, err
}
func GetGistByID(gistId string) (*Gist, error) {
gist := new(Gist)
err := db.Preload("User").Preload("Forked.User").Preload("Topics").
@@ -126,14 +143,88 @@ func GetGistByID(gistId string) (*Gist, error) {
return gist, err
}
func GetAllGistsForCurrentUser(currentUserId uint, offset int, sort string, order string) ([]*Gist, error) {
// GetAllGistsForCurrentUser returns gists visible to currentUserId - all public
// gists plus the user's own private/unlisted ones - ordered by sort/order and
// paginated to one extra row (the 11th is the peek-next sentinel).
// `since`, when non-nil, restricts results to gists updated at or after that
// instant (used by the API; the web handler passes nil).
func GetAllGistsForCurrentUser(currentUserId uint, since *time.Time, offset int, sort string, order string, limit int, perPage int) ([]*Gist, error) {
var gists []*Gist
err := db.Preload("User").
query := db.Preload("User").
Preload("Forked.User").
Preload("Topics").
Where("gists.private = 0 or gists.user_id = ?", currentUserId).
Limit(11).
Offset(offset * 10).
Where("gists.private = 0 or gists.user_id = ?", currentUserId)
if since != nil {
query = query.Where("gists.updated_at >= ?", since.Unix())
}
err := query.
Limit(limit).
Offset(offset * perPage).
Order(sort + "_at " + order).
Find(&gists).Error
return gists, err
}
// GetAllGistsFromUserVisibleTo returns gists owned by fromUserId, filtered
// to what currentUserId is allowed to see (public always; private/unlisted
// only when currentUserId == fromUserId). Same pagination/since shape as
// the other API list helpers. Pass currentUserId=0 to force the
// public-only subset.
func GetAllGistsFromUserVisibleTo(fromUserId uint, currentUserId uint, since *time.Time, offset int, sort string, order string, limit int, perPage int) ([]*Gist, error) {
var gists []*Gist
query := gistsFromUserStatement(fromUserId, currentUserId)
if since != nil {
query = query.Where("gists.updated_at >= ?", since.Unix())
}
err := query.
Limit(limit).
Offset(offset * perPage).
Order("gists." + sort + "_at " + order).
Find(&gists).Error
return gists, err
}
// GetAllGistsOfUser returns every gist owned by userID - public, unlisted,
// and private - with the same pagination/since semantics as GetAllGistsForCurrentUser.
// Used by the API list endpoint for callers whose
// token holds gist:read: they see all of their own content but nothing from
// other users (others' public gists live under /gists/public).
func GetAllGistsOfUser(userID uint, since *time.Time, offset int, sort string, order string, limit int, perPage int) ([]*Gist, error) {
var gists []*Gist
query := db.Preload("User").
Preload("Forked.User").
Preload("Topics").
Where("gists.user_id = ?", userID)
if since != nil {
query = query.Where("gists.updated_at >= ?", since.Unix())
}
err := query.
Limit(limit).
Offset(offset * perPage).
Order(sort + "_at " + order).
Find(&gists).Error
return gists, err
}
// GetAllPublicGistsOfUser returns only the public gists owned by userID, with
// the same pagination/since semantics as GetAllGistsForCurrentUser. Used by
// the API list endpoint for callers that authenticate but whose token lacks
// gist:read - they get only their own public gists.
func GetAllPublicGistsOfUser(userID uint, since *time.Time, offset int, sort string, order string, limit int, perPage int) ([]*Gist, error) {
var gists []*Gist
query := db.Preload("User").
Preload("Forked.User").
Preload("Topics").
Where("gists.private = 0 AND gists.user_id = ?", userID)
if since != nil {
query = query.Where("gists.updated_at >= ?", since.Unix())
}
err := query.
Limit(limit).
Offset(offset * perPage).
Order(sort + "_at " + order).
Find(&gists).Error
@@ -228,10 +319,19 @@ func likedStatement(fromUserId uint, currentUserId uint) *gorm.DB {
Joins("join users on likes.user_id = users.id")
}
func GetAllGistsLikedByUser(fromUserId uint, currentUserId uint, offset int, sort string, order string) ([]*Gist, error) {
// GetAllGistsLikedByUser returns gists that fromUserId has starred, filtered
// to what currentUserId is allowed to see. `since`, when non-nil, restricts
// results to gists updated at or after that instant (used by the API; the web
// handler passes nil for both since and the explicit pagination args).
func GetAllGistsLikedByUser(fromUserId uint, currentUserId uint, since *time.Time, offset int, sort string, order string, limit int, perPage int) ([]*Gist, error) {
var gists []*Gist
err := likedStatement(fromUserId, currentUserId).Limit(11).
Offset(offset * 10).
query := likedStatement(fromUserId, currentUserId)
if since != nil {
query = query.Where("gists.updated_at >= ?", since.Unix())
}
err := query.
Limit(limit).
Offset(offset * perPage).
Order("gists." + sort + "_at " + order).
Find(&gists).Error
return gists, err
@@ -250,10 +350,19 @@ func forkedStatement(fromUserId uint, currentUserId uint) *gorm.DB {
Joins("join users on gists.user_id = users.id")
}
func GetAllGistsForkedByUser(fromUserId uint, currentUserId uint, offset int, sort string, order string) ([]*Gist, error) {
// GetAllGistsForkedByUser returns gists forked by fromUserId, filtered to
// what currentUserId is allowed to see. `since`, when non-nil, restricts
// results to gists updated at or after that instant (used by the API; the
// web handler passes nil for both since and the explicit pagination args).
func GetAllGistsForkedByUser(fromUserId uint, currentUserId uint, since *time.Time, offset int, sort string, order string, limit int, perPage int) ([]*Gist, error) {
var gists []*Gist
err := forkedStatement(fromUserId, currentUserId).Limit(11).
Offset(offset * 10).
query := forkedStatement(fromUserId, currentUserId)
if since != nil {
query = query.Where("gists.updated_at >= ?", since.Unix())
}
err := query.
Limit(limit).
Offset(offset * perPage).
Order("gists." + sort + "_at " + order).
Find(&gists).Error
return gists, err
@@ -265,6 +374,59 @@ func CountAllGistsForkedByUser(fromUserId uint, currentUserId uint) (int64, erro
return count, err
}
// applySince narrows a gist query to rows updated at or after `since` when it
// is non-nil, matching the filter the API list queries apply. Kept separate so
// the count helpers stay in sync with their Find counterparts.
func applySince(q *gorm.DB, since *time.Time) *gorm.DB {
if since != nil {
return q.Where("gists.updated_at >= ?", since.Unix())
}
return q
}
// The Count* helpers below mirror the API list queries (including the optional
// `since` filter) so list responses can report a total. They're separate from
// the web UI's CountAll* helpers above, which don't take `since`.
func CountAllGistsForCurrentUser(currentUserId uint, since *time.Time) (int64, error) {
var count int64
q := applySince(db.Model(&Gist{}).Where("gists.private = 0 or gists.user_id = ?", currentUserId), since)
err := q.Count(&count).Error
return count, err
}
func CountAllGistsFromUserVisibleTo(fromUserId uint, currentUserId uint, since *time.Time) (int64, error) {
var count int64
err := applySince(gistsFromUserStatement(fromUserId, currentUserId).Model(&Gist{}), since).Count(&count).Error
return count, err
}
func CountAllGistsOfUser(userID uint, since *time.Time) (int64, error) {
var count int64
q := applySince(db.Model(&Gist{}).Where("gists.user_id = ?", userID), since)
err := q.Count(&count).Error
return count, err
}
func CountAllPublicGistsOfUser(userID uint, since *time.Time) (int64, error) {
var count int64
q := applySince(db.Model(&Gist{}).Where("gists.private = 0 AND gists.user_id = ?", userID), since)
err := q.Count(&count).Error
return count, err
}
func CountAllGistsLikedByUserSince(fromUserId uint, currentUserId uint, since *time.Time) (int64, error) {
var count int64
err := applySince(likedStatement(fromUserId, currentUserId).Model(&Gist{}), since).Count(&count).Error
return count, err
}
func CountAllGistsForkedByUserSince(fromUserId uint, currentUserId uint, since *time.Time) (int64, error) {
var count int64
err := applySince(forkedStatement(fromUserId, currentUserId).Model(&Gist{}), since).Count(&count).Error
return count, err
}
func GetAllGistsRows() ([]*Gist, error) {
var gists []*Gist
err := db.Table("gists").
@@ -328,11 +490,6 @@ func (gist *Gist) UpdateNoTimestamps() error {
}
func (gist *Gist) Delete() error {
err := gist.DeleteRepository()
if err != nil {
return err
}
return db.Delete(&gist).Error
}
@@ -382,19 +539,35 @@ func (gist *Gist) GetUsersLikes(offset int) ([]*User, error) {
return users, err
}
func (gist *Gist) GetForks(currentUserId uint, offset int) ([]*Gist, error) {
// GetForks returns gists that fork this gist, filtered to what
// currentUserId is allowed to see. `offset` is the page index (0-based);
// `limit` caps the returned slice (pass perPage+1 for the peek-next
// sentinel). `perPage` is the slice size used for the offset arithmetic
// (offset * perPage rows are skipped).
func (gist *Gist) GetForks(currentUserId uint, offset int, limit int, perPage int) ([]*Gist, error) {
var gists []*Gist
err := db.Model(&gist).Preload("User").
Where("forked_id = ?", gist.ID).
Where("(gists.private = 0) or (gists.private > 0 and gists.user_id = ?)", currentUserId).
Limit(11).
Offset(offset * 10).
Limit(limit).
Offset(offset * perPage).
Order("updated_at desc").
Find(&gists).Error
return gists, err
}
// CountForks returns the number of forks of this gist visible to currentUserId,
// using the same visibility filter as GetForks (pass 0 for the public subset).
func (gist *Gist) CountForks(currentUserId uint) (int64, error) {
var count int64
err := db.Model(&Gist{}).
Where("forked_id = ?", gist.ID).
Where("(gists.private = 0) or (gists.private > 0 and gists.user_id = ?)", currentUserId).
Count(&count).Error
return count, err
}
func (gist *Gist) CanWrite(user *User) bool {
return user != nil && gist.UserID == user.ID
}
@@ -403,18 +576,21 @@ func (gist *Gist) InitRepository() error {
return git.InitRepository(gist.User.Username, gist.Uuid)
}
func (gist *Gist) DeleteRepository() error {
return git.DeleteRepository(gist.User.Username, gist.Uuid)
func (gist *Gist) DeleteRepository() {
err := git.DeleteRepository(gist.User.Username, gist.Uuid)
if err != nil {
log.Warn().Err(err).Msgf("Could not delete repository %s/%s", gist.User.Username, gist.Uuid)
}
}
func (gist *Gist) Files(revision string, truncate bool) ([]*git.File, error) {
filesCat, err := git.CatFileBatch(gist.User.Username, gist.Uuid, revision, truncate)
func (gist *Gist) Files(revision string, truncate bool) ([]*git.File, bool, error) {
filesCat, gistTruncated, err := git.CatFileBatch(gist.User.Username, gist.Uuid, revision, truncate)
if err != nil {
// if the revision or the file do not exist
if exiterr, ok := err.(*exec.ExitError); ok && exiterr.ExitCode() == 128 {
return nil, &git.RevisionNotFoundError{}
return nil, false, &git.RevisionNotFoundError{}
}
return nil, err
return nil, false, err
}
var files []*git.File
@@ -435,7 +611,7 @@ func (gist *Gist) Files(revision string, truncate bool) ([]*git.File, error) {
MimeType: git.DetectMimeType([]byte(shortContent), filepath.Ext(fileCat.Name)),
})
}
return files, err
return files, gistTruncated, err
}
func (gist *Gist) File(revision string, filename string, truncate bool) (*git.File, error) {
@@ -474,8 +650,55 @@ func (gist *Gist) FileNames(revision string) ([]string, error) {
return git.GetFilesOfRepository(gist.User.Username, gist.Uuid, revision)
}
func (gist *Gist) Log(skip int) ([]*git.Commit, error) {
return git.GetLog(gist.User.Username, gist.Uuid, skip)
// GistCommit pairs a raw git commit with the Opengist account whose email
// matches the commit's AuthorEmail (when one exists). The git.Commit pointer
// is embedded so callers/templates can read AuthorName, Hash, Timestamp,
// Files, etc. directly. User is nil when no account matches - callers can
// fall back to the embedded AuthorName/AuthorEmail.
type GistCommit struct {
*git.Commit
User *User
}
// Log returns the gist's commit history starting from `revision` (pass
// "HEAD" for the full history or a SHA to walk from a specific commit
// downward), with each commit's author resolved to an Opengist user via a
// single bulk email lookup. Lookup is case-insensitive on both sides -
// matches the historical web behavior even when the DB stores mixed-case
// emails. `skip` is the number of commits to skip from the top of the walk
// (use offset*per_page for paging); `limit` caps the returned slice (pass
// per_page+1 to enable the peek-next sentinel trick).
func (gist *Gist) Log(revision string, skip int, limit int) ([]*GistCommit, error) {
raw, err := git.GetLog(gist.User.Username, gist.Uuid, revision, skip, limit)
if err != nil {
return nil, err
}
// Collect distinct lowercased author emails.
loweredSet := make(map[string]struct{}, len(raw))
for _, c := range raw {
if c.AuthorEmail == "" {
continue
}
loweredSet[strings.ToLower(c.AuthorEmail)] = struct{}{}
}
// One IN query, then re-key by lowercased email so we can look up
// case-insensitively even if the DB column holds a mixed-case value.
byDBEmail, _ := GetUsersFromEmails(loweredSet)
byLowered := make(map[string]*User, len(byDBEmail))
for e, u := range byDBEmail {
byLowered[strings.ToLower(e)] = u
}
out := make([]*GistCommit, len(raw))
for i, c := range raw {
out[i] = &GistCommit{
Commit: c,
User: byLowered[strings.ToLower(c.AuthorEmail)],
}
}
return out, nil
}
func (gist *Gist) NbCommits() (string, error) {
@@ -605,8 +828,36 @@ func (gist *Gist) Identifier() string {
return gist.Uuid
}
// HTTPCloneURL returns the HTTPS clone URL (`{baseURL}/{user}/{identifier}.git`).
// Returns "" when HTTP git access is disabled (config.HttpGit == false).
func (gist *Gist) HTTPCloneURL(baseURL string) string {
if !config.C.HttpGit {
return ""
}
return baseURL + "/" + gist.User.Username + "/" + gist.Identifier() + ".git"
}
// SSHCloneURL returns the SSH clone URL. `fallbackHost` is the request's Host
// header (or any host:port-shaped string) used when SshExternalDomain isn't
// configured — only its hostname part is kept. Returns "" when SSH git access
// is disabled (config.SshGit == false).
func (gist *Gist) SSHCloneURL(fallbackHost string) string {
if !config.C.SshGit {
return ""
}
sshDomain := config.C.SshExternalDomain
if sshDomain == "" {
sshDomain = strings.Split(fallbackHost, ":")[0]
}
path := gist.User.Username + "/" + gist.Identifier() + ".git"
if config.C.SshPort == "22" {
return sshDomain + ":" + path
}
return "ssh://" + sshDomain + ":" + config.C.SshPort + "/" + path
}
func (gist *Gist) GetLanguagesFromFiles() ([]string, error) {
files, err := gist.Files("HEAD", true)
files, _, err := gist.Files("HEAD", true)
if err != nil {
return nil, err
}
@@ -687,7 +938,7 @@ func (gist *Gist) UpdateLanguages() {
}
func (gist *Gist) ToDTO() (*GistDTO, error) {
files, err := gist.Files("HEAD", false)
files, _, err := gist.Files("HEAD", false)
if err != nil {
return nil, err
}
@@ -720,13 +971,19 @@ func (gist *Gist) ToDTO() (*GistDTO, error) {
// -- DTO -- //
type GistDTO struct {
Title string `validate:"max=250" form:"title"`
Description string `validate:"max=1000" form:"description"`
URL string `validate:"max=32,alphanumdashorempty" form:"url"`
Files []FileDTO `validate:"min=1,dive"`
Name []string `form:"name"`
Content []string `form:"content"`
Topics string `validate:"gisttopics" form:"topics"`
Title string `validate:"max=250" form:"title"`
Description string `validate:"max=1000" form:"description"`
URL string `validate:"max=32,alphanumdashorempty" form:"url"`
Files []FileDTO `validate:"min=1,dive"`
Name []string `form:"name"`
Content []string `form:"content"`
Topics string `validate:"gisttopics" form:"topics"`
UploadedFilesUUID []string `validate:"omitempty,dive,required,uuid" form:"uploadedfile_uuid"`
UploadedFilesNames []string `validate:"omitempty,dive,required" form:"uploadedfile_filename"`
BinaryFileOldName []string `form:"binary_old_name"`
BinaryFileNewName []string `form:"binary_new_name"`
Expire ExpirationType `validate:"omitempty,oneof=never 1hour 12hours 1day 7days 15days custom" form:"expire"`
ExpireAt string `validate:"expirationdate" form:"expire_at"`
VisibilityDTO
}
@@ -775,7 +1032,7 @@ func (dto *GistDTO) TopicStrToSlice() []GistTopic {
// -- Index -- //
func (gist *Gist) ToIndexedGist() (*index.Gist, error) {
files, err := gist.Files("HEAD", true)
files, _, err := gist.Files("HEAD", true)
if err != nil {
return nil, err
}
@@ -806,18 +1063,19 @@ func (gist *Gist) ToIndexedGist() (*index.Gist, error) {
}
indexedGist := &index.Gist{
GistID: gist.ID,
UserID: gist.UserID,
Visibility: gist.Private.Uint(),
Username: gist.User.Username,
Title: gist.Title,
Content: wholeContent,
Filenames: fileNames,
Extensions: exts,
Languages: langs,
Topics: topics,
CreatedAt: gist.CreatedAt,
UpdatedAt: gist.UpdatedAt,
GistID: gist.ID,
UserID: gist.UserID,
Visibility: gist.Private.Uint(),
Username: gist.User.Username,
Description: gist.Description,
Title: gist.Title,
Content: wholeContent,
Filenames: fileNames,
Extensions: exts,
Languages: langs,
Topics: topics,
CreatedAt: gist.CreatedAt,
UpdatedAt: gist.UpdatedAt,
}
return indexedGist, nil
+129
View File
@@ -0,0 +1,129 @@
package db
import (
"encoding/base64"
"net/url"
"strconv"
"time"
"github.com/thomiceli/opengist/internal/render/lang"
"github.com/thomiceli/opengist/internal/web/handlers/api/v1/types"
)
// ToAPISimple returns the v1 API list-shape representation of the gist. baseURL is
// the scheme+host root (no trailing slash) the caller derived from config or
// the request.
func (gist *Gist) ToAPISimple(baseURL string) types.GistSimple {
sshHost := ""
if u, err := url.Parse(baseURL); err == nil {
sshHost = u.Host
}
var expiresAt *time.Time
if gist.ExpiresAt > 0 {
t := time.Unix(gist.ExpiresAt, 0).UTC()
expiresAt = &t
}
return types.GistSimple{
ID: gist.Uuid,
Owner: gist.User.ToSimpleAPI(),
Title: gist.Title,
HTMLUrl: baseURL + "/" + gist.User.Username + "/" + gist.Identifier(),
SlugUrl: gist.Identifier(),
Description: gist.Description,
Public: gist.Private == PublicVisibility,
Visibility: gist.Private.String(),
LikeCount: gist.NbLikes,
ForkCount: gist.NbForks,
CloneUrl: gist.HTTPCloneURL(baseURL),
SSHUrl: gist.SSHCloneURL(sshHost),
Topics: gist.TopicsSlice(),
CreatedAt: time.Unix(gist.CreatedAt, 0).UTC(),
UpdatedAt: time.Unix(gist.UpdatedAt, 0).UTC(),
ExpiresAt: expiresAt,
}
}
// ToAPI returns the v1 API detail-shape representation, including file
// contents at `revision` and the 10 most recent commits up to that
// revision. Pass "HEAD" for the current state; pass a SHA to render the
// gist (and its history) as it stood at that commit. Returns any error
// encountered while listing the gist's files or commit log (an unknown
// revision surfaces here as the Files error).
func (gist *Gist) ToAPI(baseURL string, revision string) (types.Gist, error) {
files, truncated, err := gist.Files(revision, true)
if err != nil {
return types.Gist{}, err
}
fm := make(map[string]types.GistFile, len(files))
for _, f := range files {
ff := types.GistFile{
Filename: f.Filename,
Type: f.MimeType.ContentType,
Language: lang.Parse(f),
Size: int(f.Size),
Truncated: f.Truncated,
}
if f.MimeType.CanBeEdited() {
ff.Content = f.Content
ff.Encoding = f.MimeType.Charset
} else {
ff.Content = base64.StdEncoding.EncodeToString([]byte(f.Content))
ff.Encoding = "base64"
}
fm[f.Filename] = ff
}
var forked *types.GistSimple
if gist.Forked != nil {
ff := gist.Forked.ToAPISimple(baseURL)
forked = &ff
}
forks, err := gist.GetForks(gist.UserID, 0, 11, 10)
if err != nil {
return types.Gist{}, err
}
forksMap := make([]types.GistSimple, 0)
for _, fork := range forks {
forksMap = append(forksMap, fork.ToAPISimple(baseURL))
}
logCommits, err := gist.Log(revision, 0, 10)
if err != nil {
return types.Gist{}, err
}
commits := make([]types.GistCommit, 0, len(logCommits))
for _, c := range logCommits {
commits = append(commits, c.ToAPI())
}
return types.Gist{
GistSimple: gist.ToAPISimple(baseURL),
ForkOf: forked,
Forks: forksMap,
Files: fm,
Commits: commits,
Truncated: truncated,
}, nil
}
// ToAPI converts a single resolved commit into its API shape. Shared by the
// gist-detail endpoint (10-most-recent embed) and the dedicated
// /gists/:uuid/commits endpoint so the wire shape is identical.
func (c *GistCommit) ToAPI() types.GistCommit {
ts, _ := strconv.ParseInt(c.Timestamp, 10, 64)
entry := types.GistCommit{
Version: c.Hash,
Author: types.CommitAuthor{Name: c.AuthorName, Email: c.AuthorEmail},
ChangeStatus: types.CommitChangeStatus{
Files: c.FilesChanged,
Additions: c.Additions,
Deletions: c.Deletions,
Total: c.Additions + c.Deletions,
},
CommittedAt: time.Unix(ts, 0).UTC(),
}
if c.User != nil {
s := c.User.ToSimpleAPI()
entry.User = &s
}
return entry
}
+96
View File
@@ -0,0 +1,96 @@
package db
import (
"strings"
"time"
"github.com/thomiceli/opengist/internal/validator"
)
// ExpirationType is the expiration choice a user can pick when creating a
// gist: either a fixed-duration preset, "custom" (paired with an explicit
// date), or "never". The empty value and "never" both mean "never expires".
type ExpirationType string
const (
ExpiryNever ExpirationType = "never"
ExpiryOneHour ExpirationType = "1hour"
ExpiryTwelveHours ExpirationType = "12hours"
ExpiryOneDay ExpirationType = "1day"
ExpirySevenDays ExpirationType = "7days"
ExpiryFifteenDays ExpirationType = "15days"
ExpiryCustom ExpirationType = "custom"
)
// Duration returns the time span of a fixed-duration preset, or 0 for
// "never", "custom", and any unknown value (those resolve their timestamp
// elsewhere).
func (e ExpirationType) Duration() time.Duration {
switch e {
case ExpiryOneHour:
return time.Hour
case ExpiryTwelveHours:
return 12 * time.Hour
case ExpiryOneDay:
return 24 * time.Hour
case ExpirySevenDays:
return 7 * 24 * time.Hour
case ExpiryFifteenDays:
return 15 * 24 * time.Hour
default:
return 0
}
}
// ExpiresAtTimestamp converts a fixed-duration preset into an absolute Unix
// expiration timestamp relative to now, returning 0 for "never"/"custom".
// Use GistDTO.ExpiresAtTimestamp to resolve a custom date.
func (e ExpirationType) ExpiresAtTimestamp() int64 {
d := e.Duration()
if d == 0 {
return 0
}
return time.Now().Add(d).Unix()
}
// ExpiresAtTimestamp resolves the gist's absolute expiration time (Unix
// seconds) from the chosen preset, or from the custom date when Expire is
// "custom". Returns 0 when the gist never expires or the custom date can't be
// parsed - callers validate the DTO (the `expirationdate` rule) beforehand, so
// an unparseable value here only happens on the non-validated push-option path,
// where "never" is the safe fallback.
func (dto *GistDTO) ExpiresAtTimestamp() int64 {
if dto.Expire != ExpiryCustom {
return dto.Expire.ExpiresAtTimestamp()
}
t, err := validator.ParseDateTime(strings.TrimSpace(dto.ExpireAt))
if err != nil {
return 0
}
return t.Unix()
}
func (gist *Gist) IsExpired() bool {
return gist.ExpiresAt > 0 && gist.ExpiresAt <= time.Now().Unix()
}
func DeleteExpiredGists() ([]*Gist, error) {
var gists []*Gist
err := db.Preload("User").
Where("expires_at > 0 AND expires_at <= ?", time.Now().Unix()).
Find(&gists).Error
if err != nil {
return nil, err
}
if len(gists) == 0 {
return nil, nil
}
if err := db.Delete(&gists).Error; err != nil {
return nil, err
}
return gists, nil
}
+53 -28
View File
@@ -2,7 +2,9 @@ package db
import (
"fmt"
"github.com/rs/zerolog/log"
"gorm.io/gorm"
)
type MigrationVersion struct {
@@ -12,60 +14,74 @@ type MigrationVersion struct {
func applyMigrations(dbInfo *databaseInfo) error {
switch dbInfo.Type {
case SQLite:
return applySqliteMigrations()
case PostgreSQL, MySQL:
return nil
case SQLite, PostgreSQL, MySQL:
return applyAllMigrations(dbInfo.Type)
default:
return fmt.Errorf("unknown database type: %s", dbInfo.Type)
}
}
func applySqliteMigrations() error {
// Create migration table if it doesn't exist
func applyAllMigrations(dbType databaseType) error {
if err := db.AutoMigrate(&MigrationVersion{}); err != nil {
log.Fatal().Err(err).Msg("Error creating migration version table")
return err
}
// Get the current migration version
var currentVersion MigrationVersion
db.First(&currentVersion)
// Define migrations
migrations := []struct {
Version uint
DBTypes []databaseType // nil = all types
Func func() error
}{
{1, v1_modifyConstraintToSSHKeys},
{2, v2_lowercaseEmails},
// Add more migrations here as needed
{1, []databaseType{SQLite}, v1_modifyConstraintToSSHKeys},
{2, []databaseType{SQLite}, v2_lowercaseEmails},
{3, nil, v3_normalizedColumns},
}
// Apply migrations
for _, m := range migrations {
if m.Version > currentVersion.Version {
tx := db.Begin()
if err := tx.Error; err != nil {
log.Fatal().Err(err).Msg("Error starting transaction")
return err
}
if m.Version <= currentVersion.Version {
continue
}
if err := m.Func(); err != nil {
log.Fatal().Err(err).Msg(fmt.Sprintf("Error applying migration %d:", m.Version))
tx.Rollback()
return err
} else {
if err = tx.Commit().Error; err != nil {
log.Fatal().Err(err).Msg(fmt.Sprintf("Error committing migration %d:", m.Version))
return err
// Skip migrations not intended for this DB type
if len(m.DBTypes) > 0 {
applicable := false
for _, t := range m.DBTypes {
if t == dbType {
applicable = true
break
}
}
if !applicable {
// Advance version so we don't retry on next startup
currentVersion.Version = m.Version
db.Save(&currentVersion)
log.Info().Msg(fmt.Sprintf("Migration %d applied successfully", m.Version))
continue
}
}
tx := db.Begin()
if err := tx.Error; err != nil {
log.Fatal().Err(err).Msg("Error starting transaction")
return err
}
if err := m.Func(); err != nil {
tx.Rollback()
log.Fatal().Err(err).Msg(fmt.Sprintf("Error applying migration %d:", m.Version))
return err
}
if err := tx.Commit().Error; err != nil {
log.Fatal().Err(err).Msg(fmt.Sprintf("Error committing migration %d:", m.Version))
return err
}
currentVersion.Version = m.Version
db.Save(&currentVersion)
log.Info().Msg(fmt.Sprintf("Migration %d applied successfully", m.Version))
}
return nil
@@ -112,3 +128,12 @@ func v2_lowercaseEmails() error {
copySQL := `UPDATE users SET email = lower(email);`
return db.Exec(copySQL).Error
}
func v3_normalizedColumns() error {
if err := db.Model(&User{}).Where("username_normalized = '' OR username_normalized IS NULL").
Updates(map[string]interface{}{"username_normalized": gorm.Expr("LOWER(username)")}).Error; err != nil {
return err
}
return db.Model(&Gist{}).Where("url_normalized = '' OR url_normalized IS NULL").
Updates(map[string]interface{}{"url_normalized": gorm.Expr("LOWER(url)")}).Error
}
+28 -15
View File
@@ -2,24 +2,27 @@ package db
import (
"encoding/json"
"strings"
"github.com/thomiceli/opengist/internal/git"
"gorm.io/gorm"
)
type User struct {
ID uint `gorm:"primaryKey"`
Username string `gorm:"uniqueIndex,size:191"`
Password string
IsAdmin bool
CreatedAt int64
Email string
MD5Hash string // for gravatar, if no Email is specified, the value is random
AvatarURL string
GithubID string
GitlabID string
GiteaID string
OIDCID string `gorm:"column:oidc_id"`
StylePreferences string
ID uint `gorm:"primaryKey"`
Username string `gorm:"uniqueIndex,size:191"`
UsernameNormalized string `gorm:"index"`
Password string
IsAdmin bool
CreatedAt int64
Email string
MD5Hash string // for gravatar, if no Email is specified, the value is random
AvatarURL string
GithubID string
GitlabID string
GiteaID string
OIDCID string `gorm:"column:oidc_id"`
StylePreferences string
Gists []Gist `gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE;foreignKey:UserID"`
SSHKeys []SSHKey `gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE;foreignKey:UserID"`
@@ -28,6 +31,11 @@ type User struct {
AccessTokens []AccessToken `gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE;foreignKey:UserID"`
}
func (user *User) BeforeSave(_ *gorm.DB) error {
user.UsernameNormalized = strings.ToLower(user.Username)
return nil
}
func (user *User) BeforeDelete(tx *gorm.DB) error {
// Decrement likes counter using derived table
err := tx.Exec(`
@@ -93,7 +101,7 @@ func (user *User) BeforeDelete(tx *gorm.DB) error {
func UserExists(username string) (bool, error) {
var count int64
err := db.Model(&User{}).Where("username like ?", username).Count(&count).Error
err := db.Model(&User{}).Where("username_normalized = ?", strings.ToLower(username)).Count(&count).Error
return count > 0, err
}
@@ -111,7 +119,7 @@ func GetAllUsers(offset int) ([]*User, error) {
func GetUserByUsername(username string) (*User, error) {
user := new(User)
err := db.
Where("username like ?", username).
Where("username_normalized = ?", strings.ToLower(username)).
First(&user).Error
return user, err
}
@@ -258,6 +266,11 @@ type UserDTO struct {
Password string `form:"password" validate:"required"`
}
type OAuthRegisterDTO struct {
Username string `form:"username" validate:"required,max=24,alphanumdash,notreserved"`
Email string `form:"email" validate:"omitempty,email"`
}
func (dto *UserDTO) ToUser() *User {
return &User{
Username: dto.Username,
+32
View File
@@ -0,0 +1,32 @@
package db
import (
"time"
"github.com/thomiceli/opengist/internal/web/handlers/api/v1/types"
)
// ToSimpleAPI returns the public simple-user shape used inside gist responses
// and by the public user-lookup endpoints. It carries no private fields (no
// email). Fields whose underlying feature doesn't exist in Opengist
// (followers, repos, ...) are still populated with the spec-shaped URLs so
// clients can parse cleanly.
func (u *User) ToSimpleAPI() types.SimpleUser {
return types.SimpleUser{
ID: u.ID,
Login: u.Username,
Username: u.Username,
AvatarURL: u.AvatarURL,
Type: "User",
CreatedAt: time.Unix(u.CreatedAt, 0).UTC(),
}
}
// ToPrivateAPI returns the self shape for the authenticated-user endpoints
// (GET/PATCH /user): the public fields plus the caller's own email.
func (u *User) ToPrivateAPI() types.PrivateUser {
return types.PrivateUser{
SimpleUser: u.ToSimpleAPI(),
Email: u.Email,
}
}
+54 -21
View File
@@ -132,21 +132,36 @@ type catFileBatch struct {
Truncated bool
}
func CatFileBatch(user string, gist string, revision string, truncate bool) ([]*catFileBatch, error) {
func CatFileBatch(user string, gist string, revision string, truncate bool) ([]*catFileBatch, bool, error) {
repositoryPath := RepositoryPath(user, gist)
maxFiles := 50
lsTreeCmd := exec.Command("git", "ls-tree", "-l", revision)
lsTreeCmd.Dir = repositoryPath
lsTreeOutput, err := lsTreeCmd.Output()
var lsTreeStderr bytes.Buffer
lsTreeCmd.Stderr = &lsTreeStderr
lsTreeStdout, err := lsTreeCmd.StdoutPipe()
if err != nil {
return nil, err
return nil, false, err
}
if err = lsTreeCmd.Start(); err != nil {
return nil, false, err
}
fileMap := make([]*catFileBatch, 0)
gistTruncated := false
lines := strings.Split(string(lsTreeOutput), "\n")
for _, line := range lines {
fields := strings.Fields(line)
scanner := bufio.NewScanner(lsTreeStdout)
scanner.Buffer(make([]byte, 64*1024), 1024*1024)
for scanner.Scan() {
if truncate && len(fileMap) >= maxFiles {
gistTruncated = true
break
}
fields := strings.Fields(scanner.Text())
if len(fields) < 4 {
continue // Skip lines that don't have enough fields
}
@@ -164,19 +179,33 @@ func CatFileBatch(user string, gist string, revision string, truncate bool) ([]*
Name: convertOctalToUTF8(name),
})
}
scanErr := scanner.Err()
// Closing the read end before git is done writing causes git's next write
// to fail (SIGPIPE on Unix, broken-pipe error on Windows). That shows up as
// a non-zero exit from Wait, but it's expected when we stop early — so we
// only treat the Wait error as real if git actually printed something to stderr.
_ = lsTreeStdout.Close()
waitErr := lsTreeCmd.Wait()
if scanErr != nil {
return nil, false, scanErr
}
if waitErr != nil && lsTreeStderr.Len() > 0 {
return nil, false, waitErr
}
catFileCmd := exec.Command("git", "cat-file", "--batch")
catFileCmd.Dir = repositoryPath
stdin, err := catFileCmd.StdinPipe()
if err != nil {
return nil, err
return nil, false, err
}
stdout, err := catFileCmd.StdoutPipe()
if err != nil {
return nil, err
return nil, false, err
}
if err = catFileCmd.Start(); err != nil {
return nil, err
return nil, false, err
}
reader := bufio.NewReader(stdout)
@@ -184,12 +213,12 @@ func CatFileBatch(user string, gist string, revision string, truncate bool) ([]*
for _, file := range fileMap {
_, err = stdin.Write([]byte(file.Hash + "\n"))
if err != nil {
return nil, err
return nil, false, err
}
header, err := reader.ReadString('\n')
if err != nil {
return nil, err
return nil, false, err
}
parts := strings.Fields(header)
@@ -199,7 +228,7 @@ func CatFileBatch(user string, gist string, revision string, truncate bool) ([]*
size, err := strconv.ParseUint(parts[2], 10, 64)
if err != nil {
return nil, err
return nil, false, err
}
// Don't truncate Jupyter notebooks
@@ -215,7 +244,7 @@ func CatFileBatch(user string, gist string, revision string, truncate bool) ([]*
// Read exactly size bytes from header, or the max allowed if truncated
content := make([]byte, sizeToRead)
if _, err = io.ReadFull(reader, content); err != nil {
return nil, err
return nil, false, err
}
file.Content = string(content)
@@ -223,26 +252,26 @@ func CatFileBatch(user string, gist string, revision string, truncate bool) ([]*
if truncate && size > truncateLimit {
// skip other bytes if truncated
if _, err = reader.Discard(int(size - truncateLimit)); err != nil {
return nil, err
return nil, false, err
}
file.Truncated = true
}
// Read the blank line following the content
if _, err := reader.ReadByte(); err != nil {
return nil, err
return nil, false, err
}
}
if err = stdin.Close(); err != nil {
return nil, err
return nil, false, err
}
if err = catFileCmd.Wait(); err != nil {
return nil, err
return nil, false, err
}
return fileMap, nil
return fileMap, gistTruncated, nil
}
func GetFileContent(user string, gist string, revision string, filename string, truncate bool) (string, bool, error) {
@@ -298,7 +327,11 @@ func GetFileSize(user string, gist string, revision string, filename string) (ui
return strconv.ParseUint(strings.TrimSuffix(string(stdout), "\n"), 10, 64)
}
func GetLog(user string, gist string, skip int) ([]*Commit, error) {
// GetLog returns commits walked from `revision` (typically "HEAD" or a
// specific SHA), skipping `skip` rows from the top of the walk and limited
// to `limit` rows. Pass "HEAD" for the gist's full history; pass a SHA to
// see the history ending at (and including) that commit.
func GetLog(user string, gist string, revision string, skip int, limit int) ([]*Commit, error) {
repositoryPath := RepositoryPath(user, gist)
cmd := exec.Command(
@@ -306,14 +339,14 @@ func GetLog(user string, gist string, skip int) ([]*Commit, error) {
"--no-pager",
"log",
"-n",
"11",
strconv.Itoa(limit),
"--no-color",
"-p",
"--skip",
strconv.Itoa(skip),
"--format=format:c %H%na %aN%nm %ae%nt %at",
"--shortstat",
"HEAD",
revision,
)
cmd.Dir = repositoryPath
stdout, _ := cmd.StdoutPipe()
+5 -3
View File
@@ -100,14 +100,16 @@ like Opengist actually`,
require.False(t, truncated, "Content should not be truncated")
require.Equal(t, "I really\nlike Opengist actually", content, "Content is not correct")
commits, err := GetLog("thomas", "gist1", 0)
commits, err := GetLog("thomas", "gist1", "HEAD", 0, 11)
require.NoError(t, err, "Could not get log")
require.Equal(t, 2, len(commits), "Commits count are not correct")
require.Regexp(t, "[a-f0-9]{40}", commits[0].Hash, "Commit ID is not correct")
require.Regexp(t, "[0-9]{10}", commits[0].Timestamp, "Commit timestamp is not correct")
require.Equal(t, "thomas", commits[0].AuthorName, "Commit author name is not correct")
require.Equal(t, "thomas@mail.com", commits[0].AuthorEmail, "Commit author email is not correct")
require.Equal(t, "4 files changed, 2 insertions, 2 deletions", commits[0].Changed, "Commit author name is not correct")
require.Equal(t, 4, commits[0].FilesChanged, "FilesChanged is not correct")
require.Equal(t, 2, commits[0].Additions, "Additions is not correct")
require.Equal(t, 2, commits[0].Deletions, "Deletions is not correct")
require.Contains(t, commits[0].Files, File{
Filename: "my_renamed_file.txt",
@@ -157,7 +159,7 @@ like Opengist actually`,
IsDeleted: false,
}, "File new_file.txt is not correct")
commitsSkip1, err := GetLog("thomas", "gist1", 1)
commitsSkip1, err := GetLog("thomas", "gist1", "HEAD", 1, 11)
require.NoError(t, err, "Could not get log")
require.Equal(t, commitsSkip1[0], commits[1], "Commits skips are not correct")
}
+19
View File
@@ -0,0 +1,19 @@
package git
import (
"path/filepath"
"strings"
)
func CleanTreePathName(s string) string {
name := filepath.Base(s)
if name == "." || name == ".." {
return ""
}
name = strings.ReplaceAll(name, "/", "")
name = strings.ReplaceAll(name, "\\", "")
return name
}
+14 -2
View File
@@ -2,6 +2,7 @@ package git
import (
"fmt"
"mime"
"net/http"
"strings"
@@ -10,6 +11,7 @@ import (
type MimeType struct {
ContentType string
Charset string
extension string
golangContentType string // json, m3u, etc. still renderable as text
}
@@ -88,6 +90,16 @@ func (mt MimeType) RenderType() string {
return "Binary"
}
func DetectMimeType(data []byte, extension string) MimeType {
return MimeType{mimetype.Detect(data).String(), extension, http.DetectContentType(data)}
// Header returns the value for a Content-Type HTTP header, re-attaching the
// charset parameter that DetectMimeType split out of ContentType.
func (mt MimeType) Header() string {
if mt.Charset == "" {
return mt.ContentType
}
return mime.FormatMediaType(mt.ContentType, map[string]string{"charset": mt.Charset})
}
func DetectMimeType(data []byte, extension string) MimeType {
mediaType, params, _ := mime.ParseMediaType(mimetype.Detect(data).String())
return MimeType{mediaType, params["charset"], extension, http.DetectContentType(data)}
}
+31 -10
View File
@@ -6,6 +6,7 @@ import (
"fmt"
"io"
"regexp"
"strconv"
"strings"
)
@@ -23,12 +24,14 @@ type File struct {
}
type Commit struct {
Hash string
AuthorName string
AuthorEmail string
Timestamp string
Changed string
Files []File
Hash string
AuthorName string
AuthorEmail string
Timestamp string
FilesChanged int
Additions int
Deletions int
Files []File
}
func truncateCommandOutput(out io.Reader, maxBytes int64) (string, bool, error) {
@@ -60,6 +63,18 @@ func truncateCommandOutput(out io.Reader, maxBytes int64) (string, bool, error)
var reLogBinaryNames = regexp.MustCompile(`Binary files (.+) and (.+) differ`)
// shortstat patterns. Git emits a line like:
//
// " 4 files changed, 2 insertions(+), 2 deletions(-)"
//
// with insertions or deletions optionally absent when zero. The capture
// group in each regex is the count.
var (
reShortstatFiles = regexp.MustCompile(`(\d+) files? changed`)
reShortstatInsertions = regexp.MustCompile(`(\d+) insertions?`)
reShortstatDeletions = regexp.MustCompile(`(\d+) deletions?`)
)
// inspired from https://github.com/go-gitea/gitea/blob/main/services/gitdiff/gitdiff.go
func parseLog(out io.Reader, maxFiles int, maxBytes int) ([]*Commit, error) {
var commits []*Commit
@@ -120,10 +135,16 @@ loopLog:
// Commit shortstat
case ' ':
changed := []byte(line)[1:]
changed = bytes.ReplaceAll(changed, []byte("(+)"), []byte(""))
changed = bytes.ReplaceAll(changed, []byte("(-)"), []byte(""))
currentCommit.Changed = string(changed)
shortstat := line[1:]
if m := reShortstatFiles.FindStringSubmatch(shortstat); len(m) == 2 {
currentCommit.FilesChanged, _ = strconv.Atoi(m[1])
}
if m := reShortstatInsertions.FindStringSubmatch(shortstat); len(m) == 2 {
currentCommit.Additions, _ = strconv.Atoi(m[1])
}
if m := reShortstatDeletions.FindStringSubmatch(shortstat); len(m) == 2 {
currentCommit.Deletions, _ = strconv.Atoi(m[1])
}
// shortstat is followed by an empty line
line, err = input.ReadString('\n')
+42 -10
View File
@@ -3,14 +3,16 @@ package hooks
import (
"bufio"
"fmt"
"github.com/thomiceli/opengist/internal/db"
"github.com/thomiceli/opengist/internal/git"
validatorpkg "github.com/thomiceli/opengist/internal/validator"
"io"
"os"
"os/exec"
"slices"
"strings"
"time"
"github.com/thomiceli/opengist/internal/db"
"github.com/thomiceli/opengist/internal/git"
validatorpkg "github.com/thomiceli/opengist/internal/validator"
)
func PostReceive(in io.Reader, out, er io.Writer) error {
@@ -47,7 +49,7 @@ func PostReceive(in io.Reader, out, er io.Writer) error {
if slices.Contains([]string{"public", "unlisted", "private"}, opts["visibility"]) {
gist.Private = db.ParseVisibility(opts["visibility"])
outputSb.WriteString(fmt.Sprintf("Gist visibility set to %s\n\n", opts["visibility"]))
fmt.Fprintf(&outputSb, "Gist visibility set to %s\n\n", opts["visibility"])
}
if opts["url"] != "" && validator.Var(opts["url"], "max=32,alphanumdashorempty") == nil {
@@ -55,19 +57,49 @@ func PostReceive(in io.Reader, out, er io.Writer) error {
lastIndex := strings.LastIndex(gistUrl, "/")
gistUrl = gistUrl[:lastIndex+1] + gist.URL
if !newGist {
outputSb.WriteString(fmt.Sprintf("Gist URL set to %s. Set the Git remote URL via:\n", gistUrl))
outputSb.WriteString(fmt.Sprintf("git remote set-url origin %s\n\n", gistUrl))
fmt.Fprintf(&outputSb, "Gist URL set to %s. Set the Git remote URL via:\n", gistUrl)
fmt.Fprintf(&outputSb, "git remote set-url origin %s\n\n", gistUrl)
}
}
if opts["title"] != "" && validator.Var(opts["title"], "max=250") == nil {
gist.Title = opts["title"]
outputSb.WriteString(fmt.Sprintf("Gist title set to \"%s\"\n\n", opts["title"]))
fmt.Fprintf(&outputSb, "Gist title set to \"%s\"\n\n", opts["title"])
}
if opts["description"] != "" && validator.Var(opts["description"], "max=1000") == nil {
gist.Description = opts["description"]
outputSb.WriteString(fmt.Sprintf("Gist description set to \"%s\"\n\n", opts["description"]))
fmt.Fprintf(&outputSb, "Gist description set to \"%s\"\n\n", opts["description"])
}
if opts["topics"] != "" && validator.Var(opts["topics"], "gisttopics") == nil {
topicNames := strings.Fields(opts["topics"])
if len(topicNames) > 0 {
gist.Topics = make([]db.GistTopic, 0, len(topicNames))
for _, name := range topicNames {
gist.Topics = append(gist.Topics, db.GistTopic{Topic: name})
}
fmt.Fprintf(&outputSb, "Gist topics set to \"%s\"\n\n", opts["topics"])
}
}
if newGist && opts["expire"] != "" {
value := opts["expire"]
expire := db.ExpirationType(value)
switch {
case expire == db.ExpiryNever:
// no expiration
case expire.Duration() > 0:
gist.ExpiresAt = expire.ExpiresAtTimestamp()
fmt.Fprintf(&outputSb, "Gist expiration set to \"%s\"\n\n", value)
default:
if t, err := validatorpkg.ParseDateTime(value); err == nil && t.After(time.Now()) {
gist.ExpiresAt = t.Unix()
fmt.Fprintf(&outputSb, "Gist expiration set to \"%s\"\n\n", value)
} else {
fmt.Fprintf(&outputSb, "Invalid gist expiration \"%s\", ignored\n\n", value)
}
}
}
if hasNoCommits, err := git.HasNoCommits(gist.User.Username, gist.Uuid); err != nil {
@@ -90,9 +122,9 @@ func PostReceive(in io.Reader, out, er io.Writer) error {
gist.AddInIndex()
if newGist {
outputSb.WriteString(fmt.Sprintf("Your new gist has been created here: %s\n", gistUrl))
fmt.Fprintf(&outputSb, "Your new gist has been created here: %s\n", gistUrl)
outputSb.WriteString("If you want to keep working with your gist, you could set the Git remote URL via:\n")
outputSb.WriteString(fmt.Sprintf("git remote set-url origin %s\n\n", gistUrl))
fmt.Fprintf(&outputSb, "git remote set-url origin %s\n\n", gistUrl)
}
outputStr := outputSb.String()
+6
View File
@@ -56,6 +56,12 @@ func (store *LocaleStore) loadLocaleFromYAML(localeCode, path string) error {
case language.EuropeanSpanish:
name = "Español"
}
switch localeCode {
case "zh-CN":
name = "简体中文"
case "zh-TW":
name = "繁體中文"
}
locale := &Locale{
Code: localeCode,
+379
View File
@@ -0,0 +1,379 @@
gist.public: عام
gist.unlisted: غير مدرج
gist.private: خاص
gist.header.like: اعجبني
gist.header.unlike: لم يعجبني
gist.header.fork: اشتقاق
gist.header.edit: تعديل
gist.header.delete: حذف
gist.header.forked-from: مشتق من
gist.header.last-active: آخر نشاط
gist.header.select-tab: اختر علامة التبويب
gist.header.code: كود
gist.header.revisions: التعديلات
gist.header.revision: تعديل
gist.header.clone-http: نسخ عن طريق %s
gist.header.clone-http-help: نسخ باستخدام المصادقة HTTP الأساسية.
gist.header.clone-ssh: نسخ باستخدام مفتاح SSH
gist.header.clone-ssh-help: نسخ باستخدام مفتاح SSH.
gist.header.embed: تضمين
gist.header.embed-help: تضمين هذا المقطع في موقعك على الويب.
gist.header.download-zip: تنزيل ZIP
gist.raw: خام
gist.file-truncated: تم تقطيع هذا الملف.
gist.file-raw: لا يمكن عرض هذا الملف.
gist.file-binary-edit: هذا الملف ثنائي.
gist.watch-full-file: عرض الملف الكامل.
gist.file-not-valid: هذا الملف ليس ملف CSV صالح.
gist.no-content: لا توجد ملفات.
gist.preview-non-available: المعاينة غير متاحة
gist.new.new_gist: مقطع جديد
gist.new.title: العنوان
gist.new.description: الوصف
gist.new.url: الرابط
gist.new.filename-with-extension: اسم الملف مع الامتداد
gist.new.indent-mode: نمط الإزاحة
gist.new.indent-mode-space: مسافات
gist.new.indent-mode-tab: علامة تبويب
gist.new.indent-size: حجم الإزاحة
gist.new.wrap-mode: نمط التفاف السطر
gist.new.wrap-mode-no: بدون التفاف
gist.new.wrap-mode-soft: التفاف ناعم
gist.new.add-file: إضافة ملف
gist.new.create-public-button: إنشاء مقطع عام
gist.new.create-unlisted-button: إنشاء مقطع غير مدرج
gist.new.create-private-button: إنشاء مقطع خاص
gist.new.preview: معاينة
gist.new.create-a-new-gist: إنشاء مقطع جديد
gist.new.topics: المواضيع (افصل بينها بمسافات)
gist.new.drop-files: أفلت الملفات هنا أو انقر للتحميل
gist.new.any-file-type: ارفع أي نوع ملف
gist.edit.editing: تحرير
gist.edit.edit-gist: تعديل %s
gist.edit.change-visibility: جعل
gist.edit.delete: حذف
gist.edit.cancel: الغاء
gist.edit.save: حفظ
gist.delete.confirm: متأكد أنك تريد حذف هذا المقطع ؟
gist.list.joined: تم الانضمام
gist.list.all: كل المقاطع
gist.list.search-results: نتائج البحث
gist.list.sort: ترتيب
gist.list.sort-by-created: تم الانشاء
gist.list.sort-by-updated: تم التحديث
gist.list.order-by-asc: الأقدم أولاً
gist.list.order-by-desc: الأحدث أولاً
gist.list.select-tab: اختر علامة التبويب
gist.list.liked: معجب بها
gist.list.likes: إعجابات
gist.list.forked: مشتقة
gist.list.forked-from: مشتقة من
gist.list.forks: اشتقاقات
gist.list.files: ملفات
gist.list.last-active: آخر نشاط
gist.list.no-gists: لا مقاطع
gist.list.all-liked-by: كل المقاطع التي أعجب بها %s
gist.list.all-forked-by: كل المقاطع المشتقة بواسطة %s
gist.list.all-from: كل المقاطع من %s
gist.list.topic-results-topic: كل المقاطع المطابقة للموضوع %s
gist.list.topic-results: كل المقاطع المطابقة للموضوع
gist.search.found: تم العثور على مقاطع
gist.search.no-results: لم يتم العثور على مقاطع
gist.search.help.user: المقاطع التي أنشأها المستخدم
gist.search.help.title: المقاطع ذات العنوان المحدد
gist.search.help.description: المقاطع ذات الوصف المحدد
gist.search.help.filename: المقاطع التي تحتوي ملفات بالاسم المحدد
gist.search.help.extension: المقاطع التي تحتوي ملفات بالامتداد المحدد
gist.search.help.language: المقاطع التي تحتوي ملفات باللغة المحددة
gist.search.help.topic: المقاطع ذات الموضوع المحدد
gist.search.help.all: البحث في جميع الحقول
gist.search.placeholder.title: العنوان
gist.search.placeholder.visibility: مستوى الظهور
gist.search.placeholder.public: عام
gist.search.placeholder.unlisted: غير مدرج
gist.search.placeholder.private: خاص
gist.search.placeholder.language: اللغة
gist.search.placeholder.all: الكل
gist.search.placeholder.topics: المواضيع
gist.search.placeholder.search: البحث عن
gist.forks: اشتقاقات
gist.forks.view: عرض الاشتقاق
gist.forks.no: لا توجد اشتقاقات عامة
gist.forks.for: اشتقاقات %s
gist.likes: الإعجابات
gist.likes.no: لا توجد إعجابات بعد
gist.likes.for: إعجابات %s
gist.revisions: التعديلات
gist.revision.revised: عدّل هذا المقطع
gist.revision.go-to-revision: الانتقال إلى التعديل
gist.revision.file-created: تم إنشاء الملف
gist.revision.file-deleted: تم حذف الملف
gist.revision.file-renamed: تم تغيير الاسم إلى
gist.revision.diff-truncated: الفرق كبير جدًا ولا يمكن عرضه
gist.revision.file-renamed-no-changes: تم تغيير اسم الملف بدون تغييرات
gist.revision.empty-file: ملف فارغ
gist.revision.binary-file-changes: لا يتم عرض تغييرات الملفات الثنائية
gist.revision.no-changes: لا توجد تغييرات
gist.revision.no-revisions: لا توجد تعديلات للعرض
gist.revision-of: تعديل %s
settings: الإعدادات
settings.email: البريد الإلكتروني
settings.email-help: يُستخدم في عمليات commit وGravatar
settings.email-set: تعيين البريد الإلكتروني
settings.link-accounts: ربط الحسابات
settings.link-github-account: ربط حساب GitHub
settings.link-gitlab-account: ربط حساب GitLab
settings.link-gitea-account: ربط حساب Gitea
settings.unlink-github-account: إلغاء ربط حساب GitHub
settings.unlink-gitlab-account: إلغاء ربط حساب GitLab
settings.unlink-gitea-account: إلغاء ربط حساب Gitea
settings.delete-account: حذف الحساب
settings.delete-account-confirm: هل أنت متأكد أنك تريد حذف حسابك؟
settings.add-ssh-key: إضافة مفتاح SSH
settings.add-ssh-key-help: يُستخدم فقط لسحب/دفع المقاطع عبر Git باستخدام SSH
settings.add-ssh-key-title: العنوان
settings.add-ssh-key-content: المفتاح
settings.delete-ssh-key: حذف
settings.delete-ssh-key-confirm: تأكيد حذف مفتاح SSH
settings.ssh-key-added-at: أُضيف
settings.ssh-key-never-used: لم يُستخدم أبدًا
settings.ssh-key-last-used: آخر استخدام
settings.ssh-key-exists: مفتاح SSH موجود بالفعل
settings.change-username: تغيير اسم المستخدم
settings.create-password: إنشاء كلمة مرور
settings.create-password-help: أنشئ كلمة مرورك لتسجيل الدخول إلى Opengist عبر HTTP
settings.change-password: تغيير كلمة المرور
settings.change-password-help: غيّر كلمة مرورك لتسجيل الدخول إلى Opengist عبر HTTP
settings.password-label-title: كلمة المرور
settings.header.account: الحساب
settings.header.mfa: المصادقة متعددة العوامل
settings.header.ssh: SSH
settings.header.tokens: رموز الوصول
settings.header.style: المظهر
settings.style.gist-code: كود المقطع
settings.style.no-soft-wrap: بدون التفاف ناعم
settings.style.soft-wrap: التفاف ناعم
settings.style.removed-lines-color: لون الأسطر المحذوفة
settings.style.added-lines-color: لون الأسطر المضافة
settings.style.git-lines-color: لون أسطر Git
settings.style.save-style: حفظ المظهر
settings.style.theme: السمة
settings.style.theme-light: فاتح
settings.style.theme-dark: داكن
settings.style.theme-auto: تلقائي
settings.create-token: إنشاء رمز وصول
settings.create-token-help: يمكن استخدام رموز الوصول للوصول إلى API
settings.token-name: الاسم
settings.token-permissions: الأذونات
settings.token-gist-permission: المقاطع
settings.token-permission-none: بدون وصول
settings.token-permission-read: قراءة
settings.token-permission-read-write: قراءة وكتابة
settings.delete-token: حذف
settings.delete-token-confirm: تأكيد حذف رمز الوصول
settings.token-created-at: أُنشئ
settings.token-never-used: لم يُستخدم أبدًا
settings.token-last-used: آخر استخدام
settings.token-expiration: انتهاء الصلاحية
settings.token-expiration-help: اتركه فارغًا لعدم تحديد انتهاء صلاحية
settings.token-expires-at: ينتهي في
settings.token-no-expiration: بدون انتهاء صلاحية
settings.token-expired: منتهي الصلاحية
settings.token-created: تم إنشاء الرمز، تأكد من نسخه الآن، لن تتمكن من رؤيته مرة أخرى!
settings.token-deleted: تم حذف رمز الوصول
auth.signup-disabled: قام المسؤول بتعطيل إنشاء الحسابات
auth.login: تسجيل الدخول
auth.signup: انشاء
auth.new-account: حساب جديد
auth.username: اسم المستخدم
auth.password: كلمة المرور
auth.register-instead: انشاء حساب
auth.login-instead: سجل الدخول
auth.oauth: المتابعة باستخدام حساب %s
auth.oauth.no-provider: لم يتم العثور على موفر OAuth
auth.oauth.complete-registration: أكمل تسجيلك
auth.oauth.complete-registration-button: إنشاء حساب
auth.oauth.signing-in-with: تسجيل الدخول باستخدام %s
auth.oauth.cancel: الغاء
auth.oauth.existing-account: لديك حساب موجود؟
auth.oauth.already-have-account: إذا كان لديك حساب Opengist بالفعل، فسجّل الدخول أولًا ثم اربط حساب %s من الإعدادات.
auth.mfa: المصادقة متعددة العوامل
auth.mfa.passkey: مفتاح مرور
auth.mfa.passkeys: مفاتيح مرور
auth.mfa.use-passkey: استخدم مفتاح مرور
auth.mfa.bind-passkey: اربط مفتاح مرور
auth.mfa.login-with-passkey: سجل الدخول بمفتاح مرور
auth.mfa.waiting-for-passkey-input: بانتظار الإدخال من تفاعل المتصفح...
auth.mfa.use-passkey-to-finish: استخدم مفتاح مرور لانهاء المصادقة
auth.mfa.passkeys-help: أضف مفتاح مرور لتسجيل الدخول إلى حسابك واستخدامه كطريقة مصادقة متعددة العوامل.
auth.mfa.passkey-name: الاسم
auth.mfa.delete-passkey: حذف
auth.mfa.passkey-added-at: أُضيف
auth.mfa.passkey-never-used: لم يُستخدم أبدًا
auth.mfa.passkey-last-used: آخر استخدام
auth.mfa.delete-passkey-confirm: تأكيد حذف مفتاح المرور
auth.totp: كلمة مرور لمرة واحدة قائمة على الوقت (TOTP)
auth.totp.help: TOTP هي طريقة مصادقة ثنائية تستخدم سرًا مشتركًا لإنشاء كلمة مرور لمرة واحدة.
auth.totp.use: استخدام TOTP
auth.totp.regenerate-recovery-codes: إعادة إنشاء رموز الاسترداد
auth.totp.already-enabled: TOTP مفعّل بالفعل
auth.totp.invalid-secret: سر TOTP غير صالح
auth.totp.invalid-code: رمز TOTP غير صالح
auth.totp.code-used: تم استخدام رمز الاسترداد %s وهو الآن غير صالح. قد ترغب في تعطيل المصادقة متعددة العوامل حاليًا أو إعادة إنشاء رموزك.
auth.totp.disabled: تم تعطيل TOTP بنجاح
auth.totp.disable: تعطيل TOTP
auth.totp.enter-code: أدخل الرمز من تطبيق المصادقة
auth.totp.enter-recovery-key: أو مفتاح استرداد إذا فقدت جهازك
auth.totp.code: كود
auth.totp.submit: إرسال
auth.totp.proceed: متابعة
auth.totp.save-recovery-codes: احفظ رموز الاسترداد في مكان آمن. يمكنك استخدام هذه الرموز لاستعادة الوصول إلى حسابك إذا فقدت الوصول إلى تطبيق المصادقة.
auth.totp.scan-qr-code: امسح رمز QR أدناه باستخدام تطبيق المصادقة لتفعيل المصادقة الثنائية، أو أدخل السلسلة التالية ثم أكّد بالرمز الذي تم إنشاؤه.
error: خطأ
error.page-not-found: الصفحة غير موجودة
error.bad-request: طلب غير صالح
error.signup-disabled: إنشاء الحسابات معطّل
error.signup-disabled-form: إنشاء الحساب عبر نموذج التسجيل معطّل
error.login-disabled-form: تسجيل الدخول عبر نموذج تسجيل الدخول معطّل
error.complete-oauth-login: "تعذر إكمال مصادقة المستخدم: %s"
error.oauth-unsupported: موفر OAuth2 غير مدعوم
error.cannot-bind-data: تعذر ربط البيانات
error.invalid-number: رقم غير صالح
error.invalid-character-unescaped: محرف غير صالح غير مهلّب
error.not-in-mfa-session: المستخدم ليس ضمن جلسة مصادقة متعددة العوامل
error.no-file-uploaded: لم يتم رفع أي ملف
error.cannot-open-file: تعذر فتح الملف المرفوع
header.menu.all: الكل
header.menu.new: جديد
header.menu.search: البحث
header.menu.my-gists: مقاطعي
header.menu.liked: المفضلة
header.menu.admin: الإدارة
header.menu.settings: الإعدادات
header.menu.logout: تسجيل الخروج
header.menu.register: إنشاء حساب
header.menu.login: تسجيل الدخول
header.menu.light: فاتح
header.menu.dark: داكن
header.menu.system: النظام
footer.powered-by: مدعوم بواسطة %s
pagination.older: أقدم
pagination.newer: أحدث
pagination.previous: سابق
pagination.next: لاحق
admin.admin_panel: لوحة الإدارة
admin.general: عام
admin.users: المستخدمون
admin.gists: مقاطع
admin.configuration: الإعدادات
admin.invitations: الدعوات
admin.invitations.create: إنشاء دعوة
admin.versions: الإصدارات
admin.ssh_keys: مفاتيح SSH
admin.stats: الإحصائيات
admin.actions: الإجراءات
admin.actions.sync-fs: مزامنة المقاطع من نظام الملفات
admin.actions.sync-db: مزامنة المقاطع من قاعدة البيانات
admin.actions.git-gc: تنفيذ جمع القمامة لجميع مستودعات Git
admin.actions.sync-previews: مزامنة معاينات جميع المقاطع
admin.actions.reset-hooks: إعادة تعيين خطافات خادم Git لجميع المستودعات
admin.actions.index-gists: إعادة بناء فهرس البحث
admin.actions.sync-gist-languages: مزامنة لغات جميع المقاطع
admin.id: ID
admin.user: المستخدم
admin.delete: حذف
admin.created_at: تاريخ الإنشاء
admin.config-link: يمكن %s هذا الإعداد عبر ملف إعدادات YAML و/أو متغيرات البيئة.
admin.config-link-overriden: تجاوزه
admin.disable-signup: تعطيل إنشاء الحساب
admin.disable-signup_help: منع إنشاء حسابات جديدة.
admin.require-login: طلب تسجيل الدخول
admin.require-login_help: فرض تسجيل الدخول على المستخدمين لعرض المقاطع.
admin.allow-gists-without-login: السماح بالمقاطع الفردية دون تسجيل دخول
admin.allow-gists-without-login_help: السماح بعرض وتنزيل المقاطع الفردية دون تسجيل دخول، مع طلب تسجيل الدخول لاستكشاف المقاطع.
admin.disable-login: تعطيل نموذج تسجيل الدخول
admin.disable-login_help: منع تسجيل الدخول عبر النموذج لإجبار استخدام مزوّدي OAuth بدلًا من ذلك.
admin.disable-gravatar: تعطيل Gravatar
admin.disable-gravatar_help: تعطيل استخدام Gravatar كمزوّد للصور الرمزية.
admin.users.delete_confirm: هل تريد حذف هذا المستخدم؟
admin.gists.title: العنوان
admin.gists.private: خاص؟
admin.gists.nb-files: عدد الملفات
admin.gists.nb-likes: عدد الإعجابات
admin.gists.delete_confirm: هل تريد حذف هذا المقطع؟
admin.invitations.help: يمكن استخدام الدعوات لإنشاء حساب حتى لو كان إنشاء الحسابات معطّلًا.
admin.invitations.max_uses: أقصى عدد استخدامات
admin.invitations.expires_at: تنتهي في
admin.invitations.code: كود
admin.invitations.copy_link: نسخ الرابط
admin.invitations.uses: الاستخدامات
admin.invitations.expired: منتهية
admin.invitations.delete_confirm: هل تريد حذف هذه الدعوة؟
flash.admin.user-deleted: تم حذف المستخدم
flash.admin.gist-deleted: تم حذف المقطع
flash.admin.invitation-created: تم إنشاء الدعوة
flash.admin.invitation-deleted: تم حذف الدعوة
flash.admin.sync-fs: تتم مزامنة المستودعات من نظام الملفات...
flash.admin.sync-db: تتم مزامنة المستودعات من قاعدة البيانات...
flash.admin.git-gc: جارٍ تنفيذ جمع القمامة للمستودعات...
flash.admin.sync-previews: تتم مزامنة معاينات المقاطع...
flash.admin.reset-hooks: جارٍ إعادة تعيين خطافات خادم Git لجميع المستودعات...
flash.admin.index-gists: جارٍ إعادة بناء فهرس البحث...
flash.admin.sync-gist-languages: تتم مزامنة لغات المقاطع...
flash.auth.username-exists: اسم المستخدم موجود بالفعل
flash.auth.invalid-credentials: بيانات الاعتماد غير صالحة
flash.auth.account-linked-oauth: تم ربط الحساب بـ %s
flash.auth.account-unlinked-oauth: تم إلغاء ربط الحساب من %s
flash.auth.user-sshkeys-not-retrievable: تعذر جلب مفاتيح المستخدم
flash.auth.user-sshkeys-not-created: تعذر إنشاء مفتاح SSH
flash.auth.must-be-logged-in: يجب تسجيل الدخول للوصول إلى المقاطع
flash.auth.passkey-registred: تم تسجيل مفتاح المرور %s
flash.auth.passkey-deleted: تم حذف مفتاح المرور
flash.auth.oauth-session-expired: انتهت جلسة OAuth2، يرجى المحاولة مرة أخرى
flash.auth.oauth-already-linked: حساب %s هذا مرتبط بالفعل بمستخدم آخر
flash.gist.visibility-changed: تم تغيير مستوى ظهور المقطع
flash.gist.deleted: تم حذف المقطع
flash.gist.fork-own-gist: لا يمكن اشتقاق مقاطعك الخاصة
flash.gist.forked: تم اشتقاق المقطع
flash.user.email-updated: تم تحديث البريد الإلكتروني
flash.user.invalid-ssh-key: مفتاح SSH غير صالح
flash.user.ssh-key-added: تمت إضافة مفتاح SSH
flash.user.ssh-key-deleted: تم حذف مفتاح SSH
flash.user.password-updated: تم تحديث كلمة المرور
flash.user.username-updated: تم تحديث اسم المستخدم
validation.is-too-long: الحقل %s طويل جدًا
validation.should-not-be-empty: يجب ألا يكون الحقل %s فارغًا
validation.should-not-include-sub-directory: يجب ألا يتضمن الحقل %s مجلدًا فرعيًا
validation.should-only-contain-alphanumeric-characters: يجب أن يحتوي الحقل %s على أحرف وأرقام فقط
validation.should-only-contain-alphanumeric-characters-and-dashes: يجب أن يحتوي الحقل %s على أحرف وأرقام وشرطات فقط
validation.not-enough: '%s غير كافٍ'
validation.invalid: '%s غير صالح'
validation.invalid-gist-topics: مواضيع المقطع غير صالحة، يجب أن تبدأ بحرف أو رقم، وألا تتجاوز 50 حرفًا، ويمكن أن تتضمن شرطات
html.title.admin-panel: لوحة الإدارة
+2 -2
View File
@@ -192,7 +192,7 @@ admin.actions.sync-db: 'Gists von der Datenbank synchronisieren'
admin.actions.git-gc: '„garbage collection“ bei allen git Repositories ausführen'
admin.actions.sync-previews: 'Alle Gist Vorschauen synchronisieren'
admin.actions.reset-hooks: 'Alle Git server Hooks für alle Repositories synchronisieren'
admin.actions.index-gists: 'Alle Gists Indexieren'
admin.actions.index-gists: 'Suchindex neu aufbauen'
admin.id: 'ID'
admin.user: 'Benutzer'
admin.delete: 'Löschen'
@@ -236,7 +236,7 @@ flash.admin.sync-db: 'Synchronisiere Repositories aus der Datenbank...'
flash.admin.git-gc: 'Sammle Repositories...'
flash.admin.sync-previews: 'Synchronisiere Gist-Vorschauen...'
flash.admin.reset-hooks: 'Setze Git-Server-Hooks für alle Repositories zurück...'
flash.admin.index-gists: 'Indiziere alle Gists...'
flash.admin.index-gists: 'Suchindex wird neu aufgebaut...'
flash.auth.username-exists: 'Benutzername existiert bereits'
flash.auth.invalid-credentials: 'Ungültige Anmeldeinformationen'
+32 -3
View File
@@ -9,6 +9,7 @@ gist.header.edit: Edit
gist.header.delete: Delete
gist.header.forked-from: Forked from
gist.header.last-active: Last active
gist.header.expires: Expires
gist.header.select-tab: Select a tab
gist.header.code: Code
gist.header.revisions: Revisions
@@ -23,6 +24,7 @@ gist.header.download-zip: Download ZIP
gist.raw: Raw
gist.file-truncated: This file has been truncated.
gist.files-truncated: Not all files in this gist are not displayed. Clone or download the gist to see them all.
gist.file-raw: This file can't be rendered.
gist.file-binary-edit: This file is binary.
gist.watch-full-file: View the full file.
@@ -51,6 +53,14 @@ gist.new.create-a-new-gist: Create a new gist
gist.new.topics: Topics (separate with spaces)
gist.new.drop-files: Drop files here or click to upload
gist.new.any-file-type: Upload any file type
gist.new.expire: Expires
gist.new.expire-never: Never
gist.new.expire-1hour: After 1 hour
gist.new.expire-12hours: After 12 hours
gist.new.expire-1day: After 1 day
gist.new.expire-7days: After 7 days
gist.new.expire-15days: After 15 days
gist.new.expire-custom: Custom date
gist.edit.editing: Editing
gist.edit.edit-gist: Edit %s
@@ -88,10 +98,12 @@ gist.search.found: gists found
gist.search.no-results: No gists found
gist.search.help.user: gists created by user
gist.search.help.title: gists with given title
gist.search.help.description: gists with given description
gist.search.help.filename: gists having files with given name
gist.search.help.extension: gists having files with given extension
gist.search.help.language: gists having files with given language
gist.search.help.topic: gists with given topic
gist.search.help.all: search all fields
gist.search.placeholder.title: Title
gist.search.placeholder.visibility: Visibility
gist.search.placeholder.public: Public
@@ -175,6 +187,7 @@ settings.create-token-help: Access tokens can be used to access the API
settings.token-name: Name
settings.token-permissions: Permissions
settings.token-gist-permission: Gists
settings.token-user-permission: User
settings.token-permission-none: No access
settings.token-permission-read: Read
settings.token-permission-read-write: Read & Write
@@ -190,6 +203,8 @@ settings.token-no-expiration: No expiration
settings.token-expired: expired
settings.token-created: Token created, make sure to copy it now, you won't be able to see it again!
settings.token-deleted: Access token deleted
settings.api-disabled-warning: The REST API is currently disabled. Tokens you create here cannot be used until an administrator enables it.
settings.api-disabled-go-admin: Open admin configuration
auth.signup-disabled: Administrator has disabled signing up
auth.login: Login
@@ -200,6 +215,13 @@ auth.password: Password
auth.register-instead: Register instead
auth.login-instead: Login instead
auth.oauth: Continue with %s account
auth.oauth.no-provider: OAuth provider not found
auth.oauth.complete-registration: Complete your registration
auth.oauth.complete-registration-button: Create account
auth.oauth.signing-in-with: Signing in with %s
auth.oauth.cancel: Cancel
auth.oauth.existing-account: Existing account?
auth.oauth.already-have-account: If you already have an Opengist account, login first and link your %s account from your settings.
auth.mfa: Multi-factor authentication
auth.mfa.passkey: Passkey
auth.mfa.passkeys: Passkeys
@@ -241,7 +263,7 @@ error.signup-disabled: Signing up is disabled
error.signup-disabled-form: Signing up via registration form is disabled
error.login-disabled-form: Logging in via login form is disabled
error.complete-oauth-login: "Cannot complete user auth: %s"
error.oauth-unsupported: Unsupported provider
error.oauth-unsupported: Unsupported OAuth2 provider
error.cannot-bind-data: Cannot bind data
error.invalid-number: Invalid number
error.invalid-character-unescaped: Invalid character unescaped
@@ -285,8 +307,9 @@ admin.actions.sync-db: Synchronize gists from database
admin.actions.git-gc: Garbage collect all git repositories
admin.actions.sync-previews: Synchronize all gists previews
admin.actions.reset-hooks: Reset Git server hooks for all repositories
admin.actions.index-gists: Index all gists
admin.actions.index-gists: Rebuild search index
admin.actions.sync-gist-languages: Synchronize all gists languages
admin.actions.delete-expired-gists: Delete expired gists
admin.id: ID
admin.user: User
admin.delete: Delete
@@ -304,6 +327,8 @@ admin.disable-login: Disable login form
admin.disable-login_help: Forbid logging in via the login form to force using OAuth providers instead.
admin.disable-gravatar: Disable Gravatar
admin.disable-gravatar_help: Disable the usage of Gravatar as an avatar provider.
admin.api-enabled: Enable REST API at /api
admin.api-enabled_help: Allow programmatic access to gists and user info via Personal Access Tokens.
admin.users.delete_confirm: Do you want to delete this user ?
@@ -331,8 +356,9 @@ flash.admin.sync-db: Syncing repositories from database...
flash.admin.git-gc: Garbage collecting repositories...
flash.admin.sync-previews: Syncing Gist previews...
flash.admin.reset-hooks: Resetting Git server hooks for all repositories...
flash.admin.index-gists: Indexing all gists...
flash.admin.index-gists: Rebuilding search index...
flash.admin.sync-gist-languages: Syncing Gist languages...
flash.admin.delete-expired-gists: Deleting expired gists...
flash.auth.username-exists: Username already exists
flash.auth.invalid-credentials: Invalid credentials
@@ -343,6 +369,8 @@ flash.auth.user-sshkeys-not-created: Could not create ssh key
flash.auth.must-be-logged-in: You must be logged in to access gists
flash.auth.passkey-registred: Passkey %s registered
flash.auth.passkey-deleted: Passkey deleted
flash.auth.oauth-session-expired: OAuth2 session expired, please try again
flash.auth.oauth-already-linked: This %s account is already linked to another user
flash.gist.visibility-changed: Gist visibility has been changed
flash.gist.deleted: Gist has been deleted
@@ -364,5 +392,6 @@ validation.should-only-contain-alphanumeric-characters-and-dashes: Field %s shou
validation.not-enough: Not enough %s
validation.invalid: Invalid %s
validation.invalid-gist-topics: Invalid gist topics, they must start with a letter or number, consist of 50 characters or less, and can include hyphens
validation.invalid-expiration-date: Invalid expiration date, it must be a valid date in the future
html.title.admin-panel: Admin panel
+2 -2
View File
@@ -213,7 +213,7 @@ admin.invitations: 'Invitaciones'
admin.invitations.create: 'Crear invitación'
admin.actions.sync-previews: 'Sincronizar todas las vistas previas de gists'
admin.actions.reset-hooks: 'Resetear los hooks de Git en todos los repositorios'
admin.actions.index-gists: 'Indexar todos los gists'
admin.actions.index-gists: 'Reconstruir índice de búsqueda'
admin.config-link-overriden: 'sobrescrito'
admin.invitations.help: 'Las invitaciones se pueden usar para crear una cuenta aunque el registro esté deshabilitado.'
admin.invitations.max_uses: 'Cantidad máxima de usos'
@@ -231,7 +231,7 @@ flash.admin.sync-db: 'Sincronizando repositorios desde la base de datos...'
flash.admin.git-gc: 'Recolectando basura en los repositorios...'
flash.admin.sync-previews: 'Sincronizando vistas previas de gists...'
flash.admin.reset-hooks: 'Reseteando hooks del servidor Git en todos los repositorios...'
flash.admin.index-gists: 'Indexando todos los gists...'
flash.admin.index-gists: 'Reconstruyendo índice de búsqueda...'
flash.auth.username-exists: 'El nombre de usuario ya existe'
flash.auth.invalid-credentials: 'Credenciales incorrectas'
flash.auth.account-linked-oauth: 'Cuenta vinculada a %s'
+2 -2
View File
@@ -193,7 +193,7 @@ admin.actions.reset-hooks: Réinitialiser les hooks de Git pour tous les dépôt
gist.new.url: URL
gist.search.no-results: Aucun gist trouvé
settings.unlink-gitlab-account: Détacher le compte GitLab
admin.actions.index-gists: Indexer tous les gists
admin.actions.index-gists: Reconstruire l'index de recherche
gist.new.preview: 'Aperçu'
gist.new.create-a-new-gist: 'Créer un nouveau gist'
gist.edit.edit-gist: 'Modifier %s'
@@ -231,7 +231,7 @@ flash.admin.sync-db: 'Synchronisation des dépôts à partir de la base de donn
flash.admin.git-gc: 'Nettoyage des dépôts...'
flash.admin.sync-previews: 'Synchronisation des aperçus du Gist...'
flash.admin.reset-hooks: 'Réinitialisation des hooks du serveur Git pour tous les dépôts...'
flash.admin.index-gists: 'Indexation de tous les gists...'
flash.admin.index-gists: 'Reconstruction de l''index de recherche...'
flash.auth.username-exists: 'Nom d''utilisateur déjà utilisé'
flash.auth.invalid-credentials: 'Identifiants non valides'
flash.auth.account-linked-oauth: 'Compte lié à %s'
+1 -1
View File
@@ -170,7 +170,7 @@ admin.actions.sync-db: Gistek szinkronizálása az adatbázissal
admin.actions.git-gc: Használatlan git repository-k eltávolítása
admin.actions.sync-previews: Gist előnézetek szinkronizálása
admin.actions.reset-hooks: Git server hook-ok alaphelyzetbe állítása minden repository-nál
admin.actions.index-gists: Gistek indexelése
admin.actions.index-gists: Keresési index újraépítése
admin.id: Azonosító
admin.user: Felhasználó
admin.delete: Törlés
+2 -2
View File
@@ -191,7 +191,7 @@ admin.actions.sync-db: 'Sincronizza gists dal database'
admin.actions.git-gc: 'Esegui la garbage collection da tutti i repositories'
admin.actions.sync-previews: 'Sincronizza tutte le anteprime dei gists'
admin.actions.reset-hooks: 'Resetta tutti gli hook del server Git per tutti i repositories'
admin.actions.index-gists: 'Indicizza tutti i gists'
admin.actions.index-gists: 'Ricostruisci indice di ricerca'
admin.id: 'ID'
admin.user: 'Utente'
admin.delete: 'Elimina'
@@ -235,7 +235,7 @@ flash.admin.sync-db: 'Sincronizzando i repositories dal database...'
flash.admin.git-gc: 'Eseguendo il garbage collector dei repositories...'
flash.admin.sync-previews: 'Sincronizzando le anteprime dei gists...'
flash.admin.reset-hooks: 'Resettando gli hook di Git per tutti i repositories...'
flash.admin.index-gists: 'Indicizzando tutti i gists...'
flash.admin.index-gists: 'Ricostruzione indice di ricerca...'
flash.auth.username-exists: 'Il nome utente esiste già'
flash.auth.invalid-credentials: 'Credenziali errate'
+2 -2
View File
@@ -227,7 +227,7 @@ admin.actions.sync-db: 'Synchronizuj Gisty z bazy danych'
admin.actions.git-gc: 'Zbierz śmieci we wszystkich repozytoriach Git'
admin.actions.sync-previews: 'Synchronizuj podglądy wszystkich Gistów'
admin.actions.reset-hooks: 'Zresetuj hooki serwera Git dla wszystkich repozytoriów'
admin.actions.index-gists: 'Indeksuj wszystkie Gisty'
admin.actions.index-gists: 'Przebuduj indeks wyszukiwania'
admin.id: 'ID'
admin.user: 'Użytkownik'
admin.delete: 'Usuń'
@@ -271,7 +271,7 @@ flash.admin.sync-db: 'Synchronizowanie repozytoriów z bazy danych...'
flash.admin.git-gc: 'Zbieranie śmieci w repozytoriach...'
flash.admin.sync-previews: 'Synchronizowanie podglądów Gistów...'
flash.admin.reset-hooks: 'Resetowanie hooków serwera Git dla wszystkich repozytoriów...'
flash.admin.index-gists: 'Indeksowanie wszystkich Gistów...'
flash.admin.index-gists: 'Przebudowywanie indeksu wyszukiwania...'
flash.auth.username-exists: 'Nazwa użytkownika już istnieje'
flash.auth.invalid-credentials: 'Niepoprawne dane logowania'
+2 -2
View File
@@ -214,7 +214,7 @@ admin.invitations: 'Инвайты'
admin.invitations.create: 'Создать инвайт'
admin.actions.sync-previews: 'Обновить предпросмотры всех фрагментов'
admin.actions.reset-hooks: 'Сбросить хуки Git-сервера для всех репозиториев'
admin.actions.index-gists: роиндексировать все фрагменты'
admin.actions.index-gists: ерестроить поисковый индекс'
validation.should-not-be-empty: 'Поле %s не должно быть пустым'
admin.invitations.help: 'Инвайты используются для создания аккаунта, даже когда регистрация запрещена.'
admin.invitations.max_uses: 'Максимальное количество использований'
@@ -232,7 +232,7 @@ flash.admin.sync-db: 'Выполняется синхронизация репо
flash.admin.git-gc: 'Сборка мусора в репозиториях…'
flash.admin.sync-previews: 'Обновление предпросмотров фрагментов…'
flash.admin.reset-hooks: 'Пересоздание Git-хуков для всех репозиториев…'
flash.admin.index-gists: 'Выполняется индексация фрагментов…'
flash.admin.index-gists: 'Перестроение поискового индекса…'
flash.auth.username-exists: 'Такое имя пользователя уже занято'
flash.auth.invalid-credentials: 'Некорректные данные для входа'
flash.auth.account-linked-oauth: 'Учётная запись связана с %s'
+2 -2
View File
@@ -191,7 +191,7 @@ admin.actions.sync-db: Gistleri veri tabanından senkronize et
admin.actions.git-gc: Tüm Git depolarındaki gereksiz verileri temizle
admin.actions.sync-previews: Tüm gist önizlemelerini senkronize et
admin.actions.reset-hooks: Tüm depolar için Git sunucu kancalarını sıfırla
admin.actions.index-gists: Tüm gistleri indeksle
admin.actions.index-gists: Arama dizinini yeniden oluştur
admin.id: ID
admin.user: Kullanıcı
admin.delete: Sil
@@ -234,7 +234,7 @@ flash.admin.sync-db: Depolar veri tabanından senkronize ediliyor...
flash.admin.git-gc: Depolardan gereksiz veriler temizleniyor...
flash.admin.sync-previews: Gist önizlemeleri senkronize ediliyor...
flash.admin.reset-hooks: Tüm depolar için Git sunucusu kancaları sıfırlanıyor...
flash.admin.index-gists: Tüm gistler indeksleniyor...
flash.admin.index-gists: Arama dizini yeniden oluşturuluyor...
flash.auth.username-exists: Kullanıcı adı zaten mevcut
flash.auth.invalid-credentials: Geçersiz kimlik bilgileri
+4 -4
View File
@@ -77,7 +77,7 @@ gist.list.all-from: Всі gists від %s
gist.search.found: gists знайдено
gist.search.no-results: Не знайдено gists
gist.search.help.user: gists створені користувачем
gist.search.help.title: gists з наданим ім'ям
gist.search.help.title: gists з наданим ім'ям
gist.search.help.filename: gists мають файли з наданим ім'ям
gist.search.help.extension: gists мають файли з наданим розширенням
gist.search.help.language: gists мають файли з наданою мовою
@@ -192,7 +192,7 @@ admin.actions.sync-db: Синхронізувати gists з базою дани
admin.actions.git-gc: Збір сміття з репозиторіїв Git
admin.actions.sync-previews: Синхронізувати всі gists перегляди
admin.actions.reset-hooks: Скинути серверні Git hooks для всіх репозиторіїв
admin.actions.index-gists: Проіндексувати всі gists
admin.actions.index-gists: Перебудувати пошуковий індекс
admin.id: ID
admin.user: Користувач
admin.delete: Видалити
@@ -236,7 +236,7 @@ flash.admin.sync-db: Синхронізація репозиторіїв за б
flash.admin.git-gc: Збір сміття з репозиторіїв...
flash.admin.sync-previews: Синхронізація Gist переглядів...
flash.admin.reset-hooks: Скидання cерверниз Git hooks для всіх репозиторіїв...
flash.admin.index-gists: Індексація всіх gists...
flash.admin.index-gists: Перебудова пошукового індексу...
flash.auth.username-exists: Це ім'я користувача вже існує
flash.auth.invalid-credentials: Недійсні облікові дані
@@ -266,4 +266,4 @@ validation.should-only-contain-alphanumeric-characters-and-dashes: Поле %s
validation.not-enough: Недостатньо %s
validation.invalid: Недійсний %s
html.title.admin-panel: Панель адміністратора
html.title.admin-panel: Панель адміністратора
+33 -2
View File
@@ -87,6 +87,11 @@ gist.revision.no-changes: 没有任何变更
gist.revision.no-revisions: 无可供显示的修订
settings: 设置
settings.header.account: 账号
settings.header.mfa: 多因素认证
settings.header.ssh: SSH
settings.header.tokens: 访问令牌
settings.header.style: 样式
settings.email: 电子邮箱
settings.email-help: 用于提交和 Gravatar 头像
settings.email-set: 设置邮箱地址
@@ -107,6 +112,30 @@ settings.ssh-key-added-at: 添加于
settings.ssh-key-never-used: 从未使用过
settings.ssh-key-last-used: 最后使用于
settings.create-token: 创建访问令牌
settings.create-token-help: 访问令牌用于程序化调用 API
settings.token-name: 名称
settings.token-permissions: 权限
settings.token-gist-permission: Gists
settings.token-user-permission: 用户
settings.token-permission-none: 无访问权限
settings.token-permission-read: 只读
settings.token-permission-read-write: 读写
settings.delete-token: 删除
settings.delete-token-confirm: 确认删除访问令牌
settings.token-created-at: 创建于
settings.token-never-used: 从未使用
settings.token-last-used: 最后使用
settings.token-expiration: 过期时间
settings.token-expiration-help: 留空表示永不过期
settings.token-expires-at: 过期于
settings.token-no-expiration: 永不过期
settings.token-expired: 已过期
settings.token-created: 令牌已创建,请立即复制保存,离开本页后将无法再次查看!
settings.token-deleted: 访问令牌已删除
settings.api-disabled-warning: REST API 当前已禁用,在此处创建的令牌需要管理员启用 API 后才能使用。
settings.api-disabled-go-admin: 前往管理后台启用
auth.signup-disabled: 管理员已禁用注册
auth.login: 登录
auth.signup: 注册
@@ -166,6 +195,8 @@ admin.disable-login: 禁用登录表单
admin.disable-login_help: 禁止使用登录表单进行登录以强制通过 OAuth 提供方登录。
admin.disable-gravatar: 禁用 Gravatar
admin.disable-gravatar_help: 停止使用 Gravatar 作为头像提供方。
admin.api-enabled: 启用 REST API/api
admin.api-enabled_help: 允许通过 Personal Access Token 程序化访问 Gist 和用户信息。
admin.allow-gists-without-login: 允许未登录状态下访问单个 Gists
admin.allow-gists-without-login_help: 允许在不登录的情况下查看和下载 Gist,同时需要登录才能使用 Gists 的发现功能。
admin.users.delete_confirm: 您想要删除此用户吗?
@@ -214,7 +245,7 @@ admin.invitations: '邀请'
admin.invitations.create: '创建邀请'
admin.actions.sync-previews: '同步所有 Gists 预览'
admin.actions.reset-hooks: '重置所有存储库的 Git 服务 hooks'
admin.actions.index-gists: '索引所有 Gists'
admin.actions.index-gists: '重建搜索索引'
admin.invitations.help: '即使在禁用注册功能的情况下,邀请功能也可用于创建帐户。'
admin.invitations.max_uses: '最多使用次数'
admin.invitations.expires_at: '过期时间'
@@ -231,7 +262,7 @@ flash.admin.sync-db: '正在从数据库同步存储库...'
flash.admin.git-gc: '正在进行存储库垃圾回收...'
flash.admin.sync-previews: '正在同步 Gist 预览...'
flash.admin.reset-hooks: '正在重置所有存储库的 Git 服务挂钩...'
flash.admin.index-gists: '正在索引所有 Gists...'
flash.admin.index-gists: '正在重建搜索索引...'
flash.auth.username-exists: '用户名已存在'
flash.auth.invalid-credentials: '无效的凭证'
flash.auth.account-linked-oauth: '帐户已关联到 %s'
+1 -1
View File
@@ -190,7 +190,7 @@ gist.search.no-results: 沒有找到任何 Gists
gist.search.help.title: Gists 的標題
gist.search.help.filename: Gists 的檔案名稱
gist.search.help.language: Gists 的程式語言
admin.actions.index-gists: 索引所有的 Gists
admin.actions.index-gists: 重建搜尋索引
gist.search.help.user: 由使用者建立的 Gists
gist.search.found: 已找到 Gists
gist.search.help.extension: Gists 的副檔名

Some files were not shown because too many files have changed in this diff Show More