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
+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
}