Some checks failed
Backend: - AuthModule with register, login, logout, refresh, profile endpoints - JwtAuthGuard (global, opt-out via @Public) + RolesGuard (@Roles) - PrismaModule with SQLite via @prisma/adapter-libsql (Prisma 7) - Auth configuration in configuration.ts Frontend: - Auth context with session restoration via refresh cookie - Login, Register, Profile pages with ProtectedRoute - Auto-refresh on 401 with token rotation - API client refactored for auth headers Russian text: auth flows translated to Russian All text translated: auth pages, profile, layout, loading states
139 lines
4.2 KiB
TypeScript
139 lines
4.2 KiB
TypeScript
import { Injectable, ConflictException, UnauthorizedException } from '@nestjs/common';
|
|
import { ConfigService } from '@nestjs/config';
|
|
import { JwtService } from '@nestjs/jwt';
|
|
import * as bcrypt from 'bcrypt';
|
|
import { PrismaService } from '../prisma/prisma.service';
|
|
import { RegisterDto } from './dto/register.dto';
|
|
import { LoginDto } from './dto/login.dto';
|
|
import { UpdateProfileDto } from './dto/update-profile.dto';
|
|
import { JwtPayload } from './interfaces/jwt-payload.interface';
|
|
|
|
const SALT_ROUNDS = 12;
|
|
|
|
@Injectable()
|
|
export class AuthService {
|
|
constructor(
|
|
private readonly prisma: PrismaService,
|
|
private readonly jwtService: JwtService,
|
|
private readonly configService: ConfigService,
|
|
) {}
|
|
|
|
async register(dto: RegisterDto) {
|
|
const existing = await this.prisma.user.findUnique({ where: { email: dto.email } });
|
|
if (existing) {
|
|
throw new ConflictException('Email already registered');
|
|
}
|
|
|
|
const passwordHash = await bcrypt.hash(dto.password, SALT_ROUNDS);
|
|
const user = await this.prisma.user.create({
|
|
data: {
|
|
email: dto.email,
|
|
password: passwordHash,
|
|
name: dto.name ?? null,
|
|
},
|
|
});
|
|
|
|
return this.generateTokens(user);
|
|
}
|
|
|
|
async login(dto: LoginDto) {
|
|
const user = await this.prisma.user.findUnique({ where: { email: dto.email } });
|
|
if (!user) {
|
|
throw new UnauthorizedException('Invalid email or password');
|
|
}
|
|
|
|
const isValid = await bcrypt.compare(dto.password, user.password);
|
|
if (!isValid) {
|
|
throw new UnauthorizedException('Invalid email or password');
|
|
}
|
|
|
|
return this.generateTokens(user);
|
|
}
|
|
|
|
async refresh(refreshToken: string) {
|
|
try {
|
|
const payload = await this.jwtService.verifyAsync<{ sub: number; jti: string }>(
|
|
refreshToken,
|
|
{
|
|
secret: this.configService.get('app.auth.jwtRefreshSecret'),
|
|
},
|
|
);
|
|
|
|
const user = await this.prisma.user.findUnique({ where: { id: payload.sub } });
|
|
if (!user?.refreshToken) {
|
|
throw new UnauthorizedException('Invalid refresh token');
|
|
}
|
|
|
|
const isValid = await bcrypt.compare(refreshToken, user.refreshToken);
|
|
if (!isValid) {
|
|
throw new UnauthorizedException('Invalid refresh token');
|
|
}
|
|
|
|
return this.generateTokens(user);
|
|
} catch {
|
|
throw new UnauthorizedException('Invalid refresh token');
|
|
}
|
|
}
|
|
|
|
async logout(userId: number) {
|
|
await this.prisma.user.update({
|
|
where: { id: userId },
|
|
data: { refreshToken: null },
|
|
});
|
|
}
|
|
|
|
async getProfile(userId: number) {
|
|
const user = await this.prisma.user.findUnique({ where: { id: userId } });
|
|
if (!user) {
|
|
throw new UnauthorizedException('User not found');
|
|
}
|
|
return this.sanitizeUser(user);
|
|
}
|
|
|
|
async updateProfile(userId: number, dto: UpdateProfileDto) {
|
|
const user = await this.prisma.user.update({
|
|
where: { id: userId },
|
|
data: {
|
|
...(dto.name !== undefined && { name: dto.name }),
|
|
},
|
|
});
|
|
return this.sanitizeUser(user);
|
|
}
|
|
|
|
private async generateTokens(user: { id: number; email: string; name: string | null; role: string }) {
|
|
const accessPayload: JwtPayload = { sub: user.id, email: user.email, role: user.role };
|
|
const accessToken = await this.jwtService.signAsync(accessPayload, {
|
|
secret: this.configService.get('app.auth.jwtSecret'),
|
|
expiresIn: this.configService.get('app.auth.jwtAccessExpires'),
|
|
});
|
|
|
|
const jti = crypto.randomUUID();
|
|
const refreshPayload = { sub: user.id, jti };
|
|
const refreshToken = await this.jwtService.signAsync(refreshPayload, {
|
|
secret: this.configService.get('app.auth.jwtRefreshSecret'),
|
|
expiresIn: this.configService.get('app.auth.jwtRefreshExpires'),
|
|
});
|
|
|
|
const refreshHash = await bcrypt.hash(refreshToken, SALT_ROUNDS);
|
|
await this.prisma.user.update({
|
|
where: { id: user.id },
|
|
data: { refreshToken: refreshHash },
|
|
});
|
|
|
|
return {
|
|
user: this.sanitizeUser(user),
|
|
accessToken,
|
|
refreshToken,
|
|
};
|
|
}
|
|
|
|
private sanitizeUser(user: { id: number; email: string; name: string | null; role: string }) {
|
|
return {
|
|
id: user.id,
|
|
email: user.email,
|
|
name: user.name,
|
|
role: user.role,
|
|
};
|
|
}
|
|
}
|