Реализация Admin Dashboard для SaaS-приложения

Наша компания занимается разработкой, поддержкой и обслуживанием сайтов любой сложности. От простых одностраничных сайтов до масштабных кластерных систем построенных на микро сервисах. Опыт разработчиков подтвержден сертификатами от вендоров.
Разработка и обслуживание любых видов сайтов:
Информационные сайты или веб-приложения
Сайты визитки, landing page, корпоративные сайты, онлайн каталоги, квиз, промо-сайты, блоги, новостные ресурсы, информационные порталы, форумы, агрегаторы
Сайты или веб-приложения электронной коммерции
Интернет-магазины, B2B-порталы, маркетплейсы, онлайн-обменники, кэшбэк-сайты, биржи, дропшиппинг-платформы, парсеры товаров
Веб-приложения для управления бизнес-процессами
CRM-системы, ERP-системы, корпоративные порталы, системы управления производством, парсеры информации
Сайты или веб-приложения электронных услуг
Доски объявлений, онлайн-школы, онлайн-кинотеатры, конструкторы сайтов, порталы предоставления электронных услуг, видеохостинги, тематические порталы

Это лишь некоторые из технических типов сайтов, с которыми мы работаем, и каждый из них может иметь свои специфические особенности и функциональность, а также быть адаптированным под конкретные потребности и цели клиента

Предлагаемые услуги
Показано 1 из 1 услугВсе 2065 услуг
Реализация Admin Dashboard для SaaS-приложения
Сложная
~2-4 недели
Часто задаваемые вопросы
Наши компетенции:
Этапы разработки
Последние работы
  • image_website-b2b-advance_0.png
    Разработка сайта компании B2B ADVANCE
    1214
  • image_web-applications_feedme_466_0.webp
    Разработка веб-приложения для компании FEEDME
    1161
  • image_websites_belfingroup_462_0.webp
    Разработка веб-сайта для компании БЕЛФИНГРУПП
    852
  • image_ecommerce_furnoro_435_0.webp
    Разработка интернет магазина для компании FURNORO
    1041
  • image_crm_enviok_479_0.webp
    Разработка веб-приложения для компании Enviok
    823
  • image_bitrix-bitrix-24-1c_fixper_448_0.png
    Разработка веб-сайта для компании ФИКСПЕР
    815

SaaS admin-дашборд

Admin-дашборд — внутренний инструмент команды: управление тенантами, пользователями, биллингом, просмотр метрик. Не путать с дашбордом клиента.

Архитектура: отдельный роут с жёсткой авторизацией

// middleware.ts
export async function middleware(request: NextRequest) {
  const pathname = request.nextUrl.pathname;

  if (pathname.startsWith('/admin')) {
    const session = await getServerSession(authOptions);

    if (!session || session.user.role !== 'SUPER_ADMIN') {
      return NextResponse.redirect(new URL('/login', request.url));
    }

    // Дополнительно: IP whitelist для admin
    const clientIp = request.headers.get('x-forwarded-for');
    const allowedIps = process.env.ADMIN_ALLOWED_IPS?.split(',') ?? [];

    if (allowedIps.length > 0 && !allowedIps.includes(clientIp ?? '')) {
      return new NextResponse('Forbidden', { status: 403 });
    }
  }
}

Метрики бизнеса

// app/admin/page.tsx
export default async function AdminDashboard() {
  const now = new Date();
  const thirtyDaysAgo = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000);

  const [
    totalTenants,
    activeTenants,
    newTenants30d,
    mrr,
    churnedTenants30d,
    totalUsers,
    recentErrors,
  ] = await Promise.all([
    db.tenant.count(),
    db.tenant.count({ where: { status: 'ACTIVE' } }),
    db.tenant.count({
      where: { createdAt: { gte: thirtyDaysAgo }, status: 'ACTIVE' }
    }),
    calculateMRR(),
    db.subscription.count({
      where: { status: 'CANCELED', canceledAt: { gte: thirtyDaysAgo } }
    }),
    db.user.count(),
    getRecentErrors(),
  ]);

  const churnRate = activeTenants > 0
    ? ((churnedTenants30d / activeTenants) * 100).toFixed(1)
    : '0';

  return (
    <AdminLayout>
      <MetricGrid>
        <MetricCard title="Активные тенанты" value={activeTenants} />
        <MetricCard title="Новые (30 дней)" value={newTenants30d} />
        <MetricCard title="MRR" value={`$${(mrr / 100).toFixed(0)}`} />
        <MetricCard title="Churn rate" value={`${churnRate}%`} variant={parseFloat(churnRate) > 5 ? 'danger' : 'normal'} />
      </MetricGrid>
      <RecentErrorsWidget errors={recentErrors} />
    </AdminLayout>
  );
}

async function calculateMRR(): Promise<number> {
  const subscriptions = await db.subscription.findMany({
    where: { status: 'ACTIVE' },
    select: { stripePriceId: true }
  });

  let mrr = 0;
  for (const sub of subscriptions) {
    const price = await stripe.prices.retrieve(sub.stripePriceId!);
    const monthly = price.recurring?.interval === 'year'
      ? price.unit_amount! / 12
      : price.unit_amount!;
    mrr += monthly;
  }

  return mrr;
}

Управление тенантами

// app/admin/tenants/page.tsx
export default async function TenantsAdminPage({
  searchParams
}: {
  searchParams: { q?: string; plan?: string; status?: string; page?: string }
}) {
  const page = parseInt(searchParams.page ?? '1');
  const pageSize = 25;

  const tenants = await db.tenant.findMany({
    where: {
      ...(searchParams.q ? {
        OR: [
          { slug: { contains: searchParams.q, mode: 'insensitive' } },
          { name: { contains: searchParams.q, mode: 'insensitive' } },
        ]
      } : {}),
      ...(searchParams.plan ? { plan: searchParams.plan as Plan } : {}),
      ...(searchParams.status ? { status: searchParams.status as TenantStatus } : {}),
    },
    include: {
      subscription: true,
      _count: { select: { users: true } }
    },
    orderBy: { createdAt: 'desc' },
    skip: (page - 1) * pageSize,
    take: pageSize,
  });

  return (
    <div>
      <TenantFilters />
      <TenantTable tenants={tenants} />
      <Pagination page={page} pageSize={pageSize} />
    </div>
  );
}

Импersonation: вход под тенантом

// admin может войти под любым пользователем для диагностики
export async function impersonateTenant(tenantId: string) {
  'use server';

  const adminSession = await auth();
  if (adminSession?.user.role !== 'SUPER_ADMIN') {
    throw new Error('Unauthorized');
  }

  // Логируем действие
  await db.adminAuditLog.create({
    data: {
      adminId: adminSession.user.id,
      action: 'IMPERSONATE_TENANT',
      targetId: tenantId,
      metadata: { reason: 'admin_requested' },
    }
  });

  const tenant = await db.tenant.findUniqueOrThrow({
    where: { id: tenantId },
    include: { users: { take: 1, orderBy: { role: 'asc' } } }
  });

  // Устанавливаем impersonation cookie
  const cookieStore = cookies();
  cookieStore.set('impersonate_tenant_id', tenantId, {
    httpOnly: true,
    secure: true,
    sameSite: 'strict',
    maxAge: 60 * 60, // 1 час
  });

  redirect(`https://${tenant.slug}.${process.env.ROOT_DOMAIN}/dashboard`);
}

Аудит-лог

model AdminAuditLog {
  id        String   @id @default(cuid())
  adminId   String
  action    String   // 'IMPERSONATE_TENANT' | 'CANCEL_SUBSCRIPTION' | 'REFUND' | ...
  targetId  String?  // ID тенанта, пользователя или инвойса
  metadata  Json?
  ip        String?
  createdAt DateTime @default(now())

  admin User @relation(fields: [adminId], references: [id])
}
// Все admin-действия логируются
export async function cancelTenantSubscription(tenantId: string, reason: string) {
  const session = await auth();

  await db.adminAuditLog.create({
    data: {
      adminId: session!.user.id,
      action: 'CANCEL_SUBSCRIPTION',
      targetId: tenantId,
      metadata: { reason },
    }
  });

  const subscription = await db.subscription.findUnique({ where: { tenantId } });
  await stripe.subscriptions.cancel(subscription!.stripeSubscriptionId!);
}

Разработка admin-дашборда с метриками, управлением тенантами и аудит-логом — 5–8 рабочих дней.