This commit is contained in:
2025-11-10 12:39:41 +03:30
parent 06f814b331
commit 45ca2fde06
24 changed files with 794 additions and 586 deletions
+1 -1
View File
@@ -1,5 +1,5 @@
import { Controller, Post, Body } from '@nestjs/common';
import { AuthService } from './auth.service';
import { AuthService } from './services/auth.service';
import { RequestOtpDto } from './dto/request-otp.dto';
import { ApiTags, ApiOperation, ApiBody, ApiResponse } from '@nestjs/swagger';
import { VerifyOtpDto } from './dto/verify-otp.dto copy';
+2 -2
View File
@@ -1,8 +1,8 @@
import { Module } from '@nestjs/common';
import { AuthService } from './auth.service';
import { AuthService } from './services/auth.service';
import { AuthController } from './auth.controller';
import { UtilsModule } from '../utils/utils.module';
import { UserModule } from '../user/user.module';
import { UserModule } from '../users/user.module';
import { JwtModule } from '@nestjs/jwt';
import { ConfigService } from '@nestjs/config';
import { AdminModule } from '../admin/admin.module';
@@ -0,0 +1,50 @@
// guards/jwt-auth.guard.ts
import { CanActivate, ExecutionContext, Injectable, UnauthorizedException } from '@nestjs/common';
import { Request } from 'express';
import { JwtService } from '@nestjs/jwt';
import { ConfigService } from '@nestjs/config';
import { AuthEntityType, AuthRequest, JwtPayload } from './auth.guard';
@Injectable()
export class AdminAuthGuard implements CanActivate {
constructor(
private readonly jwtService: JwtService,
private readonly configService: ConfigService,
) {}
async canActivate(context: ExecutionContext) {
const request = context.switchToHttp().getRequest<AuthRequest>();
try {
const secret = this.configService.get<string>('JWT_SECRET');
const token = this.extractTokenFromHeader(request);
if (!token) {
throw new UnauthorizedException();
}
try {
const payload = await this.jwtService.verifyAsync<JwtPayload>(token, {
secret,
});
if (payload.type !== AuthEntityType.ADMIN) {
throw new UnauthorizedException('Access denied. Admin rights required.');
}
// 💡 We're assigning the payload to the request object here
// so that we can access it in our route handlers
request['user'] = payload;
} catch {
throw new UnauthorizedException();
}
return true;
} catch (err) {
console.log('error in AuthGuard', err);
throw new UnauthorizedException('Invalid or expired token');
}
}
private extractTokenFromHeader(request: Request): string | undefined {
const [type, token] = request.headers.authorization?.split(' ') ?? [];
return type === 'Bearer' ? token : undefined;
}
}
+61
View File
@@ -0,0 +1,61 @@
// guards/jwt-auth.guard.ts
import { CanActivate, ExecutionContext, Injectable, UnauthorizedException } from '@nestjs/common';
import { Request } from 'express';
import { JwtService } from '@nestjs/jwt';
import { ConfigService } from '@nestjs/config';
export enum AuthEntityType {
USER = 'user',
ADMIN = 'admin',
}
export interface AuthRequest extends Request {
user: {
sub: string; // userId
};
}
export interface JwtPayload {
sub: string; // The userId
type: AuthEntityType;
}
@Injectable()
export class AuthGuard implements CanActivate {
constructor(
private readonly jwtService: JwtService,
private readonly configService: ConfigService,
) {}
async canActivate(context: ExecutionContext) {
const request = context.switchToHttp().getRequest<AuthRequest>();
try {
const secret = this.configService.get<string>('JWT_SECRET');
const token = this.extractTokenFromHeader(request);
if (!token) {
throw new UnauthorizedException();
}
try {
const payload = await this.jwtService.verifyAsync<JwtPayload>(token, {
secret,
});
if (payload.type !== AuthEntityType.USER) {
throw new UnauthorizedException('Access denied. User rights required.');
}
// 💡 We're assigning the payload to the request object here
// so that we can access it in our route handlers
request['user'] = payload;
} catch {
throw new UnauthorizedException();
}
return true;
} catch (err) {
console.log('error in AuthGuard', err);
throw new UnauthorizedException('Invalid or expired token');
}
}
private extractTokenFromHeader(request: Request): string | undefined {
const [type, token] = request.headers.authorization?.split(' ') ?? [];
return type === 'Bearer' ? token : undefined;
}
}
+15
View File
@@ -0,0 +1,15 @@
import { RoleEnum } from "../../users/enums/role.enum";
export interface ILocalTokenPayload {
id: string;
role: RoleEnum;
wildduckUserId: string;
}
export interface IConsoleTokenPayload {
id: string;
permissions?: string[];
isAdmin?: boolean;
}
export interface ITokenPayload extends ILocalTokenPayload, IConsoleTokenPayload {}
@@ -1,12 +1,12 @@
import { Injectable, BadRequestException, UnauthorizedException, NotFoundException } from '@nestjs/common';
import { RequestOtpDto } from './dto/request-otp.dto';
import { CacheService } from '../utils/cache.service';
import { SmsService } from '../utils/sms.service';
import { UserService } from '../user/user.service';
import { RequestOtpDto } from '../dto/request-otp.dto';
import { CacheService } from '../../utils/cache.service';
import { SmsService } from '../../utils/sms.service';
import { UserService } from '../../users/user.service';
import { randomInt } from 'crypto';
import { JwtService } from '@nestjs/jwt';
import { AdminService } from '../admin/admin.service';
import { AuthEntityType } from 'src/common/guards/auth.guard';
import { AdminService } from '../../admin/admin.service';
import { AuthEntityType } from 'src/modules/auth/guards/auth.guard';
@Injectable()
export class AuthService {
+154
View File
@@ -0,0 +1,154 @@
import { randomBytes } from 'node:crypto';
import { EntityManager } from '@mikro-orm/postgresql';
import { BadRequestException, Injectable, Logger, UnauthorizedException } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { JwtService } from '@nestjs/jwt';
import dayjs from 'dayjs';
import { AuthMessage, UserMessage } from '../../../common/enums/message.enum';
import { RefreshToken } from '../../users/entities/refresh-token.entity';
import { User } from '../../users/entities/user.entity';
import { ITokenPayload } from '../interfaces/IToken-payload';
@Injectable()
export class TokensService {
private readonly logger = new Logger(TokensService.name);
constructor(
private readonly configService: ConfigService,
private readonly jwtService: JwtService,
private readonly em: EntityManager,
) {}
async generateTokens(user: User, em?: EntityManager) {
return this.generateAccessAndRefreshToken(
{
id: user.id,
role: user.role.name,
},
em,
);
}
private async generateAccessAndRefreshToken(payload: ITokenPayload, em?: EntityManager) {
const accessExpire = this.configService.getOrThrow<number>('ACCESS_TOKEN_EXPIRE');
const refreshExpire = this.configService.getOrThrow<number>('REFRESH_TOKEN_EXPIRE');
const accessToken = await this.jwtService.signAsync(payload, { expiresIn: `${accessExpire}m` });
const refreshToken = await this.generateRefreshToken();
await this.storeRefreshToken(payload.id, refreshToken, em);
return {
accessToken: { token: accessToken, expire: dayjs().add(accessExpire, 'minute').valueOf() },
refreshToken: { token: refreshToken, expire: dayjs().add(refreshExpire, 'day').valueOf() },
};
}
async storeRefreshToken(userId: string, refreshToken: string, em?: EntityManager) {
const entityManager = em || this.em.fork();
const isExternalTransaction = !!em && em.isInTransaction();
try {
// Only start transaction if no external EntityManager with transaction is provided
if (!isExternalTransaction && !entityManager.isInTransaction()) {
await entityManager.begin();
}
const refreshExpire = this.configService.getOrThrow<number>('REFRESH_TOKEN_EXPIRE');
const expiresAt = dayjs().add(refreshExpire, 'day').toDate();
const user = await entityManager.findOne(User, { id: userId });
if (!user) throw new BadRequestException(UserMessage.USER_NOT_FOUND);
const token = entityManager.create(RefreshToken, {
token: refreshToken,
user,
expiresAt,
});
// Use persist instead of persistAndFlush when in transaction
if (isExternalTransaction) {
await entityManager.persist(token);
} else {
await entityManager.persistAndFlush(token);
if (entityManager.isInTransaction()) {
await entityManager.commit();
}
}
} catch (error) {
this.logger.error(error);
// Only rollback if we started the transaction
if (!isExternalTransaction && entityManager.isInTransaction()) {
await entityManager.rollback();
}
throw error;
}
}
async refreshToken(oldRefreshToken: string) {
const entityManager = this.em.fork();
try {
await entityManager.begin();
const token = await entityManager.findOne(
RefreshToken,
{ token: oldRefreshToken },
{
populate: ['user', 'user.role'],
},
);
if (!token) throw new UnauthorizedException(AuthMessage.INVALID_REFRESH_TOKEN);
if (dayjs(token.expiresAt).isBefore(dayjs())) {
await entityManager.remove(token);
await entityManager.flush();
await entityManager.commit();
throw new UnauthorizedException(AuthMessage.REFRESH_TOKEN_EXPIRED);
}
// Remove the old token
await entityManager.remove(token);
// Generate new tokens within the same transaction
const tokens = await this.generateTokens(token.user, entityManager);
// Commit the transaction
await entityManager.flush();
await entityManager.commit();
return tokens;
} catch (error) {
this.logger.error(error);
if (entityManager.isInTransaction()) {
await entityManager.rollback();
}
throw error;
}
}
async invalidateRefreshToken(userId: string) {
const entityManager = this.em.fork();
try {
await entityManager.begin();
await entityManager.nativeDelete(RefreshToken, { user: { id: userId } });
this.logger.log(`Successfully deleted all refresh tokens for user ${userId}`);
await entityManager.commit();
} catch (error) {
this.logger.error(error);
if (entityManager.isInTransaction()) {
await entityManager.rollback();
}
throw error;
}
}
private async generateRefreshToken() {
return randomBytes(32).toString('hex');
}
}