add : direct login

This commit is contained in:
2026-01-05 10:09:37 +03:30
parent a0437b02af
commit 267bae368d
4 changed files with 199 additions and 25 deletions
-12
View File
@@ -1,12 +0,0 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsString } from 'class-validator';
export class DirectLoginDto {
@ApiProperty({ example: '09123456789', description: 'شماره تلفن کاربر' })
@IsString()
phone!: string;
@ApiProperty({ example: 'zhivan', description: 'شناسه (slug) رستوران' })
@IsString()
slug!: string;
}
+12 -5
View File
@@ -12,7 +12,6 @@ import { FindRestaurantsDto } from "./DTO/find-restaurants.dto";
import { SetupRestaurantDto } from "./DTO/create-restaurant.dto"; import { SetupRestaurantDto } from "./DTO/create-restaurant.dto";
import { UpdateRestaurantDto } from "./DTO/update-restaurant.dto"; import { UpdateRestaurantDto } from "./DTO/update-restaurant.dto";
import { CreateMyRestaurantAdminDto } from "./DTO/create-restaurant-admin.dto"; import { CreateMyRestaurantAdminDto } from "./DTO/create-restaurant-admin.dto";
import { DirectLoginDto } from "./DTO/direct-login.dto";
import { AuthGuards } from "../../common/decorators/auth-guard.decorator"; import { AuthGuards } from "../../common/decorators/auth-guard.decorator";
import { PermissionsDec } from "../../common/decorators/permission.decorator"; import { PermissionsDec } from "../../common/decorators/permission.decorator";
import { PermissionEnum } from "../users/enums/permission.enum"; import { PermissionEnum } from "../users/enums/permission.enum";
@@ -127,6 +126,13 @@ export class DmenuController {
return this.restaurantService.findAll(queryDto); return this.restaurantService.findAll(queryDto);
} }
@PermissionsDec(PermissionEnum.DMENU)
@Get('admin/dmenu/restaurants/:id')
@ApiOperation({ summary: "Get restaurant by ID" })
@ApiParam({ name: "id", required: true, type: String })
getRestaurantById(@Param("id") id: string) {
return this.restaurantService.getRestaurantById(id);
}
@ApiBearerAuth() @ApiBearerAuth()
@Get('admin/dmenu/restaurants/sms-count') @Get('admin/dmenu/restaurants/sms-count')
@@ -196,9 +202,10 @@ export class DmenuController {
return this.restaurantService.getRestaurantSubscription(subscriptionId); return this.restaurantService.getRestaurantSubscription(subscriptionId);
} }
@Post('dmenu/auth/direct-login') @Post('dmenu/auth/direct-login/:userSubscriptionId')
@ApiOperation({ summary: "Direct login with phone number and restaurant slug" }) @ApiOperation({ summary: "Direct login with user subscription ID" })
directLogin(@Body() dto: DirectLoginDto) { @ApiParam({ name: "userSubscriptionId", required: true, type: String })
return this.restaurantService.directLogin(dto); directLogin(@Param("userSubscriptionId") userSubscriptionId: string, @UserDec("id") userId: string) {
return this.restaurantService.directLogin(userSubscriptionId, userId);
} }
} }
+7 -1
View File
@@ -7,9 +7,15 @@ import { IconsService } from "./providers/icons.service";
import { RestaurantService } from "./providers/restaurant.service"; import { RestaurantService } from "./providers/restaurant.service";
import { dmenuConfig } from "../../configs/icons.config"; import { dmenuConfig } from "../../configs/icons.config";
import { SubscriptionsModule } from "../subscriptions/subscriptions.module"; import { SubscriptionsModule } from "../subscriptions/subscriptions.module";
import { UsersModule } from "../users/users.module";
import { DanakServicesModule } from "../danak-services/danak-services.module";
@Module({ @Module({
imports: [forwardRef(() => SubscriptionsModule)], imports: [
forwardRef(() => SubscriptionsModule),
forwardRef(() => UsersModule),
forwardRef(() => DanakServicesModule),
],
controllers: [DmenuController], controllers: [DmenuController],
providers: [ providers: [
IconsService, IconsService,
@@ -3,6 +3,8 @@ import { BadRequestException, HttpException, Injectable, Logger } from "@nestjs/
import { Inject } from "@nestjs/common"; import { Inject } from "@nestjs/common";
import { AxiosError } from "axios"; import { AxiosError } from "axios";
import { catchError, firstValueFrom, throwError } from "rxjs"; import { catchError, firstValueFrom, throwError } from "rxjs";
import { DataSource } from "typeorm";
import slugify from "slugify";
import { IDmenuConfig } from "../../../configs/icons.config"; import { IDmenuConfig } from "../../../configs/icons.config";
import { DMENU_CONFIG } from "../constants"; import { DMENU_CONFIG } from "../constants";
@@ -13,7 +15,12 @@ import { CreateMyRestaurantAdminDto } from "../DTO/create-restaurant-admin.dto";
import { PlanEnum, ISetupRestaurant, IExternalApiResponse } from "../interface/plan.interface"; import { PlanEnum, ISetupRestaurant, IExternalApiResponse } from "../interface/plan.interface";
import { SubscriptionsService } from "../../subscriptions/providers/subscriptions.service"; import { SubscriptionsService } from "../../subscriptions/providers/subscriptions.service";
import { SubscriptionMessage } from "../../../common/enums/message.enum"; import { SubscriptionMessage } from "../../../common/enums/message.enum";
import { DirectLoginDto } from "../DTO/direct-login.dto"; import { UsersService } from "../../users/providers/users.service";
import { DanakServicesService } from "../../danak-services/providers/danak-services.service";
import { User } from "../../users/entities/user.entity";
import { Role } from "../../users/entities/role.entity";
import { RoleEnum } from "../../users/enums/role.enum";
import { SubscribeServiceDto } from "../../subscriptions/DTO/subscribe-service.dto";
@Injectable() @Injectable()
export class RestaurantService { export class RestaurantService {
@@ -23,7 +30,10 @@ export class RestaurantService {
@Inject(DMENU_CONFIG) private readonly config: IDmenuConfig, @Inject(DMENU_CONFIG) private readonly config: IDmenuConfig,
private readonly httpService: HttpService, private readonly httpService: HttpService,
private readonly subscriptionService: SubscriptionsService, private readonly subscriptionService: SubscriptionsService,
) {} private readonly usersService: UsersService,
private readonly danakServicesService: DanakServicesService,
private readonly dataSource: DataSource,
) { }
private getHeaders(): Record<string, string> { private getHeaders(): Record<string, string> {
const credentials = Buffer.from(`${this.config.username}:${this.config.password}`).toString("base64"); const credentials = Buffer.from(`${this.config.username}:${this.config.password}`).toString("base64");
@@ -86,6 +96,35 @@ export class RestaurantService {
} }
} }
async getRestaurantById(id: string) {
try {
const { data } = await firstValueFrom(
this.httpService
.get(`${this.config.baseUrl}/super-admin/restaurants/${id}`, {
headers: this.getHeaders(),
})
.pipe(
catchError((err: AxiosError) => {
this.logger.error(`Failed to get restaurant by ID: ${err.message}`, err.stack);
return throwError(() => err);
}),
),
);
return data;
} catch (error: unknown) {
if (error instanceof AxiosError && error.response) {
this.logger.error(`External API error response:`, error.response.data);
const errorResponse = {
...error.response.data,
message: error.response.data.error?.message || error.message,
};
throw new HttpException(errorResponse, error.response.status);
}
this.logger.error(`Error getting restaurant by ID: ${error instanceof Error ? error.message : "Unknown error"}`);
throw error;
}
}
async createRestaurant(dto: SetupRestaurantDto, userId: string) { async createRestaurant(dto: SetupRestaurantDto, userId: string) {
const { userSubscription } = await this.subscriptionService.getUserSubscriptionById(dto.userSubscriptionId, userId); const { userSubscription } = await this.subscriptionService.getUserSubscriptionById(dto.userSubscriptionId, userId);
if (!userSubscription) { if (!userSubscription) {
@@ -308,8 +347,116 @@ export class RestaurantService {
} }
} }
private async updateRestaurantSubscription(restaurantId: string, subscriptionId: string, subscriptionExpiryDate: Date) {
try {
const { data } = await firstValueFrom(
this.httpService
.patch(`${this.config.baseUrl}/super-admin/restaurants/${restaurantId}/subscription`, {
subscriptionId,
subscriptionExpiryDate,
}, {
headers: this.getHeaders(),
})
.pipe(
catchError((err: AxiosError) => {
this.logger.error(`Failed to update restaurant subscription: ${err.message}`, err.stack);
return throwError(() => err);
}),
),
);
return data;
} catch (error: unknown) {
if (error instanceof AxiosError && error.response) {
this.logger.error(`External API error response:`, error.response.data);
const errorResponse = {
...error.response.data,
message: error.response.data.error?.message || error.message,
};
throw new HttpException(errorResponse, error.response.status);
}
this.logger.error(`Error updating restaurant subscription: ${error instanceof Error ? error.message : "Unknown error"}`);
throw error;
}
}
private async findOrCreateUserByPhone(phone: string, dto: CreateMyRestaurantAdminDto) {
let user = await this.usersService.findOneWithPhone(phone);
if (!user) {
// Create a basic user for restaurant admin assignment
const queryRunner = this.dataSource.createQueryRunner();
try {
await queryRunner.connect();
await queryRunner.startTransaction();
const role = await queryRunner.manager.findOneBy(Role, { name: RoleEnum.USER });
if (!role) throw new BadRequestException("User role not found");
const userName = slugify(`u-${phone}`, { lower: true, trim: true });
const defaultFirstName = "کاربر";
const defaultLastName = userName;
user = queryRunner.manager.create(User, {
phone,
userName,
password: "temp_password", // This should be changed later
firstName: dto.firstName || defaultFirstName,
lastName: dto.lastName || defaultLastName,
roles: [role],
});
await queryRunner.manager.save(user);
// Note: Skipping user settings and wallet creation for now as they might be optional for this use case
await queryRunner.commitTransaction();
} catch (error) {
await queryRunner.rollbackTransaction();
throw error;
} finally {
await queryRunner.release();
}
}
return user;
}
async assignRestaurantAdmin(restaurantId: string, dto: CreateMyRestaurantAdminDto) { async assignRestaurantAdmin(restaurantId: string, dto: CreateMyRestaurantAdminDto) {
try { try {
// 1. Create user with phone if not exists
const user = await this.findOrCreateUserByPhone(dto.phone, dto);
// 2. Get restaurant by id
const restaurant = await this.getRestaurantById(restaurantId);
// 3. If restaurant doesn't have subscription id, create user subscription with free plan of dmenu service
let subscriptionId = restaurant.subscriptionId;
let subscriptionExpiryDate = restaurant.subscriptionExpiryDate;
if (!subscriptionId) {
const dmenuService = await this.danakServicesService.getServiceByName("dmenu");
const freePlan = await this.subscriptionService['subscriptionsPlanRepository'].findOne({
where: { service: { id: dmenuService.id }, isFree: true },
});
if (!freePlan) {
throw new BadRequestException("Free plan not found for dmenu service");
}
const subscriptionDto: SubscribeServiceDto = {
planId: freePlan.id,
businessName: restaurant.name || `Restaurant ${restaurantId}`,
businessPhone: dto.phone,
description: restaurant.description || `Restaurant admin for ${restaurant.name || restaurantId}`,
slug: slugify(restaurant.name || `restaurant-${restaurantId}`, { lower: true, strict: true }),
};
const subscriptionResult = await this.subscriptionService.subscribeToPlan(dmenuService.id, subscriptionDto, user.id);
subscriptionId = subscriptionResult.userSubscription.id;
subscriptionExpiryDate = subscriptionResult.userSubscription.endDate;
}
// 4. Update restaurant with subscription id and expiry date
await this.updateRestaurantSubscription(restaurantId, subscriptionId, subscriptionExpiryDate);
// 5. Assign restaurant admin
const { data } = await firstValueFrom( const { data } = await firstValueFrom(
this.httpService this.httpService
.post(`${this.config.baseUrl}/super-admin/restaurants/${restaurantId}/admins`, dto, { .post(`${this.config.baseUrl}/super-admin/restaurants/${restaurantId}/admins`, dto, {
@@ -322,7 +469,13 @@ export class RestaurantService {
}), }),
), ),
); );
return data;
return {
...data,
user,
subscriptionId,
subscriptionExpiryDate,
};
} catch (error: unknown) { } catch (error: unknown) {
if (error instanceof AxiosError && error.response) { if (error instanceof AxiosError && error.response) {
this.logger.error(`External API error response:`, error.response.data); this.logger.error(`External API error response:`, error.response.data);
@@ -369,13 +522,33 @@ export class RestaurantService {
} }
} }
async directLogin(dto: DirectLoginDto) { async directLogin(userSubscriptionId: string, userId: string) {
try { try {
// 1. Get userSubscription by id
const { userSubscription } = await this.subscriptionService.getUserSubscriptionById(userSubscriptionId, userId);
// 2. Get phone from userSubscription
const phone = userSubscription.businessPhone;
if (!phone) {
throw new BadRequestException("Phone number not found in user subscription");
}
// 3. Get restaurant by subscription id
const restaurant = await this.getRestaurantSubscription(userSubscriptionId);
console.log('restaurant', restaurant);
// 4. Get slug from restaurant
const slug = restaurant.data.slug;
if (!slug) {
throw new BadRequestException("Restaurant slug not found");
}
// 5. Login with phone and slug
const { data } = await firstValueFrom( const { data } = await firstValueFrom(
this.httpService this.httpService
.post(`${this.config.baseUrl}/super-admin/auth/direct-login`, { .post(`${this.config.baseUrl}/super-admin/auth/direct-login`, {
phone: dto.phone, phone,
slug: dto.slug, slug,
}, { }, {
headers: this.getHeaders(), headers: this.getHeaders(),
}) })