From 567aa4cb7154200c97a1f4effc1581f21107d15f Mon Sep 17 00:00:00 2001 From: Owen Rummage Date: Fri, 24 Jan 2025 17:18:04 -0600 Subject: [PATCH] oops i forgot to commit as I go again --- components.json | 14 + package-lock.json | 388 ++++++++++++++++-- package.json | 13 + .../20250124202505_initial/migration.sql | 57 +++ .../migration.sql | 8 + .../20250124204741_app/migration.sql | 17 + .../20250124204816_fix_id/migration.sql | 11 + prisma/migrations/migration_lock.toml | 3 + prisma/schema.prisma | 45 ++ src/app.css | 81 +++- src/lib/components/ui/button/button.svelte | 25 ++ src/lib/components/ui/button/index.ts | 49 +++ .../components/ui/card/card-content.svelte | 13 + .../ui/card/card-description.svelte | 13 + src/lib/components/ui/card/card-footer.svelte | 13 + src/lib/components/ui/card/card-header.svelte | 13 + src/lib/components/ui/card/card-title.svelte | 21 + src/lib/components/ui/card/card.svelte | 16 + src/lib/components/ui/card/index.ts | 24 ++ src/lib/components/ui/input/index.ts | 29 ++ src/lib/components/ui/input/input.svelte | 42 ++ src/lib/components/ui/label/index.ts | 7 + src/lib/components/ui/label/label.svelte | 21 + src/lib/prisma.ts | 3 + src/lib/utils.ts | 62 +++ src/routes/+error.svelte | 10 + src/routes/+layout.svelte | 7 +- src/routes/api/login/+server.ts | 77 ++++ src/routes/api/me/+server.ts | 42 ++ src/routes/api/register/+server.ts | 74 ++++ src/routes/login/+page.server.ts | 56 +++ src/routes/login/+page.svelte | 72 ++++ src/routes/logout/+server.ts | 13 + src/routes/register/+page.svelte | 84 ++++ tailwind.config.ts | 69 +++- 35 files changed, 1455 insertions(+), 37 deletions(-) create mode 100644 components.json create mode 100644 prisma/migrations/20250124202505_initial/migration.sql create mode 100644 prisma/migrations/20250124204403_add_application/migration.sql create mode 100644 prisma/migrations/20250124204741_app/migration.sql create mode 100644 prisma/migrations/20250124204816_fix_id/migration.sql create mode 100644 prisma/migrations/migration_lock.toml create mode 100644 prisma/schema.prisma create mode 100644 src/lib/components/ui/button/button.svelte create mode 100644 src/lib/components/ui/button/index.ts create mode 100644 src/lib/components/ui/card/card-content.svelte create mode 100644 src/lib/components/ui/card/card-description.svelte create mode 100644 src/lib/components/ui/card/card-footer.svelte create mode 100644 src/lib/components/ui/card/card-header.svelte create mode 100644 src/lib/components/ui/card/card-title.svelte create mode 100644 src/lib/components/ui/card/card.svelte create mode 100644 src/lib/components/ui/card/index.ts create mode 100644 src/lib/components/ui/input/index.ts create mode 100644 src/lib/components/ui/input/input.svelte create mode 100644 src/lib/components/ui/label/index.ts create mode 100644 src/lib/components/ui/label/label.svelte create mode 100644 src/lib/prisma.ts create mode 100644 src/lib/utils.ts create mode 100644 src/routes/+error.svelte create mode 100644 src/routes/api/login/+server.ts create mode 100644 src/routes/api/me/+server.ts create mode 100644 src/routes/api/register/+server.ts create mode 100644 src/routes/login/+page.server.ts create mode 100644 src/routes/login/+page.svelte create mode 100644 src/routes/logout/+server.ts create mode 100644 src/routes/register/+page.svelte 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 @@ + + + +
+ + Login to Account + Please enter your credentials below to login. + You will be redirected to . + + + + {#if successMessage} +

{successMessage}

+ {:else if errorMessage} +

{errorMessage}

+ {/if} + + + + + + + +
+ + + + +
+
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 @@ + + + +
+ + Register Account + Please fill in the details below to create an account. + + + {#if successMessage} +

{successMessage}

+ {:else if errorMessage} +

{errorMessage}

+ {/if} + + + + + + + + + + + +
+ + + + +
+
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;