Updates
This commit is contained in:
parent
796066ce42
commit
7cd513457c
@ -51,3 +51,6 @@ AUTO_MIGRATE=true
|
|||||||
# hCaptcha keys, leave empty if not using it
|
# hCaptcha keys, leave empty if not using it
|
||||||
PUBLIC_HCAPTCHA_KEY=
|
PUBLIC_HCAPTCHA_KEY=
|
||||||
HCAPTCHA_SECRET=
|
HCAPTCHA_SECRET=
|
||||||
|
|
||||||
|
# Use Valkey for cache, leave empty to use in-memory cache
|
||||||
|
VALKEY_URI=
|
213
package-lock.json
generated
213
package-lock.json
generated
@ -8,14 +8,17 @@
|
|||||||
"name": "icysso",
|
"name": "icysso",
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@keyv/valkey": "^1.0.3",
|
||||||
"@sveltejs/adapter-node": "^5.2.12",
|
"@sveltejs/adapter-node": "^5.2.12",
|
||||||
"bcryptjs": "^3.0.2",
|
"bcryptjs": "^3.0.2",
|
||||||
|
"cache-manager": "^6.4.0",
|
||||||
|
"cacheable": "^1.8.8",
|
||||||
"chalk": "^5.4.1",
|
"chalk": "^5.4.1",
|
||||||
"cropperjs": "^1.6.2",
|
"cropperjs": "^1.6.2",
|
||||||
"dotenv": "^16.4.7",
|
"dotenv": "^16.4.7",
|
||||||
"drizzle-orm": "^0.39.3",
|
"drizzle-orm": "^0.39.3",
|
||||||
"image-size": "^1.2.0",
|
"image-size": "^1.2.0",
|
||||||
"jose": "^5.10.0",
|
"jose": "^6.0.4",
|
||||||
"mime-types": "^2.1.35",
|
"mime-types": "^2.1.35",
|
||||||
"mysql2": "^3.12.0",
|
"mysql2": "^3.12.0",
|
||||||
"nodemailer": "^6.10.0",
|
"nodemailer": "^6.10.0",
|
||||||
@ -1135,6 +1138,12 @@
|
|||||||
"url": "https://github.com/sponsors/nzakas"
|
"url": "https://github.com/sponsors/nzakas"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@iovalkey/commands": {
|
||||||
|
"version": "0.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@iovalkey/commands/-/commands-0.1.0.tgz",
|
||||||
|
"integrity": "sha512-/B9W4qKSSITDii5nkBCHyPkIkAi+ealUtr1oqBJsLxjSRLka4pxun2VvMNSmcwgAMxgXtQfl0qRv7TE+udPJzg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@isaacs/ttlcache": {
|
"node_modules/@isaacs/ttlcache": {
|
||||||
"version": "1.4.1",
|
"version": "1.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/@isaacs/ttlcache/-/ttlcache-1.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/@isaacs/ttlcache/-/ttlcache-1.4.1.tgz",
|
||||||
@ -1187,6 +1196,27 @@
|
|||||||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
"@jridgewell/sourcemap-codec": "^1.4.14"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@keyv/serialize": {
|
||||||
|
"version": "1.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@keyv/serialize/-/serialize-1.0.3.tgz",
|
||||||
|
"integrity": "sha512-qnEovoOp5Np2JDGonIDL6Ayihw0RhnRh6vxPuHo4RDn1UOzwEo4AeIfpL6UGIrsceWrCMiVPgwRjbHu4vYFc3g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"buffer": "^6.0.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@keyv/valkey": {
|
||||||
|
"version": "1.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@keyv/valkey/-/valkey-1.0.3.tgz",
|
||||||
|
"integrity": "sha512-QCC6OzI3K1PjAvcj9n5CB+hy8M+hl8PxLkJQWs0DAOP+4XXuJPPRdQqf/jPm7zunrZl5+G+JL8WGsv4Kq0TeoQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"iovalkey": "^0.3.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 18"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@noble/ciphers": {
|
"node_modules/@noble/ciphers": {
|
||||||
"version": "0.2.0",
|
"version": "0.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-0.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-0.2.0.tgz",
|
||||||
@ -2305,6 +2335,26 @@
|
|||||||
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
|
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/base64-js": {
|
||||||
|
"version": "1.5.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
|
||||||
|
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "patreon",
|
||||||
|
"url": "https://www.patreon.com/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "consulting",
|
||||||
|
"url": "https://feross.org/support"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/bcryptjs": {
|
"node_modules/bcryptjs": {
|
||||||
"version": "3.0.2",
|
"version": "3.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-3.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-3.0.2.tgz",
|
||||||
@ -2343,12 +2393,73 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/buffer": {
|
||||||
|
"version": "6.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
|
||||||
|
"integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "patreon",
|
||||||
|
"url": "https://www.patreon.com/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "consulting",
|
||||||
|
"url": "https://feross.org/support"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"base64-js": "^1.3.1",
|
||||||
|
"ieee754": "^1.2.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/buffer-from": {
|
"node_modules/buffer-from": {
|
||||||
"version": "1.1.2",
|
"version": "1.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
|
||||||
"integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
|
"integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/cache-manager": {
|
||||||
|
"version": "6.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/cache-manager/-/cache-manager-6.4.0.tgz",
|
||||||
|
"integrity": "sha512-eUmPyVqQYzWCt7hx1QrYzQ7oC3MGKM1etxxe8zuq1o7IB4NzdBeWcUGDSWYahaI8fkd538SEZRGadyZWQfvOzQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"keyv": "^5.2.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/cache-manager/node_modules/keyv": {
|
||||||
|
"version": "5.2.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/keyv/-/keyv-5.2.3.tgz",
|
||||||
|
"integrity": "sha512-AGKecUfzrowabUv0bH1RIR5Vf7w+l4S3xtQAypKaUpTdIR1EbrAcTxHCrpo9Q+IWeUlFE2palRtgIQcgm+PQJw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@keyv/serialize": "^1.0.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/cacheable": {
|
||||||
|
"version": "1.8.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/cacheable/-/cacheable-1.8.8.tgz",
|
||||||
|
"integrity": "sha512-OE1/jlarWxROUIpd0qGBSKFLkNsotY8pt4GeiVErUYh/NUeTNrT+SBksUgllQv4m6a0W/VZsLuiHb88maavqEw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"hookified": "^1.7.0",
|
||||||
|
"keyv": "^5.2.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/cacheable/node_modules/keyv": {
|
||||||
|
"version": "5.2.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/keyv/-/keyv-5.2.3.tgz",
|
||||||
|
"integrity": "sha512-AGKecUfzrowabUv0bH1RIR5Vf7w+l4S3xtQAypKaUpTdIR1EbrAcTxHCrpo9Q+IWeUlFE2palRtgIQcgm+PQJw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@keyv/serialize": "^1.0.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/callsites": {
|
"node_modules/callsites": {
|
||||||
"version": "3.1.0",
|
"version": "3.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
|
||||||
@ -2414,6 +2525,15 @@
|
|||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/cluster-key-slot": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz",
|
||||||
|
"integrity": "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/color-convert": {
|
"node_modules/color-convert": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||||
@ -3767,6 +3887,12 @@
|
|||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/hookified": {
|
||||||
|
"version": "1.7.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/hookified/-/hookified-1.7.1.tgz",
|
||||||
|
"integrity": "sha512-OXcdHsXeOiD7OJ5zvWj8Oy/6RCdLwntAX+wUrfemNcMGn6sux4xbEHi2QXwqePYhjQ/yvxxq2MvCRirdlHscBw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/iconv-lite": {
|
"node_modules/iconv-lite": {
|
||||||
"version": "0.6.3",
|
"version": "0.6.3",
|
||||||
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
|
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
|
||||||
@ -3778,6 +3904,26 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/ieee754": {
|
||||||
|
"version": "1.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
|
||||||
|
"integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "patreon",
|
||||||
|
"url": "https://www.patreon.com/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "consulting",
|
||||||
|
"url": "https://feross.org/support"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "BSD-3-Clause"
|
||||||
|
},
|
||||||
"node_modules/ignore": {
|
"node_modules/ignore": {
|
||||||
"version": "5.3.1",
|
"version": "5.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz",
|
||||||
@ -3842,6 +3988,26 @@
|
|||||||
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
|
||||||
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
|
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
|
||||||
},
|
},
|
||||||
|
"node_modules/iovalkey": {
|
||||||
|
"version": "0.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/iovalkey/-/iovalkey-0.3.1.tgz",
|
||||||
|
"integrity": "sha512-pSmFj/ZDFLP8AzqIAMpNJArhHYNeyqIwfcUULwZv8g6y3eaUGrlnlT7QXLrJAp0yiGCAqe1hPA33x/h5opNP9w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@iovalkey/commands": "^0.1.0",
|
||||||
|
"cluster-key-slot": "^1.1.0",
|
||||||
|
"debug": "^4.3.4",
|
||||||
|
"denque": "^2.1.0",
|
||||||
|
"lodash.defaults": "^4.2.0",
|
||||||
|
"lodash.isarguments": "^3.1.0",
|
||||||
|
"redis-errors": "^1.2.0",
|
||||||
|
"redis-parser": "^3.0.0",
|
||||||
|
"standard-as-callback": "^2.1.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18.12.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/is-core-module": {
|
"node_modules/is-core-module": {
|
||||||
"version": "2.16.1",
|
"version": "2.16.1",
|
||||||
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz",
|
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz",
|
||||||
@ -3924,9 +4090,9 @@
|
|||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
"node_modules/jose": {
|
"node_modules/jose": {
|
||||||
"version": "5.10.0",
|
"version": "6.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/jose/-/jose-5.10.0.tgz",
|
"resolved": "https://registry.npmjs.org/jose/-/jose-6.0.4.tgz",
|
||||||
"integrity": "sha512-s+3Al/p9g32Iq+oqXxkW//7jk2Vig6FF1CFqzVXoTUXt2qz89YWbL+OwS17NFYEvxC35n0FKeGO2LGYSxeM2Gg==",
|
"integrity": "sha512-GTWKmlIq0xs50pnwa7q+8eG0ZLR1DVcHxa75s2VArJcriBd+YEC18uK/kbTObF3gMXgqBeZXAoZA9eMLhPjSpQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/panva"
|
"url": "https://github.com/sponsors/panva"
|
||||||
@ -4032,6 +4198,18 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/lodash.defaults": {
|
||||||
|
"version": "4.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz",
|
||||||
|
"integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/lodash.isarguments": {
|
||||||
|
"version": "3.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz",
|
||||||
|
"integrity": "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/lodash.merge": {
|
"node_modules/lodash.merge": {
|
||||||
"version": "4.6.2",
|
"version": "4.6.2",
|
||||||
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
|
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
|
||||||
@ -4592,6 +4770,27 @@
|
|||||||
"url": "https://paulmillr.com/funding/"
|
"url": "https://paulmillr.com/funding/"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/redis-errors": {
|
||||||
|
"version": "1.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz",
|
||||||
|
"integrity": "sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/redis-parser": {
|
||||||
|
"version": "3.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-3.0.0.tgz",
|
||||||
|
"integrity": "sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"redis-errors": "^1.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/require-directory": {
|
"node_modules/require-directory": {
|
||||||
"version": "2.1.1",
|
"version": "2.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
|
||||||
@ -4833,6 +5032,12 @@
|
|||||||
"node": ">= 0.6"
|
"node": ">= 0.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/standard-as-callback": {
|
||||||
|
"version": "2.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.1.0.tgz",
|
||||||
|
"integrity": "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/string-width": {
|
"node_modules/string-width": {
|
||||||
"version": "4.2.3",
|
"version": "4.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
|
||||||
|
@ -37,14 +37,17 @@
|
|||||||
},
|
},
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@keyv/valkey": "^1.0.3",
|
||||||
"@sveltejs/adapter-node": "^5.2.12",
|
"@sveltejs/adapter-node": "^5.2.12",
|
||||||
"bcryptjs": "^3.0.2",
|
"bcryptjs": "^3.0.2",
|
||||||
|
"cache-manager": "^6.4.0",
|
||||||
|
"cacheable": "^1.8.8",
|
||||||
"chalk": "^5.4.1",
|
"chalk": "^5.4.1",
|
||||||
"cropperjs": "^1.6.2",
|
"cropperjs": "^1.6.2",
|
||||||
"dotenv": "^16.4.7",
|
"dotenv": "^16.4.7",
|
||||||
"drizzle-orm": "^0.39.3",
|
"drizzle-orm": "^0.39.3",
|
||||||
"image-size": "^1.2.0",
|
"image-size": "^1.2.0",
|
||||||
"jose": "^5.10.0",
|
"jose": "^6.0.4",
|
||||||
"mime-types": "^2.1.35",
|
"mime-types": "^2.1.35",
|
||||||
"mysql2": "^3.12.0",
|
"mysql2": "^3.12.0",
|
||||||
"nodemailer": "^6.10.0",
|
"nodemailer": "^6.10.0",
|
||||||
|
@ -6,7 +6,9 @@
|
|||||||
--in-background-image: none;
|
--in-background-image: none;
|
||||||
--in-normalized-background: #cceaff;
|
--in-normalized-background: #cceaff;
|
||||||
--in-container-background: #f1faff;
|
--in-container-background: #f1faff;
|
||||||
|
--in-inner-container-background: #b7e5ff;
|
||||||
--in-container-shadow: 0px 0px 16px 0px rgba(0, 0, 0, 0.15);
|
--in-container-shadow: 0px 0px 16px 0px rgba(0, 0, 0, 0.15);
|
||||||
|
--in-alert-shadow: 0px 5px 10px 0px rgba(0, 0, 0, 0.45);
|
||||||
|
|
||||||
--in-text-color: #000000;
|
--in-text-color: #000000;
|
||||||
--in-link-color: #000000;
|
--in-link-color: #000000;
|
||||||
@ -71,6 +73,7 @@ html[theme-base='dark'] {
|
|||||||
--in-background-image: url('/background.webp');
|
--in-background-image: url('/background.webp');
|
||||||
--in-normalized-background: #000;
|
--in-normalized-background: #000;
|
||||||
--in-container-background: transparent;
|
--in-container-background: transparent;
|
||||||
|
--in-inner-container-background: #ffffff15;
|
||||||
|
|
||||||
--in-text-color: #fff;
|
--in-text-color: #fff;
|
||||||
--in-link-color: #fff;
|
--in-link-color: #fff;
|
||||||
|
@ -1,7 +1,9 @@
|
|||||||
import { env } from '$env/dynamic/private';
|
import { env } from '$env/dynamic/private';
|
||||||
|
import { CacheBackend } from '$lib/server/cache-backend';
|
||||||
import { csrf } from '$lib/server/csrf';
|
import { csrf } from '$lib/server/csrf';
|
||||||
import { DB } from '$lib/server/drizzle';
|
import { DB } from '$lib/server/drizzle';
|
||||||
import { runSeeds } from '$lib/server/drizzle/seeds';
|
import { runSeeds } from '$lib/server/drizzle/seeds';
|
||||||
|
import { FileBackend } from '$lib/server/file-backend';
|
||||||
import { JWT } from '$lib/server/jwt';
|
import { JWT } from '$lib/server/jwt';
|
||||||
import { Logger, LogLevel } from '$lib/server/logger';
|
import { Logger, LogLevel } from '$lib/server/logger';
|
||||||
import type { ThemeModeType } from '$lib/theme-mode';
|
import type { ThemeModeType } from '$lib/theme-mode';
|
||||||
@ -12,6 +14,9 @@ import { handleSession } from 'svelte-kit-cookie-session';
|
|||||||
|
|
||||||
const { AUTO_MIGRATE, SESSION_SECRET, SESSION_SECURE, NODE_ENV } = env;
|
const { AUTO_MIGRATE, SESSION_SECRET, SESSION_SECURE, NODE_ENV } = env;
|
||||||
|
|
||||||
|
await FileBackend.init();
|
||||||
|
await CacheBackend.init();
|
||||||
|
|
||||||
// Set logger to debug mode
|
// Set logger to debug mode
|
||||||
if (NODE_ENV === 'development') {
|
if (NODE_ENV === 'development') {
|
||||||
Logger.setLogLevel(LogLevel.DEBUG);
|
Logger.setLogLevel(LogLevel.DEBUG);
|
||||||
|
@ -1,21 +1,19 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { createEventDispatcher } from 'svelte';
|
|
||||||
import Button from './Button.svelte';
|
import Button from './Button.svelte';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
type?: 'default' | 'error' | 'success';
|
type?: 'default' | 'error' | 'success';
|
||||||
dismissable?: boolean;
|
dismissable?: boolean;
|
||||||
children?: import('svelte').Snippet;
|
children?: import('svelte').Snippet;
|
||||||
|
onDismiss?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
let { type = 'default', dismissable = false, children }: Props = $props();
|
let { type = 'default', dismissable = false, children, onDismiss }: Props = $props();
|
||||||
|
|
||||||
const dispatch = createEventDispatcher();
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="alert alert-{type}" role="alert">
|
<div class="alert alert-{type}" role="alert">
|
||||||
<p>{@render children?.()}</p>
|
<p>{@render children?.()}</p>
|
||||||
{#if dismissable}<Button variant="link" onclick={() => dispatch('dismiss')}>x</Button>{/if}
|
{#if dismissable}<Button variant="link" onclick={() => onDismiss?.()}>x</Button>{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
122
src/lib/components/Messages.svelte
Normal file
122
src/lib/components/Messages.svelte
Normal file
@ -0,0 +1,122 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { t } from '$lib/i18n';
|
||||||
|
import { dismissMessage, messages } from '$lib/stores/messages.store';
|
||||||
|
import Icon from './icons/Icon.svelte';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="messages-wrapper" aria-live="assertive">
|
||||||
|
{#each $messages as msg}
|
||||||
|
<div class="appear message message--{msg.type}" role="alertdialog">
|
||||||
|
<div class="message-title">
|
||||||
|
<span>{$t(`common.${msg.type}`)}!</span>
|
||||||
|
<button onclick={() => dismissMessage(msg)} class="message-close">
|
||||||
|
<Icon icon="Close" />
|
||||||
|
<span class="visually-hidden">{$t('common.close')}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<span>{msg.text}</span>
|
||||||
|
{#if msg.actions}
|
||||||
|
<div class="message-actions">
|
||||||
|
{#each msg.actions as action}
|
||||||
|
<button onclick={action.action}>{action.label}</button>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.messages-wrapper {
|
||||||
|
position: fixed;
|
||||||
|
z-index: 30;
|
||||||
|
left: auto;
|
||||||
|
width: 100%;
|
||||||
|
right: auto;
|
||||||
|
bottom: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 16px;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message {
|
||||||
|
pointer-events: all;
|
||||||
|
background-color: var(--in-alert-color);
|
||||||
|
box-shadow: var(--in-alert-shadow);
|
||||||
|
padding: 16px;
|
||||||
|
border-radius: 8px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
max-width: 28rem;
|
||||||
|
margin: auto;
|
||||||
|
|
||||||
|
& > span {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-title {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 8px;
|
||||||
|
|
||||||
|
& > span {
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-close,
|
||||||
|
.message-actions > button {
|
||||||
|
appearance: none;
|
||||||
|
border: 0;
|
||||||
|
padding: 0;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--in-link-color);
|
||||||
|
font-size: 1rem;
|
||||||
|
width: 2rem;
|
||||||
|
height: 2rem;
|
||||||
|
margin-top: -0.5rem;
|
||||||
|
margin-right: -0.5rem;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:focus-visible {
|
||||||
|
outline: var(--in-focus-outline);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-actions > button {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-actions {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message--error {
|
||||||
|
background-color: var(--in-error-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.message--success {
|
||||||
|
background-color: var(--in-success-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.appear {
|
||||||
|
animation: popup 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
|
||||||
|
}
|
||||||
|
@keyframes popup {
|
||||||
|
from {
|
||||||
|
transform: scale(0.6);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
@ -85,7 +85,7 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
padding: 2rem 2rem 1rem 2rem;
|
padding: 1.5rem 2rem;
|
||||||
border-bottom: 1px solid var(--in-modal-divider-color);
|
border-bottom: 1px solid var(--in-modal-divider-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -93,7 +93,7 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
padding: 1rem 2rem 2rem 2rem;
|
padding: 1.5rem 2rem;
|
||||||
border-top: 1px solid var(--in-modal-divider-color);
|
border-top: 1px solid var(--in-modal-divider-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,16 +1,28 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { env } from '$env/dynamic/public';
|
import { env } from '$env/dynamic/public';
|
||||||
|
import { t } from '$lib/i18n';
|
||||||
import type { UserSession } from '$lib/types';
|
import type { UserSession } from '$lib/types';
|
||||||
|
import Button from '../Button.svelte';
|
||||||
|
import Icon from '../icons/Icon.svelte';
|
||||||
import ThemeButton from '../ThemeButton.svelte';
|
import ThemeButton from '../ThemeButton.svelte';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
user: UserSession;
|
user: UserSession;
|
||||||
|
mobileOpen?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
let { user }: Props = $props();
|
let { user, mobileOpen = $bindable() }: Props = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<header class="admin-header">
|
<header class="admin-header">
|
||||||
<a class="site-name" href="/">{env.PUBLIC_SITE_NAME}</a>
|
<div class="menu-toggle-wrapper">
|
||||||
|
<Button variant="link" onclick={() => (mobileOpen = !mobileOpen)}>
|
||||||
|
<Icon icon={mobileOpen ? 'Close' : 'Menu'} />
|
||||||
|
<span class="visually-hidden">{$t('admin.menu.' + (mobileOpen ? 'close' : 'open'))}</span>
|
||||||
|
</Button>
|
||||||
|
<a class="site-name" href="/">{env.PUBLIC_SITE_NAME}</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="aside">
|
<div class="aside">
|
||||||
<div class="admin-user">
|
<div class="admin-user">
|
||||||
<img class="admin-user-avatar" src={`/api/avatar/${user.uuid}`} alt={user.name} />
|
<img class="admin-user-avatar" src={`/api/avatar/${user.uuid}`} alt={user.name} />
|
||||||
@ -33,10 +45,27 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
.admin-header .aside :global(button) {
|
& :global(button) {
|
||||||
color: var(--ina-header-link-color);
|
color: var(--ina-header-link-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-toggle-wrapper {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
|
||||||
|
& > :global(button) {
|
||||||
|
width: 2rem;
|
||||||
|
height: 2rem;
|
||||||
|
flex-shrink: 0;
|
||||||
|
margin-left: -8px;
|
||||||
|
|
||||||
|
@media screen and (min-width: 769px) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.admin-user {
|
.admin-user {
|
||||||
@ -54,6 +83,14 @@
|
|||||||
border-radius: 100%;
|
border-radius: 100%;
|
||||||
object-fit: contain;
|
object-fit: contain;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 768px) {
|
||||||
|
padding: 4px;
|
||||||
|
|
||||||
|
& .admin-user-name {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
a {
|
a {
|
||||||
|
@ -3,12 +3,14 @@
|
|||||||
import { hasPrivileges } from '$lib/utils';
|
import { hasPrivileges } from '$lib/utils';
|
||||||
import { page } from '$app/state';
|
import { page } from '$app/state';
|
||||||
import { t } from '$lib/i18n';
|
import { t } from '$lib/i18n';
|
||||||
|
import { onNavigate } from '$app/navigation';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
user: UserSession;
|
user: UserSession;
|
||||||
|
mobileOpen?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
let { user }: Props = $props();
|
let { user, mobileOpen = $bindable() }: Props = $props();
|
||||||
|
|
||||||
const links = [
|
const links = [
|
||||||
{
|
{
|
||||||
@ -31,9 +33,13 @@
|
|||||||
let entries = $derived(
|
let entries = $derived(
|
||||||
links.filter((link) => hasPrivileges(user.privileges || [], link.privileges))
|
links.filter((link) => hasPrivileges(user.privileges || [], link.privileges))
|
||||||
);
|
);
|
||||||
|
|
||||||
|
onNavigate(() => {
|
||||||
|
mobileOpen = false;
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<aside class="admin-sidebar">
|
<aside class="admin-sidebar{mobileOpen ? ' mobile-open' : ''}">
|
||||||
<nav>
|
<nav>
|
||||||
<ul>
|
<ul>
|
||||||
{#each entries as link}
|
{#each entries as link}
|
||||||
@ -59,6 +65,21 @@
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
border-right: 2px solid var(--ina-sidebar-border-color);
|
border-right: 2px solid var(--ina-sidebar-border-color);
|
||||||
|
|
||||||
|
@media screen and (max-width: 768px) {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 111;
|
||||||
|
transition: width 250ms linear;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
box-shadow: var(--in-alert-shadow);
|
||||||
|
|
||||||
|
&:not(.mobile-open) {
|
||||||
|
width: 0;
|
||||||
|
visibility: hidden;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
& > nav {
|
& > nav {
|
||||||
margin-top: 16px;
|
margin-top: 16px;
|
||||||
|
|
||||||
|
@ -2,14 +2,15 @@
|
|||||||
interface Props {
|
interface Props {
|
||||||
src?: string;
|
src?: string;
|
||||||
alt?: string;
|
alt?: string;
|
||||||
|
variant?: 'normal' | 'small';
|
||||||
children?: import('svelte').Snippet;
|
children?: import('svelte').Snippet;
|
||||||
}
|
}
|
||||||
|
|
||||||
let { src = '', alt = '', children }: Props = $props();
|
let { src = '', alt = '', variant = 'normal', children }: Props = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="avatar-wrapper{children ? ' with-actions' : ''}">
|
<div class="avatar-wrapper{children ? ' with-actions' : ''}">
|
||||||
<div class="image-wrapper">
|
<div class="image-wrapper{variant ? ` ${variant}` : ''}">
|
||||||
<img {src} {alt} />
|
<img {src} {alt} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -37,5 +38,11 @@
|
|||||||
width: 120px;
|
width: 120px;
|
||||||
flex: 0 0 120px;
|
flex: 0 0 120px;
|
||||||
background-color: var(--in-normalized-background);
|
background-color: var(--in-normalized-background);
|
||||||
|
|
||||||
|
&.small {
|
||||||
|
height: 60px;
|
||||||
|
width: 60px;
|
||||||
|
flex: 0 0 60px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -1,13 +1,14 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
interface Props {
|
interface Props {
|
||||||
|
mobileBreak?: boolean;
|
||||||
children?: import('svelte').Snippet;
|
children?: import('svelte').Snippet;
|
||||||
action?: import('svelte').Snippet;
|
action?: import('svelte').Snippet;
|
||||||
}
|
}
|
||||||
|
|
||||||
let { children, action }: Props = $props();
|
let { children, action, mobileBreak = true }: Props = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="title-row">
|
<div class="title-row{mobileBreak ? ' mobile-break' : ''}">
|
||||||
{@render children?.()}
|
{@render children?.()}
|
||||||
{@render action?.()}
|
{@render action?.()}
|
||||||
</div>
|
</div>
|
||||||
@ -19,14 +20,20 @@
|
|||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
margin-bottom: 1.3rem;
|
margin-bottom: 1.3rem;
|
||||||
|
|
||||||
@media screen and (max-width: 768px) {
|
& > :global(*):first-child {
|
||||||
flex-direction: column;
|
align-self: flex-start;
|
||||||
gap: 1rem;
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.mobile-break {
|
||||||
|
@media screen and (max-width: 768px) {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1rem;
|
||||||
|
|
||||||
|
& > :global(*):first-child {
|
||||||
|
align-self: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.title-row > :global(*):first-child {
|
|
||||||
align-self: flex-start;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
@ -1,17 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import { t } from '$lib/i18n';
|
|
||||||
import Alert from '../Alert.svelte';
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
prefix: string;
|
|
||||||
errors?: string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
let { prefix, errors = [] }: Props = $props();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
{#if errors.length}
|
|
||||||
{#each errors as error}
|
|
||||||
<Alert type="error">{$t(`${prefix}.${error}`)}</Alert>
|
|
||||||
{/each}
|
|
||||||
{/if}
|
|
5
src/lib/components/icons/svg/Close.svelte
Normal file
5
src/lib/components/icons/svg/Close.svelte
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"
|
||||||
|
><path
|
||||||
|
d="M13.46,12L19,17.54V19H17.54L12,13.46L6.46,19H5V17.54L10.54,12L5,6.46V5H6.46L12,10.54L17.54,5H19V6.46L13.46,12Z"
|
||||||
|
/></svg
|
||||||
|
>
|
After Width: | Height: | Size: 196 B |
3
src/lib/components/icons/svg/Menu.svelte
Normal file
3
src/lib/components/icons/svg/Menu.svelte
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"
|
||||||
|
><path d="M3,6H21V8H3V6M3,11H21V13H3V11M3,16H21V18H3V16Z" /></svg
|
||||||
|
>
|
After Width: | Height: | Size: 129 B |
56
src/lib/components/oauth2/OAuth2LoginClientCard.svelte
Normal file
56
src/lib/components/oauth2/OAuth2LoginClientCard.svelte
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { t } from '$lib/i18n';
|
||||||
|
import type { OAuth2ClientInfo } from '$lib/types';
|
||||||
|
import AvatarCard from '../avatar/AvatarCard.svelte';
|
||||||
|
|
||||||
|
let { client }: { client: OAuth2ClientInfo } = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="client-wrapper">
|
||||||
|
<div class="card">
|
||||||
|
<AvatarCard src={`/api/avatar/client/${client.client_id}`} alt={client.title} variant="small">
|
||||||
|
<div class="card-inner">
|
||||||
|
<span class="card-display-name">{client.title}</span>
|
||||||
|
<span class="card-user-name">{client.description}</span>
|
||||||
|
|
||||||
|
<div class="card-links">
|
||||||
|
{#each client.links as link}
|
||||||
|
<a href={link.url} target="_blank" rel="nofollow noreferrer"
|
||||||
|
>{$t(`oauth2.authorize.link.${link.type}`)}</a
|
||||||
|
>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</AvatarCard>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.client-wrapper {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-evenly;
|
||||||
|
width: calc(100% + 80px);
|
||||||
|
margin: 0 -40px;
|
||||||
|
background-color: var(--in-inner-container-background);
|
||||||
|
|
||||||
|
& .card {
|
||||||
|
width: 100%;
|
||||||
|
padding: 16px 40px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.card-inner {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
.card-display-name {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
.card-links {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
margin-top: auto;
|
||||||
|
}
|
||||||
|
</style>
|
13
src/lib/form-errors.ts
Normal file
13
src/lib/form-errors.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import { get } from 'svelte/store';
|
||||||
|
import { displayMessage, translateError } from './stores/messages.store';
|
||||||
|
|
||||||
|
export const popupFormErrors = (form?: { errors?: string[] } | null, prefix?: string) => {
|
||||||
|
if (form?.errors) {
|
||||||
|
form.errors.forEach((error) =>
|
||||||
|
displayMessage({
|
||||||
|
text: get(translateError)(error, prefix),
|
||||||
|
type: 'error'
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
@ -1,8 +1,11 @@
|
|||||||
{
|
{
|
||||||
"title": "Admin",
|
"title": "Admin",
|
||||||
|
"success": "Changes saved successfully.",
|
||||||
"menu": {
|
"menu": {
|
||||||
|
"open": "Open menu",
|
||||||
|
"close": "Close menu",
|
||||||
"users": "Users",
|
"users": "Users",
|
||||||
"oauth2": "OAuth2 applications",
|
"oauth2": "OAuth 2.0 applications",
|
||||||
"audit": "Audit logs"
|
"audit": "Audit logs"
|
||||||
},
|
},
|
||||||
"users": {
|
"users": {
|
||||||
@ -29,7 +32,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"oauth2": {
|
"oauth2": {
|
||||||
"title": "OAuth2 applications",
|
"title": "OAuth 2.0 applications",
|
||||||
"new": "Create a new application",
|
"new": "Create a new application",
|
||||||
"clientTitle": "Application name",
|
"clientTitle": "Application name",
|
||||||
"clientId": "Client ID",
|
"clientId": "Client ID",
|
||||||
@ -42,7 +45,7 @@
|
|||||||
"scopes": "Available scopes",
|
"scopes": "Available scopes",
|
||||||
"scopesHint": "The level of access to information you will be needing for this application.",
|
"scopesHint": "The level of access to information you will be needing for this application.",
|
||||||
"grants": "Available grant types",
|
"grants": "Available grant types",
|
||||||
"grantsHint": "The OAuth2 authorization flows you will be using with this application.",
|
"grantsHint": "The OAuth 2.0 authorization flows you will be using with this application.",
|
||||||
"confidential": "Confidential",
|
"confidential": "Confidential",
|
||||||
"confidentialHint": "Uncheck this checkbox if you cannot reasonably guarantee the secrecy of your Client secret, such as in the case of your application being a native (desktop, mobile, etc.) application. Unchecking this will allow you to get tokens without client authentication. Some functionality may be limited in public applications.",
|
"confidentialHint": "Uncheck this checkbox if you cannot reasonably guarantee the secrecy of your Client secret, such as in the case of your application being a native (desktop, mobile, etc.) application. Unchecking this will allow you to get tokens without client authentication. Some functionality may be limited in public applications.",
|
||||||
"created": "Created at",
|
"created": "Created at",
|
||||||
@ -59,7 +62,6 @@
|
|||||||
"authorizationsHint": "These users have authorized this application at least once. You may assign application privileges to each user individually.",
|
"authorizationsHint": "These users have authorized this application at least once. You may assign application privileges to each user individually.",
|
||||||
"revoked": "This authorization has been revoked by the user",
|
"revoked": "This authorization has been revoked by the user",
|
||||||
"noAuthorizations": "There are no authorizations on record for this application.",
|
"noAuthorizations": "There are no authorizations on record for this application.",
|
||||||
"success": "Success!",
|
|
||||||
"privileges": {
|
"privileges": {
|
||||||
"title": "Application privileges",
|
"title": "Application privileges",
|
||||||
"name": "Privilege name",
|
"name": "Privilege name",
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"description": "{{siteName}} is a Single-Sign-On service used by other applications.",
|
"description": "{{siteName}} is a Single-Sign-On service used by other applications.",
|
||||||
"metaDescription": "{{siteName}} - Single-Sign-On service",
|
"metaDescription": "{{siteName}} - Single-Sign-On service",
|
||||||
"cookieDisclaimer": "The website may use temporary cookies for storing your login session and ensuring your security. This web service is <a href=\"https://git.icynet.eu/IcyNetwork/sso-core\" target=\"_blank\">completely open source</a> and can be audited by anyone.",
|
"cookieDisclaimer": "The website may use temporary cookies for storing your login session, theme preference and ensuring your account security. This web service is <a href=\"https://git.icynet.eu/IcyNetwork/sso-core\" target=\"_blank\">completely open source</a> and can be audited by anyone.",
|
||||||
"submit": "Submit",
|
"submit": "Submit",
|
||||||
"cancel": "Cancel",
|
"cancel": "Cancel",
|
||||||
"manage": "Manage",
|
"manage": "Manage",
|
||||||
@ -15,6 +15,9 @@
|
|||||||
"true": "Yes",
|
"true": "Yes",
|
||||||
"false": "No"
|
"false": "No"
|
||||||
},
|
},
|
||||||
|
"success": "Success",
|
||||||
|
"error": "Error",
|
||||||
|
"close": "Close",
|
||||||
"available": "Available",
|
"available": "Available",
|
||||||
"current": "Current",
|
"current": "Current",
|
||||||
"remove": "Remove",
|
"remove": "Remove",
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { SQL, count, desc, eq, inArray, lt, or, sql } from 'drizzle-orm';
|
import { SQL, count, desc, eq, inArray, or, sql } from 'drizzle-orm';
|
||||||
import { DB, auditLog, user, type AuditLog, type NewAuditLog, type User } from '../drizzle';
|
import { DB, auditLog, user, type AuditLog, type NewAuditLog, type User } from '../drizzle';
|
||||||
import {
|
import {
|
||||||
AuditAction,
|
AuditAction,
|
||||||
@ -11,6 +11,7 @@ import { Logger } from '../logger';
|
|||||||
import { AdminNotificationEmail, Emails } from '../email';
|
import { AdminNotificationEmail, Emails } from '../email';
|
||||||
import { env } from '$env/dynamic/private';
|
import { env } from '$env/dynamic/private';
|
||||||
import { env as publicEnv } from '$env/dynamic/public';
|
import { env as publicEnv } from '$env/dynamic/public';
|
||||||
|
import { CacheBackend } from '../cache-backend';
|
||||||
|
|
||||||
const FLAG_EMAIL_COOLDOWN = 1 * 60 * 1000;
|
const FLAG_EMAIL_COOLDOWN = 1 * 60 * 1000;
|
||||||
const FLAG_TRESHOLD_COOLDOWN = 30 * 60 * 1000;
|
const FLAG_TRESHOLD_COOLDOWN = 30 * 60 * 1000;
|
||||||
@ -20,9 +21,6 @@ const AUTOFLAG = [AuditAction.MALICIOUS_REQUEST, AuditAction.THROTTLE];
|
|||||||
export class Audit {
|
export class Audit {
|
||||||
protected static logger = new Logger('AUDIT');
|
protected static logger = new Logger('AUDIT');
|
||||||
protected static flagEmailTimeout: ReturnType<typeof setTimeout> | undefined = undefined;
|
protected static flagEmailTimeout: ReturnType<typeof setTimeout> | undefined = undefined;
|
||||||
protected static flagTresholdTimeout: ReturnType<typeof setTimeout> | undefined = undefined;
|
|
||||||
protected static flagTriggerCount = 0;
|
|
||||||
protected static flagEmailCount = 0;
|
|
||||||
|
|
||||||
public static async insert(
|
public static async insert(
|
||||||
action: AuditAction,
|
action: AuditAction,
|
||||||
@ -176,28 +174,22 @@ export class Audit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static async auditFlagTrigger() {
|
private static async auditFlagTrigger() {
|
||||||
Audit.flagTriggerCount += 1;
|
let flagTriggerCount = (await CacheBackend.get<number>('auditFlagTriggerCount')) ?? 0;
|
||||||
|
let flagEmailCount = (await CacheBackend.get<number>('auditFlagEmailCount')) ?? 0;
|
||||||
|
flagTriggerCount += 1;
|
||||||
|
|
||||||
clearTimeout(Audit.flagTresholdTimeout);
|
await CacheBackend.set('auditFlagTriggerCount', flagTriggerCount, FLAG_TRESHOLD_COOLDOWN);
|
||||||
Audit.flagTresholdTimeout = setTimeout(() => {
|
|
||||||
Audit.flagTriggerCount = 0;
|
|
||||||
Audit.flagEmailCount = 0;
|
|
||||||
}, FLAG_TRESHOLD_COOLDOWN);
|
|
||||||
|
|
||||||
if (
|
if (flagTriggerCount < FLAG_EMAIL_TRESHOLD || Audit.flagEmailTimeout || flagEmailCount > 1) {
|
||||||
Audit.flagTriggerCount < FLAG_EMAIL_TRESHOLD ||
|
|
||||||
Audit.flagEmailTimeout ||
|
|
||||||
Audit.flagEmailCount > 1
|
|
||||||
) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Logger.verbose('Flag treshold reached, sending out audit email to administrator');
|
Logger.verbose('Flag treshold reached, sending out audit email to administrator');
|
||||||
Audit.flagEmailTimeout = setTimeout(() => {
|
Audit.flagEmailTimeout = setTimeout(() => {
|
||||||
void Audit.sendAuditEmail(Audit.flagTriggerCount);
|
void Audit.sendAuditEmail(flagTriggerCount);
|
||||||
Audit.flagTriggerCount = 0;
|
|
||||||
Audit.flagEmailCount += 1;
|
|
||||||
Audit.flagEmailTimeout = undefined;
|
Audit.flagEmailTimeout = undefined;
|
||||||
|
void CacheBackend.set('auditFlagTriggerCount', 0, FLAG_TRESHOLD_COOLDOWN);
|
||||||
|
void CacheBackend.set('auditFlagEmailCount', flagEmailCount + 1, FLAG_TRESHOLD_COOLDOWN);
|
||||||
}, FLAG_EMAIL_COOLDOWN);
|
}, FLAG_EMAIL_COOLDOWN);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
51
src/lib/server/cache-backend.ts
Normal file
51
src/lib/server/cache-backend.ts
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
import { env } from '$env/dynamic/private';
|
||||||
|
import KeyvValkey from '@keyv/valkey';
|
||||||
|
import { createCache, type Cache } from 'cache-manager';
|
||||||
|
import { CacheableMemory, Keyv } from 'cacheable';
|
||||||
|
|
||||||
|
export class CacheBackend {
|
||||||
|
private static cache: Cache;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set a cached value
|
||||||
|
* @param key Key to set
|
||||||
|
* @param data Data to set
|
||||||
|
* @param ttl Time-to-live in milliseconds
|
||||||
|
*/
|
||||||
|
public static async set<T = unknown>(key: string, data: T, ttl?: number): Promise<T> {
|
||||||
|
return CacheBackend.cache.set(key, data, ttl);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a cached value
|
||||||
|
* @param key Key to retrieve from cache
|
||||||
|
* @returns Value
|
||||||
|
*/
|
||||||
|
public static async get<T = unknown>(key: string): Promise<T | undefined> {
|
||||||
|
return CacheBackend.cache.get(key) as T;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete a cached value
|
||||||
|
* @param key Key to delete from cache
|
||||||
|
* @returns Success
|
||||||
|
*/
|
||||||
|
public static async del(key: string) {
|
||||||
|
return CacheBackend.cache.del(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize configured cache backend
|
||||||
|
*/
|
||||||
|
public static async init() {
|
||||||
|
CacheBackend.cache = createCache({
|
||||||
|
stores: [
|
||||||
|
new Keyv({
|
||||||
|
store: env.VALKEY_URI
|
||||||
|
? new KeyvValkey(env.VALKEY_URI)
|
||||||
|
: new CacheableMemory({ ttl: 3600000, lruSize: 5000 })
|
||||||
|
})
|
||||||
|
]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@ -1,6 +1,8 @@
|
|||||||
import { mkdir, readFile, rm, stat, unlink, writeFile } from 'fs/promises';
|
import { mkdir, readFile, rm, stat, unlink, writeFile } from 'fs/promises';
|
||||||
import { dirname, join, resolve } from 'path';
|
import { dirname, join, resolve } from 'path';
|
||||||
|
|
||||||
|
// TODO: implement S3
|
||||||
|
|
||||||
export class FileBackend {
|
export class FileBackend {
|
||||||
static async fileExists(path: string | string[]) {
|
static async fileExists(path: string | string[]) {
|
||||||
try {
|
try {
|
||||||
@ -44,4 +46,8 @@ export class FileBackend {
|
|||||||
private static filePath(path: string | string[]) {
|
private static filePath(path: string | string[]) {
|
||||||
return resolve(Array.isArray(path) ? join(...path) : path);
|
return resolve(Array.isArray(path) ? join(...path) : path);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static async init() {
|
||||||
|
// stub
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { env } from '$env/dynamic/private';
|
import { env } from '$env/dynamic/private';
|
||||||
import {
|
import {
|
||||||
type JWK,
|
type JWK,
|
||||||
type KeyLike,
|
type CryptoKey,
|
||||||
importPKCS8,
|
importPKCS8,
|
||||||
importSPKI,
|
importSPKI,
|
||||||
SignJWT,
|
SignJWT,
|
||||||
@ -23,8 +23,8 @@ const ISSUER_EXPIRY = 365 * 24 * 60 * 60 * 1000;
|
|||||||
const ISSUER_ROTATE = ISSUER_EXPIRY / 2;
|
const ISSUER_ROTATE = ISSUER_EXPIRY / 2;
|
||||||
|
|
||||||
interface AvailableWebKey extends JsonKey {
|
interface AvailableWebKey extends JsonKey {
|
||||||
privateKey: KeyLike;
|
privateKey: CryptoKey;
|
||||||
publicKey: KeyLike;
|
publicKey: CryptoKey;
|
||||||
publicKeyJWK: JWK;
|
publicKeyJWK: JWK;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -17,7 +17,7 @@ import {
|
|||||||
import { Emails, OAuth2InvitationEmail } from '$lib/server/email';
|
import { Emails, OAuth2InvitationEmail } from '$lib/server/email';
|
||||||
import { Uploads } from '$lib/server/upload';
|
import { Uploads } from '$lib/server/upload';
|
||||||
import { UserTokens, Users } from '$lib/server/users';
|
import { UserTokens, Users } from '$lib/server/users';
|
||||||
import type { PaginationMeta } from '$lib/types';
|
import type { OAuth2ClientInfo, PaginationMeta } from '$lib/types';
|
||||||
import { and, count, eq, like, or, sql } from 'drizzle-orm';
|
import { and, count, eq, like, or, sql } from 'drizzle-orm';
|
||||||
import { createLocalJWKSet, exportJWK, importJWK, jwtVerify, type JWK } from 'jose';
|
import { createLocalJWKSet, exportJWK, importJWK, jwtVerify, type JWK } from 'jose';
|
||||||
|
|
||||||
@ -409,9 +409,9 @@ export class OAuth2Clients {
|
|||||||
// This prevents entering of arbitrary JSON data while also validating the key
|
// This prevents entering of arbitrary JSON data while also validating the key
|
||||||
parsedKeys = await Promise.all(
|
parsedKeys = await Promise.all(
|
||||||
preParseList.map(async (entry: JWK) => {
|
preParseList.map(async (entry: JWK) => {
|
||||||
const imported = await importJWK(entry);
|
const imported = await importJWK(entry, privateEnv.JWT_ALGORITHM);
|
||||||
const exported = await exportJWK(imported);
|
const exported = await exportJWK(imported);
|
||||||
(['use', 'kid'] as (keyof JWK)[]).forEach((nKey) => {
|
(['use', 'kid', 'alg', 'ext', 'key_ops'] as (keyof JWK)[]).forEach((nKey) => {
|
||||||
exported[nKey] = (entry[nKey] || undefined) as never;
|
exported[nKey] = (entry[nKey] || undefined) as never;
|
||||||
});
|
});
|
||||||
return exported;
|
return exported;
|
||||||
@ -423,7 +423,7 @@ export class OAuth2Clients {
|
|||||||
(entry, index, array) =>
|
(entry, index, array) =>
|
||||||
array.findIndex((item) => JSON.stringify(item) === JSON.stringify(entry)) === index
|
array.findIndex((item) => JSON.stringify(item) === JSON.stringify(entry)) === index
|
||||||
);
|
);
|
||||||
} catch {
|
} catch (e) {
|
||||||
throw new Error('Failed to parse JWKs');
|
throw new Error('Failed to parse JWKs');
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -491,7 +491,10 @@ export class OAuth2Clients {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static async authorizeClientInfo(client: OAuth2Client, scope: string[]) {
|
static async authorizeClientInfo(
|
||||||
|
client: OAuth2Client,
|
||||||
|
scope: string[]
|
||||||
|
): Promise<OAuth2ClientInfo> {
|
||||||
const links = await OAuth2Clients.getClientUrls(client);
|
const links = await OAuth2Clients.getClientUrls(client);
|
||||||
const filteredLinks = links
|
const filteredLinks = links
|
||||||
.filter((link) => link.type !== 'redirect_uri')
|
.filter((link) => link.type !== 'redirect_uri')
|
||||||
|
@ -236,7 +236,7 @@ export class OAuth2AccessTokens {
|
|||||||
) {
|
) {
|
||||||
const client = await OAuth2Clients.fetchById(clientId);
|
const client = await OAuth2Clients.fetchById(clientId);
|
||||||
const user = userId != null ? await Users.getById(userId) : undefined;
|
const user = userId != null ? await Users.getById(userId) : undefined;
|
||||||
const accessToken = CryptoUtils.generateString(64);
|
const accessToken = CryptoUtils.generateString(128);
|
||||||
|
|
||||||
const scopes = (!Array.isArray(scope) ? OAuth2Clients.splitScope(scope) : scope).join(' ');
|
const scopes = (!Array.isArray(scope) ? OAuth2Clients.splitScope(scope) : scope).join(' ');
|
||||||
|
|
||||||
|
36
src/lib/stores/messages.store.ts
Normal file
36
src/lib/stores/messages.store.ts
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import { browser } from '$app/environment';
|
||||||
|
import { locale, t } from '$lib/i18n';
|
||||||
|
import { derived, writable } from 'svelte/store';
|
||||||
|
|
||||||
|
export interface Message {
|
||||||
|
text: string;
|
||||||
|
type: 'success' | 'error';
|
||||||
|
actions?: {
|
||||||
|
label: string;
|
||||||
|
action: () => void;
|
||||||
|
}[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const messages = writable<Message[]>([]);
|
||||||
|
|
||||||
|
export const dismissMessage = (msg: Message) =>
|
||||||
|
messages.update((current) => current.filter((entry) => entry !== msg));
|
||||||
|
|
||||||
|
export const displayMessage = (msg: Message) =>
|
||||||
|
browser && messages.update((current) => [...current, msg]);
|
||||||
|
|
||||||
|
export const clearMessages = () => messages.set([]);
|
||||||
|
|
||||||
|
export const translateError = {
|
||||||
|
...derived([t, locale], ([tFn]) => (error: string, prefix = 'common.errors') => {
|
||||||
|
const [keyword, args] = error.split('(');
|
||||||
|
const argList = args
|
||||||
|
?.substring(0, args.length - 1)
|
||||||
|
.split(',')
|
||||||
|
.reduce<Record<string, string>>(
|
||||||
|
(data, value, i) => ({ ...data, [`arg${i}`]: value.trim() }),
|
||||||
|
{}
|
||||||
|
);
|
||||||
|
return tFn(`${prefix}.${keyword}`, argList);
|
||||||
|
})
|
||||||
|
};
|
@ -3,7 +3,7 @@ export type RequiredPrivileges = (string | string[])[];
|
|||||||
/**
|
/**
|
||||||
* Check a list of privileges against a list of required privileges.
|
* Check a list of privileges against a list of required privileges.
|
||||||
* @param list List of privileges
|
* @param list List of privileges
|
||||||
* @param privileges OR logic, AND's arrays
|
* @param privileges OR logic, use a nested array for AND
|
||||||
* @returns Satisfies privileges
|
* @returns Satisfies privileges
|
||||||
*/
|
*/
|
||||||
export const hasPrivileges = (list: string[], privileges: RequiredPrivileges) =>
|
export const hasPrivileges = (list: string[], privileges: RequiredPrivileges) =>
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { env } from '$env/dynamic/public';
|
import { env } from '$env/dynamic/public';
|
||||||
|
import Messages from '$lib/components/Messages.svelte';
|
||||||
import { t } from '$lib/i18n';
|
import { t } from '$lib/i18n';
|
||||||
import { forwardInitialTheme } from '$lib/theme-mode';
|
import { forwardInitialTheme } from '$lib/theme-mode';
|
||||||
import '../app.css';
|
import '../app.css';
|
||||||
@ -21,3 +22,5 @@
|
|||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
{@render children?.()}
|
{@render children?.()}
|
||||||
|
|
||||||
|
<Messages />
|
||||||
|
@ -6,7 +6,6 @@
|
|||||||
import FormSection from '$lib/components/form/FormSection.svelte';
|
import FormSection from '$lib/components/form/FormSection.svelte';
|
||||||
import FormWrapper from '$lib/components/form/FormWrapper.svelte';
|
import FormWrapper from '$lib/components/form/FormWrapper.svelte';
|
||||||
import Button from '$lib/components/Button.svelte';
|
import Button from '$lib/components/Button.svelte';
|
||||||
import Alert from '$lib/components/Alert.svelte';
|
|
||||||
import ViewColumn from '$lib/components/container/ColumnView.svelte';
|
import ViewColumn from '$lib/components/container/ColumnView.svelte';
|
||||||
import MainContainer from '$lib/components/container/MainContainer.svelte';
|
import MainContainer from '$lib/components/container/MainContainer.svelte';
|
||||||
import SplitView from '$lib/components/container/SplitView.svelte';
|
import SplitView from '$lib/components/container/SplitView.svelte';
|
||||||
@ -14,14 +13,16 @@
|
|||||||
import type { SubmitFunction } from '@sveltejs/kit';
|
import type { SubmitFunction } from '@sveltejs/kit';
|
||||||
import AvatarCard from '$lib/components/avatar/AvatarCard.svelte';
|
import AvatarCard from '$lib/components/avatar/AvatarCard.svelte';
|
||||||
import AvatarModal from '$lib/components/avatar/AvatarModal.svelte';
|
import AvatarModal from '$lib/components/avatar/AvatarModal.svelte';
|
||||||
import { writable } from 'svelte/store';
|
import { get, writable } from 'svelte/store';
|
||||||
import { env } from '$env/dynamic/public';
|
import { env } from '$env/dynamic/public';
|
||||||
import FormErrors from '$lib/components/form/FormErrors.svelte';
|
|
||||||
import TitleRow from '$lib/components/container/TitleRow.svelte';
|
import TitleRow from '$lib/components/container/TitleRow.svelte';
|
||||||
import ActionButton from '$lib/components/ActionButton.svelte';
|
import ActionButton from '$lib/components/ActionButton.svelte';
|
||||||
import ButtonRow from '$lib/components/container/ButtonRow.svelte';
|
import ButtonRow from '$lib/components/container/ButtonRow.svelte';
|
||||||
import ThemeButton from '$lib/components/ThemeButton.svelte';
|
import ThemeButton from '$lib/components/ThemeButton.svelte';
|
||||||
import { hasPrivileges } from '$lib/utils';
|
import { hasPrivileges } from '$lib/utils';
|
||||||
|
import { clearMessages, displayMessage } from '$lib/stores/messages.store';
|
||||||
|
import { popupFormErrors } from '$lib/form-errors';
|
||||||
|
import { onNavigate } from '$app/navigation';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
data: PageData;
|
data: PageData;
|
||||||
@ -30,8 +31,6 @@
|
|||||||
|
|
||||||
let { data, form }: Props = $props();
|
let { data, form }: Props = $props();
|
||||||
|
|
||||||
let internalErrors: string[] = $state([]);
|
|
||||||
let errors = $derived([...internalErrors, ...(form?.errors?.length ? form.errors : [])]);
|
|
||||||
let adminButton = $derived(hasPrivileges(data.privileges, ['admin', 'self:oauth2']));
|
let adminButton = $derived(hasPrivileges(data.privileges, ['admin', 'self:oauth2']));
|
||||||
|
|
||||||
let usernameRef: HTMLInputElement = $state()!;
|
let usernameRef: HTMLInputElement = $state()!;
|
||||||
@ -39,12 +38,15 @@
|
|||||||
let showAvatarModal = writable(false);
|
let showAvatarModal = writable(false);
|
||||||
|
|
||||||
const enhanceFn: SubmitFunction = ({ formData, cancel }) => {
|
const enhanceFn: SubmitFunction = ({ formData, cancel }) => {
|
||||||
internalErrors.length = 0;
|
clearMessages();
|
||||||
|
|
||||||
const pwd = formData.get('newPassword') as string;
|
const pwd = formData.get('newPassword') as string;
|
||||||
const repeat = formData.get('repeatPassword') as string;
|
const repeat = formData.get('repeatPassword') as string;
|
||||||
if (pwd && pwd !== repeat) {
|
if (pwd && pwd !== repeat) {
|
||||||
internalErrors.push('passwordMismatch');
|
displayMessage({
|
||||||
|
type: 'error',
|
||||||
|
text: $t('account.errors.passwordMismatch')
|
||||||
|
});
|
||||||
return cancel();
|
return cancel();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -61,6 +63,14 @@
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
$effect(() => popupFormErrors(form, 'account.errors'));
|
||||||
|
$effect(() => {
|
||||||
|
if (form?.success) {
|
||||||
|
displayMessage({ type: 'success', text: get(t)('account.changeSuccess') });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
onNavigate(() => clearMessages());
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
@ -87,9 +97,6 @@
|
|||||||
<ViewColumn>
|
<ViewColumn>
|
||||||
<form action="?/update" method="POST" use:enhance={enhanceFn}>
|
<form action="?/update" method="POST" use:enhance={enhanceFn}>
|
||||||
<FormWrapper>
|
<FormWrapper>
|
||||||
{#if form?.success}<Alert type="success">{$t('account.changeSuccess')}</Alert>{/if}
|
|
||||||
<FormErrors {errors} prefix="account.errors" />
|
|
||||||
|
|
||||||
{#if form?.otpRequired}
|
{#if form?.otpRequired}
|
||||||
<!-- Two-factor code request -->
|
<!-- Two-factor code request -->
|
||||||
<FormSection title={$t('account.login.otp')}>
|
<FormSection title={$t('account.login.otp')}>
|
||||||
|
@ -10,7 +10,9 @@
|
|||||||
import OAuth2ScopesCard from '$lib/components/oauth2/OAuth2ScopesCard.svelte';
|
import OAuth2ScopesCard from '$lib/components/oauth2/OAuth2ScopesCard.svelte';
|
||||||
import FormWrapper from '$lib/components/form/FormWrapper.svelte';
|
import FormWrapper from '$lib/components/form/FormWrapper.svelte';
|
||||||
import FormControl from '$lib/components/form/FormControl.svelte';
|
import FormControl from '$lib/components/form/FormControl.svelte';
|
||||||
import FormErrors from '$lib/components/form/FormErrors.svelte';
|
import { onNavigate } from '$app/navigation';
|
||||||
|
import { popupFormErrors } from '$lib/form-errors';
|
||||||
|
import { clearMessages } from '$lib/stores/messages.store';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
data: PageData;
|
data: PageData;
|
||||||
@ -18,6 +20,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
let { data, form }: Props = $props();
|
let { data, form }: Props = $props();
|
||||||
|
|
||||||
|
$effect(() => popupFormErrors(form, 'oauth2.errors'));
|
||||||
|
onNavigate(() => clearMessages());
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
@ -61,7 +66,6 @@
|
|||||||
|
|
||||||
<form action="" method="POST" use:enhance>
|
<form action="" method="POST" use:enhance>
|
||||||
<FormWrapper>
|
<FormWrapper>
|
||||||
<FormErrors errors={form?.errors || []} prefix="oauth2.errors" />
|
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<label for="form-code">{$t('oauth2.device.deviceCode')}</label>
|
<label for="form-code">{$t('oauth2.device.deviceCode')}</label>
|
||||||
<input type="text" autocomplete="off" name="code" id="form-code" />
|
<input type="text" autocomplete="off" name="code" id="form-code" />
|
||||||
|
@ -1,19 +1,15 @@
|
|||||||
|
import { env } from '$env/dynamic/public';
|
||||||
import { Audit } from '$lib/server/audit/audit.js';
|
import { Audit } from '$lib/server/audit/audit.js';
|
||||||
import { AuditAction } from '$lib/server/audit/types.js';
|
import { AuditAction } from '$lib/server/audit/types.js';
|
||||||
import { Challenge } from '$lib/server/challenge.js';
|
import { Challenge } from '$lib/server/challenge.js';
|
||||||
import { Changesets } from '$lib/server/changesets.js';
|
import { Changesets } from '$lib/server/changesets.js';
|
||||||
|
import { OAuth2Clients } from '$lib/server/oauth2/index.js';
|
||||||
import { Users } from '$lib/server/users/index.js';
|
import { Users } from '$lib/server/users/index.js';
|
||||||
import { TimeOTP } from '$lib/server/users/totp.js';
|
import { TimeOTP } from '$lib/server/users/totp.js';
|
||||||
|
import type { OAuth2ClientInfo } from '$lib/types';
|
||||||
import { error, fail, redirect, type Actions } from '@sveltejs/kit';
|
import { error, fail, redirect, type Actions } from '@sveltejs/kit';
|
||||||
import { RateLimiter } from 'sveltekit-rate-limiter/server';
|
import { RateLimiter } from 'sveltekit-rate-limiter/server';
|
||||||
|
|
||||||
interface LoginParams {
|
|
||||||
email: string;
|
|
||||||
password: string;
|
|
||||||
challenge: string;
|
|
||||||
otpCode: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface LoginChallenge {
|
interface LoginChallenge {
|
||||||
email: string;
|
email: string;
|
||||||
}
|
}
|
||||||
@ -132,6 +128,8 @@ export const actions = {
|
|||||||
} as Actions;
|
} as Actions;
|
||||||
|
|
||||||
export const load = async ({ locals, url, ...event }) => {
|
export const load = async ({ locals, url, ...event }) => {
|
||||||
|
let clientInfo: OAuth2ClientInfo | undefined;
|
||||||
|
|
||||||
if (url.searchParams.has('redirectTo')) {
|
if (url.searchParams.has('redirectTo')) {
|
||||||
// Check that the redirect URL is a local path
|
// Check that the redirect URL is a local path
|
||||||
if (!url.searchParams.get('redirectTo')?.startsWith('/')) {
|
if (!url.searchParams.get('redirectTo')?.startsWith('/')) {
|
||||||
@ -142,6 +140,19 @@ export const load = async ({ locals, url, ...event }) => {
|
|||||||
if (locals.session.data?.user) {
|
if (locals.session.data?.user) {
|
||||||
return redirect(301, url.searchParams.get('redirectTo') || '/');
|
return redirect(301, url.searchParams.get('redirectTo') || '/');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Take client ID from the redirect if applicable
|
||||||
|
const parsedParams = new URL(url.searchParams.get('redirectTo') as string, env.PUBLIC_URL);
|
||||||
|
const clientId = parsedParams.searchParams.get('client_id') as string;
|
||||||
|
if (clientId) {
|
||||||
|
const client = await OAuth2Clients.fetchById(clientId);
|
||||||
|
if (!client) {
|
||||||
|
return redirect(301, '/login');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Displays client info on the login page to make the redirect less confusing
|
||||||
|
clientInfo = await OAuth2Clients.authorizeClientInfo(client, []);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Activation routine
|
// Activation routine
|
||||||
@ -165,6 +176,7 @@ export const load = async ({ locals, url, ...event }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
activated: null
|
activated: null,
|
||||||
|
clientInfo
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -12,6 +12,7 @@
|
|||||||
import ButtonRow from '$lib/components/container/ButtonRow.svelte';
|
import ButtonRow from '$lib/components/container/ButtonRow.svelte';
|
||||||
import TitleRow from '$lib/components/container/TitleRow.svelte';
|
import TitleRow from '$lib/components/container/TitleRow.svelte';
|
||||||
import ThemeButton from '$lib/components/ThemeButton.svelte';
|
import ThemeButton from '$lib/components/ThemeButton.svelte';
|
||||||
|
import OAuth2LoginClientCard from '$lib/components/oauth2/OAuth2LoginClientCard.svelte';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
data: PageData;
|
data: PageData;
|
||||||
@ -26,7 +27,7 @@
|
|||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
<SideContainer>
|
<SideContainer>
|
||||||
<TitleRow>
|
<TitleRow mobileBreak={false}>
|
||||||
<h1>{env.PUBLIC_SITE_NAME}</h1>
|
<h1>{env.PUBLIC_SITE_NAME}</h1>
|
||||||
<ThemeButton />
|
<ThemeButton />
|
||||||
</TitleRow>
|
</TitleRow>
|
||||||
@ -56,6 +57,10 @@
|
|||||||
</FormSection>
|
</FormSection>
|
||||||
{:else}
|
{:else}
|
||||||
<!-- Normal login -->
|
<!-- Normal login -->
|
||||||
|
{#if data.clientInfo}
|
||||||
|
<OAuth2LoginClientCard client={data.clientInfo} />
|
||||||
|
{/if}
|
||||||
|
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<label for="login-email">{$t('account.login.email')}</label>
|
<label for="login-email">{$t('account.login.email')}</label>
|
||||||
<input id="login-email" name="email" value={form?.email ?? ''} autocomplete="username" />
|
<input id="login-email" name="email" value={form?.email ?? ''} autocomplete="username" />
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { enhance } from '$app/forms';
|
import { enhance } from '$app/forms';
|
||||||
|
import { onNavigate } from '$app/navigation';
|
||||||
import { page } from '$app/state';
|
import { page } from '$app/state';
|
||||||
import { env } from '$env/dynamic/public';
|
import { env } from '$env/dynamic/public';
|
||||||
import Alert from '$lib/components/Alert.svelte';
|
import Alert from '$lib/components/Alert.svelte';
|
||||||
@ -8,9 +9,10 @@
|
|||||||
import ColumnView from '$lib/components/container/ColumnView.svelte';
|
import ColumnView from '$lib/components/container/ColumnView.svelte';
|
||||||
import SideContainer from '$lib/components/container/SideContainer.svelte';
|
import SideContainer from '$lib/components/container/SideContainer.svelte';
|
||||||
import FormControl from '$lib/components/form/FormControl.svelte';
|
import FormControl from '$lib/components/form/FormControl.svelte';
|
||||||
import FormErrors from '$lib/components/form/FormErrors.svelte';
|
|
||||||
import FormWrapper from '$lib/components/form/FormWrapper.svelte';
|
import FormWrapper from '$lib/components/form/FormWrapper.svelte';
|
||||||
|
import { popupFormErrors } from '$lib/form-errors';
|
||||||
import { t } from '$lib/i18n';
|
import { t } from '$lib/i18n';
|
||||||
|
import { clearMessages, displayMessage } from '$lib/stores/messages.store';
|
||||||
import type { ActionData, PageData, SubmitFunction } from './$types';
|
import type { ActionData, PageData, SubmitFunction } from './$types';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@ -20,21 +22,22 @@
|
|||||||
|
|
||||||
let { data, form }: Props = $props();
|
let { data, form }: Props = $props();
|
||||||
|
|
||||||
let internalErrors: string[] = $state([]);
|
|
||||||
let submitted = $state(false);
|
let submitted = $state(false);
|
||||||
let errors = $derived([...internalErrors, ...(form?.errors?.length ? form.errors : [])]);
|
|
||||||
let actionUrl = $derived(
|
let actionUrl = $derived(
|
||||||
data.setter ? `?/setPassword&token=${page.url.searchParams.get('token')}` : '?/sendEmail'
|
data.setter ? `?/setPassword&token=${page.url.searchParams.get('token')}` : '?/sendEmail'
|
||||||
);
|
);
|
||||||
let pageTitle = $derived(data.setter ? 'setNewPassword' : 'resetPassword');
|
let pageTitle = $derived(data.setter ? 'setNewPassword' : 'resetPassword');
|
||||||
|
|
||||||
const enhanceFn: SubmitFunction = ({ formData, cancel }) => {
|
const enhanceFn: SubmitFunction = ({ formData, cancel }) => {
|
||||||
internalErrors.length = 0;
|
clearMessages();
|
||||||
|
|
||||||
const pwd = formData.get('newPassword') as string;
|
const pwd = formData.get('newPassword') as string;
|
||||||
const repeat = formData.get('repeatPassword') as string;
|
const repeat = formData.get('repeatPassword') as string;
|
||||||
if (pwd && pwd !== repeat) {
|
if (pwd && pwd !== repeat) {
|
||||||
internalErrors.push('passwordMismatch');
|
displayMessage({
|
||||||
|
type: 'error',
|
||||||
|
text: $t('account.errors.passwordMismatch')
|
||||||
|
});
|
||||||
return cancel();
|
return cancel();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -45,6 +48,9 @@
|
|||||||
submitted = false;
|
submitted = false;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
$effect(() => popupFormErrors(form, 'account.errors'));
|
||||||
|
onNavigate(() => clearMessages());
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
@ -65,8 +71,6 @@
|
|||||||
{:else}
|
{:else}
|
||||||
<form action={actionUrl} method="POST" use:enhance={enhanceFn}>
|
<form action={actionUrl} method="POST" use:enhance={enhanceFn}>
|
||||||
<FormWrapper>
|
<FormWrapper>
|
||||||
<FormErrors {errors} prefix="account.errors" />
|
|
||||||
|
|
||||||
{#if data.setter}
|
{#if data.setter}
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<label for="password-newPassword">{$t('account.newPassword')}</label>
|
<label for="password-newPassword">{$t('account.newPassword')}</label>
|
||||||
|
@ -11,9 +11,11 @@
|
|||||||
import Button from '$lib/components/Button.svelte';
|
import Button from '$lib/components/Button.svelte';
|
||||||
import FormSection from '$lib/components/form/FormSection.svelte';
|
import FormSection from '$lib/components/form/FormSection.svelte';
|
||||||
import { enhance } from '$app/forms';
|
import { enhance } from '$app/forms';
|
||||||
import FormErrors from '$lib/components/form/FormErrors.svelte';
|
|
||||||
import ButtonRow from '$lib/components/container/ButtonRow.svelte';
|
import ButtonRow from '$lib/components/container/ButtonRow.svelte';
|
||||||
import HCaptcha from '$lib/components/form/HCaptcha.svelte';
|
import HCaptcha from '$lib/components/form/HCaptcha.svelte';
|
||||||
|
import { onNavigate } from '$app/navigation';
|
||||||
|
import { popupFormErrors } from '$lib/form-errors';
|
||||||
|
import { clearMessages, displayMessage } from '$lib/stores/messages.store';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
data: PageData;
|
data: PageData;
|
||||||
@ -22,17 +24,18 @@
|
|||||||
|
|
||||||
let { data, form }: Props = $props();
|
let { data, form }: Props = $props();
|
||||||
|
|
||||||
let internalErrors: string[] = $state([]);
|
|
||||||
let submitted = $state(false);
|
let submitted = $state(false);
|
||||||
let errors = $derived([...internalErrors, ...(form?.errors?.length ? form.errors : [])]);
|
|
||||||
|
|
||||||
const enhanceFn: SubmitFunction = ({ formData, cancel }) => {
|
const enhanceFn: SubmitFunction = ({ formData, cancel }) => {
|
||||||
internalErrors.length = 0;
|
clearMessages();
|
||||||
|
|
||||||
const pwd = formData.get('newPassword') as string;
|
const pwd = formData.get('newPassword') as string;
|
||||||
const repeat = formData.get('repeatPassword') as string;
|
const repeat = formData.get('repeatPassword') as string;
|
||||||
if (pwd && pwd !== repeat) {
|
if (pwd && pwd !== repeat) {
|
||||||
internalErrors.push('passwordMismatch');
|
displayMessage({
|
||||||
|
type: 'error',
|
||||||
|
text: $t('account.errors.passwordMismatch')
|
||||||
|
});
|
||||||
return cancel();
|
return cancel();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -43,6 +46,9 @@
|
|||||||
submitted = false;
|
submitted = false;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
$effect(() => popupFormErrors(form, 'account.errors'));
|
||||||
|
onNavigate(() => clearMessages());
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
@ -63,8 +69,6 @@
|
|||||||
{:else}
|
{:else}
|
||||||
<form action="" method="POST" use:enhance={enhanceFn}>
|
<form action="" method="POST" use:enhance={enhanceFn}>
|
||||||
<FormWrapper>
|
<FormWrapper>
|
||||||
<FormErrors {errors} prefix="account.errors" />
|
|
||||||
|
|
||||||
<FormSection required>
|
<FormSection required>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<label for="register-username">{$t('account.username')}</label>
|
<label for="register-username">{$t('account.username')}</label>
|
||||||
|
@ -11,7 +11,8 @@
|
|||||||
|
|
||||||
let { data, children }: Props = $props();
|
let { data, children }: Props = $props();
|
||||||
|
|
||||||
let container: HTMLElement = $state();
|
let container: HTMLElement | undefined = $state();
|
||||||
|
let mobileOpen = $state(false);
|
||||||
|
|
||||||
// Reset container scroll.
|
// Reset container scroll.
|
||||||
afterNavigate(async ({ type }) => {
|
afterNavigate(async ({ type }) => {
|
||||||
@ -22,10 +23,10 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="admin-wrapper">
|
<div class="admin-wrapper">
|
||||||
<AdminHeader user={data.user} />
|
<AdminHeader user={data.user} bind:mobileOpen />
|
||||||
|
|
||||||
<div class="sidebar-wrapper">
|
<div class="sidebar-wrapper">
|
||||||
<AdminSidebar user={data.user} />
|
<AdminSidebar user={data.user} bind:mobileOpen />
|
||||||
|
|
||||||
<main bind:this={container}>
|
<main bind:this={container}>
|
||||||
{@render children?.()}
|
{@render children?.()}
|
||||||
|
@ -1,4 +1,13 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { t } from '$lib/i18n';
|
||||||
|
import { page } from '$app/state';
|
||||||
|
import { get, writable } from 'svelte/store';
|
||||||
|
import { env } from '$env/dynamic/public';
|
||||||
|
import { onNavigate } from '$app/navigation';
|
||||||
|
import { popupFormErrors } from '$lib/form-errors';
|
||||||
|
import type { ActionData, PageData } from './$types';
|
||||||
|
import { OAUTH2_MAX_REDIRECTS, OAUTH2_MAX_URLS } from '$lib/constants';
|
||||||
|
import { displayMessage, clearMessages } from '$lib/stores/messages.store';
|
||||||
import Alert from '$lib/components/Alert.svelte';
|
import Alert from '$lib/components/Alert.svelte';
|
||||||
import Button from '$lib/components/Button.svelte';
|
import Button from '$lib/components/Button.svelte';
|
||||||
import AvatarCard from '$lib/components/avatar/AvatarCard.svelte';
|
import AvatarCard from '$lib/components/avatar/AvatarCard.svelte';
|
||||||
@ -9,14 +18,7 @@
|
|||||||
import FormControl from '$lib/components/form/FormControl.svelte';
|
import FormControl from '$lib/components/form/FormControl.svelte';
|
||||||
import FormSection from '$lib/components/form/FormSection.svelte';
|
import FormSection from '$lib/components/form/FormSection.svelte';
|
||||||
import FormWrapper from '$lib/components/form/FormWrapper.svelte';
|
import FormWrapper from '$lib/components/form/FormWrapper.svelte';
|
||||||
import FormErrors from '$lib/components/form/FormErrors.svelte';
|
|
||||||
import ActionButton from '$lib/components/ActionButton.svelte';
|
import ActionButton from '$lib/components/ActionButton.svelte';
|
||||||
import type { ActionData, PageData } from './$types';
|
|
||||||
import { t } from '$lib/i18n';
|
|
||||||
import { page } from '$app/state';
|
|
||||||
import { writable } from 'svelte/store';
|
|
||||||
import { env } from '$env/dynamic/public';
|
|
||||||
import { OAUTH2_MAX_REDIRECTS, OAUTH2_MAX_URLS } from '$lib/constants';
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
data: PageData;
|
data: PageData;
|
||||||
@ -47,6 +49,14 @@
|
|||||||
let uuidPrefix = $derived(data.details.client_id.split('-')[0] + ':');
|
let uuidPrefix = $derived(data.details.client_id.split('-')[0] + ':');
|
||||||
|
|
||||||
const jwkPlaceholder = JSON.stringify([{ kty: 'RSA', n: '...', e: 'AQAB' }]);
|
const jwkPlaceholder = JSON.stringify([{ kty: 'RSA', n: '...', e: 'AQAB' }]);
|
||||||
|
|
||||||
|
$effect(() => popupFormErrors(form, 'admin.oauth2.errors'));
|
||||||
|
$effect(() => {
|
||||||
|
if (form?.errors && !form.errors.length) {
|
||||||
|
displayMessage({ type: 'success', text: get(t)('admin.success') });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
onNavigate(() => clearMessages());
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
@ -76,10 +86,6 @@
|
|||||||
|
|
||||||
<form action="?/update" method="POST">
|
<form action="?/update" method="POST">
|
||||||
<FormWrapper>
|
<FormWrapper>
|
||||||
{#if form?.errors && !form.errors.length}
|
|
||||||
<Alert type="success">{$t('admin.oauth2.success')}</Alert>
|
|
||||||
{/if}
|
|
||||||
<FormErrors errors={form?.errors || []} prefix="admin.oauth2.errors" />
|
|
||||||
<FormSection>
|
<FormSection>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<label for="client-title">{$t('admin.oauth2.clientTitle')}</label>
|
<label for="client-title">{$t('admin.oauth2.clientTitle')}</label>
|
||||||
|
@ -2,18 +2,23 @@
|
|||||||
import FormControl from '$lib/components/form/FormControl.svelte';
|
import FormControl from '$lib/components/form/FormControl.svelte';
|
||||||
import FormSection from '$lib/components/form/FormSection.svelte';
|
import FormSection from '$lib/components/form/FormSection.svelte';
|
||||||
import FormWrapper from '$lib/components/form/FormWrapper.svelte';
|
import FormWrapper from '$lib/components/form/FormWrapper.svelte';
|
||||||
import FormErrors from '$lib/components/form/FormErrors.svelte';
|
|
||||||
import Button from '$lib/components/Button.svelte';
|
import Button from '$lib/components/Button.svelte';
|
||||||
import { t } from '$lib/i18n';
|
import { t } from '$lib/i18n';
|
||||||
import type { ActionData } from './$types';
|
import type { ActionData } from './$types';
|
||||||
import { env } from '$env/dynamic/public';
|
import { env } from '$env/dynamic/public';
|
||||||
import SplitView from '$lib/components/container/SplitView.svelte';
|
import SplitView from '$lib/components/container/SplitView.svelte';
|
||||||
|
import { onNavigate } from '$app/navigation';
|
||||||
|
import { popupFormErrors } from '$lib/form-errors';
|
||||||
|
import { clearMessages } from '$lib/stores/messages.store';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
form: ActionData;
|
form: ActionData;
|
||||||
}
|
}
|
||||||
|
|
||||||
let { form }: Props = $props();
|
let { form }: Props = $props();
|
||||||
|
|
||||||
|
$effect(() => popupFormErrors(form, 'admin.oauth2.errors'));
|
||||||
|
onNavigate(() => clearMessages());
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
@ -25,8 +30,6 @@
|
|||||||
<SplitView>
|
<SplitView>
|
||||||
<form action="" method="POST">
|
<form action="" method="POST">
|
||||||
<FormWrapper>
|
<FormWrapper>
|
||||||
<FormErrors errors={form?.errors || []} prefix="admin.oauth2.errors" />
|
|
||||||
|
|
||||||
<FormSection required>
|
<FormSection required>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<label for="form-title">{$t('admin.oauth2.clientTitle')}</label>
|
<label for="form-title">{$t('admin.oauth2.clientTitle')}</label>
|
||||||
|
@ -9,10 +9,12 @@
|
|||||||
import { t } from '$lib/i18n';
|
import { t } from '$lib/i18n';
|
||||||
import type { ActionData, PageData } from './$types';
|
import type { ActionData, PageData } from './$types';
|
||||||
import AdminPrivilegesSelect from '$lib/components/admin/AdminPrivilegesSelect.svelte';
|
import AdminPrivilegesSelect from '$lib/components/admin/AdminPrivilegesSelect.svelte';
|
||||||
import FormErrors from '$lib/components/form/FormErrors.svelte';
|
|
||||||
import { env } from '$env/dynamic/public';
|
import { env } from '$env/dynamic/public';
|
||||||
import ActionButton from '$lib/components/ActionButton.svelte';
|
import ActionButton from '$lib/components/ActionButton.svelte';
|
||||||
import Alert from '$lib/components/Alert.svelte';
|
import { onNavigate } from '$app/navigation';
|
||||||
|
import { popupFormErrors } from '$lib/form-errors';
|
||||||
|
import { displayMessage, clearMessages } from '$lib/stores/messages.store';
|
||||||
|
import { get } from 'svelte/store';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
data: PageData;
|
data: PageData;
|
||||||
@ -20,6 +22,14 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
let { data, form }: Props = $props();
|
let { data, form }: Props = $props();
|
||||||
|
|
||||||
|
$effect(() => popupFormErrors(form, 'admin.users.errors'));
|
||||||
|
$effect(() => {
|
||||||
|
if (form?.errors && !form.errors.length) {
|
||||||
|
displayMessage({ type: 'success', text: get(t)('admin.success') });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
onNavigate(() => clearMessages());
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
@ -41,11 +51,6 @@
|
|||||||
</ColumnView>
|
</ColumnView>
|
||||||
</AvatarCard>
|
</AvatarCard>
|
||||||
|
|
||||||
{#if form?.errors && !form.errors.length}
|
|
||||||
<Alert type="success">{$t('admin.oauth2.success')}</Alert>
|
|
||||||
{/if}
|
|
||||||
<FormErrors errors={form?.errors || []} prefix="admin.users.errors" />
|
|
||||||
|
|
||||||
<form action="?/update" method="POST">
|
<form action="?/update" method="POST">
|
||||||
<FormWrapper>
|
<FormWrapper>
|
||||||
<FormSection>
|
<FormSection>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user