diff --git a/prisma/migrations/20250406115517_logout/migration.sql b/prisma/migrations/20250406115517_logout/migration.sql new file mode 100644 index 0000000..5b7b552 --- /dev/null +++ b/prisma/migrations/20250406115517_logout/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "users" ADD COLUMN "lastLogout" TIMESTAMP(3); diff --git a/prisma/schema/models/user.model.prisma b/prisma/schema/models/user.model.prisma index 35dc9f4..ea72b12 100644 --- a/prisma/schema/models/user.model.prisma +++ b/prisma/schema/models/user.model.prisma @@ -25,6 +25,7 @@ model User { passwordResetExpires DateTime? refreshToken String? lastLogin DateTime? + lastLogout DateTime? // Relations department Department? @relation(fields: [departmentId], references: [id]) diff --git a/src/controllers/auth.controller.js b/src/controllers/auth.controller.js index 6e13dd9..3d2b200 100644 --- a/src/controllers/auth.controller.js +++ b/src/controllers/auth.controller.js @@ -433,3 +433,45 @@ export const googleOAuthLogin = async (req, res) => { }); } }; + +/** + * @desc Log out user by invalidating their refresh token + * @route /api/auth/logout + * @method POST + * @access private (requires authentication) + */ +export const logout = async (req, res, next) => { + try { + // Get user ID from the authenticated request + const userId = req.user.id; + + // Find user + const user = await prisma.user.findUnique({ + where: { id: userId }, + select: { + id: true, + email: true, + username: true, + role: true, + isActive: true, + }, + }); + + if (!userId || !user || !user.isActive) { + return res.status(401).json({ message: 'Unauthorized' }); + } + + // Clear the refresh token in the database + await prisma.user.update({ + where: { id: userId }, + data: { + refreshToken: null, + lastLogout: new Date(), + }, + }); + + return res.status(200).json({ message: 'Logged out successfully' }); + } catch (error) { + next(error); + } +}; diff --git a/src/middlewares/auth.middleware.js b/src/middlewares/auth.middleware.js index ee46bb0..ab1561d 100644 --- a/src/middlewares/auth.middleware.js +++ b/src/middlewares/auth.middleware.js @@ -3,12 +3,13 @@ import jwt from 'jsonwebtoken'; export const verifyAccessToken = (req, res, next) => { // Extract token from "Bearer " format const authHeader = req.headers.authorization; - const token = authHeader && authHeader.split(' ')[1]; // ["Bearer", ""] - if (!token) { + if (!authHeader || !authHeader.startsWith('Bearer ')) { return res.status(401).json({ message: 'No token provided' }); } + const token = authHeader.split(' ')[1]; // ["Bearer", ""] + try { /* eslint no-undef: off */ const decoded = jwt.verify(token, process.env.JWT_ACCESS_SECRET); diff --git a/src/routes/auth.routes.js b/src/routes/auth.routes.js index a8b0ecf..7e9f2aa 100644 --- a/src/routes/auth.routes.js +++ b/src/routes/auth.routes.js @@ -9,8 +9,10 @@ import { verifyEmail, googleOAuthCallback, googleOAuthLogin, + logout, } from '../controllers/auth.controller.js'; import { apiLimiter } from '../utils/apiLimiter.utils.js'; +import { verifyAccessToken } from '../middlewares/auth.middleware.js'; const router = Router(); @@ -43,4 +45,7 @@ router.get( router.post('/auth/google', googleOAuthLogin); +// Logout +router.post('/api/auth/logout', verifyAccessToken, logout); + export default router;