Skip to content
Merged
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
5 changes: 5 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@ DATABASE_URL=postgresql+psycopg2://evsy:evsy@db:5432/evsy
# For local dev use http://localhost:3000
FRONTEND_URL=http://localhost:3000

# Public URL of the backend (used for building OAuth callback URLs)
# For local dev use http://localhost:8000
# IMPORTANT: In production, this must match the actual backend domain.
BACKEND_URL=http://localhost:8000


# ========================
# Frontend Configuration
Expand Down
38 changes: 28 additions & 10 deletions backend/app/api/v1/routes/fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,20 @@
description="Create a new field that can be associated with events.",
responses={
201: {"description": "Field created successfully"},
400: {"description": "Validation error"},
400: {"description": "Validation error or field already exists"},
},
)
def create_field_route(field: FieldCreate, db: Session = Depends(get_db)):
return field_crud.create_field(db=db, field=field)
try:
return field_crud.create_field(db=db, field=field)
except ValueError as e:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail={
"code": "duplicate_resource",
"message": str(e),
},
) from e


@router.get(
Expand Down Expand Up @@ -79,22 +88,31 @@ def get_field_route(
responses={
200: {"description": "Field updated successfully"},
404: {"description": "Field not found"},
400: {"description": "Validation error"},
400: {"description": "Validation error or field already exists"},
},
)
def update_field_route(
field_id: int, field: FieldCreate, db: Session = Depends(get_db)
):
db_field = field_crud.update_field(db=db, field_id=field_id, field=field)
if db_field is None:
try:
db_field = field_crud.update_field(db=db, field_id=field_id, field=field)
if db_field is None:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail={
"code": "resource_not_found",
"message": f"Field with id {field_id} not found",
},
)
return db_field
except ValueError as e:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
status_code=status.HTTP_400_BAD_REQUEST,
detail={
"code": "resource_not_found",
"message": f"Field with id {field_id} not found",
"code": "duplicate_resource",
"message": str(e),
},
)
return db_field
) from e


@router.delete(
Expand Down
13 changes: 11 additions & 2 deletions backend/app/api/v1/routes/tags.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,20 @@
description="Create a tag manually. Typically, tags are created automatically when creating or updating an event.",
responses={
201: {"description": "Tag created successfully"},
400: {"description": "Validation error"},
400: {"description": "Validation error or tag already exists"},
},
)
def create_tag_route(tag: TagCreate, db: Session = Depends(get_db)):
return tag_crud.create_tag(db=db, tag=tag)
try:
return tag_crud.create_tag(db=db, tag=tag)
except ValueError as e:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail={
"code": "duplicate_resource",
"message": str(e),
},
) from e


@router.get(
Expand Down
2 changes: 1 addition & 1 deletion backend/app/modules/auth/oauth.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,5 +129,5 @@ def exchange_code_for_email(provider: str, code: str, redirect_uri: str) -> str:


def get_email_from_oauth(login: OAuthLogin) -> str:
redirect_uri = "http://localhost:8000/api/v1/auth/oauth/callback"
redirect_uri = f"{settings.backend_url}/api/v1/auth/oauth/callback"
return exchange_code_for_email(login.provider, login.token, redirect_uri)
2 changes: 2 additions & 0 deletions backend/app/modules/auth/router.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import base64
import json
import secrets

from fastapi import APIRouter, Depends, HTTPException, Query, Request, status
from fastapi.responses import RedirectResponse
Expand Down Expand Up @@ -143,6 +144,7 @@ def start_oauth_login(
state_payload = {
"provider": provider,
"redirect": redirect,
"nonce": secrets.token_urlsafe(16),
}
state = base64.urlsafe_b64encode(json.dumps(state_payload).encode()).decode()

Expand Down
7 changes: 5 additions & 2 deletions backend/app/modules/auth/schemas.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from typing import Annotated, Literal, Optional

from pydantic import BaseModel, ConfigDict, EmailStr, StringConstraints
from pydantic import BaseModel, ConfigDict, EmailStr, Field, StringConstraints

PasswordStr = Annotated[str, StringConstraints(min_length=6)]
ProviderName = Literal["github", "google"]
Expand All @@ -21,7 +21,10 @@ class UserLogin(UserBase):

class OAuthLogin(BaseModel):
provider: ProviderName
token: str
token: str = Field(
...,
description="The authorization code received from the OAuth provider callback.",
)


class UserOut(UserBase):
Expand Down
8 changes: 4 additions & 4 deletions backend/app/modules/events/crud.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,6 @@ def create_event(db: Session, event: schemas.EventCreate):
return db_event


def get_event(db: Session, event_id: int):
return db.query(models.Event).filter(models.Event.id == event_id).first()


def get_events(db: Session):
return (
db.query(models.Event)
Expand All @@ -40,6 +36,10 @@ def get_events(db: Session):
)


def get_event(db: Session, event_id: int):
return db.query(models.Event).filter(models.Event.id == event_id).first()


def update_event(db: Session, event_id: int, event: schemas.EventCreate):
db_event = db.query(models.Event).filter(models.Event.id == event_id).first()
if db_event is None:
Expand Down
5 changes: 1 addition & 4 deletions backend/app/modules/events/schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,11 +42,8 @@ class EventCreate(EventBase):
pass


class EventOut(BaseModel):
class EventOut(EventBase):
id: int
name: str
description: Optional[str] = None
links: Optional[List[EventLink]] = None
tags: list[TagOut]
fields: list[FieldOut]
created_at: datetime
Expand Down
17 changes: 10 additions & 7 deletions backend/app/modules/fields/crud.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
from fastapi import HTTPException
from sqlalchemy import func
from sqlalchemy.exc import IntegrityError
from sqlalchemy.orm import Session
Expand All @@ -19,9 +18,7 @@ def create_field(db: Session, field: schemas.FieldCreate):
return db_field
except IntegrityError:
db.rollback()
raise HTTPException(
status_code=400, detail=f"Field with name {field.name!r} already exists."
) from None
raise ValueError(f"Field with name {field.name!r} already exists.") from None


def get_fields(db: Session):
Expand All @@ -38,9 +35,15 @@ def update_field(db: Session, field_id: int, field: schemas.FieldCreate):
db_field.name = field.name
db_field.description = field.description
db_field.field_type = field.field_type
db.commit()
db.refresh(db_field)
return db_field
try:
db.commit()
db.refresh(db_field)
return db_field
except IntegrityError:
db.rollback()
raise ValueError(
f"Field with name {field.name!r} already exists."
) from None
return None


Expand Down
11 changes: 8 additions & 3 deletions backend/app/modules/tags/crud.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from sqlalchemy.exc import IntegrityError
from sqlalchemy.orm import Session

from app.shared.models import EventTag
Expand All @@ -8,9 +9,13 @@
def create_tag(db: Session, tag: schemas.TagCreate):
db_tag = models.Tag(id=tag.id, description=tag.description)
db.add(db_tag)
db.commit()
db.refresh(db_tag)
return db_tag
try:
db.commit()
db.refresh(db_tag)
return db_tag
except IntegrityError:
db.rollback()
raise ValueError(f"Tag with id {tag.id!r} already exists.") from None


def get_tags(db: Session):
Expand Down
8 changes: 6 additions & 2 deletions backend/app/modules/tags/schemas.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
from datetime import datetime

from pydantic import BaseModel, ConfigDict
from pydantic import BaseModel, ConfigDict, Field


class TagBase(BaseModel):
id: str
id: str = Field(
min_length=1,
max_length=50,
description="Tag ID must be between 1 and 50 characters",
)
description: str | None = None


Expand Down
1 change: 1 addition & 0 deletions backend/app/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ def __init__(self, _env_file: Optional[str] = None, **kwargs):
env: Literal["dev", "prod", "demo", "test"] = Field(default="dev", alias="ENV")
database_url: str = "sqlite:///./test.db"
frontend_url: Optional[str] = None
backend_url: str = "http://localhost:8000"

secret_key: str = "your_secret_key_here"

Expand Down
Loading