moex-vibe/apps/backend/src/modules/auth/auth.service.ts
Sergey Krylov c2478277e6
Some checks failed
CI / lint (pull_request) Failing after 1m45s
CI / test (pull_request) Failing after 1m42s
CI / build (pull_request) Failing after 1m31s
CI / lint (push) Failing after 1m46s
CI / test (push) Failing after 1m34s
CI / build (push) Failing after 1m35s
feat: add authentication system with JWT + RBAC
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
2026-06-13 22:24:46 +03:00

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,
};
}
}