add me point to load user data of current user

This commit is contained in:
Anika Raemer 2025-09-27 07:47:26 +02:00
parent fac606cf97
commit e5b5d7e67d
12 changed files with 215 additions and 67 deletions

View file

@ -7,11 +7,7 @@ meta {
post {
url: http://localhost:4000/user
body: json
auth: inherit
}
headers {
Authorization: bearcer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjU2NGE5NjY0LTI2ZWYtNGMxMS1hNjIyLWU4MDI2MzczYmRkZCIsImlhdCI6MTc1ODc0MTM5MywiZXhwIjoxNzU4ODI3NzkzfQ.q33R9FfhGUIn92PTIIAmKmUnGxcLlv6om7KwiDD61Rc
auth: bearer
}
body:json {

View file

@ -0,0 +1,19 @@
meta {
name: me
type: http
seq: 3
}
get {
url: http://localhost:4000/user/me
body: none
auth: bearer
}
auth:bearer {
token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjU2NGE5NjY0LTI2ZWYtNGMxMS1hNjIyLWU4MDI2MzczYmRkZCIsImlhdCI6MTc1ODk1MTI4NSwiZXhwIjoxNzU5MDM3Njg1fQ.FeuMlAurJFAhBPyvJFx3MX64WHB0_sa5pldkbQylUOw
}
settings {
encodeUrl: true
}

View file

@ -5,12 +5,21 @@ import { UserDtoEntityMapper } from "../mappers/UserDtoEntityMapper.js";
import { LoginResponseDto } from "../dtos/LoginResponseDto.js";
import { LoginRequestDto } from "../dtos/LoginRequestDto.js";
/**
* Controller responsible for authentication, e.g., login or issueing a token with extended
* lifetime
*/
export class AuthController {
constructor(
private userRepository: UserRepository,
private mapper: UserDtoEntityMapper
) {}
/**
* Login: Check user and password and generate token
* @param loginRequest LoginRequestDto containing userName and password for login
* @returns LoginResponse containing token and user data for the user who just logged in
*/
async login(loginRequest : LoginRequestDto): Promise<LoginResponseDto> {
const userName :string|undefined = loginRequest.userName;
const password :string|undefined = loginRequest.password;
@ -25,6 +34,11 @@ export class AuthController {
if(!user){
throw new UnauthorizedError("Invalid username or password");
}
// ensure user has an id - required to generate token
const userId = user.id;
if(user.id == undefined){
throw new UnauthorizedError("Invalid username or password");
}
// Compare password
const passwordMatches = encrypt.comparepassword(password, user.password);
if (!passwordMatches) {
@ -33,7 +47,7 @@ export class AuthController {
// Create JWT
const tokenInfo = encrypt.generateToken({
id: user.id,
id: userId!, // ! to indicate that we've definitely checked for userId being defined
});
const responseDto = new LoginResponseDto();

View file

@ -1,18 +1,26 @@
import { ValidationError, ConflictError } from "../errors/httpErrors.js";
import { ValidationError, ConflictError, NotFoundError } from "../errors/httpErrors.js";
import { CreateUserRequestDto } from "../dtos/CreateUserRequestDto.js";
import { UserDto } from "../dtos/UserDto.js";
import { encrypt } from "../utils/encryptionUtils.js";
import { UserRepository } from "../repositories/UserRepository.js";
import { UserDtoEntityMapper } from "../mappers/UserDtoEntityMapper.js";
import { UUID } from "crypto";
/**
* Controls all user specific actions
*/
export class UserController {
constructor(
private userRepository: UserRepository,
private mapper: UserDtoEntityMapper
) {}
/**
* Create a new user
* @param dto CreateUserRequestDto containing data for the user to add
* @returns UserDto Data of the user as stored in the database
*/
async createUser(dto: CreateUserRequestDto): Promise<UserDto> {
// @todo make authorized! Create initial user!
// check mandatory fields
if(!dto.userData){
throw new ValidationError("User data is required")
@ -43,4 +51,21 @@ export class UserController {
return this.mapper.toDto(savedUser);
}
/**
* Load data of a specific user
* @param userId Id of user to load
* @returns UserDto containing the user's data
*/
async getUserById(userId: UUID|string|undefined): Promise<UserDto> {
if(!userId){
throw new ValidationError("userId is required");
}
const userEntity = await this.userRepository.findById(userId);
if(!userEntity){
throw new NotFoundError("user with id" + userId + "not found!")
}
return this.mapper.toDto(userEntity);
}
}

9
src/dtos/AuthPayload.ts Normal file
View file

@ -0,0 +1,9 @@
import { JwtPayload } from "jsonwebtoken";
/**
* Shape of the payload stored inside JWT tokens.
* We extend JwtPayload so "iat" / "exp" etc. remain available.
*/
export interface AuthPayload extends JwtPayload {
id: string;
}

View file

@ -3,13 +3,11 @@ import { UserController } from "../controllers/UserController.js";
import { CreateUserRequestDto } from "../dtos/CreateUserRequestDto.js";
import { UserRepository } from "../repositories/UserRepository.js";
import { UserDtoEntityMapper } from "../mappers/UserDtoEntityMapper.js";
import {
ValidationError,
ConflictError,
NotFoundError,
InternalServerError,
} from "../errors/httpErrors.js";
import { asyncHandler } from "../utils/asyncHandler.js";
/**
* Handles all user related routes
*/
const router = Router();
// Inject repo + mapper here
@ -17,22 +15,31 @@ const userRepository = new UserRepository();
const userMapper = new UserDtoEntityMapper();
const userController = new UserController(userRepository, userMapper);
router.post("/", async (req, res) => {
try {
/**
* Create a new user
*/
router.post(
"/",
asyncHandler(async (req, res) => {
const requestDto: CreateUserRequestDto = req.body;
const responseDto = await userController.createUser(requestDto);
res.status(201).json(responseDto);
} catch (err: any) {
if (err instanceof ValidationError ||
err instanceof ConflictError ||
err instanceof NotFoundError) {
res.status(err.statusCode).json({ error: err.message });
} else {
console.error("Unexpected error:", err);
const internalError = new InternalServerError("Some unexpected error occurred!");
res.status(internalError.statusCode).json({ error: internalError.message });
})
);
/**
* Get user data for current user
*/
router.get("/me",
asyncHandler(async (req, res) => {
const id = req.currentUser?.id;
if(id){
// it breaks here because id is no longer a uuid
const responseDto = await userController.getUserById(id);
res.status(201).json(responseDto);
}
}
});
})
);
export default router;

View file

@ -1,39 +1,46 @@
export class ValidationError extends Error {
statusCode = 400;
constructor(message: string) {
/**
* Base class for all HTTP-related errors.
* Extends the built-in Error with a status code and message
* so that they can be handled in a consistent way.
*/
export class HttpError extends Error {
public statusCode: number;
constructor(message: string, statusCode: number) {
super(message);
this.name = "ValidationError";
this.statusCode = statusCode;
// Restore prototype chain (important when targeting ES5/ES2015+)
Object.setPrototypeOf(this, new.target.prototype);
}
}
export class NotFoundError extends Error {
statusCode = 404;
export class ValidationError extends HttpError {
constructor(message: string) {
super(message);
this.name = "NotFoundError";
super(message, 400); // Bad Request
}
}
export class ConflictError extends Error {
statusCode = 409;
constructor(message: string) {
super(message);
this.name = "ConflictError";
export class UnauthorizedError extends HttpError {
constructor(message: string = "Unauthorized") {
super(message, 401);
}
}
export class InternalServerError extends Error {
statusCode = 500;
constructor(message: string) {
super(message);
this.name = "InternalServerError";
export class NotFoundError extends HttpError {
constructor(message: string = "Resource not found") {
super(message, 404);
}
}
export class UnauthorizedError extends Error {
statusCode = 409;
constructor(message: string) {
super(message);
this.name = "UnauthorizedError";
export class ConflictError extends HttpError {
constructor(message: string = "Conflict") {
super(message, 409);
}
}
export class InternalServerError extends HttpError {
constructor(message: string = "Internal Server Error") {
super(message, 500);
}
}

View file

@ -32,7 +32,12 @@ async function startServer() {
app.use("/user", userRoutes);
// app.use("/recipe", recipeRoutes);
// Error handling for all rest-calls
// must come last!
app.use(errorHandler);
console.log("auth and user routes added")
// catch all other routes
app.get(/(.*)/, (req: Request, res: Response, next: NextFunction) => {
res.status(400).json({ message: "Bad Request" });
});

View file

@ -2,14 +2,14 @@ import { NextFunction, Request, Response } from "express";
import jwt from "jsonwebtoken";
import dotenv from "dotenv";
import { authBasicRoute } from "../endpoints/AuthPoint.js";
import { AuthPayload } from "../dtos/AuthPayload.js";
dotenv.config();
//@todo this seems to be clumsy... We need some propper session handling as we'll have multiple users accessing the app
declare global {
namespace Express {
interface Request {
currentUser?: string | jwt.JwtPayload;
currentUser?: AuthPayload;
}
}
}
@ -46,10 +46,10 @@ export const authentication = (
}
try {
const decoded = jwt.verify(token, JWT_SECRET);
const decoded = jwt.verify(token, JWT_SECRET) as AuthPayload;
req.currentUser = decoded;
next();
} catch (err) {
} catch {
return res.status(401).json({ message: "Unauthorized" });
}
};

View file

@ -1,11 +1,35 @@
import { NextFunction, Request, Response } from "express";
// middleware/errorHandler.ts
import { Request, Response, NextFunction } from "express";
import { HttpError, InternalServerError } from "../errors/httpErrors.js";
export const errorHandler = (
error: Error,
/**
* Express global error-handling middleware.
*
* Responsibilities:
* - Catch and handle errors thrown in controllers or routes
* - Map known HttpError subclasses (ValidationError, UnauthorizedError, etc.)
* to the appropriate HTTP status code and JSON response
* - Fallback to InternalServerError for unexpected/unhandled errors
*
* Usage:
* 1. Register after all routes: `app.use(errorHandler);`
* 2. Throw `HttpError` subclasses in your controllers/services
* 3. Any other uncaught error is logged and returned as 500 Internal Server Error
*/
export function errorHandler(
err: any,
req: Request,
res: Response,
next: NextFunction
) => {
console.error(`Error: ${error.message}`);
return res.status(500).json({ message: "Internal server error" });
};
) {
if (err instanceof HttpError) {
return res.status(err.statusCode).json({ statusCode: err.statusCode, error: err.message });
}
console.error("Unexpected error:", err);
const internalError = new InternalServerError(
"An unexpected error occurred. Please try again later."
);
return res.status(internalError.statusCode).json({ error: internalError.message });
}

12
src/utils/asyncHandler.ts Normal file
View file

@ -0,0 +1,12 @@
import { Request, Response, NextFunction } from "express";
/**
* Handles asyncronous rest calls
* @param fn function to execute asyncronously
*/
export const asyncHandler =
(fn: (req: Request, res: Response, next: NextFunction) => Promise<any>) =>
(req: Request, res: Response, next: NextFunction) => {
Promise.resolve(fn(req, res, next)).catch(next);
};

View file

@ -1,10 +1,15 @@
import jwt from "jsonwebtoken";
import bcrypt from "bcrypt";
import dotenv from "dotenv";
import { AuthPayload } from "../dtos/AuthPayload.js";
dotenv.config();
const { JWT_SECRET = "" } = process.env;
/**
* Information on the current auth token containing the
* token itself as well as its expiry date
*/
export class tokenInfo{
constructor(token: string, expiryDate: Date){
this.token = token;
@ -14,22 +19,47 @@ export class tokenInfo{
expiryDate: Date;
}
/**
* Responsible for handling of encrypted passwords and token handling
*/
export class encrypt {
/**
* Encrypts a password string
* @param password Password string
* @returns encrypted password string
*/
static async encryptpass(password: string) {
return bcrypt.hashSync(password, 12);
}
static comparepassword(password: string, hashPassword: string) {
return bcrypt.compareSync(password, hashPassword);
}
static generateToken(payload: object) {
/**
* Compares a plain text password to an encrypted password
* @param password plain text password
* @param hashPassword ecrypted password
* @returns Boolean indicating whether both passwords are identical
*/
static comparepassword(password: string, hashPassword: string) : boolean{
return bcrypt.compareSync(password, hashPassword);
}
/**
* Generates an authentication token that will be valid for one day based on the payload provided
* @param payload AuthPayload containing the userId
* @returns tokenInfo containing authentication token and expiryDate
*/
static generateToken(payload: AuthPayload) : tokenInfo {
let expiryDate = new Date();
expiryDate.setDate(expiryDate.getDate() + 1); // 1 day
const token = jwt.sign(payload, JWT_SECRET, { expiresIn: "1d" });
return new tokenInfo(token, expiryDate);
}
static verifyToken(token: string) {
/**
* Verifys the token and decrypts the payload
* @param token authentiction token
* @returns Payload of the token
*/
static verifyToken(token: string) : jwt.JwtPayload | string {
return jwt.verify(token, JWT_SECRET);
}
}