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 = {}; 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 { 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");