role perms

This commit is contained in:
2025-11-12 09:28:56 +03:30
parent 046286b559
commit 440c3721ce
31 changed files with 488 additions and 98 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
import { Module } from '@nestjs/common';
import { AuthService } from './services/auth.service';
import { AuthController } from './auth.controller';
import { AuthController } from './controllers/auth.controller';
import { UtilsModule } from '../utils/utils.module';
import { UserModule } from '../users/user.module';
import { JwtModule } from '@nestjs/jwt';
@@ -0,0 +1,58 @@
import { Controller, Post, Body } from '@nestjs/common';
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';
import { Throttle } from '@nestjs/throttler';
import { RefreshTokenRateLimit } from 'src/common/decorators/rate-limit.decorator';
import { RefreshTokenDto } from '../dto/refresh-token.dto';
@ApiTags('admin/auth')
@Controller('admin/auth')
export class AuthController {
constructor(private readonly authService: AuthService) {}
@Throttle({ default: { limit: 3, ttl: 180_000 } })
@Post('otp/request')
@ApiOperation({ summary: 'Request OTP for login or signup' })
@ApiBody({ type: RequestOtpDto, description: 'Mobile number to receive OTP' })
@ApiResponse({ status: 201, description: 'OTP requested successfully' })
@ApiResponse({ status: 400, description: 'Invalid mobile number' })
otpRequest(@Body() dto: RequestOtpDto) {
return this.authService.requestOtp(dto);
}
@Post('otp/verify')
@ApiOperation({ summary: 'Verify OTP code' })
@ApiBody({ type: VerifyOtpDto, description: 'Mobile number and OTP code' })
@ApiResponse({ status: 200, description: 'OTP verified successfully' })
@ApiResponse({ status: 400, description: 'Invalid OTP or expired' })
otpVerify(@Body() dto: VerifyOtpDto) {
return this.authService.verifyOtp(dto.phone, dto.slug, dto.otp); // assuming dto has `otp` property
}
// @Post('admin/otp/request')
// @ApiOperation({ summary: 'Request OTP for login or signup' })
// @ApiBody({ type: RequestOtpDto, description: 'Mobile number to receive OTP' })
// @ApiResponse({ status: 201, description: 'OTP requested successfully' })
// @ApiResponse({ status: 400, description: 'Invalid mobile number' })
// adminOtpRequest(@Body() dto: RequestOtpDto) {
// return this.authService.requestOtpAdmin(dto);
// }
// @Post('admin/otp/verify')
// @ApiOperation({ summary: 'Verify OTP code' })
// @ApiBody({ type: VerifyOtpDto, description: 'Mobile number and OTP code' })
// @ApiResponse({ status: 200, description: 'OTP verified successfully' })
// @ApiResponse({ status: 400, description: 'Invalid OTP or expired' })
// adminOtpVerify(@Body() dto: VerifyOtpDto) {
// return this.authService.verifyOtpAdmin(dto.phone, dto.otp); // assuming dto has `otp` property
// }
@RefreshTokenRateLimit()
@ApiOperation({ summary: 'refresh the user access token / refresh token' })
@Post('refresh')
refreshToken(@Body() refreshTokenDto: RefreshTokenDto) {
return this.authService.refreshToken(refreshTokenDto.refreshToken);
}
}
@@ -1,11 +1,11 @@
import { Controller, Post, Body } from '@nestjs/common';
import { AuthService } from './services/auth.service';
import { RequestOtpDto } from './dto/request-otp.dto';
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';
import { VerifyOtpDto } from '../dto/verify-otp.dto copy';
import { Throttle } from '@nestjs/throttler';
import { RefreshTokenRateLimit } from 'src/common/decorators/rate-limit.decorator';
import { RefreshTokenDto } from './dto/refresh-token.dto';
import { RefreshTokenDto } from '../dto/refresh-token.dto';
@ApiTags('auth')
@Controller('auth')
@@ -31,14 +31,14 @@ export class AuthController {
return this.authService.verifyOtp(dto.phone, dto.slug, dto.otp); // assuming dto has `otp` property
}
@Post('admin/otp/request')
@ApiOperation({ summary: 'Request OTP for login or signup' })
@ApiBody({ type: RequestOtpDto, description: 'Mobile number to receive OTP' })
@ApiResponse({ status: 201, description: 'OTP requested successfully' })
@ApiResponse({ status: 400, description: 'Invalid mobile number' })
adminOtpRequest(@Body() dto: RequestOtpDto) {
return this.authService.requestOtpAdmin(dto);
}
// @Post('admin/otp/request')
// @ApiOperation({ summary: 'Request OTP for login or signup' })
// @ApiBody({ type: RequestOtpDto, description: 'Mobile number to receive OTP' })
// @ApiResponse({ status: 201, description: 'OTP requested successfully' })
// @ApiResponse({ status: 400, description: 'Invalid mobile number' })
// adminOtpRequest(@Body() dto: RequestOtpDto) {
// return this.authService.requestOtpAdmin(dto);
// }
// @Post('admin/otp/verify')
// @ApiOperation({ summary: 'Verify OTP code' })
@@ -0,0 +1,10 @@
import { IsNotEmpty, IsString, IsMobilePhone } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';
export class RequestOtpDto {
@IsNotEmpty()
@IsString()
@ApiProperty({ example: '09362532122', description: 'Mobile number' })
@IsMobilePhone('fa-IR')
phone: string;
}
+1 -1
View File
@@ -13,6 +13,6 @@ export class RequestOtpDto {
@IsNotEmpty()
@IsString()
@ApiProperty({ example: '', description: 'Restuarant slug' })
@ApiProperty({ example: '', description: 'restaurant slug' })
slug: string;
}
+1 -1
View File
@@ -19,6 +19,6 @@ export class VerifyOtpDto {
@IsNotEmpty()
@IsString()
@ApiProperty({ example: '', description: 'Restuarant slug' })
@ApiProperty({ example: '', description: 'restaurant slug' })
slug: string;
}
+4 -4
View File
@@ -6,7 +6,7 @@ import { ITokenPayload } from '../interfaces/IToken-payload';
export interface AuthRequest extends Request {
userId: string;
slug: string;
restId: string;
}
@Injectable()
@@ -30,9 +30,9 @@ export class AuthGuard implements CanActivate {
const payload = await this.jwtService.verifyAsync<ITokenPayload>(token, {
secret,
});
request['userId'] = payload.id;
request['slug'] = payload.restId;
console.log('payload', payload);
request['userId'] = payload.userId;
request['restId'] = payload.restId;
} catch {
throw new UnauthorizedException();
}
@@ -1,4 +1,9 @@
export interface ITokenPayload {
id: string;
userId: string;
restId: string;
}
export interface IAdminTokenPayload {
adminId: string;
restId: string;
}
+59 -12
View File
@@ -4,21 +4,22 @@ 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 { TokensService } from './tokens.service';
import { RestRepository } from 'src/modules/restaurants/repositories/rest.repository';
import { RestMessage } from 'src/common/enums/message.enum';
import { AuthMessage, RestMessage } from 'src/common/enums/message.enum';
import { AdminRepository } from 'src/modules/admin/repositories/rest.repository';
@Injectable()
export class AuthService {
readonly ADMIN_PERMISSIONS_KEY = 'admin-perms';
constructor(
private readonly cacheService: CacheService,
private readonly smsService: SmsService,
private readonly userService: UserService,
private readonly adminService: AdminService,
private readonly tokensService: TokensService,
private readonly restRepository: RestRepository,
private readonly adminRepository: AdminRepository,
) {}
async requestOtp(dto: RequestOtpDto) {
@@ -27,26 +28,26 @@ export class AuthService {
await this.cacheService.set(`otp:${slug}:${phone}`, code, 160);
await this.smsService.sendOtp(phone, code);
// await this.smsService.sendOtp(phone, code);
return { success: true, message: ' OTP sent successfully' };
return { code };
}
async requestOtpAdmin(dto: RequestOtpDto) {
const { phone } = dto;
const { phone, slug } = dto;
const admin = await this.adminService.findByPhone(phone);
const admin = await this.adminRepository.find({ phone });
if (!admin) {
throw new NotFoundException();
}
const code = this.generateOtpCode();
await this.cacheService.set(`otp-admin:${phone}`, code, 160);
await this.cacheService.set(`otp-admin:${slug}:${phone}`, code, 160);
await this.smsService.sendOtp(phone, code);
return { success: true, message: ' OTP sent successfully' };
return true;
}
async verifyOtp(phone: string, slug: string, code: string) {
@@ -57,14 +58,46 @@ export class AuthService {
await this.cacheService.del(`otp:${slug}:${phone}`);
const user = await this.userService.findOrCreateByPhone(phone);
const rest = await this.restRepository.findOne({ slug });
if (!rest) {
throw new BadRequestException(RestMessage.NOT_FOUND);
}
// const accessToken = await this.issueToken(user.id, AuthEntityType.USER);
const tokens = await this.tokensService.generateTokens(user, rest);
return { success: true, message: 'User registered successfully!', tokens };
return { tokens, user };
}
async verifyOtpAdmin(phone: string, slug: string, code: string) {
const cachedCode = await this.cacheService.get(`otp:${slug}:${phone}`);
if (!cachedCode) throw new BadRequestException('OTP expired or not found');
if (cachedCode !== code) throw new BadRequestException('Invalid OTP');
await this.cacheService.del(`otp-admin:${slug}:${phone}`);
const admin = await this.adminRepository.findByPhoneAndrestaurantSlug(phone, slug);
if (!admin) {
throw new BadRequestException(AuthMessage.USER_NOT_FOUND);
}
const rest = await this.restRepository.findOne({ slug });
if (!rest) {
throw new BadRequestException(RestMessage.NOT_FOUND);
}
const tokens = await this.tokensService.generateTokensAdmin(admin, rest);
await this.cacheService.set(
`${this.ADMIN_PERMISSIONS_KEY}:${admin.id}:${rest.id}`,
JSON.stringify(admin.role.permissions),
3600,
);
return { tokens, admin };
}
private generateOtpCode(): string {
@@ -75,4 +108,18 @@ export class AuthService {
refreshToken(oldRefreshToken: string) {
return this.tokensService.refreshToken(oldRefreshToken);
}
// async getAdminPerms(adminId: string, restId: string) {
// const cachedPerms = await this.cacheService.get(`${this.ADMIN_PERMISSIONS_KEY}:${adminId}:${restId}`);
// // eslint-disable-next-line @typescript-eslint/no-unsafe-return
// return cachedPerms ? JSON.parse(cachedPerms) : [];
// }
async setAdminPerms(adminId: string, restId: string, permissions: string) {
return this.cacheService.set(
`${this.ADMIN_PERMISSIONS_KEY}:${adminId}:${restId}`,
JSON.stringify(permissions),
3600,
);
}
}
+46 -11
View File
@@ -9,9 +9,11 @@ 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';
import { IAdminTokenPayload, ITokenPayload } from '../interfaces/IToken-payload';
import { Restaurant } from 'src/modules/restaurants/entities/restaurant.entity';
import { RestRepository } from 'src/modules/restaurants/repositories/rest.repository';
import { Admin } from 'src/modules/admin/entities/admin.entity';
import { CacheService } from 'src/modules/utils/cache.service';
@Injectable()
export class TokensService {
@@ -21,12 +23,23 @@ export class TokensService {
private readonly jwtService: JwtService,
private readonly em: EntityManager,
private readonly restRepository: RestRepository,
private readonly cacheService: CacheService,
) {}
async generateTokens(user: User, rest: Restaurant, em?: EntityManager) {
return this.generateAccessAndRefreshToken(
{
id: user.id,
userId: user.id,
restId: rest.id,
},
em,
);
}
async generateTokensAdmin(admin: Admin, rest: Restaurant, em?: EntityManager) {
return this.generateAccessAndRefreshTokenAdmin(
{
adminId: admin.id,
restId: rest.id,
},
em,
@@ -38,13 +51,13 @@ export class TokensService {
const refreshExpire = this.configService.getOrThrow<number>('REFRESH_TOKEN_EXPIRE');
const tokenPayload: ITokenPayload = {
id: payload.id,
userId: payload.userId,
restId: payload.restId,
};
const accessToken = await this.jwtService.signAsync(tokenPayload, { expiresIn: `${accessExpire}m` });
const refreshToken = this.generateRefreshToken();
await this.storeRefreshToken(payload.id, refreshToken, em);
await this.storeRefreshToken(payload.userId, payload.restId, refreshToken, em);
return {
accessToken: { token: accessToken, expire: dayjs().add(accessExpire, 'minute').valueOf() },
@@ -52,7 +65,25 @@ export class TokensService {
};
}
async storeRefreshToken(userId: string, refreshToken: string, em?: EntityManager) {
private async generateAccessAndRefreshTokenAdmin(payload: IAdminTokenPayload, em?: EntityManager) {
const accessExpire = this.configService.getOrThrow<number>('ACCESS_TOKEN_EXPIRE');
const refreshExpire = this.configService.getOrThrow<number>('REFRESH_TOKEN_EXPIRE');
const tokenPayload: IAdminTokenPayload = {
adminId: payload.adminId,
restId: payload.restId,
};
const accessToken = await this.jwtService.signAsync(tokenPayload, { expiresIn: `${accessExpire}m` });
const refreshToken = this.generateRefreshToken();
await this.storeRefreshToken(payload.adminId, payload.restId, 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, restId: string, refreshToken: string, em?: EntityManager) {
const entityManager = em || this.em.fork();
const isExternalTransaction = !!em && em.isInTransaction();
@@ -68,15 +99,19 @@ export class TokensService {
const user = await entityManager.findOne(User, { id: userId });
if (!user) throw new BadRequestException(UserMessage.USER_NOT_FOUND);
const restaurant = await entityManager.findOne(Restaurant, { id: restId });
if (!restaurant) throw new BadRequestException(UserMessage.Rest_NOT_FOUND);
const token = entityManager.create(RefreshToken, {
token: refreshToken,
restaurant,
user,
expiresAt,
});
// Use persist instead of persistAndFlush when in transaction
if (isExternalTransaction) {
await entityManager.persist(token);
entityManager.persist(token);
} else {
await entityManager.persistAndFlush(token);
if (entityManager.isInTransaction()) {
@@ -98,19 +133,19 @@ export class TokensService {
try {
await entityManager.begin();
console.log('oldRefreshToken', oldRefreshToken);
const token = await entityManager.findOne(
RefreshToken,
{ token: oldRefreshToken },
{
populate: ['user'],
populate: ['user', 'restaurant'],
},
);
if (!token) throw new UnauthorizedException(AuthMessage.INVALID_REFRESH_TOKEN);
const rest = await this.restRepository.findOne({ id: token.user.id });
if (!rest) throw new UnauthorizedException(AuthMessage.INVALID_REFRESH_TOKEN);
// const rest = await this.restRepository.findOne({ id: token.user.id });
// if (!rest) throw new UnauthorizedException(AuthMessage.INVALID_REFRESH_TOKEN);
if (dayjs(token.expiresAt).isBefore(dayjs())) {
entityManager.remove(token);
@@ -123,7 +158,7 @@ export class TokensService {
entityManager.remove(token);
// Generate new tokens within the same transaction
const tokens = await this.generateTokens(token.user, rest, entityManager);
const tokens = await this.generateTokens(token.user, token.restaurant, entityManager);
// Commit the transaction
await entityManager.flush();