mirror of
https://github.com/warmbly/warmbly.git
synced 2026-10-13 00:02:43 +00:00
feat: integrate exact cross-assignment mailbox UUID navigation with native administrator device consent schemas and focused identity lookup regressions
This commit is contained in:
@@ -0,0 +1,35 @@
|
||||
import { act } from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import { MemoryRouter } from "react-router-dom";
|
||||
import { afterEach, expect, it, vi } from "vitest";
|
||||
import MailboxesPage from "./MailboxesPage";
|
||||
|
||||
const calls = vi.hoisted(() => ({ search: vi.fn(), queries: vi.fn() }));
|
||||
vi.mock("@/lib/api/client/admin/mailboxes", () => ({ searchMailboxes: calls.search }));
|
||||
vi.mock("@/lib/api/client/admin/fleetNodes", () => ({ listFleetNodes: vi.fn() }));
|
||||
vi.mock("@/components/data/DataTable", () => ({ DataTable: () => <div>Mailbox results</div> }));
|
||||
vi.mock("@tanstack/react-query", () => ({ keepPreviousData: vi.fn(), useQuery: (options: { queryKey: string[]; queryFn: () => unknown }) => {
|
||||
calls.queries(options);
|
||||
if (options.queryKey[1] === "mailboxes") options.queryFn();
|
||||
return { data: { data: [], pagination: {} }, isLoading: false };
|
||||
} }));
|
||||
afterEach(() => { vi.clearAllMocks(); vi.unstubAllGlobals(); });
|
||||
|
||||
it("looks up an exact mailbox across inactive/moved assignments without old filters or placeholder rows", async () => {
|
||||
vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true);
|
||||
const mailbox = "33333333-3333-4333-8333-333333333333";
|
||||
const host = document.createElement("div");
|
||||
document.body.append(host);
|
||||
const root = createRoot(host);
|
||||
try {
|
||||
await act(async () => root.render(<MemoryRouter initialEntries={[`/mailboxes?mailbox_id=${mailbox}&worker=old-worker&q=old-search&org=old-org`]}><MailboxesPage /></MemoryRouter>));
|
||||
expect(calls.search).toHaveBeenCalledWith({ mailbox_id: mailbox, status: "all", limit: 50 });
|
||||
expect(calls.queries.mock.calls.filter(([options]) => options.queryKey[1] === "mailboxes").every(([options]) => options.placeholderData === undefined)).toBe(true);
|
||||
expect(host.textContent).toContain("Exact identity lookup");
|
||||
expect(host.textContent).toContain(mailbox);
|
||||
expect(host.querySelector("input")).toBeNull();
|
||||
} finally {
|
||||
await act(async () => root.unmount());
|
||||
host.remove();
|
||||
}
|
||||
});
|
||||
@@ -177,6 +177,7 @@ export default function MailboxesPage() {
|
||||
const orgId = params.get("org") || undefined;
|
||||
const userId = params.get("user") || undefined;
|
||||
const workerParam = params.get("worker") || "";
|
||||
const mailboxId = params.get("mailbox_id") || undefined;
|
||||
|
||||
// `?q=` seeds the search box so the command palette can land here on a
|
||||
// mailbox; a status of "all" keeps a disabled mailbox findable that way.
|
||||
@@ -225,7 +226,7 @@ export default function MailboxesPage() {
|
||||
];
|
||||
|
||||
const filterKey = JSON.stringify({
|
||||
query, status, provider, warmup, workerId, riskBand, pool, synced, orgId, userId,
|
||||
query, status, provider, warmup, workerId, riskBand, pool, synced, orgId, userId, mailboxId,
|
||||
warmupPaused, trackingVerified, hasTrackingDomain, hasOrg, signatureSync, hasOAuth, hasSmtp,
|
||||
capMin, capMax, gapMin, gapMax, connected, lastSynced, sort,
|
||||
});
|
||||
@@ -237,7 +238,7 @@ export default function MailboxesPage() {
|
||||
const { data, isLoading, error, refetch } = useQuery({
|
||||
queryKey: ["admin", "mailboxes", filterKey, pager.cursor],
|
||||
queryFn: () =>
|
||||
searchMailboxes({
|
||||
searchMailboxes(mailboxId ? { mailbox_id: mailboxId, status: "all", limit: 50 } : {
|
||||
q: query.trim() || undefined,
|
||||
status,
|
||||
provider: provider || undefined,
|
||||
@@ -270,7 +271,7 @@ export default function MailboxesPage() {
|
||||
sort_desc: sort.by ? sort.desc : undefined,
|
||||
}),
|
||||
staleTime: 30_000,
|
||||
placeholderData: keepPreviousData,
|
||||
placeholderData: mailboxId ? undefined : keepPreviousData,
|
||||
});
|
||||
|
||||
const rows = data?.data ?? [];
|
||||
@@ -330,6 +331,15 @@ export default function MailboxesPage() {
|
||||
setParams(next, { replace: true });
|
||||
}
|
||||
|
||||
if (mailboxId) return <div>
|
||||
<PageHeader title="Mailbox" description="Exact identity lookup across current assignments, including inactive mailboxes. Existing browse filters do not apply." />
|
||||
<p className="mb-4 break-all font-mono text-xs">{mailboxId}</p>
|
||||
<Button variant="outline" className="mb-4" onClick={() => clearParam("mailbox_id")}>Return to mailbox browser</Button>
|
||||
<DataTable columns={columns} rows={rows} getRowId={(m) => m.id} loading={isLoading} error={error}
|
||||
onRetry={() => refetch()} errorTitle="Mailbox lookup failed" noun="mailboxes"
|
||||
emptyTitle="Mailbox not found" emptyHint="This mailbox is no longer registered on this instance." />
|
||||
</div>;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<PageHeader
|
||||
|
||||
@@ -280,7 +280,7 @@ export default function WorkerDetailPage() {
|
||||
</Section>
|
||||
)}
|
||||
|
||||
<NodeDiagnostics node={node} mailboxes={mailboxes} />
|
||||
<NodeDiagnostics node={node} />
|
||||
|
||||
<Section
|
||||
title="Version"
|
||||
|
||||
@@ -12,7 +12,6 @@ import { useMe } from "@/hooks/useMe";
|
||||
import useBrowseState from "@/hooks/useBrowseState";
|
||||
import { AdminPerm, hasAdminPerm } from "@/lib/auth/permissions";
|
||||
import type { FleetNode } from "@/lib/api/client/admin/fleetNodes";
|
||||
import type { AdminWorkerEmail } from "@/lib/api/models/admin";
|
||||
import { getNodeLogs, getNodeBroker, type NodeBrokerSource, type NodeLogFilters } from "@/lib/api/client/admin/nodeDiagnostics";
|
||||
import { diagnosticReason, emptyLogFilters, eventLabels, isUUID, logFilterError, parseLogFilters, safeCount, safeLogEvent, validTime, validatedBroker } from "@/lib/nodeDiagnostics";
|
||||
|
||||
@@ -52,7 +51,7 @@ function BrokerScope({ title, source, group, topic }: { title: string; source: N
|
||||
</Panel>;
|
||||
}
|
||||
|
||||
export function NodeDiagnostics({ node, mailboxes }: { node: FleetNode; mailboxes: AdminWorkerEmail[] }) {
|
||||
export function NodeDiagnostics({ node }: { node: FleetNode }) {
|
||||
const { data: me } = useMe();
|
||||
const canViewBroker = !!me && hasAdminPerm(me.admin_permissions, AdminPerm.ViewAnalytics);
|
||||
const canViewMailboxes = !!me && hasAdminPerm(me.admin_permissions, AdminPerm.ViewUsers);
|
||||
@@ -121,7 +120,6 @@ export function NodeDiagnostics({ node, mailboxes }: { node: FleetNode; mailboxe
|
||||
<div className="mt-4 overflow-x-auto"><table className="w-full text-left text-xs" aria-label="Redacted node events">
|
||||
<thead><tr className="border-b text-muted-foreground"><th className="p-2">Observed</th><th className="p-2">Level</th><th className="p-2">Evidence</th><th className="p-2">Mailbox</th></tr></thead>
|
||||
<tbody>{events.map((event) => {
|
||||
const mailbox = mailboxes.find((m) => m.id === event.mailbox_id);
|
||||
const mailboxID = isUUID(event.mailbox_id ?? "") ? event.mailbox_id : null;
|
||||
return <tr key={event.id} className="border-b align-top">
|
||||
<td className="whitespace-nowrap p-2"><Timestamp value={event.observed_at} /></td>
|
||||
@@ -129,7 +127,7 @@ export function NodeDiagnostics({ node, mailboxes }: { node: FleetNode; mailboxe
|
||||
<td className="p-2"><div className="font-medium">{eventLabels[event.event].title}</div><div className="mt-1 text-muted-foreground">{event.category.replaceAll("_", " ")}
|
||||
{event.event === "sync_control_plane_held" && safeCount(event.http_status) && event.http_status >= 100 && event.http_status <= 599 && ` · HTTP ${event.http_status}`}
|
||||
{safeCount(event.count) && event.count <= 1000000 && ` · count ${event.count}`}</div></td>
|
||||
<td className="p-2 font-mono">{mailboxID ? mailbox && canViewMailboxes ? <Link className="text-[var(--admin-accent-strong)] underline underline-offset-2" to={`/mailboxes?q=${encodeURIComponent(mailbox.email)}&worker=${node.id}`} title="Open assigned mailbox in the mailbox browser">{mailboxID}</Link> : <span title="Mailbox identity is not in the currently loaded assignment page, or mailbox access is not permitted.">{mailboxID}</span> : "Not provided"}</td>
|
||||
<td className="p-2 font-mono">{mailboxID ? canViewMailboxes ? <Link className="text-[var(--admin-accent-strong)] underline underline-offset-2" to={`/mailboxes?mailbox_id=${encodeURIComponent(mailboxID)}`} title="Look up this exact mailbox, regardless of current assignment">{mailboxID}</Link> : <span title="Mailbox access is not permitted.">{mailboxID}</span> : "Not provided"}</td>
|
||||
</tr>;
|
||||
})}</tbody>
|
||||
</table></div>}
|
||||
|
||||
@@ -190,6 +190,7 @@ export interface AdminMailboxesResult {
|
||||
}
|
||||
|
||||
export interface AdminMailboxSearch {
|
||||
mailbox_id?: string;
|
||||
q?: string;
|
||||
status?: string;
|
||||
provider?: string;
|
||||
|
||||
@@ -232,9 +232,10 @@ Returned when:
|
||||
|
||||
This backend flow is separate from workspace API-key authorization. Platform
|
||||
admin access uses a normal bearer session, never an organization API key.
|
||||
The admin UI and CLI implement this contract in a separate rollout stage;
|
||||
these endpoints alone do not provide a consent page or CLI command. The CLI
|
||||
never listens on an HTTP port or receives a browser redirect.
|
||||
Use [remote `warmblyctl` login](/development/warmblyctl/#remote-administrator-commands)
|
||||
and the administrator website's `/device` consent page. Deploy the backend
|
||||
before upgrading the admin UI and CLI. The CLI never listens on an HTTP port
|
||||
or receives a browser redirect.
|
||||
|
||||
### Endpoint contract
|
||||
|
||||
@@ -298,8 +299,8 @@ and request. A different session cannot spend it. If decide returns
|
||||
again; never auto-approve when login/reauth returns.
|
||||
|
||||
Poll no faster than every five seconds. Pending/denied responses contain
|
||||
`status: "pending"` or `"denied"`, no `token`; the existing UUID serializer may
|
||||
include a zero `user_id`, which is not an identity. An approving poll returns:
|
||||
`status: "pending"` or `"denied"`, with neither `token` nor `user_id`.
|
||||
An approving poll returns:
|
||||
|
||||
```json
|
||||
{
|
||||
|
||||
@@ -623,6 +623,7 @@ Alongside them, `api_url` is this API's own public base (for a copyable example
|
||||
- `GET /admin/warmup-content/overview` includes effective generation controls and requires `view_warmup_pool`; `PUT /admin/warmup-content/settings` writes effective controls with `manage_settings`. These are platform-admin routes, not organization API-key scopes. See [effective generation policy and stopping](/development/warmup-generation/).
|
||||
- `POST /admin/organizations/:id/sample-data`: `manage_testers` and an MFA-verified admin session. Adds the [reviewer sample dataset](/development/accounts-and-access/#adding-sample-data) only to an active dedicated Test workspace. No request body. Returns `200` with `organization_id`, `created` (false when already seeded) and `seeded_at`; retries preserve edits and deletions. An ordinary workspace, revoked or expired tester, or missing/expired managed Test grant returns `400`. Inserts and the admin audit marker commit together. No mailbox connection, email delivery, external request or credit spend occurs.
|
||||
- `GET /admin/testers`: `view_users` and an MFA-verified admin session. Each tester in `data` may include `test_workspace_id` for its owned dedicated Test workspace and `sample_data_seeded_at` when populated. These fields are absent for a tester merely attached to an ordinary workspace.
|
||||
- `GET /admin/mailboxes`: `view_users` and an MFA-verified admin session. Optional `mailbox_id` filters by exact UUID; `status=all` includes inactive mailboxes. Node evidence links use `/mailboxes?mailbox_id=UUID`, regardless of loaded worker assignments or earlier browse filters. Deleted identities return an empty result, not a substitute mailbox.
|
||||
|
||||
`POST /subscription/checkout`, `POST /subscription/portal`, `POST /subscription/cancel`, `POST /subscription/change-plan` and `GET /subscription/preview-change` require the `MANAGE_BILLING` organization permission; reading the subscription, its limits, trial and features needs only membership. `POST /subscription/portal` requires an existing Stripe billing customer. A workspace without one receives `400` with code `bad_request` and a message to complete checkout first; free and operator-granted plans alone do not create a billing customer.
|
||||
|
||||
|
||||
@@ -435,6 +435,7 @@ type AdminMailboxesResult struct {
|
||||
// All fields optional; empty status = active, "all" returns every
|
||||
// status. provider lets ops filter to "all gmail mailboxes" quickly.
|
||||
type AdminMailboxSearch struct {
|
||||
MailboxID *ParamUUID `form:"mailbox_id"`
|
||||
Query string `form:"q"`
|
||||
Status string `form:"status"`
|
||||
Provider string `form:"provider"`
|
||||
|
||||
@@ -38,6 +38,20 @@ func TestParamUUIDRejectsGarbage(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestAdminMailboxExactIdentityBindsAndRejectsInvalidUUID(t *testing.T) {
|
||||
id := uuid.New()
|
||||
var search AdminMailboxSearch
|
||||
if err := bindQuery(t, "mailbox_id="+id.String()+"&status=all", &search); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if search.MailboxID == nil || search.MailboxID.UUID != id || search.Status != "all" || search.WorkerID != nil {
|
||||
t.Fatalf("exact mailbox lookup changed identity or placement scope: %+v", search)
|
||||
}
|
||||
if err := bindQuery(t, "mailbox_id=not-a-uuid", &AdminMailboxSearch{}); err == nil {
|
||||
t.Fatal("invalid exact mailbox identity accepted")
|
||||
}
|
||||
}
|
||||
|
||||
// Every admin search struct must bind its ids and cursor together, so a field
|
||||
// typed as a bare uuid.UUID cannot 400 an explorer again.
|
||||
func TestAdminSearchStructsBindIDsAndCursor(t *testing.T) {
|
||||
|
||||
@@ -1913,6 +1913,11 @@ func (r *adminRepository) SearchMailboxesForAdmin(ctx context.Context, search *m
|
||||
args := []interface{}{}
|
||||
argNum := 1
|
||||
where := "WHERE 1=1"
|
||||
if search.MailboxID != nil {
|
||||
where += " AND ea.id = $" + itoa(argNum)
|
||||
args = append(args, search.MailboxID.UUID)
|
||||
argNum++
|
||||
}
|
||||
|
||||
if search.Query != "" {
|
||||
where += ` AND (ea.email ILIKE $` + itoa(argNum) +
|
||||
|
||||
@@ -122,7 +122,7 @@ test("admin CLI consent and redacted node diagnostics with disclosed local respo
|
||||
await expect(logs.getByRole("table", { name: "Redacted node events", exact: true })).toBeVisible();
|
||||
await expect(logs.getByText("Control-plane sync held", { exact: true })).toBeVisible();
|
||||
await expect(logs).toContainText("HTTP 503"); await expect(logs).toContainText("Partial coverage"); await expect(logs).toContainText("per process run");
|
||||
await expect(logs.getByRole("link", { name: mailbox, exact: true })).toHaveAttribute("href", new RegExp("/mailboxes\\?q=fixture-mailbox%40example.test"));
|
||||
await expect(logs.getByRole("link", { name: mailbox, exact: true })).toHaveAttribute("href", `/mailboxes?mailbox_id=${mailbox}`);
|
||||
await proof.chapter("Redacted operational evidence", "All node capture/log/offset responses are synthetic localhost fixtures. They do not establish production sampling, provider health, delivery or the cause of an arrival 503.");
|
||||
await logs.getByRole("table", { name: "Redacted node events", exact: true }).scrollIntoViewIfNeeded();
|
||||
await proof.shot("admin-node-redacted-evidence", { caption: "Synthetic capture and allowlisted events: timestamps, partial coverage, retention bounds, three known per-run drops, numeric control-plane HTTP 503 and native mailbox link." });
|
||||
|
||||
Reference in New Issue
Block a user