Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions prisma/migrations/20250406115517_logout/migration.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "users" ADD COLUMN "lastLogout" TIMESTAMP(3);
1 change: 1 addition & 0 deletions prisma/schema/models/user.model.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ model User {
passwordResetExpires DateTime?
refreshToken String?
lastLogin DateTime?
lastLogout DateTime?

// Relations
department Department? @relation(fields: [departmentId], references: [id])
Expand Down
42 changes: 42 additions & 0 deletions src/controllers/auth.controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Copy link

Copilot AI Apr 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The logout function assumes req.user is present, but the verifyAccessToken middleware does not attach the decoded token to req. This may lead to a runtime error if req.user is undefined.

Copilot uses AI. Check for mistakes.

// 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);
}
};
5 changes: 3 additions & 2 deletions src/middlewares/auth.middleware.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,13 @@ import jwt from 'jsonwebtoken';
export const verifyAccessToken = (req, res, next) => {
// Extract token from "Bearer <token>" format
const authHeader = req.headers.authorization;
const token = authHeader && authHeader.split(' ')[1]; // ["Bearer", "<token>"]

if (!token) {
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return res.status(401).json({ message: 'No token provided' });
}

const token = authHeader.split(' ')[1]; // ["Bearer", "<token>"]

try {
/* eslint no-undef: off */
const decoded = jwt.verify(token, process.env.JWT_ACCESS_SECRET);
Expand Down
5 changes: 5 additions & 0 deletions src/routes/auth.routes.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand Down Expand Up @@ -43,4 +45,7 @@ router.get(

router.post('/auth/google', googleOAuthLogin);

// Logout
router.post('/api/auth/logout', verifyAccessToken, logout);

export default router;