diff --git a/README.md b/README.md index f59ead5..f225dd6 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,7 @@ Additionally, what makes Pocket ID special is that it only supports [passkey](ht ## 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. ### Before you start @@ -175,6 +175,7 @@ docker compose up -d | `PORT` | `3000` | no | The port on which the frontend should listen. | | `BACKEND_PORT` | `8080` | no | The port on which the backend should listen. | + ## Account recovery There are two ways to create a one-time access link for a user: diff --git a/backend/.env.example b/backend/.env.example index c86f02f..5185e92 100644 --- a/backend/.env.example +++ b/backend/.env.example @@ -5,4 +5,4 @@ SQLITE_DB_PATH=data/pocket-id.db POSTGRES_CONNECTION_STRING=postgresql://postgres:postgres@localhost:5432/pocket-id UPLOAD_PATH=data/uploads PORT=8080 -HOST=localhost \ No newline at end of file +HOST=localhost diff --git a/backend/.gitignore b/backend/.gitignore index 71f540d..d32f3a0 100644 --- a/backend/.gitignore +++ b/backend/.gitignore @@ -13,4 +13,5 @@ # Dependency directories (remove the comment below to include it) # vendor/ -./data \ No newline at end of file +./data +.env diff --git a/backend/go.mod b/backend/go.mod index 3b61bfe..b342a61 100644 --- a/backend/go.mod +++ b/backend/go.mod @@ -15,7 +15,7 @@ require ( github.com/joho/godotenv v1.5.1 github.com/mileusna/useragent v1.3.5 github.com/oschwald/maxminddb-golang/v2 v2.0.0-beta.1 - golang.org/x/crypto v0.27.0 + golang.org/x/crypto v0.31.0 golang.org/x/time v0.6.0 gorm.io/driver/postgres v1.5.11 gorm.io/driver/sqlite v1.5.6 @@ -23,12 +23,15 @@ 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/loader v0.2.0 // indirect github.com/cloudwego/base64x v0.1.4 // indirect github.com/cloudwego/iasm v0.2.0 // indirect github.com/gabriel-vasile/mimetype v1.4.5 // 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/universal-translator v0.18.1 // indirect github.com/go-webauthn/x v0.1.14 // indirect @@ -61,10 +64,10 @@ require ( go.uber.org/atomic v1.11.0 // indirect golang.org/x/arch v0.10.0 // indirect golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 // indirect - golang.org/x/net v0.29.0 // indirect - golang.org/x/sync v0.8.0 // indirect - golang.org/x/sys v0.25.0 // indirect - golang.org/x/text v0.18.0 // indirect + golang.org/x/net v0.33.0 // indirect + golang.org/x/sync v0.10.0 // indirect + golang.org/x/sys v0.28.0 // indirect + golang.org/x/text v0.21.0 // indirect google.golang.org/protobuf v1.34.2 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/backend/go.sum b/backend/go.sum index e975d96..fc345ad 100644 --- a/backend/go.sum +++ b/backend/go.sum @@ -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/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/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/go.mod h1:B8Gt/XvtZ3Fqj+iSKMypzymZxw/FVwgIGKzMzT9r/rk= 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-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU= 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/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/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 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/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 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.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= 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/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/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= 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/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk= 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/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= 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.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 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.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 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/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= 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/go.mod h1:L7UH0GbB0p47T4Rri3uHjbpCFYrVrwc1I25QhNPiGK8= 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= golang.org/x/arch v0.10.0 h1:S3huipmSclq3PJMNe76NGwkBR504WFkQ5dhzWzP8ZW8= golang.org/x/arch v0.10.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= +golang.org/x/crypto v0.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/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/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/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/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.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/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/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/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/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= 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/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.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/backend/internal/bootstrap/router_bootstrap.go b/backend/internal/bootstrap/router_bootstrap.go index c4dfeb0..9538535 100644 --- a/backend/internal/bootstrap/router_bootstrap.go +++ b/backend/internal/bootstrap/router_bootstrap.go @@ -7,6 +7,7 @@ import ( "github.com/gin-gonic/gin" "github.com/stonith404/pocket-id/backend/internal/common" "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/service" "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) testService := service.NewTestService(db, appConfigService) userGroupService := service.NewUserGroupService(db) + ldapService := service.NewLdapService(db, appConfigService, userService, userGroupService) r.Use(middleware.NewCorsMiddleware().Add()) r.Use(middleware.NewErrorHandlerMiddleware().Add()) r.Use(middleware.NewRateLimitMiddleware().Add(rate.Every(time.Second), 60)) r.Use(middleware.NewJwtAuthMiddleware(jwtService, true).Add(false)) + job.RegisterLdapJobs(ldapService, appConfigService) + // Initialize middleware jwtAuthMiddleware := middleware.NewJwtAuthMiddleware(jwtService, false) fileSizeLimitMiddleware := middleware.NewFileSizeLimitMiddleware() @@ -57,7 +61,7 @@ func initRouter(db *gorm.DB, appConfigService *service.AppConfigService) { controller.NewWebauthnController(apiGroup, jwtAuthMiddleware, middleware.NewRateLimitMiddleware(), webauthnService, appConfigService) controller.NewOidcController(apiGroup, jwtAuthMiddleware, fileSizeLimitMiddleware, oidcService, jwtService) 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.NewUserGroupController(apiGroup, jwtAuthMiddleware, userGroupService) controller.NewCustomClaimController(apiGroup, jwtAuthMiddleware, customClaimService) diff --git a/backend/internal/common/env_config.go b/backend/internal/common/env_config.go index b992c98..6071cc3 100644 --- a/backend/internal/common/env_config.go +++ b/backend/internal/common/env_config.go @@ -1,9 +1,10 @@ package common import ( + "log" + "github.com/caarlos0/env/v11" _ "github.com/joho/godotenv/autoload" - "log" ) type DbProvider string diff --git a/backend/internal/common/errors.go b/backend/internal/common/errors.go index 55773eb..84393d6 100644 --- a/backend/internal/common/errors.go +++ b/backend/internal/common/errors.go @@ -58,7 +58,9 @@ func (e *OidcInvalidAuthorizationCodeError) HttpStatusCode() int { return 400 } 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 } type FileTypeNotSupportedError struct{} @@ -160,3 +162,17 @@ func (e *OidcMissingCodeChallengeError) Error() string { return "Missing code challenge" } 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 } diff --git a/backend/internal/controller/app_config_controller.go b/backend/internal/controller/app_config_controller.go index 6bfe8c2..b065388 100644 --- a/backend/internal/controller/app_config_controller.go +++ b/backend/internal/controller/app_config_controller.go @@ -16,11 +16,13 @@ func NewAppConfigController( jwtAuthMiddleware *middleware.JwtAuthMiddleware, appConfigService *service.AppConfigService, emailService *service.EmailService, + ldapService *service.LdapService, ) { acc := &AppConfigController{ appConfigService: appConfigService, emailService: emailService, + ldapService: ldapService, } group.GET("/application-configuration", acc.listAppConfigHandler) 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.POST("/application-configuration/test-email", jwtAuthMiddleware.Add(true), acc.testEmailHandler) + group.POST("/application-configuration/sync-ldap", jwtAuthMiddleware.Add(true), acc.syncLdapHandler) } type AppConfigController struct { appConfigService *service.AppConfigService emailService *service.EmailService + ldapService *service.LdapService } 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) } +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) { userID := c.GetString("userID") diff --git a/backend/internal/controller/user_controller.go b/backend/internal/controller/user_controller.go index 6c47312..a210e03 100644 --- a/backend/internal/controller/user_controller.go +++ b/backend/internal/controller/user_controller.go @@ -1,6 +1,9 @@ package controller import ( + "net/http" + "time" + "github.com/gin-gonic/gin" "github.com/stonith404/pocket-id/backend/internal/common" "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/utils" "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) { @@ -201,7 +202,7 @@ func (uc *UserController) updateUser(c *gin.Context, updateOwnUser bool) { userID = c.Param("id") } - user, err := uc.userService.UpdateUser(userID, input, updateOwnUser) + user, err := uc.userService.UpdateUser(userID, input, updateOwnUser, false) if err != nil { c.Error(err) return diff --git a/backend/internal/controller/user_group_controller.go b/backend/internal/controller/user_group_controller.go index ebdf727..c25df94 100644 --- a/backend/internal/controller/user_group_controller.go +++ b/backend/internal/controller/user_group_controller.go @@ -107,7 +107,7 @@ func (ugc *UserGroupController) update(c *gin.Context) { return } - group, err := ugc.UserGroupService.Update(c.Param("id"), input) + group, err := ugc.UserGroupService.Update(c.Param("id"), input, false) if err != nil { c.Error(err) return diff --git a/backend/internal/dto/app_config_dto.go b/backend/internal/dto/app_config_dto.go index a10e4d7..6a66a61 100644 --- a/backend/internal/dto/app_config_dto.go +++ b/backend/internal/dto/app_config_dto.go @@ -12,16 +12,30 @@ type AppConfigVariableDto struct { } type AppConfigUpdateDto struct { - AppName string `json:"appName" binding:"required,min=1,max=30"` - SessionDuration string `json:"sessionDuration" binding:"required"` - EmailsVerified string `json:"emailsVerified" binding:"required"` - AllowOwnAccountEdit string `json:"allowOwnAccountEdit" binding:"required"` - EmailEnabled string `json:"emailEnabled" binding:"required"` - SmtHost string `json:"smtpHost"` - SmtpPort string `json:"smtpPort"` - SmtpFrom string `json:"smtpFrom" binding:"omitempty,email"` - SmtpUser string `json:"smtpUser"` - SmtpPassword string `json:"smtpPassword"` - SmtpTls string `json:"smtpTls"` - SmtpSkipCertVerify string `json:"smtpSkipCertVerify"` + AppName string `json:"appName" binding:"required,min=1,max=30"` + SessionDuration string `json:"sessionDuration" binding:"required"` + EmailsVerified string `json:"emailsVerified" binding:"required"` + AllowOwnAccountEdit string `json:"allowOwnAccountEdit" binding:"required"` + EmailEnabled string `json:"emailEnabled" binding:"required"` + SmtHost string `json:"smtpHost"` + SmtpPort string `json:"smtpPort"` + SmtpFrom string `json:"smtpFrom" binding:"omitempty,email"` + SmtpUser string `json:"smtpUser"` + SmtpPassword string `json:"smtpPassword"` + SmtpTls string `json:"smtpTls"` + 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"` } diff --git a/backend/internal/dto/user_dto.go b/backend/internal/dto/user_dto.go index 7351bd1..9d93da6 100644 --- a/backend/internal/dto/user_dto.go +++ b/backend/internal/dto/user_dto.go @@ -10,6 +10,7 @@ type UserDto struct { LastName string `json:"lastName"` IsAdmin bool `json:"isAdmin"` CustomClaims []CustomClaimDto `json:"customClaims"` + LdapID *string `json:"ldapId"` } type UserCreateDto struct { @@ -18,6 +19,7 @@ type UserCreateDto struct { FirstName string `json:"firstName" binding:"required,min=1,max=50"` LastName string `json:"lastName" binding:"required,min=1,max=50"` IsAdmin bool `json:"isAdmin"` + LdapID string `json:"-"` } type OneTimeAccessTokenCreateDto struct { diff --git a/backend/internal/dto/user_group_dto.go b/backend/internal/dto/user_group_dto.go index 90d3fb1..d4d87d6 100644 --- a/backend/internal/dto/user_group_dto.go +++ b/backend/internal/dto/user_group_dto.go @@ -10,6 +10,7 @@ type UserGroupDtoWithUsers struct { Name string `json:"name"` CustomClaims []CustomClaimDto `json:"customClaims"` Users []UserDto `json:"users"` + LdapID *string `json:"ldapId"` CreatedAt datatype.DateTime `json:"createdAt"` } @@ -19,12 +20,14 @@ type UserGroupDtoWithUserCount struct { Name string `json:"name"` CustomClaims []CustomClaimDto `json:"customClaims"` UserCount int64 `json:"userCount"` + LdapID *string `json:"ldapId"` CreatedAt datatype.DateTime `json:"createdAt"` } type UserGroupCreateDto struct { FriendlyName string `json:"friendlyName" binding:"required,min=2,max=50"` Name string `json:"name" binding:"required,min=2,max=255"` + LdapID string `json:"-"` } type UserGroupUpdateUsersDto struct { diff --git a/backend/internal/job/ldap_job.go b/backend/internal/job/ldap_job.go new file mode 100644 index 0000000..415bec9 --- /dev/null +++ b/backend/internal/job/ldap_job.go @@ -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 +} diff --git a/backend/internal/model/app_config.go b/backend/internal/model/app_config.go index b8e4471..59475bd 100644 --- a/backend/internal/model/app_config.go +++ b/backend/internal/model/app_config.go @@ -10,15 +10,16 @@ type AppConfigVariable struct { } type AppConfig struct { + // General AppName AppConfigVariable SessionDuration AppConfigVariable EmailsVerified AppConfigVariable AllowOwnAccountEdit AppConfigVariable - + // Internal BackgroundImageType AppConfigVariable LogoLightImageType AppConfigVariable LogoDarkImageType AppConfigVariable - + // Email EmailEnabled AppConfigVariable SmtpHost AppConfigVariable SmtpPort AppConfigVariable @@ -27,4 +28,19 @@ type AppConfig struct { SmtpPassword AppConfigVariable SmtpTls 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 } diff --git a/backend/internal/model/user.go b/backend/internal/model/user.go index 8a8e619..4d551d5 100644 --- a/backend/internal/model/user.go +++ b/backend/internal/model/user.go @@ -14,6 +14,7 @@ type User struct { FirstName string `sortable:"true"` LastName string `sortable:"true"` IsAdmin bool `sortable:"true"` + LdapID *string CustomClaims []CustomClaim UserGroups []UserGroup `gorm:"many2many:user_groups_users;"` diff --git a/backend/internal/model/user_group.go b/backend/internal/model/user_group.go index 786a8b0..caa670b 100644 --- a/backend/internal/model/user_group.go +++ b/backend/internal/model/user_group.go @@ -3,7 +3,8 @@ package model type UserGroup struct { Base FriendlyName string `sortable:"true"` - Name string `gorm:"unique" sortable:"true"` + Name string `sortable:"true"` + LdapID *string Users []User `gorm:"many2many:user_groups_users;"` CustomClaims []CustomClaim } diff --git a/backend/internal/service/app_config_service.go b/backend/internal/service/app_config_service.go index 84f2dfd..dd33e16 100644 --- a/backend/internal/service/app_config_service.go +++ b/backend/internal/service/app_config_service.go @@ -2,15 +2,16 @@ package service import ( "fmt" + "log" + "mime/multipart" + "os" + "reflect" + "github.com/stonith404/pocket-id/backend/internal/common" "github.com/stonith404/pocket-id/backend/internal/dto" "github.com/stonith404/pocket-id/backend/internal/model" "github.com/stonith404/pocket-id/backend/internal/utils" "gorm.io/gorm" - "log" - "mime/multipart" - "os" - "reflect" ) type AppConfigService struct { @@ -30,6 +31,7 @@ func NewAppConfigService(db *gorm.DB) *AppConfigService { } var defaultDbConfig = model.AppConfig{ + // General AppName: model.AppConfigVariable{ Key: "appName", Type: "string", @@ -52,6 +54,7 @@ var defaultDbConfig = model.AppConfig{ IsPublic: true, DefaultValue: "true", }, + // Internal BackgroundImageType: model.AppConfigVariable{ Key: "backgroundImageType", Type: "string", @@ -70,6 +73,7 @@ var defaultDbConfig = model.AppConfig{ IsInternal: true, DefaultValue: "svg", }, + // Email EmailEnabled: model.AppConfigVariable{ Key: "emailEnabled", Type: "bool", @@ -105,6 +109,65 @@ var defaultDbConfig = model.AppConfig{ Type: "bool", 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) { diff --git a/backend/internal/service/ldap_service.go b/backend/internal/service/ldap_service.go new file mode 100644 index 0000000..a0a1e0b --- /dev/null +++ b/backend/internal/service/ldap_service.go @@ -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 +} diff --git a/backend/internal/service/user_group_service.go b/backend/internal/service/user_group_service.go index 0d97132..bc3d966 100644 --- a/backend/internal/service/user_group_service.go +++ b/backend/internal/service/user_group_service.go @@ -51,6 +51,10 @@ func (s *UserGroupService) Delete(id string) error { return err } + if group.LdapID != nil { + return &common.LdapUserGroupUpdateError{} + } + return s.db.Delete(&group).Error } @@ -58,6 +62,7 @@ func (s *UserGroupService) Create(input dto.UserGroupCreateDto) (group model.Use group = model.UserGroup{ FriendlyName: input.FriendlyName, Name: input.Name, + LdapID: &input.LdapID, } 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 } -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) if err != nil { return model.UserGroup{}, err } + if group.LdapID != nil && !allowLdapUpdate { + return model.UserGroup{}, &common.LdapUserGroupUpdateError{} + } + group.Name = input.Name group.FriendlyName = input.FriendlyName + group.LdapID = &input.LdapID if err := s.db.Preload("Users").Save(&group).Error; err != nil { if errors.Is(err, gorm.ErrDuplicatedKey) { diff --git a/backend/internal/service/user_service.go b/backend/internal/service/user_service.go index 1ffb349..0fe2709 100644 --- a/backend/internal/service/user_service.go +++ b/backend/internal/service/user_service.go @@ -46,6 +46,10 @@ func (s *UserService) DeleteUser(userID string) error { return err } + if user.LdapID != nil { + return &common.LdapUserUpdateError{} + } + return s.db.Delete(&user).Error } @@ -56,6 +60,7 @@ func (s *UserService) CreateUser(input dto.UserCreateDto) (model.User, error) { Email: input.Email, Username: input.Username, IsAdmin: input.IsAdmin, + LdapID: &input.LdapID, } if err := s.db.Create(&user).Error; err != nil { if errors.Is(err, gorm.ErrDuplicatedKey) { @@ -66,11 +71,16 @@ func (s *UserService) CreateUser(input dto.UserCreateDto) (model.User, error) { 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 if err := s.db.Where("id = ?", userID).First(&user).Error; err != nil { return model.User{}, err } + + if user.LdapID != nil && !allowLdapUpdate { + return model.User{}, &common.LdapUserUpdateError{} + } + user.FirstName = updatedUser.FirstName user.LastName = updatedUser.LastName user.Email = updatedUser.Email diff --git a/backend/resources/migrations/postgres/20250117164229_ldap.down.sql b/backend/resources/migrations/postgres/20250117164229_ldap.down.sql new file mode 100644 index 0000000..facc73f --- /dev/null +++ b/backend/resources/migrations/postgres/20250117164229_ldap.down.sql @@ -0,0 +1,5 @@ +ALTER TABLE users +DROP COLUMN ldap_id; + +ALTER TABLE user_groups +DROP COLUMN ldap_id; diff --git a/backend/resources/migrations/postgres/20250117164229_ldap.up.sql b/backend/resources/migrations/postgres/20250117164229_ldap.up.sql new file mode 100644 index 0000000..c763ff5 --- /dev/null +++ b/backend/resources/migrations/postgres/20250117164229_ldap.up.sql @@ -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); diff --git a/backend/resources/migrations/sqlite/20250115155817_ldap.down.sql b/backend/resources/migrations/sqlite/20250115155817_ldap.down.sql new file mode 100644 index 0000000..894ce56 --- /dev/null +++ b/backend/resources/migrations/sqlite/20250115155817_ldap.down.sql @@ -0,0 +1,2 @@ +ALTER TABLE users DROP COLUMN ldap_id; +ALTER TABLE user_groups DROP COLUMN ldap_id; \ No newline at end of file diff --git a/backend/resources/migrations/sqlite/20250115155817_ldap.up.sql b/backend/resources/migrations/sqlite/20250115155817_ldap.up.sql new file mode 100644 index 0000000..9a6446f --- /dev/null +++ b/backend/resources/migrations/sqlite/20250115155817_ldap.up.sql @@ -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); \ No newline at end of file diff --git a/frontend/package-lock.json b/frontend/package-lock.json index df274b5..8010a9d 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1,12 +1,12 @@ { "name": "pocket-id-frontend", - "version": "0.10.0", + "version": "0.24.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "pocket-id-frontend", - "version": "0.10.0", + "version": "0.24.1", "dependencies": { "@simplewebauthn/browser": "^10.0.0", "axios": "^1.7.7", diff --git a/frontend/src/lib/components/advanced-table.svelte b/frontend/src/lib/components/advanced-table.svelte index c41a9f3..d8a0a6e 100644 --- a/frontend/src/lib/components/advanced-table.svelte +++ b/frontend/src/lib/components/advanced-table.svelte @@ -17,6 +17,7 @@ requestOptions = $bindable(), selectedIds = $bindable(), withoutSearch = false, + selectionDisabled = false, defaultSort, onRefresh, columns, @@ -26,6 +27,7 @@ requestOptions?: SearchPaginationSortRequest; selectedIds?: string[]; withoutSearch?: boolean; + selectionDisabled?: boolean; defaultSort?: { column: string; direction: 'asc' | 'desc' }; onRefresh: (requestOptions: SearchPaginationSortRequest) => Promise>; columns: { label: string; hidden?: boolean; sortColumn?: string }[]; @@ -122,7 +124,11 @@ {#if selectedIds} - onAllCheck(c as boolean)} /> + onAllCheck(c as boolean)} + /> {/if} {#each columns as column} @@ -160,6 +166,7 @@ {#if selectedIds} onCheck(c as boolean, item.id)} /> diff --git a/frontend/src/lib/components/checkbox-with-label.svelte b/frontend/src/lib/components/checkbox-with-label.svelte index 571ff02..3f2ffff 100644 --- a/frontend/src/lib/components/checkbox-with-label.svelte +++ b/frontend/src/lib/components/checkbox-with-label.svelte @@ -19,7 +19,7 @@ } = $props(); -
+
; label?: string; description?: string; + placeholder?: string; disabled?: boolean; type?: 'text' | 'password' | 'email' | 'number' | 'checkbox'; onInput?: (e: FormInputEvent) => void; @@ -38,7 +40,7 @@ {#if children} {@render children()} {:else if input} - onInput?.(e)} /> + onInput?.(e)} /> {/if} {#if input?.error}

{input.error}

diff --git a/frontend/src/lib/services/app-config-service.ts b/frontend/src/lib/services/app-config-service.ts index bb7013a..e5a8f04 100644 --- a/frontend/src/lib/services/app-config-service.ts +++ b/frontend/src/lib/services/app-config-service.ts @@ -57,6 +57,10 @@ export default class AppConfigService extends APIService { await this.api.post('/application-configuration/test-email'); } + async syncLdap() { + await this.api.post('/application-configuration/sync-ldap'); + } + async getVersionInformation() { const response = ( await axios.get('https://api.github.com/repos/stonith404/pocket-id/releases/latest') diff --git a/frontend/src/lib/types/application-configuration.ts b/frontend/src/lib/types/application-configuration.ts index bc28de3..28475fd 100644 --- a/frontend/src/lib/types/application-configuration.ts +++ b/frontend/src/lib/types/application-configuration.ts @@ -4,8 +4,10 @@ export type AppConfig = { }; export type AllAppConfig = AppConfig & { + // General sessionDuration: number; emailsVerified: boolean; + // Email emailEnabled: boolean; smtpHost: string; smtpPort: number; @@ -14,6 +16,21 @@ export type AllAppConfig = AppConfig & { smtpPassword: string; smtpTls: 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 = { diff --git a/frontend/src/lib/types/user-group.type.ts b/frontend/src/lib/types/user-group.type.ts index da02635..8306774 100644 --- a/frontend/src/lib/types/user-group.type.ts +++ b/frontend/src/lib/types/user-group.type.ts @@ -7,6 +7,7 @@ export type UserGroup = { name: string; createdAt: string; customClaims: CustomClaim[]; + ldapId?: string; }; export type UserGroupWithUsers = UserGroup & { @@ -17,4 +18,4 @@ export type UserGroupWithUserCount = UserGroup & { userCount: number; }; -export type UserGroupCreate = Pick; +export type UserGroupCreate = Pick; diff --git a/frontend/src/lib/types/user.type.ts b/frontend/src/lib/types/user.type.ts index 1157ecf..5366e52 100644 --- a/frontend/src/lib/types/user.type.ts +++ b/frontend/src/lib/types/user.type.ts @@ -8,6 +8,7 @@ export type User = { lastName: string; isAdmin: boolean; customClaims: CustomClaim[]; + ldapId?: string; }; -export type UserCreate = Omit; +export type UserCreate = Omit; diff --git a/frontend/src/routes/settings/admin/application-configuration/+page.svelte b/frontend/src/routes/settings/admin/application-configuration/+page.svelte index b8a6ad0..4ba4203 100644 --- a/frontend/src/routes/settings/admin/application-configuration/+page.svelte +++ b/frontend/src/routes/settings/admin/application-configuration/+page.svelte @@ -7,6 +7,7 @@ import { toast } from 'svelte-sonner'; import AppConfigEmailForm from './forms/app-config-email-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'; let { data } = $props(); @@ -34,8 +35,12 @@ favicon: File | null ) { const faviconPromise = favicon ? appConfigService.updateFavicon(favicon) : Promise.resolve(); - const lightLogoPromise = logoLight ? appConfigService.updateLogo(logoLight, true) : Promise.resolve(); - const darkLogoPromise = logoDark ? appConfigService.updateLogo(logoDark, false) : Promise.resolve(); + const lightLogoPromise = logoLight + ? appConfigService.updateLogo(logoLight, true) + : Promise.resolve(); + const darkLogoPromise = logoDark + ? appConfigService.updateLogo(logoDark, false) + : Promise.resolve(); const backgroundImagePromise = backgroundImage ? appConfigService.updateBackgroundImage(backgroundImage) : Promise.resolve(); @@ -72,6 +77,18 @@ + + + LDAP + + Configure LDAP settings to sync users and groups from an LDAP server. + + + + + + + Images diff --git a/frontend/src/routes/settings/admin/application-configuration/forms/app-config-email-form.svelte b/frontend/src/routes/settings/admin/application-configuration/forms/app-config-email-form.svelte index 191cce4..6321edc 100644 --- a/frontend/src/routes/settings/admin/application-configuration/forms/app-config-email-form.svelte +++ b/frontend/src/routes/settings/admin/application-configuration/forms/app-config-email-form.svelte @@ -45,7 +45,6 @@ const { inputs, ...form } = createForm(formSchema, updatedAppConfig); async function onSubmit() { - console.log('submit'); const data = form.validate(); if (!data) return false; await callback({ diff --git a/frontend/src/routes/settings/admin/application-configuration/forms/app-config-ldap-form.svelte b/frontend/src/routes/settings/admin/application-configuration/forms/app-config-ldap-form.svelte new file mode 100644 index 0000000..3535047 --- /dev/null +++ b/frontend/src/routes/settings/admin/application-configuration/forms/app-config-ldap-form.svelte @@ -0,0 +1,169 @@ + + +
+

Client Configuration

+
+ + + + + +
+

Attribute Mapping

+
+ + + + + + + + +
+ +
+ {#if ldapEnabled} + + + + {:else} + + {/if} +
+
diff --git a/frontend/src/routes/settings/admin/user-groups/[id]/+page.svelte b/frontend/src/routes/settings/admin/user-groups/[id]/+page.svelte index a2abcb6..e36bec9 100644 --- a/frontend/src/routes/settings/admin/user-groups/[id]/+page.svelte +++ b/frontend/src/routes/settings/admin/user-groups/[id]/+page.svelte @@ -1,5 +1,6 @@
-
-
- +
+
+
+ +
+
+ +
-
- +
+
-
-
- -
+
diff --git a/frontend/src/routes/settings/admin/user-groups/user-group-list.svelte b/frontend/src/routes/settings/admin/user-groups/user-group-list.svelte index a342a04..91392f6 100644 --- a/frontend/src/routes/settings/admin/user-groups/user-group-list.svelte +++ b/frontend/src/routes/settings/admin/user-groups/user-group-list.svelte @@ -68,11 +68,13 @@ Edit - deleteUserGroup(item)} - >Delete + {#if !item.ldapId} + deleteUserGroup(item)} + >Delete + {/if} diff --git a/frontend/src/routes/settings/admin/user-groups/user-selection.svelte b/frontend/src/routes/settings/admin/user-groups/user-selection.svelte index c5a0fab..fb9fdae 100644 --- a/frontend/src/routes/settings/admin/user-groups/user-selection.svelte +++ b/frontend/src/routes/settings/admin/user-groups/user-selection.svelte @@ -7,8 +7,11 @@ let { users: initialUsers, + selectionDisabled = false, selectedUserIds = $bindable() - }: { users: Paginated; selectedUserIds: string[] } = $props(); + }: { users: Paginated; + selectionDisabled?: boolean; + selectedUserIds: string[] } = $props(); const userService = new UserService(); @@ -23,6 +26,7 @@ { label: 'Email', sortColumn: 'email' } ]} bind:selectedIds={selectedUserIds} + {selectionDisabled} > {#snippet rows({ item })} {item.firstName} {item.lastName} diff --git a/frontend/src/routes/settings/admin/users/[id]/+page.svelte b/frontend/src/routes/settings/admin/users/[id]/+page.svelte index 657ae91..731f924 100644 --- a/frontend/src/routes/settings/admin/users/[id]/+page.svelte +++ b/frontend/src/routes/settings/admin/users/[id]/+page.svelte @@ -1,4 +1,5 @@
-
-
+
+
-
-
-
-
-
-
+ +
-
- +
+
-
- -
- -
+ diff --git a/frontend/src/routes/settings/admin/users/user-list.svelte b/frontend/src/routes/settings/admin/users/user-list.svelte index aaad2e4..89ab1ad 100644 --- a/frontend/src/routes/settings/admin/users/user-list.svelte +++ b/frontend/src/routes/settings/admin/users/user-list.svelte @@ -95,11 +95,13 @@ goto(`/settings/admin/users/${item.id}`)} > Edit - deleteUser(item)} - >Delete + {#if !item.ldapId} + deleteUser(item)} + >Delete + {/if} diff --git a/frontend/svelte.config.js b/frontend/svelte.config.js index 42d1a6c..14125f4 100644 --- a/frontend/svelte.config.js +++ b/frontend/svelte.config.js @@ -1,6 +1,6 @@ import adapter from '@sveltejs/adapter-node'; 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} */ const config = { @@ -14,7 +14,7 @@ const config = { // See https://kit.svelte.dev/docs/adapters for more information about adapters. adapter: adapter(), version: { - name: packageJson.version, + name: packageJson.version } } }; diff --git a/frontend/tests/application-configuration.spec.ts b/frontend/tests/application-configuration.spec.ts index 270013d..4dd929d 100644 --- a/frontend/tests/application-configuration.spec.ts +++ b/frontend/tests/application-configuration.spec.ts @@ -6,7 +6,7 @@ test.beforeEach(cleanupBackend); test('Update general configuration', async ({ page }) => { 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.getByRole('button', { name: 'Save' }).first().click(); @@ -17,7 +17,7 @@ test('Update general configuration', async ({ page }) => { 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'); }); @@ -29,7 +29,7 @@ test('Update email configuration', async ({ page }) => { await page.getByLabel('SMTP User').fill('test@gmail.com'); await page.getByLabel('SMTP Password').fill('password'); 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 expect(page.getByRole('status')).toHaveText('Email configuration updated successfully');