Admin Dashboard
The admin dashboard is a full-featured management panel served at admin.myapp.test. It provides CRUD interfaces for every resource in the system, permission-gated navigation, and a dashboard with real-time statistics. The entire UI is built with shadcn/ui components and follows a consistent sidebar + header + content layout.
Layout Structure
Every authenticated page in the admin panel renders within a three-part layout:
┌──────────────────────────────────────────────────────────┐
│ ┌──────────┐ ┌─────────────────────────────────────────┐ │
│ │ │ │ Header │ │
│ │ │ │ (Theme toggle, Language, User menu) │ │
│ │ │ ├─────────────────────────────────────────┤ │
│ │ Sidebar │ │ │ │
│ │ │ │ Main Content │ │
│ │ (Nav) │ │ │ │
│ │ │ │ ┌─────────────────────────────────┐ │ │
│ │ │ │ │ Page Component │ │ │
│ │ │ │ │ │ │ │
│ │ │ │ │ (DataTable, Forms, etc.) │ │ │
│ │ │ │ │ │ │ │
│ │ │ │ └─────────────────────────────────┘ │ │
│ └──────────┘ └─────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────┘The layout is defined in app/(dashboard)/layout.tsx and wraps all dashboard routes:
import { Sidebar } from "@/components/layout/sidebar";
import { Header } from "@/components/layout/header";
export default function DashboardLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<div className="flex h-screen">
<Sidebar />
<div className="flex flex-1 flex-col overflow-hidden">
<Header />
<main className="flex-1 overflow-y-auto p-6">
{children}
</main>
</div>
</div>
);
}Sidebar Navigation
The sidebar renders navigation items based on the authenticated user's permissions. Each item maps to a permission string -- if the user lacks the required permission, the item is not rendered at all:
const navItems: NavItem[] = [
{
titleKey: "admin.nav.Dashboard",
href: "/",
icon: LayoutDashboard,
permission: "dashboard.view",
},
{
titleKey: "admin.nav.Users",
icon: Users,
permission: "users.view",
children: [
{ titleKey: "admin.nav.All_Users", href: "/users", permission: "users.view" },
{ titleKey: "admin.nav.Create_User", href: "/users/create", permission: "users.create" },
],
},
{
titleKey: "admin.nav.Contents",
href: "/contents",
icon: Newspaper,
permission: "contents.view",
},
{
titleKey: "admin.nav.Menus",
href: "/menus",
icon: Navigation,
permission: "menus.view",
},
{
titleKey: "admin.nav.Lookups",
href: "/lookups",
icon: ListTree,
permission: "lookups.view",
},
{
titleKey: "admin.nav.Media",
href: "/media",
icon: ImageIcon,
permission: "media.view",
},
// ...additional items
];Each NavItem uses a usePermission hook that checks against the user's permission array:
const hasPermission = usePermission(item.permission || "");
// If permission is required and user lacks it, hide the item
if (item.permission && !hasPermission) {
return null;
}TIP
Navigation items with children render as collapsible sections using Radix UI's Collapsible component. The sidebar itself is collapsible on desktop (icon-only mode) and slides in as an overlay on mobile.
Login Page
The admin login page (app/(auth)/login/page.tsx) provides credential-based authentication with optional OTP support:
┌─────────────────────────────────┐
│ Admin Login │
│ │
│ ┌───────────────────────────┐ │
│ │ Email │ │
│ └───────────────────────────┘ │
│ ┌───────────────────────────┐ │
│ │ Password │ │
│ └───────────────────────────┘ │
│ │
│ [ Login ] │
│ │
│ ── or ── │
│ │
│ [ Login with OTP ] │
└─────────────────────────────────┘On successful login, the JWT access token is stored in localStorage and the user is redirected to the dashboard. The AuthProvider then calls /auth/me to load the user's profile, roles, and permissions.
Dashboard Page
The dashboard page (app/(dashboard)/page.tsx) displays statistics and recent activity:
| Section | Content |
|---|---|
| Stats Cards | Total users, active roles, content pages, recent audit events |
| Recent Activity | Latest audit log entries with user, action, resource, and timestamp |
import { Stats } from "@/components/dashboard/stats";
import { RecentActivity } from "@/components/dashboard/recent-activity";
export default function DashboardPage() {
return (
<div className="space-y-8">
<div>
<h1 className="text-3xl font-bold tracking-tight">Dashboard</h1>
<p className="text-muted-foreground">
Overview of your application
</p>
</div>
<Stats />
<RecentActivity />
</div>
);
}CRUD Pages
Every resource follows the same page structure pattern. Here is the complete list of admin CRUD modules:
| Module | Route | Permissions | Features |
|---|---|---|---|
| Users | /users | users.view/create/edit/delete | List, create, edit, delete, role assignment |
| Roles | /roles | roles.view/create/edit/delete | List, create, edit, permission assignment |
| Permissions | /permissions | permissions.view | Read-only hierarchical view |
| Contents | /contents | contents.view/create/edit/delete | Rich text editor, translations, SEO, media |
| Menus | /menus | menus.view/create/edit/delete | Hierarchical navigation builder |
| Lookups | /lookups | lookups.view/create/edit/delete | Reference data with icons and colors |
| Media | /media | media.view/upload/delete | Drag-drop upload, file browser, copy URL |
| Translations | /translations | translations.view/edit | Key-value translation editor |
| Settings | /settings | settings.view/edit | Grouped application settings |
| API Keys | /api-keys | api_keys.view/create/delete | Key generation with permission scoping |
| Audit Logs | /audit | audit.view | Read-only activity log with filters |
| Profile | /profile | (any authenticated user) | Edit name, email, password, avatar |
Page Structure Convention
Each CRUD module follows a consistent file layout:
app/(dashboard)/users/
├── page.tsx # List page with DataTable
├── columns.tsx # Column definitions for DataTable
├── create/
│ └── page.tsx # Create form
└── [id]/
├── page.tsx # View/detail page
└── edit/
└── page.tsx # Edit formUsers List Page
The users list page demonstrates the standard CRUD pattern used throughout the admin panel:
"use client";
import * as React from "react";
import Link from "next/link";
import { toast } from "sonner";
import { DataTable } from "@/components/ui/data-table";
import { Button } from "@/components/ui/button";
import { ConfirmDialog } from "@/components/ui/confirm-dialog";
import { usePermission } from "@/components/providers/auth-provider";
import { useTranslation } from "@/components/providers/i18n-provider";
import { api, getErrorMessage } from "@/lib/api";
import { getColumns } from "./columns";
import type { User } from "@/lib/types";
import { Plus } from "lucide-react";
export default function UsersPage() {
const { t } = useTranslation();
const canCreate = usePermission("users.create");
const canEdit = usePermission("users.edit");
const canDelete = usePermission("users.delete");
const [users, setUsers] = React.useState<User[]>([]);
const [isLoading, setIsLoading] = React.useState(true);
const [deleteUser, setDeleteUser] = React.useState<User | null>(null);
const [isDeleting, setIsDeleting] = React.useState(false);
const fetchUsers = React.useCallback(async () => {
setIsLoading(true);
try {
const response = await api.get<{
data: { data: User[]; meta: unknown };
}>("/admin/users");
setUsers(response.data.data);
} catch (error) {
toast.error(getErrorMessage(error));
} finally {
setIsLoading(false);
}
}, []);
React.useEffect(() => {
fetchUsers();
}, [fetchUsers]);
const handleDelete = async () => {
if (!deleteUser) return;
setIsDeleting(true);
try {
await api.delete(`/admin/users/${deleteUser.id}`);
toast.success(t("admin.users.User_deleted_successfully"));
setDeleteUser(null);
fetchUsers();
} catch (error) {
toast.error(getErrorMessage(error));
} finally {
setIsDeleting(false);
}
};
const columns = React.useMemo(
() => getColumns({ onDelete: setDeleteUser, canEdit, canDelete }),
[canEdit, canDelete]
);
return (
<div className="space-y-6">
<div className="flex items-center justify-between">
<div>
<h1 className="text-3xl font-bold tracking-tight">
{t("admin.users.Users")}
</h1>
</div>
{canCreate && (
<Button asChild>
<Link href="/users/create">
<Plus className="mr-2 h-4 w-4" />
{t("admin.users.Add_User")}
</Link>
</Button>
)}
</div>
<DataTable
columns={columns}
data={users}
searchKey="name"
searchPlaceholder={t("admin.users.Filter_users")}
/>
<ConfirmDialog
open={!!deleteUser}
onOpenChange={(open) => !open && setDeleteUser(null)}
title={t("admin.users.Delete_User")}
description={t("admin.users.Are_you_sure_you_want_to_delete_this_user?")}
confirmText={t("common.Delete")}
variant="destructive"
isLoading={isDeleting}
onConfirm={handleDelete}
/>
</div>
);
}WARNING
Every action button and menu item checks permissions before rendering. The usePermission hook returns false if the user lacks the specified permission, allowing you to conditionally show or hide create, edit, and delete controls.
Role Management
The role management page includes a permission matrix for assigning permissions to roles. The PermissionSelector component displays permissions grouped by category with select-all controls:
import { PermissionSelector } from "@/components/ui/permission-selector";
<PermissionSelector
permissions={allPermissions}
selected={selectedPermissionIds}
onChange={setSelectedPermissionIds}
/>Permissions are displayed in a hierarchical tree:
Users
├── [x] View Users
├── [x] Create Users
├── [x] Edit Users
└── [ ] Delete Users
Contents
├── [x] View Contents
├── [x] Create Contents
├── [x] Edit Contents
└── [x] Delete ContentsContent Management
The content editor provides a rich editing experience with multi-language support, SEO fields, and media management:
┌─────────────────────────────────────────────────┐
│ Create Content │
│ │
│ ┌─────────────────────────────────────────────┐ │
│ │ [English] [Arabic] │ │
│ │ │ │
│ │ Title: [________________________] │ │
│ │ Summary: [________________________] │ │
│ │ Details: ┌─────────────────────────┐ │ │
│ │ │ Rich Text Editor │ │ │
│ │ │ (TinyMCE) │ │ │
│ │ └─────────────────────────┘ │ │
│ │ SEO Title: [_______________________] │ │
│ │ SEO Desc: [_______________________] │ │
│ └─────────────────────────────────────────────┘ │
│ │
│ Slug: [________________________] │
│ Status: [Published v] │
│ Featured: [Upload Image] │
│ │
│ [Save Content] │
└─────────────────────────────────────────────────┘The ContentTranslationsInput component handles per-language tabs for title, summary, details (rich text), button text, and SEO metadata.
Menu Management
The menu builder provides a tree interface for managing navigation menus. Each menu item supports:
| Field | Description |
|---|---|
title | Display text (translatable) |
url | Link destination |
target | _self or _blank |
icon | Lucide icon name |
visibility | public, auth, or guest |
parent_id | For nesting (hierarchical) |
sort_order | Display order |
Menu items can be reordered via drag-and-drop and nested to create multi-level navigation structures.
Settings Page
The settings page displays application settings grouped by category. Each group renders as a card with its fields:
// Settings are organized by group
const groups = [
{ key: "general", label: "General Settings" },
{ key: "auth", label: "Authentication" },
{ key: "email", label: "Email Settings" },
{ key: "storage", label: "Storage Settings" },
];Settings support various input types (text, number, boolean, select) and are saved as key-value pairs via the Settings API.
Audit Logs
The audit log viewer provides a read-only view of system activity with filtering:
| Filter | Options |
|---|---|
| Action | create, update, delete, login, logout |
| Resource | users, roles, contents, settings, etc. |
| User | Filter by acting user |
| Date Range | Start and end date pickers |
Each log entry shows the user who performed the action, the resource affected, the action type, timestamp, and optionally the changed data (before/after diff).
Permission Gating
Permission checks happen at two levels in the admin dashboard:
- Navigation level -- Sidebar items are hidden if the user lacks the required permission
- Page level -- Action buttons (create, edit, delete) are conditionally rendered
// Check a single permission
const canEdit = usePermission("users.edit");
// Use in JSX
{canEdit && (
<Button asChild>
<Link href={`/users/${user.id}/edit`}>Edit</Link>
</Button>
)}The usePermission hook reads from the AuthProvider context, which stores the user's complete permission list after login:
export function usePermission(permission: string): boolean {
const { user } = useAuth();
if (!user) return false;
if (user.is_super_admin) return true;
return user.permissions?.includes(permission) ?? false;
}TIP
Super admins (is_super_admin: true) bypass all permission checks. This is handled at both the frontend (all UI is visible) and backend (middleware skips RBAC) layers.
Authentication Flow
The admin app uses a dedicated AuthProvider that wraps all pages. On mount, it calls /auth/me to verify the current JWT token:
┌──────────┐ ┌──────────────┐ ┌──────────────┐
│ Mount │───>│ Check Token │───>│ /auth/me │
└──────────┘ └──────┬───────┘ └──────┬───────┘
│ │
┌──────┴──────┐ ┌──────┴──────┐
│ No Token │ │ Valid Token │
│ (public │ │ Set user │
│ route? OK) │ │ state │
└──────┬──────┘ └─────────────┘
│
┌──────┴──────┐
│ Protected │
│ route? │
│ Redirect to │
│ /login │
└─────────────┘Theme Support
The admin dashboard supports dark and light themes via ThemeProvider (powered by next-themes). A ThemeToggle button in the header switches between modes. Theme preference is persisted in localStorage.
API Key Management
The API key management page allows administrators to create and revoke API keys for programmatic access:
- Create key -- Set a name, select permissions, and optionally set an expiration date
- View keys -- List all keys with their last used timestamp and status
- Revoke key -- Permanently disable a key (cannot be re-enabled)
WARNING
API key secrets are displayed only once at creation time. After the creation dialog closes, the secret cannot be retrieved again. Advise users to copy the key immediately.
What's Next
- Web Application -- Public-facing app structure
- Components -- DataTable, PermissionGate, and other components
- Forms & Validation -- Form patterns used in admin CRUD pages
- Data Fetching -- API client and data loading patterns