Use database sessions with cookie instead of localStorage
This commit is contained in:
parent
1932d2ae6b
commit
57e852bfbc
4 changed files with 148 additions and 53 deletions
|
|
@ -46,11 +46,21 @@ export function FamilyProvider({ children: providerChildren }: { children: React
|
|||
useEffect(() => {
|
||||
async function fetchFamilyData() {
|
||||
try {
|
||||
// Get family_id from localStorage (set during login)
|
||||
const storedFamilyId = localStorage.getItem("family_id");
|
||||
const familyIdToUse = storedFamilyId || "default";
|
||||
// Get current session from database
|
||||
const sessionRes = await fetch("/api/auth/signin");
|
||||
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();
|
||||
|
||||
if (data.children?.length > 0) {
|
||||
|
|
@ -66,16 +76,9 @@ export function FamilyProvider({ children: providerChildren }: { children: React
|
|||
setChildId(childList[0].id);
|
||||
}
|
||||
|
||||
setFamilyId(familyIdToUse);
|
||||
|
||||
// Get tier and limits from family
|
||||
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);
|
||||
}
|
||||
setFamilyId(sessionFamilyId);
|
||||
setTier(sessionData.tier || "free");
|
||||
setMemberCount(2);
|
||||
} catch (err) {
|
||||
console.error("Failed to fetch family:", err);
|
||||
} finally {
|
||||
|
|
@ -86,10 +89,6 @@ export function FamilyProvider({ children: providerChildren }: { children: React
|
|||
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 (
|
||||
<FamilyContext.Provider
|
||||
value={{
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { NextResponse } from "next/server";
|
||||
import { sql } from "@/db";
|
||||
import { cookies } from "next/headers";
|
||||
|
||||
export async function POST(request: Request) {
|
||||
const { email } = await request.json();
|
||||
|
|
@ -23,8 +24,19 @@ export async function POST(request: Request) {
|
|||
}
|
||||
|
||||
const user = users[0];
|
||||
const userId = user.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
|
||||
let family = null;
|
||||
if (familyId) {
|
||||
|
|
@ -37,16 +49,73 @@ export async function POST(request: Request) {
|
|||
}
|
||||
}
|
||||
|
||||
// Return user and family info
|
||||
return NextResponse.json({
|
||||
// Create response with cookie
|
||||
const response = NextResponse.json({
|
||||
success: true,
|
||||
userId: user.id,
|
||||
email: user.email,
|
||||
familyId: familyId,
|
||||
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) {
|
||||
console.error("Signin error:", error);
|
||||
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 });
|
||||
}
|
||||
}
|
||||
31
src/app/api/auth/signout/route.ts
Normal file
31
src/app/api/auth/signout/route.ts
Normal 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 });
|
||||
}
|
||||
}
|
||||
|
|
@ -1,45 +1,43 @@
|
|||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { useEffect } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
export default function LoginPage() {
|
||||
const [email, setEmail] = useState("");
|
||||
const [loading, setLoading] = useState(false);
|
||||
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) => {
|
||||
e.preventDefault();
|
||||
setLoading(true);
|
||||
const form = e.target as HTMLFormElement;
|
||||
const email = (form.elements.namedItem("email") as HTMLInputElement)?.value;
|
||||
|
||||
try {
|
||||
const res = await fetch("/api/auth/signin", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ email }),
|
||||
});
|
||||
if (!email) return;
|
||||
|
||||
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) {
|
||||
// Store user and family info
|
||||
localStorage.setItem("user_id", data.userId);
|
||||
localStorage.setItem("user_email", data.email);
|
||||
if (data.familyId) {
|
||||
localStorage.setItem("family_id", data.familyId);
|
||||
}
|
||||
if (data.family) {
|
||||
localStorage.setItem("family", JSON.stringify(data.family));
|
||||
}
|
||||
router.push("/");
|
||||
} else {
|
||||
alert(data.error || "Sign in failed");
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
const data = await res.json();
|
||||
|
||||
if (data.success) {
|
||||
router.push("/");
|
||||
} else {
|
||||
alert(data.error || "Sign in failed");
|
||||
}
|
||||
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
return (
|
||||
|
|
@ -50,19 +48,17 @@ export default function LoginPage() {
|
|||
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
<input
|
||||
name="email"
|
||||
type="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"
|
||||
required
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
className="w-full p-4 bg-rose-400 text-white rounded-2xl font-medium"
|
||||
>
|
||||
{loading ? "Signing in..." : "Sign In"}
|
||||
Sign In
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue