Add Forget Password feature.
This commit is contained in:
226
CLAUDE.md
Normal file
226
CLAUDE.md
Normal 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
302
README.md
@@ -1,6 +1,6 @@
|
||||
# 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
|
||||
|
||||
@@ -13,59 +13,99 @@ TimeTrack is a comprehensive web-based time tracking application built with Flas
|
||||
|
||||
### User Management & Security
|
||||
- **Two-Factor Authentication (2FA)**: TOTP-based authentication with QR codes
|
||||
- **Role-Based Access Control**: Admin, Supervisor, Team Leader, and Team Member roles
|
||||
- **User Administration**: Create, edit, block/unblock users with verification system
|
||||
- **Profile Management**: Update email, password, and personal settings
|
||||
- **Password Reset**: Secure email-based password recovery (1-hour token expiry)
|
||||
- **Role-Based Access Control**: System Admin, Admin, Supervisor, Team Leader, and Team Member roles
|
||||
- **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 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
|
||||
- **Project Assignment**: Flexible project-team assignments and access control
|
||||
|
||||
### 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
|
||||
- **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
|
||||
|
||||
### Administrative Features
|
||||
- **Company Management**: Multi-company support with separate settings
|
||||
- **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
|
||||
- **Team & Project Administration**: Full CRUD operations for teams and projects
|
||||
- **System Admin Tools**: Multi-company oversight and system health monitoring
|
||||
|
||||
## Tech Stack
|
||||
|
||||
- **Backend**: Flask 2.0.1 with SQLAlchemy ORM
|
||||
- **Database**: SQLite with comprehensive relational schema
|
||||
- **Authentication**: Flask session management with 2FA support
|
||||
- **Frontend**: Responsive HTML, CSS, JavaScript with real-time updates
|
||||
- **Security**: TOTP-based two-factor authentication, role-based access control
|
||||
- **Export**: CSV and Excel export capabilities
|
||||
- **Dependencies**: See Pipfile for complete dependency list
|
||||
- **Database**: PostgreSQL (production)
|
||||
- **Migrations**: Flask-Migrate (Alembic-based) with custom helpers
|
||||
- **Authentication**: Session-based with 2FA and password reset via Flask-Mail
|
||||
- **Frontend**: Server-side Jinja2 templates with vanilla JavaScript
|
||||
- **Mobile**: Progressive Web App (PWA) with mobile-optimized UI
|
||||
- **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
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Python 3.12
|
||||
- pip or pipenv
|
||||
- Python 3.9+
|
||||
- PostgreSQL 15+ (for production)
|
||||
- Docker & Docker Compose (recommended)
|
||||
|
||||
### Setup with pipenv (recommended)
|
||||
### Quick Start with Docker (Recommended)
|
||||
|
||||
```bash
|
||||
# Clone the repository
|
||||
git clone https://github.com/nullmedium/TimeTrack.git
|
||||
cd TimeTrack
|
||||
|
||||
# Install dependencies using pipenv
|
||||
pipenv install
|
||||
# Copy and configure environment variables
|
||||
cp .env.example .env
|
||||
# Edit .env with your settings
|
||||
|
||||
# Activate the virtual environment
|
||||
pipenv shell
|
||||
# Start the application
|
||||
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
|
||||
```
|
||||
|
||||
@@ -81,22 +121,60 @@ python app.py
|
||||
|
||||
### 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:
|
||||
- Database schema creation for new installations
|
||||
- Automatic schema updates for existing databases
|
||||
- User table enhancements (verification, roles, teams, 2FA)
|
||||
- Project and team management table creation
|
||||
- Sample data initialization
|
||||
- Data integrity maintenance during upgrades
|
||||
```bash
|
||||
# Create a new migration
|
||||
python create_migration.py "Description of changes"
|
||||
|
||||
# Apply migrations
|
||||
python apply_migration.py
|
||||
|
||||
# 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
|
||||
|
||||
The application can be configured through:
|
||||
- **Admin Dashboard**: System-wide settings and user management
|
||||
- **User Profiles**: Individual work hour and break preferences
|
||||
- **Environment Variables**: Database and Flask configuration
|
||||
#### Environment Variables
|
||||
|
||||
```bash
|
||||
# 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
|
||||
|
||||
@@ -123,36 +201,152 @@ The application can be configured through:
|
||||
## Security Features
|
||||
|
||||
- **Two-Factor Authentication**: TOTP-based 2FA with QR code setup
|
||||
- **Role-Based Access Control**: Four distinct user roles with appropriate permissions
|
||||
- **Session Management**: Secure login/logout with "remember me" functionality
|
||||
- **Data Validation**: Comprehensive input validation and error handling
|
||||
- **Account Verification**: Email verification system for new accounts
|
||||
- **Password Reset**: Secure email-based recovery with time-limited tokens
|
||||
- **Role-Based Access Control**: Five distinct user roles with granular permissions
|
||||
- **Multi-Tenancy**: Complete data isolation between companies
|
||||
- **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
|
||||
|
||||
The application provides various endpoints for different user roles:
|
||||
- `/admin/*`: Administrative functions (Admin only)
|
||||
- `/supervisor/*`: Supervisor functions (Supervisor+ roles)
|
||||
- `/team/*`: Team management (Team Leader+ roles)
|
||||
- `/export/*`: Data export functionality
|
||||
- `/auth/*`: Authentication and profile management
|
||||
Key application routes organized by function:
|
||||
- `/` - Home dashboard
|
||||
- `/login`, `/logout`, `/register` - Authentication
|
||||
- `/forgot_password`, `/reset_password/<token>` - Password recovery
|
||||
- `/time-tracking` - Main time tracking interface
|
||||
- `/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
|
||||
- `models.py`: Database models and relationships
|
||||
- `templates/`: HTML templates for all pages
|
||||
- `static/`: CSS and JavaScript files
|
||||
- `migrate_*.py`: Legacy migration scripts (no longer needed)
|
||||
### Production Deployment with Docker
|
||||
|
||||
```bash
|
||||
# Clone and configure
|
||||
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
|
||||
|
||||
1. Fork the repository
|
||||
2. Create a feature branch
|
||||
3. Make your changes
|
||||
4. Test thoroughly
|
||||
5. Submit a pull request
|
||||
2. Create a feature branch (`git checkout -b feature/amazing-feature`)
|
||||
3. Commit your changes (`git commit -m 'Add amazing feature'`)
|
||||
4. Push to the branch (`git push origin feature/amazing-feature`)
|
||||
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
|
||||
|
||||
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
100
app.py
@@ -909,6 +909,106 @@ def verify_email(token):
|
||||
|
||||
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')
|
||||
@role_required(Role.TEAM_MEMBER)
|
||||
@company_required
|
||||
|
||||
@@ -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')
|
||||
@@ -48,6 +48,10 @@ class User(db.Model):
|
||||
|
||||
# Avatar field
|
||||
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
|
||||
time_entries = db.relationship('TimeEntry', backref='user', lazy=True)
|
||||
@@ -139,6 +143,28 @@ class User(db.Model):
|
||||
elif self.username:
|
||||
return self.username[:2].upper()
|
||||
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):
|
||||
return f'<User {self.username}>'
|
||||
|
||||
@@ -3,13 +3,13 @@ Security headers middleware for Flask.
|
||||
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):
|
||||
"""Add security headers to all responses."""
|
||||
|
||||
# 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
|
||||
response.headers['Strict-Transport-Security'] = 'max-age=31536000; includeSubDomains'
|
||||
|
||||
|
||||
135
templates/forgot_password.html
Normal file
135
templates/forgot_password.html
Normal 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 %}
|
||||
@@ -47,6 +47,12 @@
|
||||
<button type="submit" class="btn btn-primary">Sign In</button>
|
||||
</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">
|
||||
<span>New to {{ g.branding.app_name if g.branding else 'TimeTrack' }}?</span>
|
||||
</div>
|
||||
|
||||
291
templates/reset_password.html
Normal file
291
templates/reset_password.html
Normal 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 %}
|
||||
Reference in New Issue
Block a user