**Module access control (admin)** - Added modules column to users table (JSON array of allowed module keys) - auth.me returns modules; admin.setUserModules/disableUser/enableUser/deleteUser procedures - UserActions dropdown with Reset Password, Manage Modules, Disable/Enable, Delete - useFeatureAccess hook + FeatureGate component for page-level gating - SidebarNav, CommandPalette, MobileTabNav filter by modules **Classification watchlists (auto-generated sector/thematic/style/region)** - watchlists schema: added kind, class_key, class_label columns - materializeClassificationWatchlists(): lazy idempotent materialization of system lists from Yahoo sector data - 11 GICS sector lists, Semiconductors, Biotech thematic lists, Small Cap style, Intl region - Materialization triggered on listWatchlists + addSymbol/removeSymbol/add - Sector thematic filter uses industry keywords (semiconductors, biotech) to avoid misclassification - Symbol overrides fixed: IREN removed from SMH theme, bitcoin miners XLE->XLK, SLNH added as Technology **Watchlist persistence & move** - active-watchlist-store.ts: Zustand persist store mirroring active-symbol-store pattern - moveSymbol procedure: removes from source, adds to target (idempotent, preserves empty lists) - Action menu (⋯) per row: Move to + Remove, click-outside close - Active watchlist survives navigation and page reloads **List protections** - default list: non-deletable, non-renamable, keeps empty row when pruned - System lists (sector/thematic etc.): read-only, add form hidden, no remove/move/delete **Per-user module restrictions** - ProtectedProcedure blocks non-active users - deleteUser refuses self-delete and last-admin-delete, cleans halt_state manually - Module gating on execution/analytics pages, sidebar, command palette, mobile nav Also fixed: schema.sql index moved to migration, materialize DB column fixes, test updates.
43 lines
1.2 KiB
TypeScript
43 lines
1.2 KiB
TypeScript
"use client";
|
|
import { useEffect } from "react";
|
|
import { useRouter } from "next/navigation";
|
|
import { useFeatureAccess, type ModuleKey } from "@/lib/useFeatureAccess";
|
|
|
|
interface FeatureGateProps {
|
|
module: ModuleKey;
|
|
children: React.ReactNode;
|
|
fallback?: React.ReactNode;
|
|
}
|
|
|
|
export function FeatureGate({ module, children, fallback }: FeatureGateProps) {
|
|
const { modules, loading } = useFeatureAccess();
|
|
const router = useRouter();
|
|
const allowed = modules.includes(module);
|
|
|
|
useEffect(() => {
|
|
if (!loading && !allowed) {
|
|
router.replace("/");
|
|
}
|
|
}, [loading, allowed, router]);
|
|
|
|
if (loading) {
|
|
return (
|
|
<div className="flex items-center justify-center py-16">
|
|
<div className="h-8 w-8 animate-spin rounded-full border-2 border-accent border-t-transparent" />
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (!allowed) {
|
|
if (fallback) return <>{fallback}</>;
|
|
return (
|
|
<div className="flex flex-col items-center justify-center py-16 space-y-4">
|
|
<p className="text-lg font-medium text-fg">Access Restricted</p>
|
|
<p className="text-sm text-fg-muted">You don't have access to this module.</p>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return <>{children}</>;
|
|
}
|