Skip to content

Commit f12b497

Browse files
committed
event submissions
1 parent 05fcd3c commit f12b497

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

57 files changed

+4265
-825
lines changed
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
name: Send Newsletter
2+
3+
on:
4+
schedule:
5+
- cron: '0 19 * * *' # 2pm EST (7pm UTC)
6+
workflow_dispatch: # Optional manual trigger
7+
8+
jobs:
9+
send_newsletter:
10+
runs-on: ubuntu-latest
11+
env:
12+
PRODUCTION: '1'
13+
DJANGO_SETTINGS_MODULE: 'config.settings.development'
14+
DATABASE_URL: ${{ secrets.SUPABASE_DB_URL }}
15+
SUPABASE_DB_URL: ${{ secrets.SUPABASE_DB_URL }}
16+
POSTGRES_DB: ${{ secrets.POSTGRES_DB }}
17+
POSTGRES_USER: ${{ secrets.POSTGRES_USER }}
18+
POSTGRES_PASSWORD: ${{ secrets.POSTGRES_PASSWORD }}
19+
POSTGRES_HOST: ${{ secrets.POSTGRES_HOST }}
20+
POSTGRES_PORT: ${{ secrets.POSTGRES_PORT }}
21+
RESEND_API_KEY: ${{ secrets.RESEND_API_KEY }}
22+
RESEND_FROM_EMAIL: ${{ secrets.RESEND_FROM_EMAIL }}
23+
EMAIL_ENCRYPTION_KEY: ${{ secrets.EMAIL_ENCRYPTION_KEY }}
24+
EMAIL_HASH_KEY: ${{ secrets.EMAIL_HASH_KEY }}
25+
SECRET_KEY: ${{ secrets.SECRET_KEY }}
26+
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
27+
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
28+
AWS_S3_BUCKET_NAME: ${{ secrets.AWS_S3_BUCKET_NAME }}
29+
AWS_DEFAULT_REGION: ${{ secrets.AWS_DEFAULT_REGION }}
30+
31+
steps:
32+
- uses: actions/checkout@v4
33+
34+
- name: Set up Python
35+
uses: actions/setup-python@v5
36+
with:
37+
python-version: '3.11'
38+
39+
- name: Cache pip
40+
uses: actions/cache@v4
41+
with:
42+
path: ~/.cache/pip
43+
key: ${{ runner.os }}-pip-${{ hashFiles('backend/requirements.txt') }}
44+
restore-keys: |
45+
${{ runner.os }}-pip-
46+
47+
- name: Install dependencies
48+
working-directory: backend
49+
run: |
50+
pip install --upgrade pip setuptools wheel
51+
pip install --prefer-binary -r requirements.txt
52+
53+
- name: Send newsletter to subscribers
54+
working-directory: backend/scripts
55+
run: python send_newsletter.py
56+
continue-on-error: true
57+
58+
- name: Upload logs as artifacts
59+
if: always()
60+
uses: actions/upload-artifact@v4
61+
with:
62+
name: newsletter-logs-${{ github.run_number }}
63+
path: |
64+
backend/scripts/logs/
65+
backend/scripts/*.log
66+
retention-days: 30
67+

.github/workflows/update-events-data.yml

Lines changed: 2 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
name: Scrape Instagram, Update Events DB, Send Newsletter, Update Static Data
1+
name: Scrape Instagram, Update Events DB, Update Static Data
22

33
on:
44
schedule:
@@ -9,10 +9,6 @@ on:
99
required: true
1010
type: boolean
1111
default: false
12-
send_newsletter:
13-
required: true
14-
type: boolean
15-
default: false
1612
MAX_POSTS:
1713
required: false
1814
type: number
@@ -125,10 +121,4 @@ jobs:
125121
git config --global user.email 'github-actions[bot]@users.noreply.github.com'
126122
git add frontend/src/data/staticData.ts frontend/public/rss.xml
127123
git commit -m "chore: update static data from DB" || echo "No changes to commit"
128-
git pull --rebase && git push --force
129-
130-
- name: Send newsletter to subscribers
131-
if: github.event_name == 'schedule' || github.event.inputs.send_newsletter == 'true'
132-
working-directory: backend/scripts
133-
run: python send_newsletter.py
134-
continue-on-error: true
124+
git pull --rebase && git push --force
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
name: Validate Event Sources
2+
3+
on:
4+
schedule:
5+
- cron: '0 5,13,17,21 * * *' # 12am, 8am, 12pm, 4pm EST (5am, 1pm, 5pm, 9pm UTC)
6+
workflow_dispatch: # Manual trigger
7+
inputs:
8+
limit:
9+
description: 'Limit number of events to check (for testing)'
10+
required: false
11+
type: number
12+
school:
13+
description: 'Filter by school name'
14+
required: false
15+
type: string
16+
default: 'University of Waterloo'
17+
workers:
18+
description: 'Max concurrent requests (default: 10)'
19+
required: false
20+
type: number
21+
default: 10
22+
23+
jobs:
24+
validate_sources:
25+
runs-on: ubuntu-latest
26+
env:
27+
PRODUCTION: '1'
28+
DJANGO_SETTINGS_MODULE: 'config.settings.development'
29+
DATABASE_URL: ${{ secrets.SUPABASE_DB_URL }}
30+
SUPABASE_DB_URL: ${{ secrets.SUPABASE_DB_URL }}
31+
POSTGRES_DB: ${{ secrets.POSTGRES_DB }}
32+
POSTGRES_USER: ${{ secrets.POSTGRES_USER }}
33+
POSTGRES_PASSWORD: ${{ secrets.POSTGRES_PASSWORD }}
34+
POSTGRES_HOST: ${{ secrets.POSTGRES_HOST }}
35+
POSTGRES_PORT: ${{ secrets.POSTGRES_PORT }}
36+
SECRET_KEY: ${{ secrets.SECRET_KEY }}
37+
38+
steps:
39+
- uses: actions/checkout@v4
40+
41+
- name: Set up Python
42+
uses: actions/setup-python@v5
43+
with:
44+
python-version: '3.11'
45+
46+
- name: Create logs directory
47+
working-directory: backend/scripts
48+
run: mkdir -p logs
49+
50+
- name: Cache pip
51+
uses: actions/cache@v4
52+
with:
53+
path: ~/.cache/pip
54+
key: ${{ runner.os }}-pip-${{ hashFiles('backend/requirements.txt') }}
55+
restore-keys: |
56+
${{ runner.os }}-pip-
57+
58+
- name: Install dependencies
59+
working-directory: backend
60+
run: |
61+
pip install --upgrade pip setuptools wheel
62+
pip install --prefer-binary -r requirements.txt
63+
64+
- name: Run validation script (scheduled)
65+
if: github.event_name == 'schedule'
66+
working-directory: backend/scripts
67+
run: |
68+
python validate_event_sources.py --school "University of Waterloo" --workers 10 2>&1 | tee logs/validation.log
69+
continue-on-error: false
70+
71+
- name: Run validation script (manual)
72+
if: github.event_name == 'workflow_dispatch'
73+
working-directory: backend/scripts
74+
run: |
75+
ARGS="--school '${{ github.event.inputs.school }}'"
76+
if [ ! -z "${{ github.event.inputs.limit }}" ]; then
77+
ARGS="$ARGS --limit ${{ github.event.inputs.limit }}"
78+
fi
79+
if [ ! -z "${{ github.event.inputs.workers }}" ]; then
80+
ARGS="$ARGS --workers ${{ github.event.inputs.workers }}"
81+
fi
82+
python validate_event_sources.py $ARGS 2>&1 | tee logs/validation.log
83+
continue-on-error: false
84+
85+
- name: Upload logs as artifacts
86+
if: always()
87+
uses: actions/upload-artifact@v4
88+
with:
89+
name: validation-logs-${{ github.run_number }}
90+
path: |
91+
backend/scripts/logs/
92+
backend/scripts/*.log
93+
retention-days: 30
94+
95+
- name: Check for deleted events
96+
if: always()
97+
working-directory: backend/scripts
98+
run: |
99+
if grep -q "Events deleted:" logs/validation.log; then
100+
DELETED=$(grep "Events deleted:" logs/validation.log | awk '{print $NF}')
101+
echo "::notice::Deleted $DELETED invalid events"
102+
fi
103+

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ python manage.py makemigrations
4343
python manage.py migrate
4444
# PRODUCTION=1 python manage.py migrate
4545
python scripts/populate-local-db-with-prod-data.py
46+
python manage.py fix_sequences
4647
python manage.py runserver 8000
4748
```
4849

backend/apps/core/auth.py

Lines changed: 27 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,96 +1,70 @@
1-
from functools import wraps, lru_cache
1+
from functools import wraps
22
import logging
33

4-
from clerk_backend_api import authenticate_request, AuthenticateRequestOptions, Clerk
4+
from clerk_backend_api import authenticate_request, AuthenticateRequestOptions
55
from django.contrib.auth import authenticate
66
from django.contrib.auth.backends import BaseBackend
7-
from django.contrib.auth.models import User
7+
from django.contrib.auth.models import AnonymousUser, User
88
from django.http import JsonResponse
99

1010
from config.settings.base import CLERK_SECRET_KEY, CLERK_AUTHORIZED_PARTIES
1111

1212
logger = logging.getLogger(__name__)
1313

14-
# Create a single Clerk instance to reuse across requests
15-
_clerk_client = Clerk(bearer_auth=CLERK_SECRET_KEY)
16-
1714

1815
class JwtAuthBackend(BaseBackend):
1916
def authenticate(self, request, **kwargs):
20-
if 'Authorization' not in request.headers:
21-
logger.warning(f"JWT auth: Missing Authorization header on {request.method} {request.path}")
22-
return None
23-
2417
try:
25-
auth_header = request.headers.get('Authorization')
26-
if not auth_header.startswith('Bearer '):
27-
logger.warning(f"JWT auth: Authorization header not Bearer on {request.method} {request.path}")
28-
return None
29-
30-
logger.debug(
31-
f"JWT auth: attempting Clerk auth (aud={CLERK_AUTHORIZED_PARTIES}) on {request.method} {request.path}"
32-
)
33-
request_state = authenticate_request(
18+
state = authenticate_request(
3419
request,
3520
AuthenticateRequestOptions(
3621
secret_key=CLERK_SECRET_KEY,
3722
authorized_parties=CLERK_AUTHORIZED_PARTIES,
3823
),
3924
)
40-
if not request_state.is_signed_in:
41-
request.error_message = request_state.message
42-
logger.warning(f"JWT auth: Clerk rejected token: {request_state.message}")
25+
26+
if not state.is_signed_in:
27+
request.error_message = getattr(state, "message", "Not signed in")
28+
logger.warning("JWT auth: Clerk rejected token: %s", request.error_message)
4329
return None
44-
print(request_state.payload, 'request_state.payload')
45-
# Ideally at this point user object must be fetched from DB and returned, but we will just return a dummy
46-
# user object
47-
user = User(username=request_state.payload.get("sub", "unknown"), password="None")
48-
# Attach payload for downstream usage
49-
try:
50-
request.auth_payload = request_state.payload
51-
except Exception:
52-
pass
53-
return user
5430

55-
except Exception as e:
31+
request.auth_payload = state.payload
32+
33+
django_user = AnonymousUser()
34+
django_user.username = state.payload.get("sub") or "clerk_user"
35+
return django_user
36+
37+
38+
except Exception:
5639
request.error_message = "Unable to authenticate user"
5740
logger.exception("JWT auth: exception during authentication")
5841
return None
5942

6043
def get_user(self, user_id):
61-
return User(username=user_id, password="None")
44+
try:
45+
return User.objects.get(pk=user_id)
46+
except Exception:
47+
return None
6248

6349

6450
def jwt_required(view_func):
6551
@wraps(view_func)
6652
def _wrapped_view(request, *args, **kwargs):
67-
user = authenticate(request)
53+
user = authenticate(request) # triggers JwtAuthBackend.authenticate
6854
if not user:
69-
error = getattr(request, 'error_message', 'User not authenticated')
70-
return JsonResponse({'detail': error}, status=401)
55+
error = getattr(request, "error_message", "User not authenticated")
56+
return JsonResponse({"detail": error}, status=401)
7157
request.user = user
7258
return view_func(request, *args, **kwargs)
73-
7459
return _wrapped_view
7560

7661

77-
@lru_cache(maxsize=100)
78-
def _get_user_metadata(user_id: str) -> dict:
79-
"""Fetch user public_metadata from Clerk API."""
80-
try:
81-
user = _clerk_client.users.get(user_id=user_id)
82-
return getattr(user, "public_metadata", None) or getattr(user, "publicMetadata", None) or {}
83-
except Exception:
84-
return {}
85-
86-
8762
def admin_required(view_func):
8863
@wraps(view_func)
8964
@jwt_required
9065
def _wrapped_view(request, *args, **kwargs):
91-
user_id = getattr(request, "auth_payload", {}).get("sub")
92-
if not user_id or _get_user_metadata(user_id).get("role") != "admin":
93-
return JsonResponse({'detail': 'Admin only'}, status=403)
66+
role = request.auth_payload.get("role")
67+
if role != "admin":
68+
return JsonResponse({"message": "Admin only"}, status=403)
9469
return view_func(request, *args, **kwargs)
95-
96-
return _wrapped_view
70+
return _wrapped_view

0 commit comments

Comments
 (0)