179 lines
4.8 KiB
TypeScript
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,
|
|
};
|
|
}
|
|
} |