auth: fix family_id join with uuid cast
This commit is contained in:
parent
ca4e1355d6
commit
149d8bc72c
1 changed files with 57 additions and 15 deletions
|
|
@ -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) {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue