add auth and user handling
This commit is contained in:
parent
db057ce342
commit
1fce467571
19 changed files with 356 additions and 32 deletions
38
src/controllers/AuthController.ts
Normal file
38
src/controllers/AuthController.ts
Normal 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),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
39
src/controllers/UserController.ts
Normal file
39
src/controllers/UserController.ts
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
export abstract class AbstractDto {
|
||||||
|
id: string;
|
||||||
|
createdAt?: Date;
|
||||||
|
updatedAt?: Date;
|
||||||
|
}
|
||||||
9
src/dtos/CreateUserRequestDto.ts
Normal file
9
src/dtos/CreateUserRequestDto.ts
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
import { UserDto } from "./UserDto";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DTO used for user creation
|
||||||
|
*/
|
||||||
|
export class CreateUserRequestDto {
|
||||||
|
userData: UserDto;
|
||||||
|
password: string;
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
33
src/endpoints/AuthPoint.ts
Normal file
33
src/endpoints/AuthPoint.ts
Normal 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;
|
||||||
38
src/endpoints/UserPoint.ts
Normal file
38
src/endpoints/UserPoint.ts
Normal 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;
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
import {
|
||||||
|
PrimaryGeneratedColumn,
|
||||||
|
CreateDateColumn,
|
||||||
|
UpdateDateColumn,
|
||||||
|
} from "typeorm";
|
||||||
|
|
||||||
|
export abstract class AbstractEntity {
|
||||||
|
@PrimaryGeneratedColumn("uuid")
|
||||||
|
id: string;
|
||||||
|
|
||||||
|
@CreateDateColumn()
|
||||||
|
createdAt: Date;
|
||||||
|
|
||||||
|
@UpdateDateColumn()
|
||||||
|
updatedAt: Date;
|
||||||
|
}
|
||||||
|
|
@ -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
39
src/errors/httpErrors.ts
Normal 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";
|
||||||
|
}
|
||||||
|
}
|
||||||
12
src/index.ts
12
src/index.ts
|
|
@ -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" });
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
// ⚠️ Don’t forget password handling — usually set elsewhere, not exposed in DTO
|
||||||
|
entity.firstName = dto.firstName;
|
||||||
|
entity.lastName = dto.lastName;
|
||||||
|
entity.role = dto.role;
|
||||||
|
|
||||||
|
return entity;
|
||||||
|
}
|
||||||
|
}
|
||||||
41
src/repositories/AbstractRepository.ts
Normal file
41
src/repositories/AbstractRepository.ts
Normal 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
12
src/repositories/UserRepository.ts
Normal file
12
src/repositories/UserRepository.ts
Normal 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
3
src/utils/stringUtils.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
export function isNeitherNullNorEmpty(input : string) : boolean {
|
||||||
|
return input && input.length !==0;
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue