441 lines
17 KiB
Python
441 lines
17 KiB
Python
from flask import Blueprint, jsonify, request
|
|
from flask_login import login_required, current_user
|
|
from models import db, Room, RoomFile, User, DocuPulseSettings, HelpArticle
|
|
import os
|
|
from datetime import datetime
|
|
|
|
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/<int:article_id>', methods=['PUT'])
|
|
@login_required
|
|
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/<int:article_id>', methods=['DELETE'])
|
|
@login_required
|
|
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
|
|
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
|
|
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/<int:article_id>', methods=['GET'])
|
|
@login_required
|
|
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}) |