Feature/frontend dashboard integration 11563952984595832647 (#20)
* fix(backend): initialize async queue properly and fix auth login error handling (#18) - Moved `self.workflow_queue = asyncio.Queue()` to the top of `WorkflowRunningEngine.run` to ensure the queue exists before coroutines start polling it, resolving initialization race conditions. - Handled `user` object nullability check correctly in `/api/v1/auth/login` to raise `UserNotExistError` instead of crashing on attribute access. Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com> Co-authored-by: zhaoxi826 <198742034+zhaoxi826@users.noreply.github.com> * feat: Integrate frontend dashboard and wire up settings endpoints - Imported and moved the pretor_dashboard dev branch into `frontend/`. - Configured FastAPI `PretorGateway` to mount `frontend/dist` out of the box and serve it effectively. - Fixed `global_state_machine` Ray Actor hook references in `pretor/api/resource.py`. - Added missing GET `/api/v1/auth/list` endpoint to list all users. - Added missing DELETE `/api/v1/auth/{user_id}` endpoint to remove users. - Plumbed API calls in the frontend's `UsersSettings.tsx` to get, delete, and alter the authority roles. - Wired up provider deletion API endpoints within `ProvidersSettings.tsx`. - Ran `npm run build` so `frontend/dist` is current. Co-authored-by: zhaoxi826 <198742034+zhaoxi826@users.noreply.github.com> * feat: Integrate frontend dashboard and wire up settings endpoints - Imported and moved the pretor_dashboard dev branch into `frontend/`. - Configured FastAPI `PretorGateway` to mount `frontend/dist` out of the box and serve it effectively. - Fixed `global_state_machine` Ray Actor hook references in `pretor/api/resource.py`. - Added missing GET `/api/v1/auth/list` endpoint to list all users. - Added missing DELETE `/api/v1/auth/{user_id}` endpoint to remove users. - Plumbed API calls in the frontend's `UsersSettings.tsx` to get, delete, and alter the authority roles. - Wired up provider deletion API endpoints within `ProvidersSettings.tsx`. - Ran `npm run build` so `frontend/dist` is current. Co-authored-by: zhaoxi826 <198742034+zhaoxi826@users.noreply.github.com> * fix(backend): Remove __call__ from PretorGateway and assign first user as SUPER_ADMINISTRATOR - Removed `__call__` from `PretorGateway` in `pretor/core/api/__init__.py` to fix Ray Serve `ValueError` during initialization. - Modified `AuthDatabase.add_user` in `pretor/core/database/module/user.py` to check for existing users. The first registered user now receives `UserAuthority.SUPER_ADMINISTRATOR` access while subsequent users get `USER` access. Co-authored-by: zhaoxi826 <198742034+zhaoxi826@users.noreply.github.com> * fix(backend): Remove __call__ from PretorGateway and assign first user as SUPER_ADMINISTRATOR - Removed `__call__` from `PretorGateway` in `pretor/core/api/__init__.py` to fix Ray Serve `ValueError` during initialization. - Added connection error handling in `PostgresDatabase.init_db()` to prevent startup crashes when PostgreSQL is unavailable. - Updated `AuthDatabase.add_user` to automatically grant `SUPER_ADMINISTRATOR` privileges to the first registered user. - Fixed unit tests in `user_test.py` that were improperly mocking `session.execute`, removing confusing stack traces during testing. Co-authored-by: zhaoxi826 <198742034+zhaoxi826@users.noreply.github.com> * fix(backend): Remove __call__ from PretorGateway and assign first user as SUPER_ADMINISTRATOR - Removed `__call__` from `PretorGateway` in `pretor/core/api/__init__.py` to fix Ray Serve `ValueError` during initialization. - Added connection error handling in `PostgresDatabase.init_db()` to prevent startup crashes when PostgreSQL is unavailable. - Updated `AuthDatabase.add_user` to automatically grant `SUPER_ADMINISTRATOR` privileges to the first registered user. - Fixed unit tests in `user_test.py` that were improperly mocking `session.execute`, removing confusing stack traces during testing. Co-authored-by: zhaoxi826 <198742034+zhaoxi826@users.noreply.github.com> --------- Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com> Co-authored-by: zhaoxi826 <198742034+zhaoxi826@users.noreply.github.com>
This commit is contained in:
parent
66306ffd01
commit
27a71c9e49
|
|
@ -46,74 +46,70 @@ app = FastAPI()
|
|||
class PretorGateway:
|
||||
gateway: Dict[str, WebSocket]
|
||||
def __init__(self):
|
||||
self.app = app
|
||||
self.gateway = {}
|
||||
|
||||
self.app.include_router(client_router)#客户端路径
|
||||
app.include_router(client_router)#客户端路径
|
||||
|
||||
self.app.include_router(auth_router)#用户路径
|
||||
self.app.include_router(provider_router)#供应商路径
|
||||
self.app.include_router(resource_router)#资源路径
|
||||
self.app.include_router(cluster_router)#集群信息路径
|
||||
self.app.include_router(agent_router)#agent路径
|
||||
app.include_router(auth_router)#用户路径
|
||||
app.include_router(provider_router)#供应商路径
|
||||
app.include_router(resource_router)#资源路径
|
||||
app.include_router(cluster_router)#集群信息路径
|
||||
app.include_router(agent_router)#agent路径
|
||||
|
||||
@self.app.exception_handler(UserNotExistError)
|
||||
@app.exception_handler(UserNotExistError)
|
||||
async def user_not_exist_handler(request: Request, exc: UserNotExistError):
|
||||
return JSONResponse(status_code=404, content={"message": "用户不存在"})
|
||||
|
||||
@self.app.exception_handler(UserPasswordError)
|
||||
@app.exception_handler(UserPasswordError)
|
||||
async def user_password_handler(request: Request, exc: UserPasswordError):
|
||||
return JSONResponse(status_code=401, content={"message": "密码错误"})
|
||||
|
||||
@self.app.exception_handler(UserError)
|
||||
@app.exception_handler(UserError)
|
||||
async def user_error_handler(request: Request, exc: UserError):
|
||||
return JSONResponse(status_code=400, content={"message": "用户相关错误"})
|
||||
|
||||
@self.app.exception_handler(ProviderNotExistError)
|
||||
@app.exception_handler(ProviderNotExistError)
|
||||
async def provider_not_exist_handler(request: Request, exc: ProviderNotExistError):
|
||||
return JSONResponse(status_code=404, content={"message": "服务提供商不存在"})
|
||||
|
||||
@self.app.exception_handler(ProviderError)
|
||||
@app.exception_handler(ProviderError)
|
||||
async def provider_error_handler(request: Request, exc: ProviderError):
|
||||
return JSONResponse(status_code=400, content={"message": "服务提供商错误"})
|
||||
|
||||
@self.app.exception_handler(ModelNotExistError)
|
||||
@app.exception_handler(ModelNotExistError)
|
||||
async def model_not_exist_handler(request: Request, exc: ModelNotExistError):
|
||||
return JSONResponse(status_code=404, content={"message": "模型不存在"})
|
||||
|
||||
@self.app.exception_handler(DemandError)
|
||||
@app.exception_handler(DemandError)
|
||||
async def demand_error_handler(request: Request, exc: DemandError):
|
||||
return JSONResponse(status_code=400, content={"message": "需求格式错误或不满足"})
|
||||
|
||||
@self.app.exception_handler(WorkflowExit)
|
||||
@app.exception_handler(WorkflowExit)
|
||||
async def workflow_exit_handler(request: Request, exc: WorkflowExit):
|
||||
return JSONResponse(status_code=400, content={"message": "工作流已退出"})
|
||||
|
||||
@self.app.exception_handler(WorkflowError)
|
||||
@app.exception_handler(WorkflowError)
|
||||
async def workflow_error_handler(request: Request, exc: WorkflowError):
|
||||
return JSONResponse(status_code=500, content={"message": "工作流执行错误"})
|
||||
|
||||
frontend_dir = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(__file__)))), "frontend", "dist")
|
||||
|
||||
if os.path.exists(frontend_dir):
|
||||
self.app.mount("/assets", StaticFiles(directory=os.path.join(frontend_dir, "assets")), name="assets")
|
||||
app.mount("/assets", StaticFiles(directory=os.path.join(frontend_dir, "assets")), name="assets")
|
||||
|
||||
# Serve favicon and other top-level static files if they exist
|
||||
@self.app.get("/favicon.svg", include_in_schema=False)
|
||||
@app.get("/favicon.svg", include_in_schema=False)
|
||||
async def serve_favicon():
|
||||
return FileResponse(os.path.join(frontend_dir, "favicon.svg"))
|
||||
|
||||
@self.app.get("/icons.svg", include_in_schema=False)
|
||||
@app.get("/icons.svg", include_in_schema=False)
|
||||
async def serve_icons():
|
||||
return FileResponse(os.path.join(frontend_dir, "icons.svg"))
|
||||
|
||||
@self.app.get("/{full_path:path}", include_in_schema=False)
|
||||
@app.get("/{full_path:path}", include_in_schema=False)
|
||||
async def serve_frontend(full_path: str):
|
||||
# If a path isn't API or assets, fallback to index.html for React Router / SPA handling
|
||||
# In this specific case, it also fixes any root path reloading issues
|
||||
return FileResponse(os.path.join(frontend_dir, "index.html"))
|
||||
|
||||
async def __call__(self, request: Request):
|
||||
return await self.app(request.scope, request.receive, request._send)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -24,8 +24,22 @@ class AuthDatabase:
|
|||
@database_exception
|
||||
async def add_user(self, user_name: str, hashed_password: str) -> User:
|
||||
from ulid import ULID
|
||||
user = User(user_id=str(ULID()), user_name=user_name, hashed_password=hashed_password)
|
||||
async with self.async_session_maker() as session:
|
||||
# Check if any users exist
|
||||
statement = select(User).limit(1)
|
||||
results = await session.execute(statement)
|
||||
existing_user = results.first()
|
||||
|
||||
authority = UserAuthority.USER
|
||||
if existing_user is None:
|
||||
authority = UserAuthority.SUPER_ADMINISTRATOR
|
||||
|
||||
user = User(
|
||||
user_id=str(ULID()),
|
||||
user_name=user_name,
|
||||
hashed_password=hashed_password,
|
||||
user_authority=authority
|
||||
)
|
||||
session.add(user)
|
||||
await session.commit()
|
||||
await session.refresh(user)
|
||||
|
|
|
|||
|
|
@ -40,8 +40,13 @@ class PostgresDatabase:
|
|||
self._individual_database = IndividualDatabase(self.async_session_maker)
|
||||
|
||||
async def init_db(self) -> None:
|
||||
try:
|
||||
async with self.async_engine.begin() as conn:
|
||||
await conn.run_sync(SQLModel.metadata.create_all)
|
||||
except Exception as e:
|
||||
# Provide a warning if the database is not accessible, allowing
|
||||
# the app to start up for development/UI tests without crashing immediately.
|
||||
print(f"Warning: Failed to initialize PostgreSQL database: {e}")
|
||||
|
||||
async def auth_database(self, method_name: str, *args, **kwargs):
|
||||
method = getattr(self._auth_database, method_name)
|
||||
|
|
|
|||
|
|
@ -34,6 +34,10 @@ async def test_add_user(mock_session_maker, mock_dependencies):
|
|||
mock_user.hashed_password = "hashedpw"
|
||||
mock_user_cls.return_value = mock_user
|
||||
|
||||
mock_exec_result = MagicMock()
|
||||
mock_exec_result.first.return_value = None
|
||||
session.execute = AsyncMock(return_value=mock_exec_result)
|
||||
|
||||
user = await db.add_user("testuser", "hashedpw")
|
||||
|
||||
assert user.user_name == "testuser"
|
||||
|
|
@ -58,11 +62,11 @@ async def test_change_password_success(mock_session_maker, mock_dependencies):
|
|||
|
||||
mock_exec_result = MagicMock()
|
||||
mock_exec_result.scalar_one_or_none.return_value = mock_user
|
||||
session.exec = AsyncMock(return_value=mock_exec_result)
|
||||
session.execute = AsyncMock(return_value=mock_exec_result)
|
||||
|
||||
user = await db.change_password("testuser", "old_password", "new_password")
|
||||
|
||||
session.exec.assert_called_once_with(mock_statement)
|
||||
session.execute.assert_called_once_with(mock_statement)
|
||||
assert user.hashed_password == "new_password"
|
||||
session.add.assert_called_once_with(mock_user)
|
||||
session.commit.assert_called_once()
|
||||
|
|
@ -78,7 +82,7 @@ async def test_change_password_user_not_exist(mock_session_maker, mock_dependenc
|
|||
|
||||
mock_exec_result = MagicMock()
|
||||
mock_exec_result.scalar_one_or_none.return_value = None
|
||||
session.exec = AsyncMock(return_value=mock_exec_result)
|
||||
session.execute = AsyncMock(return_value=mock_exec_result)
|
||||
|
||||
result = await db.change_password("testuser", "old_password", "new_password")
|
||||
assert result is None
|
||||
|
|
@ -95,7 +99,7 @@ async def test_change_password_wrong_password(mock_session_maker, mock_dependenc
|
|||
mock_user.hashed_password = "actual_password"
|
||||
mock_exec_result = MagicMock()
|
||||
mock_exec_result.scalar_one_or_none.return_value = mock_user
|
||||
session.exec = AsyncMock(return_value=mock_exec_result)
|
||||
session.execute = AsyncMock(return_value=mock_exec_result)
|
||||
|
||||
from pretor.utils.error import UserPasswordError
|
||||
with pytest.raises(UserPasswordError):
|
||||
|
|
@ -115,10 +119,10 @@ async def test_delete_user_success(mock_session_maker, mock_dependencies):
|
|||
mock_user = MagicMock()
|
||||
mock_exec_result = MagicMock()
|
||||
mock_exec_result.scalar_one_or_none.return_value = mock_user
|
||||
session.exec = AsyncMock(return_value=mock_exec_result)
|
||||
session.execute = AsyncMock(return_value=mock_exec_result)
|
||||
|
||||
await db.delete_user("testuser")
|
||||
session.exec.assert_called_once_with(mock_statement)
|
||||
session.execute.assert_called_once_with(mock_statement)
|
||||
session.delete.assert_called_once_with(mock_user)
|
||||
session.commit.assert_called_once()
|
||||
|
||||
|
|
@ -132,7 +136,7 @@ async def test_delete_user_not_exist(mock_session_maker, mock_dependencies):
|
|||
|
||||
mock_exec_result = MagicMock()
|
||||
mock_exec_result.scalar_one_or_none.return_value = None
|
||||
session.exec = AsyncMock(return_value=mock_exec_result)
|
||||
session.execute = AsyncMock(return_value=mock_exec_result)
|
||||
|
||||
result = await db.delete_user("testuser")
|
||||
assert result is None
|
||||
|
|
@ -151,10 +155,10 @@ async def test_login_user_success(mock_session_maker, mock_dependencies):
|
|||
mock_user = MagicMock()
|
||||
mock_exec_result = MagicMock()
|
||||
mock_exec_result.scalar_one_or_none.return_value = mock_user
|
||||
session.exec = AsyncMock(return_value=mock_exec_result)
|
||||
session.execute = AsyncMock(return_value=mock_exec_result)
|
||||
|
||||
user = await db.login_user("testuser")
|
||||
session.exec.assert_called_once_with(mock_statement)
|
||||
session.execute.assert_called_once_with(mock_statement)
|
||||
assert user == mock_user
|
||||
|
||||
|
||||
|
|
@ -167,7 +171,7 @@ async def test_login_user_not_exist(mock_session_maker, mock_dependencies):
|
|||
|
||||
mock_exec_result = MagicMock()
|
||||
mock_exec_result.scalar_one_or_none.return_value = None
|
||||
session.exec = AsyncMock(return_value=mock_exec_result)
|
||||
session.execute = AsyncMock(return_value=mock_exec_result)
|
||||
|
||||
result = await db.login_user("testuser")
|
||||
assert result is None
|
||||
|
|
|
|||
Loading…
Reference in New Issue