fix(admin): scope FamilyProvider out of admin routes, ensure cookies on admin fetches
Root causes:
- tia_admin_session is httpOnly so document.cookie could never read it → all
client-side cookie checks always failed and redirected before any data fetched
- Sub-pages used localStorage.getItem("admin_token") which was never stored,
and passed Authorization: Bearer null headers the server ignores
Fixes:
- FamilyProvider: use usePathname() hook instead of window.location.pathname
- admin/layout.tsx: rewrite as server component using verifyAdminSession()
(new lib/admin-auth.ts helper that uses next/headers cookies()) → server-side
redirect to /admin-login if session invalid; extract sidebar to AdminSidebar.tsx
- admin/page.tsx: remove broken document.cookie guard (layout handles auth now)
- admin-login/page.tsx: replace document.cookie check with GET /api/admin/auth call
- All 7 admin sub-pages: remove localStorage guard, remove Authorization: Bearer
headers, add credentials: include to every fetch call
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
5e36c8a848
commit
fc0e75b5ad
13 changed files with 145 additions and 221 deletions
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
import { useState, useEffect, createContext, useContext } from "react";
|
import { useState, useEffect, createContext, useContext } from "react";
|
||||||
import { ReactNode } from "react";
|
import { ReactNode } from "react";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter, usePathname } from "next/navigation";
|
||||||
|
|
||||||
interface Child {
|
interface Child {
|
||||||
id: string;
|
id: string;
|
||||||
|
|
@ -39,6 +39,7 @@ export function useFamily() {
|
||||||
|
|
||||||
export function FamilyProvider({ children: providerChildren }: { children: ReactNode }) {
|
export function FamilyProvider({ children: providerChildren }: { children: ReactNode }) {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const pathname = usePathname();
|
||||||
const [familyId, setFamilyId] = useState<string | null>(null);
|
const [familyId, setFamilyId] = useState<string | null>(null);
|
||||||
const [familyName, setFamilyName] = useState<string | null>(null);
|
const [familyName, setFamilyName] = useState<string | null>(null);
|
||||||
const [childId, setChildId] = useState<string | null>(null);
|
const [childId, setChildId] = useState<string | null>(null);
|
||||||
|
|
@ -49,12 +50,7 @@ export function FamilyProvider({ children: providerChildren }: { children: React
|
||||||
const [memberCount, setMemberCount] = useState(2);
|
const [memberCount, setMemberCount] = useState(2);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (typeof window === "undefined") {
|
if (pathname?.startsWith("/admin") || pathname === "/admin-login") {
|
||||||
setLoading(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const path = window.location.pathname;
|
|
||||||
if (path.startsWith("/admin") || path === "/admin-login") {
|
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,10 +11,10 @@ export default function AdminLoginPage() {
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const token = document.cookie.match(/tia_admin_session=([^;]+)/)?.[1];
|
fetch("/api/admin/auth", { credentials: "include" })
|
||||||
if (token) {
|
.then((r) => r.json())
|
||||||
router.push("/admin");
|
.then((d) => { if (d.authenticated) router.push("/admin"); })
|
||||||
}
|
.catch(() => {});
|
||||||
}, [router]);
|
}, [router]);
|
||||||
|
|
||||||
const handleLogin = async (e: React.FormEvent) => {
|
const handleLogin = async (e: React.FormEvent) => {
|
||||||
|
|
|
||||||
81
src/app/admin/AdminSidebar.tsx
Normal file
81
src/app/admin/AdminSidebar.tsx
Normal file
|
|
@ -0,0 +1,81 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState } from "react";
|
||||||
|
import { useRouter, usePathname } from "next/navigation";
|
||||||
|
import Link from "next/link";
|
||||||
|
|
||||||
|
interface NavItem {
|
||||||
|
name: string;
|
||||||
|
href: string;
|
||||||
|
icon: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const navItems: NavItem[] = [
|
||||||
|
{ name: "Dashboard", href: "/admin", icon: "📊" },
|
||||||
|
{ name: "Families", href: "/admin/families", icon: "🏠" },
|
||||||
|
{ name: "Users", href: "/admin/users", icon: "👥" },
|
||||||
|
{ name: "Children", href: "/admin/children", icon: "👶" },
|
||||||
|
{ name: "Revenue", href: "/admin/revenue", icon: "💰" },
|
||||||
|
{ name: "Analytics", href: "/admin/analytics", icon: "📈" },
|
||||||
|
{ name: "Support", href: "/admin/support", icon: "🎫" },
|
||||||
|
{ name: "Settings", href: "/admin/settings", icon: "⚙️" },
|
||||||
|
];
|
||||||
|
|
||||||
|
export default function AdminSidebar({ children }: { children: React.ReactNode }) {
|
||||||
|
const router = useRouter();
|
||||||
|
const pathname = usePathname();
|
||||||
|
const [sidebarOpen, setSidebarOpen] = useState(true);
|
||||||
|
|
||||||
|
const handleLogout = async () => {
|
||||||
|
try {
|
||||||
|
await fetch("/api/admin/auth", { method: "DELETE", credentials: "include" });
|
||||||
|
} catch {}
|
||||||
|
router.push("/admin-login");
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gray-900 text-white flex">
|
||||||
|
<aside className={`${sidebarOpen ? "w-64" : "w-16"} bg-gray-800 flex-shrink-0 transition-all duration-300 flex flex-col`}>
|
||||||
|
<div className="p-4 flex items-center justify-between border-b border-gray-700">
|
||||||
|
{sidebarOpen && (
|
||||||
|
<Link href="/admin" className="text-lg font-bold text-rose-400">
|
||||||
|
Tia Admin
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
|
<button onClick={() => setSidebarOpen(!sidebarOpen)} className="text-gray-400 hover:text-white">
|
||||||
|
{sidebarOpen ? "◀" : "▶"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<nav className="flex-1 p-2 space-y-1 overflow-y-auto">
|
||||||
|
{navItems.map((item) => {
|
||||||
|
const isActive = pathname === item.href || (item.href !== "/admin" && pathname.startsWith(item.href));
|
||||||
|
return (
|
||||||
|
<Link
|
||||||
|
key={item.name}
|
||||||
|
href={item.href}
|
||||||
|
className={`flex items-center gap-3 px-3 py-2.5 rounded-lg transition-colors ${
|
||||||
|
isActive ? "bg-rose-500/20 text-rose-400" : "text-gray-400 hover:bg-gray-700 hover:text-white"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<span className="text-lg">{item.icon}</span>
|
||||||
|
{sidebarOpen && <span className="font-medium">{item.name}</span>}
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div className="mt-auto p-4 border-t border-gray-700">
|
||||||
|
<button
|
||||||
|
onClick={handleLogout}
|
||||||
|
className="w-full px-3 py-2 bg-gray-700 text-gray-400 hover:text-white rounded-lg text-sm"
|
||||||
|
>
|
||||||
|
{sidebarOpen ? "Logout" : "🚪"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<main className="flex-1 overflow-auto">{children}</main>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useRouter } from "next/navigation";
|
|
||||||
|
|
||||||
interface EngagementStats {
|
interface EngagementStats {
|
||||||
totalLogs: number;
|
totalLogs: number;
|
||||||
|
|
@ -15,24 +14,16 @@ interface EngagementStats {
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function AdminAnalytics() {
|
export default function AdminAnalytics() {
|
||||||
const router = useRouter();
|
|
||||||
const [stats, setStats] = useState<EngagementStats | null>(null);
|
const [stats, setStats] = useState<EngagementStats | null>(null);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const token = localStorage.getItem("admin_token");
|
|
||||||
if (!token) {
|
|
||||||
router.push("/admin/login");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
fetchAnalytics();
|
fetchAnalytics();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const fetchAnalytics = async () => {
|
const fetchAnalytics = async () => {
|
||||||
try {
|
try {
|
||||||
const res = await fetch("/api/admin/analytics", {
|
const res = await fetch("/api/admin/analytics", { credentials: "include" });
|
||||||
headers: { Authorization: `Bearer ${localStorage.getItem("admin_token")}` },
|
|
||||||
});
|
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
setStats(data);
|
setStats(data);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useRouter } from "next/navigation";
|
|
||||||
|
|
||||||
interface Child {
|
interface Child {
|
||||||
id: string;
|
id: string;
|
||||||
|
|
@ -13,25 +12,17 @@ interface Child {
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function AdminChildren() {
|
export default function AdminChildren() {
|
||||||
const router = useRouter();
|
|
||||||
const [children, setChildren] = useState<Child[]>([]);
|
const [children, setChildren] = useState<Child[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [search, setSearch] = useState("");
|
const [search, setSearch] = useState("");
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const token = localStorage.getItem("admin_token");
|
|
||||||
if (!token) {
|
|
||||||
router.push("/admin/login");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
fetchChildren();
|
fetchChildren();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const fetchChildren = async () => {
|
const fetchChildren = async () => {
|
||||||
try {
|
try {
|
||||||
const res = await fetch("/api/admin/children", {
|
const res = await fetch("/api/admin/children", { credentials: "include" });
|
||||||
headers: { Authorization: `Bearer ${localStorage.getItem("admin_token")}` },
|
|
||||||
});
|
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
setChildren(data.children || []);
|
setChildren(data.children || []);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useRouter } from "next/navigation";
|
|
||||||
|
|
||||||
interface Member {
|
interface Member {
|
||||||
id: string;
|
id: string;
|
||||||
|
|
@ -24,7 +23,6 @@ interface Family {
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function AdminFamilies() {
|
export default function AdminFamilies() {
|
||||||
const router = useRouter();
|
|
||||||
const [families, setFamilies] = useState<Family[]>([]);
|
const [families, setFamilies] = useState<Family[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [search, setSearch] = useState("");
|
const [search, setSearch] = useState("");
|
||||||
|
|
@ -33,19 +31,12 @@ export default function AdminFamilies() {
|
||||||
const [addMember, setAddMember] = useState<{familyId: string; email: string; role: string; name: string} | null>(null);
|
const [addMember, setAddMember] = useState<{familyId: string; email: string; role: string; name: string} | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const token = localStorage.getItem("admin_token");
|
|
||||||
if (!token) {
|
|
||||||
router.push("/admin/login");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
fetchFamilies();
|
fetchFamilies();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const fetchFamilies = async () => {
|
const fetchFamilies = async () => {
|
||||||
try {
|
try {
|
||||||
const res = await fetch("/api/admin/families", {
|
const res = await fetch("/api/admin/families", { credentials: "include" });
|
||||||
headers: { Authorization: `Bearer ${localStorage.getItem("admin_token")}` },
|
|
||||||
});
|
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
if (data.error) {
|
if (data.error) {
|
||||||
console.error("API error:", data.error);
|
console.error("API error:", data.error);
|
||||||
|
|
@ -63,10 +54,8 @@ export default function AdminFamilies() {
|
||||||
try {
|
try {
|
||||||
const res = await fetch("/api/admin/families", {
|
const res = await fetch("/api/admin/families", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: { "Content-Type": "application/json" },
|
||||||
Authorization: `Bearer ${localStorage.getItem("admin_token")}`,
|
credentials: "include",
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
body: JSON.stringify({ name }),
|
body: JSON.stringify({ name }),
|
||||||
});
|
});
|
||||||
if (res.ok) fetchFamilies();
|
if (res.ok) fetchFamilies();
|
||||||
|
|
@ -80,10 +69,8 @@ export default function AdminFamilies() {
|
||||||
try {
|
try {
|
||||||
const res = await fetch("/api/admin/families", {
|
const res = await fetch("/api/admin/families", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: { "Content-Type": "application/json" },
|
||||||
Authorization: `Bearer ${localStorage.getItem("admin_token")}`,
|
credentials: "include",
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
body: JSON.stringify(addMember),
|
body: JSON.stringify(addMember),
|
||||||
});
|
});
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
|
|
@ -100,7 +87,7 @@ export default function AdminFamilies() {
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`/api/admin/families?memberId=${memberId}`, {
|
const res = await fetch(`/api/admin/families?memberId=${memberId}`, {
|
||||||
method: "DELETE",
|
method: "DELETE",
|
||||||
headers: { Authorization: `Bearer ${localStorage.getItem("admin_token")}` },
|
credentials: "include",
|
||||||
});
|
});
|
||||||
if (res.ok) fetchFamilies();
|
if (res.ok) fetchFamilies();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|
|
||||||
|
|
@ -1,110 +1,12 @@
|
||||||
"use client";
|
import { redirect } from "next/navigation";
|
||||||
|
import { verifyAdminSession } from "@/lib/admin-auth";
|
||||||
|
import AdminSidebar from "./AdminSidebar";
|
||||||
|
|
||||||
import { useEffect, useState } from "react";
|
export default async function AdminLayout({ children }: { children: React.ReactNode }) {
|
||||||
import { useRouter, usePathname } from "next/navigation";
|
const auth = await verifyAdminSession();
|
||||||
import Link from "next/link";
|
if (!auth.success) {
|
||||||
|
redirect("/admin-login");
|
||||||
interface NavItem {
|
|
||||||
name: string;
|
|
||||||
href: string;
|
|
||||||
icon: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const navItems: NavItem[] = [
|
|
||||||
{ name: "Dashboard", href: "/admin", icon: "📊" },
|
|
||||||
{ name: "Families", href: "/admin/families", icon: "🏠" },
|
|
||||||
{ name: "Users", href: "/admin/users", icon: "👥" },
|
|
||||||
{ name: "Children", href: "/admin/children", icon: "👶" },
|
|
||||||
{ name: "Revenue", href: "/admin/revenue", icon: "💰" },
|
|
||||||
{ name: "Analytics", href: "/admin/analytics", icon: "📈" },
|
|
||||||
{ name: "Support", href: "/admin/support", icon: "🎫" },
|
|
||||||
{ name: "Settings", href: "/admin/settings", icon: "⚙️" },
|
|
||||||
];
|
|
||||||
|
|
||||||
export default function AdminLayout({ children }: { children: React.ReactNode }) {
|
|
||||||
const router = useRouter();
|
|
||||||
const pathname = usePathname();
|
|
||||||
const [sidebarOpen, setSidebarOpen] = useState(true);
|
|
||||||
|
|
||||||
// Check if this is the login page - don't show sidebar
|
|
||||||
const isLoginPage = pathname === "/admin-login";
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
// Only check auth if not on login page
|
|
||||||
if (isLoginPage) return;
|
|
||||||
|
|
||||||
const token = document.cookie.match(/tia_admin_session=([^;]+)/)?.[1];
|
|
||||||
if (!token) {
|
|
||||||
router.push("/admin-login");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}, [router, isLoginPage]);
|
|
||||||
|
|
||||||
const handleLogout = async () => {
|
|
||||||
try {
|
|
||||||
await fetch("/api/admin/auth", { method: "DELETE" });
|
|
||||||
} catch (e) {}
|
|
||||||
router.push("/admin-login");
|
|
||||||
};
|
|
||||||
|
|
||||||
// Login page - render without sidebar
|
|
||||||
if (isLoginPage) {
|
|
||||||
return (
|
|
||||||
<div className="min-h-screen bg-gray-900 text-white">
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Main layout with sidebar
|
return <AdminSidebar>{children}</AdminSidebar>;
|
||||||
return (
|
|
||||||
<div className="min-h-screen bg-gray-900 text-white flex">
|
|
||||||
{/* Sidebar */}
|
|
||||||
<aside className={`${sidebarOpen ? "w-64" : "w-16"} bg-gray-800 flex-shrink-0 transition-all duration-300 flex flex-col`}>
|
|
||||||
{/* Header */}
|
|
||||||
<div className="p-4 flex items-center justify-between border-b border-gray-700">
|
|
||||||
{sidebarOpen && (
|
|
||||||
<Link href="/admin" className="text-lg font-bold text-rose-400">
|
|
||||||
Tia Admin
|
|
||||||
</Link>
|
|
||||||
)}
|
|
||||||
<button onClick={() => setSidebarOpen(!sidebarOpen)} className="text-gray-400 hover:text-white">
|
|
||||||
{sidebarOpen ? "◀" : "▶"}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Navigation */}
|
|
||||||
<nav className="flex-1 p-2 space-y-1 overflow-y-auto">
|
|
||||||
{navItems.map((item) => {
|
|
||||||
const isActive = pathname === item.href || (item.href !== "/admin" && pathname.startsWith(item.href));
|
|
||||||
return (
|
|
||||||
<Link
|
|
||||||
key={item.name}
|
|
||||||
href={item.href}
|
|
||||||
className={`flex items-center gap-3 px-3 py-2.5 rounded-lg transition-colors ${
|
|
||||||
isActive ? "bg-rose-500/20 text-rose-400" : "text-gray-400 hover:bg-gray-700 hover:text-white"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<span className="text-lg">{item.icon}</span>
|
|
||||||
{sidebarOpen && <span className="font-medium">{item.name}</span>}
|
|
||||||
</Link>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</nav>
|
|
||||||
|
|
||||||
{/* Footer */}
|
|
||||||
<div className="mt-auto p-4 border-t border-gray-700">
|
|
||||||
<button
|
|
||||||
onClick={handleLogout}
|
|
||||||
className="w-full px-3 py-2 bg-gray-700 text-gray-400 hover:text-white rounded-lg text-sm"
|
|
||||||
>
|
|
||||||
{sidebarOpen ? "Logout" : "🚪"}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</aside>
|
|
||||||
|
|
||||||
{/* Main Content */}
|
|
||||||
<main className="flex-1 overflow-auto">{children}</main>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useRouter } from "next/navigation";
|
|
||||||
|
|
||||||
interface Stats {
|
interface Stats {
|
||||||
overview: {
|
overview: {
|
||||||
|
|
@ -25,17 +24,11 @@ interface Stats {
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function AdminDashboard() {
|
export default function AdminDashboard() {
|
||||||
const router = useRouter();
|
|
||||||
const [stats, setStats] = useState<Stats | null>(null);
|
const [stats, setStats] = useState<Stats | null>(null);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [period, setPeriod] = useState("30");
|
const [period, setPeriod] = useState("30");
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const token = document.cookie.match(/tia_admin_session=([^;]+)/)?.[1];
|
|
||||||
if (!token) {
|
|
||||||
router.push("/admin-login");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
fetchStats();
|
fetchStats();
|
||||||
}, [period]);
|
}, [period]);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useRouter } from "next/navigation";
|
|
||||||
|
|
||||||
interface RevenueData {
|
interface RevenueData {
|
||||||
proFamilies: number;
|
proFamilies: number;
|
||||||
|
|
@ -13,24 +12,16 @@ interface RevenueData {
|
||||||
const PRO_PRICE = 9.99;
|
const PRO_PRICE = 9.99;
|
||||||
|
|
||||||
export default function AdminRevenue() {
|
export default function AdminRevenue() {
|
||||||
const router = useRouter();
|
|
||||||
const [data, setData] = useState<RevenueData | null>(null);
|
const [data, setData] = useState<RevenueData | null>(null);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const token = localStorage.getItem("admin_token");
|
|
||||||
if (!token) {
|
|
||||||
router.push("/admin/login");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
fetchRevenue();
|
fetchRevenue();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const fetchRevenue = async () => {
|
const fetchRevenue = async () => {
|
||||||
try {
|
try {
|
||||||
const res = await fetch("/api/admin/stats", {
|
const res = await fetch("/api/admin/stats", { credentials: "include" });
|
||||||
headers: { Authorization: `Bearer ${localStorage.getItem("admin_token")}` },
|
|
||||||
});
|
|
||||||
const stats = await res.json();
|
const stats = await res.json();
|
||||||
setData({
|
setData({
|
||||||
proFamilies: stats.overview?.proFamilies || 0,
|
proFamilies: stats.overview?.proFamilies || 0,
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useEffect, useState } from "react";
|
import { useState } from "react";
|
||||||
import { useRouter } from "next/navigation";
|
|
||||||
|
|
||||||
interface Settings {
|
interface Settings {
|
||||||
proPrice: number;
|
proPrice: number;
|
||||||
|
|
@ -12,7 +11,6 @@ interface Settings {
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function AdminSettings() {
|
export default function AdminSettings() {
|
||||||
const router = useRouter();
|
|
||||||
const [settings, setSettings] = useState<Settings>({
|
const [settings, setSettings] = useState<Settings>({
|
||||||
proPrice: 9.99,
|
proPrice: 9.99,
|
||||||
freeMaxChildren: 1,
|
freeMaxChildren: 1,
|
||||||
|
|
@ -22,14 +20,6 @@ export default function AdminSettings() {
|
||||||
});
|
});
|
||||||
const [saved, setSaved] = useState(false);
|
const [saved, setSaved] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const token = localStorage.getItem("admin_token");
|
|
||||||
if (!token) {
|
|
||||||
router.push("/admin/login");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}, [router]);
|
|
||||||
|
|
||||||
const handleSave = async () => {
|
const handleSave = async () => {
|
||||||
setSaved(true);
|
setSaved(true);
|
||||||
setTimeout(() => setSaved(false), 2000);
|
setTimeout(() => setSaved(false), 2000);
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useRouter } from "next/navigation";
|
|
||||||
|
|
||||||
interface Ticket {
|
interface Ticket {
|
||||||
id: string;
|
id: string;
|
||||||
|
|
@ -15,7 +14,6 @@ interface Ticket {
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function AdminSupport() {
|
export default function AdminSupport() {
|
||||||
const router = useRouter();
|
|
||||||
const [tickets, setTickets] = useState<Ticket[]>([]);
|
const [tickets, setTickets] = useState<Ticket[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [statusFilter, setStatusFilter] = useState("all");
|
const [statusFilter, setStatusFilter] = useState("all");
|
||||||
|
|
@ -23,19 +21,12 @@ export default function AdminSupport() {
|
||||||
const [replyMessage, setReplyMessage] = useState("");
|
const [replyMessage, setReplyMessage] = useState("");
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const token = localStorage.getItem("admin_token");
|
|
||||||
if (!token) {
|
|
||||||
router.push("/admin/login");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
fetchTickets();
|
fetchTickets();
|
||||||
}, [statusFilter]);
|
}, [statusFilter]);
|
||||||
|
|
||||||
const fetchTickets = async () => {
|
const fetchTickets = async () => {
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`/api/admin/support?status=${statusFilter}`, {
|
const res = await fetch(`/api/admin/support?status=${statusFilter}`, { credentials: "include" });
|
||||||
headers: { Authorization: `Bearer ${localStorage.getItem("admin_token")}` },
|
|
||||||
});
|
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
setTickets(data.tickets || []);
|
setTickets(data.tickets || []);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|
@ -48,10 +39,8 @@ export default function AdminSupport() {
|
||||||
try {
|
try {
|
||||||
await fetch(`/api/admin/support`, {
|
await fetch(`/api/admin/support`, {
|
||||||
method: "PATCH",
|
method: "PATCH",
|
||||||
headers: {
|
headers: { "Content-Type": "application/json" },
|
||||||
"Content-Type": "application/json",
|
credentials: "include",
|
||||||
Authorization: `Bearer ${localStorage.getItem("admin_token")}`,
|
|
||||||
},
|
|
||||||
body: JSON.stringify({ ticketId, status }),
|
body: JSON.stringify({ ticketId, status }),
|
||||||
});
|
});
|
||||||
fetchTickets();
|
fetchTickets();
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useRouter } from "next/navigation";
|
|
||||||
|
|
||||||
interface User {
|
interface User {
|
||||||
id: string;
|
id: string;
|
||||||
|
|
@ -20,7 +19,6 @@ interface Family {
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function AdminUsers() {
|
export default function AdminUsers() {
|
||||||
const router = useRouter();
|
|
||||||
const [users, setUsers] = useState<User[]>([]);
|
const [users, setUsers] = useState<User[]>([]);
|
||||||
const [families, setFamilies] = useState<Family[]>([]);
|
const [families, setFamilies] = useState<Family[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
@ -30,20 +28,13 @@ export default function AdminUsers() {
|
||||||
const [newUser, setNewUser] = useState({ email: "", name: "", familyId: "", role: "caregiver" });
|
const [newUser, setNewUser] = useState({ email: "", name: "", familyId: "", role: "caregiver" });
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const token = localStorage.getItem("admin_token");
|
|
||||||
if (!token) {
|
|
||||||
router.push("/admin/login");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
fetchUsers();
|
fetchUsers();
|
||||||
fetchFamilies();
|
fetchFamilies();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const fetchUsers = async () => {
|
const fetchUsers = async () => {
|
||||||
try {
|
try {
|
||||||
const res = await fetch("/api/admin/users", {
|
const res = await fetch("/api/admin/users", { credentials: "include" });
|
||||||
headers: { Authorization: `Bearer ${localStorage.getItem("admin_token")}` },
|
|
||||||
});
|
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
setUsers(data.users || []);
|
setUsers(data.users || []);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|
@ -54,9 +45,7 @@ export default function AdminUsers() {
|
||||||
|
|
||||||
const fetchFamilies = async () => {
|
const fetchFamilies = async () => {
|
||||||
try {
|
try {
|
||||||
const res = await fetch("/api/admin/families", {
|
const res = await fetch("/api/admin/families", { credentials: "include" });
|
||||||
headers: { Authorization: `Bearer ${localStorage.getItem("admin_token")}` },
|
|
||||||
});
|
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
setFamilies(data.families || []);
|
setFamilies(data.families || []);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|
@ -69,10 +58,8 @@ export default function AdminUsers() {
|
||||||
try {
|
try {
|
||||||
const res = await fetch("/api/admin/users", {
|
const res = await fetch("/api/admin/users", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: { "Content-Type": "application/json" },
|
||||||
Authorization: `Bearer ${localStorage.getItem("admin_token")}`,
|
credentials: "include",
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
body: JSON.stringify(newUser),
|
body: JSON.stringify(newUser),
|
||||||
});
|
});
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
|
|
@ -91,7 +78,7 @@ export default function AdminUsers() {
|
||||||
const params = memberId ? `memberId=${memberId}` : `userId=${userId}`;
|
const params = memberId ? `memberId=${memberId}` : `userId=${userId}`;
|
||||||
const res = await fetch(`/api/admin/users?${params}`, {
|
const res = await fetch(`/api/admin/users?${params}`, {
|
||||||
method: "DELETE",
|
method: "DELETE",
|
||||||
headers: { Authorization: `Bearer ${localStorage.getItem("admin_token")}` },
|
credentials: "include",
|
||||||
});
|
});
|
||||||
if (res.ok) fetchUsers();
|
if (res.ok) fetchUsers();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|
@ -104,10 +91,8 @@ export default function AdminUsers() {
|
||||||
try {
|
try {
|
||||||
const res = await fetch("/api/admin/users", {
|
const res = await fetch("/api/admin/users", {
|
||||||
method: "PATCH",
|
method: "PATCH",
|
||||||
headers: {
|
headers: { "Content-Type": "application/json" },
|
||||||
Authorization: `Bearer ${localStorage.getItem("admin_token")}`,
|
credentials: "include",
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
body: JSON.stringify({ userId, password }),
|
body: JSON.stringify({ userId, password }),
|
||||||
});
|
});
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,34 @@
|
||||||
import { NextResponse } from "next/server";
|
import { NextResponse } from "next/server";
|
||||||
|
import { cookies } from "next/headers";
|
||||||
import { sql } from "@/db";
|
import { sql } from "@/db";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verify admin session in server components using next/headers cookies.
|
||||||
|
*/
|
||||||
|
export async function verifyAdminSession(): Promise<{
|
||||||
|
success: boolean;
|
||||||
|
admin?: { username: string; role: string };
|
||||||
|
}> {
|
||||||
|
try {
|
||||||
|
const cookieStore = await cookies();
|
||||||
|
const sessionToken = cookieStore.get("tia_admin_session")?.value;
|
||||||
|
if (!sessionToken) return { success: false };
|
||||||
|
|
||||||
|
const sessions = await sql.unsafe(
|
||||||
|
`SELECT username, role FROM admin_sessions
|
||||||
|
JOIN admins ON admins.id = admin_sessions.admin_id
|
||||||
|
WHERE session_token = $1 AND expires_at > NOW()
|
||||||
|
LIMIT 1`,
|
||||||
|
[sessionToken]
|
||||||
|
);
|
||||||
|
if (!sessions || sessions.length === 0) return { success: false };
|
||||||
|
const row = sessions[0] as unknown as { username: string; role: string };
|
||||||
|
return { success: true, admin: { username: row.username, role: row.role } };
|
||||||
|
} catch {
|
||||||
|
return { success: false };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validate admin session from tia_admin_session cookie
|
* Validate admin session from tia_admin_session cookie
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue