initiales bauen mit mistral
This commit is contained in:
1 parent
7e2e8bdf59
commit
c74bca1e13
22 files changed
+5642
No files matched your search
@@ -0,0 +1,642 @@
|
||||
# ==============================================================================
|
||||
# Formeleon - Multi-tenant Form Generator
|
||||
# Author: Björn Runden <br@runden.net>
|
||||
# Module: models.py
|
||||
# Description: SQLAlchemy database models for multi-tenant form management.
|
||||
# Includes Tenant, FormConfig, Submission, and User models.
|
||||
# ==============================================================================
|
||||
|
||||
"""
|
||||
Database models for Formeleon.
|
||||
|
||||
This module defines the core data models using Flask-SQLAlchemy:
|
||||
- Tenant: Mandant/Client information with public keys and allowed origins
|
||||
- FormConfig: Dynamic form configurations with JSON structure
|
||||
- Submission: Form submissions with JSON payload data
|
||||
- User: Admin users for the management interface
|
||||
- AuditLog: Tracking of administrative actions
|
||||
|
||||
All models support Flask-Migrate for database migrations.
|
||||
"""
|
||||
|
||||
import json
|
||||
from datetime import datetime, timezone
|
||||
from typing import Optional, Any
|
||||
from enum import Enum
|
||||
|
||||
from flask_sqlalchemy import SQLAlchemy
|
||||
from werkzeug.security import generate_password_hash, check_password_hash
|
||||
|
||||
# Initialize SQLAlchemy without binding to a Flask app yet
|
||||
# This will be bound in app.py after configuration is loaded
|
||||
db = SQLAlchemy()
|
||||
|
||||
|
||||
class UserRole(Enum):
|
||||
"""
|
||||
Enumeration of user roles for access control.
|
||||
|
||||
Roles:
|
||||
ADMIN: Full access to all tenants and configurations
|
||||
TENANT_ADMIN: Admin access limited to specific tenant(s)
|
||||
USER: Standard user with form creation/view permissions
|
||||
VIEWER: Read-only access to forms and submissions
|
||||
"""
|
||||
ADMIN = 'admin'
|
||||
TENANT_ADMIN = 'tenant_admin'
|
||||
USER = 'user'
|
||||
VIEWER = 'viewer'
|
||||
|
||||
|
||||
class SubmissionStatus(Enum):
|
||||
"""
|
||||
Enumeration of submission processing statuses.
|
||||
|
||||
Statuses:
|
||||
PENDING: Submission received but not yet processed
|
||||
PROCESSED: Submission has been processed successfully
|
||||
FAILED: Submission processing failed
|
||||
SPAM: Submission flagged as spam (reCAPTCHA failed)
|
||||
"""
|
||||
PENDING = 'pending'
|
||||
PROCESSED = 'processed'
|
||||
FAILED = 'failed'
|
||||
SPAM = 'spam'
|
||||
|
||||
|
||||
class TenantStatus(Enum):
|
||||
"""
|
||||
Enumeration of tenant statuses.
|
||||
|
||||
Statuses:
|
||||
ACTIVE: Tenant is active and can receive submissions
|
||||
SUSPENDED: Tenant is temporarily suspended
|
||||
INACTIVE: Tenant is inactive/disabled
|
||||
"""
|
||||
ACTIVE = 'active'
|
||||
SUSPENDED = 'suspended'
|
||||
INACTIVE = 'inactive'
|
||||
|
||||
|
||||
# Association table for many-to-many relationship between User and Tenant
|
||||
user_tenant_association = db.Table(
|
||||
'user_tenant_association',
|
||||
db.Column('user_id', db.String(64), db.ForeignKey('user.id'), primary_key=True),
|
||||
db.Column('tenant_id', db.String(64), db.ForeignKey('tenant.id'), primary_key=True),
|
||||
db.Column('created_at', db.DateTime, default=lambda: datetime.now(timezone.utc), nullable=False)
|
||||
)
|
||||
|
||||
|
||||
class Tenant(db.Model):
|
||||
"""
|
||||
Tenant model representing a client/mandant in the multi-tenant system.
|
||||
|
||||
Each tenant has:
|
||||
- Unique identifier and name
|
||||
- Public API key for form submissions
|
||||
- Allowed origins for CORS/handshake validation
|
||||
- Status and configuration settings
|
||||
|
||||
Attributes:
|
||||
id: Unique tenant identifier
|
||||
name: Display name of the tenant
|
||||
public_key: API key used by client snippets for authentication
|
||||
allowed_origins: Comma-separated list of allowed origin domains
|
||||
status: Current status of the tenant
|
||||
config: Additional JSON configuration
|
||||
created_at: Timestamp when tenant was created
|
||||
updated_at: Timestamp when tenant was last updated
|
||||
"""
|
||||
|
||||
__tablename__ = 'tenant'
|
||||
|
||||
id = db.Column(db.String(64), primary_key=True, unique=True, nullable=False)
|
||||
name = db.Column(db.String(255), nullable=False)
|
||||
public_key = db.Column(db.String(128), unique=True, nullable=False)
|
||||
allowed_origins = db.Column(db.Text, nullable=False, default='*')
|
||||
status = db.Column(db.Enum(TenantStatus), nullable=False, default=TenantStatus.ACTIVE)
|
||||
config = db.Column(db.JSON, nullable=True)
|
||||
created_at = db.Column(db.DateTime, default=lambda: datetime.now(timezone.utc), nullable=False)
|
||||
updated_at = db.Column(
|
||||
db.DateTime,
|
||||
default=lambda: datetime.now(timezone.utc),
|
||||
onupdate=lambda: datetime.now(timezone.utc),
|
||||
nullable=False
|
||||
)
|
||||
|
||||
# Relationships
|
||||
forms = db.relationship('FormConfig', backref='tenant', lazy=True, cascade='all, delete-orphan')
|
||||
submissions = db.relationship('Submission', backref='tenant', lazy=True)
|
||||
users = db.relationship('User', secondary=user_tenant_association, backref='tenants', lazy=True)
|
||||
|
||||
def __init__(self, id: str, name: str, public_key: str, allowed_origins: str = '*',
|
||||
status: TenantStatus = TenantStatus.ACTIVE, config: Optional[dict] = None):
|
||||
"""
|
||||
Initialize a new Tenant instance.
|
||||
|
||||
:param id: Unique tenant identifier
|
||||
:param name: Display name of the tenant
|
||||
:param public_key: API key for client authentication
|
||||
:param allowed_origins: Comma-separated allowed origins
|
||||
:param status: Initial tenant status
|
||||
:param config: Additional configuration as JSON
|
||||
"""
|
||||
self.id = id
|
||||
self.name = name
|
||||
self.public_key = public_key
|
||||
self.allowed_origins = allowed_origins
|
||||
self.status = status
|
||||
self.config = config or {}
|
||||
|
||||
def get_allowed_origins_list(self) -> list:
|
||||
"""
|
||||
Get allowed origins as a list.
|
||||
|
||||
:return: List of allowed origin strings
|
||||
"""
|
||||
return [origin.strip() for origin in self.allowed_origins.split(',') if origin.strip()]
|
||||
|
||||
def is_origin_allowed(self, origin: str) -> bool:
|
||||
"""
|
||||
Check if a given origin is allowed for this tenant.
|
||||
|
||||
:param origin: Origin to check
|
||||
:return: True if origin is allowed, False otherwise
|
||||
"""
|
||||
allowed = self.get_allowed_origins_list()
|
||||
if '*' in allowed:
|
||||
return True
|
||||
return origin in allowed
|
||||
|
||||
def is_active(self) -> bool:
|
||||
"""
|
||||
Check if tenant is active.
|
||||
|
||||
:return: True if tenant status is ACTIVE, False otherwise
|
||||
"""
|
||||
return self.status == TenantStatus.ACTIVE
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
"""
|
||||
Convert tenant to dictionary for API responses.
|
||||
|
||||
:return: Dictionary representation of tenant
|
||||
"""
|
||||
return {
|
||||
'id': self.id,
|
||||
'name': self.name,
|
||||
'public_key': self.public_key,
|
||||
'allowed_origins': self.allowed_origins,
|
||||
'status': self.status.value,
|
||||
'config': self.config or {},
|
||||
'created_at': self.created_at.isoformat() if self.created_at else None,
|
||||
'updated_at': self.updated_at.isoformat() if self.updated_at else None
|
||||
}
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f'<Tenant {self.id} - {self.name}>'
|
||||
|
||||
|
||||
class FormConfig(db.Model):
|
||||
"""
|
||||
Form configuration model storing dynamic form definitions.
|
||||
|
||||
Each form configuration has:
|
||||
- Unique identifier
|
||||
- Reference to tenant
|
||||
- Name and description
|
||||
- JSON structure defining form fields and validation
|
||||
- Status and settings
|
||||
|
||||
Attributes:
|
||||
id: Unique form identifier
|
||||
tenant_id: Foreign key to tenant
|
||||
name: Display name of the form
|
||||
description: Optional description
|
||||
form_structure: JSON defining form fields, types, and validation
|
||||
settings: Additional form settings (email notifications, etc.)
|
||||
is_active: Whether the form is active and accepting submissions
|
||||
created_at: Timestamp when form was created
|
||||
updated_at: Timestamp when form was last updated
|
||||
"""
|
||||
|
||||
__tablename__ = 'form_config'
|
||||
|
||||
id = db.Column(db.String(64), primary_key=True, unique=True, nullable=False)
|
||||
tenant_id = db.Column(db.String(64), db.ForeignKey('tenant.id'), nullable=False)
|
||||
name = db.Column(db.String(255), nullable=False)
|
||||
description = db.Column(db.Text, nullable=True)
|
||||
form_structure = db.Column(db.JSON, nullable=False)
|
||||
settings = db.Column(db.JSON, nullable=True)
|
||||
is_active = db.Column(db.Boolean, default=True, nullable=False)
|
||||
created_at = db.Column(db.DateTime, default=lambda: datetime.now(timezone.utc), nullable=False)
|
||||
updated_at = db.Column(
|
||||
db.DateTime,
|
||||
default=lambda: datetime.now(timezone.utc),
|
||||
onupdate=lambda: datetime.now(timezone.utc),
|
||||
nullable=False
|
||||
)
|
||||
|
||||
# Relationships
|
||||
submissions = db.relationship('Submission', backref='form_config', lazy=True, cascade='all, delete-orphan')
|
||||
|
||||
def __init__(self, id: str, tenant_id: str, name: str, form_structure: dict,
|
||||
description: Optional[str] = None, settings: Optional[dict] = None,
|
||||
is_active: bool = True):
|
||||
"""
|
||||
Initialize a new FormConfig instance.
|
||||
|
||||
:param id: Unique form identifier
|
||||
:param tenant_id: Tenant this form belongs to
|
||||
:param name: Display name of the form
|
||||
:param form_structure: JSON defining form fields and structure
|
||||
:param description: Optional description
|
||||
:param settings: Additional form settings
|
||||
:param is_active: Whether form is active
|
||||
"""
|
||||
self.id = id
|
||||
self.tenant_id = tenant_id
|
||||
self.name = name
|
||||
self.description = description
|
||||
self.form_structure = form_structure
|
||||
self.settings = settings or {}
|
||||
self.is_active = is_active
|
||||
|
||||
def get_field_definitions(self) -> list:
|
||||
"""
|
||||
Get the list of field definitions from form structure.
|
||||
|
||||
:return: List of field definition dictionaries
|
||||
"""
|
||||
return self.form_structure.get('fields', [])
|
||||
|
||||
def get_field_names(self) -> list:
|
||||
"""
|
||||
Get list of field names from form structure.
|
||||
|
||||
:return: List of field name strings
|
||||
"""
|
||||
return [field.get('name') for field in self.get_field_definitions() if field.get('name')]
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
"""
|
||||
Convert form config to dictionary for API responses.
|
||||
|
||||
:return: Dictionary representation of form config
|
||||
"""
|
||||
return {
|
||||
'id': self.id,
|
||||
'tenant_id': self.tenant_id,
|
||||
'name': self.name,
|
||||
'description': self.description,
|
||||
'form_structure': self.form_structure,
|
||||
'settings': self.settings or {},
|
||||
'is_active': self.is_active,
|
||||
'created_at': self.created_at.isoformat() if self.created_at else None,
|
||||
'updated_at': self.updated_at.isoformat() if self.updated_at else None
|
||||
}
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f'<FormConfig {self.id} - {self.name}>'
|
||||
|
||||
|
||||
class Submission(db.Model):
|
||||
"""
|
||||
Submission model storing form submission data.
|
||||
|
||||
Each submission has:
|
||||
- Unique identifier
|
||||
- Reference to form configuration
|
||||
- JSON payload with submitted data
|
||||
- IP hash for privacy-preserving identification
|
||||
- Status and submission_metadata
|
||||
|
||||
Attributes:
|
||||
id: Unique submission identifier
|
||||
form_config_id: Foreign key to form configuration
|
||||
tenant_id: Foreign key to tenant (denormalized for easier queries)
|
||||
payload: JSON containing submitted form data
|
||||
ip_hash: Hash of submitter's IP address for rate limiting/duplicate detection
|
||||
user_agent: User agent string of submitter
|
||||
referer: Referer header from submission request
|
||||
recaptcha_score: reCAPTCHA score if validation was performed
|
||||
recaptcha_passed: Whether reCAPTCHA validation passed
|
||||
status: Current processing status
|
||||
submission_metadata: Additional metadata (headers, etc.)
|
||||
created_at: Timestamp when submission was created
|
||||
"""
|
||||
|
||||
__tablename__ = 'submission'
|
||||
|
||||
id = db.Column(db.String(64), primary_key=True, unique=True, nullable=False)
|
||||
form_config_id = db.Column(db.String(64), db.ForeignKey('form_config.id'), nullable=False)
|
||||
tenant_id = db.Column(db.String(64), db.ForeignKey('tenant.id'), nullable=False)
|
||||
payload = db.Column(db.JSON, nullable=False)
|
||||
ip_hash = db.Column(db.String(64), nullable=True)
|
||||
user_agent = db.Column(db.Text, nullable=True)
|
||||
referer = db.Column(db.Text, nullable=True)
|
||||
recaptcha_score = db.Column(db.Float, nullable=True)
|
||||
recaptcha_passed = db.Column(db.Boolean, nullable=True)
|
||||
status = db.Column(db.Enum(SubmissionStatus), nullable=False, default=SubmissionStatus.PENDING)
|
||||
submission_metadata = db.Column(db.JSON, nullable=True)
|
||||
created_at = db.Column(db.DateTime, default=lambda: datetime.now(timezone.utc), nullable=False)
|
||||
|
||||
def __init__(self, id: str, form_config_id: str, tenant_id: str, payload: dict,
|
||||
ip_hash: Optional[str] = None, user_agent: Optional[str] = None,
|
||||
referer: Optional[str] = None, recaptcha_score: Optional[float] = None,
|
||||
recaptcha_passed: Optional[bool] = None, submission_metadata: Optional[dict] = None,
|
||||
status: SubmissionStatus = SubmissionStatus.PENDING):
|
||||
"""
|
||||
Initialize a new Submission instance.
|
||||
|
||||
:param id: Unique submission identifier
|
||||
:param form_config_id: Form configuration this submission belongs to
|
||||
:param tenant_id: Tenant this submission belongs to
|
||||
:param payload: JSON containing submitted form data
|
||||
:param ip_hash: Hash of submitter's IP
|
||||
:param user_agent: User agent string
|
||||
:param referer: Referer header
|
||||
:param recaptcha_score: reCAPTCHA score
|
||||
:param recaptcha_passed: Whether reCAPTCHA passed
|
||||
:param submission_metadata: Additional submission_metadata
|
||||
:param status: Initial submission status
|
||||
"""
|
||||
self.id = id
|
||||
self.form_config_id = form_config_id
|
||||
self.tenant_id = tenant_id
|
||||
self.payload = payload
|
||||
self.ip_hash = ip_hash
|
||||
self.user_agent = user_agent
|
||||
self.referer = referer
|
||||
self.recaptcha_score = recaptcha_score
|
||||
self.recaptcha_passed = recaptcha_passed
|
||||
self.submission_metadata = submission_metadata or {}
|
||||
self.status = status
|
||||
|
||||
def get_field_value(self, field_name: str) -> Any:
|
||||
"""
|
||||
Get the value of a specific field from the payload.
|
||||
|
||||
:param field_name: Name of the field to retrieve
|
||||
:return: Value of the field or None if not found
|
||||
"""
|
||||
return self.payload.get(field_name)
|
||||
|
||||
def mark_as_processed(self) -> None:
|
||||
"""
|
||||
Mark submission as processed.
|
||||
"""
|
||||
self.status = SubmissionStatus.PROCESSED
|
||||
db.session.commit()
|
||||
|
||||
def mark_as_spam(self) -> None:
|
||||
"""
|
||||
Mark submission as spam.
|
||||
"""
|
||||
self.status = SubmissionStatus.SPAM
|
||||
db.session.commit()
|
||||
|
||||
def mark_as_failed(self) -> None:
|
||||
"""
|
||||
Mark submission as failed.
|
||||
"""
|
||||
self.status = SubmissionStatus.FAILED
|
||||
db.session.commit()
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
"""
|
||||
Convert submission to dictionary for API responses.
|
||||
|
||||
:return: Dictionary representation of submission
|
||||
"""
|
||||
return {
|
||||
'id': self.id,
|
||||
'form_config_id': self.form_config_id,
|
||||
'tenant_id': self.tenant_id,
|
||||
'payload': self.payload,
|
||||
'ip_hash': self.ip_hash,
|
||||
'user_agent': self.user_agent,
|
||||
'referer': self.referer,
|
||||
'recaptcha_score': self.recaptcha_score,
|
||||
'recaptcha_passed': self.recaptcha_passed,
|
||||
'status': self.status.value,
|
||||
'submission_metadata': self.submission_metadata or {},
|
||||
'created_at': self.created_at.isoformat() if self.created_at else None
|
||||
}
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f'<Submission {self.id} - {self.form_config_id}>'
|
||||
|
||||
|
||||
class User(db.Model):
|
||||
"""
|
||||
User model for authentication and authorization.
|
||||
|
||||
Users authenticate via Zitadel OIDC in production, with a dev mode bypass.
|
||||
Each user has:
|
||||
- Unique identifier (from OIDC or dev mode)
|
||||
- Email and name
|
||||
- Role for access control
|
||||
- Session management fields
|
||||
|
||||
Attributes:
|
||||
id: Unique user identifier
|
||||
email: User email address
|
||||
name: Display name
|
||||
role: User role for access control
|
||||
sub: OIDC subject identifier (from Zitadel)
|
||||
last_login: Timestamp of last successful login
|
||||
created_at: Timestamp when user was created
|
||||
updated_at: Timestamp when user was last updated
|
||||
"""
|
||||
|
||||
__tablename__ = 'user'
|
||||
|
||||
id = db.Column(db.String(64), primary_key=True, unique=True, nullable=False)
|
||||
email = db.Column(db.String(255), unique=True, nullable=False)
|
||||
name = db.Column(db.String(255), nullable=False)
|
||||
role = db.Column(db.Enum(UserRole), nullable=False, default=UserRole.USER)
|
||||
sub = db.Column(db.String(255), unique=True, nullable=True)
|
||||
last_login = db.Column(db.DateTime, nullable=True)
|
||||
created_at = db.Column(db.DateTime, default=lambda: datetime.now(timezone.utc), nullable=False)
|
||||
updated_at = db.Column(
|
||||
db.DateTime,
|
||||
default=lambda: datetime.now(timezone.utc),
|
||||
onupdate=lambda: datetime.now(timezone.utc),
|
||||
nullable=False
|
||||
)
|
||||
|
||||
def __init__(self, id: str, email: str, name: str, role: UserRole = UserRole.USER,
|
||||
sub: Optional[str] = None):
|
||||
"""
|
||||
Initialize a new User instance.
|
||||
|
||||
:param id: Unique user identifier
|
||||
:param email: User email address
|
||||
:param name: Display name
|
||||
:param role: User role
|
||||
:param sub: OIDC subject identifier
|
||||
"""
|
||||
self.id = id
|
||||
self.email = email
|
||||
self.name = name
|
||||
self.role = role
|
||||
self.sub = sub
|
||||
|
||||
def update_last_login(self) -> None:
|
||||
"""
|
||||
Update the last login timestamp to now.
|
||||
"""
|
||||
self.last_login = datetime.now(timezone.utc)
|
||||
db.session.commit()
|
||||
|
||||
def has_role(self, role: UserRole) -> bool:
|
||||
"""
|
||||
Check if user has a specific role or higher privilege.
|
||||
|
||||
:param role: Role to check
|
||||
:return: True if user has the role or a higher-privilege role
|
||||
"""
|
||||
role_hierarchy = {
|
||||
UserRole.ADMIN: 4,
|
||||
UserRole.TENANT_ADMIN: 3,
|
||||
UserRole.USER: 2,
|
||||
UserRole.VIEWER: 1
|
||||
}
|
||||
return role_hierarchy.get(self.role, 0) >= role_hierarchy.get(role, 0)
|
||||
|
||||
def is_admin(self) -> bool:
|
||||
"""
|
||||
Check if user has admin privileges.
|
||||
|
||||
:return: True if user is ADMIN or TENANT_ADMIN
|
||||
"""
|
||||
return self.role in [UserRole.ADMIN, UserRole.TENANT_ADMIN]
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
"""
|
||||
Convert user to dictionary for API responses.
|
||||
|
||||
:return: Dictionary representation of user
|
||||
"""
|
||||
return {
|
||||
'id': self.id,
|
||||
'email': self.email,
|
||||
'name': self.name,
|
||||
'role': self.role.value,
|
||||
'sub': self.sub,
|
||||
'last_login': self.last_login.isoformat() if self.last_login else None,
|
||||
'created_at': self.created_at.isoformat() if self.created_at else None,
|
||||
'updated_at': self.updated_at.isoformat() if self.updated_at else None
|
||||
}
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f'<User {self.id} - {self.email} ({self.role.value})>'
|
||||
|
||||
|
||||
class AuditLog(db.Model):
|
||||
"""
|
||||
Audit log model for tracking administrative actions.
|
||||
|
||||
Each audit log entry records:
|
||||
- Who performed an action
|
||||
- What action was performed
|
||||
- On which resource
|
||||
- With what result
|
||||
|
||||
Attributes:
|
||||
id: Unique log entry identifier
|
||||
user_id: User who performed the action (nullable for system actions)
|
||||
tenant_id: Tenant context (nullable for global actions)
|
||||
action: Type of action performed
|
||||
resource_type: Type of resource affected
|
||||
resource_id: Identifier of resource affected
|
||||
details: Additional details about the action
|
||||
ip_address: IP address of requester
|
||||
user_agent: User agent string
|
||||
status: Whether action succeeded or failed
|
||||
created_at: Timestamp when action was logged
|
||||
"""
|
||||
|
||||
__tablename__ = 'audit_log'
|
||||
|
||||
id = db.Column(db.String(64), primary_key=True, unique=True, nullable=False)
|
||||
user_id = db.Column(db.String(64), db.ForeignKey('user.id'), nullable=True)
|
||||
tenant_id = db.Column(db.String(64), db.ForeignKey('tenant.id'), nullable=True)
|
||||
action = db.Column(db.String(64), nullable=False)
|
||||
resource_type = db.Column(db.String(64), nullable=False)
|
||||
resource_id = db.Column(db.String(64), nullable=True)
|
||||
details = db.Column(db.JSON, nullable=True)
|
||||
ip_address = db.Column(db.String(64), nullable=True)
|
||||
user_agent = db.Column(db.Text, nullable=True)
|
||||
status = db.Column(db.String(16), nullable=False, default='success')
|
||||
created_at = db.Column(db.DateTime, default=lambda: datetime.now(timezone.utc), nullable=False)
|
||||
|
||||
def __init__(self, id: str, action: str, resource_type: str,
|
||||
user_id: Optional[str] = None, tenant_id: Optional[str] = None,
|
||||
resource_id: Optional[str] = None, details: Optional[dict] = None,
|
||||
ip_address: Optional[str] = None, user_agent: Optional[str] = None,
|
||||
status: str = 'success'):
|
||||
"""
|
||||
Initialize a new AuditLog instance.
|
||||
|
||||
:param id: Unique log identifier
|
||||
:param action: Type of action performed
|
||||
:param resource_type: Type of resource affected
|
||||
:param user_id: User who performed the action
|
||||
:param tenant_id: Tenant context
|
||||
:param resource_id: Resource identifier
|
||||
:param details: Additional details
|
||||
:param ip_address: Requester IP address
|
||||
:param user_agent: Requester user agent
|
||||
:param status: Action status (success/failure)
|
||||
"""
|
||||
self.id = id
|
||||
self.user_id = user_id
|
||||
self.tenant_id = tenant_id
|
||||
self.action = action
|
||||
self.resource_type = resource_type
|
||||
self.resource_id = resource_id
|
||||
self.details = details or {}
|
||||
self.ip_address = ip_address
|
||||
self.user_agent = user_agent
|
||||
self.status = status
|
||||
|
||||
@classmethod
|
||||
def log_action(cls, action: str, resource_type: str, user: Optional[User] = None,
|
||||
tenant: Optional[Tenant] = None, resource_id: Optional[str] = None,
|
||||
details: Optional[dict] = None, request_context: Optional[dict] = None,
|
||||
status: str = 'success') -> 'AuditLog':
|
||||
"""
|
||||
Convenience method to create an audit log entry.
|
||||
|
||||
:param action: Type of action
|
||||
:param resource_type: Type of resource
|
||||
:param user: User performing the action
|
||||
:param tenant: Tenant context
|
||||
:param resource_id: Resource identifier
|
||||
:param details: Additional details
|
||||
:param request_context: Request context (ip_address, user_agent)
|
||||
:param status: Action status
|
||||
:return: Created audit log entry
|
||||
"""
|
||||
import uuid
|
||||
log_entry = cls(
|
||||
id=str(uuid.uuid4()),
|
||||
action=action,
|
||||
resource_type=resource_type,
|
||||
user_id=user.id if user else None,
|
||||
tenant_id=tenant.id if tenant else None,
|
||||
resource_id=resource_id,
|
||||
details=details,
|
||||
ip_address=request_context.get('ip_address') if request_context else None,
|
||||
user_agent=request_context.get('user_agent') if request_context else None,
|
||||
status=status
|
||||
)
|
||||
db.session.add(log_entry)
|
||||
db.session.commit()
|
||||
return log_entry
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f'<AuditLog {self.id} - {self.action} on {self.resource_type}:{self.resource_id}>'
|
||||
|
||||
Reference in new issue
Block a user