Auth is actually working

This commit is contained in:
Sandra Modzelewska 2025-01-30 03:01:44 +00:00
parent 32281aa1ff
commit 15ff9609c4
No known key found for this signature in database
22 changed files with 631 additions and 74 deletions

View File

@ -1,2 +1,2 @@
DATABASE_USER=postgres
DATABASE_HOST=database
DATABASE_HOST=database

View File

@ -11,7 +11,7 @@ localhost {
handle {
reverse_proxy dev:5173 {
header_up Host {host}
header_up Origin "^https://localhost$" "http://localhost"
}
}

View File

@ -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

View File

@ -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",

View File

@ -5,7 +5,7 @@
{
"idx": 0,
"version": "7",
"when": 1738177092891,
"when": 1738197417069,
"tag": "0000_users-and-sessions",
"breakpoints": true
}

View File

@ -1,8 +0,0 @@
import { loadEnv } from 'vite';
console.log(process.env);
const env = loadEnv("development", ".", "");
// console.log(env);
// console.log(process.env)

View File

@ -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"
}

215
pnpm-lock.yaml generated
View File

@ -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:

70
src/hooks.server.ts Normal file
View File

@ -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<string>(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,
);

View File

@ -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,
});

View File

@ -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],
}),
}));

View File

@ -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<SessionValidationResult> {
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<void> {
await db.delete(sessions).where(eq(sessions.id, sessionId));
}
export async function invalidateUserSessions(userId: number): Promise<void> {
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<Session> {
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 };

View File

@ -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<User | null> {
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<string|null> {
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;
}

View File

@ -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,
};
};

16
src/routes/+layout.svelte Normal file
View File

@ -0,0 +1,16 @@
<script lang="ts">
const { data, children } = $props();
</script>
<nav>
{#if data.username}
<div>
<p>Logged in as {data.username}.</p>
<form method="POST" action="/logout">
<button>Log out</button>
</form>
</div>
{/if}
</nav>
{@render children()}

View File

@ -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]
};
};

View File

@ -6,5 +6,3 @@
<h1>Welcome to SvelteKit</h1>
<p>Visit <a href="https://svelte.dev/docs/kit">svelte.dev/docs/kit</a> to read the documentation</p>
<p>There is { data.c.value } users!</p>

View File

@ -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<number>([0, 1, 2, 4, 8, 16, 30, 60, 180, 300]);
const ipBucket = new RefillingTokenBucket<string>(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, "/");
}

View File

@ -0,0 +1,27 @@
<script lang="ts">
import { enhance } from "$app/forms";
import type { PageProps } from "./$types";
const { data, form }: PageProps = $props();
</script>
<form method="post" use:enhance>
<label for="form-login.username">Username</label>
<input
id="form-login.username"
name="username"
autocomplete="username"
required
value={form?.username || ""}
/><br />
<label for="form-login.password">Password</label>
<input
type="password"
id="form-login.password"
name="password"
autocomplete="current-password"
required
/><br />
<button>Continue</button>
<p>{form?.message ?? ""}</p>
</form>

View File

@ -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;

View File

@ -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;

View File

@ -6,5 +6,5 @@ export default defineConfig({
test: {
include: ["src/**/*.{test,spec}.{js,ts}"],
},
}
});