diff --git a/controller/login/LoginController.js b/controller/login/LoginController.js index 91b3f29..f80e68e 100644 --- a/controller/login/LoginController.js +++ b/controller/login/LoginController.js @@ -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 }); } diff --git a/database/createDb.js b/database/createDb.js index 2cfd2aa..c5c78fa 100644 --- a/database/createDb.js +++ b/database/createDb.js @@ -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 ( diff --git a/routes/getRoutes.js b/routes/getRoutes.js index faee98e..14c0d8b 100644 --- a/routes/getRoutes.js +++ b/routes/getRoutes.js @@ -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'); diff --git a/routes/postRoutes.js b/routes/postRoutes.js index 1325377..a362a8e 100644 --- a/routes/postRoutes.js +++ b/routes/postRoutes.js @@ -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= 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); diff --git a/scripts/createAdminUser.js b/scripts/createAdminUser.js index 0cdcf4c..5e6ae72 100644 --- a/scripts/createAdminUser.js +++ b/scripts/createAdminUser.js @@ -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';