Compare commits

...

14 Commits

Author SHA1 Message Date
Thomas Miceli
4d29a50e64 v1.12.1 2026-02-03 15:59:29 +07:00
Thomas Miceli
3a4602d412 Translated using Weblate (Russian) (#605)
Currently translated at 100.0% (341 of 341 strings)

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

Co-authored-by: FunNikita <mainik1111@icloud.com>
2026-02-03 16:56:04 +08:00
Thomas Miceli
2e10c1732a Add images and binary content on gist preview (#615) 2026-02-03 16:55:44 +08:00
Thomas Miceli
fe04c03acb Improve security on raw files endpoint (#613) 2026-02-03 02:11:39 +08:00
Thomas Miceli
2a1554d063 Fix renderable text files with different mimetypes (#612) 2026-02-03 01:59:24 +08:00
Thomas Miceli
b7dbdde66b Allow Access Tokens with Required Login (#611) 2026-02-02 19:31:07 +08:00
Thomas Miceli
b7278b60ab Update CI 2026-01-31 20:51:40 +07:00
Thomas Miceli
84c6a41340 Update CI 2026-01-29 02:01:27 +07:00
Thomas Miceli
6bd8df6a74 v1.12.0 2026-01-27 22:28:20 +07:00
Thomas Miceli
b48103c06a Translated using Weblate (Russian) (#604)
Currently translated at 58.9% (201 of 341 strings)

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

Co-authored-by: FunNikita <mainik1111@icloud.com>
2026-01-27 23:27:11 +08:00
Thomas Miceli
48f2c4f5c8 Update Go + JS deps (#603) 2026-01-27 15:02:37 +08:00
Thomas Miceli
5ddea2265d Add access tokens (#602) 2026-01-27 14:43:12 +08:00
Nova Cat
1128a81071 Ignore TCP errors (#601) 2026-01-27 13:49:37 +08:00
Thomas Miceli
145bf9d81a Move Prom metrics to a dedicated port + improve Helm chart (#599) 2026-01-26 17:28:51 +08:00
52 changed files with 1695 additions and 393 deletions

View File

@@ -28,20 +28,16 @@ jobs:
npx tailwindcss -i .vitepress/theme/style.css -o .vitepress/theme/theme.css -c .vitepress/tailwind.config.js npx tailwindcss -i .vitepress/theme/style.css -o .vitepress/theme/theme.css -c .vitepress/tailwind.config.js
npm run docs:build npm run docs:build
- name: Deploy to server - name: Push to docs repository
uses: appleboy/scp-action@master run: |
with: git clone https://${{ secrets.STATIC_REPO_TOKEN }}@github.com/${{ secrets.STATIC_REPO }}.git target-repo
host: ${{ secrets.SERVER_HOST }} rm -rf target-repo/srv/opengist
username: ${{ secrets.SERVER_USERNAME }} mkdir -p target-repo/srv/opengist
key: ${{ secrets.SERVER_SSH_KEY }} cp -r docs/.vitepress/dist/* target-repo/srv/opengist/
source: "docs/.vitepress/dist/*" cd target-repo
target: ${{ secrets.SERVER_PATH }} git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
- name: Update remote docs git add .
uses: appleboy/ssh-action@master git commit -m "Deploy docs from ${{ github.repository }}@${{ github.sha }}" || echo "No changes to commit"
with: git pull --rebase
host: ${{ secrets.SERVER_HOST }} git push
username: ${{ secrets.SERVER_USERNAME }}
key: ${{ secrets.SERVER_SSH_KEY }}
script: |
${{ secrets.UPDATE_DOCS }}

View File

@@ -34,20 +34,16 @@ jobs:
helm repo index --url https://helm.opengist.io --merge index.yaml . helm repo index --url https://helm.opengist.io --merge index.yaml .
fi fi
- name: Deploy to server - name: Push to docs repository
uses: appleboy/scp-action@master run: |
with: git clone https://${{ secrets.DOCS_REPO_TOKEN }}@github.com/${{ secrets.DOCS_REPO }}.git target-repo
host: ${{ secrets.SERVER_HOST }} mkdir -p target-repo/helm
username: ${{ secrets.SERVER_USERNAME }} cp helm/*.tgz target-repo/helm/
key: ${{ secrets.SERVER_SSH_KEY }} cp helm/index.yaml target-repo/helm/
source: "./helm/*.tgz,./helm/index.yaml" cd target-repo
target: ${{ secrets.HELM_SERVER_PATH }} git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
- name: Update remote helm repository git add .
uses: appleboy/ssh-action@master git commit -m "Deploy helm chart from ${{ github.repository }}@${{ github.sha }}" || echo "No changes to commit"
with: git pull --rebase
host: ${{ secrets.SERVER_HOST }} git push
username: ${{ secrets.SERVER_USERNAME }}
key: ${{ secrets.SERVER_SSH_KEY }}
script: |
${{ secrets.UPDATE_HELM_REPO }}

View File

@@ -1,5 +1,41 @@
# Changelog # Changelog
## [1.12.1](https://github.com/thomiceli/opengist/compare/v1.12.0...v1.12.1) - 2026-02-03
See here how to [update](https://opengist.io/docs/update) Opengist.
### Added
- More translation strings (#605)
### Fixed
- Allow Access Tokens with Required Login (#611)
- Make text files renderable with mimetypes different than text/plain (#612)
- Improve security on raw files endpoint (#613)
> Admins of Opengist instances may want to run "Synchronize all gists previews" in the admin panel.
## [1.12.0](https://github.com/thomiceli/opengist/compare/v1.11.1...v1.12.0) - 2026-01-27
See here how to [update](https://opengist.io/docs/update) Opengist.
### Added
- Access tokens (#602)
- Fuzzy search for gist search (#555)
- Allow Unicode letters/numbers in topics (#597)
- Resize editor height (#600)
- More translation strings (#516) (#604)
### Fixed
- Don't panic on Go TCP errors (#601)
### Other
- Reduce footprint of Docker image (#515)
- Update Go + JS deps (#603)
- Configure Dependabot for updates on Go and NPM (#449)
### [Helm Chart](helm/opengist)
- Use existing pvc claim of provided (#547)
- Adds StatefulSet support (#549)
- Move Prom metrics to a dedicated port + support ServiceMonitor (#599)
## [1.11.1](https://github.com/thomiceli/opengist/compare/v1.11.0...v1.11.1) - 2025-09-30 ## [1.11.1](https://github.com/thomiceli/opengist/compare/v1.11.0...v1.11.1) - 2025-09-30
See here how to [update](https://opengist.io/docs/update) Opengist. See here how to [update](https://opengist.io/docs/update) Opengist.

View File

@@ -8,11 +8,11 @@ RUN apk update && \
musl-dev \ musl-dev \
libstdc++ libstdc++
COPY --from=golang:1.25-alpine3.22 /usr/local/go/ /usr/local/go/ COPY --from=golang:1.25.6-alpine3.22 /usr/local/go/ /usr/local/go/
ENV PATH="/usr/local/go/bin:${PATH}" ENV PATH="/usr/local/go/bin:${PATH}"
ENV CGO_ENABLED=0 ENV CGO_ENABLED=0
COPY --from=node:24.9.0-alpine3.22 /usr/local/ /usr/local/ COPY --from=node:24.13.0-alpine3.22 /usr/local/ /usr/local/
ENV NODE_PATH="/usr/local/lib/node_modules" ENV NODE_PATH="/usr/local/lib/node_modules"
ENV PATH="/usr/local/bin:${PATH}" ENV PATH="/usr/local/bin:${PATH}"
@@ -31,7 +31,7 @@ RUN apk add --no-cache \
gnupg \ gnupg \
xz xz
EXPOSE 6157 2222 16157 EXPOSE 6157 6158 2222 16157
RUN git config --global --add safe.directory /opengist RUN git config --global --add safe.directory /opengist
RUN make install RUN make install
@@ -46,7 +46,7 @@ FROM base AS build
RUN make RUN make
FROM alpine:3.22 as prod FROM alpine:3.22 AS prod
RUN apk update && \ RUN apk update && \
apk add --no-cache \ apk add --no-cache \
@@ -64,7 +64,7 @@ COPY --from=build --chown=opengist:opengist /opengist/config.yml /config.yml
COPY --from=build --chown=opengist:opengist /opengist/opengist . COPY --from=build --chown=opengist:opengist /opengist/opengist .
COPY --from=build --chown=opengist:opengist /opengist/docker ./docker COPY --from=build --chown=opengist:opengist /opengist/docker ./docker
EXPOSE 6157 2222 EXPOSE 6157 6158 2222
VOLUME /opengist VOLUME /opengist
HEALTHCHECK --interval=60s --timeout=30s --start-period=15s --retries=3 CMD curl -f http://localhost:6157/healthcheck || exit 1 HEALTHCHECK --interval=60s --timeout=30s --start-period=15s --retries=3 CMD curl -f http://localhost:6157/healthcheck || exit 1
ENTRYPOINT ["./docker/entrypoint.sh"] ENTRYPOINT ["./docker/entrypoint.sh"]

View File

@@ -38,7 +38,7 @@ It is similar to [GitHub Gist](https://gist.github.com/), but open-source and co
Docker [images](https://github.com/thomiceli/opengist/pkgs/container/opengist) are available for each release : Docker [images](https://github.com/thomiceli/opengist/pkgs/container/opengist) are available for each release :
```shell ```shell
docker pull ghcr.io/thomiceli/opengist:1.11 docker pull ghcr.io/thomiceli/opengist:1.12
``` ```
It can be used in a `docker-compose.yml` file : It can be used in a `docker-compose.yml` file :
@@ -50,7 +50,7 @@ It can be used in a `docker-compose.yml` file :
```yml ```yml
services: services:
opengist: opengist:
image: ghcr.io/thomiceli/opengist:1.11 image: ghcr.io/thomiceli/opengist:1.12
container_name: opengist container_name: opengist
restart: unless-stopped restart: unless-stopped
ports: ports:
@@ -77,9 +77,9 @@ Download the archive for your system from the release page [here](https://github
```shell ```shell
# example for linux amd64 # example for linux amd64
wget https://github.com/thomiceli/opengist/releases/download/v1.11.1/opengist1.11.1-linux-amd64.tar.gz wget https://github.com/thomiceli/opengist/releases/download/v1.12.1/opengist1.12.1-linux-amd64.tar.gz
tar xzvf opengist1.11.1-linux-amd64.tar.gz tar xzvf opengist1.12.1-linux-amd64.tar.gz
cd opengist cd opengist
chmod +x opengist chmod +x opengist
./opengist # with or without `--config config.yml` ./opengist # with or without `--config config.yml`

View File

@@ -55,9 +55,15 @@ http.git-enabled: true
# File permissions for Unix socket (octal format). Default: 0666 # File permissions for Unix socket (octal format). Default: 0666
unix-socket-permissions: 0666 unix-socket-permissions: 0666
# Enable or disable the metrics endpoint (either `true` or `false`). Default: false # Enable or disable the Prometheus metrics server (either `true` or `false`). Default: false
metrics.enabled: false metrics.enabled: false
# The host on which the metrics server should bind. Default: 0.0.0.0
metrics.host: 0.0.0.0
# The port on which the metrics server should listen. Default: 6158
metrics.port: 6158
# SSH built-in server configuration # SSH built-in server configuration
# Note: it is not using the SSH daemon from your machine (yet) # Note: it is not using the SSH daemon from your machine (yet)

View File

@@ -55,6 +55,7 @@ export default defineConfig({
text: 'Usage', base: '/docs/usage', items: [ text: 'Usage', base: '/docs/usage', items: [
{text: 'Init via Git', link: '/init-via-git'}, {text: 'Init via Git', link: '/init-via-git'},
{text: 'Embed Gist', link: '/embed'}, {text: 'Embed Gist', link: '/embed'},
{text: 'Access Tokens', link: '/access-tokens'},
{text: 'Gist as JSON', link: '/gist-json'}, {text: 'Gist as JSON', link: '/gist-json'},
{text: 'Import Gists from Github', link: '/import-from-github-gist'}, {text: 'Import Gists from Github', link: '/import-from-github-gist'},
{text: 'Git push options', link: '/git-push-options'}, {text: 'Git push options', link: '/git-push-options'},

View File

@@ -19,7 +19,7 @@ export default {
<div class="mx-auto lg:text-center"> <div class="mx-auto lg:text-center">
<img class="rotating h-36 mx-auto my-8 " src="https://raw.githubusercontent.com/thomiceli/opengist/master/public/img/opengist.svg" alt="" > <img class="rotating h-36 mx-auto my-8 " src="https://raw.githubusercontent.com/thomiceli/opengist/master/public/img/opengist.svg" alt="" >
<a target="_blank" href="https://github.com/thomiceli/opengist/releases" class="inline-flex items-center rounded-full bg-indigo-100 hover:bg-indigo-200 px-4 py-1.5 text-lg font-medium text-indigo-700"> <a target="_blank" href="https://github.com/thomiceli/opengist/releases" class="inline-flex items-center rounded-full bg-indigo-100 hover:bg-indigo-200 px-4 py-1.5 text-lg font-medium text-indigo-700">
<span class="pr-1">Released 1.11</span> <span class="pr-1">Released 1.12</span>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-4"> <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-4">
<path stroke-linecap="round" stroke-linejoin="round" d="m4.5 19.5 15-15m0 0H8.25m11.25 0v11.25" /> <path stroke-linecap="round" stroke-linejoin="round" d="m4.5 19.5 15-15m0 0H8.25m11.25 0v11.25" />
</svg> </svg>

View File

@@ -21,7 +21,9 @@ aside: false
| http.port | OG_HTTP_PORT | `6157` | The port on which the HTTP server should listen. | | 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`) | | http.git-enabled | OG_HTTP_GIT_ENABLED | `true` | Enable or disable git operations (clone, pull, push) via HTTP. (`true` or `false`) |
| unix-socket-permissions | OG_UNIX_SOCKET_PERMISSIONS | `0666` | File permissions for Unix socket (octal format). | | unix-socket-permissions | OG_UNIX_SOCKET_PERMISSIONS | `0666` | File permissions for Unix socket (octal format). |
| metrics.enabled | OG_METRICS_ENABLED | `false` | Enable or disable Prometheus metrics endpoint at `/metrics` (`true` or `false`) | | metrics.enabled | OG_METRICS_ENABLED | `false` | Enable or disable Prometheus metrics server (`true` or `false`) |
| metrics.host | OG_METRICS_HOST | `0.0.0.0` | The host on which the metrics server should bind. |
| metrics.port | OG_METRICS_PORT | `6158` | The port on which the metrics server should listen. |
| ssh.git-enabled | OG_SSH_GIT_ENABLED | `true` | Enable or disable git operations (clone, pull, push) via SSH. (`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.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.port | OG_SSH_PORT | `2222` | The port on which the SSH server should listen. |

View File

@@ -4,10 +4,10 @@ Opengist offers built-in support for Prometheus metrics to help you monitor the
## Enabling metrics ## 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): By default, the metrics server is disabled for security and performance reasons. To enable it, update your configuration as stated in the [configuration cheat sheet](cheat-sheet.md):
```yaml ```yaml
metrics.enabled = true metrics.enabled: true
``` ```
Alternatively, you can use the environment variable: Alternatively, you can use the environment variable:
@@ -16,7 +16,25 @@ Alternatively, you can use the environment variable:
OG_METRICS_ENABLED=true OG_METRICS_ENABLED=true
``` ```
Once enabled, metrics are available at the /metrics endpoint. Once enabled, metrics are available on a separate server at `http://0.0.0.0:6158/metrics` by default.
## Configuration
The metrics server runs on a separate port from the main application. By default, it binds to `0.0.0.0` (all interfaces) on port `6158`.
| Config Key | Environment Variable | Default | Description |
|----------------|---------------------|-------------|------------------------------------------------|
| metrics.enabled | OG_METRICS_ENABLED | `false` | Enable or disable the metrics server |
| metrics.host | OG_METRICS_HOST | `0.0.0.0` | The host on which the metrics server binds |
| metrics.port | OG_METRICS_PORT | `6158` | The port on which the metrics server listens |
Example configuration:
```yaml
metrics.enabled: true
metrics.host: 0.0.0.0
metrics.port: 6158
```
## Available metrics ## Available metrics
@@ -36,14 +54,6 @@ These standard metrics follow the Prometheus naming convention and include label
## Security Considerations ## 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. The metrics server binds to `0.0.0.0` by default, making it accessible on all network interfaces. This default works well for containerized deployments (Docker, Kubernetes) where network isolation is handled at the infrastructure level.
Example with Nginx: For bare-metal or VM deployments where the metrics port may be exposed, consider restricting to localhost by setting `metrics.host: 127.0.0.1` to only allow local access.
```shell
location /metrics {
auth_basic "Metrics";
auth_basic_user_file /etc/nginx/.htpasswd;
proxy_pass http://localhost:6157/metrics;
}
```

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,26 @@
# Access tokens
Access tokens are used to access your private gists and their raw content. For now, it is the only use while a future API is being developed.
## Creating an access token
To create an access token, follow these steps:
1. Go to Settings
2. Select the "Access Tokens" menu
3. Choose a name for your token, the scope and an expiration date (optional), then click "Create Access Token"
## Using an access token
Once you have created an access token, you can use it to access your private gists with it.
Replace `<token>` with your actual access token in the following examples.
```shell
# Access raw content of a private gist, latest revision for "file.txt". Note: this URL is obtained from the "Raw" button on the gist page.
curl -H "Authorization: Token <token>" \
http://opengist.example.com/user/gist/raw/HEAD/file.txt
# Access the JSON representation of a private gist. See "Gist as JSON" documentation for more details.
curl -H "Authorization: Token <token>" \
http://opengist.example.com/user/gist.json
```

56
go.mod
View File

@@ -1,10 +1,10 @@
module github.com/thomiceli/opengist module github.com/thomiceli/opengist
go 1.25.1 go 1.25.5
require ( require (
github.com/Kunde21/markdownfmt/v3 v3.1.0 github.com/Kunde21/markdownfmt/v3 v3.1.0
github.com/alecthomas/chroma/v2 v2.21.1 github.com/alecthomas/chroma/v2 v2.23.1
github.com/blevesearch/bleve/v2 v2.5.7 github.com/blevesearch/bleve/v2 v2.5.7
github.com/dustin/go-humanize v1.0.1 github.com/dustin/go-humanize v1.0.1
github.com/gabriel-vasile/mimetype v1.4.12 github.com/gabriel-vasile/mimetype v1.4.12
@@ -19,18 +19,18 @@ require (
github.com/labstack/echo-contrib v0.17.4 github.com/labstack/echo-contrib v0.17.4
github.com/labstack/echo/v4 v4.15.0 github.com/labstack/echo/v4 v4.15.0
github.com/markbates/goth v1.82.0 github.com/markbates/goth v1.82.0
github.com/meilisearch/meilisearch-go v0.35.1 github.com/meilisearch/meilisearch-go v0.36.0
github.com/pquerna/otp v1.5.0 github.com/pquerna/otp v1.5.0
github.com/prometheus/client_golang v1.23.2 github.com/prometheus/client_golang v1.23.2
github.com/rs/zerolog v1.34.0 github.com/rs/zerolog v1.34.0
github.com/stretchr/testify v1.11.1 github.com/stretchr/testify v1.11.1
github.com/urfave/cli/v2 v2.27.7 github.com/urfave/cli/v2 v2.27.7
github.com/yuin/goldmark v1.7.15 github.com/yuin/goldmark v1.7.16
github.com/yuin/goldmark-emoji v1.0.6 github.com/yuin/goldmark-emoji v1.0.6
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc
go.abhg.dev/goldmark/mermaid v0.6.0 go.abhg.dev/goldmark/mermaid v0.6.0
golang.org/x/crypto v0.46.0 golang.org/x/crypto v0.47.0
golang.org/x/text v0.32.0 golang.org/x/text v0.33.0
gopkg.in/yaml.v3 v3.0.1 gopkg.in/yaml.v3 v3.0.1
gorm.io/driver/mysql v1.6.0 gorm.io/driver/mysql v1.6.0
gorm.io/driver/postgres v1.6.0 gorm.io/driver/postgres v1.6.0
@@ -39,31 +39,32 @@ require (
require ( require (
filippo.io/edwards25519 v1.1.0 // indirect filippo.io/edwards25519 v1.1.0 // indirect
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 // indirect github.com/Azure/go-ntlmssp v0.1.0 // indirect
github.com/RoaringBitmap/roaring/v2 v2.10.0 // indirect github.com/RoaringBitmap/roaring/v2 v2.14.4 // indirect
github.com/andybalholm/brotli v1.2.0 // indirect github.com/andybalholm/brotli v1.2.0 // indirect
github.com/beorn7/perks v1.0.1 // indirect github.com/beorn7/perks v1.0.1 // indirect
github.com/bits-and-blooms/bitset v1.24.0 // indirect github.com/bits-and-blooms/bitset v1.24.4 // indirect
github.com/blevesearch/bleve_index_api v1.2.11 // indirect github.com/blevesearch/bleve_index_api v1.3.1 // indirect
github.com/blevesearch/geo v0.2.4 // indirect github.com/blevesearch/geo v0.2.4 // indirect
github.com/blevesearch/go-faiss v1.0.26 // indirect github.com/blevesearch/go-faiss v1.0.27 // indirect
github.com/blevesearch/go-porterstemmer v1.0.3 // indirect github.com/blevesearch/go-porterstemmer v1.0.3 // indirect
github.com/blevesearch/gtreap v0.1.1 // indirect github.com/blevesearch/gtreap v0.1.1 // indirect
github.com/blevesearch/mmap-go v1.0.4 // indirect github.com/blevesearch/mmap-go v1.2.0 // indirect
github.com/blevesearch/scorch_segment_api/v2 v2.3.13 // indirect github.com/blevesearch/scorch_segment_api/v2 v2.4.1 // indirect
github.com/blevesearch/segment v0.9.1 // indirect github.com/blevesearch/segment v0.9.1 // indirect
github.com/blevesearch/snowballstem v0.9.0 // indirect github.com/blevesearch/snowballstem v0.9.0 // indirect
github.com/blevesearch/upsidedown_store_api v1.0.2 // indirect github.com/blevesearch/upsidedown_store_api v1.0.2 // indirect
github.com/blevesearch/vellum v1.1.0 // indirect github.com/blevesearch/vellum v1.2.0 // indirect
github.com/blevesearch/zapx/v11 v11.4.2 // indirect github.com/blevesearch/zapx/v11 v11.4.2 // indirect
github.com/blevesearch/zapx/v12 v12.4.2 // indirect github.com/blevesearch/zapx/v12 v12.4.2 // indirect
github.com/blevesearch/zapx/v13 v13.4.2 // indirect github.com/blevesearch/zapx/v13 v13.4.2 // indirect
github.com/blevesearch/zapx/v14 v14.4.2 // indirect github.com/blevesearch/zapx/v14 v14.4.2 // indirect
github.com/blevesearch/zapx/v15 v15.4.2 // indirect github.com/blevesearch/zapx/v15 v15.4.2 // indirect
github.com/blevesearch/zapx/v16 v16.2.8 // indirect github.com/blevesearch/zapx/v16 v16.3.0 // indirect
github.com/boombuler/barcode v1.1.0 // indirect github.com/boombuler/barcode v1.1.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/clipperhouse/uax29/v2 v2.2.0 // indirect github.com/clipperhouse/stringish v0.1.1 // indirect
github.com/clipperhouse/uax29/v2 v2.4.0 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect
github.com/dlclark/regexp2 v1.11.5 // indirect github.com/dlclark/regexp2 v1.11.5 // indirect
@@ -76,14 +77,13 @@ require (
github.com/go-sql-driver/mysql v1.9.3 // indirect github.com/go-sql-driver/mysql v1.9.3 // indirect
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
github.com/go-webauthn/x v0.1.26 // indirect github.com/go-webauthn/x v0.1.26 // indirect
github.com/golang-jwt/jwt/v4 v4.5.2 // indirect
github.com/golang-jwt/jwt/v5 v5.3.0 // indirect github.com/golang-jwt/jwt/v5 v5.3.0 // indirect
github.com/golang/snappy v1.0.0 // indirect github.com/golang/snappy v1.0.0 // indirect
github.com/google/go-tpm v0.9.6 // indirect github.com/google/go-tpm v0.9.6 // indirect
github.com/gorilla/mux v1.8.1 // indirect github.com/gorilla/mux v1.8.1 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
github.com/jackc/pgx/v5 v5.7.6 // indirect github.com/jackc/pgx/v5 v5.8.0 // indirect
github.com/jackc/puddle/v2 v2.2.2 // indirect github.com/jackc/puddle/v2 v2.2.2 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect github.com/jinzhu/now v1.1.5 // indirect
@@ -98,11 +98,11 @@ require (
github.com/modern-go/reflect2 v1.0.2 // indirect github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/mschoch/smat v0.2.0 // indirect github.com/mschoch/smat v0.2.0 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/ncruces/go-strftime v0.1.9 // indirect github.com/ncruces/go-strftime v1.0.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/prometheus/client_model v0.6.2 // indirect github.com/prometheus/client_model v0.6.2 // indirect
github.com/prometheus/common v0.66.1 // indirect github.com/prometheus/common v0.67.5 // indirect
github.com/prometheus/procfs v0.17.0 // indirect github.com/prometheus/procfs v0.19.2 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect
@@ -111,15 +111,15 @@ require (
github.com/xrash/smetrics v0.0.0-20250705151800-55b8f293f342 // indirect github.com/xrash/smetrics v0.0.0-20250705151800-55b8f293f342 // indirect
go.etcd.io/bbolt v1.4.3 // indirect go.etcd.io/bbolt v1.4.3 // indirect
go.yaml.in/yaml/v2 v2.4.3 // indirect go.yaml.in/yaml/v2 v2.4.3 // indirect
golang.org/x/exp v0.0.0-20251002181428-27f1f14c8bb9 // indirect golang.org/x/exp v0.0.0-20260112195511-716be5621a96 // indirect
golang.org/x/net v0.48.0 // indirect golang.org/x/net v0.49.0 // indirect
golang.org/x/oauth2 v0.31.0 // indirect golang.org/x/oauth2 v0.34.0 // indirect
golang.org/x/sync v0.19.0 // indirect golang.org/x/sync v0.19.0 // indirect
golang.org/x/sys v0.39.0 // indirect golang.org/x/sys v0.40.0 // indirect
golang.org/x/time v0.14.0 // indirect golang.org/x/time v0.14.0 // indirect
google.golang.org/protobuf v1.36.10 // indirect google.golang.org/protobuf v1.36.11 // indirect
modernc.org/libc v1.66.10 // indirect modernc.org/libc v1.67.7 // indirect
modernc.org/mathutil v1.7.1 // indirect modernc.org/mathutil v1.7.1 // indirect
modernc.org/memory v1.11.0 // indirect modernc.org/memory v1.11.0 // indirect
modernc.org/sqlite v1.39.0 // indirect modernc.org/sqlite v1.44.3 // indirect
) )

135
go.sum
View File

@@ -1,16 +1,16 @@
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= 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.1.0 h1:DjFo6YtWzNqNvQdrwEyr/e4nhU3vRiwenz5QX7sFz+A=
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU= github.com/Azure/go-ntlmssp v0.1.0/go.mod h1:NYqdhxd/8aAct/s4qSYZEerdPuH1liG2/X9DiVTbhpk=
github.com/Kunde21/markdownfmt/v3 v3.1.0 h1:KiZu9LKs+wFFBQKhrZJrFZwtLnCCWJahL+S+E/3VnM0= github.com/Kunde21/markdownfmt/v3 v3.1.0 h1:KiZu9LKs+wFFBQKhrZJrFZwtLnCCWJahL+S+E/3VnM0=
github.com/Kunde21/markdownfmt/v3 v3.1.0/go.mod h1:tPXN1RTyOzJwhfHoon9wUr4HGYmWgVxSQN6VBJDkrVc= github.com/Kunde21/markdownfmt/v3 v3.1.0/go.mod h1:tPXN1RTyOzJwhfHoon9wUr4HGYmWgVxSQN6VBJDkrVc=
github.com/RoaringBitmap/roaring/v2 v2.10.0 h1:HbJ8Cs71lfCJyvmSptxeMX2PtvOC8yonlU0GQcy2Ak0= github.com/RoaringBitmap/roaring/v2 v2.14.4 h1:4aKySrrg9G/5oRtJ3TrZLObVqxgQ9f1znCRBwEwjuVw=
github.com/RoaringBitmap/roaring/v2 v2.10.0/go.mod h1:FiJcsfkGje/nZBZgCu0ZxCPOKD/hVXDS2dXi7/eUFE0= github.com/RoaringBitmap/roaring/v2 v2.14.4/go.mod h1:oMvV6omPWr+2ifRdeZvVJyaz+aoEUopyv5iH0u/+wbY=
github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0= 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/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
github.com/alecthomas/chroma/v2 v2.2.0/go.mod h1:vf4zrexSH54oEjJ7EdB65tGNHmH3pGZmVkgTP5RHvAs= github.com/alecthomas/chroma/v2 v2.2.0/go.mod h1:vf4zrexSH54oEjJ7EdB65tGNHmH3pGZmVkgTP5RHvAs=
github.com/alecthomas/chroma/v2 v2.21.1 h1:FaSDrp6N+3pphkNKU6HPCiYLgm8dbe5UXIXcoBhZSWA= github.com/alecthomas/chroma/v2 v2.23.1 h1:nv2AVZdTyClGbVQkIzlDm/rnhk1E9bU9nXwmZ/Vk/iY=
github.com/alecthomas/chroma/v2 v2.21.1/go.mod h1:NqVhfBR0lte5Ouh3DcthuUCTUpDC9cxBOfyMbMQPs3o= github.com/alecthomas/chroma/v2 v2.23.1/go.mod h1:NqVhfBR0lte5Ouh3DcthuUCTUpDC9cxBOfyMbMQPs3o=
github.com/alecthomas/repr v0.0.0-20220113201626-b1b626ac65ae/go.mod h1:2kn6fqh/zIyPLmm3ugklbEi5hg5wS435eygvNfaDQL8= github.com/alecthomas/repr v0.0.0-20220113201626-b1b626ac65ae/go.mod h1:2kn6fqh/zIyPLmm3ugklbEi5hg5wS435eygvNfaDQL8=
github.com/alecthomas/repr v0.5.2 h1:SU73FTI9D1P5UNtvseffFSGmdNci/O6RsqzeXJtP0Qs= github.com/alecthomas/repr v0.5.2 h1:SU73FTI9D1P5UNtvseffFSGmdNci/O6RsqzeXJtP0Qs=
github.com/alecthomas/repr v0.5.2/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= github.com/alecthomas/repr v0.5.2/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
@@ -20,33 +20,32 @@ github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwTo
github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= 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/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.24.4 h1:95H15Og1clikBrKr/DuzMXkQzECs1M6hhoGXLwLQOZE=
github.com/bits-and-blooms/bitset v1.24.0 h1:H4x4TuulnokZKvHLfzVRTHJfFfnHEeSYJizujEZvmAM= github.com/bits-and-blooms/bitset v1.24.4/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
github.com/bits-and-blooms/bitset v1.24.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
github.com/blevesearch/bleve/v2 v2.5.7 h1:2d9YrL5zrX5EBBW++GOaEKjE+NPWeZGaX77IM26m1Z8= github.com/blevesearch/bleve/v2 v2.5.7 h1:2d9YrL5zrX5EBBW++GOaEKjE+NPWeZGaX77IM26m1Z8=
github.com/blevesearch/bleve/v2 v2.5.7/go.mod h1:yj0NlS7ocGC4VOSAedqDDMktdh2935v2CSWOCDMHdSA= github.com/blevesearch/bleve/v2 v2.5.7/go.mod h1:yj0NlS7ocGC4VOSAedqDDMktdh2935v2CSWOCDMHdSA=
github.com/blevesearch/bleve_index_api v1.2.11 h1:bXQ54kVuwP8hdrXUSOnvTQfgK0KI1+f9A0ITJT8tX1s= github.com/blevesearch/bleve_index_api v1.3.1 h1:LdH3CQgBbIZ5UI/5Pykz87e0jfeQtVnrdZ2WUBrHHwU=
github.com/blevesearch/bleve_index_api v1.2.11/go.mod h1:rKQDl4u51uwafZxFrPD1R7xFOwKnzZW7s/LSeK4lgo0= github.com/blevesearch/bleve_index_api v1.3.1/go.mod h1:xvd48t5XMeeioWQ5/jZvgLrV98flT2rdvEJ3l/ki4Ko=
github.com/blevesearch/geo v0.2.4 h1:ECIGQhw+QALCZaDcogRTNSJYQXRtC8/m8IKiA706cqk= github.com/blevesearch/geo v0.2.4 h1:ECIGQhw+QALCZaDcogRTNSJYQXRtC8/m8IKiA706cqk=
github.com/blevesearch/geo v0.2.4/go.mod h1:K56Q33AzXt2YExVHGObtmRSFYZKYGv0JEN5mdacJJR8= github.com/blevesearch/geo v0.2.4/go.mod h1:K56Q33AzXt2YExVHGObtmRSFYZKYGv0JEN5mdacJJR8=
github.com/blevesearch/go-faiss v1.0.26 h1:4dRLolFgjPyjkaXwff4NfbZFdE/dfywbzDqporeQvXI= github.com/blevesearch/go-faiss v1.0.27 h1:7cBImYDDQ82WJd5RUZ1ie6zXztCsC73W94ZzwOjkatk=
github.com/blevesearch/go-faiss v1.0.26/go.mod h1:OMGQwOaRRYxrmeNdMrXJPvVx8gBnvE5RYrr0BahNnkk= github.com/blevesearch/go-faiss v1.0.27/go.mod h1:OMGQwOaRRYxrmeNdMrXJPvVx8gBnvE5RYrr0BahNnkk=
github.com/blevesearch/go-porterstemmer v1.0.3 h1:GtmsqID0aZdCSNiY8SkuPJ12pD4jI+DdXTAn4YRcHCo= github.com/blevesearch/go-porterstemmer v1.0.3 h1:GtmsqID0aZdCSNiY8SkuPJ12pD4jI+DdXTAn4YRcHCo=
github.com/blevesearch/go-porterstemmer v1.0.3/go.mod h1:angGc5Ht+k2xhJdZi511LtmxuEf0OVpvUUNrwmM1P7M= github.com/blevesearch/go-porterstemmer v1.0.3/go.mod h1:angGc5Ht+k2xhJdZi511LtmxuEf0OVpvUUNrwmM1P7M=
github.com/blevesearch/gtreap v0.1.1 h1:2JWigFrzDMR+42WGIN/V2p0cUvn4UP3C4Q5nmaZGW8Y= github.com/blevesearch/gtreap v0.1.1 h1:2JWigFrzDMR+42WGIN/V2p0cUvn4UP3C4Q5nmaZGW8Y=
github.com/blevesearch/gtreap v0.1.1/go.mod h1:QaQyDRAT51sotthUWAH4Sj08awFSSWzgYICSZ3w0tYk= github.com/blevesearch/gtreap v0.1.1/go.mod h1:QaQyDRAT51sotthUWAH4Sj08awFSSWzgYICSZ3w0tYk=
github.com/blevesearch/mmap-go v1.0.4 h1:OVhDhT5B/M1HNPpYPBKIEJaD0F3Si+CrEKULGCDPWmc= github.com/blevesearch/mmap-go v1.2.0 h1:l33nNKPFcBjJUMwem6sAYJPUzhUCABoK9FxZDGiFNBI=
github.com/blevesearch/mmap-go v1.0.4/go.mod h1:EWmEAOmdAS9z/pi/+Toxu99DnsbhG1TIxUoRmJw/pSs= github.com/blevesearch/mmap-go v1.2.0/go.mod h1:Vd6+20GBhEdwJnU1Xohgt88XCD/CTWcqbCNxkZpyBo0=
github.com/blevesearch/scorch_segment_api/v2 v2.3.13 h1:ZPjv/4VwWvHJZKeMSgScCapOy8+DdmsmRyLmSB88UoY= github.com/blevesearch/scorch_segment_api/v2 v2.4.1 h1:os52/JeCSLZ0YUkOuLk/Z7pu0SKUMofDPUg+VnbrRD0=
github.com/blevesearch/scorch_segment_api/v2 v2.3.13/go.mod h1:ENk2LClTehOuMS8XzN3UxBEErYmtwkE7MAArFTXs9Vc= github.com/blevesearch/scorch_segment_api/v2 v2.4.1/go.mod h1:zvilBm4BNfbnTRLW7KgCTNgk2R31JaWzwRc2BEcD7Is=
github.com/blevesearch/segment v0.9.1 h1:+dThDy+Lvgj5JMxhmOVlgFfkUtZV2kw49xax4+jTfSU= github.com/blevesearch/segment v0.9.1 h1:+dThDy+Lvgj5JMxhmOVlgFfkUtZV2kw49xax4+jTfSU=
github.com/blevesearch/segment v0.9.1/go.mod h1:zN21iLm7+GnBHWTao9I+Au/7MBiL8pPFtJBJTsk6kQw= github.com/blevesearch/segment v0.9.1/go.mod h1:zN21iLm7+GnBHWTao9I+Au/7MBiL8pPFtJBJTsk6kQw=
github.com/blevesearch/snowballstem v0.9.0 h1:lMQ189YspGP6sXvZQ4WZ+MLawfV8wOmPoD/iWeNXm8s= github.com/blevesearch/snowballstem v0.9.0 h1:lMQ189YspGP6sXvZQ4WZ+MLawfV8wOmPoD/iWeNXm8s=
github.com/blevesearch/snowballstem v0.9.0/go.mod h1:PivSj3JMc8WuaFkTSRDW2SlrulNWPl4ABg1tC/hlgLs= github.com/blevesearch/snowballstem v0.9.0/go.mod h1:PivSj3JMc8WuaFkTSRDW2SlrulNWPl4ABg1tC/hlgLs=
github.com/blevesearch/upsidedown_store_api v1.0.2 h1:U53Q6YoWEARVLd1OYNc9kvhBMGZzVrdmaozG2MfoB+A= github.com/blevesearch/upsidedown_store_api v1.0.2 h1:U53Q6YoWEARVLd1OYNc9kvhBMGZzVrdmaozG2MfoB+A=
github.com/blevesearch/upsidedown_store_api v1.0.2/go.mod h1:M01mh3Gpfy56Ps/UXHjEO/knbqyQ1Oamg8If49gRwrQ= github.com/blevesearch/upsidedown_store_api v1.0.2/go.mod h1:M01mh3Gpfy56Ps/UXHjEO/knbqyQ1Oamg8If49gRwrQ=
github.com/blevesearch/vellum v1.1.0 h1:CinkGyIsgVlYf8Y2LUQHvdelgXr6PYuvoDIajq6yR9w= github.com/blevesearch/vellum v1.2.0 h1:xkDiOEsHc2t3Cp0NsNZZ36pvc130sCzcGKOPMzXe+e0=
github.com/blevesearch/vellum v1.1.0/go.mod h1:QgwWryE8ThtNPxtgWJof5ndPfx0/YMBh+W2weHKPw8Y= github.com/blevesearch/vellum v1.2.0/go.mod h1:uEcfBJz7mAOf0Kvq6qoEKQQkLODBF46SINYNkZNae4k=
github.com/blevesearch/zapx/v11 v11.4.2 h1:l46SV+b0gFN+Rw3wUI1YdMWdSAVhskYuvxlcgpQFljs= github.com/blevesearch/zapx/v11 v11.4.2 h1:l46SV+b0gFN+Rw3wUI1YdMWdSAVhskYuvxlcgpQFljs=
github.com/blevesearch/zapx/v11 v11.4.2/go.mod h1:4gdeyy9oGa/lLa6D34R9daXNUvfMPZqUYjPwiLmekwc= github.com/blevesearch/zapx/v11 v11.4.2/go.mod h1:4gdeyy9oGa/lLa6D34R9daXNUvfMPZqUYjPwiLmekwc=
github.com/blevesearch/zapx/v12 v12.4.2 h1:fzRbhllQmEMUuAQ7zBuMvKRlcPA5ESTgWlDEoB9uQNE= github.com/blevesearch/zapx/v12 v12.4.2 h1:fzRbhllQmEMUuAQ7zBuMvKRlcPA5ESTgWlDEoB9uQNE=
@@ -57,8 +56,8 @@ github.com/blevesearch/zapx/v14 v14.4.2 h1:2SGHakVKd+TrtEqpfeq8X+So5PShQ5nW6GNxT
github.com/blevesearch/zapx/v14 v14.4.2/go.mod h1:rz0XNb/OZSMjNorufDGSpFpjoFKhXmppH9Hi7a877D8= github.com/blevesearch/zapx/v14 v14.4.2/go.mod h1:rz0XNb/OZSMjNorufDGSpFpjoFKhXmppH9Hi7a877D8=
github.com/blevesearch/zapx/v15 v15.4.2 h1:sWxpDE0QQOTjyxYbAVjt3+0ieu8NCE0fDRaFxEsp31k= github.com/blevesearch/zapx/v15 v15.4.2 h1:sWxpDE0QQOTjyxYbAVjt3+0ieu8NCE0fDRaFxEsp31k=
github.com/blevesearch/zapx/v15 v15.4.2/go.mod h1:1pssev/59FsuWcgSnTa0OeEpOzmhtmr/0/11H0Z8+Nw= github.com/blevesearch/zapx/v15 v15.4.2/go.mod h1:1pssev/59FsuWcgSnTa0OeEpOzmhtmr/0/11H0Z8+Nw=
github.com/blevesearch/zapx/v16 v16.2.8 h1:SlnzF0YGtSlrsOE3oE7EgEX6BIepGpeqxs1IjMbHLQI= github.com/blevesearch/zapx/v16 v16.3.0 h1:hF6VlN15E9CB40RMPyqOIhlDw1OOo9RItumhKMQktxw=
github.com/blevesearch/zapx/v16 v16.2.8/go.mod h1:murSoCJPCk25MqURrcJaBQ1RekuqSCSfMjXH4rHyA14= github.com/blevesearch/zapx/v16 v16.3.0/go.mod h1:zCFjv7McXWm1C8rROL+3mUoD5WYe2RKsZP3ufqcYpLY=
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
github.com/boombuler/barcode v1.1.0 h1:ChaYjBR63fr4LFyGn8E8nt7dBSt3MiU3zMOZqFvVkHo= github.com/boombuler/barcode v1.1.0 h1:ChaYjBR63fr4LFyGn8E8nt7dBSt3MiU3zMOZqFvVkHo=
github.com/boombuler/barcode v1.1.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= github.com/boombuler/barcode v1.1.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
@@ -70,8 +69,10 @@ github.com/chromedp/chromedp v0.14.0 h1:/xE5m6wEBwivhalHwlCOyYfBcAJNwg4nLw96QiCf
github.com/chromedp/chromedp v0.14.0/go.mod h1:rHzAv60xDE7VNy/MYtTUrYreSc0ujt2O1/C3bzctYBo= github.com/chromedp/chromedp v0.14.0/go.mod h1:rHzAv60xDE7VNy/MYtTUrYreSc0ujt2O1/C3bzctYBo=
github.com/chromedp/sysutil v1.1.0 h1:PUFNv5EcprjqXZD9nJb9b/c9ibAbxiYo4exNWZyipwM= github.com/chromedp/sysutil v1.1.0 h1:PUFNv5EcprjqXZD9nJb9b/c9ibAbxiYo4exNWZyipwM=
github.com/chromedp/sysutil v1.1.0/go.mod h1:WiThHUdltqCNKGc4gaU50XgYjwjYIhKWoHGPTUfWTJ8= github.com/chromedp/sysutil v1.1.0/go.mod h1:WiThHUdltqCNKGc4gaU50XgYjwjYIhKWoHGPTUfWTJ8=
github.com/clipperhouse/uax29/v2 v2.2.0 h1:ChwIKnQN3kcZteTXMgb1wztSgaU+ZemkgWdohwgs8tY= github.com/clipperhouse/stringish v0.1.1 h1:+NSqMOr3GR6k1FdRhhnXrLfztGzuG+VuFDfatpWHKCs=
github.com/clipperhouse/uax29/v2 v2.2.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM= github.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEXNWYXQgCt4hdOzA=
github.com/clipperhouse/uax29/v2 v2.4.0 h1:RXqE/l5EiAbA4u97giimKNlmpvkmz+GrBVTelsoXy9g=
github.com/clipperhouse/uax29/v2 v2.4.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo= github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo=
github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
@@ -124,8 +125,6 @@ github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6Wezm
github.com/gobwas/ws v1.4.0 h1:CTaoG1tojrh4ucGPcoJFiAQUAsEWekEWvLy7GsVNqGs= github.com/gobwas/ws v1.4.0 h1:CTaoG1tojrh4ucGPcoJFiAQUAsEWekEWvLy7GsVNqGs=
github.com/gobwas/ws v1.4.0/go.mod h1:G3gNqMNtPppf5XUz7O4shetPpcZ1VJ7zt18dlUeakrc= github.com/gobwas/ws v1.4.0/go.mod h1:G3gNqMNtPppf5XUz7O4shetPpcZ1VJ7zt18dlUeakrc=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/golang-jwt/jwt/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.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo= github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs= github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs=
@@ -151,14 +150,16 @@ github.com/gorilla/sessions v1.4.0 h1:kpIYOp/oi6MG/p5PgxApU8srsSw9tuFbt46Lt7auzq
github.com/gorilla/sessions v1.4.0/go.mod h1:FLWm50oby91+hl7p/wRxDth9bWSuk0qVL2emc7lT5ik= github.com/gorilla/sessions v1.4.0/go.mod h1:FLWm50oby91+hl7p/wRxDth9bWSuk0qVL2emc7lT5ik=
github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8= 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/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= 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 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jackc/pgx/v5 v5.7.6 h1:rWQc5FwZSPX58r1OQmkuaNicxdmExaEz5A2DO2hUuTk= github.com/jackc/pgx/v5 v5.8.0 h1:TYPDoleBBme0xGSAX3/+NujXXtpZn9HBONkQC7IEZSo=
github.com/jackc/pgx/v5 v5.7.6/go.mod h1:aruU7o91Tc2q2cFp5h4uP3f6ztExVpyVv88Xl/8Vl8M= github.com/jackc/pgx/v5 v5.8.0/go.mod h1:QVeDInX2m9VyzvNeiCJVjCkNFqzsNb43204HshNSZKw=
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= 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 h1:9YKLH6ey7H4eDBXW8khjYslgyqG2xZikXP0EQFKrle8=
@@ -206,8 +207,8 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw= github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw=
github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
github.com/meilisearch/meilisearch-go v0.35.1 h1:5H2FeY5eR4HSkaZMJIoefNzOj3XX1+5dd7ZfhAfzeMg= github.com/meilisearch/meilisearch-go v0.36.0 h1:N1etykTektXt5KPcSbhBO0d5Xx5NaKj4pJWEM7WA5dI=
github.com/meilisearch/meilisearch-go v0.35.1/go.mod h1:cUVJZ2zMqTvvwIMEEAdsWH+zrHsrLpAw6gm8Lt1MXK0= github.com/meilisearch/meilisearch-go v0.36.0/go.mod h1:HBfHzKMxcSbTOvqdfuRA/yf6Vk9IivcwKocWRuW7W78=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
@@ -217,8 +218,8 @@ github.com/mschoch/smat v0.2.0 h1:8imxQsjDm8yFEAVBe7azKmKSgzSkZXDuKkSq9374khM=
github.com/mschoch/smat v0.2.0/go.mod h1:kc9mz7DoBKqDyiRL7VZN8KvXQMWeTaVnttLRXOlotKw= github.com/mschoch/smat v0.2.0/go.mod h1:kc9mz7DoBKqDyiRL7VZN8KvXQMWeTaVnttLRXOlotKw=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= 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 v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
@@ -228,10 +229,10 @@ github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h
github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs= github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4=
github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA= github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw=
github.com/prometheus/procfs v0.17.0 h1:FuLQ+05u4ZI+SS/w9+BWEM2TXiHKsUQ9TADiRH7DuK0= github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws=
github.com/prometheus/procfs v0.17.0/go.mod h1:oPQLaDAMRbA+u8H5Pbfq+dl3VDAvHxMUOVhe0wYB2zw= github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
@@ -259,8 +260,8 @@ github.com/xrash/smetrics v0.0.0-20250705151800-55b8f293f342/go.mod h1:Ohn+xnUBi
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
github.com/yuin/goldmark v1.4.15/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yuin/goldmark v1.4.15/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yuin/goldmark v1.7.15 h1:xYJWgq3Qd8qsaZpj5pHKoEI4mosqVZi/qRpq/MdKyyk= github.com/yuin/goldmark v1.7.16 h1:n+CJdUxaFMiDUNnWC3dMWCIQJSkxH4uz3ZwQBkAlVNE=
github.com/yuin/goldmark v1.7.15/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg= github.com/yuin/goldmark v1.7.16/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
github.com/yuin/goldmark-emoji v1.0.6 h1:QWfF2FYaXwL74tfGOW5izeiZepUDroDJfWubQI9HTHs= github.com/yuin/goldmark-emoji v1.0.6 h1:QWfF2FYaXwL74tfGOW5izeiZepUDroDJfWubQI9HTHs=
github.com/yuin/goldmark-emoji v1.0.6/go.mod h1:ukxJDKFpdFb5x0a5HqbdlcKtebh086iJpI31LTKmWuA= github.com/yuin/goldmark-emoji v1.0.6/go.mod h1:ukxJDKFpdFb5x0a5HqbdlcKtebh086iJpI31LTKmWuA=
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc h1:+IAOyRda+RLrxa1WC7umKOZRsGq4QrFFMYApOeHzQwQ= github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc h1:+IAOyRda+RLrxa1WC7umKOZRsGq4QrFFMYApOeHzQwQ=
@@ -275,39 +276,37 @@ go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU= go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0= go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0=
go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8= go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8=
golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU= golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8=
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0= golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A=
golang.org/x/exp v0.0.0-20251002181428-27f1f14c8bb9 h1:TQwNpfvNkxAVlItJf6Cr5JTsVZoC/Sj7K3OZv2Pc14A= golang.org/x/exp v0.0.0-20260112195511-716be5621a96 h1:Z/6YuSHTLOHfNFdb8zVZomZr7cqNgTJvA8+Qz75D8gU=
golang.org/x/exp v0.0.0-20251002181428-27f1f14c8bb9/go.mod h1:TwQYMMnGpvZyc+JpB/UAuTNIsVJifOlSkrZkhcvpVUk= golang.org/x/exp v0.0.0-20260112195511-716be5621a96/go.mod h1:nzimsREAkjBCIEFtHiYkrJyT+2uy9YZJB7H1k68CXZU=
golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk= golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c=
golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc= golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU=
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o=
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8=
golang.org/x/oauth2 v0.31.0 h1:8Fq0yVZLh4j4YA47vHKFTa9Ew5XIrCP8LC6UeNZnLxo= golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw=
golang.org/x/oauth2 v0.31.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q= golang.org/x/term v0.39.0 h1:RclSuaJf32jOqZz74CkPA9qFuVTX7vhLlpfj/IGWlqY=
golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg= golang.org/x/term v0.39.0/go.mod h1:yxzUCTP/U+FzoxfdKmLaA0RV1WgE0VY7hXBwKtY/4ww=
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE=
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8=
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ= golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc=
golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ= golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg=
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/driver/mysql v1.6.0 h1:eNbLmNTpPpTOVZi8MMxCi2aaIm0ZpInbORNXDwyLGvg= gorm.io/driver/mysql v1.6.0 h1:eNbLmNTpPpTOVZi8MMxCi2aaIm0ZpInbORNXDwyLGvg=
@@ -316,18 +315,20 @@ gorm.io/driver/postgres v1.6.0 h1:2dxzU8xJ+ivvqTRph34QX+WrRaJlmfyPqXmoGVjMBa4=
gorm.io/driver/postgres v1.6.0/go.mod h1:vUw0mrGgrTK+uPHEhAdV4sfFELrByKVGnaVRkXDhtWo= gorm.io/driver/postgres v1.6.0/go.mod h1:vUw0mrGgrTK+uPHEhAdV4sfFELrByKVGnaVRkXDhtWo=
gorm.io/gorm v1.31.1 h1:7CA8FTFz/gRfgqgpeKIBcervUn3xSyPUmr6B2WXJ7kg= gorm.io/gorm v1.31.1 h1:7CA8FTFz/gRfgqgpeKIBcervUn3xSyPUmr6B2WXJ7kg=
gorm.io/gorm v1.31.1/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs= gorm.io/gorm v1.31.1/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs=
modernc.org/cc/v4 v4.26.5 h1:xM3bX7Mve6G8K8b+T11ReenJOT+BmVqQj0FY5T4+5Y4= modernc.org/cc/v4 v4.27.1 h1:9W30zRlYrefrDV2JE2O8VDtJ1yPGownxciz5rrbQZis=
modernc.org/cc/v4 v4.26.5/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0= modernc.org/cc/v4 v4.27.1/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
modernc.org/ccgo/v4 v4.28.1 h1:wPKYn5EC/mYTqBO373jKjvX2n+3+aK7+sICCv4Fjy1A= modernc.org/ccgo/v4 v4.30.1 h1:4r4U1J6Fhj98NKfSjnPUN7Ze2c6MnAdL0hWw6+LrJpc=
modernc.org/ccgo/v4 v4.28.1/go.mod h1:uD+4RnfrVgE6ec9NGguUNdhqzNIeeomeXf6CL0GTE5Q= modernc.org/ccgo/v4 v4.30.1/go.mod h1:bIOeI1JL54Utlxn+LwrFyjCx2n2RDiYEaJVSrgdrRfM=
modernc.org/fileutil v1.3.40 h1:ZGMswMNc9JOCrcrakF1HrvmergNLAmxOPjizirpfqBA= modernc.org/fileutil v1.3.40 h1:ZGMswMNc9JOCrcrakF1HrvmergNLAmxOPjizirpfqBA=
modernc.org/fileutil v1.3.40/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc= modernc.org/fileutil v1.3.40/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc=
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI= modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito= modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
modernc.org/gc/v3 v3.1.1 h1:k8T3gkXWY9sEiytKhcgyiZ2L0DTyCQ/nvX+LoCljoRE=
modernc.org/gc/v3 v3.1.1/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY=
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks= modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI= modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
modernc.org/libc v1.66.10 h1:yZkb3YeLx4oynyR+iUsXsybsX4Ubx7MQlSYEw4yj59A= modernc.org/libc v1.67.7 h1:H+gYQw2PyidyxwxQsGTwQw6+6H+xUk+plvOKW7+d3TI=
modernc.org/libc v1.66.10/go.mod h1:8vGSEwvoUoltr4dlywvHqjtAqHBaw0j1jI7iFBTAr2I= modernc.org/libc v1.67.7/go.mod h1:UjCSJFl2sYbJbReVQeVpq/MgzlbmDM4cRHIYFelnaDk=
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI= modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
@@ -336,8 +337,8 @@ modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns= modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w= modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE= modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
modernc.org/sqlite v1.39.0 h1:6bwu9Ooim0yVYA7IZn9demiQk/Ejp0BtTjBWFLymSeY= modernc.org/sqlite v1.44.3 h1:+39JvV/HWMcYslAwRxHb8067w+2zowvFOUrOWIy9PjY=
modernc.org/sqlite v1.39.0/go.mod h1:cPTJYSlgg3Sfg046yBShXENNtPrWrDX8bsbAQBzgQ5E= modernc.org/sqlite v1.44.3/go.mod h1:CzbrU2lSB1DKUusvwGz7rqEKIq+NUd8GWuBBZDs9/nA=
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0= modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A= modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=

View File

@@ -0,0 +1,29 @@
# Helm Chart Changelog
## 0.6.0 - 2026-02-03
- Bump Opengist image to 1.12.1
## 0.5.0 - 2026-01-27
- Bump Opengist image to 1.12.0
- Add StatefulSet support
- Add Prometheus ServiceMonitor support if Opengist metrics are enabled
- New service for metrics endpoint, dissociated from the main service
- Use existing pvc claim of provided
## 0.4.0 - 2025-09-30
- Bump Opengist image to 1.11.1
## 0.3.0 - 2025-09-21
- Bump Opengist image to 1.11.0
## 0.2.0 - 2025-05-10
- Add `deployment.env[]` in values
## 0.1.0 - 2025-04-06
- Initial release, with Opengist image 1.10.0

View File

@@ -2,8 +2,8 @@ apiVersion: v2
name: opengist name: opengist
description: Opengist Helm chart for Kubernetes description: Opengist Helm chart for Kubernetes
type: application type: application
version: 0.5.0 version: 0.6.0
appVersion: 1.11.1 appVersion: 1.12.1
home: https://opengist.io home: https://opengist.io
icon: https://raw.githubusercontent.com/thomiceli/opengist/master/public/img/opengist.svg icon: https://raw.githubusercontent.com/thomiceli/opengist/master/public/img/opengist.svg
sources: sources:

View File

@@ -1,11 +1,12 @@
# Opengist Helm Chart # Opengist Helm Chart
![Version: 0.5.0](https://img.shields.io/badge/Version-0.5.0-informational?style=flat-square) ![AppVersion: 1.11.1](https://img.shields.io/badge/AppVersion-1.11.1-informational?style=flat-square) ![Version: 0.6.0](https://img.shields.io/badge/Version-0.6.0-informational?style=flat-square) ![AppVersion: 1.12.1](https://img.shields.io/badge/AppVersion-1.12.1-informational?style=flat-square)
Opengist Helm chart for Kubernetes. Opengist Helm chart for Kubernetes. Check [CHANGELOG.md](CHANGELOG.md) for release notes.
* [Install](#install) * [Install](#install)
* [Configuration](#configuration) * [Configuration](#configuration)
* [Metrics & Monitoring](#metrics--monitoring)
* [Dependencies](#dependencies) * [Dependencies](#dependencies)
* [Meilisearch Indexer](#meilisearch-indexer) * [Meilisearch Indexer](#meilisearch-indexer)
* [PostgreSQL Database](#postgresql-database) * [PostgreSQL Database](#postgresql-database)
@@ -47,6 +48,76 @@ If defined, this existing secret will be used instead of creating a new one.
configExistingSecret: <name of the secret> configExistingSecret: <name of the secret>
``` ```
## Metrics & Monitoring
Opengist exposes Prometheus metrics on a separate port (default: `6158`). The metrics server runs independently from the main HTTP server for security.
### Enabling Metrics
To enable metrics, set `metrics.enabled: true` in your Opengist config:
```yaml
config:
metrics.enabled: true
```
This will:
1. Start a metrics server on port 6158 inside the container
2. Create a Kubernetes Service exposing the metrics ports
### Available Metrics
| Metric Name | Type | Description |
|-------------|------|-------------|
| `opengist_users_total` | Gauge | Total number of registered users |
| `opengist_gists_total` | Gauge | Total number of gists |
| `opengist_ssh_keys_total` | Gauge | Total number of SSH keys |
| `opengist_request_duration_seconds_*` | Histogram | HTTP request duration metrics |
### ServiceMonitor for Prometheus Operator
If you're using [Prometheus Operator](https://github.com/prometheus-operator/prometheus-operator), you can enable automatic service discovery with a ServiceMonitor:
```yaml
config:
metrics.enabled: true
service:
metrics:
serviceMonitor:
enabled: true
labels:
release: prometheus # match your Prometheus serviceMonitorSelector
```
### Manual Prometheus Configuration
If you're not using Prometheus Operator, you can configure Prometheus to scrape the metrics endpoint directly:
```yaml
scrape_configs:
- job_name: 'opengist'
static_configs:
- targets: ['opengist-metrics:6158']
metrics_path: /metrics
```
Or use Kubernetes service discovery:
```yaml
scrape_configs:
- job_name: 'opengist'
kubernetes_sd_configs:
- role: service
relabel_configs:
- source_labels: [__meta_kubernetes_service_label_app_kubernetes_io_component]
regex: metrics
action: keep
- source_labels: [__meta_kubernetes_service_label_app_kubernetes_io_name]
regex: opengist
action: keep
```
## Dependencies ## Dependencies
### Meilisearch Indexer ### Meilisearch Indexer

View File

@@ -67,6 +67,11 @@ spec:
- name: http - name: http
containerPort: {{ .Values.service.http.port }} containerPort: {{ .Values.service.http.port }}
protocol: TCP protocol: TCP
{{- if index .Values.config "metrics.enabled" }}
- name: metrics
containerPort: {{ .Values.service.metrics.port }}
protocol: TCP
{{- end }}
{{- if .Values.livenessProbe.enabled }} {{- if .Values.livenessProbe.enabled }}
livenessProbe: livenessProbe:
{{- toYaml (omit .Values.livenessProbe "enabled") | nindent 12 }} {{- toYaml (omit .Values.livenessProbe "enabled") | nindent 12 }}

View File

@@ -0,0 +1,41 @@
{{- if and (index .Values.config "metrics.enabled") .Values.service.metrics.serviceMonitor.enabled }}
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
name: {{ include "opengist.fullname" . }}
namespace: {{ .Values.namespace | default .Release.Namespace }}
labels:
{{- include "opengist.labels" . | nindent 4 }}
{{- with .Values.service.metrics.serviceMonitor.labels }}
{{- toYaml . | nindent 4 }}
{{- end }}
{{- with .Values.service.metrics.serviceMonitor.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
endpoints:
- port: metrics
{{- with .Values.service.metrics.serviceMonitor.interval }}
interval: {{ . }}
{{- end }}
{{- with .Values.service.metrics.serviceMonitor.scrapeTimeout }}
scrapeTimeout: {{ . }}
{{- end }}
path: /metrics
{{- with .Values.service.metrics.serviceMonitor.relabelings }}
relabelings:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.service.metrics.serviceMonitor.metricRelabelings }}
metricRelabelings:
{{- toYaml . | nindent 8 }}
{{- end }}
namespaceSelector:
matchNames:
- {{ .Values.namespace | default .Release.Namespace }}
selector:
matchLabels:
{{- include "opengist.selectorLabels" . | nindent 6 }}
app.kubernetes.io/component: metrics
{{- end }}

View File

@@ -140,6 +140,11 @@ spec:
containerPort: {{ .Values.service.ssh.port }} containerPort: {{ .Values.service.ssh.port }}
protocol: TCP protocol: TCP
{{- end }} {{- end }}
{{- if index .Values.config "metrics.enabled" }}
- name: metrics
containerPort: {{ .Values.service.metrics.port }}
protocol: TCP
{{- end }}
{{- if .Values.livenessProbe.enabled }} {{- if .Values.livenessProbe.enabled }}
livenessProbe: livenessProbe:
{{- toYaml (omit .Values.livenessProbe "enabled") | nindent 12 }} {{- toYaml (omit .Values.livenessProbe "enabled") | nindent 12 }}

View File

@@ -0,0 +1,32 @@
{{- if index .Values.config "metrics.enabled" }}
apiVersion: v1
kind: Service
metadata:
name: {{ include "opengist.fullname" . }}-metrics
namespace: {{ .Values.namespace | default .Release.Namespace }}
labels:
{{- include "opengist.labels" . | nindent 4 }}
app.kubernetes.io/component: metrics
{{- with .Values.service.metrics.labels }}
{{- toYaml . | nindent 4 }}
{{- end }}
{{- with .Values.service.metrics.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
type: {{ .Values.service.metrics.type }}
{{- if .Values.service.metrics.clusterIP }}
clusterIP: {{ .Values.service.metrics.clusterIP }}
{{- end }}
ports:
- port: {{ .Values.service.metrics.port }}
targetPort: metrics
protocol: TCP
name: metrics
{{- if and (eq .Values.service.metrics.type "NodePort") .Values.service.metrics.nodePort }}
nodePort: {{ .Values.service.metrics.nodePort }}
{{- end }}
selector:
{{- include "opengist.selectorLabels" . | nindent 4 }}
{{- end }}

View File

@@ -8,6 +8,7 @@ namespace: ""
config: config:
log-level: "warn" log-level: "warn"
log-output: "stdout" log-output: "stdout"
metrics.enabled: false
## If defined, the existing secret will be used instead of creating a new one. ## 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. ## The secret must contain a key named `config.yml` with the YAML configuration.
@@ -17,7 +18,7 @@ configExistingSecret: ""
image: image:
repository: ghcr.io/thomiceli/opengist repository: ghcr.io/thomiceli/opengist
pullPolicy: Always pullPolicy: Always
tag: "1.11.1" tag: "1.12.1"
digest: "" digest: ""
imagePullSecrets: [] imagePullSecrets: []
# - name: "image-pull-secret" # - name: "image-pull-secret"
@@ -101,6 +102,26 @@ service:
loadBalancerSourceRanges: [] loadBalancerSourceRanges: []
externalTrafficPolicy: externalTrafficPolicy:
# A metrics K8S service on port 6158 is created when the Opengist config metrics.enabled: true
metrics:
type: ClusterIP
clusterIP:
port: 6158
nodePort:
labels: {}
annotations: {}
# A service monitor can be used to work with your Prometheus setup.
serviceMonitor:
enabled: true
labels: {}
# release: kube-prom-stack
interval:
scrapeTimeout:
annotations: {}
relabelings: []
metricRelabelings: []
## HTTP Ingress for Opengist ## HTTP Ingress for Opengist
## ref: https://kubernetes.io/docs/concepts/services-networking/ingress/ ## ref: https://kubernetes.io/docs/concepts/services-networking/ingress/
ingress: ingress:

View File

@@ -9,6 +9,7 @@ import (
"github.com/thomiceli/opengist/internal/git" "github.com/thomiceli/opengist/internal/git"
"github.com/thomiceli/opengist/internal/index" "github.com/thomiceli/opengist/internal/index"
"github.com/thomiceli/opengist/internal/ssh" "github.com/thomiceli/opengist/internal/ssh"
"github.com/thomiceli/opengist/internal/web/handlers/metrics"
"github.com/thomiceli/opengist/internal/web/server" "github.com/thomiceli/opengist/internal/web/server"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v2"
"os" "os"
@@ -36,12 +37,18 @@ var CmdStart = cli.Command{
Initialize(ctx) Initialize(ctx)
server := server.NewServer(os.Getenv("OG_DEV") == "1", path.Join(config.GetHomeDir(), "sessions"), false) httpServer := server.NewServer(os.Getenv("OG_DEV") == "1", path.Join(config.GetHomeDir(), "sessions"), false)
go server.Start() go httpServer.Start()
go ssh.Start() go ssh.Start()
var metricsServer *metrics.Server
if config.C.MetricsEnabled {
metricsServer = metrics.NewServer()
go metricsServer.Start()
}
<-stopCtx.Done() <-stopCtx.Done()
shutdown(server) shutdown(httpServer, metricsServer)
return nil return nil
}, },
} }
@@ -131,7 +138,7 @@ func Initialize(ctx *cli.Context) {
} }
} }
func shutdown(server *server.Server) { func shutdown(httpServer *server.Server, metricsServer *metrics.Server) {
log.Info().Msg("Shutting down database...") log.Info().Msg("Shutting down database...")
if err := db.Close(); err != nil { if err := db.Close(); err != nil {
log.Error().Err(err).Msg("Failed to close database") log.Error().Err(err).Msg("Failed to close database")
@@ -142,7 +149,11 @@ func shutdown(server *server.Server) {
index.Close() index.Close()
} }
server.Stop() httpServer.Stop()
if metricsServer != nil {
metricsServer.Stop()
}
log.Info().Msg("Shutdown complete") log.Info().Msg("Shutdown complete")
} }

View File

@@ -79,7 +79,9 @@ type config struct {
OIDCGroupClaimName string `yaml:"oidc.group-claim-name" env:"OG_OIDC_GROUP_CLAIM_NAME"` OIDCGroupClaimName string `yaml:"oidc.group-claim-name" env:"OG_OIDC_GROUP_CLAIM_NAME"`
OIDCAdminGroup string `yaml:"oidc.admin-group" env:"OG_OIDC_ADMIN_GROUP"` OIDCAdminGroup string `yaml:"oidc.admin-group" env:"OG_OIDC_ADMIN_GROUP"`
MetricsEnabled bool `yaml:"metrics.enabled" env:"OG_METRICS_ENABLED"` MetricsEnabled bool `yaml:"metrics.enabled" env:"OG_METRICS_ENABLED"`
MetricsHost string `yaml:"metrics.host" env:"OG_METRICS_HOST"`
MetricsPort string `yaml:"metrics.port" env:"OG_METRICS_PORT"`
LDAPUrl string `yaml:"ldap.url" env:"OG_LDAP_URL"` LDAPUrl string `yaml:"ldap.url" env:"OG_LDAP_URL"`
LDAPBindDn string `yaml:"ldap.bind-dn" env:"OG_LDAP_BIND_DN"` LDAPBindDn string `yaml:"ldap.bind-dn" env:"OG_LDAP_BIND_DN"`
@@ -128,6 +130,8 @@ func configWithDefaults() (*config, error) {
c.GiteaName = "Gitea" c.GiteaName = "Gitea"
c.MetricsEnabled = false c.MetricsEnabled = false
c.MetricsHost = "0.0.0.0"
c.MetricsPort = "6158"
return c, nil return c, nil
} }

125
internal/db/access_token.go Normal file
View File

@@ -0,0 +1,125 @@
package db
import (
"crypto/rand"
"crypto/sha256"
"encoding/hex"
"time"
)
const (
NoPermission = 0
ReadPermission = 1
ReadWritePermission = 2
)
type AccessToken struct {
ID uint `gorm:"primaryKey"`
Name string
TokenHash string `gorm:"uniqueIndex,size:64"` // SHA-256 hash of the token
CreatedAt int64
ExpiresAt int64 // 0 means no expiration
LastUsedAt int64
UserID uint
User User `validate:"-"`
ScopeGist uint // 0 = none, 1 = read, 2 = read+write
}
// GenerateToken creates a new random token and returns the plain text token.
// The token hash is stored in the AccessToken struct.
// The plain text token should be shown to the user once and never stored.
func (t *AccessToken) GenerateToken() (string, error) {
bytes := make([]byte, 32)
if _, err := rand.Read(bytes); err != nil {
return "", err
}
plainToken := "og_" + hex.EncodeToString(bytes)
hash := sha256.Sum256([]byte(plainToken))
t.TokenHash = hex.EncodeToString(hash[:])
return plainToken, nil
}
func GetAccessTokenByID(tokenID uint) (*AccessToken, error) {
token := new(AccessToken)
err := db.
Where("id = ?", tokenID).
First(&token).Error
return token, err
}
func GetAccessTokenByToken(plainToken string) (*AccessToken, error) {
hash := sha256.Sum256([]byte(plainToken))
tokenHash := hex.EncodeToString(hash[:])
token := new(AccessToken)
err := db.
Preload("User").
Where("token_hash = ?", tokenHash).
First(&token).Error
return token, err
}
func GetAccessTokensByUserID(userID uint) ([]*AccessToken, error) {
var tokens []*AccessToken
err := db.
Where("user_id = ?", userID).
Order("created_at desc").
Find(&tokens).Error
return tokens, err
}
func (t *AccessToken) Create() error {
t.CreatedAt = time.Now().Unix()
return db.Create(t).Error
}
func (t *AccessToken) Delete() error {
return db.Delete(t).Error
}
func (t *AccessToken) UpdateLastUsed() error {
return db.Model(t).Update("last_used_at", time.Now().Unix()).Error
}
func (t *AccessToken) IsExpired() bool {
if t.ExpiresAt == 0 {
return false
}
return time.Now().Unix() > t.ExpiresAt
}
func (t *AccessToken) HasGistReadPermission() bool {
return t.ScopeGist >= ReadPermission
}
func (t *AccessToken) HasGistWritePermission() bool {
return t.ScopeGist >= ReadWritePermission
}
// -- DTO -- //
type AccessTokenDTO struct {
Name string `form:"name" validate:"required,max=255"`
ScopeGist uint `form:"scope_gist" validate:"min=0,max=2"`
ExpiresAt string `form:"expires_at"` // empty means no expiration, otherwise date format (YYYY-MM-DD)
}
func (dto *AccessTokenDTO) ToAccessToken() *AccessToken {
var expiresAt int64
if dto.ExpiresAt != "" {
// date input format: 2006-01-02, expires at end of day
if t, err := time.ParseInLocation("2006-01-02", dto.ExpiresAt, time.Local); err == nil {
expiresAt = t.Add(24*time.Hour - time.Second).Unix()
}
}
return &AccessToken{
Name: dto.Name,
ScopeGist: dto.ScopeGist,
ExpiresAt: expiresAt,
}
}

View File

@@ -155,7 +155,7 @@ func Setup(dbUri string) error {
return err return err
} }
if err = db.AutoMigrate(&User{}, &Gist{}, &SSHKey{}, &AdminSetting{}, &Invitation{}, &WebAuthnCredential{}, &TOTP{}, &GistTopic{}, &GistLanguage{}, &GistInitQueue{}); err != nil { if err = db.AutoMigrate(&User{}, &Gist{}, &SSHKey{}, &AdminSetting{}, &Invitation{}, &WebAuthnCredential{}, &TOTP{}, &GistTopic{}, &GistLanguage{}, &GistInitQueue{}, &AccessToken{}); err != nil {
return err return err
} }
@@ -269,5 +269,5 @@ func DeprecationDBFilename() {
} }
func TruncateDatabase() error { func TruncateDatabase() error {
return db.Migrator().DropTable("likes", &User{}, "gists", &SSHKey{}, &AdminSetting{}, &Invitation{}, &WebAuthnCredential{}, &TOTP{}, &GistTopic{}, &GistLanguage{}, &GistInitQueue{}) return db.Migrator().DropTable("likes", &User{}, "gists", &SSHKey{}, &AdminSetting{}, &Invitation{}, &WebAuthnCredential{}, &TOTP{}, &GistTopic{}, &GistLanguage{}, &GistInitQueue{}, &AccessToken{})
} }

View File

@@ -73,6 +73,7 @@ type Gist struct {
URL string URL string
Preview string Preview string
PreviewFilename string PreviewFilename string
PreviewMimeType string
Description string Description string
Private Visibility // 0: public, 1: unlisted, 2: private Private Visibility // 0: public, 1: unlisted, 2: private
UserID uint UserID uint
@@ -551,6 +552,7 @@ func (gist *Gist) UpdatePreviewAndCount(withTimestampUpdate bool) error {
if len(filesStr) == 0 { if len(filesStr) == 0 {
gist.Preview = "" gist.Preview = ""
gist.PreviewFilename = "" gist.PreviewFilename = ""
gist.PreviewMimeType = ""
} else { } else {
for _, fileStr := range filesStr { for _, fileStr := range filesStr {
file, err := gist.File("HEAD", fileStr, true) file, err := gist.File("HEAD", fileStr, true)
@@ -562,6 +564,7 @@ func (gist *Gist) UpdatePreviewAndCount(withTimestampUpdate bool) error {
} }
gist.Preview = "" gist.Preview = ""
gist.PreviewFilename = file.Filename gist.PreviewFilename = file.Filename
gist.PreviewMimeType = file.MimeType.ContentType
if !file.MimeType.CanBeEdited() { if !file.MimeType.CanBeEdited() {
continue continue

View File

@@ -25,6 +25,7 @@ type User struct {
SSHKeys []SSHKey `gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE;foreignKey:UserID"` SSHKeys []SSHKey `gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE;foreignKey:UserID"`
Liked []Gist `gorm:"many2many:likes;constraint:OnUpdate:CASCADE,OnDelete:CASCADE"` Liked []Gist `gorm:"many2many:likes;constraint:OnUpdate:CASCADE,OnDelete:CASCADE"`
WebAuthnCredentials []WebAuthnCredential `gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE;foreignKey:UserID"` WebAuthnCredentials []WebAuthnCredential `gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE;foreignKey:UserID"`
AccessTokens []AccessToken `gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE;foreignKey:UserID"`
} }
func (user *User) BeforeDelete(tx *gorm.DB) error { func (user *User) BeforeDelete(tx *gorm.DB) error {
@@ -72,6 +73,11 @@ func (user *User) BeforeDelete(tx *gorm.DB) error {
return err return err
} }
err = tx.Where("user_id = ?", user.ID).Delete(&AccessToken{}).Error
if err != nil {
return err
}
err = tx.Where("user_id = ?", user.ID).Delete(&Gist{}).Error err = tx.Where("user_id = ?", user.ID).Delete(&Gist{}).Error
if err != nil { if err != nil {
return err return err

View File

@@ -2,43 +2,45 @@ package git
import ( import (
"fmt" "fmt"
"net/http"
"strings" "strings"
"github.com/gabriel-vasile/mimetype" "github.com/gabriel-vasile/mimetype"
) )
type MimeType struct { type MimeType struct {
ContentType string ContentType string
extension string extension string
golangContentType string // json, m3u, etc. still renderable as text
} }
func (mt MimeType) IsText() bool { func (mt MimeType) IsText() bool {
return strings.Contains(mt.ContentType, "text/") return strings.HasPrefix(mt.ContentType, "text/") || strings.HasPrefix(mt.golangContentType, "text/")
} }
func (mt MimeType) IsCSV() bool { func (mt MimeType) IsCSV() bool {
return strings.Contains(mt.ContentType, "text/csv") && return strings.HasPrefix(mt.ContentType, "text/csv") &&
(strings.HasSuffix(mt.extension, ".csv")) (strings.HasSuffix(mt.extension, ".csv"))
} }
func (mt MimeType) IsImage() bool { func (mt MimeType) IsImage() bool {
return strings.Contains(mt.ContentType, "image/") return strings.HasPrefix(mt.ContentType, "image/")
} }
func (mt MimeType) IsSVG() bool { func (mt MimeType) IsSVG() bool {
return strings.Contains(mt.ContentType, "image/svg+xml") return strings.HasPrefix(mt.ContentType, "image/svg+xml")
} }
func (mt MimeType) IsPDF() bool { func (mt MimeType) IsPDF() bool {
return strings.Contains(mt.ContentType, "application/pdf") return strings.HasPrefix(mt.ContentType, "application/pdf")
} }
func (mt MimeType) IsAudio() bool { func (mt MimeType) IsAudio() bool {
return strings.Contains(mt.ContentType, "audio/") return strings.HasPrefix(mt.ContentType, "audio/")
} }
func (mt MimeType) IsVideo() bool { func (mt MimeType) IsVideo() bool {
return strings.Contains(mt.ContentType, "video/") return strings.HasPrefix(mt.ContentType, "video/")
} }
func (mt MimeType) CanBeHighlighted() bool { func (mt MimeType) CanBeHighlighted() bool {
@@ -87,5 +89,5 @@ func (mt MimeType) RenderType() string {
} }
func DetectMimeType(data []byte, extension string) MimeType { func DetectMimeType(data []byte, extension string) MimeType {
return MimeType{mimetype.Detect(data).String(), extension} return MimeType{mimetype.Detect(data).String(), extension, http.DetectContentType(data)}
} }

View File

@@ -157,6 +157,7 @@ settings.password-label-title: Password
settings.header.account: Account settings.header.account: Account
settings.header.mfa: MFA settings.header.mfa: MFA
settings.header.ssh: SSH settings.header.ssh: SSH
settings.header.tokens: Access tokens
settings.header.style: Style settings.header.style: Style
settings.style.gist-code: Gist code settings.style.gist-code: Gist code
settings.style.no-soft-wrap: No Soft Wrap settings.style.no-soft-wrap: No Soft Wrap
@@ -169,6 +170,26 @@ settings.style.theme: Theme
settings.style.theme-light: Light settings.style.theme-light: Light
settings.style.theme-dark: Dark settings.style.theme-dark: Dark
settings.style.theme-auto: Auto settings.style.theme-auto: Auto
settings.create-token: Create access token
settings.create-token-help: Access tokens can be used to access the API
settings.token-name: Name
settings.token-permissions: Permissions
settings.token-gist-permission: Gists
settings.token-permission-none: No access
settings.token-permission-read: Read
settings.token-permission-read-write: Read & Write
settings.delete-token: Delete
settings.delete-token-confirm: Confirm deletion of access token
settings.token-created-at: Created
settings.token-never-used: Never used
settings.token-last-used: Last used
settings.token-expiration: Expiration
settings.token-expiration-help: Leave empty for no expiration
settings.token-expires-at: Expires
settings.token-no-expiration: No expiration
settings.token-expired: expired
settings.token-created: Token created, make sure to copy it now, you won't be able to see it again!
settings.token-deleted: Access token deleted
auth.signup-disabled: Administrator has disabled signing up auth.signup-disabled: Administrator has disabled signing up
auth.login: Login auth.login: Login

View File

@@ -51,7 +51,7 @@ gist.edit.save: Сохранить
gist.list.joined: Зарегистрирован gist.list.joined: Зарегистрирован
gist.list.all: Все фрагменты gist.list.all: Все фрагменты
gist.list.search-results: Результаты поиска gist.list.search-results: Результаты поиска
gist.list.sort: Сортировка gist.list.sort: Сортировка
gist.list.sort-by-created: по дате создания gist.list.sort-by-created: по дате создания
gist.list.sort-by-updated: по дате обновления gist.list.sort-by-updated: по дате обновления
@@ -159,19 +159,19 @@ admin.created_at: Создан
admin.config-link: Эти настройки могут быть %s файлом конфигурации YAML и/или переменными окружения. admin.config-link: Эти настройки могут быть %s файлом конфигурации YAML и/или переменными окружения.
admin.config-link-overriden: перекрыты admin.config-link-overriden: перекрыты
admin.disable-signup: Запретить регистрацию admin.disable-signup: Запретить регистрацию
admin.disable-signup_help: Запретить создание новых доступов admin.disable-signup_help: Запретить создание новых доступов.
admin.require-login: Требовать авторизацию admin.require-login: Требовать авторизацию
admin.require-login_help: Запретить просмотр фрагментов без авторизации. admin.require-login_help: Запретить просмотр фрагментов без авторизации.
admin.disable-login: Запретить авторизацию по паролю admin.disable-login: Запретить авторизацию по паролю
admin.disable-login_help: Запретить авторизацию с вводом пароля, форсировать внешнюю авторизацию через Gitea/GitHub. admin.disable-login_help: Запретить авторизацию с вводом пароля, форсировать внешнюю авторизацию через Gitea/GitHub.
admin.disable-gravatar: Запретить Gravatar admin.disable-gravatar: Запретить Gravatar
admin.disable-gravatar_help: Запретить использование Gravatar как провайдера изображений профиля. admin.disable-gravatar_help: Запретить использование Gravatar как провайдера изображений профиля.
admin.allow-gists-without-login: admin.allow-gists-without-login: Разрешить доступ к отдельным фрагментам без авторизации
admin.allow-gists-without-login_help: admin.allow-gists-without-login_help: Разрешает просматривать и скачивать отдельные фрагменты без входа, но требует авторизацию для поиска фрагментов.
admin.users.delete_confirm: Вы уверены что хотите удалить этого пользователя? admin.users.delete_confirm: Вы уверены что хотите удалить этого пользователя?
admin.gists.title: Название admin.gists.title: Название
admin.gists.private: Приватный admin.gists.private: Приватный?
admin.gists.nb-files: Файлов admin.gists.nb-files: Файлов
admin.gists.nb-likes: Понравилось admin.gists.nb-likes: Понравилось
admin.gists.delete_confirm: Вы уверены что хотите удалить этот фрагмент? admin.gists.delete_confirm: Вы уверены что хотите удалить этот фрагмент?
@@ -183,77 +183,175 @@ gist.list.all-liked-by: 'Все фрагменты, понравившиеся %
gist.list.all-forked-by: 'Все фрагменты, ответвлённые %s' gist.list.all-forked-by: 'Все фрагменты, ответвлённые %s'
gist.list.all-from: 'Все фрагменты от %s' gist.list.all-from: 'Все фрагменты от %s'
gist.search.found: 'фрагментов найдено' gist.search.found: 'фрагментов найдено'
gist.search.no-results: 'Не найден ни один фрагмент' gist.search.no-results: 'Фрагменты не найдены'
gist.search.help.user: 'фрагментов создано пользователем' gist.search.help.user: 'фрагментов создано пользователем'
gist.search.help.title: '' gist.search.help.title: 'фрагментов с указанным заголовком'
gist.search.help.filename: '' gist.search.help.filename: 'фрагменты содержащие файлы с указанным именем'
gist.search.help.extension: '' gist.search.help.extension: 'фрагменты, содержащие файлы с указанным расширением'
gist.search.help.language: '' gist.search.help.language: 'фрагменты, содержащие файлы с указанным языком'
gist.forks.for: '' gist.forks.for: 'Форки фрагмента %s'
gist.likes.for: '' gist.likes.for: 'Лайки фрагмента %s'
gist.revision-of: '' gist.revision-of: 'Ревизия фрагмента %s'
settings.link-gitlab-account: '' settings.link-gitlab-account: 'Привязать учётную запись Gitlab'
settings.unlink-gitlab-account: '' settings.unlink-gitlab-account: 'Отвязать учётную запись GitHub'
settings.change-username: '' settings.change-username: 'Сменить имя пользователя'
settings.create-password: '' settings.create-password: 'Создать пароль'
settings.create-password-help: '' settings.create-password-help: 'Создайте пароль для входа в Opengist по HTTP'
settings.change-password: '' settings.change-password: 'Сменить пароль'
settings.change-password-help: '' settings.change-password-help: 'Смените пароль для входа в Opengist по HTTP'
settings.password-label-title: '' settings.password-label-title: 'Пароль'
error.page-not-found: '' error.page-not-found: 'Страница не найдена'
error.bad-request: '' error.bad-request: 'Неверный запрос'
error.signup-disabled: '' error.signup-disabled: 'Регистрация недоступна'
error.signup-disabled-form: '' error.signup-disabled-form: 'Регистрация через форму недоступна'
error.login-disabled-form: '' error.login-disabled-form: 'Авторизация через форму недоступна'
error.complete-oauth-login: '' error.complete-oauth-login: 'Не удалось завершить авторизацию пользователя: %s'
error.oauth-unsupported: '' error.oauth-unsupported: 'Провайдер OAuth не поддерживается'
error.cannot-bind-data: '' error.cannot-bind-data: 'Не удалось обработать данные'
error.invalid-number: '' error.invalid-number: 'Некорректное числовое значение'
error.invalid-character-unescaped: '' error.invalid-character-unescaped: 'Обнаружен неверный неэкранированный символ'
admin.invitations: '' admin.invitations: 'Инвайты'
admin.invitations.create: '' admin.invitations.create: 'Создать инвайт'
admin.actions.sync-previews: '' admin.actions.sync-previews: 'Обновить предпросмотры всех фрагментов'
admin.actions.reset-hooks: '' admin.actions.reset-hooks: 'Сбросить хуки Git-сервера для всех репозиториев'
admin.actions.index-gists: '' admin.actions.index-gists: 'Проиндексировать все фрагменты'
validation.should-not-be-empty: '' validation.should-not-be-empty: 'Поле %s не должно быть пустым'
admin.invitations.help: '' admin.invitations.help: 'Инвайты используются для создания аккаунта, даже когда регистрация запрещена.'
admin.invitations.max_uses: '' admin.invitations.max_uses: 'Максимальное количество использований'
admin.invitations.expires_at: '' admin.invitations.expires_at: 'Истекает'
admin.invitations.code: '' admin.invitations.code: 'Код'
admin.invitations.copy_link: '' admin.invitations.copy_link: 'Скопировать ссылку'
admin.invitations.uses: '' admin.invitations.uses: 'Количество использований'
admin.invitations.expired: '' admin.invitations.expired: 'Истёк'
flash.admin.user-deleted: '' flash.admin.user-deleted: 'Пользователь удалён'
flash.admin.gist-deleted: '' flash.admin.gist-deleted: 'Фрагмент удалён'
flash.admin.invitation-created: '' flash.admin.invitation-created: 'Приглашение создано'
flash.admin.invitation-deleted: '' flash.admin.invitation-deleted: 'Приглашение удалено'
flash.admin.sync-fs: '' flash.admin.sync-fs: 'Выполняется синхронизация репозиториев с файловой системой…'
flash.admin.sync-db: '' flash.admin.sync-db: 'Выполняется синхронизация репозиториев с базой данных…'
flash.admin.git-gc: '' flash.admin.git-gc: 'Сборка мусора в репозиториях…'
flash.admin.sync-previews: '' flash.admin.sync-previews: 'Обновление предпросмотров фрагментов…'
flash.admin.reset-hooks: '' flash.admin.reset-hooks: 'Пересоздание Git-хуков для всех репозиториев…'
flash.admin.index-gists: '' flash.admin.index-gists: 'Выполняется индексация фрагментов…'
flash.auth.username-exists: '' flash.auth.username-exists: 'Такое имя пользователя уже занято'
flash.auth.invalid-credentials: '' flash.auth.invalid-credentials: 'Некорректные данные для входа'
flash.auth.account-linked-oauth: '' flash.auth.account-linked-oauth: 'Учётная запись связана с %s'
flash.auth.account-unlinked-oauth: '' flash.auth.account-unlinked-oauth: 'Учётная запись отключена от %s'
flash.auth.user-sshkeys-not-retrievable: '' flash.auth.user-sshkeys-not-retrievable: 'Не удалось получить SSH-ключи пользователя'
flash.auth.user-sshkeys-not-created: '' flash.auth.user-sshkeys-not-created: 'Не удалось создать SSH-ключ'
flash.auth.must-be-logged-in: '' flash.auth.must-be-logged-in: 'Для доступа к фрагментам необходимо войти в аккаунт'
flash.gist.visibility-changed: '' flash.gist.visibility-changed: 'Видимость фрагмента изменена'
flash.gist.deleted: '' flash.gist.deleted: 'Фрагмент удалён'
flash.gist.fork-own-gist: '' flash.gist.fork-own-gist: 'Нельзя создать форк собственного фрагмента'
flash.gist.forked: '' flash.gist.forked: 'Фрагмент создан как форк'
flash.user.email-updated: '' flash.user.email-updated: 'Адрес электронной почты обновлён'
flash.user.invalid-ssh-key: '' flash.user.invalid-ssh-key: 'Неверный SSH-ключ'
flash.user.ssh-key-added: '' flash.user.ssh-key-added: 'SSH-ключ добавлен'
flash.user.ssh-key-deleted: '' flash.user.ssh-key-deleted: 'SSH-ключ удалён'
flash.user.password-updated: '' flash.user.password-updated: 'Пароль обновлён'
flash.user.username-updated: '' flash.user.username-updated: 'Имя пользователя обновлено'
validation.is-too-long: '' validation.is-too-long: 'Поле %s слишком длинное'
validation.should-not-include-sub-directory: '' validation.should-not-include-sub-directory: 'Поле %s не должно содержать подкаталоги'
validation.should-only-contain-alphanumeric-characters: '' validation.should-only-contain-alphanumeric-characters: 'Поле %s должно содержать только буквы и цифры'
validation.should-only-contain-alphanumeric-characters-and-dashes: '' validation.should-only-contain-alphanumeric-characters-and-dashes: 'Поле %s должно содержать только буквы, цифры и дефисы'
validation.not-enough: '' validation.not-enough: 'Недостаточно %s'
validation.invalid: '' validation.invalid: 'Неверный %s'
html.title.admin-panel: '' html.title.admin-panel: 'Панель администратора'
settings.ssh-key-exists: SSH-ключ уже существует
gist.file-binary-edit: Этот файл является бинарным.
gist.preview-non-available: Предпросмотр недоступен
gist.file-raw: Не удалось отобразить файл.
gist.new.topics: Темы (через пробел)
gist.list.topic-results-topic: Все фрагменты с темой %s
gist.search.help.topic: фрагменты с заданной темой
gist.search.placeholder.title: Заголовок
gist.search.placeholder.visibility: Видимость
gist.search.placeholder.public: Публичный
gist.search.placeholder.unlisted: Скрытый
gist.search.placeholder.private: Приватный
gist.search.placeholder.language: Язык
gist.search.placeholder.all: Все
gist.search.placeholder.topics: Темы
gist.search.placeholder.search: Поиск
gist.new.drop-files: Перетащите файлы сюда или нажмите для загрузки
gist.new.any-file-type: Поддерживаются файлы любого типа
gist.delete.confirm: Вы уверены, что хотите удалить этот gist?
gist.list.topic-results: Все фрагменты с этой темой
gist.revision.binary-file-changes: Изменения в бинарных файлах не отображаются
admin.actions.sync-gist-languages: Обновить языки всех фрагментов
admin.invitations.delete_confirm: Вы хотите удалить это приглашение?
flash.auth.passkey-deleted: Ключ доступа удалён
flash.auth.passkey-registred: Ключ доступа %s зарегистрирован
validation.invalid-gist-topics: 'Некорректные темы фрагмента: они должны начинаться с буквы или цифры, быть не длиннее 50 символов и могут содержать дефисы'
settings.header.tokens: Токены доступа
settings.style.removed-lines-color: Цвет удалённых строк
settings.style.added-lines-color: Цвет добавленных строк
settings.style.git-lines-color: Цвет git-строк
settings.style.save-style: Сохранить оформление
settings.create-token: Создать токен доступа
settings.create-token-help: Токены доступа используются для доступа к API
settings.token-name: Название
settings.delete-token-confirm: Подтвердите удаление токена доступа
settings.token-permissions: Права доступа
settings.token-gist-permission: Фрагменты
settings.token-permission-none: Нет доступа
settings.token-permission-read: Чтение
settings.token-permission-read-write: Чтение и запись
settings.delete-token: Удалить
settings.token-created-at: Создан
settings.token-never-used: Не использовался
settings.token-expiration: Срок действия
settings.token-expiration-help: Оставьте пустым, чтобы срок действия не ограничивался
settings.token-expires-at: Истекает
settings.token-expired: истёк
settings.token-deleted: Токен доступа удалён
auth.mfa.delete-passkey-confirm: Подтвердите удаление ключа доступа
auth.mfa.use-passkey: Использовать ключ доступа
auth.mfa.bind-passkey: Добавить ключ доступа
auth.mfa.login-with-passkey: Войти с помощью ключа доступа
auth.mfa.waiting-for-passkey-input: Ожидание подтверждения в браузере…
auth.mfa.use-passkey-to-finish: Используйте ключ доступа для завершения аутентификации
auth.mfa.passkeys-help: Добавьте ключ доступа для входа в аккаунт и использования в качестве MFA.
auth.mfa.passkey-name: Название
auth.mfa.delete-passkey: Удалить
auth.mfa.passkey-added-at: Добавлен
auth.mfa.passkey-never-used: Никогда не использовался
auth.mfa.passkey-last-used: Последнее использование
auth.totp.already-enabled: TOTP уже включён
auth.totp.invalid-secret: Некорректный секретный ключ TOTP
auth.totp.invalid-code: Некорректный одноразовый код
auth.totp.code-used: Код восстановления %s уже был использован и больше недействителен. Вы можете отключить MFA или сгенерировать новые коды.
auth.totp.disabled: Двухфакторная аутентификация TOTP отключена
auth.totp.disable: Отключить TOTP
auth.totp.enter-code: Введите код из приложения Authenticator
auth.totp.submit: Подтвердить
auth.totp.proceed: Продолжить
auth.totp.scan-qr-code: Отсканируйте QR-код ниже в приложении-аутентификаторе для включения двухфакторной аутентификации или введите указанную строку и подтвердите кодом.
error.not-in-mfa-session: Пользователь не находится в MFA-сессии
error.no-file-uploaded: Файл не загружен
error.cannot-open-file: Не удалось открыть загруженный файл
auth.totp.help: TOTP — это метод двухфакторной аутентификации, использующий общий секрет для генерации одноразового пароля.
auth.totp.use: Использовать TOTP
auth.totp.regenerate-recovery-codes: Сгенерировать коды восстановления заново
auth.totp: Одноразовый пароль по времени (TOTP)
flash.admin.sync-gist-languages: Обновление языков фрагментов…
settings.token-created: Токен создан, обязательно сохраните его, повторно он показан не будет!
settings.token-last-used: Последнее использование
settings.token-no-expiration: Бессрочно
auth.totp.save-recovery-codes: Сохраните коды восстановления в безопасном месте. Они понадобятся для восстановления доступа к аккаунту при утере приложения-аутентификатора.
auth.totp.enter-recovery-key: или код восстановления, если вы потеряли устройство
settings.style.theme: Тема
settings.style.theme-light: Светлая тема
settings.style.theme-dark: Тёмная тема
settings.style.theme-auto: Авто
auth.mfa: Регистрация отключена администратором
auth.mfa.passkey: Вход
auth.mfa.passkeys: Ключи доступа
auth.totp.code: Код
settings.header.account: Аккаунт
settings.header.mfa: Двухфакторная аутентификация (MFA)
settings.header.ssh: SSH
settings.header.style: Тема оформления
settings.style.gist-code: Код фрагмента
settings.style.no-soft-wrap: Без переносов строк
settings.style.soft-wrap: Перенос строк

View File

@@ -27,8 +27,9 @@ func (r HighlightedFile) InternalType() string {
type RenderedGist struct { type RenderedGist struct {
*db.Gist *db.Gist
Lines []string Lines []string
HTML string HTML string
PreviewMimeType *git.MimeType
} }
func highlightFile(file *git.File) (HighlightedFile, error) { func highlightFile(file *git.File) (HighlightedFile, error) {
@@ -76,6 +77,18 @@ func HighlightGistPreview(gist *db.Gist) (RenderedGist, error) {
Gist: gist, Gist: gist,
} }
if gist.PreviewMimeType != "" {
mt := &git.MimeType{ContentType: gist.PreviewMimeType}
if mt.CanBeEmbedded() {
rendered.PreviewMimeType = mt
return rendered, nil
}
}
if gist.Preview == "" {
return rendered, nil
}
style := newStyle() style := newStyle()
lexer := newLexer(gist.PreviewFilename) lexer := newLexer(gist.PreviewFilename)
if lexer.Config().Name == "markdown" { if lexer.Config().Name == "markdown" {

View File

@@ -3,6 +3,7 @@ package gist
import ( import (
"archive/zip" "archive/zip"
"bytes" "bytes"
"net/url"
"strconv" "strconv"
"github.com/thomiceli/opengist/internal/db" "github.com/thomiceli/opengist/internal/db"
@@ -19,8 +20,23 @@ func RawFile(ctx *context.Context) error {
if file == nil { if file == nil {
return ctx.NotFound("File not found") return ctx.NotFound("File not found")
} }
ctx.Response().Header().Set("Content-Type", file.MimeType.ContentType)
ctx.Response().Header().Set("Content-Disposition", "inline; filename=\""+file.Filename+"\"") if file.MimeType.IsSVG() {
ctx.Response().Header().Set("Content-Security-Policy", "default-src 'none'; style-src 'unsafe-inline'; sandbox")
} else if file.MimeType.IsPDF() {
ctx.Response().Header().Set("Content-Security-Policy", "default-src 'none'; style-src 'unsafe-inline'")
}
if file.MimeType.CanBeEmbedded() {
ctx.Response().Header().Set("Content-Type", file.MimeType.ContentType)
} else if file.MimeType.IsText() {
ctx.Response().Header().Set("Content-Type", "text/plain")
} else {
ctx.Response().Header().Set("Content-Type", "application/octet-stream")
}
ctx.Response().Header().Set("Content-Disposition", "inline; filename=\""+url.PathEscape(file.Filename)+"\"")
ctx.Response().Header().Set("X-Content-Type-Options", "nosniff")
return ctx.PlainText(200, file.Content) return ctx.PlainText(200, file.Content)
} }
@@ -36,8 +52,9 @@ func DownloadFile(ctx *context.Context) error {
} }
ctx.Response().Header().Set("Content-Type", file.MimeType.ContentType) ctx.Response().Header().Set("Content-Type", file.MimeType.ContentType)
ctx.Response().Header().Set("Content-Disposition", "attachment; filename="+file.Filename) ctx.Response().Header().Set("Content-Disposition", "attachment; filename=\""+url.PathEscape(file.Filename)+"\"")
ctx.Response().Header().Set("Content-Length", strconv.Itoa(len(file.Content))) ctx.Response().Header().Set("Content-Length", strconv.Itoa(len(file.Content)))
ctx.Response().Header().Set("X-Content-Type-Options", "nosniff")
_, err = ctx.Response().Write([]byte(file.Content)) _, err = ctx.Response().Write([]byte(file.Content))
if err != nil { if err != nil {
return ctx.ErrorRes(500, "Error downloading the file", err) return ctx.ErrorRes(500, "Error downloading the file", err)

View File

@@ -1,16 +1,12 @@
package metrics package metrics
import ( import (
"github.com/labstack/echo-contrib/echoprometheus"
"github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto" "github.com/prometheus/client_golang/prometheus/promauto"
"github.com/thomiceli/opengist/internal/config"
"github.com/thomiceli/opengist/internal/db" "github.com/thomiceli/opengist/internal/db"
"github.com/thomiceli/opengist/internal/web/context"
) )
var ( var (
// Using promauto to automatically register metrics with the default registry
countUsersGauge prometheus.Gauge countUsersGauge prometheus.Gauge
countGistsGauge prometheus.Gauge countGistsGauge prometheus.Gauge
countSSHKeysGauge prometheus.Gauge countSSHKeysGauge prometheus.Gauge
@@ -18,84 +14,52 @@ var (
metricsInitialized bool = false metricsInitialized bool = false
) )
// initMetrics initializes metrics if they're not already initialized
func initMetrics() { func initMetrics() {
if metricsInitialized { if metricsInitialized {
return return
} }
// Only initialize metrics if they're enabled countUsersGauge = promauto.NewGauge(
if config.C.MetricsEnabled { prometheus.GaugeOpts{
countUsersGauge = promauto.NewGauge( Name: "opengist_users_total",
prometheus.GaugeOpts{ Help: "Total number of users",
Name: "opengist_users_total", },
Help: "Total number of users", )
},
)
countGistsGauge = promauto.NewGauge( countGistsGauge = promauto.NewGauge(
prometheus.GaugeOpts{ prometheus.GaugeOpts{
Name: "opengist_gists_total", Name: "opengist_gists_total",
Help: "Total number of gists", Help: "Total number of gists",
}, },
) )
countSSHKeysGauge = promauto.NewGauge( countSSHKeysGauge = promauto.NewGauge(
prometheus.GaugeOpts{ prometheus.GaugeOpts{
Name: "opengist_ssh_keys_total", Name: "opengist_ssh_keys_total",
Help: "Total number of SSH keys", Help: "Total number of SSH keys",
}, },
) )
metricsInitialized = true metricsInitialized = true
}
} }
// updateMetrics refreshes all metric values from the database
func updateMetrics() { func updateMetrics() {
// Only update metrics if they're enabled if !metricsInitialized {
if !config.C.MetricsEnabled || !metricsInitialized {
return return
} }
// Update users count
countUsers, err := db.CountAll(&db.User{}) countUsers, err := db.CountAll(&db.User{})
if err == nil { if err == nil {
countUsersGauge.Set(float64(countUsers)) countUsersGauge.Set(float64(countUsers))
} }
// Update gists count
countGists, err := db.CountAll(&db.Gist{}) countGists, err := db.CountAll(&db.Gist{})
if err == nil { if err == nil {
countGistsGauge.Set(float64(countGists)) countGistsGauge.Set(float64(countGists))
} }
// Update SSH keys count
countKeys, err := db.CountAll(&db.SSHKey{}) countKeys, err := db.CountAll(&db.SSHKey{})
if err == nil { if err == nil {
countSSHKeysGauge.Set(float64(countKeys)) countSSHKeysGauge.Set(float64(countKeys))
} }
} }
// Metrics handles prometheus metrics endpoint requests.
func Metrics(ctx *context.Context) error {
// If metrics are disabled, return 404
if !config.C.MetricsEnabled {
return ctx.NotFound("Metrics endpoint is disabled")
}
// Initialize metrics if not already done
initMetrics()
// Update metrics
updateMetrics()
// Get the Echo context
echoCtx := ctx.Context
// Use the Prometheus metrics handler
handler := echoprometheus.NewHandler()
// Call the handler
return handler(echoCtx)
}

View File

@@ -0,0 +1,50 @@
package metrics
import (
"net/http"
"github.com/labstack/echo-contrib/echoprometheus"
"github.com/labstack/echo/v4"
"github.com/rs/zerolog/log"
"github.com/thomiceli/opengist/internal/config"
)
type Server struct {
echo *echo.Echo
}
func NewServer() *Server {
e := echo.New()
e.HideBanner = true
e.HidePort = true
s := &Server{echo: e}
initMetrics()
e.GET("/metrics", func(ctx echo.Context) error {
updateMetrics()
return echoprometheus.NewHandler()(ctx)
})
return s
}
func (s *Server) Start() {
addr := config.C.MetricsHost + ":" + config.C.MetricsPort
log.Info().Msg("Starting metrics server on http://" + addr)
if err := s.echo.Start(addr); err != nil && err != http.ErrServerClosed {
log.Error().Err(err).Msg("Failed to start metrics server")
}
}
func (s *Server) Stop() {
log.Info().Msg("Stopping metrics server...")
if err := s.echo.Close(); err != nil {
log.Error().Err(err).Msg("Failed to stop metrics server")
}
}
func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
s.echo.ServeHTTP(w, r)
}

View File

@@ -0,0 +1,75 @@
package settings
import (
"strconv"
"github.com/thomiceli/opengist/internal/db"
"github.com/thomiceli/opengist/internal/i18n"
"github.com/thomiceli/opengist/internal/validator"
"github.com/thomiceli/opengist/internal/web/context"
)
func AccessTokens(ctx *context.Context) error {
user := ctx.User
tokens, err := db.GetAccessTokensByUserID(user.ID)
if err != nil {
return ctx.ErrorRes(500, "Cannot get access tokens", err)
}
ctx.SetData("accessTokens", tokens)
ctx.SetData("settingsHeaderPage", "tokens")
ctx.SetData("htmlTitle", ctx.TrH("settings"))
return ctx.Html("settings_tokens.html")
}
func AccessTokensProcess(ctx *context.Context) error {
user := ctx.User
dto := new(db.AccessTokenDTO)
if err := ctx.Bind(dto); err != nil {
return ctx.ErrorRes(400, ctx.Tr("error.cannot-bind-data"), err)
}
if err := ctx.Validate(dto); err != nil {
ctx.AddFlash(validator.ValidationMessages(&err, ctx.GetData("locale").(*i18n.Locale)), "error")
return ctx.RedirectTo("/settings/access-tokens")
}
token := dto.ToAccessToken()
token.UserID = user.ID
plainToken, err := token.GenerateToken()
if err != nil {
return ctx.ErrorRes(500, "Cannot generate token", err)
}
if err := token.Create(); err != nil {
return ctx.ErrorRes(500, "Cannot create access token", err)
}
// Show the token once to the user
ctx.AddFlash(ctx.Tr("settings.token-created"), "success")
ctx.AddFlash(plainToken, "success")
return ctx.RedirectTo("/settings/access-tokens")
}
func AccessTokensDelete(ctx *context.Context) error {
user := ctx.User
tokenID, err := strconv.Atoi(ctx.Param("id"))
if err != nil {
return ctx.RedirectTo("/settings/access-tokens")
}
token, err := db.GetAccessTokenByID(uint(tokenID))
if err != nil || token.UserID != user.ID {
return ctx.RedirectTo("/settings/access-tokens")
}
if err := token.Delete(); err != nil {
return ctx.ErrorRes(500, "Cannot delete access token", err)
}
ctx.AddFlash(ctx.Tr("settings.token-deleted"), "success")
return ctx.RedirectTo("/settings/access-tokens")
}

View File

@@ -8,6 +8,7 @@ import (
"path/filepath" "path/filepath"
"regexp" "regexp"
"strings" "strings"
"syscall"
"time" "time"
"github.com/labstack/echo-contrib/echoprometheus" "github.com/labstack/echo-contrib/echoprometheus"
@@ -37,8 +38,7 @@ func (s *Server) registerMiddlewares() {
s.echo.Use(Middleware(dataInit).toEcho()) s.echo.Use(Middleware(dataInit).toEcho())
s.echo.Use(Middleware(locale).toEcho()) s.echo.Use(Middleware(locale).toEcho())
if config.C.MetricsEnabled { if config.C.MetricsEnabled {
p := echoprometheus.NewMiddleware("opengist") s.echo.Use(echoprometheus.NewMiddleware("opengist"))
s.echo.Use(p)
} }
s.echo.Pre(middleware.MethodOverrideWithConfig(middleware.MethodOverrideConfig{ s.echo.Pre(middleware.MethodOverrideWithConfig(middleware.MethodOverrideConfig{
@@ -91,21 +91,33 @@ func (s *Server) errorHandler(err error, ctx echo.Context) {
data["error"] = err data["error"] = err
if acceptJson { if acceptJson {
if err := ctx.JSON(httpErr.Code, httpErr); err != nil { if err := ctx.JSON(httpErr.Code, httpErr); err != nil {
if errors.Is(err, syscall.EPIPE) || errors.Is(err, syscall.ECONNRESET) {
return
}
log.Fatal().Err(err).Send() log.Fatal().Err(err).Send()
} }
return return
} }
if err := ctx.Render(httpErr.Code, "error", data); err != nil { if err := ctx.Render(httpErr.Code, "error", data); err != nil {
if errors.Is(err, syscall.EPIPE) || errors.Is(err, syscall.ECONNRESET) {
return
}
log.Fatal().Err(err).Send() log.Fatal().Err(err).Send()
} }
return return
} }
if errors.Is(err, syscall.EPIPE) || errors.Is(err, syscall.ECONNRESET) {
return
}
log.Error().Err(err).Send() log.Error().Err(err).Send()
httpErr = echo.NewHTTPError(http.StatusInternalServerError, err.Error()) httpErr = echo.NewHTTPError(http.StatusInternalServerError, err.Error())
data["error"] = httpErr data["error"] = httpErr
if err := ctx.Render(500, "error", data); err != nil { if err := ctx.Render(500, "error", data); err != nil {
if errors.Is(err, syscall.EPIPE) || errors.Is(err, syscall.ECONNRESET) {
return
}
log.Fatal().Err(err).Send() log.Fatal().Err(err).Send()
} }
} }
@@ -194,6 +206,9 @@ func makeCheckRequireLogin(isSingleGistAccess bool) Middleware {
return next(ctx) return next(ctx)
} }
if getUserByToken(ctx) != nil {
return next(ctx)
}
allow, err := auth.ShouldAllowUnauthenticatedGistAccess(handlers.ContextAuthInfo{Context: ctx}, isSingleGistAccess) allow, err := auth.ShouldAllowUnauthenticatedGistAccess(handlers.ContextAuthInfo{Context: ctx}, isSingleGistAccess)
if err != nil { if err != nil {
log.Fatal().Err(err).Msg("Failed to check if unauthenticated access is allowed") log.Fatal().Err(err).Msg("Failed to check if unauthenticated access is allowed")
@@ -314,6 +329,39 @@ func loadSettings(ctx *context.Context) error {
return nil return nil
} }
// getUserByToken checks the Authorization header for token-based auth.
// Expects format: Authorization: Token <token>
// Returns the user if the token is valid and has gist read permission, nil otherwise.
func getUserByToken(ctx *context.Context) *db.User {
authHeader := ctx.Request().Header.Get("Authorization")
if authHeader == "" {
return nil
}
if !strings.HasPrefix(authHeader, "Token ") {
return nil
}
plainToken := strings.TrimPrefix(authHeader, "Token ")
accessToken, err := db.GetAccessTokenByToken(plainToken)
if err != nil {
return nil
}
if accessToken.IsExpired() {
return nil
}
if !accessToken.HasGistReadPermission() {
return nil
}
_ = accessToken.UpdateLastUsed()
return &accessToken.User
}
func gistInit(next Handler) Handler { func gistInit(next Handler) Handler {
return func(ctx *context.Context) error { return func(ctx *context.Context) error {
currUser := ctx.User currUser := ctx.User
@@ -340,7 +388,12 @@ func gistInit(next Handler) Handler {
if gist.Private == db.PrivateVisibility { if gist.Private == db.PrivateVisibility {
if currUser == nil || currUser.ID != gist.UserID { if currUser == nil || currUser.ID != gist.UserID {
return ctx.NotFound("Gist not found") // Check for token-based auth via Authorization header
if tokenUser := getUserByToken(ctx); tokenUser != nil && tokenUser.ID == gist.UserID {
// Token is valid and belongs to gist owner, allow access
} else {
return ctx.NotFound("Gist not found")
}
} }
} }

View File

@@ -192,6 +192,9 @@ func (s *Server) setFuncMap() {
"humanDate": func(t int64) string { "humanDate": func(t int64) string {
return time.Unix(t, 0).Format("02/01/2006 15:04") return time.Unix(t, 0).Format("02/01/2006 15:04")
}, },
"humanDateOnly": func(t int64) string {
return time.Unix(t, 0).Format("02/01/2006")
},
"mainTheme": func(theme *db.UserStyleDTO) string { "mainTheme": func(theme *db.UserStyleDTO) string {
if theme == nil { if theme == nil {
return "auto" return "auto"

View File

@@ -17,7 +17,6 @@ import (
"github.com/thomiceli/opengist/internal/web/handlers/gist" "github.com/thomiceli/opengist/internal/web/handlers/gist"
"github.com/thomiceli/opengist/internal/web/handlers/git" "github.com/thomiceli/opengist/internal/web/handlers/git"
"github.com/thomiceli/opengist/internal/web/handlers/health" "github.com/thomiceli/opengist/internal/web/handlers/health"
"github.com/thomiceli/opengist/internal/web/handlers/metrics"
"github.com/thomiceli/opengist/internal/web/handlers/settings" "github.com/thomiceli/opengist/internal/web/handlers/settings"
"github.com/thomiceli/opengist/public" "github.com/thomiceli/opengist/public"
) )
@@ -34,10 +33,6 @@ func (s *Server) registerRoutes() {
r.GET("/healthcheck", health.Healthcheck) r.GET("/healthcheck", health.Healthcheck)
if config.C.MetricsEnabled {
r.GET("/metrics", metrics.Metrics)
}
r.GET("/register", auth.Register) r.GET("/register", auth.Register)
r.POST("/register", auth.ProcessRegister) r.POST("/register", auth.ProcessRegister)
r.GET("/login", auth.Login) r.GET("/login", auth.Login)
@@ -67,6 +62,9 @@ func (s *Server) registerRoutes() {
sA.DELETE("/account", settings.AccountDeleteProcess) sA.DELETE("/account", settings.AccountDeleteProcess)
sA.POST("/ssh-keys", settings.SshKeysProcess) sA.POST("/ssh-keys", settings.SshKeysProcess)
sA.DELETE("/ssh-keys/:id", settings.SshKeysDelete) sA.DELETE("/ssh-keys/:id", settings.SshKeysDelete)
sA.GET("/access-tokens", settings.AccessTokens)
sA.POST("/access-tokens", settings.AccessTokensProcess)
sA.DELETE("/access-tokens/:id", settings.AccessTokensDelete)
sA.DELETE("/passkeys/:id", settings.PasskeyDelete) sA.DELETE("/passkeys/:id", settings.PasskeyDelete)
sA.PUT("/password", settings.PasswordProcess) sA.PUT("/password", settings.PasswordProcess)
sA.PUT("/username", settings.UsernameProcess) sA.PUT("/username", settings.UsernameProcess)

View File

@@ -0,0 +1,448 @@
package test
import (
"testing"
"time"
"github.com/stretchr/testify/require"
"github.com/thomiceli/opengist/internal/db"
)
func TestAccessTokensCRUD(t *testing.T) {
s := Setup(t)
defer Teardown(t, s)
// Register and login
user1 := db.UserDTO{Username: "thomas", Password: "thomas"}
register(t, s, user1)
// Access tokens page requires login
s.sessionCookie = ""
err := s.Request("GET", "/settings/access-tokens", nil, 302)
require.NoError(t, err)
login(t, s, user1)
// Access tokens page
err = s.Request("GET", "/settings/access-tokens", nil, 200)
require.NoError(t, err)
// Create a token with read permission
tokenDTO := db.AccessTokenDTO{
Name: "test-token",
ScopeGist: db.ReadPermission,
}
err = s.Request("POST", "/settings/access-tokens", tokenDTO, 302)
require.NoError(t, err)
// Verify token was created in database
tokens, err := db.GetAccessTokensByUserID(1)
require.NoError(t, err)
require.Len(t, tokens, 1)
require.Equal(t, "test-token", tokens[0].Name)
require.Equal(t, uint(db.ReadPermission), tokens[0].ScopeGist)
require.Equal(t, int64(0), tokens[0].ExpiresAt)
// Create another token with expiration
tomorrow := time.Now().AddDate(0, 0, 1).Format("2006-01-02")
tokenDTO2 := db.AccessTokenDTO{
Name: "expiring-token",
ScopeGist: db.ReadWritePermission,
ExpiresAt: tomorrow,
}
err = s.Request("POST", "/settings/access-tokens", tokenDTO2, 302)
require.NoError(t, err)
tokens, err = db.GetAccessTokensByUserID(1)
require.NoError(t, err)
require.Len(t, tokens, 2)
// Delete the first token
err = s.Request("DELETE", "/settings/access-tokens/1", nil, 302)
require.NoError(t, err)
tokens, err = db.GetAccessTokensByUserID(1)
require.NoError(t, err)
require.Len(t, tokens, 1)
require.Equal(t, "expiring-token", tokens[0].Name)
}
func TestAccessTokenPrivateGistAccess(t *testing.T) {
s := Setup(t)
defer Teardown(t, s)
// Register user and create a private gist
user1 := db.UserDTO{Username: "thomas", Password: "thomas"}
register(t, s, user1)
gist1 := db.GistDTO{
Title: "private-gist",
Description: "my private gist",
VisibilityDTO: db.VisibilityDTO{
Private: db.PrivateVisibility,
},
Name: []string{"secret.txt"},
Content: []string{"secret content"},
}
err := s.Request("POST", "/", gist1, 302)
require.NoError(t, err)
gist1db, err := db.GetGistByID("1")
require.NoError(t, err)
// Create access token with read permission
token := &db.AccessToken{
Name: "read-token",
UserID: 1,
ScopeGist: db.ReadPermission,
}
plainToken, err := token.GenerateToken()
require.NoError(t, err)
err = token.Create()
require.NoError(t, err)
// Clear session - simulate unauthenticated request
s.sessionCookie = ""
// Without token, private gist should return 404
err = s.Request("GET", "/thomas/"+gist1db.Uuid, nil, 404)
require.NoError(t, err)
// With valid token, private gist should be accessible
headers := map[string]string{"Authorization": "Token " + plainToken}
err = s.RequestWithHeaders("GET", "/thomas/"+gist1db.Uuid, nil, 200, headers)
require.NoError(t, err)
// Raw content should also be accessible with token
err = s.RequestWithHeaders("GET", "/thomas/"+gist1db.Uuid+"/raw/HEAD/secret.txt", nil, 200, headers)
require.NoError(t, err)
// JSON endpoint should also be accessible with token
err = s.RequestWithHeaders("GET", "/thomas/"+gist1db.Uuid+".json", nil, 200, headers)
require.NoError(t, err)
// Invalid token should not work
invalidHeaders := map[string]string{"Authorization": "Token invalid_token"}
err = s.RequestWithHeaders("GET", "/thomas/"+gist1db.Uuid, nil, 404, invalidHeaders)
require.NoError(t, err)
}
func TestAccessTokenPermissions(t *testing.T) {
s := Setup(t)
defer Teardown(t, s)
// Register user and create a private gist
user1 := db.UserDTO{Username: "thomas", Password: "thomas"}
register(t, s, user1)
gist1 := db.GistDTO{
Title: "private-gist",
Description: "my private gist",
VisibilityDTO: db.VisibilityDTO{
Private: db.PrivateVisibility,
},
Name: []string{"file.txt"},
Content: []string{"content"},
}
err := s.Request("POST", "/", gist1, 302)
require.NoError(t, err)
gist1db, err := db.GetGistByID("1")
require.NoError(t, err)
// Create token with NO permission
noPermToken := &db.AccessToken{
Name: "no-perm-token",
UserID: 1,
ScopeGist: db.NoPermission,
}
noPermPlain, err := noPermToken.GenerateToken()
require.NoError(t, err)
err = noPermToken.Create()
require.NoError(t, err)
// Create token with READ permission
readToken := &db.AccessToken{
Name: "read-token",
UserID: 1,
ScopeGist: db.ReadPermission,
}
readPlain, err := readToken.GenerateToken()
require.NoError(t, err)
err = readToken.Create()
require.NoError(t, err)
s.sessionCookie = ""
// No permission token should not grant access
noPermHeaders := map[string]string{"Authorization": "Token " + noPermPlain}
err = s.RequestWithHeaders("GET", "/thomas/"+gist1db.Uuid, nil, 404, noPermHeaders)
require.NoError(t, err)
// Read permission token should grant access
readHeaders := map[string]string{"Authorization": "Token " + readPlain}
err = s.RequestWithHeaders("GET", "/thomas/"+gist1db.Uuid, nil, 200, readHeaders)
require.NoError(t, err)
}
func TestAccessTokenExpiration(t *testing.T) {
s := Setup(t)
defer Teardown(t, s)
// Register user and create a private gist
user1 := db.UserDTO{Username: "thomas", Password: "thomas"}
register(t, s, user1)
gist1 := db.GistDTO{
Title: "private-gist",
Description: "my private gist",
VisibilityDTO: db.VisibilityDTO{
Private: db.PrivateVisibility,
},
Name: []string{"file.txt"},
Content: []string{"content"},
}
err := s.Request("POST", "/", gist1, 302)
require.NoError(t, err)
gist1db, err := db.GetGistByID("1")
require.NoError(t, err)
// Create an expired token
expiredToken := &db.AccessToken{
Name: "expired-token",
UserID: 1,
ScopeGist: db.ReadPermission,
ExpiresAt: time.Now().Add(-24 * time.Hour).Unix(), // Expired yesterday
}
expiredPlain, err := expiredToken.GenerateToken()
require.NoError(t, err)
err = expiredToken.Create()
require.NoError(t, err)
// Create a valid (non-expired) token
validToken := &db.AccessToken{
Name: "valid-token",
UserID: 1,
ScopeGist: db.ReadPermission,
ExpiresAt: time.Now().Add(24 * time.Hour).Unix(), // Expires tomorrow
}
validPlain, err := validToken.GenerateToken()
require.NoError(t, err)
err = validToken.Create()
require.NoError(t, err)
s.sessionCookie = ""
// Expired token should not grant access
expiredHeaders := map[string]string{"Authorization": "Token " + expiredPlain}
err = s.RequestWithHeaders("GET", "/thomas/"+gist1db.Uuid, nil, 404, expiredHeaders)
require.NoError(t, err)
// Valid token should grant access
validHeaders := map[string]string{"Authorization": "Token " + validPlain}
err = s.RequestWithHeaders("GET", "/thomas/"+gist1db.Uuid, nil, 200, validHeaders)
require.NoError(t, err)
}
func TestAccessTokenWrongUser(t *testing.T) {
s := Setup(t)
defer Teardown(t, s)
// Register two users
user1 := db.UserDTO{Username: "thomas", Password: "thomas"}
register(t, s, user1)
// Create a private gist for user1
gist1 := db.GistDTO{
Title: "thomas-private-gist",
Description: "thomas private gist",
VisibilityDTO: db.VisibilityDTO{
Private: db.PrivateVisibility,
},
Name: []string{"file.txt"},
Content: []string{"content"},
}
err := s.Request("POST", "/", gist1, 302)
require.NoError(t, err)
gist1db, err := db.GetGistByID("1")
require.NoError(t, err)
s.sessionCookie = ""
user2 := db.UserDTO{Username: "kaguya", Password: "kaguya"}
register(t, s, user2)
// Create token for user2
user2Token := &db.AccessToken{
Name: "kaguya-token",
UserID: 2,
ScopeGist: db.ReadPermission,
}
user2Plain, err := user2Token.GenerateToken()
require.NoError(t, err)
err = user2Token.Create()
require.NoError(t, err)
s.sessionCookie = ""
// User2's token should NOT grant access to user1's private gist
user2Headers := map[string]string{"Authorization": "Token " + user2Plain}
err = s.RequestWithHeaders("GET", "/thomas/"+gist1db.Uuid, nil, 404, user2Headers)
require.NoError(t, err)
// Create token for user1
user1Token := &db.AccessToken{
Name: "thomas-token",
UserID: 1,
ScopeGist: db.ReadPermission,
}
user1Plain, err := user1Token.GenerateToken()
require.NoError(t, err)
err = user1Token.Create()
require.NoError(t, err)
// User1's token SHOULD grant access to user1's private gist
user1Headers := map[string]string{"Authorization": "Token " + user1Plain}
err = s.RequestWithHeaders("GET", "/thomas/"+gist1db.Uuid, nil, 200, user1Headers)
require.NoError(t, err)
}
func TestAccessTokenLastUsedUpdate(t *testing.T) {
s := Setup(t)
defer Teardown(t, s)
// Register user and create a private gist
user1 := db.UserDTO{Username: "thomas", Password: "thomas"}
register(t, s, user1)
gist1 := db.GistDTO{
Title: "private-gist",
Description: "my private gist",
VisibilityDTO: db.VisibilityDTO{
Private: db.PrivateVisibility,
},
Name: []string{"file.txt"},
Content: []string{"content"},
}
err := s.Request("POST", "/", gist1, 302)
require.NoError(t, err)
gist1db, err := db.GetGistByID("1")
require.NoError(t, err)
// Create token
token := &db.AccessToken{
Name: "test-token",
UserID: 1,
ScopeGist: db.ReadPermission,
}
plainToken, err := token.GenerateToken()
require.NoError(t, err)
err = token.Create()
require.NoError(t, err)
// Verify LastUsedAt is 0 initially
tokenFromDB, err := db.GetAccessTokenByID(token.ID)
require.NoError(t, err)
require.Equal(t, int64(0), tokenFromDB.LastUsedAt)
s.sessionCookie = ""
// Use the token
headers := map[string]string{"Authorization": "Token " + plainToken}
err = s.RequestWithHeaders("GET", "/thomas/"+gist1db.Uuid, nil, 200, headers)
require.NoError(t, err)
// Verify LastUsedAt was updated
tokenFromDB, err = db.GetAccessTokenByID(token.ID)
require.NoError(t, err)
require.NotEqual(t, int64(0), tokenFromDB.LastUsedAt)
}
func TestAccessTokenWithRequireLogin(t *testing.T) {
s := Setup(t)
defer Teardown(t, s)
admin := db.UserDTO{Username: "thomas", Password: "thomas"}
register(t, s, admin)
gist1 := db.GistDTO{
Title: "private-gist",
Description: "my private gist",
VisibilityDTO: db.VisibilityDTO{
Private: db.PrivateVisibility,
},
Name: []string{"file.txt"},
Content: []string{"content"},
}
err := s.Request("POST", "/", gist1, 302)
require.NoError(t, err)
gist1db, err := db.GetGistByID("1")
require.NoError(t, err)
gist2 := db.GistDTO{
Title: "public-gist",
Description: "my public gist",
VisibilityDTO: db.VisibilityDTO{
Private: db.PublicVisibility,
},
Name: []string{"public.txt"},
Content: []string{"public content"},
}
err = s.Request("POST", "/", gist2, 302)
require.NoError(t, err)
gist2db, err := db.GetGistByID("2")
require.NoError(t, err)
token := &db.AccessToken{
Name: "read-token",
UserID: 1,
ScopeGist: db.ReadPermission,
}
plainToken, err := token.GenerateToken()
require.NoError(t, err)
err = token.Create()
require.NoError(t, err)
err = s.Request("PUT", "/admin-panel/set-config", settingSet{"require-login", "1"}, 200)
require.NoError(t, err)
s.sessionCookie = ""
err = s.Request("GET", "/thomas/"+gist1db.Uuid, nil, 302)
require.NoError(t, err)
err = s.Request("GET", "/thomas/"+gist2db.Uuid, nil, 302)
require.NoError(t, err)
headers := map[string]string{"Authorization": "Token " + plainToken}
err = s.RequestWithHeaders("GET", "/thomas/"+gist1db.Uuid, nil, 200, headers)
require.NoError(t, err)
err = s.RequestWithHeaders("GET", "/thomas/"+gist2db.Uuid, nil, 200, headers)
require.NoError(t, err)
err = s.RequestWithHeaders("GET", "/thomas/"+gist1db.Uuid+"/raw/HEAD/file.txt", nil, 200, headers)
require.NoError(t, err)
invalidHeaders := map[string]string{"Authorization": "Token invalid_token"}
err = s.RequestWithHeaders("GET", "/thomas/"+gist1db.Uuid, nil, 302, invalidHeaders)
require.NoError(t, err)
noPermToken := &db.AccessToken{
Name: "no-perm-token",
UserID: 1,
ScopeGist: db.NoPermission,
}
noPermPlain, err := noPermToken.GenerateToken()
require.NoError(t, err)
err = noPermToken.Create()
require.NoError(t, err)
noPermHeaders := map[string]string{"Authorization": "Token " + noPermPlain}
err = s.RequestWithHeaders("GET", "/thomas/"+gist1db.Uuid, nil, 302, noPermHeaders)
require.NoError(t, err)
}

View File

@@ -1,15 +1,15 @@
package test package test
import ( import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/thomiceli/opengist/internal/db"
"io" "io"
"net/http" "net/http/httptest"
"os"
"strconv" "strconv"
"strings" "strings"
"testing" "testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/thomiceli/opengist/internal/db"
) )
var ( var (
@@ -41,22 +41,12 @@ var (
// - Total number of SSH keys // - Total number of SSH keys
// //
// The test follows these steps: // The test follows these steps:
// 1. Enables metrics via environment variable // 1. Sets up test environment
// 2. Sets up test environment // 2. Registers and logs in an admin user
// 3. Registers and logs in an admin user // 3. Creates a gist and adds an SSH key
// 4. Creates a gist and adds an SSH key // 4. Creates a metrics server and queries the /metrics endpoint
// 5. Queries the metrics endpoint // 5. Verifies the reported metrics match expected values
// 6. Verifies the reported metrics match expected values
//
// Environment variables:
// - OG_METRICS_ENABLED: Set to "true" for this test
func TestMetrics(t *testing.T) { func TestMetrics(t *testing.T) {
originalValue := os.Getenv("OG_METRICS_ENABLED")
os.Setenv("OG_METRICS_ENABLED", "true")
defer os.Setenv("OG_METRICS_ENABLED", originalValue)
s := Setup(t) s := Setup(t)
defer Teardown(t, s) defer Teardown(t, s)
@@ -72,12 +62,16 @@ func TestMetrics(t *testing.T) {
err = s.Request("POST", "/settings/ssh-keys", SSHKey, 302) err = s.Request("POST", "/settings/ssh-keys", SSHKey, 302)
require.NoError(t, err) require.NoError(t, err)
var metricsRes http.Response // Create a metrics server and query it
err = s.Request("GET", "/metrics", nil, 200, &metricsRes) metricsServer := NewTestMetricsServer()
require.NoError(t, err)
body, err := io.ReadAll(metricsRes.Body) req := httptest.NewRequest("GET", "/metrics", nil)
defer metricsRes.Body.Close() w := httptest.NewRecorder()
metricsServer.ServeHTTP(w, req)
require.Equal(t, 200, w.Code)
body, err := io.ReadAll(w.Body)
require.NoError(t, err) require.NoError(t, err)
lines := strings.Split(string(body), "\n") lines := strings.Split(string(body), "\n")

View File

@@ -22,6 +22,7 @@ import (
"github.com/thomiceli/opengist/internal/config" "github.com/thomiceli/opengist/internal/config"
"github.com/thomiceli/opengist/internal/db" "github.com/thomiceli/opengist/internal/db"
"github.com/thomiceli/opengist/internal/git" "github.com/thomiceli/opengist/internal/git"
"github.com/thomiceli/opengist/internal/web/handlers/metrics"
"github.com/thomiceli/opengist/internal/web/server" "github.com/thomiceli/opengist/internal/web/server"
) )
@@ -50,6 +51,10 @@ func (s *TestServer) stop() {
} }
func (s *TestServer) Request(method, uri string, data interface{}, expectedCode int, responsePtr ...*http.Response) error { func (s *TestServer) Request(method, uri string, data interface{}, expectedCode int, responsePtr ...*http.Response) error {
return s.RequestWithHeaders(method, uri, data, expectedCode, nil, responsePtr...)
}
func (s *TestServer) RequestWithHeaders(method, uri string, data interface{}, expectedCode int, headers map[string]string, responsePtr ...*http.Response) error {
var bodyReader io.Reader var bodyReader io.Reader
if method == http.MethodPost || method == http.MethodPut { if method == http.MethodPost || method == http.MethodPut {
values := structToURLValues(data) values := structToURLValues(data)
@@ -63,6 +68,10 @@ func (s *TestServer) Request(method, uri string, data interface{}, expectedCode
req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
} }
for key, value := range headers {
req.Header.Set(key, value)
}
if s.sessionCookie != "" { if s.sessionCookie != "" {
req.AddCookie(&http.Cookie{Name: "session", Value: s.sessionCookie}) req.AddCookie(&http.Cookie{Name: "session", Value: s.sessionCookie})
} }
@@ -119,6 +128,9 @@ func structToURLValues(s interface{}) url.Values {
if field.Type.Kind() == reflect.Int { if field.Type.Kind() == reflect.Int {
fieldValue := rValue.Field(i).Int() fieldValue := rValue.Field(i).Int()
v.Add(tag, strconv.FormatInt(fieldValue, 10)) v.Add(tag, strconv.FormatInt(fieldValue, 10))
} else if field.Type.Kind() == reflect.Uint {
fieldValue := rValue.Field(i).Uint()
v.Add(tag, strconv.FormatUint(fieldValue, 10))
} else if field.Type.Kind() == reflect.Slice { } else if field.Type.Kind() == reflect.Slice {
fieldValue := rValue.Field(i).Interface().([]string) fieldValue := rValue.Field(i).Interface().([]string)
for _, va := range fieldValue { for _, va := range fieldValue {
@@ -240,3 +252,7 @@ type invitationAdmin struct {
nbMax string `form:"nbMax"` nbMax string `form:"nbMax"`
expiredAtUnix string `form:"expiredAtUnix"` expiredAtUnix string `form:"expiredAtUnix"`
} }
func NewTestMetricsServer() *metrics.Server {
return metrics.NewServer()
}

32
package-lock.json generated
View File

@@ -10,9 +10,9 @@
"@codemirror/commands": "^6.10.1", "@codemirror/commands": "^6.10.1",
"@codemirror/lang-javascript": "^6.2.4", "@codemirror/lang-javascript": "^6.2.4",
"@codemirror/language": "^6.12.1", "@codemirror/language": "^6.12.1",
"@codemirror/state": "^6.5.3", "@codemirror/state": "^6.5.4",
"@codemirror/text": "^0.19.6", "@codemirror/text": "^0.19.6",
"@codemirror/view": "^6.39.8", "@codemirror/view": "^6.39.11",
"@tailwindcss/forms": "^0.5.11", "@tailwindcss/forms": "^0.5.11",
"@tailwindcss/typography": "^0.5.19", "@tailwindcss/typography": "^0.5.19",
"@tailwindcss/vite": "^4.1.18", "@tailwindcss/vite": "^4.1.18",
@@ -20,12 +20,12 @@
"github-markdown-css": "^5.8.1", "github-markdown-css": "^5.8.1",
"highlight.js": "^11.11.1", "highlight.js": "^11.11.1",
"jdenticon": "^3.3.0", "jdenticon": "^3.3.0",
"katex": "^0.16.27", "katex": "^0.16.28",
"marked": "^17.0.1", "marked": "^17.0.1",
"nodemon": "^3.1.11", "nodemon": "^3.1.11",
"pdfobject": "^2.3.1", "pdfobject": "^2.3.1",
"tailwindcss": "^4.1.18", "tailwindcss": "^4.1.18",
"vite": "^7.3.0" "vite": "^7.3.1"
} }
}, },
"node_modules/@catppuccin/highlightjs": { "node_modules/@catppuccin/highlightjs": {
@@ -133,9 +133,9 @@
} }
}, },
"node_modules/@codemirror/state": { "node_modules/@codemirror/state": {
"version": "6.5.3", "version": "6.5.4",
"resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.5.3.tgz", "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.5.4.tgz",
"integrity": "sha512-MerMzJzlXogk2fxWFU1nKp36bY5orBG59HnPiz0G9nLRebWa0zXuv2siH6PLIHBvv5TH8CkQRqjBs0MlxCZu+A==", "integrity": "sha512-8y7xqG/hpB53l25CIoit9/ngxdfoG+fx+V3SHBrinnhOtLvKHRyAJJuHzkWrR4YXXLX8eXBsejgAAxHUOdW1yw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@@ -150,9 +150,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/@codemirror/view": { "node_modules/@codemirror/view": {
"version": "6.39.8", "version": "6.39.11",
"resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.39.8.tgz", "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.39.11.tgz",
"integrity": "sha512-1rASYd9Z/mE3tkbC9wInRlCNyCkSn+nLsiQKZhEDUUJiUfs/5FHDpCUDaQpoTIaNGeDc6/bhaEAyLmeEucEFPw==", "integrity": "sha512-bWdeR8gWM87l4DB/kYSF9A+dVackzDb/V56Tq7QVrQ7rn86W0rgZFtlL3g3pem6AeGcb9NQNoy3ao4WpW4h5tQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@@ -1764,9 +1764,9 @@
} }
}, },
"node_modules/katex": { "node_modules/katex": {
"version": "0.16.27", "version": "0.16.28",
"resolved": "https://registry.npmjs.org/katex/-/katex-0.16.27.tgz", "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.28.tgz",
"integrity": "sha512-aeQoDkuRWSqQN6nSvVCEFvfXdqo1OQiCmmW1kc9xSdjutPv7BGO7pqY9sQRJpMOGrEdfDgF2TfRXe5eUAD2Waw==", "integrity": "sha512-YHzO7721WbmAL6Ov1uzN/l5mY5WWWhJBSW+jq4tkfZfsxmo1hu6frS0EOswvjBUnWE6NtjEs48SFn5CQESRLZg==",
"dev": true, "dev": true,
"funding": [ "funding": [
"https://opencollective.com/katex", "https://opencollective.com/katex",
@@ -2464,9 +2464,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/vite": { "node_modules/vite": {
"version": "7.3.0", "version": "7.3.1",
"resolved": "https://registry.npmjs.org/vite/-/vite-7.3.0.tgz", "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz",
"integrity": "sha512-dZwN5L1VlUBewiP6H9s2+B3e3Jg96D0vzN+Ry73sOefebhYr9f94wwkMNN/9ouoU8pV1BqA1d1zGk8928cx0rg==", "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {

View File

@@ -15,9 +15,9 @@
"@codemirror/commands": "^6.10.1", "@codemirror/commands": "^6.10.1",
"@codemirror/lang-javascript": "^6.2.4", "@codemirror/lang-javascript": "^6.2.4",
"@codemirror/language": "^6.12.1", "@codemirror/language": "^6.12.1",
"@codemirror/state": "^6.5.3", "@codemirror/state": "^6.5.4",
"@codemirror/text": "^0.19.6", "@codemirror/text": "^0.19.6",
"@codemirror/view": "^6.39.8", "@codemirror/view": "^6.39.11",
"@tailwindcss/forms": "^0.5.11", "@tailwindcss/forms": "^0.5.11",
"@tailwindcss/typography": "^0.5.19", "@tailwindcss/typography": "^0.5.19",
"@tailwindcss/vite": "^4.1.18", "@tailwindcss/vite": "^4.1.18",
@@ -25,11 +25,11 @@
"github-markdown-css": "^5.8.1", "github-markdown-css": "^5.8.1",
"highlight.js": "^11.11.1", "highlight.js": "^11.11.1",
"jdenticon": "^3.3.0", "jdenticon": "^3.3.0",
"katex": "^0.16.27", "katex": "^0.16.28",
"marked": "^17.0.1", "marked": "^17.0.1",
"nodemon": "^3.1.11", "nodemon": "^3.1.11",
"pdfobject": "^2.3.1", "pdfobject": "^2.3.1",
"tailwindcss": "^4.1.18", "tailwindcss": "^4.1.18",
"vite": "^7.3.0" "vite": "^7.3.1"
} }
} }

View File

@@ -1,5 +1,4 @@
import '../ts/ipynb.ts'; import '../ts/ipynb.ts';
import PDFObject from 'pdfobject';
document.querySelectorAll<HTMLElement>('.table-code').forEach((el) => { document.querySelectorAll<HTMLElement>('.table-code').forEach((el) => {
el.addEventListener('click', event => { el.addEventListener('click', event => {
@@ -77,7 +76,3 @@ if (document.getElementById('gist').dataset.own) {
el.disabled = true; el.disabled = true;
}); });
} }
document.querySelectorAll(".pdf").forEach((el) => {
PDFObject.embed(el.dataset.src || "", el);
})

View File

@@ -2,9 +2,14 @@ import '../css/tailwind.css';
import '../img/favicon-32.png'; import '../img/favicon-32.png';
import '../img/opengist.svg'; import '../img/opengist.svg';
import jdenticon from 'jdenticon/standalone'; import jdenticon from 'jdenticon/standalone';
import PDFObject from 'pdfobject';
jdenticon.update("[data-jdenticon-value]") jdenticon.update("[data-jdenticon-value]")
document.querySelectorAll(".pdf").forEach((el) => {
PDFObject.embed(el.dataset.src || "", el);
})
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
document.getElementById('user-btn')?.addEventListener("click" , () => { document.getElementById('user-btn')?.addEventListener("click" , () => {
document.getElementById('user-menu')!.classList.toggle('hidden'); document.getElementById('user-menu')!.classList.toggle('hidden');

View File

@@ -15,6 +15,8 @@
{{ else }} text-gray-600 dark:text-gray-400 hover:text-gray-400 dark:hover:text-slate-300 px-3 py-2 font-medium text-sm rounded-md {{ end }}" aria-current="page">{{ .locale.Tr "settings.header.mfa" }}</a> {{ else }} text-gray-600 dark:text-gray-400 hover:text-gray-400 dark:hover:text-slate-300 px-3 py-2 font-medium text-sm rounded-md {{ end }}" aria-current="page">{{ .locale.Tr "settings.header.mfa" }}</a>
<a href="{{ $.c.ExternalUrl }}/settings/ssh" class="{{ if eq .settingsHeaderPage "ssh" }}bg-gray-100 dark:bg-gray-700 text-slate-700 dark:text-slate-300 px-3 py-2 font-medium text-sm rounded-md <a href="{{ $.c.ExternalUrl }}/settings/ssh" class="{{ if eq .settingsHeaderPage "ssh" }}bg-gray-100 dark:bg-gray-700 text-slate-700 dark:text-slate-300 px-3 py-2 font-medium text-sm rounded-md
{{ else }} text-gray-600 dark:text-gray-400 hover:text-gray-400 dark:hover:text-slate-300 px-3 py-2 font-medium text-sm rounded-md {{ end }}" aria-current="page">{{ .locale.Tr "settings.header.ssh" }}</a> {{ else }} text-gray-600 dark:text-gray-400 hover:text-gray-400 dark:hover:text-slate-300 px-3 py-2 font-medium text-sm rounded-md {{ end }}" aria-current="page">{{ .locale.Tr "settings.header.ssh" }}</a>
<a href="{{ $.c.ExternalUrl }}/settings/access-tokens" class="{{ if eq .settingsHeaderPage "tokens" }}bg-gray-100 dark:bg-gray-700 text-slate-700 dark:text-slate-300 px-3 py-2 font-medium text-sm rounded-md
{{ else }} text-gray-600 dark:text-gray-400 hover:text-gray-400 dark:hover:text-slate-300 px-3 py-2 font-medium text-sm rounded-md {{ end }}" aria-current="page">{{ .locale.Tr "settings.header.tokens" }}</a>
<a href="{{ $.c.ExternalUrl }}/settings/style" class="{{ if eq .settingsHeaderPage "style" }}bg-gray-100 dark:bg-gray-700 text-slate-700 dark:text-slate-300 px-3 py-2 font-medium text-sm rounded-md <a href="{{ $.c.ExternalUrl }}/settings/style" class="{{ if eq .settingsHeaderPage "style" }}bg-gray-100 dark:bg-gray-700 text-slate-700 dark:text-slate-300 px-3 py-2 font-medium text-sm rounded-md
{{ else }} text-gray-600 dark:text-gray-400 hover:text-gray-400 dark:hover:text-slate-300 px-3 py-2 font-medium text-sm rounded-md {{ end }}" aria-current="page">{{ .locale.Tr "settings.header.style" }}</a> {{ else }} text-gray-600 dark:text-gray-400 hover:text-gray-400 dark:hover:text-slate-300 px-3 py-2 font-medium text-sm rounded-md {{ end }}" aria-current="page">{{ .locale.Tr "settings.header.style" }}</a>
</nav> </nav>

95
templates/pages/settings_tokens.html vendored Normal file
View File

@@ -0,0 +1,95 @@
{{ template "header" .}}
{{ template "settings_header" .}}
<div class="relative mx-auto max-w-160 space-y-8">
<div class="sm:grid grid-cols-2 gap-x-4 md:gap-x-8">
<div class="w-full">
<div class="bg-white dark:bg-gray-900 rounded-md border border-1 border-gray-200 dark:border-gray-700 py-8 px-4 shadow sm:rounded-lg sm:px-10">
<h2 class="text-md font-bold text-slate-700 dark:text-slate-300">
{{ .locale.Tr "settings.create-token" }}
</h2>
<h3 class="text-sm text-gray-600 dark:text-gray-400 italic mb-4">
{{ .locale.Tr "settings.create-token-help" }}
</h3>
<form class="space-y-6" action="{{ $.c.ExternalUrl }}/settings/access-tokens" method="post">
<div>
<label for="name" class="block text-sm font-medium text-slate-700 dark:text-slate-300"> {{ .locale.Tr "settings.token-name" }} </label>
<div class="mt-1">
<input id="name" name="name" type="text" required autocomplete="off" class="dark:bg-gray-800 appearance-none block w-full px-3 py-2 border border-gray-200 dark:border-gray-700 rounded-md shadow-sm placeholder-gray-600 dark:placeholder-gray-400 focus:outline-none focus:ring-primary-500 focus:border-primary-500 sm:text-sm">
</div>
</div>
<div class="mt-4">
<label class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2"> {{ .locale.Tr "settings.token-permissions" }} </label>
<div class="space-y-2">
<label class="block text-sm text-slate-600 dark:text-slate-400">{{ .locale.Tr "settings.token-gist-permission" }}</label>
<select name="scope_gist" class="dark:bg-gray-800 block w-full px-3 py-2 border border-gray-200 dark:border-gray-700 rounded-md shadow-sm focus:outline-none focus:ring-primary-500 focus:border-primary-500 sm:text-sm">
<option value="0">{{ .locale.Tr "settings.token-permission-none" }}</option>
<option value="1">{{ .locale.Tr "settings.token-permission-read" }}</option>
<option value="2">{{ .locale.Tr "settings.token-permission-read-write" }}</option>
</select>
</div>
</div>
<div class="mt-4">
<label for="expires_at" class="block text-sm font-medium text-slate-700 dark:text-slate-300"> {{ .locale.Tr "settings.token-expiration" }} </label>
<h3 class="text-sm text-gray-600 dark:text-gray-400 italic mb-2">
{{ .locale.Tr "settings.token-expiration-help" }}
</h3>
<div class="mt-1">
<input id="expires_at" name="expires_at" type="date" class="dark:bg-gray-800 appearance-none block w-full px-3 py-2 border border-gray-200 dark:border-gray-700 rounded-md shadow-sm placeholder-gray-600 dark:placeholder-gray-400 focus:outline-none focus:ring-primary-500 focus:border-primary-500 sm:text-sm">
</div>
</div>
<button type="submit" class="inline-flex items-center px-4 py-2 border border-transparent border-gray-200 dark:border-gray-700 text-sm font-medium rounded-md shadow-sm text-white dark:text-white bg-primary-500 hover:bg-primary-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500">{{ .locale.Tr "settings.create-token" }}</button>
{{ .csrfHtml }}
</form>
</div>
</div>
<div>
<div class="mt-6 flow-root">
<ul role="list" class="-my-5 divide-y divide-gray-300 dark:divide-gray-700 list-none">
{{ if .accessTokens }}
{{ range $token := .accessTokens }}
<li class="py-5">
<div class="inline-flex">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-12 h-12 mr-4">
<path stroke-linecap="round" stroke-linejoin="round" d="M7.864 4.243A7.5 7.5 0 0 1 19.5 10.5c0 2.92-.556 5.709-1.568 8.268M5.742 6.364A7.465 7.465 0 0 0 4.5 10.5a7.464 7.464 0 0 1-1.15 3.993m1.989 3.559A11.209 11.209 0 0 0 8.25 10.5a3.75 3.75 0 1 1 7.5 0c0 .527-.021 1.049-.064 1.565M12 10.5a14.94 14.94 0 0 1-3.386 9.522m6.772-6.521a6.02 6.02 0 0 1-2.122 4.256" />
</svg>
<div>
<h3 class="text-sm font-semibold text-slate-700 dark:text-slate-300">{{ .Name }}</h3>
<p class="text-xs text-gray-500">
{{ $.locale.Tr "settings.token-gist-permission" }}:
{{ if eq .ScopeGist 0 }}{{ $.locale.Tr "settings.token-permission-none" }}{{ end }}
{{ if eq .ScopeGist 1 }}{{ $.locale.Tr "settings.token-permission-read" }}{{ end }}
{{ if eq .ScopeGist 2 }}{{ $.locale.Tr "settings.token-permission-read-write" }}{{ end }}
</p>
<p class="text-xs text-gray-500 line-clamp-2">{{ $.locale.Tr "settings.token-created-at" }} <span>{{ .CreatedAt | humanDate }}</span></p>
{{ if eq .ExpiresAt 0 }}
<p class="text-xs text-gray-500 line-clamp-2">{{ $.locale.Tr "settings.token-no-expiration" }}</p>
{{ else }}
<p class="text-xs {{ if .IsExpired }}text-rose-500{{ else }}text-gray-500{{ end }} line-clamp-2">{{ $.locale.Tr "settings.token-expires-at" }} <span>{{ .ExpiresAt | humanDateOnly }}</span>{{ if .IsExpired }} ({{ $.locale.Tr "settings.token-expired" }}){{ end }}</p>
{{ end }}
{{ if eq .LastUsedAt 0 }}
<p class="text-xs text-gray-500 line-clamp-2">{{ $.locale.Tr "settings.token-never-used" }}</p>
{{ else }}
<p class="text-xs text-gray-500 line-clamp-2">{{ $.locale.Tr "settings.token-last-used" }} <span>{{ .LastUsedAt | humanTimeDiff }}</span></p>
{{ end }}
</div>
<form action="{{ $.c.ExternalUrl }}/settings/access-tokens/{{.ID}}" method="post" class="inline-block">
<input type="hidden" name="_method" value="DELETE">
{{ $.csrfHtml }}
<button type="submit" onclick="return confirm('{{ $.locale.Tr "settings.delete-token-confirm" }}')" class="align-middle items-center leading-2 ml-2 px-3 py-1 border border-transparent border-gray-200 dark:border-gray-700 text-xs font-medium rounded-md shadow-sm text-white dark:text-white bg-rose-600 hover:bg-rose-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-rose-500">{{ $.locale.Tr "settings.delete-token" }}</button>
</form>
</div>
</li>
{{ end }}
{{ end }}
</ul>
</div>
</div>
</div>
</div>
{{ template "settings_footer" .}}
{{ template "footer" .}}

View File

@@ -60,7 +60,33 @@
<div class="rounded-md border border-1 border-gray-200 dark:border-gray-700 overflow-auto hover:border-primary-600"> <div class="rounded-md border border-1 border-gray-200 dark:border-gray-700 overflow-auto hover:border-primary-600">
<div class="code overflow-auto"> <div class="code overflow-auto">
{{ if .gist.PreviewFilename }} {{ if .gist.PreviewFilename }}
{{ if .gist.Preview }} {{ if .gist.PreviewMimeType }}
{{ if .gist.PreviewMimeType.IsSVG }}
<div class="p-8 flex justify-center">
<img src="{{ .c.ExternalUrl }}/{{ .gist.User.Username }}/{{ .gist.Identifier }}/raw/HEAD/{{ .gist.PreviewFilename }}" alt="{{ .gist.PreviewFilename }}" class="max-h-80 object-contain">
</div>
{{ else if .gist.PreviewMimeType.IsImage }}
<div class="p-8 flex justify-center">
<img src="{{ .c.ExternalUrl }}/{{ .gist.User.Username }}/{{ .gist.Identifier }}/raw/HEAD/{{ .gist.PreviewFilename }}" alt="{{ .gist.PreviewFilename }}" class="max-h-80 object-contain">
</div>
{{ else if .gist.PreviewMimeType.IsAudio }}
<div class="p-8 flex justify-center">
<audio controls class="w-full max-w-lg">
<source src="{{ .c.ExternalUrl }}/{{ .gist.User.Username }}/{{ .gist.Identifier }}/raw/HEAD/{{ .gist.PreviewFilename }}" type="{{ .gist.PreviewMimeType.ContentType }}">
</audio>
</div>
{{ else if .gist.PreviewMimeType.IsVideo }}
<div class="p-8 flex justify-center">
<video controls class="max-h-80 max-w-full">
<source src="{{ .c.ExternalUrl }}/{{ .gist.User.Username }}/{{ .gist.Identifier }}/raw/HEAD/{{ .gist.PreviewFilename }}" type="{{ .gist.PreviewMimeType.ContentType }}">
</video>
</div>
{{ else if .gist.PreviewMimeType.IsPDF }}
<div class="pdf max-h-80" data-src="{{ .c.ExternalUrl }}/{{ .gist.User.Username }}/{{ .gist.Identifier }}/raw/HEAD/{{ .gist.PreviewFilename }}"></div>
{{ else }}
<div class="pl-4 py-0.5 text-xs"><p>{{ .locale.Tr "gist.preview-non-available" }}</p></div>
{{ end }}
{{ else if .gist.Preview }}
{{ if isMarkdown .gist.PreviewFilename }} {{ if isMarkdown .gist.PreviewFilename }}
<div class="chroma preview markdown markdown-body p-8">{{ .gist.HTML | safe }}</div> <div class="chroma preview markdown markdown-body p-8">{{ .gist.HTML | safe }}</div>
{{ else }} {{ else }}
@@ -80,8 +106,8 @@
</table> </table>
{{ end }} {{ end }}
{{ else }} {{ else }}
<div class="pl-4 py-0.5 text-xs"><p>{{ .locale.Tr "gist.preview-non-available" }}</p></div> <div class="pl-4 py-0.5 text-xs"><p>{{ .locale.Tr "gist.preview-non-available" }}</p></div>
{{ end }} {{ end }}
{{ else }} {{ else }}
<div class="pl-4 py-0.5 text-xs"><p>{{ .locale.Tr "gist.no-content" }}</p></div> <div class="pl-4 py-0.5 text-xs"><p>{{ .locale.Tr "gist.no-content" }}</p></div>
{{ end }} {{ end }}