diff --git a/.gitignore b/.gitignore index 507cfe3..db3d9fd 100644 --- a/.gitignore +++ b/.gitignore @@ -145,4 +145,8 @@ node_modules/ # Temporary files *.tmp -*.temp \ No newline at end of file +*.temp + +# User uploaded content +static/uploads/avatars/* +!static/uploads/avatars/.gitkeep \ No newline at end of file diff --git a/app.py b/app.py index af74030..4cde1e8 100644 --- a/app.py +++ b/app.py @@ -1,5 +1,5 @@ from flask import Flask, render_template, request, redirect, url_for, jsonify, flash, session, g, Response, send_file -from models import db, TimeEntry, WorkConfig, User, SystemSettings, Team, Role, Project, Company, CompanyWorkConfig, CompanySettings, UserPreferences, WorkRegion, AccountType, ProjectCategory, Task, SubTask, TaskStatus, TaskPriority, TaskDependency, Sprint, SprintStatus, Announcement, SystemEvent, WidgetType, UserDashboard, DashboardWidget, WidgetTemplate, Comment, CommentVisibility +from models import db, TimeEntry, WorkConfig, User, SystemSettings, Team, Role, Project, Company, CompanyWorkConfig, CompanySettings, UserPreferences, WorkRegion, AccountType, ProjectCategory, Task, SubTask, TaskStatus, TaskPriority, TaskDependency, Sprint, SprintStatus, Announcement, SystemEvent, WidgetType, UserDashboard, DashboardWidget, WidgetTemplate, Comment, CommentVisibility, BrandingSettings from data_formatting import ( format_duration, prepare_export_data, prepare_team_hours_export_data, format_table_data, format_graph_data, format_team_data, format_burndown_data @@ -41,7 +41,7 @@ app.config['MAIL_PORT'] = int(os.environ.get('MAIL_PORT') or 587) app.config['MAIL_USE_TLS'] = os.environ.get('MAIL_USE_TLS', 'true').lower() in ['true', 'on', '1'] app.config['MAIL_USERNAME'] = os.environ.get('MAIL_USERNAME', 'your-email@example.com') app.config['MAIL_PASSWORD'] = os.environ.get('MAIL_PASSWORD', 'your-password') -app.config['MAIL_DEFAULT_SENDER'] = os.environ.get('MAIL_DEFAULT_SENDER', 'TimeTrack ') +app.config['MAIL_DEFAULT_SENDER'] = os.environ.get('MAIL_DEFAULT_SENDER', 'TimeTrack ') # Will be overridden by branding in mail sending functions # Log mail configuration (without password) logger.info(f"Mail server: {app.config['MAIL_SERVER']}") @@ -406,6 +406,9 @@ def load_logged_in_user(): return redirect(url_for('login')) else: g.company = None + + # Load branding settings + g.branding = BrandingSettings.get_current() @app.route('/') def home(): @@ -638,19 +641,19 @@ def register(): else: # Send verification email for regular users when verification is required verification_url = url_for('verify_email', token=token, _external=True) - msg = Message('Verify your TimeTrack account', recipients=[email]) + msg = Message(f'Verify your {g.branding.app_name} account', recipients=[email]) msg.body = f'''Hello {username}, -Thank you for registering with TimeTrack. To complete your registration, please click on the link below: +Thank you for registering with {g.branding.app_name}. To complete your registration, please click on the link below: {verification_url} This link will expire in 24 hours. -If you did not register for TimeTrack, please ignore this email. +If you did not register for {g.branding.app_name}, please ignore this email. Best regards, -The TimeTrack Team +The {g.branding.app_name} Team ''' mail.send(msg) logger.info(f"Verification email sent to {email}") @@ -967,17 +970,17 @@ def create_user(): # Generate verification token and send email token = new_user.generate_verification_token() verification_url = url_for('verify_email', token=token, _external=True) - msg = Message('Verify your TimeTrack account', recipients=[email]) + msg = Message(f'Verify your {g.branding.app_name} account', recipients=[email]) msg.body = f'''Hello {username}, -An administrator has created an account for you on TimeTrack. To activate your account, please click on the link below: +An administrator has created an account for you on {g.branding.app_name}. To activate your account, please click on the link below: {verification_url} This link will expire in 24 hours. Best regards, -The TimeTrack Team +The {g.branding.app_name} Team ''' mail.send(msg) @@ -1490,7 +1493,7 @@ def setup_2fa(): import io import base64 - qr_uri = g.user.get_2fa_uri() + qr_uri = g.user.get_2fa_uri(issuer_name=g.branding.app_name) qr = qrcode.QRCode(version=1, box_size=10, border=5) qr.add_data(qr_uri) qr.make(fit=True) @@ -2469,6 +2472,60 @@ def system_admin_settings(): total_users=total_users, total_system_admins=total_system_admins) +@app.route('/system-admin/branding', methods=['GET', 'POST']) +@system_admin_required +def system_admin_branding(): + """System Admin: Branding settings""" + if request.method == 'POST': + branding = BrandingSettings.get_current() + + # Handle form data + branding.app_name = request.form.get('app_name', g.branding.app_name).strip() + branding.logo_alt_text = request.form.get('logo_alt_text', '').strip() + branding.primary_color = request.form.get('primary_color', '#007bff').strip() + branding.updated_by_id = g.user.id + + # Handle logo upload + if 'logo_file' in request.files: + logo_file = request.files['logo_file'] + if logo_file and logo_file.filename: + # Create uploads directory if it doesn't exist + upload_dir = os.path.join(app.static_folder, 'uploads', 'branding') + os.makedirs(upload_dir, exist_ok=True) + + # Save the file with a timestamp to avoid conflicts + import time + filename = f"logo_{int(time.time())}_{logo_file.filename}" + logo_path = os.path.join(upload_dir, filename) + logo_file.save(logo_path) + branding.logo_filename = filename + + # Handle favicon upload + if 'favicon_file' in request.files: + favicon_file = request.files['favicon_file'] + if favicon_file and favicon_file.filename: + # Create uploads directory if it doesn't exist + upload_dir = os.path.join(app.static_folder, 'uploads', 'branding') + os.makedirs(upload_dir, exist_ok=True) + + # Save the file with a timestamp to avoid conflicts + import time + filename = f"favicon_{int(time.time())}_{favicon_file.filename}" + favicon_path = os.path.join(upload_dir, filename) + favicon_file.save(favicon_path) + branding.favicon_filename = filename + + db.session.commit() + flash('Branding settings updated successfully.', 'success') + return redirect(url_for('system_admin_branding')) + + # Get current branding settings + branding = BrandingSettings.get_current() + + return render_template('system_admin_branding.html', + title='System Administrator - Branding Settings', + branding=branding) + @app.route('/system-admin/health') @system_admin_required def system_admin_health(): diff --git a/migrate_db.py b/migrate_db.py index 5e789bb..b98cd2c 100644 --- a/migrate_db.py +++ b/migrate_db.py @@ -16,7 +16,8 @@ try: from models import (User, TimeEntry, WorkConfig, SystemSettings, Team, Role, Project, Company, CompanyWorkConfig, CompanySettings, UserPreferences, WorkRegion, AccountType, ProjectCategory, Task, SubTask, TaskStatus, TaskPriority, Announcement, SystemEvent, - WidgetType, UserDashboard, DashboardWidget, WidgetTemplate, Comment, CommentVisibility) + WidgetType, UserDashboard, DashboardWidget, WidgetTemplate, Comment, CommentVisibility, + BrandingSettings) from werkzeug.security import generate_password_hash FLASK_AVAILABLE = True except ImportError: @@ -327,6 +328,25 @@ def create_missing_tables(cursor): ) """) + # Branding Settings table + cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='branding_settings'") + if not cursor.fetchone(): + print("Creating branding_settings table...") + cursor.execute(""" + CREATE TABLE branding_settings ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + app_name VARCHAR(100) NOT NULL DEFAULT 'Time Tracker', + logo_filename VARCHAR(255), + logo_alt_text VARCHAR(255) DEFAULT 'Logo', + favicon_filename VARCHAR(255), + primary_color VARCHAR(7) DEFAULT '#007bff', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_by_id INTEGER, + FOREIGN KEY (updated_by_id) REFERENCES user (id) + ) + """) + def migrate_to_company_model(db_path): """Migrate to company-based multi-tenancy model.""" @@ -1199,6 +1219,31 @@ def migrate_postgresql_schema(): db.session.execute(text("CREATE INDEX idx_comment_created_at ON comment(created_at DESC)")) db.session.commit() + # Check if branding_settings table exists + result = db.session.execute(text(""" + SELECT table_name + FROM information_schema.tables + WHERE table_name = 'branding_settings' + """)) + + if not result.fetchone(): + print("Creating branding_settings table...") + db.session.execute(text(""" + CREATE TABLE branding_settings ( + id SERIAL PRIMARY KEY, + app_name VARCHAR(100) NOT NULL DEFAULT 'Time Tracker', + logo_filename VARCHAR(255), + logo_alt_text VARCHAR(255) DEFAULT 'Logo', + favicon_filename VARCHAR(255), + primary_color VARCHAR(7) DEFAULT '#007bff', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_by_id INTEGER, + FOREIGN KEY (updated_by_id) REFERENCES "user" (id) + ) + """)) + db.session.commit() + # Check if company_settings table exists result = db.session.execute(text(""" SELECT table_name diff --git a/models.py b/models.py index fab0828..3154611 100644 --- a/models.py +++ b/models.py @@ -195,16 +195,18 @@ class User(db.Model): import pyotp self.two_factor_secret = pyotp.random_base32() return self.two_factor_secret - - def get_2fa_uri(self): + + 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="TimeTrack" + issuer_name=issuer_name ) def verify_2fa_token(self, token, allow_setup=False): @@ -267,6 +269,38 @@ class SystemSettings(db.Model): def __repr__(self): return f'' +class BrandingSettings(db.Model): + id = db.Column(db.Integer, primary_key=True) + app_name = db.Column(db.String(100), nullable=False, default='Time Tracker') + logo_filename = db.Column(db.String(255), nullable=True) # Filename of uploaded logo + logo_alt_text = db.Column(db.String(255), nullable=True, default='Logo') + favicon_filename = db.Column(db.String(255), nullable=True) # Filename of uploaded favicon + primary_color = db.Column(db.String(7), nullable=True, default='#007bff') # Hex color + + # Meta fields + created_at = db.Column(db.DateTime, default=datetime.now) + updated_at = db.Column(db.DateTime, default=datetime.now, onupdate=datetime.now) + updated_by_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=True) + + # Relationships + updated_by = db.relationship('User', foreign_keys=[updated_by_id]) + + def __repr__(self): + return f'' + + @staticmethod + def get_current(): + """Get current branding settings or create defaults""" + settings = BrandingSettings.query.first() + if not settings: + settings = BrandingSettings( + app_name='Time Tracker', + logo_alt_text='Application Logo' + ) + db.session.add(settings) + db.session.commit() + return settings + class TimeEntry(db.Model): id = db.Column(db.Integer, primary_key=True) arrival_time = db.Column(db.DateTime, nullable=False) diff --git a/templates/about.html b/templates/about.html index c1846a9..2248ad9 100644 --- a/templates/about.html +++ b/templates/about.html @@ -5,7 +5,7 @@

Professional Time Tracking Made Simple

-

TimeTrack is a comprehensive multi-tenant time management solution designed to help organizations and individuals monitor work hours efficiently. Built with simplicity and accuracy in mind, our platform provides the tools you need to track, manage, and analyze time spent on work activities across multiple companies and teams.

+

{{ g.branding.app_name }} is a comprehensive multi-tenant time management solution designed to help organizations and individuals monitor work hours efficiently. Built with simplicity and accuracy in mind, our platform provides the tools you need to track, manage, and analyze time spent on work activities across multiple companies and teams.

@@ -39,7 +39,7 @@

🏢 Multi-Company Support

-

Enterprise-ready multi-tenant architecture allows multiple companies to operate independently within a single TimeTrack instance, with complete data isolation and security.

+

Enterprise-ready multi-tenant architecture allows multiple companies to operate independently within a single {{ g.branding.app_name }} instance, with complete data isolation and security.

@@ -76,7 +76,7 @@
-

Why Choose TimeTrack?

+

Why Choose {{ g.branding.app_name }}?

@@ -103,7 +103,7 @@

Technical Information

-

TimeTrack is built using modern web technologies including Flask (Python), SQLite database, and responsive HTML/CSS/JavaScript frontend. The application features a REST API architecture, secure session management, and email integration for user verification.

+

{{ g.branding.app_name }} is built using modern web technologies including Flask (Python), SQLite database, and responsive HTML/CSS/JavaScript frontend. The application features a REST API architecture, secure session management, and email integration for user verification.

@@ -111,12 +111,12 @@

🏢 New Company Setup

-

Setting up TimeTrack for the first time or adding a new company? Create your company profile and administrator account to get started.

+

Setting up {{ g.branding.app_name }} for the first time or adding a new company? Create your company profile and administrator account to get started.

👥 Join Existing Company

-

Already have a company using TimeTrack? Get your company code from your administrator and register to join your organization.

+

Already have a company using {{ g.branding.app_name }}? Get your company code from your administrator and register to join your organization.

diff --git a/templates/index.html b/templates/index.html index 32ebd7f..26e70f8 100644 --- a/templates/index.html +++ b/templates/index.html @@ -9,7 +9,7 @@

Transform Your Productivity

-

Experience the future of time management with TimeTrack's intelligent tracking system

+

Experience the future of time management with {{ g.branding.app_name if g.branding else 'TimeTrack' }}'s intelligent tracking system

Get Started Free Sign In diff --git a/templates/layout.html b/templates/layout.html index 98c0aa8..e8544fe 100644 --- a/templates/layout.html +++ b/templates/layout.html @@ -3,19 +3,69 @@ - {{ title }} - TimeTrack{% if g.company %} - {{ g.company.name }}{% endif %} + {{ title }} - {{ g.branding.app_name if g.branding else 'TimeTrack' }}{% if g.company %} - {{ g.company.name }}{% endif %} {% if not g.user %} {% endif %} + {% if g.branding and g.branding.favicon_filename %} + + {% endif %} + {% if g.user %}
- TimeTrack + + {% if g.branding and g.branding.logo_filename %} + + {% else %} + {{ g.branding.app_name if g.branding else 'TimeTrack' }} + {% endif %} + {% if g.company %} {{ g.company.name }} {% endif %} @@ -32,6 +82,22 @@ {% if g.user %}
+
+ + +{% endblock %} \ No newline at end of file diff --git a/templates/system_admin_dashboard.html b/templates/system_admin_dashboard.html index 4d8e318..da34ec3 100644 --- a/templates/system_admin_dashboard.html +++ b/templates/system_admin_dashboard.html @@ -183,7 +183,10 @@ ⚙️ System Settings - + + 🎨 Branding Settings + + 🏥 System Health
diff --git a/templates/system_admin_settings.html b/templates/system_admin_settings.html index 44daaf8..f7cc023 100644 --- a/templates/system_admin_settings.html +++ b/templates/system_admin_settings.html @@ -119,7 +119,7 @@

Application Version

-

TimeTrack v1.0

+

{{ g.branding.app_name }} v1.0

Database