From 15ff9609c44ff2cd8f2ad89ee9930caa7e253a60 Mon Sep 17 00:00:00 2001 From: Sandra Modzelewska Date: Thu, 30 Jan 2025 03:01:44 +0000 Subject: [PATCH] Auth is actually working --- .env.development | 2 +- dockerfiles/assets/caddy/Caddyfile | 2 +- drizzle/0000_users-and-sessions.sql | 4 +- drizzle/meta/0000_snapshot.json | 19 +-- drizzle/meta/_journal.json | 2 +- index.js | 8 -- package.json | 10 +- pnpm-lock.yaml | 215 ++++++++++++++++++++++++++-- src/hooks.server.ts | 70 +++++++++ src/lib/server/db.ts | 4 +- src/lib/server/schema.ts | 27 +++- src/lib/server/session.ts | 118 +++++++++++++++ src/lib/server/user.ts | 42 +++++- src/routes/+layout.server.ts | 7 + src/routes/+layout.svelte | 16 +++ src/routes/+page.server.ts | 12 -- src/routes/+page.svelte | 2 - src/routes/login/+page.server.ts | 83 ++++++++++- src/routes/login/+page.svelte | 27 ++++ src/routes/logout/+page.server.ts | 28 ++++ svelte.config.js | 5 +- vite.config.ts | 2 +- 22 files changed, 631 insertions(+), 74 deletions(-) delete mode 100644 index.js create mode 100644 src/hooks.server.ts create mode 100644 src/routes/+layout.server.ts create mode 100644 src/routes/+layout.svelte delete mode 100644 src/routes/+page.server.ts create mode 100644 src/routes/logout/+page.server.ts diff --git a/.env.development b/.env.development index 09c38a8..3f0f7cd 100644 --- a/.env.development +++ b/.env.development @@ -1,2 +1,2 @@ DATABASE_USER=postgres -DATABASE_HOST=database \ No newline at end of file +DATABASE_HOST=database diff --git a/dockerfiles/assets/caddy/Caddyfile b/dockerfiles/assets/caddy/Caddyfile index 6b6246a..01d5635 100644 --- a/dockerfiles/assets/caddy/Caddyfile +++ b/dockerfiles/assets/caddy/Caddyfile @@ -11,7 +11,7 @@ localhost { handle { reverse_proxy dev:5173 { - header_up Host {host} + header_up Origin "^https://localhost$" "http://localhost" } } diff --git a/drizzle/0000_users-and-sessions.sql b/drizzle/0000_users-and-sessions.sql index fca998e..0e47555 100644 --- a/drizzle/0000_users-and-sessions.sql +++ b/drizzle/0000_users-and-sessions.sql @@ -1,6 +1,6 @@ CREATE TABLE "sessions" ( - "id" integer PRIMARY KEY GENERATED ALWAYS AS IDENTITY (sequence name "sessions_id_seq" INCREMENT BY 1 MINVALUE 1 MAXVALUE 2147483647 START WITH 1 CACHE 1), - "user_id" integer, + "id" text PRIMARY KEY NOT NULL, + "user_id" integer NOT NULL, "expires_at" timestamp with time zone NOT NULL ); --> statement-breakpoint diff --git a/drizzle/meta/0000_snapshot.json b/drizzle/meta/0000_snapshot.json index f584f4b..8c46ca0 100644 --- a/drizzle/meta/0000_snapshot.json +++ b/drizzle/meta/0000_snapshot.json @@ -1,5 +1,5 @@ { - "id": "f22311a8-ce92-4bd5-9a7c-5e8085af13a9", + "id": "9f31c19a-9e3c-41db-a08b-27d9916265cb", "prevId": "00000000-0000-0000-0000-000000000000", "version": "7", "dialect": "postgresql", @@ -10,26 +10,15 @@ "columns": { "id": { "name": "id", - "type": "integer", + "type": "text", "primaryKey": true, - "notNull": true, - "identity": { - "type": "always", - "name": "sessions_id_seq", - "schema": "public", - "increment": "1", - "startWith": "1", - "minValue": "1", - "maxValue": "2147483647", - "cache": "1", - "cycle": false - } + "notNull": true }, "user_id": { "name": "user_id", "type": "integer", "primaryKey": false, - "notNull": false + "notNull": true }, "expires_at": { "name": "expires_at", diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json index f096de7..411e94a 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -5,7 +5,7 @@ { "idx": 0, "version": "7", - "when": 1738177092891, + "when": 1738197417069, "tag": "0000_users-and-sessions", "breakpoints": true } diff --git a/index.js b/index.js deleted file mode 100644 index dc0fc6c..0000000 --- a/index.js +++ /dev/null @@ -1,8 +0,0 @@ -import { loadEnv } from 'vite'; - -console.log(process.env); - -const env = loadEnv("development", ".", ""); - -// console.log(env); -// console.log(process.env) \ No newline at end of file diff --git a/package.json b/package.json index 47cfb52..f0fcdae 100644 --- a/package.json +++ b/package.json @@ -16,11 +16,14 @@ "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", "test:unit": "vitest", - "test": "npm run test:unit -- --run" + "test": "npm run test:unit -- --run", + "db:push": "drizzle-kit push", + "db:migrate": "drizzle-kit migrate", + "db:studio": "drizzle-kit studio" }, "devDependencies": { "@biomejs/biome": "1.9.4", - "@sveltejs/adapter-auto": "^4.0.0", + "@sveltejs/adapter-node": "^5.2.12", "@sveltejs/kit": "^2.16.1", "@sveltejs/vite-plugin-svelte": "^5.0.3", "@types/pg": "^8.11.11", @@ -36,7 +39,10 @@ }, "dependencies": { "@devcontainers/cli": "^0.73.0", + "@js-temporal/polyfill": "^0.4.4", "@node-rs/argon2": "^2.0.2", + "@oslojs/crypto": "^1.0.1", + "@oslojs/encoding": "^1.1.0", "drizzle-orm": "^0.39.0", "pg": "^8.13.1" } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b360db4..9a29073 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11,9 +11,18 @@ importers: '@devcontainers/cli': specifier: ^0.73.0 version: 0.73.0 + '@js-temporal/polyfill': + specifier: ^0.4.4 + version: 0.4.4 '@node-rs/argon2': specifier: ^2.0.2 version: 2.0.2 + '@oslojs/crypto': + specifier: ^1.0.1 + version: 1.0.1 + '@oslojs/encoding': + specifier: ^1.1.0 + version: 1.1.0 drizzle-orm: specifier: ^0.39.0 version: 0.39.0(@types/pg@8.11.11)(pg@8.13.1) @@ -24,9 +33,9 @@ importers: '@biomejs/biome': specifier: 1.9.4 version: 1.9.4 - '@sveltejs/adapter-auto': - specifier: ^4.0.0 - version: 4.0.0(@sveltejs/kit@2.16.1(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.19.5)(vite@6.0.11(@types/node@22.12.0)(tsx@4.19.2)))(svelte@5.19.5)(vite@6.0.11(@types/node@22.12.0)(tsx@4.19.2))) + '@sveltejs/adapter-node': + specifier: ^5.2.12 + version: 5.2.12(@sveltejs/kit@2.16.1(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.19.5)(vite@6.0.11(@types/node@22.12.0)(tsx@4.19.2)))(svelte@5.19.5)(vite@6.0.11(@types/node@22.12.0)(tsx@4.19.2))) '@sveltejs/kit': specifier: ^2.16.1 version: 2.16.1(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.19.5)(vite@6.0.11(@types/node@22.12.0)(tsx@4.19.2)))(svelte@5.19.5)(vite@6.0.11(@types/node@22.12.0)(tsx@4.19.2)) @@ -730,6 +739,10 @@ packages: '@jridgewell/trace-mapping@0.3.25': resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==} + '@js-temporal/polyfill@0.4.4': + resolution: {integrity: sha512-2X6bvghJ/JAoZO52lbgyAPFj8uCflhTo2g7nkFzEQdXd/D8rEeD4HtmTEpmtGCva260fcd66YNXBOYdnmHqSOg==} + engines: {node: '>=12'} + '@napi-rs/wasm-runtime@0.2.6': resolution: {integrity: sha512-z8YVS3XszxFTO73iwvFDNpQIzdMmSDTP/mB3E/ucR37V3Sx57hSExcXyMoNwaucWxnsWf4xfbZv0iZ30jr0M4Q==} @@ -820,9 +833,57 @@ packages: resolution: {integrity: sha512-t64wIsPEtNd4aUPuTAyeL2ubxATCBGmeluaKXEMAFk/8w6AJIVVkeLKMBpgLW6LU2t5cQxT+env/c6jxbtTQBg==} engines: {node: '>= 10'} + '@oslojs/asn1@1.0.0': + resolution: {integrity: sha512-zw/wn0sj0j0QKbIXfIlnEcTviaCzYOY3V5rAyjR6YtOByFtJiT574+8p9Wlach0lZH9fddD4yb9laEAIl4vXQA==} + + '@oslojs/binary@1.0.0': + resolution: {integrity: sha512-9RCU6OwXU6p67H4NODbuxv2S3eenuQ4/WFLrsq+K/k682xrznH5EVWA7N4VFk9VYVcbFtKqur5YQQZc0ySGhsQ==} + + '@oslojs/crypto@1.0.1': + resolution: {integrity: sha512-7n08G8nWjAr/Yu3vu9zzrd0L9XnrJfpMioQcvCMxBIiF5orECHe5/3J0jmXRVvgfqMm/+4oxlQ+Sq39COYLcNQ==} + + '@oslojs/encoding@1.1.0': + resolution: {integrity: sha512-70wQhgYmndg4GCPxPPxPGevRKqTIJ2Nh4OkiMWmDAVYsTQ+Ta7Sq+rPevXyXGdzr30/qZBnyOalCszoMxlyldQ==} + '@polka/url@1.0.0-next.28': resolution: {integrity: sha512-8LduaNlMZGwdZ6qWrKlfa+2M4gahzFkprZiAt2TF8uS0qQgBizKXpXURqvTJ4WtmupWxaLqjRb2UCTe72mu+Aw==} + '@rollup/plugin-commonjs@28.0.2': + resolution: {integrity: sha512-BEFI2EDqzl+vA1rl97IDRZ61AIwGH093d9nz8+dThxJNH8oSoB7MjWvPCX3dkaK1/RCJ/1v/R1XB15FuSs0fQw==} + engines: {node: '>=16.0.0 || 14 >= 14.17'} + peerDependencies: + rollup: ^2.68.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + + '@rollup/plugin-json@6.1.0': + resolution: {integrity: sha512-EGI2te5ENk1coGeADSIwZ7G2Q8CJS2sF120T7jLw4xFw9n7wIOXHo+kIYRAoVpJAN+kmqZSoO3Fp4JtoNF4ReA==} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + + '@rollup/plugin-node-resolve@16.0.0': + resolution: {integrity: sha512-0FPvAeVUT/zdWoO0jnb/V5BlBsUSNfkIOtFHzMO4H9MOklrmQFY6FduVHKucNb/aTFxvnGhj4MNj/T1oNdDfNg==} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: ^2.78.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + + '@rollup/pluginutils@5.1.4': + resolution: {integrity: sha512-USm05zrsFxYLPdWWq+K3STlWiT/3ELn3RcV5hJMghpeAIhxfsUIg6mt12CBJBInWMV4VneoV7SfGv8xIwo2qNQ==} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + '@rollup/rollup-android-arm-eabi@4.32.1': resolution: {integrity: sha512-/pqA4DmqyCm8u5YIDzIdlLcEmuvxb0v8fZdFhVMszSpDTgbQKdw3/mB3eMUHIbubtJ6F9j+LtmyCnHTEqIHyzA==} cpu: [arm] @@ -918,10 +979,10 @@ packages: cpu: [x64] os: [win32] - '@sveltejs/adapter-auto@4.0.0': - resolution: {integrity: sha512-kmuYSQdD2AwThymQF0haQhM8rE5rhutQXG4LNbnbShwhMO4qQGnKaaTy+88DuNSuoQDi58+thpq8XpHc1+oEKQ==} + '@sveltejs/adapter-node@5.2.12': + resolution: {integrity: sha512-0bp4Yb3jKIEcZWVcJC/L1xXp9zzJS4hDwfb4VITAkfT4OVdkspSHsx7YhqJDbb2hgLl6R9Vs7VQR+fqIVOxPUQ==} peerDependencies: - '@sveltejs/kit': ^2.0.0 + '@sveltejs/kit': ^2.4.0 '@sveltejs/kit@2.16.1': resolution: {integrity: sha512-2pF5sgGJx9brYZ/9nNDYnh5KX0JguPF14dnvvtf/MqrvlWrDj/e7Rk3LBJPecFLLK1GRs6ZniD24gFPqZm/NFw==} @@ -962,6 +1023,9 @@ packages: '@types/pg@8.11.11': resolution: {integrity: sha512-kGT1qKM8wJQ5qlawUrEkXgvMSXoV213KfMGXcwfDwUIfUHXqXYXOfS1nE1LINRJVVVx5wCm70XnFlMHaIcQAfw==} + '@types/resolve@1.20.2': + resolution: {integrity: sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==} + '@vitest/expect@3.0.4': resolution: {integrity: sha512-Nm5kJmYw6P2BxhJPkO3eKKhGYKRsnqJqf+r0yOGRKpEP+bSCBDsjXgiu1/5QFrnPMEgzfC38ZEjvCFgaNBC0Eg==} @@ -1036,6 +1100,9 @@ packages: resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} engines: {node: '>=6'} + commondir@1.0.1: + resolution: {integrity: sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==} + cookie@0.6.0: resolution: {integrity: sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==} engines: {node: '>= 0.6'} @@ -1198,6 +1265,9 @@ packages: esrap@1.4.3: resolution: {integrity: sha512-Xddc1RsoFJ4z9nR7W7BFaEPIp4UXoeQ0+077UdWLxbafMQFyU79sQJMk7kxNgRwQ9/aVgaKacCHC2pUACGwmYw==} + estree-walker@2.0.2: + resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} + estree-walker@3.0.3: resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} @@ -1218,15 +1288,35 @@ packages: engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} os: [darwin] + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + get-tsconfig@4.10.0: resolution: {integrity: sha512-kGzZ3LWWQcGIAmg6iWvXn0ei6WDtV26wzHRMwDSzmAbcXrTEXxHy6IehI6/4eT6VRKyMP1eF1VqwrVUmE/LR7A==} + hasown@2.0.2: + resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + engines: {node: '>= 0.4'} + import-meta-resolve@4.1.0: resolution: {integrity: sha512-I6fiaX09Xivtk+THaMfAwnA3MVA5Big1WHF1Dfx9hFuvNIWpXnorlkzhcQf6ehrqQiiZECRt1poOAkPmer3ruw==} + is-core-module@2.16.1: + resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==} + engines: {node: '>= 0.4'} + + is-module@1.0.0: + resolution: {integrity: sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==} + + is-reference@1.2.1: + resolution: {integrity: sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==} + is-reference@3.0.3: resolution: {integrity: sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==} + jsbi@4.3.0: + resolution: {integrity: sha512-SnZNcinB4RIcnEyZqFPdGPVgrg2AcnykiBy0sHVJQKHYeaLUvi3Exj+iaPpLnFVkDPZIV4U0yvgC9/R4uEAZ9g==} + kleur@4.1.5: resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==} engines: {node: '>=6'} @@ -1259,6 +1349,9 @@ packages: obuf@1.1.2: resolution: {integrity: sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==} + path-parse@1.0.7: + resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + pathe@2.0.2: resolution: {integrity: sha512-15Ztpk+nov8DR524R4BF7uEuzESgzUEAV4Ah7CUMNGXdE5ELuvxElxGXndBl32vMSsWa1jpNf22Z+Er3sKwq+w==} @@ -1361,6 +1454,11 @@ packages: resolve-pkg-maps@1.0.0: resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + resolve@1.22.10: + resolution: {integrity: sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==} + engines: {node: '>= 0.4'} + hasBin: true + rollup@4.32.1: resolution: {integrity: sha512-z+aeEsOeEa3mEbS1Tjl6sAZ8NE3+AalQz1RJGj81M+fizusbdDMoEJwdJNHfaB40Scr4qNu+welOfes7maKonA==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} @@ -1401,6 +1499,10 @@ packages: std-env@3.8.0: resolution: {integrity: sha512-Bc3YwwCB+OzldMxOXJIIvC6cPRWr/LxOp48CdQTOkPyk/t4JWWJbrilwBd7RJzKV8QW7tJkcgAmeuLLJugl5/w==} + supports-preserve-symlinks-flag@1.0.0: + resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} + engines: {node: '>= 0.4'} + svelte-check@4.1.4: resolution: {integrity: sha512-v0j7yLbT29MezzaQJPEDwksybTE2Ups9rUxEXy92T06TiA0cbqcO8wAOwNUVkFW6B0hsYHA+oAX3BS8b/2oHtw==} engines: {node: '>= 18.0.0'} @@ -1915,6 +2017,11 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.0 + '@js-temporal/polyfill@0.4.4': + dependencies: + jsbi: 4.3.0 + tslib: 2.8.1 + '@napi-rs/wasm-runtime@0.2.6': dependencies: '@emnapi/core': 1.3.1 @@ -1983,8 +2090,57 @@ snapshots: '@node-rs/argon2-win32-ia32-msvc': 2.0.2 '@node-rs/argon2-win32-x64-msvc': 2.0.2 + '@oslojs/asn1@1.0.0': + dependencies: + '@oslojs/binary': 1.0.0 + + '@oslojs/binary@1.0.0': {} + + '@oslojs/crypto@1.0.1': + dependencies: + '@oslojs/asn1': 1.0.0 + '@oslojs/binary': 1.0.0 + + '@oslojs/encoding@1.1.0': {} + '@polka/url@1.0.0-next.28': {} + '@rollup/plugin-commonjs@28.0.2(rollup@4.32.1)': + dependencies: + '@rollup/pluginutils': 5.1.4(rollup@4.32.1) + commondir: 1.0.1 + estree-walker: 2.0.2 + fdir: 6.4.3(picomatch@4.0.2) + is-reference: 1.2.1 + magic-string: 0.30.17 + picomatch: 4.0.2 + optionalDependencies: + rollup: 4.32.1 + + '@rollup/plugin-json@6.1.0(rollup@4.32.1)': + dependencies: + '@rollup/pluginutils': 5.1.4(rollup@4.32.1) + optionalDependencies: + rollup: 4.32.1 + + '@rollup/plugin-node-resolve@16.0.0(rollup@4.32.1)': + dependencies: + '@rollup/pluginutils': 5.1.4(rollup@4.32.1) + '@types/resolve': 1.20.2 + deepmerge: 4.3.1 + is-module: 1.0.0 + resolve: 1.22.10 + optionalDependencies: + rollup: 4.32.1 + + '@rollup/pluginutils@5.1.4(rollup@4.32.1)': + dependencies: + '@types/estree': 1.0.6 + estree-walker: 2.0.2 + picomatch: 4.0.2 + optionalDependencies: + rollup: 4.32.1 + '@rollup/rollup-android-arm-eabi@4.32.1': optional: true @@ -2042,10 +2198,13 @@ snapshots: '@rollup/rollup-win32-x64-msvc@4.32.1': optional: true - '@sveltejs/adapter-auto@4.0.0(@sveltejs/kit@2.16.1(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.19.5)(vite@6.0.11(@types/node@22.12.0)(tsx@4.19.2)))(svelte@5.19.5)(vite@6.0.11(@types/node@22.12.0)(tsx@4.19.2)))': + '@sveltejs/adapter-node@5.2.12(@sveltejs/kit@2.16.1(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.19.5)(vite@6.0.11(@types/node@22.12.0)(tsx@4.19.2)))(svelte@5.19.5)(vite@6.0.11(@types/node@22.12.0)(tsx@4.19.2)))': dependencies: + '@rollup/plugin-commonjs': 28.0.2(rollup@4.32.1) + '@rollup/plugin-json': 6.1.0(rollup@4.32.1) + '@rollup/plugin-node-resolve': 16.0.0(rollup@4.32.1) '@sveltejs/kit': 2.16.1(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.19.5)(vite@6.0.11(@types/node@22.12.0)(tsx@4.19.2)))(svelte@5.19.5)(vite@6.0.11(@types/node@22.12.0)(tsx@4.19.2)) - import-meta-resolve: 4.1.0 + rollup: 4.32.1 '@sveltejs/kit@2.16.1(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.19.5)(vite@6.0.11(@types/node@22.12.0)(tsx@4.19.2)))(svelte@5.19.5)(vite@6.0.11(@types/node@22.12.0)(tsx@4.19.2))': dependencies: @@ -2105,6 +2264,8 @@ snapshots: pg-protocol: 1.7.0 pg-types: 4.0.2 + '@types/resolve@1.20.2': {} + '@vitest/expect@3.0.4': dependencies: '@vitest/spy': 3.0.4 @@ -2177,6 +2338,8 @@ snapshots: clsx@2.1.1: {} + commondir@1.0.1: {} + cookie@0.6.0: {} debug@4.4.0: @@ -2330,6 +2493,8 @@ snapshots: dependencies: '@jridgewell/sourcemap-codec': 1.5.0 + estree-walker@2.0.2: {} + estree-walker@3.0.3: dependencies: '@types/estree': 1.0.6 @@ -2343,16 +2508,34 @@ snapshots: fsevents@2.3.3: optional: true + function-bind@1.1.2: {} + get-tsconfig@4.10.0: dependencies: resolve-pkg-maps: 1.0.0 + hasown@2.0.2: + dependencies: + function-bind: 1.1.2 + import-meta-resolve@4.1.0: {} + is-core-module@2.16.1: + dependencies: + hasown: 2.0.2 + + is-module@1.0.0: {} + + is-reference@1.2.1: + dependencies: + '@types/estree': 1.0.6 + is-reference@3.0.3: dependencies: '@types/estree': 1.0.6 + jsbi@4.3.0: {} + kleur@4.1.5: {} locate-character@3.0.0: {} @@ -2373,6 +2556,8 @@ snapshots: obuf@1.1.2: {} + path-parse@1.0.7: {} + pathe@2.0.2: {} pathval@2.0.0: {} @@ -2426,8 +2611,7 @@ snapshots: picocolors@1.1.1: {} - picomatch@4.0.2: - optional: true + picomatch@4.0.2: {} postcss@8.5.1: dependencies: @@ -2461,6 +2645,12 @@ snapshots: resolve-pkg-maps@1.0.0: {} + resolve@1.22.10: + dependencies: + is-core-module: 2.16.1 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + rollup@4.32.1: dependencies: '@types/estree': 1.0.6 @@ -2515,6 +2705,8 @@ snapshots: std-env@3.8.0: {} + supports-preserve-symlinks-flag@1.0.0: {} + svelte-check@4.1.4(picomatch@4.0.2)(svelte@5.19.5)(typescript@5.7.3): dependencies: '@jridgewell/trace-mapping': 0.3.25 @@ -2556,8 +2748,7 @@ snapshots: totalist@3.0.1: {} - tslib@2.8.1: - optional: true + tslib@2.8.1: {} tsx@4.19.2: dependencies: diff --git a/src/hooks.server.ts b/src/hooks.server.ts new file mode 100644 index 0000000..7e38205 --- /dev/null +++ b/src/hooks.server.ts @@ -0,0 +1,70 @@ +import { RefillingTokenBucket } from "$lib/server/rate-limit"; +import { + validateSessionToken, + setSessionTokenCookie, + deleteSessionTokenCookie, +} from "$lib/server/session"; +import { sequence } from "@sveltejs/kit/hooks"; + +import { redirect, type Handle } from "@sveltejs/kit"; + +const bucket = new RefillingTokenBucket(100, 1); + +const rateLimitHandle: Handle = async ({ event, resolve }) => { + // Note: Assumes X-Forwarded-For will always be defined. + const clientIP = event.request.headers.get("X-Forwarded-For"); + if (clientIP === null) { + return resolve(event); + } + let cost: number; + if (event.request.method === "GET" || event.request.method === "OPTIONS") { + cost = 1; + } else { + cost = 3; + } + if (!bucket.consume(clientIP, cost)) { + return new Response("Too many requests", { + status: 429, + }); + } + return resolve(event); +}; + +const authHandle: Handle = async ({ event, resolve }) => { + const token = event.cookies.get("session") ?? null; + if (token === null) { + event.locals.user = null; + event.locals.session = null; + return resolve(event); + } + + const { session, user } = await validateSessionToken(token); + if (session !== null) { + setSessionTokenCookie(event, token, session.expiresAt); + } else { + deleteSessionTokenCookie(event); + } + + event.locals.session = session; + event.locals.user = user; + return resolve(event); +}; + +const enforceAuthenticated: Handle = async ({ event, resolve }) => { + const routeExceptions = ["/login"]; + if (event.route.id !== null && routeExceptions.includes(event.route.id)) { + return resolve(event); + } + + if (!(event.locals.session && event.locals.user)) { + redirect(303, "/login"); + } + + return resolve(event); +}; + +export const handle = sequence( + rateLimitHandle, + authHandle, + enforceAuthenticated, +); diff --git a/src/lib/server/db.ts b/src/lib/server/db.ts index 8f77600..918a4c8 100644 --- a/src/lib/server/db.ts +++ b/src/lib/server/db.ts @@ -1,10 +1,10 @@ -import { DATABASE_PASSWORD } from "$env/static/private"; +import { DATABASE_URL } from "$env/static/private"; import { drizzle } from "drizzle-orm/node-postgres"; import * as schema from "./schema"; export const db = drizzle({ - connection: `postgres://postgres:${DATABASE_PASSWORD}@database/kredens`, + connection: DATABASE_URL, casing: "snake_case", schema, }); diff --git a/src/lib/server/schema.ts b/src/lib/server/schema.ts index bc7cc53..706af26 100644 --- a/src/lib/server/schema.ts +++ b/src/lib/server/schema.ts @@ -1,15 +1,30 @@ -import { integer, pgTable, varchar, text, timestamp } from "drizzle-orm/pg-core"; +import { relations } from "drizzle-orm"; +import { + integer, + pgTable, + varchar, + text, + timestamp, +} from "drizzle-orm/pg-core"; export const users = pgTable("users", { id: integer().primaryKey().generatedAlwaysAsIdentity(), - username: varchar({length: 128, }).unique().notNull(), - password_hash: text(), + username: varchar({ length: 128 }).unique().notNull(), + passwordHash: text(), displayName: text(), }); export const sessions = pgTable("sessions", { - id: integer().primaryKey().generatedAlwaysAsIdentity(), - userId: integer().references(() => users.id), - expiresAt: timestamp({withTimezone: true}).notNull(), + id: text().primaryKey(), + userId: integer() + .notNull() + .references(() => users.id), + expiresAt: timestamp({ withTimezone: true }).notNull(), }); +export const sessionRelations = relations(sessions, ({ one }) => ({ + user: one(users, { + fields: [ sessions.userId ], + references: [users.id], + }), +})); diff --git a/src/lib/server/session.ts b/src/lib/server/session.ts index 41f22ee..7ea83b6 100644 --- a/src/lib/server/session.ts +++ b/src/lib/server/session.ts @@ -1,5 +1,123 @@ +import { + encodeBase32LowerCaseNoPadding, + encodeHexLowerCase, +} from "@oslojs/encoding"; +import { sha256 } from "@oslojs/crypto/sha2"; +import type { RequestEvent } from "@sveltejs/kit"; +import { eq } from "drizzle-orm"; +import { Temporal, toTemporalInstant } from "@js-temporal/polyfill"; + +import { db } from "./db"; +import { users, sessions } from "./schema"; +import type { User } from "./user"; + +const sessionExpirySeconds = 30 * 86400; +const sessionExpiry = new Temporal.Duration(0, 0, 0, 0, 0, 0, sessionExpirySeconds); +const sessionRenewal = new Temporal.Duration(0, 0, 0, 0, 0, 0, sessionExpirySeconds/2); + +export async function validateSessionToken( + token: string, +): Promise { + const sessionId = encodeHexLowerCase(sha256(new TextEncoder().encode(token))); + + const row = await db.query.sessions.findFirst({ + with: { + user: true, + }, + where: (sessions, { eq }) => eq(sessions.id, sessionId), + }); + + if (!row) { + return { session: null, user: null }; + } + const session: Session = { + id: row.id, + userId: row.userId, + expiresAt: row.expiresAt, + }; + + const user: User = { + id: row.userId, + username: row.user.username, + displayName: row.user.displayName || undefined, + }; + + const now = Temporal.Now.instant(); + const expiresAt = toTemporalInstant.apply(session.expiresAt); + + if (Temporal.Instant.compare(now, expiresAt) > 0) { + await db.delete(sessions).where(eq(sessions.id, session.id)); + return { session: null, user: null }; + } + + if (Temporal.Instant.compare(now.add(sessionRenewal), expiresAt) > 0) { + session.expiresAt = new Date(now.add(sessionExpiry).epochMilliseconds); + await db.update(sessions) + .set({ expiresAt: session.expiresAt }) + .where(eq(sessions.id, session.id)); + } + + return { session, user }; +} + +export async function invalidateSession(sessionId: string): Promise { + await db.delete(sessions).where(eq(sessions.id, sessionId)); +} + +export async function invalidateUserSessions(userId: number): Promise { + await db.delete(sessions).where(eq(sessions.userId, userId)); +} + +export function setSessionTokenCookie( + event: RequestEvent, + token: string, + expiresAt: Date, +): void { + event.cookies.set("session", token, { + httpOnly: true, + path: "/", + secure: import.meta.env.PROD, + sameSite: "lax", + expires: expiresAt, + }); +} + +export function deleteSessionTokenCookie(event: RequestEvent): void { + event.cookies.set("session", "", { + httpOnly: true, + path: "/", + secure: import.meta.env.PROD, + sameSite: "lax", + maxAge: 0, + }); +} + +export function generateSessionToken(): string { + const tokenBytes = new Uint8Array(20); + crypto.getRandomValues(tokenBytes); + const token = encodeBase32LowerCaseNoPadding(tokenBytes).toLowerCase(); + return token; +} + +export async function createSession(token: string, userId: number): Promise { + const sessionId = encodeHexLowerCase(sha256(new TextEncoder().encode(token))); + const session: Session = { + id: sessionId, + userId, + expiresAt: new Date( + Temporal.Now.instant().add(sessionExpiry).epochMilliseconds, + ), + }; + await db.insert(sessions).values(session); + return session; +} + export interface Session { id: string; expiresAt: Date; userId: number; } + +type SessionValidationResult = + | { session: Session; user: User } + | { session: null; user: null }; diff --git a/src/lib/server/user.ts b/src/lib/server/user.ts index 7a107d5..ea34cd1 100644 --- a/src/lib/server/user.ts +++ b/src/lib/server/user.ts @@ -1,5 +1,45 @@ +import { eq } from "drizzle-orm"; +import { db } from "./db"; +import { users } from "./schema"; + export interface User { id: number; username: string; - displayName: string; + displayName?: string; +} + + +export async function getUserFromUsername(username: string): Promise { + const user = await db.query.users.findFirst({ + columns: { + id: true, + username: true, + displayName: true + }, + where: eq(users.username, username) + }); + + if (!user) { + return null; + } + + return { + ...user, + displayName: user.displayName || undefined, + }; +} + +export async function getUserPasswordHash(userId: number): Promise { + const user = (await db.query.users.findFirst({ + columns: { + passwordHash: true, + }, + where: eq(users.id, userId) + })); + + if (user === undefined) { + throw new Error("Invalid user ID"); + } + + return user.passwordHash; } \ No newline at end of file diff --git a/src/routes/+layout.server.ts b/src/routes/+layout.server.ts new file mode 100644 index 0000000..956e9c5 --- /dev/null +++ b/src/routes/+layout.server.ts @@ -0,0 +1,7 @@ +import type { LayoutServerLoad } from "./$types"; + +export const load: LayoutServerLoad = async (event) => { + return { + username: event.locals.user?.displayName || event.locals.user?.username, + }; +}; diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte new file mode 100644 index 0000000..1bb8782 --- /dev/null +++ b/src/routes/+layout.svelte @@ -0,0 +1,16 @@ + + + + +{@render children()} \ No newline at end of file diff --git a/src/routes/+page.server.ts b/src/routes/+page.server.ts deleted file mode 100644 index 8c360b4..0000000 --- a/src/routes/+page.server.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { count } from "drizzle-orm"; - -import { db } from "$lib/server/db"; -import { users } from "$lib/server/schema"; - -import type { PageServerLoad } from "./$types"; - -export const load: PageServerLoad = async () => { - return { - c: (await db.select({ value: count() }).from(users))[0] - }; -}; diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index f4927cb..fdb18b3 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -6,5 +6,3 @@

Welcome to SvelteKit

Visit svelte.dev/docs/kit to read the documentation

- -

There is { data.c.value } users!

\ No newline at end of file diff --git a/src/routes/login/+page.server.ts b/src/routes/login/+page.server.ts index d53ed60..d9c3e8a 100644 --- a/src/routes/login/+page.server.ts +++ b/src/routes/login/+page.server.ts @@ -1,18 +1,87 @@ -import { redirect } from "@sveltejs/kit"; -import { RefillingTokenBucket, Throttler } from "$lib/server/rate-limit"; +import { fail, redirect } from "@sveltejs/kit"; -import type { Actions, PageServerLoad, PageServerLoadEvent } from "./$types"; + +import { RefillingTokenBucket, Throttler } from "$lib/server/rate-limit"; +import type { Actions, PageServerLoad, PageServerLoadEvent, RequestEvent } from "./$types"; +import { getUserFromUsername, getUserPasswordHash } from "$lib/server/user"; +import { verifyPasswordHash } from "$lib/server/password"; +import { createSession, generateSessionToken, setSessionTokenCookie } from "$lib/server/session"; const throttler = new Throttler([0, 1, 2, 4, 8, 16, 30, 60, 180, 300]); const ipBucket = new RefillingTokenBucket(20, 1); export const load: PageServerLoad = (event: PageServerLoadEvent) => { if (event.locals.session !== null && event.locals.user !== null) { - return redirect(302, "/"); + redirect(303, "/"); } return {}; }; -export const actions = { - default: async (event) => {}, -} satisfies Actions; +export const actions: Actions = { + default: action +}; + +async function action(event: RequestEvent) { + // TODO: Assumes X-Forwarded-For is always included. + const clientIP = event.request.headers.get("X-Forwarded-For"); + if (clientIP !== null && !ipBucket.check(clientIP, 1)) { + return fail(429, { + message: "Too many requests", + email: "" + }); + } + + const formData = await event.request.formData(); + const username = formData.get("username"); + const password = formData.get("password"); + if (typeof username !== "string" || typeof password !== "string") { + return fail(400, { + message: "Invalid or missing fields", + username: "" + }); + } + if (username === "" || password === "") { + return fail(400, { + message: "Please enter your username and password.", + username: username + }); + } + const user = await getUserFromUsername(username); + if (user === null) { + return fail(400, { + message: "Account does not exist", + username: username + }); + } + if (clientIP !== null && !ipBucket.consume(clientIP, 1)) { + return fail(429, { + message: "Too many requests", + username: "" + }); + } + if (!throttler.consume(user.id)) { + return fail(429, { + message: "Too many requests", + username: "" + }); + } + const passwordHash = await getUserPasswordHash(user.id); + if (passwordHash === null) { + return fail(400, { + message: "Account locked", + username + }) + } + const validPassword = await verifyPasswordHash(passwordHash, password); + if (!validPassword) { + return fail(400, { + message: "Invalid password", + username + }); + } + throttler.reset(user.id); + const sessionToken = generateSessionToken(); + const session = await createSession(sessionToken, user.id); + setSessionTokenCookie(event, sessionToken, session.expiresAt); + return redirect(303, "/"); +} \ No newline at end of file diff --git a/src/routes/login/+page.svelte b/src/routes/login/+page.svelte index e69de29..d38f4fa 100644 --- a/src/routes/login/+page.svelte +++ b/src/routes/login/+page.svelte @@ -0,0 +1,27 @@ + +
+ +
+ +
+ +

{form?.message ?? ""}

+
diff --git a/src/routes/logout/+page.server.ts b/src/routes/logout/+page.server.ts new file mode 100644 index 0000000..ee4594d --- /dev/null +++ b/src/routes/logout/+page.server.ts @@ -0,0 +1,28 @@ +import { fail, redirect, type Actions } from "@sveltejs/kit"; + +import type { PageServerLoad } from "./$types"; +import { + deleteSessionTokenCookie, + invalidateSession, +} from "$lib/server/session"; + +export const load: PageServerLoad = (event) => { + if (event.locals.session !== null && event.locals.user !== null) { + return redirect(302, "/"); + } + return {}; +}; + +export const actions = { + default: async (event) => { + if (event.locals.session === null) { + return fail(401, { + message: "Not authenticated", + }); + } + + await invalidateSession(event.locals.session.id); + deleteSessionTokenCookie(event); + return redirect(303, "/login"); + }, +} satisfies Actions; diff --git a/svelte.config.js b/svelte.config.js index aa0ebf0..fd39733 100644 --- a/svelte.config.js +++ b/svelte.config.js @@ -1,4 +1,4 @@ -import adapter from "@sveltejs/adapter-auto"; +import adapter from "@sveltejs/adapter-node"; import { vitePreprocess } from "@sveltejs/vite-plugin-svelte"; /** @type {import('@sveltejs/kit').Config} */ @@ -13,6 +13,9 @@ const config = { // See https://svelte.dev/docs/kit/adapters for more information about adapters. adapter: adapter(), }, + compilerOptions: { + runes: true, + } }; export default config; diff --git a/vite.config.ts b/vite.config.ts index 751f380..96ad031 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -6,5 +6,5 @@ export default defineConfig({ test: { include: ["src/**/*.{test,spec}.{js,ts}"], - }, + } });