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
45 changes: 37 additions & 8 deletions backend/app/api/v1/routes/events.py
Original file line number Diff line number Diff line change
Expand Up @@ -126,12 +126,27 @@ def delete_event_route(event_id: int, db: Session = Depends(get_db)):
return db_event


@router.get("/{event_id}/schema.json", response_class=JSONResponse)
@router.get(
"/{event_id}/schema.json",
response_class=JSONResponse,
summary="Export event as JSON Schema",
description="Generate a JSON Schema for the event's data structure. Useful for validation and documentation.",
responses={
200: {"description": "JSON Schema generated successfully"},
404: {"description": "Event not found"},
},
)
def get_event_json_schema(
event_id: int,
include_descriptions: bool = Query(True),
include_examples: bool = Query(True),
additional_properties: bool = Query(True),
include_descriptions: bool = Query(
True, description="Include field descriptions in schema"
),
include_examples: bool = Query(
True, description="Include field examples in schema"
),
additional_properties: bool = Query(
True, description="Allow additional properties in schema"
),
db: Session = Depends(get_db),
):
db_event = event_crud.get_event(db=db, event_id=event_id)
Expand All @@ -148,12 +163,26 @@ def get_event_json_schema(
return schema


@router.get("/{event_id}/schema.yaml")
@router.get(
"/{event_id}/schema.yaml",
summary="Export event as YAML Schema",
description="Generate a YAML Schema for the event's data structure. Same as JSON but in YAML format.",
responses={
200: {"description": "YAML Schema generated successfully"},
404: {"description": "Event not found"},
},
)
def get_event_yaml_schema(
event_id: int,
include_descriptions: bool = Query(True),
include_examples: bool = Query(True),
additional_properties: bool = Query(True),
include_descriptions: bool = Query(
True, description="Include field descriptions in schema"
),
include_examples: bool = Query(
True, description="Include field examples in schema"
),
additional_properties: bool = Query(
True, description="Allow additional properties in schema"
),
db: Session = Depends(get_db),
):
db_event = event_crud.get_event(db=db, event_id=event_id)
Expand Down
27 changes: 22 additions & 5 deletions backend/app/api/v1/routes/fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@
status_code=status.HTTP_201_CREATED,
summary="Create a field",
description="Create a new field that can be associated with events.",
responses={
201: {"description": "Field created successfully"},
400: {"description": "Validation error"},
},
)
def create_field_route(field: FieldCreate, db: Session = Depends(get_db)):
return field_crud.create_field(db=db, field=field)
Expand All @@ -27,6 +31,7 @@ def create_field_route(field: FieldCreate, db: Session = Depends(get_db)):
response_model=list[FieldOut],
summary="List all fields",
description="Return a paginated list of all fields that can be assigned to events.",
responses={200: {"description": "List of fields returned successfully"}},
)
def list_fields_route(db: Session = Depends(get_db)):
return field_crud.get_fields(db=db)
Expand All @@ -36,12 +41,17 @@ def list_fields_route(db: Session = Depends(get_db)):
"/{field_id}",
response_model=FieldOut | FieldOutWithEventCount,
summary="Get field by ID",
description="Return a single field by its ID.",
responses={404: {"description": "Field not found"}},
description="Return a single field by its ID. Optionally include count of events using this field.",
responses={
200: {"description": "Field found and returned"},
404: {"description": "Field not found"},
},
)
def get_field_route(
field_id: int,
with_event_count: bool = Query(False),
with_event_count: bool = Query(
False, description="Include count of events using this field"
),
db: Session = Depends(get_db),
):
db_field = field_crud.get_field(db=db, field_id=field_id)
Expand All @@ -60,7 +70,11 @@ def get_field_route(
response_model=FieldOut,
summary="Update a field",
description="Update the name, description, or type of a field.",
responses={404: {"description": "Field not found"}},
responses={
200: {"description": "Field updated successfully"},
404: {"description": "Field not found"},
400: {"description": "Validation error"},
},
)
def update_field_route(
field_id: int, field: FieldCreate, db: Session = Depends(get_db)
Expand All @@ -76,7 +90,10 @@ def update_field_route(
response_model=FieldOut,
summary="Delete a field",
description="Delete a field by its ID. This will remove the field from all related events.",
responses={404: {"description": "Field not found"}},
responses={
200: {"description": "Field deleted successfully"},
404: {"description": "Field not found"},
},
)
def delete_field_route(field_id: int, db: Session = Depends(get_db)):
db_field = field_crud.delete_field(db=db, field_id=field_id)
Expand Down
34 changes: 28 additions & 6 deletions backend/app/api/v1/routes/generic.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,26 +8,48 @@
router = APIRouter()


@router.get("/ping")
@router.get(
"/ping",
summary="Health check",
description="Simple health check endpoint to verify API is running.",
responses={200: {"description": "API is healthy"}},
)
def ping(settings: Settings = Depends(get_settings)):
return {"pong": True, "debug_mode": settings.debug}
return {"pong": True, "debug_mode": settings.is_dev}


@router.get("/config")
@router.get(
"/config",
summary="Get configuration",
description="Get basic configuration information about the API instance.",
responses={200: {"description": "Configuration returned"}},
)
def get_config(settings: Settings = Depends(get_settings)):
return {
"database_url": settings.database_url,
"debug": settings.debug,
"debug": settings.is_dev,
}


@router.get("/link-types", response_model=list[str])
@router.get(
"/link-types",
response_model=list[str],
summary="Get link types",
description="Get available link types for event external links (figma, confluence, etc).",
responses={200: {"description": "List of available link types"}},
)
async def get_link_types(response: Response):
response.headers["Cache-Control"] = "public, max-age=3600"
return [link_type.value for link_type in LinkType]


@router.get("/field-types", response_model=list[str])
@router.get(
"/field-types",
response_model=list[str],
summary="Get field types",
description="Get available field data types (string, number, boolean, etc).",
responses={200: {"description": "List of available field types"}},
)
async def get_field_types(response: Response):
response.headers["Cache-Control"] = "public, max-age=3600"
return [field_type.value for field_type in FieldType]
21 changes: 18 additions & 3 deletions backend/app/api/v1/routes/tags.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@
status_code=status.HTTP_201_CREATED,
summary="Create a tag",
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"},
},
)
def create_tag_route(tag: TagCreate, db: Session = Depends(get_db)):
return tag_crud.create_tag(db=db, tag=tag)
Expand All @@ -27,6 +31,7 @@ def create_tag_route(tag: TagCreate, db: Session = Depends(get_db)):
response_model=list[TagOut],
summary="List all tags",
description="Return a paginated list of all tags available in the system.",
responses={200: {"description": "List of tags returned successfully"}},
)
def list_tags_route(db: Session = Depends(get_db)):
return tag_crud.get_tags(db=db)
Expand All @@ -37,7 +42,10 @@ def list_tags_route(db: Session = Depends(get_db)):
response_model=TagOut,
summary="Get tag by ID",
description="Return a single tag by its unique identifier.",
responses={404: {"description": "Tag not found"}},
responses={
200: {"description": "Tag found and returned"},
404: {"description": "Tag not found"},
},
)
def get_tag_route(tag_id: str, db: Session = Depends(get_db)):
db_tag = tag_crud.get_tag(db=db, tag_id=tag_id)
Expand All @@ -51,7 +59,11 @@ def get_tag_route(tag_id: str, db: Session = Depends(get_db)):
response_model=TagOut,
summary="Update a tag",
description="Update the description of an existing tag.",
responses={404: {"description": "Tag not found"}},
responses={
200: {"description": "Tag updated successfully"},
404: {"description": "Tag not found"},
400: {"description": "Validation error"},
},
)
def update_tag_route(tag_id: str, tag: TagCreate, db: Session = Depends(get_db)):
db_tag = tag_crud.update_tag(db=db, tag_id=tag_id, tag=tag)
Expand All @@ -65,7 +77,10 @@ def update_tag_route(tag_id: str, tag: TagCreate, db: Session = Depends(get_db))
response_model=TagOut,
summary="Delete a tag",
description="Delete a tag by its ID. This will remove the tag from all related events.",
responses={404: {"description": "Tag not found"}},
responses={
200: {"description": "Tag deleted successfully"},
404: {"description": "Tag not found"},
},
)
def delete_tag_route(tag_id: str, db: Session = Depends(get_db)):
db_tag = tag_crud.delete_tag(db=db, tag_id=tag_id)
Expand Down
8 changes: 4 additions & 4 deletions backend/app/modules/admin/seed/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
from app.shared.service import assert_db_empty


def seed_all(db: Session):
def seed_all(db: Session, n_tags=10, n_fields=10, n_events=10):
assert_db_empty(db)
seed_tags(db)
seed_fields(db)
seed_events(db)
seed_tags(db, count=n_tags)
seed_fields(db, count=n_fields)
seed_events(db, count=n_events)
87 changes: 78 additions & 9 deletions backend/app/modules/auth/router.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,14 +29,29 @@
response_model=TokenOut,
status_code=status.HTTP_201_CREATED,
dependencies=[Depends(ensure_not_demo)],
summary="Create user account",
description="Register a new user account with email and password. Returns authentication token.",
responses={
201: {"description": "User created successfully"},
400: {"description": "User already exists or validation error"},
},
)
def signup(user_in: UserCreate, db: Session = Depends(get_db)):
user = service.create_user(db, user_in)
token = create_access_token({"sub": user.email})
return {"access_token": token, "token_type": "bearer"}


@router.post("/login", response_model=TokenOut)
@router.post(
"/login",
response_model=TokenOut,
summary="Login with email and password",
description="Authenticate user with email and password credentials. Returns authentication token.",
responses={
200: {"description": "Login successful"},
401: {"description": "Invalid credentials"},
},
)
def login(user_in: UserLogin, db: Session = Depends(get_db)):
user = crud.get_user_by_email(db, user_in.email)
if (
Expand All @@ -51,20 +66,47 @@ def login(user_in: UserLogin, db: Session = Depends(get_db)):
return {"access_token": token, "token_type": "bearer"}


@router.post("/oauth", response_model=TokenOut, dependencies=[Depends(ensure_not_demo)])
@router.post(
"/oauth",
response_model=TokenOut,
dependencies=[Depends(ensure_not_demo)],
summary="OAuth login",
description="Authenticate user with OAuth provider (GitHub, Google). Creates account if it doesn't exist.",
responses={
200: {"description": "OAuth login successful"},
400: {"description": "Invalid OAuth payload"},
},
)
def login_oauth(payload: OAuthLogin, db: Session = Depends(get_db)):
email = oauth.get_email_from_oauth(payload)
user = service.get_or_create_oauth_user(db, email=email, provider=payload.provider)
token = create_access_token({"sub": user.email})
return {"access_token": token, "token_type": "bearer"}


@router.get("/me", response_model=schemas.UserOut)
@router.get(
"/me",
response_model=schemas.UserOut,
summary="Get current user",
description="Get details of the currently authenticated user.",
responses={
200: {"description": "User details returned"},
401: {"description": "Not authenticated"},
},
)
def read_current_user(current_user: User = Depends(get_current_user)):
return current_user


@router.post("/token")
@router.post(
"/token",
summary="Get access token (OAuth2 compatible)",
description="OAuth2 compatible token endpoint for form-based authentication.",
responses={
200: {"description": "Token generated successfully"},
400: {"description": "Invalid credentials"},
},
)
def login_for_access_token(
form_data: OAuth2PasswordRequestForm = Depends(),
db: Session = Depends(get_db),
Expand All @@ -79,11 +121,22 @@ def login_for_access_token(
return {"access_token": token, "token_type": "bearer"}


@router.get("/oauth/init/{provider}", dependencies=[Depends(ensure_not_demo)])
@router.get(
"/oauth/init/{provider}",
dependencies=[Depends(ensure_not_demo)],
summary="Start OAuth flow",
description="Initiate OAuth authentication with GitHub or Google. Redirects to provider.",
responses={
302: {"description": "Redirect to OAuth provider"},
400: {"description": "Invalid provider"},
},
)
def start_oauth_login(
provider: ProviderName,
request: Request,
redirect: str = Query("/events"),
redirect: str = Query(
"/events", description="Where to redirect after successful login"
),
):
redirect_uri = request.url_for("oauth_callback")

Expand All @@ -98,10 +151,20 @@ def start_oauth_login(


@router.get(
"/oauth/callback", name="oauth_callback", dependencies=[Depends(ensure_not_demo)]
"/oauth/callback",
name="oauth_callback",
dependencies=[Depends(ensure_not_demo)],
summary="OAuth callback",
description="Handle OAuth provider callback. Internal endpoint used by OAuth flow.",
responses={
302: {"description": "Redirect to frontend with auth code"},
400: {"description": "Invalid callback parameters"},
},
)
def handle_oauth_callback(
code: str = Query(...), state: str = Query(...), settings=Depends(get_settings)
code: str = Query(..., description="OAuth authorization code from provider"),
state: str = Query(..., description="OAuth state parameter"),
settings=Depends(get_settings),
):
try:
decoded = json.loads(base64.b64decode(state).decode())
Expand All @@ -116,6 +179,12 @@ def handle_oauth_callback(
return RedirectResponse(url=final_url)


@router.get("/providers", tags=["auth"])
@router.get(
"/providers",
tags=["auth"],
summary="List OAuth providers",
description="Get list of available OAuth authentication providers.",
responses={200: {"description": "List of available OAuth providers"}},
)
def list_oauth_providers(settings=Depends(get_settings)):
return {"providers": settings.available_oauth_providers}
Loading