tested
This commit is contained in:
Binary file not shown.
137345
backend/logs/app.log.1
137345
backend/logs/app.log.1
File diff suppressed because it is too large
Load Diff
@@ -16,7 +16,7 @@ from ..core.database import get_auth_db
|
||||
from ..services.auth_service import AuthService
|
||||
from ..schemas.auth import Token, User as UserSchema
|
||||
from ..dependencies import get_current_user
|
||||
from ..core.auth.oauth import oauth
|
||||
from ..core.auth.oauth import oauth, is_adfs_configured
|
||||
from ..core.auth.logger import log_security_event
|
||||
from ..core.logger import belief_scope
|
||||
import starlette.requests
|
||||
@@ -85,6 +85,11 @@ async def logout(current_user: UserSchema = Depends(get_current_user)):
|
||||
@router.get("/login/adfs")
|
||||
async def login_adfs(request: starlette.requests.Request):
|
||||
with belief_scope("api.auth.login_adfs"):
|
||||
if not is_adfs_configured():
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||
detail="ADFS is not configured. Please set ADFS_CLIENT_ID, ADFS_CLIENT_SECRET, and ADFS_METADATA_URL environment variables."
|
||||
)
|
||||
redirect_uri = request.url_for('auth_callback_adfs')
|
||||
return await oauth.adfs.authorize_redirect(request, str(redirect_uri))
|
||||
# [/DEF:login_adfs:Function]
|
||||
@@ -95,6 +100,11 @@ async def login_adfs(request: starlette.requests.Request):
|
||||
@router.get("/callback/adfs", name="auth_callback_adfs")
|
||||
async def auth_callback_adfs(request: starlette.requests.Request, db: Session = Depends(get_auth_db)):
|
||||
with belief_scope("api.auth.callback_adfs"):
|
||||
if not is_adfs_configured():
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||
detail="ADFS is not configured. Please set ADFS_CLIENT_ID, ADFS_CLIENT_SECRET, and ADFS_METADATA_URL environment variables."
|
||||
)
|
||||
token = await oauth.adfs.authorize_access_token(request)
|
||||
user_info = token.get('userinfo')
|
||||
if not user_info:
|
||||
|
||||
@@ -22,7 +22,7 @@ from ...schemas.auth import (
|
||||
)
|
||||
from ...models.auth import User, Role, Permission, ADGroupMapping
|
||||
from ...dependencies import has_permission, get_current_user
|
||||
from ...core.logger import belief_scope
|
||||
from ...core.logger import logger, belief_scope
|
||||
# [/SECTION]
|
||||
|
||||
# [DEF:router:Variable]
|
||||
@@ -126,13 +126,17 @@ async def delete_user(
|
||||
_ = Depends(has_permission("admin:users", "WRITE"))
|
||||
):
|
||||
with belief_scope("api.admin.delete_user"):
|
||||
logger.info(f"[DEBUG] Attempting to delete user context={{'user_id': '{user_id}'}}")
|
||||
repo = AuthRepository(db)
|
||||
user = repo.get_user_by_id(user_id)
|
||||
if not user:
|
||||
logger.warning(f"[DEBUG] User not found for deletion context={{'user_id': '{user_id}'}}")
|
||||
raise HTTPException(status_code=404, detail="User not found")
|
||||
|
||||
logger.info(f"[DEBUG] Found user to delete context={{'username': '{user.username}'}}")
|
||||
db.delete(user)
|
||||
db.commit()
|
||||
logger.info(f"[DEBUG] Successfully deleted user context={{'user_id': '{user_id}'}}")
|
||||
return None
|
||||
# [/DEF:delete_user:Function]
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
# [SECTION: IMPORTS]
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from typing import List, Dict, Optional
|
||||
from ...dependencies import get_config_manager, get_scheduler_service
|
||||
from ...dependencies import get_config_manager, get_scheduler_service, has_permission
|
||||
from ...core.superset_client import SupersetClient
|
||||
from pydantic import BaseModel, Field
|
||||
from ...core.config_models import Environment as EnvModel
|
||||
@@ -47,7 +47,10 @@ class DatabaseResponse(BaseModel):
|
||||
# @POST: Returns a list of EnvironmentResponse objects.
|
||||
# @RETURN: List[EnvironmentResponse]
|
||||
@router.get("", response_model=List[EnvironmentResponse])
|
||||
async def get_environments(config_manager=Depends(get_config_manager)):
|
||||
async def get_environments(
|
||||
config_manager=Depends(get_config_manager),
|
||||
_ = Depends(has_permission("environments", "READ"))
|
||||
):
|
||||
with belief_scope("get_environments"):
|
||||
envs = config_manager.get_environments()
|
||||
# Ensure envs is a list
|
||||
@@ -77,7 +80,8 @@ async def update_environment_schedule(
|
||||
id: str,
|
||||
schedule: ScheduleSchema,
|
||||
config_manager=Depends(get_config_manager),
|
||||
scheduler_service=Depends(get_scheduler_service)
|
||||
scheduler_service=Depends(get_scheduler_service),
|
||||
_ = Depends(has_permission("admin:settings", "WRITE"))
|
||||
):
|
||||
with belief_scope("update_environment_schedule", f"id={id}"):
|
||||
envs = config_manager.get_environments()
|
||||
@@ -104,7 +108,11 @@ async def update_environment_schedule(
|
||||
# @PARAM: id (str) - The environment ID.
|
||||
# @RETURN: List[Dict] - List of databases.
|
||||
@router.get("/{id}/databases")
|
||||
async def get_environment_databases(id: str, config_manager=Depends(get_config_manager)):
|
||||
async def get_environment_databases(
|
||||
id: str,
|
||||
config_manager=Depends(get_config_manager),
|
||||
_ = Depends(has_permission("admin:settings", "READ"))
|
||||
):
|
||||
with belief_scope("get_environment_databases", f"id={id}"):
|
||||
envs = config_manager.get_environments()
|
||||
env = next((e for e in envs if e.id == id), None)
|
||||
|
||||
@@ -13,7 +13,7 @@ from fastapi import APIRouter, Depends, HTTPException
|
||||
from sqlalchemy.orm import Session
|
||||
from typing import List, Optional
|
||||
import typing
|
||||
from src.dependencies import get_config_manager
|
||||
from src.dependencies import get_config_manager, has_permission
|
||||
from src.core.database import get_db
|
||||
from src.models.git import GitServerConfig, GitStatus, DeploymentEnvironment, GitRepository
|
||||
from src.api.routes.git_schemas import (
|
||||
@@ -34,7 +34,10 @@ git_service = GitService()
|
||||
# @POST: Returns a list of all GitServerConfig objects from the database.
|
||||
# @RETURN: List[GitServerConfigSchema]
|
||||
@router.get("/config", response_model=List[GitServerConfigSchema])
|
||||
async def get_git_configs(db: Session = Depends(get_db)):
|
||||
async def get_git_configs(
|
||||
db: Session = Depends(get_db),
|
||||
_ = Depends(has_permission("admin:settings", "READ"))
|
||||
):
|
||||
with belief_scope("get_git_configs"):
|
||||
return db.query(GitServerConfig).all()
|
||||
# [/DEF:get_git_configs:Function]
|
||||
@@ -46,7 +49,11 @@ async def get_git_configs(db: Session = Depends(get_db)):
|
||||
# @PARAM: config (GitServerConfigCreate)
|
||||
# @RETURN: GitServerConfigSchema
|
||||
@router.post("/config", response_model=GitServerConfigSchema)
|
||||
async def create_git_config(config: GitServerConfigCreate, db: Session = Depends(get_db)):
|
||||
async def create_git_config(
|
||||
config: GitServerConfigCreate,
|
||||
db: Session = Depends(get_db),
|
||||
_ = Depends(has_permission("admin:settings", "WRITE"))
|
||||
):
|
||||
with belief_scope("create_git_config"):
|
||||
db_config = GitServerConfig(**config.dict())
|
||||
db.add(db_config)
|
||||
@@ -61,7 +68,11 @@ async def create_git_config(config: GitServerConfigCreate, db: Session = Depends
|
||||
# @POST: The configuration record is removed from the database.
|
||||
# @PARAM: config_id (str)
|
||||
@router.delete("/config/{config_id}")
|
||||
async def delete_git_config(config_id: str, db: Session = Depends(get_db)):
|
||||
async def delete_git_config(
|
||||
config_id: str,
|
||||
db: Session = Depends(get_db),
|
||||
_ = Depends(has_permission("admin:settings", "WRITE"))
|
||||
):
|
||||
with belief_scope("delete_git_config"):
|
||||
db_config = db.query(GitServerConfig).filter(GitServerConfig.id == config_id).first()
|
||||
if not db_config:
|
||||
@@ -78,7 +89,10 @@ async def delete_git_config(config_id: str, db: Session = Depends(get_db)):
|
||||
# @POST: Returns success if the connection is validated via GitService.
|
||||
# @PARAM: config (GitServerConfigCreate)
|
||||
@router.post("/config/test")
|
||||
async def test_git_config(config: GitServerConfigCreate):
|
||||
async def test_git_config(
|
||||
config: GitServerConfigCreate,
|
||||
_ = Depends(has_permission("admin:settings", "READ"))
|
||||
):
|
||||
with belief_scope("test_git_config"):
|
||||
success = await git_service.test_connection(config.provider, config.url, config.pat)
|
||||
if success:
|
||||
@@ -94,7 +108,12 @@ async def test_git_config(config: GitServerConfigCreate):
|
||||
# @PARAM: dashboard_id (int)
|
||||
# @PARAM: init_data (RepoInitRequest)
|
||||
@router.post("/repositories/{dashboard_id}/init")
|
||||
async def init_repository(dashboard_id: int, init_data: RepoInitRequest, db: Session = Depends(get_db)):
|
||||
async def init_repository(
|
||||
dashboard_id: int,
|
||||
init_data: RepoInitRequest,
|
||||
db: Session = Depends(get_db),
|
||||
_ = Depends(has_permission("plugin:git", "EXECUTE"))
|
||||
):
|
||||
with belief_scope("init_repository"):
|
||||
# 1. Get config
|
||||
config = db.query(GitServerConfig).filter(GitServerConfig.id == init_data.config_id).first()
|
||||
@@ -138,7 +157,10 @@ async def init_repository(dashboard_id: int, init_data: RepoInitRequest, db: Ses
|
||||
# @PARAM: dashboard_id (int)
|
||||
# @RETURN: List[BranchSchema]
|
||||
@router.get("/repositories/{dashboard_id}/branches", response_model=List[BranchSchema])
|
||||
async def get_branches(dashboard_id: int):
|
||||
async def get_branches(
|
||||
dashboard_id: int,
|
||||
_ = Depends(has_permission("plugin:git", "EXECUTE"))
|
||||
):
|
||||
with belief_scope("get_branches"):
|
||||
try:
|
||||
return git_service.list_branches(dashboard_id)
|
||||
@@ -153,7 +175,11 @@ async def get_branches(dashboard_id: int):
|
||||
# @PARAM: dashboard_id (int)
|
||||
# @PARAM: branch_data (BranchCreate)
|
||||
@router.post("/repositories/{dashboard_id}/branches")
|
||||
async def create_branch(dashboard_id: int, branch_data: BranchCreate):
|
||||
async def create_branch(
|
||||
dashboard_id: int,
|
||||
branch_data: BranchCreate,
|
||||
_ = Depends(has_permission("plugin:git", "EXECUTE"))
|
||||
):
|
||||
with belief_scope("create_branch"):
|
||||
try:
|
||||
git_service.create_branch(dashboard_id, branch_data.name, branch_data.from_branch)
|
||||
@@ -169,7 +195,11 @@ async def create_branch(dashboard_id: int, branch_data: BranchCreate):
|
||||
# @PARAM: dashboard_id (int)
|
||||
# @PARAM: checkout_data (BranchCheckout)
|
||||
@router.post("/repositories/{dashboard_id}/checkout")
|
||||
async def checkout_branch(dashboard_id: int, checkout_data: BranchCheckout):
|
||||
async def checkout_branch(
|
||||
dashboard_id: int,
|
||||
checkout_data: BranchCheckout,
|
||||
_ = Depends(has_permission("plugin:git", "EXECUTE"))
|
||||
):
|
||||
with belief_scope("checkout_branch"):
|
||||
try:
|
||||
git_service.checkout_branch(dashboard_id, checkout_data.name)
|
||||
@@ -185,7 +215,11 @@ async def checkout_branch(dashboard_id: int, checkout_data: BranchCheckout):
|
||||
# @PARAM: dashboard_id (int)
|
||||
# @PARAM: commit_data (CommitCreate)
|
||||
@router.post("/repositories/{dashboard_id}/commit")
|
||||
async def commit_changes(dashboard_id: int, commit_data: CommitCreate):
|
||||
async def commit_changes(
|
||||
dashboard_id: int,
|
||||
commit_data: CommitCreate,
|
||||
_ = Depends(has_permission("plugin:git", "EXECUTE"))
|
||||
):
|
||||
with belief_scope("commit_changes"):
|
||||
try:
|
||||
git_service.commit_changes(dashboard_id, commit_data.message, commit_data.files)
|
||||
@@ -200,7 +234,10 @@ async def commit_changes(dashboard_id: int, commit_data: CommitCreate):
|
||||
# @POST: Local commits are pushed to the remote repository.
|
||||
# @PARAM: dashboard_id (int)
|
||||
@router.post("/repositories/{dashboard_id}/push")
|
||||
async def push_changes(dashboard_id: int):
|
||||
async def push_changes(
|
||||
dashboard_id: int,
|
||||
_ = Depends(has_permission("plugin:git", "EXECUTE"))
|
||||
):
|
||||
with belief_scope("push_changes"):
|
||||
try:
|
||||
git_service.push_changes(dashboard_id)
|
||||
@@ -215,7 +252,10 @@ async def push_changes(dashboard_id: int):
|
||||
# @POST: Remote changes are fetched and merged into the local branch.
|
||||
# @PARAM: dashboard_id (int)
|
||||
@router.post("/repositories/{dashboard_id}/pull")
|
||||
async def pull_changes(dashboard_id: int):
|
||||
async def pull_changes(
|
||||
dashboard_id: int,
|
||||
_ = Depends(has_permission("plugin:git", "EXECUTE"))
|
||||
):
|
||||
with belief_scope("pull_changes"):
|
||||
try:
|
||||
git_service.pull_changes(dashboard_id)
|
||||
@@ -231,7 +271,11 @@ async def pull_changes(dashboard_id: int):
|
||||
# @PARAM: dashboard_id (int)
|
||||
# @PARAM: source_env_id (Optional[str])
|
||||
@router.post("/repositories/{dashboard_id}/sync")
|
||||
async def sync_dashboard(dashboard_id: int, source_env_id: typing.Optional[str] = None):
|
||||
async def sync_dashboard(
|
||||
dashboard_id: int,
|
||||
source_env_id: typing.Optional[str] = None,
|
||||
_ = Depends(has_permission("plugin:git", "EXECUTE"))
|
||||
):
|
||||
with belief_scope("sync_dashboard"):
|
||||
try:
|
||||
from src.plugins.git_plugin import GitPlugin
|
||||
@@ -251,7 +295,10 @@ async def sync_dashboard(dashboard_id: int, source_env_id: typing.Optional[str]
|
||||
# @POST: Returns a list of DeploymentEnvironmentSchema objects.
|
||||
# @RETURN: List[DeploymentEnvironmentSchema]
|
||||
@router.get("/environments", response_model=List[DeploymentEnvironmentSchema])
|
||||
async def get_environments(config_manager=Depends(get_config_manager)):
|
||||
async def get_environments(
|
||||
config_manager=Depends(get_config_manager),
|
||||
_ = Depends(has_permission("environments", "READ"))
|
||||
):
|
||||
with belief_scope("get_environments"):
|
||||
envs = config_manager.get_environments()
|
||||
return [
|
||||
@@ -271,7 +318,11 @@ async def get_environments(config_manager=Depends(get_config_manager)):
|
||||
# @PARAM: dashboard_id (int)
|
||||
# @PARAM: deploy_data (DeployRequest)
|
||||
@router.post("/repositories/{dashboard_id}/deploy")
|
||||
async def deploy_dashboard(dashboard_id: int, deploy_data: DeployRequest):
|
||||
async def deploy_dashboard(
|
||||
dashboard_id: int,
|
||||
deploy_data: DeployRequest,
|
||||
_ = Depends(has_permission("plugin:git", "EXECUTE"))
|
||||
):
|
||||
with belief_scope("deploy_dashboard"):
|
||||
try:
|
||||
from src.plugins.git_plugin import GitPlugin
|
||||
@@ -293,7 +344,11 @@ async def deploy_dashboard(dashboard_id: int, deploy_data: DeployRequest):
|
||||
# @PARAM: limit (int)
|
||||
# @RETURN: List[CommitSchema]
|
||||
@router.get("/repositories/{dashboard_id}/history", response_model=List[CommitSchema])
|
||||
async def get_history(dashboard_id: int, limit: int = 50):
|
||||
async def get_history(
|
||||
dashboard_id: int,
|
||||
limit: int = 50,
|
||||
_ = Depends(has_permission("plugin:git", "EXECUTE"))
|
||||
):
|
||||
with belief_scope("get_history"):
|
||||
try:
|
||||
return git_service.get_commit_history(dashboard_id, limit)
|
||||
@@ -308,7 +363,10 @@ async def get_history(dashboard_id: int, limit: int = 50):
|
||||
# @PARAM: dashboard_id (int)
|
||||
# @RETURN: dict
|
||||
@router.get("/repositories/{dashboard_id}/status")
|
||||
async def get_repository_status(dashboard_id: int):
|
||||
async def get_repository_status(
|
||||
dashboard_id: int,
|
||||
_ = Depends(has_permission("plugin:git", "EXECUTE"))
|
||||
):
|
||||
with belief_scope("get_repository_status"):
|
||||
try:
|
||||
return git_service.get_status(dashboard_id)
|
||||
@@ -325,7 +383,12 @@ async def get_repository_status(dashboard_id: int):
|
||||
# @PARAM: staged (bool)
|
||||
# @RETURN: str
|
||||
@router.get("/repositories/{dashboard_id}/diff")
|
||||
async def get_repository_diff(dashboard_id: int, file_path: Optional[str] = None, staged: bool = False):
|
||||
async def get_repository_diff(
|
||||
dashboard_id: int,
|
||||
file_path: Optional[str] = None,
|
||||
staged: bool = False,
|
||||
_ = Depends(has_permission("plugin:git", "EXECUTE"))
|
||||
):
|
||||
with belief_scope("get_repository_diff"):
|
||||
try:
|
||||
diff_text = git_service.get_diff(dashboard_id, file_path, staged)
|
||||
|
||||
@@ -14,7 +14,7 @@ from fastapi import APIRouter, Depends, HTTPException
|
||||
from sqlalchemy.orm import Session
|
||||
from typing import List, Optional
|
||||
from ...core.logger import belief_scope
|
||||
from ...dependencies import get_config_manager
|
||||
from ...dependencies import get_config_manager, has_permission
|
||||
from ...core.database import get_db
|
||||
from ...models.mapping import DatabaseMapping
|
||||
from pydantic import BaseModel
|
||||
@@ -60,7 +60,8 @@ class SuggestRequest(BaseModel):
|
||||
async def get_mappings(
|
||||
source_env_id: Optional[str] = None,
|
||||
target_env_id: Optional[str] = None,
|
||||
db: Session = Depends(get_db)
|
||||
db: Session = Depends(get_db),
|
||||
_ = Depends(has_permission("plugin:mapper", "EXECUTE"))
|
||||
):
|
||||
with belief_scope("get_mappings"):
|
||||
query = db.query(DatabaseMapping)
|
||||
@@ -76,7 +77,11 @@ async def get_mappings(
|
||||
# @PRE: mapping is valid MappingCreate, db session is injected.
|
||||
# @POST: DatabaseMapping created or updated in database.
|
||||
@router.post("", response_model=MappingResponse)
|
||||
async def create_mapping(mapping: MappingCreate, db: Session = Depends(get_db)):
|
||||
async def create_mapping(
|
||||
mapping: MappingCreate,
|
||||
db: Session = Depends(get_db),
|
||||
_ = Depends(has_permission("plugin:mapper", "EXECUTE"))
|
||||
):
|
||||
with belief_scope("create_mapping"):
|
||||
# Check if mapping already exists
|
||||
existing = db.query(DatabaseMapping).filter(
|
||||
@@ -106,10 +111,11 @@ async def create_mapping(mapping: MappingCreate, db: Session = Depends(get_db)):
|
||||
@router.post("/suggest")
|
||||
async def suggest_mappings_api(
|
||||
request: SuggestRequest,
|
||||
config_manager=Depends(get_config_manager)
|
||||
config_manager=Depends(get_config_manager),
|
||||
_ = Depends(has_permission("plugin:mapper", "EXECUTE"))
|
||||
):
|
||||
with belief_scope("suggest_mappings_api"):
|
||||
from backend.src.services.mapping_service import MappingService
|
||||
from ...services.mapping_service import MappingService
|
||||
service = MappingService(config_manager)
|
||||
try:
|
||||
return await service.get_suggestions(request.source_env_id, request.target_env_id)
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from typing import List, Dict
|
||||
from ...dependencies import get_config_manager, get_task_manager
|
||||
from ...dependencies import get_config_manager, get_task_manager, has_permission
|
||||
from ...models.dashboard import DashboardMetadata, DashboardSelection
|
||||
from ...core.superset_client import SupersetClient
|
||||
from ...core.logger import belief_scope
|
||||
@@ -21,7 +21,11 @@ router = APIRouter(prefix="/api", tags=["migration"])
|
||||
# @PARAM: env_id (str) - The ID of the environment to fetch from.
|
||||
# @RETURN: List[DashboardMetadata]
|
||||
@router.get("/environments/{env_id}/dashboards", response_model=List[DashboardMetadata])
|
||||
async def get_dashboards(env_id: str, config_manager=Depends(get_config_manager)):
|
||||
async def get_dashboards(
|
||||
env_id: str,
|
||||
config_manager=Depends(get_config_manager),
|
||||
_ = Depends(has_permission("plugin:migration", "EXECUTE"))
|
||||
):
|
||||
with belief_scope("get_dashboards", f"env_id={env_id}"):
|
||||
environments = config_manager.get_environments()
|
||||
env = next((e for e in environments if e.id == env_id), None)
|
||||
@@ -40,7 +44,12 @@ async def get_dashboards(env_id: str, config_manager=Depends(get_config_manager)
|
||||
# @PARAM: selection (DashboardSelection) - The dashboards to migrate.
|
||||
# @RETURN: Dict - {"task_id": str, "message": str}
|
||||
@router.post("/migration/execute")
|
||||
async def execute_migration(selection: DashboardSelection, config_manager=Depends(get_config_manager), task_manager=Depends(get_task_manager)):
|
||||
async def execute_migration(
|
||||
selection: DashboardSelection,
|
||||
config_manager=Depends(get_config_manager),
|
||||
task_manager=Depends(get_task_manager),
|
||||
_ = Depends(has_permission("plugin:migration", "EXECUTE"))
|
||||
):
|
||||
with belief_scope("execute_migration"):
|
||||
# Validate environments exist
|
||||
environments = config_manager.get_environments()
|
||||
|
||||
@@ -7,7 +7,7 @@ from typing import List
|
||||
from fastapi import APIRouter, Depends
|
||||
|
||||
from ...core.plugin_base import PluginConfig
|
||||
from ...dependencies import get_plugin_loader
|
||||
from ...dependencies import get_plugin_loader, has_permission
|
||||
from ...core.logger import belief_scope
|
||||
|
||||
router = APIRouter()
|
||||
@@ -19,7 +19,8 @@ router = APIRouter()
|
||||
# @RETURN: List[PluginConfig] - List of registered plugins.
|
||||
@router.get("", response_model=List[PluginConfig])
|
||||
async def list_plugins(
|
||||
plugin_loader = Depends(get_plugin_loader)
|
||||
plugin_loader = Depends(get_plugin_loader),
|
||||
_ = Depends(has_permission("plugins", "READ"))
|
||||
):
|
||||
with belief_scope("list_plugins"):
|
||||
"""
|
||||
|
||||
@@ -31,7 +31,7 @@ router = APIRouter()
|
||||
@router.get("", response_model=AppConfig)
|
||||
async def get_settings(
|
||||
config_manager: ConfigManager = Depends(get_config_manager),
|
||||
_ = Depends(has_permission("settings", "READ"))
|
||||
_ = Depends(has_permission("admin:settings", "READ"))
|
||||
):
|
||||
with belief_scope("get_settings"):
|
||||
logger.info("[get_settings][Entry] Fetching all settings")
|
||||
@@ -53,7 +53,7 @@ async def get_settings(
|
||||
async def update_global_settings(
|
||||
settings: GlobalSettings,
|
||||
config_manager: ConfigManager = Depends(get_config_manager),
|
||||
_ = Depends(has_permission("settings", "WRITE"))
|
||||
_ = Depends(has_permission("admin:settings", "WRITE"))
|
||||
):
|
||||
with belief_scope("update_global_settings"):
|
||||
logger.info("[update_global_settings][Entry] Updating global settings")
|
||||
@@ -68,7 +68,7 @@ async def update_global_settings(
|
||||
@router.get("/storage", response_model=StorageConfig)
|
||||
async def get_storage_settings(
|
||||
config_manager: ConfigManager = Depends(get_config_manager),
|
||||
_ = Depends(has_permission("settings", "READ"))
|
||||
_ = Depends(has_permission("admin:settings", "READ"))
|
||||
):
|
||||
with belief_scope("get_storage_settings"):
|
||||
return config_manager.get_config().settings.storage
|
||||
@@ -83,7 +83,7 @@ async def get_storage_settings(
|
||||
async def update_storage_settings(
|
||||
storage: StorageConfig,
|
||||
config_manager: ConfigManager = Depends(get_config_manager),
|
||||
_ = Depends(has_permission("settings", "WRITE"))
|
||||
_ = Depends(has_permission("admin:settings", "WRITE"))
|
||||
):
|
||||
with belief_scope("update_storage_settings"):
|
||||
is_valid, message = config_manager.validate_path(storage.root_path)
|
||||
@@ -104,7 +104,7 @@ async def update_storage_settings(
|
||||
@router.get("/environments", response_model=List[Environment])
|
||||
async def get_environments(
|
||||
config_manager: ConfigManager = Depends(get_config_manager),
|
||||
_ = Depends(has_permission("settings", "READ"))
|
||||
_ = Depends(has_permission("admin:settings", "READ"))
|
||||
):
|
||||
with belief_scope("get_environments"):
|
||||
logger.info("[get_environments][Entry] Fetching environments")
|
||||
@@ -121,7 +121,7 @@ async def get_environments(
|
||||
async def add_environment(
|
||||
env: Environment,
|
||||
config_manager: ConfigManager = Depends(get_config_manager),
|
||||
_ = Depends(has_permission("settings", "WRITE"))
|
||||
_ = Depends(has_permission("admin:settings", "WRITE"))
|
||||
):
|
||||
with belief_scope("add_environment"):
|
||||
logger.info(f"[add_environment][Entry] Adding environment {env.id}")
|
||||
|
||||
@@ -8,11 +8,12 @@
|
||||
# @INVARIANT: All paths must be validated against path traversal.
|
||||
|
||||
# [SECTION: IMPORTS]
|
||||
from pathlib import Path
|
||||
from fastapi import APIRouter, Depends, UploadFile, File, Form, HTTPException
|
||||
from fastapi.responses import FileResponse
|
||||
from typing import List, Optional
|
||||
from ...models.storage import StoredFile, FileCategory
|
||||
from ...dependencies import get_plugin_loader
|
||||
from ...dependencies import get_plugin_loader, has_permission
|
||||
from ...plugins.storage.plugin import StoragePlugin
|
||||
from ...core.logger import belief_scope
|
||||
# [/SECTION]
|
||||
@@ -34,7 +35,8 @@ router = APIRouter(tags=["storage"])
|
||||
async def list_files(
|
||||
category: Optional[FileCategory] = None,
|
||||
path: Optional[str] = None,
|
||||
plugin_loader=Depends(get_plugin_loader)
|
||||
plugin_loader=Depends(get_plugin_loader),
|
||||
_ = Depends(has_permission("plugin:storage", "READ"))
|
||||
):
|
||||
with belief_scope("list_files"):
|
||||
storage_plugin: StoragePlugin = plugin_loader.get_plugin("storage-manager")
|
||||
@@ -63,7 +65,8 @@ async def upload_file(
|
||||
category: FileCategory = Form(...),
|
||||
path: Optional[str] = Form(None),
|
||||
file: UploadFile = File(...),
|
||||
plugin_loader=Depends(get_plugin_loader)
|
||||
plugin_loader=Depends(get_plugin_loader),
|
||||
_ = Depends(has_permission("plugin:storage", "WRITE"))
|
||||
):
|
||||
with belief_scope("upload_file"):
|
||||
storage_plugin: StoragePlugin = plugin_loader.get_plugin("storage-manager")
|
||||
@@ -89,7 +92,12 @@ async def upload_file(
|
||||
#
|
||||
# @RELATION: CALLS -> StoragePlugin.delete_file
|
||||
@router.delete("/files/{category}/{path:path}", status_code=204)
|
||||
async def delete_file(category: FileCategory, path: str, plugin_loader=Depends(get_plugin_loader)):
|
||||
async def delete_file(
|
||||
category: FileCategory,
|
||||
path: str,
|
||||
plugin_loader=Depends(get_plugin_loader),
|
||||
_ = Depends(has_permission("plugin:storage", "WRITE"))
|
||||
):
|
||||
with belief_scope("delete_file"):
|
||||
storage_plugin: StoragePlugin = plugin_loader.get_plugin("storage-manager")
|
||||
if not storage_plugin:
|
||||
@@ -114,7 +122,12 @@ async def delete_file(category: FileCategory, path: str, plugin_loader=Depends(g
|
||||
#
|
||||
# @RELATION: CALLS -> StoragePlugin.get_file_path
|
||||
@router.get("/download/{category}/{path:path}")
|
||||
async def download_file(category: FileCategory, path: str, plugin_loader=Depends(get_plugin_loader)):
|
||||
async def download_file(
|
||||
category: FileCategory,
|
||||
path: str,
|
||||
plugin_loader=Depends(get_plugin_loader),
|
||||
_ = Depends(has_permission("plugin:storage", "READ"))
|
||||
):
|
||||
with belief_scope("download_file"):
|
||||
storage_plugin: StoragePlugin = plugin_loader.get_plugin("storage-manager")
|
||||
if not storage_plugin:
|
||||
|
||||
@@ -64,7 +64,8 @@ async def list_tasks(
|
||||
limit: int = 10,
|
||||
offset: int = 0,
|
||||
status: Optional[TaskStatus] = None,
|
||||
task_manager: TaskManager = Depends(get_task_manager)
|
||||
task_manager: TaskManager = Depends(get_task_manager),
|
||||
_ = Depends(has_permission("tasks", "READ"))
|
||||
):
|
||||
"""
|
||||
Retrieve a list of tasks with pagination and optional status filter.
|
||||
@@ -83,7 +84,8 @@ async def list_tasks(
|
||||
# @RETURN: Task - The task details.
|
||||
async def get_task(
|
||||
task_id: str,
|
||||
task_manager: TaskManager = Depends(get_task_manager)
|
||||
task_manager: TaskManager = Depends(get_task_manager),
|
||||
_ = Depends(has_permission("tasks", "READ"))
|
||||
):
|
||||
"""
|
||||
Retrieve the details of a specific task.
|
||||
@@ -105,7 +107,8 @@ async def get_task(
|
||||
# @RETURN: List[LogEntry] - List of log entries.
|
||||
async def get_task_logs(
|
||||
task_id: str,
|
||||
task_manager: TaskManager = Depends(get_task_manager)
|
||||
task_manager: TaskManager = Depends(get_task_manager),
|
||||
_ = Depends(has_permission("tasks", "READ"))
|
||||
):
|
||||
"""
|
||||
Retrieve logs for a specific task.
|
||||
@@ -129,7 +132,8 @@ async def get_task_logs(
|
||||
async def resolve_task(
|
||||
task_id: str,
|
||||
request: ResolveTaskRequest,
|
||||
task_manager: TaskManager = Depends(get_task_manager)
|
||||
task_manager: TaskManager = Depends(get_task_manager),
|
||||
_ = Depends(has_permission("tasks", "WRITE"))
|
||||
):
|
||||
"""
|
||||
Resolve a task that is awaiting mapping.
|
||||
@@ -154,7 +158,8 @@ async def resolve_task(
|
||||
async def resume_task(
|
||||
task_id: str,
|
||||
request: ResumeTaskRequest,
|
||||
task_manager: TaskManager = Depends(get_task_manager)
|
||||
task_manager: TaskManager = Depends(get_task_manager),
|
||||
_ = Depends(has_permission("tasks", "WRITE"))
|
||||
):
|
||||
"""
|
||||
Resume a task that is awaiting input (e.g., passwords).
|
||||
@@ -176,7 +181,8 @@ async def resume_task(
|
||||
# @POST: Tasks are removed from memory/persistence.
|
||||
async def clear_tasks(
|
||||
status: Optional[TaskStatus] = None,
|
||||
task_manager: TaskManager = Depends(get_task_manager)
|
||||
task_manager: TaskManager = Depends(get_task_manager),
|
||||
_ = Depends(has_permission("tasks", "WRITE"))
|
||||
):
|
||||
"""
|
||||
Clear tasks matching the status filter. If no filter, clears all non-running tasks.
|
||||
|
||||
@@ -35,6 +35,16 @@ def register_adfs():
|
||||
)
|
||||
# [/DEF:register_adfs:Function]
|
||||
|
||||
# [DEF:is_adfs_configured:Function]
|
||||
# @PURPOSE: Checks if ADFS is properly configured.
|
||||
# @PRE: None.
|
||||
# @POST: Returns True if ADFS client is registered, False otherwise.
|
||||
# @RETURN: bool - Configuration status.
|
||||
def is_adfs_configured() -> bool:
|
||||
"""Check if ADFS OAuth client is registered."""
|
||||
return 'adfs' in oauth._registry
|
||||
# [/DEF:is_adfs_configured:Function]
|
||||
|
||||
# Initial registration
|
||||
register_adfs()
|
||||
|
||||
|
||||
@@ -86,7 +86,7 @@ def get_scheduler_service() -> SchedulerService:
|
||||
|
||||
# [DEF:oauth2_scheme:Variable]
|
||||
# @PURPOSE: OAuth2 password bearer scheme for token extraction.
|
||||
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="api/auth/login")
|
||||
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/auth/login")
|
||||
# [/DEF:oauth2_scheme:Variable]
|
||||
|
||||
# [DEF:get_current_user:Function]
|
||||
|
||||
@@ -95,7 +95,7 @@ class ADGroupMapping(Base):
|
||||
__tablename__ = "ad_group_mappings"
|
||||
|
||||
id = Column(String, primary_key=True, default=generate_uuid)
|
||||
ad_group_name = Column(String, unique=True, index=True, nullable=False)
|
||||
ad_group = Column(String, unique=True, index=True, nullable=False)
|
||||
role_id = Column(String, ForeignKey("roles.id"), nullable=False)
|
||||
|
||||
role = relationship("Role")
|
||||
|
||||
@@ -303,9 +303,9 @@ class MigrationPlugin(PluginBase):
|
||||
|
||||
try:
|
||||
exported_content, _ = from_c.export_dashboard(dash_id)
|
||||
with create_temp_file(content=exported_content, dry_run=True, suffix=".zip", logger=logger) as tmp_zip_path:
|
||||
with create_temp_file(content=exported_content, dry_run=True, suffix=".zip") as tmp_zip_path:
|
||||
# Always transform to strip databases to avoid password errors
|
||||
with create_temp_file(suffix=".zip", dry_run=True, logger=logger) as tmp_new_zip:
|
||||
with create_temp_file(suffix=".zip", dry_run=True) as tmp_new_zip:
|
||||
success = engine.transform_zip(str(tmp_zip_path), str(tmp_new_zip), db_mapping, strip_databases=False)
|
||||
|
||||
if not success and replace_db_config:
|
||||
|
||||
@@ -16,7 +16,8 @@ from pathlib import Path
|
||||
sys.path.append(str(Path(__file__).parent.parent.parent))
|
||||
|
||||
from src.core.database import AuthSessionLocal
|
||||
from src.models.auth import Permission
|
||||
from src.models.auth import Permission, Role
|
||||
from src.core.auth.repository import AuthRepository
|
||||
from src.core.logger import logger, belief_scope
|
||||
# [/SECTION]
|
||||
|
||||
@@ -29,6 +30,10 @@ INITIAL_PERMISSIONS = [
|
||||
{"resource": "admin:roles", "action": "WRITE"},
|
||||
{"resource": "admin:settings", "action": "READ"},
|
||||
{"resource": "admin:settings", "action": "WRITE"},
|
||||
{"resource": "environments", "action": "READ"},
|
||||
{"resource": "plugins", "action": "READ"},
|
||||
{"resource": "tasks", "action": "READ"},
|
||||
{"resource": "tasks", "action": "WRITE"},
|
||||
|
||||
# Plugin Permissions
|
||||
{"resource": "plugin:backup", "action": "EXECUTE"},
|
||||
@@ -37,6 +42,8 @@ INITIAL_PERMISSIONS = [
|
||||
{"resource": "plugin:search", "action": "EXECUTE"},
|
||||
{"resource": "plugin:git", "action": "EXECUTE"},
|
||||
{"resource": "plugin:storage", "action": "EXECUTE"},
|
||||
{"resource": "plugin:storage", "action": "READ"},
|
||||
{"resource": "plugin:storage", "action": "WRITE"},
|
||||
{"resource": "plugin:debug", "action": "EXECUTE"},
|
||||
]
|
||||
# [/DEF:INITIAL_PERMISSIONS:Constant]
|
||||
@@ -66,6 +73,36 @@ def seed_permissions():
|
||||
|
||||
db.commit()
|
||||
logger.info(f"Seeding completed. Added {count} new permissions.")
|
||||
|
||||
# Assign permissions to User role
|
||||
repo = AuthRepository(db)
|
||||
user_role = repo.get_role_by_name("User")
|
||||
if not user_role:
|
||||
user_role = Role(name="User", description="Standard user with plugin access")
|
||||
db.add(user_role)
|
||||
db.flush()
|
||||
|
||||
user_permissions = [
|
||||
("plugin:mapper", "EXECUTE"),
|
||||
("plugin:migration", "EXECUTE"),
|
||||
("plugin:backup", "EXECUTE"),
|
||||
("plugin:git", "EXECUTE"),
|
||||
("plugin:storage", "READ"),
|
||||
("plugin:storage", "WRITE"),
|
||||
("environments", "READ"),
|
||||
("plugins", "READ"),
|
||||
("tasks", "READ"),
|
||||
("tasks", "WRITE"),
|
||||
]
|
||||
|
||||
for res, act in user_permissions:
|
||||
perm = repo.get_permission_by_resource_action(res, act)
|
||||
if perm and perm not in user_role.permissions:
|
||||
user_role.permissions.append(perm)
|
||||
|
||||
db.commit()
|
||||
logger.info("User role permissions updated.")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to seed permissions: {e}")
|
||||
db.rollback()
|
||||
|
||||
@@ -101,7 +101,7 @@ class AuthService:
|
||||
# Update roles based on group mappings
|
||||
from ..models.auth import ADGroupMapping
|
||||
mapped_roles = self.repo.db.query(Role).join(ADGroupMapping).filter(
|
||||
ADGroupMapping.ad_group_name.in_(ad_groups)
|
||||
ADGroupMapping.ad_group.in_(ad_groups)
|
||||
).all()
|
||||
|
||||
user.roles = mapped_roles
|
||||
|
||||
@@ -10,9 +10,9 @@
|
||||
|
||||
# [SECTION: IMPORTS]
|
||||
from typing import List, Dict
|
||||
from backend.src.core.logger import belief_scope
|
||||
from backend.src.core.superset_client import SupersetClient
|
||||
from backend.src.core.utils.matching import suggest_mappings
|
||||
from ..core.logger import belief_scope
|
||||
from ..core.superset_client import SupersetClient
|
||||
from ..core.utils.matching import suggest_mappings
|
||||
# [/SECTION]
|
||||
|
||||
# [DEF:MappingService:Class]
|
||||
|
||||
BIN
backend/tasks.db
BIN
backend/tasks.db
Binary file not shown.
162
backend/tests/test_auth.py
Normal file
162
backend/tests/test_auth.py
Normal file
@@ -0,0 +1,162 @@
|
||||
import sys
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
# Add src to path
|
||||
sys.path.append(str(Path(__file__).parent.parent / "src"))
|
||||
|
||||
import pytest
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
from src.core.database import Base, get_auth_db
|
||||
from src.models.auth import User, Role, Permission, ADGroupMapping
|
||||
from src.services.auth_service import AuthService
|
||||
from src.core.auth.repository import AuthRepository
|
||||
from src.core.auth.security import verify_password, get_password_hash
|
||||
|
||||
# Create in-memory SQLite database for testing
|
||||
SQLALCHEMY_DATABASE_URL = "sqlite:///:memory:"
|
||||
|
||||
engine = create_engine(SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False})
|
||||
TestingSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
||||
|
||||
# Create all tables
|
||||
Base.metadata.create_all(bind=engine)
|
||||
|
||||
@pytest.fixture
|
||||
def db_session():
|
||||
"""Create a new database session with a transaction, rollback after test"""
|
||||
connection = engine.connect()
|
||||
transaction = connection.begin()
|
||||
session = TestingSessionLocal(bind=connection)
|
||||
|
||||
yield session
|
||||
|
||||
session.close()
|
||||
transaction.rollback()
|
||||
connection.close()
|
||||
|
||||
@pytest.fixture
|
||||
def auth_service(db_session):
|
||||
return AuthService(db_session)
|
||||
|
||||
@pytest.fixture
|
||||
def auth_repo(db_session):
|
||||
return AuthRepository(db_session)
|
||||
|
||||
def test_create_user(auth_repo):
|
||||
"""Test user creation"""
|
||||
user = User(
|
||||
username="testuser",
|
||||
email="test@example.com",
|
||||
password_hash=get_password_hash("testpassword123"),
|
||||
auth_source="LOCAL"
|
||||
)
|
||||
|
||||
auth_repo.db.add(user)
|
||||
auth_repo.db.commit()
|
||||
|
||||
retrieved_user = auth_repo.get_user_by_username("testuser")
|
||||
assert retrieved_user is not None
|
||||
assert retrieved_user.username == "testuser"
|
||||
assert retrieved_user.email == "test@example.com"
|
||||
assert verify_password("testpassword123", retrieved_user.password_hash)
|
||||
|
||||
def test_authenticate_user(auth_service, auth_repo):
|
||||
"""Test user authentication with valid and invalid credentials"""
|
||||
user = User(
|
||||
username="testuser",
|
||||
email="test@example.com",
|
||||
password_hash=get_password_hash("testpassword123"),
|
||||
auth_source="LOCAL"
|
||||
)
|
||||
|
||||
auth_repo.db.add(user)
|
||||
auth_repo.db.commit()
|
||||
|
||||
# Test valid credentials
|
||||
authenticated_user = auth_service.authenticate_user("testuser", "testpassword123")
|
||||
assert authenticated_user is not None
|
||||
assert authenticated_user.username == "testuser"
|
||||
|
||||
# Test invalid password
|
||||
invalid_user = auth_service.authenticate_user("testuser", "wrongpassword")
|
||||
assert invalid_user is None
|
||||
|
||||
# Test invalid username
|
||||
invalid_user = auth_service.authenticate_user("nonexistent", "testpassword123")
|
||||
assert invalid_user is None
|
||||
|
||||
def test_create_session(auth_service, auth_repo):
|
||||
"""Test session token creation"""
|
||||
user = User(
|
||||
username="testuser",
|
||||
email="test@example.com",
|
||||
password_hash=get_password_hash("testpassword123"),
|
||||
auth_source="LOCAL"
|
||||
)
|
||||
|
||||
auth_repo.db.add(user)
|
||||
auth_repo.db.commit()
|
||||
|
||||
session = auth_service.create_session(user)
|
||||
assert "access_token" in session
|
||||
assert "token_type" in session
|
||||
assert session["token_type"] == "bearer"
|
||||
assert len(session["access_token"]) > 0
|
||||
|
||||
def test_role_permission_association(auth_repo):
|
||||
"""Test role and permission association"""
|
||||
role = Role(name="Admin", description="System administrator")
|
||||
perm1 = Permission(resource="admin:users", action="READ")
|
||||
perm2 = Permission(resource="admin:users", action="WRITE")
|
||||
|
||||
role.permissions.extend([perm1, perm2])
|
||||
|
||||
auth_repo.db.add(role)
|
||||
auth_repo.db.commit()
|
||||
|
||||
retrieved_role = auth_repo.get_role_by_name("Admin")
|
||||
assert retrieved_role is not None
|
||||
assert len(retrieved_role.permissions) == 2
|
||||
|
||||
permissions = [f"{p.resource}:{p.action}" for p in retrieved_role.permissions]
|
||||
assert "admin:users:READ" in permissions
|
||||
assert "admin:users:WRITE" in permissions
|
||||
|
||||
def test_user_role_association(auth_repo):
|
||||
"""Test user and role association"""
|
||||
role = Role(name="Admin", description="System administrator")
|
||||
user = User(
|
||||
username="adminuser",
|
||||
email="admin@example.com",
|
||||
password_hash=get_password_hash("adminpass123"),
|
||||
auth_source="LOCAL"
|
||||
)
|
||||
|
||||
user.roles.append(role)
|
||||
|
||||
auth_repo.db.add(role)
|
||||
auth_repo.db.add(user)
|
||||
auth_repo.db.commit()
|
||||
|
||||
retrieved_user = auth_repo.get_user_by_username("adminuser")
|
||||
assert retrieved_user is not None
|
||||
assert len(retrieved_user.roles) == 1
|
||||
assert retrieved_user.roles[0].name == "Admin"
|
||||
|
||||
def test_ad_group_mapping(auth_repo):
|
||||
"""Test AD group mapping"""
|
||||
role = Role(name="ADFS_Admin", description="ADFS administrators")
|
||||
|
||||
auth_repo.db.add(role)
|
||||
auth_repo.db.commit()
|
||||
|
||||
mapping = ADGroupMapping(ad_group="DOMAIN\\ADFS_Admins", role_id=role.id)
|
||||
|
||||
auth_repo.db.add(mapping)
|
||||
auth_repo.db.commit()
|
||||
|
||||
retrieved_mapping = auth_repo.db.query(ADGroupMapping).filter_by(ad_group="DOMAIN\\ADFS_Admins").first()
|
||||
assert retrieved_mapping is not None
|
||||
assert retrieved_mapping.role_id == role.id
|
||||
@@ -21,7 +21,9 @@
|
||||
// @POST: tasks array is updated and selectedTask status synchronized.
|
||||
async function fetchTasks() {
|
||||
try {
|
||||
const res = await fetch('/api/tasks?limit=10');
|
||||
const token = localStorage.getItem('auth_token');
|
||||
const headers = token ? { 'Authorization': `Bearer ${token}` } : {};
|
||||
const res = await fetch('/api/tasks?limit=10', { headers });
|
||||
if (!res.ok) throw new Error('Failed to fetch tasks');
|
||||
tasks = await res.json();
|
||||
|
||||
@@ -58,7 +60,9 @@
|
||||
const params = new URLSearchParams();
|
||||
if (status) params.append('status', status);
|
||||
|
||||
const res = await fetch(`${url}?${params.toString()}`, { method: 'DELETE' });
|
||||
const token = localStorage.getItem('auth_token');
|
||||
const headers = token ? { 'Authorization': `Bearer ${token}` } : {};
|
||||
const res = await fetch(`${url}?${params.toString()}`, { method: 'DELETE', headers });
|
||||
if (!res.ok) throw new Error('Failed to clear tasks');
|
||||
|
||||
await fetchTasks();
|
||||
@@ -75,7 +79,9 @@
|
||||
async function selectTask(task) {
|
||||
try {
|
||||
// Fetch the full task details (including logs) before setting it as selected
|
||||
const res = await fetch(`/api/tasks/${task.id}`);
|
||||
const token = localStorage.getItem('auth_token');
|
||||
const headers = token ? { 'Authorization': `Bearer ${token}` } : {};
|
||||
const res = await fetch(`/api/tasks/${task.id}`, { headers });
|
||||
if (res.ok) {
|
||||
const fullTask = await res.json();
|
||||
selectedTask.set(fullTask);
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
<script>
|
||||
// [SECTION: IMPORTS]
|
||||
import { onMount } from 'svelte';
|
||||
import { api } from '../../lib/api.js';
|
||||
import { runTask, getTaskStatus } from '../../services/toolsService.js';
|
||||
import { selectedTask } from '../../lib/stores.js';
|
||||
import { addToast } from '../../lib/toasts.js';
|
||||
@@ -32,8 +33,7 @@
|
||||
*/
|
||||
async function fetchEnvironments() {
|
||||
try {
|
||||
const res = await fetch('/api/environments');
|
||||
envs = await res.json();
|
||||
envs = await api.getEnvironmentsList();
|
||||
} catch (e) {
|
||||
addToast('Failed to fetch environments', 'error');
|
||||
}
|
||||
|
||||
@@ -53,9 +53,15 @@ async function fetchApi(endpoint) {
|
||||
const response = await fetch(`${API_BASE_URL}${endpoint}`, {
|
||||
headers: getAuthHeaders()
|
||||
});
|
||||
console.log(`[api.fetchApi][Action] Received response context={{'status': ${response.status}, 'ok': ${response.ok}}}`);
|
||||
if (!response.ok) {
|
||||
throw new Error(`API request failed with status ${response.status}`);
|
||||
const errorData = await response.json().catch(() => ({}));
|
||||
const message = errorData.detail
|
||||
? (typeof errorData.detail === 'string' ? errorData.detail : JSON.stringify(errorData.detail))
|
||||
: `API request failed with status ${response.status}`;
|
||||
throw new Error(message);
|
||||
}
|
||||
if (response.status === 204) return null;
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
console.error(`[api.fetchApi][Coherence:Failed] Error fetching from ${endpoint}:`, error);
|
||||
@@ -80,9 +86,15 @@ async function postApi(endpoint, body) {
|
||||
headers: getAuthHeaders(),
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
console.log(`[api.postApi][Action] Received response context={{'status': ${response.status}, 'ok': ${response.ok}}}`);
|
||||
if (!response.ok) {
|
||||
throw new Error(`API request failed with status ${response.status}`);
|
||||
const errorData = await response.json().catch(() => ({}));
|
||||
const message = errorData.detail
|
||||
? (typeof errorData.detail === 'string' ? errorData.detail : JSON.stringify(errorData.detail))
|
||||
: `API request failed with status ${response.status}`;
|
||||
throw new Error(message);
|
||||
}
|
||||
if (response.status === 204) return null;
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
console.error(`[api.postApi][Coherence:Failed] Error posting to ${endpoint}:`, error);
|
||||
@@ -107,13 +119,19 @@ async function requestApi(endpoint, method = 'GET', body = null) {
|
||||
options.body = JSON.stringify(body);
|
||||
}
|
||||
const response = await fetch(`${API_BASE_URL}${endpoint}`, options);
|
||||
console.log(`[api.requestApi][Action] Received response context={{'status': ${response.status}, 'ok': ${response.ok}}}`);
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}));
|
||||
const message = errorData.detail
|
||||
? (typeof errorData.detail === 'string' ? errorData.detail : JSON.stringify(errorData.detail))
|
||||
: `API request failed with status ${response.status}`;
|
||||
console.error(`[api.requestApi][Action] Request failed context={{'status': ${response.status}, 'message': '${message}'}}`);
|
||||
throw new Error(message);
|
||||
}
|
||||
if (response.status === 204) {
|
||||
console.log('[api.requestApi][Action] 204 No Content received');
|
||||
return null;
|
||||
}
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
console.error(`[api.requestApi][Coherence:Failed] Error ${method} to ${endpoint}:`, error);
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
let roles = [];
|
||||
let loading = true;
|
||||
let error = null;
|
||||
let deletingUserId = null;
|
||||
|
||||
let showModal = false;
|
||||
let isEditing = false;
|
||||
@@ -130,9 +131,11 @@
|
||||
* @param {Object} user - The user to delete.
|
||||
*/
|
||||
async function handleDeleteUser(user) {
|
||||
if (deletingUserId) return;
|
||||
if (!confirm($t.admin.users.confirm_delete.replace('{username}', user.username))) return;
|
||||
|
||||
console.log('[AdminUsersPage][handleDeleteUser][Entry]');
|
||||
deletingUserId = user.id;
|
||||
try {
|
||||
await adminService.deleteUser(user.id);
|
||||
await loadData();
|
||||
@@ -140,6 +143,8 @@
|
||||
} catch (e) {
|
||||
alert("Failed to delete user: " + e.message);
|
||||
console.error('[AdminUsersPage][handleDeleteUser][Coherence:Failed]', e);
|
||||
} finally {
|
||||
deletingUserId = null;
|
||||
}
|
||||
}
|
||||
// [/DEF:handleDeleteUser:Function]
|
||||
@@ -208,8 +213,14 @@
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
|
||||
<button on:click={() => openEditModal(user)} class="text-blue-600 hover:text-blue-900 mr-3">{$t.common.edit}</button>
|
||||
<button on:click={() => handleDeleteUser(user)} class="text-red-600 hover:text-red-900">{$t.common.delete}</button>
|
||||
<button on:click={() => openEditModal(user)} class="text-blue-600 hover:text-blue-900 mr-3" disabled={deletingUserId === user.id}>{$t.common.edit}</button>
|
||||
<button
|
||||
on:click={() => handleDeleteUser(user)}
|
||||
class="text-red-600 hover:text-red-900 disabled:opacity-50"
|
||||
disabled={deletingUserId === user.id}
|
||||
>
|
||||
{deletingUserId === user.id ? ($t.common.deleting || 'Deleting...') : $t.common.delete}
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
import { onMount } from 'svelte';
|
||||
import DashboardGrid from '../../components/DashboardGrid.svelte';
|
||||
import { addToast as toast } from '../../lib/toasts.js';
|
||||
import { api } from '../../lib/api.js';
|
||||
import type { DashboardMetadata } from '../../types/dashboard';
|
||||
import { t } from '$lib/i18n';
|
||||
import { Button, Card, PageHeader, Select } from '$lib/ui';
|
||||
@@ -24,9 +25,7 @@
|
||||
// @POST: `environments` array is populated with data from /api/environments.
|
||||
async function fetchEnvironments() {
|
||||
try {
|
||||
const response = await fetch('/api/environments');
|
||||
if (!response.ok) throw new Error('Failed to fetch environments');
|
||||
environments = await response.json();
|
||||
environments = await api.getEnvironmentsList();
|
||||
if (environments.length > 0) {
|
||||
selectedEnvId = environments[0].id;
|
||||
}
|
||||
@@ -46,9 +45,7 @@
|
||||
if (!envId) return;
|
||||
fetchingDashboards = true;
|
||||
try {
|
||||
const response = await fetch(`/api/environments/${envId}/dashboards`);
|
||||
if (!response.ok) throw new Error('Failed to fetch dashboards');
|
||||
dashboards = await response.json();
|
||||
dashboards = await api.requestApi(`/environments/${envId}/dashboards`);
|
||||
} catch (e) {
|
||||
toast(e.message, 'error');
|
||||
dashboards = [];
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
import TaskHistory from '../../components/TaskHistory.svelte';
|
||||
import TaskLogViewer from '../../components/TaskLogViewer.svelte';
|
||||
import PasswordPrompt from '../../components/PasswordPrompt.svelte';
|
||||
import { api } from '../../lib/api.js';
|
||||
import { selectedTask } from '../../lib/stores.js';
|
||||
import { resumeTask } from '../../services/taskService.js';
|
||||
import type { DashboardMetadata, DashboardSelection } from '../../types/dashboard';
|
||||
@@ -58,9 +59,7 @@
|
||||
*/
|
||||
async function fetchEnvironments() {
|
||||
try {
|
||||
const response = await fetch('/api/environments');
|
||||
if (!response.ok) throw new Error('Failed to fetch environments');
|
||||
environments = await response.json();
|
||||
environments = await api.getEnvironmentsList();
|
||||
} catch (e) {
|
||||
error = e.message;
|
||||
} finally {
|
||||
@@ -78,9 +77,7 @@
|
||||
*/
|
||||
async function fetchDashboards(envId: string) {
|
||||
try {
|
||||
const response = await fetch(`/api/environments/${envId}/dashboards`);
|
||||
if (!response.ok) throw new Error('Failed to fetch dashboards');
|
||||
dashboards = await response.json();
|
||||
dashboards = await api.requestApi(`/environments/${envId}/dashboards`);
|
||||
selectedDashboardIds = []; // Reset selection when env changes
|
||||
} catch (e) {
|
||||
error = e.message;
|
||||
@@ -106,23 +103,17 @@
|
||||
error = "";
|
||||
|
||||
try {
|
||||
const [srcRes, tgtRes, mapRes, sugRes] = await Promise.all([
|
||||
fetch(`/api/environments/${sourceEnvId}/databases`),
|
||||
fetch(`/api/environments/${targetEnvId}/databases`),
|
||||
fetch(`/api/mappings?source_env_id=${sourceEnvId}&target_env_id=${targetEnvId}`),
|
||||
fetch(`/api/mappings/suggest`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ source_env_id: sourceEnvId, target_env_id: targetEnvId })
|
||||
})
|
||||
const [src, tgt, maps, sugs] = await Promise.all([
|
||||
api.requestApi(`/environments/${sourceEnvId}/databases`),
|
||||
api.requestApi(`/environments/${targetEnvId}/databases`),
|
||||
api.requestApi(`/mappings?source_env_id=${sourceEnvId}&target_env_id=${targetEnvId}`),
|
||||
api.postApi(`/mappings/suggest`, { source_env_id: sourceEnvId, target_env_id: targetEnvId })
|
||||
]);
|
||||
|
||||
if (!srcRes.ok || !tgtRes.ok) throw new Error('Failed to fetch databases from environments');
|
||||
|
||||
sourceDatabases = await srcRes.json();
|
||||
targetDatabases = await tgtRes.json();
|
||||
mappings = await mapRes.json();
|
||||
suggestions = await sugRes.json();
|
||||
sourceDatabases = src;
|
||||
targetDatabases = tgt;
|
||||
mappings = maps;
|
||||
suggestions = sugs;
|
||||
} catch (e) {
|
||||
error = e.message;
|
||||
} finally {
|
||||
@@ -145,22 +136,15 @@
|
||||
if (!sDb || !tDb) return;
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/mappings', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
source_env_id: sourceEnvId,
|
||||
target_env_id: targetEnvId,
|
||||
source_db_uuid: sourceUuid,
|
||||
target_db_uuid: targetUuid,
|
||||
source_db_name: sDb.database_name,
|
||||
target_db_name: tDb.database_name
|
||||
})
|
||||
const savedMapping = await api.postApi('/mappings', {
|
||||
source_env_id: sourceEnvId,
|
||||
target_env_id: targetEnvId,
|
||||
source_db_uuid: sourceUuid,
|
||||
target_db_uuid: targetUuid,
|
||||
source_db_name: sDb.database_name,
|
||||
target_db_name: tDb.database_name
|
||||
});
|
||||
|
||||
if (!response.ok) throw new Error('Failed to save mapping');
|
||||
|
||||
const savedMapping = await response.json();
|
||||
mappings = [...mappings.filter(m => m.source_db_uuid !== sourceUuid), savedMapping];
|
||||
} catch (e) {
|
||||
error = e.message;
|
||||
@@ -253,14 +237,7 @@
|
||||
replace_db_config: replaceDb
|
||||
};
|
||||
console.log(`[MigrationDashboard][Action] Starting migration with selection:`, selection);
|
||||
const response = await fetch('/api/migration/execute', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(selection)
|
||||
});
|
||||
console.log(`[MigrationDashboard][Action] API response status: ${response.status}`);
|
||||
if (!response.ok) throw new Error(`Failed to start migration: ${response.status} ${response.statusText}`);
|
||||
const result = await response.json();
|
||||
const result = await api.postApi('/migration/execute', selection);
|
||||
console.log(`[MigrationDashboard][Action] Migration started: ${result.task_id} - ${result.message}`);
|
||||
|
||||
// Wait a brief moment for the backend to ensure the task is retrievable
|
||||
@@ -268,23 +245,18 @@
|
||||
|
||||
// Fetch full task details and switch to TaskRunner view
|
||||
try {
|
||||
const taskRes = await fetch(`/api/tasks/${result.task_id}`);
|
||||
if (taskRes.ok) {
|
||||
const task = await taskRes.json();
|
||||
selectedTask.set(task);
|
||||
} else {
|
||||
// Fallback: create a temporary task object to switch view immediately
|
||||
console.warn("Could not fetch task details immediately, using placeholder.");
|
||||
selectedTask.set({
|
||||
id: result.task_id,
|
||||
plugin_id: 'superset-migration',
|
||||
status: 'RUNNING',
|
||||
logs: [],
|
||||
params: {}
|
||||
});
|
||||
}
|
||||
const task = await api.getTask(result.task_id);
|
||||
selectedTask.set(task);
|
||||
} catch (fetchErr) {
|
||||
console.error("Failed to fetch new task details:", fetchErr);
|
||||
// Fallback: create a temporary task object to switch view immediately
|
||||
console.warn("Could not fetch task details immediately, using placeholder.");
|
||||
selectedTask.set({
|
||||
id: result.task_id,
|
||||
plugin_id: 'superset-migration',
|
||||
status: 'RUNNING',
|
||||
logs: [],
|
||||
params: {}
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(`[MigrationDashboard][Failure] Migration failed:`, e);
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
<script lang="ts">
|
||||
// [SECTION: IMPORTS]
|
||||
import { onMount } from 'svelte';
|
||||
import { api } from '../../../lib/api.js';
|
||||
import EnvSelector from '../../../components/EnvSelector.svelte';
|
||||
import MappingTable from '../../../components/MappingTable.svelte';
|
||||
import { t } from '$lib/i18n';
|
||||
@@ -38,9 +39,7 @@
|
||||
// @POST: environments array is populated.
|
||||
async function fetchEnvironments() {
|
||||
try {
|
||||
const response = await fetch('/api/environments');
|
||||
if (!response.ok) throw new Error('Failed to fetch environments');
|
||||
environments = await response.json();
|
||||
environments = await api.getEnvironmentsList();
|
||||
} catch (e) {
|
||||
error = e.message;
|
||||
} finally {
|
||||
@@ -64,23 +63,17 @@
|
||||
success = "";
|
||||
|
||||
try {
|
||||
const [srcRes, tgtRes, mapRes, sugRes] = await Promise.all([
|
||||
fetch(`/api/environments/${sourceEnvId}/databases`),
|
||||
fetch(`/api/environments/${targetEnvId}/databases`),
|
||||
fetch(`/api/mappings?source_env_id=${sourceEnvId}&target_env_id=${targetEnvId}`),
|
||||
fetch(`/api/mappings/suggest`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ source_env_id: sourceEnvId, target_env_id: targetEnvId })
|
||||
})
|
||||
const [src, tgt, maps, sugs] = await Promise.all([
|
||||
api.requestApi(`/environments/${sourceEnvId}/databases`),
|
||||
api.requestApi(`/environments/${targetEnvId}/databases`),
|
||||
api.requestApi(`/mappings?source_env_id=${sourceEnvId}&target_env_id=${targetEnvId}`),
|
||||
api.postApi(`/mappings/suggest`, { source_env_id: sourceEnvId, target_env_id: targetEnvId })
|
||||
]);
|
||||
|
||||
if (!srcRes.ok || !tgtRes.ok) throw new Error('Failed to fetch databases from environments');
|
||||
|
||||
sourceDatabases = await srcRes.json();
|
||||
targetDatabases = await tgtRes.json();
|
||||
mappings = await mapRes.json();
|
||||
suggestions = await sugRes.json();
|
||||
sourceDatabases = src;
|
||||
targetDatabases = tgt;
|
||||
mappings = maps;
|
||||
suggestions = sugs;
|
||||
} catch (e) {
|
||||
error = e.message;
|
||||
} finally {
|
||||
@@ -103,22 +96,15 @@
|
||||
if (!sDb || !tDb) return;
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/mappings', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
source_env_id: sourceEnvId,
|
||||
target_env_id: targetEnvId,
|
||||
source_db_uuid: sourceUuid,
|
||||
target_db_uuid: targetUuid,
|
||||
source_db_name: sDb.database_name,
|
||||
target_db_name: tDb.database_name
|
||||
})
|
||||
const savedMapping = await api.postApi('/mappings', {
|
||||
source_env_id: sourceEnvId,
|
||||
target_env_id: targetEnvId,
|
||||
source_db_uuid: sourceUuid,
|
||||
target_db_uuid: targetUuid,
|
||||
source_db_name: sDb.database_name,
|
||||
target_db_name: tDb.database_name
|
||||
});
|
||||
|
||||
if (!response.ok) throw new Error('Failed to save mapping');
|
||||
|
||||
const savedMapping = await response.json();
|
||||
mappings = [...mappings.filter(m => m.source_db_uuid !== sourceUuid), savedMapping];
|
||||
success = "Mapping saved successfully";
|
||||
} catch (e) {
|
||||
|
||||
@@ -2,7 +2,9 @@
|
||||
* Service for interacting with the Connection Management API.
|
||||
*/
|
||||
|
||||
const API_BASE = '/api/settings/connections';
|
||||
import { requestApi } from '../lib/api';
|
||||
|
||||
const API_BASE = '/settings/connections';
|
||||
|
||||
// [DEF:getConnections:Function]
|
||||
/* @PURPOSE: Fetch a list of saved connections.
|
||||
@@ -14,11 +16,7 @@ const API_BASE = '/api/settings/connections';
|
||||
* @returns {Promise<Array>} List of connections.
|
||||
*/
|
||||
export async function getConnections() {
|
||||
const response = await fetch(API_BASE);
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch connections: ${response.statusText}`);
|
||||
}
|
||||
return await response.json();
|
||||
return requestApi(API_BASE);
|
||||
}
|
||||
// [/DEF:getConnections:Function]
|
||||
|
||||
@@ -33,19 +31,7 @@ export async function getConnections() {
|
||||
* @returns {Promise<Object>} The created connection instance.
|
||||
*/
|
||||
export async function createConnection(connectionData) {
|
||||
const response = await fetch(API_BASE, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(connectionData)
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}));
|
||||
throw new Error(errorData.detail || `Failed to create connection: ${response.statusText}`);
|
||||
}
|
||||
return await response.json();
|
||||
return requestApi(API_BASE, 'POST', connectionData);
|
||||
}
|
||||
// [/DEF:createConnection:Function]
|
||||
|
||||
@@ -59,12 +45,6 @@ export async function createConnection(connectionData) {
|
||||
* @param {string} connectionId - The ID of the connection to delete.
|
||||
*/
|
||||
export async function deleteConnection(connectionId) {
|
||||
const response = await fetch(`${API_BASE}/${connectionId}`, {
|
||||
method: 'DELETE'
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to delete connection: ${response.statusText}`);
|
||||
}
|
||||
return requestApi(`${API_BASE}/${connectionId}`, 'DELETE');
|
||||
}
|
||||
// [/DEF:deleteConnection:Function]
|
||||
@@ -6,7 +6,9 @@
|
||||
* @RELATION: DEPENDS_ON -> specs/011-git-integration-dashboard/contracts/api.md
|
||||
*/
|
||||
|
||||
const API_BASE = '/api/git';
|
||||
import { requestApi } from '../lib/api';
|
||||
|
||||
const API_BASE = '/git';
|
||||
|
||||
// [DEF:gitService:Action]
|
||||
export const gitService = {
|
||||
@@ -19,9 +21,7 @@ export const gitService = {
|
||||
*/
|
||||
async getConfigs() {
|
||||
console.log('[getConfigs][Action] Fetching Git configs');
|
||||
const response = await fetch(`${API_BASE}/config`);
|
||||
if (!response.ok) throw new Error('Failed to fetch Git configs');
|
||||
return response.json();
|
||||
return requestApi(`${API_BASE}/config`);
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -34,13 +34,7 @@ export const gitService = {
|
||||
*/
|
||||
async createConfig(config) {
|
||||
console.log('[createConfig][Action] Creating Git config');
|
||||
const response = await fetch(`${API_BASE}/config`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(config)
|
||||
});
|
||||
if (!response.ok) throw new Error('Failed to create Git config');
|
||||
return response.json();
|
||||
return requestApi(`${API_BASE}/config`, 'POST', config);
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -53,11 +47,7 @@ export const gitService = {
|
||||
*/
|
||||
async deleteConfig(configId) {
|
||||
console.log(`[deleteConfig][Action] Deleting Git config ${configId}`);
|
||||
const response = await fetch(`${API_BASE}/config/${configId}`, {
|
||||
method: 'DELETE'
|
||||
});
|
||||
if (!response.ok) throw new Error('Failed to delete Git config');
|
||||
return response.json();
|
||||
return requestApi(`${API_BASE}/config/${configId}`, 'DELETE');
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -70,12 +60,7 @@ export const gitService = {
|
||||
*/
|
||||
async testConnection(config) {
|
||||
console.log('[testConnection][Action] Testing Git connection');
|
||||
const response = await fetch(`${API_BASE}/config/test`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(config)
|
||||
});
|
||||
return response.json();
|
||||
return requestApi(`${API_BASE}/config/test`, 'POST', config);
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -90,16 +75,10 @@ export const gitService = {
|
||||
*/
|
||||
async initRepository(dashboardId, configId, remoteUrl) {
|
||||
console.log(`[initRepository][Action] Initializing repo for dashboard ${dashboardId}`);
|
||||
const response = await fetch(`${API_BASE}/repositories/${dashboardId}/init`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ config_id: configId, remote_url: remoteUrl })
|
||||
return requestApi(`${API_BASE}/repositories/${dashboardId}/init`, 'POST', {
|
||||
config_id: configId,
|
||||
remote_url: remoteUrl
|
||||
});
|
||||
if (!response.ok) {
|
||||
const err = await response.json();
|
||||
throw new Error(err.detail || 'Failed to initialize repository');
|
||||
}
|
||||
return response.json();
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -112,9 +91,7 @@ export const gitService = {
|
||||
*/
|
||||
async getBranches(dashboardId) {
|
||||
console.log(`[getBranches][Action] Fetching branches for dashboard ${dashboardId}`);
|
||||
const response = await fetch(`${API_BASE}/repositories/${dashboardId}/branches`);
|
||||
if (!response.ok) throw new Error('Failed to fetch branches');
|
||||
return response.json();
|
||||
return requestApi(`${API_BASE}/repositories/${dashboardId}/branches`);
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -129,13 +106,10 @@ export const gitService = {
|
||||
*/
|
||||
async createBranch(dashboardId, name, fromBranch) {
|
||||
console.log(`[createBranch][Action] Creating branch ${name} for dashboard ${dashboardId}`);
|
||||
const response = await fetch(`${API_BASE}/repositories/${dashboardId}/branches`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ name, from_branch: fromBranch })
|
||||
return requestApi(`${API_BASE}/repositories/${dashboardId}/branches`, 'POST', {
|
||||
name,
|
||||
from_branch: fromBranch
|
||||
});
|
||||
if (!response.ok) throw new Error('Failed to create branch');
|
||||
return response.json();
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -149,13 +123,7 @@ export const gitService = {
|
||||
*/
|
||||
async checkoutBranch(dashboardId, name) {
|
||||
console.log(`[checkoutBranch][Action] Checking out branch ${name} for dashboard ${dashboardId}`);
|
||||
const response = await fetch(`${API_BASE}/repositories/${dashboardId}/checkout`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ name })
|
||||
});
|
||||
if (!response.ok) throw new Error('Failed to checkout branch');
|
||||
return response.json();
|
||||
return requestApi(`${API_BASE}/repositories/${dashboardId}/checkout`, 'POST', { name });
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -170,13 +138,7 @@ export const gitService = {
|
||||
*/
|
||||
async commit(dashboardId, message, files) {
|
||||
console.log(`[commit][Action] Committing changes for dashboard ${dashboardId}`);
|
||||
const response = await fetch(`${API_BASE}/repositories/${dashboardId}/commit`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ message, files })
|
||||
});
|
||||
if (!response.ok) throw new Error('Failed to commit changes');
|
||||
return response.json();
|
||||
return requestApi(`${API_BASE}/repositories/${dashboardId}/commit`, 'POST', { message, files });
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -189,11 +151,7 @@ export const gitService = {
|
||||
*/
|
||||
async push(dashboardId) {
|
||||
console.log(`[push][Action] Pushing changes for dashboard ${dashboardId}`);
|
||||
const response = await fetch(`${API_BASE}/repositories/${dashboardId}/push`, {
|
||||
method: 'POST'
|
||||
});
|
||||
if (!response.ok) throw new Error('Failed to push changes');
|
||||
return response.json();
|
||||
return requestApi(`${API_BASE}/repositories/${dashboardId}/push`, 'POST');
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -206,11 +164,7 @@ export const gitService = {
|
||||
*/
|
||||
async pull(dashboardId) {
|
||||
console.log(`[pull][Action] Pulling changes for dashboard ${dashboardId}`);
|
||||
const response = await fetch(`${API_BASE}/repositories/${dashboardId}/pull`, {
|
||||
method: 'POST'
|
||||
});
|
||||
if (!response.ok) throw new Error('Failed to pull changes');
|
||||
return response.json();
|
||||
return requestApi(`${API_BASE}/repositories/${dashboardId}/pull`, 'POST');
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -221,9 +175,7 @@ export const gitService = {
|
||||
*/
|
||||
async getEnvironments() {
|
||||
console.log('[getEnvironments][Action] Fetching environments');
|
||||
const response = await fetch(`${API_BASE}/environments`);
|
||||
if (!response.ok) throw new Error('Failed to fetch environments');
|
||||
return response.json();
|
||||
return requestApi(`${API_BASE}/environments`);
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -237,13 +189,9 @@ export const gitService = {
|
||||
*/
|
||||
async deploy(dashboardId, environmentId) {
|
||||
console.log(`[deploy][Action] Deploying dashboard ${dashboardId} to environment ${environmentId}`);
|
||||
const response = await fetch(`${API_BASE}/repositories/${dashboardId}/deploy`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ environment_id: environmentId })
|
||||
return requestApi(`${API_BASE}/repositories/${dashboardId}/deploy`, 'POST', {
|
||||
environment_id: environmentId
|
||||
});
|
||||
if (!response.ok) throw new Error('Failed to deploy dashboard');
|
||||
return response.json();
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -255,9 +203,7 @@ export const gitService = {
|
||||
*/
|
||||
async getHistory(dashboardId, limit = 50) {
|
||||
console.log(`[getHistory][Action] Fetching history for dashboard ${dashboardId}`);
|
||||
const response = await fetch(`${API_BASE}/repositories/${dashboardId}/history?limit=${limit}`);
|
||||
if (!response.ok) throw new Error('Failed to fetch commit history');
|
||||
return response.json();
|
||||
return requestApi(`${API_BASE}/repositories/${dashboardId}/history?limit=${limit}`);
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -269,17 +215,9 @@ export const gitService = {
|
||||
*/
|
||||
async sync(dashboardId, sourceEnvId = null) {
|
||||
console.log(`[sync][Action] Syncing dashboard ${dashboardId}`);
|
||||
const url = new URL(`${window.location.origin}${API_BASE}/repositories/${dashboardId}/sync`);
|
||||
if (sourceEnvId) url.searchParams.append('source_env_id', sourceEnvId);
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'POST'
|
||||
});
|
||||
if (!response.ok) {
|
||||
const err = await response.json();
|
||||
throw new Error(err.detail || 'Failed to sync dashboard');
|
||||
}
|
||||
return response.json();
|
||||
let endpoint = `${API_BASE}/repositories/${dashboardId}/sync`;
|
||||
if (sourceEnvId) endpoint += `?source_env_id=${sourceEnvId}`;
|
||||
return requestApi(endpoint, 'POST');
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -292,9 +230,7 @@ export const gitService = {
|
||||
*/
|
||||
async getStatus(dashboardId) {
|
||||
console.log(`[getStatus][Action] Fetching status for dashboard ${dashboardId}`);
|
||||
const response = await fetch(`${API_BASE}/repositories/${dashboardId}/status`);
|
||||
if (!response.ok) throw new Error('Failed to fetch status');
|
||||
return response.json();
|
||||
return requestApi(`${API_BASE}/repositories/${dashboardId}/status`);
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -309,15 +245,12 @@ export const gitService = {
|
||||
*/
|
||||
async getDiff(dashboardId, filePath = null, staged = false) {
|
||||
console.log(`[getDiff][Action] Fetching diff for dashboard ${dashboardId} (file: ${filePath}, staged: ${staged})`);
|
||||
let url = `${API_BASE}/repositories/${dashboardId}/diff`;
|
||||
let endpoint = `${API_BASE}/repositories/${dashboardId}/diff`;
|
||||
const params = new URLSearchParams();
|
||||
if (filePath) params.append('file_path', filePath);
|
||||
if (staged) params.append('staged', 'true');
|
||||
if (params.toString()) url += `?${params.toString()}`;
|
||||
|
||||
const response = await fetch(url);
|
||||
if (!response.ok) throw new Error('Failed to fetch diff');
|
||||
return response.json();
|
||||
if (params.toString()) endpoint += `?${params.toString()}`;
|
||||
return requestApi(endpoint);
|
||||
}
|
||||
};
|
||||
// [/DEF:gitService:Action]
|
||||
|
||||
@@ -2,7 +2,9 @@
|
||||
* Service for interacting with the Task Management API.
|
||||
*/
|
||||
|
||||
const API_BASE = '/api/tasks';
|
||||
import { requestApi } from '../lib/api';
|
||||
|
||||
const API_BASE = '/tasks';
|
||||
|
||||
// [DEF:getTasks:Function]
|
||||
/* @PURPOSE: Fetch a list of tasks with pagination and optional status filter.
|
||||
@@ -25,11 +27,7 @@ export async function getTasks(limit = 10, offset = 0, status = null) {
|
||||
params.append('status', status);
|
||||
}
|
||||
|
||||
const response = await fetch(`${API_BASE}?${params.toString()}`);
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch tasks: ${response.statusText}`);
|
||||
}
|
||||
return await response.json();
|
||||
return requestApi(`${API_BASE}?${params.toString()}`);
|
||||
}
|
||||
// [/DEF:getTasks:Function]
|
||||
|
||||
@@ -44,11 +42,7 @@ export async function getTasks(limit = 10, offset = 0, status = null) {
|
||||
* @returns {Promise<Object>} Task details.
|
||||
*/
|
||||
export async function getTask(taskId) {
|
||||
const response = await fetch(`${API_BASE}/${taskId}`);
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch task ${taskId}: ${response.statusText}`);
|
||||
}
|
||||
return await response.json();
|
||||
return requestApi(`${API_BASE}/${taskId}`);
|
||||
}
|
||||
// [/DEF:getTask:Function]
|
||||
|
||||
@@ -86,19 +80,7 @@ export async function getTaskLogs(taskId) {
|
||||
* @returns {Promise<Object>} Updated task object.
|
||||
*/
|
||||
export async function resumeTask(taskId, passwords) {
|
||||
const response = await fetch(`${API_BASE}/${taskId}/resume`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ passwords })
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}));
|
||||
throw new Error(errorData.detail || `Failed to resume task: ${response.statusText}`);
|
||||
}
|
||||
return await response.json();
|
||||
return requestApi(`${API_BASE}/${taskId}/resume`, 'POST', { passwords });
|
||||
}
|
||||
// [/DEF:resumeTask:Function]
|
||||
|
||||
@@ -114,19 +96,7 @@ export async function resumeTask(taskId, passwords) {
|
||||
* @returns {Promise<Object>} Updated task object.
|
||||
*/
|
||||
export async function resolveTask(taskId, resolutionParams) {
|
||||
const response = await fetch(`${API_BASE}/${taskId}/resolve`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ resolution_params: resolutionParams })
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}));
|
||||
throw new Error(errorData.detail || `Failed to resolve task: ${response.statusText}`);
|
||||
}
|
||||
return await response.json();
|
||||
return requestApi(`${API_BASE}/${taskId}/resolve`, 'POST', { resolution_params: resolutionParams });
|
||||
}
|
||||
// [/DEF:resolveTask:Function]
|
||||
|
||||
@@ -145,12 +115,6 @@ export async function clearTasks(status = null) {
|
||||
params.append('status', status);
|
||||
}
|
||||
|
||||
const response = await fetch(`${API_BASE}?${params.toString()}`, {
|
||||
method: 'DELETE'
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to clear tasks: ${response.statusText}`);
|
||||
}
|
||||
return requestApi(`${API_BASE}?${params.toString()}`, 'DELETE');
|
||||
}
|
||||
// [/DEF:clearTasks:Function]
|
||||
@@ -2,7 +2,9 @@
|
||||
* Service for generic Task API communication used by Tools.
|
||||
*/
|
||||
|
||||
const API_BASE = '/api/tasks';
|
||||
import { requestApi } from '../lib/api';
|
||||
|
||||
const API_BASE = '/tasks';
|
||||
|
||||
// [DEF:runTask:Function]
|
||||
/* @PURPOSE: Start a new task for a given plugin.
|
||||
@@ -16,19 +18,7 @@ const API_BASE = '/api/tasks';
|
||||
* @returns {Promise<Object>} The created task instance.
|
||||
*/
|
||||
export async function runTask(pluginId, params) {
|
||||
const response = await fetch(API_BASE, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ plugin_id: pluginId, params })
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}));
|
||||
throw new Error(errorData.detail || `Failed to start task: ${response.statusText}`);
|
||||
}
|
||||
return await response.json();
|
||||
return requestApi(API_BASE, 'POST', { plugin_id: pluginId, params });
|
||||
}
|
||||
// [/DEF:runTask:Function]
|
||||
|
||||
@@ -43,10 +33,6 @@ export async function runTask(pluginId, params) {
|
||||
* @returns {Promise<Object>} Task details.
|
||||
*/
|
||||
export async function getTaskStatus(taskId) {
|
||||
const response = await fetch(`${API_BASE}/${taskId}`);
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch task ${taskId}: ${response.statusText}`);
|
||||
}
|
||||
return await response.json();
|
||||
return requestApi(`${API_BASE}/${taskId}`);
|
||||
}
|
||||
// [/DEF:getTaskStatus:Function]
|
||||
@@ -44,13 +44,13 @@
|
||||
- [x] T021 [US2] Protect existing plugin API routes using `has_permission` in `backend/src/api/routes/*.py`
|
||||
- [x] T022 [US2] Implement `SystemAdminPlugin` inheriting from `PluginBase` for User/Role management in `backend/src/plugins/system_admin.py`
|
||||
- [x] T023 [US2] Implement Admin API endpoints within `SystemAdminPlugin` in `backend/src/api/routes/admin.py`
|
||||
- [ ] T053 [US2] Extend Admin API with User Update/Delete and Role CRUD endpoints in `backend/src/api/routes/admin.py`
|
||||
- [ ] T054 [US2] Add Pydantic schemas for UserUpdate, RoleCreate, RoleUpdate in `backend/src/schemas/auth.py`
|
||||
- [x] T053 [US2] Extend Admin API with User Update/Delete and Role CRUD endpoints in `backend/src/api/routes/admin.py`
|
||||
- [x] T054 [US2] Add Pydantic schemas for UserUpdate, RoleCreate, RoleUpdate in `backend/src/schemas/auth.py`
|
||||
- [x] T051 [US2] Implement `adminService.js` for frontend API orchestration
|
||||
- [ ] T055 [US2] Update `adminService.js` with new CRUD methods
|
||||
- [x] T055 [US2] Update `adminService.js` with new CRUD methods
|
||||
- [x] T024 [US2] Create Admin Dashboard UI using `src/lib/ui` and `src/lib/i18n` in `frontend/src/routes/admin/users/+page.svelte`
|
||||
- [ ] T056 [US2] Update Admin User Dashboard to support Edit/Delete operations in `frontend/src/routes/admin/users/+page.svelte`
|
||||
- [ ] T057 [US4] Create Role Management UI in `frontend/src/routes/admin/roles/+page.svelte`
|
||||
- [x] T056 [US2] Update Admin User Dashboard to support Edit/Delete operations in `frontend/src/routes/admin/users/+page.svelte`
|
||||
- [x] T057 [US4] Create Role Management UI in `frontend/src/routes/admin/roles/+page.svelte`
|
||||
- [x] T025 [US2] Update Navigation Bar to hide links and show user profile/logout using `src/lib/ui` in `frontend/src/components/Navbar.svelte`
|
||||
- [x] T042 [US2] Implement `PermissionGuard` frontend component for granular UI element protection in `frontend/src/components/auth/PermissionGuard.svelte`
|
||||
- [x] T045 [US2] Implement multi-role permission resolution logic (union of permissions) in `backend/src/services/auth_service.py`
|
||||
|
||||
Reference in New Issue
Block a user