mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-27 14:37:43 +02:00
- 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>
117 lines
2.9 KiB
TypeScript
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;
|
|
}
|
|
}
|