3ea9e630ed
Prep PR for the admin auth module (docs/plan-admin-auth.md step 1).
better-auth 1.7 ships ESM only, so the API moves off CommonJS:
- "type": "module", module nodenext, target ES2024, .js suffixes on all
relative imports, require('mariadb'|'cors') replaced by imports, and
export= packages (winston, app-root-path, bcrypt) consumed via default
imports. The logger now uses appRoot.path explicitly.
- TypeScript 5.9, @types/node 26, tslint removed. Node 26 pinned via
engines and .nvmrc (Plesk runs 26).
- Jest 28 + ts-jest replaced by vitest 5. Eight test files depend on
hoisted module mocks with static imports and resetModules + require,
which Jest's ESM mode does not support; vitest keeps them nearly
verbatim. Coverage via @vitest/coverage-v8 (lcov), Sonar generic report
via vitest-sonar-reporter, so sonar-project.properties is unchanged.
vitest.config.ts sets FEEDBACK_IP_SALT so the suite passes without a
local .env.
- dotenv 8 -> 16 and axios 0.24 -> 1.x: their old typings are not
resolvable under nodenext.
- autoCommit: false dropped from the pool configs; it is not a mariadb
connector option and was silently ignored.
tsc clean, 96/96 tests green, compiled app boots and serves /, /docs and
CORS under Node ESM.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
672 lines
26 KiB
TypeScript
672 lines
26 KiB
TypeScript
/**
|
|
* Required External Modules and Interfaces
|
|
*/
|
|
|
|
import express, {Request, Response} from 'express';
|
|
import * as UserService from './users.service.js';
|
|
import {Session} from './session.interface.js';
|
|
import {User} from './user.interface.js';
|
|
import {Guid} from 'guid-typescript';
|
|
import logger from '../../../middleware/logger.js';
|
|
|
|
/**
|
|
* Router Definition
|
|
*/
|
|
|
|
export const usersRouter = express.Router();
|
|
|
|
|
|
/**
|
|
* Controller Definitions
|
|
*/
|
|
|
|
/**
|
|
* @swagger
|
|
* /calendar/users/register:
|
|
* post:
|
|
* summary: Register a new user
|
|
* description: Creates a new user account with the provided email, password, and full name. Only accepts official Nachklang email addresses.
|
|
* tags:
|
|
* - calendar
|
|
* requestBody:
|
|
* required: true
|
|
* content:
|
|
* application/json:
|
|
* schema:
|
|
* type: object
|
|
* required:
|
|
* - email
|
|
* - password
|
|
* - fullName
|
|
* properties:
|
|
* email:
|
|
* type: string
|
|
* format: email
|
|
* example: john.doe@nachklang.art
|
|
* description: Must be an official Nachklang email address
|
|
* password:
|
|
* type: string
|
|
* format: password
|
|
* example: securePassword123
|
|
* fullName:
|
|
* type: string
|
|
* example: John Doe
|
|
* responses:
|
|
* 201:
|
|
* description: User registered successfully
|
|
* content:
|
|
* application/json:
|
|
* schema:
|
|
* type: object
|
|
* properties:
|
|
* sessionId:
|
|
* type: integer
|
|
* example: 123
|
|
* sessionKey:
|
|
* type: string
|
|
* example: abc123def456
|
|
* 400:
|
|
* description: Bad request - missing or invalid parameters
|
|
* content:
|
|
* application/json:
|
|
* schema:
|
|
* type: object
|
|
* properties:
|
|
* message:
|
|
* type: string
|
|
* example: Missing parameters
|
|
* 500:
|
|
* description: Server error
|
|
* content:
|
|
* application/json:
|
|
* schema:
|
|
* type: object
|
|
* properties:
|
|
* status:
|
|
* type: string
|
|
* example: PROCESSING_ERROR
|
|
* message:
|
|
* type: string
|
|
* example: Internal Server Error. Try again later.
|
|
* reference:
|
|
* type: string
|
|
* example: 6ec1361c-4175-4e81-b2ef-a0792a9a1dc3
|
|
*/
|
|
// POST users/register
|
|
usersRouter.post('/register', async (req: Request, res: Response) => {
|
|
try {
|
|
const password: string = req.body.password;
|
|
const email: string = req.body.email;
|
|
const fullName: string = req.body.fullName;
|
|
const ip: string = req.socket.remoteAddress ?? '';
|
|
|
|
if (!password || !email || !fullName) {
|
|
// Missing
|
|
res.status(400).send(JSON.stringify({message: 'Missing parameters'}));
|
|
return;
|
|
}
|
|
|
|
const emailRegex = /^[a-zA-Z0-9\_\-\.]+@nachklang\.art$/;
|
|
|
|
if(!emailRegex.test(email)) {
|
|
res.status(400).send(JSON.stringify({message: 'Must use an official Nachklang email address'}));
|
|
return;
|
|
}
|
|
|
|
// Create the user and a session
|
|
const session: Session = await UserService.createUser(email, password, fullName, ip);
|
|
|
|
// Send the session details back to the user
|
|
res.status(201).send({
|
|
sessionId: session.sessionId,
|
|
sessionKey: session.sessionKey
|
|
});
|
|
} catch (e: any) {
|
|
let errorGuid = Guid.create().toString();
|
|
logger.error('Error handling a request: ' + e.message, {reference: errorGuid});
|
|
res.status(500).send({
|
|
'status': 'PROCESSING_ERROR',
|
|
'message': 'Internal Server Error. Try again later.',
|
|
'reference': errorGuid
|
|
});
|
|
}
|
|
});
|
|
|
|
/**
|
|
* @swagger
|
|
* /calendar/users/activate:
|
|
* get:
|
|
* summary: Activate a user account
|
|
* description: Activates a user account using the provided user ID and activation token.
|
|
* tags:
|
|
* - calendar
|
|
* parameters:
|
|
* - in: query
|
|
* name: id
|
|
* required: true
|
|
* schema:
|
|
* type: integer
|
|
* description: The ID of the user to activate
|
|
* - in: query
|
|
* name: token
|
|
* required: true
|
|
* schema:
|
|
* type: string
|
|
* description: The activation token sent to the user's email
|
|
* responses:
|
|
* 200:
|
|
* description: User activated successfully
|
|
* content:
|
|
* application/json:
|
|
* schema:
|
|
* type: object
|
|
* properties:
|
|
* status:
|
|
* type: string
|
|
* example: OK
|
|
* message:
|
|
* type: string
|
|
* example: User activated
|
|
* 400:
|
|
* description: Bad request - missing parameters or activation failed
|
|
* content:
|
|
* application/json:
|
|
* schema:
|
|
* type: object
|
|
* properties:
|
|
* status:
|
|
* type: string
|
|
* example: PROCESSING_ERROR
|
|
* message:
|
|
* type: string
|
|
* example: Error activating user. Please contact your administrator.
|
|
* 500:
|
|
* description: Server error
|
|
* content:
|
|
* application/json:
|
|
* schema:
|
|
* type: object
|
|
* properties:
|
|
* status:
|
|
* type: string
|
|
* example: PROCESSING_ERROR
|
|
* message:
|
|
* type: string
|
|
* example: Internal Server Error. Try again later.
|
|
* reference:
|
|
* type: string
|
|
* example: 6ec1361c-4175-4e81-b2ef-a0792a9a1dc3
|
|
*/
|
|
// GET /users/activate
|
|
usersRouter.get('/activate', async (req: Request, res: Response) => {
|
|
try {
|
|
const userId: number = parseInt(req.query.id as string ?? '-1', 10);
|
|
const token: string = req.query.token as string ?? '';
|
|
|
|
if (!userId || !token) {
|
|
// Missing
|
|
res.status(400).send(JSON.stringify({message: 'Missing parameters'}));
|
|
return;
|
|
}
|
|
|
|
// Create the user and a session
|
|
const success: boolean = await UserService.activateUser(userId, token);
|
|
|
|
// Send the session details back to the user
|
|
if(success) {
|
|
res.status(200).send({
|
|
'status': 'OK',
|
|
'message': 'User activated'
|
|
});
|
|
return;
|
|
}
|
|
res.status(400).send({'status': 'PROCESSING_ERROR','message': 'Error activating user. Please contact your administrator.'});
|
|
} catch (e: any) {
|
|
let errorGuid = Guid.create().toString();
|
|
logger.error('Error handling a request: ' + e.message, {reference: errorGuid});
|
|
res.status(500).send({
|
|
'status': 'PROCESSING_ERROR',
|
|
'message': 'Internal Server Error. Try again later.',
|
|
'reference': errorGuid
|
|
});
|
|
}
|
|
});
|
|
|
|
/**
|
|
* @swagger
|
|
* /calendar/users/login:
|
|
* post:
|
|
* summary: Login a user
|
|
* description: Authenticates a user with the provided email and password and returns a session.
|
|
* tags:
|
|
* - calendar
|
|
* requestBody:
|
|
* required: true
|
|
* content:
|
|
* application/json:
|
|
* schema:
|
|
* type: object
|
|
* required:
|
|
* - email
|
|
* - password
|
|
* properties:
|
|
* email:
|
|
* type: string
|
|
* format: email
|
|
* example: john.doe@nachklang.art
|
|
* password:
|
|
* type: string
|
|
* format: password
|
|
* example: securePassword123
|
|
* responses:
|
|
* 200:
|
|
* description: Login successful
|
|
* content:
|
|
* application/json:
|
|
* schema:
|
|
* type: object
|
|
* properties:
|
|
* sessionId:
|
|
* type: integer
|
|
* example: 123
|
|
* sessionKey:
|
|
* type: string
|
|
* example: abc123def456
|
|
* 400:
|
|
* description: Bad request - missing parameters
|
|
* content:
|
|
* application/json:
|
|
* schema:
|
|
* type: object
|
|
* properties:
|
|
* message:
|
|
* type: string
|
|
* example: Missing parameters
|
|
* 401:
|
|
* description: Unauthorized - invalid credentials
|
|
* content:
|
|
* application/json:
|
|
* schema:
|
|
* type: object
|
|
* properties:
|
|
* message:
|
|
* type: string
|
|
* example: Wrong username and / or password
|
|
* sessionId:
|
|
* type: integer
|
|
* example: -1
|
|
* sessionKey:
|
|
* type: string
|
|
* example: ""
|
|
* 500:
|
|
* description: Server error
|
|
* content:
|
|
* application/json:
|
|
* schema:
|
|
* type: object
|
|
* properties:
|
|
* status:
|
|
* type: string
|
|
* example: PROCESSING_ERROR
|
|
* message:
|
|
* type: string
|
|
* example: Internal Server Error. Try again later.
|
|
* reference:
|
|
* type: string
|
|
* example: 6ec1361c-4175-4e81-b2ef-a0792a9a1dc3
|
|
*/
|
|
// POST users/login
|
|
usersRouter.post('/login', async (req: Request, res: Response) => {
|
|
try {
|
|
const password: string = req.body.password;
|
|
const email: string = req.body.email;
|
|
const ip: string = req.socket.remoteAddress ?? '';
|
|
|
|
if (!password || !email) {
|
|
// Missing
|
|
res.status(400).send(JSON.stringify({message: 'Missing parameters'}));
|
|
return;
|
|
}
|
|
|
|
// Create a session
|
|
const session: Session | null = await UserService.login(email, password, ip);
|
|
|
|
if (!session || !session.sessionId) {
|
|
// Error logging in, probably wrong username / password
|
|
res.status(401).send(JSON.stringify({message: 'Wrong username and / or password', sessionId: -1, sessionKey: ''}));
|
|
return;
|
|
}
|
|
|
|
// Send the session details back to the user
|
|
res.status(200).send({
|
|
sessionId: session.sessionId,
|
|
sessionKey: session.sessionKey
|
|
});
|
|
} catch (e: any) {
|
|
let errorGuid = Guid.create().toString();
|
|
logger.error('Error handling a request: ' + e.message, {reference: errorGuid});
|
|
res.status(500).send({
|
|
'status': 'PROCESSING_ERROR',
|
|
'message': 'Internal Server Error. Try again later.',
|
|
'reference': errorGuid
|
|
});
|
|
}
|
|
});
|
|
|
|
/**
|
|
* @swagger
|
|
* /calendar/users/checkSessionValid:
|
|
* post:
|
|
* summary: Check if a session is valid
|
|
* description: Checks if the provided session is valid and returns the user information if it is.
|
|
* tags:
|
|
* - calendar
|
|
* requestBody:
|
|
* required: true
|
|
* content:
|
|
* application/json:
|
|
* schema:
|
|
* type: object
|
|
* required:
|
|
* - sessionId
|
|
* - sessionKey
|
|
* properties:
|
|
* sessionId:
|
|
* type: integer
|
|
* example: 123
|
|
* sessionKey:
|
|
* type: string
|
|
* example: abc123def456
|
|
* responses:
|
|
* 200:
|
|
* description: Session is valid
|
|
* content:
|
|
* application/json:
|
|
* schema:
|
|
* $ref: '#/components/schemas/User'
|
|
* 401:
|
|
* description: Unauthorized - invalid session
|
|
* content:
|
|
* application/json:
|
|
* schema:
|
|
* type: object
|
|
* properties:
|
|
* messages:
|
|
* type: array
|
|
* items:
|
|
* type: string
|
|
* example: ["Invalid session"]
|
|
* 500:
|
|
* description: Server error
|
|
* content:
|
|
* application/json:
|
|
* schema:
|
|
* type: object
|
|
* properties:
|
|
* status:
|
|
* type: string
|
|
* example: PROCESSING_ERROR
|
|
* message:
|
|
* type: string
|
|
* example: Internal Server Error. Try again later.
|
|
* reference:
|
|
* type: string
|
|
* example: 6ec1361c-4175-4e81-b2ef-a0792a9a1dc3
|
|
*/
|
|
// POST users/checkSessionValid
|
|
usersRouter.post('/checkSessionValid', async (req: Request, res: Response) => {
|
|
try {
|
|
const ip: string = req.socket.remoteAddress ?? '';
|
|
const session_id = req.body.sessionId;
|
|
const session_key = req.body.sessionKey;
|
|
|
|
if (!session_id || !session_key) {
|
|
// Error logging in, probably wrong username / password
|
|
res.status(401).send(JSON.stringify({messages: ['No session detected']}));
|
|
return;
|
|
}
|
|
|
|
const user: User | null = await UserService.checkSession(session_id, session_key, ip);
|
|
|
|
if (!user || !user.userId) {
|
|
// Error logging in, probably wrong username / password
|
|
res.status(401).send(JSON.stringify({messages: ['Invalid session']}));
|
|
return;
|
|
}
|
|
|
|
res.status(200).send(user);
|
|
} catch (e: any) {
|
|
let errorGuid = Guid.create().toString();
|
|
logger.error('Error handling a request: ' + e.message, {reference: errorGuid});
|
|
res.status(500).send({
|
|
'status': 'PROCESSING_ERROR',
|
|
'message': 'Internal Server Error. Try again later.',
|
|
'reference': errorGuid
|
|
});
|
|
}
|
|
});
|
|
|
|
/**
|
|
* @swagger
|
|
* /calendar/users/initiatePasswordReset:
|
|
* post:
|
|
* summary: Initiates a password reset
|
|
* description: Checks if the user exists and if so, initiates a password reset by sending an email to the user.
|
|
* tags:
|
|
* - calendar
|
|
* responses:
|
|
* 200:
|
|
* description: Success
|
|
* content:
|
|
* application/json:
|
|
* schema:
|
|
* type: object
|
|
* properties:
|
|
* messages:
|
|
* type: array
|
|
* items:
|
|
* type: string
|
|
* example: Success
|
|
* description: A list of status messages
|
|
* 400:
|
|
* description: Problem with the request. Please consider the returned detailed error.
|
|
* content:
|
|
* application/json:
|
|
* schema:
|
|
* type: object
|
|
* properties:
|
|
* messages:
|
|
* type: array
|
|
* items:
|
|
* type: string
|
|
* example: Missing parameters
|
|
* description: A list of error messages
|
|
* 401:
|
|
* description: Problem with authorizing the user. Please check the provided credentials.
|
|
* content:
|
|
* application/json:
|
|
* schema:
|
|
* type: object
|
|
* properties:
|
|
* messages:
|
|
* type: array
|
|
* items:
|
|
* type: string
|
|
* example: Invalid session
|
|
* description: A list of error messages
|
|
* 500:
|
|
* description: A server error occurred. Please try again. If this issue persists, contact the admin.
|
|
* content:
|
|
* application/json:
|
|
* schema:
|
|
* type: object
|
|
* properties:
|
|
* status:
|
|
* type: string
|
|
* description: The response status
|
|
* example: PROCESSING_ERROR
|
|
* message:
|
|
* type: string
|
|
* description: The detailed error message
|
|
* example: Internal Server Error. Try again later.
|
|
* reference:
|
|
* type: string
|
|
* description: An error reference for getting support concerning this error.
|
|
* example: 6ec1361c-4175-4e81-b2ef-a0792a9a1dc3
|
|
* requestBody:
|
|
* required: true
|
|
* content:
|
|
* application/json:
|
|
* schema:
|
|
* type: object
|
|
* properties:
|
|
* email:
|
|
* type: string
|
|
* example: patrick@nachklang.art
|
|
*/
|
|
usersRouter.post('/initiatePasswordReset', async(req: Request, res: Response) => {
|
|
try {
|
|
const username = req.body.username;
|
|
|
|
if (!username) {
|
|
// Error logging in, probably wrong username / password
|
|
res.status(400).send(JSON.stringify({messages: ['No username given']}));
|
|
return;
|
|
}
|
|
|
|
const success: boolean = await UserService.initiatePasswordReset(username);
|
|
|
|
if (!success) {
|
|
// Error logging in, probably wrong username / password
|
|
res.status(401).send(JSON.stringify({messages: ['Error']}));
|
|
return;
|
|
}
|
|
|
|
res.status(200).send(JSON.stringify({messages: ['Success']}));
|
|
} catch (e: any) {
|
|
let errorGuid = Guid.create().toString();
|
|
logger.error('Error handling a request: ' + e.message, {reference: errorGuid});
|
|
res.status(500).send({
|
|
'status': 'PROCESSING_ERROR',
|
|
'message': 'Internal Server Error. Try again later.',
|
|
'reference': errorGuid
|
|
});
|
|
}
|
|
});
|
|
|
|
/**
|
|
* @swagger
|
|
* /calendar/users/finalizePasswordReset:
|
|
* post:
|
|
* summary: Finalizes the password reset
|
|
* description: Checks if the given token is valid and if so, finalizes the password reset by setting the new password.
|
|
* tags:
|
|
* - calendar
|
|
* responses:
|
|
* 200:
|
|
* description: Success
|
|
* content:
|
|
* application/json:
|
|
* schema:
|
|
* type: object
|
|
* properties:
|
|
* messages:
|
|
* type: array
|
|
* items:
|
|
* type: string
|
|
* example: Success
|
|
* description: A list of status messages
|
|
* 400:
|
|
* description: Problem with the request. Please consider the returned detailed error.
|
|
* content:
|
|
* application/json:
|
|
* schema:
|
|
* type: object
|
|
* properties:
|
|
* messages:
|
|
* type: array
|
|
* items:
|
|
* type: string
|
|
* example: Missing parameters
|
|
* description: A list of error messages
|
|
* 401:
|
|
* description: Problem with authorizing the user. Please check the provided credentials.
|
|
* content:
|
|
* application/json:
|
|
* schema:
|
|
* type: object
|
|
* properties:
|
|
* messages:
|
|
* type: array
|
|
* items:
|
|
* type: string
|
|
* example: Invalid session
|
|
* description: A list of error messages
|
|
* 500:
|
|
* description: A server error occurred. Please try again. If this issue persists, contact the admin.
|
|
* content:
|
|
* application/json:
|
|
* schema:
|
|
* type: object
|
|
* properties:
|
|
* status:
|
|
* type: string
|
|
* description: The response status
|
|
* example: PROCESSING_ERROR
|
|
* message:
|
|
* type: string
|
|
* description: The detailed error message
|
|
* example: Internal Server Error. Try again later.
|
|
* reference:
|
|
* type: string
|
|
* description: An error reference for getting support concerning this error.
|
|
* example: 6ec1361c-4175-4e81-b2ef-a0792a9a1dc3
|
|
* requestBody:
|
|
* required: true
|
|
* content:
|
|
* application/json:
|
|
* schema:
|
|
* type: object
|
|
* properties:
|
|
* email:
|
|
* type: string
|
|
* example: patrick@nachklang.art
|
|
* token:
|
|
* type: string
|
|
* example: 3ccd147f-720b-4e29-a8b7-46b63de31555
|
|
* password:
|
|
* type: string
|
|
* example: ExtremelyBadPassword
|
|
*/
|
|
usersRouter.post('/finalizePasswordReset', async(req: Request, res: Response) => {
|
|
try {
|
|
const username = req.body.username;
|
|
const token = req.body.token;
|
|
const newPassword = req.body.password;
|
|
|
|
if (!username) {
|
|
// Error logging in, probably wrong username / password
|
|
res.status(400).send(JSON.stringify({messages: ['No username, token or password given']}));
|
|
return;
|
|
}
|
|
|
|
const success: boolean = await UserService.finalizePasswordReset(username, token, newPassword);
|
|
|
|
if (!success) {
|
|
// Error logging in, probably wrong username / password
|
|
res.status(401).send(JSON.stringify({messages: ['Error']}));
|
|
return;
|
|
}
|
|
|
|
res.status(200).send(JSON.stringify({messages: ['Success']}));
|
|
} catch (e: any) {
|
|
let errorGuid = Guid.create().toString();
|
|
logger.error('Error handling a request: ' + e.message, {reference: errorGuid});
|
|
res.status(500).send({
|
|
'status': 'PROCESSING_ERROR',
|
|
'message': 'Internal Server Error. Try again later.',
|
|
'reference': errorGuid
|
|
});
|
|
}
|
|
});
|