mirror of
https://github.com/pocket-id/pocket-id.git
synced 2025-12-17 01:11:38 +03:00
Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
712ff396f4 | ||
|
|
090eca202d | ||
|
|
d4055af3f4 | ||
|
|
692ff70c91 | ||
|
|
d5dd118a3f | ||
|
|
06b90eddd6 | ||
|
|
e284e352e2 | ||
|
|
5101b14eec | ||
|
|
bc8f454ea1 | ||
|
|
fda08ac1cd | ||
|
|
05a98ebe87 | ||
|
|
6e3728ddc8 |
18
CHANGELOG.md
18
CHANGELOG.md
@@ -1,3 +1,21 @@
|
|||||||
|
## [](https://github.com/stonith404/pocket-id/compare/v0.24.1...v) (2025-01-19)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* add LDAP sync ([#106](https://github.com/stonith404/pocket-id/issues/106)) ([5101b14](https://github.com/stonith404/pocket-id/commit/5101b14eec68a9507e1730994178d0ebe8185876))
|
||||||
|
* allow sign in with email ([#100](https://github.com/stonith404/pocket-id/issues/100)) ([06b90ed](https://github.com/stonith404/pocket-id/commit/06b90eddd645cce57813f2536e4a6a8836548f2b))
|
||||||
|
* automatically authorize client if signed in ([d5dd118](https://github.com/stonith404/pocket-id/commit/d5dd118a3f4ad6eed9ca496c458201bb10f148a0))
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* always set secure on cookie ([#130](https://github.com/stonith404/pocket-id/issues/130)) ([fda08ac](https://github.com/stonith404/pocket-id/commit/fda08ac1cd88842e25dc47395ed1288a5cfac4f8))
|
||||||
|
* don't panic if LDAP sync fails on startup ([e284e35](https://github.com/stonith404/pocket-id/commit/e284e352e2b95fac1d098de3d404e8531de4b869))
|
||||||
|
* improve spacing of checkboxes on application configuration page ([090eca2](https://github.com/stonith404/pocket-id/commit/090eca202d198852e6fbf4e6bebaf3b5ada13944))
|
||||||
|
* search input not displayed if response hasn't any items ([05a98eb](https://github.com/stonith404/pocket-id/commit/05a98ebe87d7a88e8b96b144c53250a40d724ec3))
|
||||||
|
* session duration ignored in cookie expiration ([bc8f454](https://github.com/stonith404/pocket-id/commit/bc8f454ea173ecc60e06450a1d22e24207f76714))
|
||||||
|
|
||||||
## [](https://github.com/stonith404/pocket-id/compare/v0.24.0...v) (2025-01-13)
|
## [](https://github.com/stonith404/pocket-id/compare/v0.24.0...v) (2025-01-13)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -113,7 +113,7 @@ proxy_buffer_size 256k;
|
|||||||
|
|
||||||
## Proxy Services with Pocket ID
|
## Proxy Services with Pocket ID
|
||||||
|
|
||||||
As the goal of Pocket ID is to stay simple, it doesn't have a built-in proxy provider. However, you can use [OAuth2 Proxy](https://oauth2-proxy.github.io/oauth2-proxy) to add authentication to your services that don't support OIDC.
|
The goal of Pocket ID is to function exclusively as an OIDC provider. As such, we don't have a built-in proxy provider. However, you can use other tools that act as a middleware to protect your services and support OIDC as an authentication provider.
|
||||||
|
|
||||||
See the [guide](docs/proxy-services.md) for more information.
|
See the [guide](docs/proxy-services.md) for more information.
|
||||||
|
|
||||||
@@ -175,6 +175,7 @@ docker compose up -d
|
|||||||
| `PORT` | `3000` | no | The port on which the frontend should listen. |
|
| `PORT` | `3000` | no | The port on which the frontend should listen. |
|
||||||
| `BACKEND_PORT` | `8080` | no | The port on which the backend should listen. |
|
| `BACKEND_PORT` | `8080` | no | The port on which the backend should listen. |
|
||||||
|
|
||||||
|
|
||||||
## Account recovery
|
## Account recovery
|
||||||
|
|
||||||
There are two ways to create a one-time access link for a user:
|
There are two ways to create a one-time access link for a user:
|
||||||
|
|||||||
1
backend/.gitignore
vendored
1
backend/.gitignore
vendored
@@ -14,3 +14,4 @@
|
|||||||
# Dependency directories (remove the comment below to include it)
|
# Dependency directories (remove the comment below to include it)
|
||||||
# vendor/
|
# vendor/
|
||||||
./data
|
./data
|
||||||
|
.env
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ require (
|
|||||||
github.com/joho/godotenv v1.5.1
|
github.com/joho/godotenv v1.5.1
|
||||||
github.com/mileusna/useragent v1.3.5
|
github.com/mileusna/useragent v1.3.5
|
||||||
github.com/oschwald/maxminddb-golang/v2 v2.0.0-beta.1
|
github.com/oschwald/maxminddb-golang/v2 v2.0.0-beta.1
|
||||||
golang.org/x/crypto v0.27.0
|
golang.org/x/crypto v0.31.0
|
||||||
golang.org/x/time v0.6.0
|
golang.org/x/time v0.6.0
|
||||||
gorm.io/driver/postgres v1.5.11
|
gorm.io/driver/postgres v1.5.11
|
||||||
gorm.io/driver/sqlite v1.5.6
|
gorm.io/driver/sqlite v1.5.6
|
||||||
@@ -23,12 +23,15 @@ require (
|
|||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 // indirect
|
||||||
github.com/bytedance/sonic v1.12.3 // indirect
|
github.com/bytedance/sonic v1.12.3 // indirect
|
||||||
github.com/bytedance/sonic/loader v0.2.0 // indirect
|
github.com/bytedance/sonic/loader v0.2.0 // indirect
|
||||||
github.com/cloudwego/base64x v0.1.4 // indirect
|
github.com/cloudwego/base64x v0.1.4 // indirect
|
||||||
github.com/cloudwego/iasm v0.2.0 // indirect
|
github.com/cloudwego/iasm v0.2.0 // indirect
|
||||||
github.com/gabriel-vasile/mimetype v1.4.5 // indirect
|
github.com/gabriel-vasile/mimetype v1.4.5 // indirect
|
||||||
github.com/gin-contrib/sse v0.1.0 // indirect
|
github.com/gin-contrib/sse v0.1.0 // indirect
|
||||||
|
github.com/go-asn1-ber/asn1-ber v1.5.7 // indirect
|
||||||
|
github.com/go-ldap/ldap/v3 v3.4.10 // indirect
|
||||||
github.com/go-playground/locales v0.14.1 // indirect
|
github.com/go-playground/locales v0.14.1 // indirect
|
||||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||||
github.com/go-webauthn/x v0.1.14 // indirect
|
github.com/go-webauthn/x v0.1.14 // indirect
|
||||||
@@ -61,10 +64,10 @@ require (
|
|||||||
go.uber.org/atomic v1.11.0 // indirect
|
go.uber.org/atomic v1.11.0 // indirect
|
||||||
golang.org/x/arch v0.10.0 // indirect
|
golang.org/x/arch v0.10.0 // indirect
|
||||||
golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 // indirect
|
golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 // indirect
|
||||||
golang.org/x/net v0.29.0 // indirect
|
golang.org/x/net v0.33.0 // indirect
|
||||||
golang.org/x/sync v0.8.0 // indirect
|
golang.org/x/sync v0.10.0 // indirect
|
||||||
golang.org/x/sys v0.25.0 // indirect
|
golang.org/x/sys v0.28.0 // indirect
|
||||||
golang.org/x/text v0.18.0 // indirect
|
golang.org/x/text v0.21.0 // indirect
|
||||||
google.golang.org/protobuf v1.34.2 // indirect
|
google.golang.org/protobuf v1.34.2 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0=
|
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0=
|
||||||
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
|
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
|
||||||
|
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 h1:mFRzDkZVAjdal+s7s0MwaRv9igoPqLRdzOLzw/8Xvq8=
|
||||||
|
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU=
|
||||||
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
|
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
|
||||||
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
||||||
|
github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4=
|
||||||
github.com/bytedance/sonic v1.12.3 h1:W2MGa7RCU1QTeYRTPE3+88mVC0yXmsRQRChiyVocVjU=
|
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 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.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
|
||||||
@@ -37,8 +40,12 @@ 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-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 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=
|
||||||
github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
|
github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
|
||||||
|
github.com/go-asn1-ber/asn1-ber v1.5.7 h1:DTX+lbVTWaTw1hQ+PbZPlnDZPEIs0SS/GCZAl535dDk=
|
||||||
|
github.com/go-asn1-ber/asn1-ber v1.5.7/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
|
||||||
github.com/go-co-op/gocron/v2 v2.12.1 h1:dCIIBFbzhWKdgXeEifBjHPzgQ1hoWhjS4289Hjjy1uw=
|
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-co-op/gocron/v2 v2.12.1/go.mod h1:xY7bJxGazKam1cz04EebrlP4S9q4iWdiAylMGP3jY9w=
|
||||||
|
github.com/go-ldap/ldap/v3 v3.4.10 h1:ot/iwPOhfpNVgB1o+AVXljizWZ9JTp7YF5oeyONmcJU=
|
||||||
|
github.com/go-ldap/ldap/v3 v3.4.10/go.mod h1:JXh4Uxgi40P6E9rdsYqpUtbW46D9UTjJ9QSwGRznplY=
|
||||||
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
|
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
|
||||||
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||||
@@ -70,11 +77,15 @@ github.com/google/go-tpm v0.9.1/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u
|
|||||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
|
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
|
||||||
|
github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
|
||||||
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||||
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
|
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
|
||||||
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||||
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
|
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
|
||||||
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
|
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
|
||||||
|
github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
||||||
|
github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
||||||
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
||||||
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
||||||
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk=
|
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk=
|
||||||
@@ -83,6 +94,12 @@ github.com/jackc/pgx/v5 v5.5.5 h1:amBjrZVmksIdNjxGW/IiIMzxMKZFelXbUoPNb+8sjQw=
|
|||||||
github.com/jackc/pgx/v5 v5.5.5/go.mod h1:ez9gk+OAat140fv9ErkZDYFWmXLfV+++K0uAOiwgm1A=
|
github.com/jackc/pgx/v5 v5.5.5/go.mod h1:ez9gk+OAat140fv9ErkZDYFWmXLfV+++K0uAOiwgm1A=
|
||||||
github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk=
|
github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk=
|
||||||
github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
||||||
|
github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs=
|
||||||
|
github.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM=
|
||||||
|
github.com/jcmturner/gofork v1.7.6/go.mod h1:1622LH6i/EZqLloHfE7IeZ0uEJwMSUyQ/nDd82IeqRo=
|
||||||
|
github.com/jcmturner/goidentity/v6 v6.0.1/go.mod h1:X1YW3bgtvwAXju7V3LCIMpY0Gbxyjn/mY9zx4tFonSg=
|
||||||
|
github.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs=
|
||||||
|
github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc=
|
||||||
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
||||||
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
||||||
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
|
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
|
||||||
@@ -146,6 +163,7 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+
|
|||||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
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.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
|
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
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.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.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||||
@@ -158,6 +176,7 @@ github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65E
|
|||||||
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
||||||
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
|
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
|
||||||
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
|
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
|
||||||
|
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 h1:TT4fX+nBOA/+LUkobKGW1ydGcn+G3vRw9+g5HwCphpk=
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 h1:TT4fX+nBOA/+LUkobKGW1ydGcn+G3vRw9+g5HwCphpk=
|
||||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0/go.mod h1:L7UH0GbB0p47T4Rri3uHjbpCFYrVrwc1I25QhNPiGK8=
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0/go.mod h1:L7UH0GbB0p47T4Rri3uHjbpCFYrVrwc1I25QhNPiGK8=
|
||||||
go.opentelemetry.io/otel v1.29.0 h1:PdomN/Al4q/lN6iBJEN3AwPvUiHPMlt93c8bqTG5Llw=
|
go.opentelemetry.io/otel v1.29.0 h1:PdomN/Al4q/lN6iBJEN3AwPvUiHPMlt93c8bqTG5Llw=
|
||||||
@@ -172,27 +191,98 @@ go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
|||||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||||
golang.org/x/arch v0.10.0 h1:S3huipmSclq3PJMNe76NGwkBR504WFkQ5dhzWzP8ZW8=
|
golang.org/x/arch v0.10.0 h1:S3huipmSclq3PJMNe76NGwkBR504WFkQ5dhzWzP8ZW8=
|
||||||
golang.org/x/arch v0.10.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
|
golang.org/x/arch v0.10.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
|
||||||
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
|
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||||
|
golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
|
||||||
|
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
|
||||||
|
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||||
|
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||||
golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A=
|
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/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70=
|
||||||
|
golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
|
||||||
|
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
||||||
golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 h1:e66Fs6Z+fZTbFBAxKfP3PALWBtpfqks2bwGcexMxgtk=
|
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/exp v0.0.0-20240909161429-701f63a606c0/go.mod h1:2TbTHSBQa924w8M6Xs1QcRcFwyucIwBGpK1p2f1YFFY=
|
||||||
|
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||||
|
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||||
|
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||||
|
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||||
|
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||||
|
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||||
|
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||||
|
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||||
|
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||||
|
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||||
|
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
|
||||||
|
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||||
|
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||||
golang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo=
|
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/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0=
|
||||||
|
golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
|
||||||
|
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
|
||||||
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
|
||||||
|
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||||
|
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||||
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
|
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
|
||||||
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||||
|
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
|
||||||
|
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||||
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
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.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
|
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=
|
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/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
|
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
|
||||||
|
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
|
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
|
||||||
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
|
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||||
|
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||||
|
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
||||||
|
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
|
||||||
|
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
||||||
|
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
|
||||||
|
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
|
||||||
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
|
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||||
|
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||||
|
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||||
|
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||||
|
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||||
|
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||||
golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224=
|
golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224=
|
||||||
golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
|
golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
|
||||||
|
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
|
||||||
|
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||||
golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U=
|
golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U=
|
||||||
golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||||
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
|
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||||
|
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||||
|
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
|
||||||
|
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
||||||
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
|
google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
|
||||||
google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
|
google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||||
|
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package bootstrap
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
_ "github.com/golang-migrate/migrate/v4/source/file"
|
_ "github.com/golang-migrate/migrate/v4/source/file"
|
||||||
"github.com/stonith404/pocket-id/backend/internal/job"
|
|
||||||
"github.com/stonith404/pocket-id/backend/internal/service"
|
"github.com/stonith404/pocket-id/backend/internal/service"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -11,6 +10,5 @@ func Bootstrap() {
|
|||||||
appConfigService := service.NewAppConfigService(db)
|
appConfigService := service.NewAppConfigService(db)
|
||||||
|
|
||||||
initApplicationImages()
|
initApplicationImages()
|
||||||
job.RegisterJobs(db)
|
|
||||||
initRouter(db, appConfigService)
|
initRouter(db, appConfigService)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/stonith404/pocket-id/backend/internal/common"
|
"github.com/stonith404/pocket-id/backend/internal/common"
|
||||||
"github.com/stonith404/pocket-id/backend/internal/controller"
|
"github.com/stonith404/pocket-id/backend/internal/controller"
|
||||||
|
"github.com/stonith404/pocket-id/backend/internal/job"
|
||||||
"github.com/stonith404/pocket-id/backend/internal/middleware"
|
"github.com/stonith404/pocket-id/backend/internal/middleware"
|
||||||
"github.com/stonith404/pocket-id/backend/internal/service"
|
"github.com/stonith404/pocket-id/backend/internal/service"
|
||||||
"golang.org/x/time/rate"
|
"golang.org/x/time/rate"
|
||||||
@@ -37,27 +38,34 @@ func initRouter(db *gorm.DB, appConfigService *service.AppConfigService) {
|
|||||||
auditLogService := service.NewAuditLogService(db, appConfigService, emailService, geoLiteService)
|
auditLogService := service.NewAuditLogService(db, appConfigService, emailService, geoLiteService)
|
||||||
jwtService := service.NewJwtService(appConfigService)
|
jwtService := service.NewJwtService(appConfigService)
|
||||||
webauthnService := service.NewWebAuthnService(db, jwtService, auditLogService, appConfigService)
|
webauthnService := service.NewWebAuthnService(db, jwtService, auditLogService, appConfigService)
|
||||||
userService := service.NewUserService(db, jwtService, auditLogService)
|
userService := service.NewUserService(db, jwtService, auditLogService, emailService)
|
||||||
customClaimService := service.NewCustomClaimService(db)
|
customClaimService := service.NewCustomClaimService(db)
|
||||||
oidcService := service.NewOidcService(db, jwtService, appConfigService, auditLogService, customClaimService)
|
oidcService := service.NewOidcService(db, jwtService, appConfigService, auditLogService, customClaimService)
|
||||||
testService := service.NewTestService(db, appConfigService)
|
testService := service.NewTestService(db, appConfigService)
|
||||||
userGroupService := service.NewUserGroupService(db)
|
userGroupService := service.NewUserGroupService(db)
|
||||||
|
ldapService := service.NewLdapService(db, appConfigService, userService, userGroupService)
|
||||||
|
|
||||||
|
rateLimitMiddleware := middleware.NewRateLimitMiddleware()
|
||||||
|
|
||||||
|
// Setup global middleware
|
||||||
r.Use(middleware.NewCorsMiddleware().Add())
|
r.Use(middleware.NewCorsMiddleware().Add())
|
||||||
r.Use(middleware.NewErrorHandlerMiddleware().Add())
|
r.Use(middleware.NewErrorHandlerMiddleware().Add())
|
||||||
r.Use(middleware.NewRateLimitMiddleware().Add(rate.Every(time.Second), 60))
|
r.Use(rateLimitMiddleware.Add(rate.Every(time.Second), 60))
|
||||||
r.Use(middleware.NewJwtAuthMiddleware(jwtService, true).Add(false))
|
r.Use(middleware.NewJwtAuthMiddleware(jwtService, true).Add(false))
|
||||||
|
|
||||||
// Initialize middleware
|
job.RegisterLdapJobs(ldapService, appConfigService)
|
||||||
|
job.RegisterDbCleanupJobs(db)
|
||||||
|
|
||||||
|
// Initialize middleware for specific routes
|
||||||
jwtAuthMiddleware := middleware.NewJwtAuthMiddleware(jwtService, false)
|
jwtAuthMiddleware := middleware.NewJwtAuthMiddleware(jwtService, false)
|
||||||
fileSizeLimitMiddleware := middleware.NewFileSizeLimitMiddleware()
|
fileSizeLimitMiddleware := middleware.NewFileSizeLimitMiddleware()
|
||||||
|
|
||||||
// Set up API routes
|
// Set up API routes
|
||||||
apiGroup := r.Group("/api")
|
apiGroup := r.Group("/api")
|
||||||
controller.NewWebauthnController(apiGroup, jwtAuthMiddleware, middleware.NewRateLimitMiddleware(), webauthnService)
|
controller.NewWebauthnController(apiGroup, jwtAuthMiddleware, middleware.NewRateLimitMiddleware(), webauthnService, appConfigService)
|
||||||
controller.NewOidcController(apiGroup, jwtAuthMiddleware, fileSizeLimitMiddleware, oidcService, jwtService)
|
controller.NewOidcController(apiGroup, jwtAuthMiddleware, fileSizeLimitMiddleware, oidcService, jwtService)
|
||||||
controller.NewUserController(apiGroup, jwtAuthMiddleware, middleware.NewRateLimitMiddleware(), userService, appConfigService)
|
controller.NewUserController(apiGroup, jwtAuthMiddleware, middleware.NewRateLimitMiddleware(), userService, appConfigService)
|
||||||
controller.NewAppConfigController(apiGroup, jwtAuthMiddleware, appConfigService, emailService)
|
controller.NewAppConfigController(apiGroup, jwtAuthMiddleware, appConfigService, emailService, ldapService)
|
||||||
controller.NewAuditLogController(apiGroup, auditLogService, jwtAuthMiddleware)
|
controller.NewAuditLogController(apiGroup, auditLogService, jwtAuthMiddleware)
|
||||||
controller.NewUserGroupController(apiGroup, jwtAuthMiddleware, userGroupService)
|
controller.NewUserGroupController(apiGroup, jwtAuthMiddleware, userGroupService)
|
||||||
controller.NewCustomClaimController(apiGroup, jwtAuthMiddleware, customClaimService)
|
controller.NewCustomClaimController(apiGroup, jwtAuthMiddleware, customClaimService)
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
package common
|
package common
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"log"
|
||||||
|
|
||||||
"github.com/caarlos0/env/v11"
|
"github.com/caarlos0/env/v11"
|
||||||
_ "github.com/joho/godotenv/autoload"
|
_ "github.com/joho/godotenv/autoload"
|
||||||
"log"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type DbProvider string
|
type DbProvider string
|
||||||
|
|||||||
@@ -58,7 +58,9 @@ func (e *OidcInvalidAuthorizationCodeError) HttpStatusCode() int { return 400 }
|
|||||||
|
|
||||||
type OidcInvalidCallbackURLError struct{}
|
type OidcInvalidCallbackURLError struct{}
|
||||||
|
|
||||||
func (e *OidcInvalidCallbackURLError) Error() string { return "invalid callback URL, it might be necessary for an admin to fix this" }
|
func (e *OidcInvalidCallbackURLError) Error() string {
|
||||||
|
return "invalid callback URL, it might be necessary for an admin to fix this"
|
||||||
|
}
|
||||||
func (e *OidcInvalidCallbackURLError) HttpStatusCode() int { return 400 }
|
func (e *OidcInvalidCallbackURLError) HttpStatusCode() int { return 400 }
|
||||||
|
|
||||||
type FileTypeNotSupportedError struct{}
|
type FileTypeNotSupportedError struct{}
|
||||||
@@ -95,7 +97,7 @@ func (e *MissingPermissionError) HttpStatusCode() int { return http.StatusForbid
|
|||||||
type TooManyRequestsError struct{}
|
type TooManyRequestsError struct{}
|
||||||
|
|
||||||
func (e *TooManyRequestsError) Error() string {
|
func (e *TooManyRequestsError) Error() string {
|
||||||
return "Too many requests. Please wait a while before trying again."
|
return "Too many requests"
|
||||||
}
|
}
|
||||||
func (e *TooManyRequestsError) HttpStatusCode() int { return http.StatusTooManyRequests }
|
func (e *TooManyRequestsError) HttpStatusCode() int { return http.StatusTooManyRequests }
|
||||||
|
|
||||||
@@ -160,3 +162,17 @@ func (e *OidcMissingCodeChallengeError) Error() string {
|
|||||||
return "Missing code challenge"
|
return "Missing code challenge"
|
||||||
}
|
}
|
||||||
func (e *OidcMissingCodeChallengeError) HttpStatusCode() int { return http.StatusBadRequest }
|
func (e *OidcMissingCodeChallengeError) HttpStatusCode() int { return http.StatusBadRequest }
|
||||||
|
|
||||||
|
type LdapUserUpdateError struct{}
|
||||||
|
|
||||||
|
func (e *LdapUserUpdateError) Error() string {
|
||||||
|
return "LDAP users can't be updated"
|
||||||
|
}
|
||||||
|
func (e *LdapUserUpdateError) HttpStatusCode() int { return http.StatusForbidden }
|
||||||
|
|
||||||
|
type LdapUserGroupUpdateError struct{}
|
||||||
|
|
||||||
|
func (e *LdapUserGroupUpdateError) Error() string {
|
||||||
|
return "LDAP user groups can't be updated"
|
||||||
|
}
|
||||||
|
func (e *LdapUserGroupUpdateError) HttpStatusCode() int { return http.StatusForbidden }
|
||||||
|
|||||||
@@ -16,11 +16,13 @@ func NewAppConfigController(
|
|||||||
jwtAuthMiddleware *middleware.JwtAuthMiddleware,
|
jwtAuthMiddleware *middleware.JwtAuthMiddleware,
|
||||||
appConfigService *service.AppConfigService,
|
appConfigService *service.AppConfigService,
|
||||||
emailService *service.EmailService,
|
emailService *service.EmailService,
|
||||||
|
ldapService *service.LdapService,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
acc := &AppConfigController{
|
acc := &AppConfigController{
|
||||||
appConfigService: appConfigService,
|
appConfigService: appConfigService,
|
||||||
emailService: emailService,
|
emailService: emailService,
|
||||||
|
ldapService: ldapService,
|
||||||
}
|
}
|
||||||
group.GET("/application-configuration", acc.listAppConfigHandler)
|
group.GET("/application-configuration", acc.listAppConfigHandler)
|
||||||
group.GET("/application-configuration/all", jwtAuthMiddleware.Add(true), acc.listAllAppConfigHandler)
|
group.GET("/application-configuration/all", jwtAuthMiddleware.Add(true), acc.listAllAppConfigHandler)
|
||||||
@@ -34,11 +36,13 @@ func NewAppConfigController(
|
|||||||
group.PUT("/application-configuration/background-image", jwtAuthMiddleware.Add(true), acc.updateBackgroundImageHandler)
|
group.PUT("/application-configuration/background-image", jwtAuthMiddleware.Add(true), acc.updateBackgroundImageHandler)
|
||||||
|
|
||||||
group.POST("/application-configuration/test-email", jwtAuthMiddleware.Add(true), acc.testEmailHandler)
|
group.POST("/application-configuration/test-email", jwtAuthMiddleware.Add(true), acc.testEmailHandler)
|
||||||
|
group.POST("/application-configuration/sync-ldap", jwtAuthMiddleware.Add(true), acc.syncLdapHandler)
|
||||||
}
|
}
|
||||||
|
|
||||||
type AppConfigController struct {
|
type AppConfigController struct {
|
||||||
appConfigService *service.AppConfigService
|
appConfigService *service.AppConfigService
|
||||||
emailService *service.EmailService
|
emailService *service.EmailService
|
||||||
|
ldapService *service.LdapService
|
||||||
}
|
}
|
||||||
|
|
||||||
func (acc *AppConfigController) listAppConfigHandler(c *gin.Context) {
|
func (acc *AppConfigController) listAppConfigHandler(c *gin.Context) {
|
||||||
@@ -182,6 +186,15 @@ func (acc *AppConfigController) updateImage(c *gin.Context, imageName string, ol
|
|||||||
c.Status(http.StatusNoContent)
|
c.Status(http.StatusNoContent)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (acc *AppConfigController) syncLdapHandler(c *gin.Context) {
|
||||||
|
err := acc.ldapService.SyncAll()
|
||||||
|
if err != nil {
|
||||||
|
c.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Status(http.StatusNoContent)
|
||||||
|
}
|
||||||
func (acc *AppConfigController) testEmailHandler(c *gin.Context) {
|
func (acc *AppConfigController) testEmailHandler(c *gin.Context) {
|
||||||
userID := c.GetString("userID")
|
userID := c.GetString("userID")
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
package controller
|
package controller
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/stonith404/pocket-id/backend/internal/common"
|
"github.com/stonith404/pocket-id/backend/internal/common"
|
||||||
"github.com/stonith404/pocket-id/backend/internal/dto"
|
"github.com/stonith404/pocket-id/backend/internal/dto"
|
||||||
@@ -8,14 +11,12 @@ import (
|
|||||||
"github.com/stonith404/pocket-id/backend/internal/service"
|
"github.com/stonith404/pocket-id/backend/internal/service"
|
||||||
"github.com/stonith404/pocket-id/backend/internal/utils"
|
"github.com/stonith404/pocket-id/backend/internal/utils"
|
||||||
"golang.org/x/time/rate"
|
"golang.org/x/time/rate"
|
||||||
"net/http"
|
|
||||||
"time"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func NewUserController(group *gin.RouterGroup, jwtAuthMiddleware *middleware.JwtAuthMiddleware, rateLimitMiddleware *middleware.RateLimitMiddleware, userService *service.UserService, appConfigService *service.AppConfigService) {
|
func NewUserController(group *gin.RouterGroup, jwtAuthMiddleware *middleware.JwtAuthMiddleware, rateLimitMiddleware *middleware.RateLimitMiddleware, userService *service.UserService, appConfigService *service.AppConfigService) {
|
||||||
uc := UserController{
|
uc := UserController{
|
||||||
UserService: userService,
|
userService: userService,
|
||||||
AppConfigService: appConfigService,
|
appConfigService: appConfigService,
|
||||||
}
|
}
|
||||||
|
|
||||||
group.GET("/users", jwtAuthMiddleware.Add(true), uc.listUsersHandler)
|
group.GET("/users", jwtAuthMiddleware.Add(true), uc.listUsersHandler)
|
||||||
@@ -29,11 +30,12 @@ func NewUserController(group *gin.RouterGroup, jwtAuthMiddleware *middleware.Jwt
|
|||||||
group.POST("/users/:id/one-time-access-token", jwtAuthMiddleware.Add(true), uc.createOneTimeAccessTokenHandler)
|
group.POST("/users/:id/one-time-access-token", jwtAuthMiddleware.Add(true), uc.createOneTimeAccessTokenHandler)
|
||||||
group.POST("/one-time-access-token/:token", rateLimitMiddleware.Add(rate.Every(10*time.Second), 5), uc.exchangeOneTimeAccessTokenHandler)
|
group.POST("/one-time-access-token/:token", rateLimitMiddleware.Add(rate.Every(10*time.Second), 5), uc.exchangeOneTimeAccessTokenHandler)
|
||||||
group.POST("/one-time-access-token/setup", uc.getSetupAccessTokenHandler)
|
group.POST("/one-time-access-token/setup", uc.getSetupAccessTokenHandler)
|
||||||
|
group.POST("/one-time-access-email", rateLimitMiddleware.Add(rate.Every(10*time.Minute), 3), uc.requestOneTimeAccessEmailHandler)
|
||||||
}
|
}
|
||||||
|
|
||||||
type UserController struct {
|
type UserController struct {
|
||||||
UserService *service.UserService
|
userService *service.UserService
|
||||||
AppConfigService *service.AppConfigService
|
appConfigService *service.AppConfigService
|
||||||
}
|
}
|
||||||
|
|
||||||
func (uc *UserController) listUsersHandler(c *gin.Context) {
|
func (uc *UserController) listUsersHandler(c *gin.Context) {
|
||||||
@@ -44,7 +46,7 @@ func (uc *UserController) listUsersHandler(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
users, pagination, err := uc.UserService.ListUsers(searchTerm, sortedPaginationRequest)
|
users, pagination, err := uc.userService.ListUsers(searchTerm, sortedPaginationRequest)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.Error(err)
|
c.Error(err)
|
||||||
return
|
return
|
||||||
@@ -63,7 +65,7 @@ func (uc *UserController) listUsersHandler(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (uc *UserController) getUserHandler(c *gin.Context) {
|
func (uc *UserController) getUserHandler(c *gin.Context) {
|
||||||
user, err := uc.UserService.GetUser(c.Param("id"))
|
user, err := uc.userService.GetUser(c.Param("id"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.Error(err)
|
c.Error(err)
|
||||||
return
|
return
|
||||||
@@ -79,7 +81,7 @@ func (uc *UserController) getUserHandler(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (uc *UserController) getCurrentUserHandler(c *gin.Context) {
|
func (uc *UserController) getCurrentUserHandler(c *gin.Context) {
|
||||||
user, err := uc.UserService.GetUser(c.GetString("userID"))
|
user, err := uc.userService.GetUser(c.GetString("userID"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.Error(err)
|
c.Error(err)
|
||||||
return
|
return
|
||||||
@@ -95,7 +97,7 @@ func (uc *UserController) getCurrentUserHandler(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (uc *UserController) deleteUserHandler(c *gin.Context) {
|
func (uc *UserController) deleteUserHandler(c *gin.Context) {
|
||||||
if err := uc.UserService.DeleteUser(c.Param("id")); err != nil {
|
if err := uc.userService.DeleteUser(c.Param("id")); err != nil {
|
||||||
c.Error(err)
|
c.Error(err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -110,7 +112,7 @@ func (uc *UserController) createUserHandler(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
user, err := uc.UserService.CreateUser(input)
|
user, err := uc.userService.CreateUser(input)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.Error(err)
|
c.Error(err)
|
||||||
return
|
return
|
||||||
@@ -130,7 +132,7 @@ func (uc *UserController) updateUserHandler(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (uc *UserController) updateCurrentUserHandler(c *gin.Context) {
|
func (uc *UserController) updateCurrentUserHandler(c *gin.Context) {
|
||||||
if uc.AppConfigService.DbConfig.AllowOwnAccountEdit.Value != "true" {
|
if uc.appConfigService.DbConfig.AllowOwnAccountEdit.Value != "true" {
|
||||||
c.Error(&common.AccountEditNotAllowedError{})
|
c.Error(&common.AccountEditNotAllowedError{})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -144,7 +146,7 @@ func (uc *UserController) createOneTimeAccessTokenHandler(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
token, err := uc.UserService.CreateOneTimeAccessToken(input.UserID, input.ExpiresAt, c.ClientIP(), c.Request.UserAgent())
|
token, err := uc.userService.CreateOneTimeAccessToken(input.UserID, input.ExpiresAt)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.Error(err)
|
c.Error(err)
|
||||||
return
|
return
|
||||||
@@ -153,8 +155,24 @@ func (uc *UserController) createOneTimeAccessTokenHandler(c *gin.Context) {
|
|||||||
c.JSON(http.StatusCreated, gin.H{"token": token})
|
c.JSON(http.StatusCreated, gin.H{"token": token})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (uc *UserController) requestOneTimeAccessEmailHandler(c *gin.Context) {
|
||||||
|
var input dto.OneTimeAccessEmailDto
|
||||||
|
if err := c.ShouldBindJSON(&input); err != nil {
|
||||||
|
c.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err := uc.userService.RequestOneTimeAccessEmail(input.Email, input.RedirectPath)
|
||||||
|
if err != nil {
|
||||||
|
c.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Status(http.StatusNoContent)
|
||||||
|
}
|
||||||
|
|
||||||
func (uc *UserController) exchangeOneTimeAccessTokenHandler(c *gin.Context) {
|
func (uc *UserController) exchangeOneTimeAccessTokenHandler(c *gin.Context) {
|
||||||
user, token, err := uc.UserService.ExchangeOneTimeAccessToken(c.Param("token"))
|
user, token, err := uc.userService.ExchangeOneTimeAccessToken(c.Param("token"), c.ClientIP(), c.Request.UserAgent())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.Error(err)
|
c.Error(err)
|
||||||
return
|
return
|
||||||
@@ -166,12 +184,12 @@ func (uc *UserController) exchangeOneTimeAccessTokenHandler(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
c.SetCookie("access_token", token, int(time.Hour.Seconds()), "/", "", false, true)
|
utils.AddAccessTokenCookie(c, uc.appConfigService.DbConfig.SessionDuration.Value, token)
|
||||||
c.JSON(http.StatusOK, userDto)
|
c.JSON(http.StatusOK, userDto)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (uc *UserController) getSetupAccessTokenHandler(c *gin.Context) {
|
func (uc *UserController) getSetupAccessTokenHandler(c *gin.Context) {
|
||||||
user, token, err := uc.UserService.SetupInitialAdmin()
|
user, token, err := uc.userService.SetupInitialAdmin()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.Error(err)
|
c.Error(err)
|
||||||
return
|
return
|
||||||
@@ -183,7 +201,7 @@ func (uc *UserController) getSetupAccessTokenHandler(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
c.SetCookie("access_token", token, int(time.Hour.Seconds()), "/", "", false, true)
|
utils.AddAccessTokenCookie(c, uc.appConfigService.DbConfig.SessionDuration.Value, token)
|
||||||
c.JSON(http.StatusOK, userDto)
|
c.JSON(http.StatusOK, userDto)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -201,7 +219,7 @@ func (uc *UserController) updateUser(c *gin.Context, updateOwnUser bool) {
|
|||||||
userID = c.Param("id")
|
userID = c.Param("id")
|
||||||
}
|
}
|
||||||
|
|
||||||
user, err := uc.UserService.UpdateUser(userID, input, updateOwnUser)
|
user, err := uc.userService.UpdateUser(userID, input, updateOwnUser, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.Error(err)
|
c.Error(err)
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -107,7 +107,7 @@ func (ugc *UserGroupController) update(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
group, err := ugc.UserGroupService.Update(c.Param("id"), input)
|
group, err := ugc.UserGroupService.Update(c.Param("id"), input, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.Error(err)
|
c.Error(err)
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"github.com/stonith404/pocket-id/backend/internal/common"
|
"github.com/stonith404/pocket-id/backend/internal/common"
|
||||||
"github.com/stonith404/pocket-id/backend/internal/dto"
|
"github.com/stonith404/pocket-id/backend/internal/dto"
|
||||||
"github.com/stonith404/pocket-id/backend/internal/middleware"
|
"github.com/stonith404/pocket-id/backend/internal/middleware"
|
||||||
|
"github.com/stonith404/pocket-id/backend/internal/utils"
|
||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -13,8 +14,8 @@ import (
|
|||||||
"golang.org/x/time/rate"
|
"golang.org/x/time/rate"
|
||||||
)
|
)
|
||||||
|
|
||||||
func NewWebauthnController(group *gin.RouterGroup, jwtAuthMiddleware *middleware.JwtAuthMiddleware, rateLimitMiddleware *middleware.RateLimitMiddleware, webauthnService *service.WebAuthnService) {
|
func NewWebauthnController(group *gin.RouterGroup, jwtAuthMiddleware *middleware.JwtAuthMiddleware, rateLimitMiddleware *middleware.RateLimitMiddleware, webauthnService *service.WebAuthnService, appConfigService *service.AppConfigService) {
|
||||||
wc := &WebauthnController{webAuthnService: webauthnService}
|
wc := &WebauthnController{webAuthnService: webauthnService, appConfigService: appConfigService}
|
||||||
group.GET("/webauthn/register/start", jwtAuthMiddleware.Add(false), wc.beginRegistrationHandler)
|
group.GET("/webauthn/register/start", jwtAuthMiddleware.Add(false), wc.beginRegistrationHandler)
|
||||||
group.POST("/webauthn/register/finish", jwtAuthMiddleware.Add(false), wc.verifyRegistrationHandler)
|
group.POST("/webauthn/register/finish", jwtAuthMiddleware.Add(false), wc.verifyRegistrationHandler)
|
||||||
|
|
||||||
@@ -30,6 +31,7 @@ func NewWebauthnController(group *gin.RouterGroup, jwtAuthMiddleware *middleware
|
|||||||
|
|
||||||
type WebauthnController struct {
|
type WebauthnController struct {
|
||||||
webAuthnService *service.WebAuthnService
|
webAuthnService *service.WebAuthnService
|
||||||
|
appConfigService *service.AppConfigService
|
||||||
}
|
}
|
||||||
|
|
||||||
func (wc *WebauthnController) beginRegistrationHandler(c *gin.Context) {
|
func (wc *WebauthnController) beginRegistrationHandler(c *gin.Context) {
|
||||||
@@ -40,7 +42,7 @@ func (wc *WebauthnController) beginRegistrationHandler(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
c.SetCookie("session_id", options.SessionID, int(options.Timeout.Seconds()), "/", "", false, true)
|
c.SetCookie("session_id", options.SessionID, int(options.Timeout.Seconds()), "/", "", true, true)
|
||||||
c.JSON(http.StatusOK, options.Response)
|
c.JSON(http.StatusOK, options.Response)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,7 +76,7 @@ func (wc *WebauthnController) beginLoginHandler(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
c.SetCookie("session_id", options.SessionID, int(options.Timeout.Seconds()), "/", "", false, true)
|
c.SetCookie("session_id", options.SessionID, int(options.Timeout.Seconds()), "/", "", true, true)
|
||||||
c.JSON(http.StatusOK, options.Response)
|
c.JSON(http.StatusOK, options.Response)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -103,7 +105,7 @@ func (wc *WebauthnController) verifyLoginHandler(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
c.SetCookie("access_token", token, int(time.Hour.Seconds()), "/", "", false, true)
|
utils.AddAccessTokenCookie(c, wc.appConfigService.DbConfig.SessionDuration.Value, token)
|
||||||
c.JSON(http.StatusOK, userDto)
|
c.JSON(http.StatusOK, userDto)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -163,6 +165,6 @@ func (wc *WebauthnController) updateCredentialHandler(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (wc *WebauthnController) logoutHandler(c *gin.Context) {
|
func (wc *WebauthnController) logoutHandler(c *gin.Context) {
|
||||||
c.SetCookie("access_token", "", 0, "/", "", false, true)
|
utils.AddAccessTokenCookie(c, "0", "")
|
||||||
c.Status(http.StatusNoContent)
|
c.Status(http.StatusNoContent)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ type AppConfigUpdateDto struct {
|
|||||||
SessionDuration string `json:"sessionDuration" binding:"required"`
|
SessionDuration string `json:"sessionDuration" binding:"required"`
|
||||||
EmailsVerified string `json:"emailsVerified" binding:"required"`
|
EmailsVerified string `json:"emailsVerified" binding:"required"`
|
||||||
AllowOwnAccountEdit string `json:"allowOwnAccountEdit" binding:"required"`
|
AllowOwnAccountEdit string `json:"allowOwnAccountEdit" binding:"required"`
|
||||||
EmailEnabled string `json:"emailEnabled" binding:"required"`
|
|
||||||
SmtHost string `json:"smtpHost"`
|
SmtHost string `json:"smtpHost"`
|
||||||
SmtpPort string `json:"smtpPort"`
|
SmtpPort string `json:"smtpPort"`
|
||||||
SmtpFrom string `json:"smtpFrom" binding:"omitempty,email"`
|
SmtpFrom string `json:"smtpFrom" binding:"omitempty,email"`
|
||||||
@@ -24,4 +23,20 @@ type AppConfigUpdateDto struct {
|
|||||||
SmtpPassword string `json:"smtpPassword"`
|
SmtpPassword string `json:"smtpPassword"`
|
||||||
SmtpTls string `json:"smtpTls"`
|
SmtpTls string `json:"smtpTls"`
|
||||||
SmtpSkipCertVerify string `json:"smtpSkipCertVerify"`
|
SmtpSkipCertVerify string `json:"smtpSkipCertVerify"`
|
||||||
|
LdapEnabled string `json:"ldapEnabled" binding:"required"`
|
||||||
|
LdapUrl string `json:"ldapUrl"`
|
||||||
|
LdapBindDn string `json:"ldapBindDn"`
|
||||||
|
LdapBindPassword string `json:"ldapBindPassword"`
|
||||||
|
LdapBase string `json:"ldapBase"`
|
||||||
|
LdapSkipCertVerify string `json:"ldapSkipCertVerify"`
|
||||||
|
LdapAttributeUserUniqueIdentifier string `json:"ldapAttributeUserUniqueIdentifier"`
|
||||||
|
LdapAttributeUserUsername string `json:"ldapAttributeUserUsername"`
|
||||||
|
LdapAttributeUserEmail string `json:"ldapAttributeUserEmail"`
|
||||||
|
LdapAttributeUserFirstName string `json:"ldapAttributeUserFirstName"`
|
||||||
|
LdapAttributeUserLastName string `json:"ldapAttributeUserLastName"`
|
||||||
|
LdapAttributeGroupUniqueIdentifier string `json:"ldapAttributeGroupUniqueIdentifier"`
|
||||||
|
LdapAttributeGroupName string `json:"ldapAttributeGroupName"`
|
||||||
|
LdapAttributeAdminGroup string `json:"ldapAttributeAdminGroup"`
|
||||||
|
EmailOneTimeAccessEnabled string `json:"emailOneTimeAccessEnabled" binding:"required"`
|
||||||
|
EmailLoginNotificationEnabled string `json:"emailLoginNotificationEnabled" binding:"required"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ type UserDto struct {
|
|||||||
LastName string `json:"lastName"`
|
LastName string `json:"lastName"`
|
||||||
IsAdmin bool `json:"isAdmin"`
|
IsAdmin bool `json:"isAdmin"`
|
||||||
CustomClaims []CustomClaimDto `json:"customClaims"`
|
CustomClaims []CustomClaimDto `json:"customClaims"`
|
||||||
|
LdapID *string `json:"ldapId"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type UserCreateDto struct {
|
type UserCreateDto struct {
|
||||||
@@ -18,9 +19,15 @@ type UserCreateDto struct {
|
|||||||
FirstName string `json:"firstName" binding:"required,min=1,max=50"`
|
FirstName string `json:"firstName" binding:"required,min=1,max=50"`
|
||||||
LastName string `json:"lastName" binding:"required,min=1,max=50"`
|
LastName string `json:"lastName" binding:"required,min=1,max=50"`
|
||||||
IsAdmin bool `json:"isAdmin"`
|
IsAdmin bool `json:"isAdmin"`
|
||||||
|
LdapID string `json:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type OneTimeAccessTokenCreateDto struct {
|
type OneTimeAccessTokenCreateDto struct {
|
||||||
UserID string `json:"userId" binding:"required"`
|
UserID string `json:"userId" binding:"required"`
|
||||||
ExpiresAt time.Time `json:"expiresAt" binding:"required"`
|
ExpiresAt time.Time `json:"expiresAt" binding:"required"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type OneTimeAccessEmailDto struct {
|
||||||
|
Email string `json:"email" binding:"required,email"`
|
||||||
|
RedirectPath string `json:"redirectPath"`
|
||||||
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ type UserGroupDtoWithUsers struct {
|
|||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
CustomClaims []CustomClaimDto `json:"customClaims"`
|
CustomClaims []CustomClaimDto `json:"customClaims"`
|
||||||
Users []UserDto `json:"users"`
|
Users []UserDto `json:"users"`
|
||||||
|
LdapID *string `json:"ldapId"`
|
||||||
CreatedAt datatype.DateTime `json:"createdAt"`
|
CreatedAt datatype.DateTime `json:"createdAt"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -19,12 +20,14 @@ type UserGroupDtoWithUserCount struct {
|
|||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
CustomClaims []CustomClaimDto `json:"customClaims"`
|
CustomClaims []CustomClaimDto `json:"customClaims"`
|
||||||
UserCount int64 `json:"userCount"`
|
UserCount int64 `json:"userCount"`
|
||||||
|
LdapID *string `json:"ldapId"`
|
||||||
CreatedAt datatype.DateTime `json:"createdAt"`
|
CreatedAt datatype.DateTime `json:"createdAt"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type UserGroupCreateDto struct {
|
type UserGroupCreateDto struct {
|
||||||
FriendlyName string `json:"friendlyName" binding:"required,min=2,max=50"`
|
FriendlyName string `json:"friendlyName" binding:"required,min=2,max=50"`
|
||||||
Name string `json:"name" binding:"required,min=2,max=255"`
|
Name string `json:"name" binding:"required,min=2,max=255"`
|
||||||
|
LdapID string `json:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type UserGroupUpdateUsersDto struct {
|
type UserGroupUpdateUsersDto struct {
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
func RegisterJobs(db *gorm.DB) {
|
func RegisterDbCleanupJobs(db *gorm.DB) {
|
||||||
scheduler, err := gocron.NewScheduler()
|
scheduler, err := gocron.NewScheduler()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("Failed to create a new scheduler: %s", err)
|
log.Fatalf("Failed to create a new scheduler: %s", err)
|
||||||
|
|||||||
39
backend/internal/job/ldap_job.go
Normal file
39
backend/internal/job/ldap_job.go
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
package job
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"github.com/go-co-op/gocron/v2"
|
||||||
|
"github.com/stonith404/pocket-id/backend/internal/service"
|
||||||
|
)
|
||||||
|
|
||||||
|
type LdapJobs struct {
|
||||||
|
ldapService *service.LdapService
|
||||||
|
appConfigService *service.AppConfigService
|
||||||
|
}
|
||||||
|
|
||||||
|
func RegisterLdapJobs(ldapService *service.LdapService, appConfigService *service.AppConfigService) {
|
||||||
|
jobs := &LdapJobs{ldapService: ldapService, appConfigService: appConfigService}
|
||||||
|
|
||||||
|
scheduler, err := gocron.NewScheduler()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to create a new scheduler: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register the job to run every hour
|
||||||
|
registerJob(scheduler, "SyncLdap", "0 * * * *", jobs.syncLdap)
|
||||||
|
|
||||||
|
// Run the job immediately on startup
|
||||||
|
if err := jobs.syncLdap(); err != nil {
|
||||||
|
log.Printf("Failed to sync LDAP: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
scheduler.Start()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (j *LdapJobs) syncLdap() error {
|
||||||
|
if j.appConfigService.DbConfig.LdapEnabled.Value == "true" {
|
||||||
|
return j.ldapService.SyncAll()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -16,8 +16,12 @@ func NewRateLimitMiddleware() *RateLimitMiddleware {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m *RateLimitMiddleware) Add(limit rate.Limit, burst int) gin.HandlerFunc {
|
func (m *RateLimitMiddleware) Add(limit rate.Limit, burst int) gin.HandlerFunc {
|
||||||
|
// Map to store the rate limiters per IP
|
||||||
|
var clients = make(map[string]*client)
|
||||||
|
var mu sync.Mutex
|
||||||
|
|
||||||
// Start the cleanup routine
|
// Start the cleanup routine
|
||||||
go cleanupClients()
|
go cleanupClients(&mu, clients)
|
||||||
|
|
||||||
return func(c *gin.Context) {
|
return func(c *gin.Context) {
|
||||||
ip := c.ClientIP()
|
ip := c.ClientIP()
|
||||||
@@ -29,7 +33,7 @@ func (m *RateLimitMiddleware) Add(limit rate.Limit, burst int) gin.HandlerFunc {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
limiter := getLimiter(ip, limit, burst)
|
limiter := getLimiter(ip, limit, burst, &mu, clients)
|
||||||
if !limiter.Allow() {
|
if !limiter.Allow() {
|
||||||
c.Error(&common.TooManyRequestsError{})
|
c.Error(&common.TooManyRequestsError{})
|
||||||
c.Abort()
|
c.Abort()
|
||||||
@@ -45,12 +49,8 @@ type client struct {
|
|||||||
lastSeen time.Time
|
lastSeen time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
// Map to store the rate limiters per IP
|
|
||||||
var clients = make(map[string]*client)
|
|
||||||
var mu sync.Mutex
|
|
||||||
|
|
||||||
// Cleanup routine to remove stale clients that haven't been seen for a while
|
// Cleanup routine to remove stale clients that haven't been seen for a while
|
||||||
func cleanupClients() {
|
func cleanupClients(mu *sync.Mutex, clients map[string]*client) {
|
||||||
for {
|
for {
|
||||||
time.Sleep(time.Minute)
|
time.Sleep(time.Minute)
|
||||||
mu.Lock()
|
mu.Lock()
|
||||||
@@ -64,7 +64,7 @@ func cleanupClients() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// getLimiter retrieves the rate limiter for a given IP address, creating one if it doesn't exist
|
// getLimiter retrieves the rate limiter for a given IP address, creating one if it doesn't exist
|
||||||
func getLimiter(ip string, limit rate.Limit, burst int) *rate.Limiter {
|
func getLimiter(ip string, limit rate.Limit, burst int, mu *sync.Mutex, clients map[string]*client) *rate.Limiter {
|
||||||
mu.Lock()
|
mu.Lock()
|
||||||
defer mu.Unlock()
|
defer mu.Unlock()
|
||||||
|
|
||||||
|
|||||||
@@ -10,16 +10,16 @@ type AppConfigVariable struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type AppConfig struct {
|
type AppConfig struct {
|
||||||
|
// General
|
||||||
AppName AppConfigVariable
|
AppName AppConfigVariable
|
||||||
SessionDuration AppConfigVariable
|
SessionDuration AppConfigVariable
|
||||||
EmailsVerified AppConfigVariable
|
EmailsVerified AppConfigVariable
|
||||||
AllowOwnAccountEdit AppConfigVariable
|
AllowOwnAccountEdit AppConfigVariable
|
||||||
|
// Internal
|
||||||
BackgroundImageType AppConfigVariable
|
BackgroundImageType AppConfigVariable
|
||||||
LogoLightImageType AppConfigVariable
|
LogoLightImageType AppConfigVariable
|
||||||
LogoDarkImageType AppConfigVariable
|
LogoDarkImageType AppConfigVariable
|
||||||
|
// Email
|
||||||
EmailEnabled AppConfigVariable
|
|
||||||
SmtpHost AppConfigVariable
|
SmtpHost AppConfigVariable
|
||||||
SmtpPort AppConfigVariable
|
SmtpPort AppConfigVariable
|
||||||
SmtpFrom AppConfigVariable
|
SmtpFrom AppConfigVariable
|
||||||
@@ -27,4 +27,21 @@ type AppConfig struct {
|
|||||||
SmtpPassword AppConfigVariable
|
SmtpPassword AppConfigVariable
|
||||||
SmtpTls AppConfigVariable
|
SmtpTls AppConfigVariable
|
||||||
SmtpSkipCertVerify AppConfigVariable
|
SmtpSkipCertVerify AppConfigVariable
|
||||||
|
EmailLoginNotificationEnabled AppConfigVariable
|
||||||
|
EmailOneTimeAccessEnabled AppConfigVariable
|
||||||
|
// LDAP
|
||||||
|
LdapEnabled AppConfigVariable
|
||||||
|
LdapUrl AppConfigVariable
|
||||||
|
LdapBindDn AppConfigVariable
|
||||||
|
LdapBindPassword AppConfigVariable
|
||||||
|
LdapBase AppConfigVariable
|
||||||
|
LdapSkipCertVerify AppConfigVariable
|
||||||
|
LdapAttributeUserUniqueIdentifier AppConfigVariable
|
||||||
|
LdapAttributeUserUsername AppConfigVariable
|
||||||
|
LdapAttributeUserEmail AppConfigVariable
|
||||||
|
LdapAttributeUserFirstName AppConfigVariable
|
||||||
|
LdapAttributeUserLastName AppConfigVariable
|
||||||
|
LdapAttributeGroupUniqueIdentifier AppConfigVariable
|
||||||
|
LdapAttributeGroupName AppConfigVariable
|
||||||
|
LdapAttributeAdminGroup AppConfigVariable
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ type User struct {
|
|||||||
FirstName string `sortable:"true"`
|
FirstName string `sortable:"true"`
|
||||||
LastName string `sortable:"true"`
|
LastName string `sortable:"true"`
|
||||||
IsAdmin bool `sortable:"true"`
|
IsAdmin bool `sortable:"true"`
|
||||||
|
LdapID *string
|
||||||
|
|
||||||
CustomClaims []CustomClaim
|
CustomClaims []CustomClaim
|
||||||
UserGroups []UserGroup `gorm:"many2many:user_groups_users;"`
|
UserGroups []UserGroup `gorm:"many2many:user_groups_users;"`
|
||||||
|
|||||||
@@ -3,7 +3,8 @@ package model
|
|||||||
type UserGroup struct {
|
type UserGroup struct {
|
||||||
Base
|
Base
|
||||||
FriendlyName string `sortable:"true"`
|
FriendlyName string `sortable:"true"`
|
||||||
Name string `gorm:"unique" sortable:"true"`
|
Name string `sortable:"true"`
|
||||||
|
LdapID *string
|
||||||
Users []User `gorm:"many2many:user_groups_users;"`
|
Users []User `gorm:"many2many:user_groups_users;"`
|
||||||
CustomClaims []CustomClaim
|
CustomClaims []CustomClaim
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,15 +2,16 @@ package service
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"mime/multipart"
|
||||||
|
"os"
|
||||||
|
"reflect"
|
||||||
|
|
||||||
"github.com/stonith404/pocket-id/backend/internal/common"
|
"github.com/stonith404/pocket-id/backend/internal/common"
|
||||||
"github.com/stonith404/pocket-id/backend/internal/dto"
|
"github.com/stonith404/pocket-id/backend/internal/dto"
|
||||||
"github.com/stonith404/pocket-id/backend/internal/model"
|
"github.com/stonith404/pocket-id/backend/internal/model"
|
||||||
"github.com/stonith404/pocket-id/backend/internal/utils"
|
"github.com/stonith404/pocket-id/backend/internal/utils"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
"log"
|
|
||||||
"mime/multipart"
|
|
||||||
"os"
|
|
||||||
"reflect"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type AppConfigService struct {
|
type AppConfigService struct {
|
||||||
@@ -30,6 +31,7 @@ func NewAppConfigService(db *gorm.DB) *AppConfigService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var defaultDbConfig = model.AppConfig{
|
var defaultDbConfig = model.AppConfig{
|
||||||
|
// General
|
||||||
AppName: model.AppConfigVariable{
|
AppName: model.AppConfigVariable{
|
||||||
Key: "appName",
|
Key: "appName",
|
||||||
Type: "string",
|
Type: "string",
|
||||||
@@ -52,6 +54,7 @@ var defaultDbConfig = model.AppConfig{
|
|||||||
IsPublic: true,
|
IsPublic: true,
|
||||||
DefaultValue: "true",
|
DefaultValue: "true",
|
||||||
},
|
},
|
||||||
|
// Internal
|
||||||
BackgroundImageType: model.AppConfigVariable{
|
BackgroundImageType: model.AppConfigVariable{
|
||||||
Key: "backgroundImageType",
|
Key: "backgroundImageType",
|
||||||
Type: "string",
|
Type: "string",
|
||||||
@@ -70,11 +73,7 @@ var defaultDbConfig = model.AppConfig{
|
|||||||
IsInternal: true,
|
IsInternal: true,
|
||||||
DefaultValue: "svg",
|
DefaultValue: "svg",
|
||||||
},
|
},
|
||||||
EmailEnabled: model.AppConfigVariable{
|
// Email
|
||||||
Key: "emailEnabled",
|
|
||||||
Type: "bool",
|
|
||||||
DefaultValue: "false",
|
|
||||||
},
|
|
||||||
SmtpHost: model.AppConfigVariable{
|
SmtpHost: model.AppConfigVariable{
|
||||||
Key: "smtpHost",
|
Key: "smtpHost",
|
||||||
Type: "string",
|
Type: "string",
|
||||||
@@ -105,6 +104,76 @@ var defaultDbConfig = model.AppConfig{
|
|||||||
Type: "bool",
|
Type: "bool",
|
||||||
DefaultValue: "false",
|
DefaultValue: "false",
|
||||||
},
|
},
|
||||||
|
EmailLoginNotificationEnabled: model.AppConfigVariable{
|
||||||
|
Key: "emailLoginNotificationEnabled",
|
||||||
|
Type: "bool",
|
||||||
|
DefaultValue: "false",
|
||||||
|
},
|
||||||
|
EmailOneTimeAccessEnabled: model.AppConfigVariable{
|
||||||
|
Key: "emailOneTimeAccessEnabled",
|
||||||
|
Type: "bool",
|
||||||
|
IsPublic: true,
|
||||||
|
DefaultValue: "false",
|
||||||
|
},
|
||||||
|
// LDAP
|
||||||
|
LdapEnabled: model.AppConfigVariable{
|
||||||
|
Key: "ldapEnabled",
|
||||||
|
Type: "bool",
|
||||||
|
DefaultValue: "false",
|
||||||
|
},
|
||||||
|
LdapUrl: model.AppConfigVariable{
|
||||||
|
Key: "ldapUrl",
|
||||||
|
Type: "string",
|
||||||
|
},
|
||||||
|
LdapBindDn: model.AppConfigVariable{
|
||||||
|
Key: "ldapBindDn",
|
||||||
|
Type: "string",
|
||||||
|
},
|
||||||
|
LdapBindPassword: model.AppConfigVariable{
|
||||||
|
Key: "ldapBindPassword",
|
||||||
|
Type: "string",
|
||||||
|
},
|
||||||
|
LdapBase: model.AppConfigVariable{
|
||||||
|
Key: "ldapBase",
|
||||||
|
Type: "string",
|
||||||
|
},
|
||||||
|
LdapSkipCertVerify: model.AppConfigVariable{
|
||||||
|
Key: "ldapSkipCertVerify",
|
||||||
|
Type: "bool",
|
||||||
|
DefaultValue: "false",
|
||||||
|
},
|
||||||
|
LdapAttributeUserUniqueIdentifier: model.AppConfigVariable{
|
||||||
|
Key: "ldapAttributeUserUniqueIdentifier",
|
||||||
|
Type: "string",
|
||||||
|
},
|
||||||
|
LdapAttributeUserUsername: model.AppConfigVariable{
|
||||||
|
Key: "ldapAttributeUserUsername",
|
||||||
|
Type: "string",
|
||||||
|
},
|
||||||
|
LdapAttributeUserEmail: model.AppConfigVariable{
|
||||||
|
Key: "ldapAttributeUserEmail",
|
||||||
|
Type: "string",
|
||||||
|
},
|
||||||
|
LdapAttributeUserFirstName: model.AppConfigVariable{
|
||||||
|
Key: "ldapAttributeUserFirstName",
|
||||||
|
Type: "string",
|
||||||
|
},
|
||||||
|
LdapAttributeUserLastName: model.AppConfigVariable{
|
||||||
|
Key: "ldapAttributeUserLastName",
|
||||||
|
Type: "string",
|
||||||
|
},
|
||||||
|
LdapAttributeGroupUniqueIdentifier: model.AppConfigVariable{
|
||||||
|
Key: "ldapAttributeGroupUniqueIdentifier",
|
||||||
|
Type: "string",
|
||||||
|
},
|
||||||
|
LdapAttributeGroupName: model.AppConfigVariable{
|
||||||
|
Key: "ldapAttributeGroupName",
|
||||||
|
Type: "string",
|
||||||
|
},
|
||||||
|
LdapAttributeAdminGroup: model.AppConfigVariable{
|
||||||
|
Key: "ldapAttributeAdminGroup",
|
||||||
|
Type: "string",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *AppConfigService) UpdateAppConfig(input dto.AppConfigUpdateDto) ([]model.AppConfigVariable, error) {
|
func (s *AppConfigService) UpdateAppConfig(input dto.AppConfigUpdateDto) ([]model.AppConfigVariable, error) {
|
||||||
@@ -119,6 +188,13 @@ func (s *AppConfigService) UpdateAppConfig(input dto.AppConfigUpdateDto) ([]mode
|
|||||||
key := field.Tag.Get("json")
|
key := field.Tag.Get("json")
|
||||||
value := rv.FieldByName(field.Name).String()
|
value := rv.FieldByName(field.Name).String()
|
||||||
|
|
||||||
|
// If the emailEnabled is set to false, disable the emailOneTimeAccessEnabled
|
||||||
|
if key == s.DbConfig.EmailOneTimeAccessEnabled.Key {
|
||||||
|
if rv.FieldByName("EmailEnabled").String() == "false" {
|
||||||
|
value = "false"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var appConfigVariable model.AppConfigVariable
|
var appConfigVariable model.AppConfigVariable
|
||||||
if err := tx.First(&appConfigVariable, "key = ? AND is_internal = false", key).Error; err != nil {
|
if err := tx.First(&appConfigVariable, "key = ? AND is_internal = false", key).Error; err != nil {
|
||||||
tx.Rollback()
|
tx.Rollback()
|
||||||
|
|||||||
@@ -58,8 +58,8 @@ func (s *AuditLogService) CreateNewSignInWithEmail(ipAddress, userAgent, userID
|
|||||||
return createdAuditLog
|
return createdAuditLog
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the user hasn't logged in from the same device before, send an email
|
// If the user hasn't logged in from the same device before and email notifications are enabled, send an email
|
||||||
if count <= 1 {
|
if s.appConfigService.DbConfig.EmailLoginNotificationEnabled.Value == "true" && count <= 1 {
|
||||||
go func() {
|
go func() {
|
||||||
var user model.User
|
var user model.User
|
||||||
s.db.Where("id = ?", userID).First(&user)
|
s.db.Where("id = ?", userID).First(&user)
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package service
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/stonith404/pocket-id/backend/internal/common"
|
"github.com/stonith404/pocket-id/backend/internal/common"
|
||||||
"github.com/stonith404/pocket-id/backend/internal/model"
|
"github.com/stonith404/pocket-id/backend/internal/model"
|
||||||
@@ -16,8 +15,13 @@ import (
|
|||||||
"net/smtp"
|
"net/smtp"
|
||||||
"net/textproto"
|
"net/textproto"
|
||||||
ttemplate "text/template"
|
ttemplate "text/template"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var netDialer = &net.Dialer{
|
||||||
|
Timeout: 3 * time.Second,
|
||||||
|
}
|
||||||
|
|
||||||
type EmailService struct {
|
type EmailService struct {
|
||||||
appConfigService *AppConfigService
|
appConfigService *AppConfigService
|
||||||
db *gorm.DB
|
db *gorm.DB
|
||||||
@@ -58,11 +62,6 @@ func (srv *EmailService) SendTestEmail(recipientUserId string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func SendEmail[V any](srv *EmailService, toEmail email.Address, template email.Template[V], tData *V) error {
|
func SendEmail[V any](srv *EmailService, toEmail email.Address, template email.Template[V], tData *V) error {
|
||||||
// Check if SMTP settings are set
|
|
||||||
if srv.appConfigService.DbConfig.EmailEnabled.Value != "true" {
|
|
||||||
return errors.New("email not enabled")
|
|
||||||
}
|
|
||||||
|
|
||||||
data := &email.TemplateData[V]{
|
data := &email.TemplateData[V]{
|
||||||
AppName: srv.appConfigService.DbConfig.AppName.Value,
|
AppName: srv.appConfigService.DbConfig.AppName.Value,
|
||||||
LogoURL: common.EnvConfig.AppURL + "/api/application-configuration/logo",
|
LogoURL: common.EnvConfig.AppURL + "/api/application-configuration/logo",
|
||||||
@@ -112,11 +111,13 @@ func SendEmail[V any](srv *EmailService, toEmail email.Address, template email.T
|
|||||||
tlsConfig,
|
tlsConfig,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
defer client.Quit()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to connect to SMTP server: %w", err)
|
return fmt.Errorf("failed to connect to SMTP server: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
defer client.Close()
|
||||||
|
|
||||||
smtpUser := srv.appConfigService.DbConfig.SmtpUser.Value
|
smtpUser := srv.appConfigService.DbConfig.SmtpUser.Value
|
||||||
smtpPassword := srv.appConfigService.DbConfig.SmtpPassword.Value
|
smtpPassword := srv.appConfigService.DbConfig.SmtpPassword.Value
|
||||||
|
|
||||||
@@ -141,7 +142,11 @@ func SendEmail[V any](srv *EmailService, toEmail email.Address, template email.T
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (srv *EmailService) connectToSmtpServerUsingImplicitTLS(serverAddr string, tlsConfig *tls.Config) (*smtp.Client, error) {
|
func (srv *EmailService) connectToSmtpServerUsingImplicitTLS(serverAddr string, tlsConfig *tls.Config) (*smtp.Client, error) {
|
||||||
conn, err := tls.Dial("tcp", serverAddr, tlsConfig)
|
tlsDialer := &tls.Dialer{
|
||||||
|
NetDialer: netDialer,
|
||||||
|
Config: tlsConfig,
|
||||||
|
}
|
||||||
|
conn, err := tlsDialer.Dial("tcp", serverAddr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to connect to SMTP server: %w", err)
|
return nil, fmt.Errorf("failed to connect to SMTP server: %w", err)
|
||||||
}
|
}
|
||||||
@@ -156,7 +161,7 @@ func (srv *EmailService) connectToSmtpServerUsingImplicitTLS(serverAddr string,
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (srv *EmailService) connectToSmtpServerUsingStartTLS(serverAddr string, tlsConfig *tls.Config) (*smtp.Client, error) {
|
func (srv *EmailService) connectToSmtpServerUsingStartTLS(serverAddr string, tlsConfig *tls.Config) (*smtp.Client, error) {
|
||||||
conn, err := net.Dial("tcp", serverAddr)
|
conn, err := netDialer.Dial("tcp", serverAddr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to connect to SMTP server: %w", err)
|
return nil, fmt.Errorf("failed to connect to SMTP server: %w", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import (
|
|||||||
/**
|
/**
|
||||||
How to add new template:
|
How to add new template:
|
||||||
- pick unique and descriptive template ${name} (for example "login-with-new-device")
|
- pick unique and descriptive template ${name} (for example "login-with-new-device")
|
||||||
- in backend/email-templates/ create "${name}_html.tmpl" and "${name}_text.tmpl"
|
- in backend/resources/email-templates/ create "${name}_html.tmpl" and "${name}_text.tmpl"
|
||||||
- create xxxxTemplate and xxxxTemplateData (for example NewLoginTemplate and NewLoginTemplateData)
|
- create xxxxTemplate and xxxxTemplateData (for example NewLoginTemplate and NewLoginTemplateData)
|
||||||
- Path *must* be ${name}
|
- Path *must* be ${name}
|
||||||
- add xxxTemplate.Path to "emailTemplatePaths" at the end
|
- add xxxTemplate.Path to "emailTemplatePaths" at the end
|
||||||
@@ -27,6 +27,13 @@ var NewLoginTemplate = email.Template[NewLoginTemplateData]{
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var OneTimeAccessTemplate = email.Template[OneTimeAccessTemplateData]{
|
||||||
|
Path: "one-time-access",
|
||||||
|
Title: func(data *email.TemplateData[OneTimeAccessTemplateData]) string {
|
||||||
|
return "One time access"
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
var TestTemplate = email.Template[struct{}]{
|
var TestTemplate = email.Template[struct{}]{
|
||||||
Path: "test",
|
Path: "test",
|
||||||
Title: func(data *email.TemplateData[struct{}]) string {
|
Title: func(data *email.TemplateData[struct{}]) string {
|
||||||
@@ -42,5 +49,9 @@ type NewLoginTemplateData struct {
|
|||||||
DateTime time.Time
|
DateTime time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type OneTimeAccessTemplateData = struct {
|
||||||
|
Link string
|
||||||
|
}
|
||||||
|
|
||||||
// this is list of all template paths used for preloading templates
|
// this is list of all template paths used for preloading templates
|
||||||
var emailTemplatesPaths = []string{NewLoginTemplate.Path, TestTemplate.Path}
|
var emailTemplatesPaths = []string{NewLoginTemplate.Path, OneTimeAccessTemplate.Path, TestTemplate.Path}
|
||||||
|
|||||||
261
backend/internal/service/ldap_service.go
Normal file
261
backend/internal/service/ldap_service.go
Normal file
@@ -0,0 +1,261 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/tls"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/go-ldap/ldap/v3"
|
||||||
|
"github.com/stonith404/pocket-id/backend/internal/dto"
|
||||||
|
"github.com/stonith404/pocket-id/backend/internal/model"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type LdapService struct {
|
||||||
|
db *gorm.DB
|
||||||
|
appConfigService *AppConfigService
|
||||||
|
userService *UserService
|
||||||
|
groupService *UserGroupService
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewLdapService(db *gorm.DB, appConfigService *AppConfigService, userService *UserService, groupService *UserGroupService) *LdapService {
|
||||||
|
return &LdapService{db: db, appConfigService: appConfigService, userService: userService, groupService: groupService}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *LdapService) createClient() (*ldap.Conn, error) {
|
||||||
|
if s.appConfigService.DbConfig.LdapEnabled.Value != "true" {
|
||||||
|
return nil, fmt.Errorf("LDAP is not enabled")
|
||||||
|
}
|
||||||
|
// Setup LDAP connection
|
||||||
|
ldapURL := s.appConfigService.DbConfig.LdapUrl.Value
|
||||||
|
skipTLSVerify := s.appConfigService.DbConfig.LdapSkipCertVerify.Value == "true"
|
||||||
|
client, err := ldap.DialURL(ldapURL, ldap.DialWithTLSConfig(&tls.Config{InsecureSkipVerify: skipTLSVerify}))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to connect to LDAP: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bind as service account
|
||||||
|
bindDn := s.appConfigService.DbConfig.LdapBindDn.Value
|
||||||
|
bindPassword := s.appConfigService.DbConfig.LdapBindPassword.Value
|
||||||
|
err = client.Bind(bindDn, bindPassword)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to bind to LDAP: %w", err)
|
||||||
|
}
|
||||||
|
return client, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *LdapService) SyncAll() error {
|
||||||
|
err := s.SyncUsers()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to sync users: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = s.SyncGroups()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to sync groups: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *LdapService) SyncGroups() error {
|
||||||
|
// Setup LDAP connection
|
||||||
|
client, err := s.createClient()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create LDAP client: %w", err)
|
||||||
|
}
|
||||||
|
defer client.Close()
|
||||||
|
|
||||||
|
baseDN := s.appConfigService.DbConfig.LdapBase.Value
|
||||||
|
nameAttribute := s.appConfigService.DbConfig.LdapAttributeGroupName.Value
|
||||||
|
uniqueIdentifierAttribute := s.appConfigService.DbConfig.LdapAttributeGroupUniqueIdentifier.Value
|
||||||
|
filter := "(objectClass=groupOfUniqueNames)"
|
||||||
|
|
||||||
|
searchAttrs := []string{
|
||||||
|
nameAttribute,
|
||||||
|
uniqueIdentifierAttribute,
|
||||||
|
"member",
|
||||||
|
}
|
||||||
|
|
||||||
|
searchReq := ldap.NewSearchRequest(baseDN, ldap.ScopeWholeSubtree, 0, 0, 0, false, filter, searchAttrs, []ldap.Control{})
|
||||||
|
result, err := client.Search(searchReq)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to query LDAP: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a mapping for groups that exist
|
||||||
|
ldapGroupIDs := make(map[string]bool)
|
||||||
|
|
||||||
|
for _, value := range result.Entries {
|
||||||
|
var usersToAddDto dto.UserGroupUpdateUsersDto
|
||||||
|
var membersUserId []string
|
||||||
|
|
||||||
|
ldapId := value.GetAttributeValue(uniqueIdentifierAttribute)
|
||||||
|
ldapGroupIDs[ldapId] = true
|
||||||
|
|
||||||
|
// Try to find the group in the database
|
||||||
|
var databaseGroup model.UserGroup
|
||||||
|
s.db.Where("ldap_id = ?", ldapId).First(&databaseGroup)
|
||||||
|
|
||||||
|
// Get group members and add to the correct Group
|
||||||
|
groupMembers := value.GetAttributeValues("member")
|
||||||
|
for _, member := range groupMembers {
|
||||||
|
// Normal output of this would be CN=username,ou=people,dc=example,dc=com
|
||||||
|
// Splitting at the "=" and "," then just grabbing the username for that string
|
||||||
|
singleMember := strings.Split(strings.Split(member, "=")[1], ",")[0]
|
||||||
|
|
||||||
|
var databaseUser model.User
|
||||||
|
s.db.Where("username = ?", singleMember).First(&databaseUser)
|
||||||
|
membersUserId = append(membersUserId, databaseUser.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
syncGroup := dto.UserGroupCreateDto{
|
||||||
|
Name: value.GetAttributeValue(nameAttribute),
|
||||||
|
FriendlyName: value.GetAttributeValue(nameAttribute),
|
||||||
|
LdapID: value.GetAttributeValue(uniqueIdentifierAttribute),
|
||||||
|
}
|
||||||
|
|
||||||
|
usersToAddDto = dto.UserGroupUpdateUsersDto{
|
||||||
|
UserIDs: membersUserId,
|
||||||
|
}
|
||||||
|
|
||||||
|
if databaseGroup.ID == "" {
|
||||||
|
newGroup, err := s.groupService.Create(syncGroup)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Error syncing group %s: %s", syncGroup.Name, err)
|
||||||
|
} else {
|
||||||
|
if _, err = s.groupService.UpdateUsers(newGroup.ID, usersToAddDto); err != nil {
|
||||||
|
log.Printf("Error syncing group %s: %s", syncGroup.Name, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
_, err = s.groupService.Update(databaseGroup.ID, syncGroup, true)
|
||||||
|
_, err = s.groupService.UpdateUsers(databaseGroup.ID, usersToAddDto)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Error syncing group %s: %s", syncGroup.Name, err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get all LDAP groups from the database
|
||||||
|
var ldapGroupsInDb []model.UserGroup
|
||||||
|
if err := s.db.Find(&ldapGroupsInDb, "ldap_id IS NOT NULL").Select("ldap_id").Error; err != nil {
|
||||||
|
fmt.Println(fmt.Errorf("failed to fetch groups from database: %v", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete groups that no longer exist in LDAP
|
||||||
|
for _, group := range ldapGroupsInDb {
|
||||||
|
if _, exists := ldapGroupIDs[*group.LdapID]; !exists {
|
||||||
|
if err := s.db.Delete(&model.UserGroup{}, "ldap_id = ?", group.LdapID).Error; err != nil {
|
||||||
|
log.Printf("Failed to delete group %s with: %v", group.Name, err)
|
||||||
|
} else {
|
||||||
|
log.Printf("Deleted group %s", group.Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *LdapService) SyncUsers() error {
|
||||||
|
// Setup LDAP connection
|
||||||
|
client, err := s.createClient()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create LDAP client: %w", err)
|
||||||
|
}
|
||||||
|
defer client.Close()
|
||||||
|
|
||||||
|
baseDN := s.appConfigService.DbConfig.LdapBase.Value
|
||||||
|
uniqueIdentifierAttribute := s.appConfigService.DbConfig.LdapAttributeUserUniqueIdentifier.Value
|
||||||
|
usernameAttribute := s.appConfigService.DbConfig.LdapAttributeUserUsername.Value
|
||||||
|
emailAttribute := s.appConfigService.DbConfig.LdapAttributeUserEmail.Value
|
||||||
|
firstNameAttribute := s.appConfigService.DbConfig.LdapAttributeUserFirstName.Value
|
||||||
|
lastNameAttribute := s.appConfigService.DbConfig.LdapAttributeUserLastName.Value
|
||||||
|
adminGroupAttribute := s.appConfigService.DbConfig.LdapAttributeAdminGroup.Value
|
||||||
|
|
||||||
|
filter := "(objectClass=person)"
|
||||||
|
|
||||||
|
searchAttrs := []string{
|
||||||
|
"memberOf",
|
||||||
|
"sn",
|
||||||
|
"cn",
|
||||||
|
uniqueIdentifierAttribute,
|
||||||
|
usernameAttribute,
|
||||||
|
emailAttribute,
|
||||||
|
firstNameAttribute,
|
||||||
|
lastNameAttribute,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filters must start and finish with ()!
|
||||||
|
searchReq := ldap.NewSearchRequest(baseDN, ldap.ScopeWholeSubtree, 0, 0, 0, false, filter, searchAttrs, []ldap.Control{})
|
||||||
|
|
||||||
|
result, err := client.Search(searchReq)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(fmt.Errorf("failed to query LDAP: %w", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a mapping for users that exist
|
||||||
|
ldapUserIDs := make(map[string]bool)
|
||||||
|
|
||||||
|
for _, value := range result.Entries {
|
||||||
|
ldapId := value.GetAttributeValue(uniqueIdentifierAttribute)
|
||||||
|
ldapUserIDs[ldapId] = true
|
||||||
|
|
||||||
|
// Get the user from the database
|
||||||
|
var databaseUser model.User
|
||||||
|
s.db.Where("ldap_id = ?", ldapId).First(&databaseUser)
|
||||||
|
|
||||||
|
// Check if user is admin by checking if they are in the admin group
|
||||||
|
isAdmin := false
|
||||||
|
for _, group := range value.GetAttributeValues("memberOf") {
|
||||||
|
if strings.Contains(group, adminGroupAttribute) {
|
||||||
|
isAdmin = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
newUser := dto.UserCreateDto{
|
||||||
|
Username: value.GetAttributeValue(usernameAttribute),
|
||||||
|
Email: value.GetAttributeValue(emailAttribute),
|
||||||
|
FirstName: value.GetAttributeValue(firstNameAttribute),
|
||||||
|
LastName: value.GetAttributeValue(lastNameAttribute),
|
||||||
|
IsAdmin: isAdmin,
|
||||||
|
LdapID: ldapId,
|
||||||
|
}
|
||||||
|
|
||||||
|
if databaseUser.ID == "" {
|
||||||
|
_, err = s.userService.CreateUser(newUser)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Error syncing user %s: %s", newUser.Username, err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
_, err = s.userService.UpdateUser(databaseUser.ID, newUser, false, true)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Error syncing user %s: %s", newUser.Username, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get all LDAP users from the database
|
||||||
|
var ldapUsersInDb []model.User
|
||||||
|
if err := s.db.Find(&ldapUsersInDb, "ldap_id IS NOT NULL").Select("ldap_id").Error; err != nil {
|
||||||
|
fmt.Println(fmt.Errorf("failed to fetch users from database: %v", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete users that no longer exist in LDAP
|
||||||
|
for _, user := range ldapUsersInDb {
|
||||||
|
if _, exists := ldapUserIDs[*user.LdapID]; !exists {
|
||||||
|
if err := s.db.Delete(&model.User{}, "ldap_id = ?", user.LdapID).Error; err != nil {
|
||||||
|
log.Printf("Failed to delete user %s with: %v", user.Username, err)
|
||||||
|
} else {
|
||||||
|
log.Printf("Deleted user %s", user.Username)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -51,6 +51,10 @@ func (s *UserGroupService) Delete(id string) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if group.LdapID != nil {
|
||||||
|
return &common.LdapUserGroupUpdateError{}
|
||||||
|
}
|
||||||
|
|
||||||
return s.db.Delete(&group).Error
|
return s.db.Delete(&group).Error
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -58,6 +62,7 @@ func (s *UserGroupService) Create(input dto.UserGroupCreateDto) (group model.Use
|
|||||||
group = model.UserGroup{
|
group = model.UserGroup{
|
||||||
FriendlyName: input.FriendlyName,
|
FriendlyName: input.FriendlyName,
|
||||||
Name: input.Name,
|
Name: input.Name,
|
||||||
|
LdapID: &input.LdapID,
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := s.db.Preload("Users").Create(&group).Error; err != nil {
|
if err := s.db.Preload("Users").Create(&group).Error; err != nil {
|
||||||
@@ -69,14 +74,19 @@ func (s *UserGroupService) Create(input dto.UserGroupCreateDto) (group model.Use
|
|||||||
return group, nil
|
return group, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *UserGroupService) Update(id string, input dto.UserGroupCreateDto) (group model.UserGroup, err error) {
|
func (s *UserGroupService) Update(id string, input dto.UserGroupCreateDto, allowLdapUpdate bool) (group model.UserGroup, err error) {
|
||||||
group, err = s.Get(id)
|
group, err = s.Get(id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return model.UserGroup{}, err
|
return model.UserGroup{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if group.LdapID != nil && !allowLdapUpdate {
|
||||||
|
return model.UserGroup{}, &common.LdapUserGroupUpdateError{}
|
||||||
|
}
|
||||||
|
|
||||||
group.Name = input.Name
|
group.Name = input.Name
|
||||||
group.FriendlyName = input.FriendlyName
|
group.FriendlyName = input.FriendlyName
|
||||||
|
group.LdapID = &input.LdapID
|
||||||
|
|
||||||
if err := s.db.Preload("Users").Save(&group).Error; err != nil {
|
if err := s.db.Preload("Users").Save(&group).Error; err != nil {
|
||||||
if errors.Is(err, gorm.ErrDuplicatedKey) {
|
if errors.Is(err, gorm.ErrDuplicatedKey) {
|
||||||
|
|||||||
@@ -2,12 +2,17 @@ package service
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"github.com/stonith404/pocket-id/backend/internal/common"
|
"github.com/stonith404/pocket-id/backend/internal/common"
|
||||||
"github.com/stonith404/pocket-id/backend/internal/dto"
|
"github.com/stonith404/pocket-id/backend/internal/dto"
|
||||||
"github.com/stonith404/pocket-id/backend/internal/model"
|
"github.com/stonith404/pocket-id/backend/internal/model"
|
||||||
"github.com/stonith404/pocket-id/backend/internal/model/types"
|
"github.com/stonith404/pocket-id/backend/internal/model/types"
|
||||||
"github.com/stonith404/pocket-id/backend/internal/utils"
|
"github.com/stonith404/pocket-id/backend/internal/utils"
|
||||||
|
"github.com/stonith404/pocket-id/backend/internal/utils/email"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
|
"log"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -15,10 +20,11 @@ type UserService struct {
|
|||||||
db *gorm.DB
|
db *gorm.DB
|
||||||
jwtService *JwtService
|
jwtService *JwtService
|
||||||
auditLogService *AuditLogService
|
auditLogService *AuditLogService
|
||||||
|
emailService *EmailService
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewUserService(db *gorm.DB, jwtService *JwtService, auditLogService *AuditLogService) *UserService {
|
func NewUserService(db *gorm.DB, jwtService *JwtService, auditLogService *AuditLogService, emailService *EmailService) *UserService {
|
||||||
return &UserService{db: db, jwtService: jwtService, auditLogService: auditLogService}
|
return &UserService{db: db, jwtService: jwtService, auditLogService: auditLogService, emailService: emailService}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *UserService) ListUsers(searchTerm string, sortedPaginationRequest utils.SortedPaginationRequest) ([]model.User, utils.PaginationResponse, error) {
|
func (s *UserService) ListUsers(searchTerm string, sortedPaginationRequest utils.SortedPaginationRequest) ([]model.User, utils.PaginationResponse, error) {
|
||||||
@@ -46,6 +52,10 @@ func (s *UserService) DeleteUser(userID string) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if user.LdapID != nil {
|
||||||
|
return &common.LdapUserUpdateError{}
|
||||||
|
}
|
||||||
|
|
||||||
return s.db.Delete(&user).Error
|
return s.db.Delete(&user).Error
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -56,6 +66,7 @@ func (s *UserService) CreateUser(input dto.UserCreateDto) (model.User, error) {
|
|||||||
Email: input.Email,
|
Email: input.Email,
|
||||||
Username: input.Username,
|
Username: input.Username,
|
||||||
IsAdmin: input.IsAdmin,
|
IsAdmin: input.IsAdmin,
|
||||||
|
LdapID: &input.LdapID,
|
||||||
}
|
}
|
||||||
if err := s.db.Create(&user).Error; err != nil {
|
if err := s.db.Create(&user).Error; err != nil {
|
||||||
if errors.Is(err, gorm.ErrDuplicatedKey) {
|
if errors.Is(err, gorm.ErrDuplicatedKey) {
|
||||||
@@ -66,11 +77,16 @@ func (s *UserService) CreateUser(input dto.UserCreateDto) (model.User, error) {
|
|||||||
return user, nil
|
return user, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *UserService) UpdateUser(userID string, updatedUser dto.UserCreateDto, updateOwnUser bool) (model.User, error) {
|
func (s *UserService) UpdateUser(userID string, updatedUser dto.UserCreateDto, updateOwnUser bool, allowLdapUpdate bool) (model.User, error) {
|
||||||
var user model.User
|
var user model.User
|
||||||
if err := s.db.Where("id = ?", userID).First(&user).Error; err != nil {
|
if err := s.db.Where("id = ?", userID).First(&user).Error; err != nil {
|
||||||
return model.User{}, err
|
return model.User{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if user.LdapID != nil && !allowLdapUpdate {
|
||||||
|
return model.User{}, &common.LdapUserUpdateError{}
|
||||||
|
}
|
||||||
|
|
||||||
user.FirstName = updatedUser.FirstName
|
user.FirstName = updatedUser.FirstName
|
||||||
user.LastName = updatedUser.LastName
|
user.LastName = updatedUser.LastName
|
||||||
user.Email = updatedUser.Email
|
user.Email = updatedUser.Email
|
||||||
@@ -89,7 +105,46 @@ func (s *UserService) UpdateUser(userID string, updatedUser dto.UserCreateDto, u
|
|||||||
return user, nil
|
return user, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *UserService) CreateOneTimeAccessToken(userID string, expiresAt time.Time, ipAddress, userAgent string) (string, error) {
|
func (s *UserService) RequestOneTimeAccessEmail(emailAddress, redirectPath string) error {
|
||||||
|
var user model.User
|
||||||
|
if err := s.db.Where("email = ?", emailAddress).First(&user).Error; err != nil {
|
||||||
|
// Do not return error if user not found to prevent email enumeration
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return nil
|
||||||
|
} else {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
oneTimeAccessToken, err := s.CreateOneTimeAccessToken(user.ID, time.Now().Add(time.Hour))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
link := fmt.Sprintf("%s/login/%s", common.EnvConfig.AppURL, oneTimeAccessToken)
|
||||||
|
|
||||||
|
// Add redirect path to the link
|
||||||
|
if strings.HasPrefix(redirectPath, "/") {
|
||||||
|
encodedRedirectPath := url.QueryEscape(redirectPath)
|
||||||
|
link = fmt.Sprintf("%s?redirect=%s", link, encodedRedirectPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
err := SendEmail(s.emailService, email.Address{
|
||||||
|
Name: user.Username,
|
||||||
|
Email: user.Email,
|
||||||
|
}, OneTimeAccessTemplate, &OneTimeAccessTemplateData{
|
||||||
|
Link: link,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Failed to send email to '%s': %v\n", user.Email, err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *UserService) CreateOneTimeAccessToken(userID string, expiresAt time.Time) (string, error) {
|
||||||
randomString, err := utils.GenerateRandomAlphanumericString(16)
|
randomString, err := utils.GenerateRandomAlphanumericString(16)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
@@ -105,12 +160,10 @@ func (s *UserService) CreateOneTimeAccessToken(userID string, expiresAt time.Tim
|
|||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
s.auditLogService.Create(model.AuditLogEventOneTimeAccessTokenSignIn, ipAddress, userAgent, userID, model.AuditLogData{})
|
|
||||||
|
|
||||||
return oneTimeAccessToken.Token, nil
|
return oneTimeAccessToken.Token, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *UserService) ExchangeOneTimeAccessToken(token string) (model.User, string, error) {
|
func (s *UserService) ExchangeOneTimeAccessToken(token string, ipAddress, userAgent string) (model.User, string, error) {
|
||||||
var oneTimeAccessToken model.OneTimeAccessToken
|
var oneTimeAccessToken model.OneTimeAccessToken
|
||||||
if err := s.db.Where("token = ? AND expires_at > ?", token, datatype.DateTime(time.Now())).Preload("User").First(&oneTimeAccessToken).Error; err != nil {
|
if err := s.db.Where("token = ? AND expires_at > ?", token, datatype.DateTime(time.Now())).Preload("User").First(&oneTimeAccessToken).Error; err != nil {
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
@@ -127,6 +180,10 @@ func (s *UserService) ExchangeOneTimeAccessToken(token string) (model.User, stri
|
|||||||
return model.User{}, "", err
|
return model.User{}, "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ipAddress != "" && userAgent != "" {
|
||||||
|
s.auditLogService.Create(model.AuditLogEventOneTimeAccessTokenSignIn, ipAddress, userAgent, oneTimeAccessToken.User.ID, model.AuditLogData{})
|
||||||
|
}
|
||||||
|
|
||||||
return oneTimeAccessToken.User, accessToken, nil
|
return oneTimeAccessToken.User, accessToken, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
12
backend/internal/utils/cookie_util.go
Normal file
12
backend/internal/utils/cookie_util.go
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
package utils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
func AddAccessTokenCookie(c *gin.Context, sessionDurationInMinutes string, token string) {
|
||||||
|
sessionDurationInMinutesParsed, _ := strconv.Atoi(sessionDurationInMinutes)
|
||||||
|
maxAge := sessionDurationInMinutesParsed * 60
|
||||||
|
c.SetCookie("access_token", token, maxAge, "/", "", true, true)
|
||||||
|
}
|
||||||
@@ -9,8 +9,6 @@ import (
|
|||||||
ttemplate "text/template"
|
ttemplate "text/template"
|
||||||
)
|
)
|
||||||
|
|
||||||
const templateComponentsDir = "components"
|
|
||||||
|
|
||||||
type Template[V any] struct {
|
type Template[V any] struct {
|
||||||
Path string
|
Path string
|
||||||
Title func(data *TemplateData[V]) string
|
Title func(data *TemplateData[V]) string
|
||||||
|
|||||||
@@ -63,8 +63,13 @@ func Paginate(page int, pageSize int, query *gorm.DB, result interface{}) (Pagin
|
|||||||
return PaginationResponse{}, err
|
return PaginationResponse{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
totalPages := (totalItems + int64(pageSize) - 1) / int64(pageSize)
|
||||||
|
if totalItems == 0 {
|
||||||
|
totalPages = 1
|
||||||
|
}
|
||||||
|
|
||||||
return PaginationResponse{
|
return PaginationResponse{
|
||||||
TotalPages: (totalItems + int64(pageSize) - 1) / int64(pageSize),
|
TotalPages: totalPages,
|
||||||
TotalItems: totalItems,
|
TotalItems: totalItems,
|
||||||
CurrentPage: page,
|
CurrentPage: page,
|
||||||
ItemsPerPage: pageSize,
|
ItemsPerPage: pageSize,
|
||||||
|
|||||||
@@ -76,5 +76,20 @@
|
|||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
}
|
}
|
||||||
|
.button {
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 500;
|
||||||
|
background-color: #000000;
|
||||||
|
color: #ffffff;
|
||||||
|
padding: 0.7rem 1.5rem;
|
||||||
|
outline: none;
|
||||||
|
border: none;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
.button-container {
|
||||||
|
text-align: center;
|
||||||
|
margin-top: 24px;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{{ define "base" }}
|
{{ define "base" }}
|
||||||
<div class="header">
|
<div class="header">
|
||||||
<div class="logo">
|
<div class="logo">
|
||||||
<img src="{{ .LogoURL }}" alt="Pocket ID"/>
|
<img src="{{ .LogoURL }}" alt="{{ .AppName }}"/>
|
||||||
<h1>{{ .AppName }}</h1>
|
<h1>{{ .AppName }}</h1>
|
||||||
</div>
|
</div>
|
||||||
<div class="warning">Warning</div>
|
<div class="warning">Warning</div>
|
||||||
|
|||||||
17
backend/resources/email-templates/one-time-access_html.tmpl
Normal file
17
backend/resources/email-templates/one-time-access_html.tmpl
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
{{ define "base" }}
|
||||||
|
<div class="header">
|
||||||
|
<div class="logo">
|
||||||
|
<img src="{{ .LogoURL }}" alt="{{ .AppName }}"/>
|
||||||
|
<h1>{{ .AppName }}</h1>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="content">
|
||||||
|
<h2>One-Time Access</h2>
|
||||||
|
<p class="message">
|
||||||
|
Click the button below to sign in to {{ .AppName }} with a one-time access link. This link expires in 15 minutes.
|
||||||
|
</p>
|
||||||
|
<div class="button-container">
|
||||||
|
<a class="button" href="{{ .Data.Link }}" class="button">Sign In</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{ end -}}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
{{ define "base" -}}
|
||||||
|
One-Time Access
|
||||||
|
====================
|
||||||
|
|
||||||
|
Click the link below to sign in to {{ .AppName }} with a one-time access link. This link expires in 15 minutes.
|
||||||
|
|
||||||
|
{{ .Data.Link }}
|
||||||
|
{{ end -}}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
{{ define "base" -}}
|
{{ define "base" -}}
|
||||||
<div class="header">
|
<div class="header">
|
||||||
<div class="logo">
|
<div class="logo">
|
||||||
<img src="{{ .LogoURL }}" alt="Pocket ID"/>
|
<img src="{{ .LogoURL }}" alt="{{ .AppName }}"/>
|
||||||
<h1>{{ .AppName }}</h1>
|
<h1>{{ .AppName }}</h1>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
ALTER TABLE users
|
||||||
|
DROP COLUMN ldap_id;
|
||||||
|
|
||||||
|
ALTER TABLE user_groups
|
||||||
|
DROP COLUMN ldap_id;
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
ALTER TABLE users ADD COLUMN ldap_id TEXT;
|
||||||
|
ALTER TABLE user_groups ADD COLUMN ldap_id TEXT;
|
||||||
|
|
||||||
|
CREATE UNIQUE INDEX users_ldap_id ON users (ldap_id);
|
||||||
|
CREATE UNIQUE INDEX user_groups_ldap_id ON user_groups (ldap_id);
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
UPDATE app_config_variables SET key = 'emailEnabled' WHERE key = 'emailLoginNotificationEnabled';
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
UPDATE app_config_variables SET key = 'emailLoginNotificationEnabled' WHERE key = 'emailEnabled';
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
ALTER TABLE users DROP COLUMN ldap_id;
|
||||||
|
ALTER TABLE user_groups DROP COLUMN ldap_id;
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
ALTER TABLE users ADD COLUMN ldap_id TEXT;
|
||||||
|
ALTER TABLE user_groups ADD COLUMN ldap_id TEXT;
|
||||||
|
|
||||||
|
CREATE UNIQUE INDEX users_ldap_id ON users (ldap_id);
|
||||||
|
CREATE UNIQUE INDEX user_groups_ldap_id ON user_groups (ldap_id);
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
UPDATE app_config_variables SET key = 'emailEnabled' WHERE key = 'emailLoginNotificationEnabled';
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
UPDATE app_config_variables SET key = 'emailLoginNotificationEnabled' WHERE key = 'emailEnabled';
|
||||||
@@ -1,8 +1,105 @@
|
|||||||
# Proxy Services through Pocket ID
|
# Proxy Services through Pocket ID
|
||||||
|
|
||||||
The goal of Pocket ID is to stay simple. Because of that we don't have a built-in proxy provider. However, you can use [OAuth2 Proxy](https://oauth2-proxy.github.io/oauth2-proxy/) to add authentication to your services that don't support OIDC. This guide will show you how to set up OAuth2 Proxy with Pocket ID.
|
The goal of Pocket ID is to function exclusively as an OIDC provider. As such, we don't have a built-in proxy provider. However, you can use other tools that act as a middleware to protect your services and support OIDC as an authentication provider.
|
||||||
|
|
||||||
## Docker Setup
|
There are two ways to do this:
|
||||||
|
|
||||||
|
- Implement OIDC into your reverse proxy
|
||||||
|
- Use [OAuth2 Proxy](https://oauth2-proxy.github.io/oauth2-proxy/)
|
||||||
|
|
||||||
|
## Reverse Proxy
|
||||||
|
|
||||||
|
Almost every reverse proxy somehow supports protecting your services with OIDC. Currently only Caddy is documented but you can search on Google for your reverse proxy and OIDC.
|
||||||
|
|
||||||
|
We would really appreciate if you contribute to this documentation by adding your reverse proxy and how to configure it with Pocket ID.
|
||||||
|
|
||||||
|
### Caddy
|
||||||
|
|
||||||
|
With [caddy-security](https://github.com/greenpau/caddy-security) you can easily protect your services with Pocket ID.
|
||||||
|
|
||||||
|
#### 1. Create a new OIDC client in Pocket ID.
|
||||||
|
|
||||||
|
Create a new OIDC client in Pocket ID by navigating to `https://<your-domain>/settings/admin/oidc-clients`. Now enter `https://<domain-of-proxied-service>/auth/oauth2/generic/authorization-code-callback` as the callback URL. After adding the client, you will obtain the client ID and client secret, which you will need in the next step.
|
||||||
|
|
||||||
|
#### 2. Install caddy-security
|
||||||
|
|
||||||
|
Run the following command to install caddy-security:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
caddy add-package github.com/greenpau/caddy-security
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3. Create your Caddyfile
|
||||||
|
|
||||||
|
```bash
|
||||||
|
{
|
||||||
|
# Port to listen on
|
||||||
|
http_port 443
|
||||||
|
|
||||||
|
# Configure caddy-security.
|
||||||
|
order authenticate before respond
|
||||||
|
security {
|
||||||
|
oauth identity provider generic {
|
||||||
|
realm generic
|
||||||
|
driver generic
|
||||||
|
client_id client-id-from-pocket-id # Replace with your own client ID
|
||||||
|
client_secret client-secret-from-pocket-id # Replace with your own client secret
|
||||||
|
scopes openid email profile
|
||||||
|
base_auth_url http://localhost
|
||||||
|
metadata_url http://localhost/.well-known/openid-configuration
|
||||||
|
}
|
||||||
|
|
||||||
|
authentication portal myportal {
|
||||||
|
crypto default token lifetime 3600 # Seconds until you have to re-authenticate
|
||||||
|
enable identity provider generic
|
||||||
|
cookie insecure off # Set to "on" if you're not using HTTPS
|
||||||
|
|
||||||
|
transform user {
|
||||||
|
match realm generic
|
||||||
|
action add role user
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
authorization policy mypolicy {
|
||||||
|
set auth url /auth/oauth2/generic
|
||||||
|
allow roles user
|
||||||
|
inject headers with claims
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
https://<domain-of-your-service> {
|
||||||
|
@auth {
|
||||||
|
path /auth/oauth2/generic
|
||||||
|
path /auth/oauth2/generic/authorization-code-callback
|
||||||
|
}
|
||||||
|
|
||||||
|
route @auth {
|
||||||
|
authenticate with myportal
|
||||||
|
}
|
||||||
|
|
||||||
|
route /* {
|
||||||
|
authorize with mypolicy
|
||||||
|
reverse_proxy http://<service-to-be-proxied>:<port> # Replace with your own service
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
For additional configuration options, refer to the official [caddy-security documentation](https://docs.authcrunch.com/docs/intro).
|
||||||
|
|
||||||
|
#### 4. Start Caddy
|
||||||
|
|
||||||
|
```bash
|
||||||
|
caddy run --config Caddyfile
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 5. Access the service
|
||||||
|
|
||||||
|
Your service should now be protected by Pocket ID.
|
||||||
|
|
||||||
|
## OAuth2 Proxy
|
||||||
|
|
||||||
|
### Docker Installation
|
||||||
|
|
||||||
#### 1. Add OAuth2 proxy to the service that should be proxied.
|
#### 1. Add OAuth2 proxy to the service that should be proxied.
|
||||||
|
|
||||||
@@ -74,7 +171,7 @@ docker compose up -d
|
|||||||
|
|
||||||
You can now access the service through OAuth2 Proxy by visiting `http://localhost:4180`.
|
You can now access the service through OAuth2 Proxy by visiting `http://localhost:4180`.
|
||||||
|
|
||||||
## Standalone Installation
|
### Standalone Installation
|
||||||
|
|
||||||
Setting up OAuth2 Proxy with Pocket ID without Docker is similar to the Docker setup. As the setup depends on your environment, you have to adjust the steps accordingly but is should be similar to the Docker setup.
|
Setting up OAuth2 Proxy with Pocket ID without Docker is similar to the Docker setup. As the setup depends on your environment, you have to adjust the steps accordingly but is should be similar to the Docker setup.
|
||||||
|
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ export default [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
rules: {
|
rules: {
|
||||||
"@typescript-eslint/no-explicit-any": "off"
|
'@typescript-eslint/no-explicit-any': 'off'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|||||||
4
frontend/package-lock.json
generated
4
frontend/package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "pocket-id-frontend",
|
"name": "pocket-id-frontend",
|
||||||
"version": "0.10.0",
|
"version": "0.24.1",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "pocket-id-frontend",
|
"name": "pocket-id-frontend",
|
||||||
"version": "0.10.0",
|
"version": "0.24.1",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@simplewebauthn/browser": "^10.0.0",
|
"@simplewebauthn/browser": "^10.0.0",
|
||||||
"axios": "^1.7.7",
|
"axios": "^1.7.7",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "pocket-id-frontend",
|
"name": "pocket-id-frontend",
|
||||||
"version": "0.24.1",
|
"version": "0.25.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite dev --port 3000",
|
"dev": "vite dev --port 3000",
|
||||||
|
|||||||
@@ -17,6 +17,7 @@
|
|||||||
requestOptions = $bindable(),
|
requestOptions = $bindable(),
|
||||||
selectedIds = $bindable(),
|
selectedIds = $bindable(),
|
||||||
withoutSearch = false,
|
withoutSearch = false,
|
||||||
|
selectionDisabled = false,
|
||||||
defaultSort,
|
defaultSort,
|
||||||
onRefresh,
|
onRefresh,
|
||||||
columns,
|
columns,
|
||||||
@@ -26,12 +27,15 @@
|
|||||||
requestOptions?: SearchPaginationSortRequest;
|
requestOptions?: SearchPaginationSortRequest;
|
||||||
selectedIds?: string[];
|
selectedIds?: string[];
|
||||||
withoutSearch?: boolean;
|
withoutSearch?: boolean;
|
||||||
|
selectionDisabled?: boolean;
|
||||||
defaultSort?: { column: string; direction: 'asc' | 'desc' };
|
defaultSort?: { column: string; direction: 'asc' | 'desc' };
|
||||||
onRefresh: (requestOptions: SearchPaginationSortRequest) => Promise<Paginated<T>>;
|
onRefresh: (requestOptions: SearchPaginationSortRequest) => Promise<Paginated<T>>;
|
||||||
columns: { label: string; hidden?: boolean; sortColumn?: string }[];
|
columns: { label: string; hidden?: boolean; sortColumn?: string }[];
|
||||||
rows: Snippet<[{ item: T }]>;
|
rows: Snippet<[{ item: T }]>;
|
||||||
} = $props();
|
} = $props();
|
||||||
|
|
||||||
|
let searchValue = $state('');
|
||||||
|
|
||||||
if (!requestOptions) {
|
if (!requestOptions) {
|
||||||
requestOptions = {
|
requestOptions = {
|
||||||
search: '',
|
search: '',
|
||||||
@@ -55,9 +59,10 @@
|
|||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
|
|
||||||
const onSearch = debounced(async (searchValue: string) => {
|
const onSearch = debounced(async (search: string) => {
|
||||||
requestOptions.search = searchValue;
|
requestOptions.search = search;
|
||||||
onRefresh(requestOptions);
|
await onRefresh(requestOptions);
|
||||||
|
searchValue = search;
|
||||||
}, 300);
|
}, 300);
|
||||||
|
|
||||||
async function onAllCheck(checked: boolean) {
|
async function onAllCheck(checked: boolean) {
|
||||||
@@ -95,28 +100,35 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if items.data.length === 0}
|
|
||||||
<div class="my-5 flex flex-col items-center">
|
|
||||||
<Empty class="text-muted-foreground h-20" />
|
|
||||||
<p class="text-muted-foreground mt-3 text-sm">No items found</p>
|
|
||||||
</div>
|
|
||||||
{:else}
|
|
||||||
<div class="w-full overflow-x-auto">
|
|
||||||
{#if !withoutSearch}
|
{#if !withoutSearch}
|
||||||
<Input
|
<Input
|
||||||
class="mb-4 max-w-sm"
|
value={searchValue}
|
||||||
|
class={cn(
|
||||||
|
'relative z-50 mb-4 max-w-sm',
|
||||||
|
items.data.length == 0 && searchValue == '' && 'hidden'
|
||||||
|
)}
|
||||||
placeholder={'Search...'}
|
placeholder={'Search...'}
|
||||||
type="text"
|
type="text"
|
||||||
oninput={(e) => onSearch((e.target as HTMLInputElement).value)}
|
oninput={(e) => onSearch((e.target as HTMLInputElement).value)}
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<Table.Root class="min-w-full table-auto">
|
{#if items.data.length === 0 && searchValue === ''}
|
||||||
|
<div class="my-5 flex flex-col items-center">
|
||||||
|
<Empty class="h-20 text-muted-foreground" />
|
||||||
|
<p class="mt-3 text-sm text-muted-foreground">No items found</p>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<Table.Root class="min-w-full table-auto overflow-x-auto">
|
||||||
<Table.Header>
|
<Table.Header>
|
||||||
<Table.Row>
|
<Table.Row>
|
||||||
{#if selectedIds}
|
{#if selectedIds}
|
||||||
<Table.Head class="w-12">
|
<Table.Head class="w-12">
|
||||||
<Checkbox checked={allChecked} onCheckedChange={(c) => onAllCheck(c as boolean)} />
|
<Checkbox
|
||||||
|
disabled={selectionDisabled}
|
||||||
|
checked={allChecked}
|
||||||
|
onCheckedChange={(c) => onAllCheck(c as boolean)}
|
||||||
|
/>
|
||||||
</Table.Head>
|
</Table.Head>
|
||||||
{/if}
|
{/if}
|
||||||
{#each columns as column}
|
{#each columns as column}
|
||||||
@@ -154,6 +166,7 @@
|
|||||||
{#if selectedIds}
|
{#if selectedIds}
|
||||||
<Table.Cell class="w-12">
|
<Table.Cell class="w-12">
|
||||||
<Checkbox
|
<Checkbox
|
||||||
|
disabled={selectionDisabled}
|
||||||
checked={selectedIds.includes(item.id)}
|
checked={selectedIds.includes(item.id)}
|
||||||
onCheckedChange={(c) => onCheck(c as boolean, item.id)}
|
onCheckedChange={(c) => onCheck(c as boolean, item.id)}
|
||||||
/>
|
/>
|
||||||
@@ -198,7 +211,7 @@
|
|||||||
<Pagination.PrevButton />
|
<Pagination.PrevButton />
|
||||||
</Pagination.Item>
|
</Pagination.Item>
|
||||||
{#each pages as page (page.key)}
|
{#each pages as page (page.key)}
|
||||||
{#if page.type !== 'ellipsis'}
|
{#if page.type !== 'ellipsis' && page.value != 0}
|
||||||
<Pagination.Item>
|
<Pagination.Item>
|
||||||
<Pagination.Link {page} isActive={items.pagination.currentPage === page.value}>
|
<Pagination.Link {page} isActive={items.pagination.currentPage === page.value}>
|
||||||
{page.value}
|
{page.value}
|
||||||
@@ -212,5 +225,4 @@
|
|||||||
</Pagination.Content>
|
</Pagination.Content>
|
||||||
</Pagination.Root>
|
</Pagination.Root>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -103,7 +103,7 @@
|
|||||||
onkeydown={(e) => {
|
onkeydown={(e) => {
|
||||||
if (e.key === 'Enter') handleSuggestionClick(suggestion);
|
if (e.key === 'Enter') handleSuggestionClick(suggestion);
|
||||||
}}
|
}}
|
||||||
class="hover:bg-accent hover:text-accent-foreground relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 {selectedIndex ===
|
class="relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none hover:bg-accent hover:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 {selectedIndex ===
|
||||||
index
|
index
|
||||||
? 'bg-accent text-accent-foreground'
|
? 'bg-accent text-accent-foreground'
|
||||||
: ''}"
|
: ''}"
|
||||||
|
|||||||
@@ -19,7 +19,7 @@
|
|||||||
} = $props();
|
} = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="items-top mt-5 flex space-x-2">
|
<div class="items-top flex space-x-2">
|
||||||
<Checkbox
|
<Checkbox
|
||||||
{id}
|
{id}
|
||||||
{disabled}
|
{disabled}
|
||||||
@@ -31,7 +31,7 @@
|
|||||||
{label}
|
{label}
|
||||||
</Label>
|
</Label>
|
||||||
{#if description}
|
{#if description}
|
||||||
<p class="text-muted-foreground text-[0.8rem]">
|
<p class="text-[0.8rem] text-muted-foreground">
|
||||||
{description}
|
{description}
|
||||||
</p>
|
</p>
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="mt-[20%] flex flex-col items-center">
|
<div class="mt-[20%] flex flex-col items-center">
|
||||||
<LucideXCircle class="text-muted-foreground h-12 w-12" />
|
<LucideXCircle class="h-12 w-12 text-muted-foreground" />
|
||||||
<h1 class="mt-3 text-2xl font-semibold">Something went wrong</h1>
|
<h1 class="mt-3 text-2xl font-semibold">Something went wrong</h1>
|
||||||
<p class="text-muted-foreground">{message}</p>
|
<p class="text-muted-foreground">{message}</p>
|
||||||
{#if showButton}
|
{#if showButton}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
input = $bindable(),
|
input = $bindable(),
|
||||||
label,
|
label,
|
||||||
description,
|
description,
|
||||||
|
placeholder,
|
||||||
disabled = false,
|
disabled = false,
|
||||||
type = 'text',
|
type = 'text',
|
||||||
children,
|
children,
|
||||||
@@ -18,6 +19,7 @@
|
|||||||
input?: FormInput<string | boolean | number>;
|
input?: FormInput<string | boolean | number>;
|
||||||
label?: string;
|
label?: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
|
placeholder?: string;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
type?: 'text' | 'password' | 'email' | 'number' | 'checkbox';
|
type?: 'text' | 'password' | 'email' | 'number' | 'checkbox';
|
||||||
onInput?: (e: FormInputEvent) => void;
|
onInput?: (e: FormInputEvent) => void;
|
||||||
@@ -32,13 +34,20 @@
|
|||||||
<Label class="mb-0" for={id}>{label}</Label>
|
<Label class="mb-0" for={id}>{label}</Label>
|
||||||
{/if}
|
{/if}
|
||||||
{#if description}
|
{#if description}
|
||||||
<p class="text-muted-foreground mt-1 text-xs">{description}</p>
|
<p class="mt-1 text-xs text-muted-foreground">{description}</p>
|
||||||
{/if}
|
{/if}
|
||||||
<div class={label || description ? 'mt-2' : ''}>
|
<div class={label || description ? 'mt-2' : ''}>
|
||||||
{#if children}
|
{#if children}
|
||||||
{@render children()}
|
{@render children()}
|
||||||
{:else if input}
|
{:else if input}
|
||||||
<Input {id} {type} bind:value={input.value} {disabled} on:input={(e) => onInput?.(e)} />
|
<Input
|
||||||
|
{id}
|
||||||
|
{placeholder}
|
||||||
|
{type}
|
||||||
|
bind:value={input.value}
|
||||||
|
{disabled}
|
||||||
|
on:input={(e) => onInput?.(e)}
|
||||||
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
{#if input?.error}
|
{#if input?.error}
|
||||||
<p class="mt-1 text-sm text-red-500">{input.error}</p>
|
<p class="mt-1 text-sm text-red-500">{input.error}</p>
|
||||||
|
|||||||
@@ -39,7 +39,7 @@
|
|||||||
{$userStore?.firstName}
|
{$userStore?.firstName}
|
||||||
{$userStore?.lastName}
|
{$userStore?.lastName}
|
||||||
</p>
|
</p>
|
||||||
<p class="text-muted-foreground text-xs leading-none">{$userStore?.email}</p>
|
<p class="text-xs leading-none text-muted-foreground">{$userStore?.email}</p>
|
||||||
</div>
|
</div>
|
||||||
</DropdownMenu.Label>
|
</DropdownMenu.Label>
|
||||||
<DropdownMenu.Separator />
|
<DropdownMenu.Separator />
|
||||||
|
|||||||
@@ -2,22 +2,44 @@
|
|||||||
import { browser } from '$app/environment';
|
import { browser } from '$app/environment';
|
||||||
import { browserSupportsWebAuthn } from '@simplewebauthn/browser';
|
import { browserSupportsWebAuthn } from '@simplewebauthn/browser';
|
||||||
import type { Snippet } from 'svelte';
|
import type { Snippet } from 'svelte';
|
||||||
|
import { Button } from './ui/button';
|
||||||
import * as Card from './ui/card';
|
import * as Card from './ui/card';
|
||||||
import WebAuthnUnsupported from './web-authn-unsupported.svelte';
|
import WebAuthnUnsupported from './web-authn-unsupported.svelte';
|
||||||
|
import { page } from '$app/stores';
|
||||||
|
|
||||||
let {
|
let {
|
||||||
children
|
children,
|
||||||
|
showEmailOneTimeAccessButton = false
|
||||||
}: {
|
}: {
|
||||||
children: Snippet;
|
children: Snippet;
|
||||||
|
showEmailOneTimeAccessButton?: boolean;
|
||||||
} = $props();
|
} = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<!-- Desktop -->
|
||||||
<div class="hidden h-screen items-center text-center lg:flex">
|
<div class="hidden h-screen items-center text-center lg:flex">
|
||||||
<div class="min-w-[650px] p-16">
|
<div class="h-full min-w-[650px] p-16 {showEmailOneTimeAccessButton ? 'pb-0' : ''}">
|
||||||
{#if browser && !browserSupportsWebAuthn()}
|
{#if browser && !browserSupportsWebAuthn()}
|
||||||
<WebAuthnUnsupported />
|
<WebAuthnUnsupported />
|
||||||
{:else}
|
{:else}
|
||||||
|
<div class="flex h-full flex-col">
|
||||||
|
<div class="flex flex-grow flex-col items-center justify-center">
|
||||||
{@render children()}
|
{@render children()}
|
||||||
|
</div>
|
||||||
|
{#if showEmailOneTimeAccessButton}
|
||||||
|
<div class="mb-4 flex justify-center">
|
||||||
|
<Button
|
||||||
|
href="/login/email?redirect={encodeURIComponent(
|
||||||
|
$page.url.pathname + $page.url.search
|
||||||
|
)}"
|
||||||
|
variant="link"
|
||||||
|
class="text-xs text-muted-foreground"
|
||||||
|
>
|
||||||
|
Don't have access to your passkey?
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<img
|
<img
|
||||||
@@ -27,15 +49,31 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Mobile -->
|
||||||
<div
|
<div
|
||||||
class="flex h-screen items-center justify-center bg-[url('/api/application-configuration/background-image')] bg-cover bg-center text-center lg:hidden"
|
class="flex h-screen items-center justify-center bg-[url('/api/application-configuration/background-image')] bg-cover bg-center text-center lg:hidden"
|
||||||
>
|
>
|
||||||
<Card.Root class="mx-3">
|
<Card.Root class="mx-3">
|
||||||
<Card.CardContent class="px-4 py-10 sm:p-10">
|
<Card.CardContent
|
||||||
|
class="px-4 py-10 sm:p-10 {showEmailOneTimeAccessButton ? 'pb-3 sm:pb-3' : ''}"
|
||||||
|
>
|
||||||
{#if browser && !browserSupportsWebAuthn()}
|
{#if browser && !browserSupportsWebAuthn()}
|
||||||
<WebAuthnUnsupported />
|
<WebAuthnUnsupported />
|
||||||
{:else}
|
{:else}
|
||||||
{@render children()}
|
{@render children()}
|
||||||
|
{#if showEmailOneTimeAccessButton}
|
||||||
|
<div class="mt-5">
|
||||||
|
<Button
|
||||||
|
href="/login/email?redirect={encodeURIComponent(
|
||||||
|
$page.url.pathname + $page.url.search
|
||||||
|
)}"
|
||||||
|
variant="link"
|
||||||
|
class="text-xs text-muted-foreground"
|
||||||
|
>
|
||||||
|
Don't have access to your passkey?
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
</Card.CardContent>
|
</Card.CardContent>
|
||||||
</Card.Root>
|
</Card.Root>
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { AlertDialog as AlertDialogPrimitive } from "bits-ui";
|
import { AlertDialog as AlertDialogPrimitive } from 'bits-ui';
|
||||||
import { buttonVariants } from "$lib/components/ui/button/index.js";
|
import { buttonVariants } from '$lib/components/ui/button/index.js';
|
||||||
import { cn } from "$lib/utils/style.js";
|
import { cn } from '$lib/utils/style.js';
|
||||||
|
|
||||||
type $$Props = AlertDialogPrimitive.ActionProps;
|
type $$Props = AlertDialogPrimitive.ActionProps;
|
||||||
type $$Events = AlertDialogPrimitive.ActionEvents;
|
type $$Events = AlertDialogPrimitive.ActionEvents;
|
||||||
|
|
||||||
let className: $$Props["class"] = undefined;
|
let className: $$Props['class'] = undefined;
|
||||||
export { className as class };
|
export { className as class };
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { AlertDialog as AlertDialogPrimitive } from "bits-ui";
|
import { AlertDialog as AlertDialogPrimitive } from 'bits-ui';
|
||||||
import { buttonVariants } from "$lib/components/ui/button/index.js";
|
import { buttonVariants } from '$lib/components/ui/button/index.js';
|
||||||
import { cn } from "$lib/utils/style.js";
|
import { cn } from '$lib/utils/style.js';
|
||||||
|
|
||||||
type $$Props = AlertDialogPrimitive.CancelProps;
|
type $$Props = AlertDialogPrimitive.CancelProps;
|
||||||
type $$Events = AlertDialogPrimitive.CancelEvents;
|
type $$Events = AlertDialogPrimitive.CancelEvents;
|
||||||
|
|
||||||
let className: $$Props["class"] = undefined;
|
let className: $$Props['class'] = undefined;
|
||||||
export { className as class };
|
export { className as class };
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<AlertDialogPrimitive.Cancel
|
<AlertDialogPrimitive.Cancel
|
||||||
class={cn(buttonVariants({ variant: "outline" }), "mt-2 sm:mt-0", className)}
|
class={cn(buttonVariants({ variant: 'outline' }), 'mt-2 sm:mt-0', className)}
|
||||||
{...$$restProps}
|
{...$$restProps}
|
||||||
on:click
|
on:click
|
||||||
on:keydown
|
on:keydown
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { AlertDialog as AlertDialogPrimitive } from "bits-ui";
|
import { AlertDialog as AlertDialogPrimitive } from 'bits-ui';
|
||||||
import * as AlertDialog from "./index.js";
|
import * as AlertDialog from './index.js';
|
||||||
import { cn, flyAndScale } from "$lib/utils/style.js";
|
import { cn, flyAndScale } from '$lib/utils/style.js';
|
||||||
|
|
||||||
type $$Props = AlertDialogPrimitive.ContentProps;
|
type $$Props = AlertDialogPrimitive.ContentProps;
|
||||||
|
|
||||||
export let transition: $$Props["transition"] = flyAndScale;
|
export let transition: $$Props['transition'] = flyAndScale;
|
||||||
export let transitionConfig: $$Props["transitionConfig"] = undefined;
|
export let transitionConfig: $$Props['transitionConfig'] = undefined;
|
||||||
|
|
||||||
let className: $$Props["class"] = undefined;
|
let className: $$Props['class'] = undefined;
|
||||||
export { className as class };
|
export { className as class };
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -18,7 +18,7 @@
|
|||||||
{transition}
|
{transition}
|
||||||
{transitionConfig}
|
{transitionConfig}
|
||||||
class={cn(
|
class={cn(
|
||||||
"bg-background fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border p-6 shadow-lg sm:rounded-lg md:w-full",
|
'fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg sm:rounded-lg md:w-full',
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...$$restProps}
|
{...$$restProps}
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { AlertDialog as AlertDialogPrimitive } from "bits-ui";
|
import { AlertDialog as AlertDialogPrimitive } from 'bits-ui';
|
||||||
import { cn } from "$lib/utils/style.js";
|
import { cn } from '$lib/utils/style.js';
|
||||||
|
|
||||||
type $$Props = AlertDialogPrimitive.DescriptionProps;
|
type $$Props = AlertDialogPrimitive.DescriptionProps;
|
||||||
|
|
||||||
let className: $$Props["class"] = undefined;
|
let className: $$Props['class'] = undefined;
|
||||||
export { className as class };
|
export { className as class };
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<AlertDialogPrimitive.Description
|
<AlertDialogPrimitive.Description
|
||||||
class={cn("text-muted-foreground text-sm", className)}
|
class={cn('text-sm text-muted-foreground', className)}
|
||||||
{...$$restProps}
|
{...$$restProps}
|
||||||
>
|
>
|
||||||
<slot />
|
<slot />
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { HTMLAttributes } from "svelte/elements";
|
import type { HTMLAttributes } from 'svelte/elements';
|
||||||
import { cn } from "$lib/utils/style.js";
|
import { cn } from '$lib/utils/style.js';
|
||||||
|
|
||||||
type $$Props = HTMLAttributes<HTMLDivElement>;
|
type $$Props = HTMLAttributes<HTMLDivElement>;
|
||||||
|
|
||||||
let className: $$Props["class"] = undefined;
|
let className: $$Props['class'] = undefined;
|
||||||
export { className as class };
|
export { className as class };
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class={cn("flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", className)}
|
class={cn('flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2', className)}
|
||||||
{...$$restProps}
|
{...$$restProps}
|
||||||
>
|
>
|
||||||
<slot />
|
<slot />
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { HTMLAttributes } from "svelte/elements";
|
import type { HTMLAttributes } from 'svelte/elements';
|
||||||
import { cn } from "$lib/utils/style.js";
|
import { cn } from '$lib/utils/style.js';
|
||||||
|
|
||||||
type $$Props = HTMLAttributes<HTMLDivElement>;
|
type $$Props = HTMLAttributes<HTMLDivElement>;
|
||||||
|
|
||||||
let className: $$Props["class"] = undefined;
|
let className: $$Props['class'] = undefined;
|
||||||
export { className as class };
|
export { className as class };
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class={cn("flex flex-col space-y-2 text-center sm:text-left", className)} {...$$restProps}>
|
<div class={cn('flex flex-col space-y-2 text-center sm:text-left', className)} {...$$restProps}>
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { AlertDialog as AlertDialogPrimitive } from "bits-ui";
|
import { AlertDialog as AlertDialogPrimitive } from 'bits-ui';
|
||||||
import { fade } from "svelte/transition";
|
import { fade } from 'svelte/transition';
|
||||||
import { cn } from "$lib/utils/style.js";
|
import { cn } from '$lib/utils/style.js';
|
||||||
|
|
||||||
type $$Props = AlertDialogPrimitive.OverlayProps;
|
type $$Props = AlertDialogPrimitive.OverlayProps;
|
||||||
|
|
||||||
let className: $$Props["class"] = undefined;
|
let className: $$Props['class'] = undefined;
|
||||||
export let transition: $$Props["transition"] = fade;
|
export let transition: $$Props['transition'] = fade;
|
||||||
export let transitionConfig: $$Props["transitionConfig"] = {
|
export let transitionConfig: $$Props['transitionConfig'] = {
|
||||||
duration: 150,
|
duration: 150
|
||||||
};
|
};
|
||||||
export { className as class };
|
export { className as class };
|
||||||
</script>
|
</script>
|
||||||
@@ -16,6 +16,6 @@
|
|||||||
<AlertDialogPrimitive.Overlay
|
<AlertDialogPrimitive.Overlay
|
||||||
{transition}
|
{transition}
|
||||||
{transitionConfig}
|
{transitionConfig}
|
||||||
class={cn("bg-background/80 fixed inset-0 z-50 backdrop-blur-sm ", className)}
|
class={cn('fixed inset-0 z-50 bg-background/80 backdrop-blur-sm ', className)}
|
||||||
{...$$restProps}
|
{...$$restProps}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { AlertDialog as AlertDialogPrimitive } from "bits-ui";
|
import { AlertDialog as AlertDialogPrimitive } from 'bits-ui';
|
||||||
|
|
||||||
type $$Props = AlertDialogPrimitive.PortalProps;
|
type $$Props = AlertDialogPrimitive.PortalProps;
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { AlertDialog as AlertDialogPrimitive } from "bits-ui";
|
import { AlertDialog as AlertDialogPrimitive } from 'bits-ui';
|
||||||
import { cn } from "$lib/utils/style.js";
|
import { cn } from '$lib/utils/style.js';
|
||||||
|
|
||||||
type $$Props = AlertDialogPrimitive.TitleProps;
|
type $$Props = AlertDialogPrimitive.TitleProps;
|
||||||
|
|
||||||
let className: $$Props["class"] = undefined;
|
let className: $$Props['class'] = undefined;
|
||||||
export let level: $$Props["level"] = "h3";
|
export let level: $$Props['level'] = 'h3';
|
||||||
export { className as class };
|
export { className as class };
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<AlertDialogPrimitive.Title class={cn("text-lg font-semibold", className)} {level} {...$$restProps}>
|
<AlertDialogPrimitive.Title class={cn('text-lg font-semibold', className)} {level} {...$$restProps}>
|
||||||
<slot />
|
<slot />
|
||||||
</AlertDialogPrimitive.Title>
|
</AlertDialogPrimitive.Title>
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
import { AlertDialog as AlertDialogPrimitive } from "bits-ui";
|
import { AlertDialog as AlertDialogPrimitive } from 'bits-ui';
|
||||||
|
|
||||||
import Title from "./alert-dialog-title.svelte";
|
import Title from './alert-dialog-title.svelte';
|
||||||
import Action from "./alert-dialog-action.svelte";
|
import Action from './alert-dialog-action.svelte';
|
||||||
import Cancel from "./alert-dialog-cancel.svelte";
|
import Cancel from './alert-dialog-cancel.svelte';
|
||||||
import Portal from "./alert-dialog-portal.svelte";
|
import Portal from './alert-dialog-portal.svelte';
|
||||||
import Footer from "./alert-dialog-footer.svelte";
|
import Footer from './alert-dialog-footer.svelte';
|
||||||
import Header from "./alert-dialog-header.svelte";
|
import Header from './alert-dialog-header.svelte';
|
||||||
import Overlay from "./alert-dialog-overlay.svelte";
|
import Overlay from './alert-dialog-overlay.svelte';
|
||||||
import Content from "./alert-dialog-content.svelte";
|
import Content from './alert-dialog-content.svelte';
|
||||||
import Description from "./alert-dialog-description.svelte";
|
import Description from './alert-dialog-description.svelte';
|
||||||
|
|
||||||
const Root = AlertDialogPrimitive.Root;
|
const Root = AlertDialogPrimitive.Root;
|
||||||
const Trigger = AlertDialogPrimitive.Trigger;
|
const Trigger = AlertDialogPrimitive.Trigger;
|
||||||
@@ -36,5 +36,5 @@ export {
|
|||||||
Trigger as AlertDialogTrigger,
|
Trigger as AlertDialogTrigger,
|
||||||
Overlay as AlertDialogOverlay,
|
Overlay as AlertDialogOverlay,
|
||||||
Content as AlertDialogContent,
|
Content as AlertDialogContent,
|
||||||
Description as AlertDialogDescription,
|
Description as AlertDialogDescription
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { HTMLAttributes } from "svelte/elements";
|
import type { HTMLAttributes } from 'svelte/elements';
|
||||||
import { cn } from "$lib/utils/style.js";
|
import { cn } from '$lib/utils/style.js';
|
||||||
|
|
||||||
type $$Props = HTMLAttributes<HTMLDivElement>;
|
type $$Props = HTMLAttributes<HTMLDivElement>;
|
||||||
|
|
||||||
let className: $$Props["class"] = undefined;
|
let className: $$Props['class'] = undefined;
|
||||||
export { className as class };
|
export { className as class };
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class={cn("text-sm [&_p]:leading-relaxed", className)} {...$$restProps}>
|
<div class={cn('text-sm [&_p]:leading-relaxed', className)} {...$$restProps}>
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,20 +1,20 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { HTMLAttributes } from "svelte/elements";
|
import type { HTMLAttributes } from 'svelte/elements';
|
||||||
import type { HeadingLevel } from "./index.js";
|
import type { HeadingLevel } from './index.js';
|
||||||
import { cn } from "$lib/utils/style.js";
|
import { cn } from '$lib/utils/style.js';
|
||||||
|
|
||||||
type $$Props = HTMLAttributes<HTMLHeadingElement> & {
|
type $$Props = HTMLAttributes<HTMLHeadingElement> & {
|
||||||
level?: HeadingLevel;
|
level?: HeadingLevel;
|
||||||
};
|
};
|
||||||
|
|
||||||
let className: $$Props["class"] = undefined;
|
let className: $$Props['class'] = undefined;
|
||||||
export let level: $$Props["level"] = "h5";
|
export let level: $$Props['level'] = 'h5';
|
||||||
export { className as class };
|
export { className as class };
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:element
|
<svelte:element
|
||||||
this={level}
|
this={level}
|
||||||
class={cn("mb-1 font-medium leading-none tracking-tight", className)}
|
class={cn('mb-1 font-medium leading-none tracking-tight', className)}
|
||||||
{...$$restProps}
|
{...$$restProps}
|
||||||
>
|
>
|
||||||
<slot />
|
<slot />
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { HTMLAttributes } from "svelte/elements";
|
import type { HTMLAttributes } from 'svelte/elements';
|
||||||
import { type Variant, alertVariants } from "./index.js";
|
import { type Variant, alertVariants } from './index.js';
|
||||||
import { cn } from "$lib/utils/style.js";
|
import { cn } from '$lib/utils/style.js';
|
||||||
|
|
||||||
type $$Props = HTMLAttributes<HTMLDivElement> & {
|
type $$Props = HTMLAttributes<HTMLDivElement> & {
|
||||||
variant?: Variant;
|
variant?: Variant;
|
||||||
};
|
};
|
||||||
|
|
||||||
let className: $$Props["class"] = undefined;
|
let className: $$Props['class'] = undefined;
|
||||||
export let variant: $$Props["variant"] = "default";
|
export let variant: $$Props['variant'] = 'default';
|
||||||
export { className as class };
|
export { className as class };
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Avatar as AvatarPrimitive } from "bits-ui";
|
import { Avatar as AvatarPrimitive } from 'bits-ui';
|
||||||
import { cn } from "$lib/utils/style.js";
|
import { cn } from '$lib/utils/style.js';
|
||||||
|
|
||||||
type $$Props = AvatarPrimitive.FallbackProps;
|
type $$Props = AvatarPrimitive.FallbackProps;
|
||||||
|
|
||||||
let className: $$Props["class"] = undefined;
|
let className: $$Props['class'] = undefined;
|
||||||
export { className as class };
|
export { className as class };
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<AvatarPrimitive.Fallback
|
<AvatarPrimitive.Fallback
|
||||||
class={cn("bg-muted flex h-full w-full items-center justify-center rounded-full", className)}
|
class={cn('flex h-full w-full items-center justify-center rounded-full bg-muted', className)}
|
||||||
{...$$restProps}
|
{...$$restProps}
|
||||||
>
|
>
|
||||||
<slot />
|
<slot />
|
||||||
|
|||||||
@@ -1,18 +1,18 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Avatar as AvatarPrimitive } from "bits-ui";
|
import { Avatar as AvatarPrimitive } from 'bits-ui';
|
||||||
import { cn } from "$lib/utils/style.js";
|
import { cn } from '$lib/utils/style.js';
|
||||||
|
|
||||||
type $$Props = AvatarPrimitive.ImageProps;
|
type $$Props = AvatarPrimitive.ImageProps;
|
||||||
|
|
||||||
let className: $$Props["class"] = undefined;
|
let className: $$Props['class'] = undefined;
|
||||||
export let src: $$Props["src"] = undefined;
|
export let src: $$Props['src'] = undefined;
|
||||||
export let alt: $$Props["alt"] = undefined;
|
export let alt: $$Props['alt'] = undefined;
|
||||||
export { className as class };
|
export { className as class };
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<AvatarPrimitive.Image
|
<AvatarPrimitive.Image
|
||||||
{src}
|
{src}
|
||||||
{alt}
|
{alt}
|
||||||
class={cn("aspect-square h-full w-full", className)}
|
class={cn('aspect-square h-full w-full', className)}
|
||||||
{...$$restProps}
|
{...$$restProps}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Avatar as AvatarPrimitive } from "bits-ui";
|
import { Avatar as AvatarPrimitive } from 'bits-ui';
|
||||||
import { cn } from "$lib/utils/style.js";
|
import { cn } from '$lib/utils/style.js';
|
||||||
|
|
||||||
type $$Props = AvatarPrimitive.Props;
|
type $$Props = AvatarPrimitive.Props;
|
||||||
|
|
||||||
let className: $$Props["class"] = undefined;
|
let className: $$Props['class'] = undefined;
|
||||||
export let delayMs: $$Props["delayMs"] = undefined;
|
export let delayMs: $$Props['delayMs'] = undefined;
|
||||||
export { className as class };
|
export { className as class };
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<AvatarPrimitive.Root
|
<AvatarPrimitive.Root
|
||||||
{delayMs}
|
{delayMs}
|
||||||
class={cn("relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full", className)}
|
class={cn('relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full', className)}
|
||||||
{...$$restProps}
|
{...$$restProps}
|
||||||
>
|
>
|
||||||
<slot />
|
<slot />
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import Root from "./avatar.svelte";
|
import Root from './avatar.svelte';
|
||||||
import Image from "./avatar-image.svelte";
|
import Image from './avatar-image.svelte';
|
||||||
import Fallback from "./avatar-fallback.svelte";
|
import Fallback from './avatar-fallback.svelte';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
Root,
|
Root,
|
||||||
@@ -9,5 +9,5 @@ export {
|
|||||||
//
|
//
|
||||||
Root as Avatar,
|
Root as Avatar,
|
||||||
Image as AvatarImage,
|
Image as AvatarImage,
|
||||||
Fallback as AvatarFallback,
|
Fallback as AvatarFallback
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { type Variant, badgeVariants } from "./index.js";
|
import { type Variant, badgeVariants } from './index.js';
|
||||||
import { cn } from "$lib/utils/style.js";
|
import { cn } from '$lib/utils/style.js';
|
||||||
|
|
||||||
let className: string | undefined | null = undefined;
|
let className: string | undefined | null = undefined;
|
||||||
export let href: string | undefined = undefined;
|
export let href: string | undefined = undefined;
|
||||||
export let variant: Variant = "default";
|
export let variant: Variant = 'default';
|
||||||
export { className as class };
|
export { className as class };
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:element
|
<svelte:element
|
||||||
this={href ? "a" : "span"}
|
this={href ? 'a' : 'span'}
|
||||||
{href}
|
{href}
|
||||||
class={cn(badgeVariants({ variant, className }))}
|
class={cn(badgeVariants({ variant, className }))}
|
||||||
{...$$restProps}
|
{...$$restProps}
|
||||||
|
|||||||
@@ -1,21 +1,20 @@
|
|||||||
import { type VariantProps, tv } from "tailwind-variants";
|
import { type VariantProps, tv } from 'tailwind-variants';
|
||||||
export { default as Badge } from "./badge.svelte";
|
export { default as Badge } from './badge.svelte';
|
||||||
|
|
||||||
export const badgeVariants = tv({
|
export const badgeVariants = tv({
|
||||||
base: "inline-flex select-none items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 break-keep whitespace-nowrap",
|
base: 'inline-flex select-none items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 break-keep whitespace-nowrap',
|
||||||
variants: {
|
variants: {
|
||||||
variant: {
|
variant: {
|
||||||
default: "border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
|
default: 'border-transparent bg-primary text-primary-foreground hover:bg-primary/80',
|
||||||
secondary:
|
secondary: 'border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80',
|
||||||
"border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
|
||||||
destructive:
|
destructive:
|
||||||
"border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
|
'border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80',
|
||||||
outline: "text-foreground",
|
outline: 'text-foreground'
|
||||||
},
|
}
|
||||||
},
|
},
|
||||||
defaultVariants: {
|
defaultVariants: {
|
||||||
variant: "default",
|
variant: 'default'
|
||||||
},
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
export type Variant = VariantProps<typeof badgeVariants>["variant"];
|
export type Variant = VariantProps<typeof badgeVariants>['variant'];
|
||||||
|
|||||||
@@ -1,34 +1,33 @@
|
|||||||
import { type VariantProps, tv } from "tailwind-variants";
|
import { type VariantProps, tv } from 'tailwind-variants';
|
||||||
import type { Button as ButtonPrimitive } from "bits-ui";
|
import type { Button as ButtonPrimitive } from 'bits-ui';
|
||||||
import Root from "./button.svelte";
|
import Root from './button.svelte';
|
||||||
|
|
||||||
const buttonVariants = tv({
|
const buttonVariants = tv({
|
||||||
base: "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
|
base: 'inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50',
|
||||||
variants: {
|
variants: {
|
||||||
variant: {
|
variant: {
|
||||||
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
default: 'bg-primary text-primary-foreground hover:bg-primary/90',
|
||||||
destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90",
|
destructive: 'bg-destructive text-destructive-foreground hover:bg-destructive/90',
|
||||||
outline:
|
outline: 'border border-input bg-background hover:bg-accent hover:text-accent-foreground',
|
||||||
"border border-input bg-background hover:bg-accent hover:text-accent-foreground",
|
secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
|
||||||
secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
ghost: 'hover:bg-accent hover:text-accent-foreground',
|
||||||
ghost: "hover:bg-accent hover:text-accent-foreground",
|
link: 'text-primary underline-offset-4 hover:underline'
|
||||||
link: "text-primary underline-offset-4 hover:underline",
|
|
||||||
},
|
},
|
||||||
size: {
|
size: {
|
||||||
default: "h-10 px-4 py-2",
|
default: 'h-10 px-4 py-2',
|
||||||
sm: "h-9 rounded-md px-3",
|
sm: 'h-9 rounded-md px-3',
|
||||||
lg: "h-11 rounded-md px-8",
|
lg: 'h-11 rounded-md px-8',
|
||||||
icon: "h-10 w-10",
|
icon: 'h-10 w-10'
|
||||||
},
|
}
|
||||||
},
|
},
|
||||||
defaultVariants: {
|
defaultVariants: {
|
||||||
variant: "default",
|
variant: 'default',
|
||||||
size: "default",
|
size: 'default'
|
||||||
},
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
type Variant = VariantProps<typeof buttonVariants>["variant"];
|
type Variant = VariantProps<typeof buttonVariants>['variant'];
|
||||||
type Size = VariantProps<typeof buttonVariants>["size"];
|
type Size = VariantProps<typeof buttonVariants>['size'];
|
||||||
|
|
||||||
type Props = ButtonPrimitive.Props & {
|
type Props = ButtonPrimitive.Props & {
|
||||||
variant?: Variant;
|
variant?: Variant;
|
||||||
@@ -46,5 +45,5 @@ export {
|
|||||||
Root as Button,
|
Root as Button,
|
||||||
type Props as ButtonProps,
|
type Props as ButtonProps,
|
||||||
type Events as ButtonEvents,
|
type Events as ButtonEvents,
|
||||||
buttonVariants,
|
buttonVariants
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { HTMLAttributes } from "svelte/elements";
|
import type { HTMLAttributes } from 'svelte/elements';
|
||||||
import { cn } from "$lib/utils/style.js";
|
import { cn } from '$lib/utils/style.js';
|
||||||
|
|
||||||
type $$Props = HTMLAttributes<HTMLDivElement>;
|
type $$Props = HTMLAttributes<HTMLDivElement>;
|
||||||
|
|
||||||
let className: $$Props["class"] = undefined;
|
let className: $$Props['class'] = undefined;
|
||||||
export { className as class };
|
export { className as class };
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class={cn("p-6 pt-0", className)} {...$$restProps}>
|
<div class={cn('p-6 pt-0', className)} {...$$restProps}>
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { HTMLAttributes } from "svelte/elements";
|
import type { HTMLAttributes } from 'svelte/elements';
|
||||||
import { cn } from "$lib/utils/style.js";
|
import { cn } from '$lib/utils/style.js';
|
||||||
|
|
||||||
type $$Props = HTMLAttributes<HTMLParagraphElement>;
|
type $$Props = HTMLAttributes<HTMLParagraphElement>;
|
||||||
|
|
||||||
let className: $$Props["class"] = undefined;
|
let className: $$Props['class'] = undefined;
|
||||||
export { className as class };
|
export { className as class };
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<p class={cn("text-sm text-muted-foreground", className)} {...$$restProps}>
|
<p class={cn('text-sm text-muted-foreground', className)} {...$$restProps}>
|
||||||
<slot />
|
<slot />
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { HTMLAttributes } from "svelte/elements";
|
import type { HTMLAttributes } from 'svelte/elements';
|
||||||
import { cn } from "$lib/utils/style.js";
|
import { cn } from '$lib/utils/style.js';
|
||||||
|
|
||||||
type $$Props = HTMLAttributes<HTMLDivElement>;
|
type $$Props = HTMLAttributes<HTMLDivElement>;
|
||||||
|
|
||||||
let className: $$Props["class"] = undefined;
|
let className: $$Props['class'] = undefined;
|
||||||
export { className as class };
|
export { className as class };
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class={cn("flex items-center p-6 pt-0", className)} {...$$restProps}>
|
<div class={cn('flex items-center p-6 pt-0', className)} {...$$restProps}>
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { HTMLAttributes } from "svelte/elements";
|
import type { HTMLAttributes } from 'svelte/elements';
|
||||||
import { cn } from "$lib/utils/style.js";
|
import { cn } from '$lib/utils/style.js';
|
||||||
|
|
||||||
type $$Props = HTMLAttributes<HTMLDivElement>;
|
type $$Props = HTMLAttributes<HTMLDivElement>;
|
||||||
|
|
||||||
let className: $$Props["class"] = undefined;
|
let className: $$Props['class'] = undefined;
|
||||||
export { className as class };
|
export { className as class };
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class={cn("flex flex-col space-y-1.5 p-6", className)} {...$$restProps}>
|
<div class={cn('flex flex-col space-y-1.5 p-6', className)} {...$$restProps}>
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,20 +1,20 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { HTMLAttributes } from "svelte/elements";
|
import type { HTMLAttributes } from 'svelte/elements';
|
||||||
import type { HeadingLevel } from "./index.js";
|
import type { HeadingLevel } from './index.js';
|
||||||
import { cn } from "$lib/utils/style.js";
|
import { cn } from '$lib/utils/style.js';
|
||||||
|
|
||||||
type $$Props = HTMLAttributes<HTMLHeadingElement> & {
|
type $$Props = HTMLAttributes<HTMLHeadingElement> & {
|
||||||
tag?: HeadingLevel;
|
tag?: HeadingLevel;
|
||||||
};
|
};
|
||||||
|
|
||||||
let className: $$Props["class"] = undefined;
|
let className: $$Props['class'] = undefined;
|
||||||
export let tag: $$Props["tag"] = "h3";
|
export let tag: $$Props['tag'] = 'h3';
|
||||||
export { className as class };
|
export { className as class };
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:element
|
<svelte:element
|
||||||
this={tag}
|
this={tag}
|
||||||
class={cn("text-xl font-semibold leading-none tracking-tight", className)}
|
class={cn('text-xl font-semibold leading-none tracking-tight', className)}
|
||||||
{...$$restProps}
|
{...$$restProps}
|
||||||
>
|
>
|
||||||
<slot />
|
<slot />
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { HTMLAttributes } from "svelte/elements";
|
import type { HTMLAttributes } from 'svelte/elements';
|
||||||
import { cn } from "$lib/utils/style.js";
|
import { cn } from '$lib/utils/style.js';
|
||||||
|
|
||||||
type $$Props = HTMLAttributes<HTMLDivElement>;
|
type $$Props = HTMLAttributes<HTMLDivElement>;
|
||||||
|
|
||||||
let className: $$Props["class"] = undefined;
|
let className: $$Props['class'] = undefined;
|
||||||
export { className as class };
|
export { className as class };
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class={cn("rounded-lg border bg-card text-card-foreground shadow-sm", className)}
|
class={cn('rounded-lg border bg-card text-card-foreground shadow-sm', className)}
|
||||||
{...$$restProps}
|
{...$$restProps}
|
||||||
>
|
>
|
||||||
<slot />
|
<slot />
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import Root from "./card.svelte";
|
import Root from './card.svelte';
|
||||||
import Content from "./card-content.svelte";
|
import Content from './card-content.svelte';
|
||||||
import Description from "./card-description.svelte";
|
import Description from './card-description.svelte';
|
||||||
import Footer from "./card-footer.svelte";
|
import Footer from './card-footer.svelte';
|
||||||
import Header from "./card-header.svelte";
|
import Header from './card-header.svelte';
|
||||||
import Title from "./card-title.svelte";
|
import Title from './card-title.svelte';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
Root,
|
Root,
|
||||||
@@ -18,7 +18,7 @@ export {
|
|||||||
Description as CardDescription,
|
Description as CardDescription,
|
||||||
Footer as CardFooter,
|
Footer as CardFooter,
|
||||||
Header as CardHeader,
|
Header as CardHeader,
|
||||||
Title as CardTitle,
|
Title as CardTitle
|
||||||
};
|
};
|
||||||
|
|
||||||
export type HeadingLevel = "h1" | "h2" | "h3" | "h4" | "h5" | "h6";
|
export type HeadingLevel = 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6';
|
||||||
|
|||||||
@@ -1,20 +1,20 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Checkbox as CheckboxPrimitive } from "bits-ui";
|
import { Checkbox as CheckboxPrimitive } from 'bits-ui';
|
||||||
import Check from "lucide-svelte/icons/check";
|
import Check from 'lucide-svelte/icons/check';
|
||||||
import Minus from "lucide-svelte/icons/minus";
|
import Minus from 'lucide-svelte/icons/minus';
|
||||||
import { cn } from "$lib/utils/style.js";
|
import { cn } from '$lib/utils/style.js';
|
||||||
|
|
||||||
type $$Props = CheckboxPrimitive.Props;
|
type $$Props = CheckboxPrimitive.Props;
|
||||||
type $$Events = CheckboxPrimitive.Events;
|
type $$Events = CheckboxPrimitive.Events;
|
||||||
|
|
||||||
let className: $$Props["class"] = undefined;
|
let className: $$Props['class'] = undefined;
|
||||||
export let checked: $$Props["checked"] = false;
|
export let checked: $$Props['checked'] = false;
|
||||||
export { className as class };
|
export { className as class };
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<CheckboxPrimitive.Root
|
<CheckboxPrimitive.Root
|
||||||
class={cn(
|
class={cn(
|
||||||
"peer box-content h-4 w-4 shrink-0 rounded-sm border border-primary ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[disabled=true]:cursor-not-allowed data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground data-[disabled=true]:opacity-50",
|
'peer box-content h-4 w-4 shrink-0 rounded-sm border border-primary ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[disabled=true]:cursor-not-allowed data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground data-[disabled=true]:opacity-50',
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
bind:checked
|
bind:checked
|
||||||
@@ -22,7 +22,7 @@
|
|||||||
on:click
|
on:click
|
||||||
>
|
>
|
||||||
<CheckboxPrimitive.Indicator
|
<CheckboxPrimitive.Indicator
|
||||||
class={cn("flex h-4 w-4 items-center justify-center text-current")}
|
class={cn('flex h-4 w-4 items-center justify-center text-current')}
|
||||||
let:isChecked
|
let:isChecked
|
||||||
let:isIndeterminate
|
let:isIndeterminate
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import Root from "./checkbox.svelte";
|
import Root from './checkbox.svelte';
|
||||||
export {
|
export {
|
||||||
Root,
|
Root,
|
||||||
//
|
//
|
||||||
Root as Checkbox,
|
Root as Checkbox
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Dialog as DialogPrimitive } from "bits-ui";
|
import { Dialog as DialogPrimitive } from 'bits-ui';
|
||||||
import X from "lucide-svelte/icons/x";
|
import X from 'lucide-svelte/icons/x';
|
||||||
import * as Dialog from "./index.js";
|
import * as Dialog from './index.js';
|
||||||
import { cn, flyAndScale } from "$lib/utils/style.js";
|
import { cn, flyAndScale } from '$lib/utils/style.js';
|
||||||
|
|
||||||
type $$Props = DialogPrimitive.ContentProps;
|
type $$Props = DialogPrimitive.ContentProps;
|
||||||
|
|
||||||
let className: $$Props["class"] = undefined;
|
let className: $$Props['class'] = undefined;
|
||||||
export let transition: $$Props["transition"] = flyAndScale;
|
export let transition: $$Props['transition'] = flyAndScale;
|
||||||
export let transitionConfig: $$Props["transitionConfig"] = {
|
export let transitionConfig: $$Props['transitionConfig'] = {
|
||||||
duration: 200,
|
duration: 200
|
||||||
};
|
};
|
||||||
export { className as class };
|
export { className as class };
|
||||||
</script>
|
</script>
|
||||||
@@ -20,14 +20,14 @@
|
|||||||
{transition}
|
{transition}
|
||||||
{transitionConfig}
|
{transitionConfig}
|
||||||
class={cn(
|
class={cn(
|
||||||
"bg-background fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border p-6 shadow-lg sm:rounded-lg md:w-full",
|
'fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg sm:rounded-lg md:w-full',
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...$$restProps}
|
{...$$restProps}
|
||||||
>
|
>
|
||||||
<slot />
|
<slot />
|
||||||
<DialogPrimitive.Close
|
<DialogPrimitive.Close
|
||||||
class="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute right-4 top-4 rounded-sm opacity-70 transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:pointer-events-none"
|
class="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground"
|
||||||
>
|
>
|
||||||
<X class="h-4 w-4" />
|
<X class="h-4 w-4" />
|
||||||
<span class="sr-only">Close</span>
|
<span class="sr-only">Close</span>
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Dialog as DialogPrimitive } from "bits-ui";
|
import { Dialog as DialogPrimitive } from 'bits-ui';
|
||||||
import { cn } from "$lib/utils/style.js";
|
import { cn } from '$lib/utils/style.js';
|
||||||
|
|
||||||
type $$Props = DialogPrimitive.DescriptionProps;
|
type $$Props = DialogPrimitive.DescriptionProps;
|
||||||
|
|
||||||
let className: $$Props["class"] = undefined;
|
let className: $$Props['class'] = undefined;
|
||||||
export { className as class };
|
export { className as class };
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<DialogPrimitive.Description
|
<DialogPrimitive.Description
|
||||||
class={cn("text-muted-foreground text-sm", className)}
|
class={cn('text-sm text-muted-foreground', className)}
|
||||||
{...$$restProps}
|
{...$$restProps}
|
||||||
>
|
>
|
||||||
<slot />
|
<slot />
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { HTMLAttributes } from "svelte/elements";
|
import type { HTMLAttributes } from 'svelte/elements';
|
||||||
import { cn } from "$lib/utils/style.js";
|
import { cn } from '$lib/utils/style.js';
|
||||||
|
|
||||||
type $$Props = HTMLAttributes<HTMLDivElement>;
|
type $$Props = HTMLAttributes<HTMLDivElement>;
|
||||||
|
|
||||||
let className: $$Props["class"] = undefined;
|
let className: $$Props['class'] = undefined;
|
||||||
export { className as class };
|
export { className as class };
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class={cn("flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", className)}
|
class={cn('flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2', className)}
|
||||||
{...$$restProps}
|
{...$$restProps}
|
||||||
>
|
>
|
||||||
<slot />
|
<slot />
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { HTMLAttributes } from "svelte/elements";
|
import type { HTMLAttributes } from 'svelte/elements';
|
||||||
import { cn } from "$lib/utils/style.js";
|
import { cn } from '$lib/utils/style.js';
|
||||||
|
|
||||||
type $$Props = HTMLAttributes<HTMLDivElement>;
|
type $$Props = HTMLAttributes<HTMLDivElement>;
|
||||||
|
|
||||||
let className: $$Props["class"] = undefined;
|
let className: $$Props['class'] = undefined;
|
||||||
export { className as class };
|
export { className as class };
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class={cn("flex flex-col space-y-1.5 text-center sm:text-left", className)} {...$$restProps}>
|
<div class={cn('flex flex-col space-y-1.5 text-center sm:text-left', className)} {...$$restProps}>
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Dialog as DialogPrimitive } from "bits-ui";
|
import { Dialog as DialogPrimitive } from 'bits-ui';
|
||||||
import { fade } from "svelte/transition";
|
import { fade } from 'svelte/transition';
|
||||||
import { cn } from "$lib/utils/style.js";
|
import { cn } from '$lib/utils/style.js';
|
||||||
|
|
||||||
type $$Props = DialogPrimitive.OverlayProps;
|
type $$Props = DialogPrimitive.OverlayProps;
|
||||||
|
|
||||||
let className: $$Props["class"] = undefined;
|
let className: $$Props['class'] = undefined;
|
||||||
export let transition: $$Props["transition"] = fade;
|
export let transition: $$Props['transition'] = fade;
|
||||||
export let transitionConfig: $$Props["transitionConfig"] = {
|
export let transitionConfig: $$Props['transitionConfig'] = {
|
||||||
duration: 150,
|
duration: 150
|
||||||
};
|
};
|
||||||
export { className as class };
|
export { className as class };
|
||||||
</script>
|
</script>
|
||||||
@@ -16,6 +16,6 @@
|
|||||||
<DialogPrimitive.Overlay
|
<DialogPrimitive.Overlay
|
||||||
{transition}
|
{transition}
|
||||||
{transitionConfig}
|
{transitionConfig}
|
||||||
class={cn("bg-background/80 fixed inset-0 z-50 backdrop-blur-sm", className)}
|
class={cn('fixed inset-0 z-50 bg-background/80 backdrop-blur-sm', className)}
|
||||||
{...$$restProps}
|
{...$$restProps}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Dialog as DialogPrimitive } from "bits-ui";
|
import { Dialog as DialogPrimitive } from 'bits-ui';
|
||||||
type $$Props = DialogPrimitive.PortalProps;
|
type $$Props = DialogPrimitive.PortalProps;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Dialog as DialogPrimitive } from "bits-ui";
|
import { Dialog as DialogPrimitive } from 'bits-ui';
|
||||||
import { cn } from "$lib/utils/style.js";
|
import { cn } from '$lib/utils/style.js';
|
||||||
|
|
||||||
type $$Props = DialogPrimitive.TitleProps;
|
type $$Props = DialogPrimitive.TitleProps;
|
||||||
|
|
||||||
let className: $$Props["class"] = undefined;
|
let className: $$Props['class'] = undefined;
|
||||||
export { className as class };
|
export { className as class };
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<DialogPrimitive.Title
|
<DialogPrimitive.Title
|
||||||
class={cn("text-lg font-semibold leading-none tracking-tight", className)}
|
class={cn('text-lg font-semibold leading-none tracking-tight', className)}
|
||||||
{...$$restProps}
|
{...$$restProps}
|
||||||
>
|
>
|
||||||
<slot />
|
<slot />
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import { Dialog as DialogPrimitive } from "bits-ui";
|
import { Dialog as DialogPrimitive } from 'bits-ui';
|
||||||
|
|
||||||
import Title from "./dialog-title.svelte";
|
import Title from './dialog-title.svelte';
|
||||||
import Portal from "./dialog-portal.svelte";
|
import Portal from './dialog-portal.svelte';
|
||||||
import Footer from "./dialog-footer.svelte";
|
import Footer from './dialog-footer.svelte';
|
||||||
import Header from "./dialog-header.svelte";
|
import Header from './dialog-header.svelte';
|
||||||
import Overlay from "./dialog-overlay.svelte";
|
import Overlay from './dialog-overlay.svelte';
|
||||||
import Content from "./dialog-content.svelte";
|
import Content from './dialog-content.svelte';
|
||||||
import Description from "./dialog-description.svelte";
|
import Description from './dialog-description.svelte';
|
||||||
|
|
||||||
const Root = DialogPrimitive.Root;
|
const Root = DialogPrimitive.Root;
|
||||||
const Trigger = DialogPrimitive.Trigger;
|
const Trigger = DialogPrimitive.Trigger;
|
||||||
@@ -33,5 +33,5 @@ export {
|
|||||||
Overlay as DialogOverlay,
|
Overlay as DialogOverlay,
|
||||||
Content as DialogContent,
|
Content as DialogContent,
|
||||||
Description as DialogDescription,
|
Description as DialogDescription,
|
||||||
Close as DialogClose,
|
Close as DialogClose
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,20 +1,20 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
|
import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui';
|
||||||
import Check from "lucide-svelte/icons/check";
|
import Check from 'lucide-svelte/icons/check';
|
||||||
import { cn } from "$lib/utils/style.js";
|
import { cn } from '$lib/utils/style.js';
|
||||||
|
|
||||||
type $$Props = DropdownMenuPrimitive.CheckboxItemProps;
|
type $$Props = DropdownMenuPrimitive.CheckboxItemProps;
|
||||||
type $$Events = DropdownMenuPrimitive.CheckboxItemEvents;
|
type $$Events = DropdownMenuPrimitive.CheckboxItemEvents;
|
||||||
|
|
||||||
let className: $$Props["class"] = undefined;
|
let className: $$Props['class'] = undefined;
|
||||||
export let checked: $$Props["checked"] = undefined;
|
export let checked: $$Props['checked'] = undefined;
|
||||||
export { className as class };
|
export { className as class };
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<DropdownMenuPrimitive.CheckboxItem
|
<DropdownMenuPrimitive.CheckboxItem
|
||||||
bind:checked
|
bind:checked
|
||||||
class={cn(
|
class={cn(
|
||||||
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none data-[disabled]:pointer-events-none data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground data-[disabled]:opacity-50",
|
'relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none data-[disabled]:pointer-events-none data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground data-[disabled]:opacity-50',
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...$$restProps}
|
{...$$restProps}
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
|
import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui';
|
||||||
import { cn, flyAndScale } from "$lib/utils/style.js";
|
import { cn, flyAndScale } from '$lib/utils/style.js';
|
||||||
|
|
||||||
type $$Props = DropdownMenuPrimitive.ContentProps;
|
type $$Props = DropdownMenuPrimitive.ContentProps;
|
||||||
type $$Events = DropdownMenuPrimitive.ContentEvents;
|
type $$Events = DropdownMenuPrimitive.ContentEvents;
|
||||||
|
|
||||||
let className: $$Props["class"] = undefined;
|
let className: $$Props['class'] = undefined;
|
||||||
export let sideOffset: $$Props["sideOffset"] = 4;
|
export let sideOffset: $$Props['sideOffset'] = 4;
|
||||||
export let transition: $$Props["transition"] = flyAndScale;
|
export let transition: $$Props['transition'] = flyAndScale;
|
||||||
export let transitionConfig: $$Props["transitionConfig"] = undefined;
|
export let transitionConfig: $$Props['transitionConfig'] = undefined;
|
||||||
export { className as class };
|
export { className as class };
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -17,7 +17,7 @@
|
|||||||
{transitionConfig}
|
{transitionConfig}
|
||||||
{sideOffset}
|
{sideOffset}
|
||||||
class={cn(
|
class={cn(
|
||||||
"z-50 min-w-[8rem] rounded-md border bg-popover p-1 text-popover-foreground shadow-md focus:outline-none",
|
'z-50 min-w-[8rem] rounded-md border bg-popover p-1 text-popover-foreground shadow-md focus:outline-none',
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...$$restProps}
|
{...$$restProps}
|
||||||
|
|||||||
@@ -1,21 +1,21 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
|
import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui';
|
||||||
import { cn } from "$lib/utils/style.js";
|
import { cn } from '$lib/utils/style.js';
|
||||||
|
|
||||||
type $$Props = DropdownMenuPrimitive.ItemProps & {
|
type $$Props = DropdownMenuPrimitive.ItemProps & {
|
||||||
inset?: boolean;
|
inset?: boolean;
|
||||||
};
|
};
|
||||||
type $$Events = DropdownMenuPrimitive.ItemEvents;
|
type $$Events = DropdownMenuPrimitive.ItemEvents;
|
||||||
|
|
||||||
let className: $$Props["class"] = undefined;
|
let className: $$Props['class'] = undefined;
|
||||||
export let inset: $$Props["inset"] = undefined;
|
export let inset: $$Props['inset'] = undefined;
|
||||||
export { className as class };
|
export { className as class };
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<DropdownMenuPrimitive.Item
|
<DropdownMenuPrimitive.Item
|
||||||
class={cn(
|
class={cn(
|
||||||
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[disabled]:pointer-events-none data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground data-[disabled]:opacity-50",
|
'relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[disabled]:pointer-events-none data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground data-[disabled]:opacity-50',
|
||||||
inset && "pl-8",
|
inset && 'pl-8',
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...$$restProps}
|
{...$$restProps}
|
||||||
|
|||||||
@@ -1,18 +1,18 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
|
import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui';
|
||||||
import { cn } from "$lib/utils/style.js";
|
import { cn } from '$lib/utils/style.js';
|
||||||
|
|
||||||
type $$Props = DropdownMenuPrimitive.LabelProps & {
|
type $$Props = DropdownMenuPrimitive.LabelProps & {
|
||||||
inset?: boolean;
|
inset?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
let className: $$Props["class"] = undefined;
|
let className: $$Props['class'] = undefined;
|
||||||
export let inset: $$Props["inset"] = undefined;
|
export let inset: $$Props['inset'] = undefined;
|
||||||
export { className as class };
|
export { className as class };
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<DropdownMenuPrimitive.Label
|
<DropdownMenuPrimitive.Label
|
||||||
class={cn("px-2 py-1.5 text-sm font-semibold", inset && "pl-8", className)}
|
class={cn('px-2 py-1.5 text-sm font-semibold', inset && 'pl-8', className)}
|
||||||
{...$$restProps}
|
{...$$restProps}
|
||||||
>
|
>
|
||||||
<slot />
|
<slot />
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
|
import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui';
|
||||||
|
|
||||||
type $$Props = DropdownMenuPrimitive.RadioGroupProps;
|
type $$Props = DropdownMenuPrimitive.RadioGroupProps;
|
||||||
|
|
||||||
export let value: $$Props["value"] = undefined;
|
export let value: $$Props['value'] = undefined;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<DropdownMenuPrimitive.RadioGroup {...$$restProps} bind:value>
|
<DropdownMenuPrimitive.RadioGroup {...$$restProps} bind:value>
|
||||||
|
|||||||
@@ -1,19 +1,19 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
|
import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui';
|
||||||
import Circle from "lucide-svelte/icons/circle";
|
import Circle from 'lucide-svelte/icons/circle';
|
||||||
import { cn } from "$lib/utils/style.js";
|
import { cn } from '$lib/utils/style.js';
|
||||||
|
|
||||||
type $$Props = DropdownMenuPrimitive.RadioItemProps;
|
type $$Props = DropdownMenuPrimitive.RadioItemProps;
|
||||||
type $$Events = DropdownMenuPrimitive.RadioItemEvents;
|
type $$Events = DropdownMenuPrimitive.RadioItemEvents;
|
||||||
|
|
||||||
let className: $$Props["class"] = undefined;
|
let className: $$Props['class'] = undefined;
|
||||||
export let value: $$Props["value"];
|
export let value: $$Props['value'];
|
||||||
export { className as class };
|
export { className as class };
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<DropdownMenuPrimitive.RadioItem
|
<DropdownMenuPrimitive.RadioItem
|
||||||
class={cn(
|
class={cn(
|
||||||
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none data-[disabled]:pointer-events-none data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground data-[disabled]:opacity-50",
|
'relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none data-[disabled]:pointer-events-none data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground data-[disabled]:opacity-50',
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{value}
|
{value}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user