import { Request, Response, NextFunction } from 'express'; import jwt from 'jsonwebtoken'; const AUTH_HOST = process.env.AUTH_HOST ?? 'https://account.rokojori.com'; const AUTH_INTERNAL_HOST = process.env.AUTH_INTERNAL_HOST ?? AUTH_HOST; const JWT_SECRET = process.env.JWT_SECRET ?? ''; const COOKIE_DOMAIN = process.env.COOKIE_DOMAIN ?? '.rokojori.com'; export interface JwtUser { userId: string; email: string; roles: string[]; products: string[]; settings: Record; } declare global { namespace Express { interface Request { user?: JwtUser; } } } function extractToken( req: Request ): string | undefined { const cookie = req.cookies?.accessToken as string | undefined; if ( cookie ) return cookie; const header = req.headers.authorization; if ( header?.startsWith( 'Bearer ' ) ) return header.slice( 7 ); return undefined; } function isApiRequest( req: Request ): boolean { return req.path.startsWith( '/api/' ); } function cookieOpts( maxAge: number ) { return { domain: COOKIE_DOMAIN, httpOnly: true, secure: process.env.NODE_ENV === 'production', sameSite: 'lax' as const, path: '/', maxAge }; } interface RefreshResult { accessToken: string; refreshToken: string; } async function tryRefresh( refreshToken: string ): Promise { const url = `${AUTH_INTERNAL_HOST}/api/auth/refresh`; console.log( '[auth] tryRefresh →', url ); try { const r = await fetch( url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify( { refreshToken } ) } ); console.log( '[auth] tryRefresh status:', r.status ); if ( !r.ok ) { const body = await r.text(); console.log( '[auth] tryRefresh error body:', body ); return null; } const data = await r.json() as Partial; if ( !data.accessToken || !data.refreshToken ) { console.log( '[auth] tryRefresh missing tokens in response:', Object.keys( data ) ); return null; } console.log( '[auth] tryRefresh succeeded' ); return { accessToken: data.accessToken, refreshToken: data.refreshToken }; } catch ( err ) { console.log( '[auth] tryRefresh fetch error:', err ); return null; } } export function jwtMiddleware( req: Request, res: Response, next: NextFunction ): void { const token = extractToken( req ); if ( !token ) { next(); return; } try { req.user = jwt.verify( token, JWT_SECRET ) as JwtUser; next(); } catch ( err: unknown ) { if ( !( err instanceof jwt.TokenExpiredError ) ) { next(); return; } if ( !isApiRequest( req ) ) { const redirect = encodeURIComponent( req.protocol + '://' + req.get( 'host' ) + req.originalUrl ); res.redirect( `${AUTH_HOST}/api/auth/refresh-session?redirect=${redirect}` ); return; } // API request with expired token — try transparent refresh via refreshToken cookie console.log( '[auth] expired token on API route:', req.path ); const refreshToken = req.cookies?.refreshToken as string | undefined; if ( !refreshToken ) { console.log( '[auth] no refreshToken cookie — cannot refresh' ); next(); return; } tryRefresh( refreshToken ).then( result => { if ( !result ) { console.log( '[auth] refresh failed, returning 401 for:', req.path ); next(); return; } res.cookie( 'accessToken', result.accessToken, cookieOpts( 60 * 60 * 1000 ) ); res.cookie( 'refreshToken', result.refreshToken, cookieOpts( 30 * 24 * 60 * 60 * 1000 ) ); try { req.user = jwt.verify( result.accessToken, JWT_SECRET ) as JwtUser; } catch { /* fall through — requireAuth will return 401 */ } next(); } ).catch( () => next() ); } } export function requireAuth( req: Request, res: Response, next: NextFunction ): void { if ( !req.user ) { res.status( 401 ).json( { error: 'Not authenticated' } ); return; } next(); }