Add services and auth

This commit is contained in:
Aslan 2025-12-23 17:45:51 -05:00
parent 9b0b5dc040
commit 5dec454afb
46 changed files with 900 additions and 31 deletions

221
package-lock.json generated
View file

@ -11,10 +11,13 @@
"dependencies": { "dependencies": {
"@prisma/adapter-pg": "^7.2.0", "@prisma/adapter-pg": "^7.2.0",
"@prisma/client": "^7.2.0", "@prisma/client": "^7.2.0",
"argon2": "^0.44.0",
"fastify": "^5.6.2", "fastify": "^5.6.2",
"jsonwebtoken": "^9.0.3",
"pg": "^8.16.3" "pg": "^8.16.3"
}, },
"devDependencies": { "devDependencies": {
"@types/jsonwebtoken": "^9.0.10",
"@types/node": "^25.0.3", "@types/node": "^25.0.3",
"@types/pg": "^8.16.0", "@types/pg": "^8.16.0",
"dotenv": "^17.2.3", "dotenv": "^17.2.3",
@ -103,6 +106,12 @@
"@electric-sql/pglite": "0.3.2" "@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": { "node_modules/@fastify/ajv-compiler": {
"version": "4.0.5", "version": "4.0.5",
"resolved": "https://registry.npmjs.org/@fastify/ajv-compiler/-/ajv-compiler-4.0.5.tgz", "resolved": "https://registry.npmjs.org/@fastify/ajv-compiler/-/ajv-compiler-4.0.5.tgz",
@ -269,6 +278,15 @@
"node": ">=16" "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": { "node_modules/@pinojs/redact": {
"version": "0.4.0", "version": "0.4.0",
"resolved": "https://registry.npmjs.org/@pinojs/redact/-/redact-0.4.0.tgz", "resolved": "https://registry.npmjs.org/@pinojs/redact/-/redact-0.4.0.tgz",
@ -494,6 +512,24 @@
"dev": true, "dev": true,
"license": "MIT" "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": { "node_modules/@types/node": {
"version": "25.0.3", "version": "25.0.3",
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.0.3.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-25.0.3.tgz",
@ -599,6 +635,22 @@
"dev": true, "dev": true,
"license": "MIT" "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": { "node_modules/atomic-sleep": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz", "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz",
@ -628,6 +680,12 @@
"node": ">= 6.0.0" "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": { "node_modules/c12": {
"version": "3.1.0", "version": "3.1.0",
"resolved": "https://registry.npmjs.org/c12/-/c12-3.1.0.tgz", "resolved": "https://registry.npmjs.org/c12/-/c12-3.1.0.tgz",
@ -748,11 +806,27 @@
"dev": true, "dev": true,
"license": "MIT" "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": { "node_modules/cross-spawn": {
"version": "7.0.6", "version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
"devOptional": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"path-key": "^3.1.0", "path-key": "^3.1.0",
@ -837,6 +911,15 @@
"url": "https://dotenvx.com" "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": { "node_modules/effect": {
"version": "3.18.4", "version": "3.18.4",
"resolved": "https://registry.npmjs.org/effect/-/effect-3.18.4.tgz", "resolved": "https://registry.npmjs.org/effect/-/effect-3.18.4.tgz",
@ -1125,7 +1208,6 @@
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
"devOptional": true,
"license": "ISC" "license": "ISC"
}, },
"node_modules/jiti": { "node_modules/jiti": {
@ -1163,6 +1245,49 @@
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
"license": "MIT" "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": { "node_modules/light-my-request": {
"version": "6.6.0", "version": "6.6.0",
"resolved": "https://registry.npmjs.org/light-my-request/-/light-my-request-6.6.0.tgz", "resolved": "https://registry.npmjs.org/light-my-request/-/light-my-request-6.6.0.tgz",
@ -1217,6 +1342,48 @@
"devOptional": true, "devOptional": true,
"license": "MIT" "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": { "node_modules/long": {
"version": "5.3.2", "version": "5.3.2",
"resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz",
@ -1247,6 +1414,12 @@
"dev": true, "dev": true,
"license": "ISC" "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": { "node_modules/mysql2": {
"version": "3.15.3", "version": "3.15.3",
"resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.15.3.tgz", "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.15.3.tgz",
@ -1281,6 +1454,15 @@
"node": ">=8.0.0" "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": { "node_modules/node-fetch-native": {
"version": "1.6.7", "version": "1.6.7",
"resolved": "https://registry.npmjs.org/node-fetch-native/-/node-fetch-native-1.6.7.tgz", "resolved": "https://registry.npmjs.org/node-fetch-native/-/node-fetch-native-1.6.7.tgz",
@ -1288,6 +1470,17 @@
"devOptional": true, "devOptional": true,
"license": "MIT" "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": { "node_modules/nypm": {
"version": "0.6.2", "version": "0.6.2",
"resolved": "https://registry.npmjs.org/nypm/-/nypm-0.6.2.tgz", "resolved": "https://registry.npmjs.org/nypm/-/nypm-0.6.2.tgz",
@ -1328,7 +1521,6 @@
"version": "3.1.1", "version": "3.1.1",
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
"integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
"devOptional": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=8" "node": ">=8"
@ -1760,6 +1952,26 @@
"integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==",
"license": "MIT" "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": { "node_modules/safe-regex2": {
"version": "5.0.0", "version": "5.0.0",
"resolved": "https://registry.npmjs.org/safe-regex2/-/safe-regex2-5.0.0.tgz", "resolved": "https://registry.npmjs.org/safe-regex2/-/safe-regex2-5.0.0.tgz",
@ -1847,7 +2059,6 @@
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
"integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
"devOptional": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"shebang-regex": "^3.0.0" "shebang-regex": "^3.0.0"
@ -1860,7 +2071,6 @@
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
"integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
"devOptional": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=8" "node": ">=8"
@ -2046,7 +2256,6 @@
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
"devOptional": true,
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"isexe": "^2.0.0" "isexe": "^2.0.0"

View file

@ -15,6 +15,7 @@
"start": "npx tsc && node --env-file=.env dist/index.js" "start": "npx tsc && node --env-file=.env dist/index.js"
}, },
"devDependencies": { "devDependencies": {
"@types/jsonwebtoken": "^9.0.10",
"@types/node": "^25.0.3", "@types/node": "^25.0.3",
"@types/pg": "^8.16.0", "@types/pg": "^8.16.0",
"dotenv": "^17.2.3", "dotenv": "^17.2.3",
@ -25,7 +26,9 @@
"dependencies": { "dependencies": {
"@prisma/adapter-pg": "^7.2.0", "@prisma/adapter-pg": "^7.2.0",
"@prisma/client": "^7.2.0", "@prisma/client": "^7.2.0",
"argon2": "^0.44.0",
"fastify": "^5.6.2", "fastify": "^5.6.2",
"jsonwebtoken": "^9.0.3",
"pg": "^8.16.3" "pg": "^8.16.3"
} }
} }

View file

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

View file

@ -0,0 +1,3 @@
export * from "./auth.js";
export * from "./routes.js";
export * from "./types.js";

View file

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

View file

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

View file

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

View file

@ -0,0 +1,3 @@
export * from "./channel.js";
export * from "./routes.js";
export * from "./types.js";

View file

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

View file

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

View file

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

View file

@ -0,0 +1,3 @@
export * from "./community.js";
export * from "./routes.js";
export * from "./types.js";

View file

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

View file

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

View file

@ -0,0 +1,3 @@
export * from "./role.js";
export * from "./routes.js";
export * from "./types.js";

View file

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

View file

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

View file

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

View file

@ -0,0 +1,3 @@
export * from "./session.js";
export * from "./routes.js";
export * from "./types.js";

View file

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

View file

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

View file

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

View file

@ -2,7 +2,8 @@ import { type FastifyInstance } from "fastify";
import * as controller from "./test.js"; import * as controller from "./test.js";
const testRoutes = async (fastify: FastifyInstance) => { const testRoutes = async (fastify: FastifyInstance) => {
fastify.get("/test", controller.test); fastify.get("/ping", controller.getPing);
fastify.get("/test", controller.getTest);
}; };
export { testRoutes }; export { testRoutes };

View file

@ -1,10 +1,11 @@
import { type FastifyReply, type FastifyRequest } from "fastify"; import { type FastifyReply, type FastifyRequest } from "fastify";
import { testdb } from "../../store/store.js";
const test = async (request: FastifyRequest, reply: FastifyReply) => { const getPing = async (_request: FastifyRequest, _reply: FastifyReply) => {
testdb(); return [{ message: "pong" }];
return [{ name: "Alice" }];
}; };
export { test }; const getTest = async (_request: FastifyRequest, _reply: FastifyReply) => {
return [{ message: "ok" }];
};
export { getPing, getTest };

View file

@ -0,0 +1,3 @@
export * from "./user.js";
export * from "./routes.js";
export * from "./types.js";

View file

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

View file

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

View file

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

59
src/helpers.ts Normal file
View file

@ -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<Session | null> => {
return await getDB().session.findFirst({
where: {
token: token,
},
});
};
const getUserFromToken = async (token: string): Promise<User | null> => {
const session = await getSessionFromToken(token);
return await getDB().user.findFirst({
where: {
id: session?.userId ?? "invalid",
},
});
};
const getUserFromAuth = async (
authHeader: string | undefined,
): Promise<User | null> => {
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,
};

View file

@ -2,12 +2,24 @@ import { config } from "./config.js";
import Fastify from "fastify"; import Fastify from "fastify";
import { testRoutes } from "./controllers/test/routes.js"; 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({ const app = Fastify({
logger: true, logger: true,
}); });
app.register(testRoutes); 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) => { app.listen({ port: config.port }, (err, address) => {
if (err) throw err; if (err) throw err;

72
src/services/auth/auth.ts Normal file
View file

@ -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<User | null> => {
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<Session | null> => {
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<string> => {
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 };

View file

@ -0,0 +1,2 @@
export * from "./auth.js";
export * from "./types.js";

View file

@ -0,0 +1,12 @@
interface IUserRegistration {
username: string;
password: string;
email?: string | undefined;
}
interface IUserLogin {
username: string;
password: string;
}
export { type IUserRegistration, type IUserLogin };

View file

@ -0,0 +1,10 @@
import type { Channel } from "../../generated/prisma/client.js";
import { getDB } from "../../store/store.js";
const getChannelById = async (id: string): Promise<Channel | null> => {
return await getDB().channel.findUnique({
where: { id: id },
});
};
export { getChannelById };

View file

@ -0,0 +1 @@
export * from "./channel.js";

View file

@ -0,0 +1,10 @@
import type { Community } from "../../generated/prisma/client.js";
import { getDB } from "../../store/store.js";
const getCommunityById = async (id: string): Promise<Community | null> => {
return await getDB().community.findUnique({
where: { id: id },
});
};
export { getCommunityById };

View file

@ -0,0 +1 @@
export * from "./community.js";

View file

@ -0,0 +1 @@
export * from "./role.js";

10
src/services/role/role.ts Normal file
View file

@ -0,0 +1,10 @@
import type { Role } from "../../generated/prisma/client.js";
import { getDB } from "../../store/store.js";
const getRoleById = async (id: string): Promise<Role | null> => {
return await getDB().role.findUnique({
where: { id: id },
});
};
export { getRoleById };

View file

@ -0,0 +1 @@
export * from "./session.js";

View file

@ -0,0 +1,10 @@
import type { Session } from "../../generated/prisma/client.js";
import { getDB } from "../../store/store.js";
const getSessionById = async (id: string): Promise<Session | null> => {
return await getDB().session.findUnique({
where: { id: id },
});
};
export { getSessionById };

View file

@ -0,0 +1 @@
export * from "./user.js";

27
src/services/user/user.ts Normal file
View file

@ -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<User | null> => {
return await getDB().user.findUnique({
where: { id: id },
});
};
const getUserSessionsById = async (
id: string,
authHeader: string | undefined,
): Promise<Session[] | null> => {
const user = await getUserFromAuth(authHeader);
if (!user || user.id !== id) {
return null;
}
return await getDB().session.findMany({
where: {
userId: id,
},
});
};
export { getUserById, getUserSessionsById };

View file

@ -1,2 +1 @@
export * from "./store.js"; export * from "./store.js";
export * from "./types.js";

View file

@ -10,18 +10,8 @@ const adapter = new PrismaPg(pool);
const prisma = new PrismaClient({ adapter }); const prisma = new PrismaClient({ adapter });
async function testdb() { const getDB = (): PrismaClient => {
const test = await prisma.user.findMany(); return prisma;
/* };
const user = await prisma.user.create({
data: { name: "Alice", email: `alice${Math.random()}@example.com` },
});
console.log("Created user:", user); export { getDB };
const test = await prisma.user.findMany();
console.log(test);
*/
}
export { testdb };

View file

@ -1,3 +0,0 @@
interface IState {}
export { type IState };