feat(enterprise): add custom branding system — logo, accent color, company name
- Add branding_logo_url, branding_primary_color, branding_company_name columns to Account model - Add Alembic migration (58e3f27f3e8f) for branding and SSO columns - Add GET/PATCH /accounts/me/branding endpoints (owner-only for PATCH) - Add BrandingSettingsPage with logo URL input, color picker, preview section - Add /account/branding route (ProtectedRoute owner) in router.tsx - Add Branding link card in AccountSettingsPage Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -465,3 +465,96 @@ async def delete_account(
|
||||
await db.commit()
|
||||
|
||||
return {"message": "Account deleted"}
|
||||
|
||||
|
||||
# ─── Account Branding Endpoints (Task 9) ──────────────────────────────────────
|
||||
|
||||
class AccountBrandingResponse(BaseModel):
|
||||
logo_url: Optional[str] = None
|
||||
primary_color: Optional[str] = None
|
||||
company_name: Optional[str] = None
|
||||
|
||||
model_config = {"from_attributes": True}
|
||||
|
||||
|
||||
class AccountBrandingUpdate(BaseModel):
|
||||
logo_url: Optional[str] = None
|
||||
primary_color: Optional[str] = None
|
||||
company_name: Optional[str] = None
|
||||
|
||||
|
||||
@router.get("/me/branding", response_model=AccountBrandingResponse)
|
||||
async def get_account_branding(
|
||||
db: Annotated[AsyncSession, Depends(get_db)],
|
||||
current_user: Annotated[User, Depends(get_current_active_user)],
|
||||
):
|
||||
"""Get custom branding settings for the current account."""
|
||||
result = await db.execute(select(Account).where(Account.id == current_user.account_id))
|
||||
account = result.scalar_one_or_none()
|
||||
if not account:
|
||||
raise HTTPException(status_code=404, detail="Account not found")
|
||||
|
||||
return AccountBrandingResponse(
|
||||
logo_url=account.branding_logo_url,
|
||||
primary_color=account.branding_primary_color,
|
||||
company_name=account.branding_company_name,
|
||||
)
|
||||
|
||||
|
||||
@router.patch("/me/branding", response_model=AccountBrandingResponse)
|
||||
async def update_account_branding(
|
||||
data: AccountBrandingUpdate,
|
||||
db: Annotated[AsyncSession, Depends(get_db)],
|
||||
current_user: Annotated[User, Depends(require_account_owner)],
|
||||
):
|
||||
"""Update custom branding settings. Account owner only."""
|
||||
result = await db.execute(select(Account).where(Account.id == current_user.account_id))
|
||||
account = result.scalar_one_or_none()
|
||||
if not account:
|
||||
raise HTTPException(status_code=404, detail="Account not found")
|
||||
|
||||
if data.logo_url is not None:
|
||||
account.branding_logo_url = data.logo_url or None
|
||||
if data.primary_color is not None:
|
||||
# Validate hex color format (#RRGGBB)
|
||||
color = data.primary_color.strip()
|
||||
if color and (len(color) != 7 or not color.startswith("#")):
|
||||
raise HTTPException(status_code=400, detail="primary_color must be a 7-character hex string like #06b6d4")
|
||||
account.branding_primary_color = color or None
|
||||
if data.company_name is not None:
|
||||
account.branding_company_name = data.company_name.strip() or None
|
||||
|
||||
await db.commit()
|
||||
await db.refresh(account)
|
||||
|
||||
return AccountBrandingResponse(
|
||||
logo_url=account.branding_logo_url,
|
||||
primary_color=account.branding_primary_color,
|
||||
company_name=account.branding_company_name,
|
||||
)
|
||||
|
||||
|
||||
# ─── SSO Status Endpoint (Task 11) ────────────────────────────────────────────
|
||||
|
||||
class AccountSSOStatusResponse(BaseModel):
|
||||
sso_enabled: bool
|
||||
sso_provider: Optional[str] = None
|
||||
|
||||
model_config = {"from_attributes": True}
|
||||
|
||||
|
||||
@router.get("/me/sso", response_model=AccountSSOStatusResponse)
|
||||
async def get_sso_status(
|
||||
db: Annotated[AsyncSession, Depends(get_db)],
|
||||
current_user: Annotated[User, Depends(get_current_active_user)],
|
||||
):
|
||||
"""Get SSO configuration status for the current account."""
|
||||
result = await db.execute(select(Account).where(Account.id == current_user.account_id))
|
||||
account = result.scalar_one_or_none()
|
||||
if not account:
|
||||
raise HTTPException(status_code=404, detail="Account not found")
|
||||
|
||||
return AccountSSOStatusResponse(
|
||||
sso_enabled=account.sso_enabled,
|
||||
sso_provider=account.sso_provider,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user