pushy

Signed-off-by: Kenneth Obsequio <k80308392@gmail.com>
This commit is contained in:
2026-06-28 11:29:07 +08:00
parent 463a8d3978
commit 89acdfc239
67 changed files with 2736 additions and 306 deletions
+108 -15
View File
@@ -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);