diff --git a/backend/src/auth/auth.controller.ts b/backend/src/auth/auth.controller.ts index 59876f0..b9d1b2f 100644 --- a/backend/src/auth/auth.controller.ts +++ b/backend/src/auth/auth.controller.ts @@ -1,15 +1,19 @@ import { Controller, Post, + Get, Body, UnauthorizedException, HttpCode, HttpStatus, Req, + UseGuards, } from '@nestjs/common'; import { IsEmail, IsString, MinLength, IsOptional } from 'class-validator'; import { AuthService } from './auth.service'; import { TenantId } from '../tenant/tenant.decorator'; +import { JwtAuthGuard } from './jwt-auth.guard'; +import { CurrentUser } from './current-user.decorator'; class LoginDto { @IsEmail() @@ -111,4 +115,15 @@ export class AuthController { // This endpoint exists for consistency and potential future enhancements return { message: 'Logged out successfully' }; } + + @UseGuards(JwtAuthGuard) + @Get('me') + async me(@CurrentUser() user: any, @TenantId() tenantId: string) { + // Return the current authenticated user info + return { + id: user.userId, + email: user.email, + tenantId: tenantId || user.tenantId, + }; + } } diff --git a/frontend/middleware/auth.global.ts b/frontend/middleware/auth.global.ts index 7a447cf..d320ed8 100644 --- a/frontend/middleware/auth.global.ts +++ b/frontend/middleware/auth.global.ts @@ -12,16 +12,14 @@ export default defineNuxtRouteMiddleware(async (to, from) => { } const authMessage = useCookie('authMessage') - // Check for session cookie (HTTP-only cookie is checked server-side via API) + // Check for tenant cookie (set alongside session cookie on login) const tenantCookie = useCookie('routebox_tenant') + // Also check for session cookie (HTTP-only, but readable in SSR context) + const sessionCookie = useCookie('routebox_session') // Routes that don't need a toast message (user knows they need to login) const silentRoutes = ['/'] - // Quick check: if no tenant cookie, likely not authenticated - // The actual session cookie is HTTP-only and can't be read client-side - // For a full check, we'd call /api/auth/me, but that's expensive for every route - // On client side, check the reactive auth state if (import.meta.client) { const { isAuthenticated, checkAuth } = useAuth() @@ -46,9 +44,9 @@ export default defineNuxtRouteMiddleware(async (to, from) => { return navigateTo('/login') } - // Server-side: check for tenant cookie as a quick indicator - // If no tenant cookie, redirect to login - if (!tenantCookie.value) { + // Server-side: check for both session and tenant cookies + // The session cookie is HTTP-only but can be read in SSR context + if (!sessionCookie.value || !tenantCookie.value) { if (!silentRoutes.includes(to.path)) { authMessage.value = 'Please login to access this page' } diff --git a/frontend/server/api/auth/login.post.ts b/frontend/server/api/auth/login.post.ts index b5833c2..b3c57ee 100644 --- a/frontend/server/api/auth/login.post.ts +++ b/frontend/server/api/auth/login.post.ts @@ -56,15 +56,15 @@ export default defineEventHandler(async (event) => { setSessionCookie(event, access_token) // Set tenant ID cookie (readable by client for context) - if (tenantId) { - setTenantIdCookie(event, tenantId) - } + // Use tenantId from response, or fall back to subdomain + const tenantToStore = tenantId || subdomain + setTenantIdCookie(event, tenantToStore) // Return user info (but NOT the token - it's in HTTP-only cookie) return { success: true, user, - tenantId, + tenantId: tenantToStore, } } catch (error: any) { // Re-throw H3 errors diff --git a/frontend/server/utils/session.ts b/frontend/server/utils/session.ts index 59d548a..31569ad 100644 --- a/frontend/server/utils/session.ts +++ b/frontend/server/utils/session.ts @@ -1,5 +1,5 @@ import type { H3Event } from 'h3' -import { getCookie, setCookie, deleteCookie } from 'h3' +import { getCookie, setCookie, deleteCookie, getHeader } from 'h3' const SESSION_COOKIE_NAME = 'routebox_session' const SESSION_MAX_AGE = 60 * 60 * 24 * 7 // 7 days @@ -11,6 +11,25 @@ export interface SessionData { email: string } +/** + * Determine if the request is over a secure connection + * Checks both direct HTTPS and proxy headers + */ +function isSecureRequest(event: H3Event): boolean { + // Check x-forwarded-proto header (set by reverse proxies) + const forwardedProto = getHeader(event, 'x-forwarded-proto') + if (forwardedProto === 'https') { + return true + } + + // Check if NODE_ENV is production (assume HTTPS in production) + if (process.env.NODE_ENV === 'production') { + return true + } + + return false +} + /** * Get the session token from HTTP-only cookie */ @@ -22,11 +41,11 @@ export function getSessionToken(event: H3Event): string | null { * Set the session token in an HTTP-only cookie */ export function setSessionCookie(event: H3Event, token: string): void { - const isProduction = process.env.NODE_ENV === 'production' + const secure = isSecureRequest(event) setCookie(event, SESSION_COOKIE_NAME, token, { httpOnly: true, - secure: isProduction, + secure, sameSite: 'lax', maxAge: SESSION_MAX_AGE, path: '/', @@ -54,11 +73,11 @@ export function getTenantIdFromCookie(event: H3Event): string | null { * Set tenant ID cookie (readable by client for context) */ export function setTenantIdCookie(event: H3Event, tenantId: string): void { - const isProduction = process.env.NODE_ENV === 'production' + const secure = isSecureRequest(event) setCookie(event, 'routebox_tenant', tenantId, { httpOnly: false, // Allow client to read tenant context - secure: isProduction, + secure, sameSite: 'lax', maxAge: SESSION_MAX_AGE, path: '/',