Compare commits

...

101 Commits

Author SHA1 Message Date
Thomas Miceli
c7b947580d v1.11.0 2025-09-21 04:51:49 +02:00
Thomas Miceli
4106956f6d Fix human date on iOS devices (#510) 2025-09-21 04:31:58 +02:00
Fabio Manganiello
c02bf97b63 feat: Add support for rendering .ipynb Jupyter/IPython notebooks (#491) 2025-09-21 03:48:59 +02:00
Thomas Miceli
53ce41e0e4 Add file upload on gist creation/edition (#507) 2025-09-16 01:56:38 +02:00
Thomas Miceli
594d876ba8 Add binary files support (#503) 2025-09-16 01:35:54 +02:00
Thomas Miceli
905276f24b Init gist with regular urls via git CLI (http) (#501) 2025-08-28 02:44:09 +02:00
Sebastian Ertz
2976173658 Update go dep chroma (#493) 2025-08-18 16:05:07 +02:00
Thomas Miceli
b048203216 Use db for queue (#498) 2025-08-18 16:01:50 +02:00
Thomas Miceli
a7a25c4100 Fix LDAP with valid old password login (#497) 2025-08-14 11:10:45 +02:00
Alex Martens
bb1991f3ca Add OIDC group claim name to OpenID request (#490)
This fixes Kanidm compatibility.
2025-08-01 17:55:34 +02:00
Thomas Miceli
979b302e4c Add listen to Unix websocket (#484) 2025-08-01 17:34:52 +02:00
s1shed
b18cdb9188 Redirect to $baseUrl after auth with passkey instead of / (#482)
Fixes: #481
2025-07-01 14:40:33 +02:00
Aly Smith
867aa6e57b Replace unicode characters with HTML entity codes in embed template (#480) 2025-07-01 14:39:47 +02:00
Thomas Miceli
3c0115d829 Fix Markdown preview links (#475) 2025-05-15 15:16:40 +02:00
Thomas Miceli
d796895b75 Fix filename unescape (#474) 2025-05-14 11:51:42 +02:00
Andy Piper
5542497622 Add Proxmox VE Helper-Script (#473) 2025-05-14 10:49:27 +02:00
Thomas Miceli
546f1968e0 Fix helm ci 2025-05-09 20:16:57 +02:00
Thomas Miceli
75e71fd042 Use Helm deployment.env[] values (#471) 2025-05-09 20:08:25 +02:00
Thomas Miceli
897dc43790 Add LDAP authentication (#470)
* Introduce basic LDAP authentication.

* Reformat LDAP code; use ldap in Git HTTP

* lint

---------

Co-authored-by: Santhosh Raju <santhosh.raju@gmail.com>
2025-05-09 19:32:22 +02:00
Johannes Kirchner
72e02700ec fix: Correct German spelling, use consistent wording (#468) 2025-05-05 15:04:28 +02:00
Thomas Miceli
dc43fccc04 Style preference tab for user (#467) 2025-05-05 01:31:42 +02:00
Sergey Ryazanov
0e9b778b45 Fix Gitlab avatar (#461)
* Fix GitLab user avatar method

* Fix size of Gitlab avatar
2025-05-05 00:46:29 +02:00
Johannes Kirchner
3c940cd81f feat: read psql sslmode from db uri (#462) 2025-05-05 00:29:13 +02:00
Thomas Miceli
de144d09d3 Update README.md 2025-04-09 15:45:38 +02:00
Thomas Miceli
fde8a85e2b v1.10.0 2025-04-07 16:31:45 +02:00
Thomas Miceli
b82b3d9e0e Update Go deps (#455) 2025-04-06 01:11:44 +02:00
Thomas Miceli
9e69677f58 Add Helm Chart (#454) 2025-04-06 00:51:38 +02:00
Thomas Miceli
2d8debecbe Translations update from Opengist (#438)
* Added translation using Weblate (Japanese)

* Translated using Weblate (Japanese)

Currently translated at 15.8% (47 of 297 strings)

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

* Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 100.0% (297 of 297 strings)

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

---------

Co-authored-by: YoshichikaAAA <isthisyourpen@gmail.com>
Co-authored-by: Ricky <1173024819@qq.com>
2025-04-06 00:51:18 +02:00
Johannes Kirchner
8cfaceb303 feat: read admin group from OIDC token claim (#445) 2025-04-02 13:38:11 +02:00
jmjl
7907c7bc1e Fix gist.html using relative URL (#451)
Due to the fact the file templates/base/base_header.html contains a
<base> element, all relative URLs are interpreted as dependant on the
base.[1]

I've noticed the base isn't the current page, but the element linking to
anchor identifier isn't using the complete URL to the gist page, which
means that if you go to a gist, and try to click on the link that leads
you to the file (which would make browsers automatically go down if it's
a file that has a lot of lines), you get taken to the homepage, and
unless you look at the URL closely you wouldn't notice the
fragment/anchor part.

I'm sure there's a better way of dealing with this, such as removing
<base> from the template mentioned above, but due to the fact I'd like
to have this work, I've made it put the full URL to this page.

Something that might be good to do is making the relative URLs always be
absolute, by having the '{{ $.c.ExternalUrl }}' thing everywhere where a
relative URL would be, as that'd probably fix #415, and would allow for
this commit to be reverted if that's desired.

[1] https://developer.mozilla.org/en-US/docs/Web/HTML/Element/base
2025-03-31 23:07:01 +02:00
Philipp Eckel
e3aa994d30 fix: do not hide file delete button on gist edit page (#447) 2025-03-31 22:44:04 +02:00
Ross A. Baker
91df15f957 Allow lag between admin invitation creation and test assertion (#452) 2025-03-31 11:53:12 +02:00
Thomas Miceli
efba783c56 Add Meilisearch indexer (#444) 2025-03-19 23:28:04 +01:00
Philipp Eckel
dbdfcd4e85 feat: add option to name an OIDC provider (#435) 2025-03-17 17:19:48 +01:00
awkj
da0b440360 Fix garbled/mojibake text display issues for non-English Unicode characters in browsers. (#441)
* Update util.go

Fix garbled/mojibake text display issues for non-English Unicode characters in browsers.

* add Content-Disposition, help handle file name on download

Author:    awkj <hzzbiu@gmail.com>
2025-03-17 16:22:54 +01:00
Thomas Miceli
d53885c541 Fix test database with go command (#442) 2025-03-17 16:17:53 +01:00
Philipp Eckel
1ec026e191 feat: add Prometheus metrics (#439)
* feat: add Prometheus metrics

* setup metrics using Prometheus client under /metrics endpoint
* add configuration value for metrics
* configure Prometheus middleware for generic metrics
* provide metrics for totals of users, gists and SSH keys
* modify test request to optionally return the response
* provide integration test for Prometheus metrics
* update documentation

* chore: make fmt
2025-03-17 14:30:38 +01:00
Thomas Miceli
8c7e941182 v1.9.1 2025-02-04 21:22:47 +01:00
Thomas Miceli
26b5044380 Update go deps (#430) 2025-02-04 21:17:10 +01:00
Thomas Miceli
a2259d5c77 Translations update from Opengist (#401)
* Translated using Weblate (German)

Currently translated at 94.3% (265 of 281 strings)

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

* Translated using Weblate (German)

Currently translated at 99.6% (280 of 281 strings)

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

---------

Co-authored-by: Sangelo <minecraft.sangelo89@gmail.com>
Co-authored-by: m4skedbyte <m4skedbyte@protonmail.com>
2025-02-03 23:43:59 +01:00
Thomas Miceli
6fd7f77003 Fix user avatar on gist likes list (#425) 2025-02-03 23:43:43 +01:00
Thomas Miceli
87ae60ce4c Fix SQL query for MySQL/Postgres on user profile (#424) 2025-02-03 23:29:34 +01:00
Thomas Miceli
c14380f4de v1.9.0 2025-02-02 20:48:40 +01:00
Thomas Miceli
da36e9eb55 Add Docker hub in release images registry CI 2025-02-02 20:11:58 +01:00
Thomas Miceli
7aa8f84eff Search gists on user profile with title, visibility, language & topics (#422) 2025-02-02 18:14:03 +01:00
Thomas Miceli
76fc129c09 Remove memdb for gist init (#421) 2025-01-30 10:46:35 +01:00
Thomas Miceli
62d56cd1c7 Save content form on gist create error (#420) 2025-01-29 16:00:58 +01:00
Thomas Miceli
d363743203 Fix empty password error when trying to change the username (#418) 2025-01-27 00:57:46 +01:00
Thomas Miceli
28c7e75657 Use jdenticon for default avatars (#416) 2025-01-27 00:08:50 +01:00
soup
0609b64cff feat: add MIME type support for raw file serving (#417) 2025-01-26 23:40:59 +01:00
Thomas Miceli
f5b8881d35 Add topics for Gists (#413) 2025-01-24 14:39:42 +01:00
gofastasf
8369cbf2f0 fix: replace path.Join with filepath.Join for file system paths (#414) 2025-01-21 07:46:59 +01:00
千橙
2ab9cf556f Add git push option for description (#412) 2025-01-20 18:16:31 +01:00
Thomas Miceli
662f553d37 Remove CSRF check for Git HTTP packs (#408) 2025-01-20 03:18:28 +01:00
Andreas Jaggi
a752e0561d Skip CSRF for embeds (#402)
* Skip CSRF for embeds

The CSRF middleware sets a _csrf cookie also for loading the embed
javascript on third-party sites. With this change no _csrf cookie is set
when loading the embed javascript (regardless if third-party site or
first-party).
2025-01-20 02:18:45 +01:00
Thomas Miceli
f935ee1a7e Refactor server code (#407) 2025-01-20 01:57:39 +01:00
Thomas Miceli
4c5a7bda63 v1.8.4 2024-12-16 01:46:26 +01:00
Thomas Miceli
f6bf09d5c2 Translations update from Opengist (#398)
* Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 100.0% (281 of 281 strings)

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

* Translated using Weblate (Polish)

Currently translated at 100.0% (281 of 281 strings)

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

---------

Co-authored-by: GabrielxD <gabrielxduo@outlook.com>
Co-authored-by: GGORG <GGORG0@protonmail.com>
2024-12-15 17:52:52 +01:00
Phani Rithvij
86dd59c695 fixup! esbuild for all other platforms (#395) 2024-12-15 17:52:33 +01:00
Sangelo
20aef5e694 feat: Add custom instance names (#399)
* Add custom name variable

* Add custom name variable usage to docs

* Remove leftover testing config options (oops)
2024-12-15 17:39:51 +01:00
soup
00951bf63b feat(web): prevent password manager autofill on filename inputs (#357)
* feat(web): add data-1p-ignore attribute to ignore fields

* feat(web): extend password manager ignore attributes

- Add autocomplete="off" to prevent browser autofill
- Add data-lpignore for LastPass compatibility
- Add data-bwignore for Bitwarden compatibility
2024-12-15 17:35:08 +01:00
Thomas Miceli
526da6ccbb v1.8.3 2024-11-26 22:46:25 +01:00
Phani Rithvij
3a4080176c esbuild for all other platforms (#393)
Signed-off-by: phanirithvij <phanirithvij2000@gmail.com>
2024-11-26 22:38:51 +01:00
Phani Rithvij
64306be2d6 init git config failure -> warn (#392)
* init git config failure -> warn

Signed-off-by: phanirithvij <phanirithvij2000@gmail.com>
2024-11-26 22:28:17 +01:00
Thomas Miceli
8543f3adfa v1.8.2 2024-11-25 22:29:31 +01:00
Thomas Miceli
391ffde12e Update Go deps 2024-11-25 22:20:43 +01:00
Thomas Miceli
3193a9e888 Translations update from Opengist (#373)
* Translated using Weblate (French)

Currently translated at 87.5% (245 of 280 strings)

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

* Translated using Weblate (Spanish)

Currently translated at 100.0% (280 of 280 strings)

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

* Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 100.0% (280 of 280 strings)

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

* Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 100.0% (280 of 280 strings)

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

* Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 100.0% (280 of 280 strings)

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

* Added translation using Weblate (Polish)

* Translated using Weblate (Polish)

Currently translated at 100.0% (280 of 280 strings)

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

---------

Co-authored-by: Lucas Colombo <lucasncolombo@gmail.com>
Co-authored-by: GabrielxD <gabrielxduo@outlook.com>
Co-authored-by: GGORG <GGORG0@protonmail.com>
2024-11-25 22:08:45 +01:00
Santhosh Raju
58c5ac11c7 Respect file scheme URIs for SQLite. (#387) 2024-11-25 22:07:13 +01:00
Thomas Miceli
6a8e827d61 Fix nits typos and translation (#388) 2024-11-23 17:41:15 +01:00
Thomas Miceli
8f482bce33 Improve Git config 2024-11-23 17:25:58 +01:00
Thomas Miceli
5994cd6ccd Enforce git config on startup (#383) 2024-11-21 11:23:57 +01:00
Thomas Miceli
00e3d09cc5 Fix escaping for embed gists (#381) 2024-11-18 02:29:05 +01:00
Thomas Miceli
40ff4c7b3f Fix git clone on SSH with MySQL (#382) 2024-11-17 21:25:59 +01:00
Thomas Miceli
c1e046f428 Convert octal notation file names in Git (#380) 2024-11-17 18:09:44 +01:00
Thomas Miceli
92bac3bf8c v1.8.1 2024-11-02 02:00:48 +01:00
Thomas Miceli
73c2fb55bc Fix confirm() popup messages (#370) 2024-11-02 01:40:10 +01:00
Thomas Miceli
75162b3ef9 Hide passkey login when login form is disabled (#369) 2024-11-02 01:06:14 +01:00
Thomas Miceli
d537153785 Fix Markdown preview (#368) 2024-11-02 01:05:43 +01:00
Thomas Miceli
97b9fa1100 Fix typos 2024-11-01 00:00:09 +01:00
Thomas Miceli
393c9756d4 v1.8.0 2024-10-31 20:48:54 +01:00
Aloys
63d4b46a41 Fix typos (#363) 2024-10-31 20:19:02 +01:00
Thomas Miceli
91c412d97e Translations update from Opengist (#339)
* Translated using Weblate (Turkish)

Currently translated at 100.0% (244 of 244 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 80.3% (196 of 244 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 80.3% (196 of 244 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 80.3% (196 of 244 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (262 of 262 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (262 of 262 strings)

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

---------

Co-authored-by: Taylan Tatlı <taylantatli90@gmail.com>
Co-authored-by: lkw123 <2020393267@qq.com>
Co-authored-by: Doracoin <doracoin@foxmail.com>
2024-10-31 18:34:04 +01:00
Thomas Miceli
7cc2b497ca Use mail handle if oauth nickname is empty (#362) 2024-10-31 18:24:15 +01:00
zdebel
d5e66d3994 Fix oauth endpoint to support detecting https in 'Forwarded' header, enabling google support (#359) 2024-10-31 15:03:35 +01:00
Thomas Miceli
4fd0832df9 Allow to define secret key & move the secret key file to parent directory (#358) 2024-10-31 14:50:13 +01:00
Thomas Miceli
20372f44e4 Change json response detection (#361) 2024-10-31 14:41:42 +01:00
Thomas Miceli
d0b4815798 Update Go deps and use Go 1.23 (#354) 2024-10-25 01:04:16 +02:00
Phani Rithvij
3cc3fb4572 package-lock.json add integrity, resolved fields (#350)
used https://github.com/jeslie0/npm-lockfile-fix

Signed-off-by: phanirithvij <phanirithvij2000@gmail.com>
2024-10-25 00:25:10 +02:00
Thomas Miceli
ca44abfc43 Fix build Postcss error (#353) 2024-10-24 23:37:04 +02:00
Thomas Miceli
2bf434f00e Add TOTP MFA (#342) 2024-10-24 23:23:00 +02:00
Thomas Miceli
df226cbd99 Add SVG parser (#346) 2024-10-14 21:20:56 +02:00
Thomas Miceli
3068588111 Send Markdown preview data as form params (#347) 2024-10-14 14:43:12 +02:00
Emmanuel Ferdman
12696d23b0 Update config file (#343)
Signed-off-by: Emmanuel Ferdman <emmanuelferdman@gmail.com>
2024-10-13 23:47:06 +02:00
Patrick MARIE
798a0bfc28 Allow adding multiple empty lines in editor. (#345) 2024-10-13 23:45:50 +02:00
Thomas Miceli
6959929094 Add passkeys support + MFA (#341) 2024-10-07 23:56:32 +02:00
Thomas Miceli
41dc2e451b Use Docker secrets (#340) 2024-09-28 01:31:18 +02:00
Thomas Miceli
56b4fd45fd Add queriable shorter uuids (#338) 2024-09-23 18:14:56 +02:00
Thomas Miceli
605c8b892a Add/Remove admins (#337) 2024-09-23 16:55:57 +02:00
Thomas Miceli
fa8217e27f Separate OAuth unlink URL (#336) 2024-09-22 23:21:43 +02:00
Thomas Miceli
9ac7a76f4a Fix CI trigger 2024-09-22 23:21:30 +02:00
Thomas Miceli
17237713a1 Add Postgres and MySQL databases support (#335) 2024-09-20 16:01:09 +02:00
211 changed files with 14733 additions and 5009 deletions

View File

@@ -4,42 +4,46 @@ on:
branches: branches:
- master - master
- 'dev-*' - 'dev-*'
workflow_dispatch:
pull_request: pull_request:
paths-ignore: paths-ignore:
- '**.yml' - '**.yml'
- '**.md'
jobs: jobs:
lint: lint:
name: Lint
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v3 uses: actions/checkout@v3
- name: Set up Go 1.22 - name: Set up Go 1.23
uses: actions/setup-go@v4 uses: actions/setup-go@v4
with: with:
go-version: "1.22" go-version: "1.23"
- name: Lint - name: Lint
uses: golangci/golangci-lint-action@v3 uses: golangci/golangci-lint-action@v6
with: with:
version: v1.54 version: v1.60
skip-pkg-cache: true args: --out-format=colored-line-number --timeout=20m
args: --out-format=colored-line-number --timeout=20m
- name: Format - name: Format
run: make fmt check_changes run: make fmt check_changes
check: check:
name: Check
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v3 uses: actions/checkout@v3
- name: Set up Go 1.22 - name: Set up Go 1.23
uses: actions/setup-go@v4 uses: actions/setup-go@v4
with: with:
go-version: "1.22" go-version: "1.23"
- name: Check Go modules - name: Check Go modules
run: make go_mod check_changes run: make go_mod check_changes
@@ -47,12 +51,58 @@ jobs:
- name: Check translations - name: Check translations
run: make check-tr run: make check-tr
test-db:
name: Test
strategy:
fail-fast: false
matrix:
os: ["ubuntu-latest"]
go: ["1.23"]
database: [postgres, mysql]
include:
- database: postgres
image: postgres:16
port: 5432:5432
- database: mysql
image: mysql:8
port: 3306:3306
runs-on: ${{ matrix.os }}
services:
database:
image: ${{ matrix.image }}
ports:
- ${{ matrix.port }}
env:
POSTGRES_PASSWORD: opengist
POSTGRES_DB: opengist_test
MYSQL_ROOT_PASSWORD: opengist
MYSQL_DATABASE: opengist_test
options: >-
--health-cmd ${{ matrix.database == 'postgres' && 'pg_isready' || '"mysqladmin ping"' }}
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Set up Go ${{ matrix.go }}
uses: actions/setup-go@v4
with:
go-version: ${{ matrix.go }}
- name: Run tests
run: make test TEST_DB_TYPE=${{ matrix.database }}
test: test:
name: Test
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
os: ["ubuntu-latest", "macOS-latest", "windows-latest"] os: ["ubuntu-latest", "macOS-latest", "windows-latest"]
go: ["1.22"] go: ["1.23"]
database: ["sqlite"]
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
steps: steps:
- name: Checkout - name: Checkout
@@ -64,5 +114,25 @@ jobs:
go-version: ${{ matrix.go }} go-version: ${{ matrix.go }}
- name: Run tests - name: Run tests
run: make test run: make test TEST_DB_TYPE=${{ matrix.database }}
build:
name: Build
strategy:
fail-fast: false
matrix:
os: ["ubuntu-latest", "macOS-latest", "windows-latest"]
go: ["1.23"]
runs-on: ${{ matrix.os }}
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Set up Go 1.23
uses: actions/setup-go@v4
with:
go-version: ${{ matrix.go }}
- name: Build
shell: bash
run: make

53
.github/workflows/helm.yml vendored Normal file
View File

@@ -0,0 +1,53 @@
name: Build / Deploy Helm Chart
on:
workflow_dispatch:
jobs:
build-and-deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Helm
uses: azure/setup-helm@v4.3.0
with:
version: 'latest'
- name: Update Helm chart dependencies
run: |
cd ./helm/opengist
helm dependency update
- name: Package Helm chart
run: |
cd ./helm
helm package ./opengist
# First time, create the index
wget -q https://helm.opengist.io/index.yaml
if [ ! -f index.yaml ]; then
helm repo index --url https://helm.opengist.io .
else
# For subsequent runs, merge with existing index
helm repo index --url https://helm.opengist.io --merge index.yaml .
fi
- name: Deploy to server
uses: appleboy/scp-action@master
with:
host: ${{ secrets.SERVER_HOST }}
username: ${{ secrets.SERVER_USERNAME }}
key: ${{ secrets.SERVER_SSH_KEY }}
source: "./helm/*.tgz,./helm/index.yaml"
target: ${{ secrets.HELM_SERVER_PATH }}
- name: Update remote helm repository
uses: appleboy/ssh-action@master
with:
host: ${{ secrets.SERVER_HOST }}
username: ${{ secrets.SERVER_USERNAME }}
key: ${{ secrets.SERVER_SSH_KEY }}
script: |
${{ secrets.UPDATE_HELM_REPO }}

View File

@@ -13,10 +13,10 @@ jobs:
- name: Checkout - name: Checkout
uses: actions/checkout@v3 uses: actions/checkout@v3
- name: Set up Go 1.22 - name: Set up Go 1.23
uses: actions/setup-go@v4 uses: actions/setup-go@v4
with: with:
go-version: "1.22" go-version: "1.23"
- name: Cross compile build - name: Cross compile build
run: make all_crosscompile run: make all_crosscompile
@@ -46,6 +46,7 @@ jobs:
with: with:
images: | images: |
ghcr.io/thomiceli/opengist ghcr.io/thomiceli/opengist
docker.io/thomiceli/opengist
tags: | tags: |
type=raw,value=latest,enable=${{ github.ref == format('refs/heads/{0}', github.event.repository.default_branch) }} type=raw,value=latest,enable=${{ github.ref == format('refs/heads/{0}', github.event.repository.default_branch) }}
type=semver,pattern={{major}} type=semver,pattern={{major}}
@@ -65,6 +66,12 @@ jobs:
username: ${{ github.repository_owner }} username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }} password: ${{ secrets.GITHUB_TOKEN }}
- name: Login to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push - name: Build and push
uses: docker/build-push-action@v4 uses: docker/build-push-action@v4
with: with:
@@ -74,4 +81,4 @@ jobs:
tags: ${{ steps.meta.outputs.tags }} tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }} labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha cache-from: type=gha
cache-to: type=gha,mode=max cache-to: type=gha,mode=max

4
.gitignore vendored
View File

@@ -1,11 +1,15 @@
node_modules/ node_modules/
gist.db gist.db
.idea/ .idea/
.vscode/
.DS_Store .DS_Store
/**/.DS_Store /**/.DS_Store
public/assets/* public/assets/*
public/manifest.json public/manifest.json
./opengist
opengist opengist
build/ build/
docs/.vitepress/dist/ docs/.vitepress/dist/
docs/.vitepress/cache/ docs/.vitepress/cache/
helm/opengist/charts/
vendor/

View File

@@ -1,5 +1,177 @@
# Changelog # Changelog
## [1.11.0](https://github.com/thomiceli/opengist/compare/v1.10.0...v1.11.0) - 2025-09-21
See here how to [update](https://opengist.io/docs/update) Opengist.
### Added
- LDAP authentication (#470)
- Listen to Unix websocket (#484)
- Binary files support (#503)
- Support for rendering .ipynb Jupyter/IPython notebooks (#491)
- File upload on gist creation/edition (#507)
- Read psql sslmode from db uri (#462)
- OIDC group claim name to OpenID request (#490)
- Reworked user settings page (#467)
- Style preference tab for user (#467)
- Init gist with regular urls via git CLI (http) (#501)
### Fixed
- Gitlab avatar (#461)
- Correct German spelling, use consistent wording (#468)
- Filename unescape (#474)
- Fix Markdown preview links (#475)
- Replace Unicode characters with HTML entity codes in embed template (#480)
- Redirect to $baseUrl after auth with passkey instead of / (#482)
- Human date on iOS devices (#510)
### Docs
- Add Proxmox VE Helper-Script (#473)
### Other
- Use Helm deployment.env[] values (#471)
- Update Helm Postgres version
- Use database for Gist init queue (#498)
- Update go dep chroma (#493)
## [1.10.0](https://github.com/thomiceli/opengist/compare/v1.9.1...v1.10.0) - 2025-04-07
See here how to [update](https://opengist.io/docs/update) Opengist.
### 🔴 Deprecations
_Removed in the next SemVer MAJOR version of Opengist._
* Use the configuration option `index`/`OG_INDEX` **instead of** `index.enabled`/`OG_INDEX_ENABLED`. The default value is `bleve`.
* The configuration `index.dirname`/`OG_INDEX_DIRNAME` will be removed. If you're using Bleve, the path of the index will be `opengist.index`.
### Added
- Helm Chart (#454)
- Meilisearch indexer (#444)
- Prometheus metrics (#439)
- Config to name the OIDC provider (#435)
- Read admin group from OIDC token claim (#445)
- More translation strings (#438)
### Fixed
- Garbled text display issues for non-English Unicode characters in browsers (#441)
- Test database when running `go test` (#442)
- Allow lag between admin invitation creation and test assertion (#452)
- gist.html using relative URL (#451)
- Do not hide file delete button on gist edit page (#447)
### Other
- Update deps Golang & JS deps (#455)
## [1.9.1](https://github.com/thomiceli/opengist/compare/v1.9.0...v1.9.1) - 2025-02-04
See here how to [update](https://opengist.io/docs/update) Opengist.
### Added
- More translation strings (#401)
### Fixed
- SQL query for MySQL/Postgres on user profile (#424)
- User avatar on gist likes list (#425)
### Other
- Update deps Golang & JS deps (#430)
## [1.9.0](https://github.com/thomiceli/opengist/compare/v1.8.4...v1.9.0) - 2025-02-02
See here how to [update](https://opengist.io/docs/update) Opengist.
### Added
- Topics (tags) for Gists (#413)
- Gist languages saved in database (#422)
- Search gists on user profile with title, visibility, language & topics (#422)
- Jdenticon for default avatars (#416)
- Git push option for description (#412)
- MIME type support for raw file serving (#417)
### Fixed
- Skip CSRF for embed gists (#402)
- Remove CSRF check for Git HTTP packs (#408)
- Replace path.Join with filepath.Join for file system paths (#414)
- Empty password error when trying to change the username (#418)
- Save content form on gist create error (#420)
### Other
- Refactor server code (#407)
- Remove memdb for gist init (#421)
- Added Opengist Docker images to Docker Hub
## [1.8.4](https://github.com/thomiceli/opengist/compare/v1.8.3...v1.8.4) - 2024-12-15
See here how to [update](/docs/update.md) Opengist.
### Added
- More translation strings (#398)
- Custom instance names (#399)
### Fixed
- Prevent passwords managers autofill on filename inputs (#357)
## [1.8.3](https://github.com/thomiceli/opengist/compare/v1.8.2...v1.8.3) - 2024-11-26
See here how to [update](/docs/update.md) Opengist.
### Changed
- Throw `warn` instead of `fatal` on Git global config init failure (#392)
- Define esbuild as a Javascript dependency for all other platforms (#393)
## [1.8.2](https://github.com/thomiceli/opengist/compare/v1.8.1...v1.8.2) - 2024-11-25
See here how to [update](/docs/update.md) Opengist.
### Added
- More translation strings (#373) (#388)
### Changed
- Enforce git config on startup (#383)
- Respect file scheme URIs for SQLite. (#387)
### Fixed
- Convert octal notation file names in Git (#380)
- Git clone on SSH with MySQL (#382)
- Escaping for embed gists (#381)
### Other
- Update deps Golang & JS deps
## [1.8.1](https://github.com/thomiceli/opengist/compare/v1.8.0...v1.8.1) - 2024-11-02
See here how to [update](/docs/update.md) Opengist.
### Changed
- Hide passkey login when login form is disabled (#369)
### Fixed
- Markdown preview (#368)
- confirm() popup messages (#370)
## [1.8.0](https://github.com/thomiceli/opengist/compare/v1.7.5...v1.8.0) - 2024-10-31
See here how to [update](https://opengist.io/docs/update) Opengist.
### 🔴 Deprecations
_Removed in the next SemVer MAJOR version of Opengist._
* Use the configuration option `db-uri`/`OG_DB_URI` **instead of** `db-filename`/`OG_DB_FILENAME`.\
More info [here](https://opengist.io/docs/configuration/databases/sqlite) if you plan to keep SQLite as a DBMS for Opengist.
### Added
- Postgres and MySQL databases support (#335)
- Passkeys & TOTP support + MFA (#341) (#342)
- Add/Remove admins (#337)
- Queriable shorter uuids (#338)
- Use Docker secrets (#340)
- SVG preview in Markdown (#346)
- Secret key definition & move the secret key file to its parent directory (#358)
- More translation strings (#339)
### Changed
- Separate OAuth unlink URL (#336)
### Fixed
- Adding multiple empty lines in editor. (#345)
- Config URL (#343)
- Send Markdown preview data as form params (#347)
- Fix oauth endpoint to support detecting https in 'Forwarded' header, enabling google support (#359)
- Use mail handle if OAuth nickname is empty (#362)
### Other
- Use go 1.23 and update deps (#354)
- Typos in README (#363)
## [1.7.5](https://github.com/thomiceli/opengist/compare/v1.7.4...v1.7.5) - 2024-09-12 ## [1.7.5](https://github.com/thomiceli/opengist/compare/v1.7.4...v1.7.5) - 2024-09-12
See here how to [update](/docs/update.md) Opengist. See here how to [update](/docs/update.md) Opengist.

View File

@@ -15,7 +15,7 @@ RUN apk update && \
musl-dev \ musl-dev \
libstdc++ libstdc++
COPY --from=golang:1.22-alpine /usr/local/go/ /usr/local/go/ COPY --from=golang:1.23-alpine /usr/local/go/ /usr/local/go/
ENV PATH="/usr/local/go/bin:${PATH}" ENV PATH="/usr/local/go/bin:${PATH}"
ENV CGO_ENABLED=0 ENV CGO_ENABLED=0
@@ -33,8 +33,6 @@ FROM base AS dev
EXPOSE 6157 2222 16157 EXPOSE 6157 2222 16157
VOLUME /opengist VOLUME /opengist
RUN git config --global --add safe.directory /opengist
CMD ["make", "watch"] CMD ["make", "watch"]
@@ -60,7 +58,7 @@ RUN apk update && \
libstdc++ libstdc++
RUN addgroup -S opengist && \ RUN addgroup -S opengist && \
adduser -S -G opengist -H -s /bin/ash -g 'Opengist User' opengist adduser -S -G opengist -s /bin/ash -g 'Opengist User' opengist
COPY --from=build --chown=opengist:opengist /opengist/config.yml config.yml COPY --from=build --chown=opengist:opengist /opengist/config.yml config.yml

View File

@@ -4,6 +4,7 @@
BINARY_NAME := opengist BINARY_NAME := opengist
GIT_TAG := $(shell git describe --tags) GIT_TAG := $(shell git describe --tags)
VERSION_PKG := github.com/thomiceli/opengist/internal/config.OpengistVersion VERSION_PKG := github.com/thomiceli/opengist/internal/config.OpengistVersion
TEST_DB_TYPE ?= sqlite
all: clean install build all: clean install build
@@ -49,7 +50,7 @@ watch_backend:
OG_DEV=1 npx nodemon --watch '**/*' -e html,yml,go,js --signal SIGTERM --exec 'go run -ldflags "-X $(VERSION_PKG)=$(GIT_TAG)" . --config config.yml' OG_DEV=1 npx nodemon --watch '**/*' -e html,yml,go,js --signal SIGTERM --exec 'go run -ldflags "-X $(VERSION_PKG)=$(GIT_TAG)" . --config config.yml'
watch: watch:
@sh ./scripts/watch.sh @bash ./scripts/watch.sh
clean: clean:
@echo "Cleaning up build artifacts..." @echo "Cleaning up build artifacts..."
@@ -72,7 +73,7 @@ fmt:
@go fmt ./... @go fmt ./...
test: test:
@go test ./... -p 1 @OPENGIST_TEST_DB=$(TEST_DB_TYPE) go test ./... -p 1
check-tr: check-tr:
@bash ./scripts/check-translations.sh @bash ./scripts/check-translations.sh

View File

@@ -2,9 +2,9 @@
<img height="108px" src="https://raw.githubusercontent.com/thomiceli/opengist/master/public/opengist.svg" alt="Opengist" align="right" /> <img height="108px" src="https://raw.githubusercontent.com/thomiceli/opengist/master/public/opengist.svg" alt="Opengist" align="right" />
Opengist is a **self-hosted** pastebin **powered by Git**. All snippets are stored in a Git repository and can be Opengist is a **self-hosted** Pastebin **powered by Git**. All snippets are stored in a Git repository and can be
read and/or modified using standard Git commands, or with the web interface. read and/or modified using standard Git commands, or with the web interface.
It is similiar to [GitHub Gist](https://gist.github.com/), but open-source and could be self-hosted. It is similar to [GitHub Gist](https://gist.github.com/), but open-source and could be self-hosted.
[Home Page](https://opengist.io) • [Documentation](https://opengist.io/docs) • [Discord](https://discord.gg/9Pm3X5scZT) • [Demo](https://demo.opengist.io) [Home Page](https://opengist.io) • [Documentation](https://opengist.io/docs) • [Discord](https://discord.gg/9Pm3X5scZT) • [Demo](https://demo.opengist.io)
@@ -13,22 +13,23 @@ It is similiar to [GitHub Gist](https://gist.github.com/), but open-source and c
![License](https://img.shields.io/github/license/thomiceli/opengist?color=blue) ![License](https://img.shields.io/github/license/thomiceli/opengist?color=blue)
[![Go CI](https://github.com/thomiceli/opengist/actions/workflows/go.yml/badge.svg)](https://github.com/thomiceli/opengist/actions/workflows/go.yml) [![Go CI](https://github.com/thomiceli/opengist/actions/workflows/go.yml/badge.svg)](https://github.com/thomiceli/opengist/actions/workflows/go.yml)
[![Go Report Card](https://goreportcard.com/badge/github.com/thomiceli/opengist)](https://goreportcard.com/report/github.com/thomiceli/opengist) [![Go Report Card](https://goreportcard.com/badge/github.com/thomiceli/opengist)](https://goreportcard.com/report/github.com/thomiceli/opengist)
[![Translate](https://tr.opengist.io/widget/_/svg-badge.svg)](https://tr.opengist.io/projects/_/opengist/)
## Features ## Features
* Create public, unlisted or private snippets * Create public, unlisted or private snippets
* [Init](/docs/usage/init-via-git.md) / Clone / Pull / Push snippets **via Git** over HTTP or SSH * [Init](/docs/usage/init-via-git.md) / Clone / Pull / Push snippets **via Git** over HTTP or SSH
* Syntax highlighting ; markdown & CSV support * Syntax highlighting ; markdown & CSV support
* Search code in snippets ; browse users snippets, likes and forks * Search code in snippets; browse users snippets, likes and forks
* Add topics to snippets
* Embed snippets in other websites * Embed snippets in other websites
* Revisions history * Revisions history
* Like / Fork snippets * Like / Fork snippets
* Download raw files or as a ZIP archive * Download raw files or as a ZIP archive
* OAuth2 login with GitHub, GitLab, Gitea, and OpenID Connect * OAuth2 login with GitHub, GitLab, Gitea, and OpenID Connect
* Restrict or unrestrict snippets visibility to anonymous users * Restrict or unrestrict snippets visibility to anonymous users
* Docker support * Docker support / Helm Chart
* [More...](/docs/index.md#features) * [More...](/docs/introduction.md#features)
## Quick start ## Quick start
@@ -37,7 +38,7 @@ It is similiar to [GitHub Gist](https://gist.github.com/), but open-source and c
Docker [images](https://github.com/thomiceli/opengist/pkgs/container/opengist) are available for each release : Docker [images](https://github.com/thomiceli/opengist/pkgs/container/opengist) are available for each release :
```shell ```shell
docker pull ghcr.io/thomiceli/opengist:1.7 docker pull ghcr.io/thomiceli/opengist:1.11
``` ```
It can be used in a `docker-compose.yml` file : It can be used in a `docker-compose.yml` file :
@@ -47,11 +48,9 @@ It can be used in a `docker-compose.yml` file :
3. Opengist is now running on port 6157, you can browse http://localhost:6157 3. Opengist is now running on port 6157, you can browse http://localhost:6157
```yml ```yml
version: "3"
services: services:
opengist: opengist:
image: ghcr.io/thomiceli/opengist:1.7 image: ghcr.io/thomiceli/opengist:1.11
container_name: opengist container_name: opengist
restart: unless-stopped restart: unless-stopped
ports: ports:
@@ -78,9 +77,9 @@ Download the archive for your system from the release page [here](https://github
```shell ```shell
# example for linux amd64 # example for linux amd64
wget https://github.com/thomiceli/opengist/releases/download/v1.7.5/opengist1.7.5-linux-amd64.tar.gz wget https://github.com/thomiceli/opengist/releases/download/v1.11.0/opengist1.11.0-linux-amd64.tar.gz
tar xzvf opengist1.7.5-linux-amd64.tar.gz tar xzvf opengist1.11.0-linux-amd64.tar.gz
cd opengist cd opengist
chmod +x opengist chmod +x opengist
./opengist # with or without `--config config.yml` ./opengist # with or without `--config config.yml`
@@ -90,7 +89,7 @@ Opengist is now running on port 6157, you can browse http://localhost:6157
### From source ### From source
Requirements: [Git](https://git-scm.com/downloads) (2.28+), [Go](https://go.dev/doc/install) (1.22+), [Node.js](https://nodejs.org/en/download/) (16+), [Make](https://linux.die.net/man/1/make) (optional, but easier) Requirements: [Git](https://git-scm.com/downloads) (2.28+), [Go](https://go.dev/doc/install) (1.23+), [Node.js](https://nodejs.org/en/download/) (16+), [Make](https://linux.die.net/man/1/make) (optional, but easier)
```shell ```shell
git clone https://github.com/thomiceli/opengist git clone https://github.com/thomiceli/opengist

View File

@@ -1,9 +1,9 @@
# Learn more about Opengist configuration here: # Learn more about Opengist configuration here:
# https://github.com/thomiceli/opengist/blob/master/docs/configuration/index.md # https://github.com/thomiceli/opengist/blob/master/docs/configuration/configure.md
# https://github.com/thomiceli/opengist/blob/master/docs/configuration/cheat-sheet.md # https://github.com/thomiceli/opengist/blob/master/docs/configuration/cheat-sheet.md
# Set the log level to one of the following: debug, info, warn, error, fatal. Default: warn # Set the log level to one of the following: debug, info, warn, error, fatal. Default: warn
log-level: warn log-level: debug
# Set the log output to one or more of the following: `stdout`, `file`. Default: stdout,file # Set the log output to one or more of the following: `stdout`, `file`. Default: stdout,file
log-output: stdout,file log-output: stdout,file
@@ -14,14 +14,23 @@ external-url:
# Directory where Opengist will store its data. Default: ~/.opengist/ # Directory where Opengist will store its data. Default: ~/.opengist/
opengist-home: opengist-home:
# Name of the SQLite database file. Default: opengist.db # Secret key used for session store & encrypt MFA data on database. Default: <randomized 32 bytes>
db-filename: opengist.db secret-key:
# Enable or disable the code search index (either `true` or `false`). Default: true # URI of the database. Default: opengist.db (SQLite) is placed in opengist-home
index.enabled: true # SQLite: file:/path/to/database
# PostgreSQL: postgres://user:password@host:port/database
# MySQL/MariaDB: mysql://user:password@host:port/database
db-uri: opengist.db
# Name of the directory where the code search index is stored. Default: opengist.index # Define the code indexer (either `bleve`, `meilisearch`, or empty for no index). Default: bleve
index.dirname: opengist.index index: bleve
# Set the host for the Meiliseach server
index.meili.host:
# Set the API key for the Meiliseach server
index.meili.api-key:
# Default branch name used by Opengist when initializing Git repositories. # 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 # 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
@@ -29,11 +38,12 @@ git.default-branch:
# Set the journal mode for SQLite. Default: WAL # Set the journal mode for SQLite. Default: WAL
# See https://www.sqlite.org/pragma.html#pragma_journal_mode # See https://www.sqlite.org/pragma.html#pragma_journal_mode
# For SQLite databases only.
sqlite.journal-mode: WAL sqlite.journal-mode: WAL
# HTTP server configuration # HTTP server configuration
# Host to bind to. Default: 0.0.0.0 # Host to bind to. Default: 0.0.0.0
# Use an IP address for network binding. Use a path for Unix socket binding (e.g. /run/opengist.sock)
http.host: 0.0.0.0 http.host: 0.0.0.0
# Port to bind to. Default: 6157 # Port to bind to. Default: 6157
@@ -42,6 +52,12 @@ http.port: 6157
# Enable or disable git operations (clone, pull, push) via HTTP (either `true` or `false`). Default: true # Enable or disable git operations (clone, pull, push) via HTTP (either `true` or `false`). Default: true
http.git-enabled: true http.git-enabled: true
# File permissions for Unix socket (octal format). Default: 0666
unix-socket-permissions: 0666
# Enable or disable the metrics endpoint (either `true` or `false`). Default: false
metrics.enabled: false
# SSH built-in server configuration # SSH built-in server configuration
# Note: it is not using the SSH daemon from your machine (yet) # Note: it is not using the SSH daemon from your machine (yet)
@@ -65,7 +81,6 @@ ssh.external-domain:
# Path or alias to ssh-keygen executable. Default: ssh-keygen # Path or alias to ssh-keygen executable. Default: ssh-keygen
ssh.keygen-executable: ssh-keygen ssh.keygen-executable: ssh-keygen
# OAuth2 configuration # OAuth2 configuration
# The callback/redirect URL must be http://opengist.url/oauth/<github|gitlab|gitea|openid-connect>/callback # The callback/redirect URL must be http://opengist.url/oauth/<github|gitlab|gitea|openid-connect>/callback
@@ -90,11 +105,31 @@ gitea.url: https://gitea.com/
gitea.name: Gitea gitea.name: Gitea
# To create a new OAuth2 application using OpenID Connect: # To create a new OAuth2 application using OpenID Connect:
oidc.provider-name:
oidc.client-key: oidc.client-key:
oidc.secret: oidc.secret:
# Discovery endpoint of the OpenID provider. Generally something like http://auth.example.com/.well-known/openid-configuration # Discovery endpoint of the OpenID provider. Generally something like http://auth.example.com/.well-known/openid-configuration
oidc.discovery-url: oidc.discovery-url:
# The name of the claim containing the groups
oidc.group-claim-name:
# The name of the group that should receive admin rights
oidc.admin-group:
# LDAP authentication configuration
# URL of the LDAP instance e.g: ldap://ldap.example.com:389 ; if not set, LDAP authentication is disabled
ldap.url:
# Bind DN to authenticate against the LDAP e.g: cn=read-only-admin,dc=example,dc=com
ldap.bind-dn:
# The password for the Bind DN.
ldap.bind-credentials:
# The Base DN to start search from e.g: ou=People,dc=example,dc=com
ldap.search-base:
# The filter to search against (the format string %s will be replaced with the username) e.g: (uid=%s)
ldap.search-filter:
# Instance name
# Set your own custom name to be displayed instead of 'Opengist'
custom.name:
# Custom assets # Custom assets
# Add your own custom assets, that are files relatives to $opengist-home/custom/ # Add your own custom assets, that are files relatives to $opengist-home/custom/

View File

@@ -1,75 +0,0 @@
# kustomize
## Simple
`kustomization.yaml`:
```yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
metadata:
name: opengist
resources:
- https://github.com/thomiceli/opengist/deploy/
```
## Full example
`kustomization.yaml`:
```yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
metadata:
name: opengist
namespace: opengist
resources:
- namespace.yaml
- https://github.com/thomiceli/opengist/deploy/?ref:v1.7.5
images:
- name: ghcr.io/thomiceli/opengist
newTag: 1.7.5
patches:
# Add your ingress
- path: ingress.yaml
- patch: |-
- op: add
path: /spec/rules/0/host
value: opengist.mydomain.com
target:
group: networking.k8s.io
version: v1
kind: Ingress
name: opengist
```
`namespace.yaml`:
```yaml
apiVersion: v1
kind: Namespace
metadata:
name: opengist
```
`ingress.yaml`:
```yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: opengist
annotations:
cert-manager.io/cluster-issuer: letsencrypt-production
spec:
ingressClassName: nginx
tls:
- hosts:
- opengist.mydomain.com
secretName: opengist-tls
```

View File

@@ -1,29 +0,0 @@
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: opengist
spec:
selector:
matchLabels:
app.kubernetes.io/name: opengist
template:
metadata:
labels:
app.kubernetes.io/name: opengist
spec:
containers:
- name: opengist
image: ghcr.io/thomiceli/opengist
ports:
- name: http
containerPort: 6157
- name: ssh
containerPort: 2222
volumeMounts:
- mountPath: /opengist
name: data
volumes:
- name: data
persistentVolumeClaim:
claimName: opengist-data

View File

@@ -1,20 +0,0 @@
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: opengist
labels:
app.kubernetes.io/name: opengist
app.kubernetes.io/component: ingress
spec:
rules:
- host: opengist.local
http:
paths:
- pathType: Prefix
path: "/"
backend:
service:
name: opengist
port:
name: http

View File

@@ -1,11 +0,0 @@
---
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
metadata:
name: opengist
resources:
- deployment.yaml
- pvc.yaml
- ingress.yaml
- service.yaml

View File

@@ -1,15 +0,0 @@
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: opengist-data
labels:
app.kubernetes.io/name: opengist
app.kubernetes.io/component: data
spec:
resources:
requests:
storage: 1Gi
volumeMode: Filesystem
accessModes:
- ReadWriteOnce

View File

@@ -1,14 +0,0 @@
---
apiVersion: v1
kind: Service
metadata:
name: opengist
labels:
app.kubernetes.io/name: opengist
spec:
selector:
app.kubernetes.io/name: opengist
ports:
- port: 80
targetPort: http
name: http

View File

@@ -9,4 +9,10 @@ usermod -o -u "$UID" $USER
chown -R "$USER:$USER" /opengist chown -R "$USER:$USER" /opengist
chown -R "$USER:$USER" /config.yml chown -R "$USER:$USER" /config.yml
if [ -f "/run/secrets/opengist_secrets" ]; then
set -a
. /run/secrets/opengist_secrets
set +a
fi
exec su $USER -c "OG_OPENGIST_HOME=/opengist /app/opengist/opengist --config /config.yml" exec su $USER -c "OG_OPENGIST_HOME=/opengist /app/opengist/opengist --config /config.yml"

View File

@@ -25,6 +25,7 @@ export default defineConfig({
{text: 'Introduction', link: '/docs'}, {text: 'Introduction', link: '/docs'},
{text: 'Installation', link: '/docs/installation', items: [ {text: 'Installation', link: '/docs/installation', items: [
{text: 'Docker', link: '/docs/installation/docker'}, {text: 'Docker', link: '/docs/installation/docker'},
{text: 'Kubernetes', link: '/docs/installation/kubernetes'},
{text: 'Binary', link: '/docs/installation/binary'}, {text: 'Binary', link: '/docs/installation/binary'},
{text: 'Source', link: '/docs/installation/source'}, {text: 'Source', link: '/docs/installation/source'},
], ],
@@ -36,11 +37,18 @@ export default defineConfig({
{ {
text: 'Configuration', base: '/docs/configuration', items: [ text: 'Configuration', base: '/docs/configuration', items: [
{text: 'Configure Opengist', link: '/configure'}, {text: 'Configure Opengist', link: '/configure'},
{text: 'Admin panel', link: '/admin-panel'}, {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: 'OAuth Providers', link: '/oauth-providers'},
{text: 'Custom assets', link: '/custom-assets'}, {text: 'Custom assets', link: '/custom-assets'},
{text: 'Custom links', link: '/custom-links'}, {text: 'Custom links', link: '/custom-links'},
{text: 'Cheat Sheet', link: '/cheat-sheet'}, {text: 'Cheat Sheet', link: '/cheat-sheet'},
{text: 'Metrics', link: '/metrics'},
{text: 'Admin panel', link: '/admin-panel'},
], collapsed: false ], collapsed: false
}, },
{ {

View File

@@ -19,7 +19,7 @@ export default {
<div class="mx-auto lg:text-center"> <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/opengist.svg" alt="" > <img class="rotating h-36 mx-auto my-8 " src="https://raw.githubusercontent.com/thomiceli/opengist/master/public/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"> <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.7.5</span> <span class="pr-1">Released 1.11</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"> <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" /> <path stroke-linecap="round" stroke-linejoin="round" d="m4.5 19.5 15-15m0 0H8.25m11.25 0v11.25" />
</svg> </svg>

View File

@@ -0,0 +1,11 @@
# Manage admins
You can add and remove Opengist admins from the CLI.
```bash
./opengist admin toggle-admin <username>
```
```bash
$ ./opengist admin toggle-admin thomas
User thomas admin set to true
```

View File

@@ -4,38 +4,49 @@ aside: false
# Configuration Cheat Sheet # Configuration Cheat Sheet
| YAML Config Key | Environment Variable | Default value | Description | | YAML Config Key | Environment Variable | Default value | Description |
|-----------------------|-------------------------------------|-----------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| |-------------------------|-------------------------------------|-----------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| log-level | OG_LOG_LEVEL | `warn` | Set the log level to one of the following: `debug`, `info`, `warn`, `error`, `fatal`. | | log-level | OG_LOG_LEVEL | `warn` | Set the log level to one of the following: `debug`, `info`, `warn`, `error`, `fatal`. |
| log-output | OG_LOG_OUTPUT | `stdout,file` | Set the log output to one or more of the following: `stdout`, `file`. | | log-output | OG_LOG_OUTPUT | `stdout,file` | Set the log output to one or more of the following: `stdout`, `file`. |
| external-url | OG_EXTERNAL_URL | none | Public URL to access to Opengist. | | external-url | OG_EXTERNAL_URL | none | Public URL to access to Opengist. |
| opengist-home | OG_OPENGIST_HOME | home directory | Path to the directory where Opengist stores its data. | | opengist-home | OG_OPENGIST_HOME | home directory | Path to the directory where Opengist stores its data. |
| db-filename | OG_DB_FILENAME | `opengist.db` | Name of the SQLite database file. | | secret-key | OG_SECRET_KEY | randomized 32 bytes | Secret key used for session store & encrypt MFA data on database. |
| index.enabled | OG_INDEX_ENABLED | `true` | Enable or disable the code search index (`true` or `false`) | | db-uri | OG_DB_URI | `opengist.db` | URI of the database. |
| index.dirname | OG_INDEX_DIRNAME | `opengist.index` | Name of the directory where the code search index is stored. | | index | OG_INDEX | `bleve` | Define the code indexer (either `bleve`, `meilisearch`, or empty for no index). |
| 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) | | index.meili.host | OG_MEILI_HOST | none | Set the host for the Meiliseach server. |
| 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) | | index.meili.api-key | OG_MEILI_API_KEY | none | Set the API key for the Meiliseach server. |
| http.host | OG_HTTP_HOST | `0.0.0.0` | The host on which the HTTP server should bind. | | 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) |
| http.port | OG_HTTP_PORT | `6157` | The port on which the HTTP server should listen. | | 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.git-enabled | OG_HTTP_GIT_ENABLED | `true` | Enable or disable git operations (clone, pull, push) via HTTP. (`true` or `false`) | | 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) |
| ssh.git-enabled | OG_SSH_GIT_ENABLED | `true` | Enable or disable git operations (clone, pull, push) via SSH. (`true` or `false`) | | http.port | OG_HTTP_PORT | `6157` | The port on which the HTTP server should listen. |
| ssh.host | OG_SSH_HOST | `0.0.0.0` | The host on which the SSH server should bind. | | http.git-enabled | OG_HTTP_GIT_ENABLED | `true` | Enable or disable git operations (clone, pull, push) via HTTP. (`true` or `false`) |
| ssh.port | OG_SSH_PORT | `2222` | The port on which the SSH server should listen. | | unix-socket-permissions | OG_UNIX_SOCKET_PERMISSIONS | `0666` | File permissions for Unix socket (octal format). |
| 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. | | metrics.enabled | OG_METRICS_ENABLED | `false` | Enable or disable Prometheus metrics endpoint at `/metrics` (`true` or `false`) |
| ssh.keygen-executable | OG_SSH_KEYGEN_EXECUTABLE | `ssh-keygen` | Path to the SSH key generation executable. | | ssh.git-enabled | OG_SSH_GIT_ENABLED | `true` | Enable or disable git operations (clone, pull, push) via SSH. (`true` or `false`) |
| github.client-key | OG_GITHUB_CLIENT_KEY | none | The client key for the GitHub OAuth application. | | ssh.host | OG_SSH_HOST | `0.0.0.0` | The host on which the SSH server should bind. |
| github.secret | OG_GITHUB_SECRET | none | The secret for the GitHub OAuth application. | | ssh.port | OG_SSH_PORT | `2222` | The port on which the SSH server should listen. |
| gitlab.client-key | OG_GITLAB_CLIENT_KEY | none | The client key for the GitLab OAuth application. | | 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. |
| gitlab.secret | OG_GITLAB_SECRET | none | The secret for the GitLab OAuth application. | | ssh.keygen-executable | OG_SSH_KEYGEN_EXECUTABLE | `ssh-keygen` | Path to the SSH key generation executable. |
| gitlab.url | OG_GITLAB_URL | `https://gitlab.com/` | The URL of the GitLab instance. | | github.client-key | OG_GITHUB_CLIENT_KEY | none | The client key for the GitHub OAuth application. |
| gitlab.name | OG_GITLAB_NAME | `GitLab` | The name of the GitLab instance. It is displayed in the OAuth login button. | | github.secret | OG_GITHUB_SECRET | none | The secret for the GitHub OAuth application. |
| gitea.client-key | OG_GITEA_CLIENT_KEY | none | The client key for the Gitea OAuth application. | | gitlab.client-key | OG_GITLAB_CLIENT_KEY | none | The client key for the GitLab OAuth application. |
| gitea.secret | OG_GITEA_SECRET | none | The secret for the Gitea OAuth application. | | gitlab.secret | OG_GITLAB_SECRET | none | The secret for the GitLab OAuth application. |
| gitea.url | OG_GITEA_URL | `https://gitea.com/` | The URL of the Gitea instance. | | gitlab.url | OG_GITLAB_URL | `https://gitlab.com/` | The URL of the GitLab instance. |
| gitea.name | OG_GITEA_NAME | `Gitea` | The name of the Gitea instance. It is displayed in the OAuth login button. | | gitlab.name | OG_GITLAB_NAME | `GitLab` | The name of the GitLab instance. It is displayed in the OAuth login button. |
| oidc.client-key | OG_OIDC_CLIENT_KEY | none | The client key for the OpenID application. | | gitea.client-key | OG_GITEA_CLIENT_KEY | none | The client key for the Gitea OAuth application. |
| oidc.secret | OG_OIDC_SECRET | none | The secret for the OpenID application. | | gitea.secret | OG_GITEA_SECRET | none | The secret for the Gitea OAuth application. |
| oidc.discovery-url | OG_OIDC_DISCOVERY_URL | none | Discovery endpoint of the OpenID provider. | | gitea.url | OG_GITEA_URL | `https://gitea.com/` | The URL of the Gitea instance. |
| custom.logo | OG_CUSTOM_LOGO | none | Path to an image, relative to $opengist-home/custom. | | gitea.name | OG_GITEA_NAME | `Gitea` | The name of the Gitea instance. It is displayed in the OAuth login button. |
| custom.favicon | OG_CUSTOM_FAVICON | none | Path to an image, relative to $opengist-home/custom. | | oidc.provider-name | OG_OIDC_PROVIDER_NAME | none | The name of the OIDC provider |
| custom.static-links | OG_CUSTOM_STATIC_LINK_#_(PATH,NAME) | none | Path and name to custom links, more info [here](custom-links.md). | | 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. |
| 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. |
| ldap.search-base | OG_LDAP_SEARCH_BASE | none | The Base DN to start search from. e.g: ou=People,dc=example,dc=com |
| ldap.search-filter | OG_LDAP_SEARCH_FILTER | none | The filter to search against (the format string %s will be replaced with the username). e.g: (uid=%s) |
| custom.name | OG_CUSTOM_NAME | none | The name of your instance, to be displayed in the tab title |
| custom.logo | OG_CUSTOM_LOGO | none | Path to an image, relative to $opengist-home/custom. |
| custom.favicon | OG_CUSTOM_FAVICON | none | Path to an image, relative to $opengist-home/custom. |
| custom.static-links | OG_CUSTOM_STATIC_LINK_#_(PATH,NAME) | none | Path and name to custom links, more info [here](custom-links.md). |

View File

@@ -46,3 +46,27 @@ Usage via command line :
```shell ```shell
OG_LOG_LEVEL=info ./opengist OG_LOG_LEVEL=info ./opengist
``` ```
### Using Docker Compose secrets
You can use Docker Compose secrets to not expose sensitive information in your compose file, using a `.env` file.
```dotenv
# file secrets.env
OG_GITLAB_CLIENT_KEY=your_gitlab_client_key
OG_GITLAB_SECRET=your_gitlab_secret_key
```
And then use it in your compose file :
```yml
services:
opengist:
# ...
secrets:
- opengist_secrets
secrets:
opengist_secrets:
file: ./secrets.env
```

View File

@@ -28,4 +28,18 @@ custom.favicon: favicon.png
#### Environment variable #### Environment variable
```sh ```sh
export OG_CUSTOM_FAVICON=favicon.png export OG_CUSTOM_FAVICON=favicon.png
``` ```
### Instance Name
It is also possible to set a name for your instance, that would be displayed in the title bar instead of 'Opengist'.
#### YAML
```yaml
custom.name: My Gists
```
#### Environment variable
```sh
export OG_CUSTOM_NAME="My Gists"
```

View File

@@ -0,0 +1,47 @@
# Using MySQL/MariaDB
To use MySQL/MariaDB as the database backend, you need to set the database URI configuration to the connection string of your MySQL/MariaDB database with this format :
`mysql://<user>:<password>@<host>:<port>/<database>`
#### YAML
```yaml
# Example
db-uri: mysql://root:passwd@localhost:3306/opengist_db
```
#### Environment variable
```sh
# Example
OG_DB_URI=mysql://root:passwd@localhost:3306/opengist_db
```
### Docker Compose
```yml
services:
opengist:
image: ghcr.io/thomiceli/opengist:1
container_name: opengist
restart: unless-stopped
depends_on:
- mysql
ports:
- "6157:6157"
- "2222:2222"
volumes:
- "$HOME/.opengist:/opengist"
environment:
OG_DB_URI: mysql://opengist:secret@mysql:3306/opengist_db
# other configuration options
mysql:
image: mysql:8.4
restart: unless-stopped
volumes:
- "./opengist-database:/var/lib/mysql"
environment:
MYSQL_USER: opengist
MYSQL_PASSWORD: secret
MYSQL_DATABASE: opengist_db
MYSQL_ROOT_PASSWORD: rootsecret
```

View File

@@ -0,0 +1,46 @@
# Using PostgreSQL
To use PostgreSQL as the database backend, you need to set the database URI configuration to the connection string of your PostgreSQL database with this format :
`postgres://<user>:<password>@<host>:<port>/<database>`
#### YAML
```yaml
# Example
db-uri: postgres://postgres:passwd@localhost:5432/opengist_db
```
#### Environment variable
```sh
# Example
OG_DB_URI=postgres://postgres:passwd@localhost:5432/opengist_db
```
### Docker Compose
```yml
services:
opengist:
image: ghcr.io/thomiceli/opengist:1
container_name: opengist
restart: unless-stopped
depends_on:
- postgres
ports:
- "6157:6157"
- "2222:2222"
volumes:
- "$HOME/.opengist:/opengist"
environment:
OG_DB_URI: postgres://opengist:secret@postgres:5432/opengist_db
# other configuration options
postgres:
image: postgres:16.4
restart: unless-stopped
volumes:
- "./opengist-database:/var/lib/postgresql/data"
environment:
POSTGRES_USER: opengist
POSTGRES_PASSWORD: secret
POSTGRES_DB: opengist_db
```

View File

@@ -0,0 +1,44 @@
# Using SQLite
By default, Opengist uses SQLite as the database backend.
Because SQLite is a file-based database, there is not much configuration to tweak.
The configuration `db-uri`/`OG_DB_URI` refers to the path of the SQLite database file relative in the `$opengist-home/` directory (default `opengist.db`),
although it can be left untouched. You can also use an absolute path outside the `$opengist-home/` directory.
The SQLite journal mode is set to [`WAL` (Write-Ahead Logging)](https://www.sqlite.org/pragma.html#pragma_journal_mode) by default and can be changed.
#### YAML
```yaml
# default
db-uri: opengist.db
sqlite.journal-mode: WAL
# absolute path outside the $opengist-home/ directory
db-uri: file:/home/user/opengist.db
```
#### Environment variable
```sh
# default
OG_DB_URI=opengist.db
OG_SQLITE_JOURNAL_MODE=WAL
```
### Docker Compose
```yml
services:
opengist:
image: ghcr.io/thomiceli/opengist:1
container_name: opengist
restart: unless-stopped
ports:
- "6157:6157" # HTTP port
- "2222:2222" # SSH port, can be removed if you don't use SSH
volumes:
- "$HOME/.opengist:/opengist"
environment:
OG_SQLITE_JOURNAL_MODE: WAL
# other configuration options
```

View File

@@ -0,0 +1,49 @@
# Metrics
Opengist offers built-in support for Prometheus metrics to help you monitor the performance and usage of your instance. These metrics provide insights into application health, user activity, and database statistics.
## Enabling metrics
By default, the metrics endpoint is disabled for security and performance reasons. To enable it, update your configuration as stated in the [configuration cheat sheet](cheat-sheet.md):
```yaml
metrics.enabled = true
```
Alternatively, you can use the environment variable:
```bash
OG_METRICS_ENABLED=true
```
Once enabled, metrics are available at the /metrics endpoint.
## Available metrics
### Opengist-specific metrics
| Metric Name | Type | Description |
|-------------|------|-------------|
| `opengist_users_total` | Gauge | Total number of registered users |
| `opengist_gists_total` | Gauge | Total number of gists in the system |
| `opengist_ssh_keys_total` | Gauge | Total number of SSH keys added by users |
### Standard HTTP metrics
In addition to the Opengist-specific metrics, standard Prometheus HTTP metrics are also available through the Echo Prometheus middleware. These include request durations, request counts, and request/response sizes.
These standard metrics follow the Prometheus naming convention and include labels for HTTP method, status code, and handler path.
## Security Considerations
The metrics endpoint exposes information about your Opengist instance that might be sensitive in some environments. Consider using a reverse proxy with authentication for the `/metrics` endpoint if your Opengist instance is publicly accessible.
Example with Nginx:
```shell
location /metrics {
auth_basic "Metrics";
auth_basic_user_file /etc/nginx/.htpasswd;
proxy_pass http://localhost:6157/metrics;
}
```

View File

@@ -63,15 +63,32 @@ Opengist can be configured to use OAuth to authenticate users, with GitHub, Gite
* Set 'Redirect URI' to `http://opengist.url/oauth/openid-connect/callback` * Set 'Redirect URI' to `http://opengist.url/oauth/openid-connect/callback`
* Copy the 'Client ID', 'Client Secret', and the discovery endpoint, and add them to the [configuration](cheat-sheet.md) : * Copy the 'Client ID', 'Client Secret', and the discovery endpoint, and add them to the [configuration](cheat-sheet.md) :
```yaml ```yaml
oidc.provider-name: <provider-name>
oidc.client-key: <key> oidc.client-key: <key>
oidc.secret: <secret> oidc.secret: <secret>
# Discovery endpoint of the OpenID provider. Generally something like http://auth.example.com/.well-known/openid-configuration # Discovery endpoint of the OpenID provider. Generally something like http://auth.example.com/.well-known/openid-configuration
oidc.discovery-url: http://auth.example.com/.well-known/openid-configuration oidc.discovery-url: http://auth.example.com/.well-known/openid-configuration
``` ```
```shell ```shell
OG_OIDC_PROVIDER_NAME=<provider-name>
OG_OIDC_CLIENT_KEY=<key> OG_OIDC_CLIENT_KEY=<key>
OG_OIDC_SECRET=<secret> OG_OIDC_SECRET=<secret>
# Discovery endpoint of the OpenID provider. Generally something like http://auth.example.com/.well-known/openid-configuration # Discovery endpoint of the OpenID provider. Generally something like http://auth.example.com/.well-known/openid-configuration
OG_OIDC_DISCOVERY_URL=http://auth.example.com/.well-known/openid-configuration OG_OIDC_DISCOVERY_URL=http://auth.example.com/.well-known/openid-configuration
``` ```
### OIDC Admin Group
OpenGist supports automatic admin privilege assignment based on OIDC group claims. To configure this feature:
```yaml
oidc.group-claim-name: groups # Name of the claim containing the groups
oidc.admin-group: admin-group-name # Name of the group that should receive admin rights
```
```shell
OG_OIDC_GROUP_CLAIM_NAME=groups
OG_OIDC_ADMIN_GROUP=admin-group-name
```
The `group-claim-name` must match the name of the claim in your JWT token that contains the groups.
Users who are members of the configured `admin-group` will automatically receive admin privileges in OpenGist. These privileges are synchronized on every login.

View File

@@ -4,3 +4,4 @@ The following is a list of resources made by happy users of Opengist. Feel free
- [Aetherinox/opengist-debian](https://github.com/Aetherinox/opengist-debian) - A Debian package for Opengist - [Aetherinox/opengist-debian](https://github.com/Aetherinox/opengist-debian) - A Debian package for Opengist
- [How to Install Opengist on Your Synology NAS](https://mariushosting.com/how-to-install-opengist-on-your-synology-nas/) - A guide to install Opengist on a Synology NAS - [How to Install Opengist on Your Synology NAS](https://mariushosting.com/how-to-install-opengist-on-your-synology-nas/) - A guide to install Opengist on a Synology NAS
- [Proxmox VE Helper-Script](https://community-scripts.github.io/ProxmoxVE/scripts?id=opengist) - A script to install Opengist on Proxmox VE

View File

@@ -25,7 +25,7 @@ Opengist is now running on port 6157, you can browse http://localhost:6157
Requirements: Requirements:
* [Git](https://git-scm.com/downloads) (2.28+) * [Git](https://git-scm.com/downloads) (2.28+)
* [Go](https://go.dev/doc/install) (1.22+) * [Go](https://go.dev/doc/install) (1.23+)
* [Node.js](https://nodejs.org/en/download/) (16+) * [Node.js](https://nodejs.org/en/download/) (16+)
* [Make](https://linux.die.net/man/1/make) (optional, but easier) * [Make](https://linux.die.net/man/1/make) (optional, but easier)

View File

@@ -4,9 +4,9 @@ Download the archive for your system from the release page [here](https://github
```shell ```shell
# example for linux amd64 # example for linux amd64
wget https://github.com/thomiceli/opengist/releases/download/v1.7.5/opengist1.7.5-linux-amd64.tar.gz wget https://github.com/thomiceli/opengist/releases/download/v1.11.0/opengist1.11.0-linux-amd64.tar.gz
tar xzvf opengist1.7.5-linux-amd64.tar.gz tar xzvf opengist1.11.0-linux-amd64.tar.gz
cd opengist cd opengist
chmod +x opengist chmod +x opengist
./opengist # with or without `--config config.yml` ./opengist # with or without `--config config.yml`

View File

@@ -13,8 +13,6 @@ It can be used in a `docker-compose.yml` file :
3. Opengist is now running on port 6157, you can browse http://localhost:6157 3. Opengist is now running on port 6157, you can browse http://localhost:6157
```yml ```yml
version: "3"
services: services:
opengist: opengist:
image: ghcr.io/thomiceli/opengist:1 image: ghcr.io/thomiceli/opengist:1

View File

@@ -0,0 +1,15 @@
# Install on Kubernetes
A [Helm](https://helm.sh) chart is available to install Opengist on a Kubernetes cluster.
Check the [Helm documentation](https://helm.sh/docs/) for more information on how to use Helm.
A non-customized installation of Opengist can be done with:
```bash
helm repo add opengist https://helm.opengist.io
helm install opengist opengist/opengist
```
Refer to the [Opengist chart](https://github.com/thomiceli/opengist/tree/master/helm/opengist) for more information
about the chart and to customize your installation.

View File

@@ -2,7 +2,7 @@
Requirements: Requirements:
* [Git](https://git-scm.com/downloads) (2.28+) * [Git](https://git-scm.com/downloads) (2.28+)
* [Go](https://go.dev/doc/install) (1.22+) * [Go](https://go.dev/doc/install) (1.23+)
* [Node.js](https://nodejs.org/en/download/) (16+) * [Node.js](https://nodejs.org/en/download/) (16+)
* [Make](https://linux.die.net/man/1/make) (optional, but easier) * [Make](https://linux.die.net/man/1/make) (optional, but easier)
@@ -10,7 +10,7 @@ Requirements:
git clone https://github.com/thomiceli/opengist git clone https://github.com/thomiceli/opengist
cd opengist cd opengist
git checkout v1.7.5 # optional, to checkout the latest release git checkout v1.11.0 # optional, to checkout the latest release
make make
./opengist ./opengist

View File

@@ -15,6 +15,7 @@ Written in [Go](https://go.dev), Opengist aims to be fast and easy to deploy.
* [Init](usage/init-via-git.md) / Clone / Pull / Push snippets **via Git** over HTTP or SSH * [Init](usage/init-via-git.md) / Clone / Pull / Push snippets **via Git** over HTTP or SSH
* Syntax highlighting ; markdown & CSV support * Syntax highlighting ; markdown & CSV support
* Search code in snippets ; browse users snippets, likes and forks * Search code in snippets ; browse users snippets, likes and forks
* Add topics to snippets
* Embed snippets in other websites * Embed snippets in other websites
* Revisions history * Revisions history
* Like / Fork snippets * Like / Fork snippets
@@ -31,7 +32,7 @@ Written in [Go](https://go.dev), Opengist aims to be fast and easy to deploy.
* delete users/gists; * delete users/gists;
* clean database/filesystem by syncing gists * clean database/filesystem by syncing gists
* run `git gc` for all repositories * run `git gc` for all repositories
* SQLite database * SQLite/PostgreSQL/MySQL database
* Logging * Logging
* Docker support * Docker support

View File

@@ -27,9 +27,9 @@ Stop the running instance; then like your first installation of Opengist, downlo
```shell ```shell
# example for linux amd64 # example for linux amd64
wget https://github.com/thomiceli/opengist/releases/download/v1.7.5/opengist1.7.5-linux-amd64.tar.gz wget https://github.com/thomiceli/opengist/releases/download/v1.11.0/opengist1.11.0-linux-amd64.tar.gz
tar xzvf opengist1.7.5-linux-amd64.tar.gz tar xzvf opengist1.11.0-linux-amd64.tar.gz
cd opengist cd opengist
chmod +x opengist chmod +x opengist
./opengist # with or without `--config config.yml` ./opengist # with or without `--config config.yml`

View File

@@ -17,6 +17,12 @@ git push -o title=Gist123
git push -o title="My Gist 123" git push -o title="My Gist 123"
``` ```
## Change description
```shell
git push -o description="This is my gist description"
```
## Change visibility ## Change visibility
```shell ```shell

View File

@@ -1,6 +1,6 @@
# Init Gists via Git # Init Gists via Git
Opengist allows you to create new snippets via Git over HTTP. Opengist allows you to create new snippets via Git over HTTP. You can create gists with either auto-generated URLs or custom URLs of your choice.
Simply init a new Git repository where your file(s) is/are located: Simply init a new Git repository where your file(s) is/are located:
@@ -10,19 +10,41 @@ git add .
git commit -m "My cool snippet" git commit -m "My cool snippet"
``` ```
Then add this Opengist special remote URL and push your changes: ### Option A: Regular URL
Create a gist with a custom URL using the format `http://opengist.url/username/custom-url`, where `username` is your authenticated username and `custom-url` is your desired gist identifier.
The gist must not exist yet if you want to create it, otherwise you will just push to the existing gist.
```shell ```shell
git remote add origin http://localhost:6157/init git remote add origin http://opengist.url/thomas/my-custom-gist
git push -u origin master git push -u origin master
``` ```
Log in with your Opengist account credentials, and your snippet will be created at the specified URL: **Requirements for custom URLs:**
- The username must match your authenticated username
- URL format: `http://opengist.url/username/custom-url`
- The custom URL becomes your gist's identifier and title
- `.git` suffix is automatically removed if present
### Option B: Init endpoint
Use the special `http://opengist.url/init` endpoint to create a gist with an automatically generated URL:
```shell ```shell
Username for 'http://localhost:6157': thomas git remote add origin http://opengist.url/init
Password for 'http://thomas@localhost:6157':
git push -u origin master
```
## Authentication
When you push, you'll be prompted to authenticate:
```shell
Username for 'http://opengist.url': thomas
Password for 'http://thomas@opengist.url': [your-password]
Enumerating objects: 3, done. Enumerating objects: 3, done.
Counting objects: 100% (3/3), done. Counting objects: 100% (3/3), done.
Delta compression using up to 8 threads Delta compression using up to 8 threads
@@ -30,12 +52,12 @@ Compressing objects: 100% (2/2), done.
Writing objects: 100% (3/3), 416 bytes | 416.00 KiB/s, done. Writing objects: 100% (3/3), 416 bytes | 416.00 KiB/s, done.
Total 3 (delta 0), reused 0 (delta 0), pack-reused 0 Total 3 (delta 0), reused 0 (delta 0), pack-reused 0
remote: remote:
remote: Your new repository has been created here: http://localhost:6157/thomas/6051e930f140429f9a2f3bb1fa101066 remote: Your new repository has been created here: http://opengist.url/thomas/6051e930f140429f9a2f3bb1fa101066
remote: remote:
remote: If you want to keep working with your gist, you could set the remote URL via: remote: If you want to keep working with your gist, you could set the remote URL via:
remote: git remote set-url origin http://localhost:6157/thomas/6051e930f140429f9a2f3bb1fa101066 remote: git remote set-url origin http://opengist.url/thomas/6051e930f140429f9a2f3bb1fa101066
remote: remote:
To http://localhost:6157/init To http://opengist.url/init
* [new branch] master -> master * [new branch] master -> master
``` ```

132
go.mod
View File

@@ -1,95 +1,129 @@
module github.com/thomiceli/opengist module github.com/thomiceli/opengist
go 1.22 go 1.23.0
require ( require (
github.com/Kunde21/markdownfmt/v3 v3.1.0 github.com/Kunde21/markdownfmt/v3 v3.1.0
github.com/alecthomas/chroma/v2 v2.14.0 github.com/alecthomas/chroma/v2 v2.20.0
github.com/blevesearch/bleve/v2 v2.4.0 github.com/blevesearch/bleve/v2 v2.5.0
github.com/dustin/go-humanize v1.0.1 github.com/dustin/go-humanize v1.0.1
github.com/glebarez/go-sqlite v1.22.0 github.com/gabriel-vasile/mimetype v1.4.8
github.com/glebarez/sqlite v1.11.0 github.com/glebarez/sqlite v1.11.0
github.com/go-playground/validator/v10 v10.21.0 github.com/go-ldap/ldap/v3 v3.4.8
github.com/go-playground/validator/v10 v10.26.0
github.com/go-webauthn/webauthn v0.12.3
github.com/google/uuid v1.6.0 github.com/google/uuid v1.6.0
github.com/gorilla/schema v1.4.1
github.com/gorilla/securecookie v1.1.2 github.com/gorilla/securecookie v1.1.2
github.com/gorilla/sessions v1.2.2 github.com/gorilla/sessions v1.4.0
github.com/hashicorp/go-memdb v1.3.4 github.com/labstack/echo-contrib v0.17.3
github.com/labstack/echo/v4 v4.12.0 github.com/labstack/echo/v4 v4.13.3
github.com/markbates/goth v1.80.0 github.com/markbates/goth v1.81.0
github.com/rs/zerolog v1.33.0 github.com/meilisearch/meilisearch-go v0.31.0
github.com/stretchr/testify v1.9.0 github.com/pquerna/otp v1.4.0
github.com/urfave/cli/v2 v2.27.2 github.com/prometheus/client_golang v1.21.1
github.com/yuin/goldmark v1.7.1 github.com/rs/zerolog v1.34.0
github.com/yuin/goldmark-emoji v1.0.2 github.com/stretchr/testify v1.10.0
github.com/urfave/cli/v2 v2.27.6
github.com/yuin/goldmark v1.7.8
github.com/yuin/goldmark-emoji v1.0.5
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc
go.abhg.dev/goldmark/mermaid v0.5.0 go.abhg.dev/goldmark/mermaid v0.5.0
golang.org/x/crypto v0.23.0 golang.org/x/crypto v0.36.0
golang.org/x/text v0.15.0 golang.org/x/text v0.23.0
gopkg.in/yaml.v3 v3.0.1 gopkg.in/yaml.v3 v3.0.1
gorm.io/gorm v1.25.10 gorm.io/driver/mysql v1.5.7
gorm.io/driver/postgres v1.5.11
gorm.io/gorm v1.25.12
) )
require ( require (
github.com/RoaringBitmap/roaring v1.9.4 // indirect filippo.io/edwards25519 v1.1.0 // indirect
github.com/bits-and-blooms/bitset v1.13.0 // indirect github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 // indirect
github.com/blevesearch/bleve_index_api v1.1.8 // indirect github.com/RoaringBitmap/roaring/v2 v2.4.5 // indirect
github.com/andybalholm/brotli v1.1.1 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/bits-and-blooms/bitset v1.22.0 // indirect
github.com/blevesearch/bleve_index_api v1.2.7 // indirect
github.com/blevesearch/geo v0.1.20 // indirect github.com/blevesearch/geo v0.1.20 // indirect
github.com/blevesearch/go-faiss v1.0.16 // indirect github.com/blevesearch/go-faiss v1.0.25 // indirect
github.com/blevesearch/go-porterstemmer v1.0.3 // indirect github.com/blevesearch/go-porterstemmer v1.0.3 // indirect
github.com/blevesearch/gtreap v0.1.1 // indirect github.com/blevesearch/gtreap v0.1.1 // indirect
github.com/blevesearch/mmap-go v1.0.4 // indirect github.com/blevesearch/mmap-go v1.0.4 // indirect
github.com/blevesearch/scorch_segment_api/v2 v2.2.13 // indirect github.com/blevesearch/scorch_segment_api/v2 v2.3.9 // indirect
github.com/blevesearch/segment v0.9.1 // indirect github.com/blevesearch/segment v0.9.1 // indirect
github.com/blevesearch/snowballstem v0.9.0 // indirect github.com/blevesearch/snowballstem v0.9.0 // indirect
github.com/blevesearch/upsidedown_store_api v1.0.2 // indirect github.com/blevesearch/upsidedown_store_api v1.0.2 // indirect
github.com/blevesearch/vellum v1.0.10 // indirect github.com/blevesearch/vellum v1.1.0 // indirect
github.com/blevesearch/zapx/v11 v11.3.10 // indirect github.com/blevesearch/zapx/v11 v11.4.1 // indirect
github.com/blevesearch/zapx/v12 v12.3.10 // indirect github.com/blevesearch/zapx/v12 v12.4.1 // indirect
github.com/blevesearch/zapx/v13 v13.3.10 // indirect github.com/blevesearch/zapx/v13 v13.4.1 // indirect
github.com/blevesearch/zapx/v14 v14.3.10 // indirect github.com/blevesearch/zapx/v14 v14.4.1 // indirect
github.com/blevesearch/zapx/v15 v15.3.13 // indirect github.com/blevesearch/zapx/v15 v15.4.1 // indirect
github.com/blevesearch/zapx/v16 v16.1.0 // indirect github.com/blevesearch/zapx/v16 v16.2.2 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.4 // indirect github.com/boombuler/barcode v1.0.2 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.6 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect
github.com/dlclark/regexp2 v1.11.0 // indirect github.com/dlclark/regexp2 v1.11.5 // indirect
github.com/gabriel-vasile/mimetype v1.4.4 // indirect github.com/fxamacker/cbor/v2 v2.8.0 // indirect
github.com/glebarez/go-sqlite v1.22.0 // indirect
github.com/go-asn1-ber/asn1-ber v1.5.5 // indirect
github.com/go-chi/chi/v5 v5.2.1 // indirect
github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/golang-jwt/jwt v3.2.2+incompatible // indirect github.com/go-sql-driver/mysql v1.9.1 // indirect
github.com/golang/geo v0.0.0-20230421003525-6adc56603217 // indirect github.com/go-webauthn/x v0.1.20 // indirect
github.com/golang-jwt/jwt/v4 v4.5.2 // indirect
github.com/golang-jwt/jwt/v5 v5.2.2 // indirect
github.com/golang/geo v0.0.0-20250404181303-07d601f131f3 // indirect
github.com/golang/protobuf v1.5.4 // indirect github.com/golang/protobuf v1.5.4 // indirect
github.com/golang/snappy v0.0.4 // indirect github.com/golang/snappy v1.0.0 // indirect
github.com/google/go-tpm v0.9.3 // indirect
github.com/gorilla/mux v1.8.1 // indirect github.com/gorilla/mux v1.8.1 // indirect
github.com/hashicorp/go-immutable-radix v1.3.1 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/hashicorp/golang-lru v1.0.2 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
github.com/jackc/pgx/v5 v5.7.4 // indirect
github.com/jackc/puddle/v2 v2.2.2 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect github.com/jinzhu/now v1.1.5 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/compress v1.18.0 // indirect
github.com/kr/text v0.2.0 // indirect github.com/kr/text v0.2.0 // indirect
github.com/labstack/gommon v0.4.2 // indirect github.com/labstack/gommon v0.4.2 // indirect
github.com/leodido/go-urn v1.4.0 // indirect github.com/leodido/go-urn v1.4.0 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect github.com/mailru/easyjson v0.9.0 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.15 // indirect github.com/mattn/go-runewidth v0.0.16 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/mschoch/smat v0.2.0 // indirect github.com/mschoch/smat v0.2.0 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/ncruces/go-strftime v0.1.9 // indirect github.com/ncruces/go-strftime v0.1.9 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/prometheus/client_model v0.6.1 // indirect
github.com/prometheus/common v0.63.0 // indirect
github.com/prometheus/procfs v0.16.0 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/rivo/uniseg v0.4.7 // indirect github.com/rivo/uniseg v0.4.7 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasttemplate v1.2.2 // indirect github.com/valyala/fasttemplate v1.2.2 // indirect
github.com/x448/float16 v0.8.4 // indirect
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect
go.etcd.io/bbolt v1.3.10 // indirect go.etcd.io/bbolt v1.4.0 // indirect
golang.org/x/net v0.25.0 // indirect golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 // indirect
golang.org/x/oauth2 v0.20.0 // indirect golang.org/x/net v0.38.0 // indirect
golang.org/x/sys v0.20.0 // indirect golang.org/x/oauth2 v0.29.0 // indirect
golang.org/x/time v0.5.0 // indirect golang.org/x/sync v0.13.0 // indirect
google.golang.org/protobuf v1.34.1 // indirect golang.org/x/sys v0.32.0 // indirect
modernc.org/libc v1.51.0 // indirect golang.org/x/time v0.11.0 // indirect
modernc.org/mathutil v1.6.0 // indirect google.golang.org/protobuf v1.36.6 // indirect
modernc.org/memory v1.8.0 // indirect modernc.org/libc v1.62.1 // indirect
modernc.org/sqlite v1.30.0 // indirect modernc.org/mathutil v1.7.1 // indirect
modernc.org/memory v1.9.1 // indirect
modernc.org/sqlite v1.37.0 // indirect
) )

397
go.sum
View File

@@ -1,54 +1,69 @@
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 h1:mFRzDkZVAjdal+s7s0MwaRv9igoPqLRdzOLzw/8Xvq8=
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU=
github.com/Kunde21/markdownfmt/v3 v3.1.0 h1:KiZu9LKs+wFFBQKhrZJrFZwtLnCCWJahL+S+E/3VnM0= 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/Kunde21/markdownfmt/v3 v3.1.0/go.mod h1:tPXN1RTyOzJwhfHoon9wUr4HGYmWgVxSQN6VBJDkrVc=
github.com/RoaringBitmap/roaring v1.9.4 h1:yhEIoH4YezLYT04s1nHehNO64EKFTop/wBhxv2QzDdQ= github.com/RoaringBitmap/roaring/v2 v2.4.5 h1:uGrrMreGjvAtTBobc0g5IrW1D5ldxDQYe2JW2gggRdg=
github.com/RoaringBitmap/roaring v1.9.4/go.mod h1:6AXUsoIEzDTFFQCe1RbGA6uFONMhvejWj5rqITANK90= github.com/RoaringBitmap/roaring/v2 v2.4.5/go.mod h1:FiJcsfkGje/nZBZgCu0ZxCPOKD/hVXDS2dXi7/eUFE0=
github.com/alecthomas/assert/v2 v2.7.0 h1:QtqSACNS3tF7oasA8CU6A6sXZSBDqnm7RfpLl9bZqbE= github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0=
github.com/alecthomas/assert/v2 v2.7.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k= 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.2.0/go.mod h1:vf4zrexSH54oEjJ7EdB65tGNHmH3pGZmVkgTP5RHvAs=
github.com/alecthomas/chroma/v2 v2.14.0 h1:R3+wzpnUArGcQz7fCETQBzO5n9IMNi13iIs46aU4V9E= github.com/alecthomas/chroma/v2 v2.20.0 h1:sfIHpxPyR07/Oylvmcai3X/exDlE8+FA820NTz+9sGw=
github.com/alecthomas/chroma/v2 v2.14.0/go.mod h1:QolEbTfmUHIMVpBqxeDnNBj2uoeI4EbYP4i6n68SG4I= github.com/alecthomas/chroma/v2 v2.20.0/go.mod h1:e7tViK0xh/Nf4BYHl00ycY6rV7b8iXBksI9E359yNmA=
github.com/alecthomas/repr v0.0.0-20220113201626-b1b626ac65ae/go.mod h1:2kn6fqh/zIyPLmm3ugklbEi5hg5wS435eygvNfaDQL8= github.com/alecthomas/repr v0.0.0-20220113201626-b1b626ac65ae/go.mod h1:2kn6fqh/zIyPLmm3ugklbEi5hg5wS435eygvNfaDQL8=
github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc= github.com/alecthomas/repr v0.5.1 h1:E3G4t2QbHTSNpPKBgMTln5KLkZHLOcU7r37J4pXBuIg=
github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= github.com/alecthomas/repr v0.5.1/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa h1:LHTHcTQiSGT7VVbI0o4wBRNQIgn917usHWOd6VAffYI=
github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4=
github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA=
github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA=
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.12.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= github.com/bits-and-blooms/bitset v1.12.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
github.com/bits-and-blooms/bitset v1.13.0 h1:bAQ9OPNFYbGHV6Nez0tmNI0RiEu7/hxlYJRUA0wFAVE= github.com/bits-and-blooms/bitset v1.22.0 h1:Tquv9S8+SGaS3EhyA+up3FXzmkhxPGjQQCkcs2uw7w4=
github.com/bits-and-blooms/bitset v1.13.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= github.com/bits-and-blooms/bitset v1.22.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
github.com/blevesearch/bleve/v2 v2.4.0 h1:2xyg+Wv60CFHYccXc+moGxbL+8QKT/dZK09AewHgKsg= github.com/blevesearch/bleve/v2 v2.5.0 h1:HzYqBy/5/M9Ul9ESEmXzN/3Jl7YpmWBdHM/+zzv/3k4=
github.com/blevesearch/bleve/v2 v2.4.0/go.mod h1:IhQHoFAbHgWKYavb9rQgQEJJVMuY99cKdQ0wPpst2aY= github.com/blevesearch/bleve/v2 v2.5.0/go.mod h1:PcJzTPnEynO15dCf9isxOga7YFRa/cMSsbnRwnszXUk=
github.com/blevesearch/bleve_index_api v1.1.8 h1:rJUccYfWqRY2/BGowlsv1lwrLKYK/zPE6hgNn1pTGdk= github.com/blevesearch/bleve_index_api v1.2.7 h1:c8r9vmbaYQroAMSGag7zq5gEVPiuXrUQDqfnj7uYZSY=
github.com/blevesearch/bleve_index_api v1.1.8/go.mod h1:PbcwjIcRmjhGbkS/lJCpfgVSMROV6TRubGGAODaK1W8= github.com/blevesearch/bleve_index_api v1.2.7/go.mod h1:rKQDl4u51uwafZxFrPD1R7xFOwKnzZW7s/LSeK4lgo0=
github.com/blevesearch/geo v0.1.20 h1:paaSpu2Ewh/tn5DKn/FB5SzvH0EWupxHEIwbCk/QPqM= github.com/blevesearch/geo v0.1.20 h1:paaSpu2Ewh/tn5DKn/FB5SzvH0EWupxHEIwbCk/QPqM=
github.com/blevesearch/geo v0.1.20/go.mod h1:DVG2QjwHNMFmjo+ZgzrIq2sfCh6rIHzy9d9d0B59I6w= github.com/blevesearch/geo v0.1.20/go.mod h1:DVG2QjwHNMFmjo+ZgzrIq2sfCh6rIHzy9d9d0B59I6w=
github.com/blevesearch/go-faiss v1.0.16 h1:lfzXzzjO1mAf15MRiRY5yz6KVGr02CyRrr7m0z70Ih8= github.com/blevesearch/go-faiss v1.0.25 h1:lel1rkOUGbT1CJ0YgzKwC7k+XH0XVBHnCVWahdCXk4U=
github.com/blevesearch/go-faiss v1.0.16/go.mod h1:jrxHrbl42X/RnDPI+wBoZU8joxxuRwedrxqswQ3xfU8= github.com/blevesearch/go-faiss v1.0.25/go.mod h1:OMGQwOaRRYxrmeNdMrXJPvVx8gBnvE5RYrr0BahNnkk=
github.com/blevesearch/go-porterstemmer v1.0.3 h1:GtmsqID0aZdCSNiY8SkuPJ12pD4jI+DdXTAn4YRcHCo= 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/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 h1:2JWigFrzDMR+42WGIN/V2p0cUvn4UP3C4Q5nmaZGW8Y=
github.com/blevesearch/gtreap v0.1.1/go.mod h1:QaQyDRAT51sotthUWAH4Sj08awFSSWzgYICSZ3w0tYk= github.com/blevesearch/gtreap v0.1.1/go.mod h1:QaQyDRAT51sotthUWAH4Sj08awFSSWzgYICSZ3w0tYk=
github.com/blevesearch/mmap-go v1.0.4 h1:OVhDhT5B/M1HNPpYPBKIEJaD0F3Si+CrEKULGCDPWmc= github.com/blevesearch/mmap-go v1.0.4 h1:OVhDhT5B/M1HNPpYPBKIEJaD0F3Si+CrEKULGCDPWmc=
github.com/blevesearch/mmap-go v1.0.4/go.mod h1:EWmEAOmdAS9z/pi/+Toxu99DnsbhG1TIxUoRmJw/pSs= github.com/blevesearch/mmap-go v1.0.4/go.mod h1:EWmEAOmdAS9z/pi/+Toxu99DnsbhG1TIxUoRmJw/pSs=
github.com/blevesearch/scorch_segment_api/v2 v2.2.13 h1:UfbyRpIMdcaNsgciGYS9Pib7N3xd3EEw8KKbd/aDBlA= github.com/blevesearch/scorch_segment_api/v2 v2.3.9 h1:X6nJXnNHl7nasXW+U6y2Ns2Aw8F9STszkYkyBfQ+p0o=
github.com/blevesearch/scorch_segment_api/v2 v2.2.13/go.mod h1:osG1bAUONZB2r/ozUJwjbuOzPvdrULWaLOm+vsMANsk= github.com/blevesearch/scorch_segment_api/v2 v2.3.9/go.mod h1:IrzspZlVjhf4X29oJiEhBxEteTqOY9RlYlk1lCmYHr4=
github.com/blevesearch/segment v0.9.1 h1:+dThDy+Lvgj5JMxhmOVlgFfkUtZV2kw49xax4+jTfSU= 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/segment v0.9.1/go.mod h1:zN21iLm7+GnBHWTao9I+Au/7MBiL8pPFtJBJTsk6kQw=
github.com/blevesearch/snowballstem v0.9.0 h1:lMQ189YspGP6sXvZQ4WZ+MLawfV8wOmPoD/iWeNXm8s= github.com/blevesearch/snowballstem v0.9.0 h1:lMQ189YspGP6sXvZQ4WZ+MLawfV8wOmPoD/iWeNXm8s=
github.com/blevesearch/snowballstem v0.9.0/go.mod h1:PivSj3JMc8WuaFkTSRDW2SlrulNWPl4ABg1tC/hlgLs= github.com/blevesearch/snowballstem v0.9.0/go.mod h1:PivSj3JMc8WuaFkTSRDW2SlrulNWPl4ABg1tC/hlgLs=
github.com/blevesearch/upsidedown_store_api v1.0.2 h1:U53Q6YoWEARVLd1OYNc9kvhBMGZzVrdmaozG2MfoB+A= github.com/blevesearch/upsidedown_store_api v1.0.2 h1:U53Q6YoWEARVLd1OYNc9kvhBMGZzVrdmaozG2MfoB+A=
github.com/blevesearch/upsidedown_store_api v1.0.2/go.mod h1:M01mh3Gpfy56Ps/UXHjEO/knbqyQ1Oamg8If49gRwrQ= github.com/blevesearch/upsidedown_store_api v1.0.2/go.mod h1:M01mh3Gpfy56Ps/UXHjEO/knbqyQ1Oamg8If49gRwrQ=
github.com/blevesearch/vellum v1.0.10 h1:HGPJDT2bTva12hrHepVT3rOyIKFFF4t7Gf6yMxyMIPI= github.com/blevesearch/vellum v1.1.0 h1:CinkGyIsgVlYf8Y2LUQHvdelgXr6PYuvoDIajq6yR9w=
github.com/blevesearch/vellum v1.0.10/go.mod h1:ul1oT0FhSMDIExNjIxHqJoGpVrBpKCdgDQNxfqgJt7k= github.com/blevesearch/vellum v1.1.0/go.mod h1:QgwWryE8ThtNPxtgWJof5ndPfx0/YMBh+W2weHKPw8Y=
github.com/blevesearch/zapx/v11 v11.3.10 h1:hvjgj9tZ9DeIqBCxKhi70TtSZYMdcFn7gDb71Xo/fvk= github.com/blevesearch/zapx/v11 v11.4.1 h1:qFCPlFbsEdwbbckJkysptSQOsHn4s6ZOHL5GMAIAVHA=
github.com/blevesearch/zapx/v11 v11.3.10/go.mod h1:0+gW+FaE48fNxoVtMY5ugtNHHof/PxCqh7CnhYdnMzQ= github.com/blevesearch/zapx/v11 v11.4.1/go.mod h1:qNOGxIqdPC1MXauJCD9HBG487PxviTUUbmChFOAosGs=
github.com/blevesearch/zapx/v12 v12.3.10 h1:yHfj3vXLSYmmsBleJFROXuO08mS3L1qDCdDK81jDl8s= github.com/blevesearch/zapx/v12 v12.4.1 h1:K77bhypII60a4v8mwvav7r4IxWA8qxhNjgF9xGdb9eQ=
github.com/blevesearch/zapx/v12 v12.3.10/go.mod h1:0yeZg6JhaGxITlsS5co73aqPtM04+ycnI6D1v0mhbCs= github.com/blevesearch/zapx/v12 v12.4.1/go.mod h1:QRPrlPOzAxBNMI0MkgdD+xsTqx65zbuPr3Ko4Re49II=
github.com/blevesearch/zapx/v13 v13.3.10 h1:0KY9tuxg06rXxOZHg3DwPJBjniSlqEgVpxIqMGahDE8= github.com/blevesearch/zapx/v13 v13.4.1 h1:EnkEMZFUK0lsW/jOJJF2xOcp+W8TjEsyeN5BeAZEYYE=
github.com/blevesearch/zapx/v13 v13.3.10/go.mod h1:w2wjSDQ/WBVeEIvP0fvMJZAzDwqwIEzVPnCPrz93yAk= github.com/blevesearch/zapx/v13 v13.4.1/go.mod h1:e6duBMlCvgbH9rkzNMnUa9hRI9F7ri2BRcHfphcmGn8=
github.com/blevesearch/zapx/v14 v14.3.10 h1:SG6xlsL+W6YjhX5N3aEiL/2tcWh3DO75Bnz77pSwwKU= github.com/blevesearch/zapx/v14 v14.4.1 h1:G47kGCshknBZzZAtjcnIAMn3oNx8XBLxp8DMq18ogyE=
github.com/blevesearch/zapx/v14 v14.3.10/go.mod h1:qqyuR0u230jN1yMmE4FIAuCxmahRQEOehF78m6oTgns= github.com/blevesearch/zapx/v14 v14.4.1/go.mod h1:O7sDxiaL2r2PnCXbhh1Bvm7b4sP+jp4unE9DDPWGoms=
github.com/blevesearch/zapx/v15 v15.3.13 h1:6EkfaZiPlAxqXz0neniq35my6S48QI94W/wyhnpDHHQ= github.com/blevesearch/zapx/v15 v15.4.1 h1:B5IoTMUCEzFdc9FSQbhVOxAY+BO17c05866fNruiI7g=
github.com/blevesearch/zapx/v15 v15.3.13/go.mod h1:Turk/TNRKj9es7ZpKK95PS7f6D44Y7fAFy8F4LXQtGg= github.com/blevesearch/zapx/v15 v15.4.1/go.mod h1:b/MreHjYeQoLjyY2+UaM0hGZZUajEbE0xhnr1A2/Q6Y=
github.com/blevesearch/zapx/v16 v16.1.0 h1:bHsyowFqU0QA+uVDJCjifv9OvPGb8htkV52Yc/wT6xs= github.com/blevesearch/zapx/v16 v16.2.2 h1:MifKJVRTEhMTgSlle2bDRTb39BGc9jXFRLPZc6r0Rzk=
github.com/blevesearch/zapx/v16 v16.1.0/go.mod h1:P0h9lKRyl4EKksAWfxwCQ5I5pLB9jH2XD8bhYHuIYuc= github.com/blevesearch/zapx/v16 v16.2.2/go.mod h1:B9Pk4G1CqtErgQV9DyCSA9Lb7WZe4olYfGw7fVDZ4sk=
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
github.com/boombuler/barcode v1.0.2 h1:79yrbttoZrLGkL/oOI8hBrUKucwOL0oOjUgEguGMcJ4=
github.com/boombuler/barcode v1.0.2/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/chromedp/cdproto v0.0.0-20230220211738-2b1ec77315c9 h1:wMSvdj3BswqfQOXp2R1bJOAE7xIQLt2dlMQDMf836VY= github.com/chromedp/cdproto v0.0.0-20230220211738-2b1ec77315c9 h1:wMSvdj3BswqfQOXp2R1bJOAE7xIQLt2dlMQDMf836VY=
github.com/chromedp/cdproto v0.0.0-20230220211738-2b1ec77315c9/go.mod h1:GKljq0VrfU4D5yc+2qA6OVr8pmO/MBbPEWqWQ/oqGEs= github.com/chromedp/cdproto v0.0.0-20230220211738-2b1ec77315c9/go.mod h1:GKljq0VrfU4D5yc+2qA6OVr8pmO/MBbPEWqWQ/oqGEs=
github.com/chromedp/chromedp v0.9.1 h1:CC7cC5p1BeLiiS2gfNNPwp3OaUxtRMBjfiw3E3k6dFA= github.com/chromedp/chromedp v0.9.1 h1:CC7cC5p1BeLiiS2gfNNPwp3OaUxtRMBjfiw3E3k6dFA=
@@ -56,32 +71,47 @@ github.com/chromedp/chromedp v0.9.1/go.mod h1:DUgZWRvYoEfgi66CgZ/9Yv+psgi+Sksy5D
github.com/chromedp/sysutil v1.0.0 h1:+ZxhTpfpZlmchB58ih/LBHX52ky7w2VhQVKQMucy3Ic= github.com/chromedp/sysutil v1.0.0 h1:+ZxhTpfpZlmchB58ih/LBHX52ky7w2VhQVKQMucy3Ic=
github.com/chromedp/sysutil v1.0.0/go.mod h1:kgWmDdq8fTzXYcKIBqIYvRRTnYb9aNS9moAV0xufSww= github.com/chromedp/sysutil v1.0.0/go.mod h1:kgWmDdq8fTzXYcKIBqIYvRRTnYb9aNS9moAV0xufSww=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/cpuguy83/go-md2man/v2 v2.0.4 h1:wfIWP927BUkWJb2NmU/kNDYIBTh/ziUX91+lVfRxZq4= github.com/cpuguy83/go-md2man/v2 v2.0.6 h1:XJtiaUW6dEEqVuZiMTn1ldk455QWwEIsMIJlo5vtkx0=
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 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/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.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.7.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI= github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ=
github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= 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/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/gabriel-vasile/mimetype v1.4.4 h1:QjV6pZ7/XZ7ryI2KuyeEDE8wnh7fHP9YnQy+R0LnH8I= github.com/fxamacker/cbor/v2 v2.8.0 h1:fFtUGXUzXPHTIUdne5+zzMPTfffl3RD5qYnkY40vtxU=
github.com/gabriel-vasile/mimetype v1.4.4/go.mod h1:JwLei5XPtWdGiMFB5Pjle1oEeoSeEuJfJE+TtfvdB/s= github.com/fxamacker/cbor/v2 v2.8.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM=
github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8=
github.com/glebarez/go-sqlite v1.22.0 h1:uAcMJhaA6r3LHMTFgP0SifzgXg46yJkgxqyuyec+ruQ= 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/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 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GMw=
github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ= github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ=
github.com/go-asn1-ber/asn1-ber v1.5.5 h1:MNHlNMBDgEKD4TcKr36vQN68BA00aDfjIt3/bD50WnA=
github.com/go-asn1-ber/asn1-ber v1.5.5/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
github.com/go-chi/chi/v5 v5.2.1 h1:KOIHODQj58PmL80G2Eak4WdvUzjSJSm0vG72crDCqb8=
github.com/go-chi/chi/v5 v5.2.1/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=
github.com/go-ldap/ldap/v3 v3.4.8 h1:loKJyspcRezt2Q3ZRMq2p/0v8iOurlmeXDPw6fikSvQ=
github.com/go-ldap/ldap/v3 v3.4.8/go.mod h1:qS3Sjlu76eHfHGpUdWkAXQTw4beih+cHsco2jXlIXrk=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= 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/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 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= 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 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.21.0 h1:4fZA11ovvtkdgaeev9RGWPgc1uj3H8W+rNYyH/ySBb0= github.com/go-playground/validator/v10 v10.26.0 h1:SP05Nqhjcvz81uJaRfEV0YBSSSGMc/iMaVtFbr3Sw2k=
github.com/go-playground/validator/v10 v10.21.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= github.com/go-playground/validator/v10 v10.26.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
github.com/go-sql-driver/mysql v1.9.1 h1:FrjNGn/BsJQjVRuSa8CBrM5BWA9BWoXXat3KrtSb/iI=
github.com/go-sql-driver/mysql v1.9.1/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
github.com/go-webauthn/webauthn v0.12.3 h1:hHQl1xkUuabUU9uS+ISNCMLs9z50p9mDUZI/FmkayNE=
github.com/go-webauthn/webauthn v0.12.3/go.mod h1:4JRe8Z3W7HIw8NGEWn2fnUwecoDzkkeach/NnvhkqGY=
github.com/go-webauthn/x v0.1.20 h1:brEBDqfiPtNNCdS/peu8gARtq8fIPsHz0VzpPjGvgiw=
github.com/go-webauthn/x v0.1.20/go.mod h1:n/gAc8ssZJGATM0qThE+W+vfgXiMedsWi3wf/C4lld0=
github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU= 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/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 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og=
@@ -89,42 +119,63 @@ github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6Wezm
github.com/gobwas/ws v1.1.0 h1:7RFti/xnNkMJnrK7D1yQ/iCIB5OrrY/54/H930kIbHA= github.com/gobwas/ws v1.1.0 h1:7RFti/xnNkMJnrK7D1yQ/iCIB5OrrY/54/H930kIbHA=
github.com/gobwas/ws v1.1.0/go.mod h1:nzvNcVha5eUziGrbxFCo6qFIojQHjJV5cLYIbezhfL0= github.com/gobwas/ws v1.1.0/go.mod h1:nzvNcVha5eUziGrbxFCo6qFIojQHjJV5cLYIbezhfL0=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= github.com/golang-jwt/jwt/v4 v4.5.1/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI=
github.com/golang/geo v0.0.0-20230421003525-6adc56603217 h1:HKlyj6in2JV6wVkmQ4XmG/EIm+SCYlPZ+V4GWit7Z+I= github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
github.com/golang/geo v0.0.0-20230421003525-6adc56603217/go.mod h1:8wI0hitZ3a1IxZfeH3/5I97CI8i5cLGsYe7xNhQGs9U= github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8=
github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/golang/geo v0.0.0-20250404181303-07d601f131f3 h1:8COTSTFIIXnaD81+kfCw4dRANNAKuCp06EdYLqwX30g=
github.com/golang/geo v0.0.0-20250404181303-07d601f131f3/go.mod h1:J+F9/3Ofc8ysEOY2/cNjxTMl2eB1gvPIywEHUplPgDA=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs=
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/go-tpm v0.9.3 h1:+yx0/anQuGzi+ssRqeD6WpXjW2L/V0dItUayO0i9sRc=
github.com/google/go-tpm v0.9.3/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 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 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd h1:gbpYu9NMq8jhDVbvlGkMFWCjLFlqqEZjEmObmhUy6Vo= github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs=
github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw= github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
github.com/gorilla/schema v1.4.1 h1:jUg5hUjCSDZpNGLuXQOgIWGdlgrIdYvgQ0wZtdK1M3E=
github.com/gorilla/schema v1.4.1/go.mod h1:Dg5SSm5PV60mhF2NFaTV1xuYYj8tV8NOPRo4FggUMnM=
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA= github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA=
github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo= github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo=
github.com/gorilla/sessions v1.2.2 h1:lqzMYz6bOfvn2WriPUjNByzeXIlVzURcPmgMczkmTjY= github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
github.com/gorilla/sessions v1.2.2/go.mod h1:ePLdVu+jbEgHH+KWw8I1z2wqd0BAdAQh/8LRvBeoNcQ= github.com/gorilla/sessions v1.4.0 h1:kpIYOp/oi6MG/p5PgxApU8srsSw9tuFbt46Lt7auzqQ=
github.com/hashicorp/go-immutable-radix v1.3.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= github.com/gorilla/sessions v1.4.0/go.mod h1:FLWm50oby91+hl7p/wRxDth9bWSuk0qVL2emc7lT5ik=
github.com/hashicorp/go-immutable-radix v1.3.1 h1:DKHmCUm2hRBK510BaiZlwvpD40f8bJFeZnpfm2KLowc= github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8=
github.com/hashicorp/go-memdb v1.3.4 h1:XSL3NR682X/cVk2IeV0d70N4DZ9ljI885xAEU8IoK3c= github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go-memdb v1.3.4/go.mod h1:uBTr1oQbtuMgd1SSGoR8YV27eT3sBHbYiNm53bMpgSg=
github.com/hashicorp/go-uuid v1.0.0 h1:RS8zrF7PhGwyNPOtxSClXXj9HA8feRnJzgnI1RJCSnM=
github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
github.com/hashicorp/golang-lru v1.0.2 h1:dV3g9Z/unq5DpblPpw+Oqcv4dU/1omnb4Ok8iPY6p1c=
github.com/hashicorp/golang-lru v1.0.2/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
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.7.4 h1:9wKznZrhWa2QiHL+NjTSPP6yjl3451BX3imWDnokYlg=
github.com/jackc/pgx/v5 v5.7.4/go.mod h1:ncY89UGWxg82EykZUwSpUKEfccBGGYq1xjrOpsbsfGQ=
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=
github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs=
github.com/jcmturner/dnsutils/v2 v2.0.0 h1:lltnkeZGL0wILNvrNiVCR6Ro5PGU/SeBvVO/8c/iPbo=
github.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM=
github.com/jcmturner/gofork v1.7.6 h1:QH0l3hzAU1tfT3rZCnW5zXl+orbkNMMRGJfdJjHVETg=
github.com/jcmturner/gofork v1.7.6/go.mod h1:1622LH6i/EZqLloHfE7IeZ0uEJwMSUyQ/nDd82IeqRo=
github.com/jcmturner/goidentity/v6 v6.0.1 h1:VKnZd2oEIMorCTsFBnJWbExfNN7yZr3EhJAxwOkZg6o=
github.com/jcmturner/goidentity/v6 v6.0.1/go.mod h1:X1YW3bgtvwAXju7V3LCIMpY0Gbxyjn/mY9zx4tFonSg=
github.com/jcmturner/gokrb5/v8 v8.4.4 h1:x1Sv4HaTpepFkXbt2IkL29DXRf8sOfZXo8eRKh687T8=
github.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs=
github.com/jcmturner/rpc/v2 v2.0.3 h1:7FXXj8Ti1IaVFpSAziCZWNzbNuZmnvw/i6CqLNdWfZY=
github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
@@ -133,28 +184,39 @@ github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8Hm
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/labstack/echo/v4 v4.12.0 h1:IKpw49IMryVB2p1a4dzwlhP1O2Tf2E0Ir/450lH+kI0= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/labstack/echo/v4 v4.12.0/go.mod h1:UP9Cr2DJXbOK3Kr9ONYzNowSh7HP0aG0ShAyycHSJvM= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/labstack/echo-contrib v0.17.3 h1:hj+qXksKZG1scSe9ksUXMtv7fZYN+PtQT+bPcYA3/TY=
github.com/labstack/echo-contrib v0.17.3/go.mod h1:TcRBrzW8jcC4JD+5Dc/pvOyAps0rtgzj7oBqoR3nYsc=
github.com/labstack/echo/v4 v4.13.3 h1:pwhpCPrTl5qry5HRdM5FwdXnhXSLSY+WE+YQSeCaafY=
github.com/labstack/echo/v4 v4.13.3/go.mod h1:o90YNEeQWjDozo584l7AwhJMHN0bOC4tAfg+Xox9q5g=
github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0= 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/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= 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/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4=
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU=
github.com/markbates/goth v1.80.0 h1:NnvatczZDzOs1hn9Ug+dVYf2Viwwkp/ZDX5K+GLjan8= github.com/markbates/goth v1.81.0 h1:XVcCkeGWokynPV7MXvgb8pd2s3r7DS40P7931w6kdnE=
github.com/markbates/goth v1.80.0/go.mod h1:4/GYHo+W6NWisrMPZnq0Yr2Q70UntNLn7KXEFhrIdAY= github.com/markbates/goth v1.81.0/go.mod h1:+6z31QyUms84EHmuBY7iuqYSxyoN3njIgg9iCF/lR1k=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= 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.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.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 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/meilisearch/meilisearch-go v0.31.0 h1:yZRhY1qJqdH8h6GFZALGtkDLyj8f9v5aJpsNMyrUmnY=
github.com/meilisearch/meilisearch-go v0.31.0/go.mod h1:aNtyuwurDg/ggxQIcKqWH6G9g2ptc8GyY7PLY4zMn/g=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 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 h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
@@ -162,11 +224,23 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9G
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/mschoch/smat v0.2.0 h1:8imxQsjDm8yFEAVBe7azKmKSgzSkZXDuKkSq9374khM= github.com/mschoch/smat v0.2.0 h1:8imxQsjDm8yFEAVBe7azKmKSgzSkZXDuKkSq9374khM=
github.com/mschoch/smat v0.2.0/go.mod h1:kc9mz7DoBKqDyiRL7VZN8KvXQMWeTaVnttLRXOlotKw= github.com/mschoch/smat v0.2.0/go.mod h1:kc9mz7DoBKqDyiRL7VZN8KvXQMWeTaVnttLRXOlotKw=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 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/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pquerna/otp v1.4.0 h1:wZvl1TIVxKRThZIBiwOOHOGP/1+nZyWBil9Y2XNEDzg=
github.com/pquerna/otp v1.4.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg=
github.com/prometheus/client_golang v1.21.1 h1:DOvXXTqVzvkIewV/CDPFdejpMCGeMcbGCQ8YOmu+Ibk=
github.com/prometheus/client_golang v1.21.1/go.mod h1:U9NM32ykUErtVBxdvD3zfi+EuFkkaBvMb09mIfe0Zgg=
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
github.com/prometheus/common v0.63.0 h1:YR/EIY1o3mEFP/kZCD7iDMnLPlGyuU2Gb3HIcXnA98k=
github.com/prometheus/common v0.63.0/go.mod h1:VVFF/fBIoToEnWRVkYoXEkq3R3paCoxG9PXP74SnV18=
github.com/prometheus/procfs v0.16.0 h1:xh6oHhKwnOJKMYiYBDWmkHqQPyiY40sny36Cmx2bbsM=
github.com/prometheus/procfs v0.16.0/go.mod h1:8veyXUu3nGP7oaCxhX6yeaM5u4stL2FeMXnCqhDthZg=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= 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/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
@@ -174,91 +248,156 @@ github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= 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/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8= github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY=
github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= 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/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/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/urfave/cli/v2 v2.27.2 h1:6e0H+AkS+zDckwPCUrZkKX38mRaau4nL2uipkJpbkcI= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/urfave/cli/v2 v2.27.2/go.mod h1:g0+79LmHHATl7DAcHO99smiR/T7uGLw84w8Y42x+4eM= github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/urfave/cli/v2 v2.27.6 h1:VdRdS98FNhKZ8/Az8B7MTyGQmpIr36O1EHybx/LaZ4g=
github.com/urfave/cli/v2 v2.27.6/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5hrMvTQ=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo= github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo=
github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4= github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4=
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM= github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM=
github.com/yuin/goldmark v1.3.7/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= 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.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yuin/goldmark v1.4.15/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yuin/goldmark v1.4.15/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yuin/goldmark v1.7.1 h1:3bajkSilaCbjdKVsKdZjZCLBNPL9pYzrCakKaf4U49U=
github.com/yuin/goldmark v1.7.1/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E= github.com/yuin/goldmark v1.7.1/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
github.com/yuin/goldmark-emoji v1.0.2 h1:c/RgTShNgHTtc6xdz2KKI74jJr6rWi7FPgnP9GAsO5s= github.com/yuin/goldmark v1.7.8 h1:iERMLn0/QJeHFhxSt3p6PeN9mGnvIKSpG9YYorDMnic=
github.com/yuin/goldmark-emoji v1.0.2/go.mod h1:RhP/RWpexdp+KHs7ghKnifRoIs/Bq4nDS7tRbCkOwKY= github.com/yuin/goldmark v1.7.8/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
github.com/yuin/goldmark-emoji v1.0.5 h1:EMVWyCGPlXJfUXBXpuMu+ii3TIaxbVBnEX9uaDC4cIk=
github.com/yuin/goldmark-emoji v1.0.5/go.mod h1:tTkZEbwu5wkPmgTcitqddVxY9osFZiavD+r4AzQrh1U=
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc h1:+IAOyRda+RLrxa1WC7umKOZRsGq4QrFFMYApOeHzQwQ= github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc h1:+IAOyRda+RLrxa1WC7umKOZRsGq4QrFFMYApOeHzQwQ=
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc/go.mod h1:ovIvrum6DQJA4QsJSovrkC4saKHQVs7TvcaeO8AIl5I= github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc/go.mod h1:ovIvrum6DQJA4QsJSovrkC4saKHQVs7TvcaeO8AIl5I=
go.abhg.dev/goldmark/mermaid v0.5.0 h1:mDkykpSPJ+5wCQ8bSXgzJ2KQskjXkI5Ndxz7JYDHW38= go.abhg.dev/goldmark/mermaid v0.5.0 h1:mDkykpSPJ+5wCQ8bSXgzJ2KQskjXkI5Ndxz7JYDHW38=
go.abhg.dev/goldmark/mermaid v0.5.0/go.mod h1:OCyk2o85TX2drWHH+HRy6bih2yZlUwbbv/R1MMh1YLs= go.abhg.dev/goldmark/mermaid v0.5.0/go.mod h1:OCyk2o85TX2drWHH+HRy6bih2yZlUwbbv/R1MMh1YLs=
go.etcd.io/bbolt v1.3.10 h1:+BqfJTcCzTItrop8mq/lbzL8wSGtj94UO/3U31shqG0= go.etcd.io/bbolt v1.4.0 h1:TU77id3TnN/zKr7CO/uk+fBCwF2jGcMuw2B/FMAzYIk=
go.etcd.io/bbolt v1.3.10/go.mod h1:bK3UQLPJZly7IlNmV7uVHJDxfe5aK9Ll93e/74Y9oEQ= go.etcd.io/bbolt v1.4.0/go.mod h1:AsD+OCi/qPN1giOX1aiLAha3o1U8rAz65bvN4j0sRuk=
golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/mod v0.16.0 h1:QX4fJ0Rr5cPQCF7O9lh9Se4pmwfwskqZfq5moyldzic= golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
golang.org/x/mod v0.16.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac= golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
golang.org/x/oauth2 v0.20.0 h1:4mQdhULixXKP1rwYBW0vAijoXnkTG0BLCDRzfe1idMo= golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
golang.org/x/oauth2 v0.20.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 h1:nDVHiLt8aIbd/VzvPWN6kSOPE7+F/fNFDSXLVYkE/Iw=
golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE= golang.org/x/exp v0.0.0-20250305212735-054e65f0b394/go.mod h1:sIifuuw/Yco/y6yb6+bDNfyeQ/MdPUy/hKEMYQV17cM=
golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU=
golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8=
golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
golang.org/x/oauth2 v0.29.0 h1:WdYw2tdTK1S8olAzWHdgeqfy+Mtm9XNhv/xJsY65d98=
golang.org/x/oauth2 v0.29.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610=
golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.20.0 h1:VnkxpohqXaOBYJtBmEppKUG6mXpi+4O6purfc2+sMhw= golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/tools v0.19.0 h1:tfGCXNR1OsFG+sVdLAitlpjAvD/I6dHDKnYrpEZUHkw= golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
golang.org/x/tools v0.19.0/go.mod h1:qoJWxmGSIBmAeriMx19ogtrEPrGtDbPK634QFIcLAhc= golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58=
google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg= golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y=
google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0=
golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.31.0 h1:0EedkvKDbh+qistFTd0Bcwe/YLh4vHwWEkiI0toFIBU=
golang.org/x/tools v0.31.0/go.mod h1:naFTU+Cev749tSJRXJlna0T3WxKvb1kWEx15xA4SdmQ=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/gorm v1.25.10 h1:dQpO+33KalOA+aFYGlK+EfxcI5MbO7EP2yYygwh9h+s= gorm.io/driver/mysql v1.5.7 h1:MndhOPYOfEp2rHKgkZIhJ16eVUIRf2HmzgoPmh7FCWo=
gorm.io/gorm v1.25.10/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8= gorm.io/driver/mysql v1.5.7/go.mod h1:sEtPWMiqiN1N1cMXoXmBbd8C6/l+TESwriotuRRpkDM=
modernc.org/cc/v4 v4.21.2 h1:dycHFB/jDc3IyacKipCNSDrjIC0Lm1hyoWOZTRR20Lk= gorm.io/driver/postgres v1.5.11 h1:ubBVAfbKEUld/twyKZ0IYn9rSQh448EdelLYk9Mv314=
modernc.org/cc/v4 v4.21.2/go.mod h1:HM7VJTZbUCR3rV8EYBi9wxnJ0ZBRiGE5OeGXNA0IsLQ= gorm.io/driver/postgres v1.5.11/go.mod h1:DX3GReXH+3FPWGrrgffdvCk3DQ1dwDPdmbenSkweRGI=
modernc.org/ccgo/v4 v4.17.8 h1:yyWBf2ipA0Y9GGz/MmCmi3EFpKgeS7ICrAFes+suEbs= gorm.io/gorm v1.25.7/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
modernc.org/ccgo/v4 v4.17.8/go.mod h1:buJnJ6Fn0tyAdP/dqePbrrvLyr6qslFfTbFrCuaYvtA= gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8=
gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ=
modernc.org/cc/v4 v4.25.2 h1:T2oH7sZdGvTaie0BRNFbIYsabzCxUQg8nLqCdQ2i0ic=
modernc.org/cc/v4 v4.25.2/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
modernc.org/ccgo/v4 v4.25.1 h1:TFSzPrAGmDsdnhT9X2UrcPMI3N/mJ9/X9ykKXwLhDsU=
modernc.org/ccgo/v4 v4.25.1/go.mod h1:njjuAYiPflywOOrm3B7kCB444ONP5pAVr8PIEoE0uDw=
modernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE= modernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE=
modernc.org/fileutil v1.3.0/go.mod h1:XatxS8fZi3pS8/hKG2GH/ArUogfxjpEKs3Ku3aK4JyQ= modernc.org/fileutil v1.3.0/go.mod h1:XatxS8fZi3pS8/hKG2GH/ArUogfxjpEKs3Ku3aK4JyQ=
modernc.org/gc/v2 v2.4.1 h1:9cNzOqPyMJBvrUipmynX0ZohMhcxPtMccYgGOJdOiBw= modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
modernc.org/gc/v2 v2.4.1/go.mod h1:wzN5dK1AzVGoH6XOzc3YZ+ey/jPgYHLuVckd62P0GYU= modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
modernc.org/libc v1.51.0 h1:kjSHjz1guHbI5iRdi6nEr/wIKSN6X4vzLd6TJMN+lHA= modernc.org/libc v1.62.1 h1:s0+fv5E3FymN8eJVmnk0llBe6rOxCu/DEU+XygRbS8s=
modernc.org/libc v1.51.0/go.mod h1:15P6ublJ9FJR8YQCGy8DeQ2Uwur7iW9Hserr/T3OFZE= modernc.org/libc v1.62.1/go.mod h1:iXhATfJQLjG3NWy56a6WVU73lWOcdYVxsvwCgoPljuo=
modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4= modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo= modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
modernc.org/memory v1.8.0 h1:IqGTL6eFMaDZZhEWwcREgeMXYwmW83LYW8cROZYkg+E= modernc.org/memory v1.9.1 h1:V/Z1solwAVmMW1yttq3nDdZPJqV1rM05Ccq6KMSZ34g=
modernc.org/memory v1.8.0/go.mod h1:XPZ936zp5OMKGWPqbD3JShgd/ZoQ7899TUuQqxY+peU= modernc.org/memory v1.9.1/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4= modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0= modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
modernc.org/sortutil v1.2.0 h1:jQiD3PfS2REGJNzNCMMaLSp/wdMNieTbKX920Cqdgqc= modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
modernc.org/sortutil v1.2.0/go.mod h1:TKU2s7kJMf1AE84OoiGppNHJwvB753OYfNl2WRb++Ss= modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
modernc.org/sqlite v1.30.0 h1:8YhPUs/HTnlEgErn/jSYQTwHN/ex8CjHHjg+K9iG7LM= modernc.org/sqlite v1.37.0 h1:s1TMe7T3Q3ovQiK2Ouz4Jwh7dw4ZDqbebSDTlSJdfjI=
modernc.org/sqlite v1.30.0/go.mod h1:cgkTARJ9ugeXSNaLBPK3CqbOe7Ec7ZhWPoMFGldEYEw= modernc.org/sqlite v1.37.0/go.mod h1:5YiWv+YviqGMuGw4V+PNplcyaJ5v+vQd7TQOgkACoJM=
modernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA= modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
modernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0= modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=

23
helm/opengist/.helmignore Normal file
View File

@@ -0,0 +1,23 @@
# Patterns to ignore when building packages.
# This supports shell glob matching, relative path matching, and
# negation (prefixed with !). Only one pattern per line.
.DS_Store
# Common VCS dirs
.git/
.gitignore
.bzr/
.bzrignore
.hg/
.hgignore
.svn/
# Common backup files
*.swp
*.bak
*.tmp
*.orig
*~
# Various IDEs
.project
.idea/
*.tmproj
.vscode/

9
helm/opengist/Chart.lock Normal file
View File

@@ -0,0 +1,9 @@
dependencies:
- name: postgresql
repository: oci://registry-1.docker.io/bitnamicharts
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"

19
helm/opengist/Chart.yaml Normal file
View File

@@ -0,0 +1,19 @@
apiVersion: v2
name: opengist
description: Opengist Helm chart for Kubernetes
type: application
version: 0.3.0
appVersion: 1.11.0
home: https://opengist.io
icon: https://raw.githubusercontent.com/thomiceli/opengist/master/public/opengist.svg
sources:
- https://github.com/thomiceli/opengist
dependencies:
- name: postgresql
repository: oci://registry-1.docker.io/bitnamicharts
version: 16.7.27
condition: postgresql.enabled
- name: meilisearch
repository: https://meilisearch.github.io/meilisearch-kubernetes
version: 0.17.1
condition: meilisearch.enabled

81
helm/opengist/README.md Normal file
View File

@@ -0,0 +1,81 @@
# Opengist Helm Chart
![Version: 0.2.0](https://img.shields.io/badge/Version-0.2.0-informational?style=flat-square) ![AppVersion: 1.11.0](https://img.shields.io/badge/AppVersion-1.11.0-informational?style=flat-square)
Opengist Helm chart for Kubernetes.
* [Install](#install)
* [Configuration](#configuration)
* [Dependencies](#dependencies)
* [Meilisearch Indexer](#meilisearch-indexer)
* [PostgreSQL Database](#postgresql-database)
## Install
```bash
helm repo add opengist https://helm.opengist.io
helm install opengist opengist/opengist
```
## Configuration
This part explains how to configure the Opengist instance using the Helm chart. The `config.yml` file used by Opengist
is mounted from a Kubernetes Secret with a key `config.yml` and the values formatted as YAML.
### Using values
Using Helm values, you can define the values from a key name `config`
```yaml
config:
log-level: "warn"
log-output: "stdout"
```
This will create a Kubernetes secret named `opengist` mounted to the pod as a file with the YAML content of the secret,
used by Opengist.
### Using an existing secret
If you wish to not store sensitive data in your Helm values, you can create a Kubernetes secret with a key `config.yml`
and values formatted as YAML. You can then reference this secret in the Helm chart with the `configExistingSecret` key.
If defined, this existing secret will be used instead of creating a new one.
```yaml
configExistingSecret: <name of the secret>
```
## Dependencies
### Meilisearch Indexer
By default, Opengist uses the `bleve` indexer. **It is NOT available** if there is multiple replicas of the opengist pod (only one pod can open the index at the same time).
Instead, for multiple replicas setups, you **MUST** use the `meilisearch` indexer.
By setting `meilisearch.enabled: true`, the [Meilisearch chart](https://github.com/meilisearch/meilisearch-kubernetes) will be deployed aswell.
You must define the `meilisearch.host` (Kubernetes Service) and `meilisearch.key` (value created by Meilisearch) values to connect to the Meilisearch instance in your Opengist config :
```yaml
index: meilisearch
index.meili.host: http://opengist-meilisearch:7700 # pointing to the K8S Service
index.meili.api-key: MASTER_KEY # generated by Meilisearch
```
If you want to use the `bleve` indexer, you need to set the `replicas` to `1`.
### PostgreSQL Database
By default, Opengist uses the `sqlite` database. If needed, this chart also deploys a PostgreSQL instance.
By setting `postgresql.enabled: true`, the [Bitnami PostgreSQL chart](https://github.com/bitnami/charts/tree/main/bitnami/postgresql) will be deployed aswell.
You must define the `postgresql.host`, `postgresql.port`, `postgresql.database`, `postgresql.username` and `postgresql.password` values to connect to the PostgreSQL instance.
Then define the connection string in your Opengist config:
```yaml
db-uri: postgres://user:password@opengist-postgresql:5432/opengist
```
Note: `opengist-postgresql` is the name of the K8S Service deployed by this chart.

View File

@@ -0,0 +1,22 @@
1. Get the application URL by running these commands:
{{- if .Values.ingress.enabled }}
{{- range $host := .Values.ingress.hosts }}
{{- range .paths }}
http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ .path }}
{{- end }}
{{- end }}
{{- else if contains "NodePort" .Values.service.http.type }}
export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "opengist.fullname" . }})
export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}")
echo http://$NODE_IP:$NODE_PORT
{{- else if contains "LoadBalancer" .Values.service.http.type }}
NOTE: It may take a few minutes for the LoadBalancer IP to be available.
You can watch its status by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "opengist.fullname" . }}'
export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "opengist.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}")
echo http://$SERVICE_IP:{{ .Values.service.http.port }}
{{- else if contains "ClusterIP" .Values.service.http.type }}
export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "opengist.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}")
export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}")
echo "Visit http://127.0.0.1:8080 to use your application"
kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:$CONTAINER_PORT
{{- end }}

View File

@@ -0,0 +1,85 @@
{{/*
Expand the name of the chart.
*/}}
{{- define "opengist.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Create a default fully qualified app name.
We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
If release name contains chart name it will be used as a full name.
*/}}
{{- define "opengist.fullname" -}}
{{- if .Values.fullnameOverride }}
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- $name := default .Chart.Name .Values.nameOverride }}
{{- if contains $name .Release.Name }}
{{- .Release.Name | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
{{- end }}
{{- end }}
{{- end }}
{{/*
Create chart name and version as used by the chart label.
*/}}
{{- define "opengist.chart" -}}
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Common labels
*/}}
{{- define "opengist.labels" -}}
helm.sh/chart: {{ include "opengist.chart" . }}
app: {{ include "opengist.name" . }}
{{ include "opengist.selectorLabels" . }}
{{- if .Chart.AppVersion }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
{{- end }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end }}
{{/*
Selector labels
*/}}
{{- define "opengist.selectorLabels" -}}
app.kubernetes.io/name: {{ include "opengist.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end }}
{{/*
Create the name of the service account to use
*/}}
{{- define "opengist.serviceAccountName" -}}
{{- if .Values.serviceAccount.create }}
{{- default (include "opengist.fullname" .) .Values.serviceAccount.name }}
{{- else }}
{{- default "default" .Values.serviceAccount.name }}
{{- end }}
{{- end }}
{{/*
Create image URI
*/}}
{{- define "opengist.image" -}}
{{- if .Values.image.digest -}}
{{- printf "%s@%s" .Values.image.repository .Values.image.digest -}}
{{- else -}}
{{- printf "%s:%s" .Values.image.repository (.Values.image.tag | default .Chart.AppVersion) -}}
{{- end -}}
{{- end -}}
{{/*
Create secret name
*/}}
{{- define "opengist.secretName" -}}
{{- if .Values.configExistingSecret -}}
{{- printf "%s" (tpl .Values.configExistingSecret $) -}}
{{- else -}}
{{- printf "%s" (include "opengist.fullname" .) -}}
{{- end -}}
{{- end -}}

View File

@@ -0,0 +1,122 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "opengist.fullname" . }}
namespace: {{ .Values.namespace | default .Release.Namespace }}
labels:
{{- include "opengist.labels" . | nindent 4 }}
{{- if .Values.deployment.labels }}
{{- toYaml .Values.deployment.labels | nindent 4 }}
{{- end }}
{{- with .Values.deployment.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
{{- if not .Values.autoscaling.enabled }}
replicas: {{ .Values.replicaCount }}
{{- end }}
selector:
matchLabels:
{{- include "opengist.selectorLabels" . | nindent 6 }}
template:
metadata:
annotations:
checksum/config: {{ include (print $.Template.BasePath "/secret.yaml") . | sha256sum }}
{{- with .Values.podAnnotations }}
{{- toYaml . | nindent 8 }}
{{- end }}
labels:
{{- include "opengist.labels" . | nindent 8 }}
{{- with .Values.podLabels }}
{{- toYaml . | nindent 8 }}
{{- end }}
spec:
{{- if .Values.deployment.terminationGracePeriodSeconds }}
terminationGracePeriodSeconds: {{ .Values.deployment.terminationGracePeriodSeconds }}
{{- end }}
{{- with .Values.imagePullSecrets }}
imagePullSecrets:
{{- toYaml . | nindent 8 }}
{{- end }}
serviceAccountName: {{ include "opengist.serviceAccountName" . }}
securityContext:
{{- toYaml .Values.podSecurityContext | nindent 8 }}
initContainers:
- name: init-config
image: busybox:1.37
imagePullPolicy: IfNotPresent
command: ['sh', '-c', 'cp /init/config/config.yml /config-volume/config.yml']
volumeMounts:
- name: config-secret
mountPath: /init/config
- name: config-volume
mountPath: /config-volume
{{- if .Values.deployment.env }}
env:
{{- toYaml .Values.deployment.env | nindent 12 }}
{{- end }}
containers:
- name: {{ .Chart.Name }}
securityContext:
{{- toYaml .Values.securityContext | nindent 12 }}
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
ports:
- name: http
containerPort: {{ .Values.service.http.port }}
protocol: TCP
{{- if .Values.livenessProbe.enabled }}
livenessProbe:
{{- toYaml (omit .Values.livenessProbe "enabled") | nindent 12 }}
httpGet:
port: http
path: /healthcheck
{{- end }}
{{- if .Values.readinessProbe.enabled }}
readinessProbe:
{{- toYaml (omit .Values.readinessProbe "enabled") | nindent 12 }}
httpGet:
port: http
path: /healthcheck
{{- end }}
resources:
{{- toYaml .Values.resources | nindent 12 }}
volumeMounts:
- name: config-volume
mountPath: /config.yml
subPath: config.yml
- name: opengist-data
mountPath: /opengist
{{- if gt (len .Values.extraVolumeMounts) 0 }}
{{- toYaml .Values.extraVolumeMounts | nindent 12 }}
{{- end }}
volumes:
- name: opengist-data
{{- if .Values.persistence.enabled }}
persistentVolumeClaim:
claimName: {{ include "opengist.fullname" . }}-data
{{- else }}
emptyDir: {}
{{- end }}
- name: config-secret
secret:
secretName: {{ include "opengist.secretName" . }}
defaultMode: 511
- name: config-volume
emptyDir: {}
{{- if gt (len .Values.extraVolumes) 0 }}
{{- toYaml .Values.extraVolumes | nindent 8 }}
{{- end }}
{{- with .Values.nodeSelector }}
nodeSelector:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.affinity }}
affinity:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.tolerations }}
tolerations:
{{- toYaml . | nindent 8 }}
{{- end }}

View File

@@ -0,0 +1,37 @@
{{- if .Values.autoscaling.enabled }}
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: {{ include "opengist.fullname" . }}
namespace: {{ .Values.namespace | default .Release.Namespace }}
labels:
{{- include "opengist.labels" . | nindent 4 }}
{{- with .Values.autoscaling.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: {{ include "opengist.fullname" . }}
minReplicas: {{ .Values.autoscaling.minReplicas }}
maxReplicas: {{ .Values.autoscaling.maxReplicas }}
metrics:
{{- if .Values.autoscaling.targetCPUUtilizationPercentage }}
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }}
{{- end }}
{{- if .Values.autoscaling.targetMemoryUtilizationPercentage }}
- type: Resource
resource:
name: memory
target:
type: Utilization
averageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }}
{{- end }}
{{- end }}

View File

@@ -0,0 +1,47 @@
{{- if .Values.ingress.enabled -}}
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: {{ include "opengist.fullname" . }}
namespace: {{ .Values.namespace | default .Release.Namespace }}
labels:
{{- include "opengist.labels" . | nindent 4 }}
{{- if .Values.ingress.labels }}
{{- toYaml .Values.service.http.labels | nindent 4 }}
{{- end }}
{{- with .Values.ingress.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
{{- with .Values.ingress.className }}
ingressClassName: {{ . }}
{{- end }}
{{- if .Values.ingress.tls }}
tls:
{{- range .Values.ingress.tls }}
- hosts:
{{- range .hosts }}
- {{ . | quote }}
{{- end }}
secretName: {{ .secretName }}
{{- end }}
{{- end }}
rules:
{{- range .Values.ingress.hosts }}
- host: {{ .host | quote }}
http:
paths:
{{- range .paths }}
- path: {{ .path }}
{{- with .pathType }}
pathType: {{ . }}
{{- end }}
backend:
service:
name: {{ include "opengist.fullname" $ }}-http
port:
number: {{ $.Values.service.http.port }}
{{- end }}
{{- end }}
{{- end }}

View File

@@ -0,0 +1,14 @@
{{- if .Values.podDisruptionBudget -}}
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: {{ include "opengist.fullname" . }}
namespace: {{ .Values.namespace | default .Release.Namespace }}
labels:
{{- include "opengist.labels" . | nindent 4 }}
spec:
selector:
matchLabels:
{{- include "opengist.selectorLabels" . | nindent 6 }}
{{- toYaml .Values.podDisruptionBudget | nindent 2 }}
{{- end -}}

View File

@@ -0,0 +1,28 @@
{{- if .Values.persistence.enabled }}
kind: PersistentVolumeClaim
apiVersion: v1
metadata:
name: {{ include "opengist.fullname" . }}-data
namespace: {{ .Release.Namespace }}
{{- with .Values.persistence.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
labels:
{{- include "opengist.labels" . | nindent 4 }}
{{- if .Values.persistence.labels }}
{{- toYaml .Values.persistence.labels | nindent 4 }}
{{- end }}
spec:
accessModes:
{{- if gt .Values.replicaCount 1.0 }}
- ReadWriteMany
{{- else }}
{{- .Values.persistence.accessModes | toYaml | nindent 4 }}
{{- end }}
volumeMode: Filesystem
storageClassName: {{ .Values.persistence.storageClass | quote }}
resources:
requests:
storage: {{ .Values.persistence.size }}
{{- end }}

View File

@@ -0,0 +1,13 @@
{{- if (not .Values.configExistingSecret) }}
apiVersion: v1
kind: Secret
metadata:
name: {{ include "opengist.fullname" . }}
namespace: {{ .Release.Namespace }}
labels:
{{ include "opengist.labels" . | indent 4 }}
type: Opaque
stringData:
config.yml: |-
{{- .Values.config | toYaml | nindent 4 }}
{{- end }}

View File

@@ -0,0 +1,13 @@
{{- if .Values.serviceAccount.create -}}
apiVersion: v1
kind: ServiceAccount
metadata:
name: {{ include "opengist.serviceAccountName" . }}
namespace: {{ .Values.namespace | default .Release.Namespace }}
labels:
{{- include "opengist.labels" . | nindent 4 }}
{{- with .Values.serviceAccount.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
{{- end }}

View File

@@ -0,0 +1,47 @@
apiVersion: v1
kind: Service
metadata:
name: {{ include "opengist.fullname" . }}-http
namespace: {{ .Values.namespace | default .Release.Namespace }}
labels:
{{- include "opengist.labels" . | nindent 4 }}
{{- if .Values.service.http.labels }}
{{- toYaml .Values.service.http.labels | nindent 4 }}
{{- end }}
{{- with .Values.service.http.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
type: {{ .Values.service.http.type }}
{{- if eq .Values.service.http.type "LoadBalancer" }}
{{- if and .Values.service.http.loadBalancerIP }}
loadBalancerIP: {{ .Values.service.http.loadBalancerIP }}
{{- end }}
{{- if .Values.service.http.loadBalancerSourceRanges }}
loadBalancerSourceRanges:
{{- range .Values.service.http.loadBalancerSourceRanges }}
- {{ . }}
{{- end }}
{{- end }}
{{- end }}
{{- if .Values.service.http.externalIPs }}
externalIPs:
{{- toYaml .Values.service.http.externalIPs | nindent 4 }}
{{- end }}
{{- if .Values.service.http.externalTrafficPolicy }}
externalTrafficPolicy: {{ .Values.service.http.externalTrafficPolicy }}
{{- end }}
{{- if and .Values.service.http.clusterIP (eq .Values.service.http.type "ClusterIP") }}
clusterIP: {{ .Values.service.http.clusterIP }}
{{- end }}
ports:
- name: http
port: {{ .Values.service.http.port }}
{{- if .Values.service.http.nodePort }}
nodePort: {{ .Values.service.http.nodePort }}
{{- end }}
targetPort: {{ index .Values.config "http.port" }}
selector:
{{- include "opengist.selectorLabels" . | nindent 4 }}

View File

@@ -0,0 +1,64 @@
{{- if .Values.service.ssh.enabled }}
apiVersion: v1
kind: Service
metadata:
name: {{ include "opengist.fullname" . }}-ssh
namespace: {{ .Values.namespace | default .Release.Namespace }}
labels:
{{- include "opengist.labels" . | nindent 4 }}
{{- if .Values.service.ssh.labels }}
{{- toYaml .Values.service.ssh.labels | nindent 4 }}
{{- end }}
{{- with .Values.service.http.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
type: {{ .Values.service.ssh.type }}
{{- if eq .Values.service.ssh.type "LoadBalancer" }}
{{- if .Values.service.ssh.loadBalancerClass }}
loadBalancerClass: {{ .Values.service.ssh.loadBalancerClass }}
{{- end }}
{{- if .Values.service.ssh.loadBalancerIP }}
loadBalancerIP: {{ .Values.service.ssh.loadBalancerIP }}
{{- end -}}
{{- if .Values.service.ssh.loadBalancerSourceRanges }}
loadBalancerSourceRanges:
{{- range .Values.service.ssh.loadBalancerSourceRanges }}
- {{ . }}
{{- end }}
{{- end }}
{{- end }}
{{- if and .Values.service.ssh.clusterIP (eq .Values.service.ssh.type "ClusterIP") }}
clusterIP: {{ .Values.service.ssh.clusterIP }}
{{- end }}
{{- if .Values.service.ssh.externalIPs }}
externalIPs:
{{- toYaml .Values.service.ssh.externalIPs | nindent 4 }}
{{- end }}
{{- if .Values.service.ssh.ipFamilyPolicy }}
ipFamilyPolicy: {{ .Values.service.ssh.ipFamilyPolicy }}
{{- end }}
{{- with .Values.service.ssh.ipFamilies }}
ipFamilies:
{{- toYaml . | nindent 4 }}
{{- end -}}
{{- if .Values.service.ssh.externalTrafficPolicy }}
externalTrafficPolicy: {{ .Values.service.ssh.externalTrafficPolicy }}
{{- end }}
ports:
- name: ssh
port: {{ .Values.service.ssh.port }}
{{- if .Values.service.ssh.nodePort }}
nodePort: {{ .Values.service.ssh.nodePort }}
{{- end }}
{{- if index .Values.config "ssh.port" }}
targetPort: {{ index .Values.config "ssh.port" }}
{{- else }}
targetPort: 2222
{{- end }}
protocol: TCP
selector:
{{- include "opengist.selectorLabels" . | nindent 4 }}
{{- end }}

View File

@@ -0,0 +1,15 @@
apiVersion: v1
kind: Pod
metadata:
name: "{{ include "opengist.fullname" . }}-test-connection"
labels:
{{- include "opengist.labels" . | nindent 4 }}
annotations:
"helm.sh/hook": test
spec:
containers:
- name: wget
image: busybox
command: ['wget']
args: ['{{ include "opengist.fullname" . }}:{{ .Values.service.port }}']
restartPolicy: Never

201
helm/opengist/values.yaml Normal file
View File

@@ -0,0 +1,201 @@
## Kubernetes workload configuration for Opengist
nameOverride: ""
fullnameOverride: ""
namespace: ""
## Opengist YAML Application Config. See more at https://opengist.io/docs/configuration/cheat-sheet.html
## This will create a Kubernetes secret with the key `config.yml` containing the YAML configuration mounted in the pod.
config:
log-level: "warn"
log-output: "stdout"
## If defined, the existing secret will be used instead of creating a new one.
## The secret must contain a key named `config.yml` with the YAML configuration.
configExistingSecret: ""
## Define the image repository and tag to use.
image:
repository: ghcr.io/thomiceli/opengist
pullPolicy: Always
tag: "1.10.0"
digest: ""
imagePullSecrets: []
# - name: "image-pull-secret"
## Define the deployment replica count
replicaCount: 1
## Define the deployment strategy type
strategy:
type: "RollingUpdate"
rollingUpdate:
maxSurge: "100%"
maxUnavailable: 0
## Security Context settings
## ref: https://kubernetes.io/docs/tasks/configure-pod-container/security-context/
podSecurityContext:
fsGroup: 1000
securityContext: {}
# allowPrivilegeEscalation: false
## Pod Disruption Budget settings
## ref: https://kubernetes.io/docs/tasks/run-application/configure-pdb/
podDisruptionBudget: {}
# maxUnavailable: 1
# minAvailable: 1
## Set the Kubernetes service type
## ref: https://kubernetes.io/docs/concepts/services-networking/service/
service:
http:
type: ClusterIP
clusterIP:
port: 6157
nodePort:
loadBalancerIP:
externalIPs: []
labels: {}
annotations: {}
loadBalancerSourceRanges: []
externalTrafficPolicy:
ssh:
enabled: true
type: ClusterIP
clusterIP:
port: 2222
nodePort:
loadBalancerIP:
externalIPs: []
labels: {}
annotations: {}
loadBalancerSourceRanges: []
externalTrafficPolicy:
## HTTP Ingress for Opengist
## ref: https://kubernetes.io/docs/concepts/services-networking/ingress/
ingress:
enabled: false
className: ""
labels: {}
# node-role.kubernetes.io/ingress: platform
annotations: {}
# kubernetes.io/ingress.class: nginx
hosts:
- host: opengist.example.com
paths:
- path: /
pathType: Prefix
tls: []
# - secretName: opengist-tls
# hosts:
# - opengist.example.com
## Service Account for Opengist pods
## ref: https://kubernetes.io/docs/concepts/security/service-accounts/
serviceAccount:
create: true
annotations: {}
name: ""
## Set persistence using a Persistent Volume Claim
## If more than 2 replicas are set, the access mode must be ReadWriteMany
## ref: https://kubernetes.io/docs/concepts/storage/persistent-volumes/
persistence:
enabled: true
existingClaim: ""
storageClass: ""
labels: {}
annotations:
helm.sh/resource-policy: keep
size: 5Gi
accessModes:
- ReadWriteOnce
subPath: ""
extraVolumes: []
extraVolumeMounts: []
## Additional pod labels and annotations
podLabels: {}
podAnnotations: {}
## Configure resource requests and limits
## ref: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/
resources: {}
# limits:
# cpu: 100m
# memory: 128Mi
# requests:
# cpu: 100m
# memory: 128Mi
## Configure the liveness and readiness probes
## ref: https://kubernetes.io/docs/concepts/configuration/liveness-readiness-startup-probes/
livenessProbe:
enabled: true
initialDelaySeconds: 200
timeoutSeconds: 1
periodSeconds: 10
successThreshold: 1
failureThreshold: 5
readinessProbe:
enabled: true
initialDelaySeconds: 5
timeoutSeconds: 1
periodSeconds: 10
successThreshold: 1
failureThreshold: 3
## Define autoscaling configuration using Horizontal Pod Autoscaler
## ref: https://kubernetes.io/docs/tasks/run-application/horizontal-pod-autoscale/
autoscaling:
enabled: false
minReplicas: 1
maxReplicas: 10
targetCPUUtilizationPercentage: 80
# targetMemoryUtilizationPercentage: 80
annotations: {}
## Additional deployment configuration
deployment:
env: []
terminationGracePeriodSeconds: 60
labels: {}
annotations: {}
## Set pod assignment with node labels
## ref: https://kubernetes.io/docs/concepts/scheduling-eviction/assign-pod-node/
nodeSelector: {}
tolerations: []
affinity: {}
## Use PostgreSQL as a database, using Bitnami's PostgreSQL Helm chart
## ref: https://artifacthub.io/packages/helm/bitnami/postgresql/16.5.6
postgresql:
enabled: false
global:
postgresql:
auth:
username: opengist
password: opengist
database: opengist
service:
ports:
postgresql: 5432
primary:
persistence:
size: 10Gi
## Use Meilisearch as a code indexer, using Meilisearch's Helm chart
## ref: https://github.com/meilisearch/meilisearch-kubernetes/tree/meilisearch-0.12.0
meilisearch:
enabled: false
environment:
MEILI_ENV: "production"
auth:
existingMasterKeySecret:

View File

@@ -23,6 +23,7 @@ const (
SyncGistPreviews SyncGistPreviews
ResetHooks ResetHooks
IndexGists IndexGists
SyncGistLanguages
) )
var ( var (
@@ -73,6 +74,8 @@ func Run(actionType int) {
functionToRun = resetHooks functionToRun = resetHooks
case IndexGists: case IndexGists:
functionToRun = indexGists functionToRun = indexGists
case SyncGistLanguages:
functionToRun = syncGistLanguages
default: default:
log.Error().Msg("Unknown action type") log.Error().Msg("Unknown action type")
} }
@@ -141,17 +144,8 @@ func syncGistPreviews() {
func resetHooks() { func resetHooks() {
log.Info().Msg("Resetting Git server hooks for all repositories...") log.Info().Msg("Resetting Git server hooks for all repositories...")
entries, err := filepath.Glob(filepath.Join(config.GetHomeDir(), "repos", "*", "*")) if err := git.ResetHooks(); err != nil {
if err != nil { log.Error().Err(err).Msg("Error resetting hooks for repositories")
log.Error().Err(err).Msg("Cannot read repos directories")
return
}
for _, e := range entries {
path := strings.Split(e, string(os.PathSeparator))
if err := git.CreateDotGitFiles(path[len(path)-2], path[len(path)-1]); err != nil {
log.Error().Err(err).Msgf("Cannot reset hooks for repository %s/%s", path[len(path)-2], path[len(path)-1])
}
} }
} }
@@ -175,3 +169,17 @@ func indexGists() {
} }
} }
} }
func syncGistLanguages() {
log.Info().Msg("Syncing all Gist languages...")
gists, err := db.GetAllGistsRows()
if err != nil {
log.Error().Err(err).Msg("Cannot get gists")
return
}
for _, gist := range gists {
log.Info().Msgf("Syncing languages for gist %d", gist.ID)
gist.UpdateLanguages()
}
}

View File

@@ -0,0 +1,64 @@
package ldap
import (
"fmt"
"github.com/go-ldap/ldap/v3"
"github.com/thomiceli/opengist/internal/config"
)
func Enabled() bool {
return config.C.LDAPUrl != ""
}
// Authenticate attempts to authenticate a user against the configured LDAP instance.
func Authenticate(username, password string) (bool, error) {
l, err := ldap.DialURL(config.C.LDAPUrl)
if err != nil {
return false, fmt.Errorf("unable to connect to URI: %v", config.C.LDAPUrl)
}
defer func(l *ldap.Conn) {
_ = l.Close()
}(l)
// First bind with a read only user
err = l.Bind(config.C.LDAPBindDn, config.C.LDAPBindCredentials)
if err != nil {
return false, err
}
searchFilter := fmt.Sprintf(config.C.LDAPSearchFilter, username)
searchRequest := ldap.NewSearchRequest(
config.C.LDAPSearchBase,
ldap.ScopeWholeSubtree,
ldap.NeverDerefAliases,
0,
0,
false,
searchFilter,
[]string{"dn"},
nil,
)
sr, err := l.Search(searchRequest)
if err != nil {
return false, err
}
if len(sr.Entries) != 1 {
return false, nil
}
// Bind as the user to verify their password
err = l.Bind(sr.Entries[0].DN, password)
if err != nil {
return false, nil
}
// Rebind as the read only user for any further queries
err = l.Bind(config.C.LDAPBindDn, config.C.LDAPBindCredentials)
if err != nil {
return false, err
}
return true, nil
}

View File

@@ -0,0 +1,117 @@
package oauth
import (
gocontext "context"
gojson "encoding/json"
"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 {
Provider
URL string
}
func (p *GiteaProvider) RegisterProvider() error {
goth.UseProviders(
gitea.NewCustomisedURL(
config.C.GiteaClientKey,
config.C.GiteaSecret,
urlJoin(p.URL, "/oauth/gitea/callback"),
urlJoin(config.C.GiteaUrl, "/login/oauth/authorize"),
urlJoin(config.C.GiteaUrl, "/login/oauth/access_token"),
urlJoin(config.C.GiteaUrl, "/api/v1/user"),
),
)
return nil
}
func (p *GiteaProvider) BeginAuthHandler(ctx *context.Context) {
ctxValue := gocontext.WithValue(ctx.Request().Context(), gothic.ProviderParamKey, GiteaProviderString)
ctx.SetRequest(ctx.Request().WithContext(ctxValue))
gothic.BeginAuthHandler(ctx.Response(), ctx.Request())
}
func (p *GiteaProvider) UserHasProvider(user *db.User) bool {
return user.GiteaID != ""
}
func NewGiteaProvider(url string) *GiteaProvider {
return &GiteaProvider{
URL: url,
}
}
type GiteaCallbackProvider struct {
CallbackProvider
User *goth.User
}
func (p *GiteaCallbackProvider) GetProvider() string {
return GiteaProviderString
}
func (p *GiteaCallbackProvider) GetProviderUser() *goth.User {
return p.User
}
func (p *GiteaCallbackProvider) GetProviderUserID(user *db.User) bool {
return user.GiteaID != ""
}
func (p *GiteaCallbackProvider) GetProviderUserSSHKeys() ([]string, error) {
resp, err := http.Get(urlJoin(config.C.GiteaUrl, p.User.NickName+".keys"))
if err != nil {
return nil, err
}
defer resp.Body.Close()
return readKeys(resp)
}
func (p *GiteaCallbackProvider) UpdateUserDB(user *db.User) {
user.GiteaID = p.User.UserID
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 NewGiteaCallbackProvider(user *goth.User) CallbackProvider {
return &GiteaCallbackProvider{
User: user,
}
}

View File

@@ -0,0 +1,84 @@
package oauth
import (
gocontext "context"
"github.com/markbates/goth"
"github.com/markbates/goth/gothic"
"github.com/markbates/goth/providers/github"
"github.com/thomiceli/opengist/internal/config"
"github.com/thomiceli/opengist/internal/db"
"github.com/thomiceli/opengist/internal/web/context"
"net/http"
)
type GitHubProvider struct {
Provider
URL string
}
func (p *GitHubProvider) RegisterProvider() error {
goth.UseProviders(
github.New(
config.C.GithubClientKey,
config.C.GithubSecret,
urlJoin(p.URL, "/oauth/github/callback"),
),
)
return nil
}
func (p *GitHubProvider) BeginAuthHandler(ctx *context.Context) {
ctxValue := gocontext.WithValue(ctx.Request().Context(), gothic.ProviderParamKey, GitHubProviderString)
ctx.SetRequest(ctx.Request().WithContext(ctxValue))
gothic.BeginAuthHandler(ctx.Response(), ctx.Request())
}
func (p *GitHubProvider) UserHasProvider(user *db.User) bool {
return user.GithubID != ""
}
func NewGitHubProvider(url string) *GitHubProvider {
return &GitHubProvider{
URL: url,
}
}
type GitHubCallbackProvider struct {
CallbackProvider
User *goth.User
}
func (p *GitHubCallbackProvider) GetProvider() string {
return GitHubProviderString
}
func (p *GitHubCallbackProvider) GetProviderUser() *goth.User {
return p.User
}
func (p *GitHubCallbackProvider) GetProviderUserID(user *db.User) bool {
return user.GithubID != ""
}
func (p *GitHubCallbackProvider) GetProviderUserSSHKeys() ([]string, error) {
resp, err := http.Get("https://github.com/" + p.User.NickName + ".keys")
if err != nil {
return nil, err
}
defer resp.Body.Close()
return readKeys(resp)
}
func (p *GitHubCallbackProvider) UpdateUserDB(user *db.User) {
user.GithubID = p.User.UserID
user.AvatarURL = "https://avatars.githubusercontent.com/u/" + p.User.UserID + "?v=4"
}
func NewGitHubCallbackProvider(user *goth.User) CallbackProvider {
return &GitHubCallbackProvider{
User: user,
}
}

View File

@@ -0,0 +1,118 @@
package oauth
import (
gocontext "context"
gojson "encoding/json"
"io"
"net/http"
"github.com/markbates/goth"
"github.com/markbates/goth/gothic"
"github.com/markbates/goth/providers/gitlab"
"github.com/rs/zerolog/log"
"github.com/thomiceli/opengist/internal/config"
"github.com/thomiceli/opengist/internal/db"
"github.com/thomiceli/opengist/internal/web/context"
)
type GitLabProvider struct {
Provider
URL string
}
func (p *GitLabProvider) RegisterProvider() error {
goth.UseProviders(
gitlab.NewCustomisedURL(
config.C.GitlabClientKey,
config.C.GitlabSecret,
urlJoin(p.URL, "/oauth/gitlab/callback"),
urlJoin(config.C.GitlabUrl, "/oauth/authorize"),
urlJoin(config.C.GitlabUrl, "/oauth/token"),
urlJoin(config.C.GitlabUrl, "/api/v4/user"),
),
)
return nil
}
func (p *GitLabProvider) BeginAuthHandler(ctx *context.Context) {
ctxValue := gocontext.WithValue(ctx.Request().Context(), gothic.ProviderParamKey, GitLabProviderString)
ctx.SetRequest(ctx.Request().WithContext(ctxValue))
gothic.BeginAuthHandler(ctx.Response(), ctx.Request())
}
func (p *GitLabProvider) UserHasProvider(user *db.User) bool {
return user.GitlabID != ""
}
func NewGitLabProvider(url string) *GitLabProvider {
return &GitLabProvider{
URL: url,
}
}
type GitLabCallbackProvider struct {
CallbackProvider
User *goth.User
}
func (p *GitLabCallbackProvider) GetProvider() string {
return GitLabProviderString
}
func (p *GitLabCallbackProvider) GetProviderUser() *goth.User {
return p.User
}
func (p *GitLabCallbackProvider) GetProviderUserID(user *db.User) bool {
return user.GitlabID != ""
}
func (p *GitLabCallbackProvider) GetProviderUserSSHKeys() ([]string, error) {
resp, err := http.Get(urlJoin(config.C.GitlabUrl, p.User.NickName+".keys"))
if err != nil {
return nil, err
}
defer resp.Body.Close()
return readKeys(resp)
}
func (p *GitLabCallbackProvider) UpdateUserDB(user *db.User) {
user.GitlabID = p.User.UserID
resp, err := http.Get(urlJoin(config.C.GitlabUrl, "/api/v4/avatar?size=400&email=", p.User.Email))
if err != nil {
log.Error().Err(err).Msg("Cannot get user avatar from GitLab")
return
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
log.Error().Err(err).Msg("Cannot read Gitlab response body")
return
}
var result map[string]interface{}
err = gojson.Unmarshal(body, &result)
if err != nil {
log.Error().Err(err).Msg("Cannot unmarshal Gitlab response body")
return
}
field, ok := result["avatar_url"]
if !ok {
log.Error().Msg("Field 'avatar_url' not found in Gitlab JSON response")
return
}
user.AvatarURL = field.(string)
}
func NewGitLabCallbackProvider(user *goth.User) CallbackProvider {
return &GitLabCallbackProvider{
User: user,
}
}

View File

@@ -0,0 +1,86 @@
package oauth
import (
gocontext "context"
"errors"
"github.com/markbates/goth"
"github.com/markbates/goth/gothic"
"github.com/markbates/goth/providers/openidConnect"
"github.com/thomiceli/opengist/internal/config"
"github.com/thomiceli/opengist/internal/db"
"github.com/thomiceli/opengist/internal/web/context"
)
type OIDCProvider struct {
Provider
URL string
}
func (p *OIDCProvider) RegisterProvider() error {
oidcProvider, err := openidConnect.New(
config.C.OIDCClientKey,
config.C.OIDCSecret,
urlJoin(p.URL, "/oauth/openid-connect/callback"),
config.C.OIDCDiscoveryUrl,
"openid",
"email",
"profile",
config.C.OIDCGroupClaimName,
)
if err != nil {
return errors.New("Cannot create OIDC provider: " + err.Error())
}
goth.UseProviders(oidcProvider)
return nil
}
func (p *OIDCProvider) BeginAuthHandler(ctx *context.Context) {
ctxValue := gocontext.WithValue(ctx.Request().Context(), gothic.ProviderParamKey, OpenIDConnectString)
ctx.SetRequest(ctx.Request().WithContext(ctxValue))
gothic.BeginAuthHandler(ctx.Response(), ctx.Request())
}
func (p *OIDCProvider) UserHasProvider(user *db.User) bool {
return user.OIDCID != ""
}
func NewOIDCProvider(url string) *OIDCProvider {
return &OIDCProvider{
URL: url,
}
}
type OIDCCallbackProvider struct {
CallbackProvider
User *goth.User
}
func (p *OIDCCallbackProvider) GetProvider() string {
return OpenIDConnectString
}
func (p *OIDCCallbackProvider) GetProviderUser() *goth.User {
return p.User
}
func (p *OIDCCallbackProvider) GetProviderUserID(user *db.User) bool {
return user.OIDCID != ""
}
func (p *OIDCCallbackProvider) GetProviderUserSSHKeys() ([]string, error) {
return nil, nil
}
func (p *OIDCCallbackProvider) UpdateUserDB(user *db.User) {
user.OIDCID = p.User.UserID
user.AvatarURL = p.User.AvatarURL
}
func NewOIDCCallbackProvider(user *goth.User) CallbackProvider {
return &OIDCCallbackProvider{
User: user,
}
}

View File

@@ -0,0 +1,93 @@
package oauth
import (
"fmt"
"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 (
GitHubProviderString = "github"
GitLabProviderString = "gitlab"
GiteaProviderString = "gitea"
OpenIDConnectString = "openid-connect"
)
type Provider interface {
RegisterProvider() error
BeginAuthHandler(ctx *context.Context)
UserHasProvider(user *db.User) bool
}
type CallbackProvider interface {
GetProvider() string
GetProviderUser() *goth.User
GetProviderUserID(user *db.User) bool
GetProviderUserSSHKeys() ([]string, error)
UpdateUserDB(user *db.User)
}
func DefineProvider(provider string, url string) (Provider, error) {
switch provider {
case GitHubProviderString:
return NewGitHubProvider(url), nil
case GitLabProviderString:
return NewGitLabProvider(url), nil
case GiteaProviderString:
return NewGiteaProvider(url), nil
case OpenIDConnectString:
return NewOIDCProvider(url), nil
}
return nil, fmt.Errorf("unsupported provider %s", provider)
}
func CompleteUserAuth(ctx *context.Context) (CallbackProvider, error) {
user, err := gothic.CompleteUserAuth(ctx.Response(), ctx.Request())
if err != nil {
return nil, err
}
switch user.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", user.Provider)
}
func urlJoin(base string, elem ...string) string {
joined, err := url.JoinPath(base, elem...)
if err != nil {
log.Error().Err(err).Msg("Cannot join url")
}
return joined
}
func readKeys(response *http.Response) ([]string, error) {
body, err := io.ReadAll(response.Body)
if err != nil {
return nil, fmt.Errorf("could not get user keys %v", err)
}
keys := strings.Split(string(body), "\n")
if len(keys[len(keys)-1]) == 0 {
keys = keys[:len(keys)-1]
}
return keys, nil
}

View File

@@ -1,4 +1,4 @@
package utils package password
import ( import (
"crypto/rand" "crypto/rand"
@@ -6,11 +6,12 @@ import (
"encoding/base64" "encoding/base64"
"errors" "errors"
"fmt" "fmt"
"golang.org/x/crypto/argon2"
"strings" "strings"
"golang.org/x/crypto/argon2"
) )
type Argon2ID struct { type argon2ID struct {
format string format string
version int version int
time uint32 time uint32
@@ -20,7 +21,7 @@ type Argon2ID struct {
threads uint8 threads uint8
} }
var Argon2id = Argon2ID{ var Argon2id = argon2ID{
format: "$argon2id$v=%d$m=%d,t=%d,p=%d$%s$%s", format: "$argon2id$v=%d$m=%d,t=%d,p=%d$%s$%s",
version: argon2.Version, version: argon2.Version,
time: 1, time: 1,
@@ -30,7 +31,7 @@ var Argon2id = Argon2ID{
threads: 4, threads: 4,
} }
func (a Argon2ID) Hash(plain string) (string, error) { func (a argon2ID) Hash(plain string) (string, error) {
salt := make([]byte, a.saltLen) salt := make([]byte, a.saltLen)
if _, err := rand.Read(salt); err != nil { if _, err := rand.Read(salt); err != nil {
return "", err return "", err
@@ -44,7 +45,7 @@ func (a Argon2ID) Hash(plain string) (string, error) {
), nil ), nil
} }
func (a Argon2ID) Verify(plain, hash string) (bool, error) { func (a argon2ID) Verify(plain, hash string) (bool, error) {
if hash == "" { if hash == "" {
return false, nil return false, nil
} }

View File

@@ -0,0 +1,9 @@
package password
func HashPassword(code string) (string, error) {
return Argon2id.Hash(code)
}
func VerifyPassword(code, hashedCode string) (bool, error) {
return Argon2id.Verify(code, hashedCode)
}

46
internal/auth/totp/aes.go Normal file
View File

@@ -0,0 +1,46 @@
package totp
import (
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"fmt"
"io"
)
func AESEncrypt(key, text []byte) ([]byte, error) {
block, err := aes.NewCipher(key)
if err != nil {
return nil, err
}
ciphertext := make([]byte, aes.BlockSize+len(text))
iv := ciphertext[:aes.BlockSize]
if _, err = io.ReadFull(rand.Reader, iv); err != nil {
return nil, err
}
stream := cipher.NewCFBEncrypter(block, iv)
stream.XORKeyStream(ciphertext[aes.BlockSize:], text)
return ciphertext, nil
}
func AESDecrypt(key, ciphertext []byte) ([]byte, error) {
block, err := aes.NewCipher(key)
if err != nil {
return nil, err
}
if len(ciphertext) < aes.BlockSize {
return nil, fmt.Errorf("ciphertext too short")
}
iv := ciphertext[:aes.BlockSize]
ciphertext = ciphertext[aes.BlockSize:]
stream := cipher.NewCFBDecrypter(block, iv)
stream.XORKeyStream(ciphertext, ciphertext)
return ciphertext, nil
}

View File

@@ -0,0 +1,61 @@
package totp
import (
"bytes"
"crypto/rand"
"encoding/base64"
"github.com/pquerna/otp/totp"
"html/template"
"image/png"
"strings"
)
const secretSize = 16
func GenerateQRCode(username, siteUrl string, secret []byte) (string, template.URL, error, []byte) {
var err error
if secret == nil {
secret, err = generateSecret()
if err != nil {
return "", "", err, nil
}
}
otpKey, err := totp.Generate(totp.GenerateOpts{
SecretSize: secretSize,
Issuer: "Opengist (" + strings.ReplaceAll(siteUrl, ":", "") + ")",
AccountName: username,
Secret: secret,
})
if err != nil {
return "", "", err, nil
}
qrcode, err := otpKey.Image(320, 240)
if err != nil {
return "", "", err, nil
}
var imgBytes bytes.Buffer
if err = png.Encode(&imgBytes, qrcode); err != nil {
return "", "", err, nil
}
qrcodeImage := template.URL("data:image/png;base64," + base64.StdEncoding.EncodeToString(imgBytes.Bytes()))
return otpKey.Secret(), qrcodeImage, nil, secret
}
func Validate(passcode, secret string) bool {
return totp.Validate(passcode, secret)
}
func generateSecret() ([]byte, error) {
secret := make([]byte, secretSize)
_, err := rand.Reader.Read(secret)
if err != nil {
return nil, err
}
return secret, nil
}

View File

@@ -0,0 +1,83 @@
package auth
import (
"errors"
"github.com/rs/zerolog/log"
"github.com/thomiceli/opengist/internal/auth/ldap"
passwordpkg "github.com/thomiceli/opengist/internal/auth/password"
"github.com/thomiceli/opengist/internal/db"
"gorm.io/gorm"
)
type AuthError struct {
message string
}
func (e AuthError) Error() string {
return e.message
}
func TryAuthentication(username, password string) (*db.User, error) {
user, err := db.GetUserByUsername(username)
if err != nil {
if !errors.Is(err, gorm.ErrRecordNotFound) {
log.Error().Err(err).Msgf("Cannot get user by username %s", username)
return nil, err
}
}
if user.Password != "" {
return tryDbLogin(user, password)
} else {
if ldap.Enabled() {
return tryLdapLogin(username, password)
}
return nil, AuthError{"no authentication method available"}
}
}
func tryDbLogin(user *db.User, password string) (*db.User, error) {
if ok, err := passwordpkg.VerifyPassword(password, user.Password); !ok {
if err != nil {
log.Error().Err(err).Msg("Password verification failed")
return nil, err
}
return nil, AuthError{"invalid password"}
}
return user, nil
}
func tryLdapLogin(username, password string) (user *db.User, err error) {
ok, err := ldap.Authenticate(username, password)
if err != nil {
log.Error().Err(err).Msg("LDAP authentication failed")
return nil, err
}
if !ok {
return nil, AuthError{"invalid LDAP credentials"}
}
if user, err = db.GetUserByUsername(username); err != nil {
if !errors.Is(err, gorm.ErrRecordNotFound) {
log.Error().Err(err).Msgf("Cannot get user by username %s", username)
return nil, err
}
}
if errors.Is(err, gorm.ErrRecordNotFound) {
user = &db.User{
Username: username,
}
if err = user.Create(); err != nil {
log.Warn().Err(err).Msg("Cannot create user after LDAP authentication")
return nil, err
}
return user, nil
}
return user, nil
}

View File

@@ -0,0 +1,58 @@
package webauthn
import (
"encoding/binary"
"github.com/go-webauthn/webauthn/protocol"
"github.com/go-webauthn/webauthn/webauthn"
"github.com/thomiceli/opengist/internal/db"
)
type user struct {
*db.User
}
func (u *user) WebAuthnID() []byte {
return uintToBytes(u.ID)
}
func (u *user) WebAuthnName() string {
return u.Username
}
func (u *user) WebAuthnDisplayName() string {
return u.Username
}
func (u *user) WebAuthnCredentials() []webauthn.Credential {
dbCreds, err := db.GetAllWACredentialsForUser(u.ID)
if err != nil {
return nil
}
return dbCreds
}
func (u *user) Exclusions() []protocol.CredentialDescriptor {
creds := u.WebAuthnCredentials()
exclusions := make([]protocol.CredentialDescriptor, len(creds))
for i, cred := range creds {
exclusions[i] = cred.Descriptor()
}
return exclusions
}
func discoverUser(rawID []byte, _ []byte) (webauthn.User, error) {
ogUser, err := db.GetUserByCredentialID(rawID)
if err != nil {
return nil, err
}
return &user{User: ogUser}, nil
}
func uintToBytes(n uint) []byte {
b := make([]byte, 8)
binary.BigEndian.PutUint64(b, uint64(n))
return b
}

View File

@@ -0,0 +1,138 @@
package webauthn
import (
"encoding/json"
"github.com/go-webauthn/webauthn/protocol"
"github.com/go-webauthn/webauthn/webauthn"
"github.com/rs/zerolog/log"
"github.com/thomiceli/opengist/internal/config"
"github.com/thomiceli/opengist/internal/db"
"net/http"
"net/url"
)
var webAuthn *webauthn.WebAuthn
func Init(urlStr string) error {
var rpid, rporigin string
var err error
if urlStr == "" {
log.Info().Msg("External URL is not set, passkeys RP ID and Origins will be set to localhost")
rpid = "localhost"
rporigin = "http://localhost" + ":" + config.C.HttpPort
} else {
urlStruct, err := url.Parse(urlStr)
if err != nil {
return err
}
rpid = urlStruct.Hostname()
rporigin, err = protocol.FullyQualifiedOrigin(urlStr)
if err != nil {
log.Error().Err(err).Msg("Failed to get fully qualified origin from external URL")
}
}
webAuthn, err = webauthn.New(&webauthn.Config{
RPDisplayName: "Opengist",
RPID: rpid,
RPOrigins: []string{rporigin},
})
return err
}
func BeginBinding(dbUser *db.User) (credCreation *protocol.CredentialCreation, jsonSession []byte, err error) {
waUser := &user{User: dbUser}
credCreation, session, err := webAuthn.BeginRegistration(waUser, webauthn.WithAuthenticatorSelection(
protocol.AuthenticatorSelection{
ResidentKey: protocol.ResidentKeyRequirementRequired,
UserVerification: protocol.VerificationRequired,
},
), webauthn.WithAppIdExcludeExtension("Opengist"), webauthn.WithExclusions(waUser.Exclusions()))
if err != nil {
return nil, nil, err
}
jsonSession, _ = json.Marshal(session)
return
}
func FinishBinding(dbUser *db.User, jsonSession []byte, response *http.Request) (*webauthn.Credential, error) {
waUser := &user{User: dbUser}
var session webauthn.SessionData
_ = json.Unmarshal(jsonSession, &session)
return webAuthn.FinishRegistration(waUser, session, response)
}
func BeginDiscoverableLogin() (credCreation *protocol.CredentialAssertion, jsonSession []byte, err error) {
credCreation, session, err := webAuthn.BeginDiscoverableLogin(
webauthn.WithUserVerification(protocol.VerificationPreferred),
)
jsonSession, _ = json.Marshal(session)
return
}
func FinishDiscoverableLogin(jsonSession []byte, response *http.Request) (uint, error) {
var session webauthn.SessionData
_ = json.Unmarshal(jsonSession, &session)
parsedResponse, err := protocol.ParseCredentialRequestResponse(response)
if err != nil {
return 0, err
}
waUser, cred, err := webAuthn.ValidatePasskeyLogin(discoverUser, session, parsedResponse)
if err != nil {
return 0, err
}
dbCredential, err := db.GetCredentialByID(cred.ID)
if err != nil {
return 0, err
}
if err = dbCredential.UpdateSignCount(); err != nil {
return 0, err
}
if err = dbCredential.UpdateLastUsedAt(); err != nil {
return 0, err
}
return waUser.(*user).User.ID, nil
}
func BeginLogin(dbUser *db.User) (credCreation *protocol.CredentialAssertion, jsonSession []byte, err error) {
waUser := &user{User: dbUser}
credCreation, session, err := webAuthn.BeginLogin(waUser)
jsonSession, _ = json.Marshal(session)
return
}
func FinishLogin(dbUser *db.User, jsonSession []byte, response *http.Request) error {
waUser := &user{User: dbUser}
var session webauthn.SessionData
_ = json.Unmarshal(jsonSession, &session)
cred, err := webAuthn.FinishLogin(waUser, session, response)
if err != nil {
return err
}
dbCredential, err := db.GetCredentialByID(cred.ID)
if err != nil {
return err
}
if err = dbCredential.UpdateSignCount(); err != nil {
return err
}
if err = dbCredential.UpdateLastUsedAt(); err != nil {
return err
}
return err
}

View File

@@ -2,8 +2,8 @@ package cli
import ( import (
"fmt" "fmt"
"github.com/thomiceli/opengist/internal/auth/password"
"github.com/thomiceli/opengist/internal/db" "github.com/thomiceli/opengist/internal/db"
"github.com/thomiceli/opengist/internal/utils"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v2"
) )
@@ -12,6 +12,7 @@ var CmdAdmin = cli.Command{
Usage: "Admin commands", Usage: "Admin commands",
Subcommands: []*cli.Command{ Subcommands: []*cli.Command{
&CmdAdminResetPassword, &CmdAdminResetPassword,
&CmdAdminToggleAdmin,
}, },
} }
@@ -32,7 +33,7 @@ var CmdAdminResetPassword = cli.Command{
fmt.Printf("Cannot get user %s: %s\n", username, err) fmt.Printf("Cannot get user %s: %s\n", username, err)
return err return err
} }
password, err := utils.Argon2id.Hash(plainPassword) password, err := password.HashPassword(plainPassword)
if err != nil { if err != nil {
fmt.Printf("Cannot hash password for user %s: %s\n", username, err) fmt.Printf("Cannot hash password for user %s: %s\n", username, err)
return err return err
@@ -48,3 +49,30 @@ var CmdAdminResetPassword = cli.Command{
return nil return nil
}, },
} }
var CmdAdminToggleAdmin = cli.Command{
Name: "toggle-admin",
Usage: "Toggle the admin status for a given user",
ArgsUsage: "[username]",
Action: func(ctx *cli.Context) error {
initialize(ctx)
if ctx.NArg() < 1 {
return fmt.Errorf("username is required")
}
username := ctx.Args().Get(0)
user, err := db.GetUserByUsername(username)
if err != nil {
fmt.Printf("Cannot get user %s: %s\n", username, err)
return err
}
user.IsAdmin = !user.IsAdmin
if err = user.Update(); err != nil {
fmt.Printf("Cannot update user %s: %s\n", username, err)
}
fmt.Printf("User %s admin set to %t\n", username, user.IsAdmin)
return nil
},
}

View File

@@ -8,7 +8,6 @@ import (
"github.com/urfave/cli/v2" "github.com/urfave/cli/v2"
"io" "io"
"os" "os"
"path/filepath"
) )
var CmdHook = cli.Command{ var CmdHook = cli.Command{
@@ -50,7 +49,8 @@ func initialize(ctx *cli.Context) {
} }
config.InitLog() config.InitLog()
if err := db.Setup(filepath.Join(config.GetHomeDir(), config.C.DBFilename), false); err != nil { db.DeprecationDBFilename()
if err := db.Setup(config.C.DBUri); err != nil {
log.Fatal().Err(err).Msg("Failed to initialize database in hooks") log.Fatal().Err(err).Msg("Failed to initialize database in hooks")
} }
} }

View File

@@ -3,13 +3,13 @@ package cli
import ( import (
"fmt" "fmt"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
"github.com/thomiceli/opengist/internal/auth/webauthn"
"github.com/thomiceli/opengist/internal/config" "github.com/thomiceli/opengist/internal/config"
"github.com/thomiceli/opengist/internal/db" "github.com/thomiceli/opengist/internal/db"
"github.com/thomiceli/opengist/internal/git" "github.com/thomiceli/opengist/internal/git"
"github.com/thomiceli/opengist/internal/index" "github.com/thomiceli/opengist/internal/index"
"github.com/thomiceli/opengist/internal/memdb"
"github.com/thomiceli/opengist/internal/ssh" "github.com/thomiceli/opengist/internal/ssh"
"github.com/thomiceli/opengist/internal/web" "github.com/thomiceli/opengist/internal/web/server"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v2"
"os" "os"
"os/signal" "os/signal"
@@ -36,11 +36,12 @@ var CmdStart = cli.Command{
Initialize(ctx) Initialize(ctx)
go web.NewServer(os.Getenv("OG_DEV") == "1", path.Join(config.GetHomeDir(), "sessions")).Start() server := server.NewServer(os.Getenv("OG_DEV") == "1", path.Join(config.GetHomeDir(), "sessions"), false)
go server.Start()
go ssh.Start() go ssh.Start()
<-stopCtx.Done() <-stopCtx.Done()
shutdown() shutdown(server)
return nil return nil
}, },
} }
@@ -75,6 +76,8 @@ func Initialize(ctx *cli.Context) {
panic(err) panic(err)
} }
config.SetupSecretKey()
config.InitLog() config.InitLog()
gitVersion, err := git.GetGitVersion() gitVersion, err := git.GetGitVersion()
@@ -92,6 +95,10 @@ func Initialize(ctx *cli.Context) {
homePath := config.GetHomeDir() homePath := config.GetHomeDir()
log.Info().Msg("Data directory: " + homePath) log.Info().Msg("Data directory: " + homePath)
if err := git.InitGitConfig(); err != nil {
log.Warn().Err(err).Msgf("Failed to change the host's git global config, ensure to add to `safe.directory` the path %s, and `receive.advertisePushOptions` is set to true.", homePath)
}
if err := createSymlink(homePath, ctx.String("config")); err != nil { if err := createSymlink(homePath, ctx.String("config")); err != nil {
log.Fatal().Err(err).Msg("Failed to create symlinks") log.Fatal().Err(err).Msg("Failed to create symlinks")
} }
@@ -108,32 +115,35 @@ func Initialize(ctx *cli.Context) {
if err := os.MkdirAll(filepath.Join(homePath, "custom"), 0755); err != nil { if err := os.MkdirAll(filepath.Join(homePath, "custom"), 0755); err != nil {
log.Fatal().Err(err).Send() log.Fatal().Err(err).Send()
} }
log.Info().Msg("Database file: " + filepath.Join(homePath, config.C.DBFilename))
if err := db.Setup(filepath.Join(homePath, config.C.DBFilename), false); err != nil { db.DeprecationDBFilename()
if err := db.Setup(config.C.DBUri); err != nil {
log.Fatal().Err(err).Msg("Failed to initialize database") log.Fatal().Err(err).Msg("Failed to initialize database")
} }
if err := memdb.Setup(); err != nil { if err := webauthn.Init(config.C.ExternalUrl); err != nil {
log.Fatal().Err(err).Msg("Failed to initialize in memory database") log.Error().Err(err).Msg("Failed to initialize WebAuthn")
} }
if config.C.IndexEnabled { index.DepreactionIndexDirname()
log.Info().Msg("Index directory: " + filepath.Join(homePath, config.C.IndexDirname)) if index.IndexEnabled() {
index.Init(filepath.Join(homePath, config.C.IndexDirname)) go index.NewIndexer(index.IndexType())
} }
} }
func shutdown() { func shutdown(server *server.Server) {
log.Info().Msg("Shutting down database...") log.Info().Msg("Shutting down database...")
if err := db.Close(); err != nil { if err := db.Close(); err != nil {
log.Error().Err(err).Msg("Failed to close database") log.Error().Err(err).Msg("Failed to close database")
} }
if config.C.IndexEnabled { if index.IndexEnabled() {
log.Info().Msg("Shutting down index...") log.Info().Msg("Shutting down index...")
index.Close() index.Close()
} }
server.Stop()
log.Info().Msg("Shutdown complete") log.Info().Msg("Shutdown complete")
} }

View File

@@ -2,6 +2,7 @@ package config
import ( import (
"fmt" "fmt"
"github.com/thomiceli/opengist/internal/session"
"io" "io"
"net/url" "net/url"
"os" "os"
@@ -14,7 +15,6 @@ import (
"github.com/rs/zerolog" "github.com/rs/zerolog"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
"github.com/thomiceli/opengist/internal/utils"
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
) )
@@ -22,16 +22,26 @@ var OpengistVersion = ""
var C *config var C *config
var SecretKey []byte
// Not using nested structs because the library // Not using nested structs because the library
// doesn't support dot notation in this case sadly // doesn't support dot notation in this case sadly
type config struct { type config struct {
SecretKey string `yaml:"secret-key" env:"OG_SECRET_KEY"`
LogLevel string `yaml:"log-level" env:"OG_LOG_LEVEL"` LogLevel string `yaml:"log-level" env:"OG_LOG_LEVEL"`
LogOutput string `yaml:"log-output" env:"OG_LOG_OUTPUT"` LogOutput string `yaml:"log-output" env:"OG_LOG_OUTPUT"`
ExternalUrl string `yaml:"external-url" env:"OG_EXTERNAL_URL"` ExternalUrl string `yaml:"external-url" env:"OG_EXTERNAL_URL"`
OpengistHome string `yaml:"opengist-home" env:"OG_OPENGIST_HOME"` OpengistHome string `yaml:"opengist-home" env:"OG_OPENGIST_HOME"`
DBFilename string `yaml:"db-filename" env:"OG_DB_FILENAME"`
IndexEnabled bool `yaml:"index.enabled" env:"OG_INDEX_ENABLED"` DBUri string `yaml:"db-uri" env:"OG_DB_URI"`
IndexDirname string `yaml:"index.dirname" env:"OG_INDEX_DIRNAME"` 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"`
GitDefaultBranch string `yaml:"git.default-branch" env:"OG_GIT_DEFAULT_BRANCH"` GitDefaultBranch string `yaml:"git.default-branch" env:"OG_GIT_DEFAULT_BRANCH"`
@@ -41,6 +51,8 @@ type config struct {
HttpPort string `yaml:"http.port" env:"OG_HTTP_PORT"` HttpPort string `yaml:"http.port" env:"OG_HTTP_PORT"`
HttpGit bool `yaml:"http.git-enabled" env:"OG_HTTP_GIT_ENABLED"` HttpGit bool `yaml:"http.git-enabled" env:"OG_HTTP_GIT_ENABLED"`
UnixSocketPermissions string `yaml:"unix-socket-permissions" env:"OG_UNIX_SOCKET_PERMISSIONS"`
SshGit bool `yaml:"ssh.git-enabled" env:"OG_SSH_GIT_ENABLED"` SshGit bool `yaml:"ssh.git-enabled" env:"OG_SSH_GIT_ENABLED"`
SshHost string `yaml:"ssh.host" env:"OG_SSH_HOST"` SshHost string `yaml:"ssh.host" env:"OG_SSH_HOST"`
SshPort string `yaml:"ssh.port" env:"OG_SSH_PORT"` SshPort string `yaml:"ssh.port" env:"OG_SSH_PORT"`
@@ -60,10 +72,22 @@ type config struct {
GiteaUrl string `yaml:"gitea.url" env:"OG_GITEA_URL"` GiteaUrl string `yaml:"gitea.url" env:"OG_GITEA_URL"`
GiteaName string `yaml:"gitea.name" env:"OG_GITEA_NAME"` GiteaName string `yaml:"gitea.name" env:"OG_GITEA_NAME"`
OIDCClientKey string `yaml:"oidc.client-key" env:"OG_OIDC_CLIENT_KEY"` OIDCProviderName string `yaml:"oidc.provider-name" env:"OG_OIDC_PROVIDER_NAME"`
OIDCSecret string `yaml:"oidc.secret" env:"OG_OIDC_SECRET"` OIDCClientKey string `yaml:"oidc.client-key" env:"OG_OIDC_CLIENT_KEY"`
OIDCDiscoveryUrl string `yaml:"oidc.discovery-url" env:"OG_OIDC_DISCOVERY_URL"` OIDCSecret string `yaml:"oidc.secret" env:"OG_OIDC_SECRET"`
OIDCDiscoveryUrl string `yaml:"oidc.discovery-url" env:"OG_OIDC_DISCOVERY_URL"`
OIDCGroupClaimName string `yaml:"oidc.group-claim-name" env:"OG_OIDC_GROUP_CLAIM_NAME"`
OIDCAdminGroup string `yaml:"oidc.admin-group" env:"OG_OIDC_ADMIN_GROUP"`
MetricsEnabled bool `yaml:"metrics.enabled" env:"OG_METRICS_ENABLED"`
LDAPUrl string `yaml:"ldap.url" env:"OG_LDAP_URL"`
LDAPBindDn string `yaml:"ldap.bind-dn" env:"OG_LDAP_BIND_DN"`
LDAPBindCredentials string `yaml:"ldap.bind-credentials" env:"OG_LDAP_BIND_CREDENTIALS"`
LDAPSearchBase string `yaml:"ldap.search-base" env:"OG_LDAP_SEARCH_BASE"`
LDAPSearchFilter string `yaml:"ldap.search-filter" env:"OG_LDAP_SEARCH_FILTER"`
CustomName string `yaml:"custom.name" env:"OG_CUSTOM_NAME"`
CustomLogo string `yaml:"custom.logo" env:"OG_CUSTOM_LOGO"` CustomLogo string `yaml:"custom.logo" env:"OG_CUSTOM_LOGO"`
CustomFavicon string `yaml:"custom.favicon" env:"OG_CUSTOM_FAVICON"` CustomFavicon string `yaml:"custom.favicon" env:"OG_CUSTOM_FAVICON"`
StaticLinks []StaticLink `yaml:"custom.static-links" env:"OG_CUSTOM_STATIC_LINK"` StaticLinks []StaticLink `yaml:"custom.static-links" env:"OG_CUSTOM_STATIC_LINK"`
@@ -77,12 +101,13 @@ type StaticLink struct {
func configWithDefaults() (*config, error) { func configWithDefaults() (*config, error) {
c := &config{} c := &config{}
c.SecretKey = ""
c.LogLevel = "warn" c.LogLevel = "warn"
c.LogOutput = "stdout,file" c.LogOutput = "stdout,file"
c.OpengistHome = "" c.OpengistHome = ""
c.DBFilename = "opengist.db" c.DBUri = "opengist.db"
c.IndexEnabled = true c.Index = "bleve"
c.IndexDirname = "opengist.index"
c.SqliteJournalMode = "WAL" c.SqliteJournalMode = "WAL"
@@ -90,6 +115,8 @@ func configWithDefaults() (*config, error) {
c.HttpPort = "6157" c.HttpPort = "6157"
c.HttpGit = true c.HttpGit = true
c.UnixSocketPermissions = "0666"
c.SshGit = true c.SshGit = true
c.SshHost = "0.0.0.0" c.SshHost = "0.0.0.0"
c.SshPort = "2222" c.SshPort = "2222"
@@ -100,6 +127,8 @@ func configWithDefaults() (*config, error) {
c.GiteaUrl = "https://gitea.com" c.GiteaUrl = "https://gitea.com"
c.GiteaName = "Gitea" c.GiteaName = "Gitea"
c.MetricsEnabled = false
return c, nil return c, nil
} }
@@ -133,6 +162,10 @@ func InitConfig(configPath string, out io.Writer) error {
C = c C = c
if err = migrateConfig(); err != nil {
return err
}
if err = os.Setenv("OG_OPENGIST_HOME_INTERNAL", GetHomeDir()); err != nil { if err = os.Setenv("OG_OPENGIST_HOME_INTERNAL", GetHomeDir()); err != nil {
return err return err
} }
@@ -151,9 +184,9 @@ func InitLog() {
} }
var logWriters []io.Writer var logWriters []io.Writer
logOutputTypes := utils.RemoveDuplicates[string]( logOutputTypes := strings.Split(strings.ToLower(C.LogOutput), ",")
strings.Split(strings.ToLower(C.LogOutput), ","), slices.Sort(logOutputTypes)
) logOutputTypes = slices.Compact(logOutputTypes)
consoleWriter := zerolog.NewConsoleWriter( consoleWriter := zerolog.NewConsoleWriter(
func(w *zerolog.ConsoleWriter) { func(w *zerolog.ConsoleWriter) {
@@ -228,6 +261,15 @@ func GetHomeDir() string {
return filepath.Clean(absolutePath) return filepath.Clean(absolutePath)
} }
func SetupSecretKey() {
if C.SecretKey == "" {
path := filepath.Join(GetHomeDir(), "opengist-secret.key")
SecretKey, _ = session.GenerateSecretKey(path)
} else {
SecretKey = []byte(C.SecretKey)
}
}
func loadConfigFromYaml(c *config, configPath string, out io.Writer) error { func loadConfigFromYaml(c *config, configPath string, out io.Writer) error {
if configPath != "" { if configPath != "" {
absolutePath, _ := filepath.Abs(configPath) absolutePath, _ := filepath.Abs(configPath)

View File

@@ -0,0 +1,42 @@
package config
import (
"fmt"
"os"
"path/filepath"
)
// auto migration for newer versions of Opengist
func migrateConfig() error {
configMigrations := []struct {
Version string
Func func() error
}{
{"1.8.0", v1_8_0},
}
for _, fn := range configMigrations {
err := fn.Func()
if err != nil {
return err
}
}
return nil
}
func v1_8_0() error {
homeDir := GetHomeDir()
moveFile(filepath.Join(filepath.Join(homeDir, "sessions"), "session-auth.key"), filepath.Join(homeDir, "opengist-secret.key"))
return nil
}
func moveFile(oldPath, newPath string) {
if _, err := os.Stat(oldPath); err != nil {
return
}
if err := os.Rename(oldPath, newPath); err == nil {
fmt.Printf("Automatically moved %s to %s\n", oldPath, newPath)
}
}

View File

@@ -5,7 +5,7 @@ import (
) )
type AdminSetting struct { type AdminSetting struct {
Key string `gorm:"uniqueIndex"` Key string `gorm:"index:,unique"`
Value string Value string
} }
@@ -19,7 +19,13 @@ const (
func GetSetting(key string) (string, error) { func GetSetting(key string) (string, error) {
var setting AdminSetting var setting AdminSetting
err := db.Where("key = ?", key).First(&setting).Error var err error
switch db.Dialector.Name() {
case "mysql", "sqlite":
err = db.Where("`key` = ?", key).First(&setting).Error
case "postgres":
err = db.Where("key = ?", key).First(&setting).Error
}
return setting.Value, err return setting.Value, err
} }
@@ -49,7 +55,7 @@ func UpdateSetting(key string, value string) error {
} }
func setSetting(key string, value string) error { func setSetting(key string, value string) error {
return db.Create(&AdminSetting{Key: key, Value: value}).Error return db.FirstOrCreate(&AdminSetting{Key: key, Value: value}, &AdminSetting{Key: key}).Error
} }
func initAdminSettings(settings map[string]string) error { func initAdminSettings(settings map[string]string) error {
@@ -64,9 +70,9 @@ func initAdminSettings(settings map[string]string) error {
return nil return nil
} }
type DBAuthInfo struct{} type AuthInfo struct{}
func (auth DBAuthInfo) RequireLogin() (bool, error) { func (auth AuthInfo) RequireLogin() (bool, error) {
s, err := GetSetting(SettingRequireLogin) s, err := GetSetting(SettingRequireLogin)
if err != nil { if err != nil {
return true, err return true, err
@@ -74,7 +80,7 @@ func (auth DBAuthInfo) RequireLogin() (bool, error) {
return s == "1", nil return s == "1", nil
} }
func (auth DBAuthInfo) AllowGistsWithoutLogin() (bool, error) { func (auth AuthInfo) AllowGistsWithoutLogin() (bool, error) {
s, err := GetSetting(SettingAllowGistsWithoutLogin) s, err := GetSetting(SettingAllowGistsWithoutLogin)
if err != nil { if err != nil {
return false, err return false, err

View File

@@ -2,38 +2,151 @@ package db
import ( import (
"errors" "errors"
"fmt"
"net/url"
"path/filepath"
"slices" "slices"
"strings" "strings"
"time"
msqlite "github.com/glebarez/go-sqlite"
"github.com/glebarez/sqlite" "github.com/glebarez/sqlite"
"gorm.io/driver/mysql"
"gorm.io/driver/postgres"
"gorm.io/gorm/logger"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
"github.com/thomiceli/opengist/internal/config" "github.com/thomiceli/opengist/internal/config"
"gorm.io/gorm" "gorm.io/gorm"
"gorm.io/gorm/logger"
) )
var db *gorm.DB var db *gorm.DB
func Setup(dbPath string, sharedCache bool) error { const (
var err error SQLite databaseType = iota
journalMode := strings.ToUpper(config.C.SqliteJournalMode) PostgreSQL
MySQL
)
if !slices.Contains([]string{"DELETE", "TRUNCATE", "PERSIST", "MEMORY", "WAL", "OFF"}, journalMode) { type databaseType int
log.Warn().Msg("Invalid SQLite journal mode: " + journalMode)
func (d databaseType) String() string {
return [...]string{"SQLite", "PostgreSQL", "MySQL"}[d]
}
type databaseInfo struct {
Type databaseType
Host string
Port string
User string
Password string
Database string
SSLMode string
}
var DatabaseInfo *databaseInfo
func parseDBURI(uri string) (*databaseInfo, error) {
info := &databaseInfo{}
info.SSLMode = "disable"
if uri == ":memory:" {
info.Type = SQLite
info.Database = uri
return info, nil
} }
sharedCacheStr := "" u, err := url.Parse(uri)
if sharedCache { if err != nil {
sharedCacheStr = "&cache=shared" return nil, fmt.Errorf("invalid URI: %v", err)
} }
if db, err = gorm.Open(sqlite.Open(dbPath+"?_fk=true&_journal_mode="+journalMode+sharedCacheStr), &gorm.Config{ if u.Scheme == "" {
Logger: logger.Default.LogMode(logger.Silent), info.Type = SQLite
}); err != nil { info.Database = filepath.Join(config.GetHomeDir(), uri)
return info, nil
}
switch u.Scheme {
case "postgres", "postgresql":
info.Type = PostgreSQL
case "mysql", "mariadb":
info.Type = MySQL
case "file":
info.Type = SQLite
default:
return nil, fmt.Errorf("unknown database: %v", err)
}
if u.Host != "" {
host, port, _ := strings.Cut(u.Host, ":")
info.Host = host
info.Port = port
}
if u.User != nil {
info.User = u.User.Username()
info.Password, _ = u.User.Password()
}
if u.RawQuery != "" {
q, _ := url.ParseQuery(u.RawQuery)
if sslmode := q.Get("sslmode"); sslmode != "" && info.Type == PostgreSQL {
info.SSLMode = sslmode
}
}
switch info.Type {
case PostgreSQL, MySQL:
info.Database = strings.TrimPrefix(u.Path, "/")
case SQLite:
info.Database = u.String()
default:
return nil, fmt.Errorf("unknown database: %v", err)
}
return info, nil
}
func Setup(dbUri string) error {
dbInfo, err := parseDBURI(dbUri)
if err != nil {
return err return err
} }
log.Info().Msgf("Setting up a %s database connection", dbInfo.Type)
var setupFunc func(databaseInfo) error
switch dbInfo.Type {
case SQLite:
setupFunc = setupSQLite
case PostgreSQL:
setupFunc = setupPostgres
case MySQL:
setupFunc = setupMySQL
default:
return fmt.Errorf("unknown database type: %v", dbInfo.Type)
}
maxAttempts := 60
retryInterval := 1 * time.Second
for attempt := 1; attempt <= maxAttempts; attempt++ {
err = setupFunc(*dbInfo)
if err == nil {
log.Info().Msg("Database connection established")
break
}
if attempt < maxAttempts {
log.Warn().Err(err).Msgf("Failed to connect to database (attempt %d), retrying in %v...", attempt, retryInterval)
time.Sleep(retryInterval)
} else {
return err
}
}
DatabaseInfo = dbInfo
if err = db.SetupJoinTable(&Gist{}, "Likes", &Like{}); err != nil { if err = db.SetupJoinTable(&Gist{}, "Likes", &Like{}); err != nil {
return err return err
} }
@@ -42,11 +155,11 @@ func Setup(dbPath string, sharedCache bool) error {
return err return err
} }
if err = db.AutoMigrate(&User{}, &Gist{}, &SSHKey{}, &AdminSetting{}, &Invitation{}); err != nil { if err = db.AutoMigrate(&User{}, &Gist{}, &SSHKey{}, &AdminSetting{}, &Invitation{}, &WebAuthnCredential{}, &TOTP{}, &GistTopic{}, &GistLanguage{}, &GistInitQueue{}); err != nil {
return err return err
} }
if err = ApplyMigrations(db); err != nil { if err = applyMigrations(dbInfo); err != nil {
return err return err
} }
@@ -75,11 +188,7 @@ func CountAll(table interface{}) (int64, error) {
} }
func IsUniqueConstraintViolation(err error) bool { func IsUniqueConstraintViolation(err error) bool {
var sqliteErr *msqlite.Error return errors.Is(err, gorm.ErrDuplicatedKey)
if errors.As(err, &sqliteErr) && sqliteErr.Code() == 2067 {
return true
}
return false
} }
func Ping() error { func Ping() error {
@@ -90,3 +199,75 @@ func Ping() error {
return sql.Ping() return sql.Ping()
} }
func setupSQLite(dbInfo databaseInfo) error {
var err error
var dsn string
journalMode := strings.ToUpper(config.C.SqliteJournalMode)
if !slices.Contains([]string{"DELETE", "TRUNCATE", "PERSIST", "MEMORY", "WAL", "OFF"}, journalMode) {
log.Warn().Msg("Invalid SQLite journal mode: " + journalMode)
}
if dbInfo.Database == ":memory:" {
dsn = ":memory:?_fk=true&cache=shared"
} else {
u, err := url.Parse(dbInfo.Database)
if err != nil {
return err
}
u.Scheme = "file"
q := u.Query()
q.Set("_pragma", "foreign_keys(1)")
q.Set("_journal_mode", journalMode)
u.RawQuery = q.Encode()
dsn = u.String()
}
db, err = gorm.Open(sqlite.Open(dsn), &gorm.Config{
Logger: logger.Default.LogMode(logger.Silent),
TranslateError: true,
})
return err
}
func setupPostgres(dbInfo databaseInfo) error {
var err error
dsn := fmt.Sprintf("host=%s port=%s user=%s password=%s dbname=%s sslmode=%s", dbInfo.Host, dbInfo.Port, dbInfo.User, dbInfo.Password, dbInfo.Database, dbInfo.SSLMode)
db, err = gorm.Open(postgres.Open(dsn), &gorm.Config{
Logger: logger.Default.LogMode(logger.Silent),
TranslateError: true,
})
return err
}
func setupMySQL(dbInfo databaseInfo) error {
var err error
dsn := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=utf8mb4&parseTime=True&loc=Local", dbInfo.User, dbInfo.Password, dbInfo.Host, dbInfo.Port, dbInfo.Database)
db, err = gorm.Open(mysql.New(mysql.Config{
DSN: dsn,
DontSupportRenameIndex: true,
}), &gorm.Config{
Logger: logger.Default.LogMode(logger.Silent),
TranslateError: true,
})
return err
}
func DeprecationDBFilename() {
if config.C.DBFilename != "" {
log.Warn().Msg("The 'db-filename'/'OG_DB_FILENAME' configuration option is deprecated and will be removed in a future version. Please use 'db-uri'/'OG_DB_URI' instead.")
}
if config.C.DBUri == "" {
config.C.DBUri = config.C.DBFilename
}
}
func TruncateDatabase() error {
return db.Migrator().DropTable("likes", &User{}, "gists", &SSHKey{}, &AdminSetting{}, &Invitation{}, &WebAuthnCredential{}, &TOTP{}, &GistTopic{}, &GistLanguage{}, &GistInitQueue{})
}

View File

@@ -4,6 +4,7 @@ import (
"fmt" "fmt"
"os/exec" "os/exec"
"path/filepath" "path/filepath"
"slices"
"strings" "strings"
"time" "time"
@@ -37,6 +38,10 @@ func (v Visibility) String() string {
} }
} }
func (v Visibility) Uint() uint {
return uint(v)
}
func (v Visibility) Next() Visibility { func (v Visibility) Next() Visibility {
switch v { switch v {
case PublicVisibility: case PublicVisibility:
@@ -48,16 +53,16 @@ func (v Visibility) Next() Visibility {
} }
} }
func ParseVisibility[T string | int](v T) (Visibility, error) { func ParseVisibility[T string | int](v T) Visibility {
switch s := fmt.Sprint(v); s { switch s := fmt.Sprint(v); s {
case "0", "public": case "0", "public":
return PublicVisibility, nil return PublicVisibility
case "1", "unlisted": case "1", "unlisted":
return UnlistedVisibility, nil return UnlistedVisibility
case "2", "private": case "2", "private":
return PrivateVisibility, nil return PrivateVisibility
default: default:
return -1, fmt.Errorf("unknown visibility %q", s) return PublicVisibility
} }
} }
@@ -81,6 +86,9 @@ type Gist struct {
Likes []User `gorm:"many2many:likes;constraint:OnUpdate:CASCADE,OnDelete:CASCADE"` Likes []User `gorm:"many2many:likes;constraint:OnUpdate:CASCADE,OnDelete:CASCADE"`
Forked *Gist `gorm:"foreignKey:ForkedID;constraint:OnUpdate:CASCADE,OnDelete:SET NULL"` Forked *Gist `gorm:"foreignKey:ForkedID;constraint:OnUpdate:CASCADE,OnDelete:SET NULL"`
ForkedID uint ForkedID uint
Topics []GistTopic `gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE"`
Languages []GistLanguage `gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE"`
} }
type Like struct { type Like struct {
@@ -100,8 +108,8 @@ func (gist *Gist) BeforeDelete(tx *gorm.DB) error {
func GetGist(user string, gistUuid string) (*Gist, error) { func GetGist(user string, gistUuid string) (*Gist, error) {
gist := new(Gist) gist := new(Gist)
err := db.Preload("User").Preload("Forked.User"). err := db.Preload("User").Preload("Forked.User").Preload("Topics").
Where("(gists.uuid = ? OR gists.url = ?) AND users.username like ?", gistUuid, gistUuid, user). Where("(gists.uuid like ? OR gists.url = ?) AND users.username like ?", gistUuid+"%", gistUuid, user).
Joins("join users on gists.user_id = users.id"). Joins("join users on gists.user_id = users.id").
First(&gist).Error First(&gist).Error
@@ -110,7 +118,7 @@ func GetGist(user string, gistUuid string) (*Gist, error) {
func GetGistByID(gistId string) (*Gist, error) { func GetGistByID(gistId string) (*Gist, error) {
gist := new(Gist) gist := new(Gist)
err := db.Preload("User").Preload("Forked.User"). err := db.Preload("User").Preload("Forked.User").Preload("Topics").
Where("gists.id = ?", gistId). Where("gists.id = ?", gistId).
First(&gist).Error First(&gist).Error
@@ -119,7 +127,9 @@ func GetGistByID(gistId string) (*Gist, error) {
func GetAllGistsForCurrentUser(currentUserId uint, offset int, sort string, order string) ([]*Gist, error) { func GetAllGistsForCurrentUser(currentUserId uint, offset int, sort string, order string) ([]*Gist, error) {
var gists []*Gist var gists []*Gist
err := db.Preload("User").Preload("Forked.User"). err := db.Preload("User").
Preload("Forked.User").
Preload("Topics").
Where("gists.private = 0 or gists.user_id = ?", currentUserId). Where("gists.private = 0 or gists.user_id = ?", currentUserId).
Limit(11). Limit(11).
Offset(offset * 10). Offset(offset * 10).
@@ -140,12 +150,18 @@ func GetAllGists(offset int) ([]*Gist, error) {
return gists, err return gists, err
} }
func GetAllGistsFromSearch(currentUserId uint, query string, offset int, sort string, order string) ([]*Gist, error) { func GetAllGistsFromSearch(currentUserId uint, query string, offset int, sort string, order string, topic string) ([]*Gist, error) {
var gists []*Gist var gists []*Gist
err := db.Preload("User").Preload("Forked.User"). tx := db.Preload("User").Preload("Forked.User").Preload("Topics").
Where("((gists.private = 0) or (gists.private > 0 and gists.user_id = ?))", currentUserId). Where("((gists.private = 0) or (gists.private > 0 and gists.user_id = ?))", currentUserId).
Where("gists.title like ? or gists.description like ?", "%"+query+"%", "%"+query+"%"). Where("gists.title like ? or gists.description like ?", "%"+query+"%", "%"+query+"%")
Limit(11).
if topic != "" {
tx = tx.Joins("join gist_topics on gists.id = gist_topics.gist_id").
Where("gist_topics.topic = ?", topic)
}
err := tx.Limit(11).
Offset(offset * 10). Offset(offset * 10).
Order("gists." + sort + "_at " + order). Order("gists." + sort + "_at " + order).
Find(&gists).Error Find(&gists).Error
@@ -154,20 +170,47 @@ func GetAllGistsFromSearch(currentUserId uint, query string, offset int, sort st
} }
func gistsFromUserStatement(fromUserId uint, currentUserId uint) *gorm.DB { func gistsFromUserStatement(fromUserId uint, currentUserId uint) *gorm.DB {
return db.Preload("User").Preload("Forked.User"). return db.Preload("User").Preload("Forked.User").Preload("Topics").
Where("((gists.private = 0) or (gists.private > 0 and gists.user_id = ?))", currentUserId). Where("((gists.private = 0) or (gists.private > 0 and gists.user_id = ?))", currentUserId).
Where("users.id = ?", fromUserId). Where("users.id = ?", fromUserId).
Joins("join users on gists.user_id = users.id") Joins("join users on gists.user_id = users.id")
} }
func GetAllGistsFromUser(fromUserId uint, currentUserId uint, offset int, sort string, order string) ([]*Gist, error) { func GetAllGistsFromUser(fromUserId uint, currentUserId uint, title string, language string, visibility string, topics []string, offset int, sort string, order string) ([]*Gist, int64, error) {
var gists []*Gist var gists []*Gist
err := gistsFromUserStatement(fromUserId, currentUserId).Limit(11). var count int64
baseQuery := gistsFromUserStatement(fromUserId, currentUserId).Model(&Gist{})
if title != "" {
baseQuery = baseQuery.Where("gists.title like ?", "%"+title+"%")
}
if language != "" {
baseQuery = baseQuery.Joins("join gist_languages on gists.id = gist_languages.gist_id").
Where("gist_languages.language = ?", language)
}
if visibility != "" {
baseQuery = baseQuery.Where("gists.private = ?", ParseVisibility(visibility))
}
if len(topics) > 0 {
baseQuery = baseQuery.Joins("join gist_topics on gists.id = gist_topics.gist_id").
Where("gist_topics.topic in ?", topics)
}
err := baseQuery.Count(&count).Error
if err != nil {
return nil, 0, err
}
err = baseQuery.Limit(11).
Offset(offset * 10). Offset(offset * 10).
Order("gists." + sort + "_at " + order). Order("gists." + sort + "_at " + order).
Find(&gists).Error Find(&gists).Error
return gists, err return gists, count, err
} }
func CountAllGistsFromUser(fromUserId uint, currentUserId uint) (int64, error) { func CountAllGistsFromUser(fromUserId uint, currentUserId uint) (int64, error) {
@@ -177,7 +220,7 @@ func CountAllGistsFromUser(fromUserId uint, currentUserId uint) (int64, error) {
} }
func likedStatement(fromUserId uint, currentUserId uint) *gorm.DB { func likedStatement(fromUserId uint, currentUserId uint) *gorm.DB {
return db.Preload("User").Preload("Forked.User"). return db.Preload("User").Preload("Forked.User").Preload("Topics").
Where("((gists.private = 0) or (gists.private > 0 and gists.user_id = ?))", currentUserId). Where("((gists.private = 0) or (gists.private > 0 and gists.user_id = ?))", currentUserId).
Where("likes.user_id = ?", fromUserId). Where("likes.user_id = ?", fromUserId).
Joins("join likes on gists.id = likes.gist_id"). Joins("join likes on gists.id = likes.gist_id").
@@ -200,7 +243,7 @@ func CountAllGistsLikedByUser(fromUserId uint, currentUserId uint) (int64, error
} }
func forkedStatement(fromUserId uint, currentUserId uint) *gorm.DB { func forkedStatement(fromUserId uint, currentUserId uint) *gorm.DB {
return db.Preload("User").Preload("Forked.User"). return db.Preload("User").Preload("Forked.User").Preload("Topics").
Where("gists.forked_id is not null and ((gists.private = 0) or (gists.private > 0 and gists.user_id = ?))", currentUserId). Where("gists.forked_id is not null and ((gists.private = 0) or (gists.private > 0 and gists.user_id = ?))", currentUserId).
Where("gists.user_id = ?", fromUserId). Where("gists.user_id = ?", fromUserId).
Joins("join users on gists.user_id = users.id") Joins("join users on gists.user_id = users.id")
@@ -242,11 +285,22 @@ func GetAllGistsVisibleByUser(userId uint) ([]uint, error) {
func GetAllGistsByIds(ids []uint) ([]*Gist, error) { func GetAllGistsByIds(ids []uint) ([]*Gist, error) {
var gists []*Gist var gists []*Gist
err := db.Preload("User").Preload("Forked.User"). err := db.Preload("User").Preload("Forked.User").Preload("Topics").
Where("id in ?", ids). Where("id in ?", ids).
Find(&gists).Error Find(&gists).Error
return gists, err // keep order
ordered := make([]*Gist, 0, len(ids))
for _, wantedId := range ids {
for _, gist := range gists {
if gist.ID == wantedId {
ordered = append(ordered, gist)
break
}
}
}
return ordered, err
} }
func (gist *Gist) Create() error { func (gist *Gist) Create() error {
@@ -259,6 +313,12 @@ func (gist *Gist) CreateForked() error {
} }
func (gist *Gist) Update() error { func (gist *Gist) Update() error {
// reset the topics
err := db.Model(&GistTopic{}).Where("gist_id = ?", gist.ID).Delete(&GistTopic{}).Error
if err != nil {
return err
}
return db.Omit("forked_id").Save(&gist).Error return db.Omit("forked_id").Save(&gist).Error
} }
@@ -358,12 +418,20 @@ func (gist *Gist) Files(revision string, truncate bool) ([]*git.File, error) {
var files []*git.File var files []*git.File
for _, fileCat := range filesCat { for _, fileCat := range filesCat {
var shortContent string
if len(fileCat.Content) > 512 {
shortContent = fileCat.Content[:512]
} else {
shortContent = fileCat.Content
}
files = append(files, &git.File{ files = append(files, &git.File{
Filename: fileCat.Name, Filename: fileCat.Name,
Size: fileCat.Size, Size: fileCat.Size,
HumanSize: humanize.IBytes(fileCat.Size), HumanSize: humanize.IBytes(fileCat.Size),
Content: fileCat.Content, Content: fileCat.Content,
Truncated: fileCat.Truncated, Truncated: fileCat.Truncated,
MimeType: git.DetectMimeType([]byte(shortContent)),
}) })
} }
return files, err return files, err
@@ -384,12 +452,20 @@ func (gist *Gist) File(revision string, filename string, truncate bool) (*git.Fi
return nil, err return nil, err
} }
var shortContent string
if len(content) > 512 {
shortContent = content[:512]
} else {
shortContent = content
}
return &git.File{ return &git.File{
Filename: filename, Filename: filename,
Size: size, Size: size,
HumanSize: humanize.IBytes(size), HumanSize: humanize.IBytes(size),
Content: content, Content: content,
Truncated: truncated, Truncated: truncated,
MimeType: git.DetectMimeType([]byte(shortContent)),
}, err }, err
} }
@@ -411,8 +487,14 @@ func (gist *Gist) AddAndCommitFiles(files *[]FileDTO) error {
} }
for _, file := range *files { for _, file := range *files {
if err := git.SetFileContent(gist.Uuid, file.Filename, file.Content); err != nil { if file.SourcePath != "" { // if it's an uploaded file
return err if err := git.MoveFileToRepository(gist.Uuid, file.Filename, file.SourcePath); err != nil {
return err
}
} else { // else it's a text editor file
if err := git.SetFileContent(gist.Uuid, file.Filename, file.Content); err != nil {
return err
}
} }
} }
@@ -470,19 +552,28 @@ func (gist *Gist) UpdatePreviewAndCount(withTimestampUpdate bool) error {
gist.Preview = "" gist.Preview = ""
gist.PreviewFilename = "" gist.PreviewFilename = ""
} else { } else {
file, err := gist.File("HEAD", filesStr[0], true) for _, fileStr := range filesStr {
if err != nil { file, err := gist.File("HEAD", fileStr, true)
return err if err != nil {
} return err
}
if file == nil {
continue
}
gist.Preview = ""
gist.PreviewFilename = file.Filename
split := strings.Split(file.Content, "\n") if !file.MimeType.CanBeEdited() {
if len(split) > 10 { continue
gist.Preview = strings.Join(split[:10], "\n") }
} else {
gist.Preview = file.Content
}
gist.PreviewFilename = file.Filename split := strings.Split(file.Content, "\n")
if len(split) > 10 {
gist.Preview = strings.Join(split[:10], "\n")
} else {
gist.Preview = file.Content
}
}
} }
if withTimestampUpdate { if withTimestampUpdate {
@@ -535,6 +626,94 @@ func (gist *Gist) GetLanguagesFromFiles() ([]string, error) {
return languages, nil return languages, nil
} }
func (gist *Gist) GetTopics() ([]string, error) {
var topics []string
err := db.Model(&GistTopic{}).
Where("gist_id = ?", gist.ID).
Pluck("topic", &topics).Error
return topics, err
}
func (gist *Gist) TopicsSlice() []string {
topics := make([]string, 0, len(gist.Topics))
for _, topic := range gist.Topics {
topics = append(topics, topic.Topic)
}
return topics
}
func (gist *Gist) UpdateLanguages() {
languages, err := gist.GetLanguagesFromFiles()
if err != nil {
log.Error().Err(err).Msgf("Cannot get languages for gist %d", gist.ID)
return
}
slices.Sort(languages)
languages = slices.Compact(languages)
tx := db.Begin()
if tx.Error != nil {
log.Error().Err(tx.Error).Msgf("Cannot start transaction for gist %d", gist.ID)
return
}
if err := tx.Where("gist_id = ?", gist.ID).Delete(&GistLanguage{}).Error; err != nil {
tx.Rollback()
log.Error().Err(err).Msgf("Cannot delete languages for gist %d", gist.ID)
return
}
for _, language := range languages {
gistLanguage := &GistLanguage{
GistID: gist.ID,
Language: language,
}
if err := tx.Create(gistLanguage).Error; err != nil {
tx.Rollback()
log.Error().Err(err).Msgf("Cannot create gist language %s for gist %d", language, gist.ID)
return
}
}
if err := tx.Commit().Error; err != nil {
tx.Rollback()
log.Error().Err(err).Msgf("Cannot commit transaction for gist %d", gist.ID)
return
}
}
func (gist *Gist) ToDTO() (*GistDTO, error) {
files, err := gist.Files("HEAD", false)
if err != nil {
return nil, err
}
fileDTOs := make([]FileDTO, 0, len(files))
for _, file := range files {
f := FileDTO{
Filename: file.Filename,
}
if file.MimeType.CanBeEdited() {
f.Content = file.Content
} else {
f.Binary = true
}
fileDTOs = append(fileDTOs, f)
}
return &GistDTO{
Title: gist.Title,
Description: gist.Description,
URL: gist.URL,
Files: fileDTOs,
VisibilityDTO: VisibilityDTO{
Private: gist.Private,
},
Topics: strings.Join(gist.TopicsSlice(), " "),
}, nil
}
// -- DTO -- // // -- DTO -- //
type GistDTO struct { type GistDTO struct {
@@ -544,16 +723,23 @@ type GistDTO struct {
Files []FileDTO `validate:"min=1,dive"` Files []FileDTO `validate:"min=1,dive"`
Name []string `form:"name"` Name []string `form:"name"`
Content []string `form:"content"` Content []string `form:"content"`
Topics string `validate:"gisttopics" form:"topics"`
VisibilityDTO VisibilityDTO
} }
func (dto *GistDTO) HasMetadata() bool {
return dto.Title != "" || dto.Description != "" || dto.URL != "" || dto.Topics != ""
}
type VisibilityDTO struct { type VisibilityDTO struct {
Private Visibility `validate:"number,min=0,max=2" form:"private"` Private Visibility `validate:"number,min=0,max=2" form:"private"`
} }
type FileDTO struct { type FileDTO struct {
Filename string `validate:"excludes=\x2f,excludes=\x5c,max=255"` Filename string `validate:"excludes=\x2f,excludes=\x5c,max=255"`
Content string `validate:"required"` Content string
Binary bool
SourcePath string // Path to uploaded file, used instead of Content when present
} }
func (dto *GistDTO) ToGist() *Gist { func (dto *GistDTO) ToGist() *Gist {
@@ -562,6 +748,7 @@ func (dto *GistDTO) ToGist() *Gist {
Description: dto.Description, Description: dto.Description,
Private: dto.Private, Private: dto.Private,
URL: dto.URL, URL: dto.URL,
Topics: dto.TopicStrToSlice(),
} }
} }
@@ -569,9 +756,19 @@ func (dto *GistDTO) ToExistingGist(gist *Gist) *Gist {
gist.Title = dto.Title gist.Title = dto.Title
gist.Description = dto.Description gist.Description = dto.Description
gist.URL = dto.URL gist.URL = dto.URL
gist.Topics = dto.TopicStrToSlice()
return gist return gist
} }
func (dto *GistDTO) TopicStrToSlice() []GistTopic {
topics := strings.Fields(dto.Topics)
gistTopics := make([]GistTopic, 0, len(topics))
for _, topic := range topics {
gistTopics = append(gistTopics, GistTopic{Topic: topic})
}
return gistTopics
}
// -- Index -- // // -- Index -- //
func (gist *Gist) ToIndexedGist() (*index.Gist, error) { func (gist *Gist) ToIndexedGist() (*index.Gist, error) {
@@ -584,6 +781,9 @@ func (gist *Gist) ToIndexedGist() (*index.Gist, error) {
wholeContent := "" wholeContent := ""
for _, file := range files { for _, file := range files {
wholeContent += file.Content wholeContent += file.Content
if !strings.HasSuffix(wholeContent, "\n") {
wholeContent += "\n"
}
exts = append(exts, filepath.Ext(file.Filename)) exts = append(exts, filepath.Ext(file.Filename))
} }
@@ -597,14 +797,22 @@ func (gist *Gist) ToIndexedGist() (*index.Gist, error) {
return nil, err return nil, err
} }
topics, err := gist.GetTopics()
if err != nil {
return nil, err
}
indexedGist := &index.Gist{ indexedGist := &index.Gist{
GistID: gist.ID, GistID: gist.ID,
UserID: gist.UserID,
Visibility: gist.Private.Uint(),
Username: gist.User.Username, Username: gist.User.Username,
Title: gist.Title, Title: gist.Title,
Content: wholeContent, Content: wholeContent,
Filenames: fileNames, Filenames: fileNames,
Extensions: exts, Extensions: exts,
Languages: langs, Languages: langs,
Topics: topics,
CreatedAt: gist.CreatedAt, CreatedAt: gist.CreatedAt,
UpdatedAt: gist.UpdatedAt, UpdatedAt: gist.UpdatedAt,
} }
@@ -613,7 +821,7 @@ func (gist *Gist) ToIndexedGist() (*index.Gist, error) {
} }
func (gist *Gist) AddInIndex() { func (gist *Gist) AddInIndex() {
if !index.Enabled() { if !index.IndexEnabled() {
return return
} }
@@ -631,7 +839,7 @@ func (gist *Gist) AddInIndex() {
} }
func (gist *Gist) RemoveFromIndex() { func (gist *Gist) RemoveFromIndex() {
if !index.Enabled() { if !index.IndexEnabled() {
return return
} }

View File

@@ -0,0 +1,34 @@
package db
type GistInitQueue struct {
GistID uint `gorm:"primaryKey"`
Gist Gist `gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE;foreignKey:GistID"`
UserID uint `gorm:"primaryKey"`
User User `gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE;foreignKey:UserID"`
}
func GetInitGistInQueueForUser(userID uint) (*Gist, error) {
queue := new(GistInitQueue)
err := db.Preload("Gist").Preload("Gist.User").
Where("user_id = ?", userID).
Order("gist_id asc").
First(&queue).Error
if err != nil {
return nil, err
}
err = db.Delete(&queue).Error
if err != nil {
return nil, err
}
return &queue.Gist, nil
}
func AddInitGistToQueue(gistID uint, userID uint) error {
queue := &GistInitQueue{
GistID: gistID,
UserID: userID,
}
return db.Create(&queue).Error
}

View File

@@ -0,0 +1,29 @@
package db
type GistLanguage struct {
GistID uint `gorm:"primaryKey"`
Language string `gorm:"primaryKey;size:100"`
}
func GetGistLanguagesForUser(fromUserId, currentUserId uint) ([]struct {
Language string
Count int64
}, error) {
var results []struct {
Language string
Count int64
}
err := db.Model(&GistLanguage{}).
Select("language, count(*) as count").
Joins("JOIN gists ON gists.id = gist_languages.gist_id").
Joins("JOIN users ON gists.user_id = users.id").
Where("((gists.private = 0) or (gists.private > 0 and gists.user_id = ?))", currentUserId).
Where("users.id = ?", fromUserId).
Group("language").
Order("count DESC").
Limit(15).
Find(&results).Error
return results, err
}

View File

@@ -0,0 +1,6 @@
package db
type GistTopic struct {
GistID uint `gorm:"primaryKey"`
Topic string `gorm:"primaryKey;size:50"`
}

View File

@@ -1,6 +1,7 @@
package db package db
import ( import (
"fmt"
"math/rand" "math/rand"
"time" "time"
) )
@@ -15,10 +16,21 @@ type Invitation struct {
func GetAllInvitations() ([]*Invitation, error) { func GetAllInvitations() ([]*Invitation, error) {
var invitations []*Invitation var invitations []*Invitation
err := db. dialect := db.Dialector.Name()
Order("(((expires_at >= strftime('%s', 'now')) AND ((nb_max <= 0) OR (nb_used < nb_max)))) desc"). query := db.Model(&Invitation{})
Order("id asc").
Find(&invitations).Error switch dialect {
case "sqlite":
query = query.Order("(((expires_at >= strftime('%s', 'now')) AND ((nb_max <= 0) OR (nb_used < nb_max)))) DESC")
case "postgres":
query = query.Order("(((expires_at >= EXTRACT(EPOCH FROM CURRENT_TIMESTAMP)) AND ((nb_max <= 0) OR (nb_used < nb_max)))) DESC")
case "mysql":
query = query.Order("(((expires_at >= UNIX_TIMESTAMP()) AND ((nb_max <= 0) OR (nb_used < nb_max)))) DESC")
default:
return nil, fmt.Errorf("unsupported database dialect: %s", dialect)
}
err := query.Order("id ASC").Find(&invitations).Error
return invitations, err return invitations, err
} }

View File

@@ -3,7 +3,6 @@ package db
import ( import (
"fmt" "fmt"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
"gorm.io/gorm"
) )
type MigrationVersion struct { type MigrationVersion struct {
@@ -11,7 +10,19 @@ type MigrationVersion struct {
Version uint Version uint
} }
func ApplyMigrations(db *gorm.DB) error { func applyMigrations(dbInfo *databaseInfo) error {
switch dbInfo.Type {
case SQLite:
return applySqliteMigrations()
case PostgreSQL, MySQL:
return nil
default:
return fmt.Errorf("unknown database type: %s", dbInfo.Type)
}
}
func applySqliteMigrations() error {
// Create migration table if it doesn't exist // Create migration table if it doesn't exist
if err := db.AutoMigrate(&MigrationVersion{}); err != nil { if err := db.AutoMigrate(&MigrationVersion{}); err != nil {
log.Fatal().Err(err).Msg("Error creating migration version table") log.Fatal().Err(err).Msg("Error creating migration version table")
@@ -25,7 +36,7 @@ func ApplyMigrations(db *gorm.DB) error {
// Define migrations // Define migrations
migrations := []struct { migrations := []struct {
Version uint Version uint
Func func(*gorm.DB) error Func func() error
}{ }{
{1, v1_modifyConstraintToSSHKeys}, {1, v1_modifyConstraintToSSHKeys},
{2, v2_lowercaseEmails}, {2, v2_lowercaseEmails},
@@ -41,7 +52,7 @@ func ApplyMigrations(db *gorm.DB) error {
return err return err
} }
if err := m.Func(db); err != nil { if err := m.Func(); err != nil {
log.Fatal().Err(err).Msg(fmt.Sprintf("Error applying migration %d:", m.Version)) log.Fatal().Err(err).Msg(fmt.Sprintf("Error applying migration %d:", m.Version))
tx.Rollback() tx.Rollback()
return err return err
@@ -61,7 +72,7 @@ func ApplyMigrations(db *gorm.DB) error {
} }
// Modify the constraint on the ssh_keys table to use ON DELETE CASCADE // Modify the constraint on the ssh_keys table to use ON DELETE CASCADE
func v1_modifyConstraintToSSHKeys(db *gorm.DB) error { func v1_modifyConstraintToSSHKeys() error {
createSQL := ` createSQL := `
CREATE TABLE ssh_keys_temp ( CREATE TABLE ssh_keys_temp (
id integer primary key, id integer primary key,
@@ -96,7 +107,7 @@ func v1_modifyConstraintToSSHKeys(db *gorm.DB) error {
return db.Exec(renameSQL).Error return db.Exec(renameSQL).Error
} }
func v2_lowercaseEmails(db *gorm.DB) error { func v2_lowercaseEmails() error {
// Copy the lowercase emails into the new column // Copy the lowercase emails into the new column
copySQL := `UPDATE users SET email = lower(email);` copySQL := `UPDATE users SET email = lower(email);`
return db.Exec(copySQL).Error return db.Exec(copySQL).Error

123
internal/db/totp.go Normal file
View File

@@ -0,0 +1,123 @@
package db
import (
"crypto/rand"
"encoding/base64"
"encoding/hex"
"encoding/json"
"fmt"
"slices"
"github.com/thomiceli/opengist/internal/auth/password"
ogtotp "github.com/thomiceli/opengist/internal/auth/totp"
"github.com/thomiceli/opengist/internal/config"
)
type TOTP struct {
ID uint `gorm:"primaryKey"`
UserID uint `gorm:"uniqueIndex"`
User User
Secret string
RecoveryCodes jsonData `gorm:"type:json"`
CreatedAt int64
LastUsedAt int64
}
func GetTOTPByUserID(userID uint) (*TOTP, error) {
var totp TOTP
err := db.Where("user_id = ?", userID).First(&totp).Error
return &totp, err
}
func (totp *TOTP) StoreSecret(secret string) error {
secretBytes := []byte(secret)
encrypted, err := ogtotp.AESEncrypt(config.SecretKey, secretBytes)
if err != nil {
return err
}
totp.Secret = base64.URLEncoding.EncodeToString(encrypted)
return nil
}
func (totp *TOTP) ValidateCode(code string) (bool, error) {
ciphertext, err := base64.URLEncoding.DecodeString(totp.Secret)
if err != nil {
return false, err
}
secretBytes, err := ogtotp.AESDecrypt(config.SecretKey, ciphertext)
if err != nil {
return false, err
}
return ogtotp.Validate(code, string(secretBytes)), nil
}
func (totp *TOTP) ValidateRecoveryCode(code string) (bool, error) {
var hashedCodes []string
if err := json.Unmarshal(totp.RecoveryCodes, &hashedCodes); err != nil {
return false, err
}
for i, hashedCode := range hashedCodes {
ok, err := password.VerifyPassword(code, hashedCode)
if err != nil {
return false, err
}
if ok {
codesJson, _ := json.Marshal(slices.Delete(hashedCodes, i, i+1))
totp.RecoveryCodes = codesJson
return true, db.Model(&totp).Updates(TOTP{RecoveryCodes: codesJson}).Error
}
}
return false, nil
}
func (totp *TOTP) GenerateRecoveryCodes() ([]string, error) {
codes, plainCodes, err := generateRandomCodes()
if err != nil {
return nil, err
}
codesJson, _ := json.Marshal(codes)
totp.RecoveryCodes = codesJson
return plainCodes, db.Model(&totp).Updates(TOTP{RecoveryCodes: codesJson}).Error
}
func (totp *TOTP) Create() error {
return db.Create(&totp).Error
}
func (totp *TOTP) Delete() error {
return db.Delete(&totp).Error
}
func generateRandomCodes() ([]string, []string, error) {
const count = 5
const length = 10
codes := make([]string, count)
plainCodes := make([]string, count)
for i := 0; i < count; i++ {
bytes := make([]byte, (length+1)/2)
if _, err := rand.Read(bytes); err != nil {
return nil, nil, err
}
hexCode := hex.EncodeToString(bytes)
code := fmt.Sprintf("%s-%s", hexCode[:length/2], hexCode[length/2:])
plainCodes[i] = code
hashed, err := password.HashPassword(code)
if err != nil {
return nil, nil, err
}
codes[i] = hashed
}
return codes, plainCodes, nil
}
// -- DTO -- //
type TOTPDTO struct {
Code string `form:"code" validate:"max=50"`
}

77
internal/db/types.go Normal file
View File

@@ -0,0 +1,77 @@
package db
import (
"database/sql/driver"
"encoding/json"
"errors"
"fmt"
"gorm.io/gorm"
"gorm.io/gorm/schema"
)
type binaryData []byte
func (b *binaryData) Value() (driver.Value, error) {
return []byte(*b), nil
}
func (b *binaryData) Scan(value interface{}) error {
valBytes, ok := value.([]byte)
if !ok {
return fmt.Errorf("failed to unmarshal BinaryData: %v", value)
}
*b = valBytes
return nil
}
func (*binaryData) GormDataType() string {
return "binary_data"
}
func (*binaryData) GormDBDataType(db *gorm.DB, _ *schema.Field) string {
switch db.Dialector.Name() {
case "sqlite":
return "BLOB"
case "mysql":
return "VARBINARY(1024)"
case "postgres":
return "BYTEA"
default:
return "BLOB"
}
}
type jsonData json.RawMessage
func (j *jsonData) Scan(value interface{}) error {
bytes, ok := value.([]byte)
if !ok {
return errors.New(fmt.Sprint("Failed to unmarshal JSONB value:", value))
}
result := json.RawMessage{}
err := json.Unmarshal(bytes, &result)
*j = jsonData(result)
return err
}
func (j *jsonData) Value() (driver.Value, error) {
if len(*j) == 0 {
return nil, nil
}
return json.RawMessage(*j).MarshalJSON()
}
func (*jsonData) GormDataType() string {
return "json"
}
func (*jsonData) GormDBDataType(db *gorm.DB, _ *schema.Field) string {
switch db.Dialector.Name() {
case "mysql", "sqlite":
return "JSON"
case "postgres":
return "JSONB"
}
return ""
}

View File

@@ -1,54 +1,63 @@
package db package db
import ( import (
"encoding/json"
"github.com/thomiceli/opengist/internal/git"
"gorm.io/gorm" "gorm.io/gorm"
) )
type User struct { type User struct {
ID uint `gorm:"primaryKey"` ID uint `gorm:"primaryKey"`
Username string `gorm:"uniqueIndex"` Username string `gorm:"uniqueIndex,size:191"`
Password string Password string
IsAdmin bool IsAdmin bool
CreatedAt int64 CreatedAt int64
Email string Email string
MD5Hash string // for gravatar, if no Email is specified, the value is random MD5Hash string // for gravatar, if no Email is specified, the value is random
AvatarURL string AvatarURL string
GithubID string GithubID string
GitlabID string GitlabID string
GiteaID string GiteaID string
OIDCID string `gorm:"column:oidc_id"` OIDCID string `gorm:"column:oidc_id"`
StylePreferences string
Gists []Gist `gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE;foreignKey:UserID"` Gists []Gist `gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE;foreignKey:UserID"`
SSHKeys []SSHKey `gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE;foreignKey:UserID"` SSHKeys []SSHKey `gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE;foreignKey:UserID"`
Liked []Gist `gorm:"many2many:likes;constraint:OnUpdate:CASCADE,OnDelete:CASCADE"` Liked []Gist `gorm:"many2many:likes;constraint:OnUpdate:CASCADE,OnDelete:CASCADE"`
WebAuthnCredentials []WebAuthnCredential `gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE;foreignKey:UserID"`
} }
func (user *User) BeforeDelete(tx *gorm.DB) error { func (user *User) BeforeDelete(tx *gorm.DB) error {
// Decrement likes counter for all gists liked by this user // Decrement likes counter using derived table
// The likes will be automatically deleted by the foreign key constraint err := tx.Exec(`
err := tx.Model(&Gist{}). UPDATE gists
Omit("updated_at"). SET nb_likes = nb_likes - 1
Where("id IN (?)", tx. WHERE id IN (
Select("gist_id"). SELECT gist_id
Table("likes"). FROM (
Where("user_id = ?", user.ID), SELECT gist_id
). FROM likes
UpdateColumn("nb_likes", gorm.Expr("nb_likes - 1")). WHERE user_id = ?
Error ) AS derived_likes
)
`, user.ID).Error
if err != nil { if err != nil {
return err return err
} }
// Decrement forks counter for all gists forked by this user // Decrement forks counter using derived table
err = tx.Model(&Gist{}). err = tx.Exec(`
Omit("updated_at"). UPDATE gists
Where("id IN (?)", tx. SET nb_forks = nb_forks - 1
Select("forked_id"). WHERE id IN (
Table("gists"). SELECT forked_id
Where("user_id = ?", user.ID), FROM (
). SELECT forked_id
UpdateColumn("nb_forks", gorm.Expr("nb_forks - 1")). FROM gists
Error WHERE user_id = ? AND forked_id IS NOT NULL
) AS derived_forks
)
`, user.ID).Error
if err != nil { if err != nil {
return err return err
} }
@@ -58,8 +67,22 @@ func (user *User) BeforeDelete(tx *gorm.DB) error {
return err return err
} }
// Delete all gists created by this user err = tx.Where("user_id = ?", user.ID).Delete(&WebAuthnCredential{}).Error
return tx.Where("user_id = ?", user.ID).Delete(&Gist{}).Error if err != nil {
return err
}
err = tx.Where("user_id = ?", user.ID).Delete(&Gist{}).Error
if err != nil {
return err
}
// Delete user directory
if err = git.DeleteUserDirectory(user.Username); err != nil {
return err
}
return nil
} }
func UserExists(username string) (bool, error) { func UserExists(username string) (bool, error) {
@@ -200,6 +223,28 @@ func (user *User) DeleteProviderID(provider string) error {
return nil return nil
} }
func (user *User) HasMFA() (bool, bool, error) {
var webauthn bool
var totp bool
err := db.Model(&WebAuthnCredential{}).Select("count(*) > 0").Where("user_id = ?", user.ID).Find(&webauthn).Error
if err != nil {
return false, false, err
}
err = db.Model(&TOTP{}).Select("count(*) > 0").Where("user_id = ?", user.ID).Find(&totp).Error
return webauthn, totp, err
}
func (user *User) GetStyle() *UserStyleDTO {
style := new(UserStyleDTO)
err := json.Unmarshal([]byte(user.StylePreferences), style)
if err != nil {
return nil
}
return style
}
// -- DTO -- // // -- DTO -- //
type UserDTO struct { type UserDTO struct {
@@ -213,3 +258,22 @@ func (dto *UserDTO) ToUser() *User {
Password: dto.Password, Password: dto.Password,
} }
} }
type UserUsernameDTO struct {
Username string `form:"username" validate:"required,max=24,alphanumdash,notreserved"`
}
type UserStyleDTO struct {
SoftWrap bool `form:"softwrap" json:"soft_wrap"`
RemovedLineColor string `form:"removedlinecolor" json:"removed_line_color" validate:"min=0,max=7"`
AddedLineColor string `form:"addedlinecolor" json:"added_line_color" validate:"min=0,max=7"`
GitLineColor string `form:"gitlinecolor" json:"git_line_color" validate:"min=0,max=7"`
}
func (dto *UserStyleDTO) ToJson() string {
data, err := json.Marshal(dto)
if err != nil {
return "{}"
}
return string(data)
}

View File

@@ -0,0 +1,147 @@
package db
import (
"encoding/hex"
"github.com/go-webauthn/webauthn/webauthn"
"time"
)
type WebAuthnCredential struct {
ID uint `gorm:"primaryKey"`
Name string
UserID uint
User User
CredentialID binaryData `gorm:"type:binary_data"`
PublicKey binaryData `gorm:"type:binary_data"`
AttestationType string
AAGUID binaryData `gorm:"type:binary_data"`
SignCount uint32
CloneWarning bool
FlagUserPresent bool
FlagUserVerified bool
FlagBackupEligible bool
FlagBackupState bool
CreatedAt int64
LastUsedAt int64
}
func (*WebAuthnCredential) TableName() string {
return "webauthn"
}
func GetAllWACredentialsForUser(userID uint) ([]webauthn.Credential, error) {
var creds []WebAuthnCredential
err := db.Where("user_id = ?", userID).Find(&creds).Error
if err != nil {
return nil, err
}
webCreds := make([]webauthn.Credential, len(creds))
for i, cred := range creds {
webCreds[i] = webauthn.Credential{
ID: cred.CredentialID,
PublicKey: cred.PublicKey,
AttestationType: cred.AttestationType,
Authenticator: webauthn.Authenticator{
AAGUID: cred.AAGUID,
SignCount: cred.SignCount,
CloneWarning: cred.CloneWarning,
},
Flags: webauthn.CredentialFlags{
UserPresent: cred.FlagUserPresent,
UserVerified: cred.FlagUserVerified,
BackupEligible: cred.FlagBackupEligible,
BackupState: cred.FlagBackupState,
},
}
}
return webCreds, nil
}
func GetAllCredentialsForUser(userID uint) ([]WebAuthnCredential, error) {
var creds []WebAuthnCredential
err := db.Where("user_id = ?", userID).Find(&creds).Error
return creds, err
}
func GetUserByCredentialID(credID binaryData) (*User, error) {
var credential WebAuthnCredential
var err error
switch db.Dialector.Name() {
case "postgres":
hexCredID := hex.EncodeToString(credID)
if err = db.Preload("User").Where("credential_id = decode(?, 'hex')", hexCredID).First(&credential).Error; err != nil {
return nil, err
}
case "mysql", "sqlite":
hexCredID := hex.EncodeToString(credID)
if err = db.Preload("User").Where("credential_id = unhex(?)", hexCredID).First(&credential).Error; err != nil {
return nil, err
}
}
return &credential.User, err
}
func GetCredentialByIDDB(id uint) (*WebAuthnCredential, error) {
var cred WebAuthnCredential
err := db.Where("id = ?", id).First(&cred).Error
return &cred, err
}
func GetCredentialByID(id binaryData) (*WebAuthnCredential, error) {
var cred WebAuthnCredential
var err error
switch db.Dialector.Name() {
case "postgres":
hexCredID := hex.EncodeToString(id)
if err = db.Where("credential_id = decode(?, 'hex')", hexCredID).First(&cred).Error; err != nil {
return nil, err
}
case "mysql", "sqlite":
hexCredID := hex.EncodeToString(id)
if err = db.Where("credential_id = unhex(?)", hexCredID).First(&cred).Error; err != nil {
return nil, err
}
}
return &cred, err
}
func CreateFromCrendential(userID uint, name string, cred *webauthn.Credential) (*WebAuthnCredential, error) {
credDb := &WebAuthnCredential{
UserID: userID,
Name: name,
CredentialID: cred.ID,
PublicKey: cred.PublicKey,
AttestationType: cred.AttestationType,
AAGUID: cred.Authenticator.AAGUID,
SignCount: cred.Authenticator.SignCount,
CloneWarning: cred.Authenticator.CloneWarning,
FlagUserPresent: cred.Flags.UserPresent,
FlagUserVerified: cred.Flags.UserVerified,
FlagBackupEligible: cred.Flags.BackupEligible,
FlagBackupState: cred.Flags.BackupState,
}
err := db.Create(credDb).Error
return credDb, err
}
func (w *WebAuthnCredential) UpdateSignCount() error {
return db.Model(w).Update("sign_count", w.SignCount).Error
}
func (w *WebAuthnCredential) UpdateLastUsedAt() error {
return db.Model(w).Update("last_used_at", time.Now().Unix()).Error
}
func (w *WebAuthnCredential) Delete() error {
return db.Delete(w).Error
}
// -- DTO -- //
type CrendentialDTO struct {
PasskeyName string `json:"passkeyname" validate:"max=50"`
}

View File

@@ -6,6 +6,7 @@ import (
"context" "context"
"fmt" "fmt"
"io" "io"
"net/url"
"os" "os"
"os/exec" "os/exec"
"path" "path"
@@ -37,6 +38,10 @@ func RepositoryPath(user string, gist string) string {
return filepath.Join(config.GetHomeDir(), ReposDirectory, strings.ToLower(user), gist) return filepath.Join(config.GetHomeDir(), ReposDirectory, strings.ToLower(user), gist)
} }
func UserRepositoriesPath(user string) string {
return filepath.Join(config.GetHomeDir(), ReposDirectory, strings.ToLower(user))
}
func RepositoryUrl(ctx echo.Context, user string, gist string) string { func RepositoryUrl(ctx echo.Context, user string, gist string) string {
httpProtocol := "http" httpProtocol := "http"
if ctx.Request().TLS != nil || ctx.Request().Header.Get("X-Forwarded-Proto") == "https" { if ctx.Request().TLS != nil || ctx.Request().Header.Get("X-Forwarded-Proto") == "https" {
@@ -115,6 +120,9 @@ func GetFilesOfRepository(user string, gist string, revision string) ([]string,
} }
slice := strings.Split(string(stdout), "\n") slice := strings.Split(string(stdout), "\n")
for i, s := range slice {
slice[i] = convertOctalToUTF8(s)
}
return slice[:len(slice)-1], nil return slice[:len(slice)-1], nil
} }
@@ -153,7 +161,7 @@ func CatFileBatch(user string, gist string, revision string, truncate bool) ([]*
fileMap = append(fileMap, &catFileBatch{ fileMap = append(fileMap, &catFileBatch{
Hash: hash, Hash: hash,
Size: size, Size: size,
Name: name, Name: convertOctalToUTF8(name),
}) })
} }
@@ -194,6 +202,11 @@ func CatFileBatch(user string, gist string, revision string, truncate bool) ([]*
return nil, err return nil, err
} }
// Don't truncate Jupyter notebooks
if strings.HasSuffix(file.Name, ".ipynb") {
truncate = false
}
sizeToRead := size sizeToRead := size
if truncate && sizeToRead > truncateLimit { if truncate && sizeToRead > truncateLimit {
sizeToRead = truncateLimit sizeToRead = truncateLimit
@@ -249,7 +262,7 @@ func GetFileContent(user string, gist string, revision string, filename string,
"git", "git",
"--no-pager", "--no-pager",
"show", "show",
revision+":"+filename, revision+":"+convertURLToOctal(filename),
) )
cmd.Dir = repositoryPath cmd.Dir = repositoryPath
@@ -273,7 +286,7 @@ func GetFileSize(user string, gist string, revision string, filename string) (ui
"git", "git",
"cat-file", "cat-file",
"-s", "-s",
revision+":"+filename, revision+":"+convertURLToOctal(filename),
) )
cmd.Dir = repositoryPath cmd.Dir = repositoryPath
@@ -372,6 +385,17 @@ func SetFileContent(gistTmpId string, filename string, content string) error {
return os.WriteFile(filepath.Join(repositoryPath, filename), []byte(content), 0644) return os.WriteFile(filepath.Join(repositoryPath, filename), []byte(content), 0644)
} }
func MoveFileToRepository(gistTmpId string, filename string, sourcePath string) error {
repositoryPath := TmpRepositoryPath(gistTmpId)
destPath := filepath.Join(repositoryPath, filename)
if err := os.MkdirAll(filepath.Dir(destPath), 0755); err != nil {
return err
}
return os.Rename(sourcePath, destPath)
}
func AddAll(gistTmpId string) error { func AddAll(gistTmpId string) error {
tmpPath := TmpRepositoryPath(gistTmpId) tmpPath := TmpRepositoryPath(gistTmpId)
@@ -481,6 +505,22 @@ func GcRepos() error {
return err return err
} }
func ResetHooks() error {
entries, err := filepath.Glob(filepath.Join(config.GetHomeDir(), ReposDirectory, "*", "*"))
if err != nil {
return err
}
for _, e := range entries {
repoPath := strings.Split(e, string(os.PathSeparator))
if err := CreateDotGitFiles(repoPath[len(repoPath)-2], repoPath[len(repoPath)-1]); err != nil {
log.Error().Err(err).Msgf("Cannot reset hooks for repository %s/%s", repoPath[len(repoPath)-2], repoPath[len(repoPath)-1])
}
}
return nil
}
func HasNoCommits(user string, gist string) (bool, error) { func HasNoCommits(user string, gist string) (bool, error) {
repositoryPath := RepositoryPath(user, gist) repositoryPath := RepositoryPath(user, gist)
@@ -536,6 +576,10 @@ func CreateDotGitFiles(user string, gist string) error {
return nil return nil
} }
func DeleteUserDirectory(user string) error {
return os.RemoveAll(filepath.Join(config.GetHomeDir(), ReposDirectory, user))
}
func createDotGitHookFile(repositoryPath string, hook string, content string) error { func createDotGitHookFile(repositoryPath string, hook string, content string) error {
preReceiveDst, err := os.OpenFile(filepath.Join(repositoryPath, "hooks", hook), os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0744) preReceiveDst, err := os.OpenFile(filepath.Join(repositoryPath, "hooks", hook), os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0744)
if err != nil { if err != nil {
@@ -565,6 +609,48 @@ func removeFilesExceptGit(dir string) error {
}) })
} }
func convertOctalToUTF8(name string) string {
name = strings.Trim(name, `"`)
utf8Name, err := strconv.Unquote(name)
if err != nil {
utf8Name, err = strconv.Unquote(`"` + name + `"`)
if err != nil {
return name
}
}
return utf8Name
}
func convertUTF8ToOctal(name string) string {
if strings.Contains(name, "\\") {
return name
}
needsQuoting := false
for _, r := range name {
if r > 127 {
needsQuoting = true
break
}
}
if !needsQuoting {
return name
}
quoted := fmt.Sprintf("%q", name)
return strings.Trim(quoted, `"`)
}
func convertURLToOctal(name string) string {
decoded, err := url.PathUnescape(name)
if err != nil {
return name
}
return convertUTF8ToOctal(decoded)
}
const hookTemplate = `#!/bin/sh const hookTemplate = `#!/bin/sh
"$OG_OPENGIST_HOME_INTERNAL/symlinks/opengist" --config=$OG_OPENGIST_HOME_INTERNAL/symlinks/config.yml hook %s "$OG_OPENGIST_HOME_INTERNAL/symlinks/opengist" --config=$OG_OPENGIST_HOME_INTERNAL/symlinks/config.yml hook %s
` `

View File

@@ -61,11 +61,12 @@ func TestContent(t *testing.T) {
"my_other_file.txt": `I really "my_other_file.txt": `I really
hate Opengist`, hate Opengist`,
"rip.txt": "byebye", "rip.txt": "byebye",
"中文名.txt": "中文内容",
}) })
files, err := GetFilesOfRepository("thomas", "gist1", "HEAD") files, err := GetFilesOfRepository("thomas", "gist1", "HEAD")
require.NoError(t, err, "Could not get files of repository") require.NoError(t, err, "Could not get files of repository")
require.Subset(t, []string{"my_file.txt", "my_other_file.txt", "rip.txt"}, files, "Files are not correct") require.Subset(t, []string{"my_file.txt", "my_other_file.txt", "rip.txt", "中文名.txt"}, files, "Files are not correct")
content, truncated, err := GetFileContent("thomas", "gist1", "HEAD", "my_file.txt", false) content, truncated, err := GetFileContent("thomas", "gist1", "HEAD", "my_file.txt", false)
require.NoError(t, err, "Could not get content") require.NoError(t, err, "Could not get content")
@@ -77,16 +78,22 @@ hate Opengist`,
require.False(t, truncated, "Content should not be truncated") require.False(t, truncated, "Content should not be truncated")
require.Equal(t, "I really\nhate Opengist", content, "Content is not correct") require.Equal(t, "I really\nhate Opengist", content, "Content is not correct")
content, truncated, err = GetFileContent("thomas", "gist1", "HEAD", "中文名.txt", false)
require.NoError(t, err, "Could not get content")
require.False(t, truncated, "Content should not be truncated")
require.Equal(t, "中文内容", content, "Content is not correct")
CommitToBare(t, "thomas", "gist1", map[string]string{ CommitToBare(t, "thomas", "gist1", map[string]string{
"my_renamed_file.txt": "I love Opengist\n", "my_renamed_file.txt": "I love Opengist\n",
"my_other_file.txt": `I really "my_other_file.txt": `I really
like Opengist actually`, like Opengist actually`,
"new_file.txt": "Wait now there is a new file", "new_file.txt": "Wait now there is a new file",
"中文名.txt": "中文内容",
}) })
files, err = GetFilesOfRepository("thomas", "gist1", "HEAD") files, err = GetFilesOfRepository("thomas", "gist1", "HEAD")
require.NoError(t, err, "Could not get files of repository") require.NoError(t, err, "Could not get files of repository")
require.Subset(t, []string{"my_renamed_file.txt", "my_other_file.txt", "new_file.txt"}, files, "Files are not correct") require.Subset(t, []string{"my_renamed_file.txt", "my_other_file.txt", "new_file.txt", "中文名.txt"}, files, "Files are not correct")
content, truncated, err = GetFileContent("thomas", "gist1", "HEAD", "my_other_file.txt", false) content, truncated, err = GetFileContent("thomas", "gist1", "HEAD", "my_other_file.txt", false)
require.NoError(t, err, "Could not get content") require.NoError(t, err, "Could not get content")

63
internal/git/config.go Normal file
View File

@@ -0,0 +1,63 @@
package git
import (
"errors"
"os/exec"
"regexp"
)
type configEntry struct {
value string
fn func(string, string) error
}
func InitGitConfig() error {
configs := map[string]configEntry{
"receive.advertisePushOptions": {value: "true", fn: setGitConfig},
"safe.directory": {value: "*", fn: addGitConfig},
}
for key, entry := range configs {
if err := entry.fn(key, entry.value); err != nil {
return err
}
}
return nil
}
func setGitConfig(key, value string) error {
_, err := getGitConfig(key, value)
if err != nil && !checkErrorCode(err, 1) {
return err
}
cmd := exec.Command("git", "config", "--global", key, value)
return cmd.Run()
}
func addGitConfig(key, value string) error {
_, err := getGitConfig(key, regexp.QuoteMeta(value))
if err == nil {
return nil
}
if checkErrorCode(err, 1) {
cmd := exec.Command("git", "config", "--global", "--add", key, value)
return cmd.Run()
}
return err
}
func getGitConfig(key, value string) (string, error) {
cmd := exec.Command("git", "config", "--global", "--get", key, value)
out, err := cmd.Output()
return string(out), err
}
func checkErrorCode(err error, code int) bool {
var exitError *exec.ExitError
if errors.As(err, &exitError) {
return exitError.ExitCode() == code
}
return false
}

89
internal/git/mime.go Normal file
View File

@@ -0,0 +1,89 @@
package git
import (
"fmt"
"strings"
"github.com/gabriel-vasile/mimetype"
)
type MimeType struct {
ContentType string
}
func (mt MimeType) IsText() bool {
return strings.Contains(mt.ContentType, "text/")
}
func (mt MimeType) IsCSV() bool {
return strings.Contains(mt.ContentType, "text/csv")
}
func (mt MimeType) IsImage() bool {
return strings.Contains(mt.ContentType, "image/")
}
func (mt MimeType) IsSVG() bool {
return strings.Contains(mt.ContentType, "image/svg+xml")
}
func (mt MimeType) IsPDF() bool {
return strings.Contains(mt.ContentType, "application/pdf")
}
func (mt MimeType) IsAudio() bool {
return strings.Contains(mt.ContentType, "audio/")
}
func (mt MimeType) IsVideo() bool {
return strings.Contains(mt.ContentType, "video/")
}
func (mt MimeType) CanBeHighlighted() bool {
return mt.IsText() && !mt.IsCSV()
}
func (mt MimeType) CanBeEmbedded() bool {
return mt.IsImage() || mt.IsPDF() || mt.IsAudio() || mt.IsVideo()
}
func (mt MimeType) CanBeRendered() bool {
return mt.IsText() || mt.IsImage() || mt.IsSVG() || mt.IsPDF() || mt.IsAudio() || mt.IsVideo()
}
func (mt MimeType) CanBeEdited() bool {
return mt.IsText() || mt.IsSVG()
}
func (mt MimeType) RenderType() string {
t := strings.Split(mt.ContentType, "/")
str := ""
if len(t) == 2 {
str = fmt.Sprintf("(%s)", strings.ToUpper(t[1]))
}
// More user friendly description
if mt.IsImage() || mt.IsSVG() {
return fmt.Sprintf("Image %s", str)
}
if mt.IsAudio() {
return fmt.Sprintf("Audio %s", str)
}
if mt.IsVideo() {
return fmt.Sprintf("Video %s", str)
}
if mt.IsPDF() {
return "PDF"
}
if mt.IsCSV() {
return "CSV"
}
if mt.IsText() {
return "Text"
}
return "Binary"
}
func DetectMimeType(data []byte) MimeType {
return MimeType{mimetype.Detect(data).String()}
}

View File

@@ -3,27 +3,23 @@ package git
import ( import (
"bufio" "bufio"
"bytes" "bytes"
"encoding/csv"
"fmt" "fmt"
"io" "io"
"regexp"
"strings" "strings"
) )
type File struct { type File struct {
Filename string `json:"filename"` Filename string `json:"filename"`
Size uint64 `json:"size"` Size uint64 `json:"size"`
HumanSize string `json:"human_size"` HumanSize string `json:"human_size"`
OldFilename string `json:"-"` OldFilename string `json:"-"`
Content string `json:"content"` Content string `json:"content"`
Truncated bool `json:"truncated"` Truncated bool `json:"truncated"`
IsCreated bool `json:"-"` IsCreated bool `json:"-"`
IsDeleted bool `json:"-"` IsDeleted bool `json:"-"`
} IsBinary bool `json:"-"`
MimeType MimeType `json:"-"`
type CsvFile struct {
File
Header []string
Rows [][]string
} }
type Commit struct { type Commit struct {
@@ -62,6 +58,8 @@ func truncateCommandOutput(out io.Reader, maxBytes int64) (string, bool, error)
return string(buf), truncated, nil return string(buf), truncated, nil
} }
var reLogBinaryNames = regexp.MustCompile(`Binary files (.+) and (.+) differ`)
// inspired from https://github.com/go-gitea/gitea/blob/main/services/gitdiff/gitdiff.go // 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) { func parseLog(out io.Reader, maxFiles int, maxBytes int) ([]*Commit, error) {
var commits []*Commit var commits []*Commit
@@ -193,28 +191,42 @@ loopLog:
case strings.HasPrefix(line, "dissimilarity index"): case strings.HasPrefix(line, "dissimilarity index"):
continue continue
case strings.HasPrefix(line, "rename from "): case strings.HasPrefix(line, "rename from "):
currentFile.OldFilename = line[12 : len(line)-1] currentFile.OldFilename = convertOctalToUTF8(line[12 : len(line)-1])
case strings.HasPrefix(line, "rename to "): case strings.HasPrefix(line, "rename to "):
currentFile.Filename = line[10 : len(line)-1] currentFile.Filename = convertOctalToUTF8(line[10 : len(line)-1])
parseRename = false parseRename = false
case strings.HasPrefix(line, "copy from "): case strings.HasPrefix(line, "copy from "):
currentFile.OldFilename = line[10 : len(line)-1] currentFile.OldFilename = convertOctalToUTF8(line[10 : len(line)-1])
case strings.HasPrefix(line, "copy to "): case strings.HasPrefix(line, "copy to "):
currentFile.Filename = line[8 : len(line)-1] currentFile.Filename = convertOctalToUTF8(line[8 : len(line)-1])
parseRename = false parseRename = false
case strings.HasPrefix(line, "new file"): case strings.HasPrefix(line, "new file"):
currentFile.IsCreated = true currentFile.IsCreated = true
case strings.HasPrefix(line, "deleted file"): case strings.HasPrefix(line, "deleted file"):
currentFile.IsDeleted = true currentFile.IsDeleted = true
case strings.HasPrefix(line, "Binary files"):
currentFile.IsBinary = true
names := reLogBinaryNames.FindStringSubmatch(line)
if names[1][2:] != names[2][2:] {
if currentFile.IsCreated {
currentFile.Filename = convertOctalToUTF8(names[2])[2:]
}
if currentFile.IsDeleted {
currentFile.Filename = convertOctalToUTF8(names[1])[2:]
}
} else {
currentFile.OldFilename = convertOctalToUTF8(names[1])[2:]
currentFile.Filename = convertOctalToUTF8(names[2])[2:]
}
case strings.HasPrefix(line, "--- "): case strings.HasPrefix(line, "--- "):
name := line[4 : len(line)-1] name := convertOctalToUTF8(line[4 : len(line)-1])
if parseRename && currentFile.IsDeleted { if parseRename && currentFile.IsDeleted {
currentFile.Filename = name[2:] currentFile.Filename = name[2:]
} else if parseRename && strings.HasPrefix(name, "a/") { } else if parseRename && strings.HasPrefix(name, "a/") {
currentFile.OldFilename = name[2:] currentFile.OldFilename = name[2:]
} }
case strings.HasPrefix(line, "+++ "): case strings.HasPrefix(line, "+++ "):
name := line[4 : len(line)-1] name := convertOctalToUTF8(line[4 : len(line)-1])
if parseRename && strings.HasPrefix(name, "b/") { if parseRename && strings.HasPrefix(name, "b/") {
currentFile.Filename = name[2:] currentFile.Filename = name[2:]
} }
@@ -344,27 +356,3 @@ func skipToNextCommit(input *bufio.Reader) (line string, err error) {
} }
return line, err return line, err
} }
func ParseCsv(file *File) (*CsvFile, error) {
reader := csv.NewReader(strings.NewReader(file.Content))
records, err := reader.ReadAll()
if err != nil {
return nil, err
}
header := records[0]
numColumns := len(header)
for i := 1; i < len(records); i++ {
if len(records[i]) != numColumns {
return nil, fmt.Errorf("CSV file has invalid row at index %d", i)
}
}
return &CsvFile{
File: *file,
Header: header,
Rows: records[1:],
}, nil
}

View File

@@ -5,7 +5,7 @@ import (
"fmt" "fmt"
"github.com/thomiceli/opengist/internal/db" "github.com/thomiceli/opengist/internal/db"
"github.com/thomiceli/opengist/internal/git" "github.com/thomiceli/opengist/internal/git"
"github.com/thomiceli/opengist/internal/utils" validatorpkg "github.com/thomiceli/opengist/internal/validator"
"io" "io"
"os" "os"
"os/exec" "os/exec"
@@ -18,7 +18,7 @@ func PostReceive(in io.Reader, out, er io.Writer) error {
newGist := false newGist := false
opts := pushOptions() opts := pushOptions()
gistUrl := os.Getenv("OPENGIST_REPOSITORY_URL_INTERNAL") gistUrl := os.Getenv("OPENGIST_REPOSITORY_URL_INTERNAL")
validator := utils.NewValidator() validator := validatorpkg.NewValidator()
scanner := bufio.NewScanner(in) scanner := bufio.NewScanner(in)
for scanner.Scan() { for scanner.Scan() {
@@ -46,7 +46,7 @@ func PostReceive(in io.Reader, out, er io.Writer) error {
} }
if slices.Contains([]string{"public", "unlisted", "private"}, opts["visibility"]) { if slices.Contains([]string{"public", "unlisted", "private"}, opts["visibility"]) {
gist.Private, _ = db.ParseVisibility(opts["visibility"]) gist.Private = db.ParseVisibility(opts["visibility"])
outputSb.WriteString(fmt.Sprintf("Gist visibility set to %s\n\n", opts["visibility"])) outputSb.WriteString(fmt.Sprintf("Gist visibility set to %s\n\n", opts["visibility"]))
} }
@@ -65,6 +65,11 @@ func PostReceive(in io.Reader, out, er io.Writer) error {
outputSb.WriteString(fmt.Sprintf("Gist title set to \"%s\"\n\n", opts["title"])) outputSb.WriteString(fmt.Sprintf("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"]))
}
if hasNoCommits, err := git.HasNoCommits(gist.User.Username, gist.Uuid); err != nil { if hasNoCommits, err := git.HasNoCommits(gist.User.Username, gist.Uuid); err != nil {
_, _ = fmt.Fprintln(er, "Failed to check if gist has no commits") _, _ = fmt.Fprintln(er, "Failed to check if gist has no commits")
return fmt.Errorf("failed to check if gist has no commits: %w", err) return fmt.Errorf("failed to check if gist has no commits: %w", err)

View File

@@ -21,7 +21,7 @@ gist.header.embed: 'Einbetten'
gist.header.embed-help: 'Bette diese Gist in deine Webseite ein.' gist.header.embed-help: 'Bette diese Gist in deine Webseite ein.'
gist.header.download-zip: 'ZIP Herunterladen' gist.header.download-zip: 'ZIP Herunterladen'
gist.raw: 'Orginalformat' gist.raw: 'Originalformat'
gist.file-truncated: 'Diese Datei wurde abgeschnitten.' gist.file-truncated: 'Diese Datei wurde abgeschnitten.'
gist.watch-full-file: 'Die gesamte Datei anzeigen.' gist.watch-full-file: 'Die gesamte Datei anzeigen.'
gist.file-not-valid: 'Diese Datei ist keine korrekte CSV Datei.' gist.file-not-valid: 'Diese Datei ist keine korrekte CSV Datei.'
@@ -37,7 +37,7 @@ gist.new.indent-mode-space: 'Leerzeichen'
gist.new.indent-mode-tab: 'Tab' gist.new.indent-mode-tab: 'Tab'
gist.new.indent-size: 'Einrückungs Größe' gist.new.indent-size: 'Einrückungs Größe'
gist.new.wrap-mode: 'Textumbruch Modus' gist.new.wrap-mode: 'Textumbruch Modus'
gist.new.wrap-mode-no: 'kein Textumruch' gist.new.wrap-mode-no: 'kein Textumbruch'
gist.new.wrap-mode-soft: 'weicher Zeilenumbruch' gist.new.wrap-mode-soft: 'weicher Zeilenumbruch'
gist.new.add-file: 'Datei hinzufügen' gist.new.add-file: 'Datei hinzufügen'
gist.new.create-public-button: 'Öffentliche Gist erstellen' gist.new.create-public-button: 'Öffentliche Gist erstellen'
@@ -46,14 +46,14 @@ gist.new.create-private-button: 'Private Gist erstellen'
gist.new.preview: 'Vorschau' gist.new.preview: 'Vorschau'
gist.new.create-a-new-gist: 'Neue Gist erstellen' gist.new.create-a-new-gist: 'Neue Gist erstellen'
gist.edit.editing: 'Bearbeiten' gist.edit.editing: 'In Bearbeitung'
gist.edit.edit-gist: '%s bearbeiten' gist.edit.edit-gist: '%s bearbeiten'
gist.edit.change-visibility: 'Sichtbarkeit ändern' gist.edit.change-visibility: 'Sichtbarkeit ändern'
gist.edit.delete: 'Löschen' gist.edit.delete: 'Löschen'
gist.edit.cancel: 'Abbrechen' gist.edit.cancel: 'Abbrechen'
gist.edit.save: 'Speichern' gist.edit.save: 'Speichern'
gist.list.joined: 'Gemeinsam' gist.list.joined: 'Beigetreten'
gist.list.all: 'Alle Gists' gist.list.all: 'Alle Gists'
gist.list.search-results: 'Suchergebnisse' gist.list.search-results: 'Suchergebnisse'
gist.list.sort: 'Sortieren' gist.list.sort: 'Sortieren'
@@ -61,17 +61,17 @@ gist.list.sort-by-created: 'erstellt'
gist.list.sort-by-updated: 'bearbeitet' gist.list.sort-by-updated: 'bearbeitet'
gist.list.order-by-asc: 'Älteste' gist.list.order-by-asc: 'Älteste'
gist.list.order-by-desc: 'Neueste' gist.list.order-by-desc: 'Neueste'
gist.list.select-tab: 'Tab Auswählen' gist.list.select-tab: 'Tab auswählen'
gist.list.liked: 'Favorisiert' gist.list.liked: 'Favorisiert'
gist.list.likes: 'Favoriten' gist.list.likes: 'Favoriten'
gist.list.forked: 'Forked' gist.list.forked: 'Geforkt'
gist.list.forked-from: 'Forked von' gist.list.forked-from: 'Geforkt von'
gist.list.forks: 'Forks' gist.list.forks: 'Forks'
gist.list.files: 'Dateien' gist.list.files: 'Dateien'
gist.list.last-active: 'Zuletzt aktiv' gist.list.last-active: 'Zuletzt aktiv'
gist.list.no-gists: 'Keine Gists' gist.list.no-gists: 'Keine Gists'
gist.list.all-liked-by: 'Alle Gists favorisiert von %s' gist.list.all-liked-by: 'Alle Gists favorisiert von %s'
gist.list.all-forked-by: 'Alle Gists geforked von %s' gist.list.all-forked-by: 'Alle Gists geforkt von %s'
gist.list.all-from: 'Alle Gists von %s' gist.list.all-from: 'Alle Gists von %s'
gist.search.found: 'Gists gefunden' gist.search.found: 'Gists gefunden'
@@ -89,7 +89,7 @@ gist.forks.for: 'Fork für %s'
gist.likes: 'Favoriten' gist.likes: 'Favoriten'
gist.likes.no: 'Keine Favorisierungen' gist.likes.no: 'Keine Favorisierungen'
gist.likes.for: 'Favortitisiert für %s' gist.likes.for: 'Favorisiert für %s'
gist.revisions: 'Revisionen' gist.revisions: 'Revisionen'
gist.revision.revised: 'hat die Gist bearbeitet' gist.revision.revised: 'hat die Gist bearbeitet'
@@ -112,7 +112,7 @@ settings.link-accounts: 'Accounts verlinken'
settings.link-github-account: 'GitHub-Account verlinken' settings.link-github-account: 'GitHub-Account verlinken'
settings.link-gitlab-account: 'GitLab-Account verlinken' settings.link-gitlab-account: 'GitLab-Account verlinken'
settings.link-gitea-account: 'Gitea-Account verlinken' settings.link-gitea-account: 'Gitea-Account verlinken'
settings.unlink-github-account: 'Github-Account Verlinkung aufheben' settings.unlink-github-account: 'GitHub-Account Verlinkung aufheben'
settings.unlink-gitlab-account: 'GitLab-Account Verlinkung aufheben' settings.unlink-gitlab-account: 'GitLab-Account Verlinkung aufheben'
settings.unlink-gitea-account: 'Gitea-Account Verlinkung aufheben' settings.unlink-gitea-account: 'Gitea-Account Verlinkung aufheben'
settings.delete-account: 'Account löschen' settings.delete-account: 'Account löschen'
@@ -142,7 +142,7 @@ auth.username: 'Benutzername'
auth.password: 'Passwort' auth.password: 'Passwort'
auth.register-instead: 'Stattdessen registrieren' auth.register-instead: 'Stattdessen registrieren'
auth.login-instead: 'Stattdessen anmelden' auth.login-instead: 'Stattdessen anmelden'
auth.oauth: 'Weiter mit %s Account' auth.oauth: 'Weiter mit %s Konto'
error: 'Fehler' error: 'Fehler'
error.page-not-found: 'Seite nicht gefunden' error.page-not-found: 'Seite nicht gefunden'
@@ -267,3 +267,39 @@ validation.not-enough: 'Nicht genug %s'
validation.invalid: 'Ungültiges %s' validation.invalid: 'Ungültiges %s'
html.title.admin-panel: 'Admin-Panel' html.title.admin-panel: 'Admin-Panel'
auth.totp.regenerate-recovery-codes: Wiederherstellungscodes neu generieren
auth.totp.already-enabled: TOTP ist bereits aktiv
auth.totp.invalid-secret: Ungültiger TOTP Sicherheitsschlüssel
auth.totp.disable: TOTP ausschalten
auth.mfa.passkey: Hauptschlüssel
auth.mfa.use-passkey: Hauptschlüssel nutzen
auth.mfa.use-passkey-to-finish: Nutzen Sie einen Hauptschlüssel, um Authentisierung abzuschliessen
auth.totp.invalid-code: Ungültiger TOTP Code
auth.mfa: Multi-Faktor Authentisierung
gist.delete.confirm: Sind Sie sich sicher, ob Sie dieses Gist löschen wollen?
auth.mfa.passkeys: Hauptschlüssel
auth.mfa.bind-passkey: Hauptschlüssel verbinden
auth.mfa.login-with-passkey: Mit Hauptschlüssel einloggen
auth.mfa.waiting-for-passkey-input: Auf Eingabe von Browser-Interaktion warten...
auth.mfa.passkey-name: Name
auth.mfa.delete-passkey: Löschen
auth.mfa.passkey-added-at: Hinzugefügt
auth.mfa.passkey-never-used: Niemals benutzt
auth.mfa.passkey-last-used: Zuletzt benutzt
auth.totp.code: Code
auth.totp.proceed: Fortfahren
admin.invitations.delete_confirm: Willst du diese Einladung löschen?
flash.auth.passkey-deleted: Hauptschlüssel gelöscht
flash.auth.passkey-registred: Hauptschlüssel %s wurde registriert
auth.totp: Zeitbasiertes Einmal-Passwort (TOTP)
auth.totp.use: TOTP einrichten
auth.totp.help: TOTP ist eine Zwei-Faktor-Authentifizierungsmethode, welche ein gemeinsames Geheimnis verwendet, um ein Einmal-Passwort zu generieren.
auth.totp.code-used: Der Wiederherstellungscode %s wurde verwendet. Dieser ist nun ungültig. Du solltest die Zwei-Faktor-Authentifizierung möglicherweise vorübergehend deaktivieren oder deine Codes neu generieren.
auth.totp.disabled: TOTP erfolgreich deaktiviert
auth.mfa.passkeys-help: Füge einen Passkey hinzu, um dich bei deinem Account anzumelden und ihn als Zwei-Faktor-Methode zu verwenden.
auth.mfa.delete-passkey-confirm: Löschen des Hauptschlüssels bestätigen
auth.totp.submit: Bestätigen
auth.totp.scan-qr-code: Scanne den unten stehenden QR-Code mit deiner Authentifizierungs-App, um die Zwei-Faktor-Authentifizierung zu aktivieren, oder gib den folgenden Schlüssel ein und bestätige anschliessend mit dem generierten Code.
auth.totp.enter-code: Gib den Code aus deiner Authentifizierungs-App ein
auth.totp.save-recovery-codes: Speichere deine Wiederherstellungscodes an einem sicheren Ort. Du kannst diese Codes verwenden, um wieder Zugang zu deinem Account zu erlangen, wenn du den Zugriff auf deine Authentifizierungs-App verloren hast.
error.not-in-mfa-session: Nutzer ist nicht in einer Zwei-Faktor-Sitzung

View File

@@ -23,9 +23,12 @@ gist.header.download-zip: Download ZIP
gist.raw: Raw gist.raw: Raw
gist.file-truncated: This file has been truncated. gist.file-truncated: This file has been truncated.
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. gist.watch-full-file: View the full file.
gist.file-not-valid: This file is not a valid CSV file. gist.file-not-valid: This file is not a valid CSV file.
gist.no-content: No files found gist.no-content: No files found
gist.preview-non-available: Preview not available
gist.new.new_gist: New gist gist.new.new_gist: New gist
gist.new.title: Title gist.new.title: Title
@@ -45,6 +48,9 @@ gist.new.create-unlisted-button: Create unlisted gist
gist.new.create-private-button: Create private gist gist.new.create-private-button: Create private gist
gist.new.preview: Preview gist.new.preview: Preview
gist.new.create-a-new-gist: Create a new gist 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.edit.editing: Editing gist.edit.editing: Editing
gist.edit.edit-gist: Edit %s gist.edit.edit-gist: Edit %s
@@ -52,6 +58,7 @@ gist.edit.change-visibility: Make
gist.edit.delete: Delete gist.edit.delete: Delete
gist.edit.cancel: Cancel gist.edit.cancel: Cancel
gist.edit.save: Save gist.edit.save: Save
gist.delete.confirm: Are you sure you want to delete this gist ?
gist.list.joined: Joined gist.list.joined: Joined
gist.list.all: All gists gist.list.all: All gists
@@ -73,6 +80,9 @@ gist.list.no-gists: No gists
gist.list.all-liked-by: All gists liked by %s gist.list.all-liked-by: All gists liked by %s
gist.list.all-forked-by: All gists forked by %s gist.list.all-forked-by: All gists forked by %s
gist.list.all-from: All gists from %s gist.list.all-from: All gists from %s
gist.list.topic-results-topic: All gists matching topic %s
gist.list.topic-results: All gists matching topic
gist.search.found: gists found gist.search.found: gists found
gist.search.no-results: No gists found gist.search.no-results: No gists found
@@ -81,6 +91,16 @@ gist.search.help.title: gists with given title
gist.search.help.filename: gists having files with given name gist.search.help.filename: gists having files with given name
gist.search.help.extension: gists having files with given extension gist.search.help.extension: gists having files with given extension
gist.search.help.language: gists having files with given language gist.search.help.language: gists having files with given language
gist.search.help.topic: gists with given topic
gist.search.placeholder.title: Title
gist.search.placeholder.visibility: Visibility
gist.search.placeholder.public: Public
gist.search.placeholder.unlisted: Unlisted
gist.search.placeholder.private: Private
gist.search.placeholder.language: Language
gist.search.placeholder.all: All
gist.search.placeholder.topics: Topics
gist.search.placeholder.search: Search
gist.forks: Forks gist.forks: Forks
gist.forks.view: View fork gist.forks.view: View fork
@@ -100,6 +120,7 @@ gist.revision.file-renamed: renamed to
gist.revision.diff-truncated: Diff is too large to be shown gist.revision.diff-truncated: Diff is too large to be shown
gist.revision.file-renamed-no-changes: File renamed without changes gist.revision.file-renamed-no-changes: File renamed without changes
gist.revision.empty-file: Empty file gist.revision.empty-file: Empty file
gist.revision.binary-file-changes: Binary file changes are not shown
gist.revision.no-changes: No changes gist.revision.no-changes: No changes
gist.revision.no-revisions: No revisions to show gist.revision.no-revisions: No revisions to show
gist.revision-of: Revision of %s gist.revision-of: Revision of %s
@@ -133,6 +154,17 @@ settings.create-password-help: Create your password to login to Opengist via HTT
settings.change-password: Change password settings.change-password: Change password
settings.change-password-help: Change your password to login to Opengist via HTTP settings.change-password-help: Change your password to login to Opengist via HTTP
settings.password-label-title: Password settings.password-label-title: Password
settings.header.account: Account
settings.header.mfa: MFA
settings.header.ssh: SSH
settings.header.style: Style
settings.style.gist-code: Gist code
settings.style.no-soft-wrap: No Soft Wrap
settings.style.soft-wrap: Soft Wrap
settings.style.removed-lines-color: Removed lines color
settings.style.added-lines-color: Added lines color
settings.style.git-lines-color: Git lines color
settings.style.save-style: Save style
auth.signup-disabled: Administrator has disabled signing up auth.signup-disabled: Administrator has disabled signing up
auth.login: Login auth.login: Login
@@ -143,6 +175,39 @@ auth.password: Password
auth.register-instead: Register instead auth.register-instead: Register instead
auth.login-instead: Login instead auth.login-instead: Login instead
auth.oauth: Continue with %s account auth.oauth: Continue with %s account
auth.mfa: Multi-factor authentication
auth.mfa.passkey: Passkey
auth.mfa.passkeys: Passkeys
auth.mfa.use-passkey: Use passkey
auth.mfa.bind-passkey: Bind passkey
auth.mfa.login-with-passkey: Login with passkey
auth.mfa.waiting-for-passkey-input: Waiting for input from browser interaction...
auth.mfa.use-passkey-to-finish: Use a passkey to finish authentication
auth.mfa.passkeys-help: Add a passkey to log to your account and to use as an MFA method.
auth.mfa.passkey-name: Name
auth.mfa.delete-passkey: Delete
auth.mfa.passkey-added-at: Added
auth.mfa.passkey-never-used: Never used
auth.mfa.passkey-last-used: Last used
auth.mfa.delete-passkey-confirm: Confirm deletion of passkey
auth.totp: Time based one-time password (TOTP)
auth.totp.help: TOTP is a two-factor authentication method that uses a shared secret to generate a one-time password.
auth.totp.use: Use TOTP
auth.totp.regenerate-recovery-codes: Regenerate recovery codes
auth.totp.already-enabled: TOTP is already enabled
auth.totp.invalid-secret: Invalid TOTP secret
auth.totp.invalid-code: Invalid TOTP code
auth.totp.code-used: The recovery code %s was used, it is now invalid. You may want to disable MFA for now or regenerate your codes.
auth.totp.disabled: TOTP successfully disabled
auth.totp.disable: Disable TOTP
auth.totp.enter-code: Enter the code from the Authenticator app
auth.totp.enter-recovery-key: or a recovery key if you lost your device
auth.totp.code: Code
auth.totp.submit: Submit
auth.totp.proceed: Proceed
auth.totp.save-recovery-codes: Save your recovery codes in a safe place. You can use these codes to recover access to your account if you lose access to your authenticator app.
auth.totp.scan-qr-code: Scan the QR code below with your authenticator app to enable two-factor authentication or enter the following string, then confirm with the generated code.
error: Error error: Error
error.page-not-found: Page not found error.page-not-found: Page not found
@@ -155,6 +220,9 @@ error.oauth-unsupported: Unsupported provider
error.cannot-bind-data: Cannot bind data error.cannot-bind-data: Cannot bind data
error.invalid-number: Invalid number error.invalid-number: Invalid number
error.invalid-character-unescaped: Invalid character unescaped error.invalid-character-unescaped: Invalid character unescaped
error.not-in-mfa-session: User is not in a MFA session
error.no-file-uploaded: No file uploaded
error.cannot-open-file: Cannot open uploaded file
header.menu.all: All header.menu.all: All
header.menu.new: New header.menu.new: New
@@ -193,6 +261,7 @@ admin.actions.git-gc: Garbage collect all git repositories
admin.actions.sync-previews: Synchronize all gists previews admin.actions.sync-previews: Synchronize all gists previews
admin.actions.reset-hooks: Reset Git server hooks for all repositories admin.actions.reset-hooks: Reset Git server hooks for all repositories
admin.actions.index-gists: Index all gists admin.actions.index-gists: Index all gists
admin.actions.sync-gist-languages: Synchronize all gists languages
admin.id: ID admin.id: ID
admin.user: User admin.user: User
admin.delete: Delete admin.delete: Delete
@@ -226,6 +295,7 @@ admin.invitations.code: Code
admin.invitations.copy_link: Copy link admin.invitations.copy_link: Copy link
admin.invitations.uses: Uses admin.invitations.uses: Uses
admin.invitations.expired: Expired admin.invitations.expired: Expired
admin.invitations.delete_confirm: Do you want to delete this invitation ?
flash.admin.user-deleted: User has been deleted flash.admin.user-deleted: User has been deleted
flash.admin.gist-deleted: Gist has been deleted flash.admin.gist-deleted: Gist has been deleted
@@ -237,6 +307,7 @@ flash.admin.git-gc: Garbage collecting repositories...
flash.admin.sync-previews: Syncing Gist previews... flash.admin.sync-previews: Syncing Gist previews...
flash.admin.reset-hooks: Resetting Git server hooks for all repositories... flash.admin.reset-hooks: Resetting Git server hooks for all repositories...
flash.admin.index-gists: Indexing all gists... flash.admin.index-gists: Indexing all gists...
flash.admin.sync-gist-languages: Syncing Gist languages...
flash.auth.username-exists: Username already exists flash.auth.username-exists: Username already exists
flash.auth.invalid-credentials: Invalid credentials flash.auth.invalid-credentials: Invalid credentials
@@ -245,6 +316,8 @@ flash.auth.account-unlinked-oauth: Account unlinked from %s
flash.auth.user-sshkeys-not-retrievable: Could not get user keys flash.auth.user-sshkeys-not-retrievable: Could not get user keys
flash.auth.user-sshkeys-not-created: Could not create ssh key 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.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.gist.visibility-changed: Gist visibility has been changed flash.gist.visibility-changed: Gist visibility has been changed
flash.gist.deleted: Gist has been deleted flash.gist.deleted: Gist has been deleted
@@ -265,5 +338,6 @@ validation.should-only-contain-alphanumeric-characters: Field %s should only con
validation.should-only-contain-alphanumeric-characters-and-dashes: Field %s should only contain alphanumeric characters and dashes validation.should-only-contain-alphanumeric-characters-and-dashes: Field %s should only contain alphanumeric characters and dashes
validation.not-enough: Not enough %s validation.not-enough: Not enough %s
validation.invalid: Invalid %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
html.title.admin-panel: Admin panel html.title.admin-panel: Admin panel

View File

@@ -17,8 +17,8 @@ gist.header.clone-http: Clonar via %s
gist.header.clone-http-help: Clonar con Git usando autenticación básica HTTP. gist.header.clone-http-help: Clonar con Git usando autenticación básica HTTP.
gist.header.clone-ssh: Clonar via SSH gist.header.clone-ssh: Clonar via SSH
gist.header.clone-ssh-help: Clonar con Git usando una clave SSH. gist.header.clone-ssh-help: Clonar con Git usando una clave SSH.
gist.header.embed: '' gist.header.embed: 'Embeber'
gist.header.embed-help: '' gist.header.embed-help: 'Embebe este gist en tu sitio web.'
gist.header.download-zip: Descargar ZIP gist.header.download-zip: Descargar ZIP
gist.raw: Sin formato gist.raw: Sin formato
@@ -71,8 +71,8 @@ gist.forks: Bifurcaciones
gist.forks.view: Ver bifurcación gist.forks.view: Ver bifurcación
gist.forks.no: No hay bifurcaciones públicas gist.forks.no: No hay bifurcaciones públicas
gist.likes: Gustos gist.likes: Todos los me gusta
gist.likes.no: Aún no hay gustos gist.likes.no: Aún no hay me gusta
gist.revisions: Revisiones gist.revisions: Revisiones
gist.revision.revised: revisó este gist gist.revision.revised: revisó este gist
@@ -80,7 +80,7 @@ gist.revision.go-to-revision: Ir a la revisión
gist.revision.file-created: archivo creado gist.revision.file-created: archivo creado
gist.revision.file-deleted: archivo eliminado gist.revision.file-deleted: archivo eliminado
gist.revision.file-renamed: renombrado a gist.revision.file-renamed: renombrado a
gist.revision.diff-truncated: Diferencia truncada porque es demasiado grande para mostrarse. gist.revision.diff-truncated: Diferencia truncada porque es demasiado grande para mostrarse
gist.revision.file-renamed-no-changes: Archivo renombrado sin cambios gist.revision.file-renamed-no-changes: Archivo renombrado sin cambios
gist.revision.empty-file: Archivo vacío gist.revision.empty-file: Archivo vacío
gist.revision.no-changes: Sin cambios gist.revision.no-changes: Sin cambios
@@ -165,8 +165,8 @@ admin.disable-login: Deshabilitar formulario de inicio de sesión
admin.disable-login_help: Prohibir el inicio de sesión a través del formulario de inicio de sesión para forzar el uso de proveedores de OAuth en su lugar. admin.disable-login_help: Prohibir el inicio de sesión a través del formulario de inicio de sesión para forzar el uso de proveedores de OAuth en su lugar.
admin.disable-gravatar: Deshabilitar Gravatar admin.disable-gravatar: Deshabilitar Gravatar
admin.disable-gravatar_help: Deshabilitar el uso de Gravatar como proveedor de avatar. admin.disable-gravatar_help: Deshabilitar el uso de Gravatar como proveedor de avatar.
admin.allow-gists-without-login: admin.allow-gists-without-login: Permitir gists individuales sin iniciar sesión
admin.allow-gists-without-login_help: admin.allow-gists-without-login_help: Permitir ver y descargar gists individuales sin iniciar sesión, requiriendo iniciar sesión para descubrir gists.
admin.users.delete_confirm: ¿Quieres eliminar a este usuario? admin.users.delete_confirm: ¿Quieres eliminar a este usuario?
admin.gists.title: Título admin.gists.title: Título
@@ -174,86 +174,123 @@ admin.gists.private: ¿Privado?
admin.gists.nb-files: Núm. de archivos admin.gists.nb-files: Núm. de archivos
admin.gists.nb-likes: Núm. de gustos admin.gists.nb-likes: Núm. de gustos
admin.gists.delete_confirm: ¿Quieres eliminar este gist? admin.gists.delete_confirm: ¿Quieres eliminar este gist?
gist.new.url: '' gist.new.url: 'URL'
gist.new.preview: '' gist.new.preview: 'Previsualizar'
gist.new.create-a-new-gist: '' gist.new.create-a-new-gist: 'Crear un nuevo gist'
gist.edit.edit-gist: '' gist.edit.edit-gist: 'Editar %s'
gist.list.all-liked-by: '' gist.list.all-liked-by: 'Todos los gists que le gustaron a %s'
gist.list.all-forked-by: '' gist.list.all-forked-by: 'Todos los gists bifurcados por %s'
gist.list.all-from: '' gist.list.all-from: 'Todos los gists de %s'
gist.search.found: '' gist.search.found: 'gists encontrados'
gist.search.no-results: '' gist.search.no-results: 'No se han encontrado gists'
gist.search.help.user: '' gist.search.help.user: 'gists creados por el usuario'
gist.search.help.title: '' gist.search.help.title: 'gists con el título indicado'
gist.search.help.filename: '' gist.search.help.filename: 'gists que contienen archivos con el nombre indicado'
gist.search.help.extension: '' gist.search.help.extension: 'gists que contienen archivos con la extensión indicada'
gist.search.help.language: '' gist.search.help.language: 'gists que contienen archivos con el lenguaje indicado'
gist.forks.for: '' gist.forks.for: 'Bifurcacaiones de %s'
gist.likes.for: '' gist.likes.for: 'Me gusta para %s'
gist.revision-of: '' gist.revision-of: 'Revisión de %s'
settings.link-gitlab-account: '' settings.link-gitlab-account: 'Vincular cuenta de GitLab'
settings.unlink-gitlab-account: '' settings.unlink-gitlab-account: 'Desvincular cuenta de GitLab'
settings.change-username: '' settings.change-username: 'Cambiar nombre de usuario'
settings.create-password: '' settings.create-password: 'Crear contraseña'
settings.create-password-help: '' settings.create-password-help: 'Crea tu contraseña para acceder a Opengist vía HTTP'
settings.change-password: '' settings.change-password: 'Cambiar contraseña'
settings.change-password-help: '' settings.change-password-help: 'Cambia tu contraseña para acceder a Opengist vía HTTP'
settings.password-label-title: '' settings.password-label-title: 'Contraseña'
error.page-not-found: '' error.page-not-found: 'Página no encontrada'
error.bad-request: '' error.bad-request: 'Solicitud incorrecta'
error.signup-disabled: '' error.signup-disabled: 'El registro está deshabilitado'
error.signup-disabled-form: '' error.signup-disabled-form: 'El registro mediante el formulario está deshabilitado'
error.login-disabled-form: '' error.login-disabled-form: 'El inicio de sesión mediante el formulario está deshabilitado'
error.complete-oauth-login: '' error.complete-oauth-login: 'No se puede completar la autenticación del usuario: %s'
error.oauth-unsupported: '' error.oauth-unsupported: 'Proveedor no compatible'
error.cannot-bind-data: '' error.cannot-bind-data: 'No se puede vincular los datos'
error.invalid-number: '' error.invalid-number: 'Número inválido'
error.invalid-character-unescaped: '' error.invalid-character-unescaped: 'Carácter inválido no escapado'
admin.invitations: '' admin.invitations: 'Invitaciones'
admin.invitations.create: '' admin.invitations.create: 'Crear invitación'
admin.actions.sync-previews: '' admin.actions.sync-previews: 'Sincronizar todas las vistas previas de gists'
admin.actions.reset-hooks: '' admin.actions.reset-hooks: 'Resetear los hooks de Git en todos los repositorios'
admin.actions.index-gists: '' admin.actions.index-gists: 'Indexar todos los gists'
admin.config-link-overriden: '' admin.config-link-overriden: 'sobrescrito'
admin.invitations.help: '' admin.invitations.help: 'Las invitaciones se pueden usar para crear una cuenta aunque el registro esté deshabilitado.'
admin.invitations.max_uses: '' admin.invitations.max_uses: 'Cantidad máxima de usos'
admin.invitations.expires_at: '' admin.invitations.expires_at: 'Expira el'
admin.invitations.code: '' admin.invitations.code: 'Código'
admin.invitations.copy_link: '' admin.invitations.copy_link: 'Copiar vínculo'
admin.invitations.uses: '' admin.invitations.uses: 'Usos'
admin.invitations.expired: '' admin.invitations.expired: 'Expirado'
flash.admin.user-deleted: '' flash.admin.user-deleted: 'El usuario ha sido eliminado'
flash.admin.gist-deleted: '' flash.admin.gist-deleted: 'El gist ha sido eliminado'
flash.admin.invitation-created: '' flash.admin.invitation-created: 'La invitación ha sido creada'
flash.admin.invitation-deleted: '' flash.admin.invitation-deleted: 'La invitación ha sido eliminada'
flash.admin.sync-fs: '' flash.admin.sync-fs: 'Sincronizando repositorios desde el sistema de archivos...'
flash.admin.sync-db: '' flash.admin.sync-db: 'Sincronizando repositorios desde la base de datos...'
flash.admin.git-gc: '' flash.admin.git-gc: 'Recolectando basura en los repositorios...'
flash.admin.sync-previews: '' flash.admin.sync-previews: 'Sincronizando vistas previas de gists...'
flash.admin.reset-hooks: '' flash.admin.reset-hooks: 'Reseteando hooks del servidor Git en todos los repositorios...'
flash.admin.index-gists: '' flash.admin.index-gists: 'Indexando todos los gists...'
flash.auth.username-exists: '' flash.auth.username-exists: 'El nombre de usuario ya existe'
flash.auth.invalid-credentials: '' flash.auth.invalid-credentials: 'Credenciales incorrectas'
flash.auth.account-linked-oauth: '' flash.auth.account-linked-oauth: 'Cuenta vinculada a %s'
flash.auth.account-unlinked-oauth: '' flash.auth.account-unlinked-oauth: 'Cuenta desvinculada de %s'
flash.auth.user-sshkeys-not-retrievable: '' flash.auth.user-sshkeys-not-retrievable: 'No se pudo obtener las claves del usuario'
flash.auth.user-sshkeys-not-created: '' flash.auth.user-sshkeys-not-created: 'No se pudo crear la aclave ssh'
flash.auth.must-be-logged-in: '' flash.auth.must-be-logged-in: 'Debes estar logueaado para acceder a los gists'
flash.gist.visibility-changed: '' flash.gist.visibility-changed: 'La visibilidad del Gist ha sido modificada'
flash.gist.deleted: '' flash.gist.deleted: 'El gist fue eliminado'
flash.gist.fork-own-gist: '' flash.gist.fork-own-gist: 'No se puede bifurcar gists propios'
flash.gist.forked: '' flash.gist.forked: 'El gist ha sido bifurcado'
flash.user.email-updated: '' flash.user.email-updated: 'Correo actualizado'
flash.user.invalid-ssh-key: '' flash.user.invalid-ssh-key: 'Clave SSH inválida'
flash.user.ssh-key-added: '' flash.user.ssh-key-added: 'Clave SSH añadida'
flash.user.ssh-key-deleted: '' flash.user.ssh-key-deleted: 'Clave SSH eliminada'
flash.user.password-updated: '' flash.user.password-updated: 'Contraseña actualizada'
flash.user.username-updated: '' flash.user.username-updated: 'Nombre de usuario actualizado'
validation.is-too-long: '' validation.is-too-long: 'El campo %s es demasiado largo'
validation.should-not-be-empty: '' validation.should-not-be-empty: 'El campo %s no puede estar vacío'
validation.should-not-include-sub-directory: '' validation.should-not-include-sub-directory: 'El campo %s no puede incluir un sub directorio'
validation.should-only-contain-alphanumeric-characters: '' validation.should-only-contain-alphanumeric-characters: 'El campo %s solo puede contener caracteres alfanuméricos'
validation.should-only-contain-alphanumeric-characters-and-dashes: '' validation.should-only-contain-alphanumeric-characters-and-dashes: 'El campo %s solo puede contener caracteres alfanuméricos y guiones'
validation.not-enough: '' validation.not-enough: 'No hay suficiente %s'
validation.invalid: '' validation.invalid: '%s inválido'
html.title.admin-panel: '' html.title.admin-panel: 'Panel de administración'
auth.mfa: Autenticación mult-factor
auth.mfa.passkey: Clave de acceso
auth.mfa.passkeys: Claves de acceso
auth.mfa.use-passkey: Utilizar clave de acceso
auth.mfa.bind-passkey: Vincular clave de acceso
auth.mfa.login-with-passkey: Ingresar con clave de acceso
auth.mfa.waiting-for-passkey-input: Esperando interacción del navegador...
auth.mfa.use-passkey-to-finish: Usa una clave de acceso para completar la autenticación
auth.mfa.passkeys-help: Agrega una clave de acceso para iniciar sesión en tu cuenta y usarla como método MFA.
auth.mfa.passkey-name: Nombre
auth.mfa.delete-passkey: Eliminar
auth.mfa.passkey-added-at: Agregado
auth.mfa.passkey-never-used: Nunca utilizado
auth.mfa.passkey-last-used: Último uso
auth.mfa.delete-passkey-confirm: Confirmar eliminación de clave de acceso
auth.totp.enter-recovery-key: o una clave de recuperación si perdiste tu dispositivo
auth.totp.code: Código
auth.totp.submit: Enviar
auth.totp.proceed: Proceder
auth.totp.save-recovery-codes: Guarda tus códigos de recuperación en un lugar seguro. Puedes usarlos para recuperar el acceso a tu cuenta si pierdes el acceso a tu app de autenticación.
auth.totp.scan-qr-code: Escanea el código QR con tu app de autenticación para habilitar la autenticación de doble factor o ingresa la siguiente cadena y confirma con el código generado.
error.not-in-mfa-session: El usuario no está en una sesión MFA
settings.ssh-key-exists: La clave SSH ya existe
auth.totp: Contraseña de un solo uso basada en tiempo (TOTP)
auth.totp.help: TOTP es un método de autenticación doble factor que utiliza una clave compartida para generar una contraseña de un solo uso.
auth.totp.use: Usar TOTP
auth.totp.regenerate-recovery-codes: Regenerar códigos de recuperación
auth.totp.already-enabled: TOTP ya está habilitado
auth.totp.invalid-secret: Clave TOTP inválido
auth.totp.invalid-code: Código TOTP inválido
auth.totp.code-used: El código de recuperación %s fue utilizado, ahora es inválido. Puedes desactivar MFA por ahora o regenerar tus códigos.
auth.totp.disabled: TOTP deshabilitado exitosamente
auth.totp.disable: Deshabilitar TOTP
auth.totp.enter-code: Ingresa el código de la app de autenticación
gist.delete.confirm: ¿Estás seguro que deseas eliminar este gist?
flash.auth.passkey-registred: Clave de acceso %s registrada
flash.auth.passkey-deleted: Clave de acceso eliminada

View File

@@ -258,3 +258,4 @@ validation.not-enough: 'Pas assez de %s'
validation.invalid: '%s non valide' validation.invalid: '%s non valide'
html.title.admin-panel: 'Administration' html.title.admin-panel: 'Administration'
settings.ssh-key-exists: La clé SSH existe déjà settings.ssh-key-exists: La clé SSH existe déjà
gist.delete.confirm: Voulez-vous supprimer ce Gist ?

View File

@@ -0,0 +1,324 @@
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 BASIC認証によりGitを使ってクローン'
gist.header.clone-ssh: 'SSHでクローン'
gist.header.clone-ssh-help: 'SSH鍵認証によりGitを使ってクローン'
gist.header.embed: '埋め込み'
gist.header.embed-help: 'gistをWebサイトに埋め込む'
gist.header.download-zip: 'ZIPでダウンロード'
gist.raw: 'Raw'
gist.file-truncated: ''
gist.watch-full-file: 'ファイル全体を見る'
gist.file-not-valid: '無効なCSVファイルです'
gist.no-content: 'ファイルがありません'
gist.new.new_gist: '新規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を作成'
gist.new.create-unlisted-button: '限定公開gistを作成'
gist.new.create-private-button: '非公開gistを作成'
gist.new.preview: 'プレビュー'
gist.new.create-a-new-gist: '新規gistを作成'
gist.new.topics: 'トピック (スペースで区切り)'
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: ''
gist.list.all-forked-by: ''
gist.list.all-from: ''
gist.list.topic-results-topic: ''
gist.list.topic-results: ''
gist.search.found: ''
gist.search.no-results: ''
gist.search.help.user: ''
gist.search.help.title: ''
gist.search.help.filename: ''
gist.search.help.extension: ''
gist.search.help.language: ''
gist.search.help.topic: ''
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: ''
gist.likes: ''
gist.likes.no: ''
gist.likes.for: ''
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.no-changes: ''
gist.revision.no-revisions: ''
gist.revision-of: ''
settings: ''
settings.email: ''
settings.email-help: ''
settings.email-set: ''
settings.link-accounts: ''
settings.link-github-account: ''
settings.link-gitlab-account: ''
settings.link-gitea-account: ''
settings.unlink-github-account: ''
settings.unlink-gitlab-account: ''
settings.unlink-gitea-account: ''
settings.delete-account: ''
settings.delete-account-confirm: ''
settings.add-ssh-key: ''
settings.add-ssh-key-help: ''
settings.add-ssh-key-title: ''
settings.add-ssh-key-content: ''
settings.delete-ssh-key: ''
settings.delete-ssh-key-confirm: ''
settings.ssh-key-added-at: ''
settings.ssh-key-never-used: ''
settings.ssh-key-last-used: ''
settings.ssh-key-exists: ''
settings.change-username: ''
settings.create-password: ''
settings.create-password-help: ''
settings.change-password: ''
settings.change-password-help: ''
settings.password-label-title: ''
auth.signup-disabled: ''
auth.login: ''
auth.signup: ''
auth.new-account: ''
auth.username: ''
auth.password: ''
auth.register-instead: ''
auth.login-instead: ''
auth.oauth: ''
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: ''
auth.totp.help: ''
auth.totp.use: ''
auth.totp.regenerate-recovery-codes: ''
auth.totp.already-enabled: ''
auth.totp.invalid-secret: ''
auth.totp.invalid-code: ''
auth.totp.code-used: ''
auth.totp.disabled: ''
auth.totp.disable: ''
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: ''
error: ''
error.page-not-found: ''
error.bad-request: ''
error.signup-disabled: ''
error.signup-disabled-form: ''
error.login-disabled-form: ''
error.complete-oauth-login: ""
error.oauth-unsupported: ''
error.cannot-bind-data: ''
error.invalid-number: ''
error.invalid-character-unescaped: ''
error.not-in-mfa-session: ''
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: ''
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: ''
admin.stats: ''
admin.actions: ''
admin.actions.sync-fs: ''
admin.actions.sync-db: ''
admin.actions.git-gc: ''
admin.actions.sync-previews: ''
admin.actions.reset-hooks: ''
admin.actions.index-gists: ''
admin.actions.sync-gist-languages: ''
admin.id: ''
admin.user: ''
admin.delete: ''
admin.created_at: ''
admin.config-link: ''
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: ''
admin.disable-gravatar: ''
admin.disable-gravatar_help: ''
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: ''
flash.admin.index-gists: ''
flash.admin.sync-gist-languages: ''
flash.auth.username-exists: ''
flash.auth.invalid-credentials: ''
flash.auth.account-linked-oauth: ''
flash.auth.account-unlinked-oauth: ''
flash.auth.user-sshkeys-not-retrievable: ''
flash.auth.user-sshkeys-not-created: ''
flash.auth.must-be-logged-in: ''
flash.auth.passkey-registred: ''
flash.auth.passkey-deleted: ''
flash.gist.visibility-changed: ''
flash.gist.deleted: ''
flash.gist.fork-own-gist: ''
flash.gist.forked: ''
flash.user.email-updated: ''
flash.user.invalid-ssh-key: ''
flash.user.ssh-key-added: ''
flash.user.ssh-key-deleted: ''
flash.user.password-updated: ''
flash.user.username-updated: ''
validation.is-too-long: ''
validation.should-not-be-empty: ''
validation.should-not-include-sub-directory: ''
validation.should-only-contain-alphanumeric-characters: ''
validation.should-only-contain-alphanumeric-characters-and-dashes: ''
validation.not-enough: ''
validation.invalid: ''
validation.invalid-gist-topics: ''
html.title.admin-panel: ''

View File

@@ -0,0 +1,307 @@
gist.public: 'Publiczny'
gist.unlisted: 'Niepubliczny'
gist.private: 'Prywatny'
gist.header.like: 'Polub'
gist.header.unlike: 'Cofnij polubienie'
gist.header.fork: 'Zforkuj'
gist.header.edit: 'Edytuj'
gist.header.delete: 'Usuń'
gist.header.forked-from: 'Zforkowane z'
gist.header.last-active: 'Ostatnio aktywny'
gist.header.select-tab: 'Wybierz kartę'
gist.header.code: 'Kod'
gist.header.revisions: 'Rewizje'
gist.header.revision: 'Rewizja'
gist.header.clone-http: 'Sklonuj za pomocą %s'
gist.header.clone-http-help: 'Sklonuj za pomocą Git używając podstawowej autoryzacji HTTP.'
gist.header.clone-ssh: 'Sklonuj za pomocą SSH'
gist.header.clone-ssh-help: 'Sklonuj za pomocą Git używając klucza SSH.'
gist.header.embed: 'Osadź'
gist.header.embed-help: 'Wstaw ten Gist na twoją stronę.'
gist.header.download-zip: 'Pobierz ZIP'
gist.raw: 'Surowy'
gist.file-truncated: 'Ten plik został przycięty.'
gist.watch-full-file: 'Zobacz pełny plik.'
gist.file-not-valid: 'Ten plik nie jest poprawnym plikiem CSV.'
gist.no-content: 'Nie znaleziono plików'
gist.new.new_gist: 'Nowy Gist'
gist.new.title: 'Tytuł'
gist.new.description: 'Opis'
gist.new.url: 'URL'
gist.new.filename-with-extension: 'Nazwa pliku z rozszerzeniem'
gist.new.indent-mode: 'Tryb wcięcia'
gist.new.indent-mode-space: 'Spacje'
gist.new.indent-mode-tab: 'Tabulatory'
gist.new.indent-size: 'Wielkość wcięcia'
gist.new.wrap-mode: 'Tryb zawijania'
gist.new.wrap-mode-no: 'Bez zawijania'
gist.new.wrap-mode-soft: 'Miękkie zawijanie'
gist.new.add-file: 'Dodaj plik'
gist.new.create-public-button: 'Stwórz publiczny Gist'
gist.new.create-unlisted-button: 'Stwórz niepubliczny Gist'
gist.new.create-private-button: 'Stwórz prywatny Gist'
gist.new.preview: 'Podgląd'
gist.new.create-a-new-gist: 'Stwórz nowy Gist'
gist.edit.editing: 'Edytowanie'
gist.edit.edit-gist: 'Edytuj %s'
gist.edit.change-visibility: 'Zmień widoczność na'
gist.edit.delete: 'Usuń'
gist.edit.cancel: 'Anuluj'
gist.edit.save: 'Zapisz'
gist.delete.confirm: 'Czy na pewno chcesz usunąć ten Gist?'
gist.list.joined: 'Dołączono'
gist.list.all: 'Wszystkie Gisty'
gist.list.search-results: 'Wyniki wyszukiwania'
gist.list.sort: 'Sortuj'
gist.list.sort-by-created: 'utworzono'
gist.list.sort-by-updated: 'zaktualizowano'
gist.list.order-by-asc: 'Najdawniej'
gist.list.order-by-desc: 'Ostatnio'
gist.list.select-tab: 'Wybierz kartę'
gist.list.liked: 'Polubiane'
gist.list.likes: 'polubień'
gist.list.forked: 'Zforkowane'
gist.list.forked-from: 'Zforkowane z'
gist.list.forks: 'forków'
gist.list.files: 'plików'
gist.list.last-active: 'Ostatnio aktywne'
gist.list.no-gists: 'Brak Gistów'
gist.list.all-liked-by: 'Wszystkie Gisty polubione przez %s'
gist.list.all-forked-by: 'Wszystkie Gisty zforkowane przez %s'
gist.list.all-from: 'Wszystkie Gisty od %s'
gist.search.found: 'Gistów znaleziono'
gist.search.no-results: 'Nie znaleziono żadnych Gistów'
gist.search.help.user: 'Gisty stworzone przez użytkownika'
gist.search.help.title: 'Gisty z podanym tytułem'
gist.search.help.filename: 'Gisty zawierające pliki z podanym tytułem'
gist.search.help.extension: 'Gisty zawierające pliki z podanym rozszerzeniem'
gist.search.help.language: 'Gisty zawierające pliki z podanym językiem'
gist.forks: 'Forki'
gist.forks.view: 'Zobacz forka'
gist.forks.no: 'Brak publicznych forków'
gist.forks.for: 'Forki dla %s'
gist.likes: 'Polubienia'
gist.likes.no: 'Brak polubień'
gist.likes.for: 'Polubienia dla %s'
gist.revisions: 'Rewizje'
gist.revision.revised: 'zrewidował ten Gist'
gist.revision.go-to-revision: 'Przejdź do rewizji'
gist.revision.file-created: 'stworzono plik'
gist.revision.file-deleted: 'usunięto plik'
gist.revision.file-renamed: 'zmieniono nazwę na'
gist.revision.diff-truncated: 'Porównanie jest za duże do pokazania'
gist.revision.file-renamed-no-changes: 'Zmieniono nazwę pliku bez modyfikacji zawartości'
gist.revision.empty-file: 'Pusty plik'
gist.revision.no-changes: 'Brak zmian'
gist.revision.no-revisions: 'Brak rewizji do pokazania'
gist.revision-of: 'Rewizja %s'
settings: 'Ustawienia'
settings.email: 'Email'
settings.email-help: 'Używany do commitów i Gravatar'
settings.email-set: 'Ustaw email'
settings.link-accounts: 'Połącz konta'
settings.link-github-account: 'Połącz konto GitHub'
settings.link-gitlab-account: 'Połącz konto GitLab'
settings.link-gitea-account: 'Połącz konto Gitea'
settings.unlink-github-account: 'Odłącz konto GitHub'
settings.unlink-gitlab-account: 'Odłącz konto GitLab'
settings.unlink-gitea-account: 'Odłącz konto Gitea'
settings.delete-account: 'Usuń konto'
settings.delete-account-confirm: 'Czy na pewno chcesz usunąć swoje konto?'
settings.add-ssh-key: 'Dodaj klucz SSH'
settings.add-ssh-key-help: 'Używany tylko do operacji pull/push za pomocą Git przez SSH'
settings.add-ssh-key-title: 'Tytuł'
settings.add-ssh-key-content: 'Klucz'
settings.delete-ssh-key: 'Usuń'
settings.delete-ssh-key-confirm: 'Potwierdź usunięcie klucza SSH'
settings.ssh-key-added-at: 'Dodany'
settings.ssh-key-never-used: 'Nigdy nie użyty'
settings.ssh-key-last-used: 'Ostatnio użyty'
settings.ssh-key-exists: 'Klucz SSH już istnieje'
settings.change-username: 'Zmień nazwę użytkownika'
settings.create-password: 'Stwórz hasło'
settings.create-password-help: 'Stwórz swoje hasło do logowania się do Opengist przez HTTP'
settings.change-password: 'Zmień hasło'
settings.change-password-help: 'Zmień swoje hasło do logowania się do Opengist przez HTTP'
settings.password-label-title: 'Hasło'
auth.signup-disabled: 'Rejestracja została wyłączona przez administratora'
auth.login: 'Zaloguj się'
auth.signup: 'Zarejestruj się'
auth.new-account: 'Nowe konto'
auth.username: 'Nazwa użytkownika'
auth.password: 'Hasło'
auth.register-instead: 'Lub zarejestruj się'
auth.login-instead: 'Lub zaloguj się'
auth.oauth: 'Kontynuuj z kontem %s'
auth.mfa: 'Weryfikacja wieloskładnikowa'
auth.mfa.passkey: 'Klucz Passkey'
auth.mfa.passkeys: 'Klucze Passkey'
auth.mfa.use-passkey: 'Użyj klucza Passkey'
auth.mfa.bind-passkey: 'Powiąż klucz Passkey'
auth.mfa.login-with-passkey: 'Zaloguj się za pomocą klucza Passkey'
auth.mfa.waiting-for-passkey-input: 'Oczekiwanie na wejście z interakcji przeglądarki...'
auth.mfa.use-passkey-to-finish: 'Użyj klucza Passkey aby dokończyć logowanie'
auth.mfa.passkeys-help: 'Dodaj klucz Passkey aby logować się nim do swojego konta i aby używać go jako weryfikacji wieloskładnikowej.'
auth.mfa.passkey-name: 'Nazwa'
auth.mfa.delete-passkey: 'Usuń'
auth.mfa.passkey-added-at: 'Dodany'
auth.mfa.passkey-never-used: 'Nigdy nie użyty'
auth.mfa.passkey-last-used: 'Ostatnio użyty'
auth.mfa.delete-passkey-confirm: 'Potwierdź usunięcie klucza Passkey'
auth.totp: 'Time based one-time password (TOTP)'
auth.totp.help: 'TOTP to metoda weryfikacji dwuskładnikowej, która używa współdzielonego sekretu do generowania hasła jednorazowego użytku.'
auth.totp.use: 'Użyj TOTP'
auth.totp.regenerate-recovery-codes: 'Wygeneruje ponownie kody odzyskiwania'
auth.totp.already-enabled: 'TOTP jest już włączone'
auth.totp.invalid-secret: 'Niepoprawny sekret TOTP'
auth.totp.invalid-code: 'Niepoprawny kod TOTP'
auth.totp.code-used: 'Kod odzyskiwania %s został użyty, jest teraz nieważny. Możesz chcieć wyłączyć weryfikację wieloskładnikową na teraz lub wygenerować swoje kody ponownie.'
auth.totp.disabled: 'TOTP zostało pomyślnie wyłączone'
auth.totp.disable: 'Wyłącz TOTP'
auth.totp.enter-code: 'Wpisz kod z aplikacji uwierzytelniajacej'
auth.totp.enter-recovery-key: 'lub kod odzyskiwania jeśli zgubiłeś swoje urządzenie'
auth.totp.code: 'Kod'
auth.totp.submit: 'Prześlij'
auth.totp.proceed: 'Dalej'
auth.totp.save-recovery-codes: 'Zapis swoje kody odzyskiwania w bezpiecznym miejscu. Możesz użyć tych kodów, aby odzyskać dostęp do swojego konta jeśli stracisz dostęp do swojej aplikacji uwierzytelniajacej.'
auth.totp.scan-qr-code: 'Zeskanuj kod QR poniżej używając swojej aplikacji uwierzytelniajacej, aby włączyć weryfikację dwuskładnikową lub wpisz następujący ciąg i potwierdź go wygenerowanym kodem.'
error: 'Błąd'
error.page-not-found: 'Nie znaleziono strony'
error.bad-request: 'Złe żądanie'
error.signup-disabled: 'Rejestracja jest wyłączona'
error.signup-disabled-form: 'Rejestracja za pomocą formularza rejestracyjnego jest wyłączona'
error.login-disabled-form: 'Logowanie za pomocą formularza logowania jest wyłączone'
error.complete-oauth-login: "Nie można ukończyć logowania: %s"
error.oauth-unsupported: 'Niewspierany dostawca'
error.cannot-bind-data: 'Nie można powiązać danych'
error.invalid-number: 'Niepoprawna liczba'
error.invalid-character-unescaped: 'Nieprawidłowy niechroniony znak'
error.not-in-mfa-session: 'Użytkownik nie jest w sesji uwierzytelnienia wieloskładnikowego'
header.menu.all: 'Wszystko'
header.menu.new: 'Nowy'
header.menu.search: 'Szukaj'
header.menu.my-gists: 'Moje Gisty'
header.menu.liked: 'Polubione'
header.menu.admin: 'Admin'
header.menu.settings: 'Ustawienia'
header.menu.logout: 'Wyloguj się'
header.menu.register: 'Zarejestruj się'
header.menu.login: 'Zaloguj się'
header.menu.light: 'Jasny'
header.menu.dark: 'Ciemny'
header.menu.system: 'Systemowy'
footer.powered-by: 'Obsługiwane przez %s'
pagination.older: 'Starsze'
pagination.newer: 'Nowsze'
pagination.previous: 'Poprzedni'
pagination.next: 'Nastepny'
admin.admin_panel: 'Panel administracyjny'
admin.general: 'Ogólne'
admin.users: 'Użytkownicy'
admin.gists: 'Gisty'
admin.configuration: 'Konfiguracja'
admin.invitations: 'Zaproszenia'
admin.invitations.create: 'Stwórz zaproszenie'
admin.versions: 'Wersje'
admin.ssh_keys: 'Klucze SSH'
admin.stats: 'Statystyki'
admin.actions: 'Akcje'
admin.actions.sync-fs: 'Synchronizuj Gisty z systemu plików'
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.id: 'ID'
admin.user: 'Użytkownik'
admin.delete: 'Usuń'
admin.created_at: 'Utworzono'
admin.config-link: 'Ta konfiguracja może zostać %s przez plik konfiguracyjny YAML i/lub zmienne środowiskowe.'
admin.config-link-overriden: 'nadpisana'
admin.disable-signup: 'Wyłącz rejestrację'
admin.disable-signup_help: 'Zabroń tworzenia nowych kont.'
admin.require-login: 'Wymagaj logowania'
admin.require-login_help: 'Wymagaj od użytkowników zalogowania się, aby mogli zobaczyć Gisty.'
admin.allow-gists-without-login: 'Zezwól na indywidualne Gisty bez logowania'
admin.allow-gists-without-login_help: 'Zezwalaj na przeglądanie i pobieranie pojedynczych Gistów bez logowania, ale wymagaj zalogowania się w celu odkrywania Gistów.'
admin.disable-login: 'Wyłącz formularz logowania'
admin.disable-login_help: 'Zabroń logowania się za pomocą formularza logowania, aby wymusić korzystanie z dostawców OAuth.'
admin.disable-gravatar: 'Wyłącz Gravatar'
admin.disable-gravatar_help: 'Wyłącz używanie Gravatar jako dostawcy awatarów.'
admin.users.delete_confirm: 'Czy chcesz usunąć tego użytkownika?'
admin.gists.title: 'Tytuł'
admin.gists.private: 'Prywatny?'
admin.gists.nb-files: 'Liczba plików'
admin.gists.nb-likes: 'Liczba polubień'
admin.gists.delete_confirm: 'Czy chcesz usunąć tego Gista?'
admin.invitations.help: 'Zaproszenia mogą być używane, aby stworzyć konto nawet, jeśli rejestracja jest wyłączona.'
admin.invitations.max_uses: 'Maksymalna liczba użyć'
admin.invitations.expires_at: 'Wygasa'
admin.invitations.code: 'Kod'
admin.invitations.copy_link: 'Kopiuj link'
admin.invitations.uses: 'Użyć'
admin.invitations.expired: 'Wygasło'
flash.admin.user-deleted: 'Użytkownik został usunięty'
flash.admin.gist-deleted: 'Gist został usunięty'
flash.admin.invitation-created: 'Zaproszenie zostało stworzone'
flash.admin.invitation-deleted: 'Zaproszenie zostało usunięte'
flash.admin.sync-fs: 'Synchronizowanie repozytoriów z systemu plików...'
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.auth.username-exists: 'Nazwa użytkownika już istnieje'
flash.auth.invalid-credentials: 'Niepoprawne dane logowania'
flash.auth.account-linked-oauth: 'Konto połączone z %s'
flash.auth.account-unlinked-oauth: 'Konto odłączone od %s'
flash.auth.user-sshkeys-not-retrievable: 'Nie można uzyskać kluczy użytkownika'
flash.auth.user-sshkeys-not-created: 'Nie można stworzyć klucza SSH'
flash.auth.must-be-logged-in: 'Musisz być zalogowany, aby widzieć Gisty'
flash.auth.passkey-registred: 'Zarejestrowano klucz Passkey %s'
flash.auth.passkey-deleted: 'Usunięto klucz Passkey'
flash.gist.visibility-changed: 'Widoczność Gista została zmieniona'
flash.gist.deleted: 'Gist został usunięty'
flash.gist.fork-own-gist: 'Nie można forkować własnych Gistów'
flash.gist.forked: 'Gist został zforkowany'
flash.user.email-updated: 'Email został zaktualizowany'
flash.user.invalid-ssh-key: 'niepoprawny klucz SSH'
flash.user.ssh-key-added: 'Dodano klucz SSH'
flash.user.ssh-key-deleted: 'Usunięto klucz SSH'
flash.user.password-updated: 'Zaktualizowano hasło'
flash.user.username-updated: 'Zaktualizowano nazwę użytkownika'
validation.is-too-long: 'Pole %s jest za długie'
validation.should-not-be-empty: 'Pole %s nie może być puste'
validation.should-not-include-sub-directory: 'Pole %s nie może zawierać podfolderu'
validation.should-only-contain-alphanumeric-characters: 'Pole %s może tylko zawierać znaki alfanumeryczne'
validation.should-only-contain-alphanumeric-characters-and-dashes: 'Pole %s może tylko zawierać znaki alfanumeryczne i myślniki'
validation.not-enough: 'Nie wystarczająco %s'
validation.invalid: 'Niepoprawny %s'
html.title.admin-panel: 'Panel administracyjny'
admin.invitations.delete_confirm: Czy chcesz usunąć to zaproszenie?

View File

@@ -1,5 +1,5 @@
gist.public: Herkese Açık gist.public: Herkese Açık
gist.unlisted: Liste Dışı gist.unlisted: Listelenmemiş
gist.private: Gizli gist.private: Gizli
gist.header.like: Beğen gist.header.like: Beğen
@@ -7,19 +7,19 @@ gist.header.unlike: Beğenmekten Vazgeç
gist.header.fork: Çatalla gist.header.fork: Çatalla
gist.header.edit: Düzenle gist.header.edit: Düzenle
gist.header.delete: Sil gist.header.delete: Sil
gist.header.forked-from: Çatallı gist.header.forked-from: Çatallandı
gist.header.last-active: Son aktif gist.header.last-active: Son aktivite
gist.header.select-tab: Bir sekme seç gist.header.select-tab: Bir sekme seç
gist.header.code: Kod gist.header.code: Kod
gist.header.revisions: Revizyonlar gist.header.revisions: Revizyonlar
gist.header.revision: Revizyon gist.header.revision: Revizyon
gist.header.clone-http: \%s aracılığıyla klonla gist.header.clone-http: '%s ile klonla'
gist.header.clone-http-help: HTTP temel kimlik doğrulamasını kullanarak Git ile klonlayın. gist.header.clone-http-help: HTTP temel kimlik doğrulamasını kullanarak Git ile klonlayın.
gist.header.clone-ssh: SSH aracılığıyla klonla gist.header.clone-ssh: SSH aracılığıyla klonla
gist.header.clone-ssh-help: Bir SSH anahtarı kullanarak Git ile klonlayın. gist.header.clone-ssh-help: Bir SSH anahtarı kullanarak Git ile klonlayın.
gist.header.embed: Yerleştirme gist.header.embed: Göm
gist.header.embed-help: Bu gisti web sitenize yerleştirin. gist.header.embed-help: Bu gisti web sitenize yerleştirin.
gist.header.download-zip: ZIP'i indirin gist.header.download-zip: ZIP'i İndir
gist.raw: Ham gist.raw: Ham
gist.file-truncated: Bu dosya kısaltılmıştır. gist.file-truncated: Bu dosya kısaltılmıştır.
@@ -29,27 +29,27 @@ gist.no-content: Dosya bulunamadı
gist.new.new_gist: Yeni gist gist.new.new_gist: Yeni gist
gist.new.title: Başlık gist.new.title: Başlık
gist.new.description: Description gist.new.description: ıklama
gist.new.url: URL gist.new.url: URL
gist.new.filename-with-extension: Uzantılı dosya adı gist.new.filename-with-extension: Dosya adı ve uzantısı
gist.new.indent-mode: Girinti modu gist.new.indent-mode: Girinti modu
gist.new.indent-mode-space: Boşluk gist.new.indent-mode-space: Boşluk
gist.new.indent-mode-tab: Tab gist.new.indent-mode-tab: Sekme
gist.new.indent-size: Girinti boyutu gist.new.indent-size: Girinti boyutu
gist.new.wrap-mode: '' gist.new.wrap-mode: 'Satır kaydırma modu'
gist.new.wrap-mode-no: '' gist.new.wrap-mode-no: 'Kaydırma yok'
gist.new.wrap-mode-soft: '' gist.new.wrap-mode-soft: 'Yumuşak kaydırma'
gist.new.add-file: Add file gist.new.add-file: Dosya ekle
gist.new.create-public-button: Herkese açık gist oluştur gist.new.create-public-button: Herkese açık gist oluştur
gist.new.create-unlisted-button: Liste dışı gist oluştur gist.new.create-unlisted-button: Liste dışı gist oluştur
gist.new.create-private-button: Gizli gist oluştur gist.new.create-private-button: Gizli gist oluştur
gist.new.preview: Ön izle gist.new.preview: Önizle
gist.new.create-a-new-gist: Yeni bir gist oluştur gist.new.create-a-new-gist: Yeni bir gist oluştur
gist.edit.editing: Düzenleme gist.edit.editing: Düzenleniyor
gist.edit.edit-gist: '%s düzenle' gist.edit.edit-gist: '%s düzenle'
gist.edit.change-visibility: '' gist.edit.change-visibility: 'Yap'
gist.edit.delete: Delete gist.edit.delete: Sil
gist.edit.cancel: İptal Et gist.edit.cancel: İptal Et
gist.edit.save: Kaydet gist.edit.save: Kaydet
@@ -58,210 +58,211 @@ gist.list.all: Tüm gistler
gist.list.search-results: Arama sonuçları gist.list.search-results: Arama sonuçları
gist.list.sort: Sırala gist.list.sort: Sırala
gist.list.sort-by-created: oluşturuldu gist.list.sort-by-created: oluşturuldu
gist.list.sort-by-updated: düzenlendi gist.list.sort-by-updated: güncellendi
gist.list.order-by-asc: En son yakın zamanda gist.list.order-by-asc: En eski
gist.list.order-by-desc: Son zamanlarda gist.list.order-by-desc: Yakın zamanda
gist.list.select-tab: Bir sekme seçin gist.list.select-tab: Bir sekme seçin
gist.list.liked: Beğenildi gist.list.liked: Beğenildi
gist.list.likes: beğeniler gist.list.likes: beğeniler
gist.list.forked: Çatallı gist.list.forked: Çatallandı
gist.list.forked-from: çatallandı gist.list.forked-from: Çatallandı
gist.list.forks: çatallar gist.list.forks: çatallar
gist.list.files: files gist.list.files: dosyalar
gist.list.last-active: Son aktif gist.list.last-active: Son aktivite
gist.list.no-gists: Gistler yok gist.list.no-gists: Gist yok
gist.list.all-liked-by: '%s tarafından beğenilen tüm gistler' gist.list.all-liked-by: '%s tarafından beğenilen tüm gistler'
gist.list.all-forked-by: '%s tarafından beğenilen tüm çatallar' gist.list.all-forked-by: '%s tarafından çatallanan tüm gistler'
gist.list.all-from: '%s tüm gistleri' gist.list.all-from: '%s oluşturduğu tüm gistler'
gist.search.found: bulunan gistler gist.search.found: gist bulundu
gist.search.no-results: Hiç gist bulunamadı gist.search.no-results: Hiç gist bulunamadı
gist.search.help.user: gists created by user gist.search.help.user: kullanıcı tarafından oluşturulan gistler
gist.search.help.title: gists with given title gist.search.help.title: belirtilen isme sahip gistler
gist.search.help.filename: gists having files with given name gist.search.help.filename: belirtilen isimde dosyaları olan gistler
gist.search.help.extension: gists having files with given extension gist.search.help.extension: belirtilen uzantıya sahip dosyalara sahip gistler
gist.search.help.language: gists having files with given language gist.search.help.language: belirtilen dilde dosyaları olan gistler
gist.forks: Forks gist.forks: Çatallar
gist.forks.view: View fork gist.forks.view: Çatalı görüntüle
gist.forks.no: No public forks gist.forks.no: Herkes açık çatal yok
gist.forks.for: Forks for %s gist.forks.for: '%s için çatallar'
gist.likes: Likes gist.likes: Beğeniler
gist.likes.no: No likes yet gist.likes.no: Henüz kimse beğenmedi
gist.likes.for: Likes for %s gist.likes.for: '%s için beğeniler'
gist.revisions: Revisions gist.revisions: Düzenlemeler
gist.revision.revised: revised this gist gist.revision.revised: bu gisti düzenledi
gist.revision.go-to-revision: Go to revision gist.revision.go-to-revision: Düzenlemeye git
gist.revision.file-created: file created gist.revision.file-created: dosya oluşturuldu
gist.revision.file-deleted: file deleted gist.revision.file-deleted: dosya silindi
gist.revision.file-renamed: renamed to gist.revision.file-renamed: yeniden adlandırıldı
gist.revision.diff-truncated: Diff is too large to be shown gist.revision.diff-truncated: Fark gösterilemeyecek kadar büyük
gist.revision.file-renamed-no-changes: File renamed without changes gist.revision.file-renamed-no-changes: Dosya değişiklik yapılmadan yeniden adlandırıldı
gist.revision.empty-file: Empty file gist.revision.empty-file: Boş dosya
gist.revision.no-changes: No changes gist.revision.no-changes: Değişiklik yok
gist.revision.no-revisions: No revisions to show gist.revision.no-revisions: Gösterilecek düzenleme yok
gist.revision-of: Revision of %s gist.revision-of: '%s düzenlemesi'
settings: Settings settings: Ayarlar
settings.email: Email settings.email: E-Posta
settings.email-help: Used for commits and Gravatar settings.email-help: Commit ve Gravatar için kullanılır
settings.email-set: Set email settings.email-set: E-postayı ayarla
settings.link-accounts: Link accounts settings.link-accounts: Hesapları bağla
settings.link-github-account: Link GitHub account settings.link-github-account: GitHub hesabını bağla
settings.link-gitlab-account: Link GitLab account settings.link-gitlab-account: GitLab hesabını bağla
settings.link-gitea-account: Link Gitea account settings.link-gitea-account: Gitea hesabını bağla
settings.unlink-github-account: Unlink GitHub account settings.unlink-github-account: GitHub hesabının bağlantısını kaldır
settings.unlink-gitlab-account: Unlink GitLab account settings.unlink-gitlab-account: GitLab hesabının bağlantısını kaldır
settings.unlink-gitea-account: Unlink Gitea account settings.unlink-gitea-account: Gitea hesabının bağlantısını kaldır
settings.delete-account: Delete account settings.delete-account: Hesabı sil
settings.delete-account-confirm: Are you sure you want to delete your account ? settings.delete-account-confirm: Hesabını silmek istediğinden emin misin?
settings.add-ssh-key: Add SSH key settings.add-ssh-key: SSH anahtarı ekle
settings.add-ssh-key-help: Used only to pull/push gists using Git via SSH settings.add-ssh-key-help: Sadece SSH üzerinden Git kullanarak gistlerin pull/push işlemlerinde kullanılır
settings.add-ssh-key-title: Title settings.add-ssh-key-title: Başlık
settings.add-ssh-key-content: Key settings.add-ssh-key-content: Anahtar
settings.delete-ssh-key: Delete settings.delete-ssh-key: Sil
settings.delete-ssh-key-confirm: Confirm deletion of SSH key settings.delete-ssh-key-confirm: SSH anahtarının silinmesini onaylayın
settings.ssh-key-added-at: Added settings.ssh-key-added-at: Eklendi
settings.ssh-key-never-used: Never used settings.ssh-key-never-used: Hiç kullanılmadı
settings.ssh-key-last-used: Last used settings.ssh-key-last-used: Son kullanma
settings.change-username: Change username settings.change-username: Kullanıcı adını değiştir
settings.create-password: Create password settings.create-password: Parola oluştur
settings.create-password-help: Create your password to login to Opengist via HTTP settings.create-password-help: HTTP üzerinden Opengist'e giriş yapmak için parolanızı oluşturun
settings.change-password: Change password settings.change-password: Parolayı değiştir
settings.change-password-help: Change your password to login to Opengist via HTTP settings.change-password-help: HTTP üzerinden Opengist'e giriş yapmak için parolanızı değiştirin
settings.password-label-title: Password settings.password-label-title: Parola
auth.signup-disabled: Administrator has disabled signing up auth.signup-disabled: Yönetici kaydolmayı devre dışı bıraktı
auth.login: Login auth.login: Giriş
auth.signup: Register auth.signup: Kaydol
auth.new-account: New account auth.new-account: Yeni hesap
auth.username: Username auth.username: Kullanıcı adı
auth.password: Password auth.password: Parola
auth.register-instead: Register instead auth.register-instead: Bunun yerine kayıt olun
auth.login-instead: Login instead auth.login-instead: Bunun yerine giriş yapın
auth.oauth: Continue with %s account auth.oauth: '%s hesabı ile devam et'
error: Error error: Hata
error.page-not-found: Page not found error.page-not-found: Sayfa bulunamadı
error.bad-request: Bad request error.bad-request: Geçersiz istek
error.signup-disabled: Signing up is disabled error.signup-disabled: Kullanıcı kaydı devre dışı bırakıldı
error.signup-disabled-form: Signing up via registration form is disabled error.signup-disabled-form: Kayıt formu aracılığıyla kullanıcı kaydı devre dışı bırakıldı
error.login-disabled-form: Logging in via login form is disabled error.login-disabled-form: Giriş formu üzerinden giriş yapma devre dışı bırakıldı
error.complete-oauth-login: "Cannot complete user auth: %s" error.complete-oauth-login: "Kullanıcı kimlik doğrulaması tamamlanamıyor: %s"
error.oauth-unsupported: Unsupported provider error.oauth-unsupported: Desteklenmeyen sağlayıcı
error.cannot-bind-data: Cannot bind data error.cannot-bind-data: Veri bağlanamıyor
error.invalid-number: Invalid number error.invalid-number: Geçersiz numara
error.invalid-character-unescaped: Invalid character unescaped error.invalid-character-unescaped: Geçersiz karakter için kaçış işlemi yapılamadı
header.menu.all: All header.menu.all: Tümü
header.menu.new: New header.menu.new: Yeni
header.menu.search: Search header.menu.search: Ara
header.menu.my-gists: My gists header.menu.my-gists: Gistlerim
header.menu.liked: Liked header.menu.liked: Beğenilen
header.menu.admin: Admin header.menu.admin: Yönetici
header.menu.settings: Settings header.menu.settings: Ayarlar
header.menu.logout: Logout header.menu.logout: Çıkış
header.menu.register: Register header.menu.register: Kaydol
header.menu.login: Login header.menu.login: Giriş
header.menu.light: Light header.menu.light: ık
header.menu.dark: Dark header.menu.dark: Koyu
header.menu.system: System header.menu.system: Sistem
footer.powered-by: Powered by %s footer.powered-by: '%s tarafından desteklenmektedir'
pagination.older: Older pagination.older: Daha eski
pagination.newer: Newer pagination.newer: Daha yeni
pagination.previous: Previous pagination.previous: Önceki
pagination.next: Next pagination.next: Sonraki
admin.admin_panel: Admin panel admin.admin_panel: Yönetici paneli
admin.general: General admin.general: Genel
admin.users: Users admin.users: Kullanıcılar
admin.gists: Gists admin.gists: Gistler
admin.configuration: Configuration admin.configuration: Yapılandırma
admin.invitations: Invitations admin.invitations: Davetler
admin.invitations.create: Create invitation admin.invitations.create: Davet oluştur
admin.versions: Versions admin.versions: Sürümler
admin.ssh_keys: SSH keys admin.ssh_keys: SSH anahtarları
admin.stats: Stats admin.stats: İstatistikler
admin.actions: Actions admin.actions: Eylemler
admin.actions.sync-fs: Synchronize gists from filesystem admin.actions.sync-fs: Gistleri dosya sisteminden senkronize et
admin.actions.sync-db: Synchronize gists from database admin.actions.sync-db: Gistleri veri tabanından senkronize et
admin.actions.git-gc: Garbage collect all git repositories admin.actions.git-gc: Tüm Git depolarındaki gereksiz verileri temizle
admin.actions.sync-previews: Synchronize all gists previews admin.actions.sync-previews: Tüm gist önizlemelerini senkronize et
admin.actions.reset-hooks: Reset Git server hooks for all repositories admin.actions.reset-hooks: Tüm depolar için Git sunucu kancalarını sıfırla
admin.actions.index-gists: Index all gists admin.actions.index-gists: Tüm gistleri indeksle
admin.id: ID admin.id: ID
admin.user: User admin.user: Kullanıcı
admin.delete: Delete admin.delete: Sil
admin.created_at: Created admin.created_at: Oluşturulma
admin.config-link: This configuration can be %s by a YAML config file and/or environment variables. admin.config-link: Bu yapılandırma, bir YAML yapılandırma dosyası ve/veya ortam değişkenleri tarafından %s.
admin.config-link-overriden: overridden admin.config-link-overriden: geçersiz kılınabilir
admin.disable-signup: Disable signup admin.disable-signup: Kayıt işlemini devre dışı bırak
admin.disable-signup_help: Forbid the creation of new accounts. admin.disable-signup_help: Yeni hesap oluşturulmasını yasaklar.
admin.require-login: Require login admin.require-login: Giriş gerektir
admin.require-login_help: Enforce users to be logged in to see gists. admin.require-login_help: Gistleri görebilmek için kullanıcıların oturum açmasını zorunlu kılar.
admin.disable-login: Disable login form admin.disable-login: Giriş formunu devre dışı bırak
admin.disable-login_help: Forbid logging in via the login form to force using OAuth providers instead. admin.disable-login_help: Giriş formunu kullanarak giriş yapmayı yasaklar ve bunun yerine OAuth sağlayıcılarının kullanılmasını zorunlu hale getirir.
admin.disable-gravatar: Disable Gravatar admin.disable-gravatar: Gravatar'ı devre dışı bırak
admin.disable-gravatar_help: Disable the usage of Gravatar as an avatar provider. admin.disable-gravatar_help: Gravatar'ın avatar sağlayıcı olarak kullanımını devre dışı bırakır.
admin.allow-gists-without-login: admin.allow-gists-without-login: Bireysel gistlere oturum açmadan izin ver
admin.allow-gists-without-login_help: admin.allow-gists-without-login_help: Bireysel gistlerin oturum açmadan görüntülenmesine ve indirilmesine izin verir, ancak gistleri keşfetmek için oturum açmayı zorunlu kılar.
admin.users.delete_confirm: Do you want to delete this user ? admin.users.delete_confirm: Bu kullanıcıyı silmek istiyor musun?
admin.gists.title: Title admin.gists.title: Başlık
admin.gists.private: Private ? admin.gists.private: Gizlilik
admin.gists.nb-files: Nb. files admin.gists.nb-files: Dosyalar
admin.gists.nb-likes: Nb. likes admin.gists.nb-likes: Beğeniler
admin.gists.delete_confirm: Do you want to delete this gist ? admin.gists.delete_confirm: Bu gisti silmek istiyor musun?
admin.invitations.help: Invitations can be used to create an account even if signing up is disabled. admin.invitations.help: Kayıt olma özelliği devre dışı bırakılmış olsa bile, hesap oluşturmak için davetiyeler kullanılabilir.
admin.invitations.max_uses: Max uses admin.invitations.max_uses: Maksimum kullanım
admin.invitations.expires_at: Expires at admin.invitations.expires_at: Sona erme tarihi
admin.invitations.code: Code admin.invitations.code: Kod
admin.invitations.copy_link: Copy link admin.invitations.copy_link: Linki kopyala
admin.invitations.uses: Uses admin.invitations.uses: Kullanım
admin.invitations.expired: Expired admin.invitations.expired: Süresi bitti
flash.admin.user-deleted: User has been deleted flash.admin.user-deleted: Kullanıcı silindi
flash.admin.gist-deleted: Gist has been deleted flash.admin.gist-deleted: Gist silindi
flash.admin.invitation-created: Invitation has been created flash.admin.invitation-created: Davetiye oluşturuldu
flash.admin.invitation-deleted: Invitation has been deleted flash.admin.invitation-deleted: Davetiye silindi
flash.admin.sync-fs: Syncing repositories from filesystem... flash.admin.sync-fs: Depolar dosya sisteminden senkronize ediliyor...
flash.admin.sync-db: Syncing repositories from database... flash.admin.sync-db: Depolar veri tabanından senkronize ediliyor...
flash.admin.git-gc: Garbage collecting repositories... flash.admin.git-gc: Depolardan gereksiz veriler temizleniyor...
flash.admin.sync-previews: Syncing Gist previews... flash.admin.sync-previews: Gist önizlemeleri senkronize ediliyor...
flash.admin.reset-hooks: Resetting Git server hooks for all repositories... flash.admin.reset-hooks: Tüm depolar için Git sunucusu kancaları sıfırlanıyor...
flash.admin.index-gists: Indexing all gists... flash.admin.index-gists: Tüm gistler indeksleniyor...
flash.auth.username-exists: Username already exists flash.auth.username-exists: Kullanıcı adı zaten mevcut
flash.auth.invalid-credentials: Invalid credentials flash.auth.invalid-credentials: Geçersiz kimlik bilgileri
flash.auth.account-linked-oauth: Account linked to %s flash.auth.account-linked-oauth: Hesap %s ile bağlantılı
flash.auth.account-unlinked-oauth: Account unlinked from %s flash.auth.account-unlinked-oauth: Hesabın %s ile bağlantısı kaldırıldı
flash.auth.user-sshkeys-not-retrievable: Could not get user keys flash.auth.user-sshkeys-not-retrievable: Kullanıcı anahtarları alınamadı
flash.auth.user-sshkeys-not-created: Could not create ssh key flash.auth.user-sshkeys-not-created: SSH anahtarı oluşturulamadı
flash.auth.must-be-logged-in: You must be logged in to access gists flash.auth.must-be-logged-in: Gistlere erişmek için giriş yapmış olmanız gerekli
flash.gist.visibility-changed: Gist visibility has been changed flash.gist.visibility-changed: Gist görünürlüğü değiştirildi
flash.gist.deleted: Gist has been deleted flash.gist.deleted: Gist silindi
flash.gist.fork-own-gist: Unable to fork own gists flash.gist.fork-own-gist: Kendi gistlerin çatallanamaz
flash.gist.forked: Gist has been forked flash.gist.forked: Gist çatallandı
flash.user.email-updated: Email updated flash.user.email-updated: E-posta güncellendi
flash.user.invalid-ssh-key: Invalid SSH key flash.user.invalid-ssh-key: Geçersiz SSH anahtarı
flash.user.ssh-key-added: SSH key added flash.user.ssh-key-added: SSH anahtarı eklendi
flash.user.ssh-key-deleted: SSH key deleted flash.user.ssh-key-deleted: SSH anahtarı silindi
flash.user.password-updated: Password updated flash.user.password-updated: Parola güncellendi
flash.user.username-updated: Username updated flash.user.username-updated: Kullanıcı adı güncellendi
validation.is-too-long: Field %s is too long validation.is-too-long: '%s alanı çok uzun'
validation.should-not-be-empty: Field %s should not be empty validation.should-not-be-empty: '%s alanı boş olmamalı'
validation.should-not-include-sub-directory: Field %s should not include a sub directory validation.should-not-include-sub-directory: '%s alanı bir alt dizin içermemeli'
validation.should-only-contain-alphanumeric-characters: Field %s should only contain alphanumeric characters validation.should-only-contain-alphanumeric-characters: '%s alanı yalnızca alfanümerik karakterler içermelidir'
validation.should-only-contain-alphanumeric-characters-and-dashes: Field %s should only contain alphanumeric characters and dashes validation.should-only-contain-alphanumeric-characters-and-dashes: '%s alanı yalnızca alfanümerik karakterler ve tire içermelidir'
validation.not-enough: Not enough %s validation.not-enough: Yeterli %s yok
validation.invalid: Invalid %s validation.invalid: Geçersiz %s
html.title.admin-panel: Admin panel html.title.admin-panel: Yönetici paneli
settings.ssh-key-exists: SSH anahtarı zaten mevcut

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