add auth and user handling

This commit is contained in:
Anika Raemer 2025-09-21 19:49:54 +02:00
parent db057ce342
commit 1fce467571
19 changed files with 356 additions and 32 deletions

View file

@ -0,0 +1,38 @@
import { UserRepository } from "../repositories/UserRepository";
import { encrypt } from "../utils/encryptionUtils";
import { ValidationError, UnauthorizedError } from "../errors/httpErrors";
import { UserDto } from "../dtos/UserDto";
import { UserDtoEntityMapper } from "../mappers/UserDtoEntityMapper";
export class AuthController {
constructor(
private userRepository: UserRepository,
private mapper: UserDtoEntityMapper
) {}
async login(userName: string, password: string): Promise<{ token: string; user: UserDto }> {
if (!userName || !password) {
throw new ValidationError("Username and password are required");
}
// Find user by userName
const user = await this.userRepository.findByUserName(userName);
// Compare password
const passwordMatches = encrypt.comparepassword(user.password, password);
if (!passwordMatches || !user ) {
throw new UnauthorizedError("Invalid username or password");
}
// Create JWT
const token = encrypt.generateToken({
id: user.id,
userName: user.userName,
role: user.role,
});
return {
token,
user: this.mapper.toDto(user),
};
}
}

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,33 @@
import { Router } from "express";
import { AuthController } from "../controllers/AuthController";
import { UserRepository } from "../repositories/UserRepository";
import { UserDtoEntityMapper } from "../mappers/UserDtoEntityMapper";
import {
ValidationError,
UnauthorizedError,
InternalServerError,
} from "../errors/httpErrors";
const router = Router();
const userRepository = new UserRepository();
const mapper = new UserDtoEntityMapper();
const authController = new AuthController(userRepository, mapper);
router.post("/login", async (req, res) => {
try {
const { userName, password } = req.body;
const result = await authController.login(userName, password);
res.json(result);
} catch (err: any) {
if (err instanceof ValidationError || err instanceof UnauthorizedError) {
res.status(err.statusCode).json({ error: err.message });
} else {
console.error("Unexpected error:", err);
const internalError = new InternalServerError("Unexpected error");
res.status(internalError.statusCode).json({ error: internalError.message });
}
}
});
export default router;

View file

@ -0,0 +1,38 @@
import { Router } from "express";
import { UserController } from "../controllers/UserController";
import { CreateUserRequestDto } from "../dtos/CreateUserRequestDto";
import { UserRepository } from "../repositories/UserRepository";
import { UserDtoEntityMapper } from "../mappers/UserDtoEntityMapper";
import {
ValidationError,
ConflictError,
NotFoundError,
InternalServerError,
} from "../errors/httpErrors";
const router = Router();
// Inject repo + mapper here
const userRepository = new UserRepository();
const userMapper = new UserDtoEntityMapper();
const userController = new UserController(userRepository, userMapper);
router.post("/", async (req, res) => {
try {
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 });
}
}
});
export default router;

View file

@ -0,0 +1,16 @@
import {
PrimaryGeneratedColumn,
CreateDateColumn,
UpdateDateColumn,
} from "typeorm";
export abstract class AbstractEntity {
@PrimaryGeneratedColumn("uuid")
id: string;
@CreateDateColumn()
createdAt: Date;
@UpdateDateColumn()
updatedAt: Date;
}

View file

@ -1,16 +1,8 @@
import { import { Entity, Column } from "typeorm";
Entity, import { AbstractEntity } from "./AbstractEntity";
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
UpdateDateColumn,
} from "typeorm";
@Entity({ name: "user" }) @Entity({ name: "user" })
export class UserEntity { export class UserEntity extends AbstractEntity {
@PrimaryGeneratedColumn("uuid")
id: string;
@Column({ nullable: false }) @Column({ nullable: false })
userName: string; userName: string;
@ -28,10 +20,5 @@ export class UserEntity {
@Column({ default: "user" }) @Column({ default: "user" })
role: string; role: string;
@CreateDateColumn()
createdAt: Date;
@UpdateDateColumn()
updatedAt: Date;
} }

39
src/errors/httpErrors.ts Normal file
View file

@ -0,0 +1,39 @@
export class ValidationError extends Error {
statusCode = 400;
constructor(message: string) {
super(message);
this.name = "ValidationError";
}
}
export class NotFoundError extends Error {
statusCode = 404;
constructor(message: string) {
super(message);
this.name = "NotFoundError";
}
}
export class ConflictError extends Error {
statusCode = 409;
constructor(message: string) {
super(message);
this.name = "ConflictError";
}
}
export class InternalServerError extends Error {
statusCode = 500;
constructor(message: string) {
super(message);
this.name = "InternalServerError";
}
}
export class UnauthorizedError extends Error {
statusCode = 409;
constructor(message: string) {
super(message);
this.name = "UnauthorizedError";
}
}

View file

@ -2,9 +2,9 @@ import { AppDataSource } from "./data-source"
import * as express from "express"; import * as express from "express";
import * as dotenv from "dotenv"; import * as dotenv from "dotenv";
import { Request, Response } from "express"; import { Request, Response } from "express";
import { authPoint } from "./endpoints/authPoint"; import authRoutes from "./endpoints/AuthPoint"
import { userPoint } from "./endpoints/userPoint"; import userRoutes from "./endpoints/UserPoint";
import { recipePoint } from "./endpoints/recipePoint"; //import { recipePoint } from "./endpoints/RecipePoint";
import {errorHandler} from "./middleware/errorHandler" import {errorHandler} from "./middleware/errorHandler"
import "reflect-metadata"; import "reflect-metadata";
dotenv.config(); dotenv.config();
@ -16,9 +16,9 @@ app.use(errorHandler);
const PORT = process.env.PORT ? parseInt(process.env.PORT) : 4000; const PORT = process.env.PORT ? parseInt(process.env.PORT) : 4000;
const HOST = process.env.HOST || "localhost"; const HOST = process.env.HOST || "localhost";
app.use("/auth", authPoint); app.use("/auth", authRoutes);
app.use("/user", userPoint); app.use("/user", userRoutes);
app.use("/recipe", recipePoint); //app.use("/recipe", recipePoint);
app.get("*", (req: Request, res: Response) => { app.get("*", (req: Request, res: Response) => {
res.status(505).json({ message: "Bad Request" }); res.status(505).json({ message: "Bad Request" });

View file

@ -0,0 +1,31 @@
import { AbstractDto } from "../dtos/AbstractDto";
import { AbstractEntity } from "../entities/AbstractEntity";
export abstract class AbstractDtoEntityMapper<
E extends AbstractEntity,
D extends AbstractDto
> {
/**
* Map base entity fields (id, createdAt, updatedAt) to DTO.
*/
protected mapBaseEntityToDto(entity: E, dto: D): D {
dto.id = entity.id;
dto.createdAt = entity.createdAt;
dto.updatedAt = entity.updatedAt;
return dto;
}
/**
* Map base DTO fields to entity.
*/
protected mapBaseDtoToEntity(dto: D, entity: E): E {
entity.id = dto.id;
entity.createdAt = dto.createdAt;
entity.updatedAt = dto.updatedAt;
return entity;
}
// Abstract methods to be implemented by subclasses
abstract toDto(entity: E): D;
abstract toEntity(dto: D): E;
}

View file

@ -0,0 +1,32 @@
import { AbstractDtoEntityMapper } from "./AbstractDtoEntityMapper";
import { UserEntity } from "../entities/UserEntity";
import { UserDto } from "../dtos/UserDto";
export class UserDtoEntityMapper extends AbstractDtoEntityMapper<UserEntity, UserDto> {
toDto(entity: UserEntity): UserDto {
const dto = new UserDto();
this.mapBaseEntityToDto(entity, dto);
dto.userName = entity.userName;
dto.email = entity.email;
dto.firstName = entity.firstName;
dto.lastName = entity.lastName;
dto.role = entity.role;
return dto;
}
toEntity(dto: UserDto): UserEntity {
const entity = new UserEntity();
this.mapBaseDtoToEntity(dto, entity);
entity.userName = dto.userName;
entity.email = dto.email;
// ⚠️ Dont forget password handling — usually set elsewhere, not exposed in DTO
entity.firstName = dto.firstName;
entity.lastName = dto.lastName;
entity.role = dto.role;
return entity;
}
}

View file

@ -0,0 +1,41 @@
import { Repository, DeepPartial } from "typeorm";
import { AppDataSource } from "../data-source";
export abstract class AbstractRepository<T> {
protected repo: Repository<T>;
constructor(entity: { new (): T }) {
this.repo = AppDataSource.getRepository(entity);
}
async findById(id: string): Promise<T | null> {
return this.repo.findOne({ where: { id } as any });
}
async findAll(): Promise<T[]> {
return this.repo.find();
}
async create(data: DeepPartial<T>): Promise<T> {
const entity = this.repo.create(data);
return this.repo.save(entity);
}
/* async update(id: string, partialData: DeepPartial<T>): Promise<T> {
await this.repo.update(id as any, partialData);
const updated = await this.findById(id);
if (!updated) {
throw new Error("Entity not found after update");
}
return updated;
} */
async delete(id: string): Promise<void> {
await this.repo.delete(id as any);
}
async save(entity: T): Promise<T> {
return this.repo.save(entity);
}
}

View file

@ -0,0 +1,12 @@
import { AbstractRepository } from "./AbstractRepository";
import { UserEntity } from "../entities/UserEntity";
export class UserRepository extends AbstractRepository<UserEntity> {
constructor() {
super(UserEntity);
}
async findByUserName(userName: string): Promise<UserEntity | null> {
return this.repo.findOne({ where: { userName } });
}
}

3
src/utils/stringUtils.ts Normal file
View file

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