update eslint and refactor backend

This commit is contained in:
2026-01-04 12:37:46 +01:00
parent 43824ce284
commit 090b3c10d1
13 changed files with 438 additions and 152 deletions

View File

@@ -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=="],

BIN
data.sqlite Normal file

Binary file not shown.

View File

@@ -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' ]
}
},
];

View File

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

View File

@@ -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 (
<ThemeProvider theme={theme}>
<div className="app">
{loggedIn ?
<Group /> :
<SignIn />
}
</div>
</ThemeProvider>
);
};
});
export default App;

39
src/client/Store.ts Normal file
View File

@@ -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<Store>(new Store());
export const useStore = (): Store => useContext(StoreContext);

View File

@@ -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 (
<div style={{
backgroundImage: 'radial-gradient(at 50% 50%, hsla(210, 100%, 16%, 0.5), hsl(220, 30%, 5%))',
backgroundRepeat: 'no-repeat',
}}>
<CssBaseline enableColorScheme />
<Stack direction="column" justifyContent="space-between"
sx={{
height: 'calc((1 - var(--template-frame-height, 0)) * 100dvh)',
minHeight: '100%',
padding: theme.spacing(2),
[theme.breakpoints.up('sm')]: {
padding: theme.spacing(4),
},
}}
>
<Card variant="outlined">
<Typography
component="h1"
variant="h4"
sx={{ width: '100%', fontSize: 'clamp(2rem, 10vw, 2.15rem)' }}
>
{group.name}
</Typography>
<Box
sx={{
display: 'flex',
flexDirection: 'column',
width: '100%',
gap: 2,
}}
>
<Typography>
{'Willkommen in der Gruppe!'}
</Typography>
<Typography>
{'Admin: ' + group.mail}
</Typography>
</Box>
</Card>
</Stack>
</div>
);
};
export default Group;

View File

@@ -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<HTMLFormElement>) => {
if ((emailError || email === '')) {
const handleEnterGroup = async (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
event.preventDefault();
setEmailError(true);
if (!isMailValid() || !isGroupValid()) return;
setLoading(true);
try {
const groupData = await fetchGroupByCode(group);
if (!groupData) {
setGroupError('Gruppencode existiert nicht.');
return;
}
if ((groupError || group === '')) {
event.preventDefault();
setGroupError(true);
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));
}
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 (
<div style={{
backgroundImage: 'radial-gradient(at 50% 50%, hsla(210, 100%, 16%, 0.5), hsl(220, 30%, 5%))',
@@ -96,11 +119,7 @@ const SignIn = () => {
>
{'Gruppe beitreten'}
</Typography>
<Box
component="form"
onSubmit={handleSubmit}
noValidate
sx={{
display: 'flex',
flexDirection: 'column',
@@ -110,10 +129,9 @@ const SignIn = () => {
>
<FormControl>
<FormLabel htmlFor="email">E-Mail</FormLabel>
<TextField
error={emailError}
helperText={emailError && 'Bitte gib eine gültige E-Mail Adresse ein.'}
error={emailError !== ''}
helperText={emailError}
id="email"
type="email"
name="email"
@@ -123,15 +141,16 @@ const SignIn = () => {
required
fullWidth
variant="outlined"
onBlur={validateEmail}
onBlur={isMailValid}
onChange={event => setEmail(event.target.value)}
value={email}
color={emailError ? 'error' : 'primary'} />
</FormControl>
<FormControl>
<FormLabel htmlFor="password">{'Gruppencode'}</FormLabel>
<TextField
error={groupError}
helperText={groupError && 'Bitte gib einen gültigen Gruppencode ein.'}
error={groupError !== ''}
helperText={groupError}
name="groupcode"
placeholder="Gruppe123"
type="text"
@@ -140,22 +159,20 @@ const SignIn = () => {
required
fullWidth
variant="outlined"
onBlur={validateGroup}
onBlur={isGroupValid}
onChange={event => setGroup(event.target.value)}
value={group}
color={groupError ? 'error' : 'primary'} />
</FormControl>
<ForgotPassword open={open} handleClose={handleClose} />
<Button
type="submit"
fullWidth
variant="contained"
onClick={validateInputs}
onClick={event => {void handleEnterGroup(event)}}
loading={loading}
>
{'Beitreten'}
</Button>
<Link
component="button"
type="button"
@@ -166,9 +183,7 @@ const SignIn = () => {
{'Gruppencode vergessen?'}
</Link>
</Box>
<Divider>oder</Divider>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
<Button
fullWidth
@@ -183,6 +198,6 @@ const SignIn = () => {
</Stack>
</div>
);
};
});
export default SignIn;

View File

@@ -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 = (
<StrictMode>
<StoreContext.Provider value={store}>
<App />
</StoreContext.Provider>
</StrictMode>
);
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.

View File

@@ -1,8 +1,38 @@
import type { Group } from '@/interfaces';
import type { Group, User } from '@/interfaces';
export async function fetchGroup(id: string): Promise<Group | null> {
export async function fetchUser(mail: string): Promise<User | null> {
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<User | null> {
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<Group | null> {
try {
const res = await fetch(`/api/group/${code}`);
if (!res.ok) return null;
const data: Group = await res.json();
return data;

View File

@@ -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' && {

View File

@@ -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;
};

View File

@@ -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,33 +56,111 @@ 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<User> {
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<User | undefined> {
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<Group> {
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<Group | undefined> {
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<User[]> {
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.instance = new SQL(url, {adapter: 'sqlite'});
this.prepareDB();
console.log('Database initialized at', url);
}
}
}
export default DB;