mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-08 12:32:43 +00:00
fix(ai): write no key-removal audit for a slot that held no key
This commit is contained in:
1 parent
a3f5f998cf
commit
ce2a909ed5
2 files changed
+43
-11
No files matched your search
@@ -252,6 +252,9 @@ describe("PATCH /api/user/ai-provider — key changes are audited", () => {
|
||||
const patch = PATCH as (req: Request) => Promise<Response>;
|
||||
|
||||
it("audits a removed key, naming its slot and never a value", async () => {
|
||||
vi.mocked(prisma.user.findUnique).mockResolvedValue({
|
||||
aiAnthropicKeyEncrypted: "enc:old",
|
||||
} as never);
|
||||
const res = await patch(patchRequest({ anthropicKey: null }));
|
||||
expect(res.status).toBe(200);
|
||||
expect(vi.mocked(auditLog)).toHaveBeenCalledTimes(1);
|
||||
@@ -265,6 +268,9 @@ describe("PATCH /api/user/ai-provider — key changes are audited", () => {
|
||||
});
|
||||
|
||||
it("audits a stored key without its value", async () => {
|
||||
vi.mocked(prisma.user.findUnique).mockResolvedValue({
|
||||
aiCompatKeyEncrypted: "enc:old",
|
||||
} as never);
|
||||
await patch(patchRequest({ openaiKey: "sk-test-123", compatKey: "" }));
|
||||
const calls = vi.mocked(auditLog).mock.calls;
|
||||
expect(calls.map(([action, opts]) => [action, opts?.details])).toEqual([
|
||||
@@ -274,6 +280,15 @@ describe("PATCH /api/user/ai-provider — key changes are audited", () => {
|
||||
expect(JSON.stringify(calls)).not.toContain("sk-test-123");
|
||||
});
|
||||
|
||||
it("writes no remove audit for a slot that held no key", async () => {
|
||||
vi.mocked(prisma.user.findUnique).mockResolvedValue({
|
||||
aiAnthropicKeyEncrypted: null,
|
||||
} as never);
|
||||
const res = await patch(patchRequest({ anthropicKey: null }));
|
||||
expect(res.status).toBe(200);
|
||||
expect(vi.mocked(auditLog)).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("writes no key audit for a change that touches no key", async () => {
|
||||
await patch(patchRequest({ responseTimeoutSeconds: 120 }));
|
||||
expect(vi.mocked(auditLog)).not.toHaveBeenCalled();
|
||||
|
||||
@@ -372,23 +372,40 @@ export const PATCH = apiHandler(async (request: NextRequest) => {
|
||||
updates.aiBaseUrl = null;
|
||||
}
|
||||
|
||||
// A stored provider key is a credential: setting or removing one is
|
||||
// audited like every other credential change, with the slot it went into
|
||||
// and never the value. A removal used to leave no trace at all. Clearing a
|
||||
// slot that held no key removes nothing and is not audited, so the slots a
|
||||
// removal names are read before the write.
|
||||
const keySlots = [
|
||||
["anthropic", body.anthropicKey, "aiAnthropicKeyEncrypted"],
|
||||
["openai", body.openaiKey, "aiOpenaiKeyEncrypted"],
|
||||
["local", body.localKey, "aiLocalKeyEncrypted"],
|
||||
["openai-compatible", body.compatKey, "aiCompatKeyEncrypted"],
|
||||
] as const;
|
||||
const isRemoval = (value: string | null | undefined) =>
|
||||
value === null || value === "";
|
||||
const storedKeys = keySlots.some(([, value]) => isRemoval(value))
|
||||
? await prisma.user.findUnique({
|
||||
where: { id: user.id },
|
||||
select: {
|
||||
aiAnthropicKeyEncrypted: true,
|
||||
aiOpenaiKeyEncrypted: true,
|
||||
aiLocalKeyEncrypted: true,
|
||||
aiCompatKeyEncrypted: true,
|
||||
},
|
||||
})
|
||||
: null;
|
||||
|
||||
if (Object.keys(updates).length > 0) {
|
||||
await prisma.user.update({ where: { id: user.id }, data: updates });
|
||||
}
|
||||
|
||||
// A stored provider key is a credential: setting or removing one is
|
||||
// audited like every other credential change, with the slot it went into
|
||||
// and never the value. A removal used to leave no trace at all.
|
||||
const keySlots = [
|
||||
["anthropic", body.anthropicKey],
|
||||
["openai", body.openaiKey],
|
||||
["local", body.localKey],
|
||||
["openai-compatible", body.compatKey],
|
||||
] as const;
|
||||
for (const [slot, value] of keySlots) {
|
||||
for (const [slot, value, column] of keySlots) {
|
||||
if (value === undefined) continue;
|
||||
if (isRemoval(value) && storedKeys?.[column] == null) continue;
|
||||
await auditLog(
|
||||
value === null || value === ""
|
||||
isRemoval(value)
|
||||
? "user.ai-provider.key.remove"
|
||||
: "user.ai-provider.key.set",
|
||||
{
|
||||
|
||||
Reference in new issue
Block a user