M-01+M-03: Add input validation and extra=forbid to all request schemas
- Add max_length constraints to all string fields in request schemas, matching DB column limits (title:255, description:5000, etc.) - Add min_length=1 to required name/title fields - Add ConfigDict(extra="forbid") to all request schemas to reject unknown fields (prevents silent field injection) - Add Path(ge=1, le=2147483647) to all integer path parameters across all routers to prevent integer overflow → 500 errors - Add max_length to TOTP inline schemas (code:6, mfa_token:256, etc.) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
581efa183a
commit
2f58282c31
@ -131,7 +131,7 @@ async def list_users(
|
||||
|
||||
@router.get("/users/{user_id}", response_model=UserDetailResponse)
|
||||
async def get_user(
|
||||
user_id: int,
|
||||
user_id: int = Path(ge=1, le=2147483647),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
_actor: User = Depends(get_current_user),
|
||||
):
|
||||
@ -207,8 +207,8 @@ async def create_user(
|
||||
|
||||
@router.put("/users/{user_id}/role")
|
||||
async def update_user_role(
|
||||
user_id: int,
|
||||
data: UpdateUserRoleRequest,
|
||||
user_id: int = Path(ge=1, le=2147483647),
|
||||
data: UpdateUserRoleRequest = ...,
|
||||
request: Request,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
actor: User = Depends(get_current_user),
|
||||
@ -261,8 +261,8 @@ async def update_user_role(
|
||||
|
||||
@router.post("/users/{user_id}/reset-password", response_model=ResetPasswordResponse)
|
||||
async def reset_user_password(
|
||||
user_id: int,
|
||||
request: Request,
|
||||
user_id: int = Path(ge=1, le=2147483647),
|
||||
request: Request = ...,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
actor: User = Depends(get_current_user),
|
||||
):
|
||||
@ -306,8 +306,8 @@ async def reset_user_password(
|
||||
|
||||
@router.post("/users/{user_id}/disable-mfa")
|
||||
async def disable_user_mfa(
|
||||
user_id: int,
|
||||
request: Request,
|
||||
user_id: int = Path(ge=1, le=2147483647),
|
||||
request: Request = ...,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
actor: User = Depends(get_current_user),
|
||||
):
|
||||
@ -356,8 +356,8 @@ async def disable_user_mfa(
|
||||
|
||||
@router.put("/users/{user_id}/enforce-mfa")
|
||||
async def toggle_mfa_enforce(
|
||||
user_id: int,
|
||||
data: ToggleMfaEnforceRequest,
|
||||
user_id: int = Path(ge=1, le=2147483647),
|
||||
data: ToggleMfaEnforceRequest = ...,
|
||||
request: Request,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
actor: User = Depends(get_current_user),
|
||||
@ -391,8 +391,8 @@ async def toggle_mfa_enforce(
|
||||
|
||||
@router.put("/users/{user_id}/active")
|
||||
async def toggle_user_active(
|
||||
user_id: int,
|
||||
data: ToggleActiveRequest,
|
||||
user_id: int = Path(ge=1, le=2147483647),
|
||||
data: ToggleActiveRequest = ...,
|
||||
request: Request,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
actor: User = Depends(get_current_user),
|
||||
@ -434,8 +434,8 @@ async def toggle_user_active(
|
||||
|
||||
@router.delete("/users/{user_id}/sessions")
|
||||
async def revoke_user_sessions(
|
||||
user_id: int,
|
||||
request: Request,
|
||||
user_id: int = Path(ge=1, le=2147483647),
|
||||
request: Request = ...,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
actor: User = Depends(get_current_user),
|
||||
):
|
||||
@ -465,7 +465,7 @@ async def revoke_user_sessions(
|
||||
|
||||
@router.get("/users/{user_id}/sessions")
|
||||
async def list_user_sessions(
|
||||
user_id: int,
|
||||
user_id: int = Path(ge=1, le=2147483647),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
_actor: User = Depends(get_current_user),
|
||||
):
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from fastapi import APIRouter, Depends, HTTPException, Path
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select, update
|
||||
from typing import List
|
||||
@ -48,8 +48,8 @@ async def create_calendar(
|
||||
|
||||
@router.put("/{calendar_id}", response_model=CalendarResponse)
|
||||
async def update_calendar(
|
||||
calendar_id: int,
|
||||
calendar_update: CalendarUpdate,
|
||||
calendar_id: int = Path(ge=1, le=2147483647),
|
||||
calendar_update: CalendarUpdate = ...,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
@ -77,7 +77,7 @@ async def update_calendar(
|
||||
|
||||
@router.delete("/{calendar_id}", status_code=204)
|
||||
async def delete_calendar(
|
||||
calendar_id: int,
|
||||
calendar_id: int = Path(ge=1, le=2147483647),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from fastapi import APIRouter, Depends, HTTPException, Path, status
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select
|
||||
|
||||
@ -43,8 +43,8 @@ async def create_template(
|
||||
|
||||
@router.put("/{template_id}", response_model=EventTemplateResponse)
|
||||
async def update_template(
|
||||
template_id: int,
|
||||
payload: EventTemplateUpdate,
|
||||
template_id: int = Path(ge=1, le=2147483647),
|
||||
payload: EventTemplateUpdate = ...,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
@ -68,7 +68,7 @@ async def update_template(
|
||||
|
||||
@router.delete("/{template_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def delete_template(
|
||||
template_id: int,
|
||||
template_id: int = Path(ge=1, le=2147483647),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import json
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from fastapi import APIRouter, Depends, HTTPException, Path, Query
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select, delete
|
||||
from sqlalchemy.orm import selectinload
|
||||
@ -272,7 +272,7 @@ async def create_event(
|
||||
|
||||
@router.get("/{event_id}", response_model=CalendarEventResponse)
|
||||
async def get_event(
|
||||
event_id: int,
|
||||
event_id: int = Path(ge=1, le=2147483647),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
@ -296,8 +296,8 @@ async def get_event(
|
||||
|
||||
@router.put("/{event_id}", response_model=CalendarEventResponse)
|
||||
async def update_event(
|
||||
event_id: int,
|
||||
event_update: CalendarEventUpdate,
|
||||
event_id: int = Path(ge=1, le=2147483647),
|
||||
event_update: CalendarEventUpdate = ...,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
@ -421,7 +421,7 @@ async def update_event(
|
||||
|
||||
@router.delete("/{event_id}", status_code=204)
|
||||
async def delete_event(
|
||||
event_id: int,
|
||||
event_id: int = Path(ge=1, le=2147483647),
|
||||
scope: Optional[Literal["this", "this_and_future"]] = Query(None),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from fastapi import APIRouter, Depends, HTTPException, Path, Query
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select, or_
|
||||
from datetime import datetime, timezone
|
||||
@ -120,7 +120,7 @@ async def create_location(
|
||||
|
||||
@router.get("/{location_id}", response_model=LocationResponse)
|
||||
async def get_location(
|
||||
location_id: int,
|
||||
location_id: int = Path(ge=1, le=2147483647),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
@ -138,8 +138,8 @@ async def get_location(
|
||||
|
||||
@router.put("/{location_id}", response_model=LocationResponse)
|
||||
async def update_location(
|
||||
location_id: int,
|
||||
location_update: LocationUpdate,
|
||||
location_id: int = Path(ge=1, le=2147483647),
|
||||
location_update: LocationUpdate = ...,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
@ -168,7 +168,7 @@ async def update_location(
|
||||
|
||||
@router.delete("/{location_id}", status_code=204)
|
||||
async def delete_location(
|
||||
location_id: int,
|
||||
location_id: int = Path(ge=1, le=2147483647),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from fastapi import APIRouter, Depends, HTTPException, Path, Query
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select, or_
|
||||
from datetime import datetime, timezone
|
||||
@ -91,7 +91,7 @@ async def create_person(
|
||||
|
||||
@router.get("/{person_id}", response_model=PersonResponse)
|
||||
async def get_person(
|
||||
person_id: int,
|
||||
person_id: int = Path(ge=1, le=2147483647),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
@ -109,8 +109,8 @@ async def get_person(
|
||||
|
||||
@router.put("/{person_id}", response_model=PersonResponse)
|
||||
async def update_person(
|
||||
person_id: int,
|
||||
person_update: PersonUpdate,
|
||||
person_id: int = Path(ge=1, le=2147483647),
|
||||
person_update: PersonUpdate = ...,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
@ -146,7 +146,7 @@ async def update_person(
|
||||
|
||||
@router.delete("/{person_id}", status_code=204)
|
||||
async def delete_person(
|
||||
person_id: int,
|
||||
person_id: int = Path(ge=1, le=2147483647),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from fastapi import APIRouter, Depends, HTTPException, Path, Query
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.orm import selectinload
|
||||
@ -128,7 +128,7 @@ async def create_project(
|
||||
|
||||
@router.get("/{project_id}", response_model=ProjectResponse)
|
||||
async def get_project(
|
||||
project_id: int,
|
||||
project_id: int = Path(ge=1, le=2147483647),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
@ -149,8 +149,8 @@ async def get_project(
|
||||
|
||||
@router.put("/{project_id}", response_model=ProjectResponse)
|
||||
async def update_project(
|
||||
project_id: int,
|
||||
project_update: ProjectUpdate,
|
||||
project_id: int = Path(ge=1, le=2147483647),
|
||||
project_update: ProjectUpdate = ...,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
@ -178,7 +178,7 @@ async def update_project(
|
||||
|
||||
@router.delete("/{project_id}", status_code=204)
|
||||
async def delete_project(
|
||||
project_id: int,
|
||||
project_id: int = Path(ge=1, le=2147483647),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
@ -199,7 +199,7 @@ async def delete_project(
|
||||
|
||||
@router.get("/{project_id}/tasks", response_model=List[ProjectTaskResponse])
|
||||
async def get_project_tasks(
|
||||
project_id: int,
|
||||
project_id: int = Path(ge=1, le=2147483647),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
@ -230,8 +230,8 @@ async def get_project_tasks(
|
||||
|
||||
@router.post("/{project_id}/tasks", response_model=ProjectTaskResponse, status_code=201)
|
||||
async def create_project_task(
|
||||
project_id: int,
|
||||
task: ProjectTaskCreate,
|
||||
project_id: int = Path(ge=1, le=2147483647),
|
||||
task: ProjectTaskCreate = ...,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
@ -279,8 +279,8 @@ async def create_project_task(
|
||||
|
||||
@router.put("/{project_id}/tasks/reorder", status_code=200)
|
||||
async def reorder_tasks(
|
||||
project_id: int,
|
||||
items: List[ReorderItem],
|
||||
project_id: int = Path(ge=1, le=2147483647),
|
||||
items: List[ReorderItem] = ...,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
@ -312,9 +312,9 @@ async def reorder_tasks(
|
||||
|
||||
@router.put("/{project_id}/tasks/{task_id}", response_model=ProjectTaskResponse)
|
||||
async def update_project_task(
|
||||
project_id: int,
|
||||
task_id: int,
|
||||
task_update: ProjectTaskUpdate,
|
||||
project_id: int = Path(ge=1, le=2147483647),
|
||||
task_id: int = Path(ge=1, le=2147483647),
|
||||
task_update: ProjectTaskUpdate = ...,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
@ -356,8 +356,8 @@ async def update_project_task(
|
||||
|
||||
@router.delete("/{project_id}/tasks/{task_id}", status_code=204)
|
||||
async def delete_project_task(
|
||||
project_id: int,
|
||||
task_id: int,
|
||||
project_id: int = Path(ge=1, le=2147483647),
|
||||
task_id: int = Path(ge=1, le=2147483647),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
@ -388,9 +388,9 @@ async def delete_project_task(
|
||||
|
||||
@router.post("/{project_id}/tasks/{task_id}/comments", response_model=TaskCommentResponse, status_code=201)
|
||||
async def create_task_comment(
|
||||
project_id: int,
|
||||
task_id: int,
|
||||
comment: TaskCommentCreate,
|
||||
project_id: int = Path(ge=1, le=2147483647),
|
||||
task_id: int = Path(ge=1, le=2147483647),
|
||||
comment: TaskCommentCreate = ...,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
@ -423,9 +423,9 @@ async def create_task_comment(
|
||||
|
||||
@router.delete("/{project_id}/tasks/{task_id}/comments/{comment_id}", status_code=204)
|
||||
async def delete_task_comment(
|
||||
project_id: int,
|
||||
task_id: int,
|
||||
comment_id: int,
|
||||
project_id: int = Path(ge=1, le=2147483647),
|
||||
task_id: int = Path(ge=1, le=2147483647),
|
||||
comment_id: int = Path(ge=1, le=2147483647),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from fastapi import APIRouter, Depends, HTTPException, Path, Query
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select, and_, or_
|
||||
from typing import Optional, List
|
||||
@ -69,8 +69,8 @@ async def get_due_reminders(
|
||||
|
||||
@router.patch("/{reminder_id}/snooze", response_model=ReminderResponse)
|
||||
async def snooze_reminder(
|
||||
reminder_id: int,
|
||||
body: ReminderSnooze,
|
||||
reminder_id: int = Path(ge=1, le=2147483647),
|
||||
body: ReminderSnooze = ...,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
@ -115,7 +115,7 @@ async def create_reminder(
|
||||
|
||||
@router.get("/{reminder_id}", response_model=ReminderResponse)
|
||||
async def get_reminder(
|
||||
reminder_id: int,
|
||||
reminder_id: int = Path(ge=1, le=2147483647),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
@ -136,8 +136,8 @@ async def get_reminder(
|
||||
|
||||
@router.put("/{reminder_id}", response_model=ReminderResponse)
|
||||
async def update_reminder(
|
||||
reminder_id: int,
|
||||
reminder_update: ReminderUpdate,
|
||||
reminder_id: int = Path(ge=1, le=2147483647),
|
||||
reminder_update: ReminderUpdate = ...,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
@ -175,7 +175,7 @@ async def update_reminder(
|
||||
|
||||
@router.delete("/{reminder_id}", status_code=204)
|
||||
async def delete_reminder(
|
||||
reminder_id: int,
|
||||
reminder_id: int = Path(ge=1, le=2147483647),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
@ -199,7 +199,7 @@ async def delete_reminder(
|
||||
|
||||
@router.patch("/{reminder_id}/dismiss", response_model=ReminderResponse)
|
||||
async def dismiss_reminder(
|
||||
reminder_id: int,
|
||||
reminder_id: int = Path(ge=1, le=2147483647),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from fastapi import APIRouter, Depends, HTTPException, Path, Query
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select, and_, func
|
||||
from typing import Optional, List
|
||||
@ -174,7 +174,7 @@ async def create_todo(
|
||||
|
||||
@router.get("/{todo_id}", response_model=TodoResponse)
|
||||
async def get_todo(
|
||||
todo_id: int,
|
||||
todo_id: int = Path(ge=1, le=2147483647),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
@ -192,8 +192,8 @@ async def get_todo(
|
||||
|
||||
@router.put("/{todo_id}", response_model=TodoResponse)
|
||||
async def update_todo(
|
||||
todo_id: int,
|
||||
todo_update: TodoUpdate,
|
||||
todo_id: int = Path(ge=1, le=2147483647),
|
||||
todo_update: TodoUpdate = ...,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
current_settings: Settings = Depends(get_current_settings),
|
||||
@ -249,7 +249,7 @@ async def update_todo(
|
||||
|
||||
@router.delete("/{todo_id}", status_code=204)
|
||||
async def delete_todo(
|
||||
todo_id: int,
|
||||
todo_id: int = Path(ge=1, le=2147483647),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
@ -270,7 +270,7 @@ async def delete_todo(
|
||||
|
||||
@router.patch("/{todo_id}/toggle", response_model=TodoResponse)
|
||||
async def toggle_todo(
|
||||
todo_id: int,
|
||||
todo_id: int = Path(ge=1, le=2147483647),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
current_settings: Settings = Depends(get_current_settings),
|
||||
|
||||
@ -24,7 +24,7 @@ from datetime import datetime, timedelta
|
||||
from typing import Optional
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Request, Response
|
||||
from pydantic import BaseModel
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select, delete
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
@ -76,32 +76,38 @@ _ph = PasswordHasher(
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TOTPConfirmRequest(BaseModel):
|
||||
code: str
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
code: str = Field(min_length=6, max_length=6)
|
||||
|
||||
|
||||
class TOTPVerifyRequest(BaseModel):
|
||||
mfa_token: str
|
||||
code: Optional[str] = None # 6-digit TOTP code
|
||||
backup_code: Optional[str] = None # Alternative: XXXX-XXXX backup code
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
mfa_token: str = Field(max_length=256)
|
||||
code: Optional[str] = Field(None, min_length=6, max_length=6) # 6-digit TOTP code
|
||||
backup_code: Optional[str] = Field(None, max_length=9) # XXXX-XXXX backup code
|
||||
|
||||
|
||||
class TOTPDisableRequest(BaseModel):
|
||||
password: str
|
||||
code: str # Current TOTP code required to disable
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
password: str = Field(max_length=128)
|
||||
code: str = Field(min_length=6, max_length=6) # Current TOTP code required to disable
|
||||
|
||||
|
||||
class BackupCodesRegenerateRequest(BaseModel):
|
||||
password: str
|
||||
code: str # Current TOTP code required to regenerate
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
password: str = Field(max_length=128)
|
||||
code: str = Field(min_length=6, max_length=6) # Current TOTP code required to regenerate
|
||||
|
||||
|
||||
class EnforceSetupRequest(BaseModel):
|
||||
mfa_token: str
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
mfa_token: str = Field(max_length=256)
|
||||
|
||||
|
||||
class EnforceConfirmRequest(BaseModel):
|
||||
mfa_token: str
|
||||
code: str # 6-digit TOTP code from authenticator app
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
mfa_token: str = Field(max_length=256)
|
||||
code: str = Field(min_length=6, max_length=6) # 6-digit TOTP code from authenticator app
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@ -84,6 +84,8 @@ class LoginRequest(BaseModel):
|
||||
|
||||
|
||||
class ChangePasswordRequest(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
old_password: str
|
||||
new_password: str
|
||||
|
||||
@ -94,6 +96,8 @@ class ChangePasswordRequest(BaseModel):
|
||||
|
||||
|
||||
class VerifyPasswordRequest(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
password: str
|
||||
|
||||
@field_validator("password")
|
||||
|
||||
@ -1,16 +1,20 @@
|
||||
from pydantic import BaseModel, ConfigDict
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
|
||||
|
||||
class CalendarCreate(BaseModel):
|
||||
name: str
|
||||
color: str = "#3b82f6"
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
name: str = Field(min_length=1, max_length=100)
|
||||
color: str = Field("#3b82f6", max_length=20)
|
||||
|
||||
|
||||
class CalendarUpdate(BaseModel):
|
||||
name: Optional[str] = None
|
||||
color: Optional[str] = None
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
name: Optional[str] = Field(None, min_length=1, max_length=100)
|
||||
color: Optional[str] = Field(None, max_length=20)
|
||||
is_visible: Optional[bool] = None
|
||||
|
||||
|
||||
|
||||
@ -39,12 +39,14 @@ def _coerce_recurrence_rule(v):
|
||||
|
||||
|
||||
class CalendarEventCreate(BaseModel):
|
||||
title: str
|
||||
description: Optional[str] = None
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
title: str = Field(min_length=1, max_length=255)
|
||||
description: Optional[str] = Field(None, max_length=5000)
|
||||
start_datetime: datetime
|
||||
end_datetime: datetime
|
||||
all_day: bool = False
|
||||
color: Optional[str] = None
|
||||
color: Optional[str] = Field(None, max_length=20)
|
||||
location_id: Optional[int] = None
|
||||
recurrence_rule: Optional[RecurrenceRule] = None
|
||||
is_starred: bool = False
|
||||
@ -57,12 +59,14 @@ class CalendarEventCreate(BaseModel):
|
||||
|
||||
|
||||
class CalendarEventUpdate(BaseModel):
|
||||
title: Optional[str] = None
|
||||
description: Optional[str] = None
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
title: Optional[str] = Field(None, min_length=1, max_length=255)
|
||||
description: Optional[str] = Field(None, max_length=5000)
|
||||
start_datetime: Optional[datetime] = None
|
||||
end_datetime: Optional[datetime] = None
|
||||
all_day: Optional[bool] = None
|
||||
color: Optional[str] = None
|
||||
color: Optional[str] = Field(None, max_length=20)
|
||||
location_id: Optional[int] = None
|
||||
recurrence_rule: Optional[RecurrenceRule] = None
|
||||
is_starred: Optional[bool] = None
|
||||
|
||||
@ -1,25 +1,29 @@
|
||||
from pydantic import BaseModel, ConfigDict
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
|
||||
|
||||
class EventTemplateCreate(BaseModel):
|
||||
name: str
|
||||
title: str
|
||||
description: Optional[str] = None
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
name: str = Field(min_length=1, max_length=255)
|
||||
title: str = Field(min_length=1, max_length=255)
|
||||
description: Optional[str] = Field(None, max_length=5000)
|
||||
calendar_id: Optional[int] = None
|
||||
recurrence_rule: Optional[str] = None
|
||||
recurrence_rule: Optional[str] = Field(None, max_length=5000)
|
||||
all_day: bool = False
|
||||
location_id: Optional[int] = None
|
||||
is_starred: bool = False
|
||||
|
||||
|
||||
class EventTemplateUpdate(BaseModel):
|
||||
name: Optional[str] = None
|
||||
title: Optional[str] = None
|
||||
description: Optional[str] = None
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
name: Optional[str] = Field(None, min_length=1, max_length=255)
|
||||
title: Optional[str] = Field(None, min_length=1, max_length=255)
|
||||
description: Optional[str] = Field(None, max_length=5000)
|
||||
calendar_id: Optional[int] = None
|
||||
recurrence_rule: Optional[str] = None
|
||||
recurrence_rule: Optional[str] = Field(None, max_length=5000)
|
||||
all_day: Optional[bool] = None
|
||||
location_id: Optional[int] = None
|
||||
is_starred: Optional[bool] = None
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import re
|
||||
from pydantic import BaseModel, ConfigDict, field_validator
|
||||
from pydantic import BaseModel, ConfigDict, Field, field_validator
|
||||
from datetime import datetime
|
||||
from typing import Optional, Literal
|
||||
|
||||
@ -14,13 +14,15 @@ class LocationSearchResult(BaseModel):
|
||||
|
||||
|
||||
class LocationCreate(BaseModel):
|
||||
name: str
|
||||
address: str
|
||||
category: str = "other"
|
||||
notes: Optional[str] = None
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
name: str = Field(min_length=1, max_length=255)
|
||||
address: str = Field(min_length=1, max_length=2000)
|
||||
category: str = Field("other", max_length=100)
|
||||
notes: Optional[str] = Field(None, max_length=5000)
|
||||
is_frequent: bool = False
|
||||
contact_number: Optional[str] = None
|
||||
email: Optional[str] = None
|
||||
contact_number: Optional[str] = Field(None, max_length=50)
|
||||
email: Optional[str] = Field(None, max_length=255)
|
||||
|
||||
@field_validator('email')
|
||||
@classmethod
|
||||
@ -31,13 +33,15 @@ class LocationCreate(BaseModel):
|
||||
|
||||
|
||||
class LocationUpdate(BaseModel):
|
||||
name: Optional[str] = None
|
||||
address: Optional[str] = None
|
||||
category: Optional[str] = None
|
||||
notes: Optional[str] = None
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
name: Optional[str] = Field(None, min_length=1, max_length=255)
|
||||
address: Optional[str] = Field(None, min_length=1, max_length=2000)
|
||||
category: Optional[str] = Field(None, max_length=100)
|
||||
notes: Optional[str] = Field(None, max_length=5000)
|
||||
is_frequent: Optional[bool] = None
|
||||
contact_number: Optional[str] = None
|
||||
email: Optional[str] = None
|
||||
contact_number: Optional[str] = Field(None, max_length=50)
|
||||
email: Optional[str] = Field(None, max_length=255)
|
||||
|
||||
@field_validator('email')
|
||||
@classmethod
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import re
|
||||
from pydantic import BaseModel, ConfigDict, model_validator, field_validator
|
||||
from pydantic import BaseModel, ConfigDict, Field, model_validator, field_validator
|
||||
from datetime import datetime, date
|
||||
from typing import Optional
|
||||
|
||||
@ -7,20 +7,22 @@ _EMAIL_RE = re.compile(r'^[^@\s]+@[^@\s]+\.[^@\s]+$')
|
||||
|
||||
|
||||
class PersonCreate(BaseModel):
|
||||
name: Optional[str] = None # legacy fallback — auto-split into first/last if provided alone
|
||||
first_name: Optional[str] = None
|
||||
last_name: Optional[str] = None
|
||||
nickname: Optional[str] = None
|
||||
email: Optional[str] = None
|
||||
phone: Optional[str] = None
|
||||
mobile: Optional[str] = None
|
||||
address: Optional[str] = None
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
name: Optional[str] = Field(None, max_length=255) # legacy fallback — auto-split into first/last if provided alone
|
||||
first_name: Optional[str] = Field(None, max_length=100)
|
||||
last_name: Optional[str] = Field(None, max_length=100)
|
||||
nickname: Optional[str] = Field(None, max_length=100)
|
||||
email: Optional[str] = Field(None, max_length=255)
|
||||
phone: Optional[str] = Field(None, max_length=50)
|
||||
mobile: Optional[str] = Field(None, max_length=50)
|
||||
address: Optional[str] = Field(None, max_length=2000)
|
||||
birthday: Optional[date] = None
|
||||
category: Optional[str] = None
|
||||
category: Optional[str] = Field(None, max_length=100)
|
||||
is_favourite: bool = False
|
||||
company: Optional[str] = None
|
||||
job_title: Optional[str] = None
|
||||
notes: Optional[str] = None
|
||||
company: Optional[str] = Field(None, max_length=255)
|
||||
job_title: Optional[str] = Field(None, max_length=255)
|
||||
notes: Optional[str] = Field(None, max_length=5000)
|
||||
|
||||
@model_validator(mode='after')
|
||||
def require_some_name(self) -> 'PersonCreate':
|
||||
@ -42,20 +44,22 @@ class PersonCreate(BaseModel):
|
||||
|
||||
|
||||
class PersonUpdate(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
# name is intentionally omitted — always computed from first/last/nickname
|
||||
first_name: Optional[str] = None
|
||||
last_name: Optional[str] = None
|
||||
nickname: Optional[str] = None
|
||||
email: Optional[str] = None
|
||||
phone: Optional[str] = None
|
||||
mobile: Optional[str] = None
|
||||
address: Optional[str] = None
|
||||
first_name: Optional[str] = Field(None, max_length=100)
|
||||
last_name: Optional[str] = Field(None, max_length=100)
|
||||
nickname: Optional[str] = Field(None, max_length=100)
|
||||
email: Optional[str] = Field(None, max_length=255)
|
||||
phone: Optional[str] = Field(None, max_length=50)
|
||||
mobile: Optional[str] = Field(None, max_length=50)
|
||||
address: Optional[str] = Field(None, max_length=2000)
|
||||
birthday: Optional[date] = None
|
||||
category: Optional[str] = None
|
||||
category: Optional[str] = Field(None, max_length=100)
|
||||
is_favourite: Optional[bool] = None
|
||||
company: Optional[str] = None
|
||||
job_title: Optional[str] = None
|
||||
notes: Optional[str] = None
|
||||
company: Optional[str] = Field(None, max_length=255)
|
||||
job_title: Optional[str] = Field(None, max_length=255)
|
||||
notes: Optional[str] = Field(None, max_length=5000)
|
||||
|
||||
@field_validator('email')
|
||||
@classmethod
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
from pydantic import BaseModel, ConfigDict
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
from datetime import datetime, date
|
||||
from typing import Optional, List, Literal
|
||||
from app.schemas.project_task import ProjectTaskResponse
|
||||
@ -7,19 +7,23 @@ ProjectStatus = Literal["not_started", "in_progress", "completed", "blocked", "r
|
||||
|
||||
|
||||
class ProjectCreate(BaseModel):
|
||||
name: str
|
||||
description: Optional[str] = None
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
name: str = Field(min_length=1, max_length=255)
|
||||
description: Optional[str] = Field(None, max_length=5000)
|
||||
status: ProjectStatus = "not_started"
|
||||
color: Optional[str] = None
|
||||
color: Optional[str] = Field(None, max_length=20)
|
||||
due_date: Optional[date] = None
|
||||
is_tracked: bool = False
|
||||
|
||||
|
||||
class ProjectUpdate(BaseModel):
|
||||
name: Optional[str] = None
|
||||
description: Optional[str] = None
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
name: Optional[str] = Field(None, min_length=1, max_length=255)
|
||||
description: Optional[str] = Field(None, max_length=5000)
|
||||
status: Optional[ProjectStatus] = None
|
||||
color: Optional[str] = None
|
||||
color: Optional[str] = Field(None, max_length=20)
|
||||
due_date: Optional[date] = None
|
||||
is_tracked: Optional[bool] = None
|
||||
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
from pydantic import BaseModel, ConfigDict
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
from datetime import datetime, date
|
||||
from typing import Optional, List, Literal
|
||||
from app.schemas.task_comment import TaskCommentResponse
|
||||
@ -8,8 +8,10 @@ TaskPriority = Literal["none", "low", "medium", "high"]
|
||||
|
||||
|
||||
class ProjectTaskCreate(BaseModel):
|
||||
title: str
|
||||
description: Optional[str] = None
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
title: str = Field(min_length=1, max_length=255)
|
||||
description: Optional[str] = Field(None, max_length=5000)
|
||||
status: TaskStatus = "pending"
|
||||
priority: TaskPriority = "medium"
|
||||
due_date: Optional[date] = None
|
||||
@ -19,8 +21,10 @@ class ProjectTaskCreate(BaseModel):
|
||||
|
||||
|
||||
class ProjectTaskUpdate(BaseModel):
|
||||
title: Optional[str] = None
|
||||
description: Optional[str] = None
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
title: Optional[str] = Field(None, min_length=1, max_length=255)
|
||||
description: Optional[str] = Field(None, max_length=5000)
|
||||
status: Optional[TaskStatus] = None
|
||||
priority: Optional[TaskPriority] = None
|
||||
due_date: Optional[date] = None
|
||||
|
||||
@ -1,19 +1,23 @@
|
||||
from pydantic import BaseModel, ConfigDict
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
from datetime import datetime
|
||||
from typing import Literal, Optional
|
||||
|
||||
|
||||
class ReminderCreate(BaseModel):
|
||||
title: str
|
||||
description: Optional[str] = None
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
title: str = Field(min_length=1, max_length=255)
|
||||
description: Optional[str] = Field(None, max_length=5000)
|
||||
remind_at: Optional[datetime] = None
|
||||
is_active: bool = True
|
||||
recurrence_rule: Optional[Literal['daily', 'weekly', 'monthly']] = None
|
||||
|
||||
|
||||
class ReminderUpdate(BaseModel):
|
||||
title: Optional[str] = None
|
||||
description: Optional[str] = None
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
title: Optional[str] = Field(None, min_length=1, max_length=255)
|
||||
description: Optional[str] = Field(None, max_length=5000)
|
||||
remind_at: Optional[datetime] = None
|
||||
is_active: Optional[bool] = None
|
||||
is_dismissed: Optional[bool] = None
|
||||
@ -21,6 +25,8 @@ class ReminderUpdate(BaseModel):
|
||||
|
||||
|
||||
class ReminderSnooze(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
minutes: Literal[5, 10, 15]
|
||||
client_now: Optional[datetime] = None
|
||||
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import re
|
||||
from pydantic import BaseModel, ConfigDict, field_validator
|
||||
from pydantic import BaseModel, ConfigDict, Field, field_validator
|
||||
from datetime import datetime
|
||||
from typing import Literal, Optional
|
||||
|
||||
@ -9,17 +9,19 @@ _NTFY_TOPIC_RE = re.compile(r'^[a-zA-Z0-9_-]{1,64}$')
|
||||
|
||||
|
||||
class SettingsUpdate(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
accent_color: Optional[AccentColor] = None
|
||||
upcoming_days: int | None = None
|
||||
preferred_name: str | None = None
|
||||
weather_city: str | None = None
|
||||
preferred_name: str | None = Field(None, max_length=100)
|
||||
weather_city: str | None = Field(None, max_length=100)
|
||||
weather_lat: float | None = None
|
||||
weather_lon: float | None = None
|
||||
first_day_of_week: int | None = None
|
||||
|
||||
# ntfy configuration fields
|
||||
ntfy_server_url: Optional[str] = None
|
||||
ntfy_topic: Optional[str] = None
|
||||
ntfy_server_url: Optional[str] = Field(None, max_length=500)
|
||||
ntfy_topic: Optional[str] = Field(None, max_length=100)
|
||||
# Empty string means "clear the token"; None means "leave unchanged"
|
||||
ntfy_auth_token: Optional[str] = None
|
||||
ntfy_enabled: Optional[bool] = None
|
||||
|
||||
@ -1,9 +1,11 @@
|
||||
from pydantic import BaseModel, ConfigDict
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
class TaskCommentCreate(BaseModel):
|
||||
content: str
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
content: str = Field(min_length=1, max_length=10000)
|
||||
|
||||
|
||||
class TaskCommentResponse(BaseModel):
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
from pydantic import BaseModel, ConfigDict
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
from datetime import datetime, date, time
|
||||
from typing import Optional, Literal
|
||||
|
||||
@ -7,24 +7,28 @@ RecurrenceRule = Literal["daily", "weekly", "monthly"]
|
||||
|
||||
|
||||
class TodoCreate(BaseModel):
|
||||
title: str
|
||||
description: Optional[str] = None
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
title: str = Field(min_length=1, max_length=255)
|
||||
description: Optional[str] = Field(None, max_length=5000)
|
||||
priority: TodoPriority = "medium"
|
||||
due_date: Optional[date] = None
|
||||
due_time: Optional[time] = None
|
||||
category: Optional[str] = None
|
||||
category: Optional[str] = Field(None, max_length=100)
|
||||
recurrence_rule: Optional[RecurrenceRule] = None
|
||||
project_id: Optional[int] = None
|
||||
|
||||
|
||||
class TodoUpdate(BaseModel):
|
||||
title: Optional[str] = None
|
||||
description: Optional[str] = None
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
title: Optional[str] = Field(None, min_length=1, max_length=255)
|
||||
description: Optional[str] = Field(None, max_length=5000)
|
||||
priority: Optional[TodoPriority] = None
|
||||
due_date: Optional[date] = None
|
||||
due_time: Optional[time] = None
|
||||
completed: Optional[bool] = None
|
||||
category: Optional[str] = None
|
||||
category: Optional[str] = Field(None, max_length=100)
|
||||
recurrence_rule: Optional[RecurrenceRule] = None
|
||||
project_id: Optional[int] = None
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user