kairo-code 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- image-service/main.py +178 -0
- infra/chat/app/main.py +84 -0
- kairo/backend/__init__.py +0 -0
- kairo/backend/api/__init__.py +0 -0
- kairo/backend/api/admin/__init__.py +23 -0
- kairo/backend/api/admin/audit.py +54 -0
- kairo/backend/api/admin/content.py +142 -0
- kairo/backend/api/admin/incidents.py +148 -0
- kairo/backend/api/admin/stats.py +125 -0
- kairo/backend/api/admin/system.py +87 -0
- kairo/backend/api/admin/users.py +279 -0
- kairo/backend/api/agents.py +94 -0
- kairo/backend/api/api_keys.py +85 -0
- kairo/backend/api/auth.py +116 -0
- kairo/backend/api/billing.py +41 -0
- kairo/backend/api/chat.py +72 -0
- kairo/backend/api/conversations.py +125 -0
- kairo/backend/api/device_auth.py +100 -0
- kairo/backend/api/files.py +83 -0
- kairo/backend/api/health.py +36 -0
- kairo/backend/api/images.py +80 -0
- kairo/backend/api/openai_compat.py +225 -0
- kairo/backend/api/projects.py +102 -0
- kairo/backend/api/usage.py +32 -0
- kairo/backend/api/webhooks.py +79 -0
- kairo/backend/app.py +297 -0
- kairo/backend/config.py +179 -0
- kairo/backend/core/__init__.py +0 -0
- kairo/backend/core/admin_auth.py +24 -0
- kairo/backend/core/api_key_auth.py +55 -0
- kairo/backend/core/database.py +28 -0
- kairo/backend/core/dependencies.py +70 -0
- kairo/backend/core/logging.py +23 -0
- kairo/backend/core/rate_limit.py +73 -0
- kairo/backend/core/security.py +29 -0
- kairo/backend/models/__init__.py +19 -0
- kairo/backend/models/agent.py +30 -0
- kairo/backend/models/api_key.py +25 -0
- kairo/backend/models/api_usage.py +29 -0
- kairo/backend/models/audit_log.py +26 -0
- kairo/backend/models/conversation.py +48 -0
- kairo/backend/models/device_code.py +30 -0
- kairo/backend/models/feature_flag.py +21 -0
- kairo/backend/models/image_generation.py +24 -0
- kairo/backend/models/incident.py +28 -0
- kairo/backend/models/project.py +28 -0
- kairo/backend/models/uptime_record.py +24 -0
- kairo/backend/models/usage.py +24 -0
- kairo/backend/models/user.py +49 -0
- kairo/backend/schemas/__init__.py +0 -0
- kairo/backend/schemas/admin/__init__.py +0 -0
- kairo/backend/schemas/admin/audit.py +28 -0
- kairo/backend/schemas/admin/content.py +53 -0
- kairo/backend/schemas/admin/stats.py +77 -0
- kairo/backend/schemas/admin/system.py +44 -0
- kairo/backend/schemas/admin/users.py +48 -0
- kairo/backend/schemas/agent.py +42 -0
- kairo/backend/schemas/api_key.py +30 -0
- kairo/backend/schemas/auth.py +57 -0
- kairo/backend/schemas/chat.py +26 -0
- kairo/backend/schemas/conversation.py +39 -0
- kairo/backend/schemas/device_auth.py +40 -0
- kairo/backend/schemas/image.py +15 -0
- kairo/backend/schemas/openai_compat.py +76 -0
- kairo/backend/schemas/project.py +21 -0
- kairo/backend/schemas/status.py +81 -0
- kairo/backend/schemas/usage.py +15 -0
- kairo/backend/services/__init__.py +0 -0
- kairo/backend/services/admin/__init__.py +0 -0
- kairo/backend/services/admin/audit_service.py +78 -0
- kairo/backend/services/admin/content_service.py +119 -0
- kairo/backend/services/admin/incident_service.py +94 -0
- kairo/backend/services/admin/stats_service.py +281 -0
- kairo/backend/services/admin/system_service.py +126 -0
- kairo/backend/services/admin/user_service.py +157 -0
- kairo/backend/services/agent_service.py +107 -0
- kairo/backend/services/api_key_service.py +66 -0
- kairo/backend/services/api_usage_service.py +126 -0
- kairo/backend/services/auth_service.py +101 -0
- kairo/backend/services/chat_service.py +501 -0
- kairo/backend/services/conversation_service.py +264 -0
- kairo/backend/services/device_auth_service.py +193 -0
- kairo/backend/services/email_service.py +55 -0
- kairo/backend/services/image_service.py +181 -0
- kairo/backend/services/llm_service.py +186 -0
- kairo/backend/services/project_service.py +109 -0
- kairo/backend/services/status_service.py +167 -0
- kairo/backend/services/stripe_service.py +78 -0
- kairo/backend/services/usage_service.py +150 -0
- kairo/backend/services/web_search_service.py +96 -0
- kairo/migrations/env.py +60 -0
- kairo/migrations/versions/001_initial.py +55 -0
- kairo/migrations/versions/002_usage_tracking_and_indexes.py +66 -0
- kairo/migrations/versions/003_username_to_email.py +21 -0
- kairo/migrations/versions/004_add_plans_and_verification.py +67 -0
- kairo/migrations/versions/005_add_projects.py +52 -0
- kairo/migrations/versions/006_add_image_generation.py +63 -0
- kairo/migrations/versions/007_add_admin_portal.py +107 -0
- kairo/migrations/versions/008_add_device_code_auth.py +76 -0
- kairo/migrations/versions/009_add_status_page.py +65 -0
- kairo/tools/extract_claude_data.py +465 -0
- kairo/tools/filter_claude_data.py +303 -0
- kairo/tools/generate_curated_data.py +157 -0
- kairo/tools/mix_training_data.py +295 -0
- kairo_code/__init__.py +3 -0
- kairo_code/agents/__init__.py +25 -0
- kairo_code/agents/architect.py +98 -0
- kairo_code/agents/audit.py +100 -0
- kairo_code/agents/base.py +463 -0
- kairo_code/agents/coder.py +155 -0
- kairo_code/agents/database.py +77 -0
- kairo_code/agents/docs.py +88 -0
- kairo_code/agents/explorer.py +62 -0
- kairo_code/agents/guardian.py +80 -0
- kairo_code/agents/planner.py +66 -0
- kairo_code/agents/reviewer.py +91 -0
- kairo_code/agents/security.py +94 -0
- kairo_code/agents/terraform.py +88 -0
- kairo_code/agents/testing.py +97 -0
- kairo_code/agents/uiux.py +88 -0
- kairo_code/auth.py +232 -0
- kairo_code/config.py +172 -0
- kairo_code/conversation.py +173 -0
- kairo_code/heartbeat.py +63 -0
- kairo_code/llm.py +291 -0
- kairo_code/logging_config.py +156 -0
- kairo_code/main.py +818 -0
- kairo_code/router.py +217 -0
- kairo_code/sandbox.py +248 -0
- kairo_code/settings.py +183 -0
- kairo_code/tools/__init__.py +51 -0
- kairo_code/tools/analysis.py +509 -0
- kairo_code/tools/base.py +417 -0
- kairo_code/tools/code.py +58 -0
- kairo_code/tools/definitions.py +617 -0
- kairo_code/tools/files.py +315 -0
- kairo_code/tools/review.py +390 -0
- kairo_code/tools/search.py +185 -0
- kairo_code/ui.py +418 -0
- kairo_code-0.1.0.dist-info/METADATA +13 -0
- kairo_code-0.1.0.dist-info/RECORD +144 -0
- kairo_code-0.1.0.dist-info/WHEEL +5 -0
- kairo_code-0.1.0.dist-info/entry_points.txt +2 -0
- kairo_code-0.1.0.dist-info/top_level.txt +4 -0
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
3
|
+
from fastapi import APIRouter, Depends, Query
|
|
4
|
+
from sqlalchemy.ext.asyncio import AsyncSession
|
|
5
|
+
|
|
6
|
+
from backend.core.database import get_db
|
|
7
|
+
from backend.schemas.admin.stats import (
|
|
8
|
+
DailyCountResponse,
|
|
9
|
+
DailyTokensResponse,
|
|
10
|
+
OverviewStats,
|
|
11
|
+
PlanDistributionResponse,
|
|
12
|
+
RevenueStats,
|
|
13
|
+
TopUserAnalyticsResponse,
|
|
14
|
+
TopUserEntry,
|
|
15
|
+
UsageStatsEntry,
|
|
16
|
+
UserGrowthEntry,
|
|
17
|
+
)
|
|
18
|
+
from backend.services.admin.stats_service import AdminStatsService
|
|
19
|
+
|
|
20
|
+
logger = logging.getLogger(__name__)
|
|
21
|
+
|
|
22
|
+
router = APIRouter(prefix="/stats", tags=["admin-stats"])
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@router.get("/overview", response_model=OverviewStats)
|
|
26
|
+
async def get_overview(db: AsyncSession = Depends(get_db)):
|
|
27
|
+
svc = AdminStatsService(db)
|
|
28
|
+
data = await svc.get_overview()
|
|
29
|
+
return OverviewStats(**data)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@router.get("/users", response_model=list[UserGrowthEntry])
|
|
33
|
+
async def get_user_growth(
|
|
34
|
+
days: int = Query(30, ge=1, le=365),
|
|
35
|
+
db: AsyncSession = Depends(get_db),
|
|
36
|
+
):
|
|
37
|
+
svc = AdminStatsService(db)
|
|
38
|
+
data = await svc.get_user_growth(days=days)
|
|
39
|
+
return [UserGrowthEntry(**entry) for entry in data]
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@router.get("/usage", response_model=list[UsageStatsEntry])
|
|
43
|
+
async def get_usage_stats(
|
|
44
|
+
days: int = Query(30, ge=1, le=365),
|
|
45
|
+
db: AsyncSession = Depends(get_db),
|
|
46
|
+
):
|
|
47
|
+
svc = AdminStatsService(db)
|
|
48
|
+
data = await svc.get_usage_stats(days=days)
|
|
49
|
+
return [UsageStatsEntry(**entry) for entry in data]
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@router.get("/revenue", response_model=RevenueStats)
|
|
53
|
+
async def get_revenue_stats(db: AsyncSession = Depends(get_db)):
|
|
54
|
+
svc = AdminStatsService(db)
|
|
55
|
+
data = await svc.get_revenue_stats()
|
|
56
|
+
return RevenueStats(**data)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@router.get("/top-users", response_model=list[TopUserEntry])
|
|
60
|
+
async def get_top_users(
|
|
61
|
+
limit: int = Query(20, ge=1, le=100),
|
|
62
|
+
db: AsyncSession = Depends(get_db),
|
|
63
|
+
):
|
|
64
|
+
svc = AdminStatsService(db)
|
|
65
|
+
data = await svc.get_top_users(limit=limit)
|
|
66
|
+
return [TopUserEntry(**entry) for entry in data]
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
# ------------------------------------------------------------------ #
|
|
70
|
+
# Analytics chart / table endpoints #
|
|
71
|
+
# ------------------------------------------------------------------ #
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@router.get("/signups", response_model=DailyCountResponse)
|
|
75
|
+
async def get_signups(
|
|
76
|
+
days: int = Query(30, ge=1, le=365),
|
|
77
|
+
db: AsyncSession = Depends(get_db),
|
|
78
|
+
):
|
|
79
|
+
"""Daily signup counts for the past N days (zero-filled)."""
|
|
80
|
+
svc = AdminStatsService(db)
|
|
81
|
+
data = await svc.get_daily_signups(days=days)
|
|
82
|
+
return DailyCountResponse(data=data)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
@router.get("/active-users", response_model=DailyCountResponse)
|
|
86
|
+
async def get_active_users(
|
|
87
|
+
days: int = Query(30, ge=1, le=365),
|
|
88
|
+
db: AsyncSession = Depends(get_db),
|
|
89
|
+
):
|
|
90
|
+
"""Daily active user counts (users with usage records) for the past N days."""
|
|
91
|
+
svc = AdminStatsService(db)
|
|
92
|
+
data = await svc.get_daily_active_users(days=days)
|
|
93
|
+
return DailyCountResponse(data=data)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
@router.get("/usage-over-time", response_model=DailyTokensResponse)
|
|
97
|
+
async def get_usage_over_time(
|
|
98
|
+
days: int = Query(30, ge=1, le=365),
|
|
99
|
+
db: AsyncSession = Depends(get_db),
|
|
100
|
+
):
|
|
101
|
+
"""Daily total tokens used for the past N days (zero-filled)."""
|
|
102
|
+
svc = AdminStatsService(db)
|
|
103
|
+
data = await svc.get_usage_over_time(days=days)
|
|
104
|
+
return DailyTokensResponse(data=data)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
@router.get("/plan-distribution", response_model=PlanDistributionResponse)
|
|
108
|
+
async def get_plan_distribution(
|
|
109
|
+
db: AsyncSession = Depends(get_db),
|
|
110
|
+
):
|
|
111
|
+
"""Current count of users per plan."""
|
|
112
|
+
svc = AdminStatsService(db)
|
|
113
|
+
data = await svc.get_plan_distribution()
|
|
114
|
+
return PlanDistributionResponse(data=data)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
@router.get("/top-users/analytics", response_model=TopUserAnalyticsResponse)
|
|
118
|
+
async def get_top_users_analytics(
|
|
119
|
+
limit: int = Query(20, ge=1, le=100),
|
|
120
|
+
db: AsyncSession = Depends(get_db),
|
|
121
|
+
):
|
|
122
|
+
"""Top N users by token usage with conversation and image counts."""
|
|
123
|
+
svc = AdminStatsService(db)
|
|
124
|
+
data = await svc.get_top_users_analytics(limit=limit)
|
|
125
|
+
return TopUserAnalyticsResponse(data=data)
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
3
|
+
from fastapi import APIRouter, Depends, HTTPException, Request
|
|
4
|
+
from sqlalchemy.ext.asyncio import AsyncSession
|
|
5
|
+
|
|
6
|
+
from backend.core.admin_auth import require_role
|
|
7
|
+
from backend.core.database import get_db
|
|
8
|
+
from backend.models.user import User
|
|
9
|
+
from backend.schemas.admin.system import (
|
|
10
|
+
ConfigEntry,
|
|
11
|
+
ConfigResponse,
|
|
12
|
+
FeatureFlagItem,
|
|
13
|
+
FeatureFlagResponse,
|
|
14
|
+
FeatureFlagUpdate,
|
|
15
|
+
HealthStatus,
|
|
16
|
+
)
|
|
17
|
+
from backend.services.admin.audit_service import AuditService
|
|
18
|
+
from backend.services.admin.system_service import AdminSystemService
|
|
19
|
+
|
|
20
|
+
logger = logging.getLogger(__name__)
|
|
21
|
+
|
|
22
|
+
router = APIRouter(prefix="/system", tags=["admin-system"])
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@router.get("/health", response_model=HealthStatus)
|
|
26
|
+
async def get_health(db: AsyncSession = Depends(get_db)):
|
|
27
|
+
svc = AdminSystemService(db)
|
|
28
|
+
data = await svc.get_health()
|
|
29
|
+
return HealthStatus(**data)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@router.get("/feature-flags", response_model=FeatureFlagResponse)
|
|
33
|
+
async def get_feature_flags(db: AsyncSession = Depends(get_db)):
|
|
34
|
+
svc = AdminSystemService(db)
|
|
35
|
+
flags = await svc.get_feature_flags()
|
|
36
|
+
items = []
|
|
37
|
+
for f in flags:
|
|
38
|
+
items.append(FeatureFlagItem(
|
|
39
|
+
key=f.key,
|
|
40
|
+
enabled=f.enabled,
|
|
41
|
+
updated_by=f.updated_by,
|
|
42
|
+
updated_at=f.updated_at.isoformat() if f.updated_at else None,
|
|
43
|
+
))
|
|
44
|
+
return FeatureFlagResponse(flags=items)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@router.patch(
|
|
48
|
+
"/feature-flags/{key}",
|
|
49
|
+
dependencies=[Depends(require_role("superadmin"))],
|
|
50
|
+
)
|
|
51
|
+
async def toggle_feature_flag(
|
|
52
|
+
key: str,
|
|
53
|
+
body: FeatureFlagUpdate,
|
|
54
|
+
request: Request,
|
|
55
|
+
db: AsyncSession = Depends(get_db),
|
|
56
|
+
admin: User = Depends(require_role("superadmin")),
|
|
57
|
+
):
|
|
58
|
+
svc = AdminSystemService(db)
|
|
59
|
+
flag = await svc.toggle_feature_flag(key, body.enabled, admin.id)
|
|
60
|
+
if not flag:
|
|
61
|
+
raise HTTPException(status_code=404, detail="Feature flag not found")
|
|
62
|
+
|
|
63
|
+
audit = AuditService(db)
|
|
64
|
+
ip = request.client.host if request.client else "unknown"
|
|
65
|
+
ua = request.headers.get("user-agent", "")
|
|
66
|
+
await audit.log(
|
|
67
|
+
admin_user_id=admin.id,
|
|
68
|
+
action="system.feature_flag_toggled",
|
|
69
|
+
target_type="feature_flag",
|
|
70
|
+
target_id=key,
|
|
71
|
+
details={"enabled": body.enabled, "reason": body.reason},
|
|
72
|
+
ip_address=ip,
|
|
73
|
+
user_agent=ua,
|
|
74
|
+
)
|
|
75
|
+
return FeatureFlagItem(
|
|
76
|
+
key=flag.key,
|
|
77
|
+
enabled=flag.enabled,
|
|
78
|
+
updated_by=flag.updated_by,
|
|
79
|
+
updated_at=flag.updated_at.isoformat() if flag.updated_at else None,
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
@router.get("/config", response_model=ConfigResponse)
|
|
84
|
+
async def get_config(db: AsyncSession = Depends(get_db)):
|
|
85
|
+
svc = AdminSystemService(db)
|
|
86
|
+
entries = await svc.get_config()
|
|
87
|
+
return ConfigResponse(config=[ConfigEntry(**e) for e in entries])
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
3
|
+
from fastapi import APIRouter, Depends, HTTPException, Query, Request
|
|
4
|
+
from sqlalchemy.ext.asyncio import AsyncSession
|
|
5
|
+
|
|
6
|
+
from backend.core.admin_auth import require_role
|
|
7
|
+
from backend.core.database import get_db
|
|
8
|
+
from backend.models.user import User
|
|
9
|
+
from backend.schemas.admin.users import (
|
|
10
|
+
AdminUserDetailResponse,
|
|
11
|
+
AdminUserListItem,
|
|
12
|
+
PlanChangeRequest,
|
|
13
|
+
RoleChangeRequest,
|
|
14
|
+
StatusChangeRequest,
|
|
15
|
+
)
|
|
16
|
+
from backend.services.admin.audit_service import AuditService
|
|
17
|
+
from backend.services.admin.user_service import AdminUserService
|
|
18
|
+
|
|
19
|
+
logger = logging.getLogger(__name__)
|
|
20
|
+
|
|
21
|
+
router = APIRouter(prefix="/users", tags=["admin-users"])
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _get_ip(request: Request) -> str:
|
|
25
|
+
return request.client.host if request.client else "unknown"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _get_ua(request: Request) -> str:
|
|
29
|
+
return request.headers.get("user-agent", "")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@router.get("")
|
|
33
|
+
async def list_users(
|
|
34
|
+
search: str | None = Query(None),
|
|
35
|
+
plan: str | None = Query(None),
|
|
36
|
+
status: str | None = Query(None),
|
|
37
|
+
cursor: str | None = Query(None),
|
|
38
|
+
limit: int = Query(25, ge=1, le=100),
|
|
39
|
+
db: AsyncSession = Depends(get_db),
|
|
40
|
+
):
|
|
41
|
+
svc = AdminUserService(db)
|
|
42
|
+
users = await svc.list_users(search=search, plan=plan, status=status, cursor=cursor, limit=limit + 1)
|
|
43
|
+
has_more = len(users) > limit
|
|
44
|
+
if has_more:
|
|
45
|
+
users = users[:limit]
|
|
46
|
+
items = [AdminUserListItem.model_validate(u) for u in users]
|
|
47
|
+
next_cursor = users[-1].id if has_more else None
|
|
48
|
+
return {"items": items, "next_cursor": next_cursor}
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@router.get("/{user_id}", response_model=AdminUserDetailResponse)
|
|
52
|
+
async def get_user_detail(
|
|
53
|
+
user_id: str,
|
|
54
|
+
request: Request,
|
|
55
|
+
db: AsyncSession = Depends(get_db),
|
|
56
|
+
admin: User = Depends(require_role("admin")),
|
|
57
|
+
):
|
|
58
|
+
svc = AdminUserService(db)
|
|
59
|
+
user = await svc.get_user_detail(user_id)
|
|
60
|
+
if not user:
|
|
61
|
+
raise HTTPException(status_code=404, detail="User not found")
|
|
62
|
+
|
|
63
|
+
# Audit the read
|
|
64
|
+
audit = AuditService(db)
|
|
65
|
+
await audit.log(
|
|
66
|
+
admin_user_id=admin.id,
|
|
67
|
+
action="user.detail_viewed",
|
|
68
|
+
target_type="user",
|
|
69
|
+
target_id=user_id,
|
|
70
|
+
ip_address=_get_ip(request),
|
|
71
|
+
user_agent=_get_ua(request),
|
|
72
|
+
)
|
|
73
|
+
return AdminUserDetailResponse.model_validate(user)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
@router.patch("/{user_id}/plan", response_model=AdminUserDetailResponse)
|
|
77
|
+
async def change_user_plan(
|
|
78
|
+
user_id: str,
|
|
79
|
+
body: PlanChangeRequest,
|
|
80
|
+
request: Request,
|
|
81
|
+
db: AsyncSession = Depends(get_db),
|
|
82
|
+
admin: User = Depends(require_role("admin")),
|
|
83
|
+
):
|
|
84
|
+
svc = AdminUserService(db)
|
|
85
|
+
old_user = await svc.get_user_detail(user_id)
|
|
86
|
+
if not old_user:
|
|
87
|
+
raise HTTPException(status_code=404, detail="User not found")
|
|
88
|
+
|
|
89
|
+
old_plan = old_user.plan
|
|
90
|
+
user = await svc.change_plan(user_id, body.plan)
|
|
91
|
+
|
|
92
|
+
audit = AuditService(db)
|
|
93
|
+
await audit.log(
|
|
94
|
+
admin_user_id=admin.id,
|
|
95
|
+
action="user.plan_changed",
|
|
96
|
+
target_type="user",
|
|
97
|
+
target_id=user_id,
|
|
98
|
+
details={"old_plan": old_plan, "new_plan": body.plan, "reason": body.reason},
|
|
99
|
+
ip_address=_get_ip(request),
|
|
100
|
+
user_agent=_get_ua(request),
|
|
101
|
+
)
|
|
102
|
+
return AdminUserDetailResponse.model_validate(user)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
@router.patch("/{user_id}/status", response_model=AdminUserDetailResponse)
|
|
106
|
+
async def change_user_status(
|
|
107
|
+
user_id: str,
|
|
108
|
+
body: StatusChangeRequest,
|
|
109
|
+
request: Request,
|
|
110
|
+
db: AsyncSession = Depends(get_db),
|
|
111
|
+
admin: User = Depends(require_role("admin")),
|
|
112
|
+
):
|
|
113
|
+
svc = AdminUserService(db)
|
|
114
|
+
old_user = await svc.get_user_detail(user_id)
|
|
115
|
+
if not old_user:
|
|
116
|
+
raise HTTPException(status_code=404, detail="User not found")
|
|
117
|
+
|
|
118
|
+
old_status = getattr(old_user, "status", "active")
|
|
119
|
+
user = await svc.change_status(user_id, body.status)
|
|
120
|
+
|
|
121
|
+
audit = AuditService(db)
|
|
122
|
+
await audit.log(
|
|
123
|
+
admin_user_id=admin.id,
|
|
124
|
+
action="user.status_changed",
|
|
125
|
+
target_type="user",
|
|
126
|
+
target_id=user_id,
|
|
127
|
+
details={"old_status": old_status, "new_status": body.status, "reason": body.reason},
|
|
128
|
+
ip_address=_get_ip(request),
|
|
129
|
+
user_agent=_get_ua(request),
|
|
130
|
+
)
|
|
131
|
+
return AdminUserDetailResponse.model_validate(user)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
@router.patch(
|
|
135
|
+
"/{user_id}/role",
|
|
136
|
+
response_model=AdminUserDetailResponse,
|
|
137
|
+
dependencies=[Depends(require_role("superadmin"))],
|
|
138
|
+
)
|
|
139
|
+
async def change_user_role(
|
|
140
|
+
user_id: str,
|
|
141
|
+
body: RoleChangeRequest,
|
|
142
|
+
request: Request,
|
|
143
|
+
db: AsyncSession = Depends(get_db),
|
|
144
|
+
admin: User = Depends(require_role("superadmin")),
|
|
145
|
+
):
|
|
146
|
+
svc = AdminUserService(db)
|
|
147
|
+
old_user = await svc.get_user_detail(user_id)
|
|
148
|
+
if not old_user:
|
|
149
|
+
raise HTTPException(status_code=404, detail="User not found")
|
|
150
|
+
|
|
151
|
+
old_role = getattr(old_user, "role", "user")
|
|
152
|
+
user = await svc.change_role(user_id, body.role)
|
|
153
|
+
|
|
154
|
+
audit = AuditService(db)
|
|
155
|
+
await audit.log(
|
|
156
|
+
admin_user_id=admin.id,
|
|
157
|
+
action="user.role_changed",
|
|
158
|
+
target_type="user",
|
|
159
|
+
target_id=user_id,
|
|
160
|
+
details={"old_role": old_role, "new_role": body.role, "reason": body.reason},
|
|
161
|
+
ip_address=_get_ip(request),
|
|
162
|
+
user_agent=_get_ua(request),
|
|
163
|
+
)
|
|
164
|
+
return AdminUserDetailResponse.model_validate(user)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
@router.get("/{user_id}/usage")
|
|
168
|
+
async def get_user_usage(
|
|
169
|
+
user_id: str,
|
|
170
|
+
request: Request,
|
|
171
|
+
db: AsyncSession = Depends(get_db),
|
|
172
|
+
admin: User = Depends(require_role("admin")),
|
|
173
|
+
):
|
|
174
|
+
svc = AdminUserService(db)
|
|
175
|
+
user = await svc.get_user_detail(user_id)
|
|
176
|
+
if not user:
|
|
177
|
+
raise HTTPException(status_code=404, detail="User not found")
|
|
178
|
+
|
|
179
|
+
audit = AuditService(db)
|
|
180
|
+
await audit.log(
|
|
181
|
+
admin_user_id=admin.id,
|
|
182
|
+
action="user.usage_viewed",
|
|
183
|
+
target_type="user",
|
|
184
|
+
target_id=user_id,
|
|
185
|
+
ip_address=_get_ip(request),
|
|
186
|
+
user_agent=_get_ua(request),
|
|
187
|
+
)
|
|
188
|
+
return await svc.get_user_usage(user_id)
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
@router.get("/{user_id}/conversations")
|
|
192
|
+
async def get_user_conversations(
|
|
193
|
+
user_id: str,
|
|
194
|
+
request: Request,
|
|
195
|
+
cursor: str | None = Query(None),
|
|
196
|
+
limit: int = Query(50, ge=1, le=100),
|
|
197
|
+
db: AsyncSession = Depends(get_db),
|
|
198
|
+
admin: User = Depends(require_role("admin")),
|
|
199
|
+
):
|
|
200
|
+
svc = AdminUserService(db)
|
|
201
|
+
user = await svc.get_user_detail(user_id)
|
|
202
|
+
if not user:
|
|
203
|
+
raise HTTPException(status_code=404, detail="User not found")
|
|
204
|
+
|
|
205
|
+
audit = AuditService(db)
|
|
206
|
+
await audit.log(
|
|
207
|
+
admin_user_id=admin.id,
|
|
208
|
+
action="user.conversations_viewed",
|
|
209
|
+
target_type="user",
|
|
210
|
+
target_id=user_id,
|
|
211
|
+
ip_address=_get_ip(request),
|
|
212
|
+
user_agent=_get_ua(request),
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
conversations = await svc.get_user_conversations(user_id, cursor=cursor, limit=limit)
|
|
216
|
+
return [
|
|
217
|
+
{
|
|
218
|
+
"id": c.id,
|
|
219
|
+
"title": c.title,
|
|
220
|
+
"model": c.model,
|
|
221
|
+
"created_at": c.created_at,
|
|
222
|
+
"updated_at": c.updated_at,
|
|
223
|
+
}
|
|
224
|
+
for c in conversations
|
|
225
|
+
]
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
@router.get("/{user_id}/images")
|
|
229
|
+
async def get_user_images(
|
|
230
|
+
user_id: str,
|
|
231
|
+
request: Request,
|
|
232
|
+
cursor: str | None = Query(None),
|
|
233
|
+
limit: int = Query(50, ge=1, le=100),
|
|
234
|
+
db: AsyncSession = Depends(get_db),
|
|
235
|
+
admin: User = Depends(require_role("admin")),
|
|
236
|
+
):
|
|
237
|
+
svc = AdminUserService(db)
|
|
238
|
+
user = await svc.get_user_detail(user_id)
|
|
239
|
+
if not user:
|
|
240
|
+
raise HTTPException(status_code=404, detail="User not found")
|
|
241
|
+
|
|
242
|
+
audit = AuditService(db)
|
|
243
|
+
await audit.log(
|
|
244
|
+
admin_user_id=admin.id,
|
|
245
|
+
action="user.images_viewed",
|
|
246
|
+
target_type="user",
|
|
247
|
+
target_id=user_id,
|
|
248
|
+
ip_address=_get_ip(request),
|
|
249
|
+
user_agent=_get_ua(request),
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
from backend.schemas.admin.content import AdminImageListItem
|
|
253
|
+
|
|
254
|
+
images = await svc.get_user_images(user_id, cursor=cursor, limit=limit)
|
|
255
|
+
return [AdminImageListItem.model_validate(img) for img in images]
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
@router.get("/{user_id}/api-keys")
|
|
259
|
+
async def get_user_api_keys(
|
|
260
|
+
user_id: str,
|
|
261
|
+
request: Request,
|
|
262
|
+
db: AsyncSession = Depends(get_db),
|
|
263
|
+
admin: User = Depends(require_role("admin")),
|
|
264
|
+
):
|
|
265
|
+
svc = AdminUserService(db)
|
|
266
|
+
user = await svc.get_user_detail(user_id)
|
|
267
|
+
if not user:
|
|
268
|
+
raise HTTPException(status_code=404, detail="User not found")
|
|
269
|
+
|
|
270
|
+
audit = AuditService(db)
|
|
271
|
+
await audit.log(
|
|
272
|
+
admin_user_id=admin.id,
|
|
273
|
+
action="user.api_keys_viewed",
|
|
274
|
+
target_type="user",
|
|
275
|
+
target_id=user_id,
|
|
276
|
+
ip_address=_get_ip(request),
|
|
277
|
+
user_agent=_get_ua(request),
|
|
278
|
+
)
|
|
279
|
+
return await svc.get_user_api_keys(user_id)
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
from datetime import datetime, UTC
|
|
2
|
+
|
|
3
|
+
from fastapi import APIRouter, Depends, HTTPException
|
|
4
|
+
from sqlalchemy.ext.asyncio import AsyncSession
|
|
5
|
+
|
|
6
|
+
from backend.config import settings
|
|
7
|
+
from backend.core.api_key_auth import get_api_key_user
|
|
8
|
+
from backend.core.database import get_db
|
|
9
|
+
from backend.core.dependencies import get_current_user
|
|
10
|
+
from backend.models.api_key import ApiKey
|
|
11
|
+
from backend.models.user import User
|
|
12
|
+
from backend.schemas.agent import (
|
|
13
|
+
AgentHeartbeatRequest,
|
|
14
|
+
AgentHeartbeatResponse,
|
|
15
|
+
AgentResponse,
|
|
16
|
+
RegisterAgentRequest,
|
|
17
|
+
UpdateAgentRequest,
|
|
18
|
+
)
|
|
19
|
+
from backend.services.agent_service import AgentService
|
|
20
|
+
|
|
21
|
+
router = APIRouter(prefix="/agents", tags=["Agents"])
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@router.post("/", response_model=AgentResponse)
|
|
25
|
+
async def register_agent(
|
|
26
|
+
req: RegisterAgentRequest,
|
|
27
|
+
user: User = Depends(get_current_user),
|
|
28
|
+
db: AsyncSession = Depends(get_db),
|
|
29
|
+
):
|
|
30
|
+
if not settings.FEATURE_KAIRO_AGENTS_ENABLED:
|
|
31
|
+
raise HTTPException(status_code=503, detail="Kairo Agents is coming soon.")
|
|
32
|
+
svc = AgentService(db)
|
|
33
|
+
agent = await svc.register(user.id, req)
|
|
34
|
+
return agent
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@router.get("/", response_model=list[AgentResponse])
|
|
38
|
+
async def list_agents(
|
|
39
|
+
user: User = Depends(get_current_user),
|
|
40
|
+
db: AsyncSession = Depends(get_db),
|
|
41
|
+
):
|
|
42
|
+
if not settings.FEATURE_KAIRO_AGENTS_ENABLED:
|
|
43
|
+
raise HTTPException(status_code=503, detail="Kairo Agents is coming soon.")
|
|
44
|
+
svc = AgentService(db)
|
|
45
|
+
agents = await svc.list_agents(user.id)
|
|
46
|
+
return agents
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@router.patch("/{agent_id}", response_model=AgentResponse)
|
|
50
|
+
async def update_agent(
|
|
51
|
+
agent_id: str,
|
|
52
|
+
req: UpdateAgentRequest,
|
|
53
|
+
user: User = Depends(get_current_user),
|
|
54
|
+
db: AsyncSession = Depends(get_db),
|
|
55
|
+
):
|
|
56
|
+
if not settings.FEATURE_KAIRO_AGENTS_ENABLED:
|
|
57
|
+
raise HTTPException(status_code=503, detail="Kairo Agents is coming soon.")
|
|
58
|
+
svc = AgentService(db)
|
|
59
|
+
agent = await svc.update_agent(user.id, agent_id, req)
|
|
60
|
+
if not agent:
|
|
61
|
+
raise HTTPException(status_code=404, detail="Agent not found")
|
|
62
|
+
return agent
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@router.delete("/{agent_id}")
|
|
66
|
+
async def delete_agent(
|
|
67
|
+
agent_id: str,
|
|
68
|
+
user: User = Depends(get_current_user),
|
|
69
|
+
db: AsyncSession = Depends(get_db),
|
|
70
|
+
):
|
|
71
|
+
if not settings.FEATURE_KAIRO_AGENTS_ENABLED:
|
|
72
|
+
raise HTTPException(status_code=503, detail="Kairo Agents is coming soon.")
|
|
73
|
+
svc = AgentService(db)
|
|
74
|
+
deleted = await svc.delete_agent(user.id, agent_id)
|
|
75
|
+
if not deleted:
|
|
76
|
+
raise HTTPException(status_code=404, detail="Agent not found")
|
|
77
|
+
return {"deleted": True}
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
@router.post("/heartbeat", response_model=AgentHeartbeatResponse)
|
|
81
|
+
async def agent_heartbeat(
|
|
82
|
+
req: AgentHeartbeatRequest,
|
|
83
|
+
auth: tuple[User, ApiKey] = Depends(get_api_key_user),
|
|
84
|
+
db: AsyncSession = Depends(get_db),
|
|
85
|
+
):
|
|
86
|
+
"""Agent heartbeat — authenticated via API key, not JWT."""
|
|
87
|
+
if not settings.FEATURE_KAIRO_AGENTS_ENABLED:
|
|
88
|
+
raise HTTPException(status_code=503, detail="Kairo Agents is coming soon.")
|
|
89
|
+
user, _api_key = auth
|
|
90
|
+
svc = AgentService(db)
|
|
91
|
+
agent = await svc.heartbeat(req.agent_id, user.id, req.status)
|
|
92
|
+
if not agent:
|
|
93
|
+
raise HTTPException(status_code=404, detail="Agent not found or not owned by this key's user")
|
|
94
|
+
return AgentHeartbeatResponse(acknowledged=True, server_time=datetime.now(UTC))
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
from fastapi import APIRouter, Depends, HTTPException
|
|
2
|
+
from sqlalchemy.ext.asyncio import AsyncSession
|
|
3
|
+
|
|
4
|
+
from backend.config import settings
|
|
5
|
+
from backend.core.database import get_db
|
|
6
|
+
from backend.core.dependencies import get_current_user
|
|
7
|
+
from backend.models.user import User
|
|
8
|
+
from backend.schemas.api_key import (
|
|
9
|
+
ApiKeyCreatedResponse,
|
|
10
|
+
ApiKeyResponse,
|
|
11
|
+
CreateApiKeyRequest,
|
|
12
|
+
RevokeApiKeyResponse,
|
|
13
|
+
)
|
|
14
|
+
from backend.services.api_key_service import ApiKeyService
|
|
15
|
+
from backend.services.api_usage_service import ApiUsageService
|
|
16
|
+
|
|
17
|
+
router = APIRouter(prefix="/api-keys", tags=["API Keys"])
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@router.post("/", response_model=ApiKeyCreatedResponse)
|
|
21
|
+
async def create_api_key(
|
|
22
|
+
req: CreateApiKeyRequest,
|
|
23
|
+
user: User = Depends(get_current_user),
|
|
24
|
+
db: AsyncSession = Depends(get_db),
|
|
25
|
+
):
|
|
26
|
+
if not settings.FEATURE_KAIRO_API_ENABLED:
|
|
27
|
+
raise HTTPException(status_code=503, detail="Kairo API is coming soon.")
|
|
28
|
+
svc = ApiKeyService(db)
|
|
29
|
+
api_key, raw_key = await svc.create_key(user.id, req.name, req.expires_in_days)
|
|
30
|
+
return ApiKeyCreatedResponse(
|
|
31
|
+
id=api_key.id,
|
|
32
|
+
name=api_key.name,
|
|
33
|
+
key_prefix=api_key.key_prefix,
|
|
34
|
+
is_active=api_key.is_active,
|
|
35
|
+
last_used_at=api_key.last_used_at,
|
|
36
|
+
created_at=api_key.created_at,
|
|
37
|
+
expires_at=api_key.expires_at,
|
|
38
|
+
full_key=raw_key,
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@router.get("/", response_model=list[ApiKeyResponse])
|
|
43
|
+
async def list_api_keys(
|
|
44
|
+
user: User = Depends(get_current_user),
|
|
45
|
+
db: AsyncSession = Depends(get_db),
|
|
46
|
+
):
|
|
47
|
+
if not settings.FEATURE_KAIRO_API_ENABLED:
|
|
48
|
+
raise HTTPException(status_code=503, detail="Kairo API is coming soon.")
|
|
49
|
+
svc = ApiKeyService(db)
|
|
50
|
+
keys = await svc.list_keys(user.id)
|
|
51
|
+
return keys
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@router.delete("/{key_id}", response_model=RevokeApiKeyResponse)
|
|
55
|
+
async def revoke_api_key(
|
|
56
|
+
key_id: str,
|
|
57
|
+
user: User = Depends(get_current_user),
|
|
58
|
+
db: AsyncSession = Depends(get_db),
|
|
59
|
+
):
|
|
60
|
+
if not settings.FEATURE_KAIRO_API_ENABLED:
|
|
61
|
+
raise HTTPException(status_code=503, detail="Kairo API is coming soon.")
|
|
62
|
+
svc = ApiKeyService(db)
|
|
63
|
+
revoked = await svc.revoke_key(user.id, key_id)
|
|
64
|
+
if not revoked:
|
|
65
|
+
raise HTTPException(status_code=404, detail="API key not found")
|
|
66
|
+
return RevokeApiKeyResponse(id=key_id, revoked=True)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@router.get("/{key_id}/usage")
|
|
70
|
+
async def get_key_usage(
|
|
71
|
+
key_id: str,
|
|
72
|
+
days: int = 30,
|
|
73
|
+
user: User = Depends(get_current_user),
|
|
74
|
+
db: AsyncSession = Depends(get_db),
|
|
75
|
+
):
|
|
76
|
+
if not settings.FEATURE_KAIRO_API_ENABLED:
|
|
77
|
+
raise HTTPException(status_code=503, detail="Kairo API is coming soon.")
|
|
78
|
+
# Verify key belongs to user
|
|
79
|
+
key_svc = ApiKeyService(db)
|
|
80
|
+
keys = await key_svc.list_keys(user.id)
|
|
81
|
+
if not any(k.id == key_id for k in keys):
|
|
82
|
+
raise HTTPException(status_code=404, detail="API key not found")
|
|
83
|
+
usage_svc = ApiUsageService(db)
|
|
84
|
+
history = await usage_svc.get_key_usage_summary(key_id, days)
|
|
85
|
+
return {"usage": history}
|