mirror of
https://github.com/pocket-id/pocket-id.git
synced 2025-12-12 09:13:14 +03:00
Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2587058ded | ||
|
|
ff06bf0b34 | ||
|
|
11ed661f86 | ||
|
|
29748cc6c7 | ||
|
|
edfb99d221 | ||
|
|
282ff82b0c | ||
|
|
9d5f83da78 | ||
|
|
896da812a3 | ||
|
|
d2b3b7647d | ||
|
|
025378d14e |
@@ -23,6 +23,9 @@ jobs:
|
||||
username: ${{ secrets.DOCKER_REGISTRY_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_REGISTRY_PASSWORD }}
|
||||
|
||||
- name: Download GeoLite2 City database
|
||||
run: MAXMIND_LICENSE_KEY=${{ secrets.MAXMIND_LICENSE_KEY }} sh scripts/download-ip-database.sh
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v4
|
||||
with:
|
||||
|
||||
3
.github/workflows/e2e-tests.yml
vendored
3
.github/workflows/e2e-tests.yml
vendored
@@ -16,6 +16,9 @@ jobs:
|
||||
cache: 'npm'
|
||||
cache-dependency-path: frontend/package-lock.json
|
||||
|
||||
- name: Create dummy GeoLite2 City database
|
||||
run: touch ./backend/GeoLite2-City.mmdb
|
||||
|
||||
- name: Build Docker Image
|
||||
run: docker build -t stonith404/pocket-id .
|
||||
- name: Run Docker Container
|
||||
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -34,4 +34,5 @@ vite.config.ts.timestamp-*
|
||||
# Application specific
|
||||
data
|
||||
/frontend/tests/.auth
|
||||
pocket-id-backend
|
||||
pocket-id-backend
|
||||
/backend/GeoLite2-City.mmdb
|
||||
27
CHANGELOG.md
27
CHANGELOG.md
@@ -1,3 +1,30 @@
|
||||
## [](https://github.com/stonith404/pocket-id/compare/v0.8.1...v) (2024-10-18)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add environment variable to change the caddy port in Docker ([ff06bf0](https://github.com/stonith404/pocket-id/commit/ff06bf0b34496ce472ba6d3ebd4ea249f21c0ec3))
|
||||
* use improve table for users and audit logs ([11ed661](https://github.com/stonith404/pocket-id/commit/11ed661f86a512f78f66d604a10c1d47d39f2c39))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* allow copy to clipboard for client secret ([29748cc](https://github.com/stonith404/pocket-id/commit/29748cc6c7b7e5a6b54bfe837e0b1a98fa1ad594))
|
||||
|
||||
## [](https://github.com/stonith404/pocket-id/compare/v0.8.0...v) (2024-10-11)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* add key id to JWK ([282ff82](https://github.com/stonith404/pocket-id/commit/282ff82b0c7e2414b3528c8ca325758245b8ae61))
|
||||
|
||||
## [](https://github.com/stonith404/pocket-id/compare/v0.7.1...v) (2024-10-04)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add location based on ip to the audit log ([025378d](https://github.com/stonith404/pocket-id/commit/025378d14edd2d72da76e90799a0ccdd42cf672c))
|
||||
|
||||
## [](https://github.com/stonith404/pocket-id/compare/v0.7.0...v) (2024-10-03)
|
||||
|
||||
|
||||
|
||||
@@ -31,6 +31,7 @@ COPY --from=frontend-builder /app/frontend/package.json ./frontend/package.json
|
||||
|
||||
COPY --from=backend-builder /app/backend/pocket-id-backend ./backend/pocket-id-backend
|
||||
COPY --from=backend-builder /app/backend/migrations ./backend/migrations
|
||||
COPY --from=backend-builder /app/backend/GeoLite2-City.mmdb ./backend/GeoLite2-City.mmdb
|
||||
COPY --from=backend-builder /app/backend/email-templates ./backend/email-templates
|
||||
COPY --from=backend-builder /app/backend/images ./backend/images
|
||||
|
||||
|
||||
27
README.md
27
README.md
@@ -68,6 +68,10 @@ Required tools:
|
||||
cd ..
|
||||
pm2 start pocket-id-backend --name pocket-id-backend
|
||||
|
||||
# Optional: Download the GeoLite2 city database.
|
||||
# If not downloaded the ip location in the audit log will be empty.
|
||||
MAXMIND_LICENSE_KEY=<your-key> sh scripts/download-ip-database.sh
|
||||
|
||||
# Start the frontend
|
||||
cd ../frontend
|
||||
npm install
|
||||
@@ -94,7 +98,6 @@ You may need the following information:
|
||||
- **Userinfo URL**: `https://<your-domain>/api/oidc/userinfo`
|
||||
- **Certificate URL**: `https://<your-domain>/.well-known/jwks.json`
|
||||
- **OIDC Discovery URL**: `https://<your-domain>/.well-known/openid-configuration`
|
||||
- **PKCE**: `false` as this is not supported yet.
|
||||
- **Scopes**: At least `openid email`. Optionally you can add `profile` and `groups`.
|
||||
|
||||
### Proxy Services with Pocket ID
|
||||
@@ -132,6 +135,9 @@ docker compose up -d
|
||||
cd ..
|
||||
pm2 start pocket-id-backend --name pocket-id-backend
|
||||
|
||||
# Optional: Update the GeoLite2 city database
|
||||
MAXMIND_LICENSE_KEY=<your-key> sh scripts/download-ip-database.sh
|
||||
|
||||
# Start the frontend
|
||||
cd ../frontend
|
||||
npm install
|
||||
@@ -145,15 +151,16 @@ docker compose up -d
|
||||
|
||||
### Environment variables
|
||||
|
||||
| Variable | Default Value | Recommended to change | Description |
|
||||
| ---------------------- | ----------------------- | --------------------- | --------------------------------------------- |
|
||||
| `PUBLIC_APP_URL` | `http://localhost` | yes | The URL where you will access the app. |
|
||||
| `TRUST_PROXY` | `false` | yes | Whether the app is behind a reverse proxy. |
|
||||
| `DB_PATH` | `data/pocket-id.db` | no | The path to the SQLite database. |
|
||||
| `UPLOAD_PATH` | `data/uploads` | no | The path where the uploaded files are stored. |
|
||||
| `INTERNAL_BACKEND_URL` | `http://localhost:8080` | no | The URL where the backend is accessible. |
|
||||
| `PORT` | `3000` | no | The port on which the frontend should listen. |
|
||||
| `BACKEND_PORT` | `8080` | no | The port on which the backend should listen. |
|
||||
| Variable | Default Value | Recommended to change | Description |
|
||||
| ---------------------- | ----------------------- | --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `PUBLIC_APP_URL` | `http://localhost` | yes | The URL where you will access the app. |
|
||||
| `TRUST_PROXY` | `false` | yes | Whether the app is behind a reverse proxy. |
|
||||
| `DB_PATH` | `data/pocket-id.db` | no | The path to the SQLite database. |
|
||||
| `UPLOAD_PATH` | `data/uploads` | no | The path where the uploaded files are stored. |
|
||||
| `INTERNAL_BACKEND_URL` | `http://localhost:8080` | no | The URL where the backend is accessible. |
|
||||
| `CADDY_PORT` | `80` | no | The port on which Caddy should listen. Caddy is only active inside the Docker container. If you want to change the exposed port of the container then you sould change this variable. |
|
||||
| `PORT` | `3000` | no | The port on which the frontend should listen. |
|
||||
| `BACKEND_PORT` | `8080` | no | The port on which the backend should listen. |
|
||||
|
||||
## Contribute
|
||||
|
||||
|
||||
@@ -9,9 +9,15 @@
|
||||
<div class="content">
|
||||
<h2>New Sign-In Detected</h2>
|
||||
<div class="grid">
|
||||
{{ if and .Data.City .Data.Country }}
|
||||
<div>
|
||||
<p class="label">Approximate Location</p>
|
||||
<p>{{ .Data.City }}, {{ .Data.Country }}</p>
|
||||
</div>
|
||||
{{ end }}
|
||||
<div>
|
||||
<p class="label">IP Address</p>
|
||||
<p>{{ .Data.IPAddress}}</p>
|
||||
<p>{{ .Data.IPAddress }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="label">Device</p>
|
||||
@@ -19,7 +25,7 @@
|
||||
</div>
|
||||
<div>
|
||||
<p class="label">Sign-In Time</p>
|
||||
<p>{{ .Data.DateTime.Format "2006-01-02 15:04:05 UTC"}}</p>
|
||||
<p>{{ .Data.DateTime.Format "2006-01-02 15:04:05 UTC" }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<p class="message">
|
||||
@@ -27,4 +33,4 @@
|
||||
safely ignore this message. If not, please review your account and security settings.
|
||||
</p>
|
||||
</div>
|
||||
{{ end -}}
|
||||
{{ end -}}
|
||||
@@ -2,6 +2,9 @@
|
||||
New Sign-In Detected
|
||||
====================
|
||||
|
||||
{{ if and .Data.City .Data.Country }}
|
||||
Approximate Location: {{ .Data.City }}, {{ .Data.Country }}
|
||||
{{ end }}
|
||||
IP Address: {{ .Data.IPAddress }}
|
||||
Device: {{ .Data.Device }}
|
||||
Time: {{ .Data.DateTime.Format "2006-01-02 15:04:05 UTC"}}
|
||||
|
||||
@@ -7,22 +7,23 @@ require (
|
||||
github.com/fxamacker/cbor/v2 v2.7.0
|
||||
github.com/gin-contrib/cors v1.7.2
|
||||
github.com/gin-gonic/gin v1.10.0
|
||||
github.com/go-co-op/gocron/v2 v2.11.0
|
||||
github.com/go-playground/validator/v10 v10.22.0
|
||||
github.com/go-webauthn/webauthn v0.11.1
|
||||
github.com/go-co-op/gocron/v2 v2.12.1
|
||||
github.com/go-playground/validator/v10 v10.22.1
|
||||
github.com/go-webauthn/webauthn v0.11.2
|
||||
github.com/golang-jwt/jwt/v5 v5.2.1
|
||||
github.com/golang-migrate/migrate/v4 v4.17.1
|
||||
github.com/golang-migrate/migrate/v4 v4.18.1
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/joho/godotenv v1.5.1
|
||||
github.com/mileusna/useragent v1.3.4
|
||||
golang.org/x/crypto v0.26.0
|
||||
github.com/mileusna/useragent v1.3.5
|
||||
github.com/oschwald/maxminddb-golang/v2 v2.0.0-beta.1
|
||||
golang.org/x/crypto v0.27.0
|
||||
golang.org/x/time v0.6.0
|
||||
gorm.io/driver/sqlite v1.5.6
|
||||
gorm.io/gorm v1.25.11
|
||||
gorm.io/gorm v1.25.12
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/bytedance/sonic v1.12.1 // indirect
|
||||
github.com/bytedance/sonic v1.12.3 // indirect
|
||||
github.com/bytedance/sonic/loader v0.2.0 // indirect
|
||||
github.com/cloudwego/base64x v0.1.4 // indirect
|
||||
github.com/cloudwego/iasm v0.2.0 // indirect
|
||||
@@ -30,7 +31,7 @@ require (
|
||||
github.com/gin-contrib/sse v0.1.0 // indirect
|
||||
github.com/go-playground/locales v0.14.1 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||
github.com/go-webauthn/x v0.1.12 // indirect
|
||||
github.com/go-webauthn/x v0.1.14 // indirect
|
||||
github.com/goccy/go-json v0.10.3 // indirect
|
||||
github.com/google/go-tpm v0.9.1 // indirect
|
||||
github.com/hashicorp/errwrap v1.1.0 // indirect
|
||||
@@ -43,22 +44,21 @@ require (
|
||||
github.com/kr/pretty v0.3.1 // indirect
|
||||
github.com/leodido/go-urn v1.4.0 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mattn/go-sqlite3 v1.14.22 // indirect
|
||||
github.com/mattn/go-sqlite3 v1.14.23 // indirect
|
||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.2.3 // indirect
|
||||
github.com/robfig/cron/v3 v3.0.1 // indirect
|
||||
github.com/rogpeppe/go-internal v1.11.0 // indirect
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
github.com/ugorji/go/codec v1.2.12 // indirect
|
||||
github.com/x448/float16 v0.8.4 // indirect
|
||||
go.uber.org/atomic v1.11.0 // indirect
|
||||
golang.org/x/arch v0.9.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect
|
||||
golang.org/x/net v0.27.0 // indirect
|
||||
golang.org/x/sys v0.23.0 // indirect
|
||||
golang.org/x/text v0.17.0 // indirect
|
||||
golang.org/x/arch v0.10.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 // indirect
|
||||
golang.org/x/net v0.29.0 // indirect
|
||||
golang.org/x/sys v0.25.0 // indirect
|
||||
golang.org/x/text v0.18.0 // indirect
|
||||
google.golang.org/protobuf v1.34.2 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
github.com/bytedance/sonic v1.12.1 h1:jWl5Qz1fy7X1ioY74WqO0KjAMtAGQs4sYnjiEBiyX24=
|
||||
github.com/bytedance/sonic v1.12.1/go.mod h1:B8Gt/XvtZ3Fqj+iSKMypzymZxw/FVwgIGKzMzT9r/rk=
|
||||
github.com/bytedance/sonic v1.12.3 h1:W2MGa7RCU1QTeYRTPE3+88mVC0yXmsRQRChiyVocVjU=
|
||||
github.com/bytedance/sonic v1.12.3/go.mod h1:B8Gt/XvtZ3Fqj+iSKMypzymZxw/FVwgIGKzMzT9r/rk=
|
||||
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
|
||||
github.com/bytedance/sonic/loader v0.2.0 h1:zNprn+lsIP06C/IqCHs3gPQIvnvpKbbxyXQP1iU4kWM=
|
||||
github.com/bytedance/sonic/loader v0.2.0/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
|
||||
@@ -23,26 +23,26 @@ github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE
|
||||
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
||||
github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=
|
||||
github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
|
||||
github.com/go-co-op/gocron/v2 v2.11.0 h1:IOowNA6SzwdRFnD4/Ol3Kj6G2xKfsoiiGq2Jhhm9bvE=
|
||||
github.com/go-co-op/gocron/v2 v2.11.0/go.mod h1:xY7bJxGazKam1cz04EebrlP4S9q4iWdiAylMGP3jY9w=
|
||||
github.com/go-co-op/gocron/v2 v2.12.1 h1:dCIIBFbzhWKdgXeEifBjHPzgQ1hoWhjS4289Hjjy1uw=
|
||||
github.com/go-co-op/gocron/v2 v2.12.1/go.mod h1:xY7bJxGazKam1cz04EebrlP4S9q4iWdiAylMGP3jY9w=
|
||||
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||
github.com/go-playground/validator/v10 v10.22.0 h1:k6HsTZ0sTnROkhS//R0O+55JgM8C4Bx7ia+JlgcnOao=
|
||||
github.com/go-playground/validator/v10 v10.22.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
|
||||
github.com/go-webauthn/webauthn v0.11.1 h1:5G/+dg91/VcaJHTtJUfwIlNJkLwbJCcnUc4W8VtkpzA=
|
||||
github.com/go-webauthn/webauthn v0.11.1/go.mod h1:YXRm1WG0OtUyDFaVAgB5KG7kVqW+6dYCJ7FTQH4SxEE=
|
||||
github.com/go-webauthn/x v0.1.12 h1:RjQ5cvApzyU/xLCiP+rub0PE4HBZsLggbxGR5ZpUf/A=
|
||||
github.com/go-webauthn/x v0.1.12/go.mod h1:XlRcGkNH8PT45TfeJYc6gqpOtiOendHhVmnOxh+5yHs=
|
||||
github.com/go-playground/validator/v10 v10.22.1 h1:40JcKH+bBNGFczGuoBYgX4I6m/i27HYW8P9FDk5PbgA=
|
||||
github.com/go-playground/validator/v10 v10.22.1/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
|
||||
github.com/go-webauthn/webauthn v0.11.2 h1:Fgx0/wlmkClTKlnOsdOQ+K5HcHDsDcYIvtYmfhEOSUc=
|
||||
github.com/go-webauthn/webauthn v0.11.2/go.mod h1:aOtudaF94pM71g3jRwTYYwQTG1KyTILTcZqN1srkmD0=
|
||||
github.com/go-webauthn/x v0.1.14 h1:1wrB8jzXAofojJPAaRxnZhRgagvLGnLjhCAwg3kTpT0=
|
||||
github.com/go-webauthn/x v0.1.14/go.mod h1:UuVvFZ8/NbOnkDz3y1NaxtUN87pmtpC1PQ+/5BBQRdc=
|
||||
github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA=
|
||||
github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
||||
github.com/golang-migrate/migrate/v4 v4.17.1 h1:4zQ6iqL6t6AiItphxJctQb3cFqWiSpMnX7wLTPnnYO4=
|
||||
github.com/golang-migrate/migrate/v4 v4.17.1/go.mod h1:m8hinFyWBn0SA4QKHuKh175Pm9wjmxj3S2Mia7dbXzM=
|
||||
github.com/golang-migrate/migrate/v4 v4.18.1 h1:JML/k+t4tpHCpQTCAD62Nu43NUFzHY4CV3uAuvHGC+Y=
|
||||
github.com/golang-migrate/migrate/v4 v4.18.1/go.mod h1:HAX6m3sQgcdO81tdjn5exv20+3Kb13cmGli1hrD6hks=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/go-tpm v0.9.1 h1:0pGc4X//bAlmZzMKf8iz6IsDo1nYTbYJ6FZN/rg4zdM=
|
||||
@@ -79,10 +79,10 @@ github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
|
||||
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
|
||||
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||
github.com/mileusna/useragent v1.3.4 h1:MiuRRuvGjEie1+yZHO88UBYg8YBC/ddF6T7F56i3PCk=
|
||||
github.com/mileusna/useragent v1.3.4/go.mod h1:3d8TOmwL/5I8pJjyVDteHtgDGcefrFUX4ccGOMKNYYc=
|
||||
github.com/mattn/go-sqlite3 v1.14.23 h1:gbShiuAP1W5j9UOksQ06aiiqPMxYecovVGwmTxWtuw0=
|
||||
github.com/mattn/go-sqlite3 v1.14.23/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||
github.com/mileusna/useragent v1.3.5 h1:SJM5NzBmh/hO+4LGeATKpaEX9+b4vcGg2qXGLiNGDws=
|
||||
github.com/mileusna/useragent v1.3.5/go.mod h1:3d8TOmwL/5I8pJjyVDteHtgDGcefrFUX4ccGOMKNYYc=
|
||||
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
||||
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
@@ -90,26 +90,26 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
|
||||
github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
|
||||
github.com/oschwald/maxminddb-golang/v2 v2.0.0-beta.1 h1:UihPOz+oIJ5X0JsO7wEkL50fheCODsoZ9r86mJWfNMc=
|
||||
github.com/oschwald/maxminddb-golang/v2 v2.0.0-beta.1/go.mod h1:vPpFrres6g9B5+meBwAd9xnp335KFcLEFW7EqJxBHy0=
|
||||
github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M=
|
||||
github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc=
|
||||
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
|
||||
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
|
||||
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||
github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
|
||||
github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
|
||||
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
|
||||
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
||||
@@ -122,20 +122,20 @@ go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
|
||||
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
golang.org/x/arch v0.9.0 h1:ub9TgUInamJ8mrZIGlBG6/4TqWeMszd4N8lNorbrr6k=
|
||||
golang.org/x/arch v0.9.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
|
||||
golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw=
|
||||
golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54=
|
||||
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8=
|
||||
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY=
|
||||
golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys=
|
||||
golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE=
|
||||
golang.org/x/arch v0.10.0 h1:S3huipmSclq3PJMNe76NGwkBR504WFkQ5dhzWzP8ZW8=
|
||||
golang.org/x/arch v0.10.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
|
||||
golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A=
|
||||
golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70=
|
||||
golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 h1:e66Fs6Z+fZTbFBAxKfP3PALWBtpfqks2bwGcexMxgtk=
|
||||
golang.org/x/exp v0.0.0-20240909161429-701f63a606c0/go.mod h1:2TbTHSBQa924w8M6Xs1QcRcFwyucIwBGpK1p2f1YFFY=
|
||||
golang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo=
|
||||
golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.23.0 h1:YfKFowiIMvtgl1UERQoTPPToxltDeZfbj4H7dVUCwmM=
|
||||
golang.org/x/sys v0.23.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc=
|
||||
golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
|
||||
golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=
|
||||
golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224=
|
||||
golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
|
||||
golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U=
|
||||
golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||
google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
|
||||
@@ -148,6 +148,6 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gorm.io/driver/sqlite v1.5.6 h1:fO/X46qn5NUEEOZtnjJRWRzZMe8nqJiQ9E+0hi+hKQE=
|
||||
gorm.io/driver/sqlite v1.5.6/go.mod h1:U+J8craQU6Fzkcvu8oLeAQmi50TkwPEhHDEjQZXDah4=
|
||||
gorm.io/gorm v1.25.11 h1:/Wfyg1B/je1hnDx3sMkX+gAlxrlZpn6X0BXRlwXlvHg=
|
||||
gorm.io/gorm v1.25.11/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ=
|
||||
gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8=
|
||||
gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ=
|
||||
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
|
||||
|
||||
@@ -18,7 +18,7 @@ func NewOidcController(group *gin.RouterGroup, jwtAuthMiddleware *middleware.Jwt
|
||||
|
||||
group.POST("/oidc/authorize", jwtAuthMiddleware.Add(false), oc.authorizeHandler)
|
||||
group.POST("/oidc/authorize/new-client", jwtAuthMiddleware.Add(false), oc.authorizeNewClientHandler)
|
||||
group.POST("/oidc/token", oc.createIDTokenHandler)
|
||||
group.POST("/oidc/token", oc.createTokensHandler)
|
||||
group.GET("/oidc/userinfo", oc.userInfoHandler)
|
||||
|
||||
group.GET("/oidc/clients", jwtAuthMiddleware.Add(true), oc.listClientsHandler)
|
||||
@@ -91,7 +91,7 @@ func (oc *OidcController) authorizeNewClientHandler(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, response)
|
||||
}
|
||||
|
||||
func (oc *OidcController) createIDTokenHandler(c *gin.Context) {
|
||||
func (oc *OidcController) createTokensHandler(c *gin.Context) {
|
||||
var input dto.OidcIdTokenDto
|
||||
|
||||
if err := c.ShouldBind(&input); err != nil {
|
||||
|
||||
@@ -11,6 +11,8 @@ type AuditLogDto struct {
|
||||
|
||||
Event model.AuditLogEvent `json:"event"`
|
||||
IpAddress string `json:"ipAddress"`
|
||||
Country string `json:"country"`
|
||||
City string `json:"city"`
|
||||
Device string `json:"device"`
|
||||
UserID string `json:"userID"`
|
||||
Data model.AuditLogData `json:"data"`
|
||||
|
||||
@@ -11,6 +11,8 @@ type AuditLog struct {
|
||||
|
||||
Event AuditLogEvent
|
||||
IpAddress string
|
||||
Country string
|
||||
City string
|
||||
UserAgent string
|
||||
UserID string
|
||||
Data AuditLogData
|
||||
|
||||
@@ -2,11 +2,13 @@ package service
|
||||
|
||||
import (
|
||||
userAgentParser "github.com/mileusna/useragent"
|
||||
"github.com/oschwald/maxminddb-golang/v2"
|
||||
"github.com/stonith404/pocket-id/backend/internal/model"
|
||||
"github.com/stonith404/pocket-id/backend/internal/utils"
|
||||
"github.com/stonith404/pocket-id/backend/internal/utils/email"
|
||||
"gorm.io/gorm"
|
||||
"log"
|
||||
"net/netip"
|
||||
)
|
||||
|
||||
type AuditLogService struct {
|
||||
@@ -21,9 +23,16 @@ func NewAuditLogService(db *gorm.DB, appConfigService *AppConfigService, emailSe
|
||||
|
||||
// Create creates a new audit log entry in the database
|
||||
func (s *AuditLogService) Create(event model.AuditLogEvent, ipAddress, userAgent, userID string, data model.AuditLogData) model.AuditLog {
|
||||
country, city, err := s.GetIpLocation(ipAddress)
|
||||
if err != nil {
|
||||
log.Printf("Failed to get IP location: %v\n", err)
|
||||
}
|
||||
|
||||
auditLog := model.AuditLog{
|
||||
Event: event,
|
||||
IpAddress: ipAddress,
|
||||
Country: country,
|
||||
City: city,
|
||||
UserAgent: userAgent,
|
||||
UserID: userID,
|
||||
Data: data,
|
||||
@@ -61,6 +70,8 @@ func (s *AuditLogService) CreateNewSignInWithEmail(ipAddress, userAgent, userID
|
||||
Email: user.Email,
|
||||
}, NewLoginTemplate, &NewLoginTemplateData{
|
||||
IPAddress: ipAddress,
|
||||
Country: createdAuditLog.Country,
|
||||
City: createdAuditLog.City,
|
||||
Device: s.DeviceStringFromUserAgent(userAgent),
|
||||
DateTime: createdAuditLog.CreatedAt.UTC(),
|
||||
})
|
||||
@@ -86,3 +97,29 @@ func (s *AuditLogService) DeviceStringFromUserAgent(userAgent string) string {
|
||||
ua := userAgentParser.Parse(userAgent)
|
||||
return ua.Name + " on " + ua.OS + " " + ua.OSVersion
|
||||
}
|
||||
|
||||
func (s *AuditLogService) GetIpLocation(ipAddress string) (country, city string, err error) {
|
||||
db, err := maxminddb.Open("GeoLite2-City.mmdb")
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
addr := netip.MustParseAddr(ipAddress)
|
||||
|
||||
var record struct {
|
||||
City struct {
|
||||
Names map[string]string `maxminddb:"names"`
|
||||
} `maxminddb:"city"`
|
||||
Country struct {
|
||||
Names map[string]string `maxminddb:"names"`
|
||||
} `maxminddb:"country"`
|
||||
}
|
||||
|
||||
err = db.Lookup(addr).Decode(&record)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
return record.Country.Names["en"], record.City.Names["en"], nil
|
||||
}
|
||||
|
||||
@@ -29,6 +29,8 @@ var NewLoginTemplate = email.Template[NewLoginTemplateData]{
|
||||
|
||||
type NewLoginTemplateData struct {
|
||||
IPAddress string
|
||||
Country string
|
||||
City string
|
||||
Device string
|
||||
DateTime time.Time
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package service
|
||||
import (
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/sha256"
|
||||
"crypto/x509"
|
||||
"encoding/base64"
|
||||
"encoding/pem"
|
||||
@@ -51,6 +52,7 @@ type AccessTokenJWTClaims struct {
|
||||
}
|
||||
|
||||
type JWK struct {
|
||||
Kid string `json:"kid"`
|
||||
Kty string `json:"kty"`
|
||||
Use string `json:"use"`
|
||||
Alg string `json:"alg"`
|
||||
@@ -98,7 +100,15 @@ func (s *JwtService) GenerateAccessToken(user model.User) (string, error) {
|
||||
},
|
||||
IsAdmin: user.IsAdmin,
|
||||
}
|
||||
|
||||
kid, err := s.generateKeyID(s.publicKey)
|
||||
if err != nil {
|
||||
return "", errors.New("failed to generate key ID: " + err.Error())
|
||||
}
|
||||
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodRS256, claim)
|
||||
token.Header["kid"] = kid
|
||||
|
||||
return token.SignedString(s.privateKey)
|
||||
}
|
||||
|
||||
@@ -137,9 +147,17 @@ func (s *JwtService) GenerateIDToken(userClaims map[string]interface{}, clientID
|
||||
claims["nonce"] = nonce
|
||||
}
|
||||
|
||||
kid, err := s.generateKeyID(s.publicKey)
|
||||
if err != nil {
|
||||
return "", errors.New("failed to generate key ID: " + err.Error())
|
||||
}
|
||||
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodRS256, claims)
|
||||
token.Header["kid"] = kid
|
||||
|
||||
return token.SignedString(s.privateKey)
|
||||
}
|
||||
|
||||
func (s *JwtService) GenerateOauthAccessToken(user model.User, clientID string) (string, error) {
|
||||
claim := jwt.RegisteredClaims{
|
||||
Subject: user.ID,
|
||||
@@ -148,7 +166,15 @@ func (s *JwtService) GenerateOauthAccessToken(user model.User, clientID string)
|
||||
Audience: jwt.ClaimStrings{clientID},
|
||||
Issuer: common.EnvConfig.AppURL,
|
||||
}
|
||||
|
||||
kid, err := s.generateKeyID(s.publicKey)
|
||||
if err != nil {
|
||||
return "", errors.New("failed to generate key ID: " + err.Error())
|
||||
}
|
||||
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodRS256, claim)
|
||||
token.Header["kid"] = kid
|
||||
|
||||
return token.SignedString(s.privateKey)
|
||||
}
|
||||
|
||||
@@ -174,7 +200,13 @@ func (s *JwtService) GetJWK() (JWK, error) {
|
||||
return JWK{}, errors.New("public key is not initialized")
|
||||
}
|
||||
|
||||
kid, err := s.generateKeyID(s.publicKey)
|
||||
if err != nil {
|
||||
return JWK{}, err
|
||||
}
|
||||
|
||||
jwk := JWK{
|
||||
Kid: kid,
|
||||
Kty: "RSA",
|
||||
Use: "sig",
|
||||
Alg: "RS256",
|
||||
@@ -185,6 +217,25 @@ func (s *JwtService) GetJWK() (JWK, error) {
|
||||
return jwk, nil
|
||||
}
|
||||
|
||||
// GenerateKeyID generates a Key ID for the public key using the first 8 bytes of the SHA-256 hash of the public key.
|
||||
func (s *JwtService) generateKeyID(publicKey *rsa.PublicKey) (string, error) {
|
||||
pubASN1, err := x509.MarshalPKIXPublicKey(publicKey)
|
||||
if err != nil {
|
||||
return "", errors.New("failed to marshal public key: " + err.Error())
|
||||
}
|
||||
|
||||
// Compute SHA-256 hash of the public key
|
||||
hash := sha256.New()
|
||||
hash.Write(pubASN1)
|
||||
hashed := hash.Sum(nil)
|
||||
|
||||
// Truncate the hash to the first 8 bytes for a shorter Key ID
|
||||
shortHash := hashed[:8]
|
||||
|
||||
// Return Base64 encoded truncated hash as Key ID
|
||||
return base64.RawURLEncoding.EncodeToString(shortHash), nil
|
||||
}
|
||||
|
||||
// generateKeys generates a new RSA key pair and saves them to the specified paths.
|
||||
func (s *JwtService) generateKeys() error {
|
||||
if err := os.MkdirAll(filepath.Dir(privateKeyPath), 0700); err != nil {
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
ALTER TABLE audit_logs DROP COLUMN country;
|
||||
ALTER TABLE audit_logs DROP COLUMN city;
|
||||
@@ -0,0 +1,2 @@
|
||||
ALTER TABLE audit_logs ADD COLUMN country TEXT;
|
||||
ALTER TABLE audit_logs ADD COLUMN city TEXT;
|
||||
1069
frontend/package-lock.json
generated
1069
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -12,46 +12,46 @@
|
||||
"format": "prettier --write ."
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.46.1",
|
||||
"@sveltejs/adapter-auto": "^3.2.4",
|
||||
"@sveltejs/adapter-node": "^5.2.2",
|
||||
"@sveltejs/kit": "^2.5.24",
|
||||
"@playwright/test": "^1.47.2",
|
||||
"@sveltejs/adapter-auto": "^3.2.5",
|
||||
"@sveltejs/adapter-node": "^5.2.5",
|
||||
"@sveltejs/kit": "^2.6.1",
|
||||
"@sveltejs/vite-plugin-svelte": "^3.1.2",
|
||||
"@types/eslint": "^9.6.0",
|
||||
"@types/jsonwebtoken": "^9.0.6",
|
||||
"@types/node": "^22.5.0",
|
||||
"@types/eslint": "^9.6.1",
|
||||
"@types/jsonwebtoken": "^9.0.7",
|
||||
"@types/node": "^22.7.4",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"cbor-js": "^0.1.0",
|
||||
"eslint": "^9.9.1",
|
||||
"eslint": "^9.11.1",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-plugin-svelte": "^2.40.0",
|
||||
"globals": "^15.9.0",
|
||||
"postcss": "^8.4.41",
|
||||
"eslint-plugin-svelte": "^2.44.1",
|
||||
"globals": "^15.10.0",
|
||||
"postcss": "^8.4.47",
|
||||
"prettier": "^3.3.3",
|
||||
"prettier-plugin-svelte": "^3.2.6",
|
||||
"prettier-plugin-tailwindcss": "^0.6.6",
|
||||
"svelte": "^5.0.0-next.1",
|
||||
"svelte-check": "^3.8.6",
|
||||
"tailwindcss": "^3.4.10",
|
||||
"prettier-plugin-svelte": "^3.2.7",
|
||||
"prettier-plugin-tailwindcss": "^0.6.8",
|
||||
"svelte": "^5.0.0-next.262",
|
||||
"svelte-check": "^4.0.4",
|
||||
"tailwindcss": "^3.4.13",
|
||||
"tslib": "^2.7.0",
|
||||
"typescript": "^5.5.4",
|
||||
"typescript-eslint": "^8.2.0",
|
||||
"vite": "^5.4.2"
|
||||
"typescript": "^5.6.2",
|
||||
"typescript-eslint": "^8.8.0",
|
||||
"vite": "^5.4.8"
|
||||
},
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@simplewebauthn/browser": "^10.0.0",
|
||||
"axios": "^1.7.5",
|
||||
"bits-ui": "^0.21.15",
|
||||
"axios": "^1.7.7",
|
||||
"bits-ui": "^0.21.16",
|
||||
"clsx": "^2.1.1",
|
||||
"crypto": "^1.0.1",
|
||||
"formsnap": "^1.0.1",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"lucide-svelte": "^0.435.0",
|
||||
"lucide-svelte": "^0.447.0",
|
||||
"mode-watcher": "^0.4.1",
|
||||
"svelte-sonner": "^0.3.27",
|
||||
"sveltekit-superforms": "^2.17.0",
|
||||
"tailwind-merge": "^2.5.2",
|
||||
"svelte-sonner": "^0.3.28",
|
||||
"sveltekit-superforms": "^2.19.0",
|
||||
"tailwind-merge": "^2.5.3",
|
||||
"tailwind-variants": "^0.2.1",
|
||||
"zod": "^3.23.8"
|
||||
}
|
||||
|
||||
@@ -11,12 +11,14 @@
|
||||
let {
|
||||
items,
|
||||
selectedIds = $bindable(),
|
||||
withoutSearch = false,
|
||||
fetchItems,
|
||||
columns,
|
||||
rows
|
||||
}: {
|
||||
items: Paginated<T>;
|
||||
selectedIds?: string[];
|
||||
withoutSearch?: boolean;
|
||||
fetchItems: (search: string, page: number, limit: number) => Promise<Paginated<T>>;
|
||||
columns: (string | { label: string; hidden?: boolean })[];
|
||||
rows: Snippet<[{ item: T }]>;
|
||||
@@ -65,12 +67,14 @@
|
||||
</script>
|
||||
|
||||
<div class="w-full">
|
||||
{#if !withoutSearch}
|
||||
<Input
|
||||
class="mb-4 max-w-sm"
|
||||
placeholder={'Search...'}
|
||||
type="text"
|
||||
oninput={(e) => onSearch((e.target as HTMLInputElement).value)}
|
||||
/>
|
||||
{/if}
|
||||
<Table.Root>
|
||||
<Table.Header>
|
||||
<Table.Row>
|
||||
|
||||
@@ -2,6 +2,8 @@ export type AuditLog = {
|
||||
id: string;
|
||||
event: string;
|
||||
ipAddress: string;
|
||||
country?: string;
|
||||
city?: string;
|
||||
device: string;
|
||||
createdAt: string;
|
||||
data: any;
|
||||
|
||||
@@ -26,8 +26,7 @@
|
||||
'OIDC Discovery URL': `https://${$page.url.hostname}/.well-known/openid-configuration`,
|
||||
'Token URL': `https://${$page.url.hostname}/api/oidc/token`,
|
||||
'Userinfo URL': `https://${$page.url.hostname}/api/oidc/userinfo`,
|
||||
'Certificate URL': `https://${$page.url.hostname}/.well-known/jwks.json`,
|
||||
PKCE: 'Disabled'
|
||||
'Certificate URL': `https://${$page.url.hostname}/.well-known/jwks.json`
|
||||
};
|
||||
|
||||
async function updateClient(updatedClient: OidcClientCreateWithLogo) {
|
||||
@@ -96,10 +95,16 @@
|
||||
</div>
|
||||
<div class="mb-2 mt-1 flex items-center">
|
||||
<Label class="w-44">Client secret</Label>
|
||||
<span class="text-muted-foreground text-sm" data-testid="client-secret"
|
||||
>{$clientSecretStore ?? '••••••••••••••••••••••••••••••••'}</span
|
||||
>
|
||||
{#if !$clientSecretStore}
|
||||
{#if $clientSecretStore}
|
||||
<CopyToClipboard value={$clientSecretStore}>
|
||||
<span class="text-muted-foreground text-sm" data-testid="client-secret">
|
||||
{$clientSecretStore}
|
||||
</span>
|
||||
</CopyToClipboard>
|
||||
{:else}
|
||||
<span class="text-muted-foreground text-sm" data-testid="client-secret"
|
||||
>••••••••••••••••••••••••••••••••</span
|
||||
>
|
||||
<Button
|
||||
class="ml-2"
|
||||
onclick={createClientSecret}
|
||||
|
||||
@@ -1,16 +1,14 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/stores';
|
||||
import AdvancedTable from '$lib/components/advanced-table.svelte';
|
||||
import { openConfirmDialog } from '$lib/components/confirm-dialog/';
|
||||
import { Badge } from '$lib/components/ui/badge/index';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import * as DropdownMenu from '$lib/components/ui/dropdown-menu';
|
||||
import { Input } from '$lib/components/ui/input';
|
||||
import * as Pagination from '$lib/components/ui/pagination';
|
||||
import * as Table from '$lib/components/ui/table';
|
||||
import UserService from '$lib/services/user-service';
|
||||
import type { Paginated, PaginationRequest } from '$lib/types/pagination.type';
|
||||
import type { Paginated } from '$lib/types/pagination.type';
|
||||
import type { User } from '$lib/types/user.type';
|
||||
import { debounced } from '$lib/utils/debounce-util';
|
||||
import { axiosErrorToast } from '$lib/utils/error-util';
|
||||
import { LucideLink, LucidePencil, LucideTrash } from 'lucide-svelte';
|
||||
import Ellipsis from 'lucide-svelte/icons/ellipsis';
|
||||
@@ -19,23 +17,17 @@
|
||||
|
||||
let { users: initialUsers }: { users: Paginated<User> } = $props();
|
||||
let users = $state<Paginated<User>>(initialUsers);
|
||||
let oneTimeLink = $state<string | null>(null);
|
||||
|
||||
$effect(() => {
|
||||
users = initialUsers;
|
||||
});
|
||||
|
||||
let oneTimeLink = $state<string | null>(null);
|
||||
|
||||
const userService = new UserService();
|
||||
|
||||
let pagination = $state<PaginationRequest>({
|
||||
page: 1,
|
||||
limit: 10
|
||||
});
|
||||
let search = $state('');
|
||||
|
||||
const debouncedSearch = debounced(async (searchValue: string) => {
|
||||
users = await userService.list(searchValue, pagination);
|
||||
}, 400);
|
||||
function fetchItems(search: string, page: number, limit: number) {
|
||||
return userService.list(search, { page, limit });
|
||||
}
|
||||
|
||||
async function deleteUser(user: User) {
|
||||
openConfirmDialog({
|
||||
@@ -47,7 +39,7 @@
|
||||
action: async () => {
|
||||
try {
|
||||
await userService.remove(user.id);
|
||||
users = await userService.list(search, pagination);
|
||||
users = await userService.list();
|
||||
} catch (e) {
|
||||
axiosErrorToast(e);
|
||||
}
|
||||
@@ -67,105 +59,51 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<Input
|
||||
type="search"
|
||||
placeholder="Search users"
|
||||
bind:value={search}
|
||||
on:input={(e) => debouncedSearch((e.target as HTMLInputElement).value)}
|
||||
/>
|
||||
<Table.Root>
|
||||
<Table.Header>
|
||||
<Table.Row>
|
||||
<Table.Head class="hidden md:table-cell">First name</Table.Head>
|
||||
<Table.Head class="hidden md:table-cell">Last name</Table.Head>
|
||||
<Table.Head>Email</Table.Head>
|
||||
<Table.Head>Username</Table.Head>
|
||||
<Table.Head class="hidden lg:table-cell">Role</Table.Head>
|
||||
<Table.Head>
|
||||
<span class="sr-only">Actions</span>
|
||||
</Table.Head>
|
||||
</Table.Row>
|
||||
</Table.Header>
|
||||
<Table.Body>
|
||||
{#if users.data.length === 0}
|
||||
<Table.Row>
|
||||
<Table.Cell colspan={6} class="text-center">No users found</Table.Cell>
|
||||
</Table.Row>
|
||||
{:else}
|
||||
{#each users.data as user}
|
||||
<Table.Row>
|
||||
<Table.Cell class="hidden md:table-cell">{user.firstName}</Table.Cell>
|
||||
<Table.Cell class="hidden md:table-cell">{user.lastName}</Table.Cell>
|
||||
<Table.Cell>{user.email}</Table.Cell>
|
||||
<Table.Cell>{user.username}</Table.Cell>
|
||||
<Table.Cell class="hidden lg:table-cell">
|
||||
<Badge variant="outline">{user.isAdmin ? 'Admin' : 'User'}</Badge>
|
||||
</Table.Cell>
|
||||
<Table.Cell>
|
||||
<DropdownMenu.Root>
|
||||
<DropdownMenu.Trigger asChild let:builder>
|
||||
<Button aria-haspopup="true" size="icon" variant="ghost" builders={[builder]}>
|
||||
<Ellipsis class="h-4 w-4" />
|
||||
<span class="sr-only">Toggle menu</span>
|
||||
</Button>
|
||||
</DropdownMenu.Trigger>
|
||||
<DropdownMenu.Content align="end">
|
||||
<DropdownMenu.Item on:click={() => createOneTimeAccessToken(user.id)}
|
||||
><LucideLink class="mr-2 h-4 w-4" />One-time link</DropdownMenu.Item
|
||||
>
|
||||
<DropdownMenu.Item href="/settings/admin/users/{user.id}"
|
||||
><LucidePencil class="mr-2 h-4 w-4" /> Edit</DropdownMenu.Item
|
||||
>
|
||||
<DropdownMenu.Item
|
||||
class="text-red-500 focus:!text-red-700"
|
||||
on:click={() => deleteUser(user)}
|
||||
><LucideTrash class="mr-2 h-4 w-4" />Delete</DropdownMenu.Item
|
||||
>
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Root>
|
||||
</Table.Cell>
|
||||
</Table.Row>
|
||||
{/each}
|
||||
{/if}
|
||||
</Table.Body>
|
||||
</Table.Root>
|
||||
|
||||
{#if users?.data?.length ?? 0 > 0}
|
||||
<Pagination.Root
|
||||
class="mt-5"
|
||||
count={users.pagination.totalItems}
|
||||
perPage={pagination.limit}
|
||||
onPageChange={async (p) =>
|
||||
(users = await userService.list(search, {
|
||||
page: p,
|
||||
limit: pagination.limit
|
||||
}))}
|
||||
bind:page={users.pagination.currentPage}
|
||||
let:pages
|
||||
let:currentPage
|
||||
>
|
||||
<Pagination.Content class="flex justify-end">
|
||||
<Pagination.Item>
|
||||
<Pagination.PrevButton />
|
||||
</Pagination.Item>
|
||||
{#each pages as page (page.key)}
|
||||
{#if page.type === 'ellipsis'}
|
||||
<Pagination.Item>
|
||||
<Pagination.Ellipsis />
|
||||
</Pagination.Item>
|
||||
{:else}
|
||||
<Pagination.Item>
|
||||
<Pagination.Link {page} isActive={users.pagination.currentPage === page.value}>
|
||||
{page.value}
|
||||
</Pagination.Link>
|
||||
</Pagination.Item>
|
||||
{/if}
|
||||
{/each}
|
||||
<Pagination.Item>
|
||||
<Pagination.NextButton />
|
||||
</Pagination.Item>
|
||||
</Pagination.Content>
|
||||
</Pagination.Root>
|
||||
{/if}
|
||||
<AdvancedTable
|
||||
items={users}
|
||||
{fetchItems}
|
||||
columns={[
|
||||
'First name',
|
||||
'Last name',
|
||||
'Email',
|
||||
'Username',
|
||||
'Role',
|
||||
{ label: 'Actions', hidden: true }
|
||||
]}
|
||||
withoutSearch
|
||||
>
|
||||
{#snippet rows({ item })}
|
||||
<Table.Cell>{item.firstName}</Table.Cell>
|
||||
<Table.Cell>{item.lastName}</Table.Cell>
|
||||
<Table.Cell>{item.email}</Table.Cell>
|
||||
<Table.Cell>{item.username}</Table.Cell>
|
||||
<Table.Cell class="hidden lg:table-cell">
|
||||
<Badge variant="outline">{item.isAdmin ? 'Admin' : 'User'}</Badge>
|
||||
</Table.Cell>
|
||||
<Table.Cell>
|
||||
<DropdownMenu.Root>
|
||||
<DropdownMenu.Trigger asChild let:builder>
|
||||
<Button aria-haspopup="true" size="icon" variant="ghost" builders={[builder]}>
|
||||
<Ellipsis class="h-4 w-4" />
|
||||
<span class="sr-only">Toggle menu</span>
|
||||
</Button>
|
||||
</DropdownMenu.Trigger>
|
||||
<DropdownMenu.Content align="end">
|
||||
<DropdownMenu.Item on:click={() => createOneTimeAccessToken(item.id)}
|
||||
><LucideLink class="mr-2 h-4 w-4" />One-time link</DropdownMenu.Item
|
||||
>
|
||||
<DropdownMenu.Item href="/settings/admin/users/{item.id}"
|
||||
><LucidePencil class="mr-2 h-4 w-4" /> Edit</DropdownMenu.Item
|
||||
>
|
||||
<DropdownMenu.Item
|
||||
class="text-red-500 focus:!text-red-700"
|
||||
on:click={() => deleteUser(item)}
|
||||
><LucideTrash class="mr-2 h-4 w-4" />Delete</DropdownMenu.Item
|
||||
>
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Root>
|
||||
</Table.Cell>
|
||||
{/snippet}
|
||||
</AdvancedTable>
|
||||
|
||||
<OneTimeLinkModal {oneTimeLink} />
|
||||
|
||||
@@ -1,20 +1,22 @@
|
||||
<script lang="ts">
|
||||
import AdvancedTable from '$lib/components/advanced-table.svelte';
|
||||
import { Badge } from '$lib/components/ui/badge';
|
||||
import * as Pagination from '$lib/components/ui/pagination';
|
||||
import * as Table from '$lib/components/ui/table';
|
||||
import AuditLogService from '$lib/services/audit-log-service';
|
||||
import type { AuditLog } from '$lib/types/audit-log.type';
|
||||
import type { Paginated, PaginationRequest } from '$lib/types/pagination.type';
|
||||
import type { Paginated } from '$lib/types/pagination.type';
|
||||
|
||||
let { auditLogs: initialAuditLog }: { auditLogs: Paginated<AuditLog> } = $props();
|
||||
let auditLogs = $state<Paginated<AuditLog>>(initialAuditLog);
|
||||
|
||||
const auditLogService = new AuditLogService();
|
||||
|
||||
let pagination = $state<PaginationRequest>({
|
||||
page: 1,
|
||||
limit: 15
|
||||
});
|
||||
async function fetchItems(search: string, page: number, limit: number) {
|
||||
return await auditLogService.list({
|
||||
page,
|
||||
limit
|
||||
});
|
||||
}
|
||||
|
||||
function toFriendlyEventString(event: string) {
|
||||
const words = event.split('_');
|
||||
@@ -25,71 +27,22 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<Table.Root>
|
||||
<Table.Header class="whitespace-nowrap">
|
||||
<Table.Row>
|
||||
<Table.Head>Time</Table.Head>
|
||||
<Table.Head>Event</Table.Head>
|
||||
<Table.Head>IP Address</Table.Head>
|
||||
<Table.Head>Device</Table.Head>
|
||||
<Table.Head>Client</Table.Head>
|
||||
</Table.Row>
|
||||
</Table.Header>
|
||||
<Table.Body class="whitespace-nowrap">
|
||||
{#if auditLogs.data.length === 0}
|
||||
<Table.Row>
|
||||
<Table.Cell colspan={6} class="text-center">No logs found</Table.Cell>
|
||||
</Table.Row>
|
||||
{:else}
|
||||
{#each auditLogs.data as auditLog}
|
||||
<Table.Row>
|
||||
<Table.Cell>{new Date(auditLog.createdAt).toLocaleString()}</Table.Cell>
|
||||
<Table.Cell>
|
||||
<Badge variant="outline">{toFriendlyEventString(auditLog.event)}</Badge>
|
||||
</Table.Cell>
|
||||
<Table.Cell>{auditLog.ipAddress}</Table.Cell>
|
||||
<Table.Cell>{auditLog.device}</Table.Cell>
|
||||
<Table.Cell>{auditLog.data.clientName}</Table.Cell>
|
||||
</Table.Row>
|
||||
{/each}
|
||||
{/if}
|
||||
</Table.Body>
|
||||
</Table.Root>
|
||||
|
||||
{#if auditLogs?.data?.length ?? 0 > 0}
|
||||
<Pagination.Root
|
||||
class="mt-5"
|
||||
count={auditLogs.pagination.totalItems}
|
||||
perPage={pagination.limit}
|
||||
onPageChange={async (p) =>
|
||||
(auditLogs = await auditLogService.list({
|
||||
page: p,
|
||||
limit: pagination.limit
|
||||
}))}
|
||||
bind:page={auditLogs.pagination.currentPage}
|
||||
let:pages
|
||||
let:currentPage
|
||||
>
|
||||
<Pagination.Content class="flex justify-end">
|
||||
<Pagination.Item>
|
||||
<Pagination.PrevButton />
|
||||
</Pagination.Item>
|
||||
{#each pages as page (page.key)}
|
||||
{#if page.type === 'ellipsis'}
|
||||
<Pagination.Item>
|
||||
<Pagination.Ellipsis />
|
||||
</Pagination.Item>
|
||||
{:else}
|
||||
<Pagination.Item>
|
||||
<Pagination.Link {page} isActive={auditLogs.pagination.currentPage === page.value}>
|
||||
{page.value}
|
||||
</Pagination.Link>
|
||||
</Pagination.Item>
|
||||
{/if}
|
||||
{/each}
|
||||
<Pagination.Item>
|
||||
<Pagination.NextButton />
|
||||
</Pagination.Item>
|
||||
</Pagination.Content>
|
||||
</Pagination.Root>
|
||||
{/if}
|
||||
<AdvancedTable
|
||||
items={auditLogs}
|
||||
{fetchItems}
|
||||
columns={['Time', 'Event', 'Approximate Location', 'IP Address', 'Device', 'Client']}
|
||||
withoutSearch
|
||||
>
|
||||
{#snippet rows({ item })}
|
||||
<Table.Cell>{new Date(item.createdAt).toLocaleString()}</Table.Cell>
|
||||
<Table.Cell>
|
||||
<Badge variant="outline">{toFriendlyEventString(item.event)}</Badge>
|
||||
</Table.Cell>
|
||||
<Table.Cell
|
||||
>{item.city && item.country ? `${item.city}, ${item.country}` : 'Unknown'}</Table.Cell
|
||||
>
|
||||
<Table.Cell>{item.ipAddress}</Table.Cell>
|
||||
<Table.Cell>{item.device}</Table.Cell>
|
||||
<Table.Cell>{item.data.clientName}</Table.Cell>
|
||||
{/snippet}
|
||||
</AdvancedTable>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
:80 {
|
||||
:{$CADDY_PORT:80} {
|
||||
reverse_proxy /api/* http://localhost:{$BACKEND_PORT:8080}
|
||||
reverse_proxy /.well-known/* http://localhost:{$BACKEND_PORT:8080}
|
||||
reverse_proxy /* http://localhost:{$PORT:3000}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
:80 {
|
||||
:{$CADDY_PORT:80} {
|
||||
reverse_proxy /api/* http://localhost:{$BACKEND_PORT:8080} {
|
||||
trusted_proxies 0.0.0.0/0
|
||||
}
|
||||
|
||||
31
scripts/download-ip-database.sh
Normal file
31
scripts/download-ip-database.sh
Normal file
@@ -0,0 +1,31 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Check if the license key environment variable is set
|
||||
if [ -z "$MAXMIND_LICENSE_KEY" ]; then
|
||||
echo "Error: MAXMIND_LICENSE_KEY environment variable is not set."
|
||||
echo "Please set it using 'export MAXMIND_LICENSE_KEY=your_license_key' and try again."
|
||||
exit 1
|
||||
fi
|
||||
echo $MAXMIND_LICENSE_KEY
|
||||
# GeoLite2 City Database URL
|
||||
URL="https://download.maxmind.com/app/geoip_download?edition_id=GeoLite2-City&license_key=${MAXMIND_LICENSE_KEY}&suffix=tar.gz"
|
||||
|
||||
# Download directory
|
||||
DOWNLOAD_DIR="./geolite2_db"
|
||||
TARGET_PATH=./backend/GeoLite2-City.mmdb
|
||||
mkdir -p $DOWNLOAD_DIR
|
||||
|
||||
# Download the database
|
||||
echo "Downloading GeoLite2 City database..."
|
||||
curl -L -o "$DOWNLOAD_DIR/GeoLite2-City.tar.gz" "$URL"
|
||||
|
||||
# Extract the downloaded file
|
||||
echo "Extracting GeoLite2 City database..."
|
||||
tar -xzf "$DOWNLOAD_DIR/GeoLite2-City.tar.gz" -C $DOWNLOAD_DIR --strip-components=1
|
||||
|
||||
mv "$DOWNLOAD_DIR/GeoLite2-City.mmdb" $TARGET_PATH
|
||||
|
||||
# Clean up
|
||||
rm -rf "$DOWNLOAD_DIR"
|
||||
|
||||
echo "GeoLite2 City database downloaded and extracted to $TARGET_PATH"
|
||||
Reference in New Issue
Block a user