diff --git a/backend/.env.example b/backend/.env.example index a17b1a4..9db5cf3 100644 --- a/backend/.env.example +++ b/backend/.env.example @@ -7,3 +7,4 @@ IS_LOCAL=false # Clerk Authentication CLERK_SECRET_KEY=sk_test_your_secret_key_here +CLERK_JWKS_URL=https://your-app-domain.clerk.accounts.dev/.well-known/jwks.json diff --git a/backend/app/api/event_fields.py b/backend/app/api/event_fields.py index 78a79f9..7df8c16 100644 --- a/backend/app/api/event_fields.py +++ b/backend/app/api/event_fields.py @@ -8,8 +8,11 @@ @router.get("/", response_model=List[EventFieldResponse]) -async def get_event_fields(event_id: str): - """Get all fields for an event""" +async def get_event_fields( + event_id: str, + auth: AuthenticatedUser = Depends(clerk_auth) +): + """Get all fields for an event (protected)""" try: event = await EventService.get_event(event_id) if not event: diff --git a/backend/app/api/events.py b/backend/app/api/events.py index 8203b80..c063eb5 100644 --- a/backend/app/api/events.py +++ b/backend/app/api/events.py @@ -14,7 +14,7 @@ async def create_event( ): """Create a new event (protected)""" try: - return await EventService.create_event(event) + return await EventService.create_event(event, auth) except Exception as e: raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, @@ -38,7 +38,7 @@ async def get_all_events( @router.get("/active", response_model=EventResponse) async def get_active_event(): - """Get currently active event""" + """Get currently active event. currently one event can be active (protected)""" try: event = await EventService.get_active_event() if not event: @@ -61,7 +61,7 @@ async def get_event( event_id: str, auth: AuthenticatedUser = Depends(clerk_auth) ): - """Get event by ID (protected)""" + """Get event by event ID (protected)""" try: event = await EventService.get_event(event_id) if not event: @@ -85,7 +85,7 @@ async def update_event( event: EventUpdate, auth: AuthenticatedUser = Depends(clerk_auth) ): - """Update event (protected)""" + """Update event details (protected)""" try: updated_event = await EventService.update_event(event_id, event) if not updated_event: @@ -134,7 +134,7 @@ async def clone_event( ): """Clone an existing event (protected)""" try: - event = await EventService.clone_event(event_id, new_name) + event = await EventService.clone_event(event_id, new_name, auth) if not event: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, diff --git a/backend/app/api/message_templates.py b/backend/app/api/message_templates.py index 3bb7d4a..dfdb781 100644 --- a/backend/app/api/message_templates.py +++ b/backend/app/api/message_templates.py @@ -14,7 +14,7 @@ async def create_template( ): """Create a new message template (protected)""" try: - return await message_template_service.create_template(template_data) + return await message_template_service.create_template(template_data, auth) except Exception as e: raise HTTPException(status_code=500, detail=str(e)) @@ -62,7 +62,7 @@ async def delete_template( ): """Delete a message template (protected)""" try: - await message_template_service.delete_template(template_id) + await message_template_service.delete_template(template_id ) return {"message": "Template deleted successfully"} except Exception as e: raise HTTPException(status_code=500, detail=str(e)) diff --git a/backend/app/api/qr_codes.py b/backend/app/api/qr_codes.py index f9fdf93..faa9a3a 100644 --- a/backend/app/api/qr_codes.py +++ b/backend/app/api/qr_codes.py @@ -14,7 +14,7 @@ async def create_qr_code( ): """Create a QR code for an event (protected)""" try: - return await QRService.create_qr_code(qr_data) + return await QRService.create_qr_code(qr_data, auth) except Exception as e: raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, diff --git a/backend/app/core/auth.py b/backend/app/core/auth.py index 81cd0ce..2862496 100644 --- a/backend/app/core/auth.py +++ b/backend/app/core/auth.py @@ -25,7 +25,10 @@ logger.info(f"✅ CLERK_SECRET_KEY loaded: {CLERK_SECRET_KEY[:20]}...") # JWKS URL for Clerk -JWKS_URL = "https://steady-hawk-55.clerk.accounts.dev/.well-known/jwks.json" +JWKS_URL = os.getenv("CLERK_JWKS_URL") +if not JWKS_URL: + raise ValueError("CLERK_JWKS_URL environment variable is required") + logger.info(f"🔑 Using JWKS URL: {JWKS_URL}") # Initialize PyJWKClient with SSL verification disabled for localhost diff --git a/backend/app/core/schema_manager.py b/backend/app/core/schema_manager.py index d3d63f4..24f31a0 100644 --- a/backend/app/core/schema_manager.py +++ b/backend/app/core/schema_manager.py @@ -60,11 +60,13 @@ def _define_schema(self) -> Dict[str, Table]: Column('venue_address', 'TEXT', nullable=True), Column('venue_map_link', 'TEXT', nullable=True), Column('is_active', 'INTEGER', nullable=True, default='0'), + Column('admin_user_id', 'TEXT', nullable=False), Column('created_at', 'TEXT', nullable=True, default='CURRENT_TIMESTAMP'), Column('updated_at', 'TEXT', nullable=True, default='CURRENT_TIMESTAMP'), ], indexes=[ - Index('idx_events_active', 'events', ['is_active']) + Index('idx_events_active', 'events', ['is_active']), + Index('idx_events_admin', 'events', ['admin_user_id']) ] ), @@ -79,6 +81,7 @@ def _define_schema(self) -> Dict[str, Table]: Column('is_required', 'INTEGER', nullable=True, default='0'), Column('field_options', 'TEXT', nullable=True), Column('field_order', 'INTEGER', nullable=True, default='0'), + Column('admin_user_id', 'TEXT', nullable=False) ] ), @@ -121,6 +124,7 @@ def _define_schema(self) -> Dict[str, Table]: Column('event_id', 'TEXT', nullable=False, foreign_key='events(id) ON DELETE CASCADE'), Column('message', 'TEXT', nullable=False), Column('qr_type', 'TEXT', nullable=True, default="'message'"), + Column('admin_user_id', 'TEXT', nullable=False), Column('created_at', 'TEXT', nullable=True, default='CURRENT_TIMESTAMP'), ] ), @@ -144,11 +148,13 @@ def _define_schema(self) -> Dict[str, Table]: Column('id', 'TEXT', nullable=False, primary_key=True), Column('template_name', 'TEXT', nullable=False), Column('template_text', 'TEXT', nullable=False), + Column('admin_user_id', 'TEXT', nullable=False), Column('created_at', 'TEXT', nullable=True, default='CURRENT_TIMESTAMP'), Column('updated_at', 'TEXT', nullable=True, default='CURRENT_TIMESTAMP'), ], indexes=[ - Index('idx_message_templates_name', 'message_templates', ['template_name']) + Index('idx_message_templates_name', 'message_templates', ['template_name']), + Index('idx_message_templates_admin', 'message_templates', ['admin_user_id']) ] ), diff --git a/backend/app/models/branding.py b/backend/app/models/branding.py index 8b7de38..9a9d1ad 100644 --- a/backend/app/models/branding.py +++ b/backend/app/models/branding.py @@ -17,4 +17,4 @@ class BrandingUpdate(BaseModel): site_headline: Optional[str] = None logo_url: Optional[str] = None text_style: Optional[str] = None - theme: Optional[str] = None + theme: Optional[str] = None \ No newline at end of file diff --git a/backend/app/models/message_template.py b/backend/app/models/message_template.py index 0756626..33399c8 100644 --- a/backend/app/models/message_template.py +++ b/backend/app/models/message_template.py @@ -19,6 +19,7 @@ class MessageTemplateUpdate(BaseModel): class MessageTemplate(MessageTemplateBase): id: str + admin_user_id: str created_at: str updated_at: str variables: List[str] = Field(default_factory=list, description="Extracted variables from template") diff --git a/backend/app/schemas/event.py b/backend/app/schemas/event.py index c98f3d6..a51cdf7 100644 --- a/backend/app/schemas/event.py +++ b/backend/app/schemas/event.py @@ -19,6 +19,7 @@ class EventFieldResponse(EventFieldCreate): id: str event_id: str + admin_user_id:str #this can be useful in the future class EventCreate(BaseModel): @@ -60,6 +61,7 @@ class EventResponse(BaseModel): venue_address: Optional[str] venue_map_link: Optional[str] is_active: bool + admin_user_id:str created_at: str updated_at: str fields: List[EventFieldResponse] = [] diff --git a/backend/app/schemas/qr_code.py b/backend/app/schemas/qr_code.py index 0f6bd04..e5b93ab 100644 --- a/backend/app/schemas/qr_code.py +++ b/backend/app/schemas/qr_code.py @@ -17,5 +17,6 @@ class QRCodeResponse(BaseModel): event_id: str message: str qr_type: str + admin_user_id: str qr_image: str # Base64 encoded image created_at: str diff --git a/backend/app/services/event_service.py b/backend/app/services/event_service.py index b739fab..dd70a84 100644 --- a/backend/app/services/event_service.py +++ b/backend/app/services/event_service.py @@ -2,6 +2,7 @@ import json from typing import List, Optional from app.core.database import db +from app.core.auth import AuthenticatedUser from app.schemas.event import ( EventCreate, EventUpdate, @@ -14,7 +15,7 @@ class EventService: """Service for event management""" @staticmethod - async def create_event(event_data: EventCreate) -> EventResponse: + async def create_event(event_data: EventCreate, auth: AuthenticatedUser) -> EventResponse: """Create a new event""" event_id = str(uuid.uuid4()) @@ -22,8 +23,8 @@ async def create_event(event_data: EventCreate) -> EventResponse: await db.execute( """ INSERT INTO events (id, name, description, date, time, venue, - venue_address, venue_map_link, is_active) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) + venue_address, venue_map_link, is_active, admin_user_id) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) """, [ event_id, @@ -35,6 +36,7 @@ async def create_event(event_data: EventCreate) -> EventResponse: event_data.venue_address, event_data.venue_map_link, 1 if event_data.is_active else 0, + auth.user_id, ], ) @@ -44,8 +46,8 @@ async def create_event(event_data: EventCreate) -> EventResponse: await db.execute( """ INSERT INTO event_fields (id, event_id, field_name, field_type, - field_label, is_required, field_options, field_order) - VALUES (?, ?, ?, ?, ?, ?, ?, ?) + field_label, is_required, field_options, field_order, admin_user_id) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) """, [ field_id, @@ -56,6 +58,7 @@ async def create_event(event_data: EventCreate) -> EventResponse: 1 if field.is_required else 0, field.field_options, field.field_order, + auth.user_id, ], ) @@ -84,6 +87,7 @@ async def get_event(event_id: str) -> Optional[EventResponse]: is_required=bool(row["is_required"]), field_options=row["field_options"], field_order=row["field_order"], + admin_user_id=row["admin_user_id"], ) for row in fields_rows ] @@ -98,15 +102,19 @@ async def get_event(event_id: str) -> Optional[EventResponse]: venue_address=event["venue_address"], venue_map_link=event["venue_map_link"], is_active=bool(event["is_active"]), + admin_user_id=event["admin_user_id"], created_at=event["created_at"], updated_at=event["updated_at"], fields=fields, ) + @staticmethod async def get_all_events() -> List[EventResponse]: """Get all events""" - events = await db.fetch_all("SELECT * FROM events ORDER BY created_at DESC") + events = await db.fetch_all( + "SELECT * FROM events ORDER BY created_at DESC" + ) result = [] for event in events: event_response = await EventService.get_event(event["id"]) @@ -114,15 +122,29 @@ async def get_all_events() -> List[EventResponse]: result.append(event_response) return result + + + @staticmethod + async def get_active_event() -> List[EventResponse]: + """Get active event. at this point only 1 event can be active""" + event = await db.fetch_all( + "SELECT * FROM events WHERE is_active = 1 ORDER BY created_at DESC LIMIT 1", + ) + if not event: + return None + return await EventService.get_event(event[0]["id"]) + + # we are not using now but will come handy @staticmethod - async def get_active_event() -> Optional[EventResponse]: - """Get currently active event""" - event = await db.fetch_one( - "SELECT * FROM events WHERE is_active = 1 ORDER BY created_at DESC LIMIT 1" + async def get_active_events_by_admin_id(auth: AuthenticatedUser) -> List[EventResponse]: + """Get all active events for a specific admin""" + event = await db.fetch_all( + "SELECT * FROM events WHERE admin_user_id = ? AND is_active = 1 ORDER BY created_at DESC LIMIT 1", + [auth.user_id] ) if not event: return None - return await EventService.get_event(event["id"]) + return await EventService.get_event(event[0]["id"]) @staticmethod async def update_event(event_id: str, event_data: EventUpdate) -> Optional[EventResponse]: @@ -188,7 +210,7 @@ async def delete_event(event_id: str) -> bool: return True @staticmethod - async def clone_event(event_id: str, new_name: str) -> Optional[EventResponse]: + async def clone_event(event_id: str, new_name: str, auth: AuthenticatedUser) -> Optional[EventResponse]: """Clone an existing event""" source_event = await EventService.get_event(event_id) if not source_event: @@ -217,11 +239,16 @@ async def clone_event(event_id: str, new_name: str) -> Optional[EventResponse]: ], ) - return await EventService.create_event(new_event_data) + return await EventService.create_event(new_event_data, auth) @staticmethod async def get_event_registrations(event_id: str) -> List[dict]: """Get all registrations for an event""" + # First verify the event exists + event = await db.fetch_one("SELECT id FROM events WHERE id = ?", [event_id]) + if not event: + return [] + registrations = await db.fetch_all( "SELECT * FROM registrations WHERE event_id = ? ORDER BY created_at DESC", [event_id], @@ -241,8 +268,8 @@ async def get_event_registrations(event_id: str) -> List[dict]: ] @staticmethod - async def update_event_fields(event_id: str, fields: List) -> Optional[List[EventFieldResponse]]: - """Replace all fields for an event""" + async def update_event_fields(event_id: str, fields: List, auth: AuthenticatedUser) -> Optional[List[EventFieldResponse]]: + """Replace fields for an event""" event = await db.fetch_one("SELECT id FROM events WHERE id = ?", [event_id]) if not event: return None @@ -256,8 +283,8 @@ async def update_event_fields(event_id: str, fields: List) -> Optional[List[Even await db.execute( """ INSERT INTO event_fields (id, event_id, field_name, field_type, - field_label, is_required, field_options, field_order) - VALUES (?, ?, ?, ?, ?, ?, ?, ?) + field_label, is_required, field_options, field_order, admin_user_id) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) """, [ field_id, @@ -268,6 +295,7 @@ async def update_event_fields(event_id: str, fields: List) -> Optional[List[Even 1 if field.is_required else 0, field.field_options, field.field_order, + auth.user_id, ], ) @@ -294,8 +322,8 @@ async def add_event_field(event_id: str, field) -> Optional[EventFieldResponse]: await db.execute( """ INSERT INTO event_fields (id, event_id, field_name, field_type, - field_label, is_required, field_options, field_order) - VALUES (?, ?, ?, ?, ?, ?, ?, ?) + field_label, is_required, field_options, field_order, admin_user_id) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) """, [ field_id, @@ -306,6 +334,7 @@ async def add_event_field(event_id: str, field) -> Optional[EventFieldResponse]: 1 if field.is_required else 0, field.field_options, next_order, + auth.user_id, ], ) @@ -318,13 +347,14 @@ async def add_event_field(event_id: str, field) -> Optional[EventFieldResponse]: is_required=field.is_required, field_options=field.field_options, field_order=next_order, + admin_user_id=auth.user_id, ) @staticmethod - async def delete_event_field(event_id: str, field_id: str) -> bool: + async def delete_event_field(event_id: str, field_id: str, auth: AuthenticatedUser) -> bool: """Delete a field from an event""" result = await db.execute( - "DELETE FROM event_fields WHERE id = ? AND event_id = ?", - [field_id, event_id] + "DELETE FROM event_fields WHERE id = ? AND event_id = ? AND admin_user_id = ?", + [field_id, event_id, auth.user_id] ) return True diff --git a/backend/app/services/message_template_service.py b/backend/app/services/message_template_service.py index b6b8ef8..87c1221 100644 --- a/backend/app/services/message_template_service.py +++ b/backend/app/services/message_template_service.py @@ -2,6 +2,7 @@ import re from typing import List, Optional from app.core.database import db +from app.core.auth import AuthenticatedUser from app.models.message_template import MessageTemplate, MessageTemplateCreate, MessageTemplateUpdate @@ -23,27 +24,28 @@ def substitute_variables(template_text: str, variables: dict) -> str: result = result.replace(f"{{{{{var_name}}}}}", str(var_value)) return result - async def create_template(self, template_data: MessageTemplateCreate) -> MessageTemplate: + async def create_template(self, template_data: MessageTemplateCreate, auth) -> MessageTemplate: """Create a new message template""" template_id = str(uuid.uuid4()) query = """ - INSERT INTO message_templates (id, template_name, template_text) - VALUES (?, ?, ?) + INSERT INTO message_templates (id, template_name, template_text, admin_user_id) + VALUES (?, ?, ?, ?) """ await db.execute(query, [ template_id, template_data.template_name, - template_data.template_text + template_data.template_text, + auth.user_id ]) - return await self.get_template(template_id) + return await self.get_template(template_id, auth) async def get_all_templates(self) -> List[MessageTemplate]: """Get all message templates""" query = """ - SELECT id, template_name, template_text, created_at, updated_at + SELECT id, template_name, template_text, admin_user_id, created_at, updated_at FROM message_templates ORDER BY template_name ASC """ @@ -57,6 +59,7 @@ async def get_all_templates(self) -> List[MessageTemplate]: id=row['id'], template_name=row['template_name'], template_text=row['template_text'], + admin_user_id=row['admin_user_id'], created_at=row['created_at'], updated_at=row['updated_at'], variables=variables @@ -67,7 +70,7 @@ async def get_all_templates(self) -> List[MessageTemplate]: async def get_template(self, template_id: str) -> Optional[MessageTemplate]: """Get a specific message template""" query = """ - SELECT id, template_name, template_text, created_at, updated_at + SELECT id, template_name, template_text, admin_user_id, created_at, updated_at FROM message_templates WHERE id = ? """ @@ -82,6 +85,7 @@ async def get_template(self, template_id: str) -> Optional[MessageTemplate]: id=row['id'], template_name=row['template_name'], template_text=row['template_text'], + admin_user_id=row['admin_user_id'], created_at=row['created_at'], updated_at=row['updated_at'], variables=variables diff --git a/backend/app/services/qr_service.py b/backend/app/services/qr_service.py index 59e1535..e14aa72 100644 --- a/backend/app/services/qr_service.py +++ b/backend/app/services/qr_service.py @@ -3,6 +3,7 @@ import io import base64 from app.core.database import db +from app.core.auth import AuthenticatedUser from app.schemas.qr_code import QRCodeCreate, QRCodeResponse @@ -10,7 +11,7 @@ class QRService: """Service for QR code management""" @staticmethod - async def create_qr_code(qr_data: QRCodeCreate) -> QRCodeResponse: + async def create_qr_code(qr_data: QRCodeCreate, auth) -> QRCodeResponse: """Create a QR code for an event""" qr_id = str(uuid.uuid4()) @@ -38,10 +39,10 @@ async def create_qr_code(qr_data: QRCodeCreate) -> QRCodeResponse: # Save to database await db.execute( """ - INSERT INTO qr_codes (id, event_id, message, qr_type) - VALUES (?, ?, ?, ?) + INSERT INTO qr_codes (id, event_id, message, qr_type, admin_user_id) + VALUES (?, ?, ?, ?, ?) """, - [qr_id, qr_data.event_id, qr_data.message, qr_data.qr_type], + [qr_id, qr_data.event_id, qr_data.message, qr_data.qr_type, auth.user_id], ) qr_code = await db.fetch_one("SELECT * FROM qr_codes WHERE id = ?", [qr_id]) @@ -51,6 +52,7 @@ async def create_qr_code(qr_data: QRCodeCreate) -> QRCodeResponse: event_id=qr_code["event_id"], message=qr_code["message"], qr_type=qr_code["qr_type"], + admin_user_id=qr_code["admin_user_id"], qr_image=img_base64, created_at=qr_code["created_at"], ) @@ -67,12 +69,18 @@ async def get_qr_code(qr_id: str) -> dict: "event_id": qr_code["event_id"], "message": qr_code["message"], "qr_type": qr_code["qr_type"], + "admin_user_id": qr_code["admin_user_id"], "created_at": qr_code["created_at"], } @staticmethod async def get_event_qr_codes(event_id: str) -> list: """Get all QR codes for an event""" + # First verify the event exists + event = await db.fetch_one("SELECT id FROM events WHERE id = ?", [event_id]) + if not event: + return [] + qr_codes = await db.fetch_all( "SELECT * FROM qr_codes WHERE event_id = ? ORDER BY created_at DESC", [event_id], @@ -84,6 +92,7 @@ async def get_event_qr_codes(event_id: str) -> list: "event_id": qr["event_id"], "message": qr["message"], "qr_type": qr["qr_type"], + "admin_user_id": qr["admin_user_id"], "created_at": qr["created_at"], } for qr in qr_codes diff --git a/backend/tests/test_events_api.py b/backend/tests/test_events_api.py index 982317d..120b143 100644 --- a/backend/tests/test_events_api.py +++ b/backend/tests/test_events_api.py @@ -15,6 +15,7 @@ def test_create_event(self, client, sample_event_data): assert data["description"] == sample_event_data["description"] assert data["venue"] == sample_event_data["venue"] assert data["time"] == sample_event_data["time"] + assert data["admin_user_id"] == "test_user_12345" # Mock user ID assert "id" in data def test_create_event_with_missing_fields(self, client): diff --git a/docs/API.md b/docs/API.md index f1ee36e..2c45f9e 100644 --- a/docs/API.md +++ b/docs/API.md @@ -9,7 +9,18 @@ Complete API reference for all endpoints with request/response examples. ## Authentication -Currently, no authentication is required (as per requirements). +All admin endpoints require Clerk authentication. Public endpoints (registration forms, QR check-in) do not require authentication. + +**Authentication Headers:** +``` +Authorization: Bearer +``` + +**Protected Endpoints:** All endpoints except: +- `POST /api/registrations/` (public registration) +- `GET /api/events/active/` (public active event view) +- `POST /api/registrations/check-in/{event_id}` (QR check-in) +- `GET /api/registrations/profile/autofill` (auto-fill functionality) --- @@ -33,7 +44,10 @@ GET /api/events/ "venue": "Tech Hub, Building A", "venue_map_link": "https://maps.google.com/...", "is_active": true, - "created_at": "2025-10-01T10:00:00Z" + "admin_user_id": "user_12345", + "created_at": "2025-10-01T10:00:00Z", + "updated_at": "2025-10-01T10:00:00Z", + "fields": [] } ] ``` @@ -50,13 +64,17 @@ GET /api/events/active/ "id": "550e8400-e29b-41d4-a716-446655440000", "name": "Tech Workshop 2025", "is_active": true, + "admin_user_id": "user_12345", "fields": [ { "id": "field-001", - "label": "Full Name", - "type": "text", - "required": true, - "field_order": 0 + "event_id": "550e8400-e29b-41d4-a716-446655440000", + "field_name": "fullname", + "field_type": "text", + "field_label": "Full Name", + "is_required": true, + "field_order": 0, + "admin_user_id": "user_12345" } ] } @@ -101,7 +119,10 @@ POST /api/events/ "id": "550e8400-e29b-41d4-a716-446655440000", "name": "Tech Workshop 2025", "is_active": false, - "created_at": "2025-10-01T10:00:00Z" + "admin_user_id": "user_12345", + "created_at": "2025-10-01T10:00:00Z", + "updated_at": "2025-10-01T10:00:00Z", + "fields": [] } ``` @@ -138,7 +159,11 @@ POST /api/events/{id}/clone?new_name=Cloned Event Name { "id": "new-event-id", "name": "Cloned Event Name", - "is_active": false + "is_active": false, + "admin_user_id": "user_12345", + "created_at": "2025-10-01T10:00:00Z", + "updated_at": "2025-10-01T10:00:00Z", + "fields": [] } ``` @@ -276,8 +301,8 @@ POST /api/qr-codes/ ```json { "event_id": "550e8400-e29b-41d4-a716-446655440000", - "qr_type": "text", - "qr_content": "WiFi Password: SecurePass123" + "message": "WiFi Password: SecurePass123", + "qr_type": "message" } ``` @@ -286,8 +311,8 @@ or ```json { "event_id": "550e8400-e29b-41d4-a716-446655440000", - "qr_type": "url", - "qr_content": "https://example.com/resources" + "message": "https://example.com/resources", + "qr_type": "url" } ``` @@ -296,8 +321,9 @@ or { "id": "qr-code-id", "event_id": "550e8400-e29b-41d4-a716-446655440000", + "message": "WiFi Password: SecurePass123", "qr_type": "text", - "qr_content": "WiFi Password: SecurePass123", + "admin_user_id": "user_12345", "qr_image": "base64_encoded_image_data", "created_at": "2025-10-01T10:00:00Z" } @@ -323,6 +349,131 @@ DELETE /api/qr-codes/{id} --- +## Message Templates API + +### Create Message Template + +```http +POST /api/message-templates/ +``` + +**Request Body**: +```json +{ + "template_name": "Event Reminder", + "template_text": "Hi {{name}}! Don't forget about our event on {{date}} at {{venue}}. See you there! 🎉" +} +``` + +**Response** (201): +```json +{ + "id": "template-id", + "template_name": "Event Reminder", + "template_text": "Hi {{name}}! Don't forget about our event on {{date}} at {{venue}}. See you there! 🎉", + "admin_user_id": "user_12345", + "created_at": "2025-10-01T10:00:00Z", + "updated_at": "2025-10-01T10:00:00Z", + "variables": ["name", "date", "venue"] +} +``` + +### Get All Message Templates + +```http +GET /api/message-templates/ +``` + +**Response** (200): +```json +[ + { + "id": "template-id", + "template_name": "Event Reminder", + "template_text": "Hi {{name}}! Don't forget about our event...", + "admin_user_id": "user_12345", + "created_at": "2025-10-01T10:00:00Z", + "updated_at": "2025-10-01T10:00:00Z", + "variables": ["name", "date", "venue"] + } +] +``` + +### Get Message Template + +```http +GET /api/message-templates/{template_id} +``` + +### Update Message Template + +```http +PUT /api/message-templates/{template_id} +``` + +**Request Body** (partial update): +```json +{ + "template_name": "Updated Reminder", + "template_text": "Hello {{name}}! Reminder for {{event_name}} on {{date}}." +} +``` + +### Delete Message Template + +```http +DELETE /api/message-templates/{template_id} +``` + +**Response** (200): +```json +{ + "message": "Template deleted successfully" +} +``` + +--- + +## Branding API + +### Get Branding Settings + +```http +GET /api/branding/ +``` + +**Response** (200): +```json +{ + "id": "default", + "site_title": "MagPie Events", + "site_headline": "Where Innovation Meets Community", + "logo_url": null, + "text_style": "gradient", + "theme": "default", + "updated_at": "2025-10-01T10:00:00Z" +} +``` + +### Update Branding Settings + +```http +PUT /api/branding/ +``` + +**Request Body** (partial update): +```json +{ + "site_title": "My Event Platform", + "site_headline": "Register for amazing events", + "logo_url": "https://example.com/logo.png", + "text_style": "solid", + "theme": "dark" +} +``` + +--- + ## WhatsApp API ### Send Bulk Messages @@ -335,7 +486,28 @@ POST /api/whatsapp/send-bulk/ ```json { "event_id": "550e8400-e29b-41d4-a716-446655440000", - "message": "Hi! Reminder about our event tomorrow. See you there! 🎉" + "message": "Hi! Reminder about our event tomorrow. See you there! 🎉", + "template_id": null, + "template_variables": null, + "send_to": "all", + "filter_field": null, + "filter_value": null +} +``` + +or using a template: + +```json +{ + "event_id": "550e8400-e29b-41d4-a716-446655440000", + "message": null, + "template_id": "template-id", + "template_variables": { + "name": "John", + "event_name": "Tech Workshop", + "date": "2025-10-15" + }, + "send_to": "all" } ``` diff --git a/docs/SETUP.md b/docs/SETUP.md index 1533f50..a15da69 100644 --- a/docs/SETUP.md +++ b/docs/SETUP.md @@ -75,6 +75,7 @@ FRONTEND_URL=http://localhost:3000 # Authentication (Required - see Authentication Setup section) CLERK_SECRET_KEY=sk_test_your_clerk_secret_key_here +CLERK_JWKS_URL=https://your-app-domain.clerk.accounts.dev/.well-known/jwks.json # WhatsApp Integration (Optional - see WHATSAPP_SETUP.md) TWILIO_ACCOUNT_SID=your_account_sid_here @@ -229,9 +230,10 @@ The dashboard is protected with Clerk authentication. You'll need a Clerk accoun - Create/edit/delete events - View all registrations - Generate QR codes -- Send WhatsApp messages +- Send WhatsApp messages (with templates) - Export CSV data - Configure branding/themes +- Manage message templates **Public Features (No Authentication)**: - Event registration form