mirror of
https://github.com/pocket-id/pocket-id.git
synced 2026-03-29 18:56:36 +00:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f9576479a8 |
27
CHANGELOG.md
27
CHANGELOG.md
@@ -1,30 +1,3 @@
|
|||||||
## [1.11.0](https://github.com/pocket-id/pocket-id/compare/v1.10.0...v1.11.0) (2025-09-18)
|
|
||||||
|
|
||||||
|
|
||||||
### Features
|
|
||||||
|
|
||||||
* add CSP header ([#908](https://github.com/pocket-id/pocket-id/issues/908)) ([6215e1a](https://github.com/pocket-id/pocket-id/commit/6215e1ac01c03866f8b2e89ac084ddd6a3c3ac9e))
|
|
||||||
* add custom base url ([#858](https://github.com/pocket-id/pocket-id/issues/858)) ([a3979f6](https://github.com/pocket-id/pocket-id/commit/a3979f63e07d418ee9eb1cb1abc37aede5799fc8))
|
|
||||||
* add info box to app settings if UI config is disabled ([a1d8538](https://github.com/pocket-id/pocket-id/commit/a1d8538c64beb4d7e8559934985772fba27623ca))
|
|
||||||
* add PWA support ([#938](https://github.com/pocket-id/pocket-id/issues/938)) ([5367463](https://github.com/pocket-id/pocket-id/commit/5367463239b354640fd65390bc409e4a0ac13fd1))
|
|
||||||
* add support for `LOG_LEVEL` env variable ([#942](https://github.com/pocket-id/pocket-id/issues/942)) ([2d6d5df](https://github.com/pocket-id/pocket-id/commit/2d6d5df0e7f104a148fb4eeac89a2fbb7db8047a))
|
|
||||||
* add user display name field ([#898](https://github.com/pocket-id/pocket-id/issues/898)) ([6837360](https://github.com/pocket-id/pocket-id/commit/68373604dd30065947226922233bc1e19e778b01))
|
|
||||||
* allow uppercase usernames ([#958](https://github.com/pocket-id/pocket-id/issues/958)) ([0224949](https://github.com/pocket-id/pocket-id/commit/02249491f86c289adf596d9d9922dfa04779edee))
|
|
||||||
* client_credentials flow support ([#901](https://github.com/pocket-id/pocket-id/issues/901)) ([901333f](https://github.com/pocket-id/pocket-id/commit/901333f7e43b4e925ed6dfd890dee2caa1947934))
|
|
||||||
* return new id_token when using refresh token ([#925](https://github.com/pocket-id/pocket-id/issues/925)) ([307caaa](https://github.com/pocket-id/pocket-id/commit/307caaa3efbc966341b95ee4b5ff18c81ed98e54))
|
|
||||||
|
|
||||||
|
|
||||||
### Bug Fixes
|
|
||||||
|
|
||||||
* add validation for callback URLs ([#929](https://github.com/pocket-id/pocket-id/issues/929)) ([6c91474](https://github.com/pocket-id/pocket-id/commit/6c9147483c0a370e2b5011d13898279d2acc445d))
|
|
||||||
* disable sign up options in UI if `UI_CONFIG_DISABLED` ([1d7cbc2](https://github.com/pocket-id/pocket-id/commit/1d7cbc2a4ecf352d46087f30b477f6bbaa23adf5))
|
|
||||||
* ensure users imported from LDAP have fields validated ([#923](https://github.com/pocket-id/pocket-id/issues/923)) ([4215523](https://github.com/pocket-id/pocket-id/commit/42155238b750b015b0547294f397e1e285594e3e))
|
|
||||||
* key-rotate doesn't work with database storage ([#940](https://github.com/pocket-id/pocket-id/issues/940)) ([c018f29](https://github.com/pocket-id/pocket-id/commit/c018f29ad7c61a3ef1b235b0d404a3a2024a26ca))
|
|
||||||
* list items on previous page get unselected if other items selected on next page ([6c696b4](https://github.com/pocket-id/pocket-id/commit/6c696b46c8b60b3dc4af35c9c6cf1b8e1322f4cd))
|
|
||||||
* make environment variables case insensitive where necessary ([#954](https://github.com/pocket-id/pocket-id/issues/954)) ([99f31a7](https://github.com/pocket-id/pocket-id/commit/99f31a7c26c63dec76682ddf450d88e6ee40876f)), closes [#935](https://github.com/pocket-id/pocket-id/issues/935)
|
|
||||||
* my apps card shouldn't take full width if only one item exists ([e7e53a8](https://github.com/pocket-id/pocket-id/commit/e7e53a8b8c87bee922167d24556aef3ea219b1a2))
|
|
||||||
* update localized name and description of ldap group name attribute ([#892](https://github.com/pocket-id/pocket-id/issues/892)) ([e88be7e](https://github.com/pocket-id/pocket-id/commit/e88be7e61a8aafabcae70adf9265023c50626705))
|
|
||||||
|
|
||||||
## [](https://github.com/pocket-id/pocket-id/compare/v1.9.1...v) (2025-08-27)
|
## [](https://github.com/pocket-id/pocket-id/compare/v1.9.1...v) (2025-08-27)
|
||||||
|
|
||||||
### Features
|
### Features
|
||||||
|
|||||||
@@ -28,7 +28,6 @@ require (
|
|||||||
github.com/lmittmann/tint v1.1.2
|
github.com/lmittmann/tint v1.1.2
|
||||||
github.com/mattn/go-isatty v0.0.20
|
github.com/mattn/go-isatty v0.0.20
|
||||||
github.com/mileusna/useragent v1.3.5
|
github.com/mileusna/useragent v1.3.5
|
||||||
github.com/orandin/slog-gorm v1.4.0
|
|
||||||
github.com/oschwald/maxminddb-golang/v2 v2.0.0-beta.8
|
github.com/oschwald/maxminddb-golang/v2 v2.0.0-beta.8
|
||||||
github.com/spf13/cobra v1.9.1
|
github.com/spf13/cobra v1.9.1
|
||||||
github.com/stretchr/testify v1.10.0
|
github.com/stretchr/testify v1.10.0
|
||||||
@@ -43,13 +42,13 @@ require (
|
|||||||
go.opentelemetry.io/otel/sdk/log v0.10.0
|
go.opentelemetry.io/otel/sdk/log v0.10.0
|
||||||
go.opentelemetry.io/otel/sdk/metric v1.35.0
|
go.opentelemetry.io/otel/sdk/metric v1.35.0
|
||||||
go.opentelemetry.io/otel/trace v1.37.0
|
go.opentelemetry.io/otel/trace v1.37.0
|
||||||
golang.org/x/crypto v0.41.0
|
golang.org/x/crypto v0.42.0
|
||||||
golang.org/x/image v0.30.0
|
golang.org/x/image v0.31.0
|
||||||
golang.org/x/sync v0.16.0
|
golang.org/x/sync v0.17.0
|
||||||
golang.org/x/text v0.28.0
|
golang.org/x/text v0.29.0
|
||||||
golang.org/x/time v0.12.0
|
golang.org/x/time v0.13.0
|
||||||
gorm.io/driver/postgres v1.6.0
|
gorm.io/driver/postgres v1.6.0
|
||||||
gorm.io/gorm v1.30.1
|
gorm.io/gorm v1.31.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
@@ -136,7 +135,7 @@ require (
|
|||||||
golang.org/x/exp v0.0.0-20250813145105-42675adae3e6 // indirect
|
golang.org/x/exp v0.0.0-20250813145105-42675adae3e6 // indirect
|
||||||
golang.org/x/net v0.43.0 // indirect
|
golang.org/x/net v0.43.0 // indirect
|
||||||
golang.org/x/oauth2 v0.27.0 // indirect
|
golang.org/x/oauth2 v0.27.0 // indirect
|
||||||
golang.org/x/sys v0.35.0 // indirect
|
golang.org/x/sys v0.36.0 // indirect
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20250218202821-56aae31c358a // indirect
|
google.golang.org/genproto/googleapis/api v0.0.0-20250218202821-56aae31c358a // indirect
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a // indirect
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a // indirect
|
||||||
google.golang.org/grpc v1.71.0 // indirect
|
google.golang.org/grpc v1.71.0 // indirect
|
||||||
|
|||||||
@@ -218,8 +218,6 @@ github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8
|
|||||||
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
|
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
|
||||||
github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug=
|
github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug=
|
||||||
github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM=
|
github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM=
|
||||||
github.com/orandin/slog-gorm v1.4.0 h1:FgA8hJufF9/jeNSYoEXmHPPBwET2gwlF3B85JdpsTUU=
|
|
||||||
github.com/orandin/slog-gorm v1.4.0/go.mod h1:MoZ51+b7xE9lwGNPYEhxcUtRNrYzjdcKvA8QXQQGEPA=
|
|
||||||
github.com/oschwald/maxminddb-golang/v2 v2.0.0-beta.8 h1:aM1/rO6p+XV+l+seD7UCtFZgsOefDTrFVLvPoZWjXZs=
|
github.com/oschwald/maxminddb-golang/v2 v2.0.0-beta.8 h1:aM1/rO6p+XV+l+seD7UCtFZgsOefDTrFVLvPoZWjXZs=
|
||||||
github.com/oschwald/maxminddb-golang/v2 v2.0.0-beta.8/go.mod h1:Jts8ztuE0PkUwY7VCJyp6B68ujQfr6G9P5Dn3Yx9u6w=
|
github.com/oschwald/maxminddb-golang/v2 v2.0.0-beta.8/go.mod h1:Jts8ztuE0PkUwY7VCJyp6B68ujQfr6G9P5Dn3Yx9u6w=
|
||||||
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
|
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
|
||||||
@@ -334,13 +332,13 @@ golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliY
|
|||||||
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||||
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||||
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
||||||
golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4=
|
golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI=
|
||||||
golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc=
|
golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8=
|
||||||
golang.org/x/exp v0.0.0-20250813145105-42675adae3e6 h1:SbTAbRFnd5kjQXbczszQ0hdk3ctwYf3qBNH9jIsGclE=
|
golang.org/x/exp v0.0.0-20250813145105-42675adae3e6 h1:SbTAbRFnd5kjQXbczszQ0hdk3ctwYf3qBNH9jIsGclE=
|
||||||
golang.org/x/exp v0.0.0-20250813145105-42675adae3e6/go.mod h1:4QTo5u+SEIbbKW1RacMZq1YEfOBqeXa19JeshGi+zc4=
|
golang.org/x/exp v0.0.0-20250813145105-42675adae3e6/go.mod h1:4QTo5u+SEIbbKW1RacMZq1YEfOBqeXa19JeshGi+zc4=
|
||||||
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||||
golang.org/x/image v0.30.0 h1:jD5RhkmVAnjqaCUXfbGBrn3lpxbknfN9w2UhHHU+5B4=
|
golang.org/x/image v0.31.0 h1:mLChjE2MV6g1S7oqbXC0/UcKijjm5fnJLUYKIYrLESA=
|
||||||
golang.org/x/image v0.30.0/go.mod h1:SAEUTxCCMWSrJcCy/4HwavEsfZZJlYxeHLc6tTiAe/c=
|
golang.org/x/image v0.31.0/go.mod h1:R9ec5Lcp96v9FTF+ajwaH3uGxPH4fKfHHAVbUILxghA=
|
||||||
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=
|
||||||
@@ -372,8 +370,8 @@ 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/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||||
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
|
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
|
||||||
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||||
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=
|
||||||
@@ -386,8 +384,8 @@ golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|||||||
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
|
golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
|
||||||
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||||
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
|
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||||
@@ -407,10 +405,10 @@ 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/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||||
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
|
golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk=
|
||||||
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
|
golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4=
|
||||||
golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
|
golang.org/x/time v0.13.0 h1:eUlYslOIt32DgYD6utsuUeHs4d7AsEYLuIAdg7FlYgI=
|
||||||
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
|
golang.org/x/time v0.13.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
|
||||||
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=
|
||||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||||
@@ -439,8 +437,8 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
|||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
gorm.io/driver/postgres v1.6.0 h1:2dxzU8xJ+ivvqTRph34QX+WrRaJlmfyPqXmoGVjMBa4=
|
gorm.io/driver/postgres v1.6.0 h1:2dxzU8xJ+ivvqTRph34QX+WrRaJlmfyPqXmoGVjMBa4=
|
||||||
gorm.io/driver/postgres v1.6.0/go.mod h1:vUw0mrGgrTK+uPHEhAdV4sfFELrByKVGnaVRkXDhtWo=
|
gorm.io/driver/postgres v1.6.0/go.mod h1:vUw0mrGgrTK+uPHEhAdV4sfFELrByKVGnaVRkXDhtWo=
|
||||||
gorm.io/gorm v1.30.1 h1:lSHg33jJTBxs2mgJRfRZeLDG+WZaHYCk3Wtfl6Ngzo4=
|
gorm.io/gorm v1.31.0 h1:0VlycGreVhK7RF/Bwt51Fk8v0xLiiiFdbGDPIZQ7mJY=
|
||||||
gorm.io/gorm v1.30.1/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE=
|
gorm.io/gorm v1.31.0/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs=
|
||||||
modernc.org/cc/v4 v4.26.3 h1:yEN8dzrkRFnn4PUUKXLYIqVf2PJYAEjMTFjO3BDGc3I=
|
modernc.org/cc/v4 v4.26.3 h1:yEN8dzrkRFnn4PUUKXLYIqVf2PJYAEjMTFjO3BDGc3I=
|
||||||
modernc.org/cc/v4 v4.26.3/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
|
modernc.org/cc/v4 v4.26.3/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
|
||||||
modernc.org/ccgo/v4 v4.28.0 h1:rjznn6WWehKq7dG4JtLRKxb52Ecv8OUGah8+Z/SfpNU=
|
modernc.org/ccgo/v4 v4.28.0 h1:rjznn6WWehKq7dG4JtLRKxb52Ecv8OUGah8+Z/SfpNU=
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ import (
|
|||||||
sqliteMigrate "github.com/golang-migrate/migrate/v4/database/sqlite3"
|
sqliteMigrate "github.com/golang-migrate/migrate/v4/database/sqlite3"
|
||||||
_ "github.com/golang-migrate/migrate/v4/source/github"
|
_ "github.com/golang-migrate/migrate/v4/source/github"
|
||||||
"github.com/golang-migrate/migrate/v4/source/iofs"
|
"github.com/golang-migrate/migrate/v4/source/iofs"
|
||||||
slogGorm "github.com/orandin/slog-gorm"
|
|
||||||
"gorm.io/driver/postgres"
|
"gorm.io/driver/postgres"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
gormLogger "gorm.io/gorm/logger"
|
gormLogger "gorm.io/gorm/logger"
|
||||||
@@ -416,25 +415,17 @@ func ensureSqliteTempDir(dbPath string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func getGormLogger() gormLogger.Interface {
|
func getGormLogger() gormLogger.Interface {
|
||||||
loggerOpts := make([]slogGorm.Option, 0, 5)
|
loggerCfg := gormLogger.Config{
|
||||||
loggerOpts = append(loggerOpts,
|
SlowThreshold: 200 * time.Millisecond,
|
||||||
slogGorm.WithSlowThreshold(200*time.Millisecond),
|
IgnoreRecordNotFoundError: true,
|
||||||
slogGorm.WithErrorField("error"),
|
LogLevel: gormLogger.Warn,
|
||||||
)
|
ParameterizedQueries: true,
|
||||||
|
|
||||||
if common.EnvConfig.LogLevel == "debug" {
|
|
||||||
loggerOpts = append(loggerOpts,
|
|
||||||
slogGorm.SetLogLevel(slogGorm.DefaultLogType, slog.LevelDebug),
|
|
||||||
slogGorm.WithRecordNotFoundError(),
|
|
||||||
slogGorm.WithTraceAll(),
|
|
||||||
)
|
|
||||||
|
|
||||||
} else {
|
|
||||||
loggerOpts = append(loggerOpts,
|
|
||||||
slogGorm.SetLogLevel(slogGorm.DefaultLogType, slog.LevelWarn),
|
|
||||||
slogGorm.WithIgnoreTrace(),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return slogGorm.New(loggerOpts...)
|
if common.EnvConfig.AppEnv == "debug" {
|
||||||
|
loggerCfg.IgnoreRecordNotFoundError = false
|
||||||
|
loggerCfg.LogLevel = gormLogger.Info
|
||||||
|
}
|
||||||
|
|
||||||
|
return gormLogger.NewSlogLogger(slog.Default(), loggerCfg)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,17 +32,17 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type EnvConfigSchema struct {
|
type EnvConfigSchema struct {
|
||||||
AppEnv string `env:"APP_ENV" options:"toLower"`
|
AppEnv string `env:"APP_ENV"`
|
||||||
LogLevel string `env:"LOG_LEVEL" options:"toLower"`
|
LogLevel string `env:"LOG_LEVEL"`
|
||||||
AppURL string `env:"APP_URL" options:"toLower"`
|
AppURL string `env:"APP_URL"`
|
||||||
DbProvider DbProvider `env:"DB_PROVIDER" options:"toLower"`
|
DbProvider DbProvider `env:"DB_PROVIDER"`
|
||||||
DbConnectionString string `env:"DB_CONNECTION_STRING" options:"file"`
|
DbConnectionString string `env:"DB_CONNECTION_STRING" options:"file"`
|
||||||
UploadPath string `env:"UPLOAD_PATH"`
|
UploadPath string `env:"UPLOAD_PATH"`
|
||||||
KeysPath string `env:"KEYS_PATH"`
|
KeysPath string `env:"KEYS_PATH"`
|
||||||
KeysStorage string `env:"KEYS_STORAGE"`
|
KeysStorage string `env:"KEYS_STORAGE"`
|
||||||
EncryptionKey []byte `env:"ENCRYPTION_KEY" options:"file"`
|
EncryptionKey []byte `env:"ENCRYPTION_KEY" options:"file"`
|
||||||
Port string `env:"PORT"`
|
Port string `env:"PORT"`
|
||||||
Host string `env:"HOST" options:"toLower"`
|
Host string `env:"HOST"`
|
||||||
UnixSocket string `env:"UNIX_SOCKET"`
|
UnixSocket string `env:"UNIX_SOCKET"`
|
||||||
UnixSocketMode string `env:"UNIX_SOCKET_MODE"`
|
UnixSocketMode string `env:"UNIX_SOCKET_MODE"`
|
||||||
MaxMindLicenseKey string `env:"MAXMIND_LICENSE_KEY" options:"file"`
|
MaxMindLicenseKey string `env:"MAXMIND_LICENSE_KEY" options:"file"`
|
||||||
@@ -112,40 +112,31 @@ func parseEnvConfig() error {
|
|||||||
return fmt.Errorf("error parsing env config: %w", err)
|
return fmt.Errorf("error parsing env config: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = prepareEnvConfig(&EnvConfig)
|
err = resolveFileBasedEnvVariables(&EnvConfig)
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("error preparing env config: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
err = validateEnvConfig(&EnvConfig)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
// Validate the environment variables
|
||||||
|
EnvConfig.LogLevel = strings.ToLower(EnvConfig.LogLevel)
|
||||||
}
|
if _, err := sloggin.ParseLevel(EnvConfig.LogLevel); err != nil {
|
||||||
|
|
||||||
// validateEnvConfig checks the EnvConfig for required fields and valid values
|
|
||||||
func validateEnvConfig(config *EnvConfigSchema) error {
|
|
||||||
if _, err := sloggin.ParseLevel(config.LogLevel); err != nil {
|
|
||||||
return errors.New("invalid LOG_LEVEL value. Must be 'debug', 'info', 'warn' or 'error'")
|
return errors.New("invalid LOG_LEVEL value. Must be 'debug', 'info', 'warn' or 'error'")
|
||||||
}
|
}
|
||||||
|
|
||||||
switch config.DbProvider {
|
switch EnvConfig.DbProvider {
|
||||||
case DbProviderSqlite:
|
case DbProviderSqlite:
|
||||||
if config.DbConnectionString == "" {
|
if EnvConfig.DbConnectionString == "" {
|
||||||
config.DbConnectionString = defaultSqliteConnString
|
EnvConfig.DbConnectionString = defaultSqliteConnString
|
||||||
}
|
}
|
||||||
case DbProviderPostgres:
|
case DbProviderPostgres:
|
||||||
if config.DbConnectionString == "" {
|
if EnvConfig.DbConnectionString == "" {
|
||||||
return errors.New("missing required env var 'DB_CONNECTION_STRING' for Postgres database")
|
return errors.New("missing required env var 'DB_CONNECTION_STRING' for Postgres database")
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
return errors.New("invalid DB_PROVIDER value. Must be 'sqlite' or 'postgres'")
|
return errors.New("invalid DB_PROVIDER value. Must be 'sqlite' or 'postgres'")
|
||||||
}
|
}
|
||||||
|
|
||||||
parsedAppUrl, err := url.Parse(config.AppURL)
|
parsedAppUrl, err := url.Parse(EnvConfig.AppURL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.New("APP_URL is not a valid URL")
|
return errors.New("APP_URL is not a valid URL")
|
||||||
}
|
}
|
||||||
@@ -154,10 +145,10 @@ func validateEnvConfig(config *EnvConfigSchema) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Derive INTERNAL_APP_URL from APP_URL if not set; validate only when provided
|
// Derive INTERNAL_APP_URL from APP_URL if not set; validate only when provided
|
||||||
if config.InternalAppURL == "" {
|
if EnvConfig.InternalAppURL == "" {
|
||||||
config.InternalAppURL = config.AppURL
|
EnvConfig.InternalAppURL = EnvConfig.AppURL
|
||||||
} else {
|
} else {
|
||||||
parsedInternalAppUrl, err := url.Parse(config.InternalAppURL)
|
parsedInternalAppUrl, err := url.Parse(EnvConfig.InternalAppURL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.New("INTERNAL_APP_URL is not a valid URL")
|
return errors.New("INTERNAL_APP_URL is not a valid URL")
|
||||||
}
|
}
|
||||||
@@ -166,26 +157,25 @@ func validateEnvConfig(config *EnvConfigSchema) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
switch config.KeysStorage {
|
switch EnvConfig.KeysStorage {
|
||||||
// KeysStorage defaults to "file" if empty
|
// KeysStorage defaults to "file" if empty
|
||||||
case "":
|
case "":
|
||||||
config.KeysStorage = "file"
|
EnvConfig.KeysStorage = "file"
|
||||||
case "database":
|
case "database":
|
||||||
if config.EncryptionKey == nil {
|
if EnvConfig.EncryptionKey == nil {
|
||||||
return errors.New("ENCRYPTION_KEY must be non-empty when KEYS_STORAGE is database")
|
return errors.New("ENCRYPTION_KEY must be non-empty when KEYS_STORAGE is database")
|
||||||
}
|
}
|
||||||
case "file":
|
case "file":
|
||||||
// All good, these are valid values
|
// All good, these are valid values
|
||||||
default:
|
default:
|
||||||
return fmt.Errorf("invalid value for KEYS_STORAGE: %s", config.KeysStorage)
|
return fmt.Errorf("invalid value for KEYS_STORAGE: %s", EnvConfig.KeysStorage)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// prepareEnvConfig processes special options for EnvConfig fields
|
// resolveFileBasedEnvVariables uses reflection to automatically resolve file-based secrets
|
||||||
func prepareEnvConfig(config *EnvConfigSchema) error {
|
func resolveFileBasedEnvVariables(config *EnvConfigSchema) error {
|
||||||
val := reflect.ValueOf(config).Elem()
|
val := reflect.ValueOf(config).Elem()
|
||||||
typ := val.Type()
|
typ := val.Type()
|
||||||
|
|
||||||
@@ -193,65 +183,48 @@ func prepareEnvConfig(config *EnvConfigSchema) error {
|
|||||||
field := val.Field(i)
|
field := val.Field(i)
|
||||||
fieldType := typ.Field(i)
|
fieldType := typ.Field(i)
|
||||||
|
|
||||||
optionsTag := fieldType.Tag.Get("options")
|
// Only process string and []byte fields
|
||||||
options := strings.Split(optionsTag, ",")
|
isString := field.Kind() == reflect.String
|
||||||
|
isByteSlice := field.Kind() == reflect.Slice && field.Type().Elem().Kind() == reflect.Uint8
|
||||||
|
if !isString && !isByteSlice {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
for _, option := range options {
|
// Only process fields with the "options" tag set to "file"
|
||||||
switch option {
|
optionsTag := fieldType.Tag.Get("options")
|
||||||
case "toLower":
|
if optionsTag != "file" {
|
||||||
if field.Kind() == reflect.String {
|
continue
|
||||||
field.SetString(strings.ToLower(field.String()))
|
}
|
||||||
}
|
|
||||||
case "file":
|
// Only process fields with the "env" tag
|
||||||
err := resolveFileBasedEnvVariable(field, fieldType)
|
envTag := fieldType.Tag.Get("env")
|
||||||
if err != nil {
|
if envTag == "" {
|
||||||
return err
|
continue
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
envVarName := envTag
|
||||||
|
if commaIndex := len(envTag); commaIndex > 0 {
|
||||||
|
envVarName = envTag[:commaIndex]
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the file environment variable is not set, skip
|
||||||
|
envVarFileName := envVarName + "_FILE"
|
||||||
|
envVarFileValue := os.Getenv(envVarFileName)
|
||||||
|
if envVarFileValue == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
fileContent, err := os.ReadFile(envVarFileValue)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to read file for env var %s: %w", envVarFileName, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if isString {
|
||||||
|
field.SetString(strings.TrimSpace(string(fileContent)))
|
||||||
|
} else {
|
||||||
|
field.SetBytes(fileContent)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// resolveFileBasedEnvVariable checks if an environment variable with the suffix "_FILE" is set,
|
|
||||||
// reads the content of the file specified by that variable, and sets the corresponding field's value.
|
|
||||||
func resolveFileBasedEnvVariable(field reflect.Value, fieldType reflect.StructField) error {
|
|
||||||
// Only process string and []byte fields
|
|
||||||
isString := field.Kind() == reflect.String
|
|
||||||
isByteSlice := field.Kind() == reflect.Slice && field.Type().Elem().Kind() == reflect.Uint8
|
|
||||||
if !isString && !isByteSlice {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Only process fields with the "env" tag
|
|
||||||
envTag := fieldType.Tag.Get("env")
|
|
||||||
if envTag == "" {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
envVarName := envTag
|
|
||||||
if commaIndex := len(envTag); commaIndex > 0 {
|
|
||||||
envVarName = envTag[:commaIndex]
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the file environment variable is not set, skip
|
|
||||||
envVarFileName := envVarName + "_FILE"
|
|
||||||
envVarFileValue := os.Getenv(envVarFileName)
|
|
||||||
if envVarFileValue == "" {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
fileContent, err := os.ReadFile(envVarFileValue)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to read file for env var %s: %w", envVarFileName, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if isString {
|
|
||||||
field.SetString(strings.TrimSpace(string(fileContent)))
|
|
||||||
} else {
|
|
||||||
field.SetBytes(fileContent)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -17,19 +17,18 @@ func TestParseEnvConfig(t *testing.T) {
|
|||||||
|
|
||||||
t.Run("should parse valid SQLite config correctly", func(t *testing.T) {
|
t.Run("should parse valid SQLite config correctly", func(t *testing.T) {
|
||||||
EnvConfig = defaultConfig()
|
EnvConfig = defaultConfig()
|
||||||
t.Setenv("DB_PROVIDER", "SQLITE") // should be lowercased automatically
|
t.Setenv("DB_PROVIDER", "sqlite")
|
||||||
t.Setenv("DB_CONNECTION_STRING", "file:test.db")
|
t.Setenv("DB_CONNECTION_STRING", "file:test.db")
|
||||||
t.Setenv("APP_URL", "HTTP://LOCALHOST:3000")
|
t.Setenv("APP_URL", "http://localhost:3000")
|
||||||
|
|
||||||
err := parseEnvConfig()
|
err := parseEnvConfig()
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Equal(t, DbProviderSqlite, EnvConfig.DbProvider)
|
assert.Equal(t, DbProviderSqlite, EnvConfig.DbProvider)
|
||||||
assert.Equal(t, "http://localhost:3000", EnvConfig.AppURL)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("should parse valid Postgres config correctly", func(t *testing.T) {
|
t.Run("should parse valid Postgres config correctly", func(t *testing.T) {
|
||||||
EnvConfig = defaultConfig()
|
EnvConfig = defaultConfig()
|
||||||
t.Setenv("DB_PROVIDER", "POSTGRES")
|
t.Setenv("DB_PROVIDER", "postgres")
|
||||||
t.Setenv("DB_CONNECTION_STRING", "postgres://user:pass@localhost/db")
|
t.Setenv("DB_CONNECTION_STRING", "postgres://user:pass@localhost/db")
|
||||||
t.Setenv("APP_URL", "https://example.com")
|
t.Setenv("APP_URL", "https://example.com")
|
||||||
|
|
||||||
@@ -52,6 +51,7 @@ func TestParseEnvConfig(t *testing.T) {
|
|||||||
t.Run("should set default SQLite connection string when DB_CONNECTION_STRING is empty", func(t *testing.T) {
|
t.Run("should set default SQLite connection string when DB_CONNECTION_STRING is empty", func(t *testing.T) {
|
||||||
EnvConfig = defaultConfig()
|
EnvConfig = defaultConfig()
|
||||||
t.Setenv("DB_PROVIDER", "sqlite")
|
t.Setenv("DB_PROVIDER", "sqlite")
|
||||||
|
t.Setenv("DB_CONNECTION_STRING", "") // Explicitly empty
|
||||||
t.Setenv("APP_URL", "http://localhost:3000")
|
t.Setenv("APP_URL", "http://localhost:3000")
|
||||||
|
|
||||||
err := parseEnvConfig()
|
err := parseEnvConfig()
|
||||||
@@ -192,25 +192,25 @@ func TestParseEnvConfig(t *testing.T) {
|
|||||||
t.Setenv("DB_PROVIDER", "postgres")
|
t.Setenv("DB_PROVIDER", "postgres")
|
||||||
t.Setenv("DB_CONNECTION_STRING", "postgres://test")
|
t.Setenv("DB_CONNECTION_STRING", "postgres://test")
|
||||||
t.Setenv("APP_URL", "https://prod.example.com")
|
t.Setenv("APP_URL", "https://prod.example.com")
|
||||||
t.Setenv("APP_ENV", "STAGING")
|
t.Setenv("APP_ENV", "staging")
|
||||||
t.Setenv("UPLOAD_PATH", "/custom/uploads")
|
t.Setenv("UPLOAD_PATH", "/custom/uploads")
|
||||||
t.Setenv("KEYS_PATH", "/custom/keys")
|
t.Setenv("KEYS_PATH", "/custom/keys")
|
||||||
t.Setenv("PORT", "8080")
|
t.Setenv("PORT", "8080")
|
||||||
t.Setenv("HOST", "LOCALHOST")
|
t.Setenv("HOST", "127.0.0.1")
|
||||||
t.Setenv("UNIX_SOCKET", "/tmp/app.sock")
|
t.Setenv("UNIX_SOCKET", "/tmp/app.sock")
|
||||||
t.Setenv("MAXMIND_LICENSE_KEY", "test-license")
|
t.Setenv("MAXMIND_LICENSE_KEY", "test-license")
|
||||||
t.Setenv("GEOLITE_DB_PATH", "/custom/geolite.mmdb")
|
t.Setenv("GEOLITE_DB_PATH", "/custom/geolite.mmdb")
|
||||||
|
|
||||||
err := parseEnvConfig()
|
err := parseEnvConfig()
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Equal(t, "staging", EnvConfig.AppEnv) // lowercased
|
assert.Equal(t, "staging", EnvConfig.AppEnv)
|
||||||
assert.Equal(t, "/custom/uploads", EnvConfig.UploadPath)
|
assert.Equal(t, "/custom/uploads", EnvConfig.UploadPath)
|
||||||
assert.Equal(t, "8080", EnvConfig.Port)
|
assert.Equal(t, "8080", EnvConfig.Port)
|
||||||
assert.Equal(t, "localhost", EnvConfig.Host) // lowercased
|
assert.Equal(t, "127.0.0.1", EnvConfig.Host)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestPrepareEnvConfig_FileBasedAndToLower(t *testing.T) {
|
func TestResolveFileBasedEnvVariables(t *testing.T) {
|
||||||
// Create temporary directory for test files
|
// Create temporary directory for test files
|
||||||
tempDir := t.TempDir()
|
tempDir := t.TempDir()
|
||||||
|
|
||||||
@@ -225,34 +225,103 @@ func TestPrepareEnvConfig_FileBasedAndToLower(t *testing.T) {
|
|||||||
err = os.WriteFile(dbConnFile, []byte(dbConnContent), 0600)
|
err = os.WriteFile(dbConnFile, []byte(dbConnContent), 0600)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Create a binary file for testing binary data handling
|
||||||
binaryKeyFile := tempDir + "/binary_key.bin"
|
binaryKeyFile := tempDir + "/binary_key.bin"
|
||||||
binaryKeyContent := []byte{0x01, 0x02, 0x03, 0x04}
|
binaryKeyContent := []byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0x10}
|
||||||
err = os.WriteFile(binaryKeyFile, binaryKeyContent, 0600)
|
err = os.WriteFile(binaryKeyFile, binaryKeyContent, 0600)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
t.Run("should process toLower and file options", func(t *testing.T) {
|
t.Run("should read file content for fields with options:file tag", func(t *testing.T) {
|
||||||
config := defaultConfig()
|
config := defaultConfig()
|
||||||
config.AppEnv = "STAGING"
|
|
||||||
config.Host = "LOCALHOST"
|
|
||||||
|
|
||||||
|
// Set environment variables pointing to files
|
||||||
t.Setenv("ENCRYPTION_KEY_FILE", encryptionKeyFile)
|
t.Setenv("ENCRYPTION_KEY_FILE", encryptionKeyFile)
|
||||||
t.Setenv("DB_CONNECTION_STRING_FILE", dbConnFile)
|
t.Setenv("DB_CONNECTION_STRING_FILE", dbConnFile)
|
||||||
|
|
||||||
err := prepareEnvConfig(&config)
|
err := resolveFileBasedEnvVariables(&config)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
assert.Equal(t, "staging", config.AppEnv)
|
// Verify file contents were read correctly
|
||||||
assert.Equal(t, "localhost", config.Host)
|
|
||||||
assert.Equal(t, []byte(encryptionKeyContent), config.EncryptionKey)
|
assert.Equal(t, []byte(encryptionKeyContent), config.EncryptionKey)
|
||||||
assert.Equal(t, dbConnContent, config.DbConnectionString)
|
assert.Equal(t, dbConnContent, config.DbConnectionString)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
t.Run("should skip fields without options:file tag", func(t *testing.T) {
|
||||||
|
config := defaultConfig()
|
||||||
|
originalAppURL := config.AppURL
|
||||||
|
|
||||||
|
// Set a file for a field that doesn't have options:file tag
|
||||||
|
t.Setenv("APP_URL_FILE", "/tmp/nonexistent.txt")
|
||||||
|
|
||||||
|
err := resolveFileBasedEnvVariables(&config)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// AppURL should remain unchanged
|
||||||
|
assert.Equal(t, originalAppURL, config.AppURL)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("should skip non-string fields", func(t *testing.T) {
|
||||||
|
// This test verifies that non-string fields are skipped
|
||||||
|
// We test this indirectly by ensuring the function doesn't error
|
||||||
|
// when processing the actual EnvConfigSchema which has bool fields
|
||||||
|
config := defaultConfig()
|
||||||
|
|
||||||
|
err := resolveFileBasedEnvVariables(&config)
|
||||||
|
require.NoError(t, err)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("should skip when _FILE environment variable is not set", func(t *testing.T) {
|
||||||
|
config := defaultConfig()
|
||||||
|
originalEncryptionKey := config.EncryptionKey
|
||||||
|
|
||||||
|
// Don't set ENCRYPTION_KEY_FILE environment variable
|
||||||
|
|
||||||
|
err := resolveFileBasedEnvVariables(&config)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// EncryptionKey should remain unchanged
|
||||||
|
assert.Equal(t, originalEncryptionKey, config.EncryptionKey)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("should handle multiple file-based variables simultaneously", func(t *testing.T) {
|
||||||
|
config := defaultConfig()
|
||||||
|
|
||||||
|
// Set multiple file environment variables
|
||||||
|
t.Setenv("ENCRYPTION_KEY_FILE", encryptionKeyFile)
|
||||||
|
t.Setenv("DB_CONNECTION_STRING_FILE", dbConnFile)
|
||||||
|
|
||||||
|
err := resolveFileBasedEnvVariables(&config)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// All should be resolved correctly
|
||||||
|
assert.Equal(t, []byte(encryptionKeyContent), config.EncryptionKey)
|
||||||
|
assert.Equal(t, dbConnContent, config.DbConnectionString)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("should handle mixed file and non-file environment variables", func(t *testing.T) {
|
||||||
|
config := defaultConfig()
|
||||||
|
|
||||||
|
// Set both file and non-file environment variables
|
||||||
|
t.Setenv("ENCRYPTION_KEY_FILE", encryptionKeyFile)
|
||||||
|
|
||||||
|
err := resolveFileBasedEnvVariables(&config)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// File-based should be resolved, others should remain as set by env parser
|
||||||
|
assert.Equal(t, []byte(encryptionKeyContent), config.EncryptionKey)
|
||||||
|
assert.Equal(t, "http://localhost:1411", config.AppURL)
|
||||||
|
})
|
||||||
|
|
||||||
t.Run("should handle binary data correctly", func(t *testing.T) {
|
t.Run("should handle binary data correctly", func(t *testing.T) {
|
||||||
config := defaultConfig()
|
config := defaultConfig()
|
||||||
|
|
||||||
|
// Set environment variable pointing to binary file
|
||||||
t.Setenv("ENCRYPTION_KEY_FILE", binaryKeyFile)
|
t.Setenv("ENCRYPTION_KEY_FILE", binaryKeyFile)
|
||||||
|
|
||||||
err := prepareEnvConfig(&config)
|
err := resolveFileBasedEnvVariables(&config)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Verify binary data was read correctly without corruption
|
||||||
assert.Equal(t, binaryKeyContent, config.EncryptionKey)
|
assert.Equal(t, binaryKeyContent, config.EncryptionKey)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,7 +41,6 @@ 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"`
|
||||||
LdapAttributeUserDisplayName string `json:"ldapAttributeUserDisplayName"`
|
|
||||||
LdapAttributeUserProfilePicture string `json:"ldapAttributeUserProfilePicture"`
|
LdapAttributeUserProfilePicture string `json:"ldapAttributeUserProfilePicture"`
|
||||||
LdapAttributeGroupMember string `json:"ldapAttributeGroupMember"`
|
LdapAttributeGroupMember string `json:"ldapAttributeGroupMember"`
|
||||||
LdapAttributeGroupUniqueIdentifier string `json:"ldapAttributeGroupUniqueIdentifier"`
|
LdapAttributeGroupUniqueIdentifier string `json:"ldapAttributeGroupUniqueIdentifier"`
|
||||||
|
|||||||
@@ -12,8 +12,7 @@ type UserDto struct {
|
|||||||
Username string `json:"username"`
|
Username string `json:"username"`
|
||||||
Email string `json:"email" `
|
Email string `json:"email" `
|
||||||
FirstName string `json:"firstName"`
|
FirstName string `json:"firstName"`
|
||||||
LastName *string `json:"lastName"`
|
LastName string `json:"lastName"`
|
||||||
DisplayName string `json:"displayName"`
|
|
||||||
IsAdmin bool `json:"isAdmin"`
|
IsAdmin bool `json:"isAdmin"`
|
||||||
Locale *string `json:"locale"`
|
Locale *string `json:"locale"`
|
||||||
CustomClaims []CustomClaimDto `json:"customClaims"`
|
CustomClaims []CustomClaimDto `json:"customClaims"`
|
||||||
@@ -23,15 +22,14 @@ type UserDto struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type UserCreateDto struct {
|
type UserCreateDto struct {
|
||||||
Username string `json:"username" binding:"required,username,min=2,max=50" unorm:"nfc"`
|
Username string `json:"username" binding:"required,username,min=2,max=50" unorm:"nfc"`
|
||||||
Email string `json:"email" binding:"required,email" unorm:"nfc"`
|
Email string `json:"email" binding:"required,email" unorm:"nfc"`
|
||||||
FirstName string `json:"firstName" binding:"required,min=1,max=50" unorm:"nfc"`
|
FirstName string `json:"firstName" binding:"required,min=1,max=50" unorm:"nfc"`
|
||||||
LastName string `json:"lastName" binding:"max=50" unorm:"nfc"`
|
LastName string `json:"lastName" binding:"max=50" unorm:"nfc"`
|
||||||
DisplayName string `json:"displayName" binding:"required,max=100" unorm:"nfc"`
|
IsAdmin bool `json:"isAdmin"`
|
||||||
IsAdmin bool `json:"isAdmin"`
|
Locale *string `json:"locale"`
|
||||||
Locale *string `json:"locale"`
|
Disabled bool `json:"disabled"`
|
||||||
Disabled bool `json:"disabled"`
|
LdapID string `json:"-"`
|
||||||
LdapID string `json:"-"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u UserCreateDto) Validate() error {
|
func (u UserCreateDto) Validate() error {
|
||||||
|
|||||||
@@ -15,74 +15,59 @@ func TestUserCreateDto_Validate(t *testing.T) {
|
|||||||
{
|
{
|
||||||
name: "valid input",
|
name: "valid input",
|
||||||
input: UserCreateDto{
|
input: UserCreateDto{
|
||||||
Username: "testuser",
|
Username: "testuser",
|
||||||
Email: "test@example.com",
|
Email: "test@example.com",
|
||||||
FirstName: "John",
|
FirstName: "John",
|
||||||
LastName: "Doe",
|
LastName: "Doe",
|
||||||
DisplayName: "John Doe",
|
|
||||||
},
|
},
|
||||||
wantErr: "",
|
wantErr: "",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "missing username",
|
name: "missing username",
|
||||||
input: UserCreateDto{
|
|
||||||
Email: "test@example.com",
|
|
||||||
FirstName: "John",
|
|
||||||
LastName: "Doe",
|
|
||||||
DisplayName: "John Doe",
|
|
||||||
},
|
|
||||||
wantErr: "Field validation for 'Username' failed on the 'required' tag",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "missing display name",
|
|
||||||
input: UserCreateDto{
|
input: UserCreateDto{
|
||||||
Email: "test@example.com",
|
Email: "test@example.com",
|
||||||
FirstName: "John",
|
FirstName: "John",
|
||||||
LastName: "Doe",
|
LastName: "Doe",
|
||||||
},
|
},
|
||||||
wantErr: "Field validation for 'DisplayName' failed on the 'required' tag",
|
wantErr: "Field validation for 'Username' failed on the 'required' tag",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "username contains invalid characters",
|
name: "username contains invalid characters",
|
||||||
input: UserCreateDto{
|
input: UserCreateDto{
|
||||||
Username: "test/ser",
|
Username: "test/ser",
|
||||||
Email: "test@example.com",
|
Email: "test@example.com",
|
||||||
FirstName: "John",
|
FirstName: "John",
|
||||||
LastName: "Doe",
|
LastName: "Doe",
|
||||||
DisplayName: "John Doe",
|
|
||||||
},
|
},
|
||||||
wantErr: "Field validation for 'Username' failed on the 'username' tag",
|
wantErr: "Field validation for 'Username' failed on the 'username' tag",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "invalid email",
|
name: "invalid email",
|
||||||
input: UserCreateDto{
|
input: UserCreateDto{
|
||||||
Username: "testuser",
|
Username: "testuser",
|
||||||
Email: "not-an-email",
|
Email: "not-an-email",
|
||||||
FirstName: "John",
|
FirstName: "John",
|
||||||
LastName: "Doe",
|
LastName: "Doe",
|
||||||
DisplayName: "John Doe",
|
|
||||||
},
|
},
|
||||||
wantErr: "Field validation for 'Email' failed on the 'email' tag",
|
wantErr: "Field validation for 'Email' failed on the 'email' tag",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "first name too short",
|
name: "first name too short",
|
||||||
input: UserCreateDto{
|
input: UserCreateDto{
|
||||||
Username: "testuser",
|
Username: "testuser",
|
||||||
Email: "test@example.com",
|
Email: "test@example.com",
|
||||||
FirstName: "",
|
FirstName: "",
|
||||||
LastName: "Doe",
|
LastName: "Doe",
|
||||||
DisplayName: "John Doe",
|
|
||||||
},
|
},
|
||||||
wantErr: "Field validation for 'FirstName' failed on the 'required' tag",
|
wantErr: "Field validation for 'FirstName' failed on the 'required' tag",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "last name too long",
|
name: "last name too long",
|
||||||
input: UserCreateDto{
|
input: UserCreateDto{
|
||||||
Username: "testuser",
|
Username: "testuser",
|
||||||
Email: "test@example.com",
|
Email: "test@example.com",
|
||||||
FirstName: "John",
|
FirstName: "John",
|
||||||
LastName: "abcdfghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz",
|
LastName: "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz",
|
||||||
DisplayName: "John Doe",
|
|
||||||
},
|
},
|
||||||
wantErr: "Field validation for 'LastName' failed on the 'max' tag",
|
wantErr: "Field validation for 'LastName' failed on the 'max' tag",
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ func handleValidationError(validationErrors validator.ValidationErrors) string {
|
|||||||
case "email":
|
case "email":
|
||||||
errorMessage = fmt.Sprintf("%s must be a valid email address", fieldName)
|
errorMessage = fmt.Sprintf("%s must be a valid email address", fieldName)
|
||||||
case "username":
|
case "username":
|
||||||
errorMessage = fmt.Sprintf("%s must only contain letters, numbers, underscores, dots, hyphens, and '@' symbols and not start or end with a special character", fieldName)
|
errorMessage = fmt.Sprintf("%s must only contain lowercase letters, numbers, underscores, dots, hyphens, and '@' symbols and not start or end with a special character", fieldName)
|
||||||
case "url":
|
case "url":
|
||||||
errorMessage = fmt.Sprintf("%s must be a valid URL", fieldName)
|
errorMessage = fmt.Sprintf("%s must be a valid URL", fieldName)
|
||||||
case "min":
|
case "min":
|
||||||
|
|||||||
@@ -74,7 +74,6 @@ type AppConfig struct {
|
|||||||
LdapAttributeUserEmail AppConfigVariable `key:"ldapAttributeUserEmail"`
|
LdapAttributeUserEmail AppConfigVariable `key:"ldapAttributeUserEmail"`
|
||||||
LdapAttributeUserFirstName AppConfigVariable `key:"ldapAttributeUserFirstName"`
|
LdapAttributeUserFirstName AppConfigVariable `key:"ldapAttributeUserFirstName"`
|
||||||
LdapAttributeUserLastName AppConfigVariable `key:"ldapAttributeUserLastName"`
|
LdapAttributeUserLastName AppConfigVariable `key:"ldapAttributeUserLastName"`
|
||||||
LdapAttributeUserDisplayName AppConfigVariable `key:"ldapAttributeUserDisplayName"`
|
|
||||||
LdapAttributeUserProfilePicture AppConfigVariable `key:"ldapAttributeUserProfilePicture"`
|
LdapAttributeUserProfilePicture AppConfigVariable `key:"ldapAttributeUserProfilePicture"`
|
||||||
LdapAttributeGroupMember AppConfigVariable `key:"ldapAttributeGroupMember"`
|
LdapAttributeGroupMember AppConfigVariable `key:"ldapAttributeGroupMember"`
|
||||||
LdapAttributeGroupUniqueIdentifier AppConfigVariable `key:"ldapAttributeGroupUniqueIdentifier"`
|
LdapAttributeGroupUniqueIdentifier AppConfigVariable `key:"ldapAttributeGroupUniqueIdentifier"`
|
||||||
|
|||||||
@@ -13,15 +13,14 @@ import (
|
|||||||
type User struct {
|
type User struct {
|
||||||
Base
|
Base
|
||||||
|
|
||||||
Username string `sortable:"true"`
|
Username string `sortable:"true"`
|
||||||
Email string `sortable:"true"`
|
Email string `sortable:"true"`
|
||||||
FirstName string `sortable:"true"`
|
FirstName string `sortable:"true"`
|
||||||
LastName string `sortable:"true"`
|
LastName string `sortable:"true"`
|
||||||
DisplayName string `sortable:"true"`
|
IsAdmin bool `sortable:"true"`
|
||||||
IsAdmin bool `sortable:"true"`
|
Locale *string
|
||||||
Locale *string
|
LdapID *string
|
||||||
LdapID *string
|
Disabled bool `sortable:"true"`
|
||||||
Disabled bool `sortable:"true"`
|
|
||||||
|
|
||||||
CustomClaims []CustomClaim
|
CustomClaims []CustomClaim
|
||||||
UserGroups []UserGroup `gorm:"many2many:user_groups_users;"`
|
UserGroups []UserGroup `gorm:"many2many:user_groups_users;"`
|
||||||
@@ -32,12 +31,7 @@ func (u User) WebAuthnID() []byte { return []byte(u.ID) }
|
|||||||
|
|
||||||
func (u User) WebAuthnName() string { return u.Username }
|
func (u User) WebAuthnName() string { return u.Username }
|
||||||
|
|
||||||
func (u User) WebAuthnDisplayName() string {
|
func (u User) WebAuthnDisplayName() string { return u.FirstName + " " + u.LastName }
|
||||||
if u.DisplayName != "" {
|
|
||||||
return u.DisplayName
|
|
||||||
}
|
|
||||||
return u.FirstName + " " + u.LastName
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u User) WebAuthnIcon() string { return "" }
|
func (u User) WebAuthnIcon() string { return "" }
|
||||||
|
|
||||||
@@ -72,9 +66,7 @@ func (u User) WebAuthnCredentialDescriptors() (descriptors []protocol.Credential
|
|||||||
return descriptors
|
return descriptors
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u User) FullName() string {
|
func (u User) FullName() string { return u.FirstName + " " + u.LastName }
|
||||||
return u.FirstName + " " + u.LastName
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u User) Initials() string {
|
func (u User) Initials() string {
|
||||||
first := utils.GetFirstCharacter(u.FirstName)
|
first := utils.GetFirstCharacter(u.FirstName)
|
||||||
|
|||||||
@@ -100,7 +100,6 @@ func (s *AppConfigService) getDefaultDbConfig() *model.AppConfig {
|
|||||||
LdapAttributeUserEmail: model.AppConfigVariable{},
|
LdapAttributeUserEmail: model.AppConfigVariable{},
|
||||||
LdapAttributeUserFirstName: model.AppConfigVariable{},
|
LdapAttributeUserFirstName: model.AppConfigVariable{},
|
||||||
LdapAttributeUserLastName: model.AppConfigVariable{},
|
LdapAttributeUserLastName: model.AppConfigVariable{},
|
||||||
LdapAttributeUserDisplayName: model.AppConfigVariable{Value: "cn"},
|
|
||||||
LdapAttributeUserProfilePicture: model.AppConfigVariable{},
|
LdapAttributeUserProfilePicture: model.AppConfigVariable{},
|
||||||
LdapAttributeGroupMember: model.AppConfigVariable{Value: "member"},
|
LdapAttributeGroupMember: model.AppConfigVariable{Value: "member"},
|
||||||
LdapAttributeGroupUniqueIdentifier: model.AppConfigVariable{},
|
LdapAttributeGroupUniqueIdentifier: model.AppConfigVariable{},
|
||||||
|
|||||||
@@ -25,7 +25,6 @@ func isReservedClaim(key string) bool {
|
|||||||
"name",
|
"name",
|
||||||
"email",
|
"email",
|
||||||
"preferred_username",
|
"preferred_username",
|
||||||
"display_name",
|
|
||||||
"groups",
|
"groups",
|
||||||
TokenTypeClaim,
|
TokenTypeClaim,
|
||||||
"sub",
|
"sub",
|
||||||
|
|||||||
@@ -78,23 +78,21 @@ func (s *TestService) SeedDatabase(baseURL string) error {
|
|||||||
Base: model.Base{
|
Base: model.Base{
|
||||||
ID: "f4b89dc2-62fb-46bf-9f5f-c34f4eafe93e",
|
ID: "f4b89dc2-62fb-46bf-9f5f-c34f4eafe93e",
|
||||||
},
|
},
|
||||||
Username: "tim",
|
Username: "tim",
|
||||||
Email: "tim.cook@test.com",
|
Email: "tim.cook@test.com",
|
||||||
FirstName: "Tim",
|
FirstName: "Tim",
|
||||||
LastName: "Cook",
|
LastName: "Cook",
|
||||||
DisplayName: "Tim Cook",
|
IsAdmin: true,
|
||||||
IsAdmin: true,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Base: model.Base{
|
Base: model.Base{
|
||||||
ID: "1cd19686-f9a6-43f4-a41f-14a0bf5b4036",
|
ID: "1cd19686-f9a6-43f4-a41f-14a0bf5b4036",
|
||||||
},
|
},
|
||||||
Username: "craig",
|
Username: "craig",
|
||||||
Email: "craig.federighi@test.com",
|
Email: "craig.federighi@test.com",
|
||||||
FirstName: "Craig",
|
FirstName: "Craig",
|
||||||
LastName: "Federighi",
|
LastName: "Federighi",
|
||||||
DisplayName: "Craig Federighi",
|
IsAdmin: false,
|
||||||
IsAdmin: false,
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
for _, user := range users {
|
for _, user := range users {
|
||||||
|
|||||||
@@ -278,7 +278,6 @@ func (s *LdapService) SyncUsers(ctx context.Context, tx *gorm.DB, client *ldap.C
|
|||||||
dbConfig.LdapAttributeUserFirstName.Value,
|
dbConfig.LdapAttributeUserFirstName.Value,
|
||||||
dbConfig.LdapAttributeUserLastName.Value,
|
dbConfig.LdapAttributeUserLastName.Value,
|
||||||
dbConfig.LdapAttributeUserProfilePicture.Value,
|
dbConfig.LdapAttributeUserProfilePicture.Value,
|
||||||
dbConfig.LdapAttributeUserDisplayName.Value,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Filters must start and finish with ()!
|
// Filters must start and finish with ()!
|
||||||
@@ -347,13 +346,12 @@ func (s *LdapService) SyncUsers(ctx context.Context, tx *gorm.DB, client *ldap.C
|
|||||||
}
|
}
|
||||||
|
|
||||||
newUser := dto.UserCreateDto{
|
newUser := dto.UserCreateDto{
|
||||||
Username: value.GetAttributeValue(dbConfig.LdapAttributeUserUsername.Value),
|
Username: value.GetAttributeValue(dbConfig.LdapAttributeUserUsername.Value),
|
||||||
Email: value.GetAttributeValue(dbConfig.LdapAttributeUserEmail.Value),
|
Email: value.GetAttributeValue(dbConfig.LdapAttributeUserEmail.Value),
|
||||||
FirstName: value.GetAttributeValue(dbConfig.LdapAttributeUserFirstName.Value),
|
FirstName: value.GetAttributeValue(dbConfig.LdapAttributeUserFirstName.Value),
|
||||||
LastName: value.GetAttributeValue(dbConfig.LdapAttributeUserLastName.Value),
|
LastName: value.GetAttributeValue(dbConfig.LdapAttributeUserLastName.Value),
|
||||||
DisplayName: value.GetAttributeValue(dbConfig.LdapAttributeUserDisplayName.Value),
|
IsAdmin: isAdmin,
|
||||||
IsAdmin: isAdmin,
|
LdapID: ldapId,
|
||||||
LdapID: ldapId,
|
|
||||||
}
|
}
|
||||||
dto.Normalize(newUser)
|
dto.Normalize(newUser)
|
||||||
|
|
||||||
|
|||||||
@@ -1838,6 +1838,13 @@ func (s *OidcService) getUserClaims(ctx context.Context, user *model.User, scope
|
|||||||
}
|
}
|
||||||
|
|
||||||
if slices.Contains(scopes, "profile") {
|
if slices.Contains(scopes, "profile") {
|
||||||
|
// Add profile claims
|
||||||
|
claims["given_name"] = user.FirstName
|
||||||
|
claims["family_name"] = user.LastName
|
||||||
|
claims["name"] = user.FullName()
|
||||||
|
claims["preferred_username"] = user.Username
|
||||||
|
claims["picture"] = common.EnvConfig.AppURL + "/api/users/" + user.ID + "/profile-picture.png"
|
||||||
|
|
||||||
// Add custom claims
|
// Add custom claims
|
||||||
customClaims, err := s.customClaimService.GetCustomClaimsForUserWithUserGroups(ctx, user.ID, tx)
|
customClaims, err := s.customClaimService.GetCustomClaimsForUserWithUserGroups(ctx, user.ID, tx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -1856,15 +1863,6 @@ func (s *OidcService) getUserClaims(ctx context.Context, user *model.User, scope
|
|||||||
claims[customClaim.Key] = customClaim.Value
|
claims[customClaim.Key] = customClaim.Value
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add profile claims
|
|
||||||
claims["given_name"] = user.FirstName
|
|
||||||
claims["family_name"] = user.LastName
|
|
||||||
claims["name"] = user.FullName()
|
|
||||||
claims["display_name"] = user.DisplayName
|
|
||||||
|
|
||||||
claims["preferred_username"] = user.Username
|
|
||||||
claims["picture"] = common.EnvConfig.AppURL + "/api/users/" + user.ID + "/profile-picture.png"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if slices.Contains(scopes, "email") {
|
if slices.Contains(scopes, "email") {
|
||||||
|
|||||||
@@ -245,13 +245,12 @@ func (s *UserService) CreateUser(ctx context.Context, input dto.UserCreateDto) (
|
|||||||
|
|
||||||
func (s *UserService) createUserInternal(ctx context.Context, input dto.UserCreateDto, isLdapSync bool, tx *gorm.DB) (model.User, error) {
|
func (s *UserService) createUserInternal(ctx context.Context, input dto.UserCreateDto, isLdapSync bool, tx *gorm.DB) (model.User, error) {
|
||||||
user := model.User{
|
user := model.User{
|
||||||
FirstName: input.FirstName,
|
FirstName: input.FirstName,
|
||||||
LastName: input.LastName,
|
LastName: input.LastName,
|
||||||
DisplayName: input.DisplayName,
|
Email: input.Email,
|
||||||
Email: input.Email,
|
Username: input.Username,
|
||||||
Username: input.Username,
|
IsAdmin: input.IsAdmin,
|
||||||
IsAdmin: input.IsAdmin,
|
Locale: input.Locale,
|
||||||
Locale: input.Locale,
|
|
||||||
}
|
}
|
||||||
if input.LdapID != "" {
|
if input.LdapID != "" {
|
||||||
user.LdapID = &input.LdapID
|
user.LdapID = &input.LdapID
|
||||||
@@ -363,7 +362,6 @@ func (s *UserService) updateUserInternal(ctx context.Context, userID string, upd
|
|||||||
// Full update: Allow updating all personal fields
|
// Full update: Allow updating all personal fields
|
||||||
user.FirstName = updatedUser.FirstName
|
user.FirstName = updatedUser.FirstName
|
||||||
user.LastName = updatedUser.LastName
|
user.LastName = updatedUser.LastName
|
||||||
user.DisplayName = updatedUser.DisplayName
|
|
||||||
user.Email = updatedUser.Email
|
user.Email = updatedUser.Email
|
||||||
user.Username = updatedUser.Username
|
user.Username = updatedUser.Username
|
||||||
user.Locale = updatedUser.Locale
|
user.Locale = updatedUser.Locale
|
||||||
@@ -602,12 +600,11 @@ func (s *UserService) SignUpInitialAdmin(ctx context.Context, signUpData dto.Sig
|
|||||||
}
|
}
|
||||||
|
|
||||||
userToCreate := dto.UserCreateDto{
|
userToCreate := dto.UserCreateDto{
|
||||||
FirstName: signUpData.FirstName,
|
FirstName: signUpData.FirstName,
|
||||||
LastName: signUpData.LastName,
|
LastName: signUpData.LastName,
|
||||||
DisplayName: strings.TrimSpace(signUpData.FirstName + " " + signUpData.LastName),
|
Username: signUpData.Username,
|
||||||
Username: signUpData.Username,
|
Email: signUpData.Email,
|
||||||
Email: signUpData.Email,
|
IsAdmin: true,
|
||||||
IsAdmin: true,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
user, err := s.createUserInternal(ctx, userToCreate, false, tx)
|
user, err := s.createUserInternal(ctx, userToCreate, false, tx)
|
||||||
@@ -739,11 +736,10 @@ func (s *UserService) SignUp(ctx context.Context, signupData dto.SignUpDto, ipAd
|
|||||||
}
|
}
|
||||||
|
|
||||||
userToCreate := dto.UserCreateDto{
|
userToCreate := dto.UserCreateDto{
|
||||||
Username: signupData.Username,
|
Username: signupData.Username,
|
||||||
Email: signupData.Email,
|
Email: signupData.Email,
|
||||||
FirstName: signupData.FirstName,
|
FirstName: signupData.FirstName,
|
||||||
LastName: signupData.LastName,
|
LastName: signupData.LastName,
|
||||||
DisplayName: strings.TrimSpace(signupData.FirstName + " " + signupData.LastName),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
user, err := s.createUserInternal(ctx, userToCreate, false, tx)
|
user, err := s.createUserInternal(ctx, userToCreate, false, tx)
|
||||||
|
|||||||
@@ -3,11 +3,3 @@ package utils
|
|||||||
func Ptr[T any](v T) *T {
|
func Ptr[T any](v T) *T {
|
||||||
return &v
|
return &v
|
||||||
}
|
}
|
||||||
|
|
||||||
func PtrValueOrZero[T any](ptr *T) T {
|
|
||||||
if ptr == nil {
|
|
||||||
var zero T
|
|
||||||
return zero
|
|
||||||
}
|
|
||||||
return *ptr
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
ALTER TABLE users DROP COLUMN display_name;
|
|
||||||
|
|
||||||
ALTER TABLE users ALTER COLUMN username TYPE TEXT;
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
ALTER TABLE users ADD COLUMN display_name TEXT;
|
|
||||||
UPDATE users SET display_name = trim(coalesce(first_name,'') || ' ' || coalesce(last_name,''));
|
|
||||||
ALTER TABLE users ALTER COLUMN display_name SET NOT NULL;
|
|
||||||
|
|
||||||
CREATE EXTENSION IF NOT EXISTS citext;
|
|
||||||
ALTER TABLE users ALTER COLUMN username TYPE CITEXT COLLATE "C";
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
BEGIN;
|
|
||||||
ALTER TABLE users DROP COLUMN display_name;
|
|
||||||
COMMIT;
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
PRAGMA foreign_keys = OFF;
|
|
||||||
BEGIN;
|
|
||||||
|
|
||||||
CREATE TABLE users_new
|
|
||||||
(
|
|
||||||
id TEXT NOT NULL PRIMARY KEY,
|
|
||||||
created_at DATETIME,
|
|
||||||
username TEXT NOT NULL COLLATE NOCASE UNIQUE,
|
|
||||||
email TEXT NOT NULL UNIQUE,
|
|
||||||
first_name TEXT,
|
|
||||||
last_name TEXT NOT NULL,
|
|
||||||
display_name TEXT NOT NULL,
|
|
||||||
is_admin NUMERIC NOT NULL DEFAULT FALSE,
|
|
||||||
ldap_id TEXT,
|
|
||||||
locale TEXT,
|
|
||||||
disabled NUMERIC NOT NULL DEFAULT FALSE
|
|
||||||
);
|
|
||||||
|
|
||||||
INSERT INTO users_new (id, created_at, username, email, first_name, last_name, display_name, is_admin, ldap_id, locale,
|
|
||||||
disabled)
|
|
||||||
SELECT id,
|
|
||||||
created_at,
|
|
||||||
username,
|
|
||||||
email,
|
|
||||||
first_name,
|
|
||||||
COALESCE(last_name, ''),
|
|
||||||
TRIM(COALESCE(first_name, '') || ' ' || COALESCE(last_name, '')),
|
|
||||||
is_admin,
|
|
||||||
ldap_id,
|
|
||||||
locale,
|
|
||||||
disabled
|
|
||||||
FROM users;
|
|
||||||
|
|
||||||
DROP TABLE users;
|
|
||||||
|
|
||||||
ALTER TABLE users_new
|
|
||||||
RENAME TO users;
|
|
||||||
|
|
||||||
CREATE UNIQUE INDEX users_ldap_id ON users (ldap_id);
|
|
||||||
|
|
||||||
COMMIT;
|
|
||||||
PRAGMA foreign_keys = ON;
|
|
||||||
@@ -120,8 +120,6 @@
|
|||||||
"username": "Username",
|
"username": "Username",
|
||||||
"save": "Save",
|
"save": "Save",
|
||||||
"username_can_only_contain": "Username can only contain lowercase letters, numbers, underscores, dots, hyphens, and '@' symbols",
|
"username_can_only_contain": "Username can only contain lowercase letters, numbers, underscores, dots, hyphens, and '@' symbols",
|
||||||
"username_must_start_with": "Username must start with an alphanumeric character",
|
|
||||||
"username_must_end_with": "Username must end with an alphanumeric character",
|
|
||||||
"sign_in_using_the_following_code_the_code_will_expire_in_minutes": "Sign in using the following code. The code will expire in 15 minutes.",
|
"sign_in_using_the_following_code_the_code_will_expire_in_minutes": "Sign in using the following code. The code will expire in 15 minutes.",
|
||||||
"or_visit": "or visit",
|
"or_visit": "or visit",
|
||||||
"added_on": "Added on",
|
"added_on": "Added on",
|
||||||
@@ -445,10 +443,7 @@
|
|||||||
"custom_client_id_description": "Set a custom client ID if this is required by your application. Otherwise, leave it blank to generate a random one.",
|
"custom_client_id_description": "Set a custom client ID if this is required by your application. Otherwise, leave it blank to generate a random one.",
|
||||||
"generated": "Generated",
|
"generated": "Generated",
|
||||||
"administration": "Administration",
|
"administration": "Administration",
|
||||||
"group_rdn_attribute_description": "The attribute used in the groups distinguished name (DN).",
|
"group_rdn_attribute_description": "The attribute used in the groups distinguished name (DN). Recommended value: `cn`",
|
||||||
"display_name_attribute": "Display Name Attribute",
|
|
||||||
"display_name": "Display Name",
|
|
||||||
"configure_application_images": "Configure Application Images",
|
|
||||||
"ui_config_disabled_info_title": "UI Configuration Disabled",
|
"ui_config_disabled_info_title": "UI Configuration Disabled",
|
||||||
"ui_config_disabled_info_description": "The UI configuration is disabled because the application configuration settings are managed through environment variables. Some settings may not be editable."
|
"ui_config_disabled_info_description": "The UI configuration is disabled because the application configuration settings are managed through environment variables. Some settings may not be editable."
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "pocket-id-frontend",
|
"name": "pocket-id-frontend",
|
||||||
"version": "1.11.0",
|
"version": "1.10.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -5,7 +5,6 @@
|
|||||||
import { preventDefault } from '$lib/utils/event-util';
|
import { preventDefault } from '$lib/utils/event-util';
|
||||||
import { createForm } from '$lib/utils/form-util';
|
import { createForm } from '$lib/utils/form-util';
|
||||||
import { tryCatch } from '$lib/utils/try-catch-util';
|
import { tryCatch } from '$lib/utils/try-catch-util';
|
||||||
import { emptyToUndefined, usernameSchema } from '$lib/utils/zod-util';
|
|
||||||
import { z } from 'zod/v4';
|
import { z } from 'zod/v4';
|
||||||
|
|
||||||
let {
|
let {
|
||||||
@@ -25,8 +24,12 @@
|
|||||||
|
|
||||||
const formSchema = z.object({
|
const formSchema = z.object({
|
||||||
firstName: z.string().min(1).max(50),
|
firstName: z.string().min(1).max(50),
|
||||||
lastName: emptyToUndefined(z.string().max(50).optional()),
|
lastName: z.string().max(50).optional(),
|
||||||
username: usernameSchema,
|
username: z
|
||||||
|
.string()
|
||||||
|
.min(2)
|
||||||
|
.max(30)
|
||||||
|
.regex(/^[a-z0-9_@.-]+$/, m.username_can_only_contain()),
|
||||||
email: z.email()
|
email: z.email()
|
||||||
});
|
});
|
||||||
type FormSchema = typeof formSchema;
|
type FormSchema = typeof formSchema;
|
||||||
|
|||||||
@@ -41,7 +41,6 @@ export type AllAppConfig = AppConfig & {
|
|||||||
ldapAttributeUserEmail: string;
|
ldapAttributeUserEmail: string;
|
||||||
ldapAttributeUserFirstName: string;
|
ldapAttributeUserFirstName: string;
|
||||||
ldapAttributeUserLastName: string;
|
ldapAttributeUserLastName: string;
|
||||||
ldapAttributeUserDisplayName: string;
|
|
||||||
ldapAttributeUserProfilePicture: string;
|
ldapAttributeUserProfilePicture: string;
|
||||||
ldapAttributeGroupMember: string;
|
ldapAttributeGroupMember: string;
|
||||||
ldapAttributeGroupUniqueIdentifier: string;
|
ldapAttributeGroupUniqueIdentifier: string;
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ export type User = {
|
|||||||
email: string;
|
email: string;
|
||||||
firstName: string;
|
firstName: string;
|
||||||
lastName?: string;
|
lastName?: string;
|
||||||
displayName: string;
|
|
||||||
isAdmin: boolean;
|
isAdmin: boolean;
|
||||||
userGroups: UserGroup[];
|
userGroups: UserGroup[];
|
||||||
customClaims: CustomClaim[];
|
customClaims: CustomClaim[];
|
||||||
@@ -19,6 +18,6 @@ export type User = {
|
|||||||
|
|
||||||
export type UserCreate = Omit<User, 'id' | 'customClaims' | 'ldapId' | 'userGroups'>;
|
export type UserCreate = Omit<User, 'id' | 'customClaims' | 'ldapId' | 'userGroups'>;
|
||||||
|
|
||||||
export type UserSignUp = Omit<UserCreate, 'isAdmin' | 'disabled' | 'displayName'> & {
|
export type UserSignUp = Omit<UserCreate, 'isAdmin' | 'disabled'> & {
|
||||||
token?: string;
|
token?: string;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,19 +1,8 @@
|
|||||||
import {
|
import { setLocale as setParaglideLocale, type Locale } from '$lib/paraglide/runtime';
|
||||||
extractLocaleFromCookie,
|
|
||||||
setLocale as setParaglideLocale,
|
|
||||||
type Locale
|
|
||||||
} from '$lib/paraglide/runtime';
|
|
||||||
import { setDefaultOptions } from 'date-fns';
|
import { setDefaultOptions } from 'date-fns';
|
||||||
import { z } from 'zod/v4';
|
import { z } from 'zod/v4';
|
||||||
|
|
||||||
export async function setLocale(locale: Locale, reload = true) {
|
export async function setLocale(locale: Locale, reload = true) {
|
||||||
await setLocaleForLibraries(locale);
|
|
||||||
setParaglideLocale(locale, { reload });
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function setLocaleForLibraries(
|
|
||||||
locale: Locale = (extractLocaleFromCookie() as Locale) || 'en'
|
|
||||||
) {
|
|
||||||
const [zodResult, dateFnsResult] = await Promise.allSettled([
|
const [zodResult, dateFnsResult] = await Promise.allSettled([
|
||||||
import(`../../../node_modules/zod/v4/locales/${locale}.js`),
|
import(`../../../node_modules/zod/v4/locales/${locale}.js`),
|
||||||
import(`../../../node_modules/date-fns/locale/${locale}.js`)
|
import(`../../../node_modules/date-fns/locale/${locale}.js`)
|
||||||
@@ -25,6 +14,8 @@ export async function setLocaleForLibraries(
|
|||||||
console.warn(`Failed to load zod locale for ${locale}:`, zodResult.reason);
|
console.warn(`Failed to load zod locale for ${locale}:`, zodResult.reason);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setParaglideLocale(locale, { reload });
|
||||||
|
|
||||||
if (dateFnsResult.status === 'fulfilled') {
|
if (dateFnsResult.status === 'fulfilled') {
|
||||||
setDefaultOptions({
|
setDefaultOptions({
|
||||||
locale: dateFnsResult.value.default
|
locale: dateFnsResult.value.default
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { m } from '$lib/paraglide/messages';
|
import { m } from '$lib/paraglide/messages';
|
||||||
import { z } from 'zod/v4';
|
import z from 'zod/v4';
|
||||||
|
|
||||||
export const emptyToUndefined = <T>(validation: z.ZodType<T>) =>
|
export const emptyToUndefined = <T>(validation: z.ZodType<T>) =>
|
||||||
z.preprocess((v) => (v === '' ? undefined : v), validation.optional());
|
z.preprocess((v) => (v === '' ? undefined : v), validation);
|
||||||
|
|
||||||
export const optionalUrl = z
|
export const optionalUrl = z
|
||||||
.url()
|
.url()
|
||||||
@@ -26,11 +26,3 @@ export const callbackUrlSchema = z
|
|||||||
message: m.invalid_redirect_url()
|
message: m.invalid_redirect_url()
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
export const usernameSchema = z
|
|
||||||
.string()
|
|
||||||
.min(2)
|
|
||||||
.max(30)
|
|
||||||
.regex(/^[a-zA-Z0-9]/, m.username_must_start_with())
|
|
||||||
.regex(/[a-zA-Z0-9]$/, m.username_must_end_with())
|
|
||||||
.regex(/^[a-zA-Z0-9_.@-]+$/, m.username_can_only_contain());
|
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import AppConfigService from '$lib/services/app-config-service';
|
|||||||
import UserService from '$lib/services/user-service';
|
import UserService from '$lib/services/user-service';
|
||||||
import appConfigStore from '$lib/stores/application-configuration-store';
|
import appConfigStore from '$lib/stores/application-configuration-store';
|
||||||
import userStore from '$lib/stores/user-store';
|
import userStore from '$lib/stores/user-store';
|
||||||
import { setLocaleForLibraries } from '$lib/utils/locale.util';
|
|
||||||
import type { LayoutLoad } from './$types';
|
import type { LayoutLoad } from './$types';
|
||||||
|
|
||||||
export const ssr = false;
|
export const ssr = false;
|
||||||
@@ -30,8 +29,6 @@ export const load: LayoutLoad = async () => {
|
|||||||
appConfigStore.set(appConfig);
|
appConfigStore.set(appConfig);
|
||||||
}
|
}
|
||||||
|
|
||||||
await setLocaleForLibraries();
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
user,
|
user,
|
||||||
appConfig
|
appConfig
|
||||||
|
|||||||
@@ -8,7 +8,6 @@
|
|||||||
import { axiosErrorToast } from '$lib/utils/error-util';
|
import { axiosErrorToast } from '$lib/utils/error-util';
|
||||||
import { preventDefault } from '$lib/utils/event-util';
|
import { preventDefault } from '$lib/utils/event-util';
|
||||||
import { createForm } from '$lib/utils/form-util';
|
import { createForm } from '$lib/utils/form-util';
|
||||||
import { emptyToUndefined, usernameSchema } from '$lib/utils/zod-util';
|
|
||||||
import { toast } from 'svelte-sonner';
|
import { toast } from 'svelte-sonner';
|
||||||
import { z } from 'zod/v4';
|
import { z } from 'zod/v4';
|
||||||
|
|
||||||
@@ -27,15 +26,17 @@
|
|||||||
} = $props();
|
} = $props();
|
||||||
|
|
||||||
let isLoading = $state(false);
|
let isLoading = $state(false);
|
||||||
let hasManualDisplayNameEdit = $state(!!account.displayName);
|
|
||||||
|
|
||||||
const userService = new UserService();
|
const userService = new UserService();
|
||||||
|
|
||||||
const formSchema = z.object({
|
const formSchema = z.object({
|
||||||
firstName: z.string().min(1).max(50),
|
firstName: z.string().min(1).max(50),
|
||||||
lastName: emptyToUndefined(z.string().max(50).optional()),
|
lastName: z.string().max(50).optional(),
|
||||||
displayName: z.string().max(100),
|
username: z
|
||||||
username: usernameSchema,
|
.string()
|
||||||
|
.min(2)
|
||||||
|
.max(30)
|
||||||
|
.regex(/^[a-z0-9_@.-]+$/, m.username_can_only_contain()),
|
||||||
email: z.email(),
|
email: z.email(),
|
||||||
isAdmin: z.boolean()
|
isAdmin: z.boolean()
|
||||||
});
|
});
|
||||||
@@ -43,14 +44,6 @@
|
|||||||
|
|
||||||
const { inputs, ...form } = createForm<FormSchema>(formSchema, account);
|
const { inputs, ...form } = createForm<FormSchema>(formSchema, account);
|
||||||
|
|
||||||
function onNameInput() {
|
|
||||||
if (!hasManualDisplayNameEdit) {
|
|
||||||
$inputs.displayName.value = `${$inputs.firstName.value}${
|
|
||||||
$inputs.lastName?.value ? ' ' + $inputs.lastName.value : ''
|
|
||||||
}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function onSubmit() {
|
async function onSubmit() {
|
||||||
const data = form.validate();
|
const data = form.validate();
|
||||||
if (!data) return;
|
if (!data) return;
|
||||||
@@ -75,6 +68,7 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<form onsubmit={preventDefault(onSubmit)} class="space-y-6">
|
<form onsubmit={preventDefault(onSubmit)} class="space-y-6">
|
||||||
|
<!-- Profile Picture Section -->
|
||||||
<ProfilePictureSettings
|
<ProfilePictureSettings
|
||||||
{userId}
|
{userId}
|
||||||
{isLdapUser}
|
{isLdapUser}
|
||||||
@@ -82,32 +76,31 @@
|
|||||||
resetCallback={resetProfilePicture}
|
resetCallback={resetProfilePicture}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<!-- Divider -->
|
||||||
<hr class="border-border" />
|
<hr class="border-border" />
|
||||||
|
|
||||||
|
<!-- User Information -->
|
||||||
<fieldset disabled={userInfoInputDisabled}>
|
<fieldset disabled={userInfoInputDisabled}>
|
||||||
<div class="grid grid-cols-1 gap-3 sm:grid-cols-2">
|
<div>
|
||||||
<div>
|
<div class="flex flex-col gap-3 sm:flex-row">
|
||||||
<FormInput label={m.first_name()} bind:input={$inputs.firstName} onInput={onNameInput} />
|
<div class="w-full">
|
||||||
|
<FormInput label={m.first_name()} bind:input={$inputs.firstName} />
|
||||||
|
</div>
|
||||||
|
<div class="w-full">
|
||||||
|
<FormInput label={m.last_name()} bind:input={$inputs.lastName} />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div class="mt-3 flex flex-col gap-3 sm:flex-row">
|
||||||
<FormInput label={m.last_name()} bind:input={$inputs.lastName} onInput={onNameInput} />
|
<div class="w-full">
|
||||||
</div>
|
<FormInput label={m.email()} bind:input={$inputs.email} />
|
||||||
<div>
|
</div>
|
||||||
<FormInput
|
<div class="w-full">
|
||||||
label={m.display_name()}
|
<FormInput label={m.username()} bind:input={$inputs.username} />
|
||||||
bind:input={$inputs.displayName}
|
</div>
|
||||||
onInput={() => (hasManualDisplayNameEdit = true)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<FormInput label={m.username()} bind:input={$inputs.username} />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<FormInput label={m.email()} bind:input={$inputs.email} />
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex justify-end pt-4">
|
<div class="flex justify-end pt-2">
|
||||||
<Button {isLoading} type="submit">{m.save()}</Button>
|
<Button {isLoading} type="submit">{m.save()}</Button>
|
||||||
</div>
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|||||||
@@ -120,12 +120,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<CollapsibleCard
|
<CollapsibleCard id="application-configuration-images" icon={LucideImage} title={m.images()}>
|
||||||
id="application-configuration-images"
|
|
||||||
icon={LucideImage}
|
|
||||||
title={m.images()}
|
|
||||||
description={m.configure_application_images()}
|
|
||||||
>
|
|
||||||
<UpdateApplicationImages callback={updateImages} />
|
<UpdateApplicationImages callback={updateImages} />
|
||||||
</CollapsibleCard>
|
</CollapsibleCard>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -38,7 +38,6 @@
|
|||||||
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),
|
||||||
ldapAttributeUserDisplayName: z.string().min(1),
|
|
||||||
ldapAttributeUserProfilePicture: z.string(),
|
ldapAttributeUserProfilePicture: z.string(),
|
||||||
ldapAttributeGroupMember: z.string(),
|
ldapAttributeGroupMember: z.string(),
|
||||||
ldapAttributeGroupUniqueIdentifier: z.string().min(1),
|
ldapAttributeGroupUniqueIdentifier: z.string().min(1),
|
||||||
@@ -160,11 +159,6 @@
|
|||||||
placeholder="sn"
|
placeholder="sn"
|
||||||
bind:input={$inputs.ldapAttributeUserLastName}
|
bind:input={$inputs.ldapAttributeUserLastName}
|
||||||
/>
|
/>
|
||||||
<FormInput
|
|
||||||
label={m.display_name_attribute()}
|
|
||||||
placeholder="displayName"
|
|
||||||
bind:input={$inputs.ldapAttributeUserDisplayName}
|
|
||||||
/>
|
|
||||||
<FormInput
|
<FormInput
|
||||||
label={m.user_profile_picture_attribute()}
|
label={m.user_profile_picture_attribute()}
|
||||||
description={m.the_value_of_this_attribute_can_either_be_a_url_binary_or_base64_encoded_image()}
|
description={m.the_value_of_this_attribute_can_either_be_a_url_binary_or_base64_encoded_image()}
|
||||||
|
|||||||
@@ -1,13 +1,12 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import FormInput from '$lib/components/form/form-input.svelte';
|
|
||||||
import SwitchWithLabel from '$lib/components/form/switch-with-label.svelte';
|
import SwitchWithLabel from '$lib/components/form/switch-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 { m } from '$lib/paraglide/messages';
|
import { m } from '$lib/paraglide/messages';
|
||||||
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';
|
||||||
import { preventDefault } from '$lib/utils/event-util';
|
import { preventDefault } from '$lib/utils/event-util';
|
||||||
import { createForm } from '$lib/utils/form-util';
|
import { createForm } from '$lib/utils/form-util';
|
||||||
import { emptyToUndefined, usernameSchema } from '$lib/utils/zod-util';
|
|
||||||
import { z } from 'zod/v4';
|
import { z } from 'zod/v4';
|
||||||
|
|
||||||
let {
|
let {
|
||||||
@@ -20,12 +19,10 @@
|
|||||||
|
|
||||||
let isLoading = $state(false);
|
let isLoading = $state(false);
|
||||||
let inputDisabled = $derived(!!existingUser?.ldapId && $appConfigStore.ldapEnabled);
|
let inputDisabled = $derived(!!existingUser?.ldapId && $appConfigStore.ldapEnabled);
|
||||||
let hasManualDisplayNameEdit = $state(!!existingUser?.displayName);
|
|
||||||
|
|
||||||
const user = {
|
const user = {
|
||||||
firstName: existingUser?.firstName || '',
|
firstName: existingUser?.firstName || '',
|
||||||
lastName: existingUser?.lastName || '',
|
lastName: existingUser?.lastName || '',
|
||||||
displayName: existingUser?.displayName || '',
|
|
||||||
email: existingUser?.email || '',
|
email: existingUser?.email || '',
|
||||||
username: existingUser?.username || '',
|
username: existingUser?.username || '',
|
||||||
isAdmin: existingUser?.isAdmin || false,
|
isAdmin: existingUser?.isAdmin || false,
|
||||||
@@ -34,9 +31,12 @@
|
|||||||
|
|
||||||
const formSchema = z.object({
|
const formSchema = z.object({
|
||||||
firstName: z.string().min(1).max(50),
|
firstName: z.string().min(1).max(50),
|
||||||
lastName: emptyToUndefined(z.string().max(50).optional()),
|
lastName: z.string().max(50),
|
||||||
displayName: z.string().max(100),
|
username: z
|
||||||
username: usernameSchema,
|
.string()
|
||||||
|
.min(2)
|
||||||
|
.max(30)
|
||||||
|
.regex(/^[a-z0-9_@.-]+$/, m.username_can_only_contain()),
|
||||||
email: z.email(),
|
email: z.email(),
|
||||||
isAdmin: z.boolean(),
|
isAdmin: z.boolean(),
|
||||||
disabled: z.boolean()
|
disabled: z.boolean()
|
||||||
@@ -53,29 +53,15 @@
|
|||||||
if (success && !existingUser) form.reset();
|
if (success && !existingUser) form.reset();
|
||||||
isLoading = false;
|
isLoading = false;
|
||||||
}
|
}
|
||||||
function onNameInput() {
|
|
||||||
if (!hasManualDisplayNameEdit) {
|
|
||||||
$inputs.displayName.value = `${$inputs.firstName.value}${
|
|
||||||
$inputs.lastName?.value ? ' ' + $inputs.lastName.value : ''
|
|
||||||
}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<form onsubmit={preventDefault(onSubmit)}>
|
<form onsubmit={preventDefault(onSubmit)}>
|
||||||
<fieldset disabled={inputDisabled}>
|
<fieldset disabled={inputDisabled}>
|
||||||
<div class="grid grid-cols-1 items-start gap-5 md:grid-cols-2">
|
<div class="grid grid-cols-1 items-start gap-5 md:grid-cols-2">
|
||||||
<FormInput label={m.first_name()} oninput={onNameInput} bind:input={$inputs.firstName} />
|
<FormInput label={m.first_name()} bind:input={$inputs.firstName} />
|
||||||
<FormInput label={m.last_name()} oninput={onNameInput} bind:input={$inputs.lastName} />
|
<FormInput label={m.last_name()} bind:input={$inputs.lastName} />
|
||||||
<FormInput
|
|
||||||
label={m.display_name()}
|
|
||||||
oninput={() => (hasManualDisplayNameEdit = true)}
|
|
||||||
bind:input={$inputs.displayName}
|
|
||||||
/>
|
|
||||||
<FormInput label={m.username()} bind:input={$inputs.username} />
|
<FormInput label={m.username()} bind:input={$inputs.username} />
|
||||||
<FormInput label={m.email()} bind:input={$inputs.email} />
|
<FormInput label={m.email()} bind:input={$inputs.email} />
|
||||||
</div>
|
|
||||||
<div class="mt-5 grid grid-cols-1 items-start gap-5 md:grid-cols-2">
|
|
||||||
<SwitchWithLabel
|
<SwitchWithLabel
|
||||||
id="admin-privileges"
|
id="admin-privileges"
|
||||||
label={m.admin_privileges()}
|
label={m.admin_privileges()}
|
||||||
|
|||||||
@@ -103,7 +103,6 @@
|
|||||||
columns={[
|
columns={[
|
||||||
{ label: m.first_name(), sortColumn: 'firstName' },
|
{ label: m.first_name(), sortColumn: 'firstName' },
|
||||||
{ label: m.last_name(), sortColumn: 'lastName' },
|
{ label: m.last_name(), sortColumn: 'lastName' },
|
||||||
{ label: m.display_name(), sortColumn: 'displayName' },
|
|
||||||
{ label: m.email(), sortColumn: 'email' },
|
{ label: m.email(), sortColumn: 'email' },
|
||||||
{ label: m.username(), sortColumn: 'username' },
|
{ label: m.username(), sortColumn: 'username' },
|
||||||
{ label: m.role(), sortColumn: 'isAdmin' },
|
{ label: m.role(), sortColumn: 'isAdmin' },
|
||||||
@@ -115,7 +114,6 @@
|
|||||||
{#snippet rows({ item })}
|
{#snippet rows({ item })}
|
||||||
<Table.Cell>{item.firstName}</Table.Cell>
|
<Table.Cell>{item.firstName}</Table.Cell>
|
||||||
<Table.Cell>{item.lastName}</Table.Cell>
|
<Table.Cell>{item.lastName}</Table.Cell>
|
||||||
<Table.Cell>{item.displayName}</Table.Cell>
|
|
||||||
<Table.Cell>{item.email}</Table.Cell>
|
<Table.Cell>{item.email}</Table.Cell>
|
||||||
<Table.Cell>{item.username}</Table.Cell>
|
<Table.Cell>{item.username}</Table.Cell>
|
||||||
<Table.Cell>
|
<Table.Cell>
|
||||||
|
|||||||
@@ -31,7 +31,7 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Card.Root
|
<Card.Root
|
||||||
class="border-muted group relative h-[140px] p-5 transition-all duration-200 hover:shadow-md sm:max-w-[50vw] md:max-w-[400px]"
|
class="border-muted group relative h-[140px] p-5 transition-all duration-200 hover:shadow-md"
|
||||||
data-testid="authorized-oidc-client-card"
|
data-testid="authorized-oidc-client-card"
|
||||||
>
|
>
|
||||||
<Card.Content class=" p-0">
|
<Card.Content class=" p-0">
|
||||||
@@ -49,14 +49,14 @@
|
|||||||
<div>
|
<div>
|
||||||
<div class="mb-1 flex items-start gap-2">
|
<div class="mb-1 flex items-start gap-2">
|
||||||
<h3
|
<h3
|
||||||
class="text-foreground line-clamp-2 leading-tight font-semibold break-words break-all text-ellipsis"
|
class="text-foreground line-clamp-2 text-ellipsis break-words break-all font-semibold leading-tight"
|
||||||
>
|
>
|
||||||
{client.name}
|
{client.name}
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
{#if client.launchURL}
|
{#if client.launchURL}
|
||||||
<p
|
<p
|
||||||
class="text-muted-foreground line-clamp-1 text-xs break-words break-all text-ellipsis"
|
class="text-muted-foreground line-clamp-1 text-ellipsis break-words break-all text-xs"
|
||||||
>
|
>
|
||||||
{new URL(client.launchURL).hostname}
|
{new URL(client.launchURL).hostname}
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -113,7 +113,7 @@ git add frontend/package.json
|
|||||||
|
|
||||||
# Generate changelog
|
# Generate changelog
|
||||||
echo "Generating changelog..."
|
echo "Generating changelog..."
|
||||||
conventional-changelog -p conventionalcommits -i CHANGELOG.md -s --pkg frontend/package.json
|
conventional-changelog -p conventionalcommits -i CHANGELOG.md -s
|
||||||
git add CHANGELOG.md
|
git add CHANGELOG.md
|
||||||
|
|
||||||
# Commit the changes with the new version
|
# Commit the changes with the new version
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ export const users = {
|
|||||||
id: 'f4b89dc2-62fb-46bf-9f5f-c34f4eafe93e',
|
id: 'f4b89dc2-62fb-46bf-9f5f-c34f4eafe93e',
|
||||||
firstname: 'Tim',
|
firstname: 'Tim',
|
||||||
lastname: 'Cook',
|
lastname: 'Cook',
|
||||||
displayName: 'Tim Cook',
|
|
||||||
email: 'tim.cook@test.com',
|
email: 'tim.cook@test.com',
|
||||||
username: 'tim'
|
username: 'tim'
|
||||||
},
|
},
|
||||||
@@ -11,14 +10,12 @@ export const users = {
|
|||||||
id: '1cd19686-f9a6-43f4-a41f-14a0bf5b4036',
|
id: '1cd19686-f9a6-43f4-a41f-14a0bf5b4036',
|
||||||
firstname: 'Craig',
|
firstname: 'Craig',
|
||||||
lastname: 'Federighi',
|
lastname: 'Federighi',
|
||||||
displayName: 'Craig Federighi',
|
|
||||||
email: 'craig.federighi@test.com',
|
email: 'craig.federighi@test.com',
|
||||||
username: 'craig'
|
username: 'craig'
|
||||||
},
|
},
|
||||||
steve: {
|
steve: {
|
||||||
firstname: 'Steve',
|
firstname: 'Steve',
|
||||||
lastname: 'Jobs',
|
lastname: 'Jobs',
|
||||||
displayName: 'Steve Jobs',
|
|
||||||
email: 'steve.jobs@test.com',
|
email: 'steve.jobs@test.com',
|
||||||
username: 'steve'
|
username: 'steve'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,10 +9,8 @@ test.beforeEach(async () => await cleanupBackend());
|
|||||||
test('Update account details', async ({ page }) => {
|
test('Update account details', async ({ page }) => {
|
||||||
await page.goto('/settings/account');
|
await page.goto('/settings/account');
|
||||||
|
|
||||||
await page.getByLabel('Display Name').fill('Tim Apple');
|
|
||||||
await page.getByLabel('First name').fill('Timothy');
|
await page.getByLabel('First name').fill('Timothy');
|
||||||
await page.getByLabel('Last name').fill('Apple');
|
await page.getByLabel('Last name').fill('Apple');
|
||||||
await page.getByLabel('Display Name').fill('Timothy Apple');
|
|
||||||
await page.getByLabel('Email').fill('timothy.apple@test.com');
|
await page.getByLabel('Email').fill('timothy.apple@test.com');
|
||||||
await page.getByLabel('Username').fill('timothy');
|
await page.getByLabel('Username').fill('timothy');
|
||||||
await page.getByRole('button', { name: 'Save' }).click();
|
await page.getByRole('button', { name: 'Save' }).click();
|
||||||
@@ -42,18 +40,6 @@ test('Update account details fails with already taken username', async ({ page }
|
|||||||
await expect(page.locator('[data-type="error"]')).toHaveText('Username is already in use');
|
await expect(page.locator('[data-type="error"]')).toHaveText('Username is already in use');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Update account details fails with already taken username in different casing', async ({
|
|
||||||
page
|
|
||||||
}) => {
|
|
||||||
await page.goto('/settings/account');
|
|
||||||
|
|
||||||
await page.getByLabel('Username').fill(users.craig.username.toUpperCase());
|
|
||||||
|
|
||||||
await page.getByRole('button', { name: 'Save' }).click();
|
|
||||||
|
|
||||||
await expect(page.locator('[data-type="error"]')).toHaveText('Username is already in use');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Change Locale', async ({ page }) => {
|
test('Change Locale', async ({ page }) => {
|
||||||
await page.goto('/settings/account');
|
await page.goto('/settings/account');
|
||||||
|
|
||||||
|
|||||||
@@ -14,9 +14,6 @@ test('Create user', async ({ page }) => {
|
|||||||
await page.getByLabel('Last name').fill(user.lastname);
|
await page.getByLabel('Last name').fill(user.lastname);
|
||||||
await page.getByLabel('Email').fill(user.email);
|
await page.getByLabel('Email').fill(user.email);
|
||||||
await page.getByLabel('Username').fill(user.username);
|
await page.getByLabel('Username').fill(user.username);
|
||||||
|
|
||||||
await expect(page.getByLabel('Display Name')).toHaveValue(`${user.firstname} ${user.lastname}`);
|
|
||||||
|
|
||||||
await page.getByRole('button', { name: 'Save' }).click();
|
await page.getByRole('button', { name: 'Save' }).click();
|
||||||
|
|
||||||
await expect(page.getByRole('row', { name: `${user.firstname} ${user.lastname}` })).toBeVisible();
|
await expect(page.getByRole('row', { name: `${user.firstname} ${user.lastname}` })).toBeVisible();
|
||||||
@@ -53,21 +50,6 @@ test('Create user fails with already taken username', async ({ page }) => {
|
|||||||
await expect(page.locator('[data-type="error"]')).toHaveText('Username is already in use');
|
await expect(page.locator('[data-type="error"]')).toHaveText('Username is already in use');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Create user fails with already taken username in different casing', async ({ page }) => {
|
|
||||||
const user = users.steve;
|
|
||||||
|
|
||||||
await page.goto('/settings/admin/users');
|
|
||||||
|
|
||||||
await page.getByRole('button', { name: 'Add User' }).click();
|
|
||||||
await page.getByLabel('First name').fill(user.firstname);
|
|
||||||
await page.getByLabel('Last name').fill(user.lastname);
|
|
||||||
await page.getByLabel('Email').fill(user.email);
|
|
||||||
await page.getByLabel('Username').fill(users.tim.username.toUpperCase());
|
|
||||||
await page.getByRole('button', { name: 'Save' }).click();
|
|
||||||
|
|
||||||
await expect(page.locator('[data-type="error"]')).toHaveText('Username is already in use');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Create one time access token', async ({ page, context }) => {
|
test('Create one time access token', async ({ page, context }) => {
|
||||||
await page.goto('/settings/admin/users');
|
await page.goto('/settings/admin/users');
|
||||||
|
|
||||||
@@ -124,7 +106,6 @@ test('Update user', async ({ page }) => {
|
|||||||
|
|
||||||
await page.getByLabel('First name').fill('Crack');
|
await page.getByLabel('First name').fill('Crack');
|
||||||
await page.getByLabel('Last name').fill('Apple');
|
await page.getByLabel('Last name').fill('Apple');
|
||||||
await page.getByLabel('Display Name').fill('Crack Apple');
|
|
||||||
await page.getByLabel('Email').fill('crack.apple@test.com');
|
await page.getByLabel('Email').fill('crack.apple@test.com');
|
||||||
await page.getByLabel('Username').fill('crack');
|
await page.getByLabel('Username').fill('crack');
|
||||||
await page.getByRole('button', { name: 'Save' }).first().click();
|
await page.getByRole('button', { name: 'Save' }).first().click();
|
||||||
@@ -166,23 +147,6 @@ test('Update user fails with already taken username', async ({ page }) => {
|
|||||||
await expect(page.locator('[data-type="error"]')).toHaveText('Username is already in use');
|
await expect(page.locator('[data-type="error"]')).toHaveText('Username is already in use');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Update user fails with already taken username in different casing', async ({ page }) => {
|
|
||||||
const user = users.craig;
|
|
||||||
|
|
||||||
await page.goto('/settings/admin/users');
|
|
||||||
|
|
||||||
await page
|
|
||||||
.getByRole('row', { name: `${user.firstname} ${user.lastname}` })
|
|
||||||
.getByRole('button')
|
|
||||||
.click();
|
|
||||||
await page.getByRole('menuitem', { name: 'Edit' }).click();
|
|
||||||
|
|
||||||
await page.getByLabel('Username').fill(users.tim.username.toUpperCase());
|
|
||||||
await page.getByRole('button', { name: 'Save' }).first().click();
|
|
||||||
|
|
||||||
await expect(page.locator('[data-type="error"]')).toHaveText('Username is already in use');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Update user custom claims', async ({ page }) => {
|
test('Update user custom claims', async ({ page }) => {
|
||||||
await page.goto(`/settings/admin/users/${users.craig.id}`);
|
await page.goto(`/settings/admin/users/${users.craig.id}`);
|
||||||
|
|
||||||
|
|||||||
@@ -124,7 +124,7 @@ test.describe('User Signup', () => {
|
|||||||
|
|
||||||
await page.getByRole('button', { name: 'Sign Up' }).click();
|
await page.getByRole('button', { name: 'Sign Up' }).click();
|
||||||
|
|
||||||
await expect(page.getByText('Invalid email address').first()).toBeVisible();
|
await expect(page.getByText('Invalid input').first()).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Open signup - duplicate email shows error', async ({ page }) => {
|
test('Open signup - duplicate email shows error', async ({ page }) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user