first commit

This commit is contained in:
2026-10-03 13:40:26 +02:00
parent 732a5b5a16
commit 566f415fa7
555 changed files with 62704 additions and 0 deletions

View File

@@ -0,0 +1,56 @@
import {
CanActivate,
ExecutionContext,
ForbiddenException,
Injectable,
UnauthorizedException,
} from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import {
IdentityService,
InvalidAccessTokenError,
} from '@task-stream/api/identity/application';
import { extractBearerToken, getAuthRequest } from './auth-request';
import { IS_PUBLIC, REQUIRED_APP_ROLES } from './auth.decorators';
/** Global guard: every HTTP/GraphQL operation needs a valid Pireon token unless @Public(). */
@Injectable()
export class AuthGuard implements CanActivate {
constructor(
private readonly reflector: Reflector,
private readonly identity: IdentityService,
) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
const targets = [context.getHandler(), context.getClass()];
if (this.reflector.getAllAndOverride<boolean>(IS_PUBLIC, targets))
return true;
const req = getAuthRequest(context);
// WebSocket subscriptions are authenticated once in connection_init.
if (!req.user) {
const token = extractBearerToken(req.headers['authorization']);
if (!token) throw new UnauthorizedException('Missing bearer token');
try {
req.user = await this.identity.resolveSession(token);
req.accessToken = token;
} catch (error) {
if (error instanceof InvalidAccessTokenError)
throw new UnauthorizedException(error.message);
throw error;
}
}
const roles = this.reflector.getAllAndOverride<string[]>(
REQUIRED_APP_ROLES,
targets,
);
const user = req.user;
if (roles?.length && user && !roles.some((role) => user.hasAppRole(role))) {
throw new ForbiddenException(
`Requires one of the app roles: ${roles.join(', ')}`,
);
}
return true;
}
}