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
|
||||||
|
|
||||||
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
100
app.py
@@ -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
|
||||||
|
|||||||
@@ -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 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}>'
|
||||||
|
|||||||
@@ -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'
|
||||||
|
|
||||||
|
|||||||
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>
|
<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>
|
||||||
|
|||||||
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