Skip to content

Commit 735b5a5

Browse files
authored
feat(sponsor-ads): security fixes, UX improvements, and code quality enhancements (#424)
* fix(admin): reset pagination when sponsor ads filters change * fix(security): add admin role verification to sponsor-ads endpoints * refactor(types): use correct type assertion for sponsor ad status checks * fix(api): handle malformed JSON gracefully in reject endpoint * fix(api): hide internal config details in checkout error messages * fix(a11y): add aria-label to sponsor badge when text is hidden * fix(a11y): add Escape key handler and dialog role to reject modal * fix(i18n): translate hard-coded strings in sponsor filters * fix(i18n): add FILTERS and CLEAR_ALL translations to all locales * fix(sponsor-ads): fix type errors and remove duplicate auth checks in admin routes
1 parent a5006a7 commit 735b5a5

File tree

33 files changed

+29373
-27961
lines changed

33 files changed

+29373
-27961
lines changed

app/api/admin/sponsor-ads/[id]/approve/route.ts

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -46,20 +46,13 @@ export async function POST(
4646
try {
4747
const session = await auth();
4848

49-
if (!session?.user?.id) {
49+
if (!session?.user?.isAdmin || !session.user.id) {
5050
return NextResponse.json(
51-
{ success: false, error: "Unauthorized" },
51+
{ success: false, error: "Unauthorized. Admin access required." },
5252
{ status: 401 }
5353
);
5454
}
5555

56-
if (!session.user.isAdmin) {
57-
return NextResponse.json(
58-
{ success: false, error: "Forbidden" },
59-
{ status: 403 }
60-
);
61-
}
62-
6356
const { id } = await params;
6457

6558
// Parse request body for forceApprove flag

app/api/admin/sponsor-ads/[id]/cancel/route.ts

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -61,20 +61,13 @@ export async function POST(
6161
try {
6262
const session = await auth();
6363

64-
if (!session?.user?.id) {
64+
if (!session?.user?.isAdmin) {
6565
return NextResponse.json(
66-
{ success: false, error: "Unauthorized" },
66+
{ success: false, error: "Unauthorized. Admin access required." },
6767
{ status: 401 }
6868
);
6969
}
7070

71-
if (!session.user.isAdmin) {
72-
return NextResponse.json(
73-
{ success: false, error: "Forbidden" },
74-
{ status: 403 }
75-
);
76-
}
77-
7871
const { id } = await params;
7972
const body = await request.json().catch(() => ({}));
8073

app/api/admin/sponsor-ads/[id]/reject/route.ts

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -65,22 +65,15 @@ export async function POST(
6565
try {
6666
const session = await auth();
6767

68-
if (!session?.user?.id) {
68+
if (!session?.user?.isAdmin || !session.user.id) {
6969
return NextResponse.json(
70-
{ success: false, error: "Unauthorized" },
70+
{ success: false, error: "Unauthorized. Admin access required." },
7171
{ status: 401 }
7272
);
7373
}
7474

75-
if (!session.user.isAdmin) {
76-
return NextResponse.json(
77-
{ success: false, error: "Forbidden" },
78-
{ status: 403 }
79-
);
80-
}
81-
8275
const { id } = await params;
83-
const body = await request.json();
76+
const body = await request.json().catch(() => ({}));
8477

8578
// Validate request body
8679
const validationResult = rejectSponsorAdSchema.safeParse({

app/api/admin/sponsor-ads/[id]/route.ts

Lines changed: 4 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -34,20 +34,13 @@ export async function GET(
3434
try {
3535
const session = await auth();
3636

37-
if (!session?.user?.id) {
37+
if (!session?.user?.isAdmin) {
3838
return NextResponse.json(
39-
{ success: false, error: "Unauthorized" },
39+
{ success: false, error: "Unauthorized. Admin access required." },
4040
{ status: 401 }
4141
);
4242
}
4343

44-
if (!session.user.isAdmin) {
45-
return NextResponse.json(
46-
{ success: false, error: "Forbidden" },
47-
{ status: 403 }
48-
);
49-
}
50-
5144
const { id } = await params;
5245
const sponsorAd = await sponsorAdService.getSponsorAdWithUser(id);
5346

@@ -103,20 +96,13 @@ export async function DELETE(
10396
try {
10497
const session = await auth();
10598

106-
if (!session?.user?.id) {
99+
if (!session?.user?.isAdmin) {
107100
return NextResponse.json(
108-
{ success: false, error: "Unauthorized" },
101+
{ success: false, error: "Unauthorized. Admin access required." },
109102
{ status: 401 }
110103
);
111104
}
112105

113-
if (!session.user.isAdmin) {
114-
return NextResponse.json(
115-
{ success: false, error: "Forbidden" },
116-
{ status: 403 }
117-
);
118-
}
119-
120106
const { id } = await params;
121107

122108
await sponsorAdService.deleteSponsorAd(id);

app/api/admin/sponsor-ads/route.ts

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -63,20 +63,13 @@ export async function GET(request: NextRequest) {
6363
try {
6464
const session = await auth();
6565

66-
if (!session?.user?.id) {
66+
if (!session?.user?.isAdmin) {
6767
return NextResponse.json(
68-
{ success: false, error: "Unauthorized" },
68+
{ success: false, error: "Unauthorized. Admin access required." },
6969
{ status: 401 }
7070
);
7171
}
7272

73-
if (!session.user.isAdmin) {
74-
return NextResponse.json(
75-
{ success: false, error: "Forbidden" },
76-
{ status: 403 }
77-
);
78-
}
79-
8073
const { searchParams } = new URL(request.url);
8174

8275
// Validate pagination parameters

app/api/sponsor-ads/checkout/route.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -119,10 +119,11 @@ export async function POST(request: NextRequest) {
119119
const priceId = getPriceId(sponsorAd.interval, ACTIVE_PAYMENT_PROVIDER);
120120

121121
if (!priceId) {
122+
console.error(`Price not configured for ${sponsorAd.interval} interval with ${ACTIVE_PAYMENT_PROVIDER} provider`);
122123
return NextResponse.json(
123124
{
124125
success: false,
125-
error: `Price not configured for ${sponsorAd.interval} interval with ${ACTIVE_PAYMENT_PROVIDER} provider`
126+
error: "Payment configuration is incomplete. Please contact support."
126127
},
127128
{ status: 400 }
128129
);
@@ -167,8 +168,9 @@ export async function POST(request: NextRequest) {
167168
break;
168169

169170
default:
171+
console.error(`Unsupported payment provider: ${ACTIVE_PAYMENT_PROVIDER}`);
170172
return NextResponse.json(
171-
{ success: false, error: `Unsupported payment provider: ${ACTIVE_PAYMENT_PROVIDER}` },
173+
{ success: false, error: "Payment configuration is incomplete. Please contact support." },
172174
{ status: 400 }
173175
);
174176
}

components/admin/sponsorships/reject-modal.tsx

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { useEffect } from 'react';
12
import { Button, Textarea } from '@heroui/react';
23
import { XCircle } from 'lucide-react';
34
import { useTranslations } from 'next-intl';
@@ -34,13 +35,30 @@ export function RejectModal({
3435
}: RejectModalProps) {
3536
const t = useTranslations('admin.SPONSORSHIPS');
3637

38+
// Handle Escape key to close modal
39+
useEffect(() => {
40+
const handleEscape = (e: KeyboardEvent) => {
41+
if (e.key === 'Escape' && !isSubmitting) {
42+
onClose();
43+
}
44+
};
45+
46+
if (isOpen) {
47+
document.addEventListener('keydown', handleEscape);
48+
}
49+
50+
return () => {
51+
document.removeEventListener('keydown', handleEscape);
52+
};
53+
}, [isOpen, isSubmitting, onClose]);
54+
3755
if (!isOpen) return null;
3856

3957
const isReasonValid = rejectionReason.length >= 10;
4058

4159
return (
4260
<div className={MODAL_OVERLAY}>
43-
<div className={MODAL_CONTAINER}>
61+
<div className={MODAL_CONTAINER} role="dialog" aria-modal="true">
4462
{/* Header */}
4563
<div className={MODAL_HEADER}>
4664
<div className="flex items-center space-x-3">

components/admin/sponsorships/sponsor-filters.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ export function SponsorFilters({
5757
<div className="flex items-center justify-between">
5858
<div className="flex items-center space-x-2">
5959
<Filter className="w-5 h-5 text-gray-400" />
60-
<span className="font-medium text-gray-900 dark:text-white">Filters</span>
60+
<span className="font-medium text-gray-900 dark:text-white">{t('FILTERS')}</span>
6161
{activeFilterCount > 0 && (
6262
<span className="px-2 py-0.5 text-xs font-medium bg-theme-primary text-white rounded-full">
6363
{activeFilterCount}
@@ -72,7 +72,7 @@ export function SponsorFilters({
7272
onPress={onClearFilters}
7373
startContent={<X className="w-4 h-4" />}
7474
>
75-
Clear All
75+
{t('CLEAR_ALL')}
7676
</Button>
7777
)}
7878
</div>

components/sponsor-ads/sponsor-badge.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ export function SponsorBadge({
4444
sizeClasses[size],
4545
className
4646
)}
47+
aria-label={!showText ? label : undefined}
4748
>
4849
{showIcon && <Megaphone className={cn(iconSizes[size])} />}
4950
{showText && <span className="font-medium">{label}</span>}
@@ -59,6 +60,7 @@ export function SponsorBadge({
5960
sizeClasses[size],
6061
className
6162
)}
63+
aria-label={!showText ? label : undefined}
6264
>
6365
{showIcon && <Megaphone className={cn(iconSizes[size])} />}
6466
{showText && <span className="font-medium">{label}</span>}
@@ -75,6 +77,7 @@ export function SponsorBadge({
7577
sizeClasses[size],
7678
className
7779
)}
80+
aria-label={!showText ? label : undefined}
7881
>
7982
{showIcon && <Megaphone className={cn(iconSizes[size], 'mr-1')} />}
8083
{showText && label}

hooks/use-admin-sponsor-ads.ts

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,32 @@ export function useAdminSponsorAds(
209209
const [sortBy, setSortBy] = useState<SponsorAdSortBy>(initialSortBy);
210210
const [sortOrder, setSortOrder] = useState<"asc" | "desc">(initialSortOrder);
211211

212+
// Wrapped filter setters that reset pagination
213+
const handleSetStatusFilter = useCallback((status: SponsorAdStatus | undefined) => {
214+
setStatusFilter(status);
215+
setCurrentPage(1);
216+
}, []);
217+
218+
const handleSetIntervalFilter = useCallback((interval: SponsorAdIntervalType | undefined) => {
219+
setIntervalFilter(interval);
220+
setCurrentPage(1);
221+
}, []);
222+
223+
const handleSetSearchTerm = useCallback((term: string) => {
224+
setSearchTerm(term);
225+
setCurrentPage(1);
226+
}, []);
227+
228+
const handleSetSortBy = useCallback((newSortBy: SponsorAdSortBy) => {
229+
setSortBy(newSortBy);
230+
setCurrentPage(1);
231+
}, []);
232+
233+
const handleSetSortOrder = useCallback((order: "asc" | "desc") => {
234+
setSortOrder(order);
235+
setCurrentPage(1);
236+
}, []);
237+
212238
// Query client for cache management
213239
const queryClient = useQueryClient();
214240

@@ -382,11 +408,11 @@ export function useAdminSponsorAds(
382408
deleteSponsorAd: handleDelete,
383409

384410
// Filter actions
385-
setStatusFilter,
386-
setIntervalFilter,
387-
setSearchTerm,
388-
setSortBy,
389-
setSortOrder,
411+
setStatusFilter: handleSetStatusFilter,
412+
setIntervalFilter: handleSetIntervalFilter,
413+
setSearchTerm: handleSetSearchTerm,
414+
setSortBy: handleSetSortBy,
415+
setSortOrder: handleSetSortOrder,
390416
setCurrentPage,
391417

392418
// Utility

0 commit comments

Comments
 (0)