Add Forget Password feature.

This commit is contained in:
2025-07-13 12:57:52 +02:00
parent 1500b2cf88
commit 2969fb41c9
9 changed files with 1063 additions and 56 deletions

226
CLAUDE.md Normal file
View File

@@ -0,0 +1,226 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project Overview
TimeTrack is a comprehensive web-based time tracking application built with Flask that provides enterprise-level time management capabilities for teams and organizations. It features multi-tenancy, role-based access control, project/team management, billing/invoicing, and secure authentication with 2FA.
## Tech Stack
- **Backend**: Flask 2.0.1 with SQLAlchemy ORM
- **Database**: PostgreSQL (production) / SQLite (development)
- **Migrations**: Flask-Migrate (Alembic-based)
- **Frontend**: Server-side rendered Jinja2 templates with vanilla JavaScript
- **Authentication**: Session-based with TOTP 2FA support and password reset via email
- **Export**: Pandas for CSV/Excel, ReportLab for PDF generation
- **Mobile**: Progressive Web App (PWA) support with optimized mobile UI
## Development Setup
### Local Development
```bash
# Using virtual environment
source .venv/bin/activate # or pipenv shell
# Set environment variables (PostgreSQL example)
export DATABASE_URL="postgresql://timetrack:timetrack123@localhost:5432/timetrack"
# Run the application
python app.py
```
### Docker Development
```bash
# Standard docker-compose (uses PostgreSQL)
docker-compose up
# Debug mode with hot-reload
docker-compose -f docker-compose.debug.yml up
```
## Database Operations
### Flask-Migrate Commands
```bash
# Create a new migration
python create_migration.py "Description of changes"
# Apply pending migrations
python apply_migration.py
# Check current migration state
python check_migration_state.py
# Clean migration state (CAUTION: destructive)
python clean_migration_state.py
# For Docker environments
docker exec timetrack-timetrack-1 python create_migration.py "Description"
docker exec timetrack-timetrack-1 python apply_migration.py
```
### Standard Flask-Migrate Commands
```bash
# Create migration
flask db migrate -m "Description"
# Apply migrations
flask db upgrade
# Rollback one revision
flask db downgrade
# Show current revision
flask db current
# Show migration history
flask db history
```
## Key Architecture Patterns
### 1. Blueprint-Based Modular Architecture
Routes are organized into blueprints by feature domain:
- `/routes/auth.py` - Authentication and authorization decorators
- `/routes/projects.py` - Project management
- `/routes/invoice.py` - Billing and invoicing
- `/routes/tax_configuration.py` - Tax management
- `/routes/teams.py` - Team management
- `/routes/export.py` - Data export functionality
### 2. Model Organization
Models are split by domain in `/models/`:
- `user.py` - User, Role, UserPreferences
- `company.py` - Company, CompanySettings, CompanyWorkConfig
- `project.py` - Project, ProjectCategory
- `team.py` - Team
- `time_entry.py` - TimeEntry
- `invoice.py` - Invoice, InvoiceLineItem, InvoiceStatus
- `tax_configuration.py` - TaxConfiguration
- `enums.py` - BillingType, AccountType, etc.
### 3. Multi-Tenancy Pattern
All data is scoped by company_id with automatic filtering:
```python
# Common pattern in routes
projects = Project.query.filter_by(company_id=g.user.company_id).all()
```
### 4. Role-Based Access Control
Decorators enforce permissions:
```python
@role_required(Role.SUPERVISOR) # Supervisor and above
@admin_required # Admin and System Admin only
@company_required # Ensures user has company context
```
### 5. Billing Architecture
- Projects support multiple billing types: NON_BILLABLE, HOURLY, DAILY_RATE, FIXED_RATE
- Invoices support net/gross pricing with country-specific tax configurations
- TimeEntry calculates billing based on project settings (8-hour day for daily rates)
## Common Development Tasks
### Adding a New Feature
1. Create model in appropriate file under `/models/`
2. Create blueprint in `/routes/` with proper decorators
3. Register blueprint in `app.py`
4. Create templates in `/templates/`
5. Run migration: `python create_migration.py "Add feature X"`
### Testing Database Changes
```bash
# Check what will be migrated
python check_migration_state.py
# Test in isolated environment
docker-compose -f docker-compose.debug.yml up
# Apply to development database
DATABASE_URL="postgresql://..." python apply_migration.py
```
### Working with Billing Features
When modifying billing/invoice features:
1. Check `models/enums.py` for BillingType values
2. Update TimeEntry.calculate_billing_amount() for new billing logic
3. Update Invoice.calculate_totals() for tax calculations
4. Ensure templates handle all billing types (hourly/daily rate display)
## Important Implementation Details
### Session Management
- Sessions are permanent with 7-day lifetime
- User context loaded in `app.before_request()` via `g.user`
- Company context available via `g.company`
### Time Calculations
- Time entries use UTC internally, converted for display
- Rounding rules configured per company/user
- Break time calculations handled in TimeEntry model
### Export System
- Pandas handles CSV/Excel generation
- ReportLab for PDF invoices
- Export routes handle large datasets with streaming
### Email Configuration
- Flask-Mail configured via environment variables
- MAIL_SERVER, MAIL_PORT, MAIL_USERNAME required
- Used for user invitations and password resets
### Authentication Features
- Password reset functionality with secure tokens (1-hour expiry)
- Two-factor authentication (2FA) using TOTP
- Session-based authentication with "Remember Me" option
- Email verification for new accounts (configurable)
### Mobile UI Features
- Progressive Web App (PWA) manifest for installability
- Mobile-optimized navigation with hamburger menu and bottom nav
- Touch-friendly form inputs and buttons (44px minimum touch targets)
- Responsive tables with card view on small screens
- Pull-to-refresh functionality
- Mobile gestures support (swipe, pinch-to-zoom)
- Date/time pickers that respect user preferences
## Environment Variables
Required for production:
- `DATABASE_URL` - PostgreSQL connection string
- `SECRET_KEY` - Flask session secret
- `MAIL_SERVER`, `MAIL_PORT`, `MAIL_USERNAME`, `MAIL_PASSWORD` - Email config
Optional:
- `FLASK_ENV` - Set to 'development' for debug mode
- `FORCE_HTTPS` - Set to 'true' for HTTPS enforcement
- `TRUST_PROXY_HEADERS` - Set to 'true' when behind reverse proxy
## Troubleshooting
### Migration Issues
1. Run `python check_migration_state.py` to verify database state
2. Check `/migrations/versions/` for migration files
3. If stuck, use `clean_migration_state.py` (careful - destructive)
### Import Errors
- Ensure all model imports in routes use: `from models import ModelName`
- Blueprint registration order matters in `app.py`
### Database Connection
- PostgreSQL requires running container or local instance
- Default fallback to SQLite at `/data/timetrack.db`
- Check `docker-compose.yml` for PostgreSQL credentials

302
README.md
View File

@@ -1,6 +1,6 @@
# TimeTrack # TimeTrack
TimeTrack is a comprehensive web-based time tracking application built with Flask that provides enterprise-level time management capabilities for teams and organizations. The application features role-based access control, project management, team collaboration, and secure authentication. TimeTrack is a comprehensive web-based time tracking application built with Flask that provides enterprise-level time management capabilities for teams and organizations. The application features multi-tenancy support, role-based access control, project management, team collaboration, billing/invoicing, and secure authentication with 2FA and password reset functionality. It includes a Progressive Web App (PWA) interface with mobile-optimized features.
## Features ## Features
@@ -13,59 +13,99 @@ TimeTrack is a comprehensive web-based time tracking application built with Flas
### User Management & Security ### User Management & Security
- **Two-Factor Authentication (2FA)**: TOTP-based authentication with QR codes - **Two-Factor Authentication (2FA)**: TOTP-based authentication with QR codes
- **Role-Based Access Control**: Admin, Supervisor, Team Leader, and Team Member roles - **Password Reset**: Secure email-based password recovery (1-hour token expiry)
- **User Administration**: Create, edit, block/unblock users with verification system - **Role-Based Access Control**: System Admin, Admin, Supervisor, Team Leader, and Team Member roles
- **Profile Management**: Update email, password, and personal settings - **Multi-Tenancy**: Complete data isolation between companies
- **User Administration**: Create, edit, block/unblock users with email verification
- **Profile Management**: Update email, password, avatar, and personal settings
- **Company Invitations**: Invite users to join your company via email
### Team & Project Management ### Team & Project Management
- **Team Management**: Create teams, assign members, and track team hours - **Team Management**: Create teams, assign members, and track team hours
- **Project Management**: Create projects with codes, assign to teams, set active/inactive status - **Project Management**: Create projects with codes, categories, billing types (hourly/daily/fixed)
- **Sprint Management**: Agile sprint planning and tracking
- **Task Management**: Create and manage tasks with subtasks and dependencies
- **Notes System**: Create, organize, and share notes with folder structure
- **Team Hours Tracking**: View team member working hours with date filtering - **Team Hours Tracking**: View team member working hours with date filtering
- **Project Assignment**: Flexible project-team assignments and access control - **Project Assignment**: Flexible project-team assignments and access control
### Export & Reporting ### Export & Reporting
- **Multiple Export Formats**: CSV and Excel export capabilities - **Multiple Export Formats**: CSV, Excel, and PDF export capabilities
- **Individual Time Export**: Personal time entries with date range selection - **Individual Time Export**: Personal time entries with date range selection
- **Team Hours Export**: Export team member hours with filtering options - **Team Hours Export**: Export team member hours with filtering options
- **Invoice Generation**: Create and export invoices with tax configurations
- **Analytics Dashboard**: Visual analytics with charts and statistics
- **Quick Export Options**: Today, week, month, or all-time data exports - **Quick Export Options**: Today, week, month, or all-time data exports
### Administrative Features ### Administrative Features
- **Company Management**: Multi-company support with separate settings
- **Admin Dashboard**: System overview with user, team, and activity statistics - **Admin Dashboard**: System overview with user, team, and activity statistics
- **System Settings**: Configure registration settings and global preferences - **System Settings**: Configure registration, email verification, tracking scripts
- **Work Configuration**: Set work hours, break rules, and rounding preferences
- **Tax Configuration**: Country-specific tax rates for invoicing
- **Branding**: Custom logos, colors, and application naming
- **User Administration**: Complete user lifecycle management - **User Administration**: Complete user lifecycle management
- **Team & Project Administration**: Full CRUD operations for teams and projects - **Team & Project Administration**: Full CRUD operations for teams and projects
- **System Admin Tools**: Multi-company oversight and system health monitoring
## Tech Stack ## Tech Stack
- **Backend**: Flask 2.0.1 with SQLAlchemy ORM - **Backend**: Flask 2.0.1 with SQLAlchemy ORM
- **Database**: SQLite with comprehensive relational schema - **Database**: PostgreSQL (production)
- **Authentication**: Flask session management with 2FA support - **Migrations**: Flask-Migrate (Alembic-based) with custom helpers
- **Frontend**: Responsive HTML, CSS, JavaScript with real-time updates - **Authentication**: Session-based with 2FA and password reset via Flask-Mail
- **Security**: TOTP-based two-factor authentication, role-based access control - **Frontend**: Server-side Jinja2 templates with vanilla JavaScript
- **Export**: CSV and Excel export capabilities - **Mobile**: Progressive Web App (PWA) with mobile-optimized UI
- **Dependencies**: See Pipfile for complete dependency list - **Export**: Pandas for CSV/Excel, ReportLab for PDF generation
- **Containerization**: Docker and Docker Compose for easy deployment
- **Dependencies**: See requirements.txt for complete dependency list
## Installation ## Installation
### Prerequisites ### Prerequisites
- Python 3.12 - Python 3.9+
- pip or pipenv - PostgreSQL 15+ (for production)
- Docker & Docker Compose (recommended)
### Setup with pipenv (recommended) ### Quick Start with Docker (Recommended)
```bash ```bash
# Clone the repository # Clone the repository
git clone https://github.com/nullmedium/TimeTrack.git git clone https://github.com/nullmedium/TimeTrack.git
cd TimeTrack cd TimeTrack
# Install dependencies using pipenv # Copy and configure environment variables
pipenv install cp .env.example .env
# Edit .env with your settings
# Activate the virtual environment # Start the application
pipenv shell docker-compose up -d
# Run the application (migrations run automatically on first startup) # Access at http://localhost:5000
```
### Local Development Setup
```bash
# Create virtual environment
python -m venv .venv
source .venv/bin/activate # On Windows: .venv\Scripts\activate
# Install dependencies
pip install -r requirements.txt
# Set environment variables
export DATABASE_URL="postgresql://user:pass@localhost/timetrack"
export SECRET_KEY="your-secret-key"
export MAIL_SERVER="smtp.example.com"
# ... other environment variables
# Run migrations
python create_migration.py "Initial setup"
python apply_migration.py
# Run the application
python app.py python app.py
``` ```
@@ -81,22 +121,60 @@ python app.py
### Database Migrations ### Database Migrations
**Automatic Migration System**: All database migrations now run automatically when the application starts. No manual migration scripts need to be run. **Flask-Migrate System**: The application uses Flask-Migrate (Alembic-based) for database version control.
The integrated migration system handles: ```bash
- Database schema creation for new installations # Create a new migration
- Automatic schema updates for existing databases python create_migration.py "Description of changes"
- User table enhancements (verification, roles, teams, 2FA)
- Project and team management table creation # Apply migrations
- Sample data initialization python apply_migration.py
- Data integrity maintenance during upgrades
# Check migration status
python check_migration_state.py
# For Docker environments
docker exec timetrack-timetrack-1 python create_migration.py "Description"
docker exec timetrack-timetrack-1 python apply_migration.py
```
The migration system handles:
- Automatic schema updates on container startup
- PostgreSQL-specific features (enums, constraints)
- Safe rollback capabilities
- Multi-tenancy data isolation
- Preservation of existing data during schema changes
### Configuration ### Configuration
The application can be configured through: #### Environment Variables
- **Admin Dashboard**: System-wide settings and user management
- **User Profiles**: Individual work hour and break preferences ```bash
- **Environment Variables**: Database and Flask configuration # Database
DATABASE_URL=postgresql://user:password@host:5432/dbname
# Flask
SECRET_KEY=your-secret-key-here
FLASK_ENV=development # or production
# Email (required for invitations and password reset)
MAIL_SERVER=smtp.example.com
MAIL_PORT=587
MAIL_USE_TLS=true
MAIL_USERNAME=your-email@example.com
MAIL_PASSWORD=your-password
# Optional
FORCE_HTTPS=true # Force HTTPS in production
TRUST_PROXY_HEADERS=true # When behind reverse proxy
```
#### Application Settings
- **Company Settings**: Work hours, break requirements, time rounding
- **User Preferences**: Date/time formats, timezone, UI preferences
- **System Settings**: Registration, email verification, tracking scripts
- **Branding**: Custom logos, colors, and naming
## Usage ## Usage
@@ -123,36 +201,152 @@ The application can be configured through:
## Security Features ## Security Features
- **Two-Factor Authentication**: TOTP-based 2FA with QR code setup - **Two-Factor Authentication**: TOTP-based 2FA with QR code setup
- **Role-Based Access Control**: Four distinct user roles with appropriate permissions - **Password Reset**: Secure email-based recovery with time-limited tokens
- **Session Management**: Secure login/logout with "remember me" functionality - **Role-Based Access Control**: Five distinct user roles with granular permissions
- **Data Validation**: Comprehensive input validation and error handling - **Multi-Tenancy**: Complete data isolation between companies
- **Account Verification**: Email verification system for new accounts - **Session Management**: Secure sessions with configurable lifetime
- **Email Verification**: Optional email verification for new accounts
- **Password Strength**: Enforced password complexity requirements
- **Security Headers**: HSTS, CSP, X-Frame-Options, and more
- **Input Validation**: Comprehensive server-side validation
- **CSRF Protection**: Built-in Flask CSRF protection
## Features for Mobile Users
- **Progressive Web App**: Install TimeTrack as an app on mobile devices
- **Mobile Navigation**: Bottom navigation bar and hamburger menu
- **Touch Optimization**: 44px minimum touch targets throughout
- **Responsive Tables**: Automatic card view on small screens
- **Mobile Gestures**: Swipe navigation and pull-to-refresh
- **Date/Time Pickers**: Mobile-friendly pickers respecting user preferences
- **Offline Support**: Basic offline functionality with service workers
- **Performance**: Lazy loading and optimized for mobile networks
## Project Structure
```
TimeTrack/
├── app.py # Main Flask application
├── models/ # Database models organized by domain
│ ├── user.py # User, Role, UserPreferences
│ ├── company.py # Company, CompanySettings
│ ├── project.py # Project, ProjectCategory
│ ├── time_entry.py # TimeEntry model
│ └── ... # Other domain models
├── routes/ # Blueprint-based route handlers
│ ├── auth.py # Authentication routes
│ ├── projects.py # Project management
│ ├── teams.py # Team management
│ └── ... # Other route modules
├── templates/ # Jinja2 templates
├── static/ # CSS, JS, images
│ ├── css/ # Stylesheets including mobile
│ ├── js/ # JavaScript including PWA support
│ └── manifest.json # PWA manifest
├── migrations/ # Flask-Migrate/Alembic migrations
├── docker-compose.yml # Docker configuration
└── requirements.txt # Python dependencies
```
## API Endpoints ## API Endpoints
The application provides various endpoints for different user roles: Key application routes organized by function:
- `/admin/*`: Administrative functions (Admin only) - `/` - Home dashboard
- `/supervisor/*`: Supervisor functions (Supervisor+ roles) - `/login`, `/logout`, `/register` - Authentication
- `/team/*`: Team management (Team Leader+ roles) - `/forgot_password`, `/reset_password/<token>` - Password recovery
- `/export/*`: Data export functionality - `/time-tracking` - Main time tracking interface
- `/auth/*`: Authentication and profile management - `/projects/*` - Project management
- `/teams/*` - Team management
- `/tasks/*` - Task and sprint management
- `/notes/*` - Notes system
- `/invoices/*` - Billing and invoicing
- `/export/*` - Data export functionality
- `/admin/*` - Administrative functions
- `/system-admin/*` - System administration (multi-company)
## File Structure ## Deployment
- `app.py`: Main Flask application with integrated migration system ### Production Deployment with Docker
- `models.py`: Database models and relationships
- `templates/`: HTML templates for all pages ```bash
- `static/`: CSS and JavaScript files # Clone and configure
- `migrate_*.py`: Legacy migration scripts (no longer needed) git clone https://github.com/nullmedium/TimeTrack.git
cd TimeTrack
cp .env.example .env
# Edit .env with production values
# Build and start
docker-compose -f docker-compose.yml up -d
# View logs
docker-compose logs -f
# Backup database
docker exec timetrack-db-1 pg_dump -U timetrack timetrack > backup.sql
```
### Reverse Proxy Configuration (Nginx)
```nginx
server {
listen 80;
server_name timetrack.example.com;
location / {
proxy_pass http://localhost:5000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
```
## Troubleshooting
### Common Issues
1. **Migration Errors**
- Check current state: `python check_migration_state.py`
- View migration history: `flask db history`
- Fix revision conflicts in alembic_version table
2. **Docker Issues**
- Container not starting: `docker logs timetrack-timetrack-1`
- Database connection: Ensure PostgreSQL is healthy
- Permission errors: Check volume permissions
3. **Email Not Sending**
- Verify MAIL_* environment variables
- Check firewall rules for SMTP port
- Enable "less secure apps" if using Gmail
4. **2FA Issues**
- Ensure system time is synchronized
- QR code not scanning: Check for firewall blocking images
## Contributing ## Contributing
1. Fork the repository 1. Fork the repository
2. Create a feature branch 2. Create a feature branch (`git checkout -b feature/amazing-feature`)
3. Make your changes 3. Commit your changes (`git commit -m 'Add amazing feature'`)
4. Test thoroughly 4. Push to the branch (`git push origin feature/amazing-feature`)
5. Submit a pull request 5. Open a Pull Request
### Development Guidelines
- Follow PEP 8 for Python code
- Write tests for new features
- Update documentation as needed
- Ensure migrations are reversible
- Test on both PostgreSQL and SQLite
## License ## License
This project is licensed under the MIT License. This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
## Acknowledgments
- Flask community for the excellent framework
- Contributors and testers
- Open source projects that made this possible

100
app.py
View File

@@ -909,6 +909,106 @@ def verify_email(token):
return redirect(url_for('login')) return redirect(url_for('login'))
@app.route('/forgot_password', methods=['GET', 'POST'])
def forgot_password():
"""Handle forgot password requests"""
if request.method == 'POST':
username_or_email = request.form.get('username_or_email', '').strip()
if not username_or_email:
flash('Please enter your username or email address.', 'error')
return render_template('forgot_password.html', title='Forgot Password')
# Try to find user by username or email
user = User.query.filter(
db.or_(
User.username == username_or_email,
User.email == username_or_email
)
).first()
if user and user.email:
# Generate reset token
token = user.generate_password_reset_token()
# Send reset email
reset_url = url_for('reset_password', token=token, _external=True)
msg = Message(
f'Password Reset Request - {g.branding.app_name if g.branding else "TimeTrack"}',
recipients=[user.email]
)
msg.body = f'''Hello {user.username},
You have requested to reset your password for {g.branding.app_name if g.branding else "TimeTrack"}.
To reset your password, please click on the link below:
{reset_url}
This link will expire in 1 hour.
If you did not request a password reset, please ignore this email.
Best regards,
The {g.branding.app_name if g.branding else "TimeTrack"} Team
'''
try:
mail.send(msg)
logger.info(f"Password reset email sent to user {user.username}")
except Exception as e:
logger.error(f"Failed to send password reset email: {str(e)}")
flash('Failed to send reset email. Please contact support.', 'error')
return render_template('forgot_password.html', title='Forgot Password')
# Always show success message to prevent user enumeration
flash('If an account exists with that username or email address, we have sent a password reset link.', 'success')
return redirect(url_for('login'))
return render_template('forgot_password.html', title='Forgot Password')
@app.route('/reset_password/<token>', methods=['GET', 'POST'])
def reset_password(token):
"""Handle password reset with token"""
# Find user by reset token
user = User.query.filter_by(password_reset_token=token).first()
if not user or not user.verify_password_reset_token(token):
flash('Invalid or expired reset link.', 'error')
return redirect(url_for('login'))
if request.method == 'POST':
password = request.form.get('password')
confirm_password = request.form.get('confirm_password')
# Validate input
error = None
if not password:
error = 'Password is required'
elif password != confirm_password:
error = 'Passwords do not match'
# Validate password strength
if not error:
validator = PasswordValidator()
is_valid, password_errors = validator.validate(password)
if not is_valid:
error = password_errors[0]
if error:
flash(error, 'error')
return render_template('reset_password.html', token=token, title='Reset Password')
# Update password
user.set_password(password)
user.clear_password_reset_token()
db.session.commit()
logger.info(f"Password reset successful for user {user.username}")
flash('Your password has been reset successfully. Please log in with your new password.', 'success')
return redirect(url_for('login'))
return render_template('reset_password.html', token=token, title='Reset Password')
@app.route('/dashboard') @app.route('/dashboard')
@role_required(Role.TEAM_MEMBER) @role_required(Role.TEAM_MEMBER)
@company_required @company_required

View File

@@ -0,0 +1,29 @@
"""Add password reset fields to user model
Revision ID: 85d490db548b
Revises: c72667903a91
Create Date: 2025-07-13 12:24:14.261548
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '85d490db548b'
down_revision = 'c72667903a91'
branch_labels = None
depends_on = None
def upgrade():
# Add password reset fields to user table
op.add_column('user', sa.Column('password_reset_token', sa.String(length=100), nullable=True))
op.add_column('user', sa.Column('password_reset_expiry', sa.DateTime(), nullable=True))
op.create_unique_constraint('uq_user_password_reset_token', 'user', ['password_reset_token'])
def downgrade():
# Remove password reset fields from user table
op.drop_constraint('uq_user_password_reset_token', 'user', type_='unique')
op.drop_column('user', 'password_reset_expiry')
op.drop_column('user', 'password_reset_token')

View File

@@ -48,6 +48,10 @@ class User(db.Model):
# Avatar field # Avatar field
avatar_url = db.Column(db.String(255), nullable=True) # URL to user's avatar image avatar_url = db.Column(db.String(255), nullable=True) # URL to user's avatar image
# Password reset fields
password_reset_token = db.Column(db.String(100), unique=True, nullable=True)
password_reset_expiry = db.Column(db.DateTime, nullable=True)
# Relationships # Relationships
time_entries = db.relationship('TimeEntry', backref='user', lazy=True) time_entries = db.relationship('TimeEntry', backref='user', lazy=True)
@@ -139,6 +143,28 @@ class User(db.Model):
elif self.username: elif self.username:
return self.username[:2].upper() return self.username[:2].upper()
return "??" return "??"
def generate_password_reset_token(self):
"""Generate a password reset token"""
token = secrets.token_urlsafe(32)
self.password_reset_token = token
self.password_reset_expiry = datetime.utcnow() + timedelta(hours=1)
db.session.commit()
return token
def verify_password_reset_token(self, token):
"""Verify if the password reset token is valid"""
if not self.password_reset_token or self.password_reset_token != token:
return False
if not self.password_reset_expiry or datetime.utcnow() > self.password_reset_expiry:
return False
return True
def clear_password_reset_token(self):
"""Clear the password reset token after use"""
self.password_reset_token = None
self.password_reset_expiry = None
db.session.commit()
def __repr__(self): def __repr__(self):
return f'<User {self.username}>' return f'<User {self.username}>'

View File

@@ -3,13 +3,13 @@ Security headers middleware for Flask.
Add this to ensure secure form submission and prevent security warnings. Add this to ensure secure form submission and prevent security warnings.
""" """
from flask import request from flask import request, current_app
def add_security_headers(response): def add_security_headers(response):
"""Add security headers to all responses.""" """Add security headers to all responses."""
# Force HTTPS for all resources # Force HTTPS for all resources
if request.is_secure or not request.app.debug: if request.is_secure or not current_app.debug:
# Strict Transport Security - force HTTPS for 1 year # Strict Transport Security - force HTTPS for 1 year
response.headers['Strict-Transport-Security'] = 'max-age=31536000; includeSubDomains' response.headers['Strict-Transport-Security'] = 'max-age=31536000; includeSubDomains'

View File

@@ -0,0 +1,135 @@
{% extends "layout.html" %}
{% block content %}
<div class="content">
<div class="auth-container">
<div class="auth-card">
<h2 class="auth-title">Forgot Password</h2>
<p class="auth-subtitle">
Enter your username or email address and we'll send you a link to reset your password.
</p>
<form method="POST" class="auth-form">
<div class="form-group">
<label for="username_or_email">Username or Email</label>
<input type="text"
id="username_or_email"
name="username_or_email"
class="form-control"
placeholder="Enter your username or email"
required
autofocus>
</div>
<button type="submit" class="btn btn-primary btn-block">Send Reset Link</button>
<div class="auth-links">
<a href="{{ url_for('login') }}">Back to Login</a>
</div>
</form>
</div>
</div>
</div>
<style>
.auth-container {
min-height: calc(100vh - 200px);
display: flex;
align-items: center;
justify-content: center;
padding: 2rem;
}
.auth-card {
background: white;
border-radius: 12px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
padding: 2.5rem;
width: 100%;
max-width: 400px;
}
.auth-title {
text-align: center;
margin-bottom: 0.5rem;
color: var(--primary-color);
}
.auth-subtitle {
text-align: center;
color: #666;
margin-bottom: 2rem;
line-height: 1.5;
}
.auth-form .form-group {
margin-bottom: 1.5rem;
}
.auth-form label {
display: block;
margin-bottom: 0.5rem;
font-weight: 500;
color: #333;
}
.auth-form .form-control {
width: 100%;
padding: 0.75rem;
border: 1px solid #ddd;
border-radius: 6px;
font-size: 1rem;
transition: border-color 0.2s;
}
.auth-form .form-control:focus {
outline: none;
border-color: var(--primary-color);
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
}
.btn-block {
width: 100%;
padding: 0.875rem;
font-size: 1rem;
font-weight: 500;
margin-bottom: 1rem;
}
.auth-links {
text-align: center;
margin-top: 1.5rem;
}
.auth-links a {
color: var(--primary-color);
text-decoration: none;
font-size: 0.875rem;
}
.auth-links a:hover {
text-decoration: underline;
}
/* Mobile optimization */
@media (max-width: 768px) {
.auth-container {
padding: 1rem;
min-height: calc(100vh - 140px);
}
.auth-card {
padding: 1.5rem;
}
.auth-title {
font-size: 1.5rem;
}
.auth-subtitle {
font-size: 0.875rem;
}
}
</style>
{% endblock %}

View File

@@ -47,6 +47,12 @@
<button type="submit" class="btn btn-primary">Sign In</button> <button type="submit" class="btn btn-primary">Sign In</button>
</div> </div>
<div class="auth-links" style="margin-bottom: 1rem;">
<p>
<a href="{{ url_for('forgot_password') }}">Forgot your password?</a>
</p>
</div>
<div class="social-divider"> <div class="social-divider">
<span>New to {{ g.branding.app_name if g.branding else 'TimeTrack' }}?</span> <span>New to {{ g.branding.app_name if g.branding else 'TimeTrack' }}?</span>
</div> </div>

View File

@@ -0,0 +1,291 @@
{% extends "layout.html" %}
{% block content %}
<div class="content">
<div class="auth-container">
<div class="auth-card">
<h2 class="auth-title">Reset Password</h2>
<p class="auth-subtitle">
Enter your new password below.
</p>
<form method="POST" class="auth-form">
<div class="form-group">
<label for="password">New Password</label>
<input type="password"
id="password"
name="password"
class="form-control"
placeholder="Enter new password"
required
autofocus>
<div id="password-strength" class="password-strength-meter"></div>
</div>
<div class="form-group">
<label for="confirm_password">Confirm Password</label>
<input type="password"
id="confirm_password"
name="confirm_password"
class="form-control"
placeholder="Confirm new password"
required>
</div>
<!-- Password requirements -->
<div class="password-requirements">
<p class="requirements-title">Password must contain:</p>
<ul id="password-requirements-list">
<li id="req-length">At least 8 characters</li>
<li id="req-uppercase">One uppercase letter</li>
<li id="req-lowercase">One lowercase letter</li>
<li id="req-number">One number</li>
<li id="req-special">One special character</li>
</ul>
</div>
<button type="submit" class="btn btn-primary btn-block">Reset Password</button>
<div class="auth-links">
<a href="{{ url_for('login') }}">Back to Login</a>
</div>
</form>
</div>
</div>
</div>
<style>
.auth-container {
min-height: calc(100vh - 200px);
display: flex;
align-items: center;
justify-content: center;
padding: 2rem;
}
.auth-card {
background: white;
border-radius: 12px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
padding: 2.5rem;
width: 100%;
max-width: 400px;
}
.auth-title {
text-align: center;
margin-bottom: 0.5rem;
color: var(--primary-color);
}
.auth-subtitle {
text-align: center;
color: #666;
margin-bottom: 2rem;
line-height: 1.5;
}
.auth-form .form-group {
margin-bottom: 1.5rem;
}
.auth-form label {
display: block;
margin-bottom: 0.5rem;
font-weight: 500;
color: #333;
}
.auth-form .form-control {
width: 100%;
padding: 0.75rem;
border: 1px solid #ddd;
border-radius: 6px;
font-size: 1rem;
transition: border-color 0.2s;
}
.auth-form .form-control:focus {
outline: none;
border-color: var(--primary-color);
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
}
.password-strength-meter {
margin-top: 0.5rem;
height: 4px;
background: #e0e0e0;
border-radius: 2px;
overflow: hidden;
transition: all 0.3s ease;
}
.password-strength-meter.weak {
background: #ff4444;
}
.password-strength-meter.fair {
background: #ffaa00;
}
.password-strength-meter.good {
background: #00aa00;
}
.password-strength-meter.strong {
background: #008800;
}
.password-requirements {
background: #f8f9fa;
border: 1px solid #e9ecef;
border-radius: 6px;
padding: 1rem;
margin-bottom: 1.5rem;
font-size: 0.875rem;
}
.requirements-title {
margin: 0 0 0.5rem 0;
font-weight: 500;
color: #495057;
}
#password-requirements-list {
margin: 0;
padding-left: 1.25rem;
color: #6c757d;
}
#password-requirements-list li {
margin-bottom: 0.25rem;
position: relative;
}
#password-requirements-list li.valid {
color: #28a745;
}
#password-requirements-list li.valid::before {
content: "✓ ";
position: absolute;
left: -1.25rem;
font-weight: bold;
}
.btn-block {
width: 100%;
padding: 0.875rem;
font-size: 1rem;
font-weight: 500;
margin-bottom: 1rem;
}
.auth-links {
text-align: center;
margin-top: 1.5rem;
}
.auth-links a {
color: var(--primary-color);
text-decoration: none;
font-size: 0.875rem;
}
.auth-links a:hover {
text-decoration: underline;
}
/* Mobile optimization */
@media (max-width: 768px) {
.auth-container {
padding: 1rem;
min-height: calc(100vh - 140px);
}
.auth-card {
padding: 1.5rem;
}
.auth-title {
font-size: 1.5rem;
}
.auth-subtitle {
font-size: 0.875rem;
}
.password-requirements {
font-size: 0.8125rem;
}
}
</style>
<script>
// Password strength validation
document.addEventListener('DOMContentLoaded', function() {
const passwordInput = document.getElementById('password');
const confirmInput = document.getElementById('confirm_password');
const strengthMeter = document.getElementById('password-strength');
// Requirement elements
const reqLength = document.getElementById('req-length');
const reqUppercase = document.getElementById('req-uppercase');
const reqLowercase = document.getElementById('req-lowercase');
const reqNumber = document.getElementById('req-number');
const reqSpecial = document.getElementById('req-special');
passwordInput.addEventListener('input', function() {
const password = this.value;
// Check each requirement
const hasLength = password.length >= 8;
const hasUppercase = /[A-Z]/.test(password);
const hasLowercase = /[a-z]/.test(password);
const hasNumber = /[0-9]/.test(password);
const hasSpecial = /[!@#$%^&*(),.?":{}|<>]/.test(password);
// Update requirement indicators
reqLength.classList.toggle('valid', hasLength);
reqUppercase.classList.toggle('valid', hasUppercase);
reqLowercase.classList.toggle('valid', hasLowercase);
reqNumber.classList.toggle('valid', hasNumber);
reqSpecial.classList.toggle('valid', hasSpecial);
// Calculate strength
let strength = 0;
if (hasLength) strength++;
if (hasUppercase) strength++;
if (hasLowercase) strength++;
if (hasNumber) strength++;
if (hasSpecial) strength++;
// Update strength meter
strengthMeter.className = 'password-strength-meter';
if (password.length === 0) {
strengthMeter.className = 'password-strength-meter';
} else if (strength <= 2) {
strengthMeter.className = 'password-strength-meter weak';
} else if (strength === 3) {
strengthMeter.className = 'password-strength-meter fair';
} else if (strength === 4) {
strengthMeter.className = 'password-strength-meter good';
} else {
strengthMeter.className = 'password-strength-meter strong';
}
});
// Real-time password match validation
confirmInput.addEventListener('input', function() {
if (passwordInput.value && this.value) {
if (passwordInput.value !== this.value) {
this.setCustomValidity('Passwords do not match');
} else {
this.setCustomValidity('');
}
}
});
});
</script>
{% endblock %}