From fc5dca136fe056740945e82c75ae7a87722d2ac6 Mon Sep 17 00:00:00 2001 From: Milo Schwartz Date: Tue, 1 Oct 2024 20:48:03 -0400 Subject: [PATCH] started integrating auth with lucia --- package-lock.json | 844 ++++++++++++++++++ package.json | 4 + server/auth/index.ts | 50 ++ server/auth/login.ts | 78 ++ server/auth/signup.ts | 93 ++ server/db/schema.ts | 48 +- server/index.ts | 5 + server/middlewares/formatError.ts | 23 + server/middlewares/notFound.ts | 14 + ...blob.sql => 0000_faithful_katie_power.sql} | 18 +- server/migrations/meta/0000_snapshot.json | 96 +- server/migrations/meta/_journal.json | 4 +- server/routers/external.ts | 3 + server/routers/global/global.ts | 6 + server/traefik-config-provider/index.ts | 7 +- server/types/ErrorResponse.ts | 7 + server/types/HttpCode.ts | 65 ++ server/types/MessageResponse.ts | 9 + server/types/Response.ts | 9 + server/utils/response.ts | 19 + 20 files changed, 1341 insertions(+), 61 deletions(-) create mode 100644 server/auth/index.ts create mode 100644 server/auth/login.ts create mode 100644 server/auth/signup.ts create mode 100644 server/middlewares/formatError.ts create mode 100644 server/middlewares/notFound.ts rename server/migrations/{0000_ancient_blob.sql => 0000_faithful_katie_power.sql} (81%) create mode 100644 server/types/ErrorResponse.ts create mode 100644 server/types/HttpCode.ts create mode 100644 server/types/MessageResponse.ts create mode 100644 server/types/Response.ts create mode 100644 server/utils/response.ts diff --git a/package-lock.json b/package-lock.json index a5043cc..5591f4b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,12 +8,16 @@ "name": "@fossorial/pangolin", "version": "0.1.0", "dependencies": { + "@lucia-auth/adapter-drizzle": "1.1.0", + "@node-rs/argon2": "1.8.3", "axios": "1.7.7", "better-sqlite3": "11.3.0", "cors": "2.8.5", "drizzle-orm": "0.33.0", "express": "4.21.0", "helmet": "7.1.0", + "http-errors": "2.0.0", + "lucia": "3.2.0", "next": "14.2.13", "react": "^18", "react-dom": "^18", @@ -2767,6 +2771,33 @@ "integrity": "sha512-AHy0vjc+n/4w/8Mif+w86qpppHuF3AyXbcWW+R/W7GNA3F5/p2nuhlkCJaTXSLZheB4l1rtHzOfr9A7NwoR/Zg==", "dev": true }, + "node_modules/@emnapi/core": { + "version": "0.45.0", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-0.45.0.tgz", + "integrity": "sha512-DPWjcUDQkCeEM4VnljEOEcXdAD7pp8zSZsgOujk/LGIwCXWbXJngin+MO4zbH429lzeC3WbYLGjE2MaUOwzpyw==", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "0.45.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-0.45.0.tgz", + "integrity": "sha512-Txumi3td7J4A/xTTwlssKieHKTGl3j4A1tglBx72auZ49YK7ePY6XZricgIg9mnZT4xPfA+UPCUdnhRuEFDL+w==", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.0.1.tgz", + "integrity": "sha512-iIBu7mwkq4UQGeMEM8bLwNK962nXdhodeScX4slfQnRhEMMzvYivHhutCIk8uojvmASXXPC2WNEjwxFWk72Oqw==", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@esbuild-kit/core-utils": { "version": "3.3.2", "resolved": "https://registry.npmjs.org/@esbuild-kit/core-utils/-/core-utils-3.3.2.tgz", @@ -4734,6 +4765,54 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@lucia-auth/adapter-drizzle": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@lucia-auth/adapter-drizzle/-/adapter-drizzle-1.1.0.tgz", + "integrity": "sha512-iCTnZWvfI5lLZOdUHZYiXA1jaspIFEeo2extLxQ3DjP3uOVys7IPwBi7zezLIRu9dhro4H4Kji+7gSYyjcef2A==", + "peerDependencies": { + "drizzle-orm": ">= 0.29 <1", + "lucia": "3.x" + } + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.4.tgz", + "integrity": "sha512-9zESzOO5aDByvhIAsOy9TbpZ0Ur2AJbUI7UT73kcUTS2mxAMHOBaa1st/jAymNoCtvrit99kkzT1FZuXVcgfIQ==", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.1.0", + "@emnapi/runtime": "^1.1.0", + "@tybys/wasm-util": "^0.9.0" + } + }, + "node_modules/@napi-rs/wasm-runtime/node_modules/@emnapi/core": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.2.0.tgz", + "integrity": "sha512-E7Vgw78I93we4ZWdYCb4DGAwRROGkMIXk7/y87UmANR+J6qsWusmC3gLt0H+O0KOt5e6O38U8oJamgbudrES/w==", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.0.1", + "tslib": "^2.4.0" + } + }, + "node_modules/@napi-rs/wasm-runtime/node_modules/@emnapi/runtime": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.2.0.tgz", + "integrity": "sha512-bV21/9LQmcQeCPEg3BDFtvwL6cwiTMksYNWQQ4KOxCZikEGalWtenoZ0wCiukJINlGCIi2KXx01g4FoH/LxpzQ==", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@napi-rs/wasm-runtime/node_modules/@tybys/wasm-util": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.9.0.tgz", + "integrity": "sha512-6+7nlbMVX/PVDCwaIQ8nTOPveOcFLSt8GcXdx8hD0bt39uWxYT88uXzqTd4fTvqta7oeUJqudepapKNt2DYJFw==", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@next/env": { "version": "14.2.13", "resolved": "https://registry.npmjs.org/@next/env/-/env-14.2.13.tgz", @@ -4922,6 +5001,481 @@ "url": "https://paulmillr.com/funding/" } }, + "node_modules/@node-rs/argon2": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/@node-rs/argon2/-/argon2-1.8.3.tgz", + "integrity": "sha512-sf/QAEI59hsMEEE2J8vO4hKrXrv4Oplte3KI2N4MhMDYpytH0drkVfErmHBfWFZxxIEK03fX1WsBNswS2nIZKg==", + "engines": { + "node": ">= 10" + }, + "optionalDependencies": { + "@node-rs/argon2-android-arm-eabi": "1.8.3", + "@node-rs/argon2-android-arm64": "1.8.3", + "@node-rs/argon2-darwin-arm64": "1.8.3", + "@node-rs/argon2-darwin-x64": "1.8.3", + "@node-rs/argon2-freebsd-x64": "1.8.3", + "@node-rs/argon2-linux-arm-gnueabihf": "1.8.3", + "@node-rs/argon2-linux-arm64-gnu": "1.8.3", + "@node-rs/argon2-linux-arm64-musl": "1.8.3", + "@node-rs/argon2-linux-x64-gnu": "1.8.3", + "@node-rs/argon2-linux-x64-musl": "1.8.3", + "@node-rs/argon2-wasm32-wasi": "1.8.3", + "@node-rs/argon2-win32-arm64-msvc": "1.8.3", + "@node-rs/argon2-win32-ia32-msvc": "1.8.3", + "@node-rs/argon2-win32-x64-msvc": "1.8.3" + } + }, + "node_modules/@node-rs/argon2-android-arm-eabi": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/@node-rs/argon2-android-arm-eabi/-/argon2-android-arm-eabi-1.8.3.tgz", + "integrity": "sha512-JFZPlNM0A8Og+Tncb8UZsQrhEMlbHBXPsT3hRoKImzVmTmq28Os0ucFWow0AACp2coLHBSydXH3Dh0lZup3rWw==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@node-rs/argon2-android-arm64": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/@node-rs/argon2-android-arm64/-/argon2-android-arm64-1.8.3.tgz", + "integrity": "sha512-zaf8P3T92caeW2xnMA7P1QvRA4pIt/04oilYP44XlTCtMye//vwXDMeK53sl7dvYiJKnzAWDRx41k8vZvpZazg==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@node-rs/argon2-darwin-arm64": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/@node-rs/argon2-darwin-arm64/-/argon2-darwin-arm64-1.8.3.tgz", + "integrity": "sha512-DV/IbmLGdNXBtXb5o2UI5ba6kvqXqPAJgmMOTUCuHeBSp992GlLHdfU4rzGu0dNrxudBnunNZv+crd0YdEQSUA==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@node-rs/argon2-darwin-x64": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/@node-rs/argon2-darwin-x64/-/argon2-darwin-x64-1.8.3.tgz", + "integrity": "sha512-YMjmBGFZhLfYjfQ2gll9A+BZu/zAMV7lWZIbKxb7ZgEofILQwuGmExjDtY3Jplido/6leCEdpmlk2oIsME00LA==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@node-rs/argon2-freebsd-x64": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/@node-rs/argon2-freebsd-x64/-/argon2-freebsd-x64-1.8.3.tgz", + "integrity": "sha512-Hq3Rj5Yb2RolTG/luRPnv+XiGCbi5nAK25Pc8ou/tVapwX+iktEm/NXbxc5zsMxraYVkCvfdwBjweC5O+KqCGw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@node-rs/argon2-linux-arm-gnueabihf": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/@node-rs/argon2-linux-arm-gnueabihf/-/argon2-linux-arm-gnueabihf-1.8.3.tgz", + "integrity": "sha512-x49l8RgzKoG0/V0IXa5rrEl1TcJEc936ctlYFvqcunSOyowZ6kiWtrp1qrbOR8gbaNILl11KTF52vF6+h8UlEQ==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@node-rs/argon2-linux-arm64-gnu": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/@node-rs/argon2-linux-arm64-gnu/-/argon2-linux-arm64-gnu-1.8.3.tgz", + "integrity": "sha512-gJesam/qA63reGkb9qJ2TjFSLBtY41zQh2oei7nfnYsmVQPuHHWItJxEa1Bm21SPW53gZex4jFJbDIgj0+PxIw==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@node-rs/argon2-linux-arm64-musl": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/@node-rs/argon2-linux-arm64-musl/-/argon2-linux-arm64-musl-1.8.3.tgz", + "integrity": "sha512-7O6kQdSKzB4Tjx/EBa8zKIxnmLkQE8VdJgPm6Ksrpn+ueo0mx2xf76fIDnbbTCtm3UbB+y+FkTo2wLA7tOqIKg==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@node-rs/argon2-linux-x64-gnu": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/@node-rs/argon2-linux-x64-gnu/-/argon2-linux-x64-gnu-1.8.3.tgz", + "integrity": "sha512-OBH+EFG7BGjFyldaao2H2gSCLmjtrrwf420B1L+lFn7JLW9UAjsIPFKAcWsYwPa/PwYzIge9Y7SGcpqlsSEX0w==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@node-rs/argon2-linux-x64-musl": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/@node-rs/argon2-linux-x64-musl/-/argon2-linux-x64-musl-1.8.3.tgz", + "integrity": "sha512-bDbMuyekIxZaN7NaX+gHVkOyABB8bcMEJYeRPW1vCXKHj3brJns1wiUFSxqeUXreupifNVJlQfPt1Y5B/vFXgQ==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@node-rs/argon2-wasm32-wasi": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/@node-rs/argon2-wasm32-wasi/-/argon2-wasm32-wasi-1.8.3.tgz", + "integrity": "sha512-NBf2cMCDbNKMzp13Pog8ZPmI0M9U4Ak5b95EUjkp17kdKZFds12dwW67EMnj7Zy+pRqby2QLECaWebDYfNENTg==", + "cpu": [ + "wasm32" + ], + "optional": true, + "dependencies": { + "@napi-rs/wasm-runtime": "^0.2.3" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@node-rs/argon2-win32-arm64-msvc": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/@node-rs/argon2-win32-arm64-msvc/-/argon2-win32-arm64-msvc-1.8.3.tgz", + "integrity": "sha512-AHpPo7UbdW5WWjwreVpgFSY0o1RY4A7cUFaqDXZB2OqEuyrhMxBdZct9PX7PQKI18D85pLsODnR+gvVuTwJ6rQ==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@node-rs/argon2-win32-ia32-msvc": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/@node-rs/argon2-win32-ia32-msvc/-/argon2-win32-ia32-msvc-1.8.3.tgz", + "integrity": "sha512-bqzn2rcQkEwCINefhm69ttBVVkgHJb/V03DdBKsPFtiX6H47axXKz62d1imi26zFXhOEYxhKbu3js03GobJOLw==", + "cpu": [ + "ia32" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@node-rs/argon2-win32-x64-msvc": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/@node-rs/argon2-win32-x64-msvc/-/argon2-win32-x64-msvc-1.8.3.tgz", + "integrity": "sha512-ILlrRThdbp5xNR5gwYM2ic1n/vG5rJ8dQZ+YMRqksl+lnTJ/6FDe5BOyIhiPtiDwlCiCtUA+1NxpDB9KlUCAIA==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@node-rs/bcrypt": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@node-rs/bcrypt/-/bcrypt-1.9.0.tgz", + "integrity": "sha512-u2OlIxW264bFUfvbFqDz9HZKFjwe8FHFtn7T/U8mYjPZ7DWYpbUB+/dkW/QgYfMSfR0ejkyuWaBBe0coW7/7ig==", + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "optionalDependencies": { + "@node-rs/bcrypt-android-arm-eabi": "1.9.0", + "@node-rs/bcrypt-android-arm64": "1.9.0", + "@node-rs/bcrypt-darwin-arm64": "1.9.0", + "@node-rs/bcrypt-darwin-x64": "1.9.0", + "@node-rs/bcrypt-freebsd-x64": "1.9.0", + "@node-rs/bcrypt-linux-arm-gnueabihf": "1.9.0", + "@node-rs/bcrypt-linux-arm64-gnu": "1.9.0", + "@node-rs/bcrypt-linux-arm64-musl": "1.9.0", + "@node-rs/bcrypt-linux-x64-gnu": "1.9.0", + "@node-rs/bcrypt-linux-x64-musl": "1.9.0", + "@node-rs/bcrypt-wasm32-wasi": "1.9.0", + "@node-rs/bcrypt-win32-arm64-msvc": "1.9.0", + "@node-rs/bcrypt-win32-ia32-msvc": "1.9.0", + "@node-rs/bcrypt-win32-x64-msvc": "1.9.0" + } + }, + "node_modules/@node-rs/bcrypt-android-arm-eabi": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@node-rs/bcrypt-android-arm-eabi/-/bcrypt-android-arm-eabi-1.9.0.tgz", + "integrity": "sha512-nOCFISGtnodGHNiLrG0WYLWr81qQzZKYfmwHc7muUeq+KY0sQXyHOwZk9OuNQAWv/lnntmtbwkwT0QNEmOyLvA==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@node-rs/bcrypt-android-arm64": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@node-rs/bcrypt-android-arm64/-/bcrypt-android-arm64-1.9.0.tgz", + "integrity": "sha512-+ZrIAtigVmjYkqZQTThHVlz0+TG6D+GDHWhVKvR2DifjtqJ0i+mb9gjo++hN+fWEQdWNGxKCiBBjwgT4EcXd6A==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@node-rs/bcrypt-darwin-arm64": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@node-rs/bcrypt-darwin-arm64/-/bcrypt-darwin-arm64-1.9.0.tgz", + "integrity": "sha512-CQiS+F9Pa0XozvkXR1g7uXE9QvBOPOplDg0iCCPRYTN9PqA5qYxhwe48G3o+v2UeQceNRrbnEtWuANm7JRqIhw==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@node-rs/bcrypt-darwin-x64": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@node-rs/bcrypt-darwin-x64/-/bcrypt-darwin-x64-1.9.0.tgz", + "integrity": "sha512-4pTKGawYd7sNEjdJ7R/R67uwQH1VvwPZ0SSUMmeNHbxD5QlwAPXdDH11q22uzVXsvNFZ6nGQBg8No5OUGpx6Ug==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@node-rs/bcrypt-freebsd-x64": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@node-rs/bcrypt-freebsd-x64/-/bcrypt-freebsd-x64-1.9.0.tgz", + "integrity": "sha512-UmWzySX4BJhT/B8xmTru6iFif3h0Rpx3TqxRLCcbgmH43r7k5/9QuhpiyzpvKGpKHJCFNm4F3rC2wghvw5FCIg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@node-rs/bcrypt-linux-arm-gnueabihf": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@node-rs/bcrypt-linux-arm-gnueabihf/-/bcrypt-linux-arm-gnueabihf-1.9.0.tgz", + "integrity": "sha512-8qoX4PgBND2cVwsbajoAWo3NwdfJPEXgpCsZQZURz42oMjbGyhhSYbovBCskGU3EBLoC8RA2B1jFWooeYVn5BA==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@node-rs/bcrypt-linux-arm64-gnu": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@node-rs/bcrypt-linux-arm64-gnu/-/bcrypt-linux-arm64-gnu-1.9.0.tgz", + "integrity": "sha512-TuAC6kx0SbcIA4mSEWPi+OCcDjTQUMl213v5gMNlttF+D4ieIZx6pPDGTaMO6M2PDHTeCG0CBzZl0Lu+9b0c7Q==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@node-rs/bcrypt-linux-arm64-musl": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@node-rs/bcrypt-linux-arm64-musl/-/bcrypt-linux-arm64-musl-1.9.0.tgz", + "integrity": "sha512-/sIvKDABOI8QOEnLD7hIj02BVaNOuCIWBKvxcJOt8+TuwJ6zmY1UI5kSv9d99WbiHjTp97wtAUbZQwauU4b9ew==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@node-rs/bcrypt-linux-x64-gnu": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@node-rs/bcrypt-linux-x64-gnu/-/bcrypt-linux-x64-gnu-1.9.0.tgz", + "integrity": "sha512-DyyhDHDsLBsCKz1tZ1hLvUZSc1DK0FU0v52jK6IBQxrj24WscSU9zZe7ie/V9kdmA4Ep57BfpWX8Dsa2JxGdgQ==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@node-rs/bcrypt-linux-x64-musl": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@node-rs/bcrypt-linux-x64-musl/-/bcrypt-linux-x64-musl-1.9.0.tgz", + "integrity": "sha512-duIiuqQ+Lew8ASSAYm6ZRqcmfBGWwsi81XLUwz86a2HR7Qv6V4yc3ZAUQovAikhjCsIqe8C11JlAZSK6+PlXYg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@node-rs/bcrypt-wasm32-wasi": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@node-rs/bcrypt-wasm32-wasi/-/bcrypt-wasm32-wasi-1.9.0.tgz", + "integrity": "sha512-ylaGmn9Wjwv/D5lxtawttx3H6Uu2WTTR7lWlRHGT6Ga/MB1Vj4OjSGUW8G8zIVnKuXpGbZ92pgHlt4HUpSLctw==", + "cpu": [ + "wasm32" + ], + "optional": true, + "dependencies": { + "@emnapi/core": "^0.45.0", + "@emnapi/runtime": "^0.45.0", + "@tybys/wasm-util": "^0.8.1", + "memfs-browser": "^3.4.13000" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@node-rs/bcrypt-win32-arm64-msvc": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@node-rs/bcrypt-win32-arm64-msvc/-/bcrypt-win32-arm64-msvc-1.9.0.tgz", + "integrity": "sha512-2h86gF7QFyEzODuDFml/Dp1MSJoZjxJ4yyT2Erf4NkwsiA5MqowUhUsorRwZhX6+2CtlGa7orbwi13AKMsYndw==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@node-rs/bcrypt-win32-ia32-msvc": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@node-rs/bcrypt-win32-ia32-msvc/-/bcrypt-win32-ia32-msvc-1.9.0.tgz", + "integrity": "sha512-kqxalCvhs4FkN0+gWWfa4Bdy2NQAkfiqq/CEf6mNXC13RSV673Ev9V8sRlQyNpCHCNkeXfOT9pgoBdJmMs9muA==", + "cpu": [ + "ia32" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@node-rs/bcrypt-win32-x64-msvc": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@node-rs/bcrypt-win32-x64-msvc/-/bcrypt-win32-x64-msvc-1.9.0.tgz", + "integrity": "sha512-2y0Tuo6ZAT2Cz8V7DHulSlv1Bip3zbzeXyeur+uR25IRNYXKvI/P99Zl85Fbuu/zzYAZRLLlGTRe6/9IHofe/w==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -5274,6 +5828,15 @@ "tslib": "^2.4.0" } }, + "node_modules/@tybys/wasm-util": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.8.3.tgz", + "integrity": "sha512-Z96T/L6dUFFxgFJ+pQtkPpne9q7i6kIPYCFnQBHSgSPV9idTsKfIhCss0h5iM9irweZCatkrdeP8yi5uM1eX6Q==", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@types/better-sqlite3": { "version": "7.6.11", "resolved": "https://registry.npmjs.org/@types/better-sqlite3/-/better-sqlite3-7.6.11.tgz", @@ -9254,6 +9817,12 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, + "node_modules/fs-monkey": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/fs-monkey/-/fs-monkey-1.0.6.tgz", + "integrity": "sha512-b1FMfwetIKymC0eioW7mTywihSQE4oLzQn1dB6rZB5fx/3NpNEdAWeCSMB+60/AeT0TCXsxzAlcYVEFCTAksWg==", + "optional": true + }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -11099,6 +11668,14 @@ "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", "devOptional": true }, + "node_modules/lucia": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/lucia/-/lucia-3.2.0.tgz", + "integrity": "sha512-eXMxXwk6hqtjRTj4W/x3EnTUtAztLPm0p2N2TEBMDEbakDLXiYnDQ9z/qahjPdPdhPguQc+vwO0/88zIWxlpuw==", + "dependencies": { + "oslo": "1.2.0" + } + }, "node_modules/make-dir": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", @@ -11183,6 +11760,27 @@ "node": ">= 0.6" } }, + "node_modules/memfs": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/memfs/-/memfs-3.5.3.tgz", + "integrity": "sha512-UERzLsxzllchadvbPs5aolHh65ISpKpM+ccLbOJ8/vvpBKmAWf+la7dXFy7Mr0ySHbdHrFv5kGFCUHHe6GFEmw==", + "optional": true, + "dependencies": { + "fs-monkey": "^1.0.4" + }, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/memfs-browser": { + "version": "3.5.10302", + "resolved": "https://registry.npmjs.org/memfs-browser/-/memfs-browser-3.5.10302.tgz", + "integrity": "sha512-JJTc/nh3ig05O0gBBGZjTCPOyydaTxNF0uHYBrcc1gHNnO+KIHIvo0Y1FKCJsaei6FCl8C6xfQomXqu+cuzkIw==", + "optional": true, + "dependencies": { + "memfs": "3.5.3" + } + }, "node_modules/memory-cache": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/memory-cache/-/memory-cache-0.2.0.tgz", @@ -12095,6 +12693,252 @@ "os-tmpdir": "^1.0.0" } }, + "node_modules/oslo": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/oslo/-/oslo-1.2.0.tgz", + "integrity": "sha512-OoFX6rDsNcOQVAD2gQD/z03u4vEjWZLzJtwkmgfRF+KpQUXwdgEXErD7zNhyowmHwHefP+PM9Pw13pgpHMRlzw==", + "dependencies": { + "@node-rs/argon2": "1.7.0", + "@node-rs/bcrypt": "1.9.0" + } + }, + "node_modules/oslo/node_modules/@node-rs/argon2": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@node-rs/argon2/-/argon2-1.7.0.tgz", + "integrity": "sha512-zfULc+/tmcWcxn+nHkbyY8vP3+MpEqKORbszt4UkpqZgBgDAAIYvuDN/zukfTgdmo6tmJKKVfzigZOPk4LlIog==", + "engines": { + "node": ">= 10" + }, + "optionalDependencies": { + "@node-rs/argon2-android-arm-eabi": "1.7.0", + "@node-rs/argon2-android-arm64": "1.7.0", + "@node-rs/argon2-darwin-arm64": "1.7.0", + "@node-rs/argon2-darwin-x64": "1.7.0", + "@node-rs/argon2-freebsd-x64": "1.7.0", + "@node-rs/argon2-linux-arm-gnueabihf": "1.7.0", + "@node-rs/argon2-linux-arm64-gnu": "1.7.0", + "@node-rs/argon2-linux-arm64-musl": "1.7.0", + "@node-rs/argon2-linux-x64-gnu": "1.7.0", + "@node-rs/argon2-linux-x64-musl": "1.7.0", + "@node-rs/argon2-wasm32-wasi": "1.7.0", + "@node-rs/argon2-win32-arm64-msvc": "1.7.0", + "@node-rs/argon2-win32-ia32-msvc": "1.7.0", + "@node-rs/argon2-win32-x64-msvc": "1.7.0" + } + }, + "node_modules/oslo/node_modules/@node-rs/argon2-android-arm-eabi": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@node-rs/argon2-android-arm-eabi/-/argon2-android-arm-eabi-1.7.0.tgz", + "integrity": "sha512-udDqkr5P9E+wYX1SZwAVPdyfYvaF4ry9Tm+R9LkfSHbzWH0uhU6zjIwNRp7m+n4gx691rk+lqqDAIP8RLKwbhg==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/oslo/node_modules/@node-rs/argon2-android-arm64": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@node-rs/argon2-android-arm64/-/argon2-android-arm64-1.7.0.tgz", + "integrity": "sha512-s9j/G30xKUx8WU50WIhF0fIl1EdhBGq0RQ06lEhZ0Gi0ap8lhqbE2Bn5h3/G2D1k0Dx+yjeVVNmt/xOQIRG38A==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/oslo/node_modules/@node-rs/argon2-darwin-arm64": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@node-rs/argon2-darwin-arm64/-/argon2-darwin-arm64-1.7.0.tgz", + "integrity": "sha512-ZIz4L6HGOB9U1kW23g+m7anGNuTZ0RuTw0vNp3o+2DWpb8u8rODq6A8tH4JRL79S+Co/Nq608m9uackN2pe0Rw==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/oslo/node_modules/@node-rs/argon2-darwin-x64": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@node-rs/argon2-darwin-x64/-/argon2-darwin-x64-1.7.0.tgz", + "integrity": "sha512-5oi/pxqVhODW/pj1+3zElMTn/YukQeywPHHYDbcAW3KsojFjKySfhcJMd1DjKTc+CHQI+4lOxZzSUzK7mI14Hw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/oslo/node_modules/@node-rs/argon2-freebsd-x64": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@node-rs/argon2-freebsd-x64/-/argon2-freebsd-x64-1.7.0.tgz", + "integrity": "sha512-Ify08683hA4QVXYoIm5SUWOY5DPIT/CMB0CQT+IdxQAg/F+qp342+lUkeAtD5bvStQuCx/dFO3bnnzoe2clMhA==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/oslo/node_modules/@node-rs/argon2-linux-arm-gnueabihf": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@node-rs/argon2-linux-arm-gnueabihf/-/argon2-linux-arm-gnueabihf-1.7.0.tgz", + "integrity": "sha512-7DjDZ1h5AUHAtRNjD19RnQatbhL+uuxBASuuXIBu4/w6Dx8n7YPxwTP4MXfsvuRgKuMWiOb/Ub/HJ3kXVCXRkg==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/oslo/node_modules/@node-rs/argon2-linux-arm64-gnu": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@node-rs/argon2-linux-arm64-gnu/-/argon2-linux-arm64-gnu-1.7.0.tgz", + "integrity": "sha512-nJDoMP4Y3YcqGswE4DvP080w6O24RmnFEDnL0emdI8Nou17kNYBzP2546Nasx9GCyLzRcYQwZOUjrtUuQ+od2g==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/oslo/node_modules/@node-rs/argon2-linux-arm64-musl": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@node-rs/argon2-linux-arm64-musl/-/argon2-linux-arm64-musl-1.7.0.tgz", + "integrity": "sha512-BKWS8iVconhE3jrb9mj6t1J9vwUqQPpzCbUKxfTGJfc+kNL58F1SXHBoe2cDYGnHrFEHTY0YochzXoAfm4Dm/A==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/oslo/node_modules/@node-rs/argon2-linux-x64-gnu": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@node-rs/argon2-linux-x64-gnu/-/argon2-linux-x64-gnu-1.7.0.tgz", + "integrity": "sha512-EmgqZOlf4Jurk/szW1iTsVISx25bKksVC5uttJDUloTgsAgIGReCpUUO1R24pBhu9ESJa47iv8NSf3yAfGv6jQ==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/oslo/node_modules/@node-rs/argon2-linux-x64-musl": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@node-rs/argon2-linux-x64-musl/-/argon2-linux-x64-musl-1.7.0.tgz", + "integrity": "sha512-/o1efYCYIxjfuoRYyBTi2Iy+1iFfhqHCvvVsnjNSgO1xWiWrX0Rrt/xXW5Zsl7vS2Y+yu8PL8KFWRzZhaVxfKA==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/oslo/node_modules/@node-rs/argon2-wasm32-wasi": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@node-rs/argon2-wasm32-wasi/-/argon2-wasm32-wasi-1.7.0.tgz", + "integrity": "sha512-Evmk9VcxqnuwQftfAfYEr6YZYSPLzmKUsbFIMep5nTt9PT4XYRFAERj7wNYp+rOcBenF3X4xoB+LhwcOMTNE5w==", + "cpu": [ + "wasm32" + ], + "optional": true, + "dependencies": { + "@emnapi/core": "^0.45.0", + "@emnapi/runtime": "^0.45.0", + "@tybys/wasm-util": "^0.8.1", + "memfs-browser": "^3.4.13000" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/oslo/node_modules/@node-rs/argon2-win32-arm64-msvc": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@node-rs/argon2-win32-arm64-msvc/-/argon2-win32-arm64-msvc-1.7.0.tgz", + "integrity": "sha512-qgsU7T004COWWpSA0tppDqDxbPLgg8FaU09krIJ7FBl71Sz8SFO40h7fDIjfbTT5w7u6mcaINMQ5bSHu75PCaA==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/oslo/node_modules/@node-rs/argon2-win32-ia32-msvc": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@node-rs/argon2-win32-ia32-msvc/-/argon2-win32-ia32-msvc-1.7.0.tgz", + "integrity": "sha512-JGafwWYQ/HpZ3XSwP4adQ6W41pRvhcdXvpzIWtKvX+17+xEXAe2nmGWM6s27pVkg1iV2ZtoYLRDkOUoGqZkCcg==", + "cpu": [ + "ia32" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/oslo/node_modules/@node-rs/argon2-win32-x64-msvc": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@node-rs/argon2-win32-x64-msvc/-/argon2-win32-x64-msvc-1.7.0.tgz", + "integrity": "sha512-9oq4ShyFakw8AG3mRls0AoCpxBFcimYx7+jvXeAf2OqKNO+mSA6eZ9z7KQeVCi0+SOEUYxMGf5UiGiDb9R6+9Q==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, "node_modules/p-finally": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", diff --git a/package.json b/package.json index 8717aa1..d369bca 100644 --- a/package.json +++ b/package.json @@ -12,12 +12,16 @@ "start": "ENVIRONMENT=prod node dist/server/index.js" }, "dependencies": { + "@lucia-auth/adapter-drizzle": "1.1.0", + "@node-rs/argon2": "1.8.3", "axios": "1.7.7", "better-sqlite3": "11.3.0", "cors": "2.8.5", "drizzle-orm": "0.33.0", "express": "4.21.0", "helmet": "7.1.0", + "http-errors": "2.0.0", + "lucia": "3.2.0", "next": "14.2.13", "react": "^18", "react-dom": "^18", diff --git a/server/auth/index.ts b/server/auth/index.ts new file mode 100644 index 0000000..adee29c --- /dev/null +++ b/server/auth/index.ts @@ -0,0 +1,50 @@ +import { Lucia, TimeSpan } from "lucia"; +import { DrizzleSQLiteAdapter } from "@lucia-auth/adapter-drizzle"; +import db from "@server/db"; +import { sessions, users } from "@server/db/schema"; +import environment from "@server/environment"; + +const adapter = new DrizzleSQLiteAdapter(db, sessions, users); + +export const lucia = new Lucia(adapter, { + getUserAttributes: (attributes) => { + return { + username: attributes.username, + }; + }, + // getSessionAttributes: (attributes) => { + // return { + // country: attributes.country, + // }; + // }, + sessionCookie: { + name: "session", + expires: false, // session cookies have very long lifespan (2 years) + attributes: { + secure: environment.ENVIRONMENT === "prod", + sameSite: "strict", + // domain: "example.com" + }, + }, + sessionExpiresIn: new TimeSpan(2, "w"), +}); + +export default lucia; + +// IMPORTANT! +declare module "lucia" { + interface Register { + Lucia: typeof lucia; + DatabaseUserAttributes: DatabaseUserAttributes; + DatabaseSessionAttributes: DatabaseSessionAttributes; + } +} + +interface DatabaseUserAttributes { + username: string; + passwordHash: string; +} + +interface DatabaseSessionAttributes { + // country: string; +} diff --git a/server/auth/login.ts b/server/auth/login.ts new file mode 100644 index 0000000..7f5dc76 --- /dev/null +++ b/server/auth/login.ts @@ -0,0 +1,78 @@ +import { verify } from "@node-rs/argon2"; +import lucia from "@server/auth"; +import db from "@server/db"; +import { users } from "@server/db/schema"; +import HttpCode from "@server/types/HttpCode"; +import response from "@server/utils/response"; +import { eq } from "drizzle-orm"; +import { NextFunction, Request, Response } from "express"; +import createHttpError from "http-errors"; +import { z } from "zod"; +import { fromError } from "zod-validation-error"; + +export const loginBodySchema = z.object({ + email: z.string().email(), + password: z.string(), +}); + +export async function login(req: Request, res: Response, next: NextFunction) { + const parsedBody = loginBodySchema.safeParse(req.body); + + if (!parsedBody.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + fromError(parsedBody.error).toString(), + ), + ); + } + + const { email, password } = parsedBody.data; + + const existingUserRes = await db + .select() + .from(users) + .where(eq(users.email, email)); + if (!existingUserRes || !existingUserRes.length) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + "A user with that email address does not exist", + ), + ); + } + + const existingUser = existingUserRes[0]; + + const validPassword = await verify(existingUser.passwordHash, password, { + memoryCost: 19456, + timeCost: 2, + outputLen: 32, + parallelism: 1, + }); + if (!validPassword) { + await new Promise((resolve) => setTimeout(resolve, 500)); // delay to prevent brute force attacks + return next( + createHttpError( + HttpCode.BAD_REQUEST, + "The password you entered is incorrect", + ), + ); + } + + const session = await lucia.createSession(existingUser.id, {}); + res.appendHeader( + "Set-Cookie", + lucia.createSessionCookie(session.id).serialize(), + ); + + return res.status(HttpCode.OK).send( + response({ + data: null, + success: true, + error: false, + message: "Logged in successfully", + status: HttpCode.OK, + }), + ); +} diff --git a/server/auth/signup.ts b/server/auth/signup.ts new file mode 100644 index 0000000..9fd879a --- /dev/null +++ b/server/auth/signup.ts @@ -0,0 +1,93 @@ +import { NextFunction, Request, Response } from "express"; +import db from "@server/db"; +import { hash } from "@node-rs/argon2"; +import HttpCode from "@server/types/HttpCode"; +import { z } from "zod"; +import { generateId } from "lucia"; +import { users } from "@server/db/schema"; +import lucia from "@server/auth"; +import { fromError } from "zod-validation-error"; +import createHttpError from "http-errors"; +import response from "@server/utils/response"; +import { SqliteError } from "better-sqlite3"; + +export const signupBodySchema = z.object({ + email: z.string().email(), + password: z + .string() + .min(8, { message: "Password must be at least 8 characters long" }) + .max(31, { message: "Password must be at most 31 characters long" }) + .regex(/^(?=.*?[A-Z])(?=.*?[a-z])(?=.*?[0-9])(?=.*?[#?!@$%^&*-]).*$/, { + message: `Your password must meet the following conditions: +- At least one uppercase English letter. +- At least one lowercase English letter. +- At least one digit. +- At least one special character.`, + }), +}); + +export type SignUpBody = z.infer; + +export async function signup(req: Request, res: Response, next: NextFunction) { + const parsedBody = signupBodySchema.safeParse(req.body); + + if (!parsedBody.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + fromError(parsedBody.error).toString(), + ), + ); + } + + const { email, password } = parsedBody.data; + + const passwordHash = await hash(password, { + // recommended minimum parameters + memoryCost: 19456, + timeCost: 2, + outputLen: 32, + parallelism: 1, + }); + const userId = generateId(15); + + try { + await db.insert(users).values({ + id: userId, + email: email, + passwordHash, + }); + + const session = await lucia.createSession(userId, {}); + res.appendHeader( + "Set-Cookie", + lucia.createSessionCookie(session.id).serialize(), + ); + + return res.status(HttpCode.OK).send( + response({ + data: null, + success: true, + error: false, + message: "User created successfully", + status: HttpCode.OK, + }), + ); + } catch (e) { + if (e instanceof SqliteError && e.code === "SQLITE_CONSTRAINT_UNIQUE") { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + "A user with that email address already exists", + ), + ); + } else { + return next( + createHttpError( + HttpCode.INTERNAL_SERVER_ERROR, + "Failed to create user", + ), + ); + } + } +} diff --git a/server/db/schema.ts b/server/db/schema.ts index 53a5213..b7e2aa6 100644 --- a/server/db/schema.ts +++ b/server/db/schema.ts @@ -8,32 +8,29 @@ export const orgs = sqliteTable("orgs", { domain: text("domain").notNull(), }); -// Users table -export const users = sqliteTable("users", { - userId: integer("userId").primaryKey({ autoIncrement: true }), - orgId: integer("orgId").references(() => orgs.orgId, { onDelete: "cascade" }), - name: text("name").notNull(), - email: text("email").notNull(), - groups: text("groups"), -}); - // Sites table export const sites = sqliteTable("sites", { siteId: integer("siteId").primaryKey({ autoIncrement: true }), - orgId: integer("orgId").references(() => orgs.orgId, { onDelete: "cascade" }), - exitNode: integer("exitNode").references(() => exitNodes.exitNodeId, { onDelete: "set null" }), + orgId: integer("orgId").references(() => orgs.orgId, { + onDelete: "cascade", + }), + exitNode: integer("exitNode").references(() => exitNodes.exitNodeId, { + onDelete: "set null", + }), name: text("name").notNull(), subdomain: text("subdomain"), pubKey: text("pubKey"), subnet: text("subnet"), megabytesIn: integer("bytesIn"), - megabytesOut: integer("bytesOut") + megabytesOut: integer("bytesOut"), }); // Resources table export const resources = sqliteTable("resources", { resourceId: text("resourceId", { length: 2048 }).primaryKey(), - siteId: integer("siteId").references(() => sites.siteId, { onDelete: "cascade" }), + siteId: integer("siteId").references(() => sites.siteId, { + onDelete: "cascade", + }), name: text("name").notNull(), subdomain: text("subdomain"), }); @@ -41,7 +38,9 @@ export const resources = sqliteTable("resources", { // Targets table export const targets = sqliteTable("targets", { targetId: integer("targetId").primaryKey({ autoIncrement: true }), - resourceId: text("resourceId").references(() => resources.resourceId, { onDelete: "cascade" }), + resourceId: text("resourceId").references(() => resources.resourceId, { + onDelete: "cascade", + }), ip: text("ip").notNull(), method: text("method").notNull(), port: integer("port").notNull(), @@ -61,10 +60,28 @@ export const exitNodes = sqliteTable("exitNodes", { // Routes table export const routes = sqliteTable("routes", { routeId: integer("routeId").primaryKey({ autoIncrement: true }), - exitNodeId: integer("exitNodeId").references(() => exitNodes.exitNodeId, { onDelete: "cascade" }), + exitNodeId: integer("exitNodeId").references(() => exitNodes.exitNodeId, { + onDelete: "cascade", + }), subnet: text("subnet").notNull(), }); +// Users table +export const users = sqliteTable("user", { + id: text("id").primaryKey(), // has to be id not userId for lucia + email: text("email").notNull().unique(), + passwordHash: text("passwordHash").notNull(), +}); + +// Sessions table +export const sessions = sqliteTable("session", { + id: text("id").primaryKey(), // has to be id not sessionId for lucia + userId: text("userId") + .notNull() + .references(() => users.id), + expiresAt: integer("expiresAt").notNull(), +}); + // Define the model types for type inference export type Org = InferSelectModel; export type User = InferSelectModel; @@ -73,3 +90,4 @@ export type Resource = InferSelectModel; export type ExitNode = InferSelectModel; export type Route = InferSelectModel; export type Target = InferSelectModel; +export type Session = InferSelectModel; diff --git a/server/index.ts b/server/index.ts index 706e737..b327d09 100644 --- a/server/index.ts +++ b/server/index.ts @@ -7,6 +7,8 @@ import helmet from "helmet"; import cors from "cors"; import internal from "@server/routers/internal"; import external from "@server/routers/external"; +import notFoundMiddleware from "./middlewares/notFound"; +import { errorHandlerMiddleware } from "./middlewares/formatError"; const dev = environment.ENVIRONMENT !== "prod"; const app = next({ dev }); @@ -34,6 +36,9 @@ app.prepare().then(() => { logger.info(`Main server is running on http://localhost:${mainPort}`); }); + mainServer.use(notFoundMiddleware); + mainServer.use(errorHandlerMiddleware); + // Internal server const internalServer = express(); internalServer.use(helmet()); diff --git a/server/middlewares/formatError.ts b/server/middlewares/formatError.ts new file mode 100644 index 0000000..f653345 --- /dev/null +++ b/server/middlewares/formatError.ts @@ -0,0 +1,23 @@ +import { ErrorRequestHandler, NextFunction, Response } from "express"; +import ErrorResponse from "@server/types/ErrorResponse"; +import HttpCode from "@server/types/HttpCode"; +import logger from "@server/logger"; +import environment from "@server/environment"; + +export const errorHandlerMiddleware: ErrorRequestHandler = ( + error, + req, + res: Response, + next: NextFunction, +) => { + logger.error(error); + const statusCode = error.statusCode || HttpCode.INTERNAL_SERVER_ERROR; + res?.status(statusCode).send({ + data: null, + success: false, + error: true, + message: error.message || "Internal Server Error", + status: statusCode, + stack: environment.ENVIRONMENT === "prod" ? null : error.stack, + }); +}; diff --git a/server/middlewares/notFound.ts b/server/middlewares/notFound.ts new file mode 100644 index 0000000..2d20b27 --- /dev/null +++ b/server/middlewares/notFound.ts @@ -0,0 +1,14 @@ +import { NextFunction, Request, Response } from "express"; +import createHttpError from "http-errors"; +import HttpCode from "@server/types/HttpCode"; + +export function notFoundMiddleware( + req: Request, + res: Response, + next: NextFunction, +) { + const message = `The requests url is not found - ${req.originalUrl}`; + return next(createHttpError(HttpCode.NOT_FOUND, message)); +} + +export default notFoundMiddleware; diff --git a/server/migrations/0000_ancient_blob.sql b/server/migrations/0000_faithful_katie_power.sql similarity index 81% rename from server/migrations/0000_ancient_blob.sql rename to server/migrations/0000_faithful_katie_power.sql index d74be97..55307dd 100644 --- a/server/migrations/0000_ancient_blob.sql +++ b/server/migrations/0000_faithful_katie_power.sql @@ -27,6 +27,13 @@ CREATE TABLE `routes` ( FOREIGN KEY (`exitNodeId`) REFERENCES `exitNodes`(`exitNodeId`) ON UPDATE no action ON DELETE cascade ); --> statement-breakpoint +CREATE TABLE `session` ( + `id` text PRIMARY KEY NOT NULL, + `userId` text NOT NULL, + `expiresAt` integer NOT NULL, + FOREIGN KEY (`userId`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE no action +); +--> statement-breakpoint CREATE TABLE `sites` ( `siteId` integer PRIMARY KEY AUTOINCREMENT NOT NULL, `orgId` integer, @@ -52,11 +59,10 @@ CREATE TABLE `targets` ( FOREIGN KEY (`resourceId`) REFERENCES `resources`(`resourceId`) ON UPDATE no action ON DELETE cascade ); --> statement-breakpoint -CREATE TABLE `users` ( - `userId` integer PRIMARY KEY AUTOINCREMENT NOT NULL, - `orgId` integer, - `name` text NOT NULL, +CREATE TABLE `user` ( + `id` text PRIMARY KEY NOT NULL, `email` text NOT NULL, - `groups` text, - FOREIGN KEY (`orgId`) REFERENCES `orgs`(`orgId`) ON UPDATE no action ON DELETE cascade + `passwordHash` text NOT NULL ); +--> statement-breakpoint +CREATE UNIQUE INDEX `user_email_unique` ON `user` (`email`); \ No newline at end of file diff --git a/server/migrations/meta/0000_snapshot.json b/server/migrations/meta/0000_snapshot.json index 3b608e4..708e4f8 100644 --- a/server/migrations/meta/0000_snapshot.json +++ b/server/migrations/meta/0000_snapshot.json @@ -1,7 +1,7 @@ { "version": "6", "dialect": "sqlite", - "id": "db8ede7f-7ece-463c-be9c-da36b2b20db6", + "id": "fb7ff7a8-20e6-4602-b096-2f2284ad751e", "prevId": "00000000-0000-0000-0000-000000000000", "tables": { "exitNodes": { @@ -173,6 +173,50 @@ "compositePrimaryKeys": {}, "uniqueConstraints": {} }, + "session": { + "name": "session", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expiresAt": { + "name": "expiresAt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "session_userId_user_id_fk": { + "name": "session_userId_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, "sites": { "name": "sites", "columns": { @@ -345,27 +389,13 @@ "compositePrimaryKeys": {}, "uniqueConstraints": {} }, - "users": { - "name": "users", + "user": { + "name": "user", "columns": { - "userId": { - "name": "userId", - "type": "integer", - "primaryKey": true, - "notNull": true, - "autoincrement": true - }, - "orgId": { - "name": "orgId", - "type": "integer", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "name": { - "name": "name", + "id": { + "name": "id", "type": "text", - "primaryKey": false, + "primaryKey": true, "notNull": true, "autoincrement": false }, @@ -376,30 +406,24 @@ "notNull": true, "autoincrement": false }, - "groups": { - "name": "groups", + "passwordHash": { + "name": "passwordHash", "type": "text", "primaryKey": false, - "notNull": false, + "notNull": true, "autoincrement": false } }, - "indexes": {}, - "foreignKeys": { - "users_orgId_orgs_orgId_fk": { - "name": "users_orgId_orgs_orgId_fk", - "tableFrom": "users", - "tableTo": "orgs", - "columnsFrom": [ - "orgId" + "indexes": { + "user_email_unique": { + "name": "user_email_unique", + "columns": [ + "email" ], - "columnsTo": [ - "orgId" - ], - "onDelete": "cascade", - "onUpdate": "no action" + "isUnique": true } }, + "foreignKeys": {}, "compositePrimaryKeys": {}, "uniqueConstraints": {} } diff --git a/server/migrations/meta/_journal.json b/server/migrations/meta/_journal.json index f616f0c..2819239 100644 --- a/server/migrations/meta/_journal.json +++ b/server/migrations/meta/_journal.json @@ -5,8 +5,8 @@ { "idx": 0, "version": "6", - "when": 1727582003591, - "tag": "0000_ancient_blob", + "when": 1727750917675, + "tag": "0000_faithful_katie_power", "breakpoints": true } ] diff --git a/server/routers/external.ts b/server/routers/external.ts index 3c05545..1dfecf8 100644 --- a/server/routers/external.ts +++ b/server/routers/external.ts @@ -1,6 +1,7 @@ import { Router } from "express"; import gerbil from "./gerbil/gerbil"; import pangolin from "./pangolin/pangolin"; +import global from "./global/global"; const unauth = Router(); @@ -11,4 +12,6 @@ unauth.get("/", (_, res) => { unauth.use("/newt", gerbil); unauth.use("/pangolin", pangolin); +unauth.use("/", global) + export default unauth; diff --git a/server/routers/global/global.ts b/server/routers/global/global.ts index 0993a2b..14060b7 100644 --- a/server/routers/global/global.ts +++ b/server/routers/global/global.ts @@ -1,5 +1,7 @@ import { Router } from "express"; import { createSite } from "./createSite"; +import { signup } from "@server/auth/signup"; +import { login } from "@server/auth/login"; const global = Router(); @@ -9,4 +11,8 @@ global.get("/", (_, res) => { global.get("/createSite", createSite); +// auth +global.post("/signup", signup); +global.post("/login", login); + export default global; diff --git a/server/traefik-config-provider/index.ts b/server/traefik-config-provider/index.ts index 42462d7..c05b693 100644 --- a/server/traefik-config-provider/index.ts +++ b/server/traefik-config-provider/index.ts @@ -4,16 +4,19 @@ import * as schema from "@server/db/schema"; import { DynamicTraefikConfig } from "./configSchema"; import { and, like, eq } from "drizzle-orm"; import logger from "@server/logger"; +import HttpCode from "@server/types/HttpCode"; export async function traefikConfigProvider(_: Request, res: Response) { try { const targets = await getAllTargets(); const traefikConfig = buildTraefikConfig(targets); // logger.debug("Built traefik config"); - res.status(200).send(traefikConfig); + res.status(HttpCode.OK).json(traefikConfig); } catch (e) { logger.error(`Failed to build traefik config: ${e}`); - res.status(500).send({ message: "Failed to build traefik config" }); + res.status(HttpCode.INTERNAL_SERVER_ERROR).json({ + error: "Failed to build traefik config", + }); } } diff --git a/server/types/ErrorResponse.ts b/server/types/ErrorResponse.ts new file mode 100644 index 0000000..f0c573b --- /dev/null +++ b/server/types/ErrorResponse.ts @@ -0,0 +1,7 @@ +import MessageResponse from "./MessageResponse"; + +export interface ErrorResponse extends MessageResponse { + stack?: string; +} + +export default ErrorResponse; diff --git a/server/types/HttpCode.ts b/server/types/HttpCode.ts new file mode 100644 index 0000000..70f2105 --- /dev/null +++ b/server/types/HttpCode.ts @@ -0,0 +1,65 @@ +export enum HttpCode { + CONTINUE = 100, + SWITCHING_PROTOCOLS = 101, + PROCESSING = 102, + EARLY_HINTS = 103, + OK = 200, + CREATED = 201, + ACCEPTED = 202, + NON_AUTHORITATIVE_INFORMATION = 203, + NO_CONTENT = 204, + RESET_CONTENT = 205, + PARTIAL_CONTENT = 206, + MULTI_STATUS = 207, + ALREADY_REPORTED = 208, + IM_USED = 226, + MULTIPLE_CHOICES = 300, + MOVED_PERMANENTLY = 301, + FOUND = 302, + SEE_OTHER = 303, + NOT_MODIFIED = 304, + TEMPORARY_REDIRECT = 307, + PERMANENT_REDIRECT = 308, + BAD_REQUEST = 400, + UNAUTHORIZED = 401, + PAYMENT_REQUIRED = 402, + FORBIDDEN = 403, + NOT_FOUND = 404, + METHOD_NOT_ALLOWED = 405, + NOT_ACCEPTABLE = 406, + PROXY_AUTHENTICATION_REQUIRED = 407, + REQUEST_TIMEOUT = 408, + CONFLICT = 409, + GONE = 410, + LENGTH_REQUIRED = 411, + PRECONDITION_FAILED = 412, + CONTENT_TOO_LARGE = 413, + URI_TOO_LONG = 414, + UNSUPPORTED_MEDIA_TYPE = 415, + RANGE_NOT_SATISFIABLE = 416, + EXPECTATION_FAILED = 417, + IM_A_TEAPOT = 418, + MISDIRECTED_REQUEST = 421, + UNPROCESSABLE_CONTENT = 422, + LOCKED = 423, + FAILED_DEPENDENCY = 424, + TOO_EARLY = 425, + UPGRADE_REQUIRED = 426, + PRECONDITION_REQUIRED = 428, + TOO_MANY_REQUESTS = 429, + REQUEST_HEADER_FIELDS_TOO_LARGE = 431, + UNAVAILABLE_FOR_LEGAL_REASONS = 451, + INTERNAL_SERVER_ERROR = 500, + NOT_IMPLEMENTED = 501, + BAD_GATEWAY = 502, + SERVICE_UNAVAILABLE = 503, + GATEWAY_TIMEOUT = 504, + HTTP_VERSION_NOT_SUPPORTED = 505, + VARIANT_ALSO_NEGOTIATES = 506, + INSUFFICIENT_STORAGE = 507, + LOOP_DETECTED = 508, + NOT_EXTENDED = 510, + NETWORK_AUTHENTICATION_REQUIRED = 511, +} + +export default HttpCode; diff --git a/server/types/MessageResponse.ts b/server/types/MessageResponse.ts new file mode 100644 index 0000000..c1b9b20 --- /dev/null +++ b/server/types/MessageResponse.ts @@ -0,0 +1,9 @@ +export interface ResponseT { + data: T | null; + success: boolean; + error: boolean; + message: string; + status: number; +} + +export default ResponseT; diff --git a/server/types/Response.ts b/server/types/Response.ts new file mode 100644 index 0000000..c1b9b20 --- /dev/null +++ b/server/types/Response.ts @@ -0,0 +1,9 @@ +export interface ResponseT { + data: T | null; + success: boolean; + error: boolean; + message: string; + status: number; +} + +export default ResponseT; diff --git a/server/utils/response.ts b/server/utils/response.ts new file mode 100644 index 0000000..4c07cd7 --- /dev/null +++ b/server/utils/response.ts @@ -0,0 +1,19 @@ +import { ResponseT } from "@server/types/Response"; + +export const response = ({ + data, + success, + error, + message, + status, +}: ResponseT) => { + return { + data, + success, + error, + message, + status, + }; +}; + +export default response;