Compare commits
No commits in common. "587dc06e8cbd9228fa27b253186e82dc21ef6508" and "954628a3b999408b822d5a75fbf508d9bd63ca57" have entirely different histories.
587dc06e8c
...
954628a3b9
@ -80,7 +80,6 @@ admin:
|
||||
COOKIE_SECURE: false
|
||||
COOKIE_SAME_SITE: lax
|
||||
APP_NEXTAUTH_SECRET: ""
|
||||
ADMIN_RPC_URL: adminrpc.gitdataai.svc.cluster.local:3001
|
||||
|
||||
|
||||
nodeSelector: { }
|
||||
|
||||
18
admin/package-lock.json
generated
18
admin/package-lock.json
generated
@ -8,8 +8,6 @@
|
||||
"name": "admin",
|
||||
"version": "0.1.0",
|
||||
"dependencies": {
|
||||
"@bufbuild/connect": "^0.13.0",
|
||||
"@bufbuild/protobuf": "^2.11.0",
|
||||
"@types/node-cron": "^3.0.11",
|
||||
"argon2": "^0.44.0",
|
||||
"bcrypt": "^5.1.1",
|
||||
@ -274,22 +272,6 @@
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@bufbuild/connect": {
|
||||
"version": "0.13.0",
|
||||
"resolved": "https://registry.npmmirror.com/@bufbuild/connect/-/connect-0.13.0.tgz",
|
||||
"integrity": "sha512-eZSMbVLyUFtXiZNORgCEvv580xKZeYQdMOWj2i/nxOcpXQcrEzTMTA7SZzWv4k4gveWCOSRoWmYDeOhfWXJv0g==",
|
||||
"deprecated": "Connect has moved to its own org @connectrpc and has a stable v1. Run `npx @connectrpc/connect-migrate@latest` to update. See https://github.com/connectrpc/connect-es/releases/tag/v0.13.1 for details.",
|
||||
"license": "Apache-2.0",
|
||||
"peerDependencies": {
|
||||
"@bufbuild/protobuf": "^1.2.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@bufbuild/protobuf": {
|
||||
"version": "2.11.0",
|
||||
"resolved": "https://registry.npmmirror.com/@bufbuild/protobuf/-/protobuf-2.11.0.tgz",
|
||||
"integrity": "sha512-sBXGT13cpmPR5BMgHE6UEEfEaShh5Ror6rfN3yEK5si7QVrtZg8LEPQb0VVhiLRUslD2yLnXtnRzG035J/mZXQ==",
|
||||
"license": "(Apache-2.0 AND BSD-3-Clause)"
|
||||
},
|
||||
"node_modules/@emnapi/core": {
|
||||
"version": "1.10.0",
|
||||
"dev": true,
|
||||
|
||||
@ -13,8 +13,6 @@
|
||||
"test:ui": "playwright test --ui"
|
||||
},
|
||||
"dependencies": {
|
||||
"@bufbuild/connect": "^0.13.0",
|
||||
"@bufbuild/protobuf": "^2.11.0",
|
||||
"@types/node-cron": "^3.0.11",
|
||||
"argon2": "^0.44.0",
|
||||
"bcrypt": "^5.1.1",
|
||||
|
||||
@ -18,7 +18,6 @@ interface AiConfig {
|
||||
ai_model?: string;
|
||||
ai_api_key?: string;
|
||||
ai_enabled?: string;
|
||||
basic_api_url?: string;
|
||||
smtp_host?: string;
|
||||
smtp_port?: string;
|
||||
smtp_username?: string;
|
||||
@ -319,15 +318,6 @@ export default function DailyReportPage() {
|
||||
onChange={e => setAiForm(f => ({ ...f, ai_api_key: e.target.value }))}
|
||||
placeholder="sk-..." />
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label className="form-label">自定义 API 地址(可选)</label>
|
||||
<input className="form-input" value={aiForm.basic_api_url || ""}
|
||||
onChange={e => setAiForm(f => ({ ...f, basic_api_url: e.target.value }))}
|
||||
placeholder="https://api.openai.com(留空使用默认地址)" />
|
||||
<span style={{ fontSize: "12px", color: "#737373" }}>
|
||||
支持 OpenAI 兼容接口,如 Cloudflare AI Gateway、OneAPI 等
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* SMTP Settings */}
|
||||
|
||||
@ -1,48 +1,40 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { createModel, updateModel, deleteModel } from "@/lib/adminrpc/client";
|
||||
import { RUST_BACKEND_URL, ADMIN_API_SHARED_KEY } from "@/lib/env";
|
||||
|
||||
export const runtime = "nodejs";
|
||||
|
||||
async function adminFetch(path: string, method: string, body?: unknown) {
|
||||
if (!ADMIN_API_SHARED_KEY) {
|
||||
return NextResponse.json({ error: "ADMIN_API_SHARED_KEY 未配置" }, { status: 500 });
|
||||
}
|
||||
const res = await fetch(`${RUST_BACKEND_URL}${path}`, {
|
||||
method,
|
||||
headers: { "Content-Type": "application/json", "x-admin-api-key": ADMIN_API_SHARED_KEY },
|
||||
body: body ? JSON.stringify(body) : undefined,
|
||||
signal: AbortSignal.timeout(30_000),
|
||||
});
|
||||
const data = await res.json();
|
||||
if (!res.ok) return NextResponse.json(data, { status: res.status });
|
||||
return NextResponse.json(data);
|
||||
}
|
||||
|
||||
// POST /api/admin/ai/models — create model
|
||||
export async function POST(req: NextRequest) {
|
||||
try {
|
||||
const body = await req.json();
|
||||
const data = await createModel(body);
|
||||
return NextResponse.json(data);
|
||||
} catch (e) {
|
||||
const msg = e instanceof Error ? e.message : String(e);
|
||||
console.error("Create model error:", msg);
|
||||
return NextResponse.json({ error: `创建失败: ${msg}` }, { status: 500 });
|
||||
}
|
||||
return adminFetch("/api/admin/ai/models", "POST", await req.json());
|
||||
}
|
||||
|
||||
// PATCH /api/admin/ai/models?id={id} — update model
|
||||
export async function PATCH(req: NextRequest) {
|
||||
try {
|
||||
const { searchParams } = new URL(req.url);
|
||||
const id = searchParams.get("id");
|
||||
if (!id) return NextResponse.json({ error: "Missing id" }, { status: 400 });
|
||||
const body = await req.json();
|
||||
const data = await updateModel(id, body);
|
||||
return NextResponse.json(data);
|
||||
} catch (e) {
|
||||
const msg = e instanceof Error ? e.message : String(e);
|
||||
console.error("Update model error:", msg);
|
||||
return NextResponse.json({ error: `更新失败: ${msg}` }, { status: 500 });
|
||||
}
|
||||
const { searchParams } = new URL(req.url);
|
||||
const id = searchParams.get("id");
|
||||
if (!id) return NextResponse.json({ error: "Missing id" }, { status: 400 });
|
||||
return adminFetch(`/api/admin/ai/models/${id}`, "PATCH", await req.json());
|
||||
}
|
||||
|
||||
// DELETE /api/admin/ai/models?id={id} — delete model
|
||||
export async function DELETE(req: NextRequest) {
|
||||
try {
|
||||
const { searchParams } = new URL(req.url);
|
||||
const id = searchParams.get("id");
|
||||
if (!id) return NextResponse.json({ error: "Missing id" }, { status: 400 });
|
||||
const data = await deleteModel(id);
|
||||
return NextResponse.json(data);
|
||||
} catch (e) {
|
||||
const msg = e instanceof Error ? e.message : String(e);
|
||||
console.error("Delete model error:", msg);
|
||||
return NextResponse.json({ error: `删除失败: ${msg}` }, { status: 500 });
|
||||
}
|
||||
const { searchParams } = new URL(req.url);
|
||||
const id = searchParams.get("id");
|
||||
if (!id) return NextResponse.json({ error: "Missing id" }, { status: 400 });
|
||||
return adminFetch(`/api/admin/ai/models/${id}`, "DELETE");
|
||||
}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { updatePricing } from "@/lib/adminrpc/client";
|
||||
import { RUST_BACKEND_URL, ADMIN_API_SHARED_KEY } from "@/lib/env";
|
||||
|
||||
export const runtime = "nodejs";
|
||||
|
||||
@ -8,14 +8,18 @@ export async function PATCH(
|
||||
req: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
try {
|
||||
const { id } = await params;
|
||||
const body = await req.json();
|
||||
const data = await updatePricing(id, body);
|
||||
return NextResponse.json(data);
|
||||
} catch (e) {
|
||||
const msg = e instanceof Error ? e.message : String(e);
|
||||
console.error("Update pricing error:", msg);
|
||||
return NextResponse.json({ error: `更新失败: ${msg}` }, { status: 500 });
|
||||
const { id } = await params;
|
||||
if (!ADMIN_API_SHARED_KEY) {
|
||||
return NextResponse.json({ error: "ADMIN_API_SHARED_KEY 未配置" }, { status: 500 });
|
||||
}
|
||||
const body = await req.json();
|
||||
const res = await fetch(`${RUST_BACKEND_URL}/api/admin/ai/pricing/${id}`, {
|
||||
method: "PATCH",
|
||||
headers: { "Content-Type": "application/json", "x-admin-api-key": ADMIN_API_SHARED_KEY },
|
||||
body: JSON.stringify(body),
|
||||
signal: AbortSignal.timeout(30_000),
|
||||
});
|
||||
const data = await res.json();
|
||||
if (!res.ok) return NextResponse.json(data, { status: res.status });
|
||||
return NextResponse.json(data);
|
||||
}
|
||||
|
||||
@ -1,48 +1,40 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { createProvider, updateProvider, deleteProvider } from "@/lib/adminrpc/client";
|
||||
import { RUST_BACKEND_URL, ADMIN_API_SHARED_KEY } from "@/lib/env";
|
||||
|
||||
export const runtime = "nodejs";
|
||||
|
||||
async function adminFetch(path: string, method: string, body?: unknown) {
|
||||
if (!ADMIN_API_SHARED_KEY) {
|
||||
return NextResponse.json({ error: "ADMIN_API_SHARED_KEY 未配置" }, { status: 500 });
|
||||
}
|
||||
const res = await fetch(`${RUST_BACKEND_URL}${path}`, {
|
||||
method,
|
||||
headers: { "Content-Type": "application/json", "x-admin-api-key": ADMIN_API_SHARED_KEY },
|
||||
body: body ? JSON.stringify(body) : undefined,
|
||||
signal: AbortSignal.timeout(30_000),
|
||||
});
|
||||
const data = await res.json();
|
||||
if (!res.ok) return NextResponse.json(data, { status: res.status });
|
||||
return NextResponse.json(data);
|
||||
}
|
||||
|
||||
// POST /api/admin/ai/providers — create provider
|
||||
export async function POST(req: NextRequest) {
|
||||
try {
|
||||
const body = await req.json();
|
||||
const data = await createProvider(body);
|
||||
return NextResponse.json(data);
|
||||
} catch (e) {
|
||||
const msg = e instanceof Error ? e.message : String(e);
|
||||
console.error("Create provider error:", msg);
|
||||
return NextResponse.json({ error: `创建失败: ${msg}` }, { status: 500 });
|
||||
}
|
||||
return adminFetch("/api/admin/ai/providers", "POST", await req.json());
|
||||
}
|
||||
|
||||
// PATCH /api/admin/ai/providers?id={id} — update provider
|
||||
export async function PATCH(req: NextRequest) {
|
||||
try {
|
||||
const { searchParams } = new URL(req.url);
|
||||
const id = searchParams.get("id");
|
||||
if (!id) return NextResponse.json({ error: "Missing id" }, { status: 400 });
|
||||
const body = await req.json();
|
||||
const data = await updateProvider(id, body);
|
||||
return NextResponse.json(data);
|
||||
} catch (e) {
|
||||
const msg = e instanceof Error ? e.message : String(e);
|
||||
console.error("Update provider error:", msg);
|
||||
return NextResponse.json({ error: `更新失败: ${msg}` }, { status: 500 });
|
||||
}
|
||||
const { searchParams } = new URL(req.url);
|
||||
const id = searchParams.get("id");
|
||||
if (!id) return NextResponse.json({ error: "Missing id" }, { status: 400 });
|
||||
return adminFetch(`/api/admin/ai/providers/${id}`, "PATCH", await req.json());
|
||||
}
|
||||
|
||||
// DELETE /api/admin/ai/providers?id={id} — delete provider
|
||||
export async function DELETE(req: NextRequest) {
|
||||
try {
|
||||
const { searchParams } = new URL(req.url);
|
||||
const id = searchParams.get("id");
|
||||
if (!id) return NextResponse.json({ error: "Missing id" }, { status: 400 });
|
||||
const data = await deleteProvider(id);
|
||||
return NextResponse.json(data);
|
||||
} catch (e) {
|
||||
const msg = e instanceof Error ? e.message : String(e);
|
||||
console.error("Delete provider error:", msg);
|
||||
return NextResponse.json({ error: `删除失败: ${msg}` }, { status: 500 });
|
||||
}
|
||||
const { searchParams } = new URL(req.url);
|
||||
const id = searchParams.get("id");
|
||||
if (!id) return NextResponse.json({ error: "Missing id" }, { status: 400 });
|
||||
return adminFetch(`/api/admin/ai/providers/${id}`, "DELETE");
|
||||
}
|
||||
|
||||
@ -1,48 +1,40 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { createVersion, updateVersion, deleteVersion } from "@/lib/adminrpc/client";
|
||||
import { RUST_BACKEND_URL, ADMIN_API_SHARED_KEY } from "@/lib/env";
|
||||
|
||||
export const runtime = "nodejs";
|
||||
|
||||
async function adminFetch(path: string, method: string, body?: unknown) {
|
||||
if (!ADMIN_API_SHARED_KEY) {
|
||||
return NextResponse.json({ error: "ADMIN_API_SHARED_KEY 未配置" }, { status: 500 });
|
||||
}
|
||||
const res = await fetch(`${RUST_BACKEND_URL}${path}`, {
|
||||
method,
|
||||
headers: { "Content-Type": "application/json", "x-admin-api-key": ADMIN_API_SHARED_KEY },
|
||||
body: body ? JSON.stringify(body) : undefined,
|
||||
signal: AbortSignal.timeout(30_000),
|
||||
});
|
||||
const data = await res.json();
|
||||
if (!res.ok) return NextResponse.json(data, { status: res.status });
|
||||
return NextResponse.json(data);
|
||||
}
|
||||
|
||||
// POST /api/admin/ai/versions — create version
|
||||
export async function POST(req: NextRequest) {
|
||||
try {
|
||||
const body = await req.json();
|
||||
const data = await createVersion(body);
|
||||
return NextResponse.json(data);
|
||||
} catch (e) {
|
||||
const msg = e instanceof Error ? e.message : String(e);
|
||||
console.error("Create version error:", msg);
|
||||
return NextResponse.json({ error: `创建失败: ${msg}` }, { status: 500 });
|
||||
}
|
||||
return adminFetch("/api/admin/ai/versions", "POST", await req.json());
|
||||
}
|
||||
|
||||
// PATCH /api/admin/ai/versions?id={id} — update version
|
||||
export async function PATCH(req: NextRequest) {
|
||||
try {
|
||||
const { searchParams } = new URL(req.url);
|
||||
const id = searchParams.get("id");
|
||||
if (!id) return NextResponse.json({ error: "Missing id" }, { status: 400 });
|
||||
const body = await req.json();
|
||||
const data = await updateVersion(id, body);
|
||||
return NextResponse.json(data);
|
||||
} catch (e) {
|
||||
const msg = e instanceof Error ? e.message : String(e);
|
||||
console.error("Update version error:", msg);
|
||||
return NextResponse.json({ error: `更新失败: ${msg}` }, { status: 500 });
|
||||
}
|
||||
const { searchParams } = new URL(req.url);
|
||||
const id = searchParams.get("id");
|
||||
if (!id) return NextResponse.json({ error: "Missing id" }, { status: 400 });
|
||||
return adminFetch(`/api/admin/ai/versions/${id}`, "PATCH", await req.json());
|
||||
}
|
||||
|
||||
// DELETE /api/admin/ai/versions?id={id} — delete version
|
||||
export async function DELETE(req: NextRequest) {
|
||||
try {
|
||||
const { searchParams } = new URL(req.url);
|
||||
const id = searchParams.get("id");
|
||||
if (!id) return NextResponse.json({ error: "Missing id" }, { status: 400 });
|
||||
const data = await deleteVersion(id);
|
||||
return NextResponse.json(data);
|
||||
} catch (e) {
|
||||
const msg = e instanceof Error ? e.message : String(e);
|
||||
console.error("Delete version error:", msg);
|
||||
return NextResponse.json({ error: `删除失败: ${msg}` }, { status: 500 });
|
||||
}
|
||||
const { searchParams } = new URL(req.url);
|
||||
const id = searchParams.get("id");
|
||||
if (!id) return NextResponse.json({ error: "Missing id" }, { status: 400 });
|
||||
return adminFetch(`/api/admin/ai/versions/${id}`, "DELETE");
|
||||
}
|
||||
|
||||
@ -56,7 +56,7 @@ export async function PUT(req: NextRequest) {
|
||||
const body = await req.json() as Record<string, string>;
|
||||
const adminUserId = parseInt(req.headers.get("x-admin-user-id") || "0", 10);
|
||||
const allowedKeys = [
|
||||
"ai_model", "ai_api_key", "ai_enabled", "basic_api_url",
|
||||
"ai_model", "ai_api_key", "ai_enabled",
|
||||
"smtp_host", "smtp_port", "smtp_username", "smtp_password", "smtp_from", "smtp_tls",
|
||||
"report_enabled",
|
||||
];
|
||||
|
||||
@ -50,7 +50,6 @@ interface AiConfig {
|
||||
ai_model: string;
|
||||
ai_api_key: string;
|
||||
ai_enabled: string;
|
||||
basic_api_url: string;
|
||||
smtp_host: string;
|
||||
smtp_port: string;
|
||||
smtp_username: string;
|
||||
@ -63,11 +62,10 @@ interface AiConfig {
|
||||
// ─── Main handler ─────────────────────────────────────────────────────────────
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
// Verify cron — accept internal marker OR secret from K8s CronJob
|
||||
const cronInternal = req.headers.get("x-cron-internal");
|
||||
// Verify cron secret (optional, set in K8s CronJob annotation)
|
||||
const cronSecret = req.headers.get("x-cron-secret");
|
||||
const expectedSecret = process.env.DAILY_REPORT_CRON_SECRET;
|
||||
if (!cronInternal && (!expectedSecret || cronSecret !== expectedSecret)) {
|
||||
if (expectedSecret && cronSecret !== expectedSecret) {
|
||||
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||
}
|
||||
|
||||
@ -103,12 +101,7 @@ export async function POST(req: NextRequest) {
|
||||
// ── Generate AI summary ──────────────────────────────────────────────────
|
||||
let aiSummary = "";
|
||||
if (cfg.ai_enabled === "true" && cfg.ai_api_key) {
|
||||
aiSummary = await generateAiSummary(
|
||||
stats,
|
||||
cfg.ai_model || "gpt-4o-mini",
|
||||
cfg.ai_api_key,
|
||||
cfg.basic_api_url || ""
|
||||
);
|
||||
aiSummary = await generateAiSummary(stats, cfg.ai_model || "gpt-4o-mini", cfg.ai_api_key);
|
||||
}
|
||||
|
||||
// ── Build email content ─────────────────────────────────────────────────
|
||||
@ -179,17 +172,17 @@ async function collectDailyStats(): Promise<DailyStats> {
|
||||
),
|
||||
// Active users today (users who sent messages)
|
||||
query<{ count: string }>(
|
||||
`SELECT COUNT(DISTINCT sender_id)::text as count
|
||||
`SELECT COUNT(DISTINCT user_id)::text as count
|
||||
FROM room_message WHERE created_at >= $1`,
|
||||
[todayStr]
|
||||
),
|
||||
// Top room by message count today
|
||||
query<{ room: string; room_name: string; message_count: string }>(
|
||||
`SELECT rm.room, r.name as room_name, COUNT(*)::text as message_count
|
||||
query<{ room_id: string; room_name: string; message_count: string }>(
|
||||
`SELECT rm.room_id, r.name as room_name, COUNT(*)::text as message_count
|
||||
FROM room_message rm
|
||||
JOIN room r ON r.id = rm.room
|
||||
JOIN room r ON r.id = rm.room_id
|
||||
WHERE rm.created_at >= $1
|
||||
GROUP BY rm.room, r.name
|
||||
GROUP BY rm.room_id, r.name
|
||||
ORDER BY COUNT(*) DESC
|
||||
LIMIT 1`,
|
||||
[todayStr]
|
||||
@ -203,7 +196,7 @@ async function collectDailyStats(): Promise<DailyStats> {
|
||||
activeUsers: parseInt(activeUserRow.rows[0]?.count || "0", 10),
|
||||
newCommits: 0, // filled below
|
||||
topRoom: topRoomRow.rows[0] ? {
|
||||
id: topRoomRow.rows[0].room,
|
||||
id: topRoomRow.rows[0].room_id,
|
||||
name: topRoomRow.rows[0].room_name,
|
||||
messageCount: parseInt(topRoomRow.rows[0].message_count || "0", 10),
|
||||
} : null,
|
||||
@ -228,7 +221,7 @@ async function collectDailyStats(): Promise<DailyStats> {
|
||||
const messages = await query<{ content: string; created_at: string }>(
|
||||
`SELECT content, created_at::text
|
||||
FROM room_message
|
||||
WHERE room = $1 AND created_at >= $2
|
||||
WHERE room_id = $1 AND created_at >= $2
|
||||
ORDER BY created_at DESC
|
||||
LIMIT 20`,
|
||||
[stats.topRoom.id, todayStr]
|
||||
@ -247,8 +240,7 @@ async function collectDailyStats(): Promise<DailyStats> {
|
||||
async function generateAiSummary(
|
||||
stats: DailyStats,
|
||||
model: string,
|
||||
apiKey: string,
|
||||
basicApiUrl: string
|
||||
apiKey: string
|
||||
): Promise<string> {
|
||||
const systemPrompt = `你是一名平台运营分析师。请根据以下每日平台数据,生成一段简洁的中文总结(100-200字),分析今日平台的关键变化和亮点。注意:
|
||||
1. 用专业但易懂的语言
|
||||
@ -283,11 +275,8 @@ async function generateAiSummary(
|
||||
${topRoomContext}
|
||||
${userMessagesSection}`;
|
||||
|
||||
const baseUrl = basicApiUrl || "https://api.openai.com";
|
||||
const chatEndpoint = `${baseUrl.replace(/\/$/, "")}/v1/chat/completions`;
|
||||
|
||||
try {
|
||||
const response = await fetch(chatEndpoint, {
|
||||
const response = await fetch("https://api.openai.com/v1/chat/completions", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
|
||||
@ -1,59 +0,0 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { query } from "@/lib/db";
|
||||
import { createAuditLog } from "@/lib/log";
|
||||
|
||||
export const runtime = "nodejs";
|
||||
|
||||
// GET /api/admin/daily-report/recipients — list all recipients
|
||||
export async function GET() {
|
||||
try {
|
||||
const result = await query(
|
||||
`SELECT id, email, name, is_active, created_at::text as created_at, created_by
|
||||
FROM internal_email_recipients
|
||||
ORDER BY created_at DESC`
|
||||
);
|
||||
return NextResponse.json({ recipients: result.rows });
|
||||
} catch (e) {
|
||||
console.error("[recipients] List error:", e);
|
||||
return NextResponse.json({ error: "服务器错误" }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
// POST /api/admin/daily-report/recipients — add recipient
|
||||
export async function POST(req: NextRequest) {
|
||||
try {
|
||||
const body = await req.json() as { email?: string; name?: string };
|
||||
const { email, name = "" } = body;
|
||||
|
||||
if (!email || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
|
||||
return NextResponse.json({ error: "无效的邮箱地址" }, { status: 400 });
|
||||
}
|
||||
|
||||
const adminUserId = parseInt(req.headers.get("x-admin-user-id") || "0", 10);
|
||||
|
||||
const result = await query(
|
||||
`INSERT INTO internal_email_recipients (email, name, created_by)
|
||||
VALUES ($1, $2, $3)
|
||||
RETURNING id, email, name, is_active, created_at::text as created_at`,
|
||||
[email.toLowerCase(), name, adminUserId]
|
||||
);
|
||||
|
||||
await createAuditLog({
|
||||
userId: adminUserId,
|
||||
username: req.headers.get("x-admin-username") || "unknown",
|
||||
action: "create",
|
||||
resource: "internal_email_recipient",
|
||||
resourceId: String(result.rows[0]?.id),
|
||||
requestParams: { email },
|
||||
});
|
||||
|
||||
return NextResponse.json(result.rows[0], { status: 201 });
|
||||
} catch (e: unknown) {
|
||||
const err = e as { code?: string };
|
||||
if (err?.code === "23505") {
|
||||
return NextResponse.json({ error: "该邮箱已存在" }, { status: 409 });
|
||||
}
|
||||
console.error("[recipients] Add error:", e);
|
||||
return NextResponse.json({ error: "服务器错误" }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@ -20,8 +20,8 @@ export async function PATCH(
|
||||
return NextResponse.json({ error: "无效的角色" }, { status: 400 });
|
||||
}
|
||||
|
||||
const member = await query<{ id: string; scope: string; user_uuid: string }>(
|
||||
`SELECT id, scope, user_uuid FROM project_members WHERE id = $1 AND project_uuid = $2`,
|
||||
const member = await query<{ id: string; scope: string; user: string }>(
|
||||
`SELECT id, scope, user FROM project_members WHERE id = $1 AND project = $2`,
|
||||
[memberId, id]
|
||||
);
|
||||
if (!member.rows.length) {
|
||||
@ -64,8 +64,8 @@ export async function DELETE(
|
||||
const adminUserId = parseInt(req.headers.get("x-admin-user-id") || "0", 10);
|
||||
const adminUsername = req.headers.get("x-admin-username") || "unknown";
|
||||
|
||||
const member = await query<{ id: string; scope: string; user_uuid: string }>(
|
||||
`SELECT id, scope, user_uuid FROM project_members WHERE id = $1 AND project_uuid = $2`,
|
||||
const member = await query<{ id: string; scope: string; user: string }>(
|
||||
`SELECT id, scope, user FROM project_members WHERE id = $1 AND project = $2`,
|
||||
[memberId, id]
|
||||
);
|
||||
if (!member.rows.length) {
|
||||
@ -83,7 +83,7 @@ export async function DELETE(
|
||||
action: "delete",
|
||||
resource: "project_member",
|
||||
resourceId: memberId,
|
||||
requestParams: { projectId: id, userId: member.rows[0].user_uuid },
|
||||
requestParams: { projectId: id, userId: member.rows[0].user },
|
||||
ipAddress: req.headers.get("x-forwarded-for") || undefined,
|
||||
userAgent: req.headers.get("user-agent") || undefined,
|
||||
});
|
||||
|
||||
@ -13,21 +13,21 @@ export async function GET(
|
||||
const { id } = await params;
|
||||
|
||||
const result = await query(
|
||||
`SELECT pm.id, pm.project_uuid, pm.user_uuid, pm.scope, pm.joined_at::text,
|
||||
`SELECT pm.id, pm.project, pm.user, pm.scope, pm.joined_at::text,
|
||||
u.username, u.display_name, u.avatar_url,
|
||||
COALESCE(up.is_active, true) as user_is_active
|
||||
FROM project_members pm
|
||||
JOIN "user" u ON u.uid = pm.user_uuid
|
||||
JOIN "user" u ON u.uid = pm.user
|
||||
LEFT JOIN user_password up ON up.user = u.uid
|
||||
WHERE pm.project_uuid = $1
|
||||
WHERE pm.project = $1
|
||||
ORDER BY pm.scope = 'owner' DESC, pm.scope = 'admin' DESC, pm.joined_at ASC`,
|
||||
[id]
|
||||
);
|
||||
|
||||
const members = result.rows.map((r: Record<string, unknown>) => ({
|
||||
id: r.id,
|
||||
projectId: r.project_uuid,
|
||||
userId: r.user_uuid,
|
||||
projectId: r.project,
|
||||
userId: r.user,
|
||||
scope: r.scope,
|
||||
joinedAt: r.joined_at,
|
||||
username: r.username,
|
||||
@ -81,7 +81,7 @@ export async function POST(
|
||||
|
||||
// 检查是否已是成员
|
||||
const exist = await query(
|
||||
`SELECT id FROM project_members WHERE project_uuid = $1 AND user_uuid = $2`,
|
||||
`SELECT id FROM project_members WHERE project = $1 AND user = $2`,
|
||||
[id, body.userId]
|
||||
);
|
||||
if (exist.rows.length) {
|
||||
@ -89,7 +89,7 @@ export async function POST(
|
||||
}
|
||||
|
||||
const result = await query(
|
||||
`INSERT INTO project_members (project_uuid, user_uuid, scope, joined_at)
|
||||
`INSERT INTO project_members (project, user, scope, joined_at)
|
||||
VALUES ($1, $2, $3, NOW())
|
||||
RETURNING id`,
|
||||
[id, body.userId, scope]
|
||||
|
||||
@ -8,38 +8,41 @@ export async function GET(req: NextRequest) {
|
||||
const { searchParams } = req.nextUrl;
|
||||
const type = searchParams.get("type") || "all";
|
||||
|
||||
const [providersData, modelsData, pricingData, versionsData] = await Promise.all([
|
||||
query(
|
||||
`SELECT id, name, display_name, website, status, created_at, updated_at
|
||||
FROM ai_model_provider
|
||||
ORDER BY name`
|
||||
),
|
||||
query(
|
||||
`SELECT m.id, m.name, m.modality, m.capability, m.context_length,
|
||||
m.max_output_tokens, m.training_cutoff, m.is_open_source, m.status,
|
||||
mv.model_id, mv.version,
|
||||
p.id as provider_id, p.name as provider_name
|
||||
FROM ai_model m
|
||||
JOIN ai_model_provider p ON p.id = m.provider_id
|
||||
LEFT JOIN ai_model_version mv ON mv.model_id = m.id AND mv.is_default = true
|
||||
ORDER BY p.name, m.name`
|
||||
),
|
||||
query(
|
||||
`SELECT mp.id, mp.model_version_id, mp.input_price_per_1k_tokens, mp.output_price_per_1k_tokens,
|
||||
mp.currency, mp.effective_from,
|
||||
m.name as model_name, mv.model_id
|
||||
FROM ai_model_pricing mp
|
||||
JOIN ai_model_version mv ON mv.id = mp.model_version_id
|
||||
JOIN ai_model m ON m.id = mv.model_id
|
||||
ORDER BY mp.effective_from DESC
|
||||
LIMIT 200`
|
||||
),
|
||||
query(
|
||||
`SELECT mv.id, mv.model_id, mv.version, mv.release_date, mv.change_log, mv.is_default, mv.status, mv.created_at
|
||||
FROM ai_model_version mv
|
||||
ORDER BY mv.model_id, mv.version`
|
||||
),
|
||||
]);
|
||||
const providers = query(
|
||||
`SELECT id, name, display_name, website, status, created_at
|
||||
FROM ai_model_provider
|
||||
ORDER BY name`
|
||||
);
|
||||
|
||||
const models = query(
|
||||
`SELECT m.id, m.name, m.modality, m.capability, m.context_length,
|
||||
m.max_output_tokens, m.training_cutoff, m.is_open_source, m.status,
|
||||
mv.model_id, mv.version,
|
||||
p.id as provider_id, p.name as provider_name
|
||||
FROM ai_model m
|
||||
JOIN ai_model_provider p ON p.id = m.provider_id
|
||||
LEFT JOIN ai_model_version mv ON mv.model_id = m.id AND mv.is_default = true
|
||||
ORDER BY p.name, m.name`
|
||||
);
|
||||
|
||||
const pricing = query(
|
||||
`SELECT mp.id, mp.model_version_id, mp.input_price_per_1k_tokens, mp.output_price_per_1k_tokens,
|
||||
mp.currency, mp.effective_from,
|
||||
m.name as model_name, mv.model_id
|
||||
FROM ai_model_pricing mp
|
||||
JOIN ai_model_version mv ON mv.id = mp.model_version_id
|
||||
JOIN ai_model m ON m.id = mv.model_id
|
||||
ORDER BY mp.effective_from DESC
|
||||
LIMIT 200`
|
||||
);
|
||||
|
||||
const versions = query(
|
||||
`SELECT mv.id, mv.model_id, mv.version, mv.release_date, mv.change_log, mv.is_default, mv.status, mv.created_at
|
||||
FROM ai_model_version mv
|
||||
ORDER BY mv.model_id, mv.version`
|
||||
);
|
||||
|
||||
const [providersData, modelsData, pricingData, versionsData] = await Promise.all([providers, models, pricing, versions]);
|
||||
|
||||
const providersList = providersData.rows.map((r: Record<string, unknown>) => ({
|
||||
id: String(r.id),
|
||||
|
||||
@ -1,18 +1,48 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { syncModels } from "@/lib/adminrpc/client";
|
||||
import { RUST_BACKEND_URL, ADMIN_API_SHARED_KEY } from "@/lib/env";
|
||||
|
||||
export const runtime = "nodejs";
|
||||
|
||||
/**
|
||||
* Trigger AI model sync via adminrpc gRPC.
|
||||
* Trigger AI model sync via Rust backend.
|
||||
* Calls POST /api/admin/ai/sync on the Rust app.
|
||||
*/
|
||||
export async function POST() {
|
||||
if (!ADMIN_API_SHARED_KEY) {
|
||||
return NextResponse.json(
|
||||
{ error: "ADMIN_API_SHARED_KEY 未配置" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
const data = await syncModels();
|
||||
const url = `${RUST_BACKEND_URL}/api/admin/ai/sync`;
|
||||
const res = await fetch(url, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"x-admin-api-key": ADMIN_API_SHARED_KEY,
|
||||
},
|
||||
// Timeout: 2 minutes for sync
|
||||
signal: AbortSignal.timeout(120_000),
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
const body = await res.text();
|
||||
return NextResponse.json(
|
||||
{ error: `同步失败: ${res.status} ${body}` },
|
||||
{ status: res.status }
|
||||
);
|
||||
}
|
||||
|
||||
const data = await res.json();
|
||||
return NextResponse.json(data);
|
||||
} catch (e) {
|
||||
const msg = e instanceof Error ? e.message : String(e);
|
||||
console.error("AI sync error:", msg);
|
||||
return NextResponse.json({ error: `同步失败: ${msg}` }, { status: 500 });
|
||||
return NextResponse.json(
|
||||
{ error: `同步失败: ${msg}` },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,18 +1,47 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { checkAlerts } from "@/lib/adminrpc/client";
|
||||
import { RUST_BACKEND_URL, ADMIN_API_SHARED_KEY } from "@/lib/env";
|
||||
|
||||
export const runtime = "nodejs";
|
||||
|
||||
/**
|
||||
* Trigger workspace billing alert check via adminrpc gRPC.
|
||||
* Trigger workspace billing alert check via Rust backend.
|
||||
* Calls POST /api/admin/alerts/check on the Rust app.
|
||||
*/
|
||||
export async function POST() {
|
||||
if (!ADMIN_API_SHARED_KEY) {
|
||||
return NextResponse.json(
|
||||
{ error: "ADMIN_API_SHARED_KEY 未配置" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
const data = await checkAlerts();
|
||||
const url = `${RUST_BACKEND_URL}/api/admin/alerts/check`;
|
||||
const res = await fetch(url, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"x-admin-api-key": ADMIN_API_SHARED_KEY,
|
||||
},
|
||||
signal: AbortSignal.timeout(60_000),
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
const body = await res.text();
|
||||
return NextResponse.json(
|
||||
{ error: `检查失败: ${res.status} ${body}` },
|
||||
{ status: res.status }
|
||||
);
|
||||
}
|
||||
|
||||
const data = await res.json();
|
||||
return NextResponse.json(data);
|
||||
} catch (e) {
|
||||
const msg = e instanceof Error ? e.message : String(e);
|
||||
console.error("Alert check error:", msg);
|
||||
return NextResponse.json({ error: `检查失败: ${msg}` }, { status: 500 });
|
||||
return NextResponse.json(
|
||||
{ error: `检查失败: ${msg}` },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -23,65 +23,71 @@ export async function GET(req: NextRequest) {
|
||||
const action = searchParams.get("action") || "";
|
||||
const offset = (page - 1) * pageSize;
|
||||
|
||||
const actionPattern = action ? `%${action}%` : null;
|
||||
const limitOffsetParams: unknown[] = [pageSize, offset];
|
||||
|
||||
// Build queries with proper parameter indexing
|
||||
let userQuery = "";
|
||||
let projectQuery = "";
|
||||
let userParams: unknown[] = [];
|
||||
let projectParams: unknown[] = [];
|
||||
let queryParams: unknown[] = [];
|
||||
let userCountQuery = "";
|
||||
let projectCountQuery = "";
|
||||
let paramIdx = 1;
|
||||
|
||||
// Build user_activity_log query
|
||||
if (source !== "project") {
|
||||
if (action) {
|
||||
userParams = [actionPattern, ...limitOffsetParams];
|
||||
userQuery = `SELECT 'user_activity' as source, id,
|
||||
COALESCE(user_uid::text, '') as actor_uid,
|
||||
action, NULL::text as resource,
|
||||
ip_address, user_agent, created_at::text as created_at
|
||||
FROM user_activity_log
|
||||
WHERE action ILIKE $1
|
||||
WHERE action ILIKE $${paramIdx}
|
||||
ORDER BY created_at DESC
|
||||
LIMIT $2 OFFSET $3`;
|
||||
LIMIT $${paramIdx + 1} OFFSET $${paramIdx + 2}`;
|
||||
userCountQuery = `SELECT COUNT(*) FROM user_activity_log WHERE action ILIKE $${paramIdx}`;
|
||||
queryParams.push(`%${action}%`, pageSize, offset);
|
||||
paramIdx += 3;
|
||||
} else {
|
||||
userParams = limitOffsetParams;
|
||||
userQuery = `SELECT 'user_activity' as source, id,
|
||||
COALESCE(user_uid::text, '') as actor_uid,
|
||||
action, NULL::text as resource,
|
||||
ip_address, user_agent, created_at::text as created_at
|
||||
FROM user_activity_log
|
||||
ORDER BY created_at DESC
|
||||
LIMIT $1 OFFSET $2`;
|
||||
LIMIT $${paramIdx} OFFSET $${paramIdx + 1}`;
|
||||
userCountQuery = `SELECT COUNT(*) FROM user_activity_log`;
|
||||
queryParams.push(pageSize, offset);
|
||||
paramIdx += 2;
|
||||
}
|
||||
}
|
||||
|
||||
// Build project_audit_log query
|
||||
if (source !== "user") {
|
||||
if (action) {
|
||||
projectParams = [actionPattern, ...limitOffsetParams];
|
||||
projectQuery = `SELECT 'project_audit' as source, id,
|
||||
COALESCE(actor::text, '') as actor_uid,
|
||||
action, details as resource, ip_address,
|
||||
NULL as user_agent, created_at::text as created_at
|
||||
FROM project_audit_log
|
||||
WHERE action ILIKE $1
|
||||
WHERE action ILIKE $${paramIdx}
|
||||
ORDER BY created_at DESC
|
||||
LIMIT $2 OFFSET $3`;
|
||||
LIMIT $${paramIdx + 1} OFFSET $${paramIdx + 2}`;
|
||||
projectCountQuery = `SELECT COUNT(*) FROM project_audit_log WHERE action ILIKE $${paramIdx}`;
|
||||
queryParams.push(`%${action}%`, pageSize, offset);
|
||||
paramIdx += 3;
|
||||
} else {
|
||||
projectParams = limitOffsetParams;
|
||||
projectQuery = `SELECT 'project_audit' as source, id,
|
||||
COALESCE(actor::text, '') as actor_uid,
|
||||
action, details as resource, ip_address,
|
||||
NULL as user_agent, created_at::text as created_at
|
||||
FROM project_audit_log
|
||||
ORDER BY created_at DESC
|
||||
LIMIT $1 OFFSET $2`;
|
||||
LIMIT $${paramIdx} OFFSET $${paramIdx + 1}`;
|
||||
projectCountQuery = `SELECT COUNT(*) FROM project_audit_log`;
|
||||
queryParams.push(pageSize, offset);
|
||||
paramIdx += 2;
|
||||
}
|
||||
}
|
||||
|
||||
const [userLogs, projectLogs] = await Promise.all([
|
||||
userQuery ? query<AuditLog>(userQuery, userParams) : Promise.resolve({ rows: [] as AuditLog[] }),
|
||||
projectQuery ? query<AuditLog>(projectQuery, projectParams) : Promise.resolve({ rows: [] as AuditLog[] }),
|
||||
userQuery ? query<AuditLog>(userQuery, queryParams) : Promise.resolve({ rows: [] as AuditLog[] }),
|
||||
projectQuery ? query<AuditLog>(projectQuery, queryParams) : Promise.resolve({ rows: [] as AuditLog[] }),
|
||||
]);
|
||||
|
||||
// 合并并排序
|
||||
@ -91,8 +97,8 @@ export async function GET(req: NextRequest) {
|
||||
|
||||
// 总数
|
||||
const [userCountRes, projectCountRes] = await Promise.all([
|
||||
userCountQuery(userParams, action),
|
||||
projectCountQuery(projectParams, action),
|
||||
userCountQuery ? query<{ count: string }>(userCountQuery, action ? [`%${action}%`] : []) : Promise.resolve({ rows: [{ count: "0" }] }),
|
||||
projectCountQuery ? query<{ count: string }>(projectCountQuery, action ? [`%${action}%`] : []) : Promise.resolve({ rows: [{ count: "0" }] }),
|
||||
]);
|
||||
const total = parseInt(String(userCountRes.rows[0]?.count || "0"), 10) +
|
||||
parseInt(String(projectCountRes.rows[0]?.count || "0"), 10);
|
||||
@ -103,25 +109,3 @@ export async function GET(req: NextRequest) {
|
||||
return NextResponse.json({ error: "服务器错误" }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
async function userCountQuery(params: unknown[], action: string | null) {
|
||||
if (!params.length) return { rows: [] as { count: string }[] };
|
||||
if (action) {
|
||||
return query<{ count: string }>(
|
||||
`SELECT COUNT(*) FROM user_activity_log WHERE action ILIKE $1`,
|
||||
[params[0]]
|
||||
);
|
||||
}
|
||||
return query<{ count: string }>(`SELECT COUNT(*) FROM user_activity_log`);
|
||||
}
|
||||
|
||||
async function projectCountQuery(params: unknown[], action: string | null) {
|
||||
if (!params.length) return { rows: [] as { count: string }[] };
|
||||
if (action) {
|
||||
return query<{ count: string }>(
|
||||
`SELECT COUNT(*) FROM project_audit_log WHERE action ILIKE $1`,
|
||||
[params[0]]
|
||||
);
|
||||
}
|
||||
return query<{ count: string }>(`SELECT COUNT(*) FROM project_audit_log`);
|
||||
}
|
||||
|
||||
@ -79,20 +79,20 @@ export async function PATCH(req: NextRequest) {
|
||||
|
||||
// Get user ids from uids
|
||||
const uidPlaceholders = ids.map((_, i) => `$${i + 1}`).join(", ");
|
||||
const uidResult = await query<{ uid: string }>(
|
||||
`SELECT uid FROM "user" WHERE uid IN (${uidPlaceholders})`,
|
||||
const uidResult = await query<{ id: number }>(
|
||||
`SELECT id FROM "user" WHERE uid IN (${uidPlaceholders})`,
|
||||
ids
|
||||
);
|
||||
const uids = uidResult.rows.map((r) => r.uid);
|
||||
const userIds = uidResult.rows.map((r) => r.id);
|
||||
|
||||
if (!uids.length) {
|
||||
if (!userIds.length) {
|
||||
return NextResponse.json({ error: "未找到匹配的用户" }, { status: 404 });
|
||||
}
|
||||
|
||||
const uidPlaceholders2 = uids.map((_, i) => `$${i + 1}`).join(", ");
|
||||
const idPlaceholders = userIds.map((_, i) => `$${i + 1}`).join(", ");
|
||||
await query(
|
||||
`UPDATE user_password SET is_active = $${uids.length + 1}, updated_at = NOW() WHERE "user" IN (${uidPlaceholders2})`,
|
||||
[...uids, isActive]
|
||||
`UPDATE user_password SET is_active = $${userIds.length + 1}, updated_at = NOW() WHERE user_id IN (${idPlaceholders})`,
|
||||
[...userIds, isActive]
|
||||
);
|
||||
|
||||
const adminUserId = parseInt(req.headers.get("x-admin-user-id") || "0", 10);
|
||||
@ -102,13 +102,13 @@ export async function PATCH(req: NextRequest) {
|
||||
username: adminUsername,
|
||||
action: "update",
|
||||
resource: "user_batch_status",
|
||||
resourceId: `batch(${uids.length})`,
|
||||
requestParams: { uidCount: ids.length, userIdCount: uids.length, action },
|
||||
resourceId: `batch(${userIds.length})`,
|
||||
requestParams: { uidCount: ids.length, userIdCount: userIds.length, action },
|
||||
ipAddress: req.headers.get("x-forwarded-for") || undefined,
|
||||
userAgent: req.headers.get("user-agent") || undefined,
|
||||
});
|
||||
|
||||
return NextResponse.json({ success: true, updated: uids.length });
|
||||
return NextResponse.json({ success: true, updated: userIds.length });
|
||||
} catch (e) {
|
||||
console.error("Batch update user status error:", e);
|
||||
return NextResponse.json({ error: "服务器错误" }, { status: 500 });
|
||||
|
||||
@ -66,7 +66,7 @@ export async function POST(
|
||||
// Insert billing history
|
||||
await client.query(
|
||||
`INSERT INTO workspace_billing_history
|
||||
(workspace_id, user_id, amount, reason, extra, currency)
|
||||
(workspace_id, user, amount, reason, extra, currency)
|
||||
VALUES ($1, NULL, $2, 'admin_credit', $3, $4)`,
|
||||
[id, amount, JSON.stringify({ description: description || "Admin 手动充值" }), currency]
|
||||
);
|
||||
|
||||
@ -1,270 +0,0 @@
|
||||
/**
|
||||
* AdminRPC gRPC client for Node.js environment.
|
||||
*
|
||||
* Uses raw HTTP/1.1 with the tonic gRPC server.
|
||||
* All methods pass body_json as a JSON string (matching the proto design).
|
||||
*/
|
||||
|
||||
import { create, toBinary, fromBinary } from "@bufbuild/protobuf";
|
||||
import type { IncomingMessage } from "node:http";
|
||||
import { ADMIN_RPC_URL } from "@/lib/env";
|
||||
|
||||
// Import schemas from generated proto
|
||||
import * as admin from "./generated/proto/admin_pb";
|
||||
|
||||
// ─── Low-level gRPC-Web over HTTP/1.1 ────────────────────────────────────────
|
||||
|
||||
function grpcRequest(
|
||||
baseUrl: string,
|
||||
servicePath: string,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
schema: any,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
responseSchema: any,
|
||||
body: Uint8Array,
|
||||
): Promise<Uint8Array> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const url = new URL(baseUrl);
|
||||
const isHttps = url.protocol === "https:";
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
const http = (isHttps ? require("node:https") : require("node:http")) as typeof import("node:http");
|
||||
|
||||
const req = http.request(
|
||||
{
|
||||
hostname: url.hostname,
|
||||
port: url.port || (isHttps ? 443 : 80),
|
||||
path: servicePath,
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/grpc-web+proto",
|
||||
"X-Grpc-Web": "1",
|
||||
"TE": "trailers",
|
||||
"User-Agent": "admin-module/1.0",
|
||||
"Content-Length": body.byteLength,
|
||||
},
|
||||
},
|
||||
(res: IncomingMessage) => {
|
||||
const chunks: Buffer[] = [];
|
||||
res.on("data", (chunk: Buffer) => chunks.push(chunk));
|
||||
res.on("end", () => {
|
||||
const data = Buffer.concat(chunks);
|
||||
if (data.length === 0) {
|
||||
const grpcStatus = res.headers["grpc-status"];
|
||||
if (grpcStatus && grpcStatus !== "0") {
|
||||
reject(new Error(`gRPC error ${grpcStatus}: ${res.headers["grpc-message"] || ""}`));
|
||||
return;
|
||||
}
|
||||
resolve(new Uint8Array(0));
|
||||
return;
|
||||
}
|
||||
// gRPC-web body format: [0x00, 4-byte big-endian length, payload...]
|
||||
if (data[0] === 0x00 && data.length > 5) {
|
||||
const bodyBytes = data.slice(5);
|
||||
resolve(new Uint8Array(bodyBytes.buffer, bodyBytes.byteOffset, bodyBytes.byteLength));
|
||||
} else {
|
||||
resolve(new Uint8Array(data.buffer, data.byteOffset, data.byteLength));
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
req.on("error", reject);
|
||||
req.write(body);
|
||||
req.end();
|
||||
});
|
||||
}
|
||||
|
||||
// ─── Message encoding / decoding ──────────────────────────────────────────────
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
function encode(schema: any, init: Record<string, unknown>): Uint8Array {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const msg = create(schema as any, init as any);
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
return toBinary(schema as any, msg as any);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
function decode(schema: any, bytes: Uint8Array): any {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
return fromBinary(schema as any, bytes) as any;
|
||||
}
|
||||
|
||||
// ─── AI: Sync Models ──────────────────────────────────────────────────────────
|
||||
|
||||
export async function syncModels(): Promise<unknown> {
|
||||
const body = encode(admin.SyncModelsRequestSchema, {});
|
||||
const bytes = await grpcRequest(
|
||||
ADMIN_RPC_URL,
|
||||
"/admin.SessionAdmin/SyncModels",
|
||||
admin.SyncModelsRequestSchema,
|
||||
admin.SyncModelsResponseSchema,
|
||||
body,
|
||||
);
|
||||
if (bytes.length === 0) return {};
|
||||
const resp = decode(admin.SyncModelsResponseSchema, bytes);
|
||||
return JSON.parse(resp.bodyJson || "{}");
|
||||
}
|
||||
|
||||
// ─── AI: Check Alerts ─────────────────────────────────────────────────────────
|
||||
|
||||
export async function checkAlerts(): Promise<unknown> {
|
||||
const body = encode(admin.CheckAlertsRequestSchema, {});
|
||||
const bytes = await grpcRequest(
|
||||
ADMIN_RPC_URL,
|
||||
"/admin.SessionAdmin/CheckAlerts",
|
||||
admin.CheckAlertsRequestSchema,
|
||||
admin.CheckAlertsResponseSchema,
|
||||
body,
|
||||
);
|
||||
if (bytes.length === 0) return {};
|
||||
const resp = decode(admin.CheckAlertsResponseSchema, bytes);
|
||||
return JSON.parse(resp.bodyJson || "{}");
|
||||
}
|
||||
|
||||
// ─── AI: Provider CRUD ────────────────────────────────────────────────────────
|
||||
|
||||
export async function createProvider(body: unknown): Promise<unknown> {
|
||||
const msgBody = encode(admin.CreateProviderRequestSchema, { bodyJson: JSON.stringify(body) });
|
||||
const bytes = await grpcRequest(
|
||||
ADMIN_RPC_URL,
|
||||
"/admin.SessionAdmin/CreateProvider",
|
||||
admin.CreateProviderRequestSchema,
|
||||
admin.ProviderResponseSchema,
|
||||
msgBody,
|
||||
);
|
||||
if (bytes.length === 0) return {};
|
||||
const resp = decode(admin.ProviderResponseSchema, bytes);
|
||||
return JSON.parse(resp.bodyJson || "{}");
|
||||
}
|
||||
|
||||
export async function updateProvider(id: string, body: unknown): Promise<unknown> {
|
||||
const msgBody = encode(admin.UpdateProviderRequestSchema, { id, bodyJson: JSON.stringify(body) });
|
||||
const bytes = await grpcRequest(
|
||||
ADMIN_RPC_URL,
|
||||
"/admin.SessionAdmin/UpdateProvider",
|
||||
admin.UpdateProviderRequestSchema,
|
||||
admin.ProviderResponseSchema,
|
||||
msgBody,
|
||||
);
|
||||
if (bytes.length === 0) return {};
|
||||
const resp = decode(admin.ProviderResponseSchema, bytes);
|
||||
return JSON.parse(resp.bodyJson || "{}");
|
||||
}
|
||||
|
||||
export async function deleteProvider(id: string): Promise<{ deleted: boolean }> {
|
||||
const msgBody = encode(admin.DeleteProviderRequestSchema, { id });
|
||||
const bytes = await grpcRequest(
|
||||
ADMIN_RPC_URL,
|
||||
"/admin.SessionAdmin/DeleteProvider",
|
||||
admin.DeleteProviderRequestSchema,
|
||||
admin.DeleteResponseSchema,
|
||||
msgBody,
|
||||
);
|
||||
if (bytes.length === 0) return { deleted: false };
|
||||
const resp = decode(admin.DeleteResponseSchema, bytes);
|
||||
return { deleted: resp.deleted };
|
||||
}
|
||||
|
||||
// ─── AI: Model CRUD ───────────────────────────────────────────────────────────
|
||||
|
||||
export async function createModel(body: unknown): Promise<unknown> {
|
||||
const msgBody = encode(admin.CreateModelRequestSchema, { bodyJson: JSON.stringify(body) });
|
||||
const bytes = await grpcRequest(
|
||||
ADMIN_RPC_URL,
|
||||
"/admin.SessionAdmin/CreateModel",
|
||||
admin.CreateModelRequestSchema,
|
||||
admin.ModelResponseSchema,
|
||||
msgBody,
|
||||
);
|
||||
if (bytes.length === 0) return {};
|
||||
const resp = decode(admin.ModelResponseSchema, bytes);
|
||||
return JSON.parse(resp.bodyJson || "{}");
|
||||
}
|
||||
|
||||
export async function updateModel(id: string, body: unknown): Promise<unknown> {
|
||||
const msgBody = encode(admin.UpdateModelRequestSchema, { id, bodyJson: JSON.stringify(body) });
|
||||
const bytes = await grpcRequest(
|
||||
ADMIN_RPC_URL,
|
||||
"/admin.SessionAdmin/UpdateModel",
|
||||
admin.UpdateModelRequestSchema,
|
||||
admin.ModelResponseSchema,
|
||||
msgBody,
|
||||
);
|
||||
if (bytes.length === 0) return {};
|
||||
const resp = decode(admin.ModelResponseSchema, bytes);
|
||||
return JSON.parse(resp.bodyJson || "{}");
|
||||
}
|
||||
|
||||
export async function deleteModel(id: string): Promise<{ deleted: boolean }> {
|
||||
const msgBody = encode(admin.DeleteModelRequestSchema, { id });
|
||||
const bytes = await grpcRequest(
|
||||
ADMIN_RPC_URL,
|
||||
"/admin.SessionAdmin/DeleteModel",
|
||||
admin.DeleteModelRequestSchema,
|
||||
admin.DeleteResponseSchema,
|
||||
msgBody,
|
||||
);
|
||||
if (bytes.length === 0) return { deleted: false };
|
||||
const resp = decode(admin.DeleteResponseSchema, bytes);
|
||||
return { deleted: resp.deleted };
|
||||
}
|
||||
|
||||
// ─── AI: Version CRUD ─────────────────────────────────────────────────────────
|
||||
|
||||
export async function createVersion(body: unknown): Promise<unknown> {
|
||||
const msgBody = encode(admin.CreateVersionRequestSchema, { bodyJson: JSON.stringify(body) });
|
||||
const bytes = await grpcRequest(
|
||||
ADMIN_RPC_URL,
|
||||
"/admin.SessionAdmin/CreateVersion",
|
||||
admin.CreateVersionRequestSchema,
|
||||
admin.VersionResponseSchema,
|
||||
msgBody,
|
||||
);
|
||||
if (bytes.length === 0) return {};
|
||||
const resp = decode(admin.VersionResponseSchema, bytes);
|
||||
return JSON.parse(resp.bodyJson || "{}");
|
||||
}
|
||||
|
||||
export async function updateVersion(id: string, body: unknown): Promise<unknown> {
|
||||
const msgBody = encode(admin.UpdateVersionRequestSchema, { id, bodyJson: JSON.stringify(body) });
|
||||
const bytes = await grpcRequest(
|
||||
ADMIN_RPC_URL,
|
||||
"/admin.SessionAdmin/UpdateVersion",
|
||||
admin.UpdateVersionRequestSchema,
|
||||
admin.VersionResponseSchema,
|
||||
msgBody,
|
||||
);
|
||||
if (bytes.length === 0) return {};
|
||||
const resp = decode(admin.VersionResponseSchema, bytes);
|
||||
return JSON.parse(resp.bodyJson || "{}");
|
||||
}
|
||||
|
||||
export async function deleteVersion(id: string): Promise<{ deleted: boolean }> {
|
||||
const msgBody = encode(admin.DeleteVersionRequestSchema, { id });
|
||||
const bytes = await grpcRequest(
|
||||
ADMIN_RPC_URL,
|
||||
"/admin.SessionAdmin/DeleteVersion",
|
||||
admin.DeleteVersionRequestSchema,
|
||||
admin.DeleteResponseSchema,
|
||||
msgBody,
|
||||
);
|
||||
if (bytes.length === 0) return { deleted: false };
|
||||
const resp = decode(admin.DeleteResponseSchema, bytes);
|
||||
return { deleted: resp.deleted };
|
||||
}
|
||||
|
||||
// ─── AI: Pricing Update ───────────────────────────────────────────────────────
|
||||
|
||||
export async function updatePricing(id: string, body: unknown): Promise<unknown> {
|
||||
const msgBody = encode(admin.UpdatePricingRequestSchema, { id, bodyJson: JSON.stringify(body) });
|
||||
const bytes = await grpcRequest(
|
||||
ADMIN_RPC_URL,
|
||||
"/admin.SessionAdmin/UpdatePricing",
|
||||
admin.UpdatePricingRequestSchema,
|
||||
admin.PricingResponseSchema,
|
||||
msgBody,
|
||||
);
|
||||
if (bytes.length === 0) return {};
|
||||
const resp = decode(admin.PricingResponseSchema, bytes);
|
||||
return JSON.parse(resp.bodyJson || "{}");
|
||||
}
|
||||
@ -1,225 +0,0 @@
|
||||
// @generated by protoc-gen-connect-es v0.13.0
|
||||
// @generated from file proto/admin.proto (package admin, syntax proto3)
|
||||
/* eslint-disable */
|
||||
// @ts-nocheck
|
||||
|
||||
import { CheckAlertsRequest, CheckAlertsResponse, CreateModelRequest, CreateProviderRequest, CreateVersionRequest, DeleteModelRequest, DeleteProviderRequest, DeleteResponse, DeleteVersionRequest, ExportMetricsCsvRequest, ExportMetricsCsvResponse, GetMetricsRequest, GetMetricsResponse, GetUserInfoRequest, GetUserInfoResponse, GetUserStatusRequest, GetUserStatusResponse, GetWorkspaceOnlineUsersRequest, GetWorkspaceOnlineUsersResponse, IsUserOnlineRequest, IsUserOnlineResponse, KickUserFromWorkspaceRequest, KickUserFromWorkspaceResponse, KickUserRequest, KickUserResponse, ListUserSessionsRequest, ListUserSessionsResponse, ListWorkspaceSessionsRequest, ListWorkspaceSessionsResponse, ModelResponse, PricingResponse, ProviderResponse, SyncModelsRequest, SyncModelsResponse, UpdateModelRequest, UpdatePricingRequest, UpdateProviderRequest, UpdateVersionRequest, VersionResponse } from "./admin_pb.js";
|
||||
import { MethodKind } from "@bufbuild/protobuf";
|
||||
|
||||
/**
|
||||
* @generated from service admin.SessionAdmin
|
||||
*/
|
||||
export declare const SessionAdmin: {
|
||||
readonly typeName: "admin.SessionAdmin",
|
||||
readonly methods: {
|
||||
/**
|
||||
* @generated from rpc admin.SessionAdmin.ListWorkspaceSessions
|
||||
*/
|
||||
readonly listWorkspaceSessions: {
|
||||
readonly name: "ListWorkspaceSessions",
|
||||
readonly I: typeof ListWorkspaceSessionsRequest,
|
||||
readonly O: typeof ListWorkspaceSessionsResponse,
|
||||
readonly kind: MethodKind.Unary,
|
||||
},
|
||||
/**
|
||||
* @generated from rpc admin.SessionAdmin.ListUserSessions
|
||||
*/
|
||||
readonly listUserSessions: {
|
||||
readonly name: "ListUserSessions",
|
||||
readonly I: typeof ListUserSessionsRequest,
|
||||
readonly O: typeof ListUserSessionsResponse,
|
||||
readonly kind: MethodKind.Unary,
|
||||
},
|
||||
/**
|
||||
* @generated from rpc admin.SessionAdmin.KickUserFromWorkspace
|
||||
*/
|
||||
readonly kickUserFromWorkspace: {
|
||||
readonly name: "KickUserFromWorkspace",
|
||||
readonly I: typeof KickUserFromWorkspaceRequest,
|
||||
readonly O: typeof KickUserFromWorkspaceResponse,
|
||||
readonly kind: MethodKind.Unary,
|
||||
},
|
||||
/**
|
||||
* @generated from rpc admin.SessionAdmin.KickUser
|
||||
*/
|
||||
readonly kickUser: {
|
||||
readonly name: "KickUser",
|
||||
readonly I: typeof KickUserRequest,
|
||||
readonly O: typeof KickUserResponse,
|
||||
readonly kind: MethodKind.Unary,
|
||||
},
|
||||
/**
|
||||
* @generated from rpc admin.SessionAdmin.GetUserStatus
|
||||
*/
|
||||
readonly getUserStatus: {
|
||||
readonly name: "GetUserStatus",
|
||||
readonly I: typeof GetUserStatusRequest,
|
||||
readonly O: typeof GetUserStatusResponse,
|
||||
readonly kind: MethodKind.Unary,
|
||||
},
|
||||
/**
|
||||
* @generated from rpc admin.SessionAdmin.GetUserInfo
|
||||
*/
|
||||
readonly getUserInfo: {
|
||||
readonly name: "GetUserInfo",
|
||||
readonly I: typeof GetUserInfoRequest,
|
||||
readonly O: typeof GetUserInfoResponse,
|
||||
readonly kind: MethodKind.Unary,
|
||||
},
|
||||
/**
|
||||
* @generated from rpc admin.SessionAdmin.GetWorkspaceOnlineUsers
|
||||
*/
|
||||
readonly getWorkspaceOnlineUsers: {
|
||||
readonly name: "GetWorkspaceOnlineUsers",
|
||||
readonly I: typeof GetWorkspaceOnlineUsersRequest,
|
||||
readonly O: typeof GetWorkspaceOnlineUsersResponse,
|
||||
readonly kind: MethodKind.Unary,
|
||||
},
|
||||
/**
|
||||
* @generated from rpc admin.SessionAdmin.IsUserOnline
|
||||
*/
|
||||
readonly isUserOnline: {
|
||||
readonly name: "IsUserOnline",
|
||||
readonly I: typeof IsUserOnlineRequest,
|
||||
readonly O: typeof IsUserOnlineResponse,
|
||||
readonly kind: MethodKind.Unary,
|
||||
},
|
||||
/**
|
||||
* @generated from rpc admin.SessionAdmin.GetMetrics
|
||||
*/
|
||||
readonly getMetrics: {
|
||||
readonly name: "GetMetrics",
|
||||
readonly I: typeof GetMetricsRequest,
|
||||
readonly O: typeof GetMetricsResponse,
|
||||
readonly kind: MethodKind.Unary,
|
||||
},
|
||||
/**
|
||||
* @generated from rpc admin.SessionAdmin.ExportMetricsCsv
|
||||
*/
|
||||
readonly exportMetricsCsv: {
|
||||
readonly name: "ExportMetricsCsv",
|
||||
readonly I: typeof ExportMetricsCsvRequest,
|
||||
readonly O: typeof ExportMetricsCsvResponse,
|
||||
readonly kind: MethodKind.Unary,
|
||||
},
|
||||
/**
|
||||
* AI
|
||||
*
|
||||
* @generated from rpc admin.SessionAdmin.SyncModels
|
||||
*/
|
||||
readonly syncModels: {
|
||||
readonly name: "SyncModels",
|
||||
readonly I: typeof SyncModelsRequest,
|
||||
readonly O: typeof SyncModelsResponse,
|
||||
readonly kind: MethodKind.Unary,
|
||||
},
|
||||
/**
|
||||
* @generated from rpc admin.SessionAdmin.CheckAlerts
|
||||
*/
|
||||
readonly checkAlerts: {
|
||||
readonly name: "CheckAlerts",
|
||||
readonly I: typeof CheckAlertsRequest,
|
||||
readonly O: typeof CheckAlertsResponse,
|
||||
readonly kind: MethodKind.Unary,
|
||||
},
|
||||
/**
|
||||
* AI Provider
|
||||
*
|
||||
* @generated from rpc admin.SessionAdmin.CreateProvider
|
||||
*/
|
||||
readonly createProvider: {
|
||||
readonly name: "CreateProvider",
|
||||
readonly I: typeof CreateProviderRequest,
|
||||
readonly O: typeof ProviderResponse,
|
||||
readonly kind: MethodKind.Unary,
|
||||
},
|
||||
/**
|
||||
* @generated from rpc admin.SessionAdmin.UpdateProvider
|
||||
*/
|
||||
readonly updateProvider: {
|
||||
readonly name: "UpdateProvider",
|
||||
readonly I: typeof UpdateProviderRequest,
|
||||
readonly O: typeof ProviderResponse,
|
||||
readonly kind: MethodKind.Unary,
|
||||
},
|
||||
/**
|
||||
* @generated from rpc admin.SessionAdmin.DeleteProvider
|
||||
*/
|
||||
readonly deleteProvider: {
|
||||
readonly name: "DeleteProvider",
|
||||
readonly I: typeof DeleteProviderRequest,
|
||||
readonly O: typeof DeleteResponse,
|
||||
readonly kind: MethodKind.Unary,
|
||||
},
|
||||
/**
|
||||
* AI Model
|
||||
*
|
||||
* @generated from rpc admin.SessionAdmin.CreateModel
|
||||
*/
|
||||
readonly createModel: {
|
||||
readonly name: "CreateModel",
|
||||
readonly I: typeof CreateModelRequest,
|
||||
readonly O: typeof ModelResponse,
|
||||
readonly kind: MethodKind.Unary,
|
||||
},
|
||||
/**
|
||||
* @generated from rpc admin.SessionAdmin.UpdateModel
|
||||
*/
|
||||
readonly updateModel: {
|
||||
readonly name: "UpdateModel",
|
||||
readonly I: typeof UpdateModelRequest,
|
||||
readonly O: typeof ModelResponse,
|
||||
readonly kind: MethodKind.Unary,
|
||||
},
|
||||
/**
|
||||
* @generated from rpc admin.SessionAdmin.DeleteModel
|
||||
*/
|
||||
readonly deleteModel: {
|
||||
readonly name: "DeleteModel",
|
||||
readonly I: typeof DeleteModelRequest,
|
||||
readonly O: typeof DeleteResponse,
|
||||
readonly kind: MethodKind.Unary,
|
||||
},
|
||||
/**
|
||||
* AI Version
|
||||
*
|
||||
* @generated from rpc admin.SessionAdmin.CreateVersion
|
||||
*/
|
||||
readonly createVersion: {
|
||||
readonly name: "CreateVersion",
|
||||
readonly I: typeof CreateVersionRequest,
|
||||
readonly O: typeof VersionResponse,
|
||||
readonly kind: MethodKind.Unary,
|
||||
},
|
||||
/**
|
||||
* @generated from rpc admin.SessionAdmin.UpdateVersion
|
||||
*/
|
||||
readonly updateVersion: {
|
||||
readonly name: "UpdateVersion",
|
||||
readonly I: typeof UpdateVersionRequest,
|
||||
readonly O: typeof VersionResponse,
|
||||
readonly kind: MethodKind.Unary,
|
||||
},
|
||||
/**
|
||||
* @generated from rpc admin.SessionAdmin.DeleteVersion
|
||||
*/
|
||||
readonly deleteVersion: {
|
||||
readonly name: "DeleteVersion",
|
||||
readonly I: typeof DeleteVersionRequest,
|
||||
readonly O: typeof DeleteResponse,
|
||||
readonly kind: MethodKind.Unary,
|
||||
},
|
||||
/**
|
||||
* AI Pricing
|
||||
*
|
||||
* @generated from rpc admin.SessionAdmin.UpdatePricing
|
||||
*/
|
||||
readonly updatePricing: {
|
||||
readonly name: "UpdatePricing",
|
||||
readonly I: typeof UpdatePricingRequest,
|
||||
readonly O: typeof PricingResponse,
|
||||
readonly kind: MethodKind.Unary,
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
@ -1,225 +0,0 @@
|
||||
// @generated by protoc-gen-connect-es v0.13.0
|
||||
// @generated from file proto/admin.proto (package admin, syntax proto3)
|
||||
/* eslint-disable */
|
||||
// @ts-nocheck
|
||||
|
||||
import { CheckAlertsRequest, CheckAlertsResponse, CreateModelRequest, CreateProviderRequest, CreateVersionRequest, DeleteModelRequest, DeleteProviderRequest, DeleteResponse, DeleteVersionRequest, ExportMetricsCsvRequest, ExportMetricsCsvResponse, GetMetricsRequest, GetMetricsResponse, GetUserInfoRequest, GetUserInfoResponse, GetUserStatusRequest, GetUserStatusResponse, GetWorkspaceOnlineUsersRequest, GetWorkspaceOnlineUsersResponse, IsUserOnlineRequest, IsUserOnlineResponse, KickUserFromWorkspaceRequest, KickUserFromWorkspaceResponse, KickUserRequest, KickUserResponse, ListUserSessionsRequest, ListUserSessionsResponse, ListWorkspaceSessionsRequest, ListWorkspaceSessionsResponse, ModelResponse, PricingResponse, ProviderResponse, SyncModelsRequest, SyncModelsResponse, UpdateModelRequest, UpdatePricingRequest, UpdateProviderRequest, UpdateVersionRequest, VersionResponse } from "./admin_pb.js";
|
||||
import { MethodKind } from "@bufbuild/protobuf";
|
||||
|
||||
/**
|
||||
* @generated from service admin.SessionAdmin
|
||||
*/
|
||||
export const SessionAdmin = {
|
||||
typeName: "admin.SessionAdmin",
|
||||
methods: {
|
||||
/**
|
||||
* @generated from rpc admin.SessionAdmin.ListWorkspaceSessions
|
||||
*/
|
||||
listWorkspaceSessions: {
|
||||
name: "ListWorkspaceSessions",
|
||||
I: ListWorkspaceSessionsRequest,
|
||||
O: ListWorkspaceSessionsResponse,
|
||||
kind: MethodKind.Unary,
|
||||
},
|
||||
/**
|
||||
* @generated from rpc admin.SessionAdmin.ListUserSessions
|
||||
*/
|
||||
listUserSessions: {
|
||||
name: "ListUserSessions",
|
||||
I: ListUserSessionsRequest,
|
||||
O: ListUserSessionsResponse,
|
||||
kind: MethodKind.Unary,
|
||||
},
|
||||
/**
|
||||
* @generated from rpc admin.SessionAdmin.KickUserFromWorkspace
|
||||
*/
|
||||
kickUserFromWorkspace: {
|
||||
name: "KickUserFromWorkspace",
|
||||
I: KickUserFromWorkspaceRequest,
|
||||
O: KickUserFromWorkspaceResponse,
|
||||
kind: MethodKind.Unary,
|
||||
},
|
||||
/**
|
||||
* @generated from rpc admin.SessionAdmin.KickUser
|
||||
*/
|
||||
kickUser: {
|
||||
name: "KickUser",
|
||||
I: KickUserRequest,
|
||||
O: KickUserResponse,
|
||||
kind: MethodKind.Unary,
|
||||
},
|
||||
/**
|
||||
* @generated from rpc admin.SessionAdmin.GetUserStatus
|
||||
*/
|
||||
getUserStatus: {
|
||||
name: "GetUserStatus",
|
||||
I: GetUserStatusRequest,
|
||||
O: GetUserStatusResponse,
|
||||
kind: MethodKind.Unary,
|
||||
},
|
||||
/**
|
||||
* @generated from rpc admin.SessionAdmin.GetUserInfo
|
||||
*/
|
||||
getUserInfo: {
|
||||
name: "GetUserInfo",
|
||||
I: GetUserInfoRequest,
|
||||
O: GetUserInfoResponse,
|
||||
kind: MethodKind.Unary,
|
||||
},
|
||||
/**
|
||||
* @generated from rpc admin.SessionAdmin.GetWorkspaceOnlineUsers
|
||||
*/
|
||||
getWorkspaceOnlineUsers: {
|
||||
name: "GetWorkspaceOnlineUsers",
|
||||
I: GetWorkspaceOnlineUsersRequest,
|
||||
O: GetWorkspaceOnlineUsersResponse,
|
||||
kind: MethodKind.Unary,
|
||||
},
|
||||
/**
|
||||
* @generated from rpc admin.SessionAdmin.IsUserOnline
|
||||
*/
|
||||
isUserOnline: {
|
||||
name: "IsUserOnline",
|
||||
I: IsUserOnlineRequest,
|
||||
O: IsUserOnlineResponse,
|
||||
kind: MethodKind.Unary,
|
||||
},
|
||||
/**
|
||||
* @generated from rpc admin.SessionAdmin.GetMetrics
|
||||
*/
|
||||
getMetrics: {
|
||||
name: "GetMetrics",
|
||||
I: GetMetricsRequest,
|
||||
O: GetMetricsResponse,
|
||||
kind: MethodKind.Unary,
|
||||
},
|
||||
/**
|
||||
* @generated from rpc admin.SessionAdmin.ExportMetricsCsv
|
||||
*/
|
||||
exportMetricsCsv: {
|
||||
name: "ExportMetricsCsv",
|
||||
I: ExportMetricsCsvRequest,
|
||||
O: ExportMetricsCsvResponse,
|
||||
kind: MethodKind.Unary,
|
||||
},
|
||||
/**
|
||||
* AI
|
||||
*
|
||||
* @generated from rpc admin.SessionAdmin.SyncModels
|
||||
*/
|
||||
syncModels: {
|
||||
name: "SyncModels",
|
||||
I: SyncModelsRequest,
|
||||
O: SyncModelsResponse,
|
||||
kind: MethodKind.Unary,
|
||||
},
|
||||
/**
|
||||
* @generated from rpc admin.SessionAdmin.CheckAlerts
|
||||
*/
|
||||
checkAlerts: {
|
||||
name: "CheckAlerts",
|
||||
I: CheckAlertsRequest,
|
||||
O: CheckAlertsResponse,
|
||||
kind: MethodKind.Unary,
|
||||
},
|
||||
/**
|
||||
* AI Provider
|
||||
*
|
||||
* @generated from rpc admin.SessionAdmin.CreateProvider
|
||||
*/
|
||||
createProvider: {
|
||||
name: "CreateProvider",
|
||||
I: CreateProviderRequest,
|
||||
O: ProviderResponse,
|
||||
kind: MethodKind.Unary,
|
||||
},
|
||||
/**
|
||||
* @generated from rpc admin.SessionAdmin.UpdateProvider
|
||||
*/
|
||||
updateProvider: {
|
||||
name: "UpdateProvider",
|
||||
I: UpdateProviderRequest,
|
||||
O: ProviderResponse,
|
||||
kind: MethodKind.Unary,
|
||||
},
|
||||
/**
|
||||
* @generated from rpc admin.SessionAdmin.DeleteProvider
|
||||
*/
|
||||
deleteProvider: {
|
||||
name: "DeleteProvider",
|
||||
I: DeleteProviderRequest,
|
||||
O: DeleteResponse,
|
||||
kind: MethodKind.Unary,
|
||||
},
|
||||
/**
|
||||
* AI Model
|
||||
*
|
||||
* @generated from rpc admin.SessionAdmin.CreateModel
|
||||
*/
|
||||
createModel: {
|
||||
name: "CreateModel",
|
||||
I: CreateModelRequest,
|
||||
O: ModelResponse,
|
||||
kind: MethodKind.Unary,
|
||||
},
|
||||
/**
|
||||
* @generated from rpc admin.SessionAdmin.UpdateModel
|
||||
*/
|
||||
updateModel: {
|
||||
name: "UpdateModel",
|
||||
I: UpdateModelRequest,
|
||||
O: ModelResponse,
|
||||
kind: MethodKind.Unary,
|
||||
},
|
||||
/**
|
||||
* @generated from rpc admin.SessionAdmin.DeleteModel
|
||||
*/
|
||||
deleteModel: {
|
||||
name: "DeleteModel",
|
||||
I: DeleteModelRequest,
|
||||
O: DeleteResponse,
|
||||
kind: MethodKind.Unary,
|
||||
},
|
||||
/**
|
||||
* AI Version
|
||||
*
|
||||
* @generated from rpc admin.SessionAdmin.CreateVersion
|
||||
*/
|
||||
createVersion: {
|
||||
name: "CreateVersion",
|
||||
I: CreateVersionRequest,
|
||||
O: VersionResponse,
|
||||
kind: MethodKind.Unary,
|
||||
},
|
||||
/**
|
||||
* @generated from rpc admin.SessionAdmin.UpdateVersion
|
||||
*/
|
||||
updateVersion: {
|
||||
name: "UpdateVersion",
|
||||
I: UpdateVersionRequest,
|
||||
O: VersionResponse,
|
||||
kind: MethodKind.Unary,
|
||||
},
|
||||
/**
|
||||
* @generated from rpc admin.SessionAdmin.DeleteVersion
|
||||
*/
|
||||
deleteVersion: {
|
||||
name: "DeleteVersion",
|
||||
I: DeleteVersionRequest,
|
||||
O: DeleteResponse,
|
||||
kind: MethodKind.Unary,
|
||||
},
|
||||
/**
|
||||
* AI Pricing
|
||||
*
|
||||
* @generated from rpc admin.SessionAdmin.UpdatePricing
|
||||
*/
|
||||
updatePricing: {
|
||||
name: "UpdatePricing",
|
||||
I: UpdatePricingRequest,
|
||||
O: PricingResponse,
|
||||
kind: MethodKind.Unary,
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
1017
admin/src/lib/adminrpc/generated/proto/admin_pb.d.ts
vendored
1017
admin/src/lib/adminrpc/generated/proto/admin_pb.d.ts
vendored
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
@ -167,8 +167,7 @@ export async function touchSession(sessionId: string): Promise<void> {
|
||||
const state = await loadSession(sessionId);
|
||||
if (!state) return;
|
||||
state["session:last_active"] = new Date().toISOString();
|
||||
const { saveSession } = await import("@/lib/redis");
|
||||
await saveSession(sessionId, state, ADMIN_SESSION_TTL);
|
||||
await refreshSessionTtl(sessionId, ADMIN_SESSION_TTL);
|
||||
}
|
||||
|
||||
// ============ 登出 ============
|
||||
|
||||
@ -30,16 +30,12 @@ async function runReport() {
|
||||
|
||||
// Call generate endpoint internally (server-side fetch, no auth needed for cron)
|
||||
const baseUrl = process.env.NEXT_PUBLIC_APP_URL || `http://localhost:${process.env.PORT || 3000}`;
|
||||
const headers: Record<string, string> = {
|
||||
"Content-Type": "application/json",
|
||||
"x-cron-internal": "true",
|
||||
};
|
||||
if (process.env.DAILY_REPORT_CRON_SECRET) {
|
||||
headers["x-cron-secret"] = process.env.DAILY_REPORT_CRON_SECRET;
|
||||
}
|
||||
const res = await fetch(`${baseUrl}/api/admin/daily-report/generate`, {
|
||||
method: "POST",
|
||||
headers,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"x-cron-internal": "true", // internal marker, not x-cron-secret
|
||||
},
|
||||
});
|
||||
const data = await res.json().catch(() => ({}));
|
||||
console.log("[daily-report-cron] Result:", res.status, data);
|
||||
|
||||
@ -5,29 +5,29 @@
|
||||
|
||||
// 数据库
|
||||
export const DATABASE_URL =
|
||||
process.env.DATABASE_URL || "postgresql://localhost:5432/code";
|
||||
process.env.DATABASE_URL || "postgresql://localhost:5432/code";
|
||||
|
||||
// Redis
|
||||
export const REDIS_URL = process.env.REDIS_URL || "redis://localhost:6379";
|
||||
// Redis Cluster 节点列表(逗号分隔,用于 ioredis cluster 模式)
|
||||
export const REDIS_CLUSTER_URLS = (process.env.REDIS_CLUSTER_URLS || "")
|
||||
.split(",")
|
||||
.map((u) => u.trim())
|
||||
.filter(Boolean);
|
||||
.split(",")
|
||||
.map((u) => u.trim())
|
||||
.filter(Boolean);
|
||||
|
||||
// Session
|
||||
export const ADMIN_SESSION_COOKIE_NAME =
|
||||
process.env.ADMIN_SESSION_COOKIE_NAME || "admin_session";
|
||||
process.env.ADMIN_SESSION_COOKIE_NAME || "admin_session";
|
||||
export const ADMIN_SESSION_TTL = parseInt(
|
||||
process.env.ADMIN_SESSION_TTL || "604800",
|
||||
10
|
||||
process.env.ADMIN_SESSION_TTL || "604800",
|
||||
10
|
||||
); // 7 days
|
||||
|
||||
// 超级管理员(环境变量配置)
|
||||
export const ADMIN_SUPER_USERNAME = process.env.ADMIN_SUPER_USERNAME || "";
|
||||
export const ADMIN_SUPER_PASSWORD = process.env.ADMIN_SUPER_PASSWORD || "";
|
||||
export const ADMIN_SUPER_PASSWORD_HASH =
|
||||
process.env.ADMIN_SUPER_PASSWORD_HASH || "";
|
||||
process.env.ADMIN_SUPER_PASSWORD_HASH || "";
|
||||
|
||||
// OIDC
|
||||
export const OIDC_ENABLED = process.env.OIDC_ENABLED === "true";
|
||||
@ -35,21 +35,21 @@ export const OIDC_ISSUER = process.env.OIDC_ISSUER || "";
|
||||
export const OIDC_CLIENT_ID = process.env.OIDC_CLIENT_ID || "";
|
||||
export const OIDC_CLIENT_SECRET = process.env.OIDC_CLIENT_SECRET || "";
|
||||
export const OIDC_REDIRECT_URI =
|
||||
process.env.OIDC_REDIRECT_URI ||
|
||||
"http://localhost:3000/api/auth/oidc/callback";
|
||||
process.env.OIDC_REDIRECT_URI ||
|
||||
"http://localhost:3000/api/auth/oidc/callback";
|
||||
|
||||
// Cookie 安全
|
||||
export const COOKIE_SECURE = process.env.COOKIE_SECURE === "true";
|
||||
export const COOKIE_SAME_SITE =
|
||||
(process.env.COOKIE_SAME_SITE as "strict" | "lax" | "none") || "lax";
|
||||
(process.env.COOKIE_SAME_SITE as "strict" | "lax" | "none") || "lax";
|
||||
|
||||
// Rust 主应用集成
|
||||
export const RUST_BACKEND_URL =
|
||||
process.env.RUST_BACKEND_URL || "http://localhost:3000";
|
||||
process.env.RUST_BACKEND_URL || "http://localhost:3000";
|
||||
export const ADMIN_API_SHARED_KEY =
|
||||
process.env.ADMIN_API_SHARED_KEY || "";
|
||||
process.env.ADMIN_API_SHARED_KEY || "";
|
||||
|
||||
// adminrpc HTTP 服务地址(k8s 内部默认地址)
|
||||
// 在 Kubernetes 环境中默认使用 Service DNS,在本地开发时覆盖为 localhost:9091
|
||||
export const ADMIN_RPC_URL =
|
||||
process.env.ADMIN_RPC_URL || "http://adminrpc.gitdataai.svc.cluster.local:9091";
|
||||
process.env.ADMIN_RPC_URL || "http://adminrpc.admin.svc.cluster.local:9091";
|
||||
|
||||
@ -201,12 +201,15 @@ export async function listUsers(
|
||||
const pageSize = options.pageSize ?? 20;
|
||||
const offset = (page - 1) * pageSize;
|
||||
|
||||
const params: unknown[] = [
|
||||
...(options.search ? [`%${options.search}%`] : []),
|
||||
pageSize,
|
||||
offset,
|
||||
];
|
||||
const whereClause = options.search ? `WHERE username ILIKE $1` : "";
|
||||
const params: unknown[] = [pageSize, offset];
|
||||
let whereClause = "";
|
||||
let paramIdx = 3;
|
||||
|
||||
if (options.search) {
|
||||
whereClause = `WHERE username ILIKE $1`;
|
||||
params.unshift(`%${options.search}%`);
|
||||
paramIdx = 3;
|
||||
}
|
||||
|
||||
const countParams = options.search ? [`%${options.search}%`] : [];
|
||||
const countResult = await query<{ count: string }>(
|
||||
|
||||
@ -10,7 +10,7 @@ import { REDIS_URL, REDIS_CLUSTER_URLS } from "./env";
|
||||
// Admin 专用的 Redis 前缀
|
||||
const ADMIN_PREFIX = "admin:session:";
|
||||
// 平台用户 Session 前缀(与 Rust 主应用一致)
|
||||
const PLATFORM_SESSION_PREFIX = "user:";
|
||||
const PLATFORM_SESSION_PREFIX = "session:user_uid:";
|
||||
|
||||
let redis: Redis | null = null;
|
||||
|
||||
|
||||
@ -124,7 +124,6 @@ export async function middleware(req: NextRequest) {
|
||||
permissions = tokenResult.permissions || [];
|
||||
headers.set("x-admin-auth-type", "token");
|
||||
headers.set("x-admin-token-id", String(tokenResult.tokenId));
|
||||
headers.set("x-admin-permissions", permissions.join(","));
|
||||
} else {
|
||||
// 回退到 Session 认证
|
||||
const cookieHeader = req.headers.get("cookie");
|
||||
|
||||
@ -1,11 +1,11 @@
|
||||
use std::net::SocketAddr;
|
||||
use actix_web::{web, App as ActixApp, HttpResponse, HttpServer};
|
||||
use anyhow::Context as _;
|
||||
use clap::Parser;
|
||||
use config::AppConfig;
|
||||
use deadpool_redis::{cluster, Runtime};
|
||||
use rpc::admin::server::{serve, DEFAULT_GRPC_PORT};
|
||||
use session_manager::{SessionManager, SessionStorage};
|
||||
use std::net::SocketAddr;
|
||||
use rpc::admin::server::{serve, DEFAULT_GRPC_PORT};
|
||||
use uuid::Uuid;
|
||||
|
||||
mod args;
|
||||
@ -24,8 +24,9 @@ async fn main() -> anyhow::Result<()> {
|
||||
.unwrap_or_else(|| format!("0.0.0.0:{}", DEFAULT_GRPC_PORT).parse())
|
||||
.context("invalid grpc bind address")?;
|
||||
|
||||
// Admin HTTP port is gRPC port + 1 (e.g., 9091)
|
||||
let admin_port: u16 = args.http_port.unwrap_or(grpc_addr.port() + 1);
|
||||
let admin_addr: SocketAddr = format!("0.0.0.0:{}", admin_port).parse()?;
|
||||
let admin_addr: SocketAddr = format!("0.0.0.0:{}", admin_port).parse().unwrap();
|
||||
|
||||
tracing::info!(
|
||||
app_name = %cfg.app_name().unwrap_or_default(),
|
||||
@ -34,25 +35,20 @@ async fn main() -> anyhow::Result<()> {
|
||||
"Starting admin RPC server"
|
||||
);
|
||||
|
||||
// ── OTLP tracing ─────────────────────────────────────────────────────────
|
||||
let _otel_guard = if cfg.otel_enabled().unwrap_or(false) {
|
||||
let endpoint = cfg
|
||||
.otel_endpoint()
|
||||
.unwrap_or_else(|_| "http://localhost:4317".to_string());
|
||||
let service_name = cfg
|
||||
.otel_service_name()
|
||||
.unwrap_or_else(|_| "adminrpc".to_string());
|
||||
let service_version = cfg
|
||||
.otel_service_version()
|
||||
.unwrap_or_else(|_| "0.1.0".to_string());
|
||||
let endpoint = cfg.otel_endpoint().unwrap_or_else(|_| "http://localhost:4317".to_string());
|
||||
let service_name = cfg.otel_service_name().unwrap_or_else(|_| "adminrpc".to_string());
|
||||
let service_version = cfg.otel_service_version().unwrap_or_else(|_| "0.1.0".to_string());
|
||||
tracing::info!(endpoint = %endpoint, service = %service_name, "OTLP tracing enabled");
|
||||
let guard =
|
||||
observability::init_otlp(&endpoint, &service_name, &service_version, &log_level)
|
||||
.map_err(|e| anyhow::anyhow!("OTLP init failed: {}", e))?;
|
||||
let guard = observability::init_otlp(&endpoint, &service_name, &service_version, &log_level)
|
||||
.map_err(|e| anyhow::anyhow!("OTLP init failed: {}", e))?;
|
||||
guard
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// Redis connection pool
|
||||
let redis_url = cfg.redis_url()?;
|
||||
tracing::info!(redis_url = %redis_url, "Connecting to Redis");
|
||||
let manager = cluster::Manager::new(vec![redis_url.clone()], false)
|
||||
@ -69,6 +65,7 @@ async fn main() -> anyhow::Result<()> {
|
||||
let storage = SessionStorage::new(pool.clone());
|
||||
let session_manager = SessionManager::new(storage);
|
||||
|
||||
// Spawn gRPC server in background
|
||||
let sm_for_grpc = session_manager.clone();
|
||||
let grpc_handle = tokio::spawn(async move {
|
||||
if let Err(e) = serve(grpc_addr, sm_for_grpc).await {
|
||||
@ -76,6 +73,7 @@ async fn main() -> anyhow::Result<()> {
|
||||
}
|
||||
});
|
||||
|
||||
// Start HTTP REST server
|
||||
let http_handle = tokio::spawn(async move {
|
||||
let pool_for_http = pool.clone();
|
||||
let sm_for_http = session_manager.clone();
|
||||
@ -88,35 +86,15 @@ async fn main() -> anyhow::Result<()> {
|
||||
.route("/admin/metrics/export", web::get().to(metrics_export))
|
||||
.service(
|
||||
web::scope("/api/admin")
|
||||
.route(
|
||||
"/sessions/workspace/{workspace_id}",
|
||||
web::get().to(list_workspace_sessions),
|
||||
)
|
||||
.route(
|
||||
"/sessions/user/{user_id}",
|
||||
web::get().to(list_user_sessions),
|
||||
)
|
||||
.route(
|
||||
"/sessions/user/{user_id}/status",
|
||||
web::get().to(get_user_status),
|
||||
)
|
||||
.route(
|
||||
"/sessions/user/{user_id}/info",
|
||||
web::get().to(get_user_info),
|
||||
)
|
||||
.route(
|
||||
"/sessions/workspace/{workspace_id}/online-users",
|
||||
web::get().to(get_workspace_online_users),
|
||||
)
|
||||
.route(
|
||||
"/sessions/user/{user_id}/online",
|
||||
web::get().to(is_user_online),
|
||||
)
|
||||
// Sessions
|
||||
.route("/sessions/workspace/{workspace_id}", web::get().to(list_workspace_sessions))
|
||||
.route("/sessions/user/{user_id}", web::get().to(list_user_sessions))
|
||||
.route("/sessions/user/{user_id}/status", web::get().to(get_user_status))
|
||||
.route("/sessions/user/{user_id}/info", web::get().to(get_user_info))
|
||||
.route("/sessions/workspace/{workspace_id}/online-users", web::get().to(get_workspace_online_users))
|
||||
.route("/sessions/user/{user_id}/online", web::get().to(is_user_online))
|
||||
.route("/sessions/kick", web::post().to(kick_user))
|
||||
.route(
|
||||
"/sessions/kick-workspace",
|
||||
web::post().to(kick_user_from_workspace),
|
||||
)
|
||||
.route("/sessions/kick-workspace", web::post().to(kick_user_from_workspace))
|
||||
// Metrics
|
||||
.route("/metrics", web::get().to(get_metrics))
|
||||
.route("/metrics/export", web::get().to(metrics_export)),
|
||||
@ -153,18 +131,14 @@ async fn metrics_export(pool: web::Data<cluster::Pool>) -> HttpResponse {
|
||||
Ok(csv) => HttpResponse::Ok()
|
||||
.content_type("text/csv; charset=utf-8")
|
||||
.body(csv),
|
||||
Err(e) => {
|
||||
HttpResponse::InternalServerError().json(serde_json::json!({ "error": e.to_string() }))
|
||||
}
|
||||
Err(e) => HttpResponse::InternalServerError().json(serde_json::json!({ "error": e.to_string() })),
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_metrics(pool: web::Data<cluster::Pool>) -> HttpResponse {
|
||||
match observability::query_all_instance_metrics(pool.get_ref(), "", 100).await {
|
||||
Ok(instances) => HttpResponse::Ok().json(instances),
|
||||
Err(e) => {
|
||||
HttpResponse::InternalServerError().json(serde_json::json!({ "error": e.to_string() }))
|
||||
}
|
||||
Err(e) => HttpResponse::InternalServerError().json(serde_json::json!({ "error": e.to_string() })),
|
||||
}
|
||||
}
|
||||
|
||||
@ -178,16 +152,11 @@ async fn list_workspace_sessions(
|
||||
) -> HttpResponse {
|
||||
let workspace_id = match parse_uuid(&path) {
|
||||
Some(id) => id,
|
||||
None => {
|
||||
return HttpResponse::BadRequest()
|
||||
.json(serde_json::json!({ "error": "invalid workspace_id" }));
|
||||
}
|
||||
None => return HttpResponse::BadRequest().json(serde_json::json!({ "error": "invalid workspace_id" })),
|
||||
};
|
||||
match sm.get_workspace_sessions(&workspace_id).await {
|
||||
Ok(sessions) => HttpResponse::Ok().json(sessions),
|
||||
Err(e) => {
|
||||
HttpResponse::InternalServerError().json(serde_json::json!({ "error": e.to_string() }))
|
||||
}
|
||||
Err(e) => HttpResponse::InternalServerError().json(serde_json::json!({ "error": e.to_string() })),
|
||||
}
|
||||
}
|
||||
|
||||
@ -197,50 +166,39 @@ async fn list_user_sessions(
|
||||
) -> HttpResponse {
|
||||
let user_id = match parse_uuid(&path) {
|
||||
Some(id) => id,
|
||||
None => {
|
||||
return HttpResponse::BadRequest()
|
||||
.json(serde_json::json!({ "error": "invalid user_id" }));
|
||||
}
|
||||
None => return HttpResponse::BadRequest().json(serde_json::json!({ "error": "invalid user_id" })),
|
||||
};
|
||||
match sm.get_user_sessions(&user_id).await {
|
||||
Ok(sessions) => HttpResponse::Ok().json(sessions),
|
||||
Err(e) => {
|
||||
HttpResponse::InternalServerError().json(serde_json::json!({ "error": e.to_string() }))
|
||||
}
|
||||
Err(e) => HttpResponse::InternalServerError().json(serde_json::json!({ "error": e.to_string() })),
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_user_status(sm: web::Data<SessionManager>, path: web::Path<String>) -> HttpResponse {
|
||||
async fn get_user_status(
|
||||
sm: web::Data<SessionManager>,
|
||||
path: web::Path<String>,
|
||||
) -> HttpResponse {
|
||||
let user_id = match parse_uuid(&path) {
|
||||
Some(id) => id,
|
||||
None => {
|
||||
return HttpResponse::BadRequest()
|
||||
.json(serde_json::json!({ "error": "invalid user_id" }));
|
||||
}
|
||||
None => return HttpResponse::BadRequest().json(serde_json::json!({ "error": "invalid user_id" })),
|
||||
};
|
||||
match sm.get_user_status(&user_id).await {
|
||||
Ok(status) => {
|
||||
HttpResponse::Ok().json(serde_json::json!({ "status": format!("{:?}", status) }))
|
||||
}
|
||||
Err(e) => {
|
||||
HttpResponse::InternalServerError().json(serde_json::json!({ "error": e.to_string() }))
|
||||
}
|
||||
Ok(status) => HttpResponse::Ok().json(serde_json::json!({ "status": format!("{:?}", status) })),
|
||||
Err(e) => HttpResponse::InternalServerError().json(serde_json::json!({ "error": e.to_string() })),
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_user_info(sm: web::Data<SessionManager>, path: web::Path<String>) -> HttpResponse {
|
||||
async fn get_user_info(
|
||||
sm: web::Data<SessionManager>,
|
||||
path: web::Path<String>,
|
||||
) -> HttpResponse {
|
||||
let user_id = match parse_uuid(&path) {
|
||||
Some(id) => id,
|
||||
None => {
|
||||
return HttpResponse::BadRequest()
|
||||
.json(serde_json::json!({ "error": "invalid user_id" }));
|
||||
}
|
||||
None => return HttpResponse::BadRequest().json(serde_json::json!({ "error": "invalid user_id" })),
|
||||
};
|
||||
match sm.get_user_info(&user_id).await {
|
||||
Ok(info) => HttpResponse::Ok().json(info),
|
||||
Err(e) => {
|
||||
HttpResponse::InternalServerError().json(serde_json::json!({ "error": e.to_string() }))
|
||||
}
|
||||
Err(e) => HttpResponse::InternalServerError().json(serde_json::json!({ "error": e.to_string() })),
|
||||
}
|
||||
}
|
||||
|
||||
@ -250,32 +208,25 @@ async fn get_workspace_online_users(
|
||||
) -> HttpResponse {
|
||||
let workspace_id = match parse_uuid(&path) {
|
||||
Some(id) => id,
|
||||
None => {
|
||||
return HttpResponse::BadRequest()
|
||||
.json(serde_json::json!({ "error": "invalid workspace_id" }));
|
||||
}
|
||||
None => return HttpResponse::BadRequest().json(serde_json::json!({ "error": "invalid workspace_id" })),
|
||||
};
|
||||
match sm.get_workspace_online_users(&workspace_id).await {
|
||||
Ok(user_ids) => HttpResponse::Ok().json(serde_json::json!({ "user_ids": user_ids })),
|
||||
Err(e) => {
|
||||
HttpResponse::InternalServerError().json(serde_json::json!({ "error": e.to_string() }))
|
||||
}
|
||||
Err(e) => HttpResponse::InternalServerError().json(serde_json::json!({ "error": e.to_string() })),
|
||||
}
|
||||
}
|
||||
|
||||
async fn is_user_online(sm: web::Data<SessionManager>, path: web::Path<String>) -> HttpResponse {
|
||||
async fn is_user_online(
|
||||
sm: web::Data<SessionManager>,
|
||||
path: web::Path<String>,
|
||||
) -> HttpResponse {
|
||||
let user_id = match parse_uuid(&path) {
|
||||
Some(id) => id,
|
||||
None => {
|
||||
return HttpResponse::BadRequest()
|
||||
.json(serde_json::json!({ "error": "invalid user_id" }));
|
||||
}
|
||||
None => return HttpResponse::BadRequest().json(serde_json::json!({ "error": "invalid user_id" })),
|
||||
};
|
||||
match sm.is_user_online(&user_id).await {
|
||||
Ok(online) => HttpResponse::Ok().json(serde_json::json!({ "online": online })),
|
||||
Err(e) => {
|
||||
HttpResponse::InternalServerError().json(serde_json::json!({ "error": e.to_string() }))
|
||||
}
|
||||
Err(e) => HttpResponse::InternalServerError().json(serde_json::json!({ "error": e.to_string() })),
|
||||
}
|
||||
}
|
||||
|
||||
@ -290,16 +241,11 @@ async fn kick_user(
|
||||
) -> HttpResponse {
|
||||
let user_id = match parse_uuid(&body.user_id) {
|
||||
Some(id) => id,
|
||||
None => {
|
||||
return HttpResponse::BadRequest()
|
||||
.json(serde_json::json!({ "error": "invalid user_id" }));
|
||||
}
|
||||
None => return HttpResponse::BadRequest().json(serde_json::json!({ "error": "invalid user_id" })),
|
||||
};
|
||||
match sm.kick_user(&user_id).await {
|
||||
Ok(count) => HttpResponse::Ok().json(serde_json::json!({ "kicked_count": count })),
|
||||
Err(e) => {
|
||||
HttpResponse::InternalServerError().json(serde_json::json!({ "error": e.to_string() }))
|
||||
}
|
||||
Err(e) => HttpResponse::InternalServerError().json(serde_json::json!({ "error": e.to_string() })),
|
||||
}
|
||||
}
|
||||
|
||||
@ -315,22 +261,14 @@ async fn kick_user_from_workspace(
|
||||
) -> HttpResponse {
|
||||
let user_id = match parse_uuid(&body.user_id) {
|
||||
Some(id) => id,
|
||||
None => {
|
||||
return HttpResponse::BadRequest()
|
||||
.json(serde_json::json!({ "error": "invalid user_id" }));
|
||||
}
|
||||
None => return HttpResponse::BadRequest().json(serde_json::json!({ "error": "invalid user_id" })),
|
||||
};
|
||||
let workspace_id = match parse_uuid(&body.workspace_id) {
|
||||
Some(id) => id,
|
||||
None => {
|
||||
return HttpResponse::BadRequest()
|
||||
.json(serde_json::json!({ "error": "invalid workspace_id" }));
|
||||
}
|
||||
None => return HttpResponse::BadRequest().json(serde_json::json!({ "error": "invalid workspace_id" })),
|
||||
};
|
||||
match sm.kick_user_from_workspace(&user_id, &workspace_id).await {
|
||||
Ok(count) => HttpResponse::Ok().json(serde_json::json!({ "kicked_count": count })),
|
||||
Err(e) => {
|
||||
HttpResponse::InternalServerError().json(serde_json::json!({ "error": e.to_string() }))
|
||||
}
|
||||
Err(e) => HttpResponse::InternalServerError().json(serde_json::json!({ "error": e.to_string() })),
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
use std::pin::Pin;
|
||||
use std::time::Duration;
|
||||
use async_openai::config::OpenAIConfig;
|
||||
use async_openai::Client;
|
||||
use async_openai::types::chat::{
|
||||
@ -708,28 +707,6 @@ impl ChatService {
|
||||
.await
|
||||
}
|
||||
|
||||
/// Returns true if the error message indicates a transient failure that can be retried.
|
||||
fn is_retryable_tool_error(msg: &str) -> bool {
|
||||
let msg_lower = msg.to_lowercase();
|
||||
// Transient errors: network, timeouts, rate limits, permission issues that may be temporary
|
||||
msg_lower.contains("connection")
|
||||
|| msg_lower.contains("timeout")
|
||||
|| msg_lower.contains("timed out")
|
||||
|| msg_lower.contains("rate limit")
|
||||
|| msg_lower.contains("too many")
|
||||
|| msg_lower.contains("unavailable")
|
||||
|| msg_lower.contains("service unavailable")
|
||||
|| msg_lower.contains("temporarily")
|
||||
|| msg_lower.contains("refused")
|
||||
|| msg_lower.contains("reset")
|
||||
|| msg_lower.contains("broken pipe")
|
||||
|| msg_lower.contains("deadline exceeded")
|
||||
|| msg_lower.contains("try again")
|
||||
|| msg_lower.contains("not found") // DB/Redis transient not-found
|
||||
|| msg_lower.contains("permission denied")
|
||||
|| msg_lower.contains("access denied")
|
||||
}
|
||||
|
||||
/// Process a request using the ReAct (Reasoning + Acting) agent.
|
||||
///
|
||||
/// Unlike the simple loop in `process`, the ReAct agent performs multi-step
|
||||
@ -779,70 +756,27 @@ impl ChatService {
|
||||
let registry = registry.clone();
|
||||
|
||||
Box::pin(async move {
|
||||
let max_retries = 3;
|
||||
let mut last_err = String::new();
|
||||
|
||||
for attempt in 0..=max_retries {
|
||||
let mut ctx = ToolContext::new(db.clone(), cache.clone(), config.clone(), room_id, sender_uid);
|
||||
if let Some(pid) = project_id {
|
||||
ctx = ctx.with_project(pid);
|
||||
}
|
||||
ctx.registry_mut().merge(registry.clone());
|
||||
|
||||
let tool_executor = ToolExecutor::new();
|
||||
let call = ToolCall {
|
||||
id: Uuid::new_v4().to_string(),
|
||||
name: name.clone(),
|
||||
arguments: serde_json::to_string(&args).unwrap_or_else(|_| "{}".into()),
|
||||
};
|
||||
|
||||
match tool_executor.execute_batch(vec![call], &mut ctx).await {
|
||||
Ok(results) => {
|
||||
let result = results.into_iter().next()
|
||||
.ok_or_else(|| "no tool result returned".to_string())?;
|
||||
match result.result {
|
||||
ToolResult::Ok(v) => return Ok(v),
|
||||
ToolResult::Error(msg) => {
|
||||
// Check if error is retryable
|
||||
if attempt < max_retries && Self::is_retryable_tool_error(&msg) {
|
||||
last_err = msg;
|
||||
let backoff_ms = 100u64.saturating_mul(2u64.pow(attempt as u32));
|
||||
tracing::warn!(
|
||||
tool = %name,
|
||||
attempt = attempt + 1,
|
||||
backoff_ms = backoff_ms,
|
||||
error = %last_err,
|
||||
"tool_execute_retry"
|
||||
);
|
||||
tokio::time::sleep(Duration::from_millis(backoff_ms)).await;
|
||||
continue;
|
||||
}
|
||||
// Non-retryable or exhausted retries — pass error to AI as observation
|
||||
return Err(msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
last_err = e.to_string();
|
||||
if attempt < max_retries && Self::is_retryable_tool_error(&last_err) {
|
||||
let backoff_ms = 100u64.saturating_mul(2u64.pow(attempt as u32));
|
||||
tracing::warn!(
|
||||
tool = %name,
|
||||
attempt = attempt + 1,
|
||||
backoff_ms = backoff_ms,
|
||||
error = %last_err,
|
||||
"tool_execute_retry"
|
||||
);
|
||||
tokio::time::sleep(Duration::from_millis(backoff_ms)).await;
|
||||
continue;
|
||||
}
|
||||
return Err(last_err);
|
||||
}
|
||||
}
|
||||
let mut ctx = ToolContext::new(db, cache, config, room_id, sender_uid);
|
||||
if let Some(pid) = project_id {
|
||||
ctx = ctx.with_project(pid);
|
||||
}
|
||||
ctx.registry_mut().merge(registry.clone());
|
||||
|
||||
// Should not reach here, but just in case
|
||||
Err(last_err)
|
||||
let tool_executor = ToolExecutor::new();
|
||||
let call = ToolCall {
|
||||
id: uuid::Uuid::new_v4().to_string(),
|
||||
name,
|
||||
arguments: serde_json::to_string(&args).unwrap_or_else(|_| "{}".into()),
|
||||
};
|
||||
let results: Vec<_> = tool_executor
|
||||
.execute_batch(vec![call], &mut ctx)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
let result = results.into_iter().next().ok_or_else(|| "no result".to_string())?;
|
||||
match result.result {
|
||||
ToolResult::Ok(v) => Ok(v),
|
||||
ToolResult::Error(msg) => Err(msg),
|
||||
}
|
||||
}) as Pin<Box<dyn std::future::Future<Output = std::result::Result<serde_json::Value, String>> + Send>>
|
||||
});
|
||||
|
||||
|
||||
@ -57,18 +57,6 @@ You must respond in JSON format:
|
||||
- Chain multiple tool calls if a single call is insufficient.
|
||||
- After each tool result, re-evaluate whether more data is needed before providing a final answer.
|
||||
|
||||
## Handling Tool Errors
|
||||
|
||||
When a tool returns an error observation (a JSON object with an "error" field):
|
||||
- **Transient errors** (e.g., "connection refused", "not found", "timeout", "rate limit", "permission denied"): Retry with adjusted arguments, or try an alternative tool.
|
||||
- **Permanent errors** (e.g., "invalid arguments", "tool not registered"): Do NOT retry — acknowledge the error and try a different approach or reformulate your question.
|
||||
- **Empty results** (e.g., "no issues found"): This is NOT an error — continue with the next logical tool or provide your answer based on what was found.
|
||||
|
||||
The system automatically retries transient failures up to 3 times with backoff, but you should still:
|
||||
1. Fix any malformed arguments before retrying.
|
||||
2. If the same tool fails twice with the same error, switch to a different approach.
|
||||
3. Always provide a useful answer even if all tools fail — state what you attempted and what went wrong.
|
||||
|
||||
## Principles
|
||||
|
||||
- Be precise and cite specific issue/PR numbers, commit hashes, or message IDs when available.
|
||||
|
||||
@ -432,9 +432,6 @@ use utoipa::OpenApi;
|
||||
crate::user::subscribe::get_subscribers,
|
||||
crate::user::subscribe::get_subscription_count,
|
||||
crate::user::subscribe::get_subscriber_count,
|
||||
crate::user::subscribe::get_following_list,
|
||||
crate::user::user_activity::get_user_activity,
|
||||
crate::user::stars::get_user_stars,
|
||||
crate::user::user_info::get_user_info,
|
||||
// Skill
|
||||
crate::skill::skill_list,
|
||||
@ -626,12 +623,6 @@ use utoipa::OpenApi;
|
||||
service::user::repository::UserReposResponse,
|
||||
service::user::repository::UserReposQuery,
|
||||
service::user::subscribe::SubscriptionInfo,
|
||||
service::user::subscribe::UserCard,
|
||||
service::user::user_activity::UserActivityItem,
|
||||
service::user::user_activity::UserActivityResponse,
|
||||
service::user::stars::RepoStarItem,
|
||||
service::user::stars::ProjectFollowItem,
|
||||
service::user::stars::UserStarsResponse,
|
||||
service::user::user_info::UserInfoExternal,
|
||||
// Workspace
|
||||
service::workspace::init::WorkspaceInitParams,
|
||||
|
||||
@ -6,9 +6,7 @@ pub mod profile;
|
||||
pub mod projects;
|
||||
pub mod repository;
|
||||
pub mod ssh_key;
|
||||
pub mod stars;
|
||||
pub mod subscribe;
|
||||
pub mod user_activity;
|
||||
pub mod user_info;
|
||||
|
||||
use actix_web::web;
|
||||
@ -88,8 +86,6 @@ pub fn init_user_routes(cfg: &mut web::ServiceConfig) {
|
||||
web::get().to(chpc::get_contribution_heatmap),
|
||||
)
|
||||
.route("/{username}/keys", web::get().to(ssh_key::list_ssh_keys))
|
||||
.route("/{username}/activity", web::get().to(user_activity::get_user_activity))
|
||||
.route("/{username}/stars", web::get().to(stars::get_user_stars))
|
||||
.route(
|
||||
"/{username}/keys/{key_id}",
|
||||
web::get().to(ssh_key::get_ssh_key),
|
||||
@ -122,10 +118,6 @@ pub fn init_user_routes(cfg: &mut web::ServiceConfig) {
|
||||
"/{username}/following/count",
|
||||
web::get().to(subscribe::get_subscription_count),
|
||||
)
|
||||
.route(
|
||||
"/{username}/following",
|
||||
web::get().to(subscribe::get_following_list),
|
||||
)
|
||||
.route(
|
||||
"/{username}/followers/count",
|
||||
web::get().to(subscribe::get_subscriber_count),
|
||||
|
||||
@ -1,25 +0,0 @@
|
||||
use crate::{ApiResponse, error::ApiError};
|
||||
use actix_web::{HttpResponse, Result, web};
|
||||
use service::AppService;
|
||||
use session::Session;
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/users/{username}/stars",
|
||||
params(("username" = String, Path)),
|
||||
responses(
|
||||
(status = 200, description = "Get user stars", body = ApiResponse<service::user::stars::UserStarsResponse>),
|
||||
(status = 401, description = "Unauthorized"),
|
||||
(status = 404, description = "Not found"),
|
||||
),
|
||||
tag = "User"
|
||||
)]
|
||||
pub async fn get_user_stars(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
path: web::Path<String>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
let username = path.into_inner();
|
||||
let resp = service.get_user_stars(session, username).await?;
|
||||
Ok(ApiResponse::ok(resp).to_response())
|
||||
}
|
||||
@ -131,24 +131,3 @@ pub async fn get_subscriber_count(
|
||||
let resp = service.user_get_subscriber_count(session, username).await?;
|
||||
Ok(ApiResponse::ok(serde_json::json!({ "count": resp })).to_response())
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/users/{username}/following",
|
||||
params(("username" = String, Path)),
|
||||
responses(
|
||||
(status = 200, description = "List following users", body = ApiResponse<Vec<service::user::subscribe::UserCard>>),
|
||||
(status = 401, description = "Unauthorized"),
|
||||
(status = 404, description = "Not found"),
|
||||
),
|
||||
tag = "User"
|
||||
)]
|
||||
pub async fn get_following_list(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
path: web::Path<String>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
let username = path.into_inner();
|
||||
let resp = service.user_get_following_list(session, username).await?;
|
||||
Ok(ApiResponse::ok(resp).to_response())
|
||||
}
|
||||
|
||||
@ -1,33 +0,0 @@
|
||||
use crate::{ApiResponse, error::ApiError};
|
||||
use actix_web::{HttpResponse, Result, web};
|
||||
use service::AppService;
|
||||
use service::user::user_activity::UserActivityQuery;
|
||||
use session::Session;
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/users/{username}/activity",
|
||||
params(
|
||||
("username" = String, Path),
|
||||
("page" = Option<u64>, Query),
|
||||
("per_page" = Option<u64>, Query),
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "Get user activity", body = ApiResponse<service::user::user_activity::UserActivityResponse>),
|
||||
(status = 401, description = "Unauthorized"),
|
||||
(status = 404, description = "Not found"),
|
||||
),
|
||||
tag = "User"
|
||||
)]
|
||||
pub async fn get_user_activity(
|
||||
service: web::Data<AppService>,
|
||||
session: Session,
|
||||
path: web::Path<String>,
|
||||
query: web::Query<UserActivityQuery>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
let username = path.into_inner();
|
||||
let resp = service
|
||||
.get_user_activity(session, username, query.into_inner())
|
||||
.await?;
|
||||
Ok(ApiResponse::ok(resp).to_response())
|
||||
}
|
||||
@ -1,8 +0,0 @@
|
||||
version: v2
|
||||
managed:
|
||||
enabled: true
|
||||
plugins:
|
||||
- remote: buf.build/bufbuild/es
|
||||
out: ../../admin/src/lib/adminrpc/generated
|
||||
- remote: buf.build/bufbuild/connect-es
|
||||
out: ../../admin/src/lib/adminrpc/generated
|
||||
@ -1,9 +0,0 @@
|
||||
version: v2
|
||||
name: buf.build/gitdataai/code
|
||||
deps:
|
||||
- buf.build/googleapis/googlerpc
|
||||
lint:
|
||||
use:
|
||||
- DEFAULT
|
||||
except:
|
||||
- PACKAGE_VERSION_SUFFIX
|
||||
@ -121,98 +121,6 @@ message ExportMetricsCsvResponse {
|
||||
string csv = 1;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// AI Model Sync
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
message SyncModelsRequest {}
|
||||
message SyncModelsResponse {
|
||||
string body_json = 1; // Serialized SyncModelsResponse JSON
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Billing Alert Check
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
message CheckAlertsRequest {}
|
||||
message CheckAlertsResponse {
|
||||
string body_json = 1; // Serialized CheckAlertsResponse JSON
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// AI Provider CRUD
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
message CreateProviderRequest {
|
||||
string body_json = 1; // Serialized AdminCreateProvider JSON
|
||||
}
|
||||
message UpdateProviderRequest {
|
||||
string id = 1;
|
||||
string body_json = 2; // Serialized AdminUpdateProvider JSON
|
||||
}
|
||||
message DeleteProviderRequest {
|
||||
string id = 1;
|
||||
}
|
||||
message ProviderResponse {
|
||||
string body_json = 1; // Serialized response JSON
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// AI Model CRUD
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
message CreateModelRequest {
|
||||
string body_json = 1; // Serialized AdminCreateModel JSON
|
||||
}
|
||||
message UpdateModelRequest {
|
||||
string id = 1;
|
||||
string body_json = 2; // Serialized AdminUpdateModel JSON
|
||||
}
|
||||
message DeleteModelRequest {
|
||||
string id = 1;
|
||||
}
|
||||
message ModelResponse {
|
||||
string body_json = 1;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// AI Version CRUD
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
message CreateVersionRequest {
|
||||
string body_json = 1; // Serialized AdminCreateVersion JSON
|
||||
}
|
||||
message UpdateVersionRequest {
|
||||
string id = 1;
|
||||
string body_json = 2; // Serialized AdminUpdateVersion JSON
|
||||
}
|
||||
message DeleteVersionRequest {
|
||||
string id = 1;
|
||||
}
|
||||
message VersionResponse {
|
||||
string body_json = 1;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// AI Pricing Update
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
message UpdatePricingRequest {
|
||||
string id = 1;
|
||||
string body_json = 2; // Serialized AdminUpdatePricing JSON
|
||||
}
|
||||
message PricingResponse {
|
||||
string body_json = 1;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Generic delete response
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
message DeleteResponse {
|
||||
bool deleted = 1;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Service
|
||||
// ---------------------------------------------------------------------------
|
||||
@ -228,21 +136,4 @@ service SessionAdmin {
|
||||
rpc IsUserOnline(IsUserOnlineRequest) returns (IsUserOnlineResponse);
|
||||
rpc GetMetrics(GetMetricsRequest) returns (GetMetricsResponse);
|
||||
rpc ExportMetricsCsv(ExportMetricsCsvRequest) returns (ExportMetricsCsvResponse);
|
||||
// AI
|
||||
rpc SyncModels(SyncModelsRequest) returns (SyncModelsResponse);
|
||||
rpc CheckAlerts(CheckAlertsRequest) returns (CheckAlertsResponse);
|
||||
// AI Provider
|
||||
rpc CreateProvider(CreateProviderRequest) returns (ProviderResponse);
|
||||
rpc UpdateProvider(UpdateProviderRequest) returns (ProviderResponse);
|
||||
rpc DeleteProvider(DeleteProviderRequest) returns (DeleteResponse);
|
||||
// AI Model
|
||||
rpc CreateModel(CreateModelRequest) returns (ModelResponse);
|
||||
rpc UpdateModel(UpdateModelRequest) returns (ModelResponse);
|
||||
rpc DeleteModel(DeleteModelRequest) returns (DeleteResponse);
|
||||
// AI Version
|
||||
rpc CreateVersion(CreateVersionRequest) returns (VersionResponse);
|
||||
rpc UpdateVersion(UpdateVersionRequest) returns (VersionResponse);
|
||||
rpc DeleteVersion(DeleteVersionRequest) returns (DeleteResponse);
|
||||
// AI Pricing
|
||||
rpc UpdatePricing(UpdatePricingRequest) returns (PricingResponse);
|
||||
}
|
||||
|
||||
@ -8,7 +8,5 @@ pub mod profile;
|
||||
pub mod projects;
|
||||
pub mod repository;
|
||||
pub mod ssh_key;
|
||||
pub mod stars;
|
||||
pub mod subscribe;
|
||||
pub mod user_activity;
|
||||
pub mod user_info;
|
||||
|
||||
@ -60,54 +60,24 @@ impl AppService {
|
||||
let per_page = std::cmp::Ord::min(std::cmp::Ord::max(query.per_page.unwrap_or(20), 1), 100);
|
||||
let offset = (page - 1) * per_page;
|
||||
|
||||
// Projects where user is the creator
|
||||
let created_projects: Vec<Uuid> = project::Entity::find()
|
||||
.filter(project::Column::CreatedBy.eq(target_user.uid))
|
||||
.select_only()
|
||||
.column(project::Column::Id)
|
||||
.into_tuple::<Uuid>()
|
||||
.all(&self.db)
|
||||
.await?;
|
||||
let mut condition = Condition::all().add(project::Column::CreatedBy.eq(target_user.uid));
|
||||
|
||||
// Projects where user is a member (via invitation)
|
||||
let member_projects: Vec<Uuid> = project_members::Entity::find()
|
||||
.filter(project_members::Column::User.eq(target_user.uid))
|
||||
.select_only()
|
||||
.column(project_members::Column::Project)
|
||||
.into_tuple::<Uuid>()
|
||||
.all(&self.db)
|
||||
.await?;
|
||||
|
||||
// Union + dedup (preserving first occurrence order)
|
||||
let mut project_ids: Vec<Uuid> = created_projects;
|
||||
let new_ids: Vec<Uuid> = member_projects.into_iter().filter(|id| !project_ids.contains(id)).collect();
|
||||
project_ids.extend(new_ids);
|
||||
|
||||
let total_count = project_ids.len() as u64;
|
||||
|
||||
// Paginate
|
||||
let page_ids: Vec<Uuid> = project_ids.into_iter().skip(offset as usize).take(per_page as usize).collect();
|
||||
|
||||
if page_ids.is_empty() {
|
||||
return Ok(UserProjectsResponse {
|
||||
username: target_user.username,
|
||||
projects: vec![],
|
||||
total_count,
|
||||
});
|
||||
if !is_owner && !has_admin_privilege {
|
||||
condition = condition.add(project::Column::IsPublic.eq(true));
|
||||
}
|
||||
|
||||
let project_list: Vec<project::Model> = project::Entity::find()
|
||||
.filter(project::Column::Id.is_in(page_ids.clone()))
|
||||
.all(&self.db)
|
||||
let total_count = project::Entity::find()
|
||||
.filter(condition.clone())
|
||||
.count(&self.db)
|
||||
.await?;
|
||||
|
||||
// Preserve the order from project_ids (created projects first, then member projects)
|
||||
let mut sorted_projects = project_list;
|
||||
sorted_projects.sort_by(|a, b| {
|
||||
let a_idx = page_ids.iter().position(|&x| x == a.id).unwrap_or(usize::MAX);
|
||||
let b_idx = page_ids.iter().position(|&x| x == b.id).unwrap_or(usize::MAX);
|
||||
a_idx.cmp(&b_idx)
|
||||
});
|
||||
let project_list = project::Entity::find()
|
||||
.filter(condition)
|
||||
.order_by_desc(project::Column::CreatedAt)
|
||||
.limit(per_page)
|
||||
.offset(offset)
|
||||
.all(&self.db)
|
||||
.await?;
|
||||
|
||||
let user_project_memberships: std::collections::HashSet<Uuid> =
|
||||
if let Some(uid) = current_user_uid {
|
||||
@ -125,11 +95,7 @@ impl AppService {
|
||||
};
|
||||
|
||||
let mut project_infos: Vec<UserProjectInfo> = Vec::new();
|
||||
for project in sorted_projects {
|
||||
// Privacy: non-owners/non-admins only see public projects (member or created)
|
||||
if !is_owner && !has_admin_privilege && !project.is_public {
|
||||
continue;
|
||||
}
|
||||
for project in project_list {
|
||||
let member_count = project_members::Entity::find()
|
||||
.filter(project_members::Column::Project.eq(project.id))
|
||||
.count(&self.db)
|
||||
|
||||
@ -1,152 +0,0 @@
|
||||
use crate::AppService;
|
||||
use crate::error::AppError;
|
||||
use chrono::{DateTime, Utc};
|
||||
use models::projects::{project, project_follow};
|
||||
use models::repos::{repo, repo_star};
|
||||
use models::users::user;
|
||||
use sea_orm::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use session::Session;
|
||||
use utoipa::ToSchema;
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
|
||||
pub struct RepoStarItem {
|
||||
pub uid: Uuid,
|
||||
pub repo_name: String,
|
||||
pub owner: String,
|
||||
pub description: Option<String>,
|
||||
pub is_private: bool,
|
||||
pub default_branch: String,
|
||||
pub starred_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
|
||||
pub struct ProjectFollowItem {
|
||||
pub uid: Uuid,
|
||||
pub name: String,
|
||||
pub display_name: String,
|
||||
pub description: Option<String>,
|
||||
pub is_public: bool,
|
||||
pub followed_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
|
||||
pub struct UserStarsResponse {
|
||||
pub repos: Vec<RepoStarItem>,
|
||||
pub projects: Vec<ProjectFollowItem>,
|
||||
pub total: u64,
|
||||
}
|
||||
|
||||
impl AppService {
|
||||
pub async fn get_user_stars(
|
||||
&self,
|
||||
context: Session,
|
||||
username: String,
|
||||
) -> Result<UserStarsResponse, AppError> {
|
||||
let target_user = user::Entity::find()
|
||||
.filter(user::Column::Username.eq(&username))
|
||||
.one(&self.db)
|
||||
.await?
|
||||
.ok_or(AppError::UserNotFound)?;
|
||||
|
||||
let current_uid = context.user();
|
||||
let is_owner = current_uid
|
||||
.map(|uid| uid == target_user.uid)
|
||||
.unwrap_or(false);
|
||||
|
||||
// Repo stars
|
||||
let stars = repo_star::Entity::find()
|
||||
.filter(repo_star::Column::User.eq(target_user.uid))
|
||||
.order_by_desc(repo_star::Column::CreatedAt)
|
||||
.all(&self.db)
|
||||
.await?;
|
||||
|
||||
let repo_ids: Vec<Uuid> = stars.iter().map(|s| s.repo).collect();
|
||||
|
||||
let repos: std::collections::HashMap<Uuid, repo::Model> = repo::Entity::find()
|
||||
.filter(repo::Column::Id.is_in(repo_ids.clone()))
|
||||
.all(&self.db)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|r| (r.id, r))
|
||||
.collect();
|
||||
|
||||
let project_ids: Vec<Uuid> = repos.values().map(|r| r.project).collect();
|
||||
let projects_map: std::collections::HashMap<Uuid, project::Model> = project::Entity::find()
|
||||
.filter(project::Column::Id.is_in(project_ids))
|
||||
.all(&self.db)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|p| (p.id, p))
|
||||
.collect();
|
||||
|
||||
// Build repo items with privacy filter
|
||||
let mut repo_items: Vec<RepoStarItem> = Vec::new();
|
||||
for star in &stars {
|
||||
if let Some(r) = repos.get(&star.repo) {
|
||||
// Privacy: non-owners can only see public repos
|
||||
if !is_owner && r.is_private {
|
||||
continue;
|
||||
}
|
||||
let owner_username = if let Some(p) = projects_map.get(&r.project) {
|
||||
p.name.clone()
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
repo_items.push(RepoStarItem {
|
||||
uid: r.id,
|
||||
repo_name: r.repo_name.clone(),
|
||||
owner: owner_username,
|
||||
description: r.description.clone(),
|
||||
is_private: r.is_private,
|
||||
default_branch: r.default_branch.clone(),
|
||||
starred_at: star.created_at,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Project follows
|
||||
let follows = project_follow::Entity::find()
|
||||
.filter(project_follow::Column::User.eq(target_user.uid))
|
||||
.order_by_desc(project_follow::Column::CreatedAt)
|
||||
.all(&self.db)
|
||||
.await?;
|
||||
|
||||
let followed_project_ids: Vec<Uuid> = follows.iter().map(|f| f.project).collect();
|
||||
|
||||
let followed_projects: std::collections::HashMap<Uuid, project::Model> = project::Entity::find()
|
||||
.filter(project::Column::Id.is_in(followed_project_ids))
|
||||
.all(&self.db)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|p| (p.id, p))
|
||||
.collect();
|
||||
|
||||
let mut project_items: Vec<ProjectFollowItem> = Vec::new();
|
||||
for follow in &follows {
|
||||
if let Some(p) = followed_projects.get(&follow.project) {
|
||||
// Privacy: non-owners can only see public projects
|
||||
if !is_owner && !p.is_public {
|
||||
continue;
|
||||
}
|
||||
project_items.push(ProjectFollowItem {
|
||||
uid: p.id,
|
||||
name: p.name.clone(),
|
||||
display_name: p.display_name.clone(),
|
||||
description: p.description.clone(),
|
||||
is_public: p.is_public,
|
||||
followed_at: follow.created_at,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let total = repo_items.len() as u64 + project_items.len() as u64;
|
||||
|
||||
Ok(UserStarsResponse {
|
||||
repos: repo_items,
|
||||
projects: project_items,
|
||||
total,
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -15,15 +15,6 @@ pub struct SubscriptionInfo {
|
||||
pub is_active: bool,
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize, Clone, Debug, utoipa::ToSchema)]
|
||||
pub struct UserCard {
|
||||
pub user_uid: Uuid,
|
||||
pub username: String,
|
||||
pub display_name: Option<String>,
|
||||
pub avatar_url: Option<String>,
|
||||
pub is_following_me: bool,
|
||||
}
|
||||
|
||||
impl From<user_relation::Model> for SubscriptionInfo {
|
||||
fn from(sub: user_relation::Model) -> Self {
|
||||
SubscriptionInfo {
|
||||
@ -163,63 +154,4 @@ impl AppService {
|
||||
|
||||
Ok(count)
|
||||
}
|
||||
|
||||
pub async fn user_get_following_list(
|
||||
&self,
|
||||
context: Session,
|
||||
username: String,
|
||||
) -> Result<Vec<UserCard>, AppError> {
|
||||
let target_user = self.utils_find_user_by_username(username).await?;
|
||||
let target_uid = target_user.uid;
|
||||
let current_uid = context.user();
|
||||
|
||||
let following = user_relation::Entity::find()
|
||||
.filter(user_relation::Column::User.eq(target_uid))
|
||||
.filter(user_relation::Column::RelationType.eq("follow"))
|
||||
.order_by_desc(user_relation::Column::CreatedAt)
|
||||
.all(&self.db)
|
||||
.await?;
|
||||
|
||||
let followed_uids: Vec<Uuid> = following.iter().map(|f| f.target).collect();
|
||||
|
||||
let followed_users: std::collections::HashMap<Uuid, models::users::user::Model> = models::users::user::Entity::find()
|
||||
.filter(models::users::user::Column::Uid.is_in(followed_uids.clone()))
|
||||
.all(&self.db)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|u| (u.uid, u))
|
||||
.collect();
|
||||
|
||||
// If current user is logged in, check who they also follow
|
||||
let current_follows: std::collections::HashSet<Uuid> = if let Some(uid) = current_uid {
|
||||
user_relation::Entity::find()
|
||||
.filter(user_relation::Column::User.eq(uid))
|
||||
.filter(user_relation::Column::Target.is_in(followed_uids.clone()))
|
||||
.filter(user_relation::Column::RelationType.eq("follow"))
|
||||
.select_only()
|
||||
.column(user_relation::Column::Target)
|
||||
.into_tuple::<Uuid>()
|
||||
.all(&self.db)
|
||||
.await?
|
||||
.into_iter()
|
||||
.collect()
|
||||
} else {
|
||||
std::collections::HashSet::new()
|
||||
};
|
||||
|
||||
let mut cards: Vec<UserCard> = Vec::new();
|
||||
for rel in following {
|
||||
if let Some(user) = followed_users.get(&rel.target) {
|
||||
cards.push(UserCard {
|
||||
user_uid: user.uid,
|
||||
username: user.username.clone(),
|
||||
display_name: user.display_name.clone(),
|
||||
avatar_url: user.avatar_url.clone(),
|
||||
is_following_me: current_follows.contains(&user.uid),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Ok(cards)
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,173 +0,0 @@
|
||||
use crate::AppService;
|
||||
use crate::error::AppError;
|
||||
use chrono::{DateTime, Utc};
|
||||
use models::projects::{project, project_activity};
|
||||
use models::users::{user, user_activity_log};
|
||||
use sea_orm::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use session::Session;
|
||||
use utoipa::ToSchema;
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
|
||||
pub struct UserActivityItem {
|
||||
pub id: i64,
|
||||
pub activity_type: String, // "auth" | "project"
|
||||
pub action: String,
|
||||
pub title: String,
|
||||
pub resource_type: Option<String>,
|
||||
pub resource_name: Option<String>,
|
||||
pub metadata: Option<serde_json::Value>,
|
||||
pub created_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
|
||||
pub struct UserActivityResponse {
|
||||
pub items: Vec<UserActivityItem>,
|
||||
pub total: u64,
|
||||
pub page: u64,
|
||||
pub per_page: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, utoipa::IntoParams)]
|
||||
pub struct UserActivityQuery {
|
||||
pub page: Option<u64>,
|
||||
pub per_page: Option<u64>,
|
||||
}
|
||||
|
||||
impl AppService {
|
||||
pub async fn get_user_activity(
|
||||
&self,
|
||||
context: Session,
|
||||
username: String,
|
||||
query: UserActivityQuery,
|
||||
) -> Result<UserActivityResponse, AppError> {
|
||||
let target_user = user::Entity::find()
|
||||
.filter(user::Column::Username.eq(&username))
|
||||
.one(&self.db)
|
||||
.await?
|
||||
.ok_or(AppError::UserNotFound)?;
|
||||
|
||||
let current_uid = context.user();
|
||||
let is_owner = current_uid
|
||||
.map(|uid| uid == target_user.uid)
|
||||
.unwrap_or(false);
|
||||
|
||||
let page = std::cmp::Ord::max(query.page.unwrap_or(1), 1);
|
||||
let per_page = std::cmp::Ord::min(std::cmp::Ord::max(query.per_page.unwrap_or(20), 1), 100);
|
||||
let offset = (page - 1) * per_page;
|
||||
|
||||
// User's auth activity log entries
|
||||
let auth_logs: Vec<(i64, String, DateTime<Utc>, serde_json::Value)> =
|
||||
user_activity_log::Entity::find()
|
||||
.filter(user_activity_log::Column::UserUid.eq(target_user.uid))
|
||||
.order_by_desc(user_activity_log::Column::CreatedAt)
|
||||
.select_only()
|
||||
.column(user_activity_log::Column::Id)
|
||||
.column(user_activity_log::Column::Action)
|
||||
.column(user_activity_log::Column::CreatedAt)
|
||||
.column(user_activity_log::Column::Details)
|
||||
.into_tuple()
|
||||
.all(&self.db)
|
||||
.await?;
|
||||
|
||||
// User's project activity (where user is the actor)
|
||||
let proj_activity = project_activity::Entity::find()
|
||||
.filter(project_activity::Column::Actor.eq(target_user.uid))
|
||||
.order_by_desc(project_activity::Column::CreatedAt)
|
||||
.all(&self.db)
|
||||
.await?;
|
||||
|
||||
// Privacy filter: non-owners only see public project activity
|
||||
let proj_ids: Vec<Uuid> = proj_activity.iter().map(|a| a.project).collect();
|
||||
let proj_map: std::collections::HashMap<Uuid, project::Model> = project::Entity::find()
|
||||
.filter(project::Column::Id.is_in(proj_ids))
|
||||
.all(&self.db)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|p| (p.id, p))
|
||||
.collect();
|
||||
|
||||
// Build combined activity list
|
||||
let mut items: Vec<UserActivityItem> = Vec::new();
|
||||
|
||||
// Auth events
|
||||
for (id, action, created_at, metadata) in auth_logs {
|
||||
let title = format_action_title(&action);
|
||||
items.push(UserActivityItem {
|
||||
id,
|
||||
activity_type: "auth".to_string(),
|
||||
action,
|
||||
title,
|
||||
resource_type: None,
|
||||
resource_name: None,
|
||||
metadata: if metadata != serde_json::Value::Null {
|
||||
Some(metadata)
|
||||
} else {
|
||||
None
|
||||
},
|
||||
created_at,
|
||||
});
|
||||
}
|
||||
|
||||
// Project events
|
||||
for activity in proj_activity {
|
||||
// Privacy filter
|
||||
if !is_owner {
|
||||
if let Some(proj) = proj_map.get(&activity.project) {
|
||||
if proj.is_public == false {
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
items.push(UserActivityItem {
|
||||
id: activity.id,
|
||||
activity_type: "project".to_string(),
|
||||
action: activity.event_type,
|
||||
title: activity.title,
|
||||
resource_type: Some("project".to_string()),
|
||||
resource_name: proj_map
|
||||
.get(&activity.project)
|
||||
.map(|p| p.name.clone()),
|
||||
metadata: activity.metadata,
|
||||
created_at: activity.created_at,
|
||||
});
|
||||
}
|
||||
|
||||
// Sort by created_at DESC
|
||||
items.sort_by(|a, b| b.created_at.cmp(&a.created_at));
|
||||
|
||||
let total = items.len() as u64;
|
||||
let page_items: Vec<UserActivityItem> = items.into_iter().skip(offset as usize).take(per_page as usize).collect();
|
||||
|
||||
Ok(UserActivityResponse {
|
||||
items: page_items,
|
||||
total,
|
||||
page,
|
||||
per_page,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn format_action_title(action: &str) -> String {
|
||||
match action {
|
||||
"login" => "Signed in".to_string(),
|
||||
"logout" => "Signed out".to_string(),
|
||||
"register" => "Created account".to_string(),
|
||||
"password_change" => "Changed password".to_string(),
|
||||
"password_reset" => "Reset password".to_string(),
|
||||
"2fa_enabled" => "Enabled 2FA".to_string(),
|
||||
"2fa_disabled" => "Disabled 2FA".to_string(),
|
||||
"ssh_key_add" => "Added SSH key".to_string(),
|
||||
"ssh_key_delete" => "Removed SSH key".to_string(),
|
||||
"ssh_key_update" => "Updated SSH key".to_string(),
|
||||
"ssh_key_revoke" => "Revoked SSH key".to_string(),
|
||||
"access_key_create" => "Created access token".to_string(),
|
||||
"access_key_delete" => "Deleted access token".to_string(),
|
||||
"avatar_upload" => "Updated avatar".to_string(),
|
||||
"profile_update" => "Updated profile".to_string(),
|
||||
_ => format!("Activity: {}", action),
|
||||
}
|
||||
}
|
||||
1360
openapi.json
1360
openapi.json
File diff suppressed because it is too large
Load Diff
@ -21,7 +21,7 @@ const TAG = process.env.TAG || GIT_SHA_SHORT;
|
||||
const DOCKER_USER = process.env.DOCKER_USER || process.env.HARBOR_USERNAME;
|
||||
const DOCKER_PASS = process.env.DOCKER_PASS || process.env.HARBOR_PASSWORD;
|
||||
|
||||
const SERVICES = ['app', 'gitserver', 'email-worker', 'git-hook', 'operator', 'static', 'adminrpc'];
|
||||
const SERVICES = ['app', 'gitserver', 'email-worker', 'git-hook', 'operator', 'static'];
|
||||
|
||||
const args = process.argv.slice(2);
|
||||
const targets = args.length > 0 ? args : SERVICES;
|
||||
|
||||
@ -1,78 +0,0 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { Activity, Clock } from 'lucide-react';
|
||||
import { Card, CardContent } from '@/components/ui/card';
|
||||
import { getUserActivity, type UserActivityItem } from '@/client';
|
||||
import { formatDate } from './utils';
|
||||
|
||||
export function UserActivity({ username }: { username: string }) {
|
||||
const { data, isLoading, isError } = useQuery({
|
||||
queryKey: ['user-activity', username],
|
||||
queryFn: async () => {
|
||||
const resp = await getUserActivity({ path: { username } });
|
||||
return resp.data?.data ?? null;
|
||||
},
|
||||
retry: false,
|
||||
});
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<Card className="border-border/40">
|
||||
<CardContent className="py-8 text-center text-sm text-muted-foreground">
|
||||
Loading activity...
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
if (isError || !data) {
|
||||
return (
|
||||
<Card className="border-border/40">
|
||||
<CardContent className="py-8 text-center text-sm text-muted-foreground">
|
||||
Unable to load activity.
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
if (data.items.length === 0) {
|
||||
return (
|
||||
<Card className="border-border/40">
|
||||
<CardContent className="py-12 text-center">
|
||||
<Activity className="mx-auto h-8 w-8 text-muted-foreground/40" />
|
||||
<p className="mt-2 text-sm text-muted-foreground">No activity yet</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
const getTypeColor = (type: string) => {
|
||||
if (type === 'auth') return 'bg-blue-100 dark:bg-blue-900/30 text-blue-600 dark:text-blue-400';
|
||||
return 'bg-green-100 dark:bg-green-900/30 text-green-600 dark:text-green-400';
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
{data.items.map((item: UserActivityItem) => (
|
||||
<Card key={`${item.activity_type}-${item.id}`} className="border-border/40">
|
||||
<CardContent className="flex items-start gap-3 py-4">
|
||||
<span className={`mt-0.5 flex h-7 w-7 shrink-0 items-center justify-center rounded-full text-xs font-medium ${getTypeColor(item.activity_type)}`}>
|
||||
{item.activity_type === 'auth' ? 'A' : 'P'}
|
||||
</span>
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="text-sm font-medium">{item.title}</p>
|
||||
{item.resource_name && (
|
||||
<p className="text-xs text-muted-foreground mt-0.5">
|
||||
Project: {item.resource_name}
|
||||
</p>
|
||||
)}
|
||||
<div className="flex items-center gap-1 mt-1 text-xs text-muted-foreground/70">
|
||||
<Clock className="h-3 w-3" />
|
||||
{formatDate(item.created_at)}
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -1,71 +0,0 @@
|
||||
import { Link } from 'react-router-dom';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
|
||||
import { Card, CardContent } from '@/components/ui/card';
|
||||
import { getFollowingList } from '@/client';
|
||||
import { UserPlus } from 'lucide-react';
|
||||
|
||||
export function FollowingList({ username }: { username: string }) {
|
||||
const { data, isLoading, isError } = useQuery({
|
||||
queryKey: ['user-following', username],
|
||||
queryFn: async () => {
|
||||
const resp = await getFollowingList({ path: { username } });
|
||||
return resp.data?.data ?? [];
|
||||
},
|
||||
retry: false,
|
||||
});
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<Card className="border-border/40">
|
||||
<CardContent className="py-8 text-center text-sm text-muted-foreground">
|
||||
Loading following...
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
if (isError || !data) {
|
||||
return (
|
||||
<Card className="border-border/40">
|
||||
<CardContent className="py-8 text-center text-sm text-muted-foreground">
|
||||
Unable to load following.
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
if (data.length === 0) {
|
||||
return (
|
||||
<Card className="border-border/40">
|
||||
<CardContent className="py-12 text-center">
|
||||
<UserPlus className="mx-auto h-8 w-8 text-muted-foreground/40" />
|
||||
<p className="mt-2 text-sm text-muted-foreground">Not following anyone yet</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2 md:grid-cols-3">
|
||||
{data.map((user: { user_uid: string; username: string; display_name?: string | null; avatar_url?: string | null }) => (
|
||||
<Link
|
||||
key={user.user_uid}
|
||||
to={`/user/${user.username}`}
|
||||
className="flex items-center gap-3 rounded-lg border border-border/40 p-3 transition-all hover:border-border hover:bg-muted/30"
|
||||
>
|
||||
<Avatar className="h-10 w-10">
|
||||
<AvatarImage src={user.avatar_url ?? undefined} />
|
||||
<AvatarFallback className="text-sm">
|
||||
{(user.display_name || user.username).charAt(0).toUpperCase()}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="min-w-0">
|
||||
<p className="truncate text-sm font-medium">{user.display_name || user.username}</p>
|
||||
<p className="truncate text-xs text-muted-foreground">@{user.username}</p>
|
||||
</div>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -1,149 +0,0 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { Copy, Key, Shield, Loader2, AlertTriangle } from 'lucide-react';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { listSshKeys, listAccessKeys } from '@/client';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
export function SecurityTab() {
|
||||
const { data: sshKeys, isLoading: sshLoading } = useQuery({
|
||||
queryKey: ['my-ssh-keys'],
|
||||
queryFn: async () => {
|
||||
const resp = await listSshKeys();
|
||||
return resp.data?.data?.keys ?? [];
|
||||
},
|
||||
retry: false,
|
||||
});
|
||||
|
||||
const { data: accessKeys, isLoading: accessLoading } = useQuery({
|
||||
queryKey: ['my-access-keys'],
|
||||
queryFn: async () => {
|
||||
const resp = await listAccessKeys();
|
||||
return resp.data?.data?.access_keys ?? [];
|
||||
},
|
||||
retry: false,
|
||||
});
|
||||
|
||||
const isLoading = sshLoading || accessLoading;
|
||||
|
||||
const copyFingerprint = (fingerprint: string) => {
|
||||
navigator.clipboard.writeText(fingerprint).then(() => {
|
||||
toast.success('Copied fingerprint');
|
||||
});
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<Card className="border-border/40">
|
||||
<CardContent className="flex items-center justify-center gap-2 py-12 text-sm text-muted-foreground">
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
Loading security settings...
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* SSH Keys */}
|
||||
<Card className="border-border/40">
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="flex items-center gap-2 text-sm font-medium">
|
||||
<Key className="h-4 w-4" />
|
||||
SSH Keys
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-2">
|
||||
{(sshKeys ?? []).length === 0 ? (
|
||||
<p className="py-4 text-center text-sm text-muted-foreground">No SSH keys added</p>
|
||||
) : (
|
||||
(sshKeys ?? []).map((key: {
|
||||
id: number;
|
||||
title: string;
|
||||
fingerprint: string;
|
||||
key_type: string;
|
||||
is_revoked: boolean;
|
||||
created_at: string;
|
||||
}) => (
|
||||
<div
|
||||
key={key.id}
|
||||
className="flex items-center justify-between rounded-lg border border-border/40 p-3"
|
||||
>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<p className="text-sm font-medium">{key.title}</p>
|
||||
<span className="rounded bg-muted px-1.5 py-0.5 text-[10px] font-medium uppercase">{key.key_type}</span>
|
||||
{key.is_revoked && (
|
||||
<span className="flex items-center gap-1 text-[10px] text-red-500">
|
||||
<AlertTriangle className="h-3 w-3" /> Revoked
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<button
|
||||
onClick={() => copyFingerprint(key.fingerprint)}
|
||||
className="mt-1 flex items-center gap-1 text-xs text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
{key.fingerprint}
|
||||
<Copy className="h-3 w-3" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
<Button variant="outline" size="sm" className="mt-2 w-full" onClick={() => window.location.href = '/settings/keys'}>
|
||||
Manage SSH Keys
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Personal Access Tokens */}
|
||||
<Card className="border-border/40">
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="flex items-center gap-2 text-sm font-medium">
|
||||
<Shield className="h-4 w-4" />
|
||||
Personal Access Tokens
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-2">
|
||||
{(accessKeys ?? []).length === 0 ? (
|
||||
<p className="py-4 text-center text-sm text-muted-foreground">No access tokens created</p>
|
||||
) : (
|
||||
(accessKeys ?? []).map((key: {
|
||||
id: number;
|
||||
name: string;
|
||||
scopes: string[];
|
||||
is_revoked: boolean;
|
||||
created_at: string;
|
||||
}) => (
|
||||
<div
|
||||
key={key.id}
|
||||
className="flex items-center justify-between rounded-lg border border-border/40 p-3"
|
||||
>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<p className="text-sm font-medium">{key.name}</p>
|
||||
{key.is_revoked && (
|
||||
<span className="flex items-center gap-1 text-[10px] text-red-500">
|
||||
<AlertTriangle className="h-3 w-3" /> Revoked
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="mt-1 flex flex-wrap gap-1">
|
||||
{(key.scopes ?? []).map((scope: string) => (
|
||||
<span key={scope} className="rounded bg-muted px-1.5 py-0.5 text-[10px] text-muted-foreground">
|
||||
{scope}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
<Button variant="outline" size="sm" className="mt-2 w-full" onClick={() => window.location.href = '/settings/tokens'}>
|
||||
Manage Access Tokens
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -1,107 +0,0 @@
|
||||
import { Link } from 'react-router-dom';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { Star, FolderGit2, Loader2 } from 'lucide-react';
|
||||
import { Card, CardContent } from '@/components/ui/card';
|
||||
import { getUserStars } from '@/client';
|
||||
|
||||
export function StarsList({ username }: { username: string }) {
|
||||
const { data, isLoading, isError } = useQuery({
|
||||
queryKey: ['user-stars', username],
|
||||
queryFn: async () => {
|
||||
const resp = await getUserStars({ path: { username } });
|
||||
return resp.data?.data ?? null;
|
||||
},
|
||||
retry: false,
|
||||
});
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<Card className="border-border/40">
|
||||
<CardContent className="py-8 text-center text-sm text-muted-foreground">
|
||||
<Loader2 className="mx-auto h-4 w-4 animate-spin" />
|
||||
Loading stars...
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
if (isError || !data) {
|
||||
return (
|
||||
<Card className="border-border/40">
|
||||
<CardContent className="py-8 text-center text-sm text-muted-foreground">
|
||||
Unable to load stars.
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
const hasRepos = (data.repos ?? []).length > 0;
|
||||
const hasProjects = (data.projects ?? []).length > 0;
|
||||
|
||||
if (!hasRepos && !hasProjects) {
|
||||
return (
|
||||
<Card className="border-border/40">
|
||||
<CardContent className="py-12 text-center">
|
||||
<Star className="mx-auto h-8 w-8 text-muted-foreground/40" />
|
||||
<p className="mt-2 text-sm text-muted-foreground">No starred repos or projects yet</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{hasRepos && (
|
||||
<div>
|
||||
<h3 className="mb-3 text-sm font-semibold text-foreground">Repositories</h3>
|
||||
<div className="space-y-2">
|
||||
{(data.repos ?? []).map((repo: { uid: string; repo_name: string; owner: string; description?: string | null; is_private: boolean }) => (
|
||||
<Link
|
||||
key={repo.uid}
|
||||
to={`/repository/${repo.owner}/${repo.repo_name}`}
|
||||
className="flex items-center gap-3 rounded-lg border border-border/40 p-3 transition-all hover:border-border hover:bg-muted/30"
|
||||
>
|
||||
<Star className="h-4 w-4 shrink-0 text-muted-foreground" />
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="text-sm font-medium">{repo.owner}/{repo.repo_name}</p>
|
||||
{repo.description && (
|
||||
<p className="truncate text-xs text-muted-foreground">{repo.description}</p>
|
||||
)}
|
||||
</div>
|
||||
{repo.is_private && (
|
||||
<span className="shrink-0 rounded border border-border/40 px-1.5 py-0.5 text-[10px] text-muted-foreground">Private</span>
|
||||
)}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{hasProjects && (
|
||||
<div>
|
||||
<h3 className="mb-3 text-sm font-semibold text-foreground">Projects</h3>
|
||||
<div className="space-y-2">
|
||||
{(data.projects ?? []).map((proj: { uid: string; name: string; display_name: string; description?: string | null; is_public: boolean }) => (
|
||||
<Link
|
||||
key={proj.uid}
|
||||
to={`/project/${proj.name}`}
|
||||
className="flex items-center gap-3 rounded-lg border border-border/40 p-3 transition-all hover:border-border hover:bg-muted/30"
|
||||
>
|
||||
<FolderGit2 className="h-4 w-4 shrink-0 text-muted-foreground" />
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="text-sm font-medium">{proj.display_name}</p>
|
||||
{proj.description && (
|
||||
<p className="truncate text-xs text-muted-foreground">{proj.description}</p>
|
||||
)}
|
||||
</div>
|
||||
{!proj.is_public && (
|
||||
<span className="shrink-0 rounded border border-border/40 px-1.5 py-0.5 text-[10px] text-muted-foreground">Private</span>
|
||||
)}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -1,8 +1,7 @@
|
||||
import { useContext } from 'react';
|
||||
import { Link, useNavigate, useParams, useSearchParams } from 'react-router-dom';
|
||||
import { Link, useNavigate, useParams } from 'react-router-dom';
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import {
|
||||
Activity,
|
||||
AlertCircle,
|
||||
Building2,
|
||||
Calendar,
|
||||
@ -11,7 +10,6 @@ import {
|
||||
Loader2,
|
||||
MapPin,
|
||||
Settings,
|
||||
Shield,
|
||||
Star,
|
||||
UserPlus,
|
||||
UserRoundCheck,
|
||||
@ -24,11 +22,6 @@ import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Separator } from '@/components/ui/separator';
|
||||
import { getContributionHeatmap, getSubscriberCount, getUserInfo, getUserProjects, getUserRepos, subscribeTarget, unsubscribeTarget } from '@/client';
|
||||
import { UserContext } from '@/contexts/user-context';
|
||||
import { UserActivity } from './user-activity';
|
||||
import { FollowingList } from './user-following';
|
||||
import { StarsList } from './user-stars';
|
||||
import { SecurityTab } from './user-security';
|
||||
import { formatDate } from './utils';
|
||||
|
||||
const resolveCount = (payload: unknown): number => {
|
||||
if (typeof payload === 'number') return payload;
|
||||
@ -50,6 +43,17 @@ const resolveCount = (payload: unknown): number => {
|
||||
return 0;
|
||||
};
|
||||
|
||||
const formatDate = (value?: string | null) => {
|
||||
if (!value) return '-';
|
||||
const date = new Date(value);
|
||||
if (Number.isNaN(date.getTime())) return '-';
|
||||
return date.toLocaleDateString('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
});
|
||||
};
|
||||
|
||||
const resolveHeatLevel = (count: number, max: number) => {
|
||||
if (count <= 0) return 0;
|
||||
if (max <= 1) return 4;
|
||||
@ -242,10 +246,6 @@ export function UserProfile() {
|
||||
const isAuth = currentUser !== null;
|
||||
const queryClient = useQueryClient();
|
||||
const nav = useNavigate();
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
const activeTab = searchParams.get('tab') || 'overview';
|
||||
|
||||
const setTab = (tab: string) => setSearchParams({ tab });
|
||||
|
||||
const userInfoKey = ['user-info', targetUser] as const;
|
||||
const subscriberCountKey = ['user-subscriber-count', targetUser] as const;
|
||||
@ -537,83 +537,7 @@ export function UserProfile() {
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Tab Navigation */}
|
||||
<div className="flex items-center gap-1 border-b border-border/40 -mb-2">
|
||||
<button
|
||||
onClick={() => setTab('overview')}
|
||||
className={`flex items-center gap-1.5 px-3 py-2.5 text-sm font-medium border-b-2 transition-colors ${
|
||||
activeTab === 'overview'
|
||||
? 'border-primary text-foreground'
|
||||
: 'border-transparent text-muted-foreground hover:text-foreground hover:border-border'
|
||||
}`}
|
||||
>
|
||||
<FolderGit2 className="h-4 w-4" />
|
||||
Overview
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setTab('activity')}
|
||||
className={`flex items-center gap-1.5 px-3 py-2.5 text-sm font-medium border-b-2 transition-colors ${
|
||||
activeTab === 'activity'
|
||||
? 'border-primary text-foreground'
|
||||
: 'border-transparent text-muted-foreground hover:text-foreground hover:border-border'
|
||||
}`}
|
||||
>
|
||||
<Activity className="h-4 w-4" />
|
||||
Activity
|
||||
</button>
|
||||
{/*
|
||||
<button
|
||||
onClick={() => setTab('followers')}
|
||||
className={`flex items-center gap-1.5 px-3 py-2.5 text-sm font-medium border-b-2 transition-colors ${
|
||||
activeTab === 'followers'
|
||||
? 'border-primary text-foreground'
|
||||
: 'border-transparent text-muted-foreground hover:text-foreground hover:border-border'
|
||||
}`}
|
||||
>
|
||||
<UserCheck className="h-4 w-4" />
|
||||
Followers
|
||||
</button>
|
||||
*/}
|
||||
<button
|
||||
onClick={() => setTab('following')}
|
||||
className={`flex items-center gap-1.5 px-3 py-2.5 text-sm font-medium border-b-2 transition-colors ${
|
||||
activeTab === 'following'
|
||||
? 'border-primary text-foreground'
|
||||
: 'border-transparent text-muted-foreground hover:text-foreground hover:border-border'
|
||||
}`}
|
||||
>
|
||||
<UserPlus className="h-4 w-4" />
|
||||
Following
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setTab('stars')}
|
||||
className={`flex items-center gap-1.5 px-3 py-2.5 text-sm font-medium border-b-2 transition-colors ${
|
||||
activeTab === 'stars'
|
||||
? 'border-primary text-foreground'
|
||||
: 'border-transparent text-muted-foreground hover:text-foreground hover:border-border'
|
||||
}`}
|
||||
>
|
||||
<Star className="h-4 w-4" />
|
||||
Stars
|
||||
</button>
|
||||
{userInfo.is_owner && (
|
||||
<button
|
||||
onClick={() => setTab('security')}
|
||||
className={`flex items-center gap-1.5 px-3 py-2.5 text-sm font-medium border-b-2 transition-colors ${
|
||||
activeTab === 'security'
|
||||
? 'border-primary text-foreground'
|
||||
: 'border-transparent text-muted-foreground hover:text-foreground hover:border-border'
|
||||
}`}
|
||||
>
|
||||
<Shield className="h-4 w-4" />
|
||||
Security
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Tab Content */}
|
||||
{activeTab === 'overview' && (
|
||||
<div>{/* Contribution Heatmap */}
|
||||
{/* Contribution Heatmap */}
|
||||
<ContributionHeatmap
|
||||
totalContributions={contributionHeatmap?.total_contributions ?? 0}
|
||||
heatmap={contributionHeatmap?.heatmap ?? []}
|
||||
@ -707,20 +631,6 @@ export function UserProfile() {
|
||||
</CardContent>
|
||||
</Card>
|
||||
</section>
|
||||
</div>
|
||||
)}
|
||||
{activeTab === 'activity' && (
|
||||
<UserActivity username={targetUser} />
|
||||
)}
|
||||
{activeTab === 'following' && (
|
||||
<FollowingList username={targetUser} />
|
||||
)}
|
||||
{activeTab === 'stars' && (
|
||||
<StarsList username={targetUser} />
|
||||
)}
|
||||
{activeTab === 'security' && userInfo.is_owner && (
|
||||
<SecurityTab />
|
||||
)}
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
|
||||
@ -1,10 +0,0 @@
|
||||
export const formatDate = (value?: string | null) => {
|
||||
if (!value) return '-';
|
||||
const date = new Date(value);
|
||||
if (Number.isNaN(date.getTime())) return '-';
|
||||
return date.toLocaleDateString('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
});
|
||||
};
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -56,15 +56,6 @@ export type AddSshKeyParams = {
|
||||
public_key: string;
|
||||
};
|
||||
|
||||
export type AlertDetail = {
|
||||
workspace_id: string;
|
||||
workspace_name: string;
|
||||
alert_type: string;
|
||||
threshold: number;
|
||||
current_value: number;
|
||||
recipients: Array<string>;
|
||||
};
|
||||
|
||||
export type AnswerRequest = {
|
||||
question: string;
|
||||
answer: string;
|
||||
@ -487,16 +478,6 @@ export type ApiResponseCardResponse = {
|
||||
};
|
||||
};
|
||||
|
||||
export type ApiResponseCheckAlertsResponse = {
|
||||
code: number;
|
||||
message: string;
|
||||
data?: {
|
||||
workspaces_checked: number;
|
||||
alerts_sent: number;
|
||||
details: Array<AlertDetail>;
|
||||
};
|
||||
};
|
||||
|
||||
export type ApiResponseColumnResponse = {
|
||||
code: number;
|
||||
message: string;
|
||||
@ -1486,7 +1467,6 @@ export type ApiResponseRoomAiResponse = {
|
||||
think: boolean;
|
||||
stream: boolean;
|
||||
min_score?: number | null;
|
||||
agent_type?: string | null;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
};
|
||||
@ -1549,11 +1529,6 @@ export type ApiResponseRoomMessageResponse = {
|
||||
send_at: string;
|
||||
revoked?: string | null;
|
||||
revoked_by?: string | null;
|
||||
/**
|
||||
* Highlighted content with <mark> tags around matched terms (for search results)
|
||||
*/
|
||||
highlighted_content?: string | null;
|
||||
attachment_ids?: Array<string>;
|
||||
};
|
||||
};
|
||||
|
||||
@ -1705,19 +1680,6 @@ export type ApiResponseString = {
|
||||
data?: string;
|
||||
};
|
||||
|
||||
export type ApiResponseSyncModelsResponse = {
|
||||
code: number;
|
||||
message: string;
|
||||
data?: {
|
||||
models_created: number;
|
||||
models_updated: number;
|
||||
versions_created: number;
|
||||
pricing_created: number;
|
||||
capabilities_created: number;
|
||||
profiles_created: number;
|
||||
};
|
||||
};
|
||||
|
||||
export type ApiResponseTagCountResponse = {
|
||||
code: number;
|
||||
message: string;
|
||||
@ -1866,17 +1828,6 @@ export type ApiResponseTreeIsEmptyResponse = {
|
||||
};
|
||||
};
|
||||
|
||||
export type ApiResponseUserActivityResponse = {
|
||||
code: number;
|
||||
message: string;
|
||||
data?: {
|
||||
items: Array<UserActivityItem>;
|
||||
total: number;
|
||||
page: number;
|
||||
per_page: number;
|
||||
};
|
||||
};
|
||||
|
||||
export type ApiResponseUserInfoExternal = {
|
||||
code: number;
|
||||
message: string;
|
||||
@ -1918,16 +1869,6 @@ export type ApiResponseUserReposResponse = {
|
||||
};
|
||||
};
|
||||
|
||||
export type ApiResponseUserStarsResponse = {
|
||||
code: number;
|
||||
message: string;
|
||||
data?: {
|
||||
repos: Array<RepoStarItem>;
|
||||
projects: Array<ProjectFollowItem>;
|
||||
total: number;
|
||||
};
|
||||
};
|
||||
|
||||
export type ApiResponseValue = {
|
||||
code: number;
|
||||
message: string;
|
||||
@ -2134,20 +2075,6 @@ export type ApiResponseVecMergeheadInfoResponse = {
|
||||
}>;
|
||||
};
|
||||
|
||||
export type ApiResponseVecMyWorkspaceInvitation = {
|
||||
code: number;
|
||||
message: string;
|
||||
data?: Array<{
|
||||
workspace_id: string;
|
||||
workspace_slug: string;
|
||||
workspace_name: string;
|
||||
role: string;
|
||||
invited_by_username?: string | null;
|
||||
invited_at: string;
|
||||
expires_at?: string | null;
|
||||
}>;
|
||||
};
|
||||
|
||||
export type ApiResponseVecPendingInvitationInfo = {
|
||||
code: number;
|
||||
message: string;
|
||||
@ -2197,7 +2124,6 @@ export type ApiResponseVecRoomAiResponse = {
|
||||
think: boolean;
|
||||
stream: boolean;
|
||||
min_score?: number | null;
|
||||
agent_type?: string | null;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}>;
|
||||
@ -2342,18 +2268,6 @@ export type ApiResponseVecTreeEntryResponse = {
|
||||
}>;
|
||||
};
|
||||
|
||||
export type ApiResponseVecUserCard = {
|
||||
code: number;
|
||||
message: string;
|
||||
data?: Array<{
|
||||
user_uid: string;
|
||||
username: string;
|
||||
display_name?: string | null;
|
||||
avatar_url?: string | null;
|
||||
is_following_me: boolean;
|
||||
}>;
|
||||
};
|
||||
|
||||
export type ApiResponseVecWatchUserInfo = {
|
||||
code: number;
|
||||
message: string;
|
||||
@ -2808,12 +2722,6 @@ export type ChangePasswordParams = {
|
||||
new_password: string;
|
||||
};
|
||||
|
||||
export type CheckAlertsResponse = {
|
||||
workspaces_checked: number;
|
||||
alerts_sent: number;
|
||||
details: Array<AlertDetail>;
|
||||
};
|
||||
|
||||
export type ColumnResponse = {
|
||||
id: string;
|
||||
board: string;
|
||||
@ -3084,11 +2992,6 @@ export type ConfigSnapshotResponse = {
|
||||
entries: Array<ConfigEntryResponse>;
|
||||
};
|
||||
|
||||
export type ConfirmResetPasswordParams = {
|
||||
token: string;
|
||||
new_password: string;
|
||||
};
|
||||
|
||||
export type ContextMe = {
|
||||
uid: string;
|
||||
username: string;
|
||||
@ -3296,6 +3199,11 @@ export type DiffStatsResponse = {
|
||||
deletions: number;
|
||||
};
|
||||
|
||||
export type Disable2FaParams = {
|
||||
code: string;
|
||||
password: string;
|
||||
};
|
||||
|
||||
export type EmailChangeRequest = {
|
||||
new_email: string;
|
||||
password: string;
|
||||
@ -3309,6 +3217,12 @@ export type EmailVerifyRequest = {
|
||||
token: string;
|
||||
};
|
||||
|
||||
export type Enable2FaResponse = {
|
||||
secret: string;
|
||||
qr_code: string;
|
||||
backup_codes: Array<string>;
|
||||
};
|
||||
|
||||
export type ExchangeProjectName = {
|
||||
name: string;
|
||||
};
|
||||
@ -3348,6 +3262,12 @@ export type GeneratePrDescriptionResponse = {
|
||||
billing?: null | BillingRecord;
|
||||
};
|
||||
|
||||
export type Get2FaStatusResponse = {
|
||||
is_enabled: boolean;
|
||||
method?: string | null;
|
||||
has_backup_codes: boolean;
|
||||
};
|
||||
|
||||
export type GitInitRequest = {
|
||||
path: string;
|
||||
bare?: boolean;
|
||||
@ -3632,6 +3552,7 @@ export type LoginParams = {
|
||||
username: string;
|
||||
password: string;
|
||||
captcha: string;
|
||||
totp_code?: string | null;
|
||||
};
|
||||
|
||||
export type MemberInfo = {
|
||||
@ -3821,19 +3742,6 @@ export type MoveCardParams = {
|
||||
position: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* Invitation received by the current user (workspace invitation for self).
|
||||
*/
|
||||
export type MyWorkspaceInvitation = {
|
||||
workspace_id: string;
|
||||
workspace_slug: string;
|
||||
workspace_name: string;
|
||||
role: string;
|
||||
invited_by_username?: string | null;
|
||||
invited_at: string;
|
||||
expires_at?: string | null;
|
||||
};
|
||||
|
||||
export type NotificationListResponse = {
|
||||
notifications: Array<NotificationResponse>;
|
||||
total: number;
|
||||
@ -3851,18 +3759,6 @@ export type NotificationPreferencesParams = {
|
||||
marketing_enabled?: boolean | null;
|
||||
security_enabled?: boolean | null;
|
||||
product_enabled?: boolean | null;
|
||||
/**
|
||||
* Web Push subscription endpoint (set to null to unsubscribe)
|
||||
*/
|
||||
push_subscription_endpoint?: string | null;
|
||||
/**
|
||||
* Web Push subscription p256dh key
|
||||
*/
|
||||
push_subscription_keys_p256dh?: string | null;
|
||||
/**
|
||||
* Web Push subscription auth key
|
||||
*/
|
||||
push_subscription_keys_auth?: string | null;
|
||||
};
|
||||
|
||||
export type NotificationPreferencesResponse = {
|
||||
@ -3901,7 +3797,7 @@ export type NotificationResponse = {
|
||||
expires_at?: string | null;
|
||||
};
|
||||
|
||||
export type NotificationType = 'mention' | 'invitation' | 'role_change' | 'room_created' | 'room_deleted' | 'system_announcement' | 'project_invitation' | 'workspace_invitation';
|
||||
export type NotificationType = 'mention' | 'invitation' | 'role_change' | 'room_created' | 'room_deleted' | 'system_announcement';
|
||||
|
||||
export type Pager = {
|
||||
page: number;
|
||||
@ -3920,6 +3816,16 @@ export type PendingInvitationInfo = {
|
||||
expires_at?: string | null;
|
||||
};
|
||||
|
||||
export type MyWorkspaceInvitation = {
|
||||
workspace_id: string;
|
||||
workspace_slug: string;
|
||||
workspace_name: string;
|
||||
role: string;
|
||||
invited_by_username?: string | null;
|
||||
invited_at: string;
|
||||
expires_at?: string | null;
|
||||
};
|
||||
|
||||
export type PrCommitResponse = {
|
||||
oid: string;
|
||||
short_oid: string;
|
||||
@ -4030,15 +3936,6 @@ export type ProjectBillingHistoryResponse = {
|
||||
list: Array<ProjectBillingHistoryItem>;
|
||||
};
|
||||
|
||||
export type ProjectFollowItem = {
|
||||
uid: string;
|
||||
name: string;
|
||||
display_name: string;
|
||||
description?: string | null;
|
||||
is_public: boolean;
|
||||
followed_at: string;
|
||||
};
|
||||
|
||||
export type ProjectInfoKeyValue = {
|
||||
key: string;
|
||||
value: string;
|
||||
@ -4304,20 +4201,15 @@ export type RepoSearchItem = {
|
||||
created_at: string;
|
||||
};
|
||||
|
||||
export type RepoStarItem = {
|
||||
uid: string;
|
||||
repo_name: string;
|
||||
owner: string;
|
||||
description?: string | null;
|
||||
is_private: boolean;
|
||||
default_branch: string;
|
||||
starred_at: string;
|
||||
};
|
||||
|
||||
export type ResetPasswordParams = {
|
||||
email: string;
|
||||
};
|
||||
|
||||
export type ConfirmResetPasswordParams = {
|
||||
token: string;
|
||||
new_password: string;
|
||||
};
|
||||
|
||||
export type ReviewCommentCreateRequest = {
|
||||
body: string;
|
||||
review?: string | null;
|
||||
@ -4456,7 +4348,7 @@ export type ReviewerInfo = {
|
||||
export type RoomAiResponse = {
|
||||
room: string;
|
||||
model: string;
|
||||
model_name?: string | null;
|
||||
model_name?: string;
|
||||
version?: string | null;
|
||||
call_count: number;
|
||||
last_call_at?: string | null;
|
||||
@ -4544,7 +4436,7 @@ export type RoomMessageCreateRequest = {
|
||||
content_type?: string | null;
|
||||
thread_id?: string | null;
|
||||
in_reply_to?: string | null;
|
||||
attachment_ids?: Array<string>;
|
||||
attachment_ids?: string[];
|
||||
};
|
||||
|
||||
export type RoomMessageListResponse = {
|
||||
@ -4567,11 +4459,7 @@ export type RoomMessageResponse = {
|
||||
send_at: string;
|
||||
revoked?: string | null;
|
||||
revoked_by?: string | null;
|
||||
/**
|
||||
* Highlighted content with <mark> tags around matched terms (for search results)
|
||||
*/
|
||||
highlighted_content?: string | null;
|
||||
attachment_ids?: Array<string>;
|
||||
attachment_ids?: string[];
|
||||
};
|
||||
|
||||
export type RoomMessageUpdateRequest = {
|
||||
@ -4812,15 +4700,6 @@ export type SubscriptionInfo = {
|
||||
is_active: boolean;
|
||||
};
|
||||
|
||||
export type SyncModelsResponse = {
|
||||
models_created: number;
|
||||
models_updated: number;
|
||||
versions_created: number;
|
||||
pricing_created: number;
|
||||
capabilities_created: number;
|
||||
profiles_created: number;
|
||||
};
|
||||
|
||||
export type TagCountResponse = {
|
||||
count: number;
|
||||
};
|
||||
@ -5066,32 +4945,6 @@ export type UpdateWebhookParams = {
|
||||
active?: boolean | null;
|
||||
};
|
||||
|
||||
export type UserActivityItem = {
|
||||
id: number;
|
||||
activity_type: string;
|
||||
action: string;
|
||||
title: string;
|
||||
resource_type?: string | null;
|
||||
resource_name?: string | null;
|
||||
metadata?: unknown;
|
||||
created_at: string;
|
||||
};
|
||||
|
||||
export type UserActivityResponse = {
|
||||
items: Array<UserActivityItem>;
|
||||
total: number;
|
||||
page: number;
|
||||
per_page: number;
|
||||
};
|
||||
|
||||
export type UserCard = {
|
||||
user_uid: string;
|
||||
username: string;
|
||||
display_name?: string | null;
|
||||
avatar_url?: string | null;
|
||||
is_following_me: boolean;
|
||||
};
|
||||
|
||||
export type UserInfo = {
|
||||
uid: string;
|
||||
username: string;
|
||||
@ -5170,10 +5023,8 @@ export type UserSearchItem = {
|
||||
created_at: string;
|
||||
};
|
||||
|
||||
export type UserStarsResponse = {
|
||||
repos: Array<RepoStarItem>;
|
||||
projects: Array<ProjectFollowItem>;
|
||||
total: number;
|
||||
export type Verify2FaParams = {
|
||||
code: string;
|
||||
};
|
||||
|
||||
export type WatchCountResponse = {
|
||||
@ -5216,13 +5067,6 @@ export type WebhookResponse = {
|
||||
touch_count: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* Request body for accepting workspace invitation by slug.
|
||||
*/
|
||||
export type WorkspaceAcceptBySlugParams = {
|
||||
slug: string;
|
||||
};
|
||||
|
||||
export type WorkspaceActivityItem = {
|
||||
id: number;
|
||||
project_name: string;
|
||||
@ -5304,6 +5148,10 @@ export type WorkspaceInviteParams = {
|
||||
role?: string | null;
|
||||
};
|
||||
|
||||
export type WorkspaceAcceptBySlugParams = {
|
||||
slug: string;
|
||||
};
|
||||
|
||||
export type WorkspaceListItem = {
|
||||
id: string;
|
||||
slug: string;
|
||||
@ -5377,92 +5225,6 @@ export type WorkspaceUpdateParams = {
|
||||
billing_email?: string | null;
|
||||
};
|
||||
|
||||
export type AdminSyncModelsData = {
|
||||
body?: never;
|
||||
path?: never;
|
||||
query?: never;
|
||||
url: '/api/admin/ai/sync';
|
||||
};
|
||||
|
||||
export type AdminSyncModelsErrors = {
|
||||
/**
|
||||
* Invalid or missing admin API key
|
||||
*/
|
||||
401: unknown;
|
||||
/**
|
||||
* Sync failed
|
||||
*/
|
||||
500: unknown;
|
||||
};
|
||||
|
||||
export type AdminSyncModelsResponses = {
|
||||
/**
|
||||
* Sync result
|
||||
*/
|
||||
200: ApiResponseSyncModelsResponse;
|
||||
};
|
||||
|
||||
export type AdminSyncModelsResponse = AdminSyncModelsResponses[keyof AdminSyncModelsResponses];
|
||||
|
||||
export type AdminCheckAlertsData = {
|
||||
body?: never;
|
||||
path?: never;
|
||||
query?: never;
|
||||
url: '/api/admin/alerts/check';
|
||||
};
|
||||
|
||||
export type AdminCheckAlertsErrors = {
|
||||
/**
|
||||
* Invalid or missing admin API key
|
||||
*/
|
||||
401: unknown;
|
||||
};
|
||||
|
||||
export type AdminCheckAlertsResponses = {
|
||||
/**
|
||||
* Alert check result
|
||||
*/
|
||||
200: ApiResponseCheckAlertsResponse;
|
||||
};
|
||||
|
||||
export type AdminCheckAlertsResponse = AdminCheckAlertsResponses[keyof AdminCheckAlertsResponses];
|
||||
|
||||
export type AdminWorkspaceAddCreditData = {
|
||||
body: WorkspaceBillingAddCreditParams;
|
||||
path: {
|
||||
/**
|
||||
* Workspace slug
|
||||
*/
|
||||
slug: string;
|
||||
};
|
||||
query?: never;
|
||||
url: '/api/admin/workspaces/{slug}/add-credit';
|
||||
};
|
||||
|
||||
export type AdminWorkspaceAddCreditErrors = {
|
||||
/**
|
||||
* Invalid amount
|
||||
*/
|
||||
400: unknown;
|
||||
/**
|
||||
* Invalid or missing admin API key
|
||||
*/
|
||||
401: unknown;
|
||||
/**
|
||||
* Workspace not found
|
||||
*/
|
||||
404: unknown;
|
||||
};
|
||||
|
||||
export type AdminWorkspaceAddCreditResponses = {
|
||||
/**
|
||||
* Credit added
|
||||
*/
|
||||
200: ApiResponseWorkspaceBillingCurrentResponse;
|
||||
};
|
||||
|
||||
export type AdminWorkspaceAddCreditResponse = AdminWorkspaceAddCreditResponses[keyof AdminWorkspaceAddCreditResponses];
|
||||
|
||||
export type ModelCapabilityCreateData = {
|
||||
body: CreateModelCapabilityRequest;
|
||||
path?: never;
|
||||
@ -6155,6 +5917,146 @@ export type ModelPricingListResponses = {
|
||||
|
||||
export type ModelPricingListResponse = ModelPricingListResponses[keyof ModelPricingListResponses];
|
||||
|
||||
export type Api2FaDisableData = {
|
||||
body: Disable2FaParams;
|
||||
path?: never;
|
||||
query?: never;
|
||||
url: '/api/auth/2fa/disable';
|
||||
};
|
||||
|
||||
export type Api2FaDisableErrors = {
|
||||
/**
|
||||
* 2FA not enabled or invalid code/password
|
||||
*/
|
||||
400: unknown;
|
||||
/**
|
||||
* Unauthorized
|
||||
*/
|
||||
401: unknown;
|
||||
/**
|
||||
* Not found
|
||||
*/
|
||||
404: ApiResponseApiError;
|
||||
/**
|
||||
* Internal server error
|
||||
*/
|
||||
500: unknown;
|
||||
};
|
||||
|
||||
export type Api2FaDisableError = Api2FaDisableErrors[keyof Api2FaDisableErrors];
|
||||
|
||||
export type Api2FaDisableResponses = {
|
||||
/**
|
||||
* 2FA disabled
|
||||
*/
|
||||
200: unknown;
|
||||
};
|
||||
|
||||
export type Api2FaEnableData = {
|
||||
body?: never;
|
||||
path?: never;
|
||||
query?: never;
|
||||
url: '/api/auth/2fa/enable';
|
||||
};
|
||||
|
||||
export type Api2FaEnableErrors = {
|
||||
/**
|
||||
* Unauthorized
|
||||
*/
|
||||
401: unknown;
|
||||
/**
|
||||
* Not found
|
||||
*/
|
||||
404: ApiResponseApiError;
|
||||
/**
|
||||
* 2FA already enabled
|
||||
*/
|
||||
409: unknown;
|
||||
/**
|
||||
* Internal server error
|
||||
*/
|
||||
500: unknown;
|
||||
};
|
||||
|
||||
export type Api2FaEnableError = Api2FaEnableErrors[keyof Api2FaEnableErrors];
|
||||
|
||||
export type Api2FaEnableResponses = {
|
||||
/**
|
||||
* 2FA setup initiated
|
||||
*/
|
||||
200: Enable2FaResponse;
|
||||
};
|
||||
|
||||
export type Api2FaEnableResponse = Api2FaEnableResponses[keyof Api2FaEnableResponses];
|
||||
|
||||
export type Api2FaStatusData = {
|
||||
body?: never;
|
||||
path?: never;
|
||||
query?: never;
|
||||
url: '/api/auth/2fa/status';
|
||||
};
|
||||
|
||||
export type Api2FaStatusErrors = {
|
||||
/**
|
||||
* Unauthorized
|
||||
*/
|
||||
401: unknown;
|
||||
/**
|
||||
* Not found
|
||||
*/
|
||||
404: ApiResponseApiError;
|
||||
/**
|
||||
* Internal server error
|
||||
*/
|
||||
500: unknown;
|
||||
};
|
||||
|
||||
export type Api2FaStatusError = Api2FaStatusErrors[keyof Api2FaStatusErrors];
|
||||
|
||||
export type Api2FaStatusResponses = {
|
||||
/**
|
||||
* 2FA status
|
||||
*/
|
||||
200: Get2FaStatusResponse;
|
||||
};
|
||||
|
||||
export type Api2FaStatusResponse = Api2FaStatusResponses[keyof Api2FaStatusResponses];
|
||||
|
||||
export type Api2FaVerifyData = {
|
||||
body: Verify2FaParams;
|
||||
path?: never;
|
||||
query?: never;
|
||||
url: '/api/auth/2fa/verify';
|
||||
};
|
||||
|
||||
export type Api2FaVerifyErrors = {
|
||||
/**
|
||||
* 2FA not set up
|
||||
*/
|
||||
400: unknown;
|
||||
/**
|
||||
* Unauthorized or invalid code
|
||||
*/
|
||||
401: unknown;
|
||||
/**
|
||||
* Not found
|
||||
*/
|
||||
404: ApiResponseApiError;
|
||||
/**
|
||||
* Internal server error
|
||||
*/
|
||||
500: unknown;
|
||||
};
|
||||
|
||||
export type Api2FaVerifyError = Api2FaVerifyErrors[keyof Api2FaVerifyErrors];
|
||||
|
||||
export type Api2FaVerifyResponses = {
|
||||
/**
|
||||
* 2FA verified and enabled
|
||||
*/
|
||||
200: unknown;
|
||||
};
|
||||
|
||||
export type ApiAuthCaptchaData = {
|
||||
body: CaptchaQuery;
|
||||
path?: never;
|
||||
@ -6397,39 +6299,6 @@ export type ApiUserChangePasswordResponses = {
|
||||
|
||||
export type ApiUserChangePasswordResponse = ApiUserChangePasswordResponses[keyof ApiUserChangePasswordResponses];
|
||||
|
||||
export type ApiUserConfirmPasswordResetData = {
|
||||
body: ConfirmResetPasswordParams;
|
||||
path?: never;
|
||||
query?: never;
|
||||
url: '/api/auth/password/confirm';
|
||||
};
|
||||
|
||||
export type ApiUserConfirmPasswordResetErrors = {
|
||||
/**
|
||||
* Invalid or expired token
|
||||
*/
|
||||
400: ApiResponseApiError;
|
||||
/**
|
||||
* User not found
|
||||
*/
|
||||
404: ApiResponseApiError;
|
||||
/**
|
||||
* Internal server error
|
||||
*/
|
||||
500: ApiResponseApiError;
|
||||
};
|
||||
|
||||
export type ApiUserConfirmPasswordResetError = ApiUserConfirmPasswordResetErrors[keyof ApiUserConfirmPasswordResetErrors];
|
||||
|
||||
export type ApiUserConfirmPasswordResetResponses = {
|
||||
/**
|
||||
* Password reset confirmed
|
||||
*/
|
||||
200: ApiResponseString;
|
||||
};
|
||||
|
||||
export type ApiUserConfirmPasswordResetResponse = ApiUserConfirmPasswordResetResponses[keyof ApiUserConfirmPasswordResetResponses];
|
||||
|
||||
export type ApiUserRequestPasswordResetData = {
|
||||
body: ResetPasswordParams;
|
||||
path?: never;
|
||||
@ -6463,6 +6332,39 @@ export type ApiUserRequestPasswordResetResponses = {
|
||||
|
||||
export type ApiUserRequestPasswordResetResponse = ApiUserRequestPasswordResetResponses[keyof ApiUserRequestPasswordResetResponses];
|
||||
|
||||
export type ApiUserConfirmPasswordResetData = {
|
||||
body: ConfirmResetPasswordParams;
|
||||
path?: never;
|
||||
query?: never;
|
||||
url: '/api/auth/password/confirm';
|
||||
};
|
||||
|
||||
export type ApiUserConfirmPasswordResetErrors = {
|
||||
/**
|
||||
* Invalid or expired token
|
||||
*/
|
||||
400: ApiResponseApiError;
|
||||
/**
|
||||
* User not found
|
||||
*/
|
||||
404: ApiResponseApiError;
|
||||
/**
|
||||
* Internal server error
|
||||
*/
|
||||
500: ApiResponseApiError;
|
||||
};
|
||||
|
||||
export type ApiUserConfirmPasswordResetError = ApiUserConfirmPasswordResetErrors[keyof ApiUserConfirmPasswordResetErrors];
|
||||
|
||||
export type ApiUserConfirmPasswordResetResponses = {
|
||||
/**
|
||||
* Password reset confirmed
|
||||
*/
|
||||
200: ApiResponseString;
|
||||
};
|
||||
|
||||
export type ApiUserConfirmPasswordResetResponse = ApiUserConfirmPasswordResetResponses[keyof ApiUserConfirmPasswordResetResponses];
|
||||
|
||||
export type ApiAuthRegisterData = {
|
||||
body: RegisterParams;
|
||||
path?: never;
|
||||
@ -14901,7 +14803,7 @@ export type GitReadmeData = {
|
||||
};
|
||||
query?: {
|
||||
/**
|
||||
* Git reference (branch, tag, commit). Defaults to the repository's default branch.
|
||||
* Git reference (branch, tag, commit). Defaults to HEAD.
|
||||
*/
|
||||
ref?: string;
|
||||
};
|
||||
@ -18059,38 +17961,6 @@ export type GetProfileByUsernameResponses = {
|
||||
|
||||
export type GetProfileByUsernameResponse = GetProfileByUsernameResponses[keyof GetProfileByUsernameResponses];
|
||||
|
||||
export type GetUserActivityData = {
|
||||
body?: never;
|
||||
path: {
|
||||
username: string;
|
||||
};
|
||||
query?: {
|
||||
page?: number;
|
||||
per_page?: number;
|
||||
};
|
||||
url: '/api/users/{username}/activity';
|
||||
};
|
||||
|
||||
export type GetUserActivityErrors = {
|
||||
/**
|
||||
* Unauthorized
|
||||
*/
|
||||
401: unknown;
|
||||
/**
|
||||
* Not found
|
||||
*/
|
||||
404: unknown;
|
||||
};
|
||||
|
||||
export type GetUserActivityResponses = {
|
||||
/**
|
||||
* Get user activity
|
||||
*/
|
||||
200: ApiResponseUserActivityResponse;
|
||||
};
|
||||
|
||||
export type GetUserActivityResponse = GetUserActivityResponses[keyof GetUserActivityResponses];
|
||||
|
||||
export type UnsubscribeTargetData = {
|
||||
body?: never;
|
||||
path: {
|
||||
@ -18224,35 +18094,6 @@ export type GetSubscriberCountResponses = {
|
||||
200: unknown;
|
||||
};
|
||||
|
||||
export type GetFollowingListData = {
|
||||
body?: never;
|
||||
path: {
|
||||
username: string;
|
||||
};
|
||||
query?: never;
|
||||
url: '/api/users/{username}/following';
|
||||
};
|
||||
|
||||
export type GetFollowingListErrors = {
|
||||
/**
|
||||
* Unauthorized
|
||||
*/
|
||||
401: unknown;
|
||||
/**
|
||||
* Not found
|
||||
*/
|
||||
404: unknown;
|
||||
};
|
||||
|
||||
export type GetFollowingListResponses = {
|
||||
/**
|
||||
* List following users
|
||||
*/
|
||||
200: ApiResponseVecUserCard;
|
||||
};
|
||||
|
||||
export type GetFollowingListResponse = GetFollowingListResponses[keyof GetFollowingListResponses];
|
||||
|
||||
export type GetSubscriptionCountData = {
|
||||
body?: never;
|
||||
path: {
|
||||
@ -18396,35 +18237,6 @@ export type GetUserReposResponses = {
|
||||
|
||||
export type GetUserReposResponse = GetUserReposResponses[keyof GetUserReposResponses];
|
||||
|
||||
export type GetUserStarsData = {
|
||||
body?: never;
|
||||
path: {
|
||||
username: string;
|
||||
};
|
||||
query?: never;
|
||||
url: '/api/users/{username}/stars';
|
||||
};
|
||||
|
||||
export type GetUserStarsErrors = {
|
||||
/**
|
||||
* Unauthorized
|
||||
*/
|
||||
401: unknown;
|
||||
/**
|
||||
* Not found
|
||||
*/
|
||||
404: unknown;
|
||||
};
|
||||
|
||||
export type GetUserStarsResponses = {
|
||||
/**
|
||||
* Get user stars
|
||||
*/
|
||||
200: ApiResponseUserStarsResponse;
|
||||
};
|
||||
|
||||
export type GetUserStarsResponse = GetUserStarsResponses[keyof GetUserStarsResponses];
|
||||
|
||||
export type WorkspaceCreateData = {
|
||||
body: WorkspaceInitParams;
|
||||
path?: never;
|
||||
@ -18483,6 +18295,35 @@ export type WorkspaceAcceptInvitationResponses = {
|
||||
|
||||
export type WorkspaceAcceptInvitationResponse = WorkspaceAcceptInvitationResponses[keyof WorkspaceAcceptInvitationResponses];
|
||||
|
||||
export type WorkspaceMyInvitationsData = {
|
||||
body?: never;
|
||||
path?: never;
|
||||
query?: never;
|
||||
url: '/api/workspaces/me/invitations';
|
||||
};
|
||||
|
||||
export type WorkspaceMyInvitationsErrors = {
|
||||
/**
|
||||
* Unauthorized
|
||||
*/
|
||||
401: unknown;
|
||||
};
|
||||
|
||||
export type ApiResponseVecMyWorkspaceInvitation = {
|
||||
code: number;
|
||||
message: string;
|
||||
data?: Array<MyWorkspaceInvitation>;
|
||||
};
|
||||
|
||||
export type WorkspaceMyInvitationsResponses = {
|
||||
/**
|
||||
* List my workspace invitations
|
||||
*/
|
||||
200: ApiResponseVecMyWorkspaceInvitation;
|
||||
};
|
||||
|
||||
export type WorkspaceMyInvitationsResponse = WorkspaceMyInvitationsResponses[keyof WorkspaceMyInvitationsResponses];
|
||||
|
||||
export type WorkspaceAcceptInvitationBySlugData = {
|
||||
body: WorkspaceAcceptBySlugParams;
|
||||
path?: never;
|
||||
@ -18491,10 +18332,6 @@ export type WorkspaceAcceptInvitationBySlugData = {
|
||||
};
|
||||
|
||||
export type WorkspaceAcceptInvitationBySlugErrors = {
|
||||
/**
|
||||
* Invalid or expired token
|
||||
*/
|
||||
400: unknown;
|
||||
/**
|
||||
* Unauthorized
|
||||
*/
|
||||
@ -18541,29 +18378,6 @@ export type WorkspaceListResponses = {
|
||||
|
||||
export type WorkspaceListResponse2 = WorkspaceListResponses[keyof WorkspaceListResponses];
|
||||
|
||||
export type WorkspaceMyInvitationsData = {
|
||||
body?: never;
|
||||
path?: never;
|
||||
query?: never;
|
||||
url: '/api/workspaces/me/invitations';
|
||||
};
|
||||
|
||||
export type WorkspaceMyInvitationsErrors = {
|
||||
/**
|
||||
* Unauthorized
|
||||
*/
|
||||
401: unknown;
|
||||
};
|
||||
|
||||
export type WorkspaceMyInvitationsResponses = {
|
||||
/**
|
||||
* List my workspace invitations
|
||||
*/
|
||||
200: ApiResponseVecMyWorkspaceInvitation;
|
||||
};
|
||||
|
||||
export type WorkspaceMyInvitationsResponse = WorkspaceMyInvitationsResponses[keyof WorkspaceMyInvitationsResponses];
|
||||
|
||||
export type WorkspaceDeleteData = {
|
||||
body?: never;
|
||||
path: {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user