from flask import Blueprint, jsonify, request from flask_login import login_required, current_user from models import db, Room, RoomFile, User, DocuPulseSettings, HelpArticle from extensions import csrf from utils.event_logger import log_event import os from datetime import datetime import json admin = Blueprint('admin', __name__) @admin.route('/api/admin/sync-files', methods=['POST']) @login_required def sync_files(): if not current_user.is_admin: return jsonify({'error': 'Unauthorized'}), 403 try: DATA_ROOT = '/app/uploads/rooms' admin_user = User.query.filter_by(is_admin=True).first() if not admin_user: return jsonify({'error': 'No admin user found'}), 500 rooms = Room.query.all() for room in rooms: room_dir = os.path.join(DATA_ROOT, str(room.id)) if not os.path.exists(room_dir): continue for root, dirs, files in os.walk(room_dir): rel_root = os.path.relpath(root, room_dir) rel_path = '' if rel_root == '.' else rel_root.replace('\\', '/') # Folders for d in dirs: exists = RoomFile.query.filter_by(room_id=room.id, name=d, path=rel_path, type='folder').first() folder_path = os.path.join(root, d) stat = os.stat(folder_path) if not exists: rf = RoomFile( room_id=room.id, name=d, path=rel_path, type='folder', size=None, modified=stat.st_mtime, uploaded_by=admin_user.id, uploaded_at=datetime.utcfromtimestamp(stat.st_mtime) ) db.session.add(rf) # Files for f in files: exists = RoomFile.query.filter_by(room_id=room.id, name=f, path=rel_path, type='file').first() file_path = os.path.join(root, f) stat = os.stat(file_path) if not exists: rf = RoomFile( room_id=room.id, name=f, path=rel_path, type='file', size=stat.st_size, modified=stat.st_mtime, uploaded_by=admin_user.id, uploaded_at=datetime.utcfromtimestamp(stat.st_mtime) ) db.session.add(rf) db.session.commit() return jsonify({'success': True, 'message': 'File system synchronized successfully'}) except Exception as e: db.session.rollback() return jsonify({'error': str(e)}), 500 @admin.route('/api/admin/verify-db-state', methods=['GET']) @login_required def verify_db_state(): if not current_user.is_admin: return jsonify({'error': 'Unauthorized'}), 403 try: DATA_ROOT = '/app/uploads/rooms' verification_results = { 'rooms_checked': 0, 'files_in_db_not_fs': [], 'files_in_fs_not_db': [], 'permission_mismatches': [], 'size_mismatches': [], 'modified_time_mismatches': [], 'total_files_checked': 0, 'total_folders_checked': 0 } rooms = Room.query.all() for room in rooms: verification_results['rooms_checked'] += 1 room_dir = os.path.join(DATA_ROOT, str(room.id)) # Get all files and folders from database for this room db_files = RoomFile.query.filter_by(room_id=room.id, deleted=False).all() db_paths = {(f.path, f.name): f for f in db_files} # Check filesystem if directory exists if os.path.exists(room_dir): for root, dirs, files in os.walk(room_dir): rel_root = os.path.relpath(root, room_dir) rel_path = '' if rel_root == '.' else rel_root.replace('\\', '/') # Check folders for d in dirs: verification_results['total_folders_checked'] += 1 folder_path = os.path.join(root, d) stat = os.stat(folder_path) # Check if folder exists in database db_file = db_paths.get((rel_path, d)) if not db_file: verification_results['files_in_fs_not_db'].append({ 'room_id': room.id, 'room_name': room.name, 'path': rel_path, 'name': d, 'type': 'folder' }) else: # Verify folder metadata if abs(stat.st_mtime - db_file.modified) > 1: # Allow 1 second difference verification_results['modified_time_mismatches'].append({ 'room_id': room.id, 'room_name': room.name, 'path': rel_path, 'name': d, 'type': 'folder', 'fs_modified': stat.st_mtime, 'db_modified': db_file.modified }) # Remove from db_paths as we've checked it db_paths.pop((rel_path, d), None) # Check files for f in files: verification_results['total_files_checked'] += 1 file_path = os.path.join(root, f) stat = os.stat(file_path) # Check if file exists in database db_file = db_paths.get((rel_path, f)) if not db_file: verification_results['files_in_fs_not_db'].append({ 'room_id': room.id, 'room_name': room.name, 'path': rel_path, 'name': f, 'type': 'file' }) else: # Verify file metadata if abs(stat.st_mtime - db_file.modified) > 1: # Allow 1 second difference verification_results['modified_time_mismatches'].append({ 'room_id': room.id, 'room_name': room.name, 'path': rel_path, 'name': f, 'type': 'file', 'fs_modified': stat.st_mtime, 'db_modified': db_file.modified }) if stat.st_size != db_file.size: verification_results['size_mismatches'].append({ 'room_id': room.id, 'room_name': room.name, 'path': rel_path, 'name': f, 'type': 'file', 'fs_size': stat.st_size, 'db_size': db_file.size }) # Remove from db_paths as we've checked it db_paths.pop((rel_path, f), None) # Any remaining items in db_paths are in DB but not in filesystem for (path, name), db_file in db_paths.items(): verification_results['files_in_db_not_fs'].append({ 'room_id': room.id, 'room_name': room.name, 'path': path, 'name': name, 'type': db_file.type }) # Calculate total issues total_issues = ( len(verification_results['files_in_db_not_fs']) + len(verification_results['files_in_fs_not_db']) + len(verification_results['permission_mismatches']) + len(verification_results['size_mismatches']) + len(verification_results['modified_time_mismatches']) ) # Add summary statistics verification_results['summary'] = { 'total_issues': total_issues, 'status': 'healthy' if total_issues == 0 else 'issues_found' } return jsonify(verification_results) except Exception as e: return jsonify({'error': str(e)}), 500 @admin.route('/api/admin/cleanup-orphaned-records', methods=['POST']) @login_required def cleanup_orphaned_records(): if not current_user.is_admin: return jsonify({'error': 'Unauthorized'}), 403 try: DATA_ROOT = '/app/uploads/rooms' rooms = Room.query.all() cleaned_records = [] for room in rooms: room_dir = os.path.join(DATA_ROOT, str(room.id)) # Get all files and folders from database for this room db_files = RoomFile.query.filter_by(room_id=room.id, deleted=False).all() for db_file in db_files: file_path = os.path.join(room_dir, db_file.path, db_file.name) if db_file.path else os.path.join(room_dir, db_file.name) # If file doesn't exist in filesystem, mark it as deleted in database if not os.path.exists(file_path): db_file.deleted = True cleaned_records.append({ 'room_id': room.id, 'room_name': room.name, 'path': db_file.path, 'name': db_file.name, 'type': db_file.type }) db.session.commit() return jsonify({ 'success': True, 'message': f'Cleaned up {len(cleaned_records)} orphaned records', 'cleaned_records': cleaned_records }) except Exception as e: db.session.rollback() return jsonify({'error': str(e)}), 500 @admin.route('/api/admin/usage-stats', methods=['GET']) @login_required def get_usage_stats(): if not current_user.is_admin: return jsonify({'error': 'Unauthorized'}), 403 try: stats = DocuPulseSettings.get_usage_stats() return jsonify(stats) except Exception as e: return jsonify({'error': str(e)}), 500 @admin.route('/api/admin/help-articles/', methods=['PUT']) @login_required @csrf.exempt def update_help_article(article_id): """Update a help article""" if not current_user.is_admin: return jsonify({'error': 'Unauthorized'}), 403 try: article = HelpArticle.query.get_or_404(article_id) title = request.form.get('title') category = request.form.get('category') body = request.form.get('body') order_index = int(request.form.get('order_index', 0)) is_published = request.form.get('is_published') == 'true' if not title or not category or not body: return jsonify({'error': 'Title, category, and body are required'}), 400 # Validate category valid_categories = HelpArticle.get_categories().keys() if category not in valid_categories: return jsonify({'error': 'Invalid category'}), 400 article.title = title article.category = category article.body = body article.order_index = order_index article.is_published = is_published article.updated_at = datetime.utcnow() db.session.commit() # Log the event log_event( event_type='help_article_update', details={ 'article_id': article.id, 'title': article.title, 'category': article.category, 'updated_by': f"{current_user.username} {current_user.last_name}" }, user_id=current_user.id ) db.session.commit() return jsonify({'success': True, 'message': 'Article updated successfully'}) except Exception as e: db.session.rollback() return jsonify({'error': str(e)}), 500 @admin.route('/api/admin/help-articles/', methods=['DELETE']) @login_required @csrf.exempt def delete_help_article(article_id): """Delete a help article""" if not current_user.is_admin: return jsonify({'error': 'Unauthorized'}), 403 try: article = HelpArticle.query.get_or_404(article_id) # Log the event before deletion log_event( event_type='help_article_delete', details={ 'article_id': article.id, 'title': article.title, 'category': article.category, 'deleted_by': f"{current_user.username} {current_user.last_name}" }, user_id=current_user.id ) db.session.delete(article) db.session.commit() return jsonify({'success': True, 'message': 'Article deleted successfully'}) except Exception as e: db.session.rollback() return jsonify({'error': str(e)}), 500 # Help Articles API endpoints @admin.route('/api/admin/help-articles', methods=['GET']) @login_required @csrf.exempt def get_help_articles(): """Get all help articles""" if not current_user.is_admin: return jsonify({'error': 'Unauthorized'}), 403 articles = HelpArticle.query.order_by(HelpArticle.category.asc(), HelpArticle.order_index.asc(), HelpArticle.created_at.desc()).all() articles_data = [] for article in articles: articles_data.append({ 'id': article.id, 'title': article.title, 'category': article.category, 'body': article.body, 'created_at': article.created_at.isoformat() if article.created_at else None, 'updated_at': article.updated_at.isoformat() if article.updated_at else None, 'created_by': article.created_by, 'is_published': article.is_published, 'order_index': article.order_index }) return jsonify({'articles': articles_data}) @admin.route('/api/admin/help-articles', methods=['POST']) @login_required @csrf.exempt def create_help_article(): """Create a new help article""" if not current_user.is_admin: return jsonify({'error': 'Unauthorized'}), 403 try: title = request.form.get('title') category = request.form.get('category') body = request.form.get('body') order_index = int(request.form.get('order_index', 0)) is_published = request.form.get('is_published') == 'true' if not title or not category or not body: return jsonify({'error': 'Title, category, and body are required'}), 400 # Validate category valid_categories = HelpArticle.get_categories().keys() if category not in valid_categories: return jsonify({'error': 'Invalid category'}), 400 article = HelpArticle( title=title, category=category, body=body, order_index=order_index, is_published=is_published, created_by=current_user.id ) db.session.add(article) db.session.commit() # Log the event log_event( event_type='help_article_create', details={ 'article_id': article.id, 'title': article.title, 'category': article.category, 'created_by': f"{current_user.username} {current_user.last_name}" }, user_id=current_user.id ) db.session.commit() return jsonify({'success': True, 'message': 'Article created successfully', 'article_id': article.id}) except Exception as e: db.session.rollback() return jsonify({'error': str(e)}), 500 @admin.route('/api/admin/help-articles/', methods=['GET']) @login_required @csrf.exempt def get_help_article(article_id): """Get a specific help article""" if not current_user.is_admin: return jsonify({'error': 'Unauthorized'}), 403 article = HelpArticle.query.get_or_404(article_id) article_data = { 'id': article.id, 'title': article.title, 'category': article.category, 'body': article.body, 'created_at': article.created_at.isoformat() if article.created_at else None, 'updated_at': article.updated_at.isoformat() if article.updated_at else None, 'created_by': article.created_by, 'is_published': article.is_published, 'order_index': article.order_index } return jsonify({'article': article_data}) @admin.route('/api/admin/pricing-plans', methods=['POST']) @login_required def create_pricing_plan(): if not current_user.is_admin: return jsonify({'error': 'Unauthorized'}), 403 # Check if this is a MASTER instance is_master = os.environ.get('MASTER', 'false').lower() == 'true' if not is_master: return jsonify({'error': 'Pricing configuration is only available on MASTER instances'}), 403 try: from models import PricingPlan # Get form data name = request.form.get('name') description = request.form.get('description') monthly_price = float(request.form.get('monthly_price')) annual_price = float(request.form.get('annual_price')) features = json.loads(request.form.get('features', '[]')) button_text = request.form.get('button_text', 'Get Started') monthly_stripe_link = request.form.get('monthly_stripe_link', '') annual_stripe_link = request.form.get('annual_stripe_link', '') is_popular = request.form.get('is_popular') == 'true' is_custom = request.form.get('is_custom') == 'true' is_active = request.form.get('is_active') == 'true' # Get quota fields room_quota = int(request.form.get('room_quota', 0)) conversation_quota = int(request.form.get('conversation_quota', 0)) storage_quota_gb = int(request.form.get('storage_quota_gb', 0)) manager_quota = int(request.form.get('manager_quota', 0)) admin_quota = int(request.form.get('admin_quota', 0)) # Validate required fields if not name or not features: return jsonify({'error': 'Name and features are required'}), 400 # Get the highest order index max_order = db.session.query(db.func.max(PricingPlan.order_index)).scalar() or 0 # Create new pricing plan plan = PricingPlan( name=name, description=description, monthly_price=monthly_price, annual_price=annual_price, features=features, button_text=button_text, monthly_stripe_link=monthly_stripe_link, annual_stripe_link=annual_stripe_link, is_popular=is_popular, is_custom=is_custom, is_active=is_active, room_quota=room_quota, conversation_quota=conversation_quota, storage_quota_gb=storage_quota_gb, manager_quota=manager_quota, admin_quota=admin_quota, order_index=max_order + 1, created_by=current_user.id ) db.session.add(plan) db.session.commit() return jsonify({'success': True, 'message': 'Pricing plan created successfully'}) except Exception as e: db.session.rollback() return jsonify({'error': str(e)}), 500 @admin.route('/api/admin/pricing-plans/', methods=['GET']) @login_required def get_pricing_plan(plan_id): if not current_user.is_admin: return jsonify({'error': 'Unauthorized'}), 403 try: from models import PricingPlan plan = PricingPlan.query.get(plan_id) if not plan: return jsonify({'error': 'Pricing plan not found'}), 404 return jsonify({ 'success': True, 'plan': { 'id': plan.id, 'name': plan.name, 'description': plan.description, 'monthly_price': plan.monthly_price, 'annual_price': plan.annual_price, 'features': plan.features, 'button_text': plan.button_text, 'button_url': plan.button_url, 'is_popular': plan.is_popular, 'is_custom': plan.is_custom, 'is_active': plan.is_active, 'order_index': plan.order_index, 'room_quota': plan.room_quota, 'conversation_quota': plan.conversation_quota, 'storage_quota_gb': plan.storage_quota_gb, 'manager_quota': plan.manager_quota, 'admin_quota': plan.admin_quota } }) except Exception as e: return jsonify({'error': str(e)}), 500 @admin.route('/api/admin/pricing-plans/', methods=['PUT']) @login_required def update_pricing_plan(plan_id): if not current_user.is_admin: return jsonify({'error': 'Unauthorized'}), 403 # Check if this is a MASTER instance is_master = os.environ.get('MASTER', 'false').lower() == 'true' if not is_master: return jsonify({'error': 'Pricing configuration is only available on MASTER instances'}), 403 try: from models import PricingPlan plan = PricingPlan.query.get(plan_id) if not plan: return jsonify({'error': 'Pricing plan not found'}), 404 # Get form data name = request.form.get('name') description = request.form.get('description') monthly_price = float(request.form.get('monthly_price')) annual_price = float(request.form.get('annual_price')) features = json.loads(request.form.get('features', '[]')) button_text = request.form.get('button_text', 'Get Started') monthly_stripe_link = request.form.get('monthly_stripe_link', '') annual_stripe_link = request.form.get('annual_stripe_link', '') is_popular = request.form.get('is_popular') == 'true' is_custom = request.form.get('is_custom') == 'true' is_active = request.form.get('is_active') == 'true' # Get quota fields room_quota = int(request.form.get('room_quota', 0)) conversation_quota = int(request.form.get('conversation_quota', 0)) storage_quota_gb = int(request.form.get('storage_quota_gb', 0)) manager_quota = int(request.form.get('manager_quota', 0)) admin_quota = int(request.form.get('admin_quota', 0)) # Validate required fields if not name or not features: return jsonify({'error': 'Name and features are required'}), 400 # Update plan plan.name = name plan.description = description plan.monthly_price = monthly_price plan.annual_price = annual_price plan.features = features plan.button_text = button_text plan.monthly_stripe_link = monthly_stripe_link plan.annual_stripe_link = annual_stripe_link plan.is_popular = is_popular plan.is_custom = is_custom plan.is_active = is_active plan.room_quota = room_quota plan.conversation_quota = conversation_quota plan.storage_quota_gb = storage_quota_gb plan.manager_quota = manager_quota plan.admin_quota = admin_quota db.session.commit() return jsonify({'success': True, 'message': 'Pricing plan updated successfully'}) except Exception as e: db.session.rollback() return jsonify({'error': str(e)}), 500 @admin.route('/api/admin/pricing-plans/', methods=['DELETE']) @login_required def delete_pricing_plan(plan_id): if not current_user.is_admin: return jsonify({'error': 'Unauthorized'}), 403 # Check if this is a MASTER instance is_master = os.environ.get('MASTER', 'false').lower() == 'true' if not is_master: return jsonify({'error': 'Pricing configuration is only available on MASTER instances'}), 403 try: from models import PricingPlan plan = PricingPlan.query.get(plan_id) if not plan: return jsonify({'error': 'Pricing plan not found'}), 404 db.session.delete(plan) db.session.commit() return jsonify({'success': True, 'message': 'Pricing plan deleted successfully'}) except Exception as e: db.session.rollback() return jsonify({'error': str(e)}), 500 @admin.route('/api/admin/pricing-plans//status', methods=['PATCH']) @login_required def update_pricing_plan_status(plan_id): if not current_user.is_admin: return jsonify({'error': 'Unauthorized'}), 403 # Check if this is a MASTER instance is_master = os.environ.get('MASTER', 'false').lower() == 'true' if not is_master: return jsonify({'error': 'Pricing configuration is only available on MASTER instances'}), 403 try: from models import PricingPlan plan = PricingPlan.query.get(plan_id) if not plan: return jsonify({'error': 'Pricing plan not found'}), 404 data = request.get_json() field = data.get('field') value = data.get('value') if field not in ['is_active', 'is_popular', 'is_custom']: return jsonify({'error': 'Invalid field'}), 400 setattr(plan, field, value) db.session.commit() return jsonify({'success': True, 'message': 'Plan status updated successfully'}) except Exception as e: db.session.rollback() return jsonify({'error': str(e)}), 500 @admin.route('/api/admin/pricing-plans', methods=['GET']) @login_required def get_pricing_plans(): """Get all active pricing plans for instance launch""" if not current_user.is_admin: return jsonify({'error': 'Unauthorized'}), 403 try: from models import PricingPlan # Get all active pricing plans ordered by order_index plans = PricingPlan.query.filter_by(is_active=True).order_by(PricingPlan.order_index).all() plans_data = [] for plan in plans: plans_data.append({ 'id': plan.id, 'name': plan.name, 'description': plan.description, 'monthly_price': plan.monthly_price, 'annual_price': plan.annual_price, 'features': plan.features, 'button_text': plan.button_text, 'button_url': plan.button_url, 'is_popular': plan.is_popular, 'is_custom': plan.is_custom, 'is_active': plan.is_active, 'order_index': plan.order_index, 'room_quota': plan.room_quota, 'conversation_quota': plan.conversation_quota, 'storage_quota_gb': plan.storage_quota_gb, 'manager_quota': plan.manager_quota, 'admin_quota': plan.admin_quota, 'format_quota_display': { 'room_quota': plan.format_quota_display('room_quota'), 'conversation_quota': plan.format_quota_display('conversation_quota'), 'storage_quota_gb': plan.format_quota_display('storage_quota_gb'), 'manager_quota': plan.format_quota_display('manager_quota'), 'admin_quota': plan.format_quota_display('admin_quota') } }) return jsonify({ 'success': True, 'plans': plans_data }) except Exception as e: return jsonify({'error': str(e)}), 500