import { CanActivate,ExecutionContext,ForbiddenException,Injectable } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import type { AuthenticatedRequest } from './auth.guard';
import { STEP_UP_ACTION } from './step-up.decorator';
import { DatabaseService } from '../../infrastructure/database/database.service';
@Injectable()
export class StepUpGuard implements CanActivate {
constructor(
private readonly reflector:Reflector,
private readonly database:DatabaseService,
) {}
async canActivate(context:ExecutionContext):Promise<boolean> {
const action=this.reflector.getAllAndOverride<string>(STEP_UP_ACTION,[
context.getHandler(),context.getClass(),
]);
if(!action) return true;
const req=context.switchToHttp().getRequest<AuthenticatedRequest & {organizationId?:string}>();
if(!req.auth) throw new ForbiddenException({code:'auth.step_up_required'});
const maxAge=Number(process.env.STEP_UP_MAX_AGE_SECONDS??600);
const authTime=req.auth.authTime??0;
const age=Math.floor(Date.now()/1000)-authTime;
const amr=req.auth.amr??[];
const hasStrongAuth=process.env.NODE_ENV!=='production'
? true
: amr.some((x:string)=>['mfa','otp','hwk','webauthn'].includes(x.toLowerCase()));
const state=await this.database.query<any>(
`select require_step_up_after from user_auth_security_state where user_account_id=$1`,
[req.auth.userId],
);
const forcedAfter=state[0]?.require_step_up_after
? Math.floor(new Date(state[0].require_step_up_after).getTime()/1000)
: 0;
const fresh=authTime>0 && age>=0 && age<=maxAge && authTime>=forcedAfter;
const allowed=fresh && hasStrongAuth;
await this.database.query(
`insert into privileged_action_events(
id,organization_id,user_account_id,action_key,auth_time,acr,amr_json,
correlation_id,result
) values(gen_random_uuid(),$1,$2,$3,$4,$5,$6::jsonb,$7,$8)`,
[
req.organizationId??null,req.auth.userId,action,
authTime?new Date(authTime*1000).toISOString():null,
req.auth.acr??null,JSON.stringify(amr),req.correlationId??null,
allowed?'allowed':'denied',
],
);
if(!allowed) {
throw new ForbiddenException({
code:'auth.step_up_required',
action,
maxAgeSeconds:maxAge,
});
}
return true;
}
}
