Compare commits
1 Commits
meili
...
fix/gitea-
| Author | SHA1 | Date | |
|---|---|---|---|
|
a22475d692
|
16
.github/workflows/go.yml
vendored
16
.github/workflows/go.yml
vendored
@@ -83,18 +83,6 @@ 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
|
||||
@@ -106,15 +94,13 @@ 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"]
|
||||
os: ["ubuntu-latest", "macOS-latest", "windows-latest"]
|
||||
go: ["1.25"]
|
||||
database: ["sqlite"]
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
8
.github/workflows/helm.yml
vendored
8
.github/workflows/helm.yml
vendored
@@ -36,14 +36,14 @@ jobs:
|
||||
|
||||
- name: Push to docs repository
|
||||
run: |
|
||||
git clone https://${{ secrets.STATIC_REPO_TOKEN }}@github.com/${{ secrets.STATIC_REPO }}.git target-repo
|
||||
git clone https://${{ secrets.DOCS_REPO_TOKEN }}@github.com/${{ secrets.DOCS_REPO }}.git target-repo
|
||||
mkdir -p target-repo/helm
|
||||
cp helm/*.tgz target-repo/srv/helm/
|
||||
cp helm/index.yaml target-repo/srv/helm/
|
||||
cp helm/*.tgz target-repo/helm/
|
||||
cp helm/index.yaml target-repo/helm/
|
||||
cd target-repo
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||
git add .
|
||||
git commit -m "Deploy helm chart from ${{ github.repository }}@${{ github.sha }}" || echo "No changes to commit"
|
||||
git pull --rebase
|
||||
git push
|
||||
git push
|
||||
12
.github/workflows/release.yml
vendored
12
.github/workflows/release.yml
vendored
@@ -42,7 +42,7 @@ jobs:
|
||||
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v6
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: |
|
||||
ghcr.io/thomiceli/opengist
|
||||
@@ -54,26 +54,26 @@ jobs:
|
||||
type=semver,pattern={{version}}
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v4
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v4
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v4
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v4
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v7
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
platforms: linux/amd64,linux/arm64
|
||||
|
||||
@@ -32,10 +32,6 @@ 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:
|
||||
|
||||
@@ -15,7 +15,6 @@ 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) |
|
||||
@@ -44,8 +43,6 @@ 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. |
|
||||
|
||||
22
go.mod
22
go.mod
@@ -7,19 +7,19 @@ require (
|
||||
github.com/alecthomas/chroma/v2 v2.23.1
|
||||
github.com/blevesearch/bleve/v2 v2.5.7
|
||||
github.com/dustin/go-humanize v1.0.1
|
||||
github.com/gabriel-vasile/mimetype v1.4.13
|
||||
github.com/gabriel-vasile/mimetype v1.4.12
|
||||
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.16.0
|
||||
github.com/go-webauthn/webauthn v0.15.0
|
||||
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.1
|
||||
github.com/labstack/echo/v4 v4.15.0
|
||||
github.com/markbates/goth v1.82.0
|
||||
github.com/meilisearch/meilisearch-go v0.36.1
|
||||
github.com/meilisearch/meilisearch-go v0.36.0
|
||||
github.com/pquerna/otp v1.5.0
|
||||
github.com/prometheus/client_golang v1.23.2
|
||||
github.com/rs/zerolog v1.34.0
|
||||
@@ -29,8 +29,8 @@ require (
|
||||
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.48.0
|
||||
golang.org/x/text v0.34.0
|
||||
golang.org/x/crypto v0.47.0
|
||||
golang.org/x/text v0.33.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
gorm.io/driver/mysql v1.6.0
|
||||
gorm.io/driver/postgres v1.6.0
|
||||
@@ -75,11 +75,11 @@ require (
|
||||
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.5.0 // indirect
|
||||
github.com/go-webauthn/x v0.2.1 // indirect
|
||||
github.com/golang-jwt/jwt/v5 v5.3.1 // 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/golang/snappy v1.0.0 // indirect
|
||||
github.com/google/go-tpm v0.9.8 // indirect
|
||||
github.com/google/go-tpm v0.9.6 // indirect
|
||||
github.com/gorilla/mux v1.8.1 // indirect
|
||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
||||
@@ -115,7 +115,7 @@ require (
|
||||
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.41.0 // indirect
|
||||
golang.org/x/sys v0.40.0 // indirect
|
||||
golang.org/x/time v0.14.0 // indirect
|
||||
google.golang.org/protobuf v1.36.11 // indirect
|
||||
modernc.org/libc v1.67.7 // indirect
|
||||
|
||||
50
go.sum
50
go.sum
@@ -88,8 +88,8 @@ github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkp
|
||||
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.13 h1:46nXokslUBsAJE/wMsp5gtO500a4F3Nkz9Ufpk2AcUM=
|
||||
github.com/gabriel-vasile/mimetype v1.4.13/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
|
||||
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/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=
|
||||
@@ -112,12 +112,12 @@ github.com/go-playground/validator/v10 v10.30.1 h1:f3zDSN/zOma+w6+1Wswgd9fLkdwy0
|
||||
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.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro=
|
||||
github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
||||
github.com/go-webauthn/webauthn v0.16.0 h1:A9BkfYIwWAMPSQCbM2HoWqo6JO5LFI8aqYAzo6nW7AY=
|
||||
github.com/go-webauthn/webauthn v0.16.0/go.mod h1:hm9RS/JNYeUu3KqGbzqlnHClhDGCZzTZlABjathwnN0=
|
||||
github.com/go-webauthn/x v0.2.1 h1:/oB8i0FhSANuoN+YJF5XHMtppa7zGEYaQrrf6ytotjc=
|
||||
github.com/go-webauthn/x v0.2.1/go.mod h1:Wm0X0zXkzznit4gHj4m82GiBZRMEm+TDUIoJWIQLsE4=
|
||||
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/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=
|
||||
@@ -125,16 +125,14 @@ github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6Wezm
|
||||
github.com/gobwas/ws v1.4.0 h1:CTaoG1tojrh4ucGPcoJFiAQUAsEWekEWvLy7GsVNqGs=
|
||||
github.com/gobwas/ws v1.4.0/go.mod h1:G3gNqMNtPppf5XUz7O4shetPpcZ1VJ7zt18dlUeakrc=
|
||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/golang-jwt/jwt/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-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
|
||||
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
|
||||
github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs=
|
||||
github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/go-tpm v0.9.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/go-tpm v0.9.6 h1:Ku42PT4LmjDu1H5C5ISWLlpI1mj+Zq7sPGKoRw2XROA=
|
||||
github.com/google/go-tpm v0.9.6/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
|
||||
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
@@ -192,8 +190,8 @@ github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0
|
||||
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.1 h1:S9keusg26gZpjMmPqB5hOEvNKnmd1lNmcHrbbH2lnFs=
|
||||
github.com/labstack/echo/v4 v4.15.1/go.mod h1:xmw1clThob0BSVRX1CRQkGQ/vjwcpOMjQZSZa9fKA/c=
|
||||
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/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||
@@ -209,8 +207,8 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-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.1 h1:mJTCJE5g7tRvaqKco6DfqOuJEjX+rRltDEnkEC02Y0M=
|
||||
github.com/meilisearch/meilisearch-go v0.36.1/go.mod h1:hWcR0MuWLSzHfbz9GGzIr3s9rnXLm1jqkmHkJPbUSvM=
|
||||
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/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=
|
||||
@@ -278,8 +276,8 @@ 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.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
|
||||
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
|
||||
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=
|
||||
@@ -293,12 +291,12 @@ 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.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
|
||||
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg=
|
||||
golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM=
|
||||
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
|
||||
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
|
||||
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=
|
||||
|
||||
@@ -4,6 +4,6 @@ dependencies:
|
||||
version: 16.7.27
|
||||
- name: meilisearch
|
||||
repository: https://meilisearch.github.io/meilisearch-kubernetes
|
||||
version: 0.26.0
|
||||
digest: sha256:7182bad3df032b3cb21a793ea6b027eaa96e142ff207b607b62df974bc82de90
|
||||
generated: "2026-03-09T03:39:04.820136+07:00"
|
||||
version: 0.17.1
|
||||
digest: sha256:ad702e35f258fed1f804d3e48b071767499f5730e099a8c461610950e5182368
|
||||
generated: "2025-09-21T04:49:08.679554149+02:00"
|
||||
|
||||
@@ -15,5 +15,5 @@ dependencies:
|
||||
condition: postgresql.enabled
|
||||
- name: meilisearch
|
||||
repository: https://meilisearch.github.io/meilisearch-kubernetes
|
||||
version: 0.26.0
|
||||
version: 0.17.1
|
||||
condition: meilisearch.enabled
|
||||
|
||||
@@ -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 := int (default 5432 .Values.postgresql.global.postgresql.service.ports.postgresql) }}
|
||||
{{- $port := 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 4 }}
|
||||
{{- toYaml .Values.statefulSet.updateStrategy | nindent 2 }}
|
||||
selector:
|
||||
matchLabels:
|
||||
{{- include "opengist.selectorLabels" . | nindent 6 }}
|
||||
|
||||
@@ -150,12 +150,7 @@ func resetHooks() {
|
||||
}
|
||||
|
||||
func indexGists() {
|
||||
log.Info().Msg("Rebuilding index from scratch...")
|
||||
if err := index.ResetIndex(); err != nil {
|
||||
log.Error().Err(err).Msg("Cannot reset index")
|
||||
return
|
||||
}
|
||||
|
||||
log.Info().Msg("Indexing all Gists...")
|
||||
gists, err := db.GetAllGistsRows()
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Cannot get gists")
|
||||
|
||||
@@ -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,38 +79,7 @@ func (p *GiteaCallbackProvider) GetProviderUserSSHKeys() ([]string, error) {
|
||||
|
||||
func (p *GiteaCallbackProvider) UpdateUserDB(user *db.User) {
|
||||
user.GiteaID = p.User.UserID
|
||||
|
||||
resp, err := http.Get(urlJoin(config.C.GiteaUrl, "/api/v1/users/", p.User.UserID))
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Cannot get user from Gitea")
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Cannot read Gitea response body")
|
||||
return
|
||||
}
|
||||
|
||||
var result map[string]interface{}
|
||||
err = gojson.Unmarshal(body, &result)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Cannot unmarshal Gitea response body")
|
||||
return
|
||||
}
|
||||
|
||||
field, ok := result["avatar_url"]
|
||||
if !ok {
|
||||
log.Error().Msg("Field 'avatar_url' not found in Gitea JSON response")
|
||||
return
|
||||
}
|
||||
|
||||
user.AvatarURL = field.(string)
|
||||
}
|
||||
|
||||
func (p *GiteaCallbackProvider) IsAdmin() bool {
|
||||
return false
|
||||
user.AvatarURL = p.User.AvatarURL
|
||||
}
|
||||
|
||||
func NewGiteaCallbackProvider(user *goth.User) CallbackProvider {
|
||||
|
||||
@@ -77,10 +77,6 @@ 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,10 +111,6 @@ 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,8 +3,6 @@ package oauth
|
||||
import (
|
||||
gocontext "context"
|
||||
"errors"
|
||||
"slices"
|
||||
|
||||
"github.com/markbates/goth"
|
||||
"github.com/markbates/goth/gothic"
|
||||
"github.com/markbates/goth/providers/openidConnect"
|
||||
@@ -81,31 +79,6 @@ 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,
|
||||
|
||||
@@ -2,16 +2,15 @@ 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 (
|
||||
@@ -33,7 +32,6 @@ 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) {
|
||||
@@ -71,29 +69,6 @@ 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 {
|
||||
|
||||
@@ -2,12 +2,6 @@ package cli
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/signal"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"syscall"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/thomiceli/opengist/internal/auth/webauthn"
|
||||
"github.com/thomiceli/opengist/internal/config"
|
||||
@@ -18,6 +12,11 @@ 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{
|
||||
@@ -38,7 +37,7 @@ var CmdStart = cli.Command{
|
||||
|
||||
Initialize(ctx)
|
||||
|
||||
httpServer := server.NewServer(os.Getenv("OG_DEV") == "1")
|
||||
httpServer := server.NewServer(os.Getenv("OG_DEV") == "1", path.Join(config.GetHomeDir(), "sessions"), false)
|
||||
go httpServer.Start()
|
||||
go ssh.Start()
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/thomiceli/opengist/internal/session"
|
||||
"io"
|
||||
"net/url"
|
||||
"os"
|
||||
@@ -12,8 +13,6 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/thomiceli/opengist/internal/session"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/rs/zerolog/log"
|
||||
"gopkg.in/yaml.v3"
|
||||
@@ -38,12 +37,11 @@ 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"`
|
||||
SearchDefault string `yaml:"search.default" env:"OG_SEARCH_DEFAULT"`
|
||||
IndexEnabled bool `yaml:"index.enabled" env:"OG_INDEX_ENABLED"` // deprecated
|
||||
Index string `yaml:"index" env:"OG_INDEX"`
|
||||
BleveDirname string `yaml:"index.dirname" env:"OG_INDEX_DIRNAME"` // deprecated
|
||||
MeiliHost string `yaml:"index.meili.host" env:"OG_MEILI_HOST"`
|
||||
MeiliAPIKey string `yaml:"index.meili.api-key" env:"OG_MEILI_API_KEY"`
|
||||
|
||||
GitDefaultBranch string `yaml:"git.default-branch" env:"OG_GIT_DEFAULT_BRANCH"`
|
||||
|
||||
@@ -112,7 +110,6 @@ func configWithDefaults() (*config, error) {
|
||||
c.OpengistHome = ""
|
||||
c.DBUri = "opengist.db"
|
||||
c.Index = "bleve"
|
||||
c.SearchDefault = "content"
|
||||
|
||||
c.SqliteJournalMode = "WAL"
|
||||
|
||||
|
||||
@@ -71,7 +71,6 @@ type Gist struct {
|
||||
Uuid string
|
||||
Title string
|
||||
URL string
|
||||
URLNormalized string
|
||||
Preview string
|
||||
PreviewFilename string
|
||||
PreviewMimeType string
|
||||
@@ -99,11 +98,6 @@ 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 {
|
||||
// Decrement fork counter if the gist was forked
|
||||
err := tx.Model(&Gist{}).
|
||||
@@ -116,8 +110,7 @@ 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_normalized = ?) AND users.username_normalized = ?",
|
||||
strings.ToLower(gistUuid)+"%", strings.ToLower(gistUuid), strings.ToLower(user)).
|
||||
Where("(gists.uuid like ? OR gists.url = ?) AND users.username like ?", gistUuid+"%", gistUuid, user).
|
||||
Joins("join users on gists.user_id = users.id").
|
||||
First(&gist).Error
|
||||
|
||||
@@ -727,17 +720,13 @@ 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"`
|
||||
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"`
|
||||
Title string `validate:"max=250" form:"title"`
|
||||
Description string `validate:"max=1000" form:"description"`
|
||||
URL string `validate:"max=32,alphanumdashorempty" form:"url"`
|
||||
Files []FileDTO `validate:"min=1,dive"`
|
||||
Name []string `form:"name"`
|
||||
Content []string `form:"content"`
|
||||
Topics string `validate:"gisttopics" form:"topics"`
|
||||
VisibilityDTO
|
||||
}
|
||||
|
||||
@@ -817,19 +806,18 @@ func (gist *Gist) ToIndexedGist() (*index.Gist, error) {
|
||||
}
|
||||
|
||||
indexedGist := &index.Gist{
|
||||
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,
|
||||
GistID: gist.ID,
|
||||
UserID: gist.UserID,
|
||||
Visibility: gist.Private.Uint(),
|
||||
Username: gist.User.Username,
|
||||
Title: gist.Title,
|
||||
Content: wholeContent,
|
||||
Filenames: fileNames,
|
||||
Extensions: exts,
|
||||
Languages: langs,
|
||||
Topics: topics,
|
||||
CreatedAt: gist.CreatedAt,
|
||||
UpdatedAt: gist.UpdatedAt,
|
||||
}
|
||||
|
||||
return indexedGist, nil
|
||||
|
||||
@@ -2,9 +2,7 @@ package db
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type MigrationVersion struct {
|
||||
@@ -14,74 +12,60 @@ type MigrationVersion struct {
|
||||
|
||||
func applyMigrations(dbInfo *databaseInfo) error {
|
||||
switch dbInfo.Type {
|
||||
case SQLite, PostgreSQL, MySQL:
|
||||
return applyAllMigrations(dbInfo.Type)
|
||||
case SQLite:
|
||||
return applySqliteMigrations()
|
||||
case PostgreSQL, MySQL:
|
||||
return nil
|
||||
default:
|
||||
return fmt.Errorf("unknown database type: %s", dbInfo.Type)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func applyAllMigrations(dbType databaseType) error {
|
||||
func applySqliteMigrations() error {
|
||||
// Create migration table if it doesn't exist
|
||||
if err := db.AutoMigrate(&MigrationVersion{}); err != nil {
|
||||
log.Fatal().Err(err).Msg("Error creating migration version table")
|
||||
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, []databaseType{SQLite}, v1_modifyConstraintToSSHKeys},
|
||||
{2, []databaseType{SQLite}, v2_lowercaseEmails},
|
||||
{3, nil, v3_normalizedColumns},
|
||||
{1, v1_modifyConstraintToSSHKeys},
|
||||
{2, v2_lowercaseEmails},
|
||||
// Add more migrations here as needed
|
||||
}
|
||||
|
||||
// Apply migrations
|
||||
for _, m := range migrations {
|
||||
if m.Version <= currentVersion.Version {
|
||||
continue
|
||||
}
|
||||
|
||||
// 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 m.Version > currentVersion.Version {
|
||||
tx := db.Begin()
|
||||
if err := tx.Error; err != nil {
|
||||
log.Fatal().Err(err).Msg("Error starting transaction")
|
||||
return err
|
||||
}
|
||||
if !applicable {
|
||||
// Advance version so we don't retry on next startup
|
||||
|
||||
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
|
||||
}
|
||||
currentVersion.Version = m.Version
|
||||
db.Save(¤tVersion)
|
||||
continue
|
||||
log.Info().Msg(fmt.Sprintf("Migration %d applied successfully", m.Version))
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
@@ -128,12 +112,3 @@ 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
|
||||
}
|
||||
|
||||
@@ -2,27 +2,24 @@ 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"`
|
||||
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
|
||||
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
|
||||
|
||||
Gists []Gist `gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE;foreignKey:UserID"`
|
||||
SSHKeys []SSHKey `gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE;foreignKey:UserID"`
|
||||
@@ -31,11 +28,6 @@ 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(`
|
||||
@@ -101,7 +93,7 @@ func (user *User) BeforeDelete(tx *gorm.DB) error {
|
||||
|
||||
func UserExists(username string) (bool, error) {
|
||||
var count int64
|
||||
err := db.Model(&User{}).Where("username_normalized = ?", strings.ToLower(username)).Count(&count).Error
|
||||
err := db.Model(&User{}).Where("username like ?", username).Count(&count).Error
|
||||
return count > 0, err
|
||||
}
|
||||
|
||||
@@ -119,7 +111,7 @@ func GetAllUsers(offset int) ([]*User, error) {
|
||||
func GetUserByUsername(username string) (*User, error) {
|
||||
user := new(User)
|
||||
err := db.
|
||||
Where("username_normalized = ?", strings.ToLower(username)).
|
||||
Where("username like ?", username).
|
||||
First(&user).Error
|
||||
return user, err
|
||||
}
|
||||
@@ -266,11 +258,6 @@ 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,
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
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
|
||||
}
|
||||
@@ -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: 'Suchindex neu aufbauen'
|
||||
admin.actions.index-gists: 'Alle Gists Indexieren'
|
||||
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: 'Suchindex wird neu aufgebaut...'
|
||||
flash.admin.index-gists: 'Indiziere alle Gists...'
|
||||
|
||||
flash.auth.username-exists: 'Benutzername existiert bereits'
|
||||
flash.auth.invalid-credentials: 'Ungültige Anmeldeinformationen'
|
||||
|
||||
@@ -88,12 +88,10 @@ 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
|
||||
@@ -202,13 +200,6 @@ 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
|
||||
@@ -250,7 +241,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 OAuth2 provider
|
||||
error.oauth-unsupported: Unsupported provider
|
||||
error.cannot-bind-data: Cannot bind data
|
||||
error.invalid-number: Invalid number
|
||||
error.invalid-character-unescaped: Invalid character unescaped
|
||||
@@ -294,7 +285,7 @@ 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: Rebuild search index
|
||||
admin.actions.index-gists: Index all gists
|
||||
admin.actions.sync-gist-languages: Synchronize all gists languages
|
||||
admin.id: ID
|
||||
admin.user: User
|
||||
@@ -340,7 +331,7 @@ 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: Rebuilding search index...
|
||||
flash.admin.index-gists: Indexing all gists...
|
||||
flash.admin.sync-gist-languages: Syncing Gist languages...
|
||||
|
||||
flash.auth.username-exists: Username already exists
|
||||
@@ -352,8 +343,6 @@ 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
|
||||
|
||||
@@ -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: 'Reconstruir índice de búsqueda'
|
||||
admin.actions.index-gists: 'Indexar todos los gists'
|
||||
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: 'Reconstruyendo índice de búsqueda...'
|
||||
flash.admin.index-gists: 'Indexando todos los gists...'
|
||||
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: Reconstruire l'index de recherche
|
||||
admin.actions.index-gists: Indexer tous les gists
|
||||
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: 'Reconstruction de l''index de recherche...'
|
||||
flash.admin.index-gists: 'Indexation de tous les gists...'
|
||||
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: Keresési index újraépítése
|
||||
admin.actions.index-gists: Gistek indexelé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: 'Ricostruisci indice di ricerca'
|
||||
admin.actions.index-gists: 'Indicizza tutti i gists'
|
||||
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: 'Ricostruzione indice di ricerca...'
|
||||
flash.admin.index-gists: 'Indicizzando tutti i gists...'
|
||||
|
||||
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: 'Przebuduj indeks wyszukiwania'
|
||||
admin.actions.index-gists: 'Indeksuj wszystkie Gisty'
|
||||
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: 'Przebudowywanie indeksu wyszukiwania...'
|
||||
flash.admin.index-gists: 'Indeksowanie wszystkich Gistów...'
|
||||
|
||||
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: Arama dizinini yeniden oluştur
|
||||
admin.actions.index-gists: Tüm gistleri indeksle
|
||||
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: Arama dizini yeniden oluşturuluyor...
|
||||
flash.admin.index-gists: Tüm gistler indeksleniyor...
|
||||
|
||||
flash.auth.username-exists: Kullanıcı adı zaten mevcut
|
||||
flash.auth.invalid-credentials: Geçersiz kimlik bilgileri
|
||||
|
||||
@@ -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: Перебудувати пошуковий індекс
|
||||
admin.actions.index-gists: Проіндексувати всі 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: Перебудова пошукового індексу...
|
||||
flash.admin.index-gists: Індексація всіх gists...
|
||||
|
||||
flash.auth.username-exists: Це ім'я користувача вже існує
|
||||
flash.auth.invalid-credentials: Недійсні облікові дані
|
||||
|
||||
@@ -214,7 +214,7 @@ admin.invitations: '邀请'
|
||||
admin.invitations.create: '创建邀请'
|
||||
admin.actions.sync-previews: '同步所有 Gists 预览'
|
||||
admin.actions.reset-hooks: '重置所有存储库的 Git 服务 hooks'
|
||||
admin.actions.index-gists: '重建搜索索引'
|
||||
admin.actions.index-gists: '索引所有 Gists'
|
||||
admin.invitations.help: '即使在禁用注册功能的情况下,邀请功能也可用于创建帐户。'
|
||||
admin.invitations.max_uses: '最多使用次数'
|
||||
admin.invitations.expires_at: '过期时间'
|
||||
@@ -231,7 +231,7 @@ flash.admin.sync-db: '正在从数据库同步存储库...'
|
||||
flash.admin.git-gc: '正在进行存储库垃圾回收...'
|
||||
flash.admin.sync-previews: '正在同步 Gist 预览...'
|
||||
flash.admin.reset-hooks: '正在重置所有存储库的 Git 服务挂钩...'
|
||||
flash.admin.index-gists: '正在重建搜索索引...'
|
||||
flash.admin.index-gists: '正在索引所有 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: 重建搜尋索引
|
||||
admin.actions.index-gists: 索引所有的 Gists
|
||||
gist.search.help.user: 由使用者建立的 Gists
|
||||
gist.search.found: 已找到 Gists
|
||||
gist.search.help.extension: Gists 的副檔名
|
||||
|
||||
@@ -2,21 +2,16 @@ package index
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/blevesearch/bleve/v2"
|
||||
"github.com/blevesearch/bleve/v2/analysis/analyzer/custom"
|
||||
"github.com/blevesearch/bleve/v2/analysis/token/camelcase"
|
||||
"github.com/blevesearch/bleve/v2/analysis/token/length"
|
||||
"github.com/blevesearch/bleve/v2/analysis/token/lowercase"
|
||||
"github.com/blevesearch/bleve/v2/analysis/token/unicodenorm"
|
||||
"github.com/blevesearch/bleve/v2/analysis/tokenizer/unicode"
|
||||
"github.com/blevesearch/bleve/v2/search/query"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/thomiceli/opengist/internal/config"
|
||||
)
|
||||
|
||||
type BleveIndexer struct {
|
||||
@@ -57,9 +52,14 @@ func (i *BleveIndexer) open() (bleve.Index, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
docMapping := bleve.NewDocumentMapping()
|
||||
docMapping.AddFieldMappingsAt("GistID", bleve.NewNumericFieldMapping())
|
||||
docMapping.AddFieldMappingsAt("UserID", bleve.NewNumericFieldMapping())
|
||||
docMapping.AddFieldMappingsAt("Visibility", bleve.NewNumericFieldMapping())
|
||||
docMapping.AddFieldMappingsAt("Content", bleve.NewTextFieldMapping())
|
||||
|
||||
mapping := bleve.NewIndexMapping()
|
||||
|
||||
// Token filters
|
||||
if err = mapping.AddCustomTokenFilter("unicodeNormalize", map[string]any{
|
||||
"type": unicodenorm.Name,
|
||||
"form": unicodenorm.NFC,
|
||||
@@ -67,88 +67,21 @@ func (i *BleveIndexer) open() (bleve.Index, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err = mapping.AddCustomTokenFilter("lengthMin2", map[string]interface{}{
|
||||
"type": length.Name,
|
||||
"min": 2.0,
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Analyzer: split mode (camelCase splitting for partial search)
|
||||
// "CPUCard" -> ["cpu", "card"]
|
||||
if err = mapping.AddCustomAnalyzer("codeSplit", map[string]interface{}{
|
||||
if err = mapping.AddCustomAnalyzer("gistAnalyser", map[string]interface{}{
|
||||
"type": custom.Name,
|
||||
"char_filters": []string{},
|
||||
"tokenizer": unicode.Name,
|
||||
"token_filters": []string{"unicodeNormalize", camelcase.Name, lowercase.Name, "lengthMin2"},
|
||||
"token_filters": []string{"unicodeNormalize", camelcase.Name, lowercase.Name},
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Analyzer: exact mode (no camelCase splitting for full-word search)
|
||||
// "CPUCard" -> ["cpucard"]
|
||||
if err = mapping.AddCustomAnalyzer("codeExact", map[string]interface{}{
|
||||
"type": custom.Name,
|
||||
"char_filters": []string{},
|
||||
"tokenizer": unicode.Name,
|
||||
"token_filters": []string{"unicodeNormalize", lowercase.Name},
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Analyzer: keyword with lowercase (for Languages - single token, no splitting)
|
||||
if err = mapping.AddCustomAnalyzer("lowercaseKeyword", map[string]interface{}{
|
||||
"type": custom.Name,
|
||||
"char_filters": []string{},
|
||||
"tokenizer": "single",
|
||||
"token_filters": []string{lowercase.Name},
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Document mapping
|
||||
docMapping := bleve.NewDocumentMapping()
|
||||
docMapping.AddFieldMappingsAt("GistID", bleve.NewNumericFieldMapping())
|
||||
docMapping.AddFieldMappingsAt("UserID", bleve.NewNumericFieldMapping())
|
||||
docMapping.AddFieldMappingsAt("Visibility", bleve.NewNumericFieldMapping())
|
||||
|
||||
// Content: dual indexing (exact + split)
|
||||
// "Content" uses the property name so Bleve resolves its analyzer correctly
|
||||
contentExact := bleve.NewTextFieldMapping()
|
||||
contentExact.Name = "Content"
|
||||
contentExact.Analyzer = "codeExact"
|
||||
contentExact.Store = false
|
||||
contentExact.IncludeTermVectors = true
|
||||
|
||||
contentSplit := bleve.NewTextFieldMapping()
|
||||
contentSplit.Name = "ContentSplit"
|
||||
contentSplit.Analyzer = "codeSplit"
|
||||
contentSplit.Store = false
|
||||
contentSplit.IncludeTermVectors = true
|
||||
|
||||
docMapping.AddFieldMappingsAt("Content", contentExact, contentSplit)
|
||||
|
||||
// Languages: keyword analyzer (preserves as single token)
|
||||
languageFieldMapping := bleve.NewTextFieldMapping()
|
||||
languageFieldMapping.Analyzer = "lowercaseKeyword"
|
||||
docMapping.AddFieldMappingsAt("Languages", languageFieldMapping)
|
||||
|
||||
// All other text fields use codeSplit as default
|
||||
docMapping.DefaultAnalyzer = "codeSplit"
|
||||
docMapping.DefaultAnalyzer = "gistAnalyser"
|
||||
mapping.DefaultMapping = docMapping
|
||||
|
||||
return bleve.New(i.path, mapping)
|
||||
}
|
||||
|
||||
func (i *BleveIndexer) Reset() error {
|
||||
i.Close()
|
||||
if err := os.RemoveAll(i.path); err != nil {
|
||||
return fmt.Errorf("failed to remove Bleve index directory: %w", err)
|
||||
}
|
||||
log.Info().Msg("Bleve index directory removed, re-creating index")
|
||||
return i.Init()
|
||||
}
|
||||
|
||||
func (i *BleveIndexer) Close() {
|
||||
if i == nil || i.index == nil {
|
||||
return
|
||||
@@ -172,111 +105,18 @@ func (i *BleveIndexer) Remove(gistID uint) error {
|
||||
return (*atomicIndexer.Load()).(*BleveIndexer).index.Delete(strconv.Itoa(int(gistID)))
|
||||
}
|
||||
|
||||
// Search returns a list of Gist IDs that match the given search metadata.
|
||||
// The method returns an error if any.
|
||||
//
|
||||
// The queryMetadata parameter is used to filter the search results.
|
||||
// For example, passing a non-empty Username will search for gists whose
|
||||
// username matches the given string.
|
||||
//
|
||||
// If the "All" field in queryMetadata is non-empty, the method will
|
||||
// search across all metadata fields with OR logic. Otherwise, the method
|
||||
// will add each metadata field with AND logic.
|
||||
//
|
||||
// The page parameter is used to paginate the search results.
|
||||
// The method returns the total number of search results in the second return
|
||||
// value.
|
||||
//
|
||||
// The third return value is a map of language counts for the search results.
|
||||
// The language counts are computed by asking Bleve to return the top 10
|
||||
// facets for the "Languages" field.
|
||||
func (i *BleveIndexer) Search(metadata SearchGistMetadata, userId uint, page int) ([]uint, uint64, map[string]int, error) {
|
||||
func (i *BleveIndexer) Search(queryStr string, queryMetadata SearchGistMetadata, userId uint, page int) ([]uint, uint64, map[string]int, error) {
|
||||
var err error
|
||||
var indexerQuery query.Query = bleve.NewMatchAllQuery()
|
||||
|
||||
// Query factory
|
||||
factoryQuery := func(field, value string) query.Query {
|
||||
query := bleve.NewMatchPhraseQuery(value)
|
||||
query.SetField(field)
|
||||
return query
|
||||
}
|
||||
|
||||
// Exact search
|
||||
addQuery := func(field, value string) {
|
||||
if value != "" && value != "." {
|
||||
indexerQuery = bleve.NewConjunctionQuery(indexerQuery, factoryQuery(field, value))
|
||||
}
|
||||
}
|
||||
|
||||
// Query factory for text fields: exact match boosted + match query + prefix
|
||||
factoryTextQuery := func(field, value string) query.Query {
|
||||
exact := bleve.NewMatchPhraseQuery(value)
|
||||
exact.SetField(field)
|
||||
exact.SetBoost(2.0)
|
||||
|
||||
fuzzy := bleve.NewMatchQuery(value)
|
||||
fuzzy.SetField(field)
|
||||
fuzzy.SetFuzziness(1)
|
||||
fuzzy.SetOperator(query.MatchQueryOperatorAnd)
|
||||
|
||||
queries := []query.Query{exact, fuzzy}
|
||||
|
||||
if len([]rune(value)) >= 2 {
|
||||
prefix := bleve.NewPrefixQuery(strings.ToLower(value))
|
||||
prefix.SetField(field)
|
||||
prefix.SetBoost(1.5)
|
||||
queries = append(queries, prefix)
|
||||
}
|
||||
|
||||
if len([]rune(value)) >= 4 {
|
||||
wildcard := bleve.NewWildcardQuery("*" + strings.ToLower(value) + "*")
|
||||
wildcard.SetField(field)
|
||||
wildcard.SetBoost(0.5)
|
||||
queries = append(queries, wildcard)
|
||||
}
|
||||
|
||||
return bleve.NewDisjunctionQuery(queries...)
|
||||
}
|
||||
|
||||
// Query factory for Content: searches both exact (Content) and split (ContentSplit) fields
|
||||
factoryContentQuery := func(value string) query.Query {
|
||||
// Exact field (no camelCase split): matches "cpucard"
|
||||
exactMatch := bleve.NewMatchQuery(value)
|
||||
exactMatch.SetField("Content")
|
||||
exactMatch.SetOperator(query.MatchQueryOperatorAnd)
|
||||
exactMatch.SetBoost(2.0)
|
||||
|
||||
// Split field (camelCase split): matches "cpu", "card"
|
||||
splitMatch := bleve.NewMatchQuery(value)
|
||||
splitMatch.SetField("ContentSplit")
|
||||
splitMatch.SetFuzziness(1)
|
||||
splitMatch.SetOperator(query.MatchQueryOperatorAnd)
|
||||
splitMatch.SetBoost(1.0)
|
||||
|
||||
queries := []query.Query{exactMatch, splitMatch}
|
||||
|
||||
if len([]rune(value)) >= 2 {
|
||||
prefix := bleve.NewPrefixQuery(strings.ToLower(value))
|
||||
prefix.SetField("Content")
|
||||
prefix.SetBoost(1.5)
|
||||
queries = append(queries, prefix)
|
||||
}
|
||||
|
||||
if len([]rune(value)) >= 4 {
|
||||
wildcard := bleve.NewWildcardQuery("*" + strings.ToLower(value) + "*")
|
||||
wildcard.SetField("Content")
|
||||
wildcard.SetBoost(0.5)
|
||||
queries = append(queries, wildcard)
|
||||
}
|
||||
|
||||
return bleve.NewDisjunctionQuery(queries...)
|
||||
}
|
||||
|
||||
// Text field search
|
||||
addTextQuery := func(field, value string) {
|
||||
if value != "" && value != "." {
|
||||
indexerQuery = bleve.NewConjunctionQuery(indexerQuery, factoryTextQuery(field, value))
|
||||
}
|
||||
var indexerQuery query.Query
|
||||
if queryStr != "" {
|
||||
// Use match query with fuzzy matching for more flexible content search
|
||||
contentQuery := bleve.NewMatchQuery(queryStr)
|
||||
contentQuery.SetField("Content")
|
||||
contentQuery.SetFuzziness(2)
|
||||
indexerQuery = contentQuery
|
||||
} else {
|
||||
contentQuery := bleve.NewMatchAllQuery()
|
||||
indexerQuery = contentQuery
|
||||
}
|
||||
|
||||
// Visibility filtering: show public gists (Visibility=0) OR user's own gists
|
||||
@@ -292,62 +132,48 @@ func (i *BleveIndexer) Search(metadata SearchGistMetadata, userId uint, page int
|
||||
accessQuery := bleve.NewDisjunctionQuery(publicQuery, userIdQuery)
|
||||
indexerQuery = bleve.NewConjunctionQuery(accessQuery, indexerQuery)
|
||||
|
||||
buildFieldQuery := func(field, value string) query.Query {
|
||||
switch field {
|
||||
case "Content":
|
||||
return factoryContentQuery(value)
|
||||
case "Title", "Description", "Filenames":
|
||||
return factoryTextQuery(field, value)
|
||||
case "Extensions":
|
||||
return factoryQuery(field, "."+value)
|
||||
default: // Username, Languages, Topics
|
||||
return factoryQuery(field, value)
|
||||
}
|
||||
}
|
||||
|
||||
// Handle "All" field - search across all metadata fields with OR logic
|
||||
if metadata.All != "" {
|
||||
allQueries := make([]query.Query, 0, len(AllSearchFields))
|
||||
for _, field := range AllSearchFields {
|
||||
allQueries = append(allQueries, buildFieldQuery(field, metadata.All))
|
||||
if queryMetadata.All != "" {
|
||||
allQueries := make([]query.Query, 0)
|
||||
|
||||
// Create match phrase queries for each field
|
||||
fields := []struct {
|
||||
field string
|
||||
value string
|
||||
}{
|
||||
{"Username", queryMetadata.All},
|
||||
{"Title", queryMetadata.All},
|
||||
{"Extensions", "." + queryMetadata.All},
|
||||
{"Filenames", queryMetadata.All},
|
||||
{"Languages", queryMetadata.All},
|
||||
{"Topics", queryMetadata.All},
|
||||
}
|
||||
indexerQuery = bleve.NewConjunctionQuery(indexerQuery, bleve.NewDisjunctionQuery(allQueries...))
|
||||
|
||||
for _, f := range fields {
|
||||
q := bleve.NewMatchPhraseQuery(f.value)
|
||||
q.FieldVal = f.field
|
||||
allQueries = append(allQueries, q)
|
||||
}
|
||||
|
||||
// Combine all field queries with OR (disjunction)
|
||||
allDisjunction := bleve.NewDisjunctionQuery(allQueries...)
|
||||
indexerQuery = bleve.NewConjunctionQuery(indexerQuery, allDisjunction)
|
||||
} else {
|
||||
// Original behavior: add each metadata field with AND logic
|
||||
addQuery("Username", metadata.Username)
|
||||
addTextQuery("Title", metadata.Title)
|
||||
addTextQuery("Description", metadata.Description)
|
||||
addQuery("Extensions", "."+metadata.Extension)
|
||||
addTextQuery("Filenames", metadata.Filename)
|
||||
addQuery("Languages", metadata.Language)
|
||||
addQuery("Topics", metadata.Topic)
|
||||
if metadata.Content != "" {
|
||||
indexerQuery = bleve.NewConjunctionQuery(indexerQuery, factoryContentQuery(metadata.Content))
|
||||
addQuery := func(field, value string) {
|
||||
if value != "" && value != "." {
|
||||
q := bleve.NewMatchPhraseQuery(value)
|
||||
q.FieldVal = field
|
||||
indexerQuery = bleve.NewConjunctionQuery(indexerQuery, q)
|
||||
}
|
||||
}
|
||||
|
||||
// Handle default search fields from config with OR logic
|
||||
if metadata.Default != "" {
|
||||
var fields []string
|
||||
for _, f := range strings.Split(config.C.SearchDefault, ",") {
|
||||
f = strings.TrimSpace(f)
|
||||
if f == "all" {
|
||||
fields = AllSearchFields
|
||||
break
|
||||
}
|
||||
if indexField, ok := SearchFieldMap[f]; ok {
|
||||
fields = append(fields, indexField)
|
||||
}
|
||||
}
|
||||
if len(fields) == 1 {
|
||||
indexerQuery = bleve.NewConjunctionQuery(indexerQuery, buildFieldQuery(fields[0], metadata.Default))
|
||||
} else if len(fields) > 1 {
|
||||
defaultQueries := make([]query.Query, 0, len(fields))
|
||||
for _, field := range fields {
|
||||
defaultQueries = append(defaultQueries, buildFieldQuery(field, metadata.Default))
|
||||
}
|
||||
indexerQuery = bleve.NewConjunctionQuery(indexerQuery, bleve.NewDisjunctionQuery(defaultQueries...))
|
||||
}
|
||||
}
|
||||
addQuery("Username", queryMetadata.Username)
|
||||
addQuery("Title", queryMetadata.Title)
|
||||
addQuery("Extensions", "."+queryMetadata.Extension)
|
||||
addQuery("Filenames", queryMetadata.Filename)
|
||||
addQuery("Languages", queryMetadata.Language)
|
||||
addQuery("Topics", queryMetadata.Topic)
|
||||
}
|
||||
|
||||
languageFacet := bleve.NewFacetRequest("Languages", 10)
|
||||
@@ -360,8 +186,6 @@ func (i *BleveIndexer) Search(metadata SearchGistMetadata, userId uint, page int
|
||||
s.Fields = []string{"GistID"}
|
||||
s.IncludeLocations = false
|
||||
|
||||
log.Debug().Interface("searchRequest", s).Msg("Bleve search request")
|
||||
|
||||
results, err := (*atomicIndexer.Load()).(*BleveIndexer).index.Search(s)
|
||||
if err != nil {
|
||||
return nil, 0, nil, err
|
||||
|
||||
@@ -4,31 +4,33 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// setupBleveIndexer creates a new BleveIndexer for testing
|
||||
func setupBleveIndexer(t *testing.T) (Indexer, func()) {
|
||||
zerolog.SetGlobalLevel(zerolog.Disabled)
|
||||
func setupBleveIndexer(t *testing.T) (*BleveIndexer, func()) {
|
||||
t.Helper()
|
||||
|
||||
// Create a temporary directory for the test index
|
||||
tmpDir, err := os.MkdirTemp("", "bleve-test-*")
|
||||
require.NoError(t, err)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create temp directory: %v", err)
|
||||
}
|
||||
|
||||
indexPath := filepath.Join(tmpDir, "test.index")
|
||||
indexer := NewBleveIndexer(indexPath)
|
||||
|
||||
// Initialize the indexer
|
||||
err = indexer.Init()
|
||||
if err != nil {
|
||||
os.RemoveAll(tmpDir)
|
||||
t.Fatalf("Failed to initialize BleveIndexer: %v", err)
|
||||
}
|
||||
|
||||
// Store in the global atomicIndexer since Add/Remove use it
|
||||
var idx Indexer = indexer
|
||||
atomicIndexer.Store(&idx)
|
||||
|
||||
// Return cleanup function
|
||||
cleanup := func() {
|
||||
atomicIndexer.Store(nil)
|
||||
indexer.Close()
|
||||
@@ -38,50 +40,123 @@ func setupBleveIndexer(t *testing.T) (Indexer, func()) {
|
||||
return indexer, cleanup
|
||||
}
|
||||
|
||||
func TestBleveAddAndSearch(t *testing.T) { testAddAndSearch(t, setupBleveIndexer) }
|
||||
func TestBleveAccessControl(t *testing.T) { testAccessControl(t, setupBleveIndexer) }
|
||||
func TestBleveMetadataFilters(t *testing.T) { testMetadataFilters(t, setupBleveIndexer) }
|
||||
func TestBleveAllFieldSearch(t *testing.T) { testAllFieldSearch(t, setupBleveIndexer) }
|
||||
func TestBleveFuzzySearch(t *testing.T) { testFuzzySearch(t, setupBleveIndexer) }
|
||||
func TestBleveContentSearch(t *testing.T) { testContentSearch(t, setupBleveIndexer) }
|
||||
func TestBlevePagination(t *testing.T) { testPagination(t, setupBleveIndexer) }
|
||||
func TestBleveLanguageFacets(t *testing.T) { testLanguageFacets(t, setupBleveIndexer) }
|
||||
func TestBleveWildcardSearch(t *testing.T) { testWildcardSearch(t, setupBleveIndexer) }
|
||||
func TestBleveMetadataOnlySearch(t *testing.T) { testMetadataOnlySearch(t, setupBleveIndexer) }
|
||||
func TestBleveTitleFuzzySearch(t *testing.T) { testTitleFuzzySearch(t, setupBleveIndexer) }
|
||||
func TestBleveMultiLanguageFacets(t *testing.T) { testMultiLanguageFacets(t, setupBleveIndexer) }
|
||||
func TestBleveIndexerAddGist(t *testing.T) {
|
||||
indexer, cleanup := setupBleveIndexer(t)
|
||||
defer cleanup()
|
||||
|
||||
func TestBlevePersistence(t *testing.T) {
|
||||
tmpDir, err := os.MkdirTemp("", "bleve-persist-test-*")
|
||||
require.NoError(t, err)
|
||||
testIndexerAddGist(t, indexer)
|
||||
}
|
||||
|
||||
func TestBleveIndexerAllFieldSearch(t *testing.T) {
|
||||
indexer, cleanup := setupBleveIndexer(t)
|
||||
defer cleanup()
|
||||
|
||||
testIndexerAllFieldSearch(t, indexer)
|
||||
}
|
||||
|
||||
func TestBleveIndexerFuzzySearch(t *testing.T) {
|
||||
indexer, cleanup := setupBleveIndexer(t)
|
||||
defer cleanup()
|
||||
|
||||
testIndexerFuzzySearch(t, indexer)
|
||||
}
|
||||
|
||||
func TestBleveIndexerSearchBasic(t *testing.T) {
|
||||
indexer, cleanup := setupBleveIndexer(t)
|
||||
defer cleanup()
|
||||
|
||||
testIndexerSearchBasic(t, indexer)
|
||||
}
|
||||
|
||||
func TestBleveIndexerPagination(t *testing.T) {
|
||||
indexer, cleanup := setupBleveIndexer(t)
|
||||
defer cleanup()
|
||||
|
||||
testIndexerPagination(t, indexer)
|
||||
}
|
||||
|
||||
// TestBleveIndexerInitAndClose tests Bleve-specific initialization and closing
|
||||
func TestBleveIndexerInitAndClose(t *testing.T) {
|
||||
tmpDir, err := os.MkdirTemp("", "bleve-init-test-*")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create temp directory: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(tmpDir)
|
||||
|
||||
indexPath := filepath.Join(tmpDir, "test.index")
|
||||
indexer := NewBleveIndexer(indexPath)
|
||||
|
||||
// Create and populate index
|
||||
indexer1 := NewBleveIndexer(indexPath)
|
||||
require.NoError(t, indexer1.Init())
|
||||
// Test initialization
|
||||
err = indexer.Init()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to initialize BleveIndexer: %v", err)
|
||||
}
|
||||
|
||||
var idx Indexer = indexer1
|
||||
atomicIndexer.Store(&idx)
|
||||
if indexer.index == nil {
|
||||
t.Fatal("Expected index to be initialized, got nil")
|
||||
}
|
||||
|
||||
g := newGist(1, 1, 0, "persistent data survives restart")
|
||||
require.NoError(t, indexer1.Add(g))
|
||||
// Test closing
|
||||
indexer.Close()
|
||||
|
||||
indexer1.Close()
|
||||
atomicIndexer.Store(nil)
|
||||
|
||||
// Reopen at same path
|
||||
// Test reopening the same index
|
||||
indexer2 := NewBleveIndexer(indexPath)
|
||||
require.NoError(t, indexer2.Init())
|
||||
err = indexer2.Init()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to reopen BleveIndexer: %v", err)
|
||||
}
|
||||
defer indexer2.Close()
|
||||
|
||||
idx = indexer2
|
||||
atomicIndexer.Store(&idx)
|
||||
defer atomicIndexer.Store(nil)
|
||||
|
||||
ids, total, _, err := indexer2.Search(SearchGistMetadata{Content: "persistent"}, 1, 1)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, uint64(1), total, "data should survive close+reopen")
|
||||
require.Equal(t, uint(1), ids[0])
|
||||
if indexer2.index == nil {
|
||||
t.Fatal("Expected reopened index to be initialized, got nil")
|
||||
}
|
||||
}
|
||||
|
||||
// TestBleveIndexerUnicodeSearch tests that Unicode content can be indexed and searched
|
||||
func TestBleveIndexerUnicodeSearch(t *testing.T) {
|
||||
indexer, cleanup := setupBleveIndexer(t)
|
||||
defer cleanup()
|
||||
|
||||
// Add a gist with Unicode content
|
||||
gist := &Gist{
|
||||
GistID: 100,
|
||||
UserID: 100,
|
||||
Visibility: 0,
|
||||
Username: "testuser",
|
||||
Title: "Unicode Test",
|
||||
Content: "Hello world with unicode characters: café résumé naïve",
|
||||
Filenames: []string{"test.txt"},
|
||||
Extensions: []string{".txt"},
|
||||
Languages: []string{"Text"},
|
||||
Topics: []string{"unicode"},
|
||||
CreatedAt: 1234567890,
|
||||
UpdatedAt: 1234567890,
|
||||
}
|
||||
|
||||
err := indexer.Add(gist)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to add gist: %v", err)
|
||||
}
|
||||
|
||||
// Search for unicode content
|
||||
gistIDs, total, _, err := indexer.Search("café", SearchGistMetadata{}, 100, 1)
|
||||
if err != nil {
|
||||
t.Fatalf("Search failed: %v", err)
|
||||
}
|
||||
|
||||
if total == 0 {
|
||||
t.Skip("Unicode search may require specific index configuration")
|
||||
return
|
||||
}
|
||||
|
||||
found := false
|
||||
for _, id := range gistIDs {
|
||||
if id == 100 {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Log("Unicode gist not found in search results, but other results were returned")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,43 +1,26 @@
|
||||
package index
|
||||
|
||||
var AllSearchFields = []string{"Username", "Title", "Description", "Filenames", "Extensions", "Languages", "Topics", "Content"}
|
||||
|
||||
var SearchFieldMap = map[string]string{
|
||||
"user": "Username",
|
||||
"title": "Title",
|
||||
"description": "Description",
|
||||
"filename": "Filenames",
|
||||
"extension": "Extensions",
|
||||
"language": "Languages",
|
||||
"topic": "Topics",
|
||||
"content": "Content",
|
||||
}
|
||||
|
||||
type Gist struct {
|
||||
GistID uint
|
||||
UserID uint
|
||||
Visibility uint
|
||||
Username string
|
||||
Description string
|
||||
Title string
|
||||
Content string
|
||||
Filenames []string
|
||||
Extensions []string
|
||||
Languages []string
|
||||
Topics []string
|
||||
CreatedAt int64
|
||||
UpdatedAt int64
|
||||
GistID uint
|
||||
UserID uint
|
||||
Visibility uint
|
||||
Username string
|
||||
Title string
|
||||
Content string
|
||||
Filenames []string
|
||||
Extensions []string
|
||||
Languages []string
|
||||
Topics []string
|
||||
CreatedAt int64
|
||||
UpdatedAt int64
|
||||
}
|
||||
|
||||
type SearchGistMetadata struct {
|
||||
Username string
|
||||
Title string
|
||||
Description string
|
||||
Content string
|
||||
Filename string
|
||||
Extension string
|
||||
Language string
|
||||
Topic string
|
||||
All string
|
||||
Default string
|
||||
Username string
|
||||
Title string
|
||||
Filename string
|
||||
Extension string
|
||||
Language string
|
||||
Topic string
|
||||
All string
|
||||
}
|
||||
|
||||
@@ -2,11 +2,10 @@ package index
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/thomiceli/opengist/internal/config"
|
||||
"path/filepath"
|
||||
"sync/atomic"
|
||||
)
|
||||
|
||||
var atomicIndexer atomic.Pointer[Indexer]
|
||||
@@ -14,10 +13,9 @@ var atomicIndexer atomic.Pointer[Indexer]
|
||||
type Indexer interface {
|
||||
Init() error
|
||||
Close()
|
||||
Reset() error
|
||||
Add(gist *Gist) error
|
||||
Remove(gistID uint) error
|
||||
Search(metadata SearchGistMetadata, userId uint, page int) ([]uint, uint64, map[string]int, error)
|
||||
Search(query string, metadata SearchGistMetadata, userId uint, page int) ([]uint, uint64, map[string]int, error)
|
||||
}
|
||||
|
||||
type IndexerType string
|
||||
@@ -86,19 +84,6 @@ func Close() {
|
||||
atomicIndexer.Store(nil)
|
||||
}
|
||||
|
||||
func ResetIndex() error {
|
||||
if !IndexEnabled() {
|
||||
return nil
|
||||
}
|
||||
|
||||
idx := atomicIndexer.Load()
|
||||
if idx == nil {
|
||||
return fmt.Errorf("indexer is not initialized")
|
||||
}
|
||||
|
||||
return (*idx).Reset()
|
||||
}
|
||||
|
||||
func AddInIndex(gist *Gist) error {
|
||||
if !IndexEnabled() {
|
||||
return nil
|
||||
@@ -125,11 +110,7 @@ func RemoveFromIndex(gistID uint) error {
|
||||
return (*idx).Remove(gistID)
|
||||
}
|
||||
|
||||
// SearchGists returns a list of Gist IDs that match the given search metadata.
|
||||
// If the indexer is not enabled, it returns nil, 0, nil, nil.
|
||||
// If the indexer is not initialized, it returns nil, 0, nil, fmt.Errorf("indexer is not initialized").
|
||||
// The function returns an error if any.
|
||||
func SearchGists(metadata SearchGistMetadata, userId uint, page int) ([]uint, uint64, map[string]int, error) {
|
||||
func SearchGists(query string, metadata SearchGistMetadata, userId uint, page int) ([]uint, uint64, map[string]int, error) {
|
||||
if !IndexEnabled() {
|
||||
return nil, 0, nil, nil
|
||||
}
|
||||
@@ -139,7 +120,7 @@ func SearchGists(metadata SearchGistMetadata, userId uint, page int) ([]uint, ui
|
||||
return nil, 0, nil, fmt.Errorf("indexer is not initialized")
|
||||
}
|
||||
|
||||
return (*idx).Search(metadata, userId, page)
|
||||
return (*idx).Search(query, metadata, userId, page)
|
||||
}
|
||||
|
||||
func DepreactionIndexDirname() {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -6,11 +6,9 @@ import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"unicode"
|
||||
|
||||
"github.com/meilisearch/meilisearch-go"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/thomiceli/opengist/internal/config"
|
||||
)
|
||||
|
||||
type MeiliIndexer struct {
|
||||
@@ -52,45 +50,28 @@ func (i *MeiliIndexer) open() (meilisearch.IndexManager, error) {
|
||||
i.client = meilisearch.New(i.host, meilisearch.WithAPIKey(i.apikey))
|
||||
indexResult, err := i.client.GetIndex(i.indexName)
|
||||
|
||||
if indexResult == nil || err != nil {
|
||||
_, err = i.client.CreateIndex(&meilisearch.IndexConfig{
|
||||
Uid: i.indexName,
|
||||
PrimaryKey: "GistID",
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if indexResult != nil && err == nil {
|
||||
return indexResult.IndexManager, nil
|
||||
}
|
||||
|
||||
_, err = i.client.CreateIndex(&meilisearch.IndexConfig{
|
||||
Uid: i.indexName,
|
||||
PrimaryKey: "GistID",
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
_, _ = i.client.Index(i.indexName).UpdateSettings(&meilisearch.Settings{
|
||||
FilterableAttributes: []string{"GistID", "UserID", "Visibility", "Username", "Extensions", "Languages", "Topics"},
|
||||
SearchableAttributes: []string{"Content", "ContentSplit", "Username", "Title", "Description", "Filenames", "Extensions", "Languages", "Topics"},
|
||||
RankingRules: []string{"words", "typo", "proximity", "attribute", "sort", "exactness"},
|
||||
TypoTolerance: &meilisearch.TypoTolerance{
|
||||
Enabled: true,
|
||||
DisableOnNumbers: true,
|
||||
MinWordSizeForTypos: meilisearch.MinWordSizeForTypos{OneTypo: 4, TwoTypos: 10},
|
||||
},
|
||||
FilterableAttributes: []string{"GistID", "UserID", "Visibility", "Username", "Title", "Filenames", "Extensions", "Languages", "Topics"},
|
||||
DisplayedAttributes: []string{"GistID"},
|
||||
SearchableAttributes: []string{"Content", "Username", "Title", "Filenames", "Extensions", "Languages", "Topics"},
|
||||
RankingRules: []string{"words"},
|
||||
})
|
||||
|
||||
return i.client.Index(i.indexName), nil
|
||||
}
|
||||
|
||||
func (i *MeiliIndexer) Reset() error {
|
||||
if i.client != nil {
|
||||
taskInfo, err := i.client.DeleteIndex(i.indexName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to delete Meilisearch index: %w", err)
|
||||
}
|
||||
_, err = i.client.WaitForTask(taskInfo.TaskUID, 0)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to wait for Meilisearch index deletion: %w", err)
|
||||
}
|
||||
log.Info().Msg("Meilisearch index deleted, re-creating index")
|
||||
}
|
||||
return i.Init()
|
||||
}
|
||||
|
||||
func (i *MeiliIndexer) Close() {
|
||||
if i.client != nil {
|
||||
i.client.Close()
|
||||
@@ -99,21 +80,12 @@ func (i *MeiliIndexer) Close() {
|
||||
i.client = nil
|
||||
}
|
||||
|
||||
type meiliGist struct {
|
||||
Gist
|
||||
ContentSplit string
|
||||
}
|
||||
|
||||
func (i *MeiliIndexer) Add(gist *Gist) error {
|
||||
if gist == nil {
|
||||
return errors.New("failed to add nil gist to index")
|
||||
}
|
||||
doc := &meiliGist{
|
||||
Gist: *gist,
|
||||
ContentSplit: splitCamelCase(gist.Content),
|
||||
}
|
||||
primaryKey := "GistID"
|
||||
_, err := (*atomicIndexer.Load()).(*MeiliIndexer).index.AddDocuments(doc, &meilisearch.DocumentOptions{PrimaryKey: &primaryKey})
|
||||
_, err := (*atomicIndexer.Load()).(*MeiliIndexer).index.AddDocuments(gist, &meilisearch.DocumentOptions{PrimaryKey: &primaryKey})
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -122,14 +94,13 @@ func (i *MeiliIndexer) Remove(gistID uint) error {
|
||||
return err
|
||||
}
|
||||
|
||||
func (i *MeiliIndexer) Search(queryMetadata SearchGistMetadata, userId uint, page int) ([]uint, uint64, map[string]int, error) {
|
||||
func (i *MeiliIndexer) Search(queryStr string, queryMetadata SearchGistMetadata, userId uint, page int) ([]uint, uint64, map[string]int, error) {
|
||||
searchRequest := &meilisearch.SearchRequest{
|
||||
Offset: int64((page - 1) * 10),
|
||||
Limit: 11,
|
||||
AttributesToRetrieve: []string{"GistID", "Languages"},
|
||||
Facets: []string{"Languages"},
|
||||
AttributesToSearchOn: []string{"Content", "ContentSplit"},
|
||||
MatchingStrategy: meilisearch.All,
|
||||
AttributesToSearchOn: []string{"Content"},
|
||||
}
|
||||
|
||||
var filters []string
|
||||
@@ -140,83 +111,23 @@ func (i *MeiliIndexer) Search(queryMetadata SearchGistMetadata, userId uint, pag
|
||||
filters = append(filters, fmt.Sprintf("%s = \"%s\"", field, escapeFilterValue(value)))
|
||||
}
|
||||
}
|
||||
var query string
|
||||
if queryMetadata.All != "" {
|
||||
query = queryMetadata.All
|
||||
searchRequest.AttributesToSearchOn = append(AllSearchFields, "ContentSplit")
|
||||
} else {
|
||||
// Exact-match fields stay as filters
|
||||
addFilter("Username", queryMetadata.Username)
|
||||
if queryMetadata.Extension != "" {
|
||||
ext := queryMetadata.Extension
|
||||
if !strings.HasPrefix(ext, ".") {
|
||||
ext = "." + ext
|
||||
}
|
||||
addFilter("Extensions", ext)
|
||||
}
|
||||
addFilter("Languages", queryMetadata.Language)
|
||||
addFilter("Topics", queryMetadata.Topic)
|
||||
|
||||
if queryMetadata.Default != "" {
|
||||
query = queryMetadata.Default
|
||||
var fields []string
|
||||
for _, f := range strings.Split(config.C.SearchDefault, ",") {
|
||||
f = strings.TrimSpace(f)
|
||||
if f == "all" {
|
||||
fields = AllSearchFields
|
||||
break
|
||||
}
|
||||
if indexField, ok := SearchFieldMap[f]; ok {
|
||||
fields = append(fields, indexField)
|
||||
}
|
||||
}
|
||||
if len(fields) > 0 {
|
||||
for _, f := range fields {
|
||||
if f == "Content" {
|
||||
fields = append(fields, "ContentSplit")
|
||||
break
|
||||
}
|
||||
}
|
||||
searchRequest.AttributesToSearchOn = fields
|
||||
}
|
||||
} else {
|
||||
// Fuzzy-matchable fields become part of the query
|
||||
var queryParts []string
|
||||
var searchFields []string
|
||||
|
||||
if queryMetadata.Content != "" {
|
||||
queryParts = append(queryParts, queryMetadata.Content)
|
||||
searchFields = append(searchFields, "Content", "ContentSplit")
|
||||
}
|
||||
if queryMetadata.Title != "" {
|
||||
queryParts = append(queryParts, queryMetadata.Title)
|
||||
searchFields = append(searchFields, "Title")
|
||||
}
|
||||
if queryMetadata.Description != "" {
|
||||
queryParts = append(queryParts, queryMetadata.Description)
|
||||
searchFields = append(searchFields, "Description")
|
||||
}
|
||||
if queryMetadata.Filename != "" {
|
||||
queryParts = append(queryParts, queryMetadata.Filename)
|
||||
searchFields = append(searchFields, "Filenames")
|
||||
}
|
||||
|
||||
query = strings.Join(queryParts, " ")
|
||||
if len(searchFields) > 0 {
|
||||
searchRequest.AttributesToSearchOn = searchFields
|
||||
}
|
||||
}
|
||||
}
|
||||
addFilter("Username", queryMetadata.Username)
|
||||
addFilter("Title", queryMetadata.Title)
|
||||
addFilter("Filenames", queryMetadata.Filename)
|
||||
addFilter("Extensions", queryMetadata.Extension)
|
||||
addFilter("Languages", queryMetadata.Language)
|
||||
addFilter("Topics", queryMetadata.Topic)
|
||||
|
||||
if len(filters) > 0 {
|
||||
searchRequest.Filter = strings.Join(filters, " AND ")
|
||||
}
|
||||
|
||||
response, err := (*atomicIndexer.Load()).(*MeiliIndexer).index.Search(query, searchRequest)
|
||||
response, err := (*atomicIndexer.Load()).(*MeiliIndexer).index.Search(queryStr, searchRequest)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Failed to search Meilisearch index")
|
||||
return nil, 0, nil, err
|
||||
}
|
||||
|
||||
gistIds := make([]uint, 0, len(response.Hits))
|
||||
for _, hit := range response.Hits {
|
||||
if gistIDRaw, ok := hit["GistID"]; ok {
|
||||
@@ -232,9 +143,7 @@ func (i *MeiliIndexer) Search(queryMetadata SearchGistMetadata, userId uint, pag
|
||||
var facetDist map[string]map[string]int
|
||||
if err := json.Unmarshal(response.FacetDistribution, &facetDist); err == nil {
|
||||
if facets, ok := facetDist["Languages"]; ok {
|
||||
for lang, count := range facets {
|
||||
languageCounts[strings.ToLower(lang)] += count
|
||||
}
|
||||
languageCounts = facets
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -242,30 +151,6 @@ func (i *MeiliIndexer) Search(queryMetadata SearchGistMetadata, userId uint, pag
|
||||
return gistIds, uint64(response.EstimatedTotalHits), languageCounts, nil
|
||||
}
|
||||
|
||||
func splitCamelCase(text string) string {
|
||||
var result strings.Builder
|
||||
runes := []rune(text)
|
||||
for i := 0; i < len(runes); i++ {
|
||||
r := runes[i]
|
||||
if i > 0 {
|
||||
prev := runes[i-1]
|
||||
if unicode.IsUpper(r) {
|
||||
if unicode.IsLower(prev) || unicode.IsDigit(prev) {
|
||||
result.WriteRune(' ')
|
||||
} else if unicode.IsUpper(prev) && i+1 < len(runes) && unicode.IsLower(runes[i+1]) {
|
||||
result.WriteRune(' ')
|
||||
}
|
||||
} else if unicode.IsDigit(r) && !unicode.IsDigit(prev) {
|
||||
result.WriteRune(' ')
|
||||
} else if !unicode.IsDigit(r) && unicode.IsDigit(prev) {
|
||||
result.WriteRune(' ')
|
||||
}
|
||||
}
|
||||
result.WriteRune(r)
|
||||
}
|
||||
return result.String()
|
||||
}
|
||||
|
||||
func escapeFilterValue(value string) string {
|
||||
escaped := strings.ReplaceAll(value, "\\", "\\\\")
|
||||
escaped = strings.ReplaceAll(escaped, "\"", "\\\"")
|
||||
|
||||
@@ -1,88 +0,0 @@
|
||||
package index
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
"github.com/meilisearch/meilisearch-go"
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
// syncMeiliIndexer wraps MeiliIndexer to make Add/Remove synchronous for tests.
|
||||
type syncMeiliIndexer struct {
|
||||
*MeiliIndexer
|
||||
}
|
||||
|
||||
func (s *syncMeiliIndexer) Add(gist *Gist) error {
|
||||
if gist == nil {
|
||||
return fmt.Errorf("failed to add nil gist to index")
|
||||
}
|
||||
doc := &meiliGist{
|
||||
Gist: *gist,
|
||||
ContentSplit: splitCamelCase(gist.Content),
|
||||
}
|
||||
primaryKey := "GistID"
|
||||
taskInfo, err := s.index.AddDocuments(doc, &meilisearch.DocumentOptions{PrimaryKey: &primaryKey})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = s.client.WaitForTask(taskInfo.TaskUID, 0)
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *syncMeiliIndexer) Remove(gistID uint) error {
|
||||
taskInfo, err := s.index.DeleteDocument(strconv.Itoa(int(gistID)), nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = s.client.WaitForTask(taskInfo.TaskUID, 0)
|
||||
return err
|
||||
}
|
||||
|
||||
func setupMeiliIndexer(t *testing.T) (Indexer, func()) {
|
||||
zerolog.SetGlobalLevel(zerolog.Disabled)
|
||||
t.Helper()
|
||||
|
||||
host := os.Getenv("OG_TEST_MEILI_HOST")
|
||||
if host == "" {
|
||||
host = "http://localhost:47700"
|
||||
}
|
||||
apiKey := os.Getenv("OG_TEST_MEILI_API_KEY")
|
||||
|
||||
indexName := fmt.Sprintf("test_%d", os.Getpid())
|
||||
|
||||
inner := NewMeiliIndexer(host, apiKey, indexName)
|
||||
err := inner.Init()
|
||||
if err != nil {
|
||||
t.Skipf("MeiliSearch not available at %s: %v", host, err)
|
||||
}
|
||||
|
||||
wrapped := &syncMeiliIndexer{MeiliIndexer: inner}
|
||||
|
||||
// Store the inner MeiliIndexer in atomicIndexer, because MeiliIndexer.Search
|
||||
// type-asserts the global to *MeiliIndexer.
|
||||
var idx Indexer = inner
|
||||
atomicIndexer.Store(&idx)
|
||||
|
||||
cleanup := func() {
|
||||
atomicIndexer.Store(nil)
|
||||
inner.Reset()
|
||||
inner.Close()
|
||||
}
|
||||
|
||||
return wrapped, cleanup
|
||||
}
|
||||
|
||||
func TestMeiliAddAndSearch(t *testing.T) { testAddAndSearch(t, setupMeiliIndexer) }
|
||||
func TestMeiliAccessControl(t *testing.T) { testAccessControl(t, setupMeiliIndexer) }
|
||||
func TestMeiliMetadataFilters(t *testing.T) { testMetadataFilters(t, setupMeiliIndexer) }
|
||||
func TestMeiliAllFieldSearch(t *testing.T) { testAllFieldSearch(t, setupMeiliIndexer) }
|
||||
func TestMeiliFuzzySearch(t *testing.T) { testFuzzySearch(t, setupMeiliIndexer) }
|
||||
func TestMeiliContentSearch(t *testing.T) { testContentSearch(t, setupMeiliIndexer) }
|
||||
func TestMeiliPagination(t *testing.T) { testPagination(t, setupMeiliIndexer) }
|
||||
func TestMeiliLanguageFacets(t *testing.T) { testLanguageFacets(t, setupMeiliIndexer) }
|
||||
func TestMeiliMetadataOnlySearch(t *testing.T) { testMetadataOnlySearch(t, setupMeiliIndexer) }
|
||||
func TestMeiliTitleFuzzySearch(t *testing.T) { testTitleFuzzySearch(t, setupMeiliIndexer) }
|
||||
func TestMeiliMultiLanguageFacets(t *testing.T) { testMultiLanguageFacets(t, setupMeiliIndexer) }
|
||||
@@ -59,7 +59,7 @@ func validateReservedKeywords(fl validator.FieldLevel) bool {
|
||||
name := fl.Field().String()
|
||||
|
||||
restrictedNames := map[string]struct{}{}
|
||||
for _, restrictedName := range []string{"assets", "register", "login", "logout", "settings", "admin-panel", "all", "search", "init", "healthcheck", "preview", "metrics", "mfa", "webauthn", "oauth"} {
|
||||
for _, restrictedName := range []string{"assets", "register", "login", "logout", "settings", "admin-panel", "all", "search", "init", "healthcheck", "preview", "metrics", "mfa", "webauthn"} {
|
||||
restrictedNames[restrictedName] = struct{}{}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,46 +0,0 @@
|
||||
package admin_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
webtest "github.com/thomiceli/opengist/internal/web/test"
|
||||
)
|
||||
|
||||
func TestAdminActions(t *testing.T) {
|
||||
s := webtest.Setup(t)
|
||||
defer webtest.Teardown(t)
|
||||
urls := []string{
|
||||
"/admin-panel/sync-fs",
|
||||
"/admin-panel/sync-db",
|
||||
"/admin-panel/gc-repos",
|
||||
"/admin-panel/sync-previews",
|
||||
"/admin-panel/reset-hooks",
|
||||
"/admin-panel/index-gists",
|
||||
"/admin-panel/sync-languages",
|
||||
}
|
||||
|
||||
s.Register(t, "thomas")
|
||||
s.Register(t, "nonadmin")
|
||||
|
||||
t.Run("NoUser", func(t *testing.T) {
|
||||
for _, url := range urls {
|
||||
s.Request(t, "POST", url, nil, 404)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("AdminUser", func(t *testing.T) {
|
||||
s.Login(t, "thomas")
|
||||
for _, url := range urls {
|
||||
resp := s.Request(t, "POST", url, nil, 302)
|
||||
require.Equal(t, "/admin-panel", resp.Header.Get("Location"))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("NonAdminUser", func(t *testing.T) {
|
||||
s.Login(t, "nonadmin")
|
||||
for _, url := range urls {
|
||||
s.Request(t, "POST", url, nil, 404)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -1,269 +0,0 @@
|
||||
package admin_test
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/thomiceli/opengist/internal/config"
|
||||
"github.com/thomiceli/opengist/internal/db"
|
||||
"github.com/thomiceli/opengist/internal/git"
|
||||
webtest "github.com/thomiceli/opengist/internal/web/test"
|
||||
)
|
||||
|
||||
func TestAdminPages(t *testing.T) {
|
||||
s := webtest.Setup(t)
|
||||
defer webtest.Teardown(t)
|
||||
|
||||
urls := []string{
|
||||
"/admin-panel",
|
||||
"/admin-panel/users",
|
||||
"/admin-panel/gists",
|
||||
"/admin-panel/invitations",
|
||||
"/admin-panel/configuration",
|
||||
}
|
||||
|
||||
s.Register(t, "thomas")
|
||||
s.Register(t, "nonadmin")
|
||||
|
||||
t.Run("NoUser", func(t *testing.T) {
|
||||
for _, url := range urls {
|
||||
s.Request(t, "GET", url, nil, 404)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("AdminUser", func(t *testing.T) {
|
||||
s.Login(t, "thomas")
|
||||
for _, url := range urls {
|
||||
s.Request(t, "GET", url, nil, 200)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("NonAdminUser", func(t *testing.T) {
|
||||
s.Login(t, "nonadmin")
|
||||
for _, url := range urls {
|
||||
s.Request(t, "GET", url, nil, 404)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestAdminSetConfig(t *testing.T) {
|
||||
s := webtest.Setup(t)
|
||||
defer webtest.Teardown(t)
|
||||
|
||||
settings := []string{
|
||||
db.SettingDisableSignup,
|
||||
db.SettingRequireLogin,
|
||||
db.SettingAllowGistsWithoutLogin,
|
||||
db.SettingDisableLoginForm,
|
||||
db.SettingDisableGravatar,
|
||||
}
|
||||
|
||||
s.Register(t, "thomas")
|
||||
s.Register(t, "nonadmin")
|
||||
|
||||
t.Run("NoUser", func(t *testing.T) {
|
||||
s.Request(t, "PUT", "/admin-panel/set-config", url.Values{"key": {db.SettingDisableSignup}, "value": {"1"}}, 404)
|
||||
})
|
||||
|
||||
t.Run("NonAdminUser", func(t *testing.T) {
|
||||
s.Login(t, "nonadmin")
|
||||
s.Request(t, "PUT", "/admin-panel/set-config", url.Values{"key": {db.SettingDisableSignup}, "value": {"1"}}, 404)
|
||||
})
|
||||
|
||||
t.Run("AdminUser", func(t *testing.T) {
|
||||
s.Login(t, "thomas")
|
||||
|
||||
for _, setting := range settings {
|
||||
val, err := db.GetSetting(setting)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "0", val)
|
||||
|
||||
s.Request(t, "PUT", "/admin-panel/set-config", url.Values{"key": {setting}, "value": {"1"}}, 200)
|
||||
|
||||
val, err = db.GetSetting(setting)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "1", val)
|
||||
|
||||
s.Request(t, "PUT", "/admin-panel/set-config", url.Values{"key": {setting}, "value": {"0"}}, 200)
|
||||
|
||||
val, err = db.GetSetting(setting)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "0", val)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestAdminPagination(t *testing.T) {
|
||||
s := webtest.Setup(t)
|
||||
defer webtest.Teardown(t)
|
||||
|
||||
s.Register(t, "thomas")
|
||||
for i := 0; i < 11; i++ {
|
||||
s.Register(t, "user"+strconv.Itoa(i))
|
||||
}
|
||||
|
||||
t.Run("Pagination", func(t *testing.T) {
|
||||
s.Login(t, "thomas")
|
||||
|
||||
s.Request(t, "GET", "/admin-panel/users", nil, 200)
|
||||
s.Request(t, "GET", "/admin-panel/users?page=2", nil, 200)
|
||||
s.Request(t, "GET", "/admin-panel/users?page=3", nil, 404)
|
||||
s.Request(t, "GET", "/admin-panel/users?page=0", nil, 200)
|
||||
s.Request(t, "GET", "/admin-panel/users?page=-1", nil, 200)
|
||||
s.Request(t, "GET", "/admin-panel/users?page=a", nil, 200)
|
||||
})
|
||||
}
|
||||
|
||||
func TestAdminUserOperations(t *testing.T) {
|
||||
s := webtest.Setup(t)
|
||||
defer webtest.Teardown(t)
|
||||
|
||||
s.Register(t, "thomas")
|
||||
s.Register(t, "nonadmin")
|
||||
|
||||
t.Run("DeleteUser", func(t *testing.T) {
|
||||
s.Login(t, "nonadmin")
|
||||
|
||||
gist1 := db.GistDTO{
|
||||
Title: "gist",
|
||||
VisibilityDTO: db.VisibilityDTO{
|
||||
Private: 0,
|
||||
},
|
||||
Name: []string{"gist1.txt"},
|
||||
Content: []string{"yeah"},
|
||||
Topics: "",
|
||||
}
|
||||
s.Request(t, "POST", "/", gist1, 302)
|
||||
|
||||
_, err := os.Stat(filepath.Join(config.GetHomeDir(), git.ReposDirectory, "nonadmin"))
|
||||
require.NoError(t, err)
|
||||
|
||||
count, err := db.CountAll(db.User{})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, int64(2), count)
|
||||
|
||||
s.Request(t, "POST", "/admin-panel/users/2/delete", nil, 404)
|
||||
|
||||
s.Login(t, "thomas")
|
||||
|
||||
s.Request(t, "POST", "/admin-panel/users/2/delete", nil, 302)
|
||||
|
||||
count, err = db.CountAll(db.User{})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, int64(1), count)
|
||||
|
||||
_, err = os.Stat(filepath.Join(config.GetHomeDir(), git.ReposDirectory, "nonadmin"))
|
||||
require.Error(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestAdminGistOperations(t *testing.T) {
|
||||
s := webtest.Setup(t)
|
||||
defer webtest.Teardown(t)
|
||||
|
||||
s.Register(t, "thomas")
|
||||
s.Register(t, "nonadmin")
|
||||
|
||||
t.Run("DeleteGist", func(t *testing.T) {
|
||||
s.Login(t, "nonadmin")
|
||||
gist1 := db.GistDTO{
|
||||
Title: "gist",
|
||||
VisibilityDTO: db.VisibilityDTO{
|
||||
Private: 0,
|
||||
},
|
||||
Name: []string{"gist1.txt"},
|
||||
Content: []string{"yeah"},
|
||||
Topics: "",
|
||||
}
|
||||
s.Request(t, "POST", "/", gist1, 302)
|
||||
|
||||
count, err := db.CountAll(db.Gist{})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, int64(1), count)
|
||||
|
||||
gist1Db, err := db.GetGistByID("1")
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = os.Stat(filepath.Join(config.GetHomeDir(), git.ReposDirectory, "nonadmin", gist1Db.Identifier()))
|
||||
require.NoError(t, err)
|
||||
|
||||
s.Request(t, "POST", "/admin-panel/gists/1/delete", nil, 404)
|
||||
|
||||
s.Login(t, "thomas")
|
||||
|
||||
s.Request(t, "POST", "/admin-panel/gists/1/delete", nil, 302)
|
||||
|
||||
count, err = db.CountAll(db.Gist{})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, int64(0), count)
|
||||
|
||||
_, err = os.Stat(filepath.Join(config.GetHomeDir(), git.ReposDirectory, "nonadmin", gist1Db.Identifier()))
|
||||
require.Error(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestAdminInvitationOperations(t *testing.T) {
|
||||
s := webtest.Setup(t)
|
||||
defer webtest.Teardown(t)
|
||||
|
||||
s.Register(t, "thomas")
|
||||
s.Register(t, "nonadmin")
|
||||
|
||||
t.Run("Invitation", func(t *testing.T) {
|
||||
s.Login(t, "thomas")
|
||||
|
||||
s.Request(t, "POST", "/admin-panel/invitations", url.Values{
|
||||
"nbMax": {""},
|
||||
"expiredAtUnix": {""},
|
||||
}, 302)
|
||||
invitation1, err := db.GetInvitationByID(1)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, uint(1), invitation1.ID)
|
||||
require.Equal(t, uint(0), invitation1.NbUsed)
|
||||
require.Equal(t, uint(10), invitation1.NbMax)
|
||||
require.InDelta(t, time.Now().Unix()+604800, invitation1.ExpiresAt, 10)
|
||||
|
||||
s.Request(t, "POST", "/admin-panel/invitations", url.Values{
|
||||
"nbMax": {"aa"},
|
||||
"expiredAtUnix": {"1735722000"},
|
||||
}, 302)
|
||||
invitation2, err := db.GetInvitationByID(2)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, invitation2, &db.Invitation{
|
||||
ID: 2,
|
||||
Code: invitation2.Code,
|
||||
ExpiresAt: time.Unix(1735722000, 0).Unix(),
|
||||
NbUsed: 0,
|
||||
NbMax: 10,
|
||||
})
|
||||
|
||||
s.Request(t, "POST", "/admin-panel/invitations", url.Values{
|
||||
"nbMax": {"20"},
|
||||
"expiredAtUnix": {"1735722000"},
|
||||
}, 302)
|
||||
invitation3, err := db.GetInvitationByID(3)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, invitation3, &db.Invitation{
|
||||
ID: 3,
|
||||
Code: invitation3.Code,
|
||||
ExpiresAt: time.Unix(1735722000, 0).Unix(),
|
||||
NbUsed: 0,
|
||||
NbMax: 20,
|
||||
})
|
||||
|
||||
count, err := db.CountAll(db.Invitation{})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, int64(3), count)
|
||||
|
||||
s.Request(t, "POST", "/admin-panel/invitations/1/delete", nil, 302)
|
||||
|
||||
count, err = db.CountAll(db.Invitation{})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, int64(2), count)
|
||||
})
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
package auth_test
|
||||
@@ -4,15 +4,16 @@ import (
|
||||
"crypto/md5"
|
||||
"errors"
|
||||
"fmt"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/thomiceli/opengist/internal/auth/oauth"
|
||||
"github.com/thomiceli/opengist/internal/config"
|
||||
"github.com/thomiceli/opengist/internal/db"
|
||||
"github.com/thomiceli/opengist/internal/i18n"
|
||||
"github.com/thomiceli/opengist/internal/validator"
|
||||
"github.com/thomiceli/opengist/internal/web/context"
|
||||
"golang.org/x/text/cases"
|
||||
"golang.org/x/text/language"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
@@ -47,8 +48,7 @@ func Oauth(ctx *context.Context) error {
|
||||
|
||||
provider, err := oauth.DefineProvider(providerStr, opengistUrl)
|
||||
if err != nil {
|
||||
ctx.AddFlash(ctx.Tr("error.oauth-unsupported"), "error")
|
||||
return ctx.Redirect(302, "/login")
|
||||
return ctx.ErrorRes(400, ctx.Tr("error.oauth-unsupported"), nil)
|
||||
}
|
||||
|
||||
if err = provider.RegisterProvider(); err != nil {
|
||||
@@ -62,37 +62,28 @@ func Oauth(ctx *context.Context) error {
|
||||
func OauthCallback(ctx *context.Context) error {
|
||||
provider, err := oauth.CompleteUserAuth(ctx)
|
||||
if err != nil {
|
||||
ctx.AddFlash(ctx.Tr("auth.oauth.no-provider"), "error")
|
||||
return ctx.Redirect(302, "/login")
|
||||
return ctx.ErrorRes(400, ctx.Tr("error.complete-oauth-login", err.Error()), err)
|
||||
}
|
||||
|
||||
currUser := ctx.User
|
||||
user := provider.GetProviderUser()
|
||||
|
||||
// if user is logged in, link account to user and update its avatar URL
|
||||
if currUser != nil {
|
||||
// check if this OAuth account is already linked to another user
|
||||
if existingUser, err := db.GetUserByProvider(user.UserID, provider.GetProvider()); err == nil && existingUser != nil {
|
||||
ctx.AddFlash(ctx.Tr("flash.auth.oauth-already-linked", config.C.OIDCProviderName), "error")
|
||||
return ctx.RedirectTo("/settings")
|
||||
}
|
||||
|
||||
provider.UpdateUserDB(currUser)
|
||||
|
||||
if err = currUser.Update(); err != nil {
|
||||
return ctx.ErrorRes(500, "Cannot update user "+config.C.OIDCProviderName+" id", err)
|
||||
return ctx.ErrorRes(500, "Cannot update user "+cases.Title(language.English).String(provider.GetProvider())+" id", err)
|
||||
}
|
||||
|
||||
ctx.AddFlash(ctx.Tr("flash.auth.account-linked-oauth", config.C.OIDCProviderName), "success")
|
||||
ctx.AddFlash(ctx.Tr("flash.auth.account-linked-oauth", cases.Title(language.English).String(provider.GetProvider())), "success")
|
||||
return ctx.RedirectTo("/settings")
|
||||
}
|
||||
|
||||
user := provider.GetProviderUser()
|
||||
userDB, err := db.GetUserByProvider(user.UserID, provider.GetProvider())
|
||||
// if user is not in database, redirect to OAuth registration page
|
||||
// if user is not in database, create it
|
||||
if err != nil {
|
||||
if ctx.GetData("DisableSignup") == true {
|
||||
ctx.AddFlash(ctx.Tr("error.signup-disabled"), "error")
|
||||
return ctx.Redirect(302, "/login")
|
||||
return ctx.ErrorRes(403, ctx.Tr("error.signup-disabled"), nil)
|
||||
}
|
||||
|
||||
if !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
@@ -103,174 +94,79 @@ func OauthCallback(ctx *context.Context) error {
|
||||
user.NickName = strings.Split(user.Email, "@")[0]
|
||||
}
|
||||
|
||||
sess := ctx.GetSession()
|
||||
sess.Values["oauthProvider"] = provider.GetProvider()
|
||||
sess.Values["oauthUserID"] = user.UserID
|
||||
sess.Values["oauthNickname"] = user.NickName
|
||||
sess.Values["oauthEmail"] = user.Email
|
||||
sess.Values["oauthAvatarURL"] = user.AvatarURL
|
||||
sess.Values["oauthIsAdmin"] = provider.IsAdmin()
|
||||
|
||||
sess.Options.MaxAge = 10 * 60 // 10 minutes
|
||||
ctx.SaveSession(sess)
|
||||
|
||||
return ctx.RedirectTo("/oauth/register")
|
||||
}
|
||||
|
||||
// promote user to admin from oidc group
|
||||
if !userDB.IsAdmin && provider.IsAdmin() {
|
||||
userDB.IsAdmin = true
|
||||
if err = userDB.Update(); err != nil {
|
||||
return ctx.ErrorRes(500, "Cannot set user admin", err)
|
||||
userDB = &db.User{
|
||||
Username: user.NickName,
|
||||
Email: user.Email,
|
||||
MD5Hash: fmt.Sprintf("%x", md5.Sum([]byte(strings.ToLower(strings.TrimSpace(user.Email))))),
|
||||
}
|
||||
}
|
||||
|
||||
sess := ctx.GetSession()
|
||||
sess.Values["user"] = userDB.ID
|
||||
ctx.SaveSession(sess)
|
||||
ctx.DeleteCsrfCookie()
|
||||
// set provider id and avatar URL
|
||||
provider.UpdateUserDB(userDB)
|
||||
|
||||
return ctx.RedirectTo("/")
|
||||
}
|
||||
if err = userDB.Create(); err != nil {
|
||||
if db.IsUniqueConstraintViolation(err) {
|
||||
ctx.AddFlash(ctx.Tr("flash.auth.username-exists"), "error")
|
||||
return ctx.RedirectTo("/login")
|
||||
}
|
||||
|
||||
func OauthRegister(ctx *context.Context) error {
|
||||
if ctx.GetData("DisableSignup") == true {
|
||||
ctx.AddFlash(ctx.Tr("error.signup-disabled"), "error")
|
||||
return ctx.Redirect(302, "/login")
|
||||
}
|
||||
return ctx.ErrorRes(500, "Cannot create user", err)
|
||||
}
|
||||
|
||||
sess := ctx.GetSession()
|
||||
// if oidc admin group is not configured set first user as admin
|
||||
if config.C.OIDCAdminGroup == "" && userDB.ID == 1 {
|
||||
if err = userDB.SetAdmin(); err != nil {
|
||||
return ctx.ErrorRes(500, "Cannot set user admin", err)
|
||||
}
|
||||
}
|
||||
|
||||
ctx.SetData("title", ctx.TrH("auth.oauth.complete-registration"))
|
||||
ctx.SetData("htmlTitle", ctx.TrH("auth.oauth.complete-registration"))
|
||||
ctx.SetData("oauthProvider", config.C.OIDCProviderName)
|
||||
ctx.SetData("oauthNickname", sess.Values["oauthNickname"])
|
||||
ctx.SetData("oauthEmail", sess.Values["oauthEmail"])
|
||||
ctx.SetData("oauthAvatarURL", sess.Values["oauthAvatarURL"])
|
||||
|
||||
return ctx.Html("oauth_register.html")
|
||||
}
|
||||
|
||||
func ProcessOauthRegister(ctx *context.Context) error {
|
||||
if ctx.GetData("DisableSignup") == true {
|
||||
ctx.AddFlash(ctx.Tr("error.signup-disabled"), "error")
|
||||
return ctx.Redirect(302, "/login")
|
||||
}
|
||||
|
||||
sess := ctx.GetSession()
|
||||
|
||||
providerStr := sess.Values["oauthProvider"].(string)
|
||||
oauthUserID := sess.Values["oauthUserID"].(string)
|
||||
|
||||
setOauthRegisterData := func(dto *db.OAuthRegisterDTO) {
|
||||
ctx.SetData("title", ctx.TrH("auth.oauth.complete-registration"))
|
||||
ctx.SetData("htmlTitle", ctx.TrH("auth.oauth.complete-registration"))
|
||||
ctx.SetData("oauthProvider", config.C.OIDCProviderName)
|
||||
if dto != nil {
|
||||
ctx.SetData("oauthNickname", dto.Username)
|
||||
ctx.SetData("oauthEmail", dto.Email)
|
||||
keys, err := provider.GetProviderUserSSHKeys()
|
||||
if err != nil {
|
||||
ctx.AddFlash(ctx.Tr("flash.auth.user-sshkeys-not-retrievable"), "error")
|
||||
log.Error().Err(err).Msg("Could not get user keys")
|
||||
} else {
|
||||
ctx.SetData("oauthNickname", sess.Values["oauthNickname"])
|
||||
ctx.SetData("oauthEmail", sess.Values["oauthEmail"])
|
||||
}
|
||||
ctx.SetData("oauthAvatarURL", sess.Values["oauthAvatarURL"])
|
||||
}
|
||||
for _, key := range keys {
|
||||
sshKey := db.SSHKey{
|
||||
Title: "Added from " + user.Provider,
|
||||
Content: key,
|
||||
User: *userDB,
|
||||
}
|
||||
|
||||
// Bind and validate form data
|
||||
dto := new(db.OAuthRegisterDTO)
|
||||
if err := ctx.Bind(dto); err != nil {
|
||||
return ctx.ErrorRes(400, ctx.Tr("error.cannot-bind-data"), err)
|
||||
}
|
||||
|
||||
if err := ctx.Validate(dto); err != nil {
|
||||
ctx.AddFlash(validator.ValidationMessages(&err, ctx.GetData("locale").(*i18n.Locale)), "error")
|
||||
setOauthRegisterData(dto)
|
||||
return ctx.Html("oauth_register.html")
|
||||
}
|
||||
|
||||
if exists, err := db.UserExists(dto.Username); err != nil || exists {
|
||||
ctx.AddFlash(ctx.Tr("flash.auth.username-exists"), "error")
|
||||
setOauthRegisterData(dto)
|
||||
return ctx.Html("oauth_register.html")
|
||||
}
|
||||
|
||||
// Check if OAuth account is already linked to another user (race condition protection)
|
||||
if existingUser, err := db.GetUserByProvider(oauthUserID, providerStr); err == nil && existingUser != nil {
|
||||
ctx.AddFlash(ctx.Tr("flash.auth.oauth-already-linked", config.C.OIDCProviderName), "error")
|
||||
setOauthRegisterData(dto)
|
||||
return ctx.Html("oauth_register.html")
|
||||
}
|
||||
|
||||
userDB := &db.User{
|
||||
Username: dto.Username,
|
||||
Email: dto.Email,
|
||||
}
|
||||
if dto.Email != "" {
|
||||
userDB.MD5Hash = fmt.Sprintf("%x", md5.Sum([]byte(strings.ToLower(strings.TrimSpace(dto.Email)))))
|
||||
}
|
||||
|
||||
nickname := ""
|
||||
if n, ok := sess.Values["oauthNickname"].(string); ok {
|
||||
nickname = n
|
||||
}
|
||||
avatarURL := ""
|
||||
if av, ok := sess.Values["oauthAvatarURL"].(string); ok {
|
||||
avatarURL = av
|
||||
}
|
||||
|
||||
callbackProvider, err := oauth.NewCallbackProviderFromSession(providerStr, oauthUserID, nickname, dto.Email, avatarURL)
|
||||
if err != nil {
|
||||
return ctx.ErrorRes(500, "Cannot create provider", err)
|
||||
}
|
||||
callbackProvider.UpdateUserDB(userDB)
|
||||
|
||||
if err := userDB.Create(); err != nil {
|
||||
if db.IsUniqueConstraintViolation(err) {
|
||||
ctx.AddFlash(ctx.Tr("flash.auth.username-exists"), "error")
|
||||
setOauthRegisterData(dto)
|
||||
return ctx.Html("oauth_register.html")
|
||||
}
|
||||
return ctx.ErrorRes(500, "Cannot create user", err)
|
||||
}
|
||||
|
||||
if config.C.OIDCAdminGroup == "" && userDB.ID == 1 {
|
||||
if err := userDB.SetAdmin(); err != nil {
|
||||
return ctx.ErrorRes(500, "Cannot set user admin", err)
|
||||
}
|
||||
}
|
||||
|
||||
if isAdmin, ok := sess.Values["oauthIsAdmin"].(bool); ok && isAdmin {
|
||||
userDB.IsAdmin = true
|
||||
_ = userDB.Update()
|
||||
}
|
||||
|
||||
keys, err := callbackProvider.GetProviderUserSSHKeys()
|
||||
if err != nil {
|
||||
ctx.AddFlash(ctx.Tr("flash.auth.user-sshkeys-not-retrievable"), "error")
|
||||
log.Error().Err(err).Msg("Could not get user keys")
|
||||
} else {
|
||||
for _, key := range keys {
|
||||
sshKey := db.SSHKey{
|
||||
Title: "Added from " + providerStr,
|
||||
Content: key,
|
||||
User: *userDB,
|
||||
}
|
||||
if err = sshKey.Create(); err != nil {
|
||||
ctx.AddFlash(ctx.Tr("flash.auth.user-sshkeys-not-created"), "error")
|
||||
log.Error().Err(err).Msg("Could not create ssh key")
|
||||
if err = sshKey.Create(); err != nil {
|
||||
ctx.AddFlash(ctx.Tr("flash.auth.user-sshkeys-not-created"), "error")
|
||||
log.Error().Err(err).Msg("Could not create ssh key")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
delete(sess.Values, "oauthProvider")
|
||||
delete(sess.Values, "oauthUserID")
|
||||
delete(sess.Values, "oauthNickname")
|
||||
delete(sess.Values, "oauthEmail")
|
||||
delete(sess.Values, "oauthAvatarURL")
|
||||
delete(sess.Values, "oauthIsAdmin")
|
||||
// update is admin status from oidc group
|
||||
if config.C.OIDCAdminGroup != "" {
|
||||
groupClaimName := config.C.OIDCGroupClaimName
|
||||
if groupClaimName == "" {
|
||||
log.Error().Msg("No OIDC group claim name configured")
|
||||
} else if groups, ok := user.RawData[groupClaimName].([]interface{}); ok {
|
||||
var groupNames []string
|
||||
for _, group := range groups {
|
||||
if groupName, ok := group.(string); ok {
|
||||
groupNames = append(groupNames, groupName)
|
||||
}
|
||||
}
|
||||
isOIDCAdmin := slices.Contains(groupNames, config.C.OIDCAdminGroup)
|
||||
log.Debug().Bool("isOIDCAdmin", isOIDCAdmin).Str("user", user.Name).Msg("User is in admin group")
|
||||
|
||||
if userDB.IsAdmin != isOIDCAdmin {
|
||||
userDB.IsAdmin = isOIDCAdmin
|
||||
if err = userDB.Update(); err != nil {
|
||||
return ctx.ErrorRes(500, "Cannot set user admin", err)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
log.Error().Msg("No groups found in user data")
|
||||
}
|
||||
}
|
||||
|
||||
sess := ctx.GetSession()
|
||||
sess.Values["user"] = userDB.ID
|
||||
sess.Options.MaxAge = 60 * 60 * 24 * 365 // 1 year
|
||||
ctx.SaveSession(sess)
|
||||
ctx.DeleteCsrfCookie()
|
||||
|
||||
@@ -288,10 +184,10 @@ func OauthUnlink(ctx *context.Context) error {
|
||||
|
||||
if provider.UserHasProvider(currUser) {
|
||||
if err := currUser.DeleteProviderID(providerStr); err != nil {
|
||||
return ctx.ErrorRes(500, "Cannot unlink account from "+config.C.OIDCProviderName, err)
|
||||
return ctx.ErrorRes(500, "Cannot unlink account from "+cases.Title(language.English).String(providerStr), err)
|
||||
}
|
||||
|
||||
ctx.AddFlash(ctx.Tr("flash.auth.account-unlinked-oauth", config.C.OIDCProviderName), "success")
|
||||
ctx.AddFlash(ctx.Tr("flash.auth.account-unlinked-oauth", cases.Title(language.English).String(providerStr)), "success")
|
||||
return ctx.RedirectTo("/settings")
|
||||
}
|
||||
|
||||
|
||||
@@ -1,221 +0,0 @@
|
||||
package auth_test
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"testing"
|
||||
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/thomiceli/opengist/internal/db"
|
||||
webtest "github.com/thomiceli/opengist/internal/web/test"
|
||||
)
|
||||
|
||||
func TestRegisterPage(t *testing.T) {
|
||||
s := webtest.Setup(t)
|
||||
defer webtest.Teardown(t)
|
||||
s.Register(t, "thomas")
|
||||
|
||||
t.Run("Form", func(t *testing.T) {
|
||||
s.Request(t, "GET", "/register", nil, 200)
|
||||
s.TestCtxData(t, echo.Map{
|
||||
"isLoginPage": false,
|
||||
"disableForm": false,
|
||||
"disableSignup": false,
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("FormDisabled", func(t *testing.T) {
|
||||
s.Login(t, "thomas")
|
||||
s.Request(t, "PUT", "/admin-panel/set-config", url.Values{"key": {"disable-signup"}, "value": {"1"}}, 200)
|
||||
s.Logout()
|
||||
|
||||
s.Request(t, "GET", "/register", nil, 200)
|
||||
s.TestCtxData(t, echo.Map{
|
||||
"disableSignup": true,
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("FormDisabledWithInviteCode", func(t *testing.T) {
|
||||
s.Login(t, "thomas")
|
||||
s.Request(t, "PUT", "/admin-panel/set-config", url.Values{"key": {"disable-signup"}, "value": {"1"}}, 200)
|
||||
|
||||
s.Request(t, "POST", "/admin-panel/invitations", url.Values{
|
||||
"nbMax": {"10"},
|
||||
"expiredAtUnix": {""},
|
||||
}, 302)
|
||||
|
||||
invitation, err := db.GetInvitationByID(1)
|
||||
require.NoError(t, err)
|
||||
|
||||
s.Logout()
|
||||
|
||||
s.Request(t, "GET", "/register", nil, 200)
|
||||
s.TestCtxData(t, echo.Map{
|
||||
"disableSignup": true,
|
||||
})
|
||||
s.Request(t, "GET", "/register?code="+invitation.Code, nil, 200)
|
||||
s.TestCtxData(t, echo.Map{
|
||||
"disableSignup": false,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestProcessRegister(t *testing.T) {
|
||||
s := webtest.Setup(t)
|
||||
defer webtest.Teardown(t)
|
||||
|
||||
s.Register(t, "thomas")
|
||||
s.Register(t, "alice")
|
||||
|
||||
t.Run("Register", func(t *testing.T) {
|
||||
user, err := db.GetUserByUsername("thomas")
|
||||
require.NoError(t, err)
|
||||
require.True(t, user.IsAdmin)
|
||||
s.Logout()
|
||||
|
||||
s.Request(t, "POST", "/register", db.UserDTO{Username: "seconduser", Password: "password123"}, 302)
|
||||
user, err = db.GetUserByUsername("seconduser")
|
||||
require.NoError(t, err)
|
||||
require.False(t, user.IsAdmin)
|
||||
s.Logout()
|
||||
})
|
||||
|
||||
t.Run("DuplicateUsername", func(t *testing.T) {
|
||||
s.Request(t, "POST", "/register", db.UserDTO{Username: "useraaa", Password: "password123"}, 302)
|
||||
s.Logout()
|
||||
s.Request(t, "POST", "/register", db.UserDTO{Username: "useraaa", Password: "password456"}, 200)
|
||||
s.Logout()
|
||||
})
|
||||
|
||||
t.Run("InvalidUsername", func(t *testing.T) {
|
||||
s.Request(t, "POST", "/register", db.UserDTO{Username: "", Password: "password123"}, 200)
|
||||
s.Request(t, "POST", "/register", db.UserDTO{Username: "aze@", Password: "password123"}, 200)
|
||||
})
|
||||
|
||||
t.Run("EmptyPassword", func(t *testing.T) {
|
||||
s.Request(t, "POST", "/register", db.UserDTO{Username: "newuser", Password: ""}, 200)
|
||||
})
|
||||
|
||||
t.Run("RegisterDisabled", func(t *testing.T) {
|
||||
s.Login(t, "thomas")
|
||||
s.Request(t, "PUT", "/admin-panel/set-config", url.Values{"key": {"disable-signup"}, "value": {"1"}}, 200)
|
||||
s.Logout()
|
||||
|
||||
s.Request(t, "POST", "/register", db.UserDTO{Username: "blocked", Password: "password123"}, 403)
|
||||
|
||||
exists, err := db.UserExists("blocked")
|
||||
require.NoError(t, err)
|
||||
require.False(t, exists)
|
||||
})
|
||||
|
||||
t.Run("RegisterWithInvitationCode", func(t *testing.T) {
|
||||
s.Login(t, "thomas")
|
||||
s.Request(t, "PUT", "/admin-panel/set-config", url.Values{"key": {"disable-signup"}, "value": {"1"}}, 200)
|
||||
s.Request(t, "POST", "/admin-panel/invitations", url.Values{
|
||||
"nbMax": {"10"},
|
||||
"expiredAtUnix": {""},
|
||||
}, 302)
|
||||
s.Logout()
|
||||
|
||||
invitations, err := db.GetAllInvitations()
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, invitations)
|
||||
invitation := invitations[len(invitations)-1]
|
||||
|
||||
s.Logout()
|
||||
|
||||
s.Request(t, "POST", "/register?code="+invitation.Code, db.UserDTO{Username: "inviteduser", Password: "password123"}, 302)
|
||||
|
||||
user, err := db.GetUserByUsername("inviteduser")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "inviteduser", user.Username)
|
||||
|
||||
updatedInvitation, err := db.GetInvitationByID(invitation.ID)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, uint(1), updatedInvitation.NbUsed)
|
||||
})
|
||||
}
|
||||
|
||||
func TestLoginPage(t *testing.T) {
|
||||
s := webtest.Setup(t)
|
||||
defer webtest.Teardown(t)
|
||||
s.Register(t, "thomas")
|
||||
|
||||
t.Run("Form", func(t *testing.T) {
|
||||
s.Request(t, "GET", "/login", nil, 200)
|
||||
s.TestCtxData(t, echo.Map{
|
||||
"isLoginPage": true,
|
||||
"disableForm": false,
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("FormDisabled", func(t *testing.T) {
|
||||
s.Login(t, "thomas")
|
||||
s.Request(t, "PUT", "/admin-panel/set-config", url.Values{"key": {"disable-login-form"}, "value": {"1"}}, 200)
|
||||
s.Logout()
|
||||
|
||||
s.Request(t, "GET", "/login", nil, 200)
|
||||
s.TestCtxData(t, echo.Map{
|
||||
"disableForm": true,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestProcessLogin(t *testing.T) {
|
||||
s := webtest.Setup(t)
|
||||
defer webtest.Teardown(t)
|
||||
|
||||
s.Register(t, "thomas")
|
||||
|
||||
t.Run("ValidCredentials", func(t *testing.T) {
|
||||
resp := s.Request(t, "POST", "/login", db.UserDTO{Username: "thomas", Password: "thomas"}, 302)
|
||||
require.Equal(t, "/", resp.Header.Get("Location"))
|
||||
require.NotEmpty(t, s.SessionCookie)
|
||||
require.Equal(t, "thomas", s.User().Username)
|
||||
|
||||
s.Logout()
|
||||
})
|
||||
|
||||
t.Run("InvalidPassword", func(t *testing.T) {
|
||||
resp := s.Request(t, "POST", "/login", db.UserDTO{Username: "thomas", Password: "wrongpassword"}, 302)
|
||||
require.Equal(t, "/login", resp.Header.Get("Location"))
|
||||
require.Nil(t, s.User())
|
||||
})
|
||||
|
||||
t.Run("NonExistentUser", func(t *testing.T) {
|
||||
resp := s.Request(t, "POST", "/login", db.UserDTO{Username: "nonexistent", Password: "password"}, 302)
|
||||
require.Equal(t, "/login", resp.Header.Get("Location"))
|
||||
require.Nil(t, s.User())
|
||||
})
|
||||
|
||||
t.Run("EmptyCredentials", func(t *testing.T) {
|
||||
s.Request(t, "POST", "/login", db.UserDTO{Username: "", Password: ""}, 302)
|
||||
require.Nil(t, s.User())
|
||||
})
|
||||
|
||||
t.Run("LoginFormDisabled", func(t *testing.T) {
|
||||
s.Login(t, "thomas")
|
||||
s.Request(t, "PUT", "/admin-panel/set-config", url.Values{"key": {"disable-login-form"}, "value": {"1"}}, 200)
|
||||
s.Logout()
|
||||
|
||||
s.Request(t, "POST", "/login", db.UserDTO{Username: "thomas", Password: "thomas"}, 403)
|
||||
require.Nil(t, s.User())
|
||||
})
|
||||
}
|
||||
|
||||
func TestLogout(t *testing.T) {
|
||||
s := webtest.Setup(t)
|
||||
defer webtest.Teardown(t)
|
||||
|
||||
s.Register(t, "thomas")
|
||||
|
||||
t.Run("LogoutRedirects", func(t *testing.T) {
|
||||
s.Login(t, "thomas")
|
||||
require.Equal(t, "thomas", s.User().Username)
|
||||
|
||||
resp := s.Request(t, "GET", "/logout", nil, 302)
|
||||
require.Equal(t, "/all", resp.Header.Get("Location"))
|
||||
require.Nil(t, s.User())
|
||||
s.Request(t, "GET", "/", nil, 302)
|
||||
})
|
||||
}
|
||||
@@ -164,18 +164,6 @@ func AllGists(ctx *context.Context) error {
|
||||
return ctx.Html("all.html")
|
||||
}
|
||||
|
||||
// Search handles the search page for gists.
|
||||
//
|
||||
// It takes a query parameter "q" which is a search query in the format:
|
||||
// "user:username title:title description:description filename:filename language:language topic:topic"
|
||||
//
|
||||
// It also takes a page parameter "page" which is the page number to display.
|
||||
//
|
||||
// It returns an error if the search query is invalid or if the page number is invalid.
|
||||
//
|
||||
// It returns the search results as a list of rendered gists, along with the total number of results, the languages found, and the search query.
|
||||
//
|
||||
// The search results are paginated, with 10 results per page.
|
||||
func Search(ctx *context.Context) error {
|
||||
var err error
|
||||
|
||||
@@ -183,7 +171,7 @@ func Search(ctx *context.Context) error {
|
||||
Query: ctx.QueryParam("q"),
|
||||
}
|
||||
|
||||
metadata := handlers.ParseSearchQueryStr(ctx.QueryParam("q"))
|
||||
content, meta := handlers.ParseSearchQueryStr(ctx.QueryParam("q"))
|
||||
pageInt := handlers.GetPage(ctx)
|
||||
|
||||
var currentUserId uint
|
||||
@@ -194,18 +182,14 @@ func Search(ctx *context.Context) error {
|
||||
currentUserId = 0
|
||||
}
|
||||
|
||||
// Search gists in the index and fetch the gists IDs from the database
|
||||
gistsIds, nbHits, langs, err := index.SearchGists(index.SearchGistMetadata{
|
||||
Username: metadata["user"],
|
||||
Title: metadata["title"],
|
||||
Description: metadata["description"],
|
||||
Filename: metadata["filename"],
|
||||
Extension: metadata["extension"],
|
||||
Language: metadata["language"],
|
||||
Topic: metadata["topic"],
|
||||
Content: metadata["content"],
|
||||
All: metadata["all"],
|
||||
Default: metadata["default"],
|
||||
gistsIds, nbHits, langs, err := index.SearchGists(content, index.SearchGistMetadata{
|
||||
Username: meta["user"],
|
||||
Title: meta["title"],
|
||||
Filename: meta["filename"],
|
||||
Extension: meta["extension"],
|
||||
Language: meta["language"],
|
||||
Topic: meta["topic"],
|
||||
All: meta["all"],
|
||||
}, currentUserId, pageInt)
|
||||
if err != nil {
|
||||
return ctx.ErrorRes(500, "Error searching gists", err)
|
||||
|
||||
@@ -24,6 +24,11 @@ func Create(ctx *context.Context) error {
|
||||
func ProcessCreate(ctx *context.Context) error {
|
||||
isCreate := ctx.Request().URL.Path == "/"
|
||||
|
||||
err := ctx.Request().ParseForm()
|
||||
if err != nil {
|
||||
return ctx.ErrorRes(400, ctx.Tr("error.bad-request"), err)
|
||||
}
|
||||
|
||||
dto := new(db.GistDTO)
|
||||
var gist *db.Gist
|
||||
|
||||
@@ -34,24 +39,25 @@ func ProcessCreate(ctx *context.Context) error {
|
||||
ctx.SetData("htmlTitle", ctx.TrH("gist.edit.edit-gist", gist.Title))
|
||||
}
|
||||
|
||||
err := ctx.Bind(dto)
|
||||
if err != nil {
|
||||
if err := ctx.Bind(dto); err != nil {
|
||||
return ctx.ErrorRes(400, ctx.Tr("error.cannot-bind-data"), err)
|
||||
}
|
||||
|
||||
dto.Files = make([]db.FileDTO, 0)
|
||||
fileCounter := 0
|
||||
|
||||
names := dto.Name
|
||||
contents := dto.Content
|
||||
names := ctx.Request().PostForm["name"]
|
||||
contents := ctx.Request().PostForm["content"]
|
||||
|
||||
// Process files from text editors
|
||||
for i, content := range contents {
|
||||
if content == "" {
|
||||
continue
|
||||
}
|
||||
name := git.CleanTreePathName(names[i])
|
||||
name := names[i]
|
||||
if name == "" {
|
||||
name = "gistfile" + strconv.Itoa(len(dto.Files)+1) + ".txt"
|
||||
fileCounter += 1
|
||||
name = "gistfile" + strconv.Itoa(fileCounter) + ".txt"
|
||||
}
|
||||
|
||||
escapedValue, err := url.PathUnescape(content)
|
||||
@@ -66,26 +72,18 @@ func ProcessCreate(ctx *context.Context) error {
|
||||
}
|
||||
|
||||
// Process uploaded files from UUID arrays
|
||||
fileUUIDs := dto.UploadedFilesUUID
|
||||
fileFilenames := dto.UploadedFilesNames
|
||||
fileUUIDs := ctx.Request().PostForm["uploadedfile_uuid"]
|
||||
fileFilenames := ctx.Request().PostForm["uploadedfile_filename"]
|
||||
if len(fileUUIDs) == len(fileFilenames) {
|
||||
for i, fileUUID := range fileUUIDs {
|
||||
if !uuidRegex.MatchString(filepath.Base(fileUUID)) {
|
||||
continue
|
||||
}
|
||||
filePath := filepath.Join(config.GetHomeDir(), "uploads", fileUUID)
|
||||
filePath := filepath.Join(filepath.Join(config.GetHomeDir(), "uploads"), fileUUID)
|
||||
|
||||
if _, err := os.Stat(filePath); err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
name := git.CleanTreePathName(fileFilenames[i])
|
||||
if name == "" {
|
||||
name = "gistfile" + strconv.Itoa(len(dto.Files)+1) + ".txt"
|
||||
}
|
||||
|
||||
dto.Files = append(dto.Files, db.FileDTO{
|
||||
Filename: name,
|
||||
Filename: fileFilenames[i],
|
||||
SourcePath: filePath,
|
||||
Content: "", // Empty since we're using SourcePath
|
||||
})
|
||||
@@ -93,11 +91,11 @@ func ProcessCreate(ctx *context.Context) error {
|
||||
}
|
||||
|
||||
// Process binary file operations (edit mode)
|
||||
binaryOldNames := dto.BinaryFileOldName
|
||||
binaryNewNames := dto.BinaryFileNewName
|
||||
binaryOldNames := ctx.Request().PostForm["binary_old_name"]
|
||||
binaryNewNames := ctx.Request().PostForm["binary_new_name"]
|
||||
if len(binaryOldNames) == len(binaryNewNames) {
|
||||
for i, oldName := range binaryOldNames {
|
||||
newName := git.CleanTreePathName(binaryNewNames[i])
|
||||
newName := binaryNewNames[i]
|
||||
|
||||
if newName == "" { // deletion
|
||||
continue
|
||||
|
||||
@@ -1,519 +0,0 @@
|
||||
package gist_test
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/thomiceli/opengist/internal/config"
|
||||
"github.com/thomiceli/opengist/internal/db"
|
||||
"github.com/thomiceli/opengist/internal/git"
|
||||
webtest "github.com/thomiceli/opengist/internal/web/test"
|
||||
)
|
||||
|
||||
// Helper function to extract username and gist identifier from redirect URL
|
||||
func getGistInfoFromRedirect(resp *http.Response) (username, identifier string) {
|
||||
location := resp.Header.Get("Location")
|
||||
// Location format: /{username}/{identifier}
|
||||
parts := strings.Split(strings.TrimPrefix(location, "/"), "/")
|
||||
if len(parts) >= 2 {
|
||||
return parts[0], parts[1]
|
||||
}
|
||||
return "", ""
|
||||
}
|
||||
|
||||
func verifyGistCreation(t *testing.T, gist *db.Gist, username, identifier string) {
|
||||
require.NotNil(t, gist)
|
||||
require.Equal(t, username, gist.User.Username)
|
||||
require.Equal(t, identifier, gist.Identifier())
|
||||
require.NotEmpty(t, gist.Uuid)
|
||||
require.Greater(t, gist.NbFiles, 0)
|
||||
|
||||
gistPath := filepath.Join(config.GetHomeDir(), git.ReposDirectory, username, gist.Uuid)
|
||||
_, err := os.Stat(gistPath)
|
||||
require.NoError(t, err, "Gist repository should exist on filesystem at %s", gistPath)
|
||||
}
|
||||
|
||||
func TestGistCreationPage(t *testing.T) {
|
||||
s := webtest.Setup(t)
|
||||
defer webtest.Teardown(t)
|
||||
|
||||
s.Register(t, "thomas")
|
||||
|
||||
t.Run("NoAuth", func(t *testing.T) {
|
||||
s.Request(t, "GET", "/", nil, 302)
|
||||
})
|
||||
|
||||
t.Run("Authenticated", func(t *testing.T) {
|
||||
s.Login(t, "thomas")
|
||||
s.Request(t, "GET", "/", nil, 200)
|
||||
})
|
||||
}
|
||||
|
||||
func TestGistCreation(t *testing.T) {
|
||||
s := webtest.Setup(t)
|
||||
defer webtest.Teardown(t)
|
||||
|
||||
s.Register(t, "thomas")
|
||||
|
||||
t.Run("NoAuth", func(t *testing.T) {
|
||||
s.Request(t, "POST", "/", url.Values{
|
||||
"title": {"Test Gist"},
|
||||
"name": {"test.txt"},
|
||||
"content": {"hello world"},
|
||||
}, 302) // Redirects to login
|
||||
|
||||
// Verify no gist was created
|
||||
count, err := db.CountAll(db.Gist{})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, int64(0), count)
|
||||
})
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
data url.Values
|
||||
expectedCode int
|
||||
expectGistCreated bool
|
||||
expectedTitle string
|
||||
expectedDescription string
|
||||
expectedURL string
|
||||
expectedTopics string // Expected topics string
|
||||
expectedNbFiles int
|
||||
expectedVisibility db.Visibility
|
||||
expectedFileNames []string // Expected filenames in the gist
|
||||
expectedFileContents map[string]string // Expected content for each file (filename -> content)
|
||||
}{
|
||||
{
|
||||
name: "NoFiles",
|
||||
data: url.Values{
|
||||
"title": {"Test Gist"},
|
||||
},
|
||||
expectedCode: 400,
|
||||
expectGistCreated: false,
|
||||
},
|
||||
{
|
||||
name: "EmptyContent",
|
||||
data: url.Values{
|
||||
"title": {"Test Gist"},
|
||||
"name": {"test.txt"},
|
||||
"content": {""},
|
||||
},
|
||||
expectedCode: 400,
|
||||
expectGistCreated: false,
|
||||
},
|
||||
{
|
||||
name: "TitleTooLong",
|
||||
data: url.Values{
|
||||
"title": {strings.Repeat("a", 251)}, // Max is 250
|
||||
"name": {"test.txt"},
|
||||
"content": {"hello"},
|
||||
},
|
||||
expectedCode: 400,
|
||||
expectGistCreated: false,
|
||||
},
|
||||
{
|
||||
name: "DescriptionTooLong",
|
||||
data: url.Values{
|
||||
"title": {"Test Gist"},
|
||||
"description": {strings.Repeat("a", 1001)}, // Max is 1000
|
||||
"name": {"test.txt"},
|
||||
"content": {"hello"},
|
||||
},
|
||||
expectedCode: 400,
|
||||
expectGistCreated: false,
|
||||
},
|
||||
{
|
||||
name: "URLTooLong",
|
||||
data: url.Values{
|
||||
"title": {"Test Gist"},
|
||||
"url": {strings.Repeat("a", 33)}, // Max is 32
|
||||
"name": {"test.txt"},
|
||||
"content": {"hello"},
|
||||
},
|
||||
expectedCode: 400,
|
||||
expectGistCreated: false,
|
||||
},
|
||||
{
|
||||
name: "URLInvalidCharacters",
|
||||
data: url.Values{
|
||||
"title": {"Test Gist"},
|
||||
"url": {"invalid@url#here"}, // Only alphanumeric and dashes allowed
|
||||
"name": {"test.txt"},
|
||||
"content": {"hello"},
|
||||
},
|
||||
expectedCode: 400,
|
||||
expectGistCreated: false,
|
||||
},
|
||||
{
|
||||
name: "InvalidVisibility",
|
||||
data: url.Values{
|
||||
"title": {"Test Gist"},
|
||||
"name": {"test.txt"},
|
||||
"content": {"hello"},
|
||||
"private": {"3"}, // Valid values are 0, 1, 2
|
||||
},
|
||||
expectedCode: 400,
|
||||
expectGistCreated: false,
|
||||
},
|
||||
{
|
||||
name: "Valid",
|
||||
data: url.Values{
|
||||
"title": {"My Test Gist"},
|
||||
"name": {"test.txt"},
|
||||
"url": {"my-custom-url-123"}, // Alphanumeric + dashes should be valid
|
||||
"content": {"hello world"},
|
||||
"private": {"0"},
|
||||
},
|
||||
expectedCode: 302,
|
||||
expectGistCreated: true,
|
||||
expectedTitle: "My Test Gist",
|
||||
expectedNbFiles: 1,
|
||||
expectedVisibility: db.PublicVisibility,
|
||||
expectedFileNames: []string{"test.txt"},
|
||||
expectedFileContents: map[string]string{
|
||||
"test.txt": "hello world",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "AutoNamedFile",
|
||||
data: url.Values{
|
||||
"title": {"Auto Named"},
|
||||
"name": {""},
|
||||
"content": {"content without name"},
|
||||
"private": {"0"},
|
||||
},
|
||||
expectedCode: 302,
|
||||
expectGistCreated: true,
|
||||
expectedTitle: "Auto Named",
|
||||
expectedNbFiles: 1,
|
||||
expectedVisibility: db.PublicVisibility,
|
||||
expectedFileNames: []string{"gistfile1.txt"},
|
||||
expectedFileContents: map[string]string{
|
||||
"gistfile1.txt": "content without name",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "MultipleFiles",
|
||||
data: url.Values{
|
||||
"title": {"Multi File Gist"},
|
||||
"name": []string{"", "file2.md"},
|
||||
"content": []string{"content 1", "content 2"},
|
||||
"private": {"0"},
|
||||
},
|
||||
expectedCode: 302,
|
||||
expectGistCreated: true,
|
||||
expectedTitle: "Multi File Gist",
|
||||
expectedNbFiles: 2,
|
||||
expectedVisibility: db.PublicVisibility,
|
||||
expectedFileNames: []string{"gistfile1.txt", "file2.md"},
|
||||
expectedFileContents: map[string]string{
|
||||
"gistfile1.txt": "content 1",
|
||||
"file2.md": "content 2",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "NoTitle",
|
||||
data: url.Values{
|
||||
"name": {"readme.md"},
|
||||
"content": {"# README"},
|
||||
"private": {"0"},
|
||||
},
|
||||
expectedCode: 302,
|
||||
expectGistCreated: true,
|
||||
expectedTitle: "readme.md",
|
||||
expectedNbFiles: 1,
|
||||
expectedVisibility: db.PublicVisibility,
|
||||
expectedFileNames: []string{"readme.md"},
|
||||
expectedFileContents: map[string]string{
|
||||
"readme.md": "# README",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Unlisted",
|
||||
data: url.Values{
|
||||
"title": {"Unlisted Gist"},
|
||||
"name": {"secret.txt"},
|
||||
"content": {"secret content"},
|
||||
"private": {"1"},
|
||||
},
|
||||
expectedCode: 302,
|
||||
expectGistCreated: true,
|
||||
expectedTitle: "Unlisted Gist",
|
||||
expectedNbFiles: 1,
|
||||
expectedVisibility: db.UnlistedVisibility,
|
||||
expectedFileNames: []string{"secret.txt"},
|
||||
expectedFileContents: map[string]string{
|
||||
"secret.txt": "secret content",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Private",
|
||||
data: url.Values{
|
||||
"title": {"Private Gist"},
|
||||
"name": {"secret.txt"},
|
||||
"content": {"secret content"},
|
||||
"private": {"2"},
|
||||
},
|
||||
expectedCode: 302,
|
||||
expectGistCreated: true,
|
||||
expectedTitle: "Private Gist",
|
||||
expectedNbFiles: 1,
|
||||
expectedVisibility: db.PrivateVisibility,
|
||||
expectedFileNames: []string{"secret.txt"},
|
||||
expectedFileContents: map[string]string{
|
||||
"secret.txt": "secret content",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Topics",
|
||||
data: url.Values{
|
||||
"title": {"Gist With Topics"},
|
||||
"name": {"test.txt"},
|
||||
"content": {"hello"},
|
||||
"topics": {"golang testing webdev"},
|
||||
},
|
||||
expectedCode: 302,
|
||||
expectGistCreated: true,
|
||||
expectedTitle: "Gist With Topics",
|
||||
expectedTopics: "golang,testing,webdev",
|
||||
expectedNbFiles: 1,
|
||||
expectedVisibility: db.PublicVisibility,
|
||||
expectedFileNames: []string{"test.txt"},
|
||||
expectedFileContents: map[string]string{
|
||||
"test.txt": "hello",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "TopicsTooMany",
|
||||
data: url.Values{
|
||||
"title": {"Test"},
|
||||
"name": {"test.txt"},
|
||||
"content": {"hello"},
|
||||
"topics": {"topic1 topic2 topic3 topic4 topic5 topic6 topic7 topic8 topic9 topic10 topic11"},
|
||||
},
|
||||
expectedCode: 400,
|
||||
expectGistCreated: false,
|
||||
},
|
||||
{
|
||||
name: "TopicTooLong",
|
||||
data: url.Values{
|
||||
"title": {"Test"},
|
||||
"name": {"test.txt"},
|
||||
"content": {"hello"},
|
||||
"topics": {strings.Repeat("a", 51)}, // 51 chars - exceeds max of 50
|
||||
},
|
||||
expectedCode: 400,
|
||||
expectGistCreated: false,
|
||||
},
|
||||
{
|
||||
name: "TopicInvalidCharacters",
|
||||
data: url.Values{
|
||||
"title": {"Test"},
|
||||
"name": {"test.txt"},
|
||||
"content": {"hello"},
|
||||
"topics": {"topic@name topic.name"},
|
||||
},
|
||||
expectedCode: 400,
|
||||
expectGistCreated: false,
|
||||
},
|
||||
{
|
||||
name: "TopicUnicode",
|
||||
data: url.Values{
|
||||
"title": {"Unicode Topics"},
|
||||
"name": {"test.txt"},
|
||||
"content": {"hello"},
|
||||
"topics": {"编程 тест"},
|
||||
},
|
||||
expectedCode: 302,
|
||||
expectGistCreated: true,
|
||||
expectedTitle: "Unicode Topics",
|
||||
expectedTopics: "编程,тест",
|
||||
expectedNbFiles: 1,
|
||||
expectedVisibility: db.PublicVisibility,
|
||||
expectedFileNames: []string{"test.txt"},
|
||||
},
|
||||
{
|
||||
name: "DuplicateFileNames",
|
||||
data: url.Values{
|
||||
"title": {"Duplicate Files"},
|
||||
"name": []string{"test.txt", "test.txt"},
|
||||
"content": []string{"content1", "content2"},
|
||||
},
|
||||
expectedCode: 302,
|
||||
expectGistCreated: true,
|
||||
expectedTitle: "Duplicate Files",
|
||||
expectedNbFiles: 1,
|
||||
expectedVisibility: db.PublicVisibility,
|
||||
expectedFileNames: []string{"test.txt"},
|
||||
expectedFileContents: map[string]string{
|
||||
"test.txt": "content2",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "FileNameTooLong",
|
||||
data: url.Values{
|
||||
"title": {"Too Long Filename"},
|
||||
"name": {strings.Repeat("a", 256) + ".txt"}, // 260 total - exceeds 255
|
||||
"content": {"hello"},
|
||||
},
|
||||
expectedCode: 400,
|
||||
expectGistCreated: false,
|
||||
},
|
||||
{
|
||||
name: "FileNameWithUnicode",
|
||||
data: url.Values{
|
||||
"title": {"Unicode Filename"},
|
||||
"name": {"文件.txt"},
|
||||
"content": {"hello world"},
|
||||
},
|
||||
expectedCode: 302,
|
||||
expectGistCreated: true,
|
||||
expectedTitle: "Unicode Filename",
|
||||
expectedNbFiles: 1,
|
||||
expectedVisibility: db.PublicVisibility,
|
||||
expectedFileNames: []string{"文件.txt"},
|
||||
expectedFileContents: map[string]string{
|
||||
"文件.txt": "hello world",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "FileNamePathTraversal",
|
||||
data: url.Values{
|
||||
"title": {"Path Traversal"},
|
||||
"name": {"../../../etc/passwd"},
|
||||
"content": {"malicious"},
|
||||
},
|
||||
expectedCode: 302,
|
||||
expectGistCreated: true,
|
||||
expectedTitle: "Path Traversal",
|
||||
expectedNbFiles: 1,
|
||||
expectedVisibility: db.PublicVisibility,
|
||||
expectedFileNames: []string{"passwd"},
|
||||
expectedFileContents: map[string]string{
|
||||
"passwd": "malicious",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "EmptyAndValidContent",
|
||||
data: url.Values{
|
||||
"title": {"Mixed Content"},
|
||||
"name": []string{"empty.txt", "valid.txt", "also-empty.txt"},
|
||||
"content": []string{"", "valid content", ""},
|
||||
},
|
||||
expectedCode: 302,
|
||||
expectGistCreated: true,
|
||||
expectedTitle: "Mixed Content",
|
||||
expectedNbFiles: 1,
|
||||
expectedVisibility: db.PublicVisibility,
|
||||
expectedFileNames: []string{"valid.txt"},
|
||||
expectedFileContents: map[string]string{
|
||||
"valid.txt": "valid content",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "ContentWithSpecialCharacters",
|
||||
data: url.Values{
|
||||
"title": {"Special Chars"},
|
||||
"name": {"special.txt"},
|
||||
"content": {"Line1\nLine2\tTabbed\x00NullByte😀Emoji"},
|
||||
},
|
||||
expectedCode: 302,
|
||||
expectGistCreated: true,
|
||||
expectedTitle: "Special Chars",
|
||||
expectedNbFiles: 1,
|
||||
expectedVisibility: db.PublicVisibility,
|
||||
expectedFileNames: []string{"special.txt"},
|
||||
expectedFileContents: map[string]string{
|
||||
"special.txt": "Line1\nLine2\tTabbed\x00NullByte😀Emoji",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "ContentMultibyteUnicode",
|
||||
data: url.Values{
|
||||
"title": {"Unicode Content"},
|
||||
"name": {"unicode.txt"},
|
||||
"content": {"Hello 世界 🌍 Привет"},
|
||||
},
|
||||
expectedCode: 302,
|
||||
expectGistCreated: true,
|
||||
expectedTitle: "Unicode Content",
|
||||
expectedNbFiles: 1,
|
||||
expectedVisibility: db.PublicVisibility,
|
||||
expectedFileNames: []string{"unicode.txt"},
|
||||
expectedFileContents: map[string]string{
|
||||
"unicode.txt": "Hello 世界 🌍 Привет",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
s.Login(t, "thomas")
|
||||
|
||||
resp := s.Request(t, "POST", "/", tt.data, tt.expectedCode)
|
||||
|
||||
if tt.expectGistCreated {
|
||||
// Get gist info from redirect
|
||||
username, gistIdentifier := getGistInfoFromRedirect(resp)
|
||||
require.Equal(t, "thomas", username)
|
||||
require.NotEmpty(t, gistIdentifier)
|
||||
|
||||
// Verify gist was created
|
||||
gist, err := db.GetGist(username, gistIdentifier)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Run common verification (filesystem, git, etc.)
|
||||
verifyGistCreation(t, gist, username, gistIdentifier)
|
||||
|
||||
// Verify all expected fields
|
||||
require.Equal(t, tt.expectedTitle, gist.Title, "Title mismatch")
|
||||
require.Equal(t, tt.expectedNbFiles, gist.NbFiles, "File count mismatch")
|
||||
require.Equal(t, tt.expectedVisibility, gist.Private, "Visibility mismatch")
|
||||
|
||||
// Verify description if specified
|
||||
if tt.expectedDescription != "" {
|
||||
require.Equal(t, tt.expectedDescription, gist.Description, "Description mismatch")
|
||||
}
|
||||
|
||||
// Verify URL if specified
|
||||
if tt.expectedURL != "" {
|
||||
require.Equal(t, tt.expectedURL, gist.Identifier(), "URL/Identifier mismatch")
|
||||
}
|
||||
|
||||
// Verify topics if specified
|
||||
if tt.expectedTopics != "" {
|
||||
// Get gist topics
|
||||
topics, err := gist.GetTopics()
|
||||
require.NoError(t, err, "Failed to get gist topics")
|
||||
require.ElementsMatch(t, strings.Split(tt.expectedTopics, ","), topics, "Topics mismatch")
|
||||
}
|
||||
|
||||
// Verify files if specified
|
||||
if len(tt.expectedFileNames) > 0 {
|
||||
files, err := gist.Files("HEAD", false)
|
||||
require.NoError(t, err, "Failed to get gist files")
|
||||
require.Len(t, files, len(tt.expectedFileNames), "File count mismatch")
|
||||
|
||||
actualFileNames := make([]string, len(files))
|
||||
for i, file := range files {
|
||||
actualFileNames[i] = file.Filename
|
||||
}
|
||||
require.ElementsMatch(t, tt.expectedFileNames, actualFileNames, "File names mismatch")
|
||||
|
||||
// Verify file contents if specified
|
||||
if len(tt.expectedFileContents) > 0 {
|
||||
for filename, expectedContent := range tt.expectedFileContents {
|
||||
content, _, err := git.GetFileContent(username, gist.Uuid, "HEAD", filename, false)
|
||||
require.NoError(t, err, "Failed to get content for file %s", filename)
|
||||
require.Equal(t, expectedContent, content, "Content mismatch for file %s", filename)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,74 +0,0 @@
|
||||
package gist_test
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/thomiceli/opengist/internal/db"
|
||||
webtest "github.com/thomiceli/opengist/internal/web/test"
|
||||
)
|
||||
|
||||
func TestDeleteGist(t *testing.T) {
|
||||
s := webtest.Setup(t)
|
||||
defer webtest.Teardown(t)
|
||||
|
||||
s.Register(t, "thomas")
|
||||
s.Register(t, "alice")
|
||||
|
||||
t.Run("NoAuth", func(t *testing.T) {
|
||||
gistPath, _, username, identifier := s.CreateGist(t, "0")
|
||||
|
||||
deleteURL := "/" + username + "/" + identifier + "/delete"
|
||||
s.Request(t, "POST", deleteURL, nil, 302)
|
||||
|
||||
gistCheck, err := db.GetGist(username, identifier)
|
||||
require.NoError(t, err, "Gist should still exist in database")
|
||||
require.NotNil(t, gistCheck)
|
||||
|
||||
_, err = os.Stat(gistPath)
|
||||
require.NoError(t, err, "Gist should still exist on filesystem")
|
||||
})
|
||||
|
||||
t.Run("DeleteOwnGist", func(t *testing.T) {
|
||||
gistPath, _, username, identifier := s.CreateGist(t, "0")
|
||||
|
||||
gistCheck, err := db.GetGist(username, identifier)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, gistCheck)
|
||||
|
||||
s.Login(t, "thomas")
|
||||
deleteURL := "/" + username + "/" + identifier + "/delete"
|
||||
s.Request(t, "POST", deleteURL, nil, 302)
|
||||
|
||||
gistCheck, err = db.GetGist(username, identifier)
|
||||
require.Error(t, err, "Gist should be deleted from database")
|
||||
|
||||
_, err = os.Stat(gistPath)
|
||||
require.Error(t, err, "Gist should not exist on filesystem after deletion")
|
||||
require.True(t, os.IsNotExist(err), "Filesystem should return 'not exist' error")
|
||||
require.Equal(t, uint(0), gistCheck.ID, "Gist should be not in database after deletion")
|
||||
})
|
||||
|
||||
t.Run("DeleteOthersGist", func(t *testing.T) {
|
||||
gistPath, _, username, identifier := s.CreateGist(t, "0")
|
||||
|
||||
s.Login(t, "alice")
|
||||
deleteURL := "/" + username + "/" + identifier + "/delete"
|
||||
s.Request(t, "POST", deleteURL, nil, 403)
|
||||
|
||||
gistCheck, err := db.GetGist(username, identifier)
|
||||
require.NoError(t, err, "Gist should still exist in database")
|
||||
require.NotNil(t, gistCheck)
|
||||
|
||||
_, err = os.Stat(gistPath)
|
||||
require.NoError(t, err, "Gist should still exist on filesystem")
|
||||
})
|
||||
|
||||
t.Run("DeleteNonExistentGist", func(t *testing.T) {
|
||||
s.Login(t, "thomas")
|
||||
|
||||
deleteURL := "/thomas/nonexistent-gist-12345/delete"
|
||||
s.Request(t, "POST", deleteURL, nil, 404)
|
||||
})
|
||||
}
|
||||
@@ -30,7 +30,7 @@ func RawFile(ctx *context.Context) error {
|
||||
if file.MimeType.CanBeEmbedded() {
|
||||
ctx.Response().Header().Set("Content-Type", file.MimeType.ContentType)
|
||||
} else if file.MimeType.IsText() {
|
||||
ctx.Response().Header().Set("Content-Type", "text/plain; charset=utf-8")
|
||||
ctx.Response().Header().Set("Content-Type", "text/plain")
|
||||
} else {
|
||||
ctx.Response().Header().Set("Content-Type", "application/octet-stream")
|
||||
}
|
||||
|
||||
@@ -1,141 +0,0 @@
|
||||
package gist_test
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"bytes"
|
||||
"io"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
webtest "github.com/thomiceli/opengist/internal/web/test"
|
||||
)
|
||||
|
||||
func TestDownloadZip(t *testing.T) {
|
||||
s := webtest.Setup(t)
|
||||
defer webtest.Teardown(t)
|
||||
|
||||
t.Run("MultipleFiles", func(t *testing.T) {
|
||||
_, _, username, identifier := s.CreateGist(t, "0")
|
||||
|
||||
resp := s.Request(t, "GET", "/"+username+"/"+identifier+"/archive/HEAD", nil, 200)
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
require.NoError(t, err)
|
||||
|
||||
zipReader, err := zip.NewReader(bytes.NewReader(body), int64(len(body)))
|
||||
require.NoError(t, err)
|
||||
require.Len(t, zipReader.File, 2)
|
||||
|
||||
fileNames := make([]string, len(zipReader.File))
|
||||
contents := make([]string, len(zipReader.File))
|
||||
for i, file := range zipReader.File {
|
||||
fileNames[i] = file.Name
|
||||
f, err := file.Open()
|
||||
require.NoError(t, err)
|
||||
content, err := io.ReadAll(f)
|
||||
require.NoError(t, err)
|
||||
contents[i] = string(content)
|
||||
f.Close()
|
||||
}
|
||||
require.ElementsMatch(t, []string{"file.txt", "otherfile.txt"}, fileNames)
|
||||
require.ElementsMatch(t, []string{"hello world", "other content"}, contents)
|
||||
})
|
||||
|
||||
t.Run("PrivateGist", func(t *testing.T) {
|
||||
_, _, username, identifier := s.CreateGist(t, "2")
|
||||
|
||||
s.Request(t, "GET", "/"+username+"/"+identifier+"/archive/HEAD", nil, 404)
|
||||
})
|
||||
|
||||
t.Run("NonExistentRevision", func(t *testing.T) {
|
||||
_, _, username, identifier := s.CreateGist(t, "0")
|
||||
|
||||
// TODO: return 404
|
||||
s.Request(t, "GET", "/"+username+"/"+identifier+"/archive/zz", nil, 0)
|
||||
})
|
||||
}
|
||||
|
||||
func TestRawFile(t *testing.T) {
|
||||
s := webtest.Setup(t)
|
||||
defer webtest.Teardown(t)
|
||||
|
||||
t.Run("ExistingFile", func(t *testing.T) {
|
||||
_, _, username, identifier := s.CreateGist(t, "0")
|
||||
|
||||
resp := s.Request(t, "GET", "/"+username+"/"+identifier+"/raw/HEAD/file.txt", nil, 200)
|
||||
|
||||
require.Equal(t, `inline; filename="file.txt"`, resp.Header.Get("Content-Disposition"))
|
||||
require.Equal(t, "nosniff", resp.Header.Get("X-Content-Type-Options"))
|
||||
require.Contains(t, resp.Header.Get("Content-Type"), "text/plain")
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "hello world", string(body))
|
||||
})
|
||||
|
||||
t.Run("NonExistentFile", func(t *testing.T) {
|
||||
_, _, username, identifier := s.CreateGist(t, "0")
|
||||
|
||||
s.Request(t, "GET", "/"+username+"/"+identifier+"/raw/HEAD/nonexistent.txt", nil, 404)
|
||||
})
|
||||
|
||||
t.Run("NonExistentRevision", func(t *testing.T) {
|
||||
_, _, username, identifier := s.CreateGist(t, "0")
|
||||
|
||||
s.Request(t, "GET", "/"+username+"/"+identifier+"/raw/zz/file.txt", nil, 404)
|
||||
})
|
||||
|
||||
t.Run("PrivateGist", func(t *testing.T) {
|
||||
_, _, username, identifier := s.CreateGist(t, "2")
|
||||
|
||||
s.Request(t, "GET", "/"+username+"/"+identifier+"/raw/HEAD/file.txt", nil, 404)
|
||||
})
|
||||
}
|
||||
|
||||
func TestDownloadFile(t *testing.T) {
|
||||
s := webtest.Setup(t)
|
||||
defer webtest.Teardown(t)
|
||||
|
||||
t.Run("ExistingFile", func(t *testing.T) {
|
||||
_, _, username, identifier := s.CreateGist(t, "0")
|
||||
|
||||
resp := s.Request(t, "GET", "/"+username+"/"+identifier+"/download/HEAD/file.txt", nil, 200)
|
||||
|
||||
require.Equal(t, "text/plain; charset=utf-8", resp.Header.Get("Content-Type"))
|
||||
require.Equal(t, `attachment; filename="file.txt"`, resp.Header.Get("Content-Disposition"))
|
||||
require.Equal(t, "11", resp.Header.Get("Content-Length"))
|
||||
require.Equal(t, "nosniff", resp.Header.Get("X-Content-Type-Options"))
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "hello world", string(body))
|
||||
})
|
||||
|
||||
t.Run("NonExistentFile", func(t *testing.T) {
|
||||
_, _, username, identifier := s.CreateGist(t, "0")
|
||||
|
||||
resp := s.Request(t, "GET", "/"+username+"/"+identifier+"/download/HEAD/nonexistent.txt", nil, 404)
|
||||
|
||||
_, err := io.ReadAll(resp.Body)
|
||||
require.NoError(t, err)
|
||||
// TODO: change the response to not found
|
||||
// require.Equal(t, "File not found", string(body))
|
||||
})
|
||||
|
||||
t.Run("NonExistentRevision", func(t *testing.T) {
|
||||
_, _, username, identifier := s.CreateGist(t, "0")
|
||||
|
||||
resp := s.Request(t, "GET", "/"+username+"/"+identifier+"/download/zz/file.txt", nil, 404)
|
||||
|
||||
_, err := io.ReadAll(resp.Body)
|
||||
require.NoError(t, err)
|
||||
// TODO: change the response to not found
|
||||
// require.Equal(t, "File not found", string(body))
|
||||
})
|
||||
|
||||
t.Run("PrivateGist", func(t *testing.T) {
|
||||
_, _, username, identifier := s.CreateGist(t, "2")
|
||||
|
||||
s.Request(t, "GET", "/"+username+"/"+identifier+"/download/HEAD/file.txt", nil, 404)
|
||||
})
|
||||
}
|
||||
@@ -1,11 +1,10 @@
|
||||
package gist
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"github.com/thomiceli/opengist/internal/db"
|
||||
"github.com/thomiceli/opengist/internal/render"
|
||||
"github.com/thomiceli/opengist/internal/web/context"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
func Edit(ctx *context.Context) error {
|
||||
|
||||
@@ -1,66 +0,0 @@
|
||||
package gist_test
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/thomiceli/opengist/internal/db"
|
||||
webtest "github.com/thomiceli/opengist/internal/web/test"
|
||||
)
|
||||
|
||||
func TestVisibility(t *testing.T) {
|
||||
s := webtest.Setup(t)
|
||||
defer webtest.Teardown(t)
|
||||
|
||||
s.Register(t, "thomas")
|
||||
s.Register(t, "alice")
|
||||
|
||||
t.Run("ChangeVisibility", func(t *testing.T) {
|
||||
_, _, username, identifier := s.CreateGist(t, "0")
|
||||
|
||||
s.Login(t, "thomas")
|
||||
s.Request(t, "POST", "/"+username+"/"+identifier+"/visibility", url.Values{
|
||||
"private": {"2"},
|
||||
}, 302)
|
||||
|
||||
gist, err := db.GetGist(username, identifier)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, db.PrivateVisibility, gist.Private)
|
||||
})
|
||||
|
||||
t.Run("ChangeToUnlisted", func(t *testing.T) {
|
||||
_, _, username, identifier := s.CreateGist(t, "0")
|
||||
|
||||
s.Login(t, "thomas")
|
||||
s.Request(t, "POST", "/"+username+"/"+identifier+"/visibility", url.Values{
|
||||
"private": {"1"},
|
||||
}, 302)
|
||||
|
||||
gist, err := db.GetGist(username, identifier)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, db.UnlistedVisibility, gist.Private)
|
||||
})
|
||||
|
||||
t.Run("OtherUserCannotChange", func(t *testing.T) {
|
||||
_, _, username, identifier := s.CreateGist(t, "0")
|
||||
|
||||
s.Login(t, "alice")
|
||||
s.Request(t, "POST", "/"+username+"/"+identifier+"/visibility", nil, 403)
|
||||
|
||||
gist, err := db.GetGist(username, identifier)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, db.PublicVisibility, gist.Private)
|
||||
})
|
||||
|
||||
t.Run("NoAuth", func(t *testing.T) {
|
||||
_, _, username, identifier := s.CreateGist(t, "0")
|
||||
|
||||
s.Logout()
|
||||
s.Request(t, "POST", "/"+username+"/"+identifier+"/visibility", nil, 302)
|
||||
|
||||
gist, err := db.GetGist(username, identifier)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, db.PublicVisibility, gist.Private)
|
||||
})
|
||||
}
|
||||
@@ -1,102 +0,0 @@
|
||||
package gist_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/thomiceli/opengist/internal/db"
|
||||
webtest "github.com/thomiceli/opengist/internal/web/test"
|
||||
)
|
||||
|
||||
func TestFork(t *testing.T) {
|
||||
s := webtest.Setup(t)
|
||||
defer webtest.Teardown(t)
|
||||
|
||||
s.Register(t, "thomas")
|
||||
s.Register(t, "alice")
|
||||
|
||||
t.Run("Fork", func(t *testing.T) {
|
||||
_, gist, username, identifier := s.CreateGist(t, "0")
|
||||
s.Login(t, "alice")
|
||||
|
||||
resp := s.Request(t, "POST", "/"+username+"/"+identifier+"/fork", nil, 302)
|
||||
|
||||
forkedGist, err := db.GetGistByID("2")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "alice", forkedGist.User.Username)
|
||||
require.Equal(t, gist.Title, forkedGist.Title)
|
||||
require.Equal(t, gist.Description, forkedGist.Description)
|
||||
require.Equal(t, gist.Private, forkedGist.Private)
|
||||
require.Equal(t, gist.ID, forkedGist.ForkedID)
|
||||
|
||||
forkedFiles, err := forkedGist.Files("HEAD", false)
|
||||
require.NoError(t, err)
|
||||
|
||||
gistFiles, err := gist.Files("HEAD", false)
|
||||
require.NoError(t, err)
|
||||
|
||||
for i, file := range gistFiles {
|
||||
require.Equal(t, file.Filename, forkedFiles[i].Filename)
|
||||
require.Equal(t, file.Content, forkedFiles[i].Content)
|
||||
}
|
||||
|
||||
require.Equal(t, "/alice/"+forkedGist.Identifier(), resp.Header.Get("Location"))
|
||||
|
||||
original, err := db.GetGistByID("1")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, original.NbForks)
|
||||
|
||||
forks, err := original.GetForks(2, 0)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, forks, 1)
|
||||
require.Equal(t, forkedGist.ID, forks[0].ID)
|
||||
|
||||
forkedGists, err := db.GetAllGistsForkedByUser(2, 2, 0, "created", "asc")
|
||||
require.NoError(t, err)
|
||||
require.Len(t, forkedGists, 1)
|
||||
require.Equal(t, forkedGist.ID, forkedGists[0].ID)
|
||||
})
|
||||
|
||||
t.Run("OwnGist", func(t *testing.T) {
|
||||
_, _, username, identifier := s.CreateGist(t, "0")
|
||||
s.Login(t, "thomas")
|
||||
|
||||
s.Request(t, "POST", "/"+username+"/"+identifier+"/fork", nil, 302)
|
||||
|
||||
original, err := db.GetGist(username, identifier)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 0, original.NbForks)
|
||||
})
|
||||
|
||||
t.Run("AlreadyForked", func(t *testing.T) {
|
||||
_, _, username, identifier := s.CreateGist(t, "0")
|
||||
s.Login(t, "alice")
|
||||
|
||||
firstResp := s.Request(t, "POST", "/"+username+"/"+identifier+"/fork", nil, 302)
|
||||
forkLocation := firstResp.Header.Get("Location")
|
||||
|
||||
secondResp := s.Request(t, "POST", "/"+username+"/"+identifier+"/fork", nil, 302)
|
||||
require.Equal(t, forkLocation, secondResp.Header.Get("Location"))
|
||||
|
||||
original, err := db.GetGist(username, identifier)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, original.NbForks)
|
||||
})
|
||||
|
||||
t.Run("NoAuth", func(t *testing.T) {
|
||||
_, _, username, identifier := s.CreateGist(t, "0")
|
||||
|
||||
s.Request(t, "POST", "/"+username+"/"+identifier+"/fork", nil, 302)
|
||||
|
||||
original, err := db.GetGist(username, identifier)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 0, original.NbForks)
|
||||
})
|
||||
|
||||
t.Run("PrivateGist", func(t *testing.T) {
|
||||
_, _, username, identifier := s.CreateGist(t, "2")
|
||||
s.Login(t, "alice")
|
||||
|
||||
s.Request(t, "POST", "/"+username+"/"+identifier+"/fork", nil, 404)
|
||||
})
|
||||
}
|
||||
@@ -165,39 +165,10 @@ func escapeJavaScriptContent(htmlContent, cssUrl, themeUrl string) (string, erro
|
||||
}
|
||||
|
||||
js := fmt.Sprintf(`
|
||||
(function() {
|
||||
if (!customElements.get('opengist-embed')) {
|
||||
customElements.define('opengist-embed', class extends HTMLElement {
|
||||
constructor() {
|
||||
super();
|
||||
this.attachShadow({ mode: 'open' });
|
||||
}
|
||||
|
||||
init(css1, css2, content) {
|
||||
this.shadowRoot.innerHTML = %s
|
||||
<style>
|
||||
@import url(${css1});
|
||||
@import url(${css2});
|
||||
:host { display: block; all: initial; font-family: sans-serif; }
|
||||
</style>
|
||||
<div class="container">${content}</div>
|
||||
%s;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
var currentScript = document.currentScript || (function() {
|
||||
var scripts = document.getElementsByTagName('script');
|
||||
return scripts[scripts.length - 1];
|
||||
})();
|
||||
|
||||
const instance = document.createElement('opengist-embed');
|
||||
instance.init(%s, %s, %s);
|
||||
currentScript.parentNode.insertBefore(instance, currentScript.nextSibling);
|
||||
})();
|
||||
`,
|
||||
"`",
|
||||
"`",
|
||||
document.write('<link rel="stylesheet" href=%s>');
|
||||
document.write('<link rel="stylesheet" href=%s>');
|
||||
document.write(%s);
|
||||
`,
|
||||
string(jsonCssUrl),
|
||||
string(jsonThemeUrl),
|
||||
string(jsonContent),
|
||||
|
||||
@@ -1,327 +0,0 @@
|
||||
package gist_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/url"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/thomiceli/opengist/internal/db"
|
||||
"github.com/thomiceli/opengist/internal/web/context"
|
||||
webtest "github.com/thomiceli/opengist/internal/web/test"
|
||||
)
|
||||
|
||||
func setupManifestEntries() {
|
||||
context.ManifestEntries = map[string]context.Asset{
|
||||
"embed.css": {File: "assets/embed.css"},
|
||||
"ts/embed.ts": {Css: []string{"assets/embed.css"}},
|
||||
"ts/light.ts": {Css: []string{"assets/light.css"}},
|
||||
"ts/dark.ts": {Css: []string{"assets/dark.css"}},
|
||||
}
|
||||
}
|
||||
|
||||
func TestGistIndex(t *testing.T) {
|
||||
s := webtest.Setup(t)
|
||||
defer webtest.Teardown(t)
|
||||
|
||||
s.Register(t, "thomas")
|
||||
s.Register(t, "alice")
|
||||
|
||||
t.Run("Public", func(t *testing.T) {
|
||||
_, _, username, identifier := s.CreateGist(t, "0")
|
||||
|
||||
s.Request(t, "GET", "/"+username+"/"+identifier, nil, 200)
|
||||
})
|
||||
|
||||
t.Run("NonExistentRevision", func(t *testing.T) {
|
||||
_, _, username, identifier := s.CreateGist(t, "0")
|
||||
|
||||
s.Request(t, "GET", "/"+username+"/"+identifier+"/rev/nonexistent", nil, 404)
|
||||
})
|
||||
|
||||
t.Run("NonExistentGist", func(t *testing.T) {
|
||||
s.Request(t, "GET", "/thomas/nonexistent", nil, 404)
|
||||
})
|
||||
|
||||
t.Run("Unlisted", func(t *testing.T) {
|
||||
_, _, username, identifier := s.CreateGist(t, "1")
|
||||
|
||||
s.Login(t, "thomas")
|
||||
s.Request(t, "GET", "/"+username+"/"+identifier, nil, 200)
|
||||
|
||||
s.Login(t, "alice")
|
||||
s.Request(t, "GET", "/"+username+"/"+identifier, nil, 200)
|
||||
|
||||
s.Logout()
|
||||
s.Request(t, "GET", "/"+username+"/"+identifier, nil, 200)
|
||||
})
|
||||
|
||||
t.Run("Private", func(t *testing.T) {
|
||||
_, _, username, identifier := s.CreateGist(t, "2")
|
||||
|
||||
s.Login(t, "thomas")
|
||||
s.Request(t, "GET", "/"+username+"/"+identifier, nil, 200)
|
||||
|
||||
s.Login(t, "alice")
|
||||
s.Request(t, "GET", "/"+username+"/"+identifier, nil, 404)
|
||||
|
||||
s.Logout()
|
||||
s.Request(t, "GET", "/"+username+"/"+identifier, nil, 404)
|
||||
})
|
||||
|
||||
t.Run("SpecificRevision", func(t *testing.T) {
|
||||
_, gist, username, identifier := s.CreateGist(t, "0")
|
||||
|
||||
s.Login(t, "thomas")
|
||||
s.Request(t, "POST", "/"+username+"/"+identifier+"/edit", url.Values{
|
||||
"title": {"Test"},
|
||||
"name": {"file.txt"},
|
||||
"content": {"updated content"},
|
||||
}, 302)
|
||||
|
||||
files, err := gist.Files("HEAD", false)
|
||||
require.NoError(t, err)
|
||||
found := false
|
||||
for _, f := range files {
|
||||
if f.Filename == "file.txt" {
|
||||
require.Equal(t, "updated content", f.Content)
|
||||
found = true
|
||||
}
|
||||
}
|
||||
require.True(t, found)
|
||||
|
||||
commits, err := gist.Log(0)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, commits, 2)
|
||||
|
||||
filesOld, err := gist.Files(commits[1].Hash, false)
|
||||
require.NoError(t, err)
|
||||
for _, f := range filesOld {
|
||||
if f.Filename == "file.txt" {
|
||||
require.Equal(t, "hello world", f.Content)
|
||||
}
|
||||
}
|
||||
|
||||
s.Request(t, "GET", "/"+username+"/"+identifier+"/rev/HEAD", nil, 200)
|
||||
s.Request(t, "GET", "/"+username+"/"+identifier+"/rev/"+commits[1].Hash, nil, 200)
|
||||
})
|
||||
}
|
||||
|
||||
func TestPreview(t *testing.T) {
|
||||
s := webtest.Setup(t)
|
||||
defer webtest.Teardown(t)
|
||||
|
||||
s.Register(t, "thomas")
|
||||
|
||||
t.Run("Markdown", func(t *testing.T) {
|
||||
s.Login(t, "thomas")
|
||||
|
||||
resp := s.Request(t, "POST", "/preview", url.Values{
|
||||
"content": {"# Hello\n\nThis is **bold** and *italic*."},
|
||||
}, 200)
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
require.NoError(t, err)
|
||||
html := string(body)
|
||||
require.Contains(t, html, "<h1>")
|
||||
require.Contains(t, html, "Hello")
|
||||
require.Contains(t, html, "<strong>bold</strong>")
|
||||
require.Contains(t, html, "<em>italic</em>")
|
||||
})
|
||||
|
||||
t.Run("NoAuth", func(t *testing.T) {
|
||||
s.Logout()
|
||||
s.Request(t, "POST", "/preview", url.Values{
|
||||
"content": {"# Hello"},
|
||||
}, 302)
|
||||
})
|
||||
}
|
||||
|
||||
func TestGistJson(t *testing.T) {
|
||||
setupManifestEntries()
|
||||
|
||||
s := webtest.Setup(t)
|
||||
defer webtest.Teardown(t)
|
||||
|
||||
s.Register(t, "thomas")
|
||||
s.Register(t, "alice")
|
||||
|
||||
t.Run("Public", func(t *testing.T) {
|
||||
_, gist, username, identifier := s.CreateGist(t, "0")
|
||||
|
||||
resp := s.Request(t, "GET", "/"+username+"/"+identifier+".json", nil, 200)
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
require.NoError(t, err)
|
||||
|
||||
var result map[string]interface{}
|
||||
err = json.Unmarshal(body, &result)
|
||||
require.NoError(t, err)
|
||||
t.Helper()
|
||||
|
||||
require.Equal(t, username, result["owner"])
|
||||
require.Equal(t, identifier, result["id"])
|
||||
require.Equal(t, gist.Uuid, result["uuid"])
|
||||
require.Equal(t, gist.Title, result["title"])
|
||||
require.Equal(t, "public", result["visibility"])
|
||||
require.Equal(t, []interface{}{"hello", "opengist"}, result["topics"])
|
||||
require.Equal(t, []interface{}{
|
||||
map[string]interface{}{
|
||||
"content": "hello world",
|
||||
"filename": "file.txt",
|
||||
"human_size": "11 B",
|
||||
"size": float64(11),
|
||||
"truncated": false,
|
||||
"type": "Text",
|
||||
},
|
||||
map[string]interface{}{
|
||||
"content": "other content",
|
||||
"filename": "otherfile.txt",
|
||||
"human_size": "13 B",
|
||||
"size": float64(13),
|
||||
"truncated": false,
|
||||
"type": "Text",
|
||||
},
|
||||
}, result["files"])
|
||||
|
||||
embed, ok := result["embed"].(map[string]interface{})
|
||||
require.True(t, ok)
|
||||
require.Contains(t, embed["js"], identifier+".js")
|
||||
require.Contains(t, embed["js_dark"], identifier+".js?dark")
|
||||
require.NotEmpty(t, embed["css"])
|
||||
require.NotEmpty(t, embed["html"])
|
||||
})
|
||||
|
||||
t.Run("Unlisted", func(t *testing.T) {
|
||||
s.Logout()
|
||||
_, _, username, identifier := s.CreateGist(t, "1")
|
||||
|
||||
s.Request(t, "GET", "/"+username+"/"+identifier+".json", nil, 200)
|
||||
})
|
||||
|
||||
t.Run("Private", func(t *testing.T) {
|
||||
s.Logout()
|
||||
_, _, username, identifier := s.CreateGist(t, "2")
|
||||
|
||||
s.Request(t, "GET", "/"+username+"/"+identifier+".json", nil, 404)
|
||||
})
|
||||
|
||||
t.Run("NonExistentGist", func(t *testing.T) {
|
||||
s.Request(t, "GET", "/thomas/nonexistent.json", nil, 404)
|
||||
})
|
||||
}
|
||||
|
||||
func TestGistAccess(t *testing.T) {
|
||||
s := webtest.Setup(t)
|
||||
defer webtest.Teardown(t)
|
||||
|
||||
s.Register(t, "thomas")
|
||||
s.Register(t, "alice")
|
||||
|
||||
_, _, user, publicId := s.CreateGist(t, "0")
|
||||
_, _, _, unlistedId := s.CreateGist(t, "1")
|
||||
_, _, _, privateId := s.CreateGist(t, "2")
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
settings map[string]string
|
||||
// expected codes: [owner, otherUser, anonymous] x [public, unlisted, private]
|
||||
owner, otherUser, anonymous []int
|
||||
}{
|
||||
{
|
||||
name: "Default",
|
||||
owner: []int{200, 200, 200},
|
||||
otherUser: []int{200, 200, 404},
|
||||
anonymous: []int{200, 200, 404},
|
||||
},
|
||||
{
|
||||
name: "RequireLogin",
|
||||
settings: map[string]string{db.SettingRequireLogin: "1"},
|
||||
owner: []int{200, 200, 200},
|
||||
otherUser: []int{200, 200, 404},
|
||||
anonymous: []int{302, 302, 302},
|
||||
},
|
||||
{
|
||||
name: "AllowGistsWithoutLogin",
|
||||
settings: map[string]string{db.SettingRequireLogin: "1", db.SettingAllowGistsWithoutLogin: "1"},
|
||||
owner: []int{200, 200, 200},
|
||||
otherUser: []int{200, 200, 404},
|
||||
anonymous: []int{200, 200, 404},
|
||||
},
|
||||
}
|
||||
|
||||
gists := []string{publicId, unlistedId, privateId}
|
||||
labels := []string{"Public", "Unlisted", "Private"}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
s.Login(t, "thomas")
|
||||
for k, v := range tt.settings {
|
||||
s.Request(t, "PUT", "/admin-panel/set-config", url.Values{"key": {k}, "value": {v}}, 200)
|
||||
}
|
||||
|
||||
t.Run("Owner", func(t *testing.T) {
|
||||
s.Login(t, "thomas")
|
||||
for i, id := range gists {
|
||||
s.Request(t, "GET", "/"+user+"/"+id, nil, tt.owner[i])
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("OtherUser", func(t *testing.T) {
|
||||
s.Login(t, "alice")
|
||||
for i, id := range gists {
|
||||
s.Request(t, "GET", "/"+user+"/"+id, nil, tt.otherUser[i])
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Anonymous", func(t *testing.T) {
|
||||
s.Logout()
|
||||
for i, id := range gists {
|
||||
t.Run(labels[i], func(t *testing.T) {
|
||||
s.Request(t, "GET", "/"+user+"/"+id, nil, tt.anonymous[i])
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
s.Login(t, "thomas")
|
||||
for k := range tt.settings {
|
||||
s.Request(t, "PUT", "/admin-panel/set-config", url.Values{"key": {k}, "value": {"0"}}, 200)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetGistCaseInsensitive(t *testing.T) {
|
||||
s := webtest.Setup(t)
|
||||
defer webtest.Teardown(t)
|
||||
|
||||
s.Register(t, "THOmas")
|
||||
s.Login(t, "THOmas")
|
||||
|
||||
s.Request(t, "POST", "/", url.Values{
|
||||
"title": {"Test"},
|
||||
"name": {"file.txt"},
|
||||
"content": {"hello world"},
|
||||
"url": {"my-GIST"},
|
||||
"private": {"0"},
|
||||
}, 302)
|
||||
|
||||
gist, err := db.GetGistByID("1")
|
||||
require.NoError(t, err)
|
||||
|
||||
s.Logout()
|
||||
|
||||
t.Run("URL", func(t *testing.T) {
|
||||
s.Request(t, "GET", "/thomas/my-gist", nil, 200)
|
||||
s.Request(t, "GET", "/THOMAS/MY-GIST", nil, 200)
|
||||
s.Request(t, "GET", "/thomas/MY-GIST", nil, 200)
|
||||
s.Request(t, "GET", "/THOMAS/my-gist", nil, 200)
|
||||
})
|
||||
|
||||
t.Run("UUID", func(t *testing.T) {
|
||||
s.Request(t, "GET", "/thomas/"+strings.ToLower(gist.Uuid), nil, 200)
|
||||
s.Request(t, "GET", "/THOMAS/"+strings.ToUpper(gist.Uuid), nil, 200)
|
||||
})
|
||||
}
|
||||
@@ -1,96 +0,0 @@
|
||||
package gist_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/thomiceli/opengist/internal/db"
|
||||
webtest "github.com/thomiceli/opengist/internal/web/test"
|
||||
)
|
||||
|
||||
func TestLike(t *testing.T) {
|
||||
s := webtest.Setup(t)
|
||||
defer webtest.Teardown(t)
|
||||
|
||||
s.Register(t, "thomas")
|
||||
s.Register(t, "alice")
|
||||
|
||||
t.Run("Like", func(t *testing.T) {
|
||||
_, _, username, identifier := s.CreateGist(t, "0")
|
||||
|
||||
s.Login(t, "alice")
|
||||
resp := s.Request(t, "POST", "/"+username+"/"+identifier+"/like", nil, 302)
|
||||
require.Equal(t, "/"+username+"/"+identifier, resp.Header.Get("Location"))
|
||||
|
||||
s.Login(t, "thomas")
|
||||
s.Request(t, "POST", "/"+username+"/"+identifier+"/like", nil, 302)
|
||||
|
||||
gist, err := db.GetGist(username, identifier)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 2, gist.NbLikes)
|
||||
|
||||
likers, err := gist.GetUsersLikes(0)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, likers, 2)
|
||||
})
|
||||
|
||||
t.Run("Unlike", func(t *testing.T) {
|
||||
_, _, username, identifier := s.CreateGist(t, "0")
|
||||
|
||||
s.Login(t, "alice")
|
||||
s.Request(t, "POST", "/"+username+"/"+identifier+"/like", nil, 302)
|
||||
|
||||
s.Request(t, "POST", "/"+username+"/"+identifier+"/like", nil, 302)
|
||||
|
||||
gist, err := db.GetGist(username, identifier)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 0, gist.NbLikes)
|
||||
|
||||
likers, err := gist.GetUsersLikes(0)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, likers, 0)
|
||||
})
|
||||
|
||||
t.Run("NoAuth", func(t *testing.T) {
|
||||
_, _, username, identifier := s.CreateGist(t, "0")
|
||||
|
||||
s.Logout()
|
||||
s.Request(t, "POST", "/"+username+"/"+identifier+"/like", nil, 302)
|
||||
|
||||
gist, err := db.GetGist(username, identifier)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 0, gist.NbLikes)
|
||||
})
|
||||
|
||||
t.Run("PrivateGist", func(t *testing.T) {
|
||||
_, _, username, identifier := s.CreateGist(t, "2")
|
||||
|
||||
s.Login(t, "alice")
|
||||
s.Request(t, "POST", "/"+username+"/"+identifier+"/like", nil, 404)
|
||||
})
|
||||
}
|
||||
|
||||
func TestLikes(t *testing.T) {
|
||||
s := webtest.Setup(t)
|
||||
defer webtest.Teardown(t)
|
||||
|
||||
s.Register(t, "thomas")
|
||||
s.Register(t, "alice")
|
||||
|
||||
t.Run("Likes", func(t *testing.T) {
|
||||
_, gist, username, identifier := s.CreateGist(t, "0")
|
||||
|
||||
s.Login(t, "thomas")
|
||||
s.Request(t, "POST", "/"+username+"/"+identifier+"/like", nil, 302)
|
||||
s.Login(t, "alice")
|
||||
s.Request(t, "POST", "/"+username+"/"+identifier+"/like", nil, 302)
|
||||
|
||||
s.Request(t, "GET", "/"+username+"/"+identifier+"/likes", nil, 200)
|
||||
|
||||
users, err := gist.GetUsersLikes(0)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, users, 2)
|
||||
require.Equal(t, "thomas", users[0].Username)
|
||||
require.Equal(t, "alice", users[1].Username)
|
||||
})
|
||||
}
|
||||
@@ -1,11 +1,10 @@
|
||||
package gist
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/thomiceli/opengist/internal/db"
|
||||
"github.com/thomiceli/opengist/internal/web/context"
|
||||
"github.com/thomiceli/opengist/internal/web/handlers"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func Revisions(ctx *context.Context) error {
|
||||
|
||||
@@ -1,153 +0,0 @@
|
||||
package gist_test
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/thomiceli/opengist/internal/git"
|
||||
webtest "github.com/thomiceli/opengist/internal/web/test"
|
||||
)
|
||||
|
||||
func TestRevisions(t *testing.T) {
|
||||
s := webtest.Setup(t)
|
||||
defer webtest.Teardown(t)
|
||||
|
||||
s.Register(t, "thomas")
|
||||
s.Register(t, "alice")
|
||||
|
||||
t.Run("Revisions", func(t *testing.T) {
|
||||
_, gist, username, identifier := s.CreateGist(t, "0")
|
||||
|
||||
s.Login(t, "thomas")
|
||||
s.Request(t, "POST", "/"+username+"/"+identifier+"/edit", url.Values{
|
||||
"title": {"Test"},
|
||||
"name": {"file.txt", "ok.txt"},
|
||||
"content": {"updated content", "okay"},
|
||||
}, 302)
|
||||
|
||||
s.Request(t, "POST", "/"+username+"/"+identifier+"/edit", url.Values{
|
||||
"title": {"Test"},
|
||||
"name": {"renamed.txt", "ok.txt"},
|
||||
"content": {"updated content", "okay"},
|
||||
}, 302)
|
||||
|
||||
commits, err := gist.Log(0)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Len(t, commits, 3)
|
||||
|
||||
require.Regexp(t, "^[a-f0-9]{40}$", commits[0].Hash)
|
||||
require.Regexp(t, "^[a-f0-9]{40}$", commits[1].Hash)
|
||||
require.Regexp(t, "^[a-f0-9]{40}$", commits[2].Hash)
|
||||
|
||||
require.Equal(t, &git.Commit{
|
||||
Hash: commits[0].Hash,
|
||||
Timestamp: commits[0].Timestamp,
|
||||
AuthorName: "thomas",
|
||||
Changed: "1 file changed, 0 insertions, 0 deletions",
|
||||
Files: []git.File{
|
||||
{
|
||||
Filename: "renamed.txt",
|
||||
Size: 0,
|
||||
HumanSize: "",
|
||||
OldFilename: "file.txt",
|
||||
Content: ``,
|
||||
Truncated: false,
|
||||
IsCreated: false,
|
||||
IsDeleted: false,
|
||||
IsBinary: false,
|
||||
MimeType: git.MimeType{},
|
||||
},
|
||||
},
|
||||
}, commits[0])
|
||||
|
||||
require.Equal(t, &git.Commit{
|
||||
Hash: commits[1].Hash,
|
||||
Timestamp: commits[1].Timestamp,
|
||||
AuthorName: "thomas",
|
||||
Changed: "3 files changed, 2 insertions, 2 deletions",
|
||||
Files: []git.File{
|
||||
{
|
||||
Filename: "file.txt",
|
||||
OldFilename: "file.txt",
|
||||
Content: `@@ -1 +1 @@
|
||||
-hello world
|
||||
\ No newline at end of file
|
||||
+updated content
|
||||
\ No newline at end of file
|
||||
`,
|
||||
IsCreated: false,
|
||||
IsDeleted: false,
|
||||
IsBinary: false,
|
||||
}, {
|
||||
Filename: "ok.txt",
|
||||
OldFilename: "",
|
||||
Content: `@@ -0,0 +1 @@
|
||||
+okay
|
||||
\ No newline at end of file
|
||||
`,
|
||||
IsCreated: true,
|
||||
IsDeleted: false,
|
||||
IsBinary: false,
|
||||
}, {
|
||||
Filename: "otherfile.txt",
|
||||
OldFilename: "",
|
||||
Content: `@@ -1 +0,0 @@
|
||||
-other content
|
||||
\ No newline at end of file
|
||||
`,
|
||||
IsCreated: false,
|
||||
IsDeleted: true,
|
||||
IsBinary: false,
|
||||
},
|
||||
},
|
||||
}, commits[1])
|
||||
|
||||
require.Equal(t, &git.Commit{
|
||||
Hash: commits[2].Hash,
|
||||
Timestamp: commits[2].Timestamp,
|
||||
AuthorName: "thomas",
|
||||
Changed: "2 files changed, 2 insertions",
|
||||
Files: []git.File{
|
||||
{
|
||||
Filename: "file.txt",
|
||||
OldFilename: "",
|
||||
Content: `@@ -0,0 +1 @@
|
||||
+hello world
|
||||
\ No newline at end of file
|
||||
`,
|
||||
IsCreated: true,
|
||||
IsDeleted: false,
|
||||
IsBinary: false,
|
||||
}, {
|
||||
Filename: "otherfile.txt",
|
||||
OldFilename: "",
|
||||
Content: `@@ -0,0 +1 @@
|
||||
+other content
|
||||
\ No newline at end of file
|
||||
`,
|
||||
IsCreated: true,
|
||||
IsDeleted: false,
|
||||
IsBinary: false,
|
||||
},
|
||||
},
|
||||
}, commits[2])
|
||||
|
||||
s.Request(t, "GET", "/"+username+"/"+identifier+"/revisions", nil, 200)
|
||||
})
|
||||
|
||||
t.Run("NoAuth", func(t *testing.T) {
|
||||
_, _, username, identifier := s.CreateGist(t, "0")
|
||||
|
||||
s.Logout()
|
||||
s.Request(t, "GET", "/"+username+"/"+identifier+"/revisions", nil, 200)
|
||||
})
|
||||
|
||||
t.Run("PrivateGist", func(t *testing.T) {
|
||||
_, _, username, identifier := s.CreateGist(t, "2")
|
||||
|
||||
s.Login(t, "alice")
|
||||
s.Request(t, "GET", "/"+username+"/"+identifier+"/revisions", nil, 404)
|
||||
})
|
||||
}
|
||||
@@ -4,15 +4,12 @@ import (
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/thomiceli/opengist/internal/config"
|
||||
"github.com/thomiceli/opengist/internal/web/context"
|
||||
)
|
||||
|
||||
var uuidRegex = regexp.MustCompile(`^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$`)
|
||||
|
||||
func Upload(ctx *context.Context) error {
|
||||
err := ctx.Request().ParseMultipartForm(32 << 20) // 32 MB max
|
||||
if err != nil {
|
||||
@@ -60,13 +57,13 @@ func Upload(ctx *context.Context) error {
|
||||
}
|
||||
|
||||
func DeleteUpload(ctx *context.Context) error {
|
||||
fileUuid := filepath.Base(ctx.Param("uuid"))
|
||||
|
||||
if fileUuid == "" || !uuidRegex.MatchString(fileUuid) {
|
||||
uuid := ctx.Param("uuid")
|
||||
if uuid == "" {
|
||||
return ctx.ErrorRes(400, ctx.Tr("error.bad-request"), nil)
|
||||
}
|
||||
|
||||
filePath := filepath.Join(config.GetHomeDir(), "uploads", fileUuid)
|
||||
uploadsDir := filepath.Join(config.GetHomeDir(), "uploads")
|
||||
filePath := filepath.Join(uploadsDir, uuid)
|
||||
|
||||
if _, err := os.Stat(filePath); err == nil {
|
||||
if err := os.Remove(filePath); err != nil {
|
||||
|
||||
@@ -1,151 +0,0 @@
|
||||
package gist_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/thomiceli/opengist/internal/config"
|
||||
webtest "github.com/thomiceli/opengist/internal/web/test"
|
||||
)
|
||||
|
||||
func createMultipartRequest(t *testing.T, uri, fieldName, fileName, content string) *http.Request {
|
||||
body := &bytes.Buffer{}
|
||||
writer := multipart.NewWriter(body)
|
||||
part, err := writer.CreateFormFile(fieldName, fileName)
|
||||
require.NoError(t, err)
|
||||
_, err = part.Write([]byte(content))
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, writer.Close())
|
||||
|
||||
req := httptest.NewRequest(http.MethodPost, uri, body)
|
||||
req.Header.Set("Content-Type", writer.FormDataContentType())
|
||||
return req
|
||||
}
|
||||
|
||||
func TestUpload(t *testing.T) {
|
||||
s := webtest.Setup(t)
|
||||
defer webtest.Teardown(t)
|
||||
|
||||
s.Register(t, "thomas")
|
||||
|
||||
t.Run("UploadFile", func(t *testing.T) {
|
||||
s.Login(t, "thomas")
|
||||
|
||||
req := createMultipartRequest(t, "/upload", "file", "test.txt", "file content")
|
||||
|
||||
resp := s.RawRequest(t, req, 200)
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
require.NoError(t, err)
|
||||
|
||||
var result map[string]string
|
||||
err = json.Unmarshal(body, &result)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, "test.txt", result["filename"])
|
||||
require.NotEmpty(t, result["uuid"])
|
||||
require.Regexp(t, `^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$`, result["uuid"])
|
||||
|
||||
filePath := filepath.Join(config.GetHomeDir(), "uploads", result["uuid"])
|
||||
data, err := os.ReadFile(filePath)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "file content", string(data))
|
||||
})
|
||||
|
||||
t.Run("NoFile", func(t *testing.T) {
|
||||
s.Login(t, "thomas")
|
||||
|
||||
req := httptest.NewRequest(http.MethodPost, "/upload", nil)
|
||||
req.Header.Set("Content-Type", "multipart/form-data; boundary=xxx")
|
||||
|
||||
s.RawRequest(t, req, 400)
|
||||
})
|
||||
|
||||
t.Run("NoAuth", func(t *testing.T) {
|
||||
s.Logout()
|
||||
|
||||
req := createMultipartRequest(t, "/upload", "file", "test.txt", "content")
|
||||
|
||||
s.RawRequest(t, req, 302)
|
||||
})
|
||||
}
|
||||
|
||||
func TestDeleteUpload(t *testing.T) {
|
||||
s := webtest.Setup(t)
|
||||
defer webtest.Teardown(t)
|
||||
|
||||
s.Register(t, "thomas")
|
||||
|
||||
t.Run("DeleteExistingFile", func(t *testing.T) {
|
||||
s.Login(t, "thomas")
|
||||
|
||||
req := createMultipartRequest(t, "/upload", "file", "todelete.txt", "delete me")
|
||||
|
||||
uploadResp := s.RawRequest(t, req, 200)
|
||||
|
||||
body, err := io.ReadAll(uploadResp.Body)
|
||||
require.NoError(t, err)
|
||||
var uploadResult map[string]string
|
||||
err = json.Unmarshal(body, &uploadResult)
|
||||
require.NoError(t, err)
|
||||
fileUUID := uploadResult["uuid"]
|
||||
|
||||
filePath := filepath.Join(config.GetHomeDir(), "uploads", fileUUID)
|
||||
_, err = os.Stat(filePath)
|
||||
require.NoError(t, err)
|
||||
|
||||
deleteReq := httptest.NewRequest(http.MethodDelete, "/upload/"+fileUUID, nil)
|
||||
|
||||
deleteResp := s.RawRequest(t, deleteReq, 200)
|
||||
|
||||
deleteBody, err := io.ReadAll(deleteResp.Body)
|
||||
require.NoError(t, err)
|
||||
var deleteResult map[string]string
|
||||
err = json.Unmarshal(deleteBody, &deleteResult)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "deleted", deleteResult["status"])
|
||||
|
||||
_, err = os.Stat(filePath)
|
||||
require.True(t, os.IsNotExist(err))
|
||||
})
|
||||
|
||||
t.Run("DeleteNonExistentFile", func(t *testing.T) {
|
||||
s.Login(t, "thomas")
|
||||
|
||||
req := httptest.NewRequest(http.MethodDelete, "/upload/00000000-0000-0000-0000-000000000000", nil)
|
||||
|
||||
s.RawRequest(t, req, 200)
|
||||
})
|
||||
|
||||
t.Run("InvalidUUID", func(t *testing.T) {
|
||||
s.Login(t, "thomas")
|
||||
|
||||
req := httptest.NewRequest(http.MethodDelete, "/upload/not-a-valid-uuid", nil)
|
||||
|
||||
s.RawRequest(t, req, 400)
|
||||
})
|
||||
|
||||
t.Run("PathTraversal", func(t *testing.T) {
|
||||
s.Login(t, "thomas")
|
||||
|
||||
req := httptest.NewRequest(http.MethodDelete, "/upload/../../etc/passwd", nil)
|
||||
|
||||
s.RawRequest(t, req, 400)
|
||||
})
|
||||
|
||||
t.Run("NoAuth", func(t *testing.T) {
|
||||
s.Logout()
|
||||
|
||||
req := httptest.NewRequest(http.MethodDelete, "/upload/00000000-0000-0000-0000-000000000000", nil)
|
||||
|
||||
s.RawRequest(t, req, 302)
|
||||
})
|
||||
}
|
||||
@@ -1,235 +0,0 @@
|
||||
package git_test
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/thomiceli/opengist/internal/db"
|
||||
webtest "github.com/thomiceli/opengist/internal/web/test"
|
||||
)
|
||||
|
||||
func gitClone(baseUrl, creds, user, gistId, destDir string) error {
|
||||
authUrl := baseUrl
|
||||
if creds != "" {
|
||||
authUrl = "http://" + creds + "@" + baseUrl[len("http://"):]
|
||||
}
|
||||
return exec.Command("git", "clone", authUrl+"/"+user+"/"+gistId+".git", destDir).Run()
|
||||
}
|
||||
|
||||
func gitPush(repoDir, filename, content string) error {
|
||||
if err := os.WriteFile(filepath.Join(repoDir, filename), []byte(content), 0644); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := exec.Command("git", "-C", repoDir, "add", filename).Run(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := exec.Command("git", "-C", repoDir, "commit", "-m", "add "+filename).Run(); err != nil {
|
||||
return err
|
||||
}
|
||||
return exec.Command("git", "-C", repoDir, "push", "origin").Run()
|
||||
}
|
||||
|
||||
func TestGitClonePull(t *testing.T) {
|
||||
s := webtest.Setup(t)
|
||||
defer webtest.Teardown(t)
|
||||
|
||||
baseUrl := s.StartHttpServer(t)
|
||||
|
||||
s.Register(t, "thomas")
|
||||
s.Register(t, "alice")
|
||||
|
||||
_, _, user, publicId := s.CreateGist(t, "0")
|
||||
_, _, _, unlistedId := s.CreateGist(t, "1")
|
||||
_, _, _, privateId := s.CreateGist(t, "2")
|
||||
|
||||
type credTest struct {
|
||||
name string
|
||||
creds string
|
||||
expect [3]bool // [public, unlisted, private]
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
settings map[string]string
|
||||
creds []credTest
|
||||
}{
|
||||
{
|
||||
name: "Default",
|
||||
creds: []credTest{
|
||||
{"OwnerAuth", "thomas:thomas", [3]bool{true, true, true}},
|
||||
{"OtherUserAuth", "alice:alice", [3]bool{true, true, false}},
|
||||
{"WrongPassword", "thomas:wrong", [3]bool{true, true, false}},
|
||||
{"WrongUser", "aze:aze", [3]bool{true, true, false}},
|
||||
{"Anonymous", "", [3]bool{true, true, false}},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "RequireLogin",
|
||||
settings: map[string]string{db.SettingRequireLogin: "1"},
|
||||
creds: []credTest{
|
||||
{"OwnerAuth", "thomas:thomas", [3]bool{true, true, true}},
|
||||
{"OtherUserAuth", "alice:alice", [3]bool{true, true, false}},
|
||||
{"WrongPassword", "thomas:wrong", [3]bool{false, false, false}},
|
||||
{"WrongUser", "aze:aze", [3]bool{false, false, false}},
|
||||
{"Anonymous", "", [3]bool{false, false, false}},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "AllowGistsWithoutLogin",
|
||||
settings: map[string]string{db.SettingRequireLogin: "1", db.SettingAllowGistsWithoutLogin: "1"},
|
||||
creds: []credTest{
|
||||
{"OwnerAuth", "thomas:thomas", [3]bool{true, true, true}},
|
||||
{"OtherUserAuth", "alice:alice", [3]bool{true, true, false}},
|
||||
{"WrongPassword", "thomas:wrong", [3]bool{true, true, false}},
|
||||
{"WrongUser", "aze:aze", [3]bool{true, true, false}},
|
||||
{"Anonymous", "", [3]bool{true, true, false}},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
gists := [3]string{publicId, unlistedId, privateId}
|
||||
labels := [3]string{"Public", "Unlisted", "Private"}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
s.Login(t, "thomas")
|
||||
for k, v := range tt.settings {
|
||||
s.Request(t, "PUT", "/admin-panel/set-config", url.Values{"key": {k}, "value": {v}}, 200)
|
||||
}
|
||||
|
||||
for _, ct := range tt.creds {
|
||||
t.Run(ct.name, func(t *testing.T) {
|
||||
for i, id := range gists {
|
||||
t.Run(labels[i], func(t *testing.T) {
|
||||
dest := t.TempDir()
|
||||
err := gitClone(baseUrl, ct.creds, user, id, dest)
|
||||
if ct.expect[i] {
|
||||
require.NoError(t, err)
|
||||
_, err = os.Stat(filepath.Join(dest, "file.txt"))
|
||||
require.NoError(t, err)
|
||||
} else {
|
||||
require.Error(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Reset settings
|
||||
s.Login(t, "thomas")
|
||||
for k := range tt.settings {
|
||||
s.Request(t, "PUT", "/admin-panel/set-config", url.Values{"key": {k}, "value": {"0"}}, 200)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGitPush(t *testing.T) {
|
||||
s := webtest.Setup(t)
|
||||
defer webtest.Teardown(t)
|
||||
|
||||
baseUrl := s.StartHttpServer(t)
|
||||
|
||||
s.Register(t, "thomas")
|
||||
s.Register(t, "alice")
|
||||
|
||||
_, _, user, publicId := s.CreateGist(t, "0")
|
||||
_, _, _, unlistedId := s.CreateGist(t, "1")
|
||||
_, _, _, privateId := s.CreateGist(t, "2")
|
||||
|
||||
type credTest struct {
|
||||
name string
|
||||
creds string
|
||||
expect [3]bool // [public, unlisted, private]
|
||||
}
|
||||
|
||||
tests := []credTest{
|
||||
{"OwnerAuth", "thomas:thomas", [3]bool{true, true, true}},
|
||||
{"OtherUserAuth", "alice:alice", [3]bool{false, false, false}},
|
||||
{"WrongPassword", "thomas:wrong", [3]bool{false, false, false}},
|
||||
{"WrongUser", "aze:aze", [3]bool{false, false, false}},
|
||||
{"Anonymous", "", [3]bool{false, false, false}},
|
||||
}
|
||||
|
||||
gists := [3]string{publicId, unlistedId, privateId}
|
||||
labels := [3]string{"Public", "Unlisted", "Private"}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
for i, id := range gists {
|
||||
t.Run(labels[i], func(t *testing.T) {
|
||||
dest := t.TempDir()
|
||||
require.NoError(t, gitClone(baseUrl, "thomas:thomas", user, id, dest))
|
||||
|
||||
if tt.creds != "thomas:thomas" {
|
||||
require.NoError(t, exec.Command("git", "-C", dest, "remote", "set-url", "origin",
|
||||
"http://"+tt.creds+"@"+baseUrl[len("http://"):]+"/"+user+"/"+id+".git").Run())
|
||||
}
|
||||
|
||||
err := gitPush(dest, "newfile.txt", "new content")
|
||||
if tt.expect[i] {
|
||||
require.NoError(t, err)
|
||||
} else {
|
||||
require.Error(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGitCreatePush(t *testing.T) {
|
||||
s := webtest.Setup(t)
|
||||
defer webtest.Teardown(t)
|
||||
|
||||
baseUrl := s.StartHttpServer(t)
|
||||
|
||||
s.Register(t, "thomas")
|
||||
s.Register(t, "alice")
|
||||
|
||||
gitInitAndPush := func(t *testing.T, creds, remoteUrl string) error {
|
||||
dest := t.TempDir()
|
||||
require.NoError(t, exec.Command("git", "init", "--initial-branch=master", dest).Run())
|
||||
require.NoError(t, exec.Command("git", "-C", dest, "remote", "add", "origin",
|
||||
"http://"+creds+"@"+baseUrl[len("http://"):]+remoteUrl).Run())
|
||||
|
||||
require.NoError(t, os.WriteFile(filepath.Join(dest, "hello.txt"), []byte("hello"), 0644))
|
||||
require.NoError(t, exec.Command("git", "-C", dest, "add", "hello.txt").Run())
|
||||
require.NoError(t, exec.Command("git", "-C", dest, "commit", "-m", "initial").Run())
|
||||
return exec.Command("git", "-C", dest, "push", "origin").Run()
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
creds string
|
||||
url string
|
||||
expect bool
|
||||
gistOwner string // if expect=true, verify gist exists at this owner/identifier
|
||||
gistId string
|
||||
}{
|
||||
{"OwnerCreates", "thomas:thomas", "/thomas/mygist.git", true, "thomas", "mygist"},
|
||||
{"OtherUserCreatesOnOwnUrl", "alice:alice", "/alice/alicegist.git", true, "alice", "alicegist"},
|
||||
{"WrongPassword", "thomas:wrong", "/thomas/newgist.git", false, "", ""},
|
||||
{"OtherUserCannotCreateOnOwner", "alice:alice", "/thomas/hackgist.git", false, "", ""},
|
||||
{"WrongUser", "aze:aze", "/aze/azegist.git", false, "", ""},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := gitInitAndPush(t, tt.creds, tt.url)
|
||||
if tt.expect {
|
||||
require.NoError(t, err)
|
||||
gist, err := db.GetGist(tt.gistOwner, tt.gistId)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, gist)
|
||||
require.Equal(t, tt.gistId, gist.Identifier())
|
||||
} else {
|
||||
require.Error(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
package health_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
webtest "github.com/thomiceli/opengist/internal/web/test"
|
||||
)
|
||||
|
||||
func TestHealthcheck(t *testing.T) {
|
||||
s := webtest.Setup(t)
|
||||
defer webtest.Teardown(t)
|
||||
|
||||
t.Run("OK", func(t *testing.T) {
|
||||
resp := s.Request(t, "GET", "/healthcheck", nil, 200)
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
require.NoError(t, err)
|
||||
|
||||
var result map[string]interface{}
|
||||
err = json.Unmarshal(body, &result)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, "ok", result["opengist"])
|
||||
require.Equal(t, "ok", result["database"])
|
||||
require.NotEmpty(t, result["time"])
|
||||
})
|
||||
}
|
||||
@@ -1,332 +0,0 @@
|
||||
package settings_test
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/thomiceli/opengist/internal/db"
|
||||
webtest "github.com/thomiceli/opengist/internal/web/test"
|
||||
)
|
||||
|
||||
func TestAccessTokensCRUD(t *testing.T) {
|
||||
s := webtest.Setup(t)
|
||||
defer webtest.Teardown(t)
|
||||
|
||||
s.Register(t, "thomas")
|
||||
|
||||
t.Run("RequiresAuth", func(t *testing.T) {
|
||||
s.Logout()
|
||||
s.Request(t, "GET", "/settings/access-tokens", nil, 302)
|
||||
})
|
||||
|
||||
t.Run("AccessTokensPage", func(t *testing.T) {
|
||||
s.Login(t, "thomas")
|
||||
s.Request(t, "GET", "/settings/access-tokens", nil, 200)
|
||||
})
|
||||
|
||||
t.Run("CreateReadToken", func(t *testing.T) {
|
||||
s.Login(t, "thomas")
|
||||
s.Request(t, "POST", "/settings/access-tokens", db.AccessTokenDTO{
|
||||
Name: "test-token",
|
||||
ScopeGist: db.ReadPermission,
|
||||
}, 302)
|
||||
|
||||
tokens, err := db.GetAccessTokensByUserID(1)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, tokens, 1)
|
||||
require.Equal(t, "test-token", tokens[0].Name)
|
||||
require.Equal(t, uint(db.ReadPermission), tokens[0].ScopeGist)
|
||||
require.Equal(t, int64(0), tokens[0].ExpiresAt)
|
||||
})
|
||||
|
||||
t.Run("CreateExpiringToken", func(t *testing.T) {
|
||||
s.Login(t, "thomas")
|
||||
tomorrow := time.Now().AddDate(0, 0, 1).Format("2006-01-02")
|
||||
s.Request(t, "POST", "/settings/access-tokens", db.AccessTokenDTO{
|
||||
Name: "expiring-token",
|
||||
ScopeGist: db.ReadWritePermission,
|
||||
ExpiresAt: tomorrow,
|
||||
}, 302)
|
||||
|
||||
tokens, err := db.GetAccessTokensByUserID(1)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, tokens, 2)
|
||||
})
|
||||
|
||||
t.Run("DeleteToken", func(t *testing.T) {
|
||||
s.Login(t, "thomas")
|
||||
s.Request(t, "DELETE", "/settings/access-tokens/1", nil, 302)
|
||||
|
||||
tokens, err := db.GetAccessTokensByUserID(1)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, tokens, 1)
|
||||
require.Equal(t, "expiring-token", tokens[0].Name)
|
||||
})
|
||||
}
|
||||
|
||||
func TestAccessTokenPrivateGistAccess(t *testing.T) {
|
||||
s := webtest.Setup(t)
|
||||
defer webtest.Teardown(t)
|
||||
|
||||
s.Register(t, "thomas")
|
||||
_, _, user, identifier := s.CreateGist(t, "2")
|
||||
|
||||
// Create access token with read permission
|
||||
token := &db.AccessToken{
|
||||
Name: "read-token",
|
||||
UserID: 1,
|
||||
ScopeGist: db.ReadPermission,
|
||||
}
|
||||
plainToken, err := token.GenerateToken()
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, token.Create())
|
||||
|
||||
s.Logout()
|
||||
headers := map[string]string{"Authorization": "Token " + plainToken}
|
||||
|
||||
t.Run("NoTokenReturns404", func(t *testing.T) {
|
||||
s.Request(t, "GET", "/"+user+"/"+identifier, nil, 404)
|
||||
})
|
||||
|
||||
t.Run("ValidTokenGrantsAccess", func(t *testing.T) {
|
||||
s.RequestWithHeaders(t, "GET", "/"+user+"/"+identifier, nil, 200, headers)
|
||||
})
|
||||
|
||||
t.Run("RawContentAccessible", func(t *testing.T) {
|
||||
s.RequestWithHeaders(t, "GET", "/"+user+"/"+identifier+"/raw/HEAD/file.txt", nil, 200, headers)
|
||||
})
|
||||
|
||||
t.Run("JSONEndpointAccessible", func(t *testing.T) {
|
||||
s.RequestWithHeaders(t, "GET", "/"+user+"/"+identifier+".json", nil, 200, headers)
|
||||
})
|
||||
|
||||
t.Run("InvalidTokenReturns404", func(t *testing.T) {
|
||||
s.RequestWithHeaders(t, "GET", "/"+user+"/"+identifier, nil, 404, map[string]string{
|
||||
"Authorization": "Token invalid_token",
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestAccessTokenPermissions(t *testing.T) {
|
||||
s := webtest.Setup(t)
|
||||
defer webtest.Teardown(t)
|
||||
|
||||
s.Register(t, "thomas")
|
||||
_, _, user, identifier := s.CreateGist(t, "2")
|
||||
|
||||
// Create token with NO permission
|
||||
noPermToken := &db.AccessToken{
|
||||
Name: "no-perm-token",
|
||||
UserID: 1,
|
||||
ScopeGist: db.NoPermission,
|
||||
}
|
||||
noPermPlain, err := noPermToken.GenerateToken()
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, noPermToken.Create())
|
||||
|
||||
// Create token with READ permission
|
||||
readToken := &db.AccessToken{
|
||||
Name: "read-token",
|
||||
UserID: 1,
|
||||
ScopeGist: db.ReadPermission,
|
||||
}
|
||||
readPlain, err := readToken.GenerateToken()
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, readToken.Create())
|
||||
|
||||
s.Logout()
|
||||
|
||||
t.Run("NoPermissionDenied", func(t *testing.T) {
|
||||
s.RequestWithHeaders(t, "GET", "/"+user+"/"+identifier, nil, 404, map[string]string{
|
||||
"Authorization": "Token " + noPermPlain,
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("ReadPermissionGranted", func(t *testing.T) {
|
||||
s.RequestWithHeaders(t, "GET", "/"+user+"/"+identifier, nil, 200, map[string]string{
|
||||
"Authorization": "Token " + readPlain,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestAccessTokenExpiration(t *testing.T) {
|
||||
s := webtest.Setup(t)
|
||||
defer webtest.Teardown(t)
|
||||
|
||||
s.Register(t, "thomas")
|
||||
_, _, user, identifier := s.CreateGist(t, "2")
|
||||
|
||||
// Create an expired token
|
||||
expiredToken := &db.AccessToken{
|
||||
Name: "expired-token",
|
||||
UserID: 1,
|
||||
ScopeGist: db.ReadPermission,
|
||||
ExpiresAt: time.Now().Add(-24 * time.Hour).Unix(),
|
||||
}
|
||||
expiredPlain, err := expiredToken.GenerateToken()
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, expiredToken.Create())
|
||||
|
||||
// Create a valid token
|
||||
validToken := &db.AccessToken{
|
||||
Name: "valid-token",
|
||||
UserID: 1,
|
||||
ScopeGist: db.ReadPermission,
|
||||
ExpiresAt: time.Now().Add(24 * time.Hour).Unix(),
|
||||
}
|
||||
validPlain, err := validToken.GenerateToken()
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, validToken.Create())
|
||||
|
||||
s.Logout()
|
||||
|
||||
t.Run("ExpiredTokenDenied", func(t *testing.T) {
|
||||
s.RequestWithHeaders(t, "GET", "/"+user+"/"+identifier, nil, 404, map[string]string{
|
||||
"Authorization": "Token " + expiredPlain,
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("ValidTokenGranted", func(t *testing.T) {
|
||||
s.RequestWithHeaders(t, "GET", "/"+user+"/"+identifier, nil, 200, map[string]string{
|
||||
"Authorization": "Token " + validPlain,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestAccessTokenWrongUser(t *testing.T) {
|
||||
s := webtest.Setup(t)
|
||||
defer webtest.Teardown(t)
|
||||
|
||||
s.Register(t, "thomas")
|
||||
s.Register(t, "kaguya")
|
||||
|
||||
_, _, user, identifier := s.CreateGist(t, "2")
|
||||
|
||||
// Create token for kaguya
|
||||
kaguyaToken := &db.AccessToken{
|
||||
Name: "kaguya-token",
|
||||
UserID: 2,
|
||||
ScopeGist: db.ReadPermission,
|
||||
}
|
||||
kaguyaPlain, err := kaguyaToken.GenerateToken()
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, kaguyaToken.Create())
|
||||
|
||||
// Create token for thomas
|
||||
thomasToken := &db.AccessToken{
|
||||
Name: "thomas-token",
|
||||
UserID: 1,
|
||||
ScopeGist: db.ReadPermission,
|
||||
}
|
||||
thomasPlain, err := thomasToken.GenerateToken()
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, thomasToken.Create())
|
||||
|
||||
s.Logout()
|
||||
|
||||
t.Run("OtherUserTokenDenied", func(t *testing.T) {
|
||||
s.RequestWithHeaders(t, "GET", "/"+user+"/"+identifier, nil, 404, map[string]string{
|
||||
"Authorization": "Token " + kaguyaPlain,
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("OwnerTokenGranted", func(t *testing.T) {
|
||||
s.RequestWithHeaders(t, "GET", "/"+user+"/"+identifier, nil, 200, map[string]string{
|
||||
"Authorization": "Token " + thomasPlain,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestAccessTokenLastUsedUpdate(t *testing.T) {
|
||||
s := webtest.Setup(t)
|
||||
defer webtest.Teardown(t)
|
||||
|
||||
s.Register(t, "thomas")
|
||||
_, _, user, identifier := s.CreateGist(t, "2")
|
||||
|
||||
token := &db.AccessToken{
|
||||
Name: "test-token",
|
||||
UserID: 1,
|
||||
ScopeGist: db.ReadPermission,
|
||||
}
|
||||
plainToken, err := token.GenerateToken()
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, token.Create())
|
||||
|
||||
// Verify LastUsedAt is 0 initially
|
||||
tokenFromDB, err := db.GetAccessTokenByID(token.ID)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, int64(0), tokenFromDB.LastUsedAt)
|
||||
|
||||
s.Logout()
|
||||
|
||||
// Use the token
|
||||
s.RequestWithHeaders(t, "GET", "/"+user+"/"+identifier, nil, 200, map[string]string{
|
||||
"Authorization": "Token " + plainToken,
|
||||
})
|
||||
|
||||
// Verify LastUsedAt was updated
|
||||
tokenFromDB, err = db.GetAccessTokenByID(token.ID)
|
||||
require.NoError(t, err)
|
||||
require.NotEqual(t, int64(0), tokenFromDB.LastUsedAt)
|
||||
}
|
||||
|
||||
func TestAccessTokenWithRequireLogin(t *testing.T) {
|
||||
s := webtest.Setup(t)
|
||||
defer webtest.Teardown(t)
|
||||
|
||||
s.Register(t, "thomas")
|
||||
_, _, user1, identifier1 := s.CreateGist(t, "2")
|
||||
|
||||
s.Login(t, "thomas")
|
||||
_, _, user2, identifier2 := s.CreateGist(t, "0")
|
||||
|
||||
s.Login(t, "thomas")
|
||||
token := &db.AccessToken{
|
||||
Name: "read-token",
|
||||
UserID: 1,
|
||||
ScopeGist: db.ReadPermission,
|
||||
}
|
||||
plainToken, err := token.GenerateToken()
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, token.Create())
|
||||
|
||||
s.Request(t, "PUT", "/admin-panel/set-config", url.Values{"key": {db.SettingRequireLogin}, "value": {"1"}}, 200)
|
||||
s.Logout()
|
||||
|
||||
headers := map[string]string{"Authorization": "Token " + plainToken}
|
||||
|
||||
t.Run("UnauthenticatedRedirects", func(t *testing.T) {
|
||||
s.Request(t, "GET", "/"+user1+"/"+identifier1, nil, 302)
|
||||
s.Request(t, "GET", "/"+user2+"/"+identifier2, nil, 302)
|
||||
})
|
||||
|
||||
t.Run("ValidTokenGrantsAccess", func(t *testing.T) {
|
||||
s.RequestWithHeaders(t, "GET", "/"+user1+"/"+identifier1, nil, 200, headers)
|
||||
s.RequestWithHeaders(t, "GET", "/"+user2+"/"+identifier2, nil, 200, headers)
|
||||
s.RequestWithHeaders(t, "GET", "/"+user1+"/"+identifier1+"/raw/HEAD/file.txt", nil, 200, headers)
|
||||
})
|
||||
|
||||
t.Run("InvalidTokenRedirects", func(t *testing.T) {
|
||||
s.RequestWithHeaders(t, "GET", "/"+user1+"/"+identifier1, nil, 302, map[string]string{
|
||||
"Authorization": "Token invalid_token",
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("NoPermTokenRedirects", func(t *testing.T) {
|
||||
noPermToken := &db.AccessToken{
|
||||
Name: "no-perm-token",
|
||||
UserID: 1,
|
||||
ScopeGist: db.NoPermission,
|
||||
}
|
||||
noPermPlain, err := noPermToken.GenerateToken()
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, noPermToken.Create())
|
||||
|
||||
s.RequestWithHeaders(t, "GET", "/"+user1+"/"+identifier1, nil, 302, map[string]string{
|
||||
"Authorization": "Token " + noPermPlain,
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -3,17 +3,16 @@ package settings
|
||||
import (
|
||||
"crypto/md5"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/thomiceli/opengist/internal/config"
|
||||
"github.com/thomiceli/opengist/internal/db"
|
||||
"github.com/thomiceli/opengist/internal/git"
|
||||
"github.com/thomiceli/opengist/internal/i18n"
|
||||
"github.com/thomiceli/opengist/internal/validator"
|
||||
"github.com/thomiceli/opengist/internal/web/context"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
func EmailProcess(ctx *context.Context) error {
|
||||
@@ -62,22 +61,18 @@ func UsernameProcess(ctx *context.Context) error {
|
||||
return ctx.RedirectTo("/settings")
|
||||
}
|
||||
|
||||
if !strings.EqualFold(dto.Username, user.Username) {
|
||||
if exists, err := db.UserExists(dto.Username); err != nil || exists {
|
||||
ctx.AddFlash(ctx.Tr("flash.auth.username-exists"), "error")
|
||||
return ctx.RedirectTo("/settings")
|
||||
}
|
||||
if exists, err := db.UserExists(dto.Username); err != nil || exists {
|
||||
ctx.AddFlash(ctx.Tr("flash.auth.username-exists"), "error")
|
||||
return ctx.RedirectTo("/settings")
|
||||
}
|
||||
|
||||
sourceDir := filepath.Join(config.GetHomeDir(), git.ReposDirectory, strings.ToLower(user.Username))
|
||||
destinationDir := filepath.Join(config.GetHomeDir(), git.ReposDirectory, strings.ToLower(dto.Username))
|
||||
|
||||
if sourceDir != destinationDir {
|
||||
if _, err := os.Stat(sourceDir); !os.IsNotExist(err) {
|
||||
err := os.Rename(sourceDir, destinationDir)
|
||||
if err != nil {
|
||||
return ctx.ErrorRes(500, "Cannot rename user directory", err)
|
||||
}
|
||||
if _, err := os.Stat(sourceDir); !os.IsNotExist(err) {
|
||||
err := os.Rename(sourceDir, destinationDir)
|
||||
if err != nil {
|
||||
return ctx.ErrorRes(500, "Cannot rename user directory", err)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/gorilla/schema"
|
||||
"github.com/rs/zerolog/log"
|
||||
|
||||
"github.com/thomiceli/opengist/internal/web/context"
|
||||
)
|
||||
|
||||
@@ -119,16 +119,10 @@ func Paginate[T any](ctx *context.Context, data []*T, pageInt int, perPage int,
|
||||
return nil
|
||||
}
|
||||
|
||||
// ParseSearchQueryStr parses a search query string and returns a map of metadata.
|
||||
// The query string is split into words and each word is checked if it contains a colon (:).
|
||||
// If a word contains a colon, it is split into a key-value pair and added to the metadata map.
|
||||
// If a word does not contain a colon, it is added to an "all" key in the metadata map.
|
||||
// The "all" key is used to search all fields in the index.
|
||||
// The function returns the metadata map.
|
||||
func ParseSearchQueryStr(query string) map[string]string {
|
||||
func ParseSearchQueryStr(query string) (string, map[string]string) {
|
||||
words := strings.Fields(query)
|
||||
metadata := make(map[string]string)
|
||||
var allFieldsBuilder strings.Builder
|
||||
var contentBuilder strings.Builder
|
||||
|
||||
for _, word := range words {
|
||||
if strings.Contains(word, ":") {
|
||||
@@ -139,18 +133,10 @@ func ParseSearchQueryStr(query string) map[string]string {
|
||||
metadata[key] = value
|
||||
}
|
||||
} else {
|
||||
// Add to content search by default
|
||||
allFieldsBuilder.WriteString(word + " ")
|
||||
contentBuilder.WriteString(word + " ")
|
||||
}
|
||||
}
|
||||
|
||||
// Set the default search field
|
||||
allContent := strings.TrimSpace(allFieldsBuilder.String())
|
||||
if allContent != "" {
|
||||
metadata["default"] = allContent
|
||||
}
|
||||
|
||||
log.Debug().Msgf("Metadata: %v", metadata)
|
||||
|
||||
return metadata
|
||||
content := strings.TrimSpace(contentBuilder.String())
|
||||
return content, metadata
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ import (
|
||||
func (s *Server) useCustomContext() {
|
||||
s.echo.Use(func(next echo.HandlerFunc) echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
cc := context.NewContext(c, filepath.Join(config.GetHomeDir(), "sessions"))
|
||||
cc := context.NewContext(c, s.sessionsPath)
|
||||
return next(cc)
|
||||
}
|
||||
})
|
||||
@@ -58,27 +58,29 @@ func (s *Server) registerMiddlewares() {
|
||||
s.echo.Use(middleware.Recover())
|
||||
s.echo.Use(middleware.Secure())
|
||||
s.echo.Use(Middleware(sessionInit).toEcho())
|
||||
s.echo.Use(middleware.CSRFWithConfig(middleware.CSRFConfig{
|
||||
TokenLookup: "form:_csrf,header:X-CSRF-Token",
|
||||
CookiePath: "/",
|
||||
CookieHTTPOnly: true,
|
||||
CookieSameSite: http.SameSiteStrictMode,
|
||||
Skipper: func(ctx echo.Context) bool {
|
||||
/* skip CSRF for embeds */
|
||||
gistName := ctx.Param("gistname")
|
||||
|
||||
/* skip CSRF for git clients */
|
||||
matchUploadPack, _ := regexp.MatchString("(.*?)/git-upload-pack$", ctx.Request().URL.Path)
|
||||
matchReceivePack, _ := regexp.MatchString("(.*?)/git-receive-pack$", ctx.Request().URL.Path)
|
||||
return (filepath.Ext(gistName) == ".js" && ctx.Request().Method == "GET") || matchUploadPack || matchReceivePack
|
||||
},
|
||||
ErrorHandler: func(err error, c echo.Context) error {
|
||||
log.Info().Err(err).Msg("CSRF error")
|
||||
return err
|
||||
},
|
||||
}))
|
||||
s.echo.Use(Middleware(csrfInit).toEcho())
|
||||
if !s.ignoreCsrf {
|
||||
s.echo.Use(middleware.CSRFWithConfig(middleware.CSRFConfig{
|
||||
TokenLookup: "form:_csrf,header:X-CSRF-Token",
|
||||
CookiePath: "/",
|
||||
CookieHTTPOnly: true,
|
||||
CookieSameSite: http.SameSiteStrictMode,
|
||||
Skipper: func(ctx echo.Context) bool {
|
||||
/* skip CSRF for embeds */
|
||||
gistName := ctx.Param("gistname")
|
||||
|
||||
/* skip CSRF for git clients */
|
||||
matchUploadPack, _ := regexp.MatchString("(.*?)/git-upload-pack$", ctx.Request().URL.Path)
|
||||
matchReceivePack, _ := regexp.MatchString("(.*?)/git-receive-pack$", ctx.Request().URL.Path)
|
||||
return filepath.Ext(gistName) == ".js" || matchUploadPack || matchReceivePack
|
||||
},
|
||||
ErrorHandler: func(err error, c echo.Context) error {
|
||||
log.Info().Err(err).Msg("CSRF error")
|
||||
return err
|
||||
},
|
||||
}))
|
||||
s.echo.Use(Middleware(csrfInit).toEcho())
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) errorHandler(err error, ctx echo.Context) {
|
||||
@@ -157,10 +159,10 @@ func dataInit(next Handler) Handler {
|
||||
|
||||
func writePermission(next Handler) Handler {
|
||||
return func(ctx *context.Context) error {
|
||||
gist := ctx.GetData("gist").(*db.Gist)
|
||||
gist := ctx.GetData("gist")
|
||||
user := ctx.User
|
||||
if !gist.CanWrite(user) {
|
||||
return ctx.ErrorRes(403, "You don't have permission to edit this gist", nil)
|
||||
if !gist.(*db.Gist).CanWrite(user) {
|
||||
return ctx.RedirectTo("/" + gist.(*db.Gist).User.Username + "/" + gist.(*db.Gist).Identifier())
|
||||
}
|
||||
return next(ctx)
|
||||
}
|
||||
@@ -197,17 +199,6 @@ func inMFASession(next Handler) Handler {
|
||||
}
|
||||
}
|
||||
|
||||
func inOAuthRegisterSession(next Handler) Handler {
|
||||
return func(ctx *context.Context) error {
|
||||
sess := ctx.GetSession()
|
||||
_, ok := sess.Values["oauthProvider"].(string)
|
||||
if !ok {
|
||||
return ctx.RedirectTo("/login")
|
||||
}
|
||||
return next(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
func makeCheckRequireLogin(isSingleGistAccess bool) Middleware {
|
||||
return func(next Handler) Handler {
|
||||
return func(ctx *context.Context) error {
|
||||
@@ -318,6 +309,7 @@ func csrfInit(next Handler) Handler {
|
||||
csrf = csrfToken
|
||||
}
|
||||
ctx.SetData("csrfHtml", template.HTML(`<input type="hidden" name="_csrf" value="`+csrf+`">`))
|
||||
ctx.SetData("csrfHtml", template.HTML(`<input type="hidden" name="_csrf" value="`+csrf+`">`))
|
||||
|
||||
return next(ctx)
|
||||
}
|
||||
|
||||
@@ -147,10 +147,7 @@ func (s *Server) setFuncMap() {
|
||||
return dict, nil
|
||||
},
|
||||
"addMetadataToSearchQuery": func(input, key, value string) string {
|
||||
metadata := handlers.ParseSearchQueryStr(input)
|
||||
// extract free-text content (stored under "all") and remove it from metadata
|
||||
content := metadata["all"]
|
||||
delete(metadata, "all")
|
||||
content, metadata := handlers.ParseSearchQueryStr(input)
|
||||
|
||||
metadata[key] = value
|
||||
|
||||
|
||||
@@ -38,8 +38,6 @@ func (s *Server) registerRoutes() {
|
||||
r.GET("/login", auth.Login)
|
||||
r.POST("/login", auth.ProcessLogin)
|
||||
r.GET("/logout", auth.Logout)
|
||||
r.GET("/oauth/register", auth.OauthRegister, inOAuthRegisterSession)
|
||||
r.POST("/oauth/register", auth.ProcessOauthRegister, inOAuthRegisterSession)
|
||||
r.GET("/oauth/:provider", auth.Oauth)
|
||||
r.GET("/oauth/:provider/callback", auth.OauthCallback)
|
||||
r.GET("/oauth/:provider/unlink", auth.OauthUnlink, logged)
|
||||
|
||||
@@ -2,6 +2,7 @@ package server
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/thomiceli/opengist/internal/validator"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
@@ -9,8 +10,6 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/thomiceli/opengist/internal/validator"
|
||||
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/thomiceli/opengist/internal/config"
|
||||
@@ -19,16 +18,19 @@ import (
|
||||
|
||||
type Server struct {
|
||||
echo *echo.Echo
|
||||
dev bool
|
||||
|
||||
dev bool
|
||||
sessionsPath string
|
||||
ignoreCsrf bool
|
||||
}
|
||||
|
||||
func NewServer(isDev bool) *Server {
|
||||
func NewServer(isDev bool, sessionsPath string, ignoreCsrf bool) *Server {
|
||||
e := echo.New()
|
||||
e.HideBanner = true
|
||||
e.HidePort = true
|
||||
e.Validator = validator.NewValidator()
|
||||
|
||||
s := &Server{echo: e, dev: isDev}
|
||||
s := &Server{echo: e, dev: isDev, sessionsPath: sessionsPath, ignoreCsrf: ignoreCsrf}
|
||||
|
||||
s.useCustomContext()
|
||||
|
||||
@@ -173,7 +175,3 @@ func (s *Server) createPidFile(pidPath string) error {
|
||||
func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
s.echo.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
func (s *Server) Use(middleware echo.MiddlewareFunc) {
|
||||
s.echo.Use(middleware)
|
||||
}
|
||||
|
||||
448
internal/web/test/access_token_test.go
Normal file
448
internal/web/test/access_token_test.go
Normal file
@@ -0,0 +1,448 @@
|
||||
package test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/thomiceli/opengist/internal/db"
|
||||
)
|
||||
|
||||
func TestAccessTokensCRUD(t *testing.T) {
|
||||
s := Setup(t)
|
||||
defer Teardown(t, s)
|
||||
|
||||
// Register and login
|
||||
user1 := db.UserDTO{Username: "thomas", Password: "thomas"}
|
||||
register(t, s, user1)
|
||||
|
||||
// Access tokens page requires login
|
||||
s.sessionCookie = ""
|
||||
err := s.Request("GET", "/settings/access-tokens", nil, 302)
|
||||
require.NoError(t, err)
|
||||
|
||||
login(t, s, user1)
|
||||
|
||||
// Access tokens page
|
||||
err = s.Request("GET", "/settings/access-tokens", nil, 200)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create a token with read permission
|
||||
tokenDTO := db.AccessTokenDTO{
|
||||
Name: "test-token",
|
||||
ScopeGist: db.ReadPermission,
|
||||
}
|
||||
err = s.Request("POST", "/settings/access-tokens", tokenDTO, 302)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify token was created in database
|
||||
tokens, err := db.GetAccessTokensByUserID(1)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, tokens, 1)
|
||||
require.Equal(t, "test-token", tokens[0].Name)
|
||||
require.Equal(t, uint(db.ReadPermission), tokens[0].ScopeGist)
|
||||
require.Equal(t, int64(0), tokens[0].ExpiresAt)
|
||||
|
||||
// Create another token with expiration
|
||||
tomorrow := time.Now().AddDate(0, 0, 1).Format("2006-01-02")
|
||||
tokenDTO2 := db.AccessTokenDTO{
|
||||
Name: "expiring-token",
|
||||
ScopeGist: db.ReadWritePermission,
|
||||
ExpiresAt: tomorrow,
|
||||
}
|
||||
err = s.Request("POST", "/settings/access-tokens", tokenDTO2, 302)
|
||||
require.NoError(t, err)
|
||||
|
||||
tokens, err = db.GetAccessTokensByUserID(1)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, tokens, 2)
|
||||
|
||||
// Delete the first token
|
||||
err = s.Request("DELETE", "/settings/access-tokens/1", nil, 302)
|
||||
require.NoError(t, err)
|
||||
|
||||
tokens, err = db.GetAccessTokensByUserID(1)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, tokens, 1)
|
||||
require.Equal(t, "expiring-token", tokens[0].Name)
|
||||
}
|
||||
|
||||
func TestAccessTokenPrivateGistAccess(t *testing.T) {
|
||||
s := Setup(t)
|
||||
defer Teardown(t, s)
|
||||
|
||||
// Register user and create a private gist
|
||||
user1 := db.UserDTO{Username: "thomas", Password: "thomas"}
|
||||
register(t, s, user1)
|
||||
|
||||
gist1 := db.GistDTO{
|
||||
Title: "private-gist",
|
||||
Description: "my private gist",
|
||||
VisibilityDTO: db.VisibilityDTO{
|
||||
Private: db.PrivateVisibility,
|
||||
},
|
||||
Name: []string{"secret.txt"},
|
||||
Content: []string{"secret content"},
|
||||
}
|
||||
err := s.Request("POST", "/", gist1, 302)
|
||||
require.NoError(t, err)
|
||||
|
||||
gist1db, err := db.GetGistByID("1")
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create access token with read permission
|
||||
token := &db.AccessToken{
|
||||
Name: "read-token",
|
||||
UserID: 1,
|
||||
ScopeGist: db.ReadPermission,
|
||||
}
|
||||
plainToken, err := token.GenerateToken()
|
||||
require.NoError(t, err)
|
||||
err = token.Create()
|
||||
require.NoError(t, err)
|
||||
|
||||
// Clear session - simulate unauthenticated request
|
||||
s.sessionCookie = ""
|
||||
|
||||
// Without token, private gist should return 404
|
||||
err = s.Request("GET", "/thomas/"+gist1db.Uuid, nil, 404)
|
||||
require.NoError(t, err)
|
||||
|
||||
// With valid token, private gist should be accessible
|
||||
headers := map[string]string{"Authorization": "Token " + plainToken}
|
||||
err = s.RequestWithHeaders("GET", "/thomas/"+gist1db.Uuid, nil, 200, headers)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Raw content should also be accessible with token
|
||||
err = s.RequestWithHeaders("GET", "/thomas/"+gist1db.Uuid+"/raw/HEAD/secret.txt", nil, 200, headers)
|
||||
require.NoError(t, err)
|
||||
|
||||
// JSON endpoint should also be accessible with token
|
||||
err = s.RequestWithHeaders("GET", "/thomas/"+gist1db.Uuid+".json", nil, 200, headers)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Invalid token should not work
|
||||
invalidHeaders := map[string]string{"Authorization": "Token invalid_token"}
|
||||
err = s.RequestWithHeaders("GET", "/thomas/"+gist1db.Uuid, nil, 404, invalidHeaders)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestAccessTokenPermissions(t *testing.T) {
|
||||
s := Setup(t)
|
||||
defer Teardown(t, s)
|
||||
|
||||
// Register user and create a private gist
|
||||
user1 := db.UserDTO{Username: "thomas", Password: "thomas"}
|
||||
register(t, s, user1)
|
||||
|
||||
gist1 := db.GistDTO{
|
||||
Title: "private-gist",
|
||||
Description: "my private gist",
|
||||
VisibilityDTO: db.VisibilityDTO{
|
||||
Private: db.PrivateVisibility,
|
||||
},
|
||||
Name: []string{"file.txt"},
|
||||
Content: []string{"content"},
|
||||
}
|
||||
err := s.Request("POST", "/", gist1, 302)
|
||||
require.NoError(t, err)
|
||||
|
||||
gist1db, err := db.GetGistByID("1")
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create token with NO permission
|
||||
noPermToken := &db.AccessToken{
|
||||
Name: "no-perm-token",
|
||||
UserID: 1,
|
||||
ScopeGist: db.NoPermission,
|
||||
}
|
||||
noPermPlain, err := noPermToken.GenerateToken()
|
||||
require.NoError(t, err)
|
||||
err = noPermToken.Create()
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create token with READ permission
|
||||
readToken := &db.AccessToken{
|
||||
Name: "read-token",
|
||||
UserID: 1,
|
||||
ScopeGist: db.ReadPermission,
|
||||
}
|
||||
readPlain, err := readToken.GenerateToken()
|
||||
require.NoError(t, err)
|
||||
err = readToken.Create()
|
||||
require.NoError(t, err)
|
||||
|
||||
s.sessionCookie = ""
|
||||
|
||||
// No permission token should not grant access
|
||||
noPermHeaders := map[string]string{"Authorization": "Token " + noPermPlain}
|
||||
err = s.RequestWithHeaders("GET", "/thomas/"+gist1db.Uuid, nil, 404, noPermHeaders)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Read permission token should grant access
|
||||
readHeaders := map[string]string{"Authorization": "Token " + readPlain}
|
||||
err = s.RequestWithHeaders("GET", "/thomas/"+gist1db.Uuid, nil, 200, readHeaders)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestAccessTokenExpiration(t *testing.T) {
|
||||
s := Setup(t)
|
||||
defer Teardown(t, s)
|
||||
|
||||
// Register user and create a private gist
|
||||
user1 := db.UserDTO{Username: "thomas", Password: "thomas"}
|
||||
register(t, s, user1)
|
||||
|
||||
gist1 := db.GistDTO{
|
||||
Title: "private-gist",
|
||||
Description: "my private gist",
|
||||
VisibilityDTO: db.VisibilityDTO{
|
||||
Private: db.PrivateVisibility,
|
||||
},
|
||||
Name: []string{"file.txt"},
|
||||
Content: []string{"content"},
|
||||
}
|
||||
err := s.Request("POST", "/", gist1, 302)
|
||||
require.NoError(t, err)
|
||||
|
||||
gist1db, err := db.GetGistByID("1")
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create an expired token
|
||||
expiredToken := &db.AccessToken{
|
||||
Name: "expired-token",
|
||||
UserID: 1,
|
||||
ScopeGist: db.ReadPermission,
|
||||
ExpiresAt: time.Now().Add(-24 * time.Hour).Unix(), // Expired yesterday
|
||||
}
|
||||
expiredPlain, err := expiredToken.GenerateToken()
|
||||
require.NoError(t, err)
|
||||
err = expiredToken.Create()
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create a valid (non-expired) token
|
||||
validToken := &db.AccessToken{
|
||||
Name: "valid-token",
|
||||
UserID: 1,
|
||||
ScopeGist: db.ReadPermission,
|
||||
ExpiresAt: time.Now().Add(24 * time.Hour).Unix(), // Expires tomorrow
|
||||
}
|
||||
validPlain, err := validToken.GenerateToken()
|
||||
require.NoError(t, err)
|
||||
err = validToken.Create()
|
||||
require.NoError(t, err)
|
||||
|
||||
s.sessionCookie = ""
|
||||
|
||||
// Expired token should not grant access
|
||||
expiredHeaders := map[string]string{"Authorization": "Token " + expiredPlain}
|
||||
err = s.RequestWithHeaders("GET", "/thomas/"+gist1db.Uuid, nil, 404, expiredHeaders)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Valid token should grant access
|
||||
validHeaders := map[string]string{"Authorization": "Token " + validPlain}
|
||||
err = s.RequestWithHeaders("GET", "/thomas/"+gist1db.Uuid, nil, 200, validHeaders)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestAccessTokenWrongUser(t *testing.T) {
|
||||
s := Setup(t)
|
||||
defer Teardown(t, s)
|
||||
|
||||
// Register two users
|
||||
user1 := db.UserDTO{Username: "thomas", Password: "thomas"}
|
||||
register(t, s, user1)
|
||||
|
||||
// Create a private gist for user1
|
||||
gist1 := db.GistDTO{
|
||||
Title: "thomas-private-gist",
|
||||
Description: "thomas private gist",
|
||||
VisibilityDTO: db.VisibilityDTO{
|
||||
Private: db.PrivateVisibility,
|
||||
},
|
||||
Name: []string{"file.txt"},
|
||||
Content: []string{"content"},
|
||||
}
|
||||
err := s.Request("POST", "/", gist1, 302)
|
||||
require.NoError(t, err)
|
||||
|
||||
gist1db, err := db.GetGistByID("1")
|
||||
require.NoError(t, err)
|
||||
|
||||
s.sessionCookie = ""
|
||||
user2 := db.UserDTO{Username: "kaguya", Password: "kaguya"}
|
||||
register(t, s, user2)
|
||||
|
||||
// Create token for user2
|
||||
user2Token := &db.AccessToken{
|
||||
Name: "kaguya-token",
|
||||
UserID: 2,
|
||||
ScopeGist: db.ReadPermission,
|
||||
}
|
||||
user2Plain, err := user2Token.GenerateToken()
|
||||
require.NoError(t, err)
|
||||
err = user2Token.Create()
|
||||
require.NoError(t, err)
|
||||
|
||||
s.sessionCookie = ""
|
||||
|
||||
// User2's token should NOT grant access to user1's private gist
|
||||
user2Headers := map[string]string{"Authorization": "Token " + user2Plain}
|
||||
err = s.RequestWithHeaders("GET", "/thomas/"+gist1db.Uuid, nil, 404, user2Headers)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create token for user1
|
||||
user1Token := &db.AccessToken{
|
||||
Name: "thomas-token",
|
||||
UserID: 1,
|
||||
ScopeGist: db.ReadPermission,
|
||||
}
|
||||
user1Plain, err := user1Token.GenerateToken()
|
||||
require.NoError(t, err)
|
||||
err = user1Token.Create()
|
||||
require.NoError(t, err)
|
||||
|
||||
// User1's token SHOULD grant access to user1's private gist
|
||||
user1Headers := map[string]string{"Authorization": "Token " + user1Plain}
|
||||
err = s.RequestWithHeaders("GET", "/thomas/"+gist1db.Uuid, nil, 200, user1Headers)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestAccessTokenLastUsedUpdate(t *testing.T) {
|
||||
s := Setup(t)
|
||||
defer Teardown(t, s)
|
||||
|
||||
// Register user and create a private gist
|
||||
user1 := db.UserDTO{Username: "thomas", Password: "thomas"}
|
||||
register(t, s, user1)
|
||||
|
||||
gist1 := db.GistDTO{
|
||||
Title: "private-gist",
|
||||
Description: "my private gist",
|
||||
VisibilityDTO: db.VisibilityDTO{
|
||||
Private: db.PrivateVisibility,
|
||||
},
|
||||
Name: []string{"file.txt"},
|
||||
Content: []string{"content"},
|
||||
}
|
||||
err := s.Request("POST", "/", gist1, 302)
|
||||
require.NoError(t, err)
|
||||
|
||||
gist1db, err := db.GetGistByID("1")
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create token
|
||||
token := &db.AccessToken{
|
||||
Name: "test-token",
|
||||
UserID: 1,
|
||||
ScopeGist: db.ReadPermission,
|
||||
}
|
||||
plainToken, err := token.GenerateToken()
|
||||
require.NoError(t, err)
|
||||
err = token.Create()
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify LastUsedAt is 0 initially
|
||||
tokenFromDB, err := db.GetAccessTokenByID(token.ID)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, int64(0), tokenFromDB.LastUsedAt)
|
||||
|
||||
s.sessionCookie = ""
|
||||
|
||||
// Use the token
|
||||
headers := map[string]string{"Authorization": "Token " + plainToken}
|
||||
err = s.RequestWithHeaders("GET", "/thomas/"+gist1db.Uuid, nil, 200, headers)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify LastUsedAt was updated
|
||||
tokenFromDB, err = db.GetAccessTokenByID(token.ID)
|
||||
require.NoError(t, err)
|
||||
require.NotEqual(t, int64(0), tokenFromDB.LastUsedAt)
|
||||
}
|
||||
|
||||
func TestAccessTokenWithRequireLogin(t *testing.T) {
|
||||
s := Setup(t)
|
||||
defer Teardown(t, s)
|
||||
|
||||
admin := db.UserDTO{Username: "thomas", Password: "thomas"}
|
||||
register(t, s, admin)
|
||||
|
||||
gist1 := db.GistDTO{
|
||||
Title: "private-gist",
|
||||
Description: "my private gist",
|
||||
VisibilityDTO: db.VisibilityDTO{
|
||||
Private: db.PrivateVisibility,
|
||||
},
|
||||
Name: []string{"file.txt"},
|
||||
Content: []string{"content"},
|
||||
}
|
||||
err := s.Request("POST", "/", gist1, 302)
|
||||
require.NoError(t, err)
|
||||
|
||||
gist1db, err := db.GetGistByID("1")
|
||||
require.NoError(t, err)
|
||||
|
||||
gist2 := db.GistDTO{
|
||||
Title: "public-gist",
|
||||
Description: "my public gist",
|
||||
VisibilityDTO: db.VisibilityDTO{
|
||||
Private: db.PublicVisibility,
|
||||
},
|
||||
Name: []string{"public.txt"},
|
||||
Content: []string{"public content"},
|
||||
}
|
||||
err = s.Request("POST", "/", gist2, 302)
|
||||
require.NoError(t, err)
|
||||
|
||||
gist2db, err := db.GetGistByID("2")
|
||||
require.NoError(t, err)
|
||||
|
||||
token := &db.AccessToken{
|
||||
Name: "read-token",
|
||||
UserID: 1,
|
||||
ScopeGist: db.ReadPermission,
|
||||
}
|
||||
plainToken, err := token.GenerateToken()
|
||||
require.NoError(t, err)
|
||||
err = token.Create()
|
||||
require.NoError(t, err)
|
||||
|
||||
err = s.Request("PUT", "/admin-panel/set-config", settingSet{"require-login", "1"}, 200)
|
||||
require.NoError(t, err)
|
||||
|
||||
s.sessionCookie = ""
|
||||
|
||||
err = s.Request("GET", "/thomas/"+gist1db.Uuid, nil, 302)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = s.Request("GET", "/thomas/"+gist2db.Uuid, nil, 302)
|
||||
require.NoError(t, err)
|
||||
|
||||
headers := map[string]string{"Authorization": "Token " + plainToken}
|
||||
err = s.RequestWithHeaders("GET", "/thomas/"+gist1db.Uuid, nil, 200, headers)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = s.RequestWithHeaders("GET", "/thomas/"+gist2db.Uuid, nil, 200, headers)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = s.RequestWithHeaders("GET", "/thomas/"+gist1db.Uuid+"/raw/HEAD/file.txt", nil, 200, headers)
|
||||
require.NoError(t, err)
|
||||
|
||||
invalidHeaders := map[string]string{"Authorization": "Token invalid_token"}
|
||||
err = s.RequestWithHeaders("GET", "/thomas/"+gist1db.Uuid, nil, 302, invalidHeaders)
|
||||
require.NoError(t, err)
|
||||
|
||||
noPermToken := &db.AccessToken{
|
||||
Name: "no-perm-token",
|
||||
UserID: 1,
|
||||
ScopeGist: db.NoPermission,
|
||||
}
|
||||
noPermPlain, err := noPermToken.GenerateToken()
|
||||
require.NoError(t, err)
|
||||
err = noPermToken.Create()
|
||||
require.NoError(t, err)
|
||||
|
||||
noPermHeaders := map[string]string{"Authorization": "Token " + noPermPlain}
|
||||
err = s.RequestWithHeaders("GET", "/thomas/"+gist1db.Uuid, nil, 302, noPermHeaders)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
41
internal/web/test/actions_test.go
Normal file
41
internal/web/test/actions_test.go
Normal file
@@ -0,0 +1,41 @@
|
||||
package test
|
||||
|
||||
import (
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/thomiceli/opengist/internal/db"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestAdminActions(t *testing.T) {
|
||||
s := Setup(t)
|
||||
defer Teardown(t, s)
|
||||
urls := []string{
|
||||
"/admin-panel/sync-fs",
|
||||
"/admin-panel/sync-db",
|
||||
"/admin-panel/gc-repos",
|
||||
"/admin-panel/sync-previews",
|
||||
"/admin-panel/reset-hooks",
|
||||
"/admin-panel/index-gists",
|
||||
}
|
||||
|
||||
for _, url := range urls {
|
||||
err := s.Request("POST", url, nil, 404)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
user1 := db.UserDTO{Username: "admin", Password: "admin"}
|
||||
register(t, s, user1)
|
||||
login(t, s, user1)
|
||||
for _, url := range urls {
|
||||
err := s.Request("POST", url, nil, 302)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
user2 := db.UserDTO{Username: "nonadmin", Password: "nonadmin"}
|
||||
register(t, s, user2)
|
||||
login(t, s, user2)
|
||||
for _, url := range urls {
|
||||
err := s.Request("POST", url, nil, 404)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
}
|
||||
260
internal/web/test/admin_test.go
Normal file
260
internal/web/test/admin_test.go
Normal file
@@ -0,0 +1,260 @@
|
||||
package test
|
||||
|
||||
import (
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/thomiceli/opengist/internal/config"
|
||||
"github.com/thomiceli/opengist/internal/db"
|
||||
"github.com/thomiceli/opengist/internal/git"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestAdminPages(t *testing.T) {
|
||||
s := Setup(t)
|
||||
defer Teardown(t, s)
|
||||
urls := []string{
|
||||
"/admin-panel",
|
||||
"/admin-panel/users",
|
||||
"/admin-panel/gists",
|
||||
"/admin-panel/invitations",
|
||||
"/admin-panel/configuration",
|
||||
}
|
||||
|
||||
for _, url := range urls {
|
||||
err := s.Request("GET", url, nil, 404)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
user1 := db.UserDTO{Username: "admin", Password: "admin"}
|
||||
register(t, s, user1)
|
||||
login(t, s, user1)
|
||||
for _, url := range urls {
|
||||
err := s.Request("GET", url, nil, 200)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
user2 := db.UserDTO{Username: "nonadmin", Password: "nonadmin"}
|
||||
register(t, s, user2)
|
||||
login(t, s, user2)
|
||||
for _, url := range urls {
|
||||
err := s.Request("GET", url, nil, 404)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetConfig(t *testing.T) {
|
||||
s := Setup(t)
|
||||
defer Teardown(t, s)
|
||||
settings := []string{
|
||||
db.SettingDisableSignup,
|
||||
db.SettingRequireLogin,
|
||||
db.SettingAllowGistsWithoutLogin,
|
||||
db.SettingDisableLoginForm,
|
||||
db.SettingDisableGravatar,
|
||||
}
|
||||
|
||||
user1 := db.UserDTO{Username: "admin", Password: "admin"}
|
||||
register(t, s, user1)
|
||||
login(t, s, user1)
|
||||
|
||||
for _, setting := range settings {
|
||||
val, err := db.GetSetting(setting)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "0", val)
|
||||
|
||||
err = s.Request("PUT", "/admin-panel/set-config", settingSet{setting, "1"}, 200)
|
||||
require.NoError(t, err)
|
||||
|
||||
val, err = db.GetSetting(setting)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "1", val)
|
||||
|
||||
err = s.Request("PUT", "/admin-panel/set-config", settingSet{setting, "0"}, 200)
|
||||
require.NoError(t, err)
|
||||
|
||||
val, err = db.GetSetting(setting)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "0", val)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPagination(t *testing.T) {
|
||||
s := Setup(t)
|
||||
defer Teardown(t, s)
|
||||
|
||||
user1 := db.UserDTO{Username: "admin", Password: "admin"}
|
||||
register(t, s, user1)
|
||||
for i := 0; i < 11; i++ {
|
||||
user := db.UserDTO{Username: "user" + strconv.Itoa(i), Password: "user" + strconv.Itoa(i)}
|
||||
register(t, s, user)
|
||||
}
|
||||
|
||||
login(t, s, user1)
|
||||
|
||||
err := s.Request("GET", "/admin-panel/users", nil, 200)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = s.Request("GET", "/admin-panel/users?page=2", nil, 200)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = s.Request("GET", "/admin-panel/users?page=3", nil, 404)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = s.Request("GET", "/admin-panel/users?page=0", nil, 200)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = s.Request("GET", "/admin-panel/users?page=-1", nil, 200)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = s.Request("GET", "/admin-panel/users?page=a", nil, 200)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestAdminUser(t *testing.T) {
|
||||
s := Setup(t)
|
||||
defer Teardown(t, s)
|
||||
|
||||
user1 := db.UserDTO{Username: "admin", Password: "admin"}
|
||||
user2 := db.UserDTO{Username: "nonadmin", Password: "nonadmin"}
|
||||
register(t, s, user1)
|
||||
register(t, s, user2)
|
||||
|
||||
login(t, s, user2)
|
||||
|
||||
gist1 := db.GistDTO{
|
||||
Title: "gist",
|
||||
VisibilityDTO: db.VisibilityDTO{
|
||||
Private: 0,
|
||||
},
|
||||
Name: []string{"gist1.txt"},
|
||||
Content: []string{"yeah"},
|
||||
Topics: "",
|
||||
}
|
||||
err := s.Request("POST", "/", gist1, 302)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = os.Stat(filepath.Join(config.GetHomeDir(), git.ReposDirectory, user2.Username))
|
||||
require.NoError(t, err)
|
||||
|
||||
count, err := db.CountAll(db.User{})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, int64(2), count)
|
||||
|
||||
login(t, s, user1)
|
||||
|
||||
err = s.Request("POST", "/admin-panel/users/2/delete", nil, 302)
|
||||
require.NoError(t, err)
|
||||
|
||||
count, err = db.CountAll(db.User{})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, int64(1), count)
|
||||
|
||||
_, err = os.Stat(filepath.Join(config.GetHomeDir(), git.ReposDirectory, user2.Username))
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
func TestAdminGist(t *testing.T) {
|
||||
s := Setup(t)
|
||||
defer Teardown(t, s)
|
||||
|
||||
user1 := db.UserDTO{Username: "admin", Password: "admin"}
|
||||
register(t, s, user1)
|
||||
login(t, s, user1)
|
||||
|
||||
gist1 := db.GistDTO{
|
||||
Title: "gist",
|
||||
VisibilityDTO: db.VisibilityDTO{
|
||||
Private: 0,
|
||||
},
|
||||
Name: []string{"gist1.txt"},
|
||||
Content: []string{"yeah"},
|
||||
Topics: "",
|
||||
}
|
||||
err := s.Request("POST", "/", gist1, 302)
|
||||
require.NoError(t, err)
|
||||
|
||||
count, err := db.CountAll(db.Gist{})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, int64(1), count)
|
||||
|
||||
gist1Db, err := db.GetGistByID("1")
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = os.Stat(filepath.Join(config.GetHomeDir(), git.ReposDirectory, user1.Username, gist1Db.Identifier()))
|
||||
require.NoError(t, err)
|
||||
|
||||
err = s.Request("POST", "/admin-panel/gists/1/delete", nil, 302)
|
||||
require.NoError(t, err)
|
||||
|
||||
count, err = db.CountAll(db.Gist{})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, int64(0), count)
|
||||
|
||||
_, err = os.Stat(filepath.Join(config.GetHomeDir(), git.ReposDirectory, user1.Username, gist1Db.Identifier()))
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
func TestAdminInvitation(t *testing.T) {
|
||||
s := Setup(t)
|
||||
defer Teardown(t, s)
|
||||
|
||||
user1 := db.UserDTO{Username: "admin", Password: "admin"}
|
||||
register(t, s, user1)
|
||||
login(t, s, user1)
|
||||
|
||||
err := s.Request("POST", "/admin-panel/invitations", invitationAdmin{
|
||||
nbMax: "",
|
||||
expiredAtUnix: "",
|
||||
}, 302)
|
||||
require.NoError(t, err)
|
||||
invitation1, err := db.GetInvitationByID(1)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, uint(1), invitation1.ID)
|
||||
require.Equal(t, uint(0), invitation1.NbUsed)
|
||||
require.Equal(t, uint(10), invitation1.NbMax)
|
||||
require.InDelta(t, time.Now().Unix()+604800, invitation1.ExpiresAt, 10)
|
||||
|
||||
err = s.Request("POST", "/admin-panel/invitations", invitationAdmin{
|
||||
nbMax: "aa",
|
||||
expiredAtUnix: "1735722000",
|
||||
}, 302)
|
||||
require.NoError(t, err)
|
||||
invitation2, err := db.GetInvitationByID(2)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, invitation2, &db.Invitation{
|
||||
ID: 2,
|
||||
Code: invitation2.Code,
|
||||
ExpiresAt: time.Unix(1735722000, 0).Unix(),
|
||||
NbUsed: 0,
|
||||
NbMax: 10,
|
||||
})
|
||||
|
||||
err = s.Request("POST", "/admin-panel/invitations", invitationAdmin{
|
||||
nbMax: "20",
|
||||
expiredAtUnix: "1735722000",
|
||||
}, 302)
|
||||
require.NoError(t, err)
|
||||
invitation3, err := db.GetInvitationByID(3)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, invitation3, &db.Invitation{
|
||||
ID: 3,
|
||||
Code: invitation3.Code,
|
||||
ExpiresAt: time.Unix(1735722000, 0).Unix(),
|
||||
NbUsed: 0,
|
||||
NbMax: 20,
|
||||
})
|
||||
|
||||
count, err := db.CountAll(db.Invitation{})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, int64(3), count)
|
||||
|
||||
err = s.Request("POST", "/admin-panel/invitations/1/delete", nil, 302)
|
||||
require.NoError(t, err)
|
||||
|
||||
count, err = db.CountAll(db.Invitation{})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, int64(2), count)
|
||||
}
|
||||
414
internal/web/test/auth_test.go
Normal file
414
internal/web/test/auth_test.go
Normal file
@@ -0,0 +1,414 @@
|
||||
package test
|
||||
|
||||
import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/thomiceli/opengist/internal/config"
|
||||
"github.com/thomiceli/opengist/internal/db"
|
||||
)
|
||||
|
||||
func TestRegister(t *testing.T) {
|
||||
s := Setup(t)
|
||||
defer Teardown(t, s)
|
||||
|
||||
err := s.Request("GET", "/", nil, 302)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = s.Request("GET", "/register", nil, 200)
|
||||
require.NoError(t, err)
|
||||
|
||||
user1 := db.UserDTO{Username: "thomas", Password: "thomas"}
|
||||
register(t, s, user1)
|
||||
|
||||
user1db, err := db.GetUserById(1)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, user1.Username, user1db.Username)
|
||||
require.True(t, user1db.IsAdmin)
|
||||
|
||||
err = s.Request("GET", "/", nil, 200)
|
||||
require.NoError(t, err)
|
||||
|
||||
s.sessionCookie = ""
|
||||
|
||||
user2 := db.UserDTO{Username: "thomas", Password: "azeaze"}
|
||||
err = s.Request("POST", "/register", user2, 200)
|
||||
require.Error(t, err)
|
||||
|
||||
user3 := db.UserDTO{Username: "kaguya", Password: "kaguya"}
|
||||
register(t, s, user3)
|
||||
|
||||
user3db, err := db.GetUserById(2)
|
||||
require.NoError(t, err)
|
||||
require.False(t, user3db.IsAdmin)
|
||||
|
||||
s.sessionCookie = ""
|
||||
|
||||
count, err := db.CountAll(db.User{})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, int64(2), count)
|
||||
}
|
||||
|
||||
func TestLogin(t *testing.T) {
|
||||
s := Setup(t)
|
||||
defer Teardown(t, s)
|
||||
|
||||
err := s.Request("GET", "/login", nil, 200)
|
||||
require.NoError(t, err)
|
||||
|
||||
user1 := db.UserDTO{Username: "thomas", Password: "thomas"}
|
||||
register(t, s, user1)
|
||||
|
||||
s.sessionCookie = ""
|
||||
|
||||
login(t, s, user1)
|
||||
require.NotEmpty(t, s.sessionCookie)
|
||||
|
||||
s.sessionCookie = ""
|
||||
|
||||
user2 := db.UserDTO{Username: "thomas", Password: "azeaze"}
|
||||
user3 := db.UserDTO{Username: "azeaze", Password: ""}
|
||||
|
||||
err = s.Request("POST", "/login", user2, 302)
|
||||
require.Empty(t, s.sessionCookie)
|
||||
require.Error(t, err)
|
||||
|
||||
err = s.Request("POST", "/login", user3, 302)
|
||||
require.Empty(t, s.sessionCookie)
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
func register(t *testing.T, s *TestServer, user db.UserDTO) {
|
||||
err := s.Request("POST", "/register", user, 302)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func login(t *testing.T, s *TestServer, user db.UserDTO) {
|
||||
err := s.Request("POST", "/login", user, 302)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestAnonymous(t *testing.T) {
|
||||
s := Setup(t)
|
||||
defer Teardown(t, s)
|
||||
|
||||
user := db.UserDTO{Username: "thomas", Password: "azeaze"}
|
||||
register(t, s, user)
|
||||
|
||||
err := s.Request("PUT", "/admin-panel/set-config", settingSet{"require-login", "1"}, 200)
|
||||
require.NoError(t, err)
|
||||
|
||||
gist1 := db.GistDTO{
|
||||
Title: "gist1",
|
||||
Description: "my first gist",
|
||||
VisibilityDTO: db.VisibilityDTO{
|
||||
Private: 0,
|
||||
},
|
||||
Name: []string{"gist1.txt", "gist2.txt", "gist3.txt"},
|
||||
Content: []string{"yeah", "yeah\ncool", "yeah\ncool gist actually"},
|
||||
Topics: "",
|
||||
}
|
||||
err = s.Request("POST", "/", gist1, 302)
|
||||
require.NoError(t, err)
|
||||
|
||||
gist1db, err := db.GetGistByID("1")
|
||||
require.NoError(t, err)
|
||||
|
||||
err = s.Request("GET", "/all", nil, 200)
|
||||
require.NoError(t, err)
|
||||
|
||||
cookie := s.sessionCookie
|
||||
s.sessionCookie = ""
|
||||
|
||||
err = s.Request("GET", "/all", nil, 302)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Should redirect to login if RequireLogin
|
||||
err = s.Request("GET", "/"+gist1db.User.Username+"/"+gist1db.Uuid, nil, 302)
|
||||
require.NoError(t, err)
|
||||
|
||||
s.sessionCookie = cookie
|
||||
|
||||
err = s.Request("PUT", "/admin-panel/set-config", settingSet{"allow-gists-without-login", "1"}, 200)
|
||||
require.NoError(t, err)
|
||||
|
||||
s.sessionCookie = ""
|
||||
|
||||
// Should return results
|
||||
err = s.Request("GET", "/"+gist1db.User.Username+"/"+gist1db.Uuid, nil, 200)
|
||||
require.NoError(t, err)
|
||||
|
||||
}
|
||||
|
||||
func TestGitOperations(t *testing.T) {
|
||||
s := Setup(t)
|
||||
defer Teardown(t, s)
|
||||
|
||||
admin := db.UserDTO{Username: "thomas", Password: "thomas"}
|
||||
register(t, s, admin)
|
||||
s.sessionCookie = ""
|
||||
register(t, s, db.UserDTO{Username: "fujiwara", Password: "fujiwara"})
|
||||
s.sessionCookie = ""
|
||||
register(t, s, db.UserDTO{Username: "kaguya", Password: "kaguya"})
|
||||
|
||||
gist1 := db.GistDTO{
|
||||
Title: "kaguya-pub-gist",
|
||||
URL: "kaguya-pub-gist",
|
||||
Description: "kaguya's first gist",
|
||||
VisibilityDTO: db.VisibilityDTO{
|
||||
Private: db.PublicVisibility,
|
||||
},
|
||||
Name: []string{"kaguya-file.txt"},
|
||||
Content: []string{
|
||||
"yeah",
|
||||
},
|
||||
Topics: "",
|
||||
}
|
||||
err := s.Request("POST", "/", gist1, 302)
|
||||
require.NoError(t, err)
|
||||
|
||||
gist2 := db.GistDTO{
|
||||
Title: "kaguya-unl-gist",
|
||||
URL: "kaguya-unl-gist",
|
||||
Description: "kaguya's second gist",
|
||||
VisibilityDTO: db.VisibilityDTO{
|
||||
Private: db.UnlistedVisibility,
|
||||
},
|
||||
Name: []string{"kaguya-file.txt"},
|
||||
Content: []string{
|
||||
"cool",
|
||||
},
|
||||
Topics: "",
|
||||
}
|
||||
err = s.Request("POST", "/", gist2, 302)
|
||||
require.NoError(t, err)
|
||||
|
||||
gist3 := db.GistDTO{
|
||||
Title: "kaguya-priv-gist",
|
||||
URL: "kaguya-priv-gist",
|
||||
Description: "kaguya's second gist",
|
||||
VisibilityDTO: db.VisibilityDTO{
|
||||
Private: db.PrivateVisibility,
|
||||
},
|
||||
Name: []string{"kaguya-file.txt"},
|
||||
Content: []string{
|
||||
"super",
|
||||
},
|
||||
Topics: "",
|
||||
}
|
||||
err = s.Request("POST", "/", gist3, 302)
|
||||
require.NoError(t, err)
|
||||
|
||||
tests := []struct {
|
||||
credentials string
|
||||
user string
|
||||
url string
|
||||
pushOptions string
|
||||
expectErrorClone bool
|
||||
expectErrorCheck bool
|
||||
expectErrorPush bool
|
||||
}{
|
||||
{":", "kaguya", "kaguya-pub-gist", "", false, false, true},
|
||||
{":", "kaguya", "kaguya-unl-gist", "", false, false, true},
|
||||
{":", "kaguya", "kaguya-priv-gist", "", true, true, true},
|
||||
{"kaguya:kaguya", "kaguya", "kaguya-pub-gist", "", false, false, false},
|
||||
{"kaguya:kaguya", "kaguya", "kaguya-unl-gist", "", false, false, false},
|
||||
{"kaguya:kaguya", "kaguya", "kaguya-priv-gist", "", false, false, false},
|
||||
{"fujiwara:fujiwara", "kaguya", "kaguya-pub-gist", "", false, false, true},
|
||||
{"fujiwara:fujiwara", "kaguya", "kaguya-unl-gist", "", false, false, true},
|
||||
{"fujiwara:fujiwara", "kaguya", "kaguya-priv-gist", "", true, true, true},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
gitCloneCheckPush(t, test.credentials, test.user, test.url, "kaguya-file.txt", test.pushOptions, test.expectErrorClone, test.expectErrorCheck, test.expectErrorPush)
|
||||
}
|
||||
|
||||
login(t, s, admin)
|
||||
err = s.Request("PUT", "/admin-panel/set-config", settingSet{"require-login", "1"}, 200)
|
||||
require.NoError(t, err)
|
||||
|
||||
testsRequireLogin := []struct {
|
||||
credentials string
|
||||
user string
|
||||
url string
|
||||
pushOptions string
|
||||
expectErrorClone bool
|
||||
expectErrorCheck bool
|
||||
expectErrorPush bool
|
||||
}{
|
||||
{":", "kaguya", "kaguya-pub-gist", "", true, true, true},
|
||||
{":", "kaguya", "kaguya-unl-gist", "", true, true, true},
|
||||
{":", "kaguya", "kaguya-priv-gist", "", true, true, true},
|
||||
{"kaguya:kaguya", "kaguya", "kaguya-pub-gist", "", false, false, false},
|
||||
{"kaguya:kaguya", "kaguya", "kaguya-unl-gist", "", false, false, false},
|
||||
{"kaguya:kaguya", "kaguya", "kaguya-priv-gist", "", false, false, false},
|
||||
{"fujiwara:fujiwara", "kaguya", "kaguya-pub-gist", "", false, false, true},
|
||||
{"fujiwara:fujiwara", "kaguya", "kaguya-unl-gist", "", false, false, true},
|
||||
{"fujiwara:fujiwara", "kaguya", "kaguya-priv-gist", "", true, true, true},
|
||||
}
|
||||
|
||||
for _, test := range testsRequireLogin {
|
||||
gitCloneCheckPush(t, test.credentials, test.user, test.url, "kaguya-file.txt", test.pushOptions, test.expectErrorClone, test.expectErrorCheck, test.expectErrorPush)
|
||||
}
|
||||
|
||||
login(t, s, admin)
|
||||
err = s.Request("PUT", "/admin-panel/set-config", settingSet{"allow-gists-without-login", "1"}, 200)
|
||||
require.NoError(t, err)
|
||||
|
||||
for _, test := range tests {
|
||||
gitCloneCheckPush(t, test.credentials, test.user, test.url, "kaguya-file.txt", test.pushOptions, test.expectErrorClone, test.expectErrorCheck, test.expectErrorPush)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGitInit(t *testing.T) {
|
||||
s := Setup(t)
|
||||
defer Teardown(t, s)
|
||||
|
||||
admin := db.UserDTO{Username: "thomas", Password: "thomas"}
|
||||
register(t, s, admin)
|
||||
s.sessionCookie = ""
|
||||
register(t, s, db.UserDTO{Username: "fujiwara", Password: "fujiwara"})
|
||||
s.sessionCookie = ""
|
||||
register(t, s, db.UserDTO{Username: "kaguya", Password: "kaguya"})
|
||||
|
||||
testsNewWithPush := []struct {
|
||||
credentials string
|
||||
user string
|
||||
url string
|
||||
pushOptions string
|
||||
expectErrorClone bool
|
||||
expectErrorCheck bool
|
||||
expectErrorPush bool
|
||||
}{
|
||||
{":", "kaguya", "gist1", "", true, true, true},
|
||||
{"kaguya:wrongpass", "kaguya", "gist2", "", true, true, true},
|
||||
{"fujiwara:fujiwara", "kaguya", "gist3", "", true, true, true},
|
||||
{"kaguya:kaguya", "kaguya", "gist4", "", false, false, false},
|
||||
{"kaguya:kaguya", "kaguya", "gist5/g", "", true, true, true},
|
||||
}
|
||||
|
||||
for _, test := range testsNewWithPush {
|
||||
gitInitPush(t, test.credentials, test.user, test.url, "newfile.txt", test.pushOptions, test.expectErrorPush)
|
||||
}
|
||||
|
||||
gist1db, err := db.GetGistByID("1")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "kaguya", gist1db.User.Username)
|
||||
|
||||
for _, test := range testsNewWithPush {
|
||||
gitCloneCheckPush(t, test.credentials, test.user, test.url, "newfile.txt", test.pushOptions, test.expectErrorClone, test.expectErrorCheck, test.expectErrorPush)
|
||||
}
|
||||
|
||||
count, err := db.CountAll(db.Gist{})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, int64(1), count)
|
||||
|
||||
testsNewWithInit := []struct {
|
||||
credentials string
|
||||
url string
|
||||
pushOptions string
|
||||
expectErrorPush bool
|
||||
}{
|
||||
{":", "init", "", true},
|
||||
{"fujiwara:wrongpass", "init", "", true},
|
||||
{"kaguya:kaguya", "init", "", false},
|
||||
{"fujiwara:fujiwara", "init", "", false},
|
||||
}
|
||||
|
||||
for _, test := range testsNewWithInit {
|
||||
gitInitPush(t, test.credentials, "kaguya", test.url, "newfile.txt", test.pushOptions, test.expectErrorPush)
|
||||
}
|
||||
|
||||
count, err = db.CountAll(db.Gist{})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, int64(3), count)
|
||||
|
||||
gist2db, err := db.GetGistByID("2")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "kaguya", gist2db.User.Username)
|
||||
|
||||
gist3db, err := db.GetGistByID("3")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "fujiwara", gist3db.User.Username)
|
||||
}
|
||||
|
||||
func clientGitClone(creds string, user string, url string) error {
|
||||
return exec.Command("git", "clone", "http://"+creds+"@localhost:6157/"+user+"/"+url, filepath.Join(config.GetHomeDir(), "tmp", url)).Run()
|
||||
}
|
||||
|
||||
func clientGitPush(url string, pushOptions string, file string) error {
|
||||
f, err := os.Create(filepath.Join(config.GetHomeDir(), "tmp", url, file))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, _ = f.WriteString("new file")
|
||||
_ = f.Close()
|
||||
|
||||
_ = exec.Command("git", "-C", filepath.Join(config.GetHomeDir(), "tmp", url), "add", file).Run()
|
||||
_ = exec.Command("git", "-C", filepath.Join(config.GetHomeDir(), "tmp", url), "commit", "-m", "new file").Run()
|
||||
if pushOptions != "" {
|
||||
err = exec.Command("git", "-C", filepath.Join(config.GetHomeDir(), "tmp", url), "push", pushOptions, "origin").Run()
|
||||
} else {
|
||||
err = exec.Command("git", "-C", filepath.Join(config.GetHomeDir(), "tmp", url), "push", "origin").Run()
|
||||
}
|
||||
_ = os.RemoveAll(filepath.Join(config.GetHomeDir(), "tmp", url))
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func clientGitInit(path string) error {
|
||||
return exec.Command("git", "init", "--initial-branch=master", filepath.Join(config.GetHomeDir(), "tmp", path)).Run()
|
||||
}
|
||||
|
||||
func clientGitSetRemote(path string, remoteName string, remoteUrl string) error {
|
||||
return exec.Command("git", "-C", filepath.Join(config.GetHomeDir(), "tmp", path), "remote", "add", remoteName, remoteUrl).Run()
|
||||
}
|
||||
|
||||
func clientCheckRepo(url string, file string) error {
|
||||
_, err := os.ReadFile(filepath.Join(config.GetHomeDir(), "tmp", url, file))
|
||||
return err
|
||||
}
|
||||
|
||||
func gitCloneCheckPush(t *testing.T, credentials, owner, url, filename, pushOptions string, expectErrorClone, expectErrorCheck, expectErrorPush bool) {
|
||||
log.Debug().Msgf("Testing %s %s %t %t %t", credentials, url, expectErrorClone, expectErrorCheck, expectErrorPush)
|
||||
err := clientGitClone(credentials, owner, url)
|
||||
if expectErrorClone {
|
||||
require.Error(t, err)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
err = clientCheckRepo(url, filename)
|
||||
if expectErrorCheck {
|
||||
require.Error(t, err)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
err = clientGitPush(url, pushOptions, filename)
|
||||
if expectErrorPush {
|
||||
require.Error(t, err)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
}
|
||||
|
||||
func gitInitPush(t *testing.T, credentials, owner, url, filename, pushOptions string, expectErrorPush bool) {
|
||||
log.Debug().Msgf("Testing %s %s %t", credentials, url, expectErrorPush)
|
||||
err := clientGitInit(url)
|
||||
require.NoError(t, err)
|
||||
if url == "init" {
|
||||
err = clientGitSetRemote(url, "origin", "http://"+credentials+"@localhost:6157/init/")
|
||||
} else {
|
||||
err = clientGitSetRemote(url, "origin", "http://"+credentials+"@localhost:6157/"+owner+"/"+url)
|
||||
}
|
||||
require.NoError(t, err)
|
||||
err = clientGitPush(url, pushOptions, filename)
|
||||
if expectErrorPush {
|
||||
require.Error(t, err)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
}
|
||||
342
internal/web/test/gist_test.go
Normal file
342
internal/web/test/gist_test.go
Normal file
@@ -0,0 +1,342 @@
|
||||
package test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/thomiceli/opengist/internal/db"
|
||||
"github.com/thomiceli/opengist/internal/git"
|
||||
)
|
||||
|
||||
func TestGists(t *testing.T) {
|
||||
s := Setup(t)
|
||||
defer Teardown(t, s)
|
||||
|
||||
err := s.Request("GET", "/", nil, 302)
|
||||
require.NoError(t, err)
|
||||
|
||||
user1 := db.UserDTO{Username: "thomas", Password: "thomas"}
|
||||
register(t, s, user1)
|
||||
|
||||
err = s.Request("GET", "/all", nil, 200)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = s.Request("POST", "/", nil, 400)
|
||||
require.NoError(t, err)
|
||||
|
||||
gist1 := db.GistDTO{
|
||||
Title: "gist1",
|
||||
Description: "my first gist",
|
||||
VisibilityDTO: db.VisibilityDTO{
|
||||
Private: 0,
|
||||
},
|
||||
Name: []string{"gist1.txt", "gist2.txt", "gist3.txt"},
|
||||
Content: []string{"yeah", "yeah\ncool", "yeah\ncool gist actually"},
|
||||
Topics: "",
|
||||
}
|
||||
err = s.Request("POST", "/", gist1, 302)
|
||||
require.NoError(t, err)
|
||||
|
||||
gist1db, err := db.GetGistByID("1")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, uint(1), gist1db.ID)
|
||||
require.Equal(t, gist1.Title, gist1db.Title)
|
||||
require.Equal(t, gist1.Description, gist1db.Description)
|
||||
require.Regexp(t, "[a-f0-9]{32}", gist1db.Uuid)
|
||||
require.Equal(t, user1.Username, gist1db.User.Username)
|
||||
|
||||
err = s.Request("GET", "/"+gist1db.User.Username+"/"+gist1db.Uuid, nil, 200)
|
||||
require.NoError(t, err)
|
||||
|
||||
gist1files, err := git.GetFilesOfRepository(gist1db.User.Username, gist1db.Uuid, "HEAD")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 3, len(gist1files))
|
||||
|
||||
gist1fileContent, _, err := git.GetFileContent(gist1db.User.Username, gist1db.Uuid, "HEAD", gist1.Name[0], false)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, gist1.Content[0], gist1fileContent)
|
||||
|
||||
gist2 := db.GistDTO{
|
||||
Title: "gist2",
|
||||
Description: "my second gist",
|
||||
VisibilityDTO: db.VisibilityDTO{
|
||||
Private: 0,
|
||||
},
|
||||
Name: []string{"", "gist2.txt", "gist3.txt"},
|
||||
Content: []string{"", "yeah\ncool", "yeah\ncool gist actually"},
|
||||
Topics: "",
|
||||
}
|
||||
err = s.Request("POST", "/", gist2, 302)
|
||||
require.NoError(t, err)
|
||||
|
||||
gist3 := db.GistDTO{
|
||||
Title: "gist3",
|
||||
Description: "my third gist",
|
||||
VisibilityDTO: db.VisibilityDTO{
|
||||
Private: 0,
|
||||
},
|
||||
Name: []string{""},
|
||||
Content: []string{"yeah"},
|
||||
Topics: "",
|
||||
}
|
||||
err = s.Request("POST", "/", gist3, 302)
|
||||
require.NoError(t, err)
|
||||
|
||||
gist3db, err := db.GetGistByID("3")
|
||||
require.NoError(t, err)
|
||||
|
||||
gist3files, err := git.GetFilesOfRepository(gist3db.User.Username, gist3db.Uuid, "HEAD")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "gistfile1.txt", gist3files[0])
|
||||
|
||||
err = s.Request("POST", "/"+gist1db.User.Username+"/"+gist1db.Uuid+"/edit", nil, 400)
|
||||
require.NoError(t, err)
|
||||
|
||||
gist1.Name = []string{"gist1.txt"}
|
||||
gist1.Content = []string{"only want one gist"}
|
||||
|
||||
err = s.Request("POST", "/"+gist1db.User.Username+"/"+gist1db.Uuid+"/edit", gist1, 302)
|
||||
require.NoError(t, err)
|
||||
|
||||
gist1files, err = git.GetFilesOfRepository(gist1db.User.Username, gist1db.Uuid, "HEAD")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, len(gist1files))
|
||||
|
||||
err = s.Request("POST", "/"+gist1db.User.Username+"/"+gist1db.Uuid+"/delete", nil, 302)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestVisibility(t *testing.T) {
|
||||
s := Setup(t)
|
||||
defer Teardown(t, s)
|
||||
|
||||
user1 := db.UserDTO{Username: "thomas", Password: "thomas"}
|
||||
register(t, s, user1)
|
||||
|
||||
gist1 := db.GistDTO{
|
||||
Title: "gist1",
|
||||
Description: "my first gist",
|
||||
VisibilityDTO: db.VisibilityDTO{
|
||||
Private: db.UnlistedVisibility,
|
||||
},
|
||||
Name: []string{""},
|
||||
Content: []string{"yeah"},
|
||||
Topics: "",
|
||||
}
|
||||
err := s.Request("POST", "/", gist1, 302)
|
||||
require.NoError(t, err)
|
||||
|
||||
gist1db, err := db.GetGistByID("1")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, db.UnlistedVisibility, gist1db.Private)
|
||||
|
||||
err = s.Request("POST", "/"+gist1db.User.Username+"/"+gist1db.Uuid+"/visibility", db.VisibilityDTO{Private: db.PrivateVisibility}, 302)
|
||||
require.NoError(t, err)
|
||||
gist1db, err = db.GetGistByID("1")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, db.PrivateVisibility, gist1db.Private)
|
||||
|
||||
err = s.Request("POST", "/"+gist1db.User.Username+"/"+gist1db.Uuid+"/visibility", db.VisibilityDTO{Private: db.PublicVisibility}, 302)
|
||||
require.NoError(t, err)
|
||||
gist1db, err = db.GetGistByID("1")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, db.PublicVisibility, gist1db.Private)
|
||||
|
||||
err = s.Request("POST", "/"+gist1db.User.Username+"/"+gist1db.Uuid+"/visibility", db.VisibilityDTO{Private: db.UnlistedVisibility}, 302)
|
||||
require.NoError(t, err)
|
||||
gist1db, err = db.GetGistByID("1")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, db.UnlistedVisibility, gist1db.Private)
|
||||
}
|
||||
|
||||
func TestLikeFork(t *testing.T) {
|
||||
s := Setup(t)
|
||||
defer Teardown(t, s)
|
||||
|
||||
user1 := db.UserDTO{Username: "thomas", Password: "thomas"}
|
||||
register(t, s, user1)
|
||||
|
||||
gist1 := db.GistDTO{
|
||||
Title: "gist1",
|
||||
Description: "my first gist",
|
||||
VisibilityDTO: db.VisibilityDTO{
|
||||
Private: 1,
|
||||
},
|
||||
Name: []string{""},
|
||||
Content: []string{"yeah"},
|
||||
Topics: "",
|
||||
}
|
||||
err := s.Request("POST", "/", gist1, 302)
|
||||
require.NoError(t, err)
|
||||
|
||||
s.sessionCookie = ""
|
||||
|
||||
user2 := db.UserDTO{Username: "kaguya", Password: "kaguya"}
|
||||
register(t, s, user2)
|
||||
|
||||
gist1db, err := db.GetGistByID("1")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 0, gist1db.NbLikes)
|
||||
likeCount, err := db.CountAll(db.Like{})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, int64(0), likeCount)
|
||||
|
||||
err = s.Request("POST", "/"+gist1db.User.Username+"/"+gist1db.Uuid+"/like", nil, 302)
|
||||
require.NoError(t, err)
|
||||
gist1db, err = db.GetGistByID("1")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, gist1db.NbLikes)
|
||||
likeCount, err = db.CountAll(db.Like{})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, int64(1), likeCount)
|
||||
|
||||
err = s.Request("POST", "/"+gist1db.User.Username+"/"+gist1db.Uuid+"/like", nil, 302)
|
||||
require.NoError(t, err)
|
||||
gist1db, err = db.GetGistByID("1")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 0, gist1db.NbLikes)
|
||||
likeCount, err = db.CountAll(db.Like{})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, int64(0), likeCount)
|
||||
|
||||
err = s.Request("POST", "/"+gist1db.User.Username+"/"+gist1db.Uuid+"/fork", nil, 302)
|
||||
require.NoError(t, err)
|
||||
gist2db, err := db.GetGistByID("2")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, gist1db.Title, gist2db.Title)
|
||||
require.Equal(t, gist1db.Description, gist2db.Description)
|
||||
require.Equal(t, gist1db.Private, gist2db.Private)
|
||||
require.Equal(t, user2.Username, gist2db.User.Username)
|
||||
}
|
||||
|
||||
func TestCustomUrl(t *testing.T) {
|
||||
s := Setup(t)
|
||||
defer Teardown(t, s)
|
||||
|
||||
user1 := db.UserDTO{Username: "thomas", Password: "thomas"}
|
||||
register(t, s, user1)
|
||||
|
||||
gist1 := db.GistDTO{
|
||||
Title: "gist1",
|
||||
URL: "my-gist",
|
||||
Description: "my first gist",
|
||||
VisibilityDTO: db.VisibilityDTO{
|
||||
Private: 0,
|
||||
},
|
||||
Name: []string{"gist1.txt", "gist2.txt", "gist3.txt"},
|
||||
Content: []string{"yeah", "yeah\ncool", "yeah\ncool gist actually"},
|
||||
Topics: "",
|
||||
}
|
||||
err := s.Request("POST", "/", gist1, 302)
|
||||
require.NoError(t, err)
|
||||
|
||||
gist1db, err := db.GetGistByID("1")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, uint(1), gist1db.ID)
|
||||
require.Equal(t, gist1.Title, gist1db.Title)
|
||||
require.Equal(t, gist1.Description, gist1db.Description)
|
||||
require.Regexp(t, "[a-f0-9]{32}", gist1db.Uuid)
|
||||
require.Equal(t, gist1.URL, gist1db.URL)
|
||||
require.Equal(t, user1.Username, gist1db.User.Username)
|
||||
|
||||
gist1dbUuid, err := db.GetGist(user1.Username, gist1db.Uuid)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, gist1db, gist1dbUuid)
|
||||
|
||||
gist1dbUrl, err := db.GetGist(user1.Username, gist1.URL)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, gist1db, gist1dbUrl)
|
||||
|
||||
require.Equal(t, gist1.URL, gist1db.Identifier())
|
||||
|
||||
gist2 := db.GistDTO{
|
||||
Title: "gist2",
|
||||
Description: "my second gist",
|
||||
VisibilityDTO: db.VisibilityDTO{
|
||||
Private: 0,
|
||||
},
|
||||
Name: []string{"gist1.txt", "gist2.txt", "gist3.txt"},
|
||||
Content: []string{"yeah", "yeah\ncool", "yeah\ncool gist actually"},
|
||||
Topics: "",
|
||||
}
|
||||
err = s.Request("POST", "/", gist2, 302)
|
||||
require.NoError(t, err)
|
||||
|
||||
gist2db, err := db.GetGistByID("2")
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, gist2db.Uuid, gist2db.Identifier())
|
||||
require.NotEqual(t, gist2db.URL, gist2db.Identifier())
|
||||
}
|
||||
|
||||
func TestTopics(t *testing.T) {
|
||||
s := Setup(t)
|
||||
defer Teardown(t, s)
|
||||
|
||||
user1 := db.UserDTO{Username: "thomas", Password: "thomas"}
|
||||
register(t, s, user1)
|
||||
|
||||
gist1 := db.GistDTO{
|
||||
Title: "gist1",
|
||||
URL: "my-gist",
|
||||
Description: "my first gist",
|
||||
VisibilityDTO: db.VisibilityDTO{
|
||||
Private: 0,
|
||||
},
|
||||
Name: []string{"gist1.txt", "gist2.txt", "gist3.txt"},
|
||||
Content: []string{"yeah", "yeah\ncool", "yeah\ncool gist actually"},
|
||||
Topics: "topic1 topic2 topic3",
|
||||
}
|
||||
err := s.Request("POST", "/", gist1, 302)
|
||||
require.NoError(t, err)
|
||||
|
||||
gist1db, err := db.GetGistByID("1")
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, []db.GistTopic{
|
||||
{GistID: 1, Topic: "topic1"},
|
||||
{GistID: 1, Topic: "topic2"},
|
||||
{GistID: 1, Topic: "topic3"},
|
||||
}, gist1db.Topics)
|
||||
|
||||
gist2 := db.GistDTO{
|
||||
Title: "gist2",
|
||||
URL: "my-gist",
|
||||
Description: "my second gist",
|
||||
VisibilityDTO: db.VisibilityDTO{
|
||||
Private: 0,
|
||||
},
|
||||
Name: []string{"gist1.txt", "gist2.txt", "gist3.txt"},
|
||||
Content: []string{"yeah", "yeah\ncool", "yeah\ncool gist actually"},
|
||||
Topics: "topic1 topic2 topic3 topic2 topic4 topic1",
|
||||
}
|
||||
err = s.Request("POST", "/", gist2, 302)
|
||||
require.NoError(t, err)
|
||||
|
||||
gist2db, err := db.GetGistByID("2")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, []db.GistTopic{
|
||||
{GistID: 2, Topic: "topic1"},
|
||||
{GistID: 2, Topic: "topic2"},
|
||||
{GistID: 2, Topic: "topic3"},
|
||||
{GistID: 2, Topic: "topic4"},
|
||||
}, gist2db.Topics)
|
||||
|
||||
gist3 := db.GistDTO{
|
||||
Title: "gist3",
|
||||
URL: "my-gist",
|
||||
Description: "my third gist",
|
||||
VisibilityDTO: db.VisibilityDTO{
|
||||
Private: 0,
|
||||
},
|
||||
Name: []string{"gist1.txt", "gist2.txt", "gist3.txt"},
|
||||
Content: []string{"yeah", "yeah\ncool", "yeah\ncool gist actually"},
|
||||
Topics: "topic1 topic2 topic3 topic4 topic5 topic6 topic7 topic8 topic9 topic10 topic11",
|
||||
}
|
||||
err = s.Request("POST", "/", gist3, 400)
|
||||
require.NoError(t, err)
|
||||
|
||||
gist3.Topics = "topictoolongggggggggggggggggggggggggggggggggggggggg"
|
||||
err = s.Request("POST", "/", gist3, 400)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package metrics_test
|
||||
package test
|
||||
|
||||
import (
|
||||
"io"
|
||||
@@ -10,17 +10,19 @@ import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/thomiceli/opengist/internal/db"
|
||||
webtest "github.com/thomiceli/opengist/internal/web/test"
|
||||
)
|
||||
|
||||
func TestMetrics(t *testing.T) {
|
||||
s := webtest.Setup(t)
|
||||
defer webtest.Teardown(t)
|
||||
var (
|
||||
SSHKey = db.SSHKeyDTO{
|
||||
Title: "Test SSH Key",
|
||||
Content: `ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAklOUpkDHrfHY17SbrmTIpNLTGK9Tjom/BWDSUGPl+nafzlHDTYW7hdI4yZ5ew18JH4JW9jbhUFrviQzM7xlELEVf4h9lFX5QVkbPppSwg0cda3Pbv7kOdJ/MTyBlWXFCR+HAo3FXRitBqxiX1nKhXpHAZsMciLq8V6RjsNAQwdsdMFvSlVK/7XAt3FaoJoAsncM1Q9x5+3V0Ww68/eIFmb1zuUFljQJKprrX88XypNDvjYNby6vw/Pb0rwert/EnmZ+AW4OZPnTPI89ZPmVMLuayrD2cE86Z/il8b+gw3r3+1nKatmIkjn2so1d01QraTlMqVSsbxNrRFi9wrf+M7Q== admin@admin.local`,
|
||||
}
|
||||
AdminUser = db.UserDTO{
|
||||
Username: "admin",
|
||||
Password: "admin",
|
||||
}
|
||||
|
||||
s.Register(t, "thomas")
|
||||
s.Login(t, "thomas")
|
||||
|
||||
s.Request(t, "POST", "/", db.GistDTO{
|
||||
SimpleGist = db.GistDTO{
|
||||
Title: "Simple Test Gist",
|
||||
Description: "A simple gist for testing",
|
||||
VisibilityDTO: db.VisibilityDTO{
|
||||
@@ -29,14 +31,39 @@ func TestMetrics(t *testing.T) {
|
||||
Name: []string{"file1.txt"},
|
||||
Content: []string{"This is the content of file1"},
|
||||
Topics: "",
|
||||
}, 302)
|
||||
}
|
||||
)
|
||||
|
||||
s.Request(t, "POST", "/settings/ssh-keys", db.SSHKeyDTO{
|
||||
Title: "Test SSH Key",
|
||||
Content: `ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAklOUpkDHrfHY17SbrmTIpNLTGK9Tjom/BWDSUGPl+nafzlHDTYW7hdI4yZ5ew18JH4JW9jbhUFrviQzM7xlELEVf4h9lFX5QVkbPppSwg0cda3Pbv7kOdJ/MTyBlWXFCR+HAo3FXRitBqxiX1nKhXpHAZsMciLq8V6RjsNAQwdsdMFvSlVK/7XAt3FaoJoAsncM1Q9x5+3V0Ww68/eIFmb1zuUFljQJKprrX88XypNDvjYNby6vw/Pb0rwert/EnmZ+AW4OZPnTPI89ZPmVMLuayrD2cE86Z/il8b+gw3r3+1nKatmIkjn2so1d01QraTlMqVSsbxNrRFi9wrf+M7Q== admin@admin.local`,
|
||||
}, 302)
|
||||
// TestMetrics tests the metrics endpoint functionality of the application.
|
||||
// It verifies that the metrics endpoint correctly reports counts for:
|
||||
// - Total number of users
|
||||
// - Total number of gists
|
||||
// - Total number of SSH keys
|
||||
//
|
||||
// The test follows these steps:
|
||||
// 1. Sets up test environment
|
||||
// 2. Registers and logs in an admin user
|
||||
// 3. Creates a gist and adds an SSH key
|
||||
// 4. Creates a metrics server and queries the /metrics endpoint
|
||||
// 5. Verifies the reported metrics match expected values
|
||||
func TestMetrics(t *testing.T) {
|
||||
s := Setup(t)
|
||||
defer Teardown(t, s)
|
||||
|
||||
metricsServer := webtest.NewTestMetricsServer()
|
||||
register(t, s, AdminUser)
|
||||
login(t, s, AdminUser)
|
||||
|
||||
err := s.Request("GET", "/all", nil, 200)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = s.Request("POST", "/", SimpleGist, 302)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = s.Request("POST", "/settings/ssh-keys", SSHKey, 302)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create a metrics server and query it
|
||||
metricsServer := NewTestMetricsServer()
|
||||
|
||||
req := httptest.NewRequest("GET", "/metrics", nil)
|
||||
w := httptest.NewRecorder()
|
||||
@@ -1,6 +1,8 @@
|
||||
package test
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
@@ -8,74 +10,80 @@ import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/schema"
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/thomiceli/opengist/internal/config"
|
||||
"github.com/thomiceli/opengist/internal/db"
|
||||
"github.com/thomiceli/opengist/internal/git"
|
||||
"github.com/thomiceli/opengist/internal/index"
|
||||
"github.com/thomiceli/opengist/internal/web/context"
|
||||
"github.com/thomiceli/opengist/internal/web/handlers/metrics"
|
||||
"github.com/thomiceli/opengist/internal/web/server"
|
||||
)
|
||||
|
||||
var databaseType string
|
||||
var formEncoder *schema.Encoder
|
||||
|
||||
func init() {
|
||||
formEncoder = schema.NewEncoder()
|
||||
formEncoder.SetAliasTag("form")
|
||||
}
|
||||
|
||||
type Server struct {
|
||||
type TestServer struct {
|
||||
server *server.Server
|
||||
SessionCookie string
|
||||
contextData echo.Map
|
||||
sessionCookie string
|
||||
}
|
||||
|
||||
func (s *Server) Request(t *testing.T, method, uri string, data interface{}, expectedCode int) *http.Response {
|
||||
return s.RequestWithHeaders(t, method, uri, data, expectedCode, nil)
|
||||
}
|
||||
|
||||
func (s *Server) RequestWithHeaders(t *testing.T, method, uri string, data interface{}, expectedCode int, headers map[string]string) *http.Response {
|
||||
var bodyReader io.Reader
|
||||
if method == http.MethodPost || method == http.MethodPut || method == http.MethodDelete {
|
||||
if values, ok := data.(url.Values); ok {
|
||||
bodyReader = strings.NewReader(values.Encode())
|
||||
} else if data != nil {
|
||||
values := url.Values{}
|
||||
_ = formEncoder.Encode(data, values)
|
||||
bodyReader = strings.NewReader(values.Encode())
|
||||
}
|
||||
func newTestServer() (*TestServer, error) {
|
||||
s := &TestServer{
|
||||
server: server.NewServer(true, filepath.Join(config.GetHomeDir(), "tmp", "sessions"), true),
|
||||
}
|
||||
|
||||
req := httptest.NewRequest(method, uri, bodyReader)
|
||||
go s.start()
|
||||
return s, nil
|
||||
}
|
||||
|
||||
func (s *TestServer) start() {
|
||||
s.server.Start()
|
||||
}
|
||||
|
||||
func (s *TestServer) stop() {
|
||||
s.server.Stop()
|
||||
}
|
||||
|
||||
func (s *TestServer) Request(method, uri string, data interface{}, expectedCode int, responsePtr ...*http.Response) error {
|
||||
return s.RequestWithHeaders(method, uri, data, expectedCode, nil, responsePtr...)
|
||||
}
|
||||
|
||||
func (s *TestServer) RequestWithHeaders(method, uri string, data interface{}, expectedCode int, headers map[string]string, responsePtr ...*http.Response) error {
|
||||
var bodyReader io.Reader
|
||||
if method == http.MethodPost || method == http.MethodPut {
|
||||
values := structToURLValues(data)
|
||||
bodyReader = strings.NewReader(values.Encode())
|
||||
}
|
||||
|
||||
req := httptest.NewRequest(method, "http://localhost:6157"+uri, bodyReader)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
if method == http.MethodPost || method == http.MethodPut {
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
}
|
||||
|
||||
req.Header.Set("Sec-Fetch-Site", "same-origin")
|
||||
|
||||
for key, value := range headers {
|
||||
req.Header.Set(key, value)
|
||||
}
|
||||
|
||||
if s.SessionCookie != "" {
|
||||
req.AddCookie(&http.Cookie{Name: "session", Value: s.SessionCookie})
|
||||
if s.sessionCookie != "" {
|
||||
req.AddCookie(&http.Cookie{Name: "session", Value: s.sessionCookie})
|
||||
}
|
||||
|
||||
s.server.ServeHTTP(w, req)
|
||||
if expectedCode != 0 {
|
||||
require.Equalf(t, expectedCode, w.Code, "Unexpected status code for %s %s: got %d, expected %d", method, uri, w.Code, expectedCode)
|
||||
|
||||
if w.Code != expectedCode {
|
||||
return fmt.Errorf("unexpected status code %d, expected %d", w.Code, expectedCode)
|
||||
}
|
||||
|
||||
if method == http.MethodPost {
|
||||
if strings.Contains(uri, "/login") {
|
||||
if strings.Contains(uri, "/login") || strings.Contains(uri, "/register") {
|
||||
cookie := ""
|
||||
h := w.Header().Get("Set-Cookie")
|
||||
parts := strings.Split(h, "; ")
|
||||
@@ -85,119 +93,81 @@ func (s *Server) RequestWithHeaders(t *testing.T, method, uri string, data inter
|
||||
break
|
||||
}
|
||||
}
|
||||
s.SessionCookie = strings.TrimPrefix(cookie, "session=")
|
||||
if cookie == "" {
|
||||
return errors.New("unable to find access session token in response headers")
|
||||
}
|
||||
s.sessionCookie = strings.TrimPrefix(cookie, "session=")
|
||||
} else if strings.Contains(uri, "/logout") {
|
||||
s.SessionCookie = ""
|
||||
s.sessionCookie = ""
|
||||
}
|
||||
}
|
||||
|
||||
return w.Result()
|
||||
}
|
||||
|
||||
func (s *Server) RawRequest(t *testing.T, req *http.Request, expectedCode int) *http.Response {
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
req.Header.Set("Sec-Fetch-Site", "same-origin")
|
||||
|
||||
if s.SessionCookie != "" {
|
||||
req.AddCookie(&http.Cookie{Name: "session", Value: s.SessionCookie})
|
||||
// If a response pointer was provided, fill it with the response data
|
||||
if len(responsePtr) > 0 && responsePtr[0] != nil {
|
||||
*responsePtr[0] = *w.Result()
|
||||
}
|
||||
|
||||
s.server.ServeHTTP(w, req)
|
||||
|
||||
require.Equal(t, expectedCode, w.Code, "unexpected status code for %s %s", req.Method, req.URL.Path)
|
||||
|
||||
return w.Result()
|
||||
}
|
||||
|
||||
func (s *Server) StartHttpServer(t *testing.T) string {
|
||||
hs := httptest.NewServer(s.server)
|
||||
t.Cleanup(hs.Close)
|
||||
return hs.URL
|
||||
}
|
||||
|
||||
func (s *Server) User() *db.User {
|
||||
s.Request(nil, "GET", "/", nil, 0)
|
||||
if user, ok := s.contextData["userLogged"].(*db.User); ok {
|
||||
return user
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Server) TestCtxData(t *testing.T, expected echo.Map) {
|
||||
for key, expectedValue := range expected {
|
||||
actualValue, exists := s.contextData[key]
|
||||
require.True(t, exists, "Key %q not found in context data", key)
|
||||
require.Equal(t, expectedValue, actualValue, "Context data mismatch for key %q", key)
|
||||
func structToURLValues(s interface{}) url.Values {
|
||||
v := url.Values{}
|
||||
if s == nil {
|
||||
return v
|
||||
}
|
||||
|
||||
rValue := reflect.ValueOf(s)
|
||||
if rValue.Kind() != reflect.Struct {
|
||||
return v
|
||||
}
|
||||
|
||||
for i := 0; i < rValue.NumField(); i++ {
|
||||
field := rValue.Type().Field(i)
|
||||
tag := field.Tag.Get("form")
|
||||
if tag != "" || field.Anonymous {
|
||||
if field.Type.Kind() == reflect.Int {
|
||||
fieldValue := rValue.Field(i).Int()
|
||||
v.Add(tag, strconv.FormatInt(fieldValue, 10))
|
||||
} else if field.Type.Kind() == reflect.Uint {
|
||||
fieldValue := rValue.Field(i).Uint()
|
||||
v.Add(tag, strconv.FormatUint(fieldValue, 10))
|
||||
} else if field.Type.Kind() == reflect.Slice {
|
||||
fieldValue := rValue.Field(i).Interface().([]string)
|
||||
for _, va := range fieldValue {
|
||||
v.Add(tag, va)
|
||||
}
|
||||
} else if field.Type.Kind() == reflect.Struct {
|
||||
for key, val := range structToURLValues(rValue.Field(i).Interface()) {
|
||||
for _, vv := range val {
|
||||
v.Add(key, vv)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
fieldValue := rValue.Field(i).String()
|
||||
v.Add(tag, fieldValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
func (s *Server) Register(t *testing.T, user string) {
|
||||
s.Request(t, "POST", "/register", db.UserDTO{Username: user, Password: user}, 302)
|
||||
}
|
||||
|
||||
func (s *Server) Login(t *testing.T, user string) {
|
||||
s.Request(t, "POST", "/login", db.UserDTO{Username: user, Password: user}, 302)
|
||||
}
|
||||
|
||||
func (s *Server) Logout() {
|
||||
s.SessionCookie = ""
|
||||
}
|
||||
|
||||
func (s *Server) CreateGist(t *testing.T, visibility string) (gistPath string, gist *db.Gist, username, identifier string) {
|
||||
s.Request(t, "POST", "/register", db.UserDTO{Username: "thomas", Password: "thomas"}, 0)
|
||||
s.Login(t, "thomas")
|
||||
|
||||
resp := s.Request(t, "POST", "/", url.Values{
|
||||
"title": {"Test"},
|
||||
"name": {"file.txt", "otherfile.txt"},
|
||||
"content": {"hello world", "other content"},
|
||||
"topics": {"hello opengist"},
|
||||
"private": {visibility},
|
||||
}, 302)
|
||||
|
||||
// Extract gist identifier from redirect
|
||||
location := resp.Header.Get("Location")
|
||||
parts := strings.Split(strings.TrimPrefix(location, "/"), "/")
|
||||
require.Len(t, parts, 2, "Expected redirect format: /{username}/{identifier}")
|
||||
|
||||
gistUsername := parts[0]
|
||||
gistIdentifier := parts[1]
|
||||
|
||||
gist, err := db.GetGist(gistUsername, gistIdentifier)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, gist)
|
||||
|
||||
gistPath = filepath.Join(config.GetHomeDir(), git.ReposDirectory, "thomas", gist.Uuid)
|
||||
|
||||
// Verify gist exists on filesystem
|
||||
_, err = os.Stat(gistPath)
|
||||
require.NoError(t, err, "Gist repository should exist at %s", gistPath)
|
||||
|
||||
username = gist.User.Username
|
||||
identifier = gist.Identifier()
|
||||
|
||||
s.Logout()
|
||||
return gistPath, gist, username, identifier
|
||||
}
|
||||
|
||||
func Setup(t *testing.T) *Server {
|
||||
tmpDir := t.TempDir()
|
||||
t.Setenv("OPENGIST_SKIP_GIT_HOOKS", "1")
|
||||
func Setup(t *testing.T) *TestServer {
|
||||
_ = os.Setenv("OPENGIST_SKIP_GIT_HOOKS", "1")
|
||||
|
||||
err := config.InitConfig("", io.Discard)
|
||||
require.NoError(t, err, "Could not init config")
|
||||
|
||||
config.C.LogLevel = "warn"
|
||||
config.C.LogOutput = "stdout"
|
||||
config.C.GitDefaultBranch = "master"
|
||||
config.C.OpengistHome = tmpDir
|
||||
err = os.MkdirAll(filepath.Join(config.GetHomeDir()), 0755)
|
||||
require.NoError(t, err, "Could not create Opengist home directory")
|
||||
|
||||
config.SetupSecretKey()
|
||||
config.InitLog()
|
||||
|
||||
tmpGitConfig := filepath.Join(tmpDir, "gitconfig")
|
||||
t.Setenv("GIT_CONFIG_GLOBAL", tmpGitConfig)
|
||||
git.ReposDirectory = filepath.Join("tests")
|
||||
|
||||
config.C.Index = ""
|
||||
config.C.LogLevel = "error"
|
||||
config.C.GitDefaultBranch = "master"
|
||||
config.InitLog()
|
||||
|
||||
err = exec.Command("git", "config", "--global", "--type", "bool", "push.autoSetupRemote", "true").Run()
|
||||
require.NoError(t, err)
|
||||
@@ -205,7 +175,9 @@ func Setup(t *testing.T) *Server {
|
||||
require.NoError(t, err)
|
||||
err = exec.Command("git", "config", "--global", "user.name", "test").Run()
|
||||
require.NoError(t, err)
|
||||
|
||||
homePath := config.GetHomeDir()
|
||||
log.Info().Msg("Data directory: " + homePath)
|
||||
|
||||
var databaseDsn string
|
||||
databaseType = os.Getenv("OPENGIST_TEST_DB")
|
||||
@@ -215,55 +187,70 @@ func Setup(t *testing.T) *Server {
|
||||
case "mysql":
|
||||
databaseDsn = "mysql://root:opengist@localhost:3306/opengist_test"
|
||||
default:
|
||||
databaseDsn = config.C.DBUri
|
||||
databaseDsn = "file:" + filepath.Join(homePath, "tmp", "opengist_test.db")
|
||||
}
|
||||
|
||||
err = os.MkdirAll(filepath.Join(homePath, "sessions"), 0755)
|
||||
require.NoError(t, err, "Could not create sessions directory")
|
||||
err = os.MkdirAll(filepath.Join(homePath, "tests"), 0755)
|
||||
require.NoError(t, err, "Could not create tests directory")
|
||||
|
||||
err = os.MkdirAll(filepath.Join(homePath, "repos"), 0755)
|
||||
require.NoError(t, err, "Could not create repos directory")
|
||||
err = os.MkdirAll(filepath.Join(homePath, "tmp", "sessions"), 0755)
|
||||
require.NoError(t, err, "Could not create sessions directory")
|
||||
|
||||
err = os.MkdirAll(filepath.Join(homePath, "tmp", "repos"), 0755)
|
||||
require.NoError(t, err, "Could not create tmp repos directory")
|
||||
|
||||
err = os.MkdirAll(filepath.Join(homePath, "custom"), 0755)
|
||||
require.NoError(t, err, "Could not create custom directory")
|
||||
|
||||
err = db.Setup(databaseDsn)
|
||||
require.NoError(t, err, "Could not initialize database")
|
||||
|
||||
t.Cleanup(func() {
|
||||
db.Close()
|
||||
})
|
||||
|
||||
if index.IndexEnabled() {
|
||||
go index.NewIndexer(index.IndexType())
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("Could not initialize database")
|
||||
}
|
||||
|
||||
s := &Server{
|
||||
server: server.NewServer(true),
|
||||
}
|
||||
// err = index.Open(filepath.Join(homePath, "testsindex", "opengist.index"))
|
||||
// require.NoError(t, err, "Could not open index")
|
||||
|
||||
s.server.Use(func(next echo.HandlerFunc) echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
err := next(c)
|
||||
if data, ok := c.Request().Context().Value(context.DataKeyStr).(echo.Map); ok {
|
||||
s.contextData = data
|
||||
}
|
||||
return err
|
||||
}
|
||||
})
|
||||
s, err := newTestServer()
|
||||
require.NoError(t, err, "Failed to create test server")
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
func Teardown(t *testing.T) {
|
||||
switch databaseType {
|
||||
case "postgres", "mysql":
|
||||
err := db.TruncateDatabase()
|
||||
require.NoError(t, err, "Could not truncate database")
|
||||
func Teardown(t *testing.T, s *TestServer) {
|
||||
s.stop()
|
||||
|
||||
//err := db.Close()
|
||||
//require.NoError(t, err, "Could not close database")
|
||||
|
||||
err := db.TruncateDatabase()
|
||||
require.NoError(t, err, "Could not truncate database")
|
||||
|
||||
err = os.RemoveAll(filepath.Join(config.GetHomeDir(), "tests"))
|
||||
require.NoError(t, err, "Could not remove repos directory")
|
||||
|
||||
if runtime.GOOS == "windows" {
|
||||
err = db.Close()
|
||||
require.NoError(t, err, "Could not close database")
|
||||
|
||||
time.Sleep(2 * time.Second)
|
||||
}
|
||||
err = os.RemoveAll(filepath.Join(config.GetHomeDir(), "tmp"))
|
||||
require.NoError(t, err, "Could not remove tmp directory")
|
||||
|
||||
// err = os.RemoveAll(path.Join(config.C.OpengistHome, "testsindex"))
|
||||
// require.NoError(t, err, "Could not remove repos directory")
|
||||
|
||||
// err = index.Close()
|
||||
// require.NoError(t, err, "Could not close index")
|
||||
}
|
||||
|
||||
type settingSet struct {
|
||||
key string `form:"key"`
|
||||
value string `form:"value"`
|
||||
}
|
||||
|
||||
type invitationAdmin struct {
|
||||
nbMax string `form:"nbMax"`
|
||||
expiredAtUnix string `form:"expiredAtUnix"`
|
||||
}
|
||||
|
||||
func NewTestMetricsServer() *metrics.Server {
|
||||
|
||||
22
internal/web/test/settings_test.go
Normal file
22
internal/web/test/settings_test.go
Normal file
@@ -0,0 +1,22 @@
|
||||
package test
|
||||
|
||||
import (
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/thomiceli/opengist/internal/db"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestSettingsPage(t *testing.T) {
|
||||
s := Setup(t)
|
||||
defer Teardown(t, s)
|
||||
|
||||
err := s.Request("GET", "/settings", nil, 302)
|
||||
require.NoError(t, err)
|
||||
|
||||
user1 := db.UserDTO{Username: "thomas", Password: "thomas"}
|
||||
register(t, s, user1)
|
||||
login(t, s, user1)
|
||||
|
||||
err = s.Request("GET", "/settings", nil, 200)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
330
package-lock.json
generated
330
package-lock.json
generated
@@ -8,23 +8,23 @@
|
||||
"devDependencies": {
|
||||
"@catppuccin/highlightjs": "^1.0.1",
|
||||
"@codemirror/commands": "^6.10.1",
|
||||
"@codemirror/lang-javascript": "^6.2.5",
|
||||
"@codemirror/language": "^6.12.2",
|
||||
"@codemirror/lang-javascript": "^6.2.4",
|
||||
"@codemirror/language": "^6.12.1",
|
||||
"@codemirror/state": "^6.5.4",
|
||||
"@codemirror/text": "^0.19.6",
|
||||
"@codemirror/view": "^6.39.16",
|
||||
"@codemirror/view": "^6.39.11",
|
||||
"@tailwindcss/forms": "^0.5.11",
|
||||
"@tailwindcss/typography": "^0.5.19",
|
||||
"@tailwindcss/vite": "^4.2.1",
|
||||
"@tailwindcss/vite": "^4.1.18",
|
||||
"codemirror": "^6.0.2",
|
||||
"github-markdown-css": "^5.9.0",
|
||||
"github-markdown-css": "^5.8.1",
|
||||
"highlight.js": "^11.11.1",
|
||||
"jdenticon": "^3.3.0",
|
||||
"katex": "^0.16.38",
|
||||
"marked": "^17.0.4",
|
||||
"katex": "^0.16.28",
|
||||
"marked": "^17.0.1",
|
||||
"nodemon": "^3.1.11",
|
||||
"pdfobject": "^2.3.1",
|
||||
"tailwindcss": "^4.2.1",
|
||||
"tailwindcss": "^4.1.18",
|
||||
"vite": "^7.3.1"
|
||||
}
|
||||
},
|
||||
@@ -78,9 +78,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/lang-javascript": {
|
||||
"version": "6.2.5",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/lang-javascript/-/lang-javascript-6.2.5.tgz",
|
||||
"integrity": "sha512-zD4e5mS+50htS7F+TYjBPsiIFGanfVqg4HyUz6WNFikgOPf2BgKlx+TQedI1w6n/IqRBVBbBWmGFdLB/7uxO4A==",
|
||||
"version": "6.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/lang-javascript/-/lang-javascript-6.2.4.tgz",
|
||||
"integrity": "sha512-0WVmhp1QOqZ4Rt6GlVGwKJN3KW7Xh4H2q8ZZNGZaP6lRdxXJzmjm4FqvmOojVj6khWJHIb9sp7U/72W7xQgqAA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -94,9 +94,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/language": {
|
||||
"version": "6.12.2",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.12.2.tgz",
|
||||
"integrity": "sha512-jEPmz2nGGDxhRTg3lTpzmIyGKxz3Gp3SJES4b0nAuE5SWQoKdT5GoQ69cwMmFd+wvFUhYirtDTr0/DRHpQAyWg==",
|
||||
"version": "6.12.1",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.12.1.tgz",
|
||||
"integrity": "sha512-Fa6xkSiuGKc8XC8Cn96T+TQHYj4ZZ7RdFmXA3i9xe/3hLHfwPZdM+dqfX0Cp0zQklBKhVD8Yzc8LS45rkqcwpQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -150,9 +150,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@codemirror/view": {
|
||||
"version": "6.39.16",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.39.16.tgz",
|
||||
"integrity": "sha512-m6S22fFpKtOWhq8HuhzsI1WzUP/hB9THbDj0Tl5KX4gbO6Y91hwBl7Yky33NdvB6IffuRFiBxf1R8kJMyXmA4Q==",
|
||||
"version": "6.39.11",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.39.11.tgz",
|
||||
"integrity": "sha512-bWdeR8gWM87l4DB/kYSF9A+dVackzDb/V56Tq7QVrQ7rn86W0rgZFtlL3g3pem6AeGcb9NQNoy3ao4WpW4h5tQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -1022,49 +1022,49 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/node": {
|
||||
"version": "4.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.2.1.tgz",
|
||||
"integrity": "sha512-jlx6sLk4EOwO6hHe1oCGm1Q4AN/s0rSrTTPBGPM0/RQ6Uylwq17FuU8IeJJKEjtc6K6O07zsvP+gDO6MMWo7pg==",
|
||||
"version": "4.1.18",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.18.tgz",
|
||||
"integrity": "sha512-DoR7U1P7iYhw16qJ49fgXUlry1t4CpXeErJHnQ44JgTSKMaZUdf17cfn5mHchfJ4KRBZRFA/Coo+MUF5+gOaCQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/remapping": "^2.3.5",
|
||||
"enhanced-resolve": "^5.19.0",
|
||||
"@jridgewell/remapping": "^2.3.4",
|
||||
"enhanced-resolve": "^5.18.3",
|
||||
"jiti": "^2.6.1",
|
||||
"lightningcss": "1.31.1",
|
||||
"lightningcss": "1.30.2",
|
||||
"magic-string": "^0.30.21",
|
||||
"source-map-js": "^1.2.1",
|
||||
"tailwindcss": "4.2.1"
|
||||
"tailwindcss": "4.1.18"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide": {
|
||||
"version": "4.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.2.1.tgz",
|
||||
"integrity": "sha512-yv9jeEFWnjKCI6/T3Oq50yQEOqmpmpfzG1hcZsAOaXFQPfzWprWrlHSdGPEF3WQTi8zu8ohC9Mh9J470nT5pUw==",
|
||||
"version": "4.1.18",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.18.tgz",
|
||||
"integrity": "sha512-EgCR5tTS5bUSKQgzeMClT6iCY3ToqE1y+ZB0AKldj809QXk1Y+3jB0upOYZrn9aGIzPtUsP7sX4QQ4XtjBB95A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 20"
|
||||
"node": ">= 10"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@tailwindcss/oxide-android-arm64": "4.2.1",
|
||||
"@tailwindcss/oxide-darwin-arm64": "4.2.1",
|
||||
"@tailwindcss/oxide-darwin-x64": "4.2.1",
|
||||
"@tailwindcss/oxide-freebsd-x64": "4.2.1",
|
||||
"@tailwindcss/oxide-linux-arm-gnueabihf": "4.2.1",
|
||||
"@tailwindcss/oxide-linux-arm64-gnu": "4.2.1",
|
||||
"@tailwindcss/oxide-linux-arm64-musl": "4.2.1",
|
||||
"@tailwindcss/oxide-linux-x64-gnu": "4.2.1",
|
||||
"@tailwindcss/oxide-linux-x64-musl": "4.2.1",
|
||||
"@tailwindcss/oxide-wasm32-wasi": "4.2.1",
|
||||
"@tailwindcss/oxide-win32-arm64-msvc": "4.2.1",
|
||||
"@tailwindcss/oxide-win32-x64-msvc": "4.2.1"
|
||||
"@tailwindcss/oxide-android-arm64": "4.1.18",
|
||||
"@tailwindcss/oxide-darwin-arm64": "4.1.18",
|
||||
"@tailwindcss/oxide-darwin-x64": "4.1.18",
|
||||
"@tailwindcss/oxide-freebsd-x64": "4.1.18",
|
||||
"@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.18",
|
||||
"@tailwindcss/oxide-linux-arm64-gnu": "4.1.18",
|
||||
"@tailwindcss/oxide-linux-arm64-musl": "4.1.18",
|
||||
"@tailwindcss/oxide-linux-x64-gnu": "4.1.18",
|
||||
"@tailwindcss/oxide-linux-x64-musl": "4.1.18",
|
||||
"@tailwindcss/oxide-wasm32-wasi": "4.1.18",
|
||||
"@tailwindcss/oxide-win32-arm64-msvc": "4.1.18",
|
||||
"@tailwindcss/oxide-win32-x64-msvc": "4.1.18"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-android-arm64": {
|
||||
"version": "4.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.2.1.tgz",
|
||||
"integrity": "sha512-eZ7G1Zm5EC8OOKaesIKuw77jw++QJ2lL9N+dDpdQiAB/c/B2wDh0QPFHbkBVrXnwNugvrbJFk1gK2SsVjwWReg==",
|
||||
"version": "4.1.18",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.18.tgz",
|
||||
"integrity": "sha512-dJHz7+Ugr9U/diKJA0W6N/6/cjI+ZTAoxPf9Iz9BFRF2GzEX8IvXxFIi/dZBloVJX/MZGvRuFA9rqwdiIEZQ0Q==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -1075,13 +1075,13 @@
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 20"
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-darwin-arm64": {
|
||||
"version": "4.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.2.1.tgz",
|
||||
"integrity": "sha512-q/LHkOstoJ7pI1J0q6djesLzRvQSIfEto148ppAd+BVQK0JYjQIFSK3JgYZJa+Yzi0DDa52ZsQx2rqytBnf8Hw==",
|
||||
"version": "4.1.18",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.18.tgz",
|
||||
"integrity": "sha512-Gc2q4Qhs660bhjyBSKgq6BYvwDz4G+BuyJ5H1xfhmDR3D8HnHCmT/BSkvSL0vQLy/nkMLY20PQ2OoYMO15Jd0A==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -1092,13 +1092,13 @@
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 20"
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-darwin-x64": {
|
||||
"version": "4.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.2.1.tgz",
|
||||
"integrity": "sha512-/f/ozlaXGY6QLbpvd/kFTro2l18f7dHKpB+ieXz+Cijl4Mt9AI2rTrpq7V+t04nK+j9XBQHnSMdeQRhbGyt6fw==",
|
||||
"version": "4.1.18",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.18.tgz",
|
||||
"integrity": "sha512-FL5oxr2xQsFrc3X9o1fjHKBYBMD1QZNyc1Xzw/h5Qu4XnEBi3dZn96HcHm41c/euGV+GRiXFfh2hUCyKi/e+yw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -1109,13 +1109,13 @@
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 20"
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-freebsd-x64": {
|
||||
"version": "4.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.2.1.tgz",
|
||||
"integrity": "sha512-5e/AkgYJT/cpbkys/OU2Ei2jdETCLlifwm7ogMC7/hksI2fC3iiq6OcXwjibcIjPung0kRtR3TxEITkqgn0TcA==",
|
||||
"version": "4.1.18",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.18.tgz",
|
||||
"integrity": "sha512-Fj+RHgu5bDodmV1dM9yAxlfJwkkWvLiRjbhuO2LEtwtlYlBgiAT4x/j5wQr1tC3SANAgD+0YcmWVrj8R9trVMA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -1126,13 +1126,13 @@
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 20"
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": {
|
||||
"version": "4.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.2.1.tgz",
|
||||
"integrity": "sha512-Uny1EcVTTmerCKt/1ZuKTkb0x8ZaiuYucg2/kImO5A5Y/kBz41/+j0gxUZl+hTF3xkWpDmHX+TaWhOtba2Fyuw==",
|
||||
"version": "4.1.18",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.18.tgz",
|
||||
"integrity": "sha512-Fp+Wzk/Ws4dZn+LV2Nqx3IilnhH51YZoRaYHQsVq3RQvEl+71VGKFpkfHrLM/Li+kt5c0DJe/bHXK1eHgDmdiA==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@@ -1143,13 +1143,13 @@
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 20"
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-linux-arm64-gnu": {
|
||||
"version": "4.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.2.1.tgz",
|
||||
"integrity": "sha512-CTrwomI+c7n6aSSQlsPL0roRiNMDQ/YzMD9EjcR+H4f0I1SQ8QqIuPnsVp7QgMkC1Qi8rtkekLkOFjo7OlEFRQ==",
|
||||
"version": "4.1.18",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.18.tgz",
|
||||
"integrity": "sha512-S0n3jboLysNbh55Vrt7pk9wgpyTTPD0fdQeh7wQfMqLPM/Hrxi+dVsLsPrycQjGKEQk85Kgbx+6+QnYNiHalnw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -1160,13 +1160,13 @@
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 20"
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-linux-arm64-musl": {
|
||||
"version": "4.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.2.1.tgz",
|
||||
"integrity": "sha512-WZA0CHRL/SP1TRbA5mp9htsppSEkWuQ4KsSUumYQnyl8ZdT39ntwqmz4IUHGN6p4XdSlYfJwM4rRzZLShHsGAQ==",
|
||||
"version": "4.1.18",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.18.tgz",
|
||||
"integrity": "sha512-1px92582HkPQlaaCkdRcio71p8bc8i/ap5807tPRDK/uw953cauQBT8c5tVGkOwrHMfc2Yh6UuxaH4vtTjGvHg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -1177,13 +1177,13 @@
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 20"
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-linux-x64-gnu": {
|
||||
"version": "4.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.2.1.tgz",
|
||||
"integrity": "sha512-qMFzxI2YlBOLW5PhblzuSWlWfwLHaneBE0xHzLrBgNtqN6mWfs+qYbhryGSXQjFYB1Dzf5w+LN5qbUTPhW7Y5g==",
|
||||
"version": "4.1.18",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.18.tgz",
|
||||
"integrity": "sha512-v3gyT0ivkfBLoZGF9LyHmts0Isc8jHZyVcbzio6Wpzifg/+5ZJpDiRiUhDLkcr7f/r38SWNe7ucxmGW3j3Kb/g==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -1194,13 +1194,13 @@
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 20"
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-linux-x64-musl": {
|
||||
"version": "4.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.2.1.tgz",
|
||||
"integrity": "sha512-5r1X2FKnCMUPlXTWRYpHdPYUY6a1Ar/t7P24OuiEdEOmms5lyqjDRvVY1yy9Rmioh+AunQ0rWiOTPE8F9A3v5g==",
|
||||
"version": "4.1.18",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.18.tgz",
|
||||
"integrity": "sha512-bhJ2y2OQNlcRwwgOAGMY0xTFStt4/wyU6pvI6LSuZpRgKQwxTec0/3Scu91O8ir7qCR3AuepQKLU/kX99FouqQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -1211,13 +1211,13 @@
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 20"
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-wasm32-wasi": {
|
||||
"version": "4.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.2.1.tgz",
|
||||
"integrity": "sha512-MGFB5cVPvshR85MTJkEvqDUnuNoysrsRxd6vnk1Lf2tbiqNlXpHYZqkqOQalydienEWOHHFyyuTSYRsLfxFJ2Q==",
|
||||
"version": "4.1.18",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.18.tgz",
|
||||
"integrity": "sha512-LffYTvPjODiP6PT16oNeUQJzNVyJl1cjIebq/rWWBF+3eDst5JGEFSc5cWxyRCJ0Mxl+KyIkqRxk1XPEs9x8TA==",
|
||||
"bundleDependencies": [
|
||||
"@napi-rs/wasm-runtime",
|
||||
"@emnapi/core",
|
||||
@@ -1233,19 +1233,19 @@
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@emnapi/core": "^1.8.1",
|
||||
"@emnapi/runtime": "^1.8.1",
|
||||
"@emnapi/core": "^1.7.1",
|
||||
"@emnapi/runtime": "^1.7.1",
|
||||
"@emnapi/wasi-threads": "^1.1.0",
|
||||
"@napi-rs/wasm-runtime": "^1.1.1",
|
||||
"@napi-rs/wasm-runtime": "^1.1.0",
|
||||
"@tybys/wasm-util": "^0.10.1",
|
||||
"tslib": "^2.8.1"
|
||||
"tslib": "^2.4.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/core": {
|
||||
"version": "1.8.1",
|
||||
"version": "1.7.1",
|
||||
"dev": true,
|
||||
"inBundle": true,
|
||||
"license": "MIT",
|
||||
@@ -1256,7 +1256,7 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/runtime": {
|
||||
"version": "1.8.1",
|
||||
"version": "1.7.1",
|
||||
"dev": true,
|
||||
"inBundle": true,
|
||||
"license": "MIT",
|
||||
@@ -1276,7 +1276,7 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@napi-rs/wasm-runtime": {
|
||||
"version": "1.1.1",
|
||||
"version": "1.1.0",
|
||||
"dev": true,
|
||||
"inBundle": true,
|
||||
"license": "MIT",
|
||||
@@ -1285,10 +1285,6 @@
|
||||
"@emnapi/core": "^1.7.1",
|
||||
"@emnapi/runtime": "^1.7.1",
|
||||
"@tybys/wasm-util": "^0.10.1"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/Brooooooklyn"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@tybys/wasm-util": {
|
||||
@@ -1309,9 +1305,9 @@
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-win32-arm64-msvc": {
|
||||
"version": "4.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.2.1.tgz",
|
||||
"integrity": "sha512-YlUEHRHBGnCMh4Nj4GnqQyBtsshUPdiNroZj8VPkvTZSoHsilRCwXcVKnG9kyi0ZFAS/3u+qKHBdDc81SADTRA==",
|
||||
"version": "4.1.18",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.18.tgz",
|
||||
"integrity": "sha512-HjSA7mr9HmC8fu6bdsZvZ+dhjyGCLdotjVOgLA2vEqxEBZaQo9YTX4kwgEvPCpRh8o4uWc4J/wEoFzhEmjvPbA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -1322,13 +1318,13 @@
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 20"
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-win32-x64-msvc": {
|
||||
"version": "4.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.2.1.tgz",
|
||||
"integrity": "sha512-rbO34G5sMWWyrN/idLeVxAZgAKWrn5LiR3/I90Q9MkA67s6T1oB0xtTe+0heoBvHSpbU9Mk7i6uwJnpo4u21XQ==",
|
||||
"version": "4.1.18",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.18.tgz",
|
||||
"integrity": "sha512-bJWbyYpUlqamC8dpR7pfjA0I7vdF6t5VpUGMWRkXVE3AXgIZjYUYAK7II1GNaxR8J1SSrSrppRar8G++JekE3Q==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -1339,7 +1335,7 @@
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 20"
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/typography": {
|
||||
@@ -1356,15 +1352,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/vite": {
|
||||
"version": "4.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.2.1.tgz",
|
||||
"integrity": "sha512-TBf2sJjYeb28jD2U/OhwdW0bbOsxkWPwQ7SrqGf9sVcoYwZj7rkXljroBO9wKBut9XnmQLXanuDUeqQK0lGg/w==",
|
||||
"version": "4.1.18",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.1.18.tgz",
|
||||
"integrity": "sha512-jVA+/UpKL1vRLg6Hkao5jldawNmRo7mQYrZtNHMIVpLfLhDml5nMRUo/8MwoX2vNXvnaXNNMedrMfMugAVX1nA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@tailwindcss/node": "4.2.1",
|
||||
"@tailwindcss/oxide": "4.2.1",
|
||||
"tailwindcss": "4.2.1"
|
||||
"@tailwindcss/node": "4.1.18",
|
||||
"@tailwindcss/oxide": "4.1.18",
|
||||
"tailwindcss": "4.1.18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"vite": "^5.2.0 || ^6 || ^7"
|
||||
@@ -1552,14 +1548,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/enhanced-resolve": {
|
||||
"version": "5.20.0",
|
||||
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.20.0.tgz",
|
||||
"integrity": "sha512-/ce7+jQ1PQ6rVXwe+jKEg5hW5ciicHwIQUagZkp6IufBoY3YDgdTTY1azVs0qoRgVmvsNB+rbjLJxDAeHHtwsQ==",
|
||||
"version": "5.18.4",
|
||||
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.4.tgz",
|
||||
"integrity": "sha512-LgQMM4WXU3QI+SYgEc2liRgznaD5ojbmY3sb8LxyguVkIg5FxdpTkvk72te2R38/TGKxH634oLxXRGY6d7AP+Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"graceful-fs": "^4.2.4",
|
||||
"tapable": "^2.3.0"
|
||||
"tapable": "^2.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.13.0"
|
||||
@@ -1636,9 +1632,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/github-markdown-css": {
|
||||
"version": "5.9.0",
|
||||
"resolved": "https://registry.npmjs.org/github-markdown-css/-/github-markdown-css-5.9.0.tgz",
|
||||
"integrity": "sha512-tmT5sY+zvg2302XLYEfH2mtkViIM1SWf2nvYoF5N1ZsO0V6B2qZTiw3GOzw4vpjLygK/KG35qRlPFweHqfzz5w==",
|
||||
"version": "5.8.1",
|
||||
"resolved": "https://registry.npmjs.org/github-markdown-css/-/github-markdown-css-5.8.1.tgz",
|
||||
"integrity": "sha512-8G+PFvqigBQSWLQjyzgpa2ThD9bo7+kDsriUIidGcRhXgmcaAWUIpCZf8DavJgc+xifjbCG+GvMyWr0XMXmc7g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -1768,9 +1764,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/katex": {
|
||||
"version": "0.16.38",
|
||||
"resolved": "https://registry.npmjs.org/katex/-/katex-0.16.38.tgz",
|
||||
"integrity": "sha512-cjHooZUmIAUmDsHBN+1n8LaZdpmbj03LtYeYPyuYB7OuloiaeaV6N4LcfjcnHVzGWjVQmKrxxTrpDcmSzEZQwQ==",
|
||||
"version": "0.16.28",
|
||||
"resolved": "https://registry.npmjs.org/katex/-/katex-0.16.28.tgz",
|
||||
"integrity": "sha512-YHzO7721WbmAL6Ov1uzN/l5mY5WWWhJBSW+jq4tkfZfsxmo1hu6frS0EOswvjBUnWE6NtjEs48SFn5CQESRLZg==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
"https://opencollective.com/katex",
|
||||
@@ -1795,9 +1791,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss": {
|
||||
"version": "1.31.1",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.31.1.tgz",
|
||||
"integrity": "sha512-l51N2r93WmGUye3WuFoN5k10zyvrVs0qfKBhyC5ogUQ6Ew6JUSswh78mbSO+IU3nTWsyOArqPCcShdQSadghBQ==",
|
||||
"version": "1.30.2",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.2.tgz",
|
||||
"integrity": "sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==",
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"dependencies": {
|
||||
@@ -1811,23 +1807,23 @@
|
||||
"url": "https://opencollective.com/parcel"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"lightningcss-android-arm64": "1.31.1",
|
||||
"lightningcss-darwin-arm64": "1.31.1",
|
||||
"lightningcss-darwin-x64": "1.31.1",
|
||||
"lightningcss-freebsd-x64": "1.31.1",
|
||||
"lightningcss-linux-arm-gnueabihf": "1.31.1",
|
||||
"lightningcss-linux-arm64-gnu": "1.31.1",
|
||||
"lightningcss-linux-arm64-musl": "1.31.1",
|
||||
"lightningcss-linux-x64-gnu": "1.31.1",
|
||||
"lightningcss-linux-x64-musl": "1.31.1",
|
||||
"lightningcss-win32-arm64-msvc": "1.31.1",
|
||||
"lightningcss-win32-x64-msvc": "1.31.1"
|
||||
"lightningcss-android-arm64": "1.30.2",
|
||||
"lightningcss-darwin-arm64": "1.30.2",
|
||||
"lightningcss-darwin-x64": "1.30.2",
|
||||
"lightningcss-freebsd-x64": "1.30.2",
|
||||
"lightningcss-linux-arm-gnueabihf": "1.30.2",
|
||||
"lightningcss-linux-arm64-gnu": "1.30.2",
|
||||
"lightningcss-linux-arm64-musl": "1.30.2",
|
||||
"lightningcss-linux-x64-gnu": "1.30.2",
|
||||
"lightningcss-linux-x64-musl": "1.30.2",
|
||||
"lightningcss-win32-arm64-msvc": "1.30.2",
|
||||
"lightningcss-win32-x64-msvc": "1.30.2"
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss-android-arm64": {
|
||||
"version": "1.31.1",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.31.1.tgz",
|
||||
"integrity": "sha512-HXJF3x8w9nQ4jbXRiNppBCqeZPIAfUo8zE/kOEGbW5NZvGc/K7nMxbhIr+YlFlHW5mpbg/YFPdbnCh1wAXCKFg==",
|
||||
"version": "1.30.2",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.30.2.tgz",
|
||||
"integrity": "sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -1846,9 +1842,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss-darwin-arm64": {
|
||||
"version": "1.31.1",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.31.1.tgz",
|
||||
"integrity": "sha512-02uTEqf3vIfNMq3h/z2cJfcOXnQ0GRwQrkmPafhueLb2h7mqEidiCzkE4gBMEH65abHRiQvhdcQ+aP0D0g67sg==",
|
||||
"version": "1.30.2",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.30.2.tgz",
|
||||
"integrity": "sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -1867,9 +1863,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss-darwin-x64": {
|
||||
"version": "1.31.1",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.31.1.tgz",
|
||||
"integrity": "sha512-1ObhyoCY+tGxtsz1lSx5NXCj3nirk0Y0kB/g8B8DT+sSx4G9djitg9ejFnjb3gJNWo7qXH4DIy2SUHvpoFwfTA==",
|
||||
"version": "1.30.2",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.30.2.tgz",
|
||||
"integrity": "sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -1888,9 +1884,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss-freebsd-x64": {
|
||||
"version": "1.31.1",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.31.1.tgz",
|
||||
"integrity": "sha512-1RINmQKAItO6ISxYgPwszQE1BrsVU5aB45ho6O42mu96UiZBxEXsuQ7cJW4zs4CEodPUioj/QrXW1r9pLUM74A==",
|
||||
"version": "1.30.2",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.30.2.tgz",
|
||||
"integrity": "sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -1909,9 +1905,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss-linux-arm-gnueabihf": {
|
||||
"version": "1.31.1",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.31.1.tgz",
|
||||
"integrity": "sha512-OOCm2//MZJ87CdDK62rZIu+aw9gBv4azMJuA8/KB74wmfS3lnC4yoPHm0uXZ/dvNNHmnZnB8XLAZzObeG0nS1g==",
|
||||
"version": "1.30.2",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.30.2.tgz",
|
||||
"integrity": "sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@@ -1930,9 +1926,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss-linux-arm64-gnu": {
|
||||
"version": "1.31.1",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.31.1.tgz",
|
||||
"integrity": "sha512-WKyLWztD71rTnou4xAD5kQT+982wvca7E6QoLpoawZ1gP9JM0GJj4Tp5jMUh9B3AitHbRZ2/H3W5xQmdEOUlLg==",
|
||||
"version": "1.30.2",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.30.2.tgz",
|
||||
"integrity": "sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -1951,9 +1947,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss-linux-arm64-musl": {
|
||||
"version": "1.31.1",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.31.1.tgz",
|
||||
"integrity": "sha512-mVZ7Pg2zIbe3XlNbZJdjs86YViQFoJSpc41CbVmKBPiGmC4YrfeOyz65ms2qpAobVd7WQsbW4PdsSJEMymyIMg==",
|
||||
"version": "1.30.2",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.30.2.tgz",
|
||||
"integrity": "sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -1972,9 +1968,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss-linux-x64-gnu": {
|
||||
"version": "1.31.1",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.31.1.tgz",
|
||||
"integrity": "sha512-xGlFWRMl+0KvUhgySdIaReQdB4FNudfUTARn7q0hh/V67PVGCs3ADFjw+6++kG1RNd0zdGRlEKa+T13/tQjPMA==",
|
||||
"version": "1.30.2",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.30.2.tgz",
|
||||
"integrity": "sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -1993,9 +1989,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss-linux-x64-musl": {
|
||||
"version": "1.31.1",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.31.1.tgz",
|
||||
"integrity": "sha512-eowF8PrKHw9LpoZii5tdZwnBcYDxRw2rRCyvAXLi34iyeYfqCQNA9rmUM0ce62NlPhCvof1+9ivRaTY6pSKDaA==",
|
||||
"version": "1.30.2",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.30.2.tgz",
|
||||
"integrity": "sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -2014,9 +2010,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss-win32-arm64-msvc": {
|
||||
"version": "1.31.1",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.31.1.tgz",
|
||||
"integrity": "sha512-aJReEbSEQzx1uBlQizAOBSjcmr9dCdL3XuC/6HLXAxmtErsj2ICo5yYggg1qOODQMtnjNQv2UHb9NpOuFtYe4w==",
|
||||
"version": "1.30.2",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.30.2.tgz",
|
||||
"integrity": "sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -2035,9 +2031,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss-win32-x64-msvc": {
|
||||
"version": "1.31.1",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.31.1.tgz",
|
||||
"integrity": "sha512-I9aiFrbd7oYHwlnQDqr1Roz+fTz61oDDJX7n9tYF9FJymH1cIN1DtKw3iYt6b8WZgEjoNwVSncwF4wx/ZedMhw==",
|
||||
"version": "1.30.2",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.30.2.tgz",
|
||||
"integrity": "sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -2066,9 +2062,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/marked": {
|
||||
"version": "17.0.4",
|
||||
"resolved": "https://registry.npmjs.org/marked/-/marked-17.0.4.tgz",
|
||||
"integrity": "sha512-NOmVMM+KAokHMvjWmC5N/ZOvgmSWuqJB8FoYI019j4ogb/PeRMKoKIjReZ2w3376kkA8dSJIP8uD993Kxc0iRQ==",
|
||||
"version": "17.0.1",
|
||||
"resolved": "https://registry.npmjs.org/marked/-/marked-17.0.1.tgz",
|
||||
"integrity": "sha512-boeBdiS0ghpWcSwoNm/jJBwdpFaMnZWRzjA6SkUMYb40SVaN1x7mmfGKp0jvexGcx+7y2La5zRZsYFZI6Qpypg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
@@ -2355,9 +2351,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/tailwindcss": {
|
||||
"version": "4.2.1",
|
||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.2.1.tgz",
|
||||
"integrity": "sha512-/tBrSQ36vCleJkAOsy9kbNTgaxvGbyOamC30PRePTQe/o1MFwEKHQk4Cn7BNGaPtjp+PuUrByJehM1hgxfq4sw==",
|
||||
"version": "4.1.18",
|
||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.18.tgz",
|
||||
"integrity": "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
|
||||
16
package.json
16
package.json
@@ -13,23 +13,23 @@
|
||||
"devDependencies": {
|
||||
"@catppuccin/highlightjs": "^1.0.1",
|
||||
"@codemirror/commands": "^6.10.1",
|
||||
"@codemirror/lang-javascript": "^6.2.5",
|
||||
"@codemirror/language": "^6.12.2",
|
||||
"@codemirror/lang-javascript": "^6.2.4",
|
||||
"@codemirror/language": "^6.12.1",
|
||||
"@codemirror/state": "^6.5.4",
|
||||
"@codemirror/text": "^0.19.6",
|
||||
"@codemirror/view": "^6.39.16",
|
||||
"@codemirror/view": "^6.39.11",
|
||||
"@tailwindcss/forms": "^0.5.11",
|
||||
"@tailwindcss/typography": "^0.5.19",
|
||||
"@tailwindcss/vite": "^4.2.1",
|
||||
"@tailwindcss/vite": "^4.1.18",
|
||||
"codemirror": "^6.0.2",
|
||||
"github-markdown-css": "^5.9.0",
|
||||
"github-markdown-css": "^5.8.1",
|
||||
"highlight.js": "^11.11.1",
|
||||
"jdenticon": "^3.3.0",
|
||||
"katex": "^0.16.38",
|
||||
"marked": "^17.0.4",
|
||||
"katex": "^0.16.28",
|
||||
"marked": "^17.0.1",
|
||||
"nodemon": "^3.1.11",
|
||||
"pdfobject": "^2.3.1",
|
||||
"tailwindcss": "^4.2.1",
|
||||
"tailwindcss": "^4.1.18",
|
||||
"vite": "^7.3.1"
|
||||
}
|
||||
}
|
||||
|
||||
10
public/css/embed.css
vendored
10
public/css/embed.css
vendored
@@ -35,14 +35,6 @@
|
||||
--border-width-1: 1px;
|
||||
}
|
||||
|
||||
@layer base {
|
||||
:host {
|
||||
--tw-border-style: solid;
|
||||
--tw-border-width: 0;
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.opengist-embed {
|
||||
@import "tailwindcss";
|
||||
@layer base {
|
||||
@@ -58,4 +50,4 @@
|
||||
@import './ipynb.css';
|
||||
@import "./style.css";
|
||||
}
|
||||
}
|
||||
}
|
||||
2
templates/base/base_header.html
vendored
2
templates/base/base_header.html
vendored
@@ -116,12 +116,10 @@
|
||||
<div class="p-4 text-xs space-y-1">
|
||||
<p class="text-gray-400"><code class="text-slate-800 dark:text-slate-300 pr-1">user:thomas</code> {{ .locale.Tr "gist.search.help.user" }}</p>
|
||||
<p class="text-gray-400"><code class="text-slate-800 dark:text-slate-300 pr-1">title:mygist</code> {{ .locale.Tr "gist.search.help.title" }}</p>
|
||||
<p class="text-gray-400"><code class="text-slate-800 dark:text-slate-300 pr-1">description:sync</code> {{ .locale.Tr "gist.search.help.description" }}</p>
|
||||
<p class="text-gray-400"><code class="text-slate-800 dark:text-slate-300 pr-1">filename:myfile.txt</code> {{ .locale.Tr "gist.search.help.filename" }}</p>
|
||||
<p class="text-gray-400"><code class="text-slate-800 dark:text-slate-300 pr-1">extension:yml</code> {{ .locale.Tr "gist.search.help.extension" }}</p>
|
||||
<p class="text-gray-400"><code class="text-slate-800 dark:text-slate-300 pr-1">language:go</code> {{ .locale.Tr "gist.search.help.language" }}</p>
|
||||
<p class="text-gray-400"><code class="text-slate-800 dark:text-slate-300 pr-1">topic:homelab</code> {{ .locale.Tr "gist.search.help.topic" }}</p>
|
||||
<p class="text-gray-400"><code class="text-slate-800 dark:text-slate-300 pr-1">all:systemctl</code> {{ .locale.Tr "gist.search.help.all" }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
85
templates/pages/oauth_register.html
vendored
85
templates/pages/oauth_register.html
vendored
@@ -1,85 +0,0 @@
|
||||
{{ template "header" .}}
|
||||
<div class="py-10">
|
||||
<header>
|
||||
<h1 class="text-2xl font-bold leading-tight text-slate-700 dark:text-slate-300">
|
||||
{{ .title }}
|
||||
</h1>
|
||||
</header>
|
||||
<main class="mt-4">
|
||||
<div class="grid sm:grid-cols-2">
|
||||
<div class="">
|
||||
<div class="mt-8 sm:w-full sm:max-w-md">
|
||||
<div class="bg-white dark:bg-gray-900 rounded-md border border-1 border-gray-200 dark:border-gray-700 py-8 px-4 shadow sm:rounded-lg sm:px-10">
|
||||
|
||||
<div class="mb-6 text-center">
|
||||
{{ if .oauthAvatarURL }}
|
||||
<img src="{{ .oauthAvatarURL }}" alt="Avatar" class="w-16 h-16 rounded-full mx-auto mb-2">
|
||||
{{ end }}
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400">
|
||||
{{ .locale.Tr "auth.oauth.signing-in-with" $.c.OIDCProviderName }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<form class="space-y-6" method="post">
|
||||
<div>
|
||||
<label for="username" class="block text-sm font-medium text-slate-700 dark:text-slate-300">
|
||||
{{ .locale.Tr "auth.username" }}
|
||||
</label>
|
||||
<div class="mt-1">
|
||||
<input id="username" name="username" type="text" value="{{ .oauthNickname }}" required
|
||||
class="dark:bg-gray-800 appearance-none block w-full px-3 py-2 border border-gray-200 dark:border-gray-700 rounded-md shadow-sm placeholder-gray-600 dark:placeholder-gray-400 focus:outline-none focus:ring-primary-500 focus:border-primary-500 sm:text-sm">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-8">
|
||||
<label for="email" class="block text-sm font-medium text-slate-700 dark:text-slate-300">
|
||||
{{ .locale.Tr "settings.email" }}
|
||||
</label>
|
||||
<div class="mt-1">
|
||||
<input id="email" name="email" type="email" value="{{ .oauthEmail }}"
|
||||
class="dark:bg-gray-800 appearance-none block w-full px-3 py-2 border border-gray-200 dark:border-gray-700 rounded-md shadow-sm placeholder-gray-600 dark:placeholder-gray-400 focus:outline-none focus:ring-primary-500 focus:border-primary-500 sm:text-sm">
|
||||
</div>
|
||||
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||
{{ .locale.Tr "settings.email-help" }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="flex">
|
||||
<div class="flex-auto">
|
||||
<button type="submit" class="inline-flex items-center px-4 py-2 border border-transparent border-gray-200 dark:border-gray-700 text-sm font-medium rounded-md shadow-sm text-white dark:text-white bg-primary-500 hover:bg-primary-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500">
|
||||
{{ .locale.Tr "auth.oauth.complete-registration-button" }}
|
||||
</button>
|
||||
</div>
|
||||
<span class="float-right text-sm py-2 underline">
|
||||
<a href="{{ $.c.ExternalUrl }}/login">{{ .locale.Tr "auth.oauth.cancel" }}</a>
|
||||
</span>
|
||||
</div>
|
||||
{{ .csrfHtml }}
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="">
|
||||
<div class="mt-8 sm:w-full sm:max-w-md">
|
||||
<div class="bg-white dark:bg-gray-900 rounded-md border border-1 border-gray-200 dark:border-gray-700 py-8 px-4 shadow sm:rounded-lg sm:px-10">
|
||||
<p class="block text-sm font-medium text-slate-700 dark:text-slate-300">{{ .locale.Tr "auth.oauth.existing-account" }}</p>
|
||||
<div class="flex items-center justify-center mt-4">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-14 text-gray-400">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M13.19 8.688a4.5 4.5 0 0 1 1.242 7.244l-4.5 4.5a4.5 4.5 0 0 1-6.364-6.364l1.757-1.757m13.35-.622 1.757-1.757a4.5 4.5 0 0 0-6.364-6.364l-4.5 4.5a4.5 4.5 0 0 0 1.242 7.244" />
|
||||
</svg>
|
||||
</div>
|
||||
<p class="mt-4 text-sm text-center">
|
||||
{{ .locale.Tr "auth.oauth.already-have-account" $.c.OIDCProviderName }}
|
||||
</p>
|
||||
<div class="flex items-center justify-center mt-4">
|
||||
<a href="{{ $.c.ExternalUrl }}/login" class="inline-flex items-center px-4 py-2 border border-gray-200 dark:border-gray-700 text-sm font-medium rounded-md shadow-sm text-slate-700 dark:text-slate-300 bg-gray-50 dark:bg-gray-800 hover:bg-gray-100 dark:hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500">
|
||||
{{ .locale.Tr "auth.login" }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
{{ template "footer" .}}
|
||||
Reference in New Issue
Block a user