update
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
import { Controller, Post, Body, HttpCode, HttpStatus } from '@nestjs/common';
|
import { Controller, Post, Body, HttpCode, HttpStatus, Get } from '@nestjs/common';
|
||||||
import { AdminService } from './admin.service';
|
import { AdminService } from './admin.service';
|
||||||
import { CreateAdminDto } from './dto/create-admin.dto';
|
import { CreateAdminDto } from './dto/create-admin.dto';
|
||||||
import { ApiTags, ApiOperation, ApiBody, ApiResponse } from '@nestjs/swagger';
|
import { ApiTags, ApiOperation, ApiBody, ApiResponse } from '@nestjs/swagger';
|
||||||
@@ -8,6 +8,14 @@ import { ApiTags, ApiOperation, ApiBody, ApiResponse } from '@nestjs/swagger';
|
|||||||
export class AdminController {
|
export class AdminController {
|
||||||
constructor(private readonly adminService: AdminService) {}
|
constructor(private readonly adminService: AdminService) {}
|
||||||
|
|
||||||
|
@Get()
|
||||||
|
@ApiOperation({ summary: 'admin' })
|
||||||
|
@ApiResponse({ status: 201, description: 'Admins' })
|
||||||
|
async getAll() {
|
||||||
|
const admin = await this.adminService.findAll();
|
||||||
|
return admin;
|
||||||
|
}
|
||||||
|
|
||||||
@Post()
|
@Post()
|
||||||
@HttpCode(HttpStatus.CREATED)
|
@HttpCode(HttpStatus.CREATED)
|
||||||
@ApiOperation({ summary: 'Create a new admin' })
|
@ApiOperation({ summary: 'Create a new admin' })
|
||||||
|
|||||||
@@ -21,6 +21,9 @@ export class AdminService {
|
|||||||
async findById(id: string): Promise<Admin | null> {
|
async findById(id: string): Promise<Admin | null> {
|
||||||
return this.adminRepository.findOne({ id });
|
return this.adminRepository.findOne({ id });
|
||||||
}
|
}
|
||||||
|
async findAll(): Promise<Admin[]> {
|
||||||
|
return this.adminRepository.findAll();
|
||||||
|
}
|
||||||
|
|
||||||
async create(data: { phone: string; firstName?: string; lastName?: string; roleId: string; restId: string }) {
|
async create(data: { phone: string; firstName?: string; lastName?: string; roleId: string; restId: string }) {
|
||||||
const { phone, firstName, lastName, roleId, restId } = data;
|
const { phone, firstName, lastName, roleId, restId } = data;
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { ApiProperty } from '@nestjs/swagger';
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
import { IsOptional, IsString, IsNumber } from 'class-validator';
|
import { IsOptional, IsString, IsNumber, Min } from 'class-validator';
|
||||||
|
import { Type } from 'class-transformer';
|
||||||
|
|
||||||
export class FindRolesDto {
|
export class FindRolesDto {
|
||||||
@ApiProperty({ description: 'Search by role name', required: false })
|
@ApiProperty({ description: 'Search by role name', required: false })
|
||||||
@@ -14,11 +15,15 @@ export class FindRolesDto {
|
|||||||
|
|
||||||
@ApiProperty({ description: 'Page number', required: false, default: 1 })
|
@ApiProperty({ description: 'Page number', required: false, default: 1 })
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
|
@Type(() => Number)
|
||||||
@IsNumber()
|
@IsNumber()
|
||||||
|
@Min(1)
|
||||||
page?: number;
|
page?: number;
|
||||||
|
|
||||||
@ApiProperty({ description: 'Items per page', required: false, default: 20 })
|
@ApiProperty({ description: 'Items per page', required: false, default: 20 })
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
|
@Type(() => Number)
|
||||||
@IsNumber()
|
@IsNumber()
|
||||||
|
@Min(1)
|
||||||
limit?: number;
|
limit?: number;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import { Entity, ManyToOne, OneToOne, Property, Unique } from '@mikro-orm/core';
|
import { Entity, ManyToOne, OneToOne, Property } from '@mikro-orm/core';
|
||||||
import { BaseEntity } from '../../../common/entities/base.entity';
|
import { BaseEntity } from '../../../common/entities/base.entity';
|
||||||
import { Restaurant } from '../../../modules/restaurants/entities/restaurant.entity';
|
import { Restaurant } from '../../../modules/restaurants/entities/restaurant.entity';
|
||||||
import { Role } from './role.entity';
|
import { Role } from './role.entity';
|
||||||
|
|
||||||
@Entity({ tableName: 'admins' })
|
@Entity({ tableName: 'admins' })
|
||||||
// Add the Unique constraint for the combination of 'phone' and 'restaurant'
|
// Add the Unique constraint for the combination of 'phone' and 'restaurant'
|
||||||
@Unique({ properties: ['phone', 'restaurant'] })
|
// @Unique({ properties: ['phone', 'restaurant'] })
|
||||||
export class Admin extends BaseEntity {
|
export class Admin extends BaseEntity {
|
||||||
@Property({ nullable: true })
|
@Property({ nullable: true })
|
||||||
firstName?: string;
|
firstName?: string;
|
||||||
|
|||||||
@@ -9,6 +9,10 @@ export class AdminRepository extends EntityRepository<Admin> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async findByPhoneAndrestaurantSlug(phone: string, slug: string): Promise<Admin | null> {
|
async findByPhoneAndrestaurantSlug(phone: string, slug: string): Promise<Admin | null> {
|
||||||
return this.findOne({ phone, restaurant: { slug } }, { populate: ['restaurant', 'role', 'role.permissions'] });
|
return this.em.findOne(
|
||||||
|
Admin,
|
||||||
|
{ phone, restaurant: { slug } },
|
||||||
|
{ populate: ['restaurant', 'role', 'role.permissions'] },
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ export class AuthService {
|
|||||||
async requestOtpAdmin(dto: RequestOtpDto) {
|
async requestOtpAdmin(dto: RequestOtpDto) {
|
||||||
const { phone, slug } = dto;
|
const { phone, slug } = dto;
|
||||||
|
|
||||||
const admin = await this.adminRepository.find({ phone });
|
const admin = await this.adminRepository.findByPhoneAndrestaurantSlug(phone, slug);
|
||||||
if (!admin) {
|
if (!admin) {
|
||||||
throw new NotFoundException();
|
throw new NotFoundException();
|
||||||
}
|
}
|
||||||
@@ -45,9 +45,9 @@ export class AuthService {
|
|||||||
|
|
||||||
await this.cacheService.set(`otp-admin:${slug}:${phone}`, code, 160);
|
await this.cacheService.set(`otp-admin:${slug}:${phone}`, code, 160);
|
||||||
|
|
||||||
await this.smsService.sendOtp(phone, code);
|
// await this.smsService.sendOtp(phone, code);
|
||||||
|
|
||||||
return true;
|
return code;
|
||||||
}
|
}
|
||||||
|
|
||||||
async verifyOtp(phone: string, slug: string, code: string) {
|
async verifyOtp(phone: string, slug: string, code: string) {
|
||||||
@@ -65,14 +65,16 @@ export class AuthService {
|
|||||||
throw new BadRequestException(RestMessage.NOT_FOUND);
|
throw new BadRequestException(RestMessage.NOT_FOUND);
|
||||||
}
|
}
|
||||||
|
|
||||||
const tokens = await this.tokensService.generateTokens(user, rest);
|
const tokens = await this.tokensService.generateTokens(user.id, rest);
|
||||||
|
|
||||||
return { tokens, user };
|
return { tokens, user };
|
||||||
}
|
}
|
||||||
|
|
||||||
async verifyOtpAdmin(phone: string, slug: string, code: string) {
|
async verifyOtpAdmin(phone: string, slug: string, code: string) {
|
||||||
const cachedCode = await this.cacheService.get(`otp-admin:${slug}:${phone}`);
|
const cachedCode = await this.cacheService.get(`otp-admin:${slug}:${phone}`);
|
||||||
|
|
||||||
if (!cachedCode) throw new BadRequestException('OTP expired or not found');
|
if (!cachedCode) throw new BadRequestException('OTP expired or not found');
|
||||||
|
|
||||||
if (cachedCode !== code) throw new BadRequestException('Invalid OTP');
|
if (cachedCode !== code) throw new BadRequestException('Invalid OTP');
|
||||||
|
|
||||||
await this.cacheService.del(`otp-admin:${slug}:${phone}`);
|
await this.cacheService.del(`otp-admin:${slug}:${phone}`);
|
||||||
|
|||||||
@@ -26,10 +26,10 @@ export class TokensService {
|
|||||||
private readonly cacheService: CacheService,
|
private readonly cacheService: CacheService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async generateTokens(user: User, rest: Restaurant, em?: EntityManager) {
|
async generateTokens(ownerId: string, rest: Restaurant, em?: EntityManager) {
|
||||||
return this.generateAccessAndRefreshToken(
|
return this.generateAccessAndRefreshToken(
|
||||||
{
|
{
|
||||||
userId: user.id,
|
userId: ownerId,
|
||||||
restId: rest.id,
|
restId: rest.id,
|
||||||
},
|
},
|
||||||
em,
|
em,
|
||||||
@@ -76,7 +76,7 @@ export class TokensService {
|
|||||||
const accessToken = await this.jwtService.signAsync(tokenPayload, { expiresIn: `${accessExpire}m` });
|
const accessToken = await this.jwtService.signAsync(tokenPayload, { expiresIn: `${accessExpire}m` });
|
||||||
const refreshToken = this.generateRefreshToken();
|
const refreshToken = this.generateRefreshToken();
|
||||||
|
|
||||||
await this.storeRefreshToken(payload.adminId, payload.restId, refreshToken, em);
|
await this.storeRefreshTokenAdmin(payload.adminId, payload.restId, refreshToken, em);
|
||||||
return {
|
return {
|
||||||
accessToken: { token: accessToken, expire: dayjs().add(accessExpire, 'minute').valueOf() },
|
accessToken: { token: accessToken, expire: dayjs().add(accessExpire, 'minute').valueOf() },
|
||||||
refreshToken: { token: refreshToken, expire: dayjs().add(refreshExpire, 'day').valueOf() },
|
refreshToken: { token: refreshToken, expire: dayjs().add(refreshExpire, 'day').valueOf() },
|
||||||
@@ -105,7 +105,52 @@ export class TokensService {
|
|||||||
const token = entityManager.create(RefreshToken, {
|
const token = entityManager.create(RefreshToken, {
|
||||||
token: refreshToken,
|
token: refreshToken,
|
||||||
restaurant,
|
restaurant,
|
||||||
user,
|
ownerId: userId,
|
||||||
|
expiresAt,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Use persist instead of persistAndFlush when in transaction
|
||||||
|
if (isExternalTransaction) {
|
||||||
|
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 storeRefreshTokenAdmin(adminId: string, restId: 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 admin = await entityManager.findOne(Admin, { id: adminId });
|
||||||
|
if (!admin) 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,
|
||||||
|
ownerId: adminId,
|
||||||
expiresAt,
|
expiresAt,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -138,7 +183,7 @@ export class TokensService {
|
|||||||
RefreshToken,
|
RefreshToken,
|
||||||
{ token: oldRefreshToken },
|
{ token: oldRefreshToken },
|
||||||
{
|
{
|
||||||
populate: ['user', 'restaurant'],
|
populate: ['restaurant'],
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -158,7 +203,7 @@ export class TokensService {
|
|||||||
entityManager.remove(token);
|
entityManager.remove(token);
|
||||||
|
|
||||||
// Generate new tokens within the same transaction
|
// Generate new tokens within the same transaction
|
||||||
const tokens = await this.generateTokens(token.user, token.restaurant, entityManager);
|
const tokens = await this.generateTokens(token.ownerId, token.restaurant, entityManager);
|
||||||
|
|
||||||
// Commit the transaction
|
// Commit the transaction
|
||||||
await entityManager.flush();
|
await entityManager.flush();
|
||||||
@@ -180,7 +225,7 @@ export class TokensService {
|
|||||||
try {
|
try {
|
||||||
await entityManager.begin();
|
await entityManager.begin();
|
||||||
|
|
||||||
await entityManager.nativeDelete(RefreshToken, { user: { id: userId } });
|
await entityManager.nativeDelete(RefreshToken, { ownerId: userId });
|
||||||
this.logger.log(`Successfully deleted all refresh tokens for user ${userId}`);
|
this.logger.log(`Successfully deleted all refresh tokens for user ${userId}`);
|
||||||
|
|
||||||
await entityManager.commit();
|
await entityManager.commit();
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { Entity, ManyToOne, Opt, Property } from '@mikro-orm/core';
|
import { Entity, ManyToOne, Opt, Property } from '@mikro-orm/core';
|
||||||
|
|
||||||
import { User } from './user.entity';
|
|
||||||
import { BaseEntity } from '../../../common/entities/base.entity';
|
import { BaseEntity } from '../../../common/entities/base.entity';
|
||||||
import { Restaurant } from '../../../modules/restaurants/entities/restaurant.entity';
|
import { Restaurant } from '../../../modules/restaurants/entities/restaurant.entity';
|
||||||
|
|
||||||
@@ -9,8 +8,8 @@ export class RefreshToken extends BaseEntity {
|
|||||||
@Property({ type: 'varchar', length: 255 })
|
@Property({ type: 'varchar', length: 255 })
|
||||||
token!: string;
|
token!: string;
|
||||||
|
|
||||||
@ManyToOne(() => User, { deleteRule: 'cascade' })
|
@Property()
|
||||||
user!: User;
|
ownerId!: string;
|
||||||
|
|
||||||
@ManyToOne(() => Restaurant, { deleteRule: 'cascade' })
|
@ManyToOne(() => Restaurant, { deleteRule: 'cascade' })
|
||||||
restaurant!: Restaurant;
|
restaurant!: Restaurant;
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { Entity, Property, OneToMany, Collection, OneToOne } from '@mikro-orm/core';
|
import { Entity, Property, OneToOne } from '@mikro-orm/core';
|
||||||
import { RefreshToken } from './refresh-token.entity';
|
|
||||||
import { BaseEntity } from '../../../common/entities/base.entity';
|
import { BaseEntity } from '../../../common/entities/base.entity';
|
||||||
import { Restaurant } from '../../restaurants/entities/restaurant.entity';
|
import { Restaurant } from '../../restaurants/entities/restaurant.entity';
|
||||||
|
|
||||||
@@ -8,9 +7,6 @@ export class User extends BaseEntity {
|
|||||||
@OneToOne(() => Restaurant, { owner: true })
|
@OneToOne(() => Restaurant, { owner: true })
|
||||||
restaurant!: Restaurant;
|
restaurant!: Restaurant;
|
||||||
|
|
||||||
@OneToMany(() => RefreshToken, token => token.user)
|
|
||||||
refreshTokens = new Collection<RefreshToken>(this);
|
|
||||||
|
|
||||||
@Property()
|
@Property()
|
||||||
firstName!: string;
|
firstName!: string;
|
||||||
|
|
||||||
|
|||||||
@@ -5,11 +5,12 @@ import { MikroOrmModule } from '@mikro-orm/nestjs';
|
|||||||
import { User } from './entities/user.entity';
|
import { User } from './entities/user.entity';
|
||||||
import { JwtModule } from '@nestjs/jwt';
|
import { JwtModule } from '@nestjs/jwt';
|
||||||
import { UserRepository } from './repositories/user.repository';
|
import { UserRepository } from './repositories/user.repository';
|
||||||
|
import { RefreshToken } from './entities/refresh-token.entity';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
providers: [UserService, UserRepository],
|
providers: [UserService, UserRepository],
|
||||||
controllers: [AdminUserController],
|
controllers: [AdminUserController],
|
||||||
imports: [MikroOrmModule.forFeature([User]), JwtModule],
|
imports: [MikroOrmModule.forFeature([User, RefreshToken]), JwtModule],
|
||||||
exports: [UserService, UserRepository],
|
exports: [UserService, UserRepository],
|
||||||
})
|
})
|
||||||
export class UserModule {}
|
export class UserModule {}
|
||||||
|
|||||||
Reference in New Issue
Block a user