Files
TimeTrack/models/user.py
2025-07-09 18:21:23 +02:00

199 lines
8.3 KiB
Python

"""
User-related models
"""
from datetime import datetime, timedelta
from werkzeug.security import generate_password_hash, check_password_hash
import secrets
from . import db
from .enums import Role, AccountType, WidgetType, WidgetSize
class User(db.Model):
"""User model with multi-tenancy and role-based access"""
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(80), nullable=False)
email = db.Column(db.String(120), nullable=True)
password_hash = db.Column(db.String(128))
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)
token_expiry = db.Column(db.DateTime, nullable=True)
# New field for blocking users
is_blocked = db.Column(db.Boolean, default=False)
# New fields for role and team
role = db.Column(db.Enum(Role, values_callable=lambda obj: [e.value for e in obj]), default=Role.TEAM_MEMBER)
team_id = db.Column(db.Integer, db.ForeignKey('team.id'), nullable=True)
# Freelancer support
account_type = db.Column(db.Enum(AccountType, values_callable=lambda obj: [e.value for e in obj]), default=AccountType.COMPANY_USER)
business_name = db.Column(db.String(100), nullable=True) # Optional business name for freelancers
# 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
# Avatar field
avatar_url = db.Column(db.String(255), nullable=True) # URL to user's avatar image
# Relationships
time_entries = db.relationship('TimeEntry', backref='user', lazy=True)
work_config = db.relationship('WorkConfig', backref='user', lazy=True, uselist=False)
def set_password(self, password):
self.password_hash = generate_password_hash(password)
def check_password(self, password):
return check_password_hash(self.password_hash, password)
def generate_verification_token(self):
"""Generate a verification token that expires in 24 hours"""
self.verification_token = secrets.token_urlsafe(32)
self.token_expiry = datetime.utcnow() + timedelta(hours=24)
return self.verification_token
def verify_token(self, token):
"""Verify the token and mark user as verified if valid"""
if token == self.verification_token and self.token_expiry > datetime.utcnow():
self.is_verified = True
self.verification_token = None
self.token_expiry = None
return True
return False
def generate_2fa_secret(self):
"""Generate a new 2FA secret"""
import pyotp
self.two_factor_secret = pyotp.random_base32()
return self.two_factor_secret
def get_2fa_uri(self, issuer_name=None):
"""Get the provisioning URI for QR code generation"""
if not self.two_factor_secret:
return None
import pyotp
totp = pyotp.TOTP(self.two_factor_secret)
if issuer_name is None:
issuer_name = "Time Tracker" # Default fallback
return totp.provisioning_uri(
name=self.email,
issuer_name=issuer_name
)
def verify_2fa_token(self, token, allow_setup=False):
"""Verify a 2FA token"""
if not self.two_factor_secret:
return False
# During setup, allow verification even if 2FA isn't enabled yet
if not allow_setup and not self.two_factor_enabled:
return False
import pyotp
totp = pyotp.TOTP(self.two_factor_secret)
return totp.verify(token, valid_window=1) # Allow 1 window tolerance
def get_avatar_url(self, size=40):
"""Get user's avatar URL or generate a default one"""
if self.avatar_url:
return self.avatar_url
# Generate a default avatar using DiceBear Avatars (similar to GitHub's identicons)
# Using initials style for a clean, professional look
import hashlib
# Create a hash from username for consistent colors
hash_input = f"{self.username}_{self.id}".encode('utf-8')
hash_hex = hashlib.md5(hash_input).hexdigest()
# Use DiceBear API for avatar generation
# For initials style, we need to provide the actual initials
initials = self.get_initials()
# Generate avatar URL with initials
# Using a color based on the hash for consistency
bg_colors = ['0ea5e9', '8b5cf6', 'ec4899', 'f59e0b', '10b981', 'ef4444', '3b82f6', '6366f1']
color_index = int(hash_hex[:2], 16) % len(bg_colors)
bg_color = bg_colors[color_index]
avatar_url = f"https://api.dicebear.com/7.x/initials/svg?seed={initials}&size={size}&backgroundColor={bg_color}&fontSize=50"
return avatar_url
def get_initials(self):
"""Get user initials for avatar display"""
parts = self.username.split()
if len(parts) >= 2:
return f"{parts[0][0]}{parts[-1][0]}".upper()
elif self.username:
return self.username[:2].upper()
return "??"
def __repr__(self):
return f'<User {self.username}>'
class UserPreferences(db.Model):
"""User preferences and settings"""
id = db.Column(db.Integer, primary_key=True)
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), unique=True, nullable=False)
# UI preferences
theme = db.Column(db.String(20), default='light')
language = db.Column(db.String(10), default='en')
timezone = db.Column(db.String(50), default='UTC')
date_format = db.Column(db.String(20), default='ISO') # ISO, US, German, etc.
time_format = db.Column(db.String(10), default='24h')
time_format_24h = db.Column(db.Boolean, default=True) # True for 24h, False for 12h
# Time tracking preferences
time_rounding_minutes = db.Column(db.Integer, default=0) # 0, 5, 10, 15, 30, 60
round_to_nearest = db.Column(db.Boolean, default=False) # False=round down, True=round to nearest
timer_reminder_enabled = db.Column(db.Boolean, default=True)
timer_reminder_interval = db.Column(db.Integer, default=60) # Minutes
# Dashboard preferences
dashboard_layout = db.Column(db.JSON, nullable=True) # Store custom dashboard layout
created_at = db.Column(db.DateTime, default=datetime.utcnow)
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
# Relationships
user = db.relationship('User', backref=db.backref('preferences', uselist=False))
class UserDashboard(db.Model):
"""User's dashboard configuration"""
id = db.Column(db.Integer, primary_key=True)
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
name = db.Column(db.String(100), default='My Dashboard')
is_default = db.Column(db.Boolean, default=True)
layout_config = db.Column(db.Text) # JSON string for grid layout configuration
# Dashboard settings
grid_columns = db.Column(db.Integer, default=6) # Number of grid columns
theme = db.Column(db.String(20), default='light') # light, dark, auto
auto_refresh = db.Column(db.Integer, default=300) # Auto-refresh interval in seconds
# Additional configuration (from new model)
layout = db.Column(db.JSON, nullable=True) # Grid layout configuration (alternative format)
is_locked = db.Column(db.Boolean, default=False) # Prevent accidental changes
# Timestamps
created_at = db.Column(db.DateTime, default=datetime.utcnow)
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
# Relationships
user = db.relationship('User', backref=db.backref('dashboards', lazy='dynamic'))
widgets = db.relationship('DashboardWidget', backref='dashboard', lazy=True, cascade='all, delete')