feat(instance): админ-панель с лимитами медиа и действиями над пользователями
- лимиты медиа живут в настройках инстанса (миграция 00016): аватары, оформление сервера, эмодзи, звуки и галерея; все загрузки берут предел оттуда, значения по умолчанию — из AGENT.md 7.7 - действия администратора: временный пароль (показывается один раз, сессии отзываются), выход со всех устройств, мягкое удаление пользователя (сообщения и аудит остаются), переименование и удаление любого сервера - админ-панель разбита на вкладки: обзор и здоровье, лимиты, пользователи, серверы, аудит; смена администраторов подтверждается личностью (step-up) - тесты: Go (действия администратора, лимит эмодзи из настроек) и Vitest (вкладки, сброс пароля, выход, удаление, переименование сервера)
This commit is contained in:
+128
-3
@@ -541,6 +541,8 @@ describe('настройки: аватар', () => {
|
||||
renderApp('/app/empty');
|
||||
const dialog = await openSettings();
|
||||
await goToSection(dialog, 'Инстанс');
|
||||
// Админ-панель разбита на вкладки: пользователи живут в своей.
|
||||
await userEvent.click(await within(dialog).findByTestId('instance-tab-users'));
|
||||
|
||||
expect(await within(dialog).findByTestId('instance-user-id-u-42')).toHaveTextContent('u-42');
|
||||
expect(within(dialog).getByText(/ID пользователей видны только администратору/)).toBeVisible();
|
||||
@@ -722,9 +724,132 @@ describe('настройки: безопасность', () => {
|
||||
|
||||
renderApp('/settings/account/instance');
|
||||
|
||||
// Серверы инстанса.
|
||||
await userEvent.click(await screen.findByTestId('instance-tab-guilds'));
|
||||
expect(await screen.findByTestId('instance-guilds')).toHaveTextContent('Main');
|
||||
expect(screen.getByTestId('instance-users')).toHaveTextContent('Alice');
|
||||
expect(screen.getByTestId('instance-audit')).toHaveTextContent('guild.create');
|
||||
expect(screen.getByText('motd')).toBeVisible();
|
||||
|
||||
// Пользователи инстанса.
|
||||
await userEvent.click(screen.getByTestId('instance-tab-users'));
|
||||
expect(await screen.findByTestId('instance-users')).toHaveTextContent('Alice');
|
||||
|
||||
// Журнал действий администраторов.
|
||||
await userEvent.click(screen.getByTestId('instance-tab-audit'));
|
||||
expect(await screen.findByTestId('instance-audit')).toHaveTextContent('guild.create');
|
||||
|
||||
// Лимиты: панель показывает значения из настроек.
|
||||
await userEvent.click(screen.getByTestId('instance-tab-limits'));
|
||||
expect(await screen.findByTestId('instance-limits')).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
describe('админ-панель: действия', () => {
|
||||
it('сбрасывает пароль, выкидывает со всех устройств и удаляет пользователя', async () => {
|
||||
vi.spyOn(window, 'confirm').mockReturnValue(true);
|
||||
const admin = makeUser({ is_instance_admin: true });
|
||||
const fetchMock = installFetch([
|
||||
{ match: '/api/v1/users/@me', response: () => json({ user: admin }) },
|
||||
{ match: '/api/v1/instance/settings', response: () => json({ settings: {} }) },
|
||||
{ match: '/api/v1/instance/guilds', response: () => json({ guilds: [] }) },
|
||||
{
|
||||
match: '/api/v1/instance/users/u-7/reset-password',
|
||||
method: 'POST',
|
||||
response: () => json({ user_id: 'u-7', password: 'Temp-Pass-123456', sessions_revoked: 2 }),
|
||||
},
|
||||
{
|
||||
match: '/api/v1/instance/users/u-7/logout',
|
||||
method: 'POST',
|
||||
response: () => json({ ok: true }),
|
||||
},
|
||||
{
|
||||
match: '/api/v1/instance/users/u-7',
|
||||
method: 'DELETE',
|
||||
response: () => json({ ok: true }),
|
||||
},
|
||||
{
|
||||
match: '/api/v1/instance/users',
|
||||
response: () =>
|
||||
json({
|
||||
users: [
|
||||
{
|
||||
id: 'u-7',
|
||||
username: 'bob',
|
||||
display_name: 'Bob',
|
||||
is_instance_admin: false,
|
||||
created_at: '2026-09-01T00:00:00Z',
|
||||
},
|
||||
],
|
||||
}),
|
||||
},
|
||||
{ match: '/api/v1/instance/audit', response: () => json({ entries: [] }) },
|
||||
]);
|
||||
|
||||
renderApp('/settings/account/instance');
|
||||
await userEvent.click(await screen.findByTestId('instance-tab-users'));
|
||||
const panel = await screen.findByTestId('instance-users-actions');
|
||||
|
||||
// Временный пароль показывается один раз.
|
||||
await userEvent.click(within(panel).getByTestId('instance-user-reset-u-7'));
|
||||
expect(await screen.findByTestId('instance-temp-password')).toHaveTextContent(
|
||||
'Temp-Pass-123456',
|
||||
);
|
||||
|
||||
// Выход со всех устройств.
|
||||
await userEvent.click(within(panel).getByTestId('instance-user-logout-u-7'));
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
recordedRequests(fetchMock).some(
|
||||
(request) => request.method === 'POST' && request.url.includes('/users/u-7/logout'),
|
||||
),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
// Мягкое удаление.
|
||||
await userEvent.click(within(panel).getByTestId('instance-user-delete-u-7'));
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
recordedRequests(fetchMock).some(
|
||||
(request) => request.method === 'DELETE' && request.url.endsWith('/users/u-7'),
|
||||
),
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
it('переименовывает сервер инстанса', async () => {
|
||||
const admin = makeUser({ is_instance_admin: true });
|
||||
const fetchMock = installFetch([
|
||||
{ match: '/api/v1/users/@me', response: () => json({ user: admin }) },
|
||||
{ match: '/api/v1/instance/settings', response: () => json({ settings: {} }) },
|
||||
{
|
||||
match: '/api/v1/instance/guilds/g-1',
|
||||
method: 'PATCH',
|
||||
response: () => json({ ok: true }),
|
||||
},
|
||||
{
|
||||
match: '/api/v1/instance/guilds',
|
||||
response: () =>
|
||||
json({
|
||||
guilds: [{ id: 'g-1', name: 'Старое', member_count: 3, owner_id: 'u', is_main: false }],
|
||||
}),
|
||||
},
|
||||
{ match: '/api/v1/instance/users', response: () => json({ users: [] }) },
|
||||
{ match: '/api/v1/instance/audit', response: () => json({ entries: [] }) },
|
||||
]);
|
||||
|
||||
renderApp('/settings/account/instance');
|
||||
await userEvent.click(await screen.findByTestId('instance-tab-guilds'));
|
||||
const panel = await screen.findByTestId('instance-guilds-actions');
|
||||
|
||||
await userEvent.click(within(panel).getByTestId('instance-guild-rename-g-1'));
|
||||
const nameInput = screen.getByLabelText('Новое имя сервера');
|
||||
await userEvent.clear(nameInput);
|
||||
await userEvent.type(nameInput, 'Новое');
|
||||
await userEvent.click(screen.getByRole('button', { name: 'Сохранить' }));
|
||||
|
||||
await waitFor(() => {
|
||||
const request = recordedRequests(fetchMock).find(
|
||||
(entry) => entry.method === 'PATCH' && entry.url.includes('/instance/guilds/g-1'),
|
||||
);
|
||||
expect(request?.body).toMatchObject({ name: 'Новое' });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user