mirror of
https://github.com/jhd3197/ServerKit.git
synced 2026-11-09 05:24:49 +09:00
Enforce admin and workspace permissions, improve logging, and add DNS frontend/API support. - Backend: added admin checks to cloud_provisioning and marketplace endpoints; added require_workspace_access helper and applied to multiple workspace endpoints to enforce membership/admin access; persist push subscription data in mobile API (commit to DB); replaced print() debugging with structured logger calls across agent_registry and added logging/exception handling in monitoring_service. - Frontend: added a DNS API client (frontend/src/services/api/dns.js) and registered it in the API index; extended DNSZones UI to collect provider zone ID and API token, updated create payload and form reset; added optional modal footer prop to Modal component; moved rollup native binding to optionalDependencies in package.json. - Misc: added '*.png' to .gitignore and a minor spacing change in install.sh. These changes improve security checks, observability, and add DNS management features in the UI/API.
221 lines
7.4 KiB
Python
221 lines
7.4 KiB
Python
from flask import Blueprint, request, jsonify
|
|
from flask_jwt_extended import jwt_required
|
|
from app.services.workspace_service import WorkspaceService
|
|
from app.services.audit_service import AuditService
|
|
from app.models.audit_log import AuditLog
|
|
|
|
workspaces_bp = Blueprint('workspaces', __name__)
|
|
|
|
|
|
def get_current_user():
|
|
from flask_jwt_extended import get_jwt_identity
|
|
from app.models.user import User
|
|
return User.query.get(get_jwt_identity())
|
|
|
|
|
|
def require_workspace_access(workspace_id, user):
|
|
"""Return 403 response tuple if user is not a workspace member or admin, else None."""
|
|
if user.is_admin:
|
|
return None
|
|
role = WorkspaceService.get_user_role(workspace_id, user.id)
|
|
if role is None:
|
|
return jsonify({'error': 'Workspace access denied'}), 403
|
|
return None
|
|
|
|
|
|
@workspaces_bp.route('/', methods=['GET'])
|
|
@jwt_required()
|
|
def list_workspaces():
|
|
user = get_current_user()
|
|
include_archived = request.args.get('include_archived', 'false') == 'true'
|
|
# Super-admins see all, others see their memberships
|
|
if user.is_admin and request.args.get('all') == 'true':
|
|
workspaces = WorkspaceService.get_all_workspaces_admin()
|
|
return jsonify({'workspaces': workspaces})
|
|
workspaces = WorkspaceService.list_workspaces(user_id=user.id, include_archived=include_archived)
|
|
return jsonify({'workspaces': [ws.to_dict() for ws in workspaces]})
|
|
|
|
|
|
@workspaces_bp.route('/<int:workspace_id>', methods=['GET'])
|
|
@jwt_required()
|
|
def get_workspace(workspace_id):
|
|
user = get_current_user()
|
|
denied = require_workspace_access(workspace_id, user)
|
|
if denied:
|
|
return denied
|
|
ws = WorkspaceService.get_workspace(workspace_id)
|
|
if not ws:
|
|
return jsonify({'error': 'Workspace not found'}), 404
|
|
return jsonify(ws.to_dict())
|
|
|
|
|
|
@workspaces_bp.route('/', methods=['POST'])
|
|
@jwt_required()
|
|
def create_workspace():
|
|
user = get_current_user()
|
|
data = request.get_json()
|
|
if not data:
|
|
return jsonify({'error': 'Request body required'}), 400
|
|
try:
|
|
ws = WorkspaceService.create_workspace(data, user.id)
|
|
AuditService.log(
|
|
action=AuditLog.ACTION_RESOURCE_CREATE, user_id=user.id,
|
|
target_type='workspace', target_id=ws.id,
|
|
details={'name': ws.name}
|
|
)
|
|
return jsonify(ws.to_dict()), 201
|
|
except ValueError as e:
|
|
return jsonify({'error': str(e)}), 400
|
|
|
|
|
|
@workspaces_bp.route('/<int:workspace_id>', methods=['PUT'])
|
|
@jwt_required()
|
|
def update_workspace(workspace_id):
|
|
user = get_current_user()
|
|
role = WorkspaceService.get_user_role(workspace_id, user.id)
|
|
if role not in ['owner', 'admin'] and not user.is_admin:
|
|
return jsonify({'error': 'Insufficient permissions'}), 403
|
|
|
|
data = request.get_json()
|
|
ws = WorkspaceService.update_workspace(workspace_id, data)
|
|
if not ws:
|
|
return jsonify({'error': 'Workspace not found'}), 404
|
|
return jsonify(ws.to_dict())
|
|
|
|
|
|
@workspaces_bp.route('/<int:workspace_id>/archive', methods=['POST'])
|
|
@jwt_required()
|
|
def archive_workspace(workspace_id):
|
|
user = get_current_user()
|
|
role = WorkspaceService.get_user_role(workspace_id, user.id)
|
|
if role != 'owner' and not user.is_admin:
|
|
return jsonify({'error': 'Owner access required'}), 403
|
|
ws = WorkspaceService.archive_workspace(workspace_id)
|
|
if not ws:
|
|
return jsonify({'error': 'Workspace not found'}), 404
|
|
return jsonify(ws.to_dict())
|
|
|
|
|
|
@workspaces_bp.route('/<int:workspace_id>/restore', methods=['POST'])
|
|
@jwt_required()
|
|
def restore_workspace(workspace_id):
|
|
user = get_current_user()
|
|
if not user.is_admin:
|
|
return jsonify({'error': 'Admin access required'}), 403
|
|
ws = WorkspaceService.restore_workspace(workspace_id)
|
|
if not ws:
|
|
return jsonify({'error': 'Workspace not found'}), 404
|
|
return jsonify(ws.to_dict())
|
|
|
|
|
|
@workspaces_bp.route('/<int:workspace_id>', methods=['DELETE'])
|
|
@jwt_required()
|
|
def delete_workspace(workspace_id):
|
|
user = get_current_user()
|
|
if not user.is_admin:
|
|
return jsonify({'error': 'Admin access required'}), 403
|
|
result = WorkspaceService.delete_workspace(workspace_id)
|
|
if not result:
|
|
return jsonify({'error': 'Workspace not found'}), 404
|
|
return jsonify({'message': 'Workspace deleted'})
|
|
|
|
|
|
# --- Members ---
|
|
|
|
@workspaces_bp.route('/<int:workspace_id>/members', methods=['GET'])
|
|
@jwt_required()
|
|
def get_members(workspace_id):
|
|
user = get_current_user()
|
|
denied = require_workspace_access(workspace_id, user)
|
|
if denied:
|
|
return denied
|
|
members = WorkspaceService.get_members(workspace_id)
|
|
return jsonify({'members': [m.to_dict() for m in members]})
|
|
|
|
|
|
@workspaces_bp.route('/<int:workspace_id>/members', methods=['POST'])
|
|
@jwt_required()
|
|
def add_member(workspace_id):
|
|
user = get_current_user()
|
|
role = WorkspaceService.get_user_role(workspace_id, user.id)
|
|
if role not in ['owner', 'admin'] and not user.is_admin:
|
|
return jsonify({'error': 'Insufficient permissions'}), 403
|
|
|
|
data = request.get_json() or {}
|
|
user_id = data.get('user_id')
|
|
if not user_id:
|
|
return jsonify({'error': 'user_id required'}), 400
|
|
|
|
try:
|
|
member = WorkspaceService.add_member(workspace_id, user_id, data.get('role', 'member'))
|
|
return jsonify(member.to_dict()), 201
|
|
except ValueError as e:
|
|
return jsonify({'error': str(e)}), 400
|
|
|
|
|
|
@workspaces_bp.route('/members/<int:member_id>/role', methods=['PUT'])
|
|
@jwt_required()
|
|
def update_member_role(member_id):
|
|
data = request.get_json() or {}
|
|
role = data.get('role')
|
|
if not role:
|
|
return jsonify({'error': 'role required'}), 400
|
|
member = WorkspaceService.update_member_role(member_id, role)
|
|
if not member:
|
|
return jsonify({'error': 'Member not found'}), 404
|
|
return jsonify(member.to_dict())
|
|
|
|
|
|
@workspaces_bp.route('/members/<int:member_id>', methods=['DELETE'])
|
|
@jwt_required()
|
|
def remove_member(member_id):
|
|
try:
|
|
result = WorkspaceService.remove_member(member_id)
|
|
if not result:
|
|
return jsonify({'error': 'Member not found'}), 404
|
|
return jsonify({'message': 'Member removed'})
|
|
except ValueError as e:
|
|
return jsonify({'error': str(e)}), 400
|
|
|
|
|
|
# --- API Keys ---
|
|
|
|
@workspaces_bp.route('/<int:workspace_id>/api-keys', methods=['GET'])
|
|
@jwt_required()
|
|
def list_api_keys(workspace_id):
|
|
user = get_current_user()
|
|
denied = require_workspace_access(workspace_id, user)
|
|
if denied:
|
|
return denied
|
|
keys = WorkspaceService.list_api_keys(workspace_id)
|
|
return jsonify({'api_keys': [k.to_dict() for k in keys]})
|
|
|
|
|
|
@workspaces_bp.route('/<int:workspace_id>/api-keys', methods=['POST'])
|
|
@jwt_required()
|
|
def create_api_key(workspace_id):
|
|
user = get_current_user()
|
|
denied = require_workspace_access(workspace_id, user)
|
|
if denied:
|
|
return denied
|
|
data = request.get_json() or {}
|
|
name = data.get('name')
|
|
if not name:
|
|
return jsonify({'error': 'name required'}), 400
|
|
|
|
api_key, raw_key = WorkspaceService.create_api_key(
|
|
workspace_id, name, scopes=data.get('scopes'), user_id=user.id
|
|
)
|
|
result = api_key.to_dict()
|
|
result['key'] = raw_key # Only returned once
|
|
return jsonify(result), 201
|
|
|
|
|
|
@workspaces_bp.route('/api-keys/<int:key_id>/revoke', methods=['POST'])
|
|
@jwt_required()
|
|
def revoke_api_key(key_id):
|
|
result = WorkspaceService.revoke_api_key(key_id)
|
|
if not result:
|
|
return jsonify({'error': 'API key not found'}), 404
|
|
return jsonify({'message': 'API key revoked'})
|