moex-vibe/apps/backend/src/modules/auth/auth.service.ts
Sergey Krylov 729e94b4db
Some checks failed
CI / lint (pull_request) Failing after 1m53s
CI / test (pull_request) Failing after 1m45s
CI / build (pull_request) Failing after 1m44s
CI / lint (push) Failing after 1m39s
CI / test (push) Failing after 1m40s
CI / build (push) Failing after 1m34s
test: add 95 frontend unit tests with vitest, RTL, and MSW
95 tests across 22 files covering all frontend modules:
- API layer: client, auth
- Components: BondDetails, Layout, PriceChart, ProtectedRoute, SearchBar, StockDetails
- Context: AuthContext
- Hooks: useAuth, useBond, useBondCandles, useSearch, useStock, useStockCandles, useStockDividends
- Pages: BondPage, HomePage, LoginPage, ProfilePage, RegisterPage, StockPage

Infrastructure:
- vitest + @testing-library/react + MSW v2 with 13 API handlers
- Co-located test files alongside source files
- Test utilities: setup, server, factories, test-utils
- BrowserRouter future flags for MemoryRouter test compatibility
- Root test:frontend script for workspace-wide execution
2026-06-14 08:27:15 +03:00

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