This commit is contained in:
2026-07-03 13:19:17 +03:30
parent fb4ddb4dac
commit e55d57047d
5 changed files with 101 additions and 4 deletions
+10 -1
View File
@@ -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()
+2
View File
@@ -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],
+37
View File
@@ -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<Salon>,
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<MeResponse> {
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<User> {
const existingUser = await this.usersService.findByMobile(dto.mobile);
+40
View File
@@ -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;
}
@@ -154,25 +154,34 @@ export class SalonSubscriptionsService {
}
async hasActiveSubscriptionForOwner(ownerId: string): Promise<boolean> {
const subscription = await this.findActiveSubscriptionForOwner(ownerId);
return subscription !== null;
}
async findActiveSubscriptionForOwner(
ownerId: string,
): Promise<SalonSubscription | null> {
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<void> {