Compare commits

..

No commits in common. "master" and "v1.5.1" have entirely different histories.

248 changed files with 8990 additions and 21780 deletions

1
.github/FUNDING.yml vendored
View File

@ -1 +0,0 @@
github: thomiceli

View File

@ -1,47 +0,0 @@
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 }}

View File

@ -4,105 +4,50 @@ on:
branches:
- master
- 'dev-*'
workflow_dispatch:
pull_request:
paths-ignore:
- '**.yml'
- '**.md'
jobs:
lint:
name: Lint
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Set up Go 1.23
- name: Set up Go 1.20
uses: actions/setup-go@v4
with:
go-version: "1.23"
go-version: "1.20"
- name: Lint
uses: golangci/golangci-lint-action@v6
uses: golangci/golangci-lint-action@v3
with:
version: v1.60
args: --out-format=colored-line-number --timeout=20m
version: v1.54
skip-pkg-cache: true
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
- name: Set up Go 1.20
uses: actions/setup-go@v4
with:
go-version: "1.23"
go-version: "1.20"
- name: Check Go modules
- name: Check
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"]
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"]
go: ["1.20", "1.21"]
runs-on: ${{ matrix.os }}
steps:
- name: Checkout
@ -114,25 +59,5 @@ jobs:
go-version: ${{ matrix.go }}
- name: Run tests
run: make test TEST_DB_TYPE=${{ matrix.database }}
run: make test
build:
name: Build
strategy:
fail-fast: false
matrix:
os: ["ubuntu-latest", "macOS-latest", "windows-latest"]
go: ["1.23"]
runs-on: ${{ matrix.os }}
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Set up Go 1.23
uses: actions/setup-go@v4
with:
go-version: ${{ matrix.go }}
- name: Build
shell: bash
run: make

View File

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

View File

@ -13,10 +13,10 @@ jobs:
- name: Checkout
uses: actions/checkout@v3
- name: Set up Go 1.23
- name: Set up Go 1.20
uses: actions/setup-go@v4
with:
go-version: "1.23"
go-version: "1.20"
- name: Cross compile build
run: make all_crosscompile
@ -46,7 +46,6 @@ 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}}
@ -66,12 +65,6 @@ 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:
@ -81,4 +74,4 @@ jobs:
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
cache-to: type=gha,mode=max

6
.gitignore vendored
View File

@ -1,15 +1,9 @@
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/

View File

@ -1,331 +1,5 @@
# 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)

View File

@ -1,25 +1,16 @@
FROM alpine:3.19 AS base
FROM alpine:3.17 AS build
RUN apk update && \
apk add --no-cache \
make \
shadow \
openssl \
openssh \
curl \
wget \
git \
gnupg \
xz \
gcc \
musl-dev \
libstdc++
apk add --no-cache \
make \
gcc \
musl-dev \
libstdc++
COPY --from=golang:1.23-alpine /usr/local/go/ /usr/local/go/
COPY --from=golang:1.20-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/
COPY --from=node:18-alpine /usr/local/ /usr/local/
ENV NODE_PATH="/usr/local/lib/node_modules"
ENV PATH="/usr/local/bin:${PATH}"
@ -27,21 +18,10 @@ WORKDIR /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
FROM alpine:3.17 as run
RUN apk update && \
apk add --no-cache \
@ -58,9 +38,7 @@ RUN apk update && \
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
adduser -S -G opengist -H -s /bin/ash -g 'Opengist User' opengist
WORKDIR /app/opengist
@ -69,5 +47,4 @@ COPY --from=build --chown=opengist:opengist /opengist/docker ./docker
EXPOSE 6157 2222
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"]

View File

@ -1,10 +1,7 @@
.PHONY: all all_crosscompile install build_frontend build_backend build build_crosscompile build_docker build_dev_docker run_dev_docker watch_frontend watch_backend watch clean clean_docker check_changes go_mod fmt test check-tr
.PHONY: all all_crosscompile install build_frontend build_backend build build_crosscompile build_docker watch_frontend watch_backend watch clean clean_docker check_changes go_mod fmt test
# 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: clean install build
@ -18,12 +15,11 @@ install:
build_frontend:
@echo "Building frontend assets..."
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
npx vite build
build_backend:
@echo "Building Opengist binary..."
go build -tags fs_embed -ldflags "-X $(VERSION_PKG)=$(GIT_TAG)" -o $(BINARY_NAME) .
go build -tags fs_embed -o $(BINARY_NAME) .
build: build_frontend build_backend
@ -34,23 +30,16 @@ 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..."
npx vite -c public/vite.config.js dev --port 16157 --host
npx vite dev --port 16157
watch_backend:
@echo "Building Opengist binary..."
OG_DEV=1 npx nodemon --watch '**/*' -e html,yml,go,js --signal SIGTERM --exec 'go run -ldflags "-X $(VERSION_PKG)=$(GIT_TAG)" . --config config.yml'
OG_DEV=1 npx nodemon --watch '**/*' -e html,yml,go,js --signal SIGTERM --exec 'go run . --config config.yml'
watch:
@sh ./scripts/watch.sh
@bash ./scripts/watch.sh
clean:
@echo "Cleaning up build artifacts..."
@ -73,7 +62,4 @@ fmt:
@go fmt ./...
test:
@OPENGIST_TEST_DB=$(TEST_DB_TYPE) go test ./... -p 1
check-tr:
@bash ./scripts/check-translations.sh
@go test ./... -p 1

View File

@ -1,35 +1,33 @@
# Opengist
<img height="108px" src="https://raw.githubusercontent.com/thomiceli/opengist/master/public/opengist.svg" alt="Opengist" align="right" />
<img height="108px" src="https://raw.githubusercontent.com/thomiceli/opengist/a9dd531f676d01b93bb6bd70751a69382ca563b0/public/opengist.svg" alt="Opengist" align="right" />
Opengist is a **self-hosted** Pastebin **powered by Git**. All snippets are stored in a Git repository and can be
Opengist is a **self-hosted** pastebin **powered by Git**. All snippets are stored in a Git repository and can be
read and/or modified using standard Git commands, or with the web interface.
It is similar to [GitHub Gist](https://gist.github.com/), but open-source and could be self-hosted.
It is similiar 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)
[Documentation](/docs) • [Demo](https://opengist.thomice.li)
![GitHub release (latest SemVer)](https://img.shields.io/github/v/release/thomiceli/opengist?sort=semver)
![License](https://img.shields.io/github/license/thomiceli/opengist?color=blue)
[![Go CI](https://github.com/thomiceli/opengist/actions/workflows/go.yml/badge.svg)](https://github.com/thomiceli/opengist/actions/workflows/go.yml)
[![Go Report Card](https://goreportcard.com/badge/github.com/thomiceli/opengist)](https://goreportcard.com/report/github.com/thomiceli/opengist)
[![Translate](https://tr.opengist.io/widget/_/svg-badge.svg)](https://tr.opengist.io/projects/_/opengist/)
## Features
* 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
* Syntax highlighting ; markdown & CSV support
* Like / Fork snippets
* Search for snippets ; browse users snippets, likes and forks
* Download raw files or as a ZIP archive
* OAuth2 login with GitHub, GitLab, Gitea, and OpenID Connect
* OAuth2 login with GitHub, Gitea, and OpenID Connect
* Restrict or unrestrict snippets visibility to anonymous users
* Docker support / Helm Chart
* [More...](/docs/introduction.md#features)
* Docker support
* [More...](/docs/index.md#features)
## Quick start
@ -38,7 +36,7 @@ It is similar to [GitHub Gist](https://gist.github.com/), but open-source and co
Docker [images](https://github.com/thomiceli/opengist/pkgs/container/opengist) are available for each release :
```shell
docker pull ghcr.io/thomiceli/opengist:1.10
docker pull ghcr.io/thomiceli/opengist:1
```
It can be used in a `docker-compose.yml` file :
@ -48,9 +46,11 @@ 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.10
image: ghcr.io/thomiceli/opengist:1
container_name: opengist
restart: unless-stopped
ports:
@ -77,19 +77,17 @@ Download the archive for your system from the release page [here](https://github
```shell
# example for linux amd64
wget https://github.com/thomiceli/opengist/releases/download/v1.10.0/opengist1.10.0-linux-amd64.tar.gz
wget https://github.com/thomiceli/opengist/releases/download/v1.5.1/opengist1.5.1-linux-amd64.tar.gz
tar xzvf opengist1.10.0-linux-amd64.tar.gz
tar xzvf opengist1.5.1-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.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)
Requirements : [Git](https://git-scm.com/downloads) (2.20+), [Go](https://go.dev/doc/install) (1.20+), [Node.js](https://nodejs.org/en/download/) (16+)
```shell
git clone https://github.com/thomiceli/opengist
@ -100,13 +98,10 @@ make
Opengist is now running on port 6157, you can browse http://localhost:6157
---
To create and run a development environment, see [run-development.md](/docs/contributing/development.md).
## Documentation
The documentation is available at [https://opengist.io/](https://opengist.io/) or in the [/docs](/docs) directory.
The documentation is available in [/docs](/docs) directory.
## License

View File

@ -1,46 +1,25 @@
# 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/index.md
# https://github.com/thomiceli/opengist/blob/master/docs/configuration/cheat-sheet.md
# Set the log level to one of the following: debug, info, warn, error, fatal. Default: warn
# Set the log level to one of the following: trace, debug, info, warn, error, fatal, panic. Default: warn
log-level: warn
# 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
# Public URL for the Git HTTP/SSH connection.
# If not set, uses the URL from the request
external-url:
# Directory where Opengist will store its data. Default: ~/.opengist/
opengist-home:
# 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:
# Name of the SQLite database file. Default: opengist.db
db-filename: opengist.db
# 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
http.host: 0.0.0.0
@ -51,9 +30,6 @@ http.port: 6157
# Enable or disable git operations (clone, pull, push) via HTTP (either `true` or `false`). Default: true
http.git-enabled: true
# Enable or disable the 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)
@ -77,65 +53,22 @@ ssh.external-domain:
# Path or alias to ssh-keygen executable. Default: ssh-keygen
ssh.keygen-executable: ssh-keygen
# OAuth2 configuration
# The callback/redirect URL must be http://opengist.url/oauth/<github|gitlab|gitea|openid-connect>/callback
# The callback/redirect URL must be http://opengist.domain/oauth/<github|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

View File

@ -7,12 +7,5 @@ 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"
exec su $USER -c "OG_OPENGIST_HOME=/opengist /app/opengist/opengist"

View File

@ -1,97 +0,0 @@
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
})

View File

@ -1,37 +0,0 @@
const colors = require('tailwindcss/colors')
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
"./.vitepress/theme/*.vue",
],
theme: {
colors: {
transparent: 'transparent',
current: 'currentColor',
white: colors.white,
black: colors.black,
gray: {
50: "#EEEFF1",
100: "#DEDFE3",
200: "#BABCC5",
300: "#999CA8",
400: "#75798A",
500: "#585B68",
600: "#464853",
700: "#363840",
800: "#232429",
900: "#131316"
},
indigo: colors.indigo,
},
extend: {
borderWidth: {
'1': '1px',
}
},
},
plugins: [],
darkMode: 'class',
}

View File

@ -1,101 +0,0 @@
<script>
import { withBase } from 'vitepress';
import './theme.css'
export default {
setup() {
return { withBase };
},
};
</script>
<template>
<main class="home">
<header class="hero">
<div class="mx-auto max-w-7xl px-6 lg:px-8">
<div class="mx-auto lg:text-center">
<img class="rotating h-36 mx-auto my-8 " src="https://raw.githubusercontent.com/thomiceli/opengist/master/public/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>

View File

@ -1,16 +0,0 @@
<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>

View File

@ -1,12 +0,0 @@
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

View File

@ -1,147 +0,0 @@
/**
* 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;

View File

@ -1,13 +0,0 @@
# 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"}
```

View File

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

View File

@ -1,10 +1,6 @@
# 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
Configure Nginx to proxy requests to Opengist. Here is an example configuration file :
```
server {
listen 80;
@ -20,27 +16,7 @@ server {
}
```
### 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:
Then run :
```shell
sudo systemctl restart nginx
service nginx restart
```

View File

@ -0,0 +1,39 @@
# 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.domain/oauth/github/callback`
* Copy the 'Client ID' and 'Client Secret' and add them to the [configuration](/docs/configuration/cheat-sheet.md) :
```yaml
github.client-key: <key>
github.secret: <secret>
```
## 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.domain/oauth/gitea/callback`
* Copy the 'Client ID' and 'Client Secret' and add them to the [configuration](/docs/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
```
## OpenID Connect
* Add a new OAuth app in Application settings of your OIDC provider
* Set 'Redirect URI' to `http://opengist.domain/oauth/openid-connect/callback`
* Copy the 'Client ID', 'Client Secret', and the discovery endpoint, and add them to the [configuration](/docs/configuration/cheat-sheet.md) :
```yaml
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
```

View File

@ -1,7 +0,0 @@
# 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>
```

View File

@ -2,7 +2,6 @@
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
@ -46,47 +45,3 @@ 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
```

View File

@ -1,48 +0,0 @@
# 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>

View File

@ -1,53 +0,0 @@
# 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.

View File

@ -1,51 +1,25 @@
---
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). |
| YAML Config Key | Environment Variable | Default value | Description |
|-----------------------|--------------------------|----------------------|-----------------------------------------------------------------------------------------------------------------------------------|
| log-level | OG_LOG_LEVEL | `warn` | Set the log level to one of the following: `trace`, `debug`, `info`, `warn`, `error`, `fatal`, `panic`. |
| external-url | OG_EXTERNAL_URL | none | Public URL for the Git HTTP/SSH connection. If not set, uses the URL from the request. |
| opengist-home | OG_OPENGIST_HOME | home directory | Path to the directory where Opengist stores its data. |
| db-filename | OG_DB_FILENAME | `opengist.db` | Name of the SQLite database file. |
| 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`) |
| 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. |
| 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. |
| 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. |

View File

@ -1,45 +0,0 @@
# 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"
```

View File

@ -1,62 +0,0 @@
# 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.

View File

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

View File

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

View File

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

View File

@ -12,27 +12,17 @@ The [configuration cheat sheet](cheat-sheet.md) lists all available configuratio
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.
You can start by copying and/or modifying the provided [config.yml](/config.yml) file.
## Configuration via Environment Variables
Usage with Docker Compose :
```yml
services:
opengist:
@ -41,32 +31,8 @@ services:
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
```

View File

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

View File

@ -1,94 +0,0 @@
# 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.

View File

@ -1,7 +0,0 @@
# 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

View File

@ -1,38 +0,0 @@
# 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

View File

@ -1,4 +1,52 @@
---
layout: home
navbar: false
---
# Opengist
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](/docs/usage/init-via-git.md) / Clone / Pull / Push snippets **via Git** over HTTP or SSH
* Revisions history
* Syntax highlighting ; markdown & CSV support
* Like / Fork snippets
* Search for snippets ; browse users snippets, likes and forks
* Editor with indentation mode & size ; drag and drop files
* Download raw files or as a ZIP archive
* OAuth2 login with GitHub, 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 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.20** or later is recommended as the app has not been tested with older Git versions.
[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:
* [Tailwind CSS](https://tailwindcss.com/)
* [CodeMirror](https://codemirror.net/)
* [Day.js](https://day.js.org/)
* [highlight.js](https://highlightjs.org/)
* and [others](/package.json)

View File

@ -1,7 +1,73 @@
# Install Opengist
# Installation
There are several ways to install Opengist, depending on your preferences and your environment.
## With Docker
- [Docker](installation/docker.md)
- [Source](installation/source.md)
- [Binary](installation/binary.md)
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
version: "3"
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"
```
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.5.1/opengist1.5.1-linux-amd64.tar.gz
tar xzvf opengist1.5.1-linux-amd64.tar.gz
cd opengist
chmod +x opengist
./opengist # with or without `--config config.yml`
```
## From source
Requirements :
* [Git](https://git-scm.com/downloads) (2.20+)
* [Go](https://go.dev/doc/install) (1.20+)
* [Node.js](https://nodejs.org/en/download/) (16+)
```shell
git clone https://github.com/thomiceli/opengist
cd opengist
make
./opengist
```
Opengist is now running on port 6157, you can browse http://localhost:6157

View File

@ -1,14 +0,0 @@
# 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`
```

View File

@ -1,41 +0,0 @@
# 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
```

View File

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

View File

@ -1,19 +0,0 @@
# 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

View File

@ -1,56 +0,0 @@
# 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)

View File

@ -1,17 +0,0 @@
<?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>

Before

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 MiB

View File

@ -1,58 +0,0 @@
# 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.

View File

@ -1,11 +0,0 @@
# 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>
```

View File

@ -1,37 +0,0 @@
# 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"
}
```

View File

@ -1,32 +0,0 @@
# 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
```

View File

@ -1,23 +0,0 @@
# 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
```

View File

@ -39,4 +39,4 @@ 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" />
https://github.com/thomiceli/opengist/assets/27960254/3fe1a0ba-b638-4928-83a1-f38e46fea066

147
go.mod
View File

@ -1,129 +1,54 @@
module github.com/thomiceli/opengist
go 1.23.0
go 1.20
require (
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
github.com/glebarez/go-sqlite v1.21.2
github.com/glebarez/sqlite v1.9.0
github.com/go-playground/validator/v10 v10.15.4
github.com/google/uuid v1.3.1
github.com/gorilla/sessions v1.2.1
github.com/hashicorp/go-memdb v1.3.4
github.com/labstack/echo/v4 v4.11.1
github.com/markbates/goth v1.78.0
github.com/rs/zerolog v1.30.0
github.com/stretchr/testify v1.8.4
golang.org/x/crypto v0.13.0
golang.org/x/text v0.13.0
gopkg.in/yaml.v3 v3.0.1
gorm.io/driver/mysql v1.5.7
gorm.io/driver/postgres v1.5.11
gorm.io/gorm v1.25.12
gorm.io/gorm v1.25.4
)
require (
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/dustin/go-humanize v1.0.1 // indirect
github.com/gabriel-vasile/mimetype v1.4.2 // 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/golang-jwt/jwt v3.2.2+incompatible // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/gorilla/mux v1.8.0 // indirect
github.com/gorilla/securecookie v1.1.1 // indirect
github.com/hashicorp/go-immutable-radix v1.3.1 // indirect
github.com/hashicorp/golang-lru v1.0.2 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/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/labstack/gommon v0.4.0 // indirect
github.com/leodido/go-urn v1.2.4 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.19 // 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
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
golang.org/x/net v0.15.0 // indirect
golang.org/x/oauth2 v0.12.0 // indirect
golang.org/x/sys v0.12.0 // indirect
golang.org/x/time v0.3.0 // indirect
google.golang.org/appengine v1.6.8 // indirect
google.golang.org/protobuf v1.31.0 // indirect
modernc.org/libc v1.24.1 // indirect
modernc.org/mathutil v1.6.0 // indirect
modernc.org/memory v1.7.1 // indirect
modernc.org/sqlite v1.25.0 // indirect
)

796
go.sum
View File

@ -1,403 +1,533 @@
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=
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.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/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/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/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/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/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU=
github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA=
github.com/glebarez/go-sqlite v1.21.2 h1:3a6LFC4sKahUunAmynQKLZceZCOzUthkRkEAl9gAXWo=
github.com/glebarez/go-sqlite v1.21.2/go.mod h1:sfxdZyhQjTM2Wry3gVYWaW072Ri1WMdWJi0k6+3382k=
github.com/glebarez/sqlite v1.9.0 h1:Aj6bPA12ZEx5GbSF6XADmCkYXlljPNUY+Zf1EQxynXs=
github.com/glebarez/sqlite v1.9.0/go.mod h1:YBYCoyupOao60lzp1MVBLEjZfgkq0tdB1voAQ09K9zw=
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.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/go-playground/validator/v10 v10.15.4 h1:zMXza4EpOdooxPel5xDqXEdXG5r+WggpvnAKMsalBjs=
github.com/go-playground/validator/v10 v10.15.4/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
github.com/goccy/go-json v0.9.6/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
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/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/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
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/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
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/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4=
github.com/google/uuid v1.3.1/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/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
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/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA=
github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo=
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/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
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/hashicorp/go-immutable-radix v1.3.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
github.com/hashicorp/go-immutable-radix v1.3.1 h1:DKHmCUm2hRBK510BaiZlwvpD40f8bJFeZnpfm2KLowc=
github.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
github.com/hashicorp/go-memdb v1.3.4 h1:XSL3NR682X/cVk2IeV0d70N4DZ9ljI885xAEU8IoK3c=
github.com/hashicorp/go-memdb v1.3.4/go.mod h1:uBTr1oQbtuMgd1SSGoR8YV27eT3sBHbYiNm53bMpgSg=
github.com/hashicorp/go-uuid v1.0.0 h1:RS8zrF7PhGwyNPOtxSClXXj9HA8feRnJzgnI1RJCSnM=
github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
github.com/hashicorp/golang-lru v1.0.2 h1:dV3g9Z/unq5DpblPpw+Oqcv4dU/1omnb4Ok8iPY6p1c=
github.com/hashicorp/golang-lru v1.0.2/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
github.com/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/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.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/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/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/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 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/labstack/echo/v4 v4.11.1 h1:dEpLU2FLg4UVmvCGPuk/APjlH6GDpbEPti61srUUUs4=
github.com/labstack/echo/v4 v4.11.1/go.mod h1:YuYRTSM3CHs2ybfrL8Px48bO6BAnYIN4l8wSTMP6BDQ=
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.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q=
github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4=
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.78.0 h1:7VEIFDycJp9deyVv3YraGBPdD0ZYQW93Y3Aw1eVP3BY=
github.com/markbates/goth v1.78.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/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
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/mrjones/oauth v0.0.0-20180629183705-f4e24b6d100c/go.mod h1:skjdDftzkFALcuGzYSklqYd8gvat6F1gZJ4YPVbkZpM=
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/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/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
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/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
github.com/rs/zerolog v1.30.0 h1:SymVODrcRsaRaSInD9yQtKbtWqwsfoPcRff/oRXLj4c=
github.com/rs/zerolog v1.30.0/go.mod h1:/tk+P47gFdPXq4QYjvCmT5/Gsug2nagsFWBWhAiSi1w=
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.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/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
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/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.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=
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=
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=
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-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/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.13.0 h1:mvySKfSWJ+UKUii46M40LOvyWfN0s2U+46/jDd0e6Ck=
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
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/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-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/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-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
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/net v0.15.0 h1:ugBLEUaxABaB5AJqW9enI0ACdci2RUd4eP51NTBvuJ8=
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
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/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.12.0 h1:smVPGxink+n1ZI5pkQa8y6fZT0RW0MgCO5bFpepy4B4=
golang.org/x/oauth2 v0.12.0/go.mod h1:A74bZ3aGXgCY0qaIC9Ahg6Lglin4AMAco8cIv9baba4=
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/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/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-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
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-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/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 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o=
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.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/term v0.12.0 h1:/ZfYdc3zq+q02Rv9vGqTeSItdzZTSNDmfTi0mBAuidU=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
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.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/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
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.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=
golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
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=
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
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/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/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM=
google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds=
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/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
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/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.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
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=
gorm.io/gorm v1.25.4 h1:iyNd8fNAe8W9dvtlgeRI5zSVZPsq3OpcTu37cYcpCmw=
gorm.io/gorm v1.25.4/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k=
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=
modernc.org/libc v1.24.1 h1:uvJSeCKL/AgzBo2yYIPPTy82v21KgGnizcGYfBHaNuM=
modernc.org/libc v1.24.1/go.mod h1:FmfO1RLrU3MHJfyi9eYYmZBfi/R+tqZ6+hQ3yQQUkak=
modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4=
modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo=
modernc.org/memory v1.7.1 h1:9J+2/GKTlV503mk3yv8QJ6oEpRCUrRy0ad8TXEPoV8M=
modernc.org/memory v1.7.1/go.mod h1:NO4NVCQy0N7ln+T9ngWqOQfi7ley4vpwvARR+Hjw95E=
modernc.org/sqlite v1.25.0 h1:AFweiwPNd/b3BoKnBOfFm+Y260guGMF+0UFk0savqeA=
modernc.org/sqlite v1.25.0/go.mod h1:FL3pVXie73rg3Rii6V/u5BoHlSoyeZeIgKZEgHARyCU=
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=

View File

@ -1,23 +0,0 @@
# 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/

View File

@ -1,9 +0,0 @@
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"

View File

@ -1,19 +0,0 @@
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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,15 +0,0 @@
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

View File

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

View File

@ -1,185 +0,0 @@
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()
}
}

View File

@ -1,46 +0,0 @@
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
}

View File

@ -1,76 +0,0 @@
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
}

View File

@ -1,18 +0,0 @@
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
}

View File

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

View File

@ -1,117 +0,0 @@
package oauth
import (
gocontext "context"
gojson "encoding/json"
"github.com/markbates/goth"
"github.com/markbates/goth/gothic"
"github.com/markbates/goth/providers/gitea"
"github.com/rs/zerolog/log"
"github.com/thomiceli/opengist/internal/config"
"github.com/thomiceli/opengist/internal/db"
"github.com/thomiceli/opengist/internal/web/context"
"io"
"net/http"
)
type GiteaProvider struct {
Provider
URL string
}
func (p *GiteaProvider) RegisterProvider() error {
goth.UseProviders(
gitea.NewCustomisedURL(
config.C.GiteaClientKey,
config.C.GiteaSecret,
urlJoin(p.URL, "/oauth/gitea/callback"),
urlJoin(config.C.GiteaUrl, "/login/oauth/authorize"),
urlJoin(config.C.GiteaUrl, "/login/oauth/access_token"),
urlJoin(config.C.GiteaUrl, "/api/v1/user"),
),
)
return nil
}
func (p *GiteaProvider) BeginAuthHandler(ctx *context.Context) {
ctxValue := gocontext.WithValue(ctx.Request().Context(), gothic.ProviderParamKey, GiteaProviderString)
ctx.SetRequest(ctx.Request().WithContext(ctxValue))
gothic.BeginAuthHandler(ctx.Response(), ctx.Request())
}
func (p *GiteaProvider) UserHasProvider(user *db.User) bool {
return user.GiteaID != ""
}
func NewGiteaProvider(url string) *GiteaProvider {
return &GiteaProvider{
URL: url,
}
}
type GiteaCallbackProvider struct {
CallbackProvider
User *goth.User
}
func (p *GiteaCallbackProvider) GetProvider() string {
return GiteaProviderString
}
func (p *GiteaCallbackProvider) GetProviderUser() *goth.User {
return p.User
}
func (p *GiteaCallbackProvider) GetProviderUserID(user *db.User) bool {
return user.GiteaID != ""
}
func (p *GiteaCallbackProvider) GetProviderUserSSHKeys() ([]string, error) {
resp, err := http.Get(urlJoin(config.C.GiteaUrl, p.User.NickName+".keys"))
if err != nil {
return nil, err
}
defer resp.Body.Close()
return readKeys(resp)
}
func (p *GiteaCallbackProvider) UpdateUserDB(user *db.User) {
user.GiteaID = p.User.UserID
resp, err := http.Get(urlJoin(config.C.GiteaUrl, "/api/v1/users/", p.User.UserID))
if err != nil {
log.Error().Err(err).Msg("Cannot get user from Gitea")
return
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
log.Error().Err(err).Msg("Cannot read Gitea response body")
return
}
var result map[string]interface{}
err = gojson.Unmarshal(body, &result)
if err != nil {
log.Error().Err(err).Msg("Cannot unmarshal Gitea response body")
return
}
field, ok := result["avatar_url"]
if !ok {
log.Error().Msg("Field 'avatar_url' not found in Gitea JSON response")
return
}
user.AvatarURL = field.(string)
}
func NewGiteaCallbackProvider(user *goth.User) CallbackProvider {
return &GiteaCallbackProvider{
User: user,
}
}

View File

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

View File

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

View File

@ -1,85 +0,0 @@
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,
}
}

View File

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

View File

@ -1,11 +0,0 @@
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)
}

View File

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

View File

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

View File

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

View File

@ -1,78 +0,0 @@
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
},
}

View File

@ -1,56 +0,0 @@
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")
}
}

View File

@ -1,184 +0,0 @@
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
}

View File

@ -2,48 +2,30 @@ 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"
"github.com/thomiceli/opengist/internal/utils"
"gopkg.in/yaml.v3"
)
var OpengistVersion = ""
var OpengistVersion = "1.5.1"
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 {
SecretKey string `yaml:"secret-key" env:"OG_SECRET_KEY"`
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"`
DBUri string `yaml:"db-uri" env:"OG_DB_URI"`
DBFilename string `yaml:"db-filename" env:"OG_DB_FILENAME"` // deprecated
IndexEnabled bool `yaml:"index.enabled" env:"OG_INDEX_ENABLED"` // deprecated
Index string `yaml:"index" env:"OG_INDEX"`
BleveDirname string `yaml:"index.dirname" env:"OG_INDEX_DIRNAME"` // deprecated
MeiliHost string `yaml:"index.meili.host" env:"OG_MEILI_HOST"`
MeiliAPIKey string `yaml:"index.meili.api-key" env:"OG_MEILI_API_KEY"`
GitDefaultBranch string `yaml:"git.default-branch" env:"OG_GIT_DEFAULT_BRANCH"`
DBFilename string `yaml:"db-filename" env:"OG_DB_FILENAME"`
SqliteJournalMode string `yaml:"sqlite.journal-mode" env:"OG_SQLITE_JOURNAL_MODE"`
@ -60,52 +42,25 @@ type config struct {
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"`
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"`
}
func configWithDefaults() (*config, error) {
homeDir, err := os.UserHomeDir()
c := &config{}
c.SecretKey = ""
if err != nil {
return c, err
}
c.LogLevel = "warn"
c.LogOutput = "stdout,file"
c.OpengistHome = ""
c.DBUri = "opengist.db"
c.Index = "bleve"
c.OpengistHome = filepath.Join(homeDir, ".opengist")
c.DBFilename = "opengist.db"
c.SqliteJournalMode = "WAL"
@ -118,53 +73,32 @@ func configWithDefaults() (*config, error) {
c.SshPort = "2222"
c.SshKeygen = "ssh-keygen"
c.GitlabName = "GitLab"
c.GiteaUrl = "https://gitea.com"
c.GiteaName = "Gitea"
c.MetricsEnabled = false
c.GiteaUrl = "http://gitea.com"
return c, nil
}
func InitConfig(configPath string, out io.Writer) error {
func InitConfig(configPath string) error {
// Default values
c, err := configWithDefaults()
if err != nil {
return err
}
if err = loadConfigFromYaml(c, configPath, out); err != nil {
if err = loadConfigFromYaml(c, configPath); err != nil {
return err
}
if err = loadConfigFromEnv(c, out); err != nil {
if err = loadConfigFromEnv(c); err != nil {
return err
}
if c.OpengistHome == "" {
homeDir, err := os.UserHomeDir()
if err != nil {
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")
}
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
}
@ -172,61 +106,21 @@ 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
}
var logWriters []io.Writer
logOutputTypes := strings.Split(strings.ToLower(C.LogOutput), ",")
slices.Sort(logOutputTypes)
logOutputTypes = slices.Compact(logOutputTypes)
multi := zerolog.MultiLevelWriter(zerolog.NewConsoleWriter(), file)
log.Logger = zerolog.New(multi).Level(level).With().Timestamp().Logger()
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)) {
if !utils.SliceContains([]string{"trace", "debug", "info", "warn", "error", "fatal", "panic"}, strings.ToLower(C.LogLevel)) {
log.Warn().Msg("Invalid log level: " + C.LogLevel)
}
}
@ -245,8 +139,8 @@ func CheckGitVersion(version string) (bool, error) {
return false, fmt.Errorf("invalid minor version number")
}
// Check if version is prior to 2.28
if major < 2 || (major == 2 && minor < 28) {
// Check if version is prior to 2.20
if major < 2 || (major == 2 && minor < 20) {
return false, nil
}
return true, nil
@ -257,16 +151,7 @@ func GetHomeDir() string {
return filepath.Clean(absolutePath)
}
func SetupSecretKey() {
if C.SecretKey == "" {
path := filepath.Join(GetHomeDir(), "opengist-secret.key")
SecretKey, _ = session.GenerateSecretKey(path)
} else {
SecretKey = []byte(C.SecretKey)
}
}
func loadConfigFromYaml(c *config, configPath string, out io.Writer) error {
func loadConfigFromYaml(c *config, configPath string) error {
if configPath != "" {
absolutePath, _ := filepath.Abs(configPath)
absolutePath = filepath.Clean(absolutePath)
@ -275,9 +160,9 @@ func loadConfigFromYaml(c *config, configPath string, out io.Writer) error {
if !os.IsNotExist(err) {
return err
}
_, _ = fmt.Fprintln(out, "No YAML config file found at "+absolutePath)
fmt.Println("No YAML config file found at " + absolutePath)
} else {
_, _ = fmt.Fprintln(out, "Using YAML config file: "+absolutePath)
fmt.Println("Using YAML config file: " + absolutePath)
// Override default values with values from config.yml
d := yaml.NewDecoder(file)
@ -287,13 +172,13 @@ func loadConfigFromYaml(c *config, configPath string, out io.Writer) error {
defer file.Close()
}
} else {
_, _ = fmt.Fprintln(out, "No YAML config file specified.")
fmt.Println("No YAML config file specified.")
}
return nil
}
func loadConfigFromEnv(c *config, out io.Writer) error {
func loadConfigFromEnv(c *config) error {
v := reflect.ValueOf(c).Elem()
var envVars []string
@ -305,69 +190,28 @@ func loadConfigFromEnv(c *config, out io.Writer) error {
}
envValue := os.Getenv(strings.ToUpper(tag))
if envValue == "" && v.Field(i).Kind() != reflect.Slice {
if envValue == "" {
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())
}
envVars = append(envVars, tag)
}
if len(envVars) > 0 {
_, _ = fmt.Fprintln(out, "Using environment variables config: "+strings.Join(envVars, ", "))
fmt.Println("Using environment variables config: " + strings.Join(envVars, ", "))
} else {
_, _ = fmt.Fprintln(out, "No environment variables config specified.")
fmt.Println("No environment variables config specified.")
}
return nil

View File

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

View File

@ -5,27 +5,20 @@ import (
)
type AdminSetting struct {
Key string `gorm:"index:,unique"`
Key string `gorm:"uniqueIndex"`
Value string
}
const (
SettingDisableSignup = "disable-signup"
SettingRequireLogin = "require-login"
SettingAllowGistsWithoutLogin = "allow-gists-without-login"
SettingDisableLoginForm = "disable-login-form"
SettingDisableGravatar = "disable-gravatar"
SettingDisableSignup = "disable-signup"
SettingRequireLogin = "require-login"
SettingDisableLoginForm = "disable-login-form"
SettingDisableGravatar = "disable-gravatar"
)
func GetSetting(key string) (string, error) {
var setting AdminSetting
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
}
err := db.Where("key = ?", key).First(&setting).Error
return setting.Value, err
}
@ -55,7 +48,7 @@ func UpdateSetting(key string, value string) error {
}
func setSetting(key string, value string) error {
return db.FirstOrCreate(&AdminSetting{Key: key, Value: value}, &AdminSetting{Key: key}).Error
return db.Create(&AdminSetting{Key: key, Value: value}).Error
}
func initAdminSettings(settings map[string]string) error {
@ -69,21 +62,3 @@ 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
}

View File

@ -2,150 +2,38 @@ 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"
msqlite "github.com/glebarez/go-sqlite"
"github.com/glebarez/sqlite"
"github.com/rs/zerolog/log"
"github.com/thomiceli/opengist/internal/config"
"github.com/thomiceli/opengist/internal/utils"
"gorm.io/gorm"
"gorm.io/gorm/logger"
)
var db *gorm.DB
const (
SQLite databaseType = iota
PostgreSQL
MySQL
)
func Setup(dbPath string, sharedCache bool) error {
var err error
journalMode := strings.ToUpper(config.C.SqliteJournalMode)
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
if !utils.SliceContains([]string{"DELETE", "TRUNCATE", "PERSIST", "MEMORY", "WAL", "OFF"}, journalMode) {
log.Warn().Msg("Invalid SQLite journal mode: " + journalMode)
}
u, err := url.Parse(uri)
if err != nil {
return nil, fmt.Errorf("invalid URI: %v", err)
sharedCacheStr := ""
if sharedCache {
sharedCacheStr = "&cache=shared"
}
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 {
if db, err = gorm.Open(sqlite.Open(dbPath+"?_fk=true&_journal_mode="+journalMode+sharedCacheStr), &gorm.Config{
Logger: logger.Default.LogMode(logger.Silent),
}); 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
}
@ -154,21 +42,20 @@ func Setup(dbUri string) error {
return err
}
if err = db.AutoMigrate(&User{}, &Gist{}, &SSHKey{}, &AdminSetting{}, &Invitation{}, &WebAuthnCredential{}, &TOTP{}, &GistTopic{}, &GistLanguage{}); err != nil {
if err = db.AutoMigrate(&User{}, &Gist{}, &SSHKey{}, &AdminSetting{}); err != nil {
return err
}
if err = applyMigrations(dbInfo); err != nil {
if err = ApplyMigrations(db); err != nil {
return err
}
// Default admin setting values
return initAdminSettings(map[string]string{
SettingDisableSignup: "0",
SettingRequireLogin: "0",
SettingAllowGistsWithoutLogin: "0",
SettingDisableLoginForm: "0",
SettingDisableGravatar: "0",
SettingDisableSignup: "0",
SettingRequireLogin: "0",
SettingDisableLoginForm: "0",
SettingDisableGravatar: "0",
})
}
@ -187,86 +74,9 @@ func CountAll(table interface{}) (int64, error) {
}
func IsUniqueConstraintViolation(err error) bool {
return errors.Is(err, gorm.ErrDuplicatedKey)
}
func Ping() error {
sql, err := db.DB()
if err != nil {
return err
var sqliteErr *msqlite.Error
if errors.As(err, &sqliteErr) && sqliteErr.Code() == 2067 {
return true
}
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{})
return false
}

View File

@ -1,82 +1,22 @@
package db
import (
"bytes"
"encoding/gob"
"fmt"
"github.com/labstack/echo/v4"
"github.com/thomiceli/opengist/internal/git"
"gorm.io/gorm"
"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
Private int // 0: public, 1: unlisted, 2: private
UserID uint
User User
NbFiles int
@ -88,9 +28,6 @@ type Gist struct {
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 {
@ -110,8 +47,8 @@ func (gist *Gist) BeforeDelete(tx *gorm.DB) error {
func GetGist(user string, gistUuid string) (*Gist, error) {
gist := new(Gist)
err := db.Preload("User").Preload("Forked.User").Preload("Topics").
Where("(gists.uuid like ? OR gists.url = ?) AND users.username like ?", gistUuid+"%", gistUuid, user).
err := db.Preload("User").Preload("Forked.User").
Where("gists.uuid = ? AND users.username like ?", gistUuid, user).
Joins("join users on gists.user_id = users.id").
First(&gist).Error
@ -120,7 +57,7 @@ func GetGist(user string, gistUuid string) (*Gist, error) {
func GetGistByID(gistId string) (*Gist, error) {
gist := new(Gist)
err := db.Preload("User").Preload("Forked.User").Preload("Topics").
err := db.Preload("User").Preload("Forked.User").
Where("gists.id = ?", gistId).
First(&gist).Error
@ -129,9 +66,7 @@ func GetGistByID(gistId string) (*Gist, error) {
func GetAllGistsForCurrentUser(currentUserId uint, offset int, sort string, order string) ([]*Gist, error) {
var gists []*Gist
err := db.Preload("User").
Preload("Forked.User").
Preload("Topics").
err := db.Preload("User").Preload("Forked.User").
Where("gists.private = 0 or gists.user_id = ?", currentUserId).
Limit(11).
Offset(offset * 10).
@ -152,18 +87,12 @@ func GetAllGists(offset int) ([]*Gist, error) {
return gists, err
}
func GetAllGistsFromSearch(currentUserId uint, query string, offset int, sort string, order string, topic string) ([]*Gist, error) {
func GetAllGistsFromSearch(currentUserId uint, query string, offset int, sort string, order string) ([]*Gist, error) {
var gists []*Gist
tx := db.Preload("User").Preload("Forked.User").Preload("Topics").
err := db.Preload("User").Preload("Forked.User").
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).
Where("gists.title like ? or gists.description like ?", "%"+query+"%", "%"+query+"%").
Limit(11).
Offset(offset * 10).
Order("gists." + sort + "_at " + order).
Find(&gists).Error
@ -172,47 +101,20 @@ func GetAllGistsFromSearch(currentUserId uint, query string, offset int, sort st
}
func gistsFromUserStatement(fromUserId uint, currentUserId uint) *gorm.DB {
return db.Preload("User").Preload("Forked.User").Preload("Topics").
return db.Preload("User").Preload("Forked.User").
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) {
func GetAllGistsFromUser(fromUserId uint, currentUserId uint, offset int, sort string, order string) ([]*Gist, 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).
err := gistsFromUserStatement(fromUserId, currentUserId).Limit(11).
Offset(offset * 10).
Order("gists." + sort + "_at " + order).
Find(&gists).Error
return gists, count, err
return gists, err
}
func CountAllGistsFromUser(fromUserId uint, currentUserId uint) (int64, error) {
@ -222,7 +124,7 @@ func CountAllGistsFromUser(fromUserId uint, currentUserId uint) (int64, error) {
}
func likedStatement(fromUserId uint, currentUserId uint) *gorm.DB {
return db.Preload("User").Preload("Forked.User").Preload("Topics").
return db.Preload("User").Preload("Forked.User").
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").
@ -245,7 +147,7 @@ func CountAllGistsLikedByUser(fromUserId uint, currentUserId uint) (int64, error
}
func forkedStatement(fromUserId uint, currentUserId uint) *gorm.DB {
return db.Preload("User").Preload("Forked.User").Preload("Topics").
return db.Preload("User").Preload("Forked.User").
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")
@ -275,36 +177,6 @@ func GetAllGistsRows() ([]*Gist, 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
@ -315,19 +187,9 @@ func (gist *Gist) CreateForked() 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 {
@ -404,29 +266,33 @@ func (gist *Gist) InitRepository() error {
return git.InitRepository(gist.User.Username, gist.Uuid)
}
func (gist *Gist) InitRepositoryViaInit(ctx echo.Context) error {
return git.InitRepositoryViaInit(gist.User.Username, gist.Uuid, ctx)
}
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)
func (gist *Gist) Files(revision string) ([]*git.File, error) {
var files []*git.File
filesStr, err := git.GetFilesOfRepository(gist.User.Username, gist.Uuid, revision)
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, nil
}
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,
})
for _, fileStr := range filesStr {
file, err := gist.File(revision, fileStr, true)
if err != nil {
return nil, err
}
files = append(files, file)
}
return files, err
}
@ -439,26 +305,13 @@ func (gist *Gist) File(revision string, filename string, truncate bool) (*git.Fi
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)
}
@ -468,7 +321,7 @@ func (gist *Gist) NbCommits() (string, error) {
}
func (gist *Gist) AddAndCommitFiles(files *[]FileDTO) error {
if err := git.CloneTmp(gist.User.Username, gist.Uuid, gist.Uuid, gist.User.Email, true); err != nil {
if err := git.CloneTmp(gist.User.Username, gist.Uuid, gist.Uuid, gist.User.Email); err != nil {
return err
}
@ -489,26 +342,6 @@ func (gist *Gist) AddAndCommitFiles(files *[]FileDTO) error {
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)
}
@ -521,7 +354,7 @@ func (gist *Gist) RPC(service string) ([]byte, error) {
return git.RPC(gist.User.Username, gist.Uuid, service)
}
func (gist *Gist) UpdatePreviewAndCount(withTimestampUpdate bool) error {
func (gist *Gist) UpdatePreviewAndCount() error {
filesStr, err := git.GetFilesOfRepository(gist.User.Username, gist.Uuid, "HEAD")
if err != nil {
return err
@ -547,186 +380,22 @@ func (gist *Gist) UpdatePreviewAndCount(withTimestampUpdate bool) error {
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
return gist.Update()
}
// -- 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"`
Title string `validate:"max=50" form:"title"`
Description string `validate:"max=150" form:"description"`
Private int `validate:"number,min=0,max=2" form:"private"`
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"`
Filename string `validate:"excludes=\x2f,excludes=\x5c,max=50"`
Content string `validate:"required"`
}
@ -735,106 +404,11 @@ func (dto *GistDTO) ToGist() *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)
}
}()
}

View File

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

View File

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

View File

@ -1,99 +0,0 @@
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)
}

View File

@ -3,6 +3,7 @@ package db
import (
"fmt"
"github.com/rs/zerolog/log"
"gorm.io/gorm"
)
type MigrationVersion struct {
@ -10,19 +11,7 @@ type MigrationVersion struct {
Version uint
}
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 {
func ApplyMigrations(db *gorm.DB) 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")
@ -36,7 +25,7 @@ func applySqliteMigrations() error {
// Define migrations
migrations := []struct {
Version uint
Func func() error
Func func(*gorm.DB) error
}{
{1, v1_modifyConstraintToSSHKeys},
{2, v2_lowercaseEmails},
@ -52,7 +41,7 @@ func applySqliteMigrations() error {
return err
}
if err := m.Func(); err != nil {
if err := m.Func(db); err != nil {
log.Fatal().Err(err).Msg(fmt.Sprintf("Error applying migration %d:", m.Version))
tx.Rollback()
return err
@ -72,7 +61,7 @@ func applySqliteMigrations() error {
}
// Modify the constraint on the ssh_keys table to use ON DELETE CASCADE
func v1_modifyConstraintToSSHKeys() error {
func v1_modifyConstraintToSSHKeys(db *gorm.DB) error {
createSQL := `
CREATE TABLE ssh_keys_temp (
id integer primary key,
@ -107,7 +96,7 @@ func v1_modifyConstraintToSSHKeys() error {
return db.Exec(renameSQL).Error
}
func v2_lowercaseEmails() error {
func v2_lowercaseEmails(db *gorm.DB) error {
// Copy the lowercase emails into the new column
copySQL := `UPDATE users SET email = lower(email);`
return db.Exec(copySQL).Error

View File

@ -19,7 +19,7 @@ type SSHKey struct {
User User `validate:"-" `
}
func (sshKey *SSHKey) BeforeCreate(*gorm.DB) error {
func (sshKey *SSHKey) BeforeCreate(tx *gorm.DB) error {
pubKey, _, _, _, err := ssh.ParseAuthorizedKey([]byte(sshKey.Content))
if err != nil {
return err
@ -48,12 +48,13 @@ func GetSSHKeyByID(sshKeyId uint) (*SSHKey, error) {
return sshKey, err
}
func SSHKeyDoesExists(sshKeyContent string) (bool, error) {
var count int64
err := db.Model(&SSHKey{}).
Where("content = ?", sshKeyContent).
Count(&count).Error
return count > 0, err
func SSHKeyDoesExists(sshKeyContent string) (*SSHKey, error) {
sshKey := new(SSHKey)
err := db.
Where("content like ?", sshKeyContent+"%").
First(&sshKey).Error
return sshKey, err
}
func (sshKey *SSHKey) Create() error {

View File

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

View File

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

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