fix(admin): P0 integrity — permanent bans, ACL sidebar, rank guards
Co-authored-by: Cursor <[email protected]>
This commit is contained in:
1 parent
11004626c7
commit
75cdfdebe4
14 files changed
+458
-68
No files matched your search
@@ -5,7 +5,11 @@ import { usePathname } from "next/navigation";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { AdminNavLink } from "@/components/admin/admin-nav-link";
|
||||
import { ADMIN_NAV_GROUPS, type AdminNavItem } from "@/lib/admin-nav";
|
||||
import {
|
||||
ADMIN_NAV_GROUPS,
|
||||
type AdminNavItem,
|
||||
navItemIsAllowed,
|
||||
} from "@/lib/admin-nav";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const STORAGE_KEY = "admin-nav-collapsed";
|
||||
@@ -52,13 +56,35 @@ function persistCollapsed(next: Record<string, boolean>) {
|
||||
}
|
||||
}
|
||||
|
||||
export function AdminSidebarNav() {
|
||||
export function AdminSidebarNav({
|
||||
allowedPermissions = [],
|
||||
isSuperAdmin = false,
|
||||
}: {
|
||||
/** ACL slugs the staff member holds (ignored when isSuperAdmin). */
|
||||
allowedPermissions?: string[];
|
||||
isSuperAdmin?: boolean;
|
||||
}) {
|
||||
const t = useTranslations("pages.admin.nav");
|
||||
const pathname = usePathname() ?? "";
|
||||
const [collapsed, setCollapsed] = useState<Record<string, boolean>>({});
|
||||
const [hydrated, setHydrated] = useState(false);
|
||||
|
||||
const groups = useMemo(() => ADMIN_NAV_GROUPS, []);
|
||||
const allowed = useMemo(
|
||||
() => new Set(allowedPermissions),
|
||||
[allowedPermissions],
|
||||
);
|
||||
|
||||
const groups = useMemo(() => {
|
||||
const access = {
|
||||
isSuperAdmin,
|
||||
has: (slug: string) => allowed.has(slug),
|
||||
};
|
||||
return ADMIN_NAV_GROUPS.map((group) => ({
|
||||
...group,
|
||||
items: group.items.filter((item) => navItemIsAllowed(item, access)),
|
||||
})).filter((group) => group.items.length > 0);
|
||||
}, [allowed, isSuperAdmin]);
|
||||
|
||||
const allKeys = useMemo(() => groups.map((g) => g.labelKey), [groups]);
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import Image from "next/image";
|
||||
import type { ReactNode } from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export interface OnlineUser {
|
||||
id: number;
|
||||
@@ -14,12 +15,29 @@ export interface OnlineUser {
|
||||
|
||||
type State = "ok" | "warn" | "danger" | "neutral";
|
||||
|
||||
const STATE_RING: Record<State, string> = {
|
||||
ok: "ring-1 ring-[var(--admin-success-subtle)]/50 border-[var(--admin-success-subtle)]/40",
|
||||
warn: "ring-1 ring-[var(--admin-accent)]/40 border-[var(--admin-accent)]/35",
|
||||
danger:
|
||||
"ring-1 ring-[var(--admin-error-subtle)]/50 border-[var(--admin-error-subtle)]/45",
|
||||
neutral: "border-[var(--admin-border)]",
|
||||
};
|
||||
|
||||
const STATE_ICON_BG: Record<State, string> = {
|
||||
ok: "from-[var(--admin-success-subtle)]/25 to-[var(--admin-success-subtle)]/5 text-[var(--admin-success)]",
|
||||
warn: "from-[var(--admin-accent)]/20 to-[var(--admin-accent)]/5 text-[var(--admin-accent)]",
|
||||
danger:
|
||||
"from-[var(--admin-error-subtle)]/25 to-[var(--admin-error-subtle)]/5 text-[var(--admin-error)]",
|
||||
neutral:
|
||||
"from-[var(--admin-accent)]/15 to-[var(--admin-accent)]/5 text-[var(--admin-accent)]",
|
||||
};
|
||||
|
||||
/** A single live-status tile (online users, emulator, DB, …). */
|
||||
export function StatusCard({
|
||||
label,
|
||||
value,
|
||||
hint,
|
||||
state: _state = "neutral",
|
||||
state = "neutral",
|
||||
icon,
|
||||
}: {
|
||||
label: string;
|
||||
@@ -29,13 +47,23 @@ export function StatusCard({
|
||||
icon?: ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<div className="bg-[var(--admin-surface)] border border-[var(--admin-border)] rounded-2xl p-5 shadow-card hover:shadow-card-hover transition-shadow duration-200">
|
||||
<div
|
||||
className={cn(
|
||||
"bg-[var(--admin-surface)] border rounded-2xl p-5 shadow-card hover:shadow-card-hover transition-shadow duration-200",
|
||||
STATE_RING[state],
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center justify-between gap-3 mb-3">
|
||||
<span className="text-[0.65rem] font-bold uppercase tracking-widest text-[var(--admin-text-muted)]">
|
||||
{label}
|
||||
</span>
|
||||
{icon ? (
|
||||
<span className="w-8 h-8 rounded-xl bg-gradient-to-br from-[var(--admin-accent)]/15 to-[var(--admin-accent)]/5 grid place-items-center text-[var(--admin-accent)] flex-none">
|
||||
<span
|
||||
className={cn(
|
||||
"w-8 h-8 rounded-xl bg-gradient-to-br grid place-items-center flex-none",
|
||||
STATE_ICON_BG[state],
|
||||
)}
|
||||
>
|
||||
{icon}
|
||||
</span>
|
||||
) : null}
|
||||
|
||||
Reference in new issue
Block a user