Features

Admin Dashboard

Three-tier admin system with user, team admin, and super admin dashboards

Nuxt Crouton provides a three-tier admin architecture with clear separation of concerns:

TierRoute PatternPurposeAccess
User/dashboard/[team]/*User-facing featuresAny team member
Admin/admin/[team]/*Team managementTeam admins/owners
Super Admin/super-admin/*System managementApp owner only
Separate package:@fyit/crouton-admin is a separate package, but it is automatically included when you install the @fyit/crouton meta-package. You do not need to add it to your extends array manually.

Installation

The admin functionality is included automatically via the @fyit/crouton meta-package:

pnpm add @fyit/crouton
nuxt.config.ts
export default defineNuxtConfig({
  extends: ['@fyit/crouton'],  // Includes auth, admin, i18n

  croutonAuth: {
    teams: {
      allowCreate: true,
      showSwitcher: true
    }
  }
})

Features

Dashboard Stats

View key metrics at a glance:

  • Total users and new signups
  • Banned users count
  • Total teams/organizations
  • Active sessions
  • Super admin count

User Management

Full user lifecycle management:

  • List users with search and filters (status, super admin)
  • Create users with password and role assignment
  • Ban users with reason and optional duration
  • Unban users to restore access
  • Delete users permanently

Team Oversight

View and monitor all teams:

  • List all teams with member counts
  • View team details and members
  • Monitor team activity

User Impersonation

Debug issues by becoming any user:

  • Start impersonation session
  • Visual banner showing impersonation status
  • Stop impersonation to return to admin account

Route Architecture

User Tier (/dashboard/[team]/*)

Routes for regular team members:

PagePathPurpose
Team Selection/dashboardSelect which team to access
User Dashboard/dashboard/[team]User home/overview
Profile/dashboard/[team]/profileMy profile settings
Settings/dashboard/[team]/settingsMy preferences
App Routes/dashboard/[team]/{app}Auto-discovered from apps

Admin Tier (/admin/[team]/*)

Routes for team administrators:

PagePathPurpose
Admin Dashboard/admin/[team]Team admin overview
Collections/admin/[team]/collectionsCRUD collection management
Members/admin/[team]/team/Team member management
Invitations/admin/[team]/team/invitationsPending invitations
Settings/admin/[team]/team/settingsTeam settings
Domains/admin/[team]/team/domainsCustom domain management
Look & Feel/admin/[team]/team/look-and-feelBranding and appearance
App Admin Routes/admin/[team]/{app}Auto-discovered from apps

Protected by team-admin middleware.

Super Admin Tier (/super-admin/*)

Routes for the app owner (system-wide management):

PagePathPurpose
Dashboard/super-adminStats overview and quick actions
Users/super-admin/usersUser management
Teams/super-admin/teamsTeam oversight

Protected by super-admin middleware.

Middleware

Two middleware options for different access levels:

team-admin

Requires user to be a team admin or owner.

pages/admin/[team]/custom.vue
<script setup lang="ts">
definePageMeta({
  middleware: ['auth', 'team-admin']
})
</script>

super-admin

Requires user to be the app owner (super admin). Redirects non-admins to the home page.

pages/super-admin/custom.vue
<script setup lang="ts">
definePageMeta({
  middleware: 'super-admin'
})
</script>

<template>
  <div>Custom super admin page content</div>
</template>

App Auto-Discovery

Apps can register their routes to appear automatically in sidebars using croutonApps:

Registering App Routes

app/app.config.ts
export default defineAppConfig({
  croutonApps: {
    bookings: {
      id: 'bookings',
      name: 'Bookings',
      icon: 'i-lucide-calendar',

      // User-facing routes (appear in /dashboard/[team]/ sidebar)
      dashboardRoutes: [
        {
          path: 'bookings',
          label: 'bookings.myBookings.title',  // Translation key
          icon: 'i-lucide-calendar'
        }
      ],

      // Admin routes (appear in /admin/[team]/ sidebar)
      adminRoutes: [
        {
          path: 'bookings',
          label: 'bookings.admin.title',
          icon: 'i-lucide-calendar'
        }
      ],

      // Settings pages (appear in /admin/[team]/settings/)
      settingsRoutes: [
        {
          path: 'email-templates',
          label: 'bookings.settings.emailTemplates',
          icon: 'i-lucide-mail'
        }
      ]
    }
  }
})

Using useCroutonApps

const {
  apps,              // All registered apps
  dashboardRoutes,   // All user-facing routes
  adminRoutes,       // All admin routes
  settingsRoutes,    // All settings routes
  getApp,            // Get app by ID
  hasApp             // Check if app exists
} = useCroutonApps()

// Build navigation
const navItems = dashboardRoutes.value.map(route => ({
  label: t(route.label),
  icon: route.icon,
  to: `/dashboard/${teamSlug}/${route.path}`
}))

Route Types

interface CroutonAppRoute {
  path: string          // Route path segment
  label: string         // Translation key
  icon?: string         // Heroicon name
  hidden?: boolean      // Hide from sidebar
  children?: CroutonAppRoute[]
}

interface CroutonAppConfig {
  id: string
  name: string
  icon?: string
  dashboardRoutes?: CroutonAppRoute[]
  adminRoutes?: CroutonAppRoute[]
  settingsRoutes?: CroutonAppRoute[]
}

Composables

useAdminUsers()

User management operations.

const {
  users,           // List of users
  total,           // Total count
  page,            // Current page
  loading,         // Loading state
  error,           // Error message

  getUsers,        // Fetch users with filters
  getUser,         // Get user detail
  createUser,      // Create new user
  banUser,         // Ban a user
  unbanUser,       // Unban a user
  deleteUser       // Delete user permanently
} = useAdminUsers()

// Fetch users with filters
await getUsers({
  page: 1,
  pageSize: 20,
  search: 'john',
  status: 'active',     // 'all' | 'active' | 'banned'
  superAdmin: false,
  sortBy: 'createdAt',
  sortOrder: 'desc'
})

// Ban a user
await banUser('user-id', {
  reason: 'Spam activity',
  duration: 168  // hours (null = permanent)
})

useAdminTeams()

Team management operations.

const {
  teams,           // List of teams
  total,           // Total count
  loading,         // Loading state
  error,           // Error message

  getTeams,        // Fetch teams with filters
  getTeam          // Get team detail with members
} = useAdminTeams()

// Fetch teams
await getTeams({
  page: 1,
  pageSize: 20,
  search: 'acme',
  personal: false,
  sortBy: 'memberCount',
  sortOrder: 'desc'
})

useAdminStats()

Dashboard statistics with optional auto-refresh.

const {
  stats,           // AdminStats object
  loading,         // Loading state
  error,           // Error message

  refresh,         // Fetch/refresh stats
  startAutoRefresh,// Start auto-refresh
  stopAutoRefresh  // Stop auto-refresh
} = useAdminStats({ autoRefresh: true, refreshInterval: 30000 })

// Stats include:
// - totalUsers, newUsersToday, newUsersWeek
// - bannedUsers, superAdminCount
// - totalTeams, newTeamsWeek
// - activeSessions

useImpersonation()

User impersonation for debugging.

const {
  isImpersonating,     // Boolean: currently impersonating
  impersonatedUser,    // User being impersonated
  originalAdminId,     // Original admin's ID
  loading,             // Loading state

  startImpersonation,  // Start impersonating a user
  stopImpersonation,   // Return to admin account
  checkStatus          // Check current status
} = useImpersonation()

// Start impersonating
await startImpersonation('user-id')

// Stop impersonating
await stopImpersonation()

Components

Dashboard Components

ComponentPurpose
AdminDashboardFull dashboard with stats and quick actions
AdminStatsCardIndividual stat card with icon and trend
<template>
  <AdminDashboard
    :stats="preloadedStats"
    :show-quick-actions="true"
    @navigate="handleNavigation"
  />
</template>

User Components

ComponentPurpose
AdminUserListPaginated user table with search and filters
AdminUserActionsDropdown menu for user actions
AdminUserBanFormForm for banning with reason and duration
AdminUserCreateFormForm for creating new users
<template>
  <AdminUserList
    :page-size="20"
    @user-selected="handleSelect"
  />
</template>

Global Components

ComponentPurpose
ImpersonationBannerTop banner when impersonating a user

Add to your app layout to show impersonation status:

layouts/default.vue
<template>
  <div>
    <ImpersonationBanner />
    <slot />
  </div>
</template>

API Endpoints

Users

MethodEndpointPurpose
GET/api/admin/usersList users with pagination
GET/api/admin/users/[id]Get user detail
POST/api/admin/users/createCreate new user
POST/api/admin/users/banBan a user
POST/api/admin/users/unbanUnban a user
DELETE/api/admin/users/deleteDelete a user

Teams

MethodEndpointPurpose
GET/api/admin/teamsList teams
GET/api/admin/teams/[id]Get team with members

Stats

MethodEndpointPurpose
GET/api/admin/statsGet dashboard statistics

Impersonation

MethodEndpointPurpose
POST/api/admin/impersonate/startStart impersonation
POST/api/admin/impersonate/stopStop impersonation
GET/api/admin/impersonate/statusCheck impersonation status

Server Utilities

requireSuperAdmin(event)

Server-side authorization check. Use in custom admin endpoints.

server/api/admin/custom.ts
export default defineEventHandler(async (event) => {
  // Auto-imported server utility — no import needed
  // Throws 403 if not super admin
  const { user: adminUser } = await requireSuperAdmin(event)

  // Your admin logic here
  return { admin: adminUser.name }
})

Making a User Super Admin

Super admin privileges are stored in the superAdmin field on the user table.

Via Database Migration

scripts/make-admin.ts
import { db } from '@fyit/crouton-auth'
import { user } from '@fyit/crouton-auth/server/database/schema/auth'
import { eq } from 'drizzle-orm'

await db.update(user)
  .set({ superAdmin: true })
  .where(eq(user.email, 'admin@example.com'))

Via Seed Script

scripts/seed.ts
import { db } from '@fyit/crouton-auth'
import { user } from '@fyit/crouton-auth/server/database/schema/auth'

await db.insert(user).values({
  id: 'admin-id',
  email: 'admin@example.com',
  name: 'Admin User',
  emailVerified: true,
  superAdmin: true
})

Configuration

nuxt.config.ts
export default defineNuxtConfig({
  runtimeConfig: {
    public: {
      crouton: {
        admin: {
          // Super admin route prefix (default: /super-admin)
          routePrefix: '/super-admin',

          // Enable impersonation feature
          impersonation: true,

          // Stats auto-refresh interval (ms)
          statsRefreshInterval: 30000
        }
      }
    }
  }
})

Ban Durations

Pre-defined ban duration options:

DurationHours
1 hour1
24 hours24
7 days168
30 days720
90 days2160
Permanentnull

Customization

Override Pages

Create your own page at the same path to override:

pages/super-admin/index.vue
<script setup lang="ts">
definePageMeta({ middleware: 'super-admin' })
</script>

<template>
  <div>
    <h1>My Custom Super Admin Dashboard</h1>
    <!-- Your custom content -->
  </div>
</template>

Override Impersonation Banner

Create your own ImpersonationBanner.vue:

components/ImpersonationBanner.vue
<script setup lang="ts">
const { isImpersonating, impersonatedUser, stopImpersonation } = useImpersonation()
</script>

<template>
  <div v-if="isImpersonating" class="my-custom-banner">
    Impersonating: {{ impersonatedUser?.name }}
    <button @click="stopImpersonation">Stop</button>
  </div>
</template>

Types

AdminStats

interface AdminStats {
  totalUsers: number
  newUsersToday: number
  newUsersWeek: number
  bannedUsers: number
  totalTeams: number
  newTeamsWeek: number
  activeSessions: number
  superAdminCount: number
}

BanPayload

interface BanPayload {
  userId: string
  reason: string
  duration: number | null  // hours, null = permanent
}

ImpersonationState

interface ImpersonationState {
  isImpersonating: boolean
  originalAdminId: string | null
  impersonatedUser: {
    id: string
    name: string
    email: string
  } | null
}

Security Considerations

  1. Super admin access is required for all admin endpoints
  2. Cannot ban other super admins - protects against admin lockout
  3. Cannot ban yourself - prevents self-lockout
  4. Impersonation is logged - original admin ID stored in session
  5. Sessions are invalidated when a user is banned