mirror of
https://github.com/rgrgogu/new_starr.git
synced 2026-09-27 00:12:54 +08:00
ready to test
Testing Signed-off-by: Kenneth Obsequio <k80308392@gmail.com>
This commit is contained in:
+149
-30
@@ -30,19 +30,19 @@ const mdl_Users = require('../models/users/users.mdl');
|
||||
const mdl_UserSessions = require('../models/users/user_sessions.mdl');
|
||||
const { mdl_UserGroups, mdl_UserGroupMembers } = require('../models/users/user_groups.mdl')
|
||||
const { generateTokens, verifyRefreshToken, hashToken, shouldRotateRefreshToken } = require('../utils/token.util');
|
||||
const { generateState, generateNonce, generatePKCE, buildAuthUrl, exchangeCode, verifyIdToken } = require('../utils/google_oidc.util');
|
||||
const { generateOTP, getOTPExpiry, isOTPExpired } = require('../utils/otp.util');
|
||||
const sendEmail = require('../services/email.service');
|
||||
const { onUserRegistered } = require('../services/achievements.service');
|
||||
const AdminNotification = require('../models/notifications/admin_notification.mdl');
|
||||
const UserNotification = require('../models/notifications/user_notification.mdl');
|
||||
const { NOTIFICATION_REGISTRY } = require('../data/notifications.data');
|
||||
const { sendEmail } = require('../services/email.service');
|
||||
const buildSessionInfo = require('../utils/session_info.util');
|
||||
const logActivity = require('../utils/logActivity.util');
|
||||
const R = require('../utils/response.util');
|
||||
|
||||
const EXCLUDED = ['password', 'otp_code', 'otp_expires_at', 'must_change_password', 'password_expires_at', 'createdAt', 'updatedAt', 'deletedAt', 'createdBy', 'updatedBy', 'deletedBy'];
|
||||
|
||||
// ─── Helpers ───────────────────────────────────────────────────────────────────
|
||||
const buildLoginInfo = (req) => ({
|
||||
date: new Date().toISOString(),
|
||||
ip_address: req.ip,
|
||||
device_info: req.headers['user-agent'] || 'unknown',
|
||||
});
|
||||
|
||||
const safeUser = (user, extraExclude = []) => {
|
||||
const u = user.toJSON ? user.toJSON() : { ...user };
|
||||
|
||||
@@ -100,7 +100,18 @@ exports.register = async (req, res) => {
|
||||
await sendEmail({ to: email, type: 'OTP', data: { otp } });
|
||||
|
||||
await transaction.commit();
|
||||
|
||||
|
||||
// Fire-and-forget: notify admins about the new group registration
|
||||
if (group) {
|
||||
AdminNotification.create({
|
||||
...NOTIFICATION_REGISTRY.user_registration.build({
|
||||
groupName: group.name,
|
||||
groupCode: group.group_code,
|
||||
userEmail: email,
|
||||
}),
|
||||
}).catch(err => console.error('[AUTH] Failed to emit user_registration notification:', err));
|
||||
}
|
||||
|
||||
return R.success(res, 'Registration successful. Please check your email for the OTP.', {
|
||||
email: user.email,
|
||||
}, 201);
|
||||
@@ -131,15 +142,38 @@ exports.verifyOTP = async (req, res) => {
|
||||
const { accessToken, refreshToken } = generateTokens(user);
|
||||
const session = await mdl_UserSessions.create({
|
||||
user_id: user.user_id,
|
||||
login_info: buildLoginInfo(req),
|
||||
login_info: await buildSessionInfo(req),
|
||||
refresh_token_hash: hashToken(refreshToken),
|
||||
is_active: true,
|
||||
}, { transaction });
|
||||
|
||||
await sendEmail({ to: email, type: "WELCOME", data: { name: email } });
|
||||
|
||||
await transaction.commit();
|
||||
|
||||
// Fire-and-forget: activity log
|
||||
logActivity(user.user_id, 'register');
|
||||
|
||||
// Fire-and-forget: achievements, welcome email, notification (do not block the response)
|
||||
onUserRegistered(user.user_id)
|
||||
.catch(err => console.error('[AUTH] Failed to grant achievements:', err));
|
||||
|
||||
sendEmail({ to: email, type: "WELCOME", data: { name: email } })
|
||||
.catch(err => console.error('[AUTH] Failed to send welcome email:', err));
|
||||
|
||||
mdl_UserGroupMembers.findOne({
|
||||
where: { user_id: user.user_id },
|
||||
include: [{ model: mdl_UserGroups, attributes: ['name', 'group_code'] }],
|
||||
}).then(membership => {
|
||||
const grp = membership?.UserGroup;
|
||||
return UserNotification.create({
|
||||
user_id: user.user_id,
|
||||
...NOTIFICATION_REGISTRY.welcome.build({
|
||||
groupName: grp?.name ?? null,
|
||||
groupCode: grp?.group_code ?? null,
|
||||
accType: user.acc_type,
|
||||
}),
|
||||
});
|
||||
}).catch(err => console.error('[AUTH] Failed to emit welcome notification:', err));
|
||||
|
||||
res.cookie('refreshToken', refreshToken, {
|
||||
httpOnly: true, // ← JS cannot read this
|
||||
secure: process.env.NODE_ENV === 'production',
|
||||
@@ -202,11 +236,13 @@ exports.login = async (req, res) => {
|
||||
const { accessToken, refreshToken } = generateTokens(user);
|
||||
const session = await mdl_UserSessions.create({
|
||||
user_id: user.user_id,
|
||||
login_info: buildLoginInfo(req),
|
||||
login_info: await buildSessionInfo(req),
|
||||
refresh_token_hash: hashToken(refreshToken),
|
||||
is_active: true,
|
||||
});
|
||||
|
||||
logActivity(user.user_id, 'login', { entityType: 'session', entityId: Number(session.session_id) });
|
||||
|
||||
res.cookie('refreshToken', refreshToken, {
|
||||
httpOnly: true, // ← JS cannot read this
|
||||
secure: process.env.NODE_ENV === 'production',
|
||||
@@ -225,28 +261,107 @@ exports.login = async (req, res) => {
|
||||
}
|
||||
};
|
||||
|
||||
// ─── Google OAuth Callback ─────────────────────────────────────────────────────
|
||||
// ─── Google OIDC — Redirect ────────────────────────────────────────────────────
|
||||
// Generates state, nonce, and PKCE verifier, stores them in a signed httpOnly
|
||||
// cookie, then redirects the browser to Google's authorization endpoint.
|
||||
exports.googleRedirect = (req, res) => {
|
||||
const state = generateState();
|
||||
const nonce = generateNonce();
|
||||
const { codeVerifier, codeChallenge } = generatePKCE();
|
||||
|
||||
// SameSite=Lax is required: the cookie must survive the cross-site redirect
|
||||
// back from Google (top-level GET navigations are allowed under Lax).
|
||||
res.cookie('_oauth', JSON.stringify({ state, nonce, codeVerifier }), {
|
||||
httpOnly: true,
|
||||
secure: process.env.NODE_ENV === 'production',
|
||||
sameSite: 'lax',
|
||||
maxAge: 10 * 60 * 1000, // 10 minutes — enough time to complete the flow
|
||||
signed: true,
|
||||
});
|
||||
|
||||
return res.redirect(buildAuthUrl(state, nonce, codeChallenge));
|
||||
};
|
||||
|
||||
// ─── Google OIDC — Callback ────────────────────────────────────────────────────
|
||||
// Verifies state (CSRF), exchanges the authorization code, verifies the ID token
|
||||
// (signature + nonce), finds or creates the user, sets the refresh cookie, then
|
||||
// redirects the browser to the frontend callback page.
|
||||
exports.googleCallback = async (req, res) => {
|
||||
const FRONTEND_URL = process.env.FRONTEND_URL;
|
||||
const CALLBACK_PAGE = `${FRONTEND_URL}/auth/callback/google`;
|
||||
|
||||
try {
|
||||
const user = req.user; // set by passport
|
||||
const { code, state, error } = req.query;
|
||||
|
||||
if (error) {
|
||||
return res.redirect(`${CALLBACK_PAGE}?error=${encodeURIComponent(error)}`);
|
||||
}
|
||||
|
||||
// Read and immediately clear the oauth state cookie.
|
||||
const rawCookie = req.signedCookies['_oauth'];
|
||||
res.clearCookie('_oauth');
|
||||
|
||||
if (!rawCookie) return res.redirect(`${CALLBACK_PAGE}?error=session_expired`);
|
||||
|
||||
const { state: expectedState, nonce, codeVerifier } = JSON.parse(rawCookie);
|
||||
|
||||
if (!state || state !== expectedState) {
|
||||
return res.redirect(`${CALLBACK_PAGE}?error=state_mismatch`);
|
||||
}
|
||||
|
||||
// Exchange authorization code → { id_token, access_token, ... }
|
||||
const tokens = await exchangeCode(code, codeVerifier);
|
||||
|
||||
// Verify ID token signature, audience, expiry, and nonce.
|
||||
const payload = await verifyIdToken(tokens.id_token, nonce);
|
||||
|
||||
// Find or auto-create the user.
|
||||
let user = await mdl_Users.findOne({ where: { email: payload.email } });
|
||||
if (!user) {
|
||||
user = await mdl_Users.create({
|
||||
email: payload.email,
|
||||
reg_type: 'google',
|
||||
acc_type: 'user',
|
||||
is_active: true,
|
||||
is_verified: true,
|
||||
personal_info: {
|
||||
name: {
|
||||
given_name: payload.given_name ?? '',
|
||||
last_name: payload.family_name ?? '',
|
||||
full_name: payload.name ?? '',
|
||||
},
|
||||
avatar: { url: payload.picture ?? null },
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (!user.is_active) {
|
||||
return res.redirect(`${CALLBACK_PAGE}?error=account_deactivated`);
|
||||
}
|
||||
|
||||
const { accessToken, refreshToken } = generateTokens(user);
|
||||
|
||||
await mdl_UserSessions.create({
|
||||
user_id: user.user_id,
|
||||
login_info: buildLoginInfo(req),
|
||||
const googleSession = await mdl_UserSessions.create({
|
||||
user_id: user.user_id,
|
||||
login_info: await buildSessionInfo(req),
|
||||
refresh_token_hash: hashToken(refreshToken),
|
||||
is_active: true,
|
||||
is_active: true,
|
||||
});
|
||||
|
||||
// In a real SPA: redirect with tokens in query or set httpOnly cookie
|
||||
return R.success(res, 'Google login successful.', {
|
||||
accessToken,
|
||||
refreshToken,
|
||||
user: safeUser(user),
|
||||
logActivity(user.user_id, 'login', { entityType: 'session', entityId: Number(googleSession.session_id), details: { reg_type: 'google' } });
|
||||
|
||||
res.cookie('refreshToken', refreshToken, {
|
||||
httpOnly: true,
|
||||
secure: process.env.NODE_ENV === 'production',
|
||||
sameSite: process.env.NODE_ENV === 'production' ? 'strict' : 'lax',
|
||||
maxAge: 7 * 24 * 60 * 60 * 1000,
|
||||
});
|
||||
|
||||
// Redirect to the frontend; App.jsx's restoreSession() will pick up the
|
||||
// refresh cookie and complete the login automatically.
|
||||
return res.redirect(CALLBACK_PAGE);
|
||||
} catch (err) {
|
||||
console.error('[AUTH] googleCallback error:', err);
|
||||
return R.error(res, 'Google authentication failed.', 500);
|
||||
console.error('[AUTH] googleCallback OIDC error:', err);
|
||||
return res.redirect(`${process.env.FRONTEND_URL}/auth/callback/google?error=auth_failed`);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -270,7 +385,7 @@ exports.refreshToken = async (req, res) => {
|
||||
// Check if refresh token is expired
|
||||
if (!shouldRotateRefreshToken(decoded)) {
|
||||
const { accessToken } = generateTokens(user);
|
||||
return R.success(res, 'Token refreshed.', { accessToken, user: safeUser(user) });
|
||||
return R.success(res, 'Token refreshed.', { accessToken, session_id: session.session_id, user: safeUser(user) });
|
||||
}
|
||||
|
||||
// ─── Rotate refresh token ───────────────────────────────────────────────────
|
||||
@@ -284,7 +399,7 @@ exports.refreshToken = async (req, res) => {
|
||||
maxAge: 7 * 24 * 60 * 60 * 1000,
|
||||
});
|
||||
|
||||
return R.success(res, 'Token refreshed.', { ...tokens, user: safeUser(user) });
|
||||
return R.success(res, 'Token refreshed.', { ...tokens, session_id: session.session_id, user: safeUser(user) });
|
||||
} catch (err) {
|
||||
console.error('[AUTH] refresh token error:', err);
|
||||
return R.error(res, 'Invalid or expired refresh token.', 401);
|
||||
@@ -297,11 +412,13 @@ exports.logout = async (req, res) => {
|
||||
const { session_id } = req.body;
|
||||
if (session_id) {
|
||||
await mdl_UserSessions.update(
|
||||
{ is_active: false, logout_info: buildLoginInfo(req) },
|
||||
{ is_active: false, logout_info: await buildSessionInfo(req) },
|
||||
{ where: { session_id, user_id: req.user.user_id } }
|
||||
);
|
||||
}
|
||||
|
||||
logActivity(req.user.user_id, 'logout', { entityType: 'session', entityId: session_id ? Number(session_id) : null });
|
||||
|
||||
res.clearCookie('refreshToken')
|
||||
res.clearCookie('_csrf')
|
||||
|
||||
@@ -330,6 +447,8 @@ exports.changePassword = async (req, res) => {
|
||||
// Invalidate all sessions to force re-login
|
||||
await mdl_UserSessions.update({ is_active: false }, { where: { user_id: user.user_id } });
|
||||
|
||||
logActivity(user.user_id, 'password_change');
|
||||
|
||||
return R.success(res, 'Password changed. All sessions have been invalidated. Please log in again.');
|
||||
} catch (err) {
|
||||
console.error('[AUTH] changePassword error:', err);
|
||||
|
||||
Reference in New Issue
Block a user