Skip to content

Commit 7ef64f5

Browse files
committed
added safe guards to submitted events
1 parent 4b68faf commit 7ef64f5

File tree

8 files changed

+87
-14
lines changed

8 files changed

+87
-14
lines changed

backend/apps/core/middleware.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
from django.http import HttpResponse
2+
3+
class HealthCheckMiddleware:
4+
def __init__(self, get_response):
5+
self.get_response = get_response
6+
7+
def __call__(self, request):
8+
if request.path_info == '/health':
9+
return HttpResponse("OK")
10+
return self.get_response(request)

backend/apps/events/views.py

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -449,7 +449,7 @@ def rss_feed(request):
449449

450450

451451
@api_view(["POST"])
452-
@permission_classes([ClerkAuthenticated])
452+
@permission_classes([AllowAny])
453453
@ratelimit(key="ip", rate="5/hr", block=True)
454454
def submit_event(request):
455455
"""Submit event for review - accepts screenshot file and source URL, runs extraction, creates Event and links submission"""
@@ -487,13 +487,37 @@ def submit_event(request):
487487
event_data = {"source_url": source_url, "source_image_url": screenshot_url}
488488

489489
# Create Event immediately and link submission
490-
event = Events.objects.create(**{k: v for k, v in event_data.items() if k in {f.name for f in Events._meta.get_fields()}})
490+
allowed_fields = {f.name for f in Events._meta.get_fields()}
491+
cleaned = {
492+
k: v
493+
for k, v in (event_data or {}).items()
494+
if k in allowed_fields
495+
and not (isinstance(v, str) and v.strip() in {"", "\"\"", "''", "“”"})
496+
}
497+
498+
# Basic guard: require at least a title and a start datetime to consider it an event
499+
if not cleaned.get("title") or not cleaned.get("dtstart"):
500+
return Response(
501+
{"error": "Submission does not appear to be an event."},
502+
status=status.HTTP_400_BAD_REQUEST,
503+
)
504+
505+
event = Events.objects.create(**cleaned)
506+
507+
# Attempt to capture submitting user if available (optional for anonymous)
508+
clerk_user_id = None
509+
try:
510+
clerk_user_id = (
511+
getattr(request, "clerk_user", None) or {}
512+
).get("id")
513+
except Exception:
514+
clerk_user_id = None
491515

492516
submission = EventSubmission.objects.create(
493517
screenshot_url=screenshot_url,
494518
source_url=source_url,
495519
status="pending",
496-
submitted_by=request.clerk_user.get('id'),
520+
submitted_by=clerk_user_id,
497521
created_event=event,
498522
extracted_data=event_data,
499523
)

backend/config/settings/base.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@
5454
]
5555

5656
MIDDLEWARE = [
57+
"apps.core.middleware.HealthCheckMiddleware",
5758
"corsheaders.middleware.CorsMiddleware",
5859
"django.middleware.security.SecurityMiddleware",
5960
"django.contrib.sessions.middleware.SessionMiddleware",

backend/services/openai_service.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,11 @@ def extract_events_from_caption(
122122
123123
Caption: {caption_text}
124124
125+
STRICT CONTENT POLICY:
126+
- If the content is NOT actually trying to announce or describe a real-world event (e.g., a meme, personal photo dump, random advertisement, generic brand post with no time/place), DO NOT extract an event. Return an object with empty strings/nulls as specified below.
127+
- If the content is inappropriate (nudity, explicit sexual content, or graphic violence), DO NOT extract an event. Return an object with empty strings/nulls as specified below.
128+
- If there is no specific start time, DO NOT extract an event.
129+
125130
Do NOT extract events that only mention a date without a specific start time. Only include an event if a specific start time is mentioned in the caption or image.
126131
127132
Return ONE JSON object (not an array). The object must have ALL of the following fields:
@@ -158,6 +163,7 @@ def extract_events_from_caption(
158163
- For all_day: true only if no specific time is mentioned.
159164
- For tz mappings, default to "America/Toronto" for {school}.
160165
- For rrule: only when recurring is mentioned; otherwise empty string.
166+
- If the content violates the STRICT CONTENT POLICY or is not an event, set title to "" and leave the rest of the fields empty as per defaults below. Do not fabricate an event.
161167
- If information is not available, use empty string for strings, null for price/coordinates, and false for booleans.
162168
- Return ONLY the JSON object text, no extra commentary.
163169
{f"- An image is provided at: {source_image_url}. If there are conflicts between caption and image information, prioritize the caption text." if source_image_url else ""}

frontend/src/app/App.tsx

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -29,14 +29,7 @@ function App() {
2929
<Route path="/" element={<EventsPage />} />
3030
<Route path="/events" element={<EventsPage />} />
3131
<Route path="/events/:eventId" element={<EventDetailPage />} />
32-
<Route
33-
path="/submit"
34-
element={
35-
<ProtectedRoute>
36-
<SubmitEventPage />
37-
</ProtectedRoute>
38-
}
39-
/>
32+
<Route path="/submit" element={<SubmitEventPage />} />
4033
<Route path="/clubs" element={<ClubsPage />} />
4134
<Route path="/about" element={<AboutPage />} />
4235
<Route path="/contact" element={<ContactPage />} />

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

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { CheckCircle, Calendar, Link, Upload, Image as ImageIcon } from 'lucide-
1313
export function SubmitEventPage() {
1414
const [preview, setPreview] = useState<string | null>(null);
1515
const [success, setSuccess] = useState(false);
16+
const [errorMsg, setErrorMsg] = useState<string | null>(null);
1617
const { submitEvent, isLoading } = useEventSubmission();
1718

1819
const {
@@ -37,19 +38,23 @@ export function SubmitEventPage() {
3738

3839
const onSubmit = async (data: SubmissionFormData) => {
3940
try {
41+
setErrorMsg(null);
4042
submitEvent(data, {
4143
onSuccess: () => {
4244
setSuccess(true);
45+
setErrorMsg(null);
4346
reset();
4447
setPreview(null);
4548
setTimeout(() => setSuccess(false), 5000);
4649
},
4750
onError: (error) => {
48-
console.error('Submission error:', error);
51+
const msg = error instanceof Error && error.message ? error.message : 'We could not process this submission. Please ensure it clearly describes an event with a specific start time and is appropriate.';
52+
setErrorMsg(msg);
4953
}
5054
});
5155
} catch (error) {
52-
console.error('Submission error:', error);
56+
const msg = error instanceof Error && error.message ? error.message : 'Submission failed. Please try again.';
57+
setErrorMsg(msg);
5358
}
5459
};
5560

@@ -76,6 +81,11 @@ export function SubmitEventPage() {
7681
</CardTitle>
7782
</CardHeader>
7883
<CardContent>
84+
{errorMsg && (
85+
<div className="mb-6 rounded-md border border-red-200 bg-red-50 text-red-700 dark:border-red-900/50 dark:bg-red-900/20 dark:text-red-200 p-4 text-sm">
86+
{errorMsg}
87+
</div>
88+
)}
7989
{success ? (
8090
<div className="text-center py-8">
8191
<div className="inline-flex items-center justify-center w-16 h-16 bg-green-100 rounded-full mb-4">

frontend/src/shared/api/EventsAPIClient.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,18 @@ class EventsAPIClient {
102102
});
103103

104104
if (!response.ok) {
105-
throw new Error(`HTTP error! status: ${response.status}`);
105+
let message = `Submission failed (status ${response.status})`;
106+
try {
107+
const errBody = await response.json();
108+
if (typeof errBody?.error === 'string' && errBody.error.trim()) {
109+
message = errBody.error;
110+
} else if (typeof errBody?.message === 'string' && errBody.message.trim()) {
111+
message = errBody.message;
112+
}
113+
} catch (_) {
114+
// ignore JSON parse errors
115+
}
116+
throw new Error(message);
106117
}
107118

108119
return response.json();

frontend/src/shared/components/layout/Navbar.tsx

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,17 @@ function Navbar() {
4040
>
4141
Events
4242
</Button>
43+
<Button
44+
variant="link"
45+
onMouseDown={() => navigate("/submit")}
46+
className={`text-sm font-medium ${
47+
isActive("/submit")
48+
? "text-gray-900 dark:text-white"
49+
: "text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-white"
50+
}`}
51+
>
52+
Submit
53+
</Button>
4354
<Button
4455
variant="link"
4556
onMouseDown={() => navigate("/clubs")}
@@ -152,6 +163,13 @@ function Navbar() {
152163
>
153164
Events
154165
</Button>
166+
<Button
167+
variant="ghost"
168+
className="w-full justify-start text-sm font-medium text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-white"
169+
onMouseDown={() => navigate("/submit")}
170+
>
171+
Submit
172+
</Button>
155173
<Button
156174
variant="ghost"
157175
className="w-full justify-start text-sm font-medium text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-white"

0 commit comments

Comments
 (0)