first commit

This commit is contained in:
Oscar
2026-06-02 15:52:22 +03:00
commit dc44cdd639
105 changed files with 14674 additions and 0 deletions

View File

@@ -0,0 +1,55 @@
import { Body, Controller, Post, UseGuards } from '@nestjs/common';
import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger';
import { AuthService } from './auth.service';
import { LoginDto } from './dto/login.dto';
import { RefreshTokenDto } from './dto/refresh-token.dto';
import { RegisterDto } from './dto/register.dto';
import { Public } from '../common/decorators/public.decorator';
import { CurrentUser } from '../common/decorators/current-user.decorator';
import { JwtAuthGuard } from '../common/guards/jwt-auth.guard';
@ApiTags('auth')
@Controller('auth')
export class AuthController {
constructor(private readonly authService: AuthService) {}
@Public()
@Post('register')
@ApiOperation({ summary: 'Register new user' })
register(@Body() dto: RegisterDto) {
return this.authService.register(dto);
}
@Public()
@Post('login')
@ApiOperation({ summary: 'Login with phone and password' })
login(@Body() dto: LoginDto) {
return this.authService.login(dto);
}
@UseGuards(JwtAuthGuard)
@Post('logout')
@ApiBearerAuth()
@ApiOperation({ summary: 'Logout current user' })
logout(@CurrentUser('id') userId: string) {
return this.authService.logout(userId);
}
@Public()
@Post('refresh')
@ApiOperation({ summary: 'Refresh access token' })
refresh(@Body() dto: RefreshTokenDto) {
return this.authService.refreshTokens(dto.refreshToken);
}
@UseGuards(JwtAuthGuard)
@Post('fcm-token')
@ApiBearerAuth()
@ApiOperation({ summary: 'Update FCM push token' })
updateFcmToken(
@CurrentUser('id') userId: string,
@Body('fcmToken') fcmToken: string,
) {
return this.authService.updateFcmToken(userId, fcmToken);
}
}

25
src/auth/auth.module.ts Normal file
View File

@@ -0,0 +1,25 @@
import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { JwtModule } from '@nestjs/jwt';
import { PassportModule } from '@nestjs/passport';
import { AuthController } from './auth.controller';
import { AuthService } from './auth.service';
import { JwtStrategy } from './strategies/jwt.strategy';
@Module({
imports: [
PassportModule,
JwtModule.registerAsync({
imports: [ConfigModule],
inject: [ConfigService],
useFactory: (configService: ConfigService) => ({
secret: configService.get<string>('jwt.secret'),
signOptions: { expiresIn: configService.get<string>('jwt.expiresIn') },
}),
}),
],
controllers: [AuthController],
providers: [AuthService, JwtStrategy],
exports: [AuthService, JwtModule],
})
export class AuthModule {}

160
src/auth/auth.service.ts Normal file
View File

@@ -0,0 +1,160 @@
import {
BadRequestException,
ConflictException,
Injectable,
UnauthorizedException,
} from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { JwtService } from '@nestjs/jwt';
import * as bcrypt from 'bcrypt';
import { eq } from 'drizzle-orm';
import { DrizzleService } from '../database/drizzle.service';
import { role, user } from '../database/schema';
import { RedisService } from '../redis/redis.service';
import { LoginDto } from './dto/login.dto';
import { RegisterDto } from './dto/register.dto';
import { JwtPayload } from './strategies/jwt.strategy';
@Injectable()
export class AuthService {
constructor(
private readonly drizzleService: DrizzleService,
private readonly jwtService: JwtService,
private readonly configService: ConfigService,
private readonly redisService: RedisService,
) {}
async register(dto: RegisterDto) {
const existing = await this.drizzleService.db
.select({ id: user.id })
.from(user)
.where(eq(user.phone, dto.phone))
.limit(1);
if (existing.length > 0) {
throw new ConflictException('Phone number already registered');
}
const [userRole] = await this.drizzleService.db
.select()
.from(role)
.where(eq(role.name, 'user'))
.limit(1);
const hashedPassword = await bcrypt.hash(dto.password, 10);
const [newUser] = await this.drizzleService.db
.insert(user)
.values({
phone: dto.phone,
password: hashedPassword,
status: 'active' as any,
roleId: userRole?.id || null,
} as any)
.returning();
return this.generateTokens(newUser);
}
async login(dto: LoginDto) {
const [foundUser] = await this.drizzleService.db
.select()
.from(user)
.where(eq(user.phone, dto.phone))
.limit(1);
if (!foundUser) {
throw new UnauthorizedException('Invalid credentials');
}
if (foundUser.status === 'banned') {
throw new UnauthorizedException('Account is banned');
}
const isPasswordValid = await bcrypt.compare(dto.password, foundUser.password);
if (!isPasswordValid) {
throw new UnauthorizedException('Invalid credentials');
}
const userRoleName = await this.getUserRoleName(foundUser.roleId);
return this.generateTokens(foundUser, userRoleName);
}
async logout(userId: string) {
await this.redisService.del(`refresh_token:${userId}`);
return { message: 'Logged out successfully' };
}
async refreshTokens(refreshToken: string) {
try {
const payload = this.jwtService.verify<JwtPayload & { type: string }>(refreshToken, {
secret: this.configService.get<string>('jwt.secret'),
});
if (payload.type !== 'refresh') {
throw new UnauthorizedException('Invalid token type');
}
const storedToken = await this.redisService.get(`refresh_token:${payload.sub}`);
if (!storedToken || storedToken !== refreshToken) {
throw new UnauthorizedException('Refresh token expired or invalid');
}
const [foundUser] = await this.drizzleService.db
.select()
.from(user)
.where(eq(user.id, payload.sub))
.limit(1);
if (!foundUser) throw new UnauthorizedException();
const userRoleName = await this.getUserRoleName(foundUser.roleId);
return this.generateTokens(foundUser, userRoleName);
} catch {
throw new UnauthorizedException('Invalid or expired refresh token');
}
}
async updateFcmToken(userId: string, fcmToken: string) {
await this.drizzleService.db
.update(user)
.set({ fcmToken } as any)
.where(eq(user.id, userId));
return { message: 'FCM token updated' };
}
private async getUserRoleName(roleId: string | null): Promise<string> {
if (!roleId) return 'user';
const [r] = await this.drizzleService.db
.select({ name: role.name })
.from(role)
.where(eq(role.id, roleId))
.limit(1);
return r?.name || 'user';
}
private async generateTokens(userEntity: any, roleName?: string) {
const resolvedRole = roleName || (await this.getUserRoleName(userEntity.roleId));
const payload: JwtPayload = {
sub: userEntity.id,
phone: userEntity.phone,
role: resolvedRole,
};
const accessToken = this.jwtService.sign(payload);
const refreshToken = this.jwtService.sign(
{ ...payload, type: 'refresh' },
{ expiresIn: '30d' },
);
await this.redisService.set(
`refresh_token:${userEntity.id}`,
refreshToken,
'EX',
60 * 60 * 24 * 30,
);
return { accessToken, refreshToken };
}
}

14
src/auth/dto/login.dto.ts Normal file
View File

@@ -0,0 +1,14 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsNotEmpty, IsString } from 'class-validator';
export class LoginDto {
@ApiProperty({ example: '+79991234567' })
@IsString()
@IsNotEmpty()
phone: string;
@ApiProperty({ example: 'StrongPass123!' })
@IsString()
@IsNotEmpty()
password: string;
}

View File

@@ -0,0 +1,9 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsString, IsNotEmpty } from 'class-validator';
export class RefreshTokenDto {
@ApiProperty()
@IsString()
@IsNotEmpty()
refreshToken: string;
}

View File

@@ -0,0 +1,15 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsNotEmpty, IsString, Matches, MinLength } from 'class-validator';
export class RegisterDto {
@ApiProperty({ example: '+79991234567' })
@IsString()
@IsNotEmpty()
@Matches(/^\+?[1-9]\d{6,14}$/, { message: 'Invalid phone number' })
phone: string;
@ApiProperty({ example: 'StrongPass123!' })
@IsString()
@MinLength(8)
password: string;
}

View File

@@ -0,0 +1,41 @@
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { PassportStrategy } from '@nestjs/passport';
import { ExtractJwt, Strategy } from 'passport-jwt';
import { DrizzleService } from '../../database/drizzle.service';
import { user } from '../../database/schema';
import { eq } from 'drizzle-orm';
export interface JwtPayload {
sub: string;
phone: string;
role: string;
}
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor(
configService: ConfigService,
private readonly drizzleService: DrizzleService,
) {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: false,
secretOrKey: configService.get<string>('jwt.secret'),
});
}
async validate(payload: JwtPayload) {
const [foundUser] = await this.drizzleService.db
.select()
.from(user)
.where(eq(user.id, payload.sub))
.limit(1);
if (!foundUser || foundUser.status === 'banned') {
throw new UnauthorizedException();
}
return { id: foundUser.id, phone: foundUser.phone, role: payload.role, status: foundUser.status };
}
}