Initial scaffold: unified customer/insurance/utilities platform
Next.js + NestJS + Prisma (MySQL) monorepo replacing the legacy PHP internal app. Includes a session-based auth module with Argon2 password hashing and global input validation (replacing the old app's SQL injection and plaintext password comparison), the full target Prisma schema for customers/insurance/utilities/shared ledger/bank register, Docker Compose + Dockerfiles, and an Access-to-staging migration pipeline (migration/) already run against the real source databases. See PLAN.md and RESUME.md for the full architecture and session history.
This commit is contained in:
@@ -0,0 +1,38 @@
|
||||
import { Controller, Get, HttpCode, Post, Req, Res, UseGuards } from "@nestjs/common";
|
||||
import { Request, Response } from "express";
|
||||
import { LocalAuthGuard } from "./local-auth.guard";
|
||||
import { AuthenticatedGuard } from "./authenticated.guard";
|
||||
import { LoginDto } from "./login.dto";
|
||||
|
||||
@Controller("auth")
|
||||
export class AuthController {
|
||||
// LoginDto is only used for request-shape documentation/validation here —
|
||||
// the actual credential check happens inside LocalStrategy via Passport,
|
||||
// which populates req.user before this handler runs.
|
||||
@UseGuards(LocalAuthGuard)
|
||||
@Post("login")
|
||||
@HttpCode(200)
|
||||
login(@Req() req: Request, @Res({ passthrough: true }) _res: Response, _body?: LoginDto) {
|
||||
return req.user;
|
||||
}
|
||||
|
||||
@UseGuards(AuthenticatedGuard)
|
||||
@Get("me")
|
||||
me(@Req() req: Request) {
|
||||
return req.user;
|
||||
}
|
||||
|
||||
@Post("logout")
|
||||
@HttpCode(200)
|
||||
logout(@Req() req: Request) {
|
||||
return new Promise((resolve, reject) => {
|
||||
req.logout((err) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
return;
|
||||
}
|
||||
resolve({ success: true });
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { Module } from "@nestjs/common";
|
||||
import { PassportModule } from "@nestjs/passport";
|
||||
import { UsersModule } from "../users/users.module";
|
||||
import { AuthService } from "./auth.service";
|
||||
import { AuthController } from "./auth.controller";
|
||||
import { LocalStrategy } from "./local.strategy";
|
||||
import { SessionSerializer } from "./session.serializer";
|
||||
|
||||
@Module({
|
||||
imports: [UsersModule, PassportModule.register({ session: true })],
|
||||
providers: [AuthService, LocalStrategy, SessionSerializer],
|
||||
controllers: [AuthController],
|
||||
exports: [AuthService],
|
||||
})
|
||||
export class AuthModule {}
|
||||
@@ -0,0 +1,36 @@
|
||||
import { Injectable } from "@nestjs/common";
|
||||
import * as argon2 from "argon2";
|
||||
import { UsersService } from "../users/users.service";
|
||||
import type { User } from "@jorgecuadros/database";
|
||||
|
||||
export type SafeUser = Omit<User, "passwordHash">;
|
||||
|
||||
@Injectable()
|
||||
export class AuthService {
|
||||
constructor(private readonly usersService: UsersService) {}
|
||||
|
||||
/**
|
||||
* Replaces the old app's `passwd = '$password'` plaintext SQL comparison
|
||||
* (src/core/auth.php) with a constant-time hash verification. Returns
|
||||
* null on any failure — callers should not distinguish "no such user"
|
||||
* from "wrong password" in their response.
|
||||
*/
|
||||
async validateUser(email: string, password: string): Promise<SafeUser | null> {
|
||||
const user = await this.usersService.findByEmail(email);
|
||||
if (!user || !user.active) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const passwordMatches = await argon2.verify(user.passwordHash, password);
|
||||
if (!passwordMatches) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { passwordHash: _passwordHash, ...safeUser } = user;
|
||||
return safeUser;
|
||||
}
|
||||
|
||||
static async hashPassword(plainTextPassword: string): Promise<string> {
|
||||
return argon2.hash(plainTextPassword);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import { CanActivate, ExecutionContext, Injectable } from "@nestjs/common";
|
||||
import { Request } from "express";
|
||||
|
||||
/**
|
||||
* Replaces the old app's validate_session() (src/core/auth.php), which every
|
||||
* page had to remember to call manually. Here it's a guard attached via
|
||||
* @UseGuards(AuthenticatedGuard) — a controller that forgets it simply has
|
||||
* no route, instead of silently serving unauthenticated data.
|
||||
*/
|
||||
@Injectable()
|
||||
export class AuthenticatedGuard implements CanActivate {
|
||||
canActivate(context: ExecutionContext): boolean {
|
||||
const request = context.switchToHttp().getRequest<Request>();
|
||||
return request.isAuthenticated();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import { Injectable } from "@nestjs/common";
|
||||
import { AuthGuard } from "@nestjs/passport";
|
||||
|
||||
@Injectable()
|
||||
export class LocalAuthGuard extends AuthGuard("local") {}
|
||||
@@ -0,0 +1,19 @@
|
||||
import { Injectable, UnauthorizedException } from "@nestjs/common";
|
||||
import { PassportStrategy } from "@nestjs/passport";
|
||||
import { Strategy } from "passport-local";
|
||||
import { AuthService, SafeUser } from "./auth.service";
|
||||
|
||||
@Injectable()
|
||||
export class LocalStrategy extends PassportStrategy(Strategy) {
|
||||
constructor(private readonly authService: AuthService) {
|
||||
super({ usernameField: "email", passwordField: "password" });
|
||||
}
|
||||
|
||||
async validate(email: string, password: string): Promise<SafeUser> {
|
||||
const user = await this.authService.validateUser(email, password);
|
||||
if (!user) {
|
||||
throw new UnauthorizedException("Invalid email or password");
|
||||
}
|
||||
return user;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { IsEmail, IsString, MinLength } from "class-validator";
|
||||
|
||||
export class LoginDto {
|
||||
@IsEmail()
|
||||
email!: string;
|
||||
|
||||
@IsString()
|
||||
@MinLength(1)
|
||||
password!: string;
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { Injectable } from "@nestjs/common";
|
||||
import { PassportSerializer } from "@nestjs/passport";
|
||||
import { UsersService } from "../users/users.service";
|
||||
import { SafeUser } from "./auth.service";
|
||||
|
||||
@Injectable()
|
||||
export class SessionSerializer extends PassportSerializer {
|
||||
constructor(private readonly usersService: UsersService) {
|
||||
super();
|
||||
}
|
||||
|
||||
serializeUser(user: SafeUser, done: (err: Error | null, id: string) => void) {
|
||||
done(null, user.id);
|
||||
}
|
||||
|
||||
async deserializeUser(id: string, done: (err: Error | null, user: SafeUser | null) => void) {
|
||||
const user = await this.usersService.findById(id);
|
||||
if (!user) {
|
||||
done(null, null);
|
||||
return;
|
||||
}
|
||||
const { passwordHash: _passwordHash, ...safeUser } = user;
|
||||
done(null, safeUser);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user