Командный блог StudyCode: читатели, авторы и админ. Черновики, публикация, модерация комментариев — и middleware, который решает, кому что можно.
У StudyCode появился командный блог. Регина его читает, Антон и Марина пишут статьи, а Давид — админ: следит за порядком и модерирует комментарии.
Первую версию сервера писали на скорую руку, и по всему коду расползлись проверки вида:
if (user.isAdmin) { ... }
if (user.id === 2 || user.id === 3 || user.isAdmin) { ... } // «2 и 3 — это же авторы?»Наняли нового автора — ищи все такие if по проекту и дописывай ещё один id. Забыл один — дыра в безопасности. Эта боль лечится подходом RBAC — Role-Based Access Control, «управление доступом на основе ролей».
Идея RBAC: права выдаются не человеку, а роли. Пользователь лишь носит роль, как бейдж:
{ id: 2, name: 'Антон', role: 'author' }Наняли четвёртого автора? Просто создаём пользователя с role: 'author' — и все права авторов у него уже есть. Ни одного if в коде менять не нужно.
Прежде чем писать код, RBAC-проект описывают таблицей «действие × роль». Наша:
| Действие | аноним | reader | author | admin |
|---|---|---|---|---|
| Читать опубликованное | ✅ | ✅ | ✅ | ✅ |
| Видеть черновики | — | — | только свои | все |
| Создать пост | — | — | ✅ | ✅ |
| Редактировать пост | — | — | только свои | любые |
| Опубликовать пост | — | — | только свои | любые |
| Комментировать | — | ✅ | ✅ | ✅ |
| Скрывать комментарии | — | — | — | ✅ |
Статистика /api/admin/* | — | — | — | ✅ |
Заметь колонку author: местами написано не просто «да», а «только свои». Роль отвечает на вопрос «можно ли такое действие вообще», а владение — «можно ли над этим конкретным объектом». Обе проверки живут на сервере, и мы напишем обе.
Фронтенд может спрятать кнопку «Удалить» у читателя — и это правильно для UX. Но спрятанная кнопка — не защита: любой открывает DevTools и отправляет запрос руками:
PATCH /api/comments/1/hide
Authorization: Bearer tok_reader01Если сервер не проверит роль сам — читатель станет модератором. Правило: фронт прячет кнопки для удобства, сервер проверяет права для безопасности. Каждый защищённый маршрут обязан проверять права заново, даже если «кнопки в интерфейсе всё равно нет».
Регистрацию, вход и выдачу токенов ты построил в проекте Auth — второй раз это не учим. Здесь сессии выданы заранее: в sessions уже лежат четыре токена (tok_reader01, tok_avtor01, tok_avtor02, tok_admin01), а знакомый охранник auth и его «мягкий» напарник getUser (вернёт пользователя или null, без 401) уже написаны в стартовом коде.
GET /api/posts лента (видимость зависит от роли)
POST /api/posts author, admin — создать черновик
PATCH /api/posts/:id автор — свой, админ — любой
PATCH /api/posts/:id/publish автор — свой, админ — любой; повторно → 409
GET /api/posts/:id/comments все; скрытые — только админ
POST /api/posts/:id/comments любой вошедший
PATCH /api/comments/:id/hide только admin
GET /api/admin/stats только adminНиже — стартовый скелет с данными и готовыми помощниками. Дальше — лента.
const app = express();
app.use(express.json());
// Команда блога StudyCode. У каждого — роль: 'reader' | 'author' | 'admin'
let users = [
{ id: 1, name: 'Регина', role: 'reader' },
{ id: 2, name: 'Антон', role: 'author' },
{ id: 3, name: 'Марина', role: 'author' },
{ id: 4, name: 'Давид', role: 'admin' },
];
// Вход ты уже строил в проекте Auth — здесь сессии выданы заранее
let sessions = {
tok_reader01: 1, // Регина
tok_avtor01: 2, // Антон
tok_avtor02: 3, // Марина
tok_admin01: 4, // Давид
};
let posts = [
{ id: 1, title: 'Добро пожаловать в блог', text: 'Первый пост команды StudyCode.', status: 'published', authorId: 2 },
{ id: 2, title: 'Черновик про Docker', text: 'Пока только план...', status: 'draft', authorId: 2 },
];
let nextPostId = 3;
let comments = [
{ id: 1, postId: 1, userId: 1, text: 'Отличный пост!', hidden: false },
];
let nextCommentId = 2;
// Знакомый охранник из проекта Auth — уже готов, писать заново не нужно
function auth(req, res, next) {
const header = req.headers.authorization; // Express: имена заголовков — lowercase
if (!header || !header.startsWith('Bearer ')) {
return res.status(401).json({ error: 'Нужен токен' });
}
const token = header.slice(7);
if (sessions[token] === undefined) {
return res.status(401).json({ error: 'Неверный токен' });
}
req.user = users.find(u => u.id === sessions[token]);
next();
}
// Мягкое опознание для ПУБЛИЧНЫХ маршрутов: вернёт пользователя или null.
// В отличие от auth не отвечает 401 — аноним тоже имеет право смотреть ленту.
function getUser(req) {
const header = req.headers.authorization;
if (!header || !header.startsWith('Bearer ')) return null;
const id = sessions[header.slice(7)];
if (id === undefined) return null;
return users.find(u => u.id === id) || null;
}
// Дальше здесь появятся:
// GET /api/posts — лента с учётом роли
// function requireRole(...roles) — фабрика middleware
// POST /api/posts — создать черновик (author, admin)
// PATCH /api/posts/:id — редактирование с проверкой владельца
// PATCH /api/posts/:id/publish — публикация
// GET/POST /api/posts/:id/comments — комментарии
// PATCH /api/comments/:id/hide — модерация (admin)
// GET /api/admin/stats — админ-панель
app.listen(3000, () => console.log('✅ http://localhost:3000'));Лестница проверок защищённого запроса. Нажми на шаг — увидишь код ответа и пояснение.
Клиент шлёт запрос с заголовком Authorization: Bearer <токен>. Дальше идёт лестница проверок — та же, что в проекте Auth, но с ролями и владением.