Fix build and implement user/all correctly
This commit is contained in:
parent
a9bd803112
commit
555dacfaf5
17 changed files with 170 additions and 48 deletions
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ get {
|
|||
}
|
||||
|
||||
auth:bearer {
|
||||
token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjU2NGE5NjY0LTI2ZWYtNGMxMS1hNjIyLWU4MDI2MzczYmRkZCIsImlhdCI6MTc1ODk1MTI4NSwiZXhwIjoxNzU5MDM3Njg1fQ.FeuMlAurJFAhBPyvJFx3MX64WHB0_sa5pldkbQylUOw
|
||||
token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6ImE0NDdlNDM0LTQyMWYtNDJiYS04MGRlLTM0ZDE1YzJmNWE2YyIsImlhdCI6MTc2MzQ5MDcxMywiZXhwIjoxNzYzNTc3MTEzfQ.h8ta-4tVhR7EskZDBLtcFTQ7QllV-PfC09Y0DLjYJa4
|
||||
}
|
||||
|
||||
settings {
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
31
src/apiHelpers/HttpStatusCodes.ts
Normal file
31
src/apiHelpers/HttpStatusCodes.ts
Normal 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,
|
||||
}
|
||||
|
|
@ -6,4 +6,5 @@ import { JwtPayload } from "jsonwebtoken";
|
|||
*/
|
||||
export interface AuthPayload extends JwtPayload {
|
||||
id: string;
|
||||
role?: string; // Add role to the JWT payload
|
||||
}
|
||||
|
|
|
|||
8
src/dtos/UserListResponse.ts
Normal file
8
src/dtos/UserListResponse.ts
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
import {UserDto} from "./UserDto.js";
|
||||
|
||||
/**
|
||||
* API response for delivering a list of users
|
||||
*/
|
||||
export class UserListResponse {
|
||||
valueList: UserDto[] = [];
|
||||
}
|
||||
|
|
@ -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);
|
||||
})
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
})
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
})
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
||||
|
|
|
|||
|
|
@ -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" });
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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"]);
|
||||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue