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(() => {
|
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={{
|
||||||
|
|
|
||||||
|
|
@ -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 });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
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";
|
"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>
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue