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

Делится ли в приложении функционал по ролям на нынешней работе

1.2 Junior🔥 121 комментариев
#JavaScript Core

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

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

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

Разделение функционала по ролям пользователя

В современных веб-приложениях разделение функционала по ролям пользователя (Role-Based Access Control, RBAC) - это стандартная практика. Рассмотрю, как это реализуется на фронтенде и бэкенде.

Типичная архитектура разделения ролей

В большинстве приложений есть несколько уровней пользователей:

// Типы ролей
enum UserRole {
  ADMIN = "admin",
  MODERATOR = "moderator",
  USER = "user",
  GUEST = "guest"
}

interface User {
  id: string;
  name: string;
  email: string;
  role: UserRole;
  permissions: Permission[];
}

enum Permission {
  CREATE_POST = "create_post",
  EDIT_POST = "edit_post",
  DELETE_POST = "delete_post",
  MANAGE_USERS = "manage_users",
  VIEW_ANALYTICS = "view_analytics"
}

Способ 1: Условный рендеринг на фронтенде

Простейший подход - показывать/скрывать элементы в зависимости от роли:

// React пример
function Dashboard({ user }: { user: User }) {
  return (
    <div>
      {/* Видно всем */}
      <h1>Доброе пожаловать, {user.name}!</h1>

      {/* Только для модераторов и админов */}
      {(user.role === UserRole.MODERATOR ||
        user.role === UserRole.ADMIN) && (
        <section>
          <h2>Модерация</h2>
          <ModerationPanel />
        </section>
      )}

      {/* Только для админов */}
      {user.role === UserRole.ADMIN && (
        <section>
          <h2>Администрирование</h2>
          <AdminPanel />
        </section>
      )}

      {/* Только для обычных пользователей */}
      {user.role === UserRole.USER && (
        <section>
          <h2>Мои постройки</h2>
          <UserPosts />
        </section>
      )}
    </div>
  );
}

Проблемы:

  • Небезопасно - пользователь может открыть скрытый раздел через DevTools
  • Код становится нечитаемым с множеством проверок
  • Сложно масштабировать

Способ 2: Утилита для проверки прав (useAuth хук)

Лучше создать переиспользуемый хук:

const AuthContext = React.createContext<{
  user: User | null;
  login: (email: string, password: string) => Promise<void>;
  logout: () => void;
} | null>(null);

export function useAuth() {
  const context = React.useContext(AuthContext);
  if (!context) {
    throw new Error("useAuth должен быть внутри AuthProvider");
  }
  return context;
}

export function usePermission() {
  const { user } = useAuth();

  const hasPermission = (permission: Permission): boolean => {
    if (!user) return false;

    // Админы имеют все права
    if (user.role === UserRole.ADMIN) return true;

    // Проверяем явные права
    return user.permissions.includes(permission);
  };

  const hasRole = (role: UserRole): boolean => {
    return user?.role === role;
  };

  const hasAnyRole = (roles: UserRole[]): boolean => {
    return user ? roles.includes(user.role) : false;
  };

  return { hasPermission, hasRole, hasAnyRole };
}

Способ 3: Компонент для условного рендеринга

interface ProtectedProps {
  children: React.ReactNode;
  permission?: Permission;
  role?: UserRole;
  fallback?: React.ReactNode;
}

function ProtectedSection({
  children,
  permission,
  role,
  fallback = null
}: ProtectedProps) {
  const { hasPermission, hasRole } = usePermission();

  let hasAccess = true;

  if (permission && !hasPermission(permission)) {
    hasAccess = false;
  }

  if (role && !hasRole(role)) {
    hasAccess = false;
  }

  return hasAccess ? children : fallback;
}

// Использование
function Dashboard() {
  return (
    <div>
      <ProtectedSection>
        <UserDashboard />
      </ProtectedSection>

      <ProtectedSection permission={Permission.MANAGE_USERS}>
        <AdminPanel />
      </ProtectedSection>

      <ProtectedSection role={UserRole.ADMIN}>
        <AdminOnlyFeature />
      </ProtectedSection>

      <ProtectedSection
        role={UserRole.ADMIN}
        fallback={<p>У вас нет доступа</p>}
      >
        <SecretFeature />
      </ProtectedSection>
    </div>
  );
}

Способ 4: Защита маршрутов

// React Router
const ProtectedRoute = ({
  component: Component,
  requiredRole,
  ...rest
}: any) => {
  const { user } = useAuth();

  return (
    <Route
      {...rest}
      element={
        user && user.role === requiredRole ? (
          <Component />
        ) : (
          <Navigate to="/login" />
        )
      }
    />
  );
};

// Использование
function Router() {
  return (
    <Routes>
      <Route path="/" element={<Home />} />
      <Route path="/profile" element={<Profile />} />
      <ProtectedRoute
        path="/admin"
        component={AdminPanel}
        requiredRole={UserRole.ADMIN}
      />
      <ProtectedRoute
        path="/moderator"
        component={ModerationPanel}
        requiredRole={UserRole.MODERATOR}
      />
    </Routes>
  );
}

Способ 5: Проверка прав в API запросах

Большинство проверок должны быть на бэкенде:

class ApiService {
  constructor(private http: HttpClient) {}

  // Обычные запросы
  getPublicData() {
    return this.http.get("/api/public/data");
  }

  // Защищённые запросы
  getUserData() {
    return this.http.get("/api/protected/user-data");
  }

  // Только для админов
  getAllUsers() {
    return this.http.get("/api/admin/users");
  }

  // С обработкой 403
  deleteUser(userId: string) {
    return this.http.delete(`/api/admin/users/${userId}`).pipe(
      catchError(error => {
        if (error.status === 403) {
          console.error("У вас нет прав для этого действия");
          throw new Error("Доступ запрещен");
        }
        throw error;
      })
    );
  }
}

Реальный пример: Система управления постами

// API сервис
@Injectable()
export class PostService {
  constructor(private http: HttpClient) {}

  // Получить посты (доступно всем)
  getPosts() {
    return this.http.get("/api/posts");
  }

  // Создать пост (нужна авторизация)
  createPost(data: CreatePostDto) {
    return this.http.post("/api/posts", data);
  }

  // Отредактировать свой пост
  updatePost(id: string, data: UpdatePostDto) {
    return this.http.put(`/api/posts/${id}`, data);
  }

  // Удалить свой пост
  deletePost(id: string) {
    return this.http.delete(`/api/posts/${id}`);
  }

  // Модерирование поста
  approvePost(id: string) {
    return this.http.post(`/api/admin/posts/${id}/approve`, {});
  }

  // Удаление поста админом
  forceDeletePost(id: string) {
    return this.http.delete(`/api/admin/posts/${id}`);
  }
}

// Компонент с разделением функционала
@Component({
  selector: "app-post",
  template: `
    <div class="post">
      <h2>{{ post.title }}</h2>
      <p>{{ post.content }}</p>

      <!-- Автор может редактировать/удалять -->>
      <div *ngIf="isAuthor">
        <button (click)="onEdit()">Редактировать</button>
        <button (click)="onDelete()">Удалить</button>
      </div>

      <!-- Статус (видно при наличии права) -->
      <p *ngIf="canModerate">
        Статус: {{ post.status }}
      </p>

      <!-- Кнопки модерации только для модераторов -->
      <div *ngIf="canModerate && post.status === pending">
        <button (click)="onApprove()">Одобрить</button>
        <button (click)="onReject()">Отклонить</button>
      </div>

      <!-- Удаление админом -->
      <div *ngIf="isAdmin">
        <button (click)="onForceDelete()" class="dangerous">
          Удалить (админ)
        </button>
      </div>
    </div>
  `
})
export class PostComponent implements OnInit {
  @Input() post: Post;
  @Input() currentUserId: string;
  @Input() currentUserRole: UserRole;

  isAuthor: boolean;
  canModerate: boolean;
  isAdmin: boolean;

  ngOnInit() {
    this.isAuthor = this.post.authorId === this.currentUserId;
    this.canModerate = this.currentUserRole === UserRole.MODERATOR ||
                       this.currentUserRole === UserRole.ADMIN;
    this.isAdmin = this.currentUserRole === UserRole.ADMIN;
  }

  onEdit() { /* ... */ }
  onDelete() { /* ... */ }
  onApprove() { /* ... */ }
  onReject() { /* ... */ }
  onForceDelete() { /* ... */ }
}

Сервер должен проверять права

// На сервере (Node.js/Express)
app.post("/api/admin/users/:id/ban", authMiddleware, (req, res) => {
  // Проверка 1: Пользователь авторизирован
  if (!req.user) {
    return res.status(401).json({ error: "Not authenticated" });
  }

  // Проверка 2: Только админ может выполнить это
  if (req.user.role !== "admin") {
    return res.status(403).json({ error: "Forbidden" });
  }

  // Проверка 3: ID параметр валидный
  const { id } = req.params;
  if (!isValidUUID(id)) {
    return res.status(400).json({ error: "Invalid user ID" });
  }

  // Выполняем действие
  User.findByIdAndUpdate(id, { banned: true }, (err, user) => {
    if (err) {
      return res.status(500).json({ error: "Database error" });
    }
    res.json({ message: "User banned", user });
  });
});

Лучшие практики

  1. Всегда проверяйте на бэкенде - не доверяйте фронтенду
  2. Используйте токены с информацией о ролях (JWT)
  3. Кэшируйте информацию о пользователе в контексте/хранилище
  4. Обрабатывайте 403 ошибки (доступ запрещен)
  5. Логируйте попытки несанкционированного доступа
  6. Предоставляйте понятные сообщения об ошибках
  7. Разделяйте ответственность между фронтом и беком

Итог

Разделение функционала по ролям - это критичная часть безопасности приложения. На фронтенде это улучшает UX, но основная защита должна быть на сервере. Хороший подход:

  1. Фронтенд: Скрывает UI элементы, улучшает UX
  2. Сервер: Проверяет права при каждом запросе
  3. Оба: Работают вместе для безопасного приложения

Это стандартный паттерн, используемый во всех крупных приложениях.