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 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 {
|
||||
static async login(req, res) {
|
||||
try {
|
||||
@ -56,13 +112,8 @@ class LoginController {
|
||||
}
|
||||
|
||||
// Step 5: If credentials valid, allow login (do NOT increment rate limit)
|
||||
// Set refresh token as HTTP-only cookie
|
||||
res.cookie('refreshToken', result.refreshToken, {
|
||||
httpOnly: true,
|
||||
secure: process.env.NODE_ENV === 'production',
|
||||
sameSite: 'strict',
|
||||
expires: result.refreshTokenExpires
|
||||
});
|
||||
// Set refresh token as HTTP-only cookie (shared across .profit-planet.partners)
|
||||
res.cookie('refreshToken', result.refreshToken, buildRefreshCookieOptions(req, result.refreshTokenExpires));
|
||||
|
||||
// --- Send login notification email ---
|
||||
try {
|
||||
@ -101,15 +152,11 @@ class LoginController {
|
||||
logger.warn('No refresh token provided');
|
||||
return res.status(401).json({ success: false, message: 'No refresh token provided' });
|
||||
}
|
||||
|
||||
const result = await LoginService.refresh(refreshToken);
|
||||
|
||||
// FIX: Set new refresh token as HTTP-only cookie after rotation
|
||||
res.cookie('refreshToken', result.refreshToken, {
|
||||
httpOnly: true,
|
||||
secure: process.env.NODE_ENV === 'production',
|
||||
sameSite: 'strict',
|
||||
expires: result.refreshTokenExpires
|
||||
});
|
||||
// Always set the rotated refresh token cookie with consistent attributes.
|
||||
res.cookie('refreshToken', result.refreshToken, buildRefreshCookieOptions(req, result.refreshTokenExpires));
|
||||
|
||||
logger.info('Refresh token rotated', { userId: result.user.id });
|
||||
return res.status(200).json({
|
||||
@ -191,30 +238,15 @@ class LoginController {
|
||||
// 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 {
|
||||
res.cookie('refreshToken', '', {
|
||||
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 });
|
||||
res.cookie('refreshToken', '', buildRefreshCookieClearOptions(req));
|
||||
} catch (cookieErr) {
|
||||
logger.error('[LOGOUT] Error while setting expired cookie', { error: cookieErr });
|
||||
}
|
||||
|
||||
// Clear the cookie as well
|
||||
try {
|
||||
res.clearCookie('refreshToken', {
|
||||
httpOnly: true,
|
||||
secure: process.env.NODE_ENV === 'production',
|
||||
sameSite: 'strict',
|
||||
path: '/'
|
||||
});
|
||||
logger.info('[LOGOUT] res.clearCookie called', { headersSent: !!res.headersSent });
|
||||
res.clearCookie('refreshToken', buildRefreshCookieClearOptions(req));
|
||||
} catch (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');
|
||||
|
||||
// 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
|
||||
await connection.query(`
|
||||
CREATE TABLE IF NOT EXISTS email_verifications (
|
||||
|
||||
@ -2,6 +2,9 @@ const express = require('express');
|
||||
const path = require('path');
|
||||
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 UserSettingsController = require('../controller/auth/UserSettingsController');
|
||||
const ReferralTokenController = require('../controller/referral/ReferralTokenController');
|
||||
|
||||
@ -35,7 +35,10 @@ const upload = multer({ storage: multer.memoryStorage() });
|
||||
|
||||
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('/refresh', LoginController.refresh);
|
||||
router.post('/logout', LoginController.logout);
|
||||
|
||||
@ -3,11 +3,14 @@ const UnitOfWork = require('../database/UnitOfWork');
|
||||
const argon2 = require('argon2');
|
||||
|
||||
async function createAdminUser() {
|
||||
// const adminEmail = process.env.ADMIN_EMAIL || 'office@profit-planet.com';
|
||||
// const adminEmail = process.env.ADMIN_EMAIL || 'alexander.ibrahim.ai@gmail.com';
|
||||
const adminEmail = process.env.ADMIN_EMAIL || 'loki.aahi@gmail.com';
|
||||
const adminPassword = process.env.ADMIN_PASSWORD || 'Chalanger75$%';
|
||||
// const adminPassword = process.env.ADMIN_PASSWORD || 'W.profit-planet.com.2025';
|
||||
// Avoid hardcoding credentials in repo; require env vars.
|
||||
const adminEmail = process.env.ADMIN_EMAIL;
|
||||
const adminPassword = process.env.ADMIN_PASSWORD;
|
||||
|
||||
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 lastName = process.env.ADMIN_LAST_NAME || 'User';
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user