Use database sessions with cookie instead of localStorage

This commit is contained in:
Manohar Gupta 2026-05-10 23:40:10 +05:30
parent 1932d2ae6b
commit 57e852bfbc
4 changed files with 148 additions and 53 deletions

View file

@ -46,11 +46,21 @@ export function FamilyProvider({ children: providerChildren }: { children: React
useEffect(() => { useEffect(() => {
async function fetchFamilyData() { async function fetchFamilyData() {
try { try {
// Get family_id from localStorage (set during login) // Get current session from database
const storedFamilyId = localStorage.getItem("family_id"); const sessionRes = await fetch("/api/auth/signin");
const familyIdToUse = storedFamilyId || "default"; const sessionData = await sessionRes.json();
const res = await fetch(`/api/children?familyId=${familyIdToUse}`); if (!sessionData.authenticated) {
// Not logged in, use default
setFamilyId("default");
setLoading(false);
return;
}
const sessionFamilyId = sessionData.familyId || "default";
// Fetch children for this family
const res = await fetch(`/api/children?familyId=${sessionFamilyId}`);
const data = await res.json(); const data = await res.json();
if (data.children?.length > 0) { if (data.children?.length > 0) {
@ -66,16 +76,9 @@ export function FamilyProvider({ children: providerChildren }: { children: React
setChildId(childList[0].id); setChildId(childList[0].id);
} }
setFamilyId(familyIdToUse); setFamilyId(sessionFamilyId);
setTier(sessionData.tier || "free");
// Get tier and limits from family setMemberCount(2);
const familyRes = await fetch(`/api/family?familyId=${familyIdToUse}`);
const familyData = await familyRes.json();
if (familyData.family) {
setTier(familyData.family.tier || "free");
setMemberCount(familyData.family.max_members || 2);
}
} catch (err) { } catch (err) {
console.error("Failed to fetch family:", err); console.error("Failed to fetch family:", err);
} finally { } finally {
@ -86,10 +89,6 @@ export function FamilyProvider({ children: providerChildren }: { children: React
fetchFamilyData(); fetchFamilyData();
}, []); }, []);
// Check if can add more children/members based on tier limits
const canAddChild = () => memberCount < 2 || tier === "pro"; // free = 1 child, pro = unlimited
const canAddMember = () => memberCount < 2 && tier === "free" ? false : true;
return ( return (
<FamilyContext.Provider <FamilyContext.Provider
value={{ value={{

View file

@ -1,5 +1,6 @@
import { NextResponse } from "next/server"; import { NextResponse } from "next/server";
import { sql } from "@/db"; import { sql } from "@/db";
import { cookies } from "next/headers";
export async function POST(request: Request) { export async function POST(request: Request) {
const { email } = await request.json(); const { email } = await request.json();
@ -23,8 +24,19 @@ export async function POST(request: Request) {
} }
const user = users[0]; const user = users[0];
const userId = user.id;
const familyId = user.family_id; const familyId = user.family_id;
// Create session in database
const sessionToken = crypto.randomUUID();
const expires = new Date();
expires.setDate(expires.getDate() + 30); // 30 days
await sql`
INSERT INTO sessions (session_token, user_id, expires)
VALUES ${sql(sessionToken, userId, expires)}
`;
// Get family info // Get family info
let family = null; let family = null;
if (familyId) { if (familyId) {
@ -37,16 +49,73 @@ export async function POST(request: Request) {
} }
} }
// Return user and family info // Create response with cookie
return NextResponse.json({ const response = NextResponse.json({
success: true, success: true,
userId: user.id, userId: user.id,
email: user.email, email: user.email,
familyId: familyId, familyId: familyId,
family: family, family: family,
}); });
// Set session cookie (httpOnly, secure, sameSite)
response.cookies.set("session", sessionToken, {
httpOnly: true,
secure: process.env.NODE_ENV === "production",
sameSite: "lax",
maxAge: 60 * 60 * 24 * 30, // 30 days
path: "/",
});
return response;
} catch (error) { } catch (error) {
console.error("Signin error:", error); console.error("Signin error:", error);
return NextResponse.json({ error: String(error) }, { status: 500 }); return NextResponse.json({ error: String(error) }, { status: 500 });
} }
} }
// GET current session
export async function GET() {
try {
const cookieStore = await cookies();
const sessionToken = cookieStore.get("session")?.value;
if (!sessionToken) {
return NextResponse.json({ authenticated: false });
}
// Look up session
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()
`;
if (!sessions || sessions.length === 0) {
return NextResponse.json({ authenticated: false });
}
const session = sessions[0];
// Get family via family_members
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 });
}
}

View file

@ -0,0 +1,31 @@
import { NextResponse } from "next/server";
import { sql } from "@/db";
import { cookies } from "next/headers";
export async function POST() {
try {
const cookieStore = await cookies();
const sessionToken = cookieStore.get("session")?.value;
if (sessionToken) {
// Delete session from database
await sql`
DELETE FROM sessions WHERE session_token = ${sessionToken}
`;
}
// Clear cookie
const response = NextResponse.json({ success: true });
response.cookies.set("session", "", {
httpOnly: true,
secure: process.env.NODE_ENV === "production",
sameSite: "lax",
maxAge: 0,
path: "/",
});
return response;
} catch (error) {
return NextResponse.json({ error: String(error) }, { status: 500 });
}
}

View file

@ -1,45 +1,43 @@
"use client"; "use client";
import { useState } from "react"; import { useEffect } from "react";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
export default function LoginPage() { export default function LoginPage() {
const [email, setEmail] = useState("");
const [loading, setLoading] = useState(false);
const router = useRouter(); const router = useRouter();
useEffect(() => {
// Check if already logged in via session cookie
async function checkSession() {
const res = await fetch("/api/auth/signin");
const data = await res.json();
if (data.authenticated) {
router.push("/");
}
}
checkSession();
}, [router]);
const handleSubmit = async (e: React.FormEvent) => { const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault(); e.preventDefault();
setLoading(true); const form = e.target as HTMLFormElement;
const email = (form.elements.namedItem("email") as HTMLInputElement)?.value;
try { if (!email) return;
const res = await fetch("/api/auth/signin", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email }),
});
const data = await res.json(); const res = await fetch("/api/auth/signin", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email }),
});
if (data.success) { const data = await res.json();
// Store user and family info
localStorage.setItem("user_id", data.userId); if (data.success) {
localStorage.setItem("user_email", data.email); router.push("/");
if (data.familyId) { } else {
localStorage.setItem("family_id", data.familyId); alert(data.error || "Sign in failed");
}
if (data.family) {
localStorage.setItem("family", JSON.stringify(data.family));
}
router.push("/");
} else {
alert(data.error || "Sign in failed");
}
} catch (err) {
console.error(err);
} }
setLoading(false);
}; };
return ( return (
@ -50,19 +48,17 @@ export default function LoginPage() {
<form onSubmit={handleSubmit} className="space-y-4"> <form onSubmit={handleSubmit} className="space-y-4">
<input <input
name="email"
type="email" type="email"
placeholder="Enter your email" placeholder="Enter your email"
value={email}
onChange={(e) => setEmail(e.target.value)}
className="w-full p-4 border rounded-2xl bg-white shadow-sm" className="w-full p-4 border rounded-2xl bg-white shadow-sm"
required required
/> />
<button <button
type="submit" type="submit"
disabled={loading}
className="w-full p-4 bg-rose-400 text-white rounded-2xl font-medium" className="w-full p-4 bg-rose-400 text-white rounded-2xl font-medium"
> >
{loading ? "Signing in..." : "Sign In"} Sign In
</button> </button>
</form> </form>
</div> </div>