feat: major code refactor, add login and register endpoints with swagger
All checks were successful
Update changelog / changelog (push) Successful in 27s

This commit is contained in:
2025-12-29 18:26:50 +01:00
parent 3f225a1ecb
commit 41f3b0f0f2
22 changed files with 1425 additions and 128 deletions

13
src/tools/hasher.ts Normal file
View File

@@ -0,0 +1,13 @@
// Credits:
// https://mojoauth.com/hashing/sha-512-in-typescript/
import { createHash } from 'crypto';
/**
* Generates a SHA-512 hash for the given input string.
* @param input - The input string to hash.
* @returns The SHA-512 hash as a hexadecimal string.
*/
export function generateSha512(input: string): string {
const hash = createHash('sha512');
hash.update(input);
return hash.digest('hex');
}

117
src/tools/jwt.ts Normal file
View File

@@ -0,0 +1,117 @@
// Heavily based on:
// https://github.com/TomDoesTech/REST-API-Tutorial-Updated/blob/7b5f040e1acd94d267df585516b33ee7e3b75f70/src/utils/jwt.utils.ts
import * as dotenv from 'dotenv';
dotenv.config({ quiet: true });
import jwt from 'jsonwebtoken';
import { DEFAULT_TOKEN_LIFETIME } from '../schemas/authSchema';
type JwtStatus = {
valid: boolean;
expired: boolean;
decoded: string | jwt.JwtPayload | null;
};
/**
* Get the environmental string from .env.
* Supports rewriting names to UPPER_SNAKE_CASE if isGlobal is set.
*
* @param {string} key The key
* @param {boolean} [isGlobal=true] Indicates if global
* @return {(string|undefined)} The environment string.
*/
export function getEnvString(
key: string,
isGlobal: boolean = true
): string | undefined {
let keyName: string = '';
if (isGlobal) {
// Global values are DECLARED_LIKE_THIS=...
for (let i: number = 0; i < key.length; i++) {
if (key[i].toLowerCase() === key[i]) {
// If is lowercase, skip.
keyName += key[i];
} else {
// If is uppercase, convert to snake case.
keyName += `_${key[i].toLowerCase()}`;
}
}
keyName.toUpperCase();
} else {
// Non-global keys are parsed as passed
keyName = key;
}
return process.env[keyName];
}
/**
* Sign a JWT containing sub (number), role (number, 0/1), iat/exp (unix timestamp) claims.
*
* @param {Object} object The object
* @param {('accessTokenPrivateKey'|'refreshTokenPrivateKey')} keyName The key name
* @param {} options?:jwt.SignOptions|undefined The sign options undefined
* @return {string} JWT string
*/
export function signJwt(
object: Object,
keyName: 'accessTokenPrivateKey' | 'refreshTokenPrivateKey',
options?: jwt.SignOptions | undefined
): string {
// refresh tokens aren't (yet) supported
// const signingKey = Buffer.from(
// process.env[keyName]!,
// 'base64'
// ).toString('utf8');
const secret: string = getEnvString(keyName, true)!;
// Use the default expiration time of 24 hours.
if (options === undefined)
options = { expiresIn: DEFAULT_TOKEN_LIFETIME };
return jwt.sign(object, secret, {
...options, // (options && options)?
// algorithm: 'RS256', // requires a valid private key, not a secret
});
}
/**
* Verify a JWT against one of the keys.
* Returns JwtStatus, which contains fields for checking validity, expiry and decoded subject claim (id).
*
* @param {string} token The token
* @param {('accessTokenPublicKey'|'refreshTokenPublicKey')} keyName The key name
* @return {JwtStatus} JWT status.
*/
export function verifyJwt(
token: string,
keyName: 'accessTokenPublicKey' | 'refreshTokenPublicKey'
): JwtStatus {
// refresh tokens aren't (yet) supported
// const publicKey = Buffer.from(
// process.env[keyName]!,
// 'base64'
// ).toString('utf8');
const secret: string = getEnvString(keyName, true)!;
try {
const decoded: string | jwt.JwtPayload = jwt.verify(token, secret);
return {
valid: true,
expired: false,
decoded,
};
} catch (e: any) {
console.error('JWT verify error:', e);
return {
valid: false,
expired: e.message === 'jwt expired',
decoded: null,
};
}
}

View File

@@ -0,0 +1,40 @@
// Heavily based on:
// https://github.com/TomDoesTech/REST-API-Tutorial-Updated/blob/7b5f040e1acd94d267df585516b33ee7e3b75f70/src/middleware/validateResource.ts
import { Request, Response, NextFunction } from 'express';
import { ErrorDTO } from '../schemas/miscSchema';
import z from 'zod';
const validate =
(schema: z.ZodObject) =>
(req: Request, res: Response, next: NextFunction) => {
try {
schema.parse({
body: req.body,
query: req.query,
params: req.params,
});
next();
} catch (e: any) {
if (e instanceof z.ZodError) {
let errorResponse: ErrorDTO = {
status: 'error',
error: e.issues[0]?.message ?? 'Unknown error',
code: e.issues[0]?.code
};
return res.status(400)
.json(errorResponse);
} else {
console.log('Generic error triggered:', e);
let errorResponse: ErrorDTO = {
status: 'error',
error: 'Unknown error',
code: 'generic-error'
};
return res.status(400)
.json(errorResponse);
}
}
};
export default validate;