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 { NextResponse } from "next/server";
|
||||||
import { sql } from "@/db";
|
import { sql } from "@/db";
|
||||||
import { cookies } from "next/headers";
|
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";
|
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
|
SELECT s.user_id, s.expires, u.email, fm.family_id as family_id
|
||||||
FROM sessions s
|
FROM sessions s
|
||||||
JOIN users u ON u.id = s.user_id
|
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}
|
WHERE s.session_token = ${sessionToken}
|
||||||
AND s.expires > NOW()
|
AND s.expires > NOW()
|
||||||
LIMIT 1
|
LIMIT 1
|
||||||
|
|
@ -54,20 +57,17 @@ export async function GET(request: Request) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Simple hash function (for development - in production use bcrypt)
|
// bcrypt password functions
|
||||||
function hashPassword(password: string): string {
|
async function hashPassword(password: string): Promise<string> {
|
||||||
// Simple hash for now - should use bcrypt in production
|
return await bcrypt.hash(password, 12);
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function verifyPassword(password: string, hash: string): boolean {
|
async function verifyPassword(password: string, hash: string): Promise<boolean> {
|
||||||
return hashPassword(password) === hash;
|
// 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) {
|
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 });
|
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 {
|
try {
|
||||||
// Find user
|
// Find user
|
||||||
const users = await sql`
|
const users = await sql`
|
||||||
|
|
@ -96,12 +114,19 @@ export async function POST(request: Request) {
|
||||||
return NextResponse.json({ error: "Email already exists" }, { status: 400 });
|
return NextResponse.json({ error: "Email already exists" }, { status: 400 });
|
||||||
}
|
}
|
||||||
const newUserId = crypto.randomUUID();
|
const newUserId = crypto.randomUUID();
|
||||||
const passwordHash = hashPassword(password);
|
const passwordHash = await hashPassword(password);
|
||||||
await sql`
|
await sql`
|
||||||
INSERT INTO users (id, email, password_hash, password_updated_at, created_at, updated_at)
|
INSERT INTO users (id, email, password_hash, password_updated_at, created_at, updated_at)
|
||||||
VALUES (${newUserId}, ${email}, ${passwordHash}, NOW(), NOW(), NOW())
|
VALUES (${newUserId}, ${email}, ${passwordHash}, NOW(), NOW(), NOW())
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
// Audit log
|
||||||
|
await logAudit({
|
||||||
|
userId: newUserId,
|
||||||
|
action: "signup",
|
||||||
|
request,
|
||||||
|
});
|
||||||
|
|
||||||
// Create session
|
// Create session
|
||||||
const sessionToken = crypto.randomUUID();
|
const sessionToken = crypto.randomUUID();
|
||||||
const expires = new Date();
|
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 });
|
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 });
|
return NextResponse.json({ error: "Invalid password" }, { status: 401 });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -149,6 +183,14 @@ export async function POST(request: Request) {
|
||||||
VALUES (${sessionToken}, ${user.id}, ${expires.toISOString()})
|
VALUES (${sessionToken}, ${user.id}, ${expires.toISOString()})
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
// Audit log
|
||||||
|
await logAudit({
|
||||||
|
familyId: user.family_id,
|
||||||
|
userId: user.id,
|
||||||
|
action: "login",
|
||||||
|
request,
|
||||||
|
});
|
||||||
|
|
||||||
// Get family info
|
// Get family info
|
||||||
let family = null;
|
let family = null;
|
||||||
if (user.family_id) {
|
if (user.family_id) {
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue