From 5dec454afb7478d3cb5021c1706366d5d20c06119878debf915dc058f9098068 Mon Sep 17 00:00:00 2001 From: aslan Date: Tue, 23 Dec 2025 17:45:51 -0500 Subject: [PATCH] Add services and auth --- package-lock.json | 221 ++++++++++++++++++++++++- package.json | 3 + src/controllers/auth/auth.ts | 56 +++++++ src/controllers/auth/index.ts | 3 + src/controllers/auth/routes.ts | 9 + src/controllers/auth/types.ts | 40 +++++ src/controllers/channel/channel.ts | 27 +++ src/controllers/channel/index.ts | 3 + src/controllers/channel/routes.ts | 8 + src/controllers/channel/types.ts | 20 +++ src/controllers/community/community.ts | 27 +++ src/controllers/community/index.ts | 3 + src/controllers/community/routes.ts | 8 + src/controllers/community/types.ts | 20 +++ src/controllers/role/index.ts | 3 + src/controllers/role/role.ts | 27 +++ src/controllers/role/routes.ts | 8 + src/controllers/role/types.ts | 16 ++ src/controllers/session/index.ts | 3 + src/controllers/session/routes.ts | 8 + src/controllers/session/session.ts | 26 +++ src/controllers/session/types.ts | 19 +++ src/controllers/test/routes.ts | 3 +- src/controllers/test/test.ts | 13 +- src/controllers/user/index.ts | 3 + src/controllers/user/routes.ts | 9 + src/controllers/user/types.ts | 41 +++++ src/controllers/user/user.ts | 53 ++++++ src/helpers.ts | 59 +++++++ src/index.ts | 12 ++ src/services/auth/auth.ts | 72 ++++++++ src/services/auth/index.ts | 2 + src/services/auth/types.ts | 12 ++ src/services/channel/channel.ts | 10 ++ src/services/channel/index.ts | 1 + src/services/community/community.ts | 10 ++ src/services/community/index.ts | 1 + src/services/role/index.ts | 1 + src/services/role/role.ts | 10 ++ src/services/session/index.ts | 1 + src/services/session/session.ts | 10 ++ src/services/user/index.ts | 1 + src/services/user/user.ts | 27 +++ src/store/index.ts | 1 - src/store/store.ts | 18 +- src/store/types.ts | 3 - 46 files changed, 900 insertions(+), 31 deletions(-) create mode 100644 src/controllers/auth/auth.ts create mode 100644 src/controllers/auth/index.ts create mode 100644 src/controllers/auth/routes.ts create mode 100644 src/controllers/auth/types.ts create mode 100644 src/controllers/channel/channel.ts create mode 100644 src/controllers/channel/index.ts create mode 100644 src/controllers/channel/routes.ts create mode 100644 src/controllers/channel/types.ts create mode 100644 src/controllers/community/community.ts create mode 100644 src/controllers/community/index.ts create mode 100644 src/controllers/community/routes.ts create mode 100644 src/controllers/community/types.ts create mode 100644 src/controllers/role/index.ts create mode 100644 src/controllers/role/role.ts create mode 100644 src/controllers/role/routes.ts create mode 100644 src/controllers/role/types.ts create mode 100644 src/controllers/session/index.ts create mode 100644 src/controllers/session/routes.ts create mode 100644 src/controllers/session/session.ts create mode 100644 src/controllers/session/types.ts create mode 100644 src/controllers/user/index.ts create mode 100644 src/controllers/user/routes.ts create mode 100644 src/controllers/user/types.ts create mode 100644 src/controllers/user/user.ts create mode 100644 src/helpers.ts create mode 100644 src/services/auth/auth.ts create mode 100644 src/services/auth/index.ts create mode 100644 src/services/auth/types.ts create mode 100644 src/services/channel/channel.ts create mode 100644 src/services/channel/index.ts create mode 100644 src/services/community/community.ts create mode 100644 src/services/community/index.ts create mode 100644 src/services/role/index.ts create mode 100644 src/services/role/role.ts create mode 100644 src/services/session/index.ts create mode 100644 src/services/session/session.ts create mode 100644 src/services/user/index.ts create mode 100644 src/services/user/user.ts delete mode 100644 src/store/types.ts diff --git a/package-lock.json b/package-lock.json index 8e5e984..12bd555 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,10 +11,13 @@ "dependencies": { "@prisma/adapter-pg": "^7.2.0", "@prisma/client": "^7.2.0", + "argon2": "^0.44.0", "fastify": "^5.6.2", + "jsonwebtoken": "^9.0.3", "pg": "^8.16.3" }, "devDependencies": { + "@types/jsonwebtoken": "^9.0.10", "@types/node": "^25.0.3", "@types/pg": "^8.16.0", "dotenv": "^17.2.3", @@ -103,6 +106,12 @@ "@electric-sql/pglite": "0.3.2" } }, + "node_modules/@epic-web/invariant": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@epic-web/invariant/-/invariant-1.0.0.tgz", + "integrity": "sha512-lrTPqgvfFQtR/eY/qkIzp98OGdNJu0m5ji3q/nJI8v3SXkRKEnWiOxMmbvcSoAIzv/cGiuvRy57k4suKQSAdwA==", + "license": "MIT" + }, "node_modules/@fastify/ajv-compiler": { "version": "4.0.5", "resolved": "https://registry.npmjs.org/@fastify/ajv-compiler/-/ajv-compiler-4.0.5.tgz", @@ -269,6 +278,15 @@ "node": ">=16" } }, + "node_modules/@phc/format": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@phc/format/-/format-1.0.0.tgz", + "integrity": "sha512-m7X9U6BG2+J+R1lSOdCiITLLrxm+cWlNI3HUFA92oLO77ObGNzaKdh8pMLqdZcshtkKuV84olNNXDfMc4FezBQ==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, "node_modules/@pinojs/redact": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/@pinojs/redact/-/redact-0.4.0.tgz", @@ -494,6 +512,24 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/jsonwebtoken": { + "version": "9.0.10", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.10.tgz", + "integrity": "sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/ms": "*", + "@types/node": "*" + } + }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/node": { "version": "25.0.3", "resolved": "https://registry.npmjs.org/@types/node/-/node-25.0.3.tgz", @@ -599,6 +635,22 @@ "dev": true, "license": "MIT" }, + "node_modules/argon2": { + "version": "0.44.0", + "resolved": "https://registry.npmjs.org/argon2/-/argon2-0.44.0.tgz", + "integrity": "sha512-zHPGN3S55sihSQo0dBbK0A5qpi2R31z7HZDZnry3ifOyj8bZZnpZND2gpmhnRGO1V/d555RwBqIK5W4Mrmv3ig==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "@phc/format": "^1.0.0", + "cross-env": "^10.0.0", + "node-addon-api": "^8.5.0", + "node-gyp-build": "^4.8.4" + }, + "engines": { + "node": ">=16.17.0" + } + }, "node_modules/atomic-sleep": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz", @@ -628,6 +680,12 @@ "node": ">= 6.0.0" } }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, "node_modules/c12": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/c12/-/c12-3.1.0.tgz", @@ -748,11 +806,27 @@ "dev": true, "license": "MIT" }, + "node_modules/cross-env": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-10.1.0.tgz", + "integrity": "sha512-GsYosgnACZTADcmEyJctkJIoqAhHjttw7RsFrVoJNXbsWWqaq6Ym+7kZjq6mS45O0jij6vtiReppKQEtqWy6Dw==", + "license": "MIT", + "dependencies": { + "@epic-web/invariant": "^1.0.0", + "cross-spawn": "^7.0.6" + }, + "bin": { + "cross-env": "dist/bin/cross-env.js", + "cross-env-shell": "dist/bin/cross-env-shell.js" + }, + "engines": { + "node": ">=20" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "devOptional": true, "license": "MIT", "dependencies": { "path-key": "^3.1.0", @@ -837,6 +911,15 @@ "url": "https://dotenvx.com" } }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, "node_modules/effect": { "version": "3.18.4", "resolved": "https://registry.npmjs.org/effect/-/effect-3.18.4.tgz", @@ -1125,7 +1208,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "devOptional": true, "license": "ISC" }, "node_modules/jiti": { @@ -1163,6 +1245,49 @@ "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", "license": "MIT" }, + "node_modules/jsonwebtoken": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz", + "integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==", + "license": "MIT", + "dependencies": { + "jws": "^4.0.1", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", + "license": "MIT", + "dependencies": { + "jwa": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, "node_modules/light-my-request": { "version": "6.6.0", "resolved": "https://registry.npmjs.org/light-my-request/-/light-my-request-6.6.0.tgz", @@ -1217,6 +1342,48 @@ "devOptional": true, "license": "MIT" }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", + "license": "MIT" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", + "license": "MIT" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", + "license": "MIT" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "license": "MIT" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", + "license": "MIT" + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "license": "MIT" + }, "node_modules/long": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", @@ -1247,6 +1414,12 @@ "dev": true, "license": "ISC" }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, "node_modules/mysql2": { "version": "3.15.3", "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.15.3.tgz", @@ -1281,6 +1454,15 @@ "node": ">=8.0.0" } }, + "node_modules/node-addon-api": { + "version": "8.5.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.5.0.tgz", + "integrity": "sha512-/bRZty2mXUIFY/xU5HLvveNHlswNJej+RnxBjOMkidWfwZzgTbPG1E3K5TOxRLOR+5hX7bSofy8yf1hZevMS8A==", + "license": "MIT", + "engines": { + "node": "^18 || ^20 || >= 21" + } + }, "node_modules/node-fetch-native": { "version": "1.6.7", "resolved": "https://registry.npmjs.org/node-fetch-native/-/node-fetch-native-1.6.7.tgz", @@ -1288,6 +1470,17 @@ "devOptional": true, "license": "MIT" }, + "node_modules/node-gyp-build": { + "version": "4.8.4", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz", + "integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==", + "license": "MIT", + "bin": { + "node-gyp-build": "bin.js", + "node-gyp-build-optional": "optional.js", + "node-gyp-build-test": "build-test.js" + } + }, "node_modules/nypm": { "version": "0.6.2", "resolved": "https://registry.npmjs.org/nypm/-/nypm-0.6.2.tgz", @@ -1328,7 +1521,6 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "devOptional": true, "license": "MIT", "engines": { "node": ">=8" @@ -1760,6 +1952,26 @@ "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", "license": "MIT" }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, "node_modules/safe-regex2": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/safe-regex2/-/safe-regex2-5.0.0.tgz", @@ -1847,7 +2059,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "devOptional": true, "license": "MIT", "dependencies": { "shebang-regex": "^3.0.0" @@ -1860,7 +2071,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "devOptional": true, "license": "MIT", "engines": { "node": ">=8" @@ -2046,7 +2256,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "devOptional": true, "license": "ISC", "dependencies": { "isexe": "^2.0.0" diff --git a/package.json b/package.json index e6571b1..e1b61b5 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "start": "npx tsc && node --env-file=.env dist/index.js" }, "devDependencies": { + "@types/jsonwebtoken": "^9.0.10", "@types/node": "^25.0.3", "@types/pg": "^8.16.0", "dotenv": "^17.2.3", @@ -25,7 +26,9 @@ "dependencies": { "@prisma/adapter-pg": "^7.2.0", "@prisma/client": "^7.2.0", + "argon2": "^0.44.0", "fastify": "^5.6.2", + "jsonwebtoken": "^9.0.3", "pg": "^8.16.3" } } diff --git a/src/controllers/auth/auth.ts b/src/controllers/auth/auth.ts new file mode 100644 index 0000000..500f3a9 --- /dev/null +++ b/src/controllers/auth/auth.ts @@ -0,0 +1,56 @@ +import { type FastifyReply, type FastifyRequest } from "fastify"; +import type { + ILoginRequest, + IRegisterResponseError, + IRegisterResponseSuccess, + IRegisterRequest, + ILoginResponseError, + ILoginResponseSuccess, +} from "./types.js"; +import { loginUser, registerUser } from "../../services/auth/auth.js"; + +const postRegister = async (request: FastifyRequest, _reply: FastifyReply) => { + const { username, password, email } = request.body as IRegisterRequest; + + const newUser = await registerUser({ + username: username, + password: password, + email: email, + }); + + if (!newUser) { + return { + error: "user already exists", + } as IRegisterResponseError; + } + + return { + id: newUser.id, + username: newUser.username, + registerDate: newUser.registerDate?.getTime(), + } as IRegisterResponseSuccess; +}; + +const postLogin = async (request: FastifyRequest, _reply: FastifyReply) => { + const { username, password } = request.body as ILoginRequest; + + const session = await loginUser({ + username: username, + password: password, + }); + + if (!session) { + return { + ownerId: "", + error: "incorrect credentials", + } as ILoginResponseError; + } + + return { + id: session.id, + ownerId: session.userId, + token: session.token, + } as ILoginResponseSuccess; +}; + +export { postRegister, postLogin }; diff --git a/src/controllers/auth/index.ts b/src/controllers/auth/index.ts new file mode 100644 index 0000000..fb678a1 --- /dev/null +++ b/src/controllers/auth/index.ts @@ -0,0 +1,3 @@ +export * from "./auth.js"; +export * from "./routes.js"; +export * from "./types.js"; diff --git a/src/controllers/auth/routes.ts b/src/controllers/auth/routes.ts new file mode 100644 index 0000000..87b5bd5 --- /dev/null +++ b/src/controllers/auth/routes.ts @@ -0,0 +1,9 @@ +import { type FastifyInstance } from "fastify"; +import * as controller from "./auth.js"; + +const authRoutes = async (fastify: FastifyInstance) => { + fastify.post(`/register`, controller.postRegister); + fastify.post(`/login`, controller.postLogin); +}; + +export { authRoutes }; diff --git a/src/controllers/auth/types.ts b/src/controllers/auth/types.ts new file mode 100644 index 0000000..d5211f2 --- /dev/null +++ b/src/controllers/auth/types.ts @@ -0,0 +1,40 @@ +interface IRegisterRequest { + username: string; + password: string; + email?: string; +} + +interface IRegisterResponseSuccess { + id: string; + username: string; + registerDate: number; +} + +interface IRegisterResponseError { + error: string; +} + +interface ILoginRequest { + username: string; + password: string; +} + +interface ILoginResponseSuccess { + id: string; + ownerId: string; + token: string; +} + +interface ILoginResponseError { + ownerId: string; + error: string; +} + +export { + type IRegisterRequest, + type IRegisterResponseSuccess, + type IRegisterResponseError, + type ILoginRequest, + type ILoginResponseSuccess, + type ILoginResponseError, +}; diff --git a/src/controllers/channel/channel.ts b/src/controllers/channel/channel.ts new file mode 100644 index 0000000..4de076c --- /dev/null +++ b/src/controllers/channel/channel.ts @@ -0,0 +1,27 @@ +import { type FastifyReply, type FastifyRequest } from "fastify"; +import type { + IChannelParams, + IChannelResponseError, + IChannelResponseSuccess, +} from "./types.js"; +import { getChannelById } from "../../services/channel/channel.js"; + +const getChannel = async (request: FastifyRequest, _reply: FastifyReply) => { + const { id } = request.params as IChannelParams; + + const channel = await getChannelById(id); + if (!channel) { + return { + id: id, + error: "channel does not exist", + } as IChannelResponseError; + } + + return { + id: channel.id, + name: channel.name, + communityId: channel.communityId, + } as IChannelResponseSuccess; +}; + +export { getChannel }; diff --git a/src/controllers/channel/index.ts b/src/controllers/channel/index.ts new file mode 100644 index 0000000..4383173 --- /dev/null +++ b/src/controllers/channel/index.ts @@ -0,0 +1,3 @@ +export * from "./channel.js"; +export * from "./routes.js"; +export * from "./types.js"; diff --git a/src/controllers/channel/routes.ts b/src/controllers/channel/routes.ts new file mode 100644 index 0000000..f9ec2c8 --- /dev/null +++ b/src/controllers/channel/routes.ts @@ -0,0 +1,8 @@ +import { type FastifyInstance } from "fastify"; +import * as controller from "./channel.js"; + +const channelRoutes = async (fastify: FastifyInstance) => { + fastify.get(`/:id`, controller.getChannel); +}; + +export { channelRoutes }; diff --git a/src/controllers/channel/types.ts b/src/controllers/channel/types.ts new file mode 100644 index 0000000..c74e1d9 --- /dev/null +++ b/src/controllers/channel/types.ts @@ -0,0 +1,20 @@ +interface IChannelParams { + id: string; +} + +interface IChannelResponseError { + id: string; + error: string; +} + +interface IChannelResponseSuccess { + id: string; + name: string; + communityId: string; +} + +export { + type IChannelParams, + type IChannelResponseError, + type IChannelResponseSuccess, +}; diff --git a/src/controllers/community/community.ts b/src/controllers/community/community.ts new file mode 100644 index 0000000..4233ae5 --- /dev/null +++ b/src/controllers/community/community.ts @@ -0,0 +1,27 @@ +import { type FastifyReply, type FastifyRequest } from "fastify"; +import type { + ICommunityParams, + ICommunityResponseError, + ICommunityResponseSuccess, +} from "./types.js"; +import { getCommunityById } from "../../services/community/community.js"; + +const getCommunity = async (request: FastifyRequest, _reply: FastifyReply) => { + const { id } = request.params as ICommunityParams; + + const community = await getCommunityById(id); + if (!community) { + return { + id: id, + error: "community does not exist", + } as ICommunityResponseError; + } + + return { + id: community.id, + name: community.name, + description: community.description, + } as ICommunityResponseSuccess; +}; + +export { getCommunity }; diff --git a/src/controllers/community/index.ts b/src/controllers/community/index.ts new file mode 100644 index 0000000..c481dc0 --- /dev/null +++ b/src/controllers/community/index.ts @@ -0,0 +1,3 @@ +export * from "./community.js"; +export * from "./routes.js"; +export * from "./types.js"; diff --git a/src/controllers/community/routes.ts b/src/controllers/community/routes.ts new file mode 100644 index 0000000..8ab3b2f --- /dev/null +++ b/src/controllers/community/routes.ts @@ -0,0 +1,8 @@ +import { type FastifyInstance } from "fastify"; +import * as controller from "./community.js"; + +const communityRoutes = async (fastify: FastifyInstance) => { + fastify.get(`/:id`, controller.getCommunity); +}; + +export { communityRoutes }; diff --git a/src/controllers/community/types.ts b/src/controllers/community/types.ts new file mode 100644 index 0000000..362fb30 --- /dev/null +++ b/src/controllers/community/types.ts @@ -0,0 +1,20 @@ +interface ICommunityParams { + id: string; +} + +interface ICommunityResponseError { + id: string; + error: string; +} + +interface ICommunityResponseSuccess { + id: string; + name: string; + description: string; +} + +export { + type ICommunityParams, + type ICommunityResponseError, + type ICommunityResponseSuccess, +}; diff --git a/src/controllers/role/index.ts b/src/controllers/role/index.ts new file mode 100644 index 0000000..5231437 --- /dev/null +++ b/src/controllers/role/index.ts @@ -0,0 +1,3 @@ +export * from "./role.js"; +export * from "./routes.js"; +export * from "./types.js"; diff --git a/src/controllers/role/role.ts b/src/controllers/role/role.ts new file mode 100644 index 0000000..7572886 --- /dev/null +++ b/src/controllers/role/role.ts @@ -0,0 +1,27 @@ +import { type FastifyReply, type FastifyRequest } from "fastify"; +import type { + IRoleParams, + IRoleResponseError, + IRoleResponseSuccess, +} from "./types.js"; +import { getRoleById } from "../../services/role/role.js"; + +const getRole = async (request: FastifyRequest, _reply: FastifyReply) => { + const { id } = request.params as IRoleParams; + + const role = await getRoleById(id); + if (!role) { + return { + id: id, + error: "role does not exist", + } as IRoleResponseError; + } + + return { + id: role.id, + name: role.name, + communityId: role.communityId, + } as IRoleResponseSuccess; +}; + +export { getRole }; diff --git a/src/controllers/role/routes.ts b/src/controllers/role/routes.ts new file mode 100644 index 0000000..0a63a03 --- /dev/null +++ b/src/controllers/role/routes.ts @@ -0,0 +1,8 @@ +import { type FastifyInstance } from "fastify"; +import * as controller from "./role.js"; + +const roleRoutes = async (fastify: FastifyInstance) => { + fastify.get(`/:id`, controller.getRole); +}; + +export { roleRoutes }; diff --git a/src/controllers/role/types.ts b/src/controllers/role/types.ts new file mode 100644 index 0000000..edf5d02 --- /dev/null +++ b/src/controllers/role/types.ts @@ -0,0 +1,16 @@ +interface IRoleParams { + id: string; +} + +interface IRoleResponseError { + id: string; + error: string; +} + +interface IRoleResponseSuccess { + id: string; + name: string; + communityId: string; +} + +export { type IRoleParams, type IRoleResponseError, type IRoleResponseSuccess }; diff --git a/src/controllers/session/index.ts b/src/controllers/session/index.ts new file mode 100644 index 0000000..ae5c8f4 --- /dev/null +++ b/src/controllers/session/index.ts @@ -0,0 +1,3 @@ +export * from "./session.js"; +export * from "./routes.js"; +export * from "./types.js"; diff --git a/src/controllers/session/routes.ts b/src/controllers/session/routes.ts new file mode 100644 index 0000000..f43e748 --- /dev/null +++ b/src/controllers/session/routes.ts @@ -0,0 +1,8 @@ +import { type FastifyInstance } from "fastify"; +import * as controller from "./session.js"; + +const sessionRoutes = async (fastify: FastifyInstance) => { + fastify.get(`/:id`, controller.getSession); +}; + +export { sessionRoutes }; diff --git a/src/controllers/session/session.ts b/src/controllers/session/session.ts new file mode 100644 index 0000000..e4860c9 --- /dev/null +++ b/src/controllers/session/session.ts @@ -0,0 +1,26 @@ +import { type FastifyReply, type FastifyRequest } from "fastify"; +import type { + ISessionParams, + ISessionResponseError, + ISessionResponseSuccess, +} from "./types.js"; +import { getSessionById } from "../../services/session/session.js"; + +const getSession = async (request: FastifyRequest, _reply: FastifyReply) => { + const { id } = request.params as ISessionParams; + + const session = await getSessionById(id); + if (!session) { + return { + id: id, + error: "session does not exist", + } as ISessionResponseError; + } + + return { + id: session.id, + userId: session.userId, + } as ISessionResponseSuccess; +}; + +export { getSession }; diff --git a/src/controllers/session/types.ts b/src/controllers/session/types.ts new file mode 100644 index 0000000..06cd158 --- /dev/null +++ b/src/controllers/session/types.ts @@ -0,0 +1,19 @@ +interface ISessionParams { + id: string; +} + +interface ISessionResponseError { + id: string; + error: string; +} + +interface ISessionResponseSuccess { + id: string; + userId: string; +} + +export { + type ISessionParams, + type ISessionResponseError, + type ISessionResponseSuccess, +}; diff --git a/src/controllers/test/routes.ts b/src/controllers/test/routes.ts index 436956f..4407af4 100644 --- a/src/controllers/test/routes.ts +++ b/src/controllers/test/routes.ts @@ -2,7 +2,8 @@ import { type FastifyInstance } from "fastify"; import * as controller from "./test.js"; const testRoutes = async (fastify: FastifyInstance) => { - fastify.get("/test", controller.test); + fastify.get("/ping", controller.getPing); + fastify.get("/test", controller.getTest); }; export { testRoutes }; diff --git a/src/controllers/test/test.ts b/src/controllers/test/test.ts index ecbd4f1..773dc4b 100644 --- a/src/controllers/test/test.ts +++ b/src/controllers/test/test.ts @@ -1,10 +1,11 @@ import { type FastifyReply, type FastifyRequest } from "fastify"; -import { testdb } from "../../store/store.js"; -const test = async (request: FastifyRequest, reply: FastifyReply) => { - testdb(); - - return [{ name: "Alice" }]; +const getPing = async (_request: FastifyRequest, _reply: FastifyReply) => { + return [{ message: "pong" }]; }; -export { test }; +const getTest = async (_request: FastifyRequest, _reply: FastifyReply) => { + return [{ message: "ok" }]; +}; + +export { getPing, getTest }; diff --git a/src/controllers/user/index.ts b/src/controllers/user/index.ts new file mode 100644 index 0000000..e2c377b --- /dev/null +++ b/src/controllers/user/index.ts @@ -0,0 +1,3 @@ +export * from "./user.js"; +export * from "./routes.js"; +export * from "./types.js"; diff --git a/src/controllers/user/routes.ts b/src/controllers/user/routes.ts new file mode 100644 index 0000000..ed16350 --- /dev/null +++ b/src/controllers/user/routes.ts @@ -0,0 +1,9 @@ +import { type FastifyInstance } from "fastify"; +import * as controller from "./user.js"; + +const userRoutes = async (fastify: FastifyInstance) => { + fastify.get(`/:id`, controller.getUser); + fastify.get(`/:id/sessions`, controller.getSessions); +}; + +export { userRoutes }; diff --git a/src/controllers/user/types.ts b/src/controllers/user/types.ts new file mode 100644 index 0000000..a0e3c79 --- /dev/null +++ b/src/controllers/user/types.ts @@ -0,0 +1,41 @@ +interface IUserParams { + id: string; +} + +interface IUserResponseError { + id: string; + error: string; +} + +interface IUserResponseSuccess { + id: string; + username: string; + email: string; + description: string; + admin: boolean; + registerDate: number; + lastLogin: number; +} + +interface ISessionsResponseError { + id: string; + error: string; +} + +interface ISessionsResponseSuccess { + sessions: ISessionsResponseSession[]; +} + +interface ISessionsResponseSession { + id: string; + userId: string; +} + +export { + type IUserParams, + type IUserResponseError, + type IUserResponseSuccess, + type ISessionsResponseError, + type ISessionsResponseSuccess, + type ISessionsResponseSession, +}; diff --git a/src/controllers/user/user.ts b/src/controllers/user/user.ts new file mode 100644 index 0000000..d573f43 --- /dev/null +++ b/src/controllers/user/user.ts @@ -0,0 +1,53 @@ +import { type FastifyReply, type FastifyRequest } from "fastify"; +import type { + IUserParams, + IUserResponseError, + IUserResponseSuccess, + ISessionsResponseError, + ISessionsResponseSuccess, +} from "./types.js"; +import { getUserById, getUserSessionsById } from "../../services/user/user.js"; + +const getUser = async (request: FastifyRequest, _reply: FastifyReply) => { + const { id } = request.params as IUserParams; + + const user = await getUserById(id); + if (!user) { + return { + id: id, + error: "user does not exist", + } as IUserResponseError; + } + + return { + id: user.id, + username: user.username, + email: user.email, + description: user.description, + admin: user.admin, + registerDate: user.registerDate.getTime(), + lastLogin: user.lastLogin?.getTime() ?? 0, + } as IUserResponseSuccess; +}; + +const getSessions = async (request: FastifyRequest, _reply: FastifyReply) => { + const { id } = request.params as IUserParams; + const authHeader = request.headers["authorization"]; + + const sessions = await getUserSessionsById(id, authHeader); + if (!sessions) { + return { + id: id, + error: "user does not exist or you have no access", + } as ISessionsResponseError; + } + + return { + sessions: sessions.map((session) => ({ + id: session.id, + userId: session.userId, + })), + } as ISessionsResponseSuccess; +}; + +export { getUser, getSessions }; diff --git a/src/helpers.ts b/src/helpers.ts new file mode 100644 index 0000000..c4b7933 --- /dev/null +++ b/src/helpers.ts @@ -0,0 +1,59 @@ +import jwt from "jsonwebtoken"; +import type { Session, User } from "./generated/prisma/client.js"; +import { getDB } from "./store/store.js"; + +const getJwtSecret = () => { + return process.env.JWT_SECRET || ""; +}; + +const verifyToken = (token: string): string | jwt.JwtPayload | null => { + try { + return jwt.verify(token, getJwtSecret()); + } catch { + return null; + } +}; + +const getSessionFromToken = async (token: string): Promise => { + return await getDB().session.findFirst({ + where: { + token: token, + }, + }); +}; + +const getUserFromToken = async (token: string): Promise => { + const session = await getSessionFromToken(token); + + return await getDB().user.findFirst({ + where: { + id: session?.userId ?? "invalid", + }, + }); +}; + +const getUserFromAuth = async ( + authHeader: string | undefined, +): Promise => { + const token = authHeader?.replace("Bearer ", ""); + + const verified = verifyToken(token ?? "") !== null; + if (!verified || !token) { + return null; + } + + const user = await getUserFromToken(token); + if (!user) { + return null; + } + + return user; +}; + +export { + getJwtSecret, + verifyToken, + getSessionFromToken, + getUserFromToken, + getUserFromAuth, +}; diff --git a/src/index.ts b/src/index.ts index 07a06af..0c268cf 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,12 +2,24 @@ import { config } from "./config.js"; import Fastify from "fastify"; import { testRoutes } from "./controllers/test/routes.js"; +import { authRoutes } from "./controllers/auth/routes.js"; +import { userRoutes } from "./controllers/user/routes.js"; +import { sessionRoutes } from "./controllers/session/routes.js"; +import { communityRoutes } from "./controllers/community/routes.js"; +import { channelRoutes } from "./controllers/channel/routes.js"; +import { roleRoutes } from "./controllers/role/routes.js"; const app = Fastify({ logger: true, }); app.register(testRoutes); +app.register(authRoutes, { prefix: "/api/v1/auth" }); +app.register(userRoutes, { prefix: "/api/v1/user" }); +app.register(sessionRoutes, { prefix: "/api/v1/session" }); +app.register(communityRoutes, { prefix: "/api/v1/community" }); +app.register(channelRoutes, { prefix: "/api/v1/channel" }); +app.register(roleRoutes, { prefix: "/api/v1/role" }); app.listen({ port: config.port }, (err, address) => { if (err) throw err; diff --git a/src/services/auth/auth.ts b/src/services/auth/auth.ts new file mode 100644 index 0000000..b496aba --- /dev/null +++ b/src/services/auth/auth.ts @@ -0,0 +1,72 @@ +import argon2 from "argon2"; +import jwt from "jsonwebtoken"; + +import type { User, Session } from "../../generated/prisma/client.js"; +import { getDB } from "../../store/store.js"; +import type { IUserLogin, IUserRegistration } from "./types.js"; +import { getJwtSecret } from "../../helpers.js"; + +const registerUser = async ( + registration: IUserRegistration, +): Promise => { + const existingUser = await getDB().user.findUnique({ + where: { username: registration.username }, + }); + if (existingUser) { + return null; + } + + const passwordHash = await hashPassword(registration.password); + + let newUser: User | null = null; + try { + newUser = await getDB().user.create({ + data: { + username: registration.username, + passwordHash: passwordHash, + email: registration.email ?? null, + }, + }); + } catch { + return null; + } + + return newUser; +}; + +const loginUser = async (login: IUserLogin): Promise => { + const user = await getDB().user.findUnique({ + where: { username: login.username }, + }); + + const passwordCorrect = await argon2.verify( + user?.passwordHash ?? "", + login.password, + ); + + if (!user || !passwordCorrect) { + return null; + } + + return await getDB().session.create({ + data: { + token: createToken(user.id), + userId: user.id, + }, + }); +}; + +const hashPassword = async (password: string): Promise => { + return await argon2.hash(password, { + type: argon2.argon2id, + memoryCost: 2 ** 16, + timeCost: 4, + parallelism: 1, + }); +}; + +const createToken = (userId: string) => { + return jwt.sign({ sub: userId }, getJwtSecret()); +}; + +export { registerUser, loginUser, hashPassword }; diff --git a/src/services/auth/index.ts b/src/services/auth/index.ts new file mode 100644 index 0000000..9561b83 --- /dev/null +++ b/src/services/auth/index.ts @@ -0,0 +1,2 @@ +export * from "./auth.js"; +export * from "./types.js"; diff --git a/src/services/auth/types.ts b/src/services/auth/types.ts new file mode 100644 index 0000000..10580ea --- /dev/null +++ b/src/services/auth/types.ts @@ -0,0 +1,12 @@ +interface IUserRegistration { + username: string; + password: string; + email?: string | undefined; +} + +interface IUserLogin { + username: string; + password: string; +} + +export { type IUserRegistration, type IUserLogin }; diff --git a/src/services/channel/channel.ts b/src/services/channel/channel.ts new file mode 100644 index 0000000..5763fe6 --- /dev/null +++ b/src/services/channel/channel.ts @@ -0,0 +1,10 @@ +import type { Channel } from "../../generated/prisma/client.js"; +import { getDB } from "../../store/store.js"; + +const getChannelById = async (id: string): Promise => { + return await getDB().channel.findUnique({ + where: { id: id }, + }); +}; + +export { getChannelById }; diff --git a/src/services/channel/index.ts b/src/services/channel/index.ts new file mode 100644 index 0000000..d2f4394 --- /dev/null +++ b/src/services/channel/index.ts @@ -0,0 +1 @@ +export * from "./channel.js"; diff --git a/src/services/community/community.ts b/src/services/community/community.ts new file mode 100644 index 0000000..086c7f2 --- /dev/null +++ b/src/services/community/community.ts @@ -0,0 +1,10 @@ +import type { Community } from "../../generated/prisma/client.js"; +import { getDB } from "../../store/store.js"; + +const getCommunityById = async (id: string): Promise => { + return await getDB().community.findUnique({ + where: { id: id }, + }); +}; + +export { getCommunityById }; diff --git a/src/services/community/index.ts b/src/services/community/index.ts new file mode 100644 index 0000000..893e64d --- /dev/null +++ b/src/services/community/index.ts @@ -0,0 +1 @@ +export * from "./community.js"; diff --git a/src/services/role/index.ts b/src/services/role/index.ts new file mode 100644 index 0000000..5c495fc --- /dev/null +++ b/src/services/role/index.ts @@ -0,0 +1 @@ +export * from "./role.js"; diff --git a/src/services/role/role.ts b/src/services/role/role.ts new file mode 100644 index 0000000..3183234 --- /dev/null +++ b/src/services/role/role.ts @@ -0,0 +1,10 @@ +import type { Role } from "../../generated/prisma/client.js"; +import { getDB } from "../../store/store.js"; + +const getRoleById = async (id: string): Promise => { + return await getDB().role.findUnique({ + where: { id: id }, + }); +}; + +export { getRoleById }; diff --git a/src/services/session/index.ts b/src/services/session/index.ts new file mode 100644 index 0000000..8b22865 --- /dev/null +++ b/src/services/session/index.ts @@ -0,0 +1 @@ +export * from "./session.js"; diff --git a/src/services/session/session.ts b/src/services/session/session.ts new file mode 100644 index 0000000..0d7baa6 --- /dev/null +++ b/src/services/session/session.ts @@ -0,0 +1,10 @@ +import type { Session } from "../../generated/prisma/client.js"; +import { getDB } from "../../store/store.js"; + +const getSessionById = async (id: string): Promise => { + return await getDB().session.findUnique({ + where: { id: id }, + }); +}; + +export { getSessionById }; diff --git a/src/services/user/index.ts b/src/services/user/index.ts new file mode 100644 index 0000000..fd3004f --- /dev/null +++ b/src/services/user/index.ts @@ -0,0 +1 @@ +export * from "./user.js"; diff --git a/src/services/user/user.ts b/src/services/user/user.ts new file mode 100644 index 0000000..58a8079 --- /dev/null +++ b/src/services/user/user.ts @@ -0,0 +1,27 @@ +import type { User, Session } from "../../generated/prisma/client.js"; +import { getUserFromAuth } from "../../helpers.js"; +import { getDB } from "../../store/store.js"; + +const getUserById = async (id: string): Promise => { + return await getDB().user.findUnique({ + where: { id: id }, + }); +}; + +const getUserSessionsById = async ( + id: string, + authHeader: string | undefined, +): Promise => { + const user = await getUserFromAuth(authHeader); + if (!user || user.id !== id) { + return null; + } + + return await getDB().session.findMany({ + where: { + userId: id, + }, + }); +}; + +export { getUserById, getUserSessionsById }; diff --git a/src/store/index.ts b/src/store/index.ts index 6f8f47b..97c5d8e 100644 --- a/src/store/index.ts +++ b/src/store/index.ts @@ -1,2 +1 @@ export * from "./store.js"; -export * from "./types.js"; diff --git a/src/store/store.ts b/src/store/store.ts index 51aa53e..e4314c2 100644 --- a/src/store/store.ts +++ b/src/store/store.ts @@ -10,18 +10,8 @@ const adapter = new PrismaPg(pool); const prisma = new PrismaClient({ adapter }); -async function testdb() { - const test = await prisma.user.findMany(); - /* - const user = await prisma.user.create({ - data: { name: "Alice", email: `alice${Math.random()}@example.com` }, - }); +const getDB = (): PrismaClient => { + return prisma; +}; - console.log("Created user:", user); - - const test = await prisma.user.findMany(); - console.log(test); - */ -} - -export { testdb }; +export { getDB }; diff --git a/src/store/types.ts b/src/store/types.ts deleted file mode 100644 index 0cb5f6c..0000000 --- a/src/store/types.ts +++ /dev/null @@ -1,3 +0,0 @@ -interface IState {} - -export { type IState };