mirror of
https://github.com/rgrgogu/new_starr.git
synced 2026-09-27 00:12:54 +08:00
+108
-15
@@ -28,6 +28,7 @@ const bcrypt = require('bcryptjs');
|
||||
const sequelize = require('../config/db.config')
|
||||
const mdl_Users = require('../models/users/users.mdl');
|
||||
const mdl_UserSessions = require('../models/users/user_sessions.mdl');
|
||||
const mdl_UserBans = require('../models/users/user_bans.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');
|
||||
@@ -106,7 +107,7 @@ exports.register = async (req, res) => {
|
||||
|
||||
await transaction.commit();
|
||||
|
||||
// Fire-and-forget: notify admins only for explicit group code registrations
|
||||
// Fire-and-forget: notify admins — explicit group or NOGRP fallback
|
||||
if (group) {
|
||||
AdminNotification.create({
|
||||
...NOTIFICATION_REGISTRY.user_registration.build({
|
||||
@@ -115,6 +116,13 @@ exports.register = async (req, res) => {
|
||||
userEmail: email,
|
||||
}),
|
||||
}).catch(err => console.error('[AUTH] Failed to emit user_registration notification:', err));
|
||||
} else if (enrollGroup) {
|
||||
AdminNotification.create({
|
||||
...NOTIFICATION_REGISTRY.nogrp_user_registered.build({
|
||||
userEmail: email,
|
||||
regType: 'system',
|
||||
}),
|
||||
}).catch(err => console.error('[AUTH] Failed to emit nogrp_user_registered notification:', err));
|
||||
}
|
||||
|
||||
return R.success(res, 'Registration successful. Please check your email for the OTP.', {
|
||||
@@ -138,7 +146,8 @@ exports.verifyOTP = async (req, res) => {
|
||||
if (!user) return R.error(res, 'User not found.', 404);
|
||||
if (user.is_verified) return R.error(res, 'Account already verified.', 400);
|
||||
|
||||
if (user.otp_code !== otp) return R.error(res, 'Invalid OTP.', 400);
|
||||
if (!crypto.timingSafeEqual(Buffer.from(user.otp_code ?? ''), Buffer.from(otp)))
|
||||
return R.error(res, 'Invalid OTP.', 400);
|
||||
if (isOTPExpired(user.otp_expires_at)) return R.error(res, 'OTP has expired. Please request a new one.', 400);
|
||||
|
||||
await user.update({ is_verified: true, otp_code: null, otp_expires_at: null }, { transaction });
|
||||
@@ -167,16 +176,30 @@ exports.verifyOTP = async (req, res) => {
|
||||
mdl_UserGroupMembers.findOne({
|
||||
where: { user_id: user.user_id },
|
||||
include: [{ model: mdl_UserGroups, attributes: ['name', 'group_code'] }],
|
||||
}).then(membership => {
|
||||
}).then(async 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,
|
||||
}),
|
||||
});
|
||||
const now = new Date();
|
||||
const notifications = [
|
||||
{
|
||||
user_id: user.user_id,
|
||||
...NOTIFICATION_REGISTRY.welcome.build({
|
||||
groupName: grp?.name ?? null,
|
||||
groupCode: grp?.group_code ?? null,
|
||||
accType: user.acc_type,
|
||||
}),
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
},
|
||||
];
|
||||
if (grp?.group_code === 'NOGRP') {
|
||||
notifications.push({
|
||||
user_id: user.user_id,
|
||||
...NOTIFICATION_REGISTRY.nogrp_welcome.build(),
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
});
|
||||
}
|
||||
return UserNotification.bulkCreate(notifications, { validate: false });
|
||||
}).catch(err => console.error('[AUTH] Failed to emit welcome notification:', err));
|
||||
|
||||
res.cookie('refreshToken', refreshToken, {
|
||||
@@ -188,7 +211,6 @@ exports.verifyOTP = async (req, res) => {
|
||||
|
||||
return R.success(res, 'Email verified successfully. You are now logged in.', {
|
||||
accessToken,
|
||||
refreshToken,
|
||||
session_id: session.session_id,
|
||||
user: safeUser(user),
|
||||
});
|
||||
@@ -235,6 +257,25 @@ exports.login = async (req, res) => {
|
||||
if (!user.is_active) return R.error(res, 'Account is deactivated.', 403);
|
||||
if (!user.is_verified) return R.error(res, 'Please verify your email first.', 403);
|
||||
|
||||
if (user.is_banned) {
|
||||
const stillBanned = !user.ban_expires_at || new Date() < new Date(user.ban_expires_at);
|
||||
if (stillBanned) {
|
||||
const activeBan = await mdl_UserBans.findOne({
|
||||
where: { user_id: user.user_id, is_lifted: false },
|
||||
order: [['banned_at', 'DESC']],
|
||||
attributes: ['reason', 'ban_type', 'expires_at'],
|
||||
});
|
||||
return R.error(res, 'Your account has been suspended.', 403, {
|
||||
banned: true,
|
||||
reason: activeBan?.reason ?? null,
|
||||
ban_type: activeBan?.ban_type ?? null,
|
||||
ban_expires_at: activeBan?.expires_at ?? null,
|
||||
});
|
||||
}
|
||||
// Expired temporary ban — auto-lift
|
||||
await user.update({ is_banned: false, ban_expires_at: null });
|
||||
}
|
||||
|
||||
const match = await bcrypt.compare(password, user.password);
|
||||
if (!match) return R.error(res, 'Invalid credentials.', 401);
|
||||
|
||||
@@ -321,7 +362,12 @@ exports.googleCallback = async (req, res) => {
|
||||
const payload = await verifyIdToken(tokens.id_token, nonce);
|
||||
|
||||
// Find or auto-create the user.
|
||||
let user = await mdl_Users.findOne({ where: { email: payload.email } });
|
||||
// Use paranoid:false so soft-deleted rows are visible — if one is blocking the email slot, free it first.
|
||||
let user = await mdl_Users.findOne({ where: { email: payload.email }, paranoid: false });
|
||||
if (user?.deletedAt) {
|
||||
await user.update({ email: `deleted_${user.user_id}@deleted.invalid` });
|
||||
user = null;
|
||||
}
|
||||
if (!user) {
|
||||
const t = await sequelize.transaction();
|
||||
try {
|
||||
@@ -353,9 +399,33 @@ exports.googleCallback = async (req, res) => {
|
||||
|
||||
await t.commit();
|
||||
|
||||
// Fire-and-forget: achievements + welcome notification for new Google user
|
||||
// Fire-and-forget: achievements + welcome notifications for new Google user
|
||||
onUserRegistered(user.user_id)
|
||||
.catch(err => console.error('[AUTH] googleCallback: Failed to grant achievements:', err));
|
||||
|
||||
const _now = new Date();
|
||||
UserNotification.bulkCreate([
|
||||
{
|
||||
user_id: user.user_id,
|
||||
...NOTIFICATION_REGISTRY.welcome.build({ groupName: null, groupCode: 'NOGRP', accType: 'user' }),
|
||||
createdAt: _now,
|
||||
updatedAt: _now,
|
||||
},
|
||||
{
|
||||
user_id: user.user_id,
|
||||
...NOTIFICATION_REGISTRY.nogrp_welcome.build(),
|
||||
createdAt: _now,
|
||||
updatedAt: _now,
|
||||
},
|
||||
], { validate: false })
|
||||
.catch(err => console.error('[AUTH] googleCallback: Failed to emit welcome notifications:', err));
|
||||
|
||||
AdminNotification.create({
|
||||
...NOTIFICATION_REGISTRY.nogrp_user_registered.build({
|
||||
userEmail: payload.email,
|
||||
regType: 'google',
|
||||
}),
|
||||
}).catch(err => console.error('[AUTH] googleCallback: Failed to emit nogrp_user_registered notification:', err));
|
||||
} catch (err) {
|
||||
await t.rollback();
|
||||
throw err;
|
||||
@@ -366,6 +436,23 @@ exports.googleCallback = async (req, res) => {
|
||||
return res.redirect(`${CALLBACK_PAGE}?error=account_deactivated`);
|
||||
}
|
||||
|
||||
if (user.is_banned) {
|
||||
const stillBanned = !user.ban_expires_at || new Date() < new Date(user.ban_expires_at);
|
||||
if (stillBanned) {
|
||||
const activeBan = await mdl_UserBans.findOne({
|
||||
where: { user_id: user.user_id, is_lifted: false },
|
||||
order: [['banned_at', 'DESC']],
|
||||
attributes: ['reason', 'ban_type', 'expires_at'],
|
||||
});
|
||||
const params = new URLSearchParams({ error: 'account_banned' });
|
||||
if (activeBan?.reason) params.set('reason', activeBan.reason);
|
||||
if (activeBan?.ban_type) params.set('ban_type', activeBan.ban_type);
|
||||
if (activeBan?.expires_at) params.set('expires_at', new Date(activeBan.expires_at).toISOString());
|
||||
return res.redirect(`${CALLBACK_PAGE}?${params.toString()}`);
|
||||
}
|
||||
await user.update({ is_banned: false, ban_expires_at: null });
|
||||
}
|
||||
|
||||
const { accessToken, refreshToken } = generateTokens(user);
|
||||
const googleSession = await mdl_UserSessions.create({
|
||||
user_id: user.user_id,
|
||||
@@ -409,6 +496,12 @@ exports.refreshToken = async (req, res) => {
|
||||
const user = await mdl_Users.findByPk(decoded.user_id);
|
||||
if (!user || !user.is_active) return R.error(res, 'User not found or deactivated.', 401);
|
||||
|
||||
if (user.is_banned) {
|
||||
const stillBanned = !user.ban_expires_at || new Date() < new Date(user.ban_expires_at);
|
||||
if (stillBanned) return R.error(res, 'Your account has been suspended.', 403, { banned: true });
|
||||
await user.update({ is_banned: false, ban_expires_at: null });
|
||||
}
|
||||
|
||||
// Check if refresh token is expired
|
||||
if (!shouldRotateRefreshToken(decoded)) {
|
||||
const { accessToken } = generateTokens(user);
|
||||
@@ -426,7 +519,7 @@ exports.refreshToken = async (req, res) => {
|
||||
maxAge: 7 * 24 * 60 * 60 * 1000,
|
||||
});
|
||||
|
||||
return R.success(res, 'Token refreshed.', { ...tokens, session_id: session.session_id, user: safeUser(user) });
|
||||
return R.success(res, 'Token refreshed.', { accessToken: tokens.accessToken, 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);
|
||||
|
||||
Reference in New Issue
Block a user