Implement most of status system (still kinda bootycheeks at checking if

you're connected), fionally fix prettierrc
This commit is contained in:
Soph :3 2026-01-04 20:26:42 +02:00
parent 126acf52f3
commit e64910d895
29 changed files with 2001 additions and 879 deletions

View file

@ -1,5 +1,5 @@
{
"useTabs": true,
"useTabs": false,
"singleQuote": true,
"trailingComma": "none",
"printWidth": 100,
@ -12,5 +12,5 @@
}
}
],
"tailwindStylesheet": "./src/routes/layout.css"
"tailwindStylesheet": "./src/app.css"
}

View file

@ -51,9 +51,9 @@
"packages": {
"@drizzle-team/brocli": ["@drizzle-team/brocli@0.10.2", "", {}, "sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w=="],
"@emnapi/core": ["@emnapi/core@1.7.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" } }, "sha512-o1uhUASyo921r2XtHYOHy7gdkGLge8ghBEQHMWmyJFoXlpU58kIrhhN3w26lpQb6dspetweapMn2CSNwQ8I4wg=="],
"@emnapi/core": ["@emnapi/core@1.8.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" } }, "sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg=="],
"@emnapi/runtime": ["@emnapi/runtime@1.7.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA=="],
"@emnapi/runtime": ["@emnapi/runtime@1.8.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg=="],
"@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.1.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ=="],
@ -381,20 +381,10 @@
"balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
"base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="],
"better-sqlite3": ["better-sqlite3@12.5.0", "", { "dependencies": { "bindings": "^1.5.0", "prebuild-install": "^7.1.1" } }, "sha512-WwCZ/5Diz7rsF29o27o0Gcc1Du+l7Zsv7SYtVPG0X3G/uUI1LqdxrQI7c9Hs2FWpqXXERjW9hp6g3/tH7DlVKg=="],
"bindings": ["bindings@1.5.0", "", { "dependencies": { "file-uri-to-path": "1.0.0" } }, "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ=="],
"bits-ui": ["bits-ui@2.14.4", "", { "dependencies": { "@floating-ui/core": "^1.7.1", "@floating-ui/dom": "^1.7.1", "esm-env": "^1.1.2", "runed": "^0.35.1", "svelte-toolbelt": "^0.10.6", "tabbable": "^6.2.0" }, "peerDependencies": { "@internationalized/date": "^3.8.1", "svelte": "^5.33.0" } }, "sha512-W6kenhnbd/YVvur+DKkaVJ6GldE53eLewur5AhUCqslYQ0vjZr8eWlOfwZnMiPB+PF5HMVqf61vXBvmyrAmPWg=="],
"bl": ["bl@4.1.0", "", { "dependencies": { "buffer": "^5.5.0", "inherits": "^2.0.4", "readable-stream": "^3.4.0" } }, "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w=="],
"bits-ui": ["bits-ui@2.15.2", "", { "dependencies": { "@floating-ui/core": "^1.7.1", "@floating-ui/dom": "^1.7.1", "esm-env": "^1.1.2", "runed": "^0.35.1", "svelte-toolbelt": "^0.10.6", "tabbable": "^6.2.0" }, "peerDependencies": { "@internationalized/date": "^3.8.1", "svelte": "^5.33.0" } }, "sha512-S8eDbFkZCN17kZ7J9fD3MRXziV9ozjdFt2D3vTr2bvXCl7BtrIqguYt2U/zrFgLdR2erwybvCKv0JXYn8uKLDQ=="],
"brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="],
"buffer": ["buffer@5.7.1", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" } }, "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ=="],
"buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="],
"bun-sqlite-key-value": ["bun-sqlite-key-value@1.13.1", "", { "peerDependencies": { "typescript": "^5.5.3" } }, "sha512-cb3thB8QXPeXB6B7NhObpADEYvtVNwqg/0ED7PgKt2OxVAxPSejkiTsy1+byQDC0AwLYajw3nhtr/ubKvcLcKw=="],
@ -407,8 +397,6 @@
"chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="],
"chownr": ["chownr@1.1.4", "", {}, "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg=="],
"clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="],
"color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
@ -429,10 +417,6 @@
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
"decompress-response": ["decompress-response@6.0.0", "", { "dependencies": { "mimic-response": "^3.1.0" } }, "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ=="],
"deep-extend": ["deep-extend@0.6.0", "", {}, "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA=="],
"deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="],
"deepmerge": ["deepmerge@4.3.1", "", {}, "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A=="],
@ -447,8 +431,6 @@
"drizzle-orm": ["drizzle-orm@0.45.1", "", { "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.13", "@prisma/client": "*", "@tidbcloud/serverless": "*", "@types/better-sqlite3": "*", "@types/pg": "*", "@types/sql.js": "*", "@upstash/redis": ">=1.34.7", "@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", "@upstash/redis", "@vercel/postgres", "@xata.io/client", "better-sqlite3", "bun-types", "expo-sqlite", "gel", "knex", "kysely", "mysql2", "pg", "postgres", "sql.js", "sqlite3"] }, "sha512-Te0FOdKIistGNPMq2jscdqngBRfBpC8uMFVwqjf6gtTVJHIQ/dosgV/CLBU2N4ZJBsXL5savCba9b0YJskKdcA=="],
"end-of-stream": ["end-of-stream@1.4.5", "", { "dependencies": { "once": "^1.4.0" } }, "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg=="],
"enhanced-resolve": ["enhanced-resolve@5.18.4", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.2.0" } }, "sha512-LgQMM4WXU3QI+SYgEc2liRgznaD5ojbmY3sb8LxyguVkIg5FxdpTkvk72te2R38/TGKxH634oLxXRGY6d7AP+Q=="],
"esbuild": ["esbuild@0.25.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="],
@ -483,8 +465,6 @@
"esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="],
"expand-template": ["expand-template@2.0.3", "", {}, "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg=="],
"fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
"fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="],
@ -497,8 +477,6 @@
"file-entry-cache": ["file-entry-cache@8.0.0", "", { "dependencies": { "flat-cache": "^4.0.0" } }, "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ=="],
"file-uri-to-path": ["file-uri-to-path@1.0.0", "", {}, "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw=="],
"find-up": ["find-up@5.0.0", "", { "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" } }, "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng=="],
"flat-cache": ["flat-cache@4.0.1", "", { "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.4" } }, "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw=="],
@ -507,16 +485,12 @@
"formdata-polyfill": ["formdata-polyfill@4.0.10", "", { "dependencies": { "fetch-blob": "^3.1.2" } }, "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g=="],
"fs-constants": ["fs-constants@1.0.0", "", {}, "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow=="],
"fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
"function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
"get-tsconfig": ["get-tsconfig@4.13.0", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ=="],
"github-from-package": ["github-from-package@0.0.0", "", {}, "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw=="],
"glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="],
"globals": ["globals@16.5.0", "", {}, "sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ=="],
@ -527,18 +501,12 @@
"hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="],
"ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="],
"ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="],
"import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="],
"imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="],
"inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
"ini": ["ini@1.3.8", "", {}, "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew=="],
"inline-style-parser": ["inline-style-parser@0.2.7", "", {}, "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA=="],
"is-core-module": ["is-core-module@2.16.1", "", { "dependencies": { "hasown": "^2.0.2" } }, "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w=="],
@ -611,14 +579,8 @@
"magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
"mimic-response": ["mimic-response@3.1.0", "", {}, "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ=="],
"minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="],
"minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="],
"mkdirp-classic": ["mkdirp-classic@0.5.3", "", {}, "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A=="],
"mode-watcher": ["mode-watcher@1.1.0", "", { "dependencies": { "runed": "^0.25.0", "svelte-toolbelt": "^0.7.1" }, "peerDependencies": { "svelte": "^5.27.0" } }, "sha512-mUT9RRGPDYenk59qJauN1rhsIMKBmWA3xMF+uRwE8MW/tjhaDSCCARqkSuDTq8vr4/2KcAxIGVjACxTjdk5C3g=="],
"mri": ["mri@1.2.0", "", {}, "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA=="],
@ -629,18 +591,12 @@
"nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
"napi-build-utils": ["napi-build-utils@2.0.0", "", {}, "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA=="],
"natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="],
"node-abi": ["node-abi@3.85.0", "", { "dependencies": { "semver": "^7.3.5" } }, "sha512-zsFhmbkAzwhTft6nd3VxcG0cvJsT70rL+BIGHWVq5fi6MwGrHwzqKaxXE+Hl2GmnGItnDKPPkO5/LQqjVkIdFg=="],
"node-domexception": ["node-domexception@1.0.0", "", {}, "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ=="],
"node-fetch": ["node-fetch@3.3.2", "", { "dependencies": { "data-uri-to-buffer": "^4.0.0", "fetch-blob": "^3.1.4", "formdata-polyfill": "^4.0.10" } }, "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA=="],
"once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="],
"optionator": ["optionator@0.9.4", "", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="],
"p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="],
@ -671,8 +627,6 @@
"postgres": ["postgres@3.4.7", "", {}, "sha512-Jtc2612XINuBjIl/QTWsV5UvE8UHuNblcO3vVADSrKsrc6RqGX6lOW1cEo3CM2v0XG4Nat8nI+YM7/f26VxXLw=="],
"prebuild-install": ["prebuild-install@7.1.3", "", { "dependencies": { "detect-libc": "^2.0.0", "expand-template": "^2.0.3", "github-from-package": "0.0.0", "minimist": "^1.2.3", "mkdirp-classic": "^0.5.3", "napi-build-utils": "^2.0.0", "node-abi": "^3.3.0", "pump": "^3.0.0", "rc": "^1.2.7", "simple-get": "^4.0.0", "tar-fs": "^2.0.0", "tunnel-agent": "^0.6.0" }, "bin": { "prebuild-install": "bin.js" } }, "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug=="],
"prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="],
"prettier": ["prettier@3.7.4", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA=="],
@ -683,14 +637,8 @@
"promise-limit": ["promise-limit@2.7.0", "", {}, "sha512-7nJ6v5lnJsXwGprnGXga4wx6d1POjvi5Qmf1ivTRxTjH4Z/9Czja/UCMLVmB9N93GeWOU93XaFaEt6jbuoagNw=="],
"pump": ["pump@3.0.3", "", { "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" } }, "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA=="],
"punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
"rc": ["rc@1.2.8", "", { "dependencies": { "deep-extend": "^0.6.0", "ini": "~1.3.0", "minimist": "^1.2.0", "strip-json-comments": "~2.0.1" }, "bin": { "rc": "./cli.js" } }, "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw=="],
"readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="],
"readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="],
"resolve": ["resolve@1.22.11", "", { "dependencies": { "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ=="],
@ -705,8 +653,6 @@
"sade": ["sade@1.8.1", "", { "dependencies": { "mri": "^1.1.0" } }, "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A=="],
"safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="],
"semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="],
"set-cookie-parser": ["set-cookie-parser@2.7.2", "", {}, "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw=="],
@ -715,10 +661,6 @@
"shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="],
"simple-concat": ["simple-concat@1.0.1", "", {}, "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q=="],
"simple-get": ["simple-get@4.0.1", "", { "dependencies": { "decompress-response": "^6.0.0", "once": "^1.3.1", "simple-concat": "^1.0.0" } }, "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA=="],
"sirv": ["sirv@3.0.2", "", { "dependencies": { "@polka/url": "^1.0.0-next.24", "mrmime": "^2.0.0", "totalist": "^3.0.0" } }, "sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g=="],
"source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="],
@ -727,8 +669,6 @@
"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=="],
"string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="],
"strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="],
"style-to-object": ["style-to-object@1.0.14", "", { "dependencies": { "inline-style-parser": "0.2.7" } }, "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw=="],
@ -755,20 +695,14 @@
"tapable": ["tapable@2.3.0", "", {}, "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg=="],
"tar-fs": ["tar-fs@2.1.4", "", { "dependencies": { "chownr": "^1.1.1", "mkdirp-classic": "^0.5.2", "pump": "^3.0.0", "tar-stream": "^2.1.4" } }, "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ=="],
"tar-stream": ["tar-stream@2.2.0", "", { "dependencies": { "bl": "^4.0.3", "end-of-stream": "^1.4.1", "fs-constants": "^1.0.0", "inherits": "^2.0.3", "readable-stream": "^3.1.1" } }, "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ=="],
"tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="],
"totalist": ["totalist@3.0.1", "", {}, "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ=="],
"ts-api-utils": ["ts-api-utils@2.3.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-6eg3Y9SF7SsAvGzRHQvvc1skDAhwI4YQ32ui1scxD1Ccr0G5qIIbUBT3pFTKX8kmWIQClHobtUdNuaBgwdfdWg=="],
"ts-api-utils": ["ts-api-utils@2.4.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA=="],
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"tunnel-agent": ["tunnel-agent@0.6.0", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w=="],
"tw-animate-css": ["tw-animate-css@1.4.0", "", {}, "sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ=="],
"type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="],
@ -797,8 +731,6 @@
"word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="],
"wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="],
"ws": ["ws@8.18.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg=="],
"yaml": ["yaml@1.10.2", "", {}, "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg=="],
@ -815,9 +747,9 @@
"@rollup/plugin-commonjs/is-reference": ["is-reference@1.2.1", "", { "dependencies": { "@types/estree": "*" } }, "sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ=="],
"@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.7.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" }, "bundled": true }, "sha512-o1uhUASyo921r2XtHYOHy7gdkGLge8ghBEQHMWmyJFoXlpU58kIrhhN3w26lpQb6dspetweapMn2CSNwQ8I4wg=="],
"@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.8.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" }, "bundled": true }, "sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg=="],
"@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.7.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA=="],
"@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.8.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg=="],
"@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.1.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ=="],
@ -837,8 +769,6 @@
"mode-watcher/svelte-toolbelt": ["svelte-toolbelt@0.7.1", "", { "dependencies": { "clsx": "^2.1.1", "runed": "^0.23.2", "style-to-object": "^1.0.8" }, "peerDependencies": { "svelte": "^5.0.0" } }, "sha512-HcBOcR17Vx9bjaOceUvxkY3nGmbBmCBBbuWLLEWO6jtmWH8f/QoWmbyUfQZrpDINH39en1b8mptfPQT9VKQ1xQ=="],
"rc/strip-json-comments": ["strip-json-comments@2.0.1", "", {}, "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ=="],
"vite/esbuild": ["esbuild@0.27.2", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.2", "@esbuild/android-arm": "0.27.2", "@esbuild/android-arm64": "0.27.2", "@esbuild/android-x64": "0.27.2", "@esbuild/darwin-arm64": "0.27.2", "@esbuild/darwin-x64": "0.27.2", "@esbuild/freebsd-arm64": "0.27.2", "@esbuild/freebsd-x64": "0.27.2", "@esbuild/linux-arm": "0.27.2", "@esbuild/linux-arm64": "0.27.2", "@esbuild/linux-ia32": "0.27.2", "@esbuild/linux-loong64": "0.27.2", "@esbuild/linux-mips64el": "0.27.2", "@esbuild/linux-ppc64": "0.27.2", "@esbuild/linux-riscv64": "0.27.2", "@esbuild/linux-s390x": "0.27.2", "@esbuild/linux-x64": "0.27.2", "@esbuild/netbsd-arm64": "0.27.2", "@esbuild/netbsd-x64": "0.27.2", "@esbuild/openbsd-arm64": "0.27.2", "@esbuild/openbsd-x64": "0.27.2", "@esbuild/openharmony-arm64": "0.27.2", "@esbuild/sunos-x64": "0.27.2", "@esbuild/win32-arm64": "0.27.2", "@esbuild/win32-ia32": "0.27.2", "@esbuild/win32-x64": "0.27.2" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw=="],
"@esbuild-kit/core-utils/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.18.20", "", { "os": "android", "cpu": "arm" }, "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw=="],

View file

@ -0,0 +1,9 @@
CREATE TABLE `invite` (
`id` text PRIMARY KEY NOT NULL,
`server_id` text NOT NULL,
`code` text NOT NULL,
FOREIGN KEY (`server_id`) REFERENCES `server`(`id`) ON UPDATE no action ON DELETE no action
);
--> statement-breakpoint
ALTER TABLE `friendRequest` ADD `from_username` text NOT NULL REFERENCES user(id);--> statement-breakpoint
ALTER TABLE `friendRequest` ADD `to_username` text NOT NULL REFERENCES user(id);

View file

@ -0,0 +1,9 @@
CREATE TABLE `directMessage` (
`id` text PRIMARY KEY NOT NULL,
`name` text NOT NULL,
`server_id` text NOT NULL,
`messages` text DEFAULT '[]' NOT NULL,
FOREIGN KEY (`server_id`) REFERENCES `server`(`id`) ON UPDATE no action ON DELETE no action
);
--> statement-breakpoint
ALTER TABLE `user` ADD `status_overwrite` integer DEFAULT 3 NOT NULL;

View file

@ -0,0 +1,450 @@
{
"version": "6",
"dialect": "sqlite",
"id": "c0ac7678-4e2c-433f-ac26-73c7b88cc38b",
"prevId": "e809c266-891b-4355-a87c-facd55a5293a",
"tables": {
"channel": {
"name": "channel",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"server_id": {
"name": "server_id",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"messages": {
"name": "messages",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "'[]'"
}
},
"indexes": {},
"foreignKeys": {
"channel_server_id_server_id_fk": {
"name": "channel_server_id_server_id_fk",
"tableFrom": "channel",
"tableTo": "server",
"columnsFrom": [
"server_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"friendRequest": {
"name": "friendRequest",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"from_user": {
"name": "from_user",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"from_username": {
"name": "from_username",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"to_username": {
"name": "to_username",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"to_user": {
"name": "to_user",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {
"friendRequest_from_user_user_id_fk": {
"name": "friendRequest_from_user_user_id_fk",
"tableFrom": "friendRequest",
"tableTo": "user",
"columnsFrom": [
"from_user"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
},
"friendRequest_from_username_user_id_fk": {
"name": "friendRequest_from_username_user_id_fk",
"tableFrom": "friendRequest",
"tableTo": "user",
"columnsFrom": [
"from_username"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
},
"friendRequest_to_username_user_id_fk": {
"name": "friendRequest_to_username_user_id_fk",
"tableFrom": "friendRequest",
"tableTo": "user",
"columnsFrom": [
"to_username"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
},
"friendRequest_to_user_user_id_fk": {
"name": "friendRequest_to_user_user_id_fk",
"tableFrom": "friendRequest",
"tableTo": "user",
"columnsFrom": [
"to_user"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"group": {
"name": "group",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"owner": {
"name": "owner",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"members": {
"name": "members",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "'[]'"
}
},
"indexes": {},
"foreignKeys": {
"group_owner_user_id_fk": {
"name": "group_owner_user_id_fk",
"tableFrom": "group",
"tableTo": "user",
"columnsFrom": [
"owner"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"invite": {
"name": "invite",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"server_id": {
"name": "server_id",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"code": {
"name": "code",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {
"invite_server_id_server_id_fk": {
"name": "invite_server_id_server_id_fk",
"tableFrom": "invite",
"tableTo": "server",
"columnsFrom": [
"server_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"server": {
"name": "server",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"owner": {
"name": "owner",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"members": {
"name": "members",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "'[]'"
},
"channels": {
"name": "channels",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "'[]'"
}
},
"indexes": {},
"foreignKeys": {
"server_owner_user_id_fk": {
"name": "server_owner_user_id_fk",
"tableFrom": "server",
"tableTo": "user",
"columnsFrom": [
"owner"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"session": {
"name": "session",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"user_id": {
"name": "user_id",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"expires_at": {
"name": "expires_at",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {
"session_user_id_user_id_fk": {
"name": "session_user_id_user_id_fk",
"tableFrom": "session",
"tableTo": "user",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"user": {
"name": "user",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"username": {
"name": "username",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"email": {
"name": "email",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"password_hash": {
"name": "password_hash",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"friends": {
"name": "friends",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "'[]'"
},
"servers": {
"name": "servers",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "'[]'"
},
"groups": {
"name": "groups",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "'[]'"
}
},
"indexes": {
"user_username_unique": {
"name": "user_username_unique",
"columns": [
"username"
],
"isUnique": true
},
"user_email_unique": {
"name": "user_email_unique",
"columns": [
"email"
],
"isUnique": true
}
},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
}
},
"views": {},
"enums": {},
"_meta": {
"schemas": {},
"tables": {},
"columns": {}
},
"internal": {
"indexes": {}
}
}

View file

@ -0,0 +1,511 @@
{
"version": "6",
"dialect": "sqlite",
"id": "bd16f3db-5965-4d36-89b7-d259cbd21149",
"prevId": "c0ac7678-4e2c-433f-ac26-73c7b88cc38b",
"tables": {
"channel": {
"name": "channel",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"server_id": {
"name": "server_id",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"messages": {
"name": "messages",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "'[]'"
}
},
"indexes": {},
"foreignKeys": {
"channel_server_id_server_id_fk": {
"name": "channel_server_id_server_id_fk",
"tableFrom": "channel",
"tableTo": "server",
"columnsFrom": [
"server_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"directMessage": {
"name": "directMessage",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"server_id": {
"name": "server_id",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"messages": {
"name": "messages",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "'[]'"
}
},
"indexes": {},
"foreignKeys": {
"directMessage_server_id_server_id_fk": {
"name": "directMessage_server_id_server_id_fk",
"tableFrom": "directMessage",
"tableTo": "server",
"columnsFrom": [
"server_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"friendRequest": {
"name": "friendRequest",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"from_user": {
"name": "from_user",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"from_username": {
"name": "from_username",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"to_username": {
"name": "to_username",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"to_user": {
"name": "to_user",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {
"friendRequest_from_user_user_id_fk": {
"name": "friendRequest_from_user_user_id_fk",
"tableFrom": "friendRequest",
"tableTo": "user",
"columnsFrom": [
"from_user"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
},
"friendRequest_from_username_user_id_fk": {
"name": "friendRequest_from_username_user_id_fk",
"tableFrom": "friendRequest",
"tableTo": "user",
"columnsFrom": [
"from_username"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
},
"friendRequest_to_username_user_id_fk": {
"name": "friendRequest_to_username_user_id_fk",
"tableFrom": "friendRequest",
"tableTo": "user",
"columnsFrom": [
"to_username"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
},
"friendRequest_to_user_user_id_fk": {
"name": "friendRequest_to_user_user_id_fk",
"tableFrom": "friendRequest",
"tableTo": "user",
"columnsFrom": [
"to_user"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"group": {
"name": "group",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"owner": {
"name": "owner",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"members": {
"name": "members",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "'[]'"
}
},
"indexes": {},
"foreignKeys": {
"group_owner_user_id_fk": {
"name": "group_owner_user_id_fk",
"tableFrom": "group",
"tableTo": "user",
"columnsFrom": [
"owner"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"invite": {
"name": "invite",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"server_id": {
"name": "server_id",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"code": {
"name": "code",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {
"invite_server_id_server_id_fk": {
"name": "invite_server_id_server_id_fk",
"tableFrom": "invite",
"tableTo": "server",
"columnsFrom": [
"server_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"server": {
"name": "server",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"owner": {
"name": "owner",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"members": {
"name": "members",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "'[]'"
},
"channels": {
"name": "channels",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "'[]'"
}
},
"indexes": {},
"foreignKeys": {
"server_owner_user_id_fk": {
"name": "server_owner_user_id_fk",
"tableFrom": "server",
"tableTo": "user",
"columnsFrom": [
"owner"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"session": {
"name": "session",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"user_id": {
"name": "user_id",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"expires_at": {
"name": "expires_at",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {
"session_user_id_user_id_fk": {
"name": "session_user_id_user_id_fk",
"tableFrom": "session",
"tableTo": "user",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"user": {
"name": "user",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"username": {
"name": "username",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"email": {
"name": "email",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"password_hash": {
"name": "password_hash",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"status_overwrite": {
"name": "status_overwrite",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": 3
},
"friends": {
"name": "friends",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "'[]'"
},
"servers": {
"name": "servers",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "'[]'"
},
"groups": {
"name": "groups",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "'[]'"
}
},
"indexes": {
"user_username_unique": {
"name": "user_username_unique",
"columns": [
"username"
],
"isUnique": true
},
"user_email_unique": {
"name": "user_email_unique",
"columns": [
"email"
],
"isUnique": true
}
},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
}
},
"views": {},
"enums": {},
"_meta": {
"schemas": {},
"tables": {},
"columns": {}
},
"internal": {
"indexes": {}
}
}

View file

@ -50,6 +50,20 @@
"when": 1767537153441,
"tag": "0006_gifted_machine_man",
"breakpoints": true
},
{
"idx": 7,
"version": "6",
"when": 1767543898231,
"tag": "0007_outstanding_punisher",
"breakpoints": true
},
{
"idx": 8,
"version": "6",
"when": 1767549353944,
"tag": "0008_mute_lilandra",
"breakpoints": true
}
]
}

View file

@ -1,27 +1,81 @@
import type { Handle } from '@sveltejs/kit';
import * as auth from '$lib/server/auth';
import { kvStore } from '$lib/server/db';
import { _sendToSubscribers } from './routes/api/updates/+server';
import { Status } from '$lib';
const handleAuth: Handle = async ({ event, resolve }) => {
const sessionToken = event.cookies.get(auth.sessionCookieName);
const sessionToken = event.cookies.get(auth.sessionCookieName);
if (!sessionToken) {
event.locals.user = null;
event.locals.session = null;
if (!sessionToken) {
event.locals.user = null;
event.locals.session = null;
return resolve(event);
}
return resolve(event);
}
const { session, user } = await auth.validateSessionToken(sessionToken);
if (session) {
auth.setSessionTokenCookie(event, sessionToken, session.expiresAt);
} else {
auth.deleteSessionTokenCookie(event);
}
const { session, user } = await auth.validateSessionToken(sessionToken);
if (session) {
auth.setSessionTokenCookie(event, sessionToken, session.expiresAt);
} else {
auth.deleteSessionTokenCookie(event);
}
event.locals.user = user;
event.locals.session = session;
event.locals.user = user;
event.locals.session = session;
return resolve(event);
if (event.locals.user) {
const id = event.locals.user.id;
const overwrite = event.locals.user.statusOverwrite as 1 | 2 | 3 | undefined;
const now = Date.now();
const lastActiveKey = `user-${id}-last-active`;
const stateKey = `user-${id}-state`;
const timerKey = `user-${id}-offline-timer`;
const lastActive = kvStore.get<number>(lastActiveKey) ?? 0;
const state = kvStore.get<1 | 2 | 3>(stateKey) ?? Status.OFFLINE;
// always update activity
kvStore.set(lastActiveKey, now);
if (overwrite !== Status.OFFLINE) {
const shouldSend = state === Status.OFFLINE && now - lastActive > 5000;
if (shouldSend) {
const outgoingStatus = overwrite === Status.DND ? Status.DND : Status.ONLINE;
_sendToSubscribers(id, {
type: 'status',
id,
status: outgoingStatus
});
kvStore.set(stateKey, outgoingStatus);
}
}
const existingTimer = kvStore.get<NodeJS.Timeout>(timerKey);
if (existingTimer) {
clearTimeout(existingTimer);
}
const offlineTimer = setTimeout(() => {
const last = kvStore.get<number>(lastActiveKey) ?? 0;
if (Date.now() - last >= 10_000) {
_sendToSubscribers(id, {
type: 'status',
id,
status: Status.OFFLINE
});
kvStore.set(stateKey, Status.OFFLINE);
kvStore.delete(timerKey);
}
}, 10_000);
kvStore.set(timerKey, offlineTimer + '');
}
return resolve(event);
};
export const handle: Handle = handleAuth;

View file

@ -1,22 +1,27 @@
<script lang="ts">
import { type Data, type OverviewUser } from "$lib";
import * as Collapsible from "$lib/components/ui/collapsible/index.js";
import * as Sidebar from "$lib/components/ui/sidebar/index.js";
import * as Dialog from "$lib/components/ui/dialog/index.js";
import * as Tabs from "$lib/components/ui/tabs/index.js";
import * as Card from "$lib/components/ui/card/index.js";
import MessagesSquare from "@lucide/svelte/icons/messages-square";
import MinusIcon from "@lucide/svelte/icons/minus";
import PlusIcon from "@lucide/svelte/icons/plus";
import { type Data, type OverviewUser } from '$lib';
import * as Collapsible from '$lib/components/ui/collapsible/index.js';
import * as Sidebar from '$lib/components/ui/sidebar/index.js';
import * as Dialog from '$lib/components/ui/dialog/index.js';
import * as Tabs from '$lib/components/ui/tabs/index.js';
import * as Card from '$lib/components/ui/card/index.js';
import MessagesSquare from '@lucide/svelte/icons/messages-square';
import MinusIcon from '@lucide/svelte/icons/minus';
import PlusIcon from '@lucide/svelte/icons/plus';
import UserRoundPlus from '@lucide/svelte/icons/user-round-plus';
import UsersRound from '@lucide/svelte/icons/users-round';
import CirclePlus from '@lucide/svelte/icons/circle-plus';
import Input from "./ui/input/input.svelte";
import Button, { buttonVariants } from "./ui/button/button.svelte";
import User from "./extra/User.svelte";
import type { SessionValidationResult } from "$lib/server/auth";
import Input from './ui/input/input.svelte';
import Button, { buttonVariants } from './ui/button/button.svelte';
import User from './extra/User.svelte';
import type { SessionValidationResult } from '$lib/server/auth';
let { currentPage = $bindable<string|null>(), data, user, ...restProps }: {currentPage: string|null, data: Data, user: SessionValidationResult['user'] }= $props();
let {
currentPage = $bindable<string | null>(),
data,
user,
...restProps
}: { currentPage: string | null; data: Data; user: SessionValidationResult['user'] } = $props();
</script>
<Sidebar.Root {...restProps}>
@ -25,7 +30,7 @@
<Sidebar.MenuItem>
<Sidebar.MenuButton size="lg">
<div
class="bg-sidebar-primary text-sidebar-primary-foreground flex aspect-square size-8 items-center justify-center rounded-lg"
class="flex aspect-square size-8 items-center justify-center rounded-lg bg-sidebar-primary text-sidebar-primary-foreground"
>
<MessagesSquare class="size-4" />
</div>
@ -33,193 +38,170 @@
<span class="font-medium">chat.sad.ovh</span>
</div>
</Sidebar.MenuButton>
<div class="w-full flex gap-2 justify-center">
<div class="flex w-full justify-center gap-2">
<Dialog.Root>
<Dialog.Trigger>
<Button
variant={user!.friendRequests.length > 0 ? 'destructive' : 'outline'}
size="icon"
>
<UserRoundPlus />
</Button>
</Dialog.Trigger>
<Dialog.Root>
<Dialog.Trigger>
<Button variant={user!.friendRequests.length > 0 ? "destructive" : "outline"} size="icon">
<UserRoundPlus />
<Dialog.Content class="sm:max-w-[425px]">
<Dialog.Header>
<Dialog.Title>Add a friend</Dialog.Title>
<Dialog.Description>
Add a friend using their username or manage pending requests.
</Dialog.Description>
</Dialog.Header>
</Button>
</Dialog.Trigger>
<!-- input to add a new friend -->
<form method="POST" action="?/addFriend" class="mb-4">
<Input name="username" placeholder="username" required class="mb-2" />
<Dialog.Footer>
<Dialog.Close class={buttonVariants({ variant: 'outline' })}>Cancel</Dialog.Close>
<Button type="submit">Send request</Button>
</Dialog.Footer>
</form>
<!-- Tabs for Friend Requests -->
<Tabs.Root value="outgoing">
<Tabs.List>
<Tabs.Trigger value="outgoing">Outgoing</Tabs.Trigger>
<Tabs.Trigger value="incoming">Incoming</Tabs.Trigger>
</Tabs.List>
<Dialog.Content class="sm:max-w-[425px]">
<Dialog.Header>
<Dialog.Title>Add a friend</Dialog.Title>
<Dialog.Description>
Add a friend using their username or manage pending requests.
</Dialog.Description>
</Dialog.Header>
<!-- Outgoing Requests -->
<Tabs.Content value="outgoing">
{#if user.friendRequests.filter((r) => r.fromUser === user.id).length === 0}
<p class="text-sm text-muted-foreground">No outgoing requests</p>
{:else}
{#each user.friendRequests.filter((r) => r.fromUser === user.id) as request (request.id)}
<Card.Root class="mb-2">
<Card.Header>
<Card.Title>{request.username}</Card.Title>
<Card.Description>Request sent</Card.Description>
</Card.Header>
<Card.Footer>
<form method="POST" action="?/cancelFriendRequest">
<input type="hidden" name="requestId" value={request.id} />
<Button type="submit" variant="outline" size="sm">Cancel</Button>
</form>
</Card.Footer>
</Card.Root>
{/each}
{/if}
</Tabs.Content>
<!-- input to add a new friend -->
<form method="POST" action="?/addFriend" class="mb-4">
<Input name="username" placeholder="username" required class="mb-2" />
<Dialog.Footer>
<Dialog.Close class={buttonVariants({ variant: "outline" })}>
Cancel
</Dialog.Close>
<Button type="submit">Send request</Button>
</Dialog.Footer>
</form>
<!-- Incoming Requests -->
<Tabs.Content value="incoming">
{#if user.friendRequests.filter((r) => r.toUser === user.id).length === 0}
<p class="text-sm text-muted-foreground">No incoming requests</p>
{:else}
{#each user.friendRequests.filter((r) => r.toUser === user.id) as request (request.id)}
<Card.Root class="mb-2">
<Card.Header>
<Card.Title>{request.username}</Card.Title>
<Card.Description>Sent you a friend request</Card.Description>
</Card.Header>
<Card.Footer class="flex gap-2">
<!-- accept friend -->
<form method="POST" action="?/addFriend">
<input type="hidden" name="userId" value={request.fromUser} />
<Button type="submit" size="sm">Accept</Button>
</form>
<!-- decline friend -->
<form method="POST" action="?/cancelFriendRequest">
<input type="hidden" name="requestId" value={request.id} />
<Button type="submit" variant="outline" size="sm">Decline</Button>
</form>
</Card.Footer>
</Card.Root>
{/each}
{/if}
</Tabs.Content>
</Tabs.Root>
</Dialog.Content>
</Dialog.Root>
<!-- Tabs for Friend Requests -->
<Tabs.Root value="outgoing">
<Tabs.List>
<Tabs.Trigger value="outgoing">Outgoing</Tabs.Trigger>
<Tabs.Trigger value="incoming">Incoming</Tabs.Trigger>
</Tabs.List>
<Dialog.Root>
<Dialog.Trigger>
<Button variant="outline" size="icon">
<UsersRound />
</Button>
</Dialog.Trigger>
<!-- Outgoing Requests -->
<Tabs.Content value="outgoing">
{#if user.friendRequests.filter(r => r.fromUser === user.id).length === 0}
<p class="text-sm text-muted-foreground">No outgoing requests</p>
{:else}
{#each user.friendRequests.filter(r => r.fromUser === user.id) as request (request.id)}
<Card.Root class="mb-2">
<Card.Header>
<Card.Title>{request.username}</Card.Title>
<Card.Description>Request sent</Card.Description>
</Card.Header>
<Card.Footer>
<form method="POST" action="?/cancelFriendRequest">
<input type="hidden" name="requestId" value={request.id} />
<Button type="submit" variant="outline" size="sm">
Cancel
</Button>
</form>
</Card.Footer>
</Card.Root>
{/each}
{/if}
</Tabs.Content>
<Dialog.Content class="sm:max-w-[425px]">
<form method="POST" action="?/createGroup">
<Dialog.Header>
<Dialog.Title>Create a group</Dialog.Title>
<Dialog.Description>Add friends into your group!</Dialog.Description>
</Dialog.Header>
<!-- Incoming Requests -->
<Tabs.Content value="incoming">
{#if user.friendRequests.filter(r => r.toUser === user.id).length === 0}
<p class="text-sm text-muted-foreground">No incoming requests</p>
{:else}
{#each user.friendRequests.filter(r => r.toUser === user.id) as request (request.id)}
<Card.Root class="mb-2">
<Card.Header>
<Card.Title>{request.username}</Card.Title>
<Card.Description>Sent you a friend request</Card.Description>
</Card.Header>
<Card.Footer class="flex gap-2">
<!-- accept friend -->
<form method="POST" action="?/addFriend">
<input type="hidden" name="userId" value={request.fromUser} />
<Button type="submit" size="sm">Accept</Button>
</form>
<!-- decline friend -->
<form method="POST" action="?/cancelFriendRequest">
<input type="hidden" name="requestId" value={request.id} />
<Button type="submit" variant="outline" size="sm">Decline</Button>
</form>
</Card.Footer>
</Card.Root>
{/each}
{/if}
</Tabs.Content>
</Tabs.Root>
</Dialog.Content>
{#each data.friends as friend (friend.id)}
<label class="flex items-center gap-2">
<input type="checkbox" name="member" value={friend.id} />
<User user={friend} />
</label>
{/each}
<Dialog.Footer>
<Dialog.Close class={buttonVariants({ variant: 'outline' })}>Cancel</Dialog.Close>
<Button type="submit">Create group</Button>
</Dialog.Footer>
</form>
</Dialog.Content>
</Dialog.Root>
</Dialog.Root>
<Dialog.Root>
<Dialog.Trigger>
<Button variant="outline" size="icon">
<CirclePlus />
</Button>
</Dialog.Trigger>
<Dialog.Root>
<Dialog.Trigger>
<Button variant="outline" size="icon">
<UsersRound />
</Button>
</Dialog.Trigger>
<Dialog.Content class="sm:max-w-[425px]">
<form method="POST" action="?/joinServer">
<Dialog.Header>
<Dialog.Title>Join a server</Dialog.Title>
<Dialog.Description>Enter an invite link.</Dialog.Description>
</Dialog.Header>
<Dialog.Content class="sm:max-w-[425px]">
<form method="POST" action="?/createGroup">
<Dialog.Header>
<Dialog.Title>Create a group</Dialog.Title>
<Dialog.Description>
Add friends into your group!
</Dialog.Description>
</Dialog.Header>
<Input name="invite" placeholder="invite link" required />
{#each data.friends as friend (friend.id)}
<label class="flex items-center gap-2">
<input
type="checkbox"
name="member"
value={friend.id}
/>
<User user={friend} />
</label>
{/each}
<Dialog.Footer>
<Dialog.Close class={buttonVariants({ variant: 'outline' })}>Cancel</Dialog.Close>
<Button type="submit">Join</Button>
</Dialog.Footer>
</form>
</Dialog.Content>
</Dialog.Root>
<Dialog.Footer>
<Dialog.Close class={buttonVariants({ variant: "outline" })}>
Cancel
</Dialog.Close>
<Button type="submit">Create group</Button>
</Dialog.Footer>
</form>
</Dialog.Content>
</Dialog.Root>
<Dialog.Root>
<Dialog.Trigger>
<Button variant="outline" size="icon">
<PlusIcon />
</Button>
</Dialog.Trigger>
<Dialog.Root>
<Dialog.Trigger>
<Button variant="outline" size="icon">
<CirclePlus />
</Button>
</Dialog.Trigger>
<Dialog.Content class="sm:max-w-[425px]">
<form method="POST" action="?/createServer">
<Dialog.Header>
<Dialog.Title>Create a server</Dialog.Title>
<Dialog.Description>Name your new server.</Dialog.Description>
</Dialog.Header>
<Dialog.Content class="sm:max-w-[425px]">
<form method="POST" action="?/joinServer">
<Dialog.Header>
<Dialog.Title>Join a server</Dialog.Title>
<Dialog.Description>
Enter an invite link.
</Dialog.Description>
</Dialog.Header>
<Input name="invite" placeholder="invite link" required />
<Dialog.Footer>
<Dialog.Close class={buttonVariants({ variant: "outline" })}>
Cancel
</Dialog.Close>
<Button type="submit">Join</Button>
</Dialog.Footer>
</form>
</Dialog.Content>
</Dialog.Root>
<Dialog.Root>
<Dialog.Trigger>
<Button variant="outline" size="icon">
<PlusIcon />
</Button>
</Dialog.Trigger>
<Dialog.Content class="sm:max-w-[425px]">
<form method="POST" action="?/createServer">
<Dialog.Header>
<Dialog.Title>Create a server</Dialog.Title>
<Dialog.Description>
Name your new server.
</Dialog.Description>
</Dialog.Header>
<Input name="name" placeholder="Server name" required />
<Dialog.Footer>
<Dialog.Close class={buttonVariants({ variant: "outline" })}>
Cancel
</Dialog.Close>
<Button type="submit">Create</Button>
</Dialog.Footer>
</form>
</Dialog.Content>
</Dialog.Root>
<Input name="name" placeholder="Server name" required />
<Dialog.Footer>
<Dialog.Close class={buttonVariants({ variant: 'outline' })}>Cancel</Dialog.Close>
<Button type="submit">Create</Button>
</Dialog.Footer>
</form>
</Dialog.Content>
</Dialog.Root>
</div>
</Sidebar.MenuItem>
</Sidebar.Menu>
@ -230,26 +212,24 @@
<Collapsible.Root open={true} class="group/collapsible">
<Sidebar.MenuItem>
<Collapsible.Trigger>
<Sidebar.MenuButton>
Friends
<PlusIcon
class="ms-auto group-data-[state=open]/collapsible:hidden"
/>
<MinusIcon
class="ms-auto group-data-[state=closed]/collapsible:hidden"
/>
</Sidebar.MenuButton>
<Sidebar.MenuButton>
Friends
<PlusIcon class="ms-auto group-data-[state=open]/collapsible:hidden" />
<MinusIcon class="ms-auto group-data-[state=closed]/collapsible:hidden" />
</Sidebar.MenuButton>
</Collapsible.Trigger>
<Collapsible.Content>
<Sidebar.MenuSub>
{#each data.friends as friend (friend.id)}
<Sidebar.MenuSubItem>
<Sidebar.MenuSubButton>
<User onclick={(e) => {
e.preventDefault();
currentPage = friend.id;
}} user={friend}></User>
<User
onclick={(e) => {
e.preventDefault();
currentPage = friend.id;
}}
user={friend}
></User>
</Sidebar.MenuSubButton>
</Sidebar.MenuSubItem>
{/each}
@ -263,12 +243,8 @@
<Collapsible.Trigger>
<Sidebar.MenuButton>
Groups
<PlusIcon
class="ms-auto group-data-[state=open]/collapsible:hidden"
/>
<MinusIcon
class="ms-auto group-data-[state=closed]/collapsible:hidden"
/>
<PlusIcon class="ms-auto group-data-[state=open]/collapsible:hidden" />
<MinusIcon class="ms-auto group-data-[state=closed]/collapsible:hidden" />
</Sidebar.MenuButton>
</Collapsible.Trigger>
<Collapsible.Content>
@ -276,10 +252,13 @@
{#each data.groups as group (group.id)}
<Sidebar.MenuSubItem>
<Sidebar.MenuSubButton>
<a onclick={(e) => {
e.preventDefault();
currentPage = group.id;
}} href="##">
<a
onclick={(e) => {
e.preventDefault();
currentPage = group.id;
}}
href="##"
>
{group.name} ({group.members} members)
</a>
</Sidebar.MenuSubButton>
@ -295,12 +274,8 @@
<Collapsible.Trigger>
<Sidebar.MenuButton>
Servers
<PlusIcon
class="ms-auto group-data-[state=open]/collapsible:hidden"
/>
<MinusIcon
class="ms-auto group-data-[state=closed]/collapsible:hidden"
/>
<PlusIcon class="ms-auto group-data-[state=open]/collapsible:hidden" />
<MinusIcon class="ms-auto group-data-[state=closed]/collapsible:hidden" />
</Sidebar.MenuButton>
</Collapsible.Trigger>
<Collapsible.Content>
@ -308,11 +283,19 @@
{#each data.servers as server (server.id)}
<Sidebar.MenuSubItem>
<Sidebar.MenuSubButton>
<a onclick={(e) => {
e.preventDefault();
currentPage = server.id;
}} href="##" class="flex items-center gap-2">
<img src={"https://api.dicebear.com/7.x/pixel-art/svg?seed=" + server.name} alt={server.name} class="size-6 rounded-full" />
<a
onclick={(e) => {
e.preventDefault();
currentPage = server.id;
}}
href="##"
class="flex items-center gap-2"
>
<img
src={'https://api.dicebear.com/7.x/pixel-art/svg?seed=' + server.name}
alt={server.name}
class="size-6 rounded-full"
/>
{server.name}
</a>
</Sidebar.MenuSubButton>

View file

@ -0,0 +1,9 @@
import { MediaQuery } from "svelte/reactivity";
const DEFAULT_MOBILE_BREAKPOINT = 768;
export class IsMobile extends MediaQuery {
constructor(breakpoint: number = DEFAULT_MOBILE_BREAKPOINT) {
super(`max-width: ${breakpoint - 1}px`);
}
}

View file

@ -1,48 +1,48 @@
import { definePrefix, type Puuid } from "./puuid"
import { definePrefix, type Puuid } from './puuid';
export const UserID = definePrefix("user");
export const GroupID = definePrefix("group");
export const ServerID = definePrefix("srv");
export const FriendRequestID = definePrefix("frq");
export const UserID = definePrefix('user');
export const GroupID = definePrefix('group');
export const ServerID = definePrefix('srv');
export const FriendRequestID = definePrefix('frq');
export type UserId = Puuid<"user">;
export type GroupId = Puuid<"group">;
export type ServerId = Puuid<"srv">;
export type FriendRequestID = Puuid<"frq">;
export type UserId = Puuid<'user'>;
export type GroupId = Puuid<'group'>;
export type ServerId = Puuid<'srv'>;
export type FriendRequestID = Puuid<'frq'>;
export const Status: Record<string, 1|2|3> = {
export const Status: Record<string, 1 | 2 | 3> = {
OFFLINE: 1,
DND: 2,
ONLINE: 3
}
};
export type OverviewUser = {
id: string;
username: string;
image: string;
id: string;
username: string;
image: string;
};
export type OverviewServer = {
id: string;
name: string;
id: string;
name: string;
ownerId: string;
image: string;
};
export type OverviewGroup = {
id: string;
name: string;
id: string;
name: string;
ownerId: string;
members: number;
image: string;
};
export interface OverviewData {
friends: OverviewUser[],
groups: OverviewGroup[],
servers: OverviewServer[],
};
export interface UserWithStatus extends OverviewUser {
status: 1|2|3,
statusMessage: string
status: 1 | 2 | 3;
statusMessage: string;
}
export interface OverviewData {
friends: UserWithStatus[];
groups: OverviewGroup[];
servers: OverviewServer[];
}

View file

@ -1,42 +1,36 @@
import { v4 as uuidv4 } from "uuid";
import { v7 as uuidv7 } from "uuid";
import { v4 as uuidv4 } from 'uuid';
import { v7 as uuidv7 } from 'uuid';
type Brand<K, T> = K & { __brand: T };
export type Puuid<Prefix extends string> = Brand<
string,
{ prefix: Prefix }
>;
export type Puuid<Prefix extends string> = Brand<string, { prefix: Prefix }>;
export function definePrefix<const P extends string>(prefix: P) {
const withPrefix = (uuid: string) =>
`${prefix}_${uuid}` as Puuid<P>;
const withPrefix = (uuid: string) => `${prefix}_${uuid}` as Puuid<P>;
return {
prefix,
is(value: string): value is Puuid<P> {
return value.startsWith(prefix + "_");
},
return {
prefix,
is(value: string): value is Puuid<P> {
return value.startsWith(prefix + '_');
},
newV7(): Puuid<P> {
return withPrefix(uuidv7());
},
newV7(): Puuid<P> {
return withPrefix(uuidv7());
},
newV4(): Puuid<P> {
return withPrefix(uuidv4());
},
newV4(): Puuid<P> {
return withPrefix(uuidv4());
},
parse(value: string): Puuid<P> {
if (!value.startsWith(prefix + "_")) {
throw new Error(
`Invalid prefix, expected "${prefix}_"`
);
}
return value as Puuid<P>;
},
parse(value: string): Puuid<P> {
if (!value.startsWith(prefix + '_')) {
throw new Error(`Invalid prefix, expected "${prefix}_"`);
}
return value as Puuid<P>;
},
inner(id: Puuid<P>): string {
return id.slice(prefix.length + 1);
},
};
inner(id: Puuid<P>): string {
return id.slice(prefix.length + 1);
}
};
}

View file

@ -10,141 +10,151 @@ const DAY_IN_MS = 1000 * 60 * 60 * 24;
export const sessionCookieName = 'auth-session';
export function generateSessionToken() {
const bytes = crypto.getRandomValues(new Uint8Array(18));
const token = encodeBase64url(bytes);
return token;
const bytes = crypto.getRandomValues(new Uint8Array(18));
const token = encodeBase64url(bytes);
return token;
}
export async function createSession(token: string, userId: string) {
const sessionId = encodeHexLowerCase(sha256(new TextEncoder().encode(token)));
const session: table.Session = {
id: sessionId,
userId,
expiresAt: new Date(Date.now() + DAY_IN_MS * 30)
};
await db.insert(table.session).values(session);
return session;
const sessionId = encodeHexLowerCase(sha256(new TextEncoder().encode(token)));
const session: table.Session = {
id: sessionId,
userId,
expiresAt: new Date(Date.now() + DAY_IN_MS * 30)
};
await db.insert(table.session).values(session);
return session;
}
export async function validateSessionToken(token: string) {
const sessionId = encodeHexLowerCase(sha256(new TextEncoder().encode(token)));
const [row] = await db
.select({
user: {
id: table.user.id,
username: table.user.username,
friends: table.user.friends,
servers: table.user.servers,
groups: table.user.groups,
},
session: table.session,
})
.from(table.session)
.innerJoin(table.user, eq(table.session.userId, table.user.id))
.where(eq(table.session.id, sessionId));
const sessionId = encodeHexLowerCase(sha256(new TextEncoder().encode(token)));
const [row] = await db
.select({
user: {
id: table.user.id,
username: table.user.username,
friends: table.user.friends,
servers: table.user.servers,
groups: table.user.groups,
statusOverwrite: table.user.statusOverwrite
},
session: table.session
})
.from(table.session)
.innerJoin(table.user, eq(table.session.userId, table.user.id))
.where(eq(table.session.id, sessionId));
if (!row) {
return { session: null, user: null };
}
const { session, user } = row;
if (!row) {
return { session: null, user: null };
}
const { session, user } = row;
const sessionExpired = Date.now() >= session.expiresAt.getTime();
if (sessionExpired) {
await db.delete(table.session).where(eq(table.session.id, session.id));
return { session: null, user: null };
}
const sessionExpired = Date.now() >= session.expiresAt.getTime();
if (sessionExpired) {
await db.delete(table.session).where(eq(table.session.id, session.id));
return { session: null, user: null };
}
const renewSession = Date.now() >= session.expiresAt.getTime() - DAY_IN_MS * 15;
if (renewSession) {
session.expiresAt = new Date(Date.now() + DAY_IN_MS * 30);
await db
.update(table.session)
.set({ expiresAt: session.expiresAt })
.where(eq(table.session.id, session.id));
}
const friends = (user.friends as string[]).length
? await db
.select({
id: table.user.id,
username: table.user.username,
})
.from(table.user)
.where(inArray(table.user.id, (user.friends as string[])))
: [];
const renewSession = Date.now() >= session.expiresAt.getTime() - DAY_IN_MS * 15;
if (renewSession) {
session.expiresAt = new Date(Date.now() + DAY_IN_MS * 30);
await db
.update(table.session)
.set({ expiresAt: session.expiresAt })
.where(eq(table.session.id, session.id));
}
const friends = (user.friends as string[]).length
? await db
.select({
id: table.user.id,
username: table.user.username
})
.from(table.user)
.where(inArray(table.user.id, user.friends as string[]))
: [];
const servers = (user.servers as string[]).length
? await db
.select({
id: table.server.id,
name: table.server.name,
ownerId: table.server.owner,
})
.from(table.server)
.where(inArray(table.server.id, (user.servers as string[])))
: [];
const groups = (user.groups as string[]).length
? await db
.select({
id: table.group.id,
name: table.group.name,
ownerId: table.group.owner,
members: table.group.members
})
.from(table.group)
.where(inArray(table.group.id, (user.groups as string[])))
: [];
const servers = (user.servers as string[]).length
? await db
.select({
id: table.server.id,
name: table.server.name,
ownerId: table.server.owner
})
.from(table.server)
.where(inArray(table.server.id, user.servers as string[]))
: [];
const groups = (user.groups as string[]).length
? await db
.select({
id: table.group.id,
name: table.group.name,
ownerId: table.group.owner,
members: table.group.members
})
.from(table.group)
.where(inArray(table.group.id, user.groups as string[]))
: [];
const friendRequests = await db
.select({
id: table.friendRequest.id,
fromUser: table.friendRequest.fromUser,
toUser: table.friendRequest.toUser,
})
.from(table.friendRequest)
.where(or(eq(table.friendRequest.fromUser, user.id), eq(table.friendRequest.toUser, user.id)))
const friendRequests = await db
.select({
id: table.friendRequest.id,
fromUser: table.friendRequest.fromUser,
toUser: table.friendRequest.toUser
})
.from(table.friendRequest)
.where(or(eq(table.friendRequest.fromUser, user.id), eq(table.friendRequest.toUser, user.id)));
return { session, user: {...user, servers, friends, groups: groups.map(z => { return { ...z, members: (z.members as string[]).length}}), friendRequests} };
return {
session,
user: {
...user,
servers,
friends,
groups: groups.map((z) => {
return { ...z, members: (z.members as string[]).length };
}),
friendRequests
}
};
}
export type SessionValidationResult = Awaited<ReturnType<typeof validateSessionToken>>;
export async function invalidateSession(sessionId: string) {
await db.delete(table.session).where(eq(table.session.id, sessionId));
await db.delete(table.session).where(eq(table.session.id, sessionId));
}
export function setSessionTokenCookie(event: RequestEvent, token: string, expiresAt: Date) {
event.cookies.set(sessionCookieName, token, {
expires: expiresAt,
path: '/'
});
event.cookies.set(sessionCookieName, token, {
expires: expiresAt,
path: '/'
});
}
export function deleteSessionTokenCookie(event: RequestEvent) {
event.cookies.delete(sessionCookieName, {
path: '/'
});
event.cookies.delete(sessionCookieName, {
path: '/'
});
}
export function validateUsername(username: unknown): username is string {
return (
typeof username === 'string' &&
username.length >= 3 &&
username.length <= 31 &&
/^[a-z0-9_-]+$/.test(username)
);
return (
typeof username === 'string' &&
username.length >= 3 &&
username.length <= 31 &&
/^[a-z0-9_-]+$/.test(username)
);
}
export function validatePassword(password: unknown): password is string {
return typeof password === 'string' && password.length >= 6 && password.length <= 255;
return typeof password === 'string' && password.length >= 6 && password.length <= 255;
}
export function validateEmail(email: unknown): email is string {
return (
typeof email === 'string' &&
email.length >= 5 &&
email.length <= 254 &&
/^[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,}$/i.test(email)
);
return (
typeof email === 'string' &&
email.length >= 5 &&
email.length <= 254 &&
/^[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,}$/i.test(email)
);
}

View file

@ -1,8 +1,8 @@
import { drizzle } from 'drizzle-orm/bun-sqlite';
import { Database } from 'bun:sqlite';
import { BunSqliteKeyValue } from "bun-sqlite-key-value"
import { BunSqliteKeyValue } from 'bun-sqlite-key-value';
const sqlite = new Database('database.db');
export const db = drizzle(sqlite);
export const kvStore = new BunSqliteKeyValue("./kvStore.db")
export const kvStore = new BunSqliteKeyValue('./kvStore.db');

View file

@ -1,14 +1,16 @@
import { Status } from '../../index.ts';
import { sqliteTable, integer, text } from 'drizzle-orm/sqlite-core';
export const user = sqliteTable('user', {
id: text('id').primaryKey(),
username: text('username').notNull().unique(),
email: text('email').notNull().unique(),
passwordHash: text('password_hash').notNull(),
friends: text('friends', { mode: "json"}).default([]).notNull(),
id: text('id').primaryKey(),
username: text('username').notNull().unique(),
email: text('email').notNull().unique(),
passwordHash: text('password_hash').notNull(),
statusOverwrite: integer('status_overwrite').default(Status.ONLINE).notNull(),
friends: text('friends', { mode: 'json' }).default([]).notNull(),
servers: text('servers', { mode: "json"}).default([]).notNull(), // string[] of ServerIDs
groups: text('groups', { mode: "json"}).default([]).notNull(), // string[] of GroupIDs
servers: text('servers', { mode: 'json' }).default([]).notNull(), // string[] of ServerIDs
groups: text('groups', { mode: 'json' }).default([]).notNull() // string[] of GroupIDs
});
export const session = sqliteTable('session', {
@ -19,39 +21,66 @@ export const session = sqliteTable('session', {
expiresAt: integer('expires_at', { mode: 'timestamp' }).notNull()
});
export const server = sqliteTable("server", {
export const server = sqliteTable('server', {
id: text('id').primaryKey(),
name: text('name').notNull(),
owner: text('owner').notNull().references(() => user.id),
members: text('members', { mode: "json"}).default([]).notNull(),
channels: text('channels', { mode: "json"}).default([]).notNull(), // string[] of ChannelIDs
})
owner: text('owner')
.notNull()
.references(() => user.id),
members: text('members', { mode: 'json' }).default([]).notNull(),
channels: text('channels', { mode: 'json' }).default([]).notNull() // string[] of ChannelIDs
});
export const group = sqliteTable("group", {
export const group = sqliteTable('group', {
id: text('id').primaryKey(),
name: text('name').notNull(),
owner: text('owner').notNull().references(() => user.id),
members: text('members', { mode: "json"}).default([]).notNull(),
})
owner: text('owner')
.notNull()
.references(() => user.id),
members: text('members', { mode: 'json' }).default([]).notNull()
});
export const channel = sqliteTable("channel", {
export const channel = sqliteTable('channel', {
id: text('id').primaryKey(),
name: text('name').notNull(),
serverId: text('server_id').notNull().references(() => server.id),
messages: text('messages', { mode: "json"}).default([]).notNull(),
})
serverId: text('server_id')
.notNull()
.references(() => server.id),
messages: text('messages', { mode: 'json' }).default([]).notNull()
});
export const friendRequest = sqliteTable("friendRequest", {
export const directMessage = sqliteTable('directMessage', {
id: text('id').primaryKey(),
fromUser: text('from_user').notNull().references(() => user.id),
toUser: text('to_user').notNull().references(() => user.id),
})
name: text('name').notNull(),
serverId: text('server_id')
.notNull()
.references(() => server.id),
messages: text('messages', { mode: 'json' }).default([]).notNull()
});
export const invite = sqliteTable("invite", {
export const friendRequest = sqliteTable('friendRequest', {
id: text('id').primaryKey(),
serverId: text('server_id').notNull().references(() => server.id),
code: text('code').notNull(),
})
fromUser: text('from_user')
.notNull()
.references(() => user.id),
fromUsername: text('from_username')
.notNull()
.references(() => user.id),
toUsername: text('to_username')
.notNull()
.references(() => user.id),
toUser: text('to_user')
.notNull()
.references(() => user.id)
});
export const invite = sqliteTable('invite', {
id: text('id').primaryKey(),
serverId: text('server_id')
.notNull()
.references(() => server.id),
code: text('code').notNull()
});
export type Session = typeof session.$inferSelect;
export type User = typeof user.$inferSelect;
export type Group = typeof group.$inferSelect;

View file

@ -1,13 +1,13 @@
import { clsx, type ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";
import { clsx, type ClassValue } from 'clsx';
import { twMerge } from 'tailwind-merge';
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
return twMerge(clsx(inputs));
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type WithoutChild<T> = T extends { child?: any } ? Omit<T, "child"> : T;
export type WithoutChild<T> = T extends { child?: any } ? Omit<T, 'child'> : T;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type WithoutChildren<T> = T extends { children?: any } ? Omit<T, "children"> : T;
export type WithoutChildren<T> = T extends { children?: any } ? Omit<T, 'children'> : T;
export type WithoutChildrenOrChild<T> = WithoutChildren<WithoutChild<T>>;
export type WithElementRef<T, U extends HTMLElement = HTMLElement> = T & { ref?: U | null };

View file

@ -1,7 +1,7 @@
<script lang="ts">
import '../app.css';
import favicon from '$lib/assets/favicon.svg';
import { ModeWatcher } from "mode-watcher";
import { ModeWatcher } from 'mode-watcher';
let { children } = $props();
</script>

View file

@ -3,7 +3,7 @@ import { fail, redirect } from '@sveltejs/kit';
import type { PageServerLoad } from './$types';
export const load: PageServerLoad = async (event) => {
if (event.request.url.endsWith("/?logout")) {
if (event.request.url.endsWith('/?logout')) {
if (!event.locals.session) {
return fail(401);
}

View file

@ -1,43 +1,53 @@
<script lang="ts">
import Title from "$lib/components/extra/Title.svelte";
import Button from "$lib/components/ui/button/button.svelte";
import type { PageServerData } from './$types';
import Title from '$lib/components/extra/Title.svelte';
import Button from '$lib/components/ui/button/button.svelte';
import type { PageServerData } from './$types';
let { data }: { data: PageServerData } = $props();
let { data }: { data: PageServerData } = $props();
</script>
<div class="p-6 md:w-1/2">
<Title class="text-5xl"></Title>
<div class="pb-2">A extremely simple chatting application. Supports mobile devices (as a PWA), and also works on almost every single screen size (responsive design!)</div>
<Title class="text-5xl"></Title>
<div class="pb-2">
A extremely simple chatting application. Supports mobile devices (as a PWA), and also works on
almost every single screen size (responsive design!)
</div>
<div class="pb-2">
Featurelist:
<ul class="list-disc pl-6">
<li>Lorem Ipsum is simply dummy text of the printing and typesetting industry. </li>
<li>Lorem Ipsum has been the industry's standard dummy text ever since the 1500s,</li>
<li>when an unknown printer took a galley of type and scrambled it to make a type specimen book.</li>
<li>It has survived not only five centuries, but also the leap into electronic typesetting,</li>
<li>remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages,</li>
</ul>
<div class="pb-2">
Featurelist:
<ul class="list-disc pl-6">
<li>Lorem Ipsum is simply dummy text of the printing and typesetting industry.</li>
<li>Lorem Ipsum has been the industry's standard dummy text ever since the 1500s,</li>
<li>
when an unknown printer took a galley of type and scrambled it to make a type specimen book.
</li>
<li>
It has survived not only five centuries, but also the leap into electronic typesetting,
</li>
<li>
remaining essentially unchanged. It was popularised in the 1960s with the release of
Letraset sheets containing Lorem Ipsum passages,
</li>
</ul>
</div>
<div class="pb-2">
Screenshots
<div class="grid w-76 grid-cols-2 grid-rows-2 gap-2">
<img src="https://placehold.co/150x150" alt="temporary placeholder image" />
<img src="https://placehold.co/150x150" alt="temporary placeholder image" />
<img src="https://placehold.co/150x150" alt="temporary placeholder image" />
<img src="https://placehold.co/150x150" alt="temporary placeholder image" />
</div>
</div>
<div class="pb-2">
Screenshots
<div class="grid grid-cols-2 grid-rows-2 w-76 gap-2">
<img src="https://placehold.co/150x150" alt="temporary placeholder image"/>
<img src="https://placehold.co/150x150" alt="temporary placeholder image"/>
<img src="https://placehold.co/150x150" alt="temporary placeholder image"/>
<img src="https://placehold.co/150x150" alt="temporary placeholder image"/>
</div>
</div>
<div class="flex flex-col gap-3 w-[calc(80%)]">
{#if data.user?.id}
<Button href='/app'>Enter</Button>
<Button href='/?logout'>Log out</Button>
{:else}
<Button href='/login'>Log in</Button> <Button href='/register'>Register</Button>
{/if}
</div>
<div class="flex w-[calc(80%)] flex-col gap-3">
{#if data.user?.id}
<Button href="/app">Enter</Button>
<Button href="/?logout">Log out</Button>
{:else}
<Button href="/login">Log in</Button> <Button href="/register">Register</Button>
{/if}
</div>
</div>

View file

@ -0,0 +1,3 @@
export async function GET() {
return new Response('OK!');
}

View file

@ -2,25 +2,23 @@ import { json } from '@sveltejs/kit';
import type { RequestHandler } from './$types';
export const GET: RequestHandler = async ({ params }) => {
const { groupOrServerId, channelId } = params;
const { groupOrServerId, channelId } = params;
const isGroup = !channelId;
const isGroup = !channelId;
// fake messages
const messages = Array.from({ length: 5 }, (_, i) => ({
id: crypto.randomUUID(),
authorId: `user_${Math.floor(Math.random() * 10)}`,
content: isGroup
? `Group message #${i + 1}`
: `Server message #${i + 1}`,
timestamp: Date.now() - Math.floor(Math.random() * 100000),
}));
// fake messages
const messages = Array.from({ length: 5 }, (_, i) => ({
id: crypto.randomUUID(),
authorId: `user_${Math.floor(Math.random() * 10)}`,
content: isGroup ? `Group message #${i + 1}` : `Server message #${i + 1}`,
timestamp: Date.now() - Math.floor(Math.random() * 100000)
}));
return json({
type: isGroup ? 'group' : 'server',
groupId: isGroup ? groupOrServerId : null,
serverId: isGroup ? null : groupOrServerId,
channelId: channelId ?? null,
messages,
});
return json({
type: isGroup ? 'group' : 'server',
groupId: isGroup ? groupOrServerId : null,
serverId: isGroup ? null : groupOrServerId,
channelId: channelId ?? null,
messages
});
};

View file

@ -1,17 +1,14 @@
import { json } from '@sveltejs/kit';
import type { RequestHandler } from './$types';
import { Status } from '$lib';
import { kvStore } from '$lib/server/db';
export const GET: RequestHandler = async ({ params }) => {
const { userId } = params;
const { userId } = params;
const status = Object.values(Status)[Math.floor(Math.random() * Object.values(Status).length)];
return json({
userId,
status,
lastActive: Date.now() - Math.floor(Math.random() * 600000),
customStatus: Math.random() > 0.5 ? 'vibing 🟢' : null,
});
return json({
userId,
status: kvStore.get('user-' + userId + '-state'),
//@TODO Implement statusmessage
statusMessage: Math.random() > 0.5 ? 'vibing 🟢' : null
});
};

View file

@ -0,0 +1,48 @@
interface SubscribedTo {
subscribed: string[];
userId: string;
controller: ReadableStreamDefaultController;
}
export const _clients = new Map<string, SubscribedTo>();
export function _sendToSubscribers(userId: string, payload: unknown) {
for (const client of _clients) {
if (client[1].subscribed.includes(userId)) {
try {
client[1].controller.enqueue(`data: ${JSON.stringify(payload)}\n\n`);
} catch {
_clients.delete(client[0]);
}
}
}
}
export async function GET({ locals, request }) {
if (!locals.user) {
return new Response('No authentication', { status: 401 });
}
const subscribed = locals.user.friends.map((z) => z.id);
const reqId = crypto.randomUUID();
const stream = new ReadableStream({
start(controller) {
_clients.set(reqId, { subscribed, userId: locals.user!.id, controller });
console.log(`SSE Client opened. ${_clients.size}`);
controller.enqueue(`data: ${JSON.stringify({ type: 'connected' })}\n\n`);
request.signal.addEventListener('abort', () => {
_clients.delete(reqId);
console.log(`SSE Client aborted. ${_clients.size}`);
});
}
});
return new Response(stream, {
headers: {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
Connection: 'keep-alive'
}
});
}

View file

@ -6,18 +6,17 @@ import * as table from '$lib/server/db/schema';
import { FriendRequestID, ServerID } from '$lib';
import { eq } from 'drizzle-orm';
import { and } from 'drizzle-orm';
import { User } from '$lib/server/db/schema';
import { type User } from '$lib/server/db/schema';
export const load: PageServerLoad = async () => {
const user = requireLogin();
return { user };
const user = requireLogin();
return { user };
};
export const actions = {
addFriend: async ({ request, locals }) => {
const data = await request.formData();
const username = data.get('username');
const userId = data.get('userId');
addFriend: async ({ request, locals }) => {
const data = await request.formData();
const username = data.get('username');
const userId = data.get('userId');
if (username && !userId) {
if (typeof username !== 'string' || username.length < 3) {
@ -26,162 +25,193 @@ export const actions = {
}
let user: User[];
if(username) {
user = await db.select().from(table.user).where(eq(table.user.username, username.toString())).limit(1);
} else if(userId) {
user = await db.select().from(table.user).where(eq(table.user.id, userId.toString())).limit(1);
} else {
return fail(400, { error: 'Missing username or userId' });
if (username) {
user = await db
.select()
.from(table.user)
.where(eq(table.user.username, username.toString()))
.limit(1);
} else if (userId) {
user = await db
.select()
.from(table.user)
.where(eq(table.user.id, userId.toString()))
.limit(1);
} else {
return fail(400, { error: 'Missing username or userId' });
}
if (locals.user?.friends.find((z) => z.id == user[0].id)) {
return fail(400, { error: 'Already friends' });
}
if(user?.length == 0)
return fail(400, { error: 'User not found' });
if (user?.length == 0) return fail(400, { error: 'User not found' });
const friendRequest = await db.select()
.from(table.friendRequest)
.where(and(eq(table.friendRequest.fromUser, user[0].id), eq(table.friendRequest.toUser, locals.user!.id)))
const friendRequest = await db
.select()
.from(table.friendRequest)
.where(
and(
eq(table.friendRequest.fromUser, user[0].id),
eq(table.friendRequest.toUser, locals.user!.id)
)
)
.limit(1);
// user has already sent a request to us
// means we want to accept it
//
if(friendRequest?.length != 0) {
await db.delete(table.friendRequest)
.where(and(eq(table.friendRequest.fromUser, user[0].id), eq(table.friendRequest.toUser, locals.user!.id)))
// user has already sent a request to us
// means we want to accept it
//
if (friendRequest?.length != 0) {
await db
.delete(table.friendRequest)
.where(
and(
eq(table.friendRequest.fromUser, user[0].id),
eq(table.friendRequest.toUser, locals.user!.id)
)
)
.limit(1);
// add other guy to us
await db.transaction(async (tx) => {
await tx.update(table.user)
.set({ friends: locals.user?.friends.map(z => z.id).concat(user[0].id) })
.where(eq(table.user.id, locals.user!.id));
// add other guy to us
await db.transaction(async (tx) => {
await tx
.update(table.user)
.set({ friends: locals.user?.friends.map((z) => z.id).concat(user[0].id) })
.where(eq(table.user.id, locals.user!.id));
await tx.update(table.user)
.set({ friends: (user[0].friends as string[]).concat(locals.user!.id) })
.where(eq(table.user.id, user[0].id));
});
await tx
.update(table.user)
.set({ friends: (user[0].friends as string[]).concat(locals.user!.id) })
.where(eq(table.user.id, user[0].id));
});
return {success: true}
}
return { success: true };
}
// a request from us has already been sent to user
if(locals.user?.friendRequests.find(z => z.toUser == user[0].id && z.fromUser == locals.user!.id))
return fail(400, { error: 'Already sent request' });
// a request from us has already been sent to user
if (
locals.user?.friendRequests.find(
(z) => z.toUser == user[0].id && z.fromUser == locals.user!.id
)
)
return fail(400, { error: 'Already sent request' });
await db.insert(table.friendRequest).values({
id: FriendRequestID.newV4(),
fromUser: locals.user!.id,
toUser: user[0].id,
});
await db.insert(table.friendRequest).values({
id: FriendRequestID.newV4(),
fromUser: locals.user!.id,
toUser: user[0].id
});
return { success: true };
},
cancelFriendRequest: async ({ request, locals }) => {
const data = await request.formData();
const requestId = data.get('requestId');
return { success: true };
},
cancelFriendRequest: async ({ request, locals }) => {
const data = await request.formData();
const requestId = data.get('requestId');
if (typeof requestId !== 'string') {
return fail(400, { error: 'Invalid request ID' });
}
if (typeof requestId !== 'string') {
return fail(400, { error: 'Invalid request ID' });
}
// fetch the friend request
const friendRequest = await db.select()
.from(table.friendRequest)
.where(eq(table.friendRequest.id, requestId))
.limit(1);
// fetch the friend request
const friendRequest = await db
.select()
.from(table.friendRequest)
.where(eq(table.friendRequest.id, requestId))
.limit(1);
if (!friendRequest?.length) {
return fail(404, { error: 'Friend request not found' });
}
if (!friendRequest?.length) {
return fail(404, { error: 'Friend request not found' });
}
const fr = friendRequest[0];
const fr = friendRequest[0];
// only allow cancelling if it's related to current user
if (fr.fromUser !== locals.user!.id && fr.toUser !== locals.user!.id) {
return fail(403, { error: 'Not allowed' });
}
// only allow cancelling if it's related to current user
if (fr.fromUser !== locals.user!.id && fr.toUser !== locals.user!.id) {
return fail(403, { error: 'Not allowed' });
}
// delete the request
await db.delete(table.friendRequest)
.where(eq(table.friendRequest.id, requestId))
.limit(1);
// delete the request
await db.delete(table.friendRequest).where(eq(table.friendRequest.id, requestId)).limit(1);
return { success: true };
},
createGroup: async ({ request, locals }) => {
const data = await request.formData();
const members = data.getAll('member');
return { success: true };
},
createGroup: async ({ request, locals }) => {
const data = await request.formData();
const members = data.getAll('member');
if (!members.length) {
return fail(400, { error: 'No members selected' });
}
if (!members.length) {
return fail(400, { error: 'No members selected' });
}
console.log(data, members, locals)
return { success: true };
},
return { success: true };
},
joinServer: async ({ request, locals }) => {
const data = await request.formData();
const invite = data.get('invite');
joinServer: async ({ request, locals }) => {
const data = await request.formData();
const invite = data.get('invite');
if (typeof invite !== 'string') {
return fail(400, { error: 'Invalid invite' });
}
if (typeof invite !== 'string') {
return fail(400, { error: 'Invalid invite' });
}
const inv = await db.select().from(table.invite).where(eq(table.invite.code, invite)).limit(1);
const inv = await db.select().from(table.invite).where(eq(table.invite.code, invite)).limit(1);
if (inv?.length == 0) return fail(400, { error: 'Invalid invite' });
if(inv?.length == 0)
return fail(400, { error: 'Invalid invite' });
const server = await db
.select()
.from(table.server)
.where(eq(table.server.id, inv[0].serverId))
.limit(1);
const server = await db.select().from(table.server).where(eq(table.server.id, inv[0].serverId)).limit(1);
if (server?.length == 0) return fail(400, { error: 'Invalid server' });
if(server?.length == 0)
return fail(400, { error: 'Invalid server' });
if(locals.user!.servers.some(z => z.id == server[0].id))
return fail(400, { error: 'Already in server' });
if (locals.user!.servers.some((z) => z.id == server[0].id))
return fail(400, { error: 'Already in server' });
await db.transaction(async (tx) => {
await tx.update(table.user)
.set({servers: locals.user!.servers.map(z => z.id).concat([server[0].id])})
.where(eq(table.user.id, locals.user!.id));
await tx
.update(table.user)
.set({ servers: locals.user!.servers.map((z) => z.id).concat([server[0].id]) })
.where(eq(table.user.id, locals.user!.id));
await tx.update(table.server)
.set({members: (server[0].members as string[]).concat([locals.user!.id])})
.where(eq(table.server.id, server[0].id));
})
await tx
.update(table.server)
.set({ members: (server[0].members as string[]).concat([locals.user!.id]) })
.where(eq(table.server.id, server[0].id));
});
return { success: true };
},
return { success: true };
},
createServer: async ({ request, locals }) => {
const data = await request.formData();
const name = data.get('name');
createServer: async ({ request, locals }) => {
const data = await request.formData();
const name = data.get('name');
if (typeof name !== 'string' || name.length < 3) {
return fail(400, { error: 'Server name too short' });
}
if (typeof name !== 'string' || name.length < 3) {
return fail(400, { error: 'Server name too short' });
}
const serverId = ServerID.newV4();
await db.insert(table.server)
.values({id: serverId, name, owner: locals.user!.id, members: [ locals.user!.id ]});
await db
.insert(table.server)
.values({ id: serverId, name, owner: locals.user!.id, members: [locals.user!.id] });
await db.update(table.user)
.set({servers: locals.user!.servers.map(z => z.id).concat([serverId])})
.where(eq(table.user.id, locals.user!.id));
await db
.update(table.user)
.set({ servers: locals.user!.servers.map((z) => z.id).concat([serverId]) })
.where(eq(table.user.id, locals.user!.id));
redirect(303, `/app`);
}
redirect(303, `/app`);
}
} satisfies Actions;
function requireLogin() {
const { locals } = getRequestEvent();
const { locals } = getRequestEvent();
if (!locals.user) {
return redirect(302, '/login');
}
if (!locals.user) {
return redirect(302, '/login');
}
return locals.user;
return locals.user;
}

View file

@ -1,18 +1,27 @@
<script lang="ts">
import { Status, type OverviewData,
GroupID, UserID, ServerID,
type GroupId, type ServerId, type UserId,
type OverviewUser, type OverviewGroup, type OverviewServer,
type UserWithStatus} from "$lib";
import {
Status,
type OverviewData,
GroupID,
UserID,
ServerID,
type GroupId,
type ServerId,
type UserId,
type OverviewUser,
type OverviewGroup,
type OverviewServer,
type UserWithStatus
} from '$lib';
import type { PageServerData } from './$types';
import AppSidebar from "$lib/components/app-sidebar.svelte";
import * as Sidebar from "$lib/components/ui/sidebar/index.js";
import * as AlertDialog from "$lib/components/ui/alert-dialog/index.js";
import { onMount } from "svelte";
import type { ActionData } from './$types';
import AppSidebar from '$lib/components/app-sidebar.svelte';
import * as Sidebar from '$lib/components/ui/sidebar/index.js';
import * as AlertDialog from '$lib/components/ui/alert-dialog/index.js';
import { onMount } from 'svelte';
import type { ActionData } from './$types';
let errorOpen = $state(true);
let { form, data }: { form: ActionData, data: PageServerData } = $props();
let currentPageID: (UserId|GroupId|ServerId)|null = $state(null);
let { form, data }: { form: ActionData; data: PageServerData } = $props();
let currentPageID: (UserId | GroupId | ServerId) | null = $state(null);
let currentPage: OverviewUser | OverviewGroup | OverviewServer | undefined = $state();
const overview_data: OverviewData = $state({
@ -21,84 +30,101 @@
servers: []
});
console.log(form, data, overview_data)
$effect(() => {
if (currentPageID) {
if(UserID.is(currentPageID)) {
currentPage = overview_data.friends.find(friend => friend.id === currentPageID);
} else if(GroupID.is(currentPageID)) {
currentPage = overview_data.groups.find(group => group.id === currentPageID);
} else if(ServerID.is(currentPageID)) {
currentPage = overview_data.servers.find(server => server.id === currentPageID);
if (UserID.is(currentPageID)) {
currentPage = overview_data.friends.find((friend) => friend.id === currentPageID);
} else if (GroupID.is(currentPageID)) {
currentPage = overview_data.groups.find((group) => group.id === currentPageID);
} else if (ServerID.is(currentPageID)) {
currentPage = overview_data.servers.find((server) => server.id === currentPageID);
}
} else {
currentPage = undefined;
}
});
onMount(()=>{
onMount(() => {
async function run() {
overview_data.servers = data.user.servers.map(z => {
overview_data.servers = data.user.servers.map((z) => {
return {
id: ServerID.parse(z.id),
name: z.name,
ownerId: z.ownerId,
image: "https://api.dicebear.com/7.x/pixel-art/svg?seed=" + z.name,
image: 'https://api.dicebear.com/7.x/pixel-art/svg?seed=' + z.name
};
})
overview_data.groups = data.user.groups.map(z => {
});
overview_data.groups = data.user.groups.map((z) => {
return {
id: GroupID.parse(z.id),
name: z.name,
ownerId: z.ownerId,
members: z.members,
image: "https://api.dicebear.com/7.x/pixel-art/svg?seed=" + z.name,
image: 'https://api.dicebear.com/7.x/pixel-art/svg?seed=' + z.name
};
})
});
overview_data.friends = await Promise.all(
data.user.friends.map(async (friend) => {
const res = await fetch(`/api/status/${friend.id}`);
data.user.friends.map(async (friend) => {
const res = await fetch(`/api/status/${friend.id}`);
if (!res.ok) {
throw new Error(`Failed to fetch status for ${friend.id}`);
}
if (!res.ok) {
throw new Error(`Failed to fetch status for ${friend.id}`);
}
const status = await res.json();
const status = await res.json();
return {
id: UserID.parse(friend.id),
username: friend.username,
status: status.status,
customStatus: status.customStatus,
image: "https://api.dicebear.com/7.x/pixel-art/svg?seed=" + friend.username
};
})
return {
id: UserID.parse(friend.id),
username: friend.username,
status: status.status,
statusMessage: status.statusMessage,
image: 'https://api.dicebear.com/7.x/pixel-art/svg?seed=' + friend.username
};
})
);
}
run();
});
onMount(() => {
const sse = new EventSource('/api/updates');
sse.addEventListener('message', (e) => {
const json = JSON.parse(e.data) as
| { type: 'connected' }
| { type: 'status'; id: string; status: 1 | 2 | 3 };
if (json.type == 'connected') {
console.log('SSE connected.');
}
if (json.type == 'status') {
const friend = overview_data.friends.find((z) => z.id == json.id);
if (friend) {
friend.status = json.status;
}
}
});
});
</script>
{#if form}
<AlertDialog.Root bind:open={errorOpen}>
<AlertDialog.Content>
<AlertDialog.Header>
<AlertDialog.Title>{form?.error ? "Ran into an error." : "Success!"}</AlertDialog.Title>
<AlertDialog.Description>
{form?.error || "Action completed succesfully."}
</AlertDialog.Description>
</AlertDialog.Header>
<AlertDialog.Footer>
<AlertDialog.Cancel>Close</AlertDialog.Cancel>
</AlertDialog.Footer>
</AlertDialog.Content>
</AlertDialog.Root>
<AlertDialog.Root bind:open={errorOpen}>
<AlertDialog.Content>
<AlertDialog.Header>
<AlertDialog.Title>{form?.error ? 'Ran into an error.' : 'Success!'}</AlertDialog.Title>
<AlertDialog.Description>
{form?.error || 'Action completed succesfully.'}
</AlertDialog.Description>
</AlertDialog.Header>
<AlertDialog.Footer>
<AlertDialog.Cancel>Close</AlertDialog.Cancel>
</AlertDialog.Footer>
</AlertDialog.Content>
</AlertDialog.Root>
{/if}
<Sidebar.Provider>
<AppSidebar bind:currentPage={currentPageID} user={data.user} data={overview_data} />
@ -107,24 +133,32 @@
<Sidebar.Trigger class="-ms-1" />
{#if currentPageID && currentPage}
{#if ServerID.is(currentPageID)}
{@const server = (currentPage as OverviewServer)}
{@const server = currentPage as OverviewServer}
<img src={server!.image} alt={server!.name} class="size-6 rounded-full" />
<img src={server!.image} alt={server!.name} class="size-6 rounded-full" />
<h1>{server!.name}</h1>
<h1>{server!.name}</h1>
{:else if UserID.is(currentPageID)}
{@const friend = (currentPage as UserWithStatus)}
{@const friend = currentPage as UserWithStatus}
<img src={friend.image} alt={friend!.username} class="size-6 rounded-full" />
<img src={friend.image} alt={friend!.username} class="size-6 rounded-full" />
<h1>{friend!.username} [{friend.status == Status.ONLINE ? "Online!" : friend.status == Status.DND ? "DND" : friend.status == Status.OFFLINE ? "Offline" : "Unknown"}]</h1>
<h1>
{friend!.username} [{friend.status == Status.ONLINE
? 'Online!'
: friend.status == Status.DND
? 'DND'
: friend.status == Status.OFFLINE
? 'Offline'
: 'Unknown'}]
</h1>
{:else if GroupID.is(currentPageID)}
{@const group = (currentPage as OverviewGroup)}
{@const group = currentPage as OverviewGroup}
<h1>{group!.name} ({group.members} member{group.members > 1 ? "s" : ""})</h1>
<h1>{group!.name} ({group.members} member{group.members > 1 ? 's' : ''})</h1>
{/if}
{/if}
</header>
<h1> this is like lowkirkounely the content, i should put messages and shi here</h1>
<h1>this is like lowkirkounely the content, i should put messages and shi here</h1>
</Sidebar.Inset>
</Sidebar.Provider>

View file

@ -7,56 +7,59 @@ import * as table from '$lib/server/db/schema';
import type { Actions, PageServerLoad } from './$types';
export const load: PageServerLoad = async (event) => {
if (event.locals.user) {
return redirect(302, '/demo/lucia');
}
return {};
if (event.locals.user) {
return redirect(302, '/demo/lucia');
}
return {};
};
export const actions: Actions = {
login: async (event) => {
const formData = await event.request.formData();
const username = formData.get('username');
const password = formData.get('password');
login: async (event) => {
const formData = await event.request.formData();
const username = formData.get('username');
const password = formData.get('password');
let username_is_email = false;
if (!auth.validateUsername(username)) {
if (!auth.validateUsername(username)) {
if (auth.validateEmail(username)) {
username_is_email = true;
} else {
return fail(400, {
message: 'Invalid username (min 3, max 31 characters, alphanumeric only)'
});
return fail(400, {
message: 'Invalid username (min 3, max 31 characters, alphanumeric only)'
});
}
}
if (!auth.validatePassword(password)) {
return fail(400, { message: 'Invalid password (min 6, max 255 characters)' });
}
}
if (!auth.validatePassword(password)) {
return fail(400, { message: 'Invalid password (min 6, max 255 characters)' });
}
const results = await db.select()
.from(table.user)
.where(username_is_email ? eq(table.user.email, username) : eq(table.user.username, username));
const results = await db
.select()
.from(table.user)
.where(
username_is_email ? eq(table.user.email, username) : eq(table.user.username, username)
);
const existingUser = results.at(0);
if (!existingUser) {
return fail(400, { message: 'Incorrect username or password' });
}
const existingUser = results.at(0);
if (!existingUser) {
return fail(400, { message: 'Incorrect username or password' });
}
const validPassword = await verify(existingUser.passwordHash, password, {
memoryCost: 19456,
timeCost: 2,
outputLen: 32,
parallelism: 1
});
if (!validPassword) {
return fail(400, { message: 'Incorrect username or password' });
}
const validPassword = await verify(existingUser.passwordHash, password, {
memoryCost: 19456,
timeCost: 2,
outputLen: 32,
parallelism: 1
});
if (!validPassword) {
return fail(400, { message: 'Incorrect username or password' });
}
const sessionToken = auth.generateSessionToken();
const session = await auth.createSession(sessionToken, existingUser.id);
auth.setSessionTokenCookie(event, sessionToken, session.expiresAt);
const sessionToken = auth.generateSessionToken();
const session = await auth.createSession(sessionToken, existingUser.id);
auth.setSessionTokenCookie(event, sessionToken, session.expiresAt);
return redirect(302, '/app');
}
return redirect(302, '/app');
}
};

View file

@ -1,26 +1,24 @@
<script lang="ts">
import Title from "$lib/components/extra/Title.svelte";
import Button from "$lib/components/ui/button/button.svelte";
import Input from "$lib/components/ui/input/input.svelte";
import type { ActionData } from './$types';
import Title from '$lib/components/extra/Title.svelte';
import Button from '$lib/components/ui/button/button.svelte';
import Input from '$lib/components/ui/input/input.svelte';
import type { ActionData } from './$types';
let { form }: { form: ActionData } = $props();
let { form }: { form: ActionData } = $props();
</script>
<div class="flex min-h-screen flex-col items-center justify-center gap-2">
<div class='w-1/2 p-2 bg-secondary rounded-md '>
<div class="text-center pb-2">
<Title class="text-2xl"></Title>
</div>
<form method="post" action="?/login">
<Input name="username" class="mb-2" type="text" placeholder="Username or E-mail"></Input>
<Input name="password" class="mb-2" type="password" placeholder="Password"></Input>
<Button type="submit" formaction="?/login" class="w-full">Login</Button>
</form>
<p style="color: red">{form?.message ?? ''}</p>
<div class="w-1/2 rounded-md bg-secondary p-2">
<div class="pb-2 text-center">
<Title class="text-2xl"></Title>
</div>
<form method="post" action="?/login">
<Input name="username" class="mb-2" type="text" placeholder="Username or E-mail"></Input>
<Input name="password" class="mb-2" type="password" placeholder="Password"></Input>
<Button type="submit" formaction="?/login" class="w-full">Login</Button>
</form>
<p style="color: red">{form?.message ?? ''}</p>
</div>
</div>

View file

@ -9,59 +9,58 @@ import { eq } from 'drizzle-orm';
import { UserID } from '$lib';
export const load: PageServerLoad = async (event) => {
if (event.locals.user) {
return redirect(302, '/demo/lucia');
}
return {};
if (event.locals.user) {
return redirect(302, '/demo/lucia');
}
return {};
};
export const actions: Actions = {
register: async (event) => {
const formData = await event.request.formData();
const username = formData.get('username');
const email = formData.get('email');
const password = formData.get('password');
register: async (event) => {
const formData = await event.request.formData();
const username = formData.get('username');
const email = formData.get('email');
const password = formData.get('password');
if (!auth.validateUsername(username)) {
return fail(400, { message: 'Invalid username' });
}
if (!auth.validateUsername(username)) {
return fail(400, { message: 'Invalid username' });
}
if(!auth.validateEmail(email)) {
return fail(400, { message: 'Invalid email' });
}
if (!auth.validateEmail(email)) {
return fail(400, { message: 'Invalid email' });
}
if (!auth.validatePassword(password)) {
return fail(400, { message: 'Invalid password' });
}
if (!auth.validatePassword(password)) {
return fail(400, { message: 'Invalid password' });
}
const userId = UserID.newV4();
const passwordHash = await hash(password, {
// recommended minimum parameters
memoryCost: 19456,
timeCost: 2,
outputLen: 32,
parallelism: 1
});
const results = await db.select()
.from(table.user)
.where(or(eq(table.user.email, email),
eq(table.user.username, username)));
const passwordHash = await hash(password, {
// recommended minimum parameters
memoryCost: 19456,
timeCost: 2,
outputLen: 32,
parallelism: 1
});
const results = await db
.select()
.from(table.user)
.where(or(eq(table.user.email, email), eq(table.user.username, username)));
const existingUser = results.at(0);
if (existingUser) {
return fail(400, { message: 'Username or email already registered!' });
}
const existingUser = results.at(0);
if (existingUser) {
return fail(400, { message: 'Username or email already registered!' });
}
try {
await db.insert(table.user)
.values({ id: userId, email, username, passwordHash });
try {
await db.insert(table.user).values({ id: userId, email, username, passwordHash });
const sessionToken = auth.generateSessionToken();
const session = await auth.createSession(sessionToken, userId);
auth.setSessionTokenCookie(event, sessionToken, session.expiresAt);
} catch {
return fail(500, { message: 'An error has occurred' });
}
return redirect(302, '/app');
}
const sessionToken = auth.generateSessionToken();
const session = await auth.createSession(sessionToken, userId);
auth.setSessionTokenCookie(event, sessionToken, session.expiresAt);
} catch {
return fail(500, { message: 'An error has occurred' });
}
return redirect(302, '/app');
}
};

View file

@ -1,25 +1,25 @@
<script lang="ts">
import Title from "$lib/components/extra/Title.svelte";
import Button from "$lib/components/ui/button/button.svelte";
import Input from "$lib/components/ui/input/input.svelte";
import type { ActionData } from './$types';
import Title from '$lib/components/extra/Title.svelte';
import Button from '$lib/components/ui/button/button.svelte';
import Input from '$lib/components/ui/input/input.svelte';
import type { ActionData } from './$types';
let { form }: { form: ActionData } = $props();
let { form }: { form: ActionData } = $props();
</script>
<div class="flex min-h-screen flex-col items-center justify-center gap-2">
<div class='w-1/2 p-2 bg-secondary rounded-md '>
<div class="text-center pb-2">
<Title class="text-2xl"></Title>
</div>
<form method="post" action="?/register">
<Input name="username" class="mb-2" type="text" placeholder="Username"></Input>
<Input name="email" class="mb-2" type="email" placeholder="E-mail"></Input>
<Input name="password" class="mb-2" type="password" placeholder="Password"></Input>
<Button type="submit" formaction="?/register" class="w-full">Register</Button>
</form>
<p style="color: red">{form?.message ?? ''}</p>
<div class="w-1/2 rounded-md bg-secondary p-2">
<div class="pb-2 text-center">
<Title class="text-2xl"></Title>
</div>
<form method="post" action="?/register">
<Input name="username" class="mb-2" type="text" placeholder="Username"></Input>
<Input name="email" class="mb-2" type="email" placeholder="E-mail"></Input>
<Input name="password" class="mb-2" type="password" placeholder="Password"></Input>
<Button type="submit" formaction="?/register" class="w-full">Register</Button>
</form>
<p style="color: red">{form?.message ?? ''}</p>
</div>
</div>