Add company feature.

This commit is contained in:
2025-07-02 12:42:18 +02:00
committed by Jens Luedicke
parent 85847b5d39
commit 8f49958dfa
17 changed files with 1465 additions and 216 deletions

View File

@@ -13,16 +13,49 @@ class Role(enum.Enum):
SUPERVISOR = "Supervisor"
ADMIN = "Administrator" # Keep existing admin role
# Company model for multi-tenancy
class Company(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(100), nullable=False, unique=True)
slug = db.Column(db.String(50), unique=True, nullable=False) # URL-friendly identifier
description = db.Column(db.Text)
created_at = db.Column(db.DateTime, default=datetime.now)
# Company settings
is_active = db.Column(db.Boolean, default=True)
max_users = db.Column(db.Integer, default=100) # Optional user limit
# Relationships
users = db.relationship('User', backref='company', lazy=True)
teams = db.relationship('Team', backref='company', lazy=True)
projects = db.relationship('Project', backref='company', lazy=True)
def __repr__(self):
return f'<Company {self.name}>'
def generate_slug(self):
"""Generate URL-friendly slug from company name"""
import re
slug = re.sub(r'[^\w\s-]', '', self.name.lower())
slug = re.sub(r'[-\s]+', '-', slug)
return slug.strip('-')
# Create Team model
class Team(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(100), nullable=False, unique=True)
name = db.Column(db.String(100), nullable=False)
description = db.Column(db.String(255))
created_at = db.Column(db.DateTime, default=datetime.now)
# Company association for multi-tenancy
company_id = db.Column(db.Integer, db.ForeignKey('company.id'), nullable=False)
# Relationship with users (one team has many users)
users = db.relationship('User', backref='team', lazy=True)
# Unique constraint per company
__table_args__ = (db.UniqueConstraint('company_id', 'name', name='uq_team_name_per_company'),)
def __repr__(self):
return f'<Team {self.name}>'
@@ -30,11 +63,14 @@ class Project(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(100), nullable=False)
description = db.Column(db.Text, nullable=True)
code = db.Column(db.String(20), unique=True, nullable=False) # Project code (e.g., PRJ001)
code = db.Column(db.String(20), nullable=False) # Project code (e.g., PRJ001)
is_active = db.Column(db.Boolean, default=True)
created_at = db.Column(db.DateTime, default=datetime.now)
updated_at = db.Column(db.DateTime, default=datetime.now, onupdate=datetime.now)
# Company association for multi-tenancy
company_id = db.Column(db.Integer, db.ForeignKey('company.id'), nullable=False)
# Foreign key to user who created the project (Admin/Supervisor)
created_by_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
@@ -50,6 +86,9 @@ class Project(db.Model):
team = db.relationship('Team', backref='projects')
time_entries = db.relationship('TimeEntry', backref='project', lazy=True)
# Unique constraint per company
__table_args__ = (db.UniqueConstraint('company_id', 'code', name='uq_project_code_per_company'),)
def __repr__(self):
return f'<Project {self.code}: {self.name}>'
@@ -58,7 +97,11 @@ class Project(db.Model):
if not self.is_active:
return False
# Admins and Supervisors can log time to any project
# Must be in same company
if self.company_id != user.company_id:
return False
# Admins and Supervisors can log time to any project in their company
if user.role in [Role.ADMIN, Role.SUPERVISOR]:
return True
@@ -66,18 +109,20 @@ class Project(db.Model):
if self.team_id:
return user.team_id == self.team_id
# If no team restriction, any user can log time
# If no team restriction, any user in the company can log time
return True
# Update User model to include role and team relationship
class User(db.Model):
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(80), unique=True, nullable=False)
email = db.Column(db.String(120), unique=True, nullable=False)
username = db.Column(db.String(80), nullable=False)
email = db.Column(db.String(120), nullable=False)
password_hash = db.Column(db.String(128))
is_admin = db.Column(db.Boolean, default=False)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
# Company association for multi-tenancy
company_id = db.Column(db.Integer, db.ForeignKey('company.id'), nullable=False)
# Email verification fields
is_verified = db.Column(db.Boolean, default=False)
verification_token = db.Column(db.String(100), unique=True, nullable=True)
@@ -90,6 +135,12 @@ class User(db.Model):
role = db.Column(db.Enum(Role), default=Role.TEAM_MEMBER)
team_id = db.Column(db.Integer, db.ForeignKey('team.id'), nullable=True)
# Unique constraints per company
__table_args__ = (
db.UniqueConstraint('company_id', 'username', name='uq_user_username_per_company'),
db.UniqueConstraint('company_id', 'email', name='uq_user_email_per_company'),
)
# Two-Factor Authentication fields
two_factor_enabled = db.Column(db.Boolean, default=False)
two_factor_secret = db.Column(db.String(32), nullable=True) # Base32 encoded secret