mirror of
https://github.com/thomiceli/opengist.git
synced 2025-05-18 09:50:41 +02:00
Compare commits
280 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
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 | ||
![]() |
4b039b0703 | ||
![]() |
6d31ef9732 | ||
![]() |
678fb9938c | ||
![]() |
df73b29fb1 | ||
![]() |
690a6d55f9 | ||
![]() |
0ef35fdb36 | ||
![]() |
cf4e0e303c | ||
![]() |
ab4bfcbcfb | ||
![]() |
6499e3cc63 | ||
![]() |
d4e4ae0b43 | ||
![]() |
de6578d9e8 | ||
![]() |
0950c9ce38 | ||
![]() |
f881e1c13c | ||
![]() |
069a999297 | ||
![]() |
a97f54d92f | ||
![]() |
22dbc32f23 | ||
![]() |
9043cbcefe | ||
![]() |
e969f04084 | ||
![]() |
f490f36e56 | ||
![]() |
d40eb65086 | ||
![]() |
7d113e026e | ||
![]() |
38892d8a4a | ||
![]() |
77d87aeecd | ||
![]() |
22052bd38f | ||
![]() |
2fd053a077 | ||
![]() |
97636b23f5 | ||
![]() |
f705e879a1 | ||
![]() |
6836dedda4 | ||
![]() |
88f0f6e4c0 | ||
![]() |
9b0c06d98b | ||
![]() |
0757c4e7fb | ||
![]() |
1ec77590e9 | ||
![]() |
e439d96e43 | ||
![]() |
1aa94292db | ||
![]() |
3551fd745a | ||
![]() |
785d89d6ab | ||
![]() |
6a8759e21e | ||
![]() |
a3a3d367ea | ||
![]() |
e4bbd756f0 | ||
![]() |
2782ced03d | ||
![]() |
45a84df5b4 | ||
![]() |
57273946c3 | ||
![]() |
572e834999 | ||
![]() |
f1541368e5 | ||
![]() |
9936c6bf1e | ||
![]() |
a97d9cdbf4 | ||
![]() |
ef004675a5 | ||
![]() |
3f5f4e01f1 | ||
![]() |
c185cb8933 | ||
![]() |
1c1e3a8919 | ||
![]() |
fc9a75ce8f | ||
![]() |
2bf0e9b7ce | ||
![]() |
e1303c95d0 | ||
![]() |
915287dc10 | ||
![]() |
86590d2990 | ||
![]() |
3179762fd3 | ||
![]() |
86ad88fb09 | ||
![]() |
db6d6a5eba | ||
![]() |
7a75c5ecfa | ||
![]() |
dfe70dc4cf | ||
![]() |
afbecd9a1e | ||
![]() |
7f4be43bb4 | ||
![]() |
05eccfa8e7 | ||
![]() |
a6c4183aac | ||
![]() |
7fc8577ce0 | ||
![]() |
a1524af7a9 | ||
![]() |
10cf7e6e25 | ||
![]() |
7ce94eea59 | ||
![]() |
8eb8f4e231 | ||
![]() |
af19268d6f | ||
![]() |
4215d7e43b | ||
![]() |
d85917bfb2 | ||
![]() |
7c1d6e8bfd | ||
![]() |
3a2fd2374a | ||
![]() |
87a6113cc7 | ||
![]() |
4cb7dc2d30 | ||
![]() |
f52310a841 | ||
![]() |
97707f7cca | ||
![]() |
5058ca8f27 | ||
![]() |
b3a856a05e | ||
![]() |
f557bd45df | ||
![]() |
2f8435892e | ||
![]() |
4bba26daf6 | ||
![]() |
3c97901995 | ||
![]() |
3828022a1c | ||
![]() |
85e2da054b | ||
![]() |
0753c5cb54 | ||
![]() |
845e28dd59 | ||
![]() |
eff88711ea | ||
![]() |
8466e50cc3 | ||
![]() |
c9fd58c904 | ||
![]() |
47869a77c9 | ||
![]() |
246f12c8cb | ||
![]() |
943212e492 | ||
![]() |
7a6fb98223 | ||
![]() |
3444fb9b75 | ||
![]() |
be46304e23 | ||
![]() |
5fa55dfbba | ||
![]() |
09fb647f03 | ||
![]() |
d518a44d32 | ||
![]() |
dcacde0959 | ||
![]() |
064d4d53f6 | ||
![]() |
aec7ee2708 | ||
![]() |
10fd170833 | ||
![]() |
ba03b8df38 | ||
![]() |
ef45f3d0ca | ||
![]() |
b1acea9f1c | ||
![]() |
7059d5c834 | ||
![]() |
1539499294 | ||
![]() |
6f587f4757 | ||
![]() |
632206e172 | ||
![]() |
2eeb9283f0 | ||
![]() |
d137820037 | ||
![]() |
4eedfdcf6f | ||
![]() |
bae18ecb0a | ||
![]() |
2b9eb8e127 | ||
![]() |
05523f6bb1 | ||
![]() |
30ca090e74 | ||
![]() |
fa8e068e24 | ||
![]() |
72275e7573 | ||
![]() |
5b278e2e86 | ||
![]() |
6c450c6f3b | ||
![]() |
dd050bb6a0 | ||
![]() |
c7a6b05c6d | ||
![]() |
35297a287a | ||
![]() |
85b51bf3c9 | ||
![]() |
9dff67f003 | ||
![]() |
a5ea522e45 | ||
![]() |
61e274e56d | ||
![]() |
c20ed60913 | ||
![]() |
b31d95c7f6 | ||
![]() |
689fd21afa | ||
![]() |
be3580f7b1 | ||
![]() |
6085471b81 | ||
![]() |
3943b53163 | ||
![]() |
fe674ac88b | ||
![]() |
9c29e86222 | ||
![]() |
933ba2da0d | ||
![]() |
4d0b75ed0e | ||
![]() |
1dcb900cf3 | ||
![]() |
46dea89b41 | ||
![]() |
977fc9db28 | ||
![]() |
3e83700fc2 | ||
![]() |
0d7305d9ba | ||
![]() |
d4eed91130 | ||
![]() |
ffafde2b3e | ||
![]() |
a7b346d8df | ||
![]() |
25316d7bf2 | ||
![]() |
4f623881ac | ||
![]() |
b5cd49db4c | ||
![]() |
89685bfac6 | ||
![]() |
319a89387a | ||
![]() |
24fc6dd8e4 | ||
![]() |
cc6110bb4e | ||
![]() |
2890c60124 | ||
![]() |
5bb5886770 | ||
![]() |
038d81df2d | ||
![]() |
7515e82d34 | ||
![]() |
add0299442 | ||
![]() |
06b752f567 | ||
![]() |
a35b64455a | ||
![]() |
9470622125 | ||
![]() |
7b5d035a32 | ||
![]() |
98c5cd1794 | ||
![]() |
0936cbc455 | ||
![]() |
fc421a68b5 | ||
![]() |
62711ff491 | ||
![]() |
da19e486f2 | ||
![]() |
98c85de3d6 | ||
![]() |
af3aab21e3 | ||
![]() |
fb407bcbce | ||
![]() |
3366cde385 | ||
![]() |
b2a56fe5a0 | ||
![]() |
24e3de8fc1 | ||
![]() |
c517c2d9c9 | ||
![]() |
8880e00f48 | ||
![]() |
da970d7272 | ||
![]() |
62f91c5ed2 | ||
![]() |
0a542431c9 | ||
![]() |
cecc06b332 | ||
![]() |
4a75a50370 | ||
![]() |
7cc77d80dc | ||
![]() |
df22506bdd | ||
![]() |
026bb7304c | ||
![]() |
0cae152e03 | ||
![]() |
5fe84164a5 | ||
![]() |
089d321898 | ||
![]() |
1f74affde4 | ||
![]() |
49807d04c7 | ||
![]() |
67a06cea0a | ||
![]() |
d866a6f2f2 | ||
![]() |
3cf5bc8b76 |
2
.gitattributes
vendored
2
.gitattributes
vendored
@ -1,2 +1,4 @@
|
||||
templates/**/* linguist-vendored
|
||||
public/**/*.css linguist-vendored
|
||||
public/**/*.scss linguist-vendored
|
||||
*.config.js linguist-vendored
|
||||
|
1
.github/FUNDING.yml
vendored
Normal file
1
.github/FUNDING.yml
vendored
Normal file
@ -0,0 +1 @@
|
||||
github: thomiceli
|
47
.github/workflows/docs.yml
vendored
Normal file
47
.github/workflows/docs.yml
vendored
Normal file
@ -0,0 +1,47 @@
|
||||
name: Build / Deploy docs
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build-and-deploy:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
|
||||
- name: Install JS dependencies
|
||||
run: |
|
||||
npm install vitepress@1.3.4 tailwindcss@3.4.10
|
||||
|
||||
- name: Build docs
|
||||
run: |
|
||||
cd docs
|
||||
npx tailwindcss -i .vitepress/theme/style.css -o .vitepress/theme/theme.css -c .vitepress/tailwind.config.js
|
||||
npm run docs:build
|
||||
|
||||
- name: Deploy to server
|
||||
uses: appleboy/scp-action@master
|
||||
with:
|
||||
host: ${{ secrets.SERVER_HOST }}
|
||||
username: ${{ secrets.SERVER_USERNAME }}
|
||||
key: ${{ secrets.SERVER_SSH_KEY }}
|
||||
source: "docs/.vitepress/dist/*"
|
||||
target: ${{ secrets.SERVER_PATH }}
|
||||
|
||||
- name: Update remote docs
|
||||
uses: appleboy/ssh-action@master
|
||||
with:
|
||||
host: ${{ secrets.SERVER_HOST }}
|
||||
username: ${{ secrets.SERVER_USERNAME }}
|
||||
key: ${{ secrets.SERVER_SSH_KEY }}
|
||||
script: |
|
||||
${{ secrets.UPDATE_DOCS }}
|
143
.github/workflows/go.yml
vendored
143
.github/workflows/go.yml
vendored
@ -1,49 +1,138 @@
|
||||
name: "Go"
|
||||
name: "Go CI"
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
- 'dev-*'
|
||||
workflow_dispatch:
|
||||
|
||||
pull_request:
|
||||
paths-ignore:
|
||||
- '**.yml'
|
||||
- '**.md'
|
||||
|
||||
jobs:
|
||||
ci:
|
||||
lint:
|
||||
name: Lint
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Set up Go 1.23
|
||||
uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: "1.23"
|
||||
|
||||
- name: Lint
|
||||
uses: golangci/golangci-lint-action@v6
|
||||
with:
|
||||
version: v1.60
|
||||
args: --out-format=colored-line-number --timeout=20m
|
||||
|
||||
- name: Format
|
||||
run: make fmt check_changes
|
||||
|
||||
check:
|
||||
name: Check
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Set up Go 1.23
|
||||
uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: "1.23"
|
||||
|
||||
- name: Check Go modules
|
||||
run: make go_mod check_changes
|
||||
|
||||
- name: Check translations
|
||||
run: make check-tr
|
||||
|
||||
test-db:
|
||||
name: Test
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: ["ubuntu-latest", "macOS-latest"]
|
||||
go: ["1.19", "1.20"]
|
||||
os: ["ubuntu-latest"]
|
||||
go: ["1.23"]
|
||||
database: [postgres, mysql]
|
||||
include:
|
||||
- database: postgres
|
||||
image: postgres:16
|
||||
port: 5432:5432
|
||||
- database: mysql
|
||||
image: mysql:8
|
||||
port: 3306:3306
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
services:
|
||||
database:
|
||||
image: ${{ matrix.image }}
|
||||
ports:
|
||||
- ${{ matrix.port }}
|
||||
env:
|
||||
POSTGRES_PASSWORD: opengist
|
||||
POSTGRES_DB: opengist_test
|
||||
MYSQL_ROOT_PASSWORD: opengist
|
||||
MYSQL_DATABASE: opengist_test
|
||||
options: >-
|
||||
--health-cmd ${{ matrix.database == 'postgres' && 'pg_isready' || '"mysqladmin ping"' }}
|
||||
--health-interval 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 5
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Set up Go ${{ matrix.go }}
|
||||
uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: ${{ matrix.go }}
|
||||
|
||||
- name: Run tests
|
||||
run: make test TEST_DB_TYPE=${{ matrix.database }}
|
||||
|
||||
test:
|
||||
name: Test
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: ["ubuntu-latest", "macOS-latest", "windows-latest"]
|
||||
go: ["1.23"]
|
||||
database: ["sqlite"]
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Set up Go ${{ matrix.go }}
|
||||
uses: WillAbides/setup-go-faster@v1.8.0
|
||||
uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: ${{ matrix.go }}
|
||||
|
||||
- name: Cache Go modules
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: ~/go/pkg/mod
|
||||
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-go-
|
||||
- name: Run tests
|
||||
run: make test TEST_DB_TYPE=${{ matrix.database }}
|
||||
|
||||
- name: Cache Go build cache
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: ~/.cache/go-build
|
||||
key: ${{ runner.os }}-go-build-${{ matrix.go }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-go-build-
|
||||
build:
|
||||
name: Build
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: ["ubuntu-latest", "macOS-latest", "windows-latest"]
|
||||
go: ["1.23"]
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Run go vet
|
||||
run: "go vet ./..."
|
||||
|
||||
- name: Run Staticcheck
|
||||
uses: dominikh/staticcheck-action@v1.3.0
|
||||
- name: Set up Go 1.23
|
||||
uses: actions/setup-go@v4
|
||||
with:
|
||||
version: "2023.1.1"
|
||||
install-go: false
|
||||
cache-key: ${{ matrix.go }}
|
||||
go-version: ${{ matrix.go }}
|
||||
|
||||
- name: Build
|
||||
shell: bash
|
||||
run: make
|
||||
|
53
.github/workflows/helm.yml
vendored
Normal file
53
.github/workflows/helm.yml
vendored
Normal file
@ -0,0 +1,53 @@
|
||||
name: Build / Deploy Helm Chart
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build-and-deploy:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Helm
|
||||
uses: azure/setup-helm@v4.3.0
|
||||
with:
|
||||
version: 'latest'
|
||||
|
||||
- name: Update Helm chart dependencies
|
||||
run: |
|
||||
cd ./helm/opengist
|
||||
helm dependency update
|
||||
|
||||
- name: Package Helm chart
|
||||
run: |
|
||||
cd ./helm
|
||||
helm package ./opengist
|
||||
|
||||
# First time, create the index
|
||||
wget -q https://helm.opengist.io/index.yaml
|
||||
if [ ! -f index.yaml ]; then
|
||||
helm repo index --url https://helm.opengist.io .
|
||||
else
|
||||
# For subsequent runs, merge with existing index
|
||||
helm repo index --url https://helm.opengist.io --merge index.yaml .
|
||||
fi
|
||||
|
||||
- name: Deploy to server
|
||||
uses: appleboy/scp-action@master
|
||||
with:
|
||||
host: ${{ secrets.SERVER_HOST }}
|
||||
username: ${{ secrets.SERVER_USERNAME }}
|
||||
key: ${{ secrets.SERVER_SSH_KEY }}
|
||||
source: "./helm/*.tgz,./helm/index.yaml"
|
||||
target: ${{ secrets.HELM_SERVER_PATH }}
|
||||
|
||||
- name: Update remote helm repository
|
||||
uses: appleboy/ssh-action@master
|
||||
with:
|
||||
host: ${{ secrets.SERVER_HOST }}
|
||||
username: ${{ secrets.SERVER_USERNAME }}
|
||||
key: ${{ secrets.SERVER_SSH_KEY }}
|
||||
script: |
|
||||
${{ secrets.UPDATE_HELM_REPO }}
|
@ -1,12 +1,37 @@
|
||||
name: Docker
|
||||
name: Release
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
docker:
|
||||
binaries-build-release:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Set up Go 1.23
|
||||
uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: "1.23"
|
||||
|
||||
- name: Cross compile build
|
||||
run: make all_crosscompile
|
||||
|
||||
- name: Upload Release Assets
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
files: |
|
||||
build/*.tar.gz
|
||||
build/*.zip
|
||||
build/checksums.txt
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
docker-build-release:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
@ -21,6 +46,7 @@ jobs:
|
||||
with:
|
||||
images: |
|
||||
ghcr.io/thomiceli/opengist
|
||||
docker.io/thomiceli/opengist
|
||||
tags: |
|
||||
type=raw,value=latest,enable=${{ github.ref == format('refs/heads/{0}', github.event.repository.default_branch) }}
|
||||
type=semver,pattern={{major}}
|
||||
@ -40,6 +66,12 @@ jobs:
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v4
|
||||
with:
|
7
.gitignore
vendored
7
.gitignore
vendored
@ -1,8 +1,15 @@
|
||||
node_modules/
|
||||
gist.db
|
||||
.idea/
|
||||
.vscode/
|
||||
.DS_Store
|
||||
/**/.DS_Store
|
||||
public/assets/*
|
||||
public/manifest.json
|
||||
./opengist
|
||||
opengist
|
||||
build/
|
||||
docs/.vitepress/dist/
|
||||
docs/.vitepress/cache/
|
||||
helm/opengist/charts/
|
||||
vendor/
|
||||
|
426
CHANGELOG.md
426
CHANGELOG.md
@ -1,5 +1,431 @@
|
||||
# Changelog
|
||||
|
||||
## [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
|
||||
See here how to [update](/docs/update.md) Opengist.
|
||||
|
||||
### Added
|
||||
- New website for documentation using Vitepress [https://opengist.io](https://opengist.io) (#326)
|
||||
- Ukrainian localization (#325)
|
||||
- Dummy /metrics endpoint (#327)
|
||||
|
||||
## [1.7.4](https://github.com/thomiceli/opengist/compare/v1.7.3...v1.7.4) - 2024-09-09
|
||||
See here how to [update](/docs/update.md) Opengist.
|
||||
|
||||
### Added
|
||||
- More translations strings (#294) (#304)
|
||||
- Hide change password form when login via password disabled (#314)
|
||||
- File delete button on create editor (#320)
|
||||
- Assets cache header
|
||||
- Hide secret values in admin config page
|
||||
- Atomic pointer for indexer (#321)
|
||||
|
||||
### Fixed
|
||||
- Fatal error using `cases.Title()` (#313)
|
||||
- Search unlisted gist (#319)
|
||||
|
||||
### Other
|
||||
- Removed logger `trace` and `fatal` levels (#322)
|
||||
|
||||
## [1.7.3](https://github.com/thomiceli/opengist/compare/v1.7.2...v1.7.3) - 2024-06-03
|
||||
See here how to [update](/docs/update.md) Opengist.
|
||||
|
||||
### Added
|
||||
- Setting to allow anonymous access to individual gists while still RequireLogin everywhere else (#229)
|
||||
- Make edit visibility a toggle (#277)
|
||||
- More translation strings (#274) (#281)
|
||||
- String method to visibility (#276)
|
||||
|
||||
### Fixed
|
||||
- Perms for http/ssh clone (#288)
|
||||
- Fix translation string (#293)
|
||||
|
||||
### Other
|
||||
- Update deps Golang & JS deps
|
||||
- Check translations keys in CI (#279)
|
||||
- Fix CI check for additional translations only (#289)
|
||||
|
||||
## [1.7.2](https://github.com/thomiceli/opengist/compare/v1.7.1...v1.7.2) - 2024-05-05
|
||||
See here how to [update](/docs/update.md) Opengist.
|
||||
|
||||
### Added
|
||||
- Docs:
|
||||
- Run with systemd as a normal user (#254)
|
||||
- Kubernetes deployment (#258)
|
||||
- More translation strings (#269) (#271)
|
||||
|
||||
### Changed
|
||||
- Rework git log parsing and truncating (#260)
|
||||
- Set Opengist version from git tags (#261)
|
||||
|
||||
### Fixed
|
||||
- Missing preview button when editing .md gist (#259)
|
||||
- Frontend (#267)
|
||||
- Fix mermaid display
|
||||
- Move Login/Register buttons on mobile
|
||||
- Set minimum width on avatar
|
||||
|
||||
### Other
|
||||
- Use go 1.22 and update deps (#244)
|
||||
|
||||
## [1.7.1](https://github.com/thomiceli/opengist/compare/v1.7.0...v1.7.1) - 2024-04-05
|
||||
See here how to [update](/docs/update.md) Opengist.
|
||||
|
||||
### Added
|
||||
- Docs: More detailed variant for custom pages (#248)
|
||||
|
||||
### Fixed
|
||||
- Auth page GitlabName Error (#242)
|
||||
- Empty invitation on user creation (#247)
|
||||
|
||||
## [1.7.0](https://github.com/thomiceli/opengist/compare/v1.6.1...v1.7.0) - 2024-04-03
|
||||
See here how to [update](/docs/update.md) Opengist.
|
||||
|
||||
Note: all sessions will be invalidated after this update.
|
||||
|
||||
### Added
|
||||
- Custom logo configuration (#209)
|
||||
- Custom static links (#234)
|
||||
- Invitations for closed registrations (#233)
|
||||
- Set gist visibility via Git push options (#215)
|
||||
- Set gist URL and title via push options (#216)
|
||||
- Specify custom names in the OAuth login buttons (#214)
|
||||
- Markdown preview (#224)
|
||||
- Reset a user password using CLI (#226)
|
||||
- Translations (#207, #210)
|
||||
|
||||
### Changed
|
||||
- Use filesystem session store (#240)
|
||||
- Move Git hook logic to Opengist (#213)
|
||||
- Increase login for 1 year (#222)
|
||||
|
||||
### Fixed
|
||||
- Show theme change button on responsive devices (#225)
|
||||
- New line literal in embed gists (#237)
|
||||
|
||||
### Other
|
||||
- GitHub security updates
|
||||
- New docker dev env (#220)
|
||||
|
||||
## [1.6.1](https://github.com/thomiceli/opengist/compare/v1.6.0...v1.6.1) - 2024-01-06
|
||||
See here how to [update](/docs/update.md) Opengist.
|
||||
|
||||
### Added
|
||||
- Healthcheck on Docker container (#204)
|
||||
- Translations:
|
||||
- fr-FR (#201)
|
||||
|
||||
### Fixed
|
||||
- Directory renaming on username change (#205)
|
||||
|
||||
## [1.6.0](https://github.com/thomiceli/opengist/compare/v1.5.3...v1.6.0) - 2024-01-04
|
||||
See here how to [update](/docs/update.md) Opengist.
|
||||
|
||||
### Added
|
||||
- Embedded gists (#179)
|
||||
- Gist code search (#194)
|
||||
- Custom URLS for gists (#183)
|
||||
- Gist JSON data/metadata (#179)
|
||||
- Keep default visibility when creating a gist on the UI (#155)
|
||||
- Health check endpoint (#170)
|
||||
- GitLab OAuth2 login (#174)
|
||||
- Syntax highlighting for more file types (#176)
|
||||
- Checkable Markdown checkboxes (#182)
|
||||
- Config:
|
||||
- Log output (#172)
|
||||
- Default git branch name (#171)
|
||||
- Change username setting (#190)
|
||||
- Admin actions:
|
||||
- Synchronize all gists previews (#191)
|
||||
- Reset Git server hooks for all repositories (#191)
|
||||
- Index all gists (#194)
|
||||
- Translations:
|
||||
- cs-CZ (#164)
|
||||
- zh-TW (#166, #195)
|
||||
- hu-HU (#185)
|
||||
- pt-BR (#193)
|
||||
- Docs (#198)
|
||||
|
||||
### Changed
|
||||
- Updated dependencies (#197):
|
||||
- Go `1.20` -> `1.21`
|
||||
- JavaScript packages
|
||||
- NodeJS Docker image `18` -> `20`
|
||||
- Alpine Docker image `3.17` -> `3.19`
|
||||
|
||||
### Fixed
|
||||
- Fix reverse proxy subpath support (#192)
|
||||
- Fix undecoded gist content when going back to editing in the UI (#184)
|
||||
- Fix outputting non-truncated large files for editon/zip download (#184)
|
||||
- Allow dashes in usernames (#184)
|
||||
- Delete SSH keys associated to deleted user (#184)
|
||||
- Better error message when there is no files in gist (#184)
|
||||
- Show if there is no files in gist preview (#184)
|
||||
- Log parsing for the 11th empty commit (#184)
|
||||
- Optimize reading gist files content (#186)
|
||||
|
||||
## [1.5.3](https://github.com/thomiceli/opengist/compare/v1.5.2...v1.5.3) - 2023-11-20
|
||||
### Added
|
||||
- es-ES translation (#139)
|
||||
- Create/change account password (#156)
|
||||
- Display OAuth error messages when HTTP 400 (#159)
|
||||
|
||||
### Fixed
|
||||
- Git bare repository branch name creation (#157)
|
||||
- Git file truncated output hanging (#157)
|
||||
- Home user directory detection handling (#145)
|
||||
- UI changes (#158)
|
||||
|
||||
## [1.5.2](https://github.com/thomiceli/opengist/compare/v1.5.1...v1.5.2) - 2023-10-16
|
||||
### Added
|
||||
- zh-CN translation (#130)
|
||||
- ru-RU translation (#135)
|
||||
- config.yml usage in the Docker container (#131)
|
||||
- Longer title and description (#129)
|
||||
|
||||
### Fixed
|
||||
- Private gist visibility (#128)
|
||||
- Dark background color in Markdown rendering (#137)
|
||||
- Error handling for password hashes (#132)
|
||||
|
||||
## [1.5.1](https://github.com/thomiceli/opengist/compare/v1.5.0...v1.5.1) - 2023-09-29
|
||||
### Added
|
||||
- Hungarian translations (#123)
|
||||
|
||||
### Fixed
|
||||
- .c and .h syntax highlighting (#119)
|
||||
- Login page disabled depending on locale (#120)
|
||||
- Syntax error on templates when calling locale function (#122)
|
||||
|
||||
## [1.5.0](https://github.com/thomiceli/opengist/compare/v1.4.2...v1.5.0) - 2023-09-26
|
||||
### Added
|
||||
- Private Gist visibility (#87)
|
||||
- Create gists from a special Git HTTP server remote URL (#95)
|
||||
- OIDC provider integration (#98)
|
||||
- Translation system (#104)
|
||||
- Run `git gc` on all repositories as admin (#90)
|
||||
- Unit and integration tests (#97)
|
||||
- Documentation (#110, #111)
|
||||
- New logo (#103)
|
||||
|
||||
### Changed
|
||||
- Use Non-CGO SQLite instead of CGO SQLite (#100)
|
||||
- Various UI changes (#84, #93)
|
||||
- Improved CI/CD pipeline (#99, #113)
|
||||
- Improved git http semantics and repo obfuscation (#94)
|
||||
- Updated Go deps (#102)
|
||||
|
||||
### Fixed
|
||||
- Find command for Windows users (#89)
|
||||
- Retain visibility when editing a gist (#83)
|
||||
- Typo on admin index page (#85)
|
||||
- ViteJS dev server (#91)
|
||||
- Bugs (#105)
|
||||
|
||||
### Breaking changes
|
||||
- Removed CONFIG env var
|
||||
- Removed TLS server (#101)
|
||||
|
||||
## [1.4.2](https://github.com/thomiceli/opengist/compare/v1.4.1...v1.4.2) - 2023-07-17
|
||||
### Added
|
||||
- External url to HTML links & redirects (#75)
|
||||
- Make unlisted gists not SEO crawlable (#78)
|
||||
- Warning message on OAuth unlink (#79)
|
||||
|
||||
### Changed
|
||||
- Redirect to `/all` when not logged in (#76)
|
||||
- Removed Dev Docker image (#80)
|
||||
|
||||
## [1.4.1](https://github.com/thomiceli/opengist/compare/v1.4.0...v1.4.1) - 2023-06-25
|
||||
### ⚠️ Docker users ⚠️
|
||||
Opengist Docker volume has been changed from `/root/.opengist` to `/opengist`, do not forget to update your
|
||||
`docker-compose.yml` file or any other Docker related configuration.
|
||||
|
||||
Please make a backup of your Opengist data directory before updating.
|
||||
|
||||
### Fixed
|
||||
- Git message remote: `warning: unable to access '/root/.config/git/attributes': Permission denied` (#71)
|
||||
|
||||
## [1.4.0](https://github.com/thomiceli/opengist/compare/v1.3.0...v1.4.0) - 2023-06-23
|
||||
### ⚠️ Docker users ⚠️
|
||||
Opengist Docker volume has been changed from `/root/.opengist` to `/opengist`, do not forget to update your
|
||||
`docker-compose.yml` file or any other Docker related configuration.
|
||||
|
||||
Please make a backup of your Opengist data directory before updating.
|
||||
|
||||
### Added
|
||||
- Search gists, browse users snippets, likes and forks (#68)
|
||||
- SQLite WAL journal mode by default (#54)
|
||||
- Change SQLite journal mode via configuration (#54)
|
||||
- Configuration via environment variables (#50)
|
||||
- Docker dev image (#56)
|
||||
- Choose Docker container/volumes owner via UID/GID (#63)
|
||||
|
||||
### Changed
|
||||
- Docker volume changed from `/root/.opengist` to `/opengist` (#63)
|
||||
- `DEV` environment variable renamed to `OG_DEV` (#64)
|
||||
- Use `npx` in Makefile instead of `./node_modules/.bin` (#66)
|
||||
- DEPRECATED: `OG_CONFIG` environment variable (#64)
|
||||
|
||||
### Fixed
|
||||
- Gitea URL joins (#43, #61)
|
||||
- Dark mode flickering (#44)
|
||||
- Typos (#42)
|
||||
|
||||
## [1.3.0](https://github.com/thomiceli/opengist/compare/v1.2.0...v1.3.0) - 2023-05-27
|
||||
### Added
|
||||
- Disable login form via admin panel
|
||||
- Syntax highlighting in Markdown code block (#29)
|
||||
- Better UI for admin settings (#30)
|
||||
- Disable Gravatar (#37)
|
||||
- Swap between dark and light theme (#38)
|
||||
|
||||
### Changed
|
||||
- Logs are now also appended to stdout
|
||||
- Golang module name is now `github.com/thomiceli/opengist`
|
||||
|
||||
### Fixed
|
||||
- First account registering with OAuth is now admin
|
||||
- Fix HTML entities escaping in Markdown code block (#29)
|
||||
|
||||
## [1.2.0](https://github.com/thomiceli/opengist/compare/v1.1.1...v1.2.0) - 2023-05-01
|
||||
### Added
|
||||
- Restrict or unrestrict snippets visibility to anonymous users (#19)
|
||||
|
80
Dockerfile
80
Dockerfile
@ -1,30 +1,9 @@
|
||||
FROM alpine:3.17 AS build
|
||||
FROM alpine:3.19 AS base
|
||||
|
||||
RUN apk update && \
|
||||
apk add --no-cache \
|
||||
make \
|
||||
gcc \
|
||||
musl-dev \
|
||||
libstdc++
|
||||
|
||||
COPY --from=golang:1.19-alpine /usr/local/go/ /usr/local/go/
|
||||
ENV PATH="/usr/local/go/bin:${PATH}"
|
||||
|
||||
COPY --from=node:18-alpine /usr/local/ /usr/local/
|
||||
ENV NODE_PATH="/usr/local/lib/node_modules"
|
||||
ENV PATH="/usr/local/bin:${PATH}"
|
||||
|
||||
WORKDIR /opengist
|
||||
|
||||
COPY . .
|
||||
|
||||
RUN make
|
||||
|
||||
|
||||
FROM alpine:3.17
|
||||
|
||||
RUN apk update && \
|
||||
apk add --no-cache \
|
||||
shadow \
|
||||
openssl \
|
||||
openssh \
|
||||
curl \
|
||||
@ -36,10 +15,59 @@ RUN apk update && \
|
||||
musl-dev \
|
||||
libstdc++
|
||||
|
||||
COPY --from=golang:1.23-alpine /usr/local/go/ /usr/local/go/
|
||||
ENV PATH="/usr/local/go/bin:${PATH}"
|
||||
ENV CGO_ENABLED=0
|
||||
|
||||
COPY --from=node:20-alpine /usr/local/ /usr/local/
|
||||
ENV NODE_PATH="/usr/local/lib/node_modules"
|
||||
ENV PATH="/usr/local/bin:${PATH}"
|
||||
|
||||
WORKDIR /opengist
|
||||
|
||||
COPY --from=build /opengist/opengist .
|
||||
COPY . .
|
||||
|
||||
|
||||
FROM base AS dev
|
||||
|
||||
EXPOSE 6157 2222 16157
|
||||
VOLUME /opengist
|
||||
|
||||
CMD ["make", "watch"]
|
||||
|
||||
|
||||
FROM base AS build
|
||||
|
||||
RUN make
|
||||
|
||||
|
||||
FROM alpine:3.19 as prod
|
||||
|
||||
RUN apk update && \
|
||||
apk add --no-cache \
|
||||
shadow \
|
||||
openssl \
|
||||
openssh \
|
||||
curl \
|
||||
wget \
|
||||
git \
|
||||
gnupg \
|
||||
xz \
|
||||
gcc \
|
||||
musl-dev \
|
||||
libstdc++
|
||||
|
||||
RUN addgroup -S 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
|
||||
|
||||
COPY --from=build --chown=opengist:opengist /opengist/opengist .
|
||||
COPY --from=build --chown=opengist:opengist /opengist/docker ./docker
|
||||
|
||||
EXPOSE 6157 2222
|
||||
VOLUME /root/.opengist
|
||||
CMD ["./opengist"]
|
||||
VOLUME /opengist
|
||||
HEALTHCHECK --interval=60s --timeout=30s --start-period=15s --retries=3 CMD curl -f http://localhost:6157/healthcheck || exit 1
|
||||
ENTRYPOINT ["./docker/entrypoint.sh"]
|
||||
|
49
Makefile
49
Makefile
@ -1,9 +1,14 @@
|
||||
.PHONY: all install build_frontend build_backend build build_docker watch_frontend watch_backend watch clean clean_docker
|
||||
.PHONY: all all_crosscompile install build_frontend build_backend build build_crosscompile build_docker build_dev_docker run_dev_docker watch_frontend watch_backend watch clean clean_docker check_changes go_mod fmt test check-tr
|
||||
|
||||
# Specify the name of your Go binary output
|
||||
BINARY_NAME := opengist
|
||||
GIT_TAG := $(shell git describe --tags)
|
||||
VERSION_PKG := github.com/thomiceli/opengist/internal/config.OpengistVersion
|
||||
TEST_DB_TYPE ?= sqlite
|
||||
|
||||
all: install build
|
||||
all: clean install build
|
||||
|
||||
all_crosscompile: clean install build_frontend build_crosscompile
|
||||
|
||||
install:
|
||||
@echo "Installing NPM dependencies..."
|
||||
@ -13,34 +18,62 @@ install:
|
||||
|
||||
build_frontend:
|
||||
@echo "Building frontend assets..."
|
||||
./node_modules/.bin/vite 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:
|
||||
@echo "Building Opengist binary..."
|
||||
go build -tags fs_embed -o $(BINARY_NAME) .
|
||||
go build -tags fs_embed -ldflags "-X $(VERSION_PKG)=$(GIT_TAG)" -o $(BINARY_NAME) .
|
||||
|
||||
build: build_frontend build_backend
|
||||
|
||||
build_crosscompile:
|
||||
@bash ./scripts/build-all.sh
|
||||
|
||||
build_docker:
|
||||
@echo "Building Docker image..."
|
||||
docker build -t $(BINARY_NAME):latest .
|
||||
|
||||
build_dev_docker:
|
||||
@echo "Building Docker image..."
|
||||
docker build -t $(BINARY_NAME)-dev:latest --target dev .
|
||||
|
||||
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
|
||||
|
||||
watch_frontend:
|
||||
@echo "Building frontend assets..."
|
||||
./node_modules/.bin/vite dev --port 16157
|
||||
npx vite -c public/vite.config.js dev --port 16157 --host
|
||||
|
||||
watch_backend:
|
||||
@echo "Building Opengist binary..."
|
||||
DEV=1 ./node_modules/.bin/nodemon --watch '**/*' -e html,yml,go,js --signal SIGTERM --exec 'go run . --config config.yml'
|
||||
OG_DEV=1 npx nodemon --watch '**/*' -e html,yml,go,js --signal SIGTERM --exec 'go run -ldflags "-X $(VERSION_PKG)=$(GIT_TAG)" . --config config.yml'
|
||||
|
||||
watch:
|
||||
@bash ./watch.sh
|
||||
@sh ./scripts/watch.sh
|
||||
|
||||
clean:
|
||||
@echo "Cleaning up build artifacts..."
|
||||
@rm -f $(BINARY_NAME) public/manifest.json
|
||||
@rm -rf public/assets
|
||||
@rm -rf public/assets build
|
||||
|
||||
clean_docker:
|
||||
@echo "Cleaning up Docker image..."
|
||||
@docker rmi $(BINARY_NAME)
|
||||
|
||||
check_changes:
|
||||
@echo "Checking for changes..."
|
||||
@git --no-pager diff --exit-code || (echo "There are unstaged changes detected." && exit 1)
|
||||
|
||||
go_mod:
|
||||
@go mod download
|
||||
@go mod tidy
|
||||
|
||||
fmt:
|
||||
@go fmt ./...
|
||||
|
||||
test:
|
||||
@OPENGIST_TEST_DB=$(TEST_DB_TYPE) go test ./... -p 1
|
||||
|
||||
check-tr:
|
||||
@bash ./scripts/check-translations.sh
|
216
README.md
216
README.md
@ -1,59 +1,44 @@
|
||||
# Opengist
|
||||
|
||||
<img height="108px" src="https://raw.githubusercontent.com/thomiceli/opengist/master/public/opengist.svg" alt="Opengist" align="right" />
|
||||
|
||||
Opengist is a **self-hosted** Pastebin **powered by Git**. All snippets are stored in a Git repository and can be
|
||||
read and/or modified using standard Git commands, or with the web interface.
|
||||
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)
|
||||
|
||||
|
||||

|
||||

|
||||

|
||||
|
||||
A self-hosted pastebin **powered by Git**. [Try it here](https://opengist.thomice.li).
|
||||
|
||||
* [Features](#features)
|
||||
* [Install](#install)
|
||||
* [With Docker](#with-docker)
|
||||
* [From source](#from-source)
|
||||
* [Configuration](#configuration)
|
||||
* [Administration](#administration)
|
||||
* [Use Nginx as a reverse proxy](#use-nginx-as-a-reverse-proxy)
|
||||
* [Use Fail2ban](#use-fail2ban)
|
||||
* [Configure OAuth](#configure-oauth)
|
||||
* [License](#license)
|
||||
[](https://github.com/thomiceli/opengist/actions/workflows/go.yml)
|
||||
[](https://goreportcard.com/report/github.com/thomiceli/opengist)
|
||||
[](https://tr.opengist.io/projects/_/opengist/)
|
||||
|
||||
## Features
|
||||
|
||||
* Create public or unlisted snippets
|
||||
* Clone / Pull / Push snippets **via Git** over HTTP or SSH
|
||||
* Revisions history
|
||||
* Create public, unlisted or private snippets
|
||||
* [Init](/docs/usage/init-via-git.md) / Clone / Pull / Push snippets **via Git** over HTTP or SSH
|
||||
* Syntax highlighting ; markdown & CSV support
|
||||
* Search code in snippets; browse users snippets, likes and forks
|
||||
* Add topics to snippets
|
||||
* Embed snippets in other websites
|
||||
* Revisions history
|
||||
* Like / Fork snippets
|
||||
* Search for all snippets or for certain users snippets
|
||||
* Editor with indentation mode & size ; drag and drop files
|
||||
* Download raw files or as a ZIP archive
|
||||
* OAuth2 login with GitHub and Gitea
|
||||
* Avatars
|
||||
* Responsive UI
|
||||
* Enable or disable signups
|
||||
* OAuth2 login with GitHub, GitLab, Gitea, and OpenID Connect
|
||||
* Restrict or unrestrict snippets visibility to anonymous users
|
||||
* Admin panel : delete users/gists; clean database/filesystem by syncing gists
|
||||
* SQLite database
|
||||
* Logging
|
||||
* Docker support
|
||||
* Docker support / Helm Chart
|
||||
* [More...](/docs/introduction.md#features)
|
||||
|
||||
#### Todo
|
||||
|
||||
- [ ] Light mode
|
||||
- [ ] Tests
|
||||
- [ ] Search for snippets
|
||||
- [ ] Embed snippets
|
||||
- [ ] Filesystem/Redis support for user sessions
|
||||
- [ ] Have a cool logo
|
||||
|
||||
## Install
|
||||
## Quick start
|
||||
|
||||
### With Docker
|
||||
|
||||
A Docker [image](https://github.com/users/thomiceli/packages/container/package/opengist), available for each release, can be pulled
|
||||
Docker [images](https://github.com/thomiceli/opengist/pkgs/container/opengist) are available for each release :
|
||||
|
||||
```
|
||||
docker pull ghcr.io/thomiceli/opengist:1
|
||||
```shell
|
||||
docker pull ghcr.io/thomiceli/opengist:1.10
|
||||
```
|
||||
|
||||
It can be used in a `docker-compose.yml` file :
|
||||
@ -63,26 +48,48 @@ It can be used in a `docker-compose.yml` file :
|
||||
3. Opengist is now running on port 6157, you can browse http://localhost:6157
|
||||
|
||||
```yml
|
||||
version: "3"
|
||||
|
||||
services:
|
||||
opengist:
|
||||
image: ghcr.io/thomiceli/opengist:1
|
||||
image: ghcr.io/thomiceli/opengist:1.10
|
||||
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:/root/.opengist"
|
||||
environment:
|
||||
CONFIG: |
|
||||
log-level: info
|
||||
- "$HOME/.opengist:/opengist"
|
||||
```
|
||||
|
||||
You can define which user/group should run the container and own the files by setting the `UID` and `GID` environment variables :
|
||||
|
||||
```yml
|
||||
services:
|
||||
opengist:
|
||||
# ...
|
||||
environment:
|
||||
UID: 1001
|
||||
GID: 1001
|
||||
```
|
||||
|
||||
### Via binary
|
||||
|
||||
Download the archive for your system from the release page [here](https://github.com/thomiceli/opengist/releases/latest), and extract it.
|
||||
|
||||
```shell
|
||||
# example for linux amd64
|
||||
wget https://github.com/thomiceli/opengist/releases/download/v1.10.0/opengist1.10.0-linux-amd64.tar.gz
|
||||
|
||||
tar xzvf opengist1.10.0-linux-amd64.tar.gz
|
||||
cd opengist
|
||||
chmod +x opengist
|
||||
./opengist # with or without `--config config.yml`
|
||||
```
|
||||
|
||||
Opengist is now running on port 6157, you can browse http://localhost:6157
|
||||
|
||||
### From source
|
||||
|
||||
Requirements : [Git](https://git-scm.com/downloads) (2.20+), [Go](https://go.dev/doc/install) (1.19+), [Node.js](https://nodejs.org/en/download/) (16+)
|
||||
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
|
||||
git clone https://github.com/thomiceli/opengist
|
||||
@ -93,118 +100,15 @@ make
|
||||
|
||||
Opengist is now running on port 6157, you can browse http://localhost:6157
|
||||
|
||||
## Configuration
|
||||
---
|
||||
|
||||
Opengist can be configured using YAML. The full configuration file is [config.yml](config.yml), each default key/value
|
||||
pair can be overridden.
|
||||
To create and run a development environment, see [run-development.md](/docs/contributing/development.md).
|
||||
|
||||
### With docker
|
||||
## Documentation
|
||||
|
||||
Add a `CONFIG` environment variable in the `docker-compose.yml` file to the `opengist` service :
|
||||
The documentation is available at [https://opengist.io/](https://opengist.io/) or in the [/docs](/docs) directory.
|
||||
|
||||
```diff
|
||||
environment:
|
||||
CONFIG: |
|
||||
log-level: info
|
||||
ssh.git-enabled: false
|
||||
disable-signup: true
|
||||
# ...
|
||||
```
|
||||
|
||||
### With binary
|
||||
|
||||
Create a `config.yml` file (you can reuse this [one](config.yml)) and run Opengist binary with the `--config` flag :
|
||||
|
||||
```shell
|
||||
./opengist --config /path/to/config.yml
|
||||
```
|
||||
|
||||
|
||||
## Administration
|
||||
|
||||
### Use Nginx as a reverse proxy
|
||||
|
||||
Configure Nginx to proxy requests to Opengist. Here is an example configuration file :
|
||||
```
|
||||
server {
|
||||
listen 80;
|
||||
server_name opengist.example.com;
|
||||
|
||||
location / {
|
||||
proxy_pass http://127.0.0.1:6157;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Then run :
|
||||
```shell
|
||||
service nginx restart
|
||||
```
|
||||
|
||||
### Use Fail2ban
|
||||
|
||||
Fail2ban can be used to ban IPs that try to bruteforce the login page.
|
||||
Log level must be set at least to `warn`.
|
||||
|
||||
Add this filter in `etc/fail2ban/filter.d/opengist.conf` :
|
||||
```ini
|
||||
[Definition]
|
||||
failregex = Invalid .* authentication attempt from <HOST>
|
||||
ignoreregex =
|
||||
```
|
||||
|
||||
Add this jail in `etc/fail2ban/jail.d/opengist.conf` :
|
||||
```ini
|
||||
[opengist]
|
||||
enabled = true
|
||||
filter = opengist
|
||||
logpath = /home/*/.opengist/log/opengist.log
|
||||
maxretry = 10
|
||||
findtime = 3600
|
||||
bantime = 600
|
||||
banaction = iptables-allports
|
||||
port = anyport
|
||||
```
|
||||
|
||||
Then run
|
||||
```shell
|
||||
service fail2ban restart
|
||||
```
|
||||
|
||||
## Configure OAuth
|
||||
|
||||
Opengist can be configured to use OAuth to authenticate users, with GitHub or Gitea.
|
||||
|
||||
<details>
|
||||
<summary>Integrate Github</summary>
|
||||
|
||||
* Add a new OAuth app in your [Github account settings](https://github.com/settings/applications/new)
|
||||
* Set 'Authorization callback URL' to `http://opengist.domain/oauth/github/callback`
|
||||
* Copy the 'Client ID' and 'Client Secret' and add them to the configuration :
|
||||
```yaml
|
||||
github.client-key: <key>
|
||||
github.secret: <secret>
|
||||
```
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Integrate Gitea</summary>
|
||||
|
||||
* Add a new OAuth app in Application settings from the [Gitea instance](https://gitea.com/user/settings/applications)
|
||||
* Set 'Redirect URI' to `http://opengist.domain/oauth/gitea/callback`
|
||||
* Copy the 'Client ID' and 'Client Secret' and add them to the configuration :
|
||||
```yaml
|
||||
gitea.client-key: <key>
|
||||
gitea.secret: <secret>
|
||||
# URL of the Gitea instance. Default: https://gitea.com/
|
||||
gitea.url: http://localhost:3000
|
||||
```
|
||||
</details>
|
||||
|
||||
## License
|
||||
|
||||
Opengist is licensed under the [AGPL-3.0 license](LICENSE).
|
||||
Opengist is licensed under the [AGPL-3.0 license](/LICENSE).
|
||||
|
102
config.yml
102
config.yml
@ -1,16 +1,45 @@
|
||||
# Set the log level to one of the following: trace, debug, info, warn, error, fatal, panic. Default: warn
|
||||
# Learn more about Opengist configuration here:
|
||||
# https://github.com/thomiceli/opengist/blob/master/docs/configuration/configure.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
|
||||
log-level: warn
|
||||
|
||||
# Public URL for the Git HTTP/SSH connection.
|
||||
# If not set, uses the URL from the request
|
||||
# Set the log output to one or more of the following: `stdout`, `file`. Default: stdout,file
|
||||
log-output: stdout,file
|
||||
|
||||
# Public URL to access to Opengist
|
||||
external-url:
|
||||
|
||||
# Directory where Opengist will store its data. Default: ~/.opengist/
|
||||
opengist-home:
|
||||
|
||||
# Name of the SQLite database file. Default: opengist.db
|
||||
db-filename: opengist.db
|
||||
# Secret key used for session store & encrypt MFA data on database. Default: <randomized 32 bytes>
|
||||
secret-key:
|
||||
|
||||
# URI of the database. Default: opengist.db (SQLite) is placed in opengist-home
|
||||
# SQLite: file:/path/to/database
|
||||
# PostgreSQL: postgres://user:password@host:port/database
|
||||
# MySQL/MariaDB: mysql://user:password@host:port/database
|
||||
db-uri: opengist.db
|
||||
|
||||
# Define the code indexer (either `bleve`, `meilisearch`, or empty for no index). Default: bleve
|
||||
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.
|
||||
# If not set, uses the Git default branch name. See https://git-scm.com/book/en/v2/Getting-Started-First-Time-Git-Setup#_new_default_branch
|
||||
git.default-branch:
|
||||
|
||||
# Set the journal mode for SQLite. Default: WAL
|
||||
# See https://www.sqlite.org/pragma.html#pragma_journal_mode
|
||||
# For SQLite databases only.
|
||||
sqlite.journal-mode: WAL
|
||||
|
||||
# HTTP server configuration
|
||||
# Host to bind to. Default: 0.0.0.0
|
||||
@ -22,14 +51,8 @@ http.port: 6157
|
||||
# Enable or disable git operations (clone, pull, push) via HTTP (either `true` or `false`). Default: true
|
||||
http.git-enabled: true
|
||||
|
||||
# Enable or disable TLS (either `true` or `false`). Default: false
|
||||
http.tls-enabled: false
|
||||
|
||||
# Path to the TLS certificate file if TLS is enabled
|
||||
http.cert-file:
|
||||
|
||||
# Path to the TLS key file if TLS is enabled
|
||||
http.key-file:
|
||||
# Enable or disable the metrics endpoint (either `true` or `false`). Default: false
|
||||
metrics.enabled: false
|
||||
|
||||
# SSH built-in server configuration
|
||||
# Note: it is not using the SSH daemon from your machine (yet)
|
||||
@ -54,16 +77,65 @@ ssh.external-domain:
|
||||
# Path or alias to ssh-keygen executable. Default: ssh-keygen
|
||||
ssh.keygen-executable: ssh-keygen
|
||||
|
||||
|
||||
# OAuth2 configuration
|
||||
# The callback/redirect URL must be http://opengist.domain/oauth/<github|gitea>/callback
|
||||
# The callback/redirect URL must be http://opengist.url/oauth/<github|gitlab|gitea|openid-connect>/callback
|
||||
|
||||
# To create a new OAuth2 application using GitHub : https://github.com/settings/applications/new
|
||||
github.client-key:
|
||||
github.secret:
|
||||
|
||||
# To create a new OAuth2 application using Gitlab : https://gitlab.com/-/user_settings/applications
|
||||
gitlab.client-key:
|
||||
gitlab.secret:
|
||||
# URL of the Gitlab instance. Default: https://gitlab.com/
|
||||
gitlab.url: https://gitlab.com/
|
||||
# The name of the GitLab instance. It is displayed in the OAuth login button. Default: GitLab
|
||||
gitlab.name: GitLab
|
||||
|
||||
# To create a new OAuth2 application using Gitea : https://gitea.domain/user/settings/applications
|
||||
gitea.client-key:
|
||||
gitea.secret:
|
||||
# URL of the Gitea instance. Default: https://gitea.com/
|
||||
gitea.url: https://gitea.com/
|
||||
# The name of the Gitea instance. It is displayed in the OAuth login button. Default: Gitea
|
||||
gitea.name: Gitea
|
||||
|
||||
# To create a new OAuth2 application using OpenID Connect:
|
||||
oidc.provider-name:
|
||||
oidc.client-key:
|
||||
oidc.secret:
|
||||
# Discovery endpoint of the OpenID provider. Generally something like http://auth.example.com/.well-known/openid-configuration
|
||||
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
|
||||
# Add your own custom assets, that are files relatives to $opengist-home/custom/
|
||||
custom.logo:
|
||||
custom.favicon:
|
||||
|
||||
# Static pages in footer (like legal notices, privacy policy, etc.)
|
||||
# The path can be a URL or a relative path to a file in the $opengist-home/custom/ directory
|
||||
custom.static-links:
|
||||
# - name: Gitea
|
||||
# path: https://gitea.com
|
||||
# - name: Legal notices
|
||||
# path: legal.html
|
||||
|
18
docker/entrypoint.sh
Executable file
18
docker/entrypoint.sh
Executable file
@ -0,0 +1,18 @@
|
||||
#!/bin/sh
|
||||
|
||||
export USER=opengist
|
||||
UID=${UID:-1000}
|
||||
GID=${GID:-1000}
|
||||
groupmod -o -g "$GID" $USER
|
||||
usermod -o -u "$UID" $USER
|
||||
|
||||
chown -R "$USER:$USER" /opengist
|
||||
chown -R "$USER:$USER" /config.yml
|
||||
|
||||
if [ -f "/run/secrets/opengist_secrets" ]; then
|
||||
set -a
|
||||
. /run/secrets/opengist_secrets
|
||||
set +a
|
||||
fi
|
||||
|
||||
exec su $USER -c "OG_OPENGIST_HOME=/opengist /app/opengist/opengist --config /config.yml"
|
97
docs/.vitepress/config.mts
Normal file
97
docs/.vitepress/config.mts
Normal file
@ -0,0 +1,97 @@
|
||||
import {defineConfig} from 'vitepress'
|
||||
|
||||
// https://vitepress.dev/reference/site-config
|
||||
export default defineConfig({
|
||||
title: "Opengist",
|
||||
description: "Documention for Opengist",
|
||||
rewrites: {
|
||||
'index.md': 'index.md',
|
||||
'introduction.md': 'docs/index.md',
|
||||
':path(.*)': 'docs/:path'
|
||||
},
|
||||
themeConfig: {
|
||||
// https://vitepress.dev/reference/default-theme-config
|
||||
logo: 'https://raw.githubusercontent.com/thomiceli/opengist/master/public/opengist.svg',
|
||||
logoLink: '/',
|
||||
nav: [
|
||||
{ text: 'Demo', link: 'https://demo.opengist.io' },
|
||||
{ text: 'Translate', link: 'https://tr.opengist.io' }
|
||||
],
|
||||
|
||||
sidebar: {
|
||||
'/docs/': [
|
||||
{
|
||||
text: '', items: [
|
||||
{text: 'Introduction', link: '/docs'},
|
||||
{text: 'Installation', link: '/docs/installation', items: [
|
||||
{text: 'Docker', link: '/docs/installation/docker'},
|
||||
{text: 'Kubernetes', link: '/docs/installation/kubernetes'},
|
||||
{text: 'Binary', link: '/docs/installation/binary'},
|
||||
{text: 'Source', link: '/docs/installation/source'},
|
||||
],
|
||||
collapsed: true
|
||||
},
|
||||
{text: 'Update', link: '/docs/update'},
|
||||
], collapsed: false
|
||||
},
|
||||
{
|
||||
text: 'Configuration', base: '/docs/configuration', items: [
|
||||
{text: 'Configure Opengist', link: '/configure'},
|
||||
{text: 'Databases', items: [
|
||||
{text: 'SQLite', link: '/databases/sqlite'},
|
||||
{text: 'PostgreSQL', link: '/databases/postgresql'},
|
||||
{text: 'MySQL', link: '/databases/mysql'},
|
||||
], collapsed: true
|
||||
},
|
||||
{text: 'OAuth Providers', link: '/oauth-providers'},
|
||||
{text: 'Custom assets', link: '/custom-assets'},
|
||||
{text: 'Custom links', link: '/custom-links'},
|
||||
{text: 'Cheat Sheet', link: '/cheat-sheet'},
|
||||
{text: 'Metrics', link: '/metrics'},
|
||||
{text: 'Admin panel', link: '/admin-panel'},
|
||||
], collapsed: false
|
||||
},
|
||||
{
|
||||
text: 'Usage', base: '/docs/usage', items: [
|
||||
{text: 'Init via Git', link: '/init-via-git'},
|
||||
{text: 'Embed Gist', link: '/embed'},
|
||||
{text: 'Gist as JSON', link: '/gist-json'},
|
||||
{text: 'Import Gists from Github', link: '/import-from-github-gist'},
|
||||
{text: 'Git push options', link: '/git-push-options'},
|
||||
], collapsed: false
|
||||
},
|
||||
{
|
||||
text: 'Administration', base: '/docs/administration', items: [
|
||||
{text: 'Run with systemd', link: '/run-with-systemd'},
|
||||
{text: 'Reverse proxy', items: [
|
||||
{text: 'Nginx', link: '/nginx-reverse-proxy'},
|
||||
{text: 'Traefik', link: '/traefik-reverse-proxy'},
|
||||
], collapsed: true},
|
||||
{text: 'Fail2ban', link: '/fail2ban-setup'},
|
||||
{text: 'Healthcheck', link: '/healthcheck'},
|
||||
], collapsed: false
|
||||
},
|
||||
{
|
||||
text: 'Contributing', base: '/docs/contributing', items: [
|
||||
{text: 'Community', link: '/community'},
|
||||
{text: 'Development', link: '/development'},
|
||||
], collapsed: false
|
||||
},
|
||||
|
||||
]},
|
||||
|
||||
socialLinks: [
|
||||
{icon: 'github', link: 'https://github.com/thomiceli/opengist'}
|
||||
],
|
||||
editLink: {
|
||||
pattern: 'https://github.com/thomiceli/opengist/edit/stable/docs/:path'
|
||||
},
|
||||
// @ts-ignore
|
||||
lastUpdated: true,
|
||||
|
||||
},
|
||||
head: [
|
||||
['link', {rel: 'icon', href: '/favicon.svg'}],
|
||||
],
|
||||
ignoreDeadLinks: true
|
||||
})
|
@ -1,8 +1,9 @@
|
||||
const colors = require('tailwindcss/colors')
|
||||
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
content: [
|
||||
"./templates/**/*.html",
|
||||
"./.vitepress/theme/*.vue",
|
||||
],
|
||||
theme: {
|
||||
colors: {
|
||||
@ -22,10 +23,8 @@ module.exports = {
|
||||
800: "#232429",
|
||||
900: "#131316"
|
||||
},
|
||||
emerald: colors.emerald,
|
||||
rose: colors.rose,
|
||||
primary: colors.sky,
|
||||
slate: colors.slate
|
||||
indigo: colors.indigo,
|
||||
|
||||
},
|
||||
extend: {
|
||||
borderWidth: {
|
||||
@ -33,5 +32,6 @@ module.exports = {
|
||||
}
|
||||
},
|
||||
},
|
||||
plugins: [require("@tailwindcss/typography"),require('@tailwindcss/forms')],
|
||||
plugins: [],
|
||||
darkMode: 'class',
|
||||
}
|
101
docs/.vitepress/theme/Home.vue
Normal file
101
docs/.vitepress/theme/Home.vue
Normal file
@ -0,0 +1,101 @@
|
||||
<script>
|
||||
import { withBase } from 'vitepress';
|
||||
import './theme.css'
|
||||
|
||||
export default {
|
||||
|
||||
setup() {
|
||||
return { withBase };
|
||||
},
|
||||
};
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
<template>
|
||||
<main class="home">
|
||||
<header class="hero">
|
||||
<div class="mx-auto max-w-7xl px-6 lg:px-8">
|
||||
<div class="mx-auto lg:text-center">
|
||||
<img class="rotating h-36 mx-auto my-8 " src="https://raw.githubusercontent.com/thomiceli/opengist/master/public/opengist.svg" alt="" >
|
||||
<a target="_blank" href="https://github.com/thomiceli/opengist/releases" class="inline-flex items-center rounded-full bg-indigo-100 hover:bg-indigo-200 px-4 py-1.5 text-lg font-medium text-indigo-700">
|
||||
<span class="pr-1">Released 1.10</span>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-4">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="m4.5 19.5 15-15m0 0H8.25m11.25 0v11.25" />
|
||||
</svg>
|
||||
</a>
|
||||
<h1 class="mt-5 text-4xl font-bold tracking-tight sm:text-5xl">Opengist</h1>
|
||||
<h2 class="mt-4 text-xl">Self-hosted pastebin powered by Git, open-source alternative to Github Gist.</h2>
|
||||
</div>
|
||||
<div class="space-x-2 my-12">
|
||||
<a href="/docs" class="rounded-md bg-indigo-600 mt-6 px-5 py-3 text-xl font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600">Docs</a>
|
||||
<a target="_blank" href="https://demo.opengist.io" class="rounded-md bg-indigo-400 mt-6 px-5 py-3 text-xl border-white font-semibold text-white shadow-sm hover:bg-indigo-300 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600">Live demo</a>
|
||||
<a target="_blank" href="https://github.com/thomiceli/opengist" class="rounded-md bg-gray-800 mt-6 px-3 py-3 text-xl dark:border dark:border-1 dark:border-gray-400 font-semibold text-white shadow-sm hover:bg-gray-700 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600">
|
||||
<svg stroke="currentColor" fill="currentColor" stroke-width="0" viewBox="0 0 16 16" class="w-7 h-auto inline" height="1em" width="1em" xmlns="http://www.w3.org/2000/svg"><path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27s1.36.09 2 .27c1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.01 8.01 0 0 0 16 8c0-4.42-3.58-8-8-8"></path></svg>
|
||||
</a>
|
||||
</div>
|
||||
<div class="border border-1 mt-6 px-5 py-3 rounded-md shadow-sm ">
|
||||
<code class="select-all ">docker run --name <span class="text-indigo-700 dark:text-indigo-300 font-bold">opengist</span> -p <span class="text-indigo-700 dark:text-indigo-300 font-bold">6157</span>:6157 -v "<span class="text-indigo-700 dark:text-indigo-300 font-bold">$HOME/.opengist</span>:/opengist" ghcr.io/thomiceli/opengist:1</code>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="relative w-full sm:max-w-7xl mx-auto overflow-auto">
|
||||
<img class="block w-[200vw] max-w-none sm:w-full h-auto" :src="withBase('/opengist-demo.png')" alt="demo-opengist-screenshot" />
|
||||
</div>
|
||||
|
||||
</main>
|
||||
</template>
|
||||
|
||||
|
||||
|
||||
<style>
|
||||
@-webkit-keyframes rotating /* Safari and Chrome */ {
|
||||
from {
|
||||
-webkit-transform: rotate(0deg);
|
||||
-o-transform: rotate(0deg);
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
-webkit-transform: rotate(360deg);
|
||||
-o-transform: rotate(360deg);
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
@keyframes rotating {
|
||||
from {
|
||||
-ms-transform: rotate(0deg);
|
||||
-moz-transform: rotate(0deg);
|
||||
-webkit-transform: rotate(0deg);
|
||||
-o-transform: rotate(0deg);
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
-ms-transform: rotate(360deg);
|
||||
-moz-transform: rotate(360deg);
|
||||
-webkit-transform: rotate(360deg);
|
||||
-o-transform: rotate(360deg);
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.home {
|
||||
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.rotating {
|
||||
-webkit-animation: rotating 8s linear infinite;
|
||||
-moz-animation: rotating 4s linear infinite;
|
||||
-ms-animation: rotating 4s linear infinite;
|
||||
-o-animation: rotating 4s linear infinite;
|
||||
animation: rotating 12s linear infinite;
|
||||
}
|
||||
|
||||
|
||||
</style>
|
16
docs/.vitepress/theme/Layout.vue
Normal file
16
docs/.vitepress/theme/Layout.vue
Normal file
@ -0,0 +1,16 @@
|
||||
<script setup>
|
||||
import { useData } from 'vitepress'
|
||||
import Home from './Home.vue'
|
||||
import DefaultTheme from 'vitepress/theme'
|
||||
|
||||
const { Layout } = DefaultTheme
|
||||
const { frontmatter } = useData()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Layout>
|
||||
<template v-if="frontmatter.layout === 'home'" #home-hero-after>
|
||||
<Home />
|
||||
</template>
|
||||
</Layout>
|
||||
</template>
|
12
docs/.vitepress/theme/index.ts
Normal file
12
docs/.vitepress/theme/index.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import { h } from 'vue'
|
||||
import type { Theme } from 'vitepress'
|
||||
import DefaultTheme from 'vitepress/theme'
|
||||
import Layout from "./Layout.vue";
|
||||
|
||||
export default {
|
||||
...DefaultTheme,
|
||||
Layout,
|
||||
enhanceApp({ app, router, siteData }) {
|
||||
// ...
|
||||
}
|
||||
} satisfies Theme
|
147
docs/.vitepress/theme/style.css
Normal file
147
docs/.vitepress/theme/style.css
Normal file
@ -0,0 +1,147 @@
|
||||
/**
|
||||
* Customize default theme styling by overriding CSS variables:
|
||||
* https://github.com/vuejs/vitepress/blob/main/src/client/theme-default/styles/vars.css
|
||||
*/
|
||||
|
||||
/**
|
||||
* Colors
|
||||
*
|
||||
* Each colors have exact same color scale system with 3 levels of solid
|
||||
* colors with different brightness, and 1 soft color.
|
||||
*
|
||||
* - `XXX-1`: The most solid color used mainly for colored text. It must
|
||||
* satisfy the contrast ratio against when used on top of `XXX-soft`.
|
||||
*
|
||||
* - `XXX-2`: The color used mainly for hover state of the button.
|
||||
*
|
||||
* - `XXX-3`: The color for solid background, such as bg color of the button.
|
||||
* It must satisfy the contrast ratio with pure white (#ffffff) text on
|
||||
* top of it.
|
||||
*
|
||||
* - `XXX-soft`: The color used for subtle background such as custom container
|
||||
* or badges. It must satisfy the contrast ratio when putting `XXX-1` colors
|
||||
* on top of it.
|
||||
*
|
||||
* The soft color must be semi transparent alpha channel. This is crucial
|
||||
* because it allows adding multiple "soft" colors on top of each other
|
||||
* to create a accent, such as when having inline code block inside
|
||||
* custom containers.
|
||||
*
|
||||
* - `default`: The color used purely for subtle indication without any
|
||||
* special meanings attched to it such as bg color for menu hover state.
|
||||
*
|
||||
* - `brand`: Used for primary brand colors, such as link text, button with
|
||||
* brand theme, etc.
|
||||
*
|
||||
* - `tip`: Used to indicate useful information. The default theme uses the
|
||||
* brand color for this by default.
|
||||
*
|
||||
* - `warning`: Used to indicate warning to the users. Used in custom
|
||||
* container, badges, etc.
|
||||
*
|
||||
* - `danger`: Used to show error, or dangerous message to the users. Used
|
||||
* in custom container, badges, etc.
|
||||
* -------------------------------------------------------------------------- */
|
||||
|
||||
:root {
|
||||
--vp-c-default-1: var(--vp-c-gray-1);
|
||||
--vp-c-default-2: var(--vp-c-gray-2);
|
||||
--vp-c-default-3: var(--vp-c-gray-3);
|
||||
--vp-c-default-soft: var(--vp-c-gray-soft);
|
||||
|
||||
--vp-c-brand-1: var(--vp-c-indigo-1);
|
||||
--vp-c-brand-2: var(--vp-c-indigo-2);
|
||||
--vp-c-brand-3: var(--vp-c-indigo-3);
|
||||
--vp-c-brand-soft: var(--vp-c-indigo-soft);
|
||||
|
||||
--vp-c-tip-1: var(--vp-c-brand-1);
|
||||
--vp-c-tip-2: var(--vp-c-brand-2);
|
||||
--vp-c-tip-3: var(--vp-c-brand-3);
|
||||
--vp-c-tip-soft: var(--vp-c-brand-soft);
|
||||
|
||||
--vp-c-warning-1: var(--vp-c-yellow-1);
|
||||
--vp-c-warning-2: var(--vp-c-yellow-2);
|
||||
--vp-c-warning-3: var(--vp-c-yellow-3);
|
||||
--vp-c-warning-soft: var(--vp-c-yellow-soft);
|
||||
|
||||
--vp-c-danger-1: var(--vp-c-red-1);
|
||||
--vp-c-danger-2: var(--vp-c-red-2);
|
||||
--vp-c-danger-3: var(--vp-c-red-3);
|
||||
--vp-c-danger-soft: var(--vp-c-red-soft);
|
||||
}
|
||||
|
||||
/**
|
||||
* Component: Button
|
||||
* -------------------------------------------------------------------------- */
|
||||
|
||||
:root {
|
||||
--vp-button-brand-border: transparent;
|
||||
--vp-button-brand-text: var(--vp-c-white);
|
||||
--vp-button-brand-bg: var(--vp-c-brand-3);
|
||||
--vp-button-brand-hover-border: transparent;
|
||||
--vp-button-brand-hover-text: var(--vp-c-white);
|
||||
--vp-button-brand-hover-bg: var(--vp-c-brand-2);
|
||||
--vp-button-brand-active-border: transparent;
|
||||
--vp-button-brand-active-text: var(--vp-c-white);
|
||||
--vp-button-brand-active-bg: var(--vp-c-brand-1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Component: Home
|
||||
* -------------------------------------------------------------------------- */
|
||||
|
||||
:root {
|
||||
--vp-home-hero-name-color: transparent;
|
||||
--vp-home-hero-name-background: -webkit-linear-gradient(
|
||||
120deg,
|
||||
#0f0513 30%,
|
||||
#7e8b90
|
||||
);
|
||||
|
||||
--vp-home-hero-image-background-image: linear-gradient(
|
||||
-45deg,
|
||||
#bd34fe 50%,
|
||||
#47caff 50%
|
||||
);
|
||||
--vp-home-hero-image-filter: blur(44px);
|
||||
}
|
||||
|
||||
@media (min-width: 640px) {
|
||||
:root {
|
||||
--vp-home-hero-image-filter: blur(56px);
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 960px) {
|
||||
:root {
|
||||
--vp-home-hero-image-filter: blur(68px);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Component: Custom Block
|
||||
* -------------------------------------------------------------------------- */
|
||||
|
||||
:root {
|
||||
--vp-custom-block-tip-border: transparent;
|
||||
--vp-custom-block-tip-text: var(--vp-c-text-1);
|
||||
--vp-custom-block-tip-bg: var(--vp-c-brand-soft);
|
||||
--vp-custom-block-tip-code-bg: var(--vp-c-brand-soft);
|
||||
}
|
||||
|
||||
/**
|
||||
* Component: Algolia
|
||||
* -------------------------------------------------------------------------- */
|
||||
|
||||
.DocSearch {
|
||||
--docsearch-primary-color: var(--vp-c-brand-1) !important;
|
||||
}
|
||||
|
||||
.content img {
|
||||
padding-left: 20px;
|
||||
height: 108px;
|
||||
}
|
||||
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
29
docs/administration/fail2ban-setup.md
Normal file
29
docs/administration/fail2ban-setup.md
Normal file
@ -0,0 +1,29 @@
|
||||
# Fail2ban setup
|
||||
|
||||
Fail2ban can be used to ban IPs that try to bruteforce the login page.
|
||||
Log level must be set at least to `warn`.
|
||||
|
||||
Add this filter in `etc/fail2ban/filter.d/opengist.conf` :
|
||||
```ini
|
||||
[Definition]
|
||||
failregex = Invalid .* authentication attempt from <HOST>
|
||||
ignoreregex =
|
||||
```
|
||||
|
||||
Add this jail in `etc/fail2ban/jail.d/opengist.conf` :
|
||||
```ini
|
||||
[opengist]
|
||||
enabled = true
|
||||
filter = opengist
|
||||
logpath = /home/*/.opengist/log/opengist.log
|
||||
maxretry = 10
|
||||
findtime = 3600
|
||||
bantime = 600
|
||||
banaction = iptables-allports
|
||||
port = anyport
|
||||
```
|
||||
|
||||
Then run
|
||||
```shell
|
||||
service fail2ban restart
|
||||
```
|
13
docs/administration/healthcheck.md
Normal file
13
docs/administration/healthcheck.md
Normal file
@ -0,0 +1,13 @@
|
||||
# Healthcheck
|
||||
|
||||
A healthcheck is a simple HTTP GET request to the `/healthcheck` endpoint. It returns a `200 OK` response if the server is healthy.
|
||||
|
||||
## Example
|
||||
|
||||
```shell
|
||||
curl http://localhost:6157/healthcheck
|
||||
```
|
||||
|
||||
```json
|
||||
{"database":"ok","opengist":"ok","time":"2024-01-04T05:18:33+01:00"}
|
||||
```
|
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
|
||||
```
|
46
docs/administration/nginx-reverse-proxy.md
Normal file
46
docs/administration/nginx-reverse-proxy.md
Normal file
@ -0,0 +1,46 @@
|
||||
# Use Nginx as a reverse proxy
|
||||
|
||||
Configure Nginx to proxy requests to Opengist. Here are example configuration file to use Opengist on a subdomain or on a subpath.
|
||||
|
||||
Make sure you set the base url for Opengist via the [configuration](/docs/configuration/cheat-sheet.md).
|
||||
|
||||
### Subdomain
|
||||
```
|
||||
server {
|
||||
listen 80;
|
||||
server_name opengist.example.com;
|
||||
|
||||
location / {
|
||||
proxy_pass http://127.0.0.1:6157;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Subpath
|
||||
```
|
||||
server {
|
||||
listen 80;
|
||||
server_name example.com;
|
||||
|
||||
location /opengist/ {
|
||||
rewrite ^/opengist(/.*)$ $1 break;
|
||||
proxy_pass http://127.0.0.1:6157;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header X-Forwarded-Prefix /opengist;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
To apply changes:
|
||||
```shell
|
||||
sudo systemctl restart nginx
|
||||
```
|
7
docs/administration/reset-password.md
Normal file
7
docs/administration/reset-password.md
Normal file
@ -0,0 +1,7 @@
|
||||
# Reset a user password
|
||||
|
||||
To reset a user password, run the following command using the Opengist binary:
|
||||
|
||||
```bash
|
||||
./opengist admin reset-password <username> <new-password>
|
||||
```
|
92
docs/administration/run-with-systemd.md
Normal file
92
docs/administration/run-with-systemd.md
Normal file
@ -0,0 +1,92 @@
|
||||
# Run with Systemd
|
||||
|
||||
For non-Docker users, you could run Opengist as a systemd service.
|
||||
|
||||
## As root
|
||||
On Unix distributions with systemd, place the Opengist binary like:
|
||||
|
||||
```shell
|
||||
sudo cp opengist /usr/local/bin
|
||||
sudo mkdir -p /var/lib/opengist
|
||||
sudo cp config.yml /etc/opengist
|
||||
```
|
||||
|
||||
Edit the Opengist home directory configuration in `/etc/opengist/config.yml` like:
|
||||
```shell
|
||||
opengist-home: /var/lib/opengist
|
||||
```
|
||||
|
||||
Create a new user to run Opengist:
|
||||
```shell
|
||||
sudo useradd --system opengist
|
||||
sudo mkdir -p /var/lib/opengist
|
||||
sudo chown -R opengist:opengist /var/lib/opengist
|
||||
```
|
||||
|
||||
Then create a service file at `/etc/systemd/system/opengist.service`:
|
||||
```ini
|
||||
[Unit]
|
||||
Description=opengist Server
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=opengist
|
||||
Group=opengist
|
||||
ExecStart=opengist --config /etc/opengist/config.yml
|
||||
Restart=on-failure
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
```
|
||||
|
||||
Finally, start the service:
|
||||
```shell
|
||||
systemctl daemon-reload
|
||||
systemctl enable --now opengist
|
||||
systemctl status opengist
|
||||
```
|
||||
|
||||
----
|
||||
|
||||
## As a normal user
|
||||
**NOTE: This was tested on Ubuntu 20.04 and newer. For other distros, please check the respective documentation**
|
||||
|
||||
#### For the purpose of this documentation, we will assume that:
|
||||
- You've followed the instructions on how to run opengist [from source](https://github.com/thomiceli/opengist?tab=readme-ov-file#from-source)
|
||||
- Your shell user is named `pastebin`
|
||||
- All commands are being executed as the `pastebin` user
|
||||
|
||||
_If none of the above is true, then adapt the commands and paths to fit your needs._
|
||||
|
||||
Enable lingering for the user:
|
||||
```shell
|
||||
loginctl enable-linger
|
||||
```
|
||||
|
||||
Create the user systemd folder:
|
||||
```
|
||||
mkdir -p /home/pastebin/.config/systemd/user
|
||||
```
|
||||
|
||||
Then create a service file at `/home/pastebin/.config/systemd/user/opengist.service`:
|
||||
```ini
|
||||
[Unit]
|
||||
Description=opengist Server
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
ExecStart=/home/pastebin/opengist/opengist --config /home/pastebin/opengist/config.yml
|
||||
Restart=on-failure
|
||||
|
||||
[Install]
|
||||
WantedBy=default.target
|
||||
```
|
||||
|
||||
Finally, start the service:
|
||||
```shell
|
||||
systemctl --user daemon-reload
|
||||
systemctl --user enable --now opengist
|
||||
systemctl --user status opengist
|
||||
```
|
48
docs/administration/traefik-reverse-proxy.md
Normal file
48
docs/administration/traefik-reverse-proxy.md
Normal file
@ -0,0 +1,48 @@
|
||||
# Use Traefik as a reverse proxy
|
||||
|
||||
You can set up Traefik in two ways:
|
||||
|
||||
<details>
|
||||
<summary>Using Docker labels</summary>
|
||||
|
||||
Add these labels to your `docker-compose.yml` file:
|
||||
|
||||
```yml
|
||||
labels:
|
||||
- traefik.http.routers.opengist.rule=Host(`opengist.example.com`) # Change to your subdomain
|
||||
# Uncomment the line below if you run Opengist in a subdirectory
|
||||
# - traefik.http.routers.app1.rule=PathPrefix(`/opengist{regex:$$|/.*}`) # Change opentist in the regex to yuor subdirectory name
|
||||
- traefik.http.routers.opengist.entrypoints=websecure # Change to the name of your 443 port entrypoint
|
||||
- traefik.http.routers.opengist.tls.certresolver=lets-encrypt # Change to certresolver's name
|
||||
- traefik.http.routers.opengist.service=opengist
|
||||
- traefik.http.services.opengist.loadBalancer.server.port=6157
|
||||
```
|
||||
</details>
|
||||
<details>
|
||||
<summary>Using a <code>yml</code> file</summary>
|
||||
|
||||
> [!Note]
|
||||
> Don't forget to change the `<server-address>` to your server's IP
|
||||
|
||||
`traefik_dynamic.yml`
|
||||
```yml
|
||||
http:
|
||||
routers:
|
||||
opengist:
|
||||
entrypoints: websecure
|
||||
rule: Host(`opengist.example.com`) # Comment this line and uncomment the line below if using a subpath
|
||||
# rule: PathPrefix(`/opengist{regex:$$|/.*}`) # Change opentist in the regex to yuor subdirectory name
|
||||
# middlewares:
|
||||
# - opengist-fail2ban
|
||||
service: opengist
|
||||
tls:
|
||||
certresolver: lets-encrypt
|
||||
services:
|
||||
opengist:
|
||||
loadbalancer:
|
||||
servers:
|
||||
- url: "http://<server-address>:6157"
|
||||
|
||||
```
|
||||
|
||||
</details>
|
53
docs/configuration/admin-panel.md
Normal file
53
docs/configuration/admin-panel.md
Normal file
@ -0,0 +1,53 @@
|
||||
# Admin panel
|
||||
|
||||
The first user created on your Opengist instance has access to the Admin panel.
|
||||
|
||||
To access the Admin panel:
|
||||
|
||||
1. Log in
|
||||
2. Click your username in the upper right corner
|
||||
3. Select `Admin`
|
||||
|
||||
## Usage
|
||||
|
||||
### General
|
||||
|
||||
Here you can see some basic information, like Opengist version, alongside some stats.
|
||||
|
||||
You can also start some actions like forcing synchronization of gists,
|
||||
starting garbage collection, etc.
|
||||
|
||||
### Users
|
||||
|
||||
Here you can see your users and delete them.
|
||||
|
||||
### Gists
|
||||
|
||||
Here you can see all the gists and some basic information about them. You also have an option
|
||||
to delete them.
|
||||
|
||||
|
||||
### Invitations
|
||||
|
||||
Here you can create invitation links with some options like limiting the number of signed up
|
||||
users or setting an expiration date.
|
||||
|
||||
> [!Note]
|
||||
> Invitation links override the `Disable signup` option but not the `Disable login form` option.
|
||||
>
|
||||
> Users will see only the OAuth providers when `Disable login form` is enabled.
|
||||
|
||||
### Configuration
|
||||
|
||||
Here you can change a limited number of settings without restarting the instance.
|
||||
|
||||
- Disable signup
|
||||
- Forbid the creation of new accounts.
|
||||
- Require login
|
||||
- Enforce users to be logged in to see gists.
|
||||
- Allow individual gists without login
|
||||
- Allow individual gists to be viewed and downloaded without login, while requiring login for discovering gists.
|
||||
- Disable login form
|
||||
- Forbid logging in via the login form to force using OAuth providers instead.
|
||||
- Disable Gravatar
|
||||
- Disable the usage of Gravatar as an avatar provider.
|
51
docs/configuration/cheat-sheet.md
Normal file
51
docs/configuration/cheat-sheet.md
Normal file
@ -0,0 +1,51 @@
|
||||
---
|
||||
aside: false
|
||||
---
|
||||
|
||||
# Configuration Cheat Sheet
|
||||
|
||||
| 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-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. |
|
||||
| opengist-home | OG_OPENGIST_HOME | home directory | Path to the directory where Opengist stores its data. |
|
||||
| secret-key | OG_SECRET_KEY | randomized 32 bytes | Secret key used for session store & encrypt MFA data on database. |
|
||||
| db-uri | OG_DB_URI | `opengist.db` | URI of the database. |
|
||||
| index | OG_INDEX | `bleve` | Define the code indexer (either `bleve`, `meilisearch`, or empty for no index). |
|
||||
| index.meili.host | OG_MEILI_HOST | none | Set the host for the Meiliseach server. |
|
||||
| index.meili.api-key | OG_MEILI_API_KEY | none | Set the API key for the Meiliseach server. |
|
||||
| git.default-branch | OG_GIT_DEFAULT_BRANCH | none | Default branch name used by Opengist when initializing Git repositories. If not set, uses the Git default branch name. More info [here](https://git-scm.com/book/en/v2/Getting-Started-First-Time-Git-Setup#_new_default_branch) |
|
||||
| sqlite.journal-mode | OG_SQLITE_JOURNAL_MODE | `WAL` | Set the journal mode for SQLite. More info [here](https://www.sqlite.org/pragma.html#pragma_journal_mode) |
|
||||
| http.host | OG_HTTP_HOST | `0.0.0.0` | The host on which the HTTP server should bind. |
|
||||
| http.port | OG_HTTP_PORT | `6157` | The port on which the HTTP server should listen. |
|
||||
| http.git-enabled | OG_HTTP_GIT_ENABLED | `true` | Enable or disable git operations (clone, pull, push) via HTTP. (`true` or `false`) |
|
||||
| metrics.enabled | OG_METRICS_ENABLED | `false` | Enable or disable Prometheus metrics endpoint at `/metrics` (`true` or `false`) |
|
||||
| ssh.git-enabled | OG_SSH_GIT_ENABLED | `true` | Enable or disable git operations (clone, pull, push) via SSH. (`true` or `false`) |
|
||||
| ssh.host | OG_SSH_HOST | `0.0.0.0` | The host on which the SSH server should bind. |
|
||||
| ssh.port | OG_SSH_PORT | `2222` | The port on which the SSH server should listen. |
|
||||
| ssh.external-domain | OG_SSH_EXTERNAL_DOMAIN | none | Public domain for the Git SSH connection, if it has to be different from the HTTP one. If not set, uses the URL from the request. |
|
||||
| ssh.keygen-executable | OG_SSH_KEYGEN_EXECUTABLE | `ssh-keygen` | Path to the SSH key generation executable. |
|
||||
| github.client-key | OG_GITHUB_CLIENT_KEY | none | The client key for the GitHub OAuth application. |
|
||||
| github.secret | OG_GITHUB_SECRET | none | The secret for the GitHub OAuth application. |
|
||||
| gitlab.client-key | OG_GITLAB_CLIENT_KEY | none | The client key for the GitLab OAuth application. |
|
||||
| gitlab.secret | OG_GITLAB_SECRET | none | The secret for the GitLab OAuth application. |
|
||||
| gitlab.url | OG_GITLAB_URL | `https://gitlab.com/` | The URL of the GitLab instance. |
|
||||
| gitlab.name | OG_GITLAB_NAME | `GitLab` | The name of the GitLab instance. It is displayed in the OAuth login button. |
|
||||
| gitea.client-key | OG_GITEA_CLIENT_KEY | none | The client key for the Gitea OAuth application. |
|
||||
| gitea.secret | OG_GITEA_SECRET | none | The secret for the Gitea OAuth application. |
|
||||
| gitea.url | OG_GITEA_URL | `https://gitea.com/` | The URL of the Gitea instance. |
|
||||
| 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). |
|
72
docs/configuration/configure.md
Normal file
72
docs/configuration/configure.md
Normal file
@ -0,0 +1,72 @@
|
||||
# Configuration
|
||||
|
||||
Opengist provides flexible configuration options through either a YAML file and/or environment variables.
|
||||
You would only need to specify the configuration options you want to change — for any config option left untouched,
|
||||
Opengist will simply apply the default values.
|
||||
|
||||
The [configuration cheat sheet](cheat-sheet.md) lists all available configuration options.
|
||||
|
||||
|
||||
## Configuration via YAML file
|
||||
|
||||
The configuration file must be specified when launching the application, using the `--config` flag followed by the path
|
||||
to your YAML file.
|
||||
|
||||
Usage with Docker Compose :
|
||||
```yml
|
||||
services:
|
||||
opengist:
|
||||
# ...
|
||||
volumes:
|
||||
# ...
|
||||
- "/path/to/config.yml:/config.yml"
|
||||
```
|
||||
|
||||
Usage via command line :
|
||||
```shell
|
||||
./opengist --config /path/to/config.yml
|
||||
```
|
||||
|
||||
You can start by copying and/or modifying the provided [config.yml](https://github.com/thomiceli/opengist/blob/stable/config.yml) file.
|
||||
|
||||
|
||||
## Configuration via Environment Variables
|
||||
|
||||
Usage with Docker Compose :
|
||||
```yml
|
||||
services:
|
||||
opengist:
|
||||
# ...
|
||||
environment:
|
||||
OG_LOG_LEVEL: "info"
|
||||
# etc.
|
||||
```
|
||||
|
||||
Usage via command line :
|
||||
```shell
|
||||
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
|
||||
```
|
45
docs/configuration/custom-assets.md
Normal file
45
docs/configuration/custom-assets.md
Normal file
@ -0,0 +1,45 @@
|
||||
# Custom assets
|
||||
|
||||
To add custom assets to your Opengist instance, you can use the `$opengist-home/custom` directory (where `$opengist-home` is the directory where Opengist stores its data).
|
||||
|
||||
### Logo / Favicon
|
||||
|
||||
To add a custom logo or favicon, you can add your own image file to the `$opengist-home/custom` directory, then define the relative path in the config.
|
||||
|
||||
For example, if you have a logo file `logo.png` in the `$opengist-home/custom` directory, you can set the logo path in the config as follows:
|
||||
|
||||
#### YAML
|
||||
```yaml
|
||||
custom.logo: logo.png
|
||||
```
|
||||
|
||||
#### Environment variable
|
||||
```sh
|
||||
export OG_CUSTOM_LOGO=logo.png
|
||||
```
|
||||
|
||||
Same as the favicon:
|
||||
|
||||
#### YAML
|
||||
```yaml
|
||||
custom.favicon: favicon.png
|
||||
```
|
||||
|
||||
#### Environment variable
|
||||
```sh
|
||||
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"
|
||||
```
|
62
docs/configuration/custom-links.md
Normal file
62
docs/configuration/custom-links.md
Normal file
@ -0,0 +1,62 @@
|
||||
# Custom links
|
||||
|
||||
On the footer of your Opengist instance, you can add links to custom static templates or any other website you want to link to.
|
||||
This can be useful for legal information, privacy policy, or any other information you want to provide to your users.
|
||||
|
||||
To add one or more links, you can add your own file to the `$opengist-home/custom` directory or set a URL, then define the relative path and its name in the config.
|
||||
|
||||
For example, if you have a legal information file `legal.html` in the `$opengist-home/custom` directory, and also wish to add a link to a Gitea instance, you can set the link in the config as follows:
|
||||
|
||||
#### YAML
|
||||
```yaml
|
||||
custom.static-links:
|
||||
- name: Legal notices
|
||||
path: legal.html
|
||||
- name: Gitea
|
||||
path: https://gitea.com
|
||||
```
|
||||
|
||||
#### Environment variable
|
||||
```sh
|
||||
OG_CUSTOM_STATIC_LINK_0_NAME="Legal Notices" \
|
||||
OG_CUSTOM_STATIC_LINK_0_PATH=legal.html \
|
||||
OG_CUSTOM_STATIC_LINK_1_NAME=Gitea \
|
||||
OG_CUSTOM_STATIC_LINK_1_PATH=https://gitea.com \
|
||||
./opengist
|
||||
```
|
||||
|
||||
## Templating custom HTML pages
|
||||
|
||||
In the start and end of the custom HTML files, you can use the syntax to include the header and footer of the Opengist instance:
|
||||
|
||||
```html
|
||||
{{ template "header" . }}
|
||||
|
||||
<!-- my content -->
|
||||
|
||||
{{ template "footer" . }}
|
||||
```
|
||||
|
||||
If you want your custom page to integrate well into the existing theme, you can use the following:
|
||||
|
||||
```html
|
||||
{{ template "header" . }}
|
||||
|
||||
<div class="py-10">
|
||||
<header class="pb-4 ">
|
||||
<div class="flex">
|
||||
<div class="flex-auto">
|
||||
<h2 class="text-2xl font-bold leading-tight">Heading</h2>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<main>
|
||||
<h3 class="text-xl font-bold leading-tight mt-4">Sub-Heading</h3>
|
||||
<p class="mt-4 ml-1"><!-- my content --></p>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
{{ template "footer" . }}
|
||||
```
|
||||
|
||||
You can adjust above as needed. Opengist uses TailwindCSS classes.
|
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
|
||||
```
|
49
docs/configuration/metrics.md
Normal file
49
docs/configuration/metrics.md
Normal file
@ -0,0 +1,49 @@
|
||||
# Metrics
|
||||
|
||||
Opengist offers built-in support for Prometheus metrics to help you monitor the performance and usage of your instance. These metrics provide insights into application health, user activity, and database statistics.
|
||||
|
||||
## Enabling metrics
|
||||
|
||||
By default, the metrics endpoint is disabled for security and performance reasons. To enable it, update your configuration as stated in the [configuration cheat sheet](cheat-sheet.md):
|
||||
|
||||
```yaml
|
||||
metrics.enabled = true
|
||||
```
|
||||
|
||||
Alternatively, you can use the environment variable:
|
||||
|
||||
```bash
|
||||
OG_METRICS_ENABLED=true
|
||||
```
|
||||
|
||||
Once enabled, metrics are available at the /metrics endpoint.
|
||||
|
||||
## Available metrics
|
||||
|
||||
### Opengist-specific metrics
|
||||
|
||||
| Metric Name | Type | Description |
|
||||
|-------------|------|-------------|
|
||||
| `opengist_users_total` | Gauge | Total number of registered users |
|
||||
| `opengist_gists_total` | Gauge | Total number of gists in the system |
|
||||
| `opengist_ssh_keys_total` | Gauge | Total number of SSH keys added by users |
|
||||
|
||||
### Standard HTTP metrics
|
||||
|
||||
In addition to the Opengist-specific metrics, standard Prometheus HTTP metrics are also available through the Echo Prometheus middleware. These include request durations, request counts, and request/response sizes.
|
||||
|
||||
These standard metrics follow the Prometheus naming convention and include labels for HTTP method, status code, and handler path.
|
||||
|
||||
## Security Considerations
|
||||
|
||||
The metrics endpoint exposes information about your Opengist instance that might be sensitive in some environments. Consider using a reverse proxy with authentication for the `/metrics` endpoint if your Opengist instance is publicly accessible.
|
||||
|
||||
Example with Nginx:
|
||||
|
||||
```shell
|
||||
location /metrics {
|
||||
auth_basic "Metrics";
|
||||
auth_basic_user_file /etc/nginx/.htpasswd;
|
||||
proxy_pass http://localhost:6157/metrics;
|
||||
}
|
||||
```
|
94
docs/configuration/oauth-providers.md
Normal file
94
docs/configuration/oauth-providers.md
Normal file
@ -0,0 +1,94 @@
|
||||
# Use OAuth providers
|
||||
|
||||
Opengist can be configured to use OAuth to authenticate users, with GitHub, Gitea, or OpenID Connect.
|
||||
|
||||
## GitHub
|
||||
|
||||
* Add a new OAuth app in your [GitHub account settings](https://github.com/settings/applications/new)
|
||||
* Set 'Authorization callback URL' to `http://opengist.url/oauth/github/callback`
|
||||
* Copy the 'Client ID' and 'Client Secret' and add them to the [configuration](cheat-sheet.md) :
|
||||
```yaml
|
||||
github.client-key: <key>
|
||||
github.secret: <secret>
|
||||
```
|
||||
```shell
|
||||
OG_GITHUB_CLIENT_KEY=<key>
|
||||
OG_GITHUB_SECRET=<secret>
|
||||
```
|
||||
|
||||
|
||||
## GitLab
|
||||
|
||||
* Add a new OAuth app in Application settings from the [GitLab instance](https://gitlab.com/-/user_settings/applications)
|
||||
* Set 'Redirect URI' to `http://opengist.url/oauth/gitlab/callback`
|
||||
* Copy the 'Client ID' and 'Client Secret' and add them to the [configuration](cheat-sheet.md) :
|
||||
```yaml
|
||||
gitlab.client-key: <key>
|
||||
gitlab.secret: <secret>
|
||||
# URL of the GitLab instance. Default: https://gitlab.com/
|
||||
gitlab.url: https://gitlab.com/
|
||||
```
|
||||
```shell
|
||||
OG_GITLAB_CLIENT_KEY=<key>
|
||||
OG_GITLAB_SECRET=<secret>
|
||||
# URL of the GitLab instance. Default: https://gitlab.com/
|
||||
OG_GITLAB_URL=https://gitlab.com/
|
||||
```
|
||||
|
||||
|
||||
|
||||
## Gitea
|
||||
|
||||
* Add a new OAuth app in Application settings from the [Gitea instance](https://gitea.com/user/settings/applications)
|
||||
* Set 'Redirect URI' to `http://opengist.url/oauth/gitea/callback`
|
||||
* Copy the 'Client ID' and 'Client Secret' and add them to the [configuration](cheat-sheet.md) :
|
||||
```yaml
|
||||
gitea.client-key: <key>
|
||||
gitea.secret: <secret>
|
||||
# URL of the Gitea instance. Default: https://gitea.com/
|
||||
gitea.url: http://localhost:3000
|
||||
```
|
||||
```shell
|
||||
OG_GITEA_CLIENT_KEY=<key>
|
||||
OG_GITEA_SECRET=<secret>
|
||||
# URL of the Gitea instance. Default: https://gitea.com/
|
||||
OG_GITEA_URL=http://localhost:3000
|
||||
```
|
||||
|
||||
|
||||
|
||||
## OpenID Connect
|
||||
|
||||
* Add a new OAuth app in Application settings of your OIDC provider
|
||||
* 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) :
|
||||
```yaml
|
||||
oidc.provider-name: <provider-name>
|
||||
oidc.client-key: <key>
|
||||
oidc.secret: <secret>
|
||||
# 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
|
||||
```
|
||||
```shell
|
||||
OG_OIDC_PROVIDER_NAME=<provider-name>
|
||||
OG_OIDC_CLIENT_KEY=<key>
|
||||
OG_OIDC_SECRET=<secret>
|
||||
# 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
|
||||
```
|
||||
|
||||
### 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.
|
7
docs/contributing/community.md
Normal file
7
docs/contributing/community.md
Normal file
@ -0,0 +1,7 @@
|
||||
# Community
|
||||
|
||||
The following is a list of resources made by happy users of Opengist. Feel free to make a PR add your own!
|
||||
|
||||
- [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
|
||||
- [Proxmox VE Helper-Script](https://community-scripts.github.io/ProxmoxVE/scripts?id=opengist) - A script to install Opengist on Proxmox VE
|
38
docs/contributing/development.md
Normal file
38
docs/contributing/development.md
Normal file
@ -0,0 +1,38 @@
|
||||
# Run Opengist in development mode
|
||||
|
||||
## With Docker
|
||||
|
||||
Assuming you have [Make](https://linux.die.net/man/1/make) installed,
|
||||
|
||||
```shell
|
||||
# Clone the repository
|
||||
git clone git@github.com:thomiceli/opengist.git
|
||||
cd opengist
|
||||
|
||||
# Build the development image
|
||||
make build_dev_docker
|
||||
```
|
||||
|
||||
Now you can run the development image with the following command:
|
||||
|
||||
```shell
|
||||
make run_dev_docker
|
||||
```
|
||||
|
||||
Opengist is now running on port 6157, you can browse http://localhost:6157
|
||||
|
||||
## As a binary
|
||||
|
||||
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
|
||||
git clone git@github.com:thomiceli/opengist.git
|
||||
cd opengist
|
||||
make watch
|
||||
```
|
||||
|
||||
Opengist is now running on port 6157, you can browse http://localhost:6157
|
4
docs/index.md
Normal file
4
docs/index.md
Normal file
@ -0,0 +1,4 @@
|
||||
---
|
||||
layout: home
|
||||
navbar: false
|
||||
---
|
7
docs/installation.md
Normal file
7
docs/installation.md
Normal file
@ -0,0 +1,7 @@
|
||||
# Install Opengist
|
||||
|
||||
There are several ways to install Opengist, depending on your preferences and your environment.
|
||||
|
||||
- [Docker](installation/docker.md)
|
||||
- [Source](installation/source.md)
|
||||
- [Binary](installation/binary.md)
|
14
docs/installation/binary.md
Normal file
14
docs/installation/binary.md
Normal file
@ -0,0 +1,14 @@
|
||||
# Install from binary
|
||||
|
||||
Download the archive for your system from the release page [here](https://github.com/thomiceli/opengist/releases/latest), and extract it.
|
||||
|
||||
```shell
|
||||
# example for linux amd64
|
||||
wget https://github.com/thomiceli/opengist/releases/download/v1.10.0/opengist1.10.0-linux-amd64.tar.gz
|
||||
|
||||
tar xzvf opengist1.10.0-linux-amd64.tar.gz
|
||||
cd opengist
|
||||
chmod +x opengist
|
||||
./opengist # with or without `--config config.yml`
|
||||
```
|
||||
|
41
docs/installation/docker.md
Normal file
41
docs/installation/docker.md
Normal file
@ -0,0 +1,41 @@
|
||||
# Install with Docker
|
||||
|
||||
Docker [images](https://github.com/thomiceli/opengist/pkgs/container/opengist) are available for each release :
|
||||
|
||||
```shell
|
||||
docker pull ghcr.io/thomiceli/opengist:1
|
||||
```
|
||||
|
||||
It can be used in a `docker-compose.yml` file :
|
||||
|
||||
1. Create a `docker-compose.yml` file with the following content
|
||||
2. Run `docker compose up -d`
|
||||
3. Opengist is now running on port 6157, you can browse http://localhost:6157
|
||||
|
||||
```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_LOG_LEVEL: info
|
||||
# other configuration options
|
||||
```
|
||||
|
||||
You can define which user/group should run the container and own the files by setting the `UID` and `GID` environment
|
||||
variables :
|
||||
|
||||
```yml
|
||||
services:
|
||||
opengist:
|
||||
# ...
|
||||
environment:
|
||||
UID: 1001
|
||||
GID: 1001
|
||||
```
|
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.
|
19
docs/installation/source.md
Normal file
19
docs/installation/source.md
Normal file
@ -0,0 +1,19 @@
|
||||
# Installation from source
|
||||
|
||||
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
|
||||
git clone https://github.com/thomiceli/opengist
|
||||
cd opengist
|
||||
|
||||
git checkout v1.10.0 # optional, to checkout the latest release
|
||||
|
||||
make
|
||||
./opengist
|
||||
```
|
||||
|
||||
Opengist is now running on port 6157, you can browse http://localhost:6157
|
56
docs/introduction.md
Normal file
56
docs/introduction.md
Normal file
@ -0,0 +1,56 @@
|
||||
# Opengist
|
||||
|
||||
<img height="108px" src="https://raw.githubusercontent.com/thomiceli/opengist/master/public/opengist.svg" alt="Opengist" align="right" />
|
||||
|
||||
Opengist is a **self-hosted** pastebin **powered by Git**. All snippets are stored in a Git repository and can be
|
||||
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.
|
||||
|
||||
Written in [Go](https://go.dev), Opengist aims to be fast and easy to deploy.
|
||||
|
||||
|
||||
## Features
|
||||
|
||||
* Create public, unlisted or private snippets
|
||||
* [Init](usage/init-via-git.md) / Clone / Pull / Push snippets **via Git** over HTTP or SSH
|
||||
* Syntax highlighting ; markdown & CSV support
|
||||
* Search code in snippets ; browse users snippets, likes and forks
|
||||
* Add topics to snippets
|
||||
* Embed snippets in other websites
|
||||
* Revisions history
|
||||
* Like / Fork snippets
|
||||
* Editor with indentation mode & size ; drag and drop files
|
||||
* Download raw files or as a ZIP archive
|
||||
* Retrieve snippet data/metadata via a JSON API
|
||||
* OAuth2 login with GitHub, GitLab, Gitea, and OpenID Connect
|
||||
* Avatars via Gravatar or OAuth2 providers
|
||||
* Light/Dark mode
|
||||
* Responsive UI
|
||||
* Enable or disable signups
|
||||
* Restrict or unrestrict snippets visibility to anonymous users
|
||||
* Admin panel :
|
||||
* delete users/gists;
|
||||
* clean database/filesystem by syncing gists
|
||||
* run `git gc` for all repositories
|
||||
* SQLite/PostgreSQL/MySQL database
|
||||
* Logging
|
||||
* Docker support
|
||||
|
||||
|
||||
## System requirements
|
||||
|
||||
[Git](https://git-scm.com/download) is obviously required to run Opengist, as it's the main feature of the app.
|
||||
Version **2.28** or later is recommended as the app has not been tested with older Git versions and some features would not work.
|
||||
|
||||
[OpenSSH](https://www.openssh.com/) suite if you wish to use Git over SSH.
|
||||
|
||||
|
||||
## Components
|
||||
|
||||
* Backend Web Framework: [Echo](https://echo.labstack.com/)
|
||||
* ORM: [GORM](https://gorm.io/)
|
||||
* Frontend libraries:
|
||||
* [TailwindCSS](https://tailwindcss.com/)
|
||||
* [CodeMirror](https://codemirror.net/)
|
||||
* [Day.js](https://day.js.org/)
|
||||
* and [others](/package.json)
|
17
docs/public/favicon.svg
Normal file
17
docs/public/favicon.svg
Normal file
@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Generator: Adobe Illustrator 27.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 500 500" style="enable-background:new 0 0 500 500;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:#FFFFFF;}
|
||||
</style>
|
||||
<g id="document" transform="scale(1.6666666666666667 1.6666666666666667) translate(150.0 150.0)">
|
||||
<path class="st0" d="M131.3,24.3c13.7-71-33.9-139.5-106.4-152.9C-47.7-142-117.6-95.3-131.3-24.3s33.9,139.5,106.4,152.9 C47.7,142,117.6,95.3,131.3,24.3z"/>
|
||||
<path class="st0" d="M128.9,0c0,55.7-36.8,103-88,119.8c0.2-1.2,0.3-2.5,0.3-4c0.1-22.3,0.2-36.2,0.2-52.8 c0-11.7-0.2-18.1-0.2-18.1c1.8,0,21.1-6,29.9-12.1S89.2,15.1,90.5-1.4c1.3-16.6-6-36.2-12.4-47.8C65.3-72.4,54.7-86.6,45.4-94.5 c-9.3-7.8-16.1-6.1-22.1-1.4S8.5-76.9,2.2-71.2c-3,2.8-10.6,12-20.4,3.3C-21-70.3-38-93.6-48.5-90.6c-13.1,3.7-28.1,27.3-35.1,43.8 c-9,21-10.8,33.6-6.1,63.5c4.7,29.9,7.5,60,11.8,76.4c1,4,2.3,7.4,4,10.4c-33.2-22.8-55-60.7-55-103.5 c0-69.7,57.7-126.3,128.9-126.3S128.9-69.7,128.9,0z"/>
|
||||
<path d="M0-145c-81.8,0-148.1,64.9-148.1,145S-81.8,145,0,145S148.1,80.1,148.1,0S81.8-145,0-145z M40.9,119.8 c0.2-1.2,0.3-2.5,0.3-4c0.1-22.3,0.2-36.2,0.2-52.8c0-11.7-0.2-18.1-0.2-18.1c1.8,0,21.1-6,29.9-12.1S89.2,15.1,90.5-1.4 c1.3-16.6-6-36.2-12.4-47.8C65.3-72.4,54.7-86.6,45.4-94.5c-9.3-7.8-16.1-6.1-22.1-1.4S8.5-76.9,2.2-71.2c-3,2.8-10.6,12-20.4,3.3 C-21-70.3-38-93.6-48.5-90.6c-13.1,3.7-28.1,27.3-35.1,43.8c-9,21-10.8,33.6-6.1,63.5c4.7,29.9,7.5,60,11.8,76.4 c1,4,2.3,7.4,4,10.4c-33.2-22.8-55-60.7-55-103.5c0-69.7,57.7-126.3,128.9-126.3S128.9-69.7,128.9,0 C128.9,55.7,92.1,103,40.9,119.8z"/>
|
||||
<path class="st0" d="M-102.8-7.2l91.2-9.4l-0.3-7l-91.2,9.4L-102.8-7.2z"/>
|
||||
<path class="st0" d="M12-17.3c0.8-9.6-6.5-18-16.3-18.8s-18.4,6.4-19.2,16S-17-2.1-7.2-1.3S11.2-7.7,12-17.3z"/>
|
||||
<path class="st0" d="M62.9-24.6c0.8-9.6-6.5-18-16.3-18.8c-9.8-0.8-18.4,6.4-19.2,16c-0.8,9.6,6.5,18,16.3,18.8S62.1-15,62.9-24.6z "/>
|
||||
<path class="st0" d="M-11.8-16.8l67.6-7.3l-0.5-6.3l-67.5,7.3L-11.8-16.8z"/>
|
||||
<path class="st0" d="M53.1-23.6l49.5-12.2l-0.6-6.3L52.5-29.9L53.1-23.6z"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 2.2 KiB |
BIN
docs/public/opengist-demo.png
Normal file
BIN
docs/public/opengist-demo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.6 MiB |
58
docs/update.md
Normal file
58
docs/update.md
Normal file
@ -0,0 +1,58 @@
|
||||
# Update Opengist
|
||||
|
||||
## Make a backup
|
||||
|
||||
Before updating, always make sure to backup the Opengist home directory, where all the data is stored.
|
||||
|
||||
You can do so by copying the `~/.opengist` directory (default location).
|
||||
|
||||
```shell
|
||||
cp -r ~/.opengist ~/.opengist.bak
|
||||
```
|
||||
|
||||
## Install the new version
|
||||
|
||||
### With Docker
|
||||
|
||||
Pull the last version of Opengist
|
||||
```shell
|
||||
docker pull ghcr.io/thomiceli/opengist:1
|
||||
```
|
||||
|
||||
And restart the container, using `docker compose up -d` for example if you use docker compose.
|
||||
|
||||
### Via binary
|
||||
|
||||
Stop the running instance; then like your first installation of Opengist, download the archive for your system from the release page [here](https://github.com/thomiceli/opengist/releases/latest), and extract it.
|
||||
|
||||
```shell
|
||||
# example for linux amd64
|
||||
wget https://github.com/thomiceli/opengist/releases/download/v1.10.0/opengist1.10.0-linux-amd64.tar.gz
|
||||
|
||||
tar xzvf opengist1.10.0-linux-amd64.tar.gz
|
||||
cd opengist
|
||||
chmod +x opengist
|
||||
./opengist # with or without `--config config.yml`
|
||||
```
|
||||
|
||||
### From source
|
||||
|
||||
Stop the running instance; then pull the last changes from the master branch, and build the new version.
|
||||
|
||||
```shell
|
||||
git switch master
|
||||
git pull
|
||||
make
|
||||
./opengist
|
||||
```
|
||||
|
||||
## Restore the backup
|
||||
|
||||
If you have any issue with the new version, you can restore the backup you made before updating.
|
||||
|
||||
```shell
|
||||
rm -rf ~/.opengist
|
||||
cp -r ~/.opengist.bak ~/.opengist
|
||||
```
|
||||
|
||||
Then run the old version of Opengist again.
|
11
docs/usage/embed.md
Normal file
11
docs/usage/embed.md
Normal file
@ -0,0 +1,11 @@
|
||||
# Embed a Gist to your webpage
|
||||
|
||||
To embed a Gist to your webpage, you can add a script tag with the URL of your gist followed by `.js` to your HTML page:
|
||||
|
||||
```html
|
||||
<script src="http://opengist.url/user/gist-url.js"></script>
|
||||
|
||||
<!-- Dark mode: -->
|
||||
<script src="http://opengist.url/user/gist-url.js?dark"></script>
|
||||
```
|
||||
|
37
docs/usage/gist-json.md
Normal file
37
docs/usage/gist-json.md
Normal file
@ -0,0 +1,37 @@
|
||||
# Retrieve Gist as JSON
|
||||
|
||||
To retrieve a Gist as JSON, you can add `.json` to the end of the URL of your gist:
|
||||
|
||||
```shell
|
||||
curl http://opengist.url/thomas/my-gist.json | jq '.'
|
||||
```
|
||||
|
||||
It returns a JSON object with the following structure similar to this one:
|
||||
```json
|
||||
{
|
||||
"created_at": "2023-04-12T13:15:20+02:00",
|
||||
"description": "",
|
||||
"embed": {
|
||||
"css": "http://localhost:6157/assets/embed-94abc261.css",
|
||||
"html": "<div class=\"opengist-embed\" id=\"my-gist\">\n <div class=\"html \">\n \n <div class=\"rounded-md border-1 border-gray-100 dark:border-gray-800 overflow-auto mb-4\">\n <div class=\"border-b-1 border-gray-100 dark:border-gray-700 text-xs p-2 pl-4 bg-gray-50 dark:bg-gray-800 text-gray-400\">\n <a target=\"_blank\" href=\"http://localhost:6157/thomas/my-gist#file-hello-md\"><span class=\"font-bold text-gray-700 dark:text-gray-200\">hello.md</span> · 21 B · Markdown</a>\n <span class=\"float-right\"><a target=\"_blank\" href=\"http://localhost:6157\">Hosted via Opengist</a> · <span class=\"text-gray-700 dark:text-gray-200 font-bold\"><a target=\"_blank\" href=\"http://localhost:6157/thomas/my-gist/raw/HEAD/hello.md\">view raw</a></span></span>\n </div>\n \n \n \n <div class=\"chroma markdown markdown-body p-8\"><h1>Welcome to Opengist</h1>\n</div>\n \n\n </div>\n \n </div>\n</div>\n",
|
||||
"js": "http://localhost:6157/thomas/my-gist.js",
|
||||
"js_dark": "http://localhost:6157/thomas/my-gist.js?dark"
|
||||
},
|
||||
"files": [
|
||||
{
|
||||
"filename": "hello.md",
|
||||
"size": 21,
|
||||
"human_size": "21 B",
|
||||
"content": "# Welcome to Opengist",
|
||||
"truncated": false,
|
||||
"type": "Markdown"
|
||||
}
|
||||
],
|
||||
"id": "my-gist",
|
||||
"owner": "thomas",
|
||||
"title": "hello.md",
|
||||
"uuid": "8622b297bce54b408e36d546cef8019d",
|
||||
"visibility": "public"
|
||||
}
|
||||
```
|
||||
|
32
docs/usage/git-push-options.md
Normal file
32
docs/usage/git-push-options.md
Normal file
@ -0,0 +1,32 @@
|
||||
# Push Options
|
||||
|
||||
Opengist has support for a few [Git push options](https://git-scm.com/docs/git-push#Documentation/git-push.txt--oltoptiongt).
|
||||
|
||||
These options are passed to `git push` command and can be used to change the metadata of a gist.
|
||||
|
||||
## Set URL
|
||||
|
||||
```shell
|
||||
git push -o url=mygist # Will set the URL to https://opengist.example.com/user/mygist
|
||||
```
|
||||
|
||||
## Change title
|
||||
|
||||
```shell
|
||||
git push -o title=Gist123
|
||||
git push -o title="My Gist 123"
|
||||
```
|
||||
|
||||
## Change description
|
||||
|
||||
```shell
|
||||
git push -o description="This is my gist description"
|
||||
```
|
||||
|
||||
## Change visibility
|
||||
|
||||
```shell
|
||||
git push -o visibility=public
|
||||
git push -o visibility=unlisted
|
||||
git push -o visibility=private
|
||||
```
|
23
docs/usage/import-from-github-gist.md
Normal file
23
docs/usage/import-from-github-gist.md
Normal file
@ -0,0 +1,23 @@
|
||||
# Import Gists from GitHub
|
||||
|
||||
After running Opengist at least once, you can import your Gists from GitHub using this script:
|
||||
|
||||
```shell
|
||||
github_user=user # replace with your GitHub username
|
||||
opengist_url="http://user:password@opengist.url/init" # replace user, password and Opengist url
|
||||
|
||||
curl -s https://api.github.com/users/"$github_user"/gists?per_page=100 | jq '.[] | .git_pull_url' -r | while read url; do
|
||||
git clone "$url"
|
||||
repo_dir=$(basename "$url" .git)
|
||||
|
||||
# Add remote, push, and remove the directory
|
||||
if [ -d "$repo_dir" ]; then
|
||||
cd "$repo_dir"
|
||||
git remote add gist "$opengist_url"
|
||||
git push -u gist --all
|
||||
cd ..
|
||||
rm -rf "$repo_dir"
|
||||
fi
|
||||
done
|
||||
```
|
||||
|
42
docs/usage/init-via-git.md
Normal file
42
docs/usage/init-via-git.md
Normal file
@ -0,0 +1,42 @@
|
||||
# Init Gists via Git
|
||||
|
||||
Opengist allows you to create new snippets via Git over HTTP.
|
||||
|
||||
Simply init a new Git repository where your file(s) is/are located:
|
||||
|
||||
```shell
|
||||
git init
|
||||
git add .
|
||||
git commit -m "My cool snippet"
|
||||
```
|
||||
|
||||
Then add this Opengist special remote URL and push your changes:
|
||||
|
||||
```shell
|
||||
git remote add origin http://localhost:6157/init
|
||||
|
||||
git push -u origin master
|
||||
```
|
||||
|
||||
Log in with your Opengist account credentials, and your snippet will be created at the specified URL:
|
||||
|
||||
```shell
|
||||
Username for 'http://localhost:6157': thomas
|
||||
Password for 'http://thomas@localhost:6157':
|
||||
Enumerating objects: 3, done.
|
||||
Counting objects: 100% (3/3), done.
|
||||
Delta compression using up to 8 threads
|
||||
Compressing objects: 100% (2/2), done.
|
||||
Writing objects: 100% (3/3), 416 bytes | 416.00 KiB/s, done.
|
||||
Total 3 (delta 0), reused 0 (delta 0), pack-reused 0
|
||||
remote:
|
||||
remote: Your new repository has been created here: http://localhost:6157/thomas/6051e930f140429f9a2f3bb1fa101066
|
||||
remote:
|
||||
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:
|
||||
To http://localhost:6157/init
|
||||
* [new branch] master -> master
|
||||
```
|
||||
|
||||
<video controls="controls" src="https://github.com/thomiceli/opengist/assets/27960254/3fe1a0ba-b638-4928-83a1-f38e46fea066" />
|
@ -1,8 +0,0 @@
|
||||
//go:build fs_embed
|
||||
|
||||
package main
|
||||
|
||||
import "embed"
|
||||
|
||||
//go:embed templates/*/*.html public/manifest.json public/assets/*.js public/assets/*.css public/assets/*.svg
|
||||
var dirFS embed.FS
|
7
fs_os.go
7
fs_os.go
@ -1,7 +0,0 @@
|
||||
//go:build !fs_embed
|
||||
|
||||
package main
|
||||
|
||||
import "os"
|
||||
|
||||
var dirFS = os.DirFS(".")
|
147
go.mod
147
go.mod
@ -1,42 +1,129 @@
|
||||
module opengist
|
||||
module github.com/thomiceli/opengist
|
||||
|
||||
go 1.19
|
||||
go 1.23.0
|
||||
|
||||
require (
|
||||
github.com/go-playground/validator/v10 v10.11.0
|
||||
github.com/google/uuid v1.3.0
|
||||
github.com/gorilla/sessions v1.2.1
|
||||
github.com/labstack/echo/v4 v4.10.0
|
||||
github.com/markbates/goth v1.77.0
|
||||
github.com/mattn/go-sqlite3 v1.14.13
|
||||
github.com/rs/zerolog v1.29.0
|
||||
golang.org/x/crypto v0.2.0
|
||||
github.com/Kunde21/markdownfmt/v3 v3.1.0
|
||||
github.com/alecthomas/chroma/v2 v2.16.0
|
||||
github.com/blevesearch/bleve/v2 v2.5.0
|
||||
github.com/dustin/go-humanize v1.0.1
|
||||
github.com/glebarez/sqlite v1.11.0
|
||||
github.com/go-ldap/ldap/v3 v3.4.8
|
||||
github.com/go-playground/validator/v10 v10.26.0
|
||||
github.com/go-webauthn/webauthn v0.12.3
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/gorilla/schema v1.4.1
|
||||
github.com/gorilla/securecookie v1.1.2
|
||||
github.com/gorilla/sessions v1.4.0
|
||||
github.com/labstack/echo-contrib v0.17.3
|
||||
github.com/labstack/echo/v4 v4.13.3
|
||||
github.com/markbates/goth v1.81.0
|
||||
github.com/meilisearch/meilisearch-go v0.31.0
|
||||
github.com/pquerna/otp v1.4.0
|
||||
github.com/prometheus/client_golang v1.21.1
|
||||
github.com/rs/zerolog v1.34.0
|
||||
github.com/stretchr/testify v1.10.0
|
||||
github.com/urfave/cli/v2 v2.27.6
|
||||
github.com/yuin/goldmark v1.7.8
|
||||
github.com/yuin/goldmark-emoji v1.0.5
|
||||
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc
|
||||
go.abhg.dev/goldmark/mermaid v0.5.0
|
||||
golang.org/x/crypto v0.36.0
|
||||
golang.org/x/text v0.23.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
gorm.io/driver/sqlite v1.3.2
|
||||
gorm.io/gorm v1.23.5
|
||||
gorm.io/driver/mysql v1.5.7
|
||||
gorm.io/driver/postgres v1.5.11
|
||||
gorm.io/gorm v1.25.12
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/go-playground/locales v0.14.0 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.0 // indirect
|
||||
github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
|
||||
github.com/golang/protobuf v1.4.2 // indirect
|
||||
github.com/gorilla/context v1.1.1 // indirect
|
||||
github.com/gorilla/mux v1.6.2 // indirect
|
||||
github.com/gorilla/securecookie v1.1.1 // indirect
|
||||
filippo.io/edwards25519 v1.1.0 // indirect
|
||||
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 // indirect
|
||||
github.com/RoaringBitmap/roaring/v2 v2.4.5 // indirect
|
||||
github.com/andybalholm/brotli v1.1.1 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/bits-and-blooms/bitset v1.22.0 // indirect
|
||||
github.com/blevesearch/bleve_index_api v1.2.7 // indirect
|
||||
github.com/blevesearch/geo v0.1.20 // indirect
|
||||
github.com/blevesearch/go-faiss v1.0.25 // indirect
|
||||
github.com/blevesearch/go-porterstemmer v1.0.3 // indirect
|
||||
github.com/blevesearch/gtreap v0.1.1 // indirect
|
||||
github.com/blevesearch/mmap-go v1.0.4 // indirect
|
||||
github.com/blevesearch/scorch_segment_api/v2 v2.3.9 // indirect
|
||||
github.com/blevesearch/segment v0.9.1 // indirect
|
||||
github.com/blevesearch/snowballstem v0.9.0 // indirect
|
||||
github.com/blevesearch/upsidedown_store_api v1.0.2 // indirect
|
||||
github.com/blevesearch/vellum v1.1.0 // indirect
|
||||
github.com/blevesearch/zapx/v11 v11.4.1 // indirect
|
||||
github.com/blevesearch/zapx/v12 v12.4.1 // indirect
|
||||
github.com/blevesearch/zapx/v13 v13.4.1 // indirect
|
||||
github.com/blevesearch/zapx/v14 v14.4.1 // indirect
|
||||
github.com/blevesearch/zapx/v15 v15.4.1 // indirect
|
||||
github.com/blevesearch/zapx/v16 v16.2.2 // indirect
|
||||
github.com/boombuler/barcode v1.0.2 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.6 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/dlclark/regexp2 v1.11.5 // indirect
|
||||
github.com/fxamacker/cbor/v2 v2.8.0 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.8 // indirect
|
||||
github.com/glebarez/go-sqlite v1.22.0 // indirect
|
||||
github.com/go-asn1-ber/asn1-ber v1.5.5 // indirect
|
||||
github.com/go-chi/chi/v5 v5.2.1 // indirect
|
||||
github.com/go-playground/locales v0.14.1 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||
github.com/go-sql-driver/mysql v1.9.1 // indirect
|
||||
github.com/go-webauthn/x v0.1.20 // indirect
|
||||
github.com/golang-jwt/jwt/v4 v4.5.2 // indirect
|
||||
github.com/golang-jwt/jwt/v5 v5.2.2 // indirect
|
||||
github.com/golang/geo v0.0.0-20250404181303-07d601f131f3 // indirect
|
||||
github.com/golang/protobuf v1.5.4 // indirect
|
||||
github.com/golang/snappy v1.0.0 // indirect
|
||||
github.com/google/go-tpm v0.9.3 // indirect
|
||||
github.com/gorilla/mux v1.8.1 // indirect
|
||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
||||
github.com/jackc/pgx/v5 v5.7.4 // indirect
|
||||
github.com/jackc/puddle/v2 v2.2.2 // indirect
|
||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||
github.com/jinzhu/now v1.1.5 // indirect
|
||||
github.com/labstack/gommon v0.4.0 // indirect
|
||||
github.com/leodido/go-urn v1.2.1 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.17 // indirect
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/klauspost/compress v1.18.0 // indirect
|
||||
github.com/kr/text v0.2.0 // indirect
|
||||
github.com/labstack/gommon v0.4.2 // indirect
|
||||
github.com/leodido/go-urn v1.4.0 // indirect
|
||||
github.com/mailru/easyjson v0.9.0 // indirect
|
||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.16 // indirect
|
||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/mschoch/smat v0.2.0 // indirect
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||
github.com/ncruces/go-strftime v0.1.9 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/prometheus/client_model v0.6.1 // indirect
|
||||
github.com/prometheus/common v0.63.0 // indirect
|
||||
github.com/prometheus/procfs v0.16.0 // indirect
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||
github.com/rivo/uniseg v0.4.7 // indirect
|
||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||
github.com/valyala/fasttemplate v1.2.2 // indirect
|
||||
golang.org/x/net v0.4.0 // indirect
|
||||
golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43 // indirect
|
||||
golang.org/x/sys v0.5.0 // indirect
|
||||
golang.org/x/text v0.5.0 // indirect
|
||||
golang.org/x/time v0.2.0 // indirect
|
||||
google.golang.org/appengine v1.6.6 // indirect
|
||||
google.golang.org/protobuf v1.25.0 // indirect
|
||||
github.com/x448/float16 v0.8.4 // indirect
|
||||
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect
|
||||
go.etcd.io/bbolt v1.4.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 // indirect
|
||||
golang.org/x/net v0.38.0 // indirect
|
||||
golang.org/x/oauth2 v0.29.0 // indirect
|
||||
golang.org/x/sync v0.13.0 // indirect
|
||||
golang.org/x/sys v0.32.0 // indirect
|
||||
golang.org/x/time v0.11.0 // indirect
|
||||
google.golang.org/protobuf v1.36.6 // indirect
|
||||
modernc.org/libc v1.62.1 // indirect
|
||||
modernc.org/mathutil v1.7.1 // indirect
|
||||
modernc.org/memory v1.9.1 // indirect
|
||||
modernc.org/sqlite v1.37.0 // indirect
|
||||
)
|
||||
|
803
go.sum
803
go.sum
@ -1,498 +1,403 @@
|
||||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
|
||||
cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
|
||||
cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
|
||||
cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
|
||||
cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
|
||||
cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To=
|
||||
cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4=
|
||||
cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M=
|
||||
cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc=
|
||||
cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk=
|
||||
cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs=
|
||||
cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc=
|
||||
cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY=
|
||||
cloud.google.com/go v0.67.0/go.mod h1:YNan/mUhNZFrYUor0vqrsQ0Ffl7Xtm/ACOy/vsTS858=
|
||||
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
|
||||
cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
|
||||
cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
|
||||
cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg=
|
||||
cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=
|
||||
cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=
|
||||
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
|
||||
cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
|
||||
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
|
||||
cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
|
||||
cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=
|
||||
cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU=
|
||||
cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
|
||||
cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
|
||||
cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
|
||||
cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
|
||||
cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
|
||||
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
|
||||
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
|
||||
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
|
||||
github.com/coreos/go-systemd/v22 v22.3.3-0.20220203105225-a9a7ef127534/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
||||
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
||||
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 h1:mFRzDkZVAjdal+s7s0MwaRv9igoPqLRdzOLzw/8Xvq8=
|
||||
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU=
|
||||
github.com/Kunde21/markdownfmt/v3 v3.1.0 h1:KiZu9LKs+wFFBQKhrZJrFZwtLnCCWJahL+S+E/3VnM0=
|
||||
github.com/Kunde21/markdownfmt/v3 v3.1.0/go.mod h1:tPXN1RTyOzJwhfHoon9wUr4HGYmWgVxSQN6VBJDkrVc=
|
||||
github.com/RoaringBitmap/roaring/v2 v2.4.5 h1:uGrrMreGjvAtTBobc0g5IrW1D5ldxDQYe2JW2gggRdg=
|
||||
github.com/RoaringBitmap/roaring/v2 v2.4.5/go.mod h1:FiJcsfkGje/nZBZgCu0ZxCPOKD/hVXDS2dXi7/eUFE0=
|
||||
github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0=
|
||||
github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
|
||||
github.com/alecthomas/chroma/v2 v2.2.0/go.mod h1:vf4zrexSH54oEjJ7EdB65tGNHmH3pGZmVkgTP5RHvAs=
|
||||
github.com/alecthomas/chroma/v2 v2.16.0 h1:QC5ZMizk67+HzxFDjQ4ASjni5kWBTGiigRG1u23IGvA=
|
||||
github.com/alecthomas/chroma/v2 v2.16.0/go.mod h1:RVX6AvYm4VfYe/zsk7mjHueLDZor3aWCNE14TFlepBk=
|
||||
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.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
|
||||
github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa h1:LHTHcTQiSGT7VVbI0o4wBRNQIgn917usHWOd6VAffYI=
|
||||
github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4=
|
||||
github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA=
|
||||
github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA=
|
||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||
github.com/bits-and-blooms/bitset v1.12.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
|
||||
github.com/bits-and-blooms/bitset v1.22.0 h1:Tquv9S8+SGaS3EhyA+up3FXzmkhxPGjQQCkcs2uw7w4=
|
||||
github.com/bits-and-blooms/bitset v1.22.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
|
||||
github.com/blevesearch/bleve/v2 v2.5.0 h1:HzYqBy/5/M9Ul9ESEmXzN/3Jl7YpmWBdHM/+zzv/3k4=
|
||||
github.com/blevesearch/bleve/v2 v2.5.0/go.mod h1:PcJzTPnEynO15dCf9isxOga7YFRa/cMSsbnRwnszXUk=
|
||||
github.com/blevesearch/bleve_index_api v1.2.7 h1:c8r9vmbaYQroAMSGag7zq5gEVPiuXrUQDqfnj7uYZSY=
|
||||
github.com/blevesearch/bleve_index_api v1.2.7/go.mod h1:rKQDl4u51uwafZxFrPD1R7xFOwKnzZW7s/LSeK4lgo0=
|
||||
github.com/blevesearch/geo v0.1.20 h1:paaSpu2Ewh/tn5DKn/FB5SzvH0EWupxHEIwbCk/QPqM=
|
||||
github.com/blevesearch/geo v0.1.20/go.mod h1:DVG2QjwHNMFmjo+ZgzrIq2sfCh6rIHzy9d9d0B59I6w=
|
||||
github.com/blevesearch/go-faiss v1.0.25 h1:lel1rkOUGbT1CJ0YgzKwC7k+XH0XVBHnCVWahdCXk4U=
|
||||
github.com/blevesearch/go-faiss v1.0.25/go.mod h1:OMGQwOaRRYxrmeNdMrXJPvVx8gBnvE5RYrr0BahNnkk=
|
||||
github.com/blevesearch/go-porterstemmer v1.0.3 h1:GtmsqID0aZdCSNiY8SkuPJ12pD4jI+DdXTAn4YRcHCo=
|
||||
github.com/blevesearch/go-porterstemmer v1.0.3/go.mod h1:angGc5Ht+k2xhJdZi511LtmxuEf0OVpvUUNrwmM1P7M=
|
||||
github.com/blevesearch/gtreap v0.1.1 h1:2JWigFrzDMR+42WGIN/V2p0cUvn4UP3C4Q5nmaZGW8Y=
|
||||
github.com/blevesearch/gtreap v0.1.1/go.mod h1:QaQyDRAT51sotthUWAH4Sj08awFSSWzgYICSZ3w0tYk=
|
||||
github.com/blevesearch/mmap-go v1.0.4 h1:OVhDhT5B/M1HNPpYPBKIEJaD0F3Si+CrEKULGCDPWmc=
|
||||
github.com/blevesearch/mmap-go v1.0.4/go.mod h1:EWmEAOmdAS9z/pi/+Toxu99DnsbhG1TIxUoRmJw/pSs=
|
||||
github.com/blevesearch/scorch_segment_api/v2 v2.3.9 h1:X6nJXnNHl7nasXW+U6y2Ns2Aw8F9STszkYkyBfQ+p0o=
|
||||
github.com/blevesearch/scorch_segment_api/v2 v2.3.9/go.mod h1:IrzspZlVjhf4X29oJiEhBxEteTqOY9RlYlk1lCmYHr4=
|
||||
github.com/blevesearch/segment v0.9.1 h1:+dThDy+Lvgj5JMxhmOVlgFfkUtZV2kw49xax4+jTfSU=
|
||||
github.com/blevesearch/segment v0.9.1/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/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/go.mod h1:M01mh3Gpfy56Ps/UXHjEO/knbqyQ1Oamg8If49gRwrQ=
|
||||
github.com/blevesearch/vellum v1.1.0 h1:CinkGyIsgVlYf8Y2LUQHvdelgXr6PYuvoDIajq6yR9w=
|
||||
github.com/blevesearch/vellum v1.1.0/go.mod h1:QgwWryE8ThtNPxtgWJof5ndPfx0/YMBh+W2weHKPw8Y=
|
||||
github.com/blevesearch/zapx/v11 v11.4.1 h1:qFCPlFbsEdwbbckJkysptSQOsHn4s6ZOHL5GMAIAVHA=
|
||||
github.com/blevesearch/zapx/v11 v11.4.1/go.mod h1:qNOGxIqdPC1MXauJCD9HBG487PxviTUUbmChFOAosGs=
|
||||
github.com/blevesearch/zapx/v12 v12.4.1 h1:K77bhypII60a4v8mwvav7r4IxWA8qxhNjgF9xGdb9eQ=
|
||||
github.com/blevesearch/zapx/v12 v12.4.1/go.mod h1:QRPrlPOzAxBNMI0MkgdD+xsTqx65zbuPr3Ko4Re49II=
|
||||
github.com/blevesearch/zapx/v13 v13.4.1 h1:EnkEMZFUK0lsW/jOJJF2xOcp+W8TjEsyeN5BeAZEYYE=
|
||||
github.com/blevesearch/zapx/v13 v13.4.1/go.mod h1:e6duBMlCvgbH9rkzNMnUa9hRI9F7ri2BRcHfphcmGn8=
|
||||
github.com/blevesearch/zapx/v14 v14.4.1 h1:G47kGCshknBZzZAtjcnIAMn3oNx8XBLxp8DMq18ogyE=
|
||||
github.com/blevesearch/zapx/v14 v14.4.1/go.mod h1:O7sDxiaL2r2PnCXbhh1Bvm7b4sP+jp4unE9DDPWGoms=
|
||||
github.com/blevesearch/zapx/v15 v15.4.1 h1:B5IoTMUCEzFdc9FSQbhVOxAY+BO17c05866fNruiI7g=
|
||||
github.com/blevesearch/zapx/v15 v15.4.1/go.mod h1:b/MreHjYeQoLjyY2+UaM0hGZZUajEbE0xhnr1A2/Q6Y=
|
||||
github.com/blevesearch/zapx/v16 v16.2.2 h1:MifKJVRTEhMTgSlle2bDRTb39BGc9jXFRLPZc6r0Rzk=
|
||||
github.com/blevesearch/zapx/v16 v16.2.2/go.mod h1:B9Pk4G1CqtErgQV9DyCSA9Lb7WZe4olYfGw7fVDZ4sk=
|
||||
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
|
||||
github.com/boombuler/barcode v1.0.2 h1:79yrbttoZrLGkL/oOI8hBrUKucwOL0oOjUgEguGMcJ4=
|
||||
github.com/boombuler/barcode v1.0.2/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/chromedp/cdproto v0.0.0-20230220211738-2b1ec77315c9 h1:wMSvdj3BswqfQOXp2R1bJOAE7xIQLt2dlMQDMf836VY=
|
||||
github.com/chromedp/cdproto v0.0.0-20230220211738-2b1ec77315c9/go.mod h1:GKljq0VrfU4D5yc+2qA6OVr8pmO/MBbPEWqWQ/oqGEs=
|
||||
github.com/chromedp/chromedp v0.9.1 h1:CC7cC5p1BeLiiS2gfNNPwp3OaUxtRMBjfiw3E3k6dFA=
|
||||
github.com/chromedp/chromedp v0.9.1/go.mod h1:DUgZWRvYoEfgi66CgZ/9Yv+psgi+Sksy5DTScENWjaQ=
|
||||
github.com/chromedp/sysutil v1.0.0 h1:+ZxhTpfpZlmchB58ih/LBHX52ky7w2VhQVKQMucy3Ic=
|
||||
github.com/chromedp/sysutil v1.0.0/go.mod h1:kgWmDdq8fTzXYcKIBqIYvRRTnYb9aNS9moAV0xufSww=
|
||||
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.6 h1:XJtiaUW6dEEqVuZiMTn1ldk455QWwEIsMIJlo5vtkx0=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/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/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc=
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.0-20210816181553-5444fa50b93d/go.mod h1:tmAIfUFEirG/Y8jhZ9M+h36obRZAk/1fcSpXwAVlfqE=
|
||||
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
|
||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||
github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A=
|
||||
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||
github.com/go-playground/locales v0.14.0 h1:u50s323jtVGugKlcYeyzC0etD1HifMjqmJqb8WugfUU=
|
||||
github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs=
|
||||
github.com/go-playground/universal-translator v0.18.0 h1:82dyy6p4OuJq4/CByFNOn/jYrnRPArHwAcmLoJZxyho=
|
||||
github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA=
|
||||
github.com/go-playground/validator/v10 v10.11.0 h1:0W+xRM511GY47Yy3bZUbJVitCNg2BOGlCyvTqsp/xIw=
|
||||
github.com/go-playground/validator/v10 v10.11.0/go.mod h1:i+3WkQ1FvaUjjxh1kSvIA4dMGDBiPU55YFDl0WbKdWU=
|
||||
github.com/goccy/go-json v0.9.6/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||
github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
|
||||
github.com/dlclark/regexp2 v1.7.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||
github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ=
|
||||
github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||
github.com/fxamacker/cbor/v2 v2.8.0 h1:fFtUGXUzXPHTIUdne5+zzMPTfffl3RD5qYnkY40vtxU=
|
||||
github.com/fxamacker/cbor/v2 v2.8.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
|
||||
github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM=
|
||||
github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8=
|
||||
github.com/glebarez/go-sqlite v1.22.0 h1:uAcMJhaA6r3LHMTFgP0SifzgXg46yJkgxqyuyec+ruQ=
|
||||
github.com/glebarez/go-sqlite v1.22.0/go.mod h1:PlBIdHe0+aUEFn+r2/uthrWq4FxbzugL0L8Li6yQJbc=
|
||||
github.com/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GMw=
|
||||
github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ=
|
||||
github.com/go-asn1-ber/asn1-ber v1.5.5 h1:MNHlNMBDgEKD4TcKr36vQN68BA00aDfjIt3/bD50WnA=
|
||||
github.com/go-asn1-ber/asn1-ber v1.5.5/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
|
||||
github.com/go-chi/chi/v5 v5.2.1 h1:KOIHODQj58PmL80G2Eak4WdvUzjSJSm0vG72crDCqb8=
|
||||
github.com/go-chi/chi/v5 v5.2.1/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=
|
||||
github.com/go-ldap/ldap/v3 v3.4.8 h1:loKJyspcRezt2Q3ZRMq2p/0v8iOurlmeXDPw6fikSvQ=
|
||||
github.com/go-ldap/ldap/v3 v3.4.8/go.mod h1:qS3Sjlu76eHfHGpUdWkAXQTw4beih+cHsco2jXlIXrk=
|
||||
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||
github.com/go-playground/validator/v10 v10.26.0 h1:SP05Nqhjcvz81uJaRfEV0YBSSSGMc/iMaVtFbr3Sw2k=
|
||||
github.com/go-playground/validator/v10 v10.26.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
|
||||
github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
|
||||
github.com/go-sql-driver/mysql v1.9.1 h1:FrjNGn/BsJQjVRuSa8CBrM5BWA9BWoXXat3KrtSb/iI=
|
||||
github.com/go-sql-driver/mysql v1.9.1/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
|
||||
github.com/go-webauthn/webauthn v0.12.3 h1:hHQl1xkUuabUU9uS+ISNCMLs9z50p9mDUZI/FmkayNE=
|
||||
github.com/go-webauthn/webauthn v0.12.3/go.mod h1:4JRe8Z3W7HIw8NGEWn2fnUwecoDzkkeach/NnvhkqGY=
|
||||
github.com/go-webauthn/x v0.1.20 h1:brEBDqfiPtNNCdS/peu8gARtq8fIPsHz0VzpPjGvgiw=
|
||||
github.com/go-webauthn/x v0.1.20/go.mod h1:n/gAc8ssZJGATM0qThE+W+vfgXiMedsWi3wf/C4lld0=
|
||||
github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU=
|
||||
github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM=
|
||||
github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og=
|
||||
github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
|
||||
github.com/gobwas/ws v1.1.0 h1:7RFti/xnNkMJnrK7D1yQ/iCIB5OrrY/54/H930kIbHA=
|
||||
github.com/gobwas/ws v1.1.0/go.mod h1:nzvNcVha5eUziGrbxFCo6qFIojQHjJV5cLYIbezhfL0=
|
||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
|
||||
github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
|
||||
github.com/golang-jwt/jwt/v4 v4.2.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
|
||||
github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
|
||||
github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
|
||||
github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
|
||||
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
|
||||
github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
|
||||
github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
|
||||
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
|
||||
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
|
||||
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
|
||||
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
|
||||
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
|
||||
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
|
||||
github.com/golang/protobuf v1.4.2 h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0=
|
||||
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.2 h1:X2ev0eStA3AbceY54o37/0PQ/UWqKEiiO2dKL5OPaFM=
|
||||
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
|
||||
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
|
||||
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||
github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||
github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||
github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||
github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||
github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||
github.com/google/pprof v0.0.0-20200905233945-acf8798be1f7/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
||||
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
|
||||
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
|
||||
github.com/gorilla/context v1.1.1 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8=
|
||||
github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
|
||||
github.com/gorilla/mux v1.6.2 h1:Pgr17XVTNXAk3q/r4CpKzC5xBM/qW1uVLV+IhRZpIIk=
|
||||
github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
|
||||
github.com/gorilla/pat v0.0.0-20180118222023-199c85a7f6d1/go.mod h1:YeAe0gNeiNT5hoiZRI4yiOky6jVdNvfO2N6Kav/HmxY=
|
||||
github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ=
|
||||
github.com/golang-jwt/jwt/v4 v4.5.1/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
|
||||
github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI=
|
||||
github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
||||
github.com/golang/geo v0.0.0-20250404181303-07d601f131f3 h1:8COTSTFIIXnaD81+kfCw4dRANNAKuCp06EdYLqwX30g=
|
||||
github.com/golang/geo v0.0.0-20250404181303-07d601f131f3/go.mod h1:J+F9/3Ofc8ysEOY2/cNjxTMl2eB1gvPIywEHUplPgDA=
|
||||
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||
github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs=
|
||||
github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/go-tpm v0.9.3 h1:+yx0/anQuGzi+ssRqeD6WpXjW2L/V0dItUayO0i9sRc=
|
||||
github.com/google/go-tpm v0.9.3/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
|
||||
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs=
|
||||
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/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
|
||||
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
|
||||
github.com/gorilla/schema v1.4.1 h1:jUg5hUjCSDZpNGLuXQOgIWGdlgrIdYvgQ0wZtdK1M3E=
|
||||
github.com/gorilla/schema v1.4.1/go.mod h1:Dg5SSm5PV60mhF2NFaTV1xuYYj8tV8NOPRo4FggUMnM=
|
||||
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
|
||||
github.com/gorilla/sessions v1.1.1/go.mod h1:8KCfur6+4Mqcc6S0FEfKuN15Vl5MgXW92AE8ovaJD0w=
|
||||
github.com/gorilla/sessions v1.2.1 h1:DHd3rPN5lE3Ts3D8rKkQ8x/0kqfeNmBAaiSi+o7FsgI=
|
||||
github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA=
|
||||
github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo=
|
||||
github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
|
||||
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||
github.com/jarcoal/httpmock v0.0.0-20180424175123-9c70cfe4a1da/go.mod h1:ks+b9deReOc7jgqp+e7LuFiCBH6Rm5hL32cLcEAArb4=
|
||||
github.com/gorilla/sessions v1.4.0 h1:kpIYOp/oi6MG/p5PgxApU8srsSw9tuFbt46Lt7auzqQ=
|
||||
github.com/gorilla/sessions v1.4.0/go.mod h1:FLWm50oby91+hl7p/wRxDth9bWSuk0qVL2emc7lT5ik=
|
||||
github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
||||
github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8=
|
||||
github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
||||
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/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
||||
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
|
||||
github.com/jackc/pgx/v5 v5.7.4 h1:9wKznZrhWa2QiHL+NjTSPP6yjl3451BX3imWDnokYlg=
|
||||
github.com/jackc/pgx/v5 v5.7.4/go.mod h1:ncY89UGWxg82EykZUwSpUKEfccBGGYq1xjrOpsbsfGQ=
|
||||
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
|
||||
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
||||
github.com/jcmturner/aescts/v2 v2.0.0 h1:9YKLH6ey7H4eDBXW8khjYslgyqG2xZikXP0EQFKrle8=
|
||||
github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs=
|
||||
github.com/jcmturner/dnsutils/v2 v2.0.0 h1:lltnkeZGL0wILNvrNiVCR6Ro5PGU/SeBvVO/8c/iPbo=
|
||||
github.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM=
|
||||
github.com/jcmturner/gofork v1.7.6 h1:QH0l3hzAU1tfT3rZCnW5zXl+orbkNMMRGJfdJjHVETg=
|
||||
github.com/jcmturner/gofork v1.7.6/go.mod h1:1622LH6i/EZqLloHfE7IeZ0uEJwMSUyQ/nDd82IeqRo=
|
||||
github.com/jcmturner/goidentity/v6 v6.0.1 h1:VKnZd2oEIMorCTsFBnJWbExfNN7yZr3EhJAxwOkZg6o=
|
||||
github.com/jcmturner/goidentity/v6 v6.0.1/go.mod h1:X1YW3bgtvwAXju7V3LCIMpY0Gbxyjn/mY9zx4tFonSg=
|
||||
github.com/jcmturner/gokrb5/v8 v8.4.4 h1:x1Sv4HaTpepFkXbt2IkL29DXRf8sOfZXo8eRKh687T8=
|
||||
github.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs=
|
||||
github.com/jcmturner/rpc/v2 v2.0.3 h1:7FXXj8Ti1IaVFpSAziCZWNzbNuZmnvw/i6CqLNdWfZY=
|
||||
github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc=
|
||||
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
||||
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
||||
github.com/jinzhu/now v1.1.4/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
|
||||
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
|
||||
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
|
||||
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
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/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/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/labstack/echo/v4 v4.10.0 h1:5CiyngihEO4HXsz3vVsJn7f8xAlWwRr3aY6Ih280ZKA=
|
||||
github.com/labstack/echo/v4 v4.10.0/go.mod h1:S/T/5fy/GigaXnHTkh0ZGe4LpkkQysvRjFMSUTkDRNQ=
|
||||
github.com/labstack/gommon v0.4.0 h1:y7cvthEAEbU0yHOf4axH8ZG2NH8knB9iNSoTO8dyIk8=
|
||||
github.com/labstack/gommon v0.4.0/go.mod h1:uW6kP17uPlLJsD3ijUYn3/M5bAxtlZhMI6m3MFxTMTM=
|
||||
github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w=
|
||||
github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY=
|
||||
github.com/lestrrat-go/backoff/v2 v2.0.8/go.mod h1:rHP/q/r9aT27n24JQLa7JhSQZCKBBOiM/uP402WwN8Y=
|
||||
github.com/lestrrat-go/blackmagic v1.0.0/go.mod h1:TNgH//0vYSs8VXDCfkZLgIrVTTXQELZffUV0tz3MtdQ=
|
||||
github.com/lestrrat-go/httpcc v1.0.0/go.mod h1:tGS/u00Vh5N6FHNkExqGGNId8e0Big+++0Gf8MBnAvE=
|
||||
github.com/lestrrat-go/iter v1.0.1/go.mod h1:zIdgO1mRKhn8l9vrZJZz9TUMMFbQbLeTsbqPDrJ/OJc=
|
||||
github.com/lestrrat-go/jwx v1.2.21/go.mod h1:9cfxnOH7G1gN75CaJP2hKGcxFEx5sPh1abRIA/ZJVh4=
|
||||
github.com/lestrrat-go/option v1.0.0/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I=
|
||||
github.com/markbates/going v1.0.0/go.mod h1:I6mnB4BPnEeqo85ynXIx1ZFLLbtiLHNXVgWeFO9OGOA=
|
||||
github.com/markbates/goth v1.77.0 h1:s3scqnWv/Zq/a5M766V0FKsLfOdFNdh/HEkuWCKbvT8=
|
||||
github.com/markbates/goth v1.77.0/go.mod h1:X6xdNgpapSENS0O35iTBBcMHoJDQDfI9bJl+APCkYMc=
|
||||
github.com/mattn/go-colorable v0.1.11/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
|
||||
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
|
||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
|
||||
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
||||
github.com/labstack/echo-contrib v0.17.3 h1:hj+qXksKZG1scSe9ksUXMtv7fZYN+PtQT+bPcYA3/TY=
|
||||
github.com/labstack/echo-contrib v0.17.3/go.mod h1:TcRBrzW8jcC4JD+5Dc/pvOyAps0rtgzj7oBqoR3nYsc=
|
||||
github.com/labstack/echo/v4 v4.13.3 h1:pwhpCPrTl5qry5HRdM5FwdXnhXSLSY+WE+YQSeCaafY=
|
||||
github.com/labstack/echo/v4 v4.13.3/go.mod h1:o90YNEeQWjDozo584l7AwhJMHN0bOC4tAfg+Xox9q5g=
|
||||
github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0=
|
||||
github.com/labstack/gommon v0.4.2/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/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
||||
github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4=
|
||||
github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU=
|
||||
github.com/markbates/goth v1.81.0 h1:XVcCkeGWokynPV7MXvgb8pd2s3r7DS40P7931w6kdnE=
|
||||
github.com/markbates/goth v1.81.0/go.mod h1:+6z31QyUms84EHmuBY7iuqYSxyoN3njIgg9iCF/lR1k=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
|
||||
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.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng=
|
||||
github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-sqlite3 v1.14.12/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
|
||||
github.com/mattn/go-sqlite3 v1.14.13 h1:1tj15ngiFfcZzii7yd82foL+ks+ouQcj8j/TPq3fk1I=
|
||||
github.com/mattn/go-sqlite3 v1.14.13/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
|
||||
github.com/mrjones/oauth v0.0.0-20180629183705-f4e24b6d100c/go.mod h1:skjdDftzkFALcuGzYSklqYd8gvat6F1gZJ4YPVbkZpM=
|
||||
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
||||
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
|
||||
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
github.com/meilisearch/meilisearch-go v0.31.0 h1:yZRhY1qJqdH8h6GFZALGtkDLyj8f9v5aJpsNMyrUmnY=
|
||||
github.com/meilisearch/meilisearch-go v0.31.0/go.mod h1:aNtyuwurDg/ggxQIcKqWH6G9g2ptc8GyY7PLY4zMn/g=
|
||||
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
||||
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||
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/go.mod h1:kc9mz7DoBKqDyiRL7VZN8KvXQMWeTaVnttLRXOlotKw=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
|
||||
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
||||
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/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
|
||||
github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8=
|
||||
github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
|
||||
github.com/rs/xid v1.4.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
|
||||
github.com/rs/zerolog v1.29.0 h1:Zes4hju04hjbvkVkOhdl2HpZa+0PmVwigmo8XoORE5w=
|
||||
github.com/rs/zerolog v1.29.0/go.mod h1:NILgTygv/Uej1ra5XxGf82ZFSLk58MFGAUS2o6usyD0=
|
||||
github.com/pquerna/otp v1.4.0 h1:wZvl1TIVxKRThZIBiwOOHOGP/1+nZyWBil9Y2XNEDzg=
|
||||
github.com/pquerna/otp v1.4.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg=
|
||||
github.com/prometheus/client_golang v1.21.1 h1:DOvXXTqVzvkIewV/CDPFdejpMCGeMcbGCQ8YOmu+Ibk=
|
||||
github.com/prometheus/client_golang v1.21.1/go.mod h1:U9NM32ykUErtVBxdvD3zfi+EuFkkaBvMb09mIfe0Zgg=
|
||||
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
|
||||
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
|
||||
github.com/prometheus/common v0.63.0 h1:YR/EIY1o3mEFP/kZCD7iDMnLPlGyuU2Gb3HIcXnA98k=
|
||||
github.com/prometheus/common v0.63.0/go.mod h1:VVFF/fBIoToEnWRVkYoXEkq3R3paCoxG9PXP74SnV18=
|
||||
github.com/prometheus/procfs v0.16.0 h1:xh6oHhKwnOJKMYiYBDWmkHqQPyiY40sny36Cmx2bbsM=
|
||||
github.com/prometheus/procfs v0.16.0/go.mod h1:8veyXUu3nGP7oaCxhX6yeaM5u4stL2FeMXnCqhDthZg=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/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/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
|
||||
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
|
||||
github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY=
|
||||
github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ=
|
||||
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/urfave/cli/v2 v2.27.6 h1:VdRdS98FNhKZ8/Az8B7MTyGQmpIr36O1EHybx/LaZ4g=
|
||||
github.com/urfave/cli/v2 v2.27.6/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5hrMvTQ=
|
||||
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||
github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
|
||||
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/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
|
||||
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
|
||||
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||
go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
|
||||
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
|
||||
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4=
|
||||
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/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.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
github.com/yuin/goldmark v1.4.15/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
github.com/yuin/goldmark v1.7.1/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
|
||||
github.com/yuin/goldmark v1.7.8 h1:iERMLn0/QJeHFhxSt3p6PeN9mGnvIKSpG9YYorDMnic=
|
||||
github.com/yuin/goldmark v1.7.8/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
|
||||
github.com/yuin/goldmark-emoji v1.0.5 h1:EMVWyCGPlXJfUXBXpuMu+ii3TIaxbVBnEX9uaDC4cIk=
|
||||
github.com/yuin/goldmark-emoji v1.0.5/go.mod h1:tTkZEbwu5wkPmgTcitqddVxY9osFZiavD+r4AzQrh1U=
|
||||
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc h1:+IAOyRda+RLrxa1WC7umKOZRsGq4QrFFMYApOeHzQwQ=
|
||||
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc/go.mod h1:ovIvrum6DQJA4QsJSovrkC4saKHQVs7TvcaeO8AIl5I=
|
||||
go.abhg.dev/goldmark/mermaid v0.5.0 h1:mDkykpSPJ+5wCQ8bSXgzJ2KQskjXkI5Ndxz7JYDHW38=
|
||||
go.abhg.dev/goldmark/mermaid v0.5.0/go.mod h1:OCyk2o85TX2drWHH+HRy6bih2yZlUwbbv/R1MMh1YLs=
|
||||
go.etcd.io/bbolt v1.4.0 h1:TU77id3TnN/zKr7CO/uk+fBCwF2jGcMuw2B/FMAzYIk=
|
||||
go.etcd.io/bbolt v1.4.0/go.mod h1:AsD+OCi/qPN1giOX1aiLAha3o1U8rAz65bvN4j0sRuk=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.2.0 h1:BRXPfhNivWL5Yq0BGQ39a2sW6t44aODpfxkWjYdzewE=
|
||||
golang.org/x/crypto v0.2.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
||||
golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
|
||||
golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
|
||||
golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
|
||||
golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
|
||||
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
|
||||
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
|
||||
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
|
||||
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
||||
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
|
||||
golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
||||
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
||||
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
|
||||
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
|
||||
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
|
||||
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
|
||||
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||
golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
|
||||
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
|
||||
golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
|
||||
golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
|
||||
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 h1:nDVHiLt8aIbd/VzvPWN6kSOPE7+F/fNFDSXLVYkE/Iw=
|
||||
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394/go.mod h1:sIifuuw/Yco/y6yb6+bDNfyeQ/MdPUy/hKEMYQV17cM=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU=
|
||||
golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20200927032502-5d4f70055728/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.4.0 h1:Q5QPcMlvfxFTAPV0+07Xz/MpK9NTXu2VDUuy0FeMfaU=
|
||||
golang.org/x/net v0.4.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43 h1:ld7aEMNHoBnnDAX15v1T6z31v8HwR2A9FYOuAhWqkwc=
|
||||
golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||
golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
|
||||
golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8=
|
||||
golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
|
||||
golang.org/x/oauth2 v0.29.0 h1:WdYw2tdTK1S8olAzWHdgeqfy+Mtm9XNhv/xJsY65d98=
|
||||
golang.org/x/oauth2 v0.29.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610=
|
||||
golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211103235746-7861aae1554b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
|
||||
golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.3.0 h1:qoo4akIqOcDME5bhc/NgxUdovd6BSS2uMsVjB56q1xI=
|
||||
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
||||
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
||||
golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58=
|
||||
golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y=
|
||||
golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.5.0 h1:OLmvp0KP+FVG99Ct/qFiL/Fhk4zp4QQnZ7b2U+5piUM=
|
||||
golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.2.0 h1:52I/1L54xyEQAYdtcSuxtiT84KGYTBGXwayxmIpNJhE=
|
||||
golang.org/x/time v0.2.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
|
||||
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
|
||||
golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0=
|
||||
golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
|
||||
golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
|
||||
golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
|
||||
golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
||||
golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
||||
golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
||||
golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE=
|
||||
golang.org/x/tools v0.0.0-20200929161345-d7fc70abf50f/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/tools v0.31.0 h1:0EedkvKDbh+qistFTd0Bcwe/YLh4vHwWEkiI0toFIBU=
|
||||
golang.org/x/tools v0.31.0/go.mod h1:naFTU+Cev749tSJRXJlna0T3WxKvb1kWEx15xA4SdmQ=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
|
||||
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
|
||||
google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
|
||||
google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
|
||||
google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
|
||||
google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
|
||||
google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
|
||||
google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
|
||||
google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
|
||||
google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
|
||||
google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
|
||||
google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
|
||||
google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
|
||||
google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
|
||||
google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM=
|
||||
google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=
|
||||
google.golang.org/api v0.32.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
|
||||
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||
google.golang.org/appengine v1.6.6 h1:lMO5rYAqUxkmaj76jAkRUvt5JZgFymx/+Q5Mzfivuhc=
|
||||
google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||
google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
|
||||
google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA=
|
||||
google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U=
|
||||
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
|
||||
google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=
|
||||
google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20200929141702-51c3e5b607fe/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
|
||||
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
|
||||
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
|
||||
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
|
||||
google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||
google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||
google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60=
|
||||
google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
|
||||
google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
|
||||
google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
|
||||
google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
|
||||
google.golang.org/grpc v1.32.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
|
||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
||||
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
|
||||
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
|
||||
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
|
||||
google.golang.org/protobuf v1.25.0 h1:Ejskq+SyPohKW+1uil0JJMtmHCgJPJ/qWTxr8qp+R4c=
|
||||
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
|
||||
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
|
||||
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/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/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gorm.io/driver/sqlite v1.3.2 h1:nWTy4cE52K6nnMhv23wLmur9Y3qWbZvOBz+V4PrGAxg=
|
||||
gorm.io/driver/sqlite v1.3.2/go.mod h1:B+8GyC9K7VgzJAcrcXMRPdnMcck+8FgJynEehEPM16U=
|
||||
gorm.io/gorm v1.23.4/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk=
|
||||
gorm.io/gorm v1.23.5 h1:TnlF26wScKSvknUC/Rn8t0NLLM22fypYBlvj1+aH6dM=
|
||||
gorm.io/gorm v1.23.5/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk=
|
||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
|
||||
honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
|
||||
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
|
||||
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
|
||||
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
|
||||
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
|
||||
gorm.io/driver/mysql v1.5.7 h1:MndhOPYOfEp2rHKgkZIhJ16eVUIRf2HmzgoPmh7FCWo=
|
||||
gorm.io/driver/mysql v1.5.7/go.mod h1:sEtPWMiqiN1N1cMXoXmBbd8C6/l+TESwriotuRRpkDM=
|
||||
gorm.io/driver/postgres v1.5.11 h1:ubBVAfbKEUld/twyKZ0IYn9rSQh448EdelLYk9Mv314=
|
||||
gorm.io/driver/postgres v1.5.11/go.mod h1:DX3GReXH+3FPWGrrgffdvCk3DQ1dwDPdmbenSkweRGI=
|
||||
gorm.io/gorm v1.25.7/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
|
||||
gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8=
|
||||
gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ=
|
||||
modernc.org/cc/v4 v4.25.2 h1:T2oH7sZdGvTaie0BRNFbIYsabzCxUQg8nLqCdQ2i0ic=
|
||||
modernc.org/cc/v4 v4.25.2/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
|
||||
modernc.org/ccgo/v4 v4.25.1 h1:TFSzPrAGmDsdnhT9X2UrcPMI3N/mJ9/X9ykKXwLhDsU=
|
||||
modernc.org/ccgo/v4 v4.25.1/go.mod h1:njjuAYiPflywOOrm3B7kCB444ONP5pAVr8PIEoE0uDw=
|
||||
modernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE=
|
||||
modernc.org/fileutil v1.3.0/go.mod h1:XatxS8fZi3pS8/hKG2GH/ArUogfxjpEKs3Ku3aK4JyQ=
|
||||
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
|
||||
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
|
||||
modernc.org/libc v1.62.1 h1:s0+fv5E3FymN8eJVmnk0llBe6rOxCu/DEU+XygRbS8s=
|
||||
modernc.org/libc v1.62.1/go.mod h1:iXhATfJQLjG3NWy56a6WVU73lWOcdYVxsvwCgoPljuo=
|
||||
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
|
||||
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
|
||||
modernc.org/memory v1.9.1 h1:V/Z1solwAVmMW1yttq3nDdZPJqV1rM05Ccq6KMSZ34g=
|
||||
modernc.org/memory v1.9.1/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.37.0 h1:s1TMe7T3Q3ovQiK2Ouz4Jwh7dw4ZDqbebSDTlSJdfjI=
|
||||
modernc.org/sqlite v1.37.0/go.mod h1:5YiWv+YviqGMuGw4V+PNplcyaJ5v+vQd7TQOgkACoJM=
|
||||
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/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/
|
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.5.6
|
||||
- name: meilisearch
|
||||
repository: https://meilisearch.github.io/meilisearch-kubernetes
|
||||
version: 0.12.0
|
||||
digest: sha256:31084e570aa16e3a26317aeb6d0d5dec62540c314ee4f703374e6e7827399fa6
|
||||
generated: "2025-03-27T11:34:51.840778733+01: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.2.0
|
||||
appVersion: 1.10.0
|
||||
home: https://opengist.io
|
||||
icon: https://raw.githubusercontent.com/thomiceli/opengist/master/public/opengist.svg
|
||||
sources:
|
||||
- https://github.com/thomiceli/opengist
|
||||
dependencies:
|
||||
- name: postgresql
|
||||
repository: oci://registry-1.docker.io/bitnamicharts
|
||||
version: 16.5.6
|
||||
condition: postgresql.enabled
|
||||
- name: meilisearch
|
||||
repository: https://meilisearch.github.io/meilisearch-kubernetes
|
||||
version: 0.12.0
|
||||
condition: meilisearch.enabled
|
81
helm/opengist/README.md
Normal file
81
helm/opengist/README.md
Normal file
@ -0,0 +1,81 @@
|
||||
# Opengist Helm Chart
|
||||
|
||||
 
|
||||
|
||||
Opengist Helm chart for Kubernetes.
|
||||
|
||||
* [Install](#install)
|
||||
* [Configuration](#configuration)
|
||||
* [Dependencies](#dependencies)
|
||||
* [Meilisearch Indexer](#meilisearch-indexer)
|
||||
* [PostgreSQL Database](#postgresql-database)
|
||||
|
||||
## Install
|
||||
|
||||
```bash
|
||||
helm repo add opengist https://helm.opengist.io
|
||||
|
||||
helm install opengist opengist/opengist
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
This part explains how to configure the Opengist instance using the Helm chart. The `config.yml` file used by Opengist
|
||||
is mounted from a Kubernetes Secret with a key `config.yml` and the values formatted as YAML.
|
||||
|
||||
### Using values
|
||||
|
||||
Using Helm values, you can define the values from a key name `config`
|
||||
|
||||
```yaml
|
||||
config:
|
||||
log-level: "warn"
|
||||
log-output: "stdout"
|
||||
```
|
||||
|
||||
This will create a Kubernetes secret named `opengist` mounted to the pod as a file with the YAML content of the secret,
|
||||
used by Opengist.
|
||||
|
||||
### Using an existing secret
|
||||
|
||||
If you wish to not store sensitive data in your Helm values, you can create a Kubernetes secret with a key `config.yml`
|
||||
and values formatted as YAML. You can then reference this secret in the Helm chart with the `configExistingSecret` key.
|
||||
|
||||
If defined, this existing secret will be used instead of creating a new one.
|
||||
|
||||
```yaml
|
||||
configExistingSecret: <name of the secret>
|
||||
```
|
||||
|
||||
## Dependencies
|
||||
|
||||
### Meilisearch Indexer
|
||||
|
||||
By default, Opengist uses the `bleve` indexer. **It is NOT available** if there is multiple replicas of the opengist pod (only one pod can open the index at the same time).
|
||||
|
||||
Instead, for multiple replicas setups, you **MUST** use the `meilisearch` indexer.
|
||||
|
||||
By setting `meilisearch.enabled: true`, the [Meilisearch chart](https://github.com/meilisearch/meilisearch-kubernetes) will be deployed aswell.
|
||||
You must define the `meilisearch.host` (Kubernetes Service) and `meilisearch.key` (value created by Meilisearch) values to connect to the Meilisearch instance in your Opengist config :
|
||||
|
||||
```yaml
|
||||
index: meilisearch
|
||||
index.meili.host: http://opengist-meilisearch:7700 # pointing to the K8S Service
|
||||
index.meili.api-key: MASTER_KEY # generated by Meilisearch
|
||||
```
|
||||
|
||||
If you want to use the `bleve` indexer, you need to set the `replicas` to `1`.
|
||||
|
||||
### PostgreSQL Database
|
||||
|
||||
By default, Opengist uses the `sqlite` database. If needed, this chart also deploys a PostgreSQL instance.
|
||||
|
||||
By setting `postgresql.enabled: true`, the [Bitnami PostgreSQL chart](https://github.com/bitnami/charts/tree/main/bitnami/postgresql) will be deployed aswell.
|
||||
You must define the `postgresql.host`, `postgresql.port`, `postgresql.database`, `postgresql.username` and `postgresql.password` values to connect to the PostgreSQL instance.
|
||||
|
||||
Then define the connection string in your Opengist config:
|
||||
|
||||
```yaml
|
||||
db-uri: postgres://user:password@opengist-postgresql:5432/opengist
|
||||
```
|
||||
Note: `opengist-postgresql` is the name of the K8S Service deployed by this chart.
|
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 -}}
|
122
helm/opengist/templates/deployment.yaml
Normal file
122
helm/opengist/templates/deployment.yaml
Normal file
@ -0,0 +1,122 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: {{ include "opengist.fullname" . }}
|
||||
namespace: {{ .Values.namespace | default .Release.Namespace }}
|
||||
labels:
|
||||
{{- include "opengist.labels" . | nindent 4 }}
|
||||
{{- if .Values.deployment.labels }}
|
||||
{{- toYaml .Values.deployment.labels | nindent 4 }}
|
||||
{{- end }}
|
||||
{{- with .Values.deployment.annotations }}
|
||||
annotations:
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
spec:
|
||||
{{- if not .Values.autoscaling.enabled }}
|
||||
replicas: {{ .Values.replicaCount }}
|
||||
{{- end }}
|
||||
selector:
|
||||
matchLabels:
|
||||
{{- include "opengist.selectorLabels" . | nindent 6 }}
|
||||
template:
|
||||
metadata:
|
||||
annotations:
|
||||
checksum/config: {{ include (print $.Template.BasePath "/secret.yaml") . | sha256sum }}
|
||||
{{- with .Values.podAnnotations }}
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
labels:
|
||||
{{- include "opengist.labels" . | nindent 8 }}
|
||||
{{- with .Values.podLabels }}
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
spec:
|
||||
{{- if .Values.deployment.terminationGracePeriodSeconds }}
|
||||
terminationGracePeriodSeconds: {{ .Values.deployment.terminationGracePeriodSeconds }}
|
||||
{{- end }}
|
||||
{{- with .Values.imagePullSecrets }}
|
||||
imagePullSecrets:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
serviceAccountName: {{ include "opengist.serviceAccountName" . }}
|
||||
securityContext:
|
||||
{{- toYaml .Values.podSecurityContext | nindent 8 }}
|
||||
initContainers:
|
||||
- name: init-config
|
||||
image: busybox:1.37
|
||||
imagePullPolicy: IfNotPresent
|
||||
command: ['sh', '-c', 'cp /init/config/config.yml /config-volume/config.yml']
|
||||
volumeMounts:
|
||||
- name: config-secret
|
||||
mountPath: /init/config
|
||||
- name: config-volume
|
||||
mountPath: /config-volume
|
||||
{{- if .Values.deployment.env }}
|
||||
env:
|
||||
{{- toYaml .Values.deployment.env | nindent 12 }}
|
||||
{{- end }}
|
||||
containers:
|
||||
- name: {{ .Chart.Name }}
|
||||
securityContext:
|
||||
{{- toYaml .Values.securityContext | nindent 12 }}
|
||||
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
|
||||
imagePullPolicy: {{ .Values.image.pullPolicy }}
|
||||
ports:
|
||||
- name: http
|
||||
containerPort: {{ .Values.service.http.port }}
|
||||
protocol: TCP
|
||||
{{- if .Values.livenessProbe.enabled }}
|
||||
livenessProbe:
|
||||
{{- toYaml (omit .Values.livenessProbe "enabled") | nindent 12 }}
|
||||
httpGet:
|
||||
port: http
|
||||
path: /healthcheck
|
||||
{{- end }}
|
||||
{{- if .Values.readinessProbe.enabled }}
|
||||
readinessProbe:
|
||||
{{- toYaml (omit .Values.readinessProbe "enabled") | nindent 12 }}
|
||||
httpGet:
|
||||
port: http
|
||||
path: /healthcheck
|
||||
{{- end }}
|
||||
resources:
|
||||
{{- toYaml .Values.resources | nindent 12 }}
|
||||
volumeMounts:
|
||||
- name: config-volume
|
||||
mountPath: /config.yml
|
||||
subPath: config.yml
|
||||
- name: opengist-data
|
||||
mountPath: /opengist
|
||||
{{- if gt (len .Values.extraVolumeMounts) 0 }}
|
||||
{{- toYaml .Values.extraVolumeMounts | nindent 12 }}
|
||||
{{- end }}
|
||||
volumes:
|
||||
- name: opengist-data
|
||||
{{- if .Values.persistence.enabled }}
|
||||
persistentVolumeClaim:
|
||||
claimName: {{ include "opengist.fullname" . }}-data
|
||||
{{- else }}
|
||||
emptyDir: {}
|
||||
{{- end }}
|
||||
- name: config-secret
|
||||
secret:
|
||||
secretName: {{ include "opengist.secretName" . }}
|
||||
defaultMode: 511
|
||||
- name: config-volume
|
||||
emptyDir: {}
|
||||
{{- if gt (len .Values.extraVolumes) 0 }}
|
||||
{{- toYaml .Values.extraVolumes | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with .Values.nodeSelector }}
|
||||
nodeSelector:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with .Values.affinity }}
|
||||
affinity:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with .Values.tolerations }}
|
||||
tolerations:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
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 -}}
|
28
helm/opengist/templates/pvc.yaml
Normal file
28
helm/opengist/templates/pvc.yaml
Normal file
@ -0,0 +1,28 @@
|
||||
{{- if .Values.persistence.enabled }}
|
||||
kind: PersistentVolumeClaim
|
||||
apiVersion: v1
|
||||
metadata:
|
||||
name: {{ include "opengist.fullname" . }}-data
|
||||
namespace: {{ .Release.Namespace }}
|
||||
{{- with .Values.persistence.annotations }}
|
||||
annotations:
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
labels:
|
||||
{{- include "opengist.labels" . | nindent 4 }}
|
||||
{{- if .Values.persistence.labels }}
|
||||
{{- toYaml .Values.persistence.labels | nindent 4 }}
|
||||
{{- end }}
|
||||
spec:
|
||||
accessModes:
|
||||
{{- if gt .Values.replicaCount 1.0 }}
|
||||
- ReadWriteMany
|
||||
{{- else }}
|
||||
{{- .Values.persistence.accessModes | toYaml | nindent 4 }}
|
||||
{{- end }}
|
||||
volumeMode: Filesystem
|
||||
storageClassName: {{ .Values.persistence.storageClass | quote }}
|
||||
resources:
|
||||
requests:
|
||||
storage: {{ .Values.persistence.size }}
|
||||
{{- end }}
|
13
helm/opengist/templates/secret.yaml
Normal file
13
helm/opengist/templates/secret.yaml
Normal file
@ -0,0 +1,13 @@
|
||||
{{- if (not .Values.configExistingSecret) }}
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: {{ include "opengist.fullname" . }}
|
||||
namespace: {{ .Release.Namespace }}
|
||||
labels:
|
||||
{{ include "opengist.labels" . | indent 4 }}
|
||||
type: Opaque
|
||||
stringData:
|
||||
config.yml: |-
|
||||
{{- .Values.config | toYaml | nindent 4 }}
|
||||
{{- end }}
|
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 }}
|
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 }}
|
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
|
201
helm/opengist/values.yaml
Normal file
201
helm/opengist/values.yaml
Normal file
@ -0,0 +1,201 @@
|
||||
## Kubernetes workload configuration for Opengist
|
||||
nameOverride: ""
|
||||
fullnameOverride: ""
|
||||
namespace: ""
|
||||
|
||||
## Opengist YAML Application Config. See more at https://opengist.io/docs/configuration/cheat-sheet.html
|
||||
## This will create a Kubernetes secret with the key `config.yml` containing the YAML configuration mounted in the pod.
|
||||
config:
|
||||
log-level: "warn"
|
||||
log-output: "stdout"
|
||||
|
||||
## If defined, the existing secret will be used instead of creating a new one.
|
||||
## The secret must contain a key named `config.yml` with the YAML configuration.
|
||||
configExistingSecret: ""
|
||||
|
||||
## Define the image repository and tag to use.
|
||||
image:
|
||||
repository: ghcr.io/thomiceli/opengist
|
||||
pullPolicy: Always
|
||||
tag: "1.10.0"
|
||||
digest: ""
|
||||
imagePullSecrets: []
|
||||
# - name: "image-pull-secret"
|
||||
|
||||
## Define the deployment replica count
|
||||
replicaCount: 1
|
||||
|
||||
## Define the deployment strategy type
|
||||
strategy:
|
||||
type: "RollingUpdate"
|
||||
rollingUpdate:
|
||||
maxSurge: "100%"
|
||||
maxUnavailable: 0
|
||||
|
||||
## Security Context settings
|
||||
## ref: https://kubernetes.io/docs/tasks/configure-pod-container/security-context/
|
||||
podSecurityContext:
|
||||
fsGroup: 1000
|
||||
securityContext: {}
|
||||
# allowPrivilegeEscalation: false
|
||||
|
||||
## Pod Disruption Budget settings
|
||||
## ref: https://kubernetes.io/docs/tasks/run-application/configure-pdb/
|
||||
podDisruptionBudget: {}
|
||||
# maxUnavailable: 1
|
||||
# minAvailable: 1
|
||||
|
||||
## Set the Kubernetes service type
|
||||
## ref: https://kubernetes.io/docs/concepts/services-networking/service/
|
||||
service:
|
||||
http:
|
||||
type: ClusterIP
|
||||
clusterIP:
|
||||
port: 6157
|
||||
nodePort:
|
||||
loadBalancerIP:
|
||||
externalIPs: []
|
||||
labels: {}
|
||||
annotations: {}
|
||||
loadBalancerSourceRanges: []
|
||||
externalTrafficPolicy:
|
||||
|
||||
ssh:
|
||||
enabled: true
|
||||
type: ClusterIP
|
||||
clusterIP:
|
||||
port: 2222
|
||||
nodePort:
|
||||
loadBalancerIP:
|
||||
externalIPs: []
|
||||
labels: {}
|
||||
annotations: {}
|
||||
loadBalancerSourceRanges: []
|
||||
externalTrafficPolicy:
|
||||
|
||||
## HTTP Ingress for Opengist
|
||||
## ref: https://kubernetes.io/docs/concepts/services-networking/ingress/
|
||||
ingress:
|
||||
enabled: false
|
||||
className: ""
|
||||
labels: {}
|
||||
# node-role.kubernetes.io/ingress: platform
|
||||
annotations: {}
|
||||
# kubernetes.io/ingress.class: nginx
|
||||
hosts:
|
||||
- host: opengist.example.com
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
tls: []
|
||||
# - secretName: opengist-tls
|
||||
# hosts:
|
||||
# - opengist.example.com
|
||||
|
||||
## Service Account for Opengist pods
|
||||
## ref: https://kubernetes.io/docs/concepts/security/service-accounts/
|
||||
serviceAccount:
|
||||
create: true
|
||||
annotations: {}
|
||||
name: ""
|
||||
|
||||
## Set persistence using a Persistent Volume Claim
|
||||
## If more than 2 replicas are set, the access mode must be ReadWriteMany
|
||||
## ref: https://kubernetes.io/docs/concepts/storage/persistent-volumes/
|
||||
persistence:
|
||||
enabled: true
|
||||
existingClaim: ""
|
||||
storageClass: ""
|
||||
labels: {}
|
||||
annotations:
|
||||
helm.sh/resource-policy: keep
|
||||
size: 5Gi
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
subPath: ""
|
||||
|
||||
extraVolumes: []
|
||||
extraVolumeMounts: []
|
||||
|
||||
## Additional pod labels and annotations
|
||||
podLabels: {}
|
||||
podAnnotations: {}
|
||||
|
||||
## Configure resource requests and limits
|
||||
## ref: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/
|
||||
resources: {}
|
||||
# limits:
|
||||
# cpu: 100m
|
||||
# memory: 128Mi
|
||||
# requests:
|
||||
# cpu: 100m
|
||||
# memory: 128Mi
|
||||
|
||||
|
||||
## Configure the liveness and readiness probes
|
||||
## ref: https://kubernetes.io/docs/concepts/configuration/liveness-readiness-startup-probes/
|
||||
livenessProbe:
|
||||
enabled: true
|
||||
initialDelaySeconds: 200
|
||||
timeoutSeconds: 1
|
||||
periodSeconds: 10
|
||||
successThreshold: 1
|
||||
failureThreshold: 5
|
||||
|
||||
readinessProbe:
|
||||
enabled: true
|
||||
initialDelaySeconds: 5
|
||||
timeoutSeconds: 1
|
||||
periodSeconds: 10
|
||||
successThreshold: 1
|
||||
failureThreshold: 3
|
||||
|
||||
## Define autoscaling configuration using Horizontal Pod Autoscaler
|
||||
## ref: https://kubernetes.io/docs/tasks/run-application/horizontal-pod-autoscale/
|
||||
autoscaling:
|
||||
enabled: false
|
||||
minReplicas: 1
|
||||
maxReplicas: 10
|
||||
targetCPUUtilizationPercentage: 80
|
||||
# targetMemoryUtilizationPercentage: 80
|
||||
annotations: {}
|
||||
|
||||
## Additional deployment configuration
|
||||
deployment:
|
||||
env: []
|
||||
terminationGracePeriodSeconds: 60
|
||||
labels: {}
|
||||
annotations: {}
|
||||
|
||||
## Set pod assignment with node labels
|
||||
## ref: https://kubernetes.io/docs/concepts/scheduling-eviction/assign-pod-node/
|
||||
nodeSelector: {}
|
||||
tolerations: []
|
||||
affinity: {}
|
||||
|
||||
## Use PostgreSQL as a database, using Bitnami's PostgreSQL Helm chart
|
||||
## ref: https://artifacthub.io/packages/helm/bitnami/postgresql/16.5.6
|
||||
postgresql:
|
||||
enabled: false
|
||||
global:
|
||||
postgresql:
|
||||
auth:
|
||||
username: opengist
|
||||
password: opengist
|
||||
database: opengist
|
||||
service:
|
||||
ports:
|
||||
postgresql: 5432
|
||||
primary:
|
||||
persistence:
|
||||
size: 10Gi
|
||||
|
||||
|
||||
## Use Meilisearch as a code indexer, using Meilisearch's Helm chart
|
||||
## ref: https://github.com/meilisearch/meilisearch-kubernetes/tree/meilisearch-0.12.0
|
||||
meilisearch:
|
||||
enabled: false
|
||||
environment:
|
||||
MEILI_ENV: "production"
|
||||
auth:
|
||||
existingMasterKeySecret:
|
185
internal/actions/actions.go
Normal file
185
internal/actions/actions.go
Normal file
@ -0,0 +1,185 @@
|
||||
package actions
|
||||
|
||||
import (
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/thomiceli/opengist/internal/config"
|
||||
"github.com/thomiceli/opengist/internal/db"
|
||||
"github.com/thomiceli/opengist/internal/git"
|
||||
"github.com/thomiceli/opengist/internal/index"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type ActionStatus struct {
|
||||
Running bool
|
||||
}
|
||||
|
||||
const (
|
||||
SyncReposFromFS = iota
|
||||
SyncReposFromDB
|
||||
GitGcRepos
|
||||
SyncGistPreviews
|
||||
ResetHooks
|
||||
IndexGists
|
||||
SyncGistLanguages
|
||||
)
|
||||
|
||||
var (
|
||||
mutex sync.Mutex
|
||||
actions = make(map[int]ActionStatus)
|
||||
)
|
||||
|
||||
func updateActionStatus(actionType int, running bool) {
|
||||
actions[actionType] = ActionStatus{
|
||||
Running: running,
|
||||
}
|
||||
}
|
||||
|
||||
func IsRunning(actionType int) bool {
|
||||
mutex.Lock()
|
||||
defer mutex.Unlock()
|
||||
return actions[actionType].Running
|
||||
}
|
||||
|
||||
func Run(actionType int) {
|
||||
mutex.Lock()
|
||||
|
||||
if actions[actionType].Running {
|
||||
mutex.Unlock()
|
||||
return
|
||||
}
|
||||
|
||||
updateActionStatus(actionType, true)
|
||||
mutex.Unlock()
|
||||
|
||||
defer func() {
|
||||
mutex.Lock()
|
||||
updateActionStatus(actionType, false)
|
||||
mutex.Unlock()
|
||||
}()
|
||||
|
||||
var functionToRun func()
|
||||
switch actionType {
|
||||
case SyncReposFromFS:
|
||||
functionToRun = syncReposFromFS
|
||||
case SyncReposFromDB:
|
||||
functionToRun = syncReposFromDB
|
||||
case GitGcRepos:
|
||||
functionToRun = gitGcRepos
|
||||
case SyncGistPreviews:
|
||||
functionToRun = syncGistPreviews
|
||||
case ResetHooks:
|
||||
functionToRun = resetHooks
|
||||
case IndexGists:
|
||||
functionToRun = indexGists
|
||||
case SyncGistLanguages:
|
||||
functionToRun = syncGistLanguages
|
||||
default:
|
||||
log.Error().Msg("Unknown action type")
|
||||
}
|
||||
|
||||
functionToRun()
|
||||
}
|
||||
|
||||
func syncReposFromFS() {
|
||||
log.Info().Msg("Syncing repositories from filesystem...")
|
||||
gists, err := db.GetAllGistsRows()
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Cannot get gists")
|
||||
return
|
||||
}
|
||||
for _, gist := range gists {
|
||||
// if repository does not exist, delete gist from database
|
||||
if _, err := os.Stat(git.RepositoryPath(gist.User.Username, gist.Uuid)); err != nil && !os.IsExist(err) {
|
||||
if err2 := gist.Delete(); err2 != nil {
|
||||
log.Error().Err(err2).Msgf("Cannot delete gist %d", gist.ID)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func syncReposFromDB() {
|
||||
log.Info().Msg("Syncing repositories from database...")
|
||||
entries, err := filepath.Glob(filepath.Join(config.GetHomeDir(), "repos", "*", "*"))
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Cannot read repos directories")
|
||||
return
|
||||
}
|
||||
|
||||
for _, e := range entries {
|
||||
path := strings.Split(e, string(os.PathSeparator))
|
||||
gist, _ := db.GetGist(path[len(path)-2], path[len(path)-1])
|
||||
|
||||
if gist.ID == 0 {
|
||||
if err := git.DeleteRepository(path[len(path)-2], path[len(path)-1]); err != nil {
|
||||
log.Error().Err(err).Msgf("Cannot delete repository %s/%s", path[len(path)-2], path[len(path)-1])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func gitGcRepos() {
|
||||
log.Info().Msg("Garbage collecting all repositories...")
|
||||
if err := git.GcRepos(); err != nil {
|
||||
log.Error().Err(err).Msg("Error garbage collecting repositories")
|
||||
}
|
||||
}
|
||||
|
||||
func syncGistPreviews() {
|
||||
log.Info().Msg("Syncing all Gist previews...")
|
||||
|
||||
gists, err := db.GetAllGistsRows()
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Cannot get gists")
|
||||
return
|
||||
}
|
||||
for _, gist := range gists {
|
||||
if err = gist.UpdatePreviewAndCount(false); err != nil {
|
||||
log.Error().Err(err).Msgf("Cannot update preview and count for gist %d", gist.ID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func resetHooks() {
|
||||
log.Info().Msg("Resetting Git server hooks for all repositories...")
|
||||
if err := git.ResetHooks(); err != nil {
|
||||
log.Error().Err(err).Msg("Error resetting hooks for repositories")
|
||||
}
|
||||
}
|
||||
|
||||
func indexGists() {
|
||||
log.Info().Msg("Indexing all Gists...")
|
||||
gists, err := db.GetAllGistsRows()
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Cannot get gists")
|
||||
return
|
||||
}
|
||||
|
||||
for _, gist := range gists {
|
||||
log.Info().Msgf("Indexing gist %d", gist.ID)
|
||||
indexedGist, err := gist.ToIndexedGist()
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msgf("Cannot convert gist %d to indexed gist", gist.ID)
|
||||
continue
|
||||
}
|
||||
if err = index.AddInIndex(indexedGist); err != nil {
|
||||
log.Error().Err(err).Msgf("Cannot index gist %d", gist.ID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
46
internal/auth/aes.go
Normal file
46
internal/auth/aes.go
Normal file
@ -0,0 +1,46 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"crypto/rand"
|
||||
"fmt"
|
||||
"io"
|
||||
)
|
||||
|
||||
func AESEncrypt(key, text []byte) ([]byte, error) {
|
||||
block, err := aes.NewCipher(key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ciphertext := make([]byte, aes.BlockSize+len(text))
|
||||
iv := ciphertext[:aes.BlockSize]
|
||||
if _, err = io.ReadFull(rand.Reader, iv); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
stream := cipher.NewCFBEncrypter(block, iv)
|
||||
stream.XORKeyStream(ciphertext[aes.BlockSize:], text)
|
||||
|
||||
return ciphertext, nil
|
||||
}
|
||||
|
||||
func AESDecrypt(key, ciphertext []byte) ([]byte, error) {
|
||||
block, err := aes.NewCipher(key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(ciphertext) < aes.BlockSize {
|
||||
return nil, fmt.Errorf("ciphertext too short")
|
||||
}
|
||||
|
||||
iv := ciphertext[:aes.BlockSize]
|
||||
ciphertext = ciphertext[aes.BlockSize:]
|
||||
|
||||
stream := cipher.NewCFBDecrypter(block, iv)
|
||||
stream.XORKeyStream(ciphertext, ciphertext)
|
||||
|
||||
return ciphertext, nil
|
||||
}
|
76
internal/auth/argon2id.go
Normal file
76
internal/auth/argon2id.go
Normal file
@ -0,0 +1,76 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"crypto/subtle"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"golang.org/x/crypto/argon2"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type argon2ID struct {
|
||||
format string
|
||||
version int
|
||||
time uint32
|
||||
memory uint32
|
||||
keyLen uint32
|
||||
saltLen uint32
|
||||
threads uint8
|
||||
}
|
||||
|
||||
var Argon2id = argon2ID{
|
||||
format: "$argon2id$v=%d$m=%d,t=%d,p=%d$%s$%s",
|
||||
version: argon2.Version,
|
||||
time: 1,
|
||||
memory: 64 * 1024,
|
||||
keyLen: 32,
|
||||
saltLen: 16,
|
||||
threads: 4,
|
||||
}
|
||||
|
||||
func (a argon2ID) Hash(plain string) (string, error) {
|
||||
salt := make([]byte, a.saltLen)
|
||||
if _, err := rand.Read(salt); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
hash := argon2.IDKey([]byte(plain), salt, a.time, a.memory, a.threads, a.keyLen)
|
||||
|
||||
return fmt.Sprintf(a.format, a.version, a.memory, a.time, a.threads,
|
||||
base64.RawStdEncoding.EncodeToString(salt),
|
||||
base64.RawStdEncoding.EncodeToString(hash),
|
||||
), nil
|
||||
}
|
||||
|
||||
func (a argon2ID) Verify(plain, hash string) (bool, error) {
|
||||
if hash == "" {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
hashParts := strings.Split(hash, "$")
|
||||
|
||||
if len(hashParts) != 6 {
|
||||
return false, errors.New("invalid hash")
|
||||
}
|
||||
|
||||
_, err := fmt.Sscanf(hashParts[3], "m=%d,t=%d,p=%d", &a.memory, &a.time, &a.threads)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
salt, err := base64.RawStdEncoding.DecodeString(hashParts[4])
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
decodedHash, err := base64.RawStdEncoding.DecodeString(hashParts[5])
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
hashToCompare := argon2.IDKey([]byte(plain), salt, a.time, a.memory, a.threads, uint32(len(decodedHash)))
|
||||
|
||||
return subtle.ConstantTimeCompare(decodedHash, hashToCompare) == 1, nil
|
||||
}
|
18
internal/auth/auth.go
Normal file
18
internal/auth/auth.go
Normal file
@ -0,0 +1,18 @@
|
||||
package auth
|
||||
|
||||
type AuthInfoProvider interface {
|
||||
RequireLogin() (bool, error)
|
||||
AllowGistsWithoutLogin() (bool, error)
|
||||
}
|
||||
|
||||
func ShouldAllowUnauthenticatedGistAccess(prov AuthInfoProvider, isSingleGistAccess bool) (bool, error) {
|
||||
require, err := prov.RequireLogin()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
allow, err := prov.AllowGistsWithoutLogin()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return !require || (isSingleGistAccess && allow), nil
|
||||
}
|
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,
|
||||
}
|
||||
}
|
85
internal/auth/oauth/openid.go
Normal file
85
internal/auth/oauth/openid.go
Normal file
@ -0,0 +1,85 @@
|
||||
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",
|
||||
)
|
||||
|
||||
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
|
||||
}
|
11
internal/auth/password/password.go
Normal file
11
internal/auth/password/password.go
Normal file
@ -0,0 +1,11 @@
|
||||
package password
|
||||
|
||||
import "github.com/thomiceli/opengist/internal/auth"
|
||||
|
||||
func HashPassword(code string) (string, error) {
|
||||
return auth.Argon2id.Hash(code)
|
||||
}
|
||||
|
||||
func VerifyPassword(code, hashedCode string) (bool, error) {
|
||||
return auth.Argon2id.Verify(code, hashedCode)
|
||||
}
|
61
internal/auth/totp/totp.go
Normal file
61
internal/auth/totp/totp.go
Normal file
@ -0,0 +1,61 @@
|
||||
package totp
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"github.com/pquerna/otp/totp"
|
||||
"html/template"
|
||||
"image/png"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const secretSize = 16
|
||||
|
||||
func GenerateQRCode(username, siteUrl string, secret []byte) (string, template.URL, error, []byte) {
|
||||
var err error
|
||||
if secret == nil {
|
||||
secret, err = generateSecret()
|
||||
if err != nil {
|
||||
return "", "", err, nil
|
||||
}
|
||||
}
|
||||
|
||||
otpKey, err := totp.Generate(totp.GenerateOpts{
|
||||
SecretSize: secretSize,
|
||||
Issuer: "Opengist (" + strings.ReplaceAll(siteUrl, ":", "") + ")",
|
||||
AccountName: username,
|
||||
Secret: secret,
|
||||
})
|
||||
if err != nil {
|
||||
return "", "", err, nil
|
||||
}
|
||||
|
||||
qrcode, err := otpKey.Image(320, 240)
|
||||
if err != nil {
|
||||
return "", "", err, nil
|
||||
}
|
||||
|
||||
var imgBytes bytes.Buffer
|
||||
if err = png.Encode(&imgBytes, qrcode); err != nil {
|
||||
return "", "", err, nil
|
||||
}
|
||||
|
||||
qrcodeImage := template.URL("data:image/png;base64," + base64.StdEncoding.EncodeToString(imgBytes.Bytes()))
|
||||
|
||||
return otpKey.Secret(), qrcodeImage, nil, secret
|
||||
}
|
||||
|
||||
func Validate(passcode, secret string) bool {
|
||||
return totp.Validate(passcode, secret)
|
||||
}
|
||||
|
||||
func generateSecret() ([]byte, error) {
|
||||
secret := make([]byte, secretSize)
|
||||
_, err := rand.Reader.Read(secret)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return secret, nil
|
||||
}
|
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
|
||||
}
|
138
internal/auth/webauthn/webauthn.go
Normal file
138
internal/auth/webauthn/webauthn.go
Normal file
@ -0,0 +1,138 @@
|
||||
package webauthn
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"github.com/go-webauthn/webauthn/protocol"
|
||||
"github.com/go-webauthn/webauthn/webauthn"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/thomiceli/opengist/internal/config"
|
||||
"github.com/thomiceli/opengist/internal/db"
|
||||
"net/http"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
var webAuthn *webauthn.WebAuthn
|
||||
|
||||
func Init(urlStr string) error {
|
||||
var rpid, rporigin string
|
||||
var err error
|
||||
|
||||
if urlStr == "" {
|
||||
log.Info().Msg("External URL is not set, passkeys RP ID and Origins will be set to localhost")
|
||||
rpid = "localhost"
|
||||
rporigin = "http://localhost" + ":" + config.C.HttpPort
|
||||
} else {
|
||||
urlStruct, err := url.Parse(urlStr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
rpid = urlStruct.Hostname()
|
||||
rporigin, err = protocol.FullyQualifiedOrigin(urlStr)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Failed to get fully qualified origin from external URL")
|
||||
}
|
||||
}
|
||||
|
||||
webAuthn, err = webauthn.New(&webauthn.Config{
|
||||
RPDisplayName: "Opengist",
|
||||
RPID: rpid,
|
||||
RPOrigins: []string{rporigin},
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
func BeginBinding(dbUser *db.User) (credCreation *protocol.CredentialCreation, jsonSession []byte, err error) {
|
||||
waUser := &user{User: dbUser}
|
||||
credCreation, session, err := webAuthn.BeginRegistration(waUser, webauthn.WithAuthenticatorSelection(
|
||||
protocol.AuthenticatorSelection{
|
||||
ResidentKey: protocol.ResidentKeyRequirementRequired,
|
||||
UserVerification: protocol.VerificationRequired,
|
||||
},
|
||||
), webauthn.WithAppIdExcludeExtension("Opengist"), webauthn.WithExclusions(waUser.Exclusions()))
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
jsonSession, _ = json.Marshal(session)
|
||||
return
|
||||
}
|
||||
|
||||
func FinishBinding(dbUser *db.User, jsonSession []byte, response *http.Request) (*webauthn.Credential, error) {
|
||||
waUser := &user{User: dbUser}
|
||||
|
||||
var session webauthn.SessionData
|
||||
_ = json.Unmarshal(jsonSession, &session)
|
||||
|
||||
return webAuthn.FinishRegistration(waUser, session, response)
|
||||
}
|
||||
|
||||
func BeginDiscoverableLogin() (credCreation *protocol.CredentialAssertion, jsonSession []byte, err error) {
|
||||
credCreation, session, err := webAuthn.BeginDiscoverableLogin(
|
||||
webauthn.WithUserVerification(protocol.VerificationPreferred),
|
||||
)
|
||||
|
||||
jsonSession, _ = json.Marshal(session)
|
||||
return
|
||||
}
|
||||
|
||||
func FinishDiscoverableLogin(jsonSession []byte, response *http.Request) (uint, error) {
|
||||
var session webauthn.SessionData
|
||||
_ = json.Unmarshal(jsonSession, &session)
|
||||
|
||||
parsedResponse, err := protocol.ParseCredentialRequestResponse(response)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
waUser, cred, err := webAuthn.ValidatePasskeyLogin(discoverUser, session, parsedResponse)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
dbCredential, err := db.GetCredentialByID(cred.ID)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if err = dbCredential.UpdateSignCount(); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if err = dbCredential.UpdateLastUsedAt(); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return waUser.(*user).User.ID, nil
|
||||
}
|
||||
|
||||
func BeginLogin(dbUser *db.User) (credCreation *protocol.CredentialAssertion, jsonSession []byte, err error) {
|
||||
waUser := &user{User: dbUser}
|
||||
credCreation, session, err := webAuthn.BeginLogin(waUser)
|
||||
|
||||
jsonSession, _ = json.Marshal(session)
|
||||
return
|
||||
}
|
||||
|
||||
func FinishLogin(dbUser *db.User, jsonSession []byte, response *http.Request) error {
|
||||
waUser := &user{User: dbUser}
|
||||
|
||||
var session webauthn.SessionData
|
||||
_ = json.Unmarshal(jsonSession, &session)
|
||||
|
||||
cred, err := webAuthn.FinishLogin(waUser, session, response)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
dbCredential, err := db.GetCredentialByID(cred.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err = dbCredential.UpdateSignCount(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err = dbCredential.UpdateLastUsedAt(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
78
internal/cli/admin.go
Normal file
78
internal/cli/admin.go
Normal file
@ -0,0 +1,78 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/thomiceli/opengist/internal/auth/password"
|
||||
"github.com/thomiceli/opengist/internal/db"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
var CmdAdmin = cli.Command{
|
||||
Name: "admin",
|
||||
Usage: "Admin commands",
|
||||
Subcommands: []*cli.Command{
|
||||
&CmdAdminResetPassword,
|
||||
&CmdAdminToggleAdmin,
|
||||
},
|
||||
}
|
||||
|
||||
var CmdAdminResetPassword = cli.Command{
|
||||
Name: "reset-password",
|
||||
Usage: "Reset the password for a given user",
|
||||
ArgsUsage: "[username] [password]",
|
||||
Action: func(ctx *cli.Context) error {
|
||||
initialize(ctx)
|
||||
if ctx.NArg() < 2 {
|
||||
return fmt.Errorf("username and password are required")
|
||||
}
|
||||
username := ctx.Args().Get(0)
|
||||
plainPassword := ctx.Args().Get(1)
|
||||
|
||||
user, err := db.GetUserByUsername(username)
|
||||
if err != nil {
|
||||
fmt.Printf("Cannot get user %s: %s\n", username, err)
|
||||
return err
|
||||
}
|
||||
password, err := password.HashPassword(plainPassword)
|
||||
if err != nil {
|
||||
fmt.Printf("Cannot hash password for user %s: %s\n", username, err)
|
||||
return err
|
||||
}
|
||||
user.Password = password
|
||||
|
||||
if err = user.Update(); err != nil {
|
||||
fmt.Printf("Cannot update password for user %s: %s\n", username, err)
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Printf("Password for user %s has been reset.\n", username)
|
||||
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
|
||||
},
|
||||
}
|
56
internal/cli/hook.go
Normal file
56
internal/cli/hook.go
Normal file
@ -0,0 +1,56 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/thomiceli/opengist/internal/config"
|
||||
"github.com/thomiceli/opengist/internal/db"
|
||||
"github.com/thomiceli/opengist/internal/hooks"
|
||||
"github.com/urfave/cli/v2"
|
||||
"io"
|
||||
"os"
|
||||
)
|
||||
|
||||
var CmdHook = cli.Command{
|
||||
Name: "hook",
|
||||
Usage: "Run Git server hooks, used and should only be called by Opengist itself",
|
||||
Subcommands: []*cli.Command{
|
||||
&CmdHookPreReceive,
|
||||
&CmdHookPostReceive,
|
||||
},
|
||||
}
|
||||
|
||||
var CmdHookPreReceive = cli.Command{
|
||||
Name: "pre-receive",
|
||||
Usage: "Run Git server pre-receive hook for a repository",
|
||||
Action: func(ctx *cli.Context) error {
|
||||
initialize(ctx)
|
||||
if err := hooks.PreReceive(os.Stdin, os.Stdout, os.Stderr); err != nil {
|
||||
os.Exit(1)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
var CmdHookPostReceive = cli.Command{
|
||||
Name: "post-receive",
|
||||
Usage: "Run Git server post-receive hook for a repository",
|
||||
Action: func(ctx *cli.Context) error {
|
||||
initialize(ctx)
|
||||
if err := hooks.PostReceive(os.Stdin, os.Stdout, os.Stderr); err != nil {
|
||||
os.Exit(1)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
func initialize(ctx *cli.Context) {
|
||||
if err := config.InitConfig(ctx.String("config"), io.Discard); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
config.InitLog()
|
||||
|
||||
db.DeprecationDBFilename()
|
||||
if err := db.Setup(config.C.DBUri); err != nil {
|
||||
log.Fatal().Err(err).Msg("Failed to initialize database in hooks")
|
||||
}
|
||||
}
|
184
internal/cli/main.go
Normal file
184
internal/cli/main.go
Normal file
@ -0,0 +1,184 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/thomiceli/opengist/internal/auth/webauthn"
|
||||
"github.com/thomiceli/opengist/internal/config"
|
||||
"github.com/thomiceli/opengist/internal/db"
|
||||
"github.com/thomiceli/opengist/internal/git"
|
||||
"github.com/thomiceli/opengist/internal/index"
|
||||
"github.com/thomiceli/opengist/internal/ssh"
|
||||
"github.com/thomiceli/opengist/internal/web/server"
|
||||
"github.com/urfave/cli/v2"
|
||||
"os"
|
||||
"os/signal"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
var CmdVersion = cli.Command{
|
||||
Name: "version",
|
||||
Usage: "Print the version of Opengist",
|
||||
Action: func(c *cli.Context) error {
|
||||
fmt.Println("Opengist " + config.OpengistVersion)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
var CmdStart = cli.Command{
|
||||
Name: "start",
|
||||
Usage: "Start Opengist server",
|
||||
Action: func(ctx *cli.Context) error {
|
||||
stopCtx, stop := signal.NotifyContext(ctx.Context, syscall.SIGINT, syscall.SIGTERM)
|
||||
defer stop()
|
||||
|
||||
Initialize(ctx)
|
||||
|
||||
go server.NewServer(os.Getenv("OG_DEV") == "1", path.Join(config.GetHomeDir(), "sessions"), false).Start()
|
||||
go ssh.Start()
|
||||
|
||||
<-stopCtx.Done()
|
||||
shutdown()
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
var ConfigFlag = cli.StringFlag{
|
||||
Name: "config",
|
||||
Aliases: []string{"c"},
|
||||
Usage: "Path to a config file in YAML format",
|
||||
}
|
||||
|
||||
func App() error {
|
||||
app := cli.NewApp()
|
||||
app.Name = "Opengist"
|
||||
app.Usage = "A self-hosted pastebin powered by Git."
|
||||
app.HelpName = "opengist"
|
||||
|
||||
app.Commands = []*cli.Command{&CmdVersion, &CmdStart, &CmdHook, &CmdAdmin}
|
||||
app.DefaultCommand = CmdStart.Name
|
||||
app.Flags = []cli.Flag{
|
||||
&ConfigFlag,
|
||||
}
|
||||
return app.Run(os.Args)
|
||||
}
|
||||
|
||||
func Initialize(ctx *cli.Context) {
|
||||
fmt.Println("Opengist " + config.OpengistVersion)
|
||||
|
||||
if err := config.InitConfig(ctx.String("config"), os.Stdout); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if err := os.MkdirAll(filepath.Join(config.GetHomeDir()), 0755); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
config.SetupSecretKey()
|
||||
|
||||
config.InitLog()
|
||||
|
||||
gitVersion, err := git.GetGitVersion()
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Send()
|
||||
}
|
||||
|
||||
if ok, err := config.CheckGitVersion(gitVersion); err != nil {
|
||||
log.Fatal().Err(err).Send()
|
||||
} else if !ok {
|
||||
log.Warn().Msg("Git version may be too old, as Opengist has not been tested prior git version 2.28 and some features would not work. " +
|
||||
"Current git version: " + gitVersion)
|
||||
}
|
||||
|
||||
homePath := config.GetHomeDir()
|
||||
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 {
|
||||
log.Fatal().Err(err).Msg("Failed to create symlinks")
|
||||
}
|
||||
|
||||
if err := os.MkdirAll(filepath.Join(homePath, "sessions"), 0755); err != nil {
|
||||
log.Fatal().Err(err).Send()
|
||||
}
|
||||
if err := os.MkdirAll(filepath.Join(homePath, "repos"), 0755); err != nil {
|
||||
log.Fatal().Err(err).Send()
|
||||
}
|
||||
if err := os.MkdirAll(filepath.Join(homePath, "tmp", "repos"), 0755); err != nil {
|
||||
log.Fatal().Err(err).Send()
|
||||
}
|
||||
if err := os.MkdirAll(filepath.Join(homePath, "custom"), 0755); err != nil {
|
||||
log.Fatal().Err(err).Send()
|
||||
}
|
||||
|
||||
db.DeprecationDBFilename()
|
||||
if err := db.Setup(config.C.DBUri); err != nil {
|
||||
log.Fatal().Err(err).Msg("Failed to initialize database")
|
||||
}
|
||||
|
||||
if err := webauthn.Init(config.C.ExternalUrl); err != nil {
|
||||
log.Error().Err(err).Msg("Failed to initialize WebAuthn")
|
||||
}
|
||||
|
||||
index.DepreactionIndexDirname()
|
||||
if index.IndexEnabled() {
|
||||
go index.NewIndexer(index.IndexType())
|
||||
}
|
||||
}
|
||||
|
||||
func shutdown() {
|
||||
log.Info().Msg("Shutting down database...")
|
||||
if err := db.Close(); err != nil {
|
||||
log.Error().Err(err).Msg("Failed to close database")
|
||||
}
|
||||
|
||||
if index.IndexEnabled() {
|
||||
log.Info().Msg("Shutting down index...")
|
||||
index.Close()
|
||||
}
|
||||
|
||||
log.Info().Msg("Shutdown complete")
|
||||
}
|
||||
|
||||
func createSymlink(homePath string, configPath string) error {
|
||||
if err := os.MkdirAll(filepath.Join(homePath, "symlinks"), 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
exePath, err := os.Executable()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
symlinkExePath := path.Join(config.GetHomeDir(), "symlinks", "opengist")
|
||||
if _, err := os.Lstat(symlinkExePath); err == nil {
|
||||
if err := os.Remove(symlinkExePath); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err = os.Symlink(exePath, symlinkExePath); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if configPath == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
configPath, _ = filepath.Abs(configPath)
|
||||
configPath = filepath.Clean(configPath)
|
||||
symlinkConfigPath := path.Join(config.GetHomeDir(), "symlinks", "config.yml")
|
||||
if _, err := os.Lstat(symlinkConfigPath); err == nil {
|
||||
if err := os.Remove(symlinkConfigPath); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err = os.Symlink(configPath, symlinkConfigPath); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
@ -2,116 +2,169 @@ package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/thomiceli/opengist/internal/session"
|
||||
"io"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/rs/zerolog/log"
|
||||
"gopkg.in/yaml.v3"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var OpengistVersion = "1.2.0"
|
||||
var OpengistVersion = ""
|
||||
|
||||
var C *config
|
||||
|
||||
var SecretKey []byte
|
||||
|
||||
// Not using nested structs because the library
|
||||
// doesn't support dot notation in this case sadly
|
||||
type config struct {
|
||||
LogLevel string `yaml:"log-level"`
|
||||
ExternalUrl string `yaml:"external-url"`
|
||||
OpengistHome string `yaml:"opengist-home"`
|
||||
DBFilename string `yaml:"db-filename"`
|
||||
SecretKey string `yaml:"secret-key" env:"OG_SECRET_KEY"`
|
||||
|
||||
HttpHost string `yaml:"http.host"`
|
||||
HttpPort string `yaml:"http.port"`
|
||||
HttpGit bool `yaml:"http.git-enabled"`
|
||||
HttpTLSEnabled bool `yaml:"http.tls-enabled"`
|
||||
HttpCertFile string `yaml:"http.cert-file"`
|
||||
HttpKeyFile string `yaml:"http.key-file"`
|
||||
LogLevel string `yaml:"log-level" env:"OG_LOG_LEVEL"`
|
||||
LogOutput string `yaml:"log-output" env:"OG_LOG_OUTPUT"`
|
||||
ExternalUrl string `yaml:"external-url" env:"OG_EXTERNAL_URL"`
|
||||
OpengistHome string `yaml:"opengist-home" env:"OG_OPENGIST_HOME"`
|
||||
|
||||
SshGit bool `yaml:"ssh.git-enabled"`
|
||||
SshHost string `yaml:"ssh.host"`
|
||||
SshPort string `yaml:"ssh.port"`
|
||||
SshExternalDomain string `yaml:"ssh.external-domain"`
|
||||
SshKeygen string `yaml:"ssh.keygen-executable"`
|
||||
DBUri string `yaml:"db-uri" env:"OG_DB_URI"`
|
||||
DBFilename string `yaml:"db-filename" env:"OG_DB_FILENAME"` // deprecated
|
||||
|
||||
GithubClientKey string `yaml:"github.client-key"`
|
||||
GithubSecret string `yaml:"github.secret"`
|
||||
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"`
|
||||
|
||||
GiteaClientKey string `yaml:"gitea.client-key"`
|
||||
GiteaSecret string `yaml:"gitea.secret"`
|
||||
GiteaUrl string `yaml:"gitea.url"`
|
||||
GitDefaultBranch string `yaml:"git.default-branch" env:"OG_GIT_DEFAULT_BRANCH"`
|
||||
|
||||
SqliteJournalMode string `yaml:"sqlite.journal-mode" env:"OG_SQLITE_JOURNAL_MODE"`
|
||||
|
||||
HttpHost string `yaml:"http.host" env:"OG_HTTP_HOST"`
|
||||
HttpPort string `yaml:"http.port" env:"OG_HTTP_PORT"`
|
||||
HttpGit bool `yaml:"http.git-enabled" env:"OG_HTTP_GIT_ENABLED"`
|
||||
|
||||
SshGit bool `yaml:"ssh.git-enabled" env:"OG_SSH_GIT_ENABLED"`
|
||||
SshHost string `yaml:"ssh.host" env:"OG_SSH_HOST"`
|
||||
SshPort string `yaml:"ssh.port" env:"OG_SSH_PORT"`
|
||||
SshExternalDomain string `yaml:"ssh.external-domain" env:"OG_SSH_EXTERNAL_DOMAIN"`
|
||||
SshKeygen string `yaml:"ssh.keygen-executable" env:"OG_SSH_KEYGEN_EXECUTABLE"`
|
||||
|
||||
GithubClientKey string `yaml:"github.client-key" env:"OG_GITHUB_CLIENT_KEY"`
|
||||
GithubSecret string `yaml:"github.secret" env:"OG_GITHUB_SECRET"`
|
||||
|
||||
GitlabClientKey string `yaml:"gitlab.client-key" env:"OG_GITLAB_CLIENT_KEY"`
|
||||
GitlabSecret string `yaml:"gitlab.secret" env:"OG_GITLAB_SECRET"`
|
||||
GitlabUrl string `yaml:"gitlab.url" env:"OG_GITLAB_URL"`
|
||||
GitlabName string `yaml:"gitlab.name" env:"OG_GITLAB_NAME"`
|
||||
|
||||
GiteaClientKey string `yaml:"gitea.client-key" env:"OG_GITEA_CLIENT_KEY"`
|
||||
GiteaSecret string `yaml:"gitea.secret" env:"OG_GITEA_SECRET"`
|
||||
GiteaUrl string `yaml:"gitea.url" env:"OG_GITEA_URL"`
|
||||
GiteaName string `yaml:"gitea.name" env:"OG_GITEA_NAME"`
|
||||
|
||||
OIDCProviderName string `yaml:"oidc.provider-name" env:"OG_OIDC_PROVIDER_NAME"`
|
||||
OIDCClientKey string `yaml:"oidc.client-key" env:"OG_OIDC_CLIENT_KEY"`
|
||||
OIDCSecret string `yaml:"oidc.secret" env:"OG_OIDC_SECRET"`
|
||||
OIDCDiscoveryUrl string `yaml:"oidc.discovery-url" env:"OG_OIDC_DISCOVERY_URL"`
|
||||
OIDCGroupClaimName string `yaml:"oidc.group-claim-name" env:"OG_OIDC_GROUP_CLAIM_NAME"`
|
||||
OIDCAdminGroup string `yaml:"oidc.admin-group" env:"OG_OIDC_ADMIN_GROUP"`
|
||||
|
||||
MetricsEnabled bool `yaml:"metrics.enabled" env:"OG_METRICS_ENABLED"`
|
||||
|
||||
LDAPUrl string `yaml:"ldap.url" env:"OG_LDAP_URL"`
|
||||
LDAPBindDn string `yaml:"ldap.bind-dn" env:"OG_LDAP_BIND_DN"`
|
||||
LDAPBindCredentials string `yaml:"ldap.bind-credentials" env:"OG_LDAP_BIND_CREDENTIALS"`
|
||||
LDAPSearchBase string `yaml:"ldap.search-base" env:"OG_LDAP_SEARCH_BASE"`
|
||||
LDAPSearchFilter string `yaml:"ldap.search-filter" env:"OG_LDAP_SEARCH_FILTER"`
|
||||
|
||||
CustomName string `yaml:"custom.name" env:"OG_CUSTOM_NAME"`
|
||||
CustomLogo string `yaml:"custom.logo" env:"OG_CUSTOM_LOGO"`
|
||||
CustomFavicon string `yaml:"custom.favicon" env:"OG_CUSTOM_FAVICON"`
|
||||
StaticLinks []StaticLink `yaml:"custom.static-links" env:"OG_CUSTOM_STATIC_LINK"`
|
||||
}
|
||||
|
||||
type StaticLink struct {
|
||||
Name string `yaml:"name" env:"OG_CUSTOM_STATIC_LINK_#_NAME"`
|
||||
Path string `yaml:"path" env:"OG_CUSTOM_STATIC_LINK_#_PATH"`
|
||||
}
|
||||
|
||||
func configWithDefaults() (*config, error) {
|
||||
homeDir, err := os.UserHomeDir()
|
||||
c := &config{}
|
||||
if err != nil {
|
||||
return c, err
|
||||
}
|
||||
|
||||
c.SecretKey = ""
|
||||
|
||||
c.LogLevel = "warn"
|
||||
c.OpengistHome = filepath.Join(homeDir, ".opengist")
|
||||
c.DBFilename = "opengist.db"
|
||||
c.LogOutput = "stdout,file"
|
||||
c.OpengistHome = ""
|
||||
c.DBUri = "opengist.db"
|
||||
c.Index = "bleve"
|
||||
|
||||
c.SqliteJournalMode = "WAL"
|
||||
|
||||
c.HttpHost = "0.0.0.0"
|
||||
c.HttpPort = "6157"
|
||||
c.HttpGit = true
|
||||
c.HttpTLSEnabled = false
|
||||
|
||||
c.SshGit = true
|
||||
c.SshHost = "0.0.0.0"
|
||||
c.SshPort = "2222"
|
||||
c.SshKeygen = "ssh-keygen"
|
||||
|
||||
c.GiteaUrl = "http://gitea.com"
|
||||
c.GitlabName = "GitLab"
|
||||
|
||||
c.GiteaUrl = "https://gitea.com"
|
||||
c.GiteaName = "Gitea"
|
||||
|
||||
c.MetricsEnabled = false
|
||||
|
||||
return c, nil
|
||||
}
|
||||
|
||||
func InitConfig(configPath string) error {
|
||||
func InitConfig(configPath string, out io.Writer) error {
|
||||
// Default values
|
||||
c, err := configWithDefaults()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if configPath != "" {
|
||||
absolutePath, _ := filepath.Abs(configPath)
|
||||
absolutePath = filepath.Clean(absolutePath)
|
||||
file, err := os.Open(absolutePath)
|
||||
if err = loadConfigFromYaml(c, configPath, out); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = loadConfigFromEnv(c, out); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if c.OpengistHome == "" {
|
||||
homeDir, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
if !os.IsNotExist(err) {
|
||||
return err
|
||||
}
|
||||
fmt.Println("No YML config file found at " + absolutePath)
|
||||
} else {
|
||||
fmt.Println("Using config file: " + absolutePath)
|
||||
|
||||
// Override default values with values from config.yml
|
||||
d := yaml.NewDecoder(file)
|
||||
if err = d.Decode(&c); err != nil {
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
}
|
||||
} else {
|
||||
fmt.Println("No config file specified. Using default values.")
|
||||
return fmt.Errorf("opengist home directory is not set and current user home directory could not be determined; please specify the opengist home directory manually via the configuration")
|
||||
}
|
||||
|
||||
// Override default values with environment variables (as yaml)
|
||||
configEnv := os.Getenv("CONFIG")
|
||||
if configEnv != "" {
|
||||
fmt.Println("Using config from environment variable: CONFIG")
|
||||
d := yaml.NewDecoder(strings.NewReader(configEnv))
|
||||
if err = d.Decode(&c); err != nil {
|
||||
return err
|
||||
c.OpengistHome = filepath.Join(homeDir, ".opengist")
|
||||
}
|
||||
|
||||
if err = checks(c); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
C = c
|
||||
|
||||
if err = migrateConfig(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = os.Setenv("OG_OPENGIST_HOME_INTERNAL", GetHomeDir()); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -119,22 +172,62 @@ func InitLog() {
|
||||
if err := os.MkdirAll(filepath.Join(GetHomeDir(), "log"), 0755); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
file, err := os.OpenFile(filepath.Join(GetHomeDir(), "log", "opengist.log"), os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
var level zerolog.Level
|
||||
level, err = zerolog.ParseLevel(C.LogLevel)
|
||||
level, err := zerolog.ParseLevel(C.LogLevel)
|
||||
if err != nil {
|
||||
level = zerolog.InfoLevel
|
||||
}
|
||||
|
||||
if os.Getenv("DEV") == "1" {
|
||||
multi := zerolog.MultiLevelWriter(zerolog.NewConsoleWriter(), file)
|
||||
log.Logger = zerolog.New(multi).Level(level).With().Timestamp().Logger()
|
||||
} else {
|
||||
log.Logger = zerolog.New(file).Level(level).With().Timestamp().Logger()
|
||||
var logWriters []io.Writer
|
||||
logOutputTypes := strings.Split(strings.ToLower(C.LogOutput), ",")
|
||||
slices.Sort(logOutputTypes)
|
||||
logOutputTypes = slices.Compact(logOutputTypes)
|
||||
|
||||
consoleWriter := zerolog.NewConsoleWriter(
|
||||
func(w *zerolog.ConsoleWriter) {
|
||||
w.TimeFormat = time.TimeOnly
|
||||
w.FormatCaller = func(i interface{}) string {
|
||||
file := i.(string)
|
||||
index := strings.Index(file, "internal")
|
||||
if index == -1 {
|
||||
return file
|
||||
}
|
||||
return file[index:]
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
for _, logOutputType := range logOutputTypes {
|
||||
logOutputType = strings.TrimSpace(logOutputType)
|
||||
if !slices.Contains([]string{"stdout", "file"}, logOutputType) {
|
||||
defer func() { log.Warn().Msg("Invalid log output type: " + logOutputType) }()
|
||||
continue
|
||||
}
|
||||
|
||||
switch logOutputType {
|
||||
case "stdout":
|
||||
logWriters = append(logWriters, consoleWriter)
|
||||
defer func() { log.Debug().Msg("Logging to stdout") }()
|
||||
case "file":
|
||||
file, err := os.OpenFile(filepath.Join(GetHomeDir(), "log", "opengist.log"), os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
logWriters = append(logWriters, file)
|
||||
defer func() { log.Debug().Msg("Logging to file: " + file.Name()) }()
|
||||
}
|
||||
}
|
||||
if len(logWriters) == 0 {
|
||||
logWriters = append(logWriters, consoleWriter)
|
||||
defer func() { log.Warn().Msg("No valid log outputs, defaulting to stdout") }()
|
||||
}
|
||||
|
||||
multi := zerolog.MultiLevelWriter(logWriters...)
|
||||
log.Logger = zerolog.New(multi).Level(level).With().Caller().Timestamp().Logger()
|
||||
|
||||
if !slices.Contains([]string{"debug", "info", "warn", "error", "fatal"}, strings.ToLower(C.LogLevel)) {
|
||||
log.Warn().Msg("Invalid log level: " + C.LogLevel)
|
||||
}
|
||||
}
|
||||
|
||||
@ -152,8 +245,8 @@ func CheckGitVersion(version string) (bool, error) {
|
||||
return false, fmt.Errorf("invalid minor version number")
|
||||
}
|
||||
|
||||
// Check if version is prior to 2.20
|
||||
if major < 2 || (major == 2 && minor < 20) {
|
||||
// Check if version is prior to 2.28
|
||||
if major < 2 || (major == 2 && minor < 28) {
|
||||
return false, nil
|
||||
}
|
||||
return true, nil
|
||||
@ -163,3 +256,135 @@ func GetHomeDir() string {
|
||||
absolutePath, _ := filepath.Abs(C.OpengistHome)
|
||||
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 {
|
||||
if configPath != "" {
|
||||
absolutePath, _ := filepath.Abs(configPath)
|
||||
absolutePath = filepath.Clean(absolutePath)
|
||||
file, err := os.Open(absolutePath)
|
||||
if err != nil {
|
||||
if !os.IsNotExist(err) {
|
||||
return err
|
||||
}
|
||||
_, _ = fmt.Fprintln(out, "No YAML config file found at "+absolutePath)
|
||||
} else {
|
||||
_, _ = fmt.Fprintln(out, "Using YAML config file: "+absolutePath)
|
||||
|
||||
// Override default values with values from config.yml
|
||||
d := yaml.NewDecoder(file)
|
||||
if err = d.Decode(&c); err != nil {
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
}
|
||||
} else {
|
||||
_, _ = fmt.Fprintln(out, "No YAML config file specified.")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func loadConfigFromEnv(c *config, out io.Writer) error {
|
||||
v := reflect.ValueOf(c).Elem()
|
||||
var envVars []string
|
||||
|
||||
for i := 0; i < v.NumField(); i++ {
|
||||
tag := v.Type().Field(i).Tag.Get("env")
|
||||
|
||||
if tag == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
envValue := os.Getenv(strings.ToUpper(tag))
|
||||
if envValue == "" && v.Field(i).Kind() != reflect.Slice {
|
||||
continue
|
||||
}
|
||||
|
||||
switch v.Field(i).Kind() {
|
||||
case reflect.String:
|
||||
v.Field(i).SetString(envValue)
|
||||
envVars = append(envVars, tag)
|
||||
case reflect.Bool:
|
||||
boolVal, err := strconv.ParseBool(envValue)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
v.Field(i).SetBool(boolVal)
|
||||
envVars = append(envVars, tag)
|
||||
case reflect.Slice:
|
||||
if v.Type().Field(i).Type.Elem().Kind() == reflect.Struct {
|
||||
prefix := strings.ToUpper(tag) + "_"
|
||||
var sliceValue reflect.Value
|
||||
elemType := v.Type().Field(i).Type.Elem()
|
||||
|
||||
for index := 0; ; index++ {
|
||||
allFieldsPresent := true
|
||||
elemValue := reflect.New(elemType).Elem()
|
||||
|
||||
for j := 0; j < elemValue.NumField() && allFieldsPresent; j++ {
|
||||
elemField := elemValue.Type().Field(j)
|
||||
envName := fmt.Sprintf("%s%d_%s", prefix, index, strings.ToUpper(elemField.Name))
|
||||
envValue, present := os.LookupEnv(envName)
|
||||
|
||||
if !present {
|
||||
allFieldsPresent = false
|
||||
break
|
||||
}
|
||||
|
||||
envVars = append(envVars, envName)
|
||||
elemValue.Field(j).SetString(envValue)
|
||||
}
|
||||
|
||||
if !allFieldsPresent {
|
||||
break
|
||||
}
|
||||
|
||||
if sliceValue.Kind() != reflect.Slice {
|
||||
sliceValue = reflect.MakeSlice(v.Type().Field(i).Type, 0, index+1)
|
||||
}
|
||||
sliceValue = reflect.Append(sliceValue, elemValue)
|
||||
}
|
||||
|
||||
if sliceValue.IsValid() {
|
||||
v.Field(i).Set(sliceValue)
|
||||
}
|
||||
}
|
||||
default:
|
||||
return fmt.Errorf("unsupported type: %s", v.Field(i).Kind())
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if len(envVars) > 0 {
|
||||
_, _ = fmt.Fprintln(out, "Using environment variables config: "+strings.Join(envVars, ", "))
|
||||
} else {
|
||||
_, _ = fmt.Fprintln(out, "No environment variables config specified.")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func checks(c *config) error {
|
||||
if _, err := url.Parse(c.ExternalUrl); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := url.Parse(c.GiteaUrl); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := url.Parse(c.OIDCDiscoveryUrl); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
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)
|
||||
}
|
||||
}
|
@ -1,22 +1,31 @@
|
||||
package models
|
||||
package db
|
||||
|
||||
import (
|
||||
"gorm.io/gorm/clause"
|
||||
)
|
||||
|
||||
type AdminSetting struct {
|
||||
Key string `gorm:"uniqueIndex"`
|
||||
Key string `gorm:"index:,unique"`
|
||||
Value string
|
||||
}
|
||||
|
||||
const (
|
||||
SettingDisableSignup = "disable-signup"
|
||||
SettingRequireLogin = "require-login"
|
||||
SettingAllowGistsWithoutLogin = "allow-gists-without-login"
|
||||
SettingDisableLoginForm = "disable-login-form"
|
||||
SettingDisableGravatar = "disable-gravatar"
|
||||
)
|
||||
|
||||
func GetSetting(key string) (string, error) {
|
||||
var setting AdminSetting
|
||||
err := db.Where("key = ?", key).First(&setting).Error
|
||||
var err error
|
||||
switch db.Dialector.Name() {
|
||||
case "mysql", "sqlite":
|
||||
err = db.Where("`key` = ?", key).First(&setting).Error
|
||||
case "postgres":
|
||||
err = db.Where("key = ?", key).First(&setting).Error
|
||||
}
|
||||
return setting.Value, err
|
||||
}
|
||||
|
||||
@ -46,7 +55,7 @@ func UpdateSetting(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 {
|
||||
@ -60,3 +69,21 @@ func initAdminSettings(settings map[string]string) error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type AuthInfo struct{}
|
||||
|
||||
func (auth AuthInfo) RequireLogin() (bool, error) {
|
||||
s, err := GetSetting(SettingRequireLogin)
|
||||
if err != nil {
|
||||
return true, err
|
||||
}
|
||||
return s == "1", nil
|
||||
}
|
||||
|
||||
func (auth AuthInfo) AllowGistsWithoutLogin() (bool, error) {
|
||||
s, err := GetSetting(SettingAllowGistsWithoutLogin)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return s == "1", nil
|
||||
}
|
272
internal/db/db.go
Normal file
272
internal/db/db.go
Normal file
@ -0,0 +1,272 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/glebarez/sqlite"
|
||||
"gorm.io/driver/mysql"
|
||||
"gorm.io/driver/postgres"
|
||||
"gorm.io/gorm/logger"
|
||||
"net/url"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/thomiceli/opengist/internal/config"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
var db *gorm.DB
|
||||
|
||||
const (
|
||||
SQLite databaseType = iota
|
||||
PostgreSQL
|
||||
MySQL
|
||||
)
|
||||
|
||||
type databaseType int
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
u, err := url.Parse(uri)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid URI: %v", err)
|
||||
}
|
||||
|
||||
if u.Scheme == "" {
|
||||
info.Type = SQLite
|
||||
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
|
||||
}
|
||||
|
||||
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 {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = db.SetupJoinTable(&User{}, "Liked", &Like{}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = db.AutoMigrate(&User{}, &Gist{}, &SSHKey{}, &AdminSetting{}, &Invitation{}, &WebAuthnCredential{}, &TOTP{}, &GistTopic{}, &GistLanguage{}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = applyMigrations(dbInfo); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Default admin setting values
|
||||
return initAdminSettings(map[string]string{
|
||||
SettingDisableSignup: "0",
|
||||
SettingRequireLogin: "0",
|
||||
SettingAllowGistsWithoutLogin: "0",
|
||||
SettingDisableLoginForm: "0",
|
||||
SettingDisableGravatar: "0",
|
||||
})
|
||||
}
|
||||
|
||||
func Close() error {
|
||||
sqlDB, err := db.DB()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return sqlDB.Close()
|
||||
}
|
||||
|
||||
func CountAll(table interface{}) (int64, error) {
|
||||
var count int64
|
||||
err := db.Model(table).Count(&count).Error
|
||||
return count, err
|
||||
}
|
||||
|
||||
func IsUniqueConstraintViolation(err error) bool {
|
||||
return errors.Is(err, gorm.ErrDuplicatedKey)
|
||||
}
|
||||
|
||||
func Ping() error {
|
||||
sql, err := db.DB()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
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{})
|
||||
}
|
840
internal/db/gist.go
Normal file
840
internal/db/gist.go
Normal file
@ -0,0 +1,840 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/gob"
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/alecthomas/chroma/v2"
|
||||
"github.com/alecthomas/chroma/v2/lexers"
|
||||
"github.com/dustin/go-humanize"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/thomiceli/opengist/internal/git"
|
||||
"github.com/thomiceli/opengist/internal/index"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type Visibility int
|
||||
|
||||
const (
|
||||
PublicVisibility Visibility = iota
|
||||
UnlistedVisibility
|
||||
PrivateVisibility
|
||||
)
|
||||
|
||||
func (v Visibility) String() string {
|
||||
switch v {
|
||||
case PublicVisibility:
|
||||
return "public"
|
||||
case UnlistedVisibility:
|
||||
return "unlisted"
|
||||
case PrivateVisibility:
|
||||
return "private"
|
||||
default:
|
||||
return "???"
|
||||
}
|
||||
}
|
||||
|
||||
func (v Visibility) Uint() uint {
|
||||
return uint(v)
|
||||
}
|
||||
|
||||
func (v Visibility) Next() Visibility {
|
||||
switch v {
|
||||
case PublicVisibility:
|
||||
return UnlistedVisibility
|
||||
case UnlistedVisibility:
|
||||
return PrivateVisibility
|
||||
default:
|
||||
return PublicVisibility
|
||||
}
|
||||
}
|
||||
|
||||
func ParseVisibility[T string | int](v T) Visibility {
|
||||
switch s := fmt.Sprint(v); s {
|
||||
case "0", "public":
|
||||
return PublicVisibility
|
||||
case "1", "unlisted":
|
||||
return UnlistedVisibility
|
||||
case "2", "private":
|
||||
return PrivateVisibility
|
||||
default:
|
||||
return PublicVisibility
|
||||
}
|
||||
}
|
||||
|
||||
type Gist struct {
|
||||
ID uint `gorm:"primaryKey"`
|
||||
Uuid string
|
||||
Title string
|
||||
URL string
|
||||
Preview string
|
||||
PreviewFilename string
|
||||
Description string
|
||||
Private Visibility // 0: public, 1: unlisted, 2: private
|
||||
UserID uint
|
||||
User User
|
||||
NbFiles int
|
||||
NbLikes int
|
||||
NbForks int
|
||||
CreatedAt int64
|
||||
UpdatedAt int64
|
||||
|
||||
Likes []User `gorm:"many2many:likes;constraint:OnUpdate:CASCADE,OnDelete:CASCADE"`
|
||||
Forked *Gist `gorm:"foreignKey:ForkedID;constraint:OnUpdate:CASCADE,OnDelete:SET NULL"`
|
||||
ForkedID uint
|
||||
|
||||
Topics []GistTopic `gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE"`
|
||||
Languages []GistLanguage `gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE"`
|
||||
}
|
||||
|
||||
type Like struct {
|
||||
UserID uint `gorm:"primaryKey"`
|
||||
GistID uint `gorm:"primaryKey"`
|
||||
CreatedAt int64
|
||||
}
|
||||
|
||||
func (gist *Gist) BeforeDelete(tx *gorm.DB) error {
|
||||
// Decrement fork counter if the gist was forked
|
||||
err := tx.Model(&Gist{}).
|
||||
Omit("updated_at").
|
||||
Where("id = ?", gist.ForkedID).
|
||||
UpdateColumn("nb_forks", gorm.Expr("nb_forks - 1")).Error
|
||||
return err
|
||||
}
|
||||
|
||||
func GetGist(user string, gistUuid string) (*Gist, error) {
|
||||
gist := new(Gist)
|
||||
err := db.Preload("User").Preload("Forked.User").Preload("Topics").
|
||||
Where("(gists.uuid like ? OR gists.url = ?) AND users.username like ?", gistUuid+"%", gistUuid, user).
|
||||
Joins("join users on gists.user_id = users.id").
|
||||
First(&gist).Error
|
||||
|
||||
return gist, err
|
||||
}
|
||||
|
||||
func GetGistByID(gistId string) (*Gist, error) {
|
||||
gist := new(Gist)
|
||||
err := db.Preload("User").Preload("Forked.User").Preload("Topics").
|
||||
Where("gists.id = ?", gistId).
|
||||
First(&gist).Error
|
||||
|
||||
return gist, err
|
||||
}
|
||||
|
||||
func GetAllGistsForCurrentUser(currentUserId uint, offset int, sort string, order string) ([]*Gist, error) {
|
||||
var gists []*Gist
|
||||
err := db.Preload("User").
|
||||
Preload("Forked.User").
|
||||
Preload("Topics").
|
||||
Where("gists.private = 0 or gists.user_id = ?", currentUserId).
|
||||
Limit(11).
|
||||
Offset(offset * 10).
|
||||
Order(sort + "_at " + order).
|
||||
Find(&gists).Error
|
||||
|
||||
return gists, err
|
||||
}
|
||||
|
||||
func GetAllGists(offset int) ([]*Gist, error) {
|
||||
var gists []*Gist
|
||||
err := db.Preload("User").
|
||||
Limit(11).
|
||||
Offset(offset * 10).
|
||||
Order("id asc").
|
||||
Find(&gists).Error
|
||||
|
||||
return gists, err
|
||||
}
|
||||
|
||||
func GetAllGistsFromSearch(currentUserId uint, query string, offset int, sort string, order string, topic string) ([]*Gist, error) {
|
||||
var gists []*Gist
|
||||
tx := db.Preload("User").Preload("Forked.User").Preload("Topics").
|
||||
Where("((gists.private = 0) or (gists.private > 0 and gists.user_id = ?))", currentUserId).
|
||||
Where("gists.title like ? or gists.description like ?", "%"+query+"%", "%"+query+"%")
|
||||
|
||||
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).
|
||||
Order("gists." + sort + "_at " + order).
|
||||
Find(&gists).Error
|
||||
|
||||
return gists, err
|
||||
}
|
||||
|
||||
func gistsFromUserStatement(fromUserId uint, currentUserId uint) *gorm.DB {
|
||||
return db.Preload("User").Preload("Forked.User").Preload("Topics").
|
||||
Where("((gists.private = 0) or (gists.private > 0 and gists.user_id = ?))", currentUserId).
|
||||
Where("users.id = ?", fromUserId).
|
||||
Joins("join users on gists.user_id = users.id")
|
||||
}
|
||||
|
||||
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 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).
|
||||
Order("gists." + sort + "_at " + order).
|
||||
Find(&gists).Error
|
||||
|
||||
return gists, count, err
|
||||
}
|
||||
|
||||
func CountAllGistsFromUser(fromUserId uint, currentUserId uint) (int64, error) {
|
||||
var count int64
|
||||
err := gistsFromUserStatement(fromUserId, currentUserId).Model(&Gist{}).Count(&count).Error
|
||||
return count, err
|
||||
}
|
||||
|
||||
func likedStatement(fromUserId uint, currentUserId uint) *gorm.DB {
|
||||
return db.Preload("User").Preload("Forked.User").Preload("Topics").
|
||||
Where("((gists.private = 0) or (gists.private > 0 and gists.user_id = ?))", currentUserId).
|
||||
Where("likes.user_id = ?", fromUserId).
|
||||
Joins("join likes on gists.id = likes.gist_id").
|
||||
Joins("join users on likes.user_id = users.id")
|
||||
}
|
||||
|
||||
func GetAllGistsLikedByUser(fromUserId uint, currentUserId uint, offset int, sort string, order string) ([]*Gist, error) {
|
||||
var gists []*Gist
|
||||
err := likedStatement(fromUserId, currentUserId).Limit(11).
|
||||
Offset(offset * 10).
|
||||
Order("gists." + sort + "_at " + order).
|
||||
Find(&gists).Error
|
||||
return gists, err
|
||||
}
|
||||
|
||||
func CountAllGistsLikedByUser(fromUserId uint, currentUserId uint) (int64, error) {
|
||||
var count int64
|
||||
err := likedStatement(fromUserId, currentUserId).Model(&Gist{}).Count(&count).Error
|
||||
return count, err
|
||||
}
|
||||
|
||||
func forkedStatement(fromUserId uint, currentUserId uint) *gorm.DB {
|
||||
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.user_id = ?", fromUserId).
|
||||
Joins("join users on gists.user_id = users.id")
|
||||
}
|
||||
|
||||
func GetAllGistsForkedByUser(fromUserId uint, currentUserId uint, offset int, sort string, order string) ([]*Gist, error) {
|
||||
var gists []*Gist
|
||||
err := forkedStatement(fromUserId, currentUserId).Limit(11).
|
||||
Offset(offset * 10).
|
||||
Order("gists." + sort + "_at " + order).
|
||||
Find(&gists).Error
|
||||
return gists, err
|
||||
}
|
||||
|
||||
func CountAllGistsForkedByUser(fromUserId uint, currentUserId uint) (int64, error) {
|
||||
var count int64
|
||||
err := forkedStatement(fromUserId, currentUserId).Model(&Gist{}).Count(&count).Error
|
||||
return count, err
|
||||
}
|
||||
|
||||
func GetAllGistsRows() ([]*Gist, error) {
|
||||
var gists []*Gist
|
||||
err := db.Table("gists").
|
||||
Preload("User").
|
||||
Find(&gists).Error
|
||||
|
||||
return gists, err
|
||||
}
|
||||
|
||||
func GetAllGistsVisibleByUser(userId uint) ([]uint, error) {
|
||||
var gists []uint
|
||||
|
||||
err := db.Table("gists").
|
||||
Where("gists.private = 0 or gists.user_id = ?", userId).
|
||||
Pluck("gists.id", &gists).Error
|
||||
|
||||
return gists, err
|
||||
}
|
||||
|
||||
func GetAllGistsByIds(ids []uint) ([]*Gist, error) {
|
||||
var gists []*Gist
|
||||
err := db.Preload("User").Preload("Forked.User").Preload("Topics").
|
||||
Where("id in ?", ids).
|
||||
Find(&gists).Error
|
||||
|
||||
// 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 {
|
||||
// avoids foreign key constraint error because the default value in the struct is 0
|
||||
return db.Omit("forked_id").Create(&gist).Error
|
||||
}
|
||||
|
||||
func (gist *Gist) CreateForked() error {
|
||||
return db.Create(&gist).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
|
||||
}
|
||||
|
||||
func (gist *Gist) UpdateNoTimestamps() error {
|
||||
return db.Omit("forked_id", "updated_at").Save(&gist).Error
|
||||
}
|
||||
|
||||
func (gist *Gist) Delete() error {
|
||||
err := gist.DeleteRepository()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return db.Delete(&gist).Error
|
||||
}
|
||||
|
||||
func (gist *Gist) SetLastActiveNow() error {
|
||||
return db.Model(&Gist{}).
|
||||
Where("id = ?", gist.ID).
|
||||
Update("updated_at", time.Now().Unix()).Error
|
||||
}
|
||||
|
||||
func (gist *Gist) AppendUserLike(user *User) error {
|
||||
err := db.Model(&gist).Omit("updated_at").Update("nb_likes", gist.NbLikes+1).Error
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return db.Model(&gist).Omit("updated_at").Association("Likes").Append(user)
|
||||
}
|
||||
|
||||
func (gist *Gist) RemoveUserLike(user *User) error {
|
||||
err := db.Model(&gist).Omit("updated_at").Update("nb_likes", gist.NbLikes-1).Error
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return db.Model(&gist).Omit("updated_at").Association("Likes").Delete(user)
|
||||
}
|
||||
|
||||
func (gist *Gist) IncrementForkCount() error {
|
||||
return db.Model(&gist).Omit("updated_at").Update("nb_forks", gist.NbForks+1).Error
|
||||
}
|
||||
|
||||
func (gist *Gist) GetForkParent(user *User) (*Gist, error) {
|
||||
fork := new(Gist)
|
||||
err := db.Preload("User").
|
||||
Where("forked_id = ? and user_id = ?", gist.ID, user.ID).
|
||||
First(&fork).Error
|
||||
return fork, err
|
||||
}
|
||||
|
||||
func (gist *Gist) GetUsersLikes(offset int) ([]*User, error) {
|
||||
var users []*User
|
||||
err := db.Model(&gist).
|
||||
Where("gist_id = ?", gist.ID).
|
||||
Limit(31).
|
||||
Offset(offset * 30).
|
||||
Association("Likes").Find(&users)
|
||||
return users, err
|
||||
}
|
||||
|
||||
func (gist *Gist) GetForks(currentUserId uint, offset int) ([]*Gist, error) {
|
||||
var gists []*Gist
|
||||
err := db.Model(&gist).Preload("User").
|
||||
Where("forked_id = ?", gist.ID).
|
||||
Where("(gists.private = 0) or (gists.private > 0 and gists.user_id = ?)", currentUserId).
|
||||
Limit(11).
|
||||
Offset(offset * 10).
|
||||
Order("updated_at desc").
|
||||
Find(&gists).Error
|
||||
|
||||
return gists, err
|
||||
}
|
||||
|
||||
func (gist *Gist) CanWrite(user *User) bool {
|
||||
return !(user == nil) && (gist.UserID == user.ID)
|
||||
}
|
||||
|
||||
func (gist *Gist) InitRepository() error {
|
||||
return git.InitRepository(gist.User.Username, gist.Uuid)
|
||||
}
|
||||
|
||||
func (gist *Gist) DeleteRepository() error {
|
||||
return git.DeleteRepository(gist.User.Username, gist.Uuid)
|
||||
}
|
||||
|
||||
func (gist *Gist) Files(revision string, truncate bool) ([]*git.File, error) {
|
||||
filesCat, err := git.CatFileBatch(gist.User.Username, gist.Uuid, revision, truncate)
|
||||
if err != nil {
|
||||
// if the revision or the file do not exist
|
||||
if exiterr, ok := err.(*exec.ExitError); ok && exiterr.ExitCode() == 128 {
|
||||
return nil, &git.RevisionNotFoundError{}
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var files []*git.File
|
||||
for _, fileCat := range filesCat {
|
||||
files = append(files, &git.File{
|
||||
Filename: fileCat.Name,
|
||||
Size: fileCat.Size,
|
||||
HumanSize: humanize.IBytes(fileCat.Size),
|
||||
Content: fileCat.Content,
|
||||
Truncated: fileCat.Truncated,
|
||||
})
|
||||
}
|
||||
return files, err
|
||||
}
|
||||
|
||||
func (gist *Gist) File(revision string, filename string, truncate bool) (*git.File, error) {
|
||||
content, truncated, err := git.GetFileContent(gist.User.Username, gist.Uuid, revision, filename, truncate)
|
||||
|
||||
// if the revision or the file do not exist
|
||||
if exiterr, ok := err.(*exec.ExitError); ok && exiterr.ExitCode() == 128 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
var size uint64
|
||||
|
||||
size, err = git.GetFileSize(gist.User.Username, gist.Uuid, revision, filename)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &git.File{
|
||||
Filename: filename,
|
||||
Size: size,
|
||||
HumanSize: humanize.IBytes(size),
|
||||
Content: content,
|
||||
Truncated: truncated,
|
||||
}, err
|
||||
}
|
||||
|
||||
func (gist *Gist) FileNames(revision string) ([]string, error) {
|
||||
return git.GetFilesOfRepository(gist.User.Username, gist.Uuid, revision)
|
||||
}
|
||||
|
||||
func (gist *Gist) Log(skip int) ([]*git.Commit, error) {
|
||||
return git.GetLog(gist.User.Username, gist.Uuid, skip)
|
||||
}
|
||||
|
||||
func (gist *Gist) NbCommits() (string, error) {
|
||||
return git.CountCommits(gist.User.Username, gist.Uuid)
|
||||
}
|
||||
|
||||
func (gist *Gist) AddAndCommitFiles(files *[]FileDTO) error {
|
||||
if err := git.CloneTmp(gist.User.Username, gist.Uuid, gist.Uuid, gist.User.Email, true); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, file := range *files {
|
||||
if err := git.SetFileContent(gist.Uuid, file.Filename, file.Content); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if err := git.AddAll(gist.Uuid); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := git.CommitRepository(gist.Uuid, gist.User.Username, gist.User.Email); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return git.Push(gist.Uuid)
|
||||
}
|
||||
|
||||
func (gist *Gist) AddAndCommitFile(file *FileDTO) error {
|
||||
if err := git.CloneTmp(gist.User.Username, gist.Uuid, gist.Uuid, gist.User.Email, false); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := git.SetFileContent(gist.Uuid, file.Filename, file.Content); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := git.AddAll(gist.Uuid); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := git.CommitRepository(gist.Uuid, gist.User.Username, gist.User.Email); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return git.Push(gist.Uuid)
|
||||
}
|
||||
|
||||
func (gist *Gist) ForkClone(username string, uuid string) error {
|
||||
return git.ForkClone(gist.User.Username, gist.Uuid, username, uuid)
|
||||
}
|
||||
|
||||
func (gist *Gist) UpdateServerInfo() error {
|
||||
return git.UpdateServerInfo(gist.User.Username, gist.Uuid)
|
||||
}
|
||||
|
||||
func (gist *Gist) RPC(service string) ([]byte, error) {
|
||||
return git.RPC(gist.User.Username, gist.Uuid, service)
|
||||
}
|
||||
|
||||
func (gist *Gist) UpdatePreviewAndCount(withTimestampUpdate bool) error {
|
||||
filesStr, err := git.GetFilesOfRepository(gist.User.Username, gist.Uuid, "HEAD")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
gist.NbFiles = len(filesStr)
|
||||
|
||||
if len(filesStr) == 0 {
|
||||
gist.Preview = ""
|
||||
gist.PreviewFilename = ""
|
||||
} else {
|
||||
file, err := gist.File("HEAD", filesStr[0], true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
split := strings.Split(file.Content, "\n")
|
||||
if len(split) > 10 {
|
||||
gist.Preview = strings.Join(split[:10], "\n")
|
||||
} else {
|
||||
gist.Preview = file.Content
|
||||
}
|
||||
|
||||
gist.PreviewFilename = file.Filename
|
||||
}
|
||||
|
||||
if withTimestampUpdate {
|
||||
return gist.Update()
|
||||
}
|
||||
return gist.UpdateNoTimestamps()
|
||||
}
|
||||
|
||||
func (gist *Gist) VisibilityStr() string {
|
||||
switch gist.Private {
|
||||
case PublicVisibility:
|
||||
return "public"
|
||||
case UnlistedVisibility:
|
||||
return "unlisted"
|
||||
case PrivateVisibility:
|
||||
return "private"
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func (gist *Gist) Identifier() string {
|
||||
if gist.URL != "" {
|
||||
return gist.URL
|
||||
}
|
||||
return gist.Uuid
|
||||
}
|
||||
|
||||
func (gist *Gist) GetLanguagesFromFiles() ([]string, error) {
|
||||
files, err := gist.Files("HEAD", true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
languages := make([]string, 0, len(files))
|
||||
for _, file := range files {
|
||||
var lexer chroma.Lexer
|
||||
if lexer = lexers.Get(file.Filename); lexer == nil {
|
||||
lexer = lexers.Fallback
|
||||
}
|
||||
|
||||
fileType := lexer.Config().Name
|
||||
if lexer.Config().Name == "fallback" || lexer.Config().Name == "plaintext" {
|
||||
fileType = "Text"
|
||||
}
|
||||
|
||||
languages = append(languages, fileType)
|
||||
}
|
||||
|
||||
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) SerialiseInitRepository() error {
|
||||
var gobBuffer bytes.Buffer
|
||||
encoder := gob.NewEncoder(&gobBuffer)
|
||||
if err := encoder.Encode(gist); err != nil {
|
||||
return fmt.Errorf("gob encoding error: %v", err)
|
||||
}
|
||||
|
||||
return git.SerialiseInitRepository(gist.User.Username, gobBuffer.Bytes())
|
||||
}
|
||||
|
||||
func DeserialiseInitRepository(user string) (*Gist, error) {
|
||||
data, err := git.DeserialiseInitRepository(user)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var gist Gist
|
||||
decoder := gob.NewDecoder(bytes.NewReader(data))
|
||||
if err := decoder.Decode(&gist); err != nil {
|
||||
return nil, fmt.Errorf("gob decoding error: %v", err)
|
||||
}
|
||||
return &gist, nil
|
||||
}
|
||||
|
||||
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 {
|
||||
fileDTOs = append(fileDTOs, FileDTO{
|
||||
Filename: file.Filename,
|
||||
Content: file.Content,
|
||||
})
|
||||
}
|
||||
|
||||
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 -- //
|
||||
|
||||
type GistDTO struct {
|
||||
Title string `validate:"max=250" form:"title"`
|
||||
Description string `validate:"max=1000" form:"description"`
|
||||
URL string `validate:"max=32,alphanumdashorempty" form:"url"`
|
||||
Files []FileDTO `validate:"min=1,dive"`
|
||||
Name []string `form:"name"`
|
||||
Content []string `form:"content"`
|
||||
Topics string `validate:"gisttopics" form:"topics"`
|
||||
VisibilityDTO
|
||||
}
|
||||
|
||||
func (dto *GistDTO) HasMetadata() bool {
|
||||
return dto.Title != "" || dto.Description != "" || dto.URL != "" || dto.Topics != ""
|
||||
}
|
||||
|
||||
type VisibilityDTO struct {
|
||||
Private Visibility `validate:"number,min=0,max=2" form:"private"`
|
||||
}
|
||||
|
||||
type FileDTO struct {
|
||||
Filename string `validate:"excludes=\x2f,excludes=\x5c,max=255"`
|
||||
Content string `validate:"required"`
|
||||
}
|
||||
|
||||
func (dto *GistDTO) ToGist() *Gist {
|
||||
return &Gist{
|
||||
Title: dto.Title,
|
||||
Description: dto.Description,
|
||||
Private: dto.Private,
|
||||
URL: dto.URL,
|
||||
Topics: dto.TopicStrToSlice(),
|
||||
}
|
||||
}
|
||||
|
||||
func (dto *GistDTO) ToExistingGist(gist *Gist) *Gist {
|
||||
gist.Title = dto.Title
|
||||
gist.Description = dto.Description
|
||||
gist.URL = dto.URL
|
||||
gist.Topics = dto.TopicStrToSlice()
|
||||
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 -- //
|
||||
|
||||
func (gist *Gist) ToIndexedGist() (*index.Gist, error) {
|
||||
files, err := gist.Files("HEAD", true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
exts := make([]string, 0, len(files))
|
||||
wholeContent := ""
|
||||
for _, file := range files {
|
||||
wholeContent += file.Content
|
||||
if !strings.HasSuffix(wholeContent, "\n") {
|
||||
wholeContent += "\n"
|
||||
}
|
||||
exts = append(exts, filepath.Ext(file.Filename))
|
||||
}
|
||||
|
||||
fileNames, err := gist.FileNames("HEAD")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
langs, err := gist.GetLanguagesFromFiles()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
topics, err := gist.GetTopics()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
indexedGist := &index.Gist{
|
||||
GistID: gist.ID,
|
||||
UserID: gist.UserID,
|
||||
Visibility: gist.Private.Uint(),
|
||||
Username: gist.User.Username,
|
||||
Title: gist.Title,
|
||||
Content: wholeContent,
|
||||
Filenames: fileNames,
|
||||
Extensions: exts,
|
||||
Languages: langs,
|
||||
Topics: topics,
|
||||
CreatedAt: gist.CreatedAt,
|
||||
UpdatedAt: gist.UpdatedAt,
|
||||
}
|
||||
|
||||
return indexedGist, nil
|
||||
}
|
||||
|
||||
func (gist *Gist) AddInIndex() {
|
||||
if !index.IndexEnabled() {
|
||||
return
|
||||
}
|
||||
|
||||
go func() {
|
||||
indexedGist, err := gist.ToIndexedGist()
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msgf("Cannot convert gist %d to indexed gist", gist.ID)
|
||||
return
|
||||
}
|
||||
err = index.AddInIndex(indexedGist)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msgf("Error adding gist %d to index", gist.ID)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func (gist *Gist) RemoveFromIndex() {
|
||||
if !index.IndexEnabled() {
|
||||
return
|
||||
}
|
||||
|
||||
go func() {
|
||||
err := index.RemoveFromIndex(gist.ID)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msgf("Error remove gist %d from index", gist.ID)
|
||||
}
|
||||
}()
|
||||
}
|
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"`
|
||||
}
|
99
internal/db/invitation.go
Normal file
99
internal/db/invitation.go
Normal file
@ -0,0 +1,99 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Invitation struct {
|
||||
ID uint `gorm:"primaryKey"`
|
||||
Code string
|
||||
ExpiresAt int64
|
||||
NbUsed uint
|
||||
NbMax uint
|
||||
}
|
||||
|
||||
func GetAllInvitations() ([]*Invitation, error) {
|
||||
var invitations []*Invitation
|
||||
dialect := db.Dialector.Name()
|
||||
query := db.Model(&Invitation{})
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
func GetInvitationByID(id uint) (*Invitation, error) {
|
||||
invitation := new(Invitation)
|
||||
err := db.
|
||||
Where("id = ?", id).
|
||||
First(&invitation).Error
|
||||
return invitation, err
|
||||
}
|
||||
|
||||
func GetInvitationByCode(code string) (*Invitation, error) {
|
||||
invitation := new(Invitation)
|
||||
err := db.
|
||||
Where("code = ?", code).
|
||||
First(&invitation).Error
|
||||
return invitation, err
|
||||
}
|
||||
|
||||
func InvitationCodeExists(code string) (bool, error) {
|
||||
var count int64
|
||||
err := db.Model(&Invitation{}).Where("code = ?", code).Count(&count).Error
|
||||
return count > 0, err
|
||||
}
|
||||
|
||||
func (i *Invitation) Create() error {
|
||||
i.Code = generateRandomCode()
|
||||
return db.Create(&i).Error
|
||||
}
|
||||
|
||||
func (i *Invitation) Update() error {
|
||||
return db.Save(&i).Error
|
||||
}
|
||||
|
||||
func (i *Invitation) Delete() error {
|
||||
return db.Delete(&i).Error
|
||||
}
|
||||
|
||||
func (i *Invitation) IsExpired() bool {
|
||||
return i.ExpiresAt < time.Now().Unix()
|
||||
}
|
||||
|
||||
func (i *Invitation) IsMaxedOut() bool {
|
||||
return i.NbMax > 0 && i.NbUsed >= i.NbMax
|
||||
}
|
||||
|
||||
func (i *Invitation) IsUsable() bool {
|
||||
return !i.IsExpired() && !i.IsMaxedOut()
|
||||
}
|
||||
|
||||
func (i *Invitation) Use() error {
|
||||
i.NbUsed++
|
||||
return i.Update()
|
||||
}
|
||||
|
||||
func generateRandomCode() string {
|
||||
const charset = "0123456789ABCDEF"
|
||||
var seededRand = rand.New(rand.NewSource(time.Now().UnixNano()))
|
||||
result := make([]byte, 16)
|
||||
|
||||
for i := range result {
|
||||
result[i] = charset[seededRand.Intn(len(charset))]
|
||||
}
|
||||
return string(result)
|
||||
}
|
@ -1,9 +1,8 @@
|
||||
package models
|
||||
package db
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/rs/zerolog/log"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type MigrationVersion struct {
|
||||
@ -11,7 +10,19 @@ type MigrationVersion struct {
|
||||
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
|
||||
if err := db.AutoMigrate(&MigrationVersion{}); err != nil {
|
||||
log.Fatal().Err(err).Msg("Error creating migration version table")
|
||||
@ -25,9 +36,10 @@ func ApplyMigrations(db *gorm.DB) error {
|
||||
// Define migrations
|
||||
migrations := []struct {
|
||||
Version uint
|
||||
Func func(*gorm.DB) error
|
||||
Func func() error
|
||||
}{
|
||||
{1, v1_modifyConstraintToSSHKeys},
|
||||
{2, v2_lowercaseEmails},
|
||||
// Add more migrations here as needed
|
||||
}
|
||||
|
||||
@ -40,7 +52,7 @@ func ApplyMigrations(db *gorm.DB) error {
|
||||
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))
|
||||
tx.Rollback()
|
||||
return err
|
||||
@ -60,7 +72,7 @@ func ApplyMigrations(db *gorm.DB) error {
|
||||
}
|
||||
|
||||
// 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 := `
|
||||
CREATE TABLE ssh_keys_temp (
|
||||
id integer primary key,
|
||||
@ -94,3 +106,9 @@ func v1_modifyConstraintToSSHKeys(db *gorm.DB) error {
|
||||
renameSQL := `ALTER TABLE ssh_keys_temp RENAME TO ssh_keys;`
|
||||
return db.Exec(renameSQL).Error
|
||||
}
|
||||
|
||||
func v2_lowercaseEmails() error {
|
||||
// Copy the lowercase emails into the new column
|
||||
copySQL := `UPDATE users SET email = lower(email);`
|
||||
return db.Exec(copySQL).Error
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user