feat: Implement multi-tenancy support in middleware and security layers

- Enhanced TenantMiddleware to validate tenant information from JWT tokens.
- Added LicenseValidationMiddleware to check tenant licenses before processing requests.
- Updated security utilities to extract tenant information from tokens and validate company access.
- Introduced CompanyStore to manage active company state and handle company switching in the frontend.
- Modified API routes to include company_id in requests for better resource management.
- Improved logging and error handling throughout the middleware and API layers.
- Updated frontend components to reflect changes in company management and selection.
- Added new API route for fetching user's companies with proper authentication handling.
This commit is contained in:
2025-11-11 14:00:56 -06:00
parent e1eb6bbd01
commit 52b8fcd434
242 changed files with 7067 additions and 3274 deletions

View File

@@ -1,15 +1,18 @@
from pydantic import BaseModel
from typing import Optional
class ExchangeRateBaseDTO(BaseModel):
date: int
value: Optional[float]
local_currency: Optional[str]
foreign_currency: Optional[str]
class ExchangeRateCreateDTO(ExchangeRateBaseDTO):
pass
class ExchangeRateResponseDTO(ExchangeRateBaseDTO):
class Config:
from_attributes = True
from_attributes = True

View File

@@ -1,6 +1,15 @@
from typing import Optional
from decimal import Decimal
from sqlalchemy import Integer, String, DECIMAL, PrimaryKeyConstraint, DateTime, ForeignKeyConstraint, UniqueConstraint, ForeignKey
from sqlalchemy import (
Integer,
String,
DECIMAL,
PrimaryKeyConstraint,
DateTime,
ForeignKeyConstraint,
UniqueConstraint,
ForeignKey,
)
from sqlalchemy.orm import Mapped, mapped_column
from core.database import Base
@@ -8,18 +17,24 @@ from core.database import Base
class ExchangeRate(Base):
__tablename__ = "exchange_rate"
__table_args__ = (
PrimaryKeyConstraint('id', name='exchange_rate_pkey'),
ForeignKeyConstraint(['tenant_id'], ['a76.tenants.id'], name='fk_exchange_rate_tenant'),
ForeignKeyConstraint(['company_id'], ['a76.company.id'], name='fk_exchange_rate_company'),
UniqueConstraint('tenant_id', 'company_id', 'date', name='uq_exchange_rate_date_tenant'),
{"schema": "a76"}
PrimaryKeyConstraint("id", name="exchange_rate_pkey"),
ForeignKeyConstraint(
["tenant_id"], ["a76.tenants.id"], name="fk_exchange_rate_tenant"
),
ForeignKeyConstraint(
["company_id"], ["a76.company.id"], name="fk_exchange_rate_company"
),
UniqueConstraint(
"tenant_id", "company_id", "date", name="uq_exchange_rate_date_tenant"
),
{"schema": "a76"},
)
id: Mapped[int] = mapped_column(Integer, primary_key=True)
tenant_id: Mapped[int] = mapped_column(Integer, nullable=False, index=True)
company_id: Mapped[int] = mapped_column(Integer, nullable=False, index=True)
date: Mapped[int] = mapped_column(DateTime)
value: Mapped[Optional[Decimal]] = mapped_column(DECIMAL(13, 6))
local_currency: Mapped[Optional[str]] = mapped_column(String(7))
foreign_currency: Mapped[Optional[str]] = mapped_column(String(7))
foreign_currency: Mapped[Optional[str]] = mapped_column(String(7))

View File

@@ -12,8 +12,7 @@ router = APIRouter(prefix="/exchange-rate", tags=["ExchangeRate"])
@router.get("/", response_model=List[ExchangeRateResponseDTO])
async def list_exchange_rates(
db: Session = Depends(get_core_db),
current_user: dict = Depends(get_current_user)
db: Session = Depends(get_core_db), current_user: dict = Depends(get_current_user)
):
"""
List all ExchangeRate entries.
@@ -32,7 +31,7 @@ async def list_exchange_rates(
async def read_exchange_rate(
date: int,
db: Session = Depends(get_core_db),
current_user: dict = Depends(get_current_user)
current_user: dict = Depends(get_current_user),
):
"""
Get a specific ExchangeRate by its date.
@@ -50,11 +49,13 @@ async def read_exchange_rate(
return exchange_rate
@router.post("/", response_model=ExchangeRateResponseDTO, status_code=status.HTTP_201_CREATED)
@router.post(
"/", response_model=ExchangeRateResponseDTO, status_code=status.HTTP_201_CREATED
)
async def create_exchange_rate(
exchange_rate_data: ExchangeRateCreateDTO,
db: Session = Depends(get_core_db),
current_user: dict = Depends(get_current_user)
current_user: dict = Depends(get_current_user),
):
"""
Create a new ExchangeRate entry.
@@ -73,7 +74,7 @@ async def create_exchange_rate(
async def delete_exchange_rate(
date: int,
db: Session = Depends(get_core_db),
current_user: dict = Depends(get_current_user)
current_user: dict = Depends(get_current_user),
):
"""
Delete an ExchangeRate by its date.
@@ -87,4 +88,4 @@ async def delete_exchange_rate(
exchange_rate = ExchangeRateService.delete_exchange_rate(db, date)
if not exchange_rate:
raise HTTPException(status_code=404, detail="ExchangeRate not found")
raise HTTPException(status_code=404, detail="ExchangeRate not found")

View File

@@ -1,13 +1,20 @@
from sqlalchemy.orm import Session
from . import models, dto
class ExchangeRateService:
@staticmethod
def get_exchange_rate_by_date(db: Session, date: int):
return db.query(models.ExchangeRate).filter(models.ExchangeRate.date == date).first()
return (
db.query(models.ExchangeRate)
.filter(models.ExchangeRate.date == date)
.first()
)
@staticmethod
def create_exchange_rate(db: Session, exchange_rate_data: dto.ExchangeRateCreateDTO):
def create_exchange_rate(
db: Session, exchange_rate_data: dto.ExchangeRateCreateDTO
):
new_exchange_rate = models.ExchangeRate(**exchange_rate_data.dict())
db.add(new_exchange_rate)
db.commit()
@@ -20,4 +27,4 @@ class ExchangeRateService:
if exchange_rate:
db.delete(exchange_rate)
db.commit()
return exchange_rate
return exchange_rate