fix: cookie refresh stuff
This commit is contained in:
parent
e86986d40c
commit
33c584e68e
@ -5,6 +5,62 @@ const { logger } = require('../../middleware/logger');
|
|||||||
const UnitOfWork = require('../../database/UnitOfWork');
|
const UnitOfWork = require('../../database/UnitOfWork');
|
||||||
const db = require('../../database/database');
|
const db = require('../../database/database');
|
||||||
|
|
||||||
|
// ---- Refresh token cookie helpers (keep aligned with BFF/frontend expectations) ----
|
||||||
|
function normalizeSameSite(v) {
|
||||||
|
const raw = String(v || '').toLowerCase();
|
||||||
|
if (raw === 'none') return 'none';
|
||||||
|
if (raw === 'strict') return 'strict';
|
||||||
|
return 'lax'; // default
|
||||||
|
}
|
||||||
|
|
||||||
|
function getRefreshCookieDomain() {
|
||||||
|
// In dev/localhost, setting Domain breaks cookies; only set Domain in prod (or if explicitly provided).
|
||||||
|
const envDomain = process.env.COOKIE_DOMAIN || process.env.REFRESH_COOKIE_DOMAIN;
|
||||||
|
if (envDomain) return envDomain;
|
||||||
|
if (process.env.NODE_ENV === 'production') return '.profit-planet.partners';
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function shouldUseSecureCookie(req) {
|
||||||
|
// Works behind proxies if x-forwarded-proto is present; still prefer explicit env override.
|
||||||
|
const override = process.env.COOKIE_SECURE;
|
||||||
|
if (override != null) return String(override).toLowerCase() === 'true';
|
||||||
|
const xfProto = String(req.headers['x-forwarded-proto'] || '').toLowerCase();
|
||||||
|
const isHttps = req.secure || xfProto === 'https';
|
||||||
|
return process.env.NODE_ENV === 'production' || isHttps;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildRefreshCookieOptions(req, refreshTokenExpires) {
|
||||||
|
const sameSite = normalizeSameSite(process.env.COOKIE_SAMESITE || 'lax');
|
||||||
|
const secure = shouldUseSecureCookie(req);
|
||||||
|
|
||||||
|
// SameSite=None requires Secure; force it if configured that way.
|
||||||
|
const finalSecure = sameSite === 'none' ? true : secure;
|
||||||
|
|
||||||
|
const expires = refreshTokenExpires ? new Date(refreshTokenExpires) : undefined;
|
||||||
|
const maxAge = expires ? Math.max(0, expires.getTime() - Date.now()) : undefined;
|
||||||
|
|
||||||
|
return {
|
||||||
|
domain: getRefreshCookieDomain(), // e.g. .profit-planet.partners (shared across subdomains)
|
||||||
|
path: '/', // important for consistent clearing
|
||||||
|
httpOnly: true,
|
||||||
|
secure: finalSecure,
|
||||||
|
sameSite,
|
||||||
|
...(expires ? { expires } : {}),
|
||||||
|
...(typeof maxAge === 'number' ? { maxAge } : {})
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildRefreshCookieClearOptions(req) {
|
||||||
|
// Must match Domain+Path+Secure+SameSite used when setting.
|
||||||
|
const opts = buildRefreshCookieOptions(req, null);
|
||||||
|
return {
|
||||||
|
...opts,
|
||||||
|
expires: new Date(0),
|
||||||
|
maxAge: 0
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
class LoginController {
|
class LoginController {
|
||||||
static async login(req, res) {
|
static async login(req, res) {
|
||||||
try {
|
try {
|
||||||
@ -56,13 +112,8 @@ class LoginController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Step 5: If credentials valid, allow login (do NOT increment rate limit)
|
// Step 5: If credentials valid, allow login (do NOT increment rate limit)
|
||||||
// Set refresh token as HTTP-only cookie
|
// Set refresh token as HTTP-only cookie (shared across .profit-planet.partners)
|
||||||
res.cookie('refreshToken', result.refreshToken, {
|
res.cookie('refreshToken', result.refreshToken, buildRefreshCookieOptions(req, result.refreshTokenExpires));
|
||||||
httpOnly: true,
|
|
||||||
secure: process.env.NODE_ENV === 'production',
|
|
||||||
sameSite: 'strict',
|
|
||||||
expires: result.refreshTokenExpires
|
|
||||||
});
|
|
||||||
|
|
||||||
// --- Send login notification email ---
|
// --- Send login notification email ---
|
||||||
try {
|
try {
|
||||||
@ -101,15 +152,11 @@ class LoginController {
|
|||||||
logger.warn('No refresh token provided');
|
logger.warn('No refresh token provided');
|
||||||
return res.status(401).json({ success: false, message: 'No refresh token provided' });
|
return res.status(401).json({ success: false, message: 'No refresh token provided' });
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await LoginService.refresh(refreshToken);
|
const result = await LoginService.refresh(refreshToken);
|
||||||
|
|
||||||
// FIX: Set new refresh token as HTTP-only cookie after rotation
|
// Always set the rotated refresh token cookie with consistent attributes.
|
||||||
res.cookie('refreshToken', result.refreshToken, {
|
res.cookie('refreshToken', result.refreshToken, buildRefreshCookieOptions(req, result.refreshTokenExpires));
|
||||||
httpOnly: true,
|
|
||||||
secure: process.env.NODE_ENV === 'production',
|
|
||||||
sameSite: 'strict',
|
|
||||||
expires: result.refreshTokenExpires
|
|
||||||
});
|
|
||||||
|
|
||||||
logger.info('Refresh token rotated', { userId: result.user.id });
|
logger.info('Refresh token rotated', { userId: result.user.id });
|
||||||
return res.status(200).json({
|
return res.status(200).json({
|
||||||
@ -191,30 +238,15 @@ class LoginController {
|
|||||||
// continue to attempt to clear cookie even if service logout fails
|
// continue to attempt to clear cookie even if service logout fails
|
||||||
}
|
}
|
||||||
|
|
||||||
// Explicitly expire the refresh token cookie
|
// Clear cookie using SAME Domain+Path+SameSite+Secure
|
||||||
try {
|
try {
|
||||||
res.cookie('refreshToken', '', {
|
res.cookie('refreshToken', '', buildRefreshCookieClearOptions(req));
|
||||||
httpOnly: true,
|
|
||||||
secure: process.env.NODE_ENV === 'production',
|
|
||||||
sameSite: 'strict',
|
|
||||||
path: '/',
|
|
||||||
expires: new Date(0)
|
|
||||||
});
|
|
||||||
const setCookieHeader = res.getHeader && res.getHeader('Set-Cookie');
|
|
||||||
logger.info('[LOGOUT] Set-Cookie header after expiring cookie', { setCookieHeader });
|
|
||||||
} catch (cookieErr) {
|
} catch (cookieErr) {
|
||||||
logger.error('[LOGOUT] Error while setting expired cookie', { error: cookieErr });
|
logger.error('[LOGOUT] Error while setting expired cookie', { error: cookieErr });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clear the cookie as well
|
|
||||||
try {
|
try {
|
||||||
res.clearCookie('refreshToken', {
|
res.clearCookie('refreshToken', buildRefreshCookieClearOptions(req));
|
||||||
httpOnly: true,
|
|
||||||
secure: process.env.NODE_ENV === 'production',
|
|
||||||
sameSite: 'strict',
|
|
||||||
path: '/'
|
|
||||||
});
|
|
||||||
logger.info('[LOGOUT] res.clearCookie called', { headersSent: !!res.headersSent });
|
|
||||||
} catch (clearErr) {
|
} catch (clearErr) {
|
||||||
logger.error('[LOGOUT] Error while calling res.clearCookie', { error: clearErr });
|
logger.error('[LOGOUT] Error while calling res.clearCookie', { error: clearErr });
|
||||||
}
|
}
|
||||||
|
|||||||
@ -304,6 +304,17 @@ const createDatabase = async () => {
|
|||||||
`);
|
`);
|
||||||
console.log('✅ Refresh tokens table created/verified');
|
console.log('✅ Refresh tokens table created/verified');
|
||||||
|
|
||||||
|
// Ensure refresh token column length supports longer tokens (JWT/base64/etc.)
|
||||||
|
try {
|
||||||
|
await connection.query(`
|
||||||
|
ALTER TABLE refresh_tokens
|
||||||
|
MODIFY COLUMN token VARCHAR(512) NOT NULL;
|
||||||
|
`);
|
||||||
|
console.log('🔧 Ensured refresh_tokens.token is VARCHAR(512)');
|
||||||
|
} catch (e) {
|
||||||
|
console.log('ℹ️ refresh_tokens.token length ALTER skipped:', e.message);
|
||||||
|
}
|
||||||
|
|
||||||
// 6. email_verifications table: For email verification codes
|
// 6. email_verifications table: For email verification codes
|
||||||
await connection.query(`
|
await connection.query(`
|
||||||
CREATE TABLE IF NOT EXISTS email_verifications (
|
CREATE TABLE IF NOT EXISTS email_verifications (
|
||||||
|
|||||||
@ -2,6 +2,9 @@ const express = require('express');
|
|||||||
const path = require('path');
|
const path = require('path');
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
|
// NOTE: If the frontend ever calls backend routes directly with cookies,
|
||||||
|
// CORS must allow credentials and a non-* origin; otherwise refreshToken cookies won't be sent.
|
||||||
|
|
||||||
const authMiddleware = require('../middleware/authMiddleware');
|
const authMiddleware = require('../middleware/authMiddleware');
|
||||||
const UserSettingsController = require('../controller/auth/UserSettingsController');
|
const UserSettingsController = require('../controller/auth/UserSettingsController');
|
||||||
const ReferralTokenController = require('../controller/referral/ReferralTokenController');
|
const ReferralTokenController = require('../controller/referral/ReferralTokenController');
|
||||||
|
|||||||
@ -35,7 +35,10 @@ const upload = multer({ storage: multer.memoryStorage() });
|
|||||||
|
|
||||||
console.log('🛣️ Setting up POST routes');
|
console.log('🛣️ Setting up POST routes');
|
||||||
|
|
||||||
// auth POSTs (moved from routes/auth.js)
|
// auth POSTs (BFF/frontend contract notes):
|
||||||
|
// - /login MUST set refreshToken cookie (Domain=.profit-planet.partners, Path=/, HttpOnly, Secure in prod, SameSite=Lax by default)
|
||||||
|
// - /refresh MUST rotate + also Set-Cookie refreshToken=<new> with SAME attributes
|
||||||
|
// - /logout MUST revoke server-side + clear cookie using SAME Domain+Path+SameSite+Secure (Max-Age=0)
|
||||||
router.post('/login', LoginController.login);
|
router.post('/login', LoginController.login);
|
||||||
router.post('/refresh', LoginController.refresh);
|
router.post('/refresh', LoginController.refresh);
|
||||||
router.post('/logout', LoginController.logout);
|
router.post('/logout', LoginController.logout);
|
||||||
|
|||||||
@ -3,11 +3,14 @@ const UnitOfWork = require('../database/UnitOfWork');
|
|||||||
const argon2 = require('argon2');
|
const argon2 = require('argon2');
|
||||||
|
|
||||||
async function createAdminUser() {
|
async function createAdminUser() {
|
||||||
// const adminEmail = process.env.ADMIN_EMAIL || 'office@profit-planet.com';
|
// Avoid hardcoding credentials in repo; require env vars.
|
||||||
// const adminEmail = process.env.ADMIN_EMAIL || 'alexander.ibrahim.ai@gmail.com';
|
const adminEmail = process.env.ADMIN_EMAIL;
|
||||||
const adminEmail = process.env.ADMIN_EMAIL || 'loki.aahi@gmail.com';
|
const adminPassword = process.env.ADMIN_PASSWORD;
|
||||||
const adminPassword = process.env.ADMIN_PASSWORD || 'Chalanger75$%';
|
|
||||||
// const adminPassword = process.env.ADMIN_PASSWORD || 'W.profit-planet.com.2025';
|
if (!adminEmail || !adminPassword) {
|
||||||
|
throw new Error('ADMIN_EMAIL and ADMIN_PASSWORD must be set (refusing to use hardcoded defaults).');
|
||||||
|
}
|
||||||
|
|
||||||
const firstName = process.env.ADMIN_FIRST_NAME || 'Admin';
|
const firstName = process.env.ADMIN_FIRST_NAME || 'Admin';
|
||||||
const lastName = process.env.ADMIN_LAST_NAME || 'User';
|
const lastName = process.env.ADMIN_LAST_NAME || 'User';
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user