feat(mana-core-nestjs): add OptionalAuthGuard and Public decorator

- 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 <noreply@anthropic.com>
This commit is contained in:
Till-JS 2025-11-28 21:02:20 +01:00
parent a34a3418d2
commit c90c79d6b7
6 changed files with 100 additions and 0 deletions

View file

@ -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": {

View file

@ -0,0 +1,2 @@
export { CurrentUser, JwtPayload } from './current-user.decorator';
export { Public, IS_PUBLIC_KEY } from './public.decorator';

View file

@ -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);

View file

@ -0,0 +1,2 @@
export { AuthGuard } from './auth.guard';
export { OptionalAuthGuard } from './optional-auth.guard';

View file

@ -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<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 {
// 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;
}
}

View file

@ -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 {