| 1 | import base64 |
| 2 | import re |
| 3 | from typing import Optional |
| 4 | |
| 5 | from fastapi import HTTPException |
| 6 | from pydantic import BaseModel |
| 7 | from pydantic import ConfigDict |
| 8 | from pydantic import Field |
| 9 | from pydantic import field_validator |
| 10 | |
| 11 | |
| 12 | class UpdatePortalSettingsRequest(BaseModel): |
| 13 | title: Optional[str] = Field(None, max_length=255, description="Portal title. Set to null to restore default.") |
| 14 | logo_base64: Optional[str] = Field(None, description="Base64 encoded logo image. Set to null to restore default.") |
| 15 | logo_mime_type: Optional[str] = Field(None, max_length=50, description="MIME type of the logo. Set to null to restore default.") |
| 16 | |
| 17 | @field_validator("logo_base64") |
| 18 | @classmethod |
| 19 | def validate_base64(cls, v): |
| 20 | if v is None: |
| 21 | return v |
| 22 | |
| 23 | # Remove data URL prefix if present |
| 24 | if v.startswith("data:"): |
| 25 | v = v.split(",", 1)[1] if "," in v else v |
| 26 | |
| 27 | # Check size (limit to 5MB base64 = ~3.75MB original) |
| 28 | max_size = 5 * 1024 * 1024 # 5MB |
| 29 | if len(v) > max_size: |
| 30 | raise HTTPException( |
| 31 | status_code=400, |
| 32 | detail=f"Logo file too large. Maximum size is {max_size // (1024 * 1024)}MB (base64-encoded)", |
| 33 | ) |
| 34 | |
| 35 | # Validate base64 format |
| 36 | if not re.match(r"^[A-Za-z0-9+/]*={0,2}$", v): |
| 37 | raise HTTPException(status_code=400, detail="Invalid base64 encoded string for logo_base64") |
| 38 | |
| 39 | # Optional: Try to decode to verify it's valid base64 |
| 40 | try: |
| 41 | base64.b64decode(v) |
| 42 | except Exception: |
| 43 | raise HTTPException(status_code=400, detail="Invalid base64 data - cannot decode") |
| 44 | |
| 45 | return v |
| 46 | |
| 47 | @field_validator("logo_mime_type") |
| 48 | @classmethod |
| 49 | def validate_mime_type(cls, v): |
| 50 | if v is None: |
| 51 | return v |
| 52 | |
| 53 | allowed_types = ["image/png", "image/jpeg", "image/jpg", "image/gif", "image/svg+xml", "image/webp"] |
| 54 | if v not in allowed_types: |
| 55 | raise HTTPException(status_code=400, detail=f"Invalid MIME type. Allowed types are: {', '.join(allowed_types)}") |
| 56 | return v |
| 57 | |
| 58 | model_config = ConfigDict( |
| 59 | json_schema_extra={"example": {"title": "My Custom Portal", "logo_base64": "iVBORw0KGgoAAAANS...", "logo_mime_type": "image/png"}}, |
| 60 | ) |
| 61 | |
| 62 | |
| 63 | class PortalSettingsData(BaseModel): |
| 64 | id: int |
| 65 | title: str |
| 66 | logo_base64: Optional[str] = None |
| 67 | logo_mime_type: Optional[str] = None |
| 68 | updated_at: str |
| 69 | model_config = ConfigDict(from_attributes=True) |
| 70 | |
| 71 | |
| 72 | class PortalSettingsResponse(BaseModel): |
| 73 | success: bool |
| 74 | message: str |
| 75 | settings: Optional[PortalSettingsData] = None |
| 76 | |
| 77 | |
| 78 | class UpdatePortalSettingsResponse(BaseModel): |
| 79 | success: bool |
| 80 | message: str |