diff --git a/components.json b/components.json
new file mode 100644
index 0000000..ee7d463
--- /dev/null
+++ b/components.json
@@ -0,0 +1,14 @@
+{
+ "$schema": "https://shadcn-svelte.com/schema.json",
+ "style": "default",
+ "tailwind": {
+ "config": "tailwind.config.ts",
+ "css": "src/app.css",
+ "baseColor": "neutral"
+ },
+ "aliases": {
+ "components": "$lib/components",
+ "utils": "$lib/utils"
+ },
+ "typescript": true
+}
\ No newline at end of file
diff --git a/package-lock.json b/package-lock.json
index 04ab7d8..07823aa 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -7,6 +7,13 @@
"": {
"name": "identity",
"version": "0.0.1",
+ "dependencies": {
+ "@prisma/client": "^6.2.1",
+ "bcryptjs": "^2.4.3",
+ "mode-watcher": "^0.5.1",
+ "prisma": "^6.2.1",
+ "zod": "^3.24.1"
+ },
"devDependencies": {
"@eslint/compat": "^1.2.5",
"@eslint/js": "^9.18.0",
@@ -15,7 +22,11 @@
"@sveltejs/vite-plugin-svelte": "^5.0.0",
"@tailwindcss/forms": "^0.5.10",
"@tailwindcss/typography": "^0.5.16",
+ "@types/bcryptjs": "^2.4.6",
+ "@types/node": "^22.10.10",
"autoprefixer": "^10.4.20",
+ "bits-ui": "^0.22.0",
+ "clsx": "^2.1.1",
"eslint": "^9.18.0",
"eslint-config-prettier": "^10.0.1",
"eslint-plugin-svelte": "^2.46.1",
@@ -24,6 +35,8 @@
"prettier-plugin-svelte": "^3.3.3",
"svelte": "^5.0.0",
"svelte-check": "^4.0.0",
+ "tailwind-merge": "^2.6.0",
+ "tailwind-variants": "^0.3.1",
"tailwindcss": "^3.4.17",
"typescript": "^5.0.0",
"typescript-eslint": "^8.20.0",
@@ -47,7 +60,6 @@
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz",
"integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==",
- "dev": true,
"license": "Apache-2.0",
"dependencies": {
"@jridgewell/gen-mapping": "^0.3.5",
@@ -641,6 +653,34 @@
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
}
},
+ "node_modules/@floating-ui/core": {
+ "version": "1.6.9",
+ "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.9.tgz",
+ "integrity": "sha512-uMXCuQ3BItDUbAMhIXw7UPXRfAlOAvZzdK9BWpE60MCn+Svt3aLn9jsPTi/WNGlRUu2uI0v5S7JiIUsbsvh3fw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@floating-ui/utils": "^0.2.9"
+ }
+ },
+ "node_modules/@floating-ui/dom": {
+ "version": "1.6.13",
+ "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.13.tgz",
+ "integrity": "sha512-umqzocjDgNRGTuO7Q8CU32dkHkECqI8ZdMZ5Swb6QAM0t5rnlrN3lGo1hdpscRd3WS8T6DKYK4ephgIH9iRh3w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@floating-ui/core": "^1.6.0",
+ "@floating-ui/utils": "^0.2.9"
+ }
+ },
+ "node_modules/@floating-ui/utils": {
+ "version": "0.2.9",
+ "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.9.tgz",
+ "integrity": "sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/@humanfs/core": {
"version": "0.19.1",
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
@@ -707,6 +747,16 @@
"url": "https://github.com/sponsors/nzakas"
}
},
+ "node_modules/@internationalized/date": {
+ "version": "3.7.0",
+ "resolved": "https://registry.npmjs.org/@internationalized/date/-/date-3.7.0.tgz",
+ "integrity": "sha512-VJ5WS3fcVx0bejE/YHfbDKR/yawZgKqn/if+oEeLqNwBtPzVB06olkfcnojTmEMX+gTpH+FlQ69SHNitJ8/erQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@swc/helpers": "^0.5.0"
+ }
+ },
"node_modules/@isaacs/cliui": {
"version": "8.0.2",
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
@@ -729,7 +779,6 @@
"version": "0.3.8",
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz",
"integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==",
- "dev": true,
"license": "MIT",
"dependencies": {
"@jridgewell/set-array": "^1.2.1",
@@ -744,7 +793,6 @@
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
"integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">=6.0.0"
@@ -754,7 +802,6 @@
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz",
"integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">=6.0.0"
@@ -764,14 +811,12 @@
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz",
"integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==",
- "dev": true,
"license": "MIT"
},
"node_modules/@jridgewell/trace-mapping": {
"version": "0.3.25",
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz",
"integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==",
- "dev": true,
"license": "MIT",
"dependencies": {
"@jridgewell/resolve-uri": "^3.1.0",
@@ -834,6 +879,69 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/@prisma/client": {
+ "version": "6.2.1",
+ "resolved": "https://registry.npmjs.org/@prisma/client/-/client-6.2.1.tgz",
+ "integrity": "sha512-msKY2iRLISN8t5X0Tj7hU0UWet1u0KuxSPHWuf3IRkB4J95mCvGpyQBfQ6ufcmvKNOMQSq90O2iUmJEN2e5fiA==",
+ "hasInstallScript": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=18.18"
+ },
+ "peerDependencies": {
+ "prisma": "*"
+ },
+ "peerDependenciesMeta": {
+ "prisma": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@prisma/debug": {
+ "version": "6.2.1",
+ "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-6.2.1.tgz",
+ "integrity": "sha512-0KItvt39CmQxWkEw6oW+RQMD6RZ43SJWgEUnzxN8VC9ixMysa7MzZCZf22LCK5DSooiLNf8vM3LHZm/I/Ni7bQ==",
+ "license": "Apache-2.0"
+ },
+ "node_modules/@prisma/engines": {
+ "version": "6.2.1",
+ "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-6.2.1.tgz",
+ "integrity": "sha512-lTBNLJBCxVT9iP5I7Mn6GlwqAxTpS5qMERrhebkUhtXpGVkBNd/jHnNJBZQW4kGDCKaQg/r2vlJYkzOHnAb7ZQ==",
+ "hasInstallScript": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@prisma/debug": "6.2.1",
+ "@prisma/engines-version": "6.2.0-14.4123509d24aa4dede1e864b46351bf2790323b69",
+ "@prisma/fetch-engine": "6.2.1",
+ "@prisma/get-platform": "6.2.1"
+ }
+ },
+ "node_modules/@prisma/engines-version": {
+ "version": "6.2.0-14.4123509d24aa4dede1e864b46351bf2790323b69",
+ "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-6.2.0-14.4123509d24aa4dede1e864b46351bf2790323b69.tgz",
+ "integrity": "sha512-7tw1qs/9GWSX6qbZs4He09TOTg1ff3gYsB3ubaVNN0Pp1zLm9NC5C5MZShtkz7TyQjx7blhpknB7HwEhlG+PrQ==",
+ "license": "Apache-2.0"
+ },
+ "node_modules/@prisma/fetch-engine": {
+ "version": "6.2.1",
+ "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-6.2.1.tgz",
+ "integrity": "sha512-OO7O9d6Mrx2F9i+Gu1LW+DGXXyUFkP7OE5aj9iBfA/2jjDXEJjqa9X0ZmM9NZNo8Uo7ql6zKm6yjDcbAcRrw1A==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@prisma/debug": "6.2.1",
+ "@prisma/engines-version": "6.2.0-14.4123509d24aa4dede1e864b46351bf2790323b69",
+ "@prisma/get-platform": "6.2.1"
+ }
+ },
+ "node_modules/@prisma/get-platform": {
+ "version": "6.2.1",
+ "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-6.2.1.tgz",
+ "integrity": "sha512-zp53yvroPl5m5/gXYLz7tGCNG33bhG+JYCm74ohxOq1pPnrL47VQYFfF3RbTZ7TzGWCrR3EtoiYMywUBw7UK6Q==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@prisma/debug": "6.2.1"
+ }
+ },
"node_modules/@rollup/rollup-android-arm-eabi": {
"version": "4.32.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.32.0.tgz",
@@ -1184,6 +1292,16 @@
"vite": "^6.0.0"
}
},
+ "node_modules/@swc/helpers": {
+ "version": "0.5.15",
+ "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz",
+ "integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "tslib": "^2.8.0"
+ }
+ },
"node_modules/@tailwindcss/forms": {
"version": "0.5.10",
"resolved": "https://registry.npmjs.org/@tailwindcss/forms/-/forms-0.5.10.tgz",
@@ -1213,6 +1331,13 @@
"tailwindcss": ">=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1"
}
},
+ "node_modules/@types/bcryptjs": {
+ "version": "2.4.6",
+ "resolved": "https://registry.npmjs.org/@types/bcryptjs/-/bcryptjs-2.4.6.tgz",
+ "integrity": "sha512-9xlo6R2qDs5uixm0bcIqCeMCE6HiQsIyel9KQySStiyqNl2tnj2mP3DX1Nf56MD6KMenNNlBBsy3LJ7gUEQPXQ==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/@types/cookie": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz",
@@ -1224,7 +1349,6 @@
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz",
"integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==",
- "dev": true,
"license": "MIT"
},
"node_modules/@types/json-schema": {
@@ -1234,6 +1358,16 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/@types/node": {
+ "version": "22.10.10",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.10.tgz",
+ "integrity": "sha512-X47y/mPNzxviAGY5TcYPtYL8JsY3kAq2n8fMmKoRCxq/c4v4pyGNCzM2R6+M5/umG4ZfHuT+sgqDYqWc9rJ6ww==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "undici-types": "~6.20.0"
+ }
+ },
"node_modules/@typescript-eslint/eslint-plugin": {
"version": "8.21.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.21.0.tgz",
@@ -1444,7 +1578,6 @@
"version": "8.14.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz",
"integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==",
- "dev": true,
"license": "MIT",
"bin": {
"acorn": "bin/acorn"
@@ -1467,7 +1600,6 @@
"version": "1.4.13",
"resolved": "https://registry.npmjs.org/acorn-typescript/-/acorn-typescript-1.4.13.tgz",
"integrity": "sha512-xsc9Xv0xlVfwp2o7sQ+GCQ1PgbkdcpWdTzrwXxO3xDMTAywVS3oXVOcOHuRjAPkS4P9b+yc/qNF15460v+jp4Q==",
- "dev": true,
"license": "MIT",
"peerDependencies": {
"acorn": ">=8.9.0"
@@ -1540,6 +1672,19 @@
"node": ">= 8"
}
},
+ "node_modules/anymatch/node_modules/picomatch": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
+ "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
"node_modules/arg": {
"version": "5.0.2",
"resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz",
@@ -1558,7 +1703,6 @@
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz",
"integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==",
- "dev": true,
"license": "Apache-2.0",
"engines": {
"node": ">= 0.4"
@@ -1606,7 +1750,6 @@
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz",
"integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==",
- "dev": true,
"license": "Apache-2.0",
"engines": {
"node": ">= 0.4"
@@ -1619,6 +1762,12 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/bcryptjs": {
+ "version": "2.4.3",
+ "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.4.3.tgz",
+ "integrity": "sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ==",
+ "license": "MIT"
+ },
"node_modules/binary-extensions": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
@@ -1632,6 +1781,61 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/bits-ui": {
+ "version": "0.22.0",
+ "resolved": "https://registry.npmjs.org/bits-ui/-/bits-ui-0.22.0.tgz",
+ "integrity": "sha512-r7Fw1HNgA4YxZBRcozl7oP0bheQ8EHh+kfMBZJgyFISix8t4p/nqDcHLmBgIiJ3T5XjYnJRorYDjIWaCfhb5fw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@internationalized/date": "^3.5.1",
+ "@melt-ui/svelte": "0.76.2",
+ "nanoid": "^5.0.5"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/huntabyte"
+ },
+ "peerDependencies": {
+ "svelte": "^4.0.0 || ^5.0.0"
+ }
+ },
+ "node_modules/bits-ui/node_modules/@melt-ui/svelte": {
+ "version": "0.76.2",
+ "resolved": "https://registry.npmjs.org/@melt-ui/svelte/-/svelte-0.76.2.tgz",
+ "integrity": "sha512-7SbOa11tXUS95T3fReL+dwDs5FyJtCEqrqG3inRziDws346SYLsxOQ6HmX+4BkIsQh1R8U3XNa+EMmdMt38lMA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@floating-ui/core": "^1.3.1",
+ "@floating-ui/dom": "^1.4.5",
+ "@internationalized/date": "^3.5.0",
+ "dequal": "^2.0.3",
+ "focus-trap": "^7.5.2",
+ "nanoid": "^5.0.4"
+ },
+ "peerDependencies": {
+ "svelte": ">=3 <5"
+ }
+ },
+ "node_modules/bits-ui/node_modules/nanoid": {
+ "version": "5.0.9",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.0.9.tgz",
+ "integrity": "sha512-Aooyr6MXU6HpvvWXKoVoXwKMs/KyVakWwg7xQfv5/S/RIgJMy0Ifa45H9qqYy7pTCszrHzP21Uk4PZq2HpEM8Q==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "bin": {
+ "nanoid": "bin/nanoid.js"
+ },
+ "engines": {
+ "node": "^18 || >=20"
+ }
+ },
"node_modules/brace-expansion": {
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
@@ -1767,7 +1971,6 @@
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
"integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">=6"
@@ -1883,6 +2086,16 @@
"node": ">=0.10.0"
}
},
+ "node_modules/dequal": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
+ "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/devalue": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/devalue/-/devalue-5.1.1.tgz",
@@ -2161,7 +2374,6 @@
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.2.2.tgz",
"integrity": "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==",
- "dev": true,
"license": "MIT"
},
"node_modules/espree": {
@@ -2199,7 +2411,6 @@
"version": "1.4.3",
"resolved": "https://registry.npmjs.org/esrap/-/esrap-1.4.3.tgz",
"integrity": "sha512-Xddc1RsoFJ4z9nR7W7BFaEPIp4UXoeQ0+077UdWLxbafMQFyU79sQJMk7kxNgRwQ9/aVgaKacCHC2pUACGwmYw==",
- "dev": true,
"license": "MIT",
"dependencies": {
"@jridgewell/sourcemap-codec": "^1.4.15"
@@ -2378,6 +2589,16 @@
"dev": true,
"license": "ISC"
},
+ "node_modules/focus-trap": {
+ "version": "7.6.4",
+ "resolved": "https://registry.npmjs.org/focus-trap/-/focus-trap-7.6.4.tgz",
+ "integrity": "sha512-xx560wGBk7seZ6y933idtjJQc1l+ck+pI3sKvhKozdBV1dRZoKhkW5xoCaFv9tQiX5RH1xfSxjuNu6g+lmN/gw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "tabbable": "^6.2.0"
+ }
+ },
"node_modules/foreground-child": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz",
@@ -2413,7 +2634,6 @@
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
- "dev": true,
"hasInstallScript": true,
"license": "MIT",
"optional": true,
@@ -2661,7 +2881,6 @@
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.3.tgz",
"integrity": "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==",
- "dev": true,
"license": "MIT",
"dependencies": {
"@types/estree": "^1.0.6"
@@ -2796,7 +3015,6 @@
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/locate-character/-/locate-character-3.0.0.tgz",
"integrity": "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==",
- "dev": true,
"license": "MIT"
},
"node_modules/locate-path": {
@@ -2847,7 +3065,6 @@
"version": "0.30.17",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz",
"integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==",
- "dev": true,
"license": "MIT",
"dependencies": {
"@jridgewell/sourcemap-codec": "^1.5.0"
@@ -2877,6 +3094,19 @@
"node": ">=8.6"
}
},
+ "node_modules/micromatch/node_modules/picomatch": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
+ "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
"node_modules/mini-svg-data-uri": {
"version": "1.4.4",
"resolved": "https://registry.npmjs.org/mini-svg-data-uri/-/mini-svg-data-uri-1.4.4.tgz",
@@ -2910,6 +3140,15 @@
"node": ">=16 || 14 >=14.17"
}
},
+ "node_modules/mode-watcher": {
+ "version": "0.5.1",
+ "resolved": "https://registry.npmjs.org/mode-watcher/-/mode-watcher-0.5.1.tgz",
+ "integrity": "sha512-adEC6T7TMX/kzQlaO/MtiQOSFekZfQu4MC+lXyoceQG+U5sKpJWZ4yKXqw846ExIuWJgedkOIPqAYYRk/xHm+w==",
+ "license": "MIT",
+ "peerDependencies": {
+ "svelte": "^4.0.0 || ^5.0.0-next.1"
+ }
+ },
"node_modules/mri": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz",
@@ -3144,13 +3383,15 @@
"license": "ISC"
},
"node_modules/picomatch": {
- "version": "2.3.1",
- "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
- "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz",
+ "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==",
"dev": true,
"license": "MIT",
+ "optional": true,
+ "peer": true,
"engines": {
- "node": ">=8.6"
+ "node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/jonschlinkert"
@@ -3425,6 +3666,25 @@
"svelte": "^3.2.0 || ^4.0.0-next.0 || ^5.0.0-next.0"
}
},
+ "node_modules/prisma": {
+ "version": "6.2.1",
+ "resolved": "https://registry.npmjs.org/prisma/-/prisma-6.2.1.tgz",
+ "integrity": "sha512-hhyM0H13pQleQ+br4CkzGizS5I0oInoeTw3JfLw1BRZduBSQxPILlJLwi+46wZzj9Je7ndyQEMGw/n5cN2fknA==",
+ "hasInstallScript": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@prisma/engines": "6.2.1"
+ },
+ "bin": {
+ "prisma": "build/index.js"
+ },
+ "engines": {
+ "node": ">=18.18"
+ },
+ "optionalDependencies": {
+ "fsevents": "2.3.3"
+ }
+ },
"node_modules/punycode": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
@@ -3849,7 +4109,6 @@
"version": "5.19.2",
"resolved": "https://registry.npmjs.org/svelte/-/svelte-5.19.2.tgz",
"integrity": "sha512-Ww1uLgdX5MdQrAO5zfU1dWUh6zqiPR6uIbwqm8a+4eQ+tNEYHRPgypvKKfHh9lmTkmJ30PWZ2O5qX8aS+PblRQ==",
- "dev": true,
"license": "MIT",
"dependencies": {
"@ampproject/remapping": "^2.3.0",
@@ -3971,6 +4230,52 @@
"url": "https://opencollective.com/eslint"
}
},
+ "node_modules/tabbable": {
+ "version": "6.2.0",
+ "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.2.0.tgz",
+ "integrity": "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/tailwind-merge": {
+ "version": "2.6.0",
+ "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-2.6.0.tgz",
+ "integrity": "sha512-P+Vu1qXfzediirmHOC3xKGAYeZtPcV9g76X+xg2FD4tYgR71ewMA35Y3sCz3zhiN/dwefRpJX0yBcgwi1fXNQA==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/dcastil"
+ }
+ },
+ "node_modules/tailwind-variants": {
+ "version": "0.3.1",
+ "resolved": "https://registry.npmjs.org/tailwind-variants/-/tailwind-variants-0.3.1.tgz",
+ "integrity": "sha512-krn67M3FpPwElg4FsZrOQd0U26o7UDH/QOkK8RNaiCCrr052f6YJPBUfNKnPo/s/xRzNPtv1Mldlxsg8Tb46BQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "tailwind-merge": "2.5.4"
+ },
+ "engines": {
+ "node": ">=16.x",
+ "pnpm": ">=7.x"
+ },
+ "peerDependencies": {
+ "tailwindcss": "*"
+ }
+ },
+ "node_modules/tailwind-variants/node_modules/tailwind-merge": {
+ "version": "2.5.4",
+ "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-2.5.4.tgz",
+ "integrity": "sha512-0q8cfZHMu9nuYP/b5Shb7Y7Sh1B7Nnl5GqNr1U+n2p6+mybvRtayrQ+0042Z5byvTA8ihjlP8Odo8/VnHbZu4Q==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/dcastil"
+ }
+ },
"node_modules/tailwindcss": {
"version": "3.4.17",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.17.tgz",
@@ -4060,6 +4365,19 @@
"url": "https://github.com/sponsors/antonk52"
}
},
+ "node_modules/tailwindcss/node_modules/picomatch": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
+ "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
"node_modules/tailwindcss/node_modules/postcss-load-config": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.2.tgz",
@@ -4189,6 +4507,13 @@
"dev": true,
"license": "Apache-2.0"
},
+ "node_modules/tslib": {
+ "version": "2.8.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
+ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
+ "dev": true,
+ "license": "0BSD"
+ },
"node_modules/type-check": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
@@ -4239,6 +4564,13 @@
"typescript": ">=4.8.4 <5.8.0"
}
},
+ "node_modules/undici-types": {
+ "version": "6.20.0",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz",
+ "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/update-browserslist-db": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.2.tgz",
@@ -4529,8 +4861,16 @@
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/zimmerframe/-/zimmerframe-1.1.2.tgz",
"integrity": "sha512-rAbqEGa8ovJy4pyBxZM70hg4pE6gDgaQ0Sl9M3enG3I0d6H4XSAM3GeNGLKnsBpuijUow064sf7ww1nutC5/3w==",
- "dev": true,
"license": "MIT"
+ },
+ "node_modules/zod": {
+ "version": "3.24.1",
+ "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.1.tgz",
+ "integrity": "sha512-muH7gBL9sI1nciMZV67X5fTKKBLtwpZ5VBp1vsOQzj1MhrBZ4wlVCm3gedKZWLp0Oyel8sIGfeiz54Su+OVT+A==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/colinhacks"
+ }
}
}
}
diff --git a/package.json b/package.json
index ffcb062..1b5f48a 100644
--- a/package.json
+++ b/package.json
@@ -21,7 +21,11 @@
"@sveltejs/vite-plugin-svelte": "^5.0.0",
"@tailwindcss/forms": "^0.5.10",
"@tailwindcss/typography": "^0.5.16",
+ "@types/bcryptjs": "^2.4.6",
+ "@types/node": "^22.10.10",
"autoprefixer": "^10.4.20",
+ "bits-ui": "^0.22.0",
+ "clsx": "^2.1.1",
"eslint": "^9.18.0",
"eslint-config-prettier": "^10.0.1",
"eslint-plugin-svelte": "^2.46.1",
@@ -30,9 +34,18 @@
"prettier-plugin-svelte": "^3.3.3",
"svelte": "^5.0.0",
"svelte-check": "^4.0.0",
+ "tailwind-merge": "^2.6.0",
+ "tailwind-variants": "^0.3.1",
"tailwindcss": "^3.4.17",
"typescript": "^5.0.0",
"typescript-eslint": "^8.20.0",
"vite": "^6.0.0"
+ },
+ "dependencies": {
+ "@prisma/client": "^6.2.1",
+ "bcryptjs": "^2.4.3",
+ "mode-watcher": "^0.5.1",
+ "prisma": "^6.2.1",
+ "zod": "^3.24.1"
}
}
diff --git a/prisma/migrations/20250124202505_initial/migration.sql b/prisma/migrations/20250124202505_initial/migration.sql
new file mode 100644
index 0000000..b776811
--- /dev/null
+++ b/prisma/migrations/20250124202505_initial/migration.sql
@@ -0,0 +1,57 @@
+-- CreateTable
+CREATE TABLE "User" (
+ "id" SERIAL NOT NULL,
+ "email" TEXT NOT NULL,
+ "passwordHash" TEXT NOT NULL,
+ "firstName" TEXT,
+ "lastName" TEXT,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+
+ CONSTRAINT "User_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "Role" (
+ "id" SERIAL NOT NULL,
+ "name" TEXT NOT NULL,
+
+ CONSTRAINT "Role_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "Session" (
+ "id" TEXT NOT NULL,
+ "userId" INTEGER NOT NULL,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL
+);
+
+-- CreateTable
+CREATE TABLE "_UserRoles" (
+ "A" INTEGER NOT NULL,
+ "B" INTEGER NOT NULL,
+
+ CONSTRAINT "_UserRoles_AB_pkey" PRIMARY KEY ("A","B")
+);
+
+-- CreateIndex
+CREATE UNIQUE INDEX "User_email_key" ON "User"("email");
+
+-- CreateIndex
+CREATE UNIQUE INDEX "Role_name_key" ON "Role"("name");
+
+-- CreateIndex
+CREATE UNIQUE INDEX "Session_id_key" ON "Session"("id");
+
+-- CreateIndex
+CREATE INDEX "_UserRoles_B_index" ON "_UserRoles"("B");
+
+-- AddForeignKey
+ALTER TABLE "Session" ADD CONSTRAINT "Session_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "_UserRoles" ADD CONSTRAINT "_UserRoles_A_fkey" FOREIGN KEY ("A") REFERENCES "Role"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "_UserRoles" ADD CONSTRAINT "_UserRoles_B_fkey" FOREIGN KEY ("B") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
diff --git a/prisma/migrations/20250124204403_add_application/migration.sql b/prisma/migrations/20250124204403_add_application/migration.sql
new file mode 100644
index 0000000..a3f82b9
--- /dev/null
+++ b/prisma/migrations/20250124204403_add_application/migration.sql
@@ -0,0 +1,8 @@
+-- CreateTable
+CREATE TABLE "Application" (
+ "id" SERIAL NOT NULL,
+ "name" TEXT NOT NULL,
+ "domain" TEXT NOT NULL,
+
+ CONSTRAINT "Application_pkey" PRIMARY KEY ("id")
+);
diff --git a/prisma/migrations/20250124204741_app/migration.sql b/prisma/migrations/20250124204741_app/migration.sql
new file mode 100644
index 0000000..5e6f54d
--- /dev/null
+++ b/prisma/migrations/20250124204741_app/migration.sql
@@ -0,0 +1,17 @@
+/*
+ Warnings:
+
+ - You are about to drop the `Application` table. If the table is not empty, all the data it contains will be lost.
+
+*/
+-- DropTable
+DROP TABLE "Application";
+
+-- CreateTable
+CREATE TABLE "App" (
+ "id" SERIAL NOT NULL,
+ "name" TEXT NOT NULL,
+ "domain" TEXT NOT NULL,
+
+ CONSTRAINT "App_pkey" PRIMARY KEY ("id")
+);
diff --git a/prisma/migrations/20250124204816_fix_id/migration.sql b/prisma/migrations/20250124204816_fix_id/migration.sql
new file mode 100644
index 0000000..957fd85
--- /dev/null
+++ b/prisma/migrations/20250124204816_fix_id/migration.sql
@@ -0,0 +1,11 @@
+/*
+ Warnings:
+
+ - The primary key for the `App` table will be changed. If it partially fails, the table could be left without primary key constraint.
+ - You are about to drop the column `id` on the `App` table. All the data in the column will be lost.
+
+*/
+-- AlterTable
+ALTER TABLE "App" DROP CONSTRAINT "App_pkey",
+DROP COLUMN "id",
+ADD CONSTRAINT "App_pkey" PRIMARY KEY ("domain");
diff --git a/prisma/migrations/migration_lock.toml b/prisma/migrations/migration_lock.toml
new file mode 100644
index 0000000..648c57f
--- /dev/null
+++ b/prisma/migrations/migration_lock.toml
@@ -0,0 +1,3 @@
+# Please do not edit this file manually
+# It should be added in your version-control system (e.g., Git)
+provider = "postgresql"
\ No newline at end of file
diff --git a/prisma/schema.prisma b/prisma/schema.prisma
new file mode 100644
index 0000000..68c4438
--- /dev/null
+++ b/prisma/schema.prisma
@@ -0,0 +1,45 @@
+// This is your Prisma schema file,
+// learn more about it in the docs: https://pris.ly/d/prisma-schema
+
+// Looking for ways to speed up your queries, or scale easily with your serverless or edge functions?
+// Try Prisma Accelerate: https://pris.ly/cli/accelerate-init
+
+generator client {
+ provider = "prisma-client-js"
+}
+
+datasource db {
+ provider = "postgresql"
+ url = env("DATABASE_URL")
+}
+
+model User {
+ id Int @id @default(autoincrement())
+ email String @unique
+ passwordHash String
+ firstName String?
+ lastName String?
+ roles Role[] @relation("UserRoles")
+ sessions Session[]
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+}
+
+model Role {
+ id Int @id @default(autoincrement())
+ name String @unique
+ users User[] @relation("UserRoles")
+}
+
+model Session {
+ id String @unique
+ userId Int
+ user User @relation(fields: [userId], references: [id])
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+}
+
+model App {
+ name String
+ domain String @id
+}
diff --git a/src/app.css b/src/app.css
index a31e444..8524b77 100644
--- a/src/app.css
+++ b/src/app.css
@@ -1,3 +1,78 @@
-@import 'tailwindcss/base';
-@import 'tailwindcss/components';
-@import 'tailwindcss/utilities';
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
+
+@layer base {
+ :root {
+ --background: 0 0% 100%;
+ --foreground: 0 0% 3.9%;
+
+ --muted: 0 0% 96.1%;
+ --muted-foreground: 0 0% 45.1%;
+
+ --popover: 0 0% 100%;
+ --popover-foreground: 0 0% 3.9%;
+
+ --card: 0 0% 100%;
+ --card-foreground: 0 0% 3.9%;
+
+ --border: 0 0% 89.8%;
+ --input: 0 0% 89.8%;
+
+ --primary: 0 0% 9%;
+ --primary-foreground: 0 0% 98%;
+
+ --secondary: 0 0% 96.1%;
+ --secondary-foreground: 0 0% 9%;
+
+ --accent: 0 0% 96.1%;
+ --accent-foreground: 0 0% 9%;
+
+ --destructive: 0 72.2% 50.6%;
+ --destructive-foreground: 0 0% 98%;
+
+ --ring: 0 0% 3.9%;
+
+ --radius: 0.5rem;
+ }
+
+ .dark {
+ --background: 0 0% 3.9%;
+ --foreground: 0 0% 98%;
+
+ --muted: 0 0% 14.9%;
+ --muted-foreground: 0 0% 63.9%;
+
+ --popover: 0 0% 3.9%;
+ --popover-foreground: 0 0% 98%;
+
+ --card: 0 0% 3.9%;
+ --card-foreground: 0 0% 98%;
+
+ --border: 0 0% 14.9%;
+ --input: 0 0% 14.9%;
+
+ --primary: 0 0% 98%;
+ --primary-foreground: 0 0% 9%;
+
+ --secondary: 0 0% 14.9%;
+ --secondary-foreground: 0 0% 98%;
+
+ --accent: 0 0% 14.9%;
+ --accent-foreground: 0 0% 98%;
+
+ --destructive: 0 62.8% 30.6%;
+ --destructive-foreground: 0 0% 98%;
+
+ --ring: 0 0% 83.1%;
+ }
+}
+
+@layer base {
+ * {
+ @apply border-border;
+ }
+ body {
+ @apply bg-background text-foreground;
+ }
+}
\ No newline at end of file
diff --git a/src/lib/components/ui/button/button.svelte b/src/lib/components/ui/button/button.svelte
new file mode 100644
index 0000000..86827f3
--- /dev/null
+++ b/src/lib/components/ui/button/button.svelte
@@ -0,0 +1,25 @@
+
+
+
+
+
diff --git a/src/lib/components/ui/button/index.ts b/src/lib/components/ui/button/index.ts
new file mode 100644
index 0000000..af1e188
--- /dev/null
+++ b/src/lib/components/ui/button/index.ts
@@ -0,0 +1,49 @@
+import { type VariantProps, tv } from "tailwind-variants";
+import type { Button as ButtonPrimitive } from "bits-ui";
+import Root from "./button.svelte";
+
+const buttonVariants = tv({
+ base: "ring-offset-background focus-visible:ring-ring inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
+ variants: {
+ variant: {
+ default: "bg-primary text-primary-foreground hover:bg-primary/90",
+ destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90",
+ outline:
+ "border-input bg-background hover:bg-accent hover:text-accent-foreground border",
+ secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
+ ghost: "hover:bg-accent hover:text-accent-foreground",
+ link: "text-primary underline-offset-4 hover:underline",
+ },
+ size: {
+ default: "h-10 px-4 py-2",
+ sm: "h-9 rounded-md px-3",
+ lg: "h-11 rounded-md px-8",
+ icon: "h-10 w-10",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ size: "default",
+ },
+});
+
+type Variant = VariantProps["variant"];
+type Size = VariantProps["size"];
+
+type Props = ButtonPrimitive.Props & {
+ variant?: Variant;
+ size?: Size;
+};
+
+type Events = ButtonPrimitive.Events;
+
+export {
+ Root,
+ type Props,
+ type Events,
+ //
+ Root as Button,
+ type Props as ButtonProps,
+ type Events as ButtonEvents,
+ buttonVariants,
+};
diff --git a/src/lib/components/ui/card/card-content.svelte b/src/lib/components/ui/card/card-content.svelte
new file mode 100644
index 0000000..748c644
--- /dev/null
+++ b/src/lib/components/ui/card/card-content.svelte
@@ -0,0 +1,13 @@
+
+
+
+
+
diff --git a/src/lib/components/ui/card/card-description.svelte b/src/lib/components/ui/card/card-description.svelte
new file mode 100644
index 0000000..f65821d
--- /dev/null
+++ b/src/lib/components/ui/card/card-description.svelte
@@ -0,0 +1,13 @@
+
+
+
+
+
diff --git a/src/lib/components/ui/card/card-footer.svelte b/src/lib/components/ui/card/card-footer.svelte
new file mode 100644
index 0000000..32f90bb
--- /dev/null
+++ b/src/lib/components/ui/card/card-footer.svelte
@@ -0,0 +1,13 @@
+
+
+
+
+
diff --git a/src/lib/components/ui/card/card-header.svelte b/src/lib/components/ui/card/card-header.svelte
new file mode 100644
index 0000000..9c65185
--- /dev/null
+++ b/src/lib/components/ui/card/card-header.svelte
@@ -0,0 +1,13 @@
+
+
+
+
+
diff --git a/src/lib/components/ui/card/card-title.svelte b/src/lib/components/ui/card/card-title.svelte
new file mode 100644
index 0000000..719808e
--- /dev/null
+++ b/src/lib/components/ui/card/card-title.svelte
@@ -0,0 +1,21 @@
+
+
+
+
+
diff --git a/src/lib/components/ui/card/card.svelte b/src/lib/components/ui/card/card.svelte
new file mode 100644
index 0000000..b69c15c
--- /dev/null
+++ b/src/lib/components/ui/card/card.svelte
@@ -0,0 +1,16 @@
+
+
+
+
+
diff --git a/src/lib/components/ui/card/index.ts b/src/lib/components/ui/card/index.ts
new file mode 100644
index 0000000..bcc031d
--- /dev/null
+++ b/src/lib/components/ui/card/index.ts
@@ -0,0 +1,24 @@
+import Root from "./card.svelte";
+import Content from "./card-content.svelte";
+import Description from "./card-description.svelte";
+import Footer from "./card-footer.svelte";
+import Header from "./card-header.svelte";
+import Title from "./card-title.svelte";
+
+export {
+ Root,
+ Content,
+ Description,
+ Footer,
+ Header,
+ Title,
+ //
+ Root as Card,
+ Content as CardContent,
+ Description as CardDescription,
+ Footer as CardFooter,
+ Header as CardHeader,
+ Title as CardTitle,
+};
+
+export type HeadingLevel = "h1" | "h2" | "h3" | "h4" | "h5" | "h6";
diff --git a/src/lib/components/ui/input/index.ts b/src/lib/components/ui/input/index.ts
new file mode 100644
index 0000000..75e3bc2
--- /dev/null
+++ b/src/lib/components/ui/input/index.ts
@@ -0,0 +1,29 @@
+import Root from "./input.svelte";
+
+export type FormInputEvent = T & {
+ currentTarget: EventTarget & HTMLInputElement;
+};
+export type InputEvents = {
+ blur: FormInputEvent;
+ change: FormInputEvent;
+ click: FormInputEvent;
+ focus: FormInputEvent;
+ focusin: FormInputEvent;
+ focusout: FormInputEvent;
+ keydown: FormInputEvent;
+ keypress: FormInputEvent;
+ keyup: FormInputEvent;
+ mouseover: FormInputEvent;
+ mouseenter: FormInputEvent;
+ mouseleave: FormInputEvent;
+ mousemove: FormInputEvent;
+ paste: FormInputEvent;
+ input: FormInputEvent;
+ wheel: FormInputEvent;
+};
+
+export {
+ Root,
+ //
+ Root as Input,
+};
diff --git a/src/lib/components/ui/input/input.svelte b/src/lib/components/ui/input/input.svelte
new file mode 100644
index 0000000..cab1457
--- /dev/null
+++ b/src/lib/components/ui/input/input.svelte
@@ -0,0 +1,42 @@
+
+
+
diff --git a/src/lib/components/ui/label/index.ts b/src/lib/components/ui/label/index.ts
new file mode 100644
index 0000000..8bfca0b
--- /dev/null
+++ b/src/lib/components/ui/label/index.ts
@@ -0,0 +1,7 @@
+import Root from "./label.svelte";
+
+export {
+ Root,
+ //
+ Root as Label,
+};
diff --git a/src/lib/components/ui/label/label.svelte b/src/lib/components/ui/label/label.svelte
new file mode 100644
index 0000000..2a7d479
--- /dev/null
+++ b/src/lib/components/ui/label/label.svelte
@@ -0,0 +1,21 @@
+
+
+
+
+
diff --git a/src/lib/prisma.ts b/src/lib/prisma.ts
new file mode 100644
index 0000000..9b6c4ce
--- /dev/null
+++ b/src/lib/prisma.ts
@@ -0,0 +1,3 @@
+import { PrismaClient } from '@prisma/client';
+
+export const prisma = new PrismaClient();
diff --git a/src/lib/utils.ts b/src/lib/utils.ts
new file mode 100644
index 0000000..8871245
--- /dev/null
+++ b/src/lib/utils.ts
@@ -0,0 +1,62 @@
+import { type ClassValue, clsx } from "clsx";
+import { twMerge } from "tailwind-merge";
+import { cubicOut } from "svelte/easing";
+import type { TransitionConfig } from "svelte/transition";
+
+export function cn(...inputs: ClassValue[]) {
+ return twMerge(clsx(inputs));
+}
+
+type FlyAndScaleParams = {
+ y?: number;
+ x?: number;
+ start?: number;
+ duration?: number;
+};
+
+export const flyAndScale = (
+ node: Element,
+ params: FlyAndScaleParams = { y: -8, x: 0, start: 0.95, duration: 150 }
+): TransitionConfig => {
+ const style = getComputedStyle(node);
+ const transform = style.transform === "none" ? "" : style.transform;
+
+ const scaleConversion = (
+ valueA: number,
+ scaleA: [number, number],
+ scaleB: [number, number]
+ ) => {
+ const [minA, maxA] = scaleA;
+ const [minB, maxB] = scaleB;
+
+ const percentage = (valueA - minA) / (maxA - minA);
+ const valueB = percentage * (maxB - minB) + minB;
+
+ return valueB;
+ };
+
+ const styleToString = (
+ style: Record
+ ): string => {
+ return Object.keys(style).reduce((str, key) => {
+ if (style[key] === undefined) return str;
+ return str + `${key}:${style[key]};`;
+ }, "");
+ };
+
+ return {
+ duration: params.duration ?? 200,
+ delay: 0,
+ css: (t) => {
+ const y = scaleConversion(t, [0, 1], [params.y ?? 5, 0]);
+ const x = scaleConversion(t, [0, 1], [params.x ?? 0, 0]);
+ const scale = scaleConversion(t, [0, 1], [params.start ?? 0.95, 1]);
+
+ return styleToString({
+ transform: `${transform} translate3d(${x}px, ${y}px, 0) scale(${scale})`,
+ opacity: t
+ });
+ },
+ easing: cubicOut
+ };
+};
\ No newline at end of file
diff --git a/src/routes/+error.svelte b/src/routes/+error.svelte
new file mode 100644
index 0000000..b60a51b
--- /dev/null
+++ b/src/routes/+error.svelte
@@ -0,0 +1,10 @@
+
+
+
+ {#if $page.error}
+
{$page.status}:
+
{$page.error.message}
+ {/if}
+
diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte
index 9b776b7..1931ce5 100644
--- a/src/routes/+layout.svelte
+++ b/src/routes/+layout.svelte
@@ -1,6 +1,11 @@
-{@render children()}
+
+
+
+ {@render children()}
+
diff --git a/src/routes/api/login/+server.ts b/src/routes/api/login/+server.ts
new file mode 100644
index 0000000..ac7068f
--- /dev/null
+++ b/src/routes/api/login/+server.ts
@@ -0,0 +1,77 @@
+// src/routes/api/login/+server.ts
+
+import { prisma } from '$lib/prisma';
+import bcrypt from 'bcryptjs';
+import { randomBytes } from 'crypto'; // For generating a random session ID
+import { z } from 'zod';
+import { json, type RequestEvent } from '@sveltejs/kit';
+import { serialize } from 'cookie'; // For cookie serialization
+
+// Define the schema for validation using Zod
+const loginSchema = z.object({
+ email: z.string().email().min(5, 'Email is required'),
+ password: z.string().min(8, 'Password must be at least 8 characters')
+});
+
+export const POST = async ({ request }: RequestEvent) => {
+ try {
+ // Parse the request body
+ const formData = await request.formData();
+ const email = formData.get('email') as string;
+ const password = formData.get('password') as string;
+
+ // Validate the incoming data
+ const parsed = loginSchema.safeParse({ email, password });
+
+ if (!parsed.success) {
+ return json({ message: parsed.error.errors[0].message }, { status: 400 });
+ }
+
+ // Find the user in the database by email
+ const user = await prisma.user.findUnique({
+ where: { email }
+ });
+
+ if (!user) {
+ return json({ message: 'Invalid email or password' }, { status: 400 });
+ }
+
+ // Compare the provided password with the hashed password in the database
+ const isPasswordValid = await bcrypt.compare(password, user.passwordHash);
+
+ if (!isPasswordValid) {
+ return json({ message: 'Invalid email or password' }, { status: 400 });
+ }
+
+ // Generate a secure random session ID using crypto
+ const sessionId = randomBytes(16).toString('hex'); // 64-byte hex session ID
+
+ // Create a new session in the database
+ await prisma.session.create({
+ data: {
+ id: sessionId, // Session ID is generated randomly
+ userId: user.id // Associate session with user
+ }
+ });
+
+ // Set the session ID in an HttpOnly cookie
+ const cookie = serialize('session_id', sessionId, {
+ httpOnly: true, // Ensures the cookie is not accessible via JavaScript
+ secure: process.env.NODE_ENV === 'production', // Set secure flag in production
+ maxAge: 60 * 60, // 1 hour expiry
+ path: '/' // Make the cookie available throughout the site
+ });
+
+ // Respond with the session token
+ return json(
+ { message: 'Login successful' },
+ {
+ status: 200,
+ headers: { 'Set-Cookie': cookie } // Set cookie header
+ }
+ );
+ } catch (error) {
+ console.error(error);
+ return json({ message: 'Internal server error' }, { status: 500 });
+ }
+};
diff --git a/src/routes/api/me/+server.ts b/src/routes/api/me/+server.ts
new file mode 100644
index 0000000..5c594b7
--- /dev/null
+++ b/src/routes/api/me/+server.ts
@@ -0,0 +1,42 @@
+import { json } from '@sveltejs/kit';
+import type { RequestEvent } from '@sveltejs/kit';
+import { PrismaClient } from '@prisma/client';
+
+const prisma = new PrismaClient();
+
+export const GET = async ({ cookies }: RequestEvent) => {
+ // Retrieve the session_id cookie
+ const sessionId = cookies.get('session_id');
+
+ // If there's no session_id cookie, return an empty object
+ if (!sessionId) {
+ return json({ message: 'No session cookie!' }, { status: 500 });
+ }
+
+ try {
+ // Find the session and include the associated user
+ const session = await prisma.session.findUnique({
+ where: { id: sessionId },
+ include: { user: { include: { roles: true } } }
+ });
+
+ // If no session is found, return an empty object
+ if (!session) {
+ return json({ message: 'Session doesnt exist!' }, { status: 500 });
+ }
+
+ // Sanitize the user data by removing the passwordHash and sanitizing the session
+ const sanitizedUser = {
+ ...session.user,
+ passwordHash: undefined, // Remove the password hash
+ sessions: undefined // Remove sessions if included in the model
+ };
+
+ // Return the sanitized user data
+ return json(sanitizedUser);
+ } catch (error) {
+ // Log the error and return an empty object
+ console.error('Error fetching session:', error);
+ return json({ message: 'Internal server error' }, { status: 500 });
+ }
+};
diff --git a/src/routes/api/register/+server.ts b/src/routes/api/register/+server.ts
new file mode 100644
index 0000000..6681b6d
--- /dev/null
+++ b/src/routes/api/register/+server.ts
@@ -0,0 +1,74 @@
+import { prisma } from '$lib/prisma'; // Your Prisma client instance
+import bcrypt from 'bcryptjs';
+import { z } from 'zod';
+import { type RequestHandler } from '@sveltejs/kit';
+
+// Define the schema for validation using Zod
+const registerSchema = z.object({
+ email: z.string().email().min(5, 'Email is required'),
+ password: z.string().min(8, 'Password must be at least 8 characters'),
+ firstName: z.string().optional(),
+ lastName: z.string().optional()
+});
+
+// Define the POST handler with explicit typing for the request
+export const POST: RequestHandler = async ({ request }) => {
+ try {
+ // Parse form data
+ const formData = await request.formData();
+ const email = formData.get('email') as string;
+ const password = formData.get('password') as string;
+
+ // Validate the incoming data
+ const parsed = registerSchema.safeParse({
+ email,
+ password,
+ firstName: formData.get('firstName') as string | null,
+ lastName: formData.get('lastName') as string | null
+ });
+
+ if (!parsed.success) {
+ return new Response(JSON.stringify({ message: parsed.error.errors[0].message }), {
+ status: 400,
+ headers: { 'Content-Type': 'application/json' }
+ });
+ }
+
+ // Check if the email already exists
+ const existingUser = await prisma.user.findUnique({
+ where: { email }
+ });
+
+ if (existingUser) {
+ return new Response(JSON.stringify({ message: 'Email already registered' }), {
+ status: 400,
+ headers: { 'Content-Type': 'application/json' }
+ });
+ }
+
+ // Hash the password before storing it
+ const hashedPassword = await bcrypt.hash(password, 10);
+
+ // Create the new user in the database
+ await prisma.user.create({
+ data: {
+ email,
+ passwordHash: hashedPassword,
+ firstName: parsed.data.firstName || undefined, // Store firstName if provided
+ lastName: parsed.data.lastName || undefined // Store lastName if provided
+ }
+ });
+
+ // Redirect to the login page
+ return new Response(JSON.stringify({ message: 'Successfully created an account' }), {
+ status: 200,
+ headers: { 'Content-Type': 'application/json' }
+ });
+ } catch (error) {
+ // Handle unexpected errors
+ return new Response(JSON.stringify({ message: 'Internal server error', error: error }), {
+ status: 500,
+ headers: { 'Content-Type': 'application/json' }
+ });
+ }
+};
diff --git a/src/routes/login/+page.server.ts b/src/routes/login/+page.server.ts
new file mode 100644
index 0000000..38493ce
--- /dev/null
+++ b/src/routes/login/+page.server.ts
@@ -0,0 +1,56 @@
+import { prisma } from '$lib/prisma';
+import { parse } from 'cookie';
+import { redirect, error } from '@sveltejs/kit';
+import type { ServerLoad } from '@sveltejs/kit';
+
+export const load: ServerLoad = async ({ request, url }) => {
+ const cookies = parse(request.headers.get('cookie') || '');
+ const sessionId = cookies['session_id'];
+ const redirectURL = url.searchParams.get('redirect');
+
+ if (!redirectURL) throw error(400, 'Invalid redirect domain');
+
+ if (sessionId) {
+ // Validate the session by checking if the session ID exists in the database
+ const session = await prisma.session.findUnique({
+ where: { id: sessionId },
+ include: { user: true }
+ });
+
+ if (session) {
+ // Session is valid, check if there's a redirectURL query parameter
+
+ if (redirectURL) {
+ let redirectWithToken;
+ try {
+ redirectWithToken = new URL(redirectURL);
+
+ // Extract the domain from the redirect URL
+ const redirectDomain = redirectWithToken.hostname;
+ // Check if an application exists with the provided domain
+ const app = await prisma.app.findFirst({
+ where: { domain: redirectDomain }
+ });
+
+ if (!app) {
+ // If no application with the domain exists, throw an error
+ throw error(400, 'Invalid redirect domain');
+ }
+
+ // Append the session ID or token to the redirect URL
+ redirectWithToken.searchParams.append('token', session.id);
+
+ // Perform the redirect with the session token attached
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ } catch (err) {
+ // Handle malformed URL or other errors
+ throw error(400, 'Invalid redirect domain!');
+ }
+ redirect(302, redirectWithToken.toString());
+ }
+ }
+ }
+
+ // If no session exists, allow the user to stay on the login page or show the login form
+ return {};
+};
diff --git a/src/routes/login/+page.svelte b/src/routes/login/+page.svelte
new file mode 100644
index 0000000..027b0ab
--- /dev/null
+++ b/src/routes/login/+page.svelte
@@ -0,0 +1,72 @@
+
+
+
+
+
diff --git a/src/routes/logout/+server.ts b/src/routes/logout/+server.ts
new file mode 100644
index 0000000..b7b23c0
--- /dev/null
+++ b/src/routes/logout/+server.ts
@@ -0,0 +1,13 @@
+import { redirect } from '@sveltejs/kit';
+import type { RequestEvent } from '@sveltejs/kit';
+
+export const GET = async ({ cookies, url }: RequestEvent) => {
+ // Delete the cookie
+ cookies.delete('session_id', { path: '/' });
+
+ // Get the redirect URL from the query parameter
+ const redirectUrl = url.searchParams.get('redirect') || 'https://rummage.cc';
+
+ // Redirect to the specified URL
+ throw redirect(302, redirectUrl);
+};
diff --git a/src/routes/register/+page.svelte b/src/routes/register/+page.svelte
new file mode 100644
index 0000000..36c73ad
--- /dev/null
+++ b/src/routes/register/+page.svelte
@@ -0,0 +1,84 @@
+
+
+
+
+
diff --git a/tailwind.config.ts b/tailwind.config.ts
index ae1273d..c4bf235 100644
--- a/tailwind.config.ts
+++ b/tailwind.config.ts
@@ -1,13 +1,64 @@
-import forms from '@tailwindcss/forms';
-import typography from '@tailwindcss/typography';
-import type { Config } from 'tailwindcss';
-
-export default {
- content: ['./src/**/*.{html,js,svelte,ts}'],
+import { fontFamily } from "tailwindcss/defaultTheme";
+import type { Config } from "tailwindcss";
+const config: Config = {
+ darkMode: ["class"],
+ content: ["./src/**/*.{html,js,svelte,ts}"],
+ safelist: ["dark"],
theme: {
- extend: {}
+ container: {
+ center: true,
+ padding: "2rem",
+ screens: {
+ "2xl": "1400px"
+ }
+ },
+ extend: {
+ colors: {
+ border: "hsl(var(--border) / )",
+ input: "hsl(var(--input) / )",
+ ring: "hsl(var(--ring) / )",
+ background: "hsl(var(--background) / )",
+ foreground: "hsl(var(--foreground) / )",
+ primary: {
+ DEFAULT: "hsl(var(--primary) / )",
+ foreground: "hsl(var(--primary-foreground) / )"
+ },
+ secondary: {
+ DEFAULT: "hsl(var(--secondary) / )",
+ foreground: "hsl(var(--secondary-foreground) / )"
+ },
+ destructive: {
+ DEFAULT: "hsl(var(--destructive) / )",
+ foreground: "hsl(var(--destructive-foreground) / )"
+ },
+ muted: {
+ DEFAULT: "hsl(var(--muted) / )",
+ foreground: "hsl(var(--muted-foreground) / )"
+ },
+ accent: {
+ DEFAULT: "hsl(var(--accent) / )",
+ foreground: "hsl(var(--accent-foreground) / )"
+ },
+ popover: {
+ DEFAULT: "hsl(var(--popover) / )",
+ foreground: "hsl(var(--popover-foreground) / )"
+ },
+ card: {
+ DEFAULT: "hsl(var(--card) / )",
+ foreground: "hsl(var(--card-foreground) / )"
+ }
+ },
+ borderRadius: {
+ lg: "var(--radius)",
+ md: "calc(var(--radius) - 2px)",
+ sm: "calc(var(--radius) - 4px)"
+ },
+ fontFamily: {
+ sans: [...fontFamily.sans]
+ }
+ }
},
+};
- plugins: [typography, forms]
-} satisfies Config;
+export default config;