Merge dev to main: Security improvements and comprehensive test infrastructure #39

Merged
jimmy merged 195 commits from dev into main 2026-04-08 01:42:27 +08:00
3 changed files with 50 additions and 28 deletions
Showing only changes of commit 6cffc1f9ff - Show all commits
+16 -9
View File
@@ -1,11 +1,11 @@
""" """
OPC Agents Router OPC Agents Router
""" """
from fastapi import APIRouter, Depends, HTTPException from fastapi import APIRouter, Depends, HTTPException, Request
from typing import Optional, List from typing import Optional, List
from pydantic import BaseModel from pydantic import BaseModel
from db import opc_db from db import opc_db
from rbac import require_page, User, _inject_user, require_admin from rbac import require_page, User, require_admin
router = APIRouter() router = APIRouter()
@@ -23,10 +23,11 @@ class ExecutionApproval(BaseModel):
@router.get("/agents") @router.get("/agents")
async def list_agents( async def list_agents(
request: Request,
enabled_only: bool = True, enabled_only: bool = True,
user: User = Depends(_inject_user)
): ):
"""List all agents""" """List all agents"""
user: User = request.state.user
agents = await opc_db.get_agents(enabled_only=enabled_only) agents = await opc_db.get_agents(enabled_only=enabled_only)
return {"items": agents} return {"items": agents}
@@ -34,9 +35,10 @@ async def list_agents(
@router.get("/agents/{agent_id}") @router.get("/agents/{agent_id}")
async def get_agent( async def get_agent(
agent_id: str, agent_id: str,
user: User = Depends(_inject_user) request: Request,
): ):
"""Get agent details""" """Get agent details"""
user: User = request.state.user
agent = await opc_db.get_agent(agent_id) agent = await opc_db.get_agent(agent_id)
if not agent: if not agent:
raise HTTPException(status_code=404, detail="Agent not found") raise HTTPException(status_code=404, detail="Agent not found")
@@ -47,9 +49,10 @@ async def get_agent(
async def update_agent( async def update_agent(
agent_id: str, agent_id: str,
updates: AgentUpdate, updates: AgentUpdate,
user: User = Depends(_inject_user) request: Request,
): ):
"""Update agent configuration (admin only)""" """Update agent configuration (admin only)"""
user: User = request.state.user
# This would need an update_agent function in opc_db # This would need an update_agent function in opc_db
raise HTTPException(status_code=501, detail="Not implemented yet") raise HTTPException(status_code=501, detail="Not implemented yet")
@@ -58,9 +61,10 @@ async def update_agent(
async def trigger_agent( async def trigger_agent(
agent_id: str, agent_id: str,
task_id: int, task_id: int,
user: User = Depends(_inject_user) request: Request,
): ):
"""Manually trigger agent execution""" """Manually trigger agent execution"""
user: User = request.state.user
execution = await opc_db.create_agent_execution( execution = await opc_db.create_agent_execution(
task_id=task_id, task_id=task_id,
agent_id=agent_id, agent_id=agent_id,
@@ -71,13 +75,14 @@ async def trigger_agent(
@router.get("/executions") @router.get("/executions")
async def list_executions( async def list_executions(
request: Request,
task_id: Optional[int] = None, task_id: Optional[int] = None,
agent_id: Optional[str] = None, agent_id: Optional[str] = None,
status: Optional[str] = None, status: Optional[str] = None,
limit: int = 50, limit: int = 50,
user: User = Depends(_inject_user)
): ):
"""List agent executions""" """List agent executions"""
user: User = request.state.user
executions = await opc_db.get_agent_executions( executions = await opc_db.get_agent_executions(
task_id=task_id, task_id=task_id,
agent_id=agent_id, agent_id=agent_id,
@@ -90,9 +95,10 @@ async def list_executions(
@router.get("/executions/{execution_id}") @router.get("/executions/{execution_id}")
async def get_execution( async def get_execution(
execution_id: int, execution_id: int,
user: User = Depends(_inject_user) request: Request,
): ):
"""Get execution details""" """Get execution details"""
user: User = request.state.user
executions = await opc_db.get_agent_executions(limit=1000) executions = await opc_db.get_agent_executions(limit=1000)
execution = next((e for e in executions if e["id"] == execution_id), None) execution = next((e for e in executions if e["id"] == execution_id), None)
if not execution: if not execution:
@@ -104,9 +110,10 @@ async def get_execution(
async def approve_execution( async def approve_execution(
execution_id: int, execution_id: int,
approval: ExecutionApproval, approval: ExecutionApproval,
user: User = Depends(_inject_user) request: Request,
): ):
"""Approve or reject agent execution (for CxO level actions)""" """Approve or reject agent execution (for CxO level actions)"""
user: User = request.state.user
# This would need an approve_execution function in opc_db # This would need an approve_execution function in opc_db
# For now, return placeholder # For now, return placeholder
return { return {
+14 -8
View File
@@ -1,11 +1,11 @@
""" """
OPC Projects and Invoicing Router OPC Projects and Invoicing Router
""" """
from fastapi import APIRouter, Depends, HTTPException, Response from fastapi import APIRouter, Depends, HTTPException, Response, Request
from typing import Optional from typing import Optional
from pydantic import BaseModel from pydantic import BaseModel
from db import opc_db from db import opc_db
from rbac import User, _inject_user from rbac import User
from services import pdf_service from services import pdf_service
from datetime import datetime from datetime import datetime
@@ -26,9 +26,10 @@ class InvoiceGenerate(BaseModel):
@router.get("/projects") @router.get("/projects")
async def list_projects( async def list_projects(
user: User = Depends(_inject_user) request: Request,
): ):
"""List all projects""" """List all projects"""
user: User = request.state.user
pool = await opc_db.get_pool() pool = await opc_db.get_pool()
async with pool.acquire() as conn: async with pool.acquire() as conn:
rows = await conn.fetch("SELECT * FROM projects ORDER BY created_at DESC") rows = await conn.fetch("SELECT * FROM projects ORDER BY created_at DESC")
@@ -38,9 +39,10 @@ async def list_projects(
@router.post("/projects") @router.post("/projects")
async def create_project( async def create_project(
project: ProjectCreate, project: ProjectCreate,
user: User = Depends(_inject_user) request: Request,
): ):
"""Create a new project""" """Create a new project"""
user: User = request.state.user
pool = await opc_db.get_pool() pool = await opc_db.get_pool()
async with pool.acquire() as conn: async with pool.acquire() as conn:
row = await conn.fetchrow(""" row = await conn.fetchrow("""
@@ -53,9 +55,10 @@ async def create_project(
@router.get("/clients") @router.get("/clients")
async def list_clients( async def list_clients(
user: User = Depends(_inject_user) request: Request,
): ):
"""List all clients""" """List all clients"""
user: User = request.state.user
pool = await opc_db.get_pool() pool = await opc_db.get_pool()
async with pool.acquire() as conn: async with pool.acquire() as conn:
rows = await conn.fetch("SELECT * FROM clients ORDER BY name") rows = await conn.fetch("SELECT * FROM clients ORDER BY name")
@@ -64,12 +67,13 @@ async def list_clients(
@router.post("/clients") @router.post("/clients")
async def create_client( async def create_client(
request: Request,
name: str, name: str,
email: Optional[str] = None, email: Optional[str] = None,
company: Optional[str] = None, company: Optional[str] = None,
user: User = Depends(_inject_user)
): ):
"""Create a new client""" """Create a new client"""
user: User = request.state.user
pool = await opc_db.get_pool() pool = await opc_db.get_pool()
async with pool.acquire() as conn: async with pool.acquire() as conn:
row = await conn.fetchrow(""" row = await conn.fetchrow("""
@@ -83,9 +87,10 @@ async def create_client(
@router.post("/invoices/generate") @router.post("/invoices/generate")
async def generate_invoice( async def generate_invoice(
invoice: InvoiceGenerate, invoice: InvoiceGenerate,
user: User = Depends(_inject_user) request: Request,
): ):
"""Generate PDF invoice for a project""" """Generate PDF invoice for a project"""
user: User = request.state.user
pool = await opc_db.get_pool() pool = await opc_db.get_pool()
async with pool.acquire() as conn: async with pool.acquire() as conn:
# Get client # Get client
@@ -128,9 +133,10 @@ async def generate_invoice(
@router.get("/projects/{project_id}/time") @router.get("/projects/{project_id}/time")
async def get_project_time( async def get_project_time(
project_id: int, project_id: int,
user: User = Depends(_inject_user) request: Request,
): ):
"""Get time entries for a project""" """Get time entries for a project"""
user: User = request.state.user
entries = await opc_db.get_time_entries(project_id=project_id) entries = await opc_db.get_time_entries(project_id=project_id)
total_minutes = sum(e["duration_minutes"] for e in entries) total_minutes = sum(e["duration_minutes"] for e in entries)
billable_minutes = sum(e["duration_minutes"] for e in entries if e.get("billable")) billable_minutes = sum(e["duration_minutes"] for e in entries if e.get("billable"))
+20 -11
View File
@@ -1,12 +1,12 @@
""" """
OPC Task Management Router OPC Task Management Router
""" """
from fastapi import APIRouter, Depends, Query, HTTPException from fastapi import APIRouter, Depends, Query, HTTPException, Request
from typing import Optional, List from typing import Optional, List
from pydantic import BaseModel from pydantic import BaseModel
from datetime import datetime from datetime import datetime
from db import opc_db from db import opc_db
from rbac import require_page, User, _inject_user from rbac import require_page, User
from routers import opc_ws from routers import opc_ws
router = APIRouter() router = APIRouter()
@@ -48,6 +48,7 @@ class TaskAssign(BaseModel):
@router.get("/tasks") @router.get("/tasks")
async def list_tasks( async def list_tasks(
request: Request,
status: Optional[str] = Query(None), status: Optional[str] = Query(None),
assigned_to: Optional[str] = Query(None), assigned_to: Optional[str] = Query(None),
project_id: Optional[int] = Query(None), project_id: Optional[int] = Query(None),
@@ -55,9 +56,9 @@ async def list_tasks(
tags: Optional[str] = Query(None), tags: Optional[str] = Query(None),
limit: int = Query(50, le=100), limit: int = Query(50, le=100),
offset: int = Query(0, ge=0), offset: int = Query(0, ge=0),
user: User = Depends(_inject_user)
): ):
"""List tasks with filters""" """List tasks with filters"""
user: User = request.state.user
tag_list = tags.split(",") if tags else None tag_list = tags.split(",") if tags else None
tasks = await opc_db.get_tasks( tasks = await opc_db.get_tasks(
status=status, status=status,
@@ -73,10 +74,11 @@ async def list_tasks(
@router.post("/tasks") @router.post("/tasks")
async def create_task( async def create_task(
request: Request,
task: TaskCreate, task: TaskCreate,
user: User = Depends(_inject_user)
): ):
"""Create a new task""" """Create a new task"""
user: User = request.state.user
created_task = await opc_db.create_task( created_task = await opc_db.create_task(
title=task.title, title=task.title,
description=task.description, description=task.description,
@@ -107,9 +109,10 @@ async def create_task(
@router.get("/tasks/{task_id}") @router.get("/tasks/{task_id}")
async def get_task( async def get_task(
task_id: int, task_id: int,
user: User = Depends(_inject_user) request: Request,
): ):
"""Get task details""" """Get task details"""
user: User = request.state.user
tasks = await opc_db.get_tasks(limit=1, offset=0) tasks = await opc_db.get_tasks(limit=1, offset=0)
task = next((t for t in tasks if t["id"] == task_id), None) task = next((t for t in tasks if t["id"] == task_id), None)
if not task: if not task:
@@ -121,9 +124,10 @@ async def get_task(
async def update_task( async def update_task(
task_id: int, task_id: int,
task: TaskUpdate, task: TaskUpdate,
user: User = Depends(_inject_user) request: Request,
): ):
"""Update a task""" """Update a task"""
user: User = request.state.user
updates = task.dict(exclude_unset=True) updates = task.dict(exclude_unset=True)
# Get current task status # Get current task status
@@ -167,9 +171,10 @@ async def update_task(
@router.delete("/tasks/{task_id}") @router.delete("/tasks/{task_id}")
async def delete_task( async def delete_task(
task_id: int, task_id: int,
user: User = Depends(_inject_user) request: Request,
): ):
"""Delete a task""" """Delete a task"""
user: User = request.state.user
await opc_db.delete_task(task_id=task_id, actor=user.username) await opc_db.delete_task(task_id=task_id, actor=user.username)
# Broadcast WebSocket update # Broadcast WebSocket update
@@ -182,9 +187,10 @@ async def delete_task(
async def move_task( async def move_task(
task_id: int, task_id: int,
move: TaskMove, move: TaskMove,
user: User = Depends(_inject_user) request: Request,
): ):
"""Move task to a different column""" """Move task to a different column"""
user: User = request.state.user
updates = {"status": move.status} updates = {"status": move.status}
# Get current task # Get current task
@@ -221,9 +227,10 @@ async def move_task(
async def assign_task( async def assign_task(
task_id: int, task_id: int,
assign: TaskAssign, assign: TaskAssign,
user: User = Depends(_inject_user) request: Request,
): ):
"""Assign task to human or agent""" """Assign task to human or agent"""
user: User = request.state.user
updates = { updates = {
"assigned_to": assign.assigned_to, "assigned_to": assign.assigned_to,
"assigned_type": assign.assigned_type "assigned_type": assign.assigned_type
@@ -249,9 +256,10 @@ async def assign_task(
@router.get("/tasks/{task_id}/history") @router.get("/tasks/{task_id}/history")
async def get_task_history( async def get_task_history(
task_id: int, task_id: int,
user: User = Depends(_inject_user) request: Request,
): ):
"""Get task history""" """Get task history"""
user: User = request.state.user
history = await opc_db.get_task_history(task_id=task_id) history = await opc_db.get_task_history(task_id=task_id)
return {"items": history} return {"items": history}
@@ -259,9 +267,10 @@ async def get_task_history(
@router.get("/tasks/{task_id}/time") @router.get("/tasks/{task_id}/time")
async def get_task_time( async def get_task_time(
task_id: int, task_id: int,
user: User = Depends(_inject_user) request: Request,
): ):
"""Get time entries for a task""" """Get time entries for a task"""
user: User = request.state.user
entries = await opc_db.get_time_entries(task_id=task_id) entries = await opc_db.get_time_entries(task_id=task_id)
total_minutes = sum(e["duration_minutes"] for e in entries) total_minutes = sum(e["duration_minutes"] for e in entries)
return { return {