mirror of
https://github.com/thomiceli/opengist.git
synced 2026-06-23 04:10:18 +00:00
Compare commits
72 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| cac21689cf | |||
| 28736d6b66 | |||
| 499f9c67b9 | |||
| 9d9b54a5e1 | |||
| 1ba588c90e | |||
| a2e4734e36 | |||
| 66f2793f8b | |||
| 2bba402787 | |||
| daeeed3dc5 | |||
| 06708eb351 | |||
| b41a80a335 | |||
| b43789943a | |||
| bf3257faa8 | |||
| 2946de2505 | |||
| c1ca19aec9 | |||
| 34e5a16a26 | |||
| 31bc25e569 | |||
| 3b8d947ad8 | |||
| 8e462397f4 | |||
| 5c23d7feed | |||
| 690f151592 | |||
| 8da72b9545 | |||
| f3c38ddbbb | |||
| 8a6f2d82ff | |||
| bfd75a9d58 | |||
| eef6029c95 | |||
| c60094c778 | |||
| f67bff59c3 | |||
| ec26888487 | |||
| 57d76151fd | |||
| c2ee390841 | |||
| d6fc346e70 | |||
| 4e977077ba | |||
| f865b2b099 | |||
| d26221de54 | |||
| e91139d3ec | |||
| 279da52899 | |||
| 5ad01a3304 | |||
| 1944502d14 | |||
| 2d7261ac83 | |||
| 50f2980c10 | |||
| 2e68b6893b | |||
| 9b68f08c62 | |||
| dfabdb403a | |||
| 4da067ab60 | |||
| a8339ff6bd | |||
| 7a5cdd1565 | |||
| 00dcb53e3a | |||
| f8b3bbce6a | |||
| a697b0f273 | |||
| 33cbfb0904 | |||
| dfea4eb435 | |||
| d796eeba98 | |||
| 4ab38f24c8 | |||
| e1d1b01d40 | |||
| 3c967729cc | |||
| 36bc576893 | |||
| c074d60d1d | |||
| 840a852ed2 | |||
| 34c0b0b3e2 | |||
| 093a4cb4a8 | |||
| f037206f41 | |||
| 6c22adba4e | |||
| bb63ecd048 | |||
| 6a61b720ab | |||
| 829cd68879 | |||
| 42490f2995 | |||
| f83018ebf2 | |||
| b097cfcbc0 | |||
| 7b1048ec30 | |||
| ce39df1030 | |||
| 07ba04244b |
@@ -1,17 +1,9 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
- package-ecosystem: "gomod"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
- package-ecosystem: "docker"
|
||||
directory: "/"
|
||||
schedule:
|
||||
|
||||
@@ -16,17 +16,15 @@ jobs:
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: '20'
|
||||
node-version: '26'
|
||||
|
||||
- name: Install JS dependencies
|
||||
run: |
|
||||
npm install vitepress@1.3.4 tailwindcss@3.4.10
|
||||
- name: Install dependencies
|
||||
run: npm install
|
||||
working-directory: docs
|
||||
|
||||
- name: Build docs
|
||||
run: |
|
||||
cd docs
|
||||
npx tailwindcss -i .vitepress/theme/style.css -o .vitepress/theme/theme.css -c .vitepress/tailwind.config.js
|
||||
npm run docs:build
|
||||
run: npm run build
|
||||
working-directory: docs
|
||||
|
||||
- name: Push to docs repository
|
||||
run: |
|
||||
|
||||
+30
-10
@@ -9,6 +9,7 @@ on:
|
||||
pull_request:
|
||||
paths-ignore:
|
||||
- '**.yml'
|
||||
- '**.yaml'
|
||||
- '**.md'
|
||||
|
||||
jobs:
|
||||
@@ -19,15 +20,15 @@ jobs:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Set up Go 1.25
|
||||
- name: Set up Go 1.26
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: "1.25"
|
||||
go-version: "1.26"
|
||||
|
||||
- name: Lint
|
||||
uses: golangci/golangci-lint-action@v9
|
||||
with:
|
||||
version: v2.5
|
||||
version: v2.12
|
||||
args: --timeout=20m --disable=errcheck
|
||||
|
||||
- name: Format
|
||||
@@ -40,10 +41,10 @@ jobs:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Set up Go 1.25
|
||||
- name: Set up Go 1.26
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: "1.25"
|
||||
go-version: "1.26"
|
||||
|
||||
- name: Check Go modules
|
||||
run: make go_mod check_changes
|
||||
@@ -57,7 +58,7 @@ jobs:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: ["ubuntu-latest"]
|
||||
go: ["1.25"]
|
||||
go: ["1.26"]
|
||||
database: [postgres, mysql]
|
||||
include:
|
||||
- database: postgres
|
||||
@@ -83,6 +84,18 @@ jobs:
|
||||
--health-interval 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 5
|
||||
meilisearch:
|
||||
image: getmeili/meilisearch:latest
|
||||
ports:
|
||||
- 47700:7700
|
||||
env:
|
||||
MEILI_NO_ANALYTICS: true
|
||||
MEILI_ENV: development
|
||||
options: >-
|
||||
--health-cmd "curl -sf http://localhost:7700/health"
|
||||
--health-interval 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 5
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
@@ -94,14 +107,16 @@ jobs:
|
||||
|
||||
- name: Run tests
|
||||
run: make test TEST_DB_TYPE=${{ matrix.database }}
|
||||
env:
|
||||
OG_TEST_MEILI_HOST: http://localhost:47700
|
||||
|
||||
test:
|
||||
name: Test
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: ["ubuntu-latest", "macOS-latest", "windows-latest"]
|
||||
go: ["1.25"]
|
||||
os: ["ubuntu-latest", "macOS-latest"]
|
||||
go: ["1.26"]
|
||||
database: ["sqlite"]
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
@@ -122,17 +137,22 @@ jobs:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: ["ubuntu-latest", "macOS-latest", "windows-latest"]
|
||||
go: ["1.25"]
|
||||
go: ["1.26"]
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Set up Go 1.25
|
||||
- name: Set up Go 1.26
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: ${{ matrix.go }}
|
||||
|
||||
- name: Set up Node.js 26
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: '26'
|
||||
|
||||
- name: Build
|
||||
shell: bash
|
||||
run: make
|
||||
|
||||
@@ -2,6 +2,7 @@ name: Build / Deploy Helm Chart
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
workflow_call:
|
||||
|
||||
jobs:
|
||||
build-and-deploy:
|
||||
@@ -11,7 +12,7 @@ jobs:
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Set up Helm
|
||||
uses: azure/setup-helm@v4.3.1
|
||||
uses: azure/setup-helm@v5.0.0
|
||||
with:
|
||||
version: 'latest'
|
||||
|
||||
@@ -36,14 +37,14 @@ jobs:
|
||||
|
||||
- name: Push to docs repository
|
||||
run: |
|
||||
git clone https://${{ secrets.DOCS_REPO_TOKEN }}@github.com/${{ secrets.DOCS_REPO }}.git target-repo
|
||||
git clone https://${{ secrets.STATIC_REPO_TOKEN }}@github.com/${{ secrets.STATIC_REPO }}.git target-repo
|
||||
mkdir -p target-repo/helm
|
||||
cp helm/*.tgz target-repo/helm/
|
||||
cp helm/index.yaml target-repo/helm/
|
||||
cp helm/*.tgz target-repo/srv/helm/
|
||||
cp helm/index.yaml target-repo/srv/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
|
||||
git push
|
||||
|
||||
@@ -13,16 +13,21 @@ jobs:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Set up Go 1.25
|
||||
- name: Set up Go 1.26
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: "1.25"
|
||||
go-version: "1.26"
|
||||
|
||||
- name: Set up Node.js 26
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: '26'
|
||||
|
||||
- name: Cross compile build
|
||||
run: make all_crosscompile
|
||||
|
||||
- name: Upload Release Assets
|
||||
uses: softprops/action-gh-release@v2
|
||||
uses: softprops/action-gh-release@v3
|
||||
with:
|
||||
files: |
|
||||
build/*.tar.gz
|
||||
@@ -42,7 +47,7 @@ jobs:
|
||||
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
uses: docker/metadata-action@v6
|
||||
with:
|
||||
images: |
|
||||
ghcr.io/thomiceli/opengist
|
||||
@@ -54,26 +59,26 @@ jobs:
|
||||
type=semver,pattern={{version}}
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
uses: docker/setup-qemu-action@v4
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
uses: docker/setup-buildx-action@v4
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v6
|
||||
uses: docker/build-push-action@v7
|
||||
with:
|
||||
context: .
|
||||
platforms: linux/amd64,linux/arm64
|
||||
@@ -81,4 +86,9 @@ jobs:
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
cache-to: type=gha,mode=max
|
||||
|
||||
helm-build-release:
|
||||
needs: docker-build-release
|
||||
uses: ./.github/workflows/helm.yml
|
||||
secrets: inherit
|
||||
|
||||
@@ -1,5 +1,52 @@
|
||||
# Changelog
|
||||
|
||||
## [1.13.1](https://github.com/thomiceli/opengist/compare/v1.13.0...v1.13.1) - 2026-06-10
|
||||
See here how to [update](https://opengist.io/docs/update) Opengist.
|
||||
|
||||
### Fixed
|
||||
- Embedding fix vertical scrolling and improve padding (#714)
|
||||
- Fix CSS url for json embed url (#715)
|
||||
|
||||
## [1.13.0](https://github.com/thomiceli/opengist/compare/v1.12.2...v1.13.0) - 2026-06-09
|
||||
See here how to [update](https://opengist.io/docs/update) Opengist.
|
||||
|
||||
### Added
|
||||
- REST API (#707) (#711) (#702)
|
||||
- Limit display if there is too much files in one gist (#701)
|
||||
- Topics git push option in post-receive hook (#698)
|
||||
- Allow embedding Gists for a certain file only (#709)
|
||||
- Arabic Translation (#706)
|
||||
|
||||
### Fixed
|
||||
- Server SSH key generation (#708)
|
||||
|
||||
### Other
|
||||
- Update deps Golang, JS, Docker deps (#713)
|
||||
- New docs website (#710)
|
||||
|
||||
## [1.12.2](https://github.com/thomiceli/opengist/compare/v1.12.1...v1.12.2) - 2026-03-14
|
||||
See here how to [update](https://opengist.io/docs/update) Opengist.
|
||||
|
||||
### Added
|
||||
- Search all fields (#622)
|
||||
- Display a form to create an Opengist account coming from a OAuth provider (#623)
|
||||
- Rebuild search index in admin options (#647)
|
||||
|
||||
### Fixed
|
||||
- Clean file path names on file creation (#624)
|
||||
- Support UTF-8 on gist download (#625)
|
||||
- CSRF skipper only for GET *.js request (#627)
|
||||
- Async-loaded gist embed scripts (#630)
|
||||
- Make gists username/urls case insensitive in URLS (#641)
|
||||
- Improve code search and index tests (#663)
|
||||
- Translation strings (#659)
|
||||
- Gitea avatar URL on OAuth (#674)
|
||||
|
||||
### [Helm Chart](helm/opengist)
|
||||
- Add environment variables and secrets to statefulset (#644)
|
||||
|
||||
> Admins of Opengist instances may want to run "Rebuild search index" in the admin panel.
|
||||
|
||||
## [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.
|
||||
|
||||
|
||||
+4
-5
@@ -1,4 +1,4 @@
|
||||
FROM alpine:3.22 AS base
|
||||
FROM alpine:3.23 AS base
|
||||
|
||||
RUN apk update && \
|
||||
apk add --no-cache \
|
||||
@@ -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.26.4-alpine3.23 /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:26.3.0-alpine3.23 /usr/local/ /usr/local/
|
||||
ENV NODE_PATH="/usr/local/lib/node_modules"
|
||||
ENV PATH="/usr/local/bin:${PATH}"
|
||||
|
||||
@@ -46,12 +46,11 @@ FROM base AS build
|
||||
RUN make
|
||||
|
||||
|
||||
FROM alpine:3.22 AS prod
|
||||
FROM alpine:3.23 AS prod
|
||||
|
||||
RUN apk update && \
|
||||
apk add --no-cache \
|
||||
shadow \
|
||||
openssh-server \
|
||||
curl \
|
||||
git
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
.PHONY: all all_crosscompile install build_frontend build_backend build build_crosscompile build_docker build_dev_docker run_dev_docker watch_frontend watch_backend watch clean clean_docker check_changes go_mod fmt test check-tr
|
||||
.PHONY: all all_crosscompile install build_frontend build_backend build build_crosscompile build_docker build_dev_docker run_dev_docker watch_frontend watch_backend watch clean clean_docker check_changes go_mod fmt test check-tr update_js_deps update_go_deps
|
||||
|
||||
# Specify the name of your Go binary output
|
||||
BINARY_NAME := opengist
|
||||
@@ -75,4 +75,12 @@ test:
|
||||
@OPENGIST_TEST_DB=$(TEST_DB_TYPE) go test ./... -p 1
|
||||
|
||||
check-tr:
|
||||
@bash ./scripts/check-translations.sh
|
||||
@bash ./scripts/check-translations.sh
|
||||
|
||||
update_js_deps:
|
||||
@echo "Updating NPM dependencies..."
|
||||
@npx npm-check-updates -u && npm install
|
||||
|
||||
update_go_deps:
|
||||
@echo "Updating Go dependencies..."
|
||||
@go get -u ./... && go mod tidy
|
||||
@@ -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.13
|
||||
```
|
||||
|
||||
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.13
|
||||
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.13.1/opengist1.13.1-linux-amd64.tar.gz
|
||||
|
||||
tar xzvf opengist1.12.1-linux-amd64.tar.gz
|
||||
tar xzvf opengist1.13.1-linux-amd64.tar.gz
|
||||
cd opengist
|
||||
chmod +x opengist
|
||||
./opengist # with or without `--config config.yml`
|
||||
|
||||
+7
-3
@@ -32,6 +32,10 @@ index.meili.host:
|
||||
# Set the API key for the Meiliseach server
|
||||
index.meili.api-key:
|
||||
|
||||
# Set the default search fields. Can contain multiple fields (e.g., `content,username`).
|
||||
# Fields: content,user,title,description,filename,extension,language,topic. Default: content
|
||||
search.default: content
|
||||
|
||||
# Default branch name used by Opengist when initializing Git repositories.
|
||||
# If not set, uses the Git default branch name. See https://git-scm.com/book/en/v2/Getting-Started-First-Time-Git-Setup#_new_default_branch
|
||||
git.default-branch:
|
||||
@@ -52,6 +56,9 @@ http.port: 6157
|
||||
# Enable or disable git operations (clone, pull, push) via HTTP (either `true` or `false`). Default: true
|
||||
http.git-enabled: true
|
||||
|
||||
# Enable or disable the REST API (either `true` or `false`). Default: true
|
||||
api.enabled: true
|
||||
|
||||
# File permissions for Unix socket (octal format). Default: 0666
|
||||
unix-socket-permissions: 0666
|
||||
|
||||
@@ -84,9 +91,6 @@ ssh.port: 2222
|
||||
# If not set, uses the URL from the request
|
||||
ssh.external-domain:
|
||||
|
||||
# Path or alias to ssh-keygen executable. Default: ssh-keygen
|
||||
ssh.keygen-executable: ssh-keygen
|
||||
|
||||
# OAuth2 configuration
|
||||
# The callback/redirect URL must be http://opengist.url/oauth/<github|gitlab|gitea|openid-connect>/callback
|
||||
|
||||
|
||||
+14
-5
@@ -1,5 +1,18 @@
|
||||
#!/bin/sh
|
||||
|
||||
load_secrets() {
|
||||
if [ -f "/run/secrets/opengist_secrets" ]; then
|
||||
set -a
|
||||
. /run/secrets/opengist_secrets
|
||||
set +a
|
||||
fi
|
||||
}
|
||||
|
||||
if [ "$(id -u)" -ne 0 ]; then
|
||||
load_secrets
|
||||
exec env HOME=/opengist OG_OPENGIST_HOME=/opengist /app/opengist/opengist --config /config.yml
|
||||
fi
|
||||
|
||||
export USER=opengist
|
||||
UID=${UID:-1000}
|
||||
GID=${GID:-1000}
|
||||
@@ -9,10 +22,6 @@ usermod -o -u "$UID" $USER
|
||||
chown -R "$USER:$USER" /opengist
|
||||
chown -R "$USER:$USER" /config.yml
|
||||
|
||||
if [ -f "/run/secrets/opengist_secrets" ]; then
|
||||
set -a
|
||||
. /run/secrets/opengist_secrets
|
||||
set +a
|
||||
fi
|
||||
load_secrets
|
||||
|
||||
exec su $USER -c "OG_OPENGIST_HOME=/opengist /app/opengist/opengist --config /config.yml"
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
node_modules
|
||||
.vitepress/dist
|
||||
.vitepress/cache
|
||||
+228
-66
@@ -1,12 +1,176 @@
|
||||
import {defineConfig} from 'vitepress'
|
||||
import tailwindcss from '@tailwindcss/vite'
|
||||
import {listOperations, listSchemas, readSpecRaw} from './openapi'
|
||||
|
||||
// Serve the raw OpenAPI spec verbatim at /docs/api/openapi.yaml — read from the
|
||||
// Go source tree so it never drifts. Dev via middleware, build via asset emit.
|
||||
let isSsrBuild = false
|
||||
const openapiRawPlugin = {
|
||||
name: 'opengist:openapi-raw',
|
||||
configResolved(c: any) {
|
||||
isSsrBuild = !!c.build?.ssr
|
||||
},
|
||||
configureServer(server: any) {
|
||||
server.middlewares.use((req: any, res: any, next: any) => {
|
||||
if (req.url === '/docs/api/openapi.yaml') {
|
||||
res.setHeader('Content-Type', 'text/yaml; charset=utf-8')
|
||||
res.end(readSpecRaw())
|
||||
} else next()
|
||||
})
|
||||
},
|
||||
generateBundle(this: any) {
|
||||
if (isSsrBuild) return
|
||||
this.emitFile({type: 'asset', fileName: 'docs/api/openapi.yaml', source: readSpecRaw()})
|
||||
}
|
||||
}
|
||||
|
||||
// Build the API Reference sidebar from the OpenAPI spec: one entry per
|
||||
// operation (grouped by tag), then one entry per schema, plus the Overview.
|
||||
const apiOps = listOperations()
|
||||
const apiTags = [...new Set(apiOps.map(op => op.tag))]
|
||||
const apiSidebar = [
|
||||
{text: 'Overview', link: '/docs/api'},
|
||||
...apiTags.map(tag => ({
|
||||
text: tag,
|
||||
collapsed: false,
|
||||
items: apiOps
|
||||
.filter(op => op.tag === tag)
|
||||
.map(op => ({text: op.summary, link: `/docs/api/${op.id}`})),
|
||||
})),
|
||||
{
|
||||
text: 'Schemas',
|
||||
collapsed: true,
|
||||
items: listSchemas().map(name => ({text: name, link: `/docs/api/schemas/${name}`})),
|
||||
},
|
||||
]
|
||||
|
||||
// Main docs sidebar, shared by the /docs/ pages and the standalone /changelog page.
|
||||
const docsSidebar = [
|
||||
{
|
||||
text: '', items: [
|
||||
{text: 'Introduction', link: '/docs'},
|
||||
{text: 'Installation', link: '/docs/installation', items: [
|
||||
{text: 'Docker', link: '/docs/installation/docker'},
|
||||
{text: 'Kubernetes', link: '/docs/installation/kubernetes'},
|
||||
{text: 'Binary', link: '/docs/installation/binary'},
|
||||
{text: 'Source', link: '/docs/installation/source'},
|
||||
],
|
||||
collapsed: true
|
||||
},
|
||||
{text: 'Update', link: '/docs/update'},
|
||||
], collapsed: false
|
||||
},
|
||||
{
|
||||
text: 'Configuration', base: '/docs/configuration', items: [
|
||||
{text: 'Configure Opengist', link: '/configure'},
|
||||
{text: 'Databases', items: [
|
||||
{text: 'SQLite', link: '/databases/sqlite'},
|
||||
{text: 'PostgreSQL', link: '/databases/postgresql'},
|
||||
{text: 'MySQL', link: '/databases/mysql'},
|
||||
], collapsed: true
|
||||
},
|
||||
{text: 'OAuth Providers', link: '/oauth-providers'},
|
||||
{text: 'Custom assets', link: '/custom-assets'},
|
||||
{text: 'Custom links', link: '/custom-links'},
|
||||
{text: 'Cheat Sheet', link: '/cheat-sheet'},
|
||||
{text: 'Metrics', link: '/metrics'},
|
||||
{text: 'Admin panel', link: '/admin-panel'},
|
||||
], collapsed: false
|
||||
},
|
||||
{
|
||||
text: 'Usage', base: '/docs/usage', items: [
|
||||
{text: 'Init via Git', link: '/init-via-git'},
|
||||
{text: 'Embed Gist', link: '/embed'},
|
||||
{text: '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'},
|
||||
], collapsed: false
|
||||
},
|
||||
{
|
||||
text: 'Administration', base: '/docs/administration', items: [
|
||||
{text: 'Run with systemd', link: '/run-with-systemd'},
|
||||
{text: 'Reverse proxy', items: [
|
||||
{text: 'Nginx', link: '/nginx-reverse-proxy'},
|
||||
{text: 'Traefik', link: '/traefik-reverse-proxy'},
|
||||
], collapsed: true},
|
||||
{text: 'Fail2ban', link: '/fail2ban-setup'},
|
||||
{text: 'Healthcheck', link: '/healthcheck'},
|
||||
], collapsed: false
|
||||
},
|
||||
{
|
||||
text: 'Contributing', base: '/docs/contributing', items: [
|
||||
{text: 'Community', link: '/community'},
|
||||
{text: 'Development', link: '/development'},
|
||||
], collapsed: false
|
||||
},
|
||||
]
|
||||
|
||||
// https://vitepress.dev/reference/site-config
|
||||
const hostname = 'https://opengist.io'
|
||||
const ogImage = `${hostname}/opengist-demo.png`
|
||||
|
||||
export default defineConfig({
|
||||
title: "Opengist",
|
||||
description: "Documention for Opengist",
|
||||
description: "Documentation for Opengist — a self-hosted pastebin powered by Git.",
|
||||
lang: 'en-US',
|
||||
sitemap: {
|
||||
hostname,
|
||||
},
|
||||
markdown: {
|
||||
config(md) {
|
||||
// Strip the "See here how to update Opengist." note that appears
|
||||
// under each version in the embedded CHANGELOG (source stays intact).
|
||||
md.core.ruler.push('strip_changelog_update_note', (state) => {
|
||||
const t = state.tokens
|
||||
for (let i = t.length - 1; i >= 0; i--) {
|
||||
if (
|
||||
t[i].type === 'inline' &&
|
||||
/See here how to .*update.* Opengist\./i.test(t[i].content) &&
|
||||
t[i - 1]?.type === 'paragraph_open'
|
||||
) {
|
||||
t.splice(i - 1, 3)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// Turn "#123" references into links to the matching GitHub PR/issue.
|
||||
md.inline.ruler.before('text', 'github_ref', (state, silent) => {
|
||||
const start = state.pos
|
||||
if (state.src.charCodeAt(start) !== 0x23 /* # */) return false
|
||||
// Require a boundary before '#' (avoid URL fragments like page#1).
|
||||
const prev = start > 0 ? state.src[start - 1] : ''
|
||||
if (prev && /[0-9A-Za-z]/.test(prev)) return false
|
||||
|
||||
let pos = start + 1
|
||||
while (pos < state.posMax && /[0-9]/.test(state.src[pos])) pos++
|
||||
if (pos === start + 1) return false // no digits
|
||||
// Reject things like a hex color "#1a2" (digit run followed by a letter).
|
||||
if (pos < state.posMax && /[A-Za-z]/.test(state.src[pos])) return false
|
||||
|
||||
const num = state.src.slice(start + 1, pos)
|
||||
if (!silent) {
|
||||
const open = state.push('link_open', 'a', 1)
|
||||
open.attrs = [
|
||||
['href', `https://github.com/thomiceli/opengist/pull/${num}`],
|
||||
['target', '_blank'],
|
||||
['rel', 'noreferrer'],
|
||||
]
|
||||
state.push('text', '', 0).content = `#${num}`
|
||||
state.push('link_close', 'a', -1)
|
||||
}
|
||||
state.pos = pos
|
||||
return true
|
||||
})
|
||||
},
|
||||
},
|
||||
vite: {
|
||||
plugins: [tailwindcss(), openapiRawPlugin]
|
||||
},
|
||||
rewrites: {
|
||||
'index.md': 'index.md',
|
||||
'introduction.md': 'docs/index.md',
|
||||
'changelog.md': 'changelog.md',
|
||||
':path(.*)': 'docs/:path'
|
||||
},
|
||||
themeConfig: {
|
||||
@@ -14,78 +178,39 @@ export default defineConfig({
|
||||
logo: 'https://raw.githubusercontent.com/thomiceli/opengist/master/public/img/opengist.svg',
|
||||
logoLink: '/',
|
||||
nav: [
|
||||
{ text: 'Demo', link: 'https://demo.opengist.io' },
|
||||
{ text: 'Translate', link: 'https://tr.opengist.io' }
|
||||
{ text: 'Docs', link: '/docs', activeMatch: '^/docs' },
|
||||
{
|
||||
text: 'Resources',
|
||||
items: [
|
||||
{ text: 'Demo', link: 'https://demo.opengist.io' },
|
||||
{ text: 'Translate', link: 'https://tr.opengist.io' },
|
||||
]
|
||||
},
|
||||
{
|
||||
text: 'v1.13.1',
|
||||
items: [
|
||||
{ text: 'Changelog', link: '/changelog' },
|
||||
{ text: 'Releases', link: 'https://github.com/thomiceli/opengist/releases' },
|
||||
]
|
||||
}
|
||||
],
|
||||
|
||||
sidebar: {
|
||||
'/docs/': [
|
||||
{
|
||||
text: '', items: [
|
||||
{text: 'Introduction', link: '/docs'},
|
||||
{text: 'Installation', link: '/docs/installation', items: [
|
||||
{text: 'Docker', link: '/docs/installation/docker'},
|
||||
{text: 'Kubernetes', link: '/docs/installation/kubernetes'},
|
||||
{text: 'Binary', link: '/docs/installation/binary'},
|
||||
{text: 'Source', link: '/docs/installation/source'},
|
||||
],
|
||||
collapsed: true
|
||||
},
|
||||
{text: 'Update', link: '/docs/update'},
|
||||
], collapsed: false
|
||||
},
|
||||
{
|
||||
text: 'Configuration', base: '/docs/configuration', items: [
|
||||
{text: 'Configure Opengist', link: '/configure'},
|
||||
{text: 'Databases', items: [
|
||||
{text: 'SQLite', link: '/databases/sqlite'},
|
||||
{text: 'PostgreSQL', link: '/databases/postgresql'},
|
||||
{text: 'MySQL', link: '/databases/mysql'},
|
||||
], collapsed: true
|
||||
},
|
||||
{text: 'OAuth Providers', link: '/oauth-providers'},
|
||||
{text: 'Custom assets', link: '/custom-assets'},
|
||||
{text: 'Custom links', link: '/custom-links'},
|
||||
{text: 'Cheat Sheet', link: '/cheat-sheet'},
|
||||
{text: 'Metrics', link: '/metrics'},
|
||||
{text: 'Admin panel', link: '/admin-panel'},
|
||||
], collapsed: false
|
||||
},
|
||||
{
|
||||
text: 'Usage', base: '/docs/usage', items: [
|
||||
{text: 'Init via Git', link: '/init-via-git'},
|
||||
{text: 'Embed Gist', link: '/embed'},
|
||||
{text: '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'},
|
||||
], collapsed: false
|
||||
},
|
||||
{
|
||||
text: 'Administration', base: '/docs/administration', items: [
|
||||
{text: 'Run with systemd', link: '/run-with-systemd'},
|
||||
{text: 'Reverse proxy', items: [
|
||||
{text: 'Nginx', link: '/nginx-reverse-proxy'},
|
||||
{text: 'Traefik', link: '/traefik-reverse-proxy'},
|
||||
], collapsed: true},
|
||||
{text: 'Fail2ban', link: '/fail2ban-setup'},
|
||||
{text: 'Healthcheck', link: '/healthcheck'},
|
||||
], collapsed: false
|
||||
},
|
||||
{
|
||||
text: 'Contributing', base: '/docs/contributing', items: [
|
||||
{text: 'Community', link: '/community'},
|
||||
{text: 'Development', link: '/development'},
|
||||
], collapsed: false
|
||||
},
|
||||
|
||||
]},
|
||||
// Standalone API Reference section: its own sidebar, separate from
|
||||
// the main docs navigation. Longest-prefix match means /docs/api
|
||||
// uses this instead of the '/docs/' sidebar below.
|
||||
'/docs/api': apiSidebar,
|
||||
'/docs/': docsSidebar,
|
||||
// Standalone /changelog page reuses the main docs sidebar.
|
||||
'/changelog': docsSidebar,
|
||||
},
|
||||
|
||||
socialLinks: [
|
||||
{icon: 'github', link: 'https://github.com/thomiceli/opengist'}
|
||||
{icon: 'github', link: 'https://github.com/thomiceli/opengist'},
|
||||
{icon: 'discord', link: 'https://discord.gg/9Pm3X5scZT'}
|
||||
],
|
||||
editLink: {
|
||||
pattern: 'https://github.com/thomiceli/opengist/edit/stable/docs/:path'
|
||||
pattern: 'https://github.com/thomiceli/opengist/edit/master/docs/:path'
|
||||
},
|
||||
// @ts-ignore
|
||||
lastUpdated: true,
|
||||
@@ -93,6 +218,43 @@ export default defineConfig({
|
||||
},
|
||||
head: [
|
||||
['link', {rel: 'icon', href: '/favicon.svg'}],
|
||||
['meta', {name: 'theme-color', content: '#3c79e2'}],
|
||||
// Site-wide Open Graph / Twitter Card defaults (per-page values are
|
||||
// refined in transformPageData below).
|
||||
['meta', {property: 'og:type', content: 'website'}],
|
||||
['meta', {property: 'og:site_name', content: 'Opengist'}],
|
||||
['meta', {property: 'og:image', content: ogImage}],
|
||||
['meta', {name: 'twitter:card', content: 'summary_large_image'}],
|
||||
['meta', {name: 'twitter:image', content: ogImage}],
|
||||
],
|
||||
// Per-page meta: canonical URL, description, and Open Graph / Twitter tags
|
||||
// built from each page's title + description.
|
||||
transformPageData(pageData) {
|
||||
// Mirror the `rewrites` above to compute the deployed path.
|
||||
let out
|
||||
if (pageData.relativePath === 'index.md') out = 'index.md'
|
||||
else if (pageData.relativePath === 'introduction.md') out = 'docs/index.md'
|
||||
else if (pageData.relativePath === 'changelog.md') out = 'changelog.md'
|
||||
else out = `docs/${pageData.relativePath}`
|
||||
const path = out.replace(/(^|\/)index\.md$/, '$1').replace(/\.md$/, '.html')
|
||||
const url = `${hostname}/${path}`
|
||||
|
||||
const base = pageData.title || 'Opengist'
|
||||
const title = base.includes('Opengist') ? base : `${base} | Opengist`
|
||||
const description =
|
||||
pageData.description ||
|
||||
pageData.frontmatter.description ||
|
||||
'Documentation for Opengist — a self-hosted pastebin powered by Git.'
|
||||
|
||||
pageData.frontmatter.head ??= []
|
||||
pageData.frontmatter.head.push(
|
||||
['link', {rel: 'canonical', href: url}],
|
||||
['meta', {property: 'og:title', content: title}],
|
||||
['meta', {property: 'og:description', content: description}],
|
||||
['meta', {property: 'og:url', content: url}],
|
||||
['meta', {name: 'twitter:title', content: title}],
|
||||
['meta', {name: 'twitter:description', content: description}],
|
||||
)
|
||||
},
|
||||
ignoreDeadLinks: true
|
||||
})
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import { defineLoader } from 'vitepress'
|
||||
import { buildApiData, SPEC_FILE, type ApiData } from './openapi'
|
||||
|
||||
// `data` is populated at build time and importable from any component.
|
||||
declare const data: ApiData
|
||||
export { data }
|
||||
export type { ApiData }
|
||||
|
||||
export default defineLoader({
|
||||
// Rebuild whenever the spec changes (path is relative to this file).
|
||||
watch: [SPEC_FILE],
|
||||
load: (): Promise<ApiData> => buildApiData()
|
||||
})
|
||||
@@ -0,0 +1,441 @@
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { parse } from 'yaml'
|
||||
|
||||
// The authoritative spec lives in the Go source tree.
|
||||
export const SPEC_FILE = '../../internal/web/handlers/api/openapi.yaml'
|
||||
const SPEC_URL = new URL(SPEC_FILE, import.meta.url)
|
||||
const METHOD_ORDER = ['get', 'post', 'put', 'patch', 'delete']
|
||||
|
||||
// ---- shapes consumed by the renderer components (all JSON-serializable) ----
|
||||
export interface ApiData {
|
||||
info: { title: string; version: string; descriptionHtml: string }
|
||||
authHtml: string
|
||||
servers: { url: string; description: string }[]
|
||||
groups: Group[]
|
||||
schemas: SchemaDef[]
|
||||
}
|
||||
export interface Group {
|
||||
name: string
|
||||
descriptionHtml: string
|
||||
operations: Operation[]
|
||||
}
|
||||
export interface Operation {
|
||||
id: string
|
||||
method: string
|
||||
path: string
|
||||
summary: string
|
||||
descriptionHtml: string
|
||||
auth: { anonymous: boolean; anyToken: boolean; scopes: string[] }
|
||||
parameters: Param[]
|
||||
requestBody: (BodyTable & { required: boolean }) | null
|
||||
responses: ResponseDef[]
|
||||
// Synthesized samples for the right-hand code panel (pre-highlighted HTML).
|
||||
sample: { curlHtml: string; responseStatus: string; responseHtml: string | null }
|
||||
}
|
||||
interface Param {
|
||||
name: string
|
||||
in: string
|
||||
required: boolean
|
||||
typeHtml: string
|
||||
descriptionHtml: string
|
||||
}
|
||||
interface Field {
|
||||
name: string
|
||||
typeHtml: string
|
||||
required: boolean
|
||||
descriptionHtml: string
|
||||
depth: number
|
||||
}
|
||||
// A body schema flattened into a field table (the inlined replacement for a
|
||||
// schema link): its type label, whether it's an array, and its fields.
|
||||
interface BodyTable {
|
||||
label: string
|
||||
arrayOf: boolean
|
||||
note: string
|
||||
rows: Field[]
|
||||
}
|
||||
interface ResponseDef {
|
||||
status: string
|
||||
descriptionHtml: string
|
||||
headers: { name: string; typeHtml: string; descriptionHtml: string }[]
|
||||
typeLabel: string | null
|
||||
}
|
||||
// A named component schema, rendered one-per-page.
|
||||
export interface SchemaDef {
|
||||
name: string
|
||||
descriptionHtml: string
|
||||
note: string
|
||||
rows: Field[]
|
||||
}
|
||||
|
||||
// Lightweight index used by config.mts (sidebar) and the dynamic-route
|
||||
// generator. Synchronous and markdown-free so it's cheap to call.
|
||||
export interface OpIndex {
|
||||
id: string
|
||||
method: string
|
||||
path: string
|
||||
summary: string
|
||||
tag: string
|
||||
}
|
||||
|
||||
function readSpec(): any {
|
||||
return parse(readSpecRaw())
|
||||
}
|
||||
|
||||
// Raw spec contents, for serving the file verbatim at /docs/api/openapi.yaml.
|
||||
export function readSpecRaw(): string {
|
||||
return readFileSync(fileURLToPath(SPEC_URL), 'utf-8')
|
||||
}
|
||||
|
||||
// Walk paths × methods in a stable order, yielding each operation.
|
||||
function eachOperation(spec: any, cb: (op: any, method: string, path: string, item: any) => void) {
|
||||
for (const [path, item] of Object.entries<any>(spec.paths)) {
|
||||
for (const method of METHOD_ORDER) {
|
||||
const op = item[method]
|
||||
if (op) cb(op, method, path, item)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function listOperations(): OpIndex[] {
|
||||
const spec = readSpec()
|
||||
const ops: OpIndex[] = []
|
||||
eachOperation(spec, (op, method, path) => {
|
||||
ops.push({
|
||||
id: op.operationId,
|
||||
method: method.toUpperCase(),
|
||||
path,
|
||||
summary: op.summary || op.operationId,
|
||||
tag: op.tags?.[0] || (spec.tags?.[0]?.name ?? '')
|
||||
})
|
||||
})
|
||||
return ops
|
||||
}
|
||||
|
||||
// Names of the component schemas, for the sidebar and per-schema routes.
|
||||
export function listSchemas(): string[] {
|
||||
return Object.keys(readSpec().components?.schemas || {})
|
||||
}
|
||||
|
||||
export async function buildApiData(): Promise<ApiData> {
|
||||
// Lazily pulled in so this module can be imported from config.mts without
|
||||
// dragging the markdown renderer into the config load path.
|
||||
const { createMarkdownRenderer } = await import('vitepress')
|
||||
const spec = readSpec()
|
||||
const md = await createMarkdownRenderer(fileURLToPath(new URL('..', import.meta.url)))
|
||||
// Block rendering is async because Shiki highlighting in VitePress 2 is
|
||||
// resolved via markdown-it-async; renderInline carries no code fences.
|
||||
const html = (s?: string) => (s ? md.renderAsync(s) : Promise.resolve(''))
|
||||
const inline = (s?: string) => (s ? md.renderInline(s) : '')
|
||||
// Drop the "**Required ...**" scope line from operation descriptions — the
|
||||
// Authorization header row already documents the required scopes.
|
||||
const stripRequired = (s?: string) =>
|
||||
s
|
||||
? s
|
||||
.split('\n')
|
||||
.filter((l) => !l.trimStart().startsWith('**Required'))
|
||||
.join('\n')
|
||||
: s
|
||||
|
||||
const refName = (ref: string) => ref.split('/').pop() as string
|
||||
const resolve = (ref: string): any =>
|
||||
ref
|
||||
.replace(/^#\//, '')
|
||||
.split('/')
|
||||
.reduce((acc, key) => acc?.[key], spec)
|
||||
const deref = (obj: any): any => (obj?.$ref ? resolve(obj.$ref) : obj)
|
||||
|
||||
// Named schema references link to their per-schema page.
|
||||
const link = (name: string) => `<a href="/docs/api/schemas/${name}"><code>${name}</code></a>`
|
||||
|
||||
const typeHtml = (s: any): string => {
|
||||
if (!s) return '<code>any</code>'
|
||||
if (s.$ref) return link(refName(s.$ref))
|
||||
if (s.oneOf) return s.oneOf.map(typeHtml).join(' | ')
|
||||
if (s.allOf) return s.allOf.map(typeHtml).join(' & ')
|
||||
if (s.type === 'array') return typeHtml(s.items) + '[]'
|
||||
if (s.type === 'null') return '<code>null</code>'
|
||||
if (s.type === 'object' && s.additionalProperties)
|
||||
return `map<string, ${typeHtml(s.additionalProperties)}>`
|
||||
let t = s.type || 'object'
|
||||
if (s.format) t += ` <${s.format}>`
|
||||
let out = `<code>${t}</code>`
|
||||
if (Array.isArray(s.enum))
|
||||
out += `: ${s.enum.map((e: any) => `<code>${e}</code>`).join(' | ')}`
|
||||
return out
|
||||
}
|
||||
|
||||
const constraints = (s: any): string => {
|
||||
if (!s) return ''
|
||||
const bits: string[] = []
|
||||
if (s.default !== undefined) bits.push(`default: ${s.default}`)
|
||||
if (s.minimum !== undefined) bits.push(`min: ${s.minimum}`)
|
||||
if (s.maximum !== undefined) bits.push(`max: ${s.maximum}`)
|
||||
if (s.pattern) bits.push(`pattern: <code>${s.pattern}</code>`)
|
||||
return bits.length ? ` <em>(${bits.join(', ')})</em>` : ''
|
||||
}
|
||||
|
||||
// Flatten a schema to its full property set, resolving $ref and allOf
|
||||
// (including inherited fields) so each body can render a complete table.
|
||||
// `seen` guards against recursive schemas.
|
||||
const flatten = (
|
||||
s: any,
|
||||
seen: string[] = []
|
||||
): { props: Record<string, any>; required: string[]; addl: any } => {
|
||||
const acc = { props: {} as Record<string, any>, required: [] as string[], addl: null as any }
|
||||
if (!s) return acc
|
||||
if (s.$ref) {
|
||||
const name = refName(s.$ref)
|
||||
return seen.includes(name) ? acc : flatten(resolve(s.$ref), [...seen, name])
|
||||
}
|
||||
for (const part of s.allOf || []) {
|
||||
const f = flatten(part, seen)
|
||||
Object.assign(acc.props, f.props)
|
||||
acc.required.push(...f.required)
|
||||
if (f.addl) acc.addl = f.addl
|
||||
}
|
||||
if (s.properties) Object.assign(acc.props, s.properties)
|
||||
if (s.required) acc.required.push(...s.required)
|
||||
if (s.additionalProperties) acc.addl = s.additionalProperties
|
||||
return acc
|
||||
}
|
||||
|
||||
// Turn a request/response body schema into a renderable field table.
|
||||
const bodyTable = (schema: any): BodyTable => {
|
||||
let s = schema
|
||||
let arrayOf = false
|
||||
while (s?.$ref) s = resolve(s.$ref)
|
||||
if (s?.type === 'array') {
|
||||
arrayOf = true
|
||||
s = s.items
|
||||
while (s?.$ref) s = resolve(s.$ref)
|
||||
}
|
||||
const label = schema ? typeHtml(schema) : '<code>any</code>'
|
||||
const f = flatten(s)
|
||||
const rows: Field[] = Object.entries<any>(f.props).map(([pname, ps]) => ({
|
||||
name: pname,
|
||||
typeHtml: typeHtml(ps),
|
||||
required: f.required.includes(pname),
|
||||
descriptionHtml: inline(ps.description),
|
||||
depth: 0
|
||||
}))
|
||||
const note = f.addl ? `Additional properties: map<string, ${typeHtml(f.addl)}>` : ''
|
||||
return { label, arrayOf, note, rows }
|
||||
}
|
||||
|
||||
// Recursively expand an object/array/map schema into indented field rows,
|
||||
// descending into nested objects (used for the request body table). `seen`
|
||||
// tracks $ref names on the current branch to break recursive schemas.
|
||||
const childrenOf = (schema: any, depth: number, seen: string[]): Field[] => {
|
||||
let s = schema
|
||||
const branch = [...seen]
|
||||
while (s) {
|
||||
if (s.$ref) {
|
||||
const n = refName(s.$ref)
|
||||
if (branch.includes(n)) return []
|
||||
branch.push(n)
|
||||
s = resolve(s.$ref)
|
||||
} else if (s.type === 'array') s = s.items
|
||||
else if (s.oneOf) s = s.oneOf.find((o: any) => o.type !== 'null') ?? s.oneOf[0]
|
||||
else break
|
||||
}
|
||||
if (!s) return []
|
||||
const f = flatten(s, branch)
|
||||
const entries = Object.entries<any>(f.props)
|
||||
// A pure map (additionalProperties only): descend into the value's fields.
|
||||
if (entries.length === 0 && f.addl) return childrenOf(f.addl, depth, branch)
|
||||
const out: Field[] = []
|
||||
for (const [name, ps] of entries) {
|
||||
out.push({
|
||||
name,
|
||||
typeHtml: typeHtml(ps),
|
||||
required: f.required.includes(name),
|
||||
descriptionHtml: inline(ps.description),
|
||||
depth
|
||||
})
|
||||
out.push(...childrenOf(ps, depth + 1, branch))
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// Synthesize a representative value for a schema (no examples in the spec).
|
||||
// `seen` tracks $ref names on the current branch to break recursive schemas.
|
||||
const sample = (schema: any, seen: string[] = []): any => {
|
||||
if (!schema) return null
|
||||
if (schema.$ref) {
|
||||
const name = refName(schema.$ref)
|
||||
if (seen.includes(name)) return null
|
||||
return sample(resolve(schema.$ref), [...seen, name])
|
||||
}
|
||||
if (schema.example !== undefined) return schema.example
|
||||
if (schema.default !== undefined) return schema.default
|
||||
if (Array.isArray(schema.enum)) return schema.enum[0]
|
||||
if (schema.allOf)
|
||||
return schema.allOf.reduce((acc: any, s: any) => Object.assign(acc, sample(s, seen)), {})
|
||||
if (schema.oneOf) {
|
||||
const pick = schema.oneOf.find((s: any) => s.type !== 'null') ?? schema.oneOf[0]
|
||||
return sample(pick, seen)
|
||||
}
|
||||
switch (schema.type) {
|
||||
case 'array':
|
||||
return [sample(schema.items, seen)]
|
||||
case 'object': {
|
||||
const obj: Record<string, any> = {}
|
||||
for (const [k, v] of Object.entries<any>(schema.properties || {})) obj[k] = sample(v, seen)
|
||||
if (schema.additionalProperties && typeof schema.additionalProperties === 'object')
|
||||
obj['<key>'] = sample(schema.additionalProperties, seen)
|
||||
return obj
|
||||
}
|
||||
case 'integer':
|
||||
case 'number':
|
||||
return 1
|
||||
case 'boolean':
|
||||
return true
|
||||
case 'null':
|
||||
return null
|
||||
default:
|
||||
if (schema.format === 'date-time') return '2024-01-01T00:00:00Z'
|
||||
if (schema.format === 'uri') return 'https://opengist.example/...'
|
||||
if (schema.format === 'binary') return '<binary data>'
|
||||
return 'string'
|
||||
}
|
||||
}
|
||||
|
||||
const HOST = 'https://opengist.example.com'
|
||||
const base = spec.servers?.[0]?.url ?? ''
|
||||
const fence = (lang: string, code: string) => md.renderAsync(`\`\`\`${lang}\n${code}\n\`\`\``)
|
||||
|
||||
const tagDefs: any[] = spec.tags || []
|
||||
const groups: Group[] = []
|
||||
for (const t of tagDefs)
|
||||
groups.push({ name: t.name, descriptionHtml: await html(t.description), operations: [] })
|
||||
const groupByName = new Map(groups.map((g) => [g.name, g]))
|
||||
|
||||
for (const [path, item] of Object.entries<any>(spec.paths)) {
|
||||
const pathParams: any[] = (item.parameters || []).map(deref)
|
||||
for (const method of METHOD_ORDER) {
|
||||
const op = item[method]
|
||||
if (!op) continue
|
||||
const allParams: any[] = [...pathParams, ...(op.parameters || []).map(deref)]
|
||||
|
||||
const sec: any[] = op.security ?? spec.security ?? []
|
||||
const auth = { anonymous: false, anyToken: false, scopes: [] as string[] }
|
||||
const scopeSet = new Set<string>()
|
||||
for (const req of sec) {
|
||||
if (Object.keys(req).length === 0) auth.anonymous = true
|
||||
else {
|
||||
const arr: string[] = req.bearerAuth || []
|
||||
if (arr.length === 0) auth.anyToken = true
|
||||
else arr.forEach((s) => scopeSet.add(s))
|
||||
}
|
||||
}
|
||||
auth.scopes = [...scopeSet]
|
||||
|
||||
const parameters: Param[] = allParams.map((p) => ({
|
||||
name: p.name,
|
||||
in: p.in,
|
||||
required: !!p.required,
|
||||
typeHtml: typeHtml(p.schema),
|
||||
descriptionHtml: inline(p.description) + constraints(p.schema)
|
||||
}))
|
||||
|
||||
let requestBody: Operation['requestBody'] = null
|
||||
let requestSchema: any = null
|
||||
if (op.requestBody) {
|
||||
const rb = deref(op.requestBody)
|
||||
requestSchema = rb.content?.['application/json']?.schema
|
||||
// Expand nested object fields inline so the whole body shape is visible.
|
||||
requestBody = {
|
||||
required: !!rb.required,
|
||||
...bodyTable(requestSchema),
|
||||
rows: childrenOf(requestSchema, 0, [])
|
||||
}
|
||||
}
|
||||
|
||||
// ---- curl request sample: -X, then --url, then headers / body ----
|
||||
const query = allParams
|
||||
.filter((p) => p.in === 'query' && (p.required || p.schema?.default !== undefined))
|
||||
.map((p) => `${p.name}=${p.schema?.default ?? sample(p.schema)}`)
|
||||
.join('&')
|
||||
const url = `${HOST}${base}${path}${query ? '?' + query : ''}`
|
||||
const curlLines = [
|
||||
`curl -X ${method.toUpperCase()}`,
|
||||
` --url "${url}"`,
|
||||
` -H "Authorization: Bearer og_xxxxxxxx"`
|
||||
]
|
||||
if (requestSchema) {
|
||||
curlLines.push(` -H "Content-Type: application/json"`)
|
||||
curlLines.push(` -d '${JSON.stringify(sample(requestSchema), null, 2)}'`)
|
||||
}
|
||||
const curlHtml = await fence('bash', curlLines.join(' \\\n'))
|
||||
|
||||
// ---- example response (first 2xx with a JSON body) for the code panel ----
|
||||
let responseStatus = ''
|
||||
let responseHtml: string | null = null
|
||||
for (const [status, resRaw] of Object.entries<any>(op.responses)) {
|
||||
if (!status.startsWith('2')) continue
|
||||
responseStatus = status
|
||||
const schema = deref(resRaw).content?.['application/json']?.schema
|
||||
if (schema) responseHtml = await fence('json', JSON.stringify(sample(schema), null, 2))
|
||||
break
|
||||
}
|
||||
|
||||
const responses: ResponseDef[] = Object.entries<any>(op.responses).map(([status, resRaw]) => {
|
||||
const res = deref(resRaw)
|
||||
const content = res.content || {}
|
||||
const ct = content['application/json'] || Object.values<any>(content)[0]
|
||||
return {
|
||||
status,
|
||||
descriptionHtml: inline(res.description),
|
||||
headers: Object.entries<any>(res.headers || {}).map(([name, hRaw]) => {
|
||||
const h = deref(hRaw)
|
||||
return { name, typeHtml: typeHtml(h.schema), descriptionHtml: inline(h.description) }
|
||||
}),
|
||||
typeLabel: ct?.schema ? typeHtml(ct.schema) : null
|
||||
}
|
||||
})
|
||||
|
||||
const built: Operation = {
|
||||
id: op.operationId,
|
||||
method: method.toUpperCase(),
|
||||
path,
|
||||
summary: op.summary || '',
|
||||
descriptionHtml: await html(stripRequired(op.description)),
|
||||
auth,
|
||||
parameters,
|
||||
requestBody,
|
||||
responses,
|
||||
sample: { curlHtml, responseStatus, responseHtml }
|
||||
}
|
||||
;(groupByName.get(op.tags?.[0]) ?? groups[0])?.operations.push(built)
|
||||
}
|
||||
}
|
||||
|
||||
// ---- component schemas, each rendered on its own page ----
|
||||
const schemas: SchemaDef[] = []
|
||||
for (const [name, s] of Object.entries<any>(spec.components?.schemas || {})) {
|
||||
const t = bodyTable(s)
|
||||
schemas.push({
|
||||
name,
|
||||
descriptionHtml: await html(s.description),
|
||||
note: t.note,
|
||||
rows: t.rows
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
info: {
|
||||
title: spec.info.title,
|
||||
version: spec.info.version,
|
||||
descriptionHtml: await html(spec.info.description)
|
||||
},
|
||||
authHtml: await html(spec.components?.securitySchemes?.bearerAuth?.description),
|
||||
servers: (spec.servers || []).map((s: any) => ({
|
||||
url: s.url,
|
||||
description: s.description || ''
|
||||
})),
|
||||
groups,
|
||||
schemas
|
||||
}
|
||||
}
|
||||
Vendored
-37
@@ -1,37 +0,0 @@
|
||||
const colors = require('tailwindcss/colors')
|
||||
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
content: [
|
||||
"./.vitepress/theme/*.vue",
|
||||
],
|
||||
theme: {
|
||||
colors: {
|
||||
transparent: 'transparent',
|
||||
current: 'currentColor',
|
||||
white: colors.white,
|
||||
black: colors.black,
|
||||
gray: {
|
||||
50: "#EEEFF1",
|
||||
100: "#DEDFE3",
|
||||
200: "#BABCC5",
|
||||
300: "#999CA8",
|
||||
400: "#75798A",
|
||||
500: "#585B68",
|
||||
600: "#464853",
|
||||
700: "#363840",
|
||||
800: "#232429",
|
||||
900: "#131316"
|
||||
},
|
||||
indigo: colors.indigo,
|
||||
|
||||
},
|
||||
extend: {
|
||||
borderWidth: {
|
||||
'1': '1px',
|
||||
}
|
||||
},
|
||||
},
|
||||
plugins: [],
|
||||
darkMode: 'class',
|
||||
}
|
||||
@@ -1,101 +0,0 @@
|
||||
<script>
|
||||
import { withBase } from 'vitepress';
|
||||
import './theme.css'
|
||||
|
||||
export default {
|
||||
|
||||
setup() {
|
||||
return { withBase };
|
||||
},
|
||||
};
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
<template>
|
||||
<main class="home">
|
||||
<header class="hero">
|
||||
<div class="mx-auto max-w-7xl px-6 lg:px-8">
|
||||
<div class="mx-auto lg:text-center">
|
||||
<img class="rotating h-36 mx-auto my-8 " src="https://raw.githubusercontent.com/thomiceli/opengist/master/public/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>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-4">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="m4.5 19.5 15-15m0 0H8.25m11.25 0v11.25" />
|
||||
</svg>
|
||||
</a>
|
||||
<h1 class="mt-5 text-4xl font-bold tracking-tight sm:text-5xl">Opengist</h1>
|
||||
<h2 class="mt-4 text-xl">Self-hosted pastebin powered by Git, open-source alternative to Github Gist.</h2>
|
||||
</div>
|
||||
<div class="space-x-2 my-12">
|
||||
<a href="/docs" class="rounded-md bg-indigo-600 mt-6 px-5 py-3 text-xl font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600">Docs</a>
|
||||
<a target="_blank" href="https://demo.opengist.io" class="rounded-md bg-indigo-400 mt-6 px-5 py-3 text-xl border-white font-semibold text-white shadow-sm hover:bg-indigo-300 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600">Live demo</a>
|
||||
<a target="_blank" href="https://github.com/thomiceli/opengist" class="rounded-md bg-gray-800 mt-6 px-3 py-3 text-xl dark:border dark:border-1 dark:border-gray-400 font-semibold text-white shadow-sm hover:bg-gray-700 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600">
|
||||
<svg stroke="currentColor" fill="currentColor" stroke-width="0" viewBox="0 0 16 16" class="w-7 h-auto inline" height="1em" width="1em" xmlns="http://www.w3.org/2000/svg"><path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27s1.36.09 2 .27c1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.01 8.01 0 0 0 16 8c0-4.42-3.58-8-8-8"></path></svg>
|
||||
</a>
|
||||
</div>
|
||||
<div class="border border-1 mt-6 px-5 py-3 rounded-md shadow-sm ">
|
||||
<code class="select-all ">docker run --name <span class="text-indigo-700 dark:text-indigo-300 font-bold">opengist</span> -p <span class="text-indigo-700 dark:text-indigo-300 font-bold">6157</span>:6157 -v "<span class="text-indigo-700 dark:text-indigo-300 font-bold">$HOME/.opengist</span>:/opengist" ghcr.io/thomiceli/opengist:1</code>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="relative w-full sm:max-w-7xl mx-auto overflow-auto">
|
||||
<img class="block w-[200vw] max-w-none sm:w-full h-auto" :src="withBase('/opengist-demo.png')" alt="demo-opengist-screenshot" />
|
||||
</div>
|
||||
|
||||
</main>
|
||||
</template>
|
||||
|
||||
|
||||
|
||||
<style>
|
||||
@-webkit-keyframes rotating /* Safari and Chrome */ {
|
||||
from {
|
||||
-webkit-transform: rotate(0deg);
|
||||
-o-transform: rotate(0deg);
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
-webkit-transform: rotate(360deg);
|
||||
-o-transform: rotate(360deg);
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
@keyframes rotating {
|
||||
from {
|
||||
-ms-transform: rotate(0deg);
|
||||
-moz-transform: rotate(0deg);
|
||||
-webkit-transform: rotate(0deg);
|
||||
-o-transform: rotate(0deg);
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
-ms-transform: rotate(360deg);
|
||||
-moz-transform: rotate(360deg);
|
||||
-webkit-transform: rotate(360deg);
|
||||
-o-transform: rotate(360deg);
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.home {
|
||||
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.rotating {
|
||||
-webkit-animation: rotating 8s linear infinite;
|
||||
-moz-animation: rotating 4s linear infinite;
|
||||
-ms-animation: rotating 4s linear infinite;
|
||||
-o-animation: rotating 4s linear infinite;
|
||||
animation: rotating 12s linear infinite;
|
||||
}
|
||||
|
||||
|
||||
</style>
|
||||
@@ -1,16 +1,21 @@
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
import { useData } from 'vitepress'
|
||||
import Home from './Home.vue'
|
||||
import Home from './layouts/Home.vue'
|
||||
import SectionNav from './components/SectionNav.vue'
|
||||
import DefaultTheme from 'vitepress/theme'
|
||||
|
||||
const { Layout } = DefaultTheme
|
||||
const { frontmatter } = useData()
|
||||
|
||||
const isHome = computed(() => frontmatter.value.layout === 'home')
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SectionNav v-if="!isHome" />
|
||||
<Layout>
|
||||
<template v-if="frontmatter.layout === 'home'" #home-hero-after>
|
||||
<template v-if="isHome" #home-hero-after>
|
||||
<Home />
|
||||
</template>
|
||||
</Layout>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
@@ -0,0 +1,161 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { data } from '../../openapi.data'
|
||||
|
||||
const props = defineProps<{ id: string }>()
|
||||
|
||||
const op = computed(() =>
|
||||
data.groups.flatMap((g) => g.operations).find((o) => o.id === props.id)
|
||||
)
|
||||
|
||||
// Split the path so `{param}` segments can be highlighted.
|
||||
const pathParts = computed(() =>
|
||||
(op.value?.path ?? '')
|
||||
.split(/(\{[^}]+\})/g)
|
||||
.filter(Boolean)
|
||||
.map((text) => ({ text, param: text.startsWith('{') && text.endsWith('}') }))
|
||||
)
|
||||
|
||||
// The Authorization header is implied by the spec's bearerAuth security scheme
|
||||
// rather than declared as a parameter, so synthesize a row from `auth`.
|
||||
const authHeader = computed(() => {
|
||||
const a = op.value?.auth
|
||||
if (!a || (!a.scopes.length && !a.anyToken && !a.anonymous)) return null
|
||||
const desc = a.scopes.map((s) => `<code>${s}</code>`).join(', ')
|
||||
return {
|
||||
name: 'Authorization',
|
||||
typeHtml: '<code>string</code>',
|
||||
required: !a.anonymous,
|
||||
descriptionHtml: desc
|
||||
}
|
||||
})
|
||||
|
||||
// Request parameters grouped by location, in display order, empty groups dropped.
|
||||
const paramGroups = computed(() => {
|
||||
const o = op.value
|
||||
if (!o) return []
|
||||
const headers = [
|
||||
...(authHeader.value ? [authHeader.value] : []),
|
||||
...o.parameters.filter((p) => p.in === 'header')
|
||||
]
|
||||
const groups = [
|
||||
{ label: 'Headers', descLabel: 'Scopes', optional: true, items: headers },
|
||||
{ label: 'Path parameters', descLabel: 'Description', optional: false, items: o.parameters.filter((p) => p.in === 'path') },
|
||||
{ label: 'Query parameters', descLabel: 'Description', optional: false, items: o.parameters.filter((p) => p.in === 'query') }
|
||||
]
|
||||
return groups.filter((g) => g.items.length)
|
||||
})
|
||||
|
||||
const hasRequest = computed(() => paramGroups.value.length > 0 || !!op.value?.requestBody)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="op" class="oas-op-grid">
|
||||
<!-- left / center: description, request, responses -->
|
||||
<div class="oas-main">
|
||||
<h1>{{ op.summary }}</h1>
|
||||
<div class="oas-endpoint">
|
||||
<span class="oas-method" :class="`m-${op.method.toLowerCase()}`">{{ op.method }}</span>
|
||||
<code class="oas-path"><span
|
||||
v-for="(part, i) in pathParts"
|
||||
:key="i"
|
||||
:class="{ 'oas-param': part.param }"
|
||||
>{{ part.text }}</span></code>
|
||||
</div>
|
||||
|
||||
<div class="oas-desc" v-html="op.descriptionHtml" />
|
||||
|
||||
<template v-if="hasRequest">
|
||||
<h2>Request</h2>
|
||||
|
||||
<template v-for="g in paramGroups" :key="g.label">
|
||||
<h3>{{ g.label }}</h3>
|
||||
<table class="oas-table">
|
||||
<thead>
|
||||
<tr><th>Name</th><th>Type</th><th>{{ g.descLabel }}</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="p in g.items" :key="p.name">
|
||||
<td>
|
||||
<code>{{ p.name }}</code>
|
||||
<span v-if="p.required" class="oas-req">required</span>
|
||||
<span v-else-if="g.optional" class="oas-opt">optional</span>
|
||||
</td>
|
||||
<td v-html="p.typeHtml" />
|
||||
<td v-html="p.descriptionHtml" />
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</template>
|
||||
|
||||
<template v-if="op.requestBody">
|
||||
<h3>Body parameters</h3>
|
||||
<p class="oas-bodyhead">
|
||||
<span v-html="op.requestBody.label" />
|
||||
<span v-if="op.requestBody.required" class="oas-req">required</span>
|
||||
<span class="oas-ct">application/json</span>
|
||||
</p>
|
||||
<table v-if="op.requestBody.rows.length" class="oas-table">
|
||||
<thead>
|
||||
<tr><th>Field</th><th>Type</th><th>Description</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="(row, i) in op.requestBody.rows" :key="i" :class="{ 'oas-nested': row.depth }">
|
||||
<td>
|
||||
<span class="oas-field" :style="{ paddingLeft: row.depth * 1.25 + 'rem' }">
|
||||
<span v-if="row.depth" class="oas-nest">↳</span>
|
||||
<code>{{ row.name }}</code>
|
||||
<span v-if="row.required" class="oas-req">required</span>
|
||||
<span v-else class="oas-opt">optional</span>
|
||||
</span>
|
||||
</td>
|
||||
<td v-html="row.typeHtml" />
|
||||
<td v-html="row.descriptionHtml" />
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p v-if="op.requestBody.note" class="oas-note" v-html="op.requestBody.note" />
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<h2>Responses</h2>
|
||||
<table class="oas-table">
|
||||
<thead>
|
||||
<tr><th>Status</th><th>Body</th><th>Description</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<template v-for="r in op.responses" :key="r.status">
|
||||
<tr>
|
||||
<td><code class="oas-status" :class="`s-${r.status[0]}`">{{ r.status }}</code></td>
|
||||
<td><span v-if="r.typeLabel" v-html="r.typeLabel" /><span v-else>—</span></td>
|
||||
<td v-html="r.descriptionHtml" />
|
||||
</tr>
|
||||
<tr v-if="r.headers.length" class="oas-headers">
|
||||
<td></td>
|
||||
<td colspan="2">
|
||||
<span class="oas-hlabel">Headers:</span>
|
||||
<span v-for="h in r.headers" :key="h.name" class="oas-header"><code>{{ h.name }}</code></span>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- right: code samples -->
|
||||
<aside class="oas-code">
|
||||
<div class="oas-code-sticky">
|
||||
<div class="oas-code-label">Request</div>
|
||||
<div class="oas-code-block" v-html="op.sample.curlHtml" />
|
||||
<template v-if="op.sample.responseHtml">
|
||||
<div class="oas-code-label">
|
||||
Response
|
||||
<code class="oas-status" :class="`s-${op.sample.responseStatus[0]}`">{{ op.sample.responseStatus }}</code>
|
||||
</div>
|
||||
<div class="oas-code-block" v-html="op.sample.responseHtml" />
|
||||
</template>
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
<div v-else><p>Unknown operation: <code>{{ id }}</code></p></div>
|
||||
</template>
|
||||
@@ -0,0 +1,31 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { data } from '../../openapi.data'
|
||||
|
||||
const props = defineProps<{ name: string }>()
|
||||
const schema = computed(() => data.schemas.find((s) => s.name === props.name))
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="schema" class="oas-schema">
|
||||
<h1>{{ schema.name }}</h1>
|
||||
<div v-html="schema.descriptionHtml" />
|
||||
<table v-if="schema.rows.length" class="oas-table">
|
||||
<thead>
|
||||
<tr><th>Field</th><th>Type</th><th>Description</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="row in schema.rows" :key="row.name">
|
||||
<td>
|
||||
<code>{{ row.name }}</code>
|
||||
<span v-if="row.required" class="oas-req">required</span>
|
||||
</td>
|
||||
<td v-html="row.typeHtml" />
|
||||
<td v-html="row.descriptionHtml" />
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p v-if="schema.note" class="oas-note" v-html="schema.note" />
|
||||
</div>
|
||||
<div v-else><p>Unknown schema: <code>{{ name }}</code></p></div>
|
||||
</template>
|
||||
@@ -0,0 +1,88 @@
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
import { useRoute, withBase } from 'vitepress'
|
||||
|
||||
const route = useRoute()
|
||||
const isApi = computed(() => route.path.startsWith(withBase('/docs/api')))
|
||||
|
||||
const items = computed(() => [
|
||||
{ text: 'Guide', link: '/docs', active: !isApi.value },
|
||||
{ text: 'API', link: '/docs/api', active: isApi.value },
|
||||
])
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="og-sectionbar">
|
||||
<div class="og-sectionbar__inner">
|
||||
<a
|
||||
v-for="it in items"
|
||||
:key="it.link"
|
||||
:href="withBase(it.link)"
|
||||
class="og-sectionbar__link"
|
||||
:class="{ 'is-active': it.active }"
|
||||
>
|
||||
{{ it.text }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.og-sectionbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media (min-width: 960px) {
|
||||
.og-sectionbar {
|
||||
position: fixed;
|
||||
top: var(--vp-nav-height);
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 28;
|
||||
display: block;
|
||||
height: var(--og-bar-height);
|
||||
background-color: var(--vp-nav-bg-color);
|
||||
}
|
||||
}
|
||||
|
||||
/* Same container + frame borders as the top navbar so the bar lines up: full
|
||||
--vp-layout-max-width centered, 32px inner padding, and 2px vertical + bottom
|
||||
borders matching the rest of the frame. */
|
||||
.og-sectionbar__inner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1.5rem;
|
||||
height: 100%;
|
||||
max-width: var(--vp-layout-max-width);
|
||||
margin: 0 auto;
|
||||
padding-inline: 32px;
|
||||
border-inline: var(--og-frame-border) solid var(--vp-c-divider);
|
||||
border-bottom: var(--og-frame-border) solid var(--vp-c-divider);
|
||||
}
|
||||
|
||||
.og-sectionbar__link {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--vp-c-text-2);
|
||||
transition: color 0.2s ease;
|
||||
}
|
||||
.og-sectionbar__link:hover {
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
.og-sectionbar__link.is-active {
|
||||
color: var(--vp-c-brand-1);
|
||||
}
|
||||
.og-sectionbar__link.is-active::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: -1px;
|
||||
height: 2px;
|
||||
background: var(--vp-c-brand-1);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,135 @@
|
||||
<script setup>
|
||||
const features = [
|
||||
{
|
||||
title: 'Public, unlisted & private',
|
||||
desc: 'Set the visibility of every snippet - public, unlisted, or fully private.',
|
||||
},
|
||||
{
|
||||
title: 'Powered by Git',
|
||||
desc: 'Each snippet is a Git repository. Clone and push over HTTP or SSH, or create one with a plain git push.',
|
||||
},
|
||||
{
|
||||
title: 'Git CLI & API',
|
||||
desc: 'Manage your snippets straight from the git command line, or via the API.',
|
||||
},
|
||||
{
|
||||
title: 'Revisions & diffs',
|
||||
desc: 'Browse the full history of a snippet with diffs between every revision.',
|
||||
},
|
||||
{
|
||||
title: 'Rich rendering',
|
||||
desc: 'Syntax highlighting for hundreds of languages, plus Markdown with Mermaid, LaTeX, CSV and media files.',
|
||||
},
|
||||
{
|
||||
title: 'Files & uploads',
|
||||
desc: 'Multiple files per snippet, including binary files and images, with a download as ZIP.',
|
||||
},
|
||||
{
|
||||
title: 'Likes, forks & topics',
|
||||
desc: 'Star and fork snippets, and organize them with topics for easy discovery.',
|
||||
},
|
||||
{
|
||||
title: 'Embed & JSON',
|
||||
desc: 'Drop a snippet into any webpage with the embed widget, or fetch it as JSON.',
|
||||
},
|
||||
{
|
||||
title: 'Full-text & code search',
|
||||
desc: 'Search across snippets via code or metadata.',
|
||||
},
|
||||
{
|
||||
title: 'OAuth, LDAP & passkeys',
|
||||
desc: 'Log in with GitHub, GitLab, Gitea or OIDC, LDAP, and secure accounts with TOTP or WebAuthn.',
|
||||
},
|
||||
{
|
||||
title: 'Admin & instance controls',
|
||||
desc: 'Require login for a private instance, allow anonymous snippets, manage users, and expose Prometheus metrics.',
|
||||
},
|
||||
{
|
||||
title: 'Self-hosted your way',
|
||||
desc: 'A single binary, Docker image or K8s deployment, backed by SQLite, PostgreSQL or MySQL.',
|
||||
},
|
||||
]
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="og-features">
|
||||
<div class="og-features__head">
|
||||
<h2 class="og-features__title">Features</h2>
|
||||
<p class="og-features__desc">
|
||||
All you need to self-host, manage and share your snippets.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<ul class="og-features__grid">
|
||||
<li v-for="f in features" :key="f.title" class="og-feature">
|
||||
<h3 class="og-feature__title">{{ f.title }}</h3>
|
||||
<p class="og-feature__desc">{{ f.desc }}</p>
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.og-features {
|
||||
border-bottom: var(--og-frame-border) solid var(--vp-c-divider);
|
||||
}
|
||||
|
||||
.og-features__head {
|
||||
padding: 3rem 1.5rem 2.5rem;
|
||||
text-align: center;
|
||||
}
|
||||
.og-features__title {
|
||||
margin: 0;
|
||||
border: 0;
|
||||
font-size: 1.8rem;
|
||||
line-height: 1.2;
|
||||
font-weight: 700;
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
.og-features__desc {
|
||||
margin: 0.6rem 0 0;
|
||||
font-size: 1rem;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
/* Flush grid: cells separated by the same frame border (no radius, no margin). */
|
||||
.og-features__grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: var(--og-frame-border);
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
background: var(--vp-c-divider);
|
||||
border-top: var(--og-frame-border) solid var(--vp-c-divider);
|
||||
}
|
||||
|
||||
.og-feature {
|
||||
padding: 1.75rem;
|
||||
background: var(--vp-c-bg);
|
||||
}
|
||||
.og-feature__title {
|
||||
margin: 0;
|
||||
border: 0;
|
||||
font-size: 1.05rem;
|
||||
font-weight: 600;
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
.og-feature__desc {
|
||||
margin: 0.4rem 0 0;
|
||||
font-size: 0.9rem;
|
||||
line-height: 1.55;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.og-features__grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
}
|
||||
@media (max-width: 600px) {
|
||||
.og-features__grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,338 @@
|
||||
<script setup>
|
||||
import { ref, computed, onMounted, onBeforeUnmount } from 'vue'
|
||||
|
||||
const version = 'v1.13'
|
||||
const dockerCommand = 'docker run -p 6157:6157 -v "$HOME/.opengist:/opengist" ghcr.io/thomiceli/opengist:1.13'
|
||||
|
||||
const stars = ref(null)
|
||||
const formattedStars = computed(() => {
|
||||
const n = stars.value
|
||||
if (n == null) return ''
|
||||
// if (n >= 1000) return (n / 1000).toFixed(1).replace(/\.0$/, '') + 'k'
|
||||
return String(n)
|
||||
})
|
||||
|
||||
// Cache the star count in localStorage so we don't hit the GitHub API on every
|
||||
// page load / navigation (unauthenticated API is rate-limited to 60/hour/IP).
|
||||
const STARS_KEY = 'og-gh-stars'
|
||||
const STARS_TTL = 6 * 60 * 60 * 1000 // 6 hours
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
const cached = JSON.parse(localStorage.getItem(STARS_KEY) || 'null')
|
||||
if (cached && Date.now() - cached.ts < STARS_TTL) {
|
||||
stars.value = cached.value
|
||||
return
|
||||
}
|
||||
} catch {}
|
||||
|
||||
try {
|
||||
const res = await fetch('https://api.github.com/repos/thomiceli/opengist')
|
||||
if (res.ok) {
|
||||
stars.value = (await res.json()).stargazers_count
|
||||
try {
|
||||
localStorage.setItem(STARS_KEY, JSON.stringify({ value: stars.value, ts: Date.now() }))
|
||||
} catch {}
|
||||
}
|
||||
} catch {}
|
||||
})
|
||||
|
||||
const copied = ref(false)
|
||||
let timer
|
||||
|
||||
const copy = async () => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(dockerCommand)
|
||||
copied.value = true
|
||||
clearTimeout(timer)
|
||||
timer = setTimeout(() => (copied.value = false), 1600)
|
||||
} catch {}
|
||||
}
|
||||
|
||||
onBeforeUnmount(() => clearTimeout(timer))
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="og-hero">
|
||||
<a
|
||||
class="og-badge"
|
||||
href="https://github.com/thomiceli/opengist/releases"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<span class="og-badge__dot" />
|
||||
{{ version }} released: Restful API + brand new docs page
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="og-badge__arrow">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="m9 6 6 6-6 6" />
|
||||
</svg>
|
||||
</a>
|
||||
|
||||
<img
|
||||
class="og-logo"
|
||||
src="https://raw.githubusercontent.com/thomiceli/opengist/master/public/img/opengist.svg"
|
||||
alt="Opengist"
|
||||
/>
|
||||
|
||||
<h1 class="og-title">
|
||||
<span class="og-title__shine">Opengist</span>
|
||||
</h1>
|
||||
|
||||
<p class="og-tagline">
|
||||
Self-hosted and open-source pastebin powered by Git
|
||||
</p>
|
||||
|
||||
<div class="og-actions">
|
||||
<a class="og-btn og-btn--primary" href="/docs">Get started</a>
|
||||
<a class="og-btn" href="https://demo.opengist.io" target="_blank" rel="noopener noreferrer">
|
||||
Live demo
|
||||
</a>
|
||||
<a
|
||||
class="og-btn og-btn--github"
|
||||
href="https://github.com/thomiceli/opengist"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
aria-label="Opengist on GitHub"
|
||||
>
|
||||
<svg class="og-btn__gh" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true">
|
||||
<path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27s1.36.09 2 .27c1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.01 8.01 0 0 0 16 8c0-4.42-3.58-8-8-8" />
|
||||
</svg>
|
||||
<span v-if="formattedStars" class="og-btn__stars">
|
||||
<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
|
||||
<path d="m12 17.27 6.18 3.73-1.64-7.03L22 9.24l-7.19-.61L12 2 9.19 8.63 2 9.24l5.46 4.73L5.82 21z" />
|
||||
</svg>
|
||||
{{ formattedStars }}
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="og-install">
|
||||
<span class="og-install__prompt">$</span>
|
||||
<code class="og-install__cmd">{{ dockerCommand }}</code>
|
||||
<button class="og-install__copy" type="button" aria-label="Copy command" @click="copy">
|
||||
<svg v-if="copied" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="m5 13 4 4L19 7" />
|
||||
</svg>
|
||||
<svg v-else viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<rect x="9" y="9" width="11" height="11" rx="2" />
|
||||
<path d="M5 15V5a2 2 0 0 1 2-2h10" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.og-hero {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
padding: 2.5rem 1.5rem 2rem;
|
||||
border-bottom: 1px solid var(--vp-c-divider);
|
||||
}
|
||||
|
||||
.og-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.35rem 0.9rem;
|
||||
border-radius: 999px;
|
||||
font-size: 0.825rem;
|
||||
font-weight: 500;
|
||||
color: var(--vp-c-brand-1);
|
||||
background: var(--vp-c-brand-soft);
|
||||
transition: filter 0.2s ease;
|
||||
}
|
||||
.og-badge:hover {
|
||||
filter: brightness(1.05);
|
||||
}
|
||||
.og-badge__dot {
|
||||
width: 0.45rem;
|
||||
height: 0.45rem;
|
||||
border-radius: 999px;
|
||||
background: var(--vp-c-brand-1);
|
||||
}
|
||||
.og-badge__arrow {
|
||||
width: 0.85rem;
|
||||
height: 0.85rem;
|
||||
}
|
||||
|
||||
.og-logo {
|
||||
width: 4.5rem;
|
||||
height: 4.5rem;
|
||||
margin: 2rem 0 1.25rem;
|
||||
}
|
||||
|
||||
.og-title {
|
||||
margin: 0;
|
||||
font-size: clamp(1.25rem, 6vw, 2.75rem);
|
||||
line-height: 1.18;
|
||||
font-weight: 800;
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
.og-title__shine {
|
||||
display: block;
|
||||
background: linear-gradient(
|
||||
110deg,
|
||||
var(--vp-c-text-1) 0%,
|
||||
var(--vp-c-text-1) 40%,
|
||||
var(--vp-c-brand-1) 50%,
|
||||
var(--vp-c-text-1) 60%,
|
||||
var(--vp-c-text-1) 100%
|
||||
);
|
||||
background-size: 250% 100%;
|
||||
background-position: 100% 0;
|
||||
background-clip: text;
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
animation: og-shine 6s ease-in-out 0.3s 1 forwards;
|
||||
}
|
||||
@keyframes og-shine {
|
||||
to {
|
||||
background-position: 0 0;
|
||||
}
|
||||
}
|
||||
|
||||
.og-tagline {
|
||||
max-width: 34rem;
|
||||
margin: 1.2rem 0 0;
|
||||
font-size: 1.35rem;
|
||||
line-height: 1;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
.og-license {
|
||||
margin: 0.85rem 0 0;
|
||||
font-size: 0.85rem;
|
||||
color: var(--vp-c-text-3);
|
||||
}
|
||||
|
||||
.og-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.75rem;
|
||||
margin-top: 2rem;
|
||||
}
|
||||
.og-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 2.875rem;
|
||||
padding: 0 1.5rem;
|
||||
border-radius: 0.625rem;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
background: var(--vp-c-bg-soft);
|
||||
color: var(--vp-c-text-1);
|
||||
font-size: 0.95rem;
|
||||
font-weight: 600;
|
||||
transition: border-color 0.2s ease, background-color 0.2s ease, color 0.2s ease;
|
||||
}
|
||||
.og-btn:hover {
|
||||
border-color: var(--vp-c-brand-1);
|
||||
color: var(--vp-c-brand-1);
|
||||
}
|
||||
.og-btn--primary {
|
||||
border-color: transparent;
|
||||
background: var(--vp-c-brand-3);
|
||||
color: #fff;
|
||||
}
|
||||
.og-btn--primary:hover {
|
||||
background: var(--vp-c-brand-2);
|
||||
color: #fff;
|
||||
}
|
||||
.og-btn--icon {
|
||||
width: 2.875rem;
|
||||
padding: 0;
|
||||
}
|
||||
.og-btn--icon svg {
|
||||
width: 1.35rem;
|
||||
height: 1.35rem;
|
||||
}
|
||||
|
||||
/* GitHub button with star count. */
|
||||
.og-btn--github {
|
||||
gap: 0.6rem;
|
||||
padding: 0 1.1rem;
|
||||
}
|
||||
.og-btn__gh {
|
||||
width: 1.35rem;
|
||||
height: 1.35rem;
|
||||
}
|
||||
.og-btn__stars {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.3rem;
|
||||
padding-left: 0.6rem;
|
||||
border-left: 1px solid var(--vp-c-divider);
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
.og-btn__stars svg {
|
||||
width: 0.95rem;
|
||||
height: 0.95rem;
|
||||
color: #e3b341;
|
||||
}
|
||||
|
||||
.og-install {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
max-width: 100%;
|
||||
margin-top: 2rem;
|
||||
padding: 0.6rem 0.6rem 0.6rem 1rem;
|
||||
border-radius: 0.75rem;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
background: var(--vp-c-bg-alt);
|
||||
font-family: var(--vp-font-family-mono);
|
||||
}
|
||||
.og-install__prompt {
|
||||
color: var(--vp-c-brand-1);
|
||||
user-select: none;
|
||||
}
|
||||
.og-install__cmd {
|
||||
flex: 1;
|
||||
min-width: 0; /* allow the long command to shrink + scroll instead of overflowing */
|
||||
overflow-x: auto;
|
||||
white-space: nowrap;
|
||||
font-size: 0.85rem;
|
||||
color: var(--vp-c-text-1);
|
||||
background: transparent;
|
||||
padding: 0;
|
||||
}
|
||||
.og-install__copy {
|
||||
flex-shrink: 0;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 2.1rem;
|
||||
height: 2.1rem;
|
||||
border-radius: 0.5rem;
|
||||
color: var(--vp-c-text-2);
|
||||
background: var(--vp-c-bg);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
transition: color 0.2s ease, border-color 0.2s ease;
|
||||
}
|
||||
.og-install__copy:hover {
|
||||
color: var(--vp-c-brand-1);
|
||||
border-color: var(--vp-c-brand-1);
|
||||
}
|
||||
.og-install__copy svg {
|
||||
width: 1.05rem;
|
||||
height: 1.05rem;
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.og-hero {
|
||||
padding: 2.5rem 1.25rem 2.5rem;
|
||||
}
|
||||
.og-install {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 640px) {
|
||||
.og-hero {
|
||||
padding-top: 3.5rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,75 @@
|
||||
<script setup>
|
||||
import { useData, withBase } from 'vitepress'
|
||||
|
||||
const { isDark } = useData()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="og-showcase">
|
||||
<div class="og-showcase__text">
|
||||
<h2 class="og-showcase__title">A clean, familiar interface</h2>
|
||||
<p class="og-showcase__desc">
|
||||
Create, browse and share snippets with syntax highlighting, revisions and more.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="og-showcase__frame">
|
||||
<img
|
||||
v-show="!isDark"
|
||||
class="og-showcase__img"
|
||||
:src="withBase('/opengist-demo.png')"
|
||||
alt="Opengist web interface"
|
||||
/>
|
||||
<img
|
||||
v-show="isDark"
|
||||
class="og-showcase__img"
|
||||
:src="withBase('/opengist-demo-dark.png')"
|
||||
alt="Opengist web interface"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.og-showcase {
|
||||
padding: 3rem 1.5rem;
|
||||
text-align: center;
|
||||
}
|
||||
@media (max-width: 640px) {
|
||||
.og-showcase {
|
||||
padding: 2rem 1.25rem;
|
||||
}
|
||||
}
|
||||
|
||||
.og-showcase__text {
|
||||
max-width: 34rem;
|
||||
margin: 0 auto 2rem;
|
||||
}
|
||||
.og-showcase__title {
|
||||
margin: 0;
|
||||
border: 0;
|
||||
font-size: 1.6rem;
|
||||
line-height: 1.2;
|
||||
font-weight: 700;
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
.og-showcase__desc {
|
||||
margin: 0.5rem 0 0;
|
||||
font-size: 0.95rem;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
/* Smaller, centered screenshot frame. */
|
||||
.og-showcase__frame {
|
||||
max-width: 72rem;
|
||||
margin: 0 auto;
|
||||
border-radius: 0.75rem;
|
||||
overflow: hidden;
|
||||
}
|
||||
.og-showcase__img {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
padding: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -2,11 +2,16 @@ import { h } from 'vue'
|
||||
import type { Theme } from 'vitepress'
|
||||
import DefaultTheme from 'vitepress/theme'
|
||||
import Layout from "./Layout.vue";
|
||||
import OpenApiOperation from "./components/OpenApiOperation.vue";
|
||||
import OpenApiSchema from "./components/OpenApiSchema.vue";
|
||||
import './style.css'
|
||||
import './openapi.css'
|
||||
|
||||
export default {
|
||||
...DefaultTheme,
|
||||
Layout,
|
||||
enhanceApp({ app, router, siteData }) {
|
||||
// ...
|
||||
app.component('OpenApiOperation', OpenApiOperation)
|
||||
app.component('OpenApiSchema', OpenApiSchema)
|
||||
}
|
||||
} satisfies Theme
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
<script setup>
|
||||
import Hero from '../components/home/Hero.vue'
|
||||
import Showcase from '../components/home/Showcase.vue'
|
||||
import Features from '../components/home/Features.vue'
|
||||
// Add further sections here as the landing page grows (e.g. Features, FAQ).
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<main class="og-home">
|
||||
<div class="og-wrapper">
|
||||
<Hero />
|
||||
<Features />
|
||||
<Showcase />
|
||||
<!-- Empty growing section so the side borders run to the bottom. -->
|
||||
<div class="og-fill" />
|
||||
</div>
|
||||
</main>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.og-home {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Centered content column framed with vertical borders (vite-plus style).
|
||||
Sections inside add their own border-bottom to act as horizontal rules.
|
||||
Width matches the navbar container so the home content aligns with the header. */
|
||||
.og-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-width: var(--vp-layout-max-width);
|
||||
margin: 0 auto;
|
||||
/* Fill the viewport below the navbar so the side borders run to the bottom
|
||||
even when the content is shorter than the screen. */
|
||||
min-height: calc(100vh - var(--vp-nav-height));
|
||||
/* Gap inside the frame, so the left/right borders stay visible through it. */
|
||||
padding-bottom: 2rem;
|
||||
border-inline: var(--og-frame-border) solid var(--vp-c-divider);
|
||||
}
|
||||
|
||||
/* Empty section that absorbs the remaining height, extending the side borders. */
|
||||
.og-fill {
|
||||
flex: 1;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,138 @@
|
||||
/* Shared styling for the generated API reference components. */
|
||||
.oas-op h3,
|
||||
.oas-schema h3 {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.6rem;
|
||||
}
|
||||
.oas-method {
|
||||
font-size: 0.72rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.04em;
|
||||
padding: 0.15rem 0.5rem;
|
||||
border-radius: 5px;
|
||||
color: #fff;
|
||||
}
|
||||
.oas-method.m-get { background: #1f7a4d; }
|
||||
.oas-method.m-post { background: #1860c2; }
|
||||
.oas-method.m-put { background: #b06800; }
|
||||
.oas-method.m-patch { background: #8754c9; }
|
||||
.oas-method.m-delete { background: #c0392b; }
|
||||
/* Endpoint shown as a code block: method badge on the left, then the path. */
|
||||
.oas-endpoint {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
margin: 0.75rem 0 1.5rem;
|
||||
padding: 0.6rem 0.9rem;
|
||||
background: var(--vp-c-bg-alt);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 8px;
|
||||
overflow-x: auto;
|
||||
}
|
||||
/* Plain path text (no inline-code bg/color); only `.oas-param` is highlighted. */
|
||||
.vp-doc code.oas-path {
|
||||
font-size: 0.95rem;
|
||||
background: none;
|
||||
color: var(--vp-c-text-1);
|
||||
padding: 0;
|
||||
white-space: nowrap;
|
||||
}
|
||||
/* Highlight `{param}` segments within the path. */
|
||||
.oas-param {
|
||||
color: var(--vp-c-brand-1);
|
||||
background: var(--vp-c-default-soft);
|
||||
padding: 0 0.25rem;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.oas-req {
|
||||
margin-left: 0.4rem;
|
||||
font-size: 0.7rem;
|
||||
font-weight: 600;
|
||||
color: var(--vp-c-red-1);
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.oas-opt {
|
||||
margin-left: 0.4rem;
|
||||
font-size: 0.7rem;
|
||||
font-weight: 600;
|
||||
color: var(--vp-c-text-3);
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.oas-ct {
|
||||
margin-left: 0.5rem;
|
||||
font-size: 0.8rem;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
/* `.vp-doc table` is display:block / fit-content, so force full-width here. */
|
||||
.vp-doc table.oas-table {
|
||||
display: table;
|
||||
width: 100%;
|
||||
margin: 0.5rem 0 1rem;
|
||||
}
|
||||
.oas-table th { text-align: left; }
|
||||
.oas-status { font-weight: 700; }
|
||||
.oas-status.s-2 { color: #1f7a4d; }
|
||||
.oas-status.s-4,
|
||||
.oas-status.s-5 { color: #c0392b; }
|
||||
.oas-headers td {
|
||||
padding-top: 0;
|
||||
font-size: 0.85rem;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
.oas-hlabel { margin-right: 0.4rem; }
|
||||
.oas-header { margin-right: 0.4rem; }
|
||||
.oas-includes a { margin-right: 0.5rem; }
|
||||
.oas-note { font-size: 0.85rem; color: var(--vp-c-text-2); }
|
||||
.oas-bodyhead { display: flex; align-items: center; gap: 0.5rem; }
|
||||
.oas-field { display: inline-flex; align-items: center; gap: 0.35rem; }
|
||||
.oas-nest { color: var(--vp-c-text-3); }
|
||||
.oas-nested { background: var(--vp-c-bg-soft); }
|
||||
|
||||
/* ---- three-pane endpoint layout (pages with pageClass: api-page) ---- */
|
||||
.api-page .VPDoc .container { max-width: 1180px; }
|
||||
.api-page .VPDoc.has-sidebar .content { max-width: none; }
|
||||
.api-page .VPDoc .content-container { max-width: none; }
|
||||
|
||||
.oas-op-grid {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) 26rem;
|
||||
gap: 2.5rem;
|
||||
align-items: start;
|
||||
}
|
||||
.oas-main { min-width: 0; }
|
||||
.oas-main h1 { margin-top: 0; }
|
||||
|
||||
.oas-code-sticky {
|
||||
position: sticky;
|
||||
top: calc(var(--vp-nav-height) + 1.5rem);
|
||||
}
|
||||
.oas-code-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
margin: 1rem 0 0.4rem;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.04em;
|
||||
text-transform: uppercase;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
.oas-code-label:first-child { margin-top: 0; }
|
||||
/* Smaller, denser code in the side panel. VitePress applies
|
||||
`font-size: var(--vp-code-font-size)` to the <code>, so override the var. */
|
||||
.oas-code {
|
||||
--vp-code-font-size: 0.75rem;
|
||||
--vp-code-line-height: 1.5;
|
||||
}
|
||||
.oas-code-block [class*='language-'] { margin: 0; }
|
||||
.oas-code-block pre {
|
||||
white-space: pre;
|
||||
padding: 14px 16px;
|
||||
}
|
||||
|
||||
/* Collapse to a single column when there isn't room for the side panel. */
|
||||
@media (max-width: 1280px) {
|
||||
.oas-op-grid { grid-template-columns: 1fr; }
|
||||
.oas-code-sticky { position: static; }
|
||||
}
|
||||
@@ -1,3 +1,22 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
/* Map Tailwind's `dark:` variant onto VitePress's `.dark` class. */
|
||||
@custom-variant dark (&:where(.dark, .dark *));
|
||||
|
||||
/* Opengist brand gray palette (replaces Tailwind's default gray). */
|
||||
@theme {
|
||||
--color-gray-50: #EEEFF1;
|
||||
--color-gray-100: #DEDFE3;
|
||||
--color-gray-200: #BABCC5;
|
||||
--color-gray-300: #999CA8;
|
||||
--color-gray-400: #75798A;
|
||||
--color-gray-500: #585B68;
|
||||
--color-gray-600: #464853;
|
||||
--color-gray-700: #363840;
|
||||
--color-gray-800: #232429;
|
||||
--color-gray-900: #131316;
|
||||
}
|
||||
|
||||
/**
|
||||
* Customize default theme styling by overriding CSS variables:
|
||||
* https://github.com/vuejs/vitepress/blob/main/src/client/theme-default/styles/vars.css
|
||||
@@ -49,10 +68,11 @@
|
||||
--vp-c-default-3: var(--vp-c-gray-3);
|
||||
--vp-c-default-soft: var(--vp-c-gray-soft);
|
||||
|
||||
--vp-c-brand-1: var(--vp-c-indigo-1);
|
||||
--vp-c-brand-2: var(--vp-c-indigo-2);
|
||||
--vp-c-brand-3: var(--vp-c-indigo-3);
|
||||
--vp-c-brand-soft: var(--vp-c-indigo-soft);
|
||||
/* Opengist brand blue (matches --color-primary-* in the app). */
|
||||
--vp-c-brand-1: #3c79e2;
|
||||
--vp-c-brand-2: #356fc0;
|
||||
--vp-c-brand-3: #3c79e2;
|
||||
--vp-c-brand-soft: rgba(60, 121, 226, 0.14);
|
||||
|
||||
--vp-c-tip-1: var(--vp-c-brand-1);
|
||||
--vp-c-tip-2: var(--vp-c-brand-2);
|
||||
@@ -70,6 +90,19 @@
|
||||
--vp-c-danger-soft: var(--vp-c-red-soft);
|
||||
}
|
||||
|
||||
/* Dark theme background (brand gray-900). */
|
||||
.dark {
|
||||
--vp-c-bg: #131316;
|
||||
--vp-c-bg-alt: #0f0f12;
|
||||
--vp-c-bg-elv: #1a1a1e;
|
||||
|
||||
/* Lighter brand blue for contrast on dark backgrounds. */
|
||||
--vp-c-brand-1: #74a4f6;
|
||||
--vp-c-brand-2: #588fee;
|
||||
--vp-c-brand-3: #3c79e2;
|
||||
--vp-c-brand-soft: rgba(116, 164, 246, 0.16);
|
||||
}
|
||||
|
||||
/**
|
||||
* Component: Button
|
||||
* -------------------------------------------------------------------------- */
|
||||
@@ -142,6 +175,150 @@
|
||||
height: 108px;
|
||||
}
|
||||
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
/* Home: drop VitePress's bottom margin on the home container so our framed
|
||||
wrapper (and its side borders) reaches the very bottom of the page. */
|
||||
.VPHome {
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
|
||||
/**
|
||||
* Navbar: left-align the menu (Docs / Resources) next to the logo,
|
||||
* keeping the appearance toggle and social links on the right.
|
||||
* -------------------------------------------------------------------------- */
|
||||
|
||||
@media (min-width: 768px) {
|
||||
/* Drop VitePress's `margin-right: -100vw; padding-right: 100vw` background
|
||||
bleed: it makes the flex free space negative, which neutralises the auto
|
||||
margin below. The bar's own background covers the width anyway. */
|
||||
.VPNavBar .content-body {
|
||||
margin-right: 0 !important;
|
||||
padding-right: 0 !important;
|
||||
}
|
||||
/* The (empty) search slot has flex-grow:1 and would eat all the free space,
|
||||
leaving none for the menu's auto margin. Stop it from growing. */
|
||||
.VPNavBar .VPNavBarSearch {
|
||||
flex-grow: 0 !important;
|
||||
}
|
||||
.VPNavBar .VPNavBarMenu {
|
||||
margin-right: auto !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* Keep the navbar identical on the landing page and the docs pages:
|
||||
VitePress otherwise makes it transparent and borderless on the home page. */
|
||||
.VPNavBar {
|
||||
background-color: var(--vp-nav-bg-color) !important;
|
||||
}
|
||||
/* Bottom border: match the side borders' style (2px, --vp-c-divider) and keep
|
||||
it the same width as the container instead of spanning the whole page. */
|
||||
.VPNavBar .divider {
|
||||
max-width: var(--vp-layout-max-width);
|
||||
margin-inline: auto;
|
||||
}
|
||||
.VPNavBar .divider-line {
|
||||
height: var(--og-frame-border);
|
||||
background-color: var(--vp-c-divider) !important;
|
||||
}
|
||||
|
||||
/* Drop the bottom border under the logo/title (shown on docs pages). */
|
||||
.VPNavBarTitle.has-sidebar .title {
|
||||
border-bottom-color: transparent !important;
|
||||
}
|
||||
|
||||
/* Remove the small vertical divider lines between the menu, theme toggle
|
||||
(.appearance) and social links in the navbar. */
|
||||
.VPNavBar .menu + .translations::before,
|
||||
.VPNavBar .menu + .appearance::before,
|
||||
.VPNavBar .menu + .social-links::before,
|
||||
.VPNavBar .translations + .appearance::before,
|
||||
.VPNavBar .appearance + .social-links::before {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* The dividers also provided the spacing between modules; add it back. */
|
||||
.VPNavBar .appearance,
|
||||
.VPNavBar .social-links {
|
||||
margin-left: 12px;
|
||||
}
|
||||
|
||||
/* Match the home content layout: full --vp-layout-max-width container centered
|
||||
(native margin: 0 auto), with no side padding on the wrapper. The container
|
||||
carries the same vertical borders as the home .og-wrapper, with 32px of
|
||||
inner padding so the nav content sits inside the borders. */
|
||||
.VPNavBar .wrapper {
|
||||
padding-inline: 0 !important;
|
||||
}
|
||||
.VPNavBar .container {
|
||||
max-width: var(--vp-layout-max-width) !important;
|
||||
padding-inline: 32px;
|
||||
border-inline: var(--og-frame-border) solid var(--vp-c-divider);
|
||||
}
|
||||
|
||||
/* On docs pages the navbar gets `.has-sidebar`, which offsets the title to the
|
||||
sidebar width and changes the container. Undo it so the navbar lays out the
|
||||
same as on the landing page (centered, max-width container). */
|
||||
@media (min-width: 960px) {
|
||||
.VPNavBar.has-sidebar .wrapper {
|
||||
padding: 0 !important;
|
||||
}
|
||||
.VPNavBar.has-sidebar .container {
|
||||
max-width: var(--vp-layout-max-width) !important;
|
||||
}
|
||||
.VPNavBar.has-sidebar .title {
|
||||
position: static !important;
|
||||
width: auto !important;
|
||||
padding: 0 !important;
|
||||
}
|
||||
.VPNavBar.has-sidebar .content {
|
||||
padding-left: 0 !important;
|
||||
padding-right: 0 !important;
|
||||
}
|
||||
.VPNavBar.has-sidebar .divider {
|
||||
padding-left: 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* Horizontal section bar (Guide / API) below the top navbar. Desktop only;
|
||||
on mobile the top-nav menu provides section switching. */
|
||||
:root {
|
||||
--og-bar-height: 48px;
|
||||
/* Frame border width, between 1px and 2px without using px (0.1rem ≈ 1.6px). */
|
||||
--og-frame-border: 0.09rem;
|
||||
}
|
||||
|
||||
@media (min-width: 960px) {
|
||||
/* Push the sidebar, main content and outline down by the bar height. */
|
||||
.VPSidebar {
|
||||
padding-top: calc(var(--vp-nav-height) + var(--og-bar-height)) !important;
|
||||
}
|
||||
.VPContent.has-sidebar {
|
||||
padding-top: calc(var(--vp-nav-height) + var(--og-bar-height)) !important;
|
||||
}
|
||||
.VPDoc {
|
||||
--vp-doc-top-height: var(--og-bar-height);
|
||||
}
|
||||
}
|
||||
|
||||
/* Continue the navbar/home frame down the docs pages: the sidebar gets the
|
||||
outer-left border and the sidebar/content divider, and the content (.VPDoc)
|
||||
gets the outer-right border. Same style as the rest of the frame. */
|
||||
@media (min-width: 960px) {
|
||||
.VPSidebar {
|
||||
border-left: var(--og-frame-border) solid var(--vp-c-divider);
|
||||
border-right: var(--og-frame-border) solid var(--vp-c-divider);
|
||||
}
|
||||
.VPDoc {
|
||||
border-right: var(--og-frame-border) solid var(--vp-c-divider);
|
||||
}
|
||||
}
|
||||
|
||||
/* At >=1440px the layout is centered, so pull the sidebar box in to start at the
|
||||
frame's left edge (same X as the navbar's left border) instead of the viewport
|
||||
edge. Content/nav positions are unchanged; only the box origin and width move. */
|
||||
@media (min-width: 1440px) {
|
||||
.VPSidebar {
|
||||
left: calc((100% - var(--vp-layout-max-width)) / 2) !important;
|
||||
width: var(--vp-sidebar-width) !important;
|
||||
padding-left: 32px !important;
|
||||
}
|
||||
}
|
||||
|
||||
+114
@@ -0,0 +1,114 @@
|
||||
---
|
||||
aside: true
|
||||
---
|
||||
|
||||
# Opengist API Reference
|
||||
|
||||
Opengist exposes a REST API authenticated with Personal Access Tokens, intended
|
||||
for programmatic access to gist and user resources.
|
||||
|
||||
The base URL for the API is
|
||||
```
|
||||
https://opengist.example.com/api/
|
||||
```
|
||||
|
||||
> OpenAPI 3.1 spec is available at
|
||||
> [`openapi.yaml`](api/openapi.yaml).
|
||||
>
|
||||
> A running instance also serves the raw spec at `GET /api/openapi.yaml`.
|
||||
|
||||
## Getting an access token
|
||||
|
||||
The API authenticates with a Personal Access Token. To create one:
|
||||
|
||||
1. Go to **Settings**
|
||||
2. Select the **Access Tokens** menu
|
||||
3. Choose a name, select the scopes the token should grant and an optional
|
||||
expiration date, then click **Create Access Token**
|
||||
4. Copy the token (starting with `og_`). It is shown only once.
|
||||
|
||||
Tokens carry per-resource scopes, each at read or read/write level:
|
||||
|
||||
| Scope | Grants |
|
||||
|-------|--------|
|
||||
| `gist:read` | Read gists, including the caller's private and unlisted ones |
|
||||
| `gist:write` | Create, update, delete and fork gists |
|
||||
| `user:read` | Read the authenticated user's account |
|
||||
| `user:write` | Update the authenticated user and toggle likes |
|
||||
|
||||
## Authentication
|
||||
|
||||
Send the token in the `Authorization` header using the `Bearer` scheme:
|
||||
|
||||
```
|
||||
Authorization: Bearer og_xxxxxxxx
|
||||
```
|
||||
|
||||
Each endpoint documents the scope it requires in its **Headers** section.
|
||||
|
||||
Note that every endpoint requires authentication when an admin enables the "Require login" setting, which both works for the API and the web interface.
|
||||
|
||||
The single gist endpoints are available without authentication when an admin enables "Allow individual gists without login" setting.
|
||||
|
||||
|
||||
## Schema
|
||||
|
||||
The API lives under the `/api/` prefix on the same host as your Opengist
|
||||
instance. All data is sent and received as JSON.
|
||||
|
||||
Every endpoint responds with JSON unless specified otherwise
|
||||
```
|
||||
Content-Type: application/json
|
||||
```
|
||||
|
||||
All timestamps are returned in ISO 8601 / RFC 3339 format, in UTC:
|
||||
```
|
||||
2024-01-01T00:00:00Z
|
||||
```
|
||||
|
||||
## Pagination
|
||||
|
||||
List endpoints (such as `GET /gists`) return a JSON array and page the results.
|
||||
Tune the window with these query parameters:
|
||||
|
||||
| Parameter | Type | Default | Description |
|
||||
|-----------|------|---------|-------------|
|
||||
| `page` | integer | `1` | Page number, 1-based. |
|
||||
| `per_page` | integer | `30` | Items per page (maximum `100`). |
|
||||
| `since` | string (date-time) | — | Gist lists only: return only gists updated at or after this RFC 3339 timestamp. |
|
||||
|
||||
Pagination metadata is returned in the response headers:
|
||||
|
||||
| Header | Description |
|
||||
|--------|-------------|
|
||||
| `Link` | RFC 5988 links to other pages: `rel="next"` (when more pages exist) and `rel="prev"` (when `page > 1`). |
|
||||
| `X-Page` | The current page number (1-based). |
|
||||
| `X-Per-Page` | Items per page. |
|
||||
| `X-Total` | Total number of items across all pages. |
|
||||
| `X-Total-Pages` | Total number of pages, i.e. `ceil(total / per_page)`. |
|
||||
|
||||
The `Link` header is formatted as follows:
|
||||
|
||||
```
|
||||
Link: <https://opengist.example/api/gists?page=2&per_page=30>; rel="next",
|
||||
<https://opengist.example/api/gists?page=1&per_page=30>; rel="prev"
|
||||
```
|
||||
|
||||
|
||||
## Disabling the API
|
||||
|
||||
The API is **enabled by default**. To disable it, define it in the config as follows
|
||||
|
||||
### YAML
|
||||
```yaml
|
||||
api.enabled: false
|
||||
```
|
||||
|
||||
### Environment variable
|
||||
```shell
|
||||
OG_API_ENABLED=false
|
||||
```
|
||||
|
||||
While disabled, the routing layer returns `403` for every endpoint until it is
|
||||
enabled again. Disabling does not revoke issued tokens — they resume working
|
||||
once the API is turned back on.
|
||||
@@ -0,0 +1,10 @@
|
||||
---
|
||||
aside: false
|
||||
pageClass: api-page
|
||||
---
|
||||
<script setup>
|
||||
import { useData } from 'vitepress'
|
||||
const { params } = useData()
|
||||
</script>
|
||||
|
||||
<OpenApiOperation :id="params.id" />
|
||||
@@ -0,0 +1,10 @@
|
||||
import { listOperations } from '../.vitepress/openapi'
|
||||
|
||||
// One generated page per operation, keyed by operationId.
|
||||
export default {
|
||||
paths() {
|
||||
return listOperations().map((op) => ({
|
||||
params: { operation: op.id, id: op.id, title: `${op.method} ${op.path}` }
|
||||
}))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
---
|
||||
aside: false
|
||||
---
|
||||
<script setup>
|
||||
import { useData } from 'vitepress'
|
||||
const { params } = useData()
|
||||
</script>
|
||||
|
||||
<OpenApiSchema :name="params.name" />
|
||||
@@ -0,0 +1,8 @@
|
||||
import { listSchemas } from '../../.vitepress/openapi'
|
||||
|
||||
// One generated page per component schema, keyed by its name.
|
||||
export default {
|
||||
paths() {
|
||||
return listSchemas().map((name) => ({ params: { schema: name, name } }))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
description: Release notes and version history for Opengist — new features, fixes and changes in every release.
|
||||
outline: [2, 2]
|
||||
---
|
||||
|
||||
<!--@include: ../CHANGELOG.md-->
|
||||
@@ -15,11 +15,13 @@ aside: false
|
||||
| index | OG_INDEX | `bleve` | Define the code indexer (either `bleve`, `meilisearch`, or empty for no index). |
|
||||
| index.meili.host | OG_MEILI_HOST | none | Set the host for the Meiliseach server. |
|
||||
| index.meili.api-key | OG_MEILI_API_KEY | none | Set the API key for the Meiliseach server. |
|
||||
| search.default | OG_SEARCH_DEFAULT | `content` | Set the default search fields. Can contain multiple fields (e.g., `content,username`). Fields: `content,user,title,description,filename,extension,language,topic`. |
|
||||
| git.default-branch | OG_GIT_DEFAULT_BRANCH | none | Default branch name used by Opengist when initializing Git repositories. If not set, uses the Git default branch name. More info [here](https://git-scm.com/book/en/v2/Getting-Started-First-Time-Git-Setup#_new_default_branch) |
|
||||
| sqlite.journal-mode | OG_SQLITE_JOURNAL_MODE | `WAL` | Set the journal mode for SQLite. More info [here](https://www.sqlite.org/pragma.html#pragma_journal_mode) |
|
||||
| http.host | OG_HTTP_HOST | `0.0.0.0` | The host on which the HTTP server should bind. Use an IP address for network binding. Use a path for Unix socket binding (e.g. /run/opengist.sock) |
|
||||
| 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`) |
|
||||
| api.enabled | OG_API_ENABLED | `true` | Enable or disable the REST API. (`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. |
|
||||
@@ -28,7 +30,6 @@ aside: false
|
||||
| ssh.host | OG_SSH_HOST | `0.0.0.0` | The host on which the SSH server should bind. |
|
||||
| ssh.port | OG_SSH_PORT | `2222` | The port on which the SSH server should listen. |
|
||||
| ssh.external-domain | OG_SSH_EXTERNAL_DOMAIN | none | Public domain for the Git SSH connection, if it has to be different from the HTTP one. If not set, uses the URL from the request. |
|
||||
| ssh.keygen-executable | OG_SSH_KEYGEN_EXECUTABLE | `ssh-keygen` | Path to the SSH key generation executable. |
|
||||
| github.client-key | OG_GITHUB_CLIENT_KEY | none | The client key for the GitHub OAuth application. |
|
||||
| github.secret | OG_GITHUB_SECRET | none | The secret for the GitHub OAuth application. |
|
||||
| gitlab.client-key | OG_GITLAB_CLIENT_KEY | none | The client key for the GitLab OAuth application. |
|
||||
@@ -43,6 +44,8 @@ aside: false
|
||||
| oidc.client-key | OG_OIDC_CLIENT_KEY | none | The client key for the OpenID application. |
|
||||
| oidc.secret | OG_OIDC_SECRET | none | The secret for the OpenID application. |
|
||||
| oidc.discovery-url | OG_OIDC_DISCOVERY_URL | none | Discovery endpoint of the OpenID provider. |
|
||||
| oidc.group-claim-name | OG_OIDC_GROUP_CLAIM_NAME | none | Name of the claim containing the groups. |
|
||||
| oidc.admin-group | OG_OIDC_ADMIN_GROUP | none | Name of the group that should receive admin rights. |
|
||||
| ldap.url | OG_LDAP_URL | none | URL of the LDAP instance; if not set, LDAP authentication is disabled |
|
||||
| ldap.bind-dn | OG_LDAP_BIND_DN | none | Bind DN to authenticate against the LDAP. e.g: cn=read-only-admin,dc=example,dc=com |
|
||||
| ldap.bind-credentials | OG_LDAP_BIND_CREDENTIALS | none | The password for the Bind DN. |
|
||||
|
||||
@@ -27,7 +27,7 @@ Usage via command line :
|
||||
./opengist --config /path/to/config.yml
|
||||
```
|
||||
|
||||
You can start by copying and/or modifying the provided [config.yml](https://github.com/thomiceli/opengist/blob/stable/config.yml) file.
|
||||
You can start by copying and/or modifying the provided [config.yml](https://github.com/thomiceli/opengist/blob/master/config.yml) file.
|
||||
|
||||
|
||||
## Configuration via Environment Variables
|
||||
@@ -69,4 +69,4 @@ services:
|
||||
secrets:
|
||||
opengist_secrets:
|
||||
file: ./secrets.env
|
||||
```
|
||||
```
|
||||
|
||||
+3
-1
@@ -1,4 +1,6 @@
|
||||
---
|
||||
layout: home
|
||||
navbar: false
|
||||
title: Opengist — self-hosted pastebin powered by Git
|
||||
titleTemplate: false
|
||||
description: Opengist is an open-source, self-hosted pastebin powered by Git — a lightweight alternative to GitHub Gist that you host and fully control.
|
||||
---
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
---
|
||||
description: Install Opengist with Docker, Kubernetes, a prebuilt binary, or from source — pick the method that fits your environment.
|
||||
---
|
||||
|
||||
# Install Opengist
|
||||
|
||||
There are several ways to install Opengist, depending on your preferences and your environment.
|
||||
|
||||
- [Docker](installation/docker.md)
|
||||
- [Kubernetes](installation/kubernetes.md)
|
||||
- [Source](installation/source.md)
|
||||
- [Binary](installation/binary.md)
|
||||
|
||||
@@ -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.13.1/opengist1.13.1-linux-amd64.tar.gz
|
||||
|
||||
tar xzvf opengist1.12.1-linux-amd64.tar.gz
|
||||
tar xzvf opengist1.13.1-linux-amd64.tar.gz
|
||||
cd opengist
|
||||
chmod +x opengist
|
||||
./opengist # with or without `--config config.yml`
|
||||
|
||||
@@ -39,3 +39,28 @@ services:
|
||||
UID: 1001
|
||||
GID: 1001
|
||||
```
|
||||
|
||||
## Rootless
|
||||
|
||||
By default the container starts as `root` and the entrypoint drops privileges to the
|
||||
user defined by `UID`/`GID` (see above).
|
||||
|
||||
If you'd rather have the container run as a
|
||||
non-root user from the start — for example with `user:` in Compose, or under rootless
|
||||
Docker/Podman — set the `user` key instead:
|
||||
|
||||
```yml
|
||||
services:
|
||||
opengist:
|
||||
# ...
|
||||
user: "1001:1001"
|
||||
volumes:
|
||||
- "./opengist-data:/opengist"
|
||||
```
|
||||
|
||||
In this mode the entrypoint runs Opengist directly as that user.
|
||||
Create the Opengist data directory and own it on the host first:
|
||||
```shell
|
||||
mkdir -p ./opengist-data && sudo chown -R 1001:1001 ./opengist-data
|
||||
```
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ Requirements:
|
||||
git clone https://github.com/thomiceli/opengist
|
||||
cd opengist
|
||||
|
||||
git checkout v1.12.1 # optional, to checkout the latest release
|
||||
git checkout v1.13.1
|
||||
|
||||
make
|
||||
./opengist
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
---
|
||||
description: Opengist is a self-hosted pastebin powered by Git. Snippets are stored in Git repositories and managed via standard Git commands or the web interface.
|
||||
---
|
||||
|
||||
# Opengist
|
||||
|
||||
<img height="108px" src="https://raw.githubusercontent.com/thomiceli/opengist/master/public/img/opengist.svg" alt="Opengist" align="right" />
|
||||
|
||||
Generated
+3367
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"name": "opengist-docs",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"description": "Documentation site for Opengist, built with VitePress.",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vitepress dev",
|
||||
"build": "vitepress build",
|
||||
"preview": "vitepress preview"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/vite": "^4.2.4",
|
||||
"tailwindcss": "^4.2.4",
|
||||
"vitepress": "^2.0.0-alpha.17",
|
||||
"vue": "^3.5.27",
|
||||
"yaml": "^2.9.0"
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 702 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 1.6 MiB After Width: | Height: | Size: 666 KiB |
@@ -0,0 +1,4 @@
|
||||
User-agent: *
|
||||
Allow: /
|
||||
|
||||
Sitemap: https://opengist.io/sitemap.xml
|
||||
+2
-2
@@ -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.13.1/opengist1.13.1-linux-amd64.tar.gz
|
||||
|
||||
tar xzvf opengist1.12.1-linux-amd64.tar.gz
|
||||
tar xzvf opengist1.13.1-linux-amd64.tar.gz
|
||||
cd opengist
|
||||
chmod +x opengist
|
||||
./opengist # with or without `--config config.yml`
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
# Embed a Gist to your webpage
|
||||
|
||||
> [!Tip]
|
||||
> Fancy to enforce light or dark mode on the embedded Gist?
|
||||
> Just append `?light` or `?dark` to the Gist-URL.
|
||||
> Omitting this parameter will cause OpenGist to fallback to `auto`, thus the Browser deciding on the users preference.
|
||||
|
||||
To embed a Gist to your webpage, you can add a script tag with the URL of your gist followed by `.js` to your HTML page:
|
||||
|
||||
```html
|
||||
@@ -7,5 +12,17 @@ To embed a Gist to your webpage, you can add a script tag with the URL of your g
|
||||
|
||||
<!-- Dark mode: -->
|
||||
<script src="http://opengist.url/user/gist-url.js?dark"></script>
|
||||
<!-- Light mode: -->
|
||||
<script src="http://opengist.url/user/gist-url.js?light"></script>
|
||||
```
|
||||
|
||||
If you have a Gist that holds several different files, you can also explicitely call a specific file by its filename:
|
||||
|
||||
```html
|
||||
<script src="http://opengist.url/user/gist-url.js?file=filename"></script>
|
||||
|
||||
<!-- Dark mode: -->
|
||||
<script src="http://opengist.url/user/gist-url.js?file=filename&dark"></script>
|
||||
<!-- Light mode: -->
|
||||
<script src="http://opengist.url/user/gist-url.js?file=filename&light"></script>
|
||||
```
|
||||
|
||||
@@ -30,3 +30,20 @@ git push -o visibility=public
|
||||
git push -o visibility=unlisted
|
||||
git push -o visibility=private
|
||||
```
|
||||
|
||||
## Change topics
|
||||
|
||||
```shell
|
||||
git push -o topics="golang devops"
|
||||
```
|
||||
|
||||
## Set expiration
|
||||
|
||||
Only applies when creating a gist. The value is either a preset
|
||||
(`1hour`, `12hours`, `1day`, `7days`, `15days`) or a custom date
|
||||
(RFC3339, e.g. `2026-01-02T15:04:05Z`).
|
||||
|
||||
```shell
|
||||
git push -o expire=1day
|
||||
git push -o expire=2026-01-02T15:04:05Z
|
||||
```
|
||||
|
||||
@@ -1,36 +1,39 @@
|
||||
module github.com/thomiceli/opengist
|
||||
|
||||
go 1.25.5
|
||||
go 1.26.4
|
||||
|
||||
require (
|
||||
github.com/Kunde21/markdownfmt/v3 v3.1.0
|
||||
github.com/alecthomas/chroma/v2 v2.23.1
|
||||
github.com/blevesearch/bleve/v2 v2.5.7
|
||||
github.com/alecthomas/chroma/v2 v2.26.1
|
||||
github.com/blevesearch/bleve/v2 v2.6.0
|
||||
github.com/dustin/go-humanize v1.0.1
|
||||
github.com/gabriel-vasile/mimetype v1.4.12
|
||||
github.com/gabriel-vasile/mimetype v1.4.13
|
||||
github.com/glebarez/sqlite v1.11.0
|
||||
github.com/go-ldap/ldap/v3 v3.4.12
|
||||
github.com/go-playground/validator/v10 v10.30.1
|
||||
github.com/go-webauthn/webauthn v0.15.0
|
||||
github.com/go-ldap/ldap/v3 v3.4.13
|
||||
github.com/go-playground/validator/v10 v10.30.3
|
||||
github.com/go-webauthn/webauthn v0.17.4
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/gorilla/schema v1.4.1
|
||||
github.com/gorilla/securecookie v1.1.2
|
||||
github.com/gorilla/sessions v1.4.0
|
||||
github.com/labstack/echo-contrib v0.17.4
|
||||
github.com/labstack/echo/v4 v4.15.0
|
||||
github.com/labstack/echo-contrib v0.50.1
|
||||
github.com/labstack/echo/v4 v4.15.2
|
||||
github.com/markbates/goth v1.82.0
|
||||
github.com/meilisearch/meilisearch-go v0.36.0
|
||||
github.com/meilisearch/meilisearch-go v0.36.3
|
||||
github.com/oauth2-proxy/mockoidc v0.0.0-20240214162133-caebfff84d25
|
||||
github.com/pquerna/otp v1.5.0
|
||||
github.com/prometheus/client_golang v1.23.2
|
||||
github.com/rs/zerolog v1.34.0
|
||||
github.com/robfig/cron/v3 v3.0.1
|
||||
github.com/rs/zerolog v1.35.1
|
||||
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.8.2
|
||||
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.53.0
|
||||
golang.org/x/oauth2 v0.36.0
|
||||
golang.org/x/text v0.38.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
gorm.io/driver/mysql v1.6.0
|
||||
gorm.io/driver/postgres v1.6.0
|
||||
@@ -38,88 +41,88 @@ 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/andybalholm/brotli v1.2.0 // indirect
|
||||
filippo.io/edwards25519 v1.2.0 // indirect
|
||||
github.com/Azure/go-ntlmssp v0.1.1 // indirect
|
||||
github.com/RoaringBitmap/roaring/v2 v2.18.2 // indirect
|
||||
github.com/andybalholm/brotli v1.2.1 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/bits-and-blooms/bitset v1.24.4 // indirect
|
||||
github.com/blevesearch/bleve_index_api v1.3.1 // indirect
|
||||
github.com/blevesearch/geo v0.2.4 // indirect
|
||||
github.com/blevesearch/go-faiss v1.0.27 // indirect
|
||||
github.com/bits-and-blooms/bitset v1.24.5 // indirect
|
||||
github.com/blevesearch/bleve_index_api v1.3.12 // indirect
|
||||
github.com/blevesearch/geo v0.2.5 // indirect
|
||||
github.com/blevesearch/go-faiss v1.1.4 // 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/scorch_segment_api/v2 v2.4.7 // 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/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/v11 v11.4.3 // indirect
|
||||
github.com/blevesearch/zapx/v12 v12.4.3 // indirect
|
||||
github.com/blevesearch/zapx/v13 v13.4.3 // indirect
|
||||
github.com/blevesearch/zapx/v14 v14.4.3 // indirect
|
||||
github.com/blevesearch/zapx/v15 v15.4.3 // indirect
|
||||
github.com/blevesearch/zapx/v16 v16.3.4 // indirect
|
||||
github.com/blevesearch/zapx/v17 v17.1.6 // 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.7.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
|
||||
github.com/fxamacker/cbor/v2 v2.9.0 // indirect
|
||||
github.com/dlclark/regexp2/v2 v2.2.1 // indirect
|
||||
github.com/fxamacker/cbor/v2 v2.9.2 // indirect
|
||||
github.com/glebarez/go-sqlite v1.22.0 // indirect
|
||||
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 // indirect
|
||||
github.com/go-chi/chi/v5 v5.2.3 // indirect
|
||||
github.com/go-chi/chi/v5 v5.3.0 // indirect
|
||||
github.com/go-jose/go-jose/v3 v3.0.1 // indirect
|
||||
github.com/go-playground/locales v0.14.1 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||
github.com/go-sql-driver/mysql v1.9.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/v5 v5.3.0 // indirect
|
||||
github.com/go-sql-driver/mysql v1.10.0 // indirect
|
||||
github.com/go-viper/mapstructure/v2 v2.5.0 // indirect
|
||||
github.com/go-webauthn/x v0.2.6 // indirect
|
||||
github.com/golang-jwt/jwt/v5 v5.3.1 // indirect
|
||||
github.com/golang/snappy v1.0.0 // indirect
|
||||
github.com/google/go-tpm v0.9.6 // indirect
|
||||
github.com/google/go-tpm v0.9.8 // 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.10.0 // indirect
|
||||
github.com/jackc/puddle/v2 v2.2.2 // indirect
|
||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||
github.com/jinzhu/now v1.1.5 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/kr/text v0.2.0 // indirect
|
||||
github.com/labstack/gommon v0.4.2 // indirect
|
||||
github.com/labstack/gommon v0.5.0 // indirect
|
||||
github.com/leodido/go-urn v1.4.0 // indirect
|
||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.19 // indirect
|
||||
github.com/mattn/go-colorable v0.1.15 // indirect
|
||||
github.com/mattn/go-isatty v0.0.22 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.24 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/mschoch/smat v0.2.0 // indirect
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||
github.com/ncruces/go-strftime v1.0.0 // indirect
|
||||
github.com/philhofer/fwd v1.2.0 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/prometheus/client_model v0.6.2 // indirect
|
||||
github.com/prometheus/common v0.67.5 // indirect
|
||||
github.com/prometheus/procfs v0.19.2 // indirect
|
||||
github.com/prometheus/common v0.68.1 // indirect
|
||||
github.com/prometheus/procfs v0.20.1 // indirect
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||
github.com/tinylib/msgp v1.6.4 // indirect
|
||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||
github.com/valyala/fasttemplate v1.2.2 // indirect
|
||||
github.com/x448/float16 v0.8.4 // indirect
|
||||
github.com/xrash/smetrics v0.0.0-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/sync v0.19.0 // indirect
|
||||
golang.org/x/sys v0.40.0 // indirect
|
||||
golang.org/x/time v0.14.0 // indirect
|
||||
golang.org/x/net v0.55.0 // indirect
|
||||
golang.org/x/sync v0.21.0 // indirect
|
||||
golang.org/x/sys v0.46.0 // indirect
|
||||
golang.org/x/time v0.15.0 // indirect
|
||||
google.golang.org/protobuf v1.36.11 // indirect
|
||||
modernc.org/libc v1.67.7 // indirect
|
||||
modernc.org/libc v1.73.0 // 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.52.0 // indirect
|
||||
)
|
||||
|
||||
@@ -1,43 +1,43 @@
|
||||
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=
|
||||
filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo=
|
||||
filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc=
|
||||
github.com/Azure/go-ntlmssp v0.1.1 h1:l+FM/EEMb0U9QZE7mKNEDw5Mu3mFiaa2GKOoTSsNDPw=
|
||||
github.com/Azure/go-ntlmssp v0.1.1/go.mod h1:NYqdhxd/8aAct/s4qSYZEerdPuH1liG2/X9DiVTbhpk=
|
||||
github.com/Kunde21/markdownfmt/v3 v3.1.0 h1:KiZu9LKs+wFFBQKhrZJrFZwtLnCCWJahL+S+E/3VnM0=
|
||||
github.com/Kunde21/markdownfmt/v3 v3.1.0/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.18.2 h1:oPq3Cgx//iDuJQVp6xSInAKW34J9CEwE5GmLI2z+Eic=
|
||||
github.com/RoaringBitmap/roaring/v2 v2.18.2/go.mod h1:eq4wdNXxtJIS/oikeCzdX1rBzek7ANzbth041hrU8Q4=
|
||||
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.26.1 h1:2X21EdxGZNv5GF9mG5u+uzc02GCFyGxbcBm3Grd9A78=
|
||||
github.com/alecthomas/chroma/v2 v2.26.1/go.mod h1:lxhRRa9H4hPmRLOOdYga4zkQIQjq3dtrrdwQeCfu78Y=
|
||||
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=
|
||||
github.com/alexbrainman/sspi v0.0.0-20250919150558-7d374ff0d59e h1:4dAU9FXIyQktpoUAgOJK3OTFc/xug0PCXYCqU0FgDKI=
|
||||
github.com/alexbrainman/sspi v0.0.0-20250919150558-7d374ff0d59e/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4=
|
||||
github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ=
|
||||
github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
|
||||
github.com/andybalholm/brotli v1.2.1 h1:R+f5xP285VArJDRgowrfb9DqL18yVK0gKAW/F+eTWro=
|
||||
github.com/andybalholm/brotli v1.2.1/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/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/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/bits-and-blooms/bitset v1.24.5 h1:654xBVHc23gJMAgOTkPNoCVfiRxuIOAUnAZFtopqJ4w=
|
||||
github.com/bits-and-blooms/bitset v1.24.5/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
|
||||
github.com/blevesearch/bleve/v2 v2.6.0 h1:Cyd3dd4q5tCbOV8MnKUVRUDYMHOir9xn12NZzXVSEd4=
|
||||
github.com/blevesearch/bleve/v2 v2.6.0/go.mod h1:gLmI8lWgHgrIYf7UpUX7JISI1CaqC6VScu46mHThuAY=
|
||||
github.com/blevesearch/bleve_index_api v1.3.12 h1:MirVNltwGq8z0PhOgiQp+bKL5qq8OvCxEwOOC7NnHNE=
|
||||
github.com/blevesearch/bleve_index_api v1.3.12/go.mod h1:xvd48t5XMeeioWQ5/jZvgLrV98flT2rdvEJ3l/ki4Ko=
|
||||
github.com/blevesearch/geo v0.2.5 h1:yJg9FX1oRwLnjXSXF+ECHfXFTF4diF02Ca/qUGVjJhE=
|
||||
github.com/blevesearch/geo v0.2.5/go.mod h1:Jhq7WE2K6mJTx1xS44M2pUO6Io+wjCSHh1+co3YOgH4=
|
||||
github.com/blevesearch/go-faiss v1.1.4 h1:wGHK+yiOSIvBAQMr4LcTaHBFf9v1dBebs3WpFqT93Rg=
|
||||
github.com/blevesearch/go-faiss v1.1.4/go.mod h1:w3W9AiWsFRGVaMG+/cmJi7iHEAuGyC6blsgO1EzCK/M=
|
||||
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/scorch_segment_api/v2 v2.4.7 h1:GlMzW08hcsM3DnLUxhyF/1PcDal1qtvvIuytuph5djw=
|
||||
github.com/blevesearch/scorch_segment_api/v2 v2.4.7/go.mod h1://IJ7tG3QCf0cWW/aVSXqy77tc1AvLu3fcJLYEvOAFs=
|
||||
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=
|
||||
@@ -46,18 +46,20 @@ github.com/blevesearch/upsidedown_store_api v1.0.2 h1:U53Q6YoWEARVLd1OYNc9kvhBMG
|
||||
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/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=
|
||||
github.com/blevesearch/zapx/v12 v12.4.2/go.mod h1:TdFmr7afSz1hFh/SIBCCZvcLfzYvievIH6aEISCte58=
|
||||
github.com/blevesearch/zapx/v13 v13.4.2 h1:46PIZCO/ZuKZYgxI8Y7lOJqX3Irkc3N8W82QTK3MVks=
|
||||
github.com/blevesearch/zapx/v13 v13.4.2/go.mod h1:knK8z2NdQHlb5ot/uj8wuvOq5PhDGjNYQQy0QDnopZk=
|
||||
github.com/blevesearch/zapx/v14 v14.4.2 h1:2SGHakVKd+TrtEqpfeq8X+So5PShQ5nW6GNxT7fWYz0=
|
||||
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/v11 v11.4.3 h1:PTZOO5loKpHC/x/GzmPZNa9cw7GZIQxd5qRjwij9tHY=
|
||||
github.com/blevesearch/zapx/v11 v11.4.3/go.mod h1:4gdeyy9oGa/lLa6D34R9daXNUvfMPZqUYjPwiLmekwc=
|
||||
github.com/blevesearch/zapx/v12 v12.4.3 h1:eElXvAaAX4m04t//CGBQAtHNPA+Q6A1hHZVrN3LSFYo=
|
||||
github.com/blevesearch/zapx/v12 v12.4.3/go.mod h1:TdFmr7afSz1hFh/SIBCCZvcLfzYvievIH6aEISCte58=
|
||||
github.com/blevesearch/zapx/v13 v13.4.3 h1:qsdhRhaSpVnqDFlRiH9vG5+KJ+dE7KAW9WyZz/KXAiE=
|
||||
github.com/blevesearch/zapx/v13 v13.4.3/go.mod h1:knK8z2NdQHlb5ot/uj8wuvOq5PhDGjNYQQy0QDnopZk=
|
||||
github.com/blevesearch/zapx/v14 v14.4.3 h1:GY4Hecx0C6UTmiNC2pKdeA2rOKiLR5/rwpU9WR51dgM=
|
||||
github.com/blevesearch/zapx/v14 v14.4.3/go.mod h1:rz0XNb/OZSMjNorufDGSpFpjoFKhXmppH9Hi7a877D8=
|
||||
github.com/blevesearch/zapx/v15 v15.4.3 h1:iJiMJOHrz216jyO6lS0m9RTCEkprUnzvqAI2lc/0/CU=
|
||||
github.com/blevesearch/zapx/v15 v15.4.3/go.mod h1:1pssev/59FsuWcgSnTa0OeEpOzmhtmr/0/11H0Z8+Nw=
|
||||
github.com/blevesearch/zapx/v16 v16.3.4 h1:hDAqA8qusZTNbPEL7//w5P65UZ2de6yhSeUaTbp0Po0=
|
||||
github.com/blevesearch/zapx/v16 v16.3.4/go.mod h1:zqkPPqs9GS9FzVWzCO3Wf1X044yWAV17+4zb+FTiEHg=
|
||||
github.com/blevesearch/zapx/v17 v17.1.6 h1:rVGeyH0EPElBXM4PvjrCdt8LDdRLpa4GC1gMRQkCWUE=
|
||||
github.com/blevesearch/zapx/v17 v17.1.6/go.mod h1:c+mPvbZgZnDPOUS5Z9EXhntMcJnpIVjQTM9TF5yEGJM=
|
||||
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,11 +71,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/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||
github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk=
|
||||
github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
@@ -82,57 +81,61 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
|
||||
github.com/dlclark/regexp2 v1.7.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||
github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ=
|
||||
github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||
github.com/dlclark/regexp2/v2 v2.2.1 h1:mf4KkFUj0gJuarK8P+LgiS+Lit7m9N1yAwEfPbee7R0=
|
||||
github.com/dlclark/regexp2/v2 v2.2.1/go.mod h1:avUrQvPaLz2DrFNHJF0taWAFFX2C1GMSSoeiqFjcBmU=
|
||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||
github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM=
|
||||
github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
|
||||
github.com/gabriel-vasile/mimetype v1.4.12 h1:e9hWvmLYvtp846tLHam2o++qitpguFiYCKbn0w9jyqw=
|
||||
github.com/gabriel-vasile/mimetype v1.4.12/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
|
||||
github.com/fxamacker/cbor/v2 v2.9.2 h1:X4Ksno9+x3cz0TZv69ec1hxP/+tymuR8PXQJyDwfh78=
|
||||
github.com/fxamacker/cbor/v2 v2.9.2/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
|
||||
github.com/gabriel-vasile/mimetype v1.4.13 h1:46nXokslUBsAJE/wMsp5gtO500a4F3Nkz9Ufpk2AcUM=
|
||||
github.com/gabriel-vasile/mimetype v1.4.13/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
|
||||
github.com/glebarez/go-sqlite v1.22.0 h1:uAcMJhaA6r3LHMTFgP0SifzgXg46yJkgxqyuyec+ruQ=
|
||||
github.com/glebarez/go-sqlite v1.22.0/go.mod h1:PlBIdHe0+aUEFn+r2/uthrWq4FxbzugL0L8Li6yQJbc=
|
||||
github.com/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GMw=
|
||||
github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ=
|
||||
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 h1:BP4M0CvQ4S3TGls2FvczZtj5Re/2ZzkV9VwqPHH/3Bo=
|
||||
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
|
||||
github.com/go-chi/chi/v5 v5.2.3 h1:WQIt9uxdsAbgIYgid+BpYc+liqQZGMHRaUwp0JUcvdE=
|
||||
github.com/go-chi/chi/v5 v5.2.3/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=
|
||||
github.com/go-chi/chi/v5 v5.3.0 h1:halUjDxhshgXHMrao5bB8eNBXo/rnzwr8m5m36glehM=
|
||||
github.com/go-chi/chi/v5 v5.3.0/go.mod h1:R+tYY2hNuVUUjxoPtqUdgBqevM9s9njzkTLutVsOCto=
|
||||
github.com/go-jose/go-jose/v3 v3.0.1 h1:pWmKFVtt+Jl0vBZTIpz/eAKwsm6LkIxDVVbFHKkchhA=
|
||||
github.com/go-jose/go-jose/v3 v3.0.1/go.mod h1:RNkWWRld676jZEYoV3+XK8L2ZnNSvIsxFMht0mSX+u8=
|
||||
github.com/go-json-experiment/json v0.0.0-20250725192818-e39067aee2d2 h1:iizUGZ9pEquQS5jTGkh4AqeeHCMbfbjeb0zMt0aEFzs=
|
||||
github.com/go-json-experiment/json v0.0.0-20250725192818-e39067aee2d2/go.mod h1:TiCD2a1pcmjd7YnhGH0f/zKNcCD06B029pHhzV23c2M=
|
||||
github.com/go-ldap/ldap/v3 v3.4.12 h1:1b81mv7MagXZ7+1r7cLTWmyuTqVqdwbtJSjC0DAp9s4=
|
||||
github.com/go-ldap/ldap/v3 v3.4.12/go.mod h1:+SPAGcTtOfmGsCb3h1RFiq4xpp4N636G75OEace8lNo=
|
||||
github.com/go-ldap/ldap/v3 v3.4.13 h1:+x1nG9h+MZN7h/lUi5Q3UZ0fJ1GyDQYbPvbuH38baDQ=
|
||||
github.com/go-ldap/ldap/v3 v3.4.13/go.mod h1:LxsGZV6vbaK0sIvYfsv47rfh4ca0JXokCoKjZxsszv0=
|
||||
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||
github.com/go-playground/validator/v10 v10.30.1 h1:f3zDSN/zOma+w6+1Wswgd9fLkdwy06ntQJp0BBvFG0w=
|
||||
github.com/go-playground/validator/v10 v10.30.1/go.mod h1:oSuBIQzuJxL//3MelwSLD5hc2Tu889bF0Idm9Dg26cM=
|
||||
github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo=
|
||||
github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
|
||||
github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
|
||||
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
||||
github.com/go-webauthn/webauthn v0.15.0 h1:LR1vPv62E0/6+sTenX35QrCmpMCzLeVAcnXeH4MrbJY=
|
||||
github.com/go-webauthn/webauthn v0.15.0/go.mod h1:hcAOhVChPRG7oqG7Xj6XKN1mb+8eXTGP/B7zBLzkX5A=
|
||||
github.com/go-webauthn/x v0.1.26 h1:eNzreFKnwNLDFoywGh9FA8YOMebBWTUNlNSdolQRebs=
|
||||
github.com/go-webauthn/x v0.1.26/go.mod h1:jmf/phPV6oIsF6hmdVre+ovHkxjDOmNH0t6fekWUxvg=
|
||||
github.com/go-playground/validator/v10 v10.30.3 h1:4MU6YkEwx7GbcPJOZxrtbu+QfF3pJLJuaYTeAH0DYy8=
|
||||
github.com/go-playground/validator/v10 v10.30.3/go.mod h1:4Axh7oCNGcoGkqLoE4YWt6n20mcEIsPRlB7vPk3lpyc=
|
||||
github.com/go-sql-driver/mysql v1.10.0 h1:Q+1LV8DkHJvSYAdR83XzuhDaTykuDx0l6fkXxoWCWfw=
|
||||
github.com/go-sql-driver/mysql v1.10.0/go.mod h1:M+cqaI7+xxXGG9swrdeUIoPG3Y3KCkF0pZej+SK+nWk=
|
||||
github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro=
|
||||
github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
||||
github.com/go-webauthn/webauthn v0.17.4 h1:KFTSz3R2RYDiUn/0cDi3XTJgFenSG74eKTTHlqWhlxk=
|
||||
github.com/go-webauthn/webauthn v0.17.4/go.mod h1:pZk63EE/BdztlmyS4Yc+9H5g4a8blNlbtGmdHQHbZX8=
|
||||
github.com/go-webauthn/x v0.2.6 h1:TEyDuQAIiEgYpx60nKiBJIX/5nSUC8LxNbH+uf5U9uk=
|
||||
github.com/go-webauthn/x v0.2.6/go.mod h1:45bA7YEqyQhRcQJ/TiBb46Ww8yqHBGvgEhQ3WWF0aDo=
|
||||
github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU=
|
||||
github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM=
|
||||
github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og=
|
||||
github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
|
||||
github.com/gobwas/ws v1.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/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
|
||||
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
|
||||
github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY=
|
||||
github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
|
||||
github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs=
|
||||
github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/go-tpm v0.9.6 h1:Ku42PT4LmjDu1H5C5ISWLlpI1mj+Zq7sPGKoRw2XROA=
|
||||
github.com/google/go-tpm v0.9.6/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY=
|
||||
github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo=
|
||||
github.com/google/go-tpm v0.9.8/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY=
|
||||
github.com/google/go-tpm-tools v0.3.13-0.20230620182252-4639ecce2aba h1:qJEJcuLzH5KDR0gKc0zcktin6KSAwL7+jWKBYceddTc=
|
||||
github.com/google/go-tpm-tools v0.3.13-0.20230620182252-4639ecce2aba/go.mod h1:EFYHy8/1y2KfgTAsx7Luu7NGhoxtuVHnNo8jE7FikKc=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
|
||||
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
@@ -158,8 +161,8 @@ github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsI
|
||||
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.10.0 h1:VhSvgU2jSli8o3AqIEOTJr7rZwAEUVo4E4XhR94Zfr0=
|
||||
github.com/jackc/pgx/v5 v5.10.0/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4=
|
||||
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=
|
||||
@@ -188,27 +191,24 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
|
||||
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
||||
github.com/labstack/echo-contrib v0.17.4 h1:g5mfsrJfJTKv+F5uNKCyrjLK7js+ZW6HTjg4FnDxxgk=
|
||||
github.com/labstack/echo-contrib v0.17.4/go.mod h1:9O7ZPAHUeMGTOAfg80YqQduHzt0CzLak36PZRldYrZ0=
|
||||
github.com/labstack/echo/v4 v4.15.0 h1:hoRTKWcnR5STXZFe9BmYun9AMTNeSbjHi2vtDuADJ24=
|
||||
github.com/labstack/echo/v4 v4.15.0/go.mod h1:xmw1clThob0BSVRX1CRQkGQ/vjwcpOMjQZSZa9fKA/c=
|
||||
github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0=
|
||||
github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU=
|
||||
github.com/labstack/echo-contrib v0.50.1 h1:W9cZZ9viA4TDdFtm8cuA+XGFwOcnfbjJpl7VgfsRLHE=
|
||||
github.com/labstack/echo-contrib v0.50.1/go.mod h1:8r/++U/Fw/QniApFnzunLanKaviPfBX7fX7/2QX0qOk=
|
||||
github.com/labstack/echo/v4 v4.15.2 h1:nnh2sCzGCVYnU+wCisMPiYapEg/QVo/gcI9ePKg5/T4=
|
||||
github.com/labstack/echo/v4 v4.15.2/go.mod h1:Xzp1Ns1RA2c9fY7nSgUJkpkUZGNbEIVHZbtbOMPktBI=
|
||||
github.com/labstack/gommon v0.5.0 h1:6VSQ2NOzsnEJ5W6+84E0RbcaDDmgB6NIAzWCczTEe6c=
|
||||
github.com/labstack/gommon v0.5.0/go.mod h1:Rzlg7HHy1maLfzBYGg9NZcVuz1sA68HHhLjhcEllYE0=
|
||||
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
||||
github.com/markbates/goth v1.82.0 h1:8j/c34AjBSTNzO7zTsOyP5IYCQCMBTRBHAbBt/PI0bQ=
|
||||
github.com/markbates/goth v1.82.0/go.mod h1:/DRlcq0pyqkKToyZjsL2KgiA1zbF1HIjE7u2uC79rUk=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
||||
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-runewidth v0.0.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/mattn/go-colorable v0.1.15 h1:+u9SLTRGnXv73cEsnsmoZBom+dMU88B2M0aDcWy0/jY=
|
||||
github.com/mattn/go-colorable v0.1.15/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||
github.com/mattn/go-isatty v0.0.22 h1:j8l17JJ9i6VGPUFUYoTUKPSgKe/83EYU2zBC7YNKMw4=
|
||||
github.com/mattn/go-isatty v0.0.22/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4=
|
||||
github.com/mattn/go-runewidth v0.0.24 h1:cpokDiIn0MGnhdHwuWnJBITySJ20QyNGnY2kR/ay2DU=
|
||||
github.com/mattn/go-runewidth v0.0.24/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
|
||||
github.com/meilisearch/meilisearch-go v0.36.3 h1:Yx1aTY5jDgtbStPVkhJTDoLnZTy5sejQSPyjfNMy6e4=
|
||||
github.com/meilisearch/meilisearch-go v0.36.3/go.mod h1:hWcR0MuWLSzHfbz9GGzIr3s9rnXLm1jqkmHkJPbUSvM=
|
||||
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=
|
||||
@@ -220,7 +220,10 @@ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||
github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
|
||||
github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/oauth2-proxy/mockoidc v0.0.0-20240214162133-caebfff84d25 h1:9bCMuD3TcnjeqjPT2gSlha4asp8NvgcFRYExCaikCxk=
|
||||
github.com/oauth2-proxy/mockoidc v0.0.0-20240214162133-caebfff84d25/go.mod h1:eDjgYHYDJbPLBLsyZ6qRaugP0mX8vePOhZ5id1fdzJw=
|
||||
github.com/philhofer/fwd v1.2.0 h1:e6DnBTl7vGY+Gz322/ASL4Gyp1FspeMvx1RNDoToZuM=
|
||||
github.com/philhofer/fwd v1.2.0/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/pquerna/otp v1.5.0 h1:NMMR+WrmaqXU4EzdGJEE1aUUI0AMRzsp96fFFWNPwxs=
|
||||
@@ -229,24 +232,28 @@ 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.68.1 h1:omjRRl4QP4komogpXuhfeOiisQg7xdy8VM1UY+pStaY=
|
||||
github.com/prometheus/common v0.68.1/go.mod h1:ZzL3f6u94qUxh9p+tJTrF+FvBS1XXbbRAZCQkytAL0Y=
|
||||
github.com/prometheus/procfs v0.20.1 h1:XwbrGOIplXW/AU3YhIhLODXMJYyC1isLFfYCsTEycfc=
|
||||
github.com/prometheus/procfs v0.20.1/go.mod h1:o9EMBZGRyvDrSPH1RqdxhojkuXstoe4UlK79eF5TGGo=
|
||||
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/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
|
||||
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
|
||||
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
|
||||
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
|
||||
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
|
||||
github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY=
|
||||
github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ=
|
||||
github.com/rs/zerolog v1.35.1 h1:m7xQeoiLIiV0BCEY4Hs+j2NG4Gp2o2KPKmhnnLiazKI=
|
||||
github.com/rs/zerolog v1.35.1/go.mod h1:EjML9kdfa/RMA7h/6z6pYmq1ykOuA8/mjWaEvGI+jcw=
|
||||
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/tinylib/msgp v1.6.4 h1:mOwYbyYDLPj35mkA2BjjYejgJk9BuHxDdvRnb6v2ZcQ=
|
||||
github.com/tinylib/msgp v1.6.4/go.mod h1:RSp0LW9oSxFut3KzESt5Voq4GVWyS+PSulT77roAqEA=
|
||||
github.com/urfave/cli/v2 v2.27.7 h1:bH59vdhbjLv3LAvIu6gd0usJHgoTTPhCFib8qqOwXYU=
|
||||
github.com/urfave/cli/v2 v2.27.7/go.mod h1:CyNAG/xg+iAOg0N4MPGZqVmv2rCoP267496AOXUZjA4=
|
||||
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||
@@ -260,8 +267,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.8.2 h1:kEGpgqJXdgbkhcOgBxkC0X0PmoPG1ZyoZ117rDVp4zE=
|
||||
github.com/yuin/goldmark v1.8.2/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=
|
||||
@@ -274,33 +281,35 @@ go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
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/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
|
||||
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.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/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=
|
||||
go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ=
|
||||
go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto=
|
||||
golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio=
|
||||
golang.org/x/mod v0.36.0 h1:JJjpVx6myfUsUdAzZuOSTTmRE0PfZeNWzzvKrP7amb4=
|
||||
golang.org/x/mod v0.36.0/go.mod h1:moc6ELqsWcOw5Ef3xVprK5ul/MvtVvkIXLziUOICjUQ=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8=
|
||||
golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww=
|
||||
golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
|
||||
golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=
|
||||
golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM=
|
||||
golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw=
|
||||
golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/term v0.44.0 h1:0rLvDRCtNj0gZkyIXhCyOb2OAzEhLVqc4B+hrsBhrmc=
|
||||
golang.org/x/term v0.44.0/go.mod h1:7ze4MdzUzLXpSAoFP1H0bOI9aXDqveSvatT5vKcFh2Y=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE=
|
||||
golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4=
|
||||
golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
|
||||
golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
|
||||
golang.org/x/tools v0.45.0 h1:18qN3FAooORvApf5XjCXgsuayZOEtXf6JK18I3+ONa8=
|
||||
golang.org/x/tools v0.45.0/go.mod h1:LuUGqqaXcXMEFEruIVJVm5mgDD8vww/z/SR1gQ4uE/0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
@@ -315,30 +324,30 @@ 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/fileutil v1.3.40 h1:ZGMswMNc9JOCrcrakF1HrvmergNLAmxOPjizirpfqBA=
|
||||
modernc.org/fileutil v1.3.40/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc=
|
||||
modernc.org/cc/v4 v4.28.4 h1:Hd/4Es+MBj+/7hSdZaisNyu6bv3V0Dp2MdllyfqaH+c=
|
||||
modernc.org/cc/v4 v4.28.4/go.mod h1:OnovgIhbbMXMu1aISnJ0wvVD1KnW+cAUJkIrAWh+kVI=
|
||||
modernc.org/ccgo/v4 v4.34.4 h1:OVnSOWQjVKOYkFxoHYB+qQmSHK5gqMqARM+K9DpR/Ws=
|
||||
modernc.org/ccgo/v4 v4.34.4/go.mod h1:qdKqE8FNIYyysougB1RX9MxCzp5oJOcQXSobANJ4TuE=
|
||||
modernc.org/fileutil v1.4.0 h1:j6ZzNTftVS054gi281TyLjHPp6CPHr2KCxEXjEbD6SM=
|
||||
modernc.org/fileutil v1.4.0/go.mod h1:EqdKFDxiByqxLk8ozOxObDSfcVOv/54xDs/DUHdvCUU=
|
||||
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/gc/v3 v3.1.3 h1:6QAplYyVO+KdPW3pGnqmJDUxtkec8ooEWvks/hhU3lc=
|
||||
modernc.org/gc/v3 v3.1.3/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.73.0 h1:Y/KmTxbIN5T3x+NFjYOzV/+Ha7wKClfIecmTCTuYlqQ=
|
||||
modernc.org/libc v1.73.0/go.mod h1:DXZ3eO8qMCNn2SnmTNCiC71nJ9Rcq3PsnpU6Vc4rWK8=
|
||||
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=
|
||||
modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
|
||||
modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
|
||||
modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
|
||||
modernc.org/opt v0.2.0 h1:tGyef5ApycA7FSEOMraay9SaTk5zmbx7Tu+cJs4QKZg=
|
||||
modernc.org/opt v0.2.0/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.52.0 h1:p4dhYh2tXZCiyaqHwRVJDjIGKWyXayiQpThxgDzJaxo=
|
||||
modernc.org/sqlite v1.52.0/go.mod h1:tcNzv5p84E0skkmJn038y+hWJbLQXQqEnQfeh5r2JLM=
|
||||
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,5 +1,18 @@
|
||||
# Helm Chart Changelog
|
||||
|
||||
# 0.9.0 - 2026-06-10
|
||||
|
||||
- Bump Opengist image to 1.13.1
|
||||
|
||||
# 0.8.0 - 2026-06-09
|
||||
|
||||
- Bump Opengist image to 1.13.0
|
||||
|
||||
# 0.7.0 - 2026-03-14
|
||||
|
||||
- Bump Opengist image to 1.12.2
|
||||
- Add environment variables and secrets to statefulset
|
||||
|
||||
## 0.6.0 - 2026-02-03
|
||||
|
||||
- Bump Opengist image to 1.12.1
|
||||
|
||||
@@ -4,6 +4,6 @@ dependencies:
|
||||
version: 16.7.27
|
||||
- name: meilisearch
|
||||
repository: https://meilisearch.github.io/meilisearch-kubernetes
|
||||
version: 0.17.1
|
||||
digest: sha256:ad702e35f258fed1f804d3e48b071767499f5730e099a8c461610950e5182368
|
||||
generated: "2025-09-21T04:49:08.679554149+02:00"
|
||||
version: 0.26.0
|
||||
digest: sha256:7182bad3df032b3cb21a793ea6b027eaa96e142ff207b607b62df974bc82de90
|
||||
generated: "2026-03-09T03:39:04.820136+07:00"
|
||||
|
||||
@@ -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.9.0
|
||||
appVersion: 1.13.1
|
||||
home: https://opengist.io
|
||||
icon: https://raw.githubusercontent.com/thomiceli/opengist/master/public/img/opengist.svg
|
||||
sources:
|
||||
@@ -15,5 +15,5 @@ dependencies:
|
||||
condition: postgresql.enabled
|
||||
- name: meilisearch
|
||||
repository: https://meilisearch.github.io/meilisearch-kubernetes
|
||||
version: 0.17.1
|
||||
version: 0.26.0
|
||||
condition: meilisearch.enabled
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Opengist Helm Chart
|
||||
|
||||
 
|
||||
 
|
||||
|
||||
Opengist Helm chart for Kubernetes. Check [CHANGELOG.md](CHANGELOG.md) for release notes.
|
||||
|
||||
|
||||
@@ -63,6 +63,19 @@ spec:
|
||||
{{- toYaml .Values.securityContext | nindent 12 }}
|
||||
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
|
||||
imagePullPolicy: {{ .Values.image.pullPolicy }}
|
||||
{{- if or .Values.deployment.env .Values.deployment.envFromSecrets }}
|
||||
env:
|
||||
{{- if .Values.deployment.env }}
|
||||
{{- toYaml .Values.deployment.env | nindent 12 }}
|
||||
{{- end }}
|
||||
{{- range .Values.deployment.envFromSecrets }}
|
||||
- name: {{ .name }}
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: {{ .secretName }}
|
||||
key: {{ .secretKey }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
ports:
|
||||
- name: http
|
||||
containerPort: {{ .Values.service.http.port }}
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
{{- $user := default "" .Values.postgresql.global.postgresql.auth.username }}
|
||||
{{- $pass := default "" .Values.postgresql.global.postgresql.auth.password }}
|
||||
{{- $db := default "" .Values.postgresql.global.postgresql.auth.database }}
|
||||
{{- $port := default 5432 .Values.postgresql.global.postgresql.service.ports.postgresql }}
|
||||
{{- $port := int (default 5432 .Values.postgresql.global.postgresql.service.ports.postgresql) }}
|
||||
{{- if or (eq $user "") (eq $pass "") (eq $db "") }}
|
||||
{{- fail "postgresql.enabled=true requires username/password/database (postgresql.global.postgresql.auth.*) or set config.db-uri manually" }}
|
||||
{{- end }}
|
||||
|
||||
@@ -84,7 +84,7 @@ spec:
|
||||
serviceName: {{ include "opengist.fullname" . }}-http
|
||||
podManagementPolicy: {{ .Values.statefulSet.podManagementPolicy }}
|
||||
updateStrategy:
|
||||
{{- toYaml .Values.statefulSet.updateStrategy | nindent 2 }}
|
||||
{{- toYaml .Values.statefulSet.updateStrategy | nindent 4 }}
|
||||
selector:
|
||||
matchLabels:
|
||||
{{- include "opengist.selectorLabels" . | nindent 6 }}
|
||||
@@ -131,6 +131,19 @@ spec:
|
||||
{{- toYaml .Values.securityContext | nindent 12 }}
|
||||
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
|
||||
imagePullPolicy: {{ .Values.image.pullPolicy }}
|
||||
{{- if or .Values.deployment.env .Values.deployment.envFromSecrets }}
|
||||
env:
|
||||
{{- if .Values.deployment.env }}
|
||||
{{- toYaml .Values.deployment.env | nindent 12 }}
|
||||
{{- end }}
|
||||
{{- range .Values.deployment.envFromSecrets }}
|
||||
- name: {{ .name }}
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: {{ .secretName }}
|
||||
key: {{ .secretKey }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
ports:
|
||||
- name: http
|
||||
containerPort: {{ .Values.service.http.port }}
|
||||
|
||||
@@ -18,7 +18,7 @@ configExistingSecret: ""
|
||||
image:
|
||||
repository: ghcr.io/thomiceli/opengist
|
||||
pullPolicy: Always
|
||||
tag: "1.12.1"
|
||||
tag: "1.13.1"
|
||||
digest: ""
|
||||
imagePullSecrets: []
|
||||
# - name: "image-pull-secret"
|
||||
@@ -66,7 +66,11 @@ statefulSet:
|
||||
podSecurityContext:
|
||||
fsGroup: 1000
|
||||
securityContext: {}
|
||||
# allowPrivilegeEscalation: false
|
||||
# runAsUser: 1000
|
||||
# runAsGroup: 1000
|
||||
# runAsNonRoot: true
|
||||
# allowPrivilegeEscalation: false
|
||||
# readOnlyRootFilesystem: true
|
||||
|
||||
## Pod Disruption Budget settings
|
||||
## ref: https://kubernetes.io/docs/tasks/run-application/configure-pdb/
|
||||
@@ -258,6 +262,13 @@ autoscaling:
|
||||
## Additional deployment configuration
|
||||
deployment:
|
||||
env: []
|
||||
## Load environment variables from specific secret keys
|
||||
## Each entry creates an env.valueFrom.secretKeyRef in the container spec
|
||||
## ref: https://kubernetes.io/docs/concepts/configuration/secret/#using-secrets-as-environment-variables
|
||||
envFromSecrets: []
|
||||
# - name: OG_OIDC_SECRET
|
||||
# secretName: opengist-oidc-client-secret
|
||||
# secretKey: client_secret
|
||||
terminationGracePeriodSeconds: 60
|
||||
labels: {}
|
||||
annotations: {}
|
||||
|
||||
+76
-51
@@ -1,21 +1,20 @@
|
||||
package actions
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/thomiceli/opengist/internal/config"
|
||||
"github.com/thomiceli/opengist/internal/db"
|
||||
"github.com/thomiceli/opengist/internal/git"
|
||||
"github.com/thomiceli/opengist/internal/index"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type ActionStatus struct {
|
||||
Running bool
|
||||
}
|
||||
|
||||
const (
|
||||
SyncReposFromFS = iota
|
||||
SyncReposFromDB
|
||||
@@ -24,63 +23,71 @@ const (
|
||||
ResetHooks
|
||||
IndexGists
|
||||
SyncGistLanguages
|
||||
DeleteExpiredGists
|
||||
|
||||
numActions // keep last — sizes the `running` array
|
||||
)
|
||||
|
||||
var (
|
||||
mutex sync.Mutex
|
||||
actions = make(map[int]ActionStatus)
|
||||
)
|
||||
// running tracks which actions are in progress in this instance, one slot per
|
||||
// action type. It dedupes concurrent runs (e.g. a double-clicked admin button)
|
||||
// and backs IsRunning for the admin panel; cross-instance single-flighting is
|
||||
// handled separately by the DB action lock.
|
||||
var running [numActions]atomic.Bool
|
||||
|
||||
func updateActionStatus(actionType int, running bool) {
|
||||
actions[actionType] = ActionStatus{
|
||||
Running: running,
|
||||
}
|
||||
const lockLease = time.Hour
|
||||
|
||||
type action struct {
|
||||
run func()
|
||||
spec string
|
||||
}
|
||||
|
||||
var registry = map[int]action{
|
||||
SyncReposFromFS: {run: syncReposFromFS},
|
||||
SyncReposFromDB: {run: syncReposFromDB},
|
||||
GitGcRepos: {run: gitGcRepos},
|
||||
SyncGistPreviews: {run: syncGistPreviews},
|
||||
ResetHooks: {run: resetHooks},
|
||||
IndexGists: {run: indexGists},
|
||||
SyncGistLanguages: {run: syncGistLanguages},
|
||||
DeleteExpiredGists: {run: deleteExpiredGists, spec: "@every 1m"},
|
||||
}
|
||||
|
||||
func IsRunning(actionType int) bool {
|
||||
mutex.Lock()
|
||||
defer mutex.Unlock()
|
||||
return actions[actionType].Running
|
||||
return actionType >= 0 && actionType < numActions && running[actionType].Load()
|
||||
}
|
||||
|
||||
func Run(actionType int) {
|
||||
mutex.Lock()
|
||||
|
||||
if actions[actionType].Running {
|
||||
mutex.Unlock()
|
||||
func RunOnce(actionType int) {
|
||||
a, ok := registry[actionType]
|
||||
if !ok {
|
||||
log.Error().Msgf("Unknown action type %d", actionType)
|
||||
return
|
||||
}
|
||||
|
||||
updateActionStatus(actionType, true)
|
||||
mutex.Unlock()
|
||||
if !running[actionType].CompareAndSwap(false, true) {
|
||||
return // already running in this instance
|
||||
}
|
||||
defer running[actionType].Store(false)
|
||||
|
||||
// Single-flight the action across instances sharing the database so only
|
||||
// one replica runs it at a time, whether triggered by the scheduler or
|
||||
// manually.
|
||||
acquired, err := db.AcquireLock(actionType, lockLease)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msgf("Could not acquire lock for action %d", actionType)
|
||||
return
|
||||
}
|
||||
if !acquired {
|
||||
return
|
||||
}
|
||||
defer func() {
|
||||
mutex.Lock()
|
||||
updateActionStatus(actionType, false)
|
||||
mutex.Unlock()
|
||||
if err := db.ReleaseLock(actionType); err != nil {
|
||||
log.Error().Err(err).Msgf("Could not release lock for action %d", actionType)
|
||||
}
|
||||
}()
|
||||
|
||||
var functionToRun func()
|
||||
switch actionType {
|
||||
case SyncReposFromFS:
|
||||
functionToRun = syncReposFromFS
|
||||
case SyncReposFromDB:
|
||||
functionToRun = syncReposFromDB
|
||||
case GitGcRepos:
|
||||
functionToRun = gitGcRepos
|
||||
case SyncGistPreviews:
|
||||
functionToRun = syncGistPreviews
|
||||
case ResetHooks:
|
||||
functionToRun = resetHooks
|
||||
case IndexGists:
|
||||
functionToRun = indexGists
|
||||
case SyncGistLanguages:
|
||||
functionToRun = syncGistLanguages
|
||||
default:
|
||||
log.Error().Msg("Unknown action type")
|
||||
}
|
||||
|
||||
functionToRun()
|
||||
log.Info().Msgf("Starting running action %d", actionType)
|
||||
a.run()
|
||||
log.Info().Msgf("Finished running action %d", actionType)
|
||||
}
|
||||
|
||||
func syncReposFromFS() {
|
||||
@@ -136,6 +143,7 @@ func syncGistPreviews() {
|
||||
return
|
||||
}
|
||||
for _, gist := range gists {
|
||||
fmt.Println("Syncing preview for gist", gist.ID)
|
||||
if err = gist.UpdatePreviewAndCount(false); err != nil {
|
||||
log.Error().Err(err).Msgf("Cannot update preview and count for gist %d", gist.ID)
|
||||
}
|
||||
@@ -150,7 +158,12 @@ func resetHooks() {
|
||||
}
|
||||
|
||||
func indexGists() {
|
||||
log.Info().Msg("Indexing all Gists...")
|
||||
log.Info().Msg("Rebuilding index from scratch...")
|
||||
if err := index.ResetIndex(); err != nil {
|
||||
log.Error().Err(err).Msg("Cannot reset index")
|
||||
return
|
||||
}
|
||||
|
||||
gists, err := db.GetAllGistsRows()
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Cannot get gists")
|
||||
@@ -183,3 +196,15 @@ func syncGistLanguages() {
|
||||
gist.UpdateLanguages()
|
||||
}
|
||||
}
|
||||
|
||||
func deleteExpiredGists() {
|
||||
gists, err := db.DeleteExpiredGists()
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Cannot delete expired gists")
|
||||
return
|
||||
}
|
||||
|
||||
if len(gists) > 0 {
|
||||
log.Info().Msgf("Deleted %d expired gist(s)", len(gists))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
package actions
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/robfig/cron/v3"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
const cronDrainTimeout = 10 * time.Second
|
||||
|
||||
// StartCron registers every scheduled action in `registry` (those with a spec)
|
||||
// and starts the scheduler. It returns a stop function that halts the scheduler
|
||||
// and waits (up to cronDrainTimeout) for any in-flight job to finish — call it
|
||||
// before tearing down the DB so a running action can release its lock cleanly.
|
||||
// Panicking jobs are recovered so a single failed run can't take down the server.
|
||||
func StartCron() (stop func()) {
|
||||
c := cron.New(cron.WithChain(cron.Recover(cronLogger{})))
|
||||
|
||||
for actionType, a := range registry {
|
||||
if a.spec == "" {
|
||||
continue
|
||||
}
|
||||
if _, err := c.AddFunc(a.spec, func() { RunOnce(actionType) }); err != nil {
|
||||
log.Error().Err(err).Msgf("Invalid cron spec %q for action %d", a.spec, actionType)
|
||||
}
|
||||
}
|
||||
|
||||
c.Start()
|
||||
|
||||
return func() {
|
||||
log.Info().Msg("Stopping crons...")
|
||||
select {
|
||||
case <-c.Stop().Done():
|
||||
case <-time.After(cronDrainTimeout):
|
||||
log.Warn().Msg("cron: timed out waiting for jobs to finish")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type cronLogger struct{}
|
||||
|
||||
func (cronLogger) Info(msg string, _ ...interface{}) {
|
||||
log.Info().Msgf("cron: %s", msg)
|
||||
}
|
||||
|
||||
func (cronLogger) Error(err error, msg string, _ ...interface{}) {
|
||||
log.Error().Err(err).Msgf("cron: %s", msg)
|
||||
}
|
||||
@@ -2,16 +2,15 @@ package oauth
|
||||
|
||||
import (
|
||||
gocontext "context"
|
||||
gojson "encoding/json"
|
||||
|
||||
"net/http"
|
||||
|
||||
"github.com/markbates/goth"
|
||||
"github.com/markbates/goth/gothic"
|
||||
"github.com/markbates/goth/providers/gitea"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/thomiceli/opengist/internal/config"
|
||||
"github.com/thomiceli/opengist/internal/db"
|
||||
"github.com/thomiceli/opengist/internal/web/context"
|
||||
"io"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type GiteaProvider struct {
|
||||
@@ -80,34 +79,11 @@ func (p *GiteaCallbackProvider) GetProviderUserSSHKeys() ([]string, error) {
|
||||
|
||||
func (p *GiteaCallbackProvider) UpdateUserDB(user *db.User) {
|
||||
user.GiteaID = p.User.UserID
|
||||
user.AvatarURL = p.User.AvatarURL
|
||||
}
|
||||
|
||||
resp, err := http.Get(urlJoin(config.C.GiteaUrl, "/api/v1/users/", p.User.UserID))
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Cannot get user from Gitea")
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Cannot read Gitea response body")
|
||||
return
|
||||
}
|
||||
|
||||
var result map[string]interface{}
|
||||
err = gojson.Unmarshal(body, &result)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Cannot unmarshal Gitea response body")
|
||||
return
|
||||
}
|
||||
|
||||
field, ok := result["avatar_url"]
|
||||
if !ok {
|
||||
log.Error().Msg("Field 'avatar_url' not found in Gitea JSON response")
|
||||
return
|
||||
}
|
||||
|
||||
user.AvatarURL = field.(string)
|
||||
func (p *GiteaCallbackProvider) IsAdmin() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func NewGiteaCallbackProvider(user *goth.User) CallbackProvider {
|
||||
|
||||
@@ -77,6 +77,10 @@ func (p *GitHubCallbackProvider) UpdateUserDB(user *db.User) {
|
||||
user.AvatarURL = "https://avatars.githubusercontent.com/u/" + p.User.UserID + "?v=4"
|
||||
}
|
||||
|
||||
func (p *GitHubCallbackProvider) IsAdmin() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func NewGitHubCallbackProvider(user *goth.User) CallbackProvider {
|
||||
return &GitHubCallbackProvider{
|
||||
User: user,
|
||||
|
||||
@@ -111,6 +111,10 @@ func (p *GitLabCallbackProvider) UpdateUserDB(user *db.User) {
|
||||
user.AvatarURL = field.(string)
|
||||
}
|
||||
|
||||
func (p *GitLabCallbackProvider) IsAdmin() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func NewGitLabCallbackProvider(user *goth.User) CallbackProvider {
|
||||
return &GitLabCallbackProvider{
|
||||
User: user,
|
||||
|
||||
@@ -3,9 +3,12 @@ package oauth
|
||||
import (
|
||||
gocontext "context"
|
||||
"errors"
|
||||
"slices"
|
||||
|
||||
"github.com/markbates/goth"
|
||||
"github.com/markbates/goth/gothic"
|
||||
"github.com/markbates/goth/providers/openidConnect"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/thomiceli/opengist/internal/config"
|
||||
"github.com/thomiceli/opengist/internal/db"
|
||||
"github.com/thomiceli/opengist/internal/web/context"
|
||||
@@ -37,6 +40,10 @@ func (p *OIDCProvider) RegisterProvider() error {
|
||||
}
|
||||
|
||||
func (p *OIDCProvider) BeginAuthHandler(ctx *context.Context) {
|
||||
if err := enablePKCE(ctx, OpenIDConnectString); err != nil {
|
||||
log.Error().Err(err).Msg("Cannot enable PKCE for OIDC provider")
|
||||
}
|
||||
|
||||
ctxValue := gocontext.WithValue(ctx.Request().Context(), gothic.ProviderParamKey, OpenIDConnectString)
|
||||
ctx.SetRequest(ctx.Request().WithContext(ctxValue))
|
||||
|
||||
@@ -79,6 +86,31 @@ func (p *OIDCCallbackProvider) UpdateUserDB(user *db.User) {
|
||||
user.AvatarURL = p.User.AvatarURL
|
||||
}
|
||||
|
||||
func (p *OIDCCallbackProvider) IsAdmin() bool {
|
||||
if config.C.OIDCAdminGroup == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
groupClaimName := config.C.OIDCGroupClaimName
|
||||
if groupClaimName == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
groups, ok := p.User.RawData[groupClaimName].([]interface{})
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
|
||||
var groupNames []string
|
||||
for _, group := range groups {
|
||||
if groupName, ok := group.(string); ok {
|
||||
groupNames = append(groupNames, groupName)
|
||||
}
|
||||
}
|
||||
|
||||
return slices.Contains(groupNames, config.C.OIDCAdminGroup)
|
||||
}
|
||||
|
||||
func NewOIDCCallbackProvider(user *goth.User) CallbackProvider {
|
||||
return &OIDCCallbackProvider{
|
||||
User: user,
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
package oauth
|
||||
|
||||
import (
|
||||
"github.com/markbates/goth"
|
||||
"github.com/markbates/goth/providers/openidConnect"
|
||||
"github.com/thomiceli/opengist/internal/web/context"
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
const codeVerifierSessionKey = "oauth_code_verifier"
|
||||
|
||||
func enablePKCE(ctx *context.Context, providerName string) error {
|
||||
gothProvider, err := goth.GetProvider(providerName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
oidcProvider, ok := gothProvider.(*openidConnect.Provider)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
verifier := oauth2.GenerateVerifier()
|
||||
|
||||
sess := ctx.GetSession()
|
||||
sess.Values[codeVerifierSessionKey] = verifier
|
||||
ctx.SaveSession(sess)
|
||||
|
||||
oidcProvider.SetAuthCodeOptions(map[string]string{
|
||||
"code_challenge": oauth2.S256ChallengeFromVerifier(verifier),
|
||||
"code_challenge_method": "S256",
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func injectCodeVerifier(ctx *context.Context) {
|
||||
sess := ctx.GetSession()
|
||||
verifier, ok := sess.Values[codeVerifierSessionKey].(string)
|
||||
if !ok || verifier == "" {
|
||||
return
|
||||
}
|
||||
|
||||
req := ctx.Request()
|
||||
q := req.URL.Query()
|
||||
q.Set("code_verifier", verifier)
|
||||
req.URL.RawQuery = q.Encode()
|
||||
|
||||
delete(sess.Values, codeVerifierSessionKey)
|
||||
ctx.SaveSession(sess)
|
||||
}
|
||||
@@ -2,15 +2,16 @@ package oauth
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/markbates/goth"
|
||||
"github.com/markbates/goth/gothic"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/thomiceli/opengist/internal/db"
|
||||
"github.com/thomiceli/opengist/internal/web/context"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -32,6 +33,7 @@ type CallbackProvider interface {
|
||||
GetProviderUserID(user *db.User) bool
|
||||
GetProviderUserSSHKeys() ([]string, error)
|
||||
UpdateUserDB(user *db.User)
|
||||
IsAdmin() bool
|
||||
}
|
||||
|
||||
func DefineProvider(provider string, url string) (Provider, error) {
|
||||
@@ -50,6 +52,8 @@ func DefineProvider(provider string, url string) (Provider, error) {
|
||||
}
|
||||
|
||||
func CompleteUserAuth(ctx *context.Context) (CallbackProvider, error) {
|
||||
injectCodeVerifier(ctx)
|
||||
|
||||
user, err := gothic.CompleteUserAuth(ctx.Response(), ctx.Request())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -69,6 +73,29 @@ func CompleteUserAuth(ctx *context.Context) (CallbackProvider, error) {
|
||||
return nil, fmt.Errorf("unsupported provider %s", user.Provider)
|
||||
}
|
||||
|
||||
func NewCallbackProviderFromSession(provider string, userID string, nickname string, email string, avatarURL string) (CallbackProvider, error) {
|
||||
user := &goth.User{
|
||||
Provider: provider,
|
||||
UserID: userID,
|
||||
NickName: nickname,
|
||||
Email: email,
|
||||
AvatarURL: avatarURL,
|
||||
}
|
||||
|
||||
switch provider {
|
||||
case GitHubProviderString:
|
||||
return NewGitHubCallbackProvider(user), nil
|
||||
case GitLabProviderString:
|
||||
return NewGitLabCallbackProvider(user), nil
|
||||
case GiteaProviderString:
|
||||
return NewGiteaCallbackProvider(user), nil
|
||||
case OpenIDConnectString:
|
||||
return NewOIDCCallbackProvider(user), nil
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("unsupported provider %s", provider)
|
||||
}
|
||||
|
||||
func urlJoin(base string, elem ...string) string {
|
||||
joined, err := url.JoinPath(base, elem...)
|
||||
if err != nil {
|
||||
|
||||
+10
-6
@@ -2,7 +2,14 @@ package cli
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/signal"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"syscall"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/thomiceli/opengist/internal/actions"
|
||||
"github.com/thomiceli/opengist/internal/auth/webauthn"
|
||||
"github.com/thomiceli/opengist/internal/config"
|
||||
"github.com/thomiceli/opengist/internal/db"
|
||||
@@ -12,11 +19,6 @@ import (
|
||||
"github.com/thomiceli/opengist/internal/web/handlers/metrics"
|
||||
"github.com/thomiceli/opengist/internal/web/server"
|
||||
"github.com/urfave/cli/v2"
|
||||
"os"
|
||||
"os/signal"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
var CmdVersion = cli.Command{
|
||||
@@ -37,9 +39,10 @@ var CmdStart = cli.Command{
|
||||
|
||||
Initialize(ctx)
|
||||
|
||||
httpServer := server.NewServer(os.Getenv("OG_DEV") == "1", path.Join(config.GetHomeDir(), "sessions"), false)
|
||||
httpServer := server.NewServer(os.Getenv("OG_DEV") == "1")
|
||||
go httpServer.Start()
|
||||
go ssh.Start()
|
||||
stopCron := actions.StartCron()
|
||||
|
||||
var metricsServer *metrics.Server
|
||||
if config.C.MetricsEnabled {
|
||||
@@ -48,6 +51,7 @@ var CmdStart = cli.Command{
|
||||
}
|
||||
|
||||
<-stopCtx.Done()
|
||||
stopCron()
|
||||
shutdown(httpServer, metricsServer)
|
||||
return nil
|
||||
},
|
||||
|
||||
@@ -2,7 +2,6 @@ package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/thomiceli/opengist/internal/session"
|
||||
"io"
|
||||
"net/url"
|
||||
"os"
|
||||
@@ -13,6 +12,8 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/thomiceli/opengist/internal/session"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/rs/zerolog/log"
|
||||
"gopkg.in/yaml.v3"
|
||||
@@ -37,11 +38,12 @@ type config struct {
|
||||
DBUri string `yaml:"db-uri" env:"OG_DB_URI"`
|
||||
DBFilename string `yaml:"db-filename" env:"OG_DB_FILENAME"` // deprecated
|
||||
|
||||
IndexEnabled bool `yaml:"index.enabled" env:"OG_INDEX_ENABLED"` // deprecated
|
||||
Index string `yaml:"index" env:"OG_INDEX"`
|
||||
BleveDirname string `yaml:"index.dirname" env:"OG_INDEX_DIRNAME"` // deprecated
|
||||
MeiliHost string `yaml:"index.meili.host" env:"OG_MEILI_HOST"`
|
||||
MeiliAPIKey string `yaml:"index.meili.api-key" env:"OG_MEILI_API_KEY"`
|
||||
IndexEnabled bool `yaml:"index.enabled" env:"OG_INDEX_ENABLED"` // deprecated
|
||||
Index string `yaml:"index" env:"OG_INDEX"`
|
||||
BleveDirname string `yaml:"index.dirname" env:"OG_INDEX_DIRNAME"` // deprecated
|
||||
MeiliHost string `yaml:"index.meili.host" env:"OG_MEILI_HOST"`
|
||||
MeiliAPIKey string `yaml:"index.meili.api-key" env:"OG_MEILI_API_KEY"`
|
||||
SearchDefault string `yaml:"search.default" env:"OG_SEARCH_DEFAULT"`
|
||||
|
||||
GitDefaultBranch string `yaml:"git.default-branch" env:"OG_GIT_DEFAULT_BRANCH"`
|
||||
|
||||
@@ -51,13 +53,14 @@ type config struct {
|
||||
HttpPort string `yaml:"http.port" env:"OG_HTTP_PORT"`
|
||||
HttpGit bool `yaml:"http.git-enabled" env:"OG_HTTP_GIT_ENABLED"`
|
||||
|
||||
ApiEnabled bool `yaml:"api.enabled" env:"OG_API_ENABLED"`
|
||||
|
||||
UnixSocketPermissions string `yaml:"unix-socket-permissions" env:"OG_UNIX_SOCKET_PERMISSIONS"`
|
||||
|
||||
SshGit bool `yaml:"ssh.git-enabled" env:"OG_SSH_GIT_ENABLED"`
|
||||
SshHost string `yaml:"ssh.host" env:"OG_SSH_HOST"`
|
||||
SshPort string `yaml:"ssh.port" env:"OG_SSH_PORT"`
|
||||
SshExternalDomain string `yaml:"ssh.external-domain" env:"OG_SSH_EXTERNAL_DOMAIN"`
|
||||
SshKeygen string `yaml:"ssh.keygen-executable" env:"OG_SSH_KEYGEN_EXECUTABLE"`
|
||||
|
||||
GithubClientKey string `yaml:"github.client-key" env:"OG_GITHUB_CLIENT_KEY"`
|
||||
GithubSecret string `yaml:"github.secret" env:"OG_GITHUB_SECRET"`
|
||||
@@ -110,6 +113,7 @@ func configWithDefaults() (*config, error) {
|
||||
c.OpengistHome = ""
|
||||
c.DBUri = "opengist.db"
|
||||
c.Index = "bleve"
|
||||
c.SearchDefault = "content"
|
||||
|
||||
c.SqliteJournalMode = "WAL"
|
||||
|
||||
@@ -117,12 +121,13 @@ func configWithDefaults() (*config, error) {
|
||||
c.HttpPort = "6157"
|
||||
c.HttpGit = true
|
||||
|
||||
c.ApiEnabled = true
|
||||
|
||||
c.UnixSocketPermissions = "0666"
|
||||
|
||||
c.SshGit = true
|
||||
c.SshHost = "0.0.0.0"
|
||||
c.SshPort = "2222"
|
||||
c.SshKeygen = "ssh-keygen"
|
||||
|
||||
c.GitlabName = "GitLab"
|
||||
|
||||
|
||||
@@ -8,9 +8,14 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
NoPermission = 0
|
||||
ReadPermission = 1
|
||||
ReadWritePermission = 2
|
||||
ScopeGist = iota
|
||||
ScopeUser
|
||||
)
|
||||
|
||||
const (
|
||||
NoPermission = iota
|
||||
ReadPermission
|
||||
ReadWritePermission
|
||||
)
|
||||
|
||||
type AccessToken struct {
|
||||
@@ -24,6 +29,7 @@ type AccessToken struct {
|
||||
User User `validate:"-"`
|
||||
|
||||
ScopeGist uint // 0 = none, 1 = read, 2 = read+write
|
||||
ScopeUser uint // 0 = none, 1 = read, 2 = read+write
|
||||
}
|
||||
|
||||
// GenerateToken creates a new random token and returns the plain text token.
|
||||
@@ -100,11 +106,30 @@ func (t *AccessToken) HasGistWritePermission() bool {
|
||||
return t.ScopeGist >= ReadWritePermission
|
||||
}
|
||||
|
||||
func (t *AccessToken) HasUserReadPermission() bool {
|
||||
return t.ScopeUser >= ReadPermission
|
||||
}
|
||||
|
||||
func (t *AccessToken) HasUserWritePermission() bool {
|
||||
return t.ScopeUser >= ReadWritePermission
|
||||
}
|
||||
|
||||
func (t *AccessToken) CheckForPermission(scope, permission uint) bool {
|
||||
if scope == ScopeGist {
|
||||
return t.ScopeGist >= permission
|
||||
}
|
||||
if scope == ScopeUser {
|
||||
return t.ScopeUser >= permission
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// -- DTO -- //
|
||||
|
||||
type AccessTokenDTO struct {
|
||||
Name string `form:"name" validate:"required,max=255"`
|
||||
ScopeGist uint `form:"scope_gist" validate:"min=0,max=2"`
|
||||
ScopeUser uint `form:"scope_user" validate:"min=0,max=2"`
|
||||
ExpiresAt string `form:"expires_at"` // empty means no expiration, otherwise date format (YYYY-MM-DD)
|
||||
}
|
||||
|
||||
@@ -120,6 +145,12 @@ func (dto *AccessTokenDTO) ToAccessToken() *AccessToken {
|
||||
return &AccessToken{
|
||||
Name: dto.Name,
|
||||
ScopeGist: dto.ScopeGist,
|
||||
ScopeUser: dto.ScopeUser,
|
||||
ExpiresAt: expiresAt,
|
||||
}
|
||||
}
|
||||
|
||||
// SaveAccessTokenForTest is exported for tests only; saves the entire AccessToken row.
|
||||
func SaveAccessTokenForTest(t *AccessToken) error {
|
||||
return db.Save(t).Error
|
||||
}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
package db_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/thomiceli/opengist/internal/db"
|
||||
)
|
||||
|
||||
func TestAccessTokenScopeUser(t *testing.T) {
|
||||
tok := &db.AccessToken{ScopeUser: db.ReadPermission}
|
||||
require.True(t, tok.HasUserReadPermission(), "ScopeUser=1 should grant user read")
|
||||
|
||||
tokNone := &db.AccessToken{ScopeUser: 0}
|
||||
require.False(t, tokNone.HasUserReadPermission(), "ScopeUser=0 should not grant user read")
|
||||
}
|
||||
|
||||
func TestAccessTokenDTOScopeUser(t *testing.T) {
|
||||
dto := &db.AccessTokenDTO{Name: "t", ScopeGist: 1, ScopeUser: 1}
|
||||
tok := dto.ToAccessToken()
|
||||
require.Equal(t, uint(1), tok.ScopeUser)
|
||||
require.Equal(t, uint(1), tok.ScopeGist)
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm/clause"
|
||||
)
|
||||
|
||||
// ActionLock is a DB-backed lease used to single-flight an action across
|
||||
// multiple Opengist instances sharing the same database. Each lock is one row
|
||||
// keyed by Action (the action's identifier); LockedUntil holds the Unix
|
||||
// timestamp the current lease expires at (0 = free).
|
||||
type ActionLock struct {
|
||||
Action int `gorm:"primaryKey"`
|
||||
LockedUntil int64
|
||||
}
|
||||
|
||||
func (ActionLock) TableName() string {
|
||||
return "action_lock"
|
||||
}
|
||||
|
||||
// AcquireLock atomically grabs the lock for action when it is free or its lease
|
||||
// has expired, extending the lease by leaseTTL. It returns true only for the
|
||||
// single caller that won the row. The conditional UPDATE is what makes this
|
||||
// safe across SQLite/PostgreSQL/MySQL: concurrent writers serialize on the row
|
||||
// (SQLite serializes all writes), so at most one re-evaluates the
|
||||
// `locked_until < now` predicate to true. leaseTTL only needs to outlast a
|
||||
// normal run; it's a safety net so a crashed holder doesn't block future runs.
|
||||
func AcquireLock(action int, leaseTTL time.Duration) (bool, error) {
|
||||
now := time.Now().Unix()
|
||||
|
||||
if err := db.Clauses(clause.OnConflict{DoNothing: true}).
|
||||
Create(&ActionLock{Action: action, LockedUntil: 0}).Error; err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
res := db.Model(&ActionLock{}).
|
||||
Where("action = ? AND locked_until < ?", action, now).
|
||||
Update("locked_until", time.Now().Add(leaseTTL).Unix())
|
||||
if res.Error != nil {
|
||||
return false, res.Error
|
||||
}
|
||||
return res.RowsAffected == 1, nil
|
||||
}
|
||||
|
||||
// ReleaseLock frees the lock for action so the next run can acquire it
|
||||
// immediately instead of waiting for the lease to expire.
|
||||
func ReleaseLock(action int) error {
|
||||
return db.Model(&ActionLock{}).
|
||||
Where("action = ?", action).
|
||||
Update("locked_until", 0).Error
|
||||
}
|
||||
+1
-1
@@ -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{}, &AccessToken{}, &ActionLock{}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
+309
-51
@@ -12,6 +12,7 @@ import (
|
||||
"github.com/alecthomas/chroma/v2/lexers"
|
||||
"github.com/dustin/go-humanize"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/thomiceli/opengist/internal/config"
|
||||
"github.com/thomiceli/opengist/internal/git"
|
||||
"github.com/thomiceli/opengist/internal/index"
|
||||
"gorm.io/gorm"
|
||||
@@ -71,6 +72,7 @@ type Gist struct {
|
||||
Uuid string
|
||||
Title string
|
||||
URL string
|
||||
URLNormalized string
|
||||
Preview string
|
||||
PreviewFilename string
|
||||
PreviewMimeType string
|
||||
@@ -83,6 +85,7 @@ type Gist struct {
|
||||
NbForks int
|
||||
CreatedAt int64
|
||||
UpdatedAt int64
|
||||
ExpiresAt int64 // 0: never expires
|
||||
|
||||
Likes []User `gorm:"many2many:likes;constraint:OnUpdate:CASCADE,OnDelete:CASCADE"`
|
||||
Forked *Gist `gorm:"foreignKey:ForkedID;constraint:OnUpdate:CASCADE,OnDelete:SET NULL"`
|
||||
@@ -98,7 +101,14 @@ type Like struct {
|
||||
CreatedAt int64
|
||||
}
|
||||
|
||||
func (gist *Gist) BeforeSave(_ *gorm.DB) error {
|
||||
gist.URLNormalized = strings.ToLower(gist.URL)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gist *Gist) BeforeDelete(tx *gorm.DB) error {
|
||||
gist.DeleteRepository()
|
||||
gist.RemoveFromIndex()
|
||||
// Decrement fork counter if the gist was forked
|
||||
err := tx.Model(&Gist{}).
|
||||
Omit("updated_at").
|
||||
@@ -110,13 +120,20 @@ func (gist *Gist) BeforeDelete(tx *gorm.DB) error {
|
||||
func GetGist(user string, gistUuid string) (*Gist, error) {
|
||||
gist := new(Gist)
|
||||
err := db.Preload("User").Preload("Forked.User").Preload("Topics").
|
||||
Where("(gists.uuid like ? OR gists.url = ?) AND users.username like ?", gistUuid+"%", gistUuid, user).
|
||||
Where("(gists.uuid LIKE ? OR gists.url_normalized = ?) AND users.username_normalized = ?",
|
||||
strings.ToLower(gistUuid)+"%", strings.ToLower(gistUuid), strings.ToLower(user)).
|
||||
Joins("join users on gists.user_id = users.id").
|
||||
First(&gist).Error
|
||||
|
||||
return gist, err
|
||||
}
|
||||
|
||||
func GetGistByUUID(uuid string) (*Gist, error) {
|
||||
gist := new(Gist)
|
||||
err := db.Preload("User").Preload("Forked.User").Where("uuid = ?", uuid).First(gist).Error
|
||||
return gist, err
|
||||
}
|
||||
|
||||
func GetGistByID(gistId string) (*Gist, error) {
|
||||
gist := new(Gist)
|
||||
err := db.Preload("User").Preload("Forked.User").Preload("Topics").
|
||||
@@ -126,14 +143,88 @@ func GetGistByID(gistId string) (*Gist, error) {
|
||||
return gist, err
|
||||
}
|
||||
|
||||
func GetAllGistsForCurrentUser(currentUserId uint, offset int, sort string, order string) ([]*Gist, error) {
|
||||
// GetAllGistsForCurrentUser returns gists visible to currentUserId - all public
|
||||
// gists plus the user's own private/unlisted ones - ordered by sort/order and
|
||||
// paginated to one extra row (the 11th is the peek-next sentinel).
|
||||
// `since`, when non-nil, restricts results to gists updated at or after that
|
||||
// instant (used by the API; the web handler passes nil).
|
||||
func GetAllGistsForCurrentUser(currentUserId uint, since *time.Time, offset int, sort string, order string, limit int, perPage int) ([]*Gist, error) {
|
||||
var gists []*Gist
|
||||
err := db.Preload("User").
|
||||
query := db.Preload("User").
|
||||
Preload("Forked.User").
|
||||
Preload("Topics").
|
||||
Where("gists.private = 0 or gists.user_id = ?", currentUserId).
|
||||
Limit(11).
|
||||
Offset(offset * 10).
|
||||
Where("gists.private = 0 or gists.user_id = ?", currentUserId)
|
||||
if since != nil {
|
||||
query = query.Where("gists.updated_at >= ?", since.Unix())
|
||||
}
|
||||
err := query.
|
||||
Limit(limit).
|
||||
Offset(offset * perPage).
|
||||
Order(sort + "_at " + order).
|
||||
Find(&gists).Error
|
||||
|
||||
return gists, err
|
||||
}
|
||||
|
||||
// GetAllGistsFromUserVisibleTo returns gists owned by fromUserId, filtered
|
||||
// to what currentUserId is allowed to see (public always; private/unlisted
|
||||
// only when currentUserId == fromUserId). Same pagination/since shape as
|
||||
// the other API list helpers. Pass currentUserId=0 to force the
|
||||
// public-only subset.
|
||||
func GetAllGistsFromUserVisibleTo(fromUserId uint, currentUserId uint, since *time.Time, offset int, sort string, order string, limit int, perPage int) ([]*Gist, error) {
|
||||
var gists []*Gist
|
||||
query := gistsFromUserStatement(fromUserId, currentUserId)
|
||||
if since != nil {
|
||||
query = query.Where("gists.updated_at >= ?", since.Unix())
|
||||
}
|
||||
err := query.
|
||||
Limit(limit).
|
||||
Offset(offset * perPage).
|
||||
Order("gists." + sort + "_at " + order).
|
||||
Find(&gists).Error
|
||||
|
||||
return gists, err
|
||||
}
|
||||
|
||||
// GetAllGistsOfUser returns every gist owned by userID - public, unlisted,
|
||||
// and private - with the same pagination/since semantics as GetAllGistsForCurrentUser.
|
||||
// Used by the API list endpoint for callers whose
|
||||
// token holds gist:read: they see all of their own content but nothing from
|
||||
// other users (others' public gists live under /gists/public).
|
||||
func GetAllGistsOfUser(userID uint, since *time.Time, offset int, sort string, order string, limit int, perPage int) ([]*Gist, error) {
|
||||
var gists []*Gist
|
||||
query := db.Preload("User").
|
||||
Preload("Forked.User").
|
||||
Preload("Topics").
|
||||
Where("gists.user_id = ?", userID)
|
||||
if since != nil {
|
||||
query = query.Where("gists.updated_at >= ?", since.Unix())
|
||||
}
|
||||
err := query.
|
||||
Limit(limit).
|
||||
Offset(offset * perPage).
|
||||
Order(sort + "_at " + order).
|
||||
Find(&gists).Error
|
||||
|
||||
return gists, err
|
||||
}
|
||||
|
||||
// GetAllPublicGistsOfUser returns only the public gists owned by userID, with
|
||||
// the same pagination/since semantics as GetAllGistsForCurrentUser. Used by
|
||||
// the API list endpoint for callers that authenticate but whose token lacks
|
||||
// gist:read - they get only their own public gists.
|
||||
func GetAllPublicGistsOfUser(userID uint, since *time.Time, offset int, sort string, order string, limit int, perPage int) ([]*Gist, error) {
|
||||
var gists []*Gist
|
||||
query := db.Preload("User").
|
||||
Preload("Forked.User").
|
||||
Preload("Topics").
|
||||
Where("gists.private = 0 AND gists.user_id = ?", userID)
|
||||
if since != nil {
|
||||
query = query.Where("gists.updated_at >= ?", since.Unix())
|
||||
}
|
||||
err := query.
|
||||
Limit(limit).
|
||||
Offset(offset * perPage).
|
||||
Order(sort + "_at " + order).
|
||||
Find(&gists).Error
|
||||
|
||||
@@ -228,10 +319,19 @@ func likedStatement(fromUserId uint, currentUserId uint) *gorm.DB {
|
||||
Joins("join users on likes.user_id = users.id")
|
||||
}
|
||||
|
||||
func GetAllGistsLikedByUser(fromUserId uint, currentUserId uint, offset int, sort string, order string) ([]*Gist, error) {
|
||||
// GetAllGistsLikedByUser returns gists that fromUserId has starred, filtered
|
||||
// to what currentUserId is allowed to see. `since`, when non-nil, restricts
|
||||
// results to gists updated at or after that instant (used by the API; the web
|
||||
// handler passes nil for both since and the explicit pagination args).
|
||||
func GetAllGistsLikedByUser(fromUserId uint, currentUserId uint, since *time.Time, offset int, sort string, order string, limit int, perPage int) ([]*Gist, error) {
|
||||
var gists []*Gist
|
||||
err := likedStatement(fromUserId, currentUserId).Limit(11).
|
||||
Offset(offset * 10).
|
||||
query := likedStatement(fromUserId, currentUserId)
|
||||
if since != nil {
|
||||
query = query.Where("gists.updated_at >= ?", since.Unix())
|
||||
}
|
||||
err := query.
|
||||
Limit(limit).
|
||||
Offset(offset * perPage).
|
||||
Order("gists." + sort + "_at " + order).
|
||||
Find(&gists).Error
|
||||
return gists, err
|
||||
@@ -250,10 +350,19 @@ func forkedStatement(fromUserId uint, currentUserId uint) *gorm.DB {
|
||||
Joins("join users on gists.user_id = users.id")
|
||||
}
|
||||
|
||||
func GetAllGistsForkedByUser(fromUserId uint, currentUserId uint, offset int, sort string, order string) ([]*Gist, error) {
|
||||
// GetAllGistsForkedByUser returns gists forked by fromUserId, filtered to
|
||||
// what currentUserId is allowed to see. `since`, when non-nil, restricts
|
||||
// results to gists updated at or after that instant (used by the API; the
|
||||
// web handler passes nil for both since and the explicit pagination args).
|
||||
func GetAllGistsForkedByUser(fromUserId uint, currentUserId uint, since *time.Time, offset int, sort string, order string, limit int, perPage int) ([]*Gist, error) {
|
||||
var gists []*Gist
|
||||
err := forkedStatement(fromUserId, currentUserId).Limit(11).
|
||||
Offset(offset * 10).
|
||||
query := forkedStatement(fromUserId, currentUserId)
|
||||
if since != nil {
|
||||
query = query.Where("gists.updated_at >= ?", since.Unix())
|
||||
}
|
||||
err := query.
|
||||
Limit(limit).
|
||||
Offset(offset * perPage).
|
||||
Order("gists." + sort + "_at " + order).
|
||||
Find(&gists).Error
|
||||
return gists, err
|
||||
@@ -265,6 +374,59 @@ func CountAllGistsForkedByUser(fromUserId uint, currentUserId uint) (int64, erro
|
||||
return count, err
|
||||
}
|
||||
|
||||
// applySince narrows a gist query to rows updated at or after `since` when it
|
||||
// is non-nil, matching the filter the API list queries apply. Kept separate so
|
||||
// the count helpers stay in sync with their Find counterparts.
|
||||
func applySince(q *gorm.DB, since *time.Time) *gorm.DB {
|
||||
if since != nil {
|
||||
return q.Where("gists.updated_at >= ?", since.Unix())
|
||||
}
|
||||
return q
|
||||
}
|
||||
|
||||
// The Count* helpers below mirror the API list queries (including the optional
|
||||
// `since` filter) so list responses can report a total. They're separate from
|
||||
// the web UI's CountAll* helpers above, which don't take `since`.
|
||||
|
||||
func CountAllGistsForCurrentUser(currentUserId uint, since *time.Time) (int64, error) {
|
||||
var count int64
|
||||
q := applySince(db.Model(&Gist{}).Where("gists.private = 0 or gists.user_id = ?", currentUserId), since)
|
||||
err := q.Count(&count).Error
|
||||
return count, err
|
||||
}
|
||||
|
||||
func CountAllGistsFromUserVisibleTo(fromUserId uint, currentUserId uint, since *time.Time) (int64, error) {
|
||||
var count int64
|
||||
err := applySince(gistsFromUserStatement(fromUserId, currentUserId).Model(&Gist{}), since).Count(&count).Error
|
||||
return count, err
|
||||
}
|
||||
|
||||
func CountAllGistsOfUser(userID uint, since *time.Time) (int64, error) {
|
||||
var count int64
|
||||
q := applySince(db.Model(&Gist{}).Where("gists.user_id = ?", userID), since)
|
||||
err := q.Count(&count).Error
|
||||
return count, err
|
||||
}
|
||||
|
||||
func CountAllPublicGistsOfUser(userID uint, since *time.Time) (int64, error) {
|
||||
var count int64
|
||||
q := applySince(db.Model(&Gist{}).Where("gists.private = 0 AND gists.user_id = ?", userID), since)
|
||||
err := q.Count(&count).Error
|
||||
return count, err
|
||||
}
|
||||
|
||||
func CountAllGistsLikedByUserSince(fromUserId uint, currentUserId uint, since *time.Time) (int64, error) {
|
||||
var count int64
|
||||
err := applySince(likedStatement(fromUserId, currentUserId).Model(&Gist{}), since).Count(&count).Error
|
||||
return count, err
|
||||
}
|
||||
|
||||
func CountAllGistsForkedByUserSince(fromUserId uint, currentUserId uint, since *time.Time) (int64, error) {
|
||||
var count int64
|
||||
err := applySince(forkedStatement(fromUserId, currentUserId).Model(&Gist{}), since).Count(&count).Error
|
||||
return count, err
|
||||
}
|
||||
|
||||
func GetAllGistsRows() ([]*Gist, error) {
|
||||
var gists []*Gist
|
||||
err := db.Table("gists").
|
||||
@@ -328,11 +490,6 @@ func (gist *Gist) UpdateNoTimestamps() error {
|
||||
}
|
||||
|
||||
func (gist *Gist) Delete() error {
|
||||
err := gist.DeleteRepository()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return db.Delete(&gist).Error
|
||||
}
|
||||
|
||||
@@ -382,19 +539,35 @@ func (gist *Gist) GetUsersLikes(offset int) ([]*User, error) {
|
||||
return users, err
|
||||
}
|
||||
|
||||
func (gist *Gist) GetForks(currentUserId uint, offset int) ([]*Gist, error) {
|
||||
// GetForks returns gists that fork this gist, filtered to what
|
||||
// currentUserId is allowed to see. `offset` is the page index (0-based);
|
||||
// `limit` caps the returned slice (pass perPage+1 for the peek-next
|
||||
// sentinel). `perPage` is the slice size used for the offset arithmetic
|
||||
// (offset * perPage rows are skipped).
|
||||
func (gist *Gist) GetForks(currentUserId uint, offset int, limit int, perPage int) ([]*Gist, error) {
|
||||
var gists []*Gist
|
||||
err := db.Model(&gist).Preload("User").
|
||||
Where("forked_id = ?", gist.ID).
|
||||
Where("(gists.private = 0) or (gists.private > 0 and gists.user_id = ?)", currentUserId).
|
||||
Limit(11).
|
||||
Offset(offset * 10).
|
||||
Limit(limit).
|
||||
Offset(offset * perPage).
|
||||
Order("updated_at desc").
|
||||
Find(&gists).Error
|
||||
|
||||
return gists, err
|
||||
}
|
||||
|
||||
// CountForks returns the number of forks of this gist visible to currentUserId,
|
||||
// using the same visibility filter as GetForks (pass 0 for the public subset).
|
||||
func (gist *Gist) CountForks(currentUserId uint) (int64, error) {
|
||||
var count int64
|
||||
err := db.Model(&Gist{}).
|
||||
Where("forked_id = ?", gist.ID).
|
||||
Where("(gists.private = 0) or (gists.private > 0 and gists.user_id = ?)", currentUserId).
|
||||
Count(&count).Error
|
||||
return count, err
|
||||
}
|
||||
|
||||
func (gist *Gist) CanWrite(user *User) bool {
|
||||
return user != nil && gist.UserID == user.ID
|
||||
}
|
||||
@@ -403,18 +576,21 @@ func (gist *Gist) InitRepository() error {
|
||||
return git.InitRepository(gist.User.Username, gist.Uuid)
|
||||
}
|
||||
|
||||
func (gist *Gist) DeleteRepository() error {
|
||||
return git.DeleteRepository(gist.User.Username, gist.Uuid)
|
||||
func (gist *Gist) DeleteRepository() {
|
||||
err := git.DeleteRepository(gist.User.Username, gist.Uuid)
|
||||
if err != nil {
|
||||
log.Warn().Err(err).Msgf("Could not delete repository %s/%s", gist.User.Username, gist.Uuid)
|
||||
}
|
||||
}
|
||||
|
||||
func (gist *Gist) Files(revision string, truncate bool) ([]*git.File, error) {
|
||||
filesCat, err := git.CatFileBatch(gist.User.Username, gist.Uuid, revision, truncate)
|
||||
func (gist *Gist) Files(revision string, truncate bool) ([]*git.File, bool, error) {
|
||||
filesCat, gistTruncated, err := git.CatFileBatch(gist.User.Username, gist.Uuid, revision, truncate)
|
||||
if err != nil {
|
||||
// if the revision or the file do not exist
|
||||
if exiterr, ok := err.(*exec.ExitError); ok && exiterr.ExitCode() == 128 {
|
||||
return nil, &git.RevisionNotFoundError{}
|
||||
return nil, false, &git.RevisionNotFoundError{}
|
||||
}
|
||||
return nil, err
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
var files []*git.File
|
||||
@@ -435,7 +611,7 @@ func (gist *Gist) Files(revision string, truncate bool) ([]*git.File, error) {
|
||||
MimeType: git.DetectMimeType([]byte(shortContent), filepath.Ext(fileCat.Name)),
|
||||
})
|
||||
}
|
||||
return files, err
|
||||
return files, gistTruncated, err
|
||||
}
|
||||
|
||||
func (gist *Gist) File(revision string, filename string, truncate bool) (*git.File, error) {
|
||||
@@ -474,8 +650,55 @@ func (gist *Gist) FileNames(revision string) ([]string, error) {
|
||||
return git.GetFilesOfRepository(gist.User.Username, gist.Uuid, revision)
|
||||
}
|
||||
|
||||
func (gist *Gist) Log(skip int) ([]*git.Commit, error) {
|
||||
return git.GetLog(gist.User.Username, gist.Uuid, skip)
|
||||
// GistCommit pairs a raw git commit with the Opengist account whose email
|
||||
// matches the commit's AuthorEmail (when one exists). The git.Commit pointer
|
||||
// is embedded so callers/templates can read AuthorName, Hash, Timestamp,
|
||||
// Files, etc. directly. User is nil when no account matches - callers can
|
||||
// fall back to the embedded AuthorName/AuthorEmail.
|
||||
type GistCommit struct {
|
||||
*git.Commit
|
||||
User *User
|
||||
}
|
||||
|
||||
// Log returns the gist's commit history starting from `revision` (pass
|
||||
// "HEAD" for the full history or a SHA to walk from a specific commit
|
||||
// downward), with each commit's author resolved to an Opengist user via a
|
||||
// single bulk email lookup. Lookup is case-insensitive on both sides -
|
||||
// matches the historical web behavior even when the DB stores mixed-case
|
||||
// emails. `skip` is the number of commits to skip from the top of the walk
|
||||
// (use offset*per_page for paging); `limit` caps the returned slice (pass
|
||||
// per_page+1 to enable the peek-next sentinel trick).
|
||||
func (gist *Gist) Log(revision string, skip int, limit int) ([]*GistCommit, error) {
|
||||
raw, err := git.GetLog(gist.User.Username, gist.Uuid, revision, skip, limit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Collect distinct lowercased author emails.
|
||||
loweredSet := make(map[string]struct{}, len(raw))
|
||||
for _, c := range raw {
|
||||
if c.AuthorEmail == "" {
|
||||
continue
|
||||
}
|
||||
loweredSet[strings.ToLower(c.AuthorEmail)] = struct{}{}
|
||||
}
|
||||
|
||||
// One IN query, then re-key by lowercased email so we can look up
|
||||
// case-insensitively even if the DB column holds a mixed-case value.
|
||||
byDBEmail, _ := GetUsersFromEmails(loweredSet)
|
||||
byLowered := make(map[string]*User, len(byDBEmail))
|
||||
for e, u := range byDBEmail {
|
||||
byLowered[strings.ToLower(e)] = u
|
||||
}
|
||||
|
||||
out := make([]*GistCommit, len(raw))
|
||||
for i, c := range raw {
|
||||
out[i] = &GistCommit{
|
||||
Commit: c,
|
||||
User: byLowered[strings.ToLower(c.AuthorEmail)],
|
||||
}
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (gist *Gist) NbCommits() (string, error) {
|
||||
@@ -605,8 +828,36 @@ func (gist *Gist) Identifier() string {
|
||||
return gist.Uuid
|
||||
}
|
||||
|
||||
// HTTPCloneURL returns the HTTPS clone URL (`{baseURL}/{user}/{identifier}.git`).
|
||||
// Returns "" when HTTP git access is disabled (config.HttpGit == false).
|
||||
func (gist *Gist) HTTPCloneURL(baseURL string) string {
|
||||
if !config.C.HttpGit {
|
||||
return ""
|
||||
}
|
||||
return baseURL + "/" + gist.User.Username + "/" + gist.Identifier() + ".git"
|
||||
}
|
||||
|
||||
// SSHCloneURL returns the SSH clone URL. `fallbackHost` is the request's Host
|
||||
// header (or any host:port-shaped string) used when SshExternalDomain isn't
|
||||
// configured — only its hostname part is kept. Returns "" when SSH git access
|
||||
// is disabled (config.SshGit == false).
|
||||
func (gist *Gist) SSHCloneURL(fallbackHost string) string {
|
||||
if !config.C.SshGit {
|
||||
return ""
|
||||
}
|
||||
sshDomain := config.C.SshExternalDomain
|
||||
if sshDomain == "" {
|
||||
sshDomain = strings.Split(fallbackHost, ":")[0]
|
||||
}
|
||||
path := gist.User.Username + "/" + gist.Identifier() + ".git"
|
||||
if config.C.SshPort == "22" {
|
||||
return sshDomain + ":" + path
|
||||
}
|
||||
return "ssh://" + sshDomain + ":" + config.C.SshPort + "/" + path
|
||||
}
|
||||
|
||||
func (gist *Gist) GetLanguagesFromFiles() ([]string, error) {
|
||||
files, err := gist.Files("HEAD", true)
|
||||
files, _, err := gist.Files("HEAD", true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -687,7 +938,7 @@ func (gist *Gist) UpdateLanguages() {
|
||||
}
|
||||
|
||||
func (gist *Gist) ToDTO() (*GistDTO, error) {
|
||||
files, err := gist.Files("HEAD", false)
|
||||
files, _, err := gist.Files("HEAD", false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -720,13 +971,19 @@ func (gist *Gist) ToDTO() (*GistDTO, error) {
|
||||
// -- DTO -- //
|
||||
|
||||
type GistDTO struct {
|
||||
Title string `validate:"max=250" form:"title"`
|
||||
Description string `validate:"max=1000" form:"description"`
|
||||
URL string `validate:"max=32,alphanumdashorempty" form:"url"`
|
||||
Files []FileDTO `validate:"min=1,dive"`
|
||||
Name []string `form:"name"`
|
||||
Content []string `form:"content"`
|
||||
Topics string `validate:"gisttopics" form:"topics"`
|
||||
Title string `validate:"max=250" form:"title"`
|
||||
Description string `validate:"max=1000" form:"description"`
|
||||
URL string `validate:"max=32,alphanumdashorempty" form:"url"`
|
||||
Files []FileDTO `validate:"min=1,dive"`
|
||||
Name []string `form:"name"`
|
||||
Content []string `form:"content"`
|
||||
Topics string `validate:"gisttopics" form:"topics"`
|
||||
UploadedFilesUUID []string `validate:"omitempty,dive,required,uuid" form:"uploadedfile_uuid"`
|
||||
UploadedFilesNames []string `validate:"omitempty,dive,required" form:"uploadedfile_filename"`
|
||||
BinaryFileOldName []string `form:"binary_old_name"`
|
||||
BinaryFileNewName []string `form:"binary_new_name"`
|
||||
Expire ExpirationType `validate:"omitempty,oneof=never 1hour 12hours 1day 7days 15days custom" form:"expire"`
|
||||
ExpireAt string `validate:"expirationdate" form:"expire_at"`
|
||||
VisibilityDTO
|
||||
}
|
||||
|
||||
@@ -775,7 +1032,7 @@ func (dto *GistDTO) TopicStrToSlice() []GistTopic {
|
||||
// -- Index -- //
|
||||
|
||||
func (gist *Gist) ToIndexedGist() (*index.Gist, error) {
|
||||
files, err := gist.Files("HEAD", true)
|
||||
files, _, err := gist.Files("HEAD", true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -806,18 +1063,19 @@ func (gist *Gist) ToIndexedGist() (*index.Gist, error) {
|
||||
}
|
||||
|
||||
indexedGist := &index.Gist{
|
||||
GistID: gist.ID,
|
||||
UserID: gist.UserID,
|
||||
Visibility: gist.Private.Uint(),
|
||||
Username: gist.User.Username,
|
||||
Title: gist.Title,
|
||||
Content: wholeContent,
|
||||
Filenames: fileNames,
|
||||
Extensions: exts,
|
||||
Languages: langs,
|
||||
Topics: topics,
|
||||
CreatedAt: gist.CreatedAt,
|
||||
UpdatedAt: gist.UpdatedAt,
|
||||
GistID: gist.ID,
|
||||
UserID: gist.UserID,
|
||||
Visibility: gist.Private.Uint(),
|
||||
Username: gist.User.Username,
|
||||
Description: gist.Description,
|
||||
Title: gist.Title,
|
||||
Content: wholeContent,
|
||||
Filenames: fileNames,
|
||||
Extensions: exts,
|
||||
Languages: langs,
|
||||
Topics: topics,
|
||||
CreatedAt: gist.CreatedAt,
|
||||
UpdatedAt: gist.UpdatedAt,
|
||||
}
|
||||
|
||||
return indexedGist, nil
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/thomiceli/opengist/internal/render/lang"
|
||||
"github.com/thomiceli/opengist/internal/web/handlers/api/v1/types"
|
||||
)
|
||||
|
||||
// ToAPISimple returns the v1 API list-shape representation of the gist. baseURL is
|
||||
// the scheme+host root (no trailing slash) the caller derived from config or
|
||||
// the request.
|
||||
func (gist *Gist) ToAPISimple(baseURL string) types.GistSimple {
|
||||
sshHost := ""
|
||||
if u, err := url.Parse(baseURL); err == nil {
|
||||
sshHost = u.Host
|
||||
}
|
||||
var expiresAt *time.Time
|
||||
if gist.ExpiresAt > 0 {
|
||||
t := time.Unix(gist.ExpiresAt, 0).UTC()
|
||||
expiresAt = &t
|
||||
}
|
||||
return types.GistSimple{
|
||||
ID: gist.Uuid,
|
||||
Owner: gist.User.ToSimpleAPI(),
|
||||
Title: gist.Title,
|
||||
HTMLUrl: baseURL + "/" + gist.User.Username + "/" + gist.Identifier(),
|
||||
SlugUrl: gist.Identifier(),
|
||||
Description: gist.Description,
|
||||
Public: gist.Private == PublicVisibility,
|
||||
Visibility: gist.Private.String(),
|
||||
LikeCount: gist.NbLikes,
|
||||
ForkCount: gist.NbForks,
|
||||
CloneUrl: gist.HTTPCloneURL(baseURL),
|
||||
SSHUrl: gist.SSHCloneURL(sshHost),
|
||||
Topics: gist.TopicsSlice(),
|
||||
CreatedAt: time.Unix(gist.CreatedAt, 0).UTC(),
|
||||
UpdatedAt: time.Unix(gist.UpdatedAt, 0).UTC(),
|
||||
ExpiresAt: expiresAt,
|
||||
}
|
||||
}
|
||||
|
||||
// ToAPI returns the v1 API detail-shape representation, including file
|
||||
// contents at `revision` and the 10 most recent commits up to that
|
||||
// revision. Pass "HEAD" for the current state; pass a SHA to render the
|
||||
// gist (and its history) as it stood at that commit. Returns any error
|
||||
// encountered while listing the gist's files or commit log (an unknown
|
||||
// revision surfaces here as the Files error).
|
||||
func (gist *Gist) ToAPI(baseURL string, revision string) (types.Gist, error) {
|
||||
files, truncated, err := gist.Files(revision, true)
|
||||
if err != nil {
|
||||
return types.Gist{}, err
|
||||
}
|
||||
fm := make(map[string]types.GistFile, len(files))
|
||||
for _, f := range files {
|
||||
ff := types.GistFile{
|
||||
Filename: f.Filename,
|
||||
Type: f.MimeType.ContentType,
|
||||
Language: lang.Parse(f),
|
||||
Size: int(f.Size),
|
||||
Truncated: f.Truncated,
|
||||
}
|
||||
if f.MimeType.CanBeEdited() {
|
||||
ff.Content = f.Content
|
||||
ff.Encoding = f.MimeType.Charset
|
||||
} else {
|
||||
ff.Content = base64.StdEncoding.EncodeToString([]byte(f.Content))
|
||||
ff.Encoding = "base64"
|
||||
}
|
||||
fm[f.Filename] = ff
|
||||
}
|
||||
var forked *types.GistSimple
|
||||
if gist.Forked != nil {
|
||||
ff := gist.Forked.ToAPISimple(baseURL)
|
||||
forked = &ff
|
||||
}
|
||||
forks, err := gist.GetForks(gist.UserID, 0, 11, 10)
|
||||
if err != nil {
|
||||
return types.Gist{}, err
|
||||
}
|
||||
forksMap := make([]types.GistSimple, 0)
|
||||
for _, fork := range forks {
|
||||
forksMap = append(forksMap, fork.ToAPISimple(baseURL))
|
||||
}
|
||||
|
||||
logCommits, err := gist.Log(revision, 0, 10)
|
||||
if err != nil {
|
||||
return types.Gist{}, err
|
||||
}
|
||||
commits := make([]types.GistCommit, 0, len(logCommits))
|
||||
for _, c := range logCommits {
|
||||
commits = append(commits, c.ToAPI())
|
||||
}
|
||||
|
||||
return types.Gist{
|
||||
GistSimple: gist.ToAPISimple(baseURL),
|
||||
ForkOf: forked,
|
||||
Forks: forksMap,
|
||||
Files: fm,
|
||||
Commits: commits,
|
||||
Truncated: truncated,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ToAPI converts a single resolved commit into its API shape. Shared by the
|
||||
// gist-detail endpoint (10-most-recent embed) and the dedicated
|
||||
// /gists/:uuid/commits endpoint so the wire shape is identical.
|
||||
func (c *GistCommit) ToAPI() types.GistCommit {
|
||||
ts, _ := strconv.ParseInt(c.Timestamp, 10, 64)
|
||||
entry := types.GistCommit{
|
||||
Version: c.Hash,
|
||||
Author: types.CommitAuthor{Name: c.AuthorName, Email: c.AuthorEmail},
|
||||
ChangeStatus: types.CommitChangeStatus{
|
||||
Files: c.FilesChanged,
|
||||
Additions: c.Additions,
|
||||
Deletions: c.Deletions,
|
||||
Total: c.Additions + c.Deletions,
|
||||
},
|
||||
CommittedAt: time.Unix(ts, 0).UTC(),
|
||||
}
|
||||
if c.User != nil {
|
||||
s := c.User.ToSimpleAPI()
|
||||
entry.User = &s
|
||||
}
|
||||
return entry
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/thomiceli/opengist/internal/validator"
|
||||
)
|
||||
|
||||
// ExpirationType is the expiration choice a user can pick when creating a
|
||||
// gist: either a fixed-duration preset, "custom" (paired with an explicit
|
||||
// date), or "never". The empty value and "never" both mean "never expires".
|
||||
type ExpirationType string
|
||||
|
||||
const (
|
||||
ExpiryNever ExpirationType = "never"
|
||||
ExpiryOneHour ExpirationType = "1hour"
|
||||
ExpiryTwelveHours ExpirationType = "12hours"
|
||||
ExpiryOneDay ExpirationType = "1day"
|
||||
ExpirySevenDays ExpirationType = "7days"
|
||||
ExpiryFifteenDays ExpirationType = "15days"
|
||||
ExpiryCustom ExpirationType = "custom"
|
||||
)
|
||||
|
||||
// Duration returns the time span of a fixed-duration preset, or 0 for
|
||||
// "never", "custom", and any unknown value (those resolve their timestamp
|
||||
// elsewhere).
|
||||
func (e ExpirationType) Duration() time.Duration {
|
||||
switch e {
|
||||
case ExpiryOneHour:
|
||||
return time.Hour
|
||||
case ExpiryTwelveHours:
|
||||
return 12 * time.Hour
|
||||
case ExpiryOneDay:
|
||||
return 24 * time.Hour
|
||||
case ExpirySevenDays:
|
||||
return 7 * 24 * time.Hour
|
||||
case ExpiryFifteenDays:
|
||||
return 15 * 24 * time.Hour
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
// ExpiresAtTimestamp converts a fixed-duration preset into an absolute Unix
|
||||
// expiration timestamp relative to now, returning 0 for "never"/"custom".
|
||||
// Use GistDTO.ExpiresAtTimestamp to resolve a custom date.
|
||||
func (e ExpirationType) ExpiresAtTimestamp() int64 {
|
||||
d := e.Duration()
|
||||
if d == 0 {
|
||||
return 0
|
||||
}
|
||||
return time.Now().Add(d).Unix()
|
||||
}
|
||||
|
||||
// ExpiresAtTimestamp resolves the gist's absolute expiration time (Unix
|
||||
// seconds) from the chosen preset, or from the custom date when Expire is
|
||||
// "custom". Returns 0 when the gist never expires or the custom date can't be
|
||||
// parsed - callers validate the DTO (the `expirationdate` rule) beforehand, so
|
||||
// an unparseable value here only happens on the non-validated push-option path,
|
||||
// where "never" is the safe fallback.
|
||||
func (dto *GistDTO) ExpiresAtTimestamp() int64 {
|
||||
if dto.Expire != ExpiryCustom {
|
||||
return dto.Expire.ExpiresAtTimestamp()
|
||||
}
|
||||
|
||||
t, err := validator.ParseDateTime(strings.TrimSpace(dto.ExpireAt))
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
return t.Unix()
|
||||
}
|
||||
|
||||
func (gist *Gist) IsExpired() bool {
|
||||
return gist.ExpiresAt > 0 && gist.ExpiresAt <= time.Now().Unix()
|
||||
}
|
||||
|
||||
func DeleteExpiredGists() ([]*Gist, error) {
|
||||
var gists []*Gist
|
||||
err := db.Preload("User").
|
||||
Where("expires_at > 0 AND expires_at <= ?", time.Now().Unix()).
|
||||
Find(&gists).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(gists) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
if err := db.Delete(&gists).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return gists, nil
|
||||
}
|
||||
+53
-28
@@ -2,7 +2,9 @@ package db
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type MigrationVersion struct {
|
||||
@@ -12,60 +14,74 @@ type MigrationVersion struct {
|
||||
|
||||
func applyMigrations(dbInfo *databaseInfo) error {
|
||||
switch dbInfo.Type {
|
||||
case SQLite:
|
||||
return applySqliteMigrations()
|
||||
case PostgreSQL, MySQL:
|
||||
return nil
|
||||
case SQLite, PostgreSQL, MySQL:
|
||||
return applyAllMigrations(dbInfo.Type)
|
||||
default:
|
||||
return fmt.Errorf("unknown database type: %s", dbInfo.Type)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func applySqliteMigrations() error {
|
||||
// Create migration table if it doesn't exist
|
||||
func applyAllMigrations(dbType databaseType) error {
|
||||
if err := db.AutoMigrate(&MigrationVersion{}); err != nil {
|
||||
log.Fatal().Err(err).Msg("Error creating migration version table")
|
||||
return err
|
||||
}
|
||||
|
||||
// Get the current migration version
|
||||
var currentVersion MigrationVersion
|
||||
db.First(¤tVersion)
|
||||
|
||||
// Define migrations
|
||||
migrations := []struct {
|
||||
Version uint
|
||||
DBTypes []databaseType // nil = all types
|
||||
Func func() error
|
||||
}{
|
||||
{1, v1_modifyConstraintToSSHKeys},
|
||||
{2, v2_lowercaseEmails},
|
||||
// Add more migrations here as needed
|
||||
{1, []databaseType{SQLite}, v1_modifyConstraintToSSHKeys},
|
||||
{2, []databaseType{SQLite}, v2_lowercaseEmails},
|
||||
{3, nil, v3_normalizedColumns},
|
||||
}
|
||||
|
||||
// Apply migrations
|
||||
for _, m := range migrations {
|
||||
if m.Version > currentVersion.Version {
|
||||
tx := db.Begin()
|
||||
if err := tx.Error; err != nil {
|
||||
log.Fatal().Err(err).Msg("Error starting transaction")
|
||||
return err
|
||||
}
|
||||
if m.Version <= currentVersion.Version {
|
||||
continue
|
||||
}
|
||||
|
||||
if err := m.Func(); err != nil {
|
||||
log.Fatal().Err(err).Msg(fmt.Sprintf("Error applying migration %d:", m.Version))
|
||||
tx.Rollback()
|
||||
return err
|
||||
} else {
|
||||
if err = tx.Commit().Error; err != nil {
|
||||
log.Fatal().Err(err).Msg(fmt.Sprintf("Error committing migration %d:", m.Version))
|
||||
return err
|
||||
// Skip migrations not intended for this DB type
|
||||
if len(m.DBTypes) > 0 {
|
||||
applicable := false
|
||||
for _, t := range m.DBTypes {
|
||||
if t == dbType {
|
||||
applicable = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !applicable {
|
||||
// Advance version so we don't retry on next startup
|
||||
currentVersion.Version = m.Version
|
||||
db.Save(¤tVersion)
|
||||
log.Info().Msg(fmt.Sprintf("Migration %d applied successfully", m.Version))
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
tx := db.Begin()
|
||||
if err := tx.Error; err != nil {
|
||||
log.Fatal().Err(err).Msg("Error starting transaction")
|
||||
return err
|
||||
}
|
||||
|
||||
if err := m.Func(); err != nil {
|
||||
tx.Rollback()
|
||||
log.Fatal().Err(err).Msg(fmt.Sprintf("Error applying migration %d:", m.Version))
|
||||
return err
|
||||
}
|
||||
|
||||
if err := tx.Commit().Error; err != nil {
|
||||
log.Fatal().Err(err).Msg(fmt.Sprintf("Error committing migration %d:", m.Version))
|
||||
return err
|
||||
}
|
||||
|
||||
currentVersion.Version = m.Version
|
||||
db.Save(¤tVersion)
|
||||
log.Info().Msg(fmt.Sprintf("Migration %d applied successfully", m.Version))
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -112,3 +128,12 @@ func v2_lowercaseEmails() error {
|
||||
copySQL := `UPDATE users SET email = lower(email);`
|
||||
return db.Exec(copySQL).Error
|
||||
}
|
||||
|
||||
func v3_normalizedColumns() error {
|
||||
if err := db.Model(&User{}).Where("username_normalized = '' OR username_normalized IS NULL").
|
||||
Updates(map[string]interface{}{"username_normalized": gorm.Expr("LOWER(username)")}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
return db.Model(&Gist{}).Where("url_normalized = '' OR url_normalized IS NULL").
|
||||
Updates(map[string]interface{}{"url_normalized": gorm.Expr("LOWER(url)")}).Error
|
||||
}
|
||||
|
||||
+28
-15
@@ -2,24 +2,27 @@ package db
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
|
||||
"github.com/thomiceli/opengist/internal/git"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type User struct {
|
||||
ID uint `gorm:"primaryKey"`
|
||||
Username string `gorm:"uniqueIndex,size:191"`
|
||||
Password string
|
||||
IsAdmin bool
|
||||
CreatedAt int64
|
||||
Email string
|
||||
MD5Hash string // for gravatar, if no Email is specified, the value is random
|
||||
AvatarURL string
|
||||
GithubID string
|
||||
GitlabID string
|
||||
GiteaID string
|
||||
OIDCID string `gorm:"column:oidc_id"`
|
||||
StylePreferences string
|
||||
ID uint `gorm:"primaryKey"`
|
||||
Username string `gorm:"uniqueIndex,size:191"`
|
||||
UsernameNormalized string `gorm:"index"`
|
||||
Password string
|
||||
IsAdmin bool
|
||||
CreatedAt int64
|
||||
Email string
|
||||
MD5Hash string // for gravatar, if no Email is specified, the value is random
|
||||
AvatarURL string
|
||||
GithubID string
|
||||
GitlabID string
|
||||
GiteaID string
|
||||
OIDCID string `gorm:"column:oidc_id"`
|
||||
StylePreferences string
|
||||
|
||||
Gists []Gist `gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE;foreignKey:UserID"`
|
||||
SSHKeys []SSHKey `gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE;foreignKey:UserID"`
|
||||
@@ -28,6 +31,11 @@ type User struct {
|
||||
AccessTokens []AccessToken `gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE;foreignKey:UserID"`
|
||||
}
|
||||
|
||||
func (user *User) BeforeSave(_ *gorm.DB) error {
|
||||
user.UsernameNormalized = strings.ToLower(user.Username)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (user *User) BeforeDelete(tx *gorm.DB) error {
|
||||
// Decrement likes counter using derived table
|
||||
err := tx.Exec(`
|
||||
@@ -93,7 +101,7 @@ func (user *User) BeforeDelete(tx *gorm.DB) error {
|
||||
|
||||
func UserExists(username string) (bool, error) {
|
||||
var count int64
|
||||
err := db.Model(&User{}).Where("username like ?", username).Count(&count).Error
|
||||
err := db.Model(&User{}).Where("username_normalized = ?", strings.ToLower(username)).Count(&count).Error
|
||||
return count > 0, err
|
||||
}
|
||||
|
||||
@@ -111,7 +119,7 @@ func GetAllUsers(offset int) ([]*User, error) {
|
||||
func GetUserByUsername(username string) (*User, error) {
|
||||
user := new(User)
|
||||
err := db.
|
||||
Where("username like ?", username).
|
||||
Where("username_normalized = ?", strings.ToLower(username)).
|
||||
First(&user).Error
|
||||
return user, err
|
||||
}
|
||||
@@ -258,6 +266,11 @@ type UserDTO struct {
|
||||
Password string `form:"password" validate:"required"`
|
||||
}
|
||||
|
||||
type OAuthRegisterDTO struct {
|
||||
Username string `form:"username" validate:"required,max=24,alphanumdash,notreserved"`
|
||||
Email string `form:"email" validate:"omitempty,email"`
|
||||
}
|
||||
|
||||
func (dto *UserDTO) ToUser() *User {
|
||||
return &User{
|
||||
Username: dto.Username,
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/thomiceli/opengist/internal/web/handlers/api/v1/types"
|
||||
)
|
||||
|
||||
// ToSimpleAPI returns the public simple-user shape used inside gist responses
|
||||
// and by the public user-lookup endpoints. It carries no private fields (no
|
||||
// email). Fields whose underlying feature doesn't exist in Opengist
|
||||
// (followers, repos, ...) are still populated with the spec-shaped URLs so
|
||||
// clients can parse cleanly.
|
||||
func (u *User) ToSimpleAPI() types.SimpleUser {
|
||||
return types.SimpleUser{
|
||||
ID: u.ID,
|
||||
Login: u.Username,
|
||||
Username: u.Username,
|
||||
AvatarURL: u.AvatarURL,
|
||||
Type: "User",
|
||||
CreatedAt: time.Unix(u.CreatedAt, 0).UTC(),
|
||||
}
|
||||
}
|
||||
|
||||
// ToPrivateAPI returns the self shape for the authenticated-user endpoints
|
||||
// (GET/PATCH /user): the public fields plus the caller's own email.
|
||||
func (u *User) ToPrivateAPI() types.PrivateUser {
|
||||
return types.PrivateUser{
|
||||
SimpleUser: u.ToSimpleAPI(),
|
||||
Email: u.Email,
|
||||
}
|
||||
}
|
||||
+54
-21
@@ -132,21 +132,36 @@ type catFileBatch struct {
|
||||
Truncated bool
|
||||
}
|
||||
|
||||
func CatFileBatch(user string, gist string, revision string, truncate bool) ([]*catFileBatch, error) {
|
||||
func CatFileBatch(user string, gist string, revision string, truncate bool) ([]*catFileBatch, bool, error) {
|
||||
repositoryPath := RepositoryPath(user, gist)
|
||||
maxFiles := 50
|
||||
|
||||
lsTreeCmd := exec.Command("git", "ls-tree", "-l", revision)
|
||||
lsTreeCmd.Dir = repositoryPath
|
||||
lsTreeOutput, err := lsTreeCmd.Output()
|
||||
|
||||
var lsTreeStderr bytes.Buffer
|
||||
lsTreeCmd.Stderr = &lsTreeStderr
|
||||
|
||||
lsTreeStdout, err := lsTreeCmd.StdoutPipe()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, false, err
|
||||
}
|
||||
if err = lsTreeCmd.Start(); err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
fileMap := make([]*catFileBatch, 0)
|
||||
gistTruncated := false
|
||||
|
||||
lines := strings.Split(string(lsTreeOutput), "\n")
|
||||
for _, line := range lines {
|
||||
fields := strings.Fields(line)
|
||||
scanner := bufio.NewScanner(lsTreeStdout)
|
||||
scanner.Buffer(make([]byte, 64*1024), 1024*1024)
|
||||
for scanner.Scan() {
|
||||
if truncate && len(fileMap) >= maxFiles {
|
||||
gistTruncated = true
|
||||
break
|
||||
}
|
||||
|
||||
fields := strings.Fields(scanner.Text())
|
||||
if len(fields) < 4 {
|
||||
continue // Skip lines that don't have enough fields
|
||||
}
|
||||
@@ -164,19 +179,33 @@ func CatFileBatch(user string, gist string, revision string, truncate bool) ([]*
|
||||
Name: convertOctalToUTF8(name),
|
||||
})
|
||||
}
|
||||
scanErr := scanner.Err()
|
||||
|
||||
// Closing the read end before git is done writing causes git's next write
|
||||
// to fail (SIGPIPE on Unix, broken-pipe error on Windows). That shows up as
|
||||
// a non-zero exit from Wait, but it's expected when we stop early — so we
|
||||
// only treat the Wait error as real if git actually printed something to stderr.
|
||||
_ = lsTreeStdout.Close()
|
||||
waitErr := lsTreeCmd.Wait()
|
||||
if scanErr != nil {
|
||||
return nil, false, scanErr
|
||||
}
|
||||
if waitErr != nil && lsTreeStderr.Len() > 0 {
|
||||
return nil, false, waitErr
|
||||
}
|
||||
|
||||
catFileCmd := exec.Command("git", "cat-file", "--batch")
|
||||
catFileCmd.Dir = repositoryPath
|
||||
stdin, err := catFileCmd.StdinPipe()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, false, err
|
||||
}
|
||||
stdout, err := catFileCmd.StdoutPipe()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, false, err
|
||||
}
|
||||
if err = catFileCmd.Start(); err != nil {
|
||||
return nil, err
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
reader := bufio.NewReader(stdout)
|
||||
@@ -184,12 +213,12 @@ func CatFileBatch(user string, gist string, revision string, truncate bool) ([]*
|
||||
for _, file := range fileMap {
|
||||
_, err = stdin.Write([]byte(file.Hash + "\n"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
header, err := reader.ReadString('\n')
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
parts := strings.Fields(header)
|
||||
@@ -199,7 +228,7 @@ func CatFileBatch(user string, gist string, revision string, truncate bool) ([]*
|
||||
|
||||
size, err := strconv.ParseUint(parts[2], 10, 64)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
// Don't truncate Jupyter notebooks
|
||||
@@ -215,7 +244,7 @@ func CatFileBatch(user string, gist string, revision string, truncate bool) ([]*
|
||||
// Read exactly size bytes from header, or the max allowed if truncated
|
||||
content := make([]byte, sizeToRead)
|
||||
if _, err = io.ReadFull(reader, content); err != nil {
|
||||
return nil, err
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
file.Content = string(content)
|
||||
@@ -223,26 +252,26 @@ func CatFileBatch(user string, gist string, revision string, truncate bool) ([]*
|
||||
if truncate && size > truncateLimit {
|
||||
// skip other bytes if truncated
|
||||
if _, err = reader.Discard(int(size - truncateLimit)); err != nil {
|
||||
return nil, err
|
||||
return nil, false, err
|
||||
}
|
||||
file.Truncated = true
|
||||
}
|
||||
|
||||
// Read the blank line following the content
|
||||
if _, err := reader.ReadByte(); err != nil {
|
||||
return nil, err
|
||||
return nil, false, err
|
||||
}
|
||||
}
|
||||
|
||||
if err = stdin.Close(); err != nil {
|
||||
return nil, err
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
if err = catFileCmd.Wait(); err != nil {
|
||||
return nil, err
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
return fileMap, nil
|
||||
return fileMap, gistTruncated, nil
|
||||
}
|
||||
|
||||
func GetFileContent(user string, gist string, revision string, filename string, truncate bool) (string, bool, error) {
|
||||
@@ -298,7 +327,11 @@ func GetFileSize(user string, gist string, revision string, filename string) (ui
|
||||
return strconv.ParseUint(strings.TrimSuffix(string(stdout), "\n"), 10, 64)
|
||||
}
|
||||
|
||||
func GetLog(user string, gist string, skip int) ([]*Commit, error) {
|
||||
// GetLog returns commits walked from `revision` (typically "HEAD" or a
|
||||
// specific SHA), skipping `skip` rows from the top of the walk and limited
|
||||
// to `limit` rows. Pass "HEAD" for the gist's full history; pass a SHA to
|
||||
// see the history ending at (and including) that commit.
|
||||
func GetLog(user string, gist string, revision string, skip int, limit int) ([]*Commit, error) {
|
||||
repositoryPath := RepositoryPath(user, gist)
|
||||
|
||||
cmd := exec.Command(
|
||||
@@ -306,14 +339,14 @@ func GetLog(user string, gist string, skip int) ([]*Commit, error) {
|
||||
"--no-pager",
|
||||
"log",
|
||||
"-n",
|
||||
"11",
|
||||
strconv.Itoa(limit),
|
||||
"--no-color",
|
||||
"-p",
|
||||
"--skip",
|
||||
strconv.Itoa(skip),
|
||||
"--format=format:c %H%na %aN%nm %ae%nt %at",
|
||||
"--shortstat",
|
||||
"HEAD",
|
||||
revision,
|
||||
)
|
||||
cmd.Dir = repositoryPath
|
||||
stdout, _ := cmd.StdoutPipe()
|
||||
|
||||
@@ -100,14 +100,16 @@ like Opengist actually`,
|
||||
require.False(t, truncated, "Content should not be truncated")
|
||||
require.Equal(t, "I really\nlike Opengist actually", content, "Content is not correct")
|
||||
|
||||
commits, err := GetLog("thomas", "gist1", 0)
|
||||
commits, err := GetLog("thomas", "gist1", "HEAD", 0, 11)
|
||||
require.NoError(t, err, "Could not get log")
|
||||
require.Equal(t, 2, len(commits), "Commits count are not correct")
|
||||
require.Regexp(t, "[a-f0-9]{40}", commits[0].Hash, "Commit ID is not correct")
|
||||
require.Regexp(t, "[0-9]{10}", commits[0].Timestamp, "Commit timestamp is not correct")
|
||||
require.Equal(t, "thomas", commits[0].AuthorName, "Commit author name is not correct")
|
||||
require.Equal(t, "thomas@mail.com", commits[0].AuthorEmail, "Commit author email is not correct")
|
||||
require.Equal(t, "4 files changed, 2 insertions, 2 deletions", commits[0].Changed, "Commit author name is not correct")
|
||||
require.Equal(t, 4, commits[0].FilesChanged, "FilesChanged is not correct")
|
||||
require.Equal(t, 2, commits[0].Additions, "Additions is not correct")
|
||||
require.Equal(t, 2, commits[0].Deletions, "Deletions is not correct")
|
||||
|
||||
require.Contains(t, commits[0].Files, File{
|
||||
Filename: "my_renamed_file.txt",
|
||||
@@ -157,7 +159,7 @@ like Opengist actually`,
|
||||
IsDeleted: false,
|
||||
}, "File new_file.txt is not correct")
|
||||
|
||||
commitsSkip1, err := GetLog("thomas", "gist1", 1)
|
||||
commitsSkip1, err := GetLog("thomas", "gist1", "HEAD", 1, 11)
|
||||
require.NoError(t, err, "Could not get log")
|
||||
require.Equal(t, commitsSkip1[0], commits[1], "Commits skips are not correct")
|
||||
}
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
package git
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func CleanTreePathName(s string) string {
|
||||
name := filepath.Base(s)
|
||||
|
||||
if name == "." || name == ".." {
|
||||
return ""
|
||||
}
|
||||
|
||||
name = strings.ReplaceAll(name, "/", "")
|
||||
name = strings.ReplaceAll(name, "\\", "")
|
||||
|
||||
return name
|
||||
}
|
||||
+14
-2
@@ -2,6 +2,7 @@ package git
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"mime"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
@@ -10,6 +11,7 @@ import (
|
||||
|
||||
type MimeType struct {
|
||||
ContentType string
|
||||
Charset string
|
||||
extension string
|
||||
golangContentType string // json, m3u, etc. still renderable as text
|
||||
}
|
||||
@@ -88,6 +90,16 @@ func (mt MimeType) RenderType() string {
|
||||
return "Binary"
|
||||
}
|
||||
|
||||
func DetectMimeType(data []byte, extension string) MimeType {
|
||||
return MimeType{mimetype.Detect(data).String(), extension, http.DetectContentType(data)}
|
||||
// Header returns the value for a Content-Type HTTP header, re-attaching the
|
||||
// charset parameter that DetectMimeType split out of ContentType.
|
||||
func (mt MimeType) Header() string {
|
||||
if mt.Charset == "" {
|
||||
return mt.ContentType
|
||||
}
|
||||
return mime.FormatMediaType(mt.ContentType, map[string]string{"charset": mt.Charset})
|
||||
}
|
||||
|
||||
func DetectMimeType(data []byte, extension string) MimeType {
|
||||
mediaType, params, _ := mime.ParseMediaType(mimetype.Detect(data).String())
|
||||
return MimeType{mediaType, params["charset"], extension, http.DetectContentType(data)}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
@@ -23,12 +24,14 @@ type File struct {
|
||||
}
|
||||
|
||||
type Commit struct {
|
||||
Hash string
|
||||
AuthorName string
|
||||
AuthorEmail string
|
||||
Timestamp string
|
||||
Changed string
|
||||
Files []File
|
||||
Hash string
|
||||
AuthorName string
|
||||
AuthorEmail string
|
||||
Timestamp string
|
||||
FilesChanged int
|
||||
Additions int
|
||||
Deletions int
|
||||
Files []File
|
||||
}
|
||||
|
||||
func truncateCommandOutput(out io.Reader, maxBytes int64) (string, bool, error) {
|
||||
@@ -60,6 +63,18 @@ func truncateCommandOutput(out io.Reader, maxBytes int64) (string, bool, error)
|
||||
|
||||
var reLogBinaryNames = regexp.MustCompile(`Binary files (.+) and (.+) differ`)
|
||||
|
||||
// shortstat patterns. Git emits a line like:
|
||||
//
|
||||
// " 4 files changed, 2 insertions(+), 2 deletions(-)"
|
||||
//
|
||||
// with insertions or deletions optionally absent when zero. The capture
|
||||
// group in each regex is the count.
|
||||
var (
|
||||
reShortstatFiles = regexp.MustCompile(`(\d+) files? changed`)
|
||||
reShortstatInsertions = regexp.MustCompile(`(\d+) insertions?`)
|
||||
reShortstatDeletions = regexp.MustCompile(`(\d+) deletions?`)
|
||||
)
|
||||
|
||||
// inspired from https://github.com/go-gitea/gitea/blob/main/services/gitdiff/gitdiff.go
|
||||
func parseLog(out io.Reader, maxFiles int, maxBytes int) ([]*Commit, error) {
|
||||
var commits []*Commit
|
||||
@@ -120,10 +135,16 @@ loopLog:
|
||||
|
||||
// Commit shortstat
|
||||
case ' ':
|
||||
changed := []byte(line)[1:]
|
||||
changed = bytes.ReplaceAll(changed, []byte("(+)"), []byte(""))
|
||||
changed = bytes.ReplaceAll(changed, []byte("(-)"), []byte(""))
|
||||
currentCommit.Changed = string(changed)
|
||||
shortstat := line[1:]
|
||||
if m := reShortstatFiles.FindStringSubmatch(shortstat); len(m) == 2 {
|
||||
currentCommit.FilesChanged, _ = strconv.Atoi(m[1])
|
||||
}
|
||||
if m := reShortstatInsertions.FindStringSubmatch(shortstat); len(m) == 2 {
|
||||
currentCommit.Additions, _ = strconv.Atoi(m[1])
|
||||
}
|
||||
if m := reShortstatDeletions.FindStringSubmatch(shortstat); len(m) == 2 {
|
||||
currentCommit.Deletions, _ = strconv.Atoi(m[1])
|
||||
}
|
||||
|
||||
// shortstat is followed by an empty line
|
||||
line, err = input.ReadString('\n')
|
||||
|
||||
@@ -3,14 +3,16 @@ package hooks
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"github.com/thomiceli/opengist/internal/db"
|
||||
"github.com/thomiceli/opengist/internal/git"
|
||||
validatorpkg "github.com/thomiceli/opengist/internal/validator"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/thomiceli/opengist/internal/db"
|
||||
"github.com/thomiceli/opengist/internal/git"
|
||||
validatorpkg "github.com/thomiceli/opengist/internal/validator"
|
||||
)
|
||||
|
||||
func PostReceive(in io.Reader, out, er io.Writer) error {
|
||||
@@ -47,7 +49,7 @@ func PostReceive(in io.Reader, out, er io.Writer) error {
|
||||
|
||||
if slices.Contains([]string{"public", "unlisted", "private"}, opts["visibility"]) {
|
||||
gist.Private = db.ParseVisibility(opts["visibility"])
|
||||
outputSb.WriteString(fmt.Sprintf("Gist visibility set to %s\n\n", opts["visibility"]))
|
||||
fmt.Fprintf(&outputSb, "Gist visibility set to %s\n\n", opts["visibility"])
|
||||
}
|
||||
|
||||
if opts["url"] != "" && validator.Var(opts["url"], "max=32,alphanumdashorempty") == nil {
|
||||
@@ -55,19 +57,49 @@ func PostReceive(in io.Reader, out, er io.Writer) error {
|
||||
lastIndex := strings.LastIndex(gistUrl, "/")
|
||||
gistUrl = gistUrl[:lastIndex+1] + gist.URL
|
||||
if !newGist {
|
||||
outputSb.WriteString(fmt.Sprintf("Gist URL set to %s. Set the Git remote URL via:\n", gistUrl))
|
||||
outputSb.WriteString(fmt.Sprintf("git remote set-url origin %s\n\n", gistUrl))
|
||||
fmt.Fprintf(&outputSb, "Gist URL set to %s. Set the Git remote URL via:\n", gistUrl)
|
||||
fmt.Fprintf(&outputSb, "git remote set-url origin %s\n\n", gistUrl)
|
||||
}
|
||||
}
|
||||
|
||||
if opts["title"] != "" && validator.Var(opts["title"], "max=250") == nil {
|
||||
gist.Title = opts["title"]
|
||||
outputSb.WriteString(fmt.Sprintf("Gist title set to \"%s\"\n\n", opts["title"]))
|
||||
fmt.Fprintf(&outputSb, "Gist title set to \"%s\"\n\n", opts["title"])
|
||||
}
|
||||
|
||||
if opts["description"] != "" && validator.Var(opts["description"], "max=1000") == nil {
|
||||
gist.Description = opts["description"]
|
||||
outputSb.WriteString(fmt.Sprintf("Gist description set to \"%s\"\n\n", opts["description"]))
|
||||
fmt.Fprintf(&outputSb, "Gist description set to \"%s\"\n\n", opts["description"])
|
||||
}
|
||||
|
||||
if opts["topics"] != "" && validator.Var(opts["topics"], "gisttopics") == nil {
|
||||
topicNames := strings.Fields(opts["topics"])
|
||||
if len(topicNames) > 0 {
|
||||
gist.Topics = make([]db.GistTopic, 0, len(topicNames))
|
||||
for _, name := range topicNames {
|
||||
gist.Topics = append(gist.Topics, db.GistTopic{Topic: name})
|
||||
}
|
||||
fmt.Fprintf(&outputSb, "Gist topics set to \"%s\"\n\n", opts["topics"])
|
||||
}
|
||||
}
|
||||
|
||||
if newGist && opts["expire"] != "" {
|
||||
value := opts["expire"]
|
||||
expire := db.ExpirationType(value)
|
||||
switch {
|
||||
case expire == db.ExpiryNever:
|
||||
// no expiration
|
||||
case expire.Duration() > 0:
|
||||
gist.ExpiresAt = expire.ExpiresAtTimestamp()
|
||||
fmt.Fprintf(&outputSb, "Gist expiration set to \"%s\"\n\n", value)
|
||||
default:
|
||||
if t, err := validatorpkg.ParseDateTime(value); err == nil && t.After(time.Now()) {
|
||||
gist.ExpiresAt = t.Unix()
|
||||
fmt.Fprintf(&outputSb, "Gist expiration set to \"%s\"\n\n", value)
|
||||
} else {
|
||||
fmt.Fprintf(&outputSb, "Invalid gist expiration \"%s\", ignored\n\n", value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if hasNoCommits, err := git.HasNoCommits(gist.User.Username, gist.Uuid); err != nil {
|
||||
@@ -90,9 +122,9 @@ func PostReceive(in io.Reader, out, er io.Writer) error {
|
||||
gist.AddInIndex()
|
||||
|
||||
if newGist {
|
||||
outputSb.WriteString(fmt.Sprintf("Your new gist has been created here: %s\n", gistUrl))
|
||||
fmt.Fprintf(&outputSb, "Your new gist has been created here: %s\n", gistUrl)
|
||||
outputSb.WriteString("If you want to keep working with your gist, you could set the Git remote URL via:\n")
|
||||
outputSb.WriteString(fmt.Sprintf("git remote set-url origin %s\n\n", gistUrl))
|
||||
fmt.Fprintf(&outputSb, "git remote set-url origin %s\n\n", gistUrl)
|
||||
}
|
||||
|
||||
outputStr := outputSb.String()
|
||||
|
||||
@@ -56,6 +56,12 @@ func (store *LocaleStore) loadLocaleFromYAML(localeCode, path string) error {
|
||||
case language.EuropeanSpanish:
|
||||
name = "Español"
|
||||
}
|
||||
switch localeCode {
|
||||
case "zh-CN":
|
||||
name = "简体中文"
|
||||
case "zh-TW":
|
||||
name = "繁體中文"
|
||||
}
|
||||
|
||||
locale := &Locale{
|
||||
Code: localeCode,
|
||||
|
||||
@@ -0,0 +1,379 @@
|
||||
gist.public: عام
|
||||
gist.unlisted: غير مدرج
|
||||
gist.private: خاص
|
||||
|
||||
gist.header.like: اعجبني
|
||||
gist.header.unlike: لم يعجبني
|
||||
gist.header.fork: اشتقاق
|
||||
gist.header.edit: تعديل
|
||||
gist.header.delete: حذف
|
||||
gist.header.forked-from: مشتق من
|
||||
gist.header.last-active: آخر نشاط
|
||||
gist.header.select-tab: اختر علامة التبويب
|
||||
gist.header.code: كود
|
||||
gist.header.revisions: التعديلات
|
||||
gist.header.revision: تعديل
|
||||
gist.header.clone-http: نسخ عن طريق %s
|
||||
gist.header.clone-http-help: نسخ باستخدام المصادقة HTTP الأساسية.
|
||||
gist.header.clone-ssh: نسخ باستخدام مفتاح SSH
|
||||
gist.header.clone-ssh-help: نسخ باستخدام مفتاح SSH.
|
||||
gist.header.embed: تضمين
|
||||
gist.header.embed-help: تضمين هذا المقطع في موقعك على الويب.
|
||||
gist.header.download-zip: تنزيل ZIP
|
||||
|
||||
gist.raw: خام
|
||||
gist.file-truncated: تم تقطيع هذا الملف.
|
||||
gist.file-raw: لا يمكن عرض هذا الملف.
|
||||
gist.file-binary-edit: هذا الملف ثنائي.
|
||||
gist.watch-full-file: عرض الملف الكامل.
|
||||
gist.file-not-valid: هذا الملف ليس ملف CSV صالح.
|
||||
gist.no-content: لا توجد ملفات.
|
||||
gist.preview-non-available: المعاينة غير متاحة
|
||||
|
||||
gist.new.new_gist: مقطع جديد
|
||||
gist.new.title: العنوان
|
||||
gist.new.description: الوصف
|
||||
gist.new.url: الرابط
|
||||
gist.new.filename-with-extension: اسم الملف مع الامتداد
|
||||
gist.new.indent-mode: نمط الإزاحة
|
||||
gist.new.indent-mode-space: مسافات
|
||||
gist.new.indent-mode-tab: علامة تبويب
|
||||
gist.new.indent-size: حجم الإزاحة
|
||||
gist.new.wrap-mode: نمط التفاف السطر
|
||||
gist.new.wrap-mode-no: بدون التفاف
|
||||
gist.new.wrap-mode-soft: التفاف ناعم
|
||||
gist.new.add-file: إضافة ملف
|
||||
gist.new.create-public-button: إنشاء مقطع عام
|
||||
gist.new.create-unlisted-button: إنشاء مقطع غير مدرج
|
||||
gist.new.create-private-button: إنشاء مقطع خاص
|
||||
gist.new.preview: معاينة
|
||||
gist.new.create-a-new-gist: إنشاء مقطع جديد
|
||||
gist.new.topics: المواضيع (افصل بينها بمسافات)
|
||||
gist.new.drop-files: أفلت الملفات هنا أو انقر للتحميل
|
||||
gist.new.any-file-type: ارفع أي نوع ملف
|
||||
|
||||
gist.edit.editing: تحرير
|
||||
gist.edit.edit-gist: تعديل %s
|
||||
gist.edit.change-visibility: جعل
|
||||
gist.edit.delete: حذف
|
||||
gist.edit.cancel: الغاء
|
||||
gist.edit.save: حفظ
|
||||
gist.delete.confirm: متأكد أنك تريد حذف هذا المقطع ؟
|
||||
|
||||
gist.list.joined: تم الانضمام
|
||||
gist.list.all: كل المقاطع
|
||||
gist.list.search-results: نتائج البحث
|
||||
gist.list.sort: ترتيب
|
||||
gist.list.sort-by-created: تم الانشاء
|
||||
gist.list.sort-by-updated: تم التحديث
|
||||
gist.list.order-by-asc: الأقدم أولاً
|
||||
gist.list.order-by-desc: الأحدث أولاً
|
||||
gist.list.select-tab: اختر علامة التبويب
|
||||
gist.list.liked: معجب بها
|
||||
gist.list.likes: إعجابات
|
||||
gist.list.forked: مشتقة
|
||||
gist.list.forked-from: مشتقة من
|
||||
gist.list.forks: اشتقاقات
|
||||
gist.list.files: ملفات
|
||||
gist.list.last-active: آخر نشاط
|
||||
gist.list.no-gists: لا مقاطع
|
||||
gist.list.all-liked-by: كل المقاطع التي أعجب بها %s
|
||||
gist.list.all-forked-by: كل المقاطع المشتقة بواسطة %s
|
||||
gist.list.all-from: كل المقاطع من %s
|
||||
gist.list.topic-results-topic: كل المقاطع المطابقة للموضوع %s
|
||||
gist.list.topic-results: كل المقاطع المطابقة للموضوع
|
||||
|
||||
|
||||
gist.search.found: تم العثور على مقاطع
|
||||
gist.search.no-results: لم يتم العثور على مقاطع
|
||||
gist.search.help.user: المقاطع التي أنشأها المستخدم
|
||||
gist.search.help.title: المقاطع ذات العنوان المحدد
|
||||
gist.search.help.description: المقاطع ذات الوصف المحدد
|
||||
gist.search.help.filename: المقاطع التي تحتوي ملفات بالاسم المحدد
|
||||
gist.search.help.extension: المقاطع التي تحتوي ملفات بالامتداد المحدد
|
||||
gist.search.help.language: المقاطع التي تحتوي ملفات باللغة المحددة
|
||||
gist.search.help.topic: المقاطع ذات الموضوع المحدد
|
||||
gist.search.help.all: البحث في جميع الحقول
|
||||
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.forks: اشتقاقات
|
||||
gist.forks.view: عرض الاشتقاق
|
||||
gist.forks.no: لا توجد اشتقاقات عامة
|
||||
gist.forks.for: اشتقاقات %s
|
||||
|
||||
gist.likes: الإعجابات
|
||||
gist.likes.no: لا توجد إعجابات بعد
|
||||
gist.likes.for: إعجابات %s
|
||||
|
||||
gist.revisions: التعديلات
|
||||
gist.revision.revised: عدّل هذا المقطع
|
||||
gist.revision.go-to-revision: الانتقال إلى التعديل
|
||||
gist.revision.file-created: تم إنشاء الملف
|
||||
gist.revision.file-deleted: تم حذف الملف
|
||||
gist.revision.file-renamed: تم تغيير الاسم إلى
|
||||
gist.revision.diff-truncated: الفرق كبير جدًا ولا يمكن عرضه
|
||||
gist.revision.file-renamed-no-changes: تم تغيير اسم الملف بدون تغييرات
|
||||
gist.revision.empty-file: ملف فارغ
|
||||
gist.revision.binary-file-changes: لا يتم عرض تغييرات الملفات الثنائية
|
||||
gist.revision.no-changes: لا توجد تغييرات
|
||||
gist.revision.no-revisions: لا توجد تعديلات للعرض
|
||||
gist.revision-of: تعديل %s
|
||||
|
||||
settings: الإعدادات
|
||||
settings.email: البريد الإلكتروني
|
||||
settings.email-help: يُستخدم في عمليات commit وGravatar
|
||||
settings.email-set: تعيين البريد الإلكتروني
|
||||
settings.link-accounts: ربط الحسابات
|
||||
settings.link-github-account: ربط حساب GitHub
|
||||
settings.link-gitlab-account: ربط حساب GitLab
|
||||
settings.link-gitea-account: ربط حساب Gitea
|
||||
settings.unlink-github-account: إلغاء ربط حساب GitHub
|
||||
settings.unlink-gitlab-account: إلغاء ربط حساب GitLab
|
||||
settings.unlink-gitea-account: إلغاء ربط حساب Gitea
|
||||
settings.delete-account: حذف الحساب
|
||||
settings.delete-account-confirm: هل أنت متأكد أنك تريد حذف حسابك؟
|
||||
settings.add-ssh-key: إضافة مفتاح SSH
|
||||
settings.add-ssh-key-help: يُستخدم فقط لسحب/دفع المقاطع عبر Git باستخدام SSH
|
||||
settings.add-ssh-key-title: العنوان
|
||||
settings.add-ssh-key-content: المفتاح
|
||||
settings.delete-ssh-key: حذف
|
||||
settings.delete-ssh-key-confirm: تأكيد حذف مفتاح SSH
|
||||
settings.ssh-key-added-at: أُضيف
|
||||
settings.ssh-key-never-used: لم يُستخدم أبدًا
|
||||
settings.ssh-key-last-used: آخر استخدام
|
||||
settings.ssh-key-exists: مفتاح SSH موجود بالفعل
|
||||
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: كلمة المرور
|
||||
settings.header.account: الحساب
|
||||
settings.header.mfa: المصادقة متعددة العوامل
|
||||
settings.header.ssh: SSH
|
||||
settings.header.tokens: رموز الوصول
|
||||
settings.header.style: المظهر
|
||||
settings.style.gist-code: كود المقطع
|
||||
settings.style.no-soft-wrap: بدون التفاف ناعم
|
||||
settings.style.soft-wrap: التفاف ناعم
|
||||
settings.style.removed-lines-color: لون الأسطر المحذوفة
|
||||
settings.style.added-lines-color: لون الأسطر المضافة
|
||||
settings.style.git-lines-color: لون أسطر Git
|
||||
settings.style.save-style: حفظ المظهر
|
||||
settings.style.theme: السمة
|
||||
settings.style.theme-light: فاتح
|
||||
settings.style.theme-dark: داكن
|
||||
settings.style.theme-auto: تلقائي
|
||||
settings.create-token: إنشاء رمز وصول
|
||||
settings.create-token-help: يمكن استخدام رموز الوصول للوصول إلى API
|
||||
settings.token-name: الاسم
|
||||
settings.token-permissions: الأذونات
|
||||
settings.token-gist-permission: المقاطع
|
||||
settings.token-permission-none: بدون وصول
|
||||
settings.token-permission-read: قراءة
|
||||
settings.token-permission-read-write: قراءة وكتابة
|
||||
settings.delete-token: حذف
|
||||
settings.delete-token-confirm: تأكيد حذف رمز الوصول
|
||||
settings.token-created-at: أُنشئ
|
||||
settings.token-never-used: لم يُستخدم أبدًا
|
||||
settings.token-last-used: آخر استخدام
|
||||
settings.token-expiration: انتهاء الصلاحية
|
||||
settings.token-expiration-help: اتركه فارغًا لعدم تحديد انتهاء صلاحية
|
||||
settings.token-expires-at: ينتهي في
|
||||
settings.token-no-expiration: بدون انتهاء صلاحية
|
||||
settings.token-expired: منتهي الصلاحية
|
||||
settings.token-created: تم إنشاء الرمز، تأكد من نسخه الآن، لن تتمكن من رؤيته مرة أخرى!
|
||||
settings.token-deleted: تم حذف رمز الوصول
|
||||
|
||||
auth.signup-disabled: قام المسؤول بتعطيل إنشاء الحسابات
|
||||
auth.login: تسجيل الدخول
|
||||
auth.signup: انشاء
|
||||
auth.new-account: حساب جديد
|
||||
auth.username: اسم المستخدم
|
||||
auth.password: كلمة المرور
|
||||
auth.register-instead: انشاء حساب
|
||||
auth.login-instead: سجل الدخول
|
||||
auth.oauth: المتابعة باستخدام حساب %s
|
||||
auth.oauth.no-provider: لم يتم العثور على موفر OAuth
|
||||
auth.oauth.complete-registration: أكمل تسجيلك
|
||||
auth.oauth.complete-registration-button: إنشاء حساب
|
||||
auth.oauth.signing-in-with: تسجيل الدخول باستخدام %s
|
||||
auth.oauth.cancel: الغاء
|
||||
auth.oauth.existing-account: لديك حساب موجود؟
|
||||
auth.oauth.already-have-account: إذا كان لديك حساب Opengist بالفعل، فسجّل الدخول أولًا ثم اربط حساب %s من الإعدادات.
|
||||
auth.mfa: المصادقة متعددة العوامل
|
||||
auth.mfa.passkey: مفتاح مرور
|
||||
auth.mfa.passkeys: مفاتيح مرور
|
||||
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: أضف مفتاح مرور لتسجيل الدخول إلى حسابك واستخدامه كطريقة مصادقة متعددة العوامل.
|
||||
auth.mfa.passkey-name: الاسم
|
||||
auth.mfa.delete-passkey: حذف
|
||||
auth.mfa.passkey-added-at: أُضيف
|
||||
auth.mfa.passkey-never-used: لم يُستخدم أبدًا
|
||||
auth.mfa.passkey-last-used: آخر استخدام
|
||||
auth.mfa.delete-passkey-confirm: تأكيد حذف مفتاح المرور
|
||||
auth.totp: كلمة مرور لمرة واحدة قائمة على الوقت (TOTP)
|
||||
auth.totp.help: TOTP هي طريقة مصادقة ثنائية تستخدم سرًا مشتركًا لإنشاء كلمة مرور لمرة واحدة.
|
||||
auth.totp.use: استخدام TOTP
|
||||
auth.totp.regenerate-recovery-codes: إعادة إنشاء رموز الاسترداد
|
||||
auth.totp.already-enabled: TOTP مفعّل بالفعل
|
||||
auth.totp.invalid-secret: سر TOTP غير صالح
|
||||
auth.totp.invalid-code: رمز TOTP غير صالح
|
||||
auth.totp.code-used: تم استخدام رمز الاسترداد %s وهو الآن غير صالح. قد ترغب في تعطيل المصادقة متعددة العوامل حاليًا أو إعادة إنشاء رموزك.
|
||||
auth.totp.disabled: تم تعطيل TOTP بنجاح
|
||||
auth.totp.disable: تعطيل TOTP
|
||||
auth.totp.enter-code: أدخل الرمز من تطبيق المصادقة
|
||||
auth.totp.enter-recovery-key: أو مفتاح استرداد إذا فقدت جهازك
|
||||
auth.totp.code: كود
|
||||
auth.totp.submit: إرسال
|
||||
auth.totp.proceed: متابعة
|
||||
auth.totp.save-recovery-codes: احفظ رموز الاسترداد في مكان آمن. يمكنك استخدام هذه الرموز لاستعادة الوصول إلى حسابك إذا فقدت الوصول إلى تطبيق المصادقة.
|
||||
auth.totp.scan-qr-code: امسح رمز QR أدناه باستخدام تطبيق المصادقة لتفعيل المصادقة الثنائية، أو أدخل السلسلة التالية ثم أكّد بالرمز الذي تم إنشاؤه.
|
||||
|
||||
|
||||
error: خطأ
|
||||
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: موفر OAuth2 غير مدعوم
|
||||
error.cannot-bind-data: تعذر ربط البيانات
|
||||
error.invalid-number: رقم غير صالح
|
||||
error.invalid-character-unescaped: محرف غير صالح غير مهلّب
|
||||
error.not-in-mfa-session: المستخدم ليس ضمن جلسة مصادقة متعددة العوامل
|
||||
error.no-file-uploaded: لم يتم رفع أي ملف
|
||||
error.cannot-open-file: تعذر فتح الملف المرفوع
|
||||
|
||||
header.menu.all: الكل
|
||||
header.menu.new: جديد
|
||||
header.menu.search: البحث
|
||||
header.menu.my-gists: مقاطعي
|
||||
header.menu.liked: المفضلة
|
||||
header.menu.admin: الإدارة
|
||||
header.menu.settings: الإعدادات
|
||||
header.menu.logout: تسجيل الخروج
|
||||
header.menu.register: إنشاء حساب
|
||||
header.menu.login: تسجيل الدخول
|
||||
header.menu.light: فاتح
|
||||
header.menu.dark: داكن
|
||||
header.menu.system: النظام
|
||||
footer.powered-by: مدعوم بواسطة %s
|
||||
|
||||
pagination.older: أقدم
|
||||
pagination.newer: أحدث
|
||||
pagination.previous: سابق
|
||||
pagination.next: لاحق
|
||||
|
||||
admin.admin_panel: لوحة الإدارة
|
||||
admin.general: عام
|
||||
admin.users: المستخدمون
|
||||
admin.gists: مقاطع
|
||||
admin.configuration: الإعدادات
|
||||
admin.invitations: الدعوات
|
||||
admin.invitations.create: إنشاء دعوة
|
||||
admin.versions: الإصدارات
|
||||
admin.ssh_keys: مفاتيح SSH
|
||||
admin.stats: الإحصائيات
|
||||
admin.actions: الإجراءات
|
||||
admin.actions.sync-fs: مزامنة المقاطع من نظام الملفات
|
||||
admin.actions.sync-db: مزامنة المقاطع من قاعدة البيانات
|
||||
admin.actions.git-gc: تنفيذ جمع القمامة لجميع مستودعات Git
|
||||
admin.actions.sync-previews: مزامنة معاينات جميع المقاطع
|
||||
admin.actions.reset-hooks: إعادة تعيين خطافات خادم Git لجميع المستودعات
|
||||
admin.actions.index-gists: إعادة بناء فهرس البحث
|
||||
admin.actions.sync-gist-languages: مزامنة لغات جميع المقاطع
|
||||
admin.id: ID
|
||||
admin.user: المستخدم
|
||||
admin.delete: حذف
|
||||
admin.created_at: تاريخ الإنشاء
|
||||
|
||||
admin.config-link: يمكن %s هذا الإعداد عبر ملف إعدادات YAML و/أو متغيرات البيئة.
|
||||
admin.config-link-overriden: تجاوزه
|
||||
admin.disable-signup: تعطيل إنشاء الحساب
|
||||
admin.disable-signup_help: منع إنشاء حسابات جديدة.
|
||||
admin.require-login: طلب تسجيل الدخول
|
||||
admin.require-login_help: فرض تسجيل الدخول على المستخدمين لعرض المقاطع.
|
||||
admin.allow-gists-without-login: السماح بالمقاطع الفردية دون تسجيل دخول
|
||||
admin.allow-gists-without-login_help: السماح بعرض وتنزيل المقاطع الفردية دون تسجيل دخول، مع طلب تسجيل الدخول لاستكشاف المقاطع.
|
||||
admin.disable-login: تعطيل نموذج تسجيل الدخول
|
||||
admin.disable-login_help: منع تسجيل الدخول عبر النموذج لإجبار استخدام مزوّدي OAuth بدلًا من ذلك.
|
||||
admin.disable-gravatar: تعطيل Gravatar
|
||||
admin.disable-gravatar_help: تعطيل استخدام Gravatar كمزوّد للصور الرمزية.
|
||||
|
||||
admin.users.delete_confirm: هل تريد حذف هذا المستخدم؟
|
||||
|
||||
admin.gists.title: العنوان
|
||||
admin.gists.private: خاص؟
|
||||
admin.gists.nb-files: عدد الملفات
|
||||
admin.gists.nb-likes: عدد الإعجابات
|
||||
admin.gists.delete_confirm: هل تريد حذف هذا المقطع؟
|
||||
|
||||
admin.invitations.help: يمكن استخدام الدعوات لإنشاء حساب حتى لو كان إنشاء الحسابات معطّلًا.
|
||||
admin.invitations.max_uses: أقصى عدد استخدامات
|
||||
admin.invitations.expires_at: تنتهي في
|
||||
admin.invitations.code: كود
|
||||
admin.invitations.copy_link: نسخ الرابط
|
||||
admin.invitations.uses: الاستخدامات
|
||||
admin.invitations.expired: منتهية
|
||||
admin.invitations.delete_confirm: هل تريد حذف هذه الدعوة؟
|
||||
|
||||
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.admin.sync-gist-languages: تتم مزامنة لغات المقاطع...
|
||||
|
||||
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: تعذر جلب مفاتيح المستخدم
|
||||
flash.auth.user-sshkeys-not-created: تعذر إنشاء مفتاح SSH
|
||||
flash.auth.must-be-logged-in: يجب تسجيل الدخول للوصول إلى المقاطع
|
||||
flash.auth.passkey-registred: تم تسجيل مفتاح المرور %s
|
||||
flash.auth.passkey-deleted: تم حذف مفتاح المرور
|
||||
flash.auth.oauth-session-expired: انتهت جلسة OAuth2، يرجى المحاولة مرة أخرى
|
||||
flash.auth.oauth-already-linked: حساب %s هذا مرتبط بالفعل بمستخدم آخر
|
||||
|
||||
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-be-empty: يجب ألا يكون الحقل %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 غير صالح'
|
||||
validation.invalid-gist-topics: مواضيع المقطع غير صالحة، يجب أن تبدأ بحرف أو رقم، وألا تتجاوز 50 حرفًا، ويمكن أن تتضمن شرطات
|
||||
|
||||
html.title.admin-panel: لوحة الإدارة
|
||||
@@ -192,7 +192,7 @@ admin.actions.sync-db: 'Gists von der Datenbank synchronisieren'
|
||||
admin.actions.git-gc: '„garbage collection“ bei allen git Repositories ausführen'
|
||||
admin.actions.sync-previews: 'Alle Gist Vorschauen synchronisieren'
|
||||
admin.actions.reset-hooks: 'Alle Git server Hooks für alle Repositories synchronisieren'
|
||||
admin.actions.index-gists: 'Alle Gists Indexieren'
|
||||
admin.actions.index-gists: 'Suchindex neu aufbauen'
|
||||
admin.id: 'ID'
|
||||
admin.user: 'Benutzer'
|
||||
admin.delete: 'Löschen'
|
||||
@@ -236,7 +236,7 @@ flash.admin.sync-db: 'Synchronisiere Repositories aus der Datenbank...'
|
||||
flash.admin.git-gc: 'Sammle Repositories...'
|
||||
flash.admin.sync-previews: 'Synchronisiere Gist-Vorschauen...'
|
||||
flash.admin.reset-hooks: 'Setze Git-Server-Hooks für alle Repositories zurück...'
|
||||
flash.admin.index-gists: 'Indiziere alle Gists...'
|
||||
flash.admin.index-gists: 'Suchindex wird neu aufgebaut...'
|
||||
|
||||
flash.auth.username-exists: 'Benutzername existiert bereits'
|
||||
flash.auth.invalid-credentials: 'Ungültige Anmeldeinformationen'
|
||||
|
||||
@@ -9,6 +9,7 @@ gist.header.edit: Edit
|
||||
gist.header.delete: Delete
|
||||
gist.header.forked-from: Forked from
|
||||
gist.header.last-active: Last active
|
||||
gist.header.expires: Expires
|
||||
gist.header.select-tab: Select a tab
|
||||
gist.header.code: Code
|
||||
gist.header.revisions: Revisions
|
||||
@@ -23,6 +24,7 @@ gist.header.download-zip: Download ZIP
|
||||
|
||||
gist.raw: Raw
|
||||
gist.file-truncated: This file has been truncated.
|
||||
gist.files-truncated: Not all files in this gist are not displayed. Clone or download the gist to see them all.
|
||||
gist.file-raw: This file can't be rendered.
|
||||
gist.file-binary-edit: This file is binary.
|
||||
gist.watch-full-file: View the full file.
|
||||
@@ -51,6 +53,14 @@ gist.new.create-a-new-gist: Create a new gist
|
||||
gist.new.topics: Topics (separate with spaces)
|
||||
gist.new.drop-files: Drop files here or click to upload
|
||||
gist.new.any-file-type: Upload any file type
|
||||
gist.new.expire: Expires
|
||||
gist.new.expire-never: Never
|
||||
gist.new.expire-1hour: After 1 hour
|
||||
gist.new.expire-12hours: After 12 hours
|
||||
gist.new.expire-1day: After 1 day
|
||||
gist.new.expire-7days: After 7 days
|
||||
gist.new.expire-15days: After 15 days
|
||||
gist.new.expire-custom: Custom date
|
||||
|
||||
gist.edit.editing: Editing
|
||||
gist.edit.edit-gist: Edit %s
|
||||
@@ -88,10 +98,12 @@ gist.search.found: gists found
|
||||
gist.search.no-results: No gists found
|
||||
gist.search.help.user: gists created by user
|
||||
gist.search.help.title: gists with given title
|
||||
gist.search.help.description: gists with given description
|
||||
gist.search.help.filename: gists having files with given name
|
||||
gist.search.help.extension: gists having files with given extension
|
||||
gist.search.help.language: gists having files with given language
|
||||
gist.search.help.topic: gists with given topic
|
||||
gist.search.help.all: search all fields
|
||||
gist.search.placeholder.title: Title
|
||||
gist.search.placeholder.visibility: Visibility
|
||||
gist.search.placeholder.public: Public
|
||||
@@ -175,6 +187,7 @@ 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-user-permission: User
|
||||
settings.token-permission-none: No access
|
||||
settings.token-permission-read: Read
|
||||
settings.token-permission-read-write: Read & Write
|
||||
@@ -190,6 +203,8 @@ 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
|
||||
settings.api-disabled-warning: The REST API is currently disabled. Tokens you create here cannot be used until an administrator enables it.
|
||||
settings.api-disabled-go-admin: Open admin configuration
|
||||
|
||||
auth.signup-disabled: Administrator has disabled signing up
|
||||
auth.login: Login
|
||||
@@ -200,6 +215,13 @@ auth.password: Password
|
||||
auth.register-instead: Register instead
|
||||
auth.login-instead: Login instead
|
||||
auth.oauth: Continue with %s account
|
||||
auth.oauth.no-provider: OAuth provider not found
|
||||
auth.oauth.complete-registration: Complete your registration
|
||||
auth.oauth.complete-registration-button: Create account
|
||||
auth.oauth.signing-in-with: Signing in with %s
|
||||
auth.oauth.cancel: Cancel
|
||||
auth.oauth.existing-account: Existing account?
|
||||
auth.oauth.already-have-account: If you already have an Opengist account, login first and link your %s account from your settings.
|
||||
auth.mfa: Multi-factor authentication
|
||||
auth.mfa.passkey: Passkey
|
||||
auth.mfa.passkeys: Passkeys
|
||||
@@ -241,7 +263,7 @@ error.signup-disabled: Signing up is disabled
|
||||
error.signup-disabled-form: Signing up via registration form is disabled
|
||||
error.login-disabled-form: Logging in via login form is disabled
|
||||
error.complete-oauth-login: "Cannot complete user auth: %s"
|
||||
error.oauth-unsupported: Unsupported provider
|
||||
error.oauth-unsupported: Unsupported OAuth2 provider
|
||||
error.cannot-bind-data: Cannot bind data
|
||||
error.invalid-number: Invalid number
|
||||
error.invalid-character-unescaped: Invalid character unescaped
|
||||
@@ -285,8 +307,9 @@ admin.actions.sync-db: Synchronize gists from database
|
||||
admin.actions.git-gc: Garbage collect all git repositories
|
||||
admin.actions.sync-previews: Synchronize all gists previews
|
||||
admin.actions.reset-hooks: Reset Git server hooks for all repositories
|
||||
admin.actions.index-gists: Index all gists
|
||||
admin.actions.index-gists: Rebuild search index
|
||||
admin.actions.sync-gist-languages: Synchronize all gists languages
|
||||
admin.actions.delete-expired-gists: Delete expired gists
|
||||
admin.id: ID
|
||||
admin.user: User
|
||||
admin.delete: Delete
|
||||
@@ -304,6 +327,8 @@ admin.disable-login: Disable login form
|
||||
admin.disable-login_help: Forbid logging in via the login form to force using OAuth providers instead.
|
||||
admin.disable-gravatar: Disable Gravatar
|
||||
admin.disable-gravatar_help: Disable the usage of Gravatar as an avatar provider.
|
||||
admin.api-enabled: Enable REST API at /api
|
||||
admin.api-enabled_help: Allow programmatic access to gists and user info via Personal Access Tokens.
|
||||
|
||||
admin.users.delete_confirm: Do you want to delete this user ?
|
||||
|
||||
@@ -331,8 +356,9 @@ flash.admin.sync-db: Syncing repositories from database...
|
||||
flash.admin.git-gc: Garbage collecting repositories...
|
||||
flash.admin.sync-previews: Syncing Gist previews...
|
||||
flash.admin.reset-hooks: Resetting Git server hooks for all repositories...
|
||||
flash.admin.index-gists: Indexing all gists...
|
||||
flash.admin.index-gists: Rebuilding search index...
|
||||
flash.admin.sync-gist-languages: Syncing Gist languages...
|
||||
flash.admin.delete-expired-gists: Deleting expired gists...
|
||||
|
||||
flash.auth.username-exists: Username already exists
|
||||
flash.auth.invalid-credentials: Invalid credentials
|
||||
@@ -343,6 +369,8 @@ flash.auth.user-sshkeys-not-created: Could not create ssh key
|
||||
flash.auth.must-be-logged-in: You must be logged in to access gists
|
||||
flash.auth.passkey-registred: Passkey %s registered
|
||||
flash.auth.passkey-deleted: Passkey deleted
|
||||
flash.auth.oauth-session-expired: OAuth2 session expired, please try again
|
||||
flash.auth.oauth-already-linked: This %s account is already linked to another user
|
||||
|
||||
flash.gist.visibility-changed: Gist visibility has been changed
|
||||
flash.gist.deleted: Gist has been deleted
|
||||
@@ -364,5 +392,6 @@ validation.should-only-contain-alphanumeric-characters-and-dashes: Field %s shou
|
||||
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
|
||||
validation.invalid-expiration-date: Invalid expiration date, it must be a valid date in the future
|
||||
|
||||
html.title.admin-panel: Admin panel
|
||||
|
||||
@@ -213,7 +213,7 @@ admin.invitations: 'Invitaciones'
|
||||
admin.invitations.create: 'Crear invitación'
|
||||
admin.actions.sync-previews: 'Sincronizar todas las vistas previas de gists'
|
||||
admin.actions.reset-hooks: 'Resetear los hooks de Git en todos los repositorios'
|
||||
admin.actions.index-gists: 'Indexar todos los gists'
|
||||
admin.actions.index-gists: 'Reconstruir índice de búsqueda'
|
||||
admin.config-link-overriden: 'sobrescrito'
|
||||
admin.invitations.help: 'Las invitaciones se pueden usar para crear una cuenta aunque el registro esté deshabilitado.'
|
||||
admin.invitations.max_uses: 'Cantidad máxima de usos'
|
||||
@@ -231,7 +231,7 @@ flash.admin.sync-db: 'Sincronizando repositorios desde la base de datos...'
|
||||
flash.admin.git-gc: 'Recolectando basura en los repositorios...'
|
||||
flash.admin.sync-previews: 'Sincronizando vistas previas de gists...'
|
||||
flash.admin.reset-hooks: 'Reseteando hooks del servidor Git en todos los repositorios...'
|
||||
flash.admin.index-gists: 'Indexando todos los gists...'
|
||||
flash.admin.index-gists: 'Reconstruyendo índice de búsqueda...'
|
||||
flash.auth.username-exists: 'El nombre de usuario ya existe'
|
||||
flash.auth.invalid-credentials: 'Credenciales incorrectas'
|
||||
flash.auth.account-linked-oauth: 'Cuenta vinculada a %s'
|
||||
|
||||
@@ -193,7 +193,7 @@ admin.actions.reset-hooks: Réinitialiser les hooks de Git pour tous les dépôt
|
||||
gist.new.url: URL
|
||||
gist.search.no-results: Aucun gist trouvé
|
||||
settings.unlink-gitlab-account: Détacher le compte GitLab
|
||||
admin.actions.index-gists: Indexer tous les gists
|
||||
admin.actions.index-gists: Reconstruire l'index de recherche
|
||||
gist.new.preview: 'Aperçu'
|
||||
gist.new.create-a-new-gist: 'Créer un nouveau gist'
|
||||
gist.edit.edit-gist: 'Modifier %s'
|
||||
@@ -231,7 +231,7 @@ flash.admin.sync-db: 'Synchronisation des dépôts à partir de la base de donn
|
||||
flash.admin.git-gc: 'Nettoyage des dépôts...'
|
||||
flash.admin.sync-previews: 'Synchronisation des aperçus du Gist...'
|
||||
flash.admin.reset-hooks: 'Réinitialisation des hooks du serveur Git pour tous les dépôts...'
|
||||
flash.admin.index-gists: 'Indexation de tous les gists...'
|
||||
flash.admin.index-gists: 'Reconstruction de l''index de recherche...'
|
||||
flash.auth.username-exists: 'Nom d''utilisateur déjà utilisé'
|
||||
flash.auth.invalid-credentials: 'Identifiants non valides'
|
||||
flash.auth.account-linked-oauth: 'Compte lié à %s'
|
||||
|
||||
@@ -170,7 +170,7 @@ admin.actions.sync-db: Gistek szinkronizálása az adatbázissal
|
||||
admin.actions.git-gc: Használatlan git repository-k eltávolítása
|
||||
admin.actions.sync-previews: Gist előnézetek szinkronizálása
|
||||
admin.actions.reset-hooks: Git server hook-ok alaphelyzetbe állítása minden repository-nál
|
||||
admin.actions.index-gists: Gistek indexelése
|
||||
admin.actions.index-gists: Keresési index újraépítése
|
||||
admin.id: Azonosító
|
||||
admin.user: Felhasználó
|
||||
admin.delete: Törlés
|
||||
|
||||
@@ -191,7 +191,7 @@ admin.actions.sync-db: 'Sincronizza gists dal database'
|
||||
admin.actions.git-gc: 'Esegui la garbage collection da tutti i repositories'
|
||||
admin.actions.sync-previews: 'Sincronizza tutte le anteprime dei gists'
|
||||
admin.actions.reset-hooks: 'Resetta tutti gli hook del server Git per tutti i repositories'
|
||||
admin.actions.index-gists: 'Indicizza tutti i gists'
|
||||
admin.actions.index-gists: 'Ricostruisci indice di ricerca'
|
||||
admin.id: 'ID'
|
||||
admin.user: 'Utente'
|
||||
admin.delete: 'Elimina'
|
||||
@@ -235,7 +235,7 @@ flash.admin.sync-db: 'Sincronizzando i repositories dal database...'
|
||||
flash.admin.git-gc: 'Eseguendo il garbage collector dei repositories...'
|
||||
flash.admin.sync-previews: 'Sincronizzando le anteprime dei gists...'
|
||||
flash.admin.reset-hooks: 'Resettando gli hook di Git per tutti i repositories...'
|
||||
flash.admin.index-gists: 'Indicizzando tutti i gists...'
|
||||
flash.admin.index-gists: 'Ricostruzione indice di ricerca...'
|
||||
|
||||
flash.auth.username-exists: 'Il nome utente esiste già'
|
||||
flash.auth.invalid-credentials: 'Credenziali errate'
|
||||
|
||||
@@ -227,7 +227,7 @@ admin.actions.sync-db: 'Synchronizuj Gisty z bazy danych'
|
||||
admin.actions.git-gc: 'Zbierz śmieci we wszystkich repozytoriach Git'
|
||||
admin.actions.sync-previews: 'Synchronizuj podglądy wszystkich Gistów'
|
||||
admin.actions.reset-hooks: 'Zresetuj hooki serwera Git dla wszystkich repozytoriów'
|
||||
admin.actions.index-gists: 'Indeksuj wszystkie Gisty'
|
||||
admin.actions.index-gists: 'Przebuduj indeks wyszukiwania'
|
||||
admin.id: 'ID'
|
||||
admin.user: 'Użytkownik'
|
||||
admin.delete: 'Usuń'
|
||||
@@ -271,7 +271,7 @@ flash.admin.sync-db: 'Synchronizowanie repozytoriów z bazy danych...'
|
||||
flash.admin.git-gc: 'Zbieranie śmieci w repozytoriach...'
|
||||
flash.admin.sync-previews: 'Synchronizowanie podglądów Gistów...'
|
||||
flash.admin.reset-hooks: 'Resetowanie hooków serwera Git dla wszystkich repozytoriów...'
|
||||
flash.admin.index-gists: 'Indeksowanie wszystkich Gistów...'
|
||||
flash.admin.index-gists: 'Przebudowywanie indeksu wyszukiwania...'
|
||||
|
||||
flash.auth.username-exists: 'Nazwa użytkownika już istnieje'
|
||||
flash.auth.invalid-credentials: 'Niepoprawne dane logowania'
|
||||
|
||||
@@ -214,7 +214,7 @@ admin.invitations: 'Инвайты'
|
||||
admin.invitations.create: 'Создать инвайт'
|
||||
admin.actions.sync-previews: 'Обновить предпросмотры всех фрагментов'
|
||||
admin.actions.reset-hooks: 'Сбросить хуки Git-сервера для всех репозиториев'
|
||||
admin.actions.index-gists: 'Проиндексировать все фрагменты'
|
||||
admin.actions.index-gists: 'Перестроить поисковый индекс'
|
||||
validation.should-not-be-empty: 'Поле %s не должно быть пустым'
|
||||
admin.invitations.help: 'Инвайты используются для создания аккаунта, даже когда регистрация запрещена.'
|
||||
admin.invitations.max_uses: 'Максимальное количество использований'
|
||||
@@ -232,7 +232,7 @@ flash.admin.sync-db: 'Выполняется синхронизация репо
|
||||
flash.admin.git-gc: 'Сборка мусора в репозиториях…'
|
||||
flash.admin.sync-previews: 'Обновление предпросмотров фрагментов…'
|
||||
flash.admin.reset-hooks: 'Пересоздание Git-хуков для всех репозиториев…'
|
||||
flash.admin.index-gists: 'Выполняется индексация фрагментов…'
|
||||
flash.admin.index-gists: 'Перестроение поискового индекса…'
|
||||
flash.auth.username-exists: 'Такое имя пользователя уже занято'
|
||||
flash.auth.invalid-credentials: 'Некорректные данные для входа'
|
||||
flash.auth.account-linked-oauth: 'Учётная запись связана с %s'
|
||||
|
||||
@@ -191,7 +191,7 @@ admin.actions.sync-db: Gistleri veri tabanından senkronize et
|
||||
admin.actions.git-gc: Tüm Git depolarındaki gereksiz verileri temizle
|
||||
admin.actions.sync-previews: Tüm gist önizlemelerini senkronize et
|
||||
admin.actions.reset-hooks: Tüm depolar için Git sunucu kancalarını sıfırla
|
||||
admin.actions.index-gists: Tüm gistleri indeksle
|
||||
admin.actions.index-gists: Arama dizinini yeniden oluştur
|
||||
admin.id: ID
|
||||
admin.user: Kullanıcı
|
||||
admin.delete: Sil
|
||||
@@ -234,7 +234,7 @@ flash.admin.sync-db: Depolar veri tabanından senkronize ediliyor...
|
||||
flash.admin.git-gc: Depolardan gereksiz veriler temizleniyor...
|
||||
flash.admin.sync-previews: Gist önizlemeleri senkronize ediliyor...
|
||||
flash.admin.reset-hooks: Tüm depolar için Git sunucusu kancaları sıfırlanıyor...
|
||||
flash.admin.index-gists: Tüm gistler indeksleniyor...
|
||||
flash.admin.index-gists: Arama dizini yeniden oluşturuluyor...
|
||||
|
||||
flash.auth.username-exists: Kullanıcı adı zaten mevcut
|
||||
flash.auth.invalid-credentials: Geçersiz kimlik bilgileri
|
||||
|
||||
@@ -77,7 +77,7 @@ gist.list.all-from: Всі gists від %s
|
||||
gist.search.found: gists знайдено
|
||||
gist.search.no-results: Не знайдено gists
|
||||
gist.search.help.user: gists створені користувачем
|
||||
gist.search.help.title: gists з наданим ім'ям
|
||||
gist.search.help.title: gists з наданим ім'ям
|
||||
gist.search.help.filename: gists мають файли з наданим ім'ям
|
||||
gist.search.help.extension: gists мають файли з наданим розширенням
|
||||
gist.search.help.language: gists мають файли з наданою мовою
|
||||
@@ -192,7 +192,7 @@ admin.actions.sync-db: Синхронізувати gists з базою дани
|
||||
admin.actions.git-gc: Збір сміття з репозиторіїв Git
|
||||
admin.actions.sync-previews: Синхронізувати всі gists перегляди
|
||||
admin.actions.reset-hooks: Скинути серверні Git hooks для всіх репозиторіїв
|
||||
admin.actions.index-gists: Проіндексувати всі gists
|
||||
admin.actions.index-gists: Перебудувати пошуковий індекс
|
||||
admin.id: ID
|
||||
admin.user: Користувач
|
||||
admin.delete: Видалити
|
||||
@@ -236,7 +236,7 @@ flash.admin.sync-db: Синхронізація репозиторіїв за б
|
||||
flash.admin.git-gc: Збір сміття з репозиторіїв...
|
||||
flash.admin.sync-previews: Синхронізація Gist переглядів...
|
||||
flash.admin.reset-hooks: Скидання cерверниз Git hooks для всіх репозиторіїв...
|
||||
flash.admin.index-gists: Індексація всіх gists...
|
||||
flash.admin.index-gists: Перебудова пошукового індексу...
|
||||
|
||||
flash.auth.username-exists: Це ім'я користувача вже існує
|
||||
flash.auth.invalid-credentials: Недійсні облікові дані
|
||||
@@ -266,4 +266,4 @@ validation.should-only-contain-alphanumeric-characters-and-dashes: Поле %s
|
||||
validation.not-enough: Недостатньо %s
|
||||
validation.invalid: Недійсний %s
|
||||
|
||||
html.title.admin-panel: Панель адміністратора
|
||||
html.title.admin-panel: Панель адміністратора
|
||||
|
||||
@@ -87,6 +87,11 @@ gist.revision.no-changes: 没有任何变更
|
||||
gist.revision.no-revisions: 无可供显示的修订
|
||||
|
||||
settings: 设置
|
||||
settings.header.account: 账号
|
||||
settings.header.mfa: 多因素认证
|
||||
settings.header.ssh: SSH
|
||||
settings.header.tokens: 访问令牌
|
||||
settings.header.style: 样式
|
||||
settings.email: 电子邮箱
|
||||
settings.email-help: 用于提交和 Gravatar 头像
|
||||
settings.email-set: 设置邮箱地址
|
||||
@@ -107,6 +112,30 @@ settings.ssh-key-added-at: 添加于
|
||||
settings.ssh-key-never-used: 从未使用过
|
||||
settings.ssh-key-last-used: 最后使用于
|
||||
|
||||
settings.create-token: 创建访问令牌
|
||||
settings.create-token-help: 访问令牌用于程序化调用 API
|
||||
settings.token-name: 名称
|
||||
settings.token-permissions: 权限
|
||||
settings.token-gist-permission: Gists
|
||||
settings.token-user-permission: 用户
|
||||
settings.token-permission-none: 无访问权限
|
||||
settings.token-permission-read: 只读
|
||||
settings.token-permission-read-write: 读写
|
||||
settings.delete-token: 删除
|
||||
settings.delete-token-confirm: 确认删除访问令牌
|
||||
settings.token-created-at: 创建于
|
||||
settings.token-never-used: 从未使用
|
||||
settings.token-last-used: 最后使用
|
||||
settings.token-expiration: 过期时间
|
||||
settings.token-expiration-help: 留空表示永不过期
|
||||
settings.token-expires-at: 过期于
|
||||
settings.token-no-expiration: 永不过期
|
||||
settings.token-expired: 已过期
|
||||
settings.token-created: 令牌已创建,请立即复制保存,离开本页后将无法再次查看!
|
||||
settings.token-deleted: 访问令牌已删除
|
||||
settings.api-disabled-warning: REST API 当前已禁用,在此处创建的令牌需要管理员启用 API 后才能使用。
|
||||
settings.api-disabled-go-admin: 前往管理后台启用
|
||||
|
||||
auth.signup-disabled: 管理员已禁用注册
|
||||
auth.login: 登录
|
||||
auth.signup: 注册
|
||||
@@ -166,6 +195,8 @@ admin.disable-login: 禁用登录表单
|
||||
admin.disable-login_help: 禁止使用登录表单进行登录以强制通过 OAuth 提供方登录。
|
||||
admin.disable-gravatar: 禁用 Gravatar
|
||||
admin.disable-gravatar_help: 停止使用 Gravatar 作为头像提供方。
|
||||
admin.api-enabled: 启用 REST API(/api)
|
||||
admin.api-enabled_help: 允许通过 Personal Access Token 程序化访问 Gist 和用户信息。
|
||||
admin.allow-gists-without-login: 允许未登录状态下访问单个 Gists
|
||||
admin.allow-gists-without-login_help: 允许在不登录的情况下查看和下载 Gist,同时需要登录才能使用 Gists 的发现功能。
|
||||
admin.users.delete_confirm: 您想要删除此用户吗?
|
||||
@@ -214,7 +245,7 @@ admin.invitations: '邀请'
|
||||
admin.invitations.create: '创建邀请'
|
||||
admin.actions.sync-previews: '同步所有 Gists 预览'
|
||||
admin.actions.reset-hooks: '重置所有存储库的 Git 服务 hooks'
|
||||
admin.actions.index-gists: '索引所有 Gists'
|
||||
admin.actions.index-gists: '重建搜索索引'
|
||||
admin.invitations.help: '即使在禁用注册功能的情况下,邀请功能也可用于创建帐户。'
|
||||
admin.invitations.max_uses: '最多使用次数'
|
||||
admin.invitations.expires_at: '过期时间'
|
||||
@@ -231,7 +262,7 @@ flash.admin.sync-db: '正在从数据库同步存储库...'
|
||||
flash.admin.git-gc: '正在进行存储库垃圾回收...'
|
||||
flash.admin.sync-previews: '正在同步 Gist 预览...'
|
||||
flash.admin.reset-hooks: '正在重置所有存储库的 Git 服务挂钩...'
|
||||
flash.admin.index-gists: '正在索引所有 Gists...'
|
||||
flash.admin.index-gists: '正在重建搜索索引...'
|
||||
flash.auth.username-exists: '用户名已存在'
|
||||
flash.auth.invalid-credentials: '无效的凭证'
|
||||
flash.auth.account-linked-oauth: '帐户已关联到 %s'
|
||||
|
||||
@@ -190,7 +190,7 @@ gist.search.no-results: 沒有找到任何 Gists
|
||||
gist.search.help.title: Gists 的標題
|
||||
gist.search.help.filename: Gists 的檔案名稱
|
||||
gist.search.help.language: Gists 的程式語言
|
||||
admin.actions.index-gists: 索引所有的 Gists
|
||||
admin.actions.index-gists: 重建搜尋索引
|
||||
gist.search.help.user: 由使用者建立的 Gists
|
||||
gist.search.found: 已找到 Gists
|
||||
gist.search.help.extension: Gists 的副檔名
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user