feat: phase 2 - migrate all route guards to require_permission (75 guards, 21 files)

This commit is contained in:
Agent Zero
2026-07-15 22:35:50 +02:00
parent 8ca6dfee88
commit 08fd3ab72c
18 changed files with 132 additions and 265 deletions
+2
View File
@@ -29,7 +29,9 @@ CORE_PERMISSIONS: list[dict[str, str]] = [
{"key": "groups:write", "label": "Groups: Write", "category": "core", "module": "groups"}, {"key": "groups:write", "label": "Groups: Write", "category": "core", "module": "groups"},
{"key": "groups:delete", "label": "Groups: Delete", "category": "core", "module": "groups"}, {"key": "groups:delete", "label": "Groups: Delete", "category": "core", "module": "groups"},
{"key": "audit:read", "label": "Audit Log: Read", "category": "core", "module": "audit"}, {"key": "audit:read", "label": "Audit Log: Read", "category": "core", "module": "audit"},
{"key": "settings:read", "label": "Settings: Read", "category": "core", "module": "settings"},
{"key": "settings:write", "label": "Settings: Write", "category": "core", "module": "settings"}, {"key": "settings:write", "label": "Settings: Write", "category": "core", "module": "settings"},
{"key": "plugins:read", "label": "Plugins: Read", "category": "core", "module": "plugins"},
{"key": "plugins:install", "label": "Plugins: Install", "category": "core", "module": "plugins"}, {"key": "plugins:install", "label": "Plugins: Install", "category": "core", "module": "plugins"},
{"key": "plugins:configure", "label": "Plugins: Configure", "category": "core", "module": "plugins"}, {"key": "plugins:configure", "label": "Plugins: Configure", "category": "core", "module": "plugins"},
{"key": "tenants:read", "label": "Tenants: Read", "category": "core", "module": "tenants"}, {"key": "tenants:read", "label": "Tenants: Read", "category": "core", "module": "tenants"},
+5 -24
View File
@@ -7,9 +7,8 @@ import uuid
from fastapi import APIRouter, Depends, HTTPException, Query, status from fastapi import APIRouter, Depends, HTTPException, Query, status
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from app.core.auth import check_permission
from app.core.db import get_db from app.core.db import get_db
from app.deps import get_current_user from app.deps import require_permission
from app.schemas.address import AddressCreate, AddressUpdate from app.schemas.address import AddressCreate, AddressUpdate
from app.services import address_service from app.services import address_service
@@ -21,7 +20,7 @@ async def list_addresses(
entity_type: str = Query(..., pattern="^(company|contact)$"), entity_type: str = Query(..., pattern="^(company|contact)$"),
entity_id: str = Query(...), entity_id: str = Query(...),
db: AsyncSession = Depends(get_db), db: AsyncSession = Depends(get_db),
current_user: dict = Depends(get_current_user), current_user: dict = Depends(require_permission("addresses:read")),
): ):
"""List all addresses for a given entity (company or contact).""" """List all addresses for a given entity (company or contact)."""
tenant_id = uuid.UUID(current_user["tenant_id"]) tenant_id = uuid.UUID(current_user["tenant_id"])
@@ -37,18 +36,12 @@ async def list_addresses(
async def create_address( async def create_address(
body: AddressCreate, body: AddressCreate,
db: AsyncSession = Depends(get_db), db: AsyncSession = Depends(get_db),
current_user: dict = Depends(get_current_user), current_user: dict = Depends(require_permission("addresses:write")),
): ):
"""Create a new address for a company or contact.""" """Create a new address for a company or contact."""
tenant_id = uuid.UUID(current_user["tenant_id"]) tenant_id = uuid.UUID(current_user["tenant_id"])
user_id = uuid.UUID(current_user["user_id"]) user_id = uuid.UUID(current_user["user_id"])
role = current_user.get("role", "viewer")
if not check_permission(role, "companies", "create") and not check_permission(role, "contacts", "create"):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail={"detail": "Insufficient permissions", "code": "forbidden"},
)
data = body.model_dump() data = body.model_dump()
try: try:
@@ -62,18 +55,12 @@ async def update_address(
address_id: str, address_id: str,
body: AddressUpdate, body: AddressUpdate,
db: AsyncSession = Depends(get_db), db: AsyncSession = Depends(get_db),
current_user: dict = Depends(get_current_user), current_user: dict = Depends(require_permission("addresses:write")),
): ):
"""Update an address.""" """Update an address."""
tenant_id = uuid.UUID(current_user["tenant_id"]) tenant_id = uuid.UUID(current_user["tenant_id"])
user_id = uuid.UUID(current_user["user_id"]) user_id = uuid.UUID(current_user["user_id"])
role = current_user.get("role", "viewer")
if not check_permission(role, "companies", "update") and not check_permission(role, "contacts", "update"):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail={"detail": "Insufficient permissions", "code": "forbidden"},
)
try: try:
aid = uuid.UUID(address_id) aid = uuid.UUID(address_id)
@@ -91,18 +78,12 @@ async def update_address(
async def delete_address( async def delete_address(
address_id: str, address_id: str,
db: AsyncSession = Depends(get_db), db: AsyncSession = Depends(get_db),
current_user: dict = Depends(get_current_user), current_user: dict = Depends(require_permission("addresses:write")),
): ):
"""Soft-delete an address.""" """Soft-delete an address."""
tenant_id = uuid.UUID(current_user["tenant_id"]) tenant_id = uuid.UUID(current_user["tenant_id"])
user_id = uuid.UUID(current_user["user_id"]) user_id = uuid.UUID(current_user["user_id"])
role = current_user.get("role", "viewer")
if not check_permission(role, "companies", "delete") and not check_permission(role, "contacts", "delete"):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail={"detail": "Insufficient permissions", "code": "forbidden"},
)
try: try:
aid = uuid.UUID(address_id) aid = uuid.UUID(address_id)
+5 -18
View File
@@ -9,9 +9,8 @@ from fastapi import APIRouter, Depends, File, Form, HTTPException, UploadFile, s
from fastapi.responses import FileResponse from fastapi.responses import FileResponse
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from app.core.auth import check_permission
from app.core.db import get_db from app.core.db import get_db
from app.deps import get_current_user from app.deps import require_permission
from app.services import attachment_service from app.services import attachment_service
router = APIRouter(prefix="/api/v1/attachments", tags=["attachments"]) router = APIRouter(prefix="/api/v1/attachments", tags=["attachments"])
@@ -23,18 +22,12 @@ async def upload_attachment(
entity_type: str = Form(...), entity_type: str = Form(...),
entity_id: str = Form(...), entity_id: str = Form(...),
db: AsyncSession = Depends(get_db), db: AsyncSession = Depends(get_db),
current_user: dict = Depends(get_current_user), current_user: dict = Depends(require_permission("attachments:write")),
): ):
"""Upload a file attachment. Multipart form data.""" """Upload a file attachment. Multipart form data."""
tenant_id = uuid.UUID(current_user["tenant_id"]) tenant_id = uuid.UUID(current_user["tenant_id"])
user_id = uuid.UUID(current_user["user_id"]) user_id = uuid.UUID(current_user["user_id"])
role = current_user.get("role", "viewer")
if not check_permission(role, "companies", "update"):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail={"detail": "Insufficient permissions", "code": "forbidden"},
)
try: try:
eid = uuid.UUID(entity_id) eid = uuid.UUID(entity_id)
@@ -55,7 +48,7 @@ async def list_attachments(
entity_type: str, entity_type: str,
entity_id: str, entity_id: str,
db: AsyncSession = Depends(get_db), db: AsyncSession = Depends(get_db),
current_user: dict = Depends(get_current_user), current_user: dict = Depends(require_permission("attachments:read")),
): ):
"""List attachments for a specific entity.""" """List attachments for a specific entity."""
tenant_id = uuid.UUID(current_user["tenant_id"]) tenant_id = uuid.UUID(current_user["tenant_id"])
@@ -72,7 +65,7 @@ async def list_attachments(
async def download_attachment( async def download_attachment(
attachment_id: str, attachment_id: str,
db: AsyncSession = Depends(get_db), db: AsyncSession = Depends(get_db),
current_user: dict = Depends(get_current_user), current_user: dict = Depends(require_permission("attachments:read")),
): ):
"""Download an attachment file.""" """Download an attachment file."""
tenant_id = uuid.UUID(current_user["tenant_id"]) tenant_id = uuid.UUID(current_user["tenant_id"])
@@ -101,18 +94,12 @@ async def download_attachment(
async def delete_attachment( async def delete_attachment(
attachment_id: str, attachment_id: str,
db: AsyncSession = Depends(get_db), db: AsyncSession = Depends(get_db),
current_user: dict = Depends(get_current_user), current_user: dict = Depends(require_permission("attachments:write")),
): ):
"""Delete an attachment.""" """Delete an attachment."""
tenant_id = uuid.UUID(current_user["tenant_id"]) tenant_id = uuid.UUID(current_user["tenant_id"])
user_id = uuid.UUID(current_user["user_id"]) user_id = uuid.UUID(current_user["user_id"])
role = current_user.get("role", "viewer")
if not check_permission(role, "companies", "delete"):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail={"detail": "Insufficient permissions", "code": "forbidden"},
)
try: try:
aid = uuid.UUID(attachment_id) aid = uuid.UUID(attachment_id)
+2 -7
View File
@@ -10,7 +10,7 @@ from sqlalchemy import select, func
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from app.core.db import get_db from app.core.db import get_db
from app.deps import get_current_user from app.deps import require_permission
from app.models.audit import AuditLog from app.models.audit import AuditLog
router = APIRouter(prefix="/api/v1/audit-log", tags=["audit"]) router = APIRouter(prefix="/api/v1/audit-log", tags=["audit"])
@@ -40,14 +40,9 @@ async def list_audit_logs(
page: int = Query(1, ge=1, description="Page number"), page: int = Query(1, ge=1, description="Page number"),
page_size: int = Query(50, ge=1, le=200, description="Items per page"), page_size: int = Query(50, ge=1, le=200, description="Items per page"),
db: AsyncSession = Depends(get_db), db: AsyncSession = Depends(get_db),
current_user: dict = Depends(get_current_user), current_user: dict = Depends(require_permission("audit:read")),
): ):
"""List audit log entries with filtering and pagination. Admin only.""" """List audit log entries with filtering and pagination. Admin only."""
if current_user.get("role") != "admin":
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail={"detail": "Admin access required", "code": "forbidden"},
)
tenant_id = uuid.UUID(current_user["tenant_id"]) tenant_id = uuid.UUID(current_user["tenant_id"])
+10 -29
View File
@@ -7,9 +7,8 @@ import uuid
from fastapi import APIRouter, Depends, HTTPException, Query, Response, status from fastapi import APIRouter, Depends, HTTPException, Query, Response, status
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from app.core.auth import check_permission
from app.core.db import get_db from app.core.db import get_db
from app.deps import get_current_user from app.deps import require_permission
from app.schemas.company import CompanyCreate, CompanyUpdate from app.schemas.company import CompanyCreate, CompanyUpdate
from app.services import company_service from app.services import company_service
@@ -25,7 +24,7 @@ async def list_companies(
sort_by: str = Query("name"), sort_by: str = Query("name"),
sort_order: str = Query("asc", pattern="^(asc|desc)$"), sort_order: str = Query("asc", pattern="^(asc|desc)$"),
db: AsyncSession = Depends(get_db), db: AsyncSession = Depends(get_db),
current_user: dict = Depends(get_current_user), current_user: dict = Depends(require_permission("companies:read")),
): ):
"""List companies with pagination, FTS search, industry filter, and sorting.""" """List companies with pagination, FTS search, industry filter, and sorting."""
tenant_id = uuid.UUID(current_user["tenant_id"]) tenant_id = uuid.UUID(current_user["tenant_id"])
@@ -46,18 +45,12 @@ async def list_companies(
async def create_company( async def create_company(
body: CompanyCreate, body: CompanyCreate,
db: AsyncSession = Depends(get_db), db: AsyncSession = Depends(get_db),
current_user: dict = Depends(get_current_user), current_user: dict = Depends(require_permission("companies:write")),
): ):
"""Create a company. Requires write permission.""" """Create a company. Requires write permission."""
tenant_id = uuid.UUID(current_user["tenant_id"]) tenant_id = uuid.UUID(current_user["tenant_id"])
user_id = uuid.UUID(current_user["user_id"]) user_id = uuid.UUID(current_user["user_id"])
role = current_user.get("role", "viewer")
if not check_permission(role, "companies", "create"):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail={"detail": "Insufficient permissions", "code": "forbidden"},
)
data = body.model_dump() data = body.model_dump()
return await company_service.create_company(db, tenant_id, user_id, data) return await company_service.create_company(db, tenant_id, user_id, data)
@@ -69,7 +62,7 @@ async def export_companies(
industry: str | None = Query(None), industry: str | None = Query(None),
search: str | None = Query(None), search: str | None = Query(None),
db: AsyncSession = Depends(get_db), db: AsyncSession = Depends(get_db),
current_user: dict = Depends(get_current_user), current_user: dict = Depends(require_permission("companies:read")),
): ):
"""Export companies as CSV (streaming) or XLSX (buffered).""" """Export companies as CSV (streaming) or XLSX (buffered)."""
tenant_id = uuid.UUID(current_user["tenant_id"]) tenant_id = uuid.UUID(current_user["tenant_id"])
@@ -155,7 +148,7 @@ async def export_companies(
async def get_company( async def get_company(
company_id: str, company_id: str,
db: AsyncSession = Depends(get_db), db: AsyncSession = Depends(get_db),
current_user: dict = Depends(get_current_user), current_user: dict = Depends(require_permission("companies:read")),
): ):
"""Get a single company with contacts array. Cross-tenant returns 404.""" """Get a single company with contacts array. Cross-tenant returns 404."""
tenant_id = uuid.UUID(current_user["tenant_id"]) tenant_id = uuid.UUID(current_user["tenant_id"])
@@ -177,15 +170,12 @@ async def update_company(
company_id: str, company_id: str,
body: CompanyUpdate, body: CompanyUpdate,
db: AsyncSession = Depends(get_db), db: AsyncSession = Depends(get_db),
current_user: dict = Depends(get_current_user), current_user: dict = Depends(require_permission("companies:write")),
): ):
"""Update a company (full PUT).""" """Update a company (full PUT)."""
tenant_id = uuid.UUID(current_user["tenant_id"]) tenant_id = uuid.UUID(current_user["tenant_id"])
user_id = uuid.UUID(current_user["user_id"]) user_id = uuid.UUID(current_user["user_id"])
role = current_user.get("role", "viewer")
if not check_permission(role, "companies", "update"):
raise HTTPException(403, detail={"detail": "Insufficient permissions", "code": "forbidden"})
try: try:
cid = uuid.UUID(company_id) cid = uuid.UUID(company_id)
@@ -206,15 +196,12 @@ async def delete_company(
company_id: str, company_id: str,
cascade: bool = Query(False), cascade: bool = Query(False),
db: AsyncSession = Depends(get_db), db: AsyncSession = Depends(get_db),
current_user: dict = Depends(get_current_user), current_user: dict = Depends(require_permission("companies:write")),
): ):
"""Soft-delete a company. Use cascade=true to also delete N:M links.""" """Soft-delete a company. Use cascade=true to also delete N:M links."""
tenant_id = uuid.UUID(current_user["tenant_id"]) tenant_id = uuid.UUID(current_user["tenant_id"])
user_id = uuid.UUID(current_user["user_id"]) user_id = uuid.UUID(current_user["user_id"])
role = current_user.get("role", "viewer")
if not check_permission(role, "companies", "delete"):
raise HTTPException(403, detail={"detail": "Insufficient permissions", "code": "forbidden"})
try: try:
cid = uuid.UUID(company_id) cid = uuid.UUID(company_id)
@@ -237,15 +224,12 @@ async def link_company_contact(
company_id: str, company_id: str,
contact_id: str, contact_id: str,
db: AsyncSession = Depends(get_db), db: AsyncSession = Depends(get_db),
current_user: dict = Depends(get_current_user), current_user: dict = Depends(require_permission("companies:write")),
): ):
"""Link a contact to a company (N:M).""" """Link a contact to a company (N:M)."""
tenant_id = uuid.UUID(current_user["tenant_id"]) tenant_id = uuid.UUID(current_user["tenant_id"])
user_id = uuid.UUID(current_user["user_id"]) user_id = uuid.UUID(current_user["user_id"])
role = current_user.get("role", "viewer")
if not check_permission(role, "companies", "update"):
raise HTTPException(403, detail={"detail": "Insufficient permissions", "code": "forbidden"})
try: try:
comp_id = uuid.UUID(company_id) comp_id = uuid.UUID(company_id)
@@ -266,15 +250,12 @@ async def unlink_company_contact(
company_id: str, company_id: str,
contact_id: str, contact_id: str,
db: AsyncSession = Depends(get_db), db: AsyncSession = Depends(get_db),
current_user: dict = Depends(get_current_user), current_user: dict = Depends(require_permission("companies:write")),
): ):
"""Unlink a contact from a company (N:M).""" """Unlink a contact from a company (N:M)."""
tenant_id = uuid.UUID(current_user["tenant_id"]) tenant_id = uuid.UUID(current_user["tenant_id"])
user_id = uuid.UUID(current_user["user_id"]) user_id = uuid.UUID(current_user["user_id"])
role = current_user.get("role", "viewer")
if not check_permission(role, "companies", "update"):
raise HTTPException(403, detail={"detail": "Insufficient permissions", "code": "forbidden"})
try: try:
comp_id = uuid.UUID(company_id) comp_id = uuid.UUID(company_id)
@@ -293,7 +274,7 @@ async def unlink_company_contact(
async def get_company_emails( async def get_company_emails(
company_id: str, company_id: str,
db: AsyncSession = Depends(get_db), db: AsyncSession = Depends(get_db),
current_user: dict = Depends(get_current_user), current_user: dict = Depends(require_permission("companies:read")),
): ):
"""Get emails for a company (mail plugin inactive — returns empty array).""" """Get emails for a company (mail plugin inactive — returns empty array)."""
tenant_id = uuid.UUID(current_user["tenant_id"]) tenant_id = uuid.UUID(current_user["tenant_id"])
+7 -20
View File
@@ -11,9 +11,8 @@ from fastapi.responses import StreamingResponse
from sqlalchemy import select from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from app.core.auth import check_permission
from app.core.db import get_db from app.core.db import get_db
from app.deps import get_current_user from app.deps import require_permission
from app.models.contact import Contact from app.models.contact import Contact
from app.schemas.contact import ContactCreate, ContactUpdate from app.schemas.contact import ContactCreate, ContactUpdate
from app.services import contact_service from app.services import contact_service
@@ -29,7 +28,7 @@ async def list_contacts(
sort_by: str = Query("last_name"), sort_by: str = Query("last_name"),
sort_order: str = Query("asc", pattern="^(asc|desc)$"), sort_order: str = Query("asc", pattern="^(asc|desc)$"),
db: AsyncSession = Depends(get_db), db: AsyncSession = Depends(get_db),
current_user: dict = Depends(get_current_user), current_user: dict = Depends(require_permission("contacts:read")),
): ):
"""List contacts with pagination and optional search. """List contacts with pagination and optional search.
@@ -53,7 +52,7 @@ async def export_contacts(
format: str = Query("csv", pattern="^(csv)$"), format: str = Query("csv", pattern="^(csv)$"),
search: str | None = Query(None), search: str | None = Query(None),
db: AsyncSession = Depends(get_db), db: AsyncSession = Depends(get_db),
current_user: dict = Depends(get_current_user), current_user: dict = Depends(require_permission("contacts:read")),
): ):
"""Stream contacts as CSV (not buffered — uses StreamingResponse). """Stream contacts as CSV (not buffered — uses StreamingResponse).
@@ -148,18 +147,12 @@ async def export_contacts(
async def create_contact( async def create_contact(
body: ContactCreate, body: ContactCreate,
db: AsyncSession = Depends(get_db), db: AsyncSession = Depends(get_db),
current_user: dict = Depends(get_current_user), current_user: dict = Depends(require_permission("contacts:write")),
): ):
"""Create a contact. Optionally link to companies via company_ids array.""" """Create a contact. Optionally link to companies via company_ids array."""
tenant_id = uuid.UUID(current_user["tenant_id"]) tenant_id = uuid.UUID(current_user["tenant_id"])
user_id = uuid.UUID(current_user["user_id"]) user_id = uuid.UUID(current_user["user_id"])
role = current_user.get("role", "viewer")
if not check_permission(role, "contacts", "create"):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail={"detail": "Insufficient permissions", "code": "forbidden"},
)
data = body.model_dump() data = body.model_dump()
return await contact_service.create_contact(db, tenant_id, user_id, data) return await contact_service.create_contact(db, tenant_id, user_id, data)
@@ -169,7 +162,7 @@ async def create_contact(
async def get_contact( async def get_contact(
contact_id: str, contact_id: str,
db: AsyncSession = Depends(get_db), db: AsyncSession = Depends(get_db),
current_user: dict = Depends(get_current_user), current_user: dict = Depends(require_permission("contacts:read")),
): ):
"""Get a single contact with companies array. Cross-tenant returns 404.""" """Get a single contact with companies array. Cross-tenant returns 404."""
tenant_id = uuid.UUID(current_user["tenant_id"]) tenant_id = uuid.UUID(current_user["tenant_id"])
@@ -191,15 +184,12 @@ async def update_contact(
contact_id: str, contact_id: str,
body: ContactUpdate, body: ContactUpdate,
db: AsyncSession = Depends(get_db), db: AsyncSession = Depends(get_db),
current_user: dict = Depends(get_current_user), current_user: dict = Depends(require_permission("contacts:write")),
): ):
"""Update a contact.""" """Update a contact."""
tenant_id = uuid.UUID(current_user["tenant_id"]) tenant_id = uuid.UUID(current_user["tenant_id"])
user_id = uuid.UUID(current_user["user_id"]) user_id = uuid.UUID(current_user["user_id"])
role = current_user.get("role", "viewer")
if not check_permission(role, "contacts", "update"):
raise HTTPException(403, detail={"detail": "Insufficient permissions", "code": "forbidden"})
try: try:
cid = uuid.UUID(contact_id) cid = uuid.UUID(contact_id)
@@ -220,15 +210,12 @@ async def delete_contact(
contact_id: str, contact_id: str,
gdpr: bool = Query(False), gdpr: bool = Query(False),
db: AsyncSession = Depends(get_db), db: AsyncSession = Depends(get_db),
current_user: dict = Depends(get_current_user), current_user: dict = Depends(require_permission("contacts:write")),
): ):
"""Delete a contact. Default: soft-delete. Use gdpr=true for hard-delete with deletion_log.""" """Delete a contact. Default: soft-delete. Use gdpr=true for hard-delete with deletion_log."""
tenant_id = uuid.UUID(current_user["tenant_id"]) tenant_id = uuid.UUID(current_user["tenant_id"])
user_id = uuid.UUID(current_user["user_id"]) user_id = uuid.UUID(current_user["user_id"])
role = current_user.get("role", "viewer")
if not check_permission(role, "contacts", "delete"):
raise HTTPException(403, detail={"detail": "Insufficient permissions", "code": "forbidden"})
try: try:
cid = uuid.UUID(contact_id) cid = uuid.UUID(contact_id)
+5 -24
View File
@@ -7,9 +7,8 @@ import uuid
from fastapi import APIRouter, Depends, HTTPException, status from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from app.core.auth import check_permission
from app.core.db import get_db from app.core.db import get_db
from app.deps import get_current_user from app.deps import require_permission
from app.schemas.currency import CurrencyCreate, CurrencyUpdate from app.schemas.currency import CurrencyCreate, CurrencyUpdate
from app.services import currency_service from app.services import currency_service
@@ -19,7 +18,7 @@ router = APIRouter(prefix="/api/v1/currencies", tags=["currencies"])
@router.get("") @router.get("")
async def list_currencies( async def list_currencies(
db: AsyncSession = Depends(get_db), db: AsyncSession = Depends(get_db),
current_user: dict = Depends(get_current_user), current_user: dict = Depends(require_permission("currencies:read")),
): ):
"""List all currencies for the current tenant.""" """List all currencies for the current tenant."""
tenant_id = uuid.UUID(current_user["tenant_id"]) tenant_id = uuid.UUID(current_user["tenant_id"])
@@ -30,18 +29,12 @@ async def list_currencies(
async def create_currency( async def create_currency(
body: CurrencyCreate, body: CurrencyCreate,
db: AsyncSession = Depends(get_db), db: AsyncSession = Depends(get_db),
current_user: dict = Depends(get_current_user), current_user: dict = Depends(require_permission("currencies:write")),
): ):
"""Create a currency. Requires admin permission.""" """Create a currency. Requires admin permission."""
tenant_id = uuid.UUID(current_user["tenant_id"]) tenant_id = uuid.UUID(current_user["tenant_id"])
user_id = uuid.UUID(current_user["user_id"]) user_id = uuid.UUID(current_user["user_id"])
role = current_user.get("role", "viewer")
if not check_permission(role, "settings", "create"):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail={"detail": "Insufficient permissions", "code": "forbidden"},
)
data = body.model_dump() data = body.model_dump()
return await currency_service.create_currency(db, tenant_id, user_id, data) return await currency_service.create_currency(db, tenant_id, user_id, data)
@@ -52,18 +45,12 @@ async def update_currency(
currency_id: str, currency_id: str,
body: CurrencyUpdate, body: CurrencyUpdate,
db: AsyncSession = Depends(get_db), db: AsyncSession = Depends(get_db),
current_user: dict = Depends(get_current_user), current_user: dict = Depends(require_permission("currencies:write")),
): ):
"""Update a currency. Requires admin permission.""" """Update a currency. Requires admin permission."""
tenant_id = uuid.UUID(current_user["tenant_id"]) tenant_id = uuid.UUID(current_user["tenant_id"])
user_id = uuid.UUID(current_user["user_id"]) user_id = uuid.UUID(current_user["user_id"])
role = current_user.get("role", "viewer")
if not check_permission(role, "settings", "update"):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail={"detail": "Insufficient permissions", "code": "forbidden"},
)
try: try:
cid = uuid.UUID(currency_id) cid = uuid.UUID(currency_id)
@@ -81,18 +68,12 @@ async def update_currency(
async def delete_currency( async def delete_currency(
currency_id: str, currency_id: str,
db: AsyncSession = Depends(get_db), db: AsyncSession = Depends(get_db),
current_user: dict = Depends(get_current_user), current_user: dict = Depends(require_permission("currencies:write")),
): ):
"""Delete a currency. Requires admin permission.""" """Delete a currency. Requires admin permission."""
tenant_id = uuid.UUID(current_user["tenant_id"]) tenant_id = uuid.UUID(current_user["tenant_id"])
user_id = uuid.UUID(current_user["user_id"]) user_id = uuid.UUID(current_user["user_id"])
role = current_user.get("role", "viewer")
if not check_permission(role, "settings", "delete"):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail={"detail": "Insufficient permissions", "code": "forbidden"},
)
try: try:
cid = uuid.UUID(currency_id) cid = uuid.UUID(currency_id)
+3 -10
View File
@@ -14,9 +14,8 @@ from fastapi import (
) )
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from app.core.auth import check_permission
from app.core.db import get_db from app.core.db import get_db
from app.deps import get_current_user from app.deps import require_permission
from app.services import import_export_service from app.services import import_export_service
router = APIRouter(prefix="/api/v1", tags=["import_export"]) router = APIRouter(prefix="/api/v1", tags=["import_export"])
@@ -27,17 +26,14 @@ async def import_csv(
file: UploadFile = File(...), file: UploadFile = File(...),
entity_type: str = Form("companies"), entity_type: str = Form("companies"),
db: AsyncSession = Depends(get_db), db: AsyncSession = Depends(get_db),
current_user: dict = Depends(get_current_user), current_user: dict = Depends(require_permission("import_export:write")),
): ):
"""Import companies or contacts from CSV file. """Import companies or contacts from CSV file.
entity_type: 'companies' or 'contacts'. entity_type: 'companies' or 'contacts'.
""" """
tenant_id = uuid.UUID(current_user["tenant_id"]) tenant_id = uuid.UUID(current_user["tenant_id"])
user_id = uuid.UUID(current_user["user_id"]) user_id = uuid.UUID(current_user["user_id"])
role = current_user.get("role", "viewer")
if not check_permission(role, "companies", "create"):
raise HTTPException(403, detail={"detail": "Insufficient permissions", "code": "forbidden"})
content = await file.read() content = await file.read()
csv_content = content.decode("utf-8") csv_content = content.decode("utf-8")
@@ -58,15 +54,12 @@ async def import_csv_preview(
file: UploadFile = File(...), file: UploadFile = File(...),
entity_type: str = Form("companies"), entity_type: str = Form("companies"),
db: AsyncSession = Depends(get_db), db: AsyncSession = Depends(get_db),
current_user: dict = Depends(get_current_user), current_user: dict = Depends(require_permission("import_export:read")),
): ):
"""Preview CSV import (dry-run — no DB changes).""" """Preview CSV import (dry-run — no DB changes)."""
tenant_id = uuid.UUID(current_user["tenant_id"]) tenant_id = uuid.UUID(current_user["tenant_id"])
user_id = uuid.UUID(current_user["user_id"]) user_id = uuid.UUID(current_user["user_id"])
role = current_user.get("role", "viewer")
if not check_permission(role, "companies", "read"):
raise HTTPException(403, detail={"detail": "Insufficient permissions", "code": "forbidden"})
content = await file.read() content = await file.read()
csv_content = content.decode("utf-8") csv_content = content.decode("utf-8")
+2 -2
View File
@@ -6,7 +6,7 @@ from fastapi import APIRouter, Depends
from fastapi.responses import PlainTextResponse from fastapi.responses import PlainTextResponse
from app.core.monitoring import generate_metrics from app.core.monitoring import generate_metrics
from app.deps import require_admin from app.deps import get_current_user
router = APIRouter(tags=["metrics"]) router = APIRouter(tags=["metrics"])
@@ -14,7 +14,7 @@ router = APIRouter(tags=["metrics"])
@router.get( @router.get(
"/api/v1/metrics", "/api/v1/metrics",
response_class=PlainTextResponse, response_class=PlainTextResponse,
dependencies=[Depends(require_admin)], dependencies=[Depends(get_current_user)],
) )
async def metrics(): async def metrics():
"""Prometheus metrics endpoint. """Prometheus metrics endpoint.
+7 -7
View File
@@ -14,7 +14,7 @@ from app.core.notifications import (
list_notifications, list_notifications,
mark_notification_read, mark_notification_read,
) )
from app.deps import get_current_user from app.deps import require_permission
from app.models.notification import ( from app.models.notification import (
NotificationPreference, NotificationPreference,
NotificationType, NotificationType,
@@ -29,7 +29,7 @@ async def list_notifications_endpoint(
page: int = Query(1, ge=1), page: int = Query(1, ge=1),
page_size: int = Query(25, ge=1, le=100), page_size: int = Query(25, ge=1, le=100),
db: AsyncSession = Depends(get_db), db: AsyncSession = Depends(get_db),
current_user: dict = Depends(get_current_user), current_user: dict = Depends(require_permission("notifications:read")),
): ):
"""List notifications (unread first).""" """List notifications (unread first)."""
tenant_id = uuid.UUID(current_user["tenant_id"]) tenant_id = uuid.UUID(current_user["tenant_id"])
@@ -41,7 +41,7 @@ async def list_notifications_endpoint(
async def mark_notification_read_endpoint( async def mark_notification_read_endpoint(
notification_id: str, notification_id: str,
db: AsyncSession = Depends(get_db), db: AsyncSession = Depends(get_db),
current_user: dict = Depends(get_current_user), current_user: dict = Depends(require_permission("notifications:write")),
): ):
"""Mark a notification as read.""" """Mark a notification as read."""
tenant_id = uuid.UUID(current_user["tenant_id"]) tenant_id = uuid.UUID(current_user["tenant_id"])
@@ -70,7 +70,7 @@ async def mark_notification_read_endpoint(
@router.get("/unread-count") @router.get("/unread-count")
async def unread_count_endpoint( async def unread_count_endpoint(
db: AsyncSession = Depends(get_db), db: AsyncSession = Depends(get_db),
current_user: dict = Depends(get_current_user), current_user: dict = Depends(require_permission("notifications:read")),
): ):
"""Get unread notification count.""" """Get unread notification count."""
tenant_id = uuid.UUID(current_user["tenant_id"]) tenant_id = uuid.UUID(current_user["tenant_id"])
@@ -82,7 +82,7 @@ async def unread_count_endpoint(
@router.get("/types") @router.get("/types")
async def list_notification_types( async def list_notification_types(
db: AsyncSession = Depends(get_db), db: AsyncSession = Depends(get_db),
current_user: dict = Depends(get_current_user), current_user: dict = Depends(require_permission("notifications:read")),
): ):
"""List all registered notification types with the user's preference.""" """List all registered notification types with the user's preference."""
tenant_id = uuid.UUID(current_user["tenant_id"]) tenant_id = uuid.UUID(current_user["tenant_id"])
@@ -125,7 +125,7 @@ async def list_notification_types(
@router.get("/preferences") @router.get("/preferences")
async def list_preferences( async def list_preferences(
db: AsyncSession = Depends(get_db), db: AsyncSession = Depends(get_db),
current_user: dict = Depends(get_current_user), current_user: dict = Depends(require_permission("notifications:read")),
): ):
"""List user's notification preferences.""" """List user's notification preferences."""
tenant_id = uuid.UUID(current_user["tenant_id"]) tenant_id = uuid.UUID(current_user["tenant_id"])
@@ -154,7 +154,7 @@ async def update_preference(
type_key: str, type_key: str,
data: NotificationPreferenceUpdate, data: NotificationPreferenceUpdate,
db: AsyncSession = Depends(get_db), db: AsyncSession = Depends(get_db),
current_user: dict = Depends(get_current_user), current_user: dict = Depends(require_permission("notifications:write")),
): ):
"""Update user's preference for a notification type (upsert).""" """Update user's preference for a notification type (upsert)."""
tenant_id = uuid.UUID(current_user["tenant_id"]) tenant_id = uuid.UUID(current_user["tenant_id"])
+9 -9
View File
@@ -7,7 +7,7 @@ from pydantic import BaseModel
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from app.core.db import get_db from app.core.db import get_db
from app.deps import require_admin from app.deps import require_permission
from app.plugins.migration_runner import MigrationValidationError from app.plugins.migration_runner import MigrationValidationError
from app.services.plugin_service import get_plugin_service from app.services.plugin_service import get_plugin_service
@@ -22,7 +22,7 @@ class PluginConfigUpdate(BaseModel):
@router.get("") @router.get("")
async def list_plugins( async def list_plugins(
db: AsyncSession = Depends(get_db), db: AsyncSession = Depends(get_db),
current_user: dict = Depends(require_admin), current_user: dict = Depends(require_permission("plugins:read")),
): ):
"""List all plugins with their current status (discovered, installed, active, inactive).""" """List all plugins with their current status (discovered, installed, active, inactive)."""
service = get_plugin_service() service = get_plugin_service()
@@ -32,7 +32,7 @@ async def list_plugins(
@router.get("/manifest") @router.get("/manifest")
async def get_manifest_schema( async def get_manifest_schema(
current_user: dict = Depends(require_admin), current_user: dict = Depends(require_permission("plugins:read")),
): ):
"""Get the plugin manifest schema documentation.""" """Get the plugin manifest schema documentation."""
service = get_plugin_service() service = get_plugin_service()
@@ -43,7 +43,7 @@ async def get_manifest_schema(
async def get_plugin_config( async def get_plugin_config(
name: str, name: str,
db: AsyncSession = Depends(get_db), db: AsyncSession = Depends(get_db),
current_user: dict = Depends(require_admin), current_user: dict = Depends(require_permission("plugins:read")),
): ):
"""Get the configuration for a specific plugin. """Get the configuration for a specific plugin.
@@ -64,7 +64,7 @@ async def update_plugin_config(
name: str, name: str,
body: PluginConfigUpdate, body: PluginConfigUpdate,
db: AsyncSession = Depends(get_db), db: AsyncSession = Depends(get_db),
current_user: dict = Depends(require_admin), current_user: dict = Depends(require_permission("plugins:configure")),
): ):
"""Update the configuration for a specific plugin. """Update the configuration for a specific plugin.
@@ -92,7 +92,7 @@ async def update_plugin_config(
async def install_plugin( async def install_plugin(
name: str, name: str,
db: AsyncSession = Depends(get_db), db: AsyncSession = Depends(get_db),
current_user: dict = Depends(require_admin), current_user: dict = Depends(require_permission("plugins:configure")),
): ):
"""Install a plugin by name. Runs migrations and creates DB record. """Install a plugin by name. Runs migrations and creates DB record.
@@ -125,7 +125,7 @@ async def install_plugin(
async def activate_plugin( async def activate_plugin(
name: str, name: str,
db: AsyncSession = Depends(get_db), db: AsyncSession = Depends(get_db),
current_user: dict = Depends(require_admin), current_user: dict = Depends(require_permission("plugins:configure")),
): ):
"""Activate a plugin by name. Registers event listeners and routes. """Activate a plugin by name. Registers event listeners and routes.
@@ -158,7 +158,7 @@ async def activate_plugin(
async def deactivate_plugin( async def deactivate_plugin(
name: str, name: str,
db: AsyncSession = Depends(get_db), db: AsyncSession = Depends(get_db),
current_user: dict = Depends(require_admin), current_user: dict = Depends(require_permission("plugins:configure")),
): ):
"""Deactivate a plugin by name. Unregisters event listeners and routes. """Deactivate a plugin by name. Unregisters event listeners and routes.
@@ -188,7 +188,7 @@ async def uninstall_plugin(
name: str, name: str,
remove_data: bool = Query(False), remove_data: bool = Query(False),
db: AsyncSession = Depends(get_db), db: AsyncSession = Depends(get_db),
current_user: dict = Depends(require_admin), current_user: dict = Depends(require_permission("plugins:configure")),
): ):
"""Uninstall a plugin. Optionally drop plugin-created tables with remove_data=true. """Uninstall a plugin. Optionally drop plugin-created tables with remove_data=true.
+40 -10
View File
@@ -13,7 +13,7 @@ from app.core.audit import log_audit
from app.core.auth import get_redis from app.core.auth import get_redis
from app.core.db import get_db from app.core.db import get_db
from app.core.permissions import invalidate_all_user_permissions from app.core.permissions import invalidate_all_user_permissions
from app.deps import get_current_user, require_admin from app.deps import require_permission
from app.models.plugin import Plugin as PluginModel from app.models.plugin import Plugin as PluginModel
from app.plugins.registry import get_registry from app.plugins.registry import get_registry
from app.schemas.role import RoleCreate, RoleUpdate from app.schemas.role import RoleCreate, RoleUpdate
@@ -25,23 +25,53 @@ router = APIRouter(prefix="/api/v1/roles", tags=["roles"])
SYSTEM_PERMISSIONS: list[dict[str, str]] = [ SYSTEM_PERMISSIONS: list[dict[str, str]] = [
{"key": "companies:read", "label": "Companies: Read", "category": "system"}, {"key": "companies:read", "label": "Companies: Read", "category": "system"},
{"key": "companies:write", "label": "Companies: Write", "category": "system"}, {"key": "companies:write", "label": "Companies: Write", "category": "system"},
{"key": "companies:delete", "label": "Companies: Delete", "category": "system"},
{"key": "contacts:read", "label": "Contacts: Read", "category": "system"}, {"key": "contacts:read", "label": "Contacts: Read", "category": "system"},
{"key": "contacts:write", "label": "Contacts: Write", "category": "system"}, {"key": "contacts:write", "label": "Contacts: Write", "category": "system"},
{"key": "contacts:delete", "label": "Contacts: Delete", "category": "system"},
{"key": "users:read", "label": "Users: Read", "category": "system"}, {"key": "users:read", "label": "Users: Read", "category": "system"},
{"key": "users:write", "label": "Users: Write", "category": "system"}, {"key": "users:write", "label": "Users: Write", "category": "system"},
{"key": "audit:read", "label": "Audit Log: Read", "category": "system"}, {"key": "users:delete", "label": "Users: Delete", "category": "system"},
{"key": "settings:write", "label": "Settings: Write", "category": "system"},
{"key": "plugins:install", "label": "Plugins: Install", "category": "system"},
{"key": "plugins:configure", "label": "Plugins: Configure", "category": "system"},
{"key": "roles:read", "label": "Roles: Read", "category": "system"}, {"key": "roles:read", "label": "Roles: Read", "category": "system"},
{"key": "roles:write", "label": "Roles: Write", "category": "system"}, {"key": "roles:write", "label": "Roles: Write", "category": "system"},
{"key": "roles:delete", "label": "Roles: Delete", "category": "system"},
{"key": "groups:read", "label": "Groups: Read", "category": "system"},
{"key": "groups:write", "label": "Groups: Write", "category": "system"},
{"key": "groups:delete", "label": "Groups: Delete", "category": "system"},
{"key": "audit:read", "label": "Audit Log: Read", "category": "system"},
{"key": "settings:read", "label": "Settings: Read", "category": "system"},
{"key": "settings:write", "label": "Settings: Write", "category": "system"},
{"key": "plugins:read", "label": "Plugins: Read", "category": "system"},
{"key": "plugins:install", "label": "Plugins: Install", "category": "system"},
{"key": "plugins:configure", "label": "Plugins: Configure", "category": "system"},
{"key": "tenants:read", "label": "Tenants: Read", "category": "system"},
{"key": "tenants:write", "label": "Tenants: Write", "category": "system"},
{"key": "tenants:delete", "label": "Tenants: Delete", "category": "system"},
{"key": "notifications:read", "label": "Notifications: Read", "category": "system"},
{"key": "notifications:write", "label": "Notifications: Write", "category": "system"},
{"key": "attachments:read", "label": "Attachments: Read", "category": "system"},
{"key": "attachments:write", "label": "Attachments: Write", "category": "system"},
{"key": "attachments:delete", "label": "Attachments: Delete", "category": "system"},
{"key": "workflows:read", "label": "Workflows: Read", "category": "system"},
{"key": "workflows:write", "label": "Workflows: Write", "category": "system"},
{"key": "sequences:read", "label": "Sequences: Read", "category": "system"},
{"key": "sequences:write", "label": "Sequences: Write", "category": "system"},
{"key": "addresses:read", "label": "Addresses: Read", "category": "system"},
{"key": "addresses:write", "label": "Addresses: Write", "category": "system"},
{"key": "addresses:delete", "label": "Addresses: Delete", "category": "system"},
{"key": "taxes:read", "label": "Taxes: Read", "category": "system"},
{"key": "taxes:write", "label": "Taxes: Write", "category": "system"},
{"key": "currencies:read", "label": "Currencies: Read", "category": "system"},
{"key": "currencies:write", "label": "Currencies: Write", "category": "system"},
{"key": "import_export:read", "label": "Import/Export: Read", "category": "system"},
{"key": "import_export:write", "label": "Import/Export: Write", "category": "system"},
] ]
@router.get("/permissions") @router.get("/permissions")
async def list_permissions( async def list_permissions(
db: AsyncSession = Depends(get_db), db: AsyncSession = Depends(get_db),
current_user: dict = Depends(require_admin), current_user: dict = Depends(require_permission("roles:read")),
): ):
"""List all available permissions (system + active plugin permissions). """List all available permissions (system + active plugin permissions).
@@ -84,7 +114,7 @@ async def list_permissions(
@router.get("") @router.get("")
async def list_roles( async def list_roles(
db: AsyncSession = Depends(get_db), db: AsyncSession = Depends(get_db),
current_user: dict = Depends(get_current_user), current_user: dict = Depends(require_permission("roles:read")),
): ):
"""List roles for the current tenant.""" """List roles for the current tenant."""
tenant_id = uuid.UUID(current_user["tenant_id"]) tenant_id = uuid.UUID(current_user["tenant_id"])
@@ -96,7 +126,7 @@ async def list_roles(
async def create_role( async def create_role(
body: RoleCreate, body: RoleCreate,
db: AsyncSession = Depends(get_db), db: AsyncSession = Depends(get_db),
current_user: dict = Depends(require_admin), current_user: dict = Depends(require_permission("roles:write")),
): ):
"""Create a custom role (admin only).""" """Create a custom role (admin only)."""
tenant_id = uuid.UUID(current_user["tenant_id"]) tenant_id = uuid.UUID(current_user["tenant_id"])
@@ -123,7 +153,7 @@ async def update_role(
role_id: str, role_id: str,
body: RoleUpdate, body: RoleUpdate,
db: AsyncSession = Depends(get_db), db: AsyncSession = Depends(get_db),
current_user: dict = Depends(require_admin), current_user: dict = Depends(require_permission("roles:write")),
): ):
"""Update a role (admin only).""" """Update a role (admin only)."""
tenant_id = uuid.UUID(current_user["tenant_id"]) tenant_id = uuid.UUID(current_user["tenant_id"])
@@ -168,7 +198,7 @@ async def update_role(
async def delete_role( async def delete_role(
role_id: str, role_id: str,
db: AsyncSession = Depends(get_db), db: AsyncSession = Depends(get_db),
current_user: dict = Depends(require_admin), current_user: dict = Depends(require_permission("roles:write")),
): ):
"""Delete a role (admin only).""" """Delete a role (admin only)."""
tenant_id = uuid.UUID(current_user["tenant_id"]) tenant_id = uuid.UUID(current_user["tenant_id"])
+5 -30
View File
@@ -7,9 +7,8 @@ import uuid
from fastapi import APIRouter, Depends, HTTPException, status from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from app.core.auth import check_permission
from app.core.db import get_db from app.core.db import get_db
from app.deps import get_current_user from app.deps import require_permission
from app.schemas.sequence import SequenceCreate, SequenceUpdate from app.schemas.sequence import SequenceCreate, SequenceUpdate
from app.services import sequence_service from app.services import sequence_service
@@ -19,17 +18,11 @@ router = APIRouter(prefix="/api/v1/sequences", tags=["sequences"])
@router.get("") @router.get("")
async def list_sequences( async def list_sequences(
db: AsyncSession = Depends(get_db), db: AsyncSession = Depends(get_db),
current_user: dict = Depends(get_current_user), current_user: dict = Depends(require_permission("sequences:read")),
): ):
"""List all sequences for the current tenant. Admin only.""" """List all sequences for the current tenant. Admin only."""
tenant_id = uuid.UUID(current_user["tenant_id"]) tenant_id = uuid.UUID(current_user["tenant_id"])
role = current_user.get("role", "viewer")
if not check_permission(role, "settings", "read"):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail={"detail": "Insufficient permissions", "code": "forbidden"},
)
return await sequence_service.list_sequences(db, tenant_id) return await sequence_service.list_sequences(db, tenant_id)
@@ -38,18 +31,12 @@ async def list_sequences(
async def create_sequence( async def create_sequence(
body: SequenceCreate, body: SequenceCreate,
db: AsyncSession = Depends(get_db), db: AsyncSession = Depends(get_db),
current_user: dict = Depends(get_current_user), current_user: dict = Depends(require_permission("sequences:write")),
): ):
"""Create a new sequence. Admin only.""" """Create a new sequence. Admin only."""
tenant_id = uuid.UUID(current_user["tenant_id"]) tenant_id = uuid.UUID(current_user["tenant_id"])
user_id = uuid.UUID(current_user["user_id"]) user_id = uuid.UUID(current_user["user_id"])
role = current_user.get("role", "viewer")
if not check_permission(role, "settings", "create"):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail={"detail": "Insufficient permissions", "code": "forbidden"},
)
data = body.model_dump() data = body.model_dump()
return await sequence_service.create_sequence(db, tenant_id, user_id, data) return await sequence_service.create_sequence(db, tenant_id, user_id, data)
@@ -60,18 +47,12 @@ async def update_sequence(
sequence_id: str, sequence_id: str,
body: SequenceUpdate, body: SequenceUpdate,
db: AsyncSession = Depends(get_db), db: AsyncSession = Depends(get_db),
current_user: dict = Depends(get_current_user), current_user: dict = Depends(require_permission("sequences:write")),
): ):
"""Update a sequence (name, prefix, padding only). Admin only.""" """Update a sequence (name, prefix, padding only). Admin only."""
tenant_id = uuid.UUID(current_user["tenant_id"]) tenant_id = uuid.UUID(current_user["tenant_id"])
user_id = uuid.UUID(current_user["user_id"]) user_id = uuid.UUID(current_user["user_id"])
role = current_user.get("role", "viewer")
if not check_permission(role, "settings", "update"):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail={"detail": "Insufficient permissions", "code": "forbidden"},
)
try: try:
sid = uuid.UUID(sequence_id) sid = uuid.UUID(sequence_id)
@@ -89,18 +70,12 @@ async def update_sequence(
async def delete_sequence( async def delete_sequence(
sequence_id: str, sequence_id: str,
db: AsyncSession = Depends(get_db), db: AsyncSession = Depends(get_db),
current_user: dict = Depends(get_current_user), current_user: dict = Depends(require_permission("sequences:write")),
): ):
"""Delete a sequence (soft-delete). Admin only.""" """Delete a sequence (soft-delete). Admin only."""
tenant_id = uuid.UUID(current_user["tenant_id"]) tenant_id = uuid.UUID(current_user["tenant_id"])
user_id = uuid.UUID(current_user["user_id"]) user_id = uuid.UUID(current_user["user_id"])
role = current_user.get("role", "viewer")
if not check_permission(role, "settings", "delete"):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail={"detail": "Insufficient permissions", "code": "forbidden"},
)
try: try:
sid = uuid.UUID(sequence_id) sid = uuid.UUID(sequence_id)
+3 -10
View File
@@ -7,9 +7,8 @@ import uuid
from fastapi import APIRouter, Depends, HTTPException, status from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from app.core.auth import check_permission
from app.core.db import get_db from app.core.db import get_db
from app.deps import get_current_user from app.deps import require_permission
from app.schemas.system_settings import SystemSettingsUpsert from app.schemas.system_settings import SystemSettingsUpsert
from app.services import system_settings_service from app.services import system_settings_service
@@ -19,7 +18,7 @@ router = APIRouter(prefix="/api/v1/system-settings", tags=["system-settings"])
@router.get("") @router.get("")
async def get_system_settings( async def get_system_settings(
db: AsyncSession = Depends(get_db), db: AsyncSession = Depends(get_db),
current_user: dict = Depends(get_current_user), current_user: dict = Depends(require_permission("settings:read")),
): ):
"""Get system settings for the current tenant.""" """Get system settings for the current tenant."""
tenant_id = uuid.UUID(current_user["tenant_id"]) tenant_id = uuid.UUID(current_user["tenant_id"])
@@ -33,18 +32,12 @@ async def get_system_settings(
async def upsert_system_settings( async def upsert_system_settings(
body: SystemSettingsUpsert, body: SystemSettingsUpsert,
db: AsyncSession = Depends(get_db), db: AsyncSession = Depends(get_db),
current_user: dict = Depends(get_current_user), current_user: dict = Depends(require_permission("settings:write")),
): ):
"""Upsert system settings. Requires admin permission.""" """Upsert system settings. Requires admin permission."""
tenant_id = uuid.UUID(current_user["tenant_id"]) tenant_id = uuid.UUID(current_user["tenant_id"])
user_id = uuid.UUID(current_user["user_id"]) user_id = uuid.UUID(current_user["user_id"])
role = current_user.get("role", "viewer")
if not check_permission(role, "settings", "update"):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail={"detail": "Insufficient permissions", "code": "forbidden"},
)
data = body.model_dump() data = body.model_dump()
return await system_settings_service.upsert_system_settings(db, tenant_id, user_id, data) return await system_settings_service.upsert_system_settings(db, tenant_id, user_id, data)
+5 -24
View File
@@ -7,9 +7,8 @@ import uuid
from fastapi import APIRouter, Depends, HTTPException, status from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from app.core.auth import check_permission
from app.core.db import get_db from app.core.db import get_db
from app.deps import get_current_user from app.deps import require_permission
from app.schemas.tax import TaxRateCreate, TaxRateUpdate from app.schemas.tax import TaxRateCreate, TaxRateUpdate
from app.services import tax_service from app.services import tax_service
@@ -19,7 +18,7 @@ router = APIRouter(prefix="/api/v1/taxes", tags=["taxes"])
@router.get("") @router.get("")
async def list_tax_rates( async def list_tax_rates(
db: AsyncSession = Depends(get_db), db: AsyncSession = Depends(get_db),
current_user: dict = Depends(get_current_user), current_user: dict = Depends(require_permission("taxes:read")),
): ):
"""List all tax rates for the current tenant.""" """List all tax rates for the current tenant."""
tenant_id = uuid.UUID(current_user["tenant_id"]) tenant_id = uuid.UUID(current_user["tenant_id"])
@@ -30,18 +29,12 @@ async def list_tax_rates(
async def create_tax_rate( async def create_tax_rate(
body: TaxRateCreate, body: TaxRateCreate,
db: AsyncSession = Depends(get_db), db: AsyncSession = Depends(get_db),
current_user: dict = Depends(get_current_user), current_user: dict = Depends(require_permission("taxes:write")),
): ):
"""Create a tax rate. Requires admin permission.""" """Create a tax rate. Requires admin permission."""
tenant_id = uuid.UUID(current_user["tenant_id"]) tenant_id = uuid.UUID(current_user["tenant_id"])
user_id = uuid.UUID(current_user["user_id"]) user_id = uuid.UUID(current_user["user_id"])
role = current_user.get("role", "viewer")
if not check_permission(role, "settings", "create"):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail={"detail": "Insufficient permissions", "code": "forbidden"},
)
data = body.model_dump() data = body.model_dump()
return await tax_service.create_tax_rate(db, tenant_id, user_id, data) return await tax_service.create_tax_rate(db, tenant_id, user_id, data)
@@ -52,18 +45,12 @@ async def update_tax_rate(
tax_id: str, tax_id: str,
body: TaxRateUpdate, body: TaxRateUpdate,
db: AsyncSession = Depends(get_db), db: AsyncSession = Depends(get_db),
current_user: dict = Depends(get_current_user), current_user: dict = Depends(require_permission("taxes:write")),
): ):
"""Update a tax rate. Requires admin permission.""" """Update a tax rate. Requires admin permission."""
tenant_id = uuid.UUID(current_user["tenant_id"]) tenant_id = uuid.UUID(current_user["tenant_id"])
user_id = uuid.UUID(current_user["user_id"]) user_id = uuid.UUID(current_user["user_id"])
role = current_user.get("role", "viewer")
if not check_permission(role, "settings", "update"):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail={"detail": "Insufficient permissions", "code": "forbidden"},
)
try: try:
tid = uuid.UUID(tax_id) tid = uuid.UUID(tax_id)
@@ -81,18 +68,12 @@ async def update_tax_rate(
async def delete_tax_rate( async def delete_tax_rate(
tax_id: str, tax_id: str,
db: AsyncSession = Depends(get_db), db: AsyncSession = Depends(get_db),
current_user: dict = Depends(get_current_user), current_user: dict = Depends(require_permission("taxes:write")),
): ):
"""Delete a tax rate. Requires admin permission.""" """Delete a tax rate. Requires admin permission."""
tenant_id = uuid.UUID(current_user["tenant_id"]) tenant_id = uuid.UUID(current_user["tenant_id"])
user_id = uuid.UUID(current_user["user_id"]) user_id = uuid.UUID(current_user["user_id"])
role = current_user.get("role", "viewer")
if not check_permission(role, "settings", "delete"):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail={"detail": "Insufficient permissions", "code": "forbidden"},
)
try: try:
tid = uuid.UUID(tax_id) tid = uuid.UUID(tax_id)
+5 -5
View File
@@ -8,7 +8,7 @@ from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from app.core.db import get_db from app.core.db import get_db
from app.deps import get_current_user, require_admin from app.deps import require_permission
from app.schemas.tenant import TenantCreate, TenantUserAssign from app.schemas.tenant import TenantCreate, TenantUserAssign
from app.services.tenant_service import tenant_service from app.services.tenant_service import tenant_service
@@ -18,7 +18,7 @@ router = APIRouter(prefix="/api/v1/tenants", tags=["tenants"])
@router.get("") @router.get("")
async def list_tenants( async def list_tenants(
db: AsyncSession = Depends(get_db), db: AsyncSession = Depends(get_db),
current_user: dict = Depends(get_current_user), current_user: dict = Depends(require_permission("tenants:read")),
): ):
"""List tenants for the current user.""" """List tenants for the current user."""
user_id = uuid.UUID(current_user["user_id"]) user_id = uuid.UUID(current_user["user_id"])
@@ -30,7 +30,7 @@ async def list_tenants(
async def create_tenant( async def create_tenant(
body: TenantCreate, body: TenantCreate,
db: AsyncSession = Depends(get_db), db: AsyncSession = Depends(get_db),
current_user: dict = Depends(require_admin), current_user: dict = Depends(require_permission("tenants:write")),
): ):
"""Create a new tenant (admin only).""" """Create a new tenant (admin only)."""
tenant = await tenant_service.create_tenant(db, body.name, body.slug) tenant = await tenant_service.create_tenant(db, body.name, body.slug)
@@ -45,7 +45,7 @@ async def create_tenant(
async def list_tenant_users( async def list_tenant_users(
tenant_id: str, tenant_id: str,
db: AsyncSession = Depends(get_db), db: AsyncSession = Depends(get_db),
current_user: dict = Depends(require_admin), current_user: dict = Depends(require_permission("tenants:write")),
): ):
"""List users in a tenant (admin only).""" """List users in a tenant (admin only)."""
try: try:
@@ -64,7 +64,7 @@ async def assign_user_to_tenant(
tenant_id: str, tenant_id: str,
body: TenantUserAssign, body: TenantUserAssign,
db: AsyncSession = Depends(get_db), db: AsyncSession = Depends(get_db),
current_user: dict = Depends(require_admin), current_user: dict = Depends(require_permission("tenants:write")),
): ):
"""Assign a user to a tenant (admin only).""" """Assign a user to a tenant (admin only)."""
try: try:
+6 -6
View File
@@ -13,7 +13,7 @@ from app.core.auth import get_redis
from app.core.db import get_db from app.core.db import get_db
from app.core.notifications import create_notification from app.core.notifications import create_notification
from app.core.permissions import invalidate_permission_cache from app.core.permissions import invalidate_permission_cache
from app.deps import get_current_user, require_admin from app.deps import require_permission
from app.schemas.user import UserCreate, UserUpdate from app.schemas.user import UserCreate, UserUpdate
from app.services.user_service import user_service, _UNSET from app.services.user_service import user_service, _UNSET
@@ -42,7 +42,7 @@ async def list_users(
page_size: int = Query(25, ge=1, le=100), page_size: int = Query(25, ge=1, le=100),
search: str | None = Query(None), search: str | None = Query(None),
db: AsyncSession = Depends(get_db), db: AsyncSession = Depends(get_db),
current_user: dict = Depends(require_admin), current_user: dict = Depends(require_permission("users:read")),
): ):
"""List users (admin only, paginated).""" """List users (admin only, paginated)."""
tenant_id = uuid.UUID(current_user["tenant_id"]) tenant_id = uuid.UUID(current_user["tenant_id"])
@@ -53,7 +53,7 @@ async def list_users(
async def create_user( async def create_user(
body: UserCreate, body: UserCreate,
db: AsyncSession = Depends(get_db), db: AsyncSession = Depends(get_db),
current_user: dict = Depends(require_admin), current_user: dict = Depends(require_permission("users:write")),
): ):
"""Create a new user (admin only).""" """Create a new user (admin only)."""
tenant_id = uuid.UUID(current_user["tenant_id"]) tenant_id = uuid.UUID(current_user["tenant_id"])
@@ -107,7 +107,7 @@ async def create_user(
async def get_user( async def get_user(
user_id: str, user_id: str,
db: AsyncSession = Depends(get_db), db: AsyncSession = Depends(get_db),
current_user: dict = Depends(get_current_user), current_user: dict = Depends(require_permission("users:read")),
): ):
"""Get a single user.""" """Get a single user."""
tenant_id = uuid.UUID(current_user["tenant_id"]) tenant_id = uuid.UUID(current_user["tenant_id"])
@@ -138,7 +138,7 @@ async def update_user(
user_id: str, user_id: str,
body: UserUpdate, body: UserUpdate,
db: AsyncSession = Depends(get_db), db: AsyncSession = Depends(get_db),
current_user: dict = Depends(require_admin), current_user: dict = Depends(require_permission("users:write")),
): ):
"""Update a user (admin only). """Update a user (admin only).
@@ -220,7 +220,7 @@ async def update_user(
async def delete_user( async def delete_user(
user_id: str, user_id: str,
db: AsyncSession = Depends(get_db), db: AsyncSession = Depends(get_db),
current_user: dict = Depends(require_admin), current_user: dict = Depends(require_permission("users:write")),
): ):
"""Delete a user (admin only).""" """Delete a user (admin only)."""
tenant_id = uuid.UUID(current_user["tenant_id"]) tenant_id = uuid.UUID(current_user["tenant_id"])
+11 -30
View File
@@ -7,9 +7,8 @@ import uuid
from fastapi import APIRouter, Depends, HTTPException, Query, Response, status from fastapi import APIRouter, Depends, HTTPException, Query, Response, status
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from app.core.auth import check_permission
from app.core.db import get_db from app.core.db import get_db
from app.deps import get_current_user from app.deps import require_permission
from app.schemas.workflow import AdvanceRequest, InstanceCreate, WorkflowCreate, WorkflowUpdate from app.schemas.workflow import AdvanceRequest, InstanceCreate, WorkflowCreate, WorkflowUpdate
from app.services import workflow_service from app.services import workflow_service
@@ -25,7 +24,7 @@ async def list_workflows(
page_size: int = Query(20, ge=1, le=100), page_size: int = Query(20, ge=1, le=100),
is_active: bool | None = Query(None), is_active: bool | None = Query(None),
db: AsyncSession = Depends(get_db), db: AsyncSession = Depends(get_db),
current_user: dict = Depends(get_current_user), current_user: dict = Depends(require_permission("workflows:read")),
): ):
"""List workflows with pagination.""" """List workflows with pagination."""
tenant_id = uuid.UUID(current_user["tenant_id"]) tenant_id = uuid.UUID(current_user["tenant_id"])
@@ -42,18 +41,12 @@ async def list_workflows(
async def create_workflow( async def create_workflow(
body: WorkflowCreate, body: WorkflowCreate,
db: AsyncSession = Depends(get_db), db: AsyncSession = Depends(get_db),
current_user: dict = Depends(get_current_user), current_user: dict = Depends(require_permission("workflows:write")),
): ):
"""Create a new workflow definition. Requires write permission.""" """Create a new workflow definition. Requires write permission."""
tenant_id = uuid.UUID(current_user["tenant_id"]) tenant_id = uuid.UUID(current_user["tenant_id"])
user_id = uuid.UUID(current_user["user_id"]) user_id = uuid.UUID(current_user["user_id"])
role = current_user.get("role", "viewer")
if not check_permission(role, "workflows", "create"):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail={"detail": "Insufficient permissions", "code": "forbidden"},
)
data = body.model_dump() data = body.model_dump()
return await workflow_service.create_workflow(db, tenant_id, user_id, data) return await workflow_service.create_workflow(db, tenant_id, user_id, data)
@@ -65,7 +58,7 @@ async def list_instances(
page_size: int = Query(20, ge=1, le=100), page_size: int = Query(20, ge=1, le=100),
status: str | None = Query(None), status: str | None = Query(None),
db: AsyncSession = Depends(get_db), db: AsyncSession = Depends(get_db),
current_user: dict = Depends(get_current_user), current_user: dict = Depends(require_permission("workflows:read")),
): ):
"""List workflow instances with optional status filter.""" """List workflow instances with optional status filter."""
tenant_id = uuid.UUID(current_user["tenant_id"]) tenant_id = uuid.UUID(current_user["tenant_id"])
@@ -82,7 +75,7 @@ async def list_instances(
async def get_workflow( async def get_workflow(
workflow_id: str, workflow_id: str,
db: AsyncSession = Depends(get_db), db: AsyncSession = Depends(get_db),
current_user: dict = Depends(get_current_user), current_user: dict = Depends(require_permission("workflows:read")),
): ):
"""Get a single workflow by ID.""" """Get a single workflow by ID."""
tenant_id = uuid.UUID(current_user["tenant_id"]) tenant_id = uuid.UUID(current_user["tenant_id"])
@@ -100,18 +93,12 @@ async def update_workflow(
workflow_id: str, workflow_id: str,
body: WorkflowUpdate, body: WorkflowUpdate,
db: AsyncSession = Depends(get_db), db: AsyncSession = Depends(get_db),
current_user: dict = Depends(get_current_user), current_user: dict = Depends(require_permission("workflows:write")),
): ):
"""Update a workflow definition.""" """Update a workflow definition."""
tenant_id = uuid.UUID(current_user["tenant_id"]) tenant_id = uuid.UUID(current_user["tenant_id"])
user_id = uuid.UUID(current_user["user_id"]) user_id = uuid.UUID(current_user["user_id"])
role = current_user.get("role", "viewer")
if not check_permission(role, "workflows", "update"):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail={"detail": "Insufficient permissions", "code": "forbidden"},
)
data = body.model_dump(exclude_unset=True) data = body.model_dump(exclude_unset=True)
result = await workflow_service.update_workflow(db, tenant_id, user_id, workflow_id, data) result = await workflow_service.update_workflow(db, tenant_id, user_id, workflow_id, data)
@@ -127,18 +114,12 @@ async def update_workflow(
async def delete_workflow( async def delete_workflow(
workflow_id: str, workflow_id: str,
db: AsyncSession = Depends(get_db), db: AsyncSession = Depends(get_db),
current_user: dict = Depends(get_current_user), current_user: dict = Depends(require_permission("workflows:write")),
): ):
"""Delete a workflow definition.""" """Delete a workflow definition."""
tenant_id = uuid.UUID(current_user["tenant_id"]) tenant_id = uuid.UUID(current_user["tenant_id"])
user_id = uuid.UUID(current_user["user_id"]) user_id = uuid.UUID(current_user["user_id"])
role = current_user.get("role", "viewer")
if not check_permission(role, "workflows", "delete"):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail={"detail": "Insufficient permissions", "code": "forbidden"},
)
deleted = await workflow_service.delete_workflow(db, tenant_id, user_id, workflow_id) deleted = await workflow_service.delete_workflow(db, tenant_id, user_id, workflow_id)
if not deleted: if not deleted:
@@ -157,7 +138,7 @@ async def create_instance(
workflow_id: str, workflow_id: str,
body: InstanceCreate, body: InstanceCreate,
db: AsyncSession = Depends(get_db), db: AsyncSession = Depends(get_db),
current_user: dict = Depends(get_current_user), current_user: dict = Depends(require_permission("workflows:write")),
): ):
"""Create a new workflow instance.""" """Create a new workflow instance."""
tenant_id = uuid.UUID(current_user["tenant_id"]) tenant_id = uuid.UUID(current_user["tenant_id"])
@@ -183,7 +164,7 @@ async def create_instance(
async def get_instance( async def get_instance(
instance_id: str, instance_id: str,
db: AsyncSession = Depends(get_db), db: AsyncSession = Depends(get_db),
current_user: dict = Depends(get_current_user), current_user: dict = Depends(require_permission("workflows:read")),
): ):
"""Get a workflow instance with step history.""" """Get a workflow instance with step history."""
tenant_id = uuid.UUID(current_user["tenant_id"]) tenant_id = uuid.UUID(current_user["tenant_id"])
@@ -201,7 +182,7 @@ async def advance_instance(
instance_id: str, instance_id: str,
body: AdvanceRequest, body: AdvanceRequest,
db: AsyncSession = Depends(get_db), db: AsyncSession = Depends(get_db),
current_user: dict = Depends(get_current_user), current_user: dict = Depends(require_permission("workflows:write")),
): ):
"""Advance or reject a workflow instance step. """Advance or reject a workflow instance step.
@@ -236,7 +217,7 @@ async def advance_instance(
async def cancel_instance( async def cancel_instance(
instance_id: str, instance_id: str,
db: AsyncSession = Depends(get_db), db: AsyncSession = Depends(get_db),
current_user: dict = Depends(get_current_user), current_user: dict = Depends(require_permission("workflows:write")),
): ):
"""Cancel a workflow instance. Returns 200 with cancelled instance.""" """Cancel a workflow instance. Returns 200 with cancelled instance."""
tenant_id = uuid.UUID(current_user["tenant_id"]) tenant_id = uuid.UUID(current_user["tenant_id"])