add me point to load user data of current user
This commit is contained in:
parent
fac606cf97
commit
e5b5d7e67d
12 changed files with 215 additions and 67 deletions
|
|
@ -7,11 +7,7 @@ meta {
|
|||
post {
|
||||
url: http://localhost:4000/user
|
||||
body: json
|
||||
auth: inherit
|
||||
}
|
||||
|
||||
headers {
|
||||
Authorization: bearcer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjU2NGE5NjY0LTI2ZWYtNGMxMS1hNjIyLWU4MDI2MzczYmRkZCIsImlhdCI6MTc1ODc0MTM5MywiZXhwIjoxNzU4ODI3NzkzfQ.q33R9FfhGUIn92PTIIAmKmUnGxcLlv6om7KwiDD61Rc
|
||||
auth: bearer
|
||||
}
|
||||
|
||||
body:json {
|
||||
|
|
|
|||
19
bruno/recipe-backend/me.bru
Normal file
19
bruno/recipe-backend/me.bru
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
meta {
|
||||
name: me
|
||||
type: http
|
||||
seq: 3
|
||||
}
|
||||
|
||||
get {
|
||||
url: http://localhost:4000/user/me
|
||||
body: none
|
||||
auth: bearer
|
||||
}
|
||||
|
||||
auth:bearer {
|
||||
token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjU2NGE5NjY0LTI2ZWYtNGMxMS1hNjIyLWU4MDI2MzczYmRkZCIsImlhdCI6MTc1ODk1MTI4NSwiZXhwIjoxNzU5MDM3Njg1fQ.FeuMlAurJFAhBPyvJFx3MX64WHB0_sa5pldkbQylUOw
|
||||
}
|
||||
|
||||
settings {
|
||||
encodeUrl: true
|
||||
}
|
||||
|
|
@ -5,12 +5,21 @@ import { UserDtoEntityMapper } from "../mappers/UserDtoEntityMapper.js";
|
|||
import { LoginResponseDto } from "../dtos/LoginResponseDto.js";
|
||||
import { LoginRequestDto } from "../dtos/LoginRequestDto.js";
|
||||
|
||||
/**
|
||||
* Controller responsible for authentication, e.g., login or issueing a token with extended
|
||||
* lifetime
|
||||
*/
|
||||
export class AuthController {
|
||||
constructor(
|
||||
private userRepository: UserRepository,
|
||||
private mapper: UserDtoEntityMapper
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Login: Check user and password and generate token
|
||||
* @param loginRequest LoginRequestDto containing userName and password for login
|
||||
* @returns LoginResponse containing token and user data for the user who just logged in
|
||||
*/
|
||||
async login(loginRequest : LoginRequestDto): Promise<LoginResponseDto> {
|
||||
const userName :string|undefined = loginRequest.userName;
|
||||
const password :string|undefined = loginRequest.password;
|
||||
|
|
@ -25,6 +34,11 @@ export class AuthController {
|
|||
if(!user){
|
||||
throw new UnauthorizedError("Invalid username or password");
|
||||
}
|
||||
// ensure user has an id - required to generate token
|
||||
const userId = user.id;
|
||||
if(user.id == undefined){
|
||||
throw new UnauthorizedError("Invalid username or password");
|
||||
}
|
||||
// Compare password
|
||||
const passwordMatches = encrypt.comparepassword(password, user.password);
|
||||
if (!passwordMatches) {
|
||||
|
|
@ -33,7 +47,7 @@ export class AuthController {
|
|||
|
||||
// Create JWT
|
||||
const tokenInfo = encrypt.generateToken({
|
||||
id: user.id,
|
||||
id: userId!, // ! to indicate that we've definitely checked for userId being defined
|
||||
});
|
||||
|
||||
const responseDto = new LoginResponseDto();
|
||||
|
|
|
|||
|
|
@ -1,18 +1,26 @@
|
|||
import { ValidationError, ConflictError } from "../errors/httpErrors.js";
|
||||
import { ValidationError, ConflictError, NotFoundError } from "../errors/httpErrors.js";
|
||||
import { CreateUserRequestDto } from "../dtos/CreateUserRequestDto.js";
|
||||
import { UserDto } from "../dtos/UserDto.js";
|
||||
import { encrypt } from "../utils/encryptionUtils.js";
|
||||
import { UserRepository } from "../repositories/UserRepository.js";
|
||||
import { UserDtoEntityMapper } from "../mappers/UserDtoEntityMapper.js";
|
||||
import { UUID } from "crypto";
|
||||
|
||||
/**
|
||||
* Controls all user specific actions
|
||||
*/
|
||||
export class UserController {
|
||||
constructor(
|
||||
private userRepository: UserRepository,
|
||||
private mapper: UserDtoEntityMapper
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Create a new user
|
||||
* @param dto CreateUserRequestDto containing data for the user to add
|
||||
* @returns UserDto Data of the user as stored in the database
|
||||
*/
|
||||
async createUser(dto: CreateUserRequestDto): Promise<UserDto> {
|
||||
// @todo make authorized! Create initial user!
|
||||
// check mandatory fields
|
||||
if(!dto.userData){
|
||||
throw new ValidationError("User data is required")
|
||||
|
|
@ -43,4 +51,21 @@ export class UserController {
|
|||
|
||||
return this.mapper.toDto(savedUser);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load data of a specific user
|
||||
* @param userId Id of user to load
|
||||
* @returns UserDto containing the user's data
|
||||
*/
|
||||
async getUserById(userId: UUID|string|undefined): Promise<UserDto> {
|
||||
if(!userId){
|
||||
throw new ValidationError("userId is required");
|
||||
}
|
||||
|
||||
const userEntity = await this.userRepository.findById(userId);
|
||||
if(!userEntity){
|
||||
throw new NotFoundError("user with id" + userId + "not found!")
|
||||
}
|
||||
return this.mapper.toDto(userEntity);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
9
src/dtos/AuthPayload.ts
Normal file
9
src/dtos/AuthPayload.ts
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
import { JwtPayload } from "jsonwebtoken";
|
||||
|
||||
/**
|
||||
* Shape of the payload stored inside JWT tokens.
|
||||
* We extend JwtPayload so "iat" / "exp" etc. remain available.
|
||||
*/
|
||||
export interface AuthPayload extends JwtPayload {
|
||||
id: string;
|
||||
}
|
||||
|
|
@ -3,13 +3,11 @@ import { UserController } from "../controllers/UserController.js";
|
|||
import { CreateUserRequestDto } from "../dtos/CreateUserRequestDto.js";
|
||||
import { UserRepository } from "../repositories/UserRepository.js";
|
||||
import { UserDtoEntityMapper } from "../mappers/UserDtoEntityMapper.js";
|
||||
import {
|
||||
ValidationError,
|
||||
ConflictError,
|
||||
NotFoundError,
|
||||
InternalServerError,
|
||||
} from "../errors/httpErrors.js";
|
||||
import { asyncHandler } from "../utils/asyncHandler.js";
|
||||
|
||||
/**
|
||||
* Handles all user related routes
|
||||
*/
|
||||
const router = Router();
|
||||
|
||||
// Inject repo + mapper here
|
||||
|
|
@ -17,22 +15,31 @@ const userRepository = new UserRepository();
|
|||
const userMapper = new UserDtoEntityMapper();
|
||||
const userController = new UserController(userRepository, userMapper);
|
||||
|
||||
router.post("/", async (req, res) => {
|
||||
try {
|
||||
/**
|
||||
* Create a new user
|
||||
*/
|
||||
router.post(
|
||||
"/",
|
||||
asyncHandler(async (req, res) => {
|
||||
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 });
|
||||
})
|
||||
);
|
||||
|
||||
/**
|
||||
* Get user data for current user
|
||||
*/
|
||||
router.get("/me",
|
||||
asyncHandler(async (req, res) => {
|
||||
|
||||
const id = req.currentUser?.id;
|
||||
if(id){
|
||||
// it breaks here because id is no longer a uuid
|
||||
const responseDto = await userController.getUserById(id);
|
||||
res.status(201).json(responseDto);
|
||||
}
|
||||
}
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
export default router;
|
||||
|
|
|
|||
|
|
@ -1,39 +1,46 @@
|
|||
export class ValidationError extends Error {
|
||||
statusCode = 400;
|
||||
constructor(message: string) {
|
||||
/**
|
||||
* Base class for all HTTP-related errors.
|
||||
* Extends the built-in Error with a status code and message
|
||||
* so that they can be handled in a consistent way.
|
||||
*/
|
||||
export class HttpError extends Error {
|
||||
public statusCode: number;
|
||||
|
||||
constructor(message: string, statusCode: number) {
|
||||
super(message);
|
||||
this.name = "ValidationError";
|
||||
this.statusCode = statusCode;
|
||||
|
||||
// Restore prototype chain (important when targeting ES5/ES2015+)
|
||||
Object.setPrototypeOf(this, new.target.prototype);
|
||||
}
|
||||
}
|
||||
|
||||
export class NotFoundError extends Error {
|
||||
statusCode = 404;
|
||||
export class ValidationError extends HttpError {
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
this.name = "NotFoundError";
|
||||
super(message, 400); // Bad Request
|
||||
}
|
||||
}
|
||||
|
||||
export class ConflictError extends Error {
|
||||
statusCode = 409;
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
this.name = "ConflictError";
|
||||
export class UnauthorizedError extends HttpError {
|
||||
constructor(message: string = "Unauthorized") {
|
||||
super(message, 401);
|
||||
}
|
||||
}
|
||||
|
||||
export class InternalServerError extends Error {
|
||||
statusCode = 500;
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
this.name = "InternalServerError";
|
||||
export class NotFoundError extends HttpError {
|
||||
constructor(message: string = "Resource not found") {
|
||||
super(message, 404);
|
||||
}
|
||||
}
|
||||
|
||||
export class UnauthorizedError extends Error {
|
||||
statusCode = 409;
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
this.name = "UnauthorizedError";
|
||||
export class ConflictError extends HttpError {
|
||||
constructor(message: string = "Conflict") {
|
||||
super(message, 409);
|
||||
}
|
||||
}
|
||||
|
||||
export class InternalServerError extends HttpError {
|
||||
constructor(message: string = "Internal Server Error") {
|
||||
super(message, 500);
|
||||
}
|
||||
}
|
||||
|
|
@ -32,7 +32,12 @@ async function startServer() {
|
|||
app.use("/user", userRoutes);
|
||||
// app.use("/recipe", recipeRoutes);
|
||||
|
||||
// Error handling for all rest-calls
|
||||
// must come last!
|
||||
app.use(errorHandler);
|
||||
|
||||
console.log("auth and user routes added")
|
||||
// catch all other routes
|
||||
app.get(/(.*)/, (req: Request, res: Response, next: NextFunction) => {
|
||||
res.status(400).json({ message: "Bad Request" });
|
||||
});
|
||||
|
|
|
|||
|
|
@ -2,14 +2,14 @@ import { NextFunction, Request, Response } from "express";
|
|||
import jwt from "jsonwebtoken";
|
||||
import dotenv from "dotenv";
|
||||
import { authBasicRoute } from "../endpoints/AuthPoint.js";
|
||||
import { AuthPayload } from "../dtos/AuthPayload.js";
|
||||
|
||||
dotenv.config();
|
||||
|
||||
//@todo this seems to be clumsy... We need some propper session handling as we'll have multiple users accessing the app
|
||||
declare global {
|
||||
namespace Express {
|
||||
interface Request {
|
||||
currentUser?: string | jwt.JwtPayload;
|
||||
currentUser?: AuthPayload;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -46,10 +46,10 @@ export const authentication = (
|
|||
}
|
||||
|
||||
try {
|
||||
const decoded = jwt.verify(token, JWT_SECRET);
|
||||
const decoded = jwt.verify(token, JWT_SECRET) as AuthPayload;
|
||||
req.currentUser = decoded;
|
||||
next();
|
||||
} catch (err) {
|
||||
} catch {
|
||||
return res.status(401).json({ message: "Unauthorized" });
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,11 +1,35 @@
|
|||
import { NextFunction, Request, Response } from "express";
|
||||
// middleware/errorHandler.ts
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import { HttpError, InternalServerError } from "../errors/httpErrors.js";
|
||||
|
||||
export const errorHandler = (
|
||||
error: Error,
|
||||
/**
|
||||
* Express global error-handling middleware.
|
||||
*
|
||||
* Responsibilities:
|
||||
* - Catch and handle errors thrown in controllers or routes
|
||||
* - Map known HttpError subclasses (ValidationError, UnauthorizedError, etc.)
|
||||
* to the appropriate HTTP status code and JSON response
|
||||
* - Fallback to InternalServerError for unexpected/unhandled errors
|
||||
*
|
||||
* Usage:
|
||||
* 1. Register after all routes: `app.use(errorHandler);`
|
||||
* 2. Throw `HttpError` subclasses in your controllers/services
|
||||
* 3. Any other uncaught error is logged and returned as 500 Internal Server Error
|
||||
*/
|
||||
export function errorHandler(
|
||||
err: any,
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) => {
|
||||
console.error(`Error: ${error.message}`);
|
||||
return res.status(500).json({ message: "Internal server error" });
|
||||
};
|
||||
) {
|
||||
if (err instanceof HttpError) {
|
||||
return res.status(err.statusCode).json({ statusCode: err.statusCode, error: err.message });
|
||||
}
|
||||
|
||||
console.error("Unexpected error:", err);
|
||||
|
||||
const internalError = new InternalServerError(
|
||||
"An unexpected error occurred. Please try again later."
|
||||
);
|
||||
return res.status(internalError.statusCode).json({ error: internalError.message });
|
||||
}
|
||||
|
|
|
|||
12
src/utils/asyncHandler.ts
Normal file
12
src/utils/asyncHandler.ts
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
import { Request, Response, NextFunction } from "express";
|
||||
|
||||
|
||||
/**
|
||||
* Handles asyncronous rest calls
|
||||
* @param fn function to execute asyncronously
|
||||
*/
|
||||
export const asyncHandler =
|
||||
(fn: (req: Request, res: Response, next: NextFunction) => Promise<any>) =>
|
||||
(req: Request, res: Response, next: NextFunction) => {
|
||||
Promise.resolve(fn(req, res, next)).catch(next);
|
||||
};
|
||||
|
|
@ -1,10 +1,15 @@
|
|||
import jwt from "jsonwebtoken";
|
||||
import bcrypt from "bcrypt";
|
||||
import dotenv from "dotenv";
|
||||
import { AuthPayload } from "../dtos/AuthPayload.js";
|
||||
|
||||
dotenv.config();
|
||||
const { JWT_SECRET = "" } = process.env;
|
||||
|
||||
/**
|
||||
* Information on the current auth token containing the
|
||||
* token itself as well as its expiry date
|
||||
*/
|
||||
export class tokenInfo{
|
||||
constructor(token: string, expiryDate: Date){
|
||||
this.token = token;
|
||||
|
|
@ -14,22 +19,47 @@ export class tokenInfo{
|
|||
expiryDate: Date;
|
||||
}
|
||||
|
||||
/**
|
||||
* Responsible for handling of encrypted passwords and token handling
|
||||
*/
|
||||
export class encrypt {
|
||||
/**
|
||||
* Encrypts a password string
|
||||
* @param password Password string
|
||||
* @returns encrypted password string
|
||||
*/
|
||||
static async encryptpass(password: string) {
|
||||
return bcrypt.hashSync(password, 12);
|
||||
}
|
||||
static comparepassword(password: string, hashPassword: string) {
|
||||
return bcrypt.compareSync(password, hashPassword);
|
||||
}
|
||||
|
||||
static generateToken(payload: object) {
|
||||
/**
|
||||
* Compares a plain text password to an encrypted password
|
||||
* @param password plain text password
|
||||
* @param hashPassword ecrypted password
|
||||
* @returns Boolean indicating whether both passwords are identical
|
||||
*/
|
||||
static comparepassword(password: string, hashPassword: string) : boolean{
|
||||
return bcrypt.compareSync(password, hashPassword);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates an authentication token that will be valid for one day based on the payload provided
|
||||
* @param payload AuthPayload containing the userId
|
||||
* @returns tokenInfo containing authentication token and expiryDate
|
||||
*/
|
||||
static generateToken(payload: AuthPayload) : tokenInfo {
|
||||
let expiryDate = new Date();
|
||||
expiryDate.setDate(expiryDate.getDate() + 1); // 1 day
|
||||
const token = jwt.sign(payload, JWT_SECRET, { expiresIn: "1d" });
|
||||
return new tokenInfo(token, expiryDate);
|
||||
}
|
||||
|
||||
static verifyToken(token: string) {
|
||||
/**
|
||||
* Verifys the token and decrypts the payload
|
||||
* @param token authentiction token
|
||||
* @returns Payload of the token
|
||||
*/
|
||||
static verifyToken(token: string) : jwt.JwtPayload | string {
|
||||
return jwt.verify(token, JWT_SECRET);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue