Inline filtering results, select-all across pages, country flags, settings RBAC, and inline role changes
- Round detail: add skeleton loading for filtering stats, inline results table with expandable rows, pagination, override/reinstate, CSV export, and tooltip on AI summaries button (removes need for separate results page) - Projects: add select-all-across-pages with Gmail-style banner, show country flags with tooltip instead of country codes (table + card views), add listAllIds backend endpoint - Settings: allow PROGRAM_ADMIN access to settings page, restrict infrastructure tabs (AI, Email, Storage, Security, Webhooks) to SUPER_ADMIN only - Members: add inline role change via dropdown submenu in user actions, enforce role hierarchy (only super admins can modify admin/super-admin roles) in both backend and UI Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -8,15 +8,22 @@ import { Card, CardContent, CardHeader } from '@/components/ui/card'
|
||||
import { Skeleton } from '@/components/ui/skeleton'
|
||||
import { SettingsContent } from '@/components/settings/settings-content'
|
||||
|
||||
async function SettingsLoader() {
|
||||
// Categories that only super admins can access
|
||||
const SUPER_ADMIN_CATEGORIES = new Set(['AI', 'EMAIL', 'STORAGE', 'SECURITY'])
|
||||
|
||||
async function SettingsLoader({ isSuperAdmin }: { isSuperAdmin: boolean }) {
|
||||
const settings = await prisma.systemSettings.findMany({
|
||||
orderBy: [{ category: 'asc' }, { key: 'asc' }],
|
||||
})
|
||||
|
||||
// Convert settings array to key-value map
|
||||
// For secrets, pass a marker but not the actual value
|
||||
// For non-super-admins, filter out infrastructure categories
|
||||
const settingsMap: Record<string, string> = {}
|
||||
settings.forEach((setting) => {
|
||||
if (!isSuperAdmin && SUPER_ADMIN_CATEGORIES.has(setting.category)) {
|
||||
return
|
||||
}
|
||||
if (setting.isSecret && setting.value) {
|
||||
// Pass marker for UI to show "existing" state
|
||||
settingsMap[setting.key] = '********'
|
||||
@@ -25,7 +32,7 @@ async function SettingsLoader() {
|
||||
}
|
||||
})
|
||||
|
||||
return <SettingsContent initialSettings={settingsMap} />
|
||||
return <SettingsContent initialSettings={settingsMap} isSuperAdmin={isSuperAdmin} />
|
||||
}
|
||||
|
||||
function SettingsSkeleton() {
|
||||
@@ -52,11 +59,13 @@ function SettingsSkeleton() {
|
||||
export default async function SettingsPage() {
|
||||
const session = await auth()
|
||||
|
||||
// Only super admins can access settings
|
||||
if (session?.user?.role !== 'SUPER_ADMIN') {
|
||||
// Only admins (super admin + program admin) can access settings
|
||||
if (session?.user?.role !== 'SUPER_ADMIN' && session?.user?.role !== 'PROGRAM_ADMIN') {
|
||||
redirect('/admin')
|
||||
}
|
||||
|
||||
const isSuperAdmin = session?.user?.role === 'SUPER_ADMIN'
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Header */}
|
||||
@@ -69,7 +78,7 @@ export default async function SettingsPage() {
|
||||
|
||||
{/* Content */}
|
||||
<Suspense fallback={<SettingsSkeleton />}>
|
||||
<SettingsLoader />
|
||||
<SettingsLoader isSuperAdmin={isSuperAdmin} />
|
||||
</Suspense>
|
||||
</div>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user