generated from azures04/Base-REST-API
Add auth, JWT tokens, and bootstrap startup
Introduce bootstrap entrypoint and prestartup to ping DB and generate/manage RSA keypair. Add security module for key IO. Implement JWT-based access and refresh tokens (tokensService) with refresh_tokens table and repo. Add auth, provider and user services, and refresh-token management. Update repositories (users, credentials, providers, servers) with SQL fixes and new helper methods. Move DDL execution to bootstrap (remove from server). Update package.json main and add dependencies (bcryptjs, jsonwebtoken). Update .env.example and .gitignore accordingly.
This commit is contained in:
@@ -7,3 +7,11 @@ DATABASE_HOST="host"
|
||||
DATABASE_USER="changeme"
|
||||
DATABASE_PASSWORD="password"
|
||||
DATABASE_NAME="tcjourney"
|
||||
|
||||
#Security
|
||||
BCRYPT_SALT="12"
|
||||
ACCESS_TOKEN_EXPIRY="432000" #TIME IN SECONDES
|
||||
REFRESH_TOKEN_EXPIRY="2592000" #TIME IN SECONDES
|
||||
|
||||
#LocalAuth
|
||||
SERVER_UUID="00000000-0000-0000-0000-000000000000" #DON'T CHANGE IT!!!
|
||||
|
||||
@@ -132,3 +132,9 @@ dist
|
||||
|
||||
#logs
|
||||
logs
|
||||
|
||||
#secrets
|
||||
secrets/
|
||||
|
||||
#tests
|
||||
test.js
|
||||
Vendored
+17
@@ -0,0 +1,17 @@
|
||||
const prestartup = require("./modules/prestartup")
|
||||
const database = require("./modules/database")
|
||||
const logger = require("./modules/logger")
|
||||
|
||||
async function boot() {
|
||||
try {
|
||||
await prestartup.pingDb()
|
||||
await database.runDDL()
|
||||
await prestartup.setupKeys()
|
||||
require("./server")
|
||||
} catch (error) {
|
||||
logger.error("Error while booting", ["STARTUP", "cyan"])
|
||||
logger.error(error.toString(), ["STARTUP", "cyan"])
|
||||
}
|
||||
}
|
||||
|
||||
boot()
|
||||
@@ -1,8 +1,9 @@
|
||||
CREATE TABLE IF NOT EXISTS `users` (
|
||||
`id` UUID PRIMARY KEY DEFAULT UUID(),
|
||||
`serverId` UUID NULL,
|
||||
`identifier` VARCHAR(255) NOT NULL UNIQUE,
|
||||
`remoteId` VARCHAR(255) NULL,
|
||||
`displayName` VARCHAR(255) NOT NULL,
|
||||
`displayName` VARCHAR(512) NOT NULL,
|
||||
`avatarUrl` TEXT NULL,
|
||||
`createdAt` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
CREATE TABLE IF NOT EXISTS `refresh_tokens` (
|
||||
`userId` UUID PRIMARY KEY,
|
||||
`tokenHash` TEXT NOT NULL,
|
||||
`expiresAt` TIMESTAMP NOT NULL,
|
||||
|
||||
CONSTRAINT `fk_refreshTokens_userId` FOREIGN KEY (`userId`) REFERENCES `users` (`id`) ON DELETE CASCADE
|
||||
)
|
||||
@@ -0,0 +1,53 @@
|
||||
const security = require("./security")
|
||||
const serversRepo = require("../repositories/serversRepo")
|
||||
const logger = require("./logger")
|
||||
const { pool } = require("./database")
|
||||
|
||||
const LOCAL_SERVER_ID = process.env.SERVER_UUID
|
||||
|
||||
async function setupKeys() {
|
||||
const existing = await serversRepo.findById(LOCAL_SERVER_ID)
|
||||
if (existing) {
|
||||
checkKeys(existing.publicKey)
|
||||
return existing
|
||||
}
|
||||
|
||||
const { privateKeyPem, publicKeyPem } = security.generateServerKeyPair()
|
||||
security.saveKeyPairToDisk({ privateKeyPem, publicKeyPem })
|
||||
|
||||
logger.log("Local server keypair generated and stored in data/secrets", ["KEYS", "yellow"])
|
||||
|
||||
return await serversRepo.createAndAssignIdManually(LOCAL_SERVER_ID, "internal", publicKeyPem)
|
||||
}
|
||||
|
||||
function checkKeys(publicKeyFromDb) {
|
||||
const { privateExists, publicExists } = security.keyFilesExist()
|
||||
|
||||
if (!privateExists) {
|
||||
throw new Error(
|
||||
"Private key file missing for local server, but server row exists in DB.",
|
||||
"Cannot regenerate without invalidating existing signatures/tokens."
|
||||
)
|
||||
}
|
||||
|
||||
if (!publicExists) {
|
||||
security.restorePublicKeyFile(publicKeyFromDb)
|
||||
logger.warn("Public key file was missing, restored from DB.", ["KEYS", "yellow"])
|
||||
}
|
||||
}
|
||||
|
||||
async function pingDb() {
|
||||
try {
|
||||
await pool.query("SELECT 1")
|
||||
} catch (err) {
|
||||
console.log(err)
|
||||
logger.error("Could'nt connect to db", ["PRESTARTUP", "cyan"])
|
||||
process.exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
pingDb,
|
||||
checkKeys,
|
||||
setupKeys,
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
const fs = require("node:fs")
|
||||
const path = require("node:path")
|
||||
const crypto = require("node:crypto")
|
||||
|
||||
const SECRETS_DIR = path.join(process.cwd(), "data", "secrets")
|
||||
const PRIVATE_KEY = path.join(SECRETS_DIR, "server.private.pem")
|
||||
const PUBLIC_KEY = path.join(SECRETS_DIR, "server.public.pem")
|
||||
|
||||
function generateServerKeyPair() {
|
||||
const { privateKey, publicKey } = crypto.generateKeyPairSync("rsa", {
|
||||
modulusLength: 2048,
|
||||
publicKeyEncoding: {
|
||||
type: "spki",
|
||||
format: "pem",
|
||||
},
|
||||
privateKeyEncoding: {
|
||||
type: "pkcs8",
|
||||
format: "pem",
|
||||
},
|
||||
})
|
||||
|
||||
return {
|
||||
privateKeyPem: privateKey,
|
||||
publicKeyPem: publicKey,
|
||||
}
|
||||
}
|
||||
|
||||
function saveKeyPairToDisk({ publicKeyPem, privateKeyPem }) {
|
||||
fs.mkdirSync(SECRETS_DIR, { recursive: true })
|
||||
fs.writeFileSync(PRIVATE_KEY, privateKeyPem, { mode: 0o600 })
|
||||
fs.writeFileSync(PUBLIC_KEY, publicKeyPem, { mode: 0o644 })
|
||||
}
|
||||
|
||||
function keyFilesExist() {
|
||||
return {
|
||||
privateExists: fs.existsSync(PRIVATE_KEY),
|
||||
publicExists: fs.existsSync(PUBLIC_KEY),
|
||||
}
|
||||
}
|
||||
|
||||
function restorePublicKeyFile(publicKeyPem) {
|
||||
fs.mkdirSync(SECRETS_DIR, { recursive: true })
|
||||
fs.writeFileSync(PUBLIC_KEY, publicKeyPem, { mode: 0o644 })
|
||||
}
|
||||
|
||||
function readPrivateKey() {
|
||||
return fs.readFileSync(PRIVATE_KEY, "utf8")
|
||||
}
|
||||
|
||||
function readPublicKey() {
|
||||
return fs.readFileSync(PUBLIC_KEY, "utf8")
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
generateServerKeyPair,
|
||||
saveKeyPairToDisk,
|
||||
keyFilesExist,
|
||||
restorePublicKeyFile,
|
||||
readPrivateKey,
|
||||
readPublicKey
|
||||
}
|
||||
Generated
+131
-1
@@ -9,12 +9,14 @@
|
||||
"version": "0.0.1-alpha",
|
||||
"license": "AGPL-3.0-only",
|
||||
"dependencies": {
|
||||
"bcryptjs": "^3.0.3",
|
||||
"colors": "^1.4.0",
|
||||
"cors": "^2.8.6",
|
||||
"dotenv": "^17.4.2",
|
||||
"express": "^5.2.1",
|
||||
"helmet": "^8.3.0",
|
||||
"hpp": "^0.2.3",
|
||||
"jsonwebtoken": "^9.0.3",
|
||||
"mariadb": "^3.5.3",
|
||||
"path-to-regexp": "^8.4.2",
|
||||
"zod": "^4.4.3"
|
||||
@@ -333,6 +335,15 @@
|
||||
"node": "18 || 20 || >=22"
|
||||
}
|
||||
},
|
||||
"node_modules/bcryptjs": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-3.0.3.tgz",
|
||||
"integrity": "sha512-GlF5wPWnSa/X5LKM1o0wz0suXIINz1iHRLvTS+sLyi7XPbe5ycmYI3DlZqVGZZtDgl4DmasFg7gOB3JYbphV5g==",
|
||||
"license": "BSD-3-Clause",
|
||||
"bin": {
|
||||
"bcrypt": "bin/bcrypt"
|
||||
}
|
||||
},
|
||||
"node_modules/binary-extensions": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
|
||||
@@ -396,6 +407,12 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"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/bytes": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
|
||||
@@ -608,6 +625,15 @@
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"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/ee-first": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
|
||||
@@ -1375,6 +1401,49 @@
|
||||
"dev": true,
|
||||
"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/keyv": {
|
||||
"version": "4.5.4",
|
||||
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
|
||||
@@ -1421,6 +1490,48 @@
|
||||
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
|
||||
"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/lru-cache": {
|
||||
"version": "11.5.2",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz",
|
||||
@@ -1830,6 +1941,26 @@
|
||||
"node": ">= 18"
|
||||
}
|
||||
},
|
||||
"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/safer-buffer": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
||||
@@ -1840,7 +1971,6 @@
|
||||
"version": "7.7.3",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
|
||||
"integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"bin": {
|
||||
"semver": "bin/semver.js"
|
||||
|
||||
+3
-1
@@ -13,7 +13,7 @@
|
||||
"email": "gilleslazure04@gmail.com"
|
||||
},
|
||||
"type": "commonjs",
|
||||
"main": "server.js",
|
||||
"main": "bootstrap.js",
|
||||
"scripts": {
|
||||
"start:dev": "nodemon .",
|
||||
"start": "node .",
|
||||
@@ -23,12 +23,14 @@
|
||||
"homepage": "https://gitea.azures.fr/azures04/Base-REST-API",
|
||||
"readme": "https://gitea.azures.fr/azures04/Base-REST-API/src/branch/main/README.md",
|
||||
"dependencies": {
|
||||
"bcryptjs": "^3.0.3",
|
||||
"colors": "^1.4.0",
|
||||
"cors": "^2.8.6",
|
||||
"dotenv": "^17.4.2",
|
||||
"express": "^5.2.1",
|
||||
"helmet": "^8.3.0",
|
||||
"hpp": "^0.2.3",
|
||||
"jsonwebtoken": "^9.0.3",
|
||||
"mariadb": "^3.5.3",
|
||||
"path-to-regexp": "^8.4.2",
|
||||
"zod": "^4.4.3"
|
||||
|
||||
@@ -3,7 +3,7 @@ const { DefaultError } = require("../errors/errors")
|
||||
|
||||
async function findByUserId(userId) {
|
||||
try {
|
||||
const sql = "SELECT* FROM credentials WHERE userId = ?"
|
||||
const sql = "SELECT * FROM credentials WHERE userId = ?"
|
||||
const rows = await pool.query(sql, [userId])
|
||||
return rows[0] || null
|
||||
} catch (error) {
|
||||
@@ -11,20 +11,20 @@ async function findByUserId(userId) {
|
||||
}
|
||||
}
|
||||
|
||||
async function create(userId, hashedPassword) {
|
||||
async function create(userId, passwordHash) {
|
||||
try {
|
||||
const sql = "INSERT INTO credentials (userId, hashedPassword) VALUES (?, ?)"
|
||||
const rows = await pool.query(sql, [userId, hashedPassword])
|
||||
return rows[0] || null
|
||||
const sql = "INSERT INTO credentials (userId, passwordHash) VALUES (?, ?)"
|
||||
const rows = await pool.query(sql, [userId, passwordHash])
|
||||
return rows || null
|
||||
} catch (error) {
|
||||
throw new DefaultError(500, "Internal Server Error", error)
|
||||
}
|
||||
}
|
||||
|
||||
async function updatePassword(userId, hashedPassword) {
|
||||
async function updatePassword(userId, passwordHash) {
|
||||
try {
|
||||
const sql = "UPDATE credentials SET hashedPassword = ? WHERE userId = ?"
|
||||
const rows = await pool.query(sql, [hashedPassword, userId])
|
||||
const sql = "UPDATE credentials SET passwordHash = ? WHERE userId = ?"
|
||||
const rows = await pool.query(sql, [passwordHash, userId])
|
||||
return rows.affectedRows > 0
|
||||
} catch (error) {
|
||||
throw new DefaultError(500, "Internal Server Error", error)
|
||||
|
||||
@@ -3,7 +3,7 @@ const { DefaultError } = require("../errors/errors")
|
||||
|
||||
async function findByUserId(userId) {
|
||||
try {
|
||||
const sql = "SELECT* FROM identities WHERE userId = ?"
|
||||
const sql = "SELECT * FROM identities WHERE userId = ?"
|
||||
const rows = await pool.query(sql, [userId])
|
||||
return rows || null
|
||||
} catch (error) {
|
||||
@@ -13,7 +13,7 @@ async function findByUserId(userId) {
|
||||
|
||||
async function findByProviderAndSubject(providerId, providerUserId) {
|
||||
try {
|
||||
const sql = "SELECT* FROM identities WHERE providerId = ? AND providerUserId = ?"
|
||||
const sql = "SELECT * FROM identities WHERE providerId = ? AND providerUserId = ?"
|
||||
const rows = await pool.query(sql, [providerId, providerUserId])
|
||||
return rows[0] || null
|
||||
} catch (error) {
|
||||
|
||||
@@ -3,7 +3,7 @@ const { DefaultError } = require("../errors/errors")
|
||||
|
||||
async function findById(id) {
|
||||
try {
|
||||
const sql = "SELECT* FROM providers WHERE id = ?"
|
||||
const sql = "SELECT * FROM providers WHERE id = ?"
|
||||
const rows = await pool.query(sql, [id])
|
||||
return rows[0] || null
|
||||
} catch (error) {
|
||||
@@ -13,7 +13,7 @@ async function findById(id) {
|
||||
|
||||
async function findByName(id) {
|
||||
try {
|
||||
const sql = "SELECT* FROM providers WHERE name = ?"
|
||||
const sql = "SELECT * FROM providers WHERE name = ?"
|
||||
const rows = await pool.query(sql, [id])
|
||||
return rows[0] || null
|
||||
} catch (error) {
|
||||
@@ -23,7 +23,7 @@ async function findByName(id) {
|
||||
|
||||
async function listEnabled() {
|
||||
try {
|
||||
const sql = "SELECT* FROM providers WHERE isEnabled = ?"
|
||||
const sql = "SELECT * FROM providers WHERE isEnabled = ?"
|
||||
const rows = await pool.query(sql, [true])
|
||||
return rows || []
|
||||
} catch (error) {
|
||||
@@ -31,6 +31,16 @@ async function listEnabled() {
|
||||
}
|
||||
}
|
||||
|
||||
async function listAll() {
|
||||
try {
|
||||
const sql = "SELECT * FROM providers"
|
||||
const rows = await pool.query(sql)
|
||||
return rows || []
|
||||
} catch (error) {
|
||||
throw new DefaultError(500, "Internal Server Error", error)
|
||||
}
|
||||
}
|
||||
|
||||
async function setEnabled(id, isEnabled) {
|
||||
try {
|
||||
const sql = "UPDATE providers SET isEnabled = ? WHERE id = ?"
|
||||
@@ -97,6 +107,7 @@ async function remove(id) {
|
||||
module.exports = {
|
||||
remove,
|
||||
create,
|
||||
listAll,
|
||||
findById,
|
||||
findByName,
|
||||
setEnabled,
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
const { pool } = require("../modules/database")
|
||||
const { DefaultError } = require("../errors/errors")
|
||||
|
||||
async function create(userId, tokenHash, expiresAt) {
|
||||
try {
|
||||
const sql = "INSERT INTO refresh_tokens (userId, tokenHash, expiresAt) VALUES (?, ?, ?)"
|
||||
const rows = await pool.query(sql, [userId, tokenHash, expiresAt])
|
||||
return rows.affectedRows > 0
|
||||
} catch (error) {
|
||||
throw new DefaultError(500, "Internal Server Error", error)
|
||||
}
|
||||
}
|
||||
|
||||
async function findByTokenHash(tokenHash) {
|
||||
try {
|
||||
const sql = "SELECT * FROM refresh_tokens WHERE tokenHash = ?"
|
||||
const rows = await pool.query(sql, [tokenHash])
|
||||
return rows[0] || null
|
||||
} catch (error) {
|
||||
throw new DefaultError(500, "Internal Server Error", error)
|
||||
}
|
||||
}
|
||||
|
||||
async function findByUserId(userId) {
|
||||
try {
|
||||
const sql = "SELECT * FROM refresh_tokens WHERE userId = ?"
|
||||
const rows = await pool.query(sql, [userId])
|
||||
return rows[0] || []
|
||||
} catch (error) {
|
||||
throw new DefaultError(500, "Internal Server Error", error)
|
||||
}
|
||||
}
|
||||
|
||||
async function removeByTokenHash(tokenHash) {
|
||||
try {
|
||||
const sql = "DELETE FROM refresh_tokens WHERE tokenHash = ?"
|
||||
const rows = await pool.query(sql, [tokenHash])
|
||||
return rows.affectedRows > 0
|
||||
} catch (error) {
|
||||
throw new DefaultError(500, "Internal Server Error", error)
|
||||
}
|
||||
}
|
||||
|
||||
async function removeByUserId(userId) {
|
||||
try {
|
||||
const sql = "DELETE FROM refresh_tokens WHERE userId = ?"
|
||||
const rows = await pool.query(sql, [userId])
|
||||
return rows.affectedRows > 0
|
||||
} catch (error) {
|
||||
throw new DefaultError(500, "Internal Server Error", error)
|
||||
}
|
||||
}
|
||||
|
||||
async function removeExpired() {
|
||||
try {
|
||||
const sql = "DELETE FROM refresh_tokens WHERE expiresAt < NOW()"
|
||||
const rows = await pool.query(sql)
|
||||
return rows.affectedRows
|
||||
} catch (error) {
|
||||
throw new DefaultError(500, "Internal Server Error", error)
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
create,
|
||||
findByTokenHash,
|
||||
findByUserId,
|
||||
removeByTokenHash,
|
||||
removeByUserId,
|
||||
removeExpired,
|
||||
}
|
||||
@@ -3,7 +3,7 @@ const { DefaultError } = require("../errors/errors")
|
||||
|
||||
async function findById(id) {
|
||||
try {
|
||||
const sql = "SELECT* FROM servers WHERE id = ?"
|
||||
const sql = "SELECT * FROM servers WHERE id = ?"
|
||||
const rows = await pool.query(sql, [id])
|
||||
return rows[0] || null
|
||||
} catch (error) {
|
||||
@@ -13,7 +13,7 @@ async function findById(id) {
|
||||
|
||||
async function findByUrl(url) {
|
||||
try {
|
||||
const sql = "SELECT* FROM servers WHERE url = ?"
|
||||
const sql = "SELECT * FROM servers WHERE url = ?"
|
||||
const rows = await pool.query(sql, [url])
|
||||
return rows[0] || null
|
||||
} catch (error) {
|
||||
@@ -31,6 +31,16 @@ async function create(url, publicKey) {
|
||||
}
|
||||
}
|
||||
|
||||
async function createAndAssignIdManually(id, url, publicKey) {
|
||||
try {
|
||||
const sql = "INSERT INTO servers (id, serverUrl, publicKey) VALUES (?, ?, ?)"
|
||||
const rows = await pool.query(sql, [id, url, publicKey])
|
||||
return rows[0] || null
|
||||
} catch (error) {
|
||||
throw new DefaultError(500, "Internal Server Error", error)
|
||||
}
|
||||
}
|
||||
|
||||
async function remove(id) {
|
||||
try {
|
||||
const sql = "DELETE FROM servers WHERE id = ?"
|
||||
@@ -74,5 +84,6 @@ module.exports = {
|
||||
remove,
|
||||
findById,
|
||||
findByUrl,
|
||||
updateServer
|
||||
updateServer,
|
||||
createAndAssignIdManually
|
||||
}
|
||||
@@ -3,7 +3,7 @@ const { DefaultError } = require("../errors/errors")
|
||||
|
||||
async function findById(id) {
|
||||
try {
|
||||
const sql = "SELECT* FROM servers WHERE id = ?"
|
||||
const sql = "SELECT * FROM users WHERE id = ?"
|
||||
const rows = await pool.query(sql, [id])
|
||||
return rows[0] || null
|
||||
} catch (error) {
|
||||
@@ -11,16 +11,29 @@ async function findById(id) {
|
||||
}
|
||||
}
|
||||
|
||||
async function create(serverId, remoteId = null, displayName, avatarUrl) {
|
||||
async function findByIdentifier(identifier) {
|
||||
try {
|
||||
const sql = "INSERT INTO users (serverId, remoteId, displayName, avatarUrl) VALUES (?, ?, ?, ?)"
|
||||
const rows = await pool.query(sql, [serverId, remoteId, displayName, avatarUrl])
|
||||
const sql = "SELECT * FROM users WHERE identifier = ?"
|
||||
const rows = await pool.query(sql, [identifier])
|
||||
return rows[0] || null
|
||||
} catch (error) {
|
||||
throw new DefaultError(500, "Internal Server Error", error)
|
||||
}
|
||||
}
|
||||
|
||||
async function create(identifier, serverId, remoteId = null, displayName, avatarUrl) {
|
||||
try {
|
||||
const sql = "INSERT INTO users (identifier, serverId, remoteId, displayName, avatarUrl) VALUES (?, ?, ?, ?, ?) RETURNING *"
|
||||
const rows = await pool.query(sql, [identifier, serverId, remoteId, displayName, avatarUrl])
|
||||
return rows[0] || null
|
||||
} catch (error) {
|
||||
if (error.code && error.code == "ER_DUP_ENTRY") {
|
||||
throw new DefaultError(401, "Identifier already taken.", "Identifier already assigned.")
|
||||
}
|
||||
throw new DefaultError(500, "Internal Server Error", error)
|
||||
}
|
||||
}
|
||||
|
||||
async function remove(id) {
|
||||
try {
|
||||
const sql = "DELETE FROM users WHERE id = ?"
|
||||
@@ -86,5 +99,6 @@ module.exports = {
|
||||
isLocal,
|
||||
findById,
|
||||
updateProfile,
|
||||
findByIdentifier,
|
||||
findByServerAndRemoteId
|
||||
}
|
||||
@@ -25,9 +25,6 @@ app.use(express.urlencoded({ extended: true }))
|
||||
|
||||
app.set("trust proxy", true)
|
||||
|
||||
database.runDDL()
|
||||
.catch(err => logger.error(err))
|
||||
|
||||
logger.log("Initializing routes", ["WEB", "yellow"])
|
||||
|
||||
for (const schemaFile of schemas) {
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
const bcrypt = require("bcryptjs")
|
||||
const usersRepo = require("../repositories/usersRepo")
|
||||
const tokensService = require("./tokensService")
|
||||
const identitiesRepo = require("../repositories/identitiesRepo")
|
||||
const credentialsRepo = require("../repositories/credentialsRepo")
|
||||
const { DefaultError } = require("../errors/errors")
|
||||
|
||||
async function registerLocal({ identifier, password, profile = { displayName, avatarURL } }) {
|
||||
try {
|
||||
const user = await usersRepo.create(identifier, process.env.SERVER_UUID, null, profile.displayName, profile.avatarURL)
|
||||
const hashedPassword = await bcrypt.hash(password, Number(process.env.BCRYPT_SALT))
|
||||
const credentials = await credentialsRepo.create(user.id, hashedPassword)
|
||||
if (Array.isArray(credentials)) {
|
||||
throw new DefaultError(500, "Internal Server Error", "UserRegistration")
|
||||
}
|
||||
return user
|
||||
} catch (error) {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
async function validatePassword({ password, passwordHash }) {
|
||||
try {
|
||||
await bcrypt.compare(password, passwordHash)
|
||||
} catch (error) {
|
||||
throw new DefaultError(401, "Bad credentials", "Wrong password")
|
||||
}
|
||||
}
|
||||
|
||||
async function loginLocal({ identifier, password }) {
|
||||
try {
|
||||
const user = await usersRepo.findByIdentifier(identifier)
|
||||
const credentials = await credentialsRepo.findByUserId(user.id)
|
||||
await validatePassword({ password: password, passwordHash: credentials.passwordHash })
|
||||
const accessToken = await tokensService.signToken(user)
|
||||
const refreshToken = await tokensService.signRefreshToken(user.id)
|
||||
return { user, tokens: { accessToken, refreshToken } }
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
throw new DefaultError(500, "Internal Server Error", error.toString())
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshAccessToken({ userId, refreshToken }) {
|
||||
try {
|
||||
await tokensService.verifyRefreshToken(refreshToken)
|
||||
await tokensService.revokeRefreshToken(refreshToken)
|
||||
const user = await usersRepo.findById(userId)
|
||||
const newAccessToken = await tokensService.signToken(user)
|
||||
const newRefreshToken = await tokensService.signRefreshToken(userId)
|
||||
return { accessToken: newAccessToken, refreshToken: newRefreshToken }
|
||||
} catch (error) {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
async function changePassword({ userId, password, newPassword }) {
|
||||
try {
|
||||
const credentials = await credentialsRepo.findByUserId(userId)
|
||||
await validatePassword({ password, passwordHash: credentials.passwordHash })
|
||||
await credentialsRepo.updatePassword(userId, newPassword)
|
||||
return true
|
||||
} catch (error) {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
async function resetPassword({ userId, newPassword }) {
|
||||
try {
|
||||
await credentialsRepo.updatePassword(userId, newPassword)
|
||||
return true
|
||||
} catch (error) {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
loginLocal,
|
||||
resetPassword,
|
||||
registerLocal,
|
||||
changePassword,
|
||||
refreshAccessToken
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
const providersRepo = require("../repositories/providersRepo")
|
||||
|
||||
async function getAvailableProviders() {
|
||||
return await providersRepo.listEnabled()
|
||||
}
|
||||
|
||||
async function getProviders() {
|
||||
return await providersRepo.listAll()
|
||||
}
|
||||
|
||||
async function enableProvider({ providerId }) {
|
||||
return await providersRepo.setEnabled(providerId, true)
|
||||
}
|
||||
|
||||
async function disableProvider({ providerId }) {
|
||||
return await providersRepo.setEnabled(providerId, false)
|
||||
}
|
||||
|
||||
async function createProvider({ name, issuerLogo, issuerURL }) {
|
||||
return await providersRepo.create(name, issuerLogo, issuerURL)
|
||||
}
|
||||
|
||||
async function updateProvider({ id, name, issuerLogo = undefined, issuerURL = undefined }) {
|
||||
return await providersRepo.updateProvider(id, name, issuerLogo, issuerURL)
|
||||
}
|
||||
|
||||
async function deleteProvider({ id }) {
|
||||
return await providersRepo.remove(id)
|
||||
}
|
||||
|
||||
async function getProvider({ id = undefined, name = undefined }) {
|
||||
if (typeof id != "undefined") {
|
||||
return await providersRepo.findById(id)
|
||||
} else if (typeof name != "undefined") {
|
||||
return await providersRepo.findByName(name)
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getProvider,
|
||||
getProviders,
|
||||
enableProvider,
|
||||
updateProvider,
|
||||
createProvider,
|
||||
deleteProvider,
|
||||
disableProvider,
|
||||
getAvailableProviders,
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
const jwt = require("jsonwebtoken")
|
||||
const security = require("../modules/security")
|
||||
const tokenRepo = require("../repositories/refreshTokensRepo")
|
||||
const { DefaultError } = require("../errors/errors")
|
||||
|
||||
const privateKey = security.readPrivateKey()
|
||||
const publicKey = security.readPublicKey()
|
||||
|
||||
async function signToken(user) {
|
||||
try {
|
||||
const payload = {
|
||||
id: user.id,
|
||||
identifier: user.identifier,
|
||||
displayName: user.displayName,
|
||||
avatarUrl: user.avatarUrl
|
||||
}
|
||||
const token = await jwt.sign(payload, privateKey, {
|
||||
algorithm: "RS256",
|
||||
issuer: process.env.FQDN,
|
||||
expiresIn: parseInt(process.env.ACCESS_TOKEN_EXPIRY)
|
||||
})
|
||||
return token
|
||||
} catch (error) {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
async function verifyAccessToken(token) {
|
||||
try {
|
||||
const result = jwt.verify(token, publicKey, {
|
||||
algorithms: ["RS256"],
|
||||
issuer: process.env.FQDN
|
||||
})
|
||||
console.log(result)
|
||||
return result
|
||||
} catch (error) {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
async function signRefreshToken(userId) {
|
||||
try {
|
||||
const isTokenExisting = await tokenRepo.findByUserId(userId)
|
||||
if (!Array.isArray(isTokenExisting)) {
|
||||
await tokenRepo.removeByUserId(userId)
|
||||
}
|
||||
const now = Date.now()
|
||||
const expiresIn = parseInt(process.env.REFRESH_TOKEN_EXPIRY)
|
||||
const expiresAtInMs = new Date(now + (expiresIn * 1000))
|
||||
const token = jwt.sign(
|
||||
{ userId },
|
||||
privateKey,
|
||||
{
|
||||
algorithm: "RS256",
|
||||
issuer: process.env.FQDN,
|
||||
expiresIn: expiresIn
|
||||
}
|
||||
)
|
||||
await tokenRepo.create(userId, token, expiresAtInMs)
|
||||
return token
|
||||
} catch (error) {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
async function revokeRefreshToken(token) {
|
||||
try {
|
||||
return await tokenRepo.removeByTokenHash(token)
|
||||
} catch (error) {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
async function verifyRefreshToken(token) {
|
||||
try {
|
||||
const result = jwt.verify(token, publicKey, {
|
||||
algorithms: ["RS256"],
|
||||
issuer: process.env.FQDN
|
||||
})
|
||||
if (!result) {
|
||||
throw new DefaultError(500, "Internal Server Error")
|
||||
}
|
||||
const refreshTokenInDb = await tokenRepo.findByTokenHash(token)
|
||||
if (refreshTokenInDb == null) {
|
||||
throw new DefaultError(403, "Bad refresh token", "Refresh token doesn't exists")
|
||||
}
|
||||
return true
|
||||
} catch (error) {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
async function verifyAccessTokenFromServer(token) {
|
||||
// TO-DO: Integrate server federation
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
signToken,
|
||||
signRefreshToken,
|
||||
verifyAccessToken,
|
||||
revokeRefreshToken,
|
||||
verifyRefreshToken,
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
const userRepo = require("../repositories/usersRepo")
|
||||
|
||||
async function getProfile({ userId }) {
|
||||
return await userRepo.findById(userId)
|
||||
}
|
||||
|
||||
async function updateProfile({ userId, profile = { displayName, avatarURL } }) {
|
||||
return await userRepo.updateProfile(userId, profile.displayName, profile.avatarURL)
|
||||
}
|
||||
|
||||
async function deleteAccount({ userId }) {
|
||||
return await userRepo.remove(userId)
|
||||
}
|
||||
|
||||
async function isLocal({ userId }) {
|
||||
return await userRepo.isLocal(userId)
|
||||
}
|
||||
|
||||
async function createUser({ identifier, serverId, remoteId = null, profile = { displayName, avatarUrl } }) {
|
||||
return await userRepo.create(identifier, serverId, remoteId, profile.displayName, profile.avatarUrl)
|
||||
}
|
||||
|
||||
async function getShadowUser({ serverId, remoteId }) {
|
||||
return await userRepo.findByServerAndRemoteId(serverId, remoteId)
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
isLocal,
|
||||
getProfile,
|
||||
createUser,
|
||||
updateProfile,
|
||||
deleteAccount,
|
||||
getShadowUser
|
||||
}
|
||||
Reference in New Issue
Block a user