Skip to content

Commit a503103

Browse files
prazgaitisclaude
andauthored
Fix payment admin page: show masked keys and populate form on load (#272)
The admin payments page had three bugs: form didn't load existing config values (publishable keys, price, test mode), secret keys were never displayed even masked, and allowCustomAmount wasn't returned from the query. Now the query decrypts+masks secret keys for display, the form populates on load, and each secret field shows its current masked value. Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 4ae8e61 commit a503103

3 files changed

Lines changed: 55 additions & 5 deletions

File tree

apps/web/app/challenges/[id]/admin/payments/page.tsx

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -79,12 +79,18 @@ export default function PaymentsAdminPage() {
7979
const toggleTestMode = useMutation(api.mutations.paymentConfig.toggleTestMode);
8080
const testConnection = useAction(api.actions.payments.testStripeConnection);
8181

82-
// Sync allowCustomAmount from paymentConfig when it loads
82+
// Populate form with existing config values when they load
8383
useEffect(() => {
84-
if (paymentConfig !== undefined) {
84+
if (paymentConfig !== undefined && paymentConfig !== null) {
8585
setFormData((prev) => ({
8686
...prev,
87-
allowCustomAmount: paymentConfig?.allowCustomAmount ?? false,
87+
stripePublishableKey: paymentConfig.stripePublishableKey ?? "",
88+
stripeTestPublishableKey: paymentConfig.stripeTestPublishableKey ?? "",
89+
testMode: paymentConfig.testMode ?? true,
90+
priceInDollars: paymentConfig.priceInCents > 0
91+
? (paymentConfig.priceInCents / 100).toFixed(2)
92+
: "",
93+
allowCustomAmount: paymentConfig.allowCustomAmount ?? false,
8894
}));
8995
}
9096
}, [paymentConfig]);
@@ -420,6 +426,9 @@ export default function PaymentsAdminPage() {
420426
placeholder={paymentConfig?.hasTestSecretKey ? "••••••••" : "sk_test_..."}
421427
className="h-8 border-zinc-600 bg-zinc-700 text-xs text-zinc-200"
422428
/>
429+
{paymentConfig?.maskedTestSecretKey && (
430+
<p className="text-[10px] font-mono text-zinc-500">Current: {paymentConfig.maskedTestSecretKey}</p>
431+
)}
423432
</div>
424433
</div>
425434
</div>
@@ -465,6 +474,9 @@ export default function PaymentsAdminPage() {
465474
placeholder={paymentConfig?.hasLiveSecretKey ? "••••••••" : "sk_live_..."}
466475
className="h-8 border-zinc-600 bg-zinc-700 text-xs text-zinc-200"
467476
/>
477+
{paymentConfig?.maskedLiveSecretKey && (
478+
<p className="text-[10px] font-mono text-zinc-500">Current: {paymentConfig.maskedLiveSecretKey}</p>
479+
)}
468480
</div>
469481
</div>
470482
</div>
@@ -490,6 +502,9 @@ export default function PaymentsAdminPage() {
490502
placeholder={paymentConfig?.hasTestWebhookSecret ? "••••••••" : "whsec_..."}
491503
className="h-8 border-zinc-600 bg-zinc-700 text-xs text-zinc-200"
492504
/>
505+
{paymentConfig?.maskedTestWebhookSecret && (
506+
<p className="text-[10px] font-mono text-zinc-500">Current: {paymentConfig.maskedTestWebhookSecret}</p>
507+
)}
493508
</div>
494509
<div className="space-y-1">
495510
<Label className="text-[10px] text-zinc-500">Live Webhook Secret (whsec_...)</Label>
@@ -505,6 +520,9 @@ export default function PaymentsAdminPage() {
505520
placeholder={paymentConfig?.hasWebhookSecret ? "••••••••" : "whsec_..."}
506521
className="h-8 border-zinc-600 bg-zinc-700 text-xs text-zinc-200"
507522
/>
523+
{paymentConfig?.maskedWebhookSecret && (
524+
<p className="text-[10px] font-mono text-zinc-500">Current: {paymentConfig.maskedWebhookSecret}</p>
525+
)}
508526
</div>
509527
</div>
510528
</div>

packages/backend/queries/paymentConfig.ts

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { query, internalQuery } from "../_generated/server";
22
import { v } from "convex/values";
3-
import { maskKey } from "../lib/stripe";
3+
import { decryptKey, maskKey } from "../lib/stripe";
44

55
/**
66
* Get payment configuration for a challenge (admin view with masked keys)
@@ -19,24 +19,40 @@ export const getPaymentConfig = query({
1919
return null;
2020
}
2121

22+
// Helper to safely decrypt and mask a key
23+
const maskedDecrypt = (encrypted: string | undefined): string | null => {
24+
if (!encrypted) return null;
25+
try {
26+
return maskKey(decryptKey(encrypted));
27+
} catch {
28+
return "****";
29+
}
30+
};
31+
2232
// Return config with masked secret keys
2333
return {
2434
id: config._id,
2535
challengeId: config.challengeId,
26-
// Masked keys for display
36+
// Boolean flags for quick checks
2737
hasLiveSecretKey: !!config.stripeSecretKey,
2838
hasLivePublishableKey: !!config.stripePublishableKey,
2939
hasTestSecretKey: !!config.stripeTestSecretKey,
3040
hasTestPublishableKey: !!config.stripeTestPublishableKey,
3141
hasWebhookSecret: !!config.stripeWebhookSecret,
3242
hasTestWebhookSecret: !!config.stripeTestWebhookSecret,
43+
// Masked secret keys for admin display
44+
maskedLiveSecretKey: maskedDecrypt(config.stripeSecretKey),
45+
maskedTestSecretKey: maskedDecrypt(config.stripeTestSecretKey),
46+
maskedWebhookSecret: maskedDecrypt(config.stripeWebhookSecret),
47+
maskedTestWebhookSecret: maskedDecrypt(config.stripeTestWebhookSecret),
3348
// Publishable keys can be shown (they're public)
3449
stripePublishableKey: config.stripePublishableKey || null,
3550
stripeTestPublishableKey: config.stripeTestPublishableKey || null,
3651
// Settings
3752
testMode: config.testMode,
3853
priceInCents: config.priceInCents,
3954
currency: config.currency,
55+
allowCustomAmount: config.allowCustomAmount ?? false,
4056
createdAt: config.createdAt,
4157
updatedAt: config.updatedAt,
4258
};

tasks/fix-payment-admin-page.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
# Fix Payment Admin Page
2+
3+
**Date:** 2026-04-02
4+
5+
## Problems
6+
1. Admin page doesn't populate form with existing payment config values on load
7+
2. Masked secret keys are never displayed (maskKey exists but isn't used)
8+
3. `allowCustomAmount` not returned from `getPaymentConfig` query
9+
4. Saving with empty form can overwrite existing config with zeros
10+
11+
## Implementation
12+
13+
- [x] Update `getPaymentConfig` query to return masked secret keys and `allowCustomAmount`
14+
- [x] Update admin page `useEffect` to populate all form fields from existing config
15+
- [x] Show masked key values in the UI when keys exist
16+
- [x] Set prod Stripe keys for March 2027 challenge via Convex CLI

0 commit comments

Comments
 (0)