Squashed commit of the following:
commit 1eeea9f83ad9230a5c1f7a75662770eaab0df837 Author: Jens Luedicke <jens@luedicke.me> Date: Mon Jul 7 21:15:41 2025 +0200 Disable resuming of old time entries. commit 3e3ec2f01cb7943622b819a19179388078ae1315 Author: Jens Luedicke <jens@luedicke.me> Date: Mon Jul 7 20:59:19 2025 +0200 Refactor db migrations. commit 15a51a569da36c6b7c9e01ab17b6fdbdee6ad994 Author: Jens Luedicke <jens@luedicke.me> Date: Mon Jul 7 19:58:04 2025 +0200 Apply new style for Time Tracking view. commit 77e5278b303e060d2b03853b06277f8aa567ae68 Author: Jens Luedicke <jens@luedicke.me> Date: Mon Jul 7 18:06:04 2025 +0200 Allow direct registrations as a Company. commit 188a8772757cbef374243d3a5f29e4440ddecabe Author: Jens Luedicke <jens@luedicke.me> Date: Mon Jul 7 18:04:45 2025 +0200 Add email invitation feature. commit d9ebaa02aa01b518960a20dccdd5a327d82f30c6 Author: Jens Luedicke <jens@luedicke.me> Date: Mon Jul 7 17:12:32 2025 +0200 Apply common style for Company, User, Team management pages. commit 81149caf4d8fc6317e2ab1b4f022b32fc5aa6d22 Author: Jens Luedicke <jens@luedicke.me> Date: Mon Jul 7 16:44:32 2025 +0200 Move export functions to own module. commit 1a26e19338e73f8849c671471dd15cc3c1b1fe82 Author: Jens Luedicke <jens@luedicke.me> Date: Mon Jul 7 15:51:15 2025 +0200 Split up models.py. commit 61f1ccd10f721b0ff4dc1eccf30c7a1ee13f204d Author: Jens Luedicke <jens@luedicke.me> Date: Mon Jul 7 12:05:28 2025 +0200 Move utility function into own modules. commit 84b341ed35e2c5387819a8b9f9d41eca900ae79f Author: Jens Luedicke <jens@luedicke.me> Date: Mon Jul 7 11:44:24 2025 +0200 Refactor auth functions use. commit 923e311e3da5b26d85845c2832b73b7b17c48adb Author: Jens Luedicke <jens@luedicke.me> Date: Mon Jul 7 11:35:52 2025 +0200 Refactor route nameing and fix bugs along the way. commit f0a5c4419c340e62a2615c60b2a9de28204d2995 Author: Jens Luedicke <jens@luedicke.me> Date: Mon Jul 7 10:34:33 2025 +0200 Fix URL endpoints in announcement template. commit b74d74542a1c8dc350749e4788a9464d067a88b5 Author: Jens Luedicke <jens@luedicke.me> Date: Mon Jul 7 09:25:53 2025 +0200 Move announcements to own module. commit 9563a28021ac46c82c04fe4649b394dbf96f92c7 Author: Jens Luedicke <jens@luedicke.me> Date: Mon Jul 7 09:16:30 2025 +0200 Combine Company view and edit templates. commit 6687c373e681d54e4deab6b2582fed5cea9aadf6 Author: Jens Luedicke <jens@luedicke.me> Date: Mon Jul 7 08:17:42 2025 +0200 Move Users, Company and System Administration to own modules. commit 8b7894a2e3eb84bb059f546648b6b9536fea724e Author: Jens Luedicke <jens@luedicke.me> Date: Mon Jul 7 07:40:57 2025 +0200 Move Teams and Projects to own modules. commit d11bf059d99839ecf1f5d7020b8c8c8a2454c00b Author: Jens Luedicke <jens@luedicke.me> Date: Mon Jul 7 07:09:33 2025 +0200 Move Tasks and Sprints to own modules.
This commit is contained in:
203
models/user.py
Normal file
203
models/user.py
Normal file
@@ -0,0 +1,203 @@
|
||||
"""
|
||||
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='YYYY-MM-DD')
|
||||
time_format = db.Column(db.String(10), default='24h')
|
||||
|
||||
# Notification preferences
|
||||
email_notifications = db.Column(db.Boolean, default=True)
|
||||
email_daily_summary = db.Column(db.Boolean, default=False)
|
||||
email_weekly_summary = db.Column(db.Boolean, default=True)
|
||||
|
||||
# Time tracking preferences
|
||||
default_project_id = db.Column(db.Integer, db.ForeignKey('project.id'), nullable=True)
|
||||
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))
|
||||
default_project = db.relationship('Project')
|
||||
|
||||
|
||||
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')
|
||||
Reference in New Issue
Block a user