فهرست منبع

Feature: Add logout functionality and improve login error handling with Redis caching

emrecevik106 1 ماه پیش
والد
کامیت
75278b2723

+ 5 - 2
src/actions/auth/index.ts

@@ -1,6 +1,9 @@
 export {
-    default as login 
+    default as login
 } from "./login";
 export {
-    default as register 
+    default as register
 } from "./register";
+export {
+    default as logout
+} from "./logout";

+ 11 - 5
src/actions/auth/login/index.ts

@@ -12,16 +12,20 @@ import {
     LoginResult,
     LoginInput
 } from "./types";
+import redis from "../../../config/redis";
+import {
+    formatValidationErrors
+} from "../../../utils";
 
 export const login = async (input: LoginInput): Promise<LoginResult> => {
     const dto = plainToInstance(LoginInput, input);
     const errors = await validate(dto);
 
     if (errors.length > 0) {
-        const message = Object.values(errors[0].constraints!)[0];
+        const formattedErrors = formatValidationErrors(errors);
         return {
             code: 400,
-            message,
+            message: formattedErrors[0],
         };
     }
 
@@ -63,7 +67,7 @@ export const login = async (input: LoginInput): Promise<LoginResult> => {
         {
             companyName: user.companyName,
             fullName: user.fullName,
-            userID: user._id,
+            userId: user._id,
             mail: user.mail
         },
         process.env.JWT_SECRET as string,
@@ -72,11 +76,13 @@ export const login = async (input: LoginInput): Promise<LoginResult> => {
         }
     );
 
+    await redis.setex(`user:${user._id.toString()}`, 14400, accessToken);
+
     const refreshToken = jwt.sign(
         {
             companyName: user.companyName,
             fullName: user.fullName,
-            userID: user._id,
+            userId: user._id,
             mail: user.mail
         },
         process.env.JWT_REFRESH_SECRET as string || process.env.JWT_SECRET as string,
@@ -97,7 +103,7 @@ export const login = async (input: LoginInput): Promise<LoginResult> => {
             user: {
                 phoneNumber: user.phoneNumber,
                 companyName: user.companyName,
-                userID: user._id.toString(),
+                userId: user._id.toString(),
                 firstName: user.firstName,
                 lastName: user.lastName,
                 fullName: user.fullName,

+ 1 - 1
src/actions/auth/login/types.ts

@@ -32,7 +32,7 @@ export interface LoginResult {
             firstName: string;
             lastName: string;
             fullName: string;
-            userID: string;
+            userId: string;
             mail: string;
         };
     };

+ 30 - 0
src/actions/auth/logout/index.ts

@@ -0,0 +1,30 @@
+import {
+    User
+} from "../../../models/User";
+import redis from "../../../config/redis";
+import {
+    LogoutResult
+} from "./types";
+
+export const logout = async (userId: string, token: string): Promise<LogoutResult> => {
+    try {
+        await User.findByIdAndUpdate(userId, {
+            refreshToken: null
+        });
+
+        await redis.del(`user:${userId}`);
+
+        return {
+            message: "Logout successful",
+            code: 200,
+        };
+    } catch (error) {
+        console.error("Logout action error:", error);
+        return {
+            message: "Logout failed",
+            code: 500,
+        };
+    }
+};
+
+export default logout;

+ 4 - 0
src/actions/auth/logout/types.ts

@@ -0,0 +1,4 @@
+export interface LogoutResult {
+    message: string;
+    code: number;
+}

+ 6 - 6
src/config/redis.ts

@@ -1,14 +1,14 @@
 import Redis from "ioredis";
 
-export const redis = new Redis({
-    host: process.env.REDIS_HOST || "localhost",
-    port: Number(process.env.REDIS_PORT) || 6379,
-});
+const redis = new Redis(process.env.REDIS_URL || "redis://localhost:6379");
 
 redis.on("connect", () => {
-    console.log("Redis Connected");
+    console.log("Redis Connected Successfully");
 });
 
 redis.on("error", (error) => {
     console.error("Redis connection error:", error);
-});
+    process.exit(1);
+});
+
+export default redis;

+ 51 - 5
src/controllers/authController.ts

@@ -3,18 +3,24 @@ import {
     Request
 } from "express";
 import {
-    register as _register 
+    register as _register
 } from "../actions/auth/register";
 import {
-    login as _login 
+    login as _login
 } from "../actions/auth/login";
+import {
+    logout as _logout
+} from "../actions/auth/logout";
+import {
+    AuthRequest
+} from "../middlewares/authMiddleware";
 
 export const register = async (req: Request, res: Response): Promise<void> => {
     try {
         const {
             phoneNumber,
             companyName,
-            firstName, 
+            firstName,
             lastName,
             password,
             mail
@@ -23,7 +29,7 @@ export const register = async (req: Request, res: Response): Promise<void> => {
         const result = await _register({
             phoneNumber,
             companyName,
-            firstName, 
+            firstName,
             lastName,
             password,
             mail
@@ -38,7 +44,7 @@ export const register = async (req: Request, res: Response): Promise<void> => {
         console.error("Register error:", error);
         res.status(500).json({
             message: "Internal server error",
-            code: 500, 
+            code: 500,
         });
     }
 };
@@ -71,4 +77,44 @@ export const login = async (req: Request, res: Response): Promise<void> => {
                 code: 500,
             });
     }
+};
+
+export const logout = async (req: AuthRequest, res: Response): Promise<void> => {
+    try {
+        const context = req.context;
+
+        if (!context) {
+            res.status(401).json({
+                message: "Unauthorized: Missing context",
+                code: 401
+            });
+            return;
+        }
+
+        const {
+            userId,
+            token
+        } = context;
+
+        if (!userId || !token) {
+            res.status(401).json({
+                message: "Unauthorized: Missing user information",
+                code: 401
+            });
+            return;
+        }
+
+        const result = await _logout(userId, token);
+
+        res.status(result.code).json({
+            message: result.message,
+            code: result.code,
+        });
+    } catch (error) {
+        console.error("Logout controller error:", error);
+        res.status(500).json({
+            message: "Server error",
+            code: 500,
+        });
+    }
 };

+ 3 - 1
src/index.ts

@@ -3,9 +3,10 @@ import cors from "cors";
 import "reflect-metadata";
 import dotenv from "dotenv";
 import {
-    connectDB 
+    connectDB
 } from "./config/db";
 import routes from "./routes";
+import redis from "./config/redis";
 
 dotenv.config();
 
@@ -19,6 +20,7 @@ app.use("/api", routes);
 
 const start = async () => {
     await connectDB();
+    await redis.ping();
     app.listen(PORT, () => {
         console.log(`Server running on port ${PORT}`);
     });

+ 10 - 20
src/middlewares/authMiddleware.ts

@@ -1,16 +1,12 @@
 import {
-    Request, Response, NextFunction 
+    Request, Response, NextFunction
 } from "express";
 import jwt from "jsonwebtoken";
+import redis from "../config/redis";
 import {
-    redis 
-} from "../config/redis";
-import {
-    User 
+    User
 } from "../models/User";
-
 export interface AuthRequest extends Request {
-    userId?: string;
     context?: {
         userId: string;
         token: string;
@@ -34,16 +30,14 @@ export const authMiddleware = async (req: AuthRequest, res: Response, next: Next
             decoded = jwt.verify(token, process.env.JWT_SECRET as string) as { userId: string };
         } catch (err) {
             res.status(401).json({
-                message: "expired-token",
-                code: 401
+                message: "expired-token", code: 401
             });
             return;
         }
 
         if (!decoded || !decoded.userId) {
             res.status(401).json({
-                message: "invalid-token",
-                code: 401
+                message: "invalid-token", code: 401
             });
             return;
         }
@@ -51,16 +45,14 @@ export const authMiddleware = async (req: AuthRequest, res: Response, next: Next
         const cachedToken = await redis.get(decoded.userId);
         if (!cachedToken) {
             res.status(401).json({
-                message: "expired-token",
-                code: 401
+                message: "expired-token", code: 401
             });
             return;
         }
 
         if (cachedToken !== token) {
             res.status(401).json({
-                message: "invalid-token",
-                code: 401
+                message: "invalid-token", code: 401
             });
             return;
         }
@@ -68,8 +60,7 @@ export const authMiddleware = async (req: AuthRequest, res: Response, next: Next
         const user = await User.findById(decoded.userId);
         if (!user) {
             res.status(401).json({
-                message: "user-not-found",
-                code: 401
+                message: "user-not-found", code: 401
             });
             return;
         }
@@ -78,12 +69,11 @@ export const authMiddleware = async (req: AuthRequest, res: Response, next: Next
             userId: decoded.userId,
             token: token
         };
-        
         next();
+
     } catch (error) {
         res.status(401).json({
-            message: "invalid-token",
-            code: 401
+            message: "invalid-token", code: 401
         });
     }
 };

+ 5 - 1
src/routes/authRoutes.ts

@@ -2,7 +2,9 @@ import {
     Router 
 } from "express";
 import {
-    register, login 
+    register,
+    login,
+    logout
 } from "../controllers/authController";
 import {
     authMiddleware, 
@@ -10,6 +12,8 @@ import {
 } from "../middlewares/authMiddleware";
 
 const router = Router();
+
+router.post("/logout", authMiddleware, logout);
 router.post("/register", register);
 router.post("/login", login);
 

+ 1 - 1
src/utils/index.ts

@@ -1,5 +1,5 @@
 import {
-    ValidationError 
+    ValidationError
 } from "class-validator";
 
 export const formatValidationErrors = (errors: ValidationError[]): string[] => {