""" aiohttp routers for ZMB Webui API Simplified compared to FastAPI """ import json import logging from aiohttp import web from datetime import datetime, timedelta from services.auth import auth_service from services.zfs_runner import zfs_runner logger = logging.getLogger(__name__) def verify_token(request): """Extract and verify JWT token from Authorization header""" auth_header = request.headers.get('Authorization', '') if not auth_header.startswith('Bearer '): return None token = auth_header[7:] # Remove 'Bearer ' prefix username = auth_service.verify_token(token) return username def require_auth(func): """Decorator to require authentication""" async def wrapper(request): username = verify_token(request) if not username: return web.json_response( {"detail": "Invalid token"}, status=401 ) request['username'] = username return await func(request) wrapper.__name__ = func.__name__ # Preserve function name return wrapper # ============ AUTH ROUTER ============ class AuthRouter: @staticmethod async def login(request): """POST /api/auth/login - Login with username/password""" try: data = await request.json() except Exception: return web.json_response( {"detail": "Invalid JSON"}, status=400 ) username = data.get('username') password = data.get('password') if not username or not password: return web.json_response( {"detail": "Username and password required"}, status=400 ) user = auth_service.authenticate_user(username, password) if not user: return web.json_response( {"detail": "Invalid credentials"}, status=401 ) token = auth_service.create_access_token(username) return web.json_response({ "access_token": token, "token_type": "bearer" }) @staticmethod async def verify(request): """POST /api/auth/verify - Verify token""" username = verify_token(request) if not username: return web.json_response( {"detail": "Invalid token"}, status=401 ) return web.json_response({ "valid": True, "username": username }) auth = AuthRouter() # ============ POOLS ROUTER ============ class PoolsRouter: @staticmethod @require_auth async def list_pools(request): """GET /api/pools - List all pools""" try: pools = zfs_runner.list_pools() return web.json_response(pools) except Exception as e: logger.error(f"Error listing pools: {e}") return web.json_response( {"detail": str(e)}, status=500 ) @staticmethod @require_auth async def get_pool_status(request): """GET /api/pools/{name} - Get pool status""" name = request.match_info['name'] try: status = zfs_runner.get_pool_status(name) return web.json_response(status) except Exception as e: logger.error(f"Error getting pool status: {e}") return web.json_response( {"detail": str(e)}, status=500 ) @staticmethod @require_auth async def start_scrub(request): """POST /api/pools/{name}/scrub - Start scrub""" name = request.match_info['name'] try: zfs_runner.start_scrub(name) return web.json_response({"status": "scrub started"}) except Exception as e: logger.error(f"Error starting scrub: {e}") return web.json_response( {"detail": str(e)}, status=500 ) pools = PoolsRouter() # ============ DATASETS ROUTER ============ class DatasetsRouter: @staticmethod @require_auth async def list_datasets(request): """GET /api/datasets - List datasets""" pool = request.query.get('pool', 'tank') try: datasets = zfs_runner.list_datasets(pool) return web.json_response(datasets) except Exception as e: logger.error(f"Error listing datasets: {e}") return web.json_response( {"detail": str(e)}, status=500 ) @staticmethod @require_auth async def update_dataset(request): """PATCH /api/datasets/{name} - Update dataset properties""" name = request.match_info['name'] try: data = await request.json() except Exception: return web.json_response( {"detail": "Invalid JSON"}, status=400 ) try: zfs_runner.set_dataset_properties(name, data) return web.json_response({"status": "updated"}) except Exception as e: logger.error(f"Error updating dataset: {e}") return web.json_response( {"detail": str(e)}, status=500 ) datasets = DatasetsRouter() # ============ SNAPSHOTS ROUTER ============ class SnapshotsRouter: @staticmethod @require_auth async def list_snapshots(request): """GET /api/snapshots - List snapshots""" dataset = request.query.get('dataset') limit = int(request.query.get('limit', 50)) try: snapshots = zfs_runner.list_snapshots(dataset, limit) return web.json_response(snapshots) except Exception as e: logger.error(f"Error listing snapshots: {e}") return web.json_response( {"detail": str(e)}, status=500 ) @staticmethod @require_auth async def create_snapshot(request): """POST /api/snapshots - Create snapshot""" try: data = await request.json() except Exception: return web.json_response( {"detail": "Invalid JSON"}, status=400 ) dataset = data.get('dataset') snapshot_name = data.get('snapshot_name') if not dataset or not snapshot_name: return web.json_response( {"detail": "dataset and snapshot_name required"}, status=400 ) try: zfs_runner.create_snapshot(dataset, snapshot_name) return web.json_response({"status": "created"}) except Exception as e: logger.error(f"Error creating snapshot: {e}") return web.json_response( {"detail": str(e)}, status=500 ) @staticmethod @require_auth async def rollback_snapshot(request): """POST /api/snapshots/rollback - Rollback to snapshot""" try: data = await request.json() except Exception: return web.json_response( {"detail": "Invalid JSON"}, status=400 ) snapshot = data.get('snapshot') if not snapshot: return web.json_response( {"detail": "snapshot required"}, status=400 ) try: zfs_runner.rollback_snapshot(snapshot) return web.json_response({"status": "rolled back"}) except Exception as e: logger.error(f"Error rolling back snapshot: {e}") return web.json_response( {"detail": str(e)}, status=500 ) @staticmethod @require_auth async def delete_snapshot(request): """DELETE /api/snapshots/{name} - Delete snapshot""" name = request.match_info['name'] try: zfs_runner.delete_snapshot(name) return web.json_response({"status": "deleted"}) except Exception as e: logger.error(f"Error deleting snapshot: {e}") return web.json_response( {"detail": str(e)}, status=500 ) snapshots = SnapshotsRouter() # ============ FILES ROUTER ============ class FilesRouter: @staticmethod @require_auth async def browse(request): """GET /api/files/browse - Browse directory""" path = request.query.get('path', '/') try: items = zfs_runner.browse_directory(path) return web.json_response(items) except Exception as e: logger.error(f"Error browsing directory: {e}") return web.json_response( {"detail": str(e)}, status=500 ) @staticmethod @require_auth async def read(request): """GET /api/files/read - Read file""" path = request.query.get('path') if not path: return web.json_response( {"detail": "path required"}, status=400 ) try: content = zfs_runner.read_file(path) return web.Response(text=content, content_type='text/plain') except Exception as e: logger.error(f"Error reading file: {e}") return web.json_response( {"detail": str(e)}, status=500 ) files = FilesRouter() # ============ SHARES ROUTER ============ class SharesRouter: @staticmethod @require_auth async def list_samba_shares(request): """GET /api/shares/samba - List Samba shares""" # TODO: Implement return web.json_response([]) @staticmethod @require_auth async def list_nfs_shares(request): """GET /api/shares/nfs - List NFS shares""" # TODO: Implement return web.json_response([]) shares = SharesRouter() def setup_all_routes(app): """Setup all routes""" # Auth routes app.router.add_post('/api/auth/login', auth.login) app.router.add_post('/api/auth/verify', auth.verify) # Pool routes app.router.add_get('/api/pools', pools.list_pools) app.router.add_get('/api/pools/{name}', pools.get_pool_status) app.router.add_post('/api/pools/{name}/scrub', pools.start_scrub) # Dataset routes app.router.add_get('/api/datasets', datasets.list_datasets) app.router.add_patch('/api/datasets/{name}', datasets.update_dataset) # Snapshot routes app.router.add_get('/api/snapshots', snapshots.list_snapshots) app.router.add_post('/api/snapshots', snapshots.create_snapshot) app.router.add_post('/api/snapshots/rollback', snapshots.rollback_snapshot) app.router.add_delete('/api/snapshots/{name}', snapshots.delete_snapshot) # File routes app.router.add_get('/api/files/browse', files.browse) app.router.add_get('/api/files/read', files.read) # Share routes app.router.add_get('/api/shares/samba', shares.list_samba_shares) app.router.add_get('/api/shares/nfs', shares.list_nfs_shares)