From c90c79d6b76654cf6e1e71305ea0a7ea5d016236 Mon Sep 17 00:00:00 2001 From: Till-JS <101404291+Till-JS@users.noreply.github.com> Date: Fri, 28 Nov 2025 21:02:20 +0100 Subject: [PATCH] feat(mana-core-nestjs): add OptionalAuthGuard and Public decorator MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add OptionalAuthGuard for endpoints that allow unauthenticated access - Add Public decorator to mark routes as public - Export new guards and decorators from package - Add subpath exports for guards and decorators 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .../mana-core-nestjs-integration/package.json | 10 +++ .../src/decorators/index.ts | 2 + .../src/decorators/public.decorator.ts | 8 ++ .../src/guards/index.ts | 2 + .../src/guards/optional-auth.guard.ts | 76 +++++++++++++++++++ .../mana-core-nestjs-integration/src/index.ts | 2 + 6 files changed, 100 insertions(+) create mode 100644 packages/mana-core-nestjs-integration/src/decorators/index.ts create mode 100644 packages/mana-core-nestjs-integration/src/decorators/public.decorator.ts create mode 100644 packages/mana-core-nestjs-integration/src/guards/index.ts create mode 100644 packages/mana-core-nestjs-integration/src/guards/optional-auth.guard.ts diff --git a/packages/mana-core-nestjs-integration/package.json b/packages/mana-core-nestjs-integration/package.json index 73857fb9f..97f119a7d 100644 --- a/packages/mana-core-nestjs-integration/package.json +++ b/packages/mana-core-nestjs-integration/package.json @@ -9,6 +9,16 @@ "types": "./dist/index.d.ts", "import": "./dist/index.js", "require": "./dist/index.js" + }, + "./guards": { + "types": "./dist/guards/index.d.ts", + "import": "./dist/guards/index.js", + "require": "./dist/guards/index.js" + }, + "./decorators": { + "types": "./dist/decorators/index.d.ts", + "import": "./dist/decorators/index.js", + "require": "./dist/decorators/index.js" } }, "scripts": { diff --git a/packages/mana-core-nestjs-integration/src/decorators/index.ts b/packages/mana-core-nestjs-integration/src/decorators/index.ts new file mode 100644 index 000000000..83895afa6 --- /dev/null +++ b/packages/mana-core-nestjs-integration/src/decorators/index.ts @@ -0,0 +1,2 @@ +export { CurrentUser, JwtPayload } from './current-user.decorator'; +export { Public, IS_PUBLIC_KEY } from './public.decorator'; diff --git a/packages/mana-core-nestjs-integration/src/decorators/public.decorator.ts b/packages/mana-core-nestjs-integration/src/decorators/public.decorator.ts new file mode 100644 index 000000000..35d552c95 --- /dev/null +++ b/packages/mana-core-nestjs-integration/src/decorators/public.decorator.ts @@ -0,0 +1,8 @@ +import { SetMetadata } from '@nestjs/common'; + +export const IS_PUBLIC_KEY = 'isPublic'; + +/** + * Decorator to mark a route as public (no authentication required) + */ +export const Public = () => SetMetadata(IS_PUBLIC_KEY, true); diff --git a/packages/mana-core-nestjs-integration/src/guards/index.ts b/packages/mana-core-nestjs-integration/src/guards/index.ts new file mode 100644 index 000000000..e9e9bb171 --- /dev/null +++ b/packages/mana-core-nestjs-integration/src/guards/index.ts @@ -0,0 +1,2 @@ +export { AuthGuard } from './auth.guard'; +export { OptionalAuthGuard } from './optional-auth.guard'; diff --git a/packages/mana-core-nestjs-integration/src/guards/optional-auth.guard.ts b/packages/mana-core-nestjs-integration/src/guards/optional-auth.guard.ts new file mode 100644 index 000000000..339df2212 --- /dev/null +++ b/packages/mana-core-nestjs-integration/src/guards/optional-auth.guard.ts @@ -0,0 +1,76 @@ +import { + Injectable, + CanActivate, + ExecutionContext, + Inject, + Optional, +} from '@nestjs/common'; +import * as jwt from 'jsonwebtoken'; +import { MANA_CORE_OPTIONS } from '../mana-core.module'; +import { ManaCoreModuleOptions } from '../interfaces/mana-core-options.interface'; + +/** + * Optional auth guard - allows unauthenticated requests but still extracts user info if token is present + */ +@Injectable() +export class OptionalAuthGuard implements CanActivate { + constructor( + @Optional() + @Inject(MANA_CORE_OPTIONS) + private readonly options?: ManaCoreModuleOptions + ) {} + + async canActivate(context: ExecutionContext): Promise { + 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 { + // Decode the token to extract user information + const decoded = jwt.decode(token) as jwt.JwtPayload | null; + + if (decoded && decoded.sub) { + // Attach user info to request + request.user = { + sub: decoded.sub, + email: decoded.email || '', + role: decoded.role || 'user', + app_id: decoded.app_id, + iat: decoded.iat, + exp: decoded.exp, + }; + + // Store raw token for downstream services + request.accessToken = token; + + if (this.options?.debug) { + console.log('[OptionalAuthGuard] User authenticated:', decoded.sub); + } + } else { + request.user = null; + } + } catch (error) { + if (this.options?.debug) { + console.error('[OptionalAuthGuard] Token decode failed:', error); + } + request.user = null; + } + + return true; + } + + 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; + } +} diff --git a/packages/mana-core-nestjs-integration/src/index.ts b/packages/mana-core-nestjs-integration/src/index.ts index 3fae76370..9b7b04c84 100644 --- a/packages/mana-core-nestjs-integration/src/index.ts +++ b/packages/mana-core-nestjs-integration/src/index.ts @@ -10,9 +10,11 @@ export { // Guards export { AuthGuard } from './guards/auth.guard'; +export { OptionalAuthGuard } from './guards/optional-auth.guard'; // Decorators export { CurrentUser, JwtPayload } from './decorators/current-user.decorator'; +export { Public, IS_PUBLIC_KEY } from './decorators/public.decorator'; // Services export {