mirror of
https://github.com/pocket-id/pocket-id.git
synced 2026-03-28 10:16:37 +00:00
Compare commits
21 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
12d60fea23 | ||
|
|
2d733fc79f | ||
|
|
a421d01e0c | ||
|
|
1026ee4f5b | ||
|
|
cddfe8fa4c | ||
|
|
ef25f6b6b8 | ||
|
|
1652cc65f3 | ||
|
|
4bafee4f58 | ||
|
|
e46471cc2d | ||
|
|
fde951b543 | ||
|
|
01a9de0b04 | ||
|
|
a1131bca9a | ||
|
|
9a167d4076 | ||
|
|
887c5e462a | ||
|
|
20eba1378e | ||
|
|
a6ae7ae287 | ||
|
|
840a672fc3 | ||
|
|
7446f853fc | ||
|
|
652ee6ad5d | ||
|
|
dca9e7a11a | ||
|
|
816c198a42 |
60
CHANGELOG.md
60
CHANGELOG.md
@@ -1,3 +1,63 @@
|
|||||||
|
## [](https://github.com/pocket-id/pocket-id/compare/v0.35.5...v) (2025-03-03)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* support `LOGIN` authentication method for SMTP ([#292](https://github.com/pocket-id/pocket-id/issues/292)) ([2d733fc](https://github.com/pocket-id/pocket-id/commit/2d733fc79faefca23d54b22768029c3ba3427410))
|
||||||
|
|
||||||
|
## [](https://github.com/pocket-id/pocket-id/compare/v0.35.4...v) (2025-03-03)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* profile picture orientation if image is rotated with EXIF ([1026ee4](https://github.com/pocket-id/pocket-id/commit/1026ee4f5b5c7fda78b65c94a5d0f899525defd1))
|
||||||
|
|
||||||
|
## [](https://github.com/pocket-id/pocket-id/compare/v0.35.3...v) (2025-03-01)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* add `groups` scope and claim to well known endpoint ([4bafee4](https://github.com/pocket-id/pocket-id/commit/4bafee4f58f5a76898cf66d6192916d405eea389))
|
||||||
|
* profile picture of other user can't be updated ([#273](https://github.com/pocket-id/pocket-id/issues/273)) ([ef25f6b](https://github.com/pocket-id/pocket-id/commit/ef25f6b6b84b52f1310d366d40aa3769a6fe9bef))
|
||||||
|
* support POST for OIDC userinfo endpoint ([1652cc6](https://github.com/pocket-id/pocket-id/commit/1652cc65f3f966d018d81a1ae22abb5ff1b4c47b))
|
||||||
|
|
||||||
|
## [](https://github.com/pocket-id/pocket-id/compare/v0.35.2...v) (2025-02-25)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* add option to manually select SMTP TLS method ([#268](https://github.com/pocket-id/pocket-id/issues/268)) ([01a9de0](https://github.com/pocket-id/pocket-id/commit/01a9de0b04512c62d0f223de33d711f93c49b9cc))
|
||||||
|
* **ldap:** sync error if LDAP user collides with an existing user ([fde951b](https://github.com/pocket-id/pocket-id/commit/fde951b543281fedf9f602abae26b50881e3d157))
|
||||||
|
|
||||||
|
## [](https://github.com/pocket-id/pocket-id/compare/v0.35.1...v) (2025-02-24)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* delete profile picture if user gets deleted ([9a167d4](https://github.com/pocket-id/pocket-id/commit/9a167d4076872e5e3e5d78d2a66ef7203ca5261b))
|
||||||
|
* updating profile picture of other user updates own profile picture ([887c5e4](https://github.com/pocket-id/pocket-id/commit/887c5e462a50c8fb579ca6804f1a643d8af78fe8))
|
||||||
|
|
||||||
|
## [](https://github.com/pocket-id/pocket-id/compare/v0.35.0...v) (2025-02-22)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* add validation that `PUBLIC_APP_URL` can't contain a path ([a6ae7ae](https://github.com/pocket-id/pocket-id/commit/a6ae7ae28713f7fc8018ae2aa7572986df3e1a5b))
|
||||||
|
* binary profile picture can't be imported from LDAP ([840a672](https://github.com/pocket-id/pocket-id/commit/840a672fc35ca8476caf86d7efaba9d54bce86aa))
|
||||||
|
|
||||||
|
## [](https://github.com/pocket-id/pocket-id/compare/v0.34.0...v) (2025-02-19)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* add ability to upload a profile picture ([#244](https://github.com/pocket-id/pocket-id/issues/244)) ([652ee6a](https://github.com/pocket-id/pocket-id/commit/652ee6ad5d6c46f0d35c955ff7bb9bdac6240ca6))
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* app config strings starting with a number are parsed incorrectly ([816c198](https://github.com/pocket-id/pocket-id/commit/816c198a42c189cb1f2d94885d2e3623e47e2848))
|
||||||
|
* emails do not get rendered correctly in Gmail ([dca9e7a](https://github.com/pocket-id/pocket-id/commit/dca9e7a11a3ba5d3b43a937f11cb9d16abad2db5))
|
||||||
|
|
||||||
## [](https://github.com/pocket-id/pocket-id/compare/v0.33.0...v) (2025-02-16)
|
## [](https://github.com/pocket-id/pocket-id/compare/v0.33.0...v) (2025-02-16)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,10 @@ go 1.23.1
|
|||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/caarlos0/env/v11 v11.3.1
|
github.com/caarlos0/env/v11 v11.3.1
|
||||||
|
github.com/disintegration/imageorient v0.0.0-20180920195336-8147d86e83ec
|
||||||
|
github.com/disintegration/imaging v1.6.2
|
||||||
|
github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21
|
||||||
|
github.com/emersion/go-smtp v0.21.3
|
||||||
github.com/fxamacker/cbor/v2 v2.7.0
|
github.com/fxamacker/cbor/v2 v2.7.0
|
||||||
github.com/gin-gonic/gin v1.10.0
|
github.com/gin-gonic/gin v1.10.0
|
||||||
github.com/go-co-op/gocron/v2 v2.15.0
|
github.com/go-co-op/gocron/v2 v2.15.0
|
||||||
@@ -17,6 +21,7 @@ require (
|
|||||||
github.com/mileusna/useragent v1.3.5
|
github.com/mileusna/useragent v1.3.5
|
||||||
github.com/oschwald/maxminddb-golang/v2 v2.0.0-beta.2
|
github.com/oschwald/maxminddb-golang/v2 v2.0.0-beta.2
|
||||||
golang.org/x/crypto v0.32.0
|
golang.org/x/crypto v0.32.0
|
||||||
|
golang.org/x/image v0.24.0
|
||||||
golang.org/x/time v0.9.0
|
golang.org/x/time v0.9.0
|
||||||
gorm.io/driver/postgres v1.5.11
|
gorm.io/driver/postgres v1.5.11
|
||||||
gorm.io/driver/sqlite v1.5.7
|
gorm.io/driver/sqlite v1.5.7
|
||||||
@@ -28,6 +33,7 @@ require (
|
|||||||
github.com/bytedance/sonic v1.12.8 // indirect
|
github.com/bytedance/sonic v1.12.8 // indirect
|
||||||
github.com/bytedance/sonic/loader v0.2.3 // indirect
|
github.com/bytedance/sonic/loader v0.2.3 // indirect
|
||||||
github.com/cloudwego/base64x v0.1.5 // indirect
|
github.com/cloudwego/base64x v0.1.5 // indirect
|
||||||
|
github.com/disintegration/gift v1.1.2 // indirect
|
||||||
github.com/gabriel-vasile/mimetype v1.4.8 // indirect
|
github.com/gabriel-vasile/mimetype v1.4.8 // indirect
|
||||||
github.com/gin-contrib/sse v1.0.0 // indirect
|
github.com/gin-contrib/sse v1.0.0 // indirect
|
||||||
github.com/go-asn1-ber/asn1-ber v1.5.7 // indirect
|
github.com/go-asn1-ber/asn1-ber v1.5.7 // indirect
|
||||||
@@ -64,9 +70,9 @@ require (
|
|||||||
golang.org/x/arch v0.13.0 // indirect
|
golang.org/x/arch v0.13.0 // indirect
|
||||||
golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8 // indirect
|
golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8 // indirect
|
||||||
golang.org/x/net v0.34.0 // indirect
|
golang.org/x/net v0.34.0 // indirect
|
||||||
golang.org/x/sync v0.10.0 // indirect
|
golang.org/x/sync v0.11.0 // indirect
|
||||||
golang.org/x/sys v0.29.0 // indirect
|
golang.org/x/sys v0.29.0 // indirect
|
||||||
golang.org/x/text v0.21.0 // indirect
|
golang.org/x/text v0.22.0 // indirect
|
||||||
google.golang.org/protobuf v1.36.4 // indirect
|
google.golang.org/protobuf v1.36.4 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -22,6 +22,12 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
|
|||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/dhui/dktest v0.4.4 h1:+I4s6JRE1yGuqflzwqG+aIaMdgXIorCf5P98JnaAWa8=
|
github.com/dhui/dktest v0.4.4 h1:+I4s6JRE1yGuqflzwqG+aIaMdgXIorCf5P98JnaAWa8=
|
||||||
github.com/dhui/dktest v0.4.4/go.mod h1:4+22R4lgsdAXrDyaH4Nqx2JEz2hLp49MqQmm9HLCQhM=
|
github.com/dhui/dktest v0.4.4/go.mod h1:4+22R4lgsdAXrDyaH4Nqx2JEz2hLp49MqQmm9HLCQhM=
|
||||||
|
github.com/disintegration/gift v1.1.2 h1:9ZyHJr+kPamiH10FX3Pynt1AxFUob812bU9Wt4GMzhs=
|
||||||
|
github.com/disintegration/gift v1.1.2/go.mod h1:Jh2i7f7Q2BM7Ezno3PhfezbR1xpUg9dUg3/RlKGr4HI=
|
||||||
|
github.com/disintegration/imageorient v0.0.0-20180920195336-8147d86e83ec h1:YrB6aVr9touOt75I9O1SiancmR2GMg45U9UYf0gtgWg=
|
||||||
|
github.com/disintegration/imageorient v0.0.0-20180920195336-8147d86e83ec/go.mod h1:K0KBFIr1gWu/C1Gp10nFAcAE4hsB7JxE6OgLijrJ8Sk=
|
||||||
|
github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c=
|
||||||
|
github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4=
|
||||||
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
|
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
|
||||||
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
|
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
|
||||||
github.com/docker/docker v27.2.0+incompatible h1:Rk9nIVdfH3+Vz4cyI/uhbINhEZ/oLmc+CBXmH6fbNk4=
|
github.com/docker/docker v27.2.0+incompatible h1:Rk9nIVdfH3+Vz4cyI/uhbINhEZ/oLmc+CBXmH6fbNk4=
|
||||||
@@ -30,6 +36,10 @@ github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj
|
|||||||
github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc=
|
github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc=
|
||||||
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
|
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
|
||||||
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
||||||
|
github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21 h1:OJyUGMJTzHTd1XQp98QTaHernxMYzRaOasRir9hUlFQ=
|
||||||
|
github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ=
|
||||||
|
github.com/emersion/go-smtp v0.21.3 h1:7uVwagE8iPYE48WhNsng3RRpCUpFvNl39JGNSIyGVMY=
|
||||||
|
github.com/emersion/go-smtp v0.21.3/go.mod h1:qm27SGYgoIPRot6ubfQ/GpiPy/g3PaZAVRxiO/sDUgQ=
|
||||||
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
||||||
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||||
github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E=
|
github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E=
|
||||||
@@ -211,6 +221,9 @@ golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc=
|
|||||||
golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
|
golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
|
||||||
golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8 h1:yqrTHse8TCMW1M1ZCP+VAR/l0kKxwaAIqN/il7x4voA=
|
golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8 h1:yqrTHse8TCMW1M1ZCP+VAR/l0kKxwaAIqN/il7x4voA=
|
||||||
golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8/go.mod h1:tujkw807nyEEAamNbDrEGzRav+ilXA7PCRAd6xsmwiU=
|
golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8/go.mod h1:tujkw807nyEEAamNbDrEGzRav+ilXA7PCRAd6xsmwiU=
|
||||||
|
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||||
|
golang.org/x/image v0.24.0 h1:AN7zRgVsbvmTfNyqIbbOraYL8mSwcKncEj8ofjgzcMQ=
|
||||||
|
golang.org/x/image v0.24.0/go.mod h1:4b/ITuLfqYq1hqZcjofwctIhi7sZh2WaCjvsBNjjya8=
|
||||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
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.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||||
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||||
@@ -235,8 +248,9 @@ 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.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.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||||
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
golang.org/x/sync v0.7.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/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||||
|
golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w=
|
||||||
|
golang.org/x/sync v0.11.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-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-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-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
@@ -268,8 +282,9 @@ 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.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.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||||
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||||
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/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||||
|
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
|
||||||
|
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
|
||||||
golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY=
|
golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY=
|
||||||
golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
golang.org/x/time v0.9.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-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package common
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"log"
|
"log"
|
||||||
|
"net/url"
|
||||||
|
|
||||||
"github.com/caarlos0/env/v11"
|
"github.com/caarlos0/env/v11"
|
||||||
_ "github.com/joho/godotenv/autoload"
|
_ "github.com/joho/godotenv/autoload"
|
||||||
@@ -61,4 +62,12 @@ func init() {
|
|||||||
if EnvConfig.DbProvider == DbProviderSqlite && EnvConfig.SqliteDBPath == "" {
|
if EnvConfig.DbProvider == DbProviderSqlite && EnvConfig.SqliteDBPath == "" {
|
||||||
log.Fatal("Missing SQLITE_DB_PATH environment variable")
|
log.Fatal("Missing SQLITE_DB_PATH environment variable")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
parsedAppUrl, err := url.Parse(EnvConfig.AppURL)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal("PUBLIC_APP_URL is not a valid URL")
|
||||||
|
}
|
||||||
|
if parsedAppUrl.Path != "" {
|
||||||
|
log.Fatal("PUBLIC_APP_URL must not contain a path")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -94,6 +94,11 @@ type NotSignedInError struct{}
|
|||||||
func (e *NotSignedInError) Error() string { return "You are not signed in" }
|
func (e *NotSignedInError) Error() string { return "You are not signed in" }
|
||||||
func (e *NotSignedInError) HttpStatusCode() int { return http.StatusUnauthorized }
|
func (e *NotSignedInError) HttpStatusCode() int { return http.StatusUnauthorized }
|
||||||
|
|
||||||
|
type MissingAccessToken struct{}
|
||||||
|
|
||||||
|
func (e *MissingAccessToken) Error() string { return "Missing access token" }
|
||||||
|
func (e *MissingAccessToken) HttpStatusCode() int { return http.StatusUnauthorized }
|
||||||
|
|
||||||
type MissingPermissionError struct{}
|
type MissingPermissionError struct{}
|
||||||
|
|
||||||
func (e *MissingPermissionError) Error() string {
|
func (e *MissingPermissionError) Error() string {
|
||||||
@@ -211,3 +216,11 @@ func (e *UiConfigDisabledError) Error() string {
|
|||||||
return "The configuration can't be changed since the UI configuration is disabled"
|
return "The configuration can't be changed since the UI configuration is disabled"
|
||||||
}
|
}
|
||||||
func (e *UiConfigDisabledError) HttpStatusCode() int { return http.StatusForbidden }
|
func (e *UiConfigDisabledError) HttpStatusCode() int { return http.StatusForbidden }
|
||||||
|
|
||||||
|
type InvalidUUIDError struct{}
|
||||||
|
|
||||||
|
func (e *InvalidUUIDError) Error() string {
|
||||||
|
return "Invalid UUID"
|
||||||
|
}
|
||||||
|
|
||||||
|
type InvalidEmailError struct{}
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ func NewOidcController(group *gin.RouterGroup, jwtAuthMiddleware *middleware.Jwt
|
|||||||
|
|
||||||
group.POST("/oidc/token", oc.createTokensHandler)
|
group.POST("/oidc/token", oc.createTokensHandler)
|
||||||
group.GET("/oidc/userinfo", oc.userInfoHandler)
|
group.GET("/oidc/userinfo", oc.userInfoHandler)
|
||||||
|
group.POST("/oidc/userinfo", oc.userInfoHandler)
|
||||||
group.POST("/oidc/end-session", oc.EndSessionHandler)
|
group.POST("/oidc/end-session", oc.EndSessionHandler)
|
||||||
group.GET("/oidc/end-session", oc.EndSessionHandler)
|
group.GET("/oidc/end-session", oc.EndSessionHandler)
|
||||||
|
|
||||||
@@ -111,7 +112,14 @@ func (oc *OidcController) createTokensHandler(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (oc *OidcController) userInfoHandler(c *gin.Context) {
|
func (oc *OidcController) userInfoHandler(c *gin.Context) {
|
||||||
token := strings.Split(c.GetHeader("Authorization"), " ")[1]
|
authHeaderSplit := strings.Split(c.GetHeader("Authorization"), " ")
|
||||||
|
if len(authHeaderSplit) != 2 {
|
||||||
|
c.Error(&common.MissingAccessToken{})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
token := authHeaderSplit[1]
|
||||||
|
|
||||||
jwtClaims, err := oc.jwtService.VerifyOauthAccessToken(token)
|
jwtClaims, err := oc.jwtService.VerifyOauthAccessToken(token)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.Error(err)
|
c.Error(err)
|
||||||
|
|||||||
@@ -30,6 +30,11 @@ func NewUserController(group *gin.RouterGroup, jwtAuthMiddleware *middleware.Jwt
|
|||||||
group.PUT("/users/me", jwtAuthMiddleware.Add(false), uc.updateCurrentUserHandler)
|
group.PUT("/users/me", jwtAuthMiddleware.Add(false), uc.updateCurrentUserHandler)
|
||||||
group.DELETE("/users/:id", jwtAuthMiddleware.Add(true), uc.deleteUserHandler)
|
group.DELETE("/users/:id", jwtAuthMiddleware.Add(true), uc.deleteUserHandler)
|
||||||
|
|
||||||
|
group.GET("/users/:id/profile-picture.png", uc.getUserProfilePictureHandler)
|
||||||
|
group.GET("/users/me/profile-picture.png", jwtAuthMiddleware.Add(false), uc.getCurrentUserProfilePictureHandler)
|
||||||
|
group.PUT("/users/:id/profile-picture", jwtAuthMiddleware.Add(true), uc.updateUserProfilePictureHandler)
|
||||||
|
group.PUT("/users/me/profile-picture", jwtAuthMiddleware.Add(false), uc.updateCurrentUserProfilePictureHandler)
|
||||||
|
|
||||||
group.POST("/users/:id/one-time-access-token", jwtAuthMiddleware.Add(true), uc.createOneTimeAccessTokenHandler)
|
group.POST("/users/:id/one-time-access-token", jwtAuthMiddleware.Add(true), uc.createOneTimeAccessTokenHandler)
|
||||||
group.POST("/one-time-access-token/:token", rateLimitMiddleware.Add(rate.Every(10*time.Second), 5), uc.exchangeOneTimeAccessTokenHandler)
|
group.POST("/one-time-access-token/:token", rateLimitMiddleware.Add(rate.Every(10*time.Second), 5), uc.exchangeOneTimeAccessTokenHandler)
|
||||||
group.POST("/one-time-access-token/setup", uc.getSetupAccessTokenHandler)
|
group.POST("/one-time-access-token/setup", uc.getSetupAccessTokenHandler)
|
||||||
@@ -142,6 +147,74 @@ func (uc *UserController) updateCurrentUserHandler(c *gin.Context) {
|
|||||||
uc.updateUser(c, true)
|
uc.updateUser(c, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (uc *UserController) getUserProfilePictureHandler(c *gin.Context) {
|
||||||
|
userID := c.Param("id")
|
||||||
|
|
||||||
|
picture, size, err := uc.userService.GetProfilePicture(userID)
|
||||||
|
if err != nil {
|
||||||
|
c.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.DataFromReader(http.StatusOK, size, "image/png", picture, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (uc *UserController) getCurrentUserProfilePictureHandler(c *gin.Context) {
|
||||||
|
userID := c.GetString("userID")
|
||||||
|
|
||||||
|
picture, size, err := uc.userService.GetProfilePicture(userID)
|
||||||
|
if err != nil {
|
||||||
|
c.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.DataFromReader(http.StatusOK, size, "image/png", picture, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (uc *UserController) updateUserProfilePictureHandler(c *gin.Context) {
|
||||||
|
userID := c.Param("id")
|
||||||
|
fileHeader, err := c.FormFile("file")
|
||||||
|
if err != nil {
|
||||||
|
c.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
file, err := fileHeader.Open()
|
||||||
|
if err != nil {
|
||||||
|
c.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
if err := uc.userService.UpdateProfilePicture(userID, file); err != nil {
|
||||||
|
c.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Status(http.StatusNoContent)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (uc *UserController) updateCurrentUserProfilePictureHandler(c *gin.Context) {
|
||||||
|
userID := c.GetString("userID")
|
||||||
|
fileHeader, err := c.FormFile("file")
|
||||||
|
if err != nil {
|
||||||
|
c.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
file, err := fileHeader.Open()
|
||||||
|
if err != nil {
|
||||||
|
c.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
if err := uc.userService.UpdateProfilePicture(userID, file); err != nil {
|
||||||
|
c.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Status(http.StatusNoContent)
|
||||||
|
}
|
||||||
|
|
||||||
func (uc *UserController) createOneTimeAccessTokenHandler(c *gin.Context) {
|
func (uc *UserController) createOneTimeAccessTokenHandler(c *gin.Context) {
|
||||||
var input dto.OneTimeAccessTokenCreateDto
|
var input dto.OneTimeAccessTokenCreateDto
|
||||||
if err := c.ShouldBindJSON(&input); err != nil {
|
if err := c.ShouldBindJSON(&input); err != nil {
|
||||||
|
|||||||
@@ -37,8 +37,8 @@ func (wkc *WellKnownController) openIDConfigurationHandler(c *gin.Context) {
|
|||||||
"userinfo_endpoint": appUrl + "/api/oidc/userinfo",
|
"userinfo_endpoint": appUrl + "/api/oidc/userinfo",
|
||||||
"end_session_endpoint": appUrl + "/api/oidc/end-session",
|
"end_session_endpoint": appUrl + "/api/oidc/end-session",
|
||||||
"jwks_uri": appUrl + "/.well-known/jwks.json",
|
"jwks_uri": appUrl + "/.well-known/jwks.json",
|
||||||
"scopes_supported": []string{"openid", "profile", "email"},
|
"scopes_supported": []string{"openid", "profile", "email", "groups"},
|
||||||
"claims_supported": []string{"sub", "given_name", "family_name", "name", "email", "email_verified", "preferred_username"},
|
"claims_supported": []string{"sub", "given_name", "family_name", "name", "email", "email_verified", "preferred_username", "picture", "groups"},
|
||||||
"response_types_supported": []string{"code", "id_token"},
|
"response_types_supported": []string{"code", "id_token"},
|
||||||
"subject_types_supported": []string{"public"},
|
"subject_types_supported": []string{"public"},
|
||||||
"id_token_signing_alg_values_supported": []string{"RS256"},
|
"id_token_signing_alg_values_supported": []string{"RS256"},
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ type AppConfigUpdateDto struct {
|
|||||||
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" binding:"required,oneof=none starttls tls"`
|
||||||
SmtpSkipCertVerify string `json:"smtpSkipCertVerify"`
|
SmtpSkipCertVerify string `json:"smtpSkipCertVerify"`
|
||||||
LdapEnabled string `json:"ldapEnabled" binding:"required"`
|
LdapEnabled string `json:"ldapEnabled" binding:"required"`
|
||||||
LdapUrl string `json:"ldapUrl"`
|
LdapUrl string `json:"ldapUrl"`
|
||||||
@@ -36,6 +36,7 @@ type AppConfigUpdateDto struct {
|
|||||||
LdapAttributeUserEmail string `json:"ldapAttributeUserEmail"`
|
LdapAttributeUserEmail string `json:"ldapAttributeUserEmail"`
|
||||||
LdapAttributeUserFirstName string `json:"ldapAttributeUserFirstName"`
|
LdapAttributeUserFirstName string `json:"ldapAttributeUserFirstName"`
|
||||||
LdapAttributeUserLastName string `json:"ldapAttributeUserLastName"`
|
LdapAttributeUserLastName string `json:"ldapAttributeUserLastName"`
|
||||||
|
LdapAttributeUserProfilePicture string `json:"ldapAttributeUserProfilePicture"`
|
||||||
LdapAttributeGroupMember string `json:"ldapAttributeGroupMember"`
|
LdapAttributeGroupMember string `json:"ldapAttributeGroupMember"`
|
||||||
LdapAttributeGroupUniqueIdentifier string `json:"ldapAttributeGroupUniqueIdentifier"`
|
LdapAttributeGroupUniqueIdentifier string `json:"ldapAttributeGroupUniqueIdentifier"`
|
||||||
LdapAttributeGroupName string `json:"ldapAttributeGroupName"`
|
LdapAttributeGroupName string `json:"ldapAttributeGroupName"`
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ type AppConfig struct {
|
|||||||
LdapAttributeUserEmail AppConfigVariable
|
LdapAttributeUserEmail AppConfigVariable
|
||||||
LdapAttributeUserFirstName AppConfigVariable
|
LdapAttributeUserFirstName AppConfigVariable
|
||||||
LdapAttributeUserLastName AppConfigVariable
|
LdapAttributeUserLastName AppConfigVariable
|
||||||
|
LdapAttributeUserProfilePicture AppConfigVariable
|
||||||
LdapAttributeGroupMember AppConfigVariable
|
LdapAttributeGroupMember AppConfigVariable
|
||||||
LdapAttributeGroupUniqueIdentifier AppConfigVariable
|
LdapAttributeGroupUniqueIdentifier AppConfigVariable
|
||||||
LdapAttributeGroupName AppConfigVariable
|
LdapAttributeGroupName AppConfigVariable
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ func NewAppConfigService(db *gorm.DB) *AppConfigService {
|
|||||||
if err := service.InitDbConfig(); err != nil {
|
if err := service.InitDbConfig(); err != nil {
|
||||||
log.Fatalf("Failed to initialize app config service: %v", err)
|
log.Fatalf("Failed to initialize app config service: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return service
|
return service
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -96,8 +97,8 @@ var defaultDbConfig = model.AppConfig{
|
|||||||
},
|
},
|
||||||
SmtpTls: model.AppConfigVariable{
|
SmtpTls: model.AppConfigVariable{
|
||||||
Key: "smtpTls",
|
Key: "smtpTls",
|
||||||
Type: "bool",
|
Type: "string",
|
||||||
DefaultValue: "true",
|
DefaultValue: "none",
|
||||||
},
|
},
|
||||||
SmtpSkipCertVerify: model.AppConfigVariable{
|
SmtpSkipCertVerify: model.AppConfigVariable{
|
||||||
Key: "smtpSkipCertVerify",
|
Key: "smtpSkipCertVerify",
|
||||||
@@ -173,6 +174,10 @@ var defaultDbConfig = model.AppConfig{
|
|||||||
Key: "ldapAttributeUserLastName",
|
Key: "ldapAttributeUserLastName",
|
||||||
Type: "string",
|
Type: "string",
|
||||||
},
|
},
|
||||||
|
LdapAttributeUserProfilePicture: model.AppConfigVariable{
|
||||||
|
Key: "ldapAttributeUserProfilePicture",
|
||||||
|
Type: "string",
|
||||||
|
},
|
||||||
LdapAttributeGroupMember: model.AppConfigVariable{
|
LdapAttributeGroupMember: model.AppConfigVariable{
|
||||||
Key: "ldapAttributeGroupMember",
|
Key: "ldapAttributeGroupMember",
|
||||||
Type: "string",
|
Type: "string",
|
||||||
|
|||||||
@@ -3,27 +3,23 @@ package service
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
htemplate "html/template"
|
"github.com/emersion/go-sasl"
|
||||||
"mime/multipart"
|
"github.com/emersion/go-smtp"
|
||||||
"mime/quotedprintable"
|
|
||||||
"net"
|
|
||||||
"net/smtp"
|
|
||||||
"net/textproto"
|
|
||||||
"os"
|
|
||||||
ttemplate "text/template"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/pocket-id/pocket-id/backend/internal/common"
|
"github.com/pocket-id/pocket-id/backend/internal/common"
|
||||||
"github.com/pocket-id/pocket-id/backend/internal/model"
|
"github.com/pocket-id/pocket-id/backend/internal/model"
|
||||||
"github.com/pocket-id/pocket-id/backend/internal/utils/email"
|
"github.com/pocket-id/pocket-id/backend/internal/utils/email"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
|
htemplate "html/template"
|
||||||
|
"mime/multipart"
|
||||||
|
"mime/quotedprintable"
|
||||||
|
"net/textproto"
|
||||||
|
"os"
|
||||||
|
ttemplate "text/template"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
var netDialer = &net.Dialer{
|
|
||||||
Timeout: 3 * time.Second,
|
|
||||||
}
|
|
||||||
|
|
||||||
type EmailService struct {
|
type EmailService struct {
|
||||||
appConfigService *AppConfigService
|
appConfigService *AppConfigService
|
||||||
db *gorm.DB
|
db *gorm.DB
|
||||||
@@ -114,105 +110,57 @@ func (srv *EmailService) getSmtpClient() (client *smtp.Client, err error) {
|
|||||||
ServerName: srv.appConfigService.DbConfig.SmtpHost.Value,
|
ServerName: srv.appConfigService.DbConfig.SmtpHost.Value,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Connect to the SMTP server
|
// Connect to the SMTP server based on TLS setting
|
||||||
if srv.appConfigService.DbConfig.SmtpTls.Value == "false" {
|
switch srv.appConfigService.DbConfig.SmtpTls.Value {
|
||||||
client, err = srv.connectToSmtpServer(smtpAddress)
|
case "none":
|
||||||
} else if port == "465" {
|
client, err = smtp.Dial(smtpAddress)
|
||||||
client, err = srv.connectToSmtpServerUsingImplicitTLS(
|
case "tls":
|
||||||
smtpAddress,
|
client, err = smtp.DialTLS(smtpAddress, tlsConfig)
|
||||||
tlsConfig,
|
case "starttls":
|
||||||
)
|
client, err = smtp.DialStartTLS(
|
||||||
} else {
|
|
||||||
client, err = srv.connectToSmtpServerUsingStartTLS(
|
|
||||||
smtpAddress,
|
smtpAddress,
|
||||||
tlsConfig,
|
tlsConfig,
|
||||||
)
|
)
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("invalid SMTP TLS setting: %s", srv.appConfigService.DbConfig.SmtpTls.Value)
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to connect to SMTP server: %w", err)
|
return nil, fmt.Errorf("failed to connect to SMTP server: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
client.CommandTimeout = 10 * time.Second
|
||||||
|
|
||||||
|
// Send the HELO command
|
||||||
|
if err := srv.sendHelloCommand(client); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to send HELO command: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
// Set up the authentication if user or password are set
|
// Set up the authentication if user or password are set
|
||||||
smtpUser := srv.appConfigService.DbConfig.SmtpUser.Value
|
smtpUser := srv.appConfigService.DbConfig.SmtpUser.Value
|
||||||
smtpPassword := srv.appConfigService.DbConfig.SmtpPassword.Value
|
smtpPassword := srv.appConfigService.DbConfig.SmtpPassword.Value
|
||||||
|
|
||||||
if smtpUser != "" || smtpPassword != "" {
|
if smtpUser != "" || smtpPassword != "" {
|
||||||
auth := smtp.PlainAuth("",
|
// Authenticate with plain auth
|
||||||
srv.appConfigService.DbConfig.SmtpUser.Value,
|
auth := sasl.NewPlainClient("", smtpUser, smtpPassword)
|
||||||
srv.appConfigService.DbConfig.SmtpPassword.Value,
|
|
||||||
srv.appConfigService.DbConfig.SmtpHost.Value,
|
|
||||||
)
|
|
||||||
if err := client.Auth(auth); err != nil {
|
if err := client.Auth(auth); err != nil {
|
||||||
return nil, fmt.Errorf("failed to authenticate SMTP client: %w", err)
|
// If the server does not support plain auth, try login auth
|
||||||
|
var smtpErr *smtp.SMTPError
|
||||||
|
ok := errors.As(err, &smtpErr)
|
||||||
|
if ok && smtpErr.Code == smtp.ErrAuthUnknownMechanism.Code {
|
||||||
|
auth = sasl.NewLoginClient(smtpUser, smtpPassword)
|
||||||
|
err = client.Auth(auth)
|
||||||
|
}
|
||||||
|
// Both plain and login auth failed
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to authenticate: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return client, err
|
return client, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (srv *EmailService) connectToSmtpServer(serverAddr string) (*smtp.Client, error) {
|
|
||||||
conn, err := netDialer.Dial("tcp", serverAddr)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to connect to SMTP server: %w", err)
|
|
||||||
}
|
|
||||||
client, err := smtp.NewClient(conn, srv.appConfigService.DbConfig.SmtpHost.Value)
|
|
||||||
if err != nil {
|
|
||||||
conn.Close()
|
|
||||||
return nil, fmt.Errorf("failed to create SMTP client: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := srv.sendHelloCommand(client); err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to say hello to SMTP server: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return client, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (srv *EmailService) connectToSmtpServerUsingImplicitTLS(serverAddr string, tlsConfig *tls.Config) (*smtp.Client, error) {
|
|
||||||
tlsDialer := &tls.Dialer{
|
|
||||||
NetDialer: netDialer,
|
|
||||||
Config: tlsConfig,
|
|
||||||
}
|
|
||||||
conn, err := tlsDialer.Dial("tcp", serverAddr)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to connect to SMTP server: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
client, err := smtp.NewClient(conn, srv.appConfigService.DbConfig.SmtpHost.Value)
|
|
||||||
if err != nil {
|
|
||||||
conn.Close()
|
|
||||||
return nil, fmt.Errorf("failed to create SMTP client: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := srv.sendHelloCommand(client); err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to say hello to SMTP server: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return client, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (srv *EmailService) connectToSmtpServerUsingStartTLS(serverAddr string, tlsConfig *tls.Config) (*smtp.Client, error) {
|
|
||||||
conn, err := netDialer.Dial("tcp", serverAddr)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to connect to SMTP server: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
client, err := smtp.NewClient(conn, srv.appConfigService.DbConfig.SmtpHost.Value)
|
|
||||||
if err != nil {
|
|
||||||
conn.Close()
|
|
||||||
return nil, fmt.Errorf("failed to create SMTP client: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := srv.sendHelloCommand(client); err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to say hello to SMTP server: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := client.StartTLS(tlsConfig); err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to start TLS: %w", err)
|
|
||||||
}
|
|
||||||
return client, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (srv *EmailService) sendHelloCommand(client *smtp.Client) error {
|
func (srv *EmailService) sendHelloCommand(client *smtp.Client) error {
|
||||||
hostname, err := os.Hostname()
|
hostname, err := os.Hostname()
|
||||||
if err == nil {
|
if err == nil {
|
||||||
@@ -224,23 +172,33 @@ func (srv *EmailService) sendHelloCommand(client *smtp.Client) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (srv *EmailService) sendEmailContent(client *smtp.Client, toEmail email.Address, c *email.Composer) error {
|
func (srv *EmailService) sendEmailContent(client *smtp.Client, toEmail email.Address, c *email.Composer) error {
|
||||||
if err := client.Mail(srv.appConfigService.DbConfig.SmtpFrom.Value); err != nil {
|
// Set the sender
|
||||||
|
if err := client.Mail(srv.appConfigService.DbConfig.SmtpFrom.Value, nil); err != nil {
|
||||||
return fmt.Errorf("failed to set sender: %w", err)
|
return fmt.Errorf("failed to set sender: %w", err)
|
||||||
}
|
}
|
||||||
if err := client.Rcpt(toEmail.Email); err != nil {
|
|
||||||
|
// Set the recipient
|
||||||
|
if err := client.Rcpt(toEmail.Email, nil); err != nil {
|
||||||
return fmt.Errorf("failed to set recipient: %w", err)
|
return fmt.Errorf("failed to set recipient: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get a writer to write the email data
|
||||||
w, err := client.Data()
|
w, err := client.Data()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to start data: %w", err)
|
return fmt.Errorf("failed to start data: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Write the email content
|
||||||
_, err = w.Write([]byte(c.String()))
|
_, err = w.Write([]byte(c.String()))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to write email data: %w", err)
|
return fmt.Errorf("failed to write email data: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Close the writer
|
||||||
if err := w.Close(); err != nil {
|
if err := w.Close(); err != nil {
|
||||||
return fmt.Errorf("failed to close data writer: %w", err)
|
return fmt.Errorf("failed to close data writer: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,15 @@
|
|||||||
package service
|
package service
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
|
"encoding/base64"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"log"
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/go-ldap/ldap/v3"
|
"github.com/go-ldap/ldap/v3"
|
||||||
@@ -89,7 +95,6 @@ func (s *LdapService) SyncGroups() error {
|
|||||||
ldapGroupIDs := make(map[string]bool)
|
ldapGroupIDs := make(map[string]bool)
|
||||||
|
|
||||||
for _, value := range result.Entries {
|
for _, value := range result.Entries {
|
||||||
var usersToAddDto dto.UserGroupUpdateUsersDto
|
|
||||||
var membersUserId []string
|
var membersUserId []string
|
||||||
|
|
||||||
ldapId := value.GetAttributeValue(uniqueIdentifierAttribute)
|
ldapId := value.GetAttributeValue(uniqueIdentifierAttribute)
|
||||||
@@ -107,7 +112,16 @@ func (s *LdapService) SyncGroups() error {
|
|||||||
singleMember := strings.Split(strings.Split(member, "=")[1], ",")[0]
|
singleMember := strings.Split(strings.Split(member, "=")[1], ",")[0]
|
||||||
|
|
||||||
var databaseUser model.User
|
var databaseUser model.User
|
||||||
s.db.Where("username = ?", singleMember).Where("ldap_id IS NOT NULL").First(&databaseUser)
|
err := s.db.Where("username = ? AND ldap_id IS NOT NULL", singleMember).First(&databaseUser).Error
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
// The user collides with a non-LDAP user, so we skip it
|
||||||
|
continue
|
||||||
|
} else {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
membersUserId = append(membersUserId, databaseUser.ID)
|
membersUserId = append(membersUserId, databaseUser.ID)
|
||||||
}
|
}
|
||||||
@@ -118,7 +132,7 @@ func (s *LdapService) SyncGroups() error {
|
|||||||
LdapID: value.GetAttributeValue(uniqueIdentifierAttribute),
|
LdapID: value.GetAttributeValue(uniqueIdentifierAttribute),
|
||||||
}
|
}
|
||||||
|
|
||||||
usersToAddDto = dto.UserGroupUpdateUsersDto{
|
usersToAddDto := dto.UserGroupUpdateUsersDto{
|
||||||
UserIDs: membersUserId,
|
UserIDs: membersUserId,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -177,6 +191,7 @@ func (s *LdapService) SyncUsers() error {
|
|||||||
emailAttribute := s.appConfigService.DbConfig.LdapAttributeUserEmail.Value
|
emailAttribute := s.appConfigService.DbConfig.LdapAttributeUserEmail.Value
|
||||||
firstNameAttribute := s.appConfigService.DbConfig.LdapAttributeUserFirstName.Value
|
firstNameAttribute := s.appConfigService.DbConfig.LdapAttributeUserFirstName.Value
|
||||||
lastNameAttribute := s.appConfigService.DbConfig.LdapAttributeUserLastName.Value
|
lastNameAttribute := s.appConfigService.DbConfig.LdapAttributeUserLastName.Value
|
||||||
|
profilePictureAttribute := s.appConfigService.DbConfig.LdapAttributeUserProfilePicture.Value
|
||||||
adminGroupAttribute := s.appConfigService.DbConfig.LdapAttributeAdminGroup.Value
|
adminGroupAttribute := s.appConfigService.DbConfig.LdapAttributeAdminGroup.Value
|
||||||
filter := s.appConfigService.DbConfig.LdapUserSearchFilter.Value
|
filter := s.appConfigService.DbConfig.LdapUserSearchFilter.Value
|
||||||
|
|
||||||
@@ -189,6 +204,7 @@ func (s *LdapService) SyncUsers() error {
|
|||||||
emailAttribute,
|
emailAttribute,
|
||||||
firstNameAttribute,
|
firstNameAttribute,
|
||||||
lastNameAttribute,
|
lastNameAttribute,
|
||||||
|
profilePictureAttribute,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Filters must start and finish with ()!
|
// Filters must start and finish with ()!
|
||||||
@@ -237,9 +253,14 @@ func (s *LdapService) SyncUsers() error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Error syncing user %s: %s", newUser.Username, err)
|
log.Printf("Error syncing user %s: %s", newUser.Username, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Save profile picture
|
||||||
|
if pictureString := value.GetAttributeValue(profilePictureAttribute); pictureString != "" {
|
||||||
|
if err := s.SaveProfilePicture(databaseUser.ID, pictureString); err != nil {
|
||||||
|
log.Printf("Error saving profile picture for user %s: %s", newUser.Username, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get all LDAP users from the database
|
// Get all LDAP users from the database
|
||||||
@@ -251,7 +272,7 @@ func (s *LdapService) SyncUsers() error {
|
|||||||
// Delete users that no longer exist in LDAP
|
// Delete users that no longer exist in LDAP
|
||||||
for _, user := range ldapUsersInDb {
|
for _, user := range ldapUsersInDb {
|
||||||
if _, exists := ldapUserIDs[*user.LdapID]; !exists {
|
if _, exists := ldapUserIDs[*user.LdapID]; !exists {
|
||||||
if err := s.db.Delete(&model.User{}, "ldap_id = ?", user.LdapID).Error; err != nil {
|
if err := s.userService.DeleteUser(user.ID); err != nil {
|
||||||
log.Printf("Failed to delete user %s with: %v", user.Username, err)
|
log.Printf("Failed to delete user %s with: %v", user.Username, err)
|
||||||
} else {
|
} else {
|
||||||
log.Printf("Deleted user %s", user.Username)
|
log.Printf("Deleted user %s", user.Username)
|
||||||
@@ -260,3 +281,33 @@ func (s *LdapService) SyncUsers() error {
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *LdapService) SaveProfilePicture(userId string, pictureString string) error {
|
||||||
|
var reader io.Reader
|
||||||
|
|
||||||
|
if _, err := url.ParseRequestURI(pictureString); err == nil {
|
||||||
|
// If the photo is a URL, download it
|
||||||
|
response, err := http.Get(pictureString)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to download profile picture: %w", err)
|
||||||
|
}
|
||||||
|
defer response.Body.Close()
|
||||||
|
|
||||||
|
reader = response.Body
|
||||||
|
|
||||||
|
} else if decodedPhoto, err := base64.StdEncoding.DecodeString(pictureString); err == nil {
|
||||||
|
// If the photo is a base64 encoded string, decode it
|
||||||
|
reader = bytes.NewReader(decodedPhoto)
|
||||||
|
|
||||||
|
} else {
|
||||||
|
// If the photo is a string, we assume that it's a binary string
|
||||||
|
reader = bytes.NewReader([]byte(pictureString))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the profile picture
|
||||||
|
if err := s.userService.UpdateProfilePicture(userId, reader); err != nil {
|
||||||
|
return fmt.Errorf("failed to update profile picture: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -401,6 +401,7 @@ func (s *OidcService) GetUserClaimsForClient(userID string, clientID string) (ma
|
|||||||
"family_name": user.LastName,
|
"family_name": user.LastName,
|
||||||
"name": user.FullName(),
|
"name": user.FullName(),
|
||||||
"preferred_username": user.Username,
|
"preferred_username": user.Username,
|
||||||
|
"picture": fmt.Sprintf("%s/api/users/%s/profile-picture.png", common.EnvConfig.AppURL, user.ID),
|
||||||
}
|
}
|
||||||
|
|
||||||
if strings.Contains(scope, "profile") {
|
if strings.Contains(scope, "profile") {
|
||||||
|
|||||||
@@ -3,8 +3,12 @@ package service
|
|||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"github.com/pocket-id/pocket-id/backend/internal/utils/image"
|
||||||
|
"io"
|
||||||
"log"
|
"log"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -48,6 +52,71 @@ func (s *UserService) GetUser(userID string) (model.User, error) {
|
|||||||
return user, err
|
return user, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *UserService) GetProfilePicture(userID string) (io.Reader, int64, error) {
|
||||||
|
// Validate the user ID to prevent directory traversal
|
||||||
|
if err := uuid.Validate(userID); err != nil {
|
||||||
|
return nil, 0, &common.InvalidUUIDError{}
|
||||||
|
}
|
||||||
|
|
||||||
|
profilePicturePath := fmt.Sprintf("%s/profile-pictures/%s.png", common.EnvConfig.UploadPath, userID)
|
||||||
|
file, err := os.Open(profilePicturePath)
|
||||||
|
if err == nil {
|
||||||
|
// Get the file size
|
||||||
|
fileInfo, err := file.Stat()
|
||||||
|
if err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
return file, fileInfo.Size(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the file does not exist, return the default profile picture
|
||||||
|
user, err := s.GetUser(userID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultPicture, err := profilepicture.CreateDefaultProfilePicture(user.FirstName, user.LastName)
|
||||||
|
if err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return defaultPicture, int64(defaultPicture.Len()), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *UserService) UpdateProfilePicture(userID string, file io.Reader) error {
|
||||||
|
// Validate the user ID to prevent directory traversal
|
||||||
|
if err := uuid.Validate(userID); err != nil {
|
||||||
|
return &common.InvalidUUIDError{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert the image to a smaller square image
|
||||||
|
profilePicture, err := profilepicture.CreateProfilePicture(file)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure the directory exists
|
||||||
|
profilePictureDir := fmt.Sprintf("%s/profile-pictures", common.EnvConfig.UploadPath)
|
||||||
|
if err := os.MkdirAll(profilePictureDir, os.ModePerm); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the profile picture file
|
||||||
|
createdProfilePicture, err := os.Create(fmt.Sprintf("%s/%s.png", profilePictureDir, userID))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer createdProfilePicture.Close()
|
||||||
|
|
||||||
|
// Copy the image to the file
|
||||||
|
_, err = io.Copy(createdProfilePicture, profilePicture)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (s *UserService) DeleteUser(userID string) error {
|
func (s *UserService) DeleteUser(userID string) 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 {
|
||||||
@@ -59,6 +128,12 @@ func (s *UserService) DeleteUser(userID string) error {
|
|||||||
return &common.LdapUserUpdateError{}
|
return &common.LdapUserUpdateError{}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Delete the profile picture
|
||||||
|
profilePicturePath := fmt.Sprintf("%s/profile-pictures/%s.png", common.EnvConfig.UploadPath, userID)
|
||||||
|
if err := os.Remove(profilePicturePath); err != nil && !os.IsNotExist(err) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
return s.db.Delete(&user).Error
|
return s.db.Delete(&user).Error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
97
backend/internal/utils/image/profile_picture.go
Normal file
97
backend/internal/utils/image/profile_picture.go
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
package profilepicture
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"github.com/disintegration/imageorient"
|
||||||
|
"github.com/disintegration/imaging"
|
||||||
|
"github.com/pocket-id/pocket-id/backend/resources"
|
||||||
|
"golang.org/x/image/font"
|
||||||
|
"golang.org/x/image/font/opentype"
|
||||||
|
"golang.org/x/image/math/fixed"
|
||||||
|
"image"
|
||||||
|
"image/color"
|
||||||
|
"io"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
const profilePictureSize = 300
|
||||||
|
|
||||||
|
// CreateProfilePicture resizes the profile picture to a square
|
||||||
|
func CreateProfilePicture(file io.Reader) (*bytes.Buffer, error) {
|
||||||
|
img, _, err := imageorient.Decode(file)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to decode image: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
img = imaging.Fill(img, profilePictureSize, profilePictureSize, imaging.Center, imaging.Lanczos)
|
||||||
|
|
||||||
|
var buf bytes.Buffer
|
||||||
|
err = imaging.Encode(&buf, img, imaging.PNG)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to encode image: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &buf, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateDefaultProfilePicture creates a profile picture with the initials
|
||||||
|
func CreateDefaultProfilePicture(firstName, lastName string) (*bytes.Buffer, error) {
|
||||||
|
// Get the initials
|
||||||
|
initials := ""
|
||||||
|
if len(firstName) > 0 {
|
||||||
|
initials += string(firstName[0])
|
||||||
|
}
|
||||||
|
if len(lastName) > 0 {
|
||||||
|
initials += string(lastName[0])
|
||||||
|
}
|
||||||
|
initials = strings.ToUpper(initials)
|
||||||
|
|
||||||
|
// Create a blank image with a white background
|
||||||
|
img := imaging.New(profilePictureSize, profilePictureSize, color.RGBA{R: 255, G: 255, B: 255, A: 255})
|
||||||
|
|
||||||
|
// Load the font
|
||||||
|
fontBytes, err := resources.FS.ReadFile("fonts/PlayfairDisplay-Bold.ttf")
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to read font file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse the font
|
||||||
|
fontFace, err := opentype.Parse(fontBytes)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to parse font: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a font.Face with a specific size
|
||||||
|
fontSize := 160.0
|
||||||
|
face, err := opentype.NewFace(fontFace, &opentype.FaceOptions{
|
||||||
|
Size: fontSize,
|
||||||
|
DPI: 72,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create font face: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a drawer for the image
|
||||||
|
drawer := &font.Drawer{
|
||||||
|
Dst: img,
|
||||||
|
Src: image.NewUniform(color.RGBA{R: 0, G: 0, B: 0, A: 255}), // Black text color
|
||||||
|
Face: face,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Center the initials
|
||||||
|
x := (profilePictureSize - font.MeasureString(face, initials).Ceil()) / 2
|
||||||
|
y := (profilePictureSize-face.Metrics().Height.Ceil())/2 + face.Metrics().Ascent.Ceil() - 10
|
||||||
|
drawer.Dot = fixed.P(x, y)
|
||||||
|
|
||||||
|
// Draw the initials
|
||||||
|
drawer.DrawString(initials)
|
||||||
|
|
||||||
|
var buf bytes.Buffer
|
||||||
|
err = imaging.Encode(&buf, img, imaging.PNG)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to encode image: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &buf, nil
|
||||||
|
}
|
||||||
@@ -1,95 +1,92 @@
|
|||||||
{{ define "style" }}
|
{{ define "style" }}
|
||||||
<style>
|
<style>
|
||||||
body {
|
/* Reset styles for email clients */
|
||||||
font-family: Arial, sans-serif;
|
body, table, td, p, a {
|
||||||
background-color: #f0f0f0;
|
margin: 0;
|
||||||
color: #333;
|
padding: 0;
|
||||||
margin: 0;
|
border: 0;
|
||||||
padding: 0;
|
font-size: 100%;
|
||||||
}
|
font-family: Arial, sans-serif;
|
||||||
.container {
|
line-height: 1.5;
|
||||||
background-color: #fff;
|
}
|
||||||
color: #333;
|
body {
|
||||||
padding: 32px;
|
background-color: #f0f0f0;
|
||||||
border-radius: 10px;
|
color: #333;
|
||||||
max-width: 600px;
|
}
|
||||||
margin: 40px auto;
|
.container {
|
||||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
width: 100%;
|
||||||
}
|
max-width: 600px;
|
||||||
.header {
|
margin: 40px auto;
|
||||||
display: flex;
|
background-color: #fff;
|
||||||
justify-content: space-between;
|
border-radius: 10px;
|
||||||
align-items: center;
|
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||||
margin-bottom: 24px;
|
padding: 32px;
|
||||||
}
|
}
|
||||||
.header .logo {
|
.header {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
margin-bottom: 24px;
|
||||||
gap: 8px;
|
}
|
||||||
}
|
.header .logo img {
|
||||||
.header .logo img {
|
width: 32px;
|
||||||
width: 32px;
|
height: 32px;
|
||||||
height: 32px;
|
vertical-align: middle;
|
||||||
object-fit: cover;
|
}
|
||||||
}
|
.header h1 {
|
||||||
.header h1 {
|
font-size: 1.5rem;
|
||||||
font-size: 1.5rem;
|
font-weight: bold;
|
||||||
font-weight: bold;
|
display: inline-block;
|
||||||
}
|
vertical-align: middle;
|
||||||
.warning {
|
margin-left: 8px;
|
||||||
background-color: #ffd966;
|
}
|
||||||
color: #7f6000;
|
.warning {
|
||||||
padding: 4px 12px;
|
background-color: #ffd966;
|
||||||
border-radius: 50px;
|
color: #7f6000;
|
||||||
font-size: 0.875rem;
|
padding: 4px 12px;
|
||||||
}
|
border-radius: 50px;
|
||||||
.content {
|
font-size: 0.875rem;
|
||||||
background-color: #fafafa;
|
margin: auto 0 auto auto;
|
||||||
color: #333;
|
}
|
||||||
padding: 24px;
|
.content {
|
||||||
border-radius: 10px;
|
background-color: #fafafa;
|
||||||
}
|
padding: 24px;
|
||||||
.content h2 {
|
border-radius: 10px;
|
||||||
font-size: 1.25rem;
|
}
|
||||||
font-weight: bold;
|
.content h2 {
|
||||||
margin-bottom: 16px;
|
font-size: 1.25rem;
|
||||||
}
|
font-weight: bold;
|
||||||
.grid {
|
margin-bottom: 16px;
|
||||||
display: grid;
|
}
|
||||||
grid-template-columns: 1fr 1fr;
|
.grid {
|
||||||
gap: 16px;
|
width: 100%;
|
||||||
margin-bottom: 16px;
|
margin-bottom: 16px;
|
||||||
}
|
}
|
||||||
.grid div {
|
.grid td {
|
||||||
display: flex;
|
width: 50%;
|
||||||
flex-direction: column;
|
padding-bottom: 8px;
|
||||||
}
|
vertical-align: top;
|
||||||
.grid p {
|
}
|
||||||
margin: 0;
|
.label {
|
||||||
}
|
color: #888;
|
||||||
.label {
|
font-size: 0.875rem;
|
||||||
color: #888;
|
}
|
||||||
font-size: 0.875rem;
|
.message {
|
||||||
margin-bottom: 4px;
|
font-size: 1rem;
|
||||||
}
|
line-height: 1.5;
|
||||||
.message {
|
margin-top: 16px;
|
||||||
font-size: 1rem;
|
}
|
||||||
line-height: 1.5;
|
.button {
|
||||||
}
|
background-color: #000000;
|
||||||
.button {
|
color: #ffffff;
|
||||||
border-radius: 0.375rem;
|
padding: 0.7rem 1.5rem;
|
||||||
font-size: 1rem;
|
text-decoration: none;
|
||||||
font-weight: 500;
|
border-radius: 4px;
|
||||||
background-color: #000000;
|
font-size: 1rem;
|
||||||
color: #ffffff;
|
font-weight: 500;
|
||||||
padding: 0.7rem 1.5rem;
|
display: inline-block;
|
||||||
outline: none;
|
margin-top: 24px;
|
||||||
border: none;
|
}
|
||||||
text-decoration: none;
|
.button-container {
|
||||||
}
|
text-align: center;
|
||||||
.button-container {
|
}
|
||||||
text-align: center;
|
|
||||||
margin-top: 24px;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
|||||||
@@ -1,36 +1,40 @@
|
|||||||
{{ define "base" }}
|
{{ define "base" }}
|
||||||
<div class="header">
|
<div class="header">
|
||||||
<div class="logo">
|
<div class="logo">
|
||||||
<img src="{{ .LogoURL }}" alt="{{ .AppName }}"/>
|
<img src="{{ .LogoURL }}" alt="{{ .AppName }}"/>
|
||||||
<h1>{{ .AppName }}</h1>
|
<h1>{{ .AppName }}</h1>
|
||||||
</div>
|
</div>
|
||||||
<div class="warning">Warning</div>
|
<div class="warning">Warning</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<h2>New Sign-In Detected</h2>
|
<h2>New Sign-In Detected</h2>
|
||||||
<div class="grid">
|
<table class="grid">
|
||||||
{{ if and .Data.City .Data.Country }}
|
<tr>
|
||||||
<div>
|
{{ if and .Data.City .Data.Country }}
|
||||||
<p class="label">Approximate Location</p>
|
<td>
|
||||||
<p>{{ .Data.City }}, {{ .Data.Country }}</p>
|
<p class="label">Approximate Location</p>
|
||||||
</div>
|
<p>{{ .Data.City }}, {{ .Data.Country }}</p>
|
||||||
{{ end }}
|
</td>
|
||||||
<div>
|
{{ end }}
|
||||||
<p class="label">IP Address</p>
|
<td>
|
||||||
<p>{{ .Data.IPAddress }}</p>
|
<p class="label">IP Address</p>
|
||||||
</div>
|
<p>{{ .Data.IPAddress }}</p>
|
||||||
<div>
|
</td>
|
||||||
<p class="label">Device</p>
|
</tr>
|
||||||
<p>{{ .Data.Device }}</p>
|
<tr>
|
||||||
</div>
|
<td>
|
||||||
<div>
|
<p class="label">Device</p>
|
||||||
<p class="label">Sign-In Time</p>
|
<p>{{ .Data.Device }}</p>
|
||||||
<p>{{ .Data.DateTime.Format "2006-01-02 15:04:05 UTC" }}</p>
|
</td>
|
||||||
</div>
|
<td>
|
||||||
</div>
|
<p class="label">Sign-In Time</p>
|
||||||
<p class="message">
|
<p>{{ .Data.DateTime.Format "2006-01-02 15:04:05 UTC" }}</p>
|
||||||
This sign-in was detected from a new device or location. If you recognize this activity, you can
|
</td>
|
||||||
safely ignore this message. If not, please review your account and security settings.
|
</tr>
|
||||||
</p>
|
</table>
|
||||||
</div>
|
<p class="message">
|
||||||
|
This sign-in was detected from a new device or location. If you recognize this activity, you can
|
||||||
|
safely ignore this message. If not, please review your account and security settings.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
{{ end -}}
|
{{ end -}}
|
||||||
@@ -4,5 +4,5 @@ import "embed"
|
|||||||
|
|
||||||
// Embedded file systems for the project
|
// Embedded file systems for the project
|
||||||
|
|
||||||
//go:embed email-templates images migrations
|
//go:embed email-templates images migrations fonts
|
||||||
var FS embed.FS
|
var FS embed.FS
|
||||||
|
|||||||
BIN
backend/resources/fonts/PlayfairDisplay-Bold.ttf
Normal file
BIN
backend/resources/fonts/PlayfairDisplay-Bold.ttf
Normal file
Binary file not shown.
@@ -0,0 +1 @@
|
|||||||
|
UPDATE app_config_variables SET value = 'true' WHERE key = 'smtpTls';
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
UPDATE app_config_variables AS target
|
||||||
|
SET value = CASE
|
||||||
|
WHEN target.value = 'true' AND (SELECT value FROM app_config_variables WHERE key = 'smtpPort' LIMIT 1) = '587' THEN 'starttls'
|
||||||
|
WHEN target.value = 'true' THEN 'tls'
|
||||||
|
ELSE 'none'
|
||||||
|
END
|
||||||
|
WHERE target.key = 'smtpTls';
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
UPDATE app_config_variables SET value = 'true' WHERE key = 'smtpTls';
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
UPDATE app_config_variables
|
||||||
|
SET value = CASE
|
||||||
|
WHEN value = 'true' AND (SELECT value FROM app_config_variables WHERE key = 'smtpPort' LIMIT 1) = '587' THEN 'starttls'
|
||||||
|
WHEN value = 'true' THEN 'tls'
|
||||||
|
ELSE 'none'
|
||||||
|
END
|
||||||
|
WHERE key = 'smtpTls';
|
||||||
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.30.0",
|
"version": "0.35.2",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "pocket-id-frontend",
|
"name": "pocket-id-frontend",
|
||||||
"version": "0.30.0",
|
"version": "0.35.2",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@simplewebauthn/browser": "^13.1.0",
|
"@simplewebauthn/browser": "^13.1.0",
|
||||||
"@tailwindcss/vite": "^4.0.0",
|
"@tailwindcss/vite": "^4.0.0",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "pocket-id-frontend",
|
"name": "pocket-id-frontend",
|
||||||
"version": "0.34.0",
|
"version": "0.35.6",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Checkbox } from './ui/checkbox';
|
import { Checkbox } from '$lib/components/ui/checkbox';
|
||||||
import { Label } from './ui/label';
|
import { Label } from '$lib/components/ui/label';
|
||||||
|
|
||||||
let {
|
let {
|
||||||
id,
|
id,
|
||||||
@@ -31,7 +31,7 @@
|
|||||||
{label}
|
{label}
|
||||||
</Label>
|
</Label>
|
||||||
{#if description}
|
{#if description}
|
||||||
<p class="text-[0.8rem] text-muted-foreground">
|
<p class="text-muted-foreground text-[0.8rem]">
|
||||||
{description}
|
{description}
|
||||||
</p>
|
</p>
|
||||||
{/if}
|
{/if}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import FormInput from '$lib/components/form-input.svelte';
|
import FormInput from '$lib/components/form/form-input.svelte';
|
||||||
import { Button } from '$lib/components/ui/button';
|
import { Button } from '$lib/components/ui/button';
|
||||||
import { Input } from '$lib/components/ui/input';
|
import { Input } from '$lib/components/ui/input';
|
||||||
import CustomClaimService from '$lib/services/custom-claim-service';
|
import CustomClaimService from '$lib/services/custom-claim-service';
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
import { cn } from '$lib/utils/style';
|
import { cn } from '$lib/utils/style';
|
||||||
import type { HTMLInputAttributes } from 'svelte/elements';
|
import type { HTMLInputAttributes } from 'svelte/elements';
|
||||||
import type { VariantProps } from 'tailwind-variants';
|
import type { VariantProps } from 'tailwind-variants';
|
||||||
import type { buttonVariants } from './ui/button';
|
import type { buttonVariants } from '$lib/components/ui/button';
|
||||||
|
|
||||||
let {
|
let {
|
||||||
id,
|
id,
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
import type { FormInput } from '$lib/utils/form-util';
|
import type { FormInput } from '$lib/utils/form-util';
|
||||||
import type { Snippet } from 'svelte';
|
import type { Snippet } from 'svelte';
|
||||||
import type { HTMLAttributes } from 'svelte/elements';
|
import type { HTMLAttributes } from 'svelte/elements';
|
||||||
import { Input, type FormInputEvent } from './ui/input';
|
import { Input, type FormInputEvent } from '$lib/components/ui/input';
|
||||||
|
|
||||||
let {
|
let {
|
||||||
input = $bindable(),
|
input = $bindable(),
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import FileInput from '$lib/components/form/file-input.svelte';
|
||||||
|
import * as Avatar from '$lib/components/ui/avatar';
|
||||||
|
import { LucideLoader, LucideUpload } from 'lucide-svelte';
|
||||||
|
|
||||||
|
let {
|
||||||
|
userId,
|
||||||
|
isLdapUser = false,
|
||||||
|
callback
|
||||||
|
}: {
|
||||||
|
userId: string;
|
||||||
|
isLdapUser?: boolean;
|
||||||
|
callback: (image: File) => Promise<void>;
|
||||||
|
} = $props();
|
||||||
|
|
||||||
|
let isLoading = $state(false);
|
||||||
|
|
||||||
|
let imageDataURL = $state(`/api/users/${userId}/profile-picture.png`);
|
||||||
|
|
||||||
|
async function onImageChange(e: Event) {
|
||||||
|
const file = (e.target as HTMLInputElement).files?.[0] || null;
|
||||||
|
if (!file) return;
|
||||||
|
|
||||||
|
isLoading = true;
|
||||||
|
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = (event) => {
|
||||||
|
imageDataURL = event.target?.result as string;
|
||||||
|
};
|
||||||
|
reader.readAsDataURL(file);
|
||||||
|
|
||||||
|
await callback(file).catch(() => {
|
||||||
|
imageDataURL = `/api/users/${userId}/profile-picture.png`;
|
||||||
|
});
|
||||||
|
isLoading = false;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="flex gap-5">
|
||||||
|
<div class="flex w-full flex-col justify-between gap-5 sm:flex-row">
|
||||||
|
<div>
|
||||||
|
<h3 class="text-xl font-semibold">Profile Picture</h3>
|
||||||
|
{#if isLdapUser}
|
||||||
|
<p class="text-muted-foreground mt-1 text-sm">
|
||||||
|
The profile picture is managed by the LDAP server and cannot be changed here.
|
||||||
|
</p>
|
||||||
|
{:else}
|
||||||
|
<p class="text-muted-foreground mt-1 text-sm">
|
||||||
|
Click on the profile picture to upload a custom one from your files.
|
||||||
|
</p>
|
||||||
|
<p class="text-muted-foreground mt-1 text-sm">The image should be in PNG or JPEG format.</p>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{#if isLdapUser}
|
||||||
|
<Avatar.Root class="h-24 w-24">
|
||||||
|
<Avatar.Image class="object-cover" src={imageDataURL} />
|
||||||
|
</Avatar.Root>
|
||||||
|
{:else}
|
||||||
|
<FileInput
|
||||||
|
id="profile-picture-input"
|
||||||
|
variant="secondary"
|
||||||
|
accept="image/png, image/jpeg"
|
||||||
|
onchange={onImageChange}
|
||||||
|
>
|
||||||
|
<div class="group relative h-28 w-28 rounded-full">
|
||||||
|
<Avatar.Root class="h-full w-full transition-opacity duration-200">
|
||||||
|
<Avatar.Image
|
||||||
|
class="object-cover group-hover:opacity-10 {isLoading ? 'opacity-10' : ''}"
|
||||||
|
src={imageDataURL}
|
||||||
|
/>
|
||||||
|
</Avatar.Root>
|
||||||
|
<div class="absolute inset-0 flex items-center justify-center">
|
||||||
|
{#if isLoading}
|
||||||
|
<LucideLoader class="h-5 w-5 animate-spin" />
|
||||||
|
{:else}
|
||||||
|
<LucideUpload class="h-5 w-5 opacity-0 transition-opacity group-hover:opacity-100" />
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</FileInput>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -3,22 +3,10 @@
|
|||||||
import * as DropdownMenu from '$lib/components/ui/dropdown-menu';
|
import * as DropdownMenu from '$lib/components/ui/dropdown-menu';
|
||||||
import WebAuthnService from '$lib/services/webauthn-service';
|
import WebAuthnService from '$lib/services/webauthn-service';
|
||||||
import userStore from '$lib/stores/user-store';
|
import userStore from '$lib/stores/user-store';
|
||||||
import { createSHA256hash } from '$lib/utils/crypto-util';
|
|
||||||
import { LucideLogOut, LucideUser } from 'lucide-svelte';
|
import { LucideLogOut, LucideUser } from 'lucide-svelte';
|
||||||
|
|
||||||
const webauthnService = new WebAuthnService();
|
const webauthnService = new WebAuthnService();
|
||||||
|
|
||||||
let initials = $derived(
|
|
||||||
($userStore!.firstName.charAt(0) + $userStore!.lastName?.charAt(0)).toUpperCase()
|
|
||||||
);
|
|
||||||
|
|
||||||
let gravatarURL: string | undefined = $state();
|
|
||||||
if ($userStore) {
|
|
||||||
createSHA256hash($userStore.email).then((email) => {
|
|
||||||
gravatarURL = `https://www.gravatar.com/avatar/${email}?d=404`;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function logout() {
|
async function logout() {
|
||||||
await webauthnService.logout();
|
await webauthnService.logout();
|
||||||
window.location.reload();
|
window.location.reload();
|
||||||
@@ -28,8 +16,7 @@
|
|||||||
<DropdownMenu.Root>
|
<DropdownMenu.Root>
|
||||||
<DropdownMenu.Trigger
|
<DropdownMenu.Trigger
|
||||||
><Avatar.Root class="h-9 w-9">
|
><Avatar.Root class="h-9 w-9">
|
||||||
<Avatar.Image src={gravatarURL} />
|
<Avatar.Image src="/api/users/me/profile-picture.png" />
|
||||||
<Avatar.Fallback>{initials}</Avatar.Fallback>
|
|
||||||
</Avatar.Root></DropdownMenu.Trigger
|
</Avatar.Root></DropdownMenu.Trigger
|
||||||
>
|
>
|
||||||
<DropdownMenu.Content class="min-w-40" align="start">
|
<DropdownMenu.Content class="min-w-40" align="start">
|
||||||
@@ -39,7 +26,7 @@
|
|||||||
{$userStore?.firstName}
|
{$userStore?.firstName}
|
||||||
{$userStore?.lastName}
|
{$userStore?.lastName}
|
||||||
</p>
|
</p>
|
||||||
<p class="text-xs leading-none text-muted-foreground">{$userStore?.email}</p>
|
<p class="text-muted-foreground text-xs leading-none">{$userStore?.email}</p>
|
||||||
</div>
|
</div>
|
||||||
</DropdownMenu.Label>
|
</DropdownMenu.Label>
|
||||||
<DropdownMenu.Separator />
|
<DropdownMenu.Separator />
|
||||||
|
|||||||
@@ -5,9 +5,11 @@
|
|||||||
import Logo from '../logo.svelte';
|
import Logo from '../logo.svelte';
|
||||||
import HeaderAvatar from './header-avatar.svelte';
|
import HeaderAvatar from './header-avatar.svelte';
|
||||||
|
|
||||||
const authUrls = ['/authorize', '/login', '/logout'];
|
const authUrls = [/^\/authorize$/, /^\/login(?:\/.*)?$/, /^\/logout$/];
|
||||||
let isAuthPage = $derived(!$page.error && authUrls.includes($page.url.pathname));
|
|
||||||
|
let isAuthPage = $derived(
|
||||||
|
!$page.error && authUrls.some((pattern) => pattern.test($page.url.pathname))
|
||||||
|
);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class=" w-full {isAuthPage ? 'absolute top-0 z-10 mt-4' : 'border-b'}">
|
<div class=" w-full {isAuthPage ? 'absolute top-0 z-10 mt-4' : 'border-b'}">
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
|
|
||||||
<AvatarPrimitive.Root
|
<AvatarPrimitive.Root
|
||||||
{delayMs}
|
{delayMs}
|
||||||
class={cn('relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full', className)}
|
class={cn('relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full border', className)}
|
||||||
{...$$restProps}
|
{...$$restProps}
|
||||||
>
|
>
|
||||||
<slot />
|
<slot />
|
||||||
|
|||||||
@@ -95,7 +95,7 @@ export default class AppConfigService extends APIService {
|
|||||||
return true;
|
return true;
|
||||||
} else if (value === 'false') {
|
} else if (value === 'false') {
|
||||||
return false;
|
return false;
|
||||||
} else if (!isNaN(parseFloat(value))) {
|
} else if (/^-?\d+(\.\d+)?$/.test(value)) {
|
||||||
return parseFloat(value);
|
return parseFloat(value);
|
||||||
} else {
|
} else {
|
||||||
return value;
|
return value;
|
||||||
|
|||||||
@@ -39,6 +39,20 @@ export default class UserService extends APIService {
|
|||||||
await this.api.delete(`/users/${id}`);
|
await this.api.delete(`/users/${id}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async updateProfilePicture(userId: string, image: File) {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('file', image!);
|
||||||
|
|
||||||
|
await this.api.put(`/users/${userId}/profile-picture`, formData);
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateCurrentUsersProfilePicture(image: File) {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('file', image!);
|
||||||
|
|
||||||
|
await this.api.put('/users/me/profile-picture', formData);
|
||||||
|
}
|
||||||
|
|
||||||
async createOneTimeAccessToken(userId: string, expiresAt: Date) {
|
async createOneTimeAccessToken(userId: string, expiresAt: Date) {
|
||||||
const res = await this.api.post(`/users/${userId}/one-time-access-token`, {
|
const res = await this.api.post(`/users/${userId}/one-time-access-token`, {
|
||||||
userId,
|
userId,
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ export type AllAppConfig = AppConfig & {
|
|||||||
smtpFrom: string;
|
smtpFrom: string;
|
||||||
smtpUser: string;
|
smtpUser: string;
|
||||||
smtpPassword: string;
|
smtpPassword: string;
|
||||||
smtpTls: boolean;
|
smtpTls: 'none' | 'starttls' | 'tls';
|
||||||
smtpSkipCertVerify: boolean;
|
smtpSkipCertVerify: boolean;
|
||||||
emailLoginNotificationEnabled: boolean;
|
emailLoginNotificationEnabled: boolean;
|
||||||
// LDAP
|
// LDAP
|
||||||
@@ -31,6 +31,7 @@ export type AllAppConfig = AppConfig & {
|
|||||||
ldapAttributeUserEmail: string;
|
ldapAttributeUserEmail: string;
|
||||||
ldapAttributeUserFirstName: string;
|
ldapAttributeUserFirstName: string;
|
||||||
ldapAttributeUserLastName: string;
|
ldapAttributeUserLastName: string;
|
||||||
|
ldapAttributeUserProfilePicture: string;
|
||||||
ldapAttributeGroupMember: string;
|
ldapAttributeGroupMember: string;
|
||||||
ldapAttributeGroupUniqueIdentifier: string;
|
ldapAttributeGroupUniqueIdentifier: string;
|
||||||
ldapAttributeGroupName: string;
|
ldapAttributeGroupName: string;
|
||||||
@@ -46,5 +47,5 @@ export type AppConfigRawResponse = {
|
|||||||
export type AppVersionInformation = {
|
export type AppVersionInformation = {
|
||||||
isUpToDate: boolean | null;
|
isUpToDate: boolean | null;
|
||||||
newestVersion: string | null;
|
newestVersion: string | null;
|
||||||
currentVersion: string
|
currentVersion: string;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -33,41 +33,37 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<section>
|
<section>
|
||||||
<div class="flex min-h-[calc(100vh-64px)] w-full flex-col justify-between bg-muted/40">
|
<div class="bg-muted/40 flex min-h-[calc(100vh-64px)] w-full flex-col justify-between">
|
||||||
<main
|
<main
|
||||||
class="mx-auto flex w-full max-w-[1640px] flex-col gap-x-4 gap-y-10 p-4 md:p-10 lg:flex-row"
|
class="mx-auto flex w-full max-w-[1640px] flex-col gap-x-4 gap-y-10 p-4 md:p-10 lg:flex-row"
|
||||||
>
|
>
|
||||||
<div>
|
<div class="min-w-[200px] xl:min-w-[250px]">
|
||||||
<div class="mx-auto grid w-full gap-2">
|
<div class="mx-auto grid w-full gap-2">
|
||||||
<h1 class="mb-5 text-3xl font-semibold">Settings</h1>
|
<h1 class="mb-5 text-3xl font-semibold">Settings</h1>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<nav class="text-muted-foreground grid gap-4 text-sm">
|
||||||
class="mx-auto grid items-start gap-6 md:grid-cols-[180px_1fr] lg:grid-cols-[250px_1fr]"
|
{#each links as { href, label }}
|
||||||
>
|
<a {href} class={$page.url.pathname.startsWith(href) ? 'text-primary font-bold' : ''}>
|
||||||
<nav class="grid gap-4 text-sm text-muted-foreground">
|
{label}
|
||||||
{#each links as { href, label }}
|
</a>
|
||||||
<a {href} class={$page.url.pathname.startsWith(href) ? 'font-bold text-primary' : ''}>
|
{/each}
|
||||||
{label}
|
{#if $userStore?.isAdmin && versionInformation.isUpToDate === false}
|
||||||
</a>
|
<a
|
||||||
{/each}
|
href="https://github.com/pocket-id/pocket-id/releases/latest"
|
||||||
{#if $userStore?.isAdmin && versionInformation.isUpToDate === false}
|
target="_blank"
|
||||||
<a
|
class="flex items-center gap-2"
|
||||||
href="https://github.com/pocket-id/pocket-id/releases/latest"
|
>
|
||||||
target="_blank"
|
Update Pocket ID <LucideExternalLink class="my-auto inline-block h-3 w-3" />
|
||||||
class="flex items-center gap-2"
|
</a>
|
||||||
>
|
{/if}
|
||||||
Update Pocket ID <LucideExternalLink class="my-auto inline-block h-3 w-3" />
|
</nav>
|
||||||
</a>
|
|
||||||
{/if}
|
|
||||||
</nav>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="flex w-full flex-col gap-5 overflow-x-hidden">
|
<div class="flex w-full flex-col gap-5 overflow-x-hidden">
|
||||||
{@render children()}
|
{@render children()}
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
<div class="flex flex-col items-center">
|
<div class="flex flex-col items-center">
|
||||||
<p class="py-3 text-xs text-muted-foreground">
|
<p class="text-muted-foreground py-3 text-xs">
|
||||||
Powered by <a
|
Powered by <a
|
||||||
class="text-foreground"
|
class="text-foreground"
|
||||||
href="https://github.com/pocket-id/pocket-id"
|
href="https://github.com/pocket-id/pocket-id"
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
import { toast } from 'svelte-sonner';
|
import { toast } from 'svelte-sonner';
|
||||||
import AccountForm from './account-form.svelte';
|
import AccountForm from './account-form.svelte';
|
||||||
import PasskeyList from './passkey-list.svelte';
|
import PasskeyList from './passkey-list.svelte';
|
||||||
|
import ProfilePictureSettings from '../../../lib/components/form/profile-picture-settings.svelte';
|
||||||
import RenamePasskeyModal from './rename-passkey-modal.svelte';
|
import RenamePasskeyModal from './rename-passkey-modal.svelte';
|
||||||
|
|
||||||
let { data } = $props();
|
let { data } = $props();
|
||||||
@@ -36,6 +37,13 @@
|
|||||||
return success;
|
return success;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function updateProfilePicture(image: File) {
|
||||||
|
await userService
|
||||||
|
.updateCurrentUsersProfilePicture(image)
|
||||||
|
.then(() => toast.success('Profile picture updated successfully'))
|
||||||
|
.catch(axiosErrorToast);
|
||||||
|
}
|
||||||
|
|
||||||
async function createPasskey() {
|
async function createPasskey() {
|
||||||
try {
|
try {
|
||||||
const opts = await webauthnService.getRegistrationOptions();
|
const opts = await webauthnService.getRegistrationOptions();
|
||||||
@@ -86,6 +94,12 @@
|
|||||||
</Card.Root>
|
</Card.Root>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
||||||
|
<Card.Root>
|
||||||
|
<Card.Content class="pt-6">
|
||||||
|
<ProfilePictureSettings userId="me" isLdapUser={!!account.ldapId} callback={updateProfilePicture} />
|
||||||
|
</Card.Content>
|
||||||
|
</Card.Root>
|
||||||
|
|
||||||
<Card.Root>
|
<Card.Root>
|
||||||
<Card.Header>
|
<Card.Header>
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import FormInput from '$lib/components/form-input.svelte';
|
import FormInput from '$lib/components/form/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 { UserCreate } from '$lib/types/user.type';
|
||||||
import { createForm } from '$lib/utils/form-util';
|
import { createForm } from '$lib/utils/form-util';
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import FileInput from '$lib/components/file-input.svelte';
|
import FileInput from '$lib/components/form/file-input.svelte';
|
||||||
import { Label } from '$lib/components/ui/label';
|
import { Label } from '$lib/components/ui/label';
|
||||||
import { cn } from '$lib/utils/style';
|
import { cn } from '$lib/utils/style';
|
||||||
import type { HTMLAttributes } from 'svelte/elements';
|
import type { HTMLAttributes } from 'svelte/elements';
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { env } from '$env/dynamic/public';
|
import { env } from '$env/dynamic/public';
|
||||||
import CheckboxWithLabel from '$lib/components/checkbox-with-label.svelte';
|
|
||||||
import { openConfirmDialog } from '$lib/components/confirm-dialog';
|
import { openConfirmDialog } from '$lib/components/confirm-dialog';
|
||||||
import FormInput from '$lib/components/form-input.svelte';
|
import CheckboxWithLabel from '$lib/components/form/checkbox-with-label.svelte';
|
||||||
|
import FormInput from '$lib/components/form/form-input.svelte';
|
||||||
import { Button } from '$lib/components/ui/button';
|
import { Button } from '$lib/components/ui/button';
|
||||||
|
import Label from '$lib/components/ui/label/label.svelte';
|
||||||
|
import * as Select from '$lib/components/ui/select';
|
||||||
import AppConfigService from '$lib/services/app-config-service';
|
import AppConfigService from '$lib/services/app-config-service';
|
||||||
import type { AllAppConfig } from '$lib/types/application-configuration';
|
import type { AllAppConfig } from '$lib/types/application-configuration';
|
||||||
import { createForm } from '$lib/utils/form-util';
|
import { createForm } from '$lib/utils/form-util';
|
||||||
@@ -20,6 +22,11 @@
|
|||||||
|
|
||||||
const appConfigService = new AppConfigService();
|
const appConfigService = new AppConfigService();
|
||||||
const uiConfigDisabled = env.PUBLIC_UI_CONFIG_DISABLED === 'true';
|
const uiConfigDisabled = env.PUBLIC_UI_CONFIG_DISABLED === 'true';
|
||||||
|
const tlsOptions = {
|
||||||
|
none: 'None',
|
||||||
|
starttls: 'StartTLS',
|
||||||
|
tls: 'TLS'
|
||||||
|
};
|
||||||
|
|
||||||
let isSendingTestEmail = $state(false);
|
let isSendingTestEmail = $state(false);
|
||||||
|
|
||||||
@@ -29,7 +36,7 @@
|
|||||||
smtpUser: z.string(),
|
smtpUser: z.string(),
|
||||||
smtpPassword: z.string(),
|
smtpPassword: z.string(),
|
||||||
smtpFrom: z.string().email(),
|
smtpFrom: z.string().email(),
|
||||||
smtpTls: z.boolean(),
|
smtpTls: z.enum(['none', 'starttls', 'tls']),
|
||||||
smtpSkipCertVerify: z.boolean(),
|
smtpSkipCertVerify: z.boolean(),
|
||||||
emailOneTimeAccessEnabled: z.boolean(),
|
emailOneTimeAccessEnabled: z.boolean(),
|
||||||
emailLoginNotificationEnabled: z.boolean()
|
emailLoginNotificationEnabled: z.boolean()
|
||||||
@@ -96,12 +103,22 @@
|
|||||||
<FormInput label="SMTP User" bind:input={$inputs.smtpUser} />
|
<FormInput label="SMTP User" bind:input={$inputs.smtpUser} />
|
||||||
<FormInput label="SMTP Password" type="password" bind:input={$inputs.smtpPassword} />
|
<FormInput label="SMTP Password" type="password" bind:input={$inputs.smtpPassword} />
|
||||||
<FormInput label="SMTP From" bind:input={$inputs.smtpFrom} />
|
<FormInput label="SMTP From" bind:input={$inputs.smtpFrom} />
|
||||||
<CheckboxWithLabel
|
<div class="grid gap-2">
|
||||||
id="tls"
|
<Label class="mb-0" for="smtp-tls">SMTP TLS Option</Label>
|
||||||
label="TLS"
|
<Select.Root
|
||||||
description="Enable TLS for the SMTP connection."
|
selected={{ value: $inputs.smtpTls.value, label: tlsOptions[$inputs.smtpTls.value] }}
|
||||||
bind:checked={$inputs.smtpTls.value}
|
onSelectedChange={(v) => ($inputs.smtpTls.value = v!.value)}
|
||||||
/>
|
>
|
||||||
|
<Select.Trigger>
|
||||||
|
<Select.Value placeholder="Email TLS Option" />
|
||||||
|
</Select.Trigger>
|
||||||
|
<Select.Content>
|
||||||
|
<Select.Item value="none" label="None" />
|
||||||
|
<Select.Item value="starttls" label="StartTLS" />
|
||||||
|
<Select.Item value="tls" label="TLS" />
|
||||||
|
</Select.Content>
|
||||||
|
</Select.Root>
|
||||||
|
</div>
|
||||||
<CheckboxWithLabel
|
<CheckboxWithLabel
|
||||||
id="skip-cert-verify"
|
id="skip-cert-verify"
|
||||||
label="Skip Certificate Verification"
|
label="Skip Certificate Verification"
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { env } from '$env/dynamic/public';
|
import { env } from '$env/dynamic/public';
|
||||||
import CheckboxWithLabel from '$lib/components/checkbox-with-label.svelte';
|
import CheckboxWithLabel from '$lib/components/form/checkbox-with-label.svelte';
|
||||||
import FormInput from '$lib/components/form-input.svelte';
|
import FormInput from '$lib/components/form/form-input.svelte';
|
||||||
import { Button } from '$lib/components/ui/button';
|
import { Button } from '$lib/components/ui/button';
|
||||||
import type { AllAppConfig } from '$lib/types/application-configuration';
|
import type { AllAppConfig } from '$lib/types/application-configuration';
|
||||||
import { createForm } from '$lib/utils/form-util';
|
import { createForm } from '$lib/utils/form-util';
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { env } from '$env/dynamic/public';
|
import { env } from '$env/dynamic/public';
|
||||||
import CheckboxWithLabel from '$lib/components/checkbox-with-label.svelte';
|
import CheckboxWithLabel from '$lib/components/form/checkbox-with-label.svelte';
|
||||||
import FormInput from '$lib/components/form-input.svelte';
|
import FormInput from '$lib/components/form/form-input.svelte';
|
||||||
import { Button } from '$lib/components/ui/button';
|
import { Button } from '$lib/components/ui/button';
|
||||||
import AppConfigService from '$lib/services/app-config-service';
|
import AppConfigService from '$lib/services/app-config-service';
|
||||||
import type { AllAppConfig } from '$lib/types/application-configuration';
|
import type { AllAppConfig } from '$lib/types/application-configuration';
|
||||||
@@ -38,6 +38,7 @@
|
|||||||
ldapAttributeUserEmail: appConfig.ldapAttributeUserEmail,
|
ldapAttributeUserEmail: appConfig.ldapAttributeUserEmail,
|
||||||
ldapAttributeUserFirstName: appConfig.ldapAttributeUserFirstName,
|
ldapAttributeUserFirstName: appConfig.ldapAttributeUserFirstName,
|
||||||
ldapAttributeUserLastName: appConfig.ldapAttributeUserLastName,
|
ldapAttributeUserLastName: appConfig.ldapAttributeUserLastName,
|
||||||
|
ldapAttributeUserProfilePicture: appConfig.ldapAttributeUserProfilePicture,
|
||||||
ldapAttributeGroupMember: appConfig.ldapAttributeGroupMember,
|
ldapAttributeGroupMember: appConfig.ldapAttributeGroupMember,
|
||||||
ldapAttributeGroupUniqueIdentifier: appConfig.ldapAttributeGroupUniqueIdentifier,
|
ldapAttributeGroupUniqueIdentifier: appConfig.ldapAttributeGroupUniqueIdentifier,
|
||||||
ldapAttributeGroupName: appConfig.ldapAttributeGroupName,
|
ldapAttributeGroupName: appConfig.ldapAttributeGroupName,
|
||||||
@@ -57,6 +58,7 @@
|
|||||||
ldapAttributeUserEmail: z.string().min(1),
|
ldapAttributeUserEmail: z.string().min(1),
|
||||||
ldapAttributeUserFirstName: z.string().min(1),
|
ldapAttributeUserFirstName: z.string().min(1),
|
||||||
ldapAttributeUserLastName: z.string().min(1),
|
ldapAttributeUserLastName: z.string().min(1),
|
||||||
|
ldapAttributeUserProfilePicture: z.string(),
|
||||||
ldapAttributeGroupMember: z.string(),
|
ldapAttributeGroupMember: z.string(),
|
||||||
ldapAttributeGroupUniqueIdentifier: z.string().min(1),
|
ldapAttributeGroupUniqueIdentifier: z.string().min(1),
|
||||||
ldapAttributeGroupName: z.string().min(1),
|
ldapAttributeGroupName: z.string().min(1),
|
||||||
@@ -166,6 +168,12 @@
|
|||||||
placeholder="sn"
|
placeholder="sn"
|
||||||
bind:input={$inputs.ldapAttributeUserLastName}
|
bind:input={$inputs.ldapAttributeUserLastName}
|
||||||
/>
|
/>
|
||||||
|
<FormInput
|
||||||
|
label="User Profile Picture Attribute"
|
||||||
|
description="The value of this attribute can either be a URL, a binary or a base64 encoded image."
|
||||||
|
placeholder="jpegPhoto"
|
||||||
|
bind:input={$inputs.ldapAttributeUserProfilePicture}
|
||||||
|
/>
|
||||||
<FormInput
|
<FormInput
|
||||||
label="Group Members Attribute"
|
label="Group Members Attribute"
|
||||||
description="The attribute to use for querying members of a group."
|
description="The attribute to use for querying members of a group."
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import FormInput from '$lib/components/form-input.svelte';
|
import FormInput from '$lib/components/form/form-input.svelte';
|
||||||
import { Button } from '$lib/components/ui/button';
|
import { Button } from '$lib/components/ui/button';
|
||||||
import { Input } from '$lib/components/ui/input';
|
import { Input } from '$lib/components/ui/input';
|
||||||
import { LucideMinus, LucidePlus } from 'lucide-svelte';
|
import { LucideMinus, LucidePlus } from 'lucide-svelte';
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import CheckboxWithLabel from '$lib/components/checkbox-with-label.svelte';
|
import CheckboxWithLabel from '$lib/components/form/checkbox-with-label.svelte';
|
||||||
import FileInput from '$lib/components/file-input.svelte';
|
import FileInput from '$lib/components/form/file-input.svelte';
|
||||||
import FormInput from '$lib/components/form-input.svelte';
|
import FormInput from '$lib/components/form/form-input.svelte';
|
||||||
import { Button } from '$lib/components/ui/button';
|
import { Button } from '$lib/components/ui/button';
|
||||||
import Label from '$lib/components/ui/label/label.svelte';
|
import Label from '$lib/components/ui/label/label.svelte';
|
||||||
import type {
|
import type {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import CollapsibleCard from '$lib/components/collapsible-card.svelte';
|
import CollapsibleCard from '$lib/components/collapsible-card.svelte';
|
||||||
import CustomClaimsInput from '$lib/components/custom-claims-input.svelte';
|
import CustomClaimsInput from '$lib/components/form/custom-claims-input.svelte';
|
||||||
import { Badge } from '$lib/components/ui/badge';
|
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';
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import FormInput from '$lib/components/form-input.svelte';
|
import FormInput from '$lib/components/form/form-input.svelte';
|
||||||
import { Button } from '$lib/components/ui/button';
|
import { Button } from '$lib/components/ui/button';
|
||||||
import appConfigStore from '$lib/stores/application-configuration-store';
|
import appConfigStore from '$lib/stores/application-configuration-store';
|
||||||
import type { UserGroupCreate } from '$lib/types/user-group.type';
|
import type { UserGroupCreate } from '$lib/types/user-group.type';
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import CollapsibleCard from '$lib/components/collapsible-card.svelte';
|
import CollapsibleCard from '$lib/components/collapsible-card.svelte';
|
||||||
|
import CustomClaimsInput from '$lib/components/form/custom-claims-input.svelte';
|
||||||
|
import ProfilePictureSettings from '$lib/components/form/profile-picture-settings.svelte';
|
||||||
import Badge from '$lib/components/ui/badge/badge.svelte';
|
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';
|
||||||
@@ -9,7 +11,6 @@
|
|||||||
import { axiosErrorToast } from '$lib/utils/error-util';
|
import { axiosErrorToast } from '$lib/utils/error-util';
|
||||||
import { LucideChevronLeft } from 'lucide-svelte';
|
import { LucideChevronLeft } from 'lucide-svelte';
|
||||||
import { toast } from 'svelte-sonner';
|
import { toast } from 'svelte-sonner';
|
||||||
import CustomClaimsInput from '../../../../../lib/components/custom-claims-input.svelte';
|
|
||||||
import UserForm from '../user-form.svelte';
|
import UserForm from '../user-form.svelte';
|
||||||
|
|
||||||
let { data } = $props();
|
let { data } = $props();
|
||||||
@@ -39,6 +40,13 @@
|
|||||||
axiosErrorToast(e);
|
axiosErrorToast(e);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function updateProfilePicture(image: File) {
|
||||||
|
await userService
|
||||||
|
.updateProfilePicture(user.id, image)
|
||||||
|
.then(() => toast.success('Profile picture updated successfully'))
|
||||||
|
.catch(axiosErrorToast);
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
@@ -62,6 +70,16 @@
|
|||||||
</Card.Content>
|
</Card.Content>
|
||||||
</Card.Root>
|
</Card.Root>
|
||||||
|
|
||||||
|
<Card.Root>
|
||||||
|
<Card.Content class="pt-6">
|
||||||
|
<ProfilePictureSettings
|
||||||
|
userId={user.id}
|
||||||
|
isLdapUser={!!user.ldapId}
|
||||||
|
callback={updateProfilePicture}
|
||||||
|
/>
|
||||||
|
</Card.Content>
|
||||||
|
</Card.Root>
|
||||||
|
|
||||||
<CollapsibleCard
|
<CollapsibleCard
|
||||||
id="user-custom-claims"
|
id="user-custom-claims"
|
||||||
title="Custom Claims"
|
title="Custom Claims"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import CheckboxWithLabel from '$lib/components/checkbox-with-label.svelte';
|
import CheckboxWithLabel from '$lib/components/form/checkbox-with-label.svelte';
|
||||||
import FormInput from '$lib/components/form-input.svelte';
|
import FormInput from '$lib/components/form/form-input.svelte';
|
||||||
import { Button } from '$lib/components/ui/button';
|
import { Button } from '$lib/components/ui/button';
|
||||||
import appConfigStore from '$lib/stores/application-configuration-store';
|
import appConfigStore from '$lib/stores/application-configuration-store';
|
||||||
import type { User, UserCreate } from '$lib/types/user.type';
|
import type { User, UserCreate } from '$lib/types/user.type';
|
||||||
|
|||||||
Reference in New Issue
Block a user