running now

This commit is contained in:
Anika Raemer 2025-09-22 20:17:46 +02:00
parent 85cd083750
commit c17bb05f0a
25 changed files with 156 additions and 114 deletions

2
.env
View file

@ -4,5 +4,5 @@ DB_HOST=localhost
DB_PORT=5432 DB_PORT=5432
DB_USERNAME=recipe-backend DB_USERNAME=recipe-backend
DB_PASSWORD=yeshu0bue5aigaphie0eemoFey3farei DB_PASSWORD=yeshu0bue5aigaphie0eemoFey3farei
DB_DATABASE=recipe-backend DB_DATABASE=recipe-backend-dev
NODE_ENV=dev NODE_ENV=dev

2
.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
.vscode
build

View file

@ -2,7 +2,7 @@
"name": "recipe-backend", "name": "recipe-backend",
"version": "0.0.1", "version": "0.0.1",
"description": "Awesome project developed with TypeORM.", "description": "Awesome project developed with TypeORM.",
"type": "commonjs", "type": "module",
"devDependencies": { "devDependencies": {
"@types/node": "^22.13.10", "@types/node": "^22.13.10",
"ts-node": "^10.9.2", "ts-node": "^10.9.2",

View file

@ -1,9 +1,9 @@
import { UserRepository } from "../repositories/UserRepository"; import { UserRepository } from "../repositories/UserRepository.js";
import { encrypt } from "../utils/encryptionUtils"; import { encrypt } from "../utils/encryptionUtils.js";
import { ValidationError, UnauthorizedError } from "../errors/httpErrors"; import { ValidationError, UnauthorizedError } from "../errors/httpErrors.js";
import { UserDtoEntityMapper } from "../mappers/UserDtoEntityMapper"; import { UserDtoEntityMapper } from "../mappers/UserDtoEntityMapper.js";
import { LoginResponseDto } from "../dtos/LoginResponseDto"; import { LoginResponseDto } from "../dtos/LoginResponseDto.js";
import { LoginRequestDto } from "../dtos/LoginRequestDto"; import { LoginRequestDto } from "../dtos/LoginRequestDto.js";
export class AuthController { export class AuthController {
constructor( constructor(
@ -12,25 +12,27 @@ export class AuthController {
) {} ) {}
async login(loginRequest : LoginRequestDto): Promise<LoginResponseDto> { async login(loginRequest : LoginRequestDto): Promise<LoginResponseDto> {
const userName :string = loginRequest.userName; const userName :string|undefined = loginRequest.userName;
const password :string = loginRequest.password; const password :string|undefined = loginRequest.password;
if (!userName || !password) { if (!userName || !password) {
throw new ValidationError("Username and password are required"); throw new ValidationError("Username and password are required");
} }
// Find user by userName // Find user by userName
const user = await this.userRepository.findByUserName(userName); const user = await this.userRepository.findByUserName(userName);
// check user before trying to access password!
if(!user){
throw new UnauthorizedError("Invalid username or password");
}
// Compare password // Compare password
const passwordMatches = encrypt.comparepassword(user.password, password); const passwordMatches = encrypt.comparepassword(user.password, password);
if (!passwordMatches || !user ) { if (!passwordMatches) {
throw new UnauthorizedError("Invalid username or password"); throw new UnauthorizedError("Invalid username or password");
} }
// Create JWT // Create JWT
const token = encrypt.generateToken({ const token = encrypt.generateToken({
id: user.id, id: user.id,
userName: user.userName,
role: user.role,
}); });
const responseDto = new LoginResponseDto(); const responseDto = new LoginResponseDto();

View file

@ -1,10 +1,9 @@
import { ValidationError, ConflictError } from "../errors/httpErrors"; import { ValidationError, ConflictError } from "../errors/httpErrors.js";
import { CreateUserRequestDto } from "../dtos/CreateUserRequestDto"; import { CreateUserRequestDto } from "../dtos/CreateUserRequestDto.js";
import { UserDto } from "../dtos/UserDto"; import { UserDto } from "../dtos/UserDto.js";
import { encrypt } from "../utils/encryptionUtils"; import { encrypt } from "../utils/encryptionUtils.js";
import { UserRepository } from "../repositories/UserRepository"; import { UserRepository } from "../repositories/UserRepository.js";
import { UserDtoEntityMapper } from "../mappers/UserDtoEntityMapper"; import { UserDtoEntityMapper } from "../mappers/UserDtoEntityMapper.js";
import { isNeitherNullNorEmpty } from "../utils/stringUtils";
export class UserController { export class UserController {
constructor( constructor(
@ -14,23 +13,30 @@ export class UserController {
async createUser(dto: CreateUserRequestDto): Promise<UserDto> { async createUser(dto: CreateUserRequestDto): Promise<UserDto> {
// check mandatory fields // check mandatory fields
if (!isNeitherNullNorEmpty(dto.userData.email)) { if(!dto.userData){
throw new ValidationError("User data is required")
}
const email = dto.userData.email;
if (!email || (email && email.length == 0)) {
throw new ValidationError("Email is required"); throw new ValidationError("Email is required");
} }
if (!isNeitherNullNorEmpty(dto.userData.userName)) { const password = dto.password;
throw new ValidationError("Username is required"); if(!password || (password && password.length == 0)){
}
if(!isNeitherNullNorEmpty(dto.password){
throw new ValidationError("Password is required"); throw new ValidationError("Password is required");
} }
// user name must be uniqu const userName = dto.userData.userName;
const existingUser = await this.userRepository.findByUserName(dto.userData.email); if (!userName|| (userName && userName.length == 0)){
throw new ValidationError("Username is required");
}
// user name must be unique
const existingUser = await this.userRepository.findByUserName(userName);
if (existingUser) { if (existingUser) {
throw new ConflictError("User with this user name already exists"); throw new ConflictError("User with this user name already exists");
} }
const userEntity = this.mapper.toEntity(dto.userData); const userEntity = this.mapper.toEntity(dto.userData);
userEntity.password = await encrypt.encryptpass(dto.password); userEntity.password = await encrypt.encryptpass(password);
const savedUser = await this.userRepository.create(userEntity); const savedUser = await this.userRepository.create(userEntity);

View file

@ -2,7 +2,13 @@ import "reflect-metadata";
import { DataSource } from "typeorm"; import { DataSource } from "typeorm";
import * as dotenv from "dotenv"; import * as dotenv from "dotenv";
import { UserEntity } from "./entities/UserEntity"; import { UserEntity } from "./entities/UserEntity.js";
import { fileURLToPath } from "url";
import { dirname, join } from "path";
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
dotenv.config(); dotenv.config();
@ -20,7 +26,7 @@ export const AppDataSource = new DataSource({
synchronize: NODE_ENV === "dev" ? false : false, synchronize: NODE_ENV === "dev" ? false : false,
//logging logs sql command on the terminal //logging logs sql command on the terminal
logging: NODE_ENV === "dev" ? false : false, logging: NODE_ENV === "dev" ? false : false,
entities: [UserEntity], entities: [join(__dirname, "/entities/*.{ts,js}")],
migrations: [__dirname + "/migration/*.ts"], migrations: [join(__dirname, "/migrations/*.{ts,js}")],
subscribers: [], subscribers: [],
}); });

View file

@ -1,5 +1,5 @@
export abstract class AbstractDto { export abstract class AbstractDto {
id: string; id?: string;
createdAt?: Date; createdAt?: Date;
updatedAt?: Date; updatedAt?: Date;
} }

View file

@ -1,9 +1,9 @@
import { UserDto } from "./UserDto"; import { UserDto } from "./UserDto.js";
/** /**
* DTO used for user creation * DTO used for user creation
*/ */
export class CreateUserRequestDto { export class CreateUserRequestDto {
userData: UserDto; userData?: UserDto;
password: string; password?: string;
} }

View file

@ -2,6 +2,6 @@
* Defines a login request * Defines a login request
*/ */
export class LoginRequestDto { export class LoginRequestDto {
userName: string; userName?: string;
password: string; password?: string;
} }

View file

@ -1,9 +1,9 @@
import { UserDto } from "./UserDto"; import { UserDto } from "./UserDto.js";
/** /**
* Response to a successful login * Response to a successful login
*/ */
export class LoginResponseDto { export class LoginResponseDto {
userData: UserDto; userData?: UserDto;
token: string; token?: string;
} }

View file

@ -1,9 +1,9 @@
import { AbstractDto } from "./AbstractDto"; import { AbstractDto } from "./AbstractDto.js";
export class UserDto extends AbstractDto { export class UserDto extends AbstractDto {
firstName?: string; firstName?: string;
lastName?: string; lastName?: string;
userName: string; userName!: string;
email: string; email!: string;
role: string; role?: string;
} }

View file

@ -1,13 +1,13 @@
import { Router } from "express"; import { Router } from "express";
import { AuthController } from "../controllers/AuthController"; import { AuthController } from "../controllers/AuthController.js";
import { UserRepository } from "../repositories/UserRepository"; import { UserRepository } from "../repositories/UserRepository.js";
import { UserDtoEntityMapper } from "../mappers/UserDtoEntityMapper"; import { UserDtoEntityMapper } from "../mappers/UserDtoEntityMapper.js";
import { import {
ValidationError, ValidationError,
UnauthorizedError, UnauthorizedError,
InternalServerError, InternalServerError,
} from "../errors/httpErrors"; } from "../errors/httpErrors.js";
import { LoginRequestDto } from "../dtos/LoginRequestDto"; import { LoginRequestDto } from "../dtos/LoginRequestDto.js";
const router = Router(); const router = Router();
@ -18,7 +18,7 @@ const authController = new AuthController(userRepository, mapper);
router.post("/login", async (req, res) => { router.post("/login", async (req, res) => {
try { try {
const requestDto: LoginRequestDto = req.body; const requestDto: LoginRequestDto = req.body;
const responseDto = await authController.login(requestDto)); const responseDto = await authController.login(requestDto);
res.json(responseDto); res.json(responseDto);
} catch (err: any) { } catch (err: any) {
if (err instanceof ValidationError || err instanceof UnauthorizedError) { if (err instanceof ValidationError || err instanceof UnauthorizedError) {

View file

@ -1,14 +1,14 @@
import { Router } from "express"; import { Router } from "express";
import { UserController } from "../controllers/UserController"; import { UserController } from "../controllers/UserController.js";
import { CreateUserRequestDto } from "../dtos/CreateUserRequestDto"; import { CreateUserRequestDto } from "../dtos/CreateUserRequestDto.js";
import { UserRepository } from "../repositories/UserRepository"; import { UserRepository } from "../repositories/UserRepository.js";
import { UserDtoEntityMapper } from "../mappers/UserDtoEntityMapper"; import { UserDtoEntityMapper } from "../mappers/UserDtoEntityMapper.js";
import { import {
ValidationError, ValidationError,
ConflictError, ConflictError,
NotFoundError, NotFoundError,
InternalServerError, InternalServerError,
} from "../errors/httpErrors"; } from "../errors/httpErrors.js";
const router = Router(); const router = Router();

View file

@ -6,11 +6,11 @@ import {
export abstract class AbstractEntity { export abstract class AbstractEntity {
@PrimaryGeneratedColumn("uuid") @PrimaryGeneratedColumn("uuid")
id: string; id?: string;
@CreateDateColumn() @CreateDateColumn()
createdAt: Date; createdAt?: Date;
@UpdateDateColumn() @UpdateDateColumn()
updatedAt: Date; updatedAt?: Date;
} }

View file

@ -1,24 +1,23 @@
import { Entity, Column } from "typeorm"; import { Entity, Column } from "typeorm";
import { AbstractEntity } from "./AbstractEntity"; import { AbstractEntity } from "./AbstractEntity.js";
@Entity({ name: "user" }) @Entity({ name: "user" })
export class UserEntity extends AbstractEntity { export class UserEntity extends AbstractEntity {
@Column({ nullable: false }) @Column({ nullable: false })
userName: string; userName!: string;
@Column({ nullable: false }) @Column({ nullable: false })
email: string; email!: string;
@Column({ nullable: false }) @Column({ nullable: false })
password: string; password!: string;
@Column({ nullable: true }) @Column({ nullable: true })
firstName: string; firstName?: string;
@Column({ nullable: true }) @Column({ nullable: true })
lastName: string; lastName?: string;
@Column({ default: "user" }) @Column({ default: "user" })
role: string; role!: string;
} }

View file

@ -1,19 +1,18 @@
import { AppDataSource } from "./data-source"
import * as express from "express";
import * as dotenv from "dotenv";
import { Request, Response } from "express";
import authRoutes from "./endpoints/AuthPoint"
import userRoutes from "./endpoints/UserPoint";
//import { recipePoint } from "./endpoints/RecipePoint";
import {errorHandler} from "./middleware/errorHandler"
import "reflect-metadata"; import "reflect-metadata";
import express, { NextFunction, Request, Response } from "express";
import dotenv from "dotenv";
import { AppDataSource } from "./data-source.js";
import authRoutes from "./endpoints/AuthPoint.js";
import userRoutes from "./endpoints/UserPoint.js";
// import recipeRoutes from "./endpoints/RecipePoint.js";
import { errorHandler } from "./middleware/errorHandler.js";
dotenv.config(); dotenv.config();
const app = express(); const app = express();
app.use(express.json()); app.use(express.json());
app.use(errorHandler); app.use(errorHandler);
async function startServer() { async function startServer() {
try { try {
// 1⃣ Initialize database // 1⃣ Initialize database
@ -27,21 +26,26 @@ async function startServer() {
// 2⃣ Setup routes // 2⃣ Setup routes
app.use("/auth", authRoutes); app.use("/auth", authRoutes);
app.use("/user", userRoutes); app.use("/user", userRoutes);
// app.use("/recipe", recipeRoutes);
app.get("*", (req: Request, res: Response) => { console.log("auth and user routes added")
res.status(505).json({ message: "Bad Request" }); app.get(/(.*)/, (req: Request, res: Response, next: NextFunction) => {
res.status(400).json({ message: "Bad Request" });
}); });
console.log("Routes set up")
// 3⃣ Start listening // 3⃣ Start listening
const PORT = parseInt(process.env.PORT || "4000", 10); const PORT = Number(process.env.PORT) || 4000;
const HOST = process.env.HOST || "localhost"; const HOST = process.env.HOST || "localhost";
app.listen(PORT, HOST, () => { app.listen(PORT, HOST, () => {
console.log(`Server running on http://${HOST}:${PORT}`); console.log(`Server running on http://${HOST}:${PORT}`);
}); });
} catch (err) { } catch (err) {
console.error("Error during Data Source initialization:", err); console.error("❌ Error during Data Source initialization:", err);
process.exit(1);
} }
} }
// Call the async start function // Call the async start function
startServer(); startServer();

View file

@ -1,5 +1,5 @@
import { AbstractDto } from "../dtos/AbstractDto"; import { AbstractDto } from "../dtos/AbstractDto.js";
import { AbstractEntity } from "../entities/AbstractEntity"; import { AbstractEntity } from "../entities/AbstractEntity.js";
export abstract class AbstractDtoEntityMapper< export abstract class AbstractDtoEntityMapper<
E extends AbstractEntity, E extends AbstractEntity,

View file

@ -1,6 +1,7 @@
import { AbstractDtoEntityMapper } from "./AbstractDtoEntityMapper"; import { AbstractDtoEntityMapper } from "./AbstractDtoEntityMapper.js";
import { UserEntity } from "../entities/UserEntity"; import { UserEntity } from "../entities/UserEntity.js";
import { UserDto } from "../dtos/UserDto"; import { UserDto } from "../dtos/UserDto.js";
import { ValidationError } from "../errors/httpErrors.js";
export class UserDtoEntityMapper extends AbstractDtoEntityMapper<UserEntity, UserDto> { export class UserDtoEntityMapper extends AbstractDtoEntityMapper<UserEntity, UserDto> {
toDto(entity: UserEntity): UserDto { toDto(entity: UserEntity): UserDto {
@ -22,10 +23,11 @@ export class UserDtoEntityMapper extends AbstractDtoEntityMapper<UserEntity, Use
entity.userName = dto.userName; entity.userName = dto.userName;
entity.email = dto.email; entity.email = dto.email;
// ⚠️ Dont forget password handling — usually set elsewhere, not exposed in DTO
entity.firstName = dto.firstName; entity.firstName = dto.firstName;
entity.lastName = dto.lastName; entity.lastName = dto.lastName;
entity.role = dto.role; if(dto.role){
entity.role = dto.role;
}
return entity; return entity;
} }

View file

@ -1,8 +1,17 @@
import { NextFunction, Request, Response } from "express"; import { NextFunction, Request, Response } from "express";
import * as jwt from "jsonwebtoken"; import * as jwt from "jsonwebtoken";
import * as dotenv from "dotenv"; import * as dotenv from "dotenv";
dotenv.config(); dotenv.config();
declare global {
namespace Express {
interface Request {
currentUser?: string | jwt.JwtPayload;
}
}
}
export const authentication = ( export const authentication = (
req: Request, req: Request,
res: Response, res: Response,
@ -12,14 +21,22 @@ export const authentication = (
if (!header) { if (!header) {
return res.status(401).json({ message: "Unauthorized" }); return res.status(401).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(401).json({ message: "Unauthorized" });
} }
const decode = jwt.verify(token, process.env.JWT_SECRET);
if (!decode) { const JWT_SECRET = process.env.JWT_SECRET;
if (!JWT_SECRET) {
throw new Error("JWT_SECRET not defined");
}
try {
const decoded = jwt.verify(token, JWT_SECRET);
req.currentUser = decoded;
next();
} catch (err) {
return res.status(401).json({ message: "Unauthorized" }); return res.status(401).json({ message: "Unauthorized" });
} }
req[" currentUser"] = decode; };
next();
};

View file

@ -1,10 +1,16 @@
import { NextFunction, Request, Response } from "express"; /* import { NextFunction, Request, Response } from "express";
import { AppDataSource } from "../data-source"; import { AppDataSource } from "../data-source.js";
import { UserEntity } from "../entities/UserEntity"; import { UserEntity } from "../entities/UserEntity.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[]) => { export const authorization = (roles: string[]) => {
return async (req: Request, res: Response, next: NextFunction) => { return async (req: Request, res: Response, next: NextFunction) => {
const userRepo = AppDataSource.getRepository(UserEntity); 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({ const user = await userRepo.findOne({
where: { id: req[" currentUser"].id }, where: { id: req[" currentUser"].id },
}); });
@ -13,5 +19,5 @@ export const authorization = (roles: string[]) => {
return res.status(403).json({ message: "Forbidden" }); return res.status(403).json({ message: "Forbidden" });
} }
next(); next();
}; };
}; };*/

View file

@ -1,7 +1,8 @@
import { Repository, DeepPartial } from "typeorm"; import { Repository, DeepPartial, ObjectLiteral } from "typeorm";
import { AppDataSource } from "../data-source"; import { AppDataSource } from "../data-source.js";
import { AbstractEntity } from "../entities/AbstractEntity.js";
export abstract class AbstractRepository<T> { export abstract class AbstractRepository<T extends AbstractEntity> {
protected repo: Repository<T>; protected repo: Repository<T>;
constructor(entity: { new (): T }) { constructor(entity: { new (): T }) {

View file

@ -1,5 +1,5 @@
import { AbstractRepository } from "./AbstractRepository"; import { AbstractRepository } from "./AbstractRepository.js";
import { UserEntity } from "../entities/UserEntity"; import { UserEntity } from "../entities/UserEntity.js";
export class UserRepository extends AbstractRepository<UserEntity> { export class UserRepository extends AbstractRepository<UserEntity> {
constructor() { constructor() {

View file

@ -1,7 +1,6 @@
import * as jwt from "jsonwebtoken"; import * as jwt from "jsonwebtoken";
import * as bcrypt from "bcrypt"; import * as bcrypt from "bcrypt";
import * as dotenv from "dotenv"; import * as dotenv from "dotenv";
import { payload } from "../dtos/userDto";
dotenv.config(); dotenv.config();
const { JWT_SECRET = "" } = process.env; const { JWT_SECRET = "" } = process.env;
@ -9,11 +8,11 @@ export class encrypt {
static async encryptpass(password: string) { static async encryptpass(password: string) {
return bcrypt.hashSync(password, 12); return bcrypt.hashSync(password, 12);
} }
static comparepassword(hashPassword: string, password: string) { static comparepassword(password: string, hashPassword: string) {
return bcrypt.compareSync(password, hashPassword); return bcrypt.compareSync(password, hashPassword);
} }
static generateToken(payload: payload) { static generateToken(payload: object) {
return jwt.sign(payload, JWT_SECRET, { expiresIn: "1d" }); return jwt.sign(payload, JWT_SECRET, { expiresIn: "1d" });
} }
} }

View file

@ -1,3 +0,0 @@
export function isNeitherNullNorEmpty(input : string) : boolean {
return input && input.length !==0;
}

View file

@ -4,11 +4,12 @@
"es2021" "es2021"
], ],
"target": "es2021", "target": "es2021",
"module": "commonjs", "module": "node16",
"moduleResolution": "node", "moduleResolution": "node16",
"outDir": "./build", "outDir": "./build",
"emitDecoratorMetadata": true, "emitDecoratorMetadata": true,
"experimentalDecorators": true, "experimentalDecorators": true,
"sourceMap": true "sourceMap": true,
"strict": true
} }
} }