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 {
token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6ImE0NDdlNDM0LTQyMWYtNDJiYS04MGRlLTM0ZDE1YzJmNWE2YyIsImlhdCI6MTc2MzQ5MDcxMywiZXhwIjoxNzYzNTc3MTEzfQ.h8ta-4tVhR7EskZDBLtcFTQ7QllV-PfC09Y0DLjYJa4
}
body:json {
{
"userData": {
"userName": "test2",
"userName": "test3",
"email": "test@raemer.net"
},
"password": "test"

View file

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

View file

@ -1,7 +1,7 @@
{
"name": "recipe-backend",
"version": "0.0.1",
"description": "Awesome project developed with TypeORM.",
"description": "A backend for managin recipes",
"type": "module",
"devDependencies": {
"@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 {
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 { CompactRecipeHandler } from "../handlers/CompactRecipeHandler.js";
import { CompactRecipeDtoEntityMapper } from "../mappers/CompactRecipeDtoEntityMapper.js";
import {HttpStatusCode} from "../apiHelpers/HttpStatusCodes.js";
/**
* Handles all recipe related routes
@ -10,9 +11,7 @@ import { CompactRecipeDtoEntityMapper } from "../mappers/CompactRecipeDtoEntityM
const router = Router();
// Inject repo + mapper here
const recipeRepository = new RecipeRepository();
const compactRecipeMapper = new CompactRecipeDtoEntityMapper();
const compactRecipeHandler = new CompactRecipeHandler(recipeRepository, compactRecipeMapper);
const compactRecipeHandler = new CompactRecipeHandler(new RecipeRepository(), new CompactRecipeDtoEntityMapper());
/**
* Load header data of all recipes
* Responds with a list of CompactRecipeDtos
@ -26,7 +25,7 @@ router.get(
const searchString : string = req.query.search ? req.query.search.toString().toLowerCase() : "";
console.log("Searching for recipes with title containing", 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 { RecipeIngredientGroupDtoEntityMapper } from "../mappers/RecipeIngredientGroupDtoEntityMapper.js";
import { RecipeInstructionStepDtoEntityMapper } from "../mappers/RecipeInstructionStepDtoEntityMapper.js";
import {HttpStatusCode} from "../apiHelpers/HttpStatusCodes.js";
/**
* Handles all recipe related routes
@ -35,7 +36,7 @@ router.post(
asyncHandler(async(req, res) => {
const requestDto: RecipeDto = req.body;
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) => {
const id = req.params.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 {CreateUserResponse} from "../dtos/CreateUserResponse.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
*/
const router = Router();
router.use((req, res, next) => {
console.log(`Incoming request: ${req.method} ${req.path}`);
next();
});
// Inject repo + mapper here
const handler
@ -42,9 +50,9 @@ router.post(
asyncHandler(async (req, res) => {
const dto : UserDto = req.body;
const response = await handler.updateUserData(dto);
res.status(201).json(response);
res.status(HttpStatusCode.CREATED).json(response);
})
)
);
/**
* Update password of existing user
@ -56,7 +64,22 @@ router.post(
asyncHandler(async (req, res) => {
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
@ -67,9 +90,8 @@ router.get("/me",
const id = req.currentUser?.id;
if(id){
// it breaks here because id is no longer a uuid
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.
* 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 {
constructor(message: string) {
super(message, 400); // Bad Request
super(message, HttpStatusCode.BAD_REQUEST); // Bad Request
}
}
export class UnauthorizedError extends HttpError {
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 {
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
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();

View file

@ -99,4 +99,13 @@ export class UserHandler {
}
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 { AppDataSource } from "./data-source.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 recipeRoutes from "./endpoints/RecipePoint.js";
import { errorHandler } from "./middleware/errorHandler.js";
@ -36,7 +36,7 @@ async function startServer() {
// Setup routes
app.use(authBasicRoute, authRoutes);
app.use("/user", userRoutes);
app.use(userBasicRoute, userRoutes);
app.use("/recipe", recipeRoutes);
app.use("/compact-recipe", compactRecipeRoutes);
@ -44,10 +44,11 @@ async function startServer() {
// must come last!
app.use(errorHandler);
console.log("auth and user routes added")
console.log("all routes added")
// catch all other routes
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")

View file

@ -3,6 +3,7 @@ import jwt from "jsonwebtoken";
import dotenv from "dotenv";
import { authBasicRoute } from "../endpoints/AuthPoint.js";
import { AuthPayload } from "../dtos/AuthPayload.js";
import {HttpStatusCode} from "../apiHelpers/HttpStatusCodes.js";
dotenv.config();
@ -32,12 +33,12 @@ export const authentication = (
const header = req.headers.authorization;
if (!header) {
return res.status(401).json({ message: "Unauthorized" });
return res.status(HttpStatusCode.UNAUTHORIZED).json({ message: "Unauthorized" });
}
const token = header.split(" ")[1];
if (!token) {
return res.status(401).json({ message: "Unauthorized" });
return res.status(HttpStatusCode.UNAUTHORIZED).json({ message: "Unauthorized" });
}
const JWT_SECRET = process.env.JWT_SECRET;
@ -50,6 +51,6 @@ export const authentication = (
req.currentUser = decoded;
next();
} 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 { AppDataSource } from "../data-source.js";
import { UserEntity } from "../entities/UserEntity.js";
import { NextFunction, Request, Response } from "express";
import { ForbiddenError } from "../errors/httpErrors.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[]) => {
return async (req: Request, res: Response, next: NextFunction) => {
const userRepo = AppDataSource.getRepository(UserEntity);
const currentUser = req.currentUser;
if(!currentUser){
return res.status(403).json({ message: "Forbidden - currentUser is missing" });
}
const userId = currentUser.id
const user = await userRepo.findOne({
where: { id: req[" currentUser"].id },
});
console.log(user);
if (!roles.includes(user.role)) {
return res.status(403).json({ message: "Forbidden" });
}
next();
};
};*/
/**
* Middleware to check if the current user has one of the required roles
* Must be used after the authentication middleware
*
* @param allowedRoles Array of role names that are allowed to access the route
* @returns Express middleware function
*
* @example
* router.get("/admin-only", requireRole(["admin"]), asyncHandler(async (req, res) => { ... }));
* router.post("/admin-or-moderator", requireRole(["admin", "moderator"]), asyncHandler(async (req, res) => { ... }));
*/
export const requireRole = (allowedRoles: string[]) => {
return (req: Request, res: Response, next: NextFunction) => {
// Check if user is authenticated
if (!req.currentUser) {
return res.status(HttpStatusCode.UNAUTHORIZED).json({ message: "Unauthorized" });
}
// 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 {HttpStatusCode} from "../apiHelpers/HttpStatusCodes.js";
/**
* Add CORS header
@ -15,7 +16,7 @@ export function corsHeaders (req: Request, res: Response, next: NextFunction) {
// Handle preflight requests quickly
if (req.method === 'OPTIONS') {
return res.sendStatus(200);
return res.sendStatus(HttpStatusCode.OK);
}
next();

View file

@ -9,4 +9,17 @@ export class UserRepository extends AbstractRepository<UserEntity> {
async findByUserName(userName: string): Promise<UserEntity | null> {
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",
},
});
}
}