diff --git a/src/app/api/auth/migrate/route.ts b/src/app/api/auth/migrate/route.ts new file mode 100644 index 0000000..c718c11 --- /dev/null +++ b/src/app/api/auth/migrate/route.ts @@ -0,0 +1,17 @@ +import { NextResponse } from "next/server"; +import { sql } from "@/db"; + +// Run user password migration +export async function POST() { + try { + await sql` + ALTER TABLE users ADD COLUMN IF NOT EXISTS password_hash VARCHAR(255) + `; + await sql` + ALTER TABLE users ADD COLUMN IF NOT EXISTS password_updated_at TIMESTAMP WITH TIME ZONE + `; + return NextResponse.json({ success: true }); + } catch (error) { + return NextResponse.json({ error: String(error) }, { status: 500 }); + } +} \ No newline at end of file diff --git a/src/app/api/auth/signin/route.ts b/src/app/api/auth/signin/route.ts index c9dd61d..526ed46 100644 --- a/src/app/api/auth/signin/route.ts +++ b/src/app/api/auth/signin/route.ts @@ -2,53 +2,96 @@ import { NextResponse } from "next/server"; import { sql } from "@/db"; import { cookies } from "next/headers"; +// 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); +} + +function verifyPassword(password: string, hash: string): boolean { + return hashPassword(password) === hash; +} + export async function POST(request: Request) { const body = await request.json(); - const email = body?.email; + const { email, password, action } = body; - if (!email) { - return NextResponse.json({ error: "Email required" }, { status: 400 }); + if (!email || !password) { + return NextResponse.json({ error: "Email and password required" }, { status: 400 }); } try { - // Find or create user - let users = await sql` - SELECT u.id, u.email, fm.family_id as family_id + // Find user + const users = await sql` + SELECT u.id, u.email, u.password_hash, fm.family_id as family_id FROM users u LEFT JOIN family_members fm ON fm.user_id = u.id WHERE u.email = ${email} LIMIT 1 `; - let user = users?.[0]; + const user = users?.[0]; - // Create user if not found - if (!user) { + // Sign up - create new account + if (action === "signup") { + if (user) { + return NextResponse.json({ error: "Email already exists" }, { status: 400 }); + } const newUserId = crypto.randomUUID(); + const passwordHash = hashPassword(password); await sql` - INSERT INTO users (id, email, created_at, updated_at) - VALUES (${newUserId}, ${email}, NOW(), NOW()) + INSERT INTO users (id, email, password_hash, password_updated_at, created_at, updated_at) + VALUES (${newUserId}, ${email}, ${passwordHash}, NOW(), NOW(), NOW()) `; - // Fetch the newly created user - users = await sql` - SELECT u.id, u.email - FROM users u - WHERE u.id = ${newUserId} + // Create session + const sessionToken = crypto.randomUUID(); + const expires = new Date(); + expires.setDate(expires.getDate() + 30); + await sql` + INSERT INTO sessions (session_token, user_id, expires) + VALUES (${sessionToken}, ${newUserId}, ${expires.toISOString()}) `; - user = users?.[0]; + + const response = NextResponse.json({ + success: true, + userId: newUserId, + email, + isNewUser: true, + }); + response.cookies.set("tia_session", sessionToken, { + httpOnly: true, + secure: process.env.NODE_ENV === "production", + sameSite: "lax", + maxAge: 60 * 60 * 24 * 30, // 30 days + path: "/", + }); + return response; } + // Sign in - require existing user with password if (!user) { - return NextResponse.json({ error: "Failed to create user" }, { status: 500 }); + return NextResponse.json({ error: "User not found" }, { status: 404 }); } - // Create session token + if (!user.password_hash) { + return NextResponse.json({ error: "Set password first at /login", status: 400 }); + } + + if (!verifyPassword(password, user.password_hash)) { + return NextResponse.json({ error: "Invalid password" }, { status: 401 }); + } + + // Create session const sessionToken = crypto.randomUUID(); const expires = new Date(); expires.setDate(expires.getDate() + 30); - - // Insert session await sql` INSERT INTO sessions (session_token, user_id, expires) VALUES (${sessionToken}, ${user.id}, ${expires.toISOString()}) @@ -64,16 +107,14 @@ export async function POST(request: Request) { family = families?.[0]; } - // Create response const response = NextResponse.json({ success: true, userId: user.id, email: user.email, familyId: user.family_id, - family: family, + family, }); - // Set cookie response.cookies.set("tia_session", sessionToken, { httpOnly: true, secure: process.env.NODE_ENV === "production", @@ -81,53 +122,9 @@ export async function POST(request: Request) { maxAge: 60 * 60 * 24 * 30, path: "/", }); - return response; } catch (error) { - console.error("Signin error:", error); + console.error("Auth error:", error); return NextResponse.json({ error: String(error) }, { status: 500 }); } -} - -export async function GET() { - try { - const cookieStore = await cookies(); - const sessionToken = cookieStore.get("tia_session")?.value; - - if (!sessionToken) { - return NextResponse.json({ authenticated: false }); - } - - const sessions = await sql` - SELECT s.user_id, s.expires, u.email - FROM sessions s - JOIN users u ON u.id = s.user_id - WHERE s.session_token = ${sessionToken} - AND s.expires > NOW() - `; - - const session = sessions?.[0]; - - if (!session) { - return NextResponse.json({ authenticated: false }); - } - - const members = await sql` - SELECT fm.family_id, f.name as family_name, f.tier - FROM family_members fm - JOIN families f ON f.id = fm.family_id - WHERE fm.user_id = ${session.user_id} - `; - - return NextResponse.json({ - authenticated: true, - userId: session.user_id, - email: session.email, - familyId: members?.[0]?.family_id, - familyName: members?.[0]?.family_name, - tier: members?.[0]?.tier, - }); - } catch (error) { - return NextResponse.json({ authenticated: false }); - } } \ No newline at end of file diff --git a/src/app/login/page.tsx b/src/app/login/page.tsx index 67edf2a..e0f7550 100644 --- a/src/app/login/page.tsx +++ b/src/app/login/page.tsx @@ -1,74 +1,106 @@ "use client"; -import { useEffect } from "react"; +import { useState } from "react"; import { useRouter } from "next/navigation"; export default function LoginPage() { const router = useRouter(); - - useEffect(() => { - async function checkSession() { - const res = await fetch("/api/auth/signin"); - const data = await res.json(); - - if (data.authenticated) { - if (data.familyId) { - router.push("/"); - } else { - router.push("/onboarding"); - } - } - } - checkSession(); - }, [router]); + const [email, setEmail] = useState(""); + const [password, setPassword] = useState(""); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(""); + const [mode, setMode] = useState<"login" | "signup">("login"); const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); - const form = e.target as HTMLFormElement; - const email = (form.elements.namedItem("email") as HTMLInputElement)?.value; + if (!email || !password) return; - if (!email) return; + setLoading(true); + setError(""); - const res = await fetch("/api/auth/signin", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ email }), - }); + try { + const res = await fetch("/api/auth/signin", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ email, password, action: mode }), + }); - const data = await res.json(); + const data = await res.json(); - if (data.success) { - if (data.familyId) { + if (!res.ok) { + setError(data.error || "Failed to login"); + return; + } + + // Redirect based on response + if (data.isNewUser) { + router.push("/onboarding"); + } else if (data.familyId) { router.push("/"); } else { router.push("/onboarding"); } - } else { - alert(data.error || "Sign in failed"); + } catch (err) { + setError("Something went wrong"); + } finally { + setLoading(false); } }; return ( -
-
-

Tia

-

Your baby tracking companion

+
+
+

+ {mode === "login" ? "Welcome Back" : "Create Account"} +

- +
+ + setEmail(e.target.value)} + className="w-full p-3 border rounded-xl dark:bg-gray-700 dark:text-white" + required + /> +
+ +
+ + setPassword(e.target.value)} + className="w-full p-3 border rounded-xl dark:bg-gray-700 dark:text-white" + required + minLength={4} + /> +
+ + {error && ( +

{error}

+ )} +
+ +

+ {mode === "login" ? "No account? " : "Already have an account? "} + +

);