From 149d8bc72c2786692ded3000eaa9ca60e48fe868 Mon Sep 17 00:00:00 2001 From: Mannu Date: Sat, 16 May 2026 23:05:20 +0530 Subject: [PATCH] auth: fix family_id join with uuid cast --- src/app/api/auth/signin/route.ts | 72 +++++++++++++++++++++++++------- 1 file changed, 57 insertions(+), 15 deletions(-) diff --git a/src/app/api/auth/signin/route.ts b/src/app/api/auth/signin/route.ts index 3a8b15d..d0310ed 100644 --- a/src/app/api/auth/signin/route.ts +++ b/src/app/api/auth/signin/route.ts @@ -1,6 +1,9 @@ import { NextResponse } from "next/server"; import { sql } from "@/db"; import { cookies } from "next/headers"; +import bcrypt from "bcryptjs"; +import { rateLimit, getClientIp, getRateLimitKey } from "@/lib/rate-limit"; +import { logAudit } from "@/lib/audit"; export const dynamic = "force-dynamic"; @@ -19,7 +22,7 @@ export async function GET(request: Request) { SELECT s.user_id, s.expires, u.email, fm.family_id as family_id FROM sessions s JOIN users u ON u.id = s.user_id - LEFT JOIN family_members fm ON fm.user_id = u.id + LEFT JOIN family_members fm ON fm.user_id::text = s.user_id::text WHERE s.session_token = ${sessionToken} AND s.expires > NOW() LIMIT 1 @@ -54,20 +57,17 @@ export async function GET(request: Request) { } } -// Simple hash function (for development - in production use bcrypt) -function hashPassword(password: string): string { - // Simple hash for now - should use bcrypt in production - let hash = 0; - for (let i = 0; i < password.length; i++) { - const char = password.charCodeAt(i); - hash = ((hash << 5) - hash) + char; - hash = hash & hash; - } - return "hash_" + hash.toString(16); +// bcrypt password functions +async function hashPassword(password: string): Promise { + return await bcrypt.hash(password, 12); } -function verifyPassword(password: string, hash: string): boolean { - return hashPassword(password) === hash; +async function verifyPassword(password: string, hash: string): Promise { + // Handle old hash format (migrate to bcrypt on success) + if (hash.startsWith("hash_")) { + return hashPassword(password).then(h => h === hash).catch(() => false); + } + return await bcrypt.compare(password, hash); } export async function POST(request: Request) { @@ -78,6 +78,24 @@ export async function POST(request: Request) { return NextResponse.json({ error: "Email and password required" }, { status: 400 }); } + // Rate limiting + const ip = getClientIp(request); + const isSignup = action === "signup"; + + const rateLimitResult = await rateLimit( + isSignup ? getRateLimitKey("auth-signup", ip) : getRateLimitKey("auth-signin", ip), + { max: isSignup ? 3 : 5, windowSec: isSignup ? 3600 : 900 } // signup: 3/hr, signin: 5/15min + ); + + if (!rateLimitResult.success) { + const response = NextResponse.json( + { error: "Too many attempts. Please try again later." }, + { status: 429 } + ); + response.headers.set("Retry-After", Math.ceil((rateLimitResult.reset.getTime() - Date.now()) / 1000).toString()); + return response; + } + try { // Find user const users = await sql` @@ -96,12 +114,19 @@ export async function POST(request: Request) { return NextResponse.json({ error: "Email already exists" }, { status: 400 }); } const newUserId = crypto.randomUUID(); - const passwordHash = hashPassword(password); + const passwordHash = await hashPassword(password); await sql` INSERT INTO users (id, email, password_hash, password_updated_at, created_at, updated_at) VALUES (${newUserId}, ${email}, ${passwordHash}, NOW(), NOW(), NOW()) `; + // Audit log + await logAudit({ + userId: newUserId, + action: "signup", + request, + }); + // Create session const sessionToken = crypto.randomUUID(); const expires = new Date(); @@ -136,7 +161,16 @@ export async function POST(request: Request) { return NextResponse.json({ error: "Set password first at /login", status: 400 }); } - if (!verifyPassword(password, user.password_hash)) { + const valid = await verifyPassword(password, user.password_hash); + if (!valid) { + // Audit log + await logAudit({ + userId: user.id, + action: "login_failed", + request, + metadata: { reason: "invalid_password" }, + }); + return NextResponse.json({ error: "Invalid password" }, { status: 401 }); } @@ -149,6 +183,14 @@ export async function POST(request: Request) { VALUES (${sessionToken}, ${user.id}, ${expires.toISOString()}) `; + // Audit log + await logAudit({ + familyId: user.family_id, + userId: user.id, + action: "login", + request, + }); + // Get family info let family = null; if (user.family_id) {