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 ..services.auth_service import AuthService
|
||||||
from ..schemas.auth import Token, User as UserSchema
|
from ..schemas.auth import Token, User as UserSchema
|
||||||
from ..dependencies import get_current_user
|
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.auth.logger import log_security_event
|
||||||
from ..core.logger import belief_scope
|
from ..core.logger import belief_scope
|
||||||
import starlette.requests
|
import starlette.requests
|
||||||
@@ -85,6 +85,11 @@ async def logout(current_user: UserSchema = Depends(get_current_user)):
|
|||||||
@router.get("/login/adfs")
|
@router.get("/login/adfs")
|
||||||
async def login_adfs(request: starlette.requests.Request):
|
async def login_adfs(request: starlette.requests.Request):
|
||||||
with belief_scope("api.auth.login_adfs"):
|
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')
|
redirect_uri = request.url_for('auth_callback_adfs')
|
||||||
return await oauth.adfs.authorize_redirect(request, str(redirect_uri))
|
return await oauth.adfs.authorize_redirect(request, str(redirect_uri))
|
||||||
# [/DEF:login_adfs:Function]
|
# [/DEF:login_adfs:Function]
|
||||||
@@ -95,6 +100,11 @@ async def login_adfs(request: starlette.requests.Request):
|
|||||||
@router.get("/callback/adfs", name="auth_callback_adfs")
|
@router.get("/callback/adfs", name="auth_callback_adfs")
|
||||||
async def auth_callback_adfs(request: starlette.requests.Request, db: Session = Depends(get_auth_db)):
|
async def auth_callback_adfs(request: starlette.requests.Request, db: Session = Depends(get_auth_db)):
|
||||||
with belief_scope("api.auth.callback_adfs"):
|
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)
|
token = await oauth.adfs.authorize_access_token(request)
|
||||||
user_info = token.get('userinfo')
|
user_info = token.get('userinfo')
|
||||||
if not user_info:
|
if not user_info:
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ from ...schemas.auth import (
|
|||||||
)
|
)
|
||||||
from ...models.auth import User, Role, Permission, ADGroupMapping
|
from ...models.auth import User, Role, Permission, ADGroupMapping
|
||||||
from ...dependencies import has_permission, get_current_user
|
from ...dependencies import has_permission, get_current_user
|
||||||
from ...core.logger import belief_scope
|
from ...core.logger import logger, belief_scope
|
||||||
# [/SECTION]
|
# [/SECTION]
|
||||||
|
|
||||||
# [DEF:router:Variable]
|
# [DEF:router:Variable]
|
||||||
@@ -126,13 +126,17 @@ async def delete_user(
|
|||||||
_ = Depends(has_permission("admin:users", "WRITE"))
|
_ = Depends(has_permission("admin:users", "WRITE"))
|
||||||
):
|
):
|
||||||
with belief_scope("api.admin.delete_user"):
|
with belief_scope("api.admin.delete_user"):
|
||||||
|
logger.info(f"[DEBUG] Attempting to delete user context={{'user_id': '{user_id}'}}")
|
||||||
repo = AuthRepository(db)
|
repo = AuthRepository(db)
|
||||||
user = repo.get_user_by_id(user_id)
|
user = repo.get_user_by_id(user_id)
|
||||||
if not user:
|
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")
|
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.delete(user)
|
||||||
db.commit()
|
db.commit()
|
||||||
|
logger.info(f"[DEBUG] Successfully deleted user context={{'user_id': '{user_id}'}}")
|
||||||
return None
|
return None
|
||||||
# [/DEF:delete_user:Function]
|
# [/DEF:delete_user:Function]
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
# [SECTION: IMPORTS]
|
# [SECTION: IMPORTS]
|
||||||
from fastapi import APIRouter, Depends, HTTPException
|
from fastapi import APIRouter, Depends, HTTPException
|
||||||
from typing import List, Dict, Optional
|
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 ...core.superset_client import SupersetClient
|
||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field
|
||||||
from ...core.config_models import Environment as EnvModel
|
from ...core.config_models import Environment as EnvModel
|
||||||
@@ -47,7 +47,10 @@ class DatabaseResponse(BaseModel):
|
|||||||
# @POST: Returns a list of EnvironmentResponse objects.
|
# @POST: Returns a list of EnvironmentResponse objects.
|
||||||
# @RETURN: List[EnvironmentResponse]
|
# @RETURN: List[EnvironmentResponse]
|
||||||
@router.get("", response_model=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"):
|
with belief_scope("get_environments"):
|
||||||
envs = config_manager.get_environments()
|
envs = config_manager.get_environments()
|
||||||
# Ensure envs is a list
|
# Ensure envs is a list
|
||||||
@@ -77,7 +80,8 @@ async def update_environment_schedule(
|
|||||||
id: str,
|
id: str,
|
||||||
schedule: ScheduleSchema,
|
schedule: ScheduleSchema,
|
||||||
config_manager=Depends(get_config_manager),
|
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}"):
|
with belief_scope("update_environment_schedule", f"id={id}"):
|
||||||
envs = config_manager.get_environments()
|
envs = config_manager.get_environments()
|
||||||
@@ -104,7 +108,11 @@ async def update_environment_schedule(
|
|||||||
# @PARAM: id (str) - The environment ID.
|
# @PARAM: id (str) - The environment ID.
|
||||||
# @RETURN: List[Dict] - List of databases.
|
# @RETURN: List[Dict] - List of databases.
|
||||||
@router.get("/{id}/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}"):
|
with belief_scope("get_environment_databases", f"id={id}"):
|
||||||
envs = config_manager.get_environments()
|
envs = config_manager.get_environments()
|
||||||
env = next((e for e in envs if e.id == id), None)
|
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 sqlalchemy.orm import Session
|
||||||
from typing import List, Optional
|
from typing import List, Optional
|
||||||
import typing
|
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.core.database import get_db
|
||||||
from src.models.git import GitServerConfig, GitStatus, DeploymentEnvironment, GitRepository
|
from src.models.git import GitServerConfig, GitStatus, DeploymentEnvironment, GitRepository
|
||||||
from src.api.routes.git_schemas import (
|
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.
|
# @POST: Returns a list of all GitServerConfig objects from the database.
|
||||||
# @RETURN: List[GitServerConfigSchema]
|
# @RETURN: List[GitServerConfigSchema]
|
||||||
@router.get("/config", response_model=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"):
|
with belief_scope("get_git_configs"):
|
||||||
return db.query(GitServerConfig).all()
|
return db.query(GitServerConfig).all()
|
||||||
# [/DEF:get_git_configs:Function]
|
# [/DEF:get_git_configs:Function]
|
||||||
@@ -46,7 +49,11 @@ async def get_git_configs(db: Session = Depends(get_db)):
|
|||||||
# @PARAM: config (GitServerConfigCreate)
|
# @PARAM: config (GitServerConfigCreate)
|
||||||
# @RETURN: GitServerConfigSchema
|
# @RETURN: GitServerConfigSchema
|
||||||
@router.post("/config", response_model=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"):
|
with belief_scope("create_git_config"):
|
||||||
db_config = GitServerConfig(**config.dict())
|
db_config = GitServerConfig(**config.dict())
|
||||||
db.add(db_config)
|
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.
|
# @POST: The configuration record is removed from the database.
|
||||||
# @PARAM: config_id (str)
|
# @PARAM: config_id (str)
|
||||||
@router.delete("/config/{config_id}")
|
@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"):
|
with belief_scope("delete_git_config"):
|
||||||
db_config = db.query(GitServerConfig).filter(GitServerConfig.id == config_id).first()
|
db_config = db.query(GitServerConfig).filter(GitServerConfig.id == config_id).first()
|
||||||
if not db_config:
|
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.
|
# @POST: Returns success if the connection is validated via GitService.
|
||||||
# @PARAM: config (GitServerConfigCreate)
|
# @PARAM: config (GitServerConfigCreate)
|
||||||
@router.post("/config/test")
|
@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"):
|
with belief_scope("test_git_config"):
|
||||||
success = await git_service.test_connection(config.provider, config.url, config.pat)
|
success = await git_service.test_connection(config.provider, config.url, config.pat)
|
||||||
if success:
|
if success:
|
||||||
@@ -94,7 +108,12 @@ async def test_git_config(config: GitServerConfigCreate):
|
|||||||
# @PARAM: dashboard_id (int)
|
# @PARAM: dashboard_id (int)
|
||||||
# @PARAM: init_data (RepoInitRequest)
|
# @PARAM: init_data (RepoInitRequest)
|
||||||
@router.post("/repositories/{dashboard_id}/init")
|
@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"):
|
with belief_scope("init_repository"):
|
||||||
# 1. Get config
|
# 1. Get config
|
||||||
config = db.query(GitServerConfig).filter(GitServerConfig.id == init_data.config_id).first()
|
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)
|
# @PARAM: dashboard_id (int)
|
||||||
# @RETURN: List[BranchSchema]
|
# @RETURN: List[BranchSchema]
|
||||||
@router.get("/repositories/{dashboard_id}/branches", response_model=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"):
|
with belief_scope("get_branches"):
|
||||||
try:
|
try:
|
||||||
return git_service.list_branches(dashboard_id)
|
return git_service.list_branches(dashboard_id)
|
||||||
@@ -153,7 +175,11 @@ async def get_branches(dashboard_id: int):
|
|||||||
# @PARAM: dashboard_id (int)
|
# @PARAM: dashboard_id (int)
|
||||||
# @PARAM: branch_data (BranchCreate)
|
# @PARAM: branch_data (BranchCreate)
|
||||||
@router.post("/repositories/{dashboard_id}/branches")
|
@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"):
|
with belief_scope("create_branch"):
|
||||||
try:
|
try:
|
||||||
git_service.create_branch(dashboard_id, branch_data.name, branch_data.from_branch)
|
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: dashboard_id (int)
|
||||||
# @PARAM: checkout_data (BranchCheckout)
|
# @PARAM: checkout_data (BranchCheckout)
|
||||||
@router.post("/repositories/{dashboard_id}/checkout")
|
@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"):
|
with belief_scope("checkout_branch"):
|
||||||
try:
|
try:
|
||||||
git_service.checkout_branch(dashboard_id, checkout_data.name)
|
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: dashboard_id (int)
|
||||||
# @PARAM: commit_data (CommitCreate)
|
# @PARAM: commit_data (CommitCreate)
|
||||||
@router.post("/repositories/{dashboard_id}/commit")
|
@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"):
|
with belief_scope("commit_changes"):
|
||||||
try:
|
try:
|
||||||
git_service.commit_changes(dashboard_id, commit_data.message, commit_data.files)
|
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.
|
# @POST: Local commits are pushed to the remote repository.
|
||||||
# @PARAM: dashboard_id (int)
|
# @PARAM: dashboard_id (int)
|
||||||
@router.post("/repositories/{dashboard_id}/push")
|
@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"):
|
with belief_scope("push_changes"):
|
||||||
try:
|
try:
|
||||||
git_service.push_changes(dashboard_id)
|
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.
|
# @POST: Remote changes are fetched and merged into the local branch.
|
||||||
# @PARAM: dashboard_id (int)
|
# @PARAM: dashboard_id (int)
|
||||||
@router.post("/repositories/{dashboard_id}/pull")
|
@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"):
|
with belief_scope("pull_changes"):
|
||||||
try:
|
try:
|
||||||
git_service.pull_changes(dashboard_id)
|
git_service.pull_changes(dashboard_id)
|
||||||
@@ -231,7 +271,11 @@ async def pull_changes(dashboard_id: int):
|
|||||||
# @PARAM: dashboard_id (int)
|
# @PARAM: dashboard_id (int)
|
||||||
# @PARAM: source_env_id (Optional[str])
|
# @PARAM: source_env_id (Optional[str])
|
||||||
@router.post("/repositories/{dashboard_id}/sync")
|
@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"):
|
with belief_scope("sync_dashboard"):
|
||||||
try:
|
try:
|
||||||
from src.plugins.git_plugin import GitPlugin
|
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.
|
# @POST: Returns a list of DeploymentEnvironmentSchema objects.
|
||||||
# @RETURN: List[DeploymentEnvironmentSchema]
|
# @RETURN: List[DeploymentEnvironmentSchema]
|
||||||
@router.get("/environments", response_model=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"):
|
with belief_scope("get_environments"):
|
||||||
envs = config_manager.get_environments()
|
envs = config_manager.get_environments()
|
||||||
return [
|
return [
|
||||||
@@ -271,7 +318,11 @@ async def get_environments(config_manager=Depends(get_config_manager)):
|
|||||||
# @PARAM: dashboard_id (int)
|
# @PARAM: dashboard_id (int)
|
||||||
# @PARAM: deploy_data (DeployRequest)
|
# @PARAM: deploy_data (DeployRequest)
|
||||||
@router.post("/repositories/{dashboard_id}/deploy")
|
@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"):
|
with belief_scope("deploy_dashboard"):
|
||||||
try:
|
try:
|
||||||
from src.plugins.git_plugin import GitPlugin
|
from src.plugins.git_plugin import GitPlugin
|
||||||
@@ -293,7 +344,11 @@ async def deploy_dashboard(dashboard_id: int, deploy_data: DeployRequest):
|
|||||||
# @PARAM: limit (int)
|
# @PARAM: limit (int)
|
||||||
# @RETURN: List[CommitSchema]
|
# @RETURN: List[CommitSchema]
|
||||||
@router.get("/repositories/{dashboard_id}/history", response_model=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"):
|
with belief_scope("get_history"):
|
||||||
try:
|
try:
|
||||||
return git_service.get_commit_history(dashboard_id, limit)
|
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)
|
# @PARAM: dashboard_id (int)
|
||||||
# @RETURN: dict
|
# @RETURN: dict
|
||||||
@router.get("/repositories/{dashboard_id}/status")
|
@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"):
|
with belief_scope("get_repository_status"):
|
||||||
try:
|
try:
|
||||||
return git_service.get_status(dashboard_id)
|
return git_service.get_status(dashboard_id)
|
||||||
@@ -325,7 +383,12 @@ async def get_repository_status(dashboard_id: int):
|
|||||||
# @PARAM: staged (bool)
|
# @PARAM: staged (bool)
|
||||||
# @RETURN: str
|
# @RETURN: str
|
||||||
@router.get("/repositories/{dashboard_id}/diff")
|
@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"):
|
with belief_scope("get_repository_diff"):
|
||||||
try:
|
try:
|
||||||
diff_text = git_service.get_diff(dashboard_id, file_path, staged)
|
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 sqlalchemy.orm import Session
|
||||||
from typing import List, Optional
|
from typing import List, Optional
|
||||||
from ...core.logger import belief_scope
|
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 ...core.database import get_db
|
||||||
from ...models.mapping import DatabaseMapping
|
from ...models.mapping import DatabaseMapping
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
@@ -60,7 +60,8 @@ class SuggestRequest(BaseModel):
|
|||||||
async def get_mappings(
|
async def get_mappings(
|
||||||
source_env_id: Optional[str] = None,
|
source_env_id: Optional[str] = None,
|
||||||
target_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"):
|
with belief_scope("get_mappings"):
|
||||||
query = db.query(DatabaseMapping)
|
query = db.query(DatabaseMapping)
|
||||||
@@ -76,7 +77,11 @@ async def get_mappings(
|
|||||||
# @PRE: mapping is valid MappingCreate, db session is injected.
|
# @PRE: mapping is valid MappingCreate, db session is injected.
|
||||||
# @POST: DatabaseMapping created or updated in database.
|
# @POST: DatabaseMapping created or updated in database.
|
||||||
@router.post("", response_model=MappingResponse)
|
@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"):
|
with belief_scope("create_mapping"):
|
||||||
# Check if mapping already exists
|
# Check if mapping already exists
|
||||||
existing = db.query(DatabaseMapping).filter(
|
existing = db.query(DatabaseMapping).filter(
|
||||||
@@ -106,10 +111,11 @@ async def create_mapping(mapping: MappingCreate, db: Session = Depends(get_db)):
|
|||||||
@router.post("/suggest")
|
@router.post("/suggest")
|
||||||
async def suggest_mappings_api(
|
async def suggest_mappings_api(
|
||||||
request: SuggestRequest,
|
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"):
|
with belief_scope("suggest_mappings_api"):
|
||||||
from backend.src.services.mapping_service import MappingService
|
from ...services.mapping_service import MappingService
|
||||||
service = MappingService(config_manager)
|
service = MappingService(config_manager)
|
||||||
try:
|
try:
|
||||||
return await service.get_suggestions(request.source_env_id, request.target_env_id)
|
return await service.get_suggestions(request.source_env_id, request.target_env_id)
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
|
|
||||||
from fastapi import APIRouter, Depends, HTTPException
|
from fastapi import APIRouter, Depends, HTTPException
|
||||||
from typing import List, Dict
|
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 ...models.dashboard import DashboardMetadata, DashboardSelection
|
||||||
from ...core.superset_client import SupersetClient
|
from ...core.superset_client import SupersetClient
|
||||||
from ...core.logger import belief_scope
|
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.
|
# @PARAM: env_id (str) - The ID of the environment to fetch from.
|
||||||
# @RETURN: List[DashboardMetadata]
|
# @RETURN: List[DashboardMetadata]
|
||||||
@router.get("/environments/{env_id}/dashboards", response_model=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}"):
|
with belief_scope("get_dashboards", f"env_id={env_id}"):
|
||||||
environments = config_manager.get_environments()
|
environments = config_manager.get_environments()
|
||||||
env = next((e for e in environments if e.id == env_id), None)
|
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.
|
# @PARAM: selection (DashboardSelection) - The dashboards to migrate.
|
||||||
# @RETURN: Dict - {"task_id": str, "message": str}
|
# @RETURN: Dict - {"task_id": str, "message": str}
|
||||||
@router.post("/migration/execute")
|
@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"):
|
with belief_scope("execute_migration"):
|
||||||
# Validate environments exist
|
# Validate environments exist
|
||||||
environments = config_manager.get_environments()
|
environments = config_manager.get_environments()
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ from typing import List
|
|||||||
from fastapi import APIRouter, Depends
|
from fastapi import APIRouter, Depends
|
||||||
|
|
||||||
from ...core.plugin_base import PluginConfig
|
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
|
from ...core.logger import belief_scope
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
@@ -19,7 +19,8 @@ router = APIRouter()
|
|||||||
# @RETURN: List[PluginConfig] - List of registered plugins.
|
# @RETURN: List[PluginConfig] - List of registered plugins.
|
||||||
@router.get("", response_model=List[PluginConfig])
|
@router.get("", response_model=List[PluginConfig])
|
||||||
async def list_plugins(
|
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"):
|
with belief_scope("list_plugins"):
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ router = APIRouter()
|
|||||||
@router.get("", response_model=AppConfig)
|
@router.get("", response_model=AppConfig)
|
||||||
async def get_settings(
|
async def get_settings(
|
||||||
config_manager: ConfigManager = Depends(get_config_manager),
|
config_manager: ConfigManager = Depends(get_config_manager),
|
||||||
_ = Depends(has_permission("settings", "READ"))
|
_ = Depends(has_permission("admin:settings", "READ"))
|
||||||
):
|
):
|
||||||
with belief_scope("get_settings"):
|
with belief_scope("get_settings"):
|
||||||
logger.info("[get_settings][Entry] Fetching all settings")
|
logger.info("[get_settings][Entry] Fetching all settings")
|
||||||
@@ -53,7 +53,7 @@ async def get_settings(
|
|||||||
async def update_global_settings(
|
async def update_global_settings(
|
||||||
settings: GlobalSettings,
|
settings: GlobalSettings,
|
||||||
config_manager: ConfigManager = Depends(get_config_manager),
|
config_manager: ConfigManager = Depends(get_config_manager),
|
||||||
_ = Depends(has_permission("settings", "WRITE"))
|
_ = Depends(has_permission("admin:settings", "WRITE"))
|
||||||
):
|
):
|
||||||
with belief_scope("update_global_settings"):
|
with belief_scope("update_global_settings"):
|
||||||
logger.info("[update_global_settings][Entry] Updating 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)
|
@router.get("/storage", response_model=StorageConfig)
|
||||||
async def get_storage_settings(
|
async def get_storage_settings(
|
||||||
config_manager: ConfigManager = Depends(get_config_manager),
|
config_manager: ConfigManager = Depends(get_config_manager),
|
||||||
_ = Depends(has_permission("settings", "READ"))
|
_ = Depends(has_permission("admin:settings", "READ"))
|
||||||
):
|
):
|
||||||
with belief_scope("get_storage_settings"):
|
with belief_scope("get_storage_settings"):
|
||||||
return config_manager.get_config().settings.storage
|
return config_manager.get_config().settings.storage
|
||||||
@@ -83,7 +83,7 @@ async def get_storage_settings(
|
|||||||
async def update_storage_settings(
|
async def update_storage_settings(
|
||||||
storage: StorageConfig,
|
storage: StorageConfig,
|
||||||
config_manager: ConfigManager = Depends(get_config_manager),
|
config_manager: ConfigManager = Depends(get_config_manager),
|
||||||
_ = Depends(has_permission("settings", "WRITE"))
|
_ = Depends(has_permission("admin:settings", "WRITE"))
|
||||||
):
|
):
|
||||||
with belief_scope("update_storage_settings"):
|
with belief_scope("update_storage_settings"):
|
||||||
is_valid, message = config_manager.validate_path(storage.root_path)
|
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])
|
@router.get("/environments", response_model=List[Environment])
|
||||||
async def get_environments(
|
async def get_environments(
|
||||||
config_manager: ConfigManager = Depends(get_config_manager),
|
config_manager: ConfigManager = Depends(get_config_manager),
|
||||||
_ = Depends(has_permission("settings", "READ"))
|
_ = Depends(has_permission("admin:settings", "READ"))
|
||||||
):
|
):
|
||||||
with belief_scope("get_environments"):
|
with belief_scope("get_environments"):
|
||||||
logger.info("[get_environments][Entry] Fetching environments")
|
logger.info("[get_environments][Entry] Fetching environments")
|
||||||
@@ -121,7 +121,7 @@ async def get_environments(
|
|||||||
async def add_environment(
|
async def add_environment(
|
||||||
env: Environment,
|
env: Environment,
|
||||||
config_manager: ConfigManager = Depends(get_config_manager),
|
config_manager: ConfigManager = Depends(get_config_manager),
|
||||||
_ = Depends(has_permission("settings", "WRITE"))
|
_ = Depends(has_permission("admin:settings", "WRITE"))
|
||||||
):
|
):
|
||||||
with belief_scope("add_environment"):
|
with belief_scope("add_environment"):
|
||||||
logger.info(f"[add_environment][Entry] Adding environment {env.id}")
|
logger.info(f"[add_environment][Entry] Adding environment {env.id}")
|
||||||
|
|||||||
@@ -8,11 +8,12 @@
|
|||||||
# @INVARIANT: All paths must be validated against path traversal.
|
# @INVARIANT: All paths must be validated against path traversal.
|
||||||
|
|
||||||
# [SECTION: IMPORTS]
|
# [SECTION: IMPORTS]
|
||||||
|
from pathlib import Path
|
||||||
from fastapi import APIRouter, Depends, UploadFile, File, Form, HTTPException
|
from fastapi import APIRouter, Depends, UploadFile, File, Form, HTTPException
|
||||||
from fastapi.responses import FileResponse
|
from fastapi.responses import FileResponse
|
||||||
from typing import List, Optional
|
from typing import List, Optional
|
||||||
from ...models.storage import StoredFile, FileCategory
|
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 ...plugins.storage.plugin import StoragePlugin
|
||||||
from ...core.logger import belief_scope
|
from ...core.logger import belief_scope
|
||||||
# [/SECTION]
|
# [/SECTION]
|
||||||
@@ -34,7 +35,8 @@ router = APIRouter(tags=["storage"])
|
|||||||
async def list_files(
|
async def list_files(
|
||||||
category: Optional[FileCategory] = None,
|
category: Optional[FileCategory] = None,
|
||||||
path: Optional[str] = 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"):
|
with belief_scope("list_files"):
|
||||||
storage_plugin: StoragePlugin = plugin_loader.get_plugin("storage-manager")
|
storage_plugin: StoragePlugin = plugin_loader.get_plugin("storage-manager")
|
||||||
@@ -63,7 +65,8 @@ async def upload_file(
|
|||||||
category: FileCategory = Form(...),
|
category: FileCategory = Form(...),
|
||||||
path: Optional[str] = Form(None),
|
path: Optional[str] = Form(None),
|
||||||
file: UploadFile = File(...),
|
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"):
|
with belief_scope("upload_file"):
|
||||||
storage_plugin: StoragePlugin = plugin_loader.get_plugin("storage-manager")
|
storage_plugin: StoragePlugin = plugin_loader.get_plugin("storage-manager")
|
||||||
@@ -89,7 +92,12 @@ async def upload_file(
|
|||||||
#
|
#
|
||||||
# @RELATION: CALLS -> StoragePlugin.delete_file
|
# @RELATION: CALLS -> StoragePlugin.delete_file
|
||||||
@router.delete("/files/{category}/{path:path}", status_code=204)
|
@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"):
|
with belief_scope("delete_file"):
|
||||||
storage_plugin: StoragePlugin = plugin_loader.get_plugin("storage-manager")
|
storage_plugin: StoragePlugin = plugin_loader.get_plugin("storage-manager")
|
||||||
if not storage_plugin:
|
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
|
# @RELATION: CALLS -> StoragePlugin.get_file_path
|
||||||
@router.get("/download/{category}/{path: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"):
|
with belief_scope("download_file"):
|
||||||
storage_plugin: StoragePlugin = plugin_loader.get_plugin("storage-manager")
|
storage_plugin: StoragePlugin = plugin_loader.get_plugin("storage-manager")
|
||||||
if not storage_plugin:
|
if not storage_plugin:
|
||||||
|
|||||||
@@ -64,7 +64,8 @@ async def list_tasks(
|
|||||||
limit: int = 10,
|
limit: int = 10,
|
||||||
offset: int = 0,
|
offset: int = 0,
|
||||||
status: Optional[TaskStatus] = None,
|
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.
|
Retrieve a list of tasks with pagination and optional status filter.
|
||||||
@@ -83,7 +84,8 @@ async def list_tasks(
|
|||||||
# @RETURN: Task - The task details.
|
# @RETURN: Task - The task details.
|
||||||
async def get_task(
|
async def get_task(
|
||||||
task_id: str,
|
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.
|
Retrieve the details of a specific task.
|
||||||
@@ -105,7 +107,8 @@ async def get_task(
|
|||||||
# @RETURN: List[LogEntry] - List of log entries.
|
# @RETURN: List[LogEntry] - List of log entries.
|
||||||
async def get_task_logs(
|
async def get_task_logs(
|
||||||
task_id: str,
|
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.
|
Retrieve logs for a specific task.
|
||||||
@@ -129,7 +132,8 @@ async def get_task_logs(
|
|||||||
async def resolve_task(
|
async def resolve_task(
|
||||||
task_id: str,
|
task_id: str,
|
||||||
request: ResolveTaskRequest,
|
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.
|
Resolve a task that is awaiting mapping.
|
||||||
@@ -154,7 +158,8 @@ async def resolve_task(
|
|||||||
async def resume_task(
|
async def resume_task(
|
||||||
task_id: str,
|
task_id: str,
|
||||||
request: ResumeTaskRequest,
|
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).
|
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.
|
# @POST: Tasks are removed from memory/persistence.
|
||||||
async def clear_tasks(
|
async def clear_tasks(
|
||||||
status: Optional[TaskStatus] = None,
|
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.
|
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: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
|
# Initial registration
|
||||||
register_adfs()
|
register_adfs()
|
||||||
|
|
||||||
|
|||||||
@@ -86,7 +86,7 @@ def get_scheduler_service() -> SchedulerService:
|
|||||||
|
|
||||||
# [DEF:oauth2_scheme:Variable]
|
# [DEF:oauth2_scheme:Variable]
|
||||||
# @PURPOSE: OAuth2 password bearer scheme for token extraction.
|
# @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:oauth2_scheme:Variable]
|
||||||
|
|
||||||
# [DEF:get_current_user:Function]
|
# [DEF:get_current_user:Function]
|
||||||
|
|||||||
@@ -95,7 +95,7 @@ class ADGroupMapping(Base):
|
|||||||
__tablename__ = "ad_group_mappings"
|
__tablename__ = "ad_group_mappings"
|
||||||
|
|
||||||
id = Column(String, primary_key=True, default=generate_uuid)
|
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_id = Column(String, ForeignKey("roles.id"), nullable=False)
|
||||||
|
|
||||||
role = relationship("Role")
|
role = relationship("Role")
|
||||||
|
|||||||
@@ -303,9 +303,9 @@ class MigrationPlugin(PluginBase):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
exported_content, _ = from_c.export_dashboard(dash_id)
|
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
|
# 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)
|
success = engine.transform_zip(str(tmp_zip_path), str(tmp_new_zip), db_mapping, strip_databases=False)
|
||||||
|
|
||||||
if not success and replace_db_config:
|
if not success and replace_db_config:
|
||||||
|
|||||||
@@ -16,7 +16,8 @@ from pathlib import Path
|
|||||||
sys.path.append(str(Path(__file__).parent.parent.parent))
|
sys.path.append(str(Path(__file__).parent.parent.parent))
|
||||||
|
|
||||||
from src.core.database import AuthSessionLocal
|
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
|
from src.core.logger import logger, belief_scope
|
||||||
# [/SECTION]
|
# [/SECTION]
|
||||||
|
|
||||||
@@ -29,6 +30,10 @@ INITIAL_PERMISSIONS = [
|
|||||||
{"resource": "admin:roles", "action": "WRITE"},
|
{"resource": "admin:roles", "action": "WRITE"},
|
||||||
{"resource": "admin:settings", "action": "READ"},
|
{"resource": "admin:settings", "action": "READ"},
|
||||||
{"resource": "admin:settings", "action": "WRITE"},
|
{"resource": "admin:settings", "action": "WRITE"},
|
||||||
|
{"resource": "environments", "action": "READ"},
|
||||||
|
{"resource": "plugins", "action": "READ"},
|
||||||
|
{"resource": "tasks", "action": "READ"},
|
||||||
|
{"resource": "tasks", "action": "WRITE"},
|
||||||
|
|
||||||
# Plugin Permissions
|
# Plugin Permissions
|
||||||
{"resource": "plugin:backup", "action": "EXECUTE"},
|
{"resource": "plugin:backup", "action": "EXECUTE"},
|
||||||
@@ -37,6 +42,8 @@ INITIAL_PERMISSIONS = [
|
|||||||
{"resource": "plugin:search", "action": "EXECUTE"},
|
{"resource": "plugin:search", "action": "EXECUTE"},
|
||||||
{"resource": "plugin:git", "action": "EXECUTE"},
|
{"resource": "plugin:git", "action": "EXECUTE"},
|
||||||
{"resource": "plugin:storage", "action": "EXECUTE"},
|
{"resource": "plugin:storage", "action": "EXECUTE"},
|
||||||
|
{"resource": "plugin:storage", "action": "READ"},
|
||||||
|
{"resource": "plugin:storage", "action": "WRITE"},
|
||||||
{"resource": "plugin:debug", "action": "EXECUTE"},
|
{"resource": "plugin:debug", "action": "EXECUTE"},
|
||||||
]
|
]
|
||||||
# [/DEF:INITIAL_PERMISSIONS:Constant]
|
# [/DEF:INITIAL_PERMISSIONS:Constant]
|
||||||
@@ -66,6 +73,36 @@ def seed_permissions():
|
|||||||
|
|
||||||
db.commit()
|
db.commit()
|
||||||
logger.info(f"Seeding completed. Added {count} new permissions.")
|
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:
|
except Exception as e:
|
||||||
logger.error(f"Failed to seed permissions: {e}")
|
logger.error(f"Failed to seed permissions: {e}")
|
||||||
db.rollback()
|
db.rollback()
|
||||||
|
|||||||
@@ -101,7 +101,7 @@ class AuthService:
|
|||||||
# Update roles based on group mappings
|
# Update roles based on group mappings
|
||||||
from ..models.auth import ADGroupMapping
|
from ..models.auth import ADGroupMapping
|
||||||
mapped_roles = self.repo.db.query(Role).join(ADGroupMapping).filter(
|
mapped_roles = self.repo.db.query(Role).join(ADGroupMapping).filter(
|
||||||
ADGroupMapping.ad_group_name.in_(ad_groups)
|
ADGroupMapping.ad_group.in_(ad_groups)
|
||||||
).all()
|
).all()
|
||||||
|
|
||||||
user.roles = mapped_roles
|
user.roles = mapped_roles
|
||||||
|
|||||||
@@ -10,9 +10,9 @@
|
|||||||
|
|
||||||
# [SECTION: IMPORTS]
|
# [SECTION: IMPORTS]
|
||||||
from typing import List, Dict
|
from typing import List, Dict
|
||||||
from backend.src.core.logger import belief_scope
|
from ..core.logger import belief_scope
|
||||||
from backend.src.core.superset_client import SupersetClient
|
from ..core.superset_client import SupersetClient
|
||||||
from backend.src.core.utils.matching import suggest_mappings
|
from ..core.utils.matching import suggest_mappings
|
||||||
# [/SECTION]
|
# [/SECTION]
|
||||||
|
|
||||||
# [DEF:MappingService:Class]
|
# [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.
|
// @POST: tasks array is updated and selectedTask status synchronized.
|
||||||
async function fetchTasks() {
|
async function fetchTasks() {
|
||||||
try {
|
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');
|
if (!res.ok) throw new Error('Failed to fetch tasks');
|
||||||
tasks = await res.json();
|
tasks = await res.json();
|
||||||
|
|
||||||
@@ -58,7 +60,9 @@
|
|||||||
const params = new URLSearchParams();
|
const params = new URLSearchParams();
|
||||||
if (status) params.append('status', status);
|
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');
|
if (!res.ok) throw new Error('Failed to clear tasks');
|
||||||
|
|
||||||
await fetchTasks();
|
await fetchTasks();
|
||||||
@@ -75,7 +79,9 @@
|
|||||||
async function selectTask(task) {
|
async function selectTask(task) {
|
||||||
try {
|
try {
|
||||||
// Fetch the full task details (including logs) before setting it as selected
|
// 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) {
|
if (res.ok) {
|
||||||
const fullTask = await res.json();
|
const fullTask = await res.json();
|
||||||
selectedTask.set(fullTask);
|
selectedTask.set(fullTask);
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
<script>
|
<script>
|
||||||
// [SECTION: IMPORTS]
|
// [SECTION: IMPORTS]
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
|
import { api } from '../../lib/api.js';
|
||||||
import { runTask, getTaskStatus } from '../../services/toolsService.js';
|
import { runTask, getTaskStatus } from '../../services/toolsService.js';
|
||||||
import { selectedTask } from '../../lib/stores.js';
|
import { selectedTask } from '../../lib/stores.js';
|
||||||
import { addToast } from '../../lib/toasts.js';
|
import { addToast } from '../../lib/toasts.js';
|
||||||
@@ -32,8 +33,7 @@
|
|||||||
*/
|
*/
|
||||||
async function fetchEnvironments() {
|
async function fetchEnvironments() {
|
||||||
try {
|
try {
|
||||||
const res = await fetch('/api/environments');
|
envs = await api.getEnvironmentsList();
|
||||||
envs = await res.json();
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
addToast('Failed to fetch environments', 'error');
|
addToast('Failed to fetch environments', 'error');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -53,9 +53,15 @@ async function fetchApi(endpoint) {
|
|||||||
const response = await fetch(`${API_BASE_URL}${endpoint}`, {
|
const response = await fetch(`${API_BASE_URL}${endpoint}`, {
|
||||||
headers: getAuthHeaders()
|
headers: getAuthHeaders()
|
||||||
});
|
});
|
||||||
|
console.log(`[api.fetchApi][Action] Received response context={{'status': ${response.status}, 'ok': ${response.ok}}}`);
|
||||||
if (!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();
|
return await response.json();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`[api.fetchApi][Coherence:Failed] Error fetching from ${endpoint}:`, error);
|
console.error(`[api.fetchApi][Coherence:Failed] Error fetching from ${endpoint}:`, error);
|
||||||
@@ -80,9 +86,15 @@ async function postApi(endpoint, body) {
|
|||||||
headers: getAuthHeaders(),
|
headers: getAuthHeaders(),
|
||||||
body: JSON.stringify(body),
|
body: JSON.stringify(body),
|
||||||
});
|
});
|
||||||
|
console.log(`[api.postApi][Action] Received response context={{'status': ${response.status}, 'ok': ${response.ok}}}`);
|
||||||
if (!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();
|
return await response.json();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`[api.postApi][Coherence:Failed] Error posting to ${endpoint}:`, 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);
|
options.body = JSON.stringify(body);
|
||||||
}
|
}
|
||||||
const response = await fetch(`${API_BASE_URL}${endpoint}`, options);
|
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) {
|
if (!response.ok) {
|
||||||
const errorData = await response.json().catch(() => ({}));
|
const errorData = await response.json().catch(() => ({}));
|
||||||
const message = errorData.detail
|
const message = errorData.detail
|
||||||
? (typeof errorData.detail === 'string' ? errorData.detail : JSON.stringify(errorData.detail))
|
? (typeof errorData.detail === 'string' ? errorData.detail : JSON.stringify(errorData.detail))
|
||||||
: `API request failed with status ${response.status}`;
|
: `API request failed with status ${response.status}`;
|
||||||
|
console.error(`[api.requestApi][Action] Request failed context={{'status': ${response.status}, 'message': '${message}'}}`);
|
||||||
throw new Error(message);
|
throw new Error(message);
|
||||||
}
|
}
|
||||||
|
if (response.status === 204) {
|
||||||
|
console.log('[api.requestApi][Action] 204 No Content received');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
return await response.json();
|
return await response.json();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`[api.requestApi][Coherence:Failed] Error ${method} to ${endpoint}:`, error);
|
console.error(`[api.requestApi][Coherence:Failed] Error ${method} to ${endpoint}:`, error);
|
||||||
|
|||||||
@@ -21,6 +21,7 @@
|
|||||||
let roles = [];
|
let roles = [];
|
||||||
let loading = true;
|
let loading = true;
|
||||||
let error = null;
|
let error = null;
|
||||||
|
let deletingUserId = null;
|
||||||
|
|
||||||
let showModal = false;
|
let showModal = false;
|
||||||
let isEditing = false;
|
let isEditing = false;
|
||||||
@@ -130,9 +131,11 @@
|
|||||||
* @param {Object} user - The user to delete.
|
* @param {Object} user - The user to delete.
|
||||||
*/
|
*/
|
||||||
async function handleDeleteUser(user) {
|
async function handleDeleteUser(user) {
|
||||||
|
if (deletingUserId) return;
|
||||||
if (!confirm($t.admin.users.confirm_delete.replace('{username}', user.username))) return;
|
if (!confirm($t.admin.users.confirm_delete.replace('{username}', user.username))) return;
|
||||||
|
|
||||||
console.log('[AdminUsersPage][handleDeleteUser][Entry]');
|
console.log('[AdminUsersPage][handleDeleteUser][Entry]');
|
||||||
|
deletingUserId = user.id;
|
||||||
try {
|
try {
|
||||||
await adminService.deleteUser(user.id);
|
await adminService.deleteUser(user.id);
|
||||||
await loadData();
|
await loadData();
|
||||||
@@ -140,6 +143,8 @@
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
alert("Failed to delete user: " + e.message);
|
alert("Failed to delete user: " + e.message);
|
||||||
console.error('[AdminUsersPage][handleDeleteUser][Coherence:Failed]', e);
|
console.error('[AdminUsersPage][handleDeleteUser][Coherence:Failed]', e);
|
||||||
|
} finally {
|
||||||
|
deletingUserId = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// [/DEF:handleDeleteUser:Function]
|
// [/DEF:handleDeleteUser:Function]
|
||||||
@@ -208,8 +213,14 @@
|
|||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
|
<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={() => 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">{$t.common.delete}</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>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{/each}
|
{/each}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
import DashboardGrid from '../../components/DashboardGrid.svelte';
|
import DashboardGrid from '../../components/DashboardGrid.svelte';
|
||||||
import { addToast as toast } from '../../lib/toasts.js';
|
import { addToast as toast } from '../../lib/toasts.js';
|
||||||
|
import { api } from '../../lib/api.js';
|
||||||
import type { DashboardMetadata } from '../../types/dashboard';
|
import type { DashboardMetadata } from '../../types/dashboard';
|
||||||
import { t } from '$lib/i18n';
|
import { t } from '$lib/i18n';
|
||||||
import { Button, Card, PageHeader, Select } from '$lib/ui';
|
import { Button, Card, PageHeader, Select } from '$lib/ui';
|
||||||
@@ -24,9 +25,7 @@
|
|||||||
// @POST: `environments` array is populated with data from /api/environments.
|
// @POST: `environments` array is populated with data from /api/environments.
|
||||||
async function fetchEnvironments() {
|
async function fetchEnvironments() {
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/api/environments');
|
environments = await api.getEnvironmentsList();
|
||||||
if (!response.ok) throw new Error('Failed to fetch environments');
|
|
||||||
environments = await response.json();
|
|
||||||
if (environments.length > 0) {
|
if (environments.length > 0) {
|
||||||
selectedEnvId = environments[0].id;
|
selectedEnvId = environments[0].id;
|
||||||
}
|
}
|
||||||
@@ -46,9 +45,7 @@
|
|||||||
if (!envId) return;
|
if (!envId) return;
|
||||||
fetchingDashboards = true;
|
fetchingDashboards = true;
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`/api/environments/${envId}/dashboards`);
|
dashboards = await api.requestApi(`/environments/${envId}/dashboards`);
|
||||||
if (!response.ok) throw new Error('Failed to fetch dashboards');
|
|
||||||
dashboards = await response.json();
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
toast(e.message, 'error');
|
toast(e.message, 'error');
|
||||||
dashboards = [];
|
dashboards = [];
|
||||||
|
|||||||
@@ -18,6 +18,7 @@
|
|||||||
import TaskHistory from '../../components/TaskHistory.svelte';
|
import TaskHistory from '../../components/TaskHistory.svelte';
|
||||||
import TaskLogViewer from '../../components/TaskLogViewer.svelte';
|
import TaskLogViewer from '../../components/TaskLogViewer.svelte';
|
||||||
import PasswordPrompt from '../../components/PasswordPrompt.svelte';
|
import PasswordPrompt from '../../components/PasswordPrompt.svelte';
|
||||||
|
import { api } from '../../lib/api.js';
|
||||||
import { selectedTask } from '../../lib/stores.js';
|
import { selectedTask } from '../../lib/stores.js';
|
||||||
import { resumeTask } from '../../services/taskService.js';
|
import { resumeTask } from '../../services/taskService.js';
|
||||||
import type { DashboardMetadata, DashboardSelection } from '../../types/dashboard';
|
import type { DashboardMetadata, DashboardSelection } from '../../types/dashboard';
|
||||||
@@ -58,9 +59,7 @@
|
|||||||
*/
|
*/
|
||||||
async function fetchEnvironments() {
|
async function fetchEnvironments() {
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/api/environments');
|
environments = await api.getEnvironmentsList();
|
||||||
if (!response.ok) throw new Error('Failed to fetch environments');
|
|
||||||
environments = await response.json();
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
error = e.message;
|
error = e.message;
|
||||||
} finally {
|
} finally {
|
||||||
@@ -78,9 +77,7 @@
|
|||||||
*/
|
*/
|
||||||
async function fetchDashboards(envId: string) {
|
async function fetchDashboards(envId: string) {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`/api/environments/${envId}/dashboards`);
|
dashboards = await api.requestApi(`/environments/${envId}/dashboards`);
|
||||||
if (!response.ok) throw new Error('Failed to fetch dashboards');
|
|
||||||
dashboards = await response.json();
|
|
||||||
selectedDashboardIds = []; // Reset selection when env changes
|
selectedDashboardIds = []; // Reset selection when env changes
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
error = e.message;
|
error = e.message;
|
||||||
@@ -106,23 +103,17 @@
|
|||||||
error = "";
|
error = "";
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const [srcRes, tgtRes, mapRes, sugRes] = await Promise.all([
|
const [src, tgt, maps, sugs] = await Promise.all([
|
||||||
fetch(`/api/environments/${sourceEnvId}/databases`),
|
api.requestApi(`/environments/${sourceEnvId}/databases`),
|
||||||
fetch(`/api/environments/${targetEnvId}/databases`),
|
api.requestApi(`/environments/${targetEnvId}/databases`),
|
||||||
fetch(`/api/mappings?source_env_id=${sourceEnvId}&target_env_id=${targetEnvId}`),
|
api.requestApi(`/mappings?source_env_id=${sourceEnvId}&target_env_id=${targetEnvId}`),
|
||||||
fetch(`/api/mappings/suggest`, {
|
api.postApi(`/mappings/suggest`, { source_env_id: sourceEnvId, target_env_id: targetEnvId })
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({ source_env_id: sourceEnvId, target_env_id: targetEnvId })
|
|
||||||
})
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if (!srcRes.ok || !tgtRes.ok) throw new Error('Failed to fetch databases from environments');
|
sourceDatabases = src;
|
||||||
|
targetDatabases = tgt;
|
||||||
sourceDatabases = await srcRes.json();
|
mappings = maps;
|
||||||
targetDatabases = await tgtRes.json();
|
suggestions = sugs;
|
||||||
mappings = await mapRes.json();
|
|
||||||
suggestions = await sugRes.json();
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
error = e.message;
|
error = e.message;
|
||||||
} finally {
|
} finally {
|
||||||
@@ -145,22 +136,15 @@
|
|||||||
if (!sDb || !tDb) return;
|
if (!sDb || !tDb) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/api/mappings', {
|
const savedMapping = await api.postApi('/mappings', {
|
||||||
method: 'POST',
|
source_env_id: sourceEnvId,
|
||||||
headers: { 'Content-Type': 'application/json' },
|
target_env_id: targetEnvId,
|
||||||
body: JSON.stringify({
|
source_db_uuid: sourceUuid,
|
||||||
source_env_id: sourceEnvId,
|
target_db_uuid: targetUuid,
|
||||||
target_env_id: targetEnvId,
|
source_db_name: sDb.database_name,
|
||||||
source_db_uuid: sourceUuid,
|
target_db_name: tDb.database_name
|
||||||
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];
|
mappings = [...mappings.filter(m => m.source_db_uuid !== sourceUuid), savedMapping];
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
error = e.message;
|
error = e.message;
|
||||||
@@ -253,14 +237,7 @@
|
|||||||
replace_db_config: replaceDb
|
replace_db_config: replaceDb
|
||||||
};
|
};
|
||||||
console.log(`[MigrationDashboard][Action] Starting migration with selection:`, selection);
|
console.log(`[MigrationDashboard][Action] Starting migration with selection:`, selection);
|
||||||
const response = await fetch('/api/migration/execute', {
|
const result = await api.postApi('/migration/execute', selection);
|
||||||
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();
|
|
||||||
console.log(`[MigrationDashboard][Action] Migration started: ${result.task_id} - ${result.message}`);
|
console.log(`[MigrationDashboard][Action] Migration started: ${result.task_id} - ${result.message}`);
|
||||||
|
|
||||||
// Wait a brief moment for the backend to ensure the task is retrievable
|
// 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
|
// Fetch full task details and switch to TaskRunner view
|
||||||
try {
|
try {
|
||||||
const taskRes = await fetch(`/api/tasks/${result.task_id}`);
|
const task = await api.getTask(result.task_id);
|
||||||
if (taskRes.ok) {
|
selectedTask.set(task);
|
||||||
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: {}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (fetchErr) {
|
} 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) {
|
} catch (e) {
|
||||||
console.error(`[MigrationDashboard][Failure] Migration failed:`, e);
|
console.error(`[MigrationDashboard][Failure] Migration failed:`, e);
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
// [SECTION: IMPORTS]
|
// [SECTION: IMPORTS]
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
|
import { api } from '../../../lib/api.js';
|
||||||
import EnvSelector from '../../../components/EnvSelector.svelte';
|
import EnvSelector from '../../../components/EnvSelector.svelte';
|
||||||
import MappingTable from '../../../components/MappingTable.svelte';
|
import MappingTable from '../../../components/MappingTable.svelte';
|
||||||
import { t } from '$lib/i18n';
|
import { t } from '$lib/i18n';
|
||||||
@@ -38,9 +39,7 @@
|
|||||||
// @POST: environments array is populated.
|
// @POST: environments array is populated.
|
||||||
async function fetchEnvironments() {
|
async function fetchEnvironments() {
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/api/environments');
|
environments = await api.getEnvironmentsList();
|
||||||
if (!response.ok) throw new Error('Failed to fetch environments');
|
|
||||||
environments = await response.json();
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
error = e.message;
|
error = e.message;
|
||||||
} finally {
|
} finally {
|
||||||
@@ -64,23 +63,17 @@
|
|||||||
success = "";
|
success = "";
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const [srcRes, tgtRes, mapRes, sugRes] = await Promise.all([
|
const [src, tgt, maps, sugs] = await Promise.all([
|
||||||
fetch(`/api/environments/${sourceEnvId}/databases`),
|
api.requestApi(`/environments/${sourceEnvId}/databases`),
|
||||||
fetch(`/api/environments/${targetEnvId}/databases`),
|
api.requestApi(`/environments/${targetEnvId}/databases`),
|
||||||
fetch(`/api/mappings?source_env_id=${sourceEnvId}&target_env_id=${targetEnvId}`),
|
api.requestApi(`/mappings?source_env_id=${sourceEnvId}&target_env_id=${targetEnvId}`),
|
||||||
fetch(`/api/mappings/suggest`, {
|
api.postApi(`/mappings/suggest`, { source_env_id: sourceEnvId, target_env_id: targetEnvId })
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({ source_env_id: sourceEnvId, target_env_id: targetEnvId })
|
|
||||||
})
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if (!srcRes.ok || !tgtRes.ok) throw new Error('Failed to fetch databases from environments');
|
sourceDatabases = src;
|
||||||
|
targetDatabases = tgt;
|
||||||
sourceDatabases = await srcRes.json();
|
mappings = maps;
|
||||||
targetDatabases = await tgtRes.json();
|
suggestions = sugs;
|
||||||
mappings = await mapRes.json();
|
|
||||||
suggestions = await sugRes.json();
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
error = e.message;
|
error = e.message;
|
||||||
} finally {
|
} finally {
|
||||||
@@ -103,22 +96,15 @@
|
|||||||
if (!sDb || !tDb) return;
|
if (!sDb || !tDb) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/api/mappings', {
|
const savedMapping = await api.postApi('/mappings', {
|
||||||
method: 'POST',
|
source_env_id: sourceEnvId,
|
||||||
headers: { 'Content-Type': 'application/json' },
|
target_env_id: targetEnvId,
|
||||||
body: JSON.stringify({
|
source_db_uuid: sourceUuid,
|
||||||
source_env_id: sourceEnvId,
|
target_db_uuid: targetUuid,
|
||||||
target_env_id: targetEnvId,
|
source_db_name: sDb.database_name,
|
||||||
source_db_uuid: sourceUuid,
|
target_db_name: tDb.database_name
|
||||||
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];
|
mappings = [...mappings.filter(m => m.source_db_uuid !== sourceUuid), savedMapping];
|
||||||
success = "Mapping saved successfully";
|
success = "Mapping saved successfully";
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|||||||
@@ -2,7 +2,9 @@
|
|||||||
* Service for interacting with the Connection Management API.
|
* 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]
|
// [DEF:getConnections:Function]
|
||||||
/* @PURPOSE: Fetch a list of saved connections.
|
/* @PURPOSE: Fetch a list of saved connections.
|
||||||
@@ -14,11 +16,7 @@ const API_BASE = '/api/settings/connections';
|
|||||||
* @returns {Promise<Array>} List of connections.
|
* @returns {Promise<Array>} List of connections.
|
||||||
*/
|
*/
|
||||||
export async function getConnections() {
|
export async function getConnections() {
|
||||||
const response = await fetch(API_BASE);
|
return requestApi(API_BASE);
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`Failed to fetch connections: ${response.statusText}`);
|
|
||||||
}
|
|
||||||
return await response.json();
|
|
||||||
}
|
}
|
||||||
// [/DEF:getConnections:Function]
|
// [/DEF:getConnections:Function]
|
||||||
|
|
||||||
@@ -33,19 +31,7 @@ export async function getConnections() {
|
|||||||
* @returns {Promise<Object>} The created connection instance.
|
* @returns {Promise<Object>} The created connection instance.
|
||||||
*/
|
*/
|
||||||
export async function createConnection(connectionData) {
|
export async function createConnection(connectionData) {
|
||||||
const response = await fetch(API_BASE, {
|
return requestApi(API_BASE, 'POST', connectionData);
|
||||||
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();
|
|
||||||
}
|
}
|
||||||
// [/DEF:createConnection:Function]
|
// [/DEF:createConnection:Function]
|
||||||
|
|
||||||
@@ -59,12 +45,6 @@ export async function createConnection(connectionData) {
|
|||||||
* @param {string} connectionId - The ID of the connection to delete.
|
* @param {string} connectionId - The ID of the connection to delete.
|
||||||
*/
|
*/
|
||||||
export async function deleteConnection(connectionId) {
|
export async function deleteConnection(connectionId) {
|
||||||
const response = await fetch(`${API_BASE}/${connectionId}`, {
|
return requestApi(`${API_BASE}/${connectionId}`, 'DELETE');
|
||||||
method: 'DELETE'
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`Failed to delete connection: ${response.statusText}`);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
// [/DEF:deleteConnection:Function]
|
// [/DEF:deleteConnection:Function]
|
||||||
@@ -6,7 +6,9 @@
|
|||||||
* @RELATION: DEPENDS_ON -> specs/011-git-integration-dashboard/contracts/api.md
|
* @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]
|
// [DEF:gitService:Action]
|
||||||
export const gitService = {
|
export const gitService = {
|
||||||
@@ -19,9 +21,7 @@ export const gitService = {
|
|||||||
*/
|
*/
|
||||||
async getConfigs() {
|
async getConfigs() {
|
||||||
console.log('[getConfigs][Action] Fetching Git configs');
|
console.log('[getConfigs][Action] Fetching Git configs');
|
||||||
const response = await fetch(`${API_BASE}/config`);
|
return requestApi(`${API_BASE}/config`);
|
||||||
if (!response.ok) throw new Error('Failed to fetch Git configs');
|
|
||||||
return response.json();
|
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -34,13 +34,7 @@ export const gitService = {
|
|||||||
*/
|
*/
|
||||||
async createConfig(config) {
|
async createConfig(config) {
|
||||||
console.log('[createConfig][Action] Creating Git config');
|
console.log('[createConfig][Action] Creating Git config');
|
||||||
const response = await fetch(`${API_BASE}/config`, {
|
return requestApi(`${API_BASE}/config`, 'POST', 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();
|
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -53,11 +47,7 @@ export const gitService = {
|
|||||||
*/
|
*/
|
||||||
async deleteConfig(configId) {
|
async deleteConfig(configId) {
|
||||||
console.log(`[deleteConfig][Action] Deleting Git config ${configId}`);
|
console.log(`[deleteConfig][Action] Deleting Git config ${configId}`);
|
||||||
const response = await fetch(`${API_BASE}/config/${configId}`, {
|
return requestApi(`${API_BASE}/config/${configId}`, 'DELETE');
|
||||||
method: 'DELETE'
|
|
||||||
});
|
|
||||||
if (!response.ok) throw new Error('Failed to delete Git config');
|
|
||||||
return response.json();
|
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -70,12 +60,7 @@ export const gitService = {
|
|||||||
*/
|
*/
|
||||||
async testConnection(config) {
|
async testConnection(config) {
|
||||||
console.log('[testConnection][Action] Testing Git connection');
|
console.log('[testConnection][Action] Testing Git connection');
|
||||||
const response = await fetch(`${API_BASE}/config/test`, {
|
return requestApi(`${API_BASE}/config/test`, 'POST', config);
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify(config)
|
|
||||||
});
|
|
||||||
return response.json();
|
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -90,16 +75,10 @@ export const gitService = {
|
|||||||
*/
|
*/
|
||||||
async initRepository(dashboardId, configId, remoteUrl) {
|
async initRepository(dashboardId, configId, remoteUrl) {
|
||||||
console.log(`[initRepository][Action] Initializing repo for dashboard ${dashboardId}`);
|
console.log(`[initRepository][Action] Initializing repo for dashboard ${dashboardId}`);
|
||||||
const response = await fetch(`${API_BASE}/repositories/${dashboardId}/init`, {
|
return requestApi(`${API_BASE}/repositories/${dashboardId}/init`, 'POST', {
|
||||||
method: 'POST',
|
config_id: configId,
|
||||||
headers: { 'Content-Type': 'application/json' },
|
remote_url: remoteUrl
|
||||||
body: JSON.stringify({ 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) {
|
async getBranches(dashboardId) {
|
||||||
console.log(`[getBranches][Action] Fetching branches for dashboard ${dashboardId}`);
|
console.log(`[getBranches][Action] Fetching branches for dashboard ${dashboardId}`);
|
||||||
const response = await fetch(`${API_BASE}/repositories/${dashboardId}/branches`);
|
return requestApi(`${API_BASE}/repositories/${dashboardId}/branches`);
|
||||||
if (!response.ok) throw new Error('Failed to fetch branches');
|
|
||||||
return response.json();
|
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -129,13 +106,10 @@ export const gitService = {
|
|||||||
*/
|
*/
|
||||||
async createBranch(dashboardId, name, fromBranch) {
|
async createBranch(dashboardId, name, fromBranch) {
|
||||||
console.log(`[createBranch][Action] Creating branch ${name} for dashboard ${dashboardId}`);
|
console.log(`[createBranch][Action] Creating branch ${name} for dashboard ${dashboardId}`);
|
||||||
const response = await fetch(`${API_BASE}/repositories/${dashboardId}/branches`, {
|
return requestApi(`${API_BASE}/repositories/${dashboardId}/branches`, 'POST', {
|
||||||
method: 'POST',
|
name,
|
||||||
headers: { 'Content-Type': 'application/json' },
|
from_branch: fromBranch
|
||||||
body: JSON.stringify({ 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) {
|
async checkoutBranch(dashboardId, name) {
|
||||||
console.log(`[checkoutBranch][Action] Checking out branch ${name} for dashboard ${dashboardId}`);
|
console.log(`[checkoutBranch][Action] Checking out branch ${name} for dashboard ${dashboardId}`);
|
||||||
const response = await fetch(`${API_BASE}/repositories/${dashboardId}/checkout`, {
|
return requestApi(`${API_BASE}/repositories/${dashboardId}/checkout`, 'POST', { name });
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({ name })
|
|
||||||
});
|
|
||||||
if (!response.ok) throw new Error('Failed to checkout branch');
|
|
||||||
return response.json();
|
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -170,13 +138,7 @@ export const gitService = {
|
|||||||
*/
|
*/
|
||||||
async commit(dashboardId, message, files) {
|
async commit(dashboardId, message, files) {
|
||||||
console.log(`[commit][Action] Committing changes for dashboard ${dashboardId}`);
|
console.log(`[commit][Action] Committing changes for dashboard ${dashboardId}`);
|
||||||
const response = await fetch(`${API_BASE}/repositories/${dashboardId}/commit`, {
|
return requestApi(`${API_BASE}/repositories/${dashboardId}/commit`, 'POST', { message, files });
|
||||||
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();
|
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -189,11 +151,7 @@ export const gitService = {
|
|||||||
*/
|
*/
|
||||||
async push(dashboardId) {
|
async push(dashboardId) {
|
||||||
console.log(`[push][Action] Pushing changes for dashboard ${dashboardId}`);
|
console.log(`[push][Action] Pushing changes for dashboard ${dashboardId}`);
|
||||||
const response = await fetch(`${API_BASE}/repositories/${dashboardId}/push`, {
|
return requestApi(`${API_BASE}/repositories/${dashboardId}/push`, 'POST');
|
||||||
method: 'POST'
|
|
||||||
});
|
|
||||||
if (!response.ok) throw new Error('Failed to push changes');
|
|
||||||
return response.json();
|
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -206,11 +164,7 @@ export const gitService = {
|
|||||||
*/
|
*/
|
||||||
async pull(dashboardId) {
|
async pull(dashboardId) {
|
||||||
console.log(`[pull][Action] Pulling changes for dashboard ${dashboardId}`);
|
console.log(`[pull][Action] Pulling changes for dashboard ${dashboardId}`);
|
||||||
const response = await fetch(`${API_BASE}/repositories/${dashboardId}/pull`, {
|
return requestApi(`${API_BASE}/repositories/${dashboardId}/pull`, 'POST');
|
||||||
method: 'POST'
|
|
||||||
});
|
|
||||||
if (!response.ok) throw new Error('Failed to pull changes');
|
|
||||||
return response.json();
|
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -221,9 +175,7 @@ export const gitService = {
|
|||||||
*/
|
*/
|
||||||
async getEnvironments() {
|
async getEnvironments() {
|
||||||
console.log('[getEnvironments][Action] Fetching environments');
|
console.log('[getEnvironments][Action] Fetching environments');
|
||||||
const response = await fetch(`${API_BASE}/environments`);
|
return requestApi(`${API_BASE}/environments`);
|
||||||
if (!response.ok) throw new Error('Failed to fetch environments');
|
|
||||||
return response.json();
|
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -237,13 +189,9 @@ export const gitService = {
|
|||||||
*/
|
*/
|
||||||
async deploy(dashboardId, environmentId) {
|
async deploy(dashboardId, environmentId) {
|
||||||
console.log(`[deploy][Action] Deploying dashboard ${dashboardId} to environment ${environmentId}`);
|
console.log(`[deploy][Action] Deploying dashboard ${dashboardId} to environment ${environmentId}`);
|
||||||
const response = await fetch(`${API_BASE}/repositories/${dashboardId}/deploy`, {
|
return requestApi(`${API_BASE}/repositories/${dashboardId}/deploy`, 'POST', {
|
||||||
method: 'POST',
|
environment_id: environmentId
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({ 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) {
|
async getHistory(dashboardId, limit = 50) {
|
||||||
console.log(`[getHistory][Action] Fetching history for dashboard ${dashboardId}`);
|
console.log(`[getHistory][Action] Fetching history for dashboard ${dashboardId}`);
|
||||||
const response = await fetch(`${API_BASE}/repositories/${dashboardId}/history?limit=${limit}`);
|
return requestApi(`${API_BASE}/repositories/${dashboardId}/history?limit=${limit}`);
|
||||||
if (!response.ok) throw new Error('Failed to fetch commit history');
|
|
||||||
return response.json();
|
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -269,17 +215,9 @@ export const gitService = {
|
|||||||
*/
|
*/
|
||||||
async sync(dashboardId, sourceEnvId = null) {
|
async sync(dashboardId, sourceEnvId = null) {
|
||||||
console.log(`[sync][Action] Syncing dashboard ${dashboardId}`);
|
console.log(`[sync][Action] Syncing dashboard ${dashboardId}`);
|
||||||
const url = new URL(`${window.location.origin}${API_BASE}/repositories/${dashboardId}/sync`);
|
let endpoint = `${API_BASE}/repositories/${dashboardId}/sync`;
|
||||||
if (sourceEnvId) url.searchParams.append('source_env_id', sourceEnvId);
|
if (sourceEnvId) endpoint += `?source_env_id=${sourceEnvId}`;
|
||||||
|
return requestApi(endpoint, 'POST');
|
||||||
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();
|
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -292,9 +230,7 @@ export const gitService = {
|
|||||||
*/
|
*/
|
||||||
async getStatus(dashboardId) {
|
async getStatus(dashboardId) {
|
||||||
console.log(`[getStatus][Action] Fetching status for dashboard ${dashboardId}`);
|
console.log(`[getStatus][Action] Fetching status for dashboard ${dashboardId}`);
|
||||||
const response = await fetch(`${API_BASE}/repositories/${dashboardId}/status`);
|
return requestApi(`${API_BASE}/repositories/${dashboardId}/status`);
|
||||||
if (!response.ok) throw new Error('Failed to fetch status');
|
|
||||||
return response.json();
|
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -309,15 +245,12 @@ export const gitService = {
|
|||||||
*/
|
*/
|
||||||
async getDiff(dashboardId, filePath = null, staged = false) {
|
async getDiff(dashboardId, filePath = null, staged = false) {
|
||||||
console.log(`[getDiff][Action] Fetching diff for dashboard ${dashboardId} (file: ${filePath}, staged: ${staged})`);
|
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();
|
const params = new URLSearchParams();
|
||||||
if (filePath) params.append('file_path', filePath);
|
if (filePath) params.append('file_path', filePath);
|
||||||
if (staged) params.append('staged', 'true');
|
if (staged) params.append('staged', 'true');
|
||||||
if (params.toString()) url += `?${params.toString()}`;
|
if (params.toString()) endpoint += `?${params.toString()}`;
|
||||||
|
return requestApi(endpoint);
|
||||||
const response = await fetch(url);
|
|
||||||
if (!response.ok) throw new Error('Failed to fetch diff');
|
|
||||||
return response.json();
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
// [/DEF:gitService:Action]
|
// [/DEF:gitService:Action]
|
||||||
|
|||||||
@@ -2,7 +2,9 @@
|
|||||||
* Service for interacting with the Task Management API.
|
* 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]
|
// [DEF:getTasks:Function]
|
||||||
/* @PURPOSE: Fetch a list of tasks with pagination and optional status filter.
|
/* @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);
|
params.append('status', status);
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await fetch(`${API_BASE}?${params.toString()}`);
|
return requestApi(`${API_BASE}?${params.toString()}`);
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`Failed to fetch tasks: ${response.statusText}`);
|
|
||||||
}
|
|
||||||
return await response.json();
|
|
||||||
}
|
}
|
||||||
// [/DEF:getTasks:Function]
|
// [/DEF:getTasks:Function]
|
||||||
|
|
||||||
@@ -44,11 +42,7 @@ export async function getTasks(limit = 10, offset = 0, status = null) {
|
|||||||
* @returns {Promise<Object>} Task details.
|
* @returns {Promise<Object>} Task details.
|
||||||
*/
|
*/
|
||||||
export async function getTask(taskId) {
|
export async function getTask(taskId) {
|
||||||
const response = await fetch(`${API_BASE}/${taskId}`);
|
return requestApi(`${API_BASE}/${taskId}`);
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`Failed to fetch task ${taskId}: ${response.statusText}`);
|
|
||||||
}
|
|
||||||
return await response.json();
|
|
||||||
}
|
}
|
||||||
// [/DEF:getTask:Function]
|
// [/DEF:getTask:Function]
|
||||||
|
|
||||||
@@ -86,19 +80,7 @@ export async function getTaskLogs(taskId) {
|
|||||||
* @returns {Promise<Object>} Updated task object.
|
* @returns {Promise<Object>} Updated task object.
|
||||||
*/
|
*/
|
||||||
export async function resumeTask(taskId, passwords) {
|
export async function resumeTask(taskId, passwords) {
|
||||||
const response = await fetch(`${API_BASE}/${taskId}/resume`, {
|
return requestApi(`${API_BASE}/${taskId}/resume`, 'POST', { passwords });
|
||||||
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();
|
|
||||||
}
|
}
|
||||||
// [/DEF:resumeTask:Function]
|
// [/DEF:resumeTask:Function]
|
||||||
|
|
||||||
@@ -114,19 +96,7 @@ export async function resumeTask(taskId, passwords) {
|
|||||||
* @returns {Promise<Object>} Updated task object.
|
* @returns {Promise<Object>} Updated task object.
|
||||||
*/
|
*/
|
||||||
export async function resolveTask(taskId, resolutionParams) {
|
export async function resolveTask(taskId, resolutionParams) {
|
||||||
const response = await fetch(`${API_BASE}/${taskId}/resolve`, {
|
return requestApi(`${API_BASE}/${taskId}/resolve`, 'POST', { resolution_params: resolutionParams });
|
||||||
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();
|
|
||||||
}
|
}
|
||||||
// [/DEF:resolveTask:Function]
|
// [/DEF:resolveTask:Function]
|
||||||
|
|
||||||
@@ -145,12 +115,6 @@ export async function clearTasks(status = null) {
|
|||||||
params.append('status', status);
|
params.append('status', status);
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await fetch(`${API_BASE}?${params.toString()}`, {
|
return requestApi(`${API_BASE}?${params.toString()}`, 'DELETE');
|
||||||
method: 'DELETE'
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`Failed to clear tasks: ${response.statusText}`);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
// [/DEF:clearTasks:Function]
|
// [/DEF:clearTasks:Function]
|
||||||
@@ -2,7 +2,9 @@
|
|||||||
* Service for generic Task API communication used by Tools.
|
* 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]
|
// [DEF:runTask:Function]
|
||||||
/* @PURPOSE: Start a new task for a given plugin.
|
/* @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.
|
* @returns {Promise<Object>} The created task instance.
|
||||||
*/
|
*/
|
||||||
export async function runTask(pluginId, params) {
|
export async function runTask(pluginId, params) {
|
||||||
const response = await fetch(API_BASE, {
|
return requestApi(API_BASE, 'POST', { plugin_id: pluginId, params });
|
||||||
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();
|
|
||||||
}
|
}
|
||||||
// [/DEF:runTask:Function]
|
// [/DEF:runTask:Function]
|
||||||
|
|
||||||
@@ -43,10 +33,6 @@ export async function runTask(pluginId, params) {
|
|||||||
* @returns {Promise<Object>} Task details.
|
* @returns {Promise<Object>} Task details.
|
||||||
*/
|
*/
|
||||||
export async function getTaskStatus(taskId) {
|
export async function getTaskStatus(taskId) {
|
||||||
const response = await fetch(`${API_BASE}/${taskId}`);
|
return requestApi(`${API_BASE}/${taskId}`);
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`Failed to fetch task ${taskId}: ${response.statusText}`);
|
|
||||||
}
|
|
||||||
return await response.json();
|
|
||||||
}
|
}
|
||||||
// [/DEF:getTaskStatus:Function]
|
// [/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] 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] 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`
|
- [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`
|
- [x] 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] 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
|
- [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`
|
- [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`
|
- [x] 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] 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] 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] 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`
|
- [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