bf0d130ee8
2FA обязательна для инстанс-администраторов, но до её включения вход закрыт — получался замкнутый круг. Добавлены команды обслуживания: - `glchat totp-setup --email <admin>`: создаёт секрет, подтверждает его кодом, печатает секрет, otpauth-ссылку и 8 резервных кодов (каждый одноразовый); - `glchat totp-reset --email <admin>`: удаляет секрет при потере устройства; - код `auth.2fa_enrollment_required` с подсказкой, какую команду выполнить; - установщик: флаг `--admin-2fa` для автоматического включения (по умолчанию печатает подсказку, чтобы секреты не оседали в логах установки). Проверено сквозным прогоном локально: bootstrap → 403 на входе без 2FA → totp-setup → вход с TOTP-кодом → профиль, серверы, комнаты, роли, участники, аудит, создание сервера админом, 429 на шестой попытке входа, секретов в логах нет.
36 lines
1.3 KiB
TypeScript
36 lines
1.3 KiB
TypeScript
import type { Channel } from '@/api/types';
|
|
|
|
export interface ChannelGroup {
|
|
category: Channel | null;
|
|
channels: Channel[];
|
|
}
|
|
|
|
/**
|
|
* Группирует комнаты по категориям, сохраняя порядок `position`.
|
|
* Невидимые комнаты (`can_view === false`) сервер обычно не присылает,
|
|
* но если прислал — прячем их на клиенте.
|
|
*/
|
|
export function groupChannels(channels: Channel[]): ChannelGroup[] {
|
|
const visible = channels
|
|
.filter((channel) => channel.can_view !== false && channel.type !== 'category')
|
|
.slice()
|
|
.sort((left, right) => left.position - right.position);
|
|
const categories = channels
|
|
.filter((channel) => channel.type === 'category')
|
|
.slice()
|
|
.sort((left, right) => left.position - right.position);
|
|
|
|
const groups: ChannelGroup[] = [];
|
|
const uncategorized = visible.filter((channel) => channel.parent_id === undefined);
|
|
if (uncategorized.length > 0) {
|
|
groups.push({ category: null, channels: uncategorized });
|
|
}
|
|
for (const category of categories) {
|
|
const children = visible.filter((channel) => channel.parent_id === category.id);
|
|
if (children.length > 0) {
|
|
groups.push({ category, channels: children });
|
|
}
|
|
}
|
|
return groups;
|
|
}
|