mirror of
https://github.com/Dokploy/dokploy.git
synced 2026-06-17 13:15:23 +02:00
Compare commits
43 Commits
feat/add-c
...
v0.29.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
98a586478e | ||
|
|
13248c8d8a | ||
|
|
54417ca8e7 | ||
|
|
b392e58001 | ||
|
|
d9945c0a4f | ||
|
|
f6e2c033ba | ||
|
|
5c787adae1 | ||
|
|
2ba1df1eaa | ||
|
|
e7859395b1 | ||
|
|
6f0ed89ce7 | ||
|
|
4277a509b2 | ||
|
|
f7b576cbf3 | ||
|
|
425fef6e28 | ||
|
|
958372c5f9 | ||
|
|
e7c581476e | ||
|
|
0cae8330e2 | ||
|
|
4a271c11e7 | ||
|
|
fda367b2c5 | ||
|
|
ea1238b1d1 | ||
|
|
b060f80932 | ||
|
|
04b9f56333 | ||
|
|
599b97da51 | ||
|
|
415298fddb | ||
|
|
ddff8b9de7 | ||
|
|
90f97912a4 | ||
|
|
9af745ce67 | ||
|
|
d99f2cd460 | ||
|
|
d234558822 | ||
|
|
7f25ddca44 | ||
|
|
638b3dd546 | ||
|
|
1a8fd8396d | ||
|
|
385850f354 | ||
|
|
a48306a2c6 | ||
|
|
89737e7b65 | ||
|
|
00c708483e | ||
|
|
ddf570a807 | ||
|
|
f8eb2ba4ba | ||
|
|
9f07f8e9e1 | ||
|
|
3cefa43a21 | ||
|
|
0941ec9f3e | ||
|
|
879218a8b1 | ||
|
|
4ef8c94340 | ||
|
|
ff369c9d3a |
42
.github/workflows/sync-openapi-docs.yml
vendored
42
.github/workflows/sync-openapi-docs.yml
vendored
@@ -68,3 +68,45 @@ jobs:
|
||||
|
||||
echo "✅ OpenAPI synced to website successfully"
|
||||
|
||||
- name: Sync to MCP repository
|
||||
run: |
|
||||
git clone https://x-access-token:${{ secrets.DOCS_SYNC_TOKEN }}@github.com/dokploy/mcp.git mcp-repo
|
||||
|
||||
cd mcp-repo
|
||||
|
||||
cp -f ../openapi.json openapi.json
|
||||
|
||||
git config user.name "Dokploy Bot"
|
||||
git config user.email "bot@dokploy.com"
|
||||
|
||||
git add openapi.json
|
||||
git commit -m "chore: sync OpenAPI specification [skip ci]" \
|
||||
-m "Source: ${{ github.repository }}@${{ github.sha }}" \
|
||||
-m "Updated: $(date -u +'%Y-%m-%d %H:%M:%S UTC')" \
|
||||
--allow-empty
|
||||
|
||||
git push
|
||||
|
||||
echo "✅ OpenAPI synced to MCP repository successfully"
|
||||
|
||||
- name: Sync to CLI repository
|
||||
run: |
|
||||
git clone https://x-access-token:${{ secrets.DOCS_SYNC_TOKEN }}@github.com/dokploy/cli.git cli-repo
|
||||
|
||||
cd cli-repo
|
||||
|
||||
cp -f ../openapi.json openapi.json
|
||||
|
||||
git config user.name "Dokploy Bot"
|
||||
git config user.email "bot@dokploy.com"
|
||||
|
||||
git add openapi.json
|
||||
git commit -m "chore: sync OpenAPI specification [skip ci]" \
|
||||
-m "Source: ${{ github.repository }}@${{ github.sha }}" \
|
||||
-m "Updated: $(date -u +'%Y-%m-%d %H:%M:%S UTC')" \
|
||||
--allow-empty
|
||||
|
||||
git push
|
||||
|
||||
echo "✅ OpenAPI synced to CLI repository successfully"
|
||||
|
||||
|
||||
80
.github/workflows/sync-version.yml
vendored
Normal file
80
.github/workflows/sync-version.yml
vendored
Normal file
@@ -0,0 +1,80 @@
|
||||
name: Sync version to MCP and CLI repos
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
sync-version:
|
||||
name: Sync version to external repos
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout Dokploy repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Get version
|
||||
id: get_version
|
||||
run: |
|
||||
VERSION=$(jq -r .version apps/dokploy/package.json | sed 's/^v//')
|
||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||
echo "Version: $VERSION"
|
||||
|
||||
- name: Sync version to MCP repository
|
||||
run: |
|
||||
git clone https://x-access-token:${{ secrets.DOCS_SYNC_TOKEN }}@github.com/dokploy/mcp.git /tmp/mcp-repo
|
||||
cd /tmp/mcp-repo
|
||||
|
||||
# Regenerate tools from latest OpenAPI spec
|
||||
npm install -g pnpm
|
||||
pnpm install
|
||||
pnpm run fetch-openapi
|
||||
pnpm run generate
|
||||
|
||||
# Bump version after install so pnpm install doesn't overwrite it
|
||||
jq --arg v "${{ steps.get_version.outputs.version }}" '.version = $v' package.json > package.json.tmp
|
||||
mv package.json.tmp package.json
|
||||
|
||||
git config user.name "Dokploy Bot"
|
||||
git config user.email "bot@dokploy.com"
|
||||
|
||||
git add -A
|
||||
git commit -m "chore: bump version to ${{ steps.get_version.outputs.version }}" \
|
||||
-m "Source: ${{ github.repository }}@${{ github.sha }}" \
|
||||
-m "Release: ${{ github.event.release.html_url }}" \
|
||||
--allow-empty
|
||||
|
||||
git push
|
||||
|
||||
|
||||
- name: Sync version to CLI repository
|
||||
run: |
|
||||
git clone https://x-access-token:${{ secrets.DOCS_SYNC_TOKEN }}@github.com/dokploy/cli.git /tmp/cli-repo
|
||||
|
||||
cd /tmp/cli-repo
|
||||
|
||||
# Copy latest openapi spec and regenerate commands
|
||||
cp ${{ github.workspace }}/openapi.json ./openapi.json
|
||||
npm install -g pnpm
|
||||
pnpm install
|
||||
pnpm run generate
|
||||
|
||||
# Bump version after install so pnpm install doesn't overwrite it
|
||||
if [ -f package.json ]; then
|
||||
jq --arg v "${{ steps.get_version.outputs.version }}" '.version = $v' package.json > package.json.tmp
|
||||
mv package.json.tmp package.json
|
||||
fi
|
||||
|
||||
git config user.name "Dokploy Bot"
|
||||
git config user.email "bot@dokploy.com"
|
||||
|
||||
git add -A
|
||||
git commit -m "chore: bump version to ${{ steps.get_version.outputs.version }}" \
|
||||
-m "Source: ${{ github.repository }}@${{ github.sha }}" \
|
||||
-m "Release: ${{ github.event.release.html_url }}" \
|
||||
--allow-empty
|
||||
|
||||
git push
|
||||
|
||||
echo "CLI repo synced to version ${{ steps.get_version.outputs.version }}"
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1,629 +0,0 @@
|
||||
"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>
|
||||
);
|
||||
}
|
||||
@@ -666,7 +666,7 @@ export const AddDomain = ({ id, type, domainId = "", children }: Props) => {
|
||||
<div className="space-y-0.5">
|
||||
<FormLabel>Custom Entrypoint</FormLabel>
|
||||
<FormDescription>
|
||||
Use custom entrypoint for domina
|
||||
Use custom entrypoint for domain
|
||||
<br />
|
||||
"web" and/or "websecure" is used by default.
|
||||
</FormDescription>
|
||||
|
||||
@@ -0,0 +1,290 @@
|
||||
import { Loader2, MoreHorizontal, RefreshCw } from "lucide-react";
|
||||
import dynamic from "next/dynamic";
|
||||
import { useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
import { api } from "@/utils/api";
|
||||
import { ShowContainerConfig } from "@/components/dashboard/docker/config/show-container-config";
|
||||
import { ShowContainerMounts } from "@/components/dashboard/docker/mounts/show-container-mounts";
|
||||
import { ShowContainerNetworks } from "@/components/dashboard/docker/networks/show-container-networks";
|
||||
import { DockerTerminalModal } from "@/components/dashboard/docker/terminal/docker-terminal-modal";
|
||||
|
||||
const DockerLogsId = dynamic(
|
||||
() =>
|
||||
import("@/components/dashboard/docker/logs/docker-logs-id").then(
|
||||
(e) => e.DockerLogsId,
|
||||
),
|
||||
{
|
||||
ssr: false,
|
||||
},
|
||||
);
|
||||
|
||||
interface Props {
|
||||
appName: string;
|
||||
serverId?: string;
|
||||
appType: "stack" | "docker-compose";
|
||||
}
|
||||
|
||||
export const ShowComposeContainers = ({
|
||||
appName,
|
||||
appType,
|
||||
serverId,
|
||||
}: Props) => {
|
||||
const { data, isPending, refetch } =
|
||||
api.docker.getContainersByAppNameMatch.useQuery(
|
||||
{
|
||||
appName,
|
||||
appType,
|
||||
serverId,
|
||||
},
|
||||
{
|
||||
enabled: !!appName,
|
||||
},
|
||||
);
|
||||
|
||||
return (
|
||||
<Card className="bg-background">
|
||||
<CardHeader className="flex flex-row items-center justify-between">
|
||||
<div>
|
||||
<CardTitle className="text-xl">Containers</CardTitle>
|
||||
<CardDescription>
|
||||
Inspect each container in this compose and run basic lifecycle
|
||||
actions.
|
||||
</CardDescription>
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
onClick={() => refetch()}
|
||||
disabled={isPending}
|
||||
>
|
||||
<RefreshCw className={`h-4 w-4 ${isPending ? "animate-spin" : ""}`} />
|
||||
</Button>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{isPending ? (
|
||||
<div className="flex items-center justify-center h-[20vh]">
|
||||
<Loader2 className="animate-spin h-6 w-6 text-muted-foreground" />
|
||||
</div>
|
||||
) : !data || data.length === 0 ? (
|
||||
<div className="flex items-center justify-center h-[20vh]">
|
||||
<span className="text-muted-foreground">
|
||||
No containers found. Deploy the compose to see containers here.
|
||||
</span>
|
||||
</div>
|
||||
) : (
|
||||
<div className="rounded-md border">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Name</TableHead>
|
||||
<TableHead>State</TableHead>
|
||||
<TableHead>Status</TableHead>
|
||||
<TableHead>Container ID</TableHead>
|
||||
<TableHead className="text-right" />
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{data.map((container) => (
|
||||
<ContainerRow
|
||||
key={container.containerId}
|
||||
container={container}
|
||||
serverId={serverId}
|
||||
onActionComplete={() => refetch()}
|
||||
/>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
interface ContainerRowProps {
|
||||
container: {
|
||||
containerId: string;
|
||||
name: string;
|
||||
state: string;
|
||||
status: string;
|
||||
};
|
||||
serverId?: string;
|
||||
onActionComplete: () => void;
|
||||
}
|
||||
|
||||
const ContainerRow = ({
|
||||
container,
|
||||
serverId,
|
||||
onActionComplete,
|
||||
}: ContainerRowProps) => {
|
||||
const [logsOpen, setLogsOpen] = useState(false);
|
||||
const [actionLoading, setActionLoading] = useState<string | null>(null);
|
||||
|
||||
const restartMutation = api.docker.restartContainer.useMutation();
|
||||
const startMutation = api.docker.startContainer.useMutation();
|
||||
const stopMutation = api.docker.stopContainer.useMutation();
|
||||
const killMutation = api.docker.killContainer.useMutation();
|
||||
|
||||
const handleAction = async (
|
||||
action: string,
|
||||
mutationFn: typeof restartMutation,
|
||||
) => {
|
||||
setActionLoading(action);
|
||||
try {
|
||||
await mutationFn.mutateAsync({
|
||||
containerId: container.containerId,
|
||||
serverId,
|
||||
});
|
||||
toast.success(`Container ${action} successfully`);
|
||||
onActionComplete();
|
||||
} catch (error) {
|
||||
toast.error(
|
||||
`Failed to ${action} container: ${error instanceof Error ? error.message : "Unknown error"}`,
|
||||
);
|
||||
} finally {
|
||||
setActionLoading(null);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<TableRow>
|
||||
<TableCell className="font-medium">{container.name}</TableCell>
|
||||
<TableCell>
|
||||
<Badge
|
||||
variant={
|
||||
container.state === "running"
|
||||
? "default"
|
||||
: container.state === "exited"
|
||||
? "secondary"
|
||||
: "destructive"
|
||||
}
|
||||
>
|
||||
{container.state}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell>{container.status}</TableCell>
|
||||
<TableCell className="font-mono text-sm text-muted-foreground">
|
||||
{container.containerId}
|
||||
</TableCell>
|
||||
<TableCell className="text-right">
|
||||
<Dialog open={logsOpen} onOpenChange={setLogsOpen}>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" className="h-8 w-8 p-0">
|
||||
{actionLoading ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
<MoreHorizontal className="h-4 w-4" />
|
||||
)}
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuLabel>Actions</DropdownMenuLabel>
|
||||
<DialogTrigger asChild>
|
||||
<DropdownMenuItem
|
||||
className="cursor-pointer"
|
||||
onSelect={(e) => e.preventDefault()}
|
||||
>
|
||||
View Logs
|
||||
</DropdownMenuItem>
|
||||
</DialogTrigger>
|
||||
<ShowContainerConfig
|
||||
containerId={container.containerId}
|
||||
serverId={serverId || ""}
|
||||
/>
|
||||
<ShowContainerMounts
|
||||
containerId={container.containerId}
|
||||
serverId={serverId || ""}
|
||||
/>
|
||||
<ShowContainerNetworks
|
||||
containerId={container.containerId}
|
||||
serverId={serverId || ""}
|
||||
/>
|
||||
<DockerTerminalModal
|
||||
containerId={container.containerId}
|
||||
serverId={serverId || ""}
|
||||
>
|
||||
Terminal
|
||||
</DockerTerminalModal>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem
|
||||
className="cursor-pointer"
|
||||
disabled={actionLoading !== null}
|
||||
onClick={() => handleAction("restart", restartMutation)}
|
||||
>
|
||||
Restart
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
className="cursor-pointer"
|
||||
disabled={actionLoading !== null}
|
||||
onClick={() => handleAction("start", startMutation)}
|
||||
>
|
||||
Start
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
className="cursor-pointer"
|
||||
disabled={actionLoading !== null}
|
||||
onClick={() => handleAction("stop", stopMutation)}
|
||||
>
|
||||
Stop
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
className="cursor-pointer text-red-500 focus:text-red-600"
|
||||
disabled={actionLoading !== null}
|
||||
onClick={() => handleAction("kill", killMutation)}
|
||||
>
|
||||
Kill
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<DialogContent className="sm:max-w-7xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>View Logs</DialogTitle>
|
||||
<DialogDescription>Logs for {container.name}</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<DockerLogsId
|
||||
containerId={container.containerId}
|
||||
serverId={serverId}
|
||||
runType="native"
|
||||
/>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,112 @@
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import { DropdownMenuItem } from "@/components/ui/dropdown-menu";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { api } from "@/utils/api";
|
||||
|
||||
interface Props {
|
||||
containerId: string;
|
||||
serverId?: string;
|
||||
}
|
||||
|
||||
interface Mount {
|
||||
Type: string;
|
||||
Source: string;
|
||||
Destination: string;
|
||||
Mode: string;
|
||||
RW: boolean;
|
||||
Propagation: string;
|
||||
Name?: string;
|
||||
Driver?: string;
|
||||
}
|
||||
|
||||
export const ShowContainerMounts = ({ containerId, serverId }: Props) => {
|
||||
const { data } = api.docker.getConfig.useQuery(
|
||||
{
|
||||
containerId,
|
||||
serverId,
|
||||
},
|
||||
{
|
||||
enabled: !!containerId,
|
||||
},
|
||||
);
|
||||
|
||||
const mounts: Mount[] = data?.Mounts ?? [];
|
||||
|
||||
return (
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<DropdownMenuItem
|
||||
className="w-full cursor-pointer"
|
||||
onSelect={(e) => e.preventDefault()}
|
||||
>
|
||||
View Mounts
|
||||
</DropdownMenuItem>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="w-full md:w-[70vw] min-w-[70vw]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Container Mounts</DialogTitle>
|
||||
<DialogDescription>
|
||||
Volume and bind mounts for this container
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="overflow-auto max-h-[70vh]">
|
||||
{mounts.length === 0 ? (
|
||||
<div className="text-center text-muted-foreground py-8">
|
||||
No mounts found for this container.
|
||||
</div>
|
||||
) : (
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Type</TableHead>
|
||||
<TableHead>Source</TableHead>
|
||||
<TableHead>Destination</TableHead>
|
||||
<TableHead>Mode</TableHead>
|
||||
<TableHead>Read/Write</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{mounts.map((mount, index) => (
|
||||
<TableRow key={index}>
|
||||
<TableCell>
|
||||
<Badge variant="outline">{mount.Type}</Badge>
|
||||
</TableCell>
|
||||
<TableCell className="font-mono text-xs max-w-[250px] truncate">
|
||||
{mount.Name || mount.Source}
|
||||
</TableCell>
|
||||
<TableCell className="font-mono text-xs max-w-[250px] truncate">
|
||||
{mount.Destination}
|
||||
</TableCell>
|
||||
<TableCell className="text-xs">
|
||||
{mount.Mode || "-"}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Badge variant={mount.RW ? "default" : "secondary"}>
|
||||
{mount.RW ? "RW" : "RO"}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
)}
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,119 @@
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import { DropdownMenuItem } from "@/components/ui/dropdown-menu";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { api } from "@/utils/api";
|
||||
|
||||
interface Props {
|
||||
containerId: string;
|
||||
serverId?: string;
|
||||
}
|
||||
|
||||
interface Network {
|
||||
IPAMConfig: unknown;
|
||||
Links: unknown;
|
||||
Aliases: string[] | null;
|
||||
MacAddress: string;
|
||||
NetworkID: string;
|
||||
EndpointID: string;
|
||||
Gateway: string;
|
||||
IPAddress: string;
|
||||
IPPrefixLen: number;
|
||||
IPv6Gateway: string;
|
||||
GlobalIPv6Address: string;
|
||||
GlobalIPv6PrefixLen: number;
|
||||
DriverOpts: unknown;
|
||||
}
|
||||
|
||||
export const ShowContainerNetworks = ({ containerId, serverId }: Props) => {
|
||||
const { data } = api.docker.getConfig.useQuery(
|
||||
{
|
||||
containerId,
|
||||
serverId,
|
||||
},
|
||||
{
|
||||
enabled: !!containerId,
|
||||
},
|
||||
);
|
||||
|
||||
const networks: Record<string, Network> =
|
||||
data?.NetworkSettings?.Networks ?? {};
|
||||
const entries = Object.entries(networks);
|
||||
|
||||
return (
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<DropdownMenuItem
|
||||
className="w-full cursor-pointer"
|
||||
onSelect={(e) => e.preventDefault()}
|
||||
>
|
||||
View Networks
|
||||
</DropdownMenuItem>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="w-full md:w-[70vw] min-w-[70vw]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Container Networks</DialogTitle>
|
||||
<DialogDescription>
|
||||
Networks attached to this container
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="overflow-auto max-h-[70vh]">
|
||||
{entries.length === 0 ? (
|
||||
<div className="text-center text-muted-foreground py-8">
|
||||
No networks found for this container.
|
||||
</div>
|
||||
) : (
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Network</TableHead>
|
||||
<TableHead>IP Address</TableHead>
|
||||
<TableHead>Gateway</TableHead>
|
||||
<TableHead>MAC Address</TableHead>
|
||||
<TableHead>Aliases</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{entries.map(([name, network]) => (
|
||||
<TableRow key={name}>
|
||||
<TableCell>
|
||||
<Badge variant="outline">{name}</Badge>
|
||||
</TableCell>
|
||||
<TableCell className="font-mono text-xs">
|
||||
{network.IPAddress
|
||||
? `${network.IPAddress}/${network.IPPrefixLen}`
|
||||
: "-"}
|
||||
</TableCell>
|
||||
<TableCell className="font-mono text-xs">
|
||||
{network.Gateway || "-"}
|
||||
</TableCell>
|
||||
<TableCell className="font-mono text-xs">
|
||||
{network.MacAddress || "-"}
|
||||
</TableCell>
|
||||
<TableCell className="text-xs">
|
||||
{network.Aliases?.join(", ") || "-"}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
)}
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
@@ -10,6 +10,8 @@ import {
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { ShowContainerConfig } from "../config/show-container-config";
|
||||
import { ShowDockerModalLogs } from "../logs/show-docker-modal-logs";
|
||||
import { ShowContainerMounts } from "../mounts/show-container-mounts";
|
||||
import { ShowContainerNetworks } from "../networks/show-container-networks";
|
||||
import { RemoveContainerDialog } from "../remove/remove-container";
|
||||
import { DockerTerminalModal } from "../terminal/docker-terminal-modal";
|
||||
import { UploadFileModal } from "../upload/upload-file-modal";
|
||||
@@ -123,6 +125,14 @@ export const columns: ColumnDef<Container>[] = [
|
||||
containerId={container.containerId}
|
||||
serverId={container.serverId || ""}
|
||||
/>
|
||||
<ShowContainerMounts
|
||||
containerId={container.containerId}
|
||||
serverId={container.serverId || ""}
|
||||
/>
|
||||
<ShowContainerNetworks
|
||||
containerId={container.containerId}
|
||||
serverId={container.serverId || ""}
|
||||
/>
|
||||
<DockerTerminalModal
|
||||
containerId={container.containerId}
|
||||
serverId={container.serverId || ""}
|
||||
|
||||
291
apps/dokploy/components/dashboard/home/show-home.tsx
Normal file
291
apps/dokploy/components/dashboard/home/show-home.tsx
Normal file
@@ -0,0 +1,291 @@
|
||||
import { formatDistanceToNow } from "date-fns";
|
||||
import { ArrowRight, Rocket, Server } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useMemo } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card } from "@/components/ui/card";
|
||||
import { api } from "@/utils/api";
|
||||
|
||||
type DeploymentStatus = "idle" | "running" | "done" | "error";
|
||||
|
||||
const statusDotClass: Record<string, string> = {
|
||||
done: "bg-emerald-500",
|
||||
running: "bg-amber-500",
|
||||
error: "bg-red-500",
|
||||
idle: "bg-muted-foreground/40",
|
||||
};
|
||||
|
||||
function getServiceInfo(d: any) {
|
||||
const app = d.application;
|
||||
const comp = d.compose;
|
||||
const serverName: string =
|
||||
d.server?.name ?? app?.server?.name ?? comp?.server?.name ?? "Dokploy";
|
||||
if (app?.environment?.project && app.environment) {
|
||||
return {
|
||||
name: app.name as string,
|
||||
environment: app.environment.name as string,
|
||||
projectName: app.environment.project.name as string,
|
||||
serverName,
|
||||
href: `/dashboard/project/${app.environment.project.projectId}/environment/${app.environment.environmentId}/services/application/${app.applicationId}`,
|
||||
};
|
||||
}
|
||||
if (comp?.environment?.project && comp.environment) {
|
||||
return {
|
||||
name: comp.name as string,
|
||||
environment: comp.environment.name as string,
|
||||
projectName: comp.environment.project.name as string,
|
||||
serverName,
|
||||
href: `/dashboard/project/${comp.environment.project.projectId}/environment/${comp.environment.environmentId}/services/compose/${comp.composeId}`,
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function StatCard({
|
||||
label,
|
||||
value,
|
||||
delta,
|
||||
}: {
|
||||
label: string;
|
||||
value: string;
|
||||
delta?: string;
|
||||
}) {
|
||||
return (
|
||||
<div className="rounded-xl border bg-background p-5 min-h-[140px] flex flex-col justify-between">
|
||||
<span className="text-xs uppercase tracking-wider text-muted-foreground">
|
||||
{label}
|
||||
</span>
|
||||
<div className="flex flex-col gap-1">
|
||||
<span className="text-3xl font-semibold tracking-tight">{value}</span>
|
||||
{delta && (
|
||||
<span className="text-xs text-muted-foreground">{delta}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function StatusListCard({
|
||||
label,
|
||||
items,
|
||||
}: {
|
||||
label: string;
|
||||
items: { dotClass: string; label: string; count: number }[];
|
||||
}) {
|
||||
return (
|
||||
<div className="rounded-xl border bg-background p-5 min-h-[140px] flex flex-col gap-3">
|
||||
<span className="text-xs uppercase tracking-wider text-muted-foreground">
|
||||
{label}
|
||||
</span>
|
||||
<ul className="flex flex-col gap-1.5">
|
||||
{items.map((item) => (
|
||||
<li key={item.label} className="flex items-center gap-2.5 text-sm">
|
||||
<span
|
||||
className={`size-2 rounded-full shrink-0 ${item.dotClass}`}
|
||||
aria-hidden
|
||||
/>
|
||||
<span className="font-semibold tabular-nums w-8">{item.count}</span>
|
||||
<span className="text-muted-foreground">{item.label}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export const ShowHome = () => {
|
||||
const { data: auth } = api.user.get.useQuery();
|
||||
const { data: homeStats } = api.project.homeStats.useQuery();
|
||||
const { data: permissions } = api.user.getPermissions.useQuery();
|
||||
const canReadDeployments = !!permissions?.deployment.read;
|
||||
const { data: deployments } = api.deployment.allCentralized.useQuery(
|
||||
undefined,
|
||||
{
|
||||
enabled: canReadDeployments,
|
||||
refetchInterval: 10000,
|
||||
},
|
||||
);
|
||||
|
||||
const firstName = auth?.user?.firstName?.trim();
|
||||
|
||||
const totals = homeStats ?? {
|
||||
projects: 0,
|
||||
environments: 0,
|
||||
applications: 0,
|
||||
compose: 0,
|
||||
databases: 0,
|
||||
services: 0,
|
||||
};
|
||||
const statusBreakdown = homeStats?.status ?? {
|
||||
running: 0,
|
||||
error: 0,
|
||||
idle: 0,
|
||||
};
|
||||
|
||||
const recentDeployments = useMemo(() => {
|
||||
if (!deployments) return [];
|
||||
return [...deployments]
|
||||
.sort(
|
||||
(a, b) =>
|
||||
new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime(),
|
||||
)
|
||||
.slice(0, 10);
|
||||
}, [deployments]);
|
||||
|
||||
const deployStats = useMemo(() => {
|
||||
const now = Date.now();
|
||||
const weekMs = 7 * 24 * 60 * 60 * 1000;
|
||||
const lastStart = now - weekMs;
|
||||
const prevStart = now - 2 * weekMs;
|
||||
|
||||
const last: NonNullable<typeof deployments> = [];
|
||||
const prev: NonNullable<typeof deployments> = [];
|
||||
for (const d of deployments ?? []) {
|
||||
const t = new Date(d.createdAt).getTime();
|
||||
if (t >= lastStart) last.push(d);
|
||||
else if (t >= prevStart) prev.push(d);
|
||||
}
|
||||
|
||||
const lastCount = last.length;
|
||||
const prevCount = prev.length;
|
||||
let delta: string | undefined;
|
||||
if (prevCount > 0) {
|
||||
const pct = Math.round(((lastCount - prevCount) / prevCount) * 100);
|
||||
delta = `${pct >= 0 ? "+" : ""}${pct}% vs prev 7d`;
|
||||
} else if (lastCount > 0) {
|
||||
delta = "no prior data";
|
||||
} else {
|
||||
delta = "no activity yet";
|
||||
}
|
||||
|
||||
return { value: String(lastCount), delta };
|
||||
}, [deployments]);
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
<Card className="h-full bg-sidebar p-2.5 rounded-xl min-h-[85vh]">
|
||||
<div className="rounded-xl bg-background shadow-md p-6 flex flex-col gap-6 h-full">
|
||||
<div className="flex flex-col gap-6 sm:flex-row sm:items-end sm:justify-between">
|
||||
<h1 className="text-3xl font-semibold tracking-tight">
|
||||
{firstName ? `Welcome back, ${firstName}` : "Welcome back"}
|
||||
</h1>
|
||||
<Button asChild variant="secondary" className="w-fit">
|
||||
<Link href="/dashboard/projects">
|
||||
Go to projects
|
||||
<ArrowRight className="size-4" />
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
<StatCard
|
||||
label="Projects"
|
||||
value={String(totals.projects)}
|
||||
delta={`${totals.environments} ${totals.environments === 1 ? "environment" : "environments"}`}
|
||||
/>
|
||||
<StatCard
|
||||
label="Services"
|
||||
value={String(totals.services)}
|
||||
delta={`${totals.applications} apps · ${totals.compose} compose · ${totals.databases} db`}
|
||||
/>
|
||||
<StatCard
|
||||
label="Deploys / 7d"
|
||||
value={deployStats.value}
|
||||
delta={deployStats.delta}
|
||||
/>
|
||||
<StatusListCard
|
||||
label="Status"
|
||||
items={[
|
||||
{
|
||||
dotClass: "bg-emerald-500",
|
||||
label: "running",
|
||||
count: statusBreakdown.running,
|
||||
},
|
||||
{
|
||||
dotClass: "bg-red-500",
|
||||
label: "errored",
|
||||
count: statusBreakdown.error,
|
||||
},
|
||||
{
|
||||
dotClass: "bg-muted-foreground/40",
|
||||
label: "idle",
|
||||
count: statusBreakdown.idle,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="rounded-xl border bg-background">
|
||||
<div className="flex items-center justify-between px-5 py-4 border-b">
|
||||
<div className="flex items-center gap-2">
|
||||
<Rocket className="size-4 text-muted-foreground" />
|
||||
<h2 className="text-sm font-semibold">Recent deployments</h2>
|
||||
</div>
|
||||
{canReadDeployments && (
|
||||
<Link
|
||||
href="/dashboard/deployments"
|
||||
className="text-xs text-muted-foreground hover:text-foreground transition-colors"
|
||||
>
|
||||
view all →
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
{!canReadDeployments ? (
|
||||
<div className="min-h-[400px] flex flex-col items-center justify-center gap-3 text-center text-sm text-muted-foreground p-10">
|
||||
<Rocket className="size-8 opacity-40" />
|
||||
<span>You do not have permission to view deployments.</span>
|
||||
</div>
|
||||
) : recentDeployments.length === 0 ? (
|
||||
<div className="min-h-[400px] flex flex-col items-center justify-center gap-3 text-center text-sm text-muted-foreground p-10">
|
||||
<Rocket className="size-8 opacity-40" />
|
||||
<span>No deployments yet.</span>
|
||||
</div>
|
||||
) : (
|
||||
<ul className="divide-y">
|
||||
{recentDeployments.map((d) => {
|
||||
const info = getServiceInfo(d);
|
||||
if (!info) return null;
|
||||
const status = (d.status ?? "idle") as DeploymentStatus;
|
||||
return (
|
||||
<li key={d.deploymentId}>
|
||||
<Link
|
||||
href={info.href}
|
||||
className="flex items-center gap-4 px-5 py-4 hover:bg-muted/40 transition-colors"
|
||||
>
|
||||
<span
|
||||
className={`size-2 rounded-full shrink-0 ${statusDotClass[status] ?? statusDotClass.idle}`}
|
||||
aria-hidden
|
||||
/>
|
||||
<div className="flex flex-col min-w-0 flex-1">
|
||||
<span className="text-sm truncate">{info.name}</span>
|
||||
<span className="text-xs text-muted-foreground truncate">
|
||||
{info.projectName} · {info.environment}
|
||||
</span>
|
||||
</div>
|
||||
<span className="text-xs text-muted-foreground w-36 hidden lg:flex items-center justify-end gap-1.5 truncate">
|
||||
<Server className="size-3 shrink-0" />
|
||||
<span className="truncate">{info.serverName}</span>
|
||||
</span>
|
||||
<span className="text-xs text-muted-foreground w-20 text-right hidden sm:inline">
|
||||
{status}
|
||||
</span>
|
||||
<span className="text-xs text-muted-foreground w-24 text-right hidden md:inline">
|
||||
{formatDistanceToNow(new Date(d.createdAt), {
|
||||
addSuffix: true,
|
||||
})}
|
||||
</span>
|
||||
<span className="text-xs text-muted-foreground hover:text-foreground transition-colors">
|
||||
logs →
|
||||
</span>
|
||||
</Link>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -166,6 +166,7 @@ export const ShowProjects = () => {
|
||||
return (
|
||||
total +
|
||||
(env.applications?.length || 0) +
|
||||
(env.libsql?.length || 0) +
|
||||
(env.mariadb?.length || 0) +
|
||||
(env.mongo?.length || 0) +
|
||||
(env.mysql?.length || 0) +
|
||||
@@ -178,6 +179,7 @@ export const ShowProjects = () => {
|
||||
return (
|
||||
total +
|
||||
(env.applications?.length || 0) +
|
||||
(env.libsql?.length || 0) +
|
||||
(env.mariadb?.length || 0) +
|
||||
(env.mongo?.length || 0) +
|
||||
(env.mysql?.length || 0) +
|
||||
|
||||
@@ -167,7 +167,7 @@ export const SearchCommand = () => {
|
||||
<CommandGroup heading={"Application"} hidden={true}>
|
||||
<CommandItem
|
||||
onSelect={() => {
|
||||
router.push("/dashboard/projects");
|
||||
router.push("/dashboard/home");
|
||||
setOpen(false);
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -425,7 +425,7 @@ export const WelcomeSubscription = () => {
|
||||
onClick={() => {
|
||||
if (stepper.isLast) {
|
||||
setIsOpen(false);
|
||||
push("/dashboard/projects");
|
||||
push("/dashboard/home");
|
||||
} else {
|
||||
stepper.next();
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
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";
|
||||
@@ -24,7 +23,6 @@ export const DashboardLayout = ({ children }: Props) => {
|
||||
return (
|
||||
<>
|
||||
<Page>{children}</Page>
|
||||
<ChatPanel />
|
||||
{isChatEnabled && (
|
||||
<>
|
||||
<HubSpotWidget />
|
||||
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
Forward,
|
||||
GalleryVerticalEnd,
|
||||
GitBranch,
|
||||
House,
|
||||
Key,
|
||||
KeyRound,
|
||||
Loader2,
|
||||
@@ -148,6 +149,12 @@ type Menu = {
|
||||
// The `isEnabled` function is called to determine if the item should be displayed
|
||||
const MENU: Menu = {
|
||||
home: [
|
||||
{
|
||||
isSingle: true,
|
||||
title: "Home",
|
||||
url: "/dashboard/home",
|
||||
icon: House,
|
||||
},
|
||||
{
|
||||
isSingle: true,
|
||||
title: "Projects",
|
||||
|
||||
@@ -80,7 +80,7 @@ export const UserNav = () => {
|
||||
<DropdownMenuItem
|
||||
className="cursor-pointer"
|
||||
onClick={() => {
|
||||
router.push("/dashboard/projects");
|
||||
router.push("/dashboard/home");
|
||||
}}
|
||||
>
|
||||
Projects
|
||||
|
||||
@@ -44,7 +44,7 @@ export function SignInWithSSO({ children }: SignInWithSSOProps) {
|
||||
try {
|
||||
const { data, error } = await authClient.signIn.sso({
|
||||
email: values.email,
|
||||
callbackURL: "/dashboard/projects",
|
||||
callbackURL: "/dashboard/home",
|
||||
});
|
||||
if (error) {
|
||||
toast.error(error.message ?? "Failed to sign in with SSO");
|
||||
|
||||
@@ -116,6 +116,14 @@ export function TagSelector({
|
||||
<HandleTag />
|
||||
</div>
|
||||
</CommandEmpty>
|
||||
{tags.length === 0 && (
|
||||
<div className="flex flex-col items-center gap-2 py-4">
|
||||
<span className="text-sm text-muted-foreground">
|
||||
No tags created yet.
|
||||
</span>
|
||||
<HandleTag />
|
||||
</div>
|
||||
)}
|
||||
<CommandGroup>
|
||||
{tags.map((tag) => {
|
||||
const isSelected = selectedTags.includes(tag.id);
|
||||
|
||||
198
apps/dokploy/components/ui/context-menu.tsx
Normal file
198
apps/dokploy/components/ui/context-menu.tsx
Normal file
@@ -0,0 +1,198 @@
|
||||
import * as ContextMenuPrimitive from "@radix-ui/react-context-menu";
|
||||
import { Check, ChevronRight, Circle } from "lucide-react";
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const ContextMenu = ContextMenuPrimitive.Root;
|
||||
|
||||
const ContextMenuTrigger = ContextMenuPrimitive.Trigger;
|
||||
|
||||
const ContextMenuGroup = ContextMenuPrimitive.Group;
|
||||
|
||||
const ContextMenuPortal = ContextMenuPrimitive.Portal;
|
||||
|
||||
const ContextMenuSub = ContextMenuPrimitive.Sub;
|
||||
|
||||
const ContextMenuRadioGroup = ContextMenuPrimitive.RadioGroup;
|
||||
|
||||
const ContextMenuSubTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof ContextMenuPrimitive.SubTrigger>,
|
||||
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.SubTrigger> & {
|
||||
inset?: boolean;
|
||||
}
|
||||
>(({ className, inset, children, ...props }, ref) => (
|
||||
<ContextMenuPrimitive.SubTrigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground",
|
||||
inset && "pl-8",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<ChevronRight className="ml-auto h-4 w-4" />
|
||||
</ContextMenuPrimitive.SubTrigger>
|
||||
));
|
||||
ContextMenuSubTrigger.displayName = ContextMenuPrimitive.SubTrigger.displayName;
|
||||
|
||||
const ContextMenuSubContent = React.forwardRef<
|
||||
React.ElementRef<typeof ContextMenuPrimitive.SubContent>,
|
||||
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.SubContent>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<ContextMenuPrimitive.SubContent
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
ContextMenuSubContent.displayName = ContextMenuPrimitive.SubContent.displayName;
|
||||
|
||||
const ContextMenuContent = React.forwardRef<
|
||||
React.ElementRef<typeof ContextMenuPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Content>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<ContextMenuPrimitive.Portal>
|
||||
<ContextMenuPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md animate-in fade-in-80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</ContextMenuPrimitive.Portal>
|
||||
));
|
||||
ContextMenuContent.displayName = ContextMenuPrimitive.Content.displayName;
|
||||
|
||||
const ContextMenuItem = React.forwardRef<
|
||||
React.ElementRef<typeof ContextMenuPrimitive.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Item> & {
|
||||
inset?: boolean;
|
||||
}
|
||||
>(({ className, inset, ...props }, ref) => (
|
||||
<ContextMenuPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
inset && "pl-8",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
ContextMenuItem.displayName = ContextMenuPrimitive.Item.displayName;
|
||||
|
||||
const ContextMenuCheckboxItem = React.forwardRef<
|
||||
React.ElementRef<typeof ContextMenuPrimitive.CheckboxItem>,
|
||||
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.CheckboxItem>
|
||||
>(({ className, children, checked, ...props }, ref) => (
|
||||
<ContextMenuPrimitive.CheckboxItem
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
className,
|
||||
)}
|
||||
checked={checked}
|
||||
{...props}
|
||||
>
|
||||
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<ContextMenuPrimitive.ItemIndicator>
|
||||
<Check className="h-4 w-4" />
|
||||
</ContextMenuPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</ContextMenuPrimitive.CheckboxItem>
|
||||
));
|
||||
ContextMenuCheckboxItem.displayName =
|
||||
ContextMenuPrimitive.CheckboxItem.displayName;
|
||||
|
||||
const ContextMenuRadioItem = React.forwardRef<
|
||||
React.ElementRef<typeof ContextMenuPrimitive.RadioItem>,
|
||||
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.RadioItem>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<ContextMenuPrimitive.RadioItem
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<ContextMenuPrimitive.ItemIndicator>
|
||||
<Circle className="h-2 w-2 fill-current" />
|
||||
</ContextMenuPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</ContextMenuPrimitive.RadioItem>
|
||||
));
|
||||
ContextMenuRadioItem.displayName = ContextMenuPrimitive.RadioItem.displayName;
|
||||
|
||||
const ContextMenuLabel = React.forwardRef<
|
||||
React.ElementRef<typeof ContextMenuPrimitive.Label>,
|
||||
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Label> & {
|
||||
inset?: boolean;
|
||||
}
|
||||
>(({ className, inset, ...props }, ref) => (
|
||||
<ContextMenuPrimitive.Label
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"px-2 py-1.5 text-sm font-semibold text-foreground",
|
||||
inset && "pl-8",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
ContextMenuLabel.displayName = ContextMenuPrimitive.Label.displayName;
|
||||
|
||||
const ContextMenuSeparator = React.forwardRef<
|
||||
React.ElementRef<typeof ContextMenuPrimitive.Separator>,
|
||||
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Separator>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<ContextMenuPrimitive.Separator
|
||||
ref={ref}
|
||||
className={cn("-mx-1 my-1 h-px bg-border", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
ContextMenuSeparator.displayName = ContextMenuPrimitive.Separator.displayName;
|
||||
|
||||
const ContextMenuShortcut = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLSpanElement>) => {
|
||||
return (
|
||||
<span
|
||||
className={cn(
|
||||
"ml-auto text-xs tracking-widest text-muted-foreground",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
ContextMenuShortcut.displayName = "ContextMenuShortcut";
|
||||
|
||||
export {
|
||||
ContextMenu,
|
||||
ContextMenuTrigger,
|
||||
ContextMenuContent,
|
||||
ContextMenuItem,
|
||||
ContextMenuCheckboxItem,
|
||||
ContextMenuRadioItem,
|
||||
ContextMenuLabel,
|
||||
ContextMenuSeparator,
|
||||
ContextMenuShortcut,
|
||||
ContextMenuGroup,
|
||||
ContextMenuPortal,
|
||||
ContextMenuSub,
|
||||
ContextMenuSubContent,
|
||||
ContextMenuSubTrigger,
|
||||
ContextMenuRadioGroup,
|
||||
};
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "dokploy",
|
||||
"version": "v0.29.0",
|
||||
"version": "v0.29.1",
|
||||
"private": true,
|
||||
"license": "Apache-2.0",
|
||||
"type": "module",
|
||||
@@ -46,7 +46,6 @@
|
||||
"@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",
|
||||
@@ -58,7 +57,7 @@
|
||||
"@codemirror/search": "^6.6.0",
|
||||
"@codemirror/view": "^6.39.15",
|
||||
"@dokploy/server": "workspace:*",
|
||||
"@dokploy/trpc-openapi": "0.0.19",
|
||||
"@dokploy/trpc-openapi": "0.0.18",
|
||||
"@faker-js/faker": "^8.4.1",
|
||||
"@hookform/resolvers": "^5.2.2",
|
||||
"@octokit/auth-app": "^6.1.3",
|
||||
@@ -68,6 +67,7 @@
|
||||
"@radix-ui/react-avatar": "^1.1.10",
|
||||
"@radix-ui/react-checkbox": "^1.3.2",
|
||||
"@radix-ui/react-collapsible": "^1.1.11",
|
||||
"@radix-ui/react-context-menu": "^2.2.16",
|
||||
"@radix-ui/react-dialog": "^1.1.14",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.15",
|
||||
"@radix-ui/react-label": "^2.1.7",
|
||||
|
||||
@@ -53,7 +53,7 @@ export default function Custom404({ statusCode, error }: Props) {
|
||||
|
||||
<div className="mt-5 flex flex-col justify-center items-center gap-2 sm:flex-row sm:gap-3">
|
||||
<Link
|
||||
href="/dashboard/projects"
|
||||
href="/dashboard/home"
|
||||
className={buttonVariants({
|
||||
variant: "secondary",
|
||||
className: "flex flex-row gap-2",
|
||||
|
||||
@@ -1,251 +0,0 @@
|
||||
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",
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -40,7 +40,7 @@ function DeploymentsPage() {
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
<Card className="h-full bg-sidebar p-2.5 rounded-xl max-w-8xl mx-auto min-h-[45vh]">
|
||||
<Card className="h-full bg-sidebar p-2.5 rounded-xl min-h-[45vh]">
|
||||
<div className="rounded-xl bg-background shadow-md h-full">
|
||||
<CardHeader>
|
||||
<div className="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
|
||||
@@ -102,7 +102,7 @@ export async function getServerSideProps(ctx: GetServerSidePropsContext) {
|
||||
return {
|
||||
redirect: {
|
||||
permanent: false,
|
||||
destination: "/dashboard/projects",
|
||||
destination: "/dashboard/home",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ export async function getServerSideProps(
|
||||
return {
|
||||
redirect: {
|
||||
permanent: true,
|
||||
destination: "/dashboard/projects",
|
||||
destination: "/dashboard/home",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
53
apps/dokploy/pages/dashboard/home.tsx
Normal file
53
apps/dokploy/pages/dashboard/home.tsx
Normal file
@@ -0,0 +1,53 @@
|
||||
import { validateRequest } from "@dokploy/server/lib/auth";
|
||||
import { createServerSideHelpers } from "@trpc/react-query/server";
|
||||
import type { GetServerSidePropsContext } from "next";
|
||||
import type { ReactElement } from "react";
|
||||
import superjson from "superjson";
|
||||
import { ShowHome } from "@/components/dashboard/home/show-home";
|
||||
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
||||
import { appRouter } from "@/server/api/root";
|
||||
|
||||
const Home = () => {
|
||||
return <ShowHome />;
|
||||
};
|
||||
|
||||
export default Home;
|
||||
|
||||
Home.getLayout = (page: ReactElement) => {
|
||||
return <DashboardLayout>{page}</DashboardLayout>;
|
||||
};
|
||||
|
||||
export async function getServerSideProps(ctx: GetServerSidePropsContext) {
|
||||
const { req, res } = ctx;
|
||||
const { user, session } = await validateRequest(req);
|
||||
|
||||
if (!user) {
|
||||
return {
|
||||
redirect: {
|
||||
permanent: true,
|
||||
destination: "/",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const helpers = createServerSideHelpers({
|
||||
router: appRouter,
|
||||
ctx: {
|
||||
req: req as any,
|
||||
res: res as any,
|
||||
db: null as any,
|
||||
session: session as any,
|
||||
user: user as any,
|
||||
},
|
||||
transformer: superjson,
|
||||
});
|
||||
|
||||
await helpers.settings.isCloud.prefetch();
|
||||
await helpers.user.get.prefetch();
|
||||
|
||||
return {
|
||||
props: {
|
||||
trpcState: helpers.dehydrate(),
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -96,7 +96,7 @@ export async function getServerSideProps(
|
||||
return {
|
||||
redirect: {
|
||||
permanent: true,
|
||||
destination: "/dashboard/projects",
|
||||
destination: "/dashboard/home",
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -122,7 +122,7 @@ export async function getServerSideProps(
|
||||
return {
|
||||
redirect: {
|
||||
permanent: false,
|
||||
destination: "/dashboard/projects",
|
||||
destination: "/dashboard/home",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
Loader2,
|
||||
Play,
|
||||
PlusIcon,
|
||||
RefreshCw,
|
||||
Search,
|
||||
ServerIcon,
|
||||
SquareTerminal,
|
||||
@@ -68,6 +69,14 @@ import {
|
||||
CommandInput,
|
||||
CommandItem,
|
||||
} from "@/components/ui/command";
|
||||
import {
|
||||
ContextMenu,
|
||||
ContextMenuContent,
|
||||
ContextMenuItem,
|
||||
ContextMenuLabel,
|
||||
ContextMenuSeparator,
|
||||
ContextMenuTrigger,
|
||||
} from "@/components/ui/context-menu";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
@@ -424,6 +433,7 @@ const EnvironmentPage = (
|
||||
const [isBulkDeleteDialogOpen, setIsBulkDeleteDialogOpen] = useState(false);
|
||||
const [deleteVolumes, setDeleteVolumes] = useState(false);
|
||||
const [selectedServerId, setSelectedServerId] = useState<string>("all");
|
||||
const [serviceToDelete, setServiceToDelete] = useState<Services | null>(null);
|
||||
|
||||
const handleSelectAll = () => {
|
||||
if (selectedServices.length === filteredServices.length) {
|
||||
@@ -499,6 +509,14 @@ const EnvironmentPage = (
|
||||
deploy: api.mongo.deploy.useMutation(),
|
||||
};
|
||||
|
||||
const libsqlActions = {
|
||||
start: api.libsql.start.useMutation(),
|
||||
stop: api.libsql.stop.useMutation(),
|
||||
move: api.libsql.move.useMutation(),
|
||||
delete: api.libsql.remove.useMutation(),
|
||||
deploy: api.libsql.deploy.useMutation(),
|
||||
};
|
||||
|
||||
const handleBulkStart = async () => {
|
||||
let success = 0;
|
||||
setIsBulkActionLoading(true);
|
||||
@@ -531,6 +549,9 @@ const EnvironmentPage = (
|
||||
case "mongo":
|
||||
await mongoActions.start.mutateAsync({ mongoId: serviceId });
|
||||
break;
|
||||
case "libsql":
|
||||
await libsqlActions.start.mutateAsync({ libsqlId: serviceId });
|
||||
break;
|
||||
}
|
||||
success++;
|
||||
} catch {
|
||||
@@ -578,6 +599,9 @@ const EnvironmentPage = (
|
||||
case "mongo":
|
||||
await mongoActions.stop.mutateAsync({ mongoId: serviceId });
|
||||
break;
|
||||
case "libsql":
|
||||
await libsqlActions.stop.mutateAsync({ libsqlId: serviceId });
|
||||
break;
|
||||
}
|
||||
success++;
|
||||
} catch {
|
||||
@@ -654,6 +678,12 @@ const EnvironmentPage = (
|
||||
targetEnvironmentId: selectedTargetEnvironment,
|
||||
});
|
||||
break;
|
||||
case "libsql":
|
||||
await libsqlActions.move.mutateAsync({
|
||||
libsqlId: serviceId,
|
||||
targetEnvironmentId: selectedTargetEnvironment,
|
||||
});
|
||||
break;
|
||||
}
|
||||
await utils.environment.one.invalidate({
|
||||
environmentId,
|
||||
@@ -723,6 +753,11 @@ const EnvironmentPage = (
|
||||
mongoId: serviceId,
|
||||
});
|
||||
break;
|
||||
case "libsql":
|
||||
await libsqlActions.delete.mutateAsync({
|
||||
libsqlId: serviceId,
|
||||
});
|
||||
break;
|
||||
}
|
||||
await utils.environment.one.invalidate({
|
||||
environmentId,
|
||||
@@ -789,6 +824,11 @@ const EnvironmentPage = (
|
||||
mongoId: serviceId,
|
||||
});
|
||||
break;
|
||||
case "libsql":
|
||||
await libsqlActions.deploy.mutateAsync({
|
||||
libsqlId: serviceId,
|
||||
});
|
||||
break;
|
||||
}
|
||||
success++;
|
||||
} catch (error) {
|
||||
@@ -814,6 +854,110 @@ const EnvironmentPage = (
|
||||
setIsBulkActionLoading(false);
|
||||
};
|
||||
|
||||
const getServiceActions = (service: Services) => {
|
||||
switch (service.type) {
|
||||
case "application":
|
||||
return applicationActions;
|
||||
case "compose":
|
||||
return composeActions;
|
||||
case "postgres":
|
||||
return postgresActions;
|
||||
case "mysql":
|
||||
return mysqlActions;
|
||||
case "mariadb":
|
||||
return mariadbActions;
|
||||
case "redis":
|
||||
return redisActions;
|
||||
case "mongo":
|
||||
return mongoActions;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const getServiceIdKey = (service: Services) => {
|
||||
switch (service.type) {
|
||||
case "application":
|
||||
return "applicationId";
|
||||
case "compose":
|
||||
return "composeId";
|
||||
case "postgres":
|
||||
return "postgresId";
|
||||
case "mysql":
|
||||
return "mysqlId";
|
||||
case "mariadb":
|
||||
return "mariadbId";
|
||||
case "redis":
|
||||
return "redisId";
|
||||
case "mongo":
|
||||
return "mongoId";
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const handleServiceAction = async (
|
||||
service: Services,
|
||||
action: "start" | "stop" | "deploy",
|
||||
) => {
|
||||
const actions = getServiceActions(service);
|
||||
const idKey = getServiceIdKey(service);
|
||||
if (!actions || !idKey) return;
|
||||
|
||||
const actionLabels = {
|
||||
start: { loading: "Starting", success: "started", error: "starting" },
|
||||
stop: { loading: "Stopping", success: "stopped", error: "stopping" },
|
||||
deploy: {
|
||||
loading: "Deploying",
|
||||
success: "queued for deployment",
|
||||
error: "deploying",
|
||||
},
|
||||
};
|
||||
|
||||
const labels = actionLabels[action];
|
||||
|
||||
toast.promise(
|
||||
(async () => {
|
||||
await actions[action].mutateAsync({
|
||||
[idKey]: service.id,
|
||||
} as any);
|
||||
})(),
|
||||
{
|
||||
loading: `${labels.loading} ${service.name}...`,
|
||||
success: () => {
|
||||
utils.environment.one.invalidate({ environmentId });
|
||||
return `${service.name} ${labels.success} successfully`;
|
||||
},
|
||||
error: (error) =>
|
||||
`Error ${labels.error} ${service.name}: ${error instanceof Error ? error.message : "Unknown error"}`,
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
const handleServiceDelete = async (service: Services) => {
|
||||
const actions = getServiceActions(service);
|
||||
const idKey = getServiceIdKey(service);
|
||||
if (!actions || !idKey) return;
|
||||
|
||||
toast.promise(
|
||||
(async () => {
|
||||
await actions.delete.mutateAsync({
|
||||
[idKey]: service.id,
|
||||
} as any);
|
||||
})(),
|
||||
{
|
||||
loading: `Deleting ${service.name}...`,
|
||||
success: () => {
|
||||
utils.environment.one.invalidate({ environmentId });
|
||||
return `${service.name} deleted successfully`;
|
||||
},
|
||||
error: (error) =>
|
||||
`Error deleting ${service.name}: ${error instanceof Error ? error.message : "Unknown error"}`,
|
||||
},
|
||||
);
|
||||
setServiceToDelete(null);
|
||||
};
|
||||
|
||||
// Get unique servers from services
|
||||
const availableServers = useMemo(() => {
|
||||
if (!applications) return [];
|
||||
@@ -1472,110 +1616,156 @@ const EnvironmentPage = (
|
||||
<div className="flex w-full flex-col gap-4">
|
||||
<div className="gap-5 pb-10 grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-3">
|
||||
{filteredServices?.map((service) => (
|
||||
<Link
|
||||
key={service.id}
|
||||
href={`/dashboard/project/${projectId}/environment/${environmentId}/services/${service.type}/${service.id}`}
|
||||
className="block"
|
||||
>
|
||||
<Card className="flex flex-col group relative cursor-pointer bg-transparent transition-colors hover:bg-border">
|
||||
{service.serverId && (
|
||||
<div className="absolute -left-1 -top-2">
|
||||
<ServerIcon className="size-4 text-muted-foreground" />
|
||||
</div>
|
||||
)}
|
||||
<div className="absolute -right-1 -top-2">
|
||||
<StatusTooltip status={service.status} />
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={cn(
|
||||
"absolute -left-3 -bottom-3 size-9 translate-y-1 rounded-full p-0 transition-all duration-200 z-10 bg-background border",
|
||||
selectedServices.includes(service.id)
|
||||
? "opacity-100 translate-y-0"
|
||||
: "opacity-0 group-hover:translate-y-0 group-hover:opacity-100",
|
||||
)}
|
||||
onClick={(e) =>
|
||||
handleServiceSelect(service.id, e)
|
||||
}
|
||||
<ContextMenu key={service.id}>
|
||||
<ContextMenuTrigger asChild>
|
||||
<Link
|
||||
href={`/dashboard/project/${projectId}/environment/${environmentId}/services/${service.type}/${service.id}`}
|
||||
className="block"
|
||||
>
|
||||
<div className="h-full w-full flex items-center justify-center">
|
||||
<Checkbox
|
||||
checked={selectedServices.includes(
|
||||
service.id,
|
||||
)}
|
||||
className="data-[state=checked]:bg-primary"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center justify-between">
|
||||
<div className="flex flex-row items-center gap-2 justify-between w-full">
|
||||
<div className="flex flex-col gap-2">
|
||||
<span className="text-base flex items-center gap-2 font-medium leading-none flex-wrap">
|
||||
{service.name}
|
||||
</span>
|
||||
{service.description && (
|
||||
<span className="text-sm font-medium text-muted-foreground">
|
||||
{service.description}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<span className="text-sm font-medium text-muted-foreground self-start">
|
||||
{service.type === "postgres" && (
|
||||
<PostgresqlIcon className="h-7 w-7" />
|
||||
)}
|
||||
{service.type === "redis" && (
|
||||
<RedisIcon className="h-7 w-7" />
|
||||
)}
|
||||
{service.type === "mariadb" && (
|
||||
<MariadbIcon className="h-7 w-7" />
|
||||
)}
|
||||
{service.type === "mongo" && (
|
||||
<MongodbIcon className="h-7 w-7" />
|
||||
)}
|
||||
{service.type === "mysql" && (
|
||||
<MysqlIcon className="h-7 w-7" />
|
||||
)}
|
||||
{service.type === "application" &&
|
||||
(service.icon ? (
|
||||
// biome-ignore lint/performance/noImgElement: application icon is data URL
|
||||
<img
|
||||
src={service.icon}
|
||||
alt={service.name}
|
||||
className="size-7 object-contain"
|
||||
/>
|
||||
) : (
|
||||
<GlobeIcon className="h-6 w-6" />
|
||||
))}
|
||||
{service.type === "compose" && (
|
||||
<CircuitBoard className="h-6 w-6" />
|
||||
)}
|
||||
{service.type === "libsql" && (
|
||||
<LibsqlIcon className="h-6 w-6" />
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardFooter className="mt-auto">
|
||||
<div className="space-y-1 text-sm w-full">
|
||||
{service.serverName && (
|
||||
<div className="flex items-center gap-1.5 text-xs text-muted-foreground mb-1">
|
||||
<ServerIcon className="size-3" />
|
||||
<span className="truncate">
|
||||
{service.serverName}
|
||||
</span>
|
||||
<Card className="flex flex-col group relative cursor-pointer bg-transparent transition-colors hover:bg-border">
|
||||
{service.serverId && (
|
||||
<div className="absolute -left-1 -top-2">
|
||||
<ServerIcon className="size-4 text-muted-foreground" />
|
||||
</div>
|
||||
)}
|
||||
<DateTooltip date={service.createdAt}>
|
||||
Created
|
||||
</DateTooltip>
|
||||
</div>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
</Link>
|
||||
<div className="absolute -right-1 -top-2">
|
||||
<StatusTooltip status={service.status} />
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={cn(
|
||||
"absolute -left-3 -bottom-3 size-9 translate-y-1 rounded-full p-0 transition-all duration-200 z-10 bg-background border",
|
||||
selectedServices.includes(service.id)
|
||||
? "opacity-100 translate-y-0"
|
||||
: "opacity-0 group-hover:translate-y-0 group-hover:opacity-100",
|
||||
)}
|
||||
onClick={(e) =>
|
||||
handleServiceSelect(service.id, e)
|
||||
}
|
||||
>
|
||||
<div className="h-full w-full flex items-center justify-center">
|
||||
<Checkbox
|
||||
checked={selectedServices.includes(
|
||||
service.id,
|
||||
)}
|
||||
className="data-[state=checked]:bg-primary"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center justify-between">
|
||||
<div className="flex flex-row items-center gap-2 justify-between w-full">
|
||||
<div className="flex flex-col gap-2">
|
||||
<span className="text-base flex items-center gap-2 font-medium leading-none flex-wrap">
|
||||
{service.name}
|
||||
</span>
|
||||
{service.description && (
|
||||
<span className="text-sm font-medium text-muted-foreground">
|
||||
{service.description}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<span className="text-sm font-medium text-muted-foreground self-start">
|
||||
{service.type === "postgres" && (
|
||||
<PostgresqlIcon className="h-7 w-7" />
|
||||
)}
|
||||
{service.type === "redis" && (
|
||||
<RedisIcon className="h-7 w-7" />
|
||||
)}
|
||||
{service.type === "mariadb" && (
|
||||
<MariadbIcon className="h-7 w-7" />
|
||||
)}
|
||||
{service.type === "mongo" && (
|
||||
<MongodbIcon className="h-7 w-7" />
|
||||
)}
|
||||
{service.type === "mysql" && (
|
||||
<MysqlIcon className="h-7 w-7" />
|
||||
)}
|
||||
{service.type === "application" &&
|
||||
(service.icon ? (
|
||||
// biome-ignore lint/performance/noImgElement: application icon is data URL
|
||||
<img
|
||||
src={service.icon}
|
||||
alt={service.name}
|
||||
className="size-7 object-contain"
|
||||
/>
|
||||
) : (
|
||||
<GlobeIcon className="h-6 w-6" />
|
||||
))}
|
||||
{service.type === "compose" && (
|
||||
<CircuitBoard className="h-6 w-6" />
|
||||
)}
|
||||
{service.type === "libsql" && (
|
||||
<LibsqlIcon className="h-6 w-6" />
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardFooter className="mt-auto">
|
||||
<div className="space-y-1 text-sm w-full">
|
||||
{service.serverName && (
|
||||
<div className="flex items-center gap-1.5 text-xs text-muted-foreground mb-1">
|
||||
<ServerIcon className="size-3" />
|
||||
<span className="truncate">
|
||||
{service.serverName}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
<DateTooltip date={service.createdAt}>
|
||||
Created
|
||||
</DateTooltip>
|
||||
</div>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
</Link>
|
||||
</ContextMenuTrigger>
|
||||
{service.type !== "libsql" && (
|
||||
<ContextMenuContent className="w-48">
|
||||
<ContextMenuLabel className="truncate">
|
||||
{service.name}
|
||||
</ContextMenuLabel>
|
||||
<ContextMenuSeparator />
|
||||
<ContextMenuItem
|
||||
className="flex items-center gap-2"
|
||||
onClick={() =>
|
||||
handleServiceAction(service, "start")
|
||||
}
|
||||
>
|
||||
<Play className="size-4" />
|
||||
Start
|
||||
</ContextMenuItem>
|
||||
<ContextMenuItem
|
||||
className="flex items-center gap-2"
|
||||
onClick={() =>
|
||||
handleServiceAction(service, "deploy")
|
||||
}
|
||||
>
|
||||
<RefreshCw className="size-4" />
|
||||
Deploy
|
||||
</ContextMenuItem>
|
||||
<ContextMenuItem
|
||||
className="flex items-center gap-2 text-orange-500 focus:text-orange-500"
|
||||
onClick={() =>
|
||||
handleServiceAction(service, "stop")
|
||||
}
|
||||
>
|
||||
<Ban className="size-4" />
|
||||
Stop
|
||||
</ContextMenuItem>
|
||||
<ContextMenuSeparator />
|
||||
<ContextMenuItem
|
||||
className="flex items-center gap-2 text-red-500 focus:text-red-500"
|
||||
onClick={() => setServiceToDelete(service)}
|
||||
>
|
||||
<Trash2 className="size-4" />
|
||||
Delete
|
||||
</ContextMenuItem>
|
||||
</ContextMenuContent>
|
||||
)}
|
||||
</ContextMenu>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
@@ -1586,6 +1776,38 @@ const EnvironmentPage = (
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Single Service Delete Dialog */}
|
||||
<Dialog
|
||||
open={!!serviceToDelete}
|
||||
onOpenChange={(open) => !open && setServiceToDelete(null)}
|
||||
>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Delete Service</DialogTitle>
|
||||
<DialogDescription>
|
||||
Are you sure you want to delete{" "}
|
||||
<span className="font-semibold">{serviceToDelete?.name}</span>?
|
||||
This action cannot be undone.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setServiceToDelete(null)}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
variant="destructive"
|
||||
onClick={() => {
|
||||
if (serviceToDelete) {
|
||||
handleServiceDelete(serviceToDelete);
|
||||
}
|
||||
}}
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1664,7 +1886,7 @@ export async function getServerSideProps(
|
||||
return {
|
||||
redirect: {
|
||||
permanent: false,
|
||||
destination: "/dashboard/projects",
|
||||
destination: "/dashboard/home",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -490,7 +490,7 @@ export async function getServerSideProps(
|
||||
return {
|
||||
redirect: {
|
||||
permanent: false,
|
||||
destination: "/dashboard/projects",
|
||||
destination: "/dashboard/home",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ import { ShowSchedules } from "@/components/dashboard/application/schedules/show
|
||||
import { ShowVolumeBackups } from "@/components/dashboard/application/volume-backups/show-volume-backups";
|
||||
import { AddCommandCompose } from "@/components/dashboard/compose/advanced/add-command";
|
||||
import { IsolatedDeploymentTab } from "@/components/dashboard/compose/advanced/add-isolation";
|
||||
import { ShowComposeContainers } from "@/components/dashboard/compose/containers/show-compose-containers";
|
||||
import { DeleteService } from "@/components/dashboard/compose/delete-service";
|
||||
import { ShowGeneralCompose } from "@/components/dashboard/compose/general/show";
|
||||
import { ShowDockerLogsCompose } from "@/components/dashboard/compose/logs/show";
|
||||
@@ -60,6 +61,7 @@ type TabState =
|
||||
| "advanced"
|
||||
| "deployments"
|
||||
| "domains"
|
||||
| "containers"
|
||||
| "monitoring"
|
||||
| "volumeBackups";
|
||||
|
||||
@@ -231,6 +233,9 @@ const Service = (
|
||||
Deployments
|
||||
</TabsTrigger>
|
||||
)}
|
||||
{permissions?.service.read && (
|
||||
<TabsTrigger value="containers">Containers</TabsTrigger>
|
||||
)}
|
||||
{permissions?.service.create && (
|
||||
<TabsTrigger value="backups">Backups</TabsTrigger>
|
||||
)}
|
||||
@@ -298,6 +303,18 @@ const Service = (
|
||||
</div>
|
||||
</TabsContent>
|
||||
)}
|
||||
{permissions?.service.read && (
|
||||
<TabsContent value="containers">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<ShowComposeContainers
|
||||
serverId={data?.serverId || undefined}
|
||||
appName={data?.appName || ""}
|
||||
appType={data?.composeType || "docker-compose"}
|
||||
/>
|
||||
</div>
|
||||
</TabsContent>
|
||||
)}
|
||||
|
||||
{permissions?.monitoring.read && (
|
||||
<TabsContent value="monitoring">
|
||||
<div className="pt-2.5">
|
||||
@@ -475,7 +492,7 @@ export async function getServerSideProps(
|
||||
return {
|
||||
redirect: {
|
||||
permanent: false,
|
||||
destination: "/dashboard/projects",
|
||||
destination: "/dashboard/home",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -343,7 +343,7 @@ export async function getServerSideProps(
|
||||
return {
|
||||
redirect: {
|
||||
permanent: false,
|
||||
destination: "/dashboard/projects",
|
||||
destination: "/dashboard/home",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -372,7 +372,7 @@ export async function getServerSideProps(
|
||||
return {
|
||||
redirect: {
|
||||
permanent: false,
|
||||
destination: "/dashboard/projects",
|
||||
destination: "/dashboard/home",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -376,7 +376,7 @@ export async function getServerSideProps(
|
||||
return {
|
||||
redirect: {
|
||||
permanent: false,
|
||||
destination: "/dashboard/projects",
|
||||
destination: "/dashboard/home",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -353,7 +353,7 @@ export async function getServerSideProps(
|
||||
return {
|
||||
redirect: {
|
||||
permanent: false,
|
||||
destination: "/dashboard/projects",
|
||||
destination: "/dashboard/home",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -360,7 +360,7 @@ export async function getServerSideProps(
|
||||
return {
|
||||
redirect: {
|
||||
permanent: false,
|
||||
destination: "/dashboard/projects",
|
||||
destination: "/dashboard/home",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -363,7 +363,7 @@ export async function getServerSideProps(
|
||||
return {
|
||||
redirect: {
|
||||
permanent: false,
|
||||
destination: "/dashboard/projects",
|
||||
destination: "/dashboard/home",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ export async function getServerSideProps(
|
||||
return {
|
||||
redirect: {
|
||||
permanent: true,
|
||||
destination: "/dashboard/projects",
|
||||
destination: "/dashboard/home",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ export async function getServerSideProps(
|
||||
return {
|
||||
redirect: {
|
||||
permanent: true,
|
||||
destination: "/dashboard/projects",
|
||||
destination: "/dashboard/home",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ export async function getServerSideProps(
|
||||
return {
|
||||
redirect: {
|
||||
permanent: true,
|
||||
destination: "/dashboard/projects",
|
||||
destination: "/dashboard/home",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ export async function getServerSideProps(
|
||||
return {
|
||||
redirect: {
|
||||
permanent: true,
|
||||
destination: "/dashboard/projects",
|
||||
destination: "/dashboard/home",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ export async function getServerSideProps(
|
||||
return {
|
||||
redirect: {
|
||||
permanent: true,
|
||||
destination: "/dashboard/projects",
|
||||
destination: "/dashboard/home",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -45,7 +45,7 @@ export async function getServerSideProps(
|
||||
return {
|
||||
redirect: {
|
||||
permanent: true,
|
||||
destination: "/dashboard/projects",
|
||||
destination: "/dashboard/home",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -46,7 +46,7 @@ export async function getServerSideProps(
|
||||
return {
|
||||
redirect: {
|
||||
permanent: true,
|
||||
destination: "/dashboard/projects",
|
||||
destination: "/dashboard/home",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ export async function getServerSideProps(
|
||||
return {
|
||||
redirect: {
|
||||
permanent: true,
|
||||
destination: "/dashboard/projects",
|
||||
destination: "/dashboard/home",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -82,6 +82,16 @@ export default function Home({ IS_CLOUD }: Props) {
|
||||
});
|
||||
|
||||
if (error) {
|
||||
const isEmailNotVerified =
|
||||
error.code === "EMAIL_NOT_VERIFIED" ||
|
||||
error.message?.toLowerCase().includes("email not verified");
|
||||
if (isEmailNotVerified) {
|
||||
const msg =
|
||||
"Your email is not verified. We've sent a new verification link to your email.";
|
||||
toast.info(msg);
|
||||
setError(msg);
|
||||
return;
|
||||
}
|
||||
toast.error(error.message);
|
||||
setError(error.message || "An error occurred while logging in");
|
||||
return;
|
||||
@@ -96,7 +106,7 @@ export default function Home({ IS_CLOUD }: Props) {
|
||||
}
|
||||
|
||||
toast.success("Logged in successfully");
|
||||
router.push("/dashboard/projects");
|
||||
router.push("/dashboard/home");
|
||||
} catch {
|
||||
toast.error("An error occurred while logging in");
|
||||
} finally {
|
||||
@@ -123,7 +133,7 @@ export default function Home({ IS_CLOUD }: Props) {
|
||||
}
|
||||
|
||||
toast.success("Logged in successfully");
|
||||
router.push("/dashboard/projects");
|
||||
router.push("/dashboard/home");
|
||||
} catch {
|
||||
toast.error("An error occurred while verifying 2FA code");
|
||||
} finally {
|
||||
@@ -153,7 +163,7 @@ export default function Home({ IS_CLOUD }: Props) {
|
||||
}
|
||||
|
||||
toast.success("Logged in successfully");
|
||||
router.push("/dashboard/projects");
|
||||
router.push("/dashboard/home");
|
||||
} catch {
|
||||
toast.error("An error occurred while verifying backup code");
|
||||
} finally {
|
||||
@@ -398,7 +408,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
||||
return {
|
||||
redirect: {
|
||||
permanent: true,
|
||||
destination: "/dashboard/projects",
|
||||
destination: "/dashboard/home",
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -427,7 +437,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
||||
return {
|
||||
redirect: {
|
||||
permanent: true,
|
||||
destination: "/dashboard/projects",
|
||||
destination: "/dashboard/home",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -139,7 +139,7 @@ const Invitation = ({
|
||||
});
|
||||
|
||||
toast.success("Account created successfully");
|
||||
router.push("/dashboard/projects");
|
||||
router.push("/dashboard/home");
|
||||
} catch {
|
||||
toast.error("An error occurred while creating your account");
|
||||
}
|
||||
|
||||
@@ -303,7 +303,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
||||
return {
|
||||
redirect: {
|
||||
permanent: true,
|
||||
destination: "/dashboard/projects",
|
||||
destination: "/dashboard/home",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -10,12 +10,6 @@ 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,24 +41,12 @@ 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 {
|
||||
@@ -186,75 +174,33 @@ export const aiRouter = createTRPCRouter({
|
||||
});
|
||||
}
|
||||
}),
|
||||
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 }) => {
|
||||
create: adminProcedure.input(apiCreateAi).mutation(async ({ ctx, input }) => {
|
||||
return await saveAiSettings(ctx.session.activeOrganizationId, 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 }) => {
|
||||
update: adminProcedure.input(apiUpdateAi).mutation(async ({ ctx, input }) => {
|
||||
return await saveAiSettings(ctx.session.activeOrganizationId, input);
|
||||
}),
|
||||
|
||||
getAll: adminProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "List all AI providers",
|
||||
description: "Returns all AI provider configurations for the current organization.",
|
||||
},
|
||||
})
|
||||
.query(async ({ ctx }) => {
|
||||
getAll: adminProcedure.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
|
||||
.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 }) => {
|
||||
getEnabledProviders: protectedProcedure.query(async ({ ctx }) => {
|
||||
const settings = await getAiSettingsByOrganizationId(
|
||||
ctx.session.activeOrganizationId,
|
||||
);
|
||||
@@ -264,12 +210,6 @@ 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),
|
||||
@@ -328,12 +268,6 @@ ${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),
|
||||
@@ -368,12 +302,6 @@ ${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(),
|
||||
@@ -395,12 +323,6 @@ ${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,12 +79,6 @@ 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 {
|
||||
@@ -140,12 +134,6 @@ 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");
|
||||
@@ -201,12 +189,6 @@ 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, {
|
||||
@@ -236,12 +218,6 @@ 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");
|
||||
@@ -303,12 +279,6 @@ 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, {
|
||||
@@ -331,12 +301,6 @@ 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, {
|
||||
@@ -359,12 +323,6 @@ 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, {
|
||||
@@ -409,12 +367,6 @@ 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, {
|
||||
@@ -436,12 +388,6 @@ 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, {
|
||||
@@ -467,12 +413,6 @@ 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, {
|
||||
@@ -500,12 +440,6 @@ 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, {
|
||||
@@ -534,12 +468,6 @@ 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, {
|
||||
@@ -567,12 +495,6 @@ 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, {
|
||||
@@ -599,12 +521,6 @@ 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, {
|
||||
@@ -628,12 +544,6 @@ 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, {
|
||||
@@ -659,12 +569,6 @@ 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, {
|
||||
@@ -718,12 +622,6 @@ 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, {
|
||||
@@ -739,12 +637,6 @@ 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, {
|
||||
@@ -781,12 +673,6 @@ 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, {
|
||||
@@ -805,12 +691,6 @@ 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, {
|
||||
@@ -855,12 +735,6 @@ 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, {
|
||||
@@ -869,12 +743,6 @@ 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, {
|
||||
@@ -891,12 +759,6 @@ 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, {
|
||||
@@ -912,12 +774,6 @@ 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, {
|
||||
@@ -937,12 +793,6 @@ 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(),
|
||||
@@ -999,12 +849,6 @@ 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, {
|
||||
@@ -1029,12 +873,6 @@ 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) {
|
||||
@@ -1048,12 +886,6 @@ 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(),
|
||||
@@ -1090,12 +922,6 @@ 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, {
|
||||
@@ -1146,12 +972,6 @@ 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(),
|
||||
@@ -1284,12 +1104,6 @@ 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,12 +78,6 @@ 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 {
|
||||
@@ -158,12 +152,6 @@ 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);
|
||||
@@ -184,12 +172,6 @@ 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 {
|
||||
@@ -247,12 +229,6 @@ 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 {
|
||||
@@ -296,12 +272,6 @@ 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 {
|
||||
@@ -333,12 +303,6 @@ 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 {
|
||||
@@ -366,12 +330,6 @@ 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 {
|
||||
@@ -399,12 +357,6 @@ 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 {
|
||||
@@ -432,12 +384,6 @@ 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 {
|
||||
@@ -465,12 +411,6 @@ 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 {
|
||||
@@ -498,12 +438,6 @@ 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);
|
||||
@@ -517,12 +451,6 @@ 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,12 +25,6 @@ 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 {
|
||||
@@ -56,24 +50,11 @@ 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
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "List Bitbucket providers",
|
||||
description: "Returns all Bitbucket providers accessible to the current user within the active organization.",
|
||||
},
|
||||
})
|
||||
.query(async ({ ctx }) => {
|
||||
bitbucketProviders: protectedProcedure.query(async ({ ctx }) => {
|
||||
const accessibleIds = await getAccessibleGitProviderIds(ctx.session);
|
||||
|
||||
let result = await db.query.bitbucket.findMany({
|
||||
@@ -96,34 +77,16 @@ 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 {
|
||||
@@ -138,12 +101,6 @@ 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,12 +19,6 @@ 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) {
|
||||
@@ -47,12 +41,6 @@ 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);
|
||||
@@ -65,12 +53,6 @@ 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);
|
||||
@@ -89,14 +71,7 @@ export const certificateRouter = createTRPCRouter({
|
||||
await removeCertificateById(input.certificateId);
|
||||
return true;
|
||||
}),
|
||||
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 }) => {
|
||||
all: withPermission("certificate", "read").query(async ({ ctx }) => {
|
||||
return await db.query.certificates.findMany({
|
||||
where: eq(certificates.organizationId, ctx.session.activeOrganizationId),
|
||||
with: {
|
||||
@@ -105,12 +80,6 @@ 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,12 +13,6 @@ 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(),
|
||||
@@ -31,12 +25,6 @@ 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(),
|
||||
@@ -72,12 +60,6 @@ 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(),
|
||||
@@ -101,12 +83,6 @@ 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,12 +83,6 @@ 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 {
|
||||
@@ -139,12 +133,6 @@ 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");
|
||||
@@ -201,12 +189,6 @@ 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, {
|
||||
@@ -222,12 +204,6 @@ 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, {
|
||||
@@ -253,12 +229,6 @@ 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");
|
||||
@@ -309,12 +279,6 @@ 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, {
|
||||
@@ -324,12 +288,6 @@ 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, {
|
||||
@@ -346,12 +304,6 @@ 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, {
|
||||
@@ -362,12 +314,6 @@ 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, {
|
||||
@@ -376,12 +322,6 @@ 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),
|
||||
@@ -400,12 +340,6 @@ 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 {
|
||||
@@ -431,12 +365,6 @@ 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, {
|
||||
@@ -453,12 +381,6 @@ 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, {
|
||||
@@ -478,12 +400,6 @@ 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, {
|
||||
@@ -498,12 +414,6 @@ 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, {
|
||||
@@ -554,12 +464,6 @@ 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, {
|
||||
@@ -608,12 +512,6 @@ 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, {
|
||||
@@ -630,12 +528,6 @@ 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, {
|
||||
@@ -652,12 +544,6 @@ 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, {
|
||||
@@ -668,12 +554,6 @@ 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, {
|
||||
@@ -692,12 +572,6 @@ 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(),
|
||||
@@ -806,12 +680,6 @@ 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 {
|
||||
@@ -830,12 +698,6 @@ 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);
|
||||
@@ -845,12 +707,6 @@ 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, {
|
||||
@@ -903,12 +759,6 @@ 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(),
|
||||
@@ -946,12 +796,6 @@ 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(),
|
||||
@@ -1016,12 +860,6 @@ 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(),
|
||||
@@ -1134,12 +972,6 @@ 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, {
|
||||
@@ -1193,12 +1025,6 @@ 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(),
|
||||
@@ -1307,12 +1133,6 @@ 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,12 +34,6 @@ 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, {
|
||||
@@ -49,12 +43,6 @@ 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, {
|
||||
@@ -63,24 +51,11 @@ 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")
|
||||
.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(
|
||||
allCentralized: withPermission("deployment", "read").query(
|
||||
async ({ ctx }) => {
|
||||
const orgId = ctx.session.activeOrganizationId;
|
||||
const accessedServices =
|
||||
@@ -94,14 +69,7 @@ export const deploymentRouter = createTRPCRouter({
|
||||
},
|
||||
),
|
||||
|
||||
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 }) => {
|
||||
queueList: withPermission("deployment", "read").query(async ({ ctx }) => {
|
||||
const orgId = ctx.session.activeOrganizationId;
|
||||
let rows: QueueJobRow[];
|
||||
|
||||
@@ -148,12 +116,6 @@ 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, {
|
||||
@@ -169,12 +131,6 @@ 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),
|
||||
@@ -212,12 +168,6 @@ 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),
|
||||
@@ -239,43 +189,4 @@ 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,12 +22,6 @@ 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 {
|
||||
@@ -51,12 +45,6 @@ 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 {
|
||||
@@ -114,12 +102,6 @@ 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);
|
||||
@@ -131,26 +113,13 @@ export const destinationRouter = createTRPCRouter({
|
||||
}
|
||||
return destination;
|
||||
}),
|
||||
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 }) => {
|
||||
all: withPermission("destination", "read").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 {
|
||||
@@ -178,12 +147,6 @@ 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 {
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import {
|
||||
containerKill,
|
||||
containerRemove,
|
||||
containerRestart,
|
||||
containerStart,
|
||||
containerStop,
|
||||
findServerById,
|
||||
getConfig,
|
||||
getContainers,
|
||||
@@ -20,12 +23,6 @@ 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(),
|
||||
@@ -41,39 +38,111 @@ export const dockerRouter = createTRPCRouter({
|
||||
return await getContainers(input.serverId);
|
||||
}),
|
||||
|
||||
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.",
|
||||
},
|
||||
})
|
||||
restartContainer: withPermission("service", "read")
|
||||
.input(
|
||||
z.object({
|
||||
containerId: z
|
||||
.string()
|
||||
.min(1)
|
||||
.regex(containerIdRegex, "Invalid container id."),
|
||||
serverId: z.string().optional(),
|
||||
}),
|
||||
)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const result = await containerRestart(input.containerId);
|
||||
if (input.serverId) {
|
||||
const server = await findServerById(input.serverId);
|
||||
if (server.organizationId !== ctx.session?.activeOrganizationId) {
|
||||
throw new TRPCError({ code: "UNAUTHORIZED" });
|
||||
}
|
||||
}
|
||||
await containerRestart(input.containerId, input.serverId);
|
||||
await audit(ctx, {
|
||||
action: "start",
|
||||
resourceType: "docker",
|
||||
resourceId: input.containerId,
|
||||
resourceName: input.containerId,
|
||||
});
|
||||
return result;
|
||||
}),
|
||||
|
||||
startContainer: withPermission("service", "read")
|
||||
.input(
|
||||
z.object({
|
||||
containerId: z
|
||||
.string()
|
||||
.min(1)
|
||||
.regex(containerIdRegex, "Invalid container id."),
|
||||
serverId: z.string().optional(),
|
||||
}),
|
||||
)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
if (input.serverId) {
|
||||
const server = await findServerById(input.serverId);
|
||||
if (server.organizationId !== ctx.session?.activeOrganizationId) {
|
||||
throw new TRPCError({ code: "UNAUTHORIZED" });
|
||||
}
|
||||
}
|
||||
await containerStart(input.containerId, input.serverId);
|
||||
await audit(ctx, {
|
||||
action: "start",
|
||||
resourceType: "docker",
|
||||
resourceId: input.containerId,
|
||||
resourceName: input.containerId,
|
||||
});
|
||||
}),
|
||||
|
||||
stopContainer: withPermission("service", "read")
|
||||
.input(
|
||||
z.object({
|
||||
containerId: z
|
||||
.string()
|
||||
.min(1)
|
||||
.regex(containerIdRegex, "Invalid container id."),
|
||||
serverId: z.string().optional(),
|
||||
}),
|
||||
)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
if (input.serverId) {
|
||||
const server = await findServerById(input.serverId);
|
||||
if (server.organizationId !== ctx.session?.activeOrganizationId) {
|
||||
throw new TRPCError({ code: "UNAUTHORIZED" });
|
||||
}
|
||||
}
|
||||
await containerStop(input.containerId, input.serverId);
|
||||
await audit(ctx, {
|
||||
action: "stop",
|
||||
resourceType: "docker",
|
||||
resourceId: input.containerId,
|
||||
resourceName: input.containerId,
|
||||
});
|
||||
}),
|
||||
|
||||
killContainer: withPermission("service", "read")
|
||||
.input(
|
||||
z.object({
|
||||
containerId: z
|
||||
.string()
|
||||
.min(1)
|
||||
.regex(containerIdRegex, "Invalid container id."),
|
||||
serverId: z.string().optional(),
|
||||
}),
|
||||
)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
if (input.serverId) {
|
||||
const server = await findServerById(input.serverId);
|
||||
if (server.organizationId !== ctx.session?.activeOrganizationId) {
|
||||
throw new TRPCError({ code: "UNAUTHORIZED" });
|
||||
}
|
||||
}
|
||||
await containerKill(input.containerId, input.serverId);
|
||||
await audit(ctx, {
|
||||
action: "stop",
|
||||
resourceType: "docker",
|
||||
resourceId: input.containerId,
|
||||
resourceName: input.containerId,
|
||||
});
|
||||
}),
|
||||
|
||||
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
|
||||
@@ -100,12 +169,6 @@ 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
|
||||
@@ -126,12 +189,6 @@ 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(),
|
||||
@@ -154,12 +211,6 @@ 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."),
|
||||
@@ -182,12 +233,6 @@ 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."),
|
||||
@@ -205,12 +250,6 @@ 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."),
|
||||
@@ -228,12 +267,6 @@ 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,12 +33,6 @@ 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 {
|
||||
@@ -71,12 +65,6 @@ 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, {
|
||||
@@ -85,12 +73,6 @@ 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, {
|
||||
@@ -99,12 +81,6 @@ 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(
|
||||
@@ -114,12 +90,6 @@ 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) {
|
||||
@@ -131,12 +101,6 @@ 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);
|
||||
@@ -177,15 +141,7 @@ export const domainRouter = createTRPCRouter({
|
||||
}
|
||||
return result;
|
||||
}),
|
||||
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 }) => {
|
||||
one: protectedProcedure.input(apiFindDomain).query(async ({ input, ctx }) => {
|
||||
const domain = await findDomainById(input.domainId);
|
||||
const serviceId = domain.applicationId || domain.composeId;
|
||||
if (serviceId) {
|
||||
@@ -203,12 +159,6 @@ 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);
|
||||
@@ -243,12 +193,6 @@ 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,12 +63,6 @@ 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 {
|
||||
@@ -105,12 +99,6 @@ 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);
|
||||
@@ -149,12 +137,6 @@ 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 {
|
||||
@@ -201,12 +183,6 @@ 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 {
|
||||
@@ -253,12 +229,6 @@ 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 {
|
||||
@@ -326,12 +296,6 @@ 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 {
|
||||
@@ -379,12 +343,6 @@ 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,14 +21,7 @@ import {
|
||||
} from "@/server/db/schema";
|
||||
|
||||
export const gitProviderRouter = createTRPCRouter({
|
||||
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 }) => {
|
||||
getAll: protectedProcedure.query(async ({ ctx }) => {
|
||||
const accessibleIds = await getAccessibleGitProviderIds(ctx.session);
|
||||
|
||||
if (accessibleIds.size === 0) {
|
||||
@@ -53,12 +46,6 @@ 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);
|
||||
@@ -86,12 +73,6 @@ 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) {
|
||||
@@ -115,12 +96,6 @@ 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,12 +27,6 @@ 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 {
|
||||
@@ -59,26 +53,11 @@ export const giteaRouter = createTRPCRouter({
|
||||
}
|
||||
}),
|
||||
|
||||
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);
|
||||
}),
|
||||
one: protectedProcedure.input(apiFindOneGitea).query(async ({ input }) => {
|
||||
return await findGiteaById(input.giteaId);
|
||||
}),
|
||||
|
||||
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 }) => {
|
||||
giteaProviders: protectedProcedure.query(async ({ ctx }) => {
|
||||
const accessibleIds = await getAccessibleGitProviderIds(ctx.session);
|
||||
|
||||
let result = await db.query.gitea.findMany({
|
||||
@@ -109,12 +88,6 @@ 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;
|
||||
@@ -139,12 +112,6 @@ 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;
|
||||
@@ -173,12 +140,6 @@ 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 ?? "";
|
||||
@@ -199,12 +160,6 @@ 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) {
|
||||
@@ -233,12 +188,6 @@ 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,47 +22,20 @@ import {
|
||||
} from "@/server/db/schema";
|
||||
|
||||
export const githubRouter = createTRPCRouter({
|
||||
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);
|
||||
}),
|
||||
one: protectedProcedure.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
|
||||
.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 }) => {
|
||||
githubProviders: protectedProcedure.query(async ({ ctx }) => {
|
||||
const accessibleIds = await getAccessibleGitProviderIds(ctx.session);
|
||||
|
||||
let result = await db.query.github.findMany({
|
||||
@@ -93,12 +66,6 @@ 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 {
|
||||
@@ -112,12 +79,6 @@ 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,12 +27,6 @@ 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 {
|
||||
@@ -57,25 +51,10 @@ export const gitlabRouter = createTRPCRouter({
|
||||
});
|
||||
}
|
||||
}),
|
||||
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 }) => {
|
||||
one: protectedProcedure.input(apiFindOneGitlab).query(async ({ input }) => {
|
||||
return await findGitlabById(input.gitlabId);
|
||||
}),
|
||||
gitlabProviders: protectedProcedure.query(async ({ ctx }) => {
|
||||
const accessibleIds = await getAccessibleGitProviderIds(ctx.session);
|
||||
|
||||
let result = await db.query.gitlab.findMany({
|
||||
@@ -106,35 +85,17 @@ 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 {
|
||||
@@ -149,12 +110,6 @@ 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,7 +6,6 @@ import {
|
||||
findEnvironmentById,
|
||||
findLibsqlById,
|
||||
findProjectById,
|
||||
getAccessibleServerIds,
|
||||
getContainerLogs,
|
||||
IS_CLOUD,
|
||||
rebuildDatabase,
|
||||
@@ -17,6 +16,7 @@ import {
|
||||
stopService,
|
||||
stopServiceRemote,
|
||||
updateLibsqlById,
|
||||
getAccessibleServerIds,
|
||||
} from "@dokploy/server";
|
||||
import {
|
||||
addNewService,
|
||||
@@ -43,12 +43,6 @@ 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 {
|
||||
@@ -106,12 +100,6 @@ 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");
|
||||
@@ -130,12 +118,6 @@ 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, {
|
||||
@@ -161,12 +143,6 @@ 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, {
|
||||
@@ -192,12 +168,6 @@ 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, {
|
||||
@@ -260,12 +230,6 @@ 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, {
|
||||
@@ -318,12 +282,6 @@ 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, {
|
||||
@@ -342,12 +300,6 @@ 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");
|
||||
@@ -383,12 +335,6 @@ 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, {
|
||||
@@ -413,12 +359,6 @@ 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, {
|
||||
@@ -451,12 +391,6 @@ 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;
|
||||
@@ -483,12 +417,6 @@ 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(),
|
||||
@@ -525,12 +453,6 @@ 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, {
|
||||
@@ -547,12 +469,6 @@ 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,12 +54,6 @@ 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 {
|
||||
@@ -120,12 +114,6 @@ 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");
|
||||
@@ -143,12 +131,6 @@ 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, {
|
||||
@@ -173,12 +155,6 @@ 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, {
|
||||
@@ -204,12 +180,6 @@ 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, {
|
||||
@@ -243,12 +213,6 @@ 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, {
|
||||
@@ -286,12 +250,6 @@ 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, {
|
||||
@@ -310,12 +268,6 @@ 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");
|
||||
@@ -353,12 +305,6 @@ 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, {
|
||||
@@ -383,12 +329,6 @@ 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, {
|
||||
@@ -421,12 +361,6 @@ 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;
|
||||
@@ -453,12 +387,6 @@ 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),
|
||||
@@ -516,12 +444,6 @@ 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(),
|
||||
@@ -558,12 +480,6 @@ 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, {
|
||||
@@ -579,12 +495,6 @@ 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(),
|
||||
@@ -683,12 +593,6 @@ 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,12 +53,6 @@ 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 {
|
||||
@@ -123,12 +117,6 @@ 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");
|
||||
@@ -147,12 +135,6 @@ 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, {
|
||||
@@ -178,12 +160,6 @@ 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, {
|
||||
@@ -209,12 +185,6 @@ 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, {
|
||||
@@ -248,12 +218,6 @@ 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, {
|
||||
@@ -307,12 +271,6 @@ 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, {
|
||||
@@ -331,12 +289,6 @@ 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, {
|
||||
@@ -369,12 +321,6 @@ 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");
|
||||
@@ -413,12 +359,6 @@ 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, {
|
||||
@@ -443,12 +383,6 @@ 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;
|
||||
@@ -475,12 +409,6 @@ 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),
|
||||
@@ -531,12 +459,6 @@ 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(),
|
||||
@@ -573,12 +495,6 @@ 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, {
|
||||
@@ -595,12 +511,6 @@ 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(),
|
||||
@@ -694,12 +604,6 @@ 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,12 +75,6 @@ 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, {
|
||||
@@ -96,12 +90,6 @@ 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);
|
||||
@@ -128,12 +116,6 @@ 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);
|
||||
@@ -154,12 +136,6 @@ 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);
|
||||
@@ -186,12 +162,6 @@ 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, {
|
||||
@@ -205,12 +175,6 @@ 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,12 +54,6 @@ 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 {
|
||||
@@ -124,12 +118,6 @@ 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");
|
||||
@@ -147,12 +135,6 @@ 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, {
|
||||
@@ -178,12 +160,6 @@ 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, {
|
||||
@@ -208,12 +184,6 @@ 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, {
|
||||
@@ -247,12 +217,6 @@ 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, {
|
||||
@@ -306,12 +270,6 @@ 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, {
|
||||
@@ -330,12 +288,6 @@ 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, {
|
||||
@@ -367,12 +319,6 @@ 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");
|
||||
@@ -409,12 +355,6 @@ 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, {
|
||||
@@ -439,12 +379,6 @@ 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;
|
||||
@@ -471,12 +405,6 @@ 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),
|
||||
@@ -534,12 +462,6 @@ 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(),
|
||||
@@ -576,12 +498,6 @@ 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, {
|
||||
@@ -598,12 +514,6 @@ 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(),
|
||||
@@ -697,12 +607,6 @@ 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,12 +95,6 @@ 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 {
|
||||
@@ -120,12 +114,6 @@ 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 {
|
||||
@@ -152,12 +140,6 @@ 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 {
|
||||
@@ -175,12 +157,6 @@ 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 {
|
||||
@@ -203,12 +179,6 @@ 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 {
|
||||
@@ -239,12 +209,6 @@ 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 {
|
||||
@@ -259,12 +223,6 @@ 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 {
|
||||
@@ -287,12 +245,6 @@ 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 {
|
||||
@@ -324,12 +276,6 @@ 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 {
|
||||
@@ -352,12 +298,6 @@ 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 {
|
||||
@@ -376,12 +316,6 @@ 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 {
|
||||
@@ -412,12 +346,6 @@ 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 {
|
||||
@@ -436,12 +364,6 @@ 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 {
|
||||
@@ -460,12 +382,6 @@ 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 {
|
||||
@@ -496,12 +412,6 @@ 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 {
|
||||
@@ -520,12 +430,6 @@ 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 {
|
||||
@@ -554,12 +458,6 @@ 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);
|
||||
@@ -571,14 +469,7 @@ export const notificationRouter = createTRPCRouter({
|
||||
}
|
||||
return notification;
|
||||
}),
|
||||
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 }) => {
|
||||
all: withPermission("notification", "read").query(async ({ ctx }) => {
|
||||
return await db.query.notifications.findMany({
|
||||
with: {
|
||||
slack: true,
|
||||
@@ -599,12 +490,6 @@ 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"),
|
||||
@@ -666,12 +551,6 @@ 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 {
|
||||
@@ -690,12 +569,6 @@ 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 {
|
||||
@@ -725,12 +598,6 @@ 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 {
|
||||
@@ -749,12 +616,6 @@ 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 {
|
||||
@@ -773,12 +634,6 @@ 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 {
|
||||
@@ -808,12 +663,6 @@ 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 {
|
||||
@@ -837,12 +686,6 @@ 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 {
|
||||
@@ -864,12 +707,6 @@ 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 {
|
||||
@@ -899,12 +736,6 @@ 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 {
|
||||
@@ -923,12 +754,6 @@ 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 {
|
||||
@@ -947,12 +772,6 @@ 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 {
|
||||
@@ -979,12 +798,6 @@ 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 {
|
||||
@@ -1003,12 +816,6 @@ 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 {
|
||||
@@ -1027,12 +834,6 @@ 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 {
|
||||
@@ -1062,12 +863,6 @@ 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 {
|
||||
@@ -1087,12 +882,6 @@ 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 {
|
||||
@@ -1111,12 +900,6 @@ 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 {
|
||||
@@ -1146,12 +929,6 @@ 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 {
|
||||
@@ -1169,12 +946,6 @@ 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 {
|
||||
@@ -1196,12 +967,6 @@ 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 {
|
||||
@@ -1231,12 +996,6 @@ 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 {
|
||||
@@ -1254,14 +1013,7 @@ export const notificationRouter = createTRPCRouter({
|
||||
});
|
||||
}
|
||||
}),
|
||||
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(
|
||||
getEmailProviders: withPermission("notification", "read").query(
|
||||
async ({ ctx }) => {
|
||||
return await db.query.notifications.findMany({
|
||||
where: eq(
|
||||
|
||||
@@ -15,12 +15,6 @@ 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(),
|
||||
@@ -71,14 +65,7 @@ export const organizationRouter = createTRPCRouter({
|
||||
});
|
||||
return result;
|
||||
}),
|
||||
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 }) => {
|
||||
all: protectedProcedure.query(async ({ ctx }) => {
|
||||
const memberResult = await db.query.organization.findMany({
|
||||
where: (organization) =>
|
||||
exists(
|
||||
@@ -101,12 +88,6 @@ 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(),
|
||||
@@ -133,12 +114,6 @@ 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(),
|
||||
@@ -203,12 +178,6 @@ 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(),
|
||||
@@ -279,12 +248,6 @@ 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(),
|
||||
@@ -372,26 +335,13 @@ export const organizationRouter = createTRPCRouter({
|
||||
return created;
|
||||
}),
|
||||
|
||||
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 }) => {
|
||||
allInvitations: withPermission("member", "create").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({
|
||||
@@ -427,12 +377,6 @@ 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(),
|
||||
@@ -519,12 +463,6 @@ 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),
|
||||
@@ -571,14 +509,7 @@ export const organizationRouter = createTRPCRouter({
|
||||
});
|
||||
return { success: true };
|
||||
}),
|
||||
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 }) => {
|
||||
active: protectedProcedure.query(async ({ ctx }) => {
|
||||
if (!ctx.session.activeOrganizationId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -50,12 +50,6 @@ 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;
|
||||
@@ -79,15 +73,7 @@ export const patchRouter = createTRPCRouter({
|
||||
return result;
|
||||
}),
|
||||
|
||||
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 }) => {
|
||||
one: protectedProcedure.input(apiFindPatch).query(async ({ input, ctx }) => {
|
||||
const patch = await findPatchById(input.patchId);
|
||||
const serviceId = resolvePatchServiceId(patch);
|
||||
await checkServicePermissionAndAccess(ctx, serviceId, {
|
||||
@@ -97,12 +83,6 @@ 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"]) }),
|
||||
)
|
||||
@@ -114,12 +94,6 @@ 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);
|
||||
@@ -140,12 +114,6 @@ 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);
|
||||
@@ -165,12 +133,6 @@ 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);
|
||||
@@ -193,12 +155,6 @@ 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(),
|
||||
@@ -223,12 +179,6 @@ 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),
|
||||
@@ -252,12 +202,6 @@ 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),
|
||||
@@ -297,12 +241,6 @@ 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),
|
||||
@@ -353,12 +291,6 @@ 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),
|
||||
@@ -386,12 +318,6 @@ 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,12 +16,6 @@ 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 {
|
||||
@@ -45,12 +39,6 @@ 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 {
|
||||
@@ -70,12 +58,6 @@ 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);
|
||||
@@ -103,12 +85,6 @@ 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,7 +9,6 @@ import {
|
||||
findEnvironmentById,
|
||||
findPostgresById,
|
||||
findProjectById,
|
||||
getAccessibleServerIds,
|
||||
getContainerLogs,
|
||||
getMountPath,
|
||||
getServiceContainerCommand,
|
||||
@@ -22,6 +21,7 @@ import {
|
||||
stopService,
|
||||
stopServiceRemote,
|
||||
updatePostgresById,
|
||||
getAccessibleServerIds,
|
||||
} from "@dokploy/server";
|
||||
import { db } from "@dokploy/server/db";
|
||||
import {
|
||||
@@ -55,12 +55,6 @@ 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 {
|
||||
@@ -127,12 +121,6 @@ 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");
|
||||
@@ -151,12 +139,6 @@ 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, {
|
||||
@@ -182,12 +164,6 @@ 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, {
|
||||
@@ -212,12 +188,6 @@ 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, {
|
||||
@@ -251,12 +221,6 @@ 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, {
|
||||
@@ -312,12 +276,6 @@ 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, {
|
||||
@@ -336,12 +294,6 @@ 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");
|
||||
@@ -380,12 +332,6 @@ 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, {
|
||||
@@ -410,12 +356,6 @@ 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, {
|
||||
@@ -448,12 +388,6 @@ 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;
|
||||
@@ -481,12 +415,6 @@ 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),
|
||||
@@ -537,12 +465,6 @@ 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(),
|
||||
@@ -579,12 +501,6 @@ 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, {
|
||||
@@ -601,12 +517,6 @@ 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(),
|
||||
@@ -707,12 +617,6 @@ 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,12 +16,6 @@ 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, {
|
||||
@@ -31,12 +25,6 @@ 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(
|
||||
@@ -51,12 +39,6 @@ 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(
|
||||
@@ -77,12 +59,6 @@ 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,12 +67,6 @@ 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 {
|
||||
@@ -112,12 +106,6 @@ 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") {
|
||||
@@ -205,14 +193,7 @@ export const projectRouter = createTRPCRouter({
|
||||
}
|
||||
return project;
|
||||
}),
|
||||
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 }) => {
|
||||
all: protectedProcedure.query(async ({ ctx }) => {
|
||||
if (ctx.user.role !== "owner" && ctx.user.role !== "admin") {
|
||||
const { accessedProjects, accessedEnvironments, accessedServices } =
|
||||
await findMemberByUserId(ctx.user.id, ctx.session.activeOrganizationId);
|
||||
@@ -394,14 +375,7 @@ export const projectRouter = createTRPCRouter({
|
||||
});
|
||||
}),
|
||||
|
||||
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(
|
||||
allForPermissions: withPermission("member", "update").query(
|
||||
async ({ ctx }) => {
|
||||
return await db.query.projects.findMany({
|
||||
where: eq(projects.organizationId, ctx.session.activeOrganizationId),
|
||||
@@ -513,13 +487,149 @@ 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.",
|
||||
homeStats: protectedProcedure.query(async ({ ctx }) => {
|
||||
const isPrivileged = ctx.user.role === "owner" || ctx.user.role === "admin";
|
||||
|
||||
let accessedProjects: string[] = [];
|
||||
let accessedEnvironments: string[] = [];
|
||||
let accessedServices: string[] = [];
|
||||
|
||||
if (!isPrivileged) {
|
||||
const member = await findMemberByUserId(
|
||||
ctx.user.id,
|
||||
ctx.session.activeOrganizationId,
|
||||
);
|
||||
accessedProjects = member.accessedProjects;
|
||||
accessedEnvironments = member.accessedEnvironments;
|
||||
accessedServices = member.accessedServices;
|
||||
|
||||
if (accessedProjects.length === 0) {
|
||||
return {
|
||||
projects: 0,
|
||||
environments: 0,
|
||||
applications: 0,
|
||||
compose: 0,
|
||||
databases: 0,
|
||||
services: 0,
|
||||
status: { running: 0, error: 0, idle: 0 },
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const projectIdFilter = isPrivileged
|
||||
? eq(projects.organizationId, ctx.session.activeOrganizationId)
|
||||
: and(
|
||||
sql`${projects.projectId} IN (${sql.join(
|
||||
accessedProjects.map((id) => sql`${id}`),
|
||||
sql`, `,
|
||||
)})`,
|
||||
eq(projects.organizationId, ctx.session.activeOrganizationId),
|
||||
);
|
||||
|
||||
const environmentFilter = isPrivileged
|
||||
? undefined
|
||||
: accessedEnvironments.length === 0
|
||||
? sql`false`
|
||||
: sql`${environments.environmentId} IN (${sql.join(
|
||||
accessedEnvironments.map((envId) => sql`${envId}`),
|
||||
sql`, `,
|
||||
)})`;
|
||||
|
||||
const applyFilter = (col: AnyPgColumn) =>
|
||||
isPrivileged ? undefined : buildServiceFilter(col, accessedServices);
|
||||
|
||||
const rows = await db.query.projects.findMany({
|
||||
where: projectIdFilter,
|
||||
columns: { projectId: true },
|
||||
with: {
|
||||
environments: {
|
||||
where: environmentFilter,
|
||||
columns: { environmentId: true },
|
||||
with: {
|
||||
applications: {
|
||||
where: applyFilter(applications.applicationId),
|
||||
columns: { applicationStatus: true },
|
||||
},
|
||||
compose: {
|
||||
where: applyFilter(compose.composeId),
|
||||
columns: { composeStatus: true },
|
||||
},
|
||||
libsql: {
|
||||
where: applyFilter(libsql.libsqlId),
|
||||
columns: { applicationStatus: true },
|
||||
},
|
||||
mariadb: {
|
||||
where: applyFilter(mariadb.mariadbId),
|
||||
columns: { applicationStatus: true },
|
||||
},
|
||||
mongo: {
|
||||
where: applyFilter(mongo.mongoId),
|
||||
columns: { applicationStatus: true },
|
||||
},
|
||||
mysql: {
|
||||
where: applyFilter(mysql.mysqlId),
|
||||
columns: { applicationStatus: true },
|
||||
},
|
||||
postgres: {
|
||||
where: applyFilter(postgres.postgresId),
|
||||
columns: { applicationStatus: true },
|
||||
},
|
||||
redis: {
|
||||
where: applyFilter(redis.redisId),
|
||||
columns: { applicationStatus: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
});
|
||||
|
||||
let applicationsCount = 0;
|
||||
let composeCount = 0;
|
||||
let databasesCount = 0;
|
||||
let environmentsCount = 0;
|
||||
const status = { running: 0, error: 0, idle: 0 };
|
||||
const bump = (s?: string | null) => {
|
||||
if (s === "done") status.running++;
|
||||
else if (s === "error") status.error++;
|
||||
else status.idle++;
|
||||
};
|
||||
|
||||
for (const project of rows) {
|
||||
for (const env of project.environments) {
|
||||
environmentsCount++;
|
||||
applicationsCount += env.applications.length;
|
||||
composeCount += env.compose.length;
|
||||
databasesCount +=
|
||||
env.libsql.length +
|
||||
env.mariadb.length +
|
||||
env.mongo.length +
|
||||
env.mysql.length +
|
||||
env.postgres.length +
|
||||
env.redis.length;
|
||||
|
||||
for (const a of env.applications) bump(a.applicationStatus);
|
||||
for (const c of env.compose) bump(c.composeStatus);
|
||||
for (const s of env.libsql) bump(s.applicationStatus);
|
||||
for (const s of env.mariadb) bump(s.applicationStatus);
|
||||
for (const s of env.mongo) bump(s.applicationStatus);
|
||||
for (const s of env.mysql) bump(s.applicationStatus);
|
||||
for (const s of env.postgres) bump(s.applicationStatus);
|
||||
for (const s of env.redis) bump(s.applicationStatus);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
projects: rows.length,
|
||||
environments: environmentsCount,
|
||||
applications: applicationsCount,
|
||||
compose: composeCount,
|
||||
databases: databasesCount,
|
||||
services: applicationsCount + composeCount + databasesCount,
|
||||
status,
|
||||
};
|
||||
}),
|
||||
|
||||
search: protectedProcedure
|
||||
.input(
|
||||
z.object({
|
||||
q: z.string().optional(),
|
||||
@@ -597,12 +707,6 @@ 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 {
|
||||
@@ -630,12 +734,6 @@ 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 {
|
||||
@@ -684,12 +782,6 @@ 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,12 +15,6 @@ 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, {
|
||||
@@ -37,12 +31,6 @@ 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);
|
||||
@@ -53,12 +41,6 @@ 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);
|
||||
@@ -75,12 +57,6 @@ 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,12 +51,6 @@ 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 {
|
||||
@@ -114,12 +108,6 @@ 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");
|
||||
@@ -138,12 +126,6 @@ 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, {
|
||||
@@ -169,12 +151,6 @@ 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, {
|
||||
@@ -208,12 +184,6 @@ 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, {
|
||||
@@ -238,12 +208,6 @@ 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, {
|
||||
@@ -277,12 +241,6 @@ 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, {
|
||||
@@ -335,12 +293,6 @@ 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, {
|
||||
@@ -359,12 +311,6 @@ 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");
|
||||
@@ -400,12 +346,6 @@ 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, {
|
||||
@@ -430,12 +370,6 @@ 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;
|
||||
@@ -462,12 +396,6 @@ 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),
|
||||
@@ -518,12 +446,6 @@ 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(),
|
||||
@@ -560,12 +482,6 @@ 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, {
|
||||
@@ -581,12 +497,6 @@ 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(),
|
||||
@@ -680,12 +590,6 @@ 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,12 +23,6 @@ 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);
|
||||
@@ -41,12 +35,6 @@ 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);
|
||||
@@ -65,12 +53,6 @@ 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;
|
||||
@@ -100,26 +82,13 @@ export const registryRouter = createTRPCRouter({
|
||||
});
|
||||
return true;
|
||||
}),
|
||||
all: withPermission("registry", "read")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "List all registries",
|
||||
description: "Returns all Docker registry entries for the current organization.",
|
||||
},
|
||||
})
|
||||
.query(async ({ ctx }) => {
|
||||
all: withPermission("registry", "read").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);
|
||||
@@ -132,12 +101,6 @@ 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 {
|
||||
@@ -180,12 +143,6 @@ 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,12 +11,6 @@ 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 {
|
||||
@@ -46,12 +40,6 @@ 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,12 +22,6 @@ 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;
|
||||
@@ -60,12 +54,6 @@ 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);
|
||||
@@ -111,12 +99,6 @@ 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);
|
||||
@@ -147,12 +129,6 @@ 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(),
|
||||
@@ -194,12 +170,6 @@ 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);
|
||||
@@ -213,12 +183,6 @@ 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,12 +15,6 @@ 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, {
|
||||
@@ -37,12 +31,6 @@ 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);
|
||||
@@ -53,12 +41,6 @@ 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);
|
||||
@@ -75,12 +57,6 @@ 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,12 +48,6 @@ 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 {
|
||||
@@ -86,12 +80,6 @@ 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);
|
||||
@@ -113,26 +101,13 @@ 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")
|
||||
.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 }) => {
|
||||
all: withPermission("server", "read").query(async ({ ctx }) => {
|
||||
const accessibleIds = await getAccessibleServerIds(ctx.session);
|
||||
|
||||
const result = await db
|
||||
@@ -155,12 +130,6 @@ 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) {
|
||||
@@ -183,14 +152,7 @@ export const serverRouter = createTRPCRouter({
|
||||
where: eq(server.organizationId, ctx.session.activeOrganizationId),
|
||||
});
|
||||
}),
|
||||
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 }) => {
|
||||
count: protectedProcedure.query(async ({ ctx }) => {
|
||||
const organizations = await db.query.organization.findMany({
|
||||
where: eq(organization.ownerId, ctx.user.id),
|
||||
with: {
|
||||
@@ -202,14 +164,7 @@ export const serverRouter = createTRPCRouter({
|
||||
|
||||
return servers.length ?? 0;
|
||||
}),
|
||||
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 }) => {
|
||||
withSSHKey: withPermission("server", "read").query(async ({ ctx }) => {
|
||||
const accessibleIds = await getAccessibleServerIds(ctx.session);
|
||||
|
||||
const result = await db.query.server.findMany({
|
||||
@@ -229,14 +184,7 @@ export const serverRouter = createTRPCRouter({
|
||||
});
|
||||
return result.filter((s) => accessibleIds.has(s.serverId));
|
||||
}),
|
||||
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 }) => {
|
||||
buildServers: withPermission("server", "read").query(async ({ ctx }) => {
|
||||
const accessibleIds = await getAccessibleServerIds(ctx.session);
|
||||
|
||||
const result = await db.query.server.findMany({
|
||||
@@ -257,12 +205,6 @@ 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 {
|
||||
@@ -314,12 +256,6 @@ 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 {
|
||||
@@ -368,12 +304,6 @@ 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 {
|
||||
@@ -424,12 +354,6 @@ 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 {
|
||||
@@ -478,12 +402,6 @@ 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 {
|
||||
@@ -517,12 +435,6 @@ 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 {
|
||||
@@ -555,28 +467,14 @@ export const serverRouter = createTRPCRouter({
|
||||
throw error;
|
||||
}
|
||||
}),
|
||||
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 () => {
|
||||
publicIp: protectedProcedure.query(async () => {
|
||||
if (IS_CLOUD) {
|
||||
return "";
|
||||
}
|
||||
const ip = await getPublicIpWithFallback();
|
||||
return ip;
|
||||
}),
|
||||
getServerTime: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Get server time",
|
||||
description: "Returns the current server time and timezone. Returns null in cloud mode.",
|
||||
},
|
||||
})
|
||||
.query(() => {
|
||||
getServerTime: protectedProcedure.query(() => {
|
||||
if (IS_CLOUD) {
|
||||
return null;
|
||||
}
|
||||
@@ -586,12 +484,6 @@ 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,12 +25,6 @@ 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 {
|
||||
@@ -52,12 +46,6 @@ 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 {
|
||||
@@ -81,12 +69,6 @@ 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);
|
||||
@@ -99,27 +81,13 @@ export const sshRouter = createTRPCRouter({
|
||||
}
|
||||
return sshKey;
|
||||
}),
|
||||
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 }) => {
|
||||
all: withPermission("sshKeys", "read").query(async ({ ctx }) => {
|
||||
return await db.query.sshKeys.findMany({
|
||||
where: eq(sshKeys.organizationId, ctx.session.activeOrganizationId),
|
||||
orderBy: desc(sshKeys.createdAt),
|
||||
});
|
||||
}),
|
||||
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 }) => {
|
||||
allForApps: protectedProcedure.query(async ({ ctx }) => {
|
||||
return await db.query.sshKeys.findMany({
|
||||
columns: {
|
||||
sshKeyId: true,
|
||||
@@ -130,23 +98,11 @@ 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,14 +30,7 @@ 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
|
||||
.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 }) => {
|
||||
getCurrentPlan: protectedProcedure.query(async ({ ctx }) => {
|
||||
if (!IS_CLOUD) return null;
|
||||
const owner = await findUserById(ctx.user.ownerId);
|
||||
if (!owner?.stripeCustomerId) return null;
|
||||
@@ -78,14 +71,7 @@ export const stripeRouter = createTRPCRouter({
|
||||
return null;
|
||||
}),
|
||||
|
||||
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 }) => {
|
||||
getProducts: adminProcedure.query(async ({ ctx }) => {
|
||||
const user = await findUserById(ctx.user.ownerId);
|
||||
const stripeCustomerId = user.stripeCustomerId;
|
||||
|
||||
@@ -176,12 +162,6 @@ 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({
|
||||
@@ -242,14 +222,7 @@ export const stripeRouter = createTRPCRouter({
|
||||
|
||||
return { sessionId: session.id };
|
||||
}),
|
||||
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 }) => {
|
||||
createCustomerPortalSession: adminProcedure.mutation(async ({ ctx }) => {
|
||||
// Use the organization's owner account for billing portal
|
||||
const owner = await findUserById(ctx.user.ownerId);
|
||||
|
||||
@@ -280,12 +253,6 @@ 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({
|
||||
@@ -357,14 +324,7 @@ export const stripeRouter = createTRPCRouter({
|
||||
return { ok: true };
|
||||
}),
|
||||
|
||||
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(
|
||||
canCreateMoreServers: withPermission("server", "create").query(
|
||||
async ({ ctx }) => {
|
||||
const user = await findUserById(ctx.user.ownerId);
|
||||
const servers = await findServersByUserId(user.id);
|
||||
@@ -378,12 +338,6 @@ 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) {
|
||||
@@ -399,14 +353,7 @@ export const stripeRouter = createTRPCRouter({
|
||||
return { ok: true };
|
||||
}),
|
||||
|
||||
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 }) => {
|
||||
getInvoices: adminProcedure.query(async ({ ctx }) => {
|
||||
const user = await findUserById(ctx.user.ownerId);
|
||||
const stripeCustomerId = user.stripeCustomerId;
|
||||
|
||||
|
||||
@@ -13,12 +13,6 @@ 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(),
|
||||
@@ -28,23 +22,11 @@ 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(),
|
||||
@@ -76,12 +58,6 @@ 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,12 +16,6 @@ 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 {
|
||||
@@ -53,14 +47,7 @@ export const tagRouter = createTRPCRouter({
|
||||
}
|
||||
}),
|
||||
|
||||
all: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "List all tags",
|
||||
description: "Returns all tags for the current organization, ordered alphabetically by name.",
|
||||
},
|
||||
})
|
||||
.query(async ({ ctx }) => {
|
||||
all: protectedProcedure.query(async ({ ctx }) => {
|
||||
try {
|
||||
const organizationTags = await db.query.tags.findMany({
|
||||
where: eq(tags.organizationId, ctx.session.activeOrganizationId),
|
||||
@@ -77,15 +64,7 @@ export const tagRouter = createTRPCRouter({
|
||||
}
|
||||
}),
|
||||
|
||||
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 }) => {
|
||||
one: protectedProcedure.input(apiFindOneTag).query(async ({ input, ctx }) => {
|
||||
try {
|
||||
const tag = await db.query.tags.findFirst({
|
||||
where: and(
|
||||
@@ -115,12 +94,6 @@ 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 {
|
||||
@@ -171,12 +144,6 @@ 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 {
|
||||
@@ -212,12 +179,6 @@ 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),
|
||||
@@ -306,12 +267,6 @@ 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),
|
||||
@@ -392,12 +347,6 @@ 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,14 +60,7 @@ const apiCreateApiKey = z.object({
|
||||
});
|
||||
|
||||
export const userRouter = createTRPCRouter({
|
||||
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 }) => {
|
||||
all: withPermission("member", "read").query(async ({ ctx }) => {
|
||||
return await db.query.member.findMany({
|
||||
where: eq(member.organizationId, ctx.session.activeOrganizationId),
|
||||
with: {
|
||||
@@ -77,12 +70,6 @@ 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(),
|
||||
@@ -127,14 +114,7 @@ export const userRouter = createTRPCRouter({
|
||||
|
||||
return memberResult;
|
||||
}),
|
||||
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 }) => {
|
||||
session: publicProcedure.query(async ({ ctx }) => {
|
||||
if (!ctx.user || !ctx.session || !ctx.session.activeOrganizationId) {
|
||||
return null;
|
||||
}
|
||||
@@ -147,14 +127,7 @@ export const userRouter = createTRPCRouter({
|
||||
},
|
||||
};
|
||||
}),
|
||||
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 }) => {
|
||||
get: protectedProcedure.query(async ({ ctx }) => {
|
||||
const memberResult = await db.query.member.findFirst({
|
||||
where: and(
|
||||
eq(member.userId, ctx.user.id),
|
||||
@@ -171,24 +144,10 @@ export const userRouter = createTRPCRouter({
|
||||
|
||||
return memberResult;
|
||||
}),
|
||||
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 }) => {
|
||||
getPermissions: protectedProcedure.query(async ({ ctx }) => {
|
||||
return resolvePermissions(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 }) => {
|
||||
haveRootAccess: protectedProcedure.query(async ({ ctx }) => {
|
||||
if (!IS_CLOUD) {
|
||||
return false;
|
||||
}
|
||||
@@ -200,14 +159,7 @@ export const userRouter = createTRPCRouter({
|
||||
}
|
||||
return false;
|
||||
}),
|
||||
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 }) => {
|
||||
getBackups: adminProcedure.query(async ({ ctx }) => {
|
||||
const memberResult = await db.query.member.findFirst({
|
||||
where: and(
|
||||
eq(member.userId, ctx.user.id),
|
||||
@@ -230,14 +182,8 @@ export const userRouter = createTRPCRouter({
|
||||
|
||||
return memberResult?.user;
|
||||
}),
|
||||
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 }) => {
|
||||
getServerMetrics: withPermission("monitoring", "read").query(
|
||||
async ({ ctx }) => {
|
||||
const memberResult = await db.query.member.findFirst({
|
||||
where: and(
|
||||
eq(member.userId, ctx.user.id),
|
||||
@@ -252,12 +198,6 @@ 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) {
|
||||
@@ -308,24 +248,12 @@ 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")
|
||||
.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 }) => {
|
||||
getMetricsToken: withPermission("monitoring", "read").query(
|
||||
async ({ ctx }) => {
|
||||
const user = await findUserById(ctx.user.ownerId);
|
||||
const settings = await getWebServerSettings();
|
||||
return {
|
||||
@@ -336,12 +264,6 @@ 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(),
|
||||
@@ -411,12 +333,6 @@ 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 {
|
||||
@@ -467,14 +383,7 @@ export const userRouter = createTRPCRouter({
|
||||
throw error;
|
||||
}
|
||||
}),
|
||||
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 }) => {
|
||||
getInvitations: protectedProcedure.query(async ({ ctx }) => {
|
||||
return await db.query.invitation.findMany({
|
||||
where: and(
|
||||
eq(invitation.email, ctx.user.email),
|
||||
@@ -488,12 +397,6 @@ 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(),
|
||||
@@ -552,24 +455,11 @@ export const userRouter = createTRPCRouter({
|
||||
}
|
||||
}),
|
||||
|
||||
generateToken: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Generate authentication token",
|
||||
description: "Generate a new authentication token for the current user.",
|
||||
},
|
||||
})
|
||||
.mutation(async () => {
|
||||
generateToken: protectedProcedure.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(),
|
||||
@@ -609,12 +499,6 @@ 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
|
||||
@@ -645,12 +529,6 @@ 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(),
|
||||
@@ -692,12 +570,6 @@ 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(),
|
||||
@@ -729,12 +601,6 @@ 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),
|
||||
@@ -810,14 +676,7 @@ export const userRouter = createTRPCRouter({
|
||||
return inviteLink;
|
||||
}),
|
||||
|
||||
getBookmarkedTemplates: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Get bookmarked templates",
|
||||
description: "Retrieve the list of template IDs that the current user has bookmarked.",
|
||||
},
|
||||
})
|
||||
.query(async ({ ctx }) => {
|
||||
getBookmarkedTemplates: protectedProcedure.query(async ({ ctx }) => {
|
||||
const result = await db.query.user.findFirst({
|
||||
where: eq(user.id, ctx.user.id),
|
||||
columns: { bookmarkedTemplates: true },
|
||||
@@ -827,12 +686,6 @@ 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,12 +30,6 @@ 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),
|
||||
@@ -71,12 +65,6 @@ 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 =
|
||||
@@ -114,12 +102,6 @@ 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),
|
||||
@@ -144,12 +126,6 @@ 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),
|
||||
@@ -180,12 +156,6 @@ 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);
|
||||
@@ -246,12 +216,6 @@ 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,12 +13,7 @@ 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 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 type { OpenApiMeta } from "@dokploy/trpc-openapi";
|
||||
import { initTRPC, TRPCError } from "@trpc/server";
|
||||
import type { CreateNextContextOptions } from "@trpc/server/adapters/next";
|
||||
import type { Session, User } from "better-auth";
|
||||
|
||||
@@ -104,7 +104,10 @@ process.on("uncaughtException", (err) => {
|
||||
});
|
||||
|
||||
process.on("unhandledRejection", (reason, _promise) => {
|
||||
logger.error(reason instanceof Error ? reason : { reason: String(reason) }, "Unhandled Rejection at: Promise");
|
||||
logger.error(
|
||||
reason instanceof Error ? reason : { reason: String(reason) },
|
||||
"Unhandled Rejection at: Promise",
|
||||
);
|
||||
});
|
||||
|
||||
const port = Number.parseInt(process.env.PORT || "3000");
|
||||
|
||||
@@ -142,7 +142,10 @@ export const initializeJobs = async () => {
|
||||
cronSchedule: CLEANUP_CRON_JOB,
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error(error, `Failed to schedule cleanup job for server ${serverId}`);
|
||||
logger.error(
|
||||
error,
|
||||
`Failed to schedule cleanup job for server ${serverId}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -255,7 +258,10 @@ export const initializeJobs = async () => {
|
||||
cronSchedule: volumeBackup.cronExpression,
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error(error, `Failed to schedule volume backup ${volumeBackup.volumeBackupId}`);
|
||||
logger.error(
|
||||
error,
|
||||
`Failed to schedule volume backup ${volumeBackup.volumeBackupId}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
51932
openapi.json
51932
openapi.json
File diff suppressed because it is too large
Load Diff
104
packages/server/src/emails/emails/verify-email.tsx
Normal file
104
packages/server/src/emails/emails/verify-email.tsx
Normal file
@@ -0,0 +1,104 @@
|
||||
import {
|
||||
Body,
|
||||
Button,
|
||||
Container,
|
||||
Head,
|
||||
Heading,
|
||||
Html,
|
||||
Img,
|
||||
Link,
|
||||
Preview,
|
||||
Section,
|
||||
Tailwind,
|
||||
Text,
|
||||
} from "@react-email/components";
|
||||
|
||||
export type TemplateProps = {
|
||||
userName: string;
|
||||
verificationUrl: string;
|
||||
};
|
||||
|
||||
export const VerifyEmailTemplate = ({
|
||||
userName = "User",
|
||||
verificationUrl = "https://app.dokploy.com/verify",
|
||||
}: TemplateProps) => {
|
||||
const previewText = "Verify your email address to get started with Dokploy";
|
||||
return (
|
||||
<Html>
|
||||
<Head />
|
||||
<Preview>{previewText}</Preview>
|
||||
<Tailwind
|
||||
config={{
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
brand: "#007291",
|
||||
},
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Body className="bg-[#f4f4f5] my-auto mx-auto font-sans">
|
||||
<Container className="my-[40px] mx-auto max-w-[520px]">
|
||||
{/* Header */}
|
||||
<Section className="bg-[#09090b] rounded-t-xl px-[40px] py-[32px] text-center">
|
||||
<Img
|
||||
src="https://raw.githubusercontent.com/Dokploy/website/refs/heads/main/apps/docs/public/logo-dokploy-blackpng.png"
|
||||
width="190"
|
||||
height="120"
|
||||
alt="Dokploy"
|
||||
className="my-0 mx-auto"
|
||||
/>
|
||||
</Section>
|
||||
|
||||
{/* Body */}
|
||||
<Section className="bg-white px-[40px] py-[32px]">
|
||||
<Heading className="text-[#09090b] text-[22px] font-semibold m-0 mb-[8px]">
|
||||
Verify Your Email
|
||||
</Heading>
|
||||
<Text className="text-[#71717a] text-[14px] leading-[22px] m-0 mb-[24px]">
|
||||
Hello {userName}, thank you for signing up for Dokploy. Please
|
||||
verify your email address to activate your account.
|
||||
</Text>
|
||||
|
||||
{/* CTA Button */}
|
||||
<Section className="text-center mb-[24px]">
|
||||
<Button
|
||||
href={verificationUrl}
|
||||
className="bg-[#09090b] rounded-lg text-white text-[14px] font-semibold no-underline text-center px-[24px] py-[12px]"
|
||||
>
|
||||
Verify Email Address
|
||||
</Button>
|
||||
</Section>
|
||||
|
||||
<Text className="text-[#a1a1aa] text-[13px] leading-[20px] m-0 text-center mb-[16px]">
|
||||
If the button above doesn't work, copy and paste the following
|
||||
link into your browser:
|
||||
</Text>
|
||||
<Text className="text-[#71717a] text-[12px] leading-[18px] m-0 text-center break-all">
|
||||
{verificationUrl}
|
||||
</Text>
|
||||
</Section>
|
||||
|
||||
{/* Footer */}
|
||||
<Section className="bg-[#fafafa] rounded-b-xl px-[40px] py-[24px] text-center border-t border-solid border-[#e4e4e7]">
|
||||
<Text className="text-[#a1a1aa] text-[12px] leading-[18px] m-0">
|
||||
This is an automated email from{" "}
|
||||
<Link
|
||||
href="https://dokploy.com"
|
||||
className="text-[#71717a] underline"
|
||||
>
|
||||
Dokploy Cloud
|
||||
</Link>
|
||||
. If you didn't create an account, you can safely ignore this
|
||||
email.
|
||||
</Text>
|
||||
</Section>
|
||||
</Container>
|
||||
</Body>
|
||||
</Tailwind>
|
||||
</Html>
|
||||
);
|
||||
};
|
||||
|
||||
export default VerifyEmailTemplate;
|
||||
@@ -21,7 +21,10 @@ import {
|
||||
updateWebServerSettings,
|
||||
} from "../services/web-server-settings";
|
||||
import { getHubSpotUTK, submitToHubSpot } from "../utils/tracking/hubspot";
|
||||
import { sendEmail } from "../verification/send-verification-email";
|
||||
import {
|
||||
sendEmail,
|
||||
sendVerificationEmail,
|
||||
} from "../verification/send-verification-email";
|
||||
import { getPublicIpWithFallback } from "../wss/utils";
|
||||
import { ac, adminRole, memberRole, ownerRole } from "./access-control";
|
||||
|
||||
@@ -106,14 +109,13 @@ const { handler, api } = betterAuth({
|
||||
emailVerification: {
|
||||
sendOnSignUp: true,
|
||||
autoSignInAfterVerification: true,
|
||||
sendOnSignIn: true,
|
||||
sendVerificationEmail: async ({ user, url }) => {
|
||||
if (IS_CLOUD) {
|
||||
await sendEmail({
|
||||
await sendVerificationEmail({
|
||||
userName: user.name || "User",
|
||||
email: user.email,
|
||||
subject: "Verify your email",
|
||||
text: `
|
||||
<p>Click the link to verify your email: <a href="${url}">Verify Email</a></p>
|
||||
`,
|
||||
verificationUrl: url,
|
||||
});
|
||||
}
|
||||
},
|
||||
@@ -196,7 +198,7 @@ const { handler, api } = betterAuth({
|
||||
where: eq(schema.member.role, "owner"),
|
||||
});
|
||||
|
||||
if (!IS_CLOUD) {
|
||||
if (!IS_CLOUD && !isAdminPresent) {
|
||||
await updateWebServerSettings({
|
||||
serverIp: await getPublicIpWithFallback(),
|
||||
});
|
||||
|
||||
@@ -417,21 +417,58 @@ export const getContainerLogs = async (
|
||||
}
|
||||
};
|
||||
|
||||
export const containerRestart = async (containerId: string) => {
|
||||
try {
|
||||
const { stdout, stderr } = await execAsync(
|
||||
`docker container restart ${containerId}`,
|
||||
);
|
||||
export const containerRestart = async (
|
||||
containerId: string,
|
||||
serverId?: string,
|
||||
) => {
|
||||
const command = `docker container restart ${containerId}`;
|
||||
const { stderr } = serverId
|
||||
? await execAsyncRemote(serverId, command)
|
||||
: await execAsync(command);
|
||||
|
||||
if (stderr) {
|
||||
console.error(`Error: ${stderr}`);
|
||||
return;
|
||||
}
|
||||
if (stderr) {
|
||||
console.error(`Error: ${stderr}`);
|
||||
throw new Error(stderr);
|
||||
}
|
||||
};
|
||||
|
||||
const config = JSON.parse(stdout);
|
||||
export const containerStart = async (
|
||||
containerId: string,
|
||||
serverId?: string,
|
||||
) => {
|
||||
const command = `docker container start ${containerId}`;
|
||||
const { stderr } = serverId
|
||||
? await execAsyncRemote(serverId, command)
|
||||
: await execAsync(command);
|
||||
|
||||
return config;
|
||||
} catch {}
|
||||
if (stderr) {
|
||||
console.error(`Error: ${stderr}`);
|
||||
throw new Error(stderr);
|
||||
}
|
||||
};
|
||||
|
||||
export const containerStop = async (containerId: string, serverId?: string) => {
|
||||
const command = `docker container stop ${containerId}`;
|
||||
const { stderr } = serverId
|
||||
? await execAsyncRemote(serverId, command)
|
||||
: await execAsync(command);
|
||||
|
||||
if (stderr) {
|
||||
console.error(`Error: ${stderr}`);
|
||||
throw new Error(stderr);
|
||||
}
|
||||
};
|
||||
|
||||
export const containerKill = async (containerId: string, serverId?: string) => {
|
||||
const command = `docker container kill ${containerId}`;
|
||||
const { stderr } = serverId
|
||||
? await execAsyncRemote(serverId, command)
|
||||
: await execAsync(command);
|
||||
|
||||
if (stderr) {
|
||||
console.error(`Error: ${stderr}`);
|
||||
throw new Error(stderr);
|
||||
}
|
||||
};
|
||||
|
||||
export const containerRemove = async (
|
||||
|
||||
@@ -30,13 +30,9 @@ export const findPreviewDeploymentById = async (
|
||||
with: {
|
||||
domain: true,
|
||||
application: {
|
||||
with: {
|
||||
server: true,
|
||||
environment: {
|
||||
with: {
|
||||
project: true,
|
||||
},
|
||||
},
|
||||
columns: {
|
||||
applicationId: true,
|
||||
serverId: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1,429 +0,0 @@
|
||||
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"}`;
|
||||
}
|
||||
},
|
||||
}),
|
||||
};
|
||||
}
|
||||
@@ -1,319 +0,0 @@
|
||||
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),
|
||||
};
|
||||
}
|
||||
@@ -1,317 +0,0 @@
|
||||
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;
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user