Implement backend API and database services in Docker setup

- Added a new `api` service for the NestJS backend, including health checks and dependencies on PostgreSQL, Redis, and MinIO.
- Introduced PostgreSQL and Redis services with health checks and configurations for data persistence.
- Added MinIO for S3-compatible object storage and a one-shot service to initialize required buckets.
- Updated the Nginx configuration to proxy requests to the new backend API and MinIO storage.
- Enhanced the Dockerfile to support the new API environment variables and configurations.
- Updated the `package.json` and `package-lock.json` to include new dependencies for QR code generation and other utilities.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Dorian
2026-02-12 20:14:39 +00:00
parent f19fd6feef
commit cdd24a5def
478 changed files with 55355 additions and 529 deletions

View File

@@ -0,0 +1,38 @@
import { NostrSessionService } from '../nostr-session.service';
import { UsersService } from 'src/users/users.service';
describe('NostrSessionService', () => {
const originalSecret = process.env.NOSTR_JWT_SECRET;
const originalRefresh = process.env.NOSTR_JWT_REFRESH_SECRET;
const usersService = {
findUserByNostrPubkey: jest.fn(),
} as unknown as UsersService;
beforeAll(() => {
process.env.NOSTR_JWT_SECRET = 'test-secret';
process.env.NOSTR_JWT_REFRESH_SECRET = 'test-refresh-secret';
process.env.NOSTR_JWT_TTL = '60';
process.env.NOSTR_JWT_REFRESH_TTL = '120';
});
afterAll(() => {
process.env.NOSTR_JWT_SECRET = originalSecret;
process.env.NOSTR_JWT_REFRESH_SECRET = originalRefresh;
});
it('issues and verifies session tokens', async () => {
const service = new NostrSessionService(usersService);
const { accessToken, refreshToken, expiresIn } =
await service.issueTokens('pubkey');
expect(expiresIn).toBe(60);
const accessPayload = service.verifyAccessToken(accessToken);
expect(accessPayload.sub).toBe('pubkey');
expect(accessPayload.typ).toBe('nostr-session');
const refreshPayload = service.verifyRefreshToken(refreshToken);
expect(refreshPayload.sub).toBe('pubkey');
expect(refreshPayload.typ).toBe('nostr-refresh');
});
});

View File

@@ -0,0 +1,9 @@
import { Injectable } from '@nestjs/common';
@Injectable()
export class AuthConfig {
public userPoolId: string = process.env.COGNITO_USER_POOL_ID;
public clientId: string = process.env.COGNITO_CLIENT_ID;
public region: string = process.env.AWS_REGION;
public authority = `https://cognito-idp.${process.env.AWS_REGION}.amazonaws.com/${process.env.COGNITO_USER_POOL_ID}`;
}

View File

@@ -0,0 +1,47 @@
import { Test, TestingModule } from '@nestjs/testing';
import { AuthController } from './auth.controller';
import { AuthService } from './auth.service';
import { MailService } from '../mail/mail.service';
import { UsersService } from '../users/users.service';
import { FilmmakersService } from '../filmmakers/filmmakers.service';
import { NostrSessionService } from './nostr-session.service';
// Mock MailService
const mockMailService = {
sendEmail: jest.fn(),
};
const mockAuthService = {};
const mockUsersService = {};
const mockFilmmakersService = {};
const mockNostrSessionService = {};
describe('AuthController', () => {
let controller: AuthController;
process.env.COGNITO_USER_POOL_ID = 'dummy_user_pool_id';
process.env.COGNITO_CLIENT_ID = 'dummy_client_id';
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [AuthController],
providers: [
{ provide: AuthService, useValue: mockAuthService },
{ provide: MailService, useValue: mockMailService },
{ provide: UsersService, useValue: mockUsersService },
{ provide: FilmmakersService, useValue: mockFilmmakersService },
{ provide: NostrSessionService, useValue: mockNostrSessionService },
],
}).compile();
controller = module.get<AuthController>(AuthController);
});
it('should be defined', () => {
expect(controller).toBeDefined();
});
});

View File

@@ -0,0 +1,161 @@
import {
BadRequestException,
Body,
Controller,
Get,
Post,
UseGuards,
Req,
UnauthorizedException,
} from '@nestjs/common';
import { AuthService } from './auth.service';
import { JwtAuthGuard } from './guards/jwt.guard';
import { User } from './user.decorator';
import { UsersService } from 'src/users/users.service';
import { FilmmakersService } from 'src/filmmakers/filmmakers.service';
import { RequestUser } from './dto/request/request-user.interface';
import { UserDTO } from 'src/users/dto/response/user.dto';
import { RegisterDTO } from './dto/request/register.dto';
import { ValidateSessionDTO } from './dto/request/validate-session.dto';
import { RegisterOneTimePasswordDTO } from './dto/request/register-otp.dto';
import { Request } from 'express';
import { NostrAuthGuard } from 'src/nostr-auth/nostr-auth.guard';
import { NostrSessionService } from './nostr-session.service';
import { RefreshNostrSessionDto } from './dto/request/refresh-nostr-session.dto';
import { NostrSessionJwtGuard } from './guards/nostr-session-jwt.guard';
import { HybridAuthGuard } from './guards/hybrid-auth.guard';
@Controller('auth')
export class AuthController {
constructor(
private readonly authService: AuthService,
private readonly userService: UsersService,
private readonly filmmakerService: FilmmakersService,
private readonly nostrSessionService: NostrSessionService,
) {}
@Post('register')
async register(@Body() registerDTO: RegisterDTO) {
try {
const user = await this.userService.createUser({
userDto: registerDTO,
userType: registerDTO.type,
});
if (registerDTO.type === 'audience') return user;
await this.filmmakerService.create({
professionalName: registerDTO.professionalName,
userId: user.id,
});
return user;
} catch (error) {
throw new BadRequestException(error.message);
}
}
// Register user for OTP flow if not exists, only for audience
@Post('otp/init')
async initOTP(@Body() registerDTO: RegisterOneTimePasswordDTO) {
try {
let user = await this.userService.findUserByEmail(registerDTO.email);
if (user) return { email: user.email };
// Register user
const cognitoUser = await this.authService.registerUser(registerDTO);
// Create user in the database
user = await this.userService.createUser({
userDto: {
cognitoId: cognitoUser.UserSub,
email: registerDTO.email,
legalName: registerDTO.email, // Audience not required legal name
},
userType: 'audience',
});
return { email: user.email };
} catch (error) {
throw new BadRequestException(error.message);
}
}
@Get('me')
@UseGuards(HybridAuthGuard)
async getMe(@Req() request: Request) {
const userFromGuard = (request as RequestUser).user;
if (userFromGuard) {
return new UserDTO(userFromGuard);
}
if (request.nostrPubkey) {
const user = await this.userService.findUserByNostrPubkey(
request.nostrPubkey,
);
if (user) {
return new UserDTO(user);
}
}
throw new UnauthorizedException();
}
@Post('validate-session')
@UseGuards(JwtAuthGuard)
async validateSession(
@Body() validateSessionDTO: ValidateSessionDTO,
@User() user: RequestUser['user'],
) {
return this.authService.validateSession(validateSessionDTO, user.email);
}
@Post('nostr/session')
@UseGuards(NostrAuthGuard)
async createNostrSession(@Req() request: Request) {
if (!request.nostrPubkey) {
throw new UnauthorizedException('Missing Nostr pubkey');
}
return await this.nostrSessionService.issueTokens(request.nostrPubkey);
}
@Post('nostr/refresh')
async refreshNostrSession(@Body() body: RefreshNostrSessionDto) {
const payload = this.nostrSessionService.verifyRefreshToken(
body.refreshToken,
);
return await this.nostrSessionService.issueTokens(payload.sub);
}
@Post('nostr/logout')
@UseGuards(NostrSessionJwtGuard)
logout() {
// Stateless JWT; returning success is equivalent to logout for the client.
return { success: true };
}
@Post('nostr/link')
@UseGuards(JwtAuthGuard, NostrAuthGuard)
async linkNostrPubkey(
@User() user: RequestUser['user'],
@Req() request: Request,
) {
if (!request.nostrPubkey) {
throw new UnauthorizedException('Missing Nostr pubkey');
}
try {
const updatedUser = await this.userService.linkNostrPubkey(
user.id,
request.nostrPubkey,
);
return { nostrPubkey: updatedUser.nostrPubkey };
} catch (error) {
throw new BadRequestException(error.message);
}
}
@Post('nostr/unlink')
@UseGuards(JwtAuthGuard)
async unlinkNostrPubkey(@User() user: RequestUser['user']) {
const updated = await this.userService.unlinkNostrPubkey(user.id);
return { nostrPubkey: updated.nostrPubkey };
}
}

View File

@@ -0,0 +1,46 @@
import { Global, Module, forwardRef } from '@nestjs/common';
import { AuthService } from './auth.service';
import { AuthController } from './auth.controller';
import { PassportModule } from '@nestjs/passport';
import { JwtStrategy } from './jwt.strategy';
import { ConfigModule } from '@nestjs/config';
import { FilmmakersModule } from 'src/filmmakers/filmmakers.module';
import { SubscriptionsModule } from 'src/subscriptions/subscriptions.module';
import { NostrAuthModule } from 'src/nostr-auth/nostr-auth.module';
import { JwtAuthGuard } from './guards/jwt.guard';
import { TokenAuthGuard } from './guards/token.guard';
import { HybridAuthGuard } from './guards/hybrid-auth.guard';
import { NostrSessionService } from './nostr-session.service';
import { NostrSessionJwtGuard } from './guards/nostr-session-jwt.guard';
import { UsersModule } from 'src/users/users.module';
@Global()
@Module({
imports: [
PassportModule.register({ defaultStrategy: 'jwt' }),
ConfigModule,
FilmmakersModule,
SubscriptionsModule,
NostrAuthModule,
forwardRef(() => UsersModule),
],
providers: [
JwtStrategy,
AuthService,
JwtAuthGuard,
TokenAuthGuard,
HybridAuthGuard,
NostrSessionService,
NostrSessionJwtGuard,
],
controllers: [AuthController],
exports: [
AuthService,
JwtAuthGuard,
TokenAuthGuard,
HybridAuthGuard,
NostrSessionService,
NostrSessionJwtGuard,
],
})
export class AuthModule {}

View File

@@ -0,0 +1,71 @@
import {
BadRequestException,
Inject,
Injectable,
Logger,
} from '@nestjs/common';
import { MailService } from 'src/mail/mail.service';
/**
* Auth service.
* Cognito has been removed. Authentication is now handled via:
* 1. Nostr NIP-98 HTTP Auth (primary method)
* 2. Nostr session JWT tokens (for subsequent requests)
*
* The Cognito methods are stubbed to maintain API compatibility
* while the migration to fully Nostr-based auth is completed.
*/
@Injectable()
export class AuthService {
private readonly logger = new Logger(AuthService.name);
constructor(@Inject(MailService) private readonly mailService: MailService) {
this.logger.log(
'AuthService initialized (Cognito disabled, Nostr-only mode)',
);
}
async registerUser(registerRequest: { email: string; password?: string }) {
this.logger.warn(
'registerUser called -- Cognito is disabled. Use Nostr auth instead.',
);
return {
UserSub: 'nostr-user',
UserConfirmed: true,
};
}
async authenticateUser(user: { email: string; password: string }) {
this.logger.warn(
'authenticateUser called -- Cognito is disabled. Use Nostr auth instead.',
);
throw new BadRequestException(
'Cognito authentication is disabled. Please use Nostr login.',
);
}
async verifyMfaCode(session: string, code: string, email: string) {
throw new BadRequestException('MFA is disabled in Nostr-only mode.');
}
async validateSession(
request: { code: string; session?: string; type?: string },
email: string,
) {
throw new BadRequestException(
'Session validation via Cognito is disabled. Use Nostr auth.',
);
}
async adminConfirmUser(email: string) {
this.logger.warn('adminConfirmUser called -- no-op in Nostr-only mode.');
return { success: true };
}
async deleteUserFromCognito(email: string) {
this.logger.warn(
'deleteUserFromCognito called -- no-op in Nostr-only mode.',
);
return {};
}
}

View File

@@ -0,0 +1,6 @@
import { IsString } from 'class-validator';
export class RefreshNostrSessionDto {
@IsString()
refreshToken: string;
}

View File

@@ -0,0 +1,11 @@
import { IsEmail, IsNotEmpty, IsString } from 'class-validator';
export class RegisterOneTimePasswordDTO {
@IsNotEmpty()
@IsString()
password: string;
@IsNotEmpty()
@IsEmail()
email: string;
}

View File

@@ -0,0 +1,29 @@
import {
IsNotEmpty,
IsString,
IsEmail,
IsEnum,
IsOptional,
} from 'class-validator';
export class RegisterDTO {
@IsNotEmpty()
@IsString()
legalName: string;
@IsNotEmpty()
@IsOptional()
@IsString()
professionalName?: string;
@IsNotEmpty()
@IsEmail()
email: string;
@IsString()
cognitoId: string;
@IsNotEmpty()
@IsEnum(['filmmaker', 'audience'])
type: 'filmmaker' | 'audience';
}

View File

@@ -0,0 +1,6 @@
import { Request } from 'express';
import { User } from 'src/users/entities/user.entity';
export interface RequestUser extends Request {
user: User;
}

View File

@@ -0,0 +1,15 @@
import { IsNotEmpty, IsString, IsEnum, IsOptional } from 'class-validator';
export class ValidateSessionDTO {
@IsNotEmpty()
@IsString()
code: string;
@IsString()
@IsOptional()
session?: string;
@IsNotEmpty()
@IsEnum(['password', 'mfa'])
type: 'password' | 'mfa';
}

View File

@@ -0,0 +1,38 @@
import {
Injectable,
CanActivate,
ExecutionContext,
UnauthorizedException,
Logger,
} from '@nestjs/common';
import { timingSafeEqual } from 'node:crypto';
@Injectable()
export class AdminAuthGuard implements CanActivate {
canActivate(context: ExecutionContext): boolean {
try {
const request = context.switchToHttp().getRequest();
const token = this.extractTokenFromHeader(request);
if (!token) {
throw new UnauthorizedException('No token provided');
}
if (
!timingSafeEqual(
Buffer.from(token),
Buffer.from(process.env.ADMIN_API_KEY),
)
) {
throw new UnauthorizedException('Invalid token');
}
return true;
} catch (error) {
Logger.error(`Error validating token: ${error.message}`);
throw new UnauthorizedException();
}
}
private extractTokenFromHeader(request: any): string | undefined {
const [type, token] = request.headers.authorization?.split(' ') ?? [];
return type === 'Bearer' ? token : undefined;
}
}

View File

@@ -0,0 +1,93 @@
import {
CanActivate,
ExecutionContext,
Injectable,
Logger,
UnauthorizedException,
} from '@nestjs/common';
import { Request } from 'express';
import { NostrAuthGuard } from '../../nostr-auth/nostr-auth.guard';
import { JwtAuthGuard } from './jwt.guard';
import { NostrSessionJwtGuard } from './nostr-session-jwt.guard';
import { AuthGuard } from '@nestjs/passport';
interface GuardResult {
success: boolean;
error?: Error;
}
@Injectable()
export class HybridAuthGuard extends AuthGuard('hybrid') {
constructor(
private readonly nostrAuthGuard: NostrAuthGuard,
private readonly nostrSessionJwtGuard: NostrSessionJwtGuard,
private readonly jwtAuthGuard: JwtAuthGuard,
) {
super();
}
async canActivate(context: ExecutionContext): Promise<boolean> {
const request = context.switchToHttp().getRequest<Request>();
const authorization = this.normalizeAuthorizationHeader(request);
const isNostrRequest = authorization?.toLowerCase().startsWith('nostr ');
let nostrResult: GuardResult | undefined;
if (isNostrRequest) {
nostrResult = await this.tryActivate(this.nostrAuthGuard, context);
if (nostrResult.success) {
return true;
}
}
const nostrJwtResult = await this.tryActivate(
this.nostrSessionJwtGuard,
context,
);
if (nostrJwtResult.success) {
return true;
}
const jwtResult = await this.tryActivate(this.jwtAuthGuard, context);
if (jwtResult.success) {
return true;
}
Logger.log(
`HybridAuthGuard failed: isNostrRequest=${isNostrRequest}, nostrError=${nostrResult?.error?.message}, nostrJwtError=${nostrJwtResult.error?.message}, jwtError=${jwtResult.error?.message}`,
);
if (isNostrRequest && nostrResult?.error) {
throw nostrResult.error;
}
if (jwtResult.error) {
throw jwtResult.error;
}
if (nostrResult?.error) {
throw nostrResult.error;
}
if (nostrJwtResult.error) {
throw nostrJwtResult.error;
}
throw new UnauthorizedException('Authentication failed');
}
private async tryActivate(
guard: CanActivate,
context: ExecutionContext,
): Promise<GuardResult> {
try {
const result = await guard.canActivate(context);
return { success: Boolean(result) };
} catch (error) {
return { success: false, error: error as Error };
}
}
private normalizeAuthorizationHeader(request: Request): string | undefined {
const header = request.headers.authorization;
return Array.isArray(header) ? header[0] : header;
}
}

View File

@@ -0,0 +1,27 @@
import {
ExecutionContext,
Injectable,
Logger,
UnauthorizedException,
} from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
/**
* JWT Auth Guard stub.
* Cognito JWT verification has been removed.
* This guard now always rejects -- all auth should go through
* NostrSessionJwtGuard or NostrAuthGuard via the HybridAuthGuard.
*
* Kept for API compatibility with endpoints that still reference JwtAuthGuard.
*/
@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {
async canActivate(context: ExecutionContext): Promise<boolean> {
Logger.warn(
'JwtAuthGuard.canActivate called -- Cognito is disabled. Use Nostr auth.',
);
throw new UnauthorizedException(
'Cognito authentication is disabled. Use Nostr login.',
);
}
}

View File

@@ -0,0 +1,49 @@
import {
ExecutionContext,
Injectable,
UnauthorizedException,
} from '@nestjs/common';
import { Request } from 'express';
import { NostrSessionService } from '../nostr-session.service';
import { UsersService } from '../../users/users.service';
import { AuthGuard } from '@nestjs/passport';
@Injectable()
export class NostrSessionJwtGuard extends AuthGuard('nostr-session-jwt') {
constructor(
private readonly sessionService: NostrSessionService,
private readonly usersService: UsersService,
) {
super();
}
async canActivate(context: ExecutionContext): Promise<boolean> {
const request = context
.switchToHttp()
.getRequest<Request & { user?: unknown }>();
const token = this.extractTokenFromHeader(request);
if (!token) {
throw new UnauthorizedException('No token provided');
}
try {
const payload = this.sessionService.verifyAccessToken(token);
request.nostrPubkey = payload.sub;
if (payload.uid) {
const user = await this.usersService.findUsersById(payload.uid);
request.user = user;
}
return true;
} catch (error) {
if (error instanceof UnauthorizedException) {
throw error;
}
throw new UnauthorizedException('Invalid token');
}
}
private extractTokenFromHeader(request: Request): string | undefined {
const [type, token] = request.headers.authorization?.split(' ') ?? [];
return type === 'Bearer' ? token : undefined;
}
}

View File

@@ -0,0 +1,38 @@
import {
Injectable,
CanActivate,
ExecutionContext,
UnauthorizedException,
Logger,
} from '@nestjs/common';
import { timingSafeEqual } from 'node:crypto';
@Injectable()
export class TokenAuthGuard implements CanActivate {
canActivate(context: ExecutionContext): boolean {
try {
const request = context.switchToHttp().getRequest();
const token = this.extractTokenFromHeader(request);
if (!token) {
throw new UnauthorizedException('No token provided');
}
if (
!timingSafeEqual(
Buffer.from(token),
Buffer.from(process.env.TRANSCODING_API_KEY),
)
) {
throw new UnauthorizedException('Invalid token');
}
return true;
} catch (error) {
Logger.error(`Error validating token: ${error.message}`);
throw new UnauthorizedException();
}
}
private extractTokenFromHeader(request: any): string | undefined {
const [type, token] = request.headers.authorization?.split(' ') ?? [];
return type === 'Bearer' ? token : undefined;
}
}

View File

@@ -0,0 +1,31 @@
import { ExtractJwt, Strategy } from 'passport-jwt';
import { PassportStrategy } from '@nestjs/passport';
import { Injectable, Logger } from '@nestjs/common';
/**
* JWT Strategy stub.
* Cognito JWKS verification has been removed.
* This strategy uses a dummy secret and will never actually validate
* because JwtAuthGuard now always rejects.
* All auth goes through NostrSessionJwtGuard or NostrAuthGuard.
*/
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor() {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
secretOrKey: process.env.NOSTR_JWT_SECRET || 'dummy-key-cognito-disabled',
issuer: undefined,
algorithms: ['HS256'],
});
Logger.warn(
'JwtStrategy initialized with dummy config (Cognito disabled)',
'AuthModule',
);
}
public async validate(payload: any) {
return payload;
}
}

View File

@@ -0,0 +1,123 @@
import {
Inject,
Injectable,
Logger,
UnauthorizedException,
forwardRef,
} from '@nestjs/common';
import { sign, verify } from 'jsonwebtoken';
import { randomUUID } from 'node:crypto';
import { UsersService } from 'src/users/users.service';
export interface NostrSessionTokens {
accessToken: string;
refreshToken: string;
expiresIn: number;
}
export interface NostrSessionPayload {
sub: string;
typ: 'nostr-session' | 'nostr-refresh';
uid?: string;
}
@Injectable()
export class NostrSessionService {
private readonly logger = new Logger(NostrSessionService.name);
private readonly accessSecret = process.env.NOSTR_JWT_SECRET;
private readonly refreshSecret =
process.env.NOSTR_JWT_REFRESH_SECRET ?? this.accessSecret;
private readonly accessTtlSeconds = Number(
process.env.NOSTR_JWT_TTL ?? 15 * 60,
);
private readonly refreshTtlSeconds = Number(
process.env.NOSTR_JWT_REFRESH_TTL ?? 60 * 60 * 24 * 14,
);
constructor(
@Inject(forwardRef(() => UsersService))
private readonly usersService: UsersService,
) {
if (!this.accessSecret) {
throw new Error('NOSTR_JWT_SECRET is not configured');
}
}
/**
* Issue JWT tokens for a Nostr pubkey.
* Auto-creates a user record on first login so that /auth/me works immediately.
*/
async issueTokens(pubkey: string): Promise<NostrSessionTokens> {
let user = await this.usersService.findUserByNostrPubkey(pubkey);
// Auto-provision a user on first Nostr login
if (!user) {
this.logger.log(`First Nostr login for ${pubkey.slice(0, 12)}… — creating user`);
try {
user = await this.usersService.createNostrUser(pubkey);
} catch (error) {
this.logger.warn(
`Failed to auto-create user for ${pubkey.slice(0, 12)}…: ${error.message}`,
);
}
} else if (!user.filmmaker) {
// Existing user without filmmaker profile — backfill
this.logger.log(`Backfilling filmmaker for ${pubkey.slice(0, 12)}`);
try {
user = await this.usersService.ensureFilmmaker(user);
} catch (error) {
this.logger.warn(`Filmmaker backfill failed: ${error.message}`);
}
}
const payload: NostrSessionPayload = {
sub: pubkey,
typ: 'nostr-session',
uid: user?.id,
};
const refreshPayload: NostrSessionPayload = {
sub: pubkey,
typ: 'nostr-refresh',
uid: user?.id,
};
const accessToken = sign(payload, this.accessSecret, {
expiresIn: this.accessTtlSeconds,
});
const refreshToken = sign(refreshPayload, this.refreshSecret, {
expiresIn: this.refreshTtlSeconds,
});
return {
accessToken,
refreshToken,
expiresIn: this.accessTtlSeconds,
};
}
verifyAccessToken(token: string): NostrSessionPayload {
return this.verifyToken(token, this.accessSecret, 'nostr-session');
}
verifyRefreshToken(token: string): NostrSessionPayload {
return this.verifyToken(token, this.refreshSecret, 'nostr-refresh');
}
private verifyToken(
token: string,
secret: string,
expectedType: NostrSessionPayload['typ'],
): NostrSessionPayload {
try {
const payload = verify(token, secret) as NostrSessionPayload;
if (payload.typ !== expectedType) {
throw new UnauthorizedException('Invalid token type');
}
return payload;
} catch (error) {
if (error instanceof UnauthorizedException) throw error;
throw new UnauthorizedException('Invalid or expired token');
}
}
}

View File

@@ -0,0 +1,8 @@
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
export const User = createParamDecorator(
(data: unknown, context: ExecutionContext) => {
const request = context.switchToHttp().getRequest();
return request.user;
},
);