auth: fix family_id join with uuid cast

This commit is contained in:
Manohar Gupta 2026-05-16 23:05:20 +05:30
parent ca4e1355d6
commit 149d8bc72c

View file

@ -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<string> {
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<boolean> {
// 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) {