Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
from poseidon.backend.database.models.user import User
from poseidon.backend.database.models.organization import Organization
from poseidon.backend.database.models.project import Project
from poseidon.backend.database.models.project_assignment import ProjectAssignment
from poseidon.backend.database.models.project_license import ProjectLicense
from poseidon.backend.database.models.image import Image
from poseidon.backend.database.models.camera import Camera
from poseidon.backend.database.models.model import Model
Expand All @@ -26,6 +28,8 @@ def rebuild_all_models():
organization_module = sys.modules['poseidon.backend.database.models.organization']
project_module = sys.modules['poseidon.backend.database.models.project']
user_module = sys.modules['poseidon.backend.database.models.user']
project_assignment_module = sys.modules['poseidon.backend.database.models.project_assignment']
project_license_module = sys.modules['poseidon.backend.database.models.project_license']
image_module = sys.modules['poseidon.backend.database.models.image']
camera_module = sys.modules['poseidon.backend.database.models.camera']
model_module = sys.modules['poseidon.backend.database.models.model']
Expand All @@ -39,6 +43,8 @@ def rebuild_all_models():
'Organization': Organization,
'Project': Project,
'User': User,
'ProjectAssignment': ProjectAssignment,
'ProjectLicense': ProjectLicense,
'Image': Image,
'Camera': Camera,
'Model': Model,
Expand All @@ -48,7 +54,7 @@ def rebuild_all_models():
'ScanClassification': ScanClassification,
}

for module in [organization_module, project_module, user_module, camera_module, model_module, model_deployment_module, scan_module, scan_image_module, scan_classification_module, image_module]:
for module in [organization_module, project_module, user_module, project_assignment_module, project_license_module, camera_module, model_module, model_deployment_module, scan_module, scan_image_module, scan_classification_module, image_module]:
for name, model_class in models_dict.items():
setattr(module, name, model_class)

Expand All @@ -61,11 +67,18 @@ def rebuild_all_models():

User.model_rebuild()

# 3. Image depends on Organization, Project, and User
# 3. ProjectAssignment and ProjectLicense depend on User and Project
ProjectAssignment.model_rebuild()

ProjectLicense.model_rebuild()
print("✓ ProjectLicense model rebuilt")

# 4. Image depends on Organization, Project, and User
Image.model_rebuild()

# 4. Other models depend on Organization, Project, and User
# 4. Camera depends on Organization, Project, and User
Camera.model_rebuild()
print("✓ Camera model rebuilt")

Model.model_rebuild()

Expand Down Expand Up @@ -101,6 +114,8 @@ async def initialize_database():
Organization, # Put Organization first so it's defined before other models
Project, # Put Project before models that reference it
User, # Put User before models that reference it
ProjectAssignment, # Put ProjectAssignment after User and Project
ProjectLicense, # Put ProjectLicense after User, Project, and Organization
Image, # Put Image after Organization, Project, and User
Camera,
Model,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
from .user import User
from .organization import Organization
from .project import Project
from .project_assignment import ProjectAssignment
from .image import Image
from .camera import Camera
from .model import Model
from .model_deployment import ModelDeployment
from .enums import (
SubscriptionPlan,
OrgRole,
OrgRole,
ProjectRole,
ProjectStatus,
ProjectType,
ModelValidationStatus,
Expand All @@ -25,6 +27,7 @@
"User",
"Organization",
"Project",
"ProjectAssignment",
"Image",
"Camera",
"Model",
Expand All @@ -34,6 +37,7 @@
"ScanClassification",
"SubscriptionPlan",
"OrgRole",
"ProjectRole",
"ProjectStatus",
"ProjectType",
"ModelValidationStatus",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,30 @@ class OrgRole(str, Enum):
USER = "user"
ADMIN = "admin"
SUPER_ADMIN = "super_admin"

@classmethod
def is_admin(cls, role) -> bool:
"""Check if role is admin (excludes super admin)"""
return role == cls.ADMIN

@classmethod
def is_super_admin(cls, role) -> bool:
"""Check if role is super admin"""
return role == cls.SUPER_ADMIN

@classmethod
def is_admin_or_higher(cls, role) -> bool:
"""Check if role is admin or super admin"""
return role in (cls.ADMIN, cls.SUPER_ADMIN)

@classmethod
def get_manageable_roles(cls) -> List[str]:
"""Get roles that can be managed by admins"""
return [cls.USER, cls.ADMIN]

class ProjectRole(str, Enum):
INSPECTOR = "inspector"
VIEWER = "viewer"

class ProjectStatus(str, Enum):
ACTIVE = "active"
Expand Down Expand Up @@ -67,4 +91,22 @@ class ScanImageStatus(str, Enum):
UPLOADED = "uploaded"
PROCESSING = "processing"
PROCESSED = "processed"
FAILED = "failed"
FAILED = "failed"

class LicenseStatus(str, Enum):
ACTIVE = "active"
EXPIRED = "expired"
CANCELLED = "cancelled"

@classmethod
def is_valid(cls, status) -> bool:
"""Check if license status allows full functionality"""
return status == cls.ACTIVE

@classmethod
def get_display_names(cls) -> Dict[str, str]:
return {
cls.ACTIVE: "Active",
cls.EXPIRED: "Expired",
cls.CANCELLED: "Cancelled"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
from mindtrace.database import MindtraceDocument
from typing import TYPE_CHECKING
from datetime import datetime, UTC
from .enums import ProjectRole
from beanie import Link, before_event, Insert, Replace, SaveChanges
from pydantic import Field

if TYPE_CHECKING:
from .user import User
from .project import Project

class ProjectAssignment(MindtraceDocument):
user: Link["User"]
project: Link["Project"]
role: ProjectRole = ProjectRole.VIEWER
assigned_at: datetime = Field(default_factory=lambda: datetime.now(UTC))
assigned_by: Link["User"] = None # Who assigned this user

@before_event(Insert)
async def set_timestamps(self):
self.assigned_at = datetime.now(UTC)

class Settings:
name = "project_assignments"
indexes = [
[("user", 1), ("project", 1)], # Unique constraint
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
"""Project License Model

Defines the ProjectLicense model for the simple binary license system.
Each project has one license that gates all functionality.
"""

from datetime import datetime, UTC
from typing import TYPE_CHECKING

from beanie import Document, Link
from pydantic import Field
from mindtrace.database import MindtraceDocument

from .enums import LicenseStatus

if TYPE_CHECKING:
from .user import User
from .project import Project
from .organization import Organization


class ProjectLicense(MindtraceDocument):
"""Project license for binary access control.

Simple licensing model where having a valid license grants full access
to all project features, while invalid/expired licenses restrict access.
"""

# Core license data
project: Link["Project"] = Field(description="Project this license applies to")
license_key: str = Field(description="Unique license identifier", index=True)
status: LicenseStatus = Field(default=LicenseStatus.ACTIVE, description="Current license status")

# Timestamps
issued_at: datetime = Field(default_factory=lambda: datetime.now(UTC), description="When license was issued")
expires_at: datetime = Field(description="License expiration date")

# Management tracking
issued_by: Link["User"] = Field(description="Super admin who issued the license")
organization: Link["Organization"] = Field(description="Organization for tracking and validation")

# Optional metadata
notes: str = Field(default="", description="Optional notes about the license")

class Settings:
name = "project_licenses"
indexes = [
"license_key",
"project",
"organization",
"status",
"expires_at"
]

@property
def is_valid(self) -> bool:
"""Check if license is currently valid for full functionality"""
if self.status != LicenseStatus.ACTIVE:
return False

# Check expiration
if self.expires_at:
# Ensure both datetimes are timezone-aware for comparison
now = datetime.now(UTC)
expires = self.expires_at
if expires.tzinfo is None:
expires = expires.replace(tzinfo=UTC)
if now > expires:
return False

return True

@property
def is_expired(self) -> bool:
"""Check if license has expired"""
if not self.expires_at:
return False

# Ensure both datetimes are timezone-aware for comparison
now = datetime.now(UTC)
expires = self.expires_at
if expires.tzinfo is None:
expires = expires.replace(tzinfo=UTC)
return now > expires

@property
def days_until_expiry(self) -> int:
"""Get days until license expires (negative if already expired)"""
if not self.expires_at:
return 999999 # No expiration

# Ensure both datetimes are timezone-aware for comparison
now = datetime.now(UTC)
expires = self.expires_at
if expires.tzinfo is None:
expires = expires.replace(tzinfo=UTC)
delta = expires - now
return delta.days

@property
def status_display(self) -> str:
"""Get human-readable status display"""
if self.is_expired and self.status == LicenseStatus.ACTIVE:
return "Expired"
return LicenseStatus.get_display_names().get(self.status, self.status)
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
from mindtrace.database import MindtraceDocument
from typing import List, TYPE_CHECKING
from typing import List, TYPE_CHECKING, Optional
from datetime import datetime, UTC
from .enums import OrgRole
from .enums import OrgRole, ProjectRole
from beanie import Link, before_event, Insert, Replace, SaveChanges, after_event, Delete
from pydantic import Field

if TYPE_CHECKING:
from .organization import Organization
from .project import Project
from .project_assignment import ProjectAssignment

class User(MindtraceDocument):
username: str
Expand All @@ -18,8 +19,11 @@ class User(MindtraceDocument):
# Single organization role
org_role: OrgRole = OrgRole.USER

# Reference to projects
# Reference to projects (kept for backward compatibility, will be migrated)
projects: List[Link["Project"]] = Field(default_factory=list)

# Project assignments with roles
project_assignments: List[Link["ProjectAssignment"]] = Field(default_factory=list)

is_active: bool = True
created_at: datetime = Field(default_factory=lambda: datetime.now(UTC))
Expand Down Expand Up @@ -59,6 +63,15 @@ def remove_project(self, project: "Project"):
def is_assigned_to_project(self, project: "Project") -> bool:
"""Check if user is assigned to a specific project"""
return any(p.id == project.id for p in self.projects)

async def get_project_role(self, project_id: str) -> Optional[ProjectRole]:
"""Get user's role in a specific project"""
from .project_assignment import ProjectAssignment
assignment = await ProjectAssignment.find_one(
ProjectAssignment.user.id == self.id,
ProjectAssignment.project.id == project_id
)
return assignment.role if assignment else None

@after_event(Insert)
async def increment_org_user_count(self):
Expand Down
Loading