Реалізація Admin Dashboard для SaaS-застосунку

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

Це лише деякі з технічних типів сайтів, з якими ми працюємо, і кожен із них може мати свої специфічні особливості та функціональність, а також бути адаптованим під конкретні потреби та цілі клієнта.

Пропоновані послуги
Показано 1 з 1 послугУсі 2065 послуг
Реалізація Admin Dashboard для SaaS-застосунку
Складна
~2-4 тижні
Часті питання
Наші компетенції:
Етапи розробки
Останні роботи
  • image_website-b2b-advance_0.png
    Розробка сайту компанії B2B ADVANCE
    1262
  • image_web-applications_feedme_466_0.webp
    Розробка веб-додатків для компанії FEEDME
    1171
  • image_websites_belfingroup_462_0.webp
    Розробка веб-сайту для компанії БЕЛФІНГРУП
    874
  • image_ecommerce_furnoro_435_0.webp
    Розробка інтернет магазину для компанії FURNORO
    1094
  • image_crm_enviok_479_0.webp
    Розробка веб-додатків для компанії Enviok
    831
  • image_bitrix-bitrix-24-1c_fixper_448_0.png
    Розробка веб-сайту для компанії ФІКСПЕР
    851

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>
  );
}

Impersonation: вхід під тенантом

// 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 робочих днів.