Compare commits
176 Commits
v1.7.5
...
gists-john
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4d29a50e64 | ||
|
|
3a4602d412 | ||
|
|
2e10c1732a | ||
|
|
fe04c03acb | ||
|
|
2a1554d063 | ||
|
|
b7dbdde66b | ||
|
|
b7278b60ab | ||
|
|
84c6a41340 | ||
|
|
6bd8df6a74 | ||
|
|
b48103c06a | ||
|
|
48f2c4f5c8 | ||
|
|
5ddea2265d | ||
|
|
1128a81071 | ||
|
|
145bf9d81a | ||
|
|
24d0918e73 | ||
|
|
4ff71fb255 | ||
|
|
67f7c4cadd | ||
|
|
a17effb10f | ||
|
|
b2161d8859 | ||
|
|
61bb22ebe9 | ||
|
|
6813c14e3a | ||
|
|
4ae25144a0 | ||
|
|
03420e4f91 | ||
|
|
22376d6cd3 | ||
|
|
f3dc45fe0f | ||
|
|
7b4dab143b | ||
|
|
f874b81e2e | ||
|
|
5fe6238da1 | ||
|
|
f4e472a77b | ||
|
|
4350a66afd | ||
|
|
8a958de3d7 | ||
|
|
871cb356b7 | ||
|
|
0958e80d8e | ||
|
|
cc27899b6c | ||
|
|
256da0077a | ||
|
|
0e5007dbad | ||
|
|
91de091874 | ||
|
|
07bdf983af | ||
|
|
a5907c313c | ||
|
|
dc0b429121 | ||
|
|
b2373109b8 | ||
|
|
0a106b27db | ||
|
|
f10d656355 | ||
|
|
fe211b949b | ||
|
|
a5778e77eb | ||
|
|
f24c78d0a2 | ||
|
|
34bd7bec20 | ||
|
|
4d6809bc2d | ||
|
|
a493de4325 | ||
|
|
a67c80d148 | ||
|
|
feac9dcb66 | ||
|
|
38024310df | ||
|
|
9512ba84b0 | ||
|
|
b11306851b | ||
|
|
3957dfb3ea | ||
|
|
8129906b02 | ||
|
|
7880a3438e | ||
|
|
d5a3400bf0 | ||
|
|
f529bf6a22 | ||
|
|
425b123dd9 | ||
|
|
a7eaffbf02 | ||
|
|
5d19825949 | ||
|
|
c6dc2072bd | ||
|
|
4d4f1c36a9 | ||
|
|
a7ad82e29a | ||
|
|
98d216038b | ||
|
|
395ea7bfc7 | ||
|
|
1c145e09c5 | ||
|
|
32ea7befaf | ||
|
|
f653179cbf | ||
|
|
f0a596aed0 | ||
|
|
a468f0ecfa | ||
|
|
5ef5518795 | ||
|
|
92c5569538 | ||
|
|
132e4faed2 | ||
|
|
c7b947580d | ||
|
|
4106956f6d | ||
|
|
c02bf97b63 | ||
|
|
53ce41e0e4 | ||
|
|
594d876ba8 | ||
|
|
905276f24b | ||
|
|
2976173658 | ||
|
|
b048203216 | ||
|
|
a7a25c4100 | ||
|
|
bb1991f3ca | ||
|
|
979b302e4c | ||
|
|
b18cdb9188 | ||
|
|
867aa6e57b | ||
|
|
3c0115d829 | ||
|
|
d796895b75 | ||
|
|
5542497622 | ||
|
|
546f1968e0 | ||
|
|
75e71fd042 | ||
|
|
897dc43790 | ||
|
|
72e02700ec | ||
|
|
dc43fccc04 | ||
|
|
0e9b778b45 | ||
|
|
3c940cd81f | ||
|
|
de144d09d3 | ||
|
|
fde8a85e2b | ||
|
|
b82b3d9e0e | ||
|
|
9e69677f58 | ||
|
|
2d8debecbe | ||
|
|
8cfaceb303 | ||
|
|
7907c7bc1e | ||
|
|
e3aa994d30 | ||
|
|
91df15f957 | ||
|
|
efba783c56 | ||
|
|
dbdfcd4e85 | ||
|
|
da0b440360 | ||
|
|
d53885c541 | ||
|
|
1ec026e191 | ||
|
|
8c7e941182 | ||
|
|
26b5044380 | ||
|
|
a2259d5c77 | ||
|
|
6fd7f77003 | ||
|
|
87ae60ce4c | ||
|
|
c14380f4de | ||
|
|
da36e9eb55 | ||
|
|
7aa8f84eff | ||
|
|
76fc129c09 | ||
|
|
62d56cd1c7 | ||
|
|
d363743203 | ||
|
|
28c7e75657 | ||
|
|
0609b64cff | ||
|
|
f5b8881d35 | ||
|
|
8369cbf2f0 | ||
|
|
2ab9cf556f | ||
|
|
662f553d37 | ||
|
|
a752e0561d | ||
|
|
f935ee1a7e | ||
|
|
4c5a7bda63 | ||
|
|
f6bf09d5c2 | ||
|
|
86dd59c695 | ||
|
|
20aef5e694 | ||
|
|
00951bf63b | ||
|
|
526da6ccbb | ||
|
|
3a4080176c | ||
|
|
64306be2d6 | ||
|
|
8543f3adfa | ||
|
|
391ffde12e | ||
|
|
3193a9e888 | ||
|
|
58c5ac11c7 | ||
|
|
6a8e827d61 | ||
|
|
8f482bce33 | ||
|
|
5994cd6ccd | ||
|
|
00e3d09cc5 | ||
|
|
40ff4c7b3f | ||
|
|
c1e046f428 | ||
|
|
92bac3bf8c | ||
|
|
73c2fb55bc | ||
|
|
75162b3ef9 | ||
|
|
d537153785 | ||
|
|
97b9fa1100 | ||
|
|
393c9756d4 | ||
|
|
63d4b46a41 | ||
|
|
91c412d97e | ||
|
|
7cc2b497ca | ||
|
|
d5e66d3994 | ||
|
|
4fd0832df9 | ||
|
|
20372f44e4 | ||
|
|
d0b4815798 | ||
|
|
3cc3fb4572 | ||
|
|
ca44abfc43 | ||
|
|
2bf434f00e | ||
|
|
df226cbd99 | ||
|
|
3068588111 | ||
|
|
12696d23b0 | ||
|
|
798a0bfc28 | ||
|
|
6959929094 | ||
|
|
41dc2e451b | ||
|
|
56b4fd45fd | ||
|
|
605c8b892a | ||
|
|
fa8217e27f | ||
|
|
9ac7a76f4a | ||
|
|
17237713a1 |
18
.github/dependabot.yml
vendored
Normal file
18
.github/dependabot.yml
vendored
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
version: 2
|
||||||
|
updates:
|
||||||
|
- package-ecosystem: "npm"
|
||||||
|
directory: "/"
|
||||||
|
schedule:
|
||||||
|
interval: "weekly"
|
||||||
|
- package-ecosystem: "github-actions"
|
||||||
|
directory: "/"
|
||||||
|
schedule:
|
||||||
|
interval: "weekly"
|
||||||
|
- package-ecosystem: "gomod"
|
||||||
|
directory: "/"
|
||||||
|
schedule:
|
||||||
|
interval: "weekly"
|
||||||
|
- package-ecosystem: "docker"
|
||||||
|
directory: "/"
|
||||||
|
schedule:
|
||||||
|
interval: "weekly"
|
||||||
34
.github/workflows/docs.yml
vendored
34
.github/workflows/docs.yml
vendored
@@ -11,10 +11,10 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
- name: Setup Node.js
|
- name: Setup Node.js
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v6
|
||||||
with:
|
with:
|
||||||
node-version: '20'
|
node-version: '20'
|
||||||
|
|
||||||
@@ -28,20 +28,16 @@ jobs:
|
|||||||
npx tailwindcss -i .vitepress/theme/style.css -o .vitepress/theme/theme.css -c .vitepress/tailwind.config.js
|
npx tailwindcss -i .vitepress/theme/style.css -o .vitepress/theme/theme.css -c .vitepress/tailwind.config.js
|
||||||
npm run docs:build
|
npm run docs:build
|
||||||
|
|
||||||
- name: Deploy to server
|
- name: Push to docs repository
|
||||||
uses: appleboy/scp-action@master
|
run: |
|
||||||
with:
|
git clone https://${{ secrets.STATIC_REPO_TOKEN }}@github.com/${{ secrets.STATIC_REPO }}.git target-repo
|
||||||
host: ${{ secrets.SERVER_HOST }}
|
rm -rf target-repo/srv/opengist
|
||||||
username: ${{ secrets.SERVER_USERNAME }}
|
mkdir -p target-repo/srv/opengist
|
||||||
key: ${{ secrets.SERVER_SSH_KEY }}
|
cp -r docs/.vitepress/dist/* target-repo/srv/opengist/
|
||||||
source: "docs/.vitepress/dist/*"
|
cd target-repo
|
||||||
target: ${{ secrets.SERVER_PATH }}
|
git config user.name "github-actions[bot]"
|
||||||
|
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||||
- name: Update remote docs
|
git add .
|
||||||
uses: appleboy/ssh-action@master
|
git commit -m "Deploy docs from ${{ github.repository }}@${{ github.sha }}" || echo "No changes to commit"
|
||||||
with:
|
git pull --rebase
|
||||||
host: ${{ secrets.SERVER_HOST }}
|
git push
|
||||||
username: ${{ secrets.SERVER_USERNAME }}
|
|
||||||
key: ${{ secrets.SERVER_SSH_KEY }}
|
|
||||||
script: |
|
|
||||||
${{ secrets.UPDATE_DOCS }}
|
|
||||||
|
|||||||
106
.github/workflows/go.yml
vendored
106
.github/workflows/go.yml
vendored
@@ -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@v6
|
||||||
|
|
||||||
- name: Set up Go 1.22
|
- name: Set up Go 1.25
|
||||||
uses: actions/setup-go@v4
|
uses: actions/setup-go@v6
|
||||||
with:
|
with:
|
||||||
go-version: "1.22"
|
go-version: "1.25"
|
||||||
|
|
||||||
- name: Lint
|
- name: Lint
|
||||||
uses: golangci/golangci-lint-action@v3
|
uses: golangci/golangci-lint-action@v9
|
||||||
with:
|
with:
|
||||||
version: v1.54
|
version: v2.5
|
||||||
skip-pkg-cache: true
|
args: --timeout=20m --disable=errcheck
|
||||||
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@v6
|
||||||
|
|
||||||
- name: Set up Go 1.22
|
- name: Set up Go 1.25
|
||||||
uses: actions/setup-go@v4
|
uses: actions/setup-go@v6
|
||||||
with:
|
with:
|
||||||
go-version: "1.22"
|
go-version: "1.25"
|
||||||
|
|
||||||
- name: Check Go modules
|
- name: Check Go modules
|
||||||
run: make go_mod check_changes
|
run: make go_mod check_changes
|
||||||
@@ -47,22 +51,88 @@ jobs:
|
|||||||
- name: Check translations
|
- name: Check translations
|
||||||
run: make check-tr
|
run: make check-tr
|
||||||
|
|
||||||
test:
|
test-db:
|
||||||
|
name: Test
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
os: ["ubuntu-latest", "macOS-latest", "windows-latest"]
|
os: ["ubuntu-latest"]
|
||||||
go: ["1.22"]
|
go: ["1.25"]
|
||||||
|
database: [postgres, mysql]
|
||||||
|
include:
|
||||||
|
- database: postgres
|
||||||
|
image: postgres:16
|
||||||
|
port: 5432:5432
|
||||||
|
- database: mysql
|
||||||
|
image: mysql:8
|
||||||
|
port: 3306:3306
|
||||||
|
|
||||||
runs-on: ${{ matrix.os }}
|
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:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
- name: Set up Go ${{ matrix.go }}
|
- name: Set up Go ${{ matrix.go }}
|
||||||
uses: actions/setup-go@v4
|
uses: actions/setup-go@v6
|
||||||
with:
|
with:
|
||||||
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 }}
|
||||||
|
|
||||||
|
test:
|
||||||
|
name: Test
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
os: ["ubuntu-latest", "macOS-latest", "windows-latest"]
|
||||||
|
go: ["1.25"]
|
||||||
|
database: ["sqlite"]
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
|
- name: Set up Go ${{ matrix.go }}
|
||||||
|
uses: actions/setup-go@v6
|
||||||
|
with:
|
||||||
|
go-version: ${{ matrix.go }}
|
||||||
|
|
||||||
|
- name: Run tests
|
||||||
|
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.25"]
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
|
- name: Set up Go 1.25
|
||||||
|
uses: actions/setup-go@v6
|
||||||
|
with:
|
||||||
|
go-version: ${{ matrix.go }}
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
shell: bash
|
||||||
|
run: make
|
||||||
|
|||||||
49
.github/workflows/helm.yml
vendored
Normal file
49
.github/workflows/helm.yml
vendored
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
name: Build / Deploy Helm Chart
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-and-deploy:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
|
- name: Set up Helm
|
||||||
|
uses: azure/setup-helm@v4.3.1
|
||||||
|
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: Push to docs repository
|
||||||
|
run: |
|
||||||
|
git clone https://${{ secrets.DOCS_REPO_TOKEN }}@github.com/${{ secrets.DOCS_REPO }}.git target-repo
|
||||||
|
mkdir -p target-repo/helm
|
||||||
|
cp helm/*.tgz target-repo/helm/
|
||||||
|
cp helm/index.yaml target-repo/helm/
|
||||||
|
cd target-repo
|
||||||
|
git config user.name "github-actions[bot]"
|
||||||
|
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||||
|
git add .
|
||||||
|
git commit -m "Deploy helm chart from ${{ github.repository }}@${{ github.sha }}" || echo "No changes to commit"
|
||||||
|
git pull --rebase
|
||||||
|
git push
|
||||||
31
.github/workflows/release.yml
vendored
31
.github/workflows/release.yml
vendored
@@ -11,18 +11,18 @@ jobs:
|
|||||||
contents: write
|
contents: write
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
- name: Set up Go 1.22
|
- name: Set up Go 1.25
|
||||||
uses: actions/setup-go@v4
|
uses: actions/setup-go@v6
|
||||||
with:
|
with:
|
||||||
go-version: "1.22"
|
go-version: "1.25"
|
||||||
|
|
||||||
- name: Cross compile build
|
- name: Cross compile build
|
||||||
run: make all_crosscompile
|
run: make all_crosscompile
|
||||||
|
|
||||||
- name: Upload Release Assets
|
- name: Upload Release Assets
|
||||||
uses: softprops/action-gh-release@v1
|
uses: softprops/action-gh-release@v2
|
||||||
with:
|
with:
|
||||||
files: |
|
files: |
|
||||||
build/*.tar.gz
|
build/*.tar.gz
|
||||||
@@ -38,14 +38,15 @@ jobs:
|
|||||||
packages: write
|
packages: write
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
- name: Docker meta
|
- name: Docker meta
|
||||||
id: meta
|
id: meta
|
||||||
uses: docker/metadata-action@v4
|
uses: docker/metadata-action@v5
|
||||||
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}}
|
||||||
@@ -53,20 +54,26 @@ jobs:
|
|||||||
type=semver,pattern={{version}}
|
type=semver,pattern={{version}}
|
||||||
|
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@v2
|
uses: docker/setup-qemu-action@v3
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v2
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
- name: Login to GitHub Container Registry
|
- name: Login to GitHub Container Registry
|
||||||
uses: docker/login-action@v2
|
uses: docker/login-action@v3
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
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@v3
|
||||||
|
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@v6
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
platforms: linux/amd64,linux/arm64
|
platforms: linux/amd64,linux/arm64
|
||||||
@@ -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
|
||||||
5
.gitignore
vendored
5
.gitignore
vendored
@@ -1,11 +1,16 @@
|
|||||||
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
|
||||||
|
public/.vite/*
|
||||||
|
./opengist
|
||||||
opengist
|
opengist
|
||||||
build/
|
build/
|
||||||
docs/.vitepress/dist/
|
docs/.vitepress/dist/
|
||||||
docs/.vitepress/cache/
|
docs/.vitepress/cache/
|
||||||
|
helm/opengist/charts/
|
||||||
|
vendor/
|
||||||
|
|||||||
220
CHANGELOG.md
220
CHANGELOG.md
@@ -1,5 +1,225 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## [1.12.1](https://github.com/thomiceli/opengist/compare/v1.12.0...v1.12.1) - 2026-02-03
|
||||||
|
See here how to [update](https://opengist.io/docs/update) Opengist.
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- More translation strings (#605)
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Allow Access Tokens with Required Login (#611)
|
||||||
|
- Make text files renderable with mimetypes different than text/plain (#612)
|
||||||
|
- Improve security on raw files endpoint (#613)
|
||||||
|
|
||||||
|
> Admins of Opengist instances may want to run "Synchronize all gists previews" in the admin panel.
|
||||||
|
|
||||||
|
## [1.12.0](https://github.com/thomiceli/opengist/compare/v1.11.1...v1.12.0) - 2026-01-27
|
||||||
|
See here how to [update](https://opengist.io/docs/update) Opengist.
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Access tokens (#602)
|
||||||
|
- Fuzzy search for gist search (#555)
|
||||||
|
- Allow Unicode letters/numbers in topics (#597)
|
||||||
|
- Resize editor height (#600)
|
||||||
|
- More translation strings (#516) (#604)
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Don't panic on Go TCP errors (#601)
|
||||||
|
|
||||||
|
### Other
|
||||||
|
- Reduce footprint of Docker image (#515)
|
||||||
|
- Update Go + JS deps (#603)
|
||||||
|
- Configure Dependabot for updates on Go and NPM (#449)
|
||||||
|
|
||||||
|
### [Helm Chart](helm/opengist)
|
||||||
|
- Use existing pvc claim of provided (#547)
|
||||||
|
- Adds StatefulSet support (#549)
|
||||||
|
- Move Prom metrics to a dedicated port + support ServiceMonitor (#599)
|
||||||
|
|
||||||
|
## [1.11.1](https://github.com/thomiceli/opengist/compare/v1.11.0...v1.11.1) - 2025-09-30
|
||||||
|
See here how to [update](https://opengist.io/docs/update) Opengist.
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- More translation strings (#511)
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- CSV errors for rendering (#514)
|
||||||
|
|
||||||
|
### Other
|
||||||
|
- Reset default log level to warn
|
||||||
|
|
||||||
|
## [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.
|
||||||
|
|
||||||
|
|||||||
49
Dockerfile
49
Dockerfile
@@ -1,25 +1,18 @@
|
|||||||
FROM alpine:3.19 AS base
|
FROM alpine:3.22 AS base
|
||||||
|
|
||||||
RUN apk update && \
|
RUN apk update && \
|
||||||
apk add --no-cache \
|
apk add --no-cache \
|
||||||
make \
|
make \
|
||||||
shadow \
|
|
||||||
openssl \
|
|
||||||
openssh \
|
|
||||||
curl \
|
|
||||||
wget \
|
|
||||||
git \
|
|
||||||
gnupg \
|
|
||||||
xz \
|
|
||||||
gcc \
|
gcc \
|
||||||
|
git \
|
||||||
musl-dev \
|
musl-dev \
|
||||||
libstdc++
|
libstdc++
|
||||||
|
|
||||||
COPY --from=golang:1.22-alpine /usr/local/go/ /usr/local/go/
|
COPY --from=golang:1.25.6-alpine3.22 /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
|
||||||
|
|
||||||
COPY --from=node:20-alpine /usr/local/ /usr/local/
|
COPY --from=node:24.13.0-alpine3.22 /usr/local/ /usr/local/
|
||||||
ENV NODE_PATH="/usr/local/lib/node_modules"
|
ENV NODE_PATH="/usr/local/lib/node_modules"
|
||||||
ENV PATH="/usr/local/bin:${PATH}"
|
ENV PATH="/usr/local/bin:${PATH}"
|
||||||
|
|
||||||
@@ -29,11 +22,21 @@ COPY . .
|
|||||||
|
|
||||||
|
|
||||||
FROM base AS dev
|
FROM base AS dev
|
||||||
|
RUN apk add --no-cache \
|
||||||
|
openssl \
|
||||||
|
openssh-server \
|
||||||
|
curl \
|
||||||
|
wget \
|
||||||
|
git \
|
||||||
|
gnupg \
|
||||||
|
xz
|
||||||
|
|
||||||
EXPOSE 6157 2222 16157
|
EXPOSE 6157 6158 2222 16157
|
||||||
VOLUME /opengist
|
|
||||||
|
|
||||||
RUN git config --global --add safe.directory /opengist
|
RUN git config --global --add safe.directory /opengist
|
||||||
|
RUN make install
|
||||||
|
|
||||||
|
VOLUME /opengist
|
||||||
|
|
||||||
CMD ["make", "watch"]
|
CMD ["make", "watch"]
|
||||||
|
|
||||||
@@ -43,33 +46,25 @@ FROM base AS build
|
|||||||
RUN make
|
RUN make
|
||||||
|
|
||||||
|
|
||||||
FROM alpine:3.19 as prod
|
FROM alpine:3.22 AS prod
|
||||||
|
|
||||||
RUN apk update && \
|
RUN apk update && \
|
||||||
apk add --no-cache \
|
apk add --no-cache \
|
||||||
shadow \
|
shadow \
|
||||||
openssl \
|
openssh-server \
|
||||||
openssh \
|
|
||||||
curl \
|
curl \
|
||||||
wget \
|
git
|
||||||
git \
|
|
||||||
gnupg \
|
|
||||||
xz \
|
|
||||||
gcc \
|
|
||||||
musl-dev \
|
|
||||||
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
|
|
||||||
|
|
||||||
WORKDIR /app/opengist
|
WORKDIR /app/opengist
|
||||||
|
|
||||||
|
COPY --from=build --chown=opengist:opengist /opengist/config.yml /config.yml
|
||||||
COPY --from=build --chown=opengist:opengist /opengist/opengist .
|
COPY --from=build --chown=opengist:opengist /opengist/opengist .
|
||||||
COPY --from=build --chown=opengist:opengist /opengist/docker ./docker
|
COPY --from=build --chown=opengist:opengist /opengist/docker ./docker
|
||||||
|
|
||||||
EXPOSE 6157 2222
|
EXPOSE 6157 6158 2222
|
||||||
VOLUME /opengist
|
VOLUME /opengist
|
||||||
HEALTHCHECK --interval=60s --timeout=30s --start-period=15s --retries=3 CMD curl -f http://localhost:6157/healthcheck || exit 1
|
HEALTHCHECK --interval=60s --timeout=30s --start-period=15s --retries=3 CMD curl -f http://localhost:6157/healthcheck || exit 1
|
||||||
ENTRYPOINT ["./docker/entrypoint.sh"]
|
ENTRYPOINT ["./docker/entrypoint.sh"]
|
||||||
|
|||||||
12
Makefile
12
Makefile
@@ -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
|
||||||
|
|
||||||
@@ -18,7 +19,6 @@ install:
|
|||||||
build_frontend:
|
build_frontend:
|
||||||
@echo "Building frontend assets..."
|
@echo "Building frontend assets..."
|
||||||
npx vite -c public/vite.config.js build
|
npx vite -c public/vite.config.js build
|
||||||
@EMBED=1 npx postcss 'public/assets/embed-*.css' -c public/postcss.config.js --replace # until we can .nest { @tailwind } in Sass
|
|
||||||
|
|
||||||
build_backend:
|
build_backend:
|
||||||
@echo "Building Opengist binary..."
|
@echo "Building Opengist binary..."
|
||||||
@@ -38,11 +38,11 @@ build_dev_docker:
|
|||||||
docker build -t $(BINARY_NAME)-dev:latest --target dev .
|
docker build -t $(BINARY_NAME)-dev:latest --target dev .
|
||||||
|
|
||||||
run_dev_docker:
|
run_dev_docker:
|
||||||
docker run -v .:/opengist -p 6157:6157 -p 16157:16157 -p 2222:2222 -v $(HOME)/.opengist-dev:/root/.opengist --rm $(BINARY_NAME)-dev:latest
|
docker run -v .:/opengist -v /opengist/node_modules -p 6157:6157 -p 16157:16157 -p 2222:2222 -v $(HOME)/.opengist-dev:/root/.opengist --rm $(BINARY_NAME)-dev:latest
|
||||||
|
|
||||||
watch_frontend:
|
watch_frontend:
|
||||||
@echo "Building frontend assets..."
|
@echo "Building frontend assets..."
|
||||||
npx vite -c public/vite.config.js dev --port 16157 --host
|
npx vite -c public/vite.config.js --port 16157 --host
|
||||||
|
|
||||||
watch_backend:
|
watch_backend:
|
||||||
@echo "Building Opengist binary..."
|
@echo "Building Opengist binary..."
|
||||||
@@ -53,8 +53,8 @@ watch:
|
|||||||
|
|
||||||
clean:
|
clean:
|
||||||
@echo "Cleaning up build artifacts..."
|
@echo "Cleaning up build artifacts..."
|
||||||
@rm -f $(BINARY_NAME) public/manifest.json
|
@rm -f $(BINARY_NAME)
|
||||||
@rm -rf public/assets build
|
@rm -rf public/assets public/.vite build
|
||||||
|
|
||||||
clean_docker:
|
clean_docker:
|
||||||
@echo "Cleaning up Docker image..."
|
@echo "Cleaning up Docker image..."
|
||||||
@@ -72,7 +72,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
|
||||||
27
README.md
27
README.md
@@ -1,10 +1,10 @@
|
|||||||
# Opengist
|
# Opengist
|
||||||
|
|
||||||
<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/img/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
|
|||||||

|

|
||||||
[](https://github.com/thomiceli/opengist/actions/workflows/go.yml)
|
[](https://github.com/thomiceli/opengist/actions/workflows/go.yml)
|
||||||
[](https://goreportcard.com/report/github.com/thomiceli/opengist)
|
[](https://goreportcard.com/report/github.com/thomiceli/opengist)
|
||||||
|
[](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.12
|
||||||
```
|
```
|
||||||
|
|
||||||
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.12
|
||||||
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.12.1/opengist1.12.1-linux-amd64.tar.gz
|
||||||
|
|
||||||
tar xzvf opengist1.7.5-linux-amd64.tar.gz
|
tar xzvf opengist1.12.1-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
|
||||||
|
|||||||
59
config.yml
59
config.yml
@@ -1,5 +1,5 @@
|
|||||||
# 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
|
||||||
@@ -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,18 @@ 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 Prometheus metrics server (either `true` or `false`). Default: false
|
||||||
|
metrics.enabled: false
|
||||||
|
|
||||||
|
# The host on which the metrics server should bind. Default: 0.0.0.0
|
||||||
|
metrics.host: 0.0.0.0
|
||||||
|
|
||||||
|
# The port on which the metrics server should listen. Default: 6158
|
||||||
|
metrics.port: 6158
|
||||||
|
|
||||||
# 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 +87,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 +111,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/
|
||||||
|
|||||||
@@ -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
|
|
||||||
```
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
---
|
|
||||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
|
||||||
kind: Kustomization
|
|
||||||
metadata:
|
|
||||||
name: opengist
|
|
||||||
|
|
||||||
resources:
|
|
||||||
- deployment.yaml
|
|
||||||
- pvc.yaml
|
|
||||||
- ingress.yaml
|
|
||||||
- service.yaml
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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"
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ export default defineConfig({
|
|||||||
},
|
},
|
||||||
themeConfig: {
|
themeConfig: {
|
||||||
// https://vitepress.dev/reference/default-theme-config
|
// https://vitepress.dev/reference/default-theme-config
|
||||||
logo: 'https://raw.githubusercontent.com/thomiceli/opengist/master/public/opengist.svg',
|
logo: 'https://raw.githubusercontent.com/thomiceli/opengist/master/public/img/opengist.svg',
|
||||||
logoLink: '/',
|
logoLink: '/',
|
||||||
nav: [
|
nav: [
|
||||||
{ text: 'Demo', link: 'https://demo.opengist.io' },
|
{ text: 'Demo', link: 'https://demo.opengist.io' },
|
||||||
@@ -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,17 +37,25 @@ 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
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: 'Usage', base: '/docs/usage', items: [
|
text: 'Usage', base: '/docs/usage', items: [
|
||||||
{text: 'Init via Git', link: '/init-via-git'},
|
{text: 'Init via Git', link: '/init-via-git'},
|
||||||
{text: 'Embed Gist', link: '/embed'},
|
{text: 'Embed Gist', link: '/embed'},
|
||||||
|
{text: 'Access Tokens', link: '/access-tokens'},
|
||||||
{text: 'Gist as JSON', link: '/gist-json'},
|
{text: 'Gist as JSON', link: '/gist-json'},
|
||||||
{text: 'Import Gists from Github', link: '/import-from-github-gist'},
|
{text: 'Import Gists from Github', link: '/import-from-github-gist'},
|
||||||
{text: 'Git push options', link: '/git-push-options'},
|
{text: 'Git push options', link: '/git-push-options'},
|
||||||
|
|||||||
@@ -17,9 +17,9 @@ export default {
|
|||||||
<header class="hero">
|
<header class="hero">
|
||||||
<div class="mx-auto max-w-7xl px-6 lg:px-8">
|
<div class="mx-auto max-w-7xl px-6 lg:px-8">
|
||||||
<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/img/opengist.svg" alt="" >
|
||||||
<a target="_blank" href="https://github.com/thomiceli/opengist/releases" class="inline-flex items-center rounded-full bg-indigo-100 hover:bg-indigo-200 px-4 py-1.5 text-lg font-medium text-indigo-700">
|
<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.12</span>
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-4">
|
<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>
|
||||||
@@ -98,4 +98,4 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
11
docs/administration/manage-admins.md
Normal file
11
docs/administration/manage-admins.md
Normal 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
|
||||||
|
```
|
||||||
@@ -4,38 +4,51 @@ 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 server (`true` or `false`) |
|
||||||
| ssh.keygen-executable | OG_SSH_KEYGEN_EXECUTABLE | `ssh-keygen` | Path to the SSH key generation executable. |
|
| metrics.host | OG_METRICS_HOST | `0.0.0.0` | The host on which the metrics server should bind. |
|
||||||
| github.client-key | OG_GITHUB_CLIENT_KEY | none | The client key for the GitHub OAuth application. |
|
| metrics.port | OG_METRICS_PORT | `6158` | The port on which the metrics server should listen. |
|
||||||
| github.secret | OG_GITHUB_SECRET | none | The secret for the GitHub OAuth application. |
|
| ssh.git-enabled | OG_SSH_GIT_ENABLED | `true` | Enable or disable git operations (clone, pull, push) via SSH. (`true` or `false`) |
|
||||||
| gitlab.client-key | OG_GITLAB_CLIENT_KEY | none | The client key for the GitLab OAuth application. |
|
| ssh.host | OG_SSH_HOST | `0.0.0.0` | The host on which the SSH server should bind. |
|
||||||
| gitlab.secret | OG_GITLAB_SECRET | none | The secret for the GitLab OAuth application. |
|
| ssh.port | OG_SSH_PORT | `2222` | The port on which the SSH server should listen. |
|
||||||
| gitlab.url | OG_GITLAB_URL | `https://gitlab.com/` | The URL of the GitLab instance. |
|
| 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.name | OG_GITLAB_NAME | `GitLab` | The name of the GitLab instance. It is displayed in the OAuth login button. |
|
| ssh.keygen-executable | OG_SSH_KEYGEN_EXECUTABLE | `ssh-keygen` | Path to the SSH key generation executable. |
|
||||||
| gitea.client-key | OG_GITEA_CLIENT_KEY | none | The client key for the Gitea OAuth application. |
|
| github.client-key | OG_GITHUB_CLIENT_KEY | none | The client key for the GitHub OAuth application. |
|
||||||
| gitea.secret | OG_GITEA_SECRET | none | The secret for the Gitea OAuth application. |
|
| github.secret | OG_GITHUB_SECRET | none | The secret for the GitHub OAuth application. |
|
||||||
| gitea.url | OG_GITEA_URL | `https://gitea.com/` | The URL of the Gitea instance. |
|
| gitlab.client-key | OG_GITLAB_CLIENT_KEY | none | The client key for the GitLab OAuth application. |
|
||||||
| gitea.name | OG_GITEA_NAME | `Gitea` | The name of the Gitea instance. It is displayed in the OAuth login button. |
|
| gitlab.secret | OG_GITLAB_SECRET | none | The secret for the GitLab OAuth application. |
|
||||||
| oidc.client-key | OG_OIDC_CLIENT_KEY | none | The client key for the OpenID application. |
|
| gitlab.url | OG_GITLAB_URL | `https://gitlab.com/` | The URL of the GitLab instance. |
|
||||||
| oidc.secret | OG_OIDC_SECRET | none | The secret for the OpenID application. |
|
| gitlab.name | OG_GITLAB_NAME | `GitLab` | The name of the GitLab instance. It is displayed in the OAuth login button. |
|
||||||
| oidc.discovery-url | OG_OIDC_DISCOVERY_URL | none | Discovery endpoint of the OpenID provider. |
|
| gitea.client-key | OG_GITEA_CLIENT_KEY | none | The client key for the Gitea OAuth application. |
|
||||||
| custom.logo | OG_CUSTOM_LOGO | none | Path to an image, relative to $opengist-home/custom. |
|
| gitea.secret | OG_GITEA_SECRET | none | The secret for the Gitea OAuth application. |
|
||||||
| custom.favicon | OG_CUSTOM_FAVICON | none | Path to an image, relative to $opengist-home/custom. |
|
| gitea.url | OG_GITEA_URL | `https://gitea.com/` | The URL of the Gitea instance. |
|
||||||
| custom.static-links | OG_CUSTOM_STATIC_LINK_#_(PATH,NAME) | none | Path and name to custom links, more info [here](custom-links.md). |
|
| gitea.name | OG_GITEA_NAME | `Gitea` | The name of the Gitea instance. It is displayed in the OAuth login button. |
|
||||||
|
| oidc.provider-name | OG_OIDC_PROVIDER_NAME | none | The name of the OIDC provider |
|
||||||
|
| 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). |
|
||||||
|
|||||||
@@ -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
|
||||||
|
```
|
||||||
@@ -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"
|
||||||
|
```
|
||||||
|
|||||||
47
docs/configuration/databases/mysql.md
Normal file
47
docs/configuration/databases/mysql.md
Normal 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
|
||||||
|
```
|
||||||
46
docs/configuration/databases/postgresql.md
Normal file
46
docs/configuration/databases/postgresql.md
Normal 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
|
||||||
|
```
|
||||||
44
docs/configuration/databases/sqlite.md
Normal file
44
docs/configuration/databases/sqlite.md
Normal 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
|
||||||
|
```
|
||||||
59
docs/configuration/metrics.md
Normal file
59
docs/configuration/metrics.md
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
# 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 server 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 on a separate server at `http://0.0.0.0:6158/metrics` by default.
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
The metrics server runs on a separate port from the main application. By default, it binds to `0.0.0.0` (all interfaces) on port `6158`.
|
||||||
|
|
||||||
|
| Config Key | Environment Variable | Default | Description |
|
||||||
|
|----------------|---------------------|-------------|------------------------------------------------|
|
||||||
|
| metrics.enabled | OG_METRICS_ENABLED | `false` | Enable or disable the metrics server |
|
||||||
|
| metrics.host | OG_METRICS_HOST | `0.0.0.0` | The host on which the metrics server binds |
|
||||||
|
| metrics.port | OG_METRICS_PORT | `6158` | The port on which the metrics server listens |
|
||||||
|
|
||||||
|
Example configuration:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
metrics.enabled: true
|
||||||
|
metrics.host: 0.0.0.0
|
||||||
|
metrics.port: 6158
|
||||||
|
```
|
||||||
|
|
||||||
|
## 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 server binds to `0.0.0.0` by default, making it accessible on all network interfaces. This default works well for containerized deployments (Docker, Kubernetes) where network isolation is handled at the infrastructure level.
|
||||||
|
|
||||||
|
For bare-metal or VM deployments where the metrics port may be exposed, consider restricting to localhost by setting `metrics.host: 127.0.0.1` to only allow local access.
|
||||||
@@ -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.
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -25,13 +25,14 @@ 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.25+)
|
||||||
* [Node.js](https://nodejs.org/en/download/) (16+)
|
* [Node.js](https://nodejs.org/en/download/) (20+)
|
||||||
* [Make](https://linux.die.net/man/1/make) (optional, but easier)
|
* [Make](https://linux.die.net/man/1/make) (optional, but easier)
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
git clone git@github.com:thomiceli/opengist.git
|
git clone git@github.com:thomiceli/opengist.git
|
||||||
cd opengist
|
cd opengist
|
||||||
|
make install
|
||||||
make watch
|
make watch
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -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.12.1/opengist1.12.1-linux-amd64.tar.gz
|
||||||
|
|
||||||
tar xzvf opengist1.7.5-linux-amd64.tar.gz
|
tar xzvf opengist1.12.1-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`
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
15
docs/installation/kubernetes.md
Normal file
15
docs/installation/kubernetes.md
Normal 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.
|
||||||
@@ -2,15 +2,15 @@
|
|||||||
|
|
||||||
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.25+)
|
||||||
* [Node.js](https://nodejs.org/en/download/) (16+)
|
* [Node.js](https://nodejs.org/en/download/) (20+)
|
||||||
* [Make](https://linux.die.net/man/1/make) (optional, but easier)
|
* [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
|
||||||
cd opengist
|
cd opengist
|
||||||
|
|
||||||
git checkout v1.7.5 # optional, to checkout the latest release
|
git checkout v1.12.1 # optional, to checkout the latest release
|
||||||
|
|
||||||
make
|
make
|
||||||
./opengist
|
./opengist
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Opengist
|
# Opengist
|
||||||
|
|
||||||
<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/img/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.
|
||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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.12.1/opengist1.12.1-linux-amd64.tar.gz
|
||||||
|
|
||||||
tar xzvf opengist1.7.5-linux-amd64.tar.gz
|
tar xzvf opengist1.12.1-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`
|
||||||
|
|||||||
26
docs/usage/access-tokens.md
Normal file
26
docs/usage/access-tokens.md
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
# Access tokens
|
||||||
|
|
||||||
|
Access tokens are used to access your private gists and their raw content. For now, it is the only use while a future API is being developed.
|
||||||
|
|
||||||
|
## Creating an access token
|
||||||
|
|
||||||
|
To create an access token, follow these steps:
|
||||||
|
1. Go to Settings
|
||||||
|
2. Select the "Access Tokens" menu
|
||||||
|
3. Choose a name for your token, the scope and an expiration date (optional), then click "Create Access Token"
|
||||||
|
|
||||||
|
## Using an access token
|
||||||
|
|
||||||
|
Once you have created an access token, you can use it to access your private gists with it.
|
||||||
|
|
||||||
|
Replace `<token>` with your actual access token in the following examples.
|
||||||
|
|
||||||
|
```shell
|
||||||
|
# Access raw content of a private gist, latest revision for "file.txt". Note: this URL is obtained from the "Raw" button on the gist page.
|
||||||
|
curl -H "Authorization: Token <token>" \
|
||||||
|
http://opengist.example.com/user/gist/raw/HEAD/file.txt
|
||||||
|
|
||||||
|
# Access the JSON representation of a private gist. See "Gist as JSON" documentation for more details.
|
||||||
|
curl -H "Authorization: Token <token>" \
|
||||||
|
http://opengist.example.com/user/gist.json
|
||||||
|
```
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
142
go.mod
142
go.mod
@@ -1,95 +1,125 @@
|
|||||||
module github.com/thomiceli/opengist
|
module github.com/thomiceli/opengist
|
||||||
|
|
||||||
go 1.22
|
go 1.25.5
|
||||||
|
|
||||||
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.23.1
|
||||||
github.com/blevesearch/bleve/v2 v2.4.0
|
github.com/blevesearch/bleve/v2 v2.5.7
|
||||||
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.12
|
||||||
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.12
|
||||||
|
github.com/go-playground/validator/v10 v10.30.1
|
||||||
|
github.com/go-webauthn/webauthn v0.15.0
|
||||||
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.4
|
||||||
github.com/labstack/echo/v4 v4.12.0
|
github.com/labstack/echo/v4 v4.15.0
|
||||||
github.com/markbates/goth v1.80.0
|
github.com/markbates/goth v1.82.0
|
||||||
github.com/rs/zerolog v1.33.0
|
github.com/meilisearch/meilisearch-go v0.36.0
|
||||||
github.com/stretchr/testify v1.9.0
|
github.com/pquerna/otp v1.5.0
|
||||||
github.com/urfave/cli/v2 v2.27.2
|
github.com/prometheus/client_golang v1.23.2
|
||||||
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.11.1
|
||||||
|
github.com/urfave/cli/v2 v2.27.7
|
||||||
|
github.com/yuin/goldmark v1.7.16
|
||||||
|
github.com/yuin/goldmark-emoji v1.0.6
|
||||||
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.6.0
|
||||||
golang.org/x/crypto v0.23.0
|
golang.org/x/crypto v0.47.0
|
||||||
golang.org/x/text v0.15.0
|
golang.org/x/text v0.33.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.6.0
|
||||||
|
gorm.io/driver/postgres v1.6.0
|
||||||
|
gorm.io/gorm v1.31.1
|
||||||
)
|
)
|
||||||
|
|
||||||
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.1.0 // indirect
|
||||||
github.com/blevesearch/bleve_index_api v1.1.8 // indirect
|
github.com/RoaringBitmap/roaring/v2 v2.14.4 // indirect
|
||||||
github.com/blevesearch/geo v0.1.20 // indirect
|
github.com/andybalholm/brotli v1.2.0 // indirect
|
||||||
github.com/blevesearch/go-faiss v1.0.16 // indirect
|
github.com/beorn7/perks v1.0.1 // indirect
|
||||||
|
github.com/bits-and-blooms/bitset v1.24.4 // indirect
|
||||||
|
github.com/blevesearch/bleve_index_api v1.3.1 // indirect
|
||||||
|
github.com/blevesearch/geo v0.2.4 // indirect
|
||||||
|
github.com/blevesearch/go-faiss v1.0.27 // indirect
|
||||||
github.com/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.2.0 // indirect
|
||||||
github.com/blevesearch/scorch_segment_api/v2 v2.2.13 // indirect
|
github.com/blevesearch/scorch_segment_api/v2 v2.4.1 // 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.2.0 // indirect
|
||||||
github.com/blevesearch/zapx/v11 v11.3.10 // indirect
|
github.com/blevesearch/zapx/v11 v11.4.2 // indirect
|
||||||
github.com/blevesearch/zapx/v12 v12.3.10 // indirect
|
github.com/blevesearch/zapx/v12 v12.4.2 // indirect
|
||||||
github.com/blevesearch/zapx/v13 v13.3.10 // indirect
|
github.com/blevesearch/zapx/v13 v13.4.2 // indirect
|
||||||
github.com/blevesearch/zapx/v14 v14.3.10 // indirect
|
github.com/blevesearch/zapx/v14 v14.4.2 // indirect
|
||||||
github.com/blevesearch/zapx/v15 v15.3.13 // indirect
|
github.com/blevesearch/zapx/v15 v15.4.2 // indirect
|
||||||
github.com/blevesearch/zapx/v16 v16.1.0 // indirect
|
github.com/blevesearch/zapx/v16 v16.3.0 // indirect
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.4 // indirect
|
github.com/boombuler/barcode v1.1.0 // indirect
|
||||||
|
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||||
|
github.com/clipperhouse/stringish v0.1.1 // indirect
|
||||||
|
github.com/clipperhouse/uax29/v2 v2.4.0 // indirect
|
||||||
|
github.com/cpuguy83/go-md2man/v2 v2.0.7 // 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.9.0 // indirect
|
||||||
|
github.com/glebarez/go-sqlite v1.22.0 // indirect
|
||||||
|
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 // indirect
|
||||||
|
github.com/go-chi/chi/v5 v5.2.3 // indirect
|
||||||
github.com/go-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.3 // indirect
|
||||||
github.com/golang/geo v0.0.0-20230421003525-6adc56603217 // indirect
|
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
|
||||||
github.com/golang/protobuf v1.5.4 // indirect
|
github.com/go-webauthn/x v0.1.26 // indirect
|
||||||
github.com/golang/snappy v0.0.4 // indirect
|
github.com/golang-jwt/jwt/v5 v5.3.0 // indirect
|
||||||
|
github.com/golang/snappy v1.0.0 // indirect
|
||||||
|
github.com/google/go-tpm v0.9.6 // 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.8.0 // 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/json-iterator/go v1.1.12 // indirect
|
github.com/json-iterator/go v1.1.12 // 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/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.19 // 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/ncruces/go-strftime v0.1.9 // indirect
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||||
|
github.com/ncruces/go-strftime v1.0.0 // 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.2 // indirect
|
||||||
|
github.com/prometheus/common v0.67.5 // indirect
|
||||||
|
github.com/prometheus/procfs v0.19.2 // 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/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/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect
|
github.com/x448/float16 v0.8.4 // indirect
|
||||||
go.etcd.io/bbolt v1.3.10 // indirect
|
github.com/xrash/smetrics v0.0.0-20250705151800-55b8f293f342 // indirect
|
||||||
golang.org/x/net v0.25.0 // indirect
|
go.etcd.io/bbolt v1.4.3 // indirect
|
||||||
golang.org/x/oauth2 v0.20.0 // indirect
|
go.yaml.in/yaml/v2 v2.4.3 // indirect
|
||||||
golang.org/x/sys v0.20.0 // indirect
|
golang.org/x/exp v0.0.0-20260112195511-716be5621a96 // indirect
|
||||||
golang.org/x/time v0.5.0 // indirect
|
golang.org/x/net v0.49.0 // indirect
|
||||||
google.golang.org/protobuf v1.34.1 // indirect
|
golang.org/x/oauth2 v0.34.0 // indirect
|
||||||
modernc.org/libc v1.51.0 // indirect
|
golang.org/x/sync v0.19.0 // indirect
|
||||||
modernc.org/mathutil v1.6.0 // indirect
|
golang.org/x/sys v0.40.0 // indirect
|
||||||
modernc.org/memory v1.8.0 // indirect
|
golang.org/x/time v0.14.0 // indirect
|
||||||
modernc.org/sqlite v1.30.0 // indirect
|
google.golang.org/protobuf v1.36.11 // indirect
|
||||||
|
modernc.org/libc v1.67.7 // indirect
|
||||||
|
modernc.org/mathutil v1.7.1 // indirect
|
||||||
|
modernc.org/memory v1.11.0 // indirect
|
||||||
|
modernc.org/sqlite v1.44.3 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
399
go.sum
399
go.sum
@@ -1,160 +1,214 @@
|
|||||||
|
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
||||||
|
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
||||||
|
github.com/Azure/go-ntlmssp v0.1.0 h1:DjFo6YtWzNqNvQdrwEyr/e4nhU3vRiwenz5QX7sFz+A=
|
||||||
|
github.com/Azure/go-ntlmssp v0.1.0/go.mod h1:NYqdhxd/8aAct/s4qSYZEerdPuH1liG2/X9DiVTbhpk=
|
||||||
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.14.4 h1:4aKySrrg9G/5oRtJ3TrZLObVqxgQ9f1znCRBwEwjuVw=
|
||||||
github.com/RoaringBitmap/roaring v1.9.4/go.mod h1:6AXUsoIEzDTFFQCe1RbGA6uFONMhvejWj5rqITANK90=
|
github.com/RoaringBitmap/roaring/v2 v2.14.4/go.mod h1:oMvV6omPWr+2ifRdeZvVJyaz+aoEUopyv5iH0u/+wbY=
|
||||||
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.23.1 h1:nv2AVZdTyClGbVQkIzlDm/rnhk1E9bU9nXwmZ/Vk/iY=
|
||||||
github.com/alecthomas/chroma/v2 v2.14.0/go.mod h1:QolEbTfmUHIMVpBqxeDnNBj2uoeI4EbYP4i6n68SG4I=
|
github.com/alecthomas/chroma/v2 v2.23.1/go.mod h1:NqVhfBR0lte5Ouh3DcthuUCTUpDC9cxBOfyMbMQPs3o=
|
||||||
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.2 h1:SU73FTI9D1P5UNtvseffFSGmdNci/O6RsqzeXJtP0Qs=
|
||||||
github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
|
github.com/alecthomas/repr v0.5.2/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
|
||||||
github.com/bits-and-blooms/bitset v1.12.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
|
github.com/alexbrainman/sspi v0.0.0-20250919150558-7d374ff0d59e h1:4dAU9FXIyQktpoUAgOJK3OTFc/xug0PCXYCqU0FgDKI=
|
||||||
github.com/bits-and-blooms/bitset v1.13.0 h1:bAQ9OPNFYbGHV6Nez0tmNI0RiEu7/hxlYJRUA0wFAVE=
|
github.com/alexbrainman/sspi v0.0.0-20250919150558-7d374ff0d59e/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4=
|
||||||
github.com/bits-and-blooms/bitset v1.13.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
|
github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ=
|
||||||
github.com/blevesearch/bleve/v2 v2.4.0 h1:2xyg+Wv60CFHYccXc+moGxbL+8QKT/dZK09AewHgKsg=
|
github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
|
||||||
github.com/blevesearch/bleve/v2 v2.4.0/go.mod h1:IhQHoFAbHgWKYavb9rQgQEJJVMuY99cKdQ0wPpst2aY=
|
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||||
github.com/blevesearch/bleve_index_api v1.1.8 h1:rJUccYfWqRY2/BGowlsv1lwrLKYK/zPE6hgNn1pTGdk=
|
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||||
github.com/blevesearch/bleve_index_api v1.1.8/go.mod h1:PbcwjIcRmjhGbkS/lJCpfgVSMROV6TRubGGAODaK1W8=
|
github.com/bits-and-blooms/bitset v1.24.4 h1:95H15Og1clikBrKr/DuzMXkQzECs1M6hhoGXLwLQOZE=
|
||||||
github.com/blevesearch/geo v0.1.20 h1:paaSpu2Ewh/tn5DKn/FB5SzvH0EWupxHEIwbCk/QPqM=
|
github.com/bits-and-blooms/bitset v1.24.4/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
|
||||||
github.com/blevesearch/geo v0.1.20/go.mod h1:DVG2QjwHNMFmjo+ZgzrIq2sfCh6rIHzy9d9d0B59I6w=
|
github.com/blevesearch/bleve/v2 v2.5.7 h1:2d9YrL5zrX5EBBW++GOaEKjE+NPWeZGaX77IM26m1Z8=
|
||||||
github.com/blevesearch/go-faiss v1.0.16 h1:lfzXzzjO1mAf15MRiRY5yz6KVGr02CyRrr7m0z70Ih8=
|
github.com/blevesearch/bleve/v2 v2.5.7/go.mod h1:yj0NlS7ocGC4VOSAedqDDMktdh2935v2CSWOCDMHdSA=
|
||||||
github.com/blevesearch/go-faiss v1.0.16/go.mod h1:jrxHrbl42X/RnDPI+wBoZU8joxxuRwedrxqswQ3xfU8=
|
github.com/blevesearch/bleve_index_api v1.3.1 h1:LdH3CQgBbIZ5UI/5Pykz87e0jfeQtVnrdZ2WUBrHHwU=
|
||||||
|
github.com/blevesearch/bleve_index_api v1.3.1/go.mod h1:xvd48t5XMeeioWQ5/jZvgLrV98flT2rdvEJ3l/ki4Ko=
|
||||||
|
github.com/blevesearch/geo v0.2.4 h1:ECIGQhw+QALCZaDcogRTNSJYQXRtC8/m8IKiA706cqk=
|
||||||
|
github.com/blevesearch/geo v0.2.4/go.mod h1:K56Q33AzXt2YExVHGObtmRSFYZKYGv0JEN5mdacJJR8=
|
||||||
|
github.com/blevesearch/go-faiss v1.0.27 h1:7cBImYDDQ82WJd5RUZ1ie6zXztCsC73W94ZzwOjkatk=
|
||||||
|
github.com/blevesearch/go-faiss v1.0.27/go.mod h1:OMGQwOaRRYxrmeNdMrXJPvVx8gBnvE5RYrr0BahNnkk=
|
||||||
github.com/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.2.0 h1:l33nNKPFcBjJUMwem6sAYJPUzhUCABoK9FxZDGiFNBI=
|
||||||
github.com/blevesearch/mmap-go v1.0.4/go.mod h1:EWmEAOmdAS9z/pi/+Toxu99DnsbhG1TIxUoRmJw/pSs=
|
github.com/blevesearch/mmap-go v1.2.0/go.mod h1:Vd6+20GBhEdwJnU1Xohgt88XCD/CTWcqbCNxkZpyBo0=
|
||||||
github.com/blevesearch/scorch_segment_api/v2 v2.2.13 h1:UfbyRpIMdcaNsgciGYS9Pib7N3xd3EEw8KKbd/aDBlA=
|
github.com/blevesearch/scorch_segment_api/v2 v2.4.1 h1:os52/JeCSLZ0YUkOuLk/Z7pu0SKUMofDPUg+VnbrRD0=
|
||||||
github.com/blevesearch/scorch_segment_api/v2 v2.2.13/go.mod h1:osG1bAUONZB2r/ozUJwjbuOzPvdrULWaLOm+vsMANsk=
|
github.com/blevesearch/scorch_segment_api/v2 v2.4.1/go.mod h1:zvilBm4BNfbnTRLW7KgCTNgk2R31JaWzwRc2BEcD7Is=
|
||||||
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.2.0 h1:xkDiOEsHc2t3Cp0NsNZZ36pvc130sCzcGKOPMzXe+e0=
|
||||||
github.com/blevesearch/vellum v1.0.10/go.mod h1:ul1oT0FhSMDIExNjIxHqJoGpVrBpKCdgDQNxfqgJt7k=
|
github.com/blevesearch/vellum v1.2.0/go.mod h1:uEcfBJz7mAOf0Kvq6qoEKQQkLODBF46SINYNkZNae4k=
|
||||||
github.com/blevesearch/zapx/v11 v11.3.10 h1:hvjgj9tZ9DeIqBCxKhi70TtSZYMdcFn7gDb71Xo/fvk=
|
github.com/blevesearch/zapx/v11 v11.4.2 h1:l46SV+b0gFN+Rw3wUI1YdMWdSAVhskYuvxlcgpQFljs=
|
||||||
github.com/blevesearch/zapx/v11 v11.3.10/go.mod h1:0+gW+FaE48fNxoVtMY5ugtNHHof/PxCqh7CnhYdnMzQ=
|
github.com/blevesearch/zapx/v11 v11.4.2/go.mod h1:4gdeyy9oGa/lLa6D34R9daXNUvfMPZqUYjPwiLmekwc=
|
||||||
github.com/blevesearch/zapx/v12 v12.3.10 h1:yHfj3vXLSYmmsBleJFROXuO08mS3L1qDCdDK81jDl8s=
|
github.com/blevesearch/zapx/v12 v12.4.2 h1:fzRbhllQmEMUuAQ7zBuMvKRlcPA5ESTgWlDEoB9uQNE=
|
||||||
github.com/blevesearch/zapx/v12 v12.3.10/go.mod h1:0yeZg6JhaGxITlsS5co73aqPtM04+ycnI6D1v0mhbCs=
|
github.com/blevesearch/zapx/v12 v12.4.2/go.mod h1:TdFmr7afSz1hFh/SIBCCZvcLfzYvievIH6aEISCte58=
|
||||||
github.com/blevesearch/zapx/v13 v13.3.10 h1:0KY9tuxg06rXxOZHg3DwPJBjniSlqEgVpxIqMGahDE8=
|
github.com/blevesearch/zapx/v13 v13.4.2 h1:46PIZCO/ZuKZYgxI8Y7lOJqX3Irkc3N8W82QTK3MVks=
|
||||||
github.com/blevesearch/zapx/v13 v13.3.10/go.mod h1:w2wjSDQ/WBVeEIvP0fvMJZAzDwqwIEzVPnCPrz93yAk=
|
github.com/blevesearch/zapx/v13 v13.4.2/go.mod h1:knK8z2NdQHlb5ot/uj8wuvOq5PhDGjNYQQy0QDnopZk=
|
||||||
github.com/blevesearch/zapx/v14 v14.3.10 h1:SG6xlsL+W6YjhX5N3aEiL/2tcWh3DO75Bnz77pSwwKU=
|
github.com/blevesearch/zapx/v14 v14.4.2 h1:2SGHakVKd+TrtEqpfeq8X+So5PShQ5nW6GNxT7fWYz0=
|
||||||
github.com/blevesearch/zapx/v14 v14.3.10/go.mod h1:qqyuR0u230jN1yMmE4FIAuCxmahRQEOehF78m6oTgns=
|
github.com/blevesearch/zapx/v14 v14.4.2/go.mod h1:rz0XNb/OZSMjNorufDGSpFpjoFKhXmppH9Hi7a877D8=
|
||||||
github.com/blevesearch/zapx/v15 v15.3.13 h1:6EkfaZiPlAxqXz0neniq35my6S48QI94W/wyhnpDHHQ=
|
github.com/blevesearch/zapx/v15 v15.4.2 h1:sWxpDE0QQOTjyxYbAVjt3+0ieu8NCE0fDRaFxEsp31k=
|
||||||
github.com/blevesearch/zapx/v15 v15.3.13/go.mod h1:Turk/TNRKj9es7ZpKK95PS7f6D44Y7fAFy8F4LXQtGg=
|
github.com/blevesearch/zapx/v15 v15.4.2/go.mod h1:1pssev/59FsuWcgSnTa0OeEpOzmhtmr/0/11H0Z8+Nw=
|
||||||
github.com/blevesearch/zapx/v16 v16.1.0 h1:bHsyowFqU0QA+uVDJCjifv9OvPGb8htkV52Yc/wT6xs=
|
github.com/blevesearch/zapx/v16 v16.3.0 h1:hF6VlN15E9CB40RMPyqOIhlDw1OOo9RItumhKMQktxw=
|
||||||
github.com/blevesearch/zapx/v16 v16.1.0/go.mod h1:P0h9lKRyl4EKksAWfxwCQ5I5pLB9jH2XD8bhYHuIYuc=
|
github.com/blevesearch/zapx/v16 v16.3.0/go.mod h1:zCFjv7McXWm1C8rROL+3mUoD5WYe2RKsZP3ufqcYpLY=
|
||||||
github.com/chromedp/cdproto v0.0.0-20230220211738-2b1ec77315c9 h1:wMSvdj3BswqfQOXp2R1bJOAE7xIQLt2dlMQDMf836VY=
|
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
|
||||||
github.com/chromedp/cdproto v0.0.0-20230220211738-2b1ec77315c9/go.mod h1:GKljq0VrfU4D5yc+2qA6OVr8pmO/MBbPEWqWQ/oqGEs=
|
github.com/boombuler/barcode v1.1.0 h1:ChaYjBR63fr4LFyGn8E8nt7dBSt3MiU3zMOZqFvVkHo=
|
||||||
github.com/chromedp/chromedp v0.9.1 h1:CC7cC5p1BeLiiS2gfNNPwp3OaUxtRMBjfiw3E3k6dFA=
|
github.com/boombuler/barcode v1.1.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
|
||||||
github.com/chromedp/chromedp v0.9.1/go.mod h1:DUgZWRvYoEfgi66CgZ/9Yv+psgi+Sksy5DTScENWjaQ=
|
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||||
github.com/chromedp/sysutil v1.0.0 h1:+ZxhTpfpZlmchB58ih/LBHX52ky7w2VhQVKQMucy3Ic=
|
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||||
github.com/chromedp/sysutil v1.0.0/go.mod h1:kgWmDdq8fTzXYcKIBqIYvRRTnYb9aNS9moAV0xufSww=
|
github.com/chromedp/cdproto v0.0.0-20250803210736-d308e07a266d h1:ZtA1sedVbEW7EW80Iz2GR3Ye6PwbJAJXjv7D74xG6HU=
|
||||||
|
github.com/chromedp/cdproto v0.0.0-20250803210736-d308e07a266d/go.mod h1:NItd7aLkcfOA/dcMXvl8p1u+lQqioRMq/SqDp71Pb/k=
|
||||||
|
github.com/chromedp/chromedp v0.14.0 h1:/xE5m6wEBwivhalHwlCOyYfBcAJNwg4nLw96QiCfYr0=
|
||||||
|
github.com/chromedp/chromedp v0.14.0/go.mod h1:rHzAv60xDE7VNy/MYtTUrYreSc0ujt2O1/C3bzctYBo=
|
||||||
|
github.com/chromedp/sysutil v1.1.0 h1:PUFNv5EcprjqXZD9nJb9b/c9ibAbxiYo4exNWZyipwM=
|
||||||
|
github.com/chromedp/sysutil v1.1.0/go.mod h1:WiThHUdltqCNKGc4gaU50XgYjwjYIhKWoHGPTUfWTJ8=
|
||||||
|
github.com/clipperhouse/stringish v0.1.1 h1:+NSqMOr3GR6k1FdRhhnXrLfztGzuG+VuFDfatpWHKCs=
|
||||||
|
github.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEXNWYXQgCt4hdOzA=
|
||||||
|
github.com/clipperhouse/uax29/v2 v2.4.0 h1:RXqE/l5EiAbA4u97giimKNlmpvkmz+GrBVTelsoXy9g=
|
||||||
|
github.com/clipperhouse/uax29/v2 v2.4.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g=
|
||||||
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
github.com/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.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo=
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
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.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM=
|
||||||
github.com/gabriel-vasile/mimetype v1.4.4/go.mod h1:JwLei5XPtWdGiMFB5Pjle1oEeoSeEuJfJE+TtfvdB/s=
|
github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
|
||||||
|
github.com/gabriel-vasile/mimetype v1.4.12 h1:e9hWvmLYvtp846tLHam2o++qitpguFiYCKbn0w9jyqw=
|
||||||
|
github.com/gabriel-vasile/mimetype v1.4.12/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
|
||||||
github.com/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.8-0.20250403174932-29230038a667 h1:BP4M0CvQ4S3TGls2FvczZtj5Re/2ZzkV9VwqPHH/3Bo=
|
||||||
|
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
|
||||||
|
github.com/go-chi/chi/v5 v5.2.3 h1:WQIt9uxdsAbgIYgid+BpYc+liqQZGMHRaUwp0JUcvdE=
|
||||||
|
github.com/go-chi/chi/v5 v5.2.3/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=
|
||||||
|
github.com/go-json-experiment/json v0.0.0-20250725192818-e39067aee2d2 h1:iizUGZ9pEquQS5jTGkh4AqeeHCMbfbjeb0zMt0aEFzs=
|
||||||
|
github.com/go-json-experiment/json v0.0.0-20250725192818-e39067aee2d2/go.mod h1:TiCD2a1pcmjd7YnhGH0f/zKNcCD06B029pHhzV23c2M=
|
||||||
|
github.com/go-ldap/ldap/v3 v3.4.12 h1:1b81mv7MagXZ7+1r7cLTWmyuTqVqdwbtJSjC0DAp9s4=
|
||||||
|
github.com/go-ldap/ldap/v3 v3.4.12/go.mod h1:+SPAGcTtOfmGsCb3h1RFiq4xpp4N636G75OEace8lNo=
|
||||||
github.com/go-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.30.1 h1:f3zDSN/zOma+w6+1Wswgd9fLkdwy06ntQJp0BBvFG0w=
|
||||||
github.com/go-playground/validator/v10 v10.21.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
|
github.com/go-playground/validator/v10 v10.30.1/go.mod h1:oSuBIQzuJxL//3MelwSLD5hc2Tu889bF0Idm9Dg26cM=
|
||||||
|
github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo=
|
||||||
|
github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
|
||||||
|
github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
|
||||||
|
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
||||||
|
github.com/go-webauthn/webauthn v0.15.0 h1:LR1vPv62E0/6+sTenX35QrCmpMCzLeVAcnXeH4MrbJY=
|
||||||
|
github.com/go-webauthn/webauthn v0.15.0/go.mod h1:hcAOhVChPRG7oqG7Xj6XKN1mb+8eXTGP/B7zBLzkX5A=
|
||||||
|
github.com/go-webauthn/x v0.1.26 h1:eNzreFKnwNLDFoywGh9FA8YOMebBWTUNlNSdolQRebs=
|
||||||
|
github.com/go-webauthn/x v0.1.26/go.mod h1:jmf/phPV6oIsF6hmdVre+ovHkxjDOmNH0t6fekWUxvg=
|
||||||
github.com/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=
|
||||||
github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
|
github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
|
||||||
github.com/gobwas/ws v1.1.0 h1:7RFti/xnNkMJnrK7D1yQ/iCIB5OrrY/54/H930kIbHA=
|
github.com/gobwas/ws v1.4.0 h1:CTaoG1tojrh4ucGPcoJFiAQUAsEWekEWvLy7GsVNqGs=
|
||||||
github.com/gobwas/ws v1.1.0/go.mod h1:nzvNcVha5eUziGrbxFCo6qFIojQHjJV5cLYIbezhfL0=
|
github.com/gobwas/ws v1.4.0/go.mod h1:G3gNqMNtPppf5XUz7O4shetPpcZ1VJ7zt18dlUeakrc=
|
||||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
github.com/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/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
|
||||||
github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
|
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
|
||||||
github.com/golang/geo v0.0.0-20230421003525-6adc56603217 h1:HKlyj6in2JV6wVkmQ4XmG/EIm+SCYlPZ+V4GWit7Z+I=
|
github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs=
|
||||||
github.com/golang/geo v0.0.0-20230421003525-6adc56603217/go.mod h1:8wI0hitZ3a1IxZfeH3/5I97CI8i5cLGsYe7xNhQGs9U=
|
github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||||
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||||
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||||
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
|
github.com/google/go-tpm v0.9.6 h1:Ku42PT4LmjDu1H5C5ISWLlpI1mj+Zq7sPGKoRw2XROA=
|
||||||
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
github.com/google/go-tpm v0.9.6/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY=
|
||||||
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
|
||||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
|
||||||
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.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.4.0 h1:kpIYOp/oi6MG/p5PgxApU8srsSw9tuFbt46Lt7auzqQ=
|
||||||
github.com/gorilla/sessions v1.2.2/go.mod h1:ePLdVu+jbEgHH+KWw8I1z2wqd0BAdAQh/8LRvBeoNcQ=
|
github.com/gorilla/sessions v1.4.0/go.mod h1:FLWm50oby91+hl7p/wRxDth9bWSuk0qVL2emc7lT5ik=
|
||||||
github.com/hashicorp/go-immutable-radix v1.3.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
|
github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8=
|
||||||
github.com/hashicorp/go-immutable-radix v1.3.1 h1:DKHmCUm2hRBK510BaiZlwvpD40f8bJFeZnpfm2KLowc=
|
github.com/hashicorp/go-uuid v1.0.3/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/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
|
||||||
github.com/hashicorp/go-memdb v1.3.4 h1:XSL3NR682X/cVk2IeV0d70N4DZ9ljI885xAEU8IoK3c=
|
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
|
||||||
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.8.0 h1:TYPDoleBBme0xGSAX3/+NujXXtpZn9HBONkQC7IEZSo=
|
||||||
|
github.com/jackc/pgx/v5 v5.8.0/go.mod h1:QVeDInX2m9VyzvNeiCJVjCkNFqzsNb43204HshNSZKw=
|
||||||
|
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=
|
||||||
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||||
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
|
|
||||||
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.4 h1:g5mfsrJfJTKv+F5uNKCyrjLK7js+ZW6HTjg4FnDxxgk=
|
||||||
|
github.com/labstack/echo-contrib v0.17.4/go.mod h1:9O7ZPAHUeMGTOAfg80YqQduHzt0CzLak36PZRldYrZ0=
|
||||||
|
github.com/labstack/echo/v4 v4.15.0 h1:hoRTKWcnR5STXZFe9BmYun9AMTNeSbjHi2vtDuADJ24=
|
||||||
|
github.com/labstack/echo/v4 v4.15.0/go.mod h1:xmw1clThob0BSVRX1CRQkGQ/vjwcpOMjQZSZa9fKA/c=
|
||||||
github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0=
|
github.com/labstack/gommon v0.4.2 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/markbates/goth v1.82.0 h1:8j/c34AjBSTNzO7zTsOyP5IYCQCMBTRBHAbBt/PI0bQ=
|
||||||
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
github.com/markbates/goth v1.82.0/go.mod h1:/DRlcq0pyqkKToyZjsL2KgiA1zbF1HIjE7u2uC79rUk=
|
||||||
github.com/markbates/goth v1.80.0 h1:NnvatczZDzOs1hn9Ug+dVYf2Viwwkp/ZDX5K+GLjan8=
|
|
||||||
github.com/markbates/goth v1.80.0/go.mod h1:4/GYHo+W6NWisrMPZnq0Yr2Q70UntNLn7KXEFhrIdAY=
|
|
||||||
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.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw=
|
||||||
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
|
||||||
|
github.com/meilisearch/meilisearch-go v0.36.0 h1:N1etykTektXt5KPcSbhBO0d5Xx5NaKj4pJWEM7WA5dI=
|
||||||
|
github.com/meilisearch/meilisearch-go v0.36.0/go.mod h1:HBfHzKMxcSbTOvqdfuRA/yf6Vk9IivcwKocWRuW7W78=
|
||||||
github.com/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,103 +216,130 @@ 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/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
||||||
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||||
|
github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
|
||||||
|
github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
||||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
github.com/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.5.0 h1:NMMR+WrmaqXU4EzdGJEE1aUUI0AMRzsp96fFFWNPwxs=
|
||||||
|
github.com/pquerna/otp v1.5.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg=
|
||||||
|
github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o=
|
||||||
|
github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
|
||||||
|
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
|
||||||
|
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
|
||||||
|
github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4=
|
||||||
|
github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw=
|
||||||
|
github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws=
|
||||||
|
github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw=
|
||||||
github.com/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.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
|
||||||
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/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.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.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||||
github.com/urfave/cli/v2 v2.27.2 h1:6e0H+AkS+zDckwPCUrZkKX38mRaau4nL2uipkJpbkcI=
|
github.com/urfave/cli/v2 v2.27.7 h1:bH59vdhbjLv3LAvIu6gd0usJHgoTTPhCFib8qqOwXYU=
|
||||||
github.com/urfave/cli/v2 v2.27.2/go.mod h1:g0+79LmHHATl7DAcHO99smiR/T7uGLw84w8Y42x+4eM=
|
github.com/urfave/cli/v2 v2.27.7/go.mod h1:CyNAG/xg+iAOg0N4MPGZqVmv2rCoP267496AOXUZjA4=
|
||||||
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
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/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4=
|
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
|
||||||
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM=
|
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
|
||||||
github.com/yuin/goldmark v1.3.7/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
github.com/xrash/smetrics v0.0.0-20250705151800-55b8f293f342 h1:FnBeRrxr7OU4VvAzt5X7s6266i6cSVkkFPS0TuXWbIg=
|
||||||
|
github.com/xrash/smetrics v0.0.0-20250705151800-55b8f293f342/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM=
|
||||||
|
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
|
||||||
|
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
|
||||||
github.com/yuin/goldmark v1.4.15/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
github.com/yuin/goldmark v1.4.15/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||||
github.com/yuin/goldmark v1.7.1 h1:3bajkSilaCbjdKVsKdZjZCLBNPL9pYzrCakKaf4U49U=
|
github.com/yuin/goldmark v1.7.16 h1:n+CJdUxaFMiDUNnWC3dMWCIQJSkxH4uz3ZwQBkAlVNE=
|
||||||
github.com/yuin/goldmark v1.7.1/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
|
github.com/yuin/goldmark v1.7.16/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
|
||||||
github.com/yuin/goldmark-emoji v1.0.2 h1:c/RgTShNgHTtc6xdz2KKI74jJr6rWi7FPgnP9GAsO5s=
|
github.com/yuin/goldmark-emoji v1.0.6 h1:QWfF2FYaXwL74tfGOW5izeiZepUDroDJfWubQI9HTHs=
|
||||||
github.com/yuin/goldmark-emoji v1.0.2/go.mod h1:RhP/RWpexdp+KHs7ghKnifRoIs/Bq4nDS7tRbCkOwKY=
|
github.com/yuin/goldmark-emoji v1.0.6/go.mod h1:ukxJDKFpdFb5x0a5HqbdlcKtebh086iJpI31LTKmWuA=
|
||||||
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc h1:+IAOyRda+RLrxa1WC7umKOZRsGq4QrFFMYApOeHzQwQ=
|
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.6.0 h1:VvkYFWuOjD6cmSBVJpLAtzpVCGM1h0B7/DQ9IzERwzY=
|
||||||
go.abhg.dev/goldmark/mermaid v0.5.0/go.mod h1:OCyk2o85TX2drWHH+HRy6bih2yZlUwbbv/R1MMh1YLs=
|
go.abhg.dev/goldmark/mermaid v0.6.0/go.mod h1:uMc+PcnIH2NVL7zjH10Q1wr7hL3+4n4jUMifhyBYB9I=
|
||||||
go.etcd.io/bbolt v1.3.10 h1:+BqfJTcCzTItrop8mq/lbzL8wSGtj94UO/3U31shqG0=
|
go.etcd.io/bbolt v1.4.3 h1:dEadXpI6G79deX5prL3QRNP6JB8UxVkqo4UPnHaNXJo=
|
||||||
go.etcd.io/bbolt v1.3.10/go.mod h1:bK3UQLPJZly7IlNmV7uVHJDxfe5aK9Ll93e/74Y9oEQ=
|
go.etcd.io/bbolt v1.4.3/go.mod h1:tKQlpPaYCVFctUIgFKFnAlvbmB3tpy1vkTnDWohtc0E=
|
||||||
golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI=
|
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||||
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||||
golang.org/x/mod v0.16.0 h1:QX4fJ0Rr5cPQCF7O9lh9Se4pmwfwskqZfq5moyldzic=
|
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
|
||||||
golang.org/x/mod v0.16.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
|
||||||
golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=
|
go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0=
|
||||||
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8=
|
||||||
golang.org/x/oauth2 v0.20.0 h1:4mQdhULixXKP1rwYBW0vAijoXnkTG0BLCDRzfe1idMo=
|
golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8=
|
||||||
golang.org/x/oauth2 v0.20.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
|
golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A=
|
||||||
golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE=
|
golang.org/x/exp v0.0.0-20260112195511-716be5621a96 h1:Z/6YuSHTLOHfNFdb8zVZomZr7cqNgTJvA8+Qz75D8gU=
|
||||||
golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
golang.org/x/exp v0.0.0-20260112195511-716be5621a96/go.mod h1:nzimsREAkjBCIEFtHiYkrJyT+2uy9YZJB7H1k68CXZU=
|
||||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c=
|
||||||
|
golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU=
|
||||||
|
golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o=
|
||||||
|
golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8=
|
||||||
|
golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw=
|
||||||
|
golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
|
||||||
|
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
|
||||||
|
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.6.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.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
|
||||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||||
golang.org/x/term v0.20.0 h1:VnkxpohqXaOBYJtBmEppKUG6mXpi+4O6purfc2+sMhw=
|
golang.org/x/term v0.39.0 h1:RclSuaJf32jOqZz74CkPA9qFuVTX7vhLlpfj/IGWlqY=
|
||||||
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
|
golang.org/x/term v0.39.0/go.mod h1:yxzUCTP/U+FzoxfdKmLaA0RV1WgE0VY7hXBwKtY/4ww=
|
||||||
golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk=
|
golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE=
|
||||||
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8=
|
||||||
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
|
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
|
||||||
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
|
||||||
golang.org/x/tools v0.19.0 h1:tfGCXNR1OsFG+sVdLAitlpjAvD/I6dHDKnYrpEZUHkw=
|
golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc=
|
||||||
golang.org/x/tools v0.19.0/go.mod h1:qoJWxmGSIBmAeriMx19ogtrEPrGtDbPK634QFIcLAhc=
|
golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg=
|
||||||
google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg=
|
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||||
google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
|
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
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.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.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.6.0 h1:eNbLmNTpPpTOVZi8MMxCi2aaIm0ZpInbORNXDwyLGvg=
|
||||||
gorm.io/gorm v1.25.10/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
|
gorm.io/driver/mysql v1.6.0/go.mod h1:D/oCC2GWK3M/dqoLxnOlaNKmXz8WNTfcS9y5ovaSqKo=
|
||||||
modernc.org/cc/v4 v4.21.2 h1:dycHFB/jDc3IyacKipCNSDrjIC0Lm1hyoWOZTRR20Lk=
|
gorm.io/driver/postgres v1.6.0 h1:2dxzU8xJ+ivvqTRph34QX+WrRaJlmfyPqXmoGVjMBa4=
|
||||||
modernc.org/cc/v4 v4.21.2/go.mod h1:HM7VJTZbUCR3rV8EYBi9wxnJ0ZBRiGE5OeGXNA0IsLQ=
|
gorm.io/driver/postgres v1.6.0/go.mod h1:vUw0mrGgrTK+uPHEhAdV4sfFELrByKVGnaVRkXDhtWo=
|
||||||
modernc.org/ccgo/v4 v4.17.8 h1:yyWBf2ipA0Y9GGz/MmCmi3EFpKgeS7ICrAFes+suEbs=
|
gorm.io/gorm v1.31.1 h1:7CA8FTFz/gRfgqgpeKIBcervUn3xSyPUmr6B2WXJ7kg=
|
||||||
modernc.org/ccgo/v4 v4.17.8/go.mod h1:buJnJ6Fn0tyAdP/dqePbrrvLyr6qslFfTbFrCuaYvtA=
|
gorm.io/gorm v1.31.1/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs=
|
||||||
modernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE=
|
modernc.org/cc/v4 v4.27.1 h1:9W30zRlYrefrDV2JE2O8VDtJ1yPGownxciz5rrbQZis=
|
||||||
modernc.org/fileutil v1.3.0/go.mod h1:XatxS8fZi3pS8/hKG2GH/ArUogfxjpEKs3Ku3aK4JyQ=
|
modernc.org/cc/v4 v4.27.1/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
|
||||||
modernc.org/gc/v2 v2.4.1 h1:9cNzOqPyMJBvrUipmynX0ZohMhcxPtMccYgGOJdOiBw=
|
modernc.org/ccgo/v4 v4.30.1 h1:4r4U1J6Fhj98NKfSjnPUN7Ze2c6MnAdL0hWw6+LrJpc=
|
||||||
modernc.org/gc/v2 v2.4.1/go.mod h1:wzN5dK1AzVGoH6XOzc3YZ+ey/jPgYHLuVckd62P0GYU=
|
modernc.org/ccgo/v4 v4.30.1/go.mod h1:bIOeI1JL54Utlxn+LwrFyjCx2n2RDiYEaJVSrgdrRfM=
|
||||||
modernc.org/libc v1.51.0 h1:kjSHjz1guHbI5iRdi6nEr/wIKSN6X4vzLd6TJMN+lHA=
|
modernc.org/fileutil v1.3.40 h1:ZGMswMNc9JOCrcrakF1HrvmergNLAmxOPjizirpfqBA=
|
||||||
modernc.org/libc v1.51.0/go.mod h1:15P6ublJ9FJR8YQCGy8DeQ2Uwur7iW9Hserr/T3OFZE=
|
modernc.org/fileutil v1.3.40/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc=
|
||||||
modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4=
|
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
|
||||||
modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo=
|
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
|
||||||
modernc.org/memory v1.8.0 h1:IqGTL6eFMaDZZhEWwcREgeMXYwmW83LYW8cROZYkg+E=
|
modernc.org/gc/v3 v3.1.1 h1:k8T3gkXWY9sEiytKhcgyiZ2L0DTyCQ/nvX+LoCljoRE=
|
||||||
modernc.org/memory v1.8.0/go.mod h1:XPZ936zp5OMKGWPqbD3JShgd/ZoQ7899TUuQqxY+peU=
|
modernc.org/gc/v3 v3.1.1/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY=
|
||||||
modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4=
|
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
|
||||||
modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0=
|
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
|
||||||
modernc.org/sortutil v1.2.0 h1:jQiD3PfS2REGJNzNCMMaLSp/wdMNieTbKX920Cqdgqc=
|
modernc.org/libc v1.67.7 h1:H+gYQw2PyidyxwxQsGTwQw6+6H+xUk+plvOKW7+d3TI=
|
||||||
modernc.org/sortutil v1.2.0/go.mod h1:TKU2s7kJMf1AE84OoiGppNHJwvB753OYfNl2WRb++Ss=
|
modernc.org/libc v1.67.7/go.mod h1:UjCSJFl2sYbJbReVQeVpq/MgzlbmDM4cRHIYFelnaDk=
|
||||||
modernc.org/sqlite v1.30.0 h1:8YhPUs/HTnlEgErn/jSYQTwHN/ex8CjHHjg+K9iG7LM=
|
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
|
||||||
modernc.org/sqlite v1.30.0/go.mod h1:cgkTARJ9ugeXSNaLBPK3CqbOe7Ec7ZhWPoMFGldEYEw=
|
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
|
||||||
modernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA=
|
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
|
||||||
modernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0=
|
modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
|
||||||
|
modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
|
||||||
|
modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
|
||||||
|
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
|
||||||
|
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
|
||||||
|
modernc.org/sqlite v1.44.3 h1:+39JvV/HWMcYslAwRxHb8067w+2zowvFOUrOWIy9PjY=
|
||||||
|
modernc.org/sqlite v1.44.3/go.mod h1:CzbrU2lSB1DKUusvwGz7rqEKIq+NUd8GWuBBZDs9/nA=
|
||||||
|
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
|
||||||
|
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
|
||||||
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
|
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
23
helm/opengist/.helmignore
Normal 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/
|
||||||
29
helm/opengist/CHANGELOG.md
Normal file
29
helm/opengist/CHANGELOG.md
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
# Helm Chart Changelog
|
||||||
|
|
||||||
|
## 0.6.0 - 2026-02-03
|
||||||
|
|
||||||
|
- Bump Opengist image to 1.12.1
|
||||||
|
|
||||||
|
## 0.5.0 - 2026-01-27
|
||||||
|
|
||||||
|
- Bump Opengist image to 1.12.0
|
||||||
|
- Add StatefulSet support
|
||||||
|
- Add Prometheus ServiceMonitor support if Opengist metrics are enabled
|
||||||
|
- New service for metrics endpoint, dissociated from the main service
|
||||||
|
- Use existing pvc claim of provided
|
||||||
|
|
||||||
|
## 0.4.0 - 2025-09-30
|
||||||
|
|
||||||
|
- Bump Opengist image to 1.11.1
|
||||||
|
|
||||||
|
## 0.3.0 - 2025-09-21
|
||||||
|
|
||||||
|
- Bump Opengist image to 1.11.0
|
||||||
|
|
||||||
|
## 0.2.0 - 2025-05-10
|
||||||
|
|
||||||
|
- Add `deployment.env[]` in values
|
||||||
|
|
||||||
|
## 0.1.0 - 2025-04-06
|
||||||
|
|
||||||
|
- Initial release, with Opengist image 1.10.0
|
||||||
9
helm/opengist/Chart.lock
Normal file
9
helm/opengist/Chart.lock
Normal 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
19
helm/opengist/Chart.yaml
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
apiVersion: v2
|
||||||
|
name: opengist
|
||||||
|
description: Opengist Helm chart for Kubernetes
|
||||||
|
type: application
|
||||||
|
version: 0.6.0
|
||||||
|
appVersion: 1.12.1
|
||||||
|
home: https://opengist.io
|
||||||
|
icon: https://raw.githubusercontent.com/thomiceli/opengist/master/public/img/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
|
||||||
451
helm/opengist/README.md
Normal file
451
helm/opengist/README.md
Normal file
@@ -0,0 +1,451 @@
|
|||||||
|
# Opengist Helm Chart
|
||||||
|
|
||||||
|
 
|
||||||
|
|
||||||
|
Opengist Helm chart for Kubernetes. Check [CHANGELOG.md](CHANGELOG.md) for release notes.
|
||||||
|
|
||||||
|
* [Install](#install)
|
||||||
|
* [Configuration](#configuration)
|
||||||
|
* [Metrics & Monitoring](#metrics--monitoring)
|
||||||
|
* [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>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Metrics & Monitoring
|
||||||
|
|
||||||
|
Opengist exposes Prometheus metrics on a separate port (default: `6158`). The metrics server runs independently from the main HTTP server for security.
|
||||||
|
|
||||||
|
### Enabling Metrics
|
||||||
|
|
||||||
|
To enable metrics, set `metrics.enabled: true` in your Opengist config:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
config:
|
||||||
|
metrics.enabled: true
|
||||||
|
```
|
||||||
|
|
||||||
|
This will:
|
||||||
|
1. Start a metrics server on port 6158 inside the container
|
||||||
|
2. Create a Kubernetes Service exposing the metrics ports
|
||||||
|
|
||||||
|
### Available Metrics
|
||||||
|
|
||||||
|
| Metric Name | Type | Description |
|
||||||
|
|-------------|------|-------------|
|
||||||
|
| `opengist_users_total` | Gauge | Total number of registered users |
|
||||||
|
| `opengist_gists_total` | Gauge | Total number of gists |
|
||||||
|
| `opengist_ssh_keys_total` | Gauge | Total number of SSH keys |
|
||||||
|
| `opengist_request_duration_seconds_*` | Histogram | HTTP request duration metrics |
|
||||||
|
|
||||||
|
### ServiceMonitor for Prometheus Operator
|
||||||
|
|
||||||
|
If you're using [Prometheus Operator](https://github.com/prometheus-operator/prometheus-operator), you can enable automatic service discovery with a ServiceMonitor:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
config:
|
||||||
|
metrics.enabled: true
|
||||||
|
|
||||||
|
service:
|
||||||
|
metrics:
|
||||||
|
serviceMonitor:
|
||||||
|
enabled: true
|
||||||
|
labels:
|
||||||
|
release: prometheus # match your Prometheus serviceMonitorSelector
|
||||||
|
```
|
||||||
|
|
||||||
|
### Manual Prometheus Configuration
|
||||||
|
|
||||||
|
If you're not using Prometheus Operator, you can configure Prometheus to scrape the metrics endpoint directly:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
scrape_configs:
|
||||||
|
- job_name: 'opengist'
|
||||||
|
static_configs:
|
||||||
|
- targets: ['opengist-metrics:6158']
|
||||||
|
metrics_path: /metrics
|
||||||
|
```
|
||||||
|
|
||||||
|
Or use Kubernetes service discovery:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
scrape_configs:
|
||||||
|
- job_name: 'opengist'
|
||||||
|
kubernetes_sd_configs:
|
||||||
|
- role: service
|
||||||
|
relabel_configs:
|
||||||
|
- source_labels: [__meta_kubernetes_service_label_app_kubernetes_io_component]
|
||||||
|
regex: metrics
|
||||||
|
action: keep
|
||||||
|
- source_labels: [__meta_kubernetes_service_label_app_kubernetes_io_name]
|
||||||
|
regex: opengist
|
||||||
|
action: keep
|
||||||
|
```
|
||||||
|
|
||||||
|
## 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`.
|
||||||
|
|
||||||
|
#### Passing Meilisearch configuration via nested Helm values
|
||||||
|
|
||||||
|
When using the Helm CLI with `--set`, avoid mixing a scalar `config.index` value with nested `config.index.meili.*` keys. Instead use a nested map and a `type` field which the chart flattens automatically. Example:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
helm template opengist ./helm/opengist \
|
||||||
|
--set statefulSet.enabled=true \
|
||||||
|
--set replicaCount=2 \
|
||||||
|
--set persistence.enabled=true \
|
||||||
|
--set persistence.existingClaim=opengist-shared-rwx \
|
||||||
|
--set postgresql.enabled=false \
|
||||||
|
--set config.db-uri="postgres://user:pass@db-host:5432/opengist" \
|
||||||
|
--set meilisearch.enabled=true \
|
||||||
|
--set config.index.type=meilisearch \
|
||||||
|
--set config.index.meili.host="http://opengist-meilisearch:7700" \
|
||||||
|
--set config.index.meili.api-key="MASTER_KEY"
|
||||||
|
```
|
||||||
|
|
||||||
|
Rendered `config.yml` fragment:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
index: meilisearch
|
||||||
|
index.meili.host: http://opengist-meilisearch:7700
|
||||||
|
index.meili.api-key: MASTER_KEY
|
||||||
|
```
|
||||||
|
|
||||||
|
How it works:
|
||||||
|
|
||||||
|
* You provide a map under `config.index` with keys `type` and `meili`.
|
||||||
|
* The template detects `config.index.type` and rewrites `index: <type>`.
|
||||||
|
* Nested `config.index.meili.host` / `api-key` are lifted to flat keys `index.meili.host` and `index.meili.api-key` required by Opengist.
|
||||||
|
|
||||||
|
If you set `--set config.index=meilisearch` directly and also try to set `--set config.index.meili.host=...`, Helm will first create the nested structure then overwrite it with the scalar, losing the host. Always prefer the `config.index.type` pattern for CLI usage.
|
||||||
|
|
||||||
|
### 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.
|
||||||
|
|
||||||
|
### Database Configuration
|
||||||
|
|
||||||
|
You can supply an externally managed database connection explicitly via `config.db-uri` (PostgreSQL/MySQL) or enable the bundled PostgreSQL subchart.
|
||||||
|
|
||||||
|
Behavior:
|
||||||
|
|
||||||
|
* If `postgresql.enabled: true` and `config.db-uri` is omitted, the chart auto-generates:
|
||||||
|
`postgres://<username>:<password>@<release-name>-postgresql:<port>/<database>` using values under `postgresql.global.postgresql.auth.*`.
|
||||||
|
* If any of username/password/database are missing, templating fails fast with an error message.
|
||||||
|
* If you prefer an external database or a different Postgres distribution, set `postgresql.enabled: false` and provide `config.db-uri` yourself.
|
||||||
|
|
||||||
|
**Licensing note**: Bitnami's PostgreSQL distribution may have licensing constraints. For strictly open alternatives use an external managed PostgreSQL/MySQL service and disable the subchart.
|
||||||
|
|
||||||
|
### Multi-Replica Requirements
|
||||||
|
|
||||||
|
Running more than one Opengist replica (Deployment or StatefulSet) requires:
|
||||||
|
|
||||||
|
1. Non-SQLite database (`config.db-uri` must start with `postgres://` or `mysql://`).
|
||||||
|
2. Shared RWX storage if using StatefulSet with `replicaCount > 1` (provide `persistence.existingClaim`). The chart now fails fast if you attempt `replicaCount > 1` without an explicit shared claim to prevent silent data divergence across per‑pod PVCs.
|
||||||
|
|
||||||
|
The chart will fail fast during templating if these conditions are not met when scaling above 1 replica.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
|
||||||
|
* External PostgreSQL:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
postgresql:
|
||||||
|
enabled: false
|
||||||
|
config:
|
||||||
|
db-uri: postgres://user:pass@db-host:5432/opengist
|
||||||
|
index: meilisearch
|
||||||
|
statefulSet:
|
||||||
|
enabled: true
|
||||||
|
replicaCount: 2
|
||||||
|
persistence:
|
||||||
|
existingClaim: opengist-shared-rwx
|
||||||
|
```
|
||||||
|
|
||||||
|
Bundled PostgreSQL (auto db-uri):
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
postgresql:
|
||||||
|
enabled: true
|
||||||
|
config:
|
||||||
|
index: meilisearch
|
||||||
|
statefulSet:
|
||||||
|
enabled: true
|
||||||
|
replicaCount: 2
|
||||||
|
persistence:
|
||||||
|
existingClaim: opengist-shared-rwx
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Recovering from an initial misconfiguration
|
||||||
|
|
||||||
|
If you previously scaled a StatefulSet above 1 replica **without** an `existingClaim`, each pod received its own PVC and only one held the authoritative `/opengist` data. To consolidate:
|
||||||
|
|
||||||
|
1. Scale down to 1 replica (keep the pod with the desired data):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
kubectl scale sts/opengist --replicas=1
|
||||||
|
```
|
||||||
|
|
||||||
|
1. (Optional) Inspect other PVCs and manually copy any missing files by temporarily attaching them to a debug pod.
|
||||||
|
1. Create or provision a ReadWriteMany (NFS / CephFS / Longhorn RWX / etc.) PersistentVolumeClaim named (for example) `opengist-shared-rwx`.
|
||||||
|
1. Update values with `persistence.existingClaim: opengist-shared-rwx` and re‑deploy.
|
||||||
|
1. Scale back up:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
kubectl scale sts/opengist --replicas=2
|
||||||
|
```
|
||||||
|
|
||||||
|
Going forward, all replicas mount the same shared volume and data remains consistent.
|
||||||
|
|
||||||
|
### Quick Start Examples
|
||||||
|
|
||||||
|
Common deployment scenarios with copy-paste configurations:
|
||||||
|
|
||||||
|
#### Scenario 1: Single replica with SQLite (default)
|
||||||
|
|
||||||
|
Minimal local development setup with ephemeral or persistent storage:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# Ephemeral (emptyDir)
|
||||||
|
statefulSet:
|
||||||
|
enabled: true
|
||||||
|
replicaCount: 1
|
||||||
|
persistence:
|
||||||
|
enabled: false
|
||||||
|
|
||||||
|
# OR with persistent RWO storage
|
||||||
|
statefulSet:
|
||||||
|
enabled: true
|
||||||
|
replicaCount: 1
|
||||||
|
persistence:
|
||||||
|
enabled: true
|
||||||
|
mode: perReplica # default
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Scenario 2: Multi-replica with external PostgreSQL + existing RWX PVC
|
||||||
|
|
||||||
|
Production HA setup with your own database and storage:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
statefulSet:
|
||||||
|
enabled: true
|
||||||
|
replicaCount: 2
|
||||||
|
postgresql:
|
||||||
|
enabled: false
|
||||||
|
config:
|
||||||
|
db-uri: "postgres://user:pass@db-host:5432/opengist"
|
||||||
|
index: meilisearch # required for multi-replica
|
||||||
|
persistence:
|
||||||
|
enabled: true
|
||||||
|
mode: shared
|
||||||
|
existingClaim: "opengist-shared-rwx" # pre-created RWX PVC
|
||||||
|
meilisearch:
|
||||||
|
enabled: true
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Scenario 3: Multi-replica with bundled PostgreSQL + auto-created RWX PVC
|
||||||
|
|
||||||
|
Chart manages both database and storage:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
statefulSet:
|
||||||
|
enabled: true
|
||||||
|
replicaCount: 2
|
||||||
|
postgresql:
|
||||||
|
enabled: true
|
||||||
|
global:
|
||||||
|
postgresql:
|
||||||
|
auth:
|
||||||
|
username: opengist
|
||||||
|
password: changeme
|
||||||
|
database: opengist
|
||||||
|
config:
|
||||||
|
index: meilisearch
|
||||||
|
persistence:
|
||||||
|
enabled: true
|
||||||
|
mode: shared
|
||||||
|
existingClaim: "" # empty to trigger auto-creation
|
||||||
|
create:
|
||||||
|
enabled: true
|
||||||
|
accessModes: [ReadWriteMany]
|
||||||
|
storageClass: "nfs-client" # your RWX-capable storage class
|
||||||
|
size: 20Gi
|
||||||
|
meilisearch:
|
||||||
|
enabled: true
|
||||||
|
```
|
||||||
|
|
||||||
|
### Persistence Modes
|
||||||
|
|
||||||
|
The chart supports two persistence strategies controlled by `persistence.mode`:
|
||||||
|
|
||||||
|
| Mode | Behavior | Scaling | Storage Objects | Recommended Use |
|
||||||
|
|-------------|----------|---------|-----------------|-----------------|
|
||||||
|
| `perReplica` (default) | One PVC per pod via StatefulSet `volumeClaimTemplates` (RWO) when no `existingClaim` | Safe only at `replicaCount=1` unless you supply `existingClaim` | One PVC per replica | Local dev, quick single-node trials |
|
||||||
|
| `shared` | Single RWX PVC (existing or auto-created) mounted by all pods | Horizontally scalable | One shared PVC | Production / HA |
|
||||||
|
|
||||||
|
Configuration examples:
|
||||||
|
|
||||||
|
Per-replica (single node):
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
statefulSet:
|
||||||
|
enabled: true
|
||||||
|
persistence:
|
||||||
|
mode: perReplica
|
||||||
|
enabled: true
|
||||||
|
accessModes:
|
||||||
|
- ReadWriteOnce
|
||||||
|
```
|
||||||
|
|
||||||
|
Shared (scale ready) with an existing RWX claim:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
statefulSet:
|
||||||
|
enabled: true
|
||||||
|
replicaCount: 2
|
||||||
|
persistence:
|
||||||
|
mode: shared
|
||||||
|
existingClaim: opengist-shared-rwx
|
||||||
|
```
|
||||||
|
|
||||||
|
Shared with chart-created RWX PVC:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
statefulSet:
|
||||||
|
enabled: true
|
||||||
|
replicaCount: 2
|
||||||
|
persistence:
|
||||||
|
mode: shared
|
||||||
|
existingClaim: "" # leave empty
|
||||||
|
create:
|
||||||
|
enabled: true
|
||||||
|
accessModes: [ReadWriteMany]
|
||||||
|
size: 10Gi
|
||||||
|
```
|
||||||
|
|
||||||
|
When `mode=shared` and `existingClaim` is empty, the chart creates a single PVC named `<release>-shared` (suffix configurable via `persistence.create.nameSuffix`).
|
||||||
|
|
||||||
|
Fail-fast conditions:
|
||||||
|
|
||||||
|
* `replicaCount>1` & missing external DB (still enforced).
|
||||||
|
* `replicaCount>1` & persistence disabled.
|
||||||
|
* `replicaCount>1` & neither `existingClaim` nor `mode=shared`.
|
||||||
|
* `mode=shared` & create.enabled=true but `accessModes` lacks `ReadWriteMany`.
|
||||||
|
|
||||||
|
Migration (perReplica → shared): scale down to 1, create RWX claim (or rely on create.enabled), copy data, switch mode to shared, scale up.
|
||||||
|
|
||||||
|
### Troubleshooting
|
||||||
|
|
||||||
|
#### Common Errors and Solutions
|
||||||
|
|
||||||
|
##### Error: "replicaCount=2 requires PostgreSQL/MySQL config.db-uri; scheme 'sqlite' unsupported"
|
||||||
|
|
||||||
|
* **Cause**: Multi-replica with SQLite database
|
||||||
|
* **Solution**: Either scale down to `replicaCount: 1` or configure external database:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
config:
|
||||||
|
db-uri: "postgres://user:pass@host:5432/opengist"
|
||||||
|
```
|
||||||
|
|
||||||
|
##### Error: "replicaCount=2 requires either persistence.existingClaim OR persistence.mode=shared"
|
||||||
|
|
||||||
|
* **Cause**: Multi-replica without shared storage
|
||||||
|
* **Solution**: Choose one approach:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# Option A: Use existing PVC
|
||||||
|
persistence:
|
||||||
|
existingClaim: "my-rwx-pvc"
|
||||||
|
|
||||||
|
# Option B: Let chart create PVC
|
||||||
|
persistence:
|
||||||
|
mode: shared
|
||||||
|
create:
|
||||||
|
enabled: true
|
||||||
|
accessModes: [ReadWriteMany]
|
||||||
|
```
|
||||||
|
|
||||||
|
##### Error: "persistence.mode=shared create.accessModes must include ReadWriteMany for multi-replica"
|
||||||
|
|
||||||
|
* **Cause**: Chart-created PVC lacks RWX access mode
|
||||||
|
* **Solution**: Ensure RWX is specified:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
persistence:
|
||||||
|
create:
|
||||||
|
accessModes:
|
||||||
|
- ReadWriteMany
|
||||||
|
```
|
||||||
|
|
||||||
|
##### Pods mount different data (data divergence)
|
||||||
|
|
||||||
|
* **Cause**: Previously scaled with `perReplica` mode and `replicaCount > 1`
|
||||||
|
* **Solution**: Follow recovery steps in "Recovering from an initial misconfiguration" section above
|
||||||
|
|
||||||
|
##### PVC creation fails: "no storage class available with ReadWriteMany"
|
||||||
|
|
||||||
|
* **Cause**: Cluster lacks RWX-capable storage provisioner
|
||||||
|
* **Solution**: Install a storage provider (NFS, CephFS, Longhorn) or use external managed storage and provide `existingClaim`
|
||||||
22
helm/opengist/templates/NOTES.txt
Normal file
22
helm/opengist/templates/NOTES.txt
Normal 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 }}
|
||||||
85
helm/opengist/templates/_helpers.tpl
Normal file
85
helm/opengist/templates/_helpers.tpl
Normal 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 -}}
|
||||||
134
helm/opengist/templates/deployment.yaml
Normal file
134
helm/opengist/templates/deployment.yaml
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
{{- if not .Values.statefulSet.enabled }}
|
||||||
|
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 index .Values.config "metrics.enabled" }}
|
||||||
|
- name: metrics
|
||||||
|
containerPort: {{ .Values.service.metrics.port }}
|
||||||
|
protocol: TCP
|
||||||
|
{{- end }}
|
||||||
|
{{- 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:
|
||||||
|
{{- if .Values.persistence.existingClaim }}
|
||||||
|
claimName: {{ .Values.persistence.existingClaim }}
|
||||||
|
{{- else }}
|
||||||
|
claimName: {{ include "opengist.fullname" . }}-data
|
||||||
|
{{- end }}
|
||||||
|
{{- 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 }}
|
||||||
|
|
||||||
|
{{- end }}
|
||||||
37
helm/opengist/templates/hpa.yaml
Normal file
37
helm/opengist/templates/hpa.yaml
Normal 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 }}
|
||||||
47
helm/opengist/templates/ingress.yaml
Normal file
47
helm/opengist/templates/ingress.yaml
Normal 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 }}
|
||||||
14
helm/opengist/templates/pdb.yaml
Normal file
14
helm/opengist/templates/pdb.yaml
Normal 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 -}}
|
||||||
48
helm/opengist/templates/pvc-shared.yaml
Normal file
48
helm/opengist/templates/pvc-shared.yaml
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
{{- /*
|
||||||
|
This template creates a standalone PersistentVolumeClaim for shared persistence mode.
|
||||||
|
|
||||||
|
Rendering conditions:
|
||||||
|
- statefulSet.enabled=true
|
||||||
|
- persistence.enabled=true
|
||||||
|
- persistence.mode=shared
|
||||||
|
- persistence.existingClaim is empty/unset
|
||||||
|
- persistence.create.enabled=true
|
||||||
|
|
||||||
|
When rendered, this PVC is mounted by ALL replicas in the StatefulSet (typically with ReadWriteMany
|
||||||
|
access mode for multi-replica deployments). This avoids per-replica volumeClaimTemplates and enables
|
||||||
|
horizontal scaling with a single shared storage backend.
|
||||||
|
|
||||||
|
If persistence.existingClaim is set, this template does NOT render; the StatefulSet instead references
|
||||||
|
the existing claim name directly.
|
||||||
|
*/}}
|
||||||
|
{{- if and .Values.statefulSet.enabled .Values.persistence.enabled (eq (default "perReplica" .Values.persistence.mode) "shared") (ne (default "" .Values.persistence.existingClaim) "") | not }}{{- end }}
|
||||||
|
{{- if and .Values.statefulSet.enabled .Values.persistence.enabled (eq (default "perReplica" .Values.persistence.mode) "shared") (eq (default "" .Values.persistence.existingClaim) "") .Values.persistence.create.enabled }}
|
||||||
|
apiVersion: v1
|
||||||
|
kind: PersistentVolumeClaim
|
||||||
|
metadata:
|
||||||
|
name: {{ include "opengist.fullname" . }}-{{ default "shared" .Values.persistence.create.nameSuffix }}
|
||||||
|
labels:
|
||||||
|
{{- include "opengist.labels" . | nindent 4 }}
|
||||||
|
{{- with .Values.persistence.create.labels }}
|
||||||
|
{{- toYaml . | nindent 4 }}
|
||||||
|
{{- end }}
|
||||||
|
{{- with .Values.persistence.create.annotations }}
|
||||||
|
annotations:
|
||||||
|
{{- toYaml . | nindent 4 }}
|
||||||
|
{{- end }}
|
||||||
|
spec:
|
||||||
|
accessModes:
|
||||||
|
{{- if .Values.persistence.create.accessModes }}
|
||||||
|
{{- toYaml .Values.persistence.create.accessModes | nindent 4 }}
|
||||||
|
{{- else }}
|
||||||
|
- ReadWriteMany
|
||||||
|
{{- end }}
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
storage: {{ default .Values.persistence.size .Values.persistence.create.size }}
|
||||||
|
volumeMode: Filesystem
|
||||||
|
{{- $sc := default .Values.persistence.storageClass .Values.persistence.create.storageClass }}
|
||||||
|
{{- if $sc }}
|
||||||
|
storageClassName: {{ $sc | quote }}
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
28
helm/opengist/templates/pvc.yaml
Normal file
28
helm/opengist/templates/pvc.yaml
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
{{- if and .Values.persistence.enabled (not .Values.statefulSet.enabled) (not .Values.persistence.existingClaim) }}
|
||||||
|
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 }}
|
||||||
62
helm/opengist/templates/secret.yaml
Normal file
62
helm/opengist/templates/secret.yaml
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
{{- if (not .Values.configExistingSecret) }}
|
||||||
|
{{- $cfg := deepCopy .Values.config }}
|
||||||
|
{{- /* Backward compatibility: map db-uri (deprecated) to db-uri key still expected by app, also accept dbUri coming from user */}}
|
||||||
|
{{- if and (hasKey $cfg "dbUri") (not (hasKey $cfg "db-uri")) }}
|
||||||
|
{{- $_ := set $cfg "db-uri" (index $cfg "dbUri") }}
|
||||||
|
{{- end }}
|
||||||
|
{{- $dburi := default "" (index $cfg "db-uri") }}
|
||||||
|
{{- /* Flatten possible nested index.meili.* structure if user passed --set config.index.meili.host=... */}}
|
||||||
|
{{- if and (hasKey $cfg "index") (kindIs "map" (index $cfg "index")) }}
|
||||||
|
{{- $indexMap := (index $cfg "index") }}
|
||||||
|
{{- if hasKey $indexMap "type" }}
|
||||||
|
{{- $_ := set $cfg "index" (index $indexMap "type") }}
|
||||||
|
{{- end }}
|
||||||
|
{{- if hasKey $indexMap "meili" }}
|
||||||
|
{{- $meili := (index $indexMap "meili") }}
|
||||||
|
{{- if hasKey $meili "host" }}
|
||||||
|
{{- $_ := set $cfg "index.meili.host" (index $meili "host") }}
|
||||||
|
{{- end }}
|
||||||
|
{{- if hasKey $meili "api-key" }}
|
||||||
|
{{- $_ := set $cfg "index.meili.api-key" (index $meili "api-key") }}
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
|
{{- if and .Values.postgresql.enabled (eq $dburi "") }}
|
||||||
|
{{- $user := default "" .Values.postgresql.global.postgresql.auth.username }}
|
||||||
|
{{- $pass := default "" .Values.postgresql.global.postgresql.auth.password }}
|
||||||
|
{{- $db := default "" .Values.postgresql.global.postgresql.auth.database }}
|
||||||
|
{{- $port := default 5432 .Values.postgresql.global.postgresql.service.ports.postgresql }}
|
||||||
|
{{- if or (eq $user "") (eq $pass "") (eq $db "") }}
|
||||||
|
{{- fail "postgresql.enabled=true requires username/password/database (postgresql.global.postgresql.auth.*) or set config.db-uri manually" }}
|
||||||
|
{{- end }}
|
||||||
|
{{- $autoHost := printf "%s-postgresql" (include "opengist.fullname" .) }}
|
||||||
|
{{- $autoUri := printf "postgres://%s:%s@%s:%d/%s" $user $pass $autoHost $port $db }}
|
||||||
|
{{- $_ := set $cfg "db-uri" $autoUri }}
|
||||||
|
{{- end }}
|
||||||
|
{{- $replicas := int .Values.replicaCount }}
|
||||||
|
{{- $index := default "" (index $cfg "index") }}
|
||||||
|
{{- /* Auto-set Meilisearch host if subchart enabled and host missing */}}
|
||||||
|
{{- $meiliHost := default "" (index $cfg "index.meili.host") }}
|
||||||
|
{{- if and .Values.meilisearch.enabled (eq $meiliHost "") }}
|
||||||
|
{{- $autoMeiliHost := printf "http://%s-meilisearch:7700" (include "opengist.fullname" .) }}
|
||||||
|
{{- $_ := set $cfg "index.meili.host" $autoMeiliHost }}
|
||||||
|
{{- if or (eq $index "") (ne $index "meilisearch") }}
|
||||||
|
{{- $_ := set $cfg "index" "meilisearch" }}
|
||||||
|
{{- $index = "meilisearch" }}
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
|
{{- if and (gt $replicas 1) (or (eq $index "") (eq $index "bleve")) }}
|
||||||
|
{{- fail "replicaCount>1 requires index set to 'meilisearch' (bleve not supported with multiple replicas)" }}
|
||||||
|
{{- end }}
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Secret
|
||||||
|
metadata:
|
||||||
|
name: {{ include "opengist.fullname" . }}
|
||||||
|
namespace: {{ .Release.Namespace }}
|
||||||
|
labels:
|
||||||
|
{{ include "opengist.labels" . | indent 4 }}
|
||||||
|
type: Opaque
|
||||||
|
stringData:
|
||||||
|
config.yml: |-
|
||||||
|
{{- $cfg | toYaml | nindent 4 }}
|
||||||
|
{{- end }}
|
||||||
13
helm/opengist/templates/serviceaccount.yaml
Normal file
13
helm/opengist/templates/serviceaccount.yaml
Normal 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 }}
|
||||||
41
helm/opengist/templates/servicemonitor.yaml
Normal file
41
helm/opengist/templates/servicemonitor.yaml
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
{{- if and (index .Values.config "metrics.enabled") .Values.service.metrics.serviceMonitor.enabled }}
|
||||||
|
apiVersion: monitoring.coreos.com/v1
|
||||||
|
kind: ServiceMonitor
|
||||||
|
metadata:
|
||||||
|
name: {{ include "opengist.fullname" . }}
|
||||||
|
namespace: {{ .Values.namespace | default .Release.Namespace }}
|
||||||
|
labels:
|
||||||
|
{{- include "opengist.labels" . | nindent 4 }}
|
||||||
|
{{- with .Values.service.metrics.serviceMonitor.labels }}
|
||||||
|
{{- toYaml . | nindent 4 }}
|
||||||
|
{{- end }}
|
||||||
|
{{- with .Values.service.metrics.serviceMonitor.annotations }}
|
||||||
|
annotations:
|
||||||
|
{{- toYaml . | nindent 4 }}
|
||||||
|
{{- end }}
|
||||||
|
spec:
|
||||||
|
endpoints:
|
||||||
|
- port: metrics
|
||||||
|
{{- with .Values.service.metrics.serviceMonitor.interval }}
|
||||||
|
interval: {{ . }}
|
||||||
|
{{- end }}
|
||||||
|
{{- with .Values.service.metrics.serviceMonitor.scrapeTimeout }}
|
||||||
|
scrapeTimeout: {{ . }}
|
||||||
|
{{- end }}
|
||||||
|
path: /metrics
|
||||||
|
{{- with .Values.service.metrics.serviceMonitor.relabelings }}
|
||||||
|
relabelings:
|
||||||
|
{{- toYaml . | nindent 8 }}
|
||||||
|
{{- end }}
|
||||||
|
{{- with .Values.service.metrics.serviceMonitor.metricRelabelings }}
|
||||||
|
metricRelabelings:
|
||||||
|
{{- toYaml . | nindent 8 }}
|
||||||
|
{{- end }}
|
||||||
|
namespaceSelector:
|
||||||
|
matchNames:
|
||||||
|
- {{ .Values.namespace | default .Release.Namespace }}
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
{{- include "opengist.selectorLabels" . | nindent 6 }}
|
||||||
|
app.kubernetes.io/component: metrics
|
||||||
|
{{- end }}
|
||||||
267
helm/opengist/templates/statefulset.yaml
Normal file
267
helm/opengist/templates/statefulset.yaml
Normal file
@@ -0,0 +1,267 @@
|
|||||||
|
{{- if .Values.statefulSet.enabled }}
|
||||||
|
{{- /*
|
||||||
|
========================================
|
||||||
|
VALIDATION BLOCK: Multi-replica requirements
|
||||||
|
========================================
|
||||||
|
Enforces constraints for scaling beyond 1 replica:
|
||||||
|
1. Database: Must use PostgreSQL/MySQL (not SQLite)
|
||||||
|
2. Persistence: Must be enabled
|
||||||
|
3. Storage sharing: Must use either existingClaim or mode=shared with create.enabled
|
||||||
|
4. Access mode: For mode=shared + create, must specify ReadWriteMany
|
||||||
|
*/}}
|
||||||
|
{{- $replicas := int .Values.replicaCount }}
|
||||||
|
{{- $dburi := "" }}
|
||||||
|
{{- if and .Values.config (hasKey .Values.config "dbUri") }}
|
||||||
|
{{- $dburi = (index .Values.config "dbUri") }}
|
||||||
|
{{- else if and .Values.config (hasKey .Values.config "db-uri") }}
|
||||||
|
{{- $dburi = (index .Values.config "db-uri") }}
|
||||||
|
{{- end }}
|
||||||
|
{{- $scheme := "" }}
|
||||||
|
{{- if ne $dburi "" }}
|
||||||
|
{{- $parts := splitList "://" $dburi }}
|
||||||
|
{{- if gt (len $parts) 0 }}
|
||||||
|
{{- $scheme = lower (index $parts 0) }}
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
|
{{- $multiAllowed := or (eq $scheme "postgres") (eq $scheme "postgresql") (eq $scheme "mysql") (eq $scheme "mariadb") }}
|
||||||
|
{{- $p := .Values.persistence }}
|
||||||
|
{{- $mode := default "perReplica" $p.mode }}
|
||||||
|
{{- $hasExisting := ne (default "" $p.existingClaim) "" }}
|
||||||
|
{{- $isShared := eq $mode "shared" }}
|
||||||
|
|
||||||
|
{{- /* Fail fast: Database validation */}}
|
||||||
|
{{- if and (gt $replicas 1) (not $multiAllowed) }}
|
||||||
|
{{- fail (printf "replicaCount=%d requires PostgreSQL/MySQL config.db-uri; scheme '%s' unsupported" $replicas $scheme) }}
|
||||||
|
{{- end }}
|
||||||
|
|
||||||
|
{{- /* Fail fast: Persistence must be enabled */}}
|
||||||
|
{{- if and (gt $replicas 1) (not $p.enabled) }}
|
||||||
|
{{- fail (printf "replicaCount=%d requires persistence.enabled=true" $replicas) }}
|
||||||
|
{{- end }}
|
||||||
|
|
||||||
|
{{- /* Fail fast: Prevent per-replica PVC divergence */}}
|
||||||
|
{{- if and (gt $replicas 1) (not (or $hasExisting $isShared)) }}
|
||||||
|
{{- fail (printf "replicaCount=%d requires either persistence.existingClaim (shared RWX PVC) OR persistence.mode=shared to create one; perReplica PVCs would diverge" $replicas) }}
|
||||||
|
{{- end }}
|
||||||
|
|
||||||
|
{{- /* Fail fast: Shared mode requires PVC source */}}
|
||||||
|
{{- if and (gt $replicas 1) $isShared (not $hasExisting) (hasKey $p "create") (not (get $p.create "enabled")) }}
|
||||||
|
{{- fail (printf "persistence.mode=shared but neither existingClaim nor create.enabled=true provided") }}
|
||||||
|
{{- end }}
|
||||||
|
|
||||||
|
{{- /* Fail fast: Auto-created shared PVC must be RWX */}}
|
||||||
|
{{- if and (gt $replicas 1) $isShared (not $hasExisting) $p.create.enabled }}
|
||||||
|
{{- $am := list }}
|
||||||
|
{{- if hasKey $p.create "accessModes" }}
|
||||||
|
{{- $am = $p.create.accessModes }}
|
||||||
|
{{- end }}
|
||||||
|
{{- $rwxOk := false }}
|
||||||
|
{{- range $am }}
|
||||||
|
{{- if or (eq . "ReadWriteMany") (eq . "RWX") }}
|
||||||
|
{{- $rwxOk = true }}
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
|
{{- if not $rwxOk }}
|
||||||
|
{{- fail "persistence.mode=shared create.accessModes must include ReadWriteMany for multi-replica" }}
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: StatefulSet
|
||||||
|
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:
|
||||||
|
replicas: {{ .Values.replicaCount }}
|
||||||
|
serviceName: {{ include "opengist.fullname" . }}-http
|
||||||
|
podManagementPolicy: {{ .Values.statefulSet.podManagementPolicy }}
|
||||||
|
updateStrategy:
|
||||||
|
{{- toYaml .Values.statefulSet.updateStrategy | nindent 2 }}
|
||||||
|
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.service.ssh.enabled }}
|
||||||
|
- name: ssh
|
||||||
|
containerPort: {{ .Values.service.ssh.port }}
|
||||||
|
protocol: TCP
|
||||||
|
{{- end }}
|
||||||
|
{{- if index .Values.config "metrics.enabled" }}
|
||||||
|
- name: metrics
|
||||||
|
containerPort: {{ .Values.service.metrics.port }}
|
||||||
|
protocol: TCP
|
||||||
|
{{- end }}
|
||||||
|
{{- 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: config-secret
|
||||||
|
secret:
|
||||||
|
secretName: {{ include "opengist.secretName" . }}
|
||||||
|
defaultMode: 511
|
||||||
|
- name: config-volume
|
||||||
|
emptyDir: {}
|
||||||
|
{{- /*
|
||||||
|
========================================
|
||||||
|
VOLUME MOUNTING DECISION TREE
|
||||||
|
========================================
|
||||||
|
Priority order:
|
||||||
|
1. existingClaim (user-provided PVC) → mount directly
|
||||||
|
2. mode=shared (chart-created PVC) → mount shared PVC
|
||||||
|
3. mode=perReplica → use volumeClaimTemplates (defined below)
|
||||||
|
4. persistence disabled → use emptyDir (ephemeral)
|
||||||
|
*/}}
|
||||||
|
{{- if .Values.persistence.enabled }}
|
||||||
|
{{- if ne (default "" .Values.persistence.existingClaim) "" }}
|
||||||
|
{{- /* User-provided existing claim: mount directly */}}
|
||||||
|
- name: opengist-data
|
||||||
|
persistentVolumeClaim:
|
||||||
|
claimName: {{ .Values.persistence.existingClaim }}
|
||||||
|
{{- else if eq (default "perReplica" .Values.persistence.mode) "shared" }}
|
||||||
|
{{- /* Chart creates shared PVC (via pvc-shared.yaml), reference by name */}}
|
||||||
|
- name: opengist-data
|
||||||
|
persistentVolumeClaim:
|
||||||
|
claimName: {{ include "opengist.fullname" . }}-{{ default "shared" .Values.persistence.create.nameSuffix }}
|
||||||
|
{{- else if not .Values.persistence.enabled }}
|
||||||
|
- name: opengist-data
|
||||||
|
emptyDir: {}
|
||||||
|
{{- end }}
|
||||||
|
{{- else }}
|
||||||
|
- name: opengist-data
|
||||||
|
emptyDir: {}
|
||||||
|
{{- end }}
|
||||||
|
{{- 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 }}
|
||||||
|
{{- /*
|
||||||
|
========================================
|
||||||
|
VOLUMECLAIMTEMPLATES DECISION TREE
|
||||||
|
========================================
|
||||||
|
volumeClaimTemplates are ONLY used for perReplica mode when:
|
||||||
|
- persistence.enabled=true
|
||||||
|
- persistence.existingClaim is empty
|
||||||
|
- persistence.mode=perReplica (default)
|
||||||
|
|
||||||
|
This creates one PVC per replica (RWO typically).
|
||||||
|
|
||||||
|
NOT used when:
|
||||||
|
- existingClaim is set (PVC already exists, referenced in volumes above)
|
||||||
|
- mode=shared (standalone PVC created via pvc-shared.yaml)
|
||||||
|
- persistence disabled (emptyDir used)
|
||||||
|
|
||||||
|
WARNING: perReplica + replicaCount>1 causes data divergence. Use shared mode for multi-replica.
|
||||||
|
*/}}
|
||||||
|
{{- if and .Values.persistence.enabled (ne (default "" .Values.persistence.existingClaim) "") }}
|
||||||
|
{{- /* existingClaim path: no volumeClaimTemplates, already mounted above */}}
|
||||||
|
{{- else if and .Values.persistence.enabled (eq (default "perReplica" .Values.persistence.mode) "shared") }}
|
||||||
|
{{- /* shared mode: no volumeClaimTemplates, standalone PVC rendered via pvc-shared.yaml */}}
|
||||||
|
{{- else if and .Values.persistence.enabled (eq (default "perReplica" .Values.persistence.mode) "perReplica") }}
|
||||||
|
volumeClaimTemplates:
|
||||||
|
- metadata:
|
||||||
|
name: opengist-data
|
||||||
|
labels:
|
||||||
|
{{- include "opengist.labels" . | nindent 10 }}
|
||||||
|
{{- with .Values.persistence.annotations }}
|
||||||
|
annotations:
|
||||||
|
{{- toYaml . | nindent 10 }}
|
||||||
|
{{- end }}
|
||||||
|
spec:
|
||||||
|
accessModes:
|
||||||
|
{{- .Values.persistence.accessModes | toYaml | nindent 10 }}
|
||||||
|
volumeMode: Filesystem
|
||||||
|
{{- if .Values.persistence.storageClass }}
|
||||||
|
storageClassName: {{ .Values.persistence.storageClass | quote }}
|
||||||
|
{{- end }}
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
storage: {{ .Values.persistence.size | default "10Gi" }}
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
47
helm/opengist/templates/svc-http.yaml
Normal file
47
helm/opengist/templates/svc-http.yaml
Normal 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 }}
|
||||||
32
helm/opengist/templates/svc-metrics.yaml
Normal file
32
helm/opengist/templates/svc-metrics.yaml
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
{{- if index .Values.config "metrics.enabled" }}
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: {{ include "opengist.fullname" . }}-metrics
|
||||||
|
namespace: {{ .Values.namespace | default .Release.Namespace }}
|
||||||
|
labels:
|
||||||
|
{{- include "opengist.labels" . | nindent 4 }}
|
||||||
|
app.kubernetes.io/component: metrics
|
||||||
|
{{- with .Values.service.metrics.labels }}
|
||||||
|
{{- toYaml . | nindent 4 }}
|
||||||
|
{{- end }}
|
||||||
|
{{- with .Values.service.metrics.annotations }}
|
||||||
|
annotations:
|
||||||
|
{{- toYaml . | nindent 4 }}
|
||||||
|
{{- end }}
|
||||||
|
spec:
|
||||||
|
type: {{ .Values.service.metrics.type }}
|
||||||
|
{{- if .Values.service.metrics.clusterIP }}
|
||||||
|
clusterIP: {{ .Values.service.metrics.clusterIP }}
|
||||||
|
{{- end }}
|
||||||
|
ports:
|
||||||
|
- port: {{ .Values.service.metrics.port }}
|
||||||
|
targetPort: metrics
|
||||||
|
protocol: TCP
|
||||||
|
name: metrics
|
||||||
|
{{- if and (eq .Values.service.metrics.type "NodePort") .Values.service.metrics.nodePort }}
|
||||||
|
nodePort: {{ .Values.service.metrics.nodePort }}
|
||||||
|
{{- end }}
|
||||||
|
selector:
|
||||||
|
{{- include "opengist.selectorLabels" . | nindent 4 }}
|
||||||
|
{{- end }}
|
||||||
64
helm/opengist/templates/svc-ssh.yaml
Normal file
64
helm/opengist/templates/svc-ssh.yaml
Normal 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 }}
|
||||||
15
helm/opengist/templates/tests/test-connection.yaml
Normal file
15
helm/opengist/templates/tests/test-connection.yaml
Normal 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
|
||||||
296
helm/opengist/values.yaml
Normal file
296
helm/opengist/values.yaml
Normal file
@@ -0,0 +1,296 @@
|
|||||||
|
## 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"
|
||||||
|
metrics.enabled: false
|
||||||
|
|
||||||
|
## 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.12.1"
|
||||||
|
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
|
||||||
|
|
||||||
|
## StatefulSet configuration
|
||||||
|
## Enables StatefulSet workload instead of Deployment (required for volumeClaimTemplates or stable pod identities).
|
||||||
|
##
|
||||||
|
## Single-replica SQLite example (default behavior):
|
||||||
|
## statefulSet.enabled: true
|
||||||
|
## replicaCount: 1
|
||||||
|
## persistence.mode: perReplica # or omit (default)
|
||||||
|
## # Creates one PVC per pod via volumeClaimTemplates (RWO)
|
||||||
|
##
|
||||||
|
## Multi-replica requirements (replicaCount > 1):
|
||||||
|
## 1. External database: config.db-uri must be postgres:// or mysql:// (SQLite NOT supported)
|
||||||
|
## 2. Shared storage: Use ONE of:
|
||||||
|
## a) Existing claim: persistence.existingClaim: "my-rwx-pvc"
|
||||||
|
## b) Chart-created: persistence.mode: shared + persistence.create.enabled: true + accessModes: [ReadWriteMany]
|
||||||
|
## 3. Chart will FAIL FAST if constraints are not met to prevent data divergence
|
||||||
|
##
|
||||||
|
## Persistence decision tree:
|
||||||
|
## - persistence.existingClaim set → mount that PVC directly (no volumeClaimTemplates)
|
||||||
|
## - persistence.mode=shared + create.* → chart creates single RWX PVC, all pods mount it
|
||||||
|
## - persistence.mode=perReplica (default) → volumeClaimTemplates (one PVC/pod, RWO typically)
|
||||||
|
## - persistence.enabled=false → emptyDir (ephemeral)
|
||||||
|
|
||||||
|
statefulSet:
|
||||||
|
enabled: false
|
||||||
|
podManagementPolicy: OrderedReady
|
||||||
|
updateStrategy:
|
||||||
|
type: RollingUpdate
|
||||||
|
|
||||||
|
## 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:
|
||||||
|
|
||||||
|
# A metrics K8S service on port 6158 is created when the Opengist config metrics.enabled: true
|
||||||
|
metrics:
|
||||||
|
type: ClusterIP
|
||||||
|
clusterIP:
|
||||||
|
port: 6158
|
||||||
|
nodePort:
|
||||||
|
labels: {}
|
||||||
|
annotations: {}
|
||||||
|
|
||||||
|
# A service monitor can be used to work with your Prometheus setup.
|
||||||
|
serviceMonitor:
|
||||||
|
enabled: true
|
||||||
|
labels: {}
|
||||||
|
# release: kube-prom-stack
|
||||||
|
interval:
|
||||||
|
scrapeTimeout:
|
||||||
|
annotations: {}
|
||||||
|
relabelings: []
|
||||||
|
metricRelabelings: []
|
||||||
|
|
||||||
|
## 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: ""
|
||||||
|
|
||||||
|
## Persistent storage for /opengist data directory
|
||||||
|
## ref: https://kubernetes.io/docs/concepts/storage/persistent-volumes/
|
||||||
|
persistence:
|
||||||
|
enabled: true
|
||||||
|
|
||||||
|
## Persistence mode controls how storage is provisioned:
|
||||||
|
##
|
||||||
|
## perReplica (DEFAULT):
|
||||||
|
## - StatefulSet creates one PVC per replica via volumeClaimTemplates
|
||||||
|
## - Typically RWO (ReadWriteOnce) storage
|
||||||
|
## - Safe ONLY for replicaCount=1 (multi-replica causes data divergence)
|
||||||
|
## - Use when: single-node dev/test, no horizontal scaling needed
|
||||||
|
##
|
||||||
|
## shared:
|
||||||
|
## - Single RWX (ReadWriteMany) PVC shared by all replicas
|
||||||
|
## - Required for replicaCount > 1
|
||||||
|
## - Two provisioning paths:
|
||||||
|
## a) existingClaim: "my-rwx-pvc" (you manage the PVC lifecycle)
|
||||||
|
## b) existingClaim: "" + create.enabled: true (chart creates PVC automatically)
|
||||||
|
## - Use when: multi-replica HA, horizontal scaling, shared file access
|
||||||
|
##
|
||||||
|
## WARNING: Switching modes after initial deploy requires manual data migration:
|
||||||
|
## 1. Scale down to 1 replica
|
||||||
|
## 2. Create/provision RWX PVC and copy data
|
||||||
|
## 3. Update values: mode=shared, existingClaim or create.enabled
|
||||||
|
## 4. Scale up
|
||||||
|
mode: perReplica
|
||||||
|
|
||||||
|
## Reference an existing PVC (takes precedence over create.*)
|
||||||
|
## When set:
|
||||||
|
## - Chart will NOT create a PVC
|
||||||
|
## - StatefulSet mounts this claim directly (no volumeClaimTemplates)
|
||||||
|
## - Must be RWX for replicaCount > 1
|
||||||
|
## Example: existingClaim: "opengist-shared-rwx"
|
||||||
|
existingClaim: ""
|
||||||
|
|
||||||
|
## Common persistence parameters (apply to perReplica mode OR as defaults for create.*)
|
||||||
|
storageClass: "" # Empty = cluster default
|
||||||
|
labels: {}
|
||||||
|
annotations:
|
||||||
|
helm.sh/resource-policy: keep # Prevents PVC deletion on helm uninstall
|
||||||
|
size: 5Gi
|
||||||
|
accessModes:
|
||||||
|
- ReadWriteOnce # perReplica default; override to [ReadWriteMany] if using existingClaim
|
||||||
|
subPath: "" # Optional subpath within volume
|
||||||
|
|
||||||
|
## Chart-managed PVC creation (ONLY for mode=shared when existingClaim is empty)
|
||||||
|
## Renders templates/pvc-shared.yaml
|
||||||
|
create:
|
||||||
|
enabled: true
|
||||||
|
nameSuffix: shared # PVC name: <release-name>-shared
|
||||||
|
storageClass: "" # Empty = cluster default; override if you need specific storage class
|
||||||
|
size: 5Gi # Override top-level persistence.size if needed
|
||||||
|
accessModes:
|
||||||
|
- ReadWriteMany # REQUIRED for multi-replica; NFS/CephFS/Longhorn RWX/etc.
|
||||||
|
labels: {}
|
||||||
|
annotations: {}
|
||||||
|
## Example for specific storage:
|
||||||
|
## storageClass: "nfs-client"
|
||||||
|
## size: 20Gi
|
||||||
|
|
||||||
|
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:
|
||||||
@@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
64
internal/auth/ldap/ldap.go
Normal file
64
internal/auth/ldap/ldap.go
Normal 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
|
||||||
|
}
|
||||||
117
internal/auth/oauth/gitea.go
Normal file
117
internal/auth/oauth/gitea.go
Normal 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,
|
||||||
|
}
|
||||||
|
}
|
||||||
84
internal/auth/oauth/github.go
Normal file
84
internal/auth/oauth/github.go
Normal 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,
|
||||||
|
}
|
||||||
|
}
|
||||||
118
internal/auth/oauth/gitlab.go
Normal file
118
internal/auth/oauth/gitlab.go
Normal 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,
|
||||||
|
}
|
||||||
|
}
|
||||||
86
internal/auth/oauth/openid.go
Normal file
86
internal/auth/oauth/openid.go
Normal 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,
|
||||||
|
}
|
||||||
|
}
|
||||||
93
internal/auth/oauth/provider.go
Normal file
93
internal/auth/oauth/provider.go
Normal 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
|
||||||
|
}
|
||||||
@@ -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
|
||||||
}
|
}
|
||||||
427
internal/auth/password/argon2id_test.go
Normal file
427
internal/auth/password/argon2id_test.go
Normal file
@@ -0,0 +1,427 @@
|
|||||||
|
package password
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/base64"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestArgon2ID_Hash(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
plain string
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "basic password",
|
||||||
|
plain: "password123",
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "empty string",
|
||||||
|
plain: "",
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "long password",
|
||||||
|
plain: strings.Repeat("a", 10000),
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "unicode password",
|
||||||
|
plain: "パスワード🔒",
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "special characters",
|
||||||
|
plain: "!@#$%^&*()_+-=[]{}|;:',.<>?/`~",
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
hash, err := Argon2id.Hash(tt.plain)
|
||||||
|
if (err != nil) != tt.wantErr {
|
||||||
|
t.Errorf("Argon2id.Hash() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !tt.wantErr {
|
||||||
|
// Verify the hash format
|
||||||
|
if !strings.HasPrefix(hash, "$argon2id$") {
|
||||||
|
t.Errorf("Hash does not start with $argon2id$: %v", hash)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify all parts are present
|
||||||
|
parts := strings.Split(hash, "$")
|
||||||
|
if len(parts) != 6 {
|
||||||
|
t.Errorf("Hash has %d parts, expected 6: %v", len(parts), hash)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify salt is properly encoded
|
||||||
|
if len(parts) >= 5 {
|
||||||
|
_, err := base64.RawStdEncoding.DecodeString(parts[4])
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Salt is not properly base64 encoded: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify hash is properly encoded
|
||||||
|
if len(parts) >= 6 {
|
||||||
|
_, err := base64.RawStdEncoding.DecodeString(parts[5])
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Hash is not properly base64 encoded: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestArgon2ID_Verify(t *testing.T) {
|
||||||
|
// Generate a valid hash for testing
|
||||||
|
testPassword := "correctpassword"
|
||||||
|
validHash, err := Argon2id.Hash(testPassword)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to generate test hash: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
plain string
|
||||||
|
hash string
|
||||||
|
wantMatch bool
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "correct password",
|
||||||
|
plain: testPassword,
|
||||||
|
hash: validHash,
|
||||||
|
wantMatch: true,
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "incorrect password",
|
||||||
|
plain: "wrongpassword",
|
||||||
|
hash: validHash,
|
||||||
|
wantMatch: false,
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "empty password",
|
||||||
|
plain: "",
|
||||||
|
hash: validHash,
|
||||||
|
wantMatch: false,
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "empty hash",
|
||||||
|
plain: testPassword,
|
||||||
|
hash: "",
|
||||||
|
wantMatch: false,
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid hash - too few parts",
|
||||||
|
plain: testPassword,
|
||||||
|
hash: "$argon2id$v=19$m=65536",
|
||||||
|
wantMatch: false,
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid hash - too many parts",
|
||||||
|
plain: testPassword,
|
||||||
|
hash: "$argon2id$v=19$m=65536,t=1,p=4$salt$hash$extra",
|
||||||
|
wantMatch: false,
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid hash - malformed parameters",
|
||||||
|
plain: testPassword,
|
||||||
|
hash: "$argon2id$v=19$invalid$salt$hash",
|
||||||
|
wantMatch: false,
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid hash - bad base64 salt",
|
||||||
|
plain: testPassword,
|
||||||
|
hash: "$argon2id$v=19$m=65536,t=1,p=4$not-valid-base64!@#$hash",
|
||||||
|
wantMatch: false,
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid hash - bad base64 hash",
|
||||||
|
plain: testPassword,
|
||||||
|
hash: "$argon2id$v=19$m=65536,t=1,p=4$dGVzdA$not-valid-base64!@#",
|
||||||
|
wantMatch: false,
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "wrong algorithm prefix",
|
||||||
|
plain: testPassword,
|
||||||
|
hash: "$bcrypt$rounds=10$saltsaltsaltsaltsalt",
|
||||||
|
wantMatch: false,
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
match, err := Argon2id.Verify(tt.plain, tt.hash)
|
||||||
|
if (err != nil) != tt.wantErr {
|
||||||
|
t.Errorf("Argon2id.Verify() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if match != tt.wantMatch {
|
||||||
|
t.Errorf("Argon2id.Verify() match = %v, wantMatch %v", match, tt.wantMatch)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestArgon2ID_SaltUniqueness(t *testing.T) {
|
||||||
|
password := "testpassword"
|
||||||
|
iterations := 10
|
||||||
|
|
||||||
|
hashes := make(map[string]bool)
|
||||||
|
salts := make(map[string]bool)
|
||||||
|
|
||||||
|
for i := 0; i < iterations; i++ {
|
||||||
|
hash, err := Argon2id.Hash(password)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Hash iteration %d failed: %v", i, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check hash uniqueness
|
||||||
|
if hashes[hash] {
|
||||||
|
t.Errorf("Duplicate hash generated at iteration %d", i)
|
||||||
|
}
|
||||||
|
hashes[hash] = true
|
||||||
|
|
||||||
|
// Extract and check salt uniqueness
|
||||||
|
parts := strings.Split(hash, "$")
|
||||||
|
if len(parts) >= 5 {
|
||||||
|
salt := parts[4]
|
||||||
|
if salts[salt] {
|
||||||
|
t.Errorf("Duplicate salt generated at iteration %d", i)
|
||||||
|
}
|
||||||
|
salts[salt] = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify each hash works
|
||||||
|
match, err := Argon2id.Verify(password, hash)
|
||||||
|
if err != nil || !match {
|
||||||
|
t.Errorf("Hash %d failed verification: err=%v, match=%v", i, err, match)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestArgon2ID_HashFormat(t *testing.T) {
|
||||||
|
password := "testformat"
|
||||||
|
hash, err := Argon2id.Hash(password)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Hash failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
parts := strings.Split(hash, "$")
|
||||||
|
if len(parts) != 6 {
|
||||||
|
t.Fatalf("Expected 6 parts, got %d: %v", len(parts), hash)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Part 0 should be empty (before first $)
|
||||||
|
if parts[0] != "" {
|
||||||
|
t.Errorf("Part 0 should be empty, got: %v", parts[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
// Part 1 should be "argon2id"
|
||||||
|
if parts[1] != "argon2id" {
|
||||||
|
t.Errorf("Part 1 should be 'argon2id', got: %v", parts[1])
|
||||||
|
}
|
||||||
|
|
||||||
|
// Part 2 should be version
|
||||||
|
if !strings.HasPrefix(parts[2], "v=") {
|
||||||
|
t.Errorf("Part 2 should start with 'v=', got: %v", parts[2])
|
||||||
|
}
|
||||||
|
|
||||||
|
// Part 3 should be parameters
|
||||||
|
if !strings.Contains(parts[3], "m=") || !strings.Contains(parts[3], "t=") || !strings.Contains(parts[3], "p=") {
|
||||||
|
t.Errorf("Part 3 should contain m=, t=, and p=, got: %v", parts[3])
|
||||||
|
}
|
||||||
|
|
||||||
|
// Part 4 should be base64 encoded salt
|
||||||
|
salt, err := base64.RawStdEncoding.DecodeString(parts[4])
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Salt (part 4) is not valid base64: %v", err)
|
||||||
|
}
|
||||||
|
if len(salt) != int(Argon2id.saltLen) {
|
||||||
|
t.Errorf("Salt length is %d, expected %d", len(salt), Argon2id.saltLen)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Part 5 should be base64 encoded hash
|
||||||
|
decodedHash, err := base64.RawStdEncoding.DecodeString(parts[5])
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Hash (part 5) is not valid base64: %v", err)
|
||||||
|
}
|
||||||
|
if len(decodedHash) != int(Argon2id.keyLen) {
|
||||||
|
t.Errorf("Hash length is %d, expected %d", len(decodedHash), Argon2id.keyLen)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestArgon2ID_CaseModification(t *testing.T) {
|
||||||
|
// Passwords should be case-sensitive
|
||||||
|
password := "TestPassword"
|
||||||
|
hash, err := Argon2id.Hash(password)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Hash failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Correct case should match
|
||||||
|
match, err := Argon2id.Verify(password, hash)
|
||||||
|
if err != nil || !match {
|
||||||
|
t.Errorf("Correct password failed: err=%v, match=%v", err, match)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wrong case should not match
|
||||||
|
match, err = Argon2id.Verify("testpassword", hash)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Verify returned error: %v", err)
|
||||||
|
}
|
||||||
|
if match {
|
||||||
|
t.Error("Password verification should be case-sensitive")
|
||||||
|
}
|
||||||
|
|
||||||
|
match, err = Argon2id.Verify("TESTPASSWORD", hash)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Verify returned error: %v", err)
|
||||||
|
}
|
||||||
|
if match {
|
||||||
|
t.Error("Password verification should be case-sensitive")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestArgon2ID_InvalidParameters(t *testing.T) {
|
||||||
|
password := "testpassword"
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
hash string
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "negative memory parameter",
|
||||||
|
hash: "$argon2id$v=19$m=-1,t=1,p=4$dGVzdHNhbHQ$testhash",
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "negative time parameter",
|
||||||
|
hash: "$argon2id$v=19$m=65536,t=-1,p=4$dGVzdHNhbHQ$testhash",
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "negative parallelism parameter",
|
||||||
|
hash: "$argon2id$v=19$m=65536,t=1,p=-4$dGVzdHNhbHQ$testhash",
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "zero memory parameter",
|
||||||
|
hash: "$argon2id$v=19$m=0,t=1,p=4$dGVzdHNhbHQ$testhash",
|
||||||
|
wantErr: false, // argon2 may handle this, we just test parsing
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "missing parameter value",
|
||||||
|
hash: "$argon2id$v=19$m=,t=1,p=4$dGVzdHNhbHQ$testhash",
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "non-numeric parameter",
|
||||||
|
hash: "$argon2id$v=19$m=abc,t=1,p=4$dGVzdHNhbHQ$testhash",
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "missing parameters separator",
|
||||||
|
hash: "$argon2id$v=19$m=65536 t=1 p=4$dGVzdHNhbHQ$testhash",
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
_, err := Argon2id.Verify(password, tt.hash)
|
||||||
|
if (err != nil) != tt.wantErr {
|
||||||
|
t.Errorf("Argon2id.Verify() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestArgon2ID_ConcurrentHashing(t *testing.T) {
|
||||||
|
password := "testpassword"
|
||||||
|
concurrency := 10
|
||||||
|
|
||||||
|
type result struct {
|
||||||
|
hash string
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
|
||||||
|
results := make(chan result, concurrency)
|
||||||
|
|
||||||
|
// Generate hashes concurrently
|
||||||
|
for i := 0; i < concurrency; i++ {
|
||||||
|
go func() {
|
||||||
|
hash, err := Argon2id.Hash(password)
|
||||||
|
results <- result{hash: hash, err: err}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Collect results
|
||||||
|
hashes := make(map[string]bool)
|
||||||
|
for i := 0; i < concurrency; i++ {
|
||||||
|
res := <-results
|
||||||
|
if res.err != nil {
|
||||||
|
t.Errorf("Concurrent hash %d failed: %v", i, res.err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for duplicates
|
||||||
|
if hashes[res.hash] {
|
||||||
|
t.Errorf("Duplicate hash generated in concurrent test")
|
||||||
|
}
|
||||||
|
hashes[res.hash] = true
|
||||||
|
|
||||||
|
// Verify each hash works
|
||||||
|
match, err := Argon2id.Verify(password, res.hash)
|
||||||
|
if err != nil || !match {
|
||||||
|
t.Errorf("Hash %d failed verification: err=%v, match=%v", i, err, match)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestArgon2ID_VeryLongPassword(t *testing.T) {
|
||||||
|
// Test with extremely long password (100KB)
|
||||||
|
password := strings.Repeat("a", 100*1024)
|
||||||
|
|
||||||
|
hash, err := Argon2id.Hash(password)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to hash very long password: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
match, err := Argon2id.Verify(password, hash)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to verify very long password: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !match {
|
||||||
|
t.Error("Very long password failed verification")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify wrong password still fails
|
||||||
|
wrongPassword := strings.Repeat("b", 100*1024)
|
||||||
|
match, err = Argon2id.Verify(wrongPassword, hash)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Verify returned error: %v", err)
|
||||||
|
}
|
||||||
|
if match {
|
||||||
|
t.Error("Wrong very long password should not match")
|
||||||
|
}
|
||||||
|
}
|
||||||
9
internal/auth/password/password.go
Normal file
9
internal/auth/password/password.go
Normal 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)
|
||||||
|
}
|
||||||
193
internal/auth/password/password_test.go
Normal file
193
internal/auth/password/password_test.go
Normal file
@@ -0,0 +1,193 @@
|
|||||||
|
package password
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestHashPassword(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
password string
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "simple password",
|
||||||
|
password: "password123",
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "empty password",
|
||||||
|
password: "",
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "long password",
|
||||||
|
password: strings.Repeat("a", 1000),
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "special characters",
|
||||||
|
password: "p@ssw0rd!#$%^&*()",
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "unicode characters",
|
||||||
|
password: "パスワード123",
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
hash, err := HashPassword(tt.password)
|
||||||
|
if (err != nil) != tt.wantErr {
|
||||||
|
t.Errorf("HashPassword() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !tt.wantErr {
|
||||||
|
// Verify hash format
|
||||||
|
if !strings.HasPrefix(hash, "$argon2id$") {
|
||||||
|
t.Errorf("HashPassword() returned invalid hash format: %v", hash)
|
||||||
|
}
|
||||||
|
// Verify hash has correct number of parts
|
||||||
|
parts := strings.Split(hash, "$")
|
||||||
|
if len(parts) != 6 {
|
||||||
|
t.Errorf("HashPassword() returned hash with incorrect number of parts: %v", len(parts))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestVerifyPassword(t *testing.T) {
|
||||||
|
// Pre-generate a known hash for testing
|
||||||
|
testPassword := "testpassword123"
|
||||||
|
testHash, err := HashPassword(testPassword)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to generate test hash: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
password string
|
||||||
|
hash string
|
||||||
|
wantMatch bool
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "correct password",
|
||||||
|
password: testPassword,
|
||||||
|
hash: testHash,
|
||||||
|
wantMatch: true,
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "incorrect password",
|
||||||
|
password: "wrongpassword",
|
||||||
|
hash: testHash,
|
||||||
|
wantMatch: false,
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "empty password against valid hash",
|
||||||
|
password: "",
|
||||||
|
hash: testHash,
|
||||||
|
wantMatch: false,
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "empty hash",
|
||||||
|
password: testPassword,
|
||||||
|
hash: "",
|
||||||
|
wantMatch: false,
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid hash format",
|
||||||
|
password: testPassword,
|
||||||
|
hash: "invalid",
|
||||||
|
wantMatch: false,
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "malformed hash - wrong prefix",
|
||||||
|
password: testPassword,
|
||||||
|
hash: "$bcrypt$invalid$hash",
|
||||||
|
wantMatch: false,
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
match, err := VerifyPassword(tt.password, tt.hash)
|
||||||
|
if (err != nil) != tt.wantErr {
|
||||||
|
t.Errorf("VerifyPassword() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if match != tt.wantMatch {
|
||||||
|
t.Errorf("VerifyPassword() match = %v, wantMatch %v", match, tt.wantMatch)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHashPasswordUniqueness(t *testing.T) {
|
||||||
|
password := "testpassword"
|
||||||
|
|
||||||
|
// Generate multiple hashes of the same password
|
||||||
|
hash1, err := HashPassword(password)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to hash password: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
hash2, err := HashPassword(password)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to hash password: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hashes should be different due to different salts
|
||||||
|
if hash1 == hash2 {
|
||||||
|
t.Error("HashPassword() should generate unique hashes for the same password")
|
||||||
|
}
|
||||||
|
|
||||||
|
// But both should verify correctly
|
||||||
|
match1, err := VerifyPassword(password, hash1)
|
||||||
|
if err != nil || !match1 {
|
||||||
|
t.Errorf("Failed to verify first hash: err=%v, match=%v", err, match1)
|
||||||
|
}
|
||||||
|
|
||||||
|
match2, err := VerifyPassword(password, hash2)
|
||||||
|
if err != nil || !match2 {
|
||||||
|
t.Errorf("Failed to verify second hash: err=%v, match=%v", err, match2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPasswordRoundTrip(t *testing.T) {
|
||||||
|
tests := []string{
|
||||||
|
"simple",
|
||||||
|
"with spaces and special chars !@#$%",
|
||||||
|
"パスワード",
|
||||||
|
strings.Repeat("long", 100),
|
||||||
|
"",
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, password := range tests {
|
||||||
|
t.Run(password, func(t *testing.T) {
|
||||||
|
hash, err := HashPassword(password)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("HashPassword() failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
match, err := VerifyPassword(password, hash)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("VerifyPassword() failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !match {
|
||||||
|
t.Error("Password round trip failed: hashed password does not verify")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
48
internal/auth/totp/aes.go
Normal file
48
internal/auth/totp/aes.go
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
|
// TODO: remove deprecated
|
||||||
|
//nolint:staticcheck
|
||||||
|
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:]
|
||||||
|
// TODO: remove deprecated
|
||||||
|
//nolint:staticcheck
|
||||||
|
stream := cipher.NewCFBDecrypter(block, iv)
|
||||||
|
stream.XORKeyStream(ciphertext, ciphertext)
|
||||||
|
|
||||||
|
return ciphertext, nil
|
||||||
|
}
|
||||||
430
internal/auth/totp/aes_test.go
Normal file
430
internal/auth/totp/aes_test.go
Normal file
@@ -0,0 +1,430 @@
|
|||||||
|
package totp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"crypto/aes"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestAESEncrypt(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
key []byte
|
||||||
|
text []byte
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "basic encryption with 16-byte key",
|
||||||
|
key: []byte("1234567890123456"), // 16 bytes (AES-128)
|
||||||
|
text: []byte("hello world"),
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "basic encryption with 24-byte key",
|
||||||
|
key: []byte("123456789012345678901234"), // 24 bytes (AES-192)
|
||||||
|
text: []byte("hello world"),
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "basic encryption with 32-byte key",
|
||||||
|
key: []byte("12345678901234567890123456789012"), // 32 bytes (AES-256)
|
||||||
|
text: []byte("hello world"),
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "empty text",
|
||||||
|
key: []byte("1234567890123456"),
|
||||||
|
text: []byte(""),
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "long text",
|
||||||
|
key: []byte("1234567890123456"),
|
||||||
|
text: []byte("This is a much longer text that spans multiple blocks and should be encrypted properly without any issues"),
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "binary data",
|
||||||
|
key: []byte("1234567890123456"),
|
||||||
|
text: []byte{0x00, 0x01, 0x02, 0x03, 0xFF, 0xFE, 0xFD},
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid key length - too short",
|
||||||
|
key: []byte("short"),
|
||||||
|
text: []byte("hello world"),
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid key length - 17 bytes",
|
||||||
|
key: []byte("12345678901234567"), // 17 bytes (invalid)
|
||||||
|
text: []byte("hello world"),
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "nil key",
|
||||||
|
key: nil,
|
||||||
|
text: []byte("hello world"),
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "empty key",
|
||||||
|
key: []byte(""),
|
||||||
|
text: []byte("hello world"),
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
ciphertext, err := AESEncrypt(tt.key, tt.text)
|
||||||
|
if (err != nil) != tt.wantErr {
|
||||||
|
t.Errorf("AESEncrypt() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !tt.wantErr {
|
||||||
|
// Verify ciphertext is not empty
|
||||||
|
if len(ciphertext) == 0 {
|
||||||
|
t.Error("AESEncrypt() returned empty ciphertext")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify ciphertext length is correct (IV + encrypted text)
|
||||||
|
expectedLen := aes.BlockSize + len(tt.text)
|
||||||
|
if len(ciphertext) != expectedLen {
|
||||||
|
t.Errorf("AESEncrypt() ciphertext length = %d, want %d", len(ciphertext), expectedLen)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify ciphertext is different from plaintext (unless text is empty)
|
||||||
|
if len(tt.text) > 0 && bytes.Equal(ciphertext[aes.BlockSize:], tt.text) {
|
||||||
|
t.Error("AESEncrypt() ciphertext matches plaintext")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify IV is present and non-zero
|
||||||
|
iv := ciphertext[:aes.BlockSize]
|
||||||
|
allZeros := true
|
||||||
|
for _, b := range iv {
|
||||||
|
if b != 0 {
|
||||||
|
allZeros = false
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if allZeros {
|
||||||
|
t.Error("AESEncrypt() IV is all zeros")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAESDecrypt(t *testing.T) {
|
||||||
|
validKey := []byte("1234567890123456")
|
||||||
|
validText := []byte("hello world")
|
||||||
|
|
||||||
|
// Encrypt some data to use for valid test cases
|
||||||
|
validCiphertext, err := AESEncrypt(validKey, validText)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create valid ciphertext: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
key []byte
|
||||||
|
ciphertext []byte
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "valid decryption",
|
||||||
|
key: validKey,
|
||||||
|
ciphertext: validCiphertext,
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ciphertext too short - empty",
|
||||||
|
key: validKey,
|
||||||
|
ciphertext: []byte(""),
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ciphertext too short - less than block size",
|
||||||
|
key: validKey,
|
||||||
|
ciphertext: []byte("short"),
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ciphertext exactly block size (IV only, no data)",
|
||||||
|
key: validKey,
|
||||||
|
ciphertext: make([]byte, aes.BlockSize),
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid key length",
|
||||||
|
key: []byte("short"),
|
||||||
|
ciphertext: validCiphertext,
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "wrong key",
|
||||||
|
key: []byte("6543210987654321"),
|
||||||
|
ciphertext: validCiphertext,
|
||||||
|
wantErr: false, // Decryption succeeds but produces garbage
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "nil key",
|
||||||
|
key: nil,
|
||||||
|
ciphertext: validCiphertext,
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "nil ciphertext",
|
||||||
|
key: validKey,
|
||||||
|
ciphertext: nil,
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
plaintext, err := AESDecrypt(tt.key, tt.ciphertext)
|
||||||
|
if (err != nil) != tt.wantErr {
|
||||||
|
t.Errorf("AESDecrypt() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !tt.wantErr {
|
||||||
|
// For valid decryption with correct key, verify we get original text
|
||||||
|
if tt.name == "valid decryption" && !bytes.Equal(plaintext, validText) {
|
||||||
|
t.Errorf("AESDecrypt() plaintext = %v, want %v", plaintext, validText)
|
||||||
|
}
|
||||||
|
|
||||||
|
// For ciphertext with only IV, plaintext should be empty
|
||||||
|
if tt.name == "ciphertext exactly block size (IV only, no data)" && len(plaintext) != 0 {
|
||||||
|
t.Errorf("AESDecrypt() plaintext length = %d, want 0", len(plaintext))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAESEncryptDecrypt_RoundTrip(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
key []byte
|
||||||
|
text []byte
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "basic round trip",
|
||||||
|
key: []byte("1234567890123456"),
|
||||||
|
text: []byte("hello world"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "empty text round trip",
|
||||||
|
key: []byte("1234567890123456"),
|
||||||
|
text: []byte(""),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "long text round trip",
|
||||||
|
key: []byte("1234567890123456"),
|
||||||
|
text: []byte("This is a very long text that contains multiple blocks of data and should be encrypted and decrypted correctly without any data loss or corruption"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "binary data round trip",
|
||||||
|
key: []byte("1234567890123456"),
|
||||||
|
text: []byte{0x00, 0x01, 0x02, 0x03, 0xFF, 0xFE, 0xFD, 0xFC},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "unicode text round trip",
|
||||||
|
key: []byte("1234567890123456"),
|
||||||
|
text: []byte("Hello 世界! 🔐 Encryption"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "AES-192 round trip",
|
||||||
|
key: []byte("123456789012345678901234"),
|
||||||
|
text: []byte("testing AES-192"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "AES-256 round trip",
|
||||||
|
key: []byte("12345678901234567890123456789012"),
|
||||||
|
text: []byte("testing AES-256"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "special characters",
|
||||||
|
key: []byte("1234567890123456"),
|
||||||
|
text: []byte("!@#$%^&*()_+-=[]{}|;':\",./<>?"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "newlines and tabs",
|
||||||
|
key: []byte("1234567890123456"),
|
||||||
|
text: []byte("line1\nline2\tline3\r\nline4"),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
// Encrypt
|
||||||
|
ciphertext, err := AESEncrypt(tt.key, tt.text)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("AESEncrypt() failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decrypt
|
||||||
|
plaintext, err := AESDecrypt(tt.key, ciphertext)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("AESDecrypt() failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify plaintext matches original
|
||||||
|
if !bytes.Equal(plaintext, tt.text) {
|
||||||
|
t.Errorf("Round trip failed: got %v, want %v", plaintext, tt.text)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAESEncrypt_Uniqueness(t *testing.T) {
|
||||||
|
key := []byte("1234567890123456")
|
||||||
|
text := []byte("hello world")
|
||||||
|
iterations := 10
|
||||||
|
|
||||||
|
ciphertexts := make(map[string]bool)
|
||||||
|
|
||||||
|
for i := 0; i < iterations; i++ {
|
||||||
|
ciphertext, err := AESEncrypt(key, text)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Iteration %d failed: %v", i, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Each encryption should produce different ciphertext (due to random IV)
|
||||||
|
ciphertextStr := string(ciphertext)
|
||||||
|
if ciphertexts[ciphertextStr] {
|
||||||
|
t.Errorf("Duplicate ciphertext generated at iteration %d", i)
|
||||||
|
}
|
||||||
|
ciphertexts[ciphertextStr] = true
|
||||||
|
|
||||||
|
// But all should decrypt to the same plaintext
|
||||||
|
plaintext, err := AESDecrypt(key, ciphertext)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Iteration %d decryption failed: %v", i, err)
|
||||||
|
}
|
||||||
|
if !bytes.Equal(plaintext, text) {
|
||||||
|
t.Errorf("Iteration %d: decrypted text doesn't match original", i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAESEncrypt_IVUniqueness(t *testing.T) {
|
||||||
|
key := []byte("1234567890123456")
|
||||||
|
text := []byte("test data")
|
||||||
|
iterations := 20
|
||||||
|
|
||||||
|
ivs := make(map[string]bool)
|
||||||
|
|
||||||
|
for i := 0; i < iterations; i++ {
|
||||||
|
ciphertext, err := AESEncrypt(key, text)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Iteration %d failed: %v", i, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract IV (first block)
|
||||||
|
iv := ciphertext[:aes.BlockSize]
|
||||||
|
ivStr := string(iv)
|
||||||
|
|
||||||
|
// Each IV should be unique
|
||||||
|
if ivs[ivStr] {
|
||||||
|
t.Errorf("Duplicate IV generated at iteration %d", i)
|
||||||
|
}
|
||||||
|
ivs[ivStr] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAESDecrypt_WrongKey(t *testing.T) {
|
||||||
|
originalKey := []byte("1234567890123456")
|
||||||
|
wrongKey := []byte("6543210987654321")
|
||||||
|
text := []byte("secret message")
|
||||||
|
|
||||||
|
// Encrypt with original key
|
||||||
|
ciphertext, err := AESEncrypt(originalKey, text)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("AESEncrypt() failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decrypt with wrong key - should not error but produce wrong plaintext
|
||||||
|
plaintext, err := AESDecrypt(wrongKey, ciphertext)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("AESDecrypt() with wrong key failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Plaintext should be different from original
|
||||||
|
if bytes.Equal(plaintext, text) {
|
||||||
|
t.Error("AESDecrypt() with wrong key produced correct plaintext")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAESDecrypt_CorruptedCiphertext(t *testing.T) {
|
||||||
|
key := []byte("1234567890123456")
|
||||||
|
text := []byte("hello world")
|
||||||
|
|
||||||
|
// Encrypt
|
||||||
|
ciphertext, err := AESEncrypt(key, text)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("AESEncrypt() failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Corrupt the ciphertext (flip a bit in the encrypted data, not the IV)
|
||||||
|
if len(ciphertext) > aes.BlockSize {
|
||||||
|
corruptedCiphertext := make([]byte, len(ciphertext))
|
||||||
|
copy(corruptedCiphertext, ciphertext)
|
||||||
|
corruptedCiphertext[aes.BlockSize] ^= 0xFF
|
||||||
|
|
||||||
|
// Decrypt corrupted ciphertext - should not error but produce wrong plaintext
|
||||||
|
plaintext, err := AESDecrypt(key, corruptedCiphertext)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("AESDecrypt() with corrupted ciphertext failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Plaintext should be different from original
|
||||||
|
if bytes.Equal(plaintext, text) {
|
||||||
|
t.Error("AESDecrypt() with corrupted ciphertext produced correct plaintext")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAESEncryptDecrypt_DifferentKeySizes(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
keySize int
|
||||||
|
}{
|
||||||
|
{"AES-128", 16},
|
||||||
|
{"AES-192", 24},
|
||||||
|
{"AES-256", 32},
|
||||||
|
}
|
||||||
|
|
||||||
|
text := []byte("test message for different key sizes")
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
// Generate key of specified size
|
||||||
|
key := make([]byte, tt.keySize)
|
||||||
|
for i := range key {
|
||||||
|
key[i] = byte(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Encrypt
|
||||||
|
ciphertext, err := AESEncrypt(key, text)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("AESEncrypt() failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decrypt
|
||||||
|
plaintext, err := AESDecrypt(key, ciphertext)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("AESDecrypt() failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify
|
||||||
|
if !bytes.Equal(plaintext, text) {
|
||||||
|
t.Errorf("Round trip failed for %s", tt.name)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
62
internal/auth/totp/totp.go
Normal file
62
internal/auth/totp/totp.go
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
package totp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"crypto/rand"
|
||||||
|
"encoding/base64"
|
||||||
|
"html/template"
|
||||||
|
"image/png"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/pquerna/otp/totp"
|
||||||
|
)
|
||||||
|
|
||||||
|
const secretSize = 16
|
||||||
|
|
||||||
|
func GenerateQRCode(username, siteUrl string, secret []byte) (string, template.URL, []byte, error) {
|
||||||
|
var err error
|
||||||
|
if secret == nil {
|
||||||
|
secret, err = generateSecret()
|
||||||
|
if err != nil {
|
||||||
|
return "", "", nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
otpKey, err := totp.Generate(totp.GenerateOpts{
|
||||||
|
SecretSize: secretSize,
|
||||||
|
Issuer: "Opengist (" + strings.ReplaceAll(siteUrl, ":", "") + ")",
|
||||||
|
AccountName: username,
|
||||||
|
Secret: secret,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return "", "", nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
qrcode, err := otpKey.Image(320, 240)
|
||||||
|
if err != nil {
|
||||||
|
return "", "", nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var imgBytes bytes.Buffer
|
||||||
|
if err = png.Encode(&imgBytes, qrcode); err != nil {
|
||||||
|
return "", "", nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
qrcodeImage := template.URL("data:image/png;base64," + base64.StdEncoding.EncodeToString(imgBytes.Bytes()))
|
||||||
|
|
||||||
|
return otpKey.Secret(), qrcodeImage, secret, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
431
internal/auth/totp/totp_test.go
Normal file
431
internal/auth/totp/totp_test.go
Normal file
@@ -0,0 +1,431 @@
|
|||||||
|
package totp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/base64"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/pquerna/otp/totp"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestGenerateQRCode(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
username string
|
||||||
|
siteUrl string
|
||||||
|
secret []byte
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "basic generation with nil secret",
|
||||||
|
username: "testuser",
|
||||||
|
siteUrl: "opengist.io",
|
||||||
|
secret: nil,
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "basic generation with provided secret",
|
||||||
|
username: "testuser",
|
||||||
|
siteUrl: "opengist.io",
|
||||||
|
secret: []byte("1234567890123456"),
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "username with special characters",
|
||||||
|
username: "test.user",
|
||||||
|
siteUrl: "opengist.io",
|
||||||
|
secret: nil,
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "site URL with protocol and port",
|
||||||
|
username: "testuser",
|
||||||
|
siteUrl: "https://opengist.io:6157",
|
||||||
|
secret: nil,
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "empty username",
|
||||||
|
username: "",
|
||||||
|
siteUrl: "opengist.io",
|
||||||
|
secret: nil,
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "empty site URL",
|
||||||
|
username: "testuser",
|
||||||
|
siteUrl: "",
|
||||||
|
secret: nil,
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
secretStr, qrcode, secretBytes, err := GenerateQRCode(tt.username, tt.siteUrl, tt.secret)
|
||||||
|
if (err != nil) != tt.wantErr {
|
||||||
|
t.Errorf("GenerateQRCode() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !tt.wantErr {
|
||||||
|
// Verify secret string is not empty
|
||||||
|
if secretStr == "" {
|
||||||
|
t.Error("GenerateQRCode() returned empty secret string")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify QR code image is generated
|
||||||
|
if qrcode == "" {
|
||||||
|
t.Error("GenerateQRCode() returned empty QR code")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify QR code has correct data URI prefix
|
||||||
|
if !strings.HasPrefix(string(qrcode), "data:image/png;base64,") {
|
||||||
|
t.Errorf("QR code does not have correct data URI prefix: %s", qrcode[:50])
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify QR code is valid base64 after prefix
|
||||||
|
base64Data := strings.TrimPrefix(string(qrcode), "data:image/png;base64,")
|
||||||
|
_, err := base64.StdEncoding.DecodeString(base64Data)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("QR code base64 data is invalid: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify secret bytes are returned
|
||||||
|
if secretBytes == nil {
|
||||||
|
t.Error("GenerateQRCode() returned nil secret bytes")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify secret bytes have correct length
|
||||||
|
if len(secretBytes) != secretSize {
|
||||||
|
t.Errorf("Secret bytes length = %d, want %d", len(secretBytes), secretSize)
|
||||||
|
}
|
||||||
|
|
||||||
|
// If a secret was provided, verify it matches what was returned
|
||||||
|
if tt.secret != nil && string(secretBytes) != string(tt.secret) {
|
||||||
|
t.Error("Returned secret bytes do not match provided secret")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGenerateQRCode_SecretUniqueness(t *testing.T) {
|
||||||
|
username := "testuser"
|
||||||
|
siteUrl := "opengist.io"
|
||||||
|
iterations := 10
|
||||||
|
|
||||||
|
secrets := make(map[string]bool)
|
||||||
|
secretBytes := make(map[string]bool)
|
||||||
|
|
||||||
|
for i := 0; i < iterations; i++ {
|
||||||
|
secretStr, _, secret, err := GenerateQRCode(username, siteUrl, nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Iteration %d failed: %v", i, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check secret string uniqueness
|
||||||
|
if secrets[secretStr] {
|
||||||
|
t.Errorf("Duplicate secret string generated at iteration %d", i)
|
||||||
|
}
|
||||||
|
secrets[secretStr] = true
|
||||||
|
|
||||||
|
// Check secret bytes uniqueness
|
||||||
|
secretKey := string(secret)
|
||||||
|
if secretBytes[secretKey] {
|
||||||
|
t.Errorf("Duplicate secret bytes generated at iteration %d", i)
|
||||||
|
}
|
||||||
|
secretBytes[secretKey] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGenerateQRCode_WithProvidedSecret(t *testing.T) {
|
||||||
|
username := "testuser"
|
||||||
|
siteUrl := "opengist.io"
|
||||||
|
providedSecret := []byte("mysecret12345678")
|
||||||
|
|
||||||
|
// Generate QR code multiple times with the same secret
|
||||||
|
secretStr1, _, secret1, err := GenerateQRCode(username, siteUrl, providedSecret)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("First generation failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
secretStr2, _, secret2, err := GenerateQRCode(username, siteUrl, providedSecret)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Second generation failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Secret strings should be the same when using the same input secret
|
||||||
|
if secretStr1 != secretStr2 {
|
||||||
|
t.Error("Secret strings differ when using the same provided secret")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Secret bytes should match the provided secret
|
||||||
|
if string(secret1) != string(providedSecret) {
|
||||||
|
t.Error("Returned secret bytes do not match provided secret (first call)")
|
||||||
|
}
|
||||||
|
if string(secret2) != string(providedSecret) {
|
||||||
|
t.Error("Returned secret bytes do not match provided secret (second call)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGenerateQRCode_ConcurrentGeneration(t *testing.T) {
|
||||||
|
username := "testuser"
|
||||||
|
siteUrl := "opengist.io"
|
||||||
|
concurrency := 10
|
||||||
|
|
||||||
|
type result struct {
|
||||||
|
secretStr string
|
||||||
|
secretBytes []byte
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
|
||||||
|
results := make(chan result, concurrency)
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
|
||||||
|
for i := 0; i < concurrency; i++ {
|
||||||
|
wg.Add(1)
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
secretStr, _, secretBytes, err := GenerateQRCode(username, siteUrl, nil)
|
||||||
|
results <- result{secretStr: secretStr, secretBytes: secretBytes, err: err}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
wg.Wait()
|
||||||
|
close(results)
|
||||||
|
|
||||||
|
secrets := make(map[string]bool)
|
||||||
|
for res := range results {
|
||||||
|
if res.err != nil {
|
||||||
|
t.Errorf("Concurrent generation failed: %v", res.err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for duplicates
|
||||||
|
if secrets[res.secretStr] {
|
||||||
|
t.Error("Duplicate secret generated in concurrent test")
|
||||||
|
}
|
||||||
|
secrets[res.secretStr] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestValidate(t *testing.T) {
|
||||||
|
// Generate a valid secret for testing
|
||||||
|
_, _, secret, err := GenerateQRCode("testuser", "opengist.io", nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to generate secret: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert secret bytes to base32 string for TOTP
|
||||||
|
secretStr, _, _, err := GenerateQRCode("testuser", "opengist.io", secret)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to generate secret string: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate a valid passcode for the current time
|
||||||
|
validPasscode, err := totp.GenerateCode(secretStr, time.Now())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to generate valid passcode: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
passcode string
|
||||||
|
secret string
|
||||||
|
wantValid bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "valid passcode",
|
||||||
|
passcode: validPasscode,
|
||||||
|
secret: secretStr,
|
||||||
|
wantValid: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid passcode - wrong digits",
|
||||||
|
passcode: "000000",
|
||||||
|
secret: secretStr,
|
||||||
|
wantValid: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid passcode - wrong length",
|
||||||
|
passcode: "123",
|
||||||
|
secret: secretStr,
|
||||||
|
wantValid: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "empty passcode",
|
||||||
|
passcode: "",
|
||||||
|
secret: secretStr,
|
||||||
|
wantValid: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "empty secret",
|
||||||
|
passcode: validPasscode,
|
||||||
|
secret: "",
|
||||||
|
wantValid: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid secret format",
|
||||||
|
passcode: validPasscode,
|
||||||
|
secret: "not-a-valid-base32-secret!@#",
|
||||||
|
wantValid: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "passcode with letters",
|
||||||
|
passcode: "12345A",
|
||||||
|
secret: secretStr,
|
||||||
|
wantValid: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "passcode with spaces",
|
||||||
|
passcode: "123 456",
|
||||||
|
secret: secretStr,
|
||||||
|
wantValid: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
valid := Validate(tt.passcode, tt.secret)
|
||||||
|
if valid != tt.wantValid {
|
||||||
|
t.Errorf("Validate() = %v, want %v", valid, tt.wantValid)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestValidate_TimeDrift(t *testing.T) {
|
||||||
|
// Generate a valid secret
|
||||||
|
secretStr, _, _, err := GenerateQRCode("testuser", "opengist.io", nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to generate secret: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test that passcodes from previous and next time windows are accepted
|
||||||
|
// (TOTP typically accepts codes from ±1 time window for clock drift)
|
||||||
|
pastTime := time.Now().Add(-30 * time.Second)
|
||||||
|
futureTime := time.Now().Add(30 * time.Second)
|
||||||
|
|
||||||
|
pastPasscode, err := totp.GenerateCode(secretStr, pastTime)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to generate past passcode: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
futurePasscode, err := totp.GenerateCode(secretStr, futureTime)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to generate future passcode: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// These should be valid due to time drift tolerance
|
||||||
|
if !Validate(pastPasscode, secretStr) {
|
||||||
|
t.Error("Validate() rejected passcode from previous time window")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !Validate(futurePasscode, secretStr) {
|
||||||
|
t.Error("Validate() rejected passcode from next time window")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestValidate_ExpiredPasscode(t *testing.T) {
|
||||||
|
// Generate a valid secret
|
||||||
|
secretStr, _, _, err := GenerateQRCode("testuser", "opengist.io", nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to generate secret: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate a passcode from 2 minutes ago (should be expired)
|
||||||
|
oldTime := time.Now().Add(-2 * time.Minute)
|
||||||
|
oldPasscode, err := totp.GenerateCode(secretStr, oldTime)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to generate old passcode: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// This should be invalid
|
||||||
|
if Validate(oldPasscode, secretStr) {
|
||||||
|
t.Error("Validate() accepted expired passcode from 2 minutes ago")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestValidate_RoundTrip(t *testing.T) {
|
||||||
|
// Test full round trip: generate secret, generate code, validate code
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
username string
|
||||||
|
siteUrl string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "basic round trip",
|
||||||
|
username: "testuser",
|
||||||
|
siteUrl: "opengist.io",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "round trip with dot in username",
|
||||||
|
username: "test.user",
|
||||||
|
siteUrl: "opengist.io",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "round trip with hyphen in username",
|
||||||
|
username: "test-user",
|
||||||
|
siteUrl: "opengist.io",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
// Generate QR code and secret
|
||||||
|
secretStr, _, _, err := GenerateQRCode(tt.username, tt.siteUrl, nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("GenerateQRCode() failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate a valid passcode
|
||||||
|
passcode, err := totp.GenerateCode(secretStr, time.Now())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("GenerateCode() failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate the passcode
|
||||||
|
if !Validate(passcode, secretStr) {
|
||||||
|
t.Error("Validate() rejected valid passcode")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate wrong passcode fails
|
||||||
|
wrongPasscode := "000000"
|
||||||
|
if passcode == wrongPasscode {
|
||||||
|
wrongPasscode = "111111"
|
||||||
|
}
|
||||||
|
if Validate(wrongPasscode, secretStr) {
|
||||||
|
t.Error("Validate() accepted invalid passcode")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGenerateSecret(t *testing.T) {
|
||||||
|
// Test the internal generateSecret function behavior through GenerateQRCode
|
||||||
|
for i := 0; i < 10; i++ {
|
||||||
|
_, _, secret, err := GenerateQRCode("testuser", "opengist.io", nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Iteration %d: generateSecret() failed: %v", i, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(secret) != secretSize {
|
||||||
|
t.Errorf("Iteration %d: secret length = %d, want %d", i, len(secret), secretSize)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify secret is not all zeros (extremely unlikely with crypto/rand)
|
||||||
|
allZeros := true
|
||||||
|
for _, b := range secret {
|
||||||
|
if b != 0 {
|
||||||
|
allZeros = false
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if allZeros {
|
||||||
|
t.Errorf("Iteration %d: secret is all zeros", i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
83
internal/auth/try_login.go
Normal file
83
internal/auth/try_login.go
Normal 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
|
||||||
|
}
|
||||||
58
internal/auth/webauthn/user.go
Normal file
58
internal/auth/webauthn/user.go
Normal 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
|
||||||
|
}
|
||||||
139
internal/auth/webauthn/webauthn.go
Normal file
139
internal/auth/webauthn/webauthn.go
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
package webauthn
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
|
||||||
|
"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"
|
||||||
|
)
|
||||||
|
|
||||||
|
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).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
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|||||||
@@ -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")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,13 +3,14 @@ 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/handlers/metrics"
|
||||||
|
"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 +37,18 @@ var CmdStart = cli.Command{
|
|||||||
|
|
||||||
Initialize(ctx)
|
Initialize(ctx)
|
||||||
|
|
||||||
go web.NewServer(os.Getenv("OG_DEV") == "1", path.Join(config.GetHomeDir(), "sessions")).Start()
|
httpServer := server.NewServer(os.Getenv("OG_DEV") == "1", path.Join(config.GetHomeDir(), "sessions"), false)
|
||||||
|
go httpServer.Start()
|
||||||
go ssh.Start()
|
go ssh.Start()
|
||||||
|
|
||||||
|
var metricsServer *metrics.Server
|
||||||
|
if config.C.MetricsEnabled {
|
||||||
|
metricsServer = metrics.NewServer()
|
||||||
|
go metricsServer.Start()
|
||||||
|
}
|
||||||
|
|
||||||
<-stopCtx.Done()
|
<-stopCtx.Done()
|
||||||
shutdown()
|
shutdown(httpServer, metricsServer)
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -75,6 +83,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 +102,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 +122,39 @@ 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(httpServer *server.Server, metricsServer *metrics.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()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
httpServer.Stop()
|
||||||
|
|
||||||
|
if metricsServer != nil {
|
||||||
|
metricsServer.Stop()
|
||||||
|
}
|
||||||
|
|
||||||
log.Info().Msg("Shutdown complete")
|
log.Info().Msg("Shutdown complete")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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,24 @@ 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"`
|
||||||
|
MetricsHost string `yaml:"metrics.host" env:"OG_METRICS_HOST"`
|
||||||
|
MetricsPort string `yaml:"metrics.port" env:"OG_METRICS_PORT"`
|
||||||
|
|
||||||
|
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 +103,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 +117,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 +129,10 @@ func configWithDefaults() (*config, error) {
|
|||||||
c.GiteaUrl = "https://gitea.com"
|
c.GiteaUrl = "https://gitea.com"
|
||||||
c.GiteaName = "Gitea"
|
c.GiteaName = "Gitea"
|
||||||
|
|
||||||
|
c.MetricsEnabled = false
|
||||||
|
c.MetricsHost = "0.0.0.0"
|
||||||
|
c.MetricsPort = "6158"
|
||||||
|
|
||||||
return c, nil
|
return c, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -133,6 +166,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 +188,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 +265,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)
|
||||||
|
|||||||
42
internal/config/migrate.go
Normal file
42
internal/config/migrate.go
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
125
internal/db/access_token.go
Normal file
125
internal/db/access_token.go
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
package db
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/hex"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
NoPermission = 0
|
||||||
|
ReadPermission = 1
|
||||||
|
ReadWritePermission = 2
|
||||||
|
)
|
||||||
|
|
||||||
|
type AccessToken struct {
|
||||||
|
ID uint `gorm:"primaryKey"`
|
||||||
|
Name string
|
||||||
|
TokenHash string `gorm:"uniqueIndex,size:64"` // SHA-256 hash of the token
|
||||||
|
CreatedAt int64
|
||||||
|
ExpiresAt int64 // 0 means no expiration
|
||||||
|
LastUsedAt int64
|
||||||
|
UserID uint
|
||||||
|
User User `validate:"-"`
|
||||||
|
|
||||||
|
ScopeGist uint // 0 = none, 1 = read, 2 = read+write
|
||||||
|
}
|
||||||
|
|
||||||
|
// GenerateToken creates a new random token and returns the plain text token.
|
||||||
|
// The token hash is stored in the AccessToken struct.
|
||||||
|
// The plain text token should be shown to the user once and never stored.
|
||||||
|
func (t *AccessToken) GenerateToken() (string, error) {
|
||||||
|
bytes := make([]byte, 32)
|
||||||
|
if _, err := rand.Read(bytes); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
plainToken := "og_" + hex.EncodeToString(bytes)
|
||||||
|
|
||||||
|
hash := sha256.Sum256([]byte(plainToken))
|
||||||
|
t.TokenHash = hex.EncodeToString(hash[:])
|
||||||
|
|
||||||
|
return plainToken, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetAccessTokenByID(tokenID uint) (*AccessToken, error) {
|
||||||
|
token := new(AccessToken)
|
||||||
|
err := db.
|
||||||
|
Where("id = ?", tokenID).
|
||||||
|
First(&token).Error
|
||||||
|
return token, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetAccessTokenByToken(plainToken string) (*AccessToken, error) {
|
||||||
|
hash := sha256.Sum256([]byte(plainToken))
|
||||||
|
tokenHash := hex.EncodeToString(hash[:])
|
||||||
|
|
||||||
|
token := new(AccessToken)
|
||||||
|
err := db.
|
||||||
|
Preload("User").
|
||||||
|
Where("token_hash = ?", tokenHash).
|
||||||
|
First(&token).Error
|
||||||
|
return token, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetAccessTokensByUserID(userID uint) ([]*AccessToken, error) {
|
||||||
|
var tokens []*AccessToken
|
||||||
|
err := db.
|
||||||
|
Where("user_id = ?", userID).
|
||||||
|
Order("created_at desc").
|
||||||
|
Find(&tokens).Error
|
||||||
|
return tokens, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *AccessToken) Create() error {
|
||||||
|
t.CreatedAt = time.Now().Unix()
|
||||||
|
return db.Create(t).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *AccessToken) Delete() error {
|
||||||
|
return db.Delete(t).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *AccessToken) UpdateLastUsed() error {
|
||||||
|
return db.Model(t).Update("last_used_at", time.Now().Unix()).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *AccessToken) IsExpired() bool {
|
||||||
|
if t.ExpiresAt == 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return time.Now().Unix() > t.ExpiresAt
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *AccessToken) HasGistReadPermission() bool {
|
||||||
|
return t.ScopeGist >= ReadPermission
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *AccessToken) HasGistWritePermission() bool {
|
||||||
|
return t.ScopeGist >= ReadWritePermission
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- DTO -- //
|
||||||
|
|
||||||
|
type AccessTokenDTO struct {
|
||||||
|
Name string `form:"name" validate:"required,max=255"`
|
||||||
|
ScopeGist uint `form:"scope_gist" validate:"min=0,max=2"`
|
||||||
|
ExpiresAt string `form:"expires_at"` // empty means no expiration, otherwise date format (YYYY-MM-DD)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dto *AccessTokenDTO) ToAccessToken() *AccessToken {
|
||||||
|
var expiresAt int64
|
||||||
|
if dto.ExpiresAt != "" {
|
||||||
|
// date input format: 2006-01-02, expires at end of day
|
||||||
|
if t, err := time.ParseInLocation("2006-01-02", dto.ExpiresAt, time.Local); err == nil {
|
||||||
|
expiresAt = t.Add(24*time.Hour - time.Second).Unix()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &AccessToken{
|
||||||
|
Name: dto.Name,
|
||||||
|
ScopeGist: dto.ScopeGist,
|
||||||
|
ExpiresAt: expiresAt,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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.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
|
||||||
|
|||||||
@@ -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{}, &AccessToken{}); 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{}, &AccessToken{})
|
||||||
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -68,6 +73,7 @@ type Gist struct {
|
|||||||
URL string
|
URL string
|
||||||
Preview string
|
Preview string
|
||||||
PreviewFilename string
|
PreviewFilename string
|
||||||
|
PreviewMimeType string
|
||||||
Description string
|
Description string
|
||||||
Private Visibility // 0: public, 1: unlisted, 2: private
|
Private Visibility // 0: public, 1: unlisted, 2: private
|
||||||
UserID uint
|
UserID uint
|
||||||
@@ -81,6 +87,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 +109,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 +119,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 +128,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 +151,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 +171,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 +221,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 +244,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 +286,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 +314,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
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -335,7 +396,7 @@ func (gist *Gist) GetForks(currentUserId uint, offset int) ([]*Gist, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (gist *Gist) CanWrite(user *User) bool {
|
func (gist *Gist) CanWrite(user *User) bool {
|
||||||
return !(user == nil) && (gist.UserID == user.ID)
|
return user != nil && gist.UserID == user.ID
|
||||||
}
|
}
|
||||||
|
|
||||||
func (gist *Gist) InitRepository() error {
|
func (gist *Gist) InitRepository() error {
|
||||||
@@ -358,12 +419,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), filepath.Ext(fileCat.Name)),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
return files, err
|
return files, err
|
||||||
@@ -384,12 +453,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), filepath.Ext(filename)),
|
||||||
}, err
|
}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -411,8 +488,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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -469,20 +552,31 @@ func (gist *Gist) UpdatePreviewAndCount(withTimestampUpdate bool) error {
|
|||||||
if len(filesStr) == 0 {
|
if len(filesStr) == 0 {
|
||||||
gist.Preview = ""
|
gist.Preview = ""
|
||||||
gist.PreviewFilename = ""
|
gist.PreviewFilename = ""
|
||||||
|
gist.PreviewMimeType = ""
|
||||||
} 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
|
||||||
|
gist.PreviewMimeType = file.MimeType.ContentType
|
||||||
|
|
||||||
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 +629,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 +726,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 +751,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 +759,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 +784,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 +800,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 +824,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 +842,7 @@ func (gist *Gist) AddInIndex() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (gist *Gist) RemoveFromIndex() {
|
func (gist *Gist) RemoveFromIndex() {
|
||||||
if !index.Enabled() {
|
if !index.IndexEnabled() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
34
internal/db/gist_init_queue.go
Normal file
34
internal/db/gist_init_queue.go
Normal 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
|
||||||
|
}
|
||||||
29
internal/db/gist_language.go
Normal file
29
internal/db/gist_language.go
Normal 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
|
||||||
|
}
|
||||||
6
internal/db/gist_topic.go
Normal file
6
internal/db/gist_topic.go
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
package db
|
||||||
|
|
||||||
|
type GistTopic struct {
|
||||||
|
GistID uint `gorm:"primaryKey"`
|
||||||
|
Topic string `gorm:"primaryKey;size:50"`
|
||||||
|
}
|
||||||
@@ -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.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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
123
internal/db/totp.go
Normal 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"`
|
||||||
|
}
|
||||||
78
internal/db/types.go
Normal file
78
internal/db/types.go
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
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.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.Name() {
|
||||||
|
case "mysql", "sqlite":
|
||||||
|
return "JSON"
|
||||||
|
case "postgres":
|
||||||
|
return "JSONB"
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
@@ -1,54 +1,64 @@
|
|||||||
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"`
|
||||||
|
AccessTokens []AccessToken `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 +68,27 @@ 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(&AccessToken{}).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 +229,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 +264,23 @@ 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"`
|
||||||
|
Theme string `form:"theme" json:"theme" validate:"oneof=light dark auto"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dto *UserStyleDTO) ToJson() string {
|
||||||
|
data, err := json.Marshal(dto)
|
||||||
|
if err != nil {
|
||||||
|
return "{}"
|
||||||
|
}
|
||||||
|
return string(data)
|
||||||
|
}
|
||||||
|
|||||||
148
internal/db/webauth_credential.go
Normal file
148
internal/db/webauth_credential.go
Normal file
@@ -0,0 +1,148 @@
|
|||||||
|
package db
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/hex"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/go-webauthn/webauthn/webauthn"
|
||||||
|
)
|
||||||
|
|
||||||
|
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.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.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"`
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user