Compare commits
2 Commits
gists-john
...
tcpfix
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
97bb9bd831 | ||
|
|
7a5c7c117f |
30
.github/workflows/docs.yml
vendored
30
.github/workflows/docs.yml
vendored
@@ -28,16 +28,20 @@ jobs:
|
||||
npx tailwindcss -i .vitepress/theme/style.css -o .vitepress/theme/theme.css -c .vitepress/tailwind.config.js
|
||||
npm run docs:build
|
||||
|
||||
- name: Push to docs repository
|
||||
run: |
|
||||
git clone https://${{ secrets.STATIC_REPO_TOKEN }}@github.com/${{ secrets.STATIC_REPO }}.git target-repo
|
||||
rm -rf target-repo/srv/opengist
|
||||
mkdir -p target-repo/srv/opengist
|
||||
cp -r docs/.vitepress/dist/* target-repo/srv/opengist/
|
||||
cd target-repo
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||
git add .
|
||||
git commit -m "Deploy docs from ${{ github.repository }}@${{ github.sha }}" || echo "No changes to commit"
|
||||
git pull --rebase
|
||||
git push
|
||||
- name: Deploy to server
|
||||
uses: appleboy/scp-action@master
|
||||
with:
|
||||
host: ${{ secrets.SERVER_HOST }}
|
||||
username: ${{ secrets.SERVER_USERNAME }}
|
||||
key: ${{ secrets.SERVER_SSH_KEY }}
|
||||
source: "docs/.vitepress/dist/*"
|
||||
target: ${{ secrets.SERVER_PATH }}
|
||||
|
||||
- name: Update remote docs
|
||||
uses: appleboy/ssh-action@master
|
||||
with:
|
||||
host: ${{ secrets.SERVER_HOST }}
|
||||
username: ${{ secrets.SERVER_USERNAME }}
|
||||
key: ${{ secrets.SERVER_SSH_KEY }}
|
||||
script: |
|
||||
${{ secrets.UPDATE_DOCS }}
|
||||
30
.github/workflows/helm.yml
vendored
30
.github/workflows/helm.yml
vendored
@@ -34,16 +34,20 @@ jobs:
|
||||
helm repo index --url https://helm.opengist.io --merge index.yaml .
|
||||
fi
|
||||
|
||||
- name: Push to docs repository
|
||||
run: |
|
||||
git clone https://${{ secrets.DOCS_REPO_TOKEN }}@github.com/${{ secrets.DOCS_REPO }}.git target-repo
|
||||
mkdir -p target-repo/helm
|
||||
cp helm/*.tgz target-repo/helm/
|
||||
cp helm/index.yaml target-repo/helm/
|
||||
cd target-repo
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||
git add .
|
||||
git commit -m "Deploy helm chart from ${{ github.repository }}@${{ github.sha }}" || echo "No changes to commit"
|
||||
git pull --rebase
|
||||
git push
|
||||
- name: Deploy to server
|
||||
uses: appleboy/scp-action@master
|
||||
with:
|
||||
host: ${{ secrets.SERVER_HOST }}
|
||||
username: ${{ secrets.SERVER_USERNAME }}
|
||||
key: ${{ secrets.SERVER_SSH_KEY }}
|
||||
source: "./helm/*.tgz,./helm/index.yaml"
|
||||
target: ${{ secrets.HELM_SERVER_PATH }}
|
||||
|
||||
- name: Update remote helm repository
|
||||
uses: appleboy/ssh-action@master
|
||||
with:
|
||||
host: ${{ secrets.SERVER_HOST }}
|
||||
username: ${{ secrets.SERVER_USERNAME }}
|
||||
key: ${{ secrets.SERVER_SSH_KEY }}
|
||||
script: |
|
||||
${{ secrets.UPDATE_HELM_REPO }}
|
||||
36
CHANGELOG.md
36
CHANGELOG.md
@@ -1,41 +1,5 @@
|
||||
# Changelog
|
||||
|
||||
## [1.12.1](https://github.com/thomiceli/opengist/compare/v1.12.0...v1.12.1) - 2026-02-03
|
||||
See here how to [update](https://opengist.io/docs/update) Opengist.
|
||||
|
||||
### Added
|
||||
- More translation strings (#605)
|
||||
|
||||
### Fixed
|
||||
- Allow Access Tokens with Required Login (#611)
|
||||
- Make text files renderable with mimetypes different than text/plain (#612)
|
||||
- Improve security on raw files endpoint (#613)
|
||||
|
||||
> Admins of Opengist instances may want to run "Synchronize all gists previews" in the admin panel.
|
||||
|
||||
## [1.12.0](https://github.com/thomiceli/opengist/compare/v1.11.1...v1.12.0) - 2026-01-27
|
||||
See here how to [update](https://opengist.io/docs/update) Opengist.
|
||||
|
||||
### Added
|
||||
- Access tokens (#602)
|
||||
- Fuzzy search for gist search (#555)
|
||||
- Allow Unicode letters/numbers in topics (#597)
|
||||
- Resize editor height (#600)
|
||||
- More translation strings (#516) (#604)
|
||||
|
||||
### Fixed
|
||||
- Don't panic on Go TCP errors (#601)
|
||||
|
||||
### Other
|
||||
- Reduce footprint of Docker image (#515)
|
||||
- Update Go + JS deps (#603)
|
||||
- Configure Dependabot for updates on Go and NPM (#449)
|
||||
|
||||
### [Helm Chart](helm/opengist)
|
||||
- Use existing pvc claim of provided (#547)
|
||||
- Adds StatefulSet support (#549)
|
||||
- Move Prom metrics to a dedicated port + support ServiceMonitor (#599)
|
||||
|
||||
## [1.11.1](https://github.com/thomiceli/opengist/compare/v1.11.0...v1.11.1) - 2025-09-30
|
||||
See here how to [update](https://opengist.io/docs/update) Opengist.
|
||||
|
||||
|
||||
10
Dockerfile
10
Dockerfile
@@ -8,11 +8,11 @@ RUN apk update && \
|
||||
musl-dev \
|
||||
libstdc++
|
||||
|
||||
COPY --from=golang:1.25.6-alpine3.22 /usr/local/go/ /usr/local/go/
|
||||
COPY --from=golang:1.25-alpine3.22 /usr/local/go/ /usr/local/go/
|
||||
ENV PATH="/usr/local/go/bin:${PATH}"
|
||||
ENV CGO_ENABLED=0
|
||||
|
||||
COPY --from=node:24.13.0-alpine3.22 /usr/local/ /usr/local/
|
||||
COPY --from=node:24.9.0-alpine3.22 /usr/local/ /usr/local/
|
||||
ENV NODE_PATH="/usr/local/lib/node_modules"
|
||||
ENV PATH="/usr/local/bin:${PATH}"
|
||||
|
||||
@@ -31,7 +31,7 @@ RUN apk add --no-cache \
|
||||
gnupg \
|
||||
xz
|
||||
|
||||
EXPOSE 6157 6158 2222 16157
|
||||
EXPOSE 6157 2222 16157
|
||||
|
||||
RUN git config --global --add safe.directory /opengist
|
||||
RUN make install
|
||||
@@ -46,7 +46,7 @@ FROM base AS build
|
||||
RUN make
|
||||
|
||||
|
||||
FROM alpine:3.22 AS prod
|
||||
FROM alpine:3.22 as prod
|
||||
|
||||
RUN apk update && \
|
||||
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/docker ./docker
|
||||
|
||||
EXPOSE 6157 6158 2222
|
||||
EXPOSE 6157 2222
|
||||
VOLUME /opengist
|
||||
HEALTHCHECK --interval=60s --timeout=30s --start-period=15s --retries=3 CMD curl -f http://localhost:6157/healthcheck || exit 1
|
||||
ENTRYPOINT ["./docker/entrypoint.sh"]
|
||||
|
||||
@@ -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 :
|
||||
|
||||
```shell
|
||||
docker pull ghcr.io/thomiceli/opengist:1.12
|
||||
docker pull ghcr.io/thomiceli/opengist:1.11
|
||||
```
|
||||
|
||||
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
|
||||
services:
|
||||
opengist:
|
||||
image: ghcr.io/thomiceli/opengist:1.12
|
||||
image: ghcr.io/thomiceli/opengist:1.11
|
||||
container_name: opengist
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
@@ -77,9 +77,9 @@ Download the archive for your system from the release page [here](https://github
|
||||
|
||||
```shell
|
||||
# example for linux amd64
|
||||
wget https://github.com/thomiceli/opengist/releases/download/v1.12.1/opengist1.12.1-linux-amd64.tar.gz
|
||||
wget https://github.com/thomiceli/opengist/releases/download/v1.11.1/opengist1.11.1-linux-amd64.tar.gz
|
||||
|
||||
tar xzvf opengist1.12.1-linux-amd64.tar.gz
|
||||
tar xzvf opengist1.11.1-linux-amd64.tar.gz
|
||||
cd opengist
|
||||
chmod +x opengist
|
||||
./opengist # with or without `--config config.yml`
|
||||
|
||||
@@ -55,15 +55,9 @@ http.git-enabled: true
|
||||
# File permissions for Unix socket (octal format). Default: 0666
|
||||
unix-socket-permissions: 0666
|
||||
|
||||
# Enable or disable the Prometheus metrics server (either `true` or `false`). Default: false
|
||||
# Enable or disable the metrics endpoint (either `true` or `false`). Default: false
|
||||
metrics.enabled: false
|
||||
|
||||
# The host on which the metrics server should bind. Default: 0.0.0.0
|
||||
metrics.host: 0.0.0.0
|
||||
|
||||
# The port on which the metrics server should listen. Default: 6158
|
||||
metrics.port: 6158
|
||||
|
||||
# SSH built-in server configuration
|
||||
# Note: it is not using the SSH daemon from your machine (yet)
|
||||
|
||||
|
||||
@@ -55,7 +55,6 @@ export default defineConfig({
|
||||
text: 'Usage', base: '/docs/usage', items: [
|
||||
{text: 'Init via Git', link: '/init-via-git'},
|
||||
{text: 'Embed Gist', link: '/embed'},
|
||||
{text: 'Access Tokens', link: '/access-tokens'},
|
||||
{text: 'Gist as JSON', link: '/gist-json'},
|
||||
{text: 'Import Gists from Github', link: '/import-from-github-gist'},
|
||||
{text: 'Git push options', link: '/git-push-options'},
|
||||
|
||||
@@ -19,7 +19,7 @@ export default {
|
||||
<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="" >
|
||||
<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.12</span>
|
||||
<span class="pr-1">Released 1.11</span>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-4">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="m4.5 19.5 15-15m0 0H8.25m11.25 0v11.25" />
|
||||
</svg>
|
||||
|
||||
@@ -21,9 +21,7 @@ aside: false
|
||||
| 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`) |
|
||||
| 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 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. |
|
||||
| metrics.enabled | OG_METRICS_ENABLED | `false` | Enable or disable Prometheus metrics endpoint at `/metrics` (`true` or `false`) |
|
||||
| ssh.git-enabled | OG_SSH_GIT_ENABLED | `true` | Enable or disable git operations (clone, pull, push) via SSH. (`true` or `false`) |
|
||||
| ssh.host | OG_SSH_HOST | `0.0.0.0` | The host on which the SSH server should bind. |
|
||||
| ssh.port | OG_SSH_PORT | `2222` | The port on which the SSH server should listen. |
|
||||
|
||||
@@ -4,10 +4,10 @@ Opengist offers built-in support for Prometheus metrics to help you monitor the
|
||||
|
||||
## Enabling metrics
|
||||
|
||||
By default, the metrics server is disabled for security and performance reasons. To enable it, update your configuration as stated in the [configuration cheat sheet](cheat-sheet.md):
|
||||
By default, the metrics endpoint is disabled for security and performance reasons. To enable it, update your configuration as stated in the [configuration cheat sheet](cheat-sheet.md):
|
||||
|
||||
```yaml
|
||||
metrics.enabled: true
|
||||
metrics.enabled = true
|
||||
```
|
||||
|
||||
Alternatively, you can use the environment variable:
|
||||
@@ -16,25 +16,7 @@ Alternatively, you can use the environment variable:
|
||||
OG_METRICS_ENABLED=true
|
||||
```
|
||||
|
||||
Once enabled, metrics are available on a separate server at `http://0.0.0.0:6158/metrics` by default.
|
||||
|
||||
## Configuration
|
||||
|
||||
The metrics server runs on a separate port from the main application. By default, it binds to `0.0.0.0` (all interfaces) on port `6158`.
|
||||
|
||||
| Config Key | Environment Variable | Default | Description |
|
||||
|----------------|---------------------|-------------|------------------------------------------------|
|
||||
| metrics.enabled | OG_METRICS_ENABLED | `false` | Enable or disable the metrics server |
|
||||
| metrics.host | OG_METRICS_HOST | `0.0.0.0` | The host on which the metrics server binds |
|
||||
| metrics.port | OG_METRICS_PORT | `6158` | The port on which the metrics server listens |
|
||||
|
||||
Example configuration:
|
||||
|
||||
```yaml
|
||||
metrics.enabled: true
|
||||
metrics.host: 0.0.0.0
|
||||
metrics.port: 6158
|
||||
```
|
||||
Once enabled, metrics are available at the /metrics endpoint.
|
||||
|
||||
## Available metrics
|
||||
|
||||
@@ -54,6 +36,14 @@ These standard metrics follow the Prometheus naming convention and include label
|
||||
|
||||
## Security Considerations
|
||||
|
||||
The metrics server binds to `0.0.0.0` by default, making it accessible on all network interfaces. This default works well for containerized deployments (Docker, Kubernetes) where network isolation is handled at the infrastructure level.
|
||||
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.
|
||||
|
||||
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.
|
||||
Example with Nginx:
|
||||
|
||||
```shell
|
||||
location /metrics {
|
||||
auth_basic "Metrics";
|
||||
auth_basic_user_file /etc/nginx/.htpasswd;
|
||||
proxy_pass http://localhost:6157/metrics;
|
||||
}
|
||||
```
|
||||
|
||||
@@ -25,8 +25,8 @@ Opengist is now running on port 6157, you can browse http://localhost:6157
|
||||
|
||||
Requirements:
|
||||
* [Git](https://git-scm.com/downloads) (2.28+)
|
||||
* [Go](https://go.dev/doc/install) (1.25+)
|
||||
* [Node.js](https://nodejs.org/en/download/) (20+)
|
||||
* [Go](https://go.dev/doc/install) (1.23+)
|
||||
* [Node.js](https://nodejs.org/en/download/) (16+)
|
||||
* [Make](https://linux.die.net/man/1/make) (optional, but easier)
|
||||
|
||||
```shell
|
||||
|
||||
@@ -4,9 +4,9 @@ Download the archive for your system from the release page [here](https://github
|
||||
|
||||
```shell
|
||||
# example for linux amd64
|
||||
wget https://github.com/thomiceli/opengist/releases/download/v1.12.1/opengist1.12.1-linux-amd64.tar.gz
|
||||
wget https://github.com/thomiceli/opengist/releases/download/v1.11.1/opengist1.11.1-linux-amd64.tar.gz
|
||||
|
||||
tar xzvf opengist1.12.1-linux-amd64.tar.gz
|
||||
tar xzvf opengist1.11.1-linux-amd64.tar.gz
|
||||
cd opengist
|
||||
chmod +x opengist
|
||||
./opengist # with or without `--config config.yml`
|
||||
|
||||
@@ -2,15 +2,15 @@
|
||||
|
||||
Requirements:
|
||||
* [Git](https://git-scm.com/downloads) (2.28+)
|
||||
* [Go](https://go.dev/doc/install) (1.25+)
|
||||
* [Node.js](https://nodejs.org/en/download/) (20+)
|
||||
* [Go](https://go.dev/doc/install) (1.23+)
|
||||
* [Node.js](https://nodejs.org/en/download/) (16+)
|
||||
* [Make](https://linux.die.net/man/1/make) (optional, but easier)
|
||||
|
||||
```shell
|
||||
git clone https://github.com/thomiceli/opengist
|
||||
cd opengist
|
||||
|
||||
git checkout v1.12.1 # optional, to checkout the latest release
|
||||
git checkout v1.11.1 # optional, to checkout the latest release
|
||||
|
||||
make
|
||||
./opengist
|
||||
|
||||
@@ -27,9 +27,9 @@ Stop the running instance; then like your first installation of Opengist, downlo
|
||||
|
||||
```shell
|
||||
# example for linux amd64
|
||||
wget https://github.com/thomiceli/opengist/releases/download/v1.12.1/opengist1.12.1-linux-amd64.tar.gz
|
||||
wget https://github.com/thomiceli/opengist/releases/download/v1.11.1/opengist1.11.1-linux-amd64.tar.gz
|
||||
|
||||
tar xzvf opengist1.12.1-linux-amd64.tar.gz
|
||||
tar xzvf opengist1.11.1-linux-amd64.tar.gz
|
||||
cd opengist
|
||||
chmod +x opengist
|
||||
./opengist # with or without `--config config.yml`
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
# 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
56
go.mod
@@ -1,10 +1,10 @@
|
||||
module github.com/thomiceli/opengist
|
||||
|
||||
go 1.25.5
|
||||
go 1.25.1
|
||||
|
||||
require (
|
||||
github.com/Kunde21/markdownfmt/v3 v3.1.0
|
||||
github.com/alecthomas/chroma/v2 v2.23.1
|
||||
github.com/alecthomas/chroma/v2 v2.21.1
|
||||
github.com/blevesearch/bleve/v2 v2.5.7
|
||||
github.com/dustin/go-humanize v1.0.1
|
||||
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/v4 v4.15.0
|
||||
github.com/markbates/goth v1.82.0
|
||||
github.com/meilisearch/meilisearch-go v0.36.0
|
||||
github.com/meilisearch/meilisearch-go v0.35.1
|
||||
github.com/pquerna/otp v1.5.0
|
||||
github.com/prometheus/client_golang v1.23.2
|
||||
github.com/rs/zerolog v1.34.0
|
||||
github.com/stretchr/testify v1.11.1
|
||||
github.com/urfave/cli/v2 v2.27.7
|
||||
github.com/yuin/goldmark v1.7.16
|
||||
github.com/yuin/goldmark v1.7.15
|
||||
github.com/yuin/goldmark-emoji v1.0.6
|
||||
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc
|
||||
go.abhg.dev/goldmark/mermaid v0.6.0
|
||||
golang.org/x/crypto v0.47.0
|
||||
golang.org/x/text v0.33.0
|
||||
golang.org/x/crypto v0.46.0
|
||||
golang.org/x/text v0.32.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
gorm.io/driver/mysql v1.6.0
|
||||
gorm.io/driver/postgres v1.6.0
|
||||
@@ -39,32 +39,31 @@ require (
|
||||
|
||||
require (
|
||||
filippo.io/edwards25519 v1.1.0 // indirect
|
||||
github.com/Azure/go-ntlmssp v0.1.0 // indirect
|
||||
github.com/RoaringBitmap/roaring/v2 v2.14.4 // indirect
|
||||
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 // indirect
|
||||
github.com/RoaringBitmap/roaring/v2 v2.10.0 // indirect
|
||||
github.com/andybalholm/brotli v1.2.0 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/bits-and-blooms/bitset v1.24.4 // indirect
|
||||
github.com/blevesearch/bleve_index_api v1.3.1 // indirect
|
||||
github.com/bits-and-blooms/bitset v1.24.0 // indirect
|
||||
github.com/blevesearch/bleve_index_api v1.2.11 // indirect
|
||||
github.com/blevesearch/geo v0.2.4 // indirect
|
||||
github.com/blevesearch/go-faiss v1.0.27 // indirect
|
||||
github.com/blevesearch/go-faiss v1.0.26 // indirect
|
||||
github.com/blevesearch/go-porterstemmer v1.0.3 // indirect
|
||||
github.com/blevesearch/gtreap v0.1.1 // indirect
|
||||
github.com/blevesearch/mmap-go v1.2.0 // indirect
|
||||
github.com/blevesearch/scorch_segment_api/v2 v2.4.1 // indirect
|
||||
github.com/blevesearch/mmap-go v1.0.4 // indirect
|
||||
github.com/blevesearch/scorch_segment_api/v2 v2.3.13 // indirect
|
||||
github.com/blevesearch/segment v0.9.1 // indirect
|
||||
github.com/blevesearch/snowballstem v0.9.0 // indirect
|
||||
github.com/blevesearch/upsidedown_store_api v1.0.2 // indirect
|
||||
github.com/blevesearch/vellum v1.2.0 // indirect
|
||||
github.com/blevesearch/vellum v1.1.0 // indirect
|
||||
github.com/blevesearch/zapx/v11 v11.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/v14 v14.4.2 // indirect
|
||||
github.com/blevesearch/zapx/v15 v15.4.2 // indirect
|
||||
github.com/blevesearch/zapx/v16 v16.3.0 // indirect
|
||||
github.com/blevesearch/zapx/v16 v16.2.8 // indirect
|
||||
github.com/boombuler/barcode v1.1.0 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/clipperhouse/stringish v0.1.1 // indirect
|
||||
github.com/clipperhouse/uax29/v2 v2.4.0 // indirect
|
||||
github.com/clipperhouse/uax29/v2 v2.2.0 // indirect
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/dlclark/regexp2 v1.11.5 // indirect
|
||||
@@ -77,13 +76,14 @@ require (
|
||||
github.com/go-sql-driver/mysql v1.9.3 // indirect
|
||||
github.com/go-viper/mapstructure/v2 v2.4.0 // 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/snappy v1.0.0 // indirect
|
||||
github.com/google/go-tpm v0.9.6 // indirect
|
||||
github.com/gorilla/mux v1.8.1 // indirect
|
||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
||||
github.com/jackc/pgx/v5 v5.8.0 // indirect
|
||||
github.com/jackc/pgx/v5 v5.7.6 // indirect
|
||||
github.com/jackc/puddle/v2 v2.2.2 // indirect
|
||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||
github.com/jinzhu/now v1.1.5 // indirect
|
||||
@@ -98,11 +98,11 @@ require (
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/mschoch/smat v0.2.0 // indirect
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||
github.com/ncruces/go-strftime v1.0.0 // indirect
|
||||
github.com/ncruces/go-strftime v0.1.9 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/prometheus/client_model v0.6.2 // indirect
|
||||
github.com/prometheus/common v0.67.5 // indirect
|
||||
github.com/prometheus/procfs v0.19.2 // indirect
|
||||
github.com/prometheus/common v0.66.1 // indirect
|
||||
github.com/prometheus/procfs v0.17.0 // indirect
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||
github.com/russross/blackfriday/v2 v2.1.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
|
||||
go.etcd.io/bbolt v1.4.3 // indirect
|
||||
go.yaml.in/yaml/v2 v2.4.3 // indirect
|
||||
golang.org/x/exp v0.0.0-20260112195511-716be5621a96 // indirect
|
||||
golang.org/x/net v0.49.0 // indirect
|
||||
golang.org/x/oauth2 v0.34.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20251002181428-27f1f14c8bb9 // indirect
|
||||
golang.org/x/net v0.48.0 // indirect
|
||||
golang.org/x/oauth2 v0.31.0 // indirect
|
||||
golang.org/x/sync v0.19.0 // indirect
|
||||
golang.org/x/sys v0.40.0 // indirect
|
||||
golang.org/x/sys v0.39.0 // indirect
|
||||
golang.org/x/time v0.14.0 // indirect
|
||||
google.golang.org/protobuf v1.36.11 // indirect
|
||||
modernc.org/libc v1.67.7 // indirect
|
||||
google.golang.org/protobuf v1.36.10 // indirect
|
||||
modernc.org/libc v1.66.10 // indirect
|
||||
modernc.org/mathutil v1.7.1 // indirect
|
||||
modernc.org/memory v1.11.0 // indirect
|
||||
modernc.org/sqlite v1.44.3 // indirect
|
||||
modernc.org/sqlite v1.39.0 // indirect
|
||||
)
|
||||
|
||||
135
go.sum
135
go.sum
@@ -1,16 +1,16 @@
|
||||
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
||||
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
||||
github.com/Azure/go-ntlmssp v0.1.0 h1:DjFo6YtWzNqNvQdrwEyr/e4nhU3vRiwenz5QX7sFz+A=
|
||||
github.com/Azure/go-ntlmssp v0.1.0/go.mod h1:NYqdhxd/8aAct/s4qSYZEerdPuH1liG2/X9DiVTbhpk=
|
||||
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 h1:mFRzDkZVAjdal+s7s0MwaRv9igoPqLRdzOLzw/8Xvq8=
|
||||
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU=
|
||||
github.com/Kunde21/markdownfmt/v3 v3.1.0 h1:KiZu9LKs+wFFBQKhrZJrFZwtLnCCWJahL+S+E/3VnM0=
|
||||
github.com/Kunde21/markdownfmt/v3 v3.1.0/go.mod h1:tPXN1RTyOzJwhfHoon9wUr4HGYmWgVxSQN6VBJDkrVc=
|
||||
github.com/RoaringBitmap/roaring/v2 v2.14.4 h1:4aKySrrg9G/5oRtJ3TrZLObVqxgQ9f1znCRBwEwjuVw=
|
||||
github.com/RoaringBitmap/roaring/v2 v2.14.4/go.mod h1:oMvV6omPWr+2ifRdeZvVJyaz+aoEUopyv5iH0u/+wbY=
|
||||
github.com/RoaringBitmap/roaring/v2 v2.10.0 h1:HbJ8Cs71lfCJyvmSptxeMX2PtvOC8yonlU0GQcy2Ak0=
|
||||
github.com/RoaringBitmap/roaring/v2 v2.10.0/go.mod h1:FiJcsfkGje/nZBZgCu0ZxCPOKD/hVXDS2dXi7/eUFE0=
|
||||
github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0=
|
||||
github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
|
||||
github.com/alecthomas/chroma/v2 v2.2.0/go.mod h1:vf4zrexSH54oEjJ7EdB65tGNHmH3pGZmVkgTP5RHvAs=
|
||||
github.com/alecthomas/chroma/v2 v2.23.1 h1:nv2AVZdTyClGbVQkIzlDm/rnhk1E9bU9nXwmZ/Vk/iY=
|
||||
github.com/alecthomas/chroma/v2 v2.23.1/go.mod h1:NqVhfBR0lte5Ouh3DcthuUCTUpDC9cxBOfyMbMQPs3o=
|
||||
github.com/alecthomas/chroma/v2 v2.21.1 h1:FaSDrp6N+3pphkNKU6HPCiYLgm8dbe5UXIXcoBhZSWA=
|
||||
github.com/alecthomas/chroma/v2 v2.21.1/go.mod h1:NqVhfBR0lte5Ouh3DcthuUCTUpDC9cxBOfyMbMQPs3o=
|
||||
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/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
|
||||
@@ -20,32 +20,33 @@ github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwTo
|
||||
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/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||
github.com/bits-and-blooms/bitset v1.24.4 h1:95H15Og1clikBrKr/DuzMXkQzECs1M6hhoGXLwLQOZE=
|
||||
github.com/bits-and-blooms/bitset v1.24.4/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
|
||||
github.com/bits-and-blooms/bitset v1.12.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
|
||||
github.com/bits-and-blooms/bitset v1.24.0 h1:H4x4TuulnokZKvHLfzVRTHJfFfnHEeSYJizujEZvmAM=
|
||||
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/go.mod h1:yj0NlS7ocGC4VOSAedqDDMktdh2935v2CSWOCDMHdSA=
|
||||
github.com/blevesearch/bleve_index_api v1.3.1 h1:LdH3CQgBbIZ5UI/5Pykz87e0jfeQtVnrdZ2WUBrHHwU=
|
||||
github.com/blevesearch/bleve_index_api v1.3.1/go.mod h1:xvd48t5XMeeioWQ5/jZvgLrV98flT2rdvEJ3l/ki4Ko=
|
||||
github.com/blevesearch/bleve_index_api v1.2.11 h1:bXQ54kVuwP8hdrXUSOnvTQfgK0KI1+f9A0ITJT8tX1s=
|
||||
github.com/blevesearch/bleve_index_api v1.2.11/go.mod h1:rKQDl4u51uwafZxFrPD1R7xFOwKnzZW7s/LSeK4lgo0=
|
||||
github.com/blevesearch/geo v0.2.4 h1:ECIGQhw+QALCZaDcogRTNSJYQXRtC8/m8IKiA706cqk=
|
||||
github.com/blevesearch/geo v0.2.4/go.mod h1:K56Q33AzXt2YExVHGObtmRSFYZKYGv0JEN5mdacJJR8=
|
||||
github.com/blevesearch/go-faiss v1.0.27 h1:7cBImYDDQ82WJd5RUZ1ie6zXztCsC73W94ZzwOjkatk=
|
||||
github.com/blevesearch/go-faiss v1.0.27/go.mod h1:OMGQwOaRRYxrmeNdMrXJPvVx8gBnvE5RYrr0BahNnkk=
|
||||
github.com/blevesearch/go-faiss v1.0.26 h1:4dRLolFgjPyjkaXwff4NfbZFdE/dfywbzDqporeQvXI=
|
||||
github.com/blevesearch/go-faiss v1.0.26/go.mod h1:OMGQwOaRRYxrmeNdMrXJPvVx8gBnvE5RYrr0BahNnkk=
|
||||
github.com/blevesearch/go-porterstemmer v1.0.3 h1:GtmsqID0aZdCSNiY8SkuPJ12pD4jI+DdXTAn4YRcHCo=
|
||||
github.com/blevesearch/go-porterstemmer v1.0.3/go.mod h1:angGc5Ht+k2xhJdZi511LtmxuEf0OVpvUUNrwmM1P7M=
|
||||
github.com/blevesearch/gtreap v0.1.1 h1:2JWigFrzDMR+42WGIN/V2p0cUvn4UP3C4Q5nmaZGW8Y=
|
||||
github.com/blevesearch/gtreap v0.1.1/go.mod h1:QaQyDRAT51sotthUWAH4Sj08awFSSWzgYICSZ3w0tYk=
|
||||
github.com/blevesearch/mmap-go v1.2.0 h1:l33nNKPFcBjJUMwem6sAYJPUzhUCABoK9FxZDGiFNBI=
|
||||
github.com/blevesearch/mmap-go v1.2.0/go.mod h1:Vd6+20GBhEdwJnU1Xohgt88XCD/CTWcqbCNxkZpyBo0=
|
||||
github.com/blevesearch/scorch_segment_api/v2 v2.4.1 h1:os52/JeCSLZ0YUkOuLk/Z7pu0SKUMofDPUg+VnbrRD0=
|
||||
github.com/blevesearch/scorch_segment_api/v2 v2.4.1/go.mod h1:zvilBm4BNfbnTRLW7KgCTNgk2R31JaWzwRc2BEcD7Is=
|
||||
github.com/blevesearch/mmap-go v1.0.4 h1:OVhDhT5B/M1HNPpYPBKIEJaD0F3Si+CrEKULGCDPWmc=
|
||||
github.com/blevesearch/mmap-go v1.0.4/go.mod h1:EWmEAOmdAS9z/pi/+Toxu99DnsbhG1TIxUoRmJw/pSs=
|
||||
github.com/blevesearch/scorch_segment_api/v2 v2.3.13 h1:ZPjv/4VwWvHJZKeMSgScCapOy8+DdmsmRyLmSB88UoY=
|
||||
github.com/blevesearch/scorch_segment_api/v2 v2.3.13/go.mod h1:ENk2LClTehOuMS8XzN3UxBEErYmtwkE7MAArFTXs9Vc=
|
||||
github.com/blevesearch/segment v0.9.1 h1:+dThDy+Lvgj5JMxhmOVlgFfkUtZV2kw49xax4+jTfSU=
|
||||
github.com/blevesearch/segment v0.9.1/go.mod h1:zN21iLm7+GnBHWTao9I+Au/7MBiL8pPFtJBJTsk6kQw=
|
||||
github.com/blevesearch/snowballstem v0.9.0 h1:lMQ189YspGP6sXvZQ4WZ+MLawfV8wOmPoD/iWeNXm8s=
|
||||
github.com/blevesearch/snowballstem v0.9.0/go.mod h1:PivSj3JMc8WuaFkTSRDW2SlrulNWPl4ABg1tC/hlgLs=
|
||||
github.com/blevesearch/upsidedown_store_api v1.0.2 h1:U53Q6YoWEARVLd1OYNc9kvhBMGZzVrdmaozG2MfoB+A=
|
||||
github.com/blevesearch/upsidedown_store_api v1.0.2/go.mod h1:M01mh3Gpfy56Ps/UXHjEO/knbqyQ1Oamg8If49gRwrQ=
|
||||
github.com/blevesearch/vellum v1.2.0 h1:xkDiOEsHc2t3Cp0NsNZZ36pvc130sCzcGKOPMzXe+e0=
|
||||
github.com/blevesearch/vellum v1.2.0/go.mod h1:uEcfBJz7mAOf0Kvq6qoEKQQkLODBF46SINYNkZNae4k=
|
||||
github.com/blevesearch/vellum v1.1.0 h1:CinkGyIsgVlYf8Y2LUQHvdelgXr6PYuvoDIajq6yR9w=
|
||||
github.com/blevesearch/vellum v1.1.0/go.mod h1:QgwWryE8ThtNPxtgWJof5ndPfx0/YMBh+W2weHKPw8Y=
|
||||
github.com/blevesearch/zapx/v11 v11.4.2 h1:l46SV+b0gFN+Rw3wUI1YdMWdSAVhskYuvxlcgpQFljs=
|
||||
github.com/blevesearch/zapx/v11 v11.4.2/go.mod h1:4gdeyy9oGa/lLa6D34R9daXNUvfMPZqUYjPwiLmekwc=
|
||||
github.com/blevesearch/zapx/v12 v12.4.2 h1:fzRbhllQmEMUuAQ7zBuMvKRlcPA5ESTgWlDEoB9uQNE=
|
||||
@@ -56,8 +57,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/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/v16 v16.3.0 h1:hF6VlN15E9CB40RMPyqOIhlDw1OOo9RItumhKMQktxw=
|
||||
github.com/blevesearch/zapx/v16 v16.3.0/go.mod h1:zCFjv7McXWm1C8rROL+3mUoD5WYe2RKsZP3ufqcYpLY=
|
||||
github.com/blevesearch/zapx/v16 v16.2.8 h1:SlnzF0YGtSlrsOE3oE7EgEX6BIepGpeqxs1IjMbHLQI=
|
||||
github.com/blevesearch/zapx/v16 v16.2.8/go.mod h1:murSoCJPCk25MqURrcJaBQ1RekuqSCSfMjXH4rHyA14=
|
||||
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/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
|
||||
@@ -69,10 +70,8 @@ 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/sysutil v1.1.0 h1:PUFNv5EcprjqXZD9nJb9b/c9ibAbxiYo4exNWZyipwM=
|
||||
github.com/chromedp/sysutil v1.1.0/go.mod h1:WiThHUdltqCNKGc4gaU50XgYjwjYIhKWoHGPTUfWTJ8=
|
||||
github.com/clipperhouse/stringish v0.1.1 h1:+NSqMOr3GR6k1FdRhhnXrLfztGzuG+VuFDfatpWHKCs=
|
||||
github.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEXNWYXQgCt4hdOzA=
|
||||
github.com/clipperhouse/uax29/v2 v2.4.0 h1:RXqE/l5EiAbA4u97giimKNlmpvkmz+GrBVTelsoXy9g=
|
||||
github.com/clipperhouse/uax29/v2 v2.4.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g=
|
||||
github.com/clipperhouse/uax29/v2 v2.2.0 h1:ChwIKnQN3kcZteTXMgb1wztSgaU+ZemkgWdohwgs8tY=
|
||||
github.com/clipperhouse/uax29/v2 v2.2.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM=
|
||||
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/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||
@@ -125,6 +124,8 @@ 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/go.mod h1:G3gNqMNtPppf5XUz7O4shetPpcZ1VJ7zt18dlUeakrc=
|
||||
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/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
|
||||
github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs=
|
||||
@@ -150,16 +151,14 @@ 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/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/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/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
|
||||
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
||||
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
|
||||
github.com/jackc/pgx/v5 v5.8.0 h1:TYPDoleBBme0xGSAX3/+NujXXtpZn9HBONkQC7IEZSo=
|
||||
github.com/jackc/pgx/v5 v5.8.0/go.mod h1:QVeDInX2m9VyzvNeiCJVjCkNFqzsNb43204HshNSZKw=
|
||||
github.com/jackc/pgx/v5 v5.7.6 h1:rWQc5FwZSPX58r1OQmkuaNicxdmExaEz5A2DO2hUuTk=
|
||||
github.com/jackc/pgx/v5 v5.7.6/go.mod h1:aruU7o91Tc2q2cFp5h4uP3f6ztExVpyVv88Xl/8Vl8M=
|
||||
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
|
||||
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
||||
github.com/jcmturner/aescts/v2 v2.0.0 h1:9YKLH6ey7H4eDBXW8khjYslgyqG2xZikXP0EQFKrle8=
|
||||
@@ -207,8 +206,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-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw=
|
||||
github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
|
||||
github.com/meilisearch/meilisearch-go v0.36.0 h1:N1etykTektXt5KPcSbhBO0d5Xx5NaKj4pJWEM7WA5dI=
|
||||
github.com/meilisearch/meilisearch-go v0.36.0/go.mod h1:HBfHzKMxcSbTOvqdfuRA/yf6Vk9IivcwKocWRuW7W78=
|
||||
github.com/meilisearch/meilisearch-go v0.35.1 h1:5H2FeY5eR4HSkaZMJIoefNzOj3XX1+5dd7ZfhAfzeMg=
|
||||
github.com/meilisearch/meilisearch-go v0.35.1/go.mod h1:cUVJZ2zMqTvvwIMEEAdsWH+zrHsrLpAw6gm8Lt1MXK0=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
@@ -218,8 +217,8 @@ github.com/mschoch/smat v0.2.0 h1:8imxQsjDm8yFEAVBe7azKmKSgzSkZXDuKkSq9374khM=
|
||||
github.com/mschoch/smat v0.2.0/go.mod h1:kc9mz7DoBKqDyiRL7VZN8KvXQMWeTaVnttLRXOlotKw=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||
github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
|
||||
github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
||||
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
|
||||
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
@@ -229,10 +228,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_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
|
||||
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
|
||||
github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4=
|
||||
github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw=
|
||||
github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws=
|
||||
github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw=
|
||||
github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs=
|
||||
github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA=
|
||||
github.com/prometheus/procfs v0.17.0 h1:FuLQ+05u4ZI+SS/w9+BWEM2TXiHKsUQ9TADiRH7DuK0=
|
||||
github.com/prometheus/procfs v0.17.0/go.mod h1:oPQLaDAMRbA+u8H5Pbfq+dl3VDAvHxMUOVhe0wYB2zw=
|
||||
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/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
|
||||
@@ -260,8 +259,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/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
|
||||
github.com/yuin/goldmark v1.4.15/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
github.com/yuin/goldmark v1.7.16 h1:n+CJdUxaFMiDUNnWC3dMWCIQJSkxH4uz3ZwQBkAlVNE=
|
||||
github.com/yuin/goldmark v1.7.16/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
|
||||
github.com/yuin/goldmark v1.7.15 h1:xYJWgq3Qd8qsaZpj5pHKoEI4mosqVZi/qRpq/MdKyyk=
|
||||
github.com/yuin/goldmark v1.7.15/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
|
||||
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-highlighting/v2 v2.0.0-20230729083705-37449abec8cc h1:+IAOyRda+RLrxa1WC7umKOZRsGq4QrFFMYApOeHzQwQ=
|
||||
@@ -276,37 +275,39 @@ go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
|
||||
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/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8=
|
||||
golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8=
|
||||
golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A=
|
||||
golang.org/x/exp v0.0.0-20260112195511-716be5621a96 h1:Z/6YuSHTLOHfNFdb8zVZomZr7cqNgTJvA8+Qz75D8gU=
|
||||
golang.org/x/exp v0.0.0-20260112195511-716be5621a96/go.mod h1:nzimsREAkjBCIEFtHiYkrJyT+2uy9YZJB7H1k68CXZU=
|
||||
golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c=
|
||||
golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU=
|
||||
golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o=
|
||||
golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8=
|
||||
golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw=
|
||||
golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
|
||||
golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
|
||||
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
|
||||
golang.org/x/exp v0.0.0-20251002181428-27f1f14c8bb9 h1:TQwNpfvNkxAVlItJf6Cr5JTsVZoC/Sj7K3OZv2Pc14A=
|
||||
golang.org/x/exp v0.0.0-20251002181428-27f1f14c8bb9/go.mod h1:TwQYMMnGpvZyc+JpB/UAuTNIsVJifOlSkrZkhcvpVUk=
|
||||
golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk=
|
||||
golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc=
|
||||
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
|
||||
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
|
||||
golang.org/x/oauth2 v0.31.0 h1:8Fq0yVZLh4j4YA47vHKFTa9Ew5XIrCP8LC6UeNZnLxo=
|
||||
golang.org/x/oauth2 v0.31.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
|
||||
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
|
||||
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/sys v0.0.0-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.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
|
||||
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/term v0.39.0 h1:RclSuaJf32jOqZz74CkPA9qFuVTX7vhLlpfj/IGWlqY=
|
||||
golang.org/x/term v0.39.0/go.mod h1:yxzUCTP/U+FzoxfdKmLaA0RV1WgE0VY7hXBwKtY/4ww=
|
||||
golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE=
|
||||
golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8=
|
||||
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
|
||||
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q=
|
||||
golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg=
|
||||
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
|
||||
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
|
||||
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/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc=
|
||||
golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg=
|
||||
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ=
|
||||
golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ=
|
||||
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
|
||||
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/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/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gorm.io/driver/mysql v1.6.0 h1:eNbLmNTpPpTOVZi8MMxCi2aaIm0ZpInbORNXDwyLGvg=
|
||||
@@ -315,20 +316,18 @@ 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/gorm v1.31.1 h1:7CA8FTFz/gRfgqgpeKIBcervUn3xSyPUmr6B2WXJ7kg=
|
||||
gorm.io/gorm v1.31.1/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs=
|
||||
modernc.org/cc/v4 v4.27.1 h1:9W30zRlYrefrDV2JE2O8VDtJ1yPGownxciz5rrbQZis=
|
||||
modernc.org/cc/v4 v4.27.1/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
|
||||
modernc.org/ccgo/v4 v4.30.1 h1:4r4U1J6Fhj98NKfSjnPUN7Ze2c6MnAdL0hWw6+LrJpc=
|
||||
modernc.org/ccgo/v4 v4.30.1/go.mod h1:bIOeI1JL54Utlxn+LwrFyjCx2n2RDiYEaJVSrgdrRfM=
|
||||
modernc.org/cc/v4 v4.26.5 h1:xM3bX7Mve6G8K8b+T11ReenJOT+BmVqQj0FY5T4+5Y4=
|
||||
modernc.org/cc/v4 v4.26.5/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
|
||||
modernc.org/ccgo/v4 v4.28.1 h1:wPKYn5EC/mYTqBO373jKjvX2n+3+aK7+sICCv4Fjy1A=
|
||||
modernc.org/ccgo/v4 v4.28.1/go.mod h1:uD+4RnfrVgE6ec9NGguUNdhqzNIeeomeXf6CL0GTE5Q=
|
||||
modernc.org/fileutil v1.3.40 h1:ZGMswMNc9JOCrcrakF1HrvmergNLAmxOPjizirpfqBA=
|
||||
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/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/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
|
||||
modernc.org/libc v1.67.7 h1:H+gYQw2PyidyxwxQsGTwQw6+6H+xUk+plvOKW7+d3TI=
|
||||
modernc.org/libc v1.67.7/go.mod h1:UjCSJFl2sYbJbReVQeVpq/MgzlbmDM4cRHIYFelnaDk=
|
||||
modernc.org/libc v1.66.10 h1:yZkb3YeLx4oynyR+iUsXsybsX4Ubx7MQlSYEw4yj59A=
|
||||
modernc.org/libc v1.66.10/go.mod h1:8vGSEwvoUoltr4dlywvHqjtAqHBaw0j1jI7iFBTAr2I=
|
||||
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
|
||||
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
|
||||
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
|
||||
@@ -337,8 +336,8 @@ modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
|
||||
modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
|
||||
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
|
||||
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
|
||||
modernc.org/sqlite v1.44.3 h1:+39JvV/HWMcYslAwRxHb8067w+2zowvFOUrOWIy9PjY=
|
||||
modernc.org/sqlite v1.44.3/go.mod h1:CzbrU2lSB1DKUusvwGz7rqEKIq+NUd8GWuBBZDs9/nA=
|
||||
modernc.org/sqlite v1.39.0 h1:6bwu9Ooim0yVYA7IZn9demiQk/Ejp0BtTjBWFLymSeY=
|
||||
modernc.org/sqlite v1.39.0/go.mod h1:cPTJYSlgg3Sfg046yBShXENNtPrWrDX8bsbAQBzgQ5E=
|
||||
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
|
||||
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
|
||||
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
# 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
|
||||
@@ -2,8 +2,8 @@ apiVersion: v2
|
||||
name: opengist
|
||||
description: Opengist Helm chart for Kubernetes
|
||||
type: application
|
||||
version: 0.6.0
|
||||
appVersion: 1.12.1
|
||||
version: 0.5.0
|
||||
appVersion: 1.11.1
|
||||
home: https://opengist.io
|
||||
icon: https://raw.githubusercontent.com/thomiceli/opengist/master/public/img/opengist.svg
|
||||
sources:
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
# Opengist Helm Chart
|
||||
|
||||
 
|
||||
 
|
||||
|
||||
Opengist Helm chart for Kubernetes. Check [CHANGELOG.md](CHANGELOG.md) for release notes.
|
||||
Opengist Helm chart for Kubernetes.
|
||||
|
||||
* [Install](#install)
|
||||
* [Configuration](#configuration)
|
||||
* [Metrics & Monitoring](#metrics--monitoring)
|
||||
* [Dependencies](#dependencies)
|
||||
* [Meilisearch Indexer](#meilisearch-indexer)
|
||||
* [PostgreSQL Database](#postgresql-database)
|
||||
@@ -48,76 +47,6 @@ If defined, this existing secret will be used instead of creating a new one.
|
||||
configExistingSecret: <name of the secret>
|
||||
```
|
||||
|
||||
## Metrics & Monitoring
|
||||
|
||||
Opengist exposes Prometheus metrics on a separate port (default: `6158`). The metrics server runs independently from the main HTTP server for security.
|
||||
|
||||
### Enabling Metrics
|
||||
|
||||
To enable metrics, set `metrics.enabled: true` in your Opengist config:
|
||||
|
||||
```yaml
|
||||
config:
|
||||
metrics.enabled: true
|
||||
```
|
||||
|
||||
This will:
|
||||
1. Start a metrics server on port 6158 inside the container
|
||||
2. Create a Kubernetes Service exposing the metrics ports
|
||||
|
||||
### Available Metrics
|
||||
|
||||
| Metric Name | Type | Description |
|
||||
|-------------|------|-------------|
|
||||
| `opengist_users_total` | Gauge | Total number of registered users |
|
||||
| `opengist_gists_total` | Gauge | Total number of gists |
|
||||
| `opengist_ssh_keys_total` | Gauge | Total number of SSH keys |
|
||||
| `opengist_request_duration_seconds_*` | Histogram | HTTP request duration metrics |
|
||||
|
||||
### ServiceMonitor for Prometheus Operator
|
||||
|
||||
If you're using [Prometheus Operator](https://github.com/prometheus-operator/prometheus-operator), you can enable automatic service discovery with a ServiceMonitor:
|
||||
|
||||
```yaml
|
||||
config:
|
||||
metrics.enabled: true
|
||||
|
||||
service:
|
||||
metrics:
|
||||
serviceMonitor:
|
||||
enabled: true
|
||||
labels:
|
||||
release: prometheus # match your Prometheus serviceMonitorSelector
|
||||
```
|
||||
|
||||
### Manual Prometheus Configuration
|
||||
|
||||
If you're not using Prometheus Operator, you can configure Prometheus to scrape the metrics endpoint directly:
|
||||
|
||||
```yaml
|
||||
scrape_configs:
|
||||
- job_name: 'opengist'
|
||||
static_configs:
|
||||
- targets: ['opengist-metrics:6158']
|
||||
metrics_path: /metrics
|
||||
```
|
||||
|
||||
Or use Kubernetes service discovery:
|
||||
|
||||
```yaml
|
||||
scrape_configs:
|
||||
- job_name: 'opengist'
|
||||
kubernetes_sd_configs:
|
||||
- role: service
|
||||
relabel_configs:
|
||||
- source_labels: [__meta_kubernetes_service_label_app_kubernetes_io_component]
|
||||
regex: metrics
|
||||
action: keep
|
||||
- source_labels: [__meta_kubernetes_service_label_app_kubernetes_io_name]
|
||||
regex: opengist
|
||||
action: keep
|
||||
```
|
||||
|
||||
## Dependencies
|
||||
|
||||
### Meilisearch Indexer
|
||||
|
||||
@@ -67,11 +67,6 @@ spec:
|
||||
- name: http
|
||||
containerPort: {{ .Values.service.http.port }}
|
||||
protocol: TCP
|
||||
{{- if index .Values.config "metrics.enabled" }}
|
||||
- name: metrics
|
||||
containerPort: {{ .Values.service.metrics.port }}
|
||||
protocol: TCP
|
||||
{{- end }}
|
||||
{{- if .Values.livenessProbe.enabled }}
|
||||
livenessProbe:
|
||||
{{- toYaml (omit .Values.livenessProbe "enabled") | nindent 12 }}
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
{{- 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 }}
|
||||
@@ -140,11 +140,6 @@ spec:
|
||||
containerPort: {{ .Values.service.ssh.port }}
|
||||
protocol: TCP
|
||||
{{- end }}
|
||||
{{- if index .Values.config "metrics.enabled" }}
|
||||
- name: metrics
|
||||
containerPort: {{ .Values.service.metrics.port }}
|
||||
protocol: TCP
|
||||
{{- end }}
|
||||
{{- if .Values.livenessProbe.enabled }}
|
||||
livenessProbe:
|
||||
{{- toYaml (omit .Values.livenessProbe "enabled") | nindent 12 }}
|
||||
@@ -177,7 +172,7 @@ spec:
|
||||
defaultMode: 511
|
||||
- name: config-volume
|
||||
emptyDir: {}
|
||||
{{- /*
|
||||
{{- /*
|
||||
========================================
|
||||
VOLUME MOUNTING DECISION TREE
|
||||
========================================
|
||||
@@ -221,7 +216,7 @@ spec:
|
||||
tolerations:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- /*
|
||||
{{- /*
|
||||
========================================
|
||||
VOLUMECLAIMTEMPLATES DECISION TREE
|
||||
========================================
|
||||
@@ -229,14 +224,14 @@ spec:
|
||||
- persistence.enabled=true
|
||||
- persistence.existingClaim is empty
|
||||
- persistence.mode=perReplica (default)
|
||||
|
||||
|
||||
This creates one PVC per replica (RWO typically).
|
||||
|
||||
|
||||
NOT used when:
|
||||
- existingClaim is set (PVC already exists, referenced in volumes above)
|
||||
- mode=shared (standalone PVC created via pvc-shared.yaml)
|
||||
- persistence disabled (emptyDir used)
|
||||
|
||||
|
||||
WARNING: perReplica + replicaCount>1 causes data divergence. Use shared mode for multi-replica.
|
||||
*/}}
|
||||
{{- if and .Values.persistence.enabled (ne (default "" .Values.persistence.existingClaim) "") }}
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
{{- 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 }}
|
||||
@@ -8,7 +8,6 @@ namespace: ""
|
||||
config:
|
||||
log-level: "warn"
|
||||
log-output: "stdout"
|
||||
metrics.enabled: false
|
||||
|
||||
## If defined, the existing secret will be used instead of creating a new one.
|
||||
## The secret must contain a key named `config.yml` with the YAML configuration.
|
||||
@@ -18,7 +17,7 @@ configExistingSecret: ""
|
||||
image:
|
||||
repository: ghcr.io/thomiceli/opengist
|
||||
pullPolicy: Always
|
||||
tag: "1.12.1"
|
||||
tag: "1.11.1"
|
||||
digest: ""
|
||||
imagePullSecrets: []
|
||||
# - name: "image-pull-secret"
|
||||
@@ -102,26 +101,6 @@ service:
|
||||
loadBalancerSourceRanges: []
|
||||
externalTrafficPolicy:
|
||||
|
||||
# A metrics K8S service on port 6158 is created when the Opengist config metrics.enabled: true
|
||||
metrics:
|
||||
type: ClusterIP
|
||||
clusterIP:
|
||||
port: 6158
|
||||
nodePort:
|
||||
labels: {}
|
||||
annotations: {}
|
||||
|
||||
# A service monitor can be used to work with your Prometheus setup.
|
||||
serviceMonitor:
|
||||
enabled: true
|
||||
labels: {}
|
||||
# release: kube-prom-stack
|
||||
interval:
|
||||
scrapeTimeout:
|
||||
annotations: {}
|
||||
relabelings: []
|
||||
metricRelabelings: []
|
||||
|
||||
## HTTP Ingress for Opengist
|
||||
## ref: https://kubernetes.io/docs/concepts/services-networking/ingress/
|
||||
ingress:
|
||||
|
||||
@@ -9,7 +9,6 @@ import (
|
||||
"github.com/thomiceli/opengist/internal/git"
|
||||
"github.com/thomiceli/opengist/internal/index"
|
||||
"github.com/thomiceli/opengist/internal/ssh"
|
||||
"github.com/thomiceli/opengist/internal/web/handlers/metrics"
|
||||
"github.com/thomiceli/opengist/internal/web/server"
|
||||
"github.com/urfave/cli/v2"
|
||||
"os"
|
||||
@@ -37,18 +36,12 @@ var CmdStart = cli.Command{
|
||||
|
||||
Initialize(ctx)
|
||||
|
||||
httpServer := server.NewServer(os.Getenv("OG_DEV") == "1", path.Join(config.GetHomeDir(), "sessions"), false)
|
||||
go httpServer.Start()
|
||||
server := server.NewServer(os.Getenv("OG_DEV") == "1", path.Join(config.GetHomeDir(), "sessions"), false)
|
||||
go server.Start()
|
||||
go ssh.Start()
|
||||
|
||||
var metricsServer *metrics.Server
|
||||
if config.C.MetricsEnabled {
|
||||
metricsServer = metrics.NewServer()
|
||||
go metricsServer.Start()
|
||||
}
|
||||
|
||||
<-stopCtx.Done()
|
||||
shutdown(httpServer, metricsServer)
|
||||
shutdown(server)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
@@ -138,7 +131,7 @@ func Initialize(ctx *cli.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
func shutdown(httpServer *server.Server, metricsServer *metrics.Server) {
|
||||
func shutdown(server *server.Server) {
|
||||
log.Info().Msg("Shutting down database...")
|
||||
if err := db.Close(); err != nil {
|
||||
log.Error().Err(err).Msg("Failed to close database")
|
||||
@@ -149,11 +142,7 @@ func shutdown(httpServer *server.Server, metricsServer *metrics.Server) {
|
||||
index.Close()
|
||||
}
|
||||
|
||||
httpServer.Stop()
|
||||
|
||||
if metricsServer != nil {
|
||||
metricsServer.Stop()
|
||||
}
|
||||
server.Stop()
|
||||
|
||||
log.Info().Msg("Shutdown complete")
|
||||
}
|
||||
|
||||
@@ -79,9 +79,7 @@ type config struct {
|
||||
OIDCGroupClaimName string `yaml:"oidc.group-claim-name" env:"OG_OIDC_GROUP_CLAIM_NAME"`
|
||||
OIDCAdminGroup string `yaml:"oidc.admin-group" env:"OG_OIDC_ADMIN_GROUP"`
|
||||
|
||||
MetricsEnabled bool `yaml:"metrics.enabled" env:"OG_METRICS_ENABLED"`
|
||||
MetricsHost string `yaml:"metrics.host" env:"OG_METRICS_HOST"`
|
||||
MetricsPort string `yaml:"metrics.port" env:"OG_METRICS_PORT"`
|
||||
MetricsEnabled bool `yaml:"metrics.enabled" env:"OG_METRICS_ENABLED"`
|
||||
|
||||
LDAPUrl string `yaml:"ldap.url" env:"OG_LDAP_URL"`
|
||||
LDAPBindDn string `yaml:"ldap.bind-dn" env:"OG_LDAP_BIND_DN"`
|
||||
@@ -130,8 +128,6 @@ func configWithDefaults() (*config, error) {
|
||||
c.GiteaName = "Gitea"
|
||||
|
||||
c.MetricsEnabled = false
|
||||
c.MetricsHost = "0.0.0.0"
|
||||
c.MetricsPort = "6158"
|
||||
|
||||
return c, nil
|
||||
}
|
||||
|
||||
@@ -1,125 +0,0 @@
|
||||
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,
|
||||
}
|
||||
}
|
||||
@@ -155,7 +155,7 @@ func Setup(dbUri string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = db.AutoMigrate(&User{}, &Gist{}, &SSHKey{}, &AdminSetting{}, &Invitation{}, &WebAuthnCredential{}, &TOTP{}, &GistTopic{}, &GistLanguage{}, &GistInitQueue{}, &AccessToken{}); err != nil {
|
||||
if err = db.AutoMigrate(&User{}, &Gist{}, &SSHKey{}, &AdminSetting{}, &Invitation{}, &WebAuthnCredential{}, &TOTP{}, &GistTopic{}, &GistLanguage{}, &GistInitQueue{}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -269,5 +269,5 @@ func DeprecationDBFilename() {
|
||||
}
|
||||
|
||||
func TruncateDatabase() error {
|
||||
return db.Migrator().DropTable("likes", &User{}, "gists", &SSHKey{}, &AdminSetting{}, &Invitation{}, &WebAuthnCredential{}, &TOTP{}, &GistTopic{}, &GistLanguage{}, &GistInitQueue{}, &AccessToken{})
|
||||
return db.Migrator().DropTable("likes", &User{}, "gists", &SSHKey{}, &AdminSetting{}, &Invitation{}, &WebAuthnCredential{}, &TOTP{}, &GistTopic{}, &GistLanguage{}, &GistInitQueue{})
|
||||
}
|
||||
|
||||
@@ -73,7 +73,6 @@ type Gist struct {
|
||||
URL string
|
||||
Preview string
|
||||
PreviewFilename string
|
||||
PreviewMimeType string
|
||||
Description string
|
||||
Private Visibility // 0: public, 1: unlisted, 2: private
|
||||
UserID uint
|
||||
@@ -552,7 +551,6 @@ func (gist *Gist) UpdatePreviewAndCount(withTimestampUpdate bool) error {
|
||||
if len(filesStr) == 0 {
|
||||
gist.Preview = ""
|
||||
gist.PreviewFilename = ""
|
||||
gist.PreviewMimeType = ""
|
||||
} else {
|
||||
for _, fileStr := range filesStr {
|
||||
file, err := gist.File("HEAD", fileStr, true)
|
||||
@@ -564,7 +562,6 @@ func (gist *Gist) UpdatePreviewAndCount(withTimestampUpdate bool) error {
|
||||
}
|
||||
gist.Preview = ""
|
||||
gist.PreviewFilename = file.Filename
|
||||
gist.PreviewMimeType = file.MimeType.ContentType
|
||||
|
||||
if !file.MimeType.CanBeEdited() {
|
||||
continue
|
||||
|
||||
@@ -25,7 +25,6 @@ type User struct {
|
||||
SSHKeys []SSHKey `gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE;foreignKey:UserID"`
|
||||
Liked []Gist `gorm:"many2many:likes;constraint:OnUpdate:CASCADE,OnDelete:CASCADE"`
|
||||
WebAuthnCredentials []WebAuthnCredential `gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE;foreignKey:UserID"`
|
||||
AccessTokens []AccessToken `gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE;foreignKey:UserID"`
|
||||
}
|
||||
|
||||
func (user *User) BeforeDelete(tx *gorm.DB) error {
|
||||
@@ -73,11 +72,6 @@ func (user *User) BeforeDelete(tx *gorm.DB) error {
|
||||
return err
|
||||
}
|
||||
|
||||
err = tx.Where("user_id = ?", user.ID).Delete(&AccessToken{}).Error
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = tx.Where("user_id = ?", user.ID).Delete(&Gist{}).Error
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -2,45 +2,43 @@ package git
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/gabriel-vasile/mimetype"
|
||||
)
|
||||
|
||||
type MimeType struct {
|
||||
ContentType string
|
||||
extension string
|
||||
golangContentType string // json, m3u, etc. still renderable as text
|
||||
ContentType string
|
||||
extension string
|
||||
}
|
||||
|
||||
func (mt MimeType) IsText() bool {
|
||||
return strings.HasPrefix(mt.ContentType, "text/") || strings.HasPrefix(mt.golangContentType, "text/")
|
||||
return strings.Contains(mt.ContentType, "text/")
|
||||
}
|
||||
|
||||
func (mt MimeType) IsCSV() bool {
|
||||
return strings.HasPrefix(mt.ContentType, "text/csv") &&
|
||||
return strings.Contains(mt.ContentType, "text/csv") &&
|
||||
(strings.HasSuffix(mt.extension, ".csv"))
|
||||
}
|
||||
|
||||
func (mt MimeType) IsImage() bool {
|
||||
return strings.HasPrefix(mt.ContentType, "image/")
|
||||
return strings.Contains(mt.ContentType, "image/")
|
||||
}
|
||||
|
||||
func (mt MimeType) IsSVG() bool {
|
||||
return strings.HasPrefix(mt.ContentType, "image/svg+xml")
|
||||
return strings.Contains(mt.ContentType, "image/svg+xml")
|
||||
}
|
||||
|
||||
func (mt MimeType) IsPDF() bool {
|
||||
return strings.HasPrefix(mt.ContentType, "application/pdf")
|
||||
return strings.Contains(mt.ContentType, "application/pdf")
|
||||
}
|
||||
|
||||
func (mt MimeType) IsAudio() bool {
|
||||
return strings.HasPrefix(mt.ContentType, "audio/")
|
||||
return strings.Contains(mt.ContentType, "audio/")
|
||||
}
|
||||
|
||||
func (mt MimeType) IsVideo() bool {
|
||||
return strings.HasPrefix(mt.ContentType, "video/")
|
||||
return strings.Contains(mt.ContentType, "video/")
|
||||
}
|
||||
|
||||
func (mt MimeType) CanBeHighlighted() bool {
|
||||
@@ -89,5 +87,5 @@ func (mt MimeType) RenderType() string {
|
||||
}
|
||||
|
||||
func DetectMimeType(data []byte, extension string) MimeType {
|
||||
return MimeType{mimetype.Detect(data).String(), extension, http.DetectContentType(data)}
|
||||
return MimeType{mimetype.Detect(data).String(), extension}
|
||||
}
|
||||
|
||||
@@ -157,7 +157,6 @@ settings.password-label-title: Password
|
||||
settings.header.account: Account
|
||||
settings.header.mfa: MFA
|
||||
settings.header.ssh: SSH
|
||||
settings.header.tokens: Access tokens
|
||||
settings.header.style: Style
|
||||
settings.style.gist-code: Gist code
|
||||
settings.style.no-soft-wrap: No Soft Wrap
|
||||
@@ -170,26 +169,6 @@ settings.style.theme: Theme
|
||||
settings.style.theme-light: Light
|
||||
settings.style.theme-dark: Dark
|
||||
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.login: Login
|
||||
@@ -365,4 +344,4 @@ validation.not-enough: Not enough %s
|
||||
validation.invalid: Invalid %s
|
||||
validation.invalid-gist-topics: Invalid gist topics, they must start with a letter or number, consist of 50 characters or less, and can include hyphens
|
||||
|
||||
html.title.admin-panel: Admin panel
|
||||
html.title.admin-panel: Admin panel
|
||||
@@ -51,7 +51,7 @@ gist.edit.save: Сохранить
|
||||
|
||||
gist.list.joined: Зарегистрирован
|
||||
gist.list.all: Все фрагменты
|
||||
gist.list.search-results: Результаты поиска
|
||||
gist.list.search-results: Результаты поиска
|
||||
gist.list.sort: Сортировка
|
||||
gist.list.sort-by-created: по дате создания
|
||||
gist.list.sort-by-updated: по дате обновления
|
||||
@@ -159,19 +159,19 @@ admin.created_at: Создан
|
||||
admin.config-link: Эти настройки могут быть %s файлом конфигурации YAML и/или переменными окружения.
|
||||
admin.config-link-overriden: перекрыты
|
||||
admin.disable-signup: Запретить регистрацию
|
||||
admin.disable-signup_help: Запретить создание новых доступов.
|
||||
admin.disable-signup_help: Запретить создание новых доступов
|
||||
admin.require-login: Требовать авторизацию
|
||||
admin.require-login_help: Запретить просмотр фрагментов без авторизации.
|
||||
admin.disable-login: Запретить авторизацию по паролю
|
||||
admin.disable-login_help: Запретить авторизацию с вводом пароля, форсировать внешнюю авторизацию через Gitea/GitHub.
|
||||
admin.disable-gravatar: Запретить Gravatar
|
||||
admin.disable-gravatar_help: Запретить использование Gravatar как провайдера изображений профиля.
|
||||
admin.allow-gists-without-login: Разрешить доступ к отдельным фрагментам без авторизации
|
||||
admin.allow-gists-without-login_help: Разрешает просматривать и скачивать отдельные фрагменты без входа, но требует авторизацию для поиска фрагментов.
|
||||
admin.allow-gists-without-login:
|
||||
admin.allow-gists-without-login_help:
|
||||
admin.users.delete_confirm: Вы уверены что хотите удалить этого пользователя?
|
||||
|
||||
admin.gists.title: Название
|
||||
admin.gists.private: Приватный?
|
||||
admin.gists.private: Приватный
|
||||
admin.gists.nb-files: Файлов
|
||||
admin.gists.nb-likes: Понравилось
|
||||
admin.gists.delete_confirm: Вы уверены что хотите удалить этот фрагмент?
|
||||
@@ -183,175 +183,77 @@ gist.list.all-liked-by: 'Все фрагменты, понравившиеся %
|
||||
gist.list.all-forked-by: 'Все фрагменты, ответвлённые %s'
|
||||
gist.list.all-from: 'Все фрагменты от %s'
|
||||
gist.search.found: 'фрагментов найдено'
|
||||
gist.search.no-results: 'Фрагменты не найдены'
|
||||
gist.search.no-results: 'Не найден ни один фрагмент'
|
||||
gist.search.help.user: 'фрагментов создано пользователем'
|
||||
gist.search.help.title: 'фрагментов с указанным заголовком'
|
||||
gist.search.help.filename: 'фрагменты содержащие файлы с указанным именем'
|
||||
gist.search.help.extension: 'фрагменты, содержащие файлы с указанным расширением'
|
||||
gist.search.help.language: 'фрагменты, содержащие файлы с указанным языком'
|
||||
gist.forks.for: 'Форки фрагмента %s'
|
||||
gist.likes.for: 'Лайки фрагмента %s'
|
||||
gist.revision-of: 'Ревизия фрагмента %s'
|
||||
settings.link-gitlab-account: 'Привязать учётную запись Gitlab'
|
||||
settings.unlink-gitlab-account: 'Отвязать учётную запись GitHub'
|
||||
settings.change-username: 'Сменить имя пользователя'
|
||||
settings.create-password: 'Создать пароль'
|
||||
settings.create-password-help: 'Создайте пароль для входа в Opengist по HTTP'
|
||||
settings.change-password: 'Сменить пароль'
|
||||
settings.change-password-help: 'Смените пароль для входа в Opengist по HTTP'
|
||||
settings.password-label-title: 'Пароль'
|
||||
error.page-not-found: 'Страница не найдена'
|
||||
error.bad-request: 'Неверный запрос'
|
||||
error.signup-disabled: 'Регистрация недоступна'
|
||||
error.signup-disabled-form: 'Регистрация через форму недоступна'
|
||||
error.login-disabled-form: 'Авторизация через форму недоступна'
|
||||
error.complete-oauth-login: 'Не удалось завершить авторизацию пользователя: %s'
|
||||
error.oauth-unsupported: 'Провайдер OAuth не поддерживается'
|
||||
error.cannot-bind-data: 'Не удалось обработать данные'
|
||||
error.invalid-number: 'Некорректное числовое значение'
|
||||
error.invalid-character-unescaped: 'Обнаружен неверный неэкранированный символ'
|
||||
admin.invitations: 'Инвайты'
|
||||
admin.invitations.create: 'Создать инвайт'
|
||||
admin.actions.sync-previews: 'Обновить предпросмотры всех фрагментов'
|
||||
admin.actions.reset-hooks: 'Сбросить хуки Git-сервера для всех репозиториев'
|
||||
admin.actions.index-gists: 'Проиндексировать все фрагменты'
|
||||
validation.should-not-be-empty: 'Поле %s не должно быть пустым'
|
||||
admin.invitations.help: 'Инвайты используются для создания аккаунта, даже когда регистрация запрещена.'
|
||||
admin.invitations.max_uses: 'Максимальное количество использований'
|
||||
admin.invitations.expires_at: 'Истекает'
|
||||
admin.invitations.code: 'Код'
|
||||
admin.invitations.copy_link: 'Скопировать ссылку'
|
||||
admin.invitations.uses: 'Количество использований'
|
||||
admin.invitations.expired: 'Истёк'
|
||||
flash.admin.user-deleted: 'Пользователь удалён'
|
||||
flash.admin.gist-deleted: 'Фрагмент удалён'
|
||||
flash.admin.invitation-created: 'Приглашение создано'
|
||||
flash.admin.invitation-deleted: 'Приглашение удалено'
|
||||
flash.admin.sync-fs: 'Выполняется синхронизация репозиториев с файловой системой…'
|
||||
flash.admin.sync-db: 'Выполняется синхронизация репозиториев с базой данных…'
|
||||
flash.admin.git-gc: 'Сборка мусора в репозиториях…'
|
||||
flash.admin.sync-previews: 'Обновление предпросмотров фрагментов…'
|
||||
flash.admin.reset-hooks: 'Пересоздание Git-хуков для всех репозиториев…'
|
||||
flash.admin.index-gists: 'Выполняется индексация фрагментов…'
|
||||
flash.auth.username-exists: 'Такое имя пользователя уже занято'
|
||||
flash.auth.invalid-credentials: 'Некорректные данные для входа'
|
||||
flash.auth.account-linked-oauth: 'Учётная запись связана с %s'
|
||||
flash.auth.account-unlinked-oauth: 'Учётная запись отключена от %s'
|
||||
flash.auth.user-sshkeys-not-retrievable: 'Не удалось получить SSH-ключи пользователя'
|
||||
flash.auth.user-sshkeys-not-created: 'Не удалось создать SSH-ключ'
|
||||
flash.auth.must-be-logged-in: 'Для доступа к фрагментам необходимо войти в аккаунт'
|
||||
flash.gist.visibility-changed: 'Видимость фрагмента изменена'
|
||||
flash.gist.deleted: 'Фрагмент удалён'
|
||||
flash.gist.fork-own-gist: 'Нельзя создать форк собственного фрагмента'
|
||||
flash.gist.forked: 'Фрагмент создан как форк'
|
||||
flash.user.email-updated: 'Адрес электронной почты обновлён'
|
||||
flash.user.invalid-ssh-key: 'Неверный SSH-ключ'
|
||||
flash.user.ssh-key-added: 'SSH-ключ добавлен'
|
||||
flash.user.ssh-key-deleted: 'SSH-ключ удалён'
|
||||
flash.user.password-updated: 'Пароль обновлён'
|
||||
flash.user.username-updated: 'Имя пользователя обновлено'
|
||||
validation.is-too-long: 'Поле %s слишком длинное'
|
||||
validation.should-not-include-sub-directory: 'Поле %s не должно содержать подкаталоги'
|
||||
validation.should-only-contain-alphanumeric-characters: 'Поле %s должно содержать только буквы и цифры'
|
||||
validation.should-only-contain-alphanumeric-characters-and-dashes: 'Поле %s должно содержать только буквы, цифры и дефисы'
|
||||
validation.not-enough: 'Недостаточно %s'
|
||||
validation.invalid: 'Неверный %s'
|
||||
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: Перенос строк
|
||||
gist.search.help.title: ''
|
||||
gist.search.help.filename: ''
|
||||
gist.search.help.extension: ''
|
||||
gist.search.help.language: ''
|
||||
gist.forks.for: ''
|
||||
gist.likes.for: ''
|
||||
gist.revision-of: ''
|
||||
settings.link-gitlab-account: ''
|
||||
settings.unlink-gitlab-account: ''
|
||||
settings.change-username: ''
|
||||
settings.create-password: ''
|
||||
settings.create-password-help: ''
|
||||
settings.change-password: ''
|
||||
settings.change-password-help: ''
|
||||
settings.password-label-title: ''
|
||||
error.page-not-found: ''
|
||||
error.bad-request: ''
|
||||
error.signup-disabled: ''
|
||||
error.signup-disabled-form: ''
|
||||
error.login-disabled-form: ''
|
||||
error.complete-oauth-login: ''
|
||||
error.oauth-unsupported: ''
|
||||
error.cannot-bind-data: ''
|
||||
error.invalid-number: ''
|
||||
error.invalid-character-unescaped: ''
|
||||
admin.invitations: ''
|
||||
admin.invitations.create: ''
|
||||
admin.actions.sync-previews: ''
|
||||
admin.actions.reset-hooks: ''
|
||||
admin.actions.index-gists: ''
|
||||
validation.should-not-be-empty: ''
|
||||
admin.invitations.help: ''
|
||||
admin.invitations.max_uses: ''
|
||||
admin.invitations.expires_at: ''
|
||||
admin.invitations.code: ''
|
||||
admin.invitations.copy_link: ''
|
||||
admin.invitations.uses: ''
|
||||
admin.invitations.expired: ''
|
||||
flash.admin.user-deleted: ''
|
||||
flash.admin.gist-deleted: ''
|
||||
flash.admin.invitation-created: ''
|
||||
flash.admin.invitation-deleted: ''
|
||||
flash.admin.sync-fs: ''
|
||||
flash.admin.sync-db: ''
|
||||
flash.admin.git-gc: ''
|
||||
flash.admin.sync-previews: ''
|
||||
flash.admin.reset-hooks: ''
|
||||
flash.admin.index-gists: ''
|
||||
flash.auth.username-exists: ''
|
||||
flash.auth.invalid-credentials: ''
|
||||
flash.auth.account-linked-oauth: ''
|
||||
flash.auth.account-unlinked-oauth: ''
|
||||
flash.auth.user-sshkeys-not-retrievable: ''
|
||||
flash.auth.user-sshkeys-not-created: ''
|
||||
flash.auth.must-be-logged-in: ''
|
||||
flash.gist.visibility-changed: ''
|
||||
flash.gist.deleted: ''
|
||||
flash.gist.fork-own-gist: ''
|
||||
flash.gist.forked: ''
|
||||
flash.user.email-updated: ''
|
||||
flash.user.invalid-ssh-key: ''
|
||||
flash.user.ssh-key-added: ''
|
||||
flash.user.ssh-key-deleted: ''
|
||||
flash.user.password-updated: ''
|
||||
flash.user.username-updated: ''
|
||||
validation.is-too-long: ''
|
||||
validation.should-not-include-sub-directory: ''
|
||||
validation.should-only-contain-alphanumeric-characters: ''
|
||||
validation.should-only-contain-alphanumeric-characters-and-dashes: ''
|
||||
validation.not-enough: ''
|
||||
validation.invalid: ''
|
||||
html.title.admin-panel: ''
|
||||
|
||||
@@ -27,9 +27,8 @@ func (r HighlightedFile) InternalType() string {
|
||||
|
||||
type RenderedGist struct {
|
||||
*db.Gist
|
||||
Lines []string
|
||||
HTML string
|
||||
PreviewMimeType *git.MimeType
|
||||
Lines []string
|
||||
HTML string
|
||||
}
|
||||
|
||||
func highlightFile(file *git.File) (HighlightedFile, error) {
|
||||
@@ -77,18 +76,6 @@ func HighlightGistPreview(gist *db.Gist) (RenderedGist, error) {
|
||||
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()
|
||||
lexer := newLexer(gist.PreviewFilename)
|
||||
if lexer.Config().Name == "markdown" {
|
||||
|
||||
@@ -3,7 +3,6 @@ package gist
|
||||
import (
|
||||
"archive/zip"
|
||||
"bytes"
|
||||
"net/url"
|
||||
"strconv"
|
||||
|
||||
"github.com/thomiceli/opengist/internal/db"
|
||||
@@ -20,23 +19,8 @@ func RawFile(ctx *context.Context) error {
|
||||
if file == nil {
|
||||
return ctx.NotFound("File not found")
|
||||
}
|
||||
|
||||
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")
|
||||
ctx.Response().Header().Set("Content-Type", file.MimeType.ContentType)
|
||||
ctx.Response().Header().Set("Content-Disposition", "inline; filename=\""+file.Filename+"\"")
|
||||
return ctx.PlainText(200, file.Content)
|
||||
}
|
||||
|
||||
@@ -52,9 +36,8 @@ func DownloadFile(ctx *context.Context) error {
|
||||
}
|
||||
|
||||
ctx.Response().Header().Set("Content-Type", file.MimeType.ContentType)
|
||||
ctx.Response().Header().Set("Content-Disposition", "attachment; filename=\""+url.PathEscape(file.Filename)+"\"")
|
||||
ctx.Response().Header().Set("Content-Disposition", "attachment; filename="+file.Filename)
|
||||
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))
|
||||
if err != nil {
|
||||
return ctx.ErrorRes(500, "Error downloading the file", err)
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
package metrics
|
||||
|
||||
import (
|
||||
"github.com/labstack/echo-contrib/echoprometheus"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/prometheus/client_golang/prometheus/promauto"
|
||||
"github.com/thomiceli/opengist/internal/config"
|
||||
"github.com/thomiceli/opengist/internal/db"
|
||||
"github.com/thomiceli/opengist/internal/web/context"
|
||||
)
|
||||
|
||||
var (
|
||||
// Using promauto to automatically register metrics with the default registry
|
||||
countUsersGauge prometheus.Gauge
|
||||
countGistsGauge prometheus.Gauge
|
||||
countSSHKeysGauge prometheus.Gauge
|
||||
@@ -14,52 +18,84 @@ var (
|
||||
metricsInitialized bool = false
|
||||
)
|
||||
|
||||
// initMetrics initializes metrics if they're not already initialized
|
||||
func initMetrics() {
|
||||
if metricsInitialized {
|
||||
return
|
||||
}
|
||||
|
||||
countUsersGauge = promauto.NewGauge(
|
||||
prometheus.GaugeOpts{
|
||||
Name: "opengist_users_total",
|
||||
Help: "Total number of users",
|
||||
},
|
||||
)
|
||||
// Only initialize metrics if they're enabled
|
||||
if config.C.MetricsEnabled {
|
||||
countUsersGauge = promauto.NewGauge(
|
||||
prometheus.GaugeOpts{
|
||||
Name: "opengist_users_total",
|
||||
Help: "Total number of users",
|
||||
},
|
||||
)
|
||||
|
||||
countGistsGauge = promauto.NewGauge(
|
||||
prometheus.GaugeOpts{
|
||||
Name: "opengist_gists_total",
|
||||
Help: "Total number of gists",
|
||||
},
|
||||
)
|
||||
countGistsGauge = promauto.NewGauge(
|
||||
prometheus.GaugeOpts{
|
||||
Name: "opengist_gists_total",
|
||||
Help: "Total number of gists",
|
||||
},
|
||||
)
|
||||
|
||||
countSSHKeysGauge = promauto.NewGauge(
|
||||
prometheus.GaugeOpts{
|
||||
Name: "opengist_ssh_keys_total",
|
||||
Help: "Total number of SSH keys",
|
||||
},
|
||||
)
|
||||
countSSHKeysGauge = promauto.NewGauge(
|
||||
prometheus.GaugeOpts{
|
||||
Name: "opengist_ssh_keys_total",
|
||||
Help: "Total number of SSH keys",
|
||||
},
|
||||
)
|
||||
|
||||
metricsInitialized = true
|
||||
metricsInitialized = true
|
||||
}
|
||||
}
|
||||
|
||||
// updateMetrics refreshes all metric values from the database
|
||||
func updateMetrics() {
|
||||
if !metricsInitialized {
|
||||
// Only update metrics if they're enabled
|
||||
if !config.C.MetricsEnabled || !metricsInitialized {
|
||||
return
|
||||
}
|
||||
|
||||
// Update users count
|
||||
countUsers, err := db.CountAll(&db.User{})
|
||||
if err == nil {
|
||||
countUsersGauge.Set(float64(countUsers))
|
||||
}
|
||||
|
||||
// Update gists count
|
||||
countGists, err := db.CountAll(&db.Gist{})
|
||||
if err == nil {
|
||||
countGistsGauge.Set(float64(countGists))
|
||||
}
|
||||
|
||||
// Update SSH keys count
|
||||
countKeys, err := db.CountAll(&db.SSHKey{})
|
||||
if err == nil {
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -1,50 +0,0 @@
|
||||
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)
|
||||
}
|
||||
@@ -1,75 +0,0 @@
|
||||
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")
|
||||
}
|
||||
@@ -38,7 +38,8 @@ func (s *Server) registerMiddlewares() {
|
||||
s.echo.Use(Middleware(dataInit).toEcho())
|
||||
s.echo.Use(Middleware(locale).toEcho())
|
||||
if config.C.MetricsEnabled {
|
||||
s.echo.Use(echoprometheus.NewMiddleware("opengist"))
|
||||
p := echoprometheus.NewMiddleware("opengist")
|
||||
s.echo.Use(p)
|
||||
}
|
||||
|
||||
s.echo.Pre(middleware.MethodOverrideWithConfig(middleware.MethodOverrideConfig{
|
||||
@@ -206,9 +207,6 @@ func makeCheckRequireLogin(isSingleGistAccess bool) Middleware {
|
||||
return next(ctx)
|
||||
}
|
||||
|
||||
if getUserByToken(ctx) != nil {
|
||||
return next(ctx)
|
||||
}
|
||||
allow, err := auth.ShouldAllowUnauthenticatedGistAccess(handlers.ContextAuthInfo{Context: ctx}, isSingleGistAccess)
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("Failed to check if unauthenticated access is allowed")
|
||||
@@ -329,39 +327,6 @@ func loadSettings(ctx *context.Context) error {
|
||||
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 {
|
||||
return func(ctx *context.Context) error {
|
||||
currUser := ctx.User
|
||||
@@ -388,12 +353,7 @@ func gistInit(next Handler) Handler {
|
||||
|
||||
if gist.Private == db.PrivateVisibility {
|
||||
if currUser == nil || currUser.ID != gist.UserID {
|
||||
// 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")
|
||||
}
|
||||
return ctx.NotFound("Gist not found")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -192,9 +192,6 @@ func (s *Server) setFuncMap() {
|
||||
"humanDate": func(t int64) string {
|
||||
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 {
|
||||
if theme == nil {
|
||||
return "auto"
|
||||
|
||||
@@ -17,6 +17,7 @@ import (
|
||||
"github.com/thomiceli/opengist/internal/web/handlers/gist"
|
||||
"github.com/thomiceli/opengist/internal/web/handlers/git"
|
||||
"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/public"
|
||||
)
|
||||
@@ -33,6 +34,10 @@ func (s *Server) registerRoutes() {
|
||||
|
||||
r.GET("/healthcheck", health.Healthcheck)
|
||||
|
||||
if config.C.MetricsEnabled {
|
||||
r.GET("/metrics", metrics.Metrics)
|
||||
}
|
||||
|
||||
r.GET("/register", auth.Register)
|
||||
r.POST("/register", auth.ProcessRegister)
|
||||
r.GET("/login", auth.Login)
|
||||
@@ -62,9 +67,6 @@ func (s *Server) registerRoutes() {
|
||||
sA.DELETE("/account", settings.AccountDeleteProcess)
|
||||
sA.POST("/ssh-keys", settings.SshKeysProcess)
|
||||
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.PUT("/password", settings.PasswordProcess)
|
||||
sA.PUT("/username", settings.UsernameProcess)
|
||||
|
||||
@@ -1,448 +0,0 @@
|
||||
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)
|
||||
}
|
||||
@@ -1,15 +1,15 @@
|
||||
package test
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/http/httptest"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/thomiceli/opengist/internal/db"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -41,12 +41,22 @@ var (
|
||||
// - Total number of SSH keys
|
||||
//
|
||||
// The test follows these steps:
|
||||
// 1. Sets up test environment
|
||||
// 2. Registers and logs in an admin user
|
||||
// 3. Creates a gist and adds an SSH key
|
||||
// 4. Creates a metrics server and queries the /metrics endpoint
|
||||
// 5. Verifies the reported metrics match expected values
|
||||
// 1. Enables metrics via environment variable
|
||||
// 2. Sets up test environment
|
||||
// 3. Registers and logs in an admin user
|
||||
// 4. Creates a gist and adds an SSH key
|
||||
// 5. Queries the metrics endpoint
|
||||
// 6. Verifies the reported metrics match expected values
|
||||
//
|
||||
// Environment variables:
|
||||
// - OG_METRICS_ENABLED: Set to "true" for this test
|
||||
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)
|
||||
defer Teardown(t, s)
|
||||
|
||||
@@ -62,16 +72,12 @@ func TestMetrics(t *testing.T) {
|
||||
err = s.Request("POST", "/settings/ssh-keys", SSHKey, 302)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create a metrics server and query it
|
||||
metricsServer := NewTestMetricsServer()
|
||||
var metricsRes http.Response
|
||||
err = s.Request("GET", "/metrics", nil, 200, &metricsRes)
|
||||
require.NoError(t, err)
|
||||
|
||||
req := httptest.NewRequest("GET", "/metrics", nil)
|
||||
w := httptest.NewRecorder()
|
||||
metricsServer.ServeHTTP(w, req)
|
||||
|
||||
require.Equal(t, 200, w.Code)
|
||||
|
||||
body, err := io.ReadAll(w.Body)
|
||||
body, err := io.ReadAll(metricsRes.Body)
|
||||
defer metricsRes.Body.Close()
|
||||
require.NoError(t, err)
|
||||
|
||||
lines := strings.Split(string(body), "\n")
|
||||
|
||||
@@ -22,7 +22,6 @@ import (
|
||||
"github.com/thomiceli/opengist/internal/config"
|
||||
"github.com/thomiceli/opengist/internal/db"
|
||||
"github.com/thomiceli/opengist/internal/git"
|
||||
"github.com/thomiceli/opengist/internal/web/handlers/metrics"
|
||||
"github.com/thomiceli/opengist/internal/web/server"
|
||||
)
|
||||
|
||||
@@ -51,10 +50,6 @@ func (s *TestServer) stop() {
|
||||
}
|
||||
|
||||
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
|
||||
if method == http.MethodPost || method == http.MethodPut {
|
||||
values := structToURLValues(data)
|
||||
@@ -68,10 +63,6 @@ func (s *TestServer) RequestWithHeaders(method, uri string, data interface{}, ex
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
}
|
||||
|
||||
for key, value := range headers {
|
||||
req.Header.Set(key, value)
|
||||
}
|
||||
|
||||
if s.sessionCookie != "" {
|
||||
req.AddCookie(&http.Cookie{Name: "session", Value: s.sessionCookie})
|
||||
}
|
||||
@@ -128,9 +119,6 @@ func structToURLValues(s interface{}) url.Values {
|
||||
if field.Type.Kind() == reflect.Int {
|
||||
fieldValue := rValue.Field(i).Int()
|
||||
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 {
|
||||
fieldValue := rValue.Field(i).Interface().([]string)
|
||||
for _, va := range fieldValue {
|
||||
@@ -252,7 +240,3 @@ type invitationAdmin struct {
|
||||
nbMax string `form:"nbMax"`
|
||||
expiredAtUnix string `form:"expiredAtUnix"`
|
||||
}
|
||||
|
||||
func NewTestMetricsServer() *metrics.Server {
|
||||
return metrics.NewServer()
|
||||
}
|
||||
|
||||
32
package-lock.json
generated
32
package-lock.json
generated
@@ -10,9 +10,9 @@
|
||||
"@codemirror/commands": "^6.10.1",
|
||||
"@codemirror/lang-javascript": "^6.2.4",
|
||||
"@codemirror/language": "^6.12.1",
|
||||
"@codemirror/state": "^6.5.4",
|
||||
"@codemirror/state": "^6.5.3",
|
||||
"@codemirror/text": "^0.19.6",
|
||||
"@codemirror/view": "^6.39.11",
|
||||
"@codemirror/view": "^6.39.8",
|
||||
"@tailwindcss/forms": "^0.5.11",
|
||||
"@tailwindcss/typography": "^0.5.19",
|
||||
"@tailwindcss/vite": "^4.1.18",
|
||||
@@ -20,12 +20,12 @@
|
||||
"github-markdown-css": "^5.8.1",
|
||||
"highlight.js": "^11.11.1",
|
||||
"jdenticon": "^3.3.0",
|
||||
"katex": "^0.16.28",
|
||||
"katex": "^0.16.27",
|
||||
"marked": "^17.0.1",
|
||||
"nodemon": "^3.1.11",
|
||||
"pdfobject": "^2.3.1",
|
||||
"tailwindcss": "^4.1.18",
|
||||
"vite": "^7.3.1"
|
||||
"vite": "^7.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@catppuccin/highlightjs": {
|
||||
@@ -133,9 +133,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/state": {
|
||||
"version": "6.5.4",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.5.4.tgz",
|
||||
"integrity": "sha512-8y7xqG/hpB53l25CIoit9/ngxdfoG+fx+V3SHBrinnhOtLvKHRyAJJuHzkWrR4YXXLX8eXBsejgAAxHUOdW1yw==",
|
||||
"version": "6.5.3",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.5.3.tgz",
|
||||
"integrity": "sha512-MerMzJzlXogk2fxWFU1nKp36bY5orBG59HnPiz0G9nLRebWa0zXuv2siH6PLIHBvv5TH8CkQRqjBs0MlxCZu+A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -150,9 +150,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@codemirror/view": {
|
||||
"version": "6.39.11",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.39.11.tgz",
|
||||
"integrity": "sha512-bWdeR8gWM87l4DB/kYSF9A+dVackzDb/V56Tq7QVrQ7rn86W0rgZFtlL3g3pem6AeGcb9NQNoy3ao4WpW4h5tQ==",
|
||||
"version": "6.39.8",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.39.8.tgz",
|
||||
"integrity": "sha512-1rASYd9Z/mE3tkbC9wInRlCNyCkSn+nLsiQKZhEDUUJiUfs/5FHDpCUDaQpoTIaNGeDc6/bhaEAyLmeEucEFPw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -1764,9 +1764,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/katex": {
|
||||
"version": "0.16.28",
|
||||
"resolved": "https://registry.npmjs.org/katex/-/katex-0.16.28.tgz",
|
||||
"integrity": "sha512-YHzO7721WbmAL6Ov1uzN/l5mY5WWWhJBSW+jq4tkfZfsxmo1hu6frS0EOswvjBUnWE6NtjEs48SFn5CQESRLZg==",
|
||||
"version": "0.16.27",
|
||||
"resolved": "https://registry.npmjs.org/katex/-/katex-0.16.27.tgz",
|
||||
"integrity": "sha512-aeQoDkuRWSqQN6nSvVCEFvfXdqo1OQiCmmW1kc9xSdjutPv7BGO7pqY9sQRJpMOGrEdfDgF2TfRXe5eUAD2Waw==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
"https://opencollective.com/katex",
|
||||
@@ -2464,9 +2464,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "7.3.1",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz",
|
||||
"integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==",
|
||||
"version": "7.3.0",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-7.3.0.tgz",
|
||||
"integrity": "sha512-dZwN5L1VlUBewiP6H9s2+B3e3Jg96D0vzN+Ry73sOefebhYr9f94wwkMNN/9ouoU8pV1BqA1d1zGk8928cx0rg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
|
||||
@@ -15,9 +15,9 @@
|
||||
"@codemirror/commands": "^6.10.1",
|
||||
"@codemirror/lang-javascript": "^6.2.4",
|
||||
"@codemirror/language": "^6.12.1",
|
||||
"@codemirror/state": "^6.5.4",
|
||||
"@codemirror/state": "^6.5.3",
|
||||
"@codemirror/text": "^0.19.6",
|
||||
"@codemirror/view": "^6.39.11",
|
||||
"@codemirror/view": "^6.39.8",
|
||||
"@tailwindcss/forms": "^0.5.11",
|
||||
"@tailwindcss/typography": "^0.5.19",
|
||||
"@tailwindcss/vite": "^4.1.18",
|
||||
@@ -25,11 +25,11 @@
|
||||
"github-markdown-css": "^5.8.1",
|
||||
"highlight.js": "^11.11.1",
|
||||
"jdenticon": "^3.3.0",
|
||||
"katex": "^0.16.28",
|
||||
"katex": "^0.16.27",
|
||||
"marked": "^17.0.1",
|
||||
"nodemon": "^3.1.11",
|
||||
"pdfobject": "^2.3.1",
|
||||
"tailwindcss": "^4.1.18",
|
||||
"vite": "^7.3.1"
|
||||
"vite": "^7.3.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import '../ts/ipynb.ts';
|
||||
import PDFObject from 'pdfobject';
|
||||
|
||||
document.querySelectorAll<HTMLElement>('.table-code').forEach((el) => {
|
||||
el.addEventListener('click', event => {
|
||||
@@ -75,4 +76,8 @@ if (document.getElementById('gist').dataset.own) {
|
||||
checkboxes.forEach((el: HTMLButtonElement) => {
|
||||
el.disabled = true;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
document.querySelectorAll(".pdf").forEach((el) => {
|
||||
PDFObject.embed(el.dataset.src || "", el);
|
||||
})
|
||||
@@ -2,14 +2,9 @@ import '../css/tailwind.css';
|
||||
import '../img/favicon-32.png';
|
||||
import '../img/opengist.svg';
|
||||
import jdenticon from 'jdenticon/standalone';
|
||||
import PDFObject from 'pdfobject';
|
||||
|
||||
jdenticon.update("[data-jdenticon-value]")
|
||||
|
||||
document.querySelectorAll(".pdf").forEach((el) => {
|
||||
PDFObject.embed(el.dataset.src || "", el);
|
||||
})
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
document.getElementById('user-btn')?.addEventListener("click" , () => {
|
||||
document.getElementById('user-menu')!.classList.toggle('hidden');
|
||||
|
||||
2
templates/base/settings_header.html
vendored
2
templates/base/settings_header.html
vendored
@@ -15,8 +15,6 @@
|
||||
{{ 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
|
||||
{{ 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
|
||||
{{ 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>
|
||||
|
||||
95
templates/pages/settings_tokens.html
vendored
95
templates/pages/settings_tokens.html
vendored
@@ -1,95 +0,0 @@
|
||||
{{ 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" .}}
|
||||
32
templates/partials/_gist_preview.html
vendored
32
templates/partials/_gist_preview.html
vendored
@@ -60,33 +60,7 @@
|
||||
<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">
|
||||
{{ if .gist.PreviewFilename }}
|
||||
{{ 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 .gist.Preview }}
|
||||
{{ if isMarkdown .gist.PreviewFilename }}
|
||||
<div class="chroma preview markdown markdown-body p-8">{{ .gist.HTML | safe }}</div>
|
||||
{{ else }}
|
||||
@@ -106,8 +80,8 @@
|
||||
</table>
|
||||
{{ end }}
|
||||
{{ else }}
|
||||
<div class="pl-4 py-0.5 text-xs"><p>{{ .locale.Tr "gist.preview-non-available" }}</p></div>
|
||||
{{ end }}
|
||||
<div class="pl-4 py-0.5 text-xs"><p>{{ .locale.Tr "gist.preview-non-available" }}</p></div>
|
||||
{{ end }}
|
||||
{{ else }}
|
||||
<div class="pl-4 py-0.5 text-xs"><p>{{ .locale.Tr "gist.no-content" }}</p></div>
|
||||
{{ end }}
|
||||
|
||||
Reference in New Issue
Block a user