Merge branch 'dev' of https://git.immortal.host/clubesportsudes/team-tryouts into dev
This commit is contained in:
+67
-45
@@ -19,17 +19,16 @@ from app.models import (
|
||||
Admin, User, Tryout, OrgTeam, TeamPlayer, TeamMatch,
|
||||
TeamMatchParticipant, AppSettings, BackupRecord, AuditLog,
|
||||
)
|
||||
from app.supporting_scripts import backup as backup_helper
|
||||
from app.supporting_scripts.backup import (
|
||||
BACKUP_DIR, BackupError, backup_database, backup_documents,
|
||||
create_backup_dir, parse_database_url, verify_backup,
|
||||
)
|
||||
|
||||
admin_bp = Blueprint('admin', __name__, url_prefix='/admin')
|
||||
|
||||
|
||||
def require_admin():
|
||||
"""Return True if current user is an Admin, else flash and redirect.
|
||||
|
||||
This returns False for non-admins so the caller can stop processing,
|
||||
but the redirect/abort should be handled by the caller.
|
||||
"""
|
||||
"""Return True if current user is an Admin, else flash and redirect."""
|
||||
if isinstance(current_user, Admin):
|
||||
return True
|
||||
flash('Only the president can access the admin panel.', 'danger')
|
||||
@@ -96,6 +95,27 @@ def dashboard():
|
||||
# Backups
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _create_backup_record(backup_type='manual', notes=None):
|
||||
"""Run a database backup and return a BackupRecord, or raise BackupError."""
|
||||
create_backup_dir()
|
||||
conn = parse_database_url(os.getenv('DATABASE_URL'))
|
||||
file_path = backup_database(conn)
|
||||
size = os.path.getsize(file_path) if os.path.exists(file_path) else 0
|
||||
filename = os.path.basename(file_path)
|
||||
|
||||
record = BackupRecord(
|
||||
filename=filename,
|
||||
file_path=file_path,
|
||||
size_bytes=size,
|
||||
backup_type=backup_type,
|
||||
notes=notes,
|
||||
created_by_id=current_user.id,
|
||||
)
|
||||
db.session.add(record)
|
||||
db.session.commit()
|
||||
return record
|
||||
|
||||
|
||||
@admin_bp.route('/backup/create', methods=['POST'])
|
||||
@login_required
|
||||
def create_backup():
|
||||
@@ -105,25 +125,14 @@ def create_backup():
|
||||
|
||||
notes = request.form.get('notes', '').strip() or None
|
||||
try:
|
||||
result = backup_helper.create_backup(backup_type='manual', notes=notes)
|
||||
except RuntimeError as e:
|
||||
record = _create_backup_record(backup_type='manual', notes=notes)
|
||||
except BackupError as e:
|
||||
flash(f'Backup failed: {e}', 'danger')
|
||||
_log('backup_failed', f'Error: {e}')
|
||||
return redirect(url_for('admin.dashboard'))
|
||||
|
||||
record = BackupRecord(
|
||||
filename=result['filename'],
|
||||
file_path=result['file_path'],
|
||||
size_bytes=result['size_bytes'],
|
||||
backup_type='manual',
|
||||
notes=notes,
|
||||
created_by_id=current_user.id,
|
||||
)
|
||||
db.session.add(record)
|
||||
db.session.commit()
|
||||
|
||||
_log('backup_created', f'File: {result["filename"]} ({result["size_bytes"]} bytes)')
|
||||
flash(f'Backup created successfully: {result["filename"]}', 'success')
|
||||
_log('backup_created', f'File: {record.filename} ({record.size_bytes} bytes)')
|
||||
flash(f'Backup created successfully: {record.filename}', 'success')
|
||||
return redirect(url_for('admin.dashboard'))
|
||||
|
||||
|
||||
@@ -171,7 +180,7 @@ def restore_backup(backup_id):
|
||||
"""Restore a selected backup.
|
||||
|
||||
A safety backup of the current state is created first, then the
|
||||
selected dump is restored via psql.
|
||||
selected dump is restored via pg_restore.
|
||||
"""
|
||||
if not require_admin():
|
||||
return redirect(url_for('main.dashboard'))
|
||||
@@ -183,30 +192,50 @@ def restore_backup(backup_id):
|
||||
|
||||
# Create a safety backup of the current state before restoring.
|
||||
try:
|
||||
safety = backup_helper.create_backup(backup_type='pre_restore', notes='Pre-restore safety backup')
|
||||
safety_record = BackupRecord(
|
||||
filename=safety['filename'],
|
||||
file_path=safety['file_path'],
|
||||
size_bytes=safety['size_bytes'],
|
||||
backup_type='auto',
|
||||
safety = _create_backup_record(
|
||||
backup_type='pre_restore',
|
||||
notes='Automatic safety backup before restoring ' + record.filename,
|
||||
created_by_id=current_user.id,
|
||||
)
|
||||
db.session.add(safety_record)
|
||||
db.session.commit()
|
||||
except RuntimeError as e:
|
||||
except BackupError as e:
|
||||
flash(f'Could not create safety backup, restore aborted: {e}', 'danger')
|
||||
return redirect(url_for('admin.dashboard'))
|
||||
|
||||
# Restore using pg_restore
|
||||
import subprocess
|
||||
from app.supporting_scripts.backup import (
|
||||
PG_RESTORE, dump_environment, parse_database_url,
|
||||
)
|
||||
|
||||
conn = parse_database_url(os.getenv('DATABASE_URL'))
|
||||
cmd = [
|
||||
PG_RESTORE,
|
||||
'--host', conn['host'],
|
||||
'--port', conn['port'],
|
||||
'--username', conn['user'],
|
||||
'--dbname', conn['dbname'],
|
||||
'--clean', '--if-exists', '--no-owner',
|
||||
record.file_path,
|
||||
]
|
||||
try:
|
||||
backup_helper.restore_backup(record.file_path)
|
||||
except RuntimeError as e:
|
||||
result = subprocess.run(
|
||||
cmd,
|
||||
env=dump_environment(conn),
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=900,
|
||||
)
|
||||
if result.returncode != 0:
|
||||
raise RuntimeError(result.stderr.strip() or 'pg_restore failed')
|
||||
except (subprocess.TimeoutExpired, FileNotFoundError, RuntimeError) as e:
|
||||
flash(f'Restore failed: {e}', 'danger')
|
||||
_log('backup_restore_failed', f'File: {record.filename}, Error: {e}')
|
||||
return redirect(url_for('admin.dashboard'))
|
||||
|
||||
_log('backup_restored', f'File: {record.filename}')
|
||||
flash(f'Backup {record.filename} restored successfully. The database has been rolled back.', 'success')
|
||||
flash(
|
||||
f'Backup {record.filename} restored successfully. The database has been rolled back.',
|
||||
'success',
|
||||
)
|
||||
return redirect(url_for('admin.dashboard'))
|
||||
|
||||
|
||||
@@ -322,18 +351,11 @@ def wipe_teams():
|
||||
|
||||
# Safety backup before destructive operation
|
||||
try:
|
||||
safety = backup_helper.create_backup(backup_type='pre_wipe', notes='Pre-wipe safety backup')
|
||||
safety_record = BackupRecord(
|
||||
filename=safety['filename'],
|
||||
file_path=safety['file_path'],
|
||||
size_bytes=safety['size_bytes'],
|
||||
backup_type='auto',
|
||||
_create_backup_record(
|
||||
backup_type='pre_wipe',
|
||||
notes='Automatic safety backup before wiping teams',
|
||||
created_by_id=current_user.id,
|
||||
)
|
||||
db.session.add(safety_record)
|
||||
db.session.commit()
|
||||
except RuntimeError as e:
|
||||
except BackupError as e:
|
||||
flash(f'Wipe aborted — could not create safety backup: {e}', 'danger')
|
||||
return redirect(url_for('admin.dashboard'))
|
||||
|
||||
|
||||
Reference in New Issue
Block a user