Skip to content

Commit 676d5cf

Browse files
committed
fix eventdetailspage user auth
1 parent f12b497 commit 676d5cf

File tree

5 files changed

+110
-77
lines changed

5 files changed

+110
-77
lines changed

backend/apps/core/auth.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,41 @@ def _wrapped_view(request, *args, **kwargs):
5959
return _wrapped_view
6060

6161

62+
def optional_jwt(view_func):
63+
"""
64+
Optional JWT authentication decorator.
65+
If a token is provided and valid, populates request.auth_payload.
66+
If no token or invalid token, continues without error (for public endpoints).
67+
"""
68+
@wraps(view_func)
69+
def _wrapped_view(request, *args, **kwargs):
70+
try:
71+
state = authenticate_request(
72+
request,
73+
AuthenticateRequestOptions(
74+
secret_key=CLERK_SECRET_KEY,
75+
authorized_parties=CLERK_AUTHORIZED_PARTIES,
76+
),
77+
)
78+
79+
if state.is_signed_in:
80+
request.auth_payload = state.payload
81+
django_user = AnonymousUser()
82+
django_user.username = state.payload.get("sub") or "clerk_user"
83+
request.user = django_user
84+
else:
85+
# No token or invalid token - that's fine, just continue
86+
request.auth_payload = {}
87+
request.user = AnonymousUser()
88+
except Exception:
89+
# Any error during auth - that's fine, just continue
90+
request.auth_payload = {}
91+
request.user = AnonymousUser()
92+
93+
return view_func(request, *args, **kwargs)
94+
return _wrapped_view
95+
96+
6297
def admin_required(view_func):
6398
@wraps(view_func)
6499
@jwt_required

backend/apps/events/views.py

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
from datetime import timedelta
33
import pytz
44

5-
from apps.core.auth import jwt_required, admin_required
5+
from apps.core.auth import jwt_required, admin_required, optional_jwt
66
from django.conf import settings
77
from django.db import transaction
88
from django.db.models import Q
@@ -188,6 +188,7 @@ def get_events(request):
188188

189189
@api_view(["GET"])
190190
@permission_classes([AllowAny])
191+
@optional_jwt
191192
@ratelimit(key="ip", rate="60/hr", block=True)
192193
def get_event(request, event_id):
193194
"""Get a single event by ID with upcoming dates"""
@@ -206,12 +207,22 @@ def get_event(request, event_id):
206207

207208
event_data["display_handle"] = events_utils.determine_display_handle(event_data)
208209

209-
# Get upcoming event dates
210-
upcoming_dates = EventDates.objects.filter(
211-
event_id=event_id, dtstart_utc__gte=timezone.now()
212-
).order_by('dtstart_utc')[:10].values("dtstart_utc", "dtend_utc")
210+
# Check if user is submitter or admin (optional_jwt sets auth_payload)
211+
auth_payload = request.auth_payload
212+
user_id = auth_payload.get("sub") or auth_payload.get("id")
213+
is_admin = auth_payload.get("role") == "admin"
214+
215+
submission = EventSubmission.objects.filter(created_event_id=event_id).first()
216+
is_submitter = submission and user_id and str(submission.submitted_by) == str(user_id)
217+
218+
event_data["is_submitter"] = is_submitter
219+
event_data["screenshot_url"] = submission.screenshot_url if (is_admin or is_submitter) and submission else None
213220

214-
event_data["upcoming_dates"] = list(upcoming_dates)
221+
# Get upcoming event dates
222+
event_data["upcoming_dates"] = list(
223+
EventDates.objects.filter(event_id=event_id, dtstart_utc__gte=timezone.now())
224+
.order_by('dtstart_utc')[:10].values("dtstart_utc", "dtend_utc")
225+
)
215226

216227
return Response(event_data)
217228

frontend/src/features/events/hooks/useEvents.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ export function useEvents() {
3131
hasNextPage,
3232
isFetchingNextPage,
3333
} = useInfiniteQuery<EventsResponse, Error, any, string[], string | undefined>({
34-
queryKey: ["events", searchTerm, categories, dtstart_utc, addedAt, view],
34+
queryKey: ["events", searchTerm, categories, dtstart_utc, addedAt, view, showInterested ? "interested" : ""],
3535
queryFn: async ({ pageParam }: { pageParam: string | undefined }) => {
3636
const queryParams: Record<string, any> = {
3737
limit: 30, // Load 30 events at a time
@@ -62,6 +62,12 @@ export function useEvents() {
6262
queryParams.all = true;
6363
}
6464

65+
// When viewing Interested, reveal all interested from 2025-01-01
66+
if (showInterested) {
67+
queryParams.all = true;
68+
queryParams.dtstart_utc = formatDtstartToMidnight("2025-01-01");
69+
}
70+
6571
return eventsAPIClient.getEvents(queryParams);
6672
},
6773
initialPageParam: undefined as string | undefined,
@@ -169,10 +175,14 @@ export function useEvents() {
169175
const nextParams = new URLSearchParams(prev);
170176

171177
if (showInterested) {
178+
// When deselecting interested, remove all filters to show upcoming events
172179
nextParams.delete("interested");
180+
nextParams.delete("dtstart_utc");
181+
nextParams.delete("added_at");
173182
} else {
174183
nextParams.set("interested", "true");
175-
nextParams.delete("dtstart_utc");
184+
// Reveal all interested from 2025-01-01
185+
nextParams.set("dtstart_utc", formatDtstartToMidnight("2025-01-01"));
176186
nextParams.delete("added_at");
177187
}
178188
return nextParams;

frontend/src/features/events/pages/EventDetailPage.tsx

Lines changed: 45 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { useParams, useNavigate } from "react-router-dom";
22
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
33
import { motion } from "framer-motion";
44
import { useState, useEffect } from "react";
5-
import { useAuth } from "@clerk/clerk-react";
5+
import { useAuth, useUser } from "@clerk/clerk-react";
66
import {
77
ArrowLeft,
88
Calendar,
@@ -26,8 +26,12 @@ import { getEventStatus, isEventNew } from "@/shared/lib/eventUtils";
2626
import { Event } from "@/features/events/types/events";
2727
import BadgeMask from "@/shared/components/ui/badge-mask";
2828

29-
const fetchEvent = async (eventId: string): Promise<Event> => {
30-
const response = await fetch(`${API_BASE_URL}/events/${eventId}`);
29+
const fetchEvent = async (eventId: string, token?: string | null): Promise<Event> => {
30+
const headers: HeadersInit = {};
31+
if (token) {
32+
headers.Authorization = `Bearer ${token}`;
33+
}
34+
const response = await fetch(`${API_BASE_URL}/events/${eventId}`, { headers });
3135
if (!response.ok) {
3236
throw new Error("Event not found");
3337
}
@@ -86,6 +90,8 @@ const OrganizationBadge = ({ event }: { event: Event }) => {
8690

8791
function EventDetailPage() {
8892
const { eventId } = useParams<{ eventId: string }>();
93+
const { user } = useUser();
94+
const isAdmin = user?.publicMetadata?.role === 'admin';
8995
const navigate = useNavigate();
9096
const { getToken } = useAuth();
9197
const queryClient = useQueryClient();
@@ -95,45 +101,19 @@ function EventDetailPage() {
95101

96102
const { data: event, isLoading, error } = useQuery({
97103
queryKey: ["event", eventId],
98-
queryFn: () => fetchEvent(eventId!),
99-
enabled: !!eventId,
100-
});
101-
102-
// Fetch submission info to check if user is submitter or admin
103-
const { data: submissionInfo, isLoading: isSubmissionLoading } = useQuery({
104-
queryKey: ["event-submission", eventId],
105104
queryFn: async () => {
106-
try {
107-
const token = await getToken();
108-
const headers: HeadersInit = {};
109-
if (token) {
110-
headers.Authorization = `Bearer ${token}`;
111-
}
112-
113-
const response = await fetch(`${API_BASE_URL}/events/${eventId}/submission/`, {
114-
headers,
115-
});
116-
if (!response.ok) throw new Error("Failed to fetch submission info");
117-
return response.json();
118-
} catch (error) {
119-
console.error("Error fetching submission info:", error);
120-
return { is_submitter: false, is_admin: false };
121-
}
105+
const token = await getToken();
106+
return fetchEvent(eventId!, token);
122107
},
123108
enabled: !!eventId,
124109
});
125110

126111
// Initialize editedData when event loads for pending events or for admins
127-
// Filter to only show user-editable fields
128112
useEffect(() => {
129-
if (event && !editedData && submissionInfo) {
130-
const shouldEdit = (event.status === "PENDING" && (submissionInfo.is_submitter || submissionInfo.is_admin)) || submissionInfo.is_admin;
113+
if (event && !editedData) {
114+
const shouldEdit = (event.status === "PENDING" && (event as any).is_submitter) || isAdmin;
131115
if (shouldEdit) {
132-
// Filter to only user-editable fields (same as submission form)
133-
const allowedFields = new Set([
134-
'title', 'dtstart', 'dtend', 'all_day', 'location',
135-
'price', 'food', 'registration', 'description'
136-
]);
116+
const allowedFields = new Set(['title', 'dtstart', 'dtend', 'all_day', 'location', 'price', 'food', 'registration', 'description']);
137117
const filteredData: any = {};
138118
for (const field of Object.keys(event)) {
139119
if (allowedFields.has(field)) {
@@ -143,7 +123,7 @@ function EventDetailPage() {
143123
setEditedData(filteredData);
144124
}
145125
}
146-
}, [event, submissionInfo, editedData]);
126+
}, [event, editedData, isAdmin]);
147127

148128
// Mutation for updating event
149129
const updateEventMutation = useMutation({
@@ -200,14 +180,11 @@ function EventDetailPage() {
200180
setEditError(null);
201181
};
202182

203-
// Determine if user can edit this event
204-
const canEditEvent = submissionInfo?.is_submitter || submissionInfo?.is_admin;
205-
206-
// Check if event is pending and user cannot view it (only check after submissionInfo is loaded)
207-
const isPendingAndUnauthorized = event?.status === "PENDING" && !canEditEvent && !isSubmissionLoading;
183+
const canEditEvent = (event as any)?.is_submitter || isAdmin;
184+
const isPendingAndUnauthorized = event?.status === "PENDING" && !canEditEvent;
208185

209186
// Show loading while either query is loading
210-
if (isLoading || (event?.status === "PENDING" && isSubmissionLoading)) {
187+
if (isLoading) {
211188
return (
212189
<div className="flex items-center justify-center min-h-[400px]">
213190
<div className="flex items-center space-x-2 text-gray-600 dark:text-gray-400">
@@ -237,7 +214,7 @@ function EventDetailPage() {
237214
}
238215

239216
// Render edit mode for pending events when user has permission, OR for admins regardless of status
240-
if ((event.status === "PENDING" && canEditEvent) || submissionInfo?.is_admin) {
217+
if ((event.status === "PENDING" && canEditEvent) || isAdmin) {
241218
return (
242219
<div className="max-w-4xl mx-auto">
243220
<SEOHead
@@ -255,12 +232,12 @@ function EventDetailPage() {
255232
</div>
256233

257234
{/* Original Screenshot (if available for submitter/admin) */}
258-
{submissionInfo?.screenshot_url && (
235+
{(event as any)?.screenshot_url && (
259236
<div className="mb-6 bg-white dark:bg-gray-800 rounded-lg shadow-lg p-6">
260237
<h2 className="text-xl font-bold text-gray-900 dark:text-white mb-4">Original Screenshot</h2>
261238
<div className="relative w-full max-w-2xl mx-auto">
262239
<img
263-
src={submissionInfo.screenshot_url}
240+
src={(event as any).screenshot_url}
264241
alt="Original event screenshot"
265242
className="w-full h-auto rounded-lg shadow-lg"
266243
/>
@@ -581,20 +558,28 @@ function EventDetailPage() {
581558
</p>
582559
</div>
583560
</div>
584-
{/* Google Maps Embed */}
585-
<div className="w-full h-64 rounded-lg overflow-hidden">
586-
<iframe
587-
width="100%"
588-
height="100%"
589-
style={{ border: 0 }}
590-
loading="lazy"
591-
allowFullScreen
592-
referrerPolicy="no-referrer-when-downgrade"
593-
src={`https://www.google.com/maps/embed/v1/place?key=${
594-
import.meta.env.VITE_GOOGLE_MAPS_API_KEY || ""
595-
}&q=${encodeURIComponent(`${event.location}, ${event.school || ""}`)}`}
596-
></iframe>
597-
</div>
561+
{/* Google Maps Embed - Only show for physical locations */}
562+
{(() => {
563+
const locationLower = event.location.toLowerCase();
564+
const isVirtual = locationLower.includes("virtual") ||
565+
locationLower.includes("zoom") ||
566+
locationLower.includes("google meet");
567+
return !isVirtual && (
568+
<div className="w-full h-64 rounded-lg overflow-hidden">
569+
<iframe
570+
width="100%"
571+
height="100%"
572+
style={{ border: 0 }}
573+
loading="lazy"
574+
allowFullScreen
575+
referrerPolicy="no-referrer-when-downgrade"
576+
src={`https://www.google.com/maps/embed/v1/place?key=${
577+
import.meta.env.VITE_GOOGLE_MAPS_API_KEY || ""
578+
}&q=${encodeURIComponent(`${event.location}, ${event.school || ""}`)}`}
579+
></iframe>
580+
</div>
581+
);
582+
})()}
598583
</div>
599584
)}
600585

@@ -708,7 +693,7 @@ function EventDetailPage() {
708693
</motion.div>
709694

710695
{/* Original Screenshot (if available for submitter/admin) */}
711-
{canEditEvent && submissionInfo?.screenshot_url && (
696+
{(event as any)?.screenshot_url && (
712697
<motion.div
713698
initial={{ opacity: 0, y: 20 }}
714699
animate={{ opacity: 1, y: 0 }}
@@ -721,7 +706,7 @@ function EventDetailPage() {
721706
</h3>
722707
<div className="relative w-full">
723708
<img
724-
src={submissionInfo.screenshot_url}
709+
src={(event as any).screenshot_url}
725710
alt="Original event screenshot"
726711
className="w-full h-auto rounded-lg shadow-lg"
727712
/>

frontend/src/features/events/pages/EventsPage.tsx

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -161,15 +161,7 @@ function EventsPage() {
161161
Interested
162162
</FilterButton>
163163
)}
164-
{!isShowingNewEvents && !showInterested && (
165-
<FilterButton
166-
isActive={isShowingPastEvents}
167-
onToggle={handleToggleStartDate}
168-
icon={<History className="h-4 w-4" />}
169-
>
170-
All
171-
</FilterButton>
172-
)}
164+
{/* Removed the 'All' button as requested */}
173165
{view === "grid" && (
174166
<FilterButton
175167
isActive={isSelectMode}

0 commit comments

Comments
 (0)