feat(admin): paginacao na interface, edicao de utilizadores e pagina de testes
- fila e utilizadores passam a pedir uma pagina de cada vez, com pesquisa e filtros; nada e cortado no browser, porque o que faltava nunca chegou a ser buscado. A pesquisa espera que se pare de escrever: cada tecla seria mais uma consulta, e as respostas podem chegar fora de ordem - pageWindow e pageRange tem testes proprios: uma pagina fora do intervalo vem de um URL velho ou de uma lista que encolheu, e nao pode dar um paginador vazio ou negativo, nem dizer "76 a 100 de 82" - editar utilizador: papel, quota, idioma, com os apelidos que entregam naquela caixa a mostra no mesmo painel - "Caixas de e-mail" passa a chamar-se Utilizadores, que e o que sao Pagina nova /admin/tests: verificacoes que o operador corre a mao, sobre o servidor ou sobre um utilizador. Todas leem estado; nenhuma envia correio nem altera definicoes -- uma pagina de diagnostico que mexe naquilo que diagnostica e pior do que nao existir. As regras de cada verificacao sao funcoes puras com testes, separadas da recolha dos factos. Alguns casos que estes testes fixam: uma quota zero significa sem limite, e um racio marcaria todas as caixas ilimitadas como cheias; um bloqueio ja expirado nao bloqueia ninguem e nao deve mandar ninguem investigar; e uma resposta de ausencia ligada com o periodo ja passado esta ligada sem fazer nada, que e o que parece avariado a quem a pos. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
52bf43b74f
commit
2b92abd185
16 files changed
+1431
-153
No files matched your search
@@ -917,6 +917,75 @@ export async function listAliasesForMailbox(
|
||||
);
|
||||
}
|
||||
|
||||
/** Everything one user's checks need, in a single pass over the database. */
|
||||
export async function userDiagnosticFacts(
|
||||
scope: AdminScope,
|
||||
mailboxId: string,
|
||||
): Promise<Record<string, unknown> | null> {
|
||||
const { sql, params } = scopeClause(scope, "m.domain_id");
|
||||
const row = await queryOne<Record<string, unknown>>(
|
||||
`SELECT m.email_address, m.is_active, m.locked_until,
|
||||
m.quota_bytes::text AS quota_bytes, m.used_bytes::text AS used_bytes,
|
||||
(m.password_hash IS NOT NULL AND m.password_hash <> '') AS password_hash_present,
|
||||
EXISTS (SELECT 1 FROM mfa_totp t WHERE t.mailbox_id = m.id AND t.status = 'CONFIRMED') AS mfa_enrolled,
|
||||
(SELECT count(*) FROM aliases a
|
||||
WHERE a.domain_id = m.domain_id
|
||||
AND EXISTS (SELECT 1 FROM unnest(a.destination_addresses) d
|
||||
WHERE lower(d) = lower(m.email_address)))::int AS alias_count,
|
||||
(SELECT length(sc.script) FROM sieve_scripts sc
|
||||
WHERE sc.mailbox_id = m.id AND sc.is_active
|
||||
AND sc.name <> 'vacation-autoresponder' LIMIT 1) AS sieve_bytes,
|
||||
EXISTS (SELECT 1 FROM sieve_scripts sc
|
||||
WHERE sc.mailbox_id = m.id AND sc.is_active
|
||||
AND sc.name = 'vacation-autoresponder') AS vacation_enabled
|
||||
FROM mailboxes m
|
||||
WHERE m.id = $${params.length + 1} AND ${sql}`,
|
||||
[...params, mailboxId],
|
||||
);
|
||||
return row ?? null;
|
||||
}
|
||||
|
||||
/** Everything the server-wide checks need. */
|
||||
export async function serverDiagnosticFacts(scope: AdminScope): Promise<Record<string, unknown>> {
|
||||
const { sql, params } = scopeClause(scope, "d.id");
|
||||
const domains = await query<{ name: string; dns_status: string; dane_enabled: boolean }>(
|
||||
`SELECT d.name, d.dns_status, d.dane_enabled FROM domains d WHERE ${sql} ORDER BY d.name`,
|
||||
params,
|
||||
);
|
||||
|
||||
const withoutDkim = await query<{ name: string }>(
|
||||
`SELECT d.name FROM domains d
|
||||
WHERE ${sql}
|
||||
AND NOT EXISTS (SELECT 1 FROM dkim_keys k WHERE k.domain_id = d.id)
|
||||
ORDER BY d.name`,
|
||||
params,
|
||||
);
|
||||
|
||||
const queue = await query<{ status: string; count: string }>(
|
||||
`SELECT status, count(*)::text AS count FROM outbound_jobs GROUP BY status`,
|
||||
);
|
||||
|
||||
const { sql: mSql, params: mParams } = scopeClause(scope, "m.domain_id");
|
||||
const quota = await queryOne<{ over: string; total: string }>(
|
||||
`SELECT count(*) FILTER (WHERE m.quota_bytes > 0 AND m.used_bytes >= m.quota_bytes)::text AS over,
|
||||
count(*)::text AS total
|
||||
FROM mailboxes m WHERE ${mSql}`,
|
||||
mParams,
|
||||
);
|
||||
|
||||
return {
|
||||
domains: domains.map((d) => ({
|
||||
name: d.name,
|
||||
dnsStatus: d.dns_status,
|
||||
daneEnabled: d.dane_enabled,
|
||||
})),
|
||||
domainsWithoutDkim: withoutDkim.map((d) => d.name),
|
||||
queueByStatus: Object.fromEntries(queue.map((r) => [r.status, Number(r.count)])),
|
||||
usersOverQuota: Number(quota?.over ?? 0),
|
||||
usersTotal: Number(quota?.total ?? 0),
|
||||
};
|
||||
}
|
||||
|
||||
export type AliasResult = { status: "OK"; id: string } | { status: "FORBIDDEN" } | { status: "DUPLICATE" } | { status: "INVALID" };
|
||||
|
||||
export async function createAlias(
|
||||
|
||||
Reference in new issue
Block a user