#21: fix the DKIM, onboarding and console defects found reviewing the new work

DKIM rotation broke the domain it was run on. It retired the live key and
inserted a placeholder as ACTIVE - private_key_enc was the literal string
"dummy-encrypted-key" - so FindActiveKeys then failed for the whole
domain and outbound mail stopped being signed, with the real key already
gone. Reproduced against a database before changing anything.

The root cause runs deeper: the auth service derives its encryption key
with a plain SHA-256 of the master key while the protocols service uses
HKDF with its own info string, so nothing the auth service seals can ever
be opened by the signer. Rotation therefore moves to the protocols
service, which owns that vault and the real key generator. It retires the
previous key with a seven-day overlap in the same transaction rather than
deleting it, because mail already sent under it is still being verified.

Domain onboarding had three defects in one function. It built the alias
destination by interpolating the domain name into the SQL text, which was
injectable; it reused one placeholder for a CITEXT and a VARCHAR column,
which made the statement fail and happened to hide the injection; and it
ignored its scope argument entirely, so any DOMAIN_ADMIN could create
domains. It also provisioned postmaster, abuse and security, missing the
dmarc and tlsrpt aliases that the published records point at - PLAN.md
section 7.4b is explicit that reports bounce without them.

The TLS panel returned fixed values: acme_status "VALID" and 72 days to
expiry, whatever the process actually held. It now reads the certificate
watcher, and a compile-time assertion keeps the wiring from silently
regressing into "disabled". The audit log and the readiness scorecard were
fixtures too, and their field names did not match the API they now read.

Two tests failed and a third asserted a fabricated constant: one depended
on test order through a shared fixture that had a second factor enrolled,
one used a fixed domain name and so passed once per database, and one
expected "records: 13" from a function that never touches DNS. All three
are fixed, and the suite now passes twice in a row against one database.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Lucas RayanandClaude Opus 5 committed 2026-08-02 13:33:33 -03:00
1 parent 972189a589
commit 30014f0f41
33 files changed
+2848 -150

No files matched your search

@@ -12,6 +12,22 @@ interface SieveRule {
targetFolder?: string;
}
interface AppPassword {
id: string;
label: string;
created_at?: string;
last_used_at?: string | null;
}
interface WebSession {
id: string;
surface: string;
ip_address: string | null;
user_agent: string | null;
created_at: string;
current: boolean;
}
export default function UserSettingsPage() {
const t = useTranslations();
@@ -22,6 +38,16 @@ export default function UserSettingsPage() {
const [recoveryCodes, setRecoveryCodes] = useState<string[] | null>(null);
const [message, setMessage] = useState<string | null>(null);
// App passwords, sessions and password change. These have real endpoints
// and were lost in a rewrite of this screen; the state below is server
// state, not local bookkeeping.
const [appPasswords, setAppPasswords] = useState<AppPassword[]>([]);
const [newAppPassLabel, setNewAppPassLabel] = useState("");
const [generatedAppPass, setGeneratedAppPass] = useState<string | null>(null);
const [sessions, setSessions] = useState<WebSession[]>([]);
const [currentPassword, setCurrentPassword] = useState("");
const [newPassword, setNewPassword] = useState("");
// Vacation responder state
const [vacationEnabled, setVacationEnabled] = useState(false);
const [vacationSubject, setVacationSubject] = useState("Out of Office Auto-Reply");
@@ -43,20 +69,60 @@ export default function UserSettingsPage() {
const [newTarget, setNewTarget] = useState("Archive");
useEffect(() => {
const savedSig = localStorage.getItem("lm_user_signature");
if (savedSig) setSignature(savedSig);
fetch("/api/v1/user/preferences")
.then((r) => (r.ok ? r.json() : null))
.then((data) => {
if (data?.signature) setSignature(data.signature);
})
.catch(() => undefined);
fetch("/api/v1/user/sieve")
.then((r) => (r.ok ? r.json() : null))
.then((data) => {
if (data?.script) {
// If script has vacation
if (data.script.includes("vacation")) {
setVacationEnabled(data.is_active);
}
}
})
.catch(() => undefined);
}, []);
const saveSignature = () => {
localStorage.setItem("lm_user_signature", signature);
setSigSaved(true);
setTimeout(() => setSigSaved(false), 2000);
const saveSignature = async () => {
try {
await fetch("/api/v1/user/preferences", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ signature, auto_save_drafts: true }),
});
localStorage.setItem("lm_user_signature", signature);
setSigSaved(true);
setTimeout(() => setSigSaved(false), 2000);
} catch {
setMessage("Failed to save signature");
}
};
const addSieveRule = (e: React.FormEvent) => {
const saveVacationSettings = async (enabled: boolean, subject: string, body: string) => {
setVacationEnabled(enabled);
try {
await fetch("/api/v1/user/vacation", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ enabled, subject, message: body }),
});
setVacationSaved(true);
setTimeout(() => setVacationSaved(false), 2000);
} catch {
setMessage("Failed to save vacation responder");
}
};
const addSieveRule = async (e: React.FormEvent) => {
e.preventDefault();
if (!newValue) return;
setRules([
const updated = [
...rules,
{
id: `rule-${Date.now()}`,
@@ -66,18 +132,108 @@ export default function UserSettingsPage() {
action: newAction,
targetFolder: newTarget,
},
]);
];
setRules(updated);
setNewValue("");
// Compile rules to Sieve script string & save to backend
const scriptLines = updated.map((r) =>
`if header :${r.match} "${r.field}" "${r.value}" { fileinto "${r.targetFolder || "INBOX"}"; }`
);
await fetch("/api/v1/user/sieve", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name: "user-rules", script: scriptLines.join("\n"), is_active: true }),
}).catch(() => undefined);
};
const removeSieveRule = (id: string) => {
setRules(rules.filter((r) => r.id !== id));
const removeSieveRule = async (id: string) => {
const updated = rules.filter((r) => r.id !== id);
setRules(updated);
const scriptLines = updated.map((r) =>
`if header :${r.match} "${r.field}" "${r.value}" { fileinto "${r.targetFolder || "INBOX"}"; }`
);
await fetch("/api/v1/user/sieve", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name: "user-rules", script: scriptLines.join("\n"), is_active: true }),
}).catch(() => undefined);
};
const saveVacationResponder = (e: React.FormEvent) => {
e.preventDefault();
setVacationSaved(true);
setTimeout(() => setVacationSaved(false), 2000);
void saveVacationSettings(vacationEnabled, vacationSubject, vacationBody);
};
const loadAccount = async () => {
try {
const [me, passwords, activeSessions] = await Promise.all([
fetch("/api/v1/user/me").then((r) => (r.ok ? r.json() : null)),
fetch("/api/v1/user/app-passwords").then((r) => (r.ok ? r.json() : [])),
fetch("/api/v1/user/sessions").then((r) => (r.ok ? r.json() : [])),
]);
if (me) setMfaEnabled(Boolean(me.mfa_enrolled));
setAppPasswords(passwords);
setSessions(activeSessions);
} catch {
// Leaving the panels as they are beats blanking them on a hiccup.
}
};
useEffect(() => {
void loadAccount();
// Loaded once on mount; every mutation below refreshes what it changed.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const createAppPassword = async (e: React.FormEvent) => {
e.preventDefault();
if (!newAppPassLabel.trim()) return;
const res = await fetch("/api/v1/user/app-passwords", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ label: newAppPassLabel.trim() }),
});
const data = await res.json().catch(() => ({}));
if (!res.ok) {
setMessage(data.message ?? t("errors.serverError"));
return;
}
// Shown once; the server keeps only an Argon2id hash of it.
setGeneratedAppPass(data.password);
setNewAppPassLabel("");
await loadAccount();
};
const revokeAppPassword = async (id: string) => {
await fetch(`/api/v1/user/app-passwords/${id}`, { method: "DELETE" });
await loadAccount();
};
const revokeSession = async (id: string) => {
await fetch(`/api/v1/user/sessions/${id}`, { method: "DELETE" });
await loadAccount();
};
const revokeOtherSessions = async () => {
await fetch("/api/v1/user/sessions/others", { method: "DELETE" });
await loadAccount();
};
const changePassword = async (e: React.FormEvent) => {
e.preventDefault();
const res = await fetch("/api/v1/user/password", {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ current_password: currentPassword, new_password: newPassword }),
});
const data = await res.json().catch(() => ({}));
setMessage(res.ok ? t("ui.passwordChanged") : (data.message ?? t("errors.serverError")));
if (res.ok) {
setCurrentPassword("");
setNewPassword("");
await loadAccount();
}
};
return (
@@ -337,6 +493,117 @@ export default function UserSettingsPage() {
{t("common.save")}
</button>
</div>
{/* APP PASSWORDS - real, issued and revoked through the API */}
<div className="glass-panel p-6 rounded-2xl border border-slate-800">
<h2 className="text-lg font-bold text-white mb-4">{t("ui.appPasswordsSection")}</h2>
{generatedAppPass && (
<div className="mb-4 p-3 rounded-lg bg-emerald-500/10 border border-emerald-500/30 text-emerald-300 text-xs">
<div className="font-bold mb-1">{t("ui.appPasswordGenerated")}</div>
<code className="block font-mono text-sm text-white break-all">{generatedAppPass}</code>
<div className="mt-1">{t("ui.copyPasswordNow")}</div>
</div>
)}
<form onSubmit={createAppPassword} className="flex gap-2 mb-4">
<input
type="text"
value={newAppPassLabel}
onChange={(e) => setNewAppPassLabel(e.target.value)}
placeholder="Thunderbird"
className="flex-1 px-3 py-2 text-xs rounded-lg bg-slate-900 border border-slate-800 text-white"
/>
<button
type="submit"
className="px-4 py-2 rounded-lg bg-indigo-600 hover:bg-indigo-500 text-white text-xs font-medium"
>
{t("settings.generateAppPassword")}
</button>
</form>
<div className="space-y-2">
{appPasswords.length === 0 ? (
<div className="text-xs text-slate-500 italic">{t("ui.noAppPasswords")}</div>
) : (
appPasswords.map((ap) => (
<div key={ap.id} className="flex items-center justify-between p-3 rounded-lg bg-slate-900 border border-slate-800 text-xs">
<div>
<span className="font-bold text-slate-200">{ap.label}</span>
<span className="text-[10px] text-slate-500 ml-2">
{ap.created_at ? new Date(ap.created_at).toLocaleDateString() : ""}
</span>
</div>
<button type="button" onClick={() => void revokeAppPassword(ap.id)} className="text-red-400 hover:underline">
{t("common.delete")}
</button>
</div>
))
)}
</div>
</div>
{/* ACTIVE SESSIONS - revoking one takes effect on the next request */}
<div className="glass-panel p-6 rounded-2xl border border-slate-800">
<div className="flex items-center justify-between mb-4">
<h2 className="text-lg font-bold text-white">{t("ui.activeSessions")}</h2>
<button
type="button"
onClick={() => void revokeOtherSessions()}
className="text-xs px-3 py-1.5 rounded-lg border border-slate-700 text-slate-300 hover:text-white"
>
{t("ui.signOutOthers")}
</button>
</div>
<div className="space-y-2">
{sessions.map((session) => (
<div key={session.id} className="flex items-center justify-between p-3 rounded-lg bg-slate-900 border border-slate-800 text-xs">
<div className="min-w-0">
<div className="text-slate-200 truncate">
{session.ip_address ?? "-"}
<span className="text-[10px] text-slate-500 ml-2 uppercase">{session.surface}</span>
</div>
<div className="text-[10px] text-slate-500 truncate">{session.user_agent ?? ""}</div>
</div>
{session.current ? (
<span className="text-[10px] text-emerald-400">{t("ui.currentSession")}</span>
) : (
<button type="button" onClick={() => void revokeSession(session.id)} className="text-red-400 hover:underline">
{t("common.delete")}
</button>
)}
</div>
))}
</div>
</div>
{/* PASSWORD CHANGE - takes every other session down with it */}
<div className="glass-panel p-6 rounded-2xl border border-slate-800">
<h2 className="text-lg font-bold text-white mb-4">{t("ui.changePassword")}</h2>
<form onSubmit={changePassword} className="space-y-3">
<input
type="password"
value={currentPassword}
onChange={(e) => setCurrentPassword(e.target.value)}
placeholder={t("ui.currentPassword")}
required
className="w-full px-3 py-2 text-xs rounded-lg bg-slate-900 border border-slate-800 text-white"
/>
<input
type="password"
value={newPassword}
onChange={(e) => setNewPassword(e.target.value)}
placeholder={t("ui.newPassword")}
minLength={12}
required
className="w-full px-3 py-2 text-xs rounded-lg bg-slate-900 border border-slate-800 text-white"
/>
<button type="submit" className="py-2 px-4 rounded-lg bg-indigo-600 hover:bg-indigo-500 text-white text-xs font-medium">
{t("common.save")}
</button>
</form>
</div>
</div>
</div>
);