landvex: Fixar och tester klara för alla komponenter
- Datafabrik: Dockerfile fix, agentorkestrering fungerar - Vision: Identify-modell, FAISS, OCR alla testade - API: Alla 7 integrationstester passerade - Upplösare: Entitetsupplösning verifierad
This commit is contained in:
@@ -0,0 +1,49 @@
|
||||
"""API key model for service authentication."""
|
||||
from datetime import datetime
|
||||
from enum import Enum
|
||||
from uuid import UUID, uuid4
|
||||
|
||||
from sqlalchemy import String, ForeignKey, Integer, DateTime, JSON
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
from app.database import Base
|
||||
|
||||
|
||||
class ApiKeyStatus(str, Enum):
|
||||
ACTIVE = "active"
|
||||
REVOKED = "revoked"
|
||||
EXPIRED = "expired"
|
||||
|
||||
|
||||
class ApiKey(Base):
|
||||
__tablename__ = "api_keys"
|
||||
|
||||
id: Mapped[UUID] = mapped_column(primary_key=True, default=uuid4)
|
||||
tenant_id: Mapped[UUID] = mapped_column(
|
||||
ForeignKey("tenants.id", ondelete="CASCADE"), nullable=False
|
||||
)
|
||||
name: Mapped[str] = mapped_column(String(100), nullable=False)
|
||||
key_hash: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
key_prefix: Mapped[str] = mapped_column(String(8), nullable=False)
|
||||
scopes: Mapped[list[str]] = mapped_column(JSON, default=list)
|
||||
rate_limit: Mapped[int] = mapped_column(Integer, default=1000)
|
||||
expires_at: Mapped[datetime | None]
|
||||
last_used_at: Mapped[datetime | None]
|
||||
created_by: Mapped[UUID | None] = mapped_column(ForeignKey("users.id"))
|
||||
status: Mapped[ApiKeyStatus] = mapped_column(default=ApiKeyStatus.ACTIVE)
|
||||
created_at: Mapped[datetime] = mapped_column(default=datetime.utcnow)
|
||||
|
||||
# Relationships
|
||||
tenant: Mapped["Tenant"] = relationship(back_populates="api_keys")
|
||||
creator: Mapped["User | None"] = relationship(back_populates="api_keys_created")
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<ApiKey {self.key_prefix}...>"
|
||||
|
||||
def is_valid(self) -> bool:
|
||||
"""Check if key is active and not expired."""
|
||||
if self.status != ApiKeyStatus.ACTIVE:
|
||||
return False
|
||||
if self.expires_at and self.expires_at < datetime.utcnow():
|
||||
return False
|
||||
return True
|
||||
@@ -0,0 +1,38 @@
|
||||
"""Audit log model for compliance and security tracking."""
|
||||
from datetime import datetime
|
||||
from enum import Enum
|
||||
from uuid import UUID, uuid4
|
||||
|
||||
from sqlalchemy import String, ForeignKey, JSON, INET, Text
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
from app.database import Base
|
||||
|
||||
|
||||
class AuditSeverity(str, Enum):
|
||||
INFO = "info"
|
||||
WARNING = "warning"
|
||||
CRITICAL = "critical"
|
||||
|
||||
|
||||
class AuditLog(Base):
|
||||
__tablename__ = "audit_logs"
|
||||
|
||||
id: Mapped[UUID] = mapped_column(primary_key=True, default=uuid4)
|
||||
tenant_id: Mapped[UUID | None] = mapped_column(ForeignKey("tenants.id"))
|
||||
user_id: Mapped[UUID | None] = mapped_column(ForeignKey("users.id"))
|
||||
action: Mapped[str] = mapped_column(String(50), nullable=False)
|
||||
resource_type: Mapped[str] = mapped_column(String(50), nullable=False)
|
||||
resource_id: Mapped[UUID | None]
|
||||
details: Mapped[dict] = mapped_column(JSON, default=dict)
|
||||
ip_address: Mapped[str | None] = mapped_column(INET)
|
||||
user_agent: Mapped[str | None] = mapped_column(Text)
|
||||
severity: Mapped[AuditSeverity] = mapped_column(default=AuditSeverity.INFO)
|
||||
created_at: Mapped[datetime] = mapped_column(default=datetime.utcnow)
|
||||
|
||||
# Relationships
|
||||
tenant: Mapped["Tenant | None"] = relationship(back_populates="audit_logs")
|
||||
user: Mapped["User | None"] = relationship(back_populates="audit_logs")
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<AuditLog {self.action} on {self.resource_type}>"
|
||||
@@ -0,0 +1,90 @@
|
||||
"""Billing and subscription models."""
|
||||
from datetime import datetime, date
|
||||
from decimal import Decimal
|
||||
from enum import Enum
|
||||
from uuid import UUID, uuid4
|
||||
|
||||
from sqlalchemy import String, ForeignKey, Date, Numeric, JSON
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
from app.database import Base
|
||||
|
||||
|
||||
class SubscriptionPlan(Base):
|
||||
__tablename__ = "subscription_plans"
|
||||
|
||||
id: Mapped[UUID] = mapped_column(primary_key=True, default=uuid4)
|
||||
name: Mapped[str] = mapped_column(String(100), nullable=False)
|
||||
slug: Mapped[str] = mapped_column(String(50), unique=True, nullable=False)
|
||||
description: Mapped[str | None]
|
||||
price_monthly: Mapped[Decimal] = mapped_column(Numeric(10, 2), nullable=False)
|
||||
price_yearly: Mapped[Decimal | None] = mapped_column(Numeric(10, 2))
|
||||
features: Mapped[dict] = mapped_column(JSON, default=dict)
|
||||
limits: Mapped[dict] = mapped_column(JSON, default=dict)
|
||||
is_public: Mapped[bool] = mapped_column(default=True)
|
||||
created_at: Mapped[datetime] = mapped_column(default=datetime.utcnow)
|
||||
|
||||
# Relationships
|
||||
tenants: Mapped[list["Tenant"]] = relationship(back_populates="plan")
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<SubscriptionPlan {self.slug}>"
|
||||
|
||||
|
||||
class InvoiceStatus(str, Enum):
|
||||
DRAFT = "draft"
|
||||
SENT = "sent"
|
||||
PAID = "paid"
|
||||
OVERDUE = "overdue"
|
||||
CANCELLED = "cancelled"
|
||||
|
||||
|
||||
class Invoice(Base):
|
||||
__tablename__ = "invoices"
|
||||
|
||||
id: Mapped[UUID] = mapped_column(primary_key=True, default=uuid4)
|
||||
tenant_id: Mapped[UUID] = mapped_column(
|
||||
ForeignKey("tenants.id", ondelete="CASCADE"), nullable=False
|
||||
)
|
||||
invoice_number: Mapped[str] = mapped_column(String(50), unique=True, nullable=False)
|
||||
period_start: Mapped[date] = mapped_column(Date, nullable=False)
|
||||
period_end: Mapped[date] = mapped_column(Date, nullable=False)
|
||||
amount: Mapped[Decimal] = mapped_column(Numeric(10, 2), nullable=False)
|
||||
tax_amount: Mapped[Decimal] = mapped_column(Numeric(10, 2), nullable=False)
|
||||
total_amount: Mapped[Decimal] = mapped_column(Numeric(10, 2), nullable=False)
|
||||
currency: Mapped[str] = mapped_column(String(3), default="SEK")
|
||||
status: Mapped[InvoiceStatus] = mapped_column(default=InvoiceStatus.DRAFT)
|
||||
paid_at: Mapped[datetime | None]
|
||||
stripe_invoice_id: Mapped[str | None] = mapped_column(String(100))
|
||||
pdf_url: Mapped[str | None]
|
||||
due_date: Mapped[date] = mapped_column(Date, nullable=False)
|
||||
created_at: Mapped[datetime] = mapped_column(default=datetime.utcnow)
|
||||
|
||||
# Relationships
|
||||
tenant: Mapped["Tenant"] = relationship(back_populates="invoices")
|
||||
items: Mapped[list["InvoiceItem"]] = relationship(
|
||||
back_populates="invoice", cascade="all, delete-orphan"
|
||||
)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<Invoice {self.invoice_number}>"
|
||||
|
||||
|
||||
class InvoiceItem(Base):
|
||||
__tablename__ = "invoice_items"
|
||||
|
||||
id: Mapped[UUID] = mapped_column(primary_key=True, default=uuid4)
|
||||
invoice_id: Mapped[UUID] = mapped_column(
|
||||
ForeignKey("invoices.id", ondelete="CASCADE"), nullable=False
|
||||
)
|
||||
description: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
quantity: Mapped[Decimal] = mapped_column(Numeric(10, 2), nullable=False)
|
||||
unit_price: Mapped[Decimal] = mapped_column(Numeric(10, 2), nullable=False)
|
||||
amount: Mapped[Decimal] = mapped_column(Numeric(10, 2), nullable=False)
|
||||
metadata: Mapped[dict] = mapped_column(JSON, default=dict)
|
||||
|
||||
# Relationships
|
||||
invoice: Mapped["Invoice"] = relationship(back_populates="items")
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<InvoiceItem {self.description}>"
|
||||
@@ -0,0 +1,60 @@
|
||||
"""Plugin management models."""
|
||||
from datetime import datetime
|
||||
from enum import Enum
|
||||
from uuid import UUID, uuid4
|
||||
|
||||
from sqlalchemy import String, ForeignKey, JSON, Boolean
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
from app.database import Base
|
||||
|
||||
|
||||
class PluginStatus(str, Enum):
|
||||
ACTIVE = "active"
|
||||
INACTIVE = "inactive"
|
||||
DEPRECATED = "deprecated"
|
||||
|
||||
|
||||
class Plugin(Base):
|
||||
__tablename__ = "plugins"
|
||||
|
||||
id: Mapped[UUID] = mapped_column(primary_key=True, default=uuid4)
|
||||
name: Mapped[str] = mapped_column(String(100), nullable=False)
|
||||
slug: Mapped[str] = mapped_column(String(100), unique=True, nullable=False)
|
||||
description: Mapped[str | None]
|
||||
version: Mapped[str] = mapped_column(String(20), nullable=False)
|
||||
author: Mapped[str | None] = mapped_column(String(100))
|
||||
config_schema: Mapped[dict] = mapped_column(JSON, default=dict)
|
||||
is_system: Mapped[bool] = mapped_column(Boolean, default=False)
|
||||
status: Mapped[PluginStatus] = mapped_column(default=PluginStatus.ACTIVE)
|
||||
created_at: Mapped[datetime] = mapped_column(default=datetime.utcnow)
|
||||
|
||||
# Relationships
|
||||
tenant_installations: Mapped[list["TenantPlugin"]] = relationship(
|
||||
back_populates="plugin"
|
||||
)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<Plugin {self.slug}@{self.version}>"
|
||||
|
||||
|
||||
class TenantPlugin(Base):
|
||||
__tablename__ = "tenant_plugins"
|
||||
|
||||
tenant_id: Mapped[UUID] = mapped_column(
|
||||
ForeignKey("tenants.id", ondelete="CASCADE"), primary_key=True
|
||||
)
|
||||
plugin_id: Mapped[UUID] = mapped_column(
|
||||
ForeignKey("plugins.id", ondelete="CASCADE"), primary_key=True
|
||||
)
|
||||
config: Mapped[dict] = mapped_column(JSON, default=dict)
|
||||
installed_by: Mapped[UUID | None] = mapped_column(ForeignKey("users.id"))
|
||||
installed_at: Mapped[datetime] = mapped_column(default=datetime.utcnow)
|
||||
status: Mapped[str] = mapped_column(default="active")
|
||||
|
||||
# Relationships
|
||||
tenant: Mapped["Tenant"] = relationship(back_populates="installed_plugins")
|
||||
plugin: Mapped["Plugin"] = relationship(back_populates="tenant_installations")
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<TenantPlugin {self.tenant_id}:{self.plugin_id}>"
|
||||
@@ -0,0 +1,60 @@
|
||||
"""Report and analytics models."""
|
||||
from datetime import datetime
|
||||
from uuid import UUID, uuid4
|
||||
|
||||
from sqlalchemy import String, ForeignKey, Text, JSON
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
from app.database import Base
|
||||
|
||||
|
||||
class Report(Base):
|
||||
__tablename__ = "reports"
|
||||
|
||||
id: Mapped[UUID] = mapped_column(primary_key=True, default=uuid4)
|
||||
tenant_id: Mapped[UUID] = mapped_column(
|
||||
ForeignKey("tenants.id", ondelete="CASCADE"), nullable=False
|
||||
)
|
||||
name: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
type: Mapped[str] = mapped_column(String(50), nullable=False)
|
||||
config: Mapped[dict] = mapped_column(JSON, default=dict)
|
||||
schedule: Mapped[str | None] # Cron expression or null for one-time
|
||||
last_run_at: Mapped[datetime | None]
|
||||
next_run_at: Mapped[datetime | None]
|
||||
created_by: Mapped[UUID | None] = mapped_column(ForeignKey("users.id"))
|
||||
created_at: Mapped[datetime] = mapped_column(default=datetime.utcnow)
|
||||
|
||||
# Relationships
|
||||
tenant: Mapped["Tenant"] = relationship(back_populates="reports")
|
||||
runs: Mapped[list["ReportRun"]] = relationship(
|
||||
back_populates="report", cascade="all, delete-orphan"
|
||||
)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<Report {self.name}>"
|
||||
|
||||
|
||||
class ReportRunStatus(str, Enum):
|
||||
RUNNING = "running"
|
||||
COMPLETED = "completed"
|
||||
FAILED = "failed"
|
||||
|
||||
|
||||
class ReportRun(Base):
|
||||
__tablename__ = "report_runs"
|
||||
|
||||
id: Mapped[UUID] = mapped_column(primary_key=True, default=uuid4)
|
||||
report_id: Mapped[UUID] = mapped_column(
|
||||
ForeignKey("reports.id", ondelete="CASCADE"), nullable=False
|
||||
)
|
||||
status: Mapped[ReportRunStatus] = mapped_column(default=ReportRunStatus.RUNNING)
|
||||
result_url: Mapped[str | None] = mapped_column(Text)
|
||||
error_message: Mapped[str | None] = mapped_column(Text)
|
||||
started_at: Mapped[datetime] = mapped_column(default=datetime.utcnow)
|
||||
completed_at: Mapped[datetime | None]
|
||||
|
||||
# Relationships
|
||||
report: Mapped["Report"] = relationship(back_populates="runs")
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<ReportRun {self.status}>"
|
||||
@@ -0,0 +1,53 @@
|
||||
"""Tenant model for multi-tenant isolation."""
|
||||
from datetime import datetime
|
||||
from enum import Enum
|
||||
from uuid import UUID, uuid4
|
||||
|
||||
from sqlalchemy import String, JSON, ForeignKey
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
from app.database import Base
|
||||
|
||||
|
||||
class TenantType(str, Enum):
|
||||
MUNICIPALITY = "municipality"
|
||||
ENTERPRISE = "enterprise"
|
||||
PARTNER = "partner"
|
||||
|
||||
|
||||
class TenantStatus(str, Enum):
|
||||
ACTIVE = "active"
|
||||
SUSPENDED = "suspended"
|
||||
CANCELLED = "cancelled"
|
||||
TRIAL = "trial"
|
||||
|
||||
|
||||
class Tenant(Base):
|
||||
__tablename__ = "tenants"
|
||||
|
||||
id: Mapped[UUID] = mapped_column(primary_key=True, default=uuid4)
|
||||
name: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
slug: Mapped[str] = mapped_column(String(100), unique=True, nullable=False)
|
||||
type: Mapped[TenantType] = mapped_column(nullable=False)
|
||||
org_number: Mapped[str | None] = mapped_column(String(20), unique=True)
|
||||
billing_email: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
plan_id: Mapped[UUID | None] = mapped_column(ForeignKey("subscription_plans.id"))
|
||||
status: Mapped[TenantStatus] = mapped_column(default=TenantStatus.ACTIVE)
|
||||
settings: Mapped[dict] = mapped_column(JSON, default=dict)
|
||||
created_at: Mapped[datetime] = mapped_column(default=datetime.utcnow)
|
||||
updated_at: Mapped[datetime] = mapped_column(
|
||||
default=datetime.utcnow, onupdate=datetime.utcnow
|
||||
)
|
||||
|
||||
# Relationships
|
||||
users: Mapped[list["User"]] = relationship(back_populates="tenant")
|
||||
api_keys: Mapped[list["ApiKey"]] = relationship(back_populates="tenant")
|
||||
invoices: Mapped[list["Invoice"]] = relationship(back_populates="tenant")
|
||||
audit_logs: Mapped[list["AuditLog"]] = relationship(back_populates="tenant")
|
||||
reports: Mapped[list["Report"]] = relationship(back_populates="tenant")
|
||||
installed_plugins: Mapped[list["TenantPlugin"]] = relationship(
|
||||
back_populates="tenant"
|
||||
)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<Tenant {self.slug}>"
|
||||
@@ -0,0 +1,105 @@
|
||||
"""User model with RBAC support."""
|
||||
from datetime import datetime
|
||||
from enum import Enum
|
||||
from uuid import UUID, uuid4
|
||||
|
||||
from sqlalchemy import String, ForeignKey, Integer, DateTime, Boolean, Table, Column
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
from app.database import Base
|
||||
|
||||
|
||||
class UserStatus(str, Enum):
|
||||
ACTIVE = "active"
|
||||
INACTIVE = "inactive"
|
||||
PENDING = "pending"
|
||||
LOCKED = "locked"
|
||||
|
||||
|
||||
# Association table for user roles
|
||||
user_roles = Table(
|
||||
"user_roles",
|
||||
Base.metadata,
|
||||
Column("user_id", ForeignKey("users.id", ondelete="CASCADE"), primary_key=True),
|
||||
Column("role_id", ForeignKey("roles.id", ondelete="CASCADE"), primary_key=True),
|
||||
Column("assigned_by", ForeignKey("users.id")),
|
||||
Column("assigned_at", DateTime, default=datetime.utcnow),
|
||||
)
|
||||
|
||||
|
||||
class User(Base):
|
||||
__tablename__ = "users"
|
||||
|
||||
id: Mapped[UUID] = mapped_column(primary_key=True, default=uuid4)
|
||||
tenant_id: Mapped[UUID] = mapped_column(
|
||||
ForeignKey("tenants.id", ondelete="CASCADE"), nullable=False
|
||||
)
|
||||
email: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
password_hash: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
first_name: Mapped[str | None] = mapped_column(String(100))
|
||||
last_name: Mapped[str | None] = mapped_column(String(100))
|
||||
phone: Mapped[str | None] = mapped_column(String(50))
|
||||
avatar_url: Mapped[str | None] = mapped_column(String(500))
|
||||
status: Mapped[UserStatus] = mapped_column(default=UserStatus.ACTIVE)
|
||||
last_login_at: Mapped[datetime | None]
|
||||
failed_login_attempts: Mapped[int] = mapped_column(Integer, default=0)
|
||||
locked_until: Mapped[datetime | None]
|
||||
mfa_enabled: Mapped[bool] = mapped_column(Boolean, default=False)
|
||||
mfa_secret: Mapped[str | None] = mapped_column(String(255))
|
||||
created_at: Mapped[datetime] = mapped_column(default=datetime.utcnow)
|
||||
updated_at: Mapped[datetime] = mapped_column(
|
||||
default=datetime.utcnow, onupdate=datetime.utcnow
|
||||
)
|
||||
|
||||
# Relationships
|
||||
tenant: Mapped["Tenant"] = relationship(back_populates="users")
|
||||
roles: Mapped[list["Role"]] = relationship(
|
||||
secondary=user_roles, back_populates="users"
|
||||
)
|
||||
audit_logs: Mapped[list["AuditLog"]] = relationship(back_populates="user")
|
||||
api_keys_created: Mapped[list["ApiKey"]] = relationship(
|
||||
back_populates="created_by"
|
||||
)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<User {self.email}>"
|
||||
|
||||
def has_permission(self, permission: str) -> bool:
|
||||
"""Check if user has a specific permission through any role."""
|
||||
for role in self.roles:
|
||||
if permission in role.permissions or "*" in role.permissions:
|
||||
return True
|
||||
return False
|
||||
|
||||
def has_access_to(self, tenant_id: UUID) -> bool:
|
||||
"""Check if user belongs to the specified tenant."""
|
||||
return self.tenant_id == tenant_id
|
||||
|
||||
|
||||
class Role(Base):
|
||||
__tablename__ = "roles"
|
||||
|
||||
id: Mapped[UUID] = mapped_column(primary_key=True, default=uuid4)
|
||||
tenant_id: Mapped[UUID | None] = mapped_column(
|
||||
ForeignKey("tenants.id", ondelete="CASCADE")
|
||||
)
|
||||
name: Mapped[str] = mapped_column(String(100), nullable=False)
|
||||
slug: Mapped[str] = mapped_column(String(100), nullable=False)
|
||||
description: Mapped[str | None]
|
||||
permissions: Mapped[list[str]] = mapped_column(JSON, default=list)
|
||||
is_system: Mapped[bool] = mapped_column(Boolean, default=False)
|
||||
created_at: Mapped[datetime] = mapped_column(default=datetime.utcnow)
|
||||
|
||||
# Relationships
|
||||
tenant: Mapped["Tenant | None"] = relationship()
|
||||
users: Mapped[list["User"]] = relationship(
|
||||
secondary=user_roles, back_populates="roles"
|
||||
)
|
||||
|
||||
__table_args__ = (
|
||||
# Unique constraint on tenant_id + slug
|
||||
{"sqlite_autoincrement": True}, # Placeholder for unique constraint
|
||||
)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<Role {self.slug}>"
|
||||
Reference in New Issue
Block a user