chore: standardize security and quality foundations
This commit is contained in:
@@ -1,37 +1,55 @@
|
||||
'use client'
|
||||
"use client";
|
||||
|
||||
import { usePathname } from 'next/navigation'
|
||||
import { useEffect, useState, type ReactNode } from 'react'
|
||||
import { authFetch, getApiBase, getToken } from '../lib/auth'
|
||||
import { canAccess, featureForPath, type FeatureAccess } from '../lib/features'
|
||||
import { usePathname } from "next/navigation";
|
||||
import { useEffect, useState, type ReactNode } from "react";
|
||||
import { authFetch, getApiBase, getToken } from "../lib/auth";
|
||||
import { canAccess, featureForPath, type FeatureAccess } from "../lib/features";
|
||||
|
||||
export function useFeatureUser() {
|
||||
const pathname = usePathname()
|
||||
const [state, setState] = useState<{ path: string; user: { role?: string; features?: FeatureAccess; invite_management_enabled?: boolean } | null }>({ path: '', user: null })
|
||||
const pathname = usePathname();
|
||||
const [state, setState] = useState<{
|
||||
path: string;
|
||||
user: { role?: string; features?: FeatureAccess; invite_management_enabled?: boolean } | null;
|
||||
}>({ path: "", user: null });
|
||||
useEffect(() => {
|
||||
let active = true
|
||||
let active = true;
|
||||
const load = async () => {
|
||||
if (!getToken()) { if (active) setState({ path: pathname, user: null }); return }
|
||||
if (!getToken()) {
|
||||
if (active) setState({ path: pathname, user: null });
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const response = await authFetch(`${getApiBase()}/auth/me`)
|
||||
const user = response.ok ? await response.json() : null
|
||||
if (active) setState({ path: pathname, user })
|
||||
} catch { if (active) setState({ path: pathname, user: null }) }
|
||||
}
|
||||
void load()
|
||||
window.addEventListener('focus', load)
|
||||
return () => { active = false; window.removeEventListener('focus', load) }
|
||||
}, [pathname])
|
||||
return { user: state.user, ready: state.path === pathname }
|
||||
const response = await authFetch(`${getApiBase()}/auth/me`);
|
||||
const user = response.ok ? await response.json() : null;
|
||||
if (active) setState({ path: pathname, user });
|
||||
} catch {
|
||||
if (active) setState({ path: pathname, user: null });
|
||||
}
|
||||
};
|
||||
void load();
|
||||
window.addEventListener("focus", load);
|
||||
return () => {
|
||||
active = false;
|
||||
window.removeEventListener("focus", load);
|
||||
};
|
||||
}, [pathname]);
|
||||
return { user: state.user, ready: state.path === pathname };
|
||||
}
|
||||
|
||||
export default function FeatureGate({ children }: { children: ReactNode }) {
|
||||
const pathname = usePathname()
|
||||
const { user, ready } = useFeatureUser()
|
||||
const feature = featureForPath(pathname)
|
||||
if (!feature) return children
|
||||
if (!ready) return <main className="card">Loading account access...</main>
|
||||
if (!getToken()) return children
|
||||
if (!canAccess(user, feature)) return <main className="card"><h1>Feature unavailable</h1><p>Your account does not have access to this feature. Ask an administrator if you need it enabled.</p><a href="/profile">Go to my profile</a></main>
|
||||
return children
|
||||
const pathname = usePathname();
|
||||
const { user, ready } = useFeatureUser();
|
||||
const feature = featureForPath(pathname);
|
||||
if (!feature) return children;
|
||||
if (!ready) return <main className="card">Loading account access...</main>;
|
||||
if (!getToken()) return children;
|
||||
if (!canAccess(user, feature))
|
||||
return (
|
||||
<main className="card">
|
||||
<h1>Feature unavailable</h1>
|
||||
<p>Your account does not have access to this feature. Ask an administrator if you need it enabled.</p>
|
||||
<a href="/profile">Go to my profile</a>
|
||||
</main>
|
||||
);
|
||||
return children;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user