feat: roles/permissions system and Nostr profile display on admin users
Introduce granular role-based permissions with SuperAdmin env override, admin roles UI, and permission-gated API routes. Fix admin user Nostr metadata by batching relay profile fetches, normalizing npub pubkeys to hex, and adding reusable NostrAvatar/useNostrProfile components. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -21,28 +21,37 @@ import {
|
||||
HelpCircle,
|
||||
MessageSquare,
|
||||
Building2,
|
||||
KeyRound,
|
||||
} from "lucide-react";
|
||||
|
||||
const navItems = [
|
||||
{ href: "/admin/overview", label: "Overview", icon: LayoutDashboard, adminOnly: false },
|
||||
{ href: "/admin/events", label: "Events", icon: Calendar, adminOnly: false },
|
||||
{ href: "/admin/organizers", label: "Organizers", icon: Building2, adminOnly: false },
|
||||
{ href: "/admin/gallery", label: "Gallery", icon: ImageIcon, adminOnly: false },
|
||||
{ href: "/admin/blog", label: "Blog", icon: FileText, adminOnly: false },
|
||||
{ href: "/admin/faq", label: "FAQ", icon: HelpCircle, adminOnly: false },
|
||||
{ href: "/admin/submissions", label: "Submissions", icon: Inbox, adminOnly: false },
|
||||
{ href: "/admin/messages", label: "Board", icon: MessageSquare, adminOnly: false },
|
||||
{ href: "/admin/moderation", label: "Moderation", icon: Shield, adminOnly: false },
|
||||
{ href: "/admin/categories", label: "Categories", icon: Tag, adminOnly: false },
|
||||
{ href: "/admin/users", label: "Users", icon: Users, adminOnly: true },
|
||||
{ href: "/admin/relays", label: "Relays", icon: Radio, adminOnly: true },
|
||||
{ href: "/admin/settings", label: "Settings", icon: Settings, adminOnly: true },
|
||||
{ href: "/admin/nostr", label: "Nostr Tools", icon: Wrench, adminOnly: true },
|
||||
// Each item is shown when the user holds any of its permissions. Items with no
|
||||
// permissions are always visible. SuperAdmin sees everything via can().
|
||||
const navItems: {
|
||||
href: string;
|
||||
label: string;
|
||||
icon: typeof LayoutDashboard;
|
||||
permissions?: string[];
|
||||
}[] = [
|
||||
{ href: "/admin/overview", label: "Overview", icon: LayoutDashboard },
|
||||
{ href: "/admin/events", label: "Events", icon: Calendar, permissions: ["events.create", "events.edit", "events.delete"] },
|
||||
{ href: "/admin/organizers", label: "Organizers", icon: Building2, permissions: ["organizers.manage"] },
|
||||
{ href: "/admin/gallery", label: "Gallery", icon: ImageIcon, permissions: ["gallery.upload", "gallery.delete"] },
|
||||
{ href: "/admin/blog", label: "Blog", icon: FileText, permissions: ["blog.draft", "blog.publish", "blog.delete"] },
|
||||
{ href: "/admin/faq", label: "FAQ", icon: HelpCircle, permissions: ["faq.manage"] },
|
||||
{ href: "/admin/submissions", label: "Submissions", icon: Inbox, permissions: ["submissions.review"] },
|
||||
{ href: "/admin/messages", label: "Board", icon: MessageSquare, permissions: ["board.manage"] },
|
||||
{ href: "/admin/moderation", label: "Moderation", icon: Shield, permissions: ["moderation.act"] },
|
||||
{ href: "/admin/categories", label: "Categories", icon: Tag, permissions: ["categories.manage"] },
|
||||
{ href: "/admin/users", label: "Users", icon: Users, permissions: ["users.assign_role", "nip05.assign"] },
|
||||
{ href: "/admin/roles", label: "Roles", icon: KeyRound, permissions: ["roles.edit_permissions"] },
|
||||
{ href: "/admin/relays", label: "Relays", icon: Radio, permissions: ["relays.manage"] },
|
||||
{ href: "/admin/settings", label: "Settings", icon: Settings, permissions: ["settings.edit"] },
|
||||
{ href: "/admin/nostr", label: "Nostr Tools", icon: Wrench, permissions: ["nostr_tools.use"] },
|
||||
];
|
||||
|
||||
export function AdminSidebar() {
|
||||
const pathname = usePathname();
|
||||
const { user, logout, isAdmin } = useAuth();
|
||||
const { user, logout, can } = useAuth();
|
||||
|
||||
const shortPubkey = user?.pubkey
|
||||
? `${user.pubkey.slice(0, 8)}...${user.pubkey.slice(-8)}`
|
||||
@@ -68,19 +77,22 @@ export function AdminSidebar() {
|
||||
<p className="text-on-surface/70 text-sm font-mono truncate">{shortPubkey}</p>
|
||||
<span
|
||||
className={cn(
|
||||
"inline-block mt-1 rounded-full px-3 py-1 text-xs font-bold",
|
||||
user.role === "ADMIN"
|
||||
"inline-block mt-1 rounded-full px-3 py-1 text-xs font-bold capitalize",
|
||||
user.isSuperAdmin || user.role === "admin"
|
||||
? "bg-primary-container/20 text-primary"
|
||||
: "bg-secondary-container text-on-secondary-container"
|
||||
)}
|
||||
>
|
||||
{user.role}
|
||||
{user.isSuperAdmin ? "SuperAdmin" : user.role}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<nav className="flex-1 space-y-1">
|
||||
{navItems
|
||||
.filter((item) => !item.adminOnly || isAdmin)
|
||||
.filter(
|
||||
(item) =>
|
||||
!item.permissions || item.permissions.some((p) => can(p))
|
||||
)
|
||||
.map((item) => {
|
||||
const Icon = item.icon;
|
||||
const active = pathname === item.href;
|
||||
|
||||
Reference in New Issue
Block a user