← Назад к вопросам

Как реализована админка на нынешней работе?

2.0 Middle🔥 121 комментариев
#JavaScript Core

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI3 апр. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Как реализована админка на нынешней работе?

Архитектура панели администратора

На текущей работе мы используем модульную архитектуру админки, которая встроена в основное Next.js приложение, но изолирована на отдельных маршрутах. Это позволяет иметь единую кодовую базу, но разные компоненты и логику для админов и обычных пользователей.

Структура админки:

  • Маршруты под /admin/* защищены middleware авторизацией
  • Отдельная папка app/admin/ с компонентами и страницами
  • API endpoints для администраторских операций под /api/admin/*
  • Role-based access control (RBAC) на бэкенде

Основные функции админки:

  • Управление пользователями (создание, редактирование, удаление, блокировка)
  • Управление контентом (вопросы, ответы, модерация)
  • Аналитика и статистика
  • Управление подписками и платежами
  • Логирование и аудит

Технологическая реализация

// Структура папок админки
// app/admin/
// ├── layout.tsx          // Основной layout с sidebar
// ├── page.tsx            // Dashboard
// ├── users/
// │   ├── page.tsx        // Список пользователей
// │   ├── [id]/
// │   │   └── page.tsx    // Редактирование пользователя
// ├── content/
// │   ├── questions/
// │   ├── answers/
// ├── analytics/
// │   └── page.tsx
// └── settings/

// 1. Middleware для защиты админки
// middleware.ts
import { NextRequest, NextResponse } from "next/server";

export async function middleware(request: NextRequest) {
  if (request.nextUrl.pathname.startsWith("/admin")) {
    const token = request.cookies.get("auth_token")?.value;

    if (!token) {
      return NextResponse.redirect(new URL("/login", request.url));
    }

    // Проверяем роль пользователя на бэкенде
    try {
      const response = await fetch("http://localhost:8000/api/v1/auth/me", {
        headers: { Authorization: `Bearer ${token}` }
      });

      if (!response.ok) {
        throw new Error("Unauthorized");
      }

      const user = await response.json();

      // Проверяем что пользователь админ
      if (!["admin", "moderator"].includes(user.role)) {
        return NextResponse.redirect(new URL("/forbidden", request.url));
      }
    } catch (error) {
      return NextResponse.redirect(new URL("/login", request.url));
    }
  }

  return NextResponse.next();
}

export const config = {
  matcher: ["/admin/:path*"]
};

// 2. Компонент Sidebar админки
"use client";

import Link from "next/link";
import { usePathname } from "next/navigation";
import { cn } from "@/lib/utils";

const adminMenuItems = [
  { href: "/admin", label: "Dashboard", icon: "LayoutDashboard" },
  { href: "/admin/users", label: "Users", icon: "Users" },
  { href: "/admin/content", label: "Content", icon: "FileText" },
  { href: "/admin/analytics", label: "Analytics", icon: "BarChart3" },
  { href: "/admin/settings", label: "Settings", icon: "Settings" }
];

export function AdminSidebar() {
  const pathname = usePathname();

  return (
    <aside className="w-64 bg-surface-secondary border-r border-border-default">
      <div className="p-4">
        <h1 className="text-lg font-bold">Admin Panel</h1>
      </div>
      
      <nav className="px-2">
        {adminMenuItems.map((item) => (
          <Link
            key={item.href}
            href={item.href}
            className={cn(
              "block px-4 py-2 rounded-lg mb-2",
              pathname === item.href
                ? "bg-surface-primary text-link"
                : "text-content-secondary hover:bg-surface-tertiary"
            )}
          >
            {item.label}
          </Link>
        ))}
      </nav>
    </aside>
  );
}

// 3. Layout админки
// app/admin/layout.tsx
import { AdminSidebar } from "@/components/admin/Sidebar";

export default function AdminLayout({
  children
}: {
  children: React.ReactNode;
}) {
  return (
    <div className="flex h-screen">
      <AdminSidebar />
      <main className="flex-1 overflow-auto">
        <div className="p-8">
          {children}
        </div>
      </main>
    </div>
  );
}

// 4. Страница управления пользователями
// app/admin/users/page.tsx
"use client";

import { useQuery } from "@tanstack/react-query";
import { useRouter } from "next/navigation";
import { Button } from "@/components/ui/Button";

interface User {
  id: string;
  email: string;
  name: string;
  role: "admin" | "user";
  status: "active" | "blocked";
}

export default function UsersPage() {
  const router = useRouter();
  const { data: users = [], isLoading } = useQuery({
    queryKey: ["admin/users"],
    queryFn: async () => {
      const res = await fetch("/api/admin/users");
      if (!res.ok) throw new Error("Failed to fetch users");
      return res.json();
    }
  });

  const handleBlockUser = async (userId: string) => {
    try {
      const res = await fetch(`/api/admin/users/${userId}/block`, {
        method: "POST"
      });
      if (res.ok) {
        router.refresh();
      }
    } catch (error) {
      console.error("Failed to block user", error);
    }
  };

  if (isLoading) return <div>Loading...</div>;

  return (
    <div>
      <div className="flex justify-between items-center mb-6">
        <h1 className="text-2xl font-bold">Users Management</h1>
        <Button onClick={() => router.push("/admin/users/new")}>
          Add User
        </Button>
      </div>

      <div className="overflow-x-auto">
        <table className="w-full">
          <thead>
            <tr className="border-b border-border-default">
              <th className="text-left p-4">Email</th>
              <th className="text-left p-4">Name</th>
              <th className="text-left p-4">Role</th>
              <th className="text-left p-4">Status</th>
              <th className="text-left p-4">Actions</th>
            </tr>
          </thead>
          <tbody>
            {users.map((user: User) => (
              <tr key={user.id} className="border-b border-border-default">
                <td className="p-4">{user.email}</td>
                <td className="p-4">{user.name}</td>
                <td className="p-4">{user.role}</td>
                <td className="p-4">
                  <span className={user.status === "active" ? "text-green-600" : "text-red-600"}>
                    {user.status}
                  </span>
                </td>
                <td className="p-4 flex gap-2">
                  <Button
                    variant="secondary"
                    onClick={() => router.push(`/admin/users/${user.id}`)}
                  >
                    Edit
                  </Button>
                  <Button
                    variant="destructive"
                    onClick={() => handleBlockUser(user.id)}
                  >
                    Block
                  </Button>
                </td>
              </tr>
            ))}
          </tbody>
        </table>
      </div>
    </div>
  );
}

// 5. API endpoint админки
// app/api/admin/users/route.ts
import { NextRequest, NextResponse } from "next/server";
import { verifyAdminToken } from "@/lib/auth";

export async function GET(request: NextRequest) {
  try {
    // Проверяем авторизацию
    const user = await verifyAdminToken(request);
    
    if (user.role !== "admin") {
      return NextResponse.json(
        { error: "Unauthorized" },
        { status: 403 }
      );
    }

    // Запрашиваем пользователей с бэкенда
    const response = await fetch("http://localhost:8000/api/v1/admin/users", {
      headers: {
        Authorization: `Bearer ${request.cookies.get("api_token")?.value}`
      }
    });

    const users = await response.json();
    return NextResponse.json(users);
  } catch (error) {
    return NextResponse.json(
      { error: "Internal server error" },
      { status: 500 }
    );
  }
}

// 6. Кастомный хук для админских операций
function useAdminAPI() {
  const queryClient = useQueryClient();

  const updateUser = useMutation({
    mutationFn: async (data: { id: string; updates: Record<string, unknown> }) => {
      const res = await fetch(`/api/admin/users/${data.id}`, {
        method: "PATCH",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify(data.updates)
      });
      if (!res.ok) throw new Error("Failed to update user");
      return res.json();
    },
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ["admin/users"] });
    }
  });

  return { updateUser };
}

Ключевые особенности

  1. Разделение доступа — Middleware проверяет авторизацию перед доступом к админке
  2. Role-based контроль — На бэкенде проверяем роль пользователя для каждой операции
  3. Отдельная структура — Админка в отдельной папке, но единая кодовая база
  4. Real-time обновления — React Query для кэширования и синхронизации данных
  5. Аудит операций — Все действия админов логируются на бэкенде