diff --git a/.env.example b/.env.example index 6aeb1ee..a7d36c8 100644 --- a/.env.example +++ b/.env.example @@ -1,3 +1,6 @@ +# Current environment - production or development +NODE_ENV=production + # Front-end public URL, without leading slash PUBLIC_URL=http://localhost:5173 @@ -23,6 +26,7 @@ SESSION_SECURE=true JWT_ALGORITHM=RS256 JWT_EXPIRATION=7d JWT_ISSUER=http://localhost:5173 +JWT_KEYLENGTH=2048 # SMTP settings EMAIL_ENABLED=true @@ -32,6 +36,7 @@ EMAIL_SMTP_PORT=587 EMAIL_SMTP_SECURE=false EMAIL_SMTP_USER= EMAIL_SMTP_PASS= +EMAIL_ADMIN= # Enable new account registrations REGISTRATIONS=true diff --git a/README.md b/README.md index 7578a80..516fdee 100644 --- a/README.md +++ b/README.md @@ -12,10 +12,8 @@ This is a SvelteKit-powered authentication service. 1. Clone the repository. 2. Install dependenices - `npm install`. 3. Configure the environment - `cp .env.example .env`. -4. Generate secrets and stuff: +4. Generate secrets: 1. Session secret - `node -e 'console.log(require("crypto").randomBytes(16).toString("hex"))'`. 2. Challenge secret - `node -e 'console.log(require("crypto").randomBytes(32).toString("hex"))'`. - 3. Generate JWT keys in the `private` directory - `openssl genpkey -out jwt.private.pem -algorithm RSA -pkeyopt rsa_keygen_bits:2048`. - 4. Also make the public key - `openssl rsa -in jwt.private.pem -pubout -outform PEM -out jwt.public.pem`. 5. Build the application - `npm run build`. 6. Run the application - `node -r dotenv/config build`. diff --git a/package-lock.json b/package-lock.json index e752f4f..a8c2b9c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,13 +10,14 @@ "dependencies": { "@sveltejs/adapter-node": "^5.2.9", "bcryptjs": "^2.4.3", + "chalk": "^5.4.0", "cropperjs": "^1.6.2", - "dotenv": "^16.4.5", - "drizzle-orm": "^0.36.4", + "dotenv": "^16.4.7", + "drizzle-orm": "^0.38.0", "image-size": "^1.1.1", "jose": "^5.9.6", "mime-types": "^2.1.35", - "mysql2": "^3.11.4", + "mysql2": "^3.11.5", "nodemailer": "^6.9.16", "otplib": "^12.0.1", "qrcode": "^1.5.4", @@ -27,28 +28,28 @@ "vite-plugin-mkcert": "^1.17.6" }, "devDependencies": { - "@sveltejs/kit": "^2.8.2", - "@sveltejs/vite-plugin-svelte": "^4.0.1", + "@sveltejs/kit": "^2.9.0", + "@sveltejs/vite-plugin-svelte": "^5.0.1", "@types/bcryptjs": "^2.4.6", "@types/eslint": "^9.6.1", "@types/mime-types": "^2.1.4", - "@types/node": "^22.9.3", + "@types/node": "^22.10.1", "@types/nodemailer": "^6.4.17", "@types/qrcode": "^1.5.5", "@types/uuid": "^10.0.0", - "@typescript-eslint/eslint-plugin": "^8.15.0", - "@typescript-eslint/parser": "^8.15.0", - "drizzle-kit": "^0.28.1", - "eslint": "^9.15.0", + "@typescript-eslint/eslint-plugin": "^8.17.0", + "@typescript-eslint/parser": "^8.17.0", + "drizzle-kit": "^0.30.0", + "eslint": "^9.16.0", "eslint-config-prettier": "^9.1.0", - "eslint-plugin-svelte": "^2.46.0", - "prettier": "^3.3.3", + "eslint-plugin-svelte": "^2.46.1", + "prettier": "^3.4.2", "prettier-plugin-svelte": "^3.3.2", - "svelte": "^5.2.7", - "svelte-check": "^4.1.0", + "svelte": "^5.9.1", + "svelte-check": "^4.1.1", "tslib": "^2.8.1", "typescript": "^5.7.2", - "vite": "^5.4.11" + "vite": "^6.0.3" } }, "node_modules/@ampproject/remapping": { @@ -480,348 +481,387 @@ } }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", - "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.24.0.tgz", + "integrity": "sha512-WtKdFM7ls47zkKHFVzMz8opM7LkcsIp9amDUBIAWirg70RM71WRSjdILPsY5Uv1D42ZpUfaPILDlfactHgsRkw==", "cpu": [ "ppc64" ], + "license": "MIT", "optional": true, "os": [ "aix" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/android-arm": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", - "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.24.0.tgz", + "integrity": "sha512-arAtTPo76fJ/ICkXWetLCc9EwEHKaeya4vMrReVlEIUCAUncH7M4bhMQ+M9Vf+FFOZJdTNMXNBrWwW+OXWpSew==", "cpu": [ "arm" ], + "license": "MIT", "optional": true, "os": [ "android" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/android-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", - "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.24.0.tgz", + "integrity": "sha512-Vsm497xFM7tTIPYK9bNTYJyF/lsP590Qc1WxJdlB6ljCbdZKU9SY8i7+Iin4kyhV/KV5J2rOKsBQbB77Ab7L/w==", "cpu": [ "arm64" ], + "license": "MIT", "optional": true, "os": [ "android" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/android-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", - "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.24.0.tgz", + "integrity": "sha512-t8GrvnFkiIY7pa7mMgJd7p8p8qqYIz1NYiAoKc75Zyv73L3DZW++oYMSHPRarcotTKuSs6m3hTOa5CKHaS02TQ==", "cpu": [ "x64" ], + "license": "MIT", "optional": true, "os": [ "android" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", - "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.24.0.tgz", + "integrity": "sha512-CKyDpRbK1hXwv79soeTJNHb5EiG6ct3efd/FTPdzOWdbZZfGhpbcqIpiD0+vwmpu0wTIL97ZRPZu8vUt46nBSw==", "cpu": [ "arm64" ], + "license": "MIT", "optional": true, "os": [ "darwin" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", - "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.24.0.tgz", + "integrity": "sha512-rgtz6flkVkh58od4PwTRqxbKH9cOjaXCMZgWD905JOzjFKW+7EiUObfd/Kav+A6Gyud6WZk9w+xu6QLytdi2OA==", "cpu": [ "x64" ], + "license": "MIT", "optional": true, "os": [ "darwin" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", - "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.24.0.tgz", + "integrity": "sha512-6Mtdq5nHggwfDNLAHkPlyLBpE5L6hwsuXZX8XNmHno9JuL2+bg2BX5tRkwjyfn6sKbxZTq68suOjgWqCicvPXA==", "cpu": [ "arm64" ], + "license": "MIT", "optional": true, "os": [ "freebsd" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", - "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.24.0.tgz", + "integrity": "sha512-D3H+xh3/zphoX8ck4S2RxKR6gHlHDXXzOf6f/9dbFt/NRBDIE33+cVa49Kil4WUjxMGW0ZIYBYtaGCa2+OsQwQ==", "cpu": [ "x64" ], + "license": "MIT", "optional": true, "os": [ "freebsd" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-arm": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", - "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.24.0.tgz", + "integrity": "sha512-gJKIi2IjRo5G6Glxb8d3DzYXlxdEj2NlkixPsqePSZMhLudqPhtZ4BUrpIuTjJYXxvF9njql+vRjB2oaC9XpBw==", "cpu": [ "arm" ], + "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", - "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.24.0.tgz", + "integrity": "sha512-TDijPXTOeE3eaMkRYpcy3LarIg13dS9wWHRdwYRnzlwlA370rNdZqbcp0WTyyV/k2zSxfko52+C7jU5F9Tfj1g==", "cpu": [ "arm64" ], + "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", - "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.24.0.tgz", + "integrity": "sha512-K40ip1LAcA0byL05TbCQ4yJ4swvnbzHscRmUilrmP9Am7//0UjPreh4lpYzvThT2Quw66MhjG//20mrufm40mA==", "cpu": [ "ia32" ], + "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", - "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.24.0.tgz", + "integrity": "sha512-0mswrYP/9ai+CU0BzBfPMZ8RVm3RGAN/lmOMgW4aFUSOQBjA31UP8Mr6DDhWSuMwj7jaWOT0p0WoZ6jeHhrD7g==", "cpu": [ "loong64" ], + "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", - "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.24.0.tgz", + "integrity": "sha512-hIKvXm0/3w/5+RDtCJeXqMZGkI2s4oMUGj3/jM0QzhgIASWrGO5/RlzAzm5nNh/awHE0A19h/CvHQe6FaBNrRA==", "cpu": [ "mips64el" ], + "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", - "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.24.0.tgz", + "integrity": "sha512-HcZh5BNq0aC52UoocJxaKORfFODWXZxtBaaZNuN3PUX3MoDsChsZqopzi5UupRhPHSEHotoiptqikjN/B77mYQ==", "cpu": [ "ppc64" ], + "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", - "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.24.0.tgz", + "integrity": "sha512-bEh7dMn/h3QxeR2KTy1DUszQjUrIHPZKyO6aN1X4BCnhfYhuQqedHaa5MxSQA/06j3GpiIlFGSsy1c7Gf9padw==", "cpu": [ "riscv64" ], + "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", - "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.24.0.tgz", + "integrity": "sha512-ZcQ6+qRkw1UcZGPyrCiHHkmBaj9SiCD8Oqd556HldP+QlpUIe2Wgn3ehQGVoPOvZvtHm8HPx+bH20c9pvbkX3g==", "cpu": [ "s390x" ], + "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", - "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.24.0.tgz", + "integrity": "sha512-vbutsFqQ+foy3wSSbmjBXXIJ6PL3scghJoM8zCL142cGaZKAdCZHyf+Bpu/MmX9zT9Q0zFBVKb36Ma5Fzfa8xA==", "cpu": [ "x64" ], + "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", - "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.24.0.tgz", + "integrity": "sha512-hjQ0R/ulkO8fCYFsG0FZoH+pWgTTDreqpqY7UnQntnaKv95uP5iW3+dChxnx7C3trQQU40S+OgWhUVwCjVFLvg==", "cpu": [ "x64" ], + "license": "MIT", "optional": true, "os": [ "netbsd" ], "engines": { - "node": ">=12" + "node": ">=18" } }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", - "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.24.0.tgz", + "integrity": "sha512-MD9uzzkPQbYehwcN583yx3Tu5M8EIoTD+tUgKF982WYL9Pf5rKy9ltgD0eUgs8pvKnmizxjXZyLt0z6DC3rRXg==", "cpu": [ - "x64" + "arm64" ], + "license": "MIT", "optional": true, "os": [ "openbsd" ], "engines": { - "node": ">=12" + "node": ">=18" } }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", - "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "node_modules/@esbuild/openbsd-x64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.24.0.tgz", + "integrity": "sha512-4ir0aY1NGUhIC1hdoCzr1+5b43mw99uNwVzhIq1OY3QcEwPDO3B7WNXBzaKY5Nsf1+N11i1eOfFcq+D/gOS15Q==", "cpu": [ "x64" ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.24.0.tgz", + "integrity": "sha512-jVzdzsbM5xrotH+W5f1s+JtUy1UWgjU0Cf4wMvffTB8m6wP5/kx0KiaLHlbJO+dMgtxKV8RQ/JvtlFcdZ1zCPA==", + "cpu": [ + "x64" + ], + "license": "MIT", "optional": true, "os": [ "sunos" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", - "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.24.0.tgz", + "integrity": "sha512-iKc8GAslzRpBytO2/aN3d2yb2z8XTVfNV0PjGlCxKo5SgWmNXx82I/Q3aG1tFfS+A2igVCY97TJ8tnYwpUWLCA==", "cpu": [ "arm64" ], + "license": "MIT", "optional": true, "os": [ "win32" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", - "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.24.0.tgz", + "integrity": "sha512-vQW36KZolfIudCcTnaTpmLQ24Ha1RjygBo39/aLkM2kmjkWmZGEJ5Gn9l5/7tzXA42QGIoWbICfg6KLLkIw6yw==", "cpu": [ "ia32" ], + "license": "MIT", "optional": true, "os": [ "win32" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/win32-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", - "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.24.0.tgz", + "integrity": "sha512-7IAFPrjSQIJrGsK6flwg7NFmwBoSTyF3rl7If0hNUFQU4ilTsEPL6GuMuU9BfIWVVGuRnuIidkSMC+c0Otu8IA==", "cpu": [ "x64" ], + "license": "MIT", "optional": true, "os": [ "win32" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@eslint-community/eslint-utils": { @@ -978,9 +1018,9 @@ } }, "node_modules/@eslint/js": { - "version": "9.15.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.15.0.tgz", - "integrity": "sha512-tMTqrY+EzbXmKJR5ToI8lxu7jaN5EdmrBFJpQk5JmSlyLsx6o4t27r883K5xsLuCYCpfKBCGswMSWXsM+jB7lg==", + "version": "9.16.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.16.0.tgz", + "integrity": "sha512-tw2HxzQkrbeuvyj1tG2Yqq+0H9wGoI2IMk4EOsQeX+vmd75FtJAzf+gTA69WF+baUKRYQ3x2kbLE08js5OsTVg==", "dev": true, "license": "MIT", "engines": { @@ -1751,16 +1791,16 @@ } }, "node_modules/@sveltejs/kit": { - "version": "2.8.2", - "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.8.2.tgz", - "integrity": "sha512-c9My0AnojYtaa96XDAcxcMUdMd3iIhWfrj6BLNtOFz55lMtA/Jima54ZLcYcvfMqei3c86fGRXYa2aIHO+vzFg==", + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.9.0.tgz", + "integrity": "sha512-W3E7ed3ChB6kPqRs2H7tcHp+Z7oiTFC6m+lLyAQQuyXeqw6LdNuuwEUla+5VM0OGgqQD+cYD6+7Xq80vVm17Vg==", "hasInstallScript": true, "license": "MIT", "dependencies": { "@types/cookie": "^0.6.0", "cookie": "^0.6.0", "devalue": "^5.1.0", - "esm-env": "^1.0.0", + "esm-env": "^1.2.1", "import-meta-resolve": "^4.1.0", "kleur": "^4.1.5", "magic-string": "^0.30.5", @@ -1777,36 +1817,36 @@ "node": ">=18.13" }, "peerDependencies": { - "@sveltejs/vite-plugin-svelte": "^3.0.0 || ^4.0.0-next.1", + "@sveltejs/vite-plugin-svelte": "^3.0.0 || ^4.0.0-next.1 || ^5.0.0", "svelte": "^4.0.0 || ^5.0.0-next.0", - "vite": "^5.0.3" + "vite": "^5.0.3 || ^6.0.0" } }, "node_modules/@sveltejs/vite-plugin-svelte": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-4.0.1.tgz", - "integrity": "sha512-prXoAE/GleD2C4pKgHa9vkdjpzdYwCSw/kmjw6adIyu0vk5YKCfqIztkLg10m+kOYnzZu3bb0NaPTxlWre2a9Q==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-5.0.1.tgz", + "integrity": "sha512-D5l5+STmywGoLST07T9mrqqFFU+xgv5fqyTWM+VbxTvQ6jujNn4h3lQNCvlwVYs4Erov8i0K5Rwr3LQtmBYmBw==", "license": "MIT", "dependencies": { - "@sveltejs/vite-plugin-svelte-inspector": "^3.0.0-next.0||^3.0.0", + "@sveltejs/vite-plugin-svelte-inspector": "^4.0.0", "debug": "^4.3.7", "deepmerge": "^4.3.1", "kleur": "^4.1.5", - "magic-string": "^0.30.12", + "magic-string": "^0.30.13", "vitefu": "^1.0.3" }, "engines": { "node": "^18.0.0 || ^20.0.0 || >=22" }, "peerDependencies": { - "svelte": "^5.0.0-next.96 || ^5.0.0", - "vite": "^5.0.0" + "svelte": "^5.0.0", + "vite": "^6.0.0" } }, "node_modules/@sveltejs/vite-plugin-svelte-inspector": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte-inspector/-/vite-plugin-svelte-inspector-3.0.1.tgz", - "integrity": "sha512-2CKypmj1sM4GE7HjllT7UKmo4Q6L5xFRd7VMGEWhYnZ+wc6AUVU01IBd7yUi6WnFndEwWoMNOd6e8UjoN0nbvQ==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte-inspector/-/vite-plugin-svelte-inspector-4.0.1.tgz", + "integrity": "sha512-J/Nmb2Q2y7mck2hyCX4ckVHcR5tu2J+MtBEQqpDrrgELZ2uvraQcK/ioCV61AqkdXFgriksOKIceDcQmqnGhVw==", "license": "MIT", "dependencies": { "debug": "^4.3.7" @@ -1815,9 +1855,9 @@ "node": "^18.0.0 || ^20.0.0 || >=22" }, "peerDependencies": { - "@sveltejs/vite-plugin-svelte": "^4.0.0-next.0||^4.0.0", - "svelte": "^5.0.0-next.96 || ^5.0.0", - "vite": "^5.0.0" + "@sveltejs/vite-plugin-svelte": "^5.0.0", + "svelte": "^5.0.0", + "vite": "^6.0.0" } }, "node_modules/@sveltekit-i18n/base": { @@ -1874,13 +1914,13 @@ "dev": true }, "node_modules/@types/node": { - "version": "22.9.3", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.9.3.tgz", - "integrity": "sha512-F3u1fs/fce3FFk+DAxbxc78DF8x0cY09RRL8GnXLmkJ1jvx3TtPdWoTT5/NiYfI5ASqXBmfqJi9dZ3gxMx4lzw==", + "version": "22.10.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.1.tgz", + "integrity": "sha512-qKgsUwfHZV2WCWLAnVP1JqnpE6Im6h3Y0+fYgMTasNQ7V++CBX5OT1as0g0f+OyubbFqhf6XVNIsmN4IIhEgGQ==", "devOptional": true, "license": "MIT", "dependencies": { - "undici-types": "~6.19.8" + "undici-types": "~6.20.0" } }, "node_modules/@types/nodemailer": { @@ -1916,17 +1956,17 @@ "license": "MIT" }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.15.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.15.0.tgz", - "integrity": "sha512-+zkm9AR1Ds9uLWN3fkoeXgFppaQ+uEVtfOV62dDmsy9QCNqlRHWNEck4yarvRNrvRcHQLGfqBNui3cimoz8XAg==", + "version": "8.17.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.17.0.tgz", + "integrity": "sha512-HU1KAdW3Tt8zQkdvNoIijfWDMvdSweFYm4hWh+KwhPstv+sCmWb89hCIP8msFm9N1R/ooh9honpSuvqKWlYy3w==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.15.0", - "@typescript-eslint/type-utils": "8.15.0", - "@typescript-eslint/utils": "8.15.0", - "@typescript-eslint/visitor-keys": "8.15.0", + "@typescript-eslint/scope-manager": "8.17.0", + "@typescript-eslint/type-utils": "8.17.0", + "@typescript-eslint/utils": "8.17.0", + "@typescript-eslint/visitor-keys": "8.17.0", "graphemer": "^1.4.0", "ignore": "^5.3.1", "natural-compare": "^1.4.0", @@ -1950,16 +1990,16 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.15.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.15.0.tgz", - "integrity": "sha512-7n59qFpghG4uazrF9qtGKBZXn7Oz4sOMm8dwNWDQY96Xlm2oX67eipqcblDj+oY1lLCbf1oltMZFpUso66Kl1A==", + "version": "8.17.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.17.0.tgz", + "integrity": "sha512-Drp39TXuUlD49F7ilHHCG7TTg8IkA+hxCuULdmzWYICxGXvDXmDmWEjJYZQYgf6l/TFfYNE167m7isnc3xlIEg==", "dev": true, "license": "BSD-2-Clause", "dependencies": { - "@typescript-eslint/scope-manager": "8.15.0", - "@typescript-eslint/types": "8.15.0", - "@typescript-eslint/typescript-estree": "8.15.0", - "@typescript-eslint/visitor-keys": "8.15.0", + "@typescript-eslint/scope-manager": "8.17.0", + "@typescript-eslint/types": "8.17.0", + "@typescript-eslint/typescript-estree": "8.17.0", + "@typescript-eslint/visitor-keys": "8.17.0", "debug": "^4.3.4" }, "engines": { @@ -1979,14 +2019,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.15.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.15.0.tgz", - "integrity": "sha512-QRGy8ADi4J7ii95xz4UoiymmmMd/zuy9azCaamnZ3FM8T5fZcex8UfJcjkiEZjJSztKfEBe3dZ5T/5RHAmw2mA==", + "version": "8.17.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.17.0.tgz", + "integrity": "sha512-/ewp4XjvnxaREtqsZjF4Mfn078RD/9GmiEAtTeLQ7yFdKnqwTOgRMSvFz4et9U5RiJQ15WTGXPLj89zGusvxBg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.15.0", - "@typescript-eslint/visitor-keys": "8.15.0" + "@typescript-eslint/types": "8.17.0", + "@typescript-eslint/visitor-keys": "8.17.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1997,14 +2037,14 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.15.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.15.0.tgz", - "integrity": "sha512-UU6uwXDoI3JGSXmcdnP5d8Fffa2KayOhUUqr/AiBnG1Gl7+7ut/oyagVeSkh7bxQ0zSXV9ptRh/4N15nkCqnpw==", + "version": "8.17.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.17.0.tgz", + "integrity": "sha512-q38llWJYPd63rRnJ6wY/ZQqIzPrBCkPdpIsaCfkR3Q4t3p6sb422zougfad4TFW9+ElIFLVDzWGiGAfbb/v2qw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/typescript-estree": "8.15.0", - "@typescript-eslint/utils": "8.15.0", + "@typescript-eslint/typescript-estree": "8.17.0", + "@typescript-eslint/utils": "8.17.0", "debug": "^4.3.4", "ts-api-utils": "^1.3.0" }, @@ -2025,9 +2065,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.15.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.15.0.tgz", - "integrity": "sha512-n3Gt8Y/KyJNe0S3yDCD2RVKrHBC4gTUcLTebVBXacPy091E6tNspFLKRXlk3hwT4G55nfr1n2AdFqi/XMxzmPQ==", + "version": "8.17.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.17.0.tgz", + "integrity": "sha512-gY2TVzeve3z6crqh2Ic7Cr+CAv6pfb0Egee7J5UAVWCpVvDI/F71wNfolIim4FE6hT15EbpZFVUj9j5i38jYXA==", "dev": true, "license": "MIT", "engines": { @@ -2039,14 +2079,14 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.15.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.15.0.tgz", - "integrity": "sha512-1eMp2JgNec/niZsR7ioFBlsh/Fk0oJbhaqO0jRyQBMgkz7RrFfkqF9lYYmBoGBaSiLnu8TAPQTwoTUiSTUW9dg==", + "version": "8.17.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.17.0.tgz", + "integrity": "sha512-JqkOopc1nRKZpX+opvKqnM3XUlM7LpFMD0lYxTqOTKQfCWAmxw45e3qlOCsEqEB2yuacujivudOFpCnqkBDNMw==", "dev": true, "license": "BSD-2-Clause", "dependencies": { - "@typescript-eslint/types": "8.15.0", - "@typescript-eslint/visitor-keys": "8.15.0", + "@typescript-eslint/types": "8.17.0", + "@typescript-eslint/visitor-keys": "8.17.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", @@ -2068,16 +2108,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.15.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.15.0.tgz", - "integrity": "sha512-k82RI9yGhr0QM3Dnq+egEpz9qB6Un+WLYhmoNcvl8ltMEededhh7otBVVIDDsEEttauwdY/hQoSsOv13lxrFzQ==", + "version": "8.17.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.17.0.tgz", + "integrity": "sha512-bQC8BnEkxqG8HBGKwG9wXlZqg37RKSMY7v/X8VEWD8JG2JuTHuNK0VFvMPMUKQcbk6B+tf05k+4AShAEtCtJ/w==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", - "@typescript-eslint/scope-manager": "8.15.0", - "@typescript-eslint/types": "8.15.0", - "@typescript-eslint/typescript-estree": "8.15.0" + "@typescript-eslint/scope-manager": "8.17.0", + "@typescript-eslint/types": "8.17.0", + "@typescript-eslint/typescript-estree": "8.17.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2096,13 +2136,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.15.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.15.0.tgz", - "integrity": "sha512-h8vYOulWec9LhpwfAdZf2bjr8xIp0KNKnpgqSz0qqYYKAW/QZKw3ktRndbiAtUz4acH4QLQavwZBYCc0wulA/Q==", + "version": "8.17.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.17.0.tgz", + "integrity": "sha512-1Hm7THLpO6ww5QU6H/Qp+AusUUl+z/CAm3cNZZ0jQvon9yicgO7Rwd+/WWRpMKLYV6p2UvdbR27c86rzCPpreg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.15.0", + "@typescript-eslint/types": "8.17.0", "eslint-visitor-keys": "^4.2.0" }, "engines": { @@ -2311,16 +2351,12 @@ } }, "node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.4.0.tgz", + "integrity": "sha512-ZkD35Mx92acjB2yNJgziGqT9oKHEOxjTBTDRpOsRWtdecL/0jM3z5kM/CTzHWvHIen1GvkM85p6TuFfDGfc8/Q==", + "license": "MIT", "engines": { - "node": ">=10" + "node": "^12.17.0 || ^14.13 || >=16.0.0" }, "funding": { "url": "https://github.com/chalk/chalk?sponsor=1" @@ -2507,9 +2543,10 @@ "integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==" }, "node_modules/dotenv": { - "version": "16.4.5", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz", - "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==", + "version": "16.4.7", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz", + "integrity": "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==", + "license": "BSD-2-Clause", "engines": { "node": ">=12" }, @@ -2518,9 +2555,9 @@ } }, "node_modules/drizzle-kit": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/drizzle-kit/-/drizzle-kit-0.28.1.tgz", - "integrity": "sha512-JimOV+ystXTWMgZkLHYHf2w3oS28hxiH1FR0dkmJLc7GHzdGJoJAQtQS5DRppnabsRZwE2U1F6CuezVBgmsBBQ==", + "version": "0.30.0", + "resolved": "https://registry.npmjs.org/drizzle-kit/-/drizzle-kit-0.30.0.tgz", + "integrity": "sha512-zAf0qg/BX2lV/Xip4igXrtbDv+Ub8S39U6qSOEGNvqcHrnmaQjS4mkkFZOUsgXRbuH56QUeWZxfnLHefrKCV5g==", "dev": true, "license": "MIT", "dependencies": { @@ -2940,13 +2977,13 @@ } }, "node_modules/drizzle-orm": { - "version": "0.36.4", - "resolved": "https://registry.npmjs.org/drizzle-orm/-/drizzle-orm-0.36.4.tgz", - "integrity": "sha512-1OZY3PXD7BR00Gl61UUOFihslDldfH4NFRH2MbP54Yxi0G/PKn4HfO65JYZ7c16DeP3SpM3Aw+VXVG9j6CRSXA==", + "version": "0.38.0", + "resolved": "https://registry.npmjs.org/drizzle-orm/-/drizzle-orm-0.38.0.tgz", + "integrity": "sha512-Ev1UYKOUxiVwLZZfos4hoNDMDkp313FqF0CHelhR+BeOdb76z/a6pKF6SgYQSrmj6EXH4cy6QIkmuxl2KIFW0A==", "license": "Apache-2.0", "peerDependencies": { "@aws-sdk/client-rds-data": ">=3", - "@cloudflare/workers-types": ">=3", + "@cloudflare/workers-types": ">=4", "@electric-sql/pglite": ">=0.2.0", "@libsql/client": ">=0.10.0", "@libsql/client-wasm": ">=0.10.0", @@ -3070,40 +3107,42 @@ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" }, "node_modules/esbuild": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", - "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.24.0.tgz", + "integrity": "sha512-FuLPevChGDshgSicjisSooU0cemp/sGXR841D5LHMB7mTVOmsEHcAxaH3irL53+8YDIeVNQEySh4DaYU/iuPqQ==", "hasInstallScript": true, + "license": "MIT", "bin": { "esbuild": "bin/esbuild" }, "engines": { - "node": ">=12" + "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.21.5", - "@esbuild/android-arm": "0.21.5", - "@esbuild/android-arm64": "0.21.5", - "@esbuild/android-x64": "0.21.5", - "@esbuild/darwin-arm64": "0.21.5", - "@esbuild/darwin-x64": "0.21.5", - "@esbuild/freebsd-arm64": "0.21.5", - "@esbuild/freebsd-x64": "0.21.5", - "@esbuild/linux-arm": "0.21.5", - "@esbuild/linux-arm64": "0.21.5", - "@esbuild/linux-ia32": "0.21.5", - "@esbuild/linux-loong64": "0.21.5", - "@esbuild/linux-mips64el": "0.21.5", - "@esbuild/linux-ppc64": "0.21.5", - "@esbuild/linux-riscv64": "0.21.5", - "@esbuild/linux-s390x": "0.21.5", - "@esbuild/linux-x64": "0.21.5", - "@esbuild/netbsd-x64": "0.21.5", - "@esbuild/openbsd-x64": "0.21.5", - "@esbuild/sunos-x64": "0.21.5", - "@esbuild/win32-arm64": "0.21.5", - "@esbuild/win32-ia32": "0.21.5", - "@esbuild/win32-x64": "0.21.5" + "@esbuild/aix-ppc64": "0.24.0", + "@esbuild/android-arm": "0.24.0", + "@esbuild/android-arm64": "0.24.0", + "@esbuild/android-x64": "0.24.0", + "@esbuild/darwin-arm64": "0.24.0", + "@esbuild/darwin-x64": "0.24.0", + "@esbuild/freebsd-arm64": "0.24.0", + "@esbuild/freebsd-x64": "0.24.0", + "@esbuild/linux-arm": "0.24.0", + "@esbuild/linux-arm64": "0.24.0", + "@esbuild/linux-ia32": "0.24.0", + "@esbuild/linux-loong64": "0.24.0", + "@esbuild/linux-mips64el": "0.24.0", + "@esbuild/linux-ppc64": "0.24.0", + "@esbuild/linux-riscv64": "0.24.0", + "@esbuild/linux-s390x": "0.24.0", + "@esbuild/linux-x64": "0.24.0", + "@esbuild/netbsd-x64": "0.24.0", + "@esbuild/openbsd-arm64": "0.24.0", + "@esbuild/openbsd-x64": "0.24.0", + "@esbuild/sunos-x64": "0.24.0", + "@esbuild/win32-arm64": "0.24.0", + "@esbuild/win32-ia32": "0.24.0", + "@esbuild/win32-x64": "0.24.0" } }, "node_modules/esbuild-register": { @@ -3131,9 +3170,9 @@ } }, "node_modules/eslint": { - "version": "9.15.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.15.0.tgz", - "integrity": "sha512-7CrWySmIibCgT1Os28lUU6upBshZ+GxybLOrmRzi08kS8MBuO8QA7pXEgYgY5W8vK3e74xv0lpjo9DbaGU9Rkw==", + "version": "9.16.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.16.0.tgz", + "integrity": "sha512-whp8mSQI4C8VXd+fLgSM0lh3UlmcFtVwUQjyKCFfsp+2ItAIYhlq/hqGahGqHE6cv9unM41VlqKk2VtKYR2TaA==", "dev": true, "license": "MIT", "dependencies": { @@ -3142,7 +3181,7 @@ "@eslint/config-array": "^0.19.0", "@eslint/core": "^0.9.0", "@eslint/eslintrc": "^3.2.0", - "@eslint/js": "9.15.0", + "@eslint/js": "9.16.0", "@eslint/plugin-kit": "^0.2.3", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", @@ -3219,9 +3258,9 @@ } }, "node_modules/eslint-plugin-svelte": { - "version": "2.46.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-svelte/-/eslint-plugin-svelte-2.46.0.tgz", - "integrity": "sha512-1A7iEMkzmCZ9/Iz+EAfOGYL8IoIG6zeKEq1SmpxGeM5SXmoQq+ZNnCpXFVJpsxPWYx8jIVGMerQMzX20cqUl0g==", + "version": "2.46.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-svelte/-/eslint-plugin-svelte-2.46.1.tgz", + "integrity": "sha512-7xYr2o4NID/f9OEYMqxsEQsCsj4KaMy4q5sANaKkAb6/QeCjYFxRmDm2S3YC3A3pl1kyPZ/syOx/i7LcWYSbIw==", "dev": true, "license": "MIT", "dependencies": { @@ -3291,6 +3330,23 @@ "concat-map": "0.0.1" } }, + "node_modules/eslint/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, "node_modules/eslint/node_modules/eslint-scope": { "version": "8.2.0", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.2.0.tgz", @@ -3352,9 +3408,10 @@ } }, "node_modules/esm-env": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.0.0.tgz", - "integrity": "sha512-Cf6VksWPsTuW01vU9Mk/3vRue91Zevka5SjyNf3nEpokFRuqt/KjUQoGAwq9qMmhpLTHmXzSIrFRw8zxWzmFBA==" + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.2.1.tgz", + "integrity": "sha512-U9JedYYjCnadUlXk7e1Kr+aENQhtUaoaV9+gZm1T8LC/YBAPJx3NSPIAurFOC0U5vrdSevnUJS2/wUVxGwPhng==", + "license": "MIT" }, "node_modules/espree": { "version": "9.6.1", @@ -3386,9 +3443,9 @@ } }, "node_modules/esrap": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/esrap/-/esrap-1.2.2.tgz", - "integrity": "sha512-F2pSJklxx1BlQIQgooczXCPHmcWpn6EsP5oo73LQfonG9fIlIENQ8vMmfGXeojP9MrkzUNAfyU5vdFlR9shHAw==", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/esrap/-/esrap-1.2.3.tgz", + "integrity": "sha512-ZlQmCCK+n7SGoqo7DnfKaP1sJZa49P01/dXzmjCASSo04p72w8EksT2NMK8CEX8DhKsfJXANioIw8VyHNsBfvQ==", "license": "MIT", "dependencies": { "@jridgewell/sourcemap-codec": "^1.4.15", @@ -3684,6 +3741,7 @@ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } @@ -4082,9 +4140,9 @@ "license": "MIT" }, "node_modules/mysql2": { - "version": "3.11.4", - "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.11.4.tgz", - "integrity": "sha512-Z2o3tY4Z8EvSRDwknaC40MdZ3+m0sKbpnXrShQLdxPrAvcNli7jLrD2Zd2IzsRMw4eK9Yle500FDmlkIqp+krg==", + "version": "3.11.5", + "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.11.5.tgz", + "integrity": "sha512-0XFu8rUmFN9vC0ME36iBvCUObftiMHItrYFhlCRvFWbLgpNqtC4Br/NmZX1HNCszxT0GGy5QtP+k3Q3eCJPaYA==", "license": "MIT", "dependencies": { "aws-ssl-profiles": "^1.1.1", @@ -4344,6 +4402,16 @@ } } }, + "node_modules/postcss-load-config/node_modules/yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 6" + } + }, "node_modules/postcss-safe-parser": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/postcss-safe-parser/-/postcss-safe-parser-6.0.0.tgz", @@ -4411,9 +4479,9 @@ } }, "node_modules/prettier": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.3.tgz", - "integrity": "sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==", + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.4.2.tgz", + "integrity": "sha512-e9MewbtFo+Fevyuxn/4rrcDAaq0IYxPGLvObpQjiZBMAzB9IGmzlnG9RZy3FFas+eBMu2vA0CszMeduow5dIuQ==", "dev": true, "license": "MIT", "bin": { @@ -4792,6 +4860,7 @@ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, + "license": "MIT", "dependencies": { "has-flag": "^4.0.0" }, @@ -4812,9 +4881,9 @@ } }, "node_modules/svelte": { - "version": "5.2.7", - "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.2.7.tgz", - "integrity": "sha512-cEhPGuLHiH2+Z8B1FwQgiZJgA39uUmJR4516TKrM5zrp0/cuwJkfhUfcTxhAkznanAF5fXUKzvYR4o+Ksx3ZCQ==", + "version": "5.9.1", + "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.9.1.tgz", + "integrity": "sha512-2iWB8J9j/tZqEfuplxSt7gi/iA0Fg7VFwWATxDmG/efttLI05hwLWAKIx4ltv+FtvJIhRGs28rJ0e8uY8e3LWg==", "license": "MIT", "dependencies": { "@ampproject/remapping": "^2.3.0", @@ -4824,8 +4893,8 @@ "acorn-typescript": "^1.4.13", "aria-query": "^5.3.1", "axobject-query": "^4.1.0", - "esm-env": "^1.0.0", - "esrap": "^1.2.2", + "esm-env": "^1.2.1", + "esrap": "^1.2.3", "is-reference": "^3.0.3", "locate-character": "^3.0.0", "magic-string": "^0.30.11", @@ -4836,9 +4905,9 @@ } }, "node_modules/svelte-check": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-4.1.0.tgz", - "integrity": "sha512-AflEZYqI578KuDZcpcorPSf597LStxlkN7XqXi38u09zlHODVKd7c+7OuubGzbhgGRUqNTdQCZ+Ga96iRXEf2g==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-4.1.1.tgz", + "integrity": "sha512-NfaX+6Qtc8W/CyVGS/F7/XdiSSyXz+WGYA9ZWV3z8tso14V2vzjfXviKaTFEzB7g8TqfgO2FOzP6XT4ApSTUTw==", "dev": true, "license": "MIT", "dependencies": { @@ -5035,9 +5104,9 @@ } }, "node_modules/undici-types": { - "version": "6.19.8", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", - "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", "devOptional": true, "license": "MIT" }, @@ -5078,20 +5147,20 @@ } }, "node_modules/vite": { - "version": "5.4.11", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.11.tgz", - "integrity": "sha512-c7jFQRklXua0mTzneGW9QVyxFjUgwcihC4bXEtujIo2ouWCe1Ajt/amn2PCxYnhYfd5k09JX3SB7OYWFKYqj8Q==", + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.0.3.tgz", + "integrity": "sha512-Cmuo5P0ENTN6HxLSo6IHsjCLn/81Vgrp81oaiFFMRa8gGDj5xEjIcEpf2ZymZtZR8oU0P2JX5WuUp/rlXcHkAw==", "license": "MIT", "dependencies": { - "esbuild": "^0.21.3", - "postcss": "^8.4.43", - "rollup": "^4.20.0" + "esbuild": "^0.24.0", + "postcss": "^8.4.49", + "rollup": "^4.23.0" }, "bin": { "vite": "bin/vite.js" }, "engines": { - "node": "^18.0.0 || >=20.0.0" + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" }, "funding": { "url": "https://github.com/vitejs/vite?sponsor=1" @@ -5100,19 +5169,25 @@ "fsevents": "~2.3.3" }, "peerDependencies": { - "@types/node": "^18.0.0 || >=20.0.0", + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", - "terser": "^5.4.0" + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" }, "peerDependenciesMeta": { "@types/node": { "optional": true }, + "jiti": { + "optional": true + }, "less": { "optional": true }, @@ -5133,6 +5208,12 @@ }, "terser": { "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true } } }, @@ -5226,12 +5307,17 @@ "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==" }, "node_modules/yaml": { - "version": "1.10.2", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", - "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", - "dev": true, + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.6.1.tgz", + "integrity": "sha512-7r0XPzioN/Q9kXBro/XPnA6kznR73DHq+GXh5ON7ZozRO6aMjbmiBuKste2wslTFkC5d1dw0GooOCepZXJ2SAg==", + "license": "ISC", + "optional": true, + "peer": true, + "bin": { + "yaml": "bin.mjs" + }, "engines": { - "node": ">= 6" + "node": ">= 14" } }, "node_modules/yargs": { diff --git a/package.json b/package.json index f5bb08c..c99767d 100644 --- a/package.json +++ b/package.json @@ -12,40 +12,41 @@ "format": "prettier --write ." }, "devDependencies": { - "@sveltejs/kit": "^2.8.2", - "@sveltejs/vite-plugin-svelte": "^4.0.1", + "@sveltejs/kit": "^2.9.0", + "@sveltejs/vite-plugin-svelte": "^5.0.1", "@types/bcryptjs": "^2.4.6", "@types/eslint": "^9.6.1", "@types/mime-types": "^2.1.4", - "@types/node": "^22.9.3", + "@types/node": "^22.10.1", "@types/nodemailer": "^6.4.17", "@types/qrcode": "^1.5.5", "@types/uuid": "^10.0.0", - "@typescript-eslint/eslint-plugin": "^8.15.0", - "@typescript-eslint/parser": "^8.15.0", - "drizzle-kit": "^0.28.1", - "eslint": "^9.15.0", + "@typescript-eslint/eslint-plugin": "^8.17.0", + "@typescript-eslint/parser": "^8.17.0", + "drizzle-kit": "^0.30.0", + "eslint": "^9.16.0", "eslint-config-prettier": "^9.1.0", - "eslint-plugin-svelte": "^2.46.0", - "prettier": "^3.3.3", + "eslint-plugin-svelte": "^2.46.1", + "prettier": "^3.4.2", "prettier-plugin-svelte": "^3.3.2", - "svelte": "^5.2.7", - "svelte-check": "^4.1.0", + "svelte": "^5.9.1", + "svelte-check": "^4.1.1", "tslib": "^2.8.1", "typescript": "^5.7.2", - "vite": "^5.4.11" + "vite": "^6.0.3" }, "type": "module", "dependencies": { "@sveltejs/adapter-node": "^5.2.9", "bcryptjs": "^2.4.3", + "chalk": "^5.4.0", "cropperjs": "^1.6.2", - "dotenv": "^16.4.5", - "drizzle-orm": "^0.36.4", + "dotenv": "^16.4.7", + "drizzle-orm": "^0.38.0", "image-size": "^1.1.1", "jose": "^5.9.6", "mime-types": "^2.1.35", - "mysql2": "^3.11.4", + "mysql2": "^3.11.5", "nodemailer": "^6.9.16", "otplib": "^12.0.1", "qrcode": "^1.5.4", diff --git a/src/hooks.server.ts b/src/hooks.server.ts index 4febc45..093095c 100644 --- a/src/hooks.server.ts +++ b/src/hooks.server.ts @@ -3,25 +3,34 @@ import { csrf } from '$lib/server/csrf'; import { DB } from '$lib/server/drizzle'; import { runSeeds } from '$lib/server/drizzle/seeds'; import { JWT } from '$lib/server/jwt'; +import { Logger, LogLevel } from '$lib/server/logger'; import type { ThemeModeType } from '$lib/theme-mode'; import type { Handle } from '@sveltejs/kit'; import { sequence } from '@sveltejs/kit/hooks'; import { migrate } from 'drizzle-orm/mysql2/migrator'; import { handleSession } from 'svelte-kit-cookie-session'; -const { AUTO_MIGRATE, SESSION_SECRET, SESSION_SECURE } = env; +const { AUTO_MIGRATE, SESSION_SECRET, SESSION_SECURE, NODE_ENV } = env; + +// Set logger to debug mode +if (NODE_ENV === 'development') { + Logger.setLogLevel(LogLevel.DEBUG); + Logger.debug('Debug mode enabled'); +} // Initialize the database await DB.init(); -// Initialize the JWT keys -await JWT.init(); - // Migrate the database when automatic migration is enabled if (AUTO_MIGRATE === 'true') { + Logger.info('Running automatic database migrations'); await migrate(DB.drizzle, { migrationsFolder: './migrations' }); + Logger.info('Database migrations complete'); } +// Initialize the JWT keys +await JWT.init(); + // Run database data seeders await runSeeds(); @@ -50,3 +59,15 @@ export const handle = sequence( }), handleThemeHook ); + +export async function handleError({ event, error, message, status }) { + const request = `${event.request.method} ${event.request.url} ${status} - ${message}`; + if (status && status >= 200 && status < 500) { + // Don't need error trace for non-server errors + Logger.error(request); + } else { + Logger.error(request, error); + } + + return { message }; +} diff --git a/src/lib/server/audit/audit.ts b/src/lib/server/audit/audit.ts index e515bbe..dc26750 100644 --- a/src/lib/server/audit/audit.ts +++ b/src/lib/server/audit/audit.ts @@ -1,4 +1,4 @@ -import { SQL, count, desc, eq, inArray, or, sql } from 'drizzle-orm'; +import { SQL, count, desc, eq, inArray, lt, or, sql } from 'drizzle-orm'; import { DB, auditLog, user, type AuditLog, type NewAuditLog, type User } from '../drizzle'; import { AuditAction, @@ -7,15 +7,23 @@ import { type MinimalRequestEvent } from './types'; import type { Paginated, PaginationMeta } from '$lib/types'; +import { Logger } from '../logger'; +import { AdminNotificationEmail, Emails } from '../email'; +import { env } from '$env/dynamic/private'; +import { env as publicEnv } from '$env/dynamic/public'; -const AUTOFLAG = [ - AuditAction.MALICIOUS_REQUEST, - AuditAction.THROTTLE, - AuditAction.DEACTIVATION_REQUEST, - AuditAction.DATA_DOWNLOAD_REQUEST -]; +const FLAG_EMAIL_COOLDOWN = 1 * 60 * 1000; +const FLAG_TRESHOLD_COOLDOWN = 30 * 60 * 1000; +const FLAG_EMAIL_TRESHOLD = 3; +const AUTOFLAG = [AuditAction.MALICIOUS_REQUEST, AuditAction.THROTTLE]; export class Audit { + protected static logger = new Logger('AUDIT'); + protected static flagEmailTimeout: ReturnType | undefined = undefined; + protected static flagTresholdTimeout: ReturnType | undefined = undefined; + protected static flagTriggerCount = 0; + protected static flagEmailCount = 0; + public static async insert( action: AuditAction, comment?: string, @@ -23,16 +31,23 @@ export class Audit { ip?: string, userAgent?: string ) { + const flagged = AUTOFLAG.includes(action); const newAuditLog: NewAuditLog = { action, content: comment, actorId: user?.id, actor_ip: ip, actor_ua: userAgent, - flagged: Number(AUTOFLAG.includes(action)) + flagged: Number(flagged) }; - // TODO: send flagged to administrator + Audit.logger.info( + `${action} ${user?.id || '-'} (${ip || '-'}) "${userAgent || '-'}" "${comment}" ${flagged}` + ); + + if (flagged) { + Audit.auditFlagTrigger(); + } await DB.drizzle.insert(auditLog).values(newAuditLog); } @@ -159,4 +174,51 @@ export class Audit { return accum; }, []); } + + private static async auditFlagTrigger() { + Audit.flagTriggerCount += 1; + + clearTimeout(Audit.flagTresholdTimeout); + Audit.flagTresholdTimeout = setTimeout(() => { + Audit.flagTriggerCount = 0; + Audit.flagEmailCount = 0; + }, FLAG_TRESHOLD_COOLDOWN); + + if ( + Audit.flagTriggerCount < FLAG_EMAIL_TRESHOLD || + Audit.flagEmailTimeout || + Audit.flagEmailCount > 1 + ) { + return; + } + + Logger.verbose('Flag treshold reached, sending out audit email to administrator'); + Audit.flagEmailTimeout = setTimeout(() => { + void Audit.sendAuditEmail(Audit.flagTriggerCount); + Audit.flagTriggerCount = 0; + Audit.flagEmailCount += 1; + Audit.flagEmailTimeout = undefined; + }, FLAG_EMAIL_COOLDOWN); + } + + /** + * Send an audit warning email + */ + private static async sendAuditEmail(count: number) { + if (!env.EMAIL_ADMIN) { + return; + } + + const content = AdminNotificationEmail(count); + + try { + await Emails.getSender().sendTemplate( + env.EMAIL_ADMIN, + `Audit events on ${publicEnv.PUBLIC_SITE_NAME} may require attention`, + content + ); + } catch (error) { + Logger.error('Failed to send audit email:', error); + } + } } diff --git a/src/lib/server/drizzle/index.ts b/src/lib/server/drizzle/index.ts index 42e58a5..b5b130f 100644 --- a/src/lib/server/drizzle/index.ts +++ b/src/lib/server/drizzle/index.ts @@ -1,7 +1,8 @@ import { env } from '$env/dynamic/private'; import { drizzle } from 'drizzle-orm/mysql2'; -import mysql from 'mysql2/promise'; +import mysql from 'mysql2'; import * as schema from './schema'; +import { Logger } from '../logger'; const { DATABASE_DB, DATABASE_HOST, DATABASE_PASS, DATABASE_USER } = env; @@ -24,12 +25,14 @@ export class DB { DB.mysqlConnection.on('connection', (connection) => { connection.on('error', (error) => { // We log warning on connection error, this is not a critical error - console.warn('Error received on connection. Will destroy.', { error }); + Logger.warn('Error received on connection. Will destroy.', { error }); connection.destroy(); }); }); - DB.drizzle = drizzle(DB.mysqlConnection, { schema, mode: 'default' }); + DB.drizzle = drizzle({ client: DB.mysqlConnection, schema, mode: 'default' }); + + Logger.debug('Database initialization complete'); } } diff --git a/src/lib/server/email/templates/admin-notification.email.ts b/src/lib/server/email/templates/admin-notification.email.ts new file mode 100644 index 0000000..41ef055 --- /dev/null +++ b/src/lib/server/email/templates/admin-notification.email.ts @@ -0,0 +1,17 @@ +import { env } from '$env/dynamic/public'; +import type { EmailTemplate } from '../template.interface'; + +export const AdminNotificationEmail = (count: number): EmailTemplate => ({ + text: ` +${env.PUBLIC_SITE_NAME} + +There have been ${count} flagged audit messages in the past 30 minutes. Urgent attention may be required. + +This email was sent to you because you are the administrator contact on ${env.PUBLIC_SITE_NAME}.`, + html: /* html */ ` +

${env.PUBLIC_SITE_NAME}

+ +

There have been ${count} flagged audit messages in the past 5 minutes. Urgent attention may be required.

+ +

This email was sent to you because you are the administrator contact on ${env.PUBLIC_SITE_NAME}.

` +}); diff --git a/src/lib/server/email/templates/index.ts b/src/lib/server/email/templates/index.ts index 1e513c5..ccca2f5 100644 --- a/src/lib/server/email/templates/index.ts +++ b/src/lib/server/email/templates/index.ts @@ -2,3 +2,4 @@ export * from './forgot-password.email'; export * from './invitation.email'; export * from './oauth2-invitation.email'; export * from './registration.email'; +export * from './admin-notification.email'; diff --git a/src/lib/server/file-backend.ts b/src/lib/server/file-backend.ts index b824ec5..094cd15 100644 --- a/src/lib/server/file-backend.ts +++ b/src/lib/server/file-backend.ts @@ -1,4 +1,4 @@ -import { mkdir, readFile, stat, unlink, writeFile } from 'fs/promises'; +import { mkdir, readFile, rm, stat, unlink, writeFile } from 'fs/promises'; import { dirname, join, resolve } from 'path'; export class FileBackend { @@ -32,6 +32,15 @@ export class FileBackend { } } + static async deleteDirectory(path: string | string[]) { + try { + await rm(FileBackend.filePath(path), { recursive: true }); + return true; + } catch { + return false; + } + } + private static filePath(path: string | string[]) { return resolve(Array.isArray(path) ? join(...path) : path); } diff --git a/src/lib/server/jwt.ts b/src/lib/server/jwt.ts index e698b3d..cfd0e55 100644 --- a/src/lib/server/jwt.ts +++ b/src/lib/server/jwt.ts @@ -15,8 +15,10 @@ import { v4 as uuidv4 } from 'uuid'; import { FileBackend } from './file-backend'; import { DB, jwks, type JsonKey } from './drizzle'; import { CryptoUtils } from './crypto-utils'; +import { Logger } from './logger'; +import { lt } from 'drizzle-orm'; -const { JWT_ALGORITHM, JWT_EXPIRATION, JWT_ISSUER } = env; +const { JWT_ALGORITHM, JWT_EXPIRATION, JWT_ISSUER, JWT_KEYLENGTH } = env; const ISSUER_EXPIRY = 365 * 24 * 60 * 60 * 1000; const ISSUER_ROTATE = ISSUER_EXPIRY / 2; @@ -26,11 +28,6 @@ interface AvailableWebKey extends JsonKey { publicKeyJWK: JWK; } -/** - * Generate JWT keys using the following commands: - * Private: openssl genpkey -out jwt.private.pem -algorithm RSA -pkeyopt rsa_keygen_bits:2048 - * Public: openssl rsa -in jwt.private.pem -pubout -outform PEM -out jwt.public.pem - */ export class JWT { private static keys: AvailableWebKey[] = []; @@ -44,19 +41,8 @@ export class JWT { } static async init() { - const keys = await DB.drizzle.select().from(jwks); - JWT.keys = await JWT.loadKeys(keys); - - // No current key or it is time to rotate - const keyPair = JWT.keys.find(({ current }) => current === 1); - if (!keyPair || keyPair.rotate_at.getTime() < Date.now()) { - const newKey = await JWT.createKeyPair(); - if (keyPair) { - keyPair.current = 0; - } - - JWT.keys.push(newKey); - } + await JWT.clearExpiredKeys(); + await JWT.loadFromStorage(); } static async issue(claims: Record, subject: string, audience?: string) { @@ -95,6 +81,45 @@ export class JWT { return payload; } + private static async clearExpiredKeys() { + Logger.debug(`Loading expired JWT key information`); + + const expiredQuery = lt(jwks.expires_at, new Date()); + const expireList = await DB.drizzle.select().from(jwks).where(expiredQuery); + if (!expireList.length) { + Logger.debug(`There are no expired JWT keys to clean up`); + return; + } + + for (const expired of expireList) { + Logger.warn(`Deleting expired JWT key pair ${expired.uuid} (${expired.fingerprint})`); + await FileBackend.deleteDirectory(['private', expired.uuid]); + } + + // Delete expired keys + await DB.drizzle.delete(jwks).where(expiredQuery); + } + + private static async loadFromStorage() { + // Fetch existing keys + const keys = await DB.drizzle.select().from(jwks); + Logger.verbose(`Loading ${keys.length} JWT key pairs into memory`); + JWT.keys = await JWT.loadKeys(keys); + + // No current key or it is time to rotate + const keyPair = JWT.keys.find(({ current }) => current === 1); + if (!keyPair || keyPair.rotate_at.getTime() < Date.now()) { + Logger.verbose(`Creating a new JWT key pair`); + + const newKey = await JWT.createKeyPair(); + if (keyPair) { + keyPair.current = 0; + } + + JWT.keys.push(newKey); + } + } + private static getCurrentKey() { const current = JWT.keys.find(({ current }) => current === 1); if (!current) { @@ -104,8 +129,13 @@ export class JWT { } private static async createKeyPair() { + /** + * Equivalent openssl commands: + * Private: openssl genpkey -out jwt.private.pem -algorithm RSA -pkeyopt rsa_keygen_bits:2048 + * Public: openssl rsa -in jwt.private.pem -pubout -outform PEM -out jwt.public.pem + */ const { privateKey, publicKey } = await generateKeyPair(JWT_ALGORITHM, { - modulusLength: 2048 + modulusLength: Number(JWT_KEYLENGTH) || 2048 }); const jwk = await exportJWK(publicKey); const kid = uuidv4({ random: Buffer.from(jwk.n as string).subarray(0, 16) }); diff --git a/src/lib/server/logger.ts b/src/lib/server/logger.ts new file mode 100644 index 0000000..90e743e --- /dev/null +++ b/src/lib/server/logger.ts @@ -0,0 +1,151 @@ +import chalk from 'chalk'; +import { format as strfmt, formatWithOptions as strfmtOptions } from 'node:util'; + +export enum LogLevel { + DEBUG = 0, + VERBOSE, + INFO, + WARN, + ERROR +} + +export interface ILogger { + log: (format: any, ...arg: any[]) => void; + debug: (format: any, ...arg: any[]) => void; + verbose: (format: any, ...arg: any[]) => void; + info: (format: any, ...arg: any[]) => void; + warn: (format: any, ...arg: any[]) => void; + error: (format: any, ...arg: any[]) => void; +} + +const chalkColors: Record string> = { + [LogLevel.DEBUG]: chalk.magenta, + [LogLevel.VERBOSE]: chalk.blueBright, + [LogLevel.INFO]: chalk.green, + [LogLevel.WARN]: chalk.yellow, + [LogLevel.ERROR]: chalk.red +}; + +export class Logger implements ILogger { + protected static instance?: Logger; + + constructor( + public context: string = '', + public logLevel: LogLevel = LogLevel.VERBOSE + ) {} + + static getInstance() { + if (!Logger.instance) { + Logger.instance = new Logger(); + } + return Logger.instance; + } + + protected get formatted() { + return this.logLevel === LogLevel.DEBUG; + } + + protected colorize(level: LogLevel, leading: string, trailing: string) { + if (!this.formatted) { + return leading + trailing; + } + + return chalkColors[level](leading) + trailing; + } + + protected handle(level: LogLevel, format: any, ...arg: any[]): void { + if (level < this.logLevel) { + return; + } + + const timestamp = this.getTimestamp(); + const logLevelKey = (Object.values(LogLevel) as string[])[level].toUpperCase().padStart(7); + + let formatted = this.formatted + ? strfmtOptions({ colors: true, depth: 8 }, format, ...arg) + : strfmt(format, ...arg); + + // Keep non-debug logs on one line for neatness + if (!this.formatted) { + formatted = formatted.replace(/\n/g, ' '); + } + + const outLine = this.colorize( + level, + `[${process.pid}] ${timestamp} ${logLevelKey}${this.context ? ` [${this.context}]` : ''} `, + formatted + ); + this.writeLog(LogLevel.ERROR ? 'err' : 'out', outLine); + } + + protected getTimestamp() { + return new Intl.DateTimeFormat('en-GB', { + year: 'numeric', + month: '2-digit', + day: '2-digit', + hour: '2-digit', + minute: '2-digit', + second: '2-digit' + }).format(new Date()); + } + + protected writeLog(stream: 'out' | 'err', message: string) { + (stream === 'out' ? process.stdout : process.stderr).write(`${message}\r\n`); + } + + setLogLevel(newLevel: LogLevel) { + this.logLevel = newLevel; + } + + static setLogLevel(newLevel: LogLevel) { + Logger.getInstance().logLevel = newLevel; + } + + log(format: any, ...arg: any[]) { + this.handle(LogLevel.INFO, format, ...arg); + } + + static log(format: any, ...arg: any[]) { + Logger.getInstance().handle(LogLevel.INFO, format, ...arg); + } + + debug(format: any, ...arg: any[]) { + this.handle(LogLevel.DEBUG, format, ...arg); + } + + static debug(format: any, ...arg: any[]) { + Logger.getInstance().handle(LogLevel.DEBUG, format, ...arg); + } + + verbose(format: any, ...arg: any[]) { + this.handle(LogLevel.VERBOSE, format, ...arg); + } + + static verbose(format: any, ...arg: any[]) { + Logger.getInstance().handle(LogLevel.VERBOSE, format, ...arg); + } + + info(format: any, ...arg: any[]) { + this.handle(LogLevel.INFO, format, ...arg); + } + + static info(format: any, ...arg: any[]) { + Logger.getInstance().handle(LogLevel.INFO, format, ...arg); + } + + warn(format: any, ...arg: any[]) { + this.handle(LogLevel.WARN, format, ...arg); + } + + static warn(format: any, ...arg: any[]) { + Logger.getInstance().handle(LogLevel.WARN, format, ...arg); + } + + error(format: any, ...arg: any[]) { + this.handle(LogLevel.ERROR, format, ...arg); + } + + static error(format: any, ...arg: any[]) { + Logger.getInstance().handle(LogLevel.ERROR, format, ...arg); + } +} diff --git a/src/lib/server/oauth2/controller/authorization.ts b/src/lib/server/oauth2/controller/authorization.ts index a608068..44d7d19 100644 --- a/src/lib/server/oauth2/controller/authorization.ts +++ b/src/lib/server/oauth2/controller/authorization.ts @@ -1,3 +1,4 @@ +import { Logger } from '$lib/server/logger'; import type { UserSession } from '../../users'; import { InvalidRequest, @@ -26,7 +27,7 @@ export class OAuth2AuthorizationController { } const redirectUri = url.searchParams.get('redirect_uri') as string; - // console.debug('Parameter redirect uri is', redirectUri); + Logger.debug('Parameter redirect uri is', redirectUri); if (!url.searchParams.has('client_id')) { throw new InvalidRequest('client_id field is mandatory for authorization endpoint'); @@ -40,14 +41,14 @@ export class OAuth2AuthorizationController { } const clientId = url.searchParams.get('client_id') as string; - // console.debug('Parameter client_id is', clientId); + Logger.debug('Parameter client_id is', clientId); if (!url.searchParams.has('response_type')) { throw new InvalidRequest('response_type field is mandatory for authorization endpoint'); } const responseType = url.searchParams.get('response_type') as string; - // console.debug('Parameter response_type is', responseType); + Logger.debug('Parameter response_type is', responseType); // Support multiple types const responseTypes = responseType.split(' '); @@ -77,7 +78,7 @@ export class OAuth2AuthorizationController { throw new InvalidRequest('Grant type "none" cannot be combined with other grant types'); } - // console.debug('Parameter grant_type is', grantTypes.join(' ')); + Logger.debug('Parameter grant_type is', grantTypes.join(' ')); const client = await OAuth2Clients.fetchById(clientId); if (!client || client.activated === 0) { @@ -89,7 +90,7 @@ export class OAuth2AuthorizationController { } else if (!(await OAuth2Clients.checkRedirectUri(client, redirectUri))) { throw new InvalidRequest('Wrong RedirectUri provided'); } - // console.debug('redirect_uri check passed'); + Logger.debug('redirect_uri check passed'); // The client needs to support all grant types for (const grantType of grantTypes) { @@ -97,13 +98,13 @@ export class OAuth2AuthorizationController { throw new UnauthorizedClient('This client does not support grant type ' + grantType); } } - // console.debug('Grant type check passed'); + Logger.debug('Grant type check passed'); const scope = OAuth2Clients.transformScope(url.searchParams.get('scope') as string); if (!OAuth2Clients.checkScope(client, scope)) { throw new InvalidScope('Client does not allow access to this scope'); } - // console.debug('Scope check passed'); + Logger.debug('Scope check passed'); const codeChallenge = url.searchParams.get('code_challenge') as string; const codeChallengeMethod = @@ -267,7 +268,7 @@ export class OAuth2AuthorizationController { throw new AccessDenied('User denied access to the resource'); } - // console.debug('Decision check passed'); + Logger.debug('Decision check passed'); } await OAuth2Users.saveConsent(user, client, scope); diff --git a/src/lib/server/oauth2/controller/device-authorization.ts b/src/lib/server/oauth2/controller/device-authorization.ts index 28323ab..fc3e315 100644 --- a/src/lib/server/oauth2/controller/device-authorization.ts +++ b/src/lib/server/oauth2/controller/device-authorization.ts @@ -1,5 +1,6 @@ import { env } from '$env/dynamic/public'; import { ApiUtils } from '$lib/server/api-utils'; +import { Logger } from '$lib/server/logger'; import { InvalidClient, InvalidRequest, InvalidScope, UnauthorizedClient } from '../error'; import { OAuth2Clients, OAuth2DeviceCodes, OAuth2Tokens } from '../model'; import { OAuth2Response } from '../response'; @@ -34,7 +35,7 @@ export class OAuth2DeviceAuthorizationController { clientId = pieces[0]; clientSecret = pieces[1]; - // console.debug('Client credentials parsed from basic auth header:', clientId, clientSecret); + Logger.debug('Client credentials parsed from basic auth header:', clientId, clientSecret); } if (!clientId) { diff --git a/src/lib/server/oauth2/controller/introspection.ts b/src/lib/server/oauth2/controller/introspection.ts index 1a24369..52f1bbf 100644 --- a/src/lib/server/oauth2/controller/introspection.ts +++ b/src/lib/server/oauth2/controller/introspection.ts @@ -1,4 +1,5 @@ import { ApiUtils } from '$lib/server/api-utils'; +import { Logger } from '$lib/server/logger'; import { InvalidClient, InvalidRequest, UnauthorizedClient } from '../error'; import { OAuth2AccessTokens, OAuth2Clients } from '../model'; import { OAuth2Response } from '../response'; @@ -13,7 +14,7 @@ export class OAuth2IntrospectionController { if (body.client_id && body.client_secret) { clientId = body.client_id as string; clientSecret = body.client_secret as string; - // console.debug('Client credentials parsed from body parameters ', clientId, clientSecret); + Logger.debug('Client credentials parsed from body parameters ', clientId, clientSecret); } else { if (!request.headers?.has('authorization')) { throw new InvalidRequest('No authorization header passed'); @@ -35,7 +36,7 @@ export class OAuth2IntrospectionController { clientId = pieces[0]; clientSecret = pieces[1]; - // console.debug('Client credentials parsed from basic auth header: ', clientId, clientSecret); + Logger.debug('Client credentials parsed from basic auth header: ', clientId, clientSecret); } if (!body.token) { diff --git a/src/lib/server/oauth2/controller/token.ts b/src/lib/server/oauth2/controller/token.ts index b2bf544..9c9fa36 100644 --- a/src/lib/server/oauth2/controller/token.ts +++ b/src/lib/server/oauth2/controller/token.ts @@ -1,4 +1,5 @@ import { ApiUtils } from '$lib/server/api-utils'; +import { Logger } from '$lib/server/logger'; import { InvalidRequest, InvalidClient, @@ -22,7 +23,7 @@ export class OAuth2TokenController { if (body.client_id) { clientId = body.client_id as string; clientSecret = body.client_secret as string; - // console.debug('Client credentials parsed from body parameters', clientId, clientSecret); + Logger.debug('Client credentials parsed from body parameters', clientId, clientSecret); } else { if (!request.headers?.has('authorization')) { throw new InvalidRequest('No authorization header passed'); @@ -44,7 +45,7 @@ export class OAuth2TokenController { clientId = pieces[0]; clientSecret = pieces[1]; - // console.debug('Client credentials parsed from basic auth header:', clientId, clientSecret); + Logger.debug('Client credentials parsed from basic auth header:', clientId, clientSecret); } if (!body.grant_type) { @@ -52,7 +53,7 @@ export class OAuth2TokenController { } grantType = body.grant_type as string; - // console.debug('Parameter grant_type is', grantType); + Logger.debug('Parameter grant_type is', grantType); // The spec does not allow using this grant type directly by this name, // but for verification purposes, we will simplify it below. @@ -80,7 +81,7 @@ export class OAuth2TokenController { if (!OAuth2Clients.checkGrantType(client, grantType) && grantType !== 'refresh_token') { throw new UnauthorizedClient('Invalid grant type for the client'); } - // console.debug('Grant type check passed'); + Logger.debug('Grant type check passed'); let tokenResponse: OAuth2TokenResponse = {}; try { diff --git a/src/lib/server/oauth2/controller/tokens/authorizationCode.ts b/src/lib/server/oauth2/controller/tokens/authorizationCode.ts index 6e96e23..a341c8a 100644 --- a/src/lib/server/oauth2/controller/tokens/authorizationCode.ts +++ b/src/lib/server/oauth2/controller/tokens/authorizationCode.ts @@ -1,5 +1,6 @@ import { CryptoUtils } from '$lib/server/crypto-utils'; import type { OAuth2Client, User } from '$lib/server/drizzle'; +import { Logger } from '$lib/server/logger'; import { Users } from '$lib/server/users'; import { InvalidRequest, ServerError, InvalidGrant } from '../../error'; import { @@ -38,7 +39,7 @@ export async function authorizationCode( try { code = await OAuth2Codes.fetchByCode(providedCode); } catch (err) { - console.error(err); + Logger.error('Failed to fetch OAuth2 access code', err); throw new ServerError('Failed to call code.fetchByCode function'); } @@ -54,7 +55,7 @@ export async function authorizationCode( throw new InvalidGrant('Code not found'); } - // console.debug('Code fetched', code); + Logger.debug('Code fetched', code); const scope = code.scope || ''; const cleanScope = OAuth2Clients.transformScope(scope); @@ -83,15 +84,15 @@ export async function authorizationCode( } } - // console.debug('Code passed PCKE check'); + Logger.debug('Code passed PCKE check'); if (!OAuth2Clients.checkGrantType(client, 'refresh_token')) { - // console.debug('Client does not allow grant type refresh_token, skip creation'); + Logger.debug('Client does not allow grant type refresh_token, skip creation'); } else { try { respObj.refresh_token = await OAuth2RefreshTokens.create(userId, clientId, scope); } catch (err) { - console.error(err); + Logger.error('Failed to issue an OAuth2 refresh token', err); throw new ServerError('Failed to call refreshTokens.create function'); } } @@ -104,17 +105,17 @@ export async function authorizationCode( OAuth2Tokens.tokenTtl ); } catch (err) { - console.error(err); + Logger.error('Failed to issue an OAuth2 access token', err); throw new ServerError('Failed to call accessTokens.create function'); } respObj.expires_in = OAuth2Tokens.tokenTtl; - // console.debug('Access token saved:', respObj.access_token); + Logger.debug('Access token saved:', respObj.access_token); try { await OAuth2Codes.removeByCode(providedCode); } catch (err) { - console.error(err); + Logger.error('Failed to remove OAuth2 access code', err); throw new ServerError('Failed to call codes.removeByCode function'); } @@ -129,7 +130,7 @@ export async function authorizationCode( code.nonce || undefined ); } catch (err) { - console.error(err); + Logger.error('Failed to issue a new ID token', err); throw new ServerError('Failed to issue an ID token'); } } diff --git a/src/lib/server/oauth2/controller/tokens/clientCredentials.ts b/src/lib/server/oauth2/controller/tokens/clientCredentials.ts index 67b27c3..8c22566 100644 --- a/src/lib/server/oauth2/controller/tokens/clientCredentials.ts +++ b/src/lib/server/oauth2/controller/tokens/clientCredentials.ts @@ -1,4 +1,5 @@ import type { OAuth2Client } from '$lib/server/drizzle'; +import { Logger } from '$lib/server/logger'; import { InvalidScope, ServerError } from '../../error'; import { OAuth2AccessTokens, OAuth2Clients, OAuth2Tokens } from '../../model'; import type { OAuth2TokenResponse } from '../../response'; @@ -24,7 +25,7 @@ export async function clientCredentials( throw new InvalidScope('Client does not allow access to this scope'); } - // console.debug('Scope check passed', scope); + Logger.debug('Scope check passed', scope); try { resObj.access_token = await OAuth2AccessTokens.create( diff --git a/src/lib/server/oauth2/controller/tokens/device.ts b/src/lib/server/oauth2/controller/tokens/device.ts index dfe9e8f..b288f06 100644 --- a/src/lib/server/oauth2/controller/tokens/device.ts +++ b/src/lib/server/oauth2/controller/tokens/device.ts @@ -1,4 +1,5 @@ import type { OAuth2Client } from '$lib/server/drizzle'; +import { Logger } from '$lib/server/logger'; import { Users } from '$lib/server/users'; import { AccessDenied, AuthorizationPending, ExpiredToken, ServerError } from '../../error'; import { @@ -51,7 +52,7 @@ export async function device(client: OAuth2Client, deviceCode: string) { try { resObj.id_token = await OAuth2Users.issueIdToken(user, client, cleanScope); } catch (err) { - console.error(err); + Logger.error('Failed to issue a new ID token:', err); throw new ServerError('Failed to issue an ID token'); } } diff --git a/src/lib/server/oauth2/controller/tokens/refreshToken.ts b/src/lib/server/oauth2/controller/tokens/refreshToken.ts index 729886d..75b2e24 100644 --- a/src/lib/server/oauth2/controller/tokens/refreshToken.ts +++ b/src/lib/server/oauth2/controller/tokens/refreshToken.ts @@ -1,4 +1,5 @@ import type { OAuth2Client, User } from '$lib/server/drizzle'; +import { Logger } from '$lib/server/logger'; import { Users } from '$lib/server/users'; import { InvalidRequest, ServerError, InvalidGrant, InvalidClient } from '../../error'; import { @@ -43,7 +44,7 @@ export async function refreshToken( } if (refreshToken.clientId !== client.id) { - console.warn( + Logger.warn( 'Client %s tried to fetch a refresh token which belongs to client %s!', client.id, refreshToken.clientId diff --git a/src/lib/server/users/index.ts b/src/lib/server/users/index.ts index f6242c4..df1a19f 100644 --- a/src/lib/server/users/index.ts +++ b/src/lib/server/users/index.ts @@ -8,6 +8,7 @@ import { env as privateEnv } from '$env/dynamic/private'; import { Emails, ForgotPasswordEmail, InvitationEmail, RegistrationEmail } from '../email'; import { env as publicEnv } from '$env/dynamic/public'; import { UserTokens } from './tokens'; +import { Logger } from '../logger'; export class Users { /** @@ -238,7 +239,6 @@ export class Users { `${publicEnv.PUBLIC_URL}/login?${params.toString()}` ); - // TODO: logging try { await Emails.getSender().sendTemplate( user.email, @@ -246,7 +246,7 @@ export class Users { content ); } catch (error) { - console.error(error); + Logger.error('Failed to send activation email:', error); await UserTokens.remove(token); } } @@ -267,14 +267,14 @@ export class Users { `${publicEnv.PUBLIC_URL}/login/password?${params.toString()}` ); - // TODO: logging try { await Emails.getSender().sendTemplate( user.email, `Reset your password on ${publicEnv.PUBLIC_SITE_NAME}`, content ); - } catch { + } catch (error) { + Logger.error('Failed to send password email:', error); await UserTokens.remove(token); } } @@ -294,14 +294,14 @@ export class Users { const params = new URLSearchParams({ token: token.token }); const content = InvitationEmail(`${publicEnv.PUBLIC_URL}/register?${params.toString()}`); - // TODO: logging try { await Emails.getSender().sendTemplate( email, `You have been invited to create an account on ${publicEnv.PUBLIC_SITE_NAME}`, content ); - } catch { + } catch (error) { + Logger.error('Failed to send invitation email:', error); await UserTokens.remove(token); } } diff --git a/src/routes/login/+page.svelte b/src/routes/login/+page.svelte index cb964ee..4ae12de 100644 --- a/src/routes/login/+page.svelte +++ b/src/routes/login/+page.svelte @@ -12,6 +12,7 @@ import ButtonRow from '$lib/components/container/ButtonRow.svelte'; import TitleRow from '$lib/components/container/TitleRow.svelte'; import ThemeButton from '$lib/components/ThemeButton.svelte'; + import { page } from '$app/stores'; interface Props { data: PageData;