Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 13 additions & 2 deletions app/core/security.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
from fastapi import Depends, HTTPException, status
from fastapi import Depends, HTTPException, status, Request
from fastapi.security import HTTPBearer, HTTPBasicCredentials
from typing import Annotated
from app.database.connection import get_db_connection, get_redis_client
from app.database.orm import select
from json import dumps, loads
from app.schemas.models import APIKeyResponse, APIKeyVerificationResponse
from app.core.settings import settings

security = HTTPBearer()

Expand All @@ -31,7 +32,6 @@ async def verify_api_key(credentials: Annotated[HTTPBasicCredentials, Depends(se
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED, detail="API key not found")
expected_api_key = expected_api_key[0]
# Cache for 1 hour
api_key_data = {
"name": expected_api_key["name"],
"key_value": expected_api_key["key_value"],
Expand All @@ -44,3 +44,14 @@ async def verify_api_key(credentials: Annotated[HTTPBasicCredentials, Depends(se
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid API key")
return APIKeyVerificationResponse(is_valid=True, message="API key is valid")


async def require_admin_secret(request: Request):
if not settings.admin_api_key:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Admin secret not configured")
admin_secret = request.headers.get("X-Admin-Secret")
if admin_secret != settings.admin_api_key:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN, detail="Admin access required")
return True
4 changes: 2 additions & 2 deletions app/core/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,9 @@
smtp_port=config("SMTP_PORT", default=587, cast=int),
smtp_user=config("SMTP_USER", default="user"),
smtp_password=config("SMTP_PASSWORD", default="password"),
# Admin SMTP settings for internal notification service
admin_api_key=config("ADMIN_API_KEY", default=""),
admin_smtp_server=config("ADMIN_SMTP_SERVER", default=None),
admin_smtp_port=config("ADMIN_SMTP_PORT", default=None, cast=int),
admin_smtp_port=config("ADMIN_SMTP_PORT", default=None),
admin_smtp_user=config("ADMIN_SMTP_USER", default=None),
admin_smtp_password=config("ADMIN_SMTP_PASSWORD", default=None),
paydunya_public_key=config("PAYDUNYA_PUBLIC_KEY", default=None),
Expand Down
24 changes: 24 additions & 0 deletions app/database/migrations.py
Original file line number Diff line number Diff line change
Expand Up @@ -521,6 +521,24 @@
REFERENCES events(id)
ON DELETE CASCADE
);""",
"""
CREATE TABLE IF NOT EXISTS feedbacks (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
sex VARCHAR(64),
age VARCHAR(32),
profession VARCHAR(255),
country VARCHAR(120),
python_level VARCHAR(120),
heard TEXT,
rating INTEGER CHECK (rating >= 1 AND rating <= 5),
overall TEXT,
favorite TEXT,
improvements TEXT,
comments TEXT,
is_resolved BOOLEAN NOT NULL DEFAULT FALSE,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);""",

]

Expand All @@ -539,6 +557,12 @@

ALTER_TABLE_QUERIES = [
"ALTER TABLE sponsors_partners ADD COLUMN IF NOT EXISTS package_tier package_tier_enum;",
"ALTER TABLE feedbacks DROP COLUMN IF EXISTS event_code;",
"ALTER TABLE feedbacks DROP COLUMN IF EXISTS name;",
"ALTER TABLE feedbacks DROP COLUMN IF EXISTS email;",
"ALTER TABLE feedbacks DROP COLUMN IF EXISTS subject;",
"ALTER TABLE feedbacks DROP COLUMN IF EXISTS message;",
"ALTER TABLE feedbacks ADD COLUMN IF NOT EXISTS days JSONB DEFAULT '[]'::jsonb;",
]


Expand Down
22 changes: 6 additions & 16 deletions app/database/orm.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,8 @@ async def select(db: Connection, table, columns=None, filter=None):
result = await cur.fetchall()
return result
except Exception as e:

logger.error(f"Error executing select query on {table}: {str(e)}")
# TODO: sent email to admin about error during select query execution
raise


async def select_with_join(db: Connection, table, join_table, join_condition, columns=None, filter=None):
Expand All @@ -36,26 +35,23 @@ async def select_with_join(db: Connection, table, join_table, join_condition, co

return result
except Exception as e:

logger.error(
f"Error executing select with join query on {table} and {join_table}: {str(e)}")
# TODO: sent email to admin about error during select with join query execution
raise


async def select_with_multiple_joins(db: Connection, table, joins, columns=None, filter=None):
try:

query, values = generate_multiple_joins_query(
table, joins, columns, filter)
async with db.cursor(row_factory=dict_row) as cur:
await cur.execute(query, values)
result = await cur.fetchall()
return result
except Exception as e:

logger.error(
f"Error executing select with multiple joins query on {table}: {str(e)}")
# TODO: sent email to admin about error during select with multiple joins query execution
raise


async def insert(db: Connection, table, data):
Expand All @@ -69,24 +65,20 @@ async def insert(db: Connection, table, data):
await cur.execute(query, values)
await db.commit()
except Exception as e:

logger.error(f"Error inserting record into {table}: {str(e)}")
# TODO: sent email to admin about error during insert query execution
raise


async def update(db: Connection, table, data, filter):
try:
data = remove_null_values(data)
query, values = generate_update_query(table, data, filter)
async with db.cursor() as cur:

await cur.execute(query, values)
await db.commit()
except Exception as e:

logger.error(f"Error updating record in {table}: {str(e)}")
# TODO: Log the error can be done here
# TODO: sent email to admin about error during update query execution
raise


async def delete(db: Connection, table, filter):
Expand All @@ -96,7 +88,5 @@ async def delete(db: Connection, table, filter):
await cur.execute(query, values)
await db.commit()
except Exception as e:

logger.error(f"Error deleting record from {table}: {str(e)}")
# TODO: Log the error can be done here
# TODO: sent email to admin about error during delete query execution
raise
2 changes: 2 additions & 0 deletions app/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from app.routers.api import api_routers
from app.routers.notifications import api_router as notifications_router
from app.webhooks.payments_callback import api_router as payments_callback_router
from app.routers.feedback_public import api_router as feedback_public_router
from app.core.settings import logger, settings
from pathlib import Path
from datetime import datetime, timezone
Expand Down Expand Up @@ -150,3 +151,4 @@ async def favicon():
app.include_router(api_routers)
app.include_router(payments_callback_router)
app.include_router(notifications_router)
app.include_router(feedback_public_router)
2 changes: 2 additions & 0 deletions app/routers/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
from app.routers.vauchers import api_router as vauchers_router
from app.routers.teams import api_router as teams_router
from app.routers.access_grant import api_router as access_grant_router
from app.routers.feedbacks import api_router as feedbacks_router
from fastapi import APIRouter
from app.core.security import verify_api_key

Expand All @@ -40,3 +41,4 @@
api_routers.include_router(vauchers_router)
api_routers.include_router(teams_router)
api_routers.include_router(access_grant_router)
api_routers.include_router(feedbacks_router)
26 changes: 26 additions & 0 deletions app/routers/feedback_public.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
from fastapi import APIRouter, Request, status, HTTPException

from app.utils.feedback import add_feedback

from app.schemas.models import (
FeedbackBase,
MessageResponse,
)
from app.core.settings import logger


api_router = APIRouter(prefix="/api/feedback", tags=["feedback"])


@api_router.post("/", response_model=MessageResponse, status_code=status.HTTP_201_CREATED)
async def submit_public_feedback(request: Request, payload: FeedbackBase):
"""Public endpoint to submit feedback without API key."""
try:
result = await add_feedback(request.app.state.db_pool, payload.model_dump(mode="json"))
return result
except Exception as e:
logger.error(f"Error adding public feedback: {str(e)}")
if isinstance(e, HTTPException):
raise e
raise HTTPException(
status_code=500, detail="Error adding feedback")
93 changes: 93 additions & 0 deletions app/routers/feedbacks.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
from fastapi import APIRouter, BackgroundTasks, Depends, Request, status, HTTPException

from app.utils.feedback import (
add_feedback, get_feedback_by_id, get_all_feedbacks, update_feedback, delete_feedback)

from app.schemas.models import (
FeedbackSummary,
MessageResponse,
FeedbackUpdate,
FeedbackBase,

)
from app.database.connection import get_db_connection
from app.core.settings import logger


api_router = APIRouter(prefix="/feedbacks", tags=["feedbacks"])


@api_router.get("/", response_model=list[FeedbackSummary])
async def _get_all_feedbacks(db=Depends(get_db_connection)):
try:
feedbacks = await get_all_feedbacks(db)
if not feedbacks:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND,
detail="No feedbacks found")
return feedbacks
except Exception as e:
logger.error(f"Error retrieving feedbacks: {str(e)}")
if isinstance(e, HTTPException):
raise e
raise HTTPException(
status_code=500, detail="Error retrieving feedbacks")


@api_router.get("/{feedback_id}", response_model=FeedbackSummary)
async def _get_feedback_by_id(feedback_id: str, db=Depends(get_db_connection)):
try:
feedback = await get_feedback_by_id(db, feedback_id)
if not feedback:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND,
detail=f"Feedback with id {feedback_id} not found")
return feedback
except Exception as e:
logger.error(
f"Error retrieving feedback with id {feedback_id}: {str(e)}")
if isinstance(e, HTTPException):
raise e
raise HTTPException(
status_code=500, detail="Error retrieving feedback")


@api_router.post("/send", response_model=MessageResponse, status_code=status.HTTP_201_CREATED)
async def add_feedback_message(request: Request, payload: FeedbackBase):
"""Add a new feedback."""
try:
result = await add_feedback(request.app.state.db_pool, payload.model_dump(mode="json"))
return result
except Exception as e:
logger.error(f"Error adding feedback: {str(e)}")
if isinstance(e, HTTPException):
raise e
raise HTTPException(
status_code=500, detail="Error adding feedback")


@api_router.put("/{feedback_id}", response_model=MessageResponse)
async def _update_feedback(feedback_id: str, payload: FeedbackUpdate, db=Depends(get_db_connection)):
try:
data_to_update = {k: v for k,
v in payload.model_dump(mode="json").items() if v is not None}

result = await update_feedback(db, feedback_id, data_to_update)
return result
except Exception as e:
logger.error(f"Error updating feedback: {str(e)}")
if isinstance(e, HTTPException):
raise e
raise HTTPException(
status_code=500, detail="Error updating feedback")


@api_router.delete("/{feedback_id}", response_model=MessageResponse)
async def _delete_feedback(feedback_id: str, db=Depends(get_db_connection)):
try:
result = await delete_feedback(db, feedback_id)
return result
except Exception as e:
logger.error(f"Error deleting feedback: {str(e)}")
if isinstance(e, HTTPException):
raise e
raise HTTPException(
status_code=500, detail="Error deleting feedback")
19 changes: 18 additions & 1 deletion app/routers/registrations.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,18 @@
AttendeeID,
TicketSubmissionPayload
)
from uuid import uuid4
from uuid import uuid4, UUID
import httpx
from app.utils.tickets import get_ticket_by_id
from app.database.orm import select, select_with_join
from app.routers.helper import submit_ticket
from app.utils.registrations import (
create_registration,
get_all_registrations,
)
from app.payments.paydunya_service import create_invoice
from app.core.security import require_admin_secret
from app.database.orm import select as db_select


api_router = APIRouter(tags=["registrations"])
Expand Down Expand Up @@ -145,3 +148,17 @@ async def _approve_student_registration(registration_id: AttendeeID, db=Depends(
if isinstance(e, HTTPException):
raise e
raise HTTPException(status_code=500, detail="Internal server error")


@api_router.get("/registrations", dependencies=[Depends(require_admin_secret)])
async def _list_registrations(request: Request, event_id: UUID | None = None, db=Depends(get_db_connection)):
try:
registrations = await get_all_registrations(db, event_id=event_id)
if not registrations:
return []
return registrations
except Exception as e:
logger.error(f"Error listing registrations: {str(e)}")
if isinstance(e, HTTPException):
raise e
raise HTTPException(status_code=500, detail="Error listing registrations")
1 change: 1 addition & 0 deletions app/schemas/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ class Config(BaseModel):
smtp_port: int = 587
smtp_user: str = "user"
smtp_password: str = "password"
admin_api_key: str = ""
admin_smtp_server: str | None = None
admin_smtp_port: int | None = None
admin_smtp_user: str | None = None
Expand Down
Loading