All checks were successful
Deploy Production / deploy (push) Successful in 53s
172 lines
4.6 KiB
TypeScript
172 lines
4.6 KiB
TypeScript
import {
|
|
Injectable,
|
|
UnauthorizedException,
|
|
BadRequestException,
|
|
} from "@nestjs/common";
|
|
import { ConfigService } from "@nestjs/config";
|
|
import { JwtService } from "@nestjs/jwt";
|
|
import { UsersService } from "../users/users.service";
|
|
import { LoginDto, ChangePasswordDto, SetupPasswordDto } from "./dto";
|
|
import { TokenPair } from "./interfaces";
|
|
|
|
@Injectable()
|
|
export class AuthService {
|
|
constructor(
|
|
private jwtService: JwtService,
|
|
private configService: ConfigService,
|
|
private usersService: UsersService,
|
|
) {}
|
|
|
|
async validateUser(
|
|
loginDto: LoginDto,
|
|
): Promise<{ id: string; username: string }> {
|
|
const { username, password } = loginDto;
|
|
|
|
const user = await this.usersService.findByUsername(username);
|
|
if (!user) {
|
|
throw new UnauthorizedException("Invalid credentials");
|
|
}
|
|
|
|
if (!user.isPasswordSet) {
|
|
throw new BadRequestException(
|
|
"Password not set. Please use /auth/setup endpoint first.",
|
|
);
|
|
}
|
|
|
|
const isPasswordValid = await this.usersService.validatePassword(
|
|
user,
|
|
password,
|
|
);
|
|
if (!isPasswordValid) {
|
|
throw new UnauthorizedException("Invalid credentials");
|
|
}
|
|
|
|
if (!user.isAdmin) {
|
|
throw new UnauthorizedException("Access denied");
|
|
}
|
|
|
|
return { id: user.id, username: user.username };
|
|
}
|
|
|
|
async setupPassword(
|
|
setupPasswordDto: SetupPasswordDto,
|
|
): Promise<{ id: string; username: string }> {
|
|
const { username, password } = setupPasswordDto;
|
|
|
|
const user = await this.usersService.findByUsername(username);
|
|
if (!user) {
|
|
throw new UnauthorizedException("User not found");
|
|
}
|
|
|
|
if (!user.isAdmin) {
|
|
throw new UnauthorizedException("Access denied");
|
|
}
|
|
|
|
if (user.isPasswordSet) {
|
|
throw new BadRequestException(
|
|
"Password already set. Use /auth/change-password to change it.",
|
|
);
|
|
}
|
|
|
|
await this.usersService.setupPassword(username, password);
|
|
return { id: user.id, username: user.username };
|
|
}
|
|
|
|
async needsSetup(): Promise<boolean> {
|
|
return this.usersService.needsPasswordSetup();
|
|
}
|
|
|
|
async generateTokens(userId: string, username: string): Promise<TokenPair> {
|
|
const accessPayload = {
|
|
sub: userId,
|
|
username,
|
|
type: "access",
|
|
};
|
|
|
|
const refreshPayload = {
|
|
sub: userId,
|
|
username,
|
|
type: "refresh",
|
|
};
|
|
|
|
const accessSecret =
|
|
this.configService.get<string>("JWT_ACCESS_SECRET") || "default-secret";
|
|
const refreshSecret =
|
|
this.configService.get<string>("JWT_REFRESH_SECRET") ||
|
|
"default-refresh-secret";
|
|
|
|
const [accessToken, refreshToken] = await Promise.all([
|
|
this.jwtService.signAsync(accessPayload, {
|
|
secret: accessSecret,
|
|
expiresIn: "15m",
|
|
}),
|
|
this.jwtService.signAsync(refreshPayload, {
|
|
secret: refreshSecret,
|
|
expiresIn: "7d",
|
|
}),
|
|
]);
|
|
|
|
return { accessToken, refreshToken };
|
|
}
|
|
|
|
async refreshTokens(userId: string, username: string): Promise<TokenPair> {
|
|
const user = await this.usersService.findById(userId);
|
|
if (!user || !user.isAdmin) {
|
|
throw new UnauthorizedException("User not found or no longer authorized");
|
|
}
|
|
return this.generateTokens(userId, username);
|
|
}
|
|
|
|
getCookieOptions(isRefreshToken = false) {
|
|
const isProduction =
|
|
this.configService.get<string>("NODE_ENV") === "production";
|
|
const cookieSecure =
|
|
this.configService.get<string>("COOKIE_SECURE") === "true";
|
|
const domain = this.configService.get<string>("COOKIE_DOMAIN");
|
|
|
|
return {
|
|
httpOnly: true,
|
|
secure: isProduction || cookieSecure,
|
|
sameSite: "none" as const,
|
|
path: isRefreshToken ? "/auth/refresh" : "/",
|
|
domain: domain,
|
|
maxAge: isRefreshToken ? 7 * 24 * 60 * 60 * 1000 : 15 * 60 * 1000,
|
|
};
|
|
}
|
|
|
|
getClearCookieOptions(isRefreshToken = false) {
|
|
return {
|
|
httpOnly: true,
|
|
path: isRefreshToken ? "/auth/refresh" : "/",
|
|
};
|
|
}
|
|
|
|
async changePassword(
|
|
userId: string,
|
|
changePasswordDto: ChangePasswordDto,
|
|
): Promise<void> {
|
|
const { currentPassword, newPassword } = changePasswordDto;
|
|
|
|
const user = await this.usersService.findById(userId);
|
|
if (!user) {
|
|
throw new UnauthorizedException("User not found");
|
|
}
|
|
|
|
const isCurrentPasswordValid = await this.usersService.validatePassword(
|
|
user,
|
|
currentPassword,
|
|
);
|
|
if (!isCurrentPasswordValid) {
|
|
throw new BadRequestException("Current password is incorrect");
|
|
}
|
|
|
|
if (currentPassword === newPassword) {
|
|
throw new BadRequestException(
|
|
"New password must be different from current password",
|
|
);
|
|
}
|
|
|
|
await this.usersService.updatePassword(userId, newPassword);
|
|
}
|
|
}
|