ledgerone_backend/write-files.mjs
2026-03-14 08:51:16 -04:00

296 lines
12 KiB
JavaScript

import { writeFileSync } from "fs";
// ─── auth.service.ts ─────────────────────────────────────────────────────────
writeFileSync("src/auth/auth.service.ts", `import {
BadRequestException,
Injectable,
NotFoundException,
UnauthorizedException,
} from "@nestjs/common";
import * as crypto from "crypto";
import { JwtService } from "@nestjs/jwt";
import { PrismaService } from "../prisma/prisma.service";
import { EmailService } from "../email/email.service";
import { LoginDto } from "./dto/login.dto";
import { RegisterDto } from "./dto/register.dto";
import { UpdateProfileDto } from "./dto/update-profile.dto";
import { ForgotPasswordDto } from "./dto/forgot-password.dto";
import { ResetPasswordDto } from "./dto/reset-password.dto";
const VERIFY_TOKEN_TTL_HOURS = 24;
const RESET_TOKEN_TTL_HOURS = 1;
const REFRESH_TOKEN_TTL_DAYS = 30;
@Injectable()
export class AuthService {
constructor(
private readonly prisma: PrismaService,
private readonly jwtService: JwtService,
private readonly emailService: EmailService,
) {}
async register(payload: RegisterDto) {
const email = payload.email.toLowerCase().trim();
const existing = await this.prisma.user.findUnique({ where: { email } });
if (existing) throw new BadRequestException("Email already registered.");
const passwordHash = this.hashPassword(payload.password);
const user = await this.prisma.user.create({ data: { email, passwordHash } });
await this.prisma.auditLog.create({ data: { userId: user.id, action: "auth.register", metadata: { email } } });
const verifyToken = crypto.randomBytes(32).toString("hex");
const expiresAt = new Date(Date.now() + VERIFY_TOKEN_TTL_HOURS * 3600 * 1000);
await this.prisma.emailVerificationToken.upsert({
where: { userId: user.id },
update: { token: verifyToken, expiresAt },
create: { userId: user.id, token: verifyToken, expiresAt },
});
await this.emailService.sendVerificationEmail(email, verifyToken);
const accessToken = this.signAccessToken(user.id);
const refreshToken = await this.createRefreshToken(user.id);
return {
user: { id: user.id, email: user.email, fullName: user.fullName, emailVerified: user.emailVerified },
accessToken,
refreshToken,
message: "Registration successful. Please verify your email.",
};
}
async login(payload: LoginDto) {
const email = payload.email.toLowerCase().trim();
const user = await this.prisma.user.findUnique({ where: { email } });
if (!user || !this.verifyPassword(payload.password, user.passwordHash)) {
throw new UnauthorizedException("Invalid credentials.");
}
await this.prisma.auditLog.create({ data: { userId: user.id, action: "auth.login", metadata: { email } } });
const accessToken = this.signAccessToken(user.id);
const refreshToken = await this.createRefreshToken(user.id);
return {
user: { id: user.id, email: user.email, fullName: user.fullName, emailVerified: user.emailVerified },
accessToken,
refreshToken,
};
}
async verifyEmail(token: string) {
const record = await this.prisma.emailVerificationToken.findUnique({ where: { token } });
if (!record || record.expiresAt < new Date()) {
throw new BadRequestException("Invalid or expired verification token.");
}
await this.prisma.user.update({ where: { id: record.userId }, data: { emailVerified: true } });
await this.prisma.emailVerificationToken.delete({ where: { token } });
return { message: "Email verified successfully." };
}
async refreshAccessToken(rawRefreshToken: string) {
const tokenHash = this.hashToken(rawRefreshToken);
const record = await this.prisma.refreshToken.findUnique({ where: { tokenHash } });
if (!record || record.revokedAt || record.expiresAt < new Date()) {
throw new UnauthorizedException("Invalid or expired refresh token.");
}
await this.prisma.refreshToken.update({ where: { id: record.id }, data: { revokedAt: new Date() } });
const accessToken = this.signAccessToken(record.userId);
const refreshToken = await this.createRefreshToken(record.userId);
return { accessToken, refreshToken };
}
async logout(rawRefreshToken: string) {
const tokenHash = this.hashToken(rawRefreshToken);
await this.prisma.refreshToken.updateMany({
where: { tokenHash, revokedAt: null },
data: { revokedAt: new Date() },
});
return { message: "Logged out." };
}
async forgotPassword(payload: ForgotPasswordDto) {
const email = payload.email.toLowerCase().trim();
const user = await this.prisma.user.findUnique({ where: { email } });
if (!user) return { message: "If that email exists, a reset link has been sent." };
const token = crypto.randomBytes(32).toString("hex");
const expiresAt = new Date(Date.now() + RESET_TOKEN_TTL_HOURS * 3600 * 1000);
await this.prisma.passwordResetToken.create({ data: { userId: user.id, token, expiresAt } });
await this.emailService.sendPasswordResetEmail(email, token);
return { message: "If that email exists, a reset link has been sent." };
}
async resetPassword(payload: ResetPasswordDto) {
const record = await this.prisma.passwordResetToken.findUnique({ where: { token: payload.token } });
if (!record || record.usedAt || record.expiresAt < new Date()) {
throw new BadRequestException("Invalid or expired reset token.");
}
const passwordHash = this.hashPassword(payload.password);
await this.prisma.user.update({ where: { id: record.userId }, data: { passwordHash } });
await this.prisma.passwordResetToken.update({ where: { id: record.id }, data: { usedAt: new Date() } });
await this.prisma.refreshToken.updateMany({
where: { userId: record.userId, revokedAt: null },
data: { revokedAt: new Date() },
});
return { message: "Password reset successfully. Please log in." };
}
async updateProfile(userId: string, payload: UpdateProfileDto) {
const data: Record<string, string> = {};
for (const [key, value] of Object.entries(payload)) {
const trimmed = (value as string | undefined)?.trim();
if (trimmed) data[key] = trimmed;
}
if (!Object.keys(data).length) throw new BadRequestException("No profile fields provided.");
const user = await this.prisma.user.update({ where: { id: userId }, data });
await this.prisma.auditLog.create({
data: { userId: user.id, action: "auth.profile.update", metadata: { updatedFields: Object.keys(data) } },
});
return {
user: {
id: user.id, email: user.email, fullName: user.fullName, phone: user.phone,
companyName: user.companyName, addressLine1: user.addressLine1,
addressLine2: user.addressLine2, city: user.city, state: user.state,
postalCode: user.postalCode, country: user.country,
},
};
}
async getProfile(userId: string) {
const user = await this.prisma.user.findUnique({ where: { id: userId } });
if (!user) throw new NotFoundException("User not found.");
return {
user: {
id: user.id, email: user.email, fullName: user.fullName, phone: user.phone,
companyName: user.companyName, addressLine1: user.addressLine1,
addressLine2: user.addressLine2, city: user.city, state: user.state,
postalCode: user.postalCode, country: user.country,
emailVerified: user.emailVerified, createdAt: user.createdAt,
},
};
}
verifyToken(token: string): { sub: string } {
return this.jwtService.verify<{ sub: string }>(token);
}
private signAccessToken(userId: string): string {
return this.jwtService.sign({ sub: userId });
}
private async createRefreshToken(userId: string): Promise<string> {
const raw = crypto.randomBytes(40).toString("hex");
const tokenHash = this.hashToken(raw);
const expiresAt = new Date(Date.now() + REFRESH_TOKEN_TTL_DAYS * 86400 * 1000);
await this.prisma.refreshToken.create({ data: { userId, tokenHash, expiresAt } });
return raw;
}
private hashToken(token: string): string {
return crypto.createHash("sha256").update(token).digest("hex");
}
private hashPassword(password: string): string {
const salt = crypto.randomBytes(16).toString("hex");
const hash = crypto.pbkdf2Sync(password, salt, 100_000, 64, "sha512").toString("hex");
return \`\${salt}:\${hash}\`;
}
private verifyPassword(password: string, stored: string): boolean {
const [salt, hash] = stored.split(":");
if (!salt || !hash) return false;
const computed = crypto.pbkdf2Sync(password, salt, 100_000, 64, "sha512").toString("hex");
return crypto.timingSafeEqual(Buffer.from(hash), Buffer.from(computed));
}
}
`);
// ─── auth.controller.ts ──────────────────────────────────────────────────────
writeFileSync("src/auth/auth.controller.ts", `import { Body, Controller, Get, Post, Patch, Query, UseGuards } from "@nestjs/common";
import { ok } from "../common/response";
import { AuthService } from "./auth.service";
import { LoginDto } from "./dto/login.dto";
import { RegisterDto } from "./dto/register.dto";
import { UpdateProfileDto } from "./dto/update-profile.dto";
import { ForgotPasswordDto } from "./dto/forgot-password.dto";
import { ResetPasswordDto } from "./dto/reset-password.dto";
import { JwtAuthGuard } from "../common/guards/jwt-auth.guard";
import { CurrentUser } from "../common/decorators/current-user.decorator";
import { Public } from "../common/decorators/public.decorator";
@Controller("auth")
@UseGuards(JwtAuthGuard)
export class AuthController {
constructor(private readonly authService: AuthService) {}
@Public()
@Post("register")
async register(@Body() payload: RegisterDto) {
return ok(await this.authService.register(payload));
}
@Public()
@Post("login")
async login(@Body() payload: LoginDto) {
return ok(await this.authService.login(payload));
}
@Public()
@Get("verify-email")
async verifyEmail(@Query("token") token: string) {
return ok(await this.authService.verifyEmail(token));
}
@Public()
@Post("refresh")
async refresh(@Body("refreshToken") refreshToken: string) {
return ok(await this.authService.refreshAccessToken(refreshToken));
}
@Post("logout")
async logout(@Body("refreshToken") refreshToken: string) {
return ok(await this.authService.logout(refreshToken));
}
@Public()
@Post("forgot-password")
async forgotPassword(@Body() payload: ForgotPasswordDto) {
return ok(await this.authService.forgotPassword(payload));
}
@Public()
@Post("reset-password")
async resetPassword(@Body() payload: ResetPasswordDto) {
return ok(await this.authService.resetPassword(payload));
}
@Get("me")
async me(@CurrentUser() userId: string) {
return ok(await this.authService.getProfile(userId));
}
@Patch("profile")
async updateProfile(@CurrentUser() userId: string, @Body() payload: UpdateProfileDto) {
return ok(await this.authService.updateProfile(userId, payload));
}
}
`);
// ─── auth.module.ts ──────────────────────────────────────────────────────────
writeFileSync("src/auth/auth.module.ts", `import { Module } from "@nestjs/common";
import { JwtModule } from "@nestjs/jwt";
import { AuthController } from "./auth.controller";
import { AuthService } from "./auth.service";
import { JwtAuthGuard } from "../common/guards/jwt-auth.guard";
@Module({
imports: [
JwtModule.register({
secret: process.env.JWT_SECRET,
signOptions: { expiresIn: "15m" },
}),
],
controllers: [AuthController],
providers: [AuthService, JwtAuthGuard],
exports: [AuthService, JwtModule, JwtAuthGuard],
})
export class AuthModule {}
`);
console.log("auth files written");