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:
2026-09-02 01:55:58 +02:00
parent 7a87a98c82
commit 22542243f6
20 changed files with 686 additions and 28 deletions
+8
View File
@@ -7,3 +7,11 @@ DATABASE_HOST="host"
DATABASE_USER="changeme" DATABASE_USER="changeme"
DATABASE_PASSWORD="password" DATABASE_PASSWORD="password"
DATABASE_NAME="tcjourney" 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!!!
+6
View File
@@ -132,3 +132,9 @@ dist
#logs #logs
logs logs
#secrets
secrets/
#tests
test.js
+17
View File
@@ -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()
+2 -1
View File
@@ -1,8 +1,9 @@
CREATE TABLE IF NOT EXISTS `users` ( CREATE TABLE IF NOT EXISTS `users` (
`id` UUID PRIMARY KEY DEFAULT UUID(), `id` UUID PRIMARY KEY DEFAULT UUID(),
`serverId` UUID NULL, `serverId` UUID NULL,
`identifier` VARCHAR(255) NOT NULL UNIQUE,
`remoteId` VARCHAR(255) NULL, `remoteId` VARCHAR(255) NULL,
`displayName` VARCHAR(255) NOT NULL, `displayName` VARCHAR(512) NOT NULL,
`avatarUrl` TEXT NULL, `avatarUrl` TEXT NULL,
`createdAt` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, `createdAt` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+7
View File
@@ -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
)
+53
View File
@@ -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,
}
+61
View File
@@ -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
}
+131 -1
View File
@@ -9,12 +9,14 @@
"version": "0.0.1-alpha", "version": "0.0.1-alpha",
"license": "AGPL-3.0-only", "license": "AGPL-3.0-only",
"dependencies": { "dependencies": {
"bcryptjs": "^3.0.3",
"colors": "^1.4.0", "colors": "^1.4.0",
"cors": "^2.8.6", "cors": "^2.8.6",
"dotenv": "^17.4.2", "dotenv": "^17.4.2",
"express": "^5.2.1", "express": "^5.2.1",
"helmet": "^8.3.0", "helmet": "^8.3.0",
"hpp": "^0.2.3", "hpp": "^0.2.3",
"jsonwebtoken": "^9.0.3",
"mariadb": "^3.5.3", "mariadb": "^3.5.3",
"path-to-regexp": "^8.4.2", "path-to-regexp": "^8.4.2",
"zod": "^4.4.3" "zod": "^4.4.3"
@@ -333,6 +335,15 @@
"node": "18 || 20 || >=22" "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": { "node_modules/binary-extensions": {
"version": "2.3.0", "version": "2.3.0",
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
@@ -396,6 +407,12 @@
"node": ">=8" "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": { "node_modules/bytes": {
"version": "3.1.2", "version": "3.1.2",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
@@ -608,6 +625,15 @@
"node": ">= 0.4" "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": { "node_modules/ee-first": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
@@ -1375,6 +1401,49 @@
"dev": true, "dev": true,
"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/keyv": { "node_modules/keyv": {
"version": "4.5.4", "version": "4.5.4",
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
@@ -1421,6 +1490,48 @@
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
"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/lru-cache": { "node_modules/lru-cache": {
"version": "11.5.2", "version": "11.5.2",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz",
@@ -1830,6 +1941,26 @@
"node": ">= 18" "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": { "node_modules/safer-buffer": {
"version": "2.1.2", "version": "2.1.2",
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
@@ -1840,7 +1971,6 @@
"version": "7.7.3", "version": "7.7.3",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
"integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
"dev": true,
"license": "ISC", "license": "ISC",
"bin": { "bin": {
"semver": "bin/semver.js" "semver": "bin/semver.js"
+3 -1
View File
@@ -13,7 +13,7 @@
"email": "gilleslazure04@gmail.com" "email": "gilleslazure04@gmail.com"
}, },
"type": "commonjs", "type": "commonjs",
"main": "server.js", "main": "bootstrap.js",
"scripts": { "scripts": {
"start:dev": "nodemon .", "start:dev": "nodemon .",
"start": "node .", "start": "node .",
@@ -23,12 +23,14 @@
"homepage": "https://gitea.azures.fr/azures04/Base-REST-API", "homepage": "https://gitea.azures.fr/azures04/Base-REST-API",
"readme": "https://gitea.azures.fr/azures04/Base-REST-API/src/branch/main/README.md", "readme": "https://gitea.azures.fr/azures04/Base-REST-API/src/branch/main/README.md",
"dependencies": { "dependencies": {
"bcryptjs": "^3.0.3",
"colors": "^1.4.0", "colors": "^1.4.0",
"cors": "^2.8.6", "cors": "^2.8.6",
"dotenv": "^17.4.2", "dotenv": "^17.4.2",
"express": "^5.2.1", "express": "^5.2.1",
"helmet": "^8.3.0", "helmet": "^8.3.0",
"hpp": "^0.2.3", "hpp": "^0.2.3",
"jsonwebtoken": "^9.0.3",
"mariadb": "^3.5.3", "mariadb": "^3.5.3",
"path-to-regexp": "^8.4.2", "path-to-regexp": "^8.4.2",
"zod": "^4.4.3" "zod": "^4.4.3"
+7 -7
View File
@@ -11,20 +11,20 @@ async function findByUserId(userId) {
} }
} }
async function create(userId, hashedPassword) { async function create(userId, passwordHash) {
try { try {
const sql = "INSERT INTO credentials (userId, hashedPassword) VALUES (?, ?)" const sql = "INSERT INTO credentials (userId, passwordHash) VALUES (?, ?)"
const rows = await pool.query(sql, [userId, hashedPassword]) const rows = await pool.query(sql, [userId, passwordHash])
return rows[0] || null return rows || null
} catch (error) { } catch (error) {
throw new DefaultError(500, "Internal Server Error", error) throw new DefaultError(500, "Internal Server Error", error)
} }
} }
async function updatePassword(userId, hashedPassword) { async function updatePassword(userId, passwordHash) {
try { try {
const sql = "UPDATE credentials SET hashedPassword = ? WHERE userId = ?" const sql = "UPDATE credentials SET passwordHash = ? WHERE userId = ?"
const rows = await pool.query(sql, [hashedPassword, userId]) const rows = await pool.query(sql, [passwordHash, userId])
return rows.affectedRows > 0 return rows.affectedRows > 0
} catch (error) { } catch (error) {
throw new DefaultError(500, "Internal Server Error", error) throw new DefaultError(500, "Internal Server Error", error)
+11
View File
@@ -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) { async function setEnabled(id, isEnabled) {
try { try {
const sql = "UPDATE providers SET isEnabled = ? WHERE id = ?" const sql = "UPDATE providers SET isEnabled = ? WHERE id = ?"
@@ -97,6 +107,7 @@ async function remove(id) {
module.exports = { module.exports = {
remove, remove,
create, create,
listAll,
findById, findById,
findByName, findByName,
setEnabled, setEnabled,
+71
View File
@@ -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,
}
+12 -1
View File
@@ -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) { async function remove(id) {
try { try {
const sql = "DELETE FROM servers WHERE id = ?" const sql = "DELETE FROM servers WHERE id = ?"
@@ -74,5 +84,6 @@ module.exports = {
remove, remove,
findById, findById,
findByUrl, findByUrl,
updateServer updateServer,
createAndAssignIdManually
} }
+18 -4
View File
@@ -3,7 +3,7 @@ const { DefaultError } = require("../errors/errors")
async function findById(id) { async function findById(id) {
try { try {
const sql = "SELECT* FROM servers WHERE id = ?" const sql = "SELECT * FROM users WHERE id = ?"
const rows = await pool.query(sql, [id]) const rows = await pool.query(sql, [id])
return rows[0] || null return rows[0] || null
} catch (error) { } catch (error) {
@@ -11,16 +11,29 @@ async function findById(id) {
} }
} }
async function create(serverId, remoteId = null, displayName, avatarUrl) { async function findByIdentifier(identifier) {
try { try {
const sql = "INSERT INTO users (serverId, remoteId, displayName, avatarUrl) VALUES (?, ?, ?, ?)" const sql = "SELECT * FROM users WHERE identifier = ?"
const rows = await pool.query(sql, [serverId, remoteId, displayName, avatarUrl]) const rows = await pool.query(sql, [identifier])
return rows[0] || null return rows[0] || null
} catch (error) { } catch (error) {
throw new DefaultError(500, "Internal Server Error", 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) { async function remove(id) {
try { try {
const sql = "DELETE FROM users WHERE id = ?" const sql = "DELETE FROM users WHERE id = ?"
@@ -86,5 +99,6 @@ module.exports = {
isLocal, isLocal,
findById, findById,
updateProfile, updateProfile,
findByIdentifier,
findByServerAndRemoteId findByServerAndRemoteId
} }
-3
View File
@@ -25,9 +25,6 @@ app.use(express.urlencoded({ extended: true }))
app.set("trust proxy", true) app.set("trust proxy", true)
database.runDDL()
.catch(err => logger.error(err))
logger.log("Initializing routes", ["WEB", "yellow"]) logger.log("Initializing routes", ["WEB", "yellow"])
for (const schemaFile of schemas) { for (const schemaFile of schemas) {
+83
View File
@@ -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
}
+49
View File
@@ -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,
}
+103
View File
@@ -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,
}
+34
View File
@@ -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
}