Свій адмінський клієнт
Друга половина протоколу: керування ефіром. Ця сторінка описує наскрізний шлях від входу до керування чергою.
Передбачається, що ви вже прочитали «Свій клієнт за 15 хвилин» — адмінське з'єднання будується поверх звичайного слухацького.
Дві половини автентифікації
Найчастіша причина «нічого не працює, і помилок теж немає»: адмін автентифікується двічі й окремо.
| Канал | Як | Що дає |
|---|---|---|
| HTTP | POST /api/admin/login → кука або bearer | доступ до /api/admin/* |
| Socket.io | подія admin_active | адмінські події та їх результати |
Успішний вхід через HTTP не робить ваш сокет адмінським. Поки не надіслано admin_active, сервер вважає з'єднання звичайним слухачем.
Крок 1. Вхід
POST /api/admin/login
Content-Type: application/json
{ "login": "admin", "password": "…" }{ "ok": true } // у продакшені
{ "ok": true, "token": "…" } // у режимі розробкиУ продакшені токена в тілі відповіді немає
Він приходить лише в httpOnly-куці adminToken, яку JavaScript прочитати не може. Тому запити треба слати з credentials: 'include', а походження вашого клієнта — додати в CLIENT_ORIGIN на сервері. Токен живе 12 годин, кількість спроб входу обмежена.
Крок 2. Підвищення сокета
socket.emit('admin_active'); // токен візьметься з куки
socket.emit('admin_active', jwtToken); // або передайте явно
socket.on('admin_confirmed', ({ role, privileges, authorized }) => {
// будуйте інтерфейс саме за цим списком
});
socket.on('admin_error', (raw) => {
const msg = JSON.parse(raw); // об’єкт з усіма локалями
});Помилки тут мовчазні
Якщо сокет не підвищено, адмінські події ігноруються без відповіді — ні помилки, ні колбека. Клієнт виглядатиме «завислим». Завжди дочекайтеся admin_confirmed, перш ніж показувати керування.
authorized: false означає помічника, який ще не активував себе тимчасовим паролем: до активації він не може нічого.
Крок 3. Інтерфейс за привілеями
privileges — це масив рядків, і він авторитетніший за вміст токена: сервер перечитує права з бази при кожному admin_active, тому зміни діють без повторного входу.
const can = (p) => privileges.includes(p);
if (can('queue_manage')) renderQueueControls();Повний перелік — у довіднику привілеїв. Супер-адмін отримує всі 13.
Права в інтерфейсі — це зручність, а не захист
Сервер перевіряє привілей на кожній операції незалежно. Ховати кнопки корисно, але покладатися на це не можна — і не потрібно.
Крок 4. Черга
socket.emit('get_queue', { offset: 0, limit: 20 }, ({ items, total }) => {
// items: [{ id, title, artist, orderType }]
});Читання йде через колбеки, а зміни — ні. Результат будь-якої зміни приходить окремою подією:
socket.on('admin_success', (raw) => showToast(pickLocalized(JSON.parse(raw))));
socket.on('admin_error', (raw) => showError(pickLocalized(JSON.parse(raw))));Зверніть увагу: payload — рядок, який треба спершу JSON.parse, і лише потім вибрати мову. Успішні повідомлення часто мають поле code (QUEUE_SONG_ADDED тощо) — гілкуйтеся по ньому, а не по тексту.
Операції над чергою:
socket.emit('admin_add_song', { id, title, artist, orderType: 'lastinline' });
socket.emit('admin_remove_song', position); // зсув від поточного треку
socket.emit('admin_skip_song');
socket.emit('admin_insert_song_group', { groupId });orderType буває 'lastinline' (звичайне) або 'donated' — друге ставить трек попереду звичайних замовлень.
Кулдауни — це нормальна відповідь, а не збій
Додавання, видалення й перемикання мають окремі затримки. Сервер повертає помилку з кількістю секунд очікування — покажіть її користувачеві замість того, щоб повторювати запит.
Позицію для admin_remove_song беріть із search_queue: там кожен елемент уже має поле position.
Крок 5. Замовлення слухачів
Адміни з queue_manage отримують список замовлень і оновлення до нього:
socket.on('suggestions_update', (list) => {
// [{ uid, song: { id, title, artist }, addedAt }]
});
socket.emit('admin_suggestion_action', { uid, action: 'add' }); // або 'reject'Замовлення живе 5 хвилин і згасає само — слухач у цьому разі отримає suggestion_result з auto: true. Якщо ухвалити вже згасле замовлення, повернеться помилка «не знайдено», тож список варто оновлювати за подією, а не кешувати надовго.
Одна сесія на обліковий запис
Коли той самий адмін підключається знову, попереднє з'єднання сервер прибирає. Друга вкладка тихо «відбирає» сесію в першої. Це очікувана поведінка, а не збій вашого клієнта.
Крок 6. Завантаження треку
Це не один запит, а конвеєр із трьох послідовних кроків — і трек не потрапляє в ефір, доки не пройдено останній. Потребує привілею upload_songs.
upload-check-duplicate → upload-song-file → upload-song-lyrics → upload-song-commit
(необов’язково) файл у сховище текст пісні запис у бібліотеку// 1. Дешева перевірка до довгого завантаження
await post('/api/admin/upload-check-duplicate', { trackId: `day/${filename}` });
// 2. Файл — СИРИМИ БАЙТАМИ, не multipart
const { metadata } = await fetch(`${SERVER}/api/admin/upload-song-file?mode=day`, {
method: 'POST',
credentials: 'include',
headers: {
'Content-Type': 'audio/mpeg',
'X-File-Name': encodeURIComponent(file.name),
},
body: file, // Blob або ArrayBuffer
}).then((r) => r.json());
// 3. Текст пісні (необов’язково — LRCLIB, без облікових даних)
const { lyricsEntry } = await post('/api/admin/upload-song-lyrics', {
title: metadata.title, artist: metadata.artist,
album: metadata.album, duration: metadata.duration,
});
// 4. Лише тут трек з’являється в бібліотеці
await post('/api/admin/upload-song-commit', { metadata, lyricsEntry });Файл надсилається сирими байтами
Другий крок читає тіло через express.raw, а не як multipart/form-data. FormData тут дасть 400. Ім’я файлу передається заголовком X-File-Name, бо в сирому тілі його нема звідки взяти. Обмеження — 80 МБ.
Між кроками 2 і 4 метадані можна показати користувачеві для правки — вони прочитані з ID3-тегів і часто потребують уточнення. Передавайте в commit уже виправлений об'єкт.
Після успішного commit сервер широкомовно шле library_updated — це сигнал усім клієнтам перечитати списки.
Що далі
Живий ефір — окрема тема з власним порядком дій: вихід в ефір, мікрофон, черга гостей, модерація. Розписана з діаграмами послідовностей у «Живий ефір: послідовності».
Решта адмінського контуру — редактор треків, тексти, налаштування, групи, журнал аудиту та облікові записи — розібрана в «Решта адмінських сценаріїв». Точні тіла запитів і права доступу — у довіднику REST та довіднику подій.