mirror of
https://github.com/Dokploy/dokploy.git
synced 2026-06-16 20:55:21 +02:00
Compare commits
7 Commits
v0.29.3
...
feat/add-c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d54aa02ad2 | ||
|
|
734641b516 | ||
|
|
cee2e9f002 | ||
|
|
e0b4a13340 | ||
|
|
ec202c8c6e | ||
|
|
7e89eaed4a | ||
|
|
e508f3143f |
1
apps/dokploy/.tool-embeddings.json
Normal file
1
apps/dokploy/.tool-embeddings.json
Normal file
File diff suppressed because one or more lines are too long
629
apps/dokploy/components/dashboard/ai-chat/chat-panel.tsx
Normal file
629
apps/dokploy/components/dashboard/ai-chat/chat-panel.tsx
Normal file
@@ -0,0 +1,629 @@
|
||||
"use client";
|
||||
|
||||
import { useChat } from "@ai-sdk/react";
|
||||
import type { ChatContext } from "@dokploy/server/utils/ai/chat-tools";
|
||||
import { DefaultChatTransport } from "ai";
|
||||
import {
|
||||
Bot,
|
||||
Check,
|
||||
ChevronDown,
|
||||
Loader2,
|
||||
Send,
|
||||
Trash2,
|
||||
Wrench,
|
||||
X,
|
||||
} from "lucide-react";
|
||||
import { useRouter } from "next/router";
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import ReactMarkdown from "react-markdown";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Collapsible,
|
||||
CollapsibleContent,
|
||||
CollapsibleTrigger,
|
||||
} from "@/components/ui/collapsible";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import {
|
||||
Sheet,
|
||||
SheetContent,
|
||||
SheetDescription,
|
||||
SheetHeader,
|
||||
SheetTitle,
|
||||
} from "@/components/ui/sheet";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { api } from "@/utils/api";
|
||||
|
||||
function useChatContext(): ChatContext {
|
||||
const router = useRouter();
|
||||
const { query, pathname } = router;
|
||||
|
||||
return useMemo(() => {
|
||||
const projectId =
|
||||
typeof query.projectId === "string" ? query.projectId : undefined;
|
||||
const environmentId =
|
||||
typeof query.environmentId === "string"
|
||||
? query.environmentId
|
||||
: undefined;
|
||||
const serverId =
|
||||
typeof query.serverId === "string" ? query.serverId : undefined;
|
||||
|
||||
const serviceParams = [
|
||||
{ key: "applicationId", type: "application" },
|
||||
{ key: "composeId", type: "compose" },
|
||||
{ key: "postgresId", type: "postgres" },
|
||||
{ key: "mysqlId", type: "mysql" },
|
||||
{ key: "redisId", type: "redis" },
|
||||
{ key: "mongoId", type: "mongo" },
|
||||
{ key: "mariadbId", type: "mariadb" },
|
||||
{ key: "libsqlId", type: "libsql" },
|
||||
] as const;
|
||||
|
||||
for (const { key, type } of serviceParams) {
|
||||
if (query[key] && typeof query[key] === "string") {
|
||||
return {
|
||||
type,
|
||||
id: query[key] as string,
|
||||
projectId,
|
||||
environmentId,
|
||||
serverId,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (query.projectId && typeof query.projectId === "string") {
|
||||
return {
|
||||
type: "project" as const,
|
||||
id: query.projectId,
|
||||
projectId,
|
||||
environmentId,
|
||||
serverId,
|
||||
};
|
||||
}
|
||||
return {
|
||||
type: "general" as const,
|
||||
id: "",
|
||||
projectId,
|
||||
environmentId,
|
||||
serverId,
|
||||
};
|
||||
}, [
|
||||
query.applicationId,
|
||||
query.composeId,
|
||||
query.postgresId,
|
||||
query.mysqlId,
|
||||
query.redisId,
|
||||
query.mongoId,
|
||||
query.mariadbId,
|
||||
query.libsqlId,
|
||||
query.projectId,
|
||||
query.environmentId,
|
||||
query.serverId,
|
||||
pathname,
|
||||
]);
|
||||
}
|
||||
|
||||
export function ChatPanel() {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [aiId, setAiId] = useState<string>("");
|
||||
const [input, setInput] = useState("");
|
||||
const scrollRef = useRef<HTMLDivElement>(null);
|
||||
const inputRef = useRef<HTMLTextAreaElement>(null);
|
||||
const context = useChatContext();
|
||||
const aiIdRef = useRef(aiId);
|
||||
const contextRef = useRef(context);
|
||||
aiIdRef.current = aiId;
|
||||
contextRef.current = context;
|
||||
|
||||
const { data: isCloud } = api.settings.isCloud.useQuery(undefined, {
|
||||
refetchOnWindowFocus: false,
|
||||
});
|
||||
|
||||
const { data: providers } = api.ai.getEnabledProviders.useQuery(undefined, {
|
||||
refetchOnWindowFocus: false,
|
||||
enabled: !isCloud,
|
||||
});
|
||||
|
||||
const enabledProviders = providers ?? [];
|
||||
|
||||
const STORAGE_KEY = "dokploy-chat-messages";
|
||||
const restoredRef = useRef(false);
|
||||
|
||||
const { messages, sendMessage, status, setMessages, addToolApprovalResponse } = useChat({
|
||||
id: "dokploy-chat",
|
||||
transport: new DefaultChatTransport({
|
||||
api: "/api/ai/chat",
|
||||
body: () => ({
|
||||
...(isCloud ? {} : { aiId: aiIdRef.current }),
|
||||
context: contextRef.current,
|
||||
}),
|
||||
}),
|
||||
});
|
||||
|
||||
const isLoading = status === "streaming" || status === "submitted";
|
||||
|
||||
// Restore messages from localStorage on mount
|
||||
useEffect(() => {
|
||||
if (restoredRef.current) return;
|
||||
restoredRef.current = true;
|
||||
try {
|
||||
const stored = localStorage.getItem(STORAGE_KEY);
|
||||
if (stored) {
|
||||
const parsed = JSON.parse(stored);
|
||||
if (Array.isArray(parsed) && parsed.length > 0) {
|
||||
setMessages(parsed);
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}, [setMessages]);
|
||||
|
||||
// Persist messages to localStorage
|
||||
useEffect(() => {
|
||||
if (!restoredRef.current) return;
|
||||
if (messages.length > 0) {
|
||||
try {
|
||||
const toStore = messages.slice(-50);
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(toStore));
|
||||
} catch {
|
||||
// localStorage full or unavailable — ignore
|
||||
}
|
||||
}
|
||||
}, [messages]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isCloud && !aiId && enabledProviders.length > 0 && enabledProviders[0]) {
|
||||
setAiId(enabledProviders[0].aiId);
|
||||
}
|
||||
}, [enabledProviders, aiId, isCloud]);
|
||||
|
||||
useEffect(() => {
|
||||
if (scrollRef.current) {
|
||||
scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
|
||||
}
|
||||
}, [messages, status]);
|
||||
|
||||
if (!isCloud && enabledProviders.length === 0) return null;
|
||||
|
||||
const handleSend = () => {
|
||||
if (!input.trim() || isLoading) return;
|
||||
if (!isCloud && !aiId) return;
|
||||
sendMessage({ text: input });
|
||||
setInput("");
|
||||
setTimeout(() => inputRef.current?.focus(), 0);
|
||||
};
|
||||
|
||||
const contextLabel =
|
||||
context.type === "general" ? "General" : context.type;
|
||||
|
||||
const lastMessage = messages[messages.length - 1];
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
onClick={() => setOpen(true)}
|
||||
variant="outline"
|
||||
className="fixed bottom-6 right-6 z-50 h-11 w-11 rounded-full shadow-md border"
|
||||
size="icon"
|
||||
>
|
||||
<Bot className="h-5 w-5" />
|
||||
</Button>
|
||||
|
||||
<Sheet open={open} onOpenChange={setOpen}>
|
||||
<SheetContent
|
||||
side="right"
|
||||
className="w-full sm:w-[480px] p-0 flex flex-col border-l outline-none"
|
||||
>
|
||||
<SheetHeader className="px-4 py-3 border-b shrink-0">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<Bot className="h-4 w-4 text-muted-foreground" />
|
||||
<SheetTitle className="text-sm font-medium">
|
||||
{isCloud ? "Dokploy Agent" : "AI Assistant"}
|
||||
</SheetTitle>
|
||||
{isLoading && (
|
||||
<span className="text-xs text-muted-foreground animate-pulse">
|
||||
working...
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<SheetDescription className="sr-only">
|
||||
Chat with AI to manage your infrastructure
|
||||
</SheetDescription>
|
||||
<div className="flex items-center gap-2 pt-1">
|
||||
{!isCloud && (
|
||||
<Select value={aiId} onValueChange={setAiId}>
|
||||
<SelectTrigger className="h-8 text-xs flex-1">
|
||||
<SelectValue placeholder="Select provider..." />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{enabledProviders.map((p) => (
|
||||
<SelectItem key={p.aiId} value={p.aiId}>
|
||||
{p.name} ({p.model})
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
)}
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="text-xs shrink-0 capitalize font-normal"
|
||||
>
|
||||
{contextLabel}
|
||||
</Badge>
|
||||
{messages.length > 0 && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-8 w-8 shrink-0"
|
||||
onClick={() => { setMessages([]); localStorage.removeItem(STORAGE_KEY); }}
|
||||
title="Clear chat"
|
||||
>
|
||||
<Trash2 className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</SheetHeader>
|
||||
|
||||
<div
|
||||
ref={scrollRef}
|
||||
className="flex-1 overflow-y-auto p-4 space-y-3"
|
||||
>
|
||||
{messages.length === 0 && (
|
||||
<div className="flex flex-col items-center justify-center h-full text-muted-foreground gap-3">
|
||||
<Bot className="h-8 w-8 opacity-30" />
|
||||
<p className="text-sm text-center">
|
||||
Ask me anything about your{" "}
|
||||
{context.type === "general"
|
||||
? "infrastructure"
|
||||
: context.type}
|
||||
</p>
|
||||
<div className="flex flex-wrap gap-1.5 justify-center">
|
||||
{(context.type === "application"
|
||||
? [
|
||||
"What's the status of this app?",
|
||||
"Why did the last build fail?",
|
||||
"Show me recent deployments",
|
||||
"Redeploy this app",
|
||||
]
|
||||
: context.type === "compose"
|
||||
? [
|
||||
"Show compose service status",
|
||||
"Why did the last deploy fail?",
|
||||
"Show me the domains",
|
||||
"Redeploy this service",
|
||||
]
|
||||
: context.type === "postgres" ||
|
||||
context.type === "mysql" ||
|
||||
context.type === "redis" ||
|
||||
context.type === "mongo" ||
|
||||
context.type === "mariadb" ||
|
||||
context.type === "libsql"
|
||||
? [
|
||||
`Show ${context.type} status`,
|
||||
"What's the connection info?",
|
||||
"Show recent deployments",
|
||||
"Restart this database",
|
||||
]
|
||||
: context.type === "project"
|
||||
? [
|
||||
"How many services do I have?",
|
||||
"Show me all environments",
|
||||
"Which services are failing?",
|
||||
]
|
||||
: [
|
||||
"List all my projects",
|
||||
"Show project overview",
|
||||
"What servers do I have?",
|
||||
]
|
||||
).map((suggestion) => (
|
||||
<Button
|
||||
key={suggestion}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="text-xs h-7 font-normal"
|
||||
onClick={() => setInput(suggestion)}
|
||||
>
|
||||
{suggestion}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{messages.map((message) => {
|
||||
if (message.role === "user") {
|
||||
return (
|
||||
<div key={message.id} className="flex justify-end">
|
||||
<div className="max-w-[85%] rounded-lg px-3 py-2 text-sm bg-muted">
|
||||
<p className="whitespace-pre-wrap">
|
||||
{message.parts
|
||||
.filter(
|
||||
(p): p is { type: "text"; text: string } =>
|
||||
p.type === "text",
|
||||
)
|
||||
.map((p) => p.text)
|
||||
.join("")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div key={message.id} className="flex justify-start">
|
||||
<div className="max-w-[90%] space-y-2">
|
||||
{message.parts.map((part, i) => {
|
||||
if (
|
||||
part.type === "text" &&
|
||||
(part as { text?: string }).text?.trim()
|
||||
) {
|
||||
return (
|
||||
<div
|
||||
key={`text-${message.id}-${i}`}
|
||||
className="rounded-lg border px-3 py-2 text-sm prose prose-sm dark:prose-invert max-w-none break-words"
|
||||
>
|
||||
<ReactMarkdown>
|
||||
{(part as { text: string }).text}
|
||||
</ReactMarkdown>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (part.type === "dynamic-tool") {
|
||||
return (
|
||||
<div
|
||||
key={part.toolCallId}
|
||||
className="rounded-lg border px-3 py-2"
|
||||
>
|
||||
<ToolCallDisplay
|
||||
toolCallId={part.toolCallId}
|
||||
toolName={part.toolName}
|
||||
state={part.state}
|
||||
input={(part as any).input}
|
||||
output={
|
||||
part.state === "output-available"
|
||||
? part.output
|
||||
: undefined
|
||||
}
|
||||
onApprove={(id) =>
|
||||
addToolApprovalResponse({
|
||||
id,
|
||||
approved: true,
|
||||
})
|
||||
}
|
||||
onDeny={(id) =>
|
||||
addToolApprovalResponse({
|
||||
id,
|
||||
approved: false,
|
||||
reason: "User denied",
|
||||
})
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (part.type === "reasoning") {
|
||||
return (
|
||||
<Collapsible key={`reasoning-${message.id}-${i}`}>
|
||||
<CollapsibleTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
className="flex items-center gap-1.5 text-xs text-muted-foreground hover:text-foreground transition-colors"
|
||||
>
|
||||
<Bot className="h-3 w-3" />
|
||||
<span>Thinking...</span>
|
||||
<ChevronDown className="h-3 w-3" />
|
||||
</button>
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent>
|
||||
<div className="mt-1 rounded-lg border px-3 py-2 text-xs text-muted-foreground italic">
|
||||
{(part as any).text ||
|
||||
(part as any).reasoning}
|
||||
</div>
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
{isLoading && lastMessage?.role === "user" && (
|
||||
<div className="flex justify-start">
|
||||
<div className="rounded-lg border px-3 py-2 flex items-center gap-2 text-xs text-muted-foreground">
|
||||
<Loader2 className="h-3 w-3 animate-spin" />
|
||||
Investigating...
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="border-t p-3 shrink-0 flex gap-2">
|
||||
<Textarea
|
||||
ref={inputRef}
|
||||
value={input}
|
||||
onChange={(e) => setInput(e.target.value)}
|
||||
placeholder={
|
||||
!isCloud && !aiId
|
||||
? "Select a provider first..."
|
||||
: "Ask anything..."
|
||||
}
|
||||
disabled={(!isCloud && !aiId) || isLoading}
|
||||
className="min-h-[40px] max-h-[120px] resize-none text-sm"
|
||||
rows={1}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter" && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
handleSend();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
size="icon"
|
||||
variant="outline"
|
||||
disabled={
|
||||
(!isCloud && !aiId) || !input.trim() || isLoading
|
||||
}
|
||||
className="shrink-0 h-10 w-10"
|
||||
onClick={handleSend}
|
||||
>
|
||||
<Send className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function ToolCallDisplay({
|
||||
toolCallId,
|
||||
toolName,
|
||||
state,
|
||||
input,
|
||||
output,
|
||||
onApprove,
|
||||
onDeny,
|
||||
}: {
|
||||
toolCallId: string;
|
||||
toolName: string;
|
||||
state: string;
|
||||
input?: unknown;
|
||||
output?: unknown;
|
||||
onApprove?: (id: string) => void;
|
||||
onDeny?: (id: string) => void;
|
||||
}) {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const isRunning =
|
||||
state === "input-streaming" || state === "input-available";
|
||||
const isDone = state === "output-available";
|
||||
const isError = state === "output-error";
|
||||
const needsApproval = state === "requires-approval";
|
||||
|
||||
const outputText = output
|
||||
? typeof output === "string"
|
||||
? output
|
||||
: JSON.stringify(output, null, 2)
|
||||
: null;
|
||||
|
||||
// Extract operationId and params from input
|
||||
const inputData = input as { operationId?: string; params?: Record<string, unknown> } | undefined;
|
||||
const operationId = inputData?.operationId;
|
||||
const params = inputData?.params;
|
||||
|
||||
// Format: "compose-one" → "compose → one"
|
||||
const displayLabel = operationId
|
||||
? operationId.replace("-", " → ")
|
||||
: toolName;
|
||||
|
||||
// Determine HTTP method hint from operationId
|
||||
const isReadOp = operationId?.match(/^(.*-)?(one|all|get|list|read|search|by)/i);
|
||||
|
||||
const StatusIcon = isRunning
|
||||
? () => <Loader2 className="h-3.5 w-3.5 animate-spin text-blue-500 shrink-0" />
|
||||
: isDone
|
||||
? () => <Check className="h-3.5 w-3.5 text-green-500 shrink-0" />
|
||||
: isError
|
||||
? () => <X className="h-3.5 w-3.5 text-red-500 shrink-0" />
|
||||
: () => <Wrench className="h-3.5 w-3.5 text-muted-foreground shrink-0" />;
|
||||
|
||||
if (needsApproval) {
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2 text-xs">
|
||||
<Wrench className="h-3.5 w-3.5 text-yellow-500 shrink-0" />
|
||||
<code className="font-mono text-xs font-medium">{displayLabel}</code>
|
||||
<Badge variant="outline" className="text-[10px] px-1 py-0 h-4 font-normal">
|
||||
write
|
||||
</Badge>
|
||||
</div>
|
||||
{params && Object.keys(params).length > 0 && (
|
||||
<div className="ml-5.5 flex flex-wrap gap-1">
|
||||
{Object.entries(params).map(([key, value]) => (
|
||||
<span key={key} className="text-[10px] bg-muted px-1.5 py-0.5 rounded font-mono">
|
||||
{key}={typeof value === "string" ? `"${value}"` : String(value)}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<div className="flex gap-1.5 ml-5.5">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="h-6 text-xs px-2"
|
||||
onClick={() => onApprove?.(toolCallId)}
|
||||
>
|
||||
<Check className="h-3 w-3 mr-1" />
|
||||
Approve
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-6 text-xs px-2"
|
||||
onClick={() => onDeny?.(toolCallId)}
|
||||
>
|
||||
<X className="h-3 w-3 mr-1" />
|
||||
Deny
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-1">
|
||||
<Collapsible open={isOpen} onOpenChange={setIsOpen}>
|
||||
<CollapsibleTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
className="flex items-center gap-2 text-xs w-full hover:bg-muted/50 rounded -mx-1 px-1 py-0.5 transition-colors"
|
||||
>
|
||||
<StatusIcon />
|
||||
<code className="font-mono text-xs font-medium">{displayLabel}</code>
|
||||
{isReadOp && (
|
||||
<Badge variant="secondary" className="text-[10px] px-1 py-0 h-4 font-normal">
|
||||
read
|
||||
</Badge>
|
||||
)}
|
||||
{params && Object.keys(params).length > 0 && (
|
||||
<span className="text-[10px] text-muted-foreground truncate">
|
||||
{Object.entries(params)
|
||||
.slice(0, 3)
|
||||
.map(([k, v]) => `${k}=${typeof v === "string" ? `"${String(v).slice(0, 20)}"` : String(v)}`)
|
||||
.join(", ")}
|
||||
{Object.keys(params).length > 3 ? ` +${Object.keys(params).length - 3}` : ""}
|
||||
</span>
|
||||
)}
|
||||
{(outputText || isRunning) && (
|
||||
<ChevronDown
|
||||
className={`h-3 w-3 ml-auto text-muted-foreground transition-transform shrink-0 ${isOpen ? "rotate-180" : ""}`}
|
||||
/>
|
||||
)}
|
||||
</button>
|
||||
</CollapsibleTrigger>
|
||||
{outputText && (
|
||||
<CollapsibleContent>
|
||||
<pre className="mt-1 ml-5.5 p-2 bg-muted/50 rounded text-[10px] overflow-x-auto max-h-[200px] overflow-y-auto leading-tight whitespace-pre-wrap break-words">
|
||||
{outputText}
|
||||
</pre>
|
||||
</CollapsibleContent>
|
||||
)}
|
||||
</Collapsible>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import { api } from "@/utils/api";
|
||||
import { ChatPanel } from "../dashboard/ai-chat/chat-panel";
|
||||
import { ImpersonationBar } from "../dashboard/impersonation/impersonation-bar";
|
||||
import { HubSpotWidget } from "../shared/HubSpotWidget";
|
||||
import Page from "./side";
|
||||
@@ -23,6 +24,7 @@ export const DashboardLayout = ({ children }: Props) => {
|
||||
return (
|
||||
<>
|
||||
<Page>{children}</Page>
|
||||
<ChatPanel />
|
||||
{isChatEnabled && (
|
||||
<>
|
||||
<HubSpotWidget />
|
||||
|
||||
@@ -46,6 +46,7 @@
|
||||
"@ai-sdk/mistral": "^3.0.20",
|
||||
"@ai-sdk/openai": "^3.0.29",
|
||||
"@ai-sdk/openai-compatible": "^2.0.30",
|
||||
"@ai-sdk/react": "^3.0.156",
|
||||
"@better-auth/api-key": "1.5.4",
|
||||
"@better-auth/sso": "1.5.4",
|
||||
"@codemirror/autocomplete": "^6.18.6",
|
||||
@@ -57,7 +58,7 @@
|
||||
"@codemirror/search": "^6.6.0",
|
||||
"@codemirror/view": "^6.39.15",
|
||||
"@dokploy/server": "workspace:*",
|
||||
"@dokploy/trpc-openapi": "0.0.18",
|
||||
"@dokploy/trpc-openapi": "0.0.19",
|
||||
"@faker-js/faker": "^8.4.1",
|
||||
"@hookform/resolvers": "^5.2.2",
|
||||
"@octokit/auth-app": "^6.1.3",
|
||||
|
||||
251
apps/dokploy/pages/api/ai/chat.ts
Normal file
251
apps/dokploy/pages/api/ai/chat.ts
Normal file
@@ -0,0 +1,251 @@
|
||||
import { readFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { IS_CLOUD, validateRequest } from "@dokploy/server";
|
||||
import { getAiSettingById } from "@dokploy/server/services/ai";
|
||||
import {
|
||||
type ChatContext,
|
||||
getAllTools,
|
||||
} from "@dokploy/server/utils/ai/chat-tools";
|
||||
import {
|
||||
buildEndpointCatalog,
|
||||
createApiTool,
|
||||
} from "@dokploy/server/utils/ai/api-tool";
|
||||
import {
|
||||
getOrCreateEmbeddings,
|
||||
retrieveRelevantEndpoints,
|
||||
} from "@dokploy/server/utils/ai/tool-retrieval";
|
||||
import { selectAIProvider } from "@dokploy/server/utils/ai/select-ai-provider";
|
||||
import { createAnthropic } from "@ai-sdk/anthropic";
|
||||
import { convertToModelMessages, stepCountIs, streamText } from "ai";
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
let cachedSpec: any = null;
|
||||
|
||||
function getOpenApiSpec() {
|
||||
if (!cachedSpec) {
|
||||
try {
|
||||
const specPath = join(process.cwd(), "../../openapi.json");
|
||||
cachedSpec = JSON.parse(readFileSync(specPath, "utf-8"));
|
||||
} catch {
|
||||
cachedSpec = null;
|
||||
}
|
||||
}
|
||||
return cachedSpec;
|
||||
}
|
||||
|
||||
function buildContextBlock(context: ChatContext): string {
|
||||
if (context.type === "general") {
|
||||
return "CONTEXT: The user is on the general dashboard (no specific resource selected). Use project-all to list their projects if needed.";
|
||||
}
|
||||
|
||||
const lines: string[] = [];
|
||||
lines.push(
|
||||
`CONTEXT: The user is currently viewing a specific ${context.type}. The ${context.type}Id is "${context.id}".`,
|
||||
);
|
||||
lines.push(
|
||||
`When the user says "this app", "this service", "this database", "add env var", etc., they ALWAYS mean this ${context.type} (ID: "${context.id}"). NEVER ask which service they mean.`,
|
||||
);
|
||||
|
||||
if (context.projectId) {
|
||||
lines.push(`- projectId: "${context.projectId}"`);
|
||||
}
|
||||
if (context.environmentId) {
|
||||
lines.push(`- environmentId: "${context.environmentId}"`);
|
||||
}
|
||||
if (context.serverId) {
|
||||
lines.push(`- serverId: "${context.serverId}"`);
|
||||
}
|
||||
|
||||
lines.push(
|
||||
"Use these IDs directly when calling tools — do NOT ask the user for them. You already know exactly which resource the user is talking about.",
|
||||
);
|
||||
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
function buildSystemPrompt(context: ChatContext, catalog: string | null, endpointCount?: number) {
|
||||
const contextBlock = buildContextBlock(context);
|
||||
|
||||
return `You are an autonomous DevOps agent inside Dokploy (Docker-based PaaS). You take action immediately — you don't explain, you don't ask, you DO.
|
||||
|
||||
${contextBlock}
|
||||
|
||||
THINKING PROCESS (do this before EVERY action):
|
||||
1. Scan ALL section headers (## tag — description) in the ENDPOINT CATALOG to find which sections are relevant
|
||||
2. Read the endpoint descriptions in those sections to pick the right operationId
|
||||
3. Call the endpoint with the correct params — use the IDs from the context above
|
||||
|
||||
BEHAVIOR:
|
||||
- When the user asks you to do something → DO IT. Call the API right away.
|
||||
- When you need information → call the endpoint to get it. Never say "I can't access" or "I don't have the ability to".
|
||||
- When something fails → read the error, figure out the fix, and apply it. Don't stop to explain the error — fix it.
|
||||
- EVERY capability you need is in the ENDPOINT CATALOG below. If you think you can't do something, you're wrong — scan ALL sections again.
|
||||
- You already have all the IDs you need from the context above. NEVER ask the user for IDs, paths, or information you can discover by calling endpoints.
|
||||
- NEVER ask for confirmation or permission. The only exception is deleting a service entirely. For everything else (read, update, deploy, stop, start, restart) → just do it immediately.
|
||||
|
||||
KEY PATTERN: When you need to explore files, find paths, or check repository structure → use the "patch" section endpoints to browse directories and read files. NEVER ask the user for file paths.
|
||||
|
||||
DATA MODEL: Project → Environment → Services (application, compose, postgres, mysql, redis, mongo, mariadb, libsql). Each service has deployments with build logs.
|
||||
|
||||
TOOL: You have one tool "call_api". Pass operationId + params from the catalog.
|
||||
- ALWAYS pass required params (*) in the "params" object. Example: { "operationId": "domain-byComposeId", "params": { "composeId": "abc123" } }
|
||||
- Params: * = required, ? = optional, [a|b|c] = allowed values
|
||||
- GET = read-only (auto-executed). POST/PUT/DELETE = write (user approves).
|
||||
- If a call fails, read the error message and fix the params. NEVER retry the same call with the same params.
|
||||
|
||||
RESPONSE STYLE:
|
||||
- 2-3 sentences max. No walls of text.
|
||||
- Never explain limitations — find the right endpoint and act.
|
||||
- Answer in the user's language.
|
||||
|
||||
${catalog ? `ENDPOINT CATALOG (${endpointCount} endpoints):\n${catalog}` : ""}`;
|
||||
}
|
||||
|
||||
function getUserMessages(messages: any[]): string {
|
||||
const texts: string[] = [];
|
||||
for (const msg of messages) {
|
||||
if (msg.role !== "user") continue;
|
||||
if (typeof msg.content === "string") {
|
||||
texts.push(msg.content);
|
||||
} else if (Array.isArray(msg.content)) {
|
||||
texts.push(
|
||||
msg.content
|
||||
.filter((p: any) => p.type === "text")
|
||||
.map((p: any) => p.text)
|
||||
.join(" "),
|
||||
);
|
||||
}
|
||||
}
|
||||
return texts.slice(-3).join(". ");
|
||||
}
|
||||
|
||||
export default async function handler(
|
||||
req: NextApiRequest,
|
||||
res: NextApiResponse,
|
||||
) {
|
||||
if (req.method !== "POST") {
|
||||
return res.status(405).json({ error: "Method not allowed" });
|
||||
}
|
||||
|
||||
try {
|
||||
const { session, user } = await validateRequest(req);
|
||||
if (!user || !session) {
|
||||
return res.status(401).json({ error: "Unauthorized" });
|
||||
}
|
||||
|
||||
const body = req.body;
|
||||
const messages = body.messages;
|
||||
const aiId = body.aiId;
|
||||
const context = (body.context as ChatContext) || {
|
||||
type: "general" as const,
|
||||
id: "",
|
||||
};
|
||||
|
||||
// ─── Resolve model ────────────────────────────────────────
|
||||
let model: any;
|
||||
|
||||
if (IS_CLOUD && process.env.CLOUD_ANTHROPIC_API_KEY) {
|
||||
const anthropic = createAnthropic({
|
||||
apiKey: process.env.CLOUD_ANTHROPIC_API_KEY,
|
||||
});
|
||||
model = anthropic("claude-haiku-4-5-20251001");
|
||||
} else {
|
||||
if (!aiId || !messages) {
|
||||
return res
|
||||
.status(400)
|
||||
.json({ error: "Missing aiId or messages" });
|
||||
}
|
||||
const aiSettings = await getAiSettingById(aiId);
|
||||
if (!aiSettings || !aiSettings.isEnabled) {
|
||||
return res
|
||||
.status(400)
|
||||
.json({ error: "AI provider not enabled" });
|
||||
}
|
||||
const provider = selectAIProvider(aiSettings);
|
||||
model = provider(aiSettings.model);
|
||||
}
|
||||
|
||||
if (!messages) {
|
||||
return res.status(400).json({ error: "Missing messages" });
|
||||
}
|
||||
|
||||
// ─── Resolve tools ────────────────────────────────────────
|
||||
const protocol = req.headers["x-forwarded-proto"] || "http";
|
||||
const host = req.headers.host || "localhost:3000";
|
||||
const toolConfig = {
|
||||
baseUrl: `${protocol}://${host}/api`,
|
||||
cookie: req.headers.cookie || "",
|
||||
};
|
||||
|
||||
let tools: Record<string, any>;
|
||||
let catalogText: string | null = null;
|
||||
let endpointCount = 0;
|
||||
const spec = getOpenApiSpec();
|
||||
|
||||
if (spec) {
|
||||
const voyageApiKey = process.env.VOYAGE_API_KEY;
|
||||
if (!voyageApiKey) {
|
||||
return res.status(400).json({ error: "VOYAGE_API_KEY is required" });
|
||||
}
|
||||
|
||||
const embeddingsPath = join(process.cwd(), ".tool-embeddings.json");
|
||||
const allEmbeddings = await getOrCreateEmbeddings(
|
||||
spec,
|
||||
voyageApiKey,
|
||||
embeddingsPath,
|
||||
);
|
||||
|
||||
const userQuery = getUserMessages(messages).trim();
|
||||
const { operationIds: tagFilteredIds } = buildEndpointCatalog(spec, context.type);
|
||||
|
||||
let relevantIds: Set<string> | undefined;
|
||||
|
||||
if (userQuery && allEmbeddings.length > 0) {
|
||||
const topIds = await retrieveRelevantEndpoints(
|
||||
userQuery,
|
||||
allEmbeddings,
|
||||
voyageApiKey,
|
||||
{ allowedOperationIds: tagFilteredIds, topK: 25 },
|
||||
);
|
||||
|
||||
if (topIds.length > 0) {
|
||||
relevantIds = new Set(topIds);
|
||||
}
|
||||
}
|
||||
|
||||
const { catalog, count, operationIds } = buildEndpointCatalog(
|
||||
spec,
|
||||
context.type,
|
||||
relevantIds,
|
||||
);
|
||||
catalogText = catalog;
|
||||
endpointCount = count;
|
||||
tools = createApiTool(spec, toolConfig, operationIds, 8000);
|
||||
} else {
|
||||
tools = getAllTools(context, toolConfig);
|
||||
}
|
||||
|
||||
// ─── Stream response ──────────────────────────────────────
|
||||
const modelMessages = await convertToModelMessages(messages);
|
||||
|
||||
const result = streamText({
|
||||
model,
|
||||
system: buildSystemPrompt(context, catalogText, endpointCount),
|
||||
messages: modelMessages,
|
||||
tools,
|
||||
stopWhen: stepCountIs(12),
|
||||
});
|
||||
|
||||
// Disable buffering for streaming
|
||||
res.setHeader("X-Accel-Buffering", "no");
|
||||
res.setHeader("Cache-Control", "no-cache, no-transform");
|
||||
|
||||
result.pipeUIMessageStreamToResponse(res);
|
||||
} catch (error) {
|
||||
console.error("AI chat error:", error);
|
||||
return res.status(500).json({
|
||||
error:
|
||||
error instanceof Error ? error.message : "Internal server error",
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,12 @@ import { adminProcedure, createTRPCRouter } from "../trpc";
|
||||
|
||||
export const adminRouter = createTRPCRouter({
|
||||
setupMonitoring: adminProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Update web server monitoring settings",
|
||||
description: "Update the monitoring configuration for the web server including refresh rates, thresholds, and container services. Restarts the monitoring system and returns the updated settings. Disabled on cloud.",
|
||||
},
|
||||
})
|
||||
.input(apiUpdateWebServerMonitoring)
|
||||
.mutation(async ({ input }) => {
|
||||
try {
|
||||
|
||||
@@ -41,12 +41,24 @@ import { generatePassword } from "@/templates/utils";
|
||||
|
||||
export const aiRouter = createTRPCRouter({
|
||||
one: adminProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Get AI settings by ID",
|
||||
description: "Returns a single AI provider configuration by its ID.",
|
||||
},
|
||||
})
|
||||
.input(z.object({ aiId: z.string() }))
|
||||
.query(async ({ input }) => {
|
||||
return await getAiSettingById(input.aiId);
|
||||
}),
|
||||
|
||||
getModels: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "List available AI models",
|
||||
description: "Fetches the list of models from the given AI provider URL. Supports OpenAI-compatible, Ollama, Gemini, Perplexity, ZAI, and MiniMax providers.",
|
||||
},
|
||||
})
|
||||
.input(z.object({ apiUrl: z.string().min(1), apiKey: z.string() }))
|
||||
.query(async ({ input }) => {
|
||||
try {
|
||||
@@ -174,33 +186,75 @@ export const aiRouter = createTRPCRouter({
|
||||
});
|
||||
}
|
||||
}),
|
||||
create: adminProcedure.input(apiCreateAi).mutation(async ({ ctx, input }) => {
|
||||
create: adminProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Create AI provider",
|
||||
description: "Saves a new AI provider configuration (API URL, key, model) for the current organization.",
|
||||
},
|
||||
})
|
||||
.input(apiCreateAi)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
return await saveAiSettings(ctx.session.activeOrganizationId, input);
|
||||
}),
|
||||
|
||||
update: adminProcedure.input(apiUpdateAi).mutation(async ({ ctx, input }) => {
|
||||
update: adminProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Update AI provider",
|
||||
description: "Updates an existing AI provider configuration for the current organization.",
|
||||
},
|
||||
})
|
||||
.input(apiUpdateAi)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
return await saveAiSettings(ctx.session.activeOrganizationId, input);
|
||||
}),
|
||||
|
||||
getAll: adminProcedure.query(async ({ ctx }) => {
|
||||
getAll: adminProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "List all AI providers",
|
||||
description: "Returns all AI provider configurations for the current organization.",
|
||||
},
|
||||
})
|
||||
.query(async ({ ctx }) => {
|
||||
return await getAiSettingsByOrganizationId(
|
||||
ctx.session.activeOrganizationId,
|
||||
);
|
||||
}),
|
||||
|
||||
get: adminProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Get AI provider",
|
||||
description: "Returns a single AI provider configuration by its ID.",
|
||||
},
|
||||
})
|
||||
.input(z.object({ aiId: z.string() }))
|
||||
.query(async ({ input }) => {
|
||||
return await getAiSettingById(input.aiId);
|
||||
}),
|
||||
|
||||
delete: adminProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Delete AI provider",
|
||||
description: "Removes an AI provider configuration by its ID.",
|
||||
},
|
||||
})
|
||||
.input(z.object({ aiId: z.string() }))
|
||||
.mutation(async ({ input }) => {
|
||||
return await deleteAiSettings(input.aiId);
|
||||
}),
|
||||
|
||||
getEnabledProviders: protectedProcedure.query(async ({ ctx }) => {
|
||||
getEnabledProviders: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "List enabled AI providers",
|
||||
description: "Returns a lightweight list of enabled AI providers (ID, name, model) for the current organization, suitable for dropdown selectors.",
|
||||
},
|
||||
})
|
||||
.query(async ({ ctx }) => {
|
||||
const settings = await getAiSettingsByOrganizationId(
|
||||
ctx.session.activeOrganizationId,
|
||||
);
|
||||
@@ -210,6 +264,12 @@ export const aiRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
analyzeLogs: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Analyze logs with AI",
|
||||
description: "Sends build or runtime logs to the specified AI provider for analysis. Returns a summary of issues found, root causes, and suggested fixes.",
|
||||
},
|
||||
})
|
||||
.input(
|
||||
z.object({
|
||||
aiId: z.string().min(1),
|
||||
@@ -268,6 +328,12 @@ ${input.logs}`,
|
||||
}),
|
||||
|
||||
testConnection: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Test AI provider connection",
|
||||
description: "Sends a minimal prompt to the specified AI provider and model to verify the API URL, key, and model are valid and reachable.",
|
||||
},
|
||||
})
|
||||
.input(
|
||||
z.object({
|
||||
apiUrl: z.string().min(1),
|
||||
@@ -302,6 +368,12 @@ ${input.logs}`,
|
||||
}),
|
||||
|
||||
suggest: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Suggest deployment variants",
|
||||
description: "Uses AI to generate deployment configuration suggestions (docker-compose variants) based on the user's input prompt.",
|
||||
},
|
||||
})
|
||||
.input(
|
||||
z.object({
|
||||
aiId: z.string(),
|
||||
@@ -323,6 +395,12 @@ ${input.logs}`,
|
||||
}
|
||||
}),
|
||||
deploy: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Deploy AI suggestion",
|
||||
description: "Deploys an AI-generated suggestion by creating a compose service with its docker-compose file, environment variables, domains, and config file mounts.",
|
||||
},
|
||||
})
|
||||
.input(deploySuggestionSchema)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const environment = await findEnvironmentById(input.environmentId);
|
||||
|
||||
@@ -79,6 +79,12 @@ import { cancelDeployment, deploy } from "@/server/utils/deploy";
|
||||
|
||||
export const applicationRouter = createTRPCRouter({
|
||||
create: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Create an application",
|
||||
description: "Creates a new application in the specified project environment. Supports GitHub, GitLab, Bitbucket, Git, Docker image, and drop sources.",
|
||||
},
|
||||
})
|
||||
.input(apiCreateApplication)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
@@ -134,6 +140,12 @@ export const applicationRouter = createTRPCRouter({
|
||||
}
|
||||
}),
|
||||
one: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Get an application",
|
||||
description: "Retrieves detailed information about an application by its ID, including git provider access status and deployment configuration.",
|
||||
},
|
||||
})
|
||||
.input(apiFindOneApplication)
|
||||
.query(async ({ input, ctx }) => {
|
||||
await checkServiceAccess(ctx, input.applicationId, "read");
|
||||
@@ -189,6 +201,12 @@ export const applicationRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
reload: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Reload an application",
|
||||
description: "Restarts the Docker container for the application by mechanizing it. Resets the application status to idle, then to done on success or error on failure.",
|
||||
},
|
||||
})
|
||||
.input(apiReloadApplication)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.applicationId, {
|
||||
@@ -218,6 +236,12 @@ export const applicationRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
delete: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Delete an application",
|
||||
description: "Permanently deletes an application and cleans up all associated resources including Docker services, Traefik configuration, deployments, middlewares, and source code.",
|
||||
},
|
||||
})
|
||||
.input(apiFindOneApplication)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServiceAccess(ctx, input.applicationId, "delete");
|
||||
@@ -279,6 +303,12 @@ export const applicationRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
stop: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Stop an application",
|
||||
description: "Stops the running Docker service for the application and sets its status to idle.",
|
||||
},
|
||||
})
|
||||
.input(apiFindOneApplication)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.applicationId, {
|
||||
@@ -301,6 +331,12 @@ export const applicationRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
start: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Start an application",
|
||||
description: "Starts the Docker service for the application and sets its status to done.",
|
||||
},
|
||||
})
|
||||
.input(apiFindOneApplication)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.applicationId, {
|
||||
@@ -323,6 +359,12 @@ export const applicationRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
redeploy: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Redeploy an application",
|
||||
description: "Triggers a rebuild and redeployment of the application. Queues a deployment job or executes it directly for cloud servers.",
|
||||
},
|
||||
})
|
||||
.input(apiRedeployApplication)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.applicationId, {
|
||||
@@ -367,6 +409,12 @@ export const applicationRouter = createTRPCRouter({
|
||||
});
|
||||
}),
|
||||
saveEnvironment: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Save environment variables",
|
||||
description: "Updates the environment variables, build arguments, and build secrets for an application.",
|
||||
},
|
||||
})
|
||||
.input(apiSaveEnvironmentVariables)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.applicationId, {
|
||||
@@ -388,6 +436,12 @@ export const applicationRouter = createTRPCRouter({
|
||||
return true;
|
||||
}),
|
||||
saveBuildType: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Save build type configuration",
|
||||
description: "Updates the build type and related settings for an application, including Dockerfile path, build context, publish directory, and build stage.",
|
||||
},
|
||||
})
|
||||
.input(apiSaveBuildType)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.applicationId, {
|
||||
@@ -413,6 +467,12 @@ export const applicationRouter = createTRPCRouter({
|
||||
return true;
|
||||
}),
|
||||
saveGithubProvider: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Save GitHub provider",
|
||||
description: "Configures the application to use a GitHub repository as its source, setting the repository, branch, owner, and build path.",
|
||||
},
|
||||
})
|
||||
.input(apiSaveGithubProvider)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.applicationId, {
|
||||
@@ -440,6 +500,12 @@ export const applicationRouter = createTRPCRouter({
|
||||
return true;
|
||||
}),
|
||||
saveGitlabProvider: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Save GitLab provider",
|
||||
description: "Configures the application to use a GitLab repository as its source, setting the repository, branch, owner, build path, and project ID.",
|
||||
},
|
||||
})
|
||||
.input(apiSaveGitlabProvider)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.applicationId, {
|
||||
@@ -468,6 +534,12 @@ export const applicationRouter = createTRPCRouter({
|
||||
return true;
|
||||
}),
|
||||
saveBitbucketProvider: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Save Bitbucket provider",
|
||||
description: "Configures the application to use a Bitbucket repository as its source, setting the repository, branch, owner, and build path.",
|
||||
},
|
||||
})
|
||||
.input(apiSaveBitbucketProvider)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.applicationId, {
|
||||
@@ -495,6 +567,12 @@ export const applicationRouter = createTRPCRouter({
|
||||
return true;
|
||||
}),
|
||||
saveGiteaProvider: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Save Gitea provider",
|
||||
description: "Configures the application to use a Gitea repository as its source, setting the repository, branch, owner, and build path.",
|
||||
},
|
||||
})
|
||||
.input(apiSaveGiteaProvider)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.applicationId, {
|
||||
@@ -521,6 +599,12 @@ export const applicationRouter = createTRPCRouter({
|
||||
return true;
|
||||
}),
|
||||
saveDockerProvider: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Save Docker provider",
|
||||
description: "Configures the application to use a Docker image as its source, setting the image name, registry URL, and optional credentials.",
|
||||
},
|
||||
})
|
||||
.input(apiSaveDockerProvider)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.applicationId, {
|
||||
@@ -544,6 +628,12 @@ export const applicationRouter = createTRPCRouter({
|
||||
return true;
|
||||
}),
|
||||
saveGitProvider: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Save Git provider",
|
||||
description: "Configures the application to use a custom Git repository URL as its source, with optional SSH key authentication.",
|
||||
},
|
||||
})
|
||||
.input(apiSaveGitProvider)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.applicationId, {
|
||||
@@ -569,6 +659,12 @@ export const applicationRouter = createTRPCRouter({
|
||||
return true;
|
||||
}),
|
||||
disconnectGitProvider: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Disconnect git provider",
|
||||
description: "Removes all git provider configuration from the application, resetting source type to default and clearing repository, branch, and owner fields for all providers.",
|
||||
},
|
||||
})
|
||||
.input(apiFindOneApplication)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.applicationId, {
|
||||
@@ -622,6 +718,12 @@ export const applicationRouter = createTRPCRouter({
|
||||
return true;
|
||||
}),
|
||||
markRunning: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Mark application as running",
|
||||
description: "Sets the application status to running. Used to indicate that a deployment is in progress.",
|
||||
},
|
||||
})
|
||||
.input(apiFindOneApplication)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.applicationId, {
|
||||
@@ -637,6 +739,12 @@ export const applicationRouter = createTRPCRouter({
|
||||
});
|
||||
}),
|
||||
update: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Update an application",
|
||||
description: "Updates the general configuration of an application such as name, description, memory limits, CPU limits, and other settings.",
|
||||
},
|
||||
})
|
||||
.input(apiUpdateApplication)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.applicationId, {
|
||||
@@ -673,6 +781,12 @@ export const applicationRouter = createTRPCRouter({
|
||||
return true;
|
||||
}),
|
||||
refreshToken: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Refresh deploy token",
|
||||
description: "Regenerates the webhook refresh token for the application, invalidating the previous token used for triggering deployments.",
|
||||
},
|
||||
})
|
||||
.input(apiFindOneApplication)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.applicationId, {
|
||||
@@ -691,6 +805,12 @@ export const applicationRouter = createTRPCRouter({
|
||||
return true;
|
||||
}),
|
||||
deploy: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Deploy an application",
|
||||
description: "Triggers a new deployment for the application. Queues a deployment job or executes it directly for cloud servers.",
|
||||
},
|
||||
})
|
||||
.input(apiDeployApplication)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.applicationId, {
|
||||
@@ -735,6 +855,12 @@ export const applicationRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
cleanQueues: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Clean deployment queues",
|
||||
description: "Removes all pending deployment jobs from the queue for the specified application.",
|
||||
},
|
||||
})
|
||||
.input(apiFindOneApplication)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.applicationId, {
|
||||
@@ -743,6 +869,12 @@ export const applicationRouter = createTRPCRouter({
|
||||
await cleanQueuesByApplication(input.applicationId);
|
||||
}),
|
||||
clearDeployments: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Clear old deployments",
|
||||
description: "Removes old deployment logs and artifacts for the application to free up disk space.",
|
||||
},
|
||||
})
|
||||
.input(apiFindOneApplication)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.applicationId, {
|
||||
@@ -759,6 +891,12 @@ export const applicationRouter = createTRPCRouter({
|
||||
return true;
|
||||
}),
|
||||
killBuild: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Kill active build",
|
||||
description: "Forcefully terminates the currently running Docker build process for the application.",
|
||||
},
|
||||
})
|
||||
.input(apiFindOneApplication)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.applicationId, {
|
||||
@@ -774,6 +912,12 @@ export const applicationRouter = createTRPCRouter({
|
||||
});
|
||||
}),
|
||||
readTraefikConfig: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Read Traefik configuration",
|
||||
description: "Reads the current Traefik reverse proxy configuration file for the application. Supports both local and remote server configurations.",
|
||||
},
|
||||
})
|
||||
.input(apiFindOneApplication)
|
||||
.query(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.applicationId, {
|
||||
@@ -793,6 +937,12 @@ export const applicationRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
dropDeployment: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Deploy from zip upload",
|
||||
description: "Deploys an application from an uploaded zip file. Unzips the file into the application directory and triggers a deployment.",
|
||||
},
|
||||
})
|
||||
.input(
|
||||
zfd.formData({
|
||||
applicationId: z.string(),
|
||||
@@ -849,6 +999,12 @@ export const applicationRouter = createTRPCRouter({
|
||||
return true;
|
||||
}),
|
||||
updateTraefikConfig: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Update Traefik configuration",
|
||||
description: "Writes a new Traefik reverse proxy configuration for the application. Supports both local and remote server configurations.",
|
||||
},
|
||||
})
|
||||
.input(z.object({ applicationId: z.string(), traefikConfig: z.string() }))
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.applicationId, {
|
||||
@@ -873,6 +1029,12 @@ export const applicationRouter = createTRPCRouter({
|
||||
return true;
|
||||
}),
|
||||
readAppMonitoring: withPermission("monitoring", "read")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Read application monitoring stats",
|
||||
description: "Retrieves CPU and memory monitoring statistics for the application. Only available in self-hosted mode.",
|
||||
},
|
||||
})
|
||||
.input(apiFindMonitoringStats)
|
||||
.query(async ({ input }) => {
|
||||
if (IS_CLOUD) {
|
||||
@@ -886,6 +1048,12 @@ export const applicationRouter = createTRPCRouter({
|
||||
return stats;
|
||||
}),
|
||||
move: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Move application to another environment",
|
||||
description: "Moves an application to a different environment within the same project or to another project's environment.",
|
||||
},
|
||||
})
|
||||
.input(
|
||||
z.object({
|
||||
applicationId: z.string(),
|
||||
@@ -922,6 +1090,12 @@ export const applicationRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
cancelDeployment: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Cancel a deployment",
|
||||
description: "Cancels an in-progress deployment for the application and resets its status to idle. Only available in cloud version.",
|
||||
},
|
||||
})
|
||||
.input(apiFindOneApplication)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.applicationId, {
|
||||
@@ -972,6 +1146,12 @@ export const applicationRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
search: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Search applications",
|
||||
description: "Searches applications by name, appName, description, repository, owner, or Docker image with pagination. Respects service-level access control.",
|
||||
},
|
||||
})
|
||||
.input(
|
||||
z.object({
|
||||
q: z.string().optional(),
|
||||
@@ -1104,6 +1284,12 @@ export const applicationRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
readLogs: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Read application logs",
|
||||
description: "Retrieves Docker container logs for the application with configurable tail length, time range, and optional text search filtering.",
|
||||
},
|
||||
})
|
||||
.input(
|
||||
apiFindOneApplication.extend({
|
||||
tail: z.number().int().min(1).max(10000).default(100),
|
||||
|
||||
@@ -78,6 +78,12 @@ interface RcloneFile {
|
||||
|
||||
export const backupRouter = createTRPCRouter({
|
||||
create: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Create a backup",
|
||||
description: "Creates a new backup configuration for a database or compose service. If enabled, automatically schedules the backup according to the provided cron expression.",
|
||||
},
|
||||
})
|
||||
.input(apiCreateBackup)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
@@ -152,6 +158,12 @@ export const backupRouter = createTRPCRouter({
|
||||
}
|
||||
}),
|
||||
one: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Get a backup",
|
||||
description: "Returns the details of a specific backup configuration by its ID.",
|
||||
},
|
||||
})
|
||||
.input(apiFindOneBackup)
|
||||
.query(async ({ input, ctx }) => {
|
||||
const backup = await findBackupById(input.backupId);
|
||||
@@ -172,6 +184,12 @@ export const backupRouter = createTRPCRouter({
|
||||
return backup;
|
||||
}),
|
||||
update: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Update a backup",
|
||||
description: "Updates an existing backup configuration. Reschedules or removes the backup job depending on the enabled state.",
|
||||
},
|
||||
})
|
||||
.input(apiUpdateBackup)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
@@ -229,6 +247,12 @@ export const backupRouter = createTRPCRouter({
|
||||
}
|
||||
}),
|
||||
remove: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Delete a backup",
|
||||
description: "Permanently removes a backup configuration and unschedules any associated backup job.",
|
||||
},
|
||||
})
|
||||
.input(apiRemoveBackup)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
@@ -272,6 +296,12 @@ export const backupRouter = createTRPCRouter({
|
||||
}
|
||||
}),
|
||||
manualBackupPostgres: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Run a PostgreSQL backup manually",
|
||||
description: "Immediately executes a PostgreSQL backup using the specified backup configuration. Cleans up old backups according to retention settings.",
|
||||
},
|
||||
})
|
||||
.input(apiFindOneBackup)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
@@ -303,6 +333,12 @@ export const backupRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
manualBackupMySql: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Run a MySQL backup manually",
|
||||
description: "Immediately executes a MySQL backup using the specified backup configuration. Cleans up old backups according to retention settings.",
|
||||
},
|
||||
})
|
||||
.input(apiFindOneBackup)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
@@ -330,6 +366,12 @@ export const backupRouter = createTRPCRouter({
|
||||
}
|
||||
}),
|
||||
manualBackupMariadb: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Run a MariaDB backup manually",
|
||||
description: "Immediately executes a MariaDB backup using the specified backup configuration. Cleans up old backups according to retention settings.",
|
||||
},
|
||||
})
|
||||
.input(apiFindOneBackup)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
@@ -357,6 +399,12 @@ export const backupRouter = createTRPCRouter({
|
||||
}
|
||||
}),
|
||||
manualBackupCompose: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Run a Compose backup manually",
|
||||
description: "Immediately executes a Compose service backup using the specified backup configuration. Cleans up old backups according to retention settings.",
|
||||
},
|
||||
})
|
||||
.input(apiFindOneBackup)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
@@ -384,6 +432,12 @@ export const backupRouter = createTRPCRouter({
|
||||
}
|
||||
}),
|
||||
manualBackupMongo: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Run a MongoDB backup manually",
|
||||
description: "Immediately executes a MongoDB backup using the specified backup configuration. Cleans up old backups according to retention settings.",
|
||||
},
|
||||
})
|
||||
.input(apiFindOneBackup)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
@@ -411,6 +465,12 @@ export const backupRouter = createTRPCRouter({
|
||||
}
|
||||
}),
|
||||
manualBackupLibsql: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Run a LibSQL backup manually",
|
||||
description: "Immediately executes a LibSQL backup using the specified backup configuration. Cleans up old backups according to retention settings.",
|
||||
},
|
||||
})
|
||||
.input(apiFindOneBackup)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
@@ -438,6 +498,12 @@ export const backupRouter = createTRPCRouter({
|
||||
}
|
||||
}),
|
||||
manualBackupWebServer: withPermission("backup", "create")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Run a web server backup manually",
|
||||
description: "Immediately executes a web server backup using the specified backup configuration. Cleans up old backups according to retention settings.",
|
||||
},
|
||||
})
|
||||
.input(apiFindOneBackup)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const backup = await findBackupById(input.backupId);
|
||||
@@ -451,6 +517,12 @@ export const backupRouter = createTRPCRouter({
|
||||
return true;
|
||||
}),
|
||||
listBackupFiles: withPermission("backup", "read")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "List backup files in S3",
|
||||
description: "Lists backup files stored in the S3 destination bucket. Supports searching by path prefix and returns up to 100 results.",
|
||||
},
|
||||
})
|
||||
.input(
|
||||
z.object({
|
||||
destinationId: z.string(),
|
||||
|
||||
@@ -25,6 +25,12 @@ import {
|
||||
|
||||
export const bitbucketRouter = createTRPCRouter({
|
||||
create: withPermission("gitProviders", "create")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Create Bitbucket provider",
|
||||
description: "Creates a new Bitbucket provider configuration linked to the active organization. Requires gitProviders create permission.",
|
||||
},
|
||||
})
|
||||
.input(apiCreateBitbucket)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
@@ -50,11 +56,24 @@ export const bitbucketRouter = createTRPCRouter({
|
||||
}
|
||||
}),
|
||||
one: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Get Bitbucket provider",
|
||||
description: "Returns a single Bitbucket provider configuration by its ID.",
|
||||
},
|
||||
})
|
||||
.input(apiFindOneBitbucket)
|
||||
.query(async ({ input }) => {
|
||||
return await findBitbucketById(input.bitbucketId);
|
||||
}),
|
||||
bitbucketProviders: protectedProcedure.query(async ({ ctx }) => {
|
||||
bitbucketProviders: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "List Bitbucket providers",
|
||||
description: "Returns all Bitbucket providers accessible to the current user within the active organization.",
|
||||
},
|
||||
})
|
||||
.query(async ({ ctx }) => {
|
||||
const accessibleIds = await getAccessibleGitProviderIds(ctx.session);
|
||||
|
||||
let result = await db.query.bitbucket.findMany({
|
||||
@@ -77,16 +96,34 @@ export const bitbucketRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
getBitbucketRepositories: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "List Bitbucket repositories",
|
||||
description: "Fetches the list of repositories accessible by the Bitbucket provider. Calls the Bitbucket API using the provider's credentials.",
|
||||
},
|
||||
})
|
||||
.input(apiFindOneBitbucket)
|
||||
.query(async ({ input }) => {
|
||||
return await getBitbucketRepositories(input.bitbucketId);
|
||||
}),
|
||||
getBitbucketBranches: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "List Bitbucket branches",
|
||||
description: "Fetches the list of branches for a specific Bitbucket repository. Calls the Bitbucket API using the provider's credentials.",
|
||||
},
|
||||
})
|
||||
.input(apiFindBitbucketBranches)
|
||||
.query(async ({ input }) => {
|
||||
return await getBitbucketBranches(input);
|
||||
}),
|
||||
testConnection: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Test Bitbucket connection",
|
||||
description: "Tests the connection to a Bitbucket provider by attempting to fetch its repositories. Returns the number of repositories found or throws an error on failure.",
|
||||
},
|
||||
})
|
||||
.input(apiBitbucketTestConnection)
|
||||
.mutation(async ({ input }) => {
|
||||
try {
|
||||
@@ -101,6 +138,12 @@ export const bitbucketRouter = createTRPCRouter({
|
||||
}
|
||||
}),
|
||||
update: withPermission("gitProviders", "create")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Update Bitbucket provider",
|
||||
description: "Updates a Bitbucket provider configuration. Requires gitProviders create permission.",
|
||||
},
|
||||
})
|
||||
.input(apiUpdateBitbucket)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const result = await updateBitbucket(input.bitbucketId, {
|
||||
|
||||
@@ -19,6 +19,12 @@ import {
|
||||
|
||||
export const certificateRouter = createTRPCRouter({
|
||||
create: withPermission("certificate", "create")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Create a certificate",
|
||||
description: "Creates a new SSL/TLS certificate. In cloud mode, a server must be specified. Logs an audit entry upon creation.",
|
||||
},
|
||||
})
|
||||
.input(apiCreateCertificate)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
if (IS_CLOUD && !input.serverId) {
|
||||
@@ -41,6 +47,12 @@ export const certificateRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
one: withPermission("certificate", "read")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Get a certificate",
|
||||
description: "Returns a single certificate by its ID. Verifies that the certificate belongs to the current organization.",
|
||||
},
|
||||
})
|
||||
.input(apiFindCertificate)
|
||||
.query(async ({ input, ctx }) => {
|
||||
const certificates = await findCertificateById(input.certificateId);
|
||||
@@ -53,6 +65,12 @@ export const certificateRouter = createTRPCRouter({
|
||||
return certificates;
|
||||
}),
|
||||
remove: withPermission("certificate", "delete")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Delete a certificate",
|
||||
description: "Deletes a certificate by its ID after verifying organization ownership. Logs an audit entry before removal.",
|
||||
},
|
||||
})
|
||||
.input(apiFindCertificate)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const certificates = await findCertificateById(input.certificateId);
|
||||
@@ -71,7 +89,14 @@ export const certificateRouter = createTRPCRouter({
|
||||
await removeCertificateById(input.certificateId);
|
||||
return true;
|
||||
}),
|
||||
all: withPermission("certificate", "read").query(async ({ ctx }) => {
|
||||
all: withPermission("certificate", "read")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "List all certificates",
|
||||
description: "Returns all certificates belonging to the current organization, including their associated server information.",
|
||||
},
|
||||
})
|
||||
.query(async ({ ctx }) => {
|
||||
return await db.query.certificates.findMany({
|
||||
where: eq(certificates.organizationId, ctx.session.activeOrganizationId),
|
||||
with: {
|
||||
@@ -80,6 +105,12 @@ export const certificateRouter = createTRPCRouter({
|
||||
});
|
||||
}),
|
||||
update: withPermission("certificate", "update")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Update a certificate",
|
||||
description: "Updates the name, certificate data, and private key of an existing certificate. Verifies organization ownership before applying changes.",
|
||||
},
|
||||
})
|
||||
.input(apiUpdateCertificate)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const certificate = await findCertificateById(input.certificateId);
|
||||
|
||||
@@ -13,6 +13,12 @@ import { createTRPCRouter, withPermission } from "../trpc";
|
||||
|
||||
export const clusterRouter = createTRPCRouter({
|
||||
getNodes: withPermission("server", "read")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Get cluster nodes",
|
||||
description: "Retrieves all nodes in the Docker Swarm cluster. Optionally targets a remote server.",
|
||||
},
|
||||
})
|
||||
.input(
|
||||
z.object({
|
||||
serverId: z.string().optional(),
|
||||
@@ -25,6 +31,12 @@ export const clusterRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
removeWorker: withPermission("server", "delete")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Remove a worker node",
|
||||
description: "Drains and forcefully removes a worker node from the Docker Swarm cluster. An audit log entry is created for the removal.",
|
||||
},
|
||||
})
|
||||
.input(
|
||||
z.object({
|
||||
nodeId: z.string(),
|
||||
@@ -60,6 +72,12 @@ export const clusterRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
addWorker: withPermission("server", "create")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Get worker join command",
|
||||
description: "Returns the Docker Swarm join command and token for adding a new worker node to the cluster, along with the Docker version.",
|
||||
},
|
||||
})
|
||||
.input(
|
||||
z.object({
|
||||
serverId: z.string().optional(),
|
||||
@@ -83,6 +101,12 @@ export const clusterRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
addManager: withPermission("server", "create")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Get manager join command",
|
||||
description: "Returns the Docker Swarm join command and token for adding a new manager node to the cluster, along with the Docker version.",
|
||||
},
|
||||
})
|
||||
.input(
|
||||
z.object({
|
||||
serverId: z.string().optional(),
|
||||
|
||||
@@ -83,6 +83,12 @@ import { audit } from "../utils/audit";
|
||||
|
||||
export const composeRouter = createTRPCRouter({
|
||||
create: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Create a compose service",
|
||||
description: "Creates a new Docker Compose service in the specified project environment with the given configuration.",
|
||||
},
|
||||
})
|
||||
.input(apiCreateCompose)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
try {
|
||||
@@ -133,6 +139,12 @@ export const composeRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
one: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Get a compose service",
|
||||
description: "Retrieves detailed information about a compose service by its ID, including git provider access status and deployment configuration.",
|
||||
},
|
||||
})
|
||||
.input(apiFindCompose)
|
||||
.query(async ({ input, ctx }) => {
|
||||
await checkServiceAccess(ctx, input.composeId, "read");
|
||||
@@ -189,6 +201,12 @@ export const composeRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
update: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Update a compose service",
|
||||
description: "Updates the configuration of a compose service such as name, description, compose file content, and other settings.",
|
||||
},
|
||||
})
|
||||
.input(apiUpdateCompose)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.composeId, {
|
||||
@@ -204,6 +222,12 @@ export const composeRouter = createTRPCRouter({
|
||||
return updated;
|
||||
}),
|
||||
saveEnvironment: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Save compose environment variables",
|
||||
description: "Updates the environment variables for a compose service.",
|
||||
},
|
||||
})
|
||||
.input(apiSaveEnvironmentVariablesCompose)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.composeId, {
|
||||
@@ -229,6 +253,12 @@ export const composeRouter = createTRPCRouter({
|
||||
return true;
|
||||
}),
|
||||
delete: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Delete a compose service",
|
||||
description: "Permanently deletes a compose service and cleans up associated Docker resources, deployments, and directories. Optionally deletes associated volumes.",
|
||||
},
|
||||
})
|
||||
.input(apiDeleteCompose)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServiceAccess(ctx, input.composeId, "delete");
|
||||
@@ -279,6 +309,12 @@ export const composeRouter = createTRPCRouter({
|
||||
return composeResult;
|
||||
}),
|
||||
cleanQueues: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Clean deployment queues",
|
||||
description: "Removes all pending deployment jobs from the queue for the specified compose service.",
|
||||
},
|
||||
})
|
||||
.input(apiFindCompose)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.composeId, {
|
||||
@@ -288,6 +324,12 @@ export const composeRouter = createTRPCRouter({
|
||||
return { success: true, message: "Queues cleaned successfully" };
|
||||
}),
|
||||
clearDeployments: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Clear old deployments",
|
||||
description: "Removes old deployment logs and artifacts for the compose service to free up disk space.",
|
||||
},
|
||||
})
|
||||
.input(apiFindCompose)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.composeId, {
|
||||
@@ -304,6 +346,12 @@ export const composeRouter = createTRPCRouter({
|
||||
return true;
|
||||
}),
|
||||
killBuild: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Kill active build",
|
||||
description: "Forcefully terminates the currently running Docker build process for the compose service.",
|
||||
},
|
||||
})
|
||||
.input(apiFindCompose)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.composeId, {
|
||||
@@ -314,6 +362,12 @@ export const composeRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
loadServices: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Load compose services",
|
||||
description: "Parses the compose file and returns the list of services defined in it, with their current container status.",
|
||||
},
|
||||
})
|
||||
.input(apiFetchServices)
|
||||
.query(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.composeId, {
|
||||
@@ -322,6 +376,12 @@ export const composeRouter = createTRPCRouter({
|
||||
return await loadServices(input.composeId, input.type);
|
||||
}),
|
||||
loadMountsByService: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Load mounts by service",
|
||||
description: "Retrieves the Docker volume mounts for a specific service within a compose stack by inspecting the running container.",
|
||||
},
|
||||
})
|
||||
.input(
|
||||
z.object({
|
||||
composeId: z.string().min(1),
|
||||
@@ -340,6 +400,12 @@ export const composeRouter = createTRPCRouter({
|
||||
return mounts;
|
||||
}),
|
||||
fetchSourceType: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Fetch and clone source",
|
||||
description: "Clones the compose repository from the configured git provider and returns the source type. Executes the clone command locally or on a remote server.",
|
||||
},
|
||||
})
|
||||
.input(apiFindCompose)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
@@ -365,6 +431,12 @@ export const composeRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
randomizeCompose: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Randomize compose file",
|
||||
description: "Adds a random suffix to service names and volumes in the compose file to avoid naming conflicts between deployments.",
|
||||
},
|
||||
})
|
||||
.input(apiRandomizeCompose)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.composeId, {
|
||||
@@ -381,6 +453,12 @@ export const composeRouter = createTRPCRouter({
|
||||
return result;
|
||||
}),
|
||||
isolatedDeployment: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Randomize for isolated deployment",
|
||||
description: "Randomizes the compose file for isolated deployment mode, ensuring unique service and volume names to support parallel deployments.",
|
||||
},
|
||||
})
|
||||
.input(apiRandomizeCompose)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.composeId, {
|
||||
@@ -400,6 +478,12 @@ export const composeRouter = createTRPCRouter({
|
||||
return result;
|
||||
}),
|
||||
getConvertedCompose: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Get converted compose file",
|
||||
description: "Returns the compose file with domains injected as Traefik labels, converted to YAML format ready for deployment.",
|
||||
},
|
||||
})
|
||||
.input(apiFindCompose)
|
||||
.query(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.composeId, {
|
||||
@@ -414,6 +498,12 @@ export const composeRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
deploy: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Deploy a compose service",
|
||||
description: "Triggers a new deployment for the compose service. Queues a deployment job or executes it directly for cloud servers.",
|
||||
},
|
||||
})
|
||||
.input(apiDeployCompose)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.composeId, {
|
||||
@@ -464,6 +554,12 @@ export const composeRouter = createTRPCRouter({
|
||||
};
|
||||
}),
|
||||
redeploy: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Redeploy a compose service",
|
||||
description: "Triggers a rebuild and redeployment of the compose service. Queues a deployment job or executes it directly for cloud servers.",
|
||||
},
|
||||
})
|
||||
.input(apiRedeployCompose)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.composeId, {
|
||||
@@ -512,6 +608,12 @@ export const composeRouter = createTRPCRouter({
|
||||
};
|
||||
}),
|
||||
stop: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Stop a compose service",
|
||||
description: "Stops all running containers for the compose service using docker compose stop.",
|
||||
},
|
||||
})
|
||||
.input(apiFindCompose)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.composeId, {
|
||||
@@ -528,6 +630,12 @@ export const composeRouter = createTRPCRouter({
|
||||
return true;
|
||||
}),
|
||||
start: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Start a compose service",
|
||||
description: "Starts all containers for the compose service using docker compose start.",
|
||||
},
|
||||
})
|
||||
.input(apiFindCompose)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.composeId, {
|
||||
@@ -544,6 +652,12 @@ export const composeRouter = createTRPCRouter({
|
||||
return true;
|
||||
}),
|
||||
getDefaultCommand: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Get default compose command",
|
||||
description: "Generates and returns the default docker compose command that would be used to deploy the service.",
|
||||
},
|
||||
})
|
||||
.input(apiFindCompose)
|
||||
.query(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.composeId, {
|
||||
@@ -554,6 +668,12 @@ export const composeRouter = createTRPCRouter({
|
||||
return `docker ${command}`;
|
||||
}),
|
||||
refreshToken: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Refresh deploy token",
|
||||
description: "Regenerates the webhook refresh token for the compose service, invalidating the previous token used for triggering deployments.",
|
||||
},
|
||||
})
|
||||
.input(apiFindCompose)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.composeId, {
|
||||
@@ -572,6 +692,12 @@ export const composeRouter = createTRPCRouter({
|
||||
return true;
|
||||
}),
|
||||
deployTemplate: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Deploy a template",
|
||||
description: "Creates a new compose service from a template by fetching its files, processing variables, creating mounts and domains, and setting up the compose configuration.",
|
||||
},
|
||||
})
|
||||
.input(
|
||||
z.object({
|
||||
environmentId: z.string(),
|
||||
@@ -680,6 +806,12 @@ export const composeRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
templates: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "List available templates",
|
||||
description: "Fetches the list of available compose templates from the GitHub templates repository.",
|
||||
},
|
||||
})
|
||||
.input(z.object({ baseUrl: z.string().optional() }))
|
||||
.query(async ({ input }) => {
|
||||
try {
|
||||
@@ -698,6 +830,12 @@ export const composeRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
getTags: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Get template tags",
|
||||
description: "Fetches all unique tags from the available compose templates for filtering purposes.",
|
||||
},
|
||||
})
|
||||
.input(z.object({ baseUrl: z.string().optional() }))
|
||||
.query(async ({ input }) => {
|
||||
const githubTemplates = await fetchTemplatesList(input.baseUrl);
|
||||
@@ -707,6 +845,12 @@ export const composeRouter = createTRPCRouter({
|
||||
return uniqueTags;
|
||||
}),
|
||||
disconnectGitProvider: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Disconnect git provider",
|
||||
description: "Removes all git provider configuration from the compose service, resetting source type to default and clearing repository, branch, and owner fields for all providers.",
|
||||
},
|
||||
})
|
||||
.input(apiFindCompose)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.composeId, {
|
||||
@@ -759,6 +903,12 @@ export const composeRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
move: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Move compose to another environment",
|
||||
description: "Moves a compose service to a different environment within the same project or to another project's environment.",
|
||||
},
|
||||
})
|
||||
.input(
|
||||
z.object({
|
||||
composeId: z.string(),
|
||||
@@ -796,6 +946,12 @@ export const composeRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
processTemplate: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Process a template",
|
||||
description: "Processes a base64-encoded template configuration, resolving variables and generating the compose file and environment settings without applying them.",
|
||||
},
|
||||
})
|
||||
.input(
|
||||
z.object({
|
||||
base64: z.string(),
|
||||
@@ -860,6 +1016,12 @@ export const composeRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
import: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Import a template",
|
||||
description: "Imports a base64-encoded template into an existing compose service, replacing its compose file, environment variables, mounts, and domains with the template's configuration.",
|
||||
},
|
||||
})
|
||||
.input(
|
||||
z.object({
|
||||
base64: z.string(),
|
||||
@@ -972,6 +1134,12 @@ export const composeRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
cancelDeployment: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Cancel a deployment",
|
||||
description: "Cancels an in-progress deployment for the compose service and resets its status to idle. Only available in cloud version.",
|
||||
},
|
||||
})
|
||||
.input(apiFindCompose)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.composeId, {
|
||||
@@ -1025,6 +1193,12 @@ export const composeRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
search: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Search compose services",
|
||||
description: "Searches compose services by name, appName, or description with pagination. Respects service-level access control.",
|
||||
},
|
||||
})
|
||||
.input(
|
||||
z.object({
|
||||
q: z.string().optional(),
|
||||
@@ -1133,6 +1307,12 @@ export const composeRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
readLogs: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Read compose container logs",
|
||||
description: "Retrieves Docker container logs for a specific container within the compose service with configurable tail length, time range, and optional text search filtering.",
|
||||
},
|
||||
})
|
||||
.input(
|
||||
apiFindCompose.extend({
|
||||
containerId: z
|
||||
|
||||
@@ -34,6 +34,12 @@ import { createTRPCRouter, protectedProcedure, withPermission } from "../trpc";
|
||||
|
||||
export const deploymentRouter = createTRPCRouter({
|
||||
all: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "List deployments by application",
|
||||
description: "Returns all deployments associated with the given application, ordered by creation date.",
|
||||
},
|
||||
})
|
||||
.input(apiFindAllByApplication)
|
||||
.query(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.applicationId, {
|
||||
@@ -43,6 +49,12 @@ export const deploymentRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
allByCompose: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "List deployments by compose",
|
||||
description: "Returns all deployments associated with the given compose service.",
|
||||
},
|
||||
})
|
||||
.input(apiFindAllByCompose)
|
||||
.query(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.composeId, {
|
||||
@@ -51,11 +63,24 @@ export const deploymentRouter = createTRPCRouter({
|
||||
return await findAllDeploymentsByComposeId(input.composeId);
|
||||
}),
|
||||
allByServer: withPermission("deployment", "read")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "List deployments by server",
|
||||
description: "Returns all deployments associated with the given server.",
|
||||
},
|
||||
})
|
||||
.input(apiFindAllByServer)
|
||||
.query(async ({ input }) => {
|
||||
return await findAllDeploymentsByServerId(input.serverId);
|
||||
}),
|
||||
allCentralized: withPermission("deployment", "read").query(
|
||||
allCentralized: withPermission("deployment", "read")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "List all deployments centralized",
|
||||
description: "Returns all deployments across all services in the organization. Non-admin users only see deployments for their accessible services.",
|
||||
},
|
||||
})
|
||||
.query(
|
||||
async ({ ctx }) => {
|
||||
const orgId = ctx.session.activeOrganizationId;
|
||||
const accessedServices =
|
||||
@@ -69,7 +94,14 @@ export const deploymentRouter = createTRPCRouter({
|
||||
},
|
||||
),
|
||||
|
||||
queueList: withPermission("deployment", "read").query(async ({ ctx }) => {
|
||||
queueList: withPermission("deployment", "read")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "List deployment queue jobs",
|
||||
description: "Returns all jobs in the deployment queue with their current state, timestamps, and resolved service paths.",
|
||||
},
|
||||
})
|
||||
.query(async ({ ctx }) => {
|
||||
const orgId = ctx.session.activeOrganizationId;
|
||||
let rows: QueueJobRow[];
|
||||
|
||||
@@ -116,6 +148,12 @@ export const deploymentRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
allByType: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "List deployments by service type",
|
||||
description: "Returns all deployments for a given service ID and type (application, compose, etc.), including associated rollback information.",
|
||||
},
|
||||
})
|
||||
.input(apiFindAllByType)
|
||||
.query(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.id, {
|
||||
@@ -131,6 +169,12 @@ export const deploymentRouter = createTRPCRouter({
|
||||
return deploymentsList;
|
||||
}),
|
||||
killProcess: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Cancel a running deployment",
|
||||
description: "Kills the running process of a deployment by sending SIGKILL to its PID. Updates the deployment status to error.",
|
||||
},
|
||||
})
|
||||
.input(
|
||||
z.object({
|
||||
deploymentId: z.string().min(1),
|
||||
@@ -168,6 +212,12 @@ export const deploymentRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
removeDeployment: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Delete a deployment",
|
||||
description: "Permanently removes a deployment record and its associated data.",
|
||||
},
|
||||
})
|
||||
.input(
|
||||
z.object({
|
||||
deploymentId: z.string().min(1),
|
||||
@@ -189,4 +239,43 @@ export const deploymentRouter = createTRPCRouter({
|
||||
});
|
||||
return result;
|
||||
}),
|
||||
|
||||
readBuildLogs: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Read deployment build logs",
|
||||
description:
|
||||
"Reads the build/deployment log file for a specific deployment. Returns the last N lines (default 200). Works for both local and remote server deployments.",
|
||||
},
|
||||
})
|
||||
.input(
|
||||
z.object({
|
||||
deploymentId: z.string().min(1),
|
||||
tail: z.number().int().min(1).max(10000).default(200),
|
||||
}),
|
||||
)
|
||||
.query(async ({ input, ctx }) => {
|
||||
const deployment = await findDeploymentById(input.deploymentId);
|
||||
|
||||
const serviceId = deployment.applicationId || deployment.composeId;
|
||||
if (serviceId) {
|
||||
await checkServicePermissionAndAccess(ctx, serviceId, {
|
||||
deployment: ["read"],
|
||||
});
|
||||
}
|
||||
|
||||
const command = `tail -n ${input.tail} ${deployment.logPath} 2>/dev/null || echo "Log file not found"`;
|
||||
const { stdout } = deployment.serverId
|
||||
? await execAsyncRemote(deployment.serverId, command)
|
||||
: await execAsync(command);
|
||||
|
||||
return {
|
||||
deploymentId: deployment.deploymentId,
|
||||
status: deployment.status,
|
||||
errorMessage: deployment.errorMessage || null,
|
||||
title: deployment.title,
|
||||
createdAt: deployment.createdAt,
|
||||
logs: stdout,
|
||||
};
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -22,6 +22,12 @@ import {
|
||||
|
||||
export const destinationRouter = createTRPCRouter({
|
||||
create: withPermission("destination", "create")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Create backup destination",
|
||||
description: "Creates a new S3-compatible backup destination for the current organization and logs an audit event.",
|
||||
},
|
||||
})
|
||||
.input(apiCreateDestination)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
@@ -45,6 +51,12 @@ export const destinationRouter = createTRPCRouter({
|
||||
}
|
||||
}),
|
||||
testConnection: withPermission("destination", "create")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Test backup destination connection",
|
||||
description: "Tests connectivity to an S3-compatible bucket using rclone. Runs locally or on a remote server depending on configuration.",
|
||||
},
|
||||
})
|
||||
.input(apiCreateDestination)
|
||||
.mutation(async ({ input }) => {
|
||||
const {
|
||||
@@ -102,6 +114,12 @@ export const destinationRouter = createTRPCRouter({
|
||||
}
|
||||
}),
|
||||
one: withPermission("destination", "read")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Get backup destination",
|
||||
description: "Returns a single backup destination by ID. Verifies the caller belongs to the same organization.",
|
||||
},
|
||||
})
|
||||
.input(apiFindOneDestination)
|
||||
.query(async ({ input, ctx }) => {
|
||||
const destination = await findDestinationById(input.destinationId);
|
||||
@@ -113,13 +131,26 @@ export const destinationRouter = createTRPCRouter({
|
||||
}
|
||||
return destination;
|
||||
}),
|
||||
all: withPermission("destination", "read").query(async ({ ctx }) => {
|
||||
all: withPermission("destination", "read")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "List all backup destinations",
|
||||
description: "Returns all S3-compatible backup destinations for the current organization, ordered by creation date descending.",
|
||||
},
|
||||
})
|
||||
.query(async ({ ctx }) => {
|
||||
return await db.query.destinations.findMany({
|
||||
where: eq(destinations.organizationId, ctx.session.activeOrganizationId),
|
||||
orderBy: [desc(destinations.createdAt)],
|
||||
});
|
||||
}),
|
||||
remove: withPermission("destination", "delete")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Delete backup destination",
|
||||
description: "Removes a backup destination by ID. Verifies organization ownership and logs an audit event before deletion.",
|
||||
},
|
||||
})
|
||||
.input(apiRemoveDestination)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
@@ -147,6 +178,12 @@ export const destinationRouter = createTRPCRouter({
|
||||
}
|
||||
}),
|
||||
update: withPermission("destination", "create")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Update backup destination",
|
||||
description: "Updates an existing backup destination. Verifies organization ownership before applying changes and logs an audit event.",
|
||||
},
|
||||
})
|
||||
.input(apiUpdateDestination)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
|
||||
@@ -20,6 +20,12 @@ export const containerIdRegex = /^[a-zA-Z0-9.\-_]+$/;
|
||||
|
||||
export const dockerRouter = createTRPCRouter({
|
||||
getContainers: withPermission("docker", "read")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Get Docker containers",
|
||||
description: "Retrieves a list of all Docker containers. Optionally targets a specific remote server by ID.",
|
||||
},
|
||||
})
|
||||
.input(
|
||||
z.object({
|
||||
serverId: z.string().optional(),
|
||||
@@ -36,6 +42,12 @@ export const dockerRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
restartContainer: withPermission("docker", "read")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Restart a Docker container",
|
||||
description: "Restarts a Docker container by its ID. An audit log entry is created for the action.",
|
||||
},
|
||||
})
|
||||
.input(
|
||||
z.object({
|
||||
containerId: z
|
||||
@@ -56,6 +68,12 @@ export const dockerRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
removeContainer: withPermission("docker", "read")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Remove a Docker container",
|
||||
description: "Removes a Docker container by its ID. Optionally targets a remote server. An audit log entry is created for the deletion.",
|
||||
},
|
||||
})
|
||||
.input(
|
||||
z.object({
|
||||
containerId: z
|
||||
@@ -82,6 +100,12 @@ export const dockerRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
getConfig: withPermission("docker", "read")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Get Docker container configuration",
|
||||
description: "Retrieves the configuration (inspect data) for a specific Docker container. Optionally targets a remote server.",
|
||||
},
|
||||
})
|
||||
.input(
|
||||
z.object({
|
||||
containerId: z
|
||||
@@ -102,6 +126,12 @@ export const dockerRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
getContainersByAppNameMatch: withPermission("service", "read")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Get containers by app name match",
|
||||
description: "Retrieves containers whose names match the given application name. Supports filtering by app type (stack or docker-compose) and optionally targets a remote server.",
|
||||
},
|
||||
})
|
||||
.input(
|
||||
z.object({
|
||||
appType: z.enum(["stack", "docker-compose"]).optional(),
|
||||
@@ -124,6 +154,12 @@ export const dockerRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
getContainersByAppLabel: withPermission("docker", "read")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Get containers by app label",
|
||||
description: "Retrieves containers filtered by application label. Supports standalone and swarm deployment types, and optionally targets a remote server.",
|
||||
},
|
||||
})
|
||||
.input(
|
||||
z.object({
|
||||
appName: z.string().min(1).regex(containerIdRegex, "Invalid app name."),
|
||||
@@ -146,6 +182,12 @@ export const dockerRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
getStackContainersByAppName: withPermission("docker", "read")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Get stack containers by app name",
|
||||
description: "Retrieves all containers belonging to a Docker stack by application name. Optionally targets a remote server.",
|
||||
},
|
||||
})
|
||||
.input(
|
||||
z.object({
|
||||
appName: z.string().min(1).regex(containerIdRegex, "Invalid app name."),
|
||||
@@ -163,6 +205,12 @@ export const dockerRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
getServiceContainersByAppName: withPermission("docker", "read")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Get service containers by app name",
|
||||
description: "Retrieves all containers belonging to a Docker Swarm service by application name. Optionally targets a remote server.",
|
||||
},
|
||||
})
|
||||
.input(
|
||||
z.object({
|
||||
appName: z.string().min(1).regex(containerIdRegex, "Invalid app name."),
|
||||
@@ -180,6 +228,12 @@ export const dockerRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
uploadFileToContainer: withPermission("docker", "read")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Upload a file to a Docker container",
|
||||
description: "Uploads a file to a specified path inside a Docker container. The file is converted to a buffer and transferred to the container's filesystem.",
|
||||
},
|
||||
})
|
||||
.input(uploadFileToContainerSchema)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
if (input.serverId) {
|
||||
|
||||
@@ -33,6 +33,12 @@ import {
|
||||
|
||||
export const domainRouter = createTRPCRouter({
|
||||
create: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Create a domain",
|
||||
description: "Creates a new domain for an application or compose service. Validates permissions and logs an audit entry.",
|
||||
},
|
||||
})
|
||||
.input(apiCreateDomain)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
@@ -65,6 +71,12 @@ export const domainRouter = createTRPCRouter({
|
||||
}
|
||||
}),
|
||||
byApplicationId: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "List domains by application",
|
||||
description: "Returns all domains associated with a given application ID.",
|
||||
},
|
||||
})
|
||||
.input(apiFindOneApplication)
|
||||
.query(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.applicationId, {
|
||||
@@ -73,6 +85,12 @@ export const domainRouter = createTRPCRouter({
|
||||
return await findDomainsByApplicationId(input.applicationId);
|
||||
}),
|
||||
byComposeId: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "List domains by compose service",
|
||||
description: "Returns all domains associated with a given compose service ID.",
|
||||
},
|
||||
})
|
||||
.input(apiFindCompose)
|
||||
.query(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.composeId, {
|
||||
@@ -81,6 +99,12 @@ export const domainRouter = createTRPCRouter({
|
||||
return await findDomainsByComposeId(input.composeId);
|
||||
}),
|
||||
generateDomain: withPermission("domain", "create")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Generate a traefik.me domain",
|
||||
description: "Generates a free traefik.me domain for an application, using the server IP to create a wildcard subdomain.",
|
||||
},
|
||||
})
|
||||
.input(z.object({ appName: z.string(), serverId: z.string().optional() }))
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
return generateTraefikMeDomain(
|
||||
@@ -90,6 +114,12 @@ export const domainRouter = createTRPCRouter({
|
||||
);
|
||||
}),
|
||||
canGenerateTraefikMeDomains: withPermission("domain", "read")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Check traefik.me domain availability",
|
||||
description: "Checks whether traefik.me domains can be generated by returning the server IP address. Returns the IP from the server record or web server settings.",
|
||||
},
|
||||
})
|
||||
.input(z.object({ serverId: z.string() }))
|
||||
.query(async ({ input }) => {
|
||||
if (input.serverId) {
|
||||
@@ -101,6 +131,12 @@ export const domainRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
update: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Update a domain",
|
||||
description: "Updates a domain's configuration and refreshes the Traefik routing rules for the associated application or preview deployment.",
|
||||
},
|
||||
})
|
||||
.input(apiUpdateDomain)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const currentDomain = await findDomainById(input.domainId);
|
||||
@@ -141,7 +177,15 @@ export const domainRouter = createTRPCRouter({
|
||||
}
|
||||
return result;
|
||||
}),
|
||||
one: protectedProcedure.input(apiFindDomain).query(async ({ input, ctx }) => {
|
||||
one: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Get a domain",
|
||||
description: "Returns a single domain by its ID. Validates read permissions against the associated service or preview deployment.",
|
||||
},
|
||||
})
|
||||
.input(apiFindDomain)
|
||||
.query(async ({ input, ctx }) => {
|
||||
const domain = await findDomainById(input.domainId);
|
||||
const serviceId = domain.applicationId || domain.composeId;
|
||||
if (serviceId) {
|
||||
@@ -159,6 +203,12 @@ export const domainRouter = createTRPCRouter({
|
||||
return domain;
|
||||
}),
|
||||
delete: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Delete a domain",
|
||||
description: "Deletes a domain by its ID and removes the associated Traefik routing configuration for the application.",
|
||||
},
|
||||
})
|
||||
.input(apiFindDomain)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const domain = await findDomainById(input.domainId);
|
||||
@@ -193,6 +243,12 @@ export const domainRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
validateDomain: withPermission("domain", "read")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Validate a domain",
|
||||
description: "Checks whether a domain's DNS records are correctly configured, optionally verifying against a specific server IP.",
|
||||
},
|
||||
})
|
||||
.input(
|
||||
z.object({
|
||||
domain: z.string(),
|
||||
|
||||
@@ -63,6 +63,12 @@ const filterEnvironmentServices = (
|
||||
|
||||
export const environmentRouter = createTRPCRouter({
|
||||
create: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Create environment",
|
||||
description: "Creates a new environment within a project. The name 'production' is reserved and cannot be used. Checks creation permissions and logs an audit event.",
|
||||
},
|
||||
})
|
||||
.input(apiCreateEnvironment)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
@@ -99,6 +105,12 @@ export const environmentRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
one: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Get environment",
|
||||
description: "Returns a single environment by ID with all its services. Non-admin users only see services they have been granted access to.",
|
||||
},
|
||||
})
|
||||
.input(apiFindOneEnvironment)
|
||||
.query(async ({ input, ctx }) => {
|
||||
const environment = await findEnvironmentById(input.environmentId);
|
||||
@@ -137,6 +149,12 @@ export const environmentRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
byProjectId: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "List environments by project",
|
||||
description: "Returns all environments for a given project. Non-admin users only see environments and services they have been granted access to.",
|
||||
},
|
||||
})
|
||||
.input(z.object({ projectId: z.string() }))
|
||||
.query(async ({ input, ctx }) => {
|
||||
try {
|
||||
@@ -183,6 +201,12 @@ export const environmentRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
remove: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Delete environment",
|
||||
description: "Deletes an environment by ID. The default environment cannot be deleted. Checks deletion permissions and environment access before removing.",
|
||||
},
|
||||
})
|
||||
.input(apiRemoveEnvironment)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
@@ -229,6 +253,12 @@ export const environmentRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
update: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Update environment",
|
||||
description: "Updates an environment's name, description, or env variables. The default environment cannot be renamed. Checks environment access and env-var write permissions.",
|
||||
},
|
||||
})
|
||||
.input(apiUpdateEnvironment)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
@@ -296,6 +326,12 @@ export const environmentRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
duplicate: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Duplicate environment",
|
||||
description: "Creates a copy of an existing environment including its services. Checks environment access and organization ownership before duplicating.",
|
||||
},
|
||||
})
|
||||
.input(apiDuplicateEnvironment)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
@@ -343,6 +379,12 @@ export const environmentRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
search: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Search environments",
|
||||
description: "Searches environments by name, description, or project with pagination. Non-admin users only see environments they have been granted access to.",
|
||||
},
|
||||
})
|
||||
.input(
|
||||
z.object({
|
||||
q: z.string().optional(),
|
||||
|
||||
@@ -21,7 +21,14 @@ import {
|
||||
} from "@/server/db/schema";
|
||||
|
||||
export const gitProviderRouter = createTRPCRouter({
|
||||
getAll: protectedProcedure.query(async ({ ctx }) => {
|
||||
getAll: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "List all git providers",
|
||||
description: "Returns all git providers (GitHub, GitLab, Bitbucket, Gitea) accessible to the current user within the active organization, ordered by creation date.",
|
||||
},
|
||||
})
|
||||
.query(async ({ ctx }) => {
|
||||
const accessibleIds = await getAccessibleGitProviderIds(ctx.session);
|
||||
|
||||
if (accessibleIds.size === 0) {
|
||||
@@ -46,6 +53,12 @@ export const gitProviderRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
toggleShare: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Toggle git provider sharing",
|
||||
description: "Toggles whether a git provider is shared with the entire organization. Only the owner of the provider can change this setting.",
|
||||
},
|
||||
})
|
||||
.input(apiToggleShareGitProvider)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const provider = await findGitProviderById(input.gitProviderId);
|
||||
@@ -73,6 +86,12 @@ export const gitProviderRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
allForPermissions: withPermission("member", "update")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "List git providers for permissions",
|
||||
description: "Returns a minimal list of all git providers in the organization for use in permission assignment UIs. Requires a valid enterprise license and member update permission.",
|
||||
},
|
||||
})
|
||||
.use(async ({ ctx, next }) => {
|
||||
const licensed = await hasValidLicense(ctx.session.activeOrganizationId);
|
||||
if (!licensed) {
|
||||
@@ -96,6 +115,12 @@ export const gitProviderRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
remove: withPermission("gitProviders", "delete")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Remove git provider",
|
||||
description: "Deletes a git provider from the organization. Requires gitProviders delete permission and the provider must belong to the active organization.",
|
||||
},
|
||||
})
|
||||
.input(apiRemoveGitProvider)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
|
||||
@@ -27,6 +27,12 @@ import {
|
||||
|
||||
export const giteaRouter = createTRPCRouter({
|
||||
create: withPermission("gitProviders", "create")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Create Gitea provider",
|
||||
description: "Creates a new Gitea provider configuration linked to the active organization. Requires gitProviders create permission.",
|
||||
},
|
||||
})
|
||||
.input(apiCreateGitea)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
@@ -53,11 +59,26 @@ export const giteaRouter = createTRPCRouter({
|
||||
}
|
||||
}),
|
||||
|
||||
one: protectedProcedure.input(apiFindOneGitea).query(async ({ input }) => {
|
||||
return await findGiteaById(input.giteaId);
|
||||
}),
|
||||
one: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Get Gitea provider",
|
||||
description: "Returns a single Gitea provider configuration by its ID.",
|
||||
},
|
||||
})
|
||||
.input(apiFindOneGitea)
|
||||
.query(async ({ input }) => {
|
||||
return await findGiteaById(input.giteaId);
|
||||
}),
|
||||
|
||||
giteaProviders: protectedProcedure.query(async ({ ctx }) => {
|
||||
giteaProviders: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "List Gitea providers",
|
||||
description: "Returns all Gitea providers accessible to the current user within the active organization, filtered to only those with valid credentials.",
|
||||
},
|
||||
})
|
||||
.query(async ({ ctx }) => {
|
||||
const accessibleIds = await getAccessibleGitProviderIds(ctx.session);
|
||||
|
||||
let result = await db.query.gitea.findMany({
|
||||
@@ -88,6 +109,12 @@ export const giteaRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
getGiteaRepositories: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "List Gitea repositories",
|
||||
description: "Fetches the list of repositories accessible by the Gitea provider. Calls the Gitea API using the provider's credentials.",
|
||||
},
|
||||
})
|
||||
.input(apiFindOneGitea)
|
||||
.query(async ({ input }) => {
|
||||
const { giteaId } = input;
|
||||
@@ -112,6 +139,12 @@ export const giteaRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
getGiteaBranches: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "List Gitea branches",
|
||||
description: "Fetches the list of branches for a specific Gitea repository. Calls the Gitea API using the provider's credentials.",
|
||||
},
|
||||
})
|
||||
.input(apiFindGiteaBranches)
|
||||
.query(async ({ input }) => {
|
||||
const { giteaId, owner, repositoryName } = input;
|
||||
@@ -140,6 +173,12 @@ export const giteaRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
testConnection: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Test Gitea connection",
|
||||
description: "Tests the connection to a Gitea provider by attempting to fetch its repositories. Returns the number of repositories found or throws an error on failure.",
|
||||
},
|
||||
})
|
||||
.input(apiGiteaTestConnection)
|
||||
.mutation(async ({ input }) => {
|
||||
const giteaId = input.giteaId ?? "";
|
||||
@@ -160,6 +199,12 @@ export const giteaRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
update: withPermission("gitProviders", "create")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Update Gitea provider",
|
||||
description: "Updates a Gitea provider configuration and its associated git provider record. Requires gitProviders create permission.",
|
||||
},
|
||||
})
|
||||
.input(apiUpdateGitea)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
if (input.name) {
|
||||
@@ -188,6 +233,12 @@ export const giteaRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
getGiteaUrl: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Get Gitea instance URL",
|
||||
description: "Returns the base URL of the Gitea instance associated with the given provider ID.",
|
||||
},
|
||||
})
|
||||
.input(apiFindOneGitea)
|
||||
.query(async ({ input }) => {
|
||||
const { giteaId } = input;
|
||||
|
||||
@@ -22,20 +22,47 @@ import {
|
||||
} from "@/server/db/schema";
|
||||
|
||||
export const githubRouter = createTRPCRouter({
|
||||
one: protectedProcedure.input(apiFindOneGithub).query(async ({ input }) => {
|
||||
return await findGithubById(input.githubId);
|
||||
}),
|
||||
one: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Get GitHub provider",
|
||||
description: "Returns a single GitHub provider configuration by its ID.",
|
||||
},
|
||||
})
|
||||
.input(apiFindOneGithub)
|
||||
.query(async ({ input }) => {
|
||||
return await findGithubById(input.githubId);
|
||||
}),
|
||||
getGithubRepositories: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "List GitHub repositories",
|
||||
description: "Fetches the list of repositories accessible by the GitHub provider. Calls the GitHub API using the provider's credentials.",
|
||||
},
|
||||
})
|
||||
.input(apiFindOneGithub)
|
||||
.query(async ({ input }) => {
|
||||
return await getGithubRepositories(input.githubId);
|
||||
}),
|
||||
getGithubBranches: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "List GitHub branches",
|
||||
description: "Fetches the list of branches for a specific GitHub repository. Calls the GitHub API using the provider's credentials.",
|
||||
},
|
||||
})
|
||||
.input(apiFindGithubBranches)
|
||||
.query(async ({ input }) => {
|
||||
return await getGithubBranches(input);
|
||||
}),
|
||||
githubProviders: protectedProcedure.query(async ({ ctx }) => {
|
||||
githubProviders: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "List GitHub providers",
|
||||
description: "Returns all GitHub providers accessible to the current user within the active organization, filtered to only those with valid credentials.",
|
||||
},
|
||||
})
|
||||
.query(async ({ ctx }) => {
|
||||
const accessibleIds = await getAccessibleGitProviderIds(ctx.session);
|
||||
|
||||
let result = await db.query.github.findMany({
|
||||
@@ -66,6 +93,12 @@ export const githubRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
testConnection: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Test GitHub connection",
|
||||
description: "Tests the connection to a GitHub provider by attempting to fetch its repositories. Returns the number of repositories found or throws an error on failure.",
|
||||
},
|
||||
})
|
||||
.input(apiFindOneGithub)
|
||||
.mutation(async ({ input }) => {
|
||||
try {
|
||||
@@ -79,6 +112,12 @@ export const githubRouter = createTRPCRouter({
|
||||
}
|
||||
}),
|
||||
update: withPermission("gitProviders", "create")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Update GitHub provider",
|
||||
description: "Updates a GitHub provider configuration and its associated git provider record. Requires gitProviders create permission.",
|
||||
},
|
||||
})
|
||||
.input(apiUpdateGithub)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await updateGitProvider(input.gitProviderId, {
|
||||
|
||||
@@ -27,6 +27,12 @@ import {
|
||||
|
||||
export const gitlabRouter = createTRPCRouter({
|
||||
create: withPermission("gitProviders", "create")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Create GitLab provider",
|
||||
description: "Creates a new GitLab provider configuration linked to the active organization. Requires gitProviders create permission.",
|
||||
},
|
||||
})
|
||||
.input(apiCreateGitlab)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
@@ -51,10 +57,25 @@ export const gitlabRouter = createTRPCRouter({
|
||||
});
|
||||
}
|
||||
}),
|
||||
one: protectedProcedure.input(apiFindOneGitlab).query(async ({ input }) => {
|
||||
return await findGitlabById(input.gitlabId);
|
||||
}),
|
||||
gitlabProviders: protectedProcedure.query(async ({ ctx }) => {
|
||||
one: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Get GitLab provider",
|
||||
description: "Returns a single GitLab provider configuration by its ID.",
|
||||
},
|
||||
})
|
||||
.input(apiFindOneGitlab)
|
||||
.query(async ({ input }) => {
|
||||
return await findGitlabById(input.gitlabId);
|
||||
}),
|
||||
gitlabProviders: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "List GitLab providers",
|
||||
description: "Returns all GitLab providers accessible to the current user within the active organization, filtered to only those with valid credentials.",
|
||||
},
|
||||
})
|
||||
.query(async ({ ctx }) => {
|
||||
const accessibleIds = await getAccessibleGitProviderIds(ctx.session);
|
||||
|
||||
let result = await db.query.gitlab.findMany({
|
||||
@@ -85,17 +106,35 @@ export const gitlabRouter = createTRPCRouter({
|
||||
return filtered;
|
||||
}),
|
||||
getGitlabRepositories: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "List GitLab repositories",
|
||||
description: "Fetches the list of repositories accessible by the GitLab provider. Calls the GitLab API using the provider's credentials.",
|
||||
},
|
||||
})
|
||||
.input(apiFindOneGitlab)
|
||||
.query(async ({ input }) => {
|
||||
return await getGitlabRepositories(input.gitlabId);
|
||||
}),
|
||||
|
||||
getGitlabBranches: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "List GitLab branches",
|
||||
description: "Fetches the list of branches for a specific GitLab repository. Calls the GitLab API using the provider's credentials.",
|
||||
},
|
||||
})
|
||||
.input(apiFindGitlabBranches)
|
||||
.query(async ({ input }) => {
|
||||
return await getGitlabBranches(input);
|
||||
}),
|
||||
testConnection: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Test GitLab connection",
|
||||
description: "Tests the connection to a GitLab provider by attempting to fetch its repositories. Returns the number of repositories found or throws an error on failure.",
|
||||
},
|
||||
})
|
||||
.input(apiGitlabTestConnection)
|
||||
.mutation(async ({ input }) => {
|
||||
try {
|
||||
@@ -110,6 +149,12 @@ export const gitlabRouter = createTRPCRouter({
|
||||
}
|
||||
}),
|
||||
update: withPermission("gitProviders", "create")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Update GitLab provider",
|
||||
description: "Updates a GitLab provider configuration and its associated git provider record. Requires gitProviders create permission.",
|
||||
},
|
||||
})
|
||||
.input(apiUpdateGitlab)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
if (input.name) {
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
findEnvironmentById,
|
||||
findLibsqlById,
|
||||
findProjectById,
|
||||
getAccessibleServerIds,
|
||||
getContainerLogs,
|
||||
IS_CLOUD,
|
||||
rebuildDatabase,
|
||||
@@ -16,7 +17,6 @@ import {
|
||||
stopService,
|
||||
stopServiceRemote,
|
||||
updateLibsqlById,
|
||||
getAccessibleServerIds,
|
||||
} from "@dokploy/server";
|
||||
import {
|
||||
addNewService,
|
||||
@@ -43,6 +43,12 @@ import {
|
||||
} from "@/server/db/schema";
|
||||
export const libsqlRouter = createTRPCRouter({
|
||||
create: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Create a LibSQL database",
|
||||
description: "Creates a new LibSQL database service with the specified configuration, sets up a persistent data volume, and registers it in the project.",
|
||||
},
|
||||
})
|
||||
.input(apiCreateLibsql)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
@@ -100,6 +106,12 @@ export const libsqlRouter = createTRPCRouter({
|
||||
}
|
||||
}),
|
||||
one: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Get a LibSQL database by ID",
|
||||
description: "Returns the full details of a LibSQL database service, including its environment and project configuration.",
|
||||
},
|
||||
})
|
||||
.input(apiFindOneLibsql)
|
||||
.query(async ({ input, ctx }) => {
|
||||
await checkServiceAccess(ctx, input.libsqlId, "read");
|
||||
@@ -118,6 +130,12 @@ export const libsqlRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
start: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Start a LibSQL database",
|
||||
description: "Starts the Docker container for the specified LibSQL database and sets its status to done.",
|
||||
},
|
||||
})
|
||||
.input(apiFindOneLibsql)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.libsqlId, {
|
||||
@@ -143,6 +161,12 @@ export const libsqlRouter = createTRPCRouter({
|
||||
return libsql;
|
||||
}),
|
||||
stop: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Stop a LibSQL database",
|
||||
description: "Stops the Docker container for the specified LibSQL database and sets its status to idle.",
|
||||
},
|
||||
})
|
||||
.input(apiFindOneLibsql)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.libsqlId, {
|
||||
@@ -168,6 +192,12 @@ export const libsqlRouter = createTRPCRouter({
|
||||
return libsql;
|
||||
}),
|
||||
saveExternalPorts: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Save the external ports for a LibSQL database",
|
||||
description: "Updates the external port mappings (HTTP, gRPC, admin) for the LibSQL database and triggers a redeployment. Validates that ports are not already in use.",
|
||||
},
|
||||
})
|
||||
.input(apiSaveExternalPortsLibsql)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.libsqlId, {
|
||||
@@ -230,6 +260,12 @@ export const libsqlRouter = createTRPCRouter({
|
||||
return libsql;
|
||||
}),
|
||||
deploy: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Deploy a LibSQL database",
|
||||
description: "Triggers a deployment for the specified LibSQL database, rebuilding and restarting its Docker container with the current configuration.",
|
||||
},
|
||||
})
|
||||
.input(apiDeployLibsql)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.libsqlId, {
|
||||
@@ -282,6 +318,12 @@ export const libsqlRouter = createTRPCRouter({
|
||||
}
|
||||
}),
|
||||
changeStatus: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Change LibSQL database status",
|
||||
description: "Updates the application status of a LibSQL database without starting or stopping the container.",
|
||||
},
|
||||
})
|
||||
.input(apiChangeLibsqlStatus)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.libsqlId, {
|
||||
@@ -300,6 +342,12 @@ export const libsqlRouter = createTRPCRouter({
|
||||
return libsql;
|
||||
}),
|
||||
remove: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Delete a LibSQL database",
|
||||
description: "Removes the LibSQL database service, its Docker container, and deletes the database record.",
|
||||
},
|
||||
})
|
||||
.input(apiFindOneLibsql)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServiceAccess(ctx, input.libsqlId, "delete");
|
||||
@@ -335,6 +383,12 @@ export const libsqlRouter = createTRPCRouter({
|
||||
return libsql;
|
||||
}),
|
||||
saveEnvironment: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Save environment variables for a LibSQL database",
|
||||
description: "Updates the environment variables for the specified LibSQL database service.",
|
||||
},
|
||||
})
|
||||
.input(apiSaveEnvironmentVariablesLibsql)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.libsqlId, {
|
||||
@@ -359,6 +413,12 @@ export const libsqlRouter = createTRPCRouter({
|
||||
return true;
|
||||
}),
|
||||
reload: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Reload a LibSQL database",
|
||||
description: "Restarts the LibSQL database by stopping and then starting its Docker container.",
|
||||
},
|
||||
})
|
||||
.input(apiResetLibsql)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.libsqlId, {
|
||||
@@ -391,6 +451,12 @@ export const libsqlRouter = createTRPCRouter({
|
||||
return true;
|
||||
}),
|
||||
update: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Update a LibSQL database",
|
||||
description: "Updates the configuration of an existing LibSQL database service.",
|
||||
},
|
||||
})
|
||||
.input(apiUpdateLibsql)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const { libsqlId, ...rest } = input;
|
||||
@@ -417,6 +483,12 @@ export const libsqlRouter = createTRPCRouter({
|
||||
return true;
|
||||
}),
|
||||
move: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Move a LibSQL database to another environment",
|
||||
description: "Moves the LibSQL database to a different environment within the same project.",
|
||||
},
|
||||
})
|
||||
.input(
|
||||
z.object({
|
||||
libsqlId: z.string(),
|
||||
@@ -453,6 +525,12 @@ export const libsqlRouter = createTRPCRouter({
|
||||
return updatedLibsql;
|
||||
}),
|
||||
rebuild: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Rebuild a LibSQL database",
|
||||
description: "Rebuilds the LibSQL database Docker container from scratch, pulling the latest image and recreating the service.",
|
||||
},
|
||||
})
|
||||
.input(apiRebuildLibsql)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.libsqlId, {
|
||||
@@ -469,6 +547,12 @@ export const libsqlRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
readLogs: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Read LibSQL container logs",
|
||||
description: "Retrieves the Docker container logs for the specified LibSQL database, with support for tail count, time-based filtering, and text search.",
|
||||
},
|
||||
})
|
||||
.input(
|
||||
apiFindOneLibsql.extend({
|
||||
tail: z.number().int().min(1).max(10000).default(100),
|
||||
|
||||
@@ -54,6 +54,12 @@ import {
|
||||
import { cancelJobs } from "@/server/utils/backup";
|
||||
export const mariadbRouter = createTRPCRouter({
|
||||
create: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Create a MariaDB database",
|
||||
description: "Creates a new MariaDB database service with the specified configuration, sets up a persistent data volume, and registers it in the project.",
|
||||
},
|
||||
})
|
||||
.input(apiCreateMariaDB)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
@@ -114,6 +120,12 @@ export const mariadbRouter = createTRPCRouter({
|
||||
}
|
||||
}),
|
||||
one: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Get a MariaDB database by ID",
|
||||
description: "Returns the full details of a MariaDB database service, including its environment and project configuration.",
|
||||
},
|
||||
})
|
||||
.input(apiFindOneMariaDB)
|
||||
.query(async ({ input, ctx }) => {
|
||||
await checkServiceAccess(ctx, input.mariadbId, "read");
|
||||
@@ -131,6 +143,12 @@ export const mariadbRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
start: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Start a MariaDB database",
|
||||
description: "Starts the Docker container for the specified MariaDB database and sets its status to done.",
|
||||
},
|
||||
})
|
||||
.input(apiFindOneMariaDB)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.mariadbId, {
|
||||
@@ -155,6 +173,12 @@ export const mariadbRouter = createTRPCRouter({
|
||||
return service;
|
||||
}),
|
||||
stop: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Stop a MariaDB database",
|
||||
description: "Stops the Docker container for the specified MariaDB database and sets its status to idle.",
|
||||
},
|
||||
})
|
||||
.input(apiFindOneMariaDB)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.mariadbId, {
|
||||
@@ -180,6 +204,12 @@ export const mariadbRouter = createTRPCRouter({
|
||||
return mariadb;
|
||||
}),
|
||||
saveExternalPort: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Save the external port for a MariaDB database",
|
||||
description: "Updates the external port mapping for the MariaDB database and triggers a redeployment. Validates that the port is not already in use.",
|
||||
},
|
||||
})
|
||||
.input(apiSaveExternalPortMariaDB)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.mariadbId, {
|
||||
@@ -213,6 +243,12 @@ export const mariadbRouter = createTRPCRouter({
|
||||
return mariadb;
|
||||
}),
|
||||
deploy: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Deploy a MariaDB database",
|
||||
description: "Triggers a deployment for the specified MariaDB database, rebuilding and restarting its Docker container with the current configuration.",
|
||||
},
|
||||
})
|
||||
.input(apiDeployMariaDB)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.mariadbId, {
|
||||
@@ -250,6 +286,12 @@ export const mariadbRouter = createTRPCRouter({
|
||||
});
|
||||
}),
|
||||
changeStatus: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Change MariaDB database status",
|
||||
description: "Updates the application status of a MariaDB database without starting or stopping the container.",
|
||||
},
|
||||
})
|
||||
.input(apiChangeMariaDBStatus)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.mariadbId, {
|
||||
@@ -268,6 +310,12 @@ export const mariadbRouter = createTRPCRouter({
|
||||
return mongo;
|
||||
}),
|
||||
remove: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Delete a MariaDB database",
|
||||
description: "Removes the MariaDB database service, its Docker container, cancels associated backup jobs, and deletes the database record.",
|
||||
},
|
||||
})
|
||||
.input(apiFindOneMariaDB)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServiceAccess(ctx, input.mariadbId, "delete");
|
||||
@@ -305,6 +353,12 @@ export const mariadbRouter = createTRPCRouter({
|
||||
return mongo;
|
||||
}),
|
||||
saveEnvironment: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Save environment variables for a MariaDB database",
|
||||
description: "Updates the environment variables for the specified MariaDB database service.",
|
||||
},
|
||||
})
|
||||
.input(apiSaveEnvironmentVariablesMariaDB)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.mariadbId, {
|
||||
@@ -329,6 +383,12 @@ export const mariadbRouter = createTRPCRouter({
|
||||
return true;
|
||||
}),
|
||||
reload: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Reload a MariaDB database",
|
||||
description: "Restarts the MariaDB database by stopping and then starting its Docker container.",
|
||||
},
|
||||
})
|
||||
.input(apiResetMariadb)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.mariadbId, {
|
||||
@@ -361,6 +421,12 @@ export const mariadbRouter = createTRPCRouter({
|
||||
return true;
|
||||
}),
|
||||
update: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Update a MariaDB database",
|
||||
description: "Updates the configuration of an existing MariaDB database service.",
|
||||
},
|
||||
})
|
||||
.input(apiUpdateMariaDB)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const { mariadbId, ...rest } = input;
|
||||
@@ -387,6 +453,12 @@ export const mariadbRouter = createTRPCRouter({
|
||||
return true;
|
||||
}),
|
||||
changePassword: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Change MariaDB database password",
|
||||
description: "Changes the password for a MariaDB user or root account by executing ALTER USER inside the running container and updating the stored password.",
|
||||
},
|
||||
})
|
||||
.input(
|
||||
z.object({
|
||||
mariadbId: z.string().min(1),
|
||||
@@ -444,6 +516,12 @@ export const mariadbRouter = createTRPCRouter({
|
||||
return true;
|
||||
}),
|
||||
move: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Move a MariaDB database to another environment",
|
||||
description: "Moves the MariaDB database to a different environment within the same project.",
|
||||
},
|
||||
})
|
||||
.input(
|
||||
z.object({
|
||||
mariadbId: z.string(),
|
||||
@@ -480,6 +558,12 @@ export const mariadbRouter = createTRPCRouter({
|
||||
return updatedMariadb;
|
||||
}),
|
||||
rebuild: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Rebuild a MariaDB database",
|
||||
description: "Rebuilds the MariaDB database Docker container from scratch, pulling the latest image and recreating the service.",
|
||||
},
|
||||
})
|
||||
.input(apiRebuildMariadb)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.mariadbId, {
|
||||
@@ -495,6 +579,12 @@ export const mariadbRouter = createTRPCRouter({
|
||||
return true;
|
||||
}),
|
||||
search: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Search MariaDB databases",
|
||||
description: "Returns a paginated list of MariaDB databases matching the given filters. Supports searching by name, appName, description, project, and environment.",
|
||||
},
|
||||
})
|
||||
.input(
|
||||
z.object({
|
||||
q: z.string().optional(),
|
||||
@@ -593,6 +683,12 @@ export const mariadbRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
readLogs: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Read MariaDB container logs",
|
||||
description: "Retrieves the Docker container logs for the specified MariaDB database, with support for tail count, time-based filtering, and text search.",
|
||||
},
|
||||
})
|
||||
.input(
|
||||
apiFindOneMariaDB.extend({
|
||||
tail: z.number().int().min(1).max(10000).default(100),
|
||||
|
||||
@@ -53,6 +53,12 @@ import {
|
||||
import { cancelJobs } from "@/server/utils/backup";
|
||||
export const mongoRouter = createTRPCRouter({
|
||||
create: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Create a MongoDB database",
|
||||
description: "Creates a new MongoDB database service with the specified configuration, sets up a persistent data volume, and registers it in the project.",
|
||||
},
|
||||
})
|
||||
.input(apiCreateMongo)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
@@ -117,6 +123,12 @@ export const mongoRouter = createTRPCRouter({
|
||||
}
|
||||
}),
|
||||
one: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Get a MongoDB database by ID",
|
||||
description: "Returns the full details of a MongoDB database service, including its environment and project configuration.",
|
||||
},
|
||||
})
|
||||
.input(apiFindOneMongo)
|
||||
.query(async ({ input, ctx }) => {
|
||||
await checkServiceAccess(ctx, input.mongoId, "read");
|
||||
@@ -135,6 +147,12 @@ export const mongoRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
start: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Start a MongoDB database",
|
||||
description: "Starts the Docker container for the specified MongoDB database and sets its status to done.",
|
||||
},
|
||||
})
|
||||
.input(apiFindOneMongo)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.mongoId, {
|
||||
@@ -160,6 +178,12 @@ export const mongoRouter = createTRPCRouter({
|
||||
return service;
|
||||
}),
|
||||
stop: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Stop a MongoDB database",
|
||||
description: "Stops the Docker container for the specified MongoDB database and sets its status to idle.",
|
||||
},
|
||||
})
|
||||
.input(apiFindOneMongo)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.mongoId, {
|
||||
@@ -185,6 +209,12 @@ export const mongoRouter = createTRPCRouter({
|
||||
return mongo;
|
||||
}),
|
||||
saveExternalPort: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Save the external port for a MongoDB database",
|
||||
description: "Updates the external port mapping for the MongoDB database and triggers a redeployment. Validates that the port is not already in use.",
|
||||
},
|
||||
})
|
||||
.input(apiSaveExternalPortMongo)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.mongoId, {
|
||||
@@ -218,6 +248,12 @@ export const mongoRouter = createTRPCRouter({
|
||||
return mongo;
|
||||
}),
|
||||
deploy: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Deploy a MongoDB database",
|
||||
description: "Triggers a deployment for the specified MongoDB database, rebuilding and restarting its Docker container with the current configuration.",
|
||||
},
|
||||
})
|
||||
.input(apiDeployMongo)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.mongoId, {
|
||||
@@ -271,6 +307,12 @@ export const mongoRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
changeStatus: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Change MongoDB database status",
|
||||
description: "Updates the application status of a MongoDB database without starting or stopping the container.",
|
||||
},
|
||||
})
|
||||
.input(apiChangeMongoStatus)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.mongoId, {
|
||||
@@ -289,6 +331,12 @@ export const mongoRouter = createTRPCRouter({
|
||||
return mongo;
|
||||
}),
|
||||
reload: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Reload a MongoDB database",
|
||||
description: "Restarts the MongoDB database by stopping and then starting its Docker container.",
|
||||
},
|
||||
})
|
||||
.input(apiResetMongo)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.mongoId, {
|
||||
@@ -321,6 +369,12 @@ export const mongoRouter = createTRPCRouter({
|
||||
return true;
|
||||
}),
|
||||
remove: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Delete a MongoDB database",
|
||||
description: "Removes the MongoDB database service, its Docker container, cancels associated backup jobs, and deletes the database record.",
|
||||
},
|
||||
})
|
||||
.input(apiFindOneMongo)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServiceAccess(ctx, input.mongoId, "delete");
|
||||
@@ -359,6 +413,12 @@ export const mongoRouter = createTRPCRouter({
|
||||
return mongo;
|
||||
}),
|
||||
saveEnvironment: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Save environment variables for a MongoDB database",
|
||||
description: "Updates the environment variables for the specified MongoDB database service.",
|
||||
},
|
||||
})
|
||||
.input(apiSaveEnvironmentVariablesMongo)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.mongoId, {
|
||||
@@ -383,6 +443,12 @@ export const mongoRouter = createTRPCRouter({
|
||||
return true;
|
||||
}),
|
||||
update: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Update a MongoDB database",
|
||||
description: "Updates the configuration of an existing MongoDB database service.",
|
||||
},
|
||||
})
|
||||
.input(apiUpdateMongo)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const { mongoId, ...rest } = input;
|
||||
@@ -409,6 +475,12 @@ export const mongoRouter = createTRPCRouter({
|
||||
return true;
|
||||
}),
|
||||
changePassword: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Change MongoDB database password",
|
||||
description: "Changes the password for the MongoDB database user by executing changeUserPassword via mongosh inside the running container and updating the stored password.",
|
||||
},
|
||||
})
|
||||
.input(
|
||||
z.object({
|
||||
mongoId: z.string().min(1),
|
||||
@@ -459,6 +531,12 @@ export const mongoRouter = createTRPCRouter({
|
||||
return true;
|
||||
}),
|
||||
move: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Move a MongoDB database to another environment",
|
||||
description: "Moves the MongoDB database to a different environment within the same project.",
|
||||
},
|
||||
})
|
||||
.input(
|
||||
z.object({
|
||||
mongoId: z.string(),
|
||||
@@ -495,6 +573,12 @@ export const mongoRouter = createTRPCRouter({
|
||||
return updatedMongo;
|
||||
}),
|
||||
rebuild: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Rebuild a MongoDB database",
|
||||
description: "Rebuilds the MongoDB database Docker container from scratch, pulling the latest image and recreating the service.",
|
||||
},
|
||||
})
|
||||
.input(apiRebuildMongo)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.mongoId, {
|
||||
@@ -511,6 +595,12 @@ export const mongoRouter = createTRPCRouter({
|
||||
return true;
|
||||
}),
|
||||
search: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Search MongoDB databases",
|
||||
description: "Returns a paginated list of MongoDB databases matching the given filters. Supports searching by name, appName, description, project, and environment.",
|
||||
},
|
||||
})
|
||||
.input(
|
||||
z.object({
|
||||
q: z.string().optional(),
|
||||
@@ -604,6 +694,12 @@ export const mongoRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
readLogs: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Read MongoDB container logs",
|
||||
description: "Retrieves the Docker container logs for the specified MongoDB database, with support for tail count, time-based filtering, and text search.",
|
||||
},
|
||||
})
|
||||
.input(
|
||||
apiFindOneMongo.extend({
|
||||
tail: z.number().int().min(1).max(10000).default(100),
|
||||
|
||||
@@ -75,6 +75,12 @@ async function getServiceOrganizationId(
|
||||
|
||||
export const mountRouter = createTRPCRouter({
|
||||
create: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Create mount",
|
||||
description: "Creates a new volume, bind, or file mount for a service. Checks service-level volume permissions and logs an audit event.",
|
||||
},
|
||||
})
|
||||
.input(apiCreateMount)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.serviceId, {
|
||||
@@ -90,6 +96,12 @@ export const mountRouter = createTRPCRouter({
|
||||
return mount;
|
||||
}),
|
||||
remove: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Delete mount",
|
||||
description: "Removes a mount by ID. Resolves the owning service to check volume delete permissions and logs an audit event.",
|
||||
},
|
||||
})
|
||||
.input(apiRemoveMount)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const mount = await findMountById(input.mountId);
|
||||
@@ -116,6 +128,12 @@ export const mountRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
one: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Get mount",
|
||||
description: "Returns a single mount by ID. Resolves the owning service to check volume read permissions.",
|
||||
},
|
||||
})
|
||||
.input(apiFindOneMount)
|
||||
.query(async ({ input, ctx }) => {
|
||||
const mount = await findMountById(input.mountId);
|
||||
@@ -136,6 +154,12 @@ export const mountRouter = createTRPCRouter({
|
||||
return mount;
|
||||
}),
|
||||
update: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Update mount",
|
||||
description: "Updates an existing mount. Resolves the owning service to check volume create permissions and logs an audit event.",
|
||||
},
|
||||
})
|
||||
.input(apiUpdateMount)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const mount = await findMountById(input.mountId);
|
||||
@@ -162,6 +186,12 @@ export const mountRouter = createTRPCRouter({
|
||||
return await updateMount(input.mountId, input);
|
||||
}),
|
||||
allNamedByApplicationId: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "List named volumes by application",
|
||||
description: "Returns Docker named volumes attached to the running container of a given application. Inspects the live container to retrieve mount information.",
|
||||
},
|
||||
})
|
||||
.input(z.object({ applicationId: z.string().min(1) }))
|
||||
.query(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.applicationId, {
|
||||
@@ -175,6 +205,12 @@ export const mountRouter = createTRPCRouter({
|
||||
return mounts;
|
||||
}),
|
||||
listByServiceId: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "List mounts by service",
|
||||
description: "Returns all configured mounts for a given service (application, compose, or database). Verifies service access and organization ownership.",
|
||||
},
|
||||
})
|
||||
.input(apiFindMountByApplicationId)
|
||||
.query(async ({ input, ctx }) => {
|
||||
await checkServiceAccess(ctx, input.serviceId, "read");
|
||||
|
||||
@@ -54,6 +54,12 @@ import { cancelJobs } from "@/server/utils/backup";
|
||||
|
||||
export const mysqlRouter = createTRPCRouter({
|
||||
create: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Create a MySQL database",
|
||||
description: "Creates a new MySQL database service with the specified configuration, sets up a persistent data volume, and registers it in the project.",
|
||||
},
|
||||
})
|
||||
.input(apiCreateMySql)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
@@ -118,6 +124,12 @@ export const mysqlRouter = createTRPCRouter({
|
||||
}
|
||||
}),
|
||||
one: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Get a MySQL database by ID",
|
||||
description: "Returns the full details of a MySQL database service, including its environment and project configuration.",
|
||||
},
|
||||
})
|
||||
.input(apiFindOneMySql)
|
||||
.query(async ({ input, ctx }) => {
|
||||
await checkServiceAccess(ctx, input.mysqlId, "read");
|
||||
@@ -135,6 +147,12 @@ export const mysqlRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
start: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Start a MySQL database",
|
||||
description: "Starts the Docker container for the specified MySQL database and sets its status to done.",
|
||||
},
|
||||
})
|
||||
.input(apiFindOneMySql)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.mysqlId, {
|
||||
@@ -160,6 +178,12 @@ export const mysqlRouter = createTRPCRouter({
|
||||
return service;
|
||||
}),
|
||||
stop: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Stop a MySQL database",
|
||||
description: "Stops the Docker container for the specified MySQL database and sets its status to idle.",
|
||||
},
|
||||
})
|
||||
.input(apiFindOneMySql)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.mysqlId, {
|
||||
@@ -184,6 +208,12 @@ export const mysqlRouter = createTRPCRouter({
|
||||
return mongo;
|
||||
}),
|
||||
saveExternalPort: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Save the external port for a MySQL database",
|
||||
description: "Updates the external port mapping for the MySQL database and triggers a redeployment. Validates that the port is not already in use.",
|
||||
},
|
||||
})
|
||||
.input(apiSaveExternalPortMySql)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.mysqlId, {
|
||||
@@ -217,6 +247,12 @@ export const mysqlRouter = createTRPCRouter({
|
||||
return mysql;
|
||||
}),
|
||||
deploy: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Deploy a MySQL database",
|
||||
description: "Triggers a deployment for the specified MySQL database, rebuilding and restarting its Docker container with the current configuration.",
|
||||
},
|
||||
})
|
||||
.input(apiDeployMySql)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.mysqlId, {
|
||||
@@ -270,6 +306,12 @@ export const mysqlRouter = createTRPCRouter({
|
||||
}
|
||||
}),
|
||||
changeStatus: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Change MySQL database status",
|
||||
description: "Updates the application status of a MySQL database without starting or stopping the container.",
|
||||
},
|
||||
})
|
||||
.input(apiChangeMySqlStatus)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.mysqlId, {
|
||||
@@ -288,6 +330,12 @@ export const mysqlRouter = createTRPCRouter({
|
||||
return mongo;
|
||||
}),
|
||||
reload: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Reload a MySQL database",
|
||||
description: "Restarts the MySQL database by stopping and then starting its Docker container.",
|
||||
},
|
||||
})
|
||||
.input(apiResetMysql)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.mysqlId, {
|
||||
@@ -319,6 +367,12 @@ export const mysqlRouter = createTRPCRouter({
|
||||
return true;
|
||||
}),
|
||||
remove: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Delete a MySQL database",
|
||||
description: "Removes the MySQL database service, its Docker container, cancels associated backup jobs, and deletes the database record.",
|
||||
},
|
||||
})
|
||||
.input(apiFindOneMySql)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServiceAccess(ctx, input.mysqlId, "delete");
|
||||
@@ -355,6 +409,12 @@ export const mysqlRouter = createTRPCRouter({
|
||||
return mongo;
|
||||
}),
|
||||
saveEnvironment: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Save environment variables for a MySQL database",
|
||||
description: "Updates the environment variables for the specified MySQL database service.",
|
||||
},
|
||||
})
|
||||
.input(apiSaveEnvironmentVariablesMySql)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.mysqlId, {
|
||||
@@ -379,6 +439,12 @@ export const mysqlRouter = createTRPCRouter({
|
||||
return true;
|
||||
}),
|
||||
update: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Update a MySQL database",
|
||||
description: "Updates the configuration of an existing MySQL database service.",
|
||||
},
|
||||
})
|
||||
.input(apiUpdateMySql)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const { mysqlId, ...rest } = input;
|
||||
@@ -405,6 +471,12 @@ export const mysqlRouter = createTRPCRouter({
|
||||
return true;
|
||||
}),
|
||||
changePassword: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Change MySQL database password",
|
||||
description: "Changes the password for a MySQL user or root account by executing ALTER USER inside the running container and updating the stored password.",
|
||||
},
|
||||
})
|
||||
.input(
|
||||
z.object({
|
||||
mysqlId: z.string().min(1),
|
||||
@@ -462,6 +534,12 @@ export const mysqlRouter = createTRPCRouter({
|
||||
return true;
|
||||
}),
|
||||
move: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Move a MySQL database to another environment",
|
||||
description: "Moves the MySQL database to a different environment within the same project.",
|
||||
},
|
||||
})
|
||||
.input(
|
||||
z.object({
|
||||
mysqlId: z.string(),
|
||||
@@ -498,6 +576,12 @@ export const mysqlRouter = createTRPCRouter({
|
||||
return updatedMysql;
|
||||
}),
|
||||
rebuild: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Rebuild a MySQL database",
|
||||
description: "Rebuilds the MySQL database Docker container from scratch, pulling the latest image and recreating the service.",
|
||||
},
|
||||
})
|
||||
.input(apiRebuildMysql)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.mysqlId, {
|
||||
@@ -514,6 +598,12 @@ export const mysqlRouter = createTRPCRouter({
|
||||
return true;
|
||||
}),
|
||||
search: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Search MySQL databases",
|
||||
description: "Returns a paginated list of MySQL databases matching the given filters. Supports searching by name, appName, description, project, and environment.",
|
||||
},
|
||||
})
|
||||
.input(
|
||||
z.object({
|
||||
q: z.string().optional(),
|
||||
@@ -607,6 +697,12 @@ export const mysqlRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
readLogs: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Read MySQL container logs",
|
||||
description: "Retrieves the Docker container logs for the specified MySQL database, with support for tail count, time-based filtering, and text search.",
|
||||
},
|
||||
})
|
||||
.input(
|
||||
apiFindOneMySql.extend({
|
||||
tail: z.number().int().min(1).max(10000).default(100),
|
||||
|
||||
@@ -95,6 +95,12 @@ import {
|
||||
|
||||
export const notificationRouter = createTRPCRouter({
|
||||
createSlack: withPermission("notification", "create")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Create Slack notification",
|
||||
description: "Creates a new Slack notification provider for the current organization and logs an audit event.",
|
||||
},
|
||||
})
|
||||
.input(apiCreateSlack)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
@@ -114,6 +120,12 @@ export const notificationRouter = createTRPCRouter({
|
||||
}
|
||||
}),
|
||||
updateSlack: withPermission("notification", "update")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Update Slack notification",
|
||||
description: "Updates an existing Slack notification provider. Verifies organization ownership before applying changes.",
|
||||
},
|
||||
})
|
||||
.input(apiUpdateSlack)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
@@ -140,6 +152,12 @@ export const notificationRouter = createTRPCRouter({
|
||||
}
|
||||
}),
|
||||
testSlackConnection: withPermission("notification", "create")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Test Slack connection",
|
||||
description: "Sends a test message to the configured Slack channel to verify the webhook connection works.",
|
||||
},
|
||||
})
|
||||
.input(apiTestSlackConnection)
|
||||
.mutation(async ({ input }) => {
|
||||
try {
|
||||
@@ -157,6 +175,12 @@ export const notificationRouter = createTRPCRouter({
|
||||
}
|
||||
}),
|
||||
createTelegram: withPermission("notification", "create")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Create Telegram notification",
|
||||
description: "Creates a new Telegram notification provider for the current organization and logs an audit event.",
|
||||
},
|
||||
})
|
||||
.input(apiCreateTelegram)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
@@ -179,6 +203,12 @@ export const notificationRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
updateTelegram: withPermission("notification", "update")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Update Telegram notification",
|
||||
description: "Updates an existing Telegram notification provider. Verifies organization ownership before applying changes.",
|
||||
},
|
||||
})
|
||||
.input(apiUpdateTelegram)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
@@ -209,6 +239,12 @@ export const notificationRouter = createTRPCRouter({
|
||||
}
|
||||
}),
|
||||
testTelegramConnection: withPermission("notification", "create")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Test Telegram connection",
|
||||
description: "Sends a test message to the configured Telegram chat to verify the bot token and chat ID work.",
|
||||
},
|
||||
})
|
||||
.input(apiTestTelegramConnection)
|
||||
.mutation(async ({ input }) => {
|
||||
try {
|
||||
@@ -223,6 +259,12 @@ export const notificationRouter = createTRPCRouter({
|
||||
}
|
||||
}),
|
||||
createDiscord: withPermission("notification", "create")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Create Discord notification",
|
||||
description: "Creates a new Discord notification provider for the current organization and logs an audit event.",
|
||||
},
|
||||
})
|
||||
.input(apiCreateDiscord)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
@@ -245,6 +287,12 @@ export const notificationRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
updateDiscord: withPermission("notification", "update")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Update Discord notification",
|
||||
description: "Updates an existing Discord notification provider. Verifies organization ownership before applying changes.",
|
||||
},
|
||||
})
|
||||
.input(apiUpdateDiscord)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
@@ -276,6 +324,12 @@ export const notificationRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
testDiscordConnection: withPermission("notification", "create")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Test Discord connection",
|
||||
description: "Sends a test embed message to the configured Discord webhook to verify the connection works.",
|
||||
},
|
||||
})
|
||||
.input(apiTestDiscordConnection)
|
||||
.mutation(async ({ input }) => {
|
||||
try {
|
||||
@@ -298,6 +352,12 @@ export const notificationRouter = createTRPCRouter({
|
||||
}
|
||||
}),
|
||||
createEmail: withPermission("notification", "create")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Create Email notification",
|
||||
description: "Creates a new SMTP email notification provider for the current organization and logs an audit event.",
|
||||
},
|
||||
})
|
||||
.input(apiCreateEmail)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
@@ -316,6 +376,12 @@ export const notificationRouter = createTRPCRouter({
|
||||
}
|
||||
}),
|
||||
updateEmail: withPermission("notification", "update")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Update Email notification",
|
||||
description: "Updates an existing SMTP email notification provider. Verifies organization ownership before applying changes.",
|
||||
},
|
||||
})
|
||||
.input(apiUpdateEmail)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
@@ -346,6 +412,12 @@ export const notificationRouter = createTRPCRouter({
|
||||
}
|
||||
}),
|
||||
testEmailConnection: withPermission("notification", "create")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Test Email connection",
|
||||
description: "Sends a test email via the configured SMTP settings to verify the connection works.",
|
||||
},
|
||||
})
|
||||
.input(apiTestEmailConnection)
|
||||
.mutation(async ({ input }) => {
|
||||
try {
|
||||
@@ -364,6 +436,12 @@ export const notificationRouter = createTRPCRouter({
|
||||
}
|
||||
}),
|
||||
createResend: withPermission("notification", "create")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Create Resend notification",
|
||||
description: "Creates a new Resend email notification provider for the current organization and logs an audit event.",
|
||||
},
|
||||
})
|
||||
.input(apiCreateResend)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
@@ -382,6 +460,12 @@ export const notificationRouter = createTRPCRouter({
|
||||
}
|
||||
}),
|
||||
updateResend: withPermission("notification", "update")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Update Resend notification",
|
||||
description: "Updates an existing Resend email notification provider. Verifies organization ownership before applying changes.",
|
||||
},
|
||||
})
|
||||
.input(apiUpdateResend)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
@@ -412,6 +496,12 @@ export const notificationRouter = createTRPCRouter({
|
||||
}
|
||||
}),
|
||||
testResendConnection: withPermission("notification", "create")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Test Resend connection",
|
||||
description: "Sends a test email via Resend to verify the API key and configuration work.",
|
||||
},
|
||||
})
|
||||
.input(apiTestResendConnection)
|
||||
.mutation(async ({ input }) => {
|
||||
try {
|
||||
@@ -430,6 +520,12 @@ export const notificationRouter = createTRPCRouter({
|
||||
}
|
||||
}),
|
||||
remove: withPermission("notification", "delete")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Delete notification",
|
||||
description: "Removes a notification provider by ID. Verifies organization ownership and logs an audit event before deletion.",
|
||||
},
|
||||
})
|
||||
.input(apiFindOneNotification)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
@@ -458,6 +554,12 @@ export const notificationRouter = createTRPCRouter({
|
||||
}
|
||||
}),
|
||||
one: withPermission("notification", "read")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Get notification",
|
||||
description: "Returns a single notification provider by ID. Verifies the caller belongs to the same organization.",
|
||||
},
|
||||
})
|
||||
.input(apiFindOneNotification)
|
||||
.query(async ({ input, ctx }) => {
|
||||
const notification = await findNotificationById(input.notificationId);
|
||||
@@ -469,7 +571,14 @@ export const notificationRouter = createTRPCRouter({
|
||||
}
|
||||
return notification;
|
||||
}),
|
||||
all: withPermission("notification", "read").query(async ({ ctx }) => {
|
||||
all: withPermission("notification", "read")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "List all notifications",
|
||||
description: "Returns all notification providers for the current organization, including all provider-specific details (Slack, Telegram, Discord, etc.).",
|
||||
},
|
||||
})
|
||||
.query(async ({ ctx }) => {
|
||||
return await db.query.notifications.findMany({
|
||||
with: {
|
||||
slack: true,
|
||||
@@ -490,6 +599,12 @@ export const notificationRouter = createTRPCRouter({
|
||||
});
|
||||
}),
|
||||
receiveNotification: publicProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Receive server threshold notification",
|
||||
description: "Public endpoint that receives CPU/memory threshold alerts from Dokploy or remote servers. Validates the token and dispatches notifications to all configured providers.",
|
||||
},
|
||||
})
|
||||
.input(
|
||||
z.object({
|
||||
ServerType: z.enum(["Dokploy", "Remote"]).default("Dokploy"),
|
||||
@@ -551,6 +666,12 @@ export const notificationRouter = createTRPCRouter({
|
||||
}
|
||||
}),
|
||||
createGotify: withPermission("notification", "create")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Create Gotify notification",
|
||||
description: "Creates a new Gotify notification provider for the current organization and logs an audit event.",
|
||||
},
|
||||
})
|
||||
.input(apiCreateGotify)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
@@ -569,6 +690,12 @@ export const notificationRouter = createTRPCRouter({
|
||||
}
|
||||
}),
|
||||
updateGotify: withPermission("notification", "update")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Update Gotify notification",
|
||||
description: "Updates an existing Gotify notification provider. Verifies organization ownership before applying changes.",
|
||||
},
|
||||
})
|
||||
.input(apiUpdateGotify)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
@@ -598,6 +725,12 @@ export const notificationRouter = createTRPCRouter({
|
||||
}
|
||||
}),
|
||||
testGotifyConnection: withPermission("notification", "create")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Test Gotify connection",
|
||||
description: "Sends a test notification to the configured Gotify server to verify the connection works.",
|
||||
},
|
||||
})
|
||||
.input(apiTestGotifyConnection)
|
||||
.mutation(async ({ input }) => {
|
||||
try {
|
||||
@@ -616,6 +749,12 @@ export const notificationRouter = createTRPCRouter({
|
||||
}
|
||||
}),
|
||||
createNtfy: withPermission("notification", "create")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Create ntfy notification",
|
||||
description: "Creates a new ntfy notification provider for the current organization and logs an audit event.",
|
||||
},
|
||||
})
|
||||
.input(apiCreateNtfy)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
@@ -634,6 +773,12 @@ export const notificationRouter = createTRPCRouter({
|
||||
}
|
||||
}),
|
||||
updateNtfy: withPermission("notification", "update")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Update ntfy notification",
|
||||
description: "Updates an existing ntfy notification provider. Verifies organization ownership before applying changes.",
|
||||
},
|
||||
})
|
||||
.input(apiUpdateNtfy)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
@@ -663,6 +808,12 @@ export const notificationRouter = createTRPCRouter({
|
||||
}
|
||||
}),
|
||||
testNtfyConnection: withPermission("notification", "create")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Test ntfy connection",
|
||||
description: "Sends a test notification to the configured ntfy topic to verify the connection works.",
|
||||
},
|
||||
})
|
||||
.input(apiTestNtfyConnection)
|
||||
.mutation(async ({ input }) => {
|
||||
try {
|
||||
@@ -686,6 +837,12 @@ export const notificationRouter = createTRPCRouter({
|
||||
}
|
||||
}),
|
||||
createMattermost: withPermission("notification", "create")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Create Mattermost notification",
|
||||
description: "Creates a new Mattermost notification provider for the current organization and logs an audit event.",
|
||||
},
|
||||
})
|
||||
.input(apiCreateMattermost)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
@@ -707,6 +864,12 @@ export const notificationRouter = createTRPCRouter({
|
||||
}
|
||||
}),
|
||||
updateMattermost: withPermission("notification", "update")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Update Mattermost notification",
|
||||
description: "Updates an existing Mattermost notification provider. Verifies organization ownership before applying changes.",
|
||||
},
|
||||
})
|
||||
.input(apiUpdateMattermost)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
@@ -736,6 +899,12 @@ export const notificationRouter = createTRPCRouter({
|
||||
}
|
||||
}),
|
||||
testMattermostConnection: withPermission("notification", "create")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Test Mattermost connection",
|
||||
description: "Sends a test message to the configured Mattermost webhook to verify the connection works.",
|
||||
},
|
||||
})
|
||||
.input(apiTestMattermostConnection)
|
||||
.mutation(async ({ input }) => {
|
||||
try {
|
||||
@@ -754,6 +923,12 @@ export const notificationRouter = createTRPCRouter({
|
||||
}
|
||||
}),
|
||||
createCustom: withPermission("notification", "create")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Create custom webhook notification",
|
||||
description: "Creates a new custom webhook notification provider for the current organization and logs an audit event.",
|
||||
},
|
||||
})
|
||||
.input(apiCreateCustom)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
@@ -772,6 +947,12 @@ export const notificationRouter = createTRPCRouter({
|
||||
}
|
||||
}),
|
||||
updateCustom: withPermission("notification", "update")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Update custom webhook notification",
|
||||
description: "Updates an existing custom webhook notification provider. Verifies organization ownership before applying changes.",
|
||||
},
|
||||
})
|
||||
.input(apiUpdateCustom)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
@@ -798,6 +979,12 @@ export const notificationRouter = createTRPCRouter({
|
||||
}
|
||||
}),
|
||||
testCustomConnection: withPermission("notification", "create")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Test custom webhook connection",
|
||||
description: "Sends a test payload to the configured custom webhook URL to verify the connection works.",
|
||||
},
|
||||
})
|
||||
.input(apiTestCustomConnection)
|
||||
.mutation(async ({ input }) => {
|
||||
try {
|
||||
@@ -816,6 +1003,12 @@ export const notificationRouter = createTRPCRouter({
|
||||
}
|
||||
}),
|
||||
createLark: withPermission("notification", "create")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Create Lark notification",
|
||||
description: "Creates a new Lark notification provider for the current organization and logs an audit event.",
|
||||
},
|
||||
})
|
||||
.input(apiCreateLark)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
@@ -834,6 +1027,12 @@ export const notificationRouter = createTRPCRouter({
|
||||
}
|
||||
}),
|
||||
updateLark: withPermission("notification", "update")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Update Lark notification",
|
||||
description: "Updates an existing Lark notification provider. Verifies organization ownership before applying changes.",
|
||||
},
|
||||
})
|
||||
.input(apiUpdateLark)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
@@ -863,6 +1062,12 @@ export const notificationRouter = createTRPCRouter({
|
||||
}
|
||||
}),
|
||||
testLarkConnection: withPermission("notification", "create")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Test Lark connection",
|
||||
description: "Sends a test message to the configured Lark webhook to verify the connection works.",
|
||||
},
|
||||
})
|
||||
.input(apiTestLarkConnection)
|
||||
.mutation(async ({ input }) => {
|
||||
try {
|
||||
@@ -882,6 +1087,12 @@ export const notificationRouter = createTRPCRouter({
|
||||
}
|
||||
}),
|
||||
createTeams: withPermission("notification", "create")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Create Teams notification",
|
||||
description: "Creates a new Microsoft Teams notification provider for the current organization and logs an audit event.",
|
||||
},
|
||||
})
|
||||
.input(apiCreateTeams)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
@@ -900,6 +1111,12 @@ export const notificationRouter = createTRPCRouter({
|
||||
}
|
||||
}),
|
||||
updateTeams: withPermission("notification", "update")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Update Teams notification",
|
||||
description: "Updates an existing Microsoft Teams notification provider. Verifies organization ownership before applying changes.",
|
||||
},
|
||||
})
|
||||
.input(apiUpdateTeams)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
@@ -929,6 +1146,12 @@ export const notificationRouter = createTRPCRouter({
|
||||
}
|
||||
}),
|
||||
testTeamsConnection: withPermission("notification", "create")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Test Teams connection",
|
||||
description: "Sends a test message to the configured Microsoft Teams webhook to verify the connection works.",
|
||||
},
|
||||
})
|
||||
.input(apiTestTeamsConnection)
|
||||
.mutation(async ({ input }) => {
|
||||
try {
|
||||
@@ -946,6 +1169,12 @@ export const notificationRouter = createTRPCRouter({
|
||||
}
|
||||
}),
|
||||
createPushover: withPermission("notification", "create")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Create Pushover notification",
|
||||
description: "Creates a new Pushover notification provider for the current organization and logs an audit event.",
|
||||
},
|
||||
})
|
||||
.input(apiCreatePushover)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
@@ -967,6 +1196,12 @@ export const notificationRouter = createTRPCRouter({
|
||||
}
|
||||
}),
|
||||
updatePushover: withPermission("notification", "update")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Update Pushover notification",
|
||||
description: "Updates an existing Pushover notification provider. Verifies organization ownership before applying changes.",
|
||||
},
|
||||
})
|
||||
.input(apiUpdatePushover)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
@@ -996,6 +1231,12 @@ export const notificationRouter = createTRPCRouter({
|
||||
}
|
||||
}),
|
||||
testPushoverConnection: withPermission("notification", "create")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Test Pushover connection",
|
||||
description: "Sends a test notification to the configured Pushover account to verify the connection works.",
|
||||
},
|
||||
})
|
||||
.input(apiTestPushoverConnection)
|
||||
.mutation(async ({ input }) => {
|
||||
try {
|
||||
@@ -1013,7 +1254,14 @@ export const notificationRouter = createTRPCRouter({
|
||||
});
|
||||
}
|
||||
}),
|
||||
getEmailProviders: withPermission("notification", "read").query(
|
||||
getEmailProviders: withPermission("notification", "read")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "List email notification providers",
|
||||
description: "Returns all notification providers that support email (SMTP and Resend) for the current organization.",
|
||||
},
|
||||
})
|
||||
.query(
|
||||
async ({ ctx }) => {
|
||||
return await db.query.notifications.findMany({
|
||||
where: eq(
|
||||
|
||||
@@ -15,6 +15,12 @@ import {
|
||||
import { createTRPCRouter, protectedProcedure, withPermission } from "../trpc";
|
||||
export const organizationRouter = createTRPCRouter({
|
||||
create: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Create an organization",
|
||||
description: "Create a new organization and add the current user as the owner. Only owners and admins can create organizations in self-hosted mode.",
|
||||
},
|
||||
})
|
||||
.input(
|
||||
z.object({
|
||||
name: z.string(),
|
||||
@@ -65,7 +71,14 @@ export const organizationRouter = createTRPCRouter({
|
||||
});
|
||||
return result;
|
||||
}),
|
||||
all: protectedProcedure.query(async ({ ctx }) => {
|
||||
all: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "List all organizations",
|
||||
description: "Retrieve all organizations the current user is a member of, including their membership details.",
|
||||
},
|
||||
})
|
||||
.query(async ({ ctx }) => {
|
||||
const memberResult = await db.query.organization.findMany({
|
||||
where: (organization) =>
|
||||
exists(
|
||||
@@ -88,6 +101,12 @@ export const organizationRouter = createTRPCRouter({
|
||||
return memberResult;
|
||||
}),
|
||||
one: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Get an organization by ID",
|
||||
description: "Retrieve a single organization by its ID. The current user must be a member of the organization.",
|
||||
},
|
||||
})
|
||||
.input(
|
||||
z.object({
|
||||
organizationId: z.string(),
|
||||
@@ -114,6 +133,12 @@ export const organizationRouter = createTRPCRouter({
|
||||
});
|
||||
}),
|
||||
update: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Update an organization",
|
||||
description: "Update the name and logo of an organization. Only the organization owner can perform this action.",
|
||||
},
|
||||
})
|
||||
.input(
|
||||
z.object({
|
||||
organizationId: z.string(),
|
||||
@@ -178,6 +203,12 @@ export const organizationRouter = createTRPCRouter({
|
||||
return result[0];
|
||||
}),
|
||||
delete: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Delete an organization",
|
||||
description: "Delete an organization by ID. Only the owner can delete it, and they must retain at least one organization.",
|
||||
},
|
||||
})
|
||||
.input(
|
||||
z.object({
|
||||
organizationId: z.string(),
|
||||
@@ -248,6 +279,12 @@ export const organizationRouter = createTRPCRouter({
|
||||
return result;
|
||||
}),
|
||||
inviteMember: withPermission("member", "create")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Invite a member to organization",
|
||||
description: "Create a pending invitation for a user by email to join the active organization with the specified role. Checks for existing membership and pending invitations. Supports custom roles.",
|
||||
},
|
||||
})
|
||||
.input(
|
||||
z.object({
|
||||
email: z.string().email(),
|
||||
@@ -335,13 +372,26 @@ export const organizationRouter = createTRPCRouter({
|
||||
return created;
|
||||
}),
|
||||
|
||||
allInvitations: withPermission("member", "create").query(async ({ ctx }) => {
|
||||
allInvitations: withPermission("member", "create")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "List all organization invitations",
|
||||
description: "Retrieve all invitations for the active organization, ordered by status and expiration date.",
|
||||
},
|
||||
})
|
||||
.query(async ({ ctx }) => {
|
||||
return await db.query.invitation.findMany({
|
||||
where: eq(invitation.organizationId, ctx.session.activeOrganizationId),
|
||||
orderBy: [desc(invitation.status), desc(invitation.expiresAt)],
|
||||
});
|
||||
}),
|
||||
removeInvitation: withPermission("member", "create")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Remove an invitation",
|
||||
description: "Delete a pending invitation by ID. Only invitations belonging to the active organization can be removed.",
|
||||
},
|
||||
})
|
||||
.input(z.object({ invitationId: z.string() }))
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const invitationResult = await db.query.invitation.findFirst({
|
||||
@@ -377,6 +427,12 @@ export const organizationRouter = createTRPCRouter({
|
||||
return result;
|
||||
}),
|
||||
updateMemberRole: withPermission("member", "update")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Update member role",
|
||||
description: "Change the role of a member in the active organization. Users cannot change their own role, and the owner role is nontransferable. Only owners can change admin roles. Supports custom roles.",
|
||||
},
|
||||
})
|
||||
.input(
|
||||
z.object({
|
||||
memberId: z.string(),
|
||||
@@ -463,6 +519,12 @@ export const organizationRouter = createTRPCRouter({
|
||||
return true;
|
||||
}),
|
||||
setDefault: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Set default organization",
|
||||
description: "Set an organization as the default for the current user. Unsets any previous default and marks the specified organization as the new default.",
|
||||
},
|
||||
})
|
||||
.input(
|
||||
z.object({
|
||||
organizationId: z.string().min(1),
|
||||
@@ -509,7 +571,14 @@ export const organizationRouter = createTRPCRouter({
|
||||
});
|
||||
return { success: true };
|
||||
}),
|
||||
active: protectedProcedure.query(async ({ ctx }) => {
|
||||
active: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Get active organization",
|
||||
description: "Retrieve the organization that is currently active in the user's session. Returns null if no organization is active.",
|
||||
},
|
||||
})
|
||||
.query(async ({ ctx }) => {
|
||||
if (!ctx.session.activeOrganizationId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -50,6 +50,12 @@ const resolvePatchServiceId = (patch: {
|
||||
|
||||
export const patchRouter = createTRPCRouter({
|
||||
create: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Create patch",
|
||||
description: "Creates a new file patch for an application or compose service. Checks service-level permissions and logs an audit event.",
|
||||
},
|
||||
})
|
||||
.input(apiCreatePatch)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const serviceId = input.applicationId ?? input.composeId;
|
||||
@@ -73,7 +79,15 @@ export const patchRouter = createTRPCRouter({
|
||||
return result;
|
||||
}),
|
||||
|
||||
one: protectedProcedure.input(apiFindPatch).query(async ({ input, ctx }) => {
|
||||
one: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Get patch",
|
||||
description: "Returns a single patch by ID. Resolves the associated service to verify read permissions.",
|
||||
},
|
||||
})
|
||||
.input(apiFindPatch)
|
||||
.query(async ({ input, ctx }) => {
|
||||
const patch = await findPatchById(input.patchId);
|
||||
const serviceId = resolvePatchServiceId(patch);
|
||||
await checkServicePermissionAndAccess(ctx, serviceId, {
|
||||
@@ -83,6 +97,12 @@ export const patchRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
byEntityId: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "List patches by entity",
|
||||
description: "Returns all patches associated with a given application or compose service.",
|
||||
},
|
||||
})
|
||||
.input(
|
||||
z.object({ id: z.string(), type: z.enum(["application", "compose"]) }),
|
||||
)
|
||||
@@ -94,6 +114,12 @@ export const patchRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
update: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Update patch",
|
||||
description: "Updates the content or configuration of an existing patch. Resolves the associated service to verify permissions and logs an audit event.",
|
||||
},
|
||||
})
|
||||
.input(apiUpdatePatch)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const patch = await findPatchById(input.patchId);
|
||||
@@ -114,6 +140,12 @@ export const patchRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
delete: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Delete patch",
|
||||
description: "Deletes a patch by ID. Resolves the associated service to verify delete permissions and logs an audit event.",
|
||||
},
|
||||
})
|
||||
.input(apiDeletePatch)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const patch = await findPatchById(input.patchId);
|
||||
@@ -133,6 +165,12 @@ export const patchRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
toggleEnabled: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Toggle patch enabled state",
|
||||
description: "Enables or disables a patch without deleting it. Resolves the associated service to verify permissions and logs an audit event.",
|
||||
},
|
||||
})
|
||||
.input(apiTogglePatchEnabled)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const patch = await findPatchById(input.patchId);
|
||||
@@ -155,6 +193,12 @@ export const patchRouter = createTRPCRouter({
|
||||
|
||||
// Repository Operations
|
||||
ensureRepo: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Ensure patch repository exists",
|
||||
description: "Ensures a patch repository is initialized for the given application or compose service. Creates the repo if it does not exist and logs an audit event.",
|
||||
},
|
||||
})
|
||||
.input(
|
||||
z.object({
|
||||
id: z.string(),
|
||||
@@ -179,6 +223,12 @@ export const patchRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
readRepoDirectories: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "List patch repository directories",
|
||||
description: "Reads the directory listing at a given path inside the patch repository for an application or compose service.",
|
||||
},
|
||||
})
|
||||
.input(
|
||||
z.object({
|
||||
id: z.string().min(1),
|
||||
@@ -202,6 +252,12 @@ export const patchRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
readRepoFile: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Read patch repository file",
|
||||
description: "Reads a file from the patch repository. For delete-type patches it returns the current repo content; otherwise returns the patch content if available, falling back to the repo file.",
|
||||
},
|
||||
})
|
||||
.input(
|
||||
z.object({
|
||||
id: z.string().min(1),
|
||||
@@ -241,6 +297,12 @@ export const patchRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
saveFileAsPatch: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Save file as patch",
|
||||
description: "Creates or updates a patch record from file content. If a patch already exists for the file path, it updates the existing patch; otherwise creates a new one.",
|
||||
},
|
||||
})
|
||||
.input(
|
||||
z.object({
|
||||
id: z.string().min(1),
|
||||
@@ -291,6 +353,12 @@ export const patchRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
markFileForDeletion: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Mark file for deletion",
|
||||
description: "Creates a delete-type patch that will remove the specified file from the service on next deployment. Logs an audit event.",
|
||||
},
|
||||
})
|
||||
.input(
|
||||
z.object({
|
||||
id: z.string().min(1),
|
||||
@@ -318,6 +386,12 @@ export const patchRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
cleanPatchRepos: adminProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Clean patch repositories",
|
||||
description: "Removes all patch repository working directories on the local or a specified remote server. Admin-only operation that logs an audit event.",
|
||||
},
|
||||
})
|
||||
.input(z.object({ serverId: z.string().optional() }))
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await cleanPatchRepos(input.serverId);
|
||||
|
||||
@@ -16,6 +16,12 @@ import {
|
||||
|
||||
export const portRouter = createTRPCRouter({
|
||||
create: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Create a port",
|
||||
description: "Creates a new port mapping for an application, binding a published port to a target port. Logs an audit entry.",
|
||||
},
|
||||
})
|
||||
.input(apiCreatePort)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
@@ -39,6 +45,12 @@ export const portRouter = createTRPCRouter({
|
||||
}
|
||||
}),
|
||||
one: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Get a port",
|
||||
description: "Returns a single port mapping by its ID, including the associated application details.",
|
||||
},
|
||||
})
|
||||
.input(apiFindOnePort)
|
||||
.query(async ({ input, ctx }) => {
|
||||
try {
|
||||
@@ -58,6 +70,12 @@ export const portRouter = createTRPCRouter({
|
||||
}
|
||||
}),
|
||||
delete: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Delete a port",
|
||||
description: "Deletes a port mapping by its ID and logs an audit entry with the published and target port details.",
|
||||
},
|
||||
})
|
||||
.input(apiFindOnePort)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const port = await finPortById(input.portId);
|
||||
@@ -85,6 +103,12 @@ export const portRouter = createTRPCRouter({
|
||||
}
|
||||
}),
|
||||
update: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Update a port",
|
||||
description: "Updates an existing port mapping's configuration and logs an audit entry.",
|
||||
},
|
||||
})
|
||||
.input(apiUpdatePort)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const port = await finPortById(input.portId);
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
findEnvironmentById,
|
||||
findPostgresById,
|
||||
findProjectById,
|
||||
getAccessibleServerIds,
|
||||
getContainerLogs,
|
||||
getMountPath,
|
||||
getServiceContainerCommand,
|
||||
@@ -21,7 +22,6 @@ import {
|
||||
stopService,
|
||||
stopServiceRemote,
|
||||
updatePostgresById,
|
||||
getAccessibleServerIds,
|
||||
} from "@dokploy/server";
|
||||
import { db } from "@dokploy/server/db";
|
||||
import {
|
||||
@@ -55,6 +55,12 @@ import { cancelJobs } from "@/server/utils/backup";
|
||||
|
||||
export const postgresRouter = createTRPCRouter({
|
||||
create: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Create a PostgreSQL database",
|
||||
description: "Creates a new PostgreSQL database service with the specified configuration, sets up a persistent data volume, and registers it in the project.",
|
||||
},
|
||||
})
|
||||
.input(apiCreatePostgres)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
@@ -121,6 +127,12 @@ export const postgresRouter = createTRPCRouter({
|
||||
}
|
||||
}),
|
||||
one: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Get a PostgreSQL database by ID",
|
||||
description: "Returns the full details of a PostgreSQL database service, including its environment and project configuration.",
|
||||
},
|
||||
})
|
||||
.input(apiFindOnePostgres)
|
||||
.query(async ({ input, ctx }) => {
|
||||
await checkServiceAccess(ctx, input.postgresId, "read");
|
||||
@@ -139,6 +151,12 @@ export const postgresRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
start: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Start a PostgreSQL database",
|
||||
description: "Starts the Docker container for the specified PostgreSQL database and sets its status to done.",
|
||||
},
|
||||
})
|
||||
.input(apiFindOnePostgres)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.postgresId, {
|
||||
@@ -164,6 +182,12 @@ export const postgresRouter = createTRPCRouter({
|
||||
return service;
|
||||
}),
|
||||
stop: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Stop a PostgreSQL database",
|
||||
description: "Stops the Docker container for the specified PostgreSQL database and sets its status to idle.",
|
||||
},
|
||||
})
|
||||
.input(apiFindOnePostgres)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.postgresId, {
|
||||
@@ -188,6 +212,12 @@ export const postgresRouter = createTRPCRouter({
|
||||
return postgres;
|
||||
}),
|
||||
saveExternalPort: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Save the external port for a PostgreSQL database",
|
||||
description: "Updates the external port mapping for the PostgreSQL database and triggers a redeployment. Validates that the port is not already in use.",
|
||||
},
|
||||
})
|
||||
.input(apiSaveExternalPortPostgres)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.postgresId, {
|
||||
@@ -221,6 +251,12 @@ export const postgresRouter = createTRPCRouter({
|
||||
return postgres;
|
||||
}),
|
||||
deploy: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Deploy a PostgreSQL database",
|
||||
description: "Triggers a deployment for the specified PostgreSQL database, rebuilding and restarting its Docker container with the current configuration.",
|
||||
},
|
||||
})
|
||||
.input(apiDeployPostgres)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.postgresId, {
|
||||
@@ -276,6 +312,12 @@ export const postgresRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
changeStatus: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Change PostgreSQL database status",
|
||||
description: "Updates the application status of a PostgreSQL database without starting or stopping the container.",
|
||||
},
|
||||
})
|
||||
.input(apiChangePostgresStatus)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.postgresId, {
|
||||
@@ -294,6 +336,12 @@ export const postgresRouter = createTRPCRouter({
|
||||
return postgres;
|
||||
}),
|
||||
remove: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Delete a PostgreSQL database",
|
||||
description: "Removes the PostgreSQL database service, its Docker container, cancels associated backup jobs, and deletes the database record.",
|
||||
},
|
||||
})
|
||||
.input(apiFindOnePostgres)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServiceAccess(ctx, input.postgresId, "delete");
|
||||
@@ -332,6 +380,12 @@ export const postgresRouter = createTRPCRouter({
|
||||
return postgres;
|
||||
}),
|
||||
saveEnvironment: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Save environment variables for a PostgreSQL database",
|
||||
description: "Updates the environment variables for the specified PostgreSQL database service.",
|
||||
},
|
||||
})
|
||||
.input(apiSaveEnvironmentVariablesPostgres)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.postgresId, {
|
||||
@@ -356,6 +410,12 @@ export const postgresRouter = createTRPCRouter({
|
||||
return true;
|
||||
}),
|
||||
reload: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Reload a PostgreSQL database",
|
||||
description: "Restarts the PostgreSQL database by stopping and then starting its Docker container.",
|
||||
},
|
||||
})
|
||||
.input(apiResetPostgres)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.postgresId, {
|
||||
@@ -388,6 +448,12 @@ export const postgresRouter = createTRPCRouter({
|
||||
return true;
|
||||
}),
|
||||
update: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Update a PostgreSQL database",
|
||||
description: "Updates the configuration of an existing PostgreSQL database service.",
|
||||
},
|
||||
})
|
||||
.input(apiUpdatePostgres)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const { postgresId, ...rest } = input;
|
||||
@@ -415,6 +481,12 @@ export const postgresRouter = createTRPCRouter({
|
||||
return true;
|
||||
}),
|
||||
changePassword: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Change PostgreSQL database password",
|
||||
description: "Changes the password for the PostgreSQL database user by executing ALTER USER inside the running container and updating the stored password.",
|
||||
},
|
||||
})
|
||||
.input(
|
||||
z.object({
|
||||
postgresId: z.string().min(1),
|
||||
@@ -465,6 +537,12 @@ export const postgresRouter = createTRPCRouter({
|
||||
return true;
|
||||
}),
|
||||
move: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Move a PostgreSQL database to another environment",
|
||||
description: "Moves the PostgreSQL database to a different environment within the same project.",
|
||||
},
|
||||
})
|
||||
.input(
|
||||
z.object({
|
||||
postgresId: z.string(),
|
||||
@@ -501,6 +579,12 @@ export const postgresRouter = createTRPCRouter({
|
||||
return updatedPostgres;
|
||||
}),
|
||||
rebuild: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Rebuild a PostgreSQL database",
|
||||
description: "Rebuilds the PostgreSQL database Docker container from scratch, pulling the latest image and recreating the service.",
|
||||
},
|
||||
})
|
||||
.input(apiRebuildPostgres)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.postgresId, {
|
||||
@@ -517,6 +601,12 @@ export const postgresRouter = createTRPCRouter({
|
||||
return true;
|
||||
}),
|
||||
search: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Search PostgreSQL databases",
|
||||
description: "Returns a paginated list of PostgreSQL databases matching the given filters. Supports searching by name, appName, description, project, and environment.",
|
||||
},
|
||||
})
|
||||
.input(
|
||||
z.object({
|
||||
q: z.string().optional(),
|
||||
@@ -617,6 +707,12 @@ export const postgresRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
readLogs: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Read PostgreSQL container logs",
|
||||
description: "Retrieves the Docker container logs for the specified PostgreSQL database, with support for tail count, time-based filtering, and text search.",
|
||||
},
|
||||
})
|
||||
.input(
|
||||
apiFindOnePostgres.extend({
|
||||
tail: z.number().int().min(1).max(10000).default(100),
|
||||
|
||||
@@ -16,6 +16,12 @@ import { createTRPCRouter, protectedProcedure } from "../trpc";
|
||||
|
||||
export const previewDeploymentRouter = createTRPCRouter({
|
||||
all: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "List preview deployments",
|
||||
description: "Returns all preview deployments associated with the given application.",
|
||||
},
|
||||
})
|
||||
.input(apiFindAllByApplication)
|
||||
.query(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.applicationId, {
|
||||
@@ -25,6 +31,12 @@ export const previewDeploymentRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
one: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Get a preview deployment",
|
||||
description: "Returns the details of a specific preview deployment by its ID.",
|
||||
},
|
||||
})
|
||||
.input(z.object({ previewDeploymentId: z.string() }))
|
||||
.query(async ({ input, ctx }) => {
|
||||
const previewDeployment = await findPreviewDeploymentById(
|
||||
@@ -39,6 +51,12 @@ export const previewDeploymentRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
delete: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Delete a preview deployment",
|
||||
description: "Permanently removes a preview deployment and its associated resources.",
|
||||
},
|
||||
})
|
||||
.input(z.object({ previewDeploymentId: z.string() }))
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const previewDeployment = await findPreviewDeploymentById(
|
||||
@@ -59,6 +77,12 @@ export const previewDeploymentRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
redeploy: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Redeploy a preview deployment",
|
||||
description: "Triggers a rebuild of an existing preview deployment by adding a new job to the deployment queue.",
|
||||
},
|
||||
})
|
||||
.input(
|
||||
z.object({
|
||||
previewDeploymentId: z.string(),
|
||||
|
||||
@@ -67,6 +67,12 @@ import {
|
||||
|
||||
export const projectRouter = createTRPCRouter({
|
||||
create: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Create a project",
|
||||
description: "Creates a new project in the current organization with a default environment. Validates server availability for cloud deployments.",
|
||||
},
|
||||
})
|
||||
.input(apiCreateProject)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
try {
|
||||
@@ -106,6 +112,12 @@ export const projectRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
one: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Get a project",
|
||||
description: "Retrieves a project by its ID with all environments and services. Filters services based on the user's access permissions.",
|
||||
},
|
||||
})
|
||||
.input(apiFindOneProject)
|
||||
.query(async ({ input, ctx }) => {
|
||||
if (ctx.user.role !== "owner" && ctx.user.role !== "admin") {
|
||||
@@ -193,7 +205,14 @@ export const projectRouter = createTRPCRouter({
|
||||
}
|
||||
return project;
|
||||
}),
|
||||
all: protectedProcedure.query(async ({ ctx }) => {
|
||||
all: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "List all projects",
|
||||
description: "Returns all projects in the current organization with their environments and services. Filters results based on the user's access permissions.",
|
||||
},
|
||||
})
|
||||
.query(async ({ ctx }) => {
|
||||
if (ctx.user.role !== "owner" && ctx.user.role !== "admin") {
|
||||
const { accessedProjects, accessedEnvironments, accessedServices } =
|
||||
await findMemberByUserId(ctx.user.id, ctx.session.activeOrganizationId);
|
||||
@@ -375,7 +394,14 @@ export const projectRouter = createTRPCRouter({
|
||||
});
|
||||
}),
|
||||
|
||||
allForPermissions: withPermission("member", "update").query(
|
||||
allForPermissions: withPermission("member", "update")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "List all projects for permissions",
|
||||
description: "Returns all projects with their environments and services for the permissions management UI. Requires member update permission.",
|
||||
},
|
||||
})
|
||||
.query(
|
||||
async ({ ctx }) => {
|
||||
return await db.query.projects.findMany({
|
||||
where: eq(projects.organizationId, ctx.session.activeOrganizationId),
|
||||
@@ -488,6 +514,12 @@ export const projectRouter = createTRPCRouter({
|
||||
),
|
||||
|
||||
search: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Search projects",
|
||||
description: "Searches projects by name or description with pagination. Respects project-level access control for non-admin users.",
|
||||
},
|
||||
})
|
||||
.input(
|
||||
z.object({
|
||||
q: z.string().optional(),
|
||||
@@ -565,6 +597,12 @@ export const projectRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
remove: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Delete a project",
|
||||
description: "Permanently deletes a project and all its associated environments, services, and resources.",
|
||||
},
|
||||
})
|
||||
.input(apiRemoveProject)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
@@ -592,6 +630,12 @@ export const projectRouter = createTRPCRouter({
|
||||
}
|
||||
}),
|
||||
update: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Update a project",
|
||||
description: "Updates a project's name, description, or environment variables. Validates organization ownership and project-level access permissions.",
|
||||
},
|
||||
})
|
||||
.input(apiUpdateProject)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
@@ -640,6 +684,12 @@ export const projectRouter = createTRPCRouter({
|
||||
}
|
||||
}),
|
||||
duplicate: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Duplicate a project or environment",
|
||||
description: "Duplicates services from a source environment into a new project or into the same project. Copies applications, compose services, databases, and their associated domains, mounts, ports, and backups.",
|
||||
},
|
||||
})
|
||||
.input(
|
||||
z.object({
|
||||
sourceEnvironmentId: z.string(),
|
||||
|
||||
@@ -15,6 +15,12 @@ import {
|
||||
|
||||
export const redirectsRouter = createTRPCRouter({
|
||||
create: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Create a redirect",
|
||||
description: "Creates a new redirect rule for an application using a regex pattern. Logs an audit entry.",
|
||||
},
|
||||
})
|
||||
.input(apiCreateRedirect)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.applicationId, {
|
||||
@@ -31,6 +37,12 @@ export const redirectsRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
one: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Get a redirect",
|
||||
description: "Returns a single redirect rule by its ID.",
|
||||
},
|
||||
})
|
||||
.input(apiFindOneRedirect)
|
||||
.query(async ({ input, ctx }) => {
|
||||
const redirect = await findRedirectById(input.redirectId);
|
||||
@@ -41,6 +53,12 @@ export const redirectsRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
delete: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Delete a redirect",
|
||||
description: "Deletes a redirect rule by its ID and logs an audit entry.",
|
||||
},
|
||||
})
|
||||
.input(apiFindOneRedirect)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const redirect = await findRedirectById(input.redirectId);
|
||||
@@ -57,6 +75,12 @@ export const redirectsRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
update: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Update a redirect",
|
||||
description: "Updates an existing redirect rule's configuration and logs an audit entry.",
|
||||
},
|
||||
})
|
||||
.input(apiUpdateRedirect)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const redirect = await findRedirectById(input.redirectId);
|
||||
|
||||
@@ -51,6 +51,12 @@ import {
|
||||
} from "@/server/db/schema";
|
||||
export const redisRouter = createTRPCRouter({
|
||||
create: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Create a Redis database",
|
||||
description: "Creates a new Redis database service with the specified configuration, sets up a persistent data volume, and registers it in the project.",
|
||||
},
|
||||
})
|
||||
.input(apiCreateRedis)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
@@ -108,6 +114,12 @@ export const redisRouter = createTRPCRouter({
|
||||
}
|
||||
}),
|
||||
one: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Get a Redis database by ID",
|
||||
description: "Returns the full details of a Redis database service, including its environment and project configuration.",
|
||||
},
|
||||
})
|
||||
.input(apiFindOneRedis)
|
||||
.query(async ({ input, ctx }) => {
|
||||
await checkServiceAccess(ctx, input.redisId, "read");
|
||||
@@ -126,6 +138,12 @@ export const redisRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
start: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Start a Redis database",
|
||||
description: "Starts the Docker container for the specified Redis database and sets its status to done.",
|
||||
},
|
||||
})
|
||||
.input(apiFindOneRedis)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.redisId, {
|
||||
@@ -151,6 +169,12 @@ export const redisRouter = createTRPCRouter({
|
||||
return redis;
|
||||
}),
|
||||
reload: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Reload a Redis database",
|
||||
description: "Restarts the Redis database by stopping and then starting its Docker container.",
|
||||
},
|
||||
})
|
||||
.input(apiResetRedis)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.redisId, {
|
||||
@@ -184,6 +208,12 @@ export const redisRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
stop: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Stop a Redis database",
|
||||
description: "Stops the Docker container for the specified Redis database and sets its status to idle.",
|
||||
},
|
||||
})
|
||||
.input(apiFindOneRedis)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.redisId, {
|
||||
@@ -208,6 +238,12 @@ export const redisRouter = createTRPCRouter({
|
||||
return redis;
|
||||
}),
|
||||
saveExternalPort: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Save the external port for a Redis database",
|
||||
description: "Updates the external port mapping for the Redis database and triggers a redeployment. Validates that the port is not already in use.",
|
||||
},
|
||||
})
|
||||
.input(apiSaveExternalPortRedis)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.redisId, {
|
||||
@@ -241,6 +277,12 @@ export const redisRouter = createTRPCRouter({
|
||||
return redis;
|
||||
}),
|
||||
deploy: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Deploy a Redis database",
|
||||
description: "Triggers a deployment for the specified Redis database, rebuilding and restarting its Docker container with the current configuration.",
|
||||
},
|
||||
})
|
||||
.input(apiDeployRedis)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.redisId, {
|
||||
@@ -293,6 +335,12 @@ export const redisRouter = createTRPCRouter({
|
||||
}
|
||||
}),
|
||||
changeStatus: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Change Redis database status",
|
||||
description: "Updates the application status of a Redis database without starting or stopping the container.",
|
||||
},
|
||||
})
|
||||
.input(apiChangeRedisStatus)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.redisId, {
|
||||
@@ -311,6 +359,12 @@ export const redisRouter = createTRPCRouter({
|
||||
return mongo;
|
||||
}),
|
||||
remove: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Delete a Redis database",
|
||||
description: "Removes the Redis database service, its Docker container, and deletes the database record.",
|
||||
},
|
||||
})
|
||||
.input(apiFindOneRedis)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServiceAccess(ctx, input.redisId, "delete");
|
||||
@@ -346,6 +400,12 @@ export const redisRouter = createTRPCRouter({
|
||||
return redis;
|
||||
}),
|
||||
saveEnvironment: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Save environment variables for a Redis database",
|
||||
description: "Updates the environment variables for the specified Redis database service.",
|
||||
},
|
||||
})
|
||||
.input(apiSaveEnvironmentVariablesRedis)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.redisId, {
|
||||
@@ -370,6 +430,12 @@ export const redisRouter = createTRPCRouter({
|
||||
return true;
|
||||
}),
|
||||
update: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Update a Redis database",
|
||||
description: "Updates the configuration of an existing Redis database service.",
|
||||
},
|
||||
})
|
||||
.input(apiUpdateRedis)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const { redisId, ...rest } = input;
|
||||
@@ -396,6 +462,12 @@ export const redisRouter = createTRPCRouter({
|
||||
return true;
|
||||
}),
|
||||
changePassword: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Change Redis database password",
|
||||
description: "Changes the password for the Redis database by executing CONFIG SET requirepass inside the running container and updating the stored password.",
|
||||
},
|
||||
})
|
||||
.input(
|
||||
z.object({
|
||||
redisId: z.string().min(1),
|
||||
@@ -446,6 +518,12 @@ export const redisRouter = createTRPCRouter({
|
||||
return true;
|
||||
}),
|
||||
move: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Move a Redis database to another environment",
|
||||
description: "Moves the Redis database to a different environment within the same project.",
|
||||
},
|
||||
})
|
||||
.input(
|
||||
z.object({
|
||||
redisId: z.string(),
|
||||
@@ -482,6 +560,12 @@ export const redisRouter = createTRPCRouter({
|
||||
return updatedRedis;
|
||||
}),
|
||||
rebuild: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Rebuild a Redis database",
|
||||
description: "Rebuilds the Redis database Docker container from scratch, pulling the latest image and recreating the service.",
|
||||
},
|
||||
})
|
||||
.input(apiRebuildRedis)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.redisId, {
|
||||
@@ -497,6 +581,12 @@ export const redisRouter = createTRPCRouter({
|
||||
return true;
|
||||
}),
|
||||
search: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Search Redis databases",
|
||||
description: "Returns a paginated list of Redis databases matching the given filters. Supports searching by name, appName, description, project, and environment.",
|
||||
},
|
||||
})
|
||||
.input(
|
||||
z.object({
|
||||
q: z.string().optional(),
|
||||
@@ -590,6 +680,12 @@ export const redisRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
readLogs: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Read Redis container logs",
|
||||
description: "Retrieves the Docker container logs for the specified Redis database, with support for tail count, time-based filtering, and text search.",
|
||||
},
|
||||
})
|
||||
.input(
|
||||
apiFindOneRedis.extend({
|
||||
tail: z.number().int().min(1).max(10000).default(100),
|
||||
|
||||
@@ -23,6 +23,12 @@ import {
|
||||
import { createTRPCRouter, withPermission } from "../trpc";
|
||||
export const registryRouter = createTRPCRouter({
|
||||
create: withPermission("registry", "create")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Create registry",
|
||||
description: "Creates a new Docker registry entry for the current organization and logs an audit event.",
|
||||
},
|
||||
})
|
||||
.input(apiCreateRegistry)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const reg = await createRegistry(input, ctx.session.activeOrganizationId);
|
||||
@@ -35,6 +41,12 @@ export const registryRouter = createTRPCRouter({
|
||||
return reg;
|
||||
}),
|
||||
remove: withPermission("registry", "delete")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Delete registry",
|
||||
description: "Removes a Docker registry entry by ID. Verifies organization ownership and logs an audit event before deletion.",
|
||||
},
|
||||
})
|
||||
.input(apiRemoveRegistry)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const registry = await findRegistryById(input.registryId);
|
||||
@@ -53,6 +65,12 @@ export const registryRouter = createTRPCRouter({
|
||||
return await removeRegistry(input.registryId);
|
||||
}),
|
||||
update: withPermission("registry", "create")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Update registry",
|
||||
description: "Updates an existing Docker registry entry. Verifies organization ownership before applying changes and logs an audit event.",
|
||||
},
|
||||
})
|
||||
.input(apiUpdateRegistry)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const { registryId, ...rest } = input;
|
||||
@@ -82,13 +100,26 @@ export const registryRouter = createTRPCRouter({
|
||||
});
|
||||
return true;
|
||||
}),
|
||||
all: withPermission("registry", "read").query(async ({ ctx }) => {
|
||||
all: withPermission("registry", "read")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "List all registries",
|
||||
description: "Returns all Docker registry entries for the current organization.",
|
||||
},
|
||||
})
|
||||
.query(async ({ ctx }) => {
|
||||
const registryResponse = await db.query.registry.findMany({
|
||||
where: eq(registry.organizationId, ctx.session.activeOrganizationId),
|
||||
});
|
||||
return registryResponse;
|
||||
}),
|
||||
one: withPermission("registry", "read")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Get registry",
|
||||
description: "Returns a single Docker registry entry by ID. Verifies the caller belongs to the same organization.",
|
||||
},
|
||||
})
|
||||
.input(apiFindOneRegistry)
|
||||
.query(async ({ input, ctx }) => {
|
||||
const registry = await findRegistryById(input.registryId);
|
||||
@@ -101,6 +132,12 @@ export const registryRouter = createTRPCRouter({
|
||||
return registry;
|
||||
}),
|
||||
testRegistry: withPermission("registry", "read")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Test registry credentials",
|
||||
description: "Attempts a docker login with the provided credentials to verify the registry URL, username, and password are valid. Can run locally or on a remote server.",
|
||||
},
|
||||
})
|
||||
.input(apiTestRegistry)
|
||||
.mutation(async ({ input }) => {
|
||||
try {
|
||||
@@ -143,6 +180,12 @@ export const registryRouter = createTRPCRouter({
|
||||
}
|
||||
}),
|
||||
testRegistryById: withPermission("registry", "read")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Test registry connection by ID",
|
||||
description: "Looks up a saved registry by ID and attempts a docker login with its stored credentials. Verifies organization ownership before testing.",
|
||||
},
|
||||
})
|
||||
.input(apiTestRegistryById)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
|
||||
@@ -11,6 +11,12 @@ import { createTRPCRouter, protectedProcedure } from "../trpc";
|
||||
|
||||
export const rollbackRouter = createTRPCRouter({
|
||||
delete: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Delete a rollback",
|
||||
description: "Permanently removes a rollback record by its ID.",
|
||||
},
|
||||
})
|
||||
.input(apiFindOneRollback)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
@@ -40,6 +46,12 @@ export const rollbackRouter = createTRPCRouter({
|
||||
}
|
||||
}),
|
||||
rollback: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Perform a rollback",
|
||||
description: "Rolls back an application to a previous deployment by restoring its Docker image and redeploying.",
|
||||
},
|
||||
})
|
||||
.input(apiFindOneRollback)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
|
||||
@@ -22,6 +22,12 @@ import { removeJob, schedule } from "@/server/utils/backup";
|
||||
import { createTRPCRouter, protectedProcedure } from "../trpc";
|
||||
export const scheduleRouter = createTRPCRouter({
|
||||
create: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Create a scheduled job",
|
||||
description: "Creates a new scheduled job for an application or compose service. If enabled, the job is automatically scheduled using the provided cron expression and timezone.",
|
||||
},
|
||||
})
|
||||
.input(createScheduleSchema)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const serviceId = input.applicationId || input.composeId;
|
||||
@@ -54,6 +60,12 @@ export const scheduleRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
update: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Update a scheduled job",
|
||||
description: "Updates an existing scheduled job configuration. Reschedules or removes the job depending on the enabled state.",
|
||||
},
|
||||
})
|
||||
.input(updateScheduleSchema)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const existingSchedule = await findScheduleById(input.scheduleId);
|
||||
@@ -99,6 +111,12 @@ export const scheduleRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
delete: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Delete a scheduled job",
|
||||
description: "Permanently removes a scheduled job and unschedules any associated cron job.",
|
||||
},
|
||||
})
|
||||
.input(z.object({ scheduleId: z.string() }))
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const scheduleItem = await findScheduleById(input.scheduleId);
|
||||
@@ -129,6 +147,12 @@ export const scheduleRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
list: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "List scheduled jobs",
|
||||
description: "Returns all scheduled jobs for a given service (application, compose, server, or dokploy-server), including their deployment history.",
|
||||
},
|
||||
})
|
||||
.input(
|
||||
z.object({
|
||||
id: z.string(),
|
||||
@@ -170,6 +194,12 @@ export const scheduleRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
one: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Get a scheduled job",
|
||||
description: "Returns the details of a specific scheduled job by its ID.",
|
||||
},
|
||||
})
|
||||
.input(z.object({ scheduleId: z.string() }))
|
||||
.query(async ({ input, ctx }) => {
|
||||
const schedule = await findScheduleById(input.scheduleId);
|
||||
@@ -183,6 +213,12 @@ export const scheduleRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
runManually: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Run a scheduled job manually",
|
||||
description: "Immediately executes a scheduled job outside of its normal cron schedule.",
|
||||
},
|
||||
})
|
||||
.input(z.object({ scheduleId: z.string().min(1) }))
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const scheduleItem = await findScheduleById(input.scheduleId);
|
||||
|
||||
@@ -15,6 +15,12 @@ import {
|
||||
|
||||
export const securityRouter = createTRPCRouter({
|
||||
create: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Create a security entry",
|
||||
description: "Creates a new HTTP basic auth security entry for an application with the provided username and password. Logs an audit entry.",
|
||||
},
|
||||
})
|
||||
.input(apiCreateSecurity)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.applicationId, {
|
||||
@@ -31,6 +37,12 @@ export const securityRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
one: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Get a security entry",
|
||||
description: "Returns a single HTTP basic auth security entry by its ID.",
|
||||
},
|
||||
})
|
||||
.input(apiFindOneSecurity)
|
||||
.query(async ({ input, ctx }) => {
|
||||
const security = await findSecurityById(input.securityId);
|
||||
@@ -41,6 +53,12 @@ export const securityRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
delete: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Delete a security entry",
|
||||
description: "Deletes an HTTP basic auth security entry by its ID and logs an audit entry.",
|
||||
},
|
||||
})
|
||||
.input(apiFindOneSecurity)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const security = await findSecurityById(input.securityId);
|
||||
@@ -57,6 +75,12 @@ export const securityRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
update: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Update a security entry",
|
||||
description: "Updates an existing HTTP basic auth security entry's configuration and logs an audit entry.",
|
||||
},
|
||||
})
|
||||
.input(apiUpdateSecurity)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const security = await findSecurityById(input.securityId);
|
||||
|
||||
@@ -48,6 +48,12 @@ import {
|
||||
|
||||
export const serverRouter = createTRPCRouter({
|
||||
create: withPermission("server", "create")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Create a server",
|
||||
description: "Creates a new server in the organization. In cloud mode, enforces the user's server quantity limit. Returns the newly created server.",
|
||||
},
|
||||
})
|
||||
.input(apiCreateServer)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
try {
|
||||
@@ -80,6 +86,12 @@ export const serverRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
one: withPermission("server", "read")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Get a server",
|
||||
description: "Retrieves a single server by its ID. Validates that the user has access to the server within their organization.",
|
||||
},
|
||||
})
|
||||
.input(apiFindOneServer)
|
||||
.query(async ({ input, ctx }) => {
|
||||
const server = await findServerById(input.serverId);
|
||||
@@ -101,13 +113,26 @@ export const serverRouter = createTRPCRouter({
|
||||
return server;
|
||||
}),
|
||||
getDefaultCommand: withPermission("server", "read")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Get default server command",
|
||||
description: "Returns the default setup command for a server. The command varies depending on whether the server is a build server or a deploy server.",
|
||||
},
|
||||
})
|
||||
.input(apiFindOneServer)
|
||||
.query(async ({ input }) => {
|
||||
const server = await findServerById(input.serverId);
|
||||
const isBuildServer = server.serverType === "build";
|
||||
return defaultCommand(isBuildServer);
|
||||
}),
|
||||
all: withPermission("server", "read").query(async ({ ctx }) => {
|
||||
all: withPermission("server", "read")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "List all servers",
|
||||
description: "Returns all servers in the current organization along with a count of associated services (applications, compose, databases). Results are filtered by the user's accessible server permissions.",
|
||||
},
|
||||
})
|
||||
.query(async ({ ctx }) => {
|
||||
const accessibleIds = await getAccessibleServerIds(ctx.session);
|
||||
|
||||
const result = await db
|
||||
@@ -130,6 +155,12 @@ export const serverRouter = createTRPCRouter({
|
||||
return result.filter((s) => accessibleIds.has(s.serverId));
|
||||
}),
|
||||
allForPermissions: withPermission("member", "update")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "List all servers for permissions",
|
||||
description: "Returns a minimal list of servers (ID, name, IP, type) used for configuring member permissions. Requires a valid enterprise license.",
|
||||
},
|
||||
})
|
||||
.use(async ({ ctx, next }) => {
|
||||
const licensed = await hasValidLicense(ctx.session.activeOrganizationId);
|
||||
if (!licensed) {
|
||||
@@ -152,7 +183,14 @@ export const serverRouter = createTRPCRouter({
|
||||
where: eq(server.organizationId, ctx.session.activeOrganizationId),
|
||||
});
|
||||
}),
|
||||
count: protectedProcedure.query(async ({ ctx }) => {
|
||||
count: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Get server count",
|
||||
description: "Returns the total number of servers across all organizations owned by the current user.",
|
||||
},
|
||||
})
|
||||
.query(async ({ ctx }) => {
|
||||
const organizations = await db.query.organization.findMany({
|
||||
where: eq(organization.ownerId, ctx.user.id),
|
||||
with: {
|
||||
@@ -164,7 +202,14 @@ export const serverRouter = createTRPCRouter({
|
||||
|
||||
return servers.length ?? 0;
|
||||
}),
|
||||
withSSHKey: withPermission("server", "read").query(async ({ ctx }) => {
|
||||
withSSHKey: withPermission("server", "read")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "List servers with SSH keys",
|
||||
description: "Returns all deploy-type servers that have an SSH key configured. In cloud mode, only active servers are included. Results are filtered by the user's accessible server permissions.",
|
||||
},
|
||||
})
|
||||
.query(async ({ ctx }) => {
|
||||
const accessibleIds = await getAccessibleServerIds(ctx.session);
|
||||
|
||||
const result = await db.query.server.findMany({
|
||||
@@ -184,7 +229,14 @@ export const serverRouter = createTRPCRouter({
|
||||
});
|
||||
return result.filter((s) => accessibleIds.has(s.serverId));
|
||||
}),
|
||||
buildServers: withPermission("server", "read").query(async ({ ctx }) => {
|
||||
buildServers: withPermission("server", "read")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "List build servers",
|
||||
description: "Returns all build-type servers that have an SSH key configured. In cloud mode, only active servers are included. Results are filtered by the user's accessible server permissions.",
|
||||
},
|
||||
})
|
||||
.query(async ({ ctx }) => {
|
||||
const accessibleIds = await getAccessibleServerIds(ctx.session);
|
||||
|
||||
const result = await db.query.server.findMany({
|
||||
@@ -205,6 +257,12 @@ export const serverRouter = createTRPCRouter({
|
||||
return result.filter((s) => accessibleIds.has(s.serverId));
|
||||
}),
|
||||
setup: withPermission("server", "create")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Setup a server",
|
||||
description: "Runs the initial setup process on a remote server, installing required dependencies and configuring Docker. An audit log entry is created.",
|
||||
},
|
||||
})
|
||||
.input(apiFindOneServer)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
@@ -256,6 +314,12 @@ export const serverRouter = createTRPCRouter({
|
||||
}
|
||||
}),
|
||||
validate: withPermission("server", "read")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Validate server configuration",
|
||||
description: "Checks the server for required tools and configuration including Docker, Rclone, Nixpacks, Buildpacks, Railpack, Swarm mode, network setup, and privilege mode.",
|
||||
},
|
||||
})
|
||||
.input(apiFindOneServer)
|
||||
.query(async ({ input, ctx }) => {
|
||||
try {
|
||||
@@ -304,6 +368,12 @@ export const serverRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
security: withPermission("server", "read")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Get server security audit",
|
||||
description: "Performs a security audit on the server, checking UFW firewall, SSH configuration, non-root user setup, unattended upgrades, and Fail2Ban status.",
|
||||
},
|
||||
})
|
||||
.input(apiFindOneServer)
|
||||
.query(async ({ input, ctx }) => {
|
||||
try {
|
||||
@@ -354,6 +424,12 @@ export const serverRouter = createTRPCRouter({
|
||||
}
|
||||
}),
|
||||
setupMonitoring: withPermission("server", "create")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Setup server monitoring",
|
||||
description: "Configures and deploys the monitoring agent on a server with the specified metrics configuration including refresh rates, retention, thresholds, and container service filters.",
|
||||
},
|
||||
})
|
||||
.input(apiUpdateServerMonitoring)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
@@ -402,6 +478,12 @@ export const serverRouter = createTRPCRouter({
|
||||
}
|
||||
}),
|
||||
remove: withPermission("server", "delete")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Remove a server",
|
||||
description: "Deletes a server and removes all associated deployments. Fails if the server has active services. In cloud mode, updates the user's server quantity allocation.",
|
||||
},
|
||||
})
|
||||
.input(apiRemoveServer)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
@@ -435,6 +517,12 @@ export const serverRouter = createTRPCRouter({
|
||||
}
|
||||
}),
|
||||
update: withPermission("server", "create")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Update a server",
|
||||
description: "Updates the configuration of an existing server. Fails if the server is inactive. An audit log entry is created for the update.",
|
||||
},
|
||||
})
|
||||
.input(apiUpdateServer)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
@@ -467,14 +555,28 @@ export const serverRouter = createTRPCRouter({
|
||||
throw error;
|
||||
}
|
||||
}),
|
||||
publicIp: protectedProcedure.query(async () => {
|
||||
publicIp: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Get public IP address",
|
||||
description: "Returns the public IP address of the local server. Returns an empty string in cloud mode.",
|
||||
},
|
||||
})
|
||||
.query(async () => {
|
||||
if (IS_CLOUD) {
|
||||
return "";
|
||||
}
|
||||
const ip = await getPublicIpWithFallback();
|
||||
return ip;
|
||||
}),
|
||||
getServerTime: protectedProcedure.query(() => {
|
||||
getServerTime: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Get server time",
|
||||
description: "Returns the current server time and timezone. Returns null in cloud mode.",
|
||||
},
|
||||
})
|
||||
.query(() => {
|
||||
if (IS_CLOUD) {
|
||||
return null;
|
||||
}
|
||||
@@ -484,6 +586,12 @@ export const serverRouter = createTRPCRouter({
|
||||
};
|
||||
}),
|
||||
getServerMetrics: withPermission("monitoring", "read")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Get server metrics",
|
||||
description: "Fetches monitoring metrics (CPU, memory, disk, network) from the server's monitoring agent endpoint. Requires the monitoring service to be configured and running.",
|
||||
},
|
||||
})
|
||||
.input(
|
||||
z.object({
|
||||
url: z.string(),
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -25,6 +25,12 @@ import {
|
||||
|
||||
export const sshRouter = createTRPCRouter({
|
||||
create: withPermission("sshKeys", "create")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Create SSH key",
|
||||
description: "Stores a new SSH key for the current organization and logs an audit event.",
|
||||
},
|
||||
})
|
||||
.input(apiCreateSshKey)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
@@ -46,6 +52,12 @@ export const sshRouter = createTRPCRouter({
|
||||
}
|
||||
}),
|
||||
remove: withPermission("sshKeys", "delete")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Delete SSH key",
|
||||
description: "Removes an SSH key by ID. Verifies organization ownership and logs an audit event before deletion.",
|
||||
},
|
||||
})
|
||||
.input(apiRemoveSshKey)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
@@ -69,6 +81,12 @@ export const sshRouter = createTRPCRouter({
|
||||
}
|
||||
}),
|
||||
one: withPermission("sshKeys", "read")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Get SSH key",
|
||||
description: "Returns a single SSH key by ID. Verifies the caller belongs to the same organization.",
|
||||
},
|
||||
})
|
||||
.input(apiFindOneSshKey)
|
||||
.query(async ({ input, ctx }) => {
|
||||
const sshKey = await findSSHKeyById(input.sshKeyId);
|
||||
@@ -81,13 +99,27 @@ export const sshRouter = createTRPCRouter({
|
||||
}
|
||||
return sshKey;
|
||||
}),
|
||||
all: withPermission("sshKeys", "read").query(async ({ ctx }) => {
|
||||
all: withPermission("sshKeys", "read")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "List all SSH keys",
|
||||
description: "Returns all SSH keys for the current organization, ordered by creation date descending.",
|
||||
},
|
||||
})
|
||||
.query(async ({ ctx }) => {
|
||||
return await db.query.sshKeys.findMany({
|
||||
where: eq(sshKeys.organizationId, ctx.session.activeOrganizationId),
|
||||
orderBy: desc(sshKeys.createdAt),
|
||||
});
|
||||
}),
|
||||
allForApps: protectedProcedure.query(async ({ ctx }) => {
|
||||
allForApps: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "List SSH keys for app selection",
|
||||
description: "Returns a lightweight list of SSH keys (ID and name only) for the current organization, suitable for dropdown selectors in application forms.",
|
||||
},
|
||||
})
|
||||
.query(async ({ ctx }) => {
|
||||
return await db.query.sshKeys.findMany({
|
||||
columns: {
|
||||
sshKeyId: true,
|
||||
@@ -98,11 +130,23 @@ export const sshRouter = createTRPCRouter({
|
||||
});
|
||||
}),
|
||||
generate: withPermission("sshKeys", "read")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Generate SSH key pair",
|
||||
description: "Generates a new SSH key pair of the specified type (RSA, ED25519, etc.) and returns both public and private keys.",
|
||||
},
|
||||
})
|
||||
.input(apiGenerateSSHKey)
|
||||
.mutation(async ({ input }) => {
|
||||
return await generateSSHKey(input.type);
|
||||
}),
|
||||
update: withPermission("sshKeys", "create")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Update SSH key",
|
||||
description: "Updates an existing SSH key. Verifies organization ownership before applying changes and logs an audit event.",
|
||||
},
|
||||
})
|
||||
.input(apiUpdateSshKey)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
|
||||
@@ -30,7 +30,14 @@ import {
|
||||
|
||||
export const stripeRouter = createTRPCRouter({
|
||||
/** Returns the current billing plan for the user's organization. Used to gate features like chat (Startup only). */
|
||||
getCurrentPlan: protectedProcedure.query(async ({ ctx }) => {
|
||||
getCurrentPlan: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Get current billing plan",
|
||||
description: "Returns the active Stripe billing plan (hobby, startup, or legacy) for the caller's organization owner. Returns null if not on cloud or no subscription exists.",
|
||||
},
|
||||
})
|
||||
.query(async ({ ctx }) => {
|
||||
if (!IS_CLOUD) return null;
|
||||
const owner = await findUserById(ctx.user.ownerId);
|
||||
if (!owner?.stripeCustomerId) return null;
|
||||
@@ -71,7 +78,14 @@ export const stripeRouter = createTRPCRouter({
|
||||
return null;
|
||||
}),
|
||||
|
||||
getProducts: adminProcedure.query(async ({ ctx }) => {
|
||||
getProducts: adminProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "List Stripe products and subscriptions",
|
||||
description: "Returns available Stripe products, the user's active subscriptions, current plan tier, billing interval, and price amount.",
|
||||
},
|
||||
})
|
||||
.query(async ({ ctx }) => {
|
||||
const user = await findUserById(ctx.user.ownerId);
|
||||
const stripeCustomerId = user.stripeCustomerId;
|
||||
|
||||
@@ -162,6 +176,12 @@ export const stripeRouter = createTRPCRouter({
|
||||
};
|
||||
}),
|
||||
createCheckoutSession: adminProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Create Stripe checkout session",
|
||||
description: "Creates a Stripe checkout session for subscribing to a billing plan. Handles customer creation or reuse and returns the session ID for redirect.",
|
||||
},
|
||||
})
|
||||
.input(
|
||||
z
|
||||
.object({
|
||||
@@ -222,7 +242,14 @@ export const stripeRouter = createTRPCRouter({
|
||||
|
||||
return { sessionId: session.id };
|
||||
}),
|
||||
createCustomerPortalSession: adminProcedure.mutation(async ({ ctx }) => {
|
||||
createCustomerPortalSession: adminProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Create Stripe customer portal session",
|
||||
description: "Creates a Stripe billing portal session URL so the user can manage their subscription, payment methods, and invoices.",
|
||||
},
|
||||
})
|
||||
.mutation(async ({ ctx }) => {
|
||||
// Use the organization's owner account for billing portal
|
||||
const owner = await findUserById(ctx.user.ownerId);
|
||||
|
||||
@@ -253,6 +280,12 @@ export const stripeRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
upgradeSubscription: adminProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Upgrade subscription",
|
||||
description: "Upgrades or changes the current Stripe subscription to a different tier or server quantity. Applies prorated charges for the billing period change.",
|
||||
},
|
||||
})
|
||||
.input(
|
||||
z
|
||||
.object({
|
||||
@@ -324,7 +357,14 @@ export const stripeRouter = createTRPCRouter({
|
||||
return { ok: true };
|
||||
}),
|
||||
|
||||
canCreateMoreServers: withPermission("server", "create").query(
|
||||
canCreateMoreServers: withPermission("server", "create")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Check server creation quota",
|
||||
description: "Returns whether the organization can create more servers based on their subscription's server quantity limit. Always returns true for self-hosted instances.",
|
||||
},
|
||||
})
|
||||
.query(
|
||||
async ({ ctx }) => {
|
||||
const user = await findUserById(ctx.user.ownerId);
|
||||
const servers = await findServersByUserId(user.id);
|
||||
@@ -338,6 +378,12 @@ export const stripeRouter = createTRPCRouter({
|
||||
),
|
||||
|
||||
updateInvoiceNotifications: adminProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Update invoice notification preference",
|
||||
description: "Enables or disables email notifications for invoice events. Only available on Dokploy Cloud.",
|
||||
},
|
||||
})
|
||||
.input(z.object({ enabled: z.boolean() }))
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
if (!IS_CLOUD) {
|
||||
@@ -353,7 +399,14 @@ export const stripeRouter = createTRPCRouter({
|
||||
return { ok: true };
|
||||
}),
|
||||
|
||||
getInvoices: adminProcedure.query(async ({ ctx }) => {
|
||||
getInvoices: adminProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "List invoices",
|
||||
description: "Returns up to 100 Stripe invoices for the organization owner, including status, amounts, and download links.",
|
||||
},
|
||||
})
|
||||
.query(async ({ ctx }) => {
|
||||
const user = await findUserById(ctx.user.ownerId);
|
||||
const stripeCustomerId = user.stripeCustomerId;
|
||||
|
||||
|
||||
@@ -13,6 +13,12 @@ import { containerIdRegex } from "./docker";
|
||||
|
||||
export const swarmRouter = createTRPCRouter({
|
||||
getNodes: withPermission("server", "read")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Get Swarm nodes",
|
||||
description: "Retrieves all nodes in the Docker Swarm. Optionally targets a remote server by ID.",
|
||||
},
|
||||
})
|
||||
.input(
|
||||
z.object({
|
||||
serverId: z.string().optional(),
|
||||
@@ -22,11 +28,23 @@ export const swarmRouter = createTRPCRouter({
|
||||
return await getSwarmNodes(input.serverId);
|
||||
}),
|
||||
getNodeInfo: withPermission("server", "read")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Get Swarm node info",
|
||||
description: "Retrieves detailed information about a specific Docker Swarm node by its node ID. Optionally targets a remote server.",
|
||||
},
|
||||
})
|
||||
.input(z.object({ nodeId: z.string(), serverId: z.string().optional() }))
|
||||
.query(async ({ input }) => {
|
||||
return await getNodeInfo(input.nodeId, input.serverId);
|
||||
}),
|
||||
getNodeApps: withPermission("server", "read")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Get Swarm node applications",
|
||||
description: "Retrieves all applications (services) running across Docker Swarm nodes. Optionally targets a remote server.",
|
||||
},
|
||||
})
|
||||
.input(
|
||||
z.object({
|
||||
serverId: z.string().optional(),
|
||||
@@ -58,6 +76,12 @@ export const swarmRouter = createTRPCRouter({
|
||||
return await getApplicationInfo(input.appName, input.serverId);
|
||||
}),
|
||||
getContainerStats: withPermission("server", "read")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Get container stats",
|
||||
description: "Retrieves resource usage statistics for all containers. Optionally targets a remote server and validates organization access.",
|
||||
},
|
||||
})
|
||||
.input(
|
||||
z.object({
|
||||
serverId: z.string().optional(),
|
||||
|
||||
@@ -16,6 +16,12 @@ import { createTRPCRouter, protectedProcedure, withPermission } from "../trpc";
|
||||
|
||||
export const tagRouter = createTRPCRouter({
|
||||
create: withPermission("tag", "create")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Create tag",
|
||||
description: "Creates a new tag with a name and color for the current organization. Tag names must be unique within the organization.",
|
||||
},
|
||||
})
|
||||
.input(apiCreateTag)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
@@ -47,7 +53,14 @@ export const tagRouter = createTRPCRouter({
|
||||
}
|
||||
}),
|
||||
|
||||
all: protectedProcedure.query(async ({ ctx }) => {
|
||||
all: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "List all tags",
|
||||
description: "Returns all tags for the current organization, ordered alphabetically by name.",
|
||||
},
|
||||
})
|
||||
.query(async ({ ctx }) => {
|
||||
try {
|
||||
const organizationTags = await db.query.tags.findMany({
|
||||
where: eq(tags.organizationId, ctx.session.activeOrganizationId),
|
||||
@@ -64,7 +77,15 @@ export const tagRouter = createTRPCRouter({
|
||||
}
|
||||
}),
|
||||
|
||||
one: protectedProcedure.input(apiFindOneTag).query(async ({ input, ctx }) => {
|
||||
one: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Get tag",
|
||||
description: "Returns a single tag by ID. Only returns tags belonging to the caller's organization.",
|
||||
},
|
||||
})
|
||||
.input(apiFindOneTag)
|
||||
.query(async ({ input, ctx }) => {
|
||||
try {
|
||||
const tag = await db.query.tags.findFirst({
|
||||
where: and(
|
||||
@@ -94,6 +115,12 @@ export const tagRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
update: withPermission("tag", "update")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Update tag",
|
||||
description: "Updates an existing tag's name and/or color. Verifies the tag belongs to the caller's organization. Tag names must remain unique.",
|
||||
},
|
||||
})
|
||||
.input(apiUpdateTag)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
@@ -144,6 +171,12 @@ export const tagRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
remove: withPermission("tag", "delete")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Delete tag",
|
||||
description: "Deletes a tag by ID. Cascade-deletes all project-tag associations. Verifies the tag belongs to the caller's organization.",
|
||||
},
|
||||
})
|
||||
.input(apiRemoveTag)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
@@ -179,6 +212,12 @@ export const tagRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
assignToProject: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Assign tag to project",
|
||||
description: "Associates a tag with a project. Verifies that both the tag and project belong to the caller's organization and that the caller has project access.",
|
||||
},
|
||||
})
|
||||
.input(
|
||||
z.object({
|
||||
projectId: z.string().min(1),
|
||||
@@ -267,6 +306,12 @@ export const tagRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
removeFromProject: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Remove tag from project",
|
||||
description: "Removes a tag-project association. Verifies that both the tag and project belong to the caller's organization and that the caller has project access.",
|
||||
},
|
||||
})
|
||||
.input(
|
||||
z.object({
|
||||
projectId: z.string().min(1),
|
||||
@@ -347,6 +392,12 @@ export const tagRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
bulkAssign: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Bulk assign tags to project",
|
||||
description: "Replaces all tag associations for a project with the provided list of tag IDs. Removes existing associations first, then inserts the new set.",
|
||||
},
|
||||
})
|
||||
.input(
|
||||
z.object({
|
||||
projectId: z.string().min(1),
|
||||
|
||||
@@ -60,7 +60,14 @@ const apiCreateApiKey = z.object({
|
||||
});
|
||||
|
||||
export const userRouter = createTRPCRouter({
|
||||
all: withPermission("member", "read").query(async ({ ctx }) => {
|
||||
all: withPermission("member", "read")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "List all organization members",
|
||||
description: "Retrieve all members of the current active organization, including their associated user data, ordered by creation date.",
|
||||
},
|
||||
})
|
||||
.query(async ({ ctx }) => {
|
||||
return await db.query.member.findMany({
|
||||
where: eq(member.organizationId, ctx.session.activeOrganizationId),
|
||||
with: {
|
||||
@@ -70,6 +77,12 @@ export const userRouter = createTRPCRouter({
|
||||
});
|
||||
}),
|
||||
one: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Get a user by ID",
|
||||
description: "Retrieve a specific user's membership and profile within the active organization. Users can view their own data; admins and owners can view any member. Requires member.update permission for non-self lookups.",
|
||||
},
|
||||
})
|
||||
.input(
|
||||
z.object({
|
||||
userId: z.string(),
|
||||
@@ -114,7 +127,14 @@ export const userRouter = createTRPCRouter({
|
||||
|
||||
return memberResult;
|
||||
}),
|
||||
session: publicProcedure.query(async ({ ctx }) => {
|
||||
session: publicProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Get current session",
|
||||
description: "Return the current user's ID and active organization ID from the session. Returns null if no valid session exists.",
|
||||
},
|
||||
})
|
||||
.query(async ({ ctx }) => {
|
||||
if (!ctx.user || !ctx.session || !ctx.session.activeOrganizationId) {
|
||||
return null;
|
||||
}
|
||||
@@ -127,7 +147,14 @@ export const userRouter = createTRPCRouter({
|
||||
},
|
||||
};
|
||||
}),
|
||||
get: protectedProcedure.query(async ({ ctx }) => {
|
||||
get: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Get current user profile",
|
||||
description: "Retrieve the current authenticated user's membership record including user profile and API keys for the active organization.",
|
||||
},
|
||||
})
|
||||
.query(async ({ ctx }) => {
|
||||
const memberResult = await db.query.member.findFirst({
|
||||
where: and(
|
||||
eq(member.userId, ctx.user.id),
|
||||
@@ -144,10 +171,24 @@ export const userRouter = createTRPCRouter({
|
||||
|
||||
return memberResult;
|
||||
}),
|
||||
getPermissions: protectedProcedure.query(async ({ ctx }) => {
|
||||
getPermissions: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Get resolved permissions",
|
||||
description: "Return the fully resolved permissions for the current user in the active organization, combining role-based and custom permissions.",
|
||||
},
|
||||
})
|
||||
.query(async ({ ctx }) => {
|
||||
return resolvePermissions(ctx);
|
||||
}),
|
||||
haveRootAccess: protectedProcedure.query(async ({ ctx }) => {
|
||||
haveRootAccess: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Check root access",
|
||||
description: "Check whether the current user has root admin access. Only returns true in cloud mode for the designated admin user or impersonating sessions.",
|
||||
},
|
||||
})
|
||||
.query(async ({ ctx }) => {
|
||||
if (!IS_CLOUD) {
|
||||
return false;
|
||||
}
|
||||
@@ -159,7 +200,14 @@ export const userRouter = createTRPCRouter({
|
||||
}
|
||||
return false;
|
||||
}),
|
||||
getBackups: adminProcedure.query(async ({ ctx }) => {
|
||||
getBackups: adminProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Get user backups",
|
||||
description: "Retrieve the current admin user's backup configurations including destinations, deployments, and API keys.",
|
||||
},
|
||||
})
|
||||
.query(async ({ ctx }) => {
|
||||
const memberResult = await db.query.member.findFirst({
|
||||
where: and(
|
||||
eq(member.userId, ctx.user.id),
|
||||
@@ -182,8 +230,14 @@ export const userRouter = createTRPCRouter({
|
||||
|
||||
return memberResult?.user;
|
||||
}),
|
||||
getServerMetrics: withPermission("monitoring", "read").query(
|
||||
async ({ ctx }) => {
|
||||
getServerMetrics: withPermission("monitoring", "read")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Get server metrics user",
|
||||
description: "Retrieve the user record associated with server metrics access for the current organization membership.",
|
||||
},
|
||||
})
|
||||
.query(async ({ ctx }) => {
|
||||
const memberResult = await db.query.member.findFirst({
|
||||
where: and(
|
||||
eq(member.userId, ctx.user.id),
|
||||
@@ -198,6 +252,12 @@ export const userRouter = createTRPCRouter({
|
||||
},
|
||||
),
|
||||
update: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Update current user",
|
||||
description: "Update the current user's profile. If changing the password, the current password must be provided and verified. Logs an audit event on success.",
|
||||
},
|
||||
})
|
||||
.input(apiUpdateUser)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
if (input.password || input.currentPassword) {
|
||||
@@ -248,12 +308,24 @@ export const userRouter = createTRPCRouter({
|
||||
}
|
||||
}),
|
||||
getUserByToken: publicProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Get user by token",
|
||||
description: "Look up a user by their authentication token. This is a public endpoint that does not require an active session.",
|
||||
},
|
||||
})
|
||||
.input(apiFindOneToken)
|
||||
.query(async ({ input }) => {
|
||||
return await getUserByToken(input.token);
|
||||
}),
|
||||
getMetricsToken: withPermission("monitoring", "read").query(
|
||||
async ({ ctx }) => {
|
||||
getMetricsToken: withPermission("monitoring", "read")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Get metrics token and configuration",
|
||||
description: "Retrieve the server IP, paid features flag, and monitoring configuration needed for metrics collection.",
|
||||
},
|
||||
})
|
||||
.query(async ({ ctx }) => {
|
||||
const user = await findUserById(ctx.user.ownerId);
|
||||
const settings = await getWebServerSettings();
|
||||
return {
|
||||
@@ -264,6 +336,12 @@ export const userRouter = createTRPCRouter({
|
||||
},
|
||||
),
|
||||
remove: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Remove a user",
|
||||
description: "Delete a user from the organization. Only owners and admins can remove users; owners cannot be removed, and admins cannot remove themselves or other admins. Disabled on cloud.",
|
||||
},
|
||||
})
|
||||
.input(
|
||||
z.object({
|
||||
userId: z.string(),
|
||||
@@ -333,6 +411,12 @@ export const userRouter = createTRPCRouter({
|
||||
return result;
|
||||
}),
|
||||
assignPermissions: withPermission("member", "update")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Assign member permissions",
|
||||
description: "Update permissions for a specific member in the organization. Only the organization owner can assign permissions. Git provider and server access restrictions require a valid license.",
|
||||
},
|
||||
})
|
||||
.input(apiAssignPermissions)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
@@ -383,7 +467,14 @@ export const userRouter = createTRPCRouter({
|
||||
throw error;
|
||||
}
|
||||
}),
|
||||
getInvitations: protectedProcedure.query(async ({ ctx }) => {
|
||||
getInvitations: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Get pending invitations for current user",
|
||||
description: "Retrieve all pending organization invitations for the current user's email that have not yet expired.",
|
||||
},
|
||||
})
|
||||
.query(async ({ ctx }) => {
|
||||
return await db.query.invitation.findMany({
|
||||
where: and(
|
||||
eq(invitation.email, ctx.user.email),
|
||||
@@ -397,6 +488,12 @@ export const userRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
getContainerMetrics: withPermission("monitoring", "read")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Get container metrics",
|
||||
description: "Fetch monitoring metrics for a specific container by querying the metrics endpoint. Requires an application name, metrics URL, and authentication token.",
|
||||
},
|
||||
})
|
||||
.input(
|
||||
z.object({
|
||||
url: z.string(),
|
||||
@@ -455,11 +552,24 @@ export const userRouter = createTRPCRouter({
|
||||
}
|
||||
}),
|
||||
|
||||
generateToken: protectedProcedure.mutation(async () => {
|
||||
generateToken: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Generate authentication token",
|
||||
description: "Generate a new authentication token for the current user.",
|
||||
},
|
||||
})
|
||||
.mutation(async () => {
|
||||
return "token";
|
||||
}),
|
||||
|
||||
deleteApiKey: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Delete an API key",
|
||||
description: "Delete an API key by ID. Only the owner of the API key can delete it. Logs an audit event on success.",
|
||||
},
|
||||
})
|
||||
.input(
|
||||
z.object({
|
||||
apiKeyId: z.string(),
|
||||
@@ -499,6 +609,12 @@ export const userRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
createApiKey: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Create an API key",
|
||||
description: "Create a new API key for the current user, scoped to a specific organization. Supports optional rate limiting and request limiting configuration.",
|
||||
},
|
||||
})
|
||||
.input(apiCreateApiKey)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
// Verify user is a member of the organization specified in metadata
|
||||
@@ -529,6 +645,12 @@ export const userRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
checkUserOrganizations: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Check user organization count",
|
||||
description: "Return the number of organizations a user belongs to. Users can check their own count; admins and owners can check counts for members in the active organization.",
|
||||
},
|
||||
})
|
||||
.input(
|
||||
z.object({
|
||||
userId: z.string(),
|
||||
@@ -570,6 +692,12 @@ export const userRouter = createTRPCRouter({
|
||||
return organizations.length;
|
||||
}),
|
||||
createUserWithCredentials: withPermission("member", "create")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Create user with credentials",
|
||||
description: "Create a new user with email and password and add them to the active organization with the specified role. Only available in self-hosted mode.",
|
||||
},
|
||||
})
|
||||
.input(
|
||||
z.object({
|
||||
email: z.string().email(),
|
||||
@@ -601,6 +729,12 @@ export const userRouter = createTRPCRouter({
|
||||
});
|
||||
}),
|
||||
sendInvitation: withPermission("member", "create")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Send invitation email",
|
||||
description: "Send an invitation email to a pending invitee using a configured email or Resend notification provider. Returns the generated invite link. Disabled on cloud.",
|
||||
},
|
||||
})
|
||||
.input(
|
||||
z.object({
|
||||
invitationId: z.string().min(1),
|
||||
@@ -676,7 +810,14 @@ export const userRouter = createTRPCRouter({
|
||||
return inviteLink;
|
||||
}),
|
||||
|
||||
getBookmarkedTemplates: protectedProcedure.query(async ({ ctx }) => {
|
||||
getBookmarkedTemplates: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Get bookmarked templates",
|
||||
description: "Retrieve the list of template IDs that the current user has bookmarked.",
|
||||
},
|
||||
})
|
||||
.query(async ({ ctx }) => {
|
||||
const result = await db.query.user.findFirst({
|
||||
where: eq(user.id, ctx.user.id),
|
||||
columns: { bookmarkedTemplates: true },
|
||||
@@ -686,6 +827,12 @@ export const userRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
toggleTemplateBookmark: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Toggle template bookmark",
|
||||
description: "Add or remove a template from the current user's bookmarks. Returns whether the template is now bookmarked.",
|
||||
},
|
||||
})
|
||||
.input(
|
||||
z.object({
|
||||
templateId: z.string().min(1),
|
||||
|
||||
@@ -30,6 +30,12 @@ import { createTRPCRouter, protectedProcedure, withPermission } from "../trpc";
|
||||
|
||||
export const volumeBackupsRouter = createTRPCRouter({
|
||||
list: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "List volume backups",
|
||||
description: "Returns all volume backup configurations for a given service, including related service details.",
|
||||
},
|
||||
})
|
||||
.input(
|
||||
z.object({
|
||||
id: z.string().min(1),
|
||||
@@ -65,6 +71,12 @@ export const volumeBackupsRouter = createTRPCRouter({
|
||||
});
|
||||
}),
|
||||
create: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Create a volume backup",
|
||||
description: "Creates a new volume backup configuration for a service. If enabled, automatically schedules the backup using the provided cron expression.",
|
||||
},
|
||||
})
|
||||
.input(createVolumeBackupSchema)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const serviceId =
|
||||
@@ -102,6 +114,12 @@ export const volumeBackupsRouter = createTRPCRouter({
|
||||
return newVolumeBackup;
|
||||
}),
|
||||
one: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Get a volume backup",
|
||||
description: "Returns the details of a specific volume backup configuration by its ID.",
|
||||
},
|
||||
})
|
||||
.input(
|
||||
z.object({
|
||||
volumeBackupId: z.string().min(1),
|
||||
@@ -126,6 +144,12 @@ export const volumeBackupsRouter = createTRPCRouter({
|
||||
return vb;
|
||||
}),
|
||||
delete: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Delete a volume backup",
|
||||
description: "Permanently removes a volume backup configuration by its ID.",
|
||||
},
|
||||
})
|
||||
.input(
|
||||
z.object({
|
||||
volumeBackupId: z.string().min(1),
|
||||
@@ -156,6 +180,12 @@ export const volumeBackupsRouter = createTRPCRouter({
|
||||
return result;
|
||||
}),
|
||||
update: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Update a volume backup",
|
||||
description: "Updates an existing volume backup configuration. Reschedules or removes the backup job depending on the enabled state.",
|
||||
},
|
||||
})
|
||||
.input(updateVolumeBackupSchema)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const existingVb = await findVolumeBackupById(input.volumeBackupId);
|
||||
@@ -216,6 +246,12 @@ export const volumeBackupsRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
runManually: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Run a volume backup manually",
|
||||
description: "Immediately executes a volume backup outside of its normal cron schedule.",
|
||||
},
|
||||
})
|
||||
.input(z.object({ volumeBackupId: z.string().min(1) }))
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const vb = await findVolumeBackupById(input.volumeBackupId);
|
||||
|
||||
@@ -13,7 +13,12 @@ import { hasValidLicense } from "@dokploy/server/index";
|
||||
import type { statements } from "@dokploy/server/lib/access-control";
|
||||
import { validateRequest } from "@dokploy/server/lib/auth";
|
||||
import { checkPermission } from "@dokploy/server/services/permission";
|
||||
import type { OpenApiMeta } from "@dokploy/trpc-openapi";
|
||||
import type { OpenApiMeta as _OpenApiMeta } from "@dokploy/trpc-openapi";
|
||||
|
||||
// method and path are auto-generated by @dokploy/trpc-openapi, make them optional
|
||||
type OpenApiMeta = {
|
||||
openapi?: Partial<NonNullable<_OpenApiMeta["openapi"]>>;
|
||||
};
|
||||
import { initTRPC, TRPCError } from "@trpc/server";
|
||||
import type { CreateNextContextOptions } from "@trpc/server/adapters/next";
|
||||
import type { Session, User } from "better-auth";
|
||||
|
||||
54248
openapi.json
54248
openapi.json
File diff suppressed because it is too large
Load Diff
429
packages/server/src/utils/ai/api-tool.ts
Normal file
429
packages/server/src/utils/ai/api-tool.ts
Normal file
@@ -0,0 +1,429 @@
|
||||
import { dynamicTool } from "ai";
|
||||
import { z } from "zod";
|
||||
import type { ChatContext } from "./chat-tools";
|
||||
|
||||
interface OpenApiSpec {
|
||||
paths: Record<
|
||||
string,
|
||||
Record<
|
||||
string,
|
||||
{
|
||||
operationId?: string;
|
||||
summary?: string;
|
||||
description?: string;
|
||||
tags?: string[];
|
||||
deprecated?: boolean;
|
||||
parameters?: {
|
||||
name: string;
|
||||
in: string;
|
||||
required?: boolean;
|
||||
schema?: { type?: string };
|
||||
}[];
|
||||
requestBody?: {
|
||||
content: Record<
|
||||
string,
|
||||
{ schema: { properties?: Record<string, any>; required?: string[] } }
|
||||
>;
|
||||
};
|
||||
}
|
||||
>
|
||||
>;
|
||||
}
|
||||
|
||||
interface ToolConfig {
|
||||
baseUrl: string;
|
||||
cookie: string;
|
||||
}
|
||||
|
||||
interface EndpointInfo {
|
||||
method: string;
|
||||
path: string;
|
||||
operationId: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Semantic hints for properties that the OpenAPI spec doesn't describe well.
|
||||
* These are appended to parameter names in the catalog so the model knows what values to use.
|
||||
*/
|
||||
export const PROPERTY_HINTS: Record<string, string> = {
|
||||
composePath: "path to the directory containing docker-compose.yml in the repo, e.g. '.' or 'docker/'",
|
||||
composeFile: "raw YAML content of the docker-compose.yml file",
|
||||
env: "environment variables as a single string, one per line in KEY=VALUE format",
|
||||
dockerImage: "Docker image name with optional tag, e.g. 'nginx:alpine' or 'postgres:16'",
|
||||
command: "Docker CMD override, e.g. 'npm start' or 'python app.py'",
|
||||
args: "Docker command arguments as an array of strings",
|
||||
buildPath: "path to the build context directory in the repo, e.g. '.' or './app'",
|
||||
publishDirectory: "output directory for static builds, e.g. 'dist' or '.next'",
|
||||
dockerfile: "path to the Dockerfile relative to buildPath, e.g. 'Dockerfile' or 'docker/Dockerfile.prod'",
|
||||
repository: "Git repository name, e.g. 'my-app' (not the full URL)",
|
||||
branch: "Git branch name, e.g. 'main' or 'develop'",
|
||||
customGitUrl: "full Git clone URL, e.g. 'https://github.com/user/repo.git'",
|
||||
databasePassword: "password string for the database",
|
||||
externalPort: "port number exposed to the host, e.g. 5432",
|
||||
host: "domain hostname, e.g. 'myapp.example.com'",
|
||||
appName: "unique internal service name (auto-generated, alphanumeric with dots/hyphens)",
|
||||
watchPaths: "array of file paths that trigger auto-deploy on change, e.g. ['src/**', 'package.json']",
|
||||
suffix: "custom suffix appended to the service name",
|
||||
repoPath: "path within the cloned repository to browse, use '.' for root directory",
|
||||
filePath: "path to a specific file in the repository, e.g. 'docker-compose.yml' or 'src/index.ts'",
|
||||
tail: "number of log lines to return from the end, e.g. 100",
|
||||
since: "time duration for log filtering, e.g. '1h' or '30m'",
|
||||
search: "text to search for in logs",
|
||||
};
|
||||
|
||||
const EXCLUDED_TAGS = new Set([
|
||||
"notification",
|
||||
"sso",
|
||||
"stripe",
|
||||
"auditLog",
|
||||
"ai",
|
||||
"customRole",
|
||||
"whitelabeling",
|
||||
]);
|
||||
|
||||
/** Minimal shared tags — only project/environment for navigation */
|
||||
const SHARED_TAGS = ["project", "environment"];
|
||||
|
||||
/** Tags allowed per context type (on top of SHARED_TAGS) */
|
||||
const CONTEXT_TAGS: Record<ChatContext["type"], string[]> = {
|
||||
application: [
|
||||
"application",
|
||||
"deployment",
|
||||
"domain",
|
||||
"docker",
|
||||
"mounts",
|
||||
"port",
|
||||
"security",
|
||||
"redirects",
|
||||
"registry",
|
||||
"sshKey",
|
||||
"backup",
|
||||
"volumeBackups",
|
||||
"rollback",
|
||||
"schedule",
|
||||
"patch",
|
||||
"previewDeployment",
|
||||
"bitbucket",
|
||||
"gitea",
|
||||
"github",
|
||||
"gitlab",
|
||||
"gitProvider",
|
||||
"destination",
|
||||
"tag",
|
||||
],
|
||||
compose: [
|
||||
"compose",
|
||||
"deployment",
|
||||
"domain",
|
||||
"docker",
|
||||
"backup",
|
||||
"patch",
|
||||
"sshKey",
|
||||
"bitbucket",
|
||||
"gitea",
|
||||
"github",
|
||||
"gitlab",
|
||||
"gitProvider",
|
||||
"tag",
|
||||
],
|
||||
postgres: ["postgres", "backup", "docker", "destination"],
|
||||
mysql: ["mysql", "backup", "docker", "destination"],
|
||||
redis: ["redis", "docker"],
|
||||
mongo: ["mongo", "backup", "docker", "destination"],
|
||||
mariadb: ["mariadb", "backup", "docker", "destination"],
|
||||
libsql: ["libsql", "docker"],
|
||||
project: [
|
||||
"application",
|
||||
"compose",
|
||||
"postgres",
|
||||
"mysql",
|
||||
"redis",
|
||||
"mongo",
|
||||
"mariadb",
|
||||
"libsql",
|
||||
"domain",
|
||||
"deployment",
|
||||
"docker",
|
||||
"tag",
|
||||
],
|
||||
server: [
|
||||
"server",
|
||||
"docker",
|
||||
"cluster",
|
||||
"swarm",
|
||||
"certificates",
|
||||
"registry",
|
||||
"settings",
|
||||
],
|
||||
general: [], // empty = allow all non-excluded tags
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the set of allowed tags for a given context type.
|
||||
* Returns null for "general" context (no filtering, allow all).
|
||||
*/
|
||||
function getAllowedTags(contextType: ChatContext["type"]): Set<string> | null {
|
||||
if (contextType === "general") return null;
|
||||
const contextSpecific = CONTEXT_TAGS[contextType];
|
||||
return new Set([...SHARED_TAGS, ...contextSpecific]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract enum values from a JSON Schema property (handles anyOf wrappers).
|
||||
*/
|
||||
function extractEnum(prop: any): string[] | null {
|
||||
if (prop?.enum) return prop.enum;
|
||||
if (Array.isArray(prop?.anyOf)) {
|
||||
for (const variant of prop.anyOf) {
|
||||
if (variant?.enum) return variant.enum;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Human-readable description for each tag group in the catalog */
|
||||
const TAG_DESCRIPTIONS: Record<string, string> = {
|
||||
application: "Manage application services — create, update, deploy, start, stop, and configure applications",
|
||||
compose: "Manage Docker Compose/Stack services — create, update, deploy, and configure compose files",
|
||||
postgres: "Manage PostgreSQL database services",
|
||||
mysql: "Manage MySQL database services",
|
||||
redis: "Manage Redis database services",
|
||||
mongo: "Manage MongoDB database services",
|
||||
mariadb: "Manage MariaDB database services",
|
||||
libsql: "Manage LibSQL database services",
|
||||
deployment: "View deployment history, build logs, and manage deployment lifecycle",
|
||||
domain: "Manage domains, SSL certificates, and routing for services",
|
||||
docker: "Interact with Docker containers — inspect, restart, remove, and view logs",
|
||||
backup: "Create and manage database backups, run manual backups, and restore from backups",
|
||||
patch: "Browse and modify source code files in a service's cloned repository — read directories, read files, and create file patches",
|
||||
mounts: "Manage persistent volume mounts for services",
|
||||
port: "Manage exposed port mappings for services",
|
||||
security: "Manage HTTP basic auth security rules for services",
|
||||
redirects: "Manage HTTP redirect rules for domains",
|
||||
registry: "Manage Docker registries for pulling private images",
|
||||
sshKey: "Manage SSH keys for Git repository access",
|
||||
rollback: "Rollback a service to a previous deployment",
|
||||
schedule: "Create and manage scheduled tasks (cron jobs) for services",
|
||||
previewDeployment: "Manage preview deployments for pull requests",
|
||||
volumeBackups: "Create and manage volume-level backups and restores",
|
||||
project: "Manage projects — create, update, delete, and list projects",
|
||||
environment: "Manage environments within projects — create, duplicate, and configure",
|
||||
server: "Manage servers — configure, monitor, and connect remote servers",
|
||||
settings: "View and update global Dokploy settings",
|
||||
destination: "Manage S3/storage destinations for backups",
|
||||
tag: "Manage tags for organizing and labeling services",
|
||||
cluster: "Manage Docker Swarm cluster nodes",
|
||||
swarm: "Manage Docker Swarm settings and configuration",
|
||||
certificates: "Manage SSL/TLS certificates",
|
||||
gitProvider: "Manage Git provider integrations",
|
||||
github: "Manage GitHub provider connections and repositories",
|
||||
gitlab: "Manage GitLab provider connections and repositories",
|
||||
bitbucket: "Manage Bitbucket provider connections and repositories",
|
||||
gitea: "Manage Gitea provider connections and repositories",
|
||||
user: "Manage user accounts and permissions",
|
||||
};
|
||||
|
||||
export interface CatalogResult {
|
||||
catalog: string;
|
||||
count: number;
|
||||
operationIds: Set<string>;
|
||||
}
|
||||
|
||||
export function buildEndpointCatalog(
|
||||
spec: OpenApiSpec,
|
||||
contextType: ChatContext["type"] = "general",
|
||||
relevantOperationIds?: Set<string>,
|
||||
): CatalogResult {
|
||||
const operationIds = new Set<string>();
|
||||
const allowedTags = getAllowedTags(contextType);
|
||||
const groups = new Map<string, string[]>();
|
||||
|
||||
for (const methods of Object.values(spec.paths)) {
|
||||
for (const op of Object.values(methods)) {
|
||||
if (!op.operationId || op.deprecated) continue;
|
||||
if (op.tags?.some((t) => EXCLUDED_TAGS.has(t))) continue;
|
||||
if (allowedTags && !op.tags?.some((t) => allowedTags.has(t))) continue;
|
||||
if (relevantOperationIds && !relevantOperationIds.has(op.operationId)) continue;
|
||||
|
||||
operationIds.add(op.operationId);
|
||||
|
||||
const requiredParams: string[] = [];
|
||||
const optionalParams: string[] = [];
|
||||
|
||||
if (op.parameters) {
|
||||
for (const p of op.parameters) {
|
||||
if (p.in === "header") continue;
|
||||
if (p.required) {
|
||||
requiredParams.push(`${p.name}*`);
|
||||
} else {
|
||||
optionalParams.push(`${p.name}?`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (op.requestBody?.content?.["application/json"]?.schema) {
|
||||
const schema = op.requestBody.content["application/json"].schema;
|
||||
const requiredSet = new Set(schema.required ?? []);
|
||||
if (schema.properties) {
|
||||
for (const [key, prop] of Object.entries(
|
||||
schema.properties as Record<string, any>,
|
||||
)) {
|
||||
const enumVals = extractEnum(prop);
|
||||
const hint = PROPERTY_HINTS[key];
|
||||
const suffix = enumVals
|
||||
? `[${enumVals.join("|")}]`
|
||||
: hint
|
||||
? `(${hint})`
|
||||
: "";
|
||||
if (requiredSet.has(key)) {
|
||||
requiredParams.push(`${key}*${suffix}`);
|
||||
} else {
|
||||
optionalParams.push(`${key}?${suffix}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const allParams = [...requiredParams, ...optionalParams];
|
||||
const paramStr =
|
||||
allParams.length > 0 ? `(${allParams.join(", ")})` : "";
|
||||
const summary = op.summary ? ` — ${op.summary}` : "";
|
||||
const desc = op.description ? `\n ${op.description}` : "";
|
||||
const line = `${op.operationId}${paramStr}${summary}${desc}`;
|
||||
|
||||
const tag = op.tags?.[0] ?? "other";
|
||||
if (!groups.has(tag)) groups.set(tag, []);
|
||||
groups.get(tag)!.push(line);
|
||||
}
|
||||
}
|
||||
|
||||
// Order sections: context-specific tags first (in CONTEXT_TAGS order), then shared, then rest
|
||||
const contextOrder = CONTEXT_TAGS[contextType];
|
||||
const sharedOrder = SHARED_TAGS;
|
||||
const orderedTags: string[] = [];
|
||||
const seen = new Set<string>();
|
||||
|
||||
for (const t of contextOrder) {
|
||||
if (groups.has(t) && !seen.has(t)) { orderedTags.push(t); seen.add(t); }
|
||||
}
|
||||
for (const t of sharedOrder) {
|
||||
if (groups.has(t) && !seen.has(t)) { orderedTags.push(t); seen.add(t); }
|
||||
}
|
||||
for (const t of groups.keys()) {
|
||||
if (!seen.has(t)) { orderedTags.push(t); seen.add(t); }
|
||||
}
|
||||
|
||||
const sections: string[] = [];
|
||||
for (const tag of orderedTags) {
|
||||
const lines = groups.get(tag)!;
|
||||
const tagDesc = TAG_DESCRIPTIONS[tag];
|
||||
const header = tagDesc ? `## ${tag} — ${tagDesc}` : `## ${tag}`;
|
||||
sections.push(`${header}\n${lines.join("\n")}`);
|
||||
}
|
||||
|
||||
return {
|
||||
catalog: sections.join("\n\n"),
|
||||
count: operationIds.size,
|
||||
operationIds,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a lookup map from operationId to endpoint info for execution.
|
||||
*/
|
||||
function buildEndpointMap(
|
||||
spec: OpenApiSpec,
|
||||
): Map<string, EndpointInfo> {
|
||||
const map = new Map<string, EndpointInfo>();
|
||||
for (const [path, methods] of Object.entries(spec.paths)) {
|
||||
for (const [method, op] of Object.entries(methods)) {
|
||||
if (!op.operationId) continue;
|
||||
map.set(op.operationId, { method, path, operationId: op.operationId });
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a single "call_api" tool that only allows endpoints present in allowedOperationIds.
|
||||
*/
|
||||
export function createApiTool(
|
||||
spec: OpenApiSpec,
|
||||
config: ToolConfig,
|
||||
allowedOperationIds?: Set<string>,
|
||||
maxResponseSize = 4000,
|
||||
) {
|
||||
const endpointMap = buildEndpointMap(spec);
|
||||
|
||||
return {
|
||||
call_api: dynamicTool({
|
||||
description:
|
||||
"Call a Dokploy API endpoint. Use the operationId from the endpoint catalog and pass the required parameters.",
|
||||
inputSchema: z.object({
|
||||
operationId: z
|
||||
.string()
|
||||
.describe("The operationId from the endpoint catalog"),
|
||||
params: z
|
||||
.record(z.string(), z.any())
|
||||
.optional()
|
||||
.describe("Parameters for the endpoint (* = required)"),
|
||||
}),
|
||||
execute: async (rawInput: unknown) => {
|
||||
const { operationId, params } = rawInput as {
|
||||
operationId: string;
|
||||
params?: Record<string, unknown>;
|
||||
};
|
||||
|
||||
if (allowedOperationIds && !allowedOperationIds.has(operationId)) {
|
||||
return `Error: "${operationId}" is not available in the current context. Only use operationIds from the ENDPOINT CATALOG.`;
|
||||
}
|
||||
|
||||
const endpoint = endpointMap.get(operationId);
|
||||
if (!endpoint) {
|
||||
return `Error: Unknown endpoint "${operationId}". Check the endpoint catalog for valid operationIds.`;
|
||||
}
|
||||
|
||||
const headers: Record<string, string> = {
|
||||
"Content-Type": "application/json",
|
||||
Cookie: config.cookie,
|
||||
};
|
||||
|
||||
let url = `${config.baseUrl}${endpoint.path}`;
|
||||
|
||||
if (endpoint.method === "get" && params) {
|
||||
const searchParams = new URLSearchParams();
|
||||
for (const [key, value] of Object.entries(params)) {
|
||||
if (value !== undefined && value !== null) {
|
||||
searchParams.append(key, String(value));
|
||||
}
|
||||
}
|
||||
const qs = searchParams.toString();
|
||||
if (qs) url += `?${qs}`;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
method: endpoint.method.toUpperCase(),
|
||||
headers,
|
||||
...(endpoint.method !== "get" && params
|
||||
? { body: JSON.stringify(params) }
|
||||
: {}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
return `API error (${response.status}): ${errorText.slice(0, 500)}\n\nHint: Check the ENDPOINT CATALOG for required parameters (*). You called "${operationId}" with params: ${JSON.stringify(params ?? {})}`;
|
||||
}
|
||||
|
||||
const json = JSON.stringify(await response.json(), null, 2);
|
||||
if (json.length > maxResponseSize) {
|
||||
return `${json.slice(0, maxResponseSize)}\n\n... [Truncated — ${json.length} chars total]`;
|
||||
}
|
||||
return json;
|
||||
} catch (error) {
|
||||
return `Error: ${error instanceof Error ? error.message : "Unknown error"}`;
|
||||
}
|
||||
},
|
||||
}),
|
||||
};
|
||||
}
|
||||
319
packages/server/src/utils/ai/chat-tools.ts
Normal file
319
packages/server/src/utils/ai/chat-tools.ts
Normal file
@@ -0,0 +1,319 @@
|
||||
import { readFile } from "node:fs/promises";
|
||||
import { dynamicTool } from "ai";
|
||||
import { z } from "zod";
|
||||
import { findDeploymentById } from "../../services/deployment";
|
||||
|
||||
export type ServiceType =
|
||||
| "application"
|
||||
| "compose"
|
||||
| "postgres"
|
||||
| "mysql"
|
||||
| "redis"
|
||||
| "mongo"
|
||||
| "mariadb"
|
||||
| "libsql";
|
||||
|
||||
export interface ChatContext {
|
||||
type: ServiceType | "project" | "server" | "general";
|
||||
id: string;
|
||||
projectId?: string;
|
||||
environmentId?: string;
|
||||
serverId?: string;
|
||||
}
|
||||
|
||||
interface ToolConfig {
|
||||
baseUrl: string;
|
||||
cookie: string;
|
||||
}
|
||||
|
||||
async function callApi(
|
||||
config: ToolConfig,
|
||||
method: "GET" | "POST",
|
||||
path: string,
|
||||
params?: Record<string, unknown>,
|
||||
) {
|
||||
const headers: Record<string, string> = {
|
||||
"Content-Type": "application/json",
|
||||
Cookie: config.cookie,
|
||||
};
|
||||
|
||||
let url = `${config.baseUrl}${path}`;
|
||||
|
||||
if (method === "GET" && params) {
|
||||
const searchParams = new URLSearchParams();
|
||||
for (const [key, value] of Object.entries(params)) {
|
||||
if (value !== undefined && value !== null) {
|
||||
searchParams.append(key, String(value));
|
||||
}
|
||||
}
|
||||
const qs = searchParams.toString();
|
||||
if (qs) url += `?${qs}`;
|
||||
}
|
||||
|
||||
const response = await fetch(url, {
|
||||
method,
|
||||
headers,
|
||||
...(method === "POST" && params ? { body: JSON.stringify(params) } : {}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
throw new Error(`API error (${response.status}): ${errorText.slice(0, 500)}`);
|
||||
}
|
||||
|
||||
return response.json();
|
||||
}
|
||||
|
||||
function makeTool(
|
||||
description: string,
|
||||
inputSchema: z.ZodObject<z.ZodRawShape>,
|
||||
executeFn: (input: Record<string, unknown>) => Promise<unknown>,
|
||||
) {
|
||||
return dynamicTool({
|
||||
description,
|
||||
inputSchema,
|
||||
execute: async (rawInput: unknown) => {
|
||||
try {
|
||||
const input = (rawInput ?? {}) as Record<string, unknown>;
|
||||
const result = await executeFn(input);
|
||||
const json = JSON.stringify(result, null, 2);
|
||||
// Truncate very large responses
|
||||
if (json.length > 15000) {
|
||||
return `${json.slice(0, 15000)}\n\n... [Truncated — ${json.length} chars total]`;
|
||||
}
|
||||
return json;
|
||||
} catch (error) {
|
||||
return `Error: ${error instanceof Error ? error.message : "Unknown error"}`;
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// ─── READ TOOLS ──────────────────────────────────────────────
|
||||
|
||||
function readTools(context: ChatContext, config: ToolConfig) {
|
||||
const tools: Record<string, ReturnType<typeof dynamicTool>> = {};
|
||||
|
||||
if (context.type === "application") {
|
||||
tools["get-application-info"] = makeTool(
|
||||
"Get the full configuration of the current application: name, status, build type, source, env vars, resource limits, and more. Call this first to understand the app state.",
|
||||
z.object({}),
|
||||
() => callApi(config, "GET", "/application.one", { applicationId: context.id }),
|
||||
);
|
||||
|
||||
tools["list-deployments"] = makeTool(
|
||||
"List the 10 most recent deployments for this application. Each deployment has a status (done/error/running), title, error message, and timestamps. Use this to find failed builds.",
|
||||
z.object({}),
|
||||
() => callApi(config, "GET", "/deployment.allByType", { id: context.id, type: "application" }),
|
||||
);
|
||||
|
||||
tools["list-domains"] = makeTool(
|
||||
"List all domains configured for this application.",
|
||||
z.object({}),
|
||||
() => callApi(config, "GET", "/domain.byApplicationId", { applicationId: context.id }),
|
||||
);
|
||||
|
||||
tools["get-containers"] = makeTool(
|
||||
"List running Docker containers for this application. Shows container state, status, and names.",
|
||||
z.object({}),
|
||||
async () => {
|
||||
const app = await callApi(config, "GET", "/application.one", { applicationId: context.id });
|
||||
return callApi(config, "GET", "/docker.getContainersByAppNameMatch", {
|
||||
appName: (app as { appName: string }).appName,
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
if (context.type === "compose") {
|
||||
tools["get-compose-info"] = makeTool(
|
||||
"Get the full configuration of the current compose service: name, status, compose file content, env vars, and more.",
|
||||
z.object({}),
|
||||
() => callApi(config, "GET", "/compose.one", { composeId: context.id }),
|
||||
);
|
||||
|
||||
tools["list-deployments"] = makeTool(
|
||||
"List the 10 most recent deployments for this compose service.",
|
||||
z.object({}),
|
||||
() => callApi(config, "GET", "/deployment.allByType", { id: context.id, type: "compose" }),
|
||||
);
|
||||
|
||||
tools["list-domains"] = makeTool(
|
||||
"List all domains configured for this compose service.",
|
||||
z.object({}),
|
||||
() => callApi(config, "GET", "/domain.byComposeId", { composeId: context.id }),
|
||||
);
|
||||
}
|
||||
|
||||
if (context.type === "project") {
|
||||
tools["get-project-info"] = makeTool(
|
||||
"Get the full project details including ALL environments and ALL services (applications, compose, databases). Use this to count services, see what's deployed, and find failing services.",
|
||||
z.object({}),
|
||||
() => callApi(config, "GET", "/project.one", { projectId: context.id }),
|
||||
);
|
||||
}
|
||||
|
||||
if (context.type === "general") {
|
||||
tools["list-projects"] = makeTool(
|
||||
"List all projects in the organization.",
|
||||
z.object({}),
|
||||
() => callApi(config, "GET", "/project.all"),
|
||||
);
|
||||
}
|
||||
|
||||
// Available in both application and compose contexts
|
||||
if (context.type === "application" || context.type === "compose") {
|
||||
tools["read-deployment-logs"] = dynamicTool({
|
||||
description:
|
||||
"Read the build/deployment logs for a specific deployment. ALWAYS call list-deployments first to find the deploymentId. This reads the actual log file content to diagnose build failures.",
|
||||
inputSchema: z.object({
|
||||
deploymentId: z.string().describe("The deployment ID from list-deployments"),
|
||||
}),
|
||||
execute: async (rawInput: unknown) => {
|
||||
const { deploymentId } = rawInput as { deploymentId: string };
|
||||
try {
|
||||
const deployment = await findDeploymentById(deploymentId);
|
||||
const content = await readFile(deployment.logPath, "utf-8");
|
||||
const lines = content.split("\n");
|
||||
const last200 = lines.slice(-200).join("\n");
|
||||
return `Deployment status: ${deployment.status}\nError message: ${deployment.errorMessage || "none"}\n\nLast 200 lines of build log:\n${last200}`;
|
||||
} catch {
|
||||
return "Could not read deployment logs — the log file may not exist.";
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
tools["read-runtime-logs"] = makeTool(
|
||||
"Read the runtime/container logs (stdout/stderr) of this application. Shows the last N lines of the running application output. Use this to diagnose runtime errors, crashes, or check if the app is working.",
|
||||
z.object({
|
||||
tail: z.number().optional().describe("Number of lines to read (default 200, max 500)"),
|
||||
}),
|
||||
(input) => {
|
||||
const tail = Math.min((input.tail as number) || 200, 500);
|
||||
const endpoint = context.type === "compose" ? "/compose.readLogs" : "/application.readLogs";
|
||||
const idKey = context.type === "compose" ? "composeId" : "applicationId";
|
||||
return callApi(config, "GET", endpoint, {
|
||||
[idKey]: context.id,
|
||||
tail,
|
||||
since: "all",
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
return tools;
|
||||
}
|
||||
|
||||
// ─── WRITE TOOLS ─────────────────────────────────────────────
|
||||
|
||||
function writeTools(context: ChatContext, config: ToolConfig) {
|
||||
const tools: Record<string, ReturnType<typeof dynamicTool>> = {};
|
||||
|
||||
if (context.type === "application") {
|
||||
tools["update-env-vars"] = makeTool(
|
||||
"Update the environment variables for this application. Pass the FULL env string (KEY=VALUE format, one per line). This REPLACES all existing env vars, so include the ones you want to keep.",
|
||||
z.object({
|
||||
env: z.string().describe("Full environment variables, one KEY=VALUE per line"),
|
||||
}),
|
||||
(input) =>
|
||||
callApi(config, "POST", "/application.saveEnvironment", {
|
||||
applicationId: context.id,
|
||||
env: input.env,
|
||||
}),
|
||||
);
|
||||
|
||||
tools["deploy-application"] = makeTool(
|
||||
"Trigger a new deployment/build for this application. The build will run in the background.",
|
||||
z.object({}),
|
||||
() =>
|
||||
callApi(config, "POST", "/application.deploy", {
|
||||
applicationId: context.id,
|
||||
title: "AI-triggered deployment",
|
||||
description: "Deployed via AI Assistant",
|
||||
}),
|
||||
);
|
||||
|
||||
tools["redeploy-application"] = makeTool(
|
||||
"Redeploy the application using the existing build (no new build). Faster than deploy.",
|
||||
z.object({}),
|
||||
() =>
|
||||
callApi(config, "POST", "/application.redeploy", {
|
||||
applicationId: context.id,
|
||||
title: "AI-triggered redeployment",
|
||||
description: "Redeployed via AI Assistant",
|
||||
}),
|
||||
);
|
||||
|
||||
tools["stop-application"] = makeTool(
|
||||
"Stop the application. This will stop all containers.",
|
||||
z.object({}),
|
||||
() => callApi(config, "POST", "/application.stop", { applicationId: context.id }),
|
||||
);
|
||||
|
||||
tools["start-application"] = makeTool(
|
||||
"Start a stopped application.",
|
||||
z.object({}),
|
||||
() => callApi(config, "POST", "/application.start", { applicationId: context.id }),
|
||||
);
|
||||
|
||||
tools["restart-container"] = makeTool(
|
||||
"Restart a specific Docker container. Use get-containers first to find the container ID.",
|
||||
z.object({
|
||||
containerId: z.string().describe("The container ID from get-containers"),
|
||||
}),
|
||||
(input) => callApi(config, "POST", "/docker.restartContainer", { containerId: input.containerId }),
|
||||
);
|
||||
}
|
||||
|
||||
if (context.type === "compose") {
|
||||
tools["update-compose-env"] = makeTool(
|
||||
"Update the environment variables for this compose service. Pass the FULL env string.",
|
||||
z.object({
|
||||
env: z.string().describe("Full environment variables, one KEY=VALUE per line"),
|
||||
}),
|
||||
(input) =>
|
||||
callApi(config, "POST", "/compose.update", {
|
||||
composeId: context.id,
|
||||
env: input.env,
|
||||
}),
|
||||
);
|
||||
|
||||
tools["deploy-compose"] = makeTool(
|
||||
"Trigger a new deployment for this compose service.",
|
||||
z.object({}),
|
||||
() =>
|
||||
callApi(config, "POST", "/compose.deploy", {
|
||||
composeId: context.id,
|
||||
title: "AI-triggered deployment",
|
||||
description: "Deployed via AI Assistant",
|
||||
}),
|
||||
);
|
||||
|
||||
tools["stop-compose"] = makeTool(
|
||||
"Stop the compose service.",
|
||||
z.object({}),
|
||||
() => callApi(config, "POST", "/compose.stop", { composeId: context.id }),
|
||||
);
|
||||
|
||||
tools["start-compose"] = makeTool(
|
||||
"Start a stopped compose service.",
|
||||
z.object({}),
|
||||
() => callApi(config, "POST", "/compose.start", { composeId: context.id }),
|
||||
);
|
||||
}
|
||||
|
||||
return tools;
|
||||
}
|
||||
|
||||
// ─── PUBLIC API ──────────────────────────────────────────────
|
||||
|
||||
export function getReadTools(context: ChatContext, config: ToolConfig) {
|
||||
return readTools(context, config);
|
||||
}
|
||||
|
||||
export function getAllTools(context: ChatContext, config: ToolConfig) {
|
||||
return {
|
||||
...readTools(context, config),
|
||||
...writeTools(context, config),
|
||||
};
|
||||
}
|
||||
317
packages/server/src/utils/ai/openapi-tools.ts
Normal file
317
packages/server/src/utils/ai/openapi-tools.ts
Normal file
@@ -0,0 +1,317 @@
|
||||
import { dynamicTool } from "ai";
|
||||
import { z } from "zod";
|
||||
|
||||
/**
|
||||
* Converts an OpenAPI spec into AI SDK tool definitions automatically.
|
||||
*
|
||||
* Each endpoint becomes a tool that the agent can call. The tool name
|
||||
* is the operationId, the description comes from the endpoint's
|
||||
* summary/description, and the input schema is derived from the
|
||||
* request body or query parameters.
|
||||
*/
|
||||
|
||||
interface OpenApiSpec {
|
||||
paths: Record<string, Record<string, OpenApiOperation>>;
|
||||
}
|
||||
|
||||
interface OpenApiOperation {
|
||||
operationId: string;
|
||||
summary?: string;
|
||||
description?: string;
|
||||
tags?: string[];
|
||||
deprecated?: boolean;
|
||||
parameters?: OpenApiParameter[];
|
||||
requestBody?: {
|
||||
required?: boolean;
|
||||
content: Record<
|
||||
string,
|
||||
{
|
||||
schema: JsonSchema;
|
||||
}
|
||||
>;
|
||||
};
|
||||
}
|
||||
|
||||
interface OpenApiParameter {
|
||||
name: string;
|
||||
in: "query" | "path" | "header";
|
||||
required?: boolean;
|
||||
schema: JsonSchema;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
interface JsonSchema {
|
||||
type?: string;
|
||||
properties?: Record<string, JsonSchema>;
|
||||
required?: string[];
|
||||
items?: JsonSchema;
|
||||
enum?: unknown[];
|
||||
description?: string;
|
||||
default?: unknown;
|
||||
nullable?: boolean;
|
||||
anyOf?: JsonSchema[];
|
||||
oneOf?: JsonSchema[];
|
||||
allOf?: JsonSchema[];
|
||||
format?: string;
|
||||
minimum?: number;
|
||||
maximum?: number;
|
||||
minLength?: number;
|
||||
maxLength?: number;
|
||||
}
|
||||
|
||||
interface ToolConfig {
|
||||
baseUrl: string;
|
||||
cookie: string;
|
||||
}
|
||||
|
||||
interface GenerateToolsOptions {
|
||||
/** Only include tools whose tag matches one of these */
|
||||
tags?: string[];
|
||||
/** Only include these specific operationIds */
|
||||
operationIds?: string[];
|
||||
/** Exclude these operationIds */
|
||||
exclude?: string[];
|
||||
/** Max response size in chars before truncating (default: 15000) */
|
||||
maxResponseSize?: number;
|
||||
}
|
||||
|
||||
// ─── JSON Schema → Zod conversion ──────────────────────────────
|
||||
|
||||
function jsonSchemaToZod(schema: JsonSchema): z.ZodTypeAny {
|
||||
if (!schema || !schema.type) {
|
||||
// anyOf / oneOf / allOf — just accept anything
|
||||
if (schema?.anyOf || schema?.oneOf || schema?.allOf) {
|
||||
return z.any();
|
||||
}
|
||||
return z.any();
|
||||
}
|
||||
|
||||
switch (schema.type) {
|
||||
case "string": {
|
||||
let s = z.string();
|
||||
if (schema.enum) {
|
||||
return z.enum(schema.enum as [string, ...string[]]);
|
||||
}
|
||||
if (schema.minLength) s = s.min(schema.minLength);
|
||||
if (schema.maxLength) s = s.max(schema.maxLength);
|
||||
if (schema.description) s = s.describe(schema.description);
|
||||
return s;
|
||||
}
|
||||
case "number":
|
||||
case "integer": {
|
||||
let n = z.number();
|
||||
if (schema.minimum !== undefined) n = n.min(schema.minimum);
|
||||
if (schema.maximum !== undefined) n = n.max(schema.maximum);
|
||||
if (schema.description) n = n.describe(schema.description);
|
||||
return n;
|
||||
}
|
||||
case "boolean":
|
||||
return z.boolean();
|
||||
case "array": {
|
||||
const itemSchema = schema.items
|
||||
? jsonSchemaToZod(schema.items)
|
||||
: z.any();
|
||||
return z.array(itemSchema);
|
||||
}
|
||||
case "object": {
|
||||
if (!schema.properties) {
|
||||
return z.object({});
|
||||
}
|
||||
const shape: Record<string, z.ZodTypeAny> = {};
|
||||
const required = new Set(schema.required ?? []);
|
||||
for (const [key, propSchema] of Object.entries(schema.properties)) {
|
||||
const zodProp = jsonSchemaToZod(propSchema);
|
||||
shape[key] = required.has(key) ? zodProp : zodProp.optional();
|
||||
}
|
||||
return z.object(shape);
|
||||
}
|
||||
default:
|
||||
return z.any();
|
||||
}
|
||||
}
|
||||
|
||||
function buildInputSchema(
|
||||
operation: OpenApiOperation,
|
||||
): z.ZodObject<z.ZodRawShape> {
|
||||
const shape: Record<string, z.ZodTypeAny> = {};
|
||||
|
||||
// Query/path parameters → flat keys
|
||||
if (operation.parameters) {
|
||||
for (const param of operation.parameters) {
|
||||
if (param.in === "header") continue;
|
||||
const zodParam = jsonSchemaToZod(param.schema);
|
||||
const described = param.description
|
||||
? zodParam.describe(param.description)
|
||||
: zodParam;
|
||||
shape[param.name] = param.required ? described : described.optional();
|
||||
}
|
||||
}
|
||||
|
||||
// Request body → merge properties into the same object
|
||||
if (operation.requestBody) {
|
||||
const content = operation.requestBody.content;
|
||||
const jsonContent = content["application/json"];
|
||||
if (jsonContent?.schema) {
|
||||
const bodySchema = jsonContent.schema;
|
||||
if (bodySchema.properties) {
|
||||
const required = new Set(bodySchema.required ?? []);
|
||||
for (const [key, propSchema] of Object.entries(
|
||||
bodySchema.properties,
|
||||
)) {
|
||||
const zodProp = jsonSchemaToZod(propSchema);
|
||||
shape[key] = required.has(key) ? zodProp : zodProp.optional();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return z.object(shape);
|
||||
}
|
||||
|
||||
// ─── API caller ─────────────────────────────────────────────────
|
||||
|
||||
async function callApi(
|
||||
config: ToolConfig,
|
||||
method: string,
|
||||
path: string,
|
||||
params: Record<string, unknown> | undefined,
|
||||
maxResponseSize: number,
|
||||
): Promise<string> {
|
||||
const headers: Record<string, string> = {
|
||||
"Content-Type": "application/json",
|
||||
Cookie: config.cookie,
|
||||
};
|
||||
|
||||
let url = `${config.baseUrl}${path}`;
|
||||
|
||||
if (method === "get" && params) {
|
||||
const searchParams = new URLSearchParams();
|
||||
for (const [key, value] of Object.entries(params)) {
|
||||
if (value !== undefined && value !== null) {
|
||||
searchParams.append(key, String(value));
|
||||
}
|
||||
}
|
||||
const qs = searchParams.toString();
|
||||
if (qs) url += `?${qs}`;
|
||||
}
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: method.toUpperCase(),
|
||||
headers,
|
||||
...(method !== "get" && params
|
||||
? { body: JSON.stringify(params) }
|
||||
: {}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
throw new Error(
|
||||
`API error (${response.status}): ${errorText.slice(0, 500)}`,
|
||||
);
|
||||
}
|
||||
|
||||
const json = JSON.stringify(await response.json(), null, 2);
|
||||
if (json.length > maxResponseSize) {
|
||||
return `${json.slice(0, maxResponseSize)}\n\n... [Truncated — ${json.length} chars total]`;
|
||||
}
|
||||
return json;
|
||||
}
|
||||
|
||||
// ─── Main conversion ────────────────────────────────────────────
|
||||
|
||||
export function openApiToTools(
|
||||
spec: OpenApiSpec,
|
||||
config: ToolConfig,
|
||||
options: GenerateToolsOptions = {},
|
||||
) {
|
||||
const {
|
||||
tags,
|
||||
operationIds,
|
||||
exclude,
|
||||
maxResponseSize = 15000,
|
||||
} = options;
|
||||
|
||||
const tagSet = tags ? new Set(tags) : null;
|
||||
const idSet = operationIds ? new Set(operationIds) : null;
|
||||
const excludeSet = exclude ? new Set(exclude) : null;
|
||||
const tools: Record<string, ReturnType<typeof dynamicTool>> = {};
|
||||
|
||||
for (const [path, methods] of Object.entries(spec.paths)) {
|
||||
for (const [method, operation] of Object.entries(methods)) {
|
||||
const opId = operation.operationId;
|
||||
if (!opId) continue;
|
||||
if (operation.deprecated) continue;
|
||||
|
||||
// Filtering
|
||||
if (excludeSet?.has(opId)) continue;
|
||||
if (idSet && !idSet.has(opId)) continue;
|
||||
if (tagSet && !operation.tags?.some((t) => tagSet.has(t))) continue;
|
||||
|
||||
const description = [operation.summary, operation.description]
|
||||
.filter(Boolean)
|
||||
.join(". ");
|
||||
|
||||
const inputSchema = buildInputSchema(operation);
|
||||
|
||||
const isWriteAction = method !== "get";
|
||||
|
||||
tools[opId] = dynamicTool({
|
||||
description: description || `Call ${method.toUpperCase()} ${path}`,
|
||||
inputSchema,
|
||||
needsApproval: isWriteAction,
|
||||
execute: async (rawInput: unknown) => {
|
||||
const input = (rawInput ?? {}) as Record<string, unknown>;
|
||||
const params =
|
||||
Object.keys(input).length > 0 ? input : undefined;
|
||||
return callApi(config, method, path, params, maxResponseSize);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return tools;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a summary of all available tools (name + description).
|
||||
* Useful for debugging or for the system prompt.
|
||||
*/
|
||||
export function getToolsSummary(
|
||||
spec: OpenApiSpec,
|
||||
options: GenerateToolsOptions = {},
|
||||
): { name: string; description: string; tag: string; method: string }[] {
|
||||
const { tags, operationIds, exclude } = options;
|
||||
|
||||
const tagSet = tags ? new Set(tags) : null;
|
||||
const idSet = operationIds ? new Set(operationIds) : null;
|
||||
const excludeSet = exclude ? new Set(exclude) : null;
|
||||
const summary: {
|
||||
name: string;
|
||||
description: string;
|
||||
tag: string;
|
||||
method: string;
|
||||
}[] = [];
|
||||
|
||||
for (const [_path, methods] of Object.entries(spec.paths)) {
|
||||
for (const [method, operation] of Object.entries(methods)) {
|
||||
const opId = operation.operationId;
|
||||
if (!opId) continue;
|
||||
if (operation.deprecated) continue;
|
||||
if (excludeSet?.has(opId)) continue;
|
||||
if (idSet && !idSet.has(opId)) continue;
|
||||
if (tagSet && !operation.tags?.some((t) => tagSet.has(t))) continue;
|
||||
|
||||
summary.push({
|
||||
name: opId,
|
||||
description:
|
||||
[operation.summary, operation.description]
|
||||
.filter(Boolean)
|
||||
.join(". ") || "",
|
||||
tag: operation.tags?.[0] ?? "default",
|
||||
method: method.toUpperCase(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return summary;
|
||||
}
|
||||
250
packages/server/src/utils/ai/tool-retrieval.ts
Normal file
250
packages/server/src/utils/ai/tool-retrieval.ts
Normal file
@@ -0,0 +1,250 @@
|
||||
import { readFileSync, writeFileSync, existsSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { PROPERTY_HINTS } from "./api-tool";
|
||||
|
||||
interface EndpointEmbedding {
|
||||
operationId: string;
|
||||
text: string;
|
||||
tags: string[];
|
||||
embedding: number[];
|
||||
}
|
||||
|
||||
const VOYAGE_MODEL = "voyage-3-lite";
|
||||
const VOYAGE_API = "https://api.voyageai.com/v1/embeddings";
|
||||
const BATCH_SIZE = 128;
|
||||
|
||||
/**
|
||||
* Call Voyage AI to embed an array of texts.
|
||||
*/
|
||||
async function embedTexts(
|
||||
texts: string[],
|
||||
apiKey: string,
|
||||
inputType: "document" | "query" = "document",
|
||||
): Promise<number[][]> {
|
||||
const results: number[][] = [];
|
||||
|
||||
for (let i = 0; i < texts.length; i += BATCH_SIZE) {
|
||||
const batch = texts.slice(i, i + BATCH_SIZE);
|
||||
const response = await fetch(VOYAGE_API, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${apiKey}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model: VOYAGE_MODEL,
|
||||
input: batch,
|
||||
input_type: inputType,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
`Voyage API error: ${response.status} ${await response.text()}`,
|
||||
);
|
||||
}
|
||||
|
||||
const data = (await response.json()) as {
|
||||
data: { embedding: number[] }[];
|
||||
};
|
||||
for (const item of data.data) {
|
||||
results.push(item.embedding);
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cosine similarity between two vectors.
|
||||
*/
|
||||
function cosineSimilarity(a: number[], b: number[]): number {
|
||||
let dot = 0;
|
||||
let normA = 0;
|
||||
let normB = 0;
|
||||
for (let i = 0; i < a.length; i++) {
|
||||
dot += a[i]! * b[i]!;
|
||||
normA += a[i]! * a[i]!;
|
||||
normB += b[i]! * b[i]!;
|
||||
}
|
||||
return dot / (Math.sqrt(normA) * Math.sqrt(normB));
|
||||
}
|
||||
|
||||
// In-memory cache
|
||||
let cachedEmbeddings: EndpointEmbedding[] | null = null;
|
||||
|
||||
/**
|
||||
* Extract enum values from a JSON Schema property (handles anyOf wrappers).
|
||||
*/
|
||||
function extractEnum(prop: any): string[] | null {
|
||||
if (prop?.enum) return prop.enum;
|
||||
if (Array.isArray(prop?.anyOf)) {
|
||||
for (const variant of prop.anyOf) {
|
||||
if (variant?.enum) return variant.enum;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a rich text representation for an endpoint (used for embedding).
|
||||
* Includes: operationId, method, path, params with enums, summary, description.
|
||||
*/
|
||||
function buildEndpointText(
|
||||
op: any,
|
||||
method: string,
|
||||
path: string,
|
||||
): string {
|
||||
const parts: string[] = [];
|
||||
|
||||
// Operation identity
|
||||
parts.push(`${op.operationId} [${method.toUpperCase()} ${path}]`);
|
||||
|
||||
// Tags
|
||||
if (op.tags?.length) {
|
||||
parts.push(`Tags: ${op.tags.join(", ")}`);
|
||||
}
|
||||
|
||||
// Summary + description
|
||||
if (op.summary) parts.push(op.summary);
|
||||
if (op.description) parts.push(op.description);
|
||||
|
||||
// Parameters
|
||||
const params: string[] = [];
|
||||
if (op.parameters) {
|
||||
for (const p of op.parameters) {
|
||||
if (p.in === "header") continue;
|
||||
const req = p.required ? "required" : "optional";
|
||||
params.push(`${p.name} (${req})`);
|
||||
}
|
||||
}
|
||||
|
||||
if (op.requestBody?.content?.["application/json"]?.schema) {
|
||||
const schema = op.requestBody.content["application/json"].schema;
|
||||
const requiredSet = new Set(schema.required ?? []);
|
||||
if (schema.properties) {
|
||||
for (const [key, prop] of Object.entries(
|
||||
schema.properties as Record<string, any>,
|
||||
)) {
|
||||
const req = requiredSet.has(key) ? "required" : "optional";
|
||||
const enumVals = extractEnum(prop);
|
||||
const hint = PROPERTY_HINTS[key];
|
||||
const extra = enumVals
|
||||
? ` [${enumVals.join("|")}]`
|
||||
: hint
|
||||
? ` — ${hint}`
|
||||
: "";
|
||||
params.push(`${key} (${req})${extra}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (params.length > 0) {
|
||||
parts.push(`Parameters: ${params.join(", ")}`);
|
||||
}
|
||||
|
||||
return parts.join(". ");
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate or load embeddings for all endpoints in the OpenAPI spec.
|
||||
* Embeddings are cached in .tool-embeddings.json and in memory.
|
||||
*/
|
||||
export async function getOrCreateEmbeddings(
|
||||
spec: any,
|
||||
voyageApiKey: string,
|
||||
cachePath?: string,
|
||||
): Promise<EndpointEmbedding[]> {
|
||||
// Return from memory cache
|
||||
if (cachedEmbeddings) return cachedEmbeddings;
|
||||
|
||||
// Try loading from file cache
|
||||
const filePath =
|
||||
cachePath || join(process.cwd(), ".tool-embeddings.json");
|
||||
|
||||
if (existsSync(filePath)) {
|
||||
try {
|
||||
const data = JSON.parse(readFileSync(filePath, "utf-8"));
|
||||
if (Array.isArray(data) && data.length > 0 && data[0].embedding) {
|
||||
cachedEmbeddings = data;
|
||||
return cachedEmbeddings;
|
||||
}
|
||||
} catch {
|
||||
// Corrupted file — regenerate
|
||||
}
|
||||
}
|
||||
|
||||
// Generate embeddings from spec
|
||||
const endpoints: { operationId: string; text: string; tags: string[] }[] =
|
||||
[];
|
||||
|
||||
for (const [path, methods] of Object.entries(spec.paths ?? {})) {
|
||||
for (const [method, op] of Object.entries(methods as Record<string, any>)) {
|
||||
if (!op.operationId || op.deprecated) continue;
|
||||
endpoints.push({
|
||||
operationId: op.operationId,
|
||||
text: buildEndpointText(op, method, path),
|
||||
tags: op.tags ?? [],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (endpoints.length === 0) {
|
||||
cachedEmbeddings = [];
|
||||
return cachedEmbeddings;
|
||||
}
|
||||
|
||||
const texts = endpoints.map((e) => e.text);
|
||||
const embeddings = await embedTexts(texts, voyageApiKey, "document");
|
||||
|
||||
cachedEmbeddings = endpoints.map((e, i) => ({
|
||||
...e,
|
||||
embedding: embeddings[i]!,
|
||||
}));
|
||||
|
||||
// Persist to file
|
||||
try {
|
||||
writeFileSync(filePath, JSON.stringify(cachedEmbeddings));
|
||||
} catch {
|
||||
// Non-critical — will regenerate next time
|
||||
}
|
||||
|
||||
return cachedEmbeddings;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the top-K most relevant endpoints for a user query,
|
||||
* optionally filtered to a pre-computed set of allowed operationIds.
|
||||
*/
|
||||
export async function retrieveRelevantEndpoints(
|
||||
query: string,
|
||||
allEmbeddings: EndpointEmbedding[],
|
||||
voyageApiKey: string,
|
||||
options?: {
|
||||
allowedOperationIds?: Set<string>;
|
||||
topK?: number;
|
||||
},
|
||||
): Promise<string[]> {
|
||||
const { allowedOperationIds, topK = 20 } = options ?? {};
|
||||
|
||||
// Filter to allowed operationIds (from tag filtering)
|
||||
const candidates = allowedOperationIds
|
||||
? allEmbeddings.filter((e) => allowedOperationIds.has(e.operationId))
|
||||
: allEmbeddings;
|
||||
|
||||
if (candidates.length === 0) return [];
|
||||
|
||||
// Embed the user query
|
||||
const [queryEmbedding] = await embedTexts([query], voyageApiKey, "query");
|
||||
if (!queryEmbedding) return [];
|
||||
|
||||
// Score and rank
|
||||
const scored = candidates.map((e) => ({
|
||||
operationId: e.operationId,
|
||||
score: cosineSimilarity(queryEmbedding, e.embedding),
|
||||
}));
|
||||
|
||||
scored.sort((a, b) => b.score - a.score);
|
||||
|
||||
return scored.slice(0, topK).map((s) => s.operationId);
|
||||
}
|
||||
142
pnpm-lock.yaml
generated
142
pnpm-lock.yaml
generated
@@ -113,12 +113,15 @@ importers:
|
||||
'@ai-sdk/openai-compatible':
|
||||
specifier: ^2.0.30
|
||||
version: 2.0.30(zod@4.3.6)
|
||||
'@ai-sdk/react':
|
||||
specifier: ^3.0.156
|
||||
version: 3.0.156(react@18.2.0)(zod@4.3.6)
|
||||
'@better-auth/api-key':
|
||||
specifier: 1.5.4
|
||||
version: 1.5.4(@better-auth/core@1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@2.0.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.1))(@better-auth/utils@0.3.1)(better-auth@1.5.4(febde88eaf587188179e6ecc47119e50))
|
||||
version: 1.5.4(@better-auth/core@1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@2.0.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.1))(@better-auth/utils@0.3.1)(better-auth@1.5.4(219be630f7f6fef2e235cef94eaddc25))
|
||||
'@better-auth/sso':
|
||||
specifier: 1.5.4
|
||||
version: 1.5.4(@better-auth/core@1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@2.0.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.1))(@better-auth/utils@0.3.1)(better-auth@1.5.4(febde88eaf587188179e6ecc47119e50))(better-call@2.0.2(zod@4.3.6))
|
||||
version: 1.5.4(@better-auth/core@1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@2.0.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.1))(@better-auth/utils@0.3.1)(better-auth@1.5.4(219be630f7f6fef2e235cef94eaddc25))(better-call@2.0.2(zod@4.3.6))
|
||||
'@codemirror/autocomplete':
|
||||
specifier: ^6.18.6
|
||||
version: 6.20.0
|
||||
@@ -147,8 +150,8 @@ importers:
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/server
|
||||
'@dokploy/trpc-openapi':
|
||||
specifier: 0.0.18
|
||||
version: 0.0.18(@trpc/server@11.10.0(typescript@5.9.3))(zod-openapi@5.4.6(zod@4.3.6))(zod@4.3.6)
|
||||
specifier: 0.0.19
|
||||
version: 0.0.19(@trpc/server@11.10.0(typescript@5.9.3))(zod-openapi@5.4.6(zod@4.3.6))(zod@4.3.6)
|
||||
'@faker-js/faker':
|
||||
specifier: ^8.4.1
|
||||
version: 8.4.1
|
||||
@@ -277,7 +280,7 @@ importers:
|
||||
version: 5.1.1
|
||||
better-auth:
|
||||
specifier: 1.5.4
|
||||
version: 1.5.4(febde88eaf587188179e6ecc47119e50)
|
||||
version: 1.5.4(219be630f7f6fef2e235cef94eaddc25)
|
||||
bl:
|
||||
specifier: 6.0.11
|
||||
version: 6.0.11
|
||||
@@ -536,10 +539,10 @@ importers:
|
||||
version: 5.9.3
|
||||
vite-tsconfig-paths:
|
||||
specifier: 4.3.2
|
||||
version: 4.3.2(typescript@5.9.3)(vite@7.3.1(@types/node@24.10.13)(jiti@1.21.7)(tsx@4.16.2)(yaml@2.8.1))
|
||||
version: 4.3.2(typescript@5.9.3)(vite@7.3.1(@types/node@24.10.13)(jiti@2.6.1)(tsx@4.16.2)(yaml@2.8.1))
|
||||
vitest:
|
||||
specifier: ^4.0.18
|
||||
version: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@24.10.13)(jiti@1.21.7)(tsx@4.16.2)(yaml@2.8.1)
|
||||
version: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@24.10.13)(jiti@2.6.1)(tsx@4.16.2)(yaml@2.8.1)
|
||||
|
||||
apps/schedules:
|
||||
dependencies:
|
||||
@@ -627,10 +630,10 @@ importers:
|
||||
version: 2.0.30(zod@4.3.6)
|
||||
'@better-auth/api-key':
|
||||
specifier: 1.5.4
|
||||
version: 1.5.4(@better-auth/core@1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@2.0.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.1))(@better-auth/utils@0.3.1)(better-auth@1.5.4(334901c35c1fcda64bb596793b2e4934))
|
||||
version: 1.5.4(@better-auth/core@1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@2.0.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.1))(@better-auth/utils@0.3.1)(better-auth@1.5.4(8933545d763d3f096150f97f9213a424))
|
||||
'@better-auth/sso':
|
||||
specifier: 1.5.4
|
||||
version: 1.5.4(@better-auth/core@1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@2.0.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.1))(@better-auth/utils@0.3.1)(better-auth@1.5.4(334901c35c1fcda64bb596793b2e4934))(better-call@2.0.2(zod@4.3.6))
|
||||
version: 1.5.4(@better-auth/core@1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@2.0.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.1))(@better-auth/utils@0.3.1)(better-auth@1.5.4(8933545d763d3f096150f97f9213a424))(better-call@2.0.2(zod@4.3.6))
|
||||
'@better-auth/utils':
|
||||
specifier: 0.3.1
|
||||
version: 0.3.1
|
||||
@@ -669,7 +672,7 @@ importers:
|
||||
version: 5.1.1
|
||||
better-auth:
|
||||
specifier: 1.5.4
|
||||
version: 1.5.4(334901c35c1fcda64bb596793b2e4934)
|
||||
version: 1.5.4(8933545d763d3f096150f97f9213a424)
|
||||
better-call:
|
||||
specifier: 2.0.2
|
||||
version: 2.0.2(zod@4.3.6)
|
||||
@@ -772,7 +775,7 @@ importers:
|
||||
devDependencies:
|
||||
'@better-auth/cli':
|
||||
specifier: 1.4.21
|
||||
version: 1.4.21(@better-fetch/fetch@1.1.21)(@electric-sql/pglite@0.3.15)(@opentelemetry/api@1.9.0)(better-call@2.0.2(zod@4.3.6))(drizzle-kit@0.31.9)(jose@6.1.3)(kysely@0.28.11)(mongodb@7.1.0)(mysql2@3.15.3)(nanostores@1.1.1)(next@16.2.0(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(postgres@3.4.4)(prisma@7.4.1(@types/react@18.3.5)(better-sqlite3@12.6.2)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3))(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(vitest@4.0.18(@opentelemetry/api@1.9.0)(@types/node@24.10.13)(jiti@2.6.1)(tsx@4.16.2)(yaml@2.8.1))
|
||||
version: 1.4.21(@better-fetch/fetch@1.1.21)(@electric-sql/pglite@0.3.15)(@opentelemetry/api@1.9.0)(better-call@2.0.2(zod@4.3.6))(drizzle-kit@0.31.9)(jose@6.1.3)(kysely@0.28.11)(mongodb@7.1.0)(mysql2@3.15.3)(nanostores@1.1.1)(next@16.2.0(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(postgres@3.4.4)(prisma@7.4.1(@types/react@18.3.5)(better-sqlite3@12.6.2)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3))(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(vitest@4.0.18(@opentelemetry/api@1.9.0)(@types/node@24.10.13)(jiti@1.21.7)(tsx@4.16.2)(yaml@2.8.1))
|
||||
'@types/adm-zip':
|
||||
specifier: ^0.5.7
|
||||
version: 0.5.7
|
||||
@@ -878,6 +881,12 @@ packages:
|
||||
peerDependencies:
|
||||
zod: ^3.25.76 || ^4.1.8
|
||||
|
||||
'@ai-sdk/gateway@3.0.94':
|
||||
resolution: {integrity: sha512-uDDwLZhCkvC89crVS3S90D5L7AcVN8WriGuYVNYgVAaVcvy3Mthy3R9ICfzG75BObhz6pm2FWnhxDfNRK+t69Q==}
|
||||
engines: {node: '>=18'}
|
||||
peerDependencies:
|
||||
zod: ^3.25.76 || ^4.1.8
|
||||
|
||||
'@ai-sdk/mistral@3.0.20':
|
||||
resolution: {integrity: sha512-oZcx2pE6nJ+Qj/U6HFV5mJ52jXJPBSpvki/NtIocZkI/rKxphKBaecOH1h0Y7yK3HIbBxsMqefB1pb72cAHGVg==}
|
||||
engines: {node: '>=18'}
|
||||
@@ -902,10 +911,22 @@ packages:
|
||||
peerDependencies:
|
||||
zod: ^3.25.76 || ^4.1.8
|
||||
|
||||
'@ai-sdk/provider-utils@4.0.23':
|
||||
resolution: {integrity: sha512-z8GlDaCmRSDlqkMF2f4/RFgWxdarvIbyuk+m6WXT1LYgsnGiXRJGTD2Z1+SDl3LqtFuRtGX1aghYvQLoHL/9pg==}
|
||||
engines: {node: '>=18'}
|
||||
peerDependencies:
|
||||
zod: ^3.25.76 || ^4.1.8
|
||||
|
||||
'@ai-sdk/provider@3.0.8':
|
||||
resolution: {integrity: sha512-oGMAgGoQdBXbZqNG0Ze56CHjDZ1IDYOwGYxYjO5KLSlz5HiNQ9udIXsPZ61VWaHGZ5XW/jyjmr6t2xz2jGVwbQ==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
'@ai-sdk/react@3.0.156':
|
||||
resolution: {integrity: sha512-/6rmGxOJlCNS6wJBUNsO49aeSK740fS2wVcA3Xn8IOBRFFz3hWm6auQTMoA0nHKu4hnH6ivA6hog6Ul+1Bv4Rg==}
|
||||
engines: {node: '>=18'}
|
||||
peerDependencies:
|
||||
react: ^18 || ~19.0.1 || ~19.1.2 || ^19.2.1
|
||||
|
||||
'@alloc/quick-lru@5.2.0':
|
||||
resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==}
|
||||
engines: {node: '>=10'}
|
||||
@@ -1291,8 +1312,8 @@ packages:
|
||||
'@codemirror/view@6.39.15':
|
||||
resolution: {integrity: sha512-aCWjgweIIXLBHh7bY6cACvXuyrZ0xGafjQ2VInjp4RM4gMfscK5uESiNdrH0pE+e1lZr2B4ONGsjchl2KsKZzg==}
|
||||
|
||||
'@dokploy/trpc-openapi@0.0.18':
|
||||
resolution: {integrity: sha512-CbppvUEe8eK1fiNGQL5AH8KIRRlHk5bGPUEIyc2VBZE0un4kfUs5DXKSKsMLDomoES5ZEdrjT4nKpwYvhDha0w==}
|
||||
'@dokploy/trpc-openapi@0.0.19':
|
||||
resolution: {integrity: sha512-pmajIu1tIU3yqqbYdRHKFbA8gP37gO7F61PL9AFNtoyhh6gVxALXQLDabtE7dobKAhNDmIj6vV1a2vyP1Zi7/w==}
|
||||
peerDependencies:
|
||||
'@trpc/server': ^11.1.0
|
||||
zod: ^4.3.6
|
||||
@@ -4241,6 +4262,12 @@ packages:
|
||||
peerDependencies:
|
||||
ai: ^6.0.89
|
||||
|
||||
ai@6.0.154:
|
||||
resolution: {integrity: sha512-HfKJKCTJsDZxqrIUDSVnBQ7DpQlx5WI4ExqtLd7Bl70epLmvkpc/HYMzU1hP9W+g9VEAcvZo4fbMqc3v5D+9gQ==}
|
||||
engines: {node: '>=18'}
|
||||
peerDependencies:
|
||||
zod: ^3.25.76 || ^4.1.8
|
||||
|
||||
ai@6.0.97:
|
||||
resolution: {integrity: sha512-eZIAcBymwGhBwncRH/v9pillZNMeRCDkc4BwcvwXerXd7sxjVxRis3ZNCNCpP02pVH4NLs81ljm4cElC4vbNcQ==}
|
||||
engines: {node: '>=18'}
|
||||
@@ -7707,6 +7734,11 @@ packages:
|
||||
react: '>=16.8.0 <20'
|
||||
react-dom: '>=16.8.0 <20'
|
||||
|
||||
swr@2.4.1:
|
||||
resolution: {integrity: sha512-2CC6CiKQtEwaEeNiqWTAw9PGykW8SR5zZX8MZk6TeAvEAnVS7Visz8WzphqgtQ8v2xz/4Q5K+j+SeMaKXeeQIA==}
|
||||
peerDependencies:
|
||||
react: ^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||
|
||||
symbol-observable@1.2.0:
|
||||
resolution: {integrity: sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
@@ -7764,6 +7796,10 @@ packages:
|
||||
thread-stream@3.1.0:
|
||||
resolution: {integrity: sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A==}
|
||||
|
||||
throttleit@2.1.0:
|
||||
resolution: {integrity: sha512-nt6AMGKW1p/70DF/hGBdJB57B8Tspmbp5gfJ8ilhLnt7kkr2ye7hzD6NVG8GGErk2HWF34igrL2CXmNIkzKqKw==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
tiny-invariant@1.3.3:
|
||||
resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==}
|
||||
|
||||
@@ -8329,6 +8365,13 @@ snapshots:
|
||||
'@vercel/oidc': 3.1.0
|
||||
zod: 4.3.6
|
||||
|
||||
'@ai-sdk/gateway@3.0.94(zod@4.3.6)':
|
||||
dependencies:
|
||||
'@ai-sdk/provider': 3.0.8
|
||||
'@ai-sdk/provider-utils': 4.0.23(zod@4.3.6)
|
||||
'@vercel/oidc': 3.1.0
|
||||
zod: 4.3.6
|
||||
|
||||
'@ai-sdk/mistral@3.0.20(zod@4.3.6)':
|
||||
dependencies:
|
||||
'@ai-sdk/provider': 3.0.8
|
||||
@@ -8354,10 +8397,27 @@ snapshots:
|
||||
eventsource-parser: 3.0.6
|
||||
zod: 4.3.6
|
||||
|
||||
'@ai-sdk/provider-utils@4.0.23(zod@4.3.6)':
|
||||
dependencies:
|
||||
'@ai-sdk/provider': 3.0.8
|
||||
'@standard-schema/spec': 1.1.0
|
||||
eventsource-parser: 3.0.6
|
||||
zod: 4.3.6
|
||||
|
||||
'@ai-sdk/provider@3.0.8':
|
||||
dependencies:
|
||||
json-schema: 0.4.0
|
||||
|
||||
'@ai-sdk/react@3.0.156(react@18.2.0)(zod@4.3.6)':
|
||||
dependencies:
|
||||
'@ai-sdk/provider-utils': 4.0.23(zod@4.3.6)
|
||||
ai: 6.0.154(zod@4.3.6)
|
||||
react: 18.2.0
|
||||
swr: 2.4.1(react@18.2.0)
|
||||
throttleit: 2.1.0
|
||||
transitivePeerDependencies:
|
||||
- zod
|
||||
|
||||
'@alloc/quick-lru@5.2.0': {}
|
||||
|
||||
'@authenio/xml-encryption@2.0.2':
|
||||
@@ -8601,21 +8661,21 @@ snapshots:
|
||||
|
||||
'@balena/dockerignore@1.0.2': {}
|
||||
|
||||
'@better-auth/api-key@1.5.4(@better-auth/core@1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@2.0.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.1))(@better-auth/utils@0.3.1)(better-auth@1.5.4(334901c35c1fcda64bb596793b2e4934))':
|
||||
'@better-auth/api-key@1.5.4(@better-auth/core@1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@2.0.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.1))(@better-auth/utils@0.3.1)(better-auth@1.5.4(219be630f7f6fef2e235cef94eaddc25))':
|
||||
dependencies:
|
||||
'@better-auth/core': 1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@2.0.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.1)
|
||||
'@better-auth/utils': 0.3.1
|
||||
better-auth: 1.5.4(334901c35c1fcda64bb596793b2e4934)
|
||||
better-auth: 1.5.4(219be630f7f6fef2e235cef94eaddc25)
|
||||
zod: 4.3.6
|
||||
|
||||
'@better-auth/api-key@1.5.4(@better-auth/core@1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@2.0.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.1))(@better-auth/utils@0.3.1)(better-auth@1.5.4(febde88eaf587188179e6ecc47119e50))':
|
||||
'@better-auth/api-key@1.5.4(@better-auth/core@1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@2.0.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.1))(@better-auth/utils@0.3.1)(better-auth@1.5.4(8933545d763d3f096150f97f9213a424))':
|
||||
dependencies:
|
||||
'@better-auth/core': 1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@2.0.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.1)
|
||||
'@better-auth/utils': 0.3.1
|
||||
better-auth: 1.5.4(febde88eaf587188179e6ecc47119e50)
|
||||
better-auth: 1.5.4(8933545d763d3f096150f97f9213a424)
|
||||
zod: 4.3.6
|
||||
|
||||
'@better-auth/cli@1.4.21(@better-fetch/fetch@1.1.21)(@electric-sql/pglite@0.3.15)(@opentelemetry/api@1.9.0)(better-call@2.0.2(zod@4.3.6))(drizzle-kit@0.31.9)(jose@6.1.3)(kysely@0.28.11)(mongodb@7.1.0)(mysql2@3.15.3)(nanostores@1.1.1)(next@16.2.0(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(postgres@3.4.4)(prisma@7.4.1(@types/react@18.3.5)(better-sqlite3@12.6.2)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3))(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(vitest@4.0.18(@opentelemetry/api@1.9.0)(@types/node@24.10.13)(jiti@2.6.1)(tsx@4.16.2)(yaml@2.8.1))':
|
||||
'@better-auth/cli@1.4.21(@better-fetch/fetch@1.1.21)(@electric-sql/pglite@0.3.15)(@opentelemetry/api@1.9.0)(better-call@2.0.2(zod@4.3.6))(drizzle-kit@0.31.9)(jose@6.1.3)(kysely@0.28.11)(mongodb@7.1.0)(mysql2@3.15.3)(nanostores@1.1.1)(next@16.2.0(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(postgres@3.4.4)(prisma@7.4.1(@types/react@18.3.5)(better-sqlite3@12.6.2)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3))(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(vitest@4.0.18(@opentelemetry/api@1.9.0)(@types/node@24.10.13)(jiti@1.21.7)(tsx@4.16.2)(yaml@2.8.1))':
|
||||
dependencies:
|
||||
'@babel/core': 7.29.0
|
||||
'@babel/preset-react': 7.28.5(@babel/core@7.29.0)
|
||||
@@ -8627,7 +8687,7 @@ snapshots:
|
||||
'@mrleebo/prisma-ast': 0.13.1
|
||||
'@prisma/client': 5.22.0(prisma@7.4.1(@types/react@18.3.5)(better-sqlite3@12.6.2)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3))
|
||||
'@types/pg': 8.16.0
|
||||
better-auth: 1.4.21(b1bc00b9e18c5e6af4e13b03fc4304ac)
|
||||
better-auth: 1.4.21(db78b83f9b5449d160708cdf9d272aa3)
|
||||
better-sqlite3: 12.6.2
|
||||
c12: 3.3.3
|
||||
chalk: 5.6.2
|
||||
@@ -8761,24 +8821,24 @@ snapshots:
|
||||
'@prisma/client': 5.22.0(prisma@7.4.1(@types/react@18.3.5)(better-sqlite3@12.6.2)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3))
|
||||
prisma: 7.4.1(@types/react@18.3.5)(better-sqlite3@12.6.2)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3)
|
||||
|
||||
'@better-auth/sso@1.5.4(@better-auth/core@1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@2.0.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.1))(@better-auth/utils@0.3.1)(better-auth@1.5.4(334901c35c1fcda64bb596793b2e4934))(better-call@2.0.2(zod@4.3.6))':
|
||||
'@better-auth/sso@1.5.4(@better-auth/core@1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@2.0.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.1))(@better-auth/utils@0.3.1)(better-auth@1.5.4(219be630f7f6fef2e235cef94eaddc25))(better-call@2.0.2(zod@4.3.6))':
|
||||
dependencies:
|
||||
'@better-auth/core': 1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@2.0.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.1)
|
||||
'@better-auth/utils': 0.3.1
|
||||
'@better-fetch/fetch': 1.1.21
|
||||
better-auth: 1.5.4(334901c35c1fcda64bb596793b2e4934)
|
||||
better-auth: 1.5.4(219be630f7f6fef2e235cef94eaddc25)
|
||||
better-call: 2.0.2(zod@4.3.6)
|
||||
fast-xml-parser: 5.5.1
|
||||
jose: 6.1.3
|
||||
samlify: 2.10.2
|
||||
zod: 4.3.6
|
||||
|
||||
'@better-auth/sso@1.5.4(@better-auth/core@1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@2.0.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.1))(@better-auth/utils@0.3.1)(better-auth@1.5.4(febde88eaf587188179e6ecc47119e50))(better-call@2.0.2(zod@4.3.6))':
|
||||
'@better-auth/sso@1.5.4(@better-auth/core@1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@2.0.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.1))(@better-auth/utils@0.3.1)(better-auth@1.5.4(8933545d763d3f096150f97f9213a424))(better-call@2.0.2(zod@4.3.6))':
|
||||
dependencies:
|
||||
'@better-auth/core': 1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@2.0.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.1)
|
||||
'@better-auth/utils': 0.3.1
|
||||
'@better-fetch/fetch': 1.1.21
|
||||
better-auth: 1.5.4(febde88eaf587188179e6ecc47119e50)
|
||||
better-auth: 1.5.4(8933545d763d3f096150f97f9213a424)
|
||||
better-call: 2.0.2(zod@4.3.6)
|
||||
fast-xml-parser: 5.5.1
|
||||
jose: 6.1.3
|
||||
@@ -8946,7 +9006,7 @@ snapshots:
|
||||
style-mod: 4.1.3
|
||||
w3c-keyname: 2.2.8
|
||||
|
||||
'@dokploy/trpc-openapi@0.0.18(@trpc/server@11.10.0(typescript@5.9.3))(zod-openapi@5.4.6(zod@4.3.6))(zod@4.3.6)':
|
||||
'@dokploy/trpc-openapi@0.0.19(@trpc/server@11.10.0(typescript@5.9.3))(zod-openapi@5.4.6(zod@4.3.6))(zod@4.3.6)':
|
||||
dependencies:
|
||||
'@trpc/server': 11.10.0(typescript@5.9.3)
|
||||
co-body: 6.2.0
|
||||
@@ -12244,6 +12304,7 @@ snapshots:
|
||||
magic-string: 0.30.21
|
||||
optionalDependencies:
|
||||
vite: 7.3.1(@types/node@24.10.13)(jiti@1.21.7)(tsx@4.16.2)(yaml@2.8.1)
|
||||
optional: true
|
||||
|
||||
'@vitest/mocker@4.0.18(vite@7.3.1(@types/node@24.10.13)(jiti@2.6.1)(tsx@4.16.2)(yaml@2.8.1))':
|
||||
dependencies:
|
||||
@@ -12252,7 +12313,6 @@ snapshots:
|
||||
magic-string: 0.30.21
|
||||
optionalDependencies:
|
||||
vite: 7.3.1(@types/node@24.10.13)(jiti@2.6.1)(tsx@4.16.2)(yaml@2.8.1)
|
||||
optional: true
|
||||
|
||||
'@vitest/pretty-format@4.0.18':
|
||||
dependencies:
|
||||
@@ -12337,6 +12397,14 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- zod
|
||||
|
||||
ai@6.0.154(zod@4.3.6):
|
||||
dependencies:
|
||||
'@ai-sdk/gateway': 3.0.94(zod@4.3.6)
|
||||
'@ai-sdk/provider': 3.0.8
|
||||
'@ai-sdk/provider-utils': 4.0.23(zod@4.3.6)
|
||||
'@opentelemetry/api': 1.9.0
|
||||
zod: 4.3.6
|
||||
|
||||
ai@6.0.97(zod@4.3.6):
|
||||
dependencies:
|
||||
'@ai-sdk/gateway': 3.0.53(zod@4.3.6)
|
||||
@@ -12457,7 +12525,7 @@ snapshots:
|
||||
|
||||
before-after-hook@2.2.3: {}
|
||||
|
||||
better-auth@1.4.21(b1bc00b9e18c5e6af4e13b03fc4304ac):
|
||||
better-auth@1.4.21(db78b83f9b5449d160708cdf9d272aa3):
|
||||
dependencies:
|
||||
'@better-auth/core': 1.4.21(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.21)(better-call@1.1.8(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.0)
|
||||
'@better-auth/telemetry': 1.4.21(@better-auth/core@1.4.21(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.21)(better-call@2.0.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.1))
|
||||
@@ -12483,9 +12551,9 @@ snapshots:
|
||||
prisma: 7.4.1(@types/react@18.3.5)(better-sqlite3@12.6.2)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3)
|
||||
react: 18.2.0
|
||||
react-dom: 18.2.0(react@18.2.0)
|
||||
vitest: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@24.10.13)(jiti@2.6.1)(tsx@4.16.2)(yaml@2.8.1)
|
||||
vitest: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@24.10.13)(jiti@1.21.7)(tsx@4.16.2)(yaml@2.8.1)
|
||||
|
||||
better-auth@1.5.4(334901c35c1fcda64bb596793b2e4934):
|
||||
better-auth@1.5.4(219be630f7f6fef2e235cef94eaddc25):
|
||||
dependencies:
|
||||
'@better-auth/core': 1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@1.3.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.1)
|
||||
'@better-auth/drizzle-adapter': 1.5.4(@better-auth/core@1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@2.0.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.1))(@better-auth/utils@0.3.1)(drizzle-orm@0.45.1(@electric-sql/pglite@0.3.15)(@opentelemetry/api@1.9.0)(@prisma/client@5.22.0(prisma@7.4.1(@types/react@18.3.5)(better-sqlite3@12.6.2)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3)))(@types/pg@8.16.0)(better-sqlite3@12.6.2)(kysely@0.28.11)(mysql2@3.15.3)(pg@8.18.0)(postgres@3.4.4)(prisma@7.4.1(@types/react@18.3.5)(better-sqlite3@12.6.2)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3)))
|
||||
@@ -12520,7 +12588,7 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- '@cloudflare/workers-types'
|
||||
|
||||
better-auth@1.5.4(febde88eaf587188179e6ecc47119e50):
|
||||
better-auth@1.5.4(8933545d763d3f096150f97f9213a424):
|
||||
dependencies:
|
||||
'@better-auth/core': 1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@1.3.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.1)
|
||||
'@better-auth/drizzle-adapter': 1.5.4(@better-auth/core@1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@2.0.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.1))(@better-auth/utils@0.3.1)(drizzle-orm@0.45.1(@electric-sql/pglite@0.3.15)(@opentelemetry/api@1.9.0)(@prisma/client@5.22.0(prisma@7.4.1(@types/react@18.3.5)(better-sqlite3@12.6.2)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3)))(@types/pg@8.16.0)(better-sqlite3@12.6.2)(kysely@0.28.11)(mysql2@3.15.3)(pg@8.18.0)(postgres@3.4.4)(prisma@7.4.1(@types/react@18.3.5)(better-sqlite3@12.6.2)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3)))
|
||||
@@ -15935,6 +16003,12 @@ snapshots:
|
||||
- '@types/react'
|
||||
- debug
|
||||
|
||||
swr@2.4.1(react@18.2.0):
|
||||
dependencies:
|
||||
dequal: 2.0.3
|
||||
react: 18.2.0
|
||||
use-sync-external-store: 1.6.0(react@18.2.0)
|
||||
|
||||
symbol-observable@1.2.0: {}
|
||||
|
||||
tailwind-merge@2.6.1: {}
|
||||
@@ -16025,6 +16099,8 @@ snapshots:
|
||||
dependencies:
|
||||
real-require: 0.2.0
|
||||
|
||||
throttleit@2.1.0: {}
|
||||
|
||||
tiny-invariant@1.3.3: {}
|
||||
|
||||
tiny-warning@1.0.3: {}
|
||||
@@ -16278,13 +16354,13 @@ snapshots:
|
||||
d3-time: 3.1.0
|
||||
d3-timer: 3.0.1
|
||||
|
||||
vite-tsconfig-paths@4.3.2(typescript@5.9.3)(vite@7.3.1(@types/node@24.10.13)(jiti@1.21.7)(tsx@4.16.2)(yaml@2.8.1)):
|
||||
vite-tsconfig-paths@4.3.2(typescript@5.9.3)(vite@7.3.1(@types/node@24.10.13)(jiti@2.6.1)(tsx@4.16.2)(yaml@2.8.1)):
|
||||
dependencies:
|
||||
debug: 4.4.3
|
||||
globrex: 0.1.2
|
||||
tsconfck: 3.1.6(typescript@5.9.3)
|
||||
optionalDependencies:
|
||||
vite: 7.3.1(@types/node@24.10.13)(jiti@1.21.7)(tsx@4.16.2)(yaml@2.8.1)
|
||||
vite: 7.3.1(@types/node@24.10.13)(jiti@2.6.1)(tsx@4.16.2)(yaml@2.8.1)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
- typescript
|
||||
@@ -16303,6 +16379,7 @@ snapshots:
|
||||
jiti: 1.21.7
|
||||
tsx: 4.16.2
|
||||
yaml: 2.8.1
|
||||
optional: true
|
||||
|
||||
vite@7.3.1(@types/node@24.10.13)(jiti@2.6.1)(tsx@4.16.2)(yaml@2.8.1):
|
||||
dependencies:
|
||||
@@ -16318,7 +16395,6 @@ snapshots:
|
||||
jiti: 2.6.1
|
||||
tsx: 4.16.2
|
||||
yaml: 2.8.1
|
||||
optional: true
|
||||
|
||||
vitest@4.0.18(@opentelemetry/api@1.9.0)(@types/node@24.10.13)(jiti@1.21.7)(tsx@4.16.2)(yaml@2.8.1):
|
||||
dependencies:
|
||||
@@ -16357,6 +16433,7 @@ snapshots:
|
||||
- terser
|
||||
- tsx
|
||||
- yaml
|
||||
optional: true
|
||||
|
||||
vitest@4.0.18(@opentelemetry/api@1.9.0)(@types/node@24.10.13)(jiti@2.6.1)(tsx@4.16.2)(yaml@2.8.1):
|
||||
dependencies:
|
||||
@@ -16395,7 +16472,6 @@ snapshots:
|
||||
- terser
|
||||
- tsx
|
||||
- yaml
|
||||
optional: true
|
||||
|
||||
w3c-keyname@2.2.8: {}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user