Unify workspace dropdowns; Update layouts (#458)

Unified Account and Workspace drop-downs; Layout updates, now header lives within the PageBody component; Sidebars now use floating variant
This commit is contained in:
Giancarlo Buomprisco
2026-03-11 14:45:42 +08:00
committed by GitHub
parent ca585e09be
commit 4bc8448a1d
530 changed files with 14398 additions and 11198 deletions

View File

@@ -0,0 +1,56 @@
# Super Admin
## Critical Security Rules
- **ALWAYS** use `AdminGuard` to protect pages
- **ALWAYS** validate admin status before operations
- **NEVER** bypass authentication or authorization
- **ALWAYS** audit admin operations with logging
- **ALWAYS** use `adminAction` to wrap admin actions @packages/features/admin/src/lib/server/utils/admin-action.ts
## Page Structure
```typescript
import { AdminGuard } from '@kit/admin/components/admin-guard';
import { PageBody, PageHeader } from '@kit/ui/page';
async function AdminPage() {
return (
<>
<PageHeader title="Admin" />
<PageBody>{/* Content */}</PageBody>
</>
);
}
export default AdminGuard(AdminPage);
```
## Admin Client Usage
```typescript
import { isSuperAdmin } from '@kit/admin';
import { getSupabaseServerAdminClient } from '@kit/supabase/server-admin-client';
async function adminOperation() {
// CRITICAL: Validate first - admin client bypasses RLS
if (!(await isSuperAdmin(currentUser))) {
throw new Error('Unauthorized');
}
const adminClient = getSupabaseServerAdminClient();
// Safe to proceed
}
```
## Audit Logging
```typescript
const logger = await getLogger();
logger.info({
name: 'admin-audit',
action: 'delete-user',
adminId: currentUser.id,
targetId: userId,
}, 'Admin action performed');
```

View File

@@ -0,0 +1 @@
@AGENTS.md

View File

@@ -0,0 +1,68 @@
'use client';
import Link from 'next/link';
import { usePathname } from 'next/navigation';
import { LayoutDashboard, Users } from 'lucide-react';
import {
Sidebar,
SidebarContent,
SidebarFooter,
SidebarGroup,
SidebarGroupContent,
SidebarGroupLabel,
SidebarHeader,
SidebarMenu,
SidebarMenuButton,
} from '@kit/ui/sidebar';
import { AppLogo } from '~/components/app-logo';
import { ProfileAccountDropdownContainer } from '~/components/personal-account-dropdown-container';
export function AdminSidebar() {
const path = usePathname();
return (
<Sidebar collapsible="icon">
<SidebarHeader className={'m-2'}>
<AppLogo href={'/admin'} className="max-w-full" />
</SidebarHeader>
<SidebarContent>
<SidebarGroup>
<SidebarGroupLabel>Super Admin</SidebarGroupLabel>
<SidebarGroupContent>
<SidebarMenu>
<SidebarMenuButton
isActive={path === '/admin'}
render={<Link className={'flex gap-2.5'} href={'/admin'} />}
>
<LayoutDashboard className={'h-4'} />
<span>Dashboard</span>
</SidebarMenuButton>
<SidebarMenuButton
isActive={path.includes('/admin/accounts')}
render={
<Link
className={'flex size-full gap-2.5'}
href={'/admin/accounts'}
>
<Users className={'h-4'} />
<span>Accounts</span>
</Link>
}
/>
</SidebarMenu>
</SidebarGroupContent>
</SidebarGroup>
</SidebarContent>
<SidebarFooter>
<ProfileAccountDropdownContainer />
</SidebarFooter>
</Sidebar>
);
}

View File

@@ -0,0 +1,30 @@
import Link from 'next/link';
import { Menu } from 'lucide-react';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from '@kit/ui/dropdown-menu';
export function AdminMobileNavigation() {
return (
<DropdownMenu>
<DropdownMenuTrigger>
<Menu className={'h-8 w-8'} />
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem>
<Link href={'/admin'}>Home</Link>
</DropdownMenuItem>
<DropdownMenuItem>
<Link href={'/admin/accounts'}>Accounts</Link>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
);
}

View File

@@ -0,0 +1,47 @@
import { cache } from 'react';
import { AdminAccountPage } from '@kit/admin/components/admin-account-page';
import { AdminGuard } from '@kit/admin/components/admin-guard';
import { getSupabaseServerClient } from '@kit/supabase/server-client';
interface Params {
params: Promise<{
id: string;
}>;
}
export const generateMetadata = async (props: Params) => {
const params = await props.params;
const account = await loadAccount(params.id);
return {
title: `Admin | ${account.name}`,
};
};
async function AccountPage(props: Params) {
const params = await props.params;
const account = await loadAccount(params.id);
return <AdminAccountPage account={account} />;
}
export default AdminGuard(AccountPage);
const loadAccount = cache(accountLoader);
async function accountLoader(id: string) {
const client = getSupabaseServerClient();
const { data, error } = await client
.from('accounts')
.select('*, memberships: accounts_memberships (*)')
.eq('id', id)
.single();
if (error) {
throw error;
}
return data;
}

View File

@@ -0,0 +1,3 @@
import { GlobalLoader } from '@kit/ui/global-loader';
export default GlobalLoader;

View File

@@ -0,0 +1,79 @@
import { ServerDataLoader } from '@makerkit/data-loader-supabase-nextjs';
import { AdminAccountsTable } from '@kit/admin/components/admin-accounts-table';
import { AdminCreateUserDialog } from '@kit/admin/components/admin-create-user-dialog';
import { AdminGuard } from '@kit/admin/components/admin-guard';
import { getSupabaseServerClient } from '@kit/supabase/server-client';
import { AppBreadcrumbs } from '@kit/ui/app-breadcrumbs';
import { Button } from '@kit/ui/button';
import { PageBody, PageHeader } from '@kit/ui/page';
interface SearchParams {
page?: string;
account_type?: 'all' | 'team' | 'personal';
query?: string;
}
interface AdminAccountsPageProps {
searchParams: Promise<SearchParams>;
}
export const metadata = {
title: `Accounts`,
};
async function AccountsPage(props: AdminAccountsPageProps) {
const client = getSupabaseServerClient();
const searchParams = await props.searchParams;
const page = searchParams.page ? parseInt(searchParams.page) : 1;
return (
<>
<PageHeader description={<AppBreadcrumbs />}>
<div className="flex justify-end">
<AdminCreateUserDialog>
<Button data-test="admin-create-user-button">Create User</Button>
</AdminCreateUserDialog>
</div>
</PageHeader>
<PageBody>
<ServerDataLoader
table={'accounts'}
client={client}
page={page}
where={(queryBuilder) => {
const { account_type: type, query } = searchParams;
if (type && type !== 'all') {
queryBuilder.eq('is_personal_account', type === 'personal');
}
if (query) {
queryBuilder.or(`name.ilike.%${query}%,email.ilike.%${query}%`);
}
return queryBuilder;
}}
>
{({ data, page, pageSize, pageCount }) => {
return (
<AdminAccountsTable
page={page}
pageSize={pageSize}
pageCount={pageCount}
data={data}
filters={{
type: searchParams.account_type ?? 'all',
query: searchParams.query ?? '',
}}
/>
);
}}
</ServerDataLoader>
</PageBody>
</>
);
}
export default AdminGuard(AccountsPage);

View File

@@ -0,0 +1,44 @@
import { use } from 'react';
import { cookies } from 'next/headers';
import { Page, PageMobileNavigation, PageNavigation } from '@kit/ui/page';
import { SidebarProvider } from '@kit/ui/sidebar';
import { AdminSidebar } from '~/admin/_components/admin-sidebar';
import { AdminMobileNavigation } from '~/admin/_components/mobile-navigation';
export const metadata = {
title: `Super Admin`,
};
export const dynamic = 'force-dynamic';
export default function AdminLayout(props: React.PropsWithChildren) {
const state = use(getLayoutState());
return (
<SidebarProvider defaultOpen={state.open}>
<Page style={'sidebar'}>
<PageNavigation>
<AdminSidebar />
</PageNavigation>
<PageMobileNavigation>
<AdminMobileNavigation />
</PageMobileNavigation>
{props.children}
</Page>
</SidebarProvider>
);
}
async function getLayoutState() {
const cookieStore = await cookies();
const sidebarOpenCookie = cookieStore.get('sidebar:state');
return {
open: sidebarOpenCookie?.value !== 'true',
};
}

View File

@@ -0,0 +1,17 @@
import { AdminDashboard } from '@kit/admin/components/admin-dashboard';
import { AdminGuard } from '@kit/admin/components/admin-guard';
import { PageBody, PageHeader } from '@kit/ui/page';
function AdminPage() {
return (
<>
<PageHeader description={`Super Admin`} />
<PageBody>
<AdminDashboard />
</PageBody>
</>
);
}
export default AdminGuard(AdminPage);