role perms
This commit is contained in:
@@ -19,6 +19,7 @@ export const enum AuthMessage {
|
||||
INVALID_EMAIL_FORMAT = 'فرمت ایمیل صحیح نیست',
|
||||
PasswordNotEmpty = 'پسورد نمیتواند خالی باشد.',
|
||||
USER_NOT_FOUND = 'کاربری با این شماره وجود ندارد',
|
||||
ADMIN_NOT_FOUND = 'ادمینی با این شماره وجود ندارد',
|
||||
USER_EXISTS = 'با این شماره قبلا ثبت نام شده است',
|
||||
USER_REGISTER_SUCCESS = 'ثبت نام موفقیت آمیز بود',
|
||||
INVALID_USER = 'کاربری با این مشخصات یافت نشد',
|
||||
@@ -69,6 +70,7 @@ export const enum AuthMessage {
|
||||
|
||||
export const enum UserMessage {
|
||||
USER_NOT_FOUND = 'کاربری با این مشخصات یافت نشد',
|
||||
Rest_NOT_FOUND = 'رستورانی با این مشخصات یافت نشد',
|
||||
USER_EXISTS = 'با این شماره قبلا ثبت نام شده است',
|
||||
USER_REGISTER_SUCCESS = 'ثبت نام موفقیت آمیز بود',
|
||||
EMAIL_ADDRESS_ALREADY_EXISTS = 'ایمیل قبلا ثبت شده است',
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
import { ArgumentsHost, Catch, ExceptionFilter, HttpException } from '@nestjs/common';
|
||||
import { Request, Response } from 'express';
|
||||
|
||||
@Catch(HttpException)
|
||||
export class HttpErrorFilter implements ExceptionFilter {
|
||||
catch(exception: HttpException, host: ArgumentsHost) {
|
||||
const ctx = host.switchToHttp();
|
||||
const response = ctx.getResponse<Response>();
|
||||
const request = ctx.getRequest<Request>();
|
||||
|
||||
const status = exception.getStatus();
|
||||
|
||||
const exceptionResponse = exception.getResponse();
|
||||
|
||||
let messages: (string | object)[] = [];
|
||||
if (typeof exceptionResponse === 'string') {
|
||||
messages = [exceptionResponse];
|
||||
} else if (typeof exceptionResponse === 'object' && exceptionResponse !== null && 'message' in exceptionResponse) {
|
||||
messages = Array.isArray(exceptionResponse.message)
|
||||
? (exceptionResponse.message as string[])
|
||||
: [exceptionResponse.message as string];
|
||||
} else messages = ['Unexpected error'];
|
||||
|
||||
const errorResponse = {
|
||||
success: false,
|
||||
timestamp: new Date().toISOString(),
|
||||
path: request.url,
|
||||
messages,
|
||||
};
|
||||
|
||||
response.status(status).json(errorResponse);
|
||||
}
|
||||
}
|
||||
Executable
+36
@@ -0,0 +1,36 @@
|
||||
import { ArgumentsHost, Catch, ExceptionFilter, HttpException, HttpStatus } from '@nestjs/common';
|
||||
import { FastifyReply } from 'fastify';
|
||||
|
||||
@Catch(HttpException)
|
||||
export class HttpExceptionFilter implements ExceptionFilter {
|
||||
catch(exception: HttpException, host: ArgumentsHost) {
|
||||
const ctx = host.switchToHttp();
|
||||
const reply = ctx.getResponse<FastifyReply>();
|
||||
// const request = ctx.getRequest<FastifyRequest>();
|
||||
const status = exception.getStatus() || HttpStatus.INTERNAL_SERVER_ERROR;
|
||||
|
||||
const exceptionResponse = exception.getResponse();
|
||||
const message = this.extractMessage(exceptionResponse);
|
||||
|
||||
reply.status(status).send({
|
||||
statusCode: status,
|
||||
success: false,
|
||||
error: {
|
||||
message,
|
||||
// timestamp: new Date().toISOString(),
|
||||
// path: request.url,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private extractMessage(response: string | Record<string, any>): string | string[] {
|
||||
if (typeof response === 'string') {
|
||||
return [response];
|
||||
}
|
||||
if (typeof response === 'object' && 'message' in response) {
|
||||
return Array.isArray(response.message) ? response.message : [response.message];
|
||||
}
|
||||
|
||||
return 'something wrong';
|
||||
}
|
||||
}
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
// import { CallHandler, ExecutionContext, Injectable, Logger, NestInterceptor } from '@nestjs/common';
|
||||
// import { FastifyRequest } from 'fastify';
|
||||
// import { Observable, map } from 'rxjs';
|
||||
|
||||
// import { IPageFormat } from '../../../../danak_dsc_api/src/common/interfaces/IPagination';
|
||||
|
||||
// @Injectable()
|
||||
// export class PaginationInterceptor implements NestInterceptor {
|
||||
// private readonly logger = new Logger(PaginationInterceptor.name);
|
||||
|
||||
// intercept(context: ExecutionContext, next: CallHandler): Observable<unknown> {
|
||||
// const request = context.switchToHttp().getRequest<FastifyRequest>();
|
||||
// const query = request.query as { page: string; limit: string };
|
||||
|
||||
// const page = parseInt(query.page, 10) || 1;
|
||||
// const limit = parseInt(query.limit, 10) || 10;
|
||||
|
||||
// const className = context.getClass().name;
|
||||
// const handlerName = context.getHandler().name;
|
||||
|
||||
// return next.handle().pipe(
|
||||
// map(data => {
|
||||
// if (data && (data.paginate || data.count)) {
|
||||
// const { count, paginate, ...response } = data;
|
||||
// this.logger.log(`paginate response from ${className}.${handlerName}`);
|
||||
// const pager = this.formatPage(page, limit, count, request);
|
||||
|
||||
// return {
|
||||
// pager,
|
||||
// ...response,
|
||||
// };
|
||||
// }
|
||||
|
||||
// return data;
|
||||
// }),
|
||||
// );
|
||||
// }
|
||||
|
||||
// private formatPage(page: number, limit: number, totalItems: number, request: FastifyRequest): IPageFormat {
|
||||
// const totalPages = Math.ceil(totalItems / limit);
|
||||
// const prevPage = page === 1 ? false : page - 1;
|
||||
// const nextPage = page >= totalPages ? false : page + 1;
|
||||
|
||||
// const formatLink = (pageNum: number | boolean): string | boolean => {
|
||||
// if (!pageNum) return false;
|
||||
// const protocol = request.protocol;
|
||||
// const hostname = request.hostname;
|
||||
// const originalUrl = request.url;
|
||||
// return `${protocol}://${hostname}${originalUrl.split('?')[0]}?page=${pageNum}&limit=${limit}`;
|
||||
// };
|
||||
|
||||
// return {
|
||||
// page,
|
||||
// limit,
|
||||
// totalItems,
|
||||
// totalPages,
|
||||
// prevPage: formatLink(prevPage),
|
||||
// nextPage: formatLink(nextPage),
|
||||
// };
|
||||
// }
|
||||
// }
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common';
|
||||
import { FastifyReply } from 'fastify';
|
||||
import { Observable } from 'rxjs';
|
||||
import { map } from 'rxjs/operators';
|
||||
|
||||
@Injectable()
|
||||
export class ResponseInterceptor implements NestInterceptor {
|
||||
//
|
||||
intercept(context: ExecutionContext, next: CallHandler<Record<string, unknown>>): Observable<unknown> {
|
||||
const ctx = context.switchToHttp().getResponse<FastifyReply>();
|
||||
const statusCode = ctx.statusCode;
|
||||
|
||||
return next.handle().pipe(
|
||||
map(data => {
|
||||
if (data && data.data !== undefined) {
|
||||
return {
|
||||
statusCode,
|
||||
success: true,
|
||||
data: data.data,
|
||||
};
|
||||
}
|
||||
return {
|
||||
statusCode,
|
||||
success: true,
|
||||
data,
|
||||
};
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
Executable
+26
@@ -0,0 +1,26 @@
|
||||
import { Injectable, Logger, NestMiddleware } from "@nestjs/common";
|
||||
import { FastifyReply, FastifyRequest } from "fastify";
|
||||
|
||||
@Injectable()
|
||||
export class HTTPLogger implements NestMiddleware {
|
||||
private readonly logger = new Logger(HTTPLogger.name);
|
||||
// use(req: any, res: any, next: (error?: Error | any) => void) {}
|
||||
|
||||
use(req: FastifyRequest, rep: FastifyReply["raw"], next: () => void) {
|
||||
const { ip, method, originalUrl } = req;
|
||||
const userAgent = req.headers["user-agent"] || "";
|
||||
const startAt = process.hrtime();
|
||||
|
||||
rep.on("finish", () => {
|
||||
const { statusCode } = rep;
|
||||
const contentLength = rep.getHeader("content-length") || 0;
|
||||
const dif = process.hrtime(startAt);
|
||||
const responseTime = dif[0] * 1e3 + dif[1] * 1e-6;
|
||||
this.logger.log(
|
||||
`${method} - ${originalUrl} - ${statusCode} - ${contentLength} - ${userAgent} - ${ip} - ${responseTime.toFixed(2)}ms`,
|
||||
);
|
||||
});
|
||||
|
||||
next();
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,9 @@ import multipart from '@fastify/multipart';
|
||||
|
||||
// 👈 Import the Fastify application type
|
||||
import { type NestFastifyApplication, FastifyAdapter } from '@nestjs/platform-fastify';
|
||||
import { HttpExceptionFilter } from './core/exceptions/http.exceptions';
|
||||
import { ResponseInterceptor } from './core/interceptors/response.interceptor';
|
||||
// import { PaginationInterceptor } from './core/interceptors/pagination.interceptor';
|
||||
|
||||
async function bootstrap() {
|
||||
const logger = new Logger('APP');
|
||||
@@ -22,6 +25,12 @@ async function bootstrap() {
|
||||
await app.register(multipart);
|
||||
app.useGlobalPipes(new ValidationPipe());
|
||||
|
||||
app.useGlobalInterceptors(
|
||||
new ResponseInterceptor(),
|
||||
// , new PaginationInterceptor()
|
||||
);
|
||||
app.useGlobalFilters(new HttpExceptionFilter());
|
||||
|
||||
const configService = app.get<ConfigService>(ConfigService);
|
||||
// Ensure the port is handled correctly, use 4000 as fallback if needed
|
||||
const APP_PORT = configService.getOrThrow<number>('APP_PORT') ?? 4000;
|
||||
|
||||
@@ -3,11 +3,15 @@ import { AdminService } from './admin.service';
|
||||
import { MikroOrmModule } from '@mikro-orm/nestjs';
|
||||
import { Admin } from './entities/admin.entity';
|
||||
import { JwtModule } from '@nestjs/jwt';
|
||||
import { AdminRepository } from './repositories/rest.repository';
|
||||
import { Role } from './entities/role.entity';
|
||||
import { Permission } from './entities/permission.entity';
|
||||
import { RolePermission } from './entities/rolePermission.entity';
|
||||
|
||||
@Module({
|
||||
providers: [AdminService],
|
||||
providers: [AdminService, AdminRepository],
|
||||
controllers: [],
|
||||
imports: [MikroOrmModule.forFeature([Admin]), JwtModule],
|
||||
exports: [AdminService],
|
||||
imports: [MikroOrmModule.forFeature([Admin, Role, Permission, RolePermission]), JwtModule],
|
||||
exports: [AdminService, AdminRepository],
|
||||
})
|
||||
export class AdminModule {}
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
import { Entity, Property } from '@mikro-orm/core';
|
||||
import { Entity, ManyToOne, OneToOne, Property, Unique } from '@mikro-orm/core';
|
||||
import { BaseEntity } from '../../../common/entities/base.entity';
|
||||
import { Restaurant } from 'src/modules/restaurants/entities/restaurant.entity';
|
||||
import { Role } from './role.entity';
|
||||
|
||||
@Entity({ tableName: 'admins' })
|
||||
// Add the Unique constraint for the combination of 'phone' and 'restaurant'
|
||||
@Unique({ properties: ['phone', 'restaurant'] })
|
||||
export class Admin extends BaseEntity {
|
||||
@Property({ nullable: true })
|
||||
firstName?: string;
|
||||
@@ -9,6 +13,13 @@ export class Admin extends BaseEntity {
|
||||
@Property({ nullable: true })
|
||||
lastName?: string;
|
||||
|
||||
@Property({ unique: true })
|
||||
@Property()
|
||||
phone!: string;
|
||||
|
||||
// Add the new role property
|
||||
@ManyToOne(() => Role)
|
||||
role!: Role;
|
||||
|
||||
@OneToOne(() => Restaurant, { owner: true })
|
||||
restaurant!: Restaurant;
|
||||
}
|
||||
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
import { Entity, Property, Unique, ManyToMany, Collection } from '@mikro-orm/core';
|
||||
import { BaseEntity } from '../../../common/entities/base.entity';
|
||||
import { Role } from './role.entity';
|
||||
|
||||
@Entity({ tableName: 'permissions' })
|
||||
export class Permission extends BaseEntity {
|
||||
@Property()
|
||||
@Unique()
|
||||
name!: string;
|
||||
|
||||
@ManyToMany({ entity: () => Role, mappedBy: r => r.permissions })
|
||||
roles = new Collection<Role>(this);
|
||||
}
|
||||
Executable
+22
@@ -0,0 +1,22 @@
|
||||
// role.entity.ts
|
||||
import { Collection, Entity, ManyToMany, OneToMany, OneToOne, Property } from '@mikro-orm/core';
|
||||
import { BaseEntity } from '../../../common/entities/base.entity';
|
||||
import { Admin } from './admin.entity';
|
||||
import { Permission } from './permission.entity';
|
||||
import { Restaurant } from 'src/modules/restaurants/entities/restaurant.entity';
|
||||
import { RolePermission } from './rolePermission.entity';
|
||||
|
||||
@Entity()
|
||||
export class Role extends BaseEntity {
|
||||
@Property()
|
||||
name!: string;
|
||||
|
||||
@ManyToMany({ entity: () => Permission, pivotEntity: () => RolePermission, inversedBy: p => p.roles })
|
||||
permissions = new Collection<Permission>(this);
|
||||
|
||||
@OneToMany(() => Admin, admin => admin.role)
|
||||
admins = new Collection<Admin>(this);
|
||||
|
||||
@OneToOne(() => Restaurant, { owner: true, nullable: true })
|
||||
restaurant?: Restaurant;
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
// role-permission.entity.ts (pivot)
|
||||
import { Entity, ManyToOne } from '@mikro-orm/core';
|
||||
import { Role } from './role.entity';
|
||||
import { Permission } from './permission.entity';
|
||||
|
||||
@Entity({ tableName: 'role_permissions' })
|
||||
export class RolePermission {
|
||||
// دو ManyToOne دقیقاً؛ هر کدام میتونن primary: true باشند برای composite PK
|
||||
@ManyToOne(() => Role, { primary: true })
|
||||
role!: Role;
|
||||
|
||||
@ManyToOne(() => Permission, { primary: true })
|
||||
permission!: Permission;
|
||||
|
||||
// **نکته:** اینجا *فقط* همین دو خصوصیت کافی است —
|
||||
// از تعریف جداگانهی roleId / permissionId با @PrimaryKey خودداری کن مگر بخوای نام/نوع ستون را سفارشی کنی.
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { EntityManager, EntityRepository } from '@mikro-orm/postgresql';
|
||||
import { Admin } from '../entities/admin.entity';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
@Injectable()
|
||||
export class AdminRepository extends EntityRepository<Admin> {
|
||||
constructor(readonly em: EntityManager) {
|
||||
super(em, Admin);
|
||||
}
|
||||
|
||||
async findByPhoneAndrestaurantSlug(phone: string, slug: string): Promise<Admin | null> {
|
||||
return this.findOne({ phone, restaurant: { slug } }, { populate: ['restaurant', 'role', 'role.permissions'] });
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
+12
-12
@@ -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;
|
||||
}
|
||||
@@ -13,6 +13,6 @@ export class RequestOtpDto {
|
||||
|
||||
@IsNotEmpty()
|
||||
@IsString()
|
||||
@ApiProperty({ example: '', description: 'Restuarant slug' })
|
||||
@ApiProperty({ example: '', description: 'restaurant slug' })
|
||||
slug: string;
|
||||
}
|
||||
|
||||
@@ -19,6 +19,6 @@ export class VerifyOtpDto {
|
||||
|
||||
@IsNotEmpty()
|
||||
@IsString()
|
||||
@ApiProperty({ example: '', description: 'Restuarant slug' })
|
||||
@ApiProperty({ example: '', description: 'restaurant slug' })
|
||||
slug: string;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import { EntityRepository } from '@mikro-orm/postgresql';
|
||||
import type { Restaurant } from '../entities/restaurant.entity';
|
||||
import { EntityManager, EntityRepository } from '@mikro-orm/postgresql';
|
||||
import { Restaurant } from '../entities/restaurant.entity';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
// import { PaginationUtils } from '../../utils/services/pagination.utils';
|
||||
|
||||
export class RestRepository extends EntityRepository<Restaurant> {}
|
||||
@Injectable()
|
||||
export class RestRepository extends EntityRepository<Restaurant> {
|
||||
constructor(readonly em: EntityManager) {
|
||||
super(em, Restaurant);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,10 +3,16 @@ import { CreateRestaurantDto } from './dto/create-restaurant.dto';
|
||||
import { UpdateRestaurantDto } from './dto/update-restaurant.dto';
|
||||
import { Restaurant } from './entities/restaurant.entity';
|
||||
import { EntityManager } from '@mikro-orm/postgresql';
|
||||
import { RestRepository } from './repositories/rest.repository';
|
||||
import { InjectRepository } from '@mikro-orm/nestjs';
|
||||
|
||||
@Injectable()
|
||||
export class RestaurantsService {
|
||||
constructor(private readonly em: EntityManager) {}
|
||||
constructor(
|
||||
private readonly em: EntityManager,
|
||||
@InjectRepository(Restaurant)
|
||||
private readonly restRepository: RestRepository,
|
||||
) {}
|
||||
|
||||
async create(dto: CreateRestaurantDto): Promise<Restaurant> {
|
||||
const restaurant = this.em.create(Restaurant, {
|
||||
@@ -19,7 +25,7 @@ export class RestaurantsService {
|
||||
}
|
||||
|
||||
findAll() {
|
||||
return `This action returns all restaurants`;
|
||||
return this.restRepository.findAll();
|
||||
}
|
||||
|
||||
async findBySlug(slug: string): Promise<Restaurant> {
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Entity, ManyToOne, Opt, Property } from '@mikro-orm/core';
|
||||
|
||||
import { User } from './user.entity';
|
||||
import { BaseEntity } from '../../../common/entities/base.entity';
|
||||
import { Restaurant } from '../../../modules/restaurants/entities/restaurant.entity';
|
||||
|
||||
@Entity({ tableName: 'refreshtokens' })
|
||||
export class RefreshToken extends BaseEntity {
|
||||
@@ -11,6 +12,9 @@ export class RefreshToken extends BaseEntity {
|
||||
@ManyToOne(() => User, { deleteRule: 'cascade' })
|
||||
user!: User;
|
||||
|
||||
@ManyToOne(() => Restaurant, { deleteRule: 'cascade' })
|
||||
restaurant!: Restaurant;
|
||||
|
||||
@Property({ type: 'timestamptz' })
|
||||
expiresAt!: Date;
|
||||
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
import { EntityRepository } from '@mikro-orm/postgresql';
|
||||
import type { User } from '../entities/user.entity';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { EntityManager, EntityRepository } from '@mikro-orm/postgresql';
|
||||
import { User } from '../entities/user.entity';
|
||||
|
||||
export class UserRepository extends EntityRepository<User> {}
|
||||
@Injectable()
|
||||
export class UserRepository extends EntityRepository<User> {
|
||||
constructor(readonly em: EntityManager) {
|
||||
super(em, User);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,10 +15,13 @@ export class UserController {
|
||||
@Get('/me')
|
||||
async getUser(@Req() req: AuthRequest) {
|
||||
const userId = req.userId;
|
||||
const restId = req.restId;
|
||||
const user = await this.userService.findById(userId);
|
||||
return {
|
||||
message: `GET request: Retrieving profile for authenticated user `,
|
||||
user,
|
||||
userId,
|
||||
restId,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -4,11 +4,12 @@ import { UserController } from './user.controller';
|
||||
import { MikroOrmModule } from '@mikro-orm/nestjs';
|
||||
import { User } from './entities/user.entity';
|
||||
import { JwtModule } from '@nestjs/jwt';
|
||||
import { UserRepository } from './repositories/user.repository';
|
||||
|
||||
@Module({
|
||||
providers: [UserService],
|
||||
providers: [UserService, UserRepository],
|
||||
controllers: [UserController],
|
||||
imports: [MikroOrmModule.forFeature([User]), JwtModule],
|
||||
exports: [UserService],
|
||||
exports: [UserService, UserRepository],
|
||||
})
|
||||
export class UserModule {}
|
||||
|
||||
@@ -1,17 +1,16 @@
|
||||
import { Injectable, NotFoundException } from '@nestjs/common';
|
||||
import { InjectRepository } from '@mikro-orm/nestjs';
|
||||
import { EntityRepository, FilterQuery } from '@mikro-orm/core';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { FilterQuery } from '@mikro-orm/core';
|
||||
import { User } from './entities/user.entity';
|
||||
import { EntityManager } from '@mikro-orm/postgresql';
|
||||
// import { UpdateUserDto } from './dto/update-user.dto';
|
||||
import { FindUsersDto } from './dto/find-user.dto';
|
||||
import { PaginatedResult } from 'src/common/interfaces/pagination.interface';
|
||||
import { UserRepository } from './repositories/user.repository';
|
||||
|
||||
@Injectable()
|
||||
export class UserService {
|
||||
constructor(
|
||||
@InjectRepository(User)
|
||||
private readonly userRepository: EntityRepository<User>,
|
||||
private readonly userRepository: UserRepository,
|
||||
private readonly em: EntityManager,
|
||||
) {}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user