Files
TimeTracker/app/models/api_token.py
Dries Peeters a1aaee6afd feat: Redesign and enhance backup restore functionality with dual restore methods
Major improvements to the backup restore system with a complete UI overhaul
and enhanced functionality:

UI/UX Improvements:
- Complete redesign of restore page with modern Tailwind CSS
- Added prominent warning banners and danger badges to prevent accidental data loss
- Implemented drag-and-drop file upload with visual feedback
- Added real-time progress tracking with auto-refresh every 2 seconds
- Added comprehensive safety information sidebar with checklists
- Full dark mode support throughout restore interface
- Enhanced confirmation flows with checkbox and modal confirmations

Functionality Enhancements:
- Added dual restore methods: upload new backup or restore from existing server backups
- Enhanced restore route to accept optional filename parameter for existing backups
- Added "Restore" button to each backup in the backups management page
- Implemented restore confirmation modal with critical warnings
- Added loading states and button disabling during restore operations
- Improved error handling and user feedback

Backend Changes:
- Enhanced admin.restore() to support both file upload and existing backup restore
- Added dual route support: /admin/restore and /admin/restore/<filename>
- Added shutil import for file copy operations during restore
- Improved security with secure_filename validation and file type checking
- Maintained existing rate limiting (3 requests per minute)

Frontend Improvements:
- Added interactive JavaScript for file selection, drag-and-drop, and modal management
- Implemented auto-refresh during restore process to show live progress
- Added escape key support for closing modals
- Enhanced user feedback with file name display and button states

Safety Features:
- Pre-restore checklist with 5 verification steps
- Multiple warning levels throughout the flow
- Confirmation checkbox required before upload restore
- Modal confirmation required before existing backup restore
- Clear documentation of what gets restored and post-restore steps

Dependencies:
- Updated flask-swagger-ui from 4.11.1 to 5.21.0

Files modified:
- app/templates/admin/restore.html (complete rewrite)
- app/templates/admin/backups.html (added restore functionality)
- app/routes/admin.py (enhanced restore route)
- requirements.txt (updated flask-swagger-ui version)
- RESTORE_BACKUP_IMPROVEMENTS.md (documentation)

This provides a significantly improved user experience for the restore process
while maintaining security and adding powerful new restore capabilities.
2025-10-27 09:34:51 +01:00

154 lines
5.2 KiB
Python

"""API Token model for REST API authentication"""
import secrets
from datetime import datetime, timedelta
from app import db
from sqlalchemy.orm import relationship
class ApiToken(db.Model):
"""API Token for authenticating REST API requests"""
__tablename__ = 'api_tokens'
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(100), nullable=False)
description = db.Column(db.Text)
token_hash = db.Column(db.String(128), unique=True, nullable=False, index=True)
token_prefix = db.Column(db.String(10), nullable=False) # First 8 chars for identification
# Ownership and permissions
user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
user = relationship('User', backref='api_tokens')
# Scopes for fine-grained permissions (comma-separated)
# Examples: read:projects, write:time_entries, admin:all
scopes = db.Column(db.Text, default='')
# Token lifecycle
created_at = db.Column(db.DateTime, nullable=False, default=datetime.utcnow)
expires_at = db.Column(db.DateTime)
last_used_at = db.Column(db.DateTime)
is_active = db.Column(db.Boolean, default=True, nullable=False)
# IP restrictions (comma-separated list of allowed IPs/CIDR blocks)
ip_whitelist = db.Column(db.Text)
# Usage tracking
usage_count = db.Column(db.Integer, default=0, nullable=False)
def __repr__(self):
return f'<ApiToken {self.name} ({self.token_prefix}...)>'
@staticmethod
def generate_token():
"""Generate a new secure random token"""
# Format: tt_<32 random chars>
random_part = secrets.token_urlsafe(32)[:32]
return f"tt_{random_part}"
@staticmethod
def hash_token(token):
"""Hash a token for storage"""
import hashlib
return hashlib.sha256(token.encode()).hexdigest()
@classmethod
def create_token(cls, user_id, name, description='', scopes='', expires_days=None):
"""Create a new API token
Args:
user_id: User ID who owns this token
name: Human-readable name for the token
description: Optional description
scopes: Comma-separated list of scopes
expires_days: Number of days until expiration (None = never expires)
Returns:
tuple: (ApiToken instance, plain_token)
"""
plain_token = cls.generate_token()
token_hash = cls.hash_token(plain_token)
token_prefix = plain_token[:8]
expires_at = None
if expires_days:
expires_at = datetime.utcnow() + timedelta(days=expires_days)
api_token = cls(
name=name,
description=description,
token_hash=token_hash,
token_prefix=token_prefix,
user_id=user_id,
scopes=scopes,
expires_at=expires_at
)
return api_token, plain_token
def verify_token(self, plain_token):
"""Verify if the provided token matches this record"""
return self.token_hash == self.hash_token(plain_token)
def is_valid(self):
"""Check if token is valid (active and not expired)"""
if not self.is_active:
return False
if self.expires_at and self.expires_at < datetime.utcnow():
return False
return True
def has_scope(self, required_scope):
"""Check if token has a specific scope
Args:
required_scope: The scope to check (e.g., 'read:projects')
Returns:
bool: True if token has the scope
"""
if not self.scopes:
return False
token_scopes = [s.strip() for s in self.scopes.split(',')]
# Check for wildcard admin scope
if 'admin:all' in token_scopes or '*' in token_scopes:
return True
# Check for exact match
if required_scope in token_scopes:
return True
# Check for wildcard resource scope (e.g., read:* matches read:projects)
resource_type = required_scope.split(':')[0] if ':' in required_scope else None
if resource_type and f"{resource_type}:*" in token_scopes:
return True
return False
def record_usage(self, ip_address=None):
"""Record token usage"""
self.last_used_at = datetime.utcnow()
self.usage_count += 1
db.session.commit()
def to_dict(self, include_token=False):
"""Convert to dictionary for API responses"""
data = {
'id': self.id,
'name': self.name,
'description': self.description,
'token_prefix': self.token_prefix,
'scopes': self.scopes.split(',') if self.scopes else [],
'created_at': self.created_at.isoformat() if self.created_at else None,
'expires_at': self.expires_at.isoformat() if self.expires_at else None,
'last_used_at': self.last_used_at.isoformat() if self.last_used_at else None,
'is_active': self.is_active,
'usage_count': self.usage_count,
'user_id': self.user_id
}
return data