managarten/packages/mana-core-nestjs-integration/src/guards/optional-auth.guard.ts
Till JS 49df3ead09 feat(taktik): add detail pages, duration rounding, confirmation dialogs
- Project detail page (/projects/[id]): stats, budget progress, inline
  edit, full entry list with billing value calculation
- Client detail page (/clients/[id]): stats, project cards, entry list,
  billing value summary
- Duration rounding: configurable increment (1-15 min) and method
  (up/down/nearest), applied automatically when timer stops
- ConfirmDialog component: reusable modal for destructive actions
- Confirmation required before deleting entries, projects, and clients
- 18 new rounding tests (67 total, all passing)
- i18n: added deleteConfirm keys for DE and EN

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 08:44:59 +02:00

117 lines
2.9 KiB
TypeScript

import { Injectable, CanActivate, ExecutionContext, Inject, Optional } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { MANA_CORE_OPTIONS } from '../mana-core.module';
import { ManaCoreModuleOptions } from '../interfaces/mana-core-options.interface';
interface TokenValidationResponse {
valid: boolean;
payload?: {
sub: string;
email: string;
role: string;
sessionId?: string;
sid?: string;
app_id?: string;
iat?: number;
exp?: number;
};
error?: string;
}
/**
* Optional auth guard - allows unauthenticated requests but still validates and extracts user info if token is present
*/
@Injectable()
export class OptionalAuthGuard implements CanActivate {
constructor(
@Optional()
@Inject(MANA_CORE_OPTIONS)
private readonly options?: ManaCoreModuleOptions,
@Optional()
private readonly configService?: ConfigService
) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
const request = context.switchToHttp().getRequest();
const token = this.extractTokenFromHeader(request);
if (!token) {
// No token - allow request but user will be undefined
request.user = null;
return true;
}
try {
const userData = await this.validateToken(token);
if (userData) {
request.user = userData;
request.accessToken = token;
if (this.options?.debug) {
console.log('[OptionalAuthGuard] User authenticated:', userData.sub);
}
} else {
request.user = null;
}
} catch (error) {
if (this.options?.debug) {
console.error('[OptionalAuthGuard] Token validation failed:', error);
}
// For optional auth, we allow the request to proceed even if token validation fails
request.user = null;
}
return true;
}
/**
* Validate token with Mana Core Auth service
*/
private async validateToken(token: string): Promise<any | null> {
const authUrl =
this.configService?.get<string>('MANA_CORE_AUTH_URL') ||
process.env.MANA_CORE_AUTH_URL ||
'http://localhost:3001';
try {
const response = await fetch(`${authUrl}/api/v1/auth/validate`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ token }),
});
if (!response.ok) {
return null;
}
const result = (await response.json()) as TokenValidationResponse;
if (!result.valid || !result.payload) {
return null;
}
return {
sub: result.payload.sub,
email: result.payload.email,
role: result.payload.role,
app_id: result.payload.app_id || this.options?.appId,
sessionId: result.payload.sessionId || result.payload.sid,
iat: result.payload.iat,
exp: result.payload.exp,
};
} catch {
return null;
}
}
private extractTokenFromHeader(request: any): string | undefined {
const authHeader = request.headers.authorization;
if (!authHeader) {
return undefined;
}
const [type, token] = authHeader.split(' ');
return type === 'Bearer' ? token : undefined;
}
}