From e55d57047d35b4530d859b5c77a206603d64d5bc Mon Sep 17 00:00:00 2001 From: hamid zarghami Date: Fri, 3 Jul 2026 13:19:17 +0330 Subject: [PATCH] /me --- src/auth/auth.controller.ts | 11 ++++- src/auth/auth.module.ts | 2 + src/auth/auth.service.ts | 37 +++++++++++++++++ src/auth/dto/me-response.dto.ts | 40 +++++++++++++++++++ .../salon-subscriptions.service.ts | 15 +++++-- 5 files changed, 101 insertions(+), 4 deletions(-) create mode 100644 src/auth/dto/me-response.dto.ts diff --git a/src/auth/auth.controller.ts b/src/auth/auth.controller.ts index 58b895c..d97fe36 100644 --- a/src/auth/auth.controller.ts +++ b/src/auth/auth.controller.ts @@ -1,4 +1,4 @@ -import { Body, Controller, Post } from '@nestjs/common'; +import { Body, Controller, Get, Post } from '@nestjs/common'; import { ApiTags } from '@nestjs/swagger'; import { AllowWithoutSubscription } from '../auth/decorators/allow-without-subscription.decorator'; import { @@ -18,6 +18,7 @@ import { Public } from './decorators/public.decorator'; import { AuthTokensResponseDto } from './dto/auth-tokens-response.dto'; import { CheckSalonMobileDto } from './dto/check-salon-mobile.dto'; import { CheckSalonMobileResponseDto } from './dto/check-salon-mobile-response.dto'; +import { MeResponseDto } from './dto/me-response.dto'; import { RefreshTokenDto } from './dto/refresh-token.dto'; import { RequestOtpDto } from './dto/request-otp.dto'; import { RequestOtpResponseDto } from './dto/request-otp-response.dto'; @@ -89,6 +90,14 @@ export class AuthController { return this.authService.refresh(dto.refreshToken); } + @AllowWithoutSubscription() + @ApiProtectedEndpoint('Get current authenticated user profile') + @ApiStandardOkResponse(MeResponseDto) + @Get('me') + getMe(@CurrentUser() user: AuthUser) { + return this.authService.getMe(user); + } + @AllowWithoutSubscription() @ApiProtectedEndpoint('Logout and invalidate refresh token') @ApiStandardEmptyResponse() diff --git a/src/auth/auth.module.ts b/src/auth/auth.module.ts index a82dc55..4058e8f 100644 --- a/src/auth/auth.module.ts +++ b/src/auth/auth.module.ts @@ -8,6 +8,7 @@ import { AuthorizationModule } from '../authorization/authorization.module'; import { Customer } from '../customers/entities/customer.entity'; import { Salon } from '../salons/entities/salon.entity'; import { SmsModule } from '../sms/sms.module'; +import { SubscriptionsModule } from '../subscriptions/subscriptions.module'; import { UsersModule } from '../users/users.module'; import { AuthController } from './auth.controller'; import { AuthService } from './auth.service'; @@ -33,6 +34,7 @@ import { JwtStrategy } from './strategies/jwt.strategy'; }), UsersModule, SmsModule, + SubscriptionsModule, AuthorizationModule, ], controllers: [AuthController], diff --git a/src/auth/auth.service.ts b/src/auth/auth.service.ts index 63e0995..09618c9 100644 --- a/src/auth/auth.service.ts +++ b/src/auth/auth.service.ts @@ -15,6 +15,8 @@ import { Repository, IsNull } from 'typeorm'; import { Customer } from '../customers/entities/customer.entity'; import { Salon } from '../salons/entities/salon.entity'; import { SmsService } from '../sms/sms.service'; +import { SalonSubscription } from '../subscriptions/entities/salon-subscription.entity'; +import { SalonSubscriptionsService } from '../subscriptions/salon-subscriptions.service'; import { UserRole } from '../users/enums/user-role.enum'; import { User } from '../users/entities/user.entity'; import { UsersService } from '../users/users.service'; @@ -22,6 +24,7 @@ import { VerifyOtpDto } from './dto/verify-otp.dto'; import { VerifySalonOtpDto } from './dto/verify-salon-otp.dto'; import { Otp } from './entities/otp.entity'; import { RefreshToken } from './entities/refresh-token.entity'; +import { AuthUser } from './interfaces/auth-user.interface'; import { JwtPayload } from './interfaces/jwt-payload.interface'; export interface AuthTokensResponse { @@ -40,6 +43,12 @@ export interface SalonAuthResponse extends AuthTokensResponse { salon?: Salon; } +export interface MeResponse { + user: AuthTokensResponse['user']; + salons?: Salon[]; + activeSubscription?: SalonSubscription | null; +} + @Injectable() export class AuthService { private readonly logger = new Logger(AuthService.name); @@ -60,6 +69,7 @@ export class AuthService { @InjectRepository(Salon) private readonly salonsRepository: Repository, private readonly usersService: UsersService, + private readonly salonSubscriptionsService: SalonSubscriptionsService, private readonly smsService: SmsService, private readonly jwtService: JwtService, private readonly configService: ConfigService, @@ -336,6 +346,33 @@ export class AuthService { throw new UnauthorizedException('Invalid refresh token'); } + async getMe(authUser: AuthUser): Promise { + const user = await this.usersService.findOne(authUser.id); + + const response: MeResponse = { + user: { + id: user.id, + firstName: user.firstName, + lastName: user.lastName, + mobile: user.mobile, + role: user.role, + }, + }; + + if (user.role === UserRole.SALON) { + response.salons = await this.salonsRepository.find({ + where: { ownerId: user.id }, + order: { createdAt: 'ASC' }, + }); + response.activeSubscription = + await this.salonSubscriptionsService.findActiveSubscriptionForOwner( + user.id, + ); + } + + return response; + } + private async resolveUserForLogin(dto: VerifyOtpDto): Promise { const existingUser = await this.usersService.findByMobile(dto.mobile); diff --git a/src/auth/dto/me-response.dto.ts b/src/auth/dto/me-response.dto.ts new file mode 100644 index 0000000..aea0bba --- /dev/null +++ b/src/auth/dto/me-response.dto.ts @@ -0,0 +1,40 @@ +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { Salon } from '../../salons/entities/salon.entity'; +import { SalonSubscription } from '../../subscriptions/entities/salon-subscription.entity'; +import { UserRole } from '../../users/enums/user-role.enum'; + +class MeUserResponseDto { + @ApiProperty({ format: 'uuid' }) + id: string; + + @ApiProperty() + firstName: string; + + @ApiProperty() + lastName: string; + + @ApiProperty() + mobile: string; + + @ApiProperty({ enum: UserRole }) + role: UserRole; +} + +export class MeResponseDto { + @ApiProperty({ type: MeUserResponseDto }) + user: MeUserResponseDto; + + @ApiPropertyOptional({ + type: Salon, + isArray: true, + description: 'Present for salon owners', + }) + salons?: Salon[]; + + @ApiPropertyOptional({ + type: SalonSubscription, + nullable: true, + description: 'Active subscription for salon owners, null if none', + }) + activeSubscription?: SalonSubscription | null; +} diff --git a/src/subscriptions/salon-subscriptions.service.ts b/src/subscriptions/salon-subscriptions.service.ts index b4b0be0..1d3dbfb 100644 --- a/src/subscriptions/salon-subscriptions.service.ts +++ b/src/subscriptions/salon-subscriptions.service.ts @@ -154,25 +154,34 @@ export class SalonSubscriptionsService { } async hasActiveSubscriptionForOwner(ownerId: string): Promise { + const subscription = await this.findActiveSubscriptionForOwner(ownerId); + return subscription !== null; + } + + async findActiveSubscriptionForOwner( + ownerId: string, + ): Promise { const subscriptions = await this.subscriptionsRepository .createQueryBuilder('subscription') - .innerJoin('subscription.salon', 'salon') + .innerJoinAndSelect('subscription.salon', 'salon') + .innerJoinAndSelect('subscription.plan', 'plan') .where('salon.ownerId = :ownerId', { ownerId }) .andWhere('subscription.status = :status', { status: SalonSubscriptionStatus.ACTIVE, }) + .orderBy('subscription.createdAt', 'DESC') .getMany(); const now = new Date(); for (const subscription of subscriptions) { if (subscription.endDate > now) { - return true; + return subscription; } subscription.status = SalonSubscriptionStatus.EXPIRED; await this.subscriptionsRepository.save(subscription); } - return false; + return null; } async consumeSms(salonId: string, count = 1): Promise {