diff --git a/.env.example b/.env.example index e69de29..73217c6 100644 --- a/.env.example +++ b/.env.example @@ -0,0 +1,13 @@ +DATABASE_URL="./data/db.sqlite" + +# Admin user is seeded +# Generate id With `bun -e "console.log(Bun.randomUUIDv7('base64url'))";` +ADMIN_USER_ID="some unique id" +ADMIN_USER_EMAIL="admin" +ADMIN_USER_PASSWORD="password" + +#SESSION_DURATION_IN_DAYS=31 + +BEARER_TOKEN="Token" +API_SERVICE_URL="http://localhost:3000" +#API_SERVICE_URL="https://pt-api.skaarup.dev" diff --git a/.gitignore b/.gitignore index f1c7090..a7750ce 100644 --- a/.gitignore +++ b/.gitignore @@ -34,3 +34,6 @@ report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json .DS_Store build + +data/* +!data/.gitkeep diff --git a/bun.lock b/bun.lock index 6b260f5..e26253c 100644 --- a/bun.lock +++ b/bun.lock @@ -6,24 +6,78 @@ "dependencies": { "bun-plugin-svelte": "^0.0.6", "bun-plugin-tailwind": "^0.0.15", - "svelte": "^5.25.10", + "drizzle-orm": "^0.41.0", + "svelte": "^5.26.1", "tailwindcss": "^4.1.3", }, "devDependencies": { "@types/bun": "latest", + "drizzle-kit": "^0.30.6", "oxlint": "latest", "prettier": "^4.0.0-alpha.12", "prettier-plugin-svelte": "^3.3.3", "prettier-plugin-tailwindcss": "^0.6.11", }, "peerDependencies": { - "typescript": "^5.8.2", + "typescript": "^5.8.3", }, }, }, "packages": { "@ampproject/remapping": ["@ampproject/remapping@2.3.0", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw=="], + "@drizzle-team/brocli": ["@drizzle-team/brocli@0.10.2", "", {}, "sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w=="], + + "@esbuild-kit/core-utils": ["@esbuild-kit/core-utils@3.3.2", "", { "dependencies": { "esbuild": "~0.18.20", "source-map-support": "^0.5.21" } }, "sha512-sPRAnw9CdSsRmEtnsl2WXWdyquogVpB3yZ3dgwJfe8zrOzTsV7cJvmwrKVa+0ma5BoiGJ+BoqkMvawbayKUsqQ=="], + + "@esbuild-kit/esm-loader": ["@esbuild-kit/esm-loader@2.6.5", "", { "dependencies": { "@esbuild-kit/core-utils": "^3.3.2", "get-tsconfig": "^4.7.0" } }, "sha512-FxEMIkJKnodyA1OaCUoEvbYRkoZlLZ4d/eXFu9Fh8CbBBgP5EmZxrfTRyN0qpXZ4vOvqnE5YdRdcrmUUXuU+dA=="], + + "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.19.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA=="], + + "@esbuild/android-arm": ["@esbuild/android-arm@0.19.12", "", { "os": "android", "cpu": "arm" }, "sha512-qg/Lj1mu3CdQlDEEiWrlC4eaPZ1KztwGJ9B6J+/6G+/4ewxJg7gqj8eVYWvao1bXrqGiW2rsBZFSX3q2lcW05w=="], + + "@esbuild/android-arm64": ["@esbuild/android-arm64@0.19.12", "", { "os": "android", "cpu": "arm64" }, "sha512-P0UVNGIienjZv3f5zq0DP3Nt2IE/3plFzuaS96vihvD0Hd6H/q4WXUGpCxD/E8YrSXfNyRPbpTq+T8ZQioSuPA=="], + + "@esbuild/android-x64": ["@esbuild/android-x64@0.19.12", "", { "os": "android", "cpu": "x64" }, "sha512-3k7ZoUW6Q6YqhdhIaq/WZ7HwBpnFBlW905Fa4s4qWJyiNOgT1dOqDiVAQFwBH7gBRZr17gLrlFCRzF6jFh7Kew=="], + + "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.19.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-B6IeSgZgtEzGC42jsI+YYu9Z3HKRxp8ZT3cqhvliEHovq8HSX2YX8lNocDn79gCKJXOSaEot9MVYky7AKjCs8g=="], + + "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.19.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-hKoVkKzFiToTgn+41qGhsUJXFlIjxI/jSYeZf3ugemDYZldIXIxhvwN6erJGlX4t5h417iFuheZ7l+YVn05N3A=="], + + "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.19.12", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-4aRvFIXmwAcDBw9AueDQ2YnGmz5L6obe5kmPT8Vd+/+x/JMVKCgdcRwH6APrbpNXsPz+K653Qg8HB/oXvXVukA=="], + + "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.19.12", "", { "os": "freebsd", "cpu": "x64" }, "sha512-EYoXZ4d8xtBoVN7CEwWY2IN4ho76xjYXqSXMNccFSx2lgqOG/1TBPW0yPx1bJZk94qu3tX0fycJeeQsKovA8gg=="], + + "@esbuild/linux-arm": ["@esbuild/linux-arm@0.19.12", "", { "os": "linux", "cpu": "arm" }, "sha512-J5jPms//KhSNv+LO1S1TX1UWp1ucM6N6XuL6ITdKWElCu8wXP72l9MM0zDTzzeikVyqFE6U8YAV9/tFyj0ti+w=="], + + "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.19.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-EoTjyYyLuVPfdPLsGVVVC8a0p1BFFvtpQDB/YLEhaXyf/5bczaGeN15QkR+O4S5LeJ92Tqotve7i1jn35qwvdA=="], + + "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.19.12", "", { "os": "linux", "cpu": "ia32" }, "sha512-Thsa42rrP1+UIGaWz47uydHSBOgTUnwBwNq59khgIwktK6x60Hivfbux9iNR0eHCHzOLjLMLfUMLCypBkZXMHA=="], + + "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.19.12", "", { "os": "linux", "cpu": "none" }, "sha512-LiXdXA0s3IqRRjm6rV6XaWATScKAXjI4R4LoDlvO7+yQqFdlr1Bax62sRwkVvRIrwXxvtYEHHI4dm50jAXkuAA=="], + + "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.19.12", "", { "os": "linux", "cpu": "none" }, "sha512-fEnAuj5VGTanfJ07ff0gOA6IPsvrVHLVb6Lyd1g2/ed67oU1eFzL0r9WL7ZzscD+/N6i3dWumGE1Un4f7Amf+w=="], + + "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.19.12", "", { "os": "linux", "cpu": "ppc64" }, "sha512-nYJA2/QPimDQOh1rKWedNOe3Gfc8PabU7HT3iXWtNUbRzXS9+vgB0Fjaqr//XNbd82mCxHzik2qotuI89cfixg=="], + + "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.19.12", "", { "os": "linux", "cpu": "none" }, "sha512-2MueBrlPQCw5dVJJpQdUYgeqIzDQgw3QtiAHUC4RBz9FXPrskyyU3VI1hw7C0BSKB9OduwSJ79FTCqtGMWqJHg=="], + + "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.19.12", "", { "os": "linux", "cpu": "s390x" }, "sha512-+Pil1Nv3Umes4m3AZKqA2anfhJiVmNCYkPchwFJNEJN5QxmTs1uzyy4TvmDrCRNT2ApwSari7ZIgrPeUx4UZDg=="], + + "@esbuild/linux-x64": ["@esbuild/linux-x64@0.19.12", "", { "os": "linux", "cpu": "x64" }, "sha512-B71g1QpxfwBvNrfyJdVDexenDIt1CiDN1TIXLbhOw0KhJzE78KIFGX6OJ9MrtC0oOqMWf+0xop4qEU8JrJTwCg=="], + + "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.19.12", "", { "os": "none", "cpu": "x64" }, "sha512-3ltjQ7n1owJgFbuC61Oj++XhtzmymoCihNFgT84UAmJnxJfm4sYCiSLTXZtE00VWYpPMYc+ZQmB6xbSdVh0JWA=="], + + "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.19.12", "", { "os": "openbsd", "cpu": "x64" }, "sha512-RbrfTB9SWsr0kWmb9srfF+L933uMDdu9BIzdA7os2t0TXhCRjrQyCeOt6wVxr79CKD4c+p+YhCj31HBkYcXebw=="], + + "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.19.12", "", { "os": "sunos", "cpu": "x64" }, "sha512-HKjJwRrW8uWtCQnQOz9qcU3mUZhTUQvi56Q8DPTLLB+DawoiQdjsYq+j+D3s9I8VFtDr+F9CjgXKKC4ss89IeA=="], + + "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.19.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-URgtR1dJnmGvX864pn1B2YUYNzjmXkuJOIqG2HdU62MVS4EHpU2946OZoTMnRUHklGtJdJZ33QfzdjGACXhn1A=="], + + "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.19.12", "", { "os": "win32", "cpu": "ia32" }, "sha512-+ZOE6pUkMOJfmxmBZElNOx72NKpIa/HFOMGzu8fqzQJ5kgf6aTGrcJaFsNiVMH4JKpMipyK+7k0n2UXN7a8YKQ=="], + + "@esbuild/win32-x64": ["@esbuild/win32-x64@0.19.12", "", { "os": "win32", "cpu": "x64" }, "sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA=="], + "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.8", "", { "dependencies": { "@jridgewell/set-array": "^1.2.1", "@jridgewell/sourcemap-codec": "^1.4.10", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA=="], "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="], @@ -50,6 +104,8 @@ "@oxlint/win32-x64": ["@oxlint/win32-x64@0.16.5", "", { "os": "win32", "cpu": "x64" }, "sha512-cHJJRyVA2XlsGjIVKqw2DC5dkzWGOH6gxQwf6StTHn8F4i5P8gksV70VoNW5mwEXefF2USDX7H43YVIDG5E/Yw=="], + "@petamoriken/float16": ["@petamoriken/float16@3.9.2", "", {}, "sha512-VgffxawQde93xKxT3qap3OH+meZf7VaSB5Sqd4Rqc+FP5alWbpOyan/7tRbOAvynjpG3GpdtAuGU/NdhQpmrog=="], + "@prettier/cli": ["@prettier/cli@0.7.1", "", { "dependencies": { "atomically": "^2.0.3", "fast-ignore": "^1.1.3", "find-up-json": "^2.0.4", "function-once": "^3.0.0", "import-meta-resolve": "^4.1.0", "is-binary-path": "^2.1.0", "js-yaml": "^4.1.0", "json-sorted-stringify": "^1.0.0", "json5": "^2.2.3", "kasi": "^1.1.0", "lomemo": "^1.0.0", "pioppo": "^1.2.0", "promise-resolve-timeout": "^2.0.0", "smol-toml": "^1.3.1", "specialist": "^1.4.5", "tiny-editorconfig": "^1.0.0", "tiny-jsonc": "^1.0.1", "tiny-readdir": "^2.7.4", "tiny-readdir-glob": "^1.23.1", "tiny-spinner": "^2.0.4", "worktank": "^2.7.3", "zeptomatch": "^2.0.0", "zeptomatch-escape": "^1.0.0", "zeptomatch-is-static": "^1.0.0" }, "peerDependencies": { "prettier": "^3.1.0 || ^4.0.0-alpha" }, "bin": { "prettier-next": "dist/bin.js" } }, "sha512-YoXPLOLmEEHP4MKgzcEilzaUtlo80Qm5Pb+59QbgDeOsIExGBkRqZmC2+iwzSwEhlhTEpGqDwqb3+nP/dPay9A=="], "@sveltejs/acorn-typescript": ["@sveltejs/acorn-typescript@1.0.5", "", { "peerDependencies": { "acorn": "^8.9.0" } }, "sha512-IwQk4yfwLdibDlrXVE04jTZYlLnwsTT2PIOQQGNLWfjavGifnk1JD1LcZjZaBTRcxZu2FfPfNLOE04DSu9lqtQ=="], @@ -58,9 +114,9 @@ "@types/estree": ["@types/estree@1.0.7", "", {}, "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ=="], - "@types/node": ["@types/node@22.13.14", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-Zs/Ollc1SJ8nKUAgc7ivOEdIBM8JAKgrqqUYi2J997JuKO7/tpQC+WCetQ1sypiKCQWHdvdg9wBNpUPEWZae7w=="], + "@types/node": ["@types/node@22.14.1", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-u0HuPQwe/dHrItgHHpmw3N2fYCR6x4ivMNbPHRkBVP4CvN+kiRrKHWk3i8tXiO/joPwXLMYvF9TTF0eqgHIuOw=="], - "@types/ws": ["@types/ws@8.18.0", "", { "dependencies": { "@types/node": "*" } }, "sha512-8svvI3hMyvN0kKCJMvTJP/x6Y/EoQbepff882wL+Sn5QsXb3etnamgrJq4isrBxSJj5L2AuXcI0+bgkoAXGUJw=="], + "@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="], "acorn": ["acorn@8.14.1", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg=="], @@ -78,6 +134,8 @@ "binary-extensions": ["binary-extensions@2.3.0", "", {}, "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw=="], + "buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="], + "bun-plugin-svelte": ["bun-plugin-svelte@0.0.6", "", { "peerDependencies": { "svelte": "^5" } }, "sha512-HuEDvOieVwXvhpcHLcASeQIOVgje2GRO3Tu0ypJh3MkjGLkEBOQ1+6FWsMgb54FxfbPw9JP3q0WlBd8SjgrnGQ=="], "bun-plugin-tailwind": ["bun-plugin-tailwind@0.0.15", "", { "peerDependencies": { "typescript": "^5.0.0" } }, "sha512-qtAXMNGG4R0UGGI8zWrqm2B7BdXqx48vunJXBPzfDOHPA5WkRUZdTSbE7TFwO4jLhYqSE23YMWsM9NhE6ovobw=="], @@ -86,8 +144,20 @@ "clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="], + "debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="], + "dettle": ["dettle@1.0.5", "", {}, "sha512-ZVyjhAJ7sCe1PNXEGveObOH9AC8QvMga3HJIghHawtG7mE4K5pW9nz/vDGAr/U7a3LWgdOzEE7ac9MURnyfaTA=="], + "drizzle-kit": ["drizzle-kit@0.30.6", "", { "dependencies": { "@drizzle-team/brocli": "^0.10.2", "@esbuild-kit/esm-loader": "^2.5.5", "esbuild": "^0.19.7", "esbuild-register": "^3.5.0", "gel": "^2.0.0" }, "bin": { "drizzle-kit": "bin.cjs" } }, "sha512-U4wWit0fyZuGuP7iNmRleQyK2V8wCuv57vf5l3MnG4z4fzNTjY/U13M8owyQ5RavqvqxBifWORaR3wIUzlN64g=="], + + "drizzle-orm": ["drizzle-orm@0.41.0", "", { "peerDependencies": { "@aws-sdk/client-rds-data": ">=3", "@cloudflare/workers-types": ">=4", "@electric-sql/pglite": ">=0.2.0", "@libsql/client": ">=0.10.0", "@libsql/client-wasm": ">=0.10.0", "@neondatabase/serverless": ">=0.10.0", "@op-engineering/op-sqlite": ">=2", "@opentelemetry/api": "^1.4.1", "@planetscale/database": ">=1", "@prisma/client": "*", "@tidbcloud/serverless": "*", "@types/better-sqlite3": "*", "@types/pg": "*", "@types/sql.js": "*", "@vercel/postgres": ">=0.8.0", "@xata.io/client": "*", "better-sqlite3": ">=7", "bun-types": "*", "expo-sqlite": ">=14.0.0", "gel": ">=2", "knex": "*", "kysely": "*", "mysql2": ">=2", "pg": ">=8", "postgres": ">=3", "sql.js": ">=1", "sqlite3": ">=5" }, "optionalPeers": ["@aws-sdk/client-rds-data", "@cloudflare/workers-types", "@electric-sql/pglite", "@libsql/client", "@libsql/client-wasm", "@neondatabase/serverless", "@op-engineering/op-sqlite", "@opentelemetry/api", "@planetscale/database", "@prisma/client", "@tidbcloud/serverless", "@types/better-sqlite3", "@types/pg", "@types/sql.js", "@vercel/postgres", "@xata.io/client", "better-sqlite3", "bun-types", "expo-sqlite", "gel", "knex", "kysely", "mysql2", "pg", "postgres", "sql.js", "sqlite3"] }, "sha512-7A4ZxhHk9gdlXmTdPj/lREtP+3u8KvZ4yEN6MYVxBzZGex5Wtdc+CWSbu7btgF6TB0N+MNPrvW7RKBbxJchs/Q=="], + + "env-paths": ["env-paths@3.0.0", "", {}, "sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A=="], + + "esbuild": ["esbuild@0.19.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.19.12", "@esbuild/android-arm": "0.19.12", "@esbuild/android-arm64": "0.19.12", "@esbuild/android-x64": "0.19.12", "@esbuild/darwin-arm64": "0.19.12", "@esbuild/darwin-x64": "0.19.12", "@esbuild/freebsd-arm64": "0.19.12", "@esbuild/freebsd-x64": "0.19.12", "@esbuild/linux-arm": "0.19.12", "@esbuild/linux-arm64": "0.19.12", "@esbuild/linux-ia32": "0.19.12", "@esbuild/linux-loong64": "0.19.12", "@esbuild/linux-mips64el": "0.19.12", "@esbuild/linux-ppc64": "0.19.12", "@esbuild/linux-riscv64": "0.19.12", "@esbuild/linux-s390x": "0.19.12", "@esbuild/linux-x64": "0.19.12", "@esbuild/netbsd-x64": "0.19.12", "@esbuild/openbsd-x64": "0.19.12", "@esbuild/sunos-x64": "0.19.12", "@esbuild/win32-arm64": "0.19.12", "@esbuild/win32-ia32": "0.19.12", "@esbuild/win32-x64": "0.19.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-aARqgq8roFBj054KvQr5f1sFu0D65G+miZRCuJyJ0G13Zwx7vRar5Zhn2tkQNzIXcBrNVsv/8stehpj+GAjgbg=="], + + "esbuild-register": ["esbuild-register@3.6.0", "", { "dependencies": { "debug": "^4.3.4" }, "peerDependencies": { "esbuild": ">=0.12 <1" } }, "sha512-H2/S7Pm8a9CL1uhp9OvjwrBh5Pvx0H8qVOxNu8Wed9Y7qv56MPtq+GGM8RJpq6glYJn9Wspr8uw7l55uyinNeg=="], + "esm-env": ["esm-env@1.2.2", "", {}, "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA=="], "esrap": ["esrap@1.4.6", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.4.15" } }, "sha512-F/D2mADJ9SHY3IwksD4DAXjTt7qt7GWUf3/8RhCNWmC/67tyb55dpimHmy7EplakFaflV0R/PC+fdSPqrRHAQw=="], @@ -104,8 +174,12 @@ "function-once": ["function-once@3.0.1", "", {}, "sha512-bE3E8REk4jANDot3l0sLFkXgywBwzFKsmbwdnVHLJUnt/3kV6dNG0oJJqoRBuS1Z9Lr4ZoQgwV0ZNLDgWDbv7Q=="], + "gel": ["gel@2.0.2", "", { "dependencies": { "@petamoriken/float16": "^3.8.7", "debug": "^4.3.4", "env-paths": "^3.0.0", "semver": "^7.6.2", "shell-quote": "^1.8.1", "which": "^4.0.0" }, "bin": { "gel": "dist/cli.mjs" } }, "sha512-XTKpfNR9HZOw+k0Bl04nETZjuP5pypVAXsZADSdwr3EtyygTTe1RqvftU2FjGu7Tp9e576a9b/iIOxWrRBxMiQ=="], + "get-current-package": ["get-current-package@1.0.1", "", { "dependencies": { "find-up-json": "^2.0.5" } }, "sha512-c/Rw5ByDQ+zg+Lh/emBWv0bDpugEFdmXPR6/srIemVtIvol0XbT0JAr8Db0cX+Jj/xY9wj1wdjeq2qNB35Tayg=="], + "get-tsconfig": ["get-tsconfig@4.10.0", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-kGzZ3LWWQcGIAmg6iWvXn0ei6WDtV26wzHRMwDSzmAbcXrTEXxHy6IehI6/4eT6VRKyMP1eF1VqwrVUmE/LR7A=="], + "grammex": ["grammex@3.1.10", "", {}, "sha512-UCfMsV/sfqk4TN1+m5ehSOXuADyLUgSuwMI2vCVlbN/REoSmTl4eagswC9DzzVxtsKv7Yp2CmIJNn4fMk8PaQA=="], "import-meta-resolve": ["import-meta-resolve@4.1.0", "", {}, "sha512-I6fiaX09Xivtk+THaMfAwnA3MVA5Big1WHF1Dfx9hFuvNIWpXnorlkzhcQf6ehrqQiiZECRt1poOAkPmer3ruw=="], @@ -118,6 +192,8 @@ "is-reference": ["is-reference@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.6" } }, "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw=="], + "isexe": ["isexe@3.1.1", "", {}, "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ=="], + "js-yaml": ["js-yaml@4.1.0", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA=="], "json-sorted-stringify": ["json-sorted-stringify@1.0.1", "", {}, "sha512-pWv9hqWho37EpwpBgqDYVPKPCgT/ytuvqtlBvb6M44BrnvooTk/5D/aSeohsGDLp+g8waP5dUUGODR+Ley+Idg=="], @@ -132,6 +208,8 @@ "magic-string": ["magic-string@0.30.17", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0" } }, "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA=="], + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + "oxlint": ["oxlint@0.16.5", "", { "optionalDependencies": { "@oxlint/darwin-arm64": "0.16.5", "@oxlint/darwin-x64": "0.16.5", "@oxlint/linux-arm64-gnu": "0.16.5", "@oxlint/linux-arm64-musl": "0.16.5", "@oxlint/linux-x64-gnu": "0.16.5", "@oxlint/linux-x64-musl": "0.16.5", "@oxlint/win32-arm64": "0.16.5", "@oxlint/win32-x64": "0.16.5" }, "bin": { "oxlint": "bin/oxlint", "oxc_language_server": "bin/oxc_language_server" } }, "sha512-z5MX2v4KUqzZQTnRkHHBPE4qEo08f5mJ4dQxi+r5t3xpGspVH/pBzWQ0UUy2xO3JHO8H6wpoeoh8bGs6jEygvQ=="], "pioppo": ["pioppo@1.2.1", "", { "dependencies": { "dettle": "^1.0.5", "when-exit": "^2.1.4" } }, "sha512-1oErGVWD6wFDPmrJWEY1Cj2p829UGT6Fw9OItYFxLkWtBjCvQSMC8wA5IcAR5ms/6gqiY8pnJvIV/+/Imyobew=="], @@ -148,8 +226,18 @@ "promise-resolve-timeout": ["promise-resolve-timeout@2.0.1", "", {}, "sha512-90Qzzu5SmR+ksmTPsc79121NZGtEiPvKACQLCl6yofknRx5xJI9kNj3oDVSX6dVTneF8Ju6+xpVFdDSzb7cNcg=="], + "resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="], + + "semver": ["semver@7.7.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA=="], + + "shell-quote": ["shell-quote@1.8.2", "", {}, "sha512-AzqKpGKjrj7EM6rKVQEPpB288oCfnrEIuyoT9cyF4nmGa7V8Zk6f7RRqYisX8X9m+Q7bd632aZW4ky7EhbQztA=="], + "smol-toml": ["smol-toml@1.3.1", "", {}, "sha512-tEYNll18pPKHroYSmLLrksq233j021G0giwW7P3D24jC54pQ5W5BXMsQ/Mvw1OJCmEYDgY+lrzT+3nNUtoNfXQ=="], + "source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], + + "source-map-support": ["source-map-support@0.5.21", "", { "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" } }, "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w=="], + "specialist": ["specialist@1.4.5", "", { "dependencies": { "tiny-bin": "^1.10.3", "tiny-colors": "^2.2.2", "tiny-parse-argv": "^2.8.1", "tiny-updater": "^3.5.3" } }, "sha512-4mPQEREzBUW2hzlXX/dWFbQdUWzpkqvMFVpUAdRlo1lUlhKMObDHiAo09oZ94x4cS3uWMJebPOTn+GaQYLfv3Q=="], "stdin-blocker": ["stdin-blocker@2.0.1", "", {}, "sha512-NEcAEpag+gE/Iivx1prq1AFPwnmgmcyHNvGZLUqGBoOE/7DZtmhtP9iYqJt8ymueFL+kknhfEebAMWbrWp3FJw=="], @@ -186,14 +274,16 @@ "tiny-updater": ["tiny-updater@3.5.3", "", { "dependencies": { "ionstore": "^1.0.1", "tiny-colors": "^2.2.2", "when-exit": "^2.1.4" } }, "sha512-wEUssfOOkVLg2raSaRbyZDHpVCDj6fnp7UjynpNE4XGuF+Gkj8GRRMoHdfk73VzLQs/AHKsbY8fCxXNz8Hx4Qg=="], - "typescript": ["typescript@5.8.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ=="], + "typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="], - "undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="], + "undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], "webworker-shim": ["webworker-shim@1.1.1", "", {}, "sha512-XCWuBjJH3Xn/7SbyUF1WrrCbe6ZEsgaD7kxlFhxIwdkljGYX3BqP/dhG6ge0NBT+V7ZPjR4/BXq5BvbdaxrpKg=="], "when-exit": ["when-exit@2.1.4", "", {}, "sha512-4rnvd3A1t16PWzrBUcSDZqcAmsUIy4minDXT/CZ8F2mVDgd65i4Aalimgz1aQkRGU0iH5eT5+6Rx2TK8o443Pg=="], + "which": ["which@4.0.0", "", { "dependencies": { "isexe": "^3.1.1" }, "bin": { "node-which": "bin/which.js" } }, "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg=="], + "worktank": ["worktank@2.7.3", "", { "dependencies": { "promise-make-naked": "^2.0.0", "webworker-shim": "^1.1.0" } }, "sha512-M0fesnpttBPdvNYBdzRvLDsacN0na9RYWFxwmM/x1+/6mufjduv9/9vBObK8EXDqxRMX/SOYJabpo0UCYYBUdQ=="], "zeptomatch": ["zeptomatch@2.0.1", "", { "dependencies": { "grammex": "^3.1.10" } }, "sha512-nbnIYF2n3o3EqV36HkIhEMLIDFbG3M6RUjhkdKIn6qqIJkdkL7bgVSfTTCEXBJpk1T45tLfEYfStndJc2lUEnA=="], @@ -208,10 +298,56 @@ "zimmerframe": ["zimmerframe@1.1.2", "", {}, "sha512-rAbqEGa8ovJy4pyBxZM70hg4pE6gDgaQ0Sl9M3enG3I0d6H4XSAM3GeNGLKnsBpuijUow064sf7ww1nutC5/3w=="], + "@esbuild-kit/core-utils/esbuild": ["esbuild@0.18.20", "", { "optionalDependencies": { "@esbuild/android-arm": "0.18.20", "@esbuild/android-arm64": "0.18.20", "@esbuild/android-x64": "0.18.20", "@esbuild/darwin-arm64": "0.18.20", "@esbuild/darwin-x64": "0.18.20", "@esbuild/freebsd-arm64": "0.18.20", "@esbuild/freebsd-x64": "0.18.20", "@esbuild/linux-arm": "0.18.20", "@esbuild/linux-arm64": "0.18.20", "@esbuild/linux-ia32": "0.18.20", "@esbuild/linux-loong64": "0.18.20", "@esbuild/linux-mips64el": "0.18.20", "@esbuild/linux-ppc64": "0.18.20", "@esbuild/linux-riscv64": "0.18.20", "@esbuild/linux-s390x": "0.18.20", "@esbuild/linux-x64": "0.18.20", "@esbuild/netbsd-x64": "0.18.20", "@esbuild/openbsd-x64": "0.18.20", "@esbuild/sunos-x64": "0.18.20", "@esbuild/win32-arm64": "0.18.20", "@esbuild/win32-ia32": "0.18.20", "@esbuild/win32-x64": "0.18.20" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA=="], + "promise-make-counter/promise-make-naked": ["promise-make-naked@3.0.2", "", {}, "sha512-B+b+kQ1YrYS7zO7P7bQcoqqMUizP06BOyNSBEnB5VJKDSWo8fsVuDkfSmwdjF0JsRtaNh83so5MMFJ95soH5jg=="], "tiny-editorconfig/zeptomatch": ["zeptomatch@1.2.2", "", { "dependencies": { "grammex": "^3.1.1" } }, "sha512-0ETdzEO0hdYmT8aXHHf5aMjpX+FHFE61sG4qKFAoJD2Umt3TWdCmH7ADxn2oUiWTlqBGC+SGr8sYMfr+37J8pQ=="], "tiny-readdir-glob/zeptomatch": ["zeptomatch@1.2.2", "", { "dependencies": { "grammex": "^3.1.1" } }, "sha512-0ETdzEO0hdYmT8aXHHf5aMjpX+FHFE61sG4qKFAoJD2Umt3TWdCmH7ADxn2oUiWTlqBGC+SGr8sYMfr+37J8pQ=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.18.20", "", { "os": "android", "cpu": "arm" }, "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.18.20", "", { "os": "android", "cpu": "arm64" }, "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.18.20", "", { "os": "android", "cpu": "x64" }, "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.18.20", "", { "os": "darwin", "cpu": "arm64" }, "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.18.20", "", { "os": "darwin", "cpu": "x64" }, "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.18.20", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.18.20", "", { "os": "freebsd", "cpu": "x64" }, "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.18.20", "", { "os": "linux", "cpu": "arm" }, "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.18.20", "", { "os": "linux", "cpu": "arm64" }, "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.18.20", "", { "os": "linux", "cpu": "ia32" }, "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.18.20", "", { "os": "linux", "cpu": "none" }, "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.18.20", "", { "os": "linux", "cpu": "none" }, "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.18.20", "", { "os": "linux", "cpu": "ppc64" }, "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.18.20", "", { "os": "linux", "cpu": "none" }, "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.18.20", "", { "os": "linux", "cpu": "s390x" }, "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.18.20", "", { "os": "linux", "cpu": "x64" }, "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.18.20", "", { "os": "none", "cpu": "x64" }, "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.18.20", "", { "os": "openbsd", "cpu": "x64" }, "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.18.20", "", { "os": "sunos", "cpu": "x64" }, "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.18.20", "", { "os": "win32", "cpu": "arm64" }, "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.18.20", "", { "os": "win32", "cpu": "ia32" }, "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.18.20", "", { "os": "win32", "cpu": "x64" }, "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ=="], } } diff --git a/data/.gitkeep b/data/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/drizzle.config.ts b/drizzle.config.ts new file mode 100644 index 0000000..73f5315 --- /dev/null +++ b/drizzle.config.ts @@ -0,0 +1,14 @@ +import { env } from "bun"; +import type { Config } from "drizzle-kit"; + +const url = process.env.DATABASE_URL; +if (!url) { + throw new Error('DATABASE_URL is not set'); +} + +export default { + schema: 'src/server/db/schema.ts', + out: './drizzle', + dialect: 'sqlite', + dbCredentials: { url } +} satisfies Config; diff --git a/drizzle/0000_gigantic_namorita.sql b/drizzle/0000_gigantic_namorita.sql new file mode 100644 index 0000000..fcfaa5f --- /dev/null +++ b/drizzle/0000_gigantic_namorita.sql @@ -0,0 +1,20 @@ +CREATE TABLE `user_sessions` ( + `token` text PRIMARY KEY NOT NULL, + `user_id` text NOT NULL, + `max_age` integer NOT NULL, + `created_at` integer DEFAULT (unixepoch('subsec') * 1000) NOT NULL, + `updated_at` integer DEFAULT (unixepoch('subsec') * 1000) NOT NULL, + FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE cascade +); +--> statement-breakpoint +CREATE UNIQUE INDEX `user_sessions_token_unique` ON `user_sessions` (`token`);--> statement-breakpoint +CREATE TABLE `users` ( + `id` text PRIMARY KEY NOT NULL, + `email` text NOT NULL, + `name` text NOT NULL, + `hash` text NOT NULL, + `created_at` integer DEFAULT (unixepoch('subsec') * 1000) NOT NULL, + `updated_at` integer DEFAULT (unixepoch('subsec') * 1000) NOT NULL +); +--> statement-breakpoint +CREATE UNIQUE INDEX `users_email_unique` ON `users` (`email`); \ No newline at end of file diff --git a/drizzle/meta/0000_snapshot.json b/drizzle/meta/0000_snapshot.json new file mode 100644 index 0000000..b29dfff --- /dev/null +++ b/drizzle/meta/0000_snapshot.json @@ -0,0 +1,149 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "d689db90-3590-437d-a697-5a073797bd45", + "prevId": "00000000-0000-0000-0000-000000000000", + "tables": { + "user_sessions": { + "name": "user_sessions", + "columns": { + "token": { + "name": "token", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "max_age": { + "name": "max_age", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch('subsec') * 1000)" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch('subsec') * 1000)" + } + }, + "indexes": { + "user_sessions_token_unique": { + "name": "user_sessions_token_unique", + "columns": [ + "token" + ], + "isUnique": true + } + }, + "foreignKeys": { + "user_sessions_user_id_users_id_fk": { + "name": "user_sessions_user_id_users_id_fk", + "tableFrom": "user_sessions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "users": { + "name": "users", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "hash": { + "name": "hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch('subsec') * 1000)" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch('subsec') * 1000)" + } + }, + "indexes": { + "users_email_unique": { + "name": "users_email_unique", + "columns": [ + "email" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} \ No newline at end of file diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json new file mode 100644 index 0000000..b2501ac --- /dev/null +++ b/drizzle/meta/_journal.json @@ -0,0 +1,13 @@ +{ + "version": "7", + "dialect": "sqlite", + "entries": [ + { + "idx": 0, + "version": "6", + "when": 1744418681004, + "tag": "0000_gigantic_namorita", + "breakpoints": true + } + ] +} \ No newline at end of file diff --git a/package.json b/package.json index 4c014ce..ee2cb93 100644 --- a/package.json +++ b/package.json @@ -1,18 +1,23 @@ { "name": "progress-tracker-v3", - "module": "src/server.ts", + "module": "src/index.ts", "type": "module", "private": true, "scripts": { - "start": "bun src/server.ts", - "dev": "bun --watch run src/server.ts", - "hot": "bun --hot run src/server.ts", - "build": "bun build src/server.ts --compile --minify --sourcemap --target=bun-linux-x64-modern --outfile server", + "start": "bun src/index.ts", + "dev": "bun --watch run src/index.ts", + "hot": "bun --hot run src/index.ts", + "build": "bun build src/index.ts --compile --minify --sourcemap --target=bun-linux-x64-modern --outfile dist/app", "lint": "oxlint .", - "format": "prettier --write ." + "format": "prettier --write .", + "migrate": "bun --bun run ./src/server/db/migrate.ts", + "generate": "bunx --bun drizzle-kit generate --config=./drizzle.config.ts", + "studio": "bunx --bun drizzle-kit studio --config=./drizzle.config.ts", + "up": "drizzle-kit up --config=./drizzle.config.ts" }, "devDependencies": { "@types/bun": "latest", + "drizzle-kit": "^0.30.6", "oxlint": "latest", "prettier": "^4.0.0-alpha.12", "prettier-plugin-svelte": "^3.3.3", @@ -21,10 +26,11 @@ "dependencies": { "bun-plugin-svelte": "^0.0.6", "bun-plugin-tailwind": "^0.0.15", + "drizzle-orm": "^0.41.0", "svelte": "^5.26.1", "tailwindcss": "^4.1.3" }, "peerDependencies": { - "typescript": "^5.8.2" + "typescript": "^5.8.3" } } diff --git a/src/components/header.svelte b/src/client/components/header.svelte similarity index 91% rename from src/components/header.svelte rename to src/client/components/header.svelte index 7134e76..f664827 100644 --- a/src/components/header.svelte +++ b/src/client/components/header.svelte @@ -1,3 +1,10 @@ + + Skip to main content + +
diff --git a/src/components/input-email.svelte b/src/client/components/input-email.svelte similarity index 100% rename from src/components/input-email.svelte rename to src/client/components/input-email.svelte diff --git a/src/components/input-password.svelte b/src/client/components/input-password.svelte similarity index 100% rename from src/components/input-password.svelte rename to src/client/components/input-password.svelte diff --git a/src/components/login.svelte b/src/client/components/login.svelte similarity index 86% rename from src/components/login.svelte rename to src/client/components/login.svelte index 5c171a7..9cd40c1 100644 --- a/src/components/login.svelte +++ b/src/client/components/login.svelte @@ -1,12 +1,11 @@ - diff --git a/src/index.svelte b/src/client/index.svelte similarity index 54% rename from src/index.svelte rename to src/client/index.svelte index 2e87377..b07b08b 100644 --- a/src/index.svelte +++ b/src/client/index.svelte @@ -5,20 +5,18 @@ import ProgressTable from './components/progress-table.svelte'; import './index.css'; import { userstate } from './shared.svelte'; + let promise = userstate.checkIsLoggedIn(); - - Skip to main content - -
- {#if !userstate.isLoggedIn} - - {:else} - - {/if} + {#await promise} +
loading
+ {:then _} + {#if !userstate.isLoggedIn} + + {:else} + + {/if} + {/await}
diff --git a/src/client/index.ts b/src/client/index.ts new file mode 100644 index 0000000..3a852b5 --- /dev/null +++ b/src/client/index.ts @@ -0,0 +1,30 @@ +import { mount, unmount } from 'svelte'; +import index from './index.svelte'; +import './index.css'; + +declare global { + var didMount: boolean | undefined; +} + +let app: Record | undefined; + +// mount the application entrypoint to the DOM on first load. On subsequent hot +// updates, the app will be unmounted and re-mounted via the accept handler. + +const target = document.querySelector('body>div')!; +if (!globalThis.didMount) { + app = mount(index, { target }); +} + +globalThis.didMount = true; + +if (import.meta.hot) { + import.meta.hot.accept(async () => { + // avoid unmounting twice when another update gets accepted while outros are playing + if (!app) return; + const prevApp = app; + app = undefined; + await unmount(prevApp, { outro: true }); + app = mount(index, { target }); + }); +} diff --git a/src/client/shared.svelte.ts b/src/client/shared.svelte.ts new file mode 100644 index 0000000..24d9b58 --- /dev/null +++ b/src/client/shared.svelte.ts @@ -0,0 +1,16 @@ +class UserState { + isLoggedIn = $state(false); + + constructor() { } + + async checkIsLoggedIn() { + const res = await fetch('/auth/verify', { + mode: 'no-cors', + method: 'GET', + }); + + this.isLoggedIn = res.ok; + } +} + +export const userstate = new UserState(); diff --git a/src/static/favicon.png b/src/client/static/favicon.png similarity index 100% rename from src/static/favicon.png rename to src/client/static/favicon.png diff --git a/src/static/manifest.json b/src/client/static/manifest.json similarity index 100% rename from src/static/manifest.json rename to src/client/static/manifest.json diff --git a/src/static/robots.txt b/src/client/static/robots.txt similarity index 100% rename from src/static/robots.txt rename to src/client/static/robots.txt diff --git a/src/static/sitemap.txt b/src/client/static/sitemap.txt similarity index 100% rename from src/static/sitemap.txt rename to src/client/static/sitemap.txt diff --git a/src/util.ts b/src/client/util.ts similarity index 94% rename from src/util.ts rename to src/client/util.ts index 59d7c1e..e8ef1ff 100644 --- a/src/util.ts +++ b/src/client/util.ts @@ -1,5 +1,5 @@ import { userstate } from './shared.svelte'; -import type { SelectEntry } from './server/pt-api'; +import type { SelectEntry } from '../server/pt-api'; export async function fetchEntries(retry: boolean = false): Promise { try { diff --git a/src/index.ts b/src/index.ts index 3a852b5..3d6a8a1 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,30 +1,107 @@ -import { mount, unmount } from 'svelte'; -import index from './index.svelte'; -import './index.css'; +import { env } from 'bun'; +import homepage from './client/index.html'; +import robotsTxt from './client/static/robots.txt'; +import sitemapTxt from './client/static/sitemap.txt'; +// @ts-ignore ts2307 +import icon from './client/static/favicon.png' with { type: 'file' }; +const favicon = await Bun.file(icon).bytes(); +const development = env.NODE_ENV !== 'production'; +import { randomUUIDv7 } from 'bun'; +import ptApi from './server/pt-api'; +import auth from './server/auth'; declare global { - var didMount: boolean | undefined; + var loginTokens: Set; } -let app: Record | undefined; +let entriesCache = ''; +let entriesCacheTime = 0; +const entriesCacheTimeLimit = 1000 * 15; -// mount the application entrypoint to the DOM on first load. On subsequent hot -// updates, the app will be unmounted and re-mounted via the accept handler. - -const target = document.querySelector('body>div')!; -if (!globalThis.didMount) { - app = mount(index, { target }); +async function getEntriesCached() { + if (entriesCacheTime + entriesCacheTimeLimit < Date.now()) { + const data = await ptApi.query(); + entriesCache = JSON.stringify(data); + entriesCacheTime = Date.now(); + } + return entriesCache; } -globalThis.didMount = true; +Bun.serve({ + routes: { + '/': homepage, + '/robots.txt': new Response(robotsTxt, { + headers: { 'Content-Type': 'text/plain' }, + }), + '/sitemap.txt': new Response(sitemapTxt, { + headers: { 'Content-Type': 'text/plain' }, + }), + '/favicon.ico': new Response(favicon, { + headers: { 'Content-Type': 'image/png' }, + }), + '/health': new Response('OK'), + '/api/entries': { + async GET(req) { + const session = await auth.verify(req.headers); + if (!session) { + return auth.verifyFailResponse(); + } -if (import.meta.hot) { - import.meta.hot.accept(async () => { - // avoid unmounting twice when another update gets accepted while outros are playing - if (!app) return; - const prevApp = app; - app = undefined; - await unmount(prevApp, { outro: true }); - app = mount(index, { target }); - }); -} + return new Response(await getEntriesCached(), { + headers: { + 'Content-Type': 'application/json' + } + }); + }, + }, + + '/auth/logout': { + async POST() { + return auth.logoutResponse(); + }, + }, + '/auth/verify': { + async GET(req) { + const session = await auth.verify(req.headers); + if (!session) { + return auth.verifyFailResponse(); + } + + return auth.verifyResponse(session.token); + }, + }, + '/auth/login': { + async POST(req) { + const json = await req.json(); + const data = json as { email?: string, password?: string }; + const email = data.email?.toLocaleLowerCase(); + const password = data.password; + + if ( + typeof email !== 'string' || + typeof password !== 'string' || + email.length < 3 || + password.length === 0 + ) { + return new Response('Missing email or password', { status: 400 }); + } + + const token = await auth.login(email, password); + + if (!token) { + await new Promise((resolve) => setTimeout(resolve, (Math.random() * 200) + 800)); + return new Response('Incorrect email or password', { + status: 400, + }); + } + + return auth.loginResponse(token) + }, + }, + }, + development, + reusePort: true, + // async fetch(req, server) { + // return new Response("Not found", { status: 404 }); + // }, +}); diff --git a/src/server.ts b/src/server.ts deleted file mode 100644 index 8bf6ba7..0000000 --- a/src/server.ts +++ /dev/null @@ -1,148 +0,0 @@ -import { env } from 'bun'; -import homepage from './index.html'; -import robotsTxt from './static/robots.txt'; -import sitemapTxt from './static/sitemap.txt'; -// @ts-ignore ts2307 -import icon from './static/favicon.png' with { type: 'file' }; -const favicon = await Bun.file(icon).bytes(); -const development = env.NODE_ENV !== 'production'; -import { randomUUIDv7 } from 'bun'; -import ptApi from './server/pt-api'; - -declare global { - var loginTokens: Set; -} - -if (!globalThis.loginTokens) { - globalThis.loginTokens = new Set(); -} - -const authCookie = 'pt-auth'; - -let entriesCache = ''; -let entriesCacheTime = 0; -const entriesCacheTimeLimit = 1000 * 15; - -async function getEntriesCached() { - if (entriesCacheTime + entriesCacheTimeLimit < Date.now()) { - const data = await ptApi.query(); - entriesCache = JSON.stringify(data); - entriesCacheTime = Date.now(); - } - return entriesCache; -} - -Bun.serve({ - routes: { - '/': homepage, - '/robots.txt': new Response(robotsTxt, { - headers: { 'Content-Type': 'text/plain' }, - }), - '/sitemap.txt': new Response(sitemapTxt, { - headers: { 'Content-Type': 'text/plain' }, - }), - '/favicon.ico': new Response(favicon, { - headers: { 'Content-Type': 'image/png' }, - }), - '/health': new Response('OK'), - '/api/entries': { - async GET(req) { - if (!isLoggedIn(req)) return unauthorizedResp(); - - return new Response(await getEntriesCached(), { - headers: { - 'Content-Type': 'application/json' - } - }); - }, - }, - '/auth/logout': { - async POST() { - return new Response('Logout successful', { headers: logoutHeaders() }); - }, - }, - '/auth/login': { - async POST(req) { - const json = await req.json(); - const data = json as Record; - const email = data.email; - const password = data.password; - - if ( - typeof email !== 'string' || - typeof password !== 'string' || - email.length < 3 || - password.length === 0 - ) { - return new Response('Missing email or password', { status: 400 }); - } - - await new Promise((resolve) => setTimeout(resolve, 100)); - - if (email === env.EMAIL && password === env.PASSWORD) { - let token = randomUUIDv7('base64url'); - while (globalThis.loginTokens.has(token)) { - // generate a new token if it already exists - // this is unlikely to happen, but just in case - token = randomUUIDv7(); - } - - const cookie = new Bun.Cookie({ - name: authCookie, - value: token, - path: '/', - maxAge: 60 * 60 * 24 * 31, - secure: true, - sameSite: 'strict', - }); - - const headers = new Headers({ 'Set-Cookie': cookie.toString() }); - - globalThis.loginTokens.add(token); - - setTimeout(getEntriesCached, 1); - - return new Response('Login successful', { headers }); - } - - await new Promise((resolve) => setTimeout(resolve, 900)); - - return new Response('Incorrect email or password', { - status: 400, - }); - }, - }, - }, - development, - reusePort: true, - // async fetch(req, server) { - // return new Response("Not found", { status: 404 }); - // }, -}); - -function isLoggedIn(req: Request) { - const cookie = new Bun.CookieMap(req.headers.get('cookie') || ''); - const token = cookie.get(authCookie); - - if (!token) { - return false; - } - - return globalThis.loginTokens.has(token); -} - -function logoutHeaders() { - return new Headers({ - 'Set-Cookie': new Bun.Cookie({ - name: authCookie, - path: '/', - maxAge: -1, - secure: true, - sameSite: 'strict', - }).toString(), - }); -} - -function unauthorizedResp() { - return new Response('Unauthorized', { status: 401, headers: logoutHeaders() }); -} diff --git a/src/server/auth.ts b/src/server/auth.ts new file mode 100644 index 0000000..b4ee90b --- /dev/null +++ b/src/server/auth.ts @@ -0,0 +1,161 @@ +import { eq } from "drizzle-orm"; +import { drizzleDB } from "./db"; +import { users, userSessions } from "./db/schema"; +import { env } from "bun"; + +const authCookie = 'pt-auth'; + +const day = 1000 * 60 * 60 * 24; +let days = Number.parseInt(env.SESSION_DURATION_IN_DAYS ?? '31', 10); +if (Number.isNaN(days)) { + days = 31; +} + +const maxAge = days * day; +const renewalTime = Math.abs(Math.floor(maxAge / 7)); + +async function login(email: string, password: string): Promise { + const result = await drizzleDB.select().from(users).where(eq(users.email, email.toLocaleLowerCase())); + if (result.length === 0) { + return null; + } + + const user = result[0]; + if (!user) { + return null; + } + + // verify password + const valid = await Bun.password.verify(password, user.hash); + if (!valid) { + return null; + } + + const token = Bun.randomUUIDv7('base64url'); + + await drizzleDB.insert(userSessions).values({ + token, + maxAge: Date.now() + maxAge, + userId: user.id, + }); + + return token; +} + +function loginResponse(token: string) { + const cookie = new Bun.Cookie({ + name: authCookie, + value: token, + path: '/', + maxAge, + secure: true, + sameSite: 'strict', + httpOnly: true, + }); + + return new Response('Login successful', { + headers: new Headers({ + 'Set-Cookie': cookie.toString(), + }), + }); +} + +function logoutHeaders() { + return new Headers({ + 'Set-Cookie': new Bun.Cookie({ + name: authCookie, + path: '/', + maxAge: -1, + secure: true, + sameSite: 'strict', + httpOnly: true, + }).toString(), + }); +} + +function logoutResponse() { + return new Response('Logout successful', { status: 303, headers: logoutHeaders() }); +} + +function unAuthorizedResponse() { + return new Response('Unauthorized', { status: 401, headers: logoutHeaders() }); +} + +async function verify(headers: Headers) { + const cookieMap = new Bun.CookieMap(headers.get('cookie') || ''); + const token = cookieMap.get(authCookie); + if (!token) { + // cookie not found + return null; + } + + const result = await drizzleDB.select().from(userSessions).where(eq(userSessions.token, token)); + if (result.length === 0) { + // session not found + return null; + } + + const session = result[0]; + if (!session) { + // session not found + return null; + } + + const now = Date.now(); + if (session.maxAge <= now) { + // session expired + await drizzleDB.delete(userSessions).where(eq(userSessions.token, token)); + return null; + } + + if (session.maxAge <= now - renewalTime) { + // renew session + const newMaxAge = now + maxAge; + await drizzleDB.update(userSessions).set({ maxAge: newMaxAge }).where(eq(userSessions.token, token)); + session.maxAge = newMaxAge; // renew session + } + + return session; +} + +function verifyResponse(token: string) { + const cookie = new Bun.Cookie({ + name: authCookie, + value: token, + path: '/', + maxAge, + secure: true, + sameSite: 'strict', + httpOnly: true, + }); + + return new Response('Verify successful', { + headers: new Headers({ 'Set-Cookie': cookie.toString() }), + }); +} + +function verifyFailResponse() { + const cookie = new Bun.Cookie({ + name: authCookie, + path: '/', + maxAge: -1, + secure: true, + sameSite: 'strict', + httpOnly: true, + }); + + return new Response('Verify failure', { + status: 401, + headers: new Headers({ 'Set-Cookie': cookie.toString() }), + }); +} + +export default { + login, + loginResponse, + logoutResponse, + unAuthorizedResponse, + verify, + verifyResponse, + verifyFailResponse, +} diff --git a/src/server/db/index.ts b/src/server/db/index.ts new file mode 100644 index 0000000..7e04444 --- /dev/null +++ b/src/server/db/index.ts @@ -0,0 +1,71 @@ +import { Database } from 'bun:sqlite'; +import { env } from 'bun'; +import { drizzle } from 'drizzle-orm/bun-sqlite'; +import { createWrappedTimer } from '../wrapped-timer'; +import { users } from './schema'; + +function initDb() { + // global.db.exec('PRAGMA journal_mode = delete'); + global.db.exec('PRAGMA journal_mode = WAL'); + global.db.exec('PRAGMA synchronous = NORMAL'); + global.db.exec('PRAGMA auto_vacuum = INCREMENTAL'); + global.db.exec('PRAGMA wal_autocheckpoint = 1000'); +} + +function incrementalVacuumDb() { + global.db.exec('PRAGMA incremental_vacuum'); +} + +function optimizeDb() { + global.db.exec('PRAGMA optimize'); +} + +function vacuumDb() { + global.db.exec('vacuum'); +} + +async function seedDB(drizzle: typeof global.drizzleDB) { + let id = env.ADMIN_USER_ID; + let email = env.ADMIN_USER_EMAIL; + let name = env.ADMIN_USER_NAME ?? 'admin'; + let password = env.ADMIN_USER_PASSWORD; + + if (!id || !email || !password) { + console.warn('Missing environment variables for seeding database'); + return; + } + + const hash = await Bun.password.hash(password); + + await drizzle.insert(users).values({ + id, + email, + name, + hash + }).onConflictDoUpdate({ + target: users.id, + set: { email, name, hash } + }); +} + +if (global.db === undefined || global.drizzleDB === undefined) { + global.db = new Database(env.DATABASE_URL, { create: true, strict: true }); + initDb(); + global.drizzleDB = drizzle(global.db); + await seedDB(global.drizzleDB); +} + +const incrementalVacuumInterval = 1000 * 30; // 30 seconds +const incrementalVacuumRunnable = createWrappedTimer('databaseIncrementalVacuum', incrementalVacuumDb, incrementalVacuumInterval); + +setTimeout(incrementalVacuumRunnable.callback, 0); +const optimizeInterval = 1000 * 60 * 30; // 30 minutes +const optimizeRunnable = createWrappedTimer('databaseOptimize', optimizeDb, optimizeInterval); +setTimeout(optimizeRunnable.callback, 0); + +const vacuumInterval = 1000 * 60 * 60 * 24; // 24 hours +const vacuumRunnable = createWrappedTimer('databaseVacuum', vacuumDb, vacuumInterval); +setTimeout(vacuumRunnable.callback, 0); + +export const db = global.db; +export const drizzleDB = global.drizzleDB; diff --git a/src/server/db/migrate.ts b/src/server/db/migrate.ts new file mode 100644 index 0000000..ae82a41 --- /dev/null +++ b/src/server/db/migrate.ts @@ -0,0 +1,10 @@ +import { Database } from 'bun:sqlite'; +import { env } from 'bun'; +import { drizzle } from 'drizzle-orm/bun-sqlite'; +import { migrate } from 'drizzle-orm/bun-sqlite/migrator'; + +const sqlite = new Database(env.DATABASE_URL, { create: true, strict: true }); + +const db = drizzle(sqlite); + +migrate(db, { migrationsFolder: './drizzle' }); diff --git a/src/server/db/schema.ts b/src/server/db/schema.ts new file mode 100644 index 0000000..f21af71 --- /dev/null +++ b/src/server/db/schema.ts @@ -0,0 +1,37 @@ +import { sql, type InferSelectModel } from 'drizzle-orm'; +import { foreignKey, index, integer, primaryKey, sqliteTable, text } from 'drizzle-orm/sqlite-core'; + +const createdAt = integer('created_at', { mode: 'timestamp_ms' }) + .notNull() + .default(sql`(unixepoch('subsec') * 1000)`) + .$defaultFn(() => new Date()); +const updatedAt = integer('updated_at', { mode: 'timestamp_ms' }) + .notNull() + .default(sql`(unixepoch('subsec') * 1000)`) + .$onUpdateFn(() => new Date()); + +export const users = sqliteTable( + 'users', + { + id: text('id').primaryKey(), + email: text('email').notNull().unique(), + name: text('name').notNull(), + hash: text('hash').notNull(), + createdAt, + updatedAt, + } +); + +export const userSessions = sqliteTable( + 'user_sessions', + { + token: text('token').primaryKey().unique(), + userId: text('user_id').notNull(), + maxAge: integer('max_age', { mode: 'number' }).notNull(), + createdAt, + updatedAt, + }, + (t) => [ + foreignKey({ columns: [t.userId], foreignColumns: [users.id] }).onDelete('cascade'), + ], +); diff --git a/src/server/index.d.ts b/src/server/index.d.ts new file mode 100644 index 0000000..545ac5a --- /dev/null +++ b/src/server/index.d.ts @@ -0,0 +1,10 @@ +import type { Database } from 'bun:sqlite'; +import type { BunSQLiteDatabase } from 'drizzle-orm/bun-sqlite'; +import type { WrappedTimer } from './wrapped-timer'; + +declare global { + var db: Database; + var drizzleDB: BunSQLiteDatabase; + var performanceObserver: PerformanceObserver; + var wrappedTimers: Map; +} diff --git a/src/server/wrapped-timer.ts b/src/server/wrapped-timer.ts new file mode 100644 index 0000000..d19de7d --- /dev/null +++ b/src/server/wrapped-timer.ts @@ -0,0 +1,78 @@ +if (global.wrappedTimers === undefined) { + global.wrappedTimers = new Map(); +} + +export type WrappedTimer = { timer: Timer | undefined; running: boolean; }; +type WrappedTimerResult = { callback: ReturnType; wrappedTimer: WrappedTimer; }; + +/** + * Create a callback handler for a wrapped timer that prevents the callback from running if it is already running + * and prevents the callback from running if it is already running. + * @template TArgs + * @param {string} key unique identifier for the timer + * @param {(...args: Array) => (Promise | void)} callback function to run + * @param {Array} args arguments to pass to the callback + * @returns {() => Promise} + */ +function getCallbackHandler(key: string, callback: (...args: Array) => (Promise | void), ...args: Array): () => Promise { + return async () => { + const thisTimer = global.wrappedTimers.get(key); + if (thisTimer === undefined) { + // console.debug(`Wrapped timer ${key} does not exist`); + return; + } + if (thisTimer.running) { + // console.debug(`Wrapped timer ${key} is already running`); + return; + } + + try { + thisTimer.running = true; + await callback(...args); + } catch (e) { + console.error(e); + } finally { + thisTimer.running = false; + } + }; +} + +/** + * Create a wrapped timer aka interval + * @template TArgs + * @param {string} key unique identifier for the timer + * @param {number} interval in milliseconds + * @param {(...args: Array) => (Promise | void)} callback function to run + * @param {Array} args arguments to pass to the callback + * @returns {WrappedTimerResult} + */ +export function createWrappedTimer(key: string, callback: (...args: Array) => (Promise | void), interval: number, ...args: Array): WrappedTimerResult { + const thisTimer = global.wrappedTimers.get(key); + const handler = getCallbackHandler(key, callback, ...args); + + if (thisTimer !== undefined) { + // console.debug(`Wrapped timer ${key} already exists, clearing timer`); + clearInterval(thisTimer.timer); + // console.debug(`Wrapped timer ${key} set with interval ${interval}ms`); + thisTimer.timer = setInterval(handler, interval); + + return { + callback: handler, + wrappedTimer: thisTimer, + }; + } + + // console.debug(`Wrapped timer ${key} created with interval ${interval}ms`); + + const wrappedTimer: WrappedTimer = { + timer: setInterval(handler, interval), + running: false, + }; + + global.wrappedTimers.set(key, wrappedTimer); + + return { + callback: handler, + wrappedTimer, + }; +} diff --git a/src/shared.svelte.ts b/src/shared.svelte.ts deleted file mode 100644 index 9721734..0000000 --- a/src/shared.svelte.ts +++ /dev/null @@ -1,28 +0,0 @@ -function getCookieMap() { - const cookies = document.cookie.split('; '); - const cookieMap = new Map(); - for (let i = 0; i < cookies.length; i++) { - const cookie = cookies[i]; - if (!cookie) continue; - let [key, value] = cookie.split('='); - if (!key || !value) continue; - cookieMap.set(key, value); - } - - return cookieMap; -} - -class UserState { - isLoggedIn = $state(false); - - constructor() { - this.checkIsLoggedIn(); - } - - checkIsLoggedIn() { - const cookieVal = getCookieMap().get('pt-auth'); - this.isLoggedIn = cookieVal !== undefined; - } -} - -export const userstate = new UserState(); diff --git a/tsconfig.json b/tsconfig.json index 0abd418..eb040b6 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -24,5 +24,8 @@ "noUnusedLocals": false, "noUnusedParameters": false, "noPropertyAccessFromIndexSignature": false - } + }, + "exclude": [ + "node_modules" + ] }