mirror of
https://github.com/pocket-id/pocket-id.git
synced 2025-12-13 00:33:06 +03:00
feat: add LDAP sync (#106)
Co-authored-by: Elias Schneider <login@eliasschneider.com>
This commit is contained in:
@@ -30,7 +30,7 @@ Additionally, what makes Pocket ID special is that it only supports [passkey](ht
|
|||||||
|
|
||||||
## Setup
|
## Setup
|
||||||
|
|
||||||
> [!WARNING]
|
> [!WARNING]
|
||||||
> Pocket ID is in its early stages and may contain bugs. There might be OIDC features that are not yet implemented. If you encounter any issues, please open an issue.
|
> Pocket ID is in its early stages and may contain bugs. There might be OIDC features that are not yet implemented. If you encounter any issues, please open an issue.
|
||||||
|
|
||||||
### Before you start
|
### Before you start
|
||||||
@@ -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:
|
||||||
|
|||||||
@@ -5,4 +5,4 @@ SQLITE_DB_PATH=data/pocket-id.db
|
|||||||
POSTGRES_CONNECTION_STRING=postgresql://postgres:postgres@localhost:5432/pocket-id
|
POSTGRES_CONNECTION_STRING=postgresql://postgres:postgres@localhost:5432/pocket-id
|
||||||
UPLOAD_PATH=data/uploads
|
UPLOAD_PATH=data/uploads
|
||||||
PORT=8080
|
PORT=8080
|
||||||
HOST=localhost
|
HOST=localhost
|
||||||
|
|||||||
3
backend/.gitignore
vendored
3
backend/.gitignore
vendored
@@ -13,4 +13,5 @@
|
|||||||
|
|
||||||
# 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=
|
||||||
|
|||||||
@@ -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"
|
||||||
@@ -42,12 +43,15 @@ func initRouter(db *gorm.DB, appConfigService *service.AppConfigService) {
|
|||||||
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)
|
||||||
|
|
||||||
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(middleware.NewRateLimitMiddleware().Add(rate.Every(time.Second), 60))
|
||||||
r.Use(middleware.NewJwtAuthMiddleware(jwtService, true).Add(false))
|
r.Use(middleware.NewJwtAuthMiddleware(jwtService, true).Add(false))
|
||||||
|
|
||||||
|
job.RegisterLdapJobs(ldapService, appConfigService)
|
||||||
|
|
||||||
// Initialize middleware
|
// Initialize middleware
|
||||||
jwtAuthMiddleware := middleware.NewJwtAuthMiddleware(jwtService, false)
|
jwtAuthMiddleware := middleware.NewJwtAuthMiddleware(jwtService, false)
|
||||||
fileSizeLimitMiddleware := middleware.NewFileSizeLimitMiddleware()
|
fileSizeLimitMiddleware := middleware.NewFileSizeLimitMiddleware()
|
||||||
@@ -57,7 +61,7 @@ func initRouter(db *gorm.DB, appConfigService *service.AppConfigService) {
|
|||||||
controller.NewWebauthnController(apiGroup, jwtAuthMiddleware, middleware.NewRateLimitMiddleware(), webauthnService, appConfigService)
|
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{}
|
||||||
@@ -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,8 +11,6 @@ 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) {
|
||||||
@@ -201,7 +202,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
|
||||||
|
|||||||
@@ -12,16 +12,30 @@ type AppConfigVariableDto struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type AppConfigUpdateDto struct {
|
type AppConfigUpdateDto struct {
|
||||||
AppName string `json:"appName" binding:"required,min=1,max=30"`
|
AppName string `json:"appName" binding:"required,min=1,max=30"`
|
||||||
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"`
|
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"`
|
||||||
SmtpUser string `json:"smtpUser"`
|
SmtpUser string `json:"smtpUser"`
|
||||||
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"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,6 +19,7 @@ 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 {
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
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.Fatalf("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
|
||||||
|
}
|
||||||
@@ -10,15 +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
|
EmailEnabled AppConfigVariable
|
||||||
SmtpHost AppConfigVariable
|
SmtpHost AppConfigVariable
|
||||||
SmtpPort AppConfigVariable
|
SmtpPort AppConfigVariable
|
||||||
@@ -27,4 +28,19 @@ type AppConfig struct {
|
|||||||
SmtpPassword AppConfigVariable
|
SmtpPassword AppConfigVariable
|
||||||
SmtpTls AppConfigVariable
|
SmtpTls AppConfigVariable
|
||||||
SmtpSkipCertVerify AppConfigVariable
|
SmtpSkipCertVerify 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,6 +73,7 @@ var defaultDbConfig = model.AppConfig{
|
|||||||
IsInternal: true,
|
IsInternal: true,
|
||||||
DefaultValue: "svg",
|
DefaultValue: "svg",
|
||||||
},
|
},
|
||||||
|
// Email
|
||||||
EmailEnabled: model.AppConfigVariable{
|
EmailEnabled: model.AppConfigVariable{
|
||||||
Key: "emailEnabled",
|
Key: "emailEnabled",
|
||||||
Type: "bool",
|
Type: "bool",
|
||||||
@@ -105,6 +109,65 @@ var defaultDbConfig = model.AppConfig{
|
|||||||
Type: "bool",
|
Type: "bool",
|
||||||
DefaultValue: "false",
|
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) {
|
||||||
|
|||||||
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) {
|
||||||
|
|||||||
@@ -46,6 +46,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 +60,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 +71,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
|
||||||
|
|||||||
@@ -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,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);
|
||||||
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",
|
||||||
|
|||||||
@@ -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,6 +27,7 @@
|
|||||||
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 }[];
|
||||||
@@ -122,7 +124,11 @@
|
|||||||
<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}
|
||||||
@@ -160,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)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -38,7 +40,7 @@
|
|||||||
{#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>
|
||||||
|
|||||||
@@ -57,6 +57,10 @@ export default class AppConfigService extends APIService {
|
|||||||
await this.api.post('/application-configuration/test-email');
|
await this.api.post('/application-configuration/test-email');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async syncLdap() {
|
||||||
|
await this.api.post('/application-configuration/sync-ldap');
|
||||||
|
}
|
||||||
|
|
||||||
async getVersionInformation() {
|
async getVersionInformation() {
|
||||||
const response = (
|
const response = (
|
||||||
await axios.get('https://api.github.com/repos/stonith404/pocket-id/releases/latest')
|
await axios.get('https://api.github.com/repos/stonith404/pocket-id/releases/latest')
|
||||||
|
|||||||
@@ -4,8 +4,10 @@ export type AppConfig = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type AllAppConfig = AppConfig & {
|
export type AllAppConfig = AppConfig & {
|
||||||
|
// General
|
||||||
sessionDuration: number;
|
sessionDuration: number;
|
||||||
emailsVerified: boolean;
|
emailsVerified: boolean;
|
||||||
|
// Email
|
||||||
emailEnabled: boolean;
|
emailEnabled: boolean;
|
||||||
smtpHost: string;
|
smtpHost: string;
|
||||||
smtpPort: number;
|
smtpPort: number;
|
||||||
@@ -14,6 +16,21 @@ export type AllAppConfig = AppConfig & {
|
|||||||
smtpPassword: string;
|
smtpPassword: string;
|
||||||
smtpTls: boolean;
|
smtpTls: boolean;
|
||||||
smtpSkipCertVerify: boolean;
|
smtpSkipCertVerify: boolean;
|
||||||
|
// LDAP
|
||||||
|
ldapEnabled: boolean;
|
||||||
|
ldapUrl: string;
|
||||||
|
ldapBindDn: string;
|
||||||
|
ldapBindPassword: string;
|
||||||
|
ldapBase: string;
|
||||||
|
ldapSkipCertVerify: boolean;
|
||||||
|
ldapAttributeUserUniqueIdentifier: string;
|
||||||
|
ldapAttributeUserUsername: string;
|
||||||
|
ldapAttributeUserEmail: string;
|
||||||
|
ldapAttributeUserFirstName: string;
|
||||||
|
ldapAttributeUserLastName: string;
|
||||||
|
ldapAttributeGroupUniqueIdentifier: string;
|
||||||
|
ldapAttributeGroupName: string;
|
||||||
|
ldapAttributeAdminGroup: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type AppConfigRawResponse = {
|
export type AppConfigRawResponse = {
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ export type UserGroup = {
|
|||||||
name: string;
|
name: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
customClaims: CustomClaim[];
|
customClaims: CustomClaim[];
|
||||||
|
ldapId?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type UserGroupWithUsers = UserGroup & {
|
export type UserGroupWithUsers = UserGroup & {
|
||||||
@@ -17,4 +18,4 @@ export type UserGroupWithUserCount = UserGroup & {
|
|||||||
userCount: number;
|
userCount: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type UserGroupCreate = Pick<UserGroup, 'friendlyName' | 'name'>;
|
export type UserGroupCreate = Pick<UserGroup, 'friendlyName' | 'name' | 'ldapId'>;
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ export type User = {
|
|||||||
lastName: string;
|
lastName: string;
|
||||||
isAdmin: boolean;
|
isAdmin: boolean;
|
||||||
customClaims: CustomClaim[];
|
customClaims: CustomClaim[];
|
||||||
|
ldapId?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type UserCreate = Omit<User, 'id' | 'customClaims'>;
|
export type UserCreate = Omit<User, 'id' | 'customClaims' | 'ldapId'>;
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
import { toast } from 'svelte-sonner';
|
import { toast } from 'svelte-sonner';
|
||||||
import AppConfigEmailForm from './forms/app-config-email-form.svelte';
|
import AppConfigEmailForm from './forms/app-config-email-form.svelte';
|
||||||
import AppConfigGeneralForm from './forms/app-config-general-form.svelte';
|
import AppConfigGeneralForm from './forms/app-config-general-form.svelte';
|
||||||
|
import AppConfigLdapForm from './forms/app-config-ldap-form.svelte';
|
||||||
import UpdateApplicationImages from './update-application-images.svelte';
|
import UpdateApplicationImages from './update-application-images.svelte';
|
||||||
|
|
||||||
let { data } = $props();
|
let { data } = $props();
|
||||||
@@ -34,8 +35,12 @@
|
|||||||
favicon: File | null
|
favicon: File | null
|
||||||
) {
|
) {
|
||||||
const faviconPromise = favicon ? appConfigService.updateFavicon(favicon) : Promise.resolve();
|
const faviconPromise = favicon ? appConfigService.updateFavicon(favicon) : Promise.resolve();
|
||||||
const lightLogoPromise = logoLight ? appConfigService.updateLogo(logoLight, true) : Promise.resolve();
|
const lightLogoPromise = logoLight
|
||||||
const darkLogoPromise = logoDark ? appConfigService.updateLogo(logoDark, false) : Promise.resolve();
|
? appConfigService.updateLogo(logoLight, true)
|
||||||
|
: Promise.resolve();
|
||||||
|
const darkLogoPromise = logoDark
|
||||||
|
? appConfigService.updateLogo(logoDark, false)
|
||||||
|
: Promise.resolve();
|
||||||
const backgroundImagePromise = backgroundImage
|
const backgroundImagePromise = backgroundImage
|
||||||
? appConfigService.updateBackgroundImage(backgroundImage)
|
? appConfigService.updateBackgroundImage(backgroundImage)
|
||||||
: Promise.resolve();
|
: Promise.resolve();
|
||||||
@@ -72,6 +77,18 @@
|
|||||||
</Card.Content>
|
</Card.Content>
|
||||||
</Card.Root>
|
</Card.Root>
|
||||||
|
|
||||||
|
<Card.Root>
|
||||||
|
<Card.Header>
|
||||||
|
<Card.Title>LDAP</Card.Title>
|
||||||
|
<Card.Description>
|
||||||
|
Configure LDAP settings to sync users and groups from an LDAP server.
|
||||||
|
</Card.Description>
|
||||||
|
</Card.Header>
|
||||||
|
<Card.Content>
|
||||||
|
<AppConfigLdapForm {appConfig} callback={updateAppConfig} />
|
||||||
|
</Card.Content>
|
||||||
|
</Card.Root>
|
||||||
|
|
||||||
<Card.Root>
|
<Card.Root>
|
||||||
<Card.Header>
|
<Card.Header>
|
||||||
<Card.Title>Images</Card.Title>
|
<Card.Title>Images</Card.Title>
|
||||||
|
|||||||
@@ -45,7 +45,6 @@
|
|||||||
const { inputs, ...form } = createForm<typeof formSchema>(formSchema, updatedAppConfig);
|
const { inputs, ...form } = createForm<typeof formSchema>(formSchema, updatedAppConfig);
|
||||||
|
|
||||||
async function onSubmit() {
|
async function onSubmit() {
|
||||||
console.log('submit');
|
|
||||||
const data = form.validate();
|
const data = form.validate();
|
||||||
if (!data) return false;
|
if (!data) return false;
|
||||||
await callback({
|
await callback({
|
||||||
|
|||||||
@@ -0,0 +1,169 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import CheckboxWithLabel from '$lib/components/checkbox-with-label.svelte';
|
||||||
|
import FormInput from '$lib/components/form-input.svelte';
|
||||||
|
import { Button } from '$lib/components/ui/button';
|
||||||
|
import AppConfigService from '$lib/services/app-config-service';
|
||||||
|
import type { AllAppConfig } from '$lib/types/application-configuration';
|
||||||
|
import { axiosErrorToast } from '$lib/utils/error-util';
|
||||||
|
import { createForm } from '$lib/utils/form-util';
|
||||||
|
import { toast } from 'svelte-sonner';
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
let {
|
||||||
|
callback,
|
||||||
|
appConfig
|
||||||
|
}: {
|
||||||
|
appConfig: AllAppConfig;
|
||||||
|
callback: (appConfig: Partial<AllAppConfig>) => Promise<void>;
|
||||||
|
} = $props();
|
||||||
|
|
||||||
|
const appConfigService = new AppConfigService();
|
||||||
|
|
||||||
|
let ldapEnabled = $state(appConfig.ldapEnabled);
|
||||||
|
let ldapSyncing = $state(false);
|
||||||
|
|
||||||
|
const updatedAppConfig = {
|
||||||
|
ldapEnabled: appConfig.ldapEnabled,
|
||||||
|
ldapUrl: appConfig.ldapUrl,
|
||||||
|
ldapBindDn: appConfig.ldapBindDn,
|
||||||
|
ldapBindPassword: appConfig.ldapBindPassword,
|
||||||
|
ldapBase: appConfig.ldapBase,
|
||||||
|
ldapSkipCertVerify: appConfig.ldapSkipCertVerify,
|
||||||
|
ldapAttributeUserUniqueIdentifier: appConfig.ldapAttributeUserUniqueIdentifier,
|
||||||
|
ldapAttributeUserUsername: appConfig.ldapAttributeUserUsername,
|
||||||
|
ldapAttributeUserEmail: appConfig.ldapAttributeUserEmail,
|
||||||
|
ldapAttributeUserFirstName: appConfig.ldapAttributeUserFirstName,
|
||||||
|
ldapAttributeUserLastName: appConfig.ldapAttributeUserLastName,
|
||||||
|
ldapAttributeGroupUniqueIdentifier: appConfig.ldapAttributeGroupUniqueIdentifier,
|
||||||
|
ldapAttributeGroupName: appConfig.ldapAttributeGroupName,
|
||||||
|
ldapAttributeAdminGroup: appConfig.ldapAttributeAdminGroup
|
||||||
|
};
|
||||||
|
|
||||||
|
const formSchema = z.object({
|
||||||
|
ldapUrl: z.string().url(),
|
||||||
|
ldapBindDn: z.string().min(1),
|
||||||
|
ldapBindPassword: z.string().min(1),
|
||||||
|
ldapBase: z.string().min(1),
|
||||||
|
ldapSkipCertVerify: z.boolean(),
|
||||||
|
ldapAttributeUserUniqueIdentifier: z.string().min(1),
|
||||||
|
ldapAttributeUserUsername: z.string().min(1),
|
||||||
|
ldapAttributeUserEmail: z.string().min(1),
|
||||||
|
ldapAttributeUserFirstName: z.string().min(1),
|
||||||
|
ldapAttributeUserLastName: z.string().min(1),
|
||||||
|
ldapAttributeGroupUniqueIdentifier: z.string().min(1),
|
||||||
|
ldapAttributeGroupName: z.string().min(1),
|
||||||
|
ldapAttributeAdminGroup: z.string()
|
||||||
|
});
|
||||||
|
|
||||||
|
const { inputs, ...form } = createForm<typeof formSchema>(formSchema, updatedAppConfig);
|
||||||
|
|
||||||
|
async function onSubmit() {
|
||||||
|
const data = form.validate();
|
||||||
|
if (!data) return false;
|
||||||
|
await callback({
|
||||||
|
...data,
|
||||||
|
ldapEnabled: true
|
||||||
|
});
|
||||||
|
toast.success('LDAP configuration updated successfully');
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onDisable() {
|
||||||
|
ldapEnabled = false;
|
||||||
|
await callback({ ldapEnabled });
|
||||||
|
toast.success('LDAP disabled successfully');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onEnable() {
|
||||||
|
if (await onSubmit()) {
|
||||||
|
ldapEnabled = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function syncLdap() {
|
||||||
|
ldapSyncing = true;
|
||||||
|
await appConfigService.syncLdap()
|
||||||
|
.then(()=> toast.success('LDAP sync finished'))
|
||||||
|
.catch(axiosErrorToast);
|
||||||
|
|
||||||
|
ldapSyncing = false;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<form onsubmit={onSubmit}>
|
||||||
|
<h4 class="text-lg font-semibold">Client Configuration</h4>
|
||||||
|
<div class="mt-4 grid grid-cols-1 items-start gap-5 md:grid-cols-2">
|
||||||
|
<FormInput label="LDAP URL" placeholder="ldap://example.com:389" bind:input={$inputs.ldapUrl} />
|
||||||
|
<FormInput
|
||||||
|
label="LDAP Bind DN"
|
||||||
|
placeholder="cn=people,dc=example,dc=com"
|
||||||
|
bind:input={$inputs.ldapBindDn}
|
||||||
|
/>
|
||||||
|
<FormInput label="LDAP Bind Password" type="password" bind:input={$inputs.ldapBindPassword} />
|
||||||
|
<FormInput label="LDAP Base DN" placeholder="dc=example,dc=com" bind:input={$inputs.ldapBase} />
|
||||||
|
<CheckboxWithLabel
|
||||||
|
id="skip-cert-verify"
|
||||||
|
label="Skip Certificate Verification"
|
||||||
|
description="This can be useful for self-signed certificates."
|
||||||
|
bind:checked={$inputs.ldapSkipCertVerify.value}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<h4 class="mt-10 text-lg font-semibold">Attribute Mapping</h4>
|
||||||
|
<div class="mt-4 grid grid-cols-1 items-end gap-5 md:grid-cols-2">
|
||||||
|
<FormInput
|
||||||
|
label="User Unique Identifier Attribute"
|
||||||
|
description="The value of this attribute should never change."
|
||||||
|
placeholder="uuid"
|
||||||
|
bind:input={$inputs.ldapAttributeUserUniqueIdentifier}
|
||||||
|
/>
|
||||||
|
<FormInput
|
||||||
|
label="Username Attribute"
|
||||||
|
placeholder="uid"
|
||||||
|
bind:input={$inputs.ldapAttributeUserUsername}
|
||||||
|
/>
|
||||||
|
<FormInput
|
||||||
|
label="User Mail Attribute"
|
||||||
|
placeholder="mail"
|
||||||
|
bind:input={$inputs.ldapAttributeUserEmail}
|
||||||
|
/>
|
||||||
|
<FormInput
|
||||||
|
label="User First Name Attribute"
|
||||||
|
placeholder="givenName"
|
||||||
|
bind:input={$inputs.ldapAttributeUserFirstName}
|
||||||
|
/>
|
||||||
|
<FormInput
|
||||||
|
label="User Last Name Attribute"
|
||||||
|
placeholder="sn"
|
||||||
|
bind:input={$inputs.ldapAttributeUserLastName}
|
||||||
|
/>
|
||||||
|
<FormInput
|
||||||
|
label="Group Unique Identifier Attribute"
|
||||||
|
description="The value of this attribute should never change."
|
||||||
|
placeholder="uuid"
|
||||||
|
bind:input={$inputs.ldapAttributeGroupUniqueIdentifier}
|
||||||
|
/>
|
||||||
|
<FormInput
|
||||||
|
label="Group Name Attribute"
|
||||||
|
placeholder="cn"
|
||||||
|
bind:input={$inputs.ldapAttributeGroupName}
|
||||||
|
/>
|
||||||
|
<FormInput
|
||||||
|
label="Admin Group Name"
|
||||||
|
description="Members of this group will have Admin Privileges in Pocket ID."
|
||||||
|
placeholder="_admin_group_name"
|
||||||
|
bind:input={$inputs.ldapAttributeAdminGroup}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-8 flex flex-wrap justify-end gap-3">
|
||||||
|
{#if ldapEnabled}
|
||||||
|
<Button variant="secondary" onclick={onDisable}>Disable</Button>
|
||||||
|
<Button variant="secondary" onclick={syncLdap} isLoading={ldapSyncing}>Sync now</Button>
|
||||||
|
<Button type="submit">Save</Button>
|
||||||
|
{:else}
|
||||||
|
<Button onclick={onEnable}>Enable</Button>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import CustomClaimsInput from '$lib/components/custom-claims-input.svelte';
|
import CustomClaimsInput from '$lib/components/custom-claims-input.svelte';
|
||||||
|
import { Badge } from '$lib/components/ui/badge';
|
||||||
import { Button } from '$lib/components/ui/button';
|
import { Button } from '$lib/components/ui/button';
|
||||||
import * as Card from '$lib/components/ui/card';
|
import * as Card from '$lib/components/ui/card';
|
||||||
import CustomClaimService from '$lib/services/custom-claim-service';
|
import CustomClaimService from '$lib/services/custom-claim-service';
|
||||||
@@ -58,10 +59,13 @@
|
|||||||
<title>User Group Details {userGroup.name}</title>
|
<title>User Group Details {userGroup.name}</title>
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
<div>
|
<div class="flex items-center justify-between">
|
||||||
<a class="text-muted-foreground flex text-sm" href="/settings/admin/user-groups"
|
<a class="text-muted-foreground flex text-sm" href="/settings/admin/user-groups"
|
||||||
><LucideChevronLeft class="h-5 w-5" /> Back</a
|
><LucideChevronLeft class="h-5 w-5" /> Back</a
|
||||||
>
|
>
|
||||||
|
{#if !!userGroup.ldapId}
|
||||||
|
<Badge variant="default" class="">LDAP</Badge>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<Card.Root>
|
<Card.Root>
|
||||||
<Card.Header>
|
<Card.Header>
|
||||||
@@ -81,10 +85,17 @@
|
|||||||
|
|
||||||
<Card.Content>
|
<Card.Content>
|
||||||
{#await userService.list() then users}
|
{#await userService.list() then users}
|
||||||
<UserSelection {users} bind:selectedUserIds={userGroup.userIds} />
|
<UserSelection
|
||||||
|
{users}
|
||||||
|
bind:selectedUserIds={userGroup.userIds}
|
||||||
|
selectionDisabled={!!userGroup.ldapId}
|
||||||
|
/>
|
||||||
{/await}
|
{/await}
|
||||||
<div class="mt-5 flex justify-end">
|
<div class="mt-5 flex justify-end">
|
||||||
<Button on:click={() => updateUserGroupUsers(userGroup.userIds)}>Save</Button>
|
<Button
|
||||||
|
disabled={!!userGroup.ldapId}
|
||||||
|
on:click={() => updateUserGroupUsers(userGroup.userIds)}>Save</Button
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
</Card.Content>
|
</Card.Content>
|
||||||
</Card.Root>
|
</Card.Root>
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
} = $props();
|
} = $props();
|
||||||
|
|
||||||
let isLoading = $state(false);
|
let isLoading = $state(false);
|
||||||
|
let inputDisabled = $derived(!!existingUserGroup?.ldapId);
|
||||||
let hasManualNameEdit = $state(!!existingUserGroup?.friendlyName);
|
let hasManualNameEdit = $state(!!existingUserGroup?.friendlyName);
|
||||||
|
|
||||||
const userGroup = {
|
const userGroup = {
|
||||||
@@ -23,10 +24,7 @@
|
|||||||
|
|
||||||
const formSchema = z.object({
|
const formSchema = z.object({
|
||||||
friendlyName: z.string().min(2).max(50),
|
friendlyName: z.string().min(2).max(50),
|
||||||
name: z
|
name: z.string().min(2).max(255)
|
||||||
.string()
|
|
||||||
.min(2)
|
|
||||||
.max(255)
|
|
||||||
});
|
});
|
||||||
type FormSchema = typeof formSchema;
|
type FormSchema = typeof formSchema;
|
||||||
|
|
||||||
@@ -57,25 +55,27 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<form onsubmit={onSubmit}>
|
<form onsubmit={onSubmit}>
|
||||||
<div class="flex flex-col gap-3 sm:flex-row">
|
<fieldset disabled={inputDisabled}>
|
||||||
<div class="w-full">
|
<div class="flex flex-col gap-3 sm:flex-row">
|
||||||
<FormInput
|
<div class="w-full">
|
||||||
label="Friendly Name"
|
<FormInput
|
||||||
description="Name that will be displayed in the UI"
|
label="Friendly Name"
|
||||||
bind:input={$inputs.friendlyName}
|
description="Name that will be displayed in the UI"
|
||||||
onInput={onFriendlyNameInput}
|
bind:input={$inputs.friendlyName}
|
||||||
/>
|
onInput={onFriendlyNameInput}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="w-full">
|
||||||
|
<FormInput
|
||||||
|
label="Name"
|
||||||
|
description={`Name that will be in the "groups" claim`}
|
||||||
|
bind:input={$inputs.name}
|
||||||
|
onInput={onNameInput}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="w-full">
|
<div class="mt-5 flex justify-end">
|
||||||
<FormInput
|
<Button {isLoading} type="submit">Save</Button>
|
||||||
label="Name"
|
|
||||||
description={`Name that will be in the "groups" claim`}
|
|
||||||
bind:input={$inputs.name}
|
|
||||||
onInput={onNameInput}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</fieldset>
|
||||||
<div class="mt-5 flex justify-end">
|
|
||||||
<Button {isLoading} type="submit">Save</Button>
|
|
||||||
</div>
|
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -68,11 +68,13 @@
|
|||||||
<DropdownMenu.Item href="/settings/admin/user-groups/{item.id}"
|
<DropdownMenu.Item href="/settings/admin/user-groups/{item.id}"
|
||||||
><LucidePencil class="mr-2 h-4 w-4" /> Edit</DropdownMenu.Item
|
><LucidePencil class="mr-2 h-4 w-4" /> Edit</DropdownMenu.Item
|
||||||
>
|
>
|
||||||
<DropdownMenu.Item
|
{#if !item.ldapId}
|
||||||
class="text-red-500 focus:!text-red-700"
|
<DropdownMenu.Item
|
||||||
on:click={() => deleteUserGroup(item)}
|
class="text-red-500 focus:!text-red-700"
|
||||||
><LucideTrash class="mr-2 h-4 w-4" />Delete</DropdownMenu.Item
|
on:click={() => deleteUserGroup(item)}
|
||||||
>
|
><LucideTrash class="mr-2 h-4 w-4" />Delete</DropdownMenu.Item
|
||||||
|
>
|
||||||
|
{/if}
|
||||||
</DropdownMenu.Content>
|
</DropdownMenu.Content>
|
||||||
</DropdownMenu.Root>
|
</DropdownMenu.Root>
|
||||||
</Table.Cell>
|
</Table.Cell>
|
||||||
|
|||||||
@@ -7,8 +7,11 @@
|
|||||||
|
|
||||||
let {
|
let {
|
||||||
users: initialUsers,
|
users: initialUsers,
|
||||||
|
selectionDisabled = false,
|
||||||
selectedUserIds = $bindable()
|
selectedUserIds = $bindable()
|
||||||
}: { users: Paginated<User>; selectedUserIds: string[] } = $props();
|
}: { users: Paginated<User>;
|
||||||
|
selectionDisabled?: boolean;
|
||||||
|
selectedUserIds: string[] } = $props();
|
||||||
|
|
||||||
const userService = new UserService();
|
const userService = new UserService();
|
||||||
|
|
||||||
@@ -23,6 +26,7 @@
|
|||||||
{ label: 'Email', sortColumn: 'email' }
|
{ label: 'Email', sortColumn: 'email' }
|
||||||
]}
|
]}
|
||||||
bind:selectedIds={selectedUserIds}
|
bind:selectedIds={selectedUserIds}
|
||||||
|
{selectionDisabled}
|
||||||
>
|
>
|
||||||
{#snippet rows({ item })}
|
{#snippet rows({ item })}
|
||||||
<Table.Cell>{item.firstName} {item.lastName}</Table.Cell>
|
<Table.Cell>{item.firstName} {item.lastName}</Table.Cell>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import Badge from '$lib/components/ui/badge/badge.svelte';
|
||||||
import { Button } from '$lib/components/ui/button';
|
import { Button } from '$lib/components/ui/button';
|
||||||
import * as Card from '$lib/components/ui/card';
|
import * as Card from '$lib/components/ui/card';
|
||||||
import CustomClaimService from '$lib/services/custom-claim-service';
|
import CustomClaimService from '$lib/services/custom-claim-service';
|
||||||
@@ -43,10 +44,13 @@
|
|||||||
<title>User Details {user.firstName} {user.lastName}</title>
|
<title>User Details {user.firstName} {user.lastName}</title>
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
<div>
|
<div class="flex items-center justify-between">
|
||||||
<a class="text-muted-foreground flex text-sm" href="/settings/admin/users"
|
<a class="text-muted-foreground flex text-sm" href="/settings/admin/users"
|
||||||
><LucideChevronLeft class="h-5 w-5" /> Back</a
|
><LucideChevronLeft class="h-5 w-5" /> Back</a
|
||||||
>
|
>
|
||||||
|
{#if !!user.ldapId}
|
||||||
|
<Badge variant="default" class="">LDAP</Badge>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<Card.Root>
|
<Card.Root>
|
||||||
<Card.Header>
|
<Card.Header>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
import CheckboxWithLabel from '$lib/components/checkbox-with-label.svelte';
|
import CheckboxWithLabel from '$lib/components/checkbox-with-label.svelte';
|
||||||
import FormInput from '$lib/components/form-input.svelte';
|
import FormInput from '$lib/components/form-input.svelte';
|
||||||
import { Button } from '$lib/components/ui/button';
|
import { Button } from '$lib/components/ui/button';
|
||||||
import type { UserCreate } from '$lib/types/user.type';
|
import type { User, UserCreate } from '$lib/types/user.type';
|
||||||
import { createForm } from '$lib/utils/form-util';
|
import { createForm } from '$lib/utils/form-util';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
@@ -10,11 +10,12 @@
|
|||||||
callback,
|
callback,
|
||||||
existingUser
|
existingUser
|
||||||
}: {
|
}: {
|
||||||
existingUser?: UserCreate;
|
existingUser?: User;
|
||||||
callback: (user: UserCreate) => Promise<boolean>;
|
callback: (user: UserCreate) => Promise<boolean>;
|
||||||
} = $props();
|
} = $props();
|
||||||
|
|
||||||
let isLoading = $state(false);
|
let isLoading = $state(false);
|
||||||
|
let inputDisabled = $derived(!!existingUser?.ldapId);
|
||||||
|
|
||||||
const user = {
|
const user = {
|
||||||
firstName: existingUser?.firstName || '',
|
firstName: existingUser?.firstName || '',
|
||||||
@@ -53,29 +54,21 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<form onsubmit={onSubmit}>
|
<form onsubmit={onSubmit}>
|
||||||
<div class="flex flex-col gap-3 sm:flex-row">
|
<fieldset disabled={inputDisabled}>
|
||||||
<div class="w-full">
|
<div class="grid grid-cols-1 items-start gap-5 md:grid-cols-2">
|
||||||
<FormInput label="First name" bind:input={$inputs.firstName} />
|
<FormInput label="First name" bind:input={$inputs.firstName} />
|
||||||
</div>
|
|
||||||
<div class="w-full">
|
|
||||||
<FormInput label="Last name" bind:input={$inputs.lastName} />
|
<FormInput label="Last name" bind:input={$inputs.lastName} />
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="mt-3 flex flex-col gap-3 sm:flex-row">
|
|
||||||
<div class="w-full">
|
|
||||||
<FormInput label="Email" bind:input={$inputs.email} />
|
|
||||||
</div>
|
|
||||||
<div class="w-full">
|
|
||||||
<FormInput label="Username" bind:input={$inputs.username} />
|
<FormInput label="Username" bind:input={$inputs.username} />
|
||||||
|
<FormInput label="Email" bind:input={$inputs.email} />
|
||||||
|
<CheckboxWithLabel
|
||||||
|
id="admin-privileges"
|
||||||
|
label="Admin Privileges"
|
||||||
|
description="Admins have full access to the admin panel."
|
||||||
|
bind:checked={$inputs.isAdmin.value}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div class="mt-5 flex justify-end">
|
||||||
<CheckboxWithLabel
|
<Button {isLoading} type="submit">Save</Button>
|
||||||
id="admin-privileges"
|
</div>
|
||||||
label="Admin Privileges"
|
</fieldset>
|
||||||
description="Admins have full access to the admin panel."
|
|
||||||
bind:checked={$inputs.isAdmin.value}
|
|
||||||
/>
|
|
||||||
<div class="mt-5 flex justify-end">
|
|
||||||
<Button {isLoading} type="submit">Save</Button>
|
|
||||||
</div>
|
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -95,11 +95,13 @@
|
|||||||
<DropdownMenu.Item onclick={() => goto(`/settings/admin/users/${item.id}`)}
|
<DropdownMenu.Item onclick={() => goto(`/settings/admin/users/${item.id}`)}
|
||||||
><LucidePencil class="mr-2 h-4 w-4" /> Edit</DropdownMenu.Item
|
><LucidePencil class="mr-2 h-4 w-4" /> Edit</DropdownMenu.Item
|
||||||
>
|
>
|
||||||
<DropdownMenu.Item
|
{#if !item.ldapId}
|
||||||
class="text-red-500 focus:!text-red-700"
|
<DropdownMenu.Item
|
||||||
onclick={() => deleteUser(item)}
|
class="text-red-500 focus:!text-red-700"
|
||||||
><LucideTrash class="mr-2 h-4 w-4" />Delete</DropdownMenu.Item
|
onclick={() => deleteUser(item)}
|
||||||
>
|
><LucideTrash class="mr-2 h-4 w-4" />Delete</DropdownMenu.Item
|
||||||
|
>
|
||||||
|
{/if}
|
||||||
</DropdownMenu.Content>
|
</DropdownMenu.Content>
|
||||||
</DropdownMenu.Root>
|
</DropdownMenu.Root>
|
||||||
</Table.Cell>
|
</Table.Cell>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import adapter from '@sveltejs/adapter-node';
|
import adapter from '@sveltejs/adapter-node';
|
||||||
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
|
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
|
||||||
import packageJson from "./package.json" assert { type: "json" };
|
import packageJson from './package.json' assert { type: 'json' };
|
||||||
|
|
||||||
/** @type {import('@sveltejs/kit').Config} */
|
/** @type {import('@sveltejs/kit').Config} */
|
||||||
const config = {
|
const config = {
|
||||||
@@ -14,7 +14,7 @@ const config = {
|
|||||||
// See https://kit.svelte.dev/docs/adapters for more information about adapters.
|
// See https://kit.svelte.dev/docs/adapters for more information about adapters.
|
||||||
adapter: adapter(),
|
adapter: adapter(),
|
||||||
version: {
|
version: {
|
||||||
name: packageJson.version,
|
name: packageJson.version
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ test.beforeEach(cleanupBackend);
|
|||||||
test('Update general configuration', async ({ page }) => {
|
test('Update general configuration', async ({ page }) => {
|
||||||
await page.goto('/settings/admin/application-configuration');
|
await page.goto('/settings/admin/application-configuration');
|
||||||
|
|
||||||
await page.getByLabel('Name').fill('Updated Name');
|
await page.getByLabel('Application Name', { exact: true }).fill('Updated Name');
|
||||||
await page.getByLabel('Session Duration').fill('30');
|
await page.getByLabel('Session Duration').fill('30');
|
||||||
await page.getByRole('button', { name: 'Save' }).first().click();
|
await page.getByRole('button', { name: 'Save' }).first().click();
|
||||||
|
|
||||||
@@ -17,7 +17,7 @@ test('Update general configuration', async ({ page }) => {
|
|||||||
|
|
||||||
await page.reload();
|
await page.reload();
|
||||||
|
|
||||||
await expect(page.getByLabel('Name')).toHaveValue('Updated Name');
|
await expect(page.getByLabel('Application Name', { exact: true })).toHaveValue('Updated Name');
|
||||||
await expect(page.getByLabel('Session Duration')).toHaveValue('30');
|
await expect(page.getByLabel('Session Duration')).toHaveValue('30');
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -29,7 +29,7 @@ test('Update email configuration', async ({ page }) => {
|
|||||||
await page.getByLabel('SMTP User').fill('test@gmail.com');
|
await page.getByLabel('SMTP User').fill('test@gmail.com');
|
||||||
await page.getByLabel('SMTP Password').fill('password');
|
await page.getByLabel('SMTP Password').fill('password');
|
||||||
await page.getByLabel('SMTP From').fill('test@gmail.com');
|
await page.getByLabel('SMTP From').fill('test@gmail.com');
|
||||||
await page.getByRole('button', { name: 'Enable' }).click();
|
await page.getByRole('button', { name: 'Enable' }).nth(0).click();
|
||||||
await page.getByRole('status').click();
|
await page.getByRole('status').click();
|
||||||
|
|
||||||
await expect(page.getByRole('status')).toHaveText('Email configuration updated successfully');
|
await expect(page.getByRole('status')).toHaveText('Email configuration updated successfully');
|
||||||
|
|||||||
Reference in New Issue
Block a user