From 090b3c10d18171710f83deb8e93921bb17b9ab52 Mon Sep 17 00:00:00 2001 From: Sebastian Brenner Date: Sun, 4 Jan 2026 12:37:46 +0100 Subject: [PATCH] update eslint and refactor backend --- bun.lock | 57 ++++++++++---- data.sqlite | Bin 0 -> 49152 bytes eslint.config.js | 54 +++---------- package.json | 14 ++-- src/client/App.tsx | 15 +++- src/client/Store.ts | 39 ++++++++++ src/client/components/Group.tsx | 64 +++++++++++++++ src/client/components/SignIn.tsx | 107 ++++++++++++++----------- src/client/frontend.tsx | 13 +++- src/client/serverApi.ts | 36 ++++++++- src/index.ts | 51 ++++++++++-- src/interfaces.ts | 11 +++ src/server/db.ts | 129 +++++++++++++++++++++++++------ 13 files changed, 438 insertions(+), 152 deletions(-) create mode 100644 data.sqlite create mode 100644 src/client/Store.ts create mode 100644 src/client/components/Group.tsx diff --git a/bun.lock b/bun.lock index 0a19423..d5b46a6 100644 --- a/bun.lock +++ b/bun.lock @@ -9,24 +9,19 @@ "@fontsource/roboto": "5.2.8", "@mui/icons-material": "7.3.5", "@mui/material": "7.3.5", + "cookie": "^1.0.2", + "mobx-react-lite": "^4.1.1", "react": "19", "react-dom": "19", "react-router-dom": "^7.9.5", }, "devDependencies": { - "@eslint/js": "9.39.1", - "@stylistic/eslint-plugin": "5.5.0", + "@sebastianbrenner/eslint-config": "git+https://gitea.sebastianbrenner.dev/sebastianbrenner/eslint-config.git/#1.2.0", "@types/bun": "1.3.1", "@types/react": "19", "@types/react-dom": "19", - "eslint": "9.39.1", - "eslint-plugin-jsx-a11y": "6.10.2", - "eslint-plugin-react": "7.37.5", - "eslint-plugin-react-hooks": "7.0.1", - "eslint-plugin-unused-imports": "^4.3.0", "prettier": "3.6.2", "typescript": "5.9.3", - "typescript-eslint": "8.46.3", }, }, }, @@ -153,6 +148,10 @@ "@popperjs/core": ["@popperjs/core@2.11.8", "", {}, "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A=="], + "@rtsao/scc": ["@rtsao/scc@1.1.0", "", {}, "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g=="], + + "@sebastianbrenner/eslint-config": ["@sebastianbrenner/eslint-config@git+https://gitea.sebastianbrenner.dev/sebastianbrenner/eslint-config.git/#98e16522c315347de4203ffcbe0fff554f22c370", { "peerDependencies": { "@stylistic/eslint-plugin": "1.7.0", "@typescript-eslint/parser": "8.48.1", "eslint": "^9.0.0", "eslint-plugin-import": "2.32.0", "eslint-plugin-jsx-a11y": "6.10.2", "eslint-plugin-react": "7.37.5", "eslint-plugin-react-hooks": "7.0.1", "eslint-plugin-unused-imports": "4.3.0", "typescript": "5" } }, "98e16522c315347de4203ffcbe0fff554f22c370"], + "@stylistic/eslint-plugin": ["@stylistic/eslint-plugin@5.5.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.0", "@typescript-eslint/types": "^8.46.1", "eslint-visitor-keys": "^4.2.1", "espree": "^10.4.0", "estraverse": "^5.3.0", "picomatch": "^4.0.3" }, "peerDependencies": { "eslint": ">=9.0.0" } }, "sha512-IeZF+8H0ns6prg4VrkhgL+yrvDXWDH2cKchrbh80ejG9dQgZWp10epHMbgRuQvgchLII/lfh6Xn3lu6+6L86Hw=="], "@types/bun": ["@types/bun@1.3.1", "", { "dependencies": { "bun-types": "1.3.1" } }, "sha512-4jNMk2/K9YJtfqwoAa28c8wK+T7nvJFOjxI4h/7sORWcypRNxBpr+TPNaCfVWq70tLCJsqoFwcf0oI0JU/fvMQ=="], @@ -161,6 +160,8 @@ "@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="], + "@types/json5": ["@types/json5@0.0.29", "", {}, "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ=="], + "@types/node": ["@types/node@24.10.0", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-qzQZRBqkFsYyaSWXuEHc2WR9c0a0CXwiE5FWUvn7ZM+vdy1uZLfCunD38UzhuB7YN/J11ndbDBcTmOdxJo9Q7A=="], "@types/parse-json": ["@types/parse-json@4.0.2", "", {}, "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw=="], @@ -211,6 +212,8 @@ "array.prototype.findlast": ["array.prototype.findlast@1.2.5", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-abstract": "^1.23.2", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "es-shim-unscopables": "^1.0.2" } }, "sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ=="], + "array.prototype.findlastindex": ["array.prototype.findlastindex@1.2.6", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.4", "define-properties": "^1.2.1", "es-abstract": "^1.23.9", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "es-shim-unscopables": "^1.1.0" } }, "sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ=="], + "array.prototype.flat": ["array.prototype.flat@1.3.3", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.5", "es-shim-unscopables": "^1.0.2" } }, "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg=="], "array.prototype.flatmap": ["array.prototype.flatmap@1.3.3", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.5", "es-shim-unscopables": "^1.0.2" } }, "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg=="], @@ -263,7 +266,7 @@ "concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="], - "convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="], + "convert-source-map": ["convert-source-map@1.9.0", "", {}, "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A=="], "cookie": ["cookie@1.0.2", "", {}, "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA=="], @@ -323,6 +326,12 @@ "eslint": ["eslint@9.39.1", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.21.1", "@eslint/config-helpers": "^0.4.2", "@eslint/core": "^0.17.0", "@eslint/eslintrc": "^3.3.1", "@eslint/js": "9.39.1", "@eslint/plugin-kit": "^0.4.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.4.0", "eslint-visitor-keys": "^4.2.1", "espree": "^10.4.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g=="], + "eslint-import-resolver-node": ["eslint-import-resolver-node@0.3.9", "", { "dependencies": { "debug": "^3.2.7", "is-core-module": "^2.13.0", "resolve": "^1.22.4" } }, "sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g=="], + + "eslint-module-utils": ["eslint-module-utils@2.12.1", "", { "dependencies": { "debug": "^3.2.7" } }, "sha512-L8jSWTze7K2mTg0vos/RuLRS5soomksDPoJLXIslC7c8Wmut3bx7CPpJijDcBZtxQ5lrbUdM+s0OlNbz0DCDNw=="], + + "eslint-plugin-import": ["eslint-plugin-import@2.32.0", "", { "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", "array.prototype.findlastindex": "^1.2.6", "array.prototype.flat": "^1.3.3", "array.prototype.flatmap": "^1.3.3", "debug": "^3.2.7", "doctrine": "^2.1.0", "eslint-import-resolver-node": "^0.3.9", "eslint-module-utils": "^2.12.1", "hasown": "^2.0.2", "is-core-module": "^2.16.1", "is-glob": "^4.0.3", "minimatch": "^3.1.2", "object.fromentries": "^2.0.8", "object.groupby": "^1.0.3", "object.values": "^1.2.1", "semver": "^6.3.1", "string.prototype.trimend": "^1.0.9", "tsconfig-paths": "^3.15.0" }, "peerDependencies": { "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9" } }, "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA=="], + "eslint-plugin-jsx-a11y": ["eslint-plugin-jsx-a11y@6.10.2", "", { "dependencies": { "aria-query": "^5.3.2", "array-includes": "^3.1.8", "array.prototype.flatmap": "^1.3.2", "ast-types-flow": "^0.0.8", "axe-core": "^4.10.0", "axobject-query": "^4.1.0", "damerau-levenshtein": "^1.0.8", "emoji-regex": "^9.2.2", "hasown": "^2.0.2", "jsx-ast-utils": "^3.3.5", "language-tags": "^1.0.9", "minimatch": "^3.1.2", "object.fromentries": "^2.0.8", "safe-regex-test": "^1.0.3", "string.prototype.includes": "^2.0.1" }, "peerDependencies": { "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9" } }, "sha512-scB3nz4WmG75pV8+3eRUQOHZlNSUhFNq37xnpgRkCCELU3XMvXAxLk1eqWWyE22Ki4Q01Fnsw9BA3cJHDPgn2Q=="], "eslint-plugin-react": ["eslint-plugin-react@7.37.5", "", { "dependencies": { "array-includes": "^3.1.8", "array.prototype.findlast": "^1.2.5", "array.prototype.flatmap": "^1.3.3", "array.prototype.tosorted": "^1.1.4", "doctrine": "^2.1.0", "es-iterator-helpers": "^1.2.1", "estraverse": "^5.3.0", "hasown": "^2.0.2", "jsx-ast-utils": "^2.4.1 || ^3.0.0", "minimatch": "^3.1.2", "object.entries": "^1.1.9", "object.fromentries": "^2.0.8", "object.values": "^1.2.1", "prop-types": "^15.8.1", "resolve": "^2.0.0-next.5", "semver": "^6.3.1", "string.prototype.matchall": "^4.0.12", "string.prototype.repeat": "^1.0.0" }, "peerDependencies": { "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7" } }, "sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA=="], @@ -495,7 +504,7 @@ "json-stable-stringify-without-jsonify": ["json-stable-stringify-without-jsonify@1.0.1", "", {}, "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="], - "json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="], + "json5": ["json5@1.0.2", "", { "dependencies": { "minimist": "^1.2.0" }, "bin": { "json5": "lib/cli.js" } }, "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA=="], "jsx-ast-utils": ["jsx-ast-utils@3.3.5", "", { "dependencies": { "array-includes": "^3.1.6", "array.prototype.flat": "^1.3.1", "object.assign": "^4.1.4", "object.values": "^1.1.6" } }, "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ=="], @@ -525,6 +534,12 @@ "minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], + "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], + + "mobx": ["mobx@6.15.0", "", {}, "sha512-UczzB+0nnwGotYSgllfARAqWCJ5e/skuV2K/l+Zyck/H6pJIhLXuBnz+6vn2i211o7DtbE78HQtsYEKICHGI+g=="], + + "mobx-react-lite": ["mobx-react-lite@4.1.1", "", { "dependencies": { "use-sync-external-store": "^1.4.0" }, "peerDependencies": { "mobx": "^6.9.0", "react": "^16.8.0 || ^17 || ^18 || ^19" } }, "sha512-iUxiMpsvNraCKXU+yPotsOncNNmyeS2B5DKL+TL6Tar/xm+wwNJAubJmtRSeAoYawdZqwv8Z/+5nPRHeQxTiXg=="], + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], "natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="], @@ -543,6 +558,8 @@ "object.fromentries": ["object.fromentries@2.0.8", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-abstract": "^1.23.2", "es-object-atoms": "^1.0.0" } }, "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ=="], + "object.groupby": ["object.groupby@1.0.3", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-abstract": "^1.23.2" } }, "sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ=="], + "object.values": ["object.values@1.2.1", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0" } }, "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA=="], "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=="], @@ -651,6 +668,8 @@ "string.prototype.trimstart": ["string.prototype.trimstart@1.0.8", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0" } }, "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg=="], + "strip-bom": ["strip-bom@3.0.0", "", {}, "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA=="], + "strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="], "stylis": ["stylis@4.2.0", "", {}, "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw=="], @@ -663,6 +682,8 @@ "ts-api-utils": ["ts-api-utils@2.1.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ=="], + "tsconfig-paths": ["tsconfig-paths@3.15.0", "", { "dependencies": { "@types/json5": "^0.0.29", "json5": "^1.0.2", "minimist": "^1.2.6", "strip-bom": "^3.0.0" } }, "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg=="], + "type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="], "typed-array-buffer": ["typed-array-buffer@1.0.3", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-typed-array": "^1.1.14" } }, "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw=="], @@ -675,8 +696,6 @@ "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], - "typescript-eslint": ["typescript-eslint@8.46.3", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.46.3", "@typescript-eslint/parser": "8.46.3", "@typescript-eslint/typescript-estree": "8.46.3", "@typescript-eslint/utils": "8.46.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-bAfgMavTuGo+8n6/QQDVQz4tZ4f7Soqg53RbrlZQEoAltYop/XR4RAts/I0BrO3TTClTSTFJ0wYbla+P8cEWJA=="], - "unbox-primitive": ["unbox-primitive@1.1.0", "", { "dependencies": { "call-bound": "^1.0.3", "has-bigints": "^1.0.2", "has-symbols": "^1.1.0", "which-boxed-primitive": "^1.1.1" } }, "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw=="], "undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], @@ -685,6 +704,8 @@ "uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="], + "use-sync-external-store": ["use-sync-external-store@1.6.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w=="], + "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], "which-boxed-primitive": ["which-boxed-primitive@1.1.1", "", { "dependencies": { "is-bigint": "^1.1.0", "is-boolean-object": "^1.2.1", "is-number-object": "^1.1.1", "is-string": "^1.1.1", "is-symbol": "^1.1.1" } }, "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA=="], @@ -707,7 +728,9 @@ "zod-validation-error": ["zod-validation-error@4.0.2", "", { "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" } }, "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ=="], - "@emotion/babel-plugin/convert-source-map": ["convert-source-map@1.9.0", "", {}, "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A=="], + "@babel/core/convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="], + + "@babel/core/json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="], "@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], @@ -719,6 +742,14 @@ "babel-plugin-macros/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=="], + "eslint-import-resolver-node/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="], + + "eslint-import-resolver-node/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=="], + + "eslint-module-utils/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="], + + "eslint-plugin-import/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="], + "fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], "hoist-non-react-statics/react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="], diff --git a/data.sqlite b/data.sqlite new file mode 100644 index 0000000000000000000000000000000000000000..976d2c53fd0e8f3e81086e2a22b3164cbfd85a0c GIT binary patch literal 49152 zcmeI)%WvXF90%|jUIr4@Ri&)OA=^P!6i^-ooX0MEXh_x|6hgAZNX-d3VVYRtwHUU0 ztBTS?5B&rBkMtkdWB-I+dTrIyN?mn(>ez+=yJ4knyJ(BPBW%Vq8N+=1vth8E)5k@# z%h{K$PQ&Ojm3%}fC3}n!Lhg!bQB1=W5E&s57*RGD*jB0uo@7!8ZDHR@9wD_QYoefu!7`4qEm$gE^WWR9yrka(toL1IK2imFI z#)_M@DUAjzPmH&*%bX_8n@^|<0VN-!#BA31GwVrRd|9d*y>81{u8wG`sZoBhe=j5{ z%ggjyc8I?+t*g3eb%)R43EpApEOA9I8cf%|&{^qNOqF7BwyOQMBq>Wv^xFdm=-g;p z_7t3eu}fx&GKhxsFtBGua{KEH-_2Ws)7}*ayU8&8*V&-mO(vai19>$VP#!K#3uN26 znzHAie#$Q?D=YMR#o0FQn%&?Y&j%*9)p*S;x7i1Bl-Lafl*5&2xZ^n0)cBJ0jpmaS zMWKDu!FCQl&dfi->lD4SYr`<>)An|=VO(;yUp(F)h2ceq8(m(jiUHvx@iCwmjx_d2 z%VjG?ogGxl;^3-R?ZQ(%dvxMP*NHcvtgX>Arx%06q{W}~c=Lk4w&YrqST* zqGh808I_c1lwLm=g`wrxS)PeCxMlB*bJ41C7srjITaJkKdz$9=Pi@;*hBa8(kaYFZ z=w9)T*}Po+SKDCC6Hq>nzBLHNKJIdP#rKJz{hcBrCMrKC@^kq+dl?A=5P$##AOHaf zKmY;|fB*y_0D<=?aGwTAbU8{sQFjjWIW<}1Po1HfBQz+2enY{3DTn z5i=wRKmY;|fB*y_009U<00Izz00cfDfw<%+8$|7jEB*KQGo#V2^UaG^WAf&JEj5#T zaL?l>QKGsx0*LzY>;F}83xGZT7ytAR2?7v+00bZa0SG_<0uX=z1Rwx`ITo<*|Ht@$ zjyEv61_1~_00Izz00bZa0SG_<0uT@aIRB3@fB*y_009U<00Izz00bZa0SL^$0M7r< z|29SsApijgKmY;|fB*y_009U<00KDwj~IXe1Rwwb2tWV=5P$##AOHaf%)fv={-^RQ zA|6N(fB*y_009U<00Izz00bZa0SL^SfS>wEkb3Ov|Ec^J5f3B?KmY;|fB*y_009U< S00Izz00ibv!0)5Mkmqm7Y@dn% literal 0 HcmV?d00001 diff --git a/eslint.config.js b/eslint.config.js index 63fe44b..aa1e3d6 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -1,54 +1,22 @@ // eslint.config.js -import js from '@eslint/js'; -import stylistic from '@stylistic/eslint-plugin'; -import jsxA11y from 'eslint-plugin-jsx-a11y'; -import react from 'eslint-plugin-react'; -import reactHooks from 'eslint-plugin-react-hooks'; -import tseslint from 'typescript-eslint'; -import unusedImports from 'eslint-plugin-unused-imports'; +import config from '@sebastianbrenner/eslint-config/src/index.js'; export default [ - js.configs.recommended, - ...tseslint.configs.recommended, - { - plugins: { - react, - 'react-hooks': reactHooks, - 'jsx-a11y': jsxA11y, - '@stylistic': stylistic, - 'unused-imports': unusedImports, - }, - rules: { - // React 19 JSX transform requires no React in scope - 'react/react-in-jsx-scope': 'off', - - // Hooks rules - 'react-hooks/rules-of-hooks': 'error', - 'react-hooks/exhaustive-deps': 'warn', - - // Accessibility - 'jsx-a11y/alt-text': 'warn', - - // Style rules (using @stylistic) - '@stylistic/indent': ['error', 4], - '@stylistic/semi': ['error', 'always'], - '@stylistic/quotes': ['error', 'single', { avoidEscape: true }], - '@stylistic/no-trailing-spaces': 'error', - '@stylistic/no-multiple-empty-lines': 'error', - - 'unused-imports/no-unused-imports': 'error', - }, - settings: { - react: { - version: 'detect', - }, - }, - }, + ...config, { ignores: [ 'dist', 'build', 'node_modules', ], + rules: { + "react/react-in-jsx-scope": "off", + "react/jsx-newline": ["error", { "prevent": true }], + "@typescript-eslint/no-unsafe-assignment": "off", + "indent": ["error", 4] + }, + settings: { + 'import/core-modules': ['bun' ] + } }, ]; diff --git a/package.json b/package.json index 0a8f1e3..1110b31 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,8 @@ "scripts": { "dev": "bun --hot src/index.ts", "build": "bun build ./src/index.html --outdir=dist --sourcemap --target=browser --minify --define:process.env.NODE_ENV='\"production\"' --env='BUN_PUBLIC_*'", - "start": "NODE_ENV=production bun src/index.ts" + "start": "NODE_ENV=production bun src/index.ts", + "eslint-quiet": "eslint 'src/**/*.{ts,tsx}' --quiet" }, "dependencies": { "@emotion/react": "11.14.0", @@ -14,23 +15,18 @@ "@fontsource/roboto": "5.2.8", "@mui/icons-material": "7.3.5", "@mui/material": "7.3.5", + "cookie": "^1.0.2", + "mobx-react-lite": "^4.1.1", "react": "19", "react-dom": "19", "react-router-dom": "^7.9.5" }, "devDependencies": { - "@eslint/js": "9.39.1", - "@stylistic/eslint-plugin": "5.5.0", "@types/bun": "1.3.1", "@types/react": "19", "@types/react-dom": "19", - "eslint": "9.39.1", - "eslint-plugin-jsx-a11y": "6.10.2", - "eslint-plugin-react": "7.37.5", - "eslint-plugin-react-hooks": "7.0.1", - "eslint-plugin-unused-imports": "^4.3.0", "prettier": "3.6.2", "typescript": "5.9.3", - "typescript-eslint": "8.46.3" + "@sebastianbrenner/eslint-config": "git+https://gitea.sebastianbrenner.dev/sebastianbrenner/eslint-config.git/#1.2.0" } } \ No newline at end of file diff --git a/src/client/App.tsx b/src/client/App.tsx index 50605bd..1108e0f 100644 --- a/src/client/App.tsx +++ b/src/client/App.tsx @@ -1,20 +1,29 @@ import SignIn from './components/SignIn'; import { createTheme, ThemeProvider } from '@mui/material'; +import { useStore } from './Store'; +import Group from './components/Group'; +import { observer } from 'mobx-react-lite'; -const App = () => { +const App = observer(() => { const theme = createTheme({ palette: { mode: 'dark', }, }); + const store = useStore(); + const { loggedIn } = store; + return (
- + {loggedIn ? + : + + }
); -}; +}); export default App; diff --git a/src/client/Store.ts b/src/client/Store.ts new file mode 100644 index 0000000..5386b41 --- /dev/null +++ b/src/client/Store.ts @@ -0,0 +1,39 @@ +import { createContext, useContext } from 'react'; +import { makeAutoObservable } from 'mobx'; +import cookie from 'cookie'; +import type { Group, User } from '@/interfaces'; + +export type CookieData = { + user: User; + group: Group; +}; + +export class Store { + cookieData: CookieData | null = null; + + processCookie() { + const parsed = cookie.parse(document.cookie); + + this.cookieData = { + user: parsed.user ? JSON.parse(parsed.user) : null, + group: parsed.group ? JSON.parse(parsed.group) : null, + } as CookieData; + } + + get loggedIn(): boolean { + if (this.cookieData === null) { + return false; + }; + return !!this.cookieData.user && !!this.cookieData.group; + } + + constructor() { + console.log('Initializing Store'); + this.processCookie(); + makeAutoObservable(this); + } +} + +export const StoreContext = createContext(new Store()); + +export const useStore = (): Store => useContext(StoreContext); \ No newline at end of file diff --git a/src/client/components/Group.tsx b/src/client/components/Group.tsx new file mode 100644 index 0000000..7f9c9a8 --- /dev/null +++ b/src/client/components/Group.tsx @@ -0,0 +1,64 @@ +import * as React from 'react'; +import Box from '@mui/material/Box'; +import CssBaseline from '@mui/material/CssBaseline'; +import Typography from '@mui/material/Typography'; +import Stack from '@mui/material/Stack'; +import { useTheme } from '@mui/material/styles'; +import Card from './Card'; +import { useStore } from '../Store'; + +const Group = () => { + const [loading, setLoading] = React.useState(false); + + const theme = useTheme(); + const store = useStore(); + const { group } = store.cookieData!; + + console.log('Group component rendered with group:', group.name); + + return ( +
+ + + + + {group.name} + + + + {'Willkommen in der Gruppe!'} + + + {'Admin: ' + group.mail} + + + + +
+ ); +}; + +export default Group; \ No newline at end of file diff --git a/src/client/components/SignIn.tsx b/src/client/components/SignIn.tsx index 7277869..ba85af0 100644 --- a/src/client/components/SignIn.tsx +++ b/src/client/components/SignIn.tsx @@ -13,65 +13,88 @@ import { useTheme } from '@mui/material/styles'; import GroupAddIcon from '@mui/icons-material/GroupAdd'; import ForgotPassword from './ForgotPassword'; import Card from './Card'; +import { createUser, fetchGroupByCode, fetchUser } from '../serverApi'; +import { observer } from 'mobx-react-lite'; -const SignIn = () => { +const SignIn = observer(() => { const [email, setEmail] = React.useState(''); - const [emailError, setEmailError] = React.useState(false); + const [emailError, setEmailError] = React.useState(''); const [group, setGroup] = React.useState(''); - const [groupError, setGroupError] = React.useState(false); + const [groupError, setGroupError] = React.useState(''); const [open, setOpen] = React.useState(false); + const [loading, setLoading] = React.useState(false); const theme = useTheme(); + React.useEffect(() => { + const params = new URLSearchParams(window.location.search); + const groupParam = params.get('gruppe'); + if (groupParam) { + setGroup(groupParam); + } + }, []); + const handleClickOpen = () => { setOpen(true); }; - const handleClose = () => { setOpen(false); }; - const handleSubmit = (event: React.FormEvent) => { - if ((emailError || email === '')) { - event.preventDefault(); - setEmailError(true); - return; - } + const handleEnterGroup = async (event: React.MouseEvent) => { + event.preventDefault(); + + if (!isMailValid() || !isGroupValid()) return; + + setLoading(true); + + try { + const groupData = await fetchGroupByCode(group); + + if (!groupData) { + setGroupError('Gruppencode existiert nicht.'); + return; + } + + let user = await fetchUser(email); + + if (!user) { + user = await createUser(email); + if (!user) throw new Error('Error creating user'); + } + + await cookieStore.set('user', JSON.stringify(user)); + await cookieStore.set('group', JSON.stringify(groupData)); - if ((groupError || group === '')) { - event.preventDefault(); - setGroupError(true); - return; } + catch (error) { + console.error('Error during sign-in process:', error); + } finally { + setLoading(false); } }; - const validateEmail = (): boolean => { + const isMailValid = (): boolean => { let isValid = true; - if (email !== '' && !/\S+@\S+\.\S+/.test(email)) { - setEmailError(true);; + if (!/\S+@\S+\.\S+/.test(email)) { + setEmailError('Bitte gib eine gültige E-Mail Adresse ein.'); isValid = false; } else { - setEmailError(false); + setEmailError(''); } return isValid; }; - const validateGroup = (): boolean => { + const isGroupValid = (): boolean => { let isValid = true; - if (group !== '' && !/\S+@\S+\.\S+/.test(email)) { - setGroupError(true); + if (!/^[A-Z0-9]{6}$/.test(group)) { + setGroupError('Bitte gib einen gültigen Gruppencode ein.'); isValid = false; } else { - setGroupError(false); + setGroupError(''); } return isValid; }; - const validateInputs = () => { - validateEmail(); - validateGroup(); - }; - return (
{ > {'Gruppe beitreten'} - { > E-Mail - { required fullWidth variant="outlined" - onBlur={validateEmail} + onBlur={isMailValid} onChange={event => setEmail(event.target.value)} + value={email} color={emailError ? 'error' : 'primary'} /> {'Gruppencode'} { required fullWidth variant="outlined" - onBlur={validateGroup} + onBlur={isGroupValid} onChange={event => setGroup(event.target.value)} + value={group} color={groupError ? 'error' : 'primary'} /> - - - { {'Gruppencode vergessen?'} - oder -
); -}; +}); export default SignIn; \ No newline at end of file diff --git a/src/client/frontend.tsx b/src/client/frontend.tsx index c2c1a8f..d618bd9 100644 --- a/src/client/frontend.tsx +++ b/src/client/frontend.tsx @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ /** * This file is the entry point for the React app, it sets up the root * element and renders the App component to the DOM. @@ -6,19 +7,25 @@ */ import { StrictMode } from 'react'; -import { createRoot } from 'react-dom/client'; +import { createRoot, type Root } from 'react-dom/client'; import App from './App'; +import { Store, StoreContext } from './Store'; const elem = document.getElementById('root')!; + +const store = new Store(); + const app = ( - + + + ); if (import.meta.hot) { // With hot module reloading, `import.meta.hot.data` is persisted. - const root = (import.meta.hot.data.root ??= createRoot(elem)); + const root: Root = (import.meta.hot.data.root ??= createRoot(elem)); root.render(app); } else { // The hot module reloading API is not available in production. diff --git a/src/client/serverApi.ts b/src/client/serverApi.ts index 408e7d9..8477e65 100644 --- a/src/client/serverApi.ts +++ b/src/client/serverApi.ts @@ -1,8 +1,38 @@ -import type { Group } from '@/interfaces'; +import type { Group, User } from '@/interfaces'; -export async function fetchGroup(id: string): Promise { +export async function fetchUser(mail: string): Promise { try { - const res = await fetch(`/api/group/${id}`); + const res = await fetch(`/api/user/${mail}`); + if (!res.ok) return null; + const data: User = await res.json(); + return data; + } catch (err) { + console.error('Failed to fetch user:', err); + return null; + } +} + +export async function createUser(mail: string): Promise { + try { + const res = await fetch('/api/user', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ mail }), + }); + if (!res.ok) return null; + const data: User = await res.json(); + return data; + } catch (err) { + console.error('Failed to create user:', err); + return null; + } +} + +export async function fetchGroupByCode(code: string): Promise { + try { + const res = await fetch(`/api/group/${code}`); if (!res.ok) return null; const data: Group = await res.json(); return data; diff --git a/src/index.ts b/src/index.ts index 333d483..6ea5ce2 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,31 +2,66 @@ import { serve } from 'bun'; import index from './index.html'; import DB from './server/db'; +const db = new DB(); + const server = serve({ routes: { // Serve index.html for all unmatched routes. '/*': index, - '/api/group/:id': async req => { - const id = req.params.id; - const group = await new DB().getGroup(id); - console.log('Fetching group with ID:', id, 'Result:', group); - if (!group) return new Response(JSON.stringify({ error: 'Not found' }), { status: 404 }); + '/api/user/:mail': async req => { + const mail = req.params.mail; + console.log('Received request for user:', mail); + const user = await db.getUserByMail(mail); + console.log('Fetching user with mail:', mail, 'Result:', user); + if (!user) return new Response(JSON.stringify({ error: 'User not found' }), { status: 404 }); + return new Response(JSON.stringify(user), { headers: { 'Content-Type': 'application/json' } }); + }, + + '/api/user': { + async POST(req) { + try { + const { mail } = await req.json() as { mail: string }; + console.log('Received request to create user with mail:', mail); + const user = await db.createUser(mail); + return new Response(JSON.stringify(user), { headers: { 'Content-Type': 'application/json' } }); + } catch (err) { + console.error('Error creating user:', err); + return new Response(JSON.stringify({ error: 'Failed to create user' }), { status: 500 }); + } + }, + }, + + '/api/group/:code': async req => { + const code = req.params.code; + console.log('Received request for group:', code); + const group = await db.getGroupByCode(code); + console.log('Fetching group with ID:', code, 'Result:', group); + if (!group) return new Response(JSON.stringify({ error: 'Group not found' }), { status: 404 }); return new Response(JSON.stringify(group), { headers: { 'Content-Type': 'application/json' } }); }, '/api/group': { async POST(req) { try { - const group = (await req.formData() as FormData).get('code') as string; - const response = await new DB().createGroup(group); + const { name, mail } = await req.json() as { name: string; mail: string }; + console.log('Received request to create group with name:', name, 'and mail:', mail); + const response = db.createGroup({ name, mail }); return new Response(JSON.stringify(response), { headers: { 'Content-Type': 'application/json' } }); } catch (err) { console.error('Error creating group:', err); return new Response(JSON.stringify({ error: 'Failed to create group' }), { status: 500 }); } } - } + }, + + '/api/group_members/:groupId': async req => { + const groupId = parseInt(req.params.groupId, 10); + console.log('Received request for group members of group ID:', groupId); + const members = await db.getGroupMembers(groupId); + console.log('Fetching members for group ID:', groupId, 'Result:', members); + return new Response(JSON.stringify(members), { headers: { 'Content-Type': 'application/json' } }); + }, }, development: process.env.NODE_ENV !== 'production' && { diff --git a/src/interfaces.ts b/src/interfaces.ts index 77cd8c2..2344218 100644 --- a/src/interfaces.ts +++ b/src/interfaces.ts @@ -1,5 +1,16 @@ export type Group = { id: number; code: string; + name: string; + mail: string; + image: Uint8Array | null; + phase: string; + created_at: string; +}; + +export type User = { + id: number; + mail: string; + image: Uint8Array | null; created_at: string; }; \ No newline at end of file diff --git a/src/server/db.ts b/src/server/db.ts index 0875c25..b8e7281 100644 --- a/src/server/db.ts +++ b/src/server/db.ts @@ -1,14 +1,16 @@ -import { Database } from 'bun:sqlite'; +import type { Group, User } from '@/interfaces'; +import { SQL } from 'bun'; // create tables if they don't exist const createTableScript = ` -- GROUP TRABLE -- - CREATE TABLE IF NOT EXISTS group ( + CREATE TABLE IF NOT EXISTS groups ( id INTEGER PRIMARY KEY AUTOINCREMENT, code TEXT UNIQUE NOT NULL, name TEXT NOT NULL, mail TEXT NOT NULL, image BLOB, + phase TEXT DEFAULT 'gathering', created_at DATETIME DEFAULT CURRENT_TIMESTAMP ); @@ -17,20 +19,21 @@ const createTableScript = ` id INTEGER PRIMARY KEY AUTOINCREMENT, mail TEXT NOT NULL, image BLOB, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP ); - -- GROUP_MEMBER TABLE -- - CREATE TABLE IF NOT EXISTS group_member ( + -- MEMBERSHIP TABLE -- + CREATE TABLE IF NOT EXISTS membership ( id INTEGER PRIMARY KEY AUTOINCREMENT, group_id INTEGER, user_id INTEGER, - UNIQUE(email, group_id), + UNIQUE(user_id, group_id), FOREIGN KEY(group_id) REFERENCES groups(id), FOREIGN KEY(user_id) REFERENCES users(id) ); -- BAN TABLE -- - CREATE TABLE IF NOT EXISTS ban ( + CREATE TABLE IF NOT EXISTS bans ( id INTEGER PRIMARY KEY AUTOINCREMENT, group_id INTEGER, user_id INTEGER, @@ -38,7 +41,7 @@ const createTableScript = ` FOREIGN KEY(group_id) REFERENCES groups(id), FOREIGN KEY(user_id) REFERENCES users(id), FOREIGN KEY(user_id2) REFERENCES users(id) - UNIQUE(group_id, user_id, user_id2), + UNIQUE(group_id, user_id, user_id2) ); -- WHISHLIST TABLE -- @@ -53,32 +56,110 @@ const createTableScript = ` `; class DB { - private instance: Database = new Database(); + private instance: SQL; - private prepareDB () { - this.instance.run(createTableScript); + private executeScript(script: string, name: string) { + try { + void this.instance`${script}`; + } catch (err) { + console.error(`error executing script ${name}: ${err as Error}`); + } } - public async getGroup(id: string) { - const stmt = this.instance.prepare('SELECT * FROM groups WHERE code = ?'); - const group = await stmt.get(id); + /** + * Prepare the database by creating necessary tables. + */ + private prepareDB () { + this.executeScript(createTableScript, 'createTableScript'); + } + + /* USERS */ + + /** + * Create a new user + * @param mail: string + * @returns created user + */ + public async createUser(mail: string): Promise { + const user: User = await this.instance` + INSERT INTO users (mail) VALUES (${mail}) + RETURNING * + `; + + return user; + } + + /** + * Get user by mail + * @param mail: string + * @returns user object or undefined + */ + public async getUserByMail(mail: string): Promise { + const user: User = await this.instance` + SELECT * FROM users WHERE mail = ${mail} + `; + return user; + } + + /* GROUPS */ + + /** + * Create a new group + * @param name: string + * @param mail: string + * @returns object with id of the created group + */ + public async createGroup( {name, mail}: {name: string, mail: string}): Promise { + const code = Math.random().toString(36).substring(2, 8).toUpperCase(); + const group: Group = await this.instance` + INSERT INTO groups (code, name, mail) VALUES (${code}, ${name}, ${mail}) + RETURNING * + `; + return group; + }; + + /** + * Get group by ID + * @param id: string + * @returns group object or undefined + */ + public async getGroupByCode(code: string): Promise { + const group: Group | undefined = await this.instance` + SELECT * FROM groups WHERE code = ${code} + `; return group; } - public createGroup(code: string): { id: number | bigint} { - const stmt = this.instance.prepare('INSERT INTO groups (code) VALUES (?)'); - const changes = stmt.run(code); - console.log('Inserted group with ID:', changes); - return { id: changes.lastInsertRowid }; - }; + /* GROUP MEMBER */ + /** + * Add user to group + * @param userId: number + * @param groupId: number + * @returns object with id of the created group member entry + */ + public async UserEntersGroup(userId: number, groupId: number): Promise<{ id: number | bigint; }> { + const membership: { id: number | bigint} = await this.instance` + INSERT OR IGNORE INTO membership (user_id, group_id) VALUES (${userId}, ${groupId}) + RETURNING * + `; + return membership; + } + + public async getGroupMembers(groupId: number): Promise { + const users: User[] = await this.instance` + SELECT * + FROM users u + JOIN membership m ON u.id = m.user_id + WHERE m.group_id = ${groupId} + `; + return users; + } constructor(url: string = './data.sqlite') { - if (this.instance) { - this.instance = new Database(url); - this.prepareDB(); - console.log('Database initialized at', url); - } + this.instance = new SQL(url, {adapter: 'sqlite'}); + this.prepareDB(); + console.log('Database initialized at', url); } }