mirror of
https://github.com/jhd3197/ServerKit.git
synced 2026-11-09 05:24:49 +09:00
- Project / Environment hierarchy (C2): Workspace -> Project -> Environment org model. New Project/Environment models + CRUD APIs, nullable applications.project_id/environment_id (opt-in, no destructive backfill), ProjectService.ensure_default, a Projects list + ProjectDetail (env tabs), a project/environment selector in the create wizard, and sidebar nav. - Polymorphic shared resources facade (C9): ResourceTag + SharedVariableGroup /SharedVariable/attachment models keyed by (resource_type, resource_id), reusing the env-var Fernet encryption; SharedResourceService resolves merged variables (most-recent attachment wins) with secrets masked. /api/v1/shared endpoints + reusable TagsPanel/EnvironmentVariablesPanel + a Shared Variables management page. Existing per-resource tables untouched (facade first). - PR preview environments (C3): ApplicationPreview + settings models, a PreviewService with pure domain-templating + reconcile (open PRs vs active previews) cores and best-effort provisioning (incl. guarded WordPress file+DB clone), a pull_request webhook endpoint, preview.* jobs, and a Previews tab on the app detail page. - Per-server managed proxy stack (C6): opt-in Dockerized Traefik/Caddy as a Compose stack (host Nginx stays default and superior for PHP/WordPress). ProxyStack model + ProxyStackService with pure compose/config generators and versioned config backups, /api/v1/servers/<id>/proxy endpoints, and a Proxy tab on the server detail page. Migrations 031-034 add the new tables/columns; the full chain applies cleanly from an empty DB. Full backend suite: 871 passed, 14 skipped; frontend builds. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
127 lines
4.7 KiB
Python
127 lines
4.7 KiB
Python
"""Environment endpoints (under a Project).
|
|
|
|
Environments are children of a project; access is derived from the parent
|
|
project's workspace, mirroring projects.py.
|
|
"""
|
|
from flask import Blueprint, request, jsonify
|
|
from flask_jwt_extended import jwt_required, get_jwt_identity
|
|
|
|
from app.models.user import User
|
|
from app.services.project_service import ProjectService
|
|
from app.services.workspace_service import WorkspaceService
|
|
|
|
environments_bp = Blueprint('environments', __name__)
|
|
|
|
|
|
def _current_user():
|
|
return User.query.get(get_jwt_identity())
|
|
|
|
|
|
def _accessible_workspace_ids(user):
|
|
ws_id = WorkspaceService.resolve_workspace_id(
|
|
user, request.headers.get('X-Workspace-Id') or request.args.get('workspace_id'))
|
|
if ws_id is not None:
|
|
return [ws_id]
|
|
if user.is_admin:
|
|
from app.models.workspace import Workspace
|
|
return [w.id for w in Workspace.query.all()]
|
|
return [w.id for w in WorkspaceService.list_workspaces(user_id=user.id)]
|
|
|
|
|
|
def _project_for_write(user, project_id):
|
|
"""Return (project, error_response). error_response is a (json, status) tuple
|
|
when the project can't be written by this user, else None."""
|
|
project = ProjectService.get_project(project_id)
|
|
if not project:
|
|
return None, (jsonify({'error': 'Project not found'}), 404)
|
|
if not user.is_admin and project.workspace_id not in set(_accessible_workspace_ids(user)):
|
|
return None, (jsonify({'error': 'Access denied'}), 403)
|
|
if not WorkspaceService.can_write_in_workspace(user, project.workspace_id):
|
|
return None, (jsonify({'error': 'You have read-only access to this workspace'}), 403)
|
|
return project, None
|
|
|
|
|
|
@environments_bp.route('', methods=['POST'])
|
|
@environments_bp.route('/', methods=['POST'])
|
|
@jwt_required()
|
|
def create_environment():
|
|
"""Create an environment under a project. Body: {project_id, name, is_default?}."""
|
|
user = _current_user()
|
|
data = request.get_json() or {}
|
|
project_id = data.get('project_id')
|
|
if not project_id:
|
|
return jsonify({'error': 'project_id is required'}), 400
|
|
name = (data.get('name') or '').strip()
|
|
if not name:
|
|
return jsonify({'error': 'Environment name is required'}), 400
|
|
|
|
project, err = _project_for_write(user, project_id)
|
|
if err:
|
|
return err
|
|
|
|
try:
|
|
env = ProjectService.create_environment(
|
|
project_id, name=name, is_default=bool(data.get('is_default')))
|
|
except ValueError as exc:
|
|
return jsonify({'error': str(exc)}), 400
|
|
from app import db
|
|
db.session.commit()
|
|
return jsonify({'message': 'Environment created', 'environment': env.to_dict(include_counts=True)}), 201
|
|
|
|
|
|
@environments_bp.route('/<int:environment_id>', methods=['PUT'])
|
|
@jwt_required()
|
|
def update_environment(environment_id):
|
|
user = _current_user()
|
|
env = ProjectService.get_environment(environment_id)
|
|
if not env:
|
|
return jsonify({'error': 'Environment not found'}), 404
|
|
_, err = _project_for_write(user, env.project_id)
|
|
if err:
|
|
return err
|
|
|
|
data = request.get_json() or {}
|
|
updated = ProjectService.update_environment(
|
|
environment_id, name=data.get('name'), is_default=data.get('is_default'))
|
|
return jsonify({'message': 'Environment updated', 'environment': updated.to_dict(include_counts=True)}), 200
|
|
|
|
|
|
@environments_bp.route('/<int:environment_id>', methods=['DELETE'])
|
|
@jwt_required()
|
|
def delete_environment(environment_id):
|
|
"""Delete an environment. Refuses (409) if it's the project's only one;
|
|
detaches assigned apps (environment_id -> NULL) otherwise."""
|
|
user = _current_user()
|
|
env = ProjectService.get_environment(environment_id)
|
|
if not env:
|
|
return jsonify({'error': 'Environment not found'}), 404
|
|
_, err = _project_for_write(user, env.project_id)
|
|
if err:
|
|
return err
|
|
|
|
result = ProjectService.delete_environment(environment_id)
|
|
if result == 'last':
|
|
return jsonify({'error': 'Cannot delete the only environment of a project.'}), 409
|
|
return jsonify({'message': 'Environment deleted'}), 200
|
|
|
|
|
|
@environments_bp.route('/reorder', methods=['POST'])
|
|
@jwt_required()
|
|
def reorder_environments():
|
|
"""Reorder a project's environments. Body: {project_id, ordered_ids:[...]}"""
|
|
user = _current_user()
|
|
data = request.get_json() or {}
|
|
project_id = data.get('project_id')
|
|
ordered_ids = data.get('ordered_ids')
|
|
if not project_id:
|
|
return jsonify({'error': 'project_id is required'}), 400
|
|
if not isinstance(ordered_ids, list):
|
|
return jsonify({'error': 'ordered_ids must be a list'}), 400
|
|
|
|
_, err = _project_for_write(user, project_id)
|
|
if err:
|
|
return err
|
|
|
|
envs = ProjectService.reorder_environments(project_id, ordered_ids)
|
|
return jsonify({'environments': [e.to_dict(include_counts=True) for e in envs]}), 200
|