diff --git a/concord-server/.editorconfig b/concord-server/.editorconfig new file mode 100644 index 0000000..1e09672 --- /dev/null +++ b/concord-server/.editorconfig @@ -0,0 +1,10 @@ +# EditorConfig helps maintain consistent coding styles +root = true + +[*] +indent_style = space +indent_size = 2 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true diff --git a/concord-server/.gitignore b/concord-server/.gitignore index 506e4c3..9371620 100644 --- a/concord-server/.gitignore +++ b/concord-server/.gitignore @@ -1,2 +1,3 @@ # deps node_modules/ +generated/ \ No newline at end of file diff --git a/concord-server/README.md b/concord-server/README.md index 6dd13e7..d950ba6 100644 --- a/concord-server/README.md +++ b/concord-server/README.md @@ -1,9 +1,11 @@ To install dependencies: + ```sh bun install ``` To run: + ```sh bun run dev ``` diff --git a/concord-server/bun.lock b/concord-server/bun.lock index 365d51b..d3d7309 100644 --- a/concord-server/bun.lock +++ b/concord-server/bun.lock @@ -4,93 +4,240 @@ "": { "name": "concord-server", "dependencies": { + "@hono/standard-validator": "^0.1.5", + "@hono/zod-validator": "^0.7.3", + "@prisma/client": "^6.16.2", + "@scalar/hono-api-reference": "^0.9.19", + "@socket.io/bun-engine": "^0.0.3", "hono": "^4.9.9", + "hono-openapi": "^1.1.0", "prisma": "^6.16.2", + "socket.io": "^4.8.1", + "zod": "^4.1.11", }, "devDependencies": { "@types/bun": "latest", + "prisma-zod-generator": "^1.22.1", }, }, }, "packages": { - "@prisma/config": ["@prisma/config@6.16.2", "", { "dependencies": { "c12": "3.1.0", "deepmerge-ts": "7.1.5", "effect": "3.16.12", "empathic": "2.0.0" } }, "sha512-mKXSUrcqXj0LXWPmJsK2s3p9PN+aoAbyMx7m5E1v1FufofR1ZpPoIArjjzOIm+bJRLLvYftoNYLx1tbHgF9/yg=="], + "@hono/standard-validator": ["@hono/standard-validator@0.1.5", "", { "peerDependencies": { "@standard-schema/spec": "1.0.0", "hono": ">=3.9.0" } }, "sha512-EIyZPPwkyLn6XKwFj5NBEWHXhXbgmnVh2ceIFo5GO7gKI9WmzTjPDKnppQB0KrqKeAkq3kpoW4SIbu5X1dgx3w=="], - "@prisma/debug": ["@prisma/debug@6.16.2", "", {}, "sha512-bo4/gA/HVV6u8YK2uY6glhNsJ7r+k/i5iQ9ny/3q5bt9ijCj7WMPUwfTKPvtEgLP+/r26Z686ly11hhcLiQ8zA=="], + "@hono/zod-validator": ["@hono/zod-validator@0.7.3", "", { "peerDependencies": { "hono": ">=3.9.0", "zod": "^3.25.0 || ^4.0.0" } }, "sha512-uYGdgVib3RlGD698WR5dVM0zB3UuPY5vHKXffGUbUh7r4xY+mFIhF3/v4AcQVLrU5CQdBso8BJr4wuVoCrjTuQ=="], - "@prisma/engines": ["@prisma/engines@6.16.2", "", { "dependencies": { "@prisma/debug": "6.16.2", "@prisma/engines-version": "6.16.0-7.1c57fdcd7e44b29b9313256c76699e91c3ac3c43", "@prisma/fetch-engine": "6.16.2", "@prisma/get-platform": "6.16.2" } }, "sha512-7yf3AjfPUgsg/l7JSu1iEhsmZZ/YE00yURPjTikqm2z4btM0bCl2coFtTGfeSOWbQMmq45Jab+53yGUIAT1sjA=="], + "@prisma/client": ["@prisma/client@6.16.2", "", { "peerDependencies": { "prisma": "*", "typescript": ">=5.1.0" }, "optionalPeers": ["typescript"] }, "sha512-E00PxBcalMfYO/TWnXobBVUai6eW/g5OsifWQsQDzJYm7yaY+IRLo7ZLsaefi0QkTpxfuhFcQ/w180i6kX3iJw=="], - "@prisma/engines-version": ["@prisma/engines-version@6.16.0-7.1c57fdcd7e44b29b9313256c76699e91c3ac3c43", "", {}, "sha512-ThvlDaKIVrnrv97ujNFDYiQbeMQpLa0O86HFA2mNoip4mtFqM7U5GSz2ie1i2xByZtvPztJlNRgPsXGeM/kqAA=="], + "@prisma/config": ["@prisma/config@6.16.2", "", { "dependencies": { "c12": "3.1.0", "deepmerge-ts": "7.1.5", "effect": "3.16.12", "empathic": "2.0.0" } }, ""], - "@prisma/fetch-engine": ["@prisma/fetch-engine@6.16.2", "", { "dependencies": { "@prisma/debug": "6.16.2", "@prisma/engines-version": "6.16.0-7.1c57fdcd7e44b29b9313256c76699e91c3ac3c43", "@prisma/get-platform": "6.16.2" } }, "sha512-wPnZ8DMRqpgzye758ZvfAMiNJRuYpz+rhgEBZi60ZqDIgOU2694oJxiuu3GKFeYeR/hXxso4/2oBC243t/whxQ=="], + "@prisma/debug": ["@prisma/debug@6.16.2", "", {}, ""], - "@prisma/get-platform": ["@prisma/get-platform@6.16.2", "", { "dependencies": { "@prisma/debug": "6.16.2" } }, "sha512-U/P36Uke5wS7r1+omtAgJpEB94tlT4SdlgaeTc6HVTTT93pXj7zZ+B/cZnmnvjcNPfWddgoDx8RLjmQwqGDYyA=="], + "@prisma/dmmf": ["@prisma/dmmf@6.16.2", "", {}, "sha512-o9ztgdbj2KZXl6DL+oP56TTC0poTLPns9/MeU761b49E1IQ/fd0jwdov1bidlNOiwio8Nsou23xNrYE/db10aA=="], - "@standard-schema/spec": ["@standard-schema/spec@1.0.0", "", {}, "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA=="], + "@prisma/driver-adapter-utils": ["@prisma/driver-adapter-utils@6.16.2", "", { "dependencies": { "@prisma/debug": "6.16.2" } }, "sha512-DMgfafnG0zPd+QoAQOC0Trn1xlb0fVAfQi2MpkpzSf641KiVkVPkJRXDSbcTbxGxO2HRdd0vI9U6LlesWad4XA=="], + + "@prisma/engines": ["@prisma/engines@6.16.2", "", { "dependencies": { "@prisma/debug": "6.16.2", "@prisma/engines-version": "6.16.0-7.1c57fdcd7e44b29b9313256c76699e91c3ac3c43", "@prisma/fetch-engine": "6.16.2", "@prisma/get-platform": "6.16.2" } }, ""], + + "@prisma/engines-version": ["@prisma/engines-version@6.16.0-7.1c57fdcd7e44b29b9313256c76699e91c3ac3c43", "", {}, ""], + + "@prisma/fetch-engine": ["@prisma/fetch-engine@6.16.2", "", { "dependencies": { "@prisma/debug": "6.16.2", "@prisma/engines-version": "6.16.0-7.1c57fdcd7e44b29b9313256c76699e91c3ac3c43", "@prisma/get-platform": "6.16.2" } }, ""], + + "@prisma/generator": ["@prisma/generator@6.16.2", "", {}, "sha512-7bwRmtMIgfe1rUynh1p9VlmYvEiidbRO6aBphPBS6YGEGSvNe8+QExbRpsqFlFBvIX76BhZCxuEj7ZwALMYRKQ=="], + + "@prisma/generator-helper": ["@prisma/generator-helper@6.16.2", "", { "dependencies": { "@prisma/debug": "6.16.2", "@prisma/dmmf": "6.16.2", "@prisma/generator": "6.16.2" } }, "sha512-8tVnWM8ETJNrvI5CT9eKCW23+aPLNkidC+g9NJn7ghXm60Q7GGlLX5tmvn5dE8tXvs/FSX3MN7KNmNJpOr89Hw=="], + + "@prisma/get-platform": ["@prisma/get-platform@6.16.2", "", { "dependencies": { "@prisma/debug": "6.16.2" } }, ""], + + "@prisma/internals": ["@prisma/internals@6.16.2", "", { "dependencies": { "@prisma/config": "6.16.2", "@prisma/debug": "6.16.2", "@prisma/dmmf": "6.16.2", "@prisma/driver-adapter-utils": "6.16.2", "@prisma/engines": "6.16.2", "@prisma/fetch-engine": "6.16.2", "@prisma/generator": "6.16.2", "@prisma/generator-helper": "6.16.2", "@prisma/get-platform": "6.16.2", "@prisma/prisma-schema-wasm": "6.16.0-7.1c57fdcd7e44b29b9313256c76699e91c3ac3c43", "@prisma/schema-engine-wasm": "6.16.0-7.1c57fdcd7e44b29b9313256c76699e91c3ac3c43", "@prisma/schema-files-loader": "6.16.2", "arg": "5.0.2", "prompts": "2.4.2" }, "peerDependencies": { "typescript": ">=5.1.0" }, "optionalPeers": ["typescript"] }, "sha512-gwmWl7H8iTbi+58RXua5Lsus5LDbIZGO2wQ4RoSX9YtEbKWHwRP8TUzTVLwRNeJ2DHwfnzhTLrUnybwotqiACg=="], + + "@prisma/prisma-schema-wasm": ["@prisma/prisma-schema-wasm@6.16.0-7.1c57fdcd7e44b29b9313256c76699e91c3ac3c43", "", {}, "sha512-DvYi0zKqzPd49Z5japS3FawyMylscaoUmlXNhnRAXb8HZryG4Q7TM1FLX8OIAfCgLmoWS1c/Zf4UZznBXkvWSg=="], + + "@prisma/schema-engine-wasm": ["@prisma/schema-engine-wasm@6.16.0-7.1c57fdcd7e44b29b9313256c76699e91c3ac3c43", "", {}, "sha512-HDgFE0um5OHkk2pkQbAgARR284i2VpoM+7GYRAT0zxoTagsdaZ6yquJF2LEZuAKfibib0Ct7JZxRCB8eN/Ru6g=="], + + "@prisma/schema-files-loader": ["@prisma/schema-files-loader@6.16.2", "", { "dependencies": { "@prisma/prisma-schema-wasm": "6.16.0-7.1c57fdcd7e44b29b9313256c76699e91c3ac3c43", "fs-extra": "11.3.0" } }, "sha512-TN77DUFgOxT/WL6wuxVhn7qVDvwVRl0TEzhFfRh5vKQsuZ5itLzA7Ki4TgOs4Pk18wwZnti6ZKdzR3Y7cu2KsA=="], + + "@scalar/core": ["@scalar/core@0.3.17", "", { "dependencies": { "@scalar/types": "0.2.16" } }, "sha512-G6tP+2oorFA90szI8DGiEQ23SmobiuhN93GfTJNpFMhz/kdEtC4lcYawAxL1tWkZhlK/QYRcaCZcVzmpTUgBCA=="], + + "@scalar/hono-api-reference": ["@scalar/hono-api-reference@0.9.19", "", { "dependencies": { "@scalar/core": "0.3.17" }, "peerDependencies": { "hono": "^4.0.0" } }, "sha512-MeBdIwuAAhwvsUt1f9174wASu0C8b4WrXuVdC/6FjrsTxLHSbJVx+XFzZAKF5ex+1kaXjvYrm3fiovRFHLyfJA=="], + + "@scalar/openapi-types": ["@scalar/openapi-types@0.3.7", "", { "dependencies": { "zod": "3.24.1" } }, "sha512-QHSvHBVDze3+dUwAhIGq6l1iOev4jdoqdBK7QpfeN1Q4h+6qpVEw3EEqBiH0AXUSh/iWwObBv4uMgfIx0aNZ5g=="], + + "@scalar/types": ["@scalar/types@0.2.16", "", { "dependencies": { "@scalar/openapi-types": "0.3.7", "nanoid": "5.1.5", "zod": "3.24.1" } }, "sha512-XWff9jWfYaj6q3ww94x66S6Q58u/3kA1sDOUhLAwb9va7r58bzk3NRwLOkEEdJmyEns1MEJAM53mY8KRWX6elA=="], + + "@socket.io/bun-engine": ["@socket.io/bun-engine@0.0.3", "", { "peerDependencies": { "typescript": "^5" } }, "sha512-OK2ZObq9hKsxyAcV7xDTcWGubBmfEY3Lt4nb04K+HlYl9G5PDgrY9hxJm9uV+B0xo3MhKFrgdg9VQsVZ3pbT/g=="], + + "@socket.io/component-emitter": ["@socket.io/component-emitter@3.1.2", "", {}, "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA=="], + + "@standard-community/standard-json": ["@standard-community/standard-json@0.3.5", "", { "peerDependencies": { "@standard-schema/spec": "^1.0.0", "@types/json-schema": "^7.0.15", "@valibot/to-json-schema": "^1.3.0", "arktype": "^2.1.20", "effect": "^3.16.8", "quansync": "^0.2.11", "sury": "^10.0.0", "typebox": "^1.0.17", "valibot": "^1.1.0", "zod": "^3.25.0 || ^4.0.0", "zod-to-json-schema": "^3.24.5" }, "optionalPeers": ["@valibot/to-json-schema", "arktype", "effect", "sury", "typebox", "valibot", "zod", "zod-to-json-schema"] }, "sha512-4+ZPorwDRt47i+O7RjyuaxHRK/37QY/LmgxlGrRrSTLYoFatEOzvqIc85GTlM18SFZ5E91C+v0o/M37wZPpUHA=="], + + "@standard-community/standard-openapi": ["@standard-community/standard-openapi@0.2.8", "", { "peerDependencies": { "@standard-community/standard-json": "^0.3.5", "@standard-schema/spec": "^1.0.0", "arktype": "^2.1.20", "effect": "^3.17.14", "openapi-types": "^12.1.3", "sury": "^10.0.0", "typebox": "^1.0.0", "valibot": "^1.1.0", "zod": "^3.25.0 || ^4.0.0", "zod-openapi": "^4" }, "optionalPeers": ["arktype", "effect", "sury", "typebox", "valibot", "zod", "zod-openapi"] }, "sha512-80ap74p5oy/SU4al5HkPwO5+NbN2wH/FBr6kwaE5ROq7AvcDFaxzUfTazewroNaCotbvdGcvzXb9oEoOIyfC/Q=="], + + "@standard-schema/spec": ["@standard-schema/spec@1.0.0", "", {}, ""], "@types/bun": ["@types/bun@1.2.22", "", { "dependencies": { "bun-types": "1.2.22" } }, "sha512-5A/KrKos2ZcN0c6ljRSOa1fYIyCKhZfIVYeuyb4snnvomnpFqC0tTsEkdqNxbAgExV384OETQ//WAjl3XbYqQA=="], - "@types/node": ["@types/node@24.5.2", "", { "dependencies": { "undici-types": "~7.12.0" } }, "sha512-FYxk1I7wPv3K2XBaoyH2cTnocQEu8AOZ60hPbsyukMPLv5/5qr7V1i8PLHdl6Zf87I+xZXFvPCXYjiTFq+YSDQ=="], + "@types/cors": ["@types/cors@2.8.19", "", { "dependencies": { "@types/node": "*" } }, "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg=="], - "@types/react": ["@types/react@19.1.14", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-ukd93VGzaNPMAUPy0gRDSC57UuQbnH9Kussp7HBjM06YFi9uZTFhOvMSO2OKqXm1rSgzOE+pVx1k1PYHGwlc8Q=="], + "@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="], - "bun-types": ["bun-types@1.2.22", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-hwaAu8tct/Zn6Zft4U9BsZcXkYomzpHJX28ofvx7k0Zz2HNz54n1n+tDgxoWFGB4PcFvJXJQloPhaV2eP3Q6EA=="], + "@types/node": ["@types/node@24.5.2", "", { "dependencies": { "undici-types": "~7.12.0" } }, ""], - "c12": ["c12@3.1.0", "", { "dependencies": { "chokidar": "^4.0.3", "confbox": "^0.2.2", "defu": "^6.1.4", "dotenv": "^16.6.1", "exsolve": "^1.0.7", "giget": "^2.0.0", "jiti": "^2.4.2", "ohash": "^2.0.11", "pathe": "^2.0.3", "perfect-debounce": "^1.0.0", "pkg-types": "^2.2.0", "rc9": "^2.1.2" }, "peerDependencies": { "magicast": "^0.3.5" }, "optionalPeers": ["magicast"] }, "sha512-uWoS8OU1MEIsOv8p/5a82c3H31LsWVR5qiyXVfBNOzfffjUWtPnhAb4BYI2uG2HfGmZmFjCtui5XNWaps+iFuw=="], + "@types/react": ["@types/react@19.1.14", "", { "dependencies": { "csstype": "^3.0.2" } }, ""], - "chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="], + "accepts": ["accepts@1.3.8", "", { "dependencies": { "mime-types": "~2.1.34", "negotiator": "0.6.3" } }, "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw=="], - "citty": ["citty@0.1.6", "", { "dependencies": { "consola": "^3.2.3" } }, "sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ=="], + "ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="], - "confbox": ["confbox@0.2.2", "", {}, "sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ=="], + "ajv-formats": ["ajv-formats@3.0.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ=="], - "consola": ["consola@3.4.2", "", {}, "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA=="], + "arg": ["arg@5.0.2", "", {}, "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg=="], - "csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="], + "base64id": ["base64id@2.0.0", "", {}, "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog=="], - "deepmerge-ts": ["deepmerge-ts@7.1.5", "", {}, "sha512-HOJkrhaYsweh+W+e74Yn7YStZOilkoPb6fycpwNLKzSPtruFs48nYis0zy5yJz1+ktUhHxoRDJ27RQAWLIJVJw=="], + "bun-types": ["bun-types@1.2.22", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, ""], - "defu": ["defu@6.1.4", "", {}, "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg=="], + "c12": ["c12@3.1.0", "", { "dependencies": { "chokidar": "^4.0.3", "confbox": "^0.2.2", "defu": "^6.1.4", "dotenv": "^16.6.1", "exsolve": "^1.0.7", "giget": "^2.0.0", "jiti": "^2.4.2", "ohash": "^2.0.11", "pathe": "^2.0.3", "perfect-debounce": "^1.0.0", "pkg-types": "^2.2.0", "rc9": "^2.1.2" }, "peerDependencies": { "magicast": "^0.3.5" }, "optionalPeers": ["magicast"] }, ""], - "destr": ["destr@2.0.5", "", {}, "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA=="], + "chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, ""], - "dotenv": ["dotenv@16.6.1", "", {}, "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow=="], + "citty": ["citty@0.1.6", "", { "dependencies": { "consola": "^3.2.3" } }, ""], - "effect": ["effect@3.16.12", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "fast-check": "^3.23.1" } }, "sha512-N39iBk0K71F9nb442TLbTkjl24FLUzuvx2i1I2RsEAQsdAdUTuUoW0vlfUXgkMTUOnYqKnWcFfqw4hK4Pw27hg=="], + "confbox": ["confbox@0.2.2", "", {}, ""], - "empathic": ["empathic@2.0.0", "", {}, "sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA=="], + "consola": ["consola@3.4.2", "", {}, ""], - "exsolve": ["exsolve@1.0.7", "", {}, "sha512-VO5fQUzZtI6C+vx4w/4BWJpg3s/5l+6pRQEHzFRM8WFi4XffSP1Z+4qi7GbjWbvRQEbdIco5mIMq+zX4rPuLrw=="], + "cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="], - "fast-check": ["fast-check@3.23.2", "", { "dependencies": { "pure-rand": "^6.1.0" } }, "sha512-h5+1OzzfCC3Ef7VbtKdcv7zsstUQwUDlYpUTvjeUsJAssPgLn7QzbboPtL5ro04Mq0rPOsMzl7q5hIbRs2wD1A=="], + "cors": ["cors@2.8.5", "", { "dependencies": { "object-assign": "^4", "vary": "^1" } }, "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g=="], - "giget": ["giget@2.0.0", "", { "dependencies": { "citty": "^0.1.6", "consola": "^3.4.0", "defu": "^6.1.4", "node-fetch-native": "^1.6.6", "nypm": "^0.6.0", "pathe": "^2.0.3" }, "bin": { "giget": "dist/cli.mjs" } }, "sha512-L5bGsVkxJbJgdnwyuheIunkGatUF/zssUoxxjACCseZYAVbaqdh9Tsmmlkl8vYan09H7sbvKt4pS8GqKLBrEzA=="], + "csstype": ["csstype@3.1.3", "", {}, ""], - "hono": ["hono@4.9.9", "", {}, "sha512-Hxw4wT6zjJGZJdkJzAx9PyBdf7ZpxaTSA0NfxqjLghwMrLBX8p33hJBzoETRakF3UJu6OdNQBZAlNSkGqKFukw=="], + "data-uri-to-buffer": ["data-uri-to-buffer@4.0.1", "", {}, "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A=="], - "jiti": ["jiti@2.6.0", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-VXe6RjJkBPj0ohtqaO8vSWP3ZhAKo66fKrFNCll4BTcwljPLz03pCbaNKfzGP5MbrCYcbJ7v0nOYYwUzTEIdXQ=="], + "debug": ["debug@4.3.7", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ=="], - "node-fetch-native": ["node-fetch-native@1.6.7", "", {}, "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q=="], + "deepmerge-ts": ["deepmerge-ts@7.1.5", "", {}, ""], - "nypm": ["nypm@0.6.2", "", { "dependencies": { "citty": "^0.1.6", "consola": "^3.4.2", "pathe": "^2.0.3", "pkg-types": "^2.3.0", "tinyexec": "^1.0.1" }, "bin": { "nypm": "dist/cli.mjs" } }, "sha512-7eM+hpOtrKrBDCh7Ypu2lJ9Z7PNZBdi/8AT3AX8xoCj43BBVHD0hPSTEvMtkMpfs8FCqBGhxB+uToIQimA111g=="], + "defu": ["defu@6.1.4", "", {}, ""], - "ohash": ["ohash@2.0.11", "", {}, "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ=="], + "destr": ["destr@2.0.5", "", {}, ""], - "pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], + "dotenv": ["dotenv@16.6.1", "", {}, ""], - "perfect-debounce": ["perfect-debounce@1.0.0", "", {}, "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA=="], + "effect": ["effect@3.16.12", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "fast-check": "^3.23.1" } }, ""], - "pkg-types": ["pkg-types@2.3.0", "", { "dependencies": { "confbox": "^0.2.2", "exsolve": "^1.0.7", "pathe": "^2.0.3" } }, "sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig=="], + "empathic": ["empathic@2.0.0", "", {}, ""], - "prisma": ["prisma@6.16.2", "", { "dependencies": { "@prisma/config": "6.16.2", "@prisma/engines": "6.16.2" }, "peerDependencies": { "typescript": ">=5.1.0" }, "optionalPeers": ["typescript"], "bin": { "prisma": "build/index.js" } }, "sha512-aRvldGE5UUJTtVmFiH3WfNFNiqFlAtePUxcI0UEGlnXCX7DqhiMT5TRYwncHFeA/Reca5W6ToXXyCMTeFPdSXA=="], + "engine.io": ["engine.io@6.6.4", "", { "dependencies": { "@types/cors": "^2.8.12", "@types/node": ">=10.0.0", "accepts": "~1.3.4", "base64id": "2.0.0", "cookie": "~0.7.2", "cors": "~2.8.5", "debug": "~4.3.1", "engine.io-parser": "~5.2.1", "ws": "~8.17.1" } }, "sha512-ZCkIjSYNDyGn0R6ewHDtXgns/Zre/NT6Agvq1/WobF7JXgFff4SeDroKiCO3fNJreU9YG429Sc81o4w5ok/W5g=="], - "pure-rand": ["pure-rand@6.1.0", "", {}, "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA=="], + "engine.io-parser": ["engine.io-parser@5.2.3", "", {}, "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q=="], - "rc9": ["rc9@2.1.2", "", { "dependencies": { "defu": "^6.1.4", "destr": "^2.0.3" } }, "sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg=="], + "exsolve": ["exsolve@1.0.7", "", {}, ""], - "readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="], + "fast-check": ["fast-check@3.23.2", "", { "dependencies": { "pure-rand": "^6.1.0" } }, ""], - "tinyexec": ["tinyexec@1.0.1", "", {}, "sha512-5uC6DDlmeqiOwCPmK9jMSdOuZTh8bU39Ys6yidB+UTt5hfZUPGAypSgFRiEp+jbi9qH40BLDvy85jIU88wKSqw=="], + "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], - "undici-types": ["undici-types@7.12.0", "", {}, "sha512-goOacqME2GYyOZZfb5Lgtu+1IDmAlAEu5xnD3+xTzS10hT0vzpf0SPjkXwAw9Jm+4n/mQGDP3LO8CPbYROeBfQ=="], + "fast-uri": ["fast-uri@3.1.0", "", {}, "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA=="], + + "fetch-blob": ["fetch-blob@3.2.0", "", { "dependencies": { "node-domexception": "^1.0.0", "web-streams-polyfill": "^3.0.3" } }, "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ=="], + + "formdata-polyfill": ["formdata-polyfill@4.0.10", "", { "dependencies": { "fetch-blob": "^3.1.2" } }, "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g=="], + + "fs-extra": ["fs-extra@11.3.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-Z4XaCL6dUDHfP/jT25jJKMmtxvuwbkrD1vNSMFlo9lNLY2c5FHYSQgHPRZUjAB26TpDEoW9HCOgplrdbaPV/ew=="], + + "giget": ["giget@2.0.0", "", { "dependencies": { "citty": "^0.1.6", "consola": "^3.4.0", "defu": "^6.1.4", "node-fetch-native": "^1.6.6", "nypm": "^0.6.0", "pathe": "^2.0.3" }, "bin": "dist/cli.mjs" }, ""], + + "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], + + "hono": ["hono@4.9.9", "", {}, ""], + + "hono-openapi": ["hono-openapi@1.1.0", "", { "peerDependencies": { "@hono/standard-validator": "^0.1.2", "@standard-community/standard-json": "^0.3.5", "@standard-community/standard-openapi": "^0.2.8", "@types/json-schema": "^7.0.15", "hono": "^4.8.3", "openapi-types": "^12.1.3" }, "optionalPeers": ["@hono/standard-validator", "hono"] }, "sha512-eA5hN8D2O30EkPPUxWFilcZcThAe81TShbH38Y183ZZp8WkgMh4BrPEDeZ/EFN2tyDi3cmTgKTa3+oStyJX0UA=="], + + "jiti": ["jiti@2.6.0", "", { "bin": "lib/jiti-cli.mjs" }, ""], + + "json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], + + "jsonfile": ["jsonfile@6.2.0", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg=="], + + "kleur": ["kleur@3.0.3", "", {}, "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w=="], + + "mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], + + "mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], + + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + + "nanoid": ["nanoid@5.1.5", "", { "bin": { "nanoid": "bin/nanoid.js" } }, "sha512-Ir/+ZpE9fDsNH0hQ3C68uyThDXzYcim2EqcZ8zn8Chtt1iylPT9xXJB0kPCnqzgcEGikO9RxSrh63MsmVCU7Fw=="], + + "negotiator": ["negotiator@0.6.3", "", {}, "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg=="], + + "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=="], + + "node-fetch-native": ["node-fetch-native@1.6.7", "", {}, ""], + + "nypm": ["nypm@0.6.2", "", { "dependencies": { "citty": "^0.1.6", "consola": "^3.4.2", "pathe": "^2.0.3", "pkg-types": "^2.3.0", "tinyexec": "^1.0.1" }, "bin": "dist/cli.mjs" }, ""], + + "object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="], + + "ohash": ["ohash@2.0.11", "", {}, ""], + + "openapi-types": ["openapi-types@12.1.3", "", {}, "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw=="], + + "pathe": ["pathe@2.0.3", "", {}, ""], + + "perfect-debounce": ["perfect-debounce@1.0.0", "", {}, ""], + + "pkg-types": ["pkg-types@2.3.0", "", { "dependencies": { "confbox": "^0.2.2", "exsolve": "^1.0.7", "pathe": "^2.0.3" } }, ""], + + "prettier": ["prettier@3.6.2", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ=="], + + "prisma": ["prisma@6.16.2", "", { "dependencies": { "@prisma/config": "6.16.2", "@prisma/engines": "6.16.2" }, "peerDependencies": { "typescript": ">=5.1.0" }, "optionalPeers": ["typescript"], "bin": "build/index.js" }, ""], + + "prisma-zod-generator": ["prisma-zod-generator@1.22.1", "", { "dependencies": { "@prisma/client": "^6.16.2", "@prisma/generator-helper": "^6.16.2", "@prisma/internals": "^6.16.2", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "node-fetch": "^3.3.2", "prettier": "^3.6.2", "tslib": "^2.8.1" }, "peerDependencies": { "zod": ">=3.25.0 <5" }, "bin": { "prisma-zod-generator": "lib/generator.js" } }, "sha512-nBr00sfR8onGCD5eIDLHoFrpeJTSuZxSeaO61Zg6CAEyXPR51gpkO1ev9huG7+tsV+mm8me8VNl8hMcVtWl8FA=="], + + "prompts": ["prompts@2.4.2", "", { "dependencies": { "kleur": "^3.0.3", "sisteransi": "^1.0.5" } }, "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q=="], + + "pure-rand": ["pure-rand@6.1.0", "", {}, ""], + + "quansync": ["quansync@0.2.11", "", {}, "sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA=="], + + "rc9": ["rc9@2.1.2", "", { "dependencies": { "defu": "^6.1.4", "destr": "^2.0.3" } }, ""], + + "readdirp": ["readdirp@4.1.2", "", {}, ""], + + "require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="], + + "sisteransi": ["sisteransi@1.0.5", "", {}, "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg=="], + + "socket.io": ["socket.io@4.8.1", "", { "dependencies": { "accepts": "~1.3.4", "base64id": "~2.0.0", "cors": "~2.8.5", "debug": "~4.3.2", "engine.io": "~6.6.0", "socket.io-adapter": "~2.5.2", "socket.io-parser": "~4.2.4" } }, "sha512-oZ7iUCxph8WYRHHcjBEc9unw3adt5CmSNlppj/5Q4k2RIrhl8Z5yY2Xr4j9zj0+wzVZ0bxmYoGSzKJnRl6A4yg=="], + + "socket.io-adapter": ["socket.io-adapter@2.5.5", "", { "dependencies": { "debug": "~4.3.4", "ws": "~8.17.1" } }, "sha512-eLDQas5dzPgOWCk9GuuJC2lBqItuhKI4uxGgo9aIV7MYbk2h9Q6uULEh8WBzThoI7l+qU9Ast9fVUmkqPP9wYg=="], + + "socket.io-parser": ["socket.io-parser@4.2.4", "", { "dependencies": { "@socket.io/component-emitter": "~3.1.0", "debug": "~4.3.1" } }, "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew=="], + + "tinyexec": ["tinyexec@1.0.1", "", {}, ""], + + "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + + "typescript": ["typescript@5.9.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A=="], + + "undici-types": ["undici-types@7.12.0", "", {}, ""], + + "universalify": ["universalify@2.0.1", "", {}, "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw=="], + + "vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="], + + "web-streams-polyfill": ["web-streams-polyfill@3.3.3", "", {}, "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw=="], + + "ws": ["ws@8.17.1", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ=="], + + "zod": ["zod@4.1.11", "", {}, "sha512-WPsqwxITS2tzx1bzhIKsEs19ABD5vmCVa4xBo2tq/SrV4RNZtfws1EnCWQXM6yh8bD08a1idvkB5MZSBiZsjwg=="], + + "@scalar/openapi-types/zod": ["zod@3.24.1", "", {}, "sha512-muH7gBL9sI1nciMZV67X5fTKKBLtwpZ5VBp1vsOQzj1MhrBZ4wlVCm3gedKZWLp0Oyel8sIGfeiz54Su+OVT+A=="], + + "@scalar/types/zod": ["zod@3.24.1", "", {}, "sha512-muH7gBL9sI1nciMZV67X5fTKKBLtwpZ5VBp1vsOQzj1MhrBZ4wlVCm3gedKZWLp0Oyel8sIGfeiz54Su+OVT+A=="], } } diff --git a/concord-server/migrations/20250927190915_init2finalfinal/migration.sql b/concord-server/migrations/20250927190915_init2finalfinal/migration.sql new file mode 100644 index 0000000..6defc25 --- /dev/null +++ b/concord-server/migrations/20250927190915_init2finalfinal/migration.sql @@ -0,0 +1,131 @@ +-- CreateTable +CREATE TABLE "public"."Instance" ( + "id" TEXT NOT NULL, + "name" TEXT NOT NULL, + "icon" TEXT, + + CONSTRAINT "Instance_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "public"."User" ( + "id" TEXT NOT NULL, + "username" TEXT NOT NULL, + "nickname" TEXT, + "bio" TEXT, + "picture" TEXT, + "banner" TEXT, + "admin" BOOLEAN NOT NULL, + "status" TEXT NOT NULL, + + CONSTRAINT "User_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "public"."Role" ( + "userId" TEXT NOT NULL, + "instanceId" TEXT NOT NULL, + "type" TEXT NOT NULL +); + +-- CreateTable +CREATE TABLE "public"."UserAuth" ( + "userId" TEXT NOT NULL, + "password" TEXT NOT NULL, + "token" TEXT +); + +-- CreateTable +CREATE TABLE "public"."Category" ( + "id" TEXT NOT NULL, + "instanceId" TEXT, + "name" TEXT NOT NULL, + "position" INTEGER NOT NULL, + + CONSTRAINT "Category_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "public"."Channel" ( + "id" TEXT NOT NULL, + "type" TEXT NOT NULL, + "categoryId" TEXT, + "name" TEXT NOT NULL, + "description" TEXT NOT NULL, + + CONSTRAINT "Channel_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "public"."Message" ( + "id" TEXT NOT NULL, + "channelId" TEXT NOT NULL, + "userId" TEXT NOT NULL, + "deleted" BOOLEAN NOT NULL, + "text" TEXT NOT NULL, + + CONSTRAINT "Message_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "public"."Reply" ( + "messageId" TEXT NOT NULL, + "repliesToId" TEXT NOT NULL +); + +-- CreateTable +CREATE TABLE "public"."MessagePing" ( + "messageId" TEXT NOT NULL, + "pingsUserId" TEXT NOT NULL +); + +-- CreateIndex +CREATE UNIQUE INDEX "Role_userId_instanceId_key" ON "public"."Role"("userId", "instanceId"); + +-- CreateIndex +CREATE UNIQUE INDEX "UserAuth_userId_key" ON "public"."UserAuth"("userId"); + +-- CreateIndex +CREATE UNIQUE INDEX "Reply_messageId_key" ON "public"."Reply"("messageId"); + +-- CreateIndex +CREATE UNIQUE INDEX "Reply_repliesToId_key" ON "public"."Reply"("repliesToId"); + +-- CreateIndex +CREATE UNIQUE INDEX "Reply_messageId_repliesToId_key" ON "public"."Reply"("messageId", "repliesToId"); + +-- CreateIndex +CREATE UNIQUE INDEX "MessagePing_messageId_pingsUserId_key" ON "public"."MessagePing"("messageId", "pingsUserId"); + +-- AddForeignKey +ALTER TABLE "public"."Role" ADD CONSTRAINT "Role_userId_fkey" FOREIGN KEY ("userId") REFERENCES "public"."User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "public"."Role" ADD CONSTRAINT "Role_instanceId_fkey" FOREIGN KEY ("instanceId") REFERENCES "public"."Instance"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "public"."UserAuth" ADD CONSTRAINT "UserAuth_userId_fkey" FOREIGN KEY ("userId") REFERENCES "public"."User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "public"."Category" ADD CONSTRAINT "Category_instanceId_fkey" FOREIGN KEY ("instanceId") REFERENCES "public"."Instance"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "public"."Channel" ADD CONSTRAINT "Channel_categoryId_fkey" FOREIGN KEY ("categoryId") REFERENCES "public"."Category"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "public"."Message" ADD CONSTRAINT "Message_channelId_fkey" FOREIGN KEY ("channelId") REFERENCES "public"."Channel"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "public"."Message" ADD CONSTRAINT "Message_userId_fkey" FOREIGN KEY ("userId") REFERENCES "public"."User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "public"."Reply" ADD CONSTRAINT "Reply_messageId_fkey" FOREIGN KEY ("messageId") REFERENCES "public"."Message"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "public"."Reply" ADD CONSTRAINT "Reply_repliesToId_fkey" FOREIGN KEY ("repliesToId") REFERENCES "public"."Message"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "public"."MessagePing" ADD CONSTRAINT "MessagePing_messageId_fkey" FOREIGN KEY ("messageId") REFERENCES "public"."Message"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "public"."MessagePing" ADD CONSTRAINT "MessagePing_pingsUserId_fkey" FOREIGN KEY ("pingsUserId") REFERENCES "public"."User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/concord-server/migrations/20250927230554_timestampify/migration.sql b/concord-server/migrations/20250927230554_timestampify/migration.sql new file mode 100644 index 0000000..7285123 --- /dev/null +++ b/concord-server/migrations/20250927230554_timestampify/migration.sql @@ -0,0 +1,71 @@ +/* + Warnings: + + - Added the required column `updatedAt` to the `Category` table without a default value. This is not possible if the table is not empty. + - Added the required column `pinnedId` to the `Channel` table without a default value. This is not possible if the table is not empty. + - Added the required column `updatedAt` to the `Channel` table without a default value. This is not possible if the table is not empty. + - Added the required column `updatedAt` to the `Instance` table without a default value. This is not possible if the table is not empty. + - Added the required column `updatedAt` to the `Message` table without a default value. This is not possible if the table is not empty. + - Added the required column `updatedAt` to the `Reply` table without a default value. This is not possible if the table is not empty. + - Added the required column `updatedAt` to the `Role` table without a default value. This is not possible if the table is not empty. + - Added the required column `updatedAt` to the `User` table without a default value. This is not possible if the table is not empty. + - Added the required column `updatedAt` to the `UserAuth` table without a default value. This is not possible if the table is not empty. + +*/ +-- AlterTable +ALTER TABLE "public"."Category" ADD COLUMN "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, +ADD COLUMN "updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP; + +-- AlterTable +ALTER TABLE "public"."Channel" ADD COLUMN "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, +ADD COLUMN "pinnedId" TEXT, +ADD COLUMN "updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP; + +-- AlterTable +ALTER TABLE "public"."Instance" ADD COLUMN "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, +ADD COLUMN "updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP; + +-- AlterTable +ALTER TABLE "public"."Message" ADD COLUMN "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, +ADD COLUMN "updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP; + +-- AlterTable +ALTER TABLE "public"."MessagePing" ADD COLUMN "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP; + +-- AlterTable +ALTER TABLE "public"."Reply" ADD COLUMN "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, +ADD COLUMN "updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP; + +-- AlterTable +ALTER TABLE "public"."Role" ADD COLUMN "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, +ADD COLUMN "updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP; + +-- AlterTable +ALTER TABLE "public"."User" ADD COLUMN "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, +ADD COLUMN "updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP; + +-- AlterTable +ALTER TABLE "public"."UserAuth" ADD COLUMN "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, +ADD COLUMN "updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP; + +-- CreateTable +CREATE TABLE "public"."ChannelPin" ( + "messageId" TEXT NOT NULL, + "channelId" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +-- CreateIndex +CREATE UNIQUE INDEX "ChannelPin_messageId_key" ON "public"."ChannelPin"("messageId"); + +-- CreateIndex +CREATE UNIQUE INDEX "ChannelPin_channelId_key" ON "public"."ChannelPin"("channelId"); + +-- CreateIndex +CREATE UNIQUE INDEX "ChannelPin_messageId_channelId_key" ON "public"."ChannelPin"("messageId", "channelId"); + +-- AddForeignKey +ALTER TABLE "public"."ChannelPin" ADD CONSTRAINT "ChannelPin_messageId_fkey" FOREIGN KEY ("messageId") REFERENCES "public"."Message"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "public"."ChannelPin" ADD CONSTRAINT "ChannelPin_channelId_fkey" FOREIGN KEY ("channelId") REFERENCES "public"."Channel"("id") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/concord-server/migrations/20250927231139_pinnedidnullable/migration.sql b/concord-server/migrations/20250927231139_pinnedidnullable/migration.sql new file mode 100644 index 0000000..4614fa0 --- /dev/null +++ b/concord-server/migrations/20250927231139_pinnedidnullable/migration.sql @@ -0,0 +1,26 @@ +-- AlterTable +ALTER TABLE "public"."Category" ALTER COLUMN "updatedAt" DROP DEFAULT; + +-- AlterTable +ALTER TABLE "public"."Channel" ALTER COLUMN "updatedAt" DROP DEFAULT; + +-- AlterTable +ALTER TABLE "public"."Channel" ALTER COLUMN "pinnedId" DROP NOT NULL; + +-- AlterTable +ALTER TABLE "public"."Instance" ALTER COLUMN "updatedAt" DROP DEFAULT; + +-- AlterTable +ALTER TABLE "public"."Message" ALTER COLUMN "updatedAt" DROP DEFAULT; + +-- AlterTable +ALTER TABLE "public"."Reply" ALTER COLUMN "updatedAt" DROP DEFAULT; + +-- AlterTable +ALTER TABLE "public"."Role" ALTER COLUMN "updatedAt" DROP DEFAULT; + +-- AlterTable +ALTER TABLE "public"."User" ALTER COLUMN "updatedAt" DROP DEFAULT; + +-- AlterTable +ALTER TABLE "public"."UserAuth" ALTER COLUMN "updatedAt" DROP DEFAULT; diff --git a/concord-server/migrations/migration_lock.toml b/concord-server/migrations/migration_lock.toml new file mode 100644 index 0000000..044d57c --- /dev/null +++ b/concord-server/migrations/migration_lock.toml @@ -0,0 +1,3 @@ +# Please do not edit this file manually +# It should be added in your version-control system (e.g., Git) +provider = "postgresql" diff --git a/concord-server/package-lock.json b/concord-server/package-lock.json new file mode 100644 index 0000000..cef0762 --- /dev/null +++ b/concord-server/package-lock.json @@ -0,0 +1,390 @@ +{ + "name": "concord-server", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "concord-server", + "dependencies": { + "@prisma/client": "^6.16.2", + "hono": "^4.9.9", + "prisma": "^6.16.2" + }, + "devDependencies": { + "@types/bun": "latest" + } + }, + "node_modules/@prisma/client": { + "version": "6.16.2", + "resolved": "https://registry.npmjs.org/@prisma/client/-/client-6.16.2.tgz", + "integrity": "sha512-E00PxBcalMfYO/TWnXobBVUai6eW/g5OsifWQsQDzJYm7yaY+IRLo7ZLsaefi0QkTpxfuhFcQ/w180i6kX3iJw==", + "hasInstallScript": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "peerDependencies": { + "prisma": "*", + "typescript": ">=5.1.0" + }, + "peerDependenciesMeta": { + "prisma": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, + "node_modules/@prisma/config": { + "version": "6.16.2", + "license": "Apache-2.0", + "dependencies": { + "c12": "3.1.0", + "deepmerge-ts": "7.1.5", + "effect": "3.16.12", + "empathic": "2.0.0" + } + }, + "node_modules/@prisma/debug": { + "version": "6.16.2", + "license": "Apache-2.0" + }, + "node_modules/@prisma/engines": { + "version": "6.16.2", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "6.16.2", + "@prisma/engines-version": "6.16.0-7.1c57fdcd7e44b29b9313256c76699e91c3ac3c43", + "@prisma/fetch-engine": "6.16.2", + "@prisma/get-platform": "6.16.2" + } + }, + "node_modules/@prisma/engines-version": { + "version": "6.16.0-7.1c57fdcd7e44b29b9313256c76699e91c3ac3c43", + "license": "Apache-2.0" + }, + "node_modules/@prisma/fetch-engine": { + "version": "6.16.2", + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "6.16.2", + "@prisma/engines-version": "6.16.0-7.1c57fdcd7e44b29b9313256c76699e91c3ac3c43", + "@prisma/get-platform": "6.16.2" + } + }, + "node_modules/@prisma/get-platform": { + "version": "6.16.2", + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "6.16.2" + } + }, + "node_modules/@standard-schema/spec": { + "version": "1.0.0", + "license": "MIT" + }, + "node_modules/@types/bun": { + "version": "1.2.22", + "resolved": "https://registry.npmjs.org/@types/bun/-/bun-1.2.22.tgz", + "integrity": "sha512-5A/KrKos2ZcN0c6ljRSOa1fYIyCKhZfIVYeuyb4snnvomnpFqC0tTsEkdqNxbAgExV384OETQ//WAjl3XbYqQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "bun-types": "1.2.22" + } + }, + "node_modules/@types/node": { + "version": "24.5.2", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.12.0" + } + }, + "node_modules/@types/react": { + "version": "19.1.14", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "csstype": "^3.0.2" + } + }, + "node_modules/bun-types": { + "version": "1.2.22", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + }, + "peerDependencies": { + "@types/react": "^19" + } + }, + "node_modules/c12": { + "version": "3.1.0", + "license": "MIT", + "dependencies": { + "chokidar": "^4.0.3", + "confbox": "^0.2.2", + "defu": "^6.1.4", + "dotenv": "^16.6.1", + "exsolve": "^1.0.7", + "giget": "^2.0.0", + "jiti": "^2.4.2", + "ohash": "^2.0.11", + "pathe": "^2.0.3", + "perfect-debounce": "^1.0.0", + "pkg-types": "^2.2.0", + "rc9": "^2.1.2" + }, + "peerDependencies": { + "magicast": "^0.3.5" + }, + "peerDependenciesMeta": { + "magicast": { + "optional": true + } + } + }, + "node_modules/chokidar": { + "version": "4.0.3", + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/citty": { + "version": "0.1.6", + "license": "MIT", + "dependencies": { + "consola": "^3.2.3" + } + }, + "node_modules/confbox": { + "version": "0.2.2", + "license": "MIT" + }, + "node_modules/consola": { + "version": "3.4.2", + "license": "MIT", + "engines": { + "node": "^14.18.0 || >=16.10.0" + } + }, + "node_modules/csstype": { + "version": "3.1.3", + "dev": true, + "license": "MIT" + }, + "node_modules/deepmerge-ts": { + "version": "7.1.5", + "license": "BSD-3-Clause", + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/defu": { + "version": "6.1.4", + "license": "MIT" + }, + "node_modules/destr": { + "version": "2.0.5", + "license": "MIT" + }, + "node_modules/dotenv": { + "version": "16.6.1", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/effect": { + "version": "3.16.12", + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "fast-check": "^3.23.1" + } + }, + "node_modules/empathic": { + "version": "2.0.0", + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "node_modules/exsolve": { + "version": "1.0.7", + "license": "MIT" + }, + "node_modules/fast-check": { + "version": "3.23.2", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT", + "dependencies": { + "pure-rand": "^6.1.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/giget": { + "version": "2.0.0", + "license": "MIT", + "dependencies": { + "citty": "^0.1.6", + "consola": "^3.4.0", + "defu": "^6.1.4", + "node-fetch-native": "^1.6.6", + "nypm": "^0.6.0", + "pathe": "^2.0.3" + }, + "bin": { + "giget": "dist/cli.mjs" + } + }, + "node_modules/hono": { + "version": "4.9.9", + "license": "MIT", + "engines": { + "node": ">=16.9.0" + } + }, + "node_modules/jiti": { + "version": "2.6.0", + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/node-fetch-native": { + "version": "1.6.7", + "license": "MIT" + }, + "node_modules/nypm": { + "version": "0.6.2", + "license": "MIT", + "dependencies": { + "citty": "^0.1.6", + "consola": "^3.4.2", + "pathe": "^2.0.3", + "pkg-types": "^2.3.0", + "tinyexec": "^1.0.1" + }, + "bin": { + "nypm": "dist/cli.mjs" + }, + "engines": { + "node": "^14.16.0 || >=16.10.0" + } + }, + "node_modules/ohash": { + "version": "2.0.11", + "license": "MIT" + }, + "node_modules/pathe": { + "version": "2.0.3", + "license": "MIT" + }, + "node_modules/perfect-debounce": { + "version": "1.0.0", + "license": "MIT" + }, + "node_modules/pkg-types": { + "version": "2.3.0", + "license": "MIT", + "dependencies": { + "confbox": "^0.2.2", + "exsolve": "^1.0.7", + "pathe": "^2.0.3" + } + }, + "node_modules/prisma": { + "version": "6.16.2", + "hasInstallScript": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@prisma/config": "6.16.2", + "@prisma/engines": "6.16.2" + }, + "bin": { + "prisma": "build/index.js" + }, + "engines": { + "node": ">=18.18" + }, + "peerDependencies": { + "typescript": ">=5.1.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/pure-rand": { + "version": "6.1.0", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT" + }, + "node_modules/rc9": { + "version": "2.1.2", + "license": "MIT", + "dependencies": { + "defu": "^6.1.4", + "destr": "^2.0.3" + } + }, + "node_modules/readdirp": { + "version": "4.1.2", + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/tinyexec": { + "version": "1.0.1", + "license": "MIT" + }, + "node_modules/undici-types": { + "version": "7.12.0", + "dev": true, + "license": "MIT" + } + } +} diff --git a/concord-server/package.json b/concord-server/package.json index 77eee4a..fe84cc1 100644 --- a/concord-server/package.json +++ b/concord-server/package.json @@ -4,10 +4,19 @@ "dev": "bun run --hot src/index.ts" }, "dependencies": { + "@hono/standard-validator": "^0.1.5", + "@hono/zod-validator": "^0.7.3", + "@prisma/client": "^6.16.2", + "@scalar/hono-api-reference": "^0.9.19", + "@socket.io/bun-engine": "^0.0.3", "hono": "^4.9.9", - "prisma": "^6.16.2" + "hono-openapi": "^1.1.0", + "prisma": "^6.16.2", + "socket.io": "^4.8.1", + "zod": "^4.1.11" }, "devDependencies": { - "@types/bun": "latest" + "@types/bun": "latest", + "prisma-zod-generator": "^1.22.1" } -} \ No newline at end of file +} diff --git a/concord-server/schema.prisma b/concord-server/schema.prisma new file mode 100644 index 0000000..a4fbfa8 --- /dev/null +++ b/concord-server/schema.prisma @@ -0,0 +1,135 @@ +generator client { + provider = "prisma-client-js" +} + +generator zod { + provider = "prisma-zod-generator" +} + +datasource db { + provider = "postgresql" + url = env("DATABASE_URL") +} + +model Instance { + id String @id @default(uuid(7)) + name String + icon String? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + Role Role[] + Category Category[] +} + +model User { + id String @id @default(uuid(7)) + username String + nickname String? + bio String? + picture String? + banner String? + admin Boolean + status String // online/offline/dnd/idle/invis + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + Role Role[] + UserAuth UserAuth? + Message Message[] + MessagePing MessagePing[] +} + +model Role { + User User @relation(fields: [userId], references: [id]) + userId String + Instance Instance @relation(fields: [instanceId], references: [id]) + instanceId String + type String + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@unique([userId, instanceId]) +} + +model UserAuth { + User User @relation(fields: [userId], references: [id]) + userId String + password String // HASHED PASSWORD AS STRING USING SHA-256 + token String? // current user token + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@unique([userId]) +} + +model Category { + id String @id @default(uuid(7)) + Instance Instance? @relation(fields: [instanceId], references: [id]) + instanceId String? + name String + position Int + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + Channel Channel[] +} + +model Channel { + id String @id @default(uuid(7)) + type String + Category Category? @relation(fields: [categoryId], references: [id]) + categoryId String? + name String + description String + pinnedId String? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + Message Message[] + ChannelPin ChannelPin? +} + +model ChannelPin { + messageId String @unique + channelId String @unique + + Message Message @relation(fields: [messageId], references: [id]) + Channel Channel @relation(fields: [channelId], references: [id]) + createdAt DateTime @default(now()) + + @@unique([messageId, channelId]) +} + +model Message { + id String @id @default(uuid(7)) + Channel Channel @relation(fields: [channelId], references: [id]) + channelId String + User User @relation(fields: [userId], references: [id]) + userId String + deleted Boolean + text String + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + replies Reply? @relation("MessageToReply") + repliedTo Reply? @relation("ReplyToMessage") + MessagePing MessagePing[] + ChannelPin ChannelPin? +} + +model Reply { + message Message @relation("MessageToReply", fields: [messageId], references: [id]) //message text + messageId String @unique //message id of the reply + repliesTo Message @relation("ReplyToMessage", fields: [repliesToId], references: [id]) //message id that this message replies to + repliesToId String @unique //replies to this message id + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@unique([messageId, repliesToId]) +} + +model MessagePing { + Message Message @relation(fields: [messageId], references: [id]) + messageId String + PingsUser User @relation(fields: [pingsUserId], references: [id]) + pingsUserId String + createdAt DateTime @default(now()) + + @@unique([messageId, pingsUserId]) +} diff --git a/concord-server/src/controller/categoryController.ts b/concord-server/src/controller/categoryController.ts new file mode 100644 index 0000000..c2b5a1b --- /dev/null +++ b/concord-server/src/controller/categoryController.ts @@ -0,0 +1,40 @@ +import { + createCategory, + getCategory, + getCategoriesByInstance, + updateCategory, + deleteCategory, + deleteAllCategoriesFromInstance, +} from "../services/channelService"; +import { + CreateCategoryInput, + UpdateCategoryInput, + DeleteCategoryInput, + DeleteCategoriesByInstanceIdInput, +} from "../validators/categoryValidator"; + +export async function createNewCategory(data: CreateCategoryInput) { + return await createCategory(data); +} + +export async function fetchCategoryData(id: string) { + return await getCategory(id); +} + +export async function fetchCategoriesByInstance(instanceId: string) { + return await getCategoriesByInstance(instanceId); +} + +export async function updateExistingCategory(data: UpdateCategoryInput) { + return await updateCategory(data); +} + +export async function deleteExistingCategory(data: DeleteCategoryInput) { + return await deleteCategory(data); +} + +export async function deleteAllCategoriesByInstance( + data: DeleteCategoriesByInstanceIdInput, +) { + return await deleteAllCategoriesFromInstance(data); +} diff --git a/concord-server/src/controller/channelController.ts b/concord-server/src/controller/channelController.ts new file mode 100644 index 0000000..1c09a84 --- /dev/null +++ b/concord-server/src/controller/channelController.ts @@ -0,0 +1,40 @@ +import { + createChannel, + getChannel, + getChannelsByCategory, + updateChannel, + deleteChannel, + deleteAllChannelsFromCategory, +} from "../services/channelService"; +import { + CreateChannelInput, + UpdateChannelInput, + DeleteChannelInput, + DeleteChannelsByCategoryIdInput, +} from "../validators/channelValidator"; + +export async function createNewChannel(data: CreateChannelInput) { + return await createChannel(data); +} + +export async function fetchChannelData(id: string) { + return await getChannel(id); +} + +export async function fetchChannelsByCategory(categoryId: string) { + return await getChannelsByCategory(categoryId); +} + +export async function updateExistingChannel(data: UpdateChannelInput) { + return await updateChannel(data); +} + +export async function deleteExistingChannel(data: DeleteChannelInput) { + return await deleteChannel(data); +} + +export async function deleteAllChannelsByCategory( + data: DeleteChannelsByCategoryIdInput, +) { + return await deleteAllChannelsFromCategory(data); +} diff --git a/concord-server/src/controller/instanceController.ts b/concord-server/src/controller/instanceController.ts new file mode 100644 index 0000000..a586e35 --- /dev/null +++ b/concord-server/src/controller/instanceController.ts @@ -0,0 +1,10 @@ +import { createInstance, getAllInstances } from "../services/instanceService"; +import { CreateInstanceRequest } from "../validators/instanceValidator"; + +export async function createInstanceReq(data: CreateInstanceRequest) { + return await createInstance(data); +} + +export async function getAllInstancesReq() { + return await getAllInstances(); +} diff --git a/concord-server/src/controller/messageController.ts b/concord-server/src/controller/messageController.ts new file mode 100644 index 0000000..483b184 --- /dev/null +++ b/concord-server/src/controller/messageController.ts @@ -0,0 +1,29 @@ +import { + getMessageInformation, + getMessagesBefore, + sendMessageToChannel, +} from "../services/messageService"; + +export async function fetchMessageData(id: string) { + return await getMessageInformation(id); +} + +export async function fetchMessagesBefore(date: string, channelId: string) { + return getMessagesBefore(date, channelId); +} + +export async function sendMessage( + channelId: string, + userId: string, + content: string, + token: string, + repliedMessageId: string | null, +) { + return await sendMessageToChannel( + channelId, + userId, + content, + token, + repliedMessageId, + ); +} diff --git a/concord-server/src/controller/realtime.ts b/concord-server/src/controller/realtime.ts new file mode 100644 index 0000000..5f9b961 --- /dev/null +++ b/concord-server/src/controller/realtime.ts @@ -0,0 +1,126 @@ +import { Context } from "hono"; +import { + sendMessageToChannel, + removeMessageFromChannel, +} from "../services/realtime.js"; +import { success } from "zod"; + +export async function postMessageToChannel(io: any, c: Context) { + try { + io = c.get("io"); + + const instanceId = c.req.param("instanceId"); + const categoryId = c.req.param("categoryId"); + const channelId = c.req.param("channelId"); + const message = await c.req.json(); + + const result = await sendMessageToChannel( + instanceId, + categoryId, + channelId, + message, + "new_channel_message", + io, + ); + + if (result === "Event not implemented") { + console.log( + "controller::realtime::postMessageToChannel - Failed to send message", + ); + return c.json({ + success: false, + message: "Event not implemented or recognized", + status: 400, + }); + } + + if (result === "no acknowledgment") { + console.log( + "controller::realtime::postMessageToChannel - No acknowledgment received from client", + ); + return c.json({ + success: false, + message: "No acknowledgment received from client", + status: 500, + }); + } + + if (!result) { + throw new Error("failed to send message"); + } + + return c.json({ + success: true, + message: "Message sent successfully", + status: 200, + }); + } catch (err) { + const errMessage = err as Error; + console.log("controller::realtime::postMessageToChannel - ", errMessage); + return c.json({ + success: false, + message: errMessage.message, + status: 500, + }); + } +} + +export async function deleteMessageFromChannel(io: any, c: Context) { + try { + io = c.get("io"); + + const instanceId = c.req.param("instanceId"); + const categoryId = c.req.param("categoryId"); + const channelId = c.req.param("channelId"); + const messageId = c.req.param("messageId"); + + const result = await removeMessageFromChannel( + instanceId, + categoryId, + channelId, + messageId, + "delete_channel_message", + io, + ); + + if (result === "event not implemented") { + console.log( + "controller::realtime::deleteMessageFromChannel - Event not implemented", + ); + return c.json({ + success: false, + message: "Event not implemented or recognized", + status: 400, + }); + } + + if (result === "no acknowledgment") { + console.log( + "controller::realtime::deleteMessageFromChannel - No acknowledgment received from client", + ); + return c.json({ + success: false, + message: "No acknowledgment received from client", + status: 500, + }); + } + + if (!result) { + throw new Error("failed to delete message"); + } + + c.json({ + success: true, + message: "Message deleted successfully", + status: 200, + }); + } catch (err) { + const errMessage = err as Error; + console.log("services::realtime::deleteMessageFromChannel - ", errMessage); + return c.json({ + success: false, + message: errMessage.message, + status: 500, + }); + } +} diff --git a/concord-server/src/controller/userController.ts b/concord-server/src/controller/userController.ts new file mode 100644 index 0000000..0ba558d --- /dev/null +++ b/concord-server/src/controller/userController.ts @@ -0,0 +1,18 @@ +import { + getAllUsersFrom, + getUserInformation, + createUser, +} from "../services/userService"; +import { CreateUserInput } from "../validators/userValidator"; + +export async function fetchUserData(id: string) { + return await getUserInformation(id); +} + +export async function fetchAllUsers(instanceId: string) { + return await getAllUsersFrom(instanceId); +} + +export async function createNewUser(data: CreateUserInput) { + return await createUser(data); +} diff --git a/concord-server/src/helper/hashing.ts b/concord-server/src/helper/hashing.ts new file mode 100644 index 0000000..d987684 --- /dev/null +++ b/concord-server/src/helper/hashing.ts @@ -0,0 +1,5 @@ +import * as crypto from "crypto"; + +export default function shaHash(data: string, salt: string): string { + return crypto.createHmac("sha256", salt).update(data).digest("hex"); +} diff --git a/concord-server/src/index.ts b/concord-server/src/index.ts index 3191383..1348a03 100644 --- a/concord-server/src/index.ts +++ b/concord-server/src/index.ts @@ -1,9 +1,71 @@ -import { Hono } from 'hono' +import { Hono } from "hono"; +import { cors } from "hono/cors"; +import { Server as Engine } from "@socket.io/bun-engine"; +import { Server } from "socket.io"; +import routes from "./routes/index"; +import { Scalar } from "@scalar/hono-api-reference"; +import { openAPIRouteHandler } from "hono-openapi"; -const app = new Hono() +//initialize socket.io server +const io = new Server(); -app.get('/', (c) => { - return c.text('Hello Hono!') -}) +//initialize bun engine +//then bind to socket.io server +const engine = new Engine(); +io.bind(engine); -export default app +io.on("connection", (socket) => { + //get userId and clientId from query params + const userId = socket.handshake.query.userId; + const clientId = socket.handshake.query.clientId; + if (!userId || Array.isArray(userId)) { + socket.disconnect(); + throw new Error("Invalid user ID"); + } + + if (!clientId || Array.isArray(clientId)) { + socket.disconnect(); + throw new Error("Invalid client ID"); + } + + socket.join(userId); + console.log( + `User ${userId} connected. Client ID ${clientId} on socket ${socket.id}`, + ); + + socket.on("disconnect", () => { + console.log(`User ${userId} disconnected from socket ${socket.id}`); + }); +}); + +const app = new Hono(); + +app.use( + "*", + cors({ + origin: "http://localhost:5173", + allowHeaders: ["Content-Type", "Authorization"], + allowMethods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"], + credentials: true, + }), +); + +app.route("/api", routes); + +app.get( + "/openapi", + openAPIRouteHandler(app, { + documentation: { + info: { + title: "Hono API", + version: "1.0.0", + description: "Greeting API", + }, + servers: [{ url: "http://localhost:3000", description: "Local Server" }], + }, + }), +); + +app.get("/scalar", Scalar({ url: "/openapi" })); + +export default app; diff --git a/concord-server/src/routes/categoryRoutes.ts b/concord-server/src/routes/categoryRoutes.ts new file mode 100644 index 0000000..9b48532 --- /dev/null +++ b/concord-server/src/routes/categoryRoutes.ts @@ -0,0 +1,315 @@ +import { + createNewCategory, + fetchCategoryData, + fetchCategoriesByInstance, + updateExistingCategory, + deleteExistingCategory, + deleteAllCategoriesByInstance, +} from "../controller/categoryController"; + +import { + createCategorySchema, + getCategorySchema, + getCategoriesByInstanceIdSchema, + updateCategorySchema, + deleteCategorySchema, + deleteCategoriesByInstanceIdSchema, + CreateCategoryInput, + GetCategoryInput, + GetCategoriesByInstanceIdInput, + UpdateCategoryInput, + DeleteCategoryInput, + DeleteCategoriesByInstanceIdInput, +} from "../validators/categoryValidator"; +import { zValidator } from "@hono/zod-validator"; +import { Hono } from "hono"; +import { describeRoute, resolver } from "hono-openapi"; +const categoryRoutes = new Hono(); + +// Create a new category +categoryRoutes.post( + "/", + describeRoute({ + description: "Create a new category", + responses: { + 200: { + description: "Success creating category", + content: { + "application/json": { schema: resolver(createCategorySchema) }, + }, + }, + 400: { + description: "Bad Request - Invalid input data", + content: { + "application/json": { schema: resolver(createCategorySchema) }, + }, + }, + 401: { + description: "Unauthorized - Admin access required", + content: { + "application/json": { schema: resolver(createCategorySchema) }, + }, + }, + 404: { + description: "User Id not found", + content: { + "application/json": { schema: resolver(createCategorySchema) }, + }, + }, + }, + }), + zValidator("json", createCategorySchema), + async (c) => { + const data = c.req.valid("json") as CreateCategoryInput; + const categoryData = await createNewCategory(data); + if (categoryData) { + return c.json(categoryData); + } else { + return c.json({ error: "Failed to create category" }, 400); + } + }, +); + +// Get a category by ID +categoryRoutes.get( + "/:id", + describeRoute({ + description: "Get category by id", + responses: { + 200: { + description: "Success getting category", + content: { + "application/json": { schema: resolver(getCategorySchema) }, + }, + }, + 404: { + description: "Category id not found", + content: { + "application/json": { schema: resolver(getCategorySchema) }, + }, + }, + }, + }), + async (c) => { + const id = c.req.param("id"); + const categoryData = await fetchCategoryData(id); + if (categoryData) { + return c.json(categoryData); + } else { + return c.json({ error: "Category not found" }, 404); + } + }, +); + +// Get all categories by instance ID +categoryRoutes.get( + "/instance/:instanceId", + describeRoute({ + description: "Get all categories by instance id", + responses: { + 200: { + description: "Success getting all categories in instance", + content: { + "application/json": { + schema: resolver(getCategoriesByInstanceIdSchema), + }, + }, + }, + 400: { + description: "Bad Request - Missing instance ID", + content: { + "application/json": { + schema: resolver(getCategoriesByInstanceIdSchema), + }, + }, + }, + }, + }), + async (c) => { + const instanceId = c.req.param("instanceId"); + if (!instanceId) { + return c.json({ error: "No instance id provided" }, 400); + } + + const categoryData = await fetchCategoriesByInstance(instanceId); + if (categoryData) { + return c.json(categoryData); + } else { + return c.json( + { error: "Error getting all categories from instance" }, + 500, + ); + } + }, +); + +// Update a category +categoryRoutes.put( + "/:id", + describeRoute({ + description: "Update an existing category", + responses: { + 200: { + description: "Success updating category", + content: { + "application/json": { schema: resolver(updateCategorySchema) }, + }, + }, + 400: { + description: "Bad Request - Invalid input data", + content: { + "application/json": { schema: resolver(updateCategorySchema) }, + }, + }, + 401: { + description: "Unauthorized - Admin access required", + content: { + "application/json": { schema: resolver(updateCategorySchema) }, + }, + }, + 404: { + description: "Category id or User Id not found", + content: { + "application/json": { schema: resolver(updateCategorySchema) }, + }, + }, + }, + }), + zValidator("json", updateCategorySchema), + async (c) => { + const id = c.req.param("id"); + const data = c.req.valid("json") as UpdateCategoryInput; + + // Ensure the ID in the path matches the one in the body + if (data.id && data.id !== id) { + return c.json({ error: "ID in path does not match ID in body" }, 400); + } + + // Set ID from path if not in body + if (!data.id) { + data.id = id; + } + + const categoryData = await updateExistingCategory(data); + if (categoryData) { + return c.json(categoryData); + } else { + return c.json({ error: "Failed to update category" }, 400); + } + }, +); + +// Delete a specific category +categoryRoutes.delete( + "/:id", + describeRoute({ + description: "Delete an existing category", + responses: { + 200: { + description: "Success deleting category", + content: { + "application/json": { schema: resolver(deleteCategorySchema) }, + }, + }, + 400: { + description: "Bad Request - Invalid input data", + content: { + "application/json": { schema: resolver(deleteCategorySchema) }, + }, + }, + 401: { + description: "Unauthorized - Admin access required", + content: { + "application/json": { schema: resolver(deleteCategorySchema) }, + }, + }, + 404: { + description: "Category id or User Id not found", + content: { + "application/json": { schema: resolver(deleteCategorySchema) }, + }, + }, + }, + }), + zValidator("json", deleteCategorySchema), + async (c) => { + const id = c.req.param("id"); + const data = c.req.valid("json") as DeleteCategoryInput; + + // Ensure the ID in the path matches the one in the body + if (data.id !== id) { + return c.json({ error: "ID in path does not match ID in body" }, 400); + } + + const categoryData = await deleteExistingCategory(data); + if (categoryData) { + return c.json(categoryData); + } else { + return c.json({ error: "Failed to delete category" }, 400); + } + }, +); + +// Delete all categories by instance ID +categoryRoutes.delete( + "/instance/:instanceId", + describeRoute({ + description: "Delete all categories by instance id", + responses: { + 200: { + description: "Success deleting all categories in instance", + content: { + "application/json": { + schema: resolver(deleteCategoriesByInstanceIdSchema), + }, + }, + }, + 400: { + description: "Bad Request - Invalid input data", + content: { + "application/json": { + schema: resolver(deleteCategoriesByInstanceIdSchema), + }, + }, + }, + 401: { + description: "Unauthorized - Admin access required", + content: { + "application/json": { + schema: resolver(deleteCategoriesByInstanceIdSchema), + }, + }, + }, + 404: { + description: "Instance id or User Id not found", + content: { + "application/json": { + schema: resolver(deleteCategoriesByInstanceIdSchema), + }, + }, + }, + }, + }), + zValidator("json", deleteCategoriesByInstanceIdSchema), + async (c) => { + const instanceId = c.req.param("instanceId"); + const data = c.req.valid("json") as DeleteCategoriesByInstanceIdInput; + + // Ensure the instanceId in the path matches the one in the body + if (data.instanceId !== instanceId) { + return c.json( + { error: "Instance ID in path does not match Instance ID in body" }, + 400, + ); + } + + const categoryData = await deleteAllCategoriesByInstance(data); + if (categoryData) { + return c.json(categoryData); + } else { + return c.json({ error: "Failed to delete categories" }, 400); + } + }, +); + +export { categoryRoutes }; diff --git a/concord-server/src/routes/channelRoutes.ts b/concord-server/src/routes/channelRoutes.ts new file mode 100644 index 0000000..43f38ad --- /dev/null +++ b/concord-server/src/routes/channelRoutes.ts @@ -0,0 +1,313 @@ +import { + createNewChannel, + fetchChannelData, + fetchChannelsByCategory, + updateExistingChannel, + deleteExistingChannel, + deleteAllChannelsByCategory, +} from "../controller/channelController"; + +import { + createChannelSchema, + getChannelSchema, + getChannelsByCategoryIdSchema, + updateChannelSchema, + deleteChannelSchema, + deleteChannelsByCategoryIdSchema, + CreateChannelInput, + GetChannelInput, + GetChannelsByCategoryIdInput, + UpdateChannelInput, + DeleteChannelInput, + DeleteChannelsByCategoryIdInput, +} from "../validators/channelValidator"; +import { zValidator } from "@hono/zod-validator"; +import { Hono } from "hono"; +import { describeRoute, resolver } from "hono-openapi"; + +const channelRoutes = new Hono(); + +// Create a new channel +channelRoutes.post( + "/", + describeRoute({ + description: "Create a new channel", + responses: { + 200: { + description: "Success creating channel", + content: { + "application/json": { schema: resolver(createChannelSchema) }, + }, + }, + 400: { + description: "Bad Request - Invalid input data", + content: { + "application/json": { schema: resolver(createChannelSchema) }, + }, + }, + 401: { + description: "Unauthorized - Admin access required", + content: { + "application/json": { schema: resolver(createChannelSchema) }, + }, + }, + 404: { + description: "User Id not found", + content: { + "application/json": { schema: resolver(createChannelSchema) }, + }, + }, + }, + }), + zValidator("json", createChannelSchema), + async (c) => { + const data = c.req.valid("json") as CreateChannelInput; + const channelData = await createNewChannel(data); + if (channelData) { + return c.json(channelData); + } else { + return c.json({ error: "Failed to create channel" }, 400); + } + }, +); + +// Get a channel by ID +channelRoutes.get( + "/:id", + describeRoute({ + description: "Get channel by id", + responses: { + 200: { + description: "Success getting channel", + content: { + "application/json": { schema: resolver(getChannelSchema) }, + }, + }, + 404: { + description: "Channel id not found", + content: { + "application/json": { schema: resolver(getChannelSchema) }, + }, + }, + }, + }), + async (c) => { + const id = c.req.param("id"); + const channelData = await fetchChannelData(id); + if (channelData) { + return c.json(channelData); + } else { + return c.json({ error: "Channel not found" }, 404); + } + }, +); + +// Get all channels by category ID +channelRoutes.get( + "/category/:categoryId", + describeRoute({ + description: "Get all channels by category id", + responses: { + 200: { + description: "Success getting all channels in category", + content: { + "application/json": { + schema: resolver(getChannelsByCategoryIdSchema), + }, + }, + }, + 400: { + description: "Bad Request - Missing category ID", + content: { + "application/json": { + schema: resolver(getChannelsByCategoryIdSchema), + }, + }, + }, + }, + }), + async (c) => { + const categoryId = c.req.param("categoryId"); + if (!categoryId) { + return c.json({ error: "No category id provided" }, 400); + } + + const channels = await fetchChannelsByCategory(categoryId); + if (channels) { + return c.json(channels); + } else { + return c.json({ error: "Error getting channels from category" }, 500); + } + }, +); + +// Update a channel +channelRoutes.put( + "/:id", + describeRoute({ + description: "Update an existing channel", + responses: { + 200: { + description: "Success updating channel", + content: { + "application/json": { schema: resolver(updateChannelSchema) }, + }, + }, + 400: { + description: "Bad Request - Invalid input data", + content: { + "application/json": { schema: resolver(updateChannelSchema) }, + }, + }, + 401: { + description: "Unauthorized - Admin access required", + content: { + "application/json": { schema: resolver(updateChannelSchema) }, + }, + }, + 404: { + description: "Channel id or User Id not found", + content: { + "application/json": { schema: resolver(updateChannelSchema) }, + }, + }, + }, + }), + zValidator("json", updateChannelSchema), + async (c) => { + const id = c.req.param("id"); + const data = c.req.valid("json") as UpdateChannelInput; + + // Ensure the ID in the path matches the one in the body + if (data.id && data.id !== id) { + return c.json({ error: "ID in path does not match ID in body" }, 400); + } + + // Set ID from path if not in body + if (!data.id) { + data.id = id; + } + + const result = await updateExistingChannel(data); + if (result) { + return c.json(result); + } else { + return c.json({ error: "Failed to update channel" }, 400); + } + }, +); + +// Delete a specific channel +channelRoutes.delete( + "/:id", + describeRoute({ + description: "Delete an existing channel", + responses: { + 200: { + description: "Success deleting channel", + content: { + "application/json": { schema: resolver(deleteChannelSchema) }, + }, + }, + 400: { + description: "Bad Request - Invalid input data", + content: { + "application/json": { schema: resolver(deleteChannelSchema) }, + }, + }, + 401: { + description: "Unauthorized - Admin access required", + content: { + "application/json": { schema: resolver(deleteChannelSchema) }, + }, + }, + 404: { + description: "Channel id or User Id not found", + content: { + "application/json": { schema: resolver(deleteChannelSchema) }, + }, + }, + }, + }), + zValidator("json", deleteChannelSchema), + async (c) => { + const id = c.req.param("id"); + const data = c.req.valid("json") as DeleteChannelInput; + + // Ensure the ID in the path matches the one in the body + if (data.id !== id) { + return c.json({ error: "ID in path does not match ID in body" }, 400); + } + + const result = await deleteExistingChannel(data); + if (result) { + return c.json({ success: true }); + } else { + return c.json({ error: "Failed to delete channel" }, 400); + } + }, +); + +// Delete all channels by category ID +channelRoutes.delete( + "/category/:categoryId", + describeRoute({ + description: "Delete all channels by category id", + responses: { + 200: { + description: "Success deleting all channels in category", + content: { + "application/json": { + schema: resolver(deleteChannelsByCategoryIdSchema), + }, + }, + }, + 400: { + description: "Bad Request - Invalid input data", + content: { + "application/json": { + schema: resolver(deleteChannelsByCategoryIdSchema), + }, + }, + }, + 401: { + description: "Unauthorized - Admin access required", + content: { + "application/json": { + schema: resolver(deleteChannelsByCategoryIdSchema), + }, + }, + }, + 404: { + description: "Category id or User Id not found", + content: { + "application/json": { + schema: resolver(deleteChannelsByCategoryIdSchema), + }, + }, + }, + }, + }), + zValidator("json", deleteChannelsByCategoryIdSchema), + async (c) => { + const categoryId = c.req.param("categoryId"); + const data = c.req.valid("json") as DeleteChannelsByCategoryIdInput; + + // Ensure the categoryId in the path matches the one in the body + if (data.categoryId !== categoryId) { + return c.json( + { error: "Category ID in path does not match Category ID in body" }, + 400, + ); + } + + const result = await deleteAllChannelsByCategory(data); + if (result) { + return c.json({ success: true }); + } else { + return c.json({ error: "Failed to delete channels" }, 400); + } + }, +); + +export { channelRoutes }; diff --git a/concord-server/src/routes/index.ts b/concord-server/src/routes/index.ts new file mode 100644 index 0000000..84ef367 --- /dev/null +++ b/concord-server/src/routes/index.ts @@ -0,0 +1,17 @@ +//place exported routes below this line +import { Hono } from "hono"; +import userRoutes from "./userRoutes"; +import messageRoutes from "./messageRoutes"; +import { channelRoutes } from "./channelRoutes"; +import instanceRoutes from "./instanceRoutes"; +import { categoryRoutes } from "./categoryRoutes"; + +const routes = new Hono(); + +routes.route("/user", userRoutes); +routes.route("/message", messageRoutes); +routes.route("/channel", channelRoutes); +routes.route("/instance", instanceRoutes); +routes.route("/category", categoryRoutes); + +export default routes; diff --git a/concord-server/src/routes/instanceRoutes.ts b/concord-server/src/routes/instanceRoutes.ts new file mode 100644 index 0000000..4e27d75 --- /dev/null +++ b/concord-server/src/routes/instanceRoutes.ts @@ -0,0 +1,77 @@ +import { Hono } from "hono"; +import { describeRoute, resolver } from "hono-openapi"; +import { + createInstanceRequestSchema, + getAllInstancesResponseSchema, +} from "../validators/instanceValidator"; +import { zValidator } from "@hono/zod-validator"; +import { + createInstanceReq, + getAllInstancesReq, +} from "../controller/instanceController"; + +const instanceRoutes = new Hono(); + +instanceRoutes.post( + "", + describeRoute({ + description: "Create instance", + responses: { + 200: { + description: "Instance created", + content: { + "application/json": { schema: resolver(createInstanceRequestSchema) }, + }, + }, + 400: { + description: "Invalid request", + }, + }, + }), + zValidator("json", createInstanceRequestSchema), + async (c) => { + const data = await c.req.json(); + if (!data) { + return c.json({ error: "could not parse data" }, 400); + } + + const instance = await createInstanceReq(data); + return c.json(instance, 201); + }, +); + +instanceRoutes.get( + "", + describeRoute({ + description: "Get all instances", + responses: { + 200: { + description: "List of all instances", + content: { + "application/json": { + schema: resolver(getAllInstancesResponseSchema), + }, + }, + }, + 500: { + description: "Server error", + }, + }, + }), + async (c) => { + const instances = await getAllInstancesReq(); + if (instances.success) { + return c.json(instances, 200); + } else { + return c.json( + { + success: false, + error: instances.error || "Failed to fetch instances", + }, + 500, + ); + } + }, +); + +export default instanceRoutes; diff --git a/concord-server/src/routes/messageRoutes.ts b/concord-server/src/routes/messageRoutes.ts new file mode 100644 index 0000000..17a576e --- /dev/null +++ b/concord-server/src/routes/messageRoutes.ts @@ -0,0 +1,146 @@ +import { Hono } from "hono"; +import { describeResponse, describeRoute, resolver } from "hono-openapi"; +import { + getMessageByIdSchema, + getMessagesBeforeDate, + sendMessageSchema, +} from "../validators/messageValidator"; +import { zValidator } from "@hono/zod-validator"; +import { + fetchMessageData, + fetchMessagesBefore, + sendMessage, +} from "../controller/messageController"; + +const messageRoutes = new Hono(); + +messageRoutes.get( + "/:id", + describeRoute({ + description: "Get message by id", + responses: { + 200: { + description: "Success getting message", + content: { + "application/json": { schema: resolver(getMessageByIdSchema) }, + }, + }, + 404: { + description: "Message id not found", + content: { + "application/json": { schema: resolver(getMessageByIdSchema) }, + }, + }, + }, + }), + zValidator("param", getMessageByIdSchema), + async (c) => { + const id = c.req.param("id"); + const messageData = await fetchMessageData(id); + + if (messageData) { + return c.json(messageData, 200); + } else { + return c.json({ error: "Message not found" }, 404); + } + }, +); + +messageRoutes.get( + "", + describeRoute({ + description: "Get up to 50 messages prior to given datetime", + responses: { + 200: { + description: "Success getting up to 50 messages", + content: { + "application/json": { schema: resolver(getMessagesBeforeDate) }, + }, + }, + }, + }), + zValidator("query", getMessagesBeforeDate), + async (c) => { + const date = c.req.query("date"); + if (!date) { + return c.json({ error: "date not provided" }, 400); + } + + const channelId = c.req.query("channelId"); + if (!channelId) { + return c.json({ error: "channelId not provided" }, 400); + } + + const messagesArr = await fetchMessagesBefore(date, channelId); + + if (messagesArr) { + return c.json(messagesArr, 200); + } else { + return c.json({ error: "Failed to fetch messages" }, 500); + } + }, +); + +messageRoutes.post( + "", + describeRoute({ + description: "Send a message to a channel", + responses: { + 201: { + description: "Message sent successfully", + content: { + "application/json": { schema: resolver(sendMessageSchema) }, + }, + }, + 401: { + description: "Unauthorized - invalid token or user credentials", + content: { + "application/json": { + schema: { + type: "object", + properties: { error: { type: "string" } }, + }, + }, + }, + }, + 500: { + description: "Server error", + content: { + "application/json": { + schema: { + type: "object", + properties: { error: { type: "string" } }, + }, + }, + }, + }, + }, + }), + zValidator("json", sendMessageSchema), + async (c) => { + const { channelId, userId, content, token, repliedMessageId } = + await c.req.json(); + + const result = await sendMessage( + channelId, + userId, + content, + token, + repliedMessageId || null, + ); + + if (result) { + return c.json(result, 201); + } else { + return c.json( + { + error: + "Failed to send message. Check your credentials and try again.", + }, + 401, + ); + } + }, +); + +export default messageRoutes; diff --git a/concord-server/src/routes/realtime.ts b/concord-server/src/routes/realtime.ts new file mode 100644 index 0000000..933b839 --- /dev/null +++ b/concord-server/src/routes/realtime.ts @@ -0,0 +1,29 @@ +import { Hono } from "hono"; +import { zValidator } from "@hono/zod-validator"; +import { describeRoute, resolver } from "hono-openapi"; +import { + postMessageToChannel, + deleteMessageFromChannel, +} from "../controller/realtime"; + +const app = new Hono(); + +app.post( + "message/", + zValidator({ + body: z.object({ + content: z.string().min(1).max(500), + }), + }), + async (c) => { + const { instanceId, categoryId, channelId } = c.req.params; + const { content } = c.req.body; + + return postMessageToChannel(c.get("io"), { + instanceId, + categoryId, + channelId, + content, + }); + }, +); diff --git a/concord-server/src/routes/userRoutes.ts b/concord-server/src/routes/userRoutes.ts new file mode 100644 index 0000000..a2c1fd9 --- /dev/null +++ b/concord-server/src/routes/userRoutes.ts @@ -0,0 +1,110 @@ +import { Hono } from "hono"; +import { + fetchAllUsers, + fetchUserData, + createNewUser, +} from "../controller/userController"; +import { + createUserSchema, + queryAllUsersByInstanceId, + queryUserByIdSchema, +} from "../validators/userValidator"; +import { zValidator } from "@hono/zod-validator"; +import { describeRoute, resolver } from "hono-openapi"; +const userRoutes = new Hono(); + +userRoutes.get( + "/:id", + describeRoute({ + description: "Get user by id", + responses: { + 200: { + description: "Success getting user", + content: { + "application/json": { schema: resolver(queryUserByIdSchema) }, + }, + }, + 404: { + description: "User id not found", + content: { + "application/json": { schema: resolver(queryUserByIdSchema) }, + }, + }, + }, + }), + zValidator("param", queryUserByIdSchema), + async (c) => { + const id = c.req.param("id"); + const userData = await fetchUserData(id); + if (userData) { + return c.json(userData); + } else { + return c.json({ error: "User not found" }, 404); + } + }, +); + +userRoutes.get( + "", + describeRoute({ + description: "Get all users by instance id", + responses: { + 200: { + description: "Success getting all users in instance", + content: { + "application/json": { schema: resolver(queryAllUsersByInstanceId) }, + }, + }, + }, + }), + zValidator("query", queryAllUsersByInstanceId), + async (c) => { + const instanceId = c.req.query("instanceId"); + if (!instanceId) { + return c.json({ error: "No instance id provided" }, 400); + } + + const userData = await fetchAllUsers(instanceId); + if (userData) { + return c.json(userData); + } else { + return c.json({ error: "Error getting all users from instance" }, 500); + } + }, +); + +userRoutes.post( + "", + describeRoute({ + description: "Create a new user", + responses: { + 201: { + description: "Success", + content: { + "application/json": { schema: resolver(createUserSchema) }, + }, + }, + 400: { + description: "Bad request (user exists)", + content: { + "application/json": { schema: resolver(createUserSchema) }, + }, + }, + }, + }), + zValidator("json", createUserSchema), + async (c) => { + try { + const data = await c.req.json(); + const newUser = await createNewUser(data); + if (!newUser) { + return c.json({ error: "User already exists" }, 400); + } + return c.json(newUser, 201); + } catch (error) { + return c.json({ error: "Error creating user" }, 500); + } + }, +); + +export default userRoutes; diff --git a/concord-server/src/services/channelService.ts b/concord-server/src/services/channelService.ts new file mode 100644 index 0000000..4e12c51 --- /dev/null +++ b/concord-server/src/services/channelService.ts @@ -0,0 +1,457 @@ +import { Channel, Category } from "@prisma/client"; +import { PrismaClient } from "@prisma/client"; +import { getUserInformation, getUserCredentials } from "./userService"; +import { + CreateChannelInput, + UpdateChannelInput, + DeleteChannelInput, + DeleteChannelsByCategoryIdInput, +} from "../validators/channelValidator"; +import { + UpdateCategoryInput, + DeleteCategoryInput, + DeleteCategoriesByInstanceIdInput, + CreateCategoryInput, +} from "../validators/categoryValidator"; + +const prisma = new PrismaClient(); + +export async function createCategory( + data: CreateCategoryInput, +): Promise { + try { + //Confirm if user exists and is admin + const requestingUser = await getUserInformation(data.requestingUserId); + const requestingUserCredentials = await getUserCredentials( + data.requestingUserId, + ); + + if ( + !requestingUser || + !requestingUserCredentials || + !requestingUser.admin || + requestingUserCredentials.token == null || + data.requestingUserToken != requestingUserCredentials.token + ) { + return null; + } + + const newCategory = await prisma.category.create({ + data: { + name: data.name, + position: data.position, + }, + }); + + if (!newCategory) { + throw new Error("could not create category"); + } + + let curInstance; + if (data.instanceId) { + curInstance = await prisma.instance.findUnique({ + where: { + id: data.instanceId, + }, + include: { + Category: true, + }, + }); + + if (!curInstance) { + throw new Error("could not find instance to add category to"); + } + + await prisma.category.update({ + where: { + id: newCategory.id, + }, + data: { + instanceId: curInstance.id, + }, + }); + + return newCategory; + } + + return newCategory; + } catch (err) { + console.log("services::channelService::createCategory - ", err); + return null; + } +} + +export async function getCategory( + categoryId: string, +): Promise { + try { + const category = await prisma.category.findUnique({ + where: { + id: categoryId, + }, + }); + + if (!category) { + throw new Error("could not find category"); + } + + return category; + } catch (err) { + console.log("services::channelService::getCategory - ", err); + return null; + } +} + +export async function getCategoriesByInstance( + instanceId: string, +): Promise { + try { + const categories = await prisma.category.findMany({ + where: { + instanceId: instanceId, + }, + include: { + Channel: true, + }, + orderBy: { + position: "asc", + }, + }); + + if (!categories) { + throw new Error("could not find categories for instance"); + } + + return categories; + } catch (err) { + console.log("services::channelService::getCategoriesByInstance - ", err); + return null; + } +} + +export async function updateCategory( + data: UpdateCategoryInput, +): Promise { + try { + //Confirm if user exists and is admin + const requestingUser = await getUserInformation(data.requestingUserId); + const requestingUserCredentials = await getUserCredentials( + data.requestingUserId, + ); + + if ( + !requestingUser || + !requestingUserCredentials || + !requestingUser.admin || + requestingUserCredentials.token == null || + data.requestingUserToken != requestingUserCredentials.token + ) { + return null; + } + + const updatedCategory = await prisma.category.update({ + where: { + id: data.id, + }, + data: { + name: data.name, + position: data.position, + Channel: data.channels ? { set: data.channels } : undefined, + }, + }); + + if (!updatedCategory) { + throw new Error("could not update category"); + } + + return updatedCategory; + } catch (err) { + console.log("services::channelService::updateCategory - ", err); + return null; + } +} + +export async function deleteCategory( + data: DeleteCategoryInput, +): Promise { + try { + //Confirm if user exists and is admin + const requestingUser = await getUserInformation(data.requestingUserId); + const requestingUserCredentials = await getUserCredentials( + data.requestingUserId, + ); + + if ( + !requestingUser || + !requestingUserCredentials || + !requestingUser.admin || + requestingUserCredentials.token == null || + data.requestingUserToken != requestingUserCredentials.token + ) { + return null; + } + + const deleteAllChannels = await prisma.channel.deleteMany({ + where: { + categoryId: data.id, + }, + }); + + if (deleteAllChannels.count === 0) { + throw new Error("could not delete channels from category"); + } + + const deletedCategory = await prisma.category.delete({ + where: { + id: data.id, + }, + }); + + if (!deletedCategory) { + throw new Error("could not delete category"); + } + + return true; + } catch (err) { + console.log("services::channelService::deleteCategory - ", err); + return false; + } +} + +export async function deleteAllCategoriesFromInstance( + data: DeleteCategoriesByInstanceIdInput, +): Promise { + try { + //Confirm if user exists and is admin + const requestingUser = await getUserInformation(data.requestingUserId); + const requestingUserCredentials = await getUserCredentials( + data.requestingUserId, + ); + + if ( + !requestingUser || + !requestingUserCredentials || + !requestingUser.admin || + requestingUserCredentials.token == null || + data.requestingUserToken != requestingUserCredentials.token + ) { + return null; + } + + const deletedCategories = await prisma.category.deleteMany({ + where: { + instanceId: data.instanceId, + }, + }); + + if (deletedCategories.count === 0) { + throw new Error("could not delete categories from instance"); + } + + return true; + } catch (err) { + console.log( + "services::channelService::deleteAllCategoriesFromInstance - ", + err, + ); + return false; + } +} + +export async function createChannel( + data: CreateChannelInput, +): Promise { + try { + //Confirm if user exists and is admin + const requestingUser = await getUserInformation(data.requestingUserId); + const requestingUserCredentials = await getUserCredentials( + data.requestingUserId, + ); + + if ( + !requestingUser || + !requestingUserCredentials || + !requestingUser.admin || + requestingUserCredentials.token == null || + data.requestingUserToken != requestingUserCredentials.token + ) { + return null; + } + + const newChannel = await prisma.channel.create({ + data: { + type: data.type, + name: data.name, + description: data.description, + categoryId: data.categoryId ? data.categoryId : null, + }, + }); + + if (!newChannel) { + throw new Error("could not create channel"); + } + + return newChannel; + } catch (err) { + console.log("services::channelService::createChannel - ", err); + return null; + } +} + +export async function getChannel(channelId: string): Promise { + try { + const channel = await prisma.channel.findUnique({ + where: { + id: channelId, + }, + }); + + if (!channel) { + throw new Error("could not find channel"); + } + + return channel; + } catch (err) { + console.log("services::channelService::getChannel - ", err); + return null; + } +} + +export async function getChannelsByCategory( + categoryId: string, +): Promise { + try { + const channels = await prisma.channel.findMany({ + where: { + categoryId: categoryId, + }, + }); + + if (!channels) { + throw new Error("could not find channels for category"); + } + return channels; + } catch (err) { + console.log("services::channelService::getChannelsByCategory - ", err); + return null; + } +} + +export async function updateChannel( + data: UpdateChannelInput, +): Promise { + try { + //Confirm if user exists and is admin + const requestingUser = await getUserInformation(data.requestingUserId); + const requestingUserCredentials = await getUserCredentials( + data.requestingUserId, + ); + + if ( + !requestingUser || + !requestingUserCredentials || + !requestingUser.admin || + requestingUserCredentials.token == null || + data.requestingUserToken != requestingUserCredentials.token + ) { + return null; + } + + const updatedChannel = await prisma.channel.update({ + where: { + id: data.id, + }, + data: { + name: data.name, + description: data.description, + categoryId: data.categoryId ? data.categoryId : undefined, + }, + }); + + if (!updatedChannel) { + throw new Error("could not update channel"); + } + + return updatedChannel; + } catch (err) { + console.log("services::channelService::updateChannel - ", err); + return null; + } +} + +export async function deleteChannel( + data: DeleteChannelInput, +): Promise { + try { + //Confirm if user exists and is admin + const requestingUser = await getUserInformation(data.requestingUserId); + const requestingUserCredentials = await getUserCredentials( + data.requestingUserId, + ); + + if ( + !requestingUser || + !requestingUserCredentials || + !requestingUser.admin || + requestingUserCredentials.token == null || + data.requestingUserToken != requestingUserCredentials.token + ) { + return null; + } + + const deletedChannel = await prisma.channel.delete({ + where: { + id: data.id, + }, + }); + + if (!deletedChannel) { + throw new Error("could not delete channel"); + } + + return true; + } catch (err) { + console.log("services::channelService::deleteChannel - ", err); + return false; + } +} + +export async function deleteAllChannelsFromCategory( + data: DeleteChannelsByCategoryIdInput, +): Promise { + try { + //Confirm if user exists and is admin + const requestingUser = await getUserInformation(data.requestingUserId); + const requestingUserCredentials = await getUserCredentials( + data.requestingUserId, + ); + + if ( + !requestingUser || + !requestingUserCredentials || + !requestingUser.admin || + requestingUserCredentials.token == null || + data.requestingUserToken != requestingUserCredentials.token + ) { + return null; + } + + const deletedChannels = await prisma.channel.deleteMany({ + where: { + categoryId: data.categoryId, + }, + }); + + if (deletedChannels.count === 0) { + throw new Error("could not delete channels from category"); + } + + return true; + } catch (err) { + console.log( + "services::channelService::deleteAllChannelsFromCategory - ", + err, + ); + return false; + } +} diff --git a/concord-server/src/services/instanceService.ts b/concord-server/src/services/instanceService.ts new file mode 100644 index 0000000..bb4008a --- /dev/null +++ b/concord-server/src/services/instanceService.ts @@ -0,0 +1,59 @@ +import { PrismaClient } from "@prisma/client"; +import { CreateInstanceRequest } from "../validators/instanceValidator"; +import { getUserCredentials, getUserInformation } from "./userService"; + +const prisma = new PrismaClient(); + +export async function createInstance(data: CreateInstanceRequest) { + try { + const creds = await getUserCredentials(data.requestingUserId); + const user = await getUserInformation(data.requestingUserId); + if ( + !creds || + creds.token != data.requestingUserToken || + !user || + !user.admin + ) { + return null; + } + + const newInstance = await prisma.instance.create({ + data: { + name: data.name, + icon: data.icon, + }, + }); + + return { + success: true, + data: newInstance, + }; + } catch (error) { + console.error("Error creating instance:", error); + return { + success: false, + error: "Failed to create instance", + }; + } +} + +export async function getAllInstances() { + try { + const instances = await prisma.instance.findMany({ + orderBy: { + createdAt: "desc", + }, + }); + + return { + success: true, + data: instances, + }; + } catch (error) { + console.error("Error fetching instances:", error); + return { + success: false, + error: "Failed to fetch instances", + }; + } +} diff --git a/concord-server/src/services/messageService.ts b/concord-server/src/services/messageService.ts new file mode 100644 index 0000000..a01f59e --- /dev/null +++ b/concord-server/src/services/messageService.ts @@ -0,0 +1,230 @@ +import { PrismaClient } from "@prisma/client"; +import { getUserCredentials } from "./userService"; + +const prisma = new PrismaClient(); + +export async function getMessageInformation(id: string): Promise<{ + id: string; + channelId: string; + userId: string; + text: string; + deleted: boolean; + replies: null | { + messageId: string; + repliesToId: string; + repliesToText: string; + }; +} | null> { + try { + if (!id) { + throw new Error("missing messageId"); + } + + const message = await prisma.message.findUnique({ + where: { + id: id, + }, + }); + + if (!message) { + throw new Error("could not find message"); + } + + // Check if this message is a reply to another message + const replyData = await prisma.reply.findFirst({ + where: { + messageId: id, + }, + }); + + let originalMessage = null; + if (replyData) { + originalMessage = await prisma.message.findUnique({ + where: { + id: replyData.repliesToId, + }, + }); + } + + return { + id: message.id, + channelId: message.channelId!, + userId: message.userId!, + text: message.text, + deleted: message.deleted, + replies: originalMessage + ? { + messageId: message.id, + repliesToId: originalMessage.id, + repliesToText: originalMessage.text, + } + : null, + }; + } catch (err) { + const errMessage = err as Error; + + if (errMessage.message === "missing messageId") { + console.log( + "services::actions::getMessageInformation - missing messageId", + ); + return null; + } + + if (errMessage.message === "could not find message") { + console.log( + "services::actions::getMessageInformation - unable to find message", + ); + return null; + } + + console.log( + "services::actions::getMessageInformation - unknown error", + errMessage, + ); + return null; + } +} + +export async function getMessagesBefore(date: string, channelId: string) { + try { + if (!date || !channelId) { + throw new Error("missing date or channelId"); + } + + const messages = await prisma.message.findMany({ + where: { + channelId: channelId, + createdAt: { + lt: new Date(date), + }, + }, + orderBy: { + createdAt: "desc", + }, + take: 50, + }); + + const messageInformationPromises = messages.map(async (message) => { + const replyData = await prisma.reply.findFirst({ + where: { + messageId: message.id, + }, + }); + + let originalMessage = null; + if (replyData) { + originalMessage = await prisma.message.findUnique({ + where: { + id: replyData.repliesToId, + }, + }); + } + + return { + id: message.id, + channelId: message.channelId!, + userId: message.userId!, + text: message.text, + deleted: message.deleted, + replies: originalMessage + ? { + messageId: message.id, + repliesToId: originalMessage.id, + repliesToText: originalMessage.text, + } + : null, + }; + }); + + return Promise.all(messageInformationPromises); + } catch (err) { + const errMessage = err as Error; + + if (errMessage.message === "missing date or channelId") { + console.log( + "services::actions::getMessagesBefore - missing date or channelId", + ); + return null; + } + + console.log( + "services::actions::getMessagesBefore - unknown error", + errMessage, + ); + return null; + } +} + +export async function sendMessageToChannel( + channelId: string, + userId: string, + content: string, + token: string, + repliedMessageId: string | null, +): Promise<{ + id: string; + channelId: string; + userId: string; + text: string; + deleted: boolean; + replies: null | { + messageId: string; + repliesToId: string; + repliesToText: string; + }; +} | null> { + try { + const userCreds = await getUserCredentials(userId); + if (!userCreds || userCreds.token != token) { + return null; + } + + const newMessage = await prisma.message.create({ + data: { + channelId: channelId, + userId: userId, + text: content, + deleted: false, + }, + }); + + if (!newMessage) { + return null; + } + + let origMessage; + if (repliedMessageId) { + origMessage = await prisma.message.findUnique({ + where: { + id: repliedMessageId, + }, + }); + + if (!origMessage) { + throw new Error("could not find original message to reply to"); + } + + await prisma.reply.create({ + data: { + messageId: newMessage.id, + repliesToId: origMessage.id, + }, + }); + } + + return { + ...newMessage, + channelId: newMessage.channelId!, + userId: newMessage.userId!, + replies: origMessage + ? { + messageId: newMessage.id, + repliesToId: origMessage?.id, + repliesToText: origMessage?.text, + } + : null, + }; + } catch (error) { + return null; + } +} diff --git a/concord-server/src/services/realtime.ts b/concord-server/src/services/realtime.ts new file mode 100644 index 0000000..434de72 --- /dev/null +++ b/concord-server/src/services/realtime.ts @@ -0,0 +1,90 @@ +import { readonly } from "zod"; + +const EVENTS = { + NEW_CHANNEL_MESSAGE: "new_channel_message", + DELETE_CHANNEL_MESSAGE: "delete_channel_message", +}; + +export async function sendMessageToChannel( + instanceId: string, + categoryId: string, + channelId: string, + message: any, + event: string, + io: any, +): Promise { + try { + //TODO: implement middleware to replace this + if (EVENTS.NEW_CHANNEL_MESSAGE === event) { + throw new Error("Event not implemented"); + } + + //TODO: add prisma to save channel message to DB + + return new Promise((resolve) => { + io.to(instanceId).emit(event, message, (ack: any) => { + if (ack && ack.status === "received") { + console.log(`Message ${ack.messageId} acknowledged by client.`); + resolve(true); + } else { + console.log( + "services::realtime::sendMessageToChannel No acknowledgment received from client.", + ); + resolve("no acknowledgment"); + } + }); + }); + } catch (err) { + const errMessage = err as Error; + if (errMessage.message === "Event not implemented") { + console.log( + `services::realtime::sendMessageToChannel - Event not implemented. Attempted event: ${event}`, + ); + return "event not implemented"; + } + console.log("services::realtime::sendMessageToChannel - ", errMessage); + return false; + } +} + +export async function removeMessageFromChannel( + instanceId: string, + categoryId: string, + channelId: string, + messageId: string, + event: string, + io: any, +): Promise { + try { + //TODO: implement middleware to replace this + if (EVENTS.DELETE_CHANNEL_MESSAGE === event) { + throw new Error("event not implemented"); + } + + //TODO: add prisma to flag a channel message as deleted + + return new Promise((resolve) => { + io.to(instanceId).emit(event, { messageId }, (ack: any) => { + if (ack && ack.status === "received") { + console.log(`Message ${ack.messageId} acknowledged by client.`); + resolve(true); + } else { + console.log( + "services::realtime::deleteMessageFromChannel No acknowledgment received from client.", + ); + resolve("no acknowledgment"); + } + }); + }); + } catch (err) { + const errMessage = err as Error; + if (errMessage.message === "Event not implemented") { + console.log( + `services::realtime::deleteMessageFromChannel - Event not implemented. Attempted event: ${event}`, + ); + return false; + } + console.log("services::realtime::deleteMessageFromChannel - ", errMessage); + return false; + } +} diff --git a/concord-server/src/services/userService.ts b/concord-server/src/services/userService.ts new file mode 100644 index 0000000..4730981 --- /dev/null +++ b/concord-server/src/services/userService.ts @@ -0,0 +1,297 @@ +import { + Message, + MessagePing, + PrismaClient, + Role, + UserAuth, +} from "@prisma/client"; +import { CreateUserInput } from "../validators/userValidator"; +import shaHash from "../helper/hashing"; + +const prisma = new PrismaClient(); + +export async function createUser(data: CreateUserInput): Promise<{ + username: string; + nickname: string | null; + bio: string | null; + picture: string | null; + banner: string | null; + status: string; + admin: boolean; +} | null> { + const requestingUser = await getUserInformation(data.requestingUserId); + const requestingUserCredentials = await getUserCredentials( + data.requestingUserId, + ); + if ( + !requestingUser || + !requestingUserCredentials || + !requestingUser.admin || + requestingUserCredentials.token == null || + data.requestingUserToken != requestingUserCredentials.token + ) { + return null; + } + + if ((await prisma.user.count({ where: { username: data.username } })) >= 1) { + return null; + } + + const userData = await prisma.user.create({ + data: { + username: data.username, + nickname: data.nickname, + bio: data.bio, + picture: data.picture, + banner: data.banner, + status: data.status, + admin: data.admin, + }, + }); + + if ( + !(await prisma.userAuth.create({ + data: { + userId: userData.id, + password: shaHash(data.passwordhash, userData.id), + token: null, + }, + })) + ) { + return null; + } + + return userData; +} + +export async function getUserCredentials(userId: string): Promise<{ + userId: string; + password: string; + token: string | null; +} | null> { + try { + if (!userId) { + throw new Error("missing userId"); + } + + const userAuth = await prisma.userAuth.findUnique({ + where: { + userId: userId, + }, + }); + + if (!userAuth) { + throw new Error("could not find user credentials"); + } + + return { + userId: userAuth.userId, + password: userAuth.password, + token: userAuth.token, + }; + } catch (err) { + const errMessage = err as Error; + + if (errMessage.message === "missing userId") { + console.log("services::actions::getUserCredentials - missing userId"); + return null; + } + + if (errMessage.message === "could not find user credentials") { + console.log( + "services::actions::getUserCredentials - unable to find user credentials", + ); + return null; + } + + console.log( + "services::actions::getUserCredentials - unknown error", + errMessage, + ); + return null; + } +} + +export async function getUserInformation(userId: string): Promise<{ + id: string; + userName: string; + nickName: string | null; + bio: string | null; + picture: string | null; + banner: string | null; + admin: boolean; + status: "online" | "offline" | "dnd" | "idle" | "invis"; + role: Role[]; +} | null> { + try { + if (!userId) { + throw new Error("missing userId"); + } + + const user = await prisma.user.findUnique({ + where: { + id: userId, + }, + }); + + if (!user) { + throw new Error("could not find user"); + } + + const userRoles = await prisma.role.findMany({ + where: { + userId: userId, + }, + }); + + return { + id: userId, + userName: user.username, + nickName: user.nickname, + bio: user.bio, + picture: user.picture, + banner: user.banner, + admin: user.admin, + status: (["online", "offline", "dnd", "idle", "invis"] as const).includes( + user.status as any, + ) + ? (user.status as "online" | "offline" | "dnd" | "idle" | "invis") + : "offline", + role: userRoles, + }; + } catch (err) { + const errMessage = err as Error; + + if (errMessage.message === "missing userId") { + console.log("services::actions::getUserInformation - missing userId"); + return null; + } + + if (errMessage.message === "could not find user") { + console.log( + "services::actions::getUserInformation - unable to find user", + ); + return null; + } + + console.log( + "services::actions::getUserInformation - unknown error", + errMessage, + ); + return null; + } +} + +export async function getAllUsersFrom(instanceId: string): Promise< + | { + id: string; + userName: string; + nickName: string | null; + bio: string | null; + picture: string | null; + banner: string | null; + admin: boolean; + status: "online" | "offline" | "dnd" | "idle" | "invis"; + role: { + userId: string; + instanceId: string; + }[]; + }[] + | null +> { + try { + const instances = await prisma.instance.count({ + where: { + id: instanceId, + }, + }); + if (instances < 1) { + throw new Error("could not find given instance id"); + } + + const users = await prisma.user.findMany({ + where: { + Role: { + some: { + instanceId: instanceId, + }, + }, + }, + }); + if (!users) { + throw new Error("could not get all users in instance"); + } + + const admins = await prisma.user.findMany({ + where: { + admin: true, + }, + }); + if (!admins) { + throw new Error("could not get all admins"); + } + const adminData = await Promise.all( + admins.map(async (u) => { + const adminRoles = await prisma.role.findMany({ + where: { + userId: u.id, + }, + }); + + if (!adminRoles) { + throw new Error("could not get admin roles for admin " + u.id); + } + + return { + id: u.id, + userName: u.username, + nickName: u.nickname, + bio: u.bio, + picture: u.picture, + banner: u.banner, + admin: u.admin, + status: ( + ["online", "offline", "dnd", "idle", "invis"] as const + ).includes(u.status as any) + ? (u.status as "online" | "offline" | "dnd" | "idle" | "invis") + : "offline", + role: adminRoles, + }; + }), + ); + + const userData = await Promise.all( + users.map(async (u) => { + const userRoles = await prisma.role.findMany({ + where: { + userId: u.id, + }, + }); + if (!userRoles) { + throw new Error("could not get user roles for user " + u.id); + } + + return { + id: u.id, + userName: u.username, + nickName: u.nickname, + bio: u.bio, + picture: u.picture, + banner: u.banner, + admin: u.admin, + status: ( + ["online", "offline", "dnd", "idle", "invis"] as const + ).includes(u.status as any) + ? (u.status as "online" | "offline" | "dnd" | "idle" | "invis") + : "offline", + role: userRoles, + }; + }), + ); + + return userData.concat(adminData); + } catch (err) { + console.log(err); + return null; + } +} diff --git a/concord-server/src/validators/categoryValidator.ts b/concord-server/src/validators/categoryValidator.ts new file mode 100644 index 0000000..f8ef491 --- /dev/null +++ b/concord-server/src/validators/categoryValidator.ts @@ -0,0 +1,61 @@ +import { z } from "zod"; + +//category validators + +export const createCategorySchema = z.object({ + name: z.string().min(1).max(50), + position: z.number().min(0), + instanceId: z.uuidv7().optional(), + admin: z.boolean(), + requestingUserId: z.uuidv7(), + requestingUserToken: z.uuidv4(), +}); + +export const getCategorySchema = z.object({ + id: z.uuidv7(), +}); + +export const getCategoriesByInstanceIdSchema = z.object({ + instanceId: z.uuidv7(), +}); + +export const updateCategorySchema = z.object({ + id: z.uuidv7(), + name: z.string().min(1).max(50).optional(), + position: z.number().min(0).optional(), + channels: z + .array( + z.object({ + id: z.string(), + }), + ) + .optional(), + admin: z.boolean(), + requestingUserId: z.uuidv7(), + requestingUserToken: z.uuidv4(), +}); + +export const deleteCategorySchema = z.object({ + id: z.uuidv7(), + admin: z.boolean(), + requestingUserId: z.uuidv7(), + requestingUserToken: z.uuidv4(), +}); + +export const deleteCategoriesByInstanceIdSchema = z.object({ + instanceId: z.uuidv7(), + admin: z.boolean(), + requestingUserId: z.uuidv7(), + requestingUserToken: z.uuidv4(), +}); + +export type CreateCategoryInput = z.infer; +export type GetCategoryInput = z.infer; +export type GetCategoriesByInstanceIdInput = z.infer< + typeof getCategoriesByInstanceIdSchema +>; +export type UpdateCategoryInput = z.infer; +export type DeleteCategoryInput = z.infer; +export type DeleteCategoriesByInstanceIdInput = z.infer< + typeof deleteCategoriesByInstanceIdSchema +>; diff --git a/concord-server/src/validators/channelValidator.ts b/concord-server/src/validators/channelValidator.ts new file mode 100644 index 0000000..23cb918 --- /dev/null +++ b/concord-server/src/validators/channelValidator.ts @@ -0,0 +1,56 @@ +import { z } from "zod"; + +//channel validators + +export const createChannelSchema = z.object({ + type: z.enum(["text", "voice"]), + name: z.string().min(1).max(50), + description: z.string().max(255), + categoryId: z.uuidv7().optional(), + admin: z.boolean(), + requestingUserId: z.uuidv7(), + requestingUserToken: z.uuidv4(), +}); + +export const getChannelSchema = z.object({ + id: z.uuidv7(), +}); + +export const getChannelsByCategoryIdSchema = z.object({ + categoryId: z.uuidv7(), +}); + +export const updateChannelSchema = z.object({ + id: z.uuidv7(), + name: z.string().min(1).max(50).optional(), + description: z.string().max(255).optional(), + categoryId: z.uuidv7().optional(), + admin: z.boolean(), + requestingUserId: z.uuidv7(), + requestingUserToken: z.uuidv4(), +}); + +export const deleteChannelSchema = z.object({ + id: z.uuidv7(), + admin: z.boolean(), + requestingUserId: z.uuidv7(), + requestingUserToken: z.uuidv4(), +}); + +export const deleteChannelsByCategoryIdSchema = z.object({ + categoryId: z.uuidv7(), + admin: z.boolean(), + requestingUserId: z.uuidv7(), + requestingUserToken: z.uuidv4(), +}); + +export type CreateChannelInput = z.infer; +export type GetChannelInput = z.infer; +export type GetChannelsByCategoryIdInput = z.infer< + typeof getChannelsByCategoryIdSchema +>; +export type UpdateChannelInput = z.infer; +export type DeleteChannelInput = z.infer; +export type DeleteChannelsByCategoryIdInput = z.infer< + typeof deleteChannelsByCategoryIdSchema +>; diff --git a/concord-server/src/validators/instanceValidator.ts b/concord-server/src/validators/instanceValidator.ts new file mode 100644 index 0000000..880f957 --- /dev/null +++ b/concord-server/src/validators/instanceValidator.ts @@ -0,0 +1,33 @@ +import { z } from "zod"; + +export const createInstanceRequestSchema = z.object({ + name: z.string().min(1, "Instance name cannot be empty"), + icon: z.url().optional(), + requestingUserId: z.uuidv7(), + requestingUserToken: z.string(), +}); + +export const getAllInstancesResponseSchema = z.object({ + success: z.boolean(), + data: z + .array( + z.object({ + id: z.string(), + name: z.string(), + icon: z.string().nullable(), + createdAt: z.string().refine((val) => !isNaN(Date.parse(val)), { + message: "Invalid date string format", + }), + updatedAt: z.string().refine((val) => !isNaN(Date.parse(val)), { + message: "Invalid date string format", + }), + }), + ) + .optional(), + error: z.string().optional(), +}); + +export type CreateInstanceRequest = z.infer; +export type GetAllInstancesResponse = z.infer< + typeof getAllInstancesResponseSchema +>; diff --git a/concord-server/src/validators/messageValidator.ts b/concord-server/src/validators/messageValidator.ts new file mode 100644 index 0000000..baead86 --- /dev/null +++ b/concord-server/src/validators/messageValidator.ts @@ -0,0 +1,20 @@ +import { z } from "zod"; + +export const getMessageByIdSchema = z.object({ + id: z.uuidv7(), +}); + +export const getMessagesBeforeDate = z.object({ + date: z.string().refine((val) => !isNaN(Date.parse(val)), { + message: "Invalid date string format", + }), + channelId: z.uuidv7(), +}); + +export const sendMessageSchema = z.object({ + channelId: z.uuidv7(), + userId: z.uuidv7(), + content: z.string(), + token: z.string(), + repliedMessageId: z.uuidv7().nullable().optional(), +}); diff --git a/concord-server/src/validators/userValidator.ts b/concord-server/src/validators/userValidator.ts new file mode 100644 index 0000000..e4f1632 --- /dev/null +++ b/concord-server/src/validators/userValidator.ts @@ -0,0 +1,30 @@ +import { z } from "zod"; + +export const queryUserByIdSchema = z.object({ + id: z.uuidv7(), +}); + +export const queryAllUsersByInstanceId = z.object({ + instanceId: z.uuidv7(), +}); +import { is } from "zod/v4/locales"; +export const createUserSchema = z.object({ + username: z.string().min(3).max(30), + nickname: z.string().min(1).max(30).optional(), + bio: z.string().max(500).optional(), + picture: z.url().optional(), + banner: z.url().optional(), + status: z + .enum(["online", "offline", "dnd", "idle", "invis"]) + .default("online"), + admin: z.boolean().default(false), + requestingUserId: z.uuidv7(), + requestingUserToken: z.uuidv4(), + passwordhash: z.string(), +}); + +export type QueryUserByIdInput = z.infer; +export type QueryAllUsersByInstanceIdInput = z.infer< + typeof queryAllUsersByInstanceId +>; +export type CreateUserInput = z.infer; diff --git a/concord-server/tsconfig.json b/concord-server/tsconfig.json index c442b33..9136c14 100644 --- a/concord-server/tsconfig.json +++ b/concord-server/tsconfig.json @@ -4,4 +4,4 @@ "jsx": "react-jsx", "jsxImportSource": "hono/jsx" } -} \ No newline at end of file +}