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 {
|
post {
|
||||||
url: http://localhost:4000/user
|
url: http://localhost:4000/user
|
||||||
body: json
|
body: json
|
||||||
auth: inherit
|
auth: bearer
|
||||||
}
|
|
||||||
|
|
||||||
headers {
|
|
||||||
Authorization: bearcer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjU2NGE5NjY0LTI2ZWYtNGMxMS1hNjIyLWU4MDI2MzczYmRkZCIsImlhdCI6MTc1ODc0MTM5MywiZXhwIjoxNzU4ODI3NzkzfQ.q33R9FfhGUIn92PTIIAmKmUnGxcLlv6om7KwiDD61Rc
|
|
||||||
}
|
}
|
||||||
|
|
||||||
body:json {
|
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 { LoginResponseDto } from "../dtos/LoginResponseDto.js";
|
||||||
import { LoginRequestDto } from "../dtos/LoginRequestDto.js";
|
import { LoginRequestDto } from "../dtos/LoginRequestDto.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Controller responsible for authentication, e.g., login or issueing a token with extended
|
||||||
|
* lifetime
|
||||||
|
*/
|
||||||
export class AuthController {
|
export class AuthController {
|
||||||
constructor(
|
constructor(
|
||||||
private userRepository: UserRepository,
|
private userRepository: UserRepository,
|
||||||
private mapper: UserDtoEntityMapper
|
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> {
|
async login(loginRequest : LoginRequestDto): Promise<LoginResponseDto> {
|
||||||
const userName :string|undefined = loginRequest.userName;
|
const userName :string|undefined = loginRequest.userName;
|
||||||
const password :string|undefined = loginRequest.password;
|
const password :string|undefined = loginRequest.password;
|
||||||
|
|
@ -25,6 +34,11 @@ export class AuthController {
|
||||||
if(!user){
|
if(!user){
|
||||||
throw new UnauthorizedError("Invalid username or password");
|
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
|
// Compare password
|
||||||
const passwordMatches = encrypt.comparepassword(password, user.password);
|
const passwordMatches = encrypt.comparepassword(password, user.password);
|
||||||
if (!passwordMatches) {
|
if (!passwordMatches) {
|
||||||
|
|
@ -33,7 +47,7 @@ export class AuthController {
|
||||||
|
|
||||||
// Create JWT
|
// Create JWT
|
||||||
const tokenInfo = encrypt.generateToken({
|
const tokenInfo = encrypt.generateToken({
|
||||||
id: user.id,
|
id: userId!, // ! to indicate that we've definitely checked for userId being defined
|
||||||
});
|
});
|
||||||
|
|
||||||
const responseDto = new LoginResponseDto();
|
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 { CreateUserRequestDto } from "../dtos/CreateUserRequestDto.js";
|
||||||
import { UserDto } from "../dtos/UserDto.js";
|
import { UserDto } from "../dtos/UserDto.js";
|
||||||
import { encrypt } from "../utils/encryptionUtils.js";
|
import { encrypt } from "../utils/encryptionUtils.js";
|
||||||
import { UserRepository } from "../repositories/UserRepository.js";
|
import { UserRepository } from "../repositories/UserRepository.js";
|
||||||
import { UserDtoEntityMapper } from "../mappers/UserDtoEntityMapper.js";
|
import { UserDtoEntityMapper } from "../mappers/UserDtoEntityMapper.js";
|
||||||
|
import { UUID } from "crypto";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Controls all user specific actions
|
||||||
|
*/
|
||||||
export class UserController {
|
export class UserController {
|
||||||
constructor(
|
constructor(
|
||||||
private userRepository: UserRepository,
|
private userRepository: UserRepository,
|
||||||
private mapper: UserDtoEntityMapper
|
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> {
|
async createUser(dto: CreateUserRequestDto): Promise<UserDto> {
|
||||||
// @todo make authorized! Create initial user!
|
|
||||||
// check mandatory fields
|
// check mandatory fields
|
||||||
if(!dto.userData){
|
if(!dto.userData){
|
||||||
throw new ValidationError("User data is required")
|
throw new ValidationError("User data is required")
|
||||||
|
|
@ -43,4 +51,21 @@ export class UserController {
|
||||||
|
|
||||||
return this.mapper.toDto(savedUser);
|
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 { CreateUserRequestDto } from "../dtos/CreateUserRequestDto.js";
|
||||||
import { UserRepository } from "../repositories/UserRepository.js";
|
import { UserRepository } from "../repositories/UserRepository.js";
|
||||||
import { UserDtoEntityMapper } from "../mappers/UserDtoEntityMapper.js";
|
import { UserDtoEntityMapper } from "../mappers/UserDtoEntityMapper.js";
|
||||||
import {
|
import { asyncHandler } from "../utils/asyncHandler.js";
|
||||||
ValidationError,
|
|
||||||
ConflictError,
|
|
||||||
NotFoundError,
|
|
||||||
InternalServerError,
|
|
||||||
} from "../errors/httpErrors.js";
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles all user related routes
|
||||||
|
*/
|
||||||
const router = Router();
|
const router = Router();
|
||||||
|
|
||||||
// Inject repo + mapper here
|
// Inject repo + mapper here
|
||||||
|
|
@ -17,22 +15,31 @@ const userRepository = new UserRepository();
|
||||||
const userMapper = new UserDtoEntityMapper();
|
const userMapper = new UserDtoEntityMapper();
|
||||||
const userController = new UserController(userRepository, userMapper);
|
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 requestDto: CreateUserRequestDto = req.body;
|
||||||
const responseDto = await userController.createUser(requestDto);
|
const responseDto = await userController.createUser(requestDto);
|
||||||
res.status(201).json(responseDto);
|
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 });
|
* Get user data for current user
|
||||||
} else {
|
*/
|
||||||
console.error("Unexpected error:", err);
|
router.get("/me",
|
||||||
const internalError = new InternalServerError("Some unexpected error occurred!");
|
asyncHandler(async (req, res) => {
|
||||||
res.status(internalError.statusCode).json({ error: internalError.message });
|
|
||||||
|
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;
|
export default router;
|
||||||
|
|
|
||||||
|
|
@ -1,39 +1,46 @@
|
||||||
export class ValidationError extends Error {
|
/**
|
||||||
statusCode = 400;
|
* Base class for all HTTP-related errors.
|
||||||
constructor(message: string) {
|
* 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);
|
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 {
|
export class ValidationError extends HttpError {
|
||||||
statusCode = 404;
|
|
||||||
constructor(message: string) {
|
constructor(message: string) {
|
||||||
super(message);
|
super(message, 400); // Bad Request
|
||||||
this.name = "NotFoundError";
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ConflictError extends Error {
|
export class UnauthorizedError extends HttpError {
|
||||||
statusCode = 409;
|
constructor(message: string = "Unauthorized") {
|
||||||
constructor(message: string) {
|
super(message, 401);
|
||||||
super(message);
|
|
||||||
this.name = "ConflictError";
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class InternalServerError extends Error {
|
export class NotFoundError extends HttpError {
|
||||||
statusCode = 500;
|
constructor(message: string = "Resource not found") {
|
||||||
constructor(message: string) {
|
super(message, 404);
|
||||||
super(message);
|
|
||||||
this.name = "InternalServerError";
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class UnauthorizedError extends Error {
|
export class ConflictError extends HttpError {
|
||||||
statusCode = 409;
|
constructor(message: string = "Conflict") {
|
||||||
constructor(message: string) {
|
super(message, 409);
|
||||||
super(message);
|
}
|
||||||
this.name = "UnauthorizedError";
|
}
|
||||||
|
|
||||||
|
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("/user", userRoutes);
|
||||||
// app.use("/recipe", recipeRoutes);
|
// app.use("/recipe", recipeRoutes);
|
||||||
|
|
||||||
|
// Error handling for all rest-calls
|
||||||
|
// must come last!
|
||||||
|
app.use(errorHandler);
|
||||||
|
|
||||||
console.log("auth and user routes added")
|
console.log("auth and user routes added")
|
||||||
|
// catch all other routes
|
||||||
app.get(/(.*)/, (req: Request, res: Response, next: NextFunction) => {
|
app.get(/(.*)/, (req: Request, res: Response, next: NextFunction) => {
|
||||||
res.status(400).json({ message: "Bad Request" });
|
res.status(400).json({ message: "Bad Request" });
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -2,14 +2,14 @@ import { NextFunction, Request, Response } from "express";
|
||||||
import jwt from "jsonwebtoken";
|
import jwt from "jsonwebtoken";
|
||||||
import dotenv from "dotenv";
|
import dotenv from "dotenv";
|
||||||
import { authBasicRoute } from "../endpoints/AuthPoint.js";
|
import { authBasicRoute } from "../endpoints/AuthPoint.js";
|
||||||
|
import { AuthPayload } from "../dtos/AuthPayload.js";
|
||||||
|
|
||||||
dotenv.config();
|
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 {
|
declare global {
|
||||||
namespace Express {
|
namespace Express {
|
||||||
interface Request {
|
interface Request {
|
||||||
currentUser?: string | jwt.JwtPayload;
|
currentUser?: AuthPayload;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -46,10 +46,10 @@ export const authentication = (
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const decoded = jwt.verify(token, JWT_SECRET);
|
const decoded = jwt.verify(token, JWT_SECRET) as AuthPayload;
|
||||||
req.currentUser = decoded;
|
req.currentUser = decoded;
|
||||||
next();
|
next();
|
||||||
} catch (err) {
|
} catch {
|
||||||
return res.status(401).json({ message: "Unauthorized" });
|
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,
|
req: Request,
|
||||||
res: Response,
|
res: Response,
|
||||||
next: NextFunction
|
next: NextFunction
|
||||||
) => {
|
) {
|
||||||
console.error(`Error: ${error.message}`);
|
if (err instanceof HttpError) {
|
||||||
return res.status(500).json({ message: "Internal server error" });
|
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 jwt from "jsonwebtoken";
|
||||||
import bcrypt from "bcrypt";
|
import bcrypt from "bcrypt";
|
||||||
import dotenv from "dotenv";
|
import dotenv from "dotenv";
|
||||||
|
import { AuthPayload } from "../dtos/AuthPayload.js";
|
||||||
|
|
||||||
dotenv.config();
|
dotenv.config();
|
||||||
const { JWT_SECRET = "" } = process.env;
|
const { JWT_SECRET = "" } = process.env;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Information on the current auth token containing the
|
||||||
|
* token itself as well as its expiry date
|
||||||
|
*/
|
||||||
export class tokenInfo{
|
export class tokenInfo{
|
||||||
constructor(token: string, expiryDate: Date){
|
constructor(token: string, expiryDate: Date){
|
||||||
this.token = token;
|
this.token = token;
|
||||||
|
|
@ -14,22 +19,47 @@ export class tokenInfo{
|
||||||
expiryDate: Date;
|
expiryDate: Date;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Responsible for handling of encrypted passwords and token handling
|
||||||
|
*/
|
||||||
export class encrypt {
|
export class encrypt {
|
||||||
|
/**
|
||||||
|
* Encrypts a password string
|
||||||
|
* @param password Password string
|
||||||
|
* @returns encrypted password string
|
||||||
|
*/
|
||||||
static async encryptpass(password: string) {
|
static async encryptpass(password: string) {
|
||||||
return bcrypt.hashSync(password, 12);
|
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();
|
let expiryDate = new Date();
|
||||||
expiryDate.setDate(expiryDate.getDate() + 1); // 1 day
|
expiryDate.setDate(expiryDate.getDate() + 1); // 1 day
|
||||||
const token = jwt.sign(payload, JWT_SECRET, { expiresIn: "1d" });
|
const token = jwt.sign(payload, JWT_SECRET, { expiresIn: "1d" });
|
||||||
return new tokenInfo(token, expiryDate);
|
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);
|
return jwt.verify(token, JWT_SECRET);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue