first commit
This commit is contained in:
55
src/auth/auth.controller.ts
Normal file
55
src/auth/auth.controller.ts
Normal 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
25
src/auth/auth.module.ts
Normal 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
160
src/auth/auth.service.ts
Normal 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
14
src/auth/dto/login.dto.ts
Normal 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;
|
||||
}
|
||||
9
src/auth/dto/refresh-token.dto.ts
Normal file
9
src/auth/dto/refresh-token.dto.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsString, IsNotEmpty } from 'class-validator';
|
||||
|
||||
export class RefreshTokenDto {
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
refreshToken: string;
|
||||
}
|
||||
15
src/auth/dto/register.dto.ts
Normal file
15
src/auth/dto/register.dto.ts
Normal 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;
|
||||
}
|
||||
41
src/auth/strategies/jwt.strategy.ts
Normal file
41
src/auth/strategies/jwt.strategy.ts
Normal 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 };
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user