Files
mybabyguess/apps/api/src/auth/auth.service.ts
T
2026-05-03 21:58:59 +02:00

179 lines
4.8 KiB
TypeScript

import { Injectable, UnauthorizedException } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { JwtService } from '@nestjs/jwt';
import type { StringValue } from 'ms';
import * as bcrypt from 'bcrypt';
import { PrismaService } from '../prisma/prisma.service';
import { LoginDto } from './dto/login.dto';
import { RefreshTokenDto } from './dto/refresh-token.dto';
@Injectable()
export class AuthService {
constructor(
private readonly prismaService: PrismaService,
private readonly jwtService: JwtService,
private readonly configService: ConfigService,
) {}
private async issueTokens(user: {
id: string;
username: string;
role: string;
displayName: string | null;
profileImageUrl: string | null;
workspaceId: string | null;
createdAt: Date;
}) {
const membership = await this.prismaService.projectMembership.findUnique({
where: { userId: user.id },
select: { projectId: true },
});
const currentProjectId = membership?.projectId ?? null;
const accessToken = await this.jwtService.signAsync({
sub: user.id,
username: user.username,
role: user.role,
workspaceId: user.workspaceId,
projectId: currentProjectId,
});
const refreshToken = await this.jwtService.signAsync(
{ sub: user.id, type: 'refresh' },
{
secret:
this.configService.get<string>('REFRESH_TOKEN_SECRET') ??
'change_me_refresh_secret',
expiresIn:
(this.configService.get<string>(
'REFRESH_TOKEN_EXPIRES_IN',
) as StringValue) ?? '30d',
},
);
const refreshTokenHash = await bcrypt.hash(refreshToken, 12);
await this.prismaService.user.update({
where: { id: user.id },
data: { refreshTokenHash },
});
return {
accessToken,
refreshToken,
user: {
id: user.id,
username: user.username,
displayName: user.displayName,
profileImageUrl: user.profileImageUrl,
workspaceId: user.workspaceId,
currentProjectId,
role: user.role,
createdAt: user.createdAt,
},
};
}
async login(loginDto: LoginDto) {
const normalizedUsername = loginDto.username.trim();
const user = await this.prismaService.user.findUnique({
where: { username: normalizedUsername },
});
const caseInsensitiveUser = user ?? (await this.prismaService.user.findFirst({
where: {
username: {
equals: normalizedUsername,
mode: 'insensitive',
},
},
}));
if (!caseInsensitiveUser) {
throw new UnauthorizedException('Invalid credentials');
}
const passwordMatches = await bcrypt.compare(
loginDto.password,
caseInsensitiveUser.passwordHash,
);
if (!passwordMatches) {
throw new UnauthorizedException('Invalid credentials');
}
return this.issueTokens(caseInsensitiveUser);
}
async refresh(refreshTokenDto: RefreshTokenDto) {
const refreshSecret =
this.configService.get<string>('REFRESH_TOKEN_SECRET') ??
'change_me_refresh_secret';
try {
const payload = await this.jwtService.verifyAsync(refreshTokenDto.refreshToken, {
secret: refreshSecret,
});
if (payload.type !== 'refresh') {
throw new UnauthorizedException('Invalid refresh token');
}
const user = await this.prismaService.user.findUnique({
where: { id: payload.sub as string },
});
if (!user || !user.refreshTokenHash) {
throw new UnauthorizedException('Invalid refresh token');
}
const isRefreshTokenValid = await bcrypt.compare(
refreshTokenDto.refreshToken,
user.refreshTokenHash,
);
if (!isRefreshTokenValid) {
throw new UnauthorizedException('Invalid refresh token');
}
return this.issueTokens(user);
} catch {
throw new UnauthorizedException('Invalid or expired refresh token');
}
}
async logout(userId: string) {
await this.prismaService.user.updateMany({
where: { id: userId },
data: { refreshTokenHash: null },
});
return { loggedOut: true };
}
async me(userId: string) {
const user = await this.prismaService.user.findUnique({ where: { id: userId } });
if (!user) {
throw new UnauthorizedException('User not found');
}
return {
id: user.id,
username: user.username,
displayName: user.displayName,
profileImageUrl: user.profileImageUrl,
workspaceId: user.workspaceId,
currentProjectId: (
await this.prismaService.projectMembership.findUnique({
where: { userId: user.id },
select: { projectId: true },
})
)?.projectId ?? null,
role: user.role,
createdAt: user.createdAt,
};
}
}