mirror of
https://github.com/DRYTRIX/TimeTracker.git
synced 2026-01-07 03:59:48 -06:00
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.
154 lines
5.2 KiB
Python
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
|
|
|