Fix build and implement user/all correctly

This commit is contained in:
araemer 2025-11-18 20:26:28 +01:00
parent a9bd803112
commit 555dacfaf5
17 changed files with 170 additions and 48 deletions

View file

@ -10,10 +10,14 @@ post {
auth: bearer auth: bearer
} }
auth:bearer {
token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6ImE0NDdlNDM0LTQyMWYtNDJiYS04MGRlLTM0ZDE1YzJmNWE2YyIsImlhdCI6MTc2MzQ5MDcxMywiZXhwIjoxNzYzNTc3MTEzfQ.h8ta-4tVhR7EskZDBLtcFTQ7QllV-PfC09Y0DLjYJa4
}
body:json { body:json {
{ {
"userData": { "userData": {
"userName": "test2", "userName": "test3",
"email": "test@raemer.net" "email": "test@raemer.net"
}, },
"password": "test" "password": "test"

View file

@ -11,7 +11,7 @@ get {
} }
auth:bearer { auth:bearer {
token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjU2NGE5NjY0LTI2ZWYtNGMxMS1hNjIyLWU4MDI2MzczYmRkZCIsImlhdCI6MTc1ODk1MTI4NSwiZXhwIjoxNzU5MDM3Njg1fQ.FeuMlAurJFAhBPyvJFx3MX64WHB0_sa5pldkbQylUOw token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6ImE0NDdlNDM0LTQyMWYtNDJiYS04MGRlLTM0ZDE1YzJmNWE2YyIsImlhdCI6MTc2MzQ5MDcxMywiZXhwIjoxNzYzNTc3MTEzfQ.h8ta-4tVhR7EskZDBLtcFTQ7QllV-PfC09Y0DLjYJa4
} }
settings { settings {

View file

@ -1,7 +1,7 @@
{ {
"name": "recipe-backend", "name": "recipe-backend",
"version": "0.0.1", "version": "0.0.1",
"description": "Awesome project developed with TypeORM.", "description": "A backend for managin recipes",
"type": "module", "type": "module",
"devDependencies": { "devDependencies": {
"@types/node": "^22.13.10", "@types/node": "^22.13.10",

View file

@ -0,0 +1,31 @@
/**
* HTTP Status Codes
* Comprehensive enum for all standard HTTP status codes
*/
export const enum HttpStatusCode {
// 2xx Success
OK = 200,
CREATED = 201,
ACCEPTED = 202,
NO_CONTENT = 204,
// 3xx Redirection
MOVED_PERMANENTLY = 301,
FOUND = 302,
NOT_MODIFIED = 304,
// 4xx Client Errors
BAD_REQUEST = 400,
UNAUTHORIZED = 401,
FORBIDDEN = 403,
NOT_FOUND = 404,
METHOD_NOT_ALLOWED = 405,
CONFLICT = 409,
UNPROCESSABLE_ENTITY = 422,
// 5xx Server Errors
INTERNAL_SERVER_ERROR = 500,
NOT_IMPLEMENTED = 501,
BAD_GATEWAY = 502,
SERVICE_UNAVAILABLE = 503,
}

View file

@ -6,4 +6,5 @@ import { JwtPayload } from "jsonwebtoken";
*/ */
export interface AuthPayload extends JwtPayload { export interface AuthPayload extends JwtPayload {
id: string; id: string;
role?: string; // Add role to the JWT payload
} }

View file

@ -0,0 +1,8 @@
import {UserDto} from "./UserDto.js";
/**
* API response for delivering a list of users
*/
export class UserListResponse {
valueList: UserDto[] = [];
}

View file

@ -3,6 +3,7 @@ import { asyncHandler } from "../utils/asyncHandler.js";
import { RecipeRepository } from "../repositories/RecipeRepository.js"; import { RecipeRepository } from "../repositories/RecipeRepository.js";
import { CompactRecipeHandler } from "../handlers/CompactRecipeHandler.js"; import { CompactRecipeHandler } from "../handlers/CompactRecipeHandler.js";
import { CompactRecipeDtoEntityMapper } from "../mappers/CompactRecipeDtoEntityMapper.js"; import { CompactRecipeDtoEntityMapper } from "../mappers/CompactRecipeDtoEntityMapper.js";
import {HttpStatusCode} from "../apiHelpers/HttpStatusCodes.js";
/** /**
* Handles all recipe related routes * Handles all recipe related routes
@ -10,9 +11,7 @@ import { CompactRecipeDtoEntityMapper } from "../mappers/CompactRecipeDtoEntityM
const router = Router(); const router = Router();
// Inject repo + mapper here // Inject repo + mapper here
const recipeRepository = new RecipeRepository(); const compactRecipeHandler = new CompactRecipeHandler(new RecipeRepository(), new CompactRecipeDtoEntityMapper());
const compactRecipeMapper = new CompactRecipeDtoEntityMapper();
const compactRecipeHandler = new CompactRecipeHandler(recipeRepository, compactRecipeMapper);
/** /**
* Load header data of all recipes * Load header data of all recipes
* Responds with a list of CompactRecipeDtos * Responds with a list of CompactRecipeDtos
@ -26,7 +25,7 @@ router.get(
const searchString : string = req.query.search ? req.query.search.toString().toLowerCase() : ""; const searchString : string = req.query.search ? req.query.search.toString().toLowerCase() : "";
console.log("Searching for recipes with title containing", searchString) console.log("Searching for recipes with title containing", searchString)
const response = await compactRecipeHandler.getMatchingRecipes(searchString); const response = await compactRecipeHandler.getMatchingRecipes(searchString);
res.status(201).json(response); res.status(HttpStatusCode.OK).json(response);
}) })
); );

View file

@ -7,6 +7,7 @@ import { RecipeDto } from "../dtos/RecipeDto.js";
import { RecipeIngredientDtoEntityMapper } from "../mappers/RecipeIngredientDtoEntityMapper.js"; import { RecipeIngredientDtoEntityMapper } from "../mappers/RecipeIngredientDtoEntityMapper.js";
import { RecipeIngredientGroupDtoEntityMapper } from "../mappers/RecipeIngredientGroupDtoEntityMapper.js"; import { RecipeIngredientGroupDtoEntityMapper } from "../mappers/RecipeIngredientGroupDtoEntityMapper.js";
import { RecipeInstructionStepDtoEntityMapper } from "../mappers/RecipeInstructionStepDtoEntityMapper.js"; import { RecipeInstructionStepDtoEntityMapper } from "../mappers/RecipeInstructionStepDtoEntityMapper.js";
import {HttpStatusCode} from "../apiHelpers/HttpStatusCodes.js";
/** /**
* Handles all recipe related routes * Handles all recipe related routes
@ -35,7 +36,7 @@ router.post(
asyncHandler(async(req, res) => { asyncHandler(async(req, res) => {
const requestDto: RecipeDto = req.body; const requestDto: RecipeDto = req.body;
const responseDto = await recipeHandler.createOrUpdateRecipe(requestDto); const responseDto = await recipeHandler.createOrUpdateRecipe(requestDto);
res.status(201).json(responseDto); res.status(HttpStatusCode.CREATED).json(responseDto);
}) })
) )
@ -48,7 +49,7 @@ router.get(
asyncHandler(async(req, res) => { asyncHandler(async(req, res) => {
const id = req.params.id; const id = req.params.id;
const responseDto = await recipeHandler.getRecipeById(id); const responseDto = await recipeHandler.getRecipeById(id);
res.status(201).json(responseDto); res.status(HttpStatusCode.OK).json(responseDto);
}) })
); );

View file

@ -6,11 +6,19 @@ import { UserDtoEntityMapper } from "../mappers/UserDtoEntityMapper.js";
import { asyncHandler } from "../utils/asyncHandler.js"; import { asyncHandler } from "../utils/asyncHandler.js";
import {CreateUserResponse} from "../dtos/CreateUserResponse.js"; import {CreateUserResponse} from "../dtos/CreateUserResponse.js";
import {UserDto} from "../dtos/UserDto.js"; import {UserDto} from "../dtos/UserDto.js";
import {requireAdmin} from "../middleware/authorizationMiddleware.js";
import {UserListResponse} from "../dtos/UserListResponse.js";
import {HttpStatusCode} from "../apiHelpers/HttpStatusCodes.js";
export const userBasicRoute = "/user";
/** /**
* Handles all user related routes * Handles all user related routes
*/ */
const router = Router(); const router = Router();
router.use((req, res, next) => {
console.log(`Incoming request: ${req.method} ${req.path}`);
next();
});
// Inject repo + mapper here // Inject repo + mapper here
const handler const handler
@ -42,9 +50,9 @@ router.post(
asyncHandler(async (req, res) => { asyncHandler(async (req, res) => {
const dto : UserDto = req.body; const dto : UserDto = req.body;
const response = await handler.updateUserData(dto); const response = await handler.updateUserData(dto);
res.status(201).json(response); res.status(HttpStatusCode.CREATED).json(response);
}) })
) );
/** /**
* Update password of existing user * Update password of existing user
@ -56,7 +64,22 @@ router.post(
asyncHandler(async (req, res) => { asyncHandler(async (req, res) => {
throw Error("not implemented!"); throw Error("not implemented!");
}) })
) );
/**
* Get all users (admin only)
* Responds with array of UserDto
*/
router.get(
"/all",
//requireAdmin,
asyncHandler(async (req, res) => {
const id = req.currentUser?.id;
const users = await handler.getAllUsers();
const response : UserListResponse = {valueList: users}
res.status(HttpStatusCode.OK).json(response);
})
);
/** /**
* Get user data for current user * Get user data for current user
@ -67,9 +90,8 @@ router.get("/me",
const id = req.currentUser?.id; const id = req.currentUser?.id;
if(id){ if(id){
// it breaks here because id is no longer a uuid
const responseDto = await handler.getUserById(id); const responseDto = await handler.getUserById(id);
res.status(201).json(responseDto); res.status(HttpStatusCode.OK).json(responseDto);
} }
}) })
); );

View file

@ -1,3 +1,5 @@
import {HttpStatusCode} from "../apiHelpers/HttpStatusCodes.js";
/** /**
* Base class for all HTTP-related errors. * Base class for all HTTP-related errors.
* Extends the built-in Error with a status code and message * Extends the built-in Error with a status code and message
@ -17,19 +19,25 @@ export class HttpError extends Error {
export class ValidationError extends HttpError { export class ValidationError extends HttpError {
constructor(message: string) { constructor(message: string) {
super(message, 400); // Bad Request super(message, HttpStatusCode.BAD_REQUEST); // Bad Request
} }
} }
export class UnauthorizedError extends HttpError { export class UnauthorizedError extends HttpError {
constructor(message: string = "Unauthorized") { constructor(message: string = "Unauthorized") {
super(message, 401); super(message, HttpStatusCode.UNAUTHORIZED);
} }
} }
export class ForbiddenError extends HttpError {
constructor(message: string = "Forbidden") {
super(message, HttpStatusCode.FORBIDDEN);
}
}
export class NotFoundError extends HttpError { export class NotFoundError extends HttpError {
constructor(message: string = "Resource not found") { constructor(message: string = "Resource not found") {
super(message, 404); super(message, HttpStatusCode.NOT_FOUND);
} }
} }

View file

@ -47,7 +47,8 @@ export class AuthHandler {
// Create JWT // Create JWT
const tokenInfo = encrypt.generateToken({ const tokenInfo = encrypt.generateToken({
id: userId!, // ! to indicate that we've definitely checked for userId being defined id: userId!, // ! to indicate that we've definitely checked for userId being defined
role: user.role
}); });
const responseDto = new LoginResponse(); const responseDto = new LoginResponse();

View file

@ -99,4 +99,13 @@ export class UserHandler {
} }
return this.mapper.toDto(userEntity); return this.mapper.toDto(userEntity);
} }
/**
* Get all users
* @returns Array of UserDto containing all users
*/
async getAllUsers(): Promise<UserDto[]> {
const userEntities = await this.userRepository.findAll();
return userEntities.map((entity) => this.mapper.toDto(entity));
}
} }

View file

@ -3,7 +3,7 @@ import express, { NextFunction, Request, Response } from "express";
import dotenv from "dotenv"; import dotenv from "dotenv";
import { AppDataSource } from "./data-source.js"; import { AppDataSource } from "./data-source.js";
import authRoutes, { authBasicRoute } from "./endpoints/AuthPoint.js"; import authRoutes, { authBasicRoute } from "./endpoints/AuthPoint.js";
import userRoutes from "./endpoints/UserPoint.js"; import userRoutes, {userBasicRoute} from "./endpoints/UserPoint.js";
import compactRecipeRoutes from "./endpoints/CompactRecipePoint.js"; import compactRecipeRoutes from "./endpoints/CompactRecipePoint.js";
import recipeRoutes from "./endpoints/RecipePoint.js"; import recipeRoutes from "./endpoints/RecipePoint.js";
import { errorHandler } from "./middleware/errorHandler.js"; import { errorHandler } from "./middleware/errorHandler.js";
@ -36,7 +36,7 @@ async function startServer() {
// Setup routes // Setup routes
app.use(authBasicRoute, authRoutes); app.use(authBasicRoute, authRoutes);
app.use("/user", userRoutes); app.use(userBasicRoute, userRoutes);
app.use("/recipe", recipeRoutes); app.use("/recipe", recipeRoutes);
app.use("/compact-recipe", compactRecipeRoutes); app.use("/compact-recipe", compactRecipeRoutes);
@ -44,10 +44,11 @@ async function startServer() {
// must come last! // must come last!
app.use(errorHandler); app.use(errorHandler);
console.log("auth and user routes added") console.log("all routes added")
// catch all other routes // catch all other routes
app.get(/(.*)/, (req: Request, res: Response, next: NextFunction) => { app.get(/(.*)/, (req: Request, res: Response, next: NextFunction) => {
res.status(400).json({ message: "Bad Request" }); console.log("unknown route", req.url);
res.status(400).json({ message: "Bad Request - unknown route" });
}); });
console.log("Routes set up") console.log("Routes set up")

View file

@ -3,6 +3,7 @@ import jwt from "jsonwebtoken";
import dotenv from "dotenv"; import dotenv from "dotenv";
import { authBasicRoute } from "../endpoints/AuthPoint.js"; import { authBasicRoute } from "../endpoints/AuthPoint.js";
import { AuthPayload } from "../dtos/AuthPayload.js"; import { AuthPayload } from "../dtos/AuthPayload.js";
import {HttpStatusCode} from "../apiHelpers/HttpStatusCodes.js";
dotenv.config(); dotenv.config();
@ -32,12 +33,12 @@ export const authentication = (
const header = req.headers.authorization; const header = req.headers.authorization;
if (!header) { if (!header) {
return res.status(401).json({ message: "Unauthorized" }); return res.status(HttpStatusCode.UNAUTHORIZED).json({ message: "Unauthorized" });
} }
const token = header.split(" ")[1]; const token = header.split(" ")[1];
if (!token) { if (!token) {
return res.status(401).json({ message: "Unauthorized" }); return res.status(HttpStatusCode.UNAUTHORIZED).json({ message: "Unauthorized" });
} }
const JWT_SECRET = process.env.JWT_SECRET; const JWT_SECRET = process.env.JWT_SECRET;
@ -50,6 +51,6 @@ export const authentication = (
req.currentUser = decoded; req.currentUser = decoded;
next(); next();
} catch { } catch {
return res.status(401).json({ message: "Unauthorized" }); return res.status(HttpStatusCode.UNAUTHORIZED).json({ message: "Unauthorized" });
} }
}; };

View file

@ -1,23 +1,45 @@
/* import { NextFunction, Request, Response } from "express"; import { NextFunction, Request, Response } from "express";
import { AppDataSource } from "../data-source.js"; import { ForbiddenError } from "../errors/httpErrors.js";
import { UserEntity } from "../entities/UserEntity.js"; import {HttpStatusCode} from "../apiHelpers/HttpStatusCodes.js";
// @todo we'll need some other means to determin the user corresponding to the token here as it seems... /**
export const authorization = (roles: string[]) => { * Middleware to check if the current user has one of the required roles
return async (req: Request, res: Response, next: NextFunction) => { * Must be used after the authentication middleware
const userRepo = AppDataSource.getRepository(UserEntity); *
const currentUser = req.currentUser; * @param allowedRoles Array of role names that are allowed to access the route
if(!currentUser){ * @returns Express middleware function
return res.status(403).json({ message: "Forbidden - currentUser is missing" }); *
} * @example
const userId = currentUser.id * router.get("/admin-only", requireRole(["admin"]), asyncHandler(async (req, res) => { ... }));
const user = await userRepo.findOne({ * router.post("/admin-or-moderator", requireRole(["admin", "moderator"]), asyncHandler(async (req, res) => { ... }));
where: { id: req[" currentUser"].id }, */
}); export const requireRole = (allowedRoles: string[]) => {
console.log(user); return (req: Request, res: Response, next: NextFunction) => {
if (!roles.includes(user.role)) { // Check if user is authenticated
return res.status(403).json({ message: "Forbidden" }); if (!req.currentUser) {
} return res.status(HttpStatusCode.UNAUTHORIZED).json({ message: "Unauthorized" });
next(); }
};
};*/ // Get user's role from the auth payload
const userRole = req.currentUser.role;
console.log("userRole", userRole);
// Check if user has one of the allowed roles
if (!userRole || !allowedRoles.includes(userRole)) {
throw new ForbiddenError(
`Access denied. Required role: ${allowedRoles.join(" or ")}`
);
}
// User has required role, proceed
next();
};
};
/**
* Convenience middleware for admin-only routes
*
* @example
* router.get("/admin-panel", requireAdmin, asyncHandler(async (req, res) => { ... }));
*/
export const requireAdmin = requireRole(["admin"]);

View file

@ -1,4 +1,5 @@
import { Request, Response, NextFunction } from "express"; import { Request, Response, NextFunction } from "express";
import {HttpStatusCode} from "../apiHelpers/HttpStatusCodes.js";
/** /**
* Add CORS header * Add CORS header
@ -15,7 +16,7 @@ export function corsHeaders (req: Request, res: Response, next: NextFunction) {
// Handle preflight requests quickly // Handle preflight requests quickly
if (req.method === 'OPTIONS') { if (req.method === 'OPTIONS') {
return res.sendStatus(200); return res.sendStatus(HttpStatusCode.OK);
} }
next(); next();

View file

@ -9,4 +9,17 @@ export class UserRepository extends AbstractRepository<UserEntity> {
async findByUserName(userName: string): Promise<UserEntity | null> { async findByUserName(userName: string): Promise<UserEntity | null> {
return this.repo.findOne({ where: { userName } }); return this.repo.findOne({ where: { userName } });
} }
/**
* Find all users
* @returns Array of all UserEntity records
*/
async findAll(): Promise<UserEntity[]> {
return await this.repo.find({
order: {
lastName: "ASC",
firstName: "ASC",
},
});
}
} }