Compare commits

...

25 Commits
main ... dev

Author SHA1 Message Date
Patrick d9502cb13d start of implementation of tasklist with an overlay 2025-10-30 16:08:36 +01:00
Patrick caee9f6fe9 upgraded dependencies 2025-10-30 16:05:31 +01:00
Patrick a6f22ea3c9 updated schema for task to have topic and description 2025-10-25 09:52:36 +02:00
Patrick 2911f05dcd started impl of task ui 2025-10-22 13:54:40 +02:00
Patrick b0b6557464 added basic task endpoint 2025-10-19 17:12:04 +02:00
Patrick d7dcbb3560 Added login bypass 2025-10-16 15:07:04 +02:00
Patrick 16b5aba457 implemented automated user session login and session refresh 2025-10-04 13:05:03 +02:00
Patrick 976cd3edb9 added user to locals 2025-10-02 11:43:35 +02:00
Patrick 9c3f103758 added global css 2025-10-02 11:41:56 +02:00
Patrick c4ee8fc372 moved lifetime to config 2025-09-30 11:09:21 +02:00
Patrick 6566bb4403 added login page 2025-09-30 10:59:41 +02:00
Patrick db44350bc9 implemented login 2025-09-30 10:59:27 +02:00
Patrick 2feb97ddfe Added error messages 2025-09-20 17:28:28 +02:00
Patrick 8de9738d85 Added error handling to registration 2025-09-20 17:27:50 +02:00
Patrick 1d6ca4abd8 implemented basic user registration 2025-09-18 16:55:10 +02:00
Patrick 8c03efbfc0 implemented email format check 2025-09-18 16:52:33 +02:00
Patrick 47c29ad10e changed favicon to svg 2025-09-18 11:55:41 +02:00
Patrick f5362f52e7 Added register page 2025-09-18 11:54:18 +02:00
Patrick 78e9f268b6 moved generated prisma client to node_modules 2025-09-17 14:54:12 +02:00
Patrick 8eacebc306 Created scaffolding for Logging 2025-09-17 13:46:32 +02:00
Patrick ecff8798fe Created scaffolding for config module with log default 2025-09-17 13:36:43 +02:00
Patrick 8e8c2bbf68 installed bun types 2025-09-17 13:35:46 +02:00
Patrick 0d5024f6d3 Added rudimentary login page 2025-09-16 17:38:19 +02:00
Patrick 3e582537df Cleaned template code 2025-09-16 15:20:32 +02:00
Patrick e0d76a4cb0 Initial database layout 2025-09-16 15:03:50 +02:00
34 changed files with 1869 additions and 54 deletions

View File

@ -3,15 +3,19 @@
"workspaces": {
"": {
"name": "todo",
"dependencies": {
"@prisma/client": "^6.18.0",
"@types/bun": "^1.3.1",
},
"devDependencies": {
"@sveltejs/adapter-auto": "^6.0.0",
"@sveltejs/kit": "^2.22.0",
"@sveltejs/vite-plugin-svelte": "^6.0.0",
"prisma": "^6.16.1",
"svelte": "^5.0.0",
"svelte-check": "^4.0.0",
"typescript": "^5.0.0",
"vite": "^7.0.4",
"@sveltejs/adapter-auto": "^6.1.1",
"@sveltejs/kit": "^2.47.3",
"@sveltejs/vite-plugin-svelte": "^6.2.1",
"prisma": "^6.18.0",
"svelte": "^5.41.4",
"svelte-check": "^4.3.3",
"typescript": "^5.9.3",
"vite": "^7.1.12",
},
},
},
@ -80,17 +84,19 @@
"@polka/url": ["@polka/url@1.0.0-next.29", "", {}, "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww=="],
"@prisma/config": ["@prisma/config@6.16.1", "", { "dependencies": { "c12": "3.1.0", "deepmerge-ts": "7.1.5", "effect": "3.16.12", "empathic": "2.0.0" } }, "sha512-sz3uxRPNL62QrJ0EYiujCFkIGZ3hg+9hgC1Ae1HjoYuj0BxCqHua4JNijYvYCrh9LlofZDZcRBX3tHBfLvAngA=="],
"@prisma/client": ["@prisma/client@6.18.0", "", { "peerDependencies": { "prisma": "*", "typescript": ">=5.1.0" }, "optionalPeers": ["prisma", "typescript"] }, "sha512-jnL2I9gDnPnw4A+4h5SuNn8Gc+1mL1Z79U/3I9eE2gbxJG1oSA+62ByPW4xkeDgwE0fqMzzpAZ7IHxYnLZ4iQA=="],
"@prisma/debug": ["@prisma/debug@6.16.1", "", {}, "sha512-RWv/VisW5vJE4cDRTuAHeVedtGoItXTnhuLHsSlJ9202QKz60uiXWywBlVcqXVq8bFeIZoCoWH+R1duZJPwqLw=="],
"@prisma/config": ["@prisma/config@6.18.0", "", { "dependencies": { "c12": "3.1.0", "deepmerge-ts": "7.1.5", "effect": "3.18.4", "empathic": "2.0.0" } }, "sha512-rgFzspCpwsE+q3OF/xkp0fI2SJ3PfNe9LLMmuSVbAZ4nN66WfBiKqJKo/hLz3ysxiPQZf8h1SMf2ilqPMeWATQ=="],
"@prisma/engines": ["@prisma/engines@6.16.1", "", { "dependencies": { "@prisma/debug": "6.16.1", "@prisma/engines-version": "6.16.0-7.1c57fdcd7e44b29b9313256c76699e91c3ac3c43", "@prisma/fetch-engine": "6.16.1", "@prisma/get-platform": "6.16.1" } }, "sha512-EOnEM5HlosPudBqbI+jipmaW/vQEaF0bKBo4gVkGabasINHR6RpC6h44fKZEqx4GD8CvH+einD2+b49DQrwrAg=="],
"@prisma/debug": ["@prisma/debug@6.18.0", "", {}, "sha512-PMVPMmxPj0ps1VY75DIrT430MoOyQx9hmm174k6cmLZpcI95rAPXOQ+pp8ANQkJtNyLVDxnxVJ0QLbrm/ViBcg=="],
"@prisma/engines-version": ["@prisma/engines-version@6.16.0-7.1c57fdcd7e44b29b9313256c76699e91c3ac3c43", "", {}, "sha512-ThvlDaKIVrnrv97ujNFDYiQbeMQpLa0O86HFA2mNoip4mtFqM7U5GSz2ie1i2xByZtvPztJlNRgPsXGeM/kqAA=="],
"@prisma/engines": ["@prisma/engines@6.18.0", "", { "dependencies": { "@prisma/debug": "6.18.0", "@prisma/engines-version": "6.18.0-8.34b5a692b7bd79939a9a2c3ef97d816e749cda2f", "@prisma/fetch-engine": "6.18.0", "@prisma/get-platform": "6.18.0" } }, "sha512-i5RzjGF/ex6AFgqEe2o1IW8iIxJGYVQJVRau13kHPYEL1Ck8Zvwuzamqed/1iIljs5C7L+Opiz5TzSsUebkriA=="],
"@prisma/fetch-engine": ["@prisma/fetch-engine@6.16.1", "", { "dependencies": { "@prisma/debug": "6.16.1", "@prisma/engines-version": "6.16.0-7.1c57fdcd7e44b29b9313256c76699e91c3ac3c43", "@prisma/get-platform": "6.16.1" } }, "sha512-fl/PKQ8da5YTayw86WD3O9OmKJEM43gD3vANy2hS5S1CnfW2oPXk+Q03+gUWqcKK306QqhjjIHRFuTZ31WaosQ=="],
"@prisma/engines-version": ["@prisma/engines-version@6.18.0-8.34b5a692b7bd79939a9a2c3ef97d816e749cda2f", "", {}, "sha512-T7Af4QsJQnSgWN1zBbX+Cha5t4qjHRxoeoWpK4JugJzG/ipmmDMY5S+O0N1ET6sCBNVkf6lz+Y+ZNO9+wFU8pQ=="],
"@prisma/get-platform": ["@prisma/get-platform@6.16.1", "", { "dependencies": { "@prisma/debug": "6.16.1" } }, "sha512-kUfg4vagBG7dnaGRcGd1c0ytQFcDj2SUABiuveIpL3bthFdTLI6PJeLEia6Q8Dgh+WhPdo0N2q0Fzjk63XTyaA=="],
"@prisma/fetch-engine": ["@prisma/fetch-engine@6.18.0", "", { "dependencies": { "@prisma/debug": "6.18.0", "@prisma/engines-version": "6.18.0-8.34b5a692b7bd79939a9a2c3ef97d816e749cda2f", "@prisma/get-platform": "6.18.0" } }, "sha512-TdaBvTtBwP3IoqVYoGIYpD4mWlk0pJpjTJjir/xLeNWlwog7Sl3bD2J0jJ8+5+q/6RBg+acb9drsv5W6lqae7A=="],
"@prisma/get-platform": ["@prisma/get-platform@6.18.0", "", { "dependencies": { "@prisma/debug": "6.18.0" } }, "sha512-uXNJCJGhxTCXo2B25Ta91Rk1/Nmlqg9p7G9GKh8TPhxvAyXCvMNQoogj4JLEUy+3ku8g59cpyQIKFhqY2xO2bg=="],
"@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.50.1", "", { "os": "android", "cpu": "arm" }, "sha512-HJXwzoZN4eYTdD8bVV22DN8gsPCAj3V20NHKOs8ezfXanGpmVPR7kalUHd+Y31IJp9stdB87VKPFbsGY3H/2ag=="],
@ -138,24 +144,32 @@
"@sveltejs/acorn-typescript": ["@sveltejs/acorn-typescript@1.0.5", "", { "peerDependencies": { "acorn": "^8.9.0" } }, "sha512-IwQk4yfwLdibDlrXVE04jTZYlLnwsTT2PIOQQGNLWfjavGifnk1JD1LcZjZaBTRcxZu2FfPfNLOE04DSu9lqtQ=="],
"@sveltejs/adapter-auto": ["@sveltejs/adapter-auto@6.1.0", "", { "peerDependencies": { "@sveltejs/kit": "^2.0.0" } }, "sha512-shOuLI5D2s+0zTv2ab5M5PqfknXqWbKi+0UwB9yLTRIdzsK1R93JOO8jNhIYSHdW+IYXIYnLniu+JZqXs7h9Wg=="],
"@sveltejs/adapter-auto": ["@sveltejs/adapter-auto@6.1.1", "", { "peerDependencies": { "@sveltejs/kit": "^2.0.0" } }, "sha512-cBNt4jgH4KuaNO5gRSB2CZKkGtz+OCZ8lPjRQGjhvVUD4akotnj2weUia6imLl2v07K3IgsQRyM36909miSwoQ=="],
"@sveltejs/kit": ["@sveltejs/kit@2.39.0", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "@sveltejs/acorn-typescript": "^1.0.5", "@types/cookie": "^0.6.0", "acorn": "^8.14.1", "cookie": "^0.6.0", "devalue": "^5.3.2", "esm-env": "^1.2.2", "kleur": "^4.1.5", "magic-string": "^0.30.5", "mrmime": "^2.0.0", "sade": "^1.8.1", "set-cookie-parser": "^2.6.0", "sirv": "^3.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.0.0", "@sveltejs/vite-plugin-svelte": "^3.0.0 || ^4.0.0-next.1 || ^5.0.0 || ^6.0.0-next.0", "svelte": "^4.0.0 || ^5.0.0-next.0", "vite": "^5.0.3 || ^6.0.0 || ^7.0.0-beta.0" }, "optionalPeers": ["@opentelemetry/api"], "bin": { "svelte-kit": "svelte-kit.js" } }, "sha512-8yZpfCfv9FQfJUBxtD8XMS+T/+ls5xdPZvMwKg4qNS9fmzfWsah3Dz8IR5cmbXtCzHkgr0j/+v6zq5Fk4IuTBA=="],
"@sveltejs/kit": ["@sveltejs/kit@2.47.3", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "@sveltejs/acorn-typescript": "^1.0.5", "@types/cookie": "^0.6.0", "acorn": "^8.14.1", "cookie": "^0.6.0", "devalue": "^5.3.2", "esm-env": "^1.2.2", "kleur": "^4.1.5", "magic-string": "^0.30.5", "mrmime": "^2.0.0", "sade": "^1.8.1", "set-cookie-parser": "^2.6.0", "sirv": "^3.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.0.0", "@sveltejs/vite-plugin-svelte": "^3.0.0 || ^4.0.0-next.1 || ^5.0.0 || ^6.0.0-next.0", "svelte": "^4.0.0 || ^5.0.0-next.0", "vite": "^5.0.3 || ^6.0.0 || ^7.0.0-beta.0" }, "optionalPeers": ["@opentelemetry/api"], "bin": { "svelte-kit": "svelte-kit.js" } }, "sha512-zN2yzBc2dIES2BSzLhNP2weYhwB77kgM/oAktICZVmmljyEmPZrlUwr14jjdK9/eDu7WdAuf6gTdYIJLTcN3Fw=="],
"@sveltejs/vite-plugin-svelte": ["@sveltejs/vite-plugin-svelte@6.2.0", "", { "dependencies": { "@sveltejs/vite-plugin-svelte-inspector": "^5.0.0", "debug": "^4.4.1", "deepmerge": "^4.3.1", "magic-string": "^0.30.17", "vitefu": "^1.1.1" }, "peerDependencies": { "svelte": "^5.0.0", "vite": "^6.3.0 || ^7.0.0" } }, "sha512-nJsV36+o7rZUDlrnSduMNl11+RoDE1cKqOI0yUEBCcqFoAZOk47TwD3dPKS2WmRutke9StXnzsPBslY7prDM9w=="],
"@sveltejs/vite-plugin-svelte": ["@sveltejs/vite-plugin-svelte@6.2.1", "", { "dependencies": { "@sveltejs/vite-plugin-svelte-inspector": "^5.0.0", "debug": "^4.4.1", "deepmerge": "^4.3.1", "magic-string": "^0.30.17", "vitefu": "^1.1.1" }, "peerDependencies": { "svelte": "^5.0.0", "vite": "^6.3.0 || ^7.0.0" } }, "sha512-YZs/OSKOQAQCnJvM/P+F1URotNnYNeU3P2s4oIpzm1uFaqUEqRxUB0g5ejMjEb5Gjb9/PiBI5Ktrq4rUUF8UVQ=="],
"@sveltejs/vite-plugin-svelte-inspector": ["@sveltejs/vite-plugin-svelte-inspector@5.0.1", "", { "dependencies": { "debug": "^4.4.1" }, "peerDependencies": { "@sveltejs/vite-plugin-svelte": "^6.0.0-next.0", "svelte": "^5.0.0", "vite": "^6.3.0 || ^7.0.0" } }, "sha512-ubWshlMk4bc8mkwWbg6vNvCeT7lGQojE3ijDh3QTR6Zr/R+GXxsGbyH4PExEPpiFmqPhYiVSVmHBjUcVc1JIrA=="],
"@types/bun": ["@types/bun@1.3.1", "", { "dependencies": { "bun-types": "1.3.1" } }, "sha512-4jNMk2/K9YJtfqwoAa28c8wK+T7nvJFOjxI4h/7sORWcypRNxBpr+TPNaCfVWq70tLCJsqoFwcf0oI0JU/fvMQ=="],
"@types/cookie": ["@types/cookie@0.6.0", "", {}, "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA=="],
"@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
"@types/node": ["@types/node@24.5.0", "", { "dependencies": { "undici-types": "~7.12.0" } }, "sha512-y1dMvuvJspJiPSDZUQ+WMBvF7dpnEqN4x9DDC9ie5Fs/HUZJA3wFp7EhHoVaKX/iI0cRoECV8X2jL8zi0xrHCg=="],
"@types/react": ["@types/react@19.1.13", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-hHkbU/eoO3EG5/MZkuFSKmYqPbSVk5byPFa3e7y/8TybHiLMACgI8seVYlicwk7H5K/rI2px9xrQp/C+AUDTiQ=="],
"acorn": ["acorn@8.15.0", "", { "bin": "bin/acorn" }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="],
"aria-query": ["aria-query@5.3.2", "", {}, "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw=="],
"axobject-query": ["axobject-query@4.1.0", "", {}, "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ=="],
"bun-types": ["bun-types@1.3.1", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-NMrcy7smratanWJ2mMXdpatalovtxVggkj11bScuWuiOoXTiKIu2eVS1/7qbyI/4yHedtsn175n4Sm4JcdHLXw=="],
"c12": ["c12@3.1.0", "", { "dependencies": { "chokidar": "^4.0.3", "confbox": "^0.2.2", "defu": "^6.1.4", "dotenv": "^16.6.1", "exsolve": "^1.0.7", "giget": "^2.0.0", "jiti": "^2.4.2", "ohash": "^2.0.11", "pathe": "^2.0.3", "perfect-debounce": "^1.0.0", "pkg-types": "^2.2.0", "rc9": "^2.1.2" }, "peerDependencies": { "magicast": "^0.3.5" }, "optionalPeers": ["magicast"] }, "sha512-uWoS8OU1MEIsOv8p/5a82c3H31LsWVR5qiyXVfBNOzfffjUWtPnhAb4BYI2uG2HfGmZmFjCtui5XNWaps+iFuw=="],
"chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="],
@ -170,6 +184,8 @@
"cookie": ["cookie@0.6.0", "", {}, "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw=="],
"csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="],
"debug": ["debug@4.4.1", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ=="],
"deepmerge": ["deepmerge@4.3.1", "", {}, "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A=="],
@ -184,7 +200,7 @@
"dotenv": ["dotenv@16.6.1", "", {}, "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow=="],
"effect": ["effect@3.16.12", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "fast-check": "^3.23.1" } }, "sha512-N39iBk0K71F9nb442TLbTkjl24FLUzuvx2i1I2RsEAQsdAdUTuUoW0vlfUXgkMTUOnYqKnWcFfqw4hK4Pw27hg=="],
"effect": ["effect@3.18.4", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "fast-check": "^3.23.1" } }, "sha512-b1LXQJLe9D11wfnOKAk3PKxuqYshQ0Heez+y5pnkd3jLj1yx9QhM72zZ9uUrOQyNvrs2GZZd/3maL0ZV18YuDA=="],
"empathic": ["empathic@2.0.0", "", {}, "sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA=="],
@ -240,7 +256,7 @@
"postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="],
"prisma": ["prisma@6.16.1", "", { "dependencies": { "@prisma/config": "6.16.1", "@prisma/engines": "6.16.1" }, "peerDependencies": { "typescript": ">=5.1.0" }, "optionalPeers": ["typescript"], "bin": { "prisma": "build/index.js" } }, "sha512-MFkMU0eaDDKAT4R/By2IA9oQmwLTxokqv2wegAErr9Rf+oIe7W2sYpE/Uxq0H2DliIR7vnV63PkC1bEwUtl98w=="],
"prisma": ["prisma@6.18.0", "", { "dependencies": { "@prisma/config": "6.18.0", "@prisma/engines": "6.18.0" }, "peerDependencies": { "typescript": ">=5.1.0" }, "optionalPeers": ["typescript"], "bin": { "prisma": "build/index.js" } }, "sha512-bXWy3vTk8mnRmT+SLyZBQoC2vtV9Z8u7OHvEu+aULYxwiop/CPiFZ+F56KsNRNf35jw+8wcu8pmLsjxpBxAO9g=="],
"pure-rand": ["pure-rand@6.1.0", "", {}, "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA=="],
@ -258,9 +274,9 @@
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
"svelte": ["svelte@5.38.10", "", { "dependencies": { "@jridgewell/remapping": "^2.3.4", "@jridgewell/sourcemap-codec": "^1.5.0", "@sveltejs/acorn-typescript": "^1.0.5", "@types/estree": "^1.0.5", "acorn": "^8.12.1", "aria-query": "^5.3.1", "axobject-query": "^4.1.0", "clsx": "^2.1.1", "esm-env": "^1.2.1", "esrap": "^2.1.0", "is-reference": "^3.0.3", "locate-character": "^3.0.0", "magic-string": "^0.30.11", "zimmerframe": "^1.1.2" } }, "sha512-UY+OhrWK7WI22bCZ00P/M3HtyWgwJPi9IxSRkoAE2MeAy6kl7ZlZWJZ8RaB+X4KD/G+wjis+cGVnVYaoqbzBqg=="],
"svelte": ["svelte@5.41.4", "", { "dependencies": { "@jridgewell/remapping": "^2.3.4", "@jridgewell/sourcemap-codec": "^1.5.0", "@sveltejs/acorn-typescript": "^1.0.5", "@types/estree": "^1.0.5", "acorn": "^8.12.1", "aria-query": "^5.3.1", "axobject-query": "^4.1.0", "clsx": "^2.1.1", "esm-env": "^1.2.1", "esrap": "^2.1.0", "is-reference": "^3.0.3", "locate-character": "^3.0.0", "magic-string": "^0.30.11", "zimmerframe": "^1.1.2" } }, "sha512-sBrVmskY0eij5+CSSuMK1kOk1xSiRh0BRTEhUnxnd1x3p42GbQmm4rwq4WMD23rUG3QR53Nfv8kpaER3tV5F4A=="],
"svelte-check": ["svelte-check@4.3.1", "", { "dependencies": { "@jridgewell/trace-mapping": "^0.3.25", "chokidar": "^4.0.1", "fdir": "^6.2.0", "picocolors": "^1.0.0", "sade": "^1.7.4" }, "peerDependencies": { "svelte": "^4.0.0 || ^5.0.0-next.0", "typescript": ">=5.0.0" }, "bin": "bin/svelte-check" }, "sha512-lkh8gff5gpHLjxIV+IaApMxQhTGnir2pNUAqcNgeKkvK5bT/30Ey/nzBxNLDlkztCH4dP7PixkMt9SWEKFPBWg=="],
"svelte-check": ["svelte-check@4.3.3", "", { "dependencies": { "@jridgewell/trace-mapping": "^0.3.25", "chokidar": "^4.0.1", "fdir": "^6.2.0", "picocolors": "^1.0.0", "sade": "^1.7.4" }, "peerDependencies": { "svelte": "^4.0.0 || ^5.0.0-next.0", "typescript": ">=5.0.0" }, "bin": { "svelte-check": "bin/svelte-check" } }, "sha512-RYP0bEwenDXzfv0P1sKAwjZSlaRyqBn0Fz1TVni58lqyEiqgwztTpmodJrGzP6ZT2aHl4MbTvWP6gbmQ3FOnBg=="],
"tinyexec": ["tinyexec@1.0.1", "", {}, "sha512-5uC6DDlmeqiOwCPmK9jMSdOuZTh8bU39Ys6yidB+UTt5hfZUPGAypSgFRiEp+jbi9qH40BLDvy85jIU88wKSqw=="],
@ -268,9 +284,11 @@
"totalist": ["totalist@3.0.1", "", {}, "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ=="],
"typescript": ["typescript@5.9.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A=="],
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
"vite": ["vite@7.1.5", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": "bin/vite.js" }, "sha512-4cKBO9wR75r0BeIWWWId9XK9Lj6La5X846Zw9dFfzMRw38IlTk2iCcUt6hsyiDRcPidc55ZParFYDXi0nXOeLQ=="],
"undici-types": ["undici-types@7.12.0", "", {}, "sha512-goOacqME2GYyOZZfb5Lgtu+1IDmAlAEu5xnD3+xTzS10hT0vzpf0SPjkXwAw9Jm+4n/mQGDP3LO8CPbYROeBfQ=="],
"vite": ["vite@7.1.12", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-ZWyE8YXEXqJrrSLvYgrRP7p62OziLW7xI5HYGWFzOvupfAlrLvURSzv/FyGyy0eidogEM3ujU+kUG1zuHgb6Ug=="],
"vitefu": ["vitefu@1.1.1", "", { "peerDependencies": { "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0-beta.0" } }, "sha512-B/Fegf3i8zh0yFbpzZ21amWzHmuNlLlmJT6n7bu5e+pCHUKQIfXSYokrqOBGEMMe9UG2sostKQF9mml/vYaWJQ=="],

390
package-lock.json generated
View File

@ -7,10 +7,14 @@
"": {
"name": "todo",
"version": "0.0.1",
"dependencies": {
"@prisma/client": "^6.16.1"
},
"devDependencies": {
"@sveltejs/adapter-auto": "^6.0.0",
"@sveltejs/kit": "^2.22.0",
"@sveltejs/vite-plugin-svelte": "^6.0.0",
"prisma": "^6.16.1",
"svelte": "^5.0.0",
"svelte-check": "^4.0.0",
"typescript": "^5.0.0",
@ -516,6 +520,91 @@
"dev": true,
"license": "MIT"
},
"node_modules/@prisma/client": {
"version": "6.16.1",
"resolved": "https://registry.npmjs.org/@prisma/client/-/client-6.16.1.tgz",
"integrity": "sha512-QaBCOY29lLAxEFFJgBPyW3WInCW52fJeQTmWx/h6YsP5u0bwuqP51aP0uhqFvhK9DaZPwvai/M4tSDYLVE9vRg==",
"hasInstallScript": true,
"license": "Apache-2.0",
"engines": {
"node": ">=18.18"
},
"peerDependencies": {
"prisma": "*",
"typescript": ">=5.1.0"
},
"peerDependenciesMeta": {
"prisma": {
"optional": true
},
"typescript": {
"optional": true
}
}
},
"node_modules/@prisma/config": {
"version": "6.16.1",
"resolved": "https://registry.npmjs.org/@prisma/config/-/config-6.16.1.tgz",
"integrity": "sha512-sz3uxRPNL62QrJ0EYiujCFkIGZ3hg+9hgC1Ae1HjoYuj0BxCqHua4JNijYvYCrh9LlofZDZcRBX3tHBfLvAngA==",
"devOptional": true,
"license": "Apache-2.0",
"dependencies": {
"c12": "3.1.0",
"deepmerge-ts": "7.1.5",
"effect": "3.16.12",
"empathic": "2.0.0"
}
},
"node_modules/@prisma/debug": {
"version": "6.16.1",
"resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-6.16.1.tgz",
"integrity": "sha512-RWv/VisW5vJE4cDRTuAHeVedtGoItXTnhuLHsSlJ9202QKz60uiXWywBlVcqXVq8bFeIZoCoWH+R1duZJPwqLw==",
"devOptional": true,
"license": "Apache-2.0"
},
"node_modules/@prisma/engines": {
"version": "6.16.1",
"resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-6.16.1.tgz",
"integrity": "sha512-EOnEM5HlosPudBqbI+jipmaW/vQEaF0bKBo4gVkGabasINHR6RpC6h44fKZEqx4GD8CvH+einD2+b49DQrwrAg==",
"devOptional": true,
"hasInstallScript": true,
"license": "Apache-2.0",
"dependencies": {
"@prisma/debug": "6.16.1",
"@prisma/engines-version": "6.16.0-7.1c57fdcd7e44b29b9313256c76699e91c3ac3c43",
"@prisma/fetch-engine": "6.16.1",
"@prisma/get-platform": "6.16.1"
}
},
"node_modules/@prisma/engines-version": {
"version": "6.16.0-7.1c57fdcd7e44b29b9313256c76699e91c3ac3c43",
"resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-6.16.0-7.1c57fdcd7e44b29b9313256c76699e91c3ac3c43.tgz",
"integrity": "sha512-ThvlDaKIVrnrv97ujNFDYiQbeMQpLa0O86HFA2mNoip4mtFqM7U5GSz2ie1i2xByZtvPztJlNRgPsXGeM/kqAA==",
"devOptional": true,
"license": "Apache-2.0"
},
"node_modules/@prisma/fetch-engine": {
"version": "6.16.1",
"resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-6.16.1.tgz",
"integrity": "sha512-fl/PKQ8da5YTayw86WD3O9OmKJEM43gD3vANy2hS5S1CnfW2oPXk+Q03+gUWqcKK306QqhjjIHRFuTZ31WaosQ==",
"devOptional": true,
"license": "Apache-2.0",
"dependencies": {
"@prisma/debug": "6.16.1",
"@prisma/engines-version": "6.16.0-7.1c57fdcd7e44b29b9313256c76699e91c3ac3c43",
"@prisma/get-platform": "6.16.1"
}
},
"node_modules/@prisma/get-platform": {
"version": "6.16.1",
"resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-6.16.1.tgz",
"integrity": "sha512-kUfg4vagBG7dnaGRcGd1c0ytQFcDj2SUABiuveIpL3bthFdTLI6PJeLEia6Q8Dgh+WhPdo0N2q0Fzjk63XTyaA==",
"devOptional": true,
"license": "Apache-2.0",
"dependencies": {
"@prisma/debug": "6.16.1"
}
},
"node_modules/@rollup/rollup-android-arm-eabi": {
"version": "4.50.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.50.1.tgz",
@ -814,7 +903,7 @@
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz",
"integrity": "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==",
"dev": true,
"devOptional": true,
"license": "MIT"
},
"node_modules/@sveltejs/acorn-typescript": {
@ -962,11 +1051,40 @@
"node": ">= 0.4"
}
},
"node_modules/c12": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/c12/-/c12-3.1.0.tgz",
"integrity": "sha512-uWoS8OU1MEIsOv8p/5a82c3H31LsWVR5qiyXVfBNOzfffjUWtPnhAb4BYI2uG2HfGmZmFjCtui5XNWaps+iFuw==",
"devOptional": true,
"license": "MIT",
"dependencies": {
"chokidar": "^4.0.3",
"confbox": "^0.2.2",
"defu": "^6.1.4",
"dotenv": "^16.6.1",
"exsolve": "^1.0.7",
"giget": "^2.0.0",
"jiti": "^2.4.2",
"ohash": "^2.0.11",
"pathe": "^2.0.3",
"perfect-debounce": "^1.0.0",
"pkg-types": "^2.2.0",
"rc9": "^2.1.2"
},
"peerDependencies": {
"magicast": "^0.3.5"
},
"peerDependenciesMeta": {
"magicast": {
"optional": true
}
}
},
"node_modules/chokidar": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz",
"integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==",
"dev": true,
"devOptional": true,
"license": "MIT",
"dependencies": {
"readdirp": "^4.0.1"
@ -978,6 +1096,16 @@
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/citty": {
"version": "0.1.6",
"resolved": "https://registry.npmjs.org/citty/-/citty-0.1.6.tgz",
"integrity": "sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==",
"devOptional": true,
"license": "MIT",
"dependencies": {
"consola": "^3.2.3"
}
},
"node_modules/clsx": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
@ -988,6 +1116,23 @@
"node": ">=6"
}
},
"node_modules/confbox": {
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.2.tgz",
"integrity": "sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ==",
"devOptional": true,
"license": "MIT"
},
"node_modules/consola": {
"version": "3.4.2",
"resolved": "https://registry.npmjs.org/consola/-/consola-3.4.2.tgz",
"integrity": "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==",
"devOptional": true,
"license": "MIT",
"engines": {
"node": "^14.18.0 || >=16.10.0"
}
},
"node_modules/cookie": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz",
@ -1026,6 +1171,30 @@
"node": ">=0.10.0"
}
},
"node_modules/deepmerge-ts": {
"version": "7.1.5",
"resolved": "https://registry.npmjs.org/deepmerge-ts/-/deepmerge-ts-7.1.5.tgz",
"integrity": "sha512-HOJkrhaYsweh+W+e74Yn7YStZOilkoPb6fycpwNLKzSPtruFs48nYis0zy5yJz1+ktUhHxoRDJ27RQAWLIJVJw==",
"devOptional": true,
"license": "BSD-3-Clause",
"engines": {
"node": ">=16.0.0"
}
},
"node_modules/defu": {
"version": "6.1.4",
"resolved": "https://registry.npmjs.org/defu/-/defu-6.1.4.tgz",
"integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==",
"devOptional": true,
"license": "MIT"
},
"node_modules/destr": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/destr/-/destr-2.0.5.tgz",
"integrity": "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==",
"devOptional": true,
"license": "MIT"
},
"node_modules/devalue": {
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/devalue/-/devalue-5.3.2.tgz",
@ -1033,6 +1202,40 @@
"dev": true,
"license": "MIT"
},
"node_modules/dotenv": {
"version": "16.6.1",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz",
"integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==",
"devOptional": true,
"license": "BSD-2-Clause",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://dotenvx.com"
}
},
"node_modules/effect": {
"version": "3.16.12",
"resolved": "https://registry.npmjs.org/effect/-/effect-3.16.12.tgz",
"integrity": "sha512-N39iBk0K71F9nb442TLbTkjl24FLUzuvx2i1I2RsEAQsdAdUTuUoW0vlfUXgkMTUOnYqKnWcFfqw4hK4Pw27hg==",
"devOptional": true,
"license": "MIT",
"dependencies": {
"@standard-schema/spec": "^1.0.0",
"fast-check": "^3.23.1"
}
},
"node_modules/empathic": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/empathic/-/empathic-2.0.0.tgz",
"integrity": "sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA==",
"devOptional": true,
"license": "MIT",
"engines": {
"node": ">=14"
}
},
"node_modules/esbuild": {
"version": "0.25.9",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.9.tgz",
@ -1092,6 +1295,36 @@
"@jridgewell/sourcemap-codec": "^1.4.15"
}
},
"node_modules/exsolve": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.7.tgz",
"integrity": "sha512-VO5fQUzZtI6C+vx4w/4BWJpg3s/5l+6pRQEHzFRM8WFi4XffSP1Z+4qi7GbjWbvRQEbdIco5mIMq+zX4rPuLrw==",
"devOptional": true,
"license": "MIT"
},
"node_modules/fast-check": {
"version": "3.23.2",
"resolved": "https://registry.npmjs.org/fast-check/-/fast-check-3.23.2.tgz",
"integrity": "sha512-h5+1OzzfCC3Ef7VbtKdcv7zsstUQwUDlYpUTvjeUsJAssPgLn7QzbboPtL5ro04Mq0rPOsMzl7q5hIbRs2wD1A==",
"devOptional": true,
"funding": [
{
"type": "individual",
"url": "https://github.com/sponsors/dubzzz"
},
{
"type": "opencollective",
"url": "https://opencollective.com/fast-check"
}
],
"license": "MIT",
"dependencies": {
"pure-rand": "^6.1.0"
},
"engines": {
"node": ">=8.0.0"
}
},
"node_modules/fdir": {
"version": "6.5.0",
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
@ -1125,6 +1358,24 @@
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/giget": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/giget/-/giget-2.0.0.tgz",
"integrity": "sha512-L5bGsVkxJbJgdnwyuheIunkGatUF/zssUoxxjACCseZYAVbaqdh9Tsmmlkl8vYan09H7sbvKt4pS8GqKLBrEzA==",
"devOptional": true,
"license": "MIT",
"dependencies": {
"citty": "^0.1.6",
"consola": "^3.4.0",
"defu": "^6.1.4",
"node-fetch-native": "^1.6.6",
"nypm": "^0.6.0",
"pathe": "^2.0.3"
},
"bin": {
"giget": "dist/cli.mjs"
}
},
"node_modules/is-reference": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.3.tgz",
@ -1135,6 +1386,16 @@
"@types/estree": "^1.0.6"
}
},
"node_modules/jiti": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/jiti/-/jiti-2.5.1.tgz",
"integrity": "sha512-twQoecYPiVA5K/h6SxtORw/Bs3ar+mLUtoPSc7iMXzQzK8d7eJ/R09wmTwAjiamETn1cXYPGfNnu7DMoHgu12w==",
"devOptional": true,
"license": "MIT",
"bin": {
"jiti": "lib/jiti-cli.mjs"
}
},
"node_modules/kleur": {
"version": "4.1.5",
"resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz",
@ -1208,6 +1469,54 @@
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
}
},
"node_modules/node-fetch-native": {
"version": "1.6.7",
"resolved": "https://registry.npmjs.org/node-fetch-native/-/node-fetch-native-1.6.7.tgz",
"integrity": "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==",
"devOptional": true,
"license": "MIT"
},
"node_modules/nypm": {
"version": "0.6.2",
"resolved": "https://registry.npmjs.org/nypm/-/nypm-0.6.2.tgz",
"integrity": "sha512-7eM+hpOtrKrBDCh7Ypu2lJ9Z7PNZBdi/8AT3AX8xoCj43BBVHD0hPSTEvMtkMpfs8FCqBGhxB+uToIQimA111g==",
"devOptional": true,
"license": "MIT",
"dependencies": {
"citty": "^0.1.6",
"consola": "^3.4.2",
"pathe": "^2.0.3",
"pkg-types": "^2.3.0",
"tinyexec": "^1.0.1"
},
"bin": {
"nypm": "dist/cli.mjs"
},
"engines": {
"node": "^14.16.0 || >=16.10.0"
}
},
"node_modules/ohash": {
"version": "2.0.11",
"resolved": "https://registry.npmjs.org/ohash/-/ohash-2.0.11.tgz",
"integrity": "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==",
"devOptional": true,
"license": "MIT"
},
"node_modules/pathe": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
"integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==",
"devOptional": true,
"license": "MIT"
},
"node_modules/perfect-debounce": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz",
"integrity": "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==",
"devOptional": true,
"license": "MIT"
},
"node_modules/picocolors": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
@ -1228,6 +1537,18 @@
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/pkg-types": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.3.0.tgz",
"integrity": "sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==",
"devOptional": true,
"license": "MIT",
"dependencies": {
"confbox": "^0.2.2",
"exsolve": "^1.0.7",
"pathe": "^2.0.3"
}
},
"node_modules/postcss": {
"version": "8.5.6",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
@ -1257,11 +1578,65 @@
"node": "^10 || ^12 || >=14"
}
},
"node_modules/prisma": {
"version": "6.16.1",
"resolved": "https://registry.npmjs.org/prisma/-/prisma-6.16.1.tgz",
"integrity": "sha512-MFkMU0eaDDKAT4R/By2IA9oQmwLTxokqv2wegAErr9Rf+oIe7W2sYpE/Uxq0H2DliIR7vnV63PkC1bEwUtl98w==",
"devOptional": true,
"hasInstallScript": true,
"license": "Apache-2.0",
"dependencies": {
"@prisma/config": "6.16.1",
"@prisma/engines": "6.16.1"
},
"bin": {
"prisma": "build/index.js"
},
"engines": {
"node": ">=18.18"
},
"peerDependencies": {
"typescript": ">=5.1.0"
},
"peerDependenciesMeta": {
"typescript": {
"optional": true
}
}
},
"node_modules/pure-rand": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz",
"integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==",
"devOptional": true,
"funding": [
{
"type": "individual",
"url": "https://github.com/sponsors/dubzzz"
},
{
"type": "opencollective",
"url": "https://opencollective.com/fast-check"
}
],
"license": "MIT"
},
"node_modules/rc9": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/rc9/-/rc9-2.1.2.tgz",
"integrity": "sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg==",
"devOptional": true,
"license": "MIT",
"dependencies": {
"defu": "^6.1.4",
"destr": "^2.0.3"
}
},
"node_modules/readdirp": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz",
"integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==",
"dev": true,
"devOptional": true,
"license": "MIT",
"engines": {
"node": ">= 14.18.0"
@ -1407,6 +1782,13 @@
"typescript": ">=5.0.0"
}
},
"node_modules/tinyexec": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.1.tgz",
"integrity": "sha512-5uC6DDlmeqiOwCPmK9jMSdOuZTh8bU39Ys6yidB+UTt5hfZUPGAypSgFRiEp+jbi9qH40BLDvy85jIU88wKSqw==",
"devOptional": true,
"license": "MIT"
},
"node_modules/tinyglobby": {
"version": "0.2.15",
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
@ -1438,7 +1820,7 @@
"version": "5.9.2",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz",
"integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==",
"dev": true,
"devOptional": true,
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",

View File

@ -2,14 +2,14 @@
"name": "todo",
"version": "0.0.1",
"devDependencies": {
"@sveltejs/adapter-auto": "^6.0.0",
"@sveltejs/kit": "^2.22.0",
"@sveltejs/vite-plugin-svelte": "^6.0.0",
"prisma": "^6.16.1",
"svelte": "^5.0.0",
"svelte-check": "^4.0.0",
"typescript": "^5.0.0",
"vite": "^7.0.4"
"@sveltejs/adapter-auto": "^6.1.1",
"@sveltejs/kit": "^2.47.3",
"@sveltejs/vite-plugin-svelte": "^6.2.1",
"prisma": "^6.18.0",
"svelte": "^5.41.4",
"svelte-check": "^4.3.3",
"typescript": "^5.9.3",
"vite": "^7.1.12"
},
"private": true,
"scripts": {
@ -20,5 +20,9 @@
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch"
},
"type": "module"
"type": "module",
"dependencies": {
"@prisma/client": "^6.18.0",
"@types/bun": "^1.3.1"
}
}

View File

@ -0,0 +1,34 @@
-- CreateTable
CREATE TABLE "public"."User" (
"id" SERIAL NOT NULL,
"display_name" TEXT NOT NULL,
"email" TEXT NOT NULL,
"password_hash" TEXT NOT NULL,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,
CONSTRAINT "User_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "public"."Task" (
"id" SERIAL NOT NULL,
"userId" INTEGER NOT NULL,
"checked" BOOLEAN NOT NULL DEFAULT false,
"checked_at" TIMESTAMP(3),
"content" TEXT NOT NULL,
"parentId" INTEGER,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,
CONSTRAINT "Task_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "User_email_key" ON "public"."User"("email");
-- AddForeignKey
ALTER TABLE "public"."Task" ADD CONSTRAINT "Task_userId_fkey" FOREIGN KEY ("userId") REFERENCES "public"."User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "public"."Task" ADD CONSTRAINT "Task_parentId_fkey" FOREIGN KEY ("parentId") REFERENCES "public"."Task"("id") ON DELETE SET NULL ON UPDATE CASCADE;

View File

@ -0,0 +1,10 @@
/*
Warnings:
- You are about to drop the column `content` on the `Task` table. All the data in the column will be lost.
- Added the required column `topic` to the `Task` table without a default value. This is not possible if the table is not empty.
*/
-- AlterTable
ALTER TABLE "public"."Task" RENAME COLUMN "content" TO "topic";
ALTER TABLE "public"."Task" ADD COLUMN "description" TEXT;

View File

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

View File

@ -1,15 +1,37 @@
// 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"
output = "../.generated.prisma"
output = "../node_modules/.prisma/client"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
model User {
id Int @id @default(autoincrement())
display_name String
email String @unique
password_hash String
tasks Task[]
created_at DateTime @default(now())
updated_at DateTime @updatedAt
}
model Task {
id Int @id @default(autoincrement())
userId Int
user User @relation(fields: [userId], references: [id])
checked Boolean @default(false)
checked_at DateTime?
topic String
description String?
parentId Int?
parent Task? @relation("Subtasks", fields: [parentId], references: [id])
subtasks Task[] @relation("Subtasks")
created_at DateTime @default(now())
updated_at DateTime @updatedAt
}

9
src/app.d.ts vendored
View File

@ -2,8 +2,13 @@
// for information about these interfaces
declare global {
namespace App {
// interface Error {}
// interface Locals {}
interface Error {
message: string
cause?: number,
}
interface Locals {
user?: User
}
// interface PageData {}
// interface PageState {}
// interface Platform {}

View File

@ -3,6 +3,8 @@
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" href="%sveltekit.assets%/favicon.svg" />
<link rel="stylesheet" href="%sveltekit.assets%/global.css" />
%sveltekit.head%
</head>
<body data-sveltekit-preload-data="hover">

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

@ -0,0 +1,77 @@
import type { Handle } from "@sveltejs/kit"
import { error, redirect, json, fail } from "@sveltejs/kit"
import { Error401Cause } from "$lib/errors"
import Config from "$lib/server/config"
import UserMgmt from "$lib/server/usermgmt"
function action_fail<T = undefined>(status: number, data: T) {
return json(fail(status, data))
}
export const handle: Handle = async ({ event, resolve }) => {
const token = event.cookies.get("session")
let session = await UserMgmt.session_login(token ?? "")
if (session && event.route.id !== "/api/refresh") {
if (session.expires.getTime() < Date.now() + Config.session_refresh_grace) {
const response = await event.fetch("/api/refresh", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
token: token
})
})
if (response.ok) {
const new_session = await response.json()
if (new_session && new_session.token !== null && new_session.expires !== null) {
event.cookies.set("session", new_session.token, {
expires: new Date(new_session.expires),
httpOnly: true,
secure: true,
sameSite: "strict",
path: "/"
})
// For mobile
/*event.setHeaders({
"X-Authorization-Refresh": new_session.token
})*/
session = await UserMgmt.session_login(new_session.token)
}
}
}
}
event.locals.user = session?.user
if (!event.route.id) {
return error(404, { message: "Diese Seite existiert nicht" })
}
if (event.route.id.startsWith("/api")) {
if (event.route.id.startsWith("/api/users") && !event.locals.user) {
return error(401, { cause: Error401Cause.NotLoggedIn, message: "Please log in" })
}
} else {
if (!event.locals.user && !event.route.id.startsWith("/login")) {
if (event.request.method === "POST") {
if (event.request.headers.get("x-sveltekit-action")) {
return action_fail(401, { cause: Error401Cause.NotLoggedIn, message: "Bitte melden Sie sich an." })
}
return error(401, { cause: Error401Cause.NotLoggedIn, message: "Bitte melden Sie sich an." })
}
return redirect(307, "/login")
}
}
return await resolve(event)
}

View File

@ -0,0 +1,99 @@
<script lang="ts">
import { onMount } from "svelte";
let { checked = $bindable(), ...props} = $props()
let loaded = $state(false)
onMount(() => {
loaded = true
})
</script>
<div class="shell" class:loaded={loaded}>
<label>
<input type="checkbox" bind:checked/>
<svg width="10px" height="10px" viewBox="0 0 12 9">
<polyline points="1,5 4,8 11,1" fill="none"></polyline>
</svg>
</label>
</div>
<style>
label {
display: block;
width: 16px;
height: 16px;
position: relative;
top: 50%;
left: 50%;
transform: translateY(-50%) translateX(-50%);
border: 1px solid black;
border-radius: 50%;
user-select: none;
cursor: pointer;
}
label:hover {
border-color: gray;
}
label:has(>input:checked) {
background: green;
}
.loaded label:has(>input:checked) {
animation: pop 0.6s ease;
}
.loaded label::before {
content: "";
position: absolute;
inset: 0;
background: white;
border-radius: 50%;
opacity: 1;
transform: scale(0);
transition-delay: 0.2s;
}
.loaded label:has(>input:checked)::before {
transform: scale(2.2);
opacity: 0;
transition: all 0.6s ease;
}
@keyframes pop {
50% {
transform: translateY(-50%) translateX(-50%) scale(1.2);
}
}
input {
display: none;
}
svg {
position: absolute;
top: 3px;
left: 3px;
stroke: white;
stroke-width: 1.5px;
stroke-dasharray: 16px;
stroke-dashoffset: 16px;
}
.loaded svg {
transition: all 0.5s ease;
}
label:has(> input:checked) > svg {
stroke-dashoffset: 0px;
}
</style>

View File

@ -0,0 +1,146 @@
<script module lang="ts">
import type { Task } from "@prisma/client";
export interface Props {
task: Task | null
form: string
open?: boolean
}
</script>
<script lang="ts">
let { task, form, open = $bindable() }: Props = $props();
let submitButton: HTMLButtonElement;
let topicTextArea: HTMLTextAreaElement;
let descriptionTextArea: HTMLTextAreaElement;
function cancelOverlay() {
// add change alert
open = false;
}
</script>
<svelte:window onkeydown={(event) => {
if (event instanceof KeyboardEvent) {
if (event.key === "Escape") {
cancelOverlay()
}
}
}} />
<div class="overlay" class:open={open}>
<div class="background" role="presentation" aria-hidden="true" onclick={() => { cancelOverlay() }}></div>
<div class="content">
<textarea class="topic" form={form} bind:this={topicTextArea} name="topic" placeholder="What would you like to do" rows="1">{task?.topic ?? ""}</textarea>
<textarea class="description" form={form} bind:this={descriptionTextArea} name="description" placeholder="Add a description here.">{task?.description ?? ""}</textarea>
<button
type="submit"
form={form}
bind:this={submitButton}
onclick={(event) => {
if (topicTextArea.value.length != 0) {
(event.target as HTMLButtonElement).form?.requestSubmit()
}
event.preventDefault()
}}
>Save and close</button>
</div>
</div>
<style>
.overlay {
display: none;
position: fixed;
top: 0;
left: 0;
z-index: 10;
width: 100%;
height: 100%;
overflow: auto;
}
.open {
display: block;
}
:global(body:has(.overlay.open)) {
overflow: hidden;
}
.background {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: black;
opacity: 50%;
}
.content {
position: absolute;
top: 50%;
left: 50%;
transform: translateX(-50%) translateY(-50%);
width: 75%;
min-height: 20%;
background: darkgreen;
padding: 10px;
display: flex;
flex-direction: column;
gap: 10px;
}
textarea {
box-sizing: border-box;
min-width: 100%;
background: none;
border: none;
field-sizing: content;
&:focus {
outline: none;
}
&.topic {
font-size: 24px;
&:focus {
border-bottom: 1px solid black;
}
}
&.description {
min-height: 10em;
&:focus {
border-left: 1px solid black;
}
}
}
</style>

View File

@ -0,0 +1,116 @@
<script module>
import type { Task } from "@prisma/client"
import Checkbox from "./checkbox.svelte"
import { onMount } from "svelte";
export interface TaskComponentProps {
task: Task
}
</script>
<script lang="ts">
const { task }: TaskComponentProps = $props()
let loaded = $state(false)
onMount(() => { loaded = true })
let checked = $state(task.checked)
</script>
<div class="container" class:loaded={loaded}>
<div class="main-task">
<div class="checkbox">
<Checkbox bind:checked={checked}/>
</div>
<div class="taskheader">
<div><h2 class="taskheader">{task.content}</h2></div>
</div>
{#if true}
<div class="taskcontent">{@html [task.content, "<br>"].join().repeat(5)}</div>
{/if}
<div class="due">
Due: <br/>
{new Date(task.created_at).toLocaleDateString()} <br/>
{new Date(task.created_at).toLocaleTimeString()}
</div>
</div>
</div>
<style>
.container {
width: var(--width, 100%);
min-height: var(--min-height, 50px);
padding: 10px;
border: 1px solid gray;
display: flex;
flex-direction: column;
justify-items: center;
justify-content: stretch;
align-content: flex-start;
}
.main-task {
width: 100%;
display: grid;
grid-template-columns: 25px auto fit-content(11ch);
grid-template-areas:
'checkboxArea headerArea dueArea';
column-gap: 10px;
align-content: stretch;
}
.main-task:has(.taskcontent) {
grid-template-areas:
'checkboxArea headerArea dueArea'
'. contentArea dueArea';
}
.checkbox {
grid-area: checkboxArea;
display: flex;
justify-content: center;
align-content: center;
}
.taskheader {
grid-area: headerArea;
width: fit-content;
transform: translateY(0.075em);
}
.taskheader h2 {
margin: 0;
}
.taskcontent {
grid-area: contentArea;
width: 100%;
text-align: left;
margin-top: 5px;
}
.due {
grid-area: dueArea;
min-width: 11ch;
}
.loaded {
transition: none;
}
.loaded .checked {
transition: all 0.3s ease;
}
</style>

45
src/lib/errors.ts Normal file
View File

@ -0,0 +1,45 @@
export class ArgumentError extends Error {
constructor(message: string) {
super(message)
}
}
export class DuplicateError extends Error {
fields: Array<string>
constructor(fields: Array<string>, message: string) {
super(message)
this.fields = fields
}
}
export const enum Error401Cause {
NotLoggedIn
}
export const enum RegisterResponseCause {
Server = 1,
MalformedRequest,
EmailLength,
EmailFormat,
EmailDuplicate,
PasswordLength,
DisplayNameLength
}
export const enum LoginResponseCause {
Server = 1,
MalformedRequest,
EmailLength,
PasswordLength,
NotFound,
Timeout
}
export const enum RefreshResponseCause {
Server = 1,
MalformedRequest,
InvalidToken,
InvalidSession
}

View File

@ -1 +0,0 @@
// place files you want to import through the `$lib` alias in this folder.

65
src/lib/server/config.ts Normal file
View File

@ -0,0 +1,65 @@
import Bun from "bun"
import path from "node:path"
const to_absolute_path = (p: string): string => {
if (path.isAbsolute(p)) {
return p
}
return path.resolve(process.cwd(), p)
}
const resolve_env_to_path = (env: string|undefined, fallback: string): string => {
if (!env) {
return to_absolute_path(fallback)
}
return to_absolute_path(env)
}
const resolve_env_to_boolean = (env: string|undefined, fallback: boolean): boolean => {
if (!env) {
return fallback
}
const str = env.toLowerCase()
switch (str) {
case "false":
case "no":
case "off": return false
}
return true
}
class Config {
private _log_dir: string
private _log_to_file_when_debug: boolean
readonly is_debug: boolean = process.env.NODE_ENV != "production"
readonly is_production: boolean = process.env.NODE_ENV == "production"
private _session_timeout: number = 15 * 60 * 1000
private _session_refresh_grace: number = 5 * 60 * 1000 // time until expiration
readonly bypass_login = this.is_debug && process.env.BYPASS_LOGIN == "true"
get log_dir(): string {
return this._log_dir
}
get log_to_file_when_debug(): boolean {
return this._log_to_file_when_debug
}
get session_timeout(): number { return this._session_timeout }
get session_refresh_grace(): number { return this._session_refresh_grace }
constructor() {
this._log_dir = resolve_env_to_path(process.env.APP_LOG_DIR, "./data/logs")
this._log_to_file_when_debug = resolve_env_to_boolean(process.env.LOG_TO_FILE_WHEN_DEBUG, false)
}
}
const _config = new Config()
export default _config;

View File

@ -0,0 +1,5 @@
import { PrismaClient } from "@prisma/client"
const prisma = new PrismaClient()
export default prisma

45
src/lib/server/log.ts Normal file
View File

@ -0,0 +1,45 @@
export enum LogSeverity {
FATAL,
ERROR,
WARN,
INFO,
DEBUG,
TRACE
}
export enum LogModule {
PROCESS,
USER,
DATABASE
}
abstract class Logger {
readonly module = {
...LogModule
}
abstract log(severity: LogSeverity, module: LogModule, message: string): void
fatal(module: LogModule, message: string): void { this.log(LogSeverity.FATAL, module, message) }
error(module: LogModule, message: string): void { this.log(LogSeverity.ERROR, module, message) }
warn(module: LogModule, message: string): void { this.log(LogSeverity.WARN, module, message) }
info(module: LogModule, message: string): void { this.log(LogSeverity.INFO, module, message) }
debug(module: LogModule, message: string): void { this.log(LogSeverity.DEBUG, module, message) }
trace(module: LogModule, message: string): void { this.log(LogSeverity.TRACE, module, message) }
}
function create_log_string(severity: LogSeverity, module: LogModule, message: string): string {
return `${(new Date()).toISOString()} [${LogModule[module]}] [${LogSeverity[severity]}] ${message}`
}
class DebugLogger extends Logger {
log(severity: LogSeverity, module: LogModule, message: string): void {
console.log(create_log_string(severity, module, message))
}
}
const _logger: Logger = new DebugLogger()
export default _logger

203
src/lib/server/usermgmt.ts Normal file
View File

@ -0,0 +1,203 @@
import Bun from "bun"
import Crypto from "node:crypto"
import { Prisma } from "@prisma/client"
import { ArgumentError, DuplicateError } from "$lib/errors"
import Config from "$lib/server/config"
import Log from "$lib/server/log"
import db from "$lib/server/database"
export type User = Prisma.UserGetPayload<Prisma.UserDefaultArgs>
export interface SessionData {
user: User
token: string
issued: Date
expires: Date
}
interface CacheUserInfo {
user: User
last_update: Date
}
interface CacheSessionInfo {
user_id: number
issued: Date
expires: Date
}
class Cache {
private _id_map: Map<number, CacheUserInfo> = new Map()
private _session_map: Map<string, CacheSessionInfo> = new Map()
add(user: User, token: string, issued: Date, expires: Date): SessionData {
this._id_map.set(user.id, {
user: user,
last_update: new Date()
})
this._session_map.set(token, {
user_id: user.id,
issued: issued,
expires: expires
})
return {
user: user,
token: token,
issued: issued,
expires: expires
}
}
invalidate_session(session: SessionData) {
this._session_map.delete(session.token)
}
get_session(token: string): SessionData|null {
const session_info = this._session_map.get(token)
if (!session_info) return null
const user_info = this._id_map.get(session_info.user_id)
if (!user_info) return null
return {
user: user_info.user,
token: token,
issued: session_info.issued,
expires: session_info.expires
}
}
}
class UserMgmt {
_cache: Cache = new Cache()
async register(email: string, password: string, display_name: string): Promise<User|null> {
if (email.length == 0 || password.length == 0 || display_name.length == 0) {
throw new ArgumentError("No field may be empty")
}
try {
const user = await db.user.create({
data: {
email: email,
password_hash: await Bun.password.hash(password, "argon2id"),
display_name: display_name
}
})
Log.info(Log.module.USER, `Created user with id ${user.id}`)
return user;
} catch (e) {
if (e instanceof Prisma.PrismaClientKnownRequestError && e.code == "P2002") {
const duplicates = Array.isArray(e.meta?.target) ? e.meta.target as Array<string> : ["unknown"]
throw new DuplicateError(duplicates, "Already exists")
}
Log.error(Log.module.DATABASE, `Failed to create User in database! Error: ${JSON.stringify(e)}`)
return null
}
}
async login(email: string, password: string): Promise<SessionData|null> {
const user = await db.user.findUnique({
where: {
email: email
}
})
if (!user) {
// throw of timing attacks
await Bun.password.verify("a", "$argon2id$v=19$m=16,t=2,p=1$ZHB6Zjd4NXV6RXZBZk9wRg$QaYjeAGLon+x3c5I1KB7UQ")
return null
}
if (!await Bun.password.verify(password, user.password_hash)) {
return null
}
const session_info = this._generate_session_for_user(user)
return session_info
}
async session_login(token: string): Promise<SessionData|null> {
const session = this._cache.get_session(token)
if (!session) {
return null
}
if (session?.expires.getTime() < Date.now()) {
return null
}
return session
}
async refresh_session(token: string): Promise<SessionData|null> {
const session = await this.session_login(token)
if (!session) {
return null
}
const new_session = this._generate_session_for_user(session.user)
this._cache.invalidate_session(session)
return new_session
}
private _generate_session_for_user(user: User): SessionData {
const token = Crypto.randomBytes(32).toBase64()
const session_info = this._cache.add(user, token, new Date(), new Date(Date.now() + Config.session_timeout))
return session_info
}
}
class _LOGIN_BYPASS_Mgmt extends UserMgmt {
global_session: SessionData | null = null
constructor() {
super()
db.user.findFirst().then((user) => {
if (!user) {
return
}
this.global_session = {
token: "",
user: user,
expires: new Date(8640000000000000), // Max Time
issued: new Date()
}
})
}
async login(): Promise<SessionData | null> {
if (!this.global_session) {
const user = await db.user.findFirst()
if (user) {
this.global_session = {
token: "",
user: user,
expires: new Date(8640000000000000), // Max Time
issued: new Date()
}
}
}
return this.global_session
}
async session_login(): Promise<SessionData | null> {
return this.login()
}
}
const _manager = (Config.is_debug && Config.bypass_login) ? new _LOGIN_BYPASS_Mgmt() : new UserMgmt()
export default _manager

13
src/lib/util.ts Normal file
View File

@ -0,0 +1,13 @@
function check_email_format(email: string) {
const trimmed = email.trim()
if (trimmed.length > 254) return false
const emailRegex = /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:.[a-zA-Z0-9-]+)*$/;
return emailRegex.test(trimmed)
}
export default {
check_email_format,
}

View File

@ -1,11 +1,5 @@
<script lang="ts">
import favicon from '$lib/assets/favicon.svg';
let { children } = $props();
</script>
<svelte:head>
<link rel="icon" href={favicon} />
</svelte:head>
{@render children?.()}

View File

@ -0,0 +1,28 @@
import type { Actions, PageServerLoad } from "./$types"
import { fail } from "@sveltejs/kit"
export const load: PageServerLoad = async ({ locals, fetch }) => {
const response = await fetch("/api/users/tasks", {
method: "GET"
})
return { tasks: await response.json() }
}
export const actions = {
edit: async ({ request, fetch }) => {
const data = await request.formData()
const response = await fetch("/api/users/tasks/create", {
method: "POST",
body: data
})
console.log(data)
return { }
}
} satisfies Actions

View File

@ -1,2 +1,100 @@
<h1>Welcome to SvelteKit</h1>
<p>Visit <a href="https://svelte.dev/docs/kit">svelte.dev/docs/kit</a> to read the documentation</p>
<script lang="ts">
import { enhance } from '$app/forms'
import { pushState, replaceState } from '$app/navigation'
import { page } from "$app/state"
import Task from '$lib/components/task.svelte'
import TaskOverlay from '$lib/components/task-overlay.svelte'
const { data } = $props()
$inspect(data)
let open = $state(false);
let lastOpen = false
let selectedTask = $state(null)
$effect(() => {
open
selectedTask
if (open != lastOpen) {
lastOpen = open
if (open) {
//pushState(`/tasks/${data.tasks[0].id}`, {})
} else {
if (page.route.id !== "/") {
history.back()
}
}
}
})
</script>
<form id="form_edit_task" method="POST"
action="?/edit"
use:enhance={({ }) => {
return async ({ result }) => {
if (result.type === 'success') {
open = false
} else /*if (result.type === 'error' || result.type === 'failure')*/ {
console.log(result)
}
}
}}
></form>
<div class="container">
<h1>Tasks</h1>
<div class="create_task">
<input form="form_edit_task" name="content" placeholder="Create task here" onclick={() => { open = true }}/>
</div>
{#each data.tasks as task}
<Task task={task} --width="75%" />
{/each}
</div>
{#if open}
<TaskOverlay task={selectedTask} bind:open={open} form="form_edit_task" />
{/if}
<style>
.container {
width: 100%;
text-align: center;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
gap: 5px;
}
.create_task {
width: 75%;
padding: 5px;
display: grid;
grid-template-columns: 25px auto 11ch;
grid-template-areas:
'. headerArea .';
column-gap: 10px;
align-content: stretch;
}
.create_task input {
grid-area: headerArea;
width: 100%;
}
</style>

View File

@ -0,0 +1,33 @@
import type { RequestHandler } from "./$types"
import { error, json, text } from "@sveltejs/kit"
import UserMgmt from "$lib/server/usermgmt"
import { LoginResponseCause } from "$lib/errors"
export const POST: RequestHandler = async ({ request, cookies }) => {
const data = await request.formData()
const email = data.get("email")
const password = data.get("password")
if (data.keys.length > 2 || !(typeof email === "string" && typeof password === "string")) {
return error(400, { cause: LoginResponseCause.MalformedRequest, message: "Invalid request" })
}
if (email.length == 0) {
return error(400, { cause: LoginResponseCause.EmailLength, message: "Email must be provided" })
}
if (password.length == 0) {
return error(400, { cause: LoginResponseCause.PasswordLength, message: "Password must be provided" })
}
const session = await UserMgmt.login(email, password)
if (!session) {
return error(401, { message: "Invalid username or password" })
}
return json({ token: session.token, expires: session.expires })
}

View File

@ -0,0 +1,27 @@
import type { RequestHandler } from "./$types"
import { error, json } from "@sveltejs/kit"
import UserMgmt from "$lib/server/usermgmt"
import { RefreshResponseCause } from "$lib/errors"
export const POST: RequestHandler = async ({ request }) => {
const data = await request.json()
const token = data["token"]
if (!token || typeof token !== "string") {
return error(400, { cause: RefreshResponseCause.MalformedRequest, message: "token must be provided as string." })
}
const new_session = await UserMgmt.refresh_session(token)
if (!new_session) {
return error(401, { cause: RefreshResponseCause.InvalidSession, message: "No session for token" })
}
return json({
token: new_session.token,
expires: new_session.expires
})
}

View File

@ -0,0 +1,48 @@
import type { RequestHandler } from "./$types"
import { json, error } from "@sveltejs/kit"
import UserMgmt from "$lib/server/usermgmt"
import Util from "$lib/util"
import { DuplicateError, RegisterResponseCause } from "$lib/errors"
export const POST: RequestHandler = async ({ request }) => {
const data = await request.formData()
const email = data.get("email")
const password = data.get("password")
const display_name = data.get("display_name")
if (data.keys.length > 3 || !(typeof email === "string" && typeof password === "string" && typeof display_name === "string")) {
return error(400, { cause: RegisterResponseCause.MalformedRequest, message: "Invalid field arguments" })
}
if (email.length == 0) {
return error(400, { cause: RegisterResponseCause.EmailLength, message: "email must not be empty" })
}
if (password.length == 0) {
return error(400, { cause: RegisterResponseCause.PasswordLength, message: "password must not be empty" })
}
if (display_name.length == 0) {
return error(400, { cause: RegisterResponseCause.DisplayNameLength, message: "Display name must not be empty" })
}
if (!Util.check_email_format(email)) {
return error(400, { cause: RegisterResponseCause.EmailFormat, message: "invalid email format" })
}
try {
const user = await UserMgmt.register(email, password, display_name)
} catch (e) {
if (e instanceof DuplicateError) {
if (e.fields.includes("email")) {
return error(409, { cause: RegisterResponseCause.EmailDuplicate, message: "email already in use" })
}
}
return error(500, { cause: RegisterResponseCause.Server, message: "Server failed to create user"})
}
return json({})
}

View File

@ -0,0 +1,31 @@
import type { RequestHandler } from "./$types";
import { json, error } from "@sveltejs/kit"
import { Error401Cause } from "$lib/errors";
import db from "$lib/server/database"
enum StatusFilterValues {
all,
open,
done,
}
export const GET: RequestHandler = async ({ request, locals, url }) => {
const filter_param = url.searchParams.get("status")
const filter = (filter_param && filter_param in StatusFilterValues)
? StatusFilterValues[filter_param as keyof typeof StatusFilterValues]
: StatusFilterValues.all
const tasks = await db.task.findMany({
where: {
userId: {
equals: locals.user.id
}
}
})
return json(tasks)
}

View File

@ -0,0 +1,29 @@
import type { RequestHandler } from "./$types";
import { json, error } from "@sveltejs/kit"
import db from "$lib/server/database"
export const POST: RequestHandler = async ({ request, locals }) => {
const data = await request.formData()
const content = data.get("content")
if (!content || typeof content !== "string") {
return error(400, { message: "content must be specified" })
}
const task = await db.task.create({
data: {
content: content,
user: {
connect: {
id: locals.user.id
}
}
}
})
return json(task)
}

View File

@ -0,0 +1,46 @@
import type { Actions } from "./$types"
import { fail } from "@sveltejs/kit"
import Log from "$lib/server/log"
export const actions = {
login: async ({ request, fetch, cookies }) => {
console.log("login")
const formData = await request.formData()
const result = await fetch("/api/login", {
method: "POST",
body: formData
})
if (!result.ok) {
return fail(401, { email: formData.get("email") ?? "", message: "Benutzername oder Passwort ist falsch." })
}
const body = await (async () => {
try {
return await result.json()
} catch (e) {
Log.error(Log.module.PROCESS, `Failed to parse body of login response from endpoint`)
return null
}
})()
if (!body || !body.token || !body.expires) {
return fail(502, "Invalid response from login endpoint")
}
cookies.set("session", body.token, {
expires: new Date(body.expires),
httpOnly: true,
secure: true,
sameSite: "strict",
path: "/"
})
return { message: "Erfolg" }
}
} satisfies Actions;

View File

@ -0,0 +1,68 @@
<script lang="ts">
import type { PageProps } from "./$types"
const { form }: PageProps = $props()
</script>
<form action="?/login" method="POST" id="form_login"></form>
<div class="container">
<div class="grid">
<h1>Login</h1>
<p class="error_msg">{form?.message}</p>
<label for="username">E-Mail: </label><input type="text" id="username" form="form_login" name="email" />
<label for="password">Passwort: </label><input type="password" id="password" form="form_login" name="password" />
<button type="submit" form="form_login">-></button>
</div>
<p><a href="/login/register">Noch nicht registriert? Klicke hier!</a></p>
</div>
<style>
.container {
display: flex;
width: 100%;
height: 100%;
padding-top: 100px;
flex-direction: column;
justify-content: center;
align-items: center;
}
.grid {
display: grid;
gap: 10px 50px;
grid-template-columns: 1fr 3fr;
justify-content: space-between;
}
.grid > h1 {
width: 100%;
text-align: center;
grid-column: 1 / span 2;
}
.grid > p {
width: 100%;
grid-column: 1 / span 2;
}
.grid > button {
width: 50%;
margin-top: 10px;
margin-left: auto;
grid-column: 2;
}
.error_msg {
color: red;
}
</style>

View File

@ -0,0 +1,38 @@
import { RegisterResponseCause } from "$lib/errors";
import type { Actions } from "./$types";
import { fail } from "@sveltejs/kit"
export const actions = {
register: async ({ request, fetch }) => {
console.log("register")
const result = await fetch("/api/register", {
method: "POST",
body: await request.formData()
})
const result_body = await result.json()
if (!result.ok) {
if (!result_body.cause) {
return fail(500, { message: "Interner Fehler, versuche es erneut." })
}
switch (result_body.cause) {
case RegisterResponseCause.MalformedRequest: return fail(400, { message: "Bitte versuche es erneut." })
case RegisterResponseCause.EmailLength: return fail(400, { message: "Bitte gib eine Email ein." })
case RegisterResponseCause.EmailFormat: return fail(400, { message: "Das Format der Email ist nicht gültig." })
case RegisterResponseCause.EmailDuplicate: return fail(409, { message: "Email wird bereits verwendet." })
case RegisterResponseCause.DisplayNameLength: return fail(400, { message: "Bitt gib einen Display Namen ein" })
case RegisterResponseCause.PasswordLength: return fail(400, { message: "Bitte gib ein Passwort ein." })
default:
return fail(500, { message: "Ein interner Fehler ist aufgetreten. Bitte versuche es erneut."})
}
}
return { message: "Erfolg" }
}
} satisfies Actions

View File

@ -0,0 +1,69 @@
<script lang="ts">
import type { PageProps } from "./$types"
const { form }: PageProps = $props()
</script>
<form action="?/register" method="POST" id="form_register"></form>
<div class="container">
<div class="grid">
<h1>Login</h1>
<p class="error_msg">{form?.message}</p>
<label for="username">E-Mail: </label><input type="text" id="username" form="form_register" name="email" />
<label for="password">Passwort: </label><input type="password" id="password" form="form_register" name="password" />
<label for="display_name">Anzeigename: </label><input type="text" id="display_name" form="form_register" name="display_name" />
<button type="submit" form="form_register">Registrieren</button>
</div>
<p><a href="/login">Schon registriert? Klicke hier!</a></p>
</div>
<style>
.container {
display: flex;
width: 100%;
height: 100%;
padding-top: 100px;
flex-direction: column;
justify-content: center;
align-items: center;
}
.grid {
display: grid;
gap: 10px 50px;
grid-template-columns: 1fr 3fr;
justify-content: space-between;
}
.grid > h1 {
width: 100%;
text-align: center;
grid-column: 1 / span 2;
}
.grid > p {
width: 100%;
grid-column: 1 / span 2;
}
.grid > button {
width: 50%;
margin-top: 10px;
margin-left: auto;
grid-column: 2;
}
.error_msg {
color: red;
}
</style>

View File

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

13
static/global.css Normal file
View File

@ -0,0 +1,13 @@
body {
--primary-bg-color: darkgreen;
--primary-text-color: black;
width: 100%;
background-color: var(--primary-bg-color, white);
}
* {
color: var(--primary-text-color, black);
}