mirror of
https://github.com/Dokploy/dokploy.git
synced 2026-06-16 20:55:21 +02:00
Compare commits
7 Commits
v0.29.2
...
feat/add-c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d54aa02ad2 | ||
|
|
734641b516 | ||
|
|
cee2e9f002 | ||
|
|
e0b4a13340 | ||
|
|
ec202c8c6e | ||
|
|
7e89eaed4a | ||
|
|
e508f3143f |
42
.github/workflows/sync-openapi-docs.yml
vendored
42
.github/workflows/sync-openapi-docs.yml
vendored
@@ -68,45 +68,3 @@ 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"
|
||||
|
||||
|
||||
83
.github/workflows/sync-version.yml
vendored
83
.github/workflows/sync-version.yml
vendored
@@ -1,83 +0,0 @@
|
||||
name: Sync version to MCP and CLI repos
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
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 }}"
|
||||
|
||||
1
apps/dokploy/.tool-embeddings.json
Normal file
1
apps/dokploy/.tool-embeddings.json
Normal file
File diff suppressed because one or more lines are too long
629
apps/dokploy/components/dashboard/ai-chat/chat-panel.tsx
Normal file
629
apps/dokploy/components/dashboard/ai-chat/chat-panel.tsx
Normal file
@@ -0,0 +1,629 @@
|
||||
"use client";
|
||||
|
||||
import { useChat } from "@ai-sdk/react";
|
||||
import type { ChatContext } from "@dokploy/server/utils/ai/chat-tools";
|
||||
import { DefaultChatTransport } from "ai";
|
||||
import {
|
||||
Bot,
|
||||
Check,
|
||||
ChevronDown,
|
||||
Loader2,
|
||||
Send,
|
||||
Trash2,
|
||||
Wrench,
|
||||
X,
|
||||
} from "lucide-react";
|
||||
import { useRouter } from "next/router";
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import ReactMarkdown from "react-markdown";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Collapsible,
|
||||
CollapsibleContent,
|
||||
CollapsibleTrigger,
|
||||
} from "@/components/ui/collapsible";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import {
|
||||
Sheet,
|
||||
SheetContent,
|
||||
SheetDescription,
|
||||
SheetHeader,
|
||||
SheetTitle,
|
||||
} from "@/components/ui/sheet";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { api } from "@/utils/api";
|
||||
|
||||
function useChatContext(): ChatContext {
|
||||
const router = useRouter();
|
||||
const { query, pathname } = router;
|
||||
|
||||
return useMemo(() => {
|
||||
const projectId =
|
||||
typeof query.projectId === "string" ? query.projectId : undefined;
|
||||
const environmentId =
|
||||
typeof query.environmentId === "string"
|
||||
? query.environmentId
|
||||
: undefined;
|
||||
const serverId =
|
||||
typeof query.serverId === "string" ? query.serverId : undefined;
|
||||
|
||||
const serviceParams = [
|
||||
{ key: "applicationId", type: "application" },
|
||||
{ key: "composeId", type: "compose" },
|
||||
{ key: "postgresId", type: "postgres" },
|
||||
{ key: "mysqlId", type: "mysql" },
|
||||
{ key: "redisId", type: "redis" },
|
||||
{ key: "mongoId", type: "mongo" },
|
||||
{ key: "mariadbId", type: "mariadb" },
|
||||
{ key: "libsqlId", type: "libsql" },
|
||||
] as const;
|
||||
|
||||
for (const { key, type } of serviceParams) {
|
||||
if (query[key] && typeof query[key] === "string") {
|
||||
return {
|
||||
type,
|
||||
id: query[key] as string,
|
||||
projectId,
|
||||
environmentId,
|
||||
serverId,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (query.projectId && typeof query.projectId === "string") {
|
||||
return {
|
||||
type: "project" as const,
|
||||
id: query.projectId,
|
||||
projectId,
|
||||
environmentId,
|
||||
serverId,
|
||||
};
|
||||
}
|
||||
return {
|
||||
type: "general" as const,
|
||||
id: "",
|
||||
projectId,
|
||||
environmentId,
|
||||
serverId,
|
||||
};
|
||||
}, [
|
||||
query.applicationId,
|
||||
query.composeId,
|
||||
query.postgresId,
|
||||
query.mysqlId,
|
||||
query.redisId,
|
||||
query.mongoId,
|
||||
query.mariadbId,
|
||||
query.libsqlId,
|
||||
query.projectId,
|
||||
query.environmentId,
|
||||
query.serverId,
|
||||
pathname,
|
||||
]);
|
||||
}
|
||||
|
||||
export function ChatPanel() {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [aiId, setAiId] = useState<string>("");
|
||||
const [input, setInput] = useState("");
|
||||
const scrollRef = useRef<HTMLDivElement>(null);
|
||||
const inputRef = useRef<HTMLTextAreaElement>(null);
|
||||
const context = useChatContext();
|
||||
const aiIdRef = useRef(aiId);
|
||||
const contextRef = useRef(context);
|
||||
aiIdRef.current = aiId;
|
||||
contextRef.current = context;
|
||||
|
||||
const { data: isCloud } = api.settings.isCloud.useQuery(undefined, {
|
||||
refetchOnWindowFocus: false,
|
||||
});
|
||||
|
||||
const { data: providers } = api.ai.getEnabledProviders.useQuery(undefined, {
|
||||
refetchOnWindowFocus: false,
|
||||
enabled: !isCloud,
|
||||
});
|
||||
|
||||
const enabledProviders = providers ?? [];
|
||||
|
||||
const STORAGE_KEY = "dokploy-chat-messages";
|
||||
const restoredRef = useRef(false);
|
||||
|
||||
const { messages, sendMessage, status, setMessages, addToolApprovalResponse } = useChat({
|
||||
id: "dokploy-chat",
|
||||
transport: new DefaultChatTransport({
|
||||
api: "/api/ai/chat",
|
||||
body: () => ({
|
||||
...(isCloud ? {} : { aiId: aiIdRef.current }),
|
||||
context: contextRef.current,
|
||||
}),
|
||||
}),
|
||||
});
|
||||
|
||||
const isLoading = status === "streaming" || status === "submitted";
|
||||
|
||||
// Restore messages from localStorage on mount
|
||||
useEffect(() => {
|
||||
if (restoredRef.current) return;
|
||||
restoredRef.current = true;
|
||||
try {
|
||||
const stored = localStorage.getItem(STORAGE_KEY);
|
||||
if (stored) {
|
||||
const parsed = JSON.parse(stored);
|
||||
if (Array.isArray(parsed) && parsed.length > 0) {
|
||||
setMessages(parsed);
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}, [setMessages]);
|
||||
|
||||
// Persist messages to localStorage
|
||||
useEffect(() => {
|
||||
if (!restoredRef.current) return;
|
||||
if (messages.length > 0) {
|
||||
try {
|
||||
const toStore = messages.slice(-50);
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(toStore));
|
||||
} catch {
|
||||
// localStorage full or unavailable — ignore
|
||||
}
|
||||
}
|
||||
}, [messages]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isCloud && !aiId && enabledProviders.length > 0 && enabledProviders[0]) {
|
||||
setAiId(enabledProviders[0].aiId);
|
||||
}
|
||||
}, [enabledProviders, aiId, isCloud]);
|
||||
|
||||
useEffect(() => {
|
||||
if (scrollRef.current) {
|
||||
scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
|
||||
}
|
||||
}, [messages, status]);
|
||||
|
||||
if (!isCloud && enabledProviders.length === 0) return null;
|
||||
|
||||
const handleSend = () => {
|
||||
if (!input.trim() || isLoading) return;
|
||||
if (!isCloud && !aiId) return;
|
||||
sendMessage({ text: input });
|
||||
setInput("");
|
||||
setTimeout(() => inputRef.current?.focus(), 0);
|
||||
};
|
||||
|
||||
const contextLabel =
|
||||
context.type === "general" ? "General" : context.type;
|
||||
|
||||
const lastMessage = messages[messages.length - 1];
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
onClick={() => setOpen(true)}
|
||||
variant="outline"
|
||||
className="fixed bottom-6 right-6 z-50 h-11 w-11 rounded-full shadow-md border"
|
||||
size="icon"
|
||||
>
|
||||
<Bot className="h-5 w-5" />
|
||||
</Button>
|
||||
|
||||
<Sheet open={open} onOpenChange={setOpen}>
|
||||
<SheetContent
|
||||
side="right"
|
||||
className="w-full sm:w-[480px] p-0 flex flex-col border-l outline-none"
|
||||
>
|
||||
<SheetHeader className="px-4 py-3 border-b shrink-0">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<Bot className="h-4 w-4 text-muted-foreground" />
|
||||
<SheetTitle className="text-sm font-medium">
|
||||
{isCloud ? "Dokploy Agent" : "AI Assistant"}
|
||||
</SheetTitle>
|
||||
{isLoading && (
|
||||
<span className="text-xs text-muted-foreground animate-pulse">
|
||||
working...
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<SheetDescription className="sr-only">
|
||||
Chat with AI to manage your infrastructure
|
||||
</SheetDescription>
|
||||
<div className="flex items-center gap-2 pt-1">
|
||||
{!isCloud && (
|
||||
<Select value={aiId} onValueChange={setAiId}>
|
||||
<SelectTrigger className="h-8 text-xs flex-1">
|
||||
<SelectValue placeholder="Select provider..." />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{enabledProviders.map((p) => (
|
||||
<SelectItem key={p.aiId} value={p.aiId}>
|
||||
{p.name} ({p.model})
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
)}
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="text-xs shrink-0 capitalize font-normal"
|
||||
>
|
||||
{contextLabel}
|
||||
</Badge>
|
||||
{messages.length > 0 && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-8 w-8 shrink-0"
|
||||
onClick={() => { setMessages([]); localStorage.removeItem(STORAGE_KEY); }}
|
||||
title="Clear chat"
|
||||
>
|
||||
<Trash2 className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</SheetHeader>
|
||||
|
||||
<div
|
||||
ref={scrollRef}
|
||||
className="flex-1 overflow-y-auto p-4 space-y-3"
|
||||
>
|
||||
{messages.length === 0 && (
|
||||
<div className="flex flex-col items-center justify-center h-full text-muted-foreground gap-3">
|
||||
<Bot className="h-8 w-8 opacity-30" />
|
||||
<p className="text-sm text-center">
|
||||
Ask me anything about your{" "}
|
||||
{context.type === "general"
|
||||
? "infrastructure"
|
||||
: context.type}
|
||||
</p>
|
||||
<div className="flex flex-wrap gap-1.5 justify-center">
|
||||
{(context.type === "application"
|
||||
? [
|
||||
"What's the status of this app?",
|
||||
"Why did the last build fail?",
|
||||
"Show me recent deployments",
|
||||
"Redeploy this app",
|
||||
]
|
||||
: context.type === "compose"
|
||||
? [
|
||||
"Show compose service status",
|
||||
"Why did the last deploy fail?",
|
||||
"Show me the domains",
|
||||
"Redeploy this service",
|
||||
]
|
||||
: context.type === "postgres" ||
|
||||
context.type === "mysql" ||
|
||||
context.type === "redis" ||
|
||||
context.type === "mongo" ||
|
||||
context.type === "mariadb" ||
|
||||
context.type === "libsql"
|
||||
? [
|
||||
`Show ${context.type} status`,
|
||||
"What's the connection info?",
|
||||
"Show recent deployments",
|
||||
"Restart this database",
|
||||
]
|
||||
: context.type === "project"
|
||||
? [
|
||||
"How many services do I have?",
|
||||
"Show me all environments",
|
||||
"Which services are failing?",
|
||||
]
|
||||
: [
|
||||
"List all my projects",
|
||||
"Show project overview",
|
||||
"What servers do I have?",
|
||||
]
|
||||
).map((suggestion) => (
|
||||
<Button
|
||||
key={suggestion}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="text-xs h-7 font-normal"
|
||||
onClick={() => setInput(suggestion)}
|
||||
>
|
||||
{suggestion}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{messages.map((message) => {
|
||||
if (message.role === "user") {
|
||||
return (
|
||||
<div key={message.id} className="flex justify-end">
|
||||
<div className="max-w-[85%] rounded-lg px-3 py-2 text-sm bg-muted">
|
||||
<p className="whitespace-pre-wrap">
|
||||
{message.parts
|
||||
.filter(
|
||||
(p): p is { type: "text"; text: string } =>
|
||||
p.type === "text",
|
||||
)
|
||||
.map((p) => p.text)
|
||||
.join("")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div key={message.id} className="flex justify-start">
|
||||
<div className="max-w-[90%] space-y-2">
|
||||
{message.parts.map((part, i) => {
|
||||
if (
|
||||
part.type === "text" &&
|
||||
(part as { text?: string }).text?.trim()
|
||||
) {
|
||||
return (
|
||||
<div
|
||||
key={`text-${message.id}-${i}`}
|
||||
className="rounded-lg border px-3 py-2 text-sm prose prose-sm dark:prose-invert max-w-none break-words"
|
||||
>
|
||||
<ReactMarkdown>
|
||||
{(part as { text: string }).text}
|
||||
</ReactMarkdown>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (part.type === "dynamic-tool") {
|
||||
return (
|
||||
<div
|
||||
key={part.toolCallId}
|
||||
className="rounded-lg border px-3 py-2"
|
||||
>
|
||||
<ToolCallDisplay
|
||||
toolCallId={part.toolCallId}
|
||||
toolName={part.toolName}
|
||||
state={part.state}
|
||||
input={(part as any).input}
|
||||
output={
|
||||
part.state === "output-available"
|
||||
? part.output
|
||||
: undefined
|
||||
}
|
||||
onApprove={(id) =>
|
||||
addToolApprovalResponse({
|
||||
id,
|
||||
approved: true,
|
||||
})
|
||||
}
|
||||
onDeny={(id) =>
|
||||
addToolApprovalResponse({
|
||||
id,
|
||||
approved: false,
|
||||
reason: "User denied",
|
||||
})
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (part.type === "reasoning") {
|
||||
return (
|
||||
<Collapsible key={`reasoning-${message.id}-${i}`}>
|
||||
<CollapsibleTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
className="flex items-center gap-1.5 text-xs text-muted-foreground hover:text-foreground transition-colors"
|
||||
>
|
||||
<Bot className="h-3 w-3" />
|
||||
<span>Thinking...</span>
|
||||
<ChevronDown className="h-3 w-3" />
|
||||
</button>
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent>
|
||||
<div className="mt-1 rounded-lg border px-3 py-2 text-xs text-muted-foreground italic">
|
||||
{(part as any).text ||
|
||||
(part as any).reasoning}
|
||||
</div>
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
{isLoading && lastMessage?.role === "user" && (
|
||||
<div className="flex justify-start">
|
||||
<div className="rounded-lg border px-3 py-2 flex items-center gap-2 text-xs text-muted-foreground">
|
||||
<Loader2 className="h-3 w-3 animate-spin" />
|
||||
Investigating...
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="border-t p-3 shrink-0 flex gap-2">
|
||||
<Textarea
|
||||
ref={inputRef}
|
||||
value={input}
|
||||
onChange={(e) => setInput(e.target.value)}
|
||||
placeholder={
|
||||
!isCloud && !aiId
|
||||
? "Select a provider first..."
|
||||
: "Ask anything..."
|
||||
}
|
||||
disabled={(!isCloud && !aiId) || isLoading}
|
||||
className="min-h-[40px] max-h-[120px] resize-none text-sm"
|
||||
rows={1}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter" && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
handleSend();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
size="icon"
|
||||
variant="outline"
|
||||
disabled={
|
||||
(!isCloud && !aiId) || !input.trim() || isLoading
|
||||
}
|
||||
className="shrink-0 h-10 w-10"
|
||||
onClick={handleSend}
|
||||
>
|
||||
<Send className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function ToolCallDisplay({
|
||||
toolCallId,
|
||||
toolName,
|
||||
state,
|
||||
input,
|
||||
output,
|
||||
onApprove,
|
||||
onDeny,
|
||||
}: {
|
||||
toolCallId: string;
|
||||
toolName: string;
|
||||
state: string;
|
||||
input?: unknown;
|
||||
output?: unknown;
|
||||
onApprove?: (id: string) => void;
|
||||
onDeny?: (id: string) => void;
|
||||
}) {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const isRunning =
|
||||
state === "input-streaming" || state === "input-available";
|
||||
const isDone = state === "output-available";
|
||||
const isError = state === "output-error";
|
||||
const needsApproval = state === "requires-approval";
|
||||
|
||||
const outputText = output
|
||||
? typeof output === "string"
|
||||
? output
|
||||
: JSON.stringify(output, null, 2)
|
||||
: null;
|
||||
|
||||
// Extract operationId and params from input
|
||||
const inputData = input as { operationId?: string; params?: Record<string, unknown> } | undefined;
|
||||
const operationId = inputData?.operationId;
|
||||
const params = inputData?.params;
|
||||
|
||||
// Format: "compose-one" → "compose → one"
|
||||
const displayLabel = operationId
|
||||
? operationId.replace("-", " → ")
|
||||
: toolName;
|
||||
|
||||
// Determine HTTP method hint from operationId
|
||||
const isReadOp = operationId?.match(/^(.*-)?(one|all|get|list|read|search|by)/i);
|
||||
|
||||
const StatusIcon = isRunning
|
||||
? () => <Loader2 className="h-3.5 w-3.5 animate-spin text-blue-500 shrink-0" />
|
||||
: isDone
|
||||
? () => <Check className="h-3.5 w-3.5 text-green-500 shrink-0" />
|
||||
: isError
|
||||
? () => <X className="h-3.5 w-3.5 text-red-500 shrink-0" />
|
||||
: () => <Wrench className="h-3.5 w-3.5 text-muted-foreground shrink-0" />;
|
||||
|
||||
if (needsApproval) {
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2 text-xs">
|
||||
<Wrench className="h-3.5 w-3.5 text-yellow-500 shrink-0" />
|
||||
<code className="font-mono text-xs font-medium">{displayLabel}</code>
|
||||
<Badge variant="outline" className="text-[10px] px-1 py-0 h-4 font-normal">
|
||||
write
|
||||
</Badge>
|
||||
</div>
|
||||
{params && Object.keys(params).length > 0 && (
|
||||
<div className="ml-5.5 flex flex-wrap gap-1">
|
||||
{Object.entries(params).map(([key, value]) => (
|
||||
<span key={key} className="text-[10px] bg-muted px-1.5 py-0.5 rounded font-mono">
|
||||
{key}={typeof value === "string" ? `"${value}"` : String(value)}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<div className="flex gap-1.5 ml-5.5">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="h-6 text-xs px-2"
|
||||
onClick={() => onApprove?.(toolCallId)}
|
||||
>
|
||||
<Check className="h-3 w-3 mr-1" />
|
||||
Approve
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-6 text-xs px-2"
|
||||
onClick={() => onDeny?.(toolCallId)}
|
||||
>
|
||||
<X className="h-3 w-3 mr-1" />
|
||||
Deny
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-1">
|
||||
<Collapsible open={isOpen} onOpenChange={setIsOpen}>
|
||||
<CollapsibleTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
className="flex items-center gap-2 text-xs w-full hover:bg-muted/50 rounded -mx-1 px-1 py-0.5 transition-colors"
|
||||
>
|
||||
<StatusIcon />
|
||||
<code className="font-mono text-xs font-medium">{displayLabel}</code>
|
||||
{isReadOp && (
|
||||
<Badge variant="secondary" className="text-[10px] px-1 py-0 h-4 font-normal">
|
||||
read
|
||||
</Badge>
|
||||
)}
|
||||
{params && Object.keys(params).length > 0 && (
|
||||
<span className="text-[10px] text-muted-foreground truncate">
|
||||
{Object.entries(params)
|
||||
.slice(0, 3)
|
||||
.map(([k, v]) => `${k}=${typeof v === "string" ? `"${String(v).slice(0, 20)}"` : String(v)}`)
|
||||
.join(", ")}
|
||||
{Object.keys(params).length > 3 ? ` +${Object.keys(params).length - 3}` : ""}
|
||||
</span>
|
||||
)}
|
||||
{(outputText || isRunning) && (
|
||||
<ChevronDown
|
||||
className={`h-3 w-3 ml-auto text-muted-foreground transition-transform shrink-0 ${isOpen ? "rotate-180" : ""}`}
|
||||
/>
|
||||
)}
|
||||
</button>
|
||||
</CollapsibleTrigger>
|
||||
{outputText && (
|
||||
<CollapsibleContent>
|
||||
<pre className="mt-1 ml-5.5 p-2 bg-muted/50 rounded text-[10px] overflow-x-auto max-h-[200px] overflow-y-auto leading-tight whitespace-pre-wrap break-words">
|
||||
{outputText}
|
||||
</pre>
|
||||
</CollapsibleContent>
|
||||
)}
|
||||
</Collapsible>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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 domain
|
||||
Use custom entrypoint for domina
|
||||
<br />
|
||||
"web" and/or "websecure" is used by default.
|
||||
</FormDescription>
|
||||
|
||||
@@ -1,290 +0,0 @@
|
||||
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>
|
||||
);
|
||||
};
|
||||
@@ -1,112 +0,0 @@
|
||||
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>
|
||||
);
|
||||
};
|
||||
@@ -1,119 +0,0 @@
|
||||
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,8 +10,6 @@ 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";
|
||||
@@ -125,14 +123,6 @@ 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 || ""}
|
||||
|
||||
@@ -1,291 +0,0 @@
|
||||
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,7 +166,6 @@ 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) +
|
||||
@@ -179,7 +178,6 @@ 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) +
|
||||
|
||||
@@ -79,11 +79,8 @@ export const columns: ColumnDef<LogEntry>[] = [
|
||||
: log.RequestPath}
|
||||
</div>
|
||||
<div className="flex flex-row gap-3 w-full">
|
||||
<Badge
|
||||
variant={getStatusColor(log.OriginStatus || log.DownstreamStatus)}
|
||||
>
|
||||
Status:{" "}
|
||||
{formatStatusLabel(log.OriginStatus || log.DownstreamStatus)}
|
||||
<Badge variant={getStatusColor(log.OriginStatus)}>
|
||||
Status: {formatStatusLabel(log.OriginStatus)}
|
||||
</Badge>
|
||||
<Badge variant={"secondary"}>
|
||||
Exec Time: {formatDuration(log.Duration)}
|
||||
|
||||
@@ -185,7 +185,7 @@ export const RequestsTable = ({ dateRange }: RequestsTableProps) => {
|
||||
<div className="flex flex-col gap-4 w-full overflow-auto">
|
||||
<div className="flex items-center gap-2 max-sm:flex-wrap">
|
||||
<Input
|
||||
placeholder="Filter by hostname..."
|
||||
placeholder="Filter by name..."
|
||||
value={search}
|
||||
onChange={(event) => setSearch(event.target.value)}
|
||||
className="md:max-w-sm"
|
||||
|
||||
@@ -167,7 +167,7 @@ export const SearchCommand = () => {
|
||||
<CommandGroup heading={"Application"} hidden={true}>
|
||||
<CommandItem
|
||||
onSelect={() => {
|
||||
router.push("/dashboard/home");
|
||||
router.push("/dashboard/projects");
|
||||
setOpen(false);
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -425,7 +425,7 @@ export const WelcomeSubscription = () => {
|
||||
onClick={() => {
|
||||
if (stepper.isLast) {
|
||||
setIsOpen(false);
|
||||
push("/dashboard/home");
|
||||
push("/dashboard/projects");
|
||||
} else {
|
||||
stepper.next();
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { api } from "@/utils/api";
|
||||
import { ChatPanel } from "../dashboard/ai-chat/chat-panel";
|
||||
import { ImpersonationBar } from "../dashboard/impersonation/impersonation-bar";
|
||||
import { HubSpotWidget } from "../shared/HubSpotWidget";
|
||||
import Page from "./side";
|
||||
@@ -23,6 +24,7 @@ export const DashboardLayout = ({ children }: Props) => {
|
||||
return (
|
||||
<>
|
||||
<Page>{children}</Page>
|
||||
<ChatPanel />
|
||||
{isChatEnabled && (
|
||||
<>
|
||||
<HubSpotWidget />
|
||||
|
||||
@@ -19,7 +19,6 @@ import {
|
||||
Forward,
|
||||
GalleryVerticalEnd,
|
||||
GitBranch,
|
||||
House,
|
||||
Key,
|
||||
KeyRound,
|
||||
Loader2,
|
||||
@@ -149,12 +148,6 @@ 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/home");
|
||||
router.push("/dashboard/projects");
|
||||
}}
|
||||
>
|
||||
Projects
|
||||
|
||||
@@ -44,7 +44,7 @@ export function SignInWithSSO({ children }: SignInWithSSOProps) {
|
||||
try {
|
||||
const { data, error } = await authClient.signIn.sso({
|
||||
email: values.email,
|
||||
callbackURL: "/dashboard/home",
|
||||
callbackURL: "/dashboard/projects",
|
||||
});
|
||||
if (error) {
|
||||
toast.error(error.message ?? "Failed to sign in with SSO");
|
||||
|
||||
@@ -116,14 +116,6 @@ 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);
|
||||
|
||||
@@ -1,198 +0,0 @@
|
||||
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.2",
|
||||
"version": "v0.29.0",
|
||||
"private": true,
|
||||
"license": "Apache-2.0",
|
||||
"type": "module",
|
||||
@@ -46,6 +46,7 @@
|
||||
"@ai-sdk/mistral": "^3.0.20",
|
||||
"@ai-sdk/openai": "^3.0.29",
|
||||
"@ai-sdk/openai-compatible": "^2.0.30",
|
||||
"@ai-sdk/react": "^3.0.156",
|
||||
"@better-auth/api-key": "1.5.4",
|
||||
"@better-auth/sso": "1.5.4",
|
||||
"@codemirror/autocomplete": "^6.18.6",
|
||||
@@ -57,7 +58,7 @@
|
||||
"@codemirror/search": "^6.6.0",
|
||||
"@codemirror/view": "^6.39.15",
|
||||
"@dokploy/server": "workspace:*",
|
||||
"@dokploy/trpc-openapi": "0.0.18",
|
||||
"@dokploy/trpc-openapi": "0.0.19",
|
||||
"@faker-js/faker": "^8.4.1",
|
||||
"@hookform/resolvers": "^5.2.2",
|
||||
"@octokit/auth-app": "^6.1.3",
|
||||
@@ -67,7 +68,6 @@
|
||||
"@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",
|
||||
@@ -147,7 +147,7 @@
|
||||
"shell-quote": "^1.8.1",
|
||||
"slugify": "^1.6.6",
|
||||
"sonner": "^1.7.4",
|
||||
"ssh2": "~1.16.0",
|
||||
"ssh2": "1.15.0",
|
||||
"stripe": "17.2.0",
|
||||
"superjson": "^2.2.2",
|
||||
"swagger-ui-react": "^5.31.2",
|
||||
|
||||
@@ -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/home"
|
||||
href="/dashboard/projects"
|
||||
className={buttonVariants({
|
||||
variant: "secondary",
|
||||
className: "flex flex-row gap-2",
|
||||
|
||||
251
apps/dokploy/pages/api/ai/chat.ts
Normal file
251
apps/dokploy/pages/api/ai/chat.ts
Normal file
@@ -0,0 +1,251 @@
|
||||
import { readFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { IS_CLOUD, validateRequest } from "@dokploy/server";
|
||||
import { getAiSettingById } from "@dokploy/server/services/ai";
|
||||
import {
|
||||
type ChatContext,
|
||||
getAllTools,
|
||||
} from "@dokploy/server/utils/ai/chat-tools";
|
||||
import {
|
||||
buildEndpointCatalog,
|
||||
createApiTool,
|
||||
} from "@dokploy/server/utils/ai/api-tool";
|
||||
import {
|
||||
getOrCreateEmbeddings,
|
||||
retrieveRelevantEndpoints,
|
||||
} from "@dokploy/server/utils/ai/tool-retrieval";
|
||||
import { selectAIProvider } from "@dokploy/server/utils/ai/select-ai-provider";
|
||||
import { createAnthropic } from "@ai-sdk/anthropic";
|
||||
import { convertToModelMessages, stepCountIs, streamText } from "ai";
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
let cachedSpec: any = null;
|
||||
|
||||
function getOpenApiSpec() {
|
||||
if (!cachedSpec) {
|
||||
try {
|
||||
const specPath = join(process.cwd(), "../../openapi.json");
|
||||
cachedSpec = JSON.parse(readFileSync(specPath, "utf-8"));
|
||||
} catch {
|
||||
cachedSpec = null;
|
||||
}
|
||||
}
|
||||
return cachedSpec;
|
||||
}
|
||||
|
||||
function buildContextBlock(context: ChatContext): string {
|
||||
if (context.type === "general") {
|
||||
return "CONTEXT: The user is on the general dashboard (no specific resource selected). Use project-all to list their projects if needed.";
|
||||
}
|
||||
|
||||
const lines: string[] = [];
|
||||
lines.push(
|
||||
`CONTEXT: The user is currently viewing a specific ${context.type}. The ${context.type}Id is "${context.id}".`,
|
||||
);
|
||||
lines.push(
|
||||
`When the user says "this app", "this service", "this database", "add env var", etc., they ALWAYS mean this ${context.type} (ID: "${context.id}"). NEVER ask which service they mean.`,
|
||||
);
|
||||
|
||||
if (context.projectId) {
|
||||
lines.push(`- projectId: "${context.projectId}"`);
|
||||
}
|
||||
if (context.environmentId) {
|
||||
lines.push(`- environmentId: "${context.environmentId}"`);
|
||||
}
|
||||
if (context.serverId) {
|
||||
lines.push(`- serverId: "${context.serverId}"`);
|
||||
}
|
||||
|
||||
lines.push(
|
||||
"Use these IDs directly when calling tools — do NOT ask the user for them. You already know exactly which resource the user is talking about.",
|
||||
);
|
||||
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
function buildSystemPrompt(context: ChatContext, catalog: string | null, endpointCount?: number) {
|
||||
const contextBlock = buildContextBlock(context);
|
||||
|
||||
return `You are an autonomous DevOps agent inside Dokploy (Docker-based PaaS). You take action immediately — you don't explain, you don't ask, you DO.
|
||||
|
||||
${contextBlock}
|
||||
|
||||
THINKING PROCESS (do this before EVERY action):
|
||||
1. Scan ALL section headers (## tag — description) in the ENDPOINT CATALOG to find which sections are relevant
|
||||
2. Read the endpoint descriptions in those sections to pick the right operationId
|
||||
3. Call the endpoint with the correct params — use the IDs from the context above
|
||||
|
||||
BEHAVIOR:
|
||||
- When the user asks you to do something → DO IT. Call the API right away.
|
||||
- When you need information → call the endpoint to get it. Never say "I can't access" or "I don't have the ability to".
|
||||
- When something fails → read the error, figure out the fix, and apply it. Don't stop to explain the error — fix it.
|
||||
- EVERY capability you need is in the ENDPOINT CATALOG below. If you think you can't do something, you're wrong — scan ALL sections again.
|
||||
- You already have all the IDs you need from the context above. NEVER ask the user for IDs, paths, or information you can discover by calling endpoints.
|
||||
- NEVER ask for confirmation or permission. The only exception is deleting a service entirely. For everything else (read, update, deploy, stop, start, restart) → just do it immediately.
|
||||
|
||||
KEY PATTERN: When you need to explore files, find paths, or check repository structure → use the "patch" section endpoints to browse directories and read files. NEVER ask the user for file paths.
|
||||
|
||||
DATA MODEL: Project → Environment → Services (application, compose, postgres, mysql, redis, mongo, mariadb, libsql). Each service has deployments with build logs.
|
||||
|
||||
TOOL: You have one tool "call_api". Pass operationId + params from the catalog.
|
||||
- ALWAYS pass required params (*) in the "params" object. Example: { "operationId": "domain-byComposeId", "params": { "composeId": "abc123" } }
|
||||
- Params: * = required, ? = optional, [a|b|c] = allowed values
|
||||
- GET = read-only (auto-executed). POST/PUT/DELETE = write (user approves).
|
||||
- If a call fails, read the error message and fix the params. NEVER retry the same call with the same params.
|
||||
|
||||
RESPONSE STYLE:
|
||||
- 2-3 sentences max. No walls of text.
|
||||
- Never explain limitations — find the right endpoint and act.
|
||||
- Answer in the user's language.
|
||||
|
||||
${catalog ? `ENDPOINT CATALOG (${endpointCount} endpoints):\n${catalog}` : ""}`;
|
||||
}
|
||||
|
||||
function getUserMessages(messages: any[]): string {
|
||||
const texts: string[] = [];
|
||||
for (const msg of messages) {
|
||||
if (msg.role !== "user") continue;
|
||||
if (typeof msg.content === "string") {
|
||||
texts.push(msg.content);
|
||||
} else if (Array.isArray(msg.content)) {
|
||||
texts.push(
|
||||
msg.content
|
||||
.filter((p: any) => p.type === "text")
|
||||
.map((p: any) => p.text)
|
||||
.join(" "),
|
||||
);
|
||||
}
|
||||
}
|
||||
return texts.slice(-3).join(". ");
|
||||
}
|
||||
|
||||
export default async function handler(
|
||||
req: NextApiRequest,
|
||||
res: NextApiResponse,
|
||||
) {
|
||||
if (req.method !== "POST") {
|
||||
return res.status(405).json({ error: "Method not allowed" });
|
||||
}
|
||||
|
||||
try {
|
||||
const { session, user } = await validateRequest(req);
|
||||
if (!user || !session) {
|
||||
return res.status(401).json({ error: "Unauthorized" });
|
||||
}
|
||||
|
||||
const body = req.body;
|
||||
const messages = body.messages;
|
||||
const aiId = body.aiId;
|
||||
const context = (body.context as ChatContext) || {
|
||||
type: "general" as const,
|
||||
id: "",
|
||||
};
|
||||
|
||||
// ─── Resolve model ────────────────────────────────────────
|
||||
let model: any;
|
||||
|
||||
if (IS_CLOUD && process.env.CLOUD_ANTHROPIC_API_KEY) {
|
||||
const anthropic = createAnthropic({
|
||||
apiKey: process.env.CLOUD_ANTHROPIC_API_KEY,
|
||||
});
|
||||
model = anthropic("claude-haiku-4-5-20251001");
|
||||
} else {
|
||||
if (!aiId || !messages) {
|
||||
return res
|
||||
.status(400)
|
||||
.json({ error: "Missing aiId or messages" });
|
||||
}
|
||||
const aiSettings = await getAiSettingById(aiId);
|
||||
if (!aiSettings || !aiSettings.isEnabled) {
|
||||
return res
|
||||
.status(400)
|
||||
.json({ error: "AI provider not enabled" });
|
||||
}
|
||||
const provider = selectAIProvider(aiSettings);
|
||||
model = provider(aiSettings.model);
|
||||
}
|
||||
|
||||
if (!messages) {
|
||||
return res.status(400).json({ error: "Missing messages" });
|
||||
}
|
||||
|
||||
// ─── Resolve tools ────────────────────────────────────────
|
||||
const protocol = req.headers["x-forwarded-proto"] || "http";
|
||||
const host = req.headers.host || "localhost:3000";
|
||||
const toolConfig = {
|
||||
baseUrl: `${protocol}://${host}/api`,
|
||||
cookie: req.headers.cookie || "",
|
||||
};
|
||||
|
||||
let tools: Record<string, any>;
|
||||
let catalogText: string | null = null;
|
||||
let endpointCount = 0;
|
||||
const spec = getOpenApiSpec();
|
||||
|
||||
if (spec) {
|
||||
const voyageApiKey = process.env.VOYAGE_API_KEY;
|
||||
if (!voyageApiKey) {
|
||||
return res.status(400).json({ error: "VOYAGE_API_KEY is required" });
|
||||
}
|
||||
|
||||
const embeddingsPath = join(process.cwd(), ".tool-embeddings.json");
|
||||
const allEmbeddings = await getOrCreateEmbeddings(
|
||||
spec,
|
||||
voyageApiKey,
|
||||
embeddingsPath,
|
||||
);
|
||||
|
||||
const userQuery = getUserMessages(messages).trim();
|
||||
const { operationIds: tagFilteredIds } = buildEndpointCatalog(spec, context.type);
|
||||
|
||||
let relevantIds: Set<string> | undefined;
|
||||
|
||||
if (userQuery && allEmbeddings.length > 0) {
|
||||
const topIds = await retrieveRelevantEndpoints(
|
||||
userQuery,
|
||||
allEmbeddings,
|
||||
voyageApiKey,
|
||||
{ allowedOperationIds: tagFilteredIds, topK: 25 },
|
||||
);
|
||||
|
||||
if (topIds.length > 0) {
|
||||
relevantIds = new Set(topIds);
|
||||
}
|
||||
}
|
||||
|
||||
const { catalog, count, operationIds } = buildEndpointCatalog(
|
||||
spec,
|
||||
context.type,
|
||||
relevantIds,
|
||||
);
|
||||
catalogText = catalog;
|
||||
endpointCount = count;
|
||||
tools = createApiTool(spec, toolConfig, operationIds, 8000);
|
||||
} else {
|
||||
tools = getAllTools(context, toolConfig);
|
||||
}
|
||||
|
||||
// ─── Stream response ──────────────────────────────────────
|
||||
const modelMessages = await convertToModelMessages(messages);
|
||||
|
||||
const result = streamText({
|
||||
model,
|
||||
system: buildSystemPrompt(context, catalogText, endpointCount),
|
||||
messages: modelMessages,
|
||||
tools,
|
||||
stopWhen: stepCountIs(12),
|
||||
});
|
||||
|
||||
// Disable buffering for streaming
|
||||
res.setHeader("X-Accel-Buffering", "no");
|
||||
res.setHeader("Cache-Control", "no-cache, no-transform");
|
||||
|
||||
result.pipeUIMessageStreamToResponse(res);
|
||||
} catch (error) {
|
||||
console.error("AI chat error:", error);
|
||||
return res.status(500).json({
|
||||
error:
|
||||
error instanceof Error ? error.message : "Internal server error",
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -12,15 +12,6 @@ import type { DeploymentJob } from "@/server/queues/queue-types";
|
||||
import { myQueue } from "@/server/queues/queueSetup";
|
||||
import { deploy } from "@/server/utils/deploy";
|
||||
|
||||
/**
|
||||
* Log a webhook handler error server-side without leaking its shape to the HTTP
|
||||
* response. Drizzle errors carry the raw SQL query, column list and parameters,
|
||||
* so we never forward the error object to the client.
|
||||
*/
|
||||
export const logWebhookError = (context: string, error: unknown) => {
|
||||
console.error(context, error);
|
||||
};
|
||||
|
||||
/**
|
||||
* Helper function to get package_version from registry_package events
|
||||
*/
|
||||
@@ -271,15 +262,14 @@ export default async function handler(
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
logWebhookError("Error deploying Application:", error);
|
||||
res.status(400).json({ message: "Error deploying Application" });
|
||||
res.status(400).json({ message: "Error deploying Application", error });
|
||||
return;
|
||||
}
|
||||
|
||||
res.status(200).json({ message: "Application deployed successfully" });
|
||||
} catch (error) {
|
||||
logWebhookError("Error deploying Application:", error);
|
||||
res.status(400).json({ message: "Error deploying Application" });
|
||||
console.log(error);
|
||||
res.status(400).json({ message: "Error deploying Application", error });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -12,7 +12,6 @@ import {
|
||||
extractCommittedPaths,
|
||||
extractHash,
|
||||
getProviderByHeader,
|
||||
logWebhookError,
|
||||
} from "../[refreshToken]";
|
||||
|
||||
export default async function handler(
|
||||
@@ -196,14 +195,13 @@ export default async function handler(
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
logWebhookError("Error deploying Compose:", error);
|
||||
res.status(400).json({ message: "Error deploying Compose" });
|
||||
res.status(400).json({ message: "Error deploying Compose", error });
|
||||
return;
|
||||
}
|
||||
|
||||
res.status(200).json({ message: "Compose deployed successfully" });
|
||||
} catch (error) {
|
||||
logWebhookError("Error deploying Compose:", error);
|
||||
res.status(400).json({ message: "Error deploying Compose" });
|
||||
console.log(error);
|
||||
res.status(400).json({ message: "Error deploying Compose", error });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,11 +17,7 @@ import { applications, compose, github } from "@/server/db/schema";
|
||||
import type { DeploymentJob } from "@/server/queues/queue-types";
|
||||
import { myQueue } from "@/server/queues/queueSetup";
|
||||
import { deploy } from "@/server/utils/deploy";
|
||||
import {
|
||||
extractCommitMessage,
|
||||
extractHash,
|
||||
logWebhookError,
|
||||
} from "./[refreshToken]";
|
||||
import { extractCommitMessage, extractHash } from "./[refreshToken]";
|
||||
|
||||
export default async function handler(
|
||||
req: NextApiRequest,
|
||||
@@ -201,8 +197,10 @@ export default async function handler(
|
||||
});
|
||||
return;
|
||||
} catch (error) {
|
||||
logWebhookError("Error deploying applications on tag:", error);
|
||||
res.status(400).json({ message: "Error deploying applications on tag" });
|
||||
console.error("Error deploying applications on tag:", error);
|
||||
res
|
||||
.status(400)
|
||||
.json({ message: "Error deploying applications on tag", error });
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -324,8 +322,7 @@ export default async function handler(
|
||||
}
|
||||
res.status(200).json({ message: `Deployed ${totalApps} apps` });
|
||||
} catch (error) {
|
||||
logWebhookError("Error deploying Application:", error);
|
||||
res.status(400).json({ message: "Error deploying Application" });
|
||||
res.status(400).json({ message: "Error deploying Application", error });
|
||||
}
|
||||
} else if (req.headers["x-github-event"] === "pull_request") {
|
||||
const prId = githubBody?.pull_request?.id;
|
||||
|
||||
@@ -40,7 +40,7 @@ function DeploymentsPage() {
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
<Card className="h-full bg-sidebar p-2.5 rounded-xl min-h-[45vh]">
|
||||
<Card className="h-full bg-sidebar p-2.5 rounded-xl max-w-8xl mx-auto 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/home",
|
||||
destination: "/dashboard/projects",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ export async function getServerSideProps(
|
||||
return {
|
||||
redirect: {
|
||||
permanent: true,
|
||||
destination: "/dashboard/home",
|
||||
destination: "/dashboard/projects",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
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/home",
|
||||
destination: "/dashboard/projects",
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -122,7 +122,7 @@ export async function getServerSideProps(
|
||||
return {
|
||||
redirect: {
|
||||
permanent: false,
|
||||
destination: "/dashboard/home",
|
||||
destination: "/dashboard/projects",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -12,7 +12,6 @@ import {
|
||||
Loader2,
|
||||
Play,
|
||||
PlusIcon,
|
||||
RefreshCw,
|
||||
Search,
|
||||
ServerIcon,
|
||||
SquareTerminal,
|
||||
@@ -69,14 +68,6 @@ import {
|
||||
CommandInput,
|
||||
CommandItem,
|
||||
} from "@/components/ui/command";
|
||||
import {
|
||||
ContextMenu,
|
||||
ContextMenuContent,
|
||||
ContextMenuItem,
|
||||
ContextMenuLabel,
|
||||
ContextMenuSeparator,
|
||||
ContextMenuTrigger,
|
||||
} from "@/components/ui/context-menu";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
@@ -433,7 +424,6 @@ 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) {
|
||||
@@ -509,14 +499,6 @@ 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);
|
||||
@@ -549,9 +531,6 @@ const EnvironmentPage = (
|
||||
case "mongo":
|
||||
await mongoActions.start.mutateAsync({ mongoId: serviceId });
|
||||
break;
|
||||
case "libsql":
|
||||
await libsqlActions.start.mutateAsync({ libsqlId: serviceId });
|
||||
break;
|
||||
}
|
||||
success++;
|
||||
} catch {
|
||||
@@ -599,9 +578,6 @@ const EnvironmentPage = (
|
||||
case "mongo":
|
||||
await mongoActions.stop.mutateAsync({ mongoId: serviceId });
|
||||
break;
|
||||
case "libsql":
|
||||
await libsqlActions.stop.mutateAsync({ libsqlId: serviceId });
|
||||
break;
|
||||
}
|
||||
success++;
|
||||
} catch {
|
||||
@@ -678,12 +654,6 @@ const EnvironmentPage = (
|
||||
targetEnvironmentId: selectedTargetEnvironment,
|
||||
});
|
||||
break;
|
||||
case "libsql":
|
||||
await libsqlActions.move.mutateAsync({
|
||||
libsqlId: serviceId,
|
||||
targetEnvironmentId: selectedTargetEnvironment,
|
||||
});
|
||||
break;
|
||||
}
|
||||
await utils.environment.one.invalidate({
|
||||
environmentId,
|
||||
@@ -753,11 +723,6 @@ const EnvironmentPage = (
|
||||
mongoId: serviceId,
|
||||
});
|
||||
break;
|
||||
case "libsql":
|
||||
await libsqlActions.delete.mutateAsync({
|
||||
libsqlId: serviceId,
|
||||
});
|
||||
break;
|
||||
}
|
||||
await utils.environment.one.invalidate({
|
||||
environmentId,
|
||||
@@ -824,11 +789,6 @@ const EnvironmentPage = (
|
||||
mongoId: serviceId,
|
||||
});
|
||||
break;
|
||||
case "libsql":
|
||||
await libsqlActions.deploy.mutateAsync({
|
||||
libsqlId: serviceId,
|
||||
});
|
||||
break;
|
||||
}
|
||||
success++;
|
||||
} catch (error) {
|
||||
@@ -854,110 +814,6 @@ 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 [];
|
||||
@@ -1616,156 +1472,110 @@ 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) => (
|
||||
<ContextMenu key={service.id}>
|
||||
<ContextMenuTrigger asChild>
|
||||
<Link
|
||||
href={`/dashboard/project/${projectId}/environment/${environmentId}/services/${service.type}/${service.id}`}
|
||||
className="block"
|
||||
<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)
|
||||
}
|
||||
>
|
||||
<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 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>
|
||||
)}
|
||||
<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>
|
||||
<DateTooltip date={service.createdAt}>
|
||||
Created
|
||||
</DateTooltip>
|
||||
</div>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
@@ -1776,38 +1586,6 @@ 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>
|
||||
);
|
||||
};
|
||||
@@ -1886,7 +1664,7 @@ export async function getServerSideProps(
|
||||
return {
|
||||
redirect: {
|
||||
permanent: false,
|
||||
destination: "/dashboard/home",
|
||||
destination: "/dashboard/projects",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -490,7 +490,7 @@ export async function getServerSideProps(
|
||||
return {
|
||||
redirect: {
|
||||
permanent: false,
|
||||
destination: "/dashboard/home",
|
||||
destination: "/dashboard/projects",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -22,7 +22,6 @@ 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";
|
||||
@@ -61,7 +60,6 @@ type TabState =
|
||||
| "advanced"
|
||||
| "deployments"
|
||||
| "domains"
|
||||
| "containers"
|
||||
| "monitoring"
|
||||
| "volumeBackups";
|
||||
|
||||
@@ -233,9 +231,6 @@ const Service = (
|
||||
Deployments
|
||||
</TabsTrigger>
|
||||
)}
|
||||
{permissions?.service.read && (
|
||||
<TabsTrigger value="containers">Containers</TabsTrigger>
|
||||
)}
|
||||
{permissions?.service.create && (
|
||||
<TabsTrigger value="backups">Backups</TabsTrigger>
|
||||
)}
|
||||
@@ -303,18 +298,6 @@ 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">
|
||||
@@ -492,7 +475,7 @@ export async function getServerSideProps(
|
||||
return {
|
||||
redirect: {
|
||||
permanent: false,
|
||||
destination: "/dashboard/home",
|
||||
destination: "/dashboard/projects",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -343,7 +343,7 @@ export async function getServerSideProps(
|
||||
return {
|
||||
redirect: {
|
||||
permanent: false,
|
||||
destination: "/dashboard/home",
|
||||
destination: "/dashboard/projects",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -372,7 +372,7 @@ export async function getServerSideProps(
|
||||
return {
|
||||
redirect: {
|
||||
permanent: false,
|
||||
destination: "/dashboard/home",
|
||||
destination: "/dashboard/projects",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -376,7 +376,7 @@ export async function getServerSideProps(
|
||||
return {
|
||||
redirect: {
|
||||
permanent: false,
|
||||
destination: "/dashboard/home",
|
||||
destination: "/dashboard/projects",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -353,7 +353,7 @@ export async function getServerSideProps(
|
||||
return {
|
||||
redirect: {
|
||||
permanent: false,
|
||||
destination: "/dashboard/home",
|
||||
destination: "/dashboard/projects",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -360,7 +360,7 @@ export async function getServerSideProps(
|
||||
return {
|
||||
redirect: {
|
||||
permanent: false,
|
||||
destination: "/dashboard/home",
|
||||
destination: "/dashboard/projects",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -363,7 +363,7 @@ export async function getServerSideProps(
|
||||
return {
|
||||
redirect: {
|
||||
permanent: false,
|
||||
destination: "/dashboard/home",
|
||||
destination: "/dashboard/projects",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ export async function getServerSideProps(
|
||||
return {
|
||||
redirect: {
|
||||
permanent: true,
|
||||
destination: "/dashboard/home",
|
||||
destination: "/dashboard/projects",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ export async function getServerSideProps(
|
||||
return {
|
||||
redirect: {
|
||||
permanent: true,
|
||||
destination: "/dashboard/home",
|
||||
destination: "/dashboard/projects",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ export async function getServerSideProps(
|
||||
return {
|
||||
redirect: {
|
||||
permanent: true,
|
||||
destination: "/dashboard/home",
|
||||
destination: "/dashboard/projects",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ export async function getServerSideProps(
|
||||
return {
|
||||
redirect: {
|
||||
permanent: true,
|
||||
destination: "/dashboard/home",
|
||||
destination: "/dashboard/projects",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ export async function getServerSideProps(
|
||||
return {
|
||||
redirect: {
|
||||
permanent: true,
|
||||
destination: "/dashboard/home",
|
||||
destination: "/dashboard/projects",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -45,7 +45,7 @@ export async function getServerSideProps(
|
||||
return {
|
||||
redirect: {
|
||||
permanent: true,
|
||||
destination: "/dashboard/home",
|
||||
destination: "/dashboard/projects",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -46,7 +46,7 @@ export async function getServerSideProps(
|
||||
return {
|
||||
redirect: {
|
||||
permanent: true,
|
||||
destination: "/dashboard/home",
|
||||
destination: "/dashboard/projects",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ export async function getServerSideProps(
|
||||
return {
|
||||
redirect: {
|
||||
permanent: true,
|
||||
destination: "/dashboard/home",
|
||||
destination: "/dashboard/projects",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -82,16 +82,6 @@ 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;
|
||||
@@ -106,7 +96,7 @@ export default function Home({ IS_CLOUD }: Props) {
|
||||
}
|
||||
|
||||
toast.success("Logged in successfully");
|
||||
router.push("/dashboard/home");
|
||||
router.push("/dashboard/projects");
|
||||
} catch {
|
||||
toast.error("An error occurred while logging in");
|
||||
} finally {
|
||||
@@ -133,7 +123,7 @@ export default function Home({ IS_CLOUD }: Props) {
|
||||
}
|
||||
|
||||
toast.success("Logged in successfully");
|
||||
router.push("/dashboard/home");
|
||||
router.push("/dashboard/projects");
|
||||
} catch {
|
||||
toast.error("An error occurred while verifying 2FA code");
|
||||
} finally {
|
||||
@@ -163,7 +153,7 @@ export default function Home({ IS_CLOUD }: Props) {
|
||||
}
|
||||
|
||||
toast.success("Logged in successfully");
|
||||
router.push("/dashboard/home");
|
||||
router.push("/dashboard/projects");
|
||||
} catch {
|
||||
toast.error("An error occurred while verifying backup code");
|
||||
} finally {
|
||||
@@ -408,7 +398,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
||||
return {
|
||||
redirect: {
|
||||
permanent: true,
|
||||
destination: "/dashboard/home",
|
||||
destination: "/dashboard/projects",
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -437,7 +427,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
||||
return {
|
||||
redirect: {
|
||||
permanent: true,
|
||||
destination: "/dashboard/home",
|
||||
destination: "/dashboard/projects",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -139,7 +139,7 @@ const Invitation = ({
|
||||
});
|
||||
|
||||
toast.success("Account created successfully");
|
||||
router.push("/dashboard/home");
|
||||
router.push("/dashboard/projects");
|
||||
} 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/home",
|
||||
destination: "/dashboard/projects",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -10,6 +10,12 @@ import { adminProcedure, createTRPCRouter } from "../trpc";
|
||||
|
||||
export const adminRouter = createTRPCRouter({
|
||||
setupMonitoring: adminProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Update web server monitoring settings",
|
||||
description: "Update the monitoring configuration for the web server including refresh rates, thresholds, and container services. Restarts the monitoring system and returns the updated settings. Disabled on cloud.",
|
||||
},
|
||||
})
|
||||
.input(apiUpdateWebServerMonitoring)
|
||||
.mutation(async ({ input }) => {
|
||||
try {
|
||||
|
||||
@@ -41,12 +41,24 @@ import { generatePassword } from "@/templates/utils";
|
||||
|
||||
export const aiRouter = createTRPCRouter({
|
||||
one: adminProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Get AI settings by ID",
|
||||
description: "Returns a single AI provider configuration by its ID.",
|
||||
},
|
||||
})
|
||||
.input(z.object({ aiId: z.string() }))
|
||||
.query(async ({ input }) => {
|
||||
return await getAiSettingById(input.aiId);
|
||||
}),
|
||||
|
||||
getModels: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "List available AI models",
|
||||
description: "Fetches the list of models from the given AI provider URL. Supports OpenAI-compatible, Ollama, Gemini, Perplexity, ZAI, and MiniMax providers.",
|
||||
},
|
||||
})
|
||||
.input(z.object({ apiUrl: z.string().min(1), apiKey: z.string() }))
|
||||
.query(async ({ input }) => {
|
||||
try {
|
||||
@@ -174,33 +186,75 @@ export const aiRouter = createTRPCRouter({
|
||||
});
|
||||
}
|
||||
}),
|
||||
create: adminProcedure.input(apiCreateAi).mutation(async ({ ctx, input }) => {
|
||||
create: adminProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Create AI provider",
|
||||
description: "Saves a new AI provider configuration (API URL, key, model) for the current organization.",
|
||||
},
|
||||
})
|
||||
.input(apiCreateAi)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
return await saveAiSettings(ctx.session.activeOrganizationId, input);
|
||||
}),
|
||||
|
||||
update: adminProcedure.input(apiUpdateAi).mutation(async ({ ctx, input }) => {
|
||||
update: adminProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Update AI provider",
|
||||
description: "Updates an existing AI provider configuration for the current organization.",
|
||||
},
|
||||
})
|
||||
.input(apiUpdateAi)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
return await saveAiSettings(ctx.session.activeOrganizationId, input);
|
||||
}),
|
||||
|
||||
getAll: adminProcedure.query(async ({ ctx }) => {
|
||||
getAll: adminProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "List all AI providers",
|
||||
description: "Returns all AI provider configurations for the current organization.",
|
||||
},
|
||||
})
|
||||
.query(async ({ ctx }) => {
|
||||
return await getAiSettingsByOrganizationId(
|
||||
ctx.session.activeOrganizationId,
|
||||
);
|
||||
}),
|
||||
|
||||
get: adminProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Get AI provider",
|
||||
description: "Returns a single AI provider configuration by its ID.",
|
||||
},
|
||||
})
|
||||
.input(z.object({ aiId: z.string() }))
|
||||
.query(async ({ input }) => {
|
||||
return await getAiSettingById(input.aiId);
|
||||
}),
|
||||
|
||||
delete: adminProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Delete AI provider",
|
||||
description: "Removes an AI provider configuration by its ID.",
|
||||
},
|
||||
})
|
||||
.input(z.object({ aiId: z.string() }))
|
||||
.mutation(async ({ input }) => {
|
||||
return await deleteAiSettings(input.aiId);
|
||||
}),
|
||||
|
||||
getEnabledProviders: protectedProcedure.query(async ({ ctx }) => {
|
||||
getEnabledProviders: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "List enabled AI providers",
|
||||
description: "Returns a lightweight list of enabled AI providers (ID, name, model) for the current organization, suitable for dropdown selectors.",
|
||||
},
|
||||
})
|
||||
.query(async ({ ctx }) => {
|
||||
const settings = await getAiSettingsByOrganizationId(
|
||||
ctx.session.activeOrganizationId,
|
||||
);
|
||||
@@ -210,6 +264,12 @@ export const aiRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
analyzeLogs: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Analyze logs with AI",
|
||||
description: "Sends build or runtime logs to the specified AI provider for analysis. Returns a summary of issues found, root causes, and suggested fixes.",
|
||||
},
|
||||
})
|
||||
.input(
|
||||
z.object({
|
||||
aiId: z.string().min(1),
|
||||
@@ -268,6 +328,12 @@ ${input.logs}`,
|
||||
}),
|
||||
|
||||
testConnection: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Test AI provider connection",
|
||||
description: "Sends a minimal prompt to the specified AI provider and model to verify the API URL, key, and model are valid and reachable.",
|
||||
},
|
||||
})
|
||||
.input(
|
||||
z.object({
|
||||
apiUrl: z.string().min(1),
|
||||
@@ -302,6 +368,12 @@ ${input.logs}`,
|
||||
}),
|
||||
|
||||
suggest: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Suggest deployment variants",
|
||||
description: "Uses AI to generate deployment configuration suggestions (docker-compose variants) based on the user's input prompt.",
|
||||
},
|
||||
})
|
||||
.input(
|
||||
z.object({
|
||||
aiId: z.string(),
|
||||
@@ -323,6 +395,12 @@ ${input.logs}`,
|
||||
}
|
||||
}),
|
||||
deploy: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Deploy AI suggestion",
|
||||
description: "Deploys an AI-generated suggestion by creating a compose service with its docker-compose file, environment variables, domains, and config file mounts.",
|
||||
},
|
||||
})
|
||||
.input(deploySuggestionSchema)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const environment = await findEnvironmentById(input.environmentId);
|
||||
|
||||
@@ -79,6 +79,12 @@ import { cancelDeployment, deploy } from "@/server/utils/deploy";
|
||||
|
||||
export const applicationRouter = createTRPCRouter({
|
||||
create: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Create an application",
|
||||
description: "Creates a new application in the specified project environment. Supports GitHub, GitLab, Bitbucket, Git, Docker image, and drop sources.",
|
||||
},
|
||||
})
|
||||
.input(apiCreateApplication)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
@@ -134,6 +140,12 @@ export const applicationRouter = createTRPCRouter({
|
||||
}
|
||||
}),
|
||||
one: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Get an application",
|
||||
description: "Retrieves detailed information about an application by its ID, including git provider access status and deployment configuration.",
|
||||
},
|
||||
})
|
||||
.input(apiFindOneApplication)
|
||||
.query(async ({ input, ctx }) => {
|
||||
await checkServiceAccess(ctx, input.applicationId, "read");
|
||||
@@ -189,6 +201,12 @@ export const applicationRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
reload: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Reload an application",
|
||||
description: "Restarts the Docker container for the application by mechanizing it. Resets the application status to idle, then to done on success or error on failure.",
|
||||
},
|
||||
})
|
||||
.input(apiReloadApplication)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.applicationId, {
|
||||
@@ -218,6 +236,12 @@ export const applicationRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
delete: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Delete an application",
|
||||
description: "Permanently deletes an application and cleans up all associated resources including Docker services, Traefik configuration, deployments, middlewares, and source code.",
|
||||
},
|
||||
})
|
||||
.input(apiFindOneApplication)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServiceAccess(ctx, input.applicationId, "delete");
|
||||
@@ -279,6 +303,12 @@ export const applicationRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
stop: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Stop an application",
|
||||
description: "Stops the running Docker service for the application and sets its status to idle.",
|
||||
},
|
||||
})
|
||||
.input(apiFindOneApplication)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.applicationId, {
|
||||
@@ -301,6 +331,12 @@ export const applicationRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
start: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Start an application",
|
||||
description: "Starts the Docker service for the application and sets its status to done.",
|
||||
},
|
||||
})
|
||||
.input(apiFindOneApplication)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.applicationId, {
|
||||
@@ -323,6 +359,12 @@ export const applicationRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
redeploy: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Redeploy an application",
|
||||
description: "Triggers a rebuild and redeployment of the application. Queues a deployment job or executes it directly for cloud servers.",
|
||||
},
|
||||
})
|
||||
.input(apiRedeployApplication)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.applicationId, {
|
||||
@@ -367,6 +409,12 @@ export const applicationRouter = createTRPCRouter({
|
||||
});
|
||||
}),
|
||||
saveEnvironment: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Save environment variables",
|
||||
description: "Updates the environment variables, build arguments, and build secrets for an application.",
|
||||
},
|
||||
})
|
||||
.input(apiSaveEnvironmentVariables)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.applicationId, {
|
||||
@@ -388,6 +436,12 @@ export const applicationRouter = createTRPCRouter({
|
||||
return true;
|
||||
}),
|
||||
saveBuildType: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Save build type configuration",
|
||||
description: "Updates the build type and related settings for an application, including Dockerfile path, build context, publish directory, and build stage.",
|
||||
},
|
||||
})
|
||||
.input(apiSaveBuildType)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.applicationId, {
|
||||
@@ -413,6 +467,12 @@ export const applicationRouter = createTRPCRouter({
|
||||
return true;
|
||||
}),
|
||||
saveGithubProvider: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Save GitHub provider",
|
||||
description: "Configures the application to use a GitHub repository as its source, setting the repository, branch, owner, and build path.",
|
||||
},
|
||||
})
|
||||
.input(apiSaveGithubProvider)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.applicationId, {
|
||||
@@ -440,6 +500,12 @@ export const applicationRouter = createTRPCRouter({
|
||||
return true;
|
||||
}),
|
||||
saveGitlabProvider: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Save GitLab provider",
|
||||
description: "Configures the application to use a GitLab repository as its source, setting the repository, branch, owner, build path, and project ID.",
|
||||
},
|
||||
})
|
||||
.input(apiSaveGitlabProvider)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.applicationId, {
|
||||
@@ -468,6 +534,12 @@ export const applicationRouter = createTRPCRouter({
|
||||
return true;
|
||||
}),
|
||||
saveBitbucketProvider: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Save Bitbucket provider",
|
||||
description: "Configures the application to use a Bitbucket repository as its source, setting the repository, branch, owner, and build path.",
|
||||
},
|
||||
})
|
||||
.input(apiSaveBitbucketProvider)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.applicationId, {
|
||||
@@ -495,6 +567,12 @@ export const applicationRouter = createTRPCRouter({
|
||||
return true;
|
||||
}),
|
||||
saveGiteaProvider: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Save Gitea provider",
|
||||
description: "Configures the application to use a Gitea repository as its source, setting the repository, branch, owner, and build path.",
|
||||
},
|
||||
})
|
||||
.input(apiSaveGiteaProvider)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.applicationId, {
|
||||
@@ -521,6 +599,12 @@ export const applicationRouter = createTRPCRouter({
|
||||
return true;
|
||||
}),
|
||||
saveDockerProvider: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Save Docker provider",
|
||||
description: "Configures the application to use a Docker image as its source, setting the image name, registry URL, and optional credentials.",
|
||||
},
|
||||
})
|
||||
.input(apiSaveDockerProvider)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.applicationId, {
|
||||
@@ -544,6 +628,12 @@ export const applicationRouter = createTRPCRouter({
|
||||
return true;
|
||||
}),
|
||||
saveGitProvider: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Save Git provider",
|
||||
description: "Configures the application to use a custom Git repository URL as its source, with optional SSH key authentication.",
|
||||
},
|
||||
})
|
||||
.input(apiSaveGitProvider)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.applicationId, {
|
||||
@@ -569,6 +659,12 @@ export const applicationRouter = createTRPCRouter({
|
||||
return true;
|
||||
}),
|
||||
disconnectGitProvider: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Disconnect git provider",
|
||||
description: "Removes all git provider configuration from the application, resetting source type to default and clearing repository, branch, and owner fields for all providers.",
|
||||
},
|
||||
})
|
||||
.input(apiFindOneApplication)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.applicationId, {
|
||||
@@ -622,6 +718,12 @@ export const applicationRouter = createTRPCRouter({
|
||||
return true;
|
||||
}),
|
||||
markRunning: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Mark application as running",
|
||||
description: "Sets the application status to running. Used to indicate that a deployment is in progress.",
|
||||
},
|
||||
})
|
||||
.input(apiFindOneApplication)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.applicationId, {
|
||||
@@ -637,6 +739,12 @@ export const applicationRouter = createTRPCRouter({
|
||||
});
|
||||
}),
|
||||
update: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Update an application",
|
||||
description: "Updates the general configuration of an application such as name, description, memory limits, CPU limits, and other settings.",
|
||||
},
|
||||
})
|
||||
.input(apiUpdateApplication)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.applicationId, {
|
||||
@@ -673,6 +781,12 @@ export const applicationRouter = createTRPCRouter({
|
||||
return true;
|
||||
}),
|
||||
refreshToken: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Refresh deploy token",
|
||||
description: "Regenerates the webhook refresh token for the application, invalidating the previous token used for triggering deployments.",
|
||||
},
|
||||
})
|
||||
.input(apiFindOneApplication)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.applicationId, {
|
||||
@@ -691,6 +805,12 @@ export const applicationRouter = createTRPCRouter({
|
||||
return true;
|
||||
}),
|
||||
deploy: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Deploy an application",
|
||||
description: "Triggers a new deployment for the application. Queues a deployment job or executes it directly for cloud servers.",
|
||||
},
|
||||
})
|
||||
.input(apiDeployApplication)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.applicationId, {
|
||||
@@ -735,6 +855,12 @@ export const applicationRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
cleanQueues: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Clean deployment queues",
|
||||
description: "Removes all pending deployment jobs from the queue for the specified application.",
|
||||
},
|
||||
})
|
||||
.input(apiFindOneApplication)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.applicationId, {
|
||||
@@ -743,6 +869,12 @@ export const applicationRouter = createTRPCRouter({
|
||||
await cleanQueuesByApplication(input.applicationId);
|
||||
}),
|
||||
clearDeployments: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Clear old deployments",
|
||||
description: "Removes old deployment logs and artifacts for the application to free up disk space.",
|
||||
},
|
||||
})
|
||||
.input(apiFindOneApplication)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.applicationId, {
|
||||
@@ -759,6 +891,12 @@ export const applicationRouter = createTRPCRouter({
|
||||
return true;
|
||||
}),
|
||||
killBuild: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Kill active build",
|
||||
description: "Forcefully terminates the currently running Docker build process for the application.",
|
||||
},
|
||||
})
|
||||
.input(apiFindOneApplication)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.applicationId, {
|
||||
@@ -774,6 +912,12 @@ export const applicationRouter = createTRPCRouter({
|
||||
});
|
||||
}),
|
||||
readTraefikConfig: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Read Traefik configuration",
|
||||
description: "Reads the current Traefik reverse proxy configuration file for the application. Supports both local and remote server configurations.",
|
||||
},
|
||||
})
|
||||
.input(apiFindOneApplication)
|
||||
.query(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.applicationId, {
|
||||
@@ -793,6 +937,12 @@ export const applicationRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
dropDeployment: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Deploy from zip upload",
|
||||
description: "Deploys an application from an uploaded zip file. Unzips the file into the application directory and triggers a deployment.",
|
||||
},
|
||||
})
|
||||
.input(
|
||||
zfd.formData({
|
||||
applicationId: z.string(),
|
||||
@@ -849,6 +999,12 @@ export const applicationRouter = createTRPCRouter({
|
||||
return true;
|
||||
}),
|
||||
updateTraefikConfig: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Update Traefik configuration",
|
||||
description: "Writes a new Traefik reverse proxy configuration for the application. Supports both local and remote server configurations.",
|
||||
},
|
||||
})
|
||||
.input(z.object({ applicationId: z.string(), traefikConfig: z.string() }))
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.applicationId, {
|
||||
@@ -873,6 +1029,12 @@ export const applicationRouter = createTRPCRouter({
|
||||
return true;
|
||||
}),
|
||||
readAppMonitoring: withPermission("monitoring", "read")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Read application monitoring stats",
|
||||
description: "Retrieves CPU and memory monitoring statistics for the application. Only available in self-hosted mode.",
|
||||
},
|
||||
})
|
||||
.input(apiFindMonitoringStats)
|
||||
.query(async ({ input }) => {
|
||||
if (IS_CLOUD) {
|
||||
@@ -886,6 +1048,12 @@ export const applicationRouter = createTRPCRouter({
|
||||
return stats;
|
||||
}),
|
||||
move: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Move application to another environment",
|
||||
description: "Moves an application to a different environment within the same project or to another project's environment.",
|
||||
},
|
||||
})
|
||||
.input(
|
||||
z.object({
|
||||
applicationId: z.string(),
|
||||
@@ -922,6 +1090,12 @@ export const applicationRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
cancelDeployment: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Cancel a deployment",
|
||||
description: "Cancels an in-progress deployment for the application and resets its status to idle. Only available in cloud version.",
|
||||
},
|
||||
})
|
||||
.input(apiFindOneApplication)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.applicationId, {
|
||||
@@ -972,6 +1146,12 @@ export const applicationRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
search: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Search applications",
|
||||
description: "Searches applications by name, appName, description, repository, owner, or Docker image with pagination. Respects service-level access control.",
|
||||
},
|
||||
})
|
||||
.input(
|
||||
z.object({
|
||||
q: z.string().optional(),
|
||||
@@ -1104,6 +1284,12 @@ export const applicationRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
readLogs: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Read application logs",
|
||||
description: "Retrieves Docker container logs for the application with configurable tail length, time range, and optional text search filtering.",
|
||||
},
|
||||
})
|
||||
.input(
|
||||
apiFindOneApplication.extend({
|
||||
tail: z.number().int().min(1).max(10000).default(100),
|
||||
|
||||
@@ -78,6 +78,12 @@ interface RcloneFile {
|
||||
|
||||
export const backupRouter = createTRPCRouter({
|
||||
create: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Create a backup",
|
||||
description: "Creates a new backup configuration for a database or compose service. If enabled, automatically schedules the backup according to the provided cron expression.",
|
||||
},
|
||||
})
|
||||
.input(apiCreateBackup)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
@@ -152,6 +158,12 @@ export const backupRouter = createTRPCRouter({
|
||||
}
|
||||
}),
|
||||
one: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Get a backup",
|
||||
description: "Returns the details of a specific backup configuration by its ID.",
|
||||
},
|
||||
})
|
||||
.input(apiFindOneBackup)
|
||||
.query(async ({ input, ctx }) => {
|
||||
const backup = await findBackupById(input.backupId);
|
||||
@@ -172,6 +184,12 @@ export const backupRouter = createTRPCRouter({
|
||||
return backup;
|
||||
}),
|
||||
update: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Update a backup",
|
||||
description: "Updates an existing backup configuration. Reschedules or removes the backup job depending on the enabled state.",
|
||||
},
|
||||
})
|
||||
.input(apiUpdateBackup)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
@@ -229,6 +247,12 @@ export const backupRouter = createTRPCRouter({
|
||||
}
|
||||
}),
|
||||
remove: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Delete a backup",
|
||||
description: "Permanently removes a backup configuration and unschedules any associated backup job.",
|
||||
},
|
||||
})
|
||||
.input(apiRemoveBackup)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
@@ -272,6 +296,12 @@ export const backupRouter = createTRPCRouter({
|
||||
}
|
||||
}),
|
||||
manualBackupPostgres: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Run a PostgreSQL backup manually",
|
||||
description: "Immediately executes a PostgreSQL backup using the specified backup configuration. Cleans up old backups according to retention settings.",
|
||||
},
|
||||
})
|
||||
.input(apiFindOneBackup)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
@@ -303,6 +333,12 @@ export const backupRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
manualBackupMySql: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Run a MySQL backup manually",
|
||||
description: "Immediately executes a MySQL backup using the specified backup configuration. Cleans up old backups according to retention settings.",
|
||||
},
|
||||
})
|
||||
.input(apiFindOneBackup)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
@@ -330,6 +366,12 @@ export const backupRouter = createTRPCRouter({
|
||||
}
|
||||
}),
|
||||
manualBackupMariadb: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Run a MariaDB backup manually",
|
||||
description: "Immediately executes a MariaDB backup using the specified backup configuration. Cleans up old backups according to retention settings.",
|
||||
},
|
||||
})
|
||||
.input(apiFindOneBackup)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
@@ -357,6 +399,12 @@ export const backupRouter = createTRPCRouter({
|
||||
}
|
||||
}),
|
||||
manualBackupCompose: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Run a Compose backup manually",
|
||||
description: "Immediately executes a Compose service backup using the specified backup configuration. Cleans up old backups according to retention settings.",
|
||||
},
|
||||
})
|
||||
.input(apiFindOneBackup)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
@@ -384,6 +432,12 @@ export const backupRouter = createTRPCRouter({
|
||||
}
|
||||
}),
|
||||
manualBackupMongo: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Run a MongoDB backup manually",
|
||||
description: "Immediately executes a MongoDB backup using the specified backup configuration. Cleans up old backups according to retention settings.",
|
||||
},
|
||||
})
|
||||
.input(apiFindOneBackup)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
@@ -411,6 +465,12 @@ export const backupRouter = createTRPCRouter({
|
||||
}
|
||||
}),
|
||||
manualBackupLibsql: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Run a LibSQL backup manually",
|
||||
description: "Immediately executes a LibSQL backup using the specified backup configuration. Cleans up old backups according to retention settings.",
|
||||
},
|
||||
})
|
||||
.input(apiFindOneBackup)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
@@ -438,6 +498,12 @@ export const backupRouter = createTRPCRouter({
|
||||
}
|
||||
}),
|
||||
manualBackupWebServer: withPermission("backup", "create")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Run a web server backup manually",
|
||||
description: "Immediately executes a web server backup using the specified backup configuration. Cleans up old backups according to retention settings.",
|
||||
},
|
||||
})
|
||||
.input(apiFindOneBackup)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const backup = await findBackupById(input.backupId);
|
||||
@@ -451,6 +517,12 @@ export const backupRouter = createTRPCRouter({
|
||||
return true;
|
||||
}),
|
||||
listBackupFiles: withPermission("backup", "read")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "List backup files in S3",
|
||||
description: "Lists backup files stored in the S3 destination bucket. Supports searching by path prefix and returns up to 100 results.",
|
||||
},
|
||||
})
|
||||
.input(
|
||||
z.object({
|
||||
destinationId: z.string(),
|
||||
@@ -458,26 +530,9 @@ export const backupRouter = createTRPCRouter({
|
||||
serverId: z.string().optional(),
|
||||
}),
|
||||
)
|
||||
.query(async ({ input, ctx }) => {
|
||||
.query(async ({ input }) => {
|
||||
try {
|
||||
const destination = await findDestinationById(input.destinationId);
|
||||
if (destination.organizationId !== ctx.session.activeOrganizationId) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You don't have access to this destination.",
|
||||
});
|
||||
}
|
||||
if (input.serverId) {
|
||||
const targetServer = await findServerById(input.serverId);
|
||||
if (
|
||||
targetServer.organizationId !== ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You don't have access to this server.",
|
||||
});
|
||||
}
|
||||
}
|
||||
const rcloneFlags = getS3Credentials(destination);
|
||||
const bucketPath = `:s3:${destination.bucket}`;
|
||||
|
||||
|
||||
@@ -25,6 +25,12 @@ import {
|
||||
|
||||
export const bitbucketRouter = createTRPCRouter({
|
||||
create: withPermission("gitProviders", "create")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Create Bitbucket provider",
|
||||
description: "Creates a new Bitbucket provider configuration linked to the active organization. Requires gitProviders create permission.",
|
||||
},
|
||||
})
|
||||
.input(apiCreateBitbucket)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
@@ -50,11 +56,24 @@ export const bitbucketRouter = createTRPCRouter({
|
||||
}
|
||||
}),
|
||||
one: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Get Bitbucket provider",
|
||||
description: "Returns a single Bitbucket provider configuration by its ID.",
|
||||
},
|
||||
})
|
||||
.input(apiFindOneBitbucket)
|
||||
.query(async ({ input }) => {
|
||||
return await findBitbucketById(input.bitbucketId);
|
||||
}),
|
||||
bitbucketProviders: protectedProcedure.query(async ({ ctx }) => {
|
||||
bitbucketProviders: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "List Bitbucket providers",
|
||||
description: "Returns all Bitbucket providers accessible to the current user within the active organization.",
|
||||
},
|
||||
})
|
||||
.query(async ({ ctx }) => {
|
||||
const accessibleIds = await getAccessibleGitProviderIds(ctx.session);
|
||||
|
||||
let result = await db.query.bitbucket.findMany({
|
||||
@@ -77,16 +96,34 @@ export const bitbucketRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
getBitbucketRepositories: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "List Bitbucket repositories",
|
||||
description: "Fetches the list of repositories accessible by the Bitbucket provider. Calls the Bitbucket API using the provider's credentials.",
|
||||
},
|
||||
})
|
||||
.input(apiFindOneBitbucket)
|
||||
.query(async ({ input }) => {
|
||||
return await getBitbucketRepositories(input.bitbucketId);
|
||||
}),
|
||||
getBitbucketBranches: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "List Bitbucket branches",
|
||||
description: "Fetches the list of branches for a specific Bitbucket repository. Calls the Bitbucket API using the provider's credentials.",
|
||||
},
|
||||
})
|
||||
.input(apiFindBitbucketBranches)
|
||||
.query(async ({ input }) => {
|
||||
return await getBitbucketBranches(input);
|
||||
}),
|
||||
testConnection: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Test Bitbucket connection",
|
||||
description: "Tests the connection to a Bitbucket provider by attempting to fetch its repositories. Returns the number of repositories found or throws an error on failure.",
|
||||
},
|
||||
})
|
||||
.input(apiBitbucketTestConnection)
|
||||
.mutation(async ({ input }) => {
|
||||
try {
|
||||
@@ -101,6 +138,12 @@ export const bitbucketRouter = createTRPCRouter({
|
||||
}
|
||||
}),
|
||||
update: withPermission("gitProviders", "create")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Update Bitbucket provider",
|
||||
description: "Updates a Bitbucket provider configuration. Requires gitProviders create permission.",
|
||||
},
|
||||
})
|
||||
.input(apiUpdateBitbucket)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const result = await updateBitbucket(input.bitbucketId, {
|
||||
|
||||
@@ -19,6 +19,12 @@ import {
|
||||
|
||||
export const certificateRouter = createTRPCRouter({
|
||||
create: withPermission("certificate", "create")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Create a certificate",
|
||||
description: "Creates a new SSL/TLS certificate. In cloud mode, a server must be specified. Logs an audit entry upon creation.",
|
||||
},
|
||||
})
|
||||
.input(apiCreateCertificate)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
if (IS_CLOUD && !input.serverId) {
|
||||
@@ -41,6 +47,12 @@ export const certificateRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
one: withPermission("certificate", "read")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Get a certificate",
|
||||
description: "Returns a single certificate by its ID. Verifies that the certificate belongs to the current organization.",
|
||||
},
|
||||
})
|
||||
.input(apiFindCertificate)
|
||||
.query(async ({ input, ctx }) => {
|
||||
const certificates = await findCertificateById(input.certificateId);
|
||||
@@ -53,6 +65,12 @@ export const certificateRouter = createTRPCRouter({
|
||||
return certificates;
|
||||
}),
|
||||
remove: withPermission("certificate", "delete")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Delete a certificate",
|
||||
description: "Deletes a certificate by its ID after verifying organization ownership. Logs an audit entry before removal.",
|
||||
},
|
||||
})
|
||||
.input(apiFindCertificate)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const certificates = await findCertificateById(input.certificateId);
|
||||
@@ -71,7 +89,14 @@ export const certificateRouter = createTRPCRouter({
|
||||
await removeCertificateById(input.certificateId);
|
||||
return true;
|
||||
}),
|
||||
all: withPermission("certificate", "read").query(async ({ ctx }) => {
|
||||
all: withPermission("certificate", "read")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "List all certificates",
|
||||
description: "Returns all certificates belonging to the current organization, including their associated server information.",
|
||||
},
|
||||
})
|
||||
.query(async ({ ctx }) => {
|
||||
return await db.query.certificates.findMany({
|
||||
where: eq(certificates.organizationId, ctx.session.activeOrganizationId),
|
||||
with: {
|
||||
@@ -80,6 +105,12 @@ export const certificateRouter = createTRPCRouter({
|
||||
});
|
||||
}),
|
||||
update: withPermission("certificate", "update")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Update a certificate",
|
||||
description: "Updates the name, certificate data, and private key of an existing certificate. Verifies organization ownership before applying changes.",
|
||||
},
|
||||
})
|
||||
.input(apiUpdateCertificate)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const certificate = await findCertificateById(input.certificateId);
|
||||
|
||||
@@ -13,27 +13,30 @@ 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(),
|
||||
}),
|
||||
)
|
||||
.query(async ({ input, ctx }) => {
|
||||
if (input.serverId) {
|
||||
const targetServer = await findServerById(input.serverId);
|
||||
if (targetServer.organizationId !== ctx.session.activeOrganizationId) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You don't have access to this server.",
|
||||
});
|
||||
}
|
||||
}
|
||||
.query(async ({ input }) => {
|
||||
const docker = await getRemoteDocker(input.serverId);
|
||||
const workers: DockerNode[] = await docker.listNodes();
|
||||
return workers;
|
||||
}),
|
||||
|
||||
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(),
|
||||
@@ -41,15 +44,6 @@ export const clusterRouter = createTRPCRouter({
|
||||
}),
|
||||
)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
if (input.serverId) {
|
||||
const targetServer = await findServerById(input.serverId);
|
||||
if (targetServer.organizationId !== ctx.session.activeOrganizationId) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You don't have access to this server.",
|
||||
});
|
||||
}
|
||||
}
|
||||
try {
|
||||
const drainCommand = `docker node update --availability drain ${input.nodeId}`;
|
||||
const removeCommand = `docker node rm ${input.nodeId} --force`;
|
||||
@@ -78,21 +72,18 @@ 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(),
|
||||
}),
|
||||
)
|
||||
.query(async ({ input, ctx }) => {
|
||||
if (input.serverId) {
|
||||
const targetServer = await findServerById(input.serverId);
|
||||
if (targetServer.organizationId !== ctx.session.activeOrganizationId) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You don't have access to this server.",
|
||||
});
|
||||
}
|
||||
}
|
||||
.query(async ({ input }) => {
|
||||
const docker = await getRemoteDocker(input.serverId);
|
||||
const result = await docker.swarmInspect();
|
||||
const docker_version = await docker.version();
|
||||
@@ -110,21 +101,18 @@ 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(),
|
||||
}),
|
||||
)
|
||||
.query(async ({ input, ctx }) => {
|
||||
if (input.serverId) {
|
||||
const targetServer = await findServerById(input.serverId);
|
||||
if (targetServer.organizationId !== ctx.session.activeOrganizationId) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You don't have access to this server.",
|
||||
});
|
||||
}
|
||||
}
|
||||
.query(async ({ input }) => {
|
||||
const docker = await getRemoteDocker(input.serverId);
|
||||
const result = await docker.swarmInspect();
|
||||
const docker_version = await docker.version();
|
||||
|
||||
@@ -83,6 +83,12 @@ import { audit } from "../utils/audit";
|
||||
|
||||
export const composeRouter = createTRPCRouter({
|
||||
create: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Create a compose service",
|
||||
description: "Creates a new Docker Compose service in the specified project environment with the given configuration.",
|
||||
},
|
||||
})
|
||||
.input(apiCreateCompose)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
try {
|
||||
@@ -133,6 +139,12 @@ export const composeRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
one: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Get a compose service",
|
||||
description: "Retrieves detailed information about a compose service by its ID, including git provider access status and deployment configuration.",
|
||||
},
|
||||
})
|
||||
.input(apiFindCompose)
|
||||
.query(async ({ input, ctx }) => {
|
||||
await checkServiceAccess(ctx, input.composeId, "read");
|
||||
@@ -189,6 +201,12 @@ export const composeRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
update: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Update a compose service",
|
||||
description: "Updates the configuration of a compose service such as name, description, compose file content, and other settings.",
|
||||
},
|
||||
})
|
||||
.input(apiUpdateCompose)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.composeId, {
|
||||
@@ -204,6 +222,12 @@ export const composeRouter = createTRPCRouter({
|
||||
return updated;
|
||||
}),
|
||||
saveEnvironment: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Save compose environment variables",
|
||||
description: "Updates the environment variables for a compose service.",
|
||||
},
|
||||
})
|
||||
.input(apiSaveEnvironmentVariablesCompose)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.composeId, {
|
||||
@@ -229,6 +253,12 @@ export const composeRouter = createTRPCRouter({
|
||||
return true;
|
||||
}),
|
||||
delete: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Delete a compose service",
|
||||
description: "Permanently deletes a compose service and cleans up associated Docker resources, deployments, and directories. Optionally deletes associated volumes.",
|
||||
},
|
||||
})
|
||||
.input(apiDeleteCompose)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServiceAccess(ctx, input.composeId, "delete");
|
||||
@@ -279,6 +309,12 @@ export const composeRouter = createTRPCRouter({
|
||||
return composeResult;
|
||||
}),
|
||||
cleanQueues: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Clean deployment queues",
|
||||
description: "Removes all pending deployment jobs from the queue for the specified compose service.",
|
||||
},
|
||||
})
|
||||
.input(apiFindCompose)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.composeId, {
|
||||
@@ -288,6 +324,12 @@ export const composeRouter = createTRPCRouter({
|
||||
return { success: true, message: "Queues cleaned successfully" };
|
||||
}),
|
||||
clearDeployments: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Clear old deployments",
|
||||
description: "Removes old deployment logs and artifacts for the compose service to free up disk space.",
|
||||
},
|
||||
})
|
||||
.input(apiFindCompose)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.composeId, {
|
||||
@@ -304,6 +346,12 @@ export const composeRouter = createTRPCRouter({
|
||||
return true;
|
||||
}),
|
||||
killBuild: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Kill active build",
|
||||
description: "Forcefully terminates the currently running Docker build process for the compose service.",
|
||||
},
|
||||
})
|
||||
.input(apiFindCompose)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.composeId, {
|
||||
@@ -314,6 +362,12 @@ export const composeRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
loadServices: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Load compose services",
|
||||
description: "Parses the compose file and returns the list of services defined in it, with their current container status.",
|
||||
},
|
||||
})
|
||||
.input(apiFetchServices)
|
||||
.query(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.composeId, {
|
||||
@@ -322,6 +376,12 @@ export const composeRouter = createTRPCRouter({
|
||||
return await loadServices(input.composeId, input.type);
|
||||
}),
|
||||
loadMountsByService: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Load mounts by service",
|
||||
description: "Retrieves the Docker volume mounts for a specific service within a compose stack by inspecting the running container.",
|
||||
},
|
||||
})
|
||||
.input(
|
||||
z.object({
|
||||
composeId: z.string().min(1),
|
||||
@@ -340,6 +400,12 @@ export const composeRouter = createTRPCRouter({
|
||||
return mounts;
|
||||
}),
|
||||
fetchSourceType: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Fetch and clone source",
|
||||
description: "Clones the compose repository from the configured git provider and returns the source type. Executes the clone command locally or on a remote server.",
|
||||
},
|
||||
})
|
||||
.input(apiFindCompose)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
@@ -365,6 +431,12 @@ export const composeRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
randomizeCompose: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Randomize compose file",
|
||||
description: "Adds a random suffix to service names and volumes in the compose file to avoid naming conflicts between deployments.",
|
||||
},
|
||||
})
|
||||
.input(apiRandomizeCompose)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.composeId, {
|
||||
@@ -381,6 +453,12 @@ export const composeRouter = createTRPCRouter({
|
||||
return result;
|
||||
}),
|
||||
isolatedDeployment: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Randomize for isolated deployment",
|
||||
description: "Randomizes the compose file for isolated deployment mode, ensuring unique service and volume names to support parallel deployments.",
|
||||
},
|
||||
})
|
||||
.input(apiRandomizeCompose)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.composeId, {
|
||||
@@ -400,6 +478,12 @@ export const composeRouter = createTRPCRouter({
|
||||
return result;
|
||||
}),
|
||||
getConvertedCompose: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Get converted compose file",
|
||||
description: "Returns the compose file with domains injected as Traefik labels, converted to YAML format ready for deployment.",
|
||||
},
|
||||
})
|
||||
.input(apiFindCompose)
|
||||
.query(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.composeId, {
|
||||
@@ -414,6 +498,12 @@ export const composeRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
deploy: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Deploy a compose service",
|
||||
description: "Triggers a new deployment for the compose service. Queues a deployment job or executes it directly for cloud servers.",
|
||||
},
|
||||
})
|
||||
.input(apiDeployCompose)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.composeId, {
|
||||
@@ -464,6 +554,12 @@ export const composeRouter = createTRPCRouter({
|
||||
};
|
||||
}),
|
||||
redeploy: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Redeploy a compose service",
|
||||
description: "Triggers a rebuild and redeployment of the compose service. Queues a deployment job or executes it directly for cloud servers.",
|
||||
},
|
||||
})
|
||||
.input(apiRedeployCompose)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.composeId, {
|
||||
@@ -512,6 +608,12 @@ export const composeRouter = createTRPCRouter({
|
||||
};
|
||||
}),
|
||||
stop: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Stop a compose service",
|
||||
description: "Stops all running containers for the compose service using docker compose stop.",
|
||||
},
|
||||
})
|
||||
.input(apiFindCompose)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.composeId, {
|
||||
@@ -528,6 +630,12 @@ export const composeRouter = createTRPCRouter({
|
||||
return true;
|
||||
}),
|
||||
start: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Start a compose service",
|
||||
description: "Starts all containers for the compose service using docker compose start.",
|
||||
},
|
||||
})
|
||||
.input(apiFindCompose)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.composeId, {
|
||||
@@ -544,6 +652,12 @@ export const composeRouter = createTRPCRouter({
|
||||
return true;
|
||||
}),
|
||||
getDefaultCommand: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Get default compose command",
|
||||
description: "Generates and returns the default docker compose command that would be used to deploy the service.",
|
||||
},
|
||||
})
|
||||
.input(apiFindCompose)
|
||||
.query(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.composeId, {
|
||||
@@ -554,6 +668,12 @@ export const composeRouter = createTRPCRouter({
|
||||
return `docker ${command}`;
|
||||
}),
|
||||
refreshToken: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Refresh deploy token",
|
||||
description: "Regenerates the webhook refresh token for the compose service, invalidating the previous token used for triggering deployments.",
|
||||
},
|
||||
})
|
||||
.input(apiFindCompose)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.composeId, {
|
||||
@@ -572,6 +692,12 @@ export const composeRouter = createTRPCRouter({
|
||||
return true;
|
||||
}),
|
||||
deployTemplate: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Deploy a template",
|
||||
description: "Creates a new compose service from a template by fetching its files, processing variables, creating mounts and domains, and setting up the compose configuration.",
|
||||
},
|
||||
})
|
||||
.input(
|
||||
z.object({
|
||||
environmentId: z.string(),
|
||||
@@ -680,6 +806,12 @@ export const composeRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
templates: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "List available templates",
|
||||
description: "Fetches the list of available compose templates from the GitHub templates repository.",
|
||||
},
|
||||
})
|
||||
.input(z.object({ baseUrl: z.string().optional() }))
|
||||
.query(async ({ input }) => {
|
||||
try {
|
||||
@@ -698,6 +830,12 @@ export const composeRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
getTags: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Get template tags",
|
||||
description: "Fetches all unique tags from the available compose templates for filtering purposes.",
|
||||
},
|
||||
})
|
||||
.input(z.object({ baseUrl: z.string().optional() }))
|
||||
.query(async ({ input }) => {
|
||||
const githubTemplates = await fetchTemplatesList(input.baseUrl);
|
||||
@@ -707,6 +845,12 @@ export const composeRouter = createTRPCRouter({
|
||||
return uniqueTags;
|
||||
}),
|
||||
disconnectGitProvider: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Disconnect git provider",
|
||||
description: "Removes all git provider configuration from the compose service, resetting source type to default and clearing repository, branch, and owner fields for all providers.",
|
||||
},
|
||||
})
|
||||
.input(apiFindCompose)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.composeId, {
|
||||
@@ -759,6 +903,12 @@ export const composeRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
move: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Move compose to another environment",
|
||||
description: "Moves a compose service to a different environment within the same project or to another project's environment.",
|
||||
},
|
||||
})
|
||||
.input(
|
||||
z.object({
|
||||
composeId: z.string(),
|
||||
@@ -796,6 +946,12 @@ export const composeRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
processTemplate: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Process a template",
|
||||
description: "Processes a base64-encoded template configuration, resolving variables and generating the compose file and environment settings without applying them.",
|
||||
},
|
||||
})
|
||||
.input(
|
||||
z.object({
|
||||
base64: z.string(),
|
||||
@@ -860,6 +1016,12 @@ export const composeRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
import: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Import a template",
|
||||
description: "Imports a base64-encoded template into an existing compose service, replacing its compose file, environment variables, mounts, and domains with the template's configuration.",
|
||||
},
|
||||
})
|
||||
.input(
|
||||
z.object({
|
||||
base64: z.string(),
|
||||
@@ -972,6 +1134,12 @@ export const composeRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
cancelDeployment: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Cancel a deployment",
|
||||
description: "Cancels an in-progress deployment for the compose service and resets its status to idle. Only available in cloud version.",
|
||||
},
|
||||
})
|
||||
.input(apiFindCompose)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.composeId, {
|
||||
@@ -1025,6 +1193,12 @@ export const composeRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
search: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Search compose services",
|
||||
description: "Searches compose services by name, appName, or description with pagination. Respects service-level access control.",
|
||||
},
|
||||
})
|
||||
.input(
|
||||
z.object({
|
||||
q: z.string().optional(),
|
||||
@@ -1133,6 +1307,12 @@ export const composeRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
readLogs: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Read compose container logs",
|
||||
description: "Retrieves Docker container logs for a specific container within the compose service with configurable tail length, time range, and optional text search filtering.",
|
||||
},
|
||||
})
|
||||
.input(
|
||||
apiFindCompose.extend({
|
||||
containerId: z
|
||||
|
||||
@@ -16,7 +16,6 @@ import {
|
||||
checkServicePermissionAndAccess,
|
||||
findMemberByUserId,
|
||||
} from "@dokploy/server/services/permission";
|
||||
import { findServerById } from "@dokploy/server/services/server";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { desc, eq } from "drizzle-orm";
|
||||
import { z } from "zod";
|
||||
@@ -35,6 +34,12 @@ import { createTRPCRouter, protectedProcedure, withPermission } from "../trpc";
|
||||
|
||||
export const deploymentRouter = createTRPCRouter({
|
||||
all: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "List deployments by application",
|
||||
description: "Returns all deployments associated with the given application, ordered by creation date.",
|
||||
},
|
||||
})
|
||||
.input(apiFindAllByApplication)
|
||||
.query(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.applicationId, {
|
||||
@@ -44,6 +49,12 @@ export const deploymentRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
allByCompose: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "List deployments by compose",
|
||||
description: "Returns all deployments associated with the given compose service.",
|
||||
},
|
||||
})
|
||||
.input(apiFindAllByCompose)
|
||||
.query(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.composeId, {
|
||||
@@ -52,18 +63,24 @@ export const deploymentRouter = createTRPCRouter({
|
||||
return await findAllDeploymentsByComposeId(input.composeId);
|
||||
}),
|
||||
allByServer: withPermission("deployment", "read")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "List deployments by server",
|
||||
description: "Returns all deployments associated with the given server.",
|
||||
},
|
||||
})
|
||||
.input(apiFindAllByServer)
|
||||
.query(async ({ input, ctx }) => {
|
||||
const targetServer = await findServerById(input.serverId);
|
||||
if (targetServer.organizationId !== ctx.session.activeOrganizationId) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You don't have access to this server.",
|
||||
});
|
||||
}
|
||||
.query(async ({ input }) => {
|
||||
return await findAllDeploymentsByServerId(input.serverId);
|
||||
}),
|
||||
allCentralized: withPermission("deployment", "read").query(
|
||||
allCentralized: withPermission("deployment", "read")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "List all deployments centralized",
|
||||
description: "Returns all deployments across all services in the organization. Non-admin users only see deployments for their accessible services.",
|
||||
},
|
||||
})
|
||||
.query(
|
||||
async ({ ctx }) => {
|
||||
const orgId = ctx.session.activeOrganizationId;
|
||||
const accessedServices =
|
||||
@@ -77,7 +94,14 @@ export const deploymentRouter = createTRPCRouter({
|
||||
},
|
||||
),
|
||||
|
||||
queueList: withPermission("deployment", "read").query(async ({ ctx }) => {
|
||||
queueList: withPermission("deployment", "read")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "List deployment queue jobs",
|
||||
description: "Returns all jobs in the deployment queue with their current state, timestamps, and resolved service paths.",
|
||||
},
|
||||
})
|
||||
.query(async ({ ctx }) => {
|
||||
const orgId = ctx.session.activeOrganizationId;
|
||||
let rows: QueueJobRow[];
|
||||
|
||||
@@ -124,6 +148,12 @@ export const deploymentRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
allByType: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "List deployments by service type",
|
||||
description: "Returns all deployments for a given service ID and type (application, compose, etc.), including associated rollback information.",
|
||||
},
|
||||
})
|
||||
.input(apiFindAllByType)
|
||||
.query(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.id, {
|
||||
@@ -139,6 +169,12 @@ export const deploymentRouter = createTRPCRouter({
|
||||
return deploymentsList;
|
||||
}),
|
||||
killProcess: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Cancel a running deployment",
|
||||
description: "Kills the running process of a deployment by sending SIGKILL to its PID. Updates the deployment status to error.",
|
||||
},
|
||||
})
|
||||
.input(
|
||||
z.object({
|
||||
deploymentId: z.string().min(1),
|
||||
@@ -176,6 +212,12 @@ export const deploymentRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
removeDeployment: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Delete a deployment",
|
||||
description: "Permanently removes a deployment record and its associated data.",
|
||||
},
|
||||
})
|
||||
.input(
|
||||
z.object({
|
||||
deploymentId: z.string().min(1),
|
||||
@@ -197,4 +239,43 @@ export const deploymentRouter = createTRPCRouter({
|
||||
});
|
||||
return result;
|
||||
}),
|
||||
|
||||
readBuildLogs: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Read deployment build logs",
|
||||
description:
|
||||
"Reads the build/deployment log file for a specific deployment. Returns the last N lines (default 200). Works for both local and remote server deployments.",
|
||||
},
|
||||
})
|
||||
.input(
|
||||
z.object({
|
||||
deploymentId: z.string().min(1),
|
||||
tail: z.number().int().min(1).max(10000).default(200),
|
||||
}),
|
||||
)
|
||||
.query(async ({ input, ctx }) => {
|
||||
const deployment = await findDeploymentById(input.deploymentId);
|
||||
|
||||
const serviceId = deployment.applicationId || deployment.composeId;
|
||||
if (serviceId) {
|
||||
await checkServicePermissionAndAccess(ctx, serviceId, {
|
||||
deployment: ["read"],
|
||||
});
|
||||
}
|
||||
|
||||
const command = `tail -n ${input.tail} ${deployment.logPath} 2>/dev/null || echo "Log file not found"`;
|
||||
const { stdout } = deployment.serverId
|
||||
? await execAsyncRemote(deployment.serverId, command)
|
||||
: await execAsync(command);
|
||||
|
||||
return {
|
||||
deploymentId: deployment.deploymentId,
|
||||
status: deployment.status,
|
||||
errorMessage: deployment.errorMessage || null,
|
||||
title: deployment.title,
|
||||
createdAt: deployment.createdAt,
|
||||
logs: stdout,
|
||||
};
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -22,6 +22,12 @@ import {
|
||||
|
||||
export const destinationRouter = createTRPCRouter({
|
||||
create: withPermission("destination", "create")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Create backup destination",
|
||||
description: "Creates a new S3-compatible backup destination for the current organization and logs an audit event.",
|
||||
},
|
||||
})
|
||||
.input(apiCreateDestination)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
@@ -45,6 +51,12 @@ export const destinationRouter = createTRPCRouter({
|
||||
}
|
||||
}),
|
||||
testConnection: withPermission("destination", "create")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Test backup destination connection",
|
||||
description: "Tests connectivity to an S3-compatible bucket using rclone. Runs locally or on a remote server depending on configuration.",
|
||||
},
|
||||
})
|
||||
.input(apiCreateDestination)
|
||||
.mutation(async ({ input }) => {
|
||||
const {
|
||||
@@ -102,6 +114,12 @@ export const destinationRouter = createTRPCRouter({
|
||||
}
|
||||
}),
|
||||
one: withPermission("destination", "read")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Get backup destination",
|
||||
description: "Returns a single backup destination by ID. Verifies the caller belongs to the same organization.",
|
||||
},
|
||||
})
|
||||
.input(apiFindOneDestination)
|
||||
.query(async ({ input, ctx }) => {
|
||||
const destination = await findDestinationById(input.destinationId);
|
||||
@@ -113,13 +131,26 @@ export const destinationRouter = createTRPCRouter({
|
||||
}
|
||||
return destination;
|
||||
}),
|
||||
all: withPermission("destination", "read").query(async ({ ctx }) => {
|
||||
all: withPermission("destination", "read")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "List all backup destinations",
|
||||
description: "Returns all S3-compatible backup destinations for the current organization, ordered by creation date descending.",
|
||||
},
|
||||
})
|
||||
.query(async ({ ctx }) => {
|
||||
return await db.query.destinations.findMany({
|
||||
where: eq(destinations.organizationId, ctx.session.activeOrganizationId),
|
||||
orderBy: [desc(destinations.createdAt)],
|
||||
});
|
||||
}),
|
||||
remove: withPermission("destination", "delete")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Delete backup destination",
|
||||
description: "Removes a backup destination by ID. Verifies organization ownership and logs an audit event before deletion.",
|
||||
},
|
||||
})
|
||||
.input(apiRemoveDestination)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
@@ -147,6 +178,12 @@ export const destinationRouter = createTRPCRouter({
|
||||
}
|
||||
}),
|
||||
update: withPermission("destination", "create")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Update backup destination",
|
||||
description: "Updates an existing backup destination. Verifies organization ownership before applying changes and logs an audit event.",
|
||||
},
|
||||
})
|
||||
.input(apiUpdateDestination)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
import {
|
||||
containerKill,
|
||||
containerRemove,
|
||||
containerRestart,
|
||||
containerStart,
|
||||
containerStop,
|
||||
findServerById,
|
||||
getConfig,
|
||||
getContainers,
|
||||
@@ -23,6 +20,12 @@ export const containerIdRegex = /^[a-zA-Z0-9.\-_]+$/;
|
||||
|
||||
export const dockerRouter = createTRPCRouter({
|
||||
getContainers: withPermission("docker", "read")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Get Docker containers",
|
||||
description: "Retrieves a list of all Docker containers. Optionally targets a specific remote server by ID.",
|
||||
},
|
||||
})
|
||||
.input(
|
||||
z.object({
|
||||
serverId: z.string().optional(),
|
||||
@@ -38,111 +41,39 @@ export const dockerRouter = createTRPCRouter({
|
||||
return await getContainers(input.serverId);
|
||||
}),
|
||||
|
||||
restartContainer: withPermission("service", "read")
|
||||
restartContainer: withPermission("docker", "read")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Restart a Docker container",
|
||||
description: "Restarts a Docker container by its ID. An audit log entry is created for the action.",
|
||||
},
|
||||
})
|
||||
.input(
|
||||
z.object({
|
||||
containerId: z
|
||||
.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 containerRestart(input.containerId, input.serverId);
|
||||
const result = await containerRestart(input.containerId);
|
||||
await audit(ctx, {
|
||||
action: "start",
|
||||
resourceType: "docker",
|
||||
resourceId: input.containerId,
|
||||
resourceName: input.containerId,
|
||||
});
|
||||
}),
|
||||
|
||||
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,
|
||||
});
|
||||
return result;
|
||||
}),
|
||||
|
||||
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
|
||||
@@ -169,6 +100,12 @@ export const dockerRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
getConfig: withPermission("docker", "read")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Get Docker container configuration",
|
||||
description: "Retrieves the configuration (inspect data) for a specific Docker container. Optionally targets a remote server.",
|
||||
},
|
||||
})
|
||||
.input(
|
||||
z.object({
|
||||
containerId: z
|
||||
@@ -189,6 +126,12 @@ export const dockerRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
getContainersByAppNameMatch: withPermission("service", "read")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Get containers by app name match",
|
||||
description: "Retrieves containers whose names match the given application name. Supports filtering by app type (stack or docker-compose) and optionally targets a remote server.",
|
||||
},
|
||||
})
|
||||
.input(
|
||||
z.object({
|
||||
appType: z.enum(["stack", "docker-compose"]).optional(),
|
||||
@@ -211,6 +154,12 @@ export const dockerRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
getContainersByAppLabel: withPermission("docker", "read")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Get containers by app label",
|
||||
description: "Retrieves containers filtered by application label. Supports standalone and swarm deployment types, and optionally targets a remote server.",
|
||||
},
|
||||
})
|
||||
.input(
|
||||
z.object({
|
||||
appName: z.string().min(1).regex(containerIdRegex, "Invalid app name."),
|
||||
@@ -233,6 +182,12 @@ export const dockerRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
getStackContainersByAppName: withPermission("docker", "read")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Get stack containers by app name",
|
||||
description: "Retrieves all containers belonging to a Docker stack by application name. Optionally targets a remote server.",
|
||||
},
|
||||
})
|
||||
.input(
|
||||
z.object({
|
||||
appName: z.string().min(1).regex(containerIdRegex, "Invalid app name."),
|
||||
@@ -250,6 +205,12 @@ export const dockerRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
getServiceContainersByAppName: withPermission("docker", "read")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Get service containers by app name",
|
||||
description: "Retrieves all containers belonging to a Docker Swarm service by application name. Optionally targets a remote server.",
|
||||
},
|
||||
})
|
||||
.input(
|
||||
z.object({
|
||||
appName: z.string().min(1).regex(containerIdRegex, "Invalid app name."),
|
||||
@@ -267,6 +228,12 @@ export const dockerRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
uploadFileToContainer: withPermission("docker", "read")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Upload a file to a Docker container",
|
||||
description: "Uploads a file to a specified path inside a Docker container. The file is converted to a buffer and transferred to the container's filesystem.",
|
||||
},
|
||||
})
|
||||
.input(uploadFileToContainerSchema)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
if (input.serverId) {
|
||||
|
||||
@@ -33,6 +33,12 @@ import {
|
||||
|
||||
export const domainRouter = createTRPCRouter({
|
||||
create: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Create a domain",
|
||||
description: "Creates a new domain for an application or compose service. Validates permissions and logs an audit entry.",
|
||||
},
|
||||
})
|
||||
.input(apiCreateDomain)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
@@ -65,6 +71,12 @@ export const domainRouter = createTRPCRouter({
|
||||
}
|
||||
}),
|
||||
byApplicationId: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "List domains by application",
|
||||
description: "Returns all domains associated with a given application ID.",
|
||||
},
|
||||
})
|
||||
.input(apiFindOneApplication)
|
||||
.query(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.applicationId, {
|
||||
@@ -73,6 +85,12 @@ export const domainRouter = createTRPCRouter({
|
||||
return await findDomainsByApplicationId(input.applicationId);
|
||||
}),
|
||||
byComposeId: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "List domains by compose service",
|
||||
description: "Returns all domains associated with a given compose service ID.",
|
||||
},
|
||||
})
|
||||
.input(apiFindCompose)
|
||||
.query(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.composeId, {
|
||||
@@ -81,6 +99,12 @@ export const domainRouter = createTRPCRouter({
|
||||
return await findDomainsByComposeId(input.composeId);
|
||||
}),
|
||||
generateDomain: withPermission("domain", "create")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Generate a traefik.me domain",
|
||||
description: "Generates a free traefik.me domain for an application, using the server IP to create a wildcard subdomain.",
|
||||
},
|
||||
})
|
||||
.input(z.object({ appName: z.string(), serverId: z.string().optional() }))
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
return generateTraefikMeDomain(
|
||||
@@ -90,6 +114,12 @@ export const domainRouter = createTRPCRouter({
|
||||
);
|
||||
}),
|
||||
canGenerateTraefikMeDomains: withPermission("domain", "read")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Check traefik.me domain availability",
|
||||
description: "Checks whether traefik.me domains can be generated by returning the server IP address. Returns the IP from the server record or web server settings.",
|
||||
},
|
||||
})
|
||||
.input(z.object({ serverId: z.string() }))
|
||||
.query(async ({ input }) => {
|
||||
if (input.serverId) {
|
||||
@@ -101,6 +131,12 @@ export const domainRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
update: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Update a domain",
|
||||
description: "Updates a domain's configuration and refreshes the Traefik routing rules for the associated application or preview deployment.",
|
||||
},
|
||||
})
|
||||
.input(apiUpdateDomain)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const currentDomain = await findDomainById(input.domainId);
|
||||
@@ -141,7 +177,15 @@ export const domainRouter = createTRPCRouter({
|
||||
}
|
||||
return result;
|
||||
}),
|
||||
one: protectedProcedure.input(apiFindDomain).query(async ({ input, ctx }) => {
|
||||
one: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Get a domain",
|
||||
description: "Returns a single domain by its ID. Validates read permissions against the associated service or preview deployment.",
|
||||
},
|
||||
})
|
||||
.input(apiFindDomain)
|
||||
.query(async ({ input, ctx }) => {
|
||||
const domain = await findDomainById(input.domainId);
|
||||
const serviceId = domain.applicationId || domain.composeId;
|
||||
if (serviceId) {
|
||||
@@ -159,6 +203,12 @@ export const domainRouter = createTRPCRouter({
|
||||
return domain;
|
||||
}),
|
||||
delete: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Delete a domain",
|
||||
description: "Deletes a domain by its ID and removes the associated Traefik routing configuration for the application.",
|
||||
},
|
||||
})
|
||||
.input(apiFindDomain)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const domain = await findDomainById(input.domainId);
|
||||
@@ -193,6 +243,12 @@ export const domainRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
validateDomain: withPermission("domain", "read")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Validate a domain",
|
||||
description: "Checks whether a domain's DNS records are correctly configured, optionally verifying against a specific server IP.",
|
||||
},
|
||||
})
|
||||
.input(
|
||||
z.object({
|
||||
domain: z.string(),
|
||||
|
||||
@@ -63,6 +63,12 @@ const filterEnvironmentServices = (
|
||||
|
||||
export const environmentRouter = createTRPCRouter({
|
||||
create: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Create environment",
|
||||
description: "Creates a new environment within a project. The name 'production' is reserved and cannot be used. Checks creation permissions and logs an audit event.",
|
||||
},
|
||||
})
|
||||
.input(apiCreateEnvironment)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
@@ -99,6 +105,12 @@ export const environmentRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
one: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Get environment",
|
||||
description: "Returns a single environment by ID with all its services. Non-admin users only see services they have been granted access to.",
|
||||
},
|
||||
})
|
||||
.input(apiFindOneEnvironment)
|
||||
.query(async ({ input, ctx }) => {
|
||||
const environment = await findEnvironmentById(input.environmentId);
|
||||
@@ -137,6 +149,12 @@ export const environmentRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
byProjectId: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "List environments by project",
|
||||
description: "Returns all environments for a given project. Non-admin users only see environments and services they have been granted access to.",
|
||||
},
|
||||
})
|
||||
.input(z.object({ projectId: z.string() }))
|
||||
.query(async ({ input, ctx }) => {
|
||||
try {
|
||||
@@ -183,6 +201,12 @@ export const environmentRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
remove: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Delete environment",
|
||||
description: "Deletes an environment by ID. The default environment cannot be deleted. Checks deletion permissions and environment access before removing.",
|
||||
},
|
||||
})
|
||||
.input(apiRemoveEnvironment)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
@@ -229,6 +253,12 @@ export const environmentRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
update: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Update environment",
|
||||
description: "Updates an environment's name, description, or env variables. The default environment cannot be renamed. Checks environment access and env-var write permissions.",
|
||||
},
|
||||
})
|
||||
.input(apiUpdateEnvironment)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
@@ -296,6 +326,12 @@ export const environmentRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
duplicate: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Duplicate environment",
|
||||
description: "Creates a copy of an existing environment including its services. Checks environment access and organization ownership before duplicating.",
|
||||
},
|
||||
})
|
||||
.input(apiDuplicateEnvironment)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
@@ -343,6 +379,12 @@ export const environmentRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
search: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Search environments",
|
||||
description: "Searches environments by name, description, or project with pagination. Non-admin users only see environments they have been granted access to.",
|
||||
},
|
||||
})
|
||||
.input(
|
||||
z.object({
|
||||
q: z.string().optional(),
|
||||
|
||||
@@ -21,7 +21,14 @@ import {
|
||||
} from "@/server/db/schema";
|
||||
|
||||
export const gitProviderRouter = createTRPCRouter({
|
||||
getAll: protectedProcedure.query(async ({ ctx }) => {
|
||||
getAll: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "List all git providers",
|
||||
description: "Returns all git providers (GitHub, GitLab, Bitbucket, Gitea) accessible to the current user within the active organization, ordered by creation date.",
|
||||
},
|
||||
})
|
||||
.query(async ({ ctx }) => {
|
||||
const accessibleIds = await getAccessibleGitProviderIds(ctx.session);
|
||||
|
||||
if (accessibleIds.size === 0) {
|
||||
@@ -46,6 +53,12 @@ export const gitProviderRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
toggleShare: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Toggle git provider sharing",
|
||||
description: "Toggles whether a git provider is shared with the entire organization. Only the owner of the provider can change this setting.",
|
||||
},
|
||||
})
|
||||
.input(apiToggleShareGitProvider)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const provider = await findGitProviderById(input.gitProviderId);
|
||||
@@ -73,6 +86,12 @@ export const gitProviderRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
allForPermissions: withPermission("member", "update")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "List git providers for permissions",
|
||||
description: "Returns a minimal list of all git providers in the organization for use in permission assignment UIs. Requires a valid enterprise license and member update permission.",
|
||||
},
|
||||
})
|
||||
.use(async ({ ctx, next }) => {
|
||||
const licensed = await hasValidLicense(ctx.session.activeOrganizationId);
|
||||
if (!licensed) {
|
||||
@@ -96,6 +115,12 @@ export const gitProviderRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
remove: withPermission("gitProviders", "delete")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Remove git provider",
|
||||
description: "Deletes a git provider from the organization. Requires gitProviders delete permission and the provider must belong to the active organization.",
|
||||
},
|
||||
})
|
||||
.input(apiRemoveGitProvider)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
|
||||
@@ -27,6 +27,12 @@ import {
|
||||
|
||||
export const giteaRouter = createTRPCRouter({
|
||||
create: withPermission("gitProviders", "create")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Create Gitea provider",
|
||||
description: "Creates a new Gitea provider configuration linked to the active organization. Requires gitProviders create permission.",
|
||||
},
|
||||
})
|
||||
.input(apiCreateGitea)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
@@ -53,11 +59,26 @@ export const giteaRouter = createTRPCRouter({
|
||||
}
|
||||
}),
|
||||
|
||||
one: protectedProcedure.input(apiFindOneGitea).query(async ({ input }) => {
|
||||
return await findGiteaById(input.giteaId);
|
||||
}),
|
||||
one: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Get Gitea provider",
|
||||
description: "Returns a single Gitea provider configuration by its ID.",
|
||||
},
|
||||
})
|
||||
.input(apiFindOneGitea)
|
||||
.query(async ({ input }) => {
|
||||
return await findGiteaById(input.giteaId);
|
||||
}),
|
||||
|
||||
giteaProviders: protectedProcedure.query(async ({ ctx }) => {
|
||||
giteaProviders: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "List Gitea providers",
|
||||
description: "Returns all Gitea providers accessible to the current user within the active organization, filtered to only those with valid credentials.",
|
||||
},
|
||||
})
|
||||
.query(async ({ ctx }) => {
|
||||
const accessibleIds = await getAccessibleGitProviderIds(ctx.session);
|
||||
|
||||
let result = await db.query.gitea.findMany({
|
||||
@@ -88,6 +109,12 @@ export const giteaRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
getGiteaRepositories: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "List Gitea repositories",
|
||||
description: "Fetches the list of repositories accessible by the Gitea provider. Calls the Gitea API using the provider's credentials.",
|
||||
},
|
||||
})
|
||||
.input(apiFindOneGitea)
|
||||
.query(async ({ input }) => {
|
||||
const { giteaId } = input;
|
||||
@@ -112,6 +139,12 @@ export const giteaRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
getGiteaBranches: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "List Gitea branches",
|
||||
description: "Fetches the list of branches for a specific Gitea repository. Calls the Gitea API using the provider's credentials.",
|
||||
},
|
||||
})
|
||||
.input(apiFindGiteaBranches)
|
||||
.query(async ({ input }) => {
|
||||
const { giteaId, owner, repositoryName } = input;
|
||||
@@ -140,6 +173,12 @@ export const giteaRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
testConnection: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Test Gitea connection",
|
||||
description: "Tests the connection to a Gitea provider by attempting to fetch its repositories. Returns the number of repositories found or throws an error on failure.",
|
||||
},
|
||||
})
|
||||
.input(apiGiteaTestConnection)
|
||||
.mutation(async ({ input }) => {
|
||||
const giteaId = input.giteaId ?? "";
|
||||
@@ -160,6 +199,12 @@ export const giteaRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
update: withPermission("gitProviders", "create")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Update Gitea provider",
|
||||
description: "Updates a Gitea provider configuration and its associated git provider record. Requires gitProviders create permission.",
|
||||
},
|
||||
})
|
||||
.input(apiUpdateGitea)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
if (input.name) {
|
||||
@@ -188,6 +233,12 @@ export const giteaRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
getGiteaUrl: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Get Gitea instance URL",
|
||||
description: "Returns the base URL of the Gitea instance associated with the given provider ID.",
|
||||
},
|
||||
})
|
||||
.input(apiFindOneGitea)
|
||||
.query(async ({ input }) => {
|
||||
const { giteaId } = input;
|
||||
|
||||
@@ -22,20 +22,47 @@ import {
|
||||
} from "@/server/db/schema";
|
||||
|
||||
export const githubRouter = createTRPCRouter({
|
||||
one: protectedProcedure.input(apiFindOneGithub).query(async ({ input }) => {
|
||||
return await findGithubById(input.githubId);
|
||||
}),
|
||||
one: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Get GitHub provider",
|
||||
description: "Returns a single GitHub provider configuration by its ID.",
|
||||
},
|
||||
})
|
||||
.input(apiFindOneGithub)
|
||||
.query(async ({ input }) => {
|
||||
return await findGithubById(input.githubId);
|
||||
}),
|
||||
getGithubRepositories: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "List GitHub repositories",
|
||||
description: "Fetches the list of repositories accessible by the GitHub provider. Calls the GitHub API using the provider's credentials.",
|
||||
},
|
||||
})
|
||||
.input(apiFindOneGithub)
|
||||
.query(async ({ input }) => {
|
||||
return await getGithubRepositories(input.githubId);
|
||||
}),
|
||||
getGithubBranches: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "List GitHub branches",
|
||||
description: "Fetches the list of branches for a specific GitHub repository. Calls the GitHub API using the provider's credentials.",
|
||||
},
|
||||
})
|
||||
.input(apiFindGithubBranches)
|
||||
.query(async ({ input }) => {
|
||||
return await getGithubBranches(input);
|
||||
}),
|
||||
githubProviders: protectedProcedure.query(async ({ ctx }) => {
|
||||
githubProviders: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "List GitHub providers",
|
||||
description: "Returns all GitHub providers accessible to the current user within the active organization, filtered to only those with valid credentials.",
|
||||
},
|
||||
})
|
||||
.query(async ({ ctx }) => {
|
||||
const accessibleIds = await getAccessibleGitProviderIds(ctx.session);
|
||||
|
||||
let result = await db.query.github.findMany({
|
||||
@@ -66,6 +93,12 @@ export const githubRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
testConnection: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Test GitHub connection",
|
||||
description: "Tests the connection to a GitHub provider by attempting to fetch its repositories. Returns the number of repositories found or throws an error on failure.",
|
||||
},
|
||||
})
|
||||
.input(apiFindOneGithub)
|
||||
.mutation(async ({ input }) => {
|
||||
try {
|
||||
@@ -79,6 +112,12 @@ export const githubRouter = createTRPCRouter({
|
||||
}
|
||||
}),
|
||||
update: withPermission("gitProviders", "create")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Update GitHub provider",
|
||||
description: "Updates a GitHub provider configuration and its associated git provider record. Requires gitProviders create permission.",
|
||||
},
|
||||
})
|
||||
.input(apiUpdateGithub)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await updateGitProvider(input.gitProviderId, {
|
||||
|
||||
@@ -27,6 +27,12 @@ import {
|
||||
|
||||
export const gitlabRouter = createTRPCRouter({
|
||||
create: withPermission("gitProviders", "create")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Create GitLab provider",
|
||||
description: "Creates a new GitLab provider configuration linked to the active organization. Requires gitProviders create permission.",
|
||||
},
|
||||
})
|
||||
.input(apiCreateGitlab)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
@@ -51,10 +57,25 @@ export const gitlabRouter = createTRPCRouter({
|
||||
});
|
||||
}
|
||||
}),
|
||||
one: protectedProcedure.input(apiFindOneGitlab).query(async ({ input }) => {
|
||||
return await findGitlabById(input.gitlabId);
|
||||
}),
|
||||
gitlabProviders: protectedProcedure.query(async ({ ctx }) => {
|
||||
one: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Get GitLab provider",
|
||||
description: "Returns a single GitLab provider configuration by its ID.",
|
||||
},
|
||||
})
|
||||
.input(apiFindOneGitlab)
|
||||
.query(async ({ input }) => {
|
||||
return await findGitlabById(input.gitlabId);
|
||||
}),
|
||||
gitlabProviders: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "List GitLab providers",
|
||||
description: "Returns all GitLab providers accessible to the current user within the active organization, filtered to only those with valid credentials.",
|
||||
},
|
||||
})
|
||||
.query(async ({ ctx }) => {
|
||||
const accessibleIds = await getAccessibleGitProviderIds(ctx.session);
|
||||
|
||||
let result = await db.query.gitlab.findMany({
|
||||
@@ -85,17 +106,35 @@ export const gitlabRouter = createTRPCRouter({
|
||||
return filtered;
|
||||
}),
|
||||
getGitlabRepositories: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "List GitLab repositories",
|
||||
description: "Fetches the list of repositories accessible by the GitLab provider. Calls the GitLab API using the provider's credentials.",
|
||||
},
|
||||
})
|
||||
.input(apiFindOneGitlab)
|
||||
.query(async ({ input }) => {
|
||||
return await getGitlabRepositories(input.gitlabId);
|
||||
}),
|
||||
|
||||
getGitlabBranches: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "List GitLab branches",
|
||||
description: "Fetches the list of branches for a specific GitLab repository. Calls the GitLab API using the provider's credentials.",
|
||||
},
|
||||
})
|
||||
.input(apiFindGitlabBranches)
|
||||
.query(async ({ input }) => {
|
||||
return await getGitlabBranches(input);
|
||||
}),
|
||||
testConnection: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Test GitLab connection",
|
||||
description: "Tests the connection to a GitLab provider by attempting to fetch its repositories. Returns the number of repositories found or throws an error on failure.",
|
||||
},
|
||||
})
|
||||
.input(apiGitlabTestConnection)
|
||||
.mutation(async ({ input }) => {
|
||||
try {
|
||||
@@ -110,6 +149,12 @@ export const gitlabRouter = createTRPCRouter({
|
||||
}
|
||||
}),
|
||||
update: withPermission("gitProviders", "create")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Update GitLab provider",
|
||||
description: "Updates a GitLab provider configuration and its associated git provider record. Requires gitProviders create permission.",
|
||||
},
|
||||
})
|
||||
.input(apiUpdateGitlab)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
if (input.name) {
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
findEnvironmentById,
|
||||
findLibsqlById,
|
||||
findProjectById,
|
||||
getAccessibleServerIds,
|
||||
getContainerLogs,
|
||||
IS_CLOUD,
|
||||
rebuildDatabase,
|
||||
@@ -16,7 +17,6 @@ import {
|
||||
stopService,
|
||||
stopServiceRemote,
|
||||
updateLibsqlById,
|
||||
getAccessibleServerIds,
|
||||
} from "@dokploy/server";
|
||||
import {
|
||||
addNewService,
|
||||
@@ -43,6 +43,12 @@ import {
|
||||
} from "@/server/db/schema";
|
||||
export const libsqlRouter = createTRPCRouter({
|
||||
create: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Create a LibSQL database",
|
||||
description: "Creates a new LibSQL database service with the specified configuration, sets up a persistent data volume, and registers it in the project.",
|
||||
},
|
||||
})
|
||||
.input(apiCreateLibsql)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
@@ -100,6 +106,12 @@ export const libsqlRouter = createTRPCRouter({
|
||||
}
|
||||
}),
|
||||
one: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Get a LibSQL database by ID",
|
||||
description: "Returns the full details of a LibSQL database service, including its environment and project configuration.",
|
||||
},
|
||||
})
|
||||
.input(apiFindOneLibsql)
|
||||
.query(async ({ input, ctx }) => {
|
||||
await checkServiceAccess(ctx, input.libsqlId, "read");
|
||||
@@ -118,6 +130,12 @@ export const libsqlRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
start: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Start a LibSQL database",
|
||||
description: "Starts the Docker container for the specified LibSQL database and sets its status to done.",
|
||||
},
|
||||
})
|
||||
.input(apiFindOneLibsql)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.libsqlId, {
|
||||
@@ -143,6 +161,12 @@ export const libsqlRouter = createTRPCRouter({
|
||||
return libsql;
|
||||
}),
|
||||
stop: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Stop a LibSQL database",
|
||||
description: "Stops the Docker container for the specified LibSQL database and sets its status to idle.",
|
||||
},
|
||||
})
|
||||
.input(apiFindOneLibsql)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.libsqlId, {
|
||||
@@ -168,6 +192,12 @@ export const libsqlRouter = createTRPCRouter({
|
||||
return libsql;
|
||||
}),
|
||||
saveExternalPorts: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Save the external ports for a LibSQL database",
|
||||
description: "Updates the external port mappings (HTTP, gRPC, admin) for the LibSQL database and triggers a redeployment. Validates that ports are not already in use.",
|
||||
},
|
||||
})
|
||||
.input(apiSaveExternalPortsLibsql)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.libsqlId, {
|
||||
@@ -230,6 +260,12 @@ export const libsqlRouter = createTRPCRouter({
|
||||
return libsql;
|
||||
}),
|
||||
deploy: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Deploy a LibSQL database",
|
||||
description: "Triggers a deployment for the specified LibSQL database, rebuilding and restarting its Docker container with the current configuration.",
|
||||
},
|
||||
})
|
||||
.input(apiDeployLibsql)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.libsqlId, {
|
||||
@@ -282,6 +318,12 @@ export const libsqlRouter = createTRPCRouter({
|
||||
}
|
||||
}),
|
||||
changeStatus: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Change LibSQL database status",
|
||||
description: "Updates the application status of a LibSQL database without starting or stopping the container.",
|
||||
},
|
||||
})
|
||||
.input(apiChangeLibsqlStatus)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.libsqlId, {
|
||||
@@ -300,6 +342,12 @@ export const libsqlRouter = createTRPCRouter({
|
||||
return libsql;
|
||||
}),
|
||||
remove: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Delete a LibSQL database",
|
||||
description: "Removes the LibSQL database service, its Docker container, and deletes the database record.",
|
||||
},
|
||||
})
|
||||
.input(apiFindOneLibsql)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServiceAccess(ctx, input.libsqlId, "delete");
|
||||
@@ -335,6 +383,12 @@ export const libsqlRouter = createTRPCRouter({
|
||||
return libsql;
|
||||
}),
|
||||
saveEnvironment: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Save environment variables for a LibSQL database",
|
||||
description: "Updates the environment variables for the specified LibSQL database service.",
|
||||
},
|
||||
})
|
||||
.input(apiSaveEnvironmentVariablesLibsql)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.libsqlId, {
|
||||
@@ -359,6 +413,12 @@ export const libsqlRouter = createTRPCRouter({
|
||||
return true;
|
||||
}),
|
||||
reload: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Reload a LibSQL database",
|
||||
description: "Restarts the LibSQL database by stopping and then starting its Docker container.",
|
||||
},
|
||||
})
|
||||
.input(apiResetLibsql)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.libsqlId, {
|
||||
@@ -391,6 +451,12 @@ export const libsqlRouter = createTRPCRouter({
|
||||
return true;
|
||||
}),
|
||||
update: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Update a LibSQL database",
|
||||
description: "Updates the configuration of an existing LibSQL database service.",
|
||||
},
|
||||
})
|
||||
.input(apiUpdateLibsql)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const { libsqlId, ...rest } = input;
|
||||
@@ -417,6 +483,12 @@ export const libsqlRouter = createTRPCRouter({
|
||||
return true;
|
||||
}),
|
||||
move: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Move a LibSQL database to another environment",
|
||||
description: "Moves the LibSQL database to a different environment within the same project.",
|
||||
},
|
||||
})
|
||||
.input(
|
||||
z.object({
|
||||
libsqlId: z.string(),
|
||||
@@ -453,6 +525,12 @@ export const libsqlRouter = createTRPCRouter({
|
||||
return updatedLibsql;
|
||||
}),
|
||||
rebuild: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Rebuild a LibSQL database",
|
||||
description: "Rebuilds the LibSQL database Docker container from scratch, pulling the latest image and recreating the service.",
|
||||
},
|
||||
})
|
||||
.input(apiRebuildLibsql)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.libsqlId, {
|
||||
@@ -469,6 +547,12 @@ export const libsqlRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
readLogs: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Read LibSQL container logs",
|
||||
description: "Retrieves the Docker container logs for the specified LibSQL database, with support for tail count, time-based filtering, and text search.",
|
||||
},
|
||||
})
|
||||
.input(
|
||||
apiFindOneLibsql.extend({
|
||||
tail: z.number().int().min(1).max(10000).default(100),
|
||||
|
||||
@@ -54,6 +54,12 @@ import {
|
||||
import { cancelJobs } from "@/server/utils/backup";
|
||||
export const mariadbRouter = createTRPCRouter({
|
||||
create: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Create a MariaDB database",
|
||||
description: "Creates a new MariaDB database service with the specified configuration, sets up a persistent data volume, and registers it in the project.",
|
||||
},
|
||||
})
|
||||
.input(apiCreateMariaDB)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
@@ -114,6 +120,12 @@ export const mariadbRouter = createTRPCRouter({
|
||||
}
|
||||
}),
|
||||
one: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Get a MariaDB database by ID",
|
||||
description: "Returns the full details of a MariaDB database service, including its environment and project configuration.",
|
||||
},
|
||||
})
|
||||
.input(apiFindOneMariaDB)
|
||||
.query(async ({ input, ctx }) => {
|
||||
await checkServiceAccess(ctx, input.mariadbId, "read");
|
||||
@@ -131,6 +143,12 @@ export const mariadbRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
start: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Start a MariaDB database",
|
||||
description: "Starts the Docker container for the specified MariaDB database and sets its status to done.",
|
||||
},
|
||||
})
|
||||
.input(apiFindOneMariaDB)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.mariadbId, {
|
||||
@@ -155,6 +173,12 @@ export const mariadbRouter = createTRPCRouter({
|
||||
return service;
|
||||
}),
|
||||
stop: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Stop a MariaDB database",
|
||||
description: "Stops the Docker container for the specified MariaDB database and sets its status to idle.",
|
||||
},
|
||||
})
|
||||
.input(apiFindOneMariaDB)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.mariadbId, {
|
||||
@@ -180,6 +204,12 @@ export const mariadbRouter = createTRPCRouter({
|
||||
return mariadb;
|
||||
}),
|
||||
saveExternalPort: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Save the external port for a MariaDB database",
|
||||
description: "Updates the external port mapping for the MariaDB database and triggers a redeployment. Validates that the port is not already in use.",
|
||||
},
|
||||
})
|
||||
.input(apiSaveExternalPortMariaDB)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.mariadbId, {
|
||||
@@ -213,6 +243,12 @@ export const mariadbRouter = createTRPCRouter({
|
||||
return mariadb;
|
||||
}),
|
||||
deploy: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Deploy a MariaDB database",
|
||||
description: "Triggers a deployment for the specified MariaDB database, rebuilding and restarting its Docker container with the current configuration.",
|
||||
},
|
||||
})
|
||||
.input(apiDeployMariaDB)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.mariadbId, {
|
||||
@@ -250,6 +286,12 @@ export const mariadbRouter = createTRPCRouter({
|
||||
});
|
||||
}),
|
||||
changeStatus: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Change MariaDB database status",
|
||||
description: "Updates the application status of a MariaDB database without starting or stopping the container.",
|
||||
},
|
||||
})
|
||||
.input(apiChangeMariaDBStatus)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.mariadbId, {
|
||||
@@ -268,6 +310,12 @@ export const mariadbRouter = createTRPCRouter({
|
||||
return mongo;
|
||||
}),
|
||||
remove: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Delete a MariaDB database",
|
||||
description: "Removes the MariaDB database service, its Docker container, cancels associated backup jobs, and deletes the database record.",
|
||||
},
|
||||
})
|
||||
.input(apiFindOneMariaDB)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServiceAccess(ctx, input.mariadbId, "delete");
|
||||
@@ -305,6 +353,12 @@ export const mariadbRouter = createTRPCRouter({
|
||||
return mongo;
|
||||
}),
|
||||
saveEnvironment: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Save environment variables for a MariaDB database",
|
||||
description: "Updates the environment variables for the specified MariaDB database service.",
|
||||
},
|
||||
})
|
||||
.input(apiSaveEnvironmentVariablesMariaDB)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.mariadbId, {
|
||||
@@ -329,6 +383,12 @@ export const mariadbRouter = createTRPCRouter({
|
||||
return true;
|
||||
}),
|
||||
reload: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Reload a MariaDB database",
|
||||
description: "Restarts the MariaDB database by stopping and then starting its Docker container.",
|
||||
},
|
||||
})
|
||||
.input(apiResetMariadb)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.mariadbId, {
|
||||
@@ -361,6 +421,12 @@ export const mariadbRouter = createTRPCRouter({
|
||||
return true;
|
||||
}),
|
||||
update: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Update a MariaDB database",
|
||||
description: "Updates the configuration of an existing MariaDB database service.",
|
||||
},
|
||||
})
|
||||
.input(apiUpdateMariaDB)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const { mariadbId, ...rest } = input;
|
||||
@@ -387,6 +453,12 @@ export const mariadbRouter = createTRPCRouter({
|
||||
return true;
|
||||
}),
|
||||
changePassword: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Change MariaDB database password",
|
||||
description: "Changes the password for a MariaDB user or root account by executing ALTER USER inside the running container and updating the stored password.",
|
||||
},
|
||||
})
|
||||
.input(
|
||||
z.object({
|
||||
mariadbId: z.string().min(1),
|
||||
@@ -444,6 +516,12 @@ export const mariadbRouter = createTRPCRouter({
|
||||
return true;
|
||||
}),
|
||||
move: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Move a MariaDB database to another environment",
|
||||
description: "Moves the MariaDB database to a different environment within the same project.",
|
||||
},
|
||||
})
|
||||
.input(
|
||||
z.object({
|
||||
mariadbId: z.string(),
|
||||
@@ -480,6 +558,12 @@ export const mariadbRouter = createTRPCRouter({
|
||||
return updatedMariadb;
|
||||
}),
|
||||
rebuild: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Rebuild a MariaDB database",
|
||||
description: "Rebuilds the MariaDB database Docker container from scratch, pulling the latest image and recreating the service.",
|
||||
},
|
||||
})
|
||||
.input(apiRebuildMariadb)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.mariadbId, {
|
||||
@@ -495,6 +579,12 @@ export const mariadbRouter = createTRPCRouter({
|
||||
return true;
|
||||
}),
|
||||
search: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Search MariaDB databases",
|
||||
description: "Returns a paginated list of MariaDB databases matching the given filters. Supports searching by name, appName, description, project, and environment.",
|
||||
},
|
||||
})
|
||||
.input(
|
||||
z.object({
|
||||
q: z.string().optional(),
|
||||
@@ -593,6 +683,12 @@ export const mariadbRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
readLogs: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Read MariaDB container logs",
|
||||
description: "Retrieves the Docker container logs for the specified MariaDB database, with support for tail count, time-based filtering, and text search.",
|
||||
},
|
||||
})
|
||||
.input(
|
||||
apiFindOneMariaDB.extend({
|
||||
tail: z.number().int().min(1).max(10000).default(100),
|
||||
|
||||
@@ -53,6 +53,12 @@ import {
|
||||
import { cancelJobs } from "@/server/utils/backup";
|
||||
export const mongoRouter = createTRPCRouter({
|
||||
create: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Create a MongoDB database",
|
||||
description: "Creates a new MongoDB database service with the specified configuration, sets up a persistent data volume, and registers it in the project.",
|
||||
},
|
||||
})
|
||||
.input(apiCreateMongo)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
@@ -117,6 +123,12 @@ export const mongoRouter = createTRPCRouter({
|
||||
}
|
||||
}),
|
||||
one: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Get a MongoDB database by ID",
|
||||
description: "Returns the full details of a MongoDB database service, including its environment and project configuration.",
|
||||
},
|
||||
})
|
||||
.input(apiFindOneMongo)
|
||||
.query(async ({ input, ctx }) => {
|
||||
await checkServiceAccess(ctx, input.mongoId, "read");
|
||||
@@ -135,6 +147,12 @@ export const mongoRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
start: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Start a MongoDB database",
|
||||
description: "Starts the Docker container for the specified MongoDB database and sets its status to done.",
|
||||
},
|
||||
})
|
||||
.input(apiFindOneMongo)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.mongoId, {
|
||||
@@ -160,6 +178,12 @@ export const mongoRouter = createTRPCRouter({
|
||||
return service;
|
||||
}),
|
||||
stop: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Stop a MongoDB database",
|
||||
description: "Stops the Docker container for the specified MongoDB database and sets its status to idle.",
|
||||
},
|
||||
})
|
||||
.input(apiFindOneMongo)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.mongoId, {
|
||||
@@ -185,6 +209,12 @@ export const mongoRouter = createTRPCRouter({
|
||||
return mongo;
|
||||
}),
|
||||
saveExternalPort: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Save the external port for a MongoDB database",
|
||||
description: "Updates the external port mapping for the MongoDB database and triggers a redeployment. Validates that the port is not already in use.",
|
||||
},
|
||||
})
|
||||
.input(apiSaveExternalPortMongo)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.mongoId, {
|
||||
@@ -218,6 +248,12 @@ export const mongoRouter = createTRPCRouter({
|
||||
return mongo;
|
||||
}),
|
||||
deploy: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Deploy a MongoDB database",
|
||||
description: "Triggers a deployment for the specified MongoDB database, rebuilding and restarting its Docker container with the current configuration.",
|
||||
},
|
||||
})
|
||||
.input(apiDeployMongo)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.mongoId, {
|
||||
@@ -271,6 +307,12 @@ export const mongoRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
changeStatus: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Change MongoDB database status",
|
||||
description: "Updates the application status of a MongoDB database without starting or stopping the container.",
|
||||
},
|
||||
})
|
||||
.input(apiChangeMongoStatus)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.mongoId, {
|
||||
@@ -289,6 +331,12 @@ export const mongoRouter = createTRPCRouter({
|
||||
return mongo;
|
||||
}),
|
||||
reload: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Reload a MongoDB database",
|
||||
description: "Restarts the MongoDB database by stopping and then starting its Docker container.",
|
||||
},
|
||||
})
|
||||
.input(apiResetMongo)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.mongoId, {
|
||||
@@ -321,6 +369,12 @@ export const mongoRouter = createTRPCRouter({
|
||||
return true;
|
||||
}),
|
||||
remove: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Delete a MongoDB database",
|
||||
description: "Removes the MongoDB database service, its Docker container, cancels associated backup jobs, and deletes the database record.",
|
||||
},
|
||||
})
|
||||
.input(apiFindOneMongo)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServiceAccess(ctx, input.mongoId, "delete");
|
||||
@@ -359,6 +413,12 @@ export const mongoRouter = createTRPCRouter({
|
||||
return mongo;
|
||||
}),
|
||||
saveEnvironment: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Save environment variables for a MongoDB database",
|
||||
description: "Updates the environment variables for the specified MongoDB database service.",
|
||||
},
|
||||
})
|
||||
.input(apiSaveEnvironmentVariablesMongo)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.mongoId, {
|
||||
@@ -383,6 +443,12 @@ export const mongoRouter = createTRPCRouter({
|
||||
return true;
|
||||
}),
|
||||
update: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Update a MongoDB database",
|
||||
description: "Updates the configuration of an existing MongoDB database service.",
|
||||
},
|
||||
})
|
||||
.input(apiUpdateMongo)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const { mongoId, ...rest } = input;
|
||||
@@ -409,6 +475,12 @@ export const mongoRouter = createTRPCRouter({
|
||||
return true;
|
||||
}),
|
||||
changePassword: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Change MongoDB database password",
|
||||
description: "Changes the password for the MongoDB database user by executing changeUserPassword via mongosh inside the running container and updating the stored password.",
|
||||
},
|
||||
})
|
||||
.input(
|
||||
z.object({
|
||||
mongoId: z.string().min(1),
|
||||
@@ -459,6 +531,12 @@ export const mongoRouter = createTRPCRouter({
|
||||
return true;
|
||||
}),
|
||||
move: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Move a MongoDB database to another environment",
|
||||
description: "Moves the MongoDB database to a different environment within the same project.",
|
||||
},
|
||||
})
|
||||
.input(
|
||||
z.object({
|
||||
mongoId: z.string(),
|
||||
@@ -495,6 +573,12 @@ export const mongoRouter = createTRPCRouter({
|
||||
return updatedMongo;
|
||||
}),
|
||||
rebuild: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Rebuild a MongoDB database",
|
||||
description: "Rebuilds the MongoDB database Docker container from scratch, pulling the latest image and recreating the service.",
|
||||
},
|
||||
})
|
||||
.input(apiRebuildMongo)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.mongoId, {
|
||||
@@ -511,6 +595,12 @@ export const mongoRouter = createTRPCRouter({
|
||||
return true;
|
||||
}),
|
||||
search: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Search MongoDB databases",
|
||||
description: "Returns a paginated list of MongoDB databases matching the given filters. Supports searching by name, appName, description, project, and environment.",
|
||||
},
|
||||
})
|
||||
.input(
|
||||
z.object({
|
||||
q: z.string().optional(),
|
||||
@@ -604,6 +694,12 @@ export const mongoRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
readLogs: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Read MongoDB container logs",
|
||||
description: "Retrieves the Docker container logs for the specified MongoDB database, with support for tail count, time-based filtering, and text search.",
|
||||
},
|
||||
})
|
||||
.input(
|
||||
apiFindOneMongo.extend({
|
||||
tail: z.number().int().min(1).max(10000).default(100),
|
||||
|
||||
@@ -75,6 +75,12 @@ async function getServiceOrganizationId(
|
||||
|
||||
export const mountRouter = createTRPCRouter({
|
||||
create: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Create mount",
|
||||
description: "Creates a new volume, bind, or file mount for a service. Checks service-level volume permissions and logs an audit event.",
|
||||
},
|
||||
})
|
||||
.input(apiCreateMount)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.serviceId, {
|
||||
@@ -90,6 +96,12 @@ export const mountRouter = createTRPCRouter({
|
||||
return mount;
|
||||
}),
|
||||
remove: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Delete mount",
|
||||
description: "Removes a mount by ID. Resolves the owning service to check volume delete permissions and logs an audit event.",
|
||||
},
|
||||
})
|
||||
.input(apiRemoveMount)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const mount = await findMountById(input.mountId);
|
||||
@@ -116,6 +128,12 @@ export const mountRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
one: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Get mount",
|
||||
description: "Returns a single mount by ID. Resolves the owning service to check volume read permissions.",
|
||||
},
|
||||
})
|
||||
.input(apiFindOneMount)
|
||||
.query(async ({ input, ctx }) => {
|
||||
const mount = await findMountById(input.mountId);
|
||||
@@ -136,6 +154,12 @@ export const mountRouter = createTRPCRouter({
|
||||
return mount;
|
||||
}),
|
||||
update: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Update mount",
|
||||
description: "Updates an existing mount. Resolves the owning service to check volume create permissions and logs an audit event.",
|
||||
},
|
||||
})
|
||||
.input(apiUpdateMount)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const mount = await findMountById(input.mountId);
|
||||
@@ -162,6 +186,12 @@ export const mountRouter = createTRPCRouter({
|
||||
return await updateMount(input.mountId, input);
|
||||
}),
|
||||
allNamedByApplicationId: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "List named volumes by application",
|
||||
description: "Returns Docker named volumes attached to the running container of a given application. Inspects the live container to retrieve mount information.",
|
||||
},
|
||||
})
|
||||
.input(z.object({ applicationId: z.string().min(1) }))
|
||||
.query(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.applicationId, {
|
||||
@@ -175,6 +205,12 @@ export const mountRouter = createTRPCRouter({
|
||||
return mounts;
|
||||
}),
|
||||
listByServiceId: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "List mounts by service",
|
||||
description: "Returns all configured mounts for a given service (application, compose, or database). Verifies service access and organization ownership.",
|
||||
},
|
||||
})
|
||||
.input(apiFindMountByApplicationId)
|
||||
.query(async ({ input, ctx }) => {
|
||||
await checkServiceAccess(ctx, input.serviceId, "read");
|
||||
|
||||
@@ -54,6 +54,12 @@ import { cancelJobs } from "@/server/utils/backup";
|
||||
|
||||
export const mysqlRouter = createTRPCRouter({
|
||||
create: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Create a MySQL database",
|
||||
description: "Creates a new MySQL database service with the specified configuration, sets up a persistent data volume, and registers it in the project.",
|
||||
},
|
||||
})
|
||||
.input(apiCreateMySql)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
@@ -118,6 +124,12 @@ export const mysqlRouter = createTRPCRouter({
|
||||
}
|
||||
}),
|
||||
one: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Get a MySQL database by ID",
|
||||
description: "Returns the full details of a MySQL database service, including its environment and project configuration.",
|
||||
},
|
||||
})
|
||||
.input(apiFindOneMySql)
|
||||
.query(async ({ input, ctx }) => {
|
||||
await checkServiceAccess(ctx, input.mysqlId, "read");
|
||||
@@ -135,6 +147,12 @@ export const mysqlRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
start: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Start a MySQL database",
|
||||
description: "Starts the Docker container for the specified MySQL database and sets its status to done.",
|
||||
},
|
||||
})
|
||||
.input(apiFindOneMySql)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.mysqlId, {
|
||||
@@ -160,6 +178,12 @@ export const mysqlRouter = createTRPCRouter({
|
||||
return service;
|
||||
}),
|
||||
stop: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Stop a MySQL database",
|
||||
description: "Stops the Docker container for the specified MySQL database and sets its status to idle.",
|
||||
},
|
||||
})
|
||||
.input(apiFindOneMySql)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.mysqlId, {
|
||||
@@ -184,6 +208,12 @@ export const mysqlRouter = createTRPCRouter({
|
||||
return mongo;
|
||||
}),
|
||||
saveExternalPort: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Save the external port for a MySQL database",
|
||||
description: "Updates the external port mapping for the MySQL database and triggers a redeployment. Validates that the port is not already in use.",
|
||||
},
|
||||
})
|
||||
.input(apiSaveExternalPortMySql)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.mysqlId, {
|
||||
@@ -217,6 +247,12 @@ export const mysqlRouter = createTRPCRouter({
|
||||
return mysql;
|
||||
}),
|
||||
deploy: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Deploy a MySQL database",
|
||||
description: "Triggers a deployment for the specified MySQL database, rebuilding and restarting its Docker container with the current configuration.",
|
||||
},
|
||||
})
|
||||
.input(apiDeployMySql)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.mysqlId, {
|
||||
@@ -270,6 +306,12 @@ export const mysqlRouter = createTRPCRouter({
|
||||
}
|
||||
}),
|
||||
changeStatus: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Change MySQL database status",
|
||||
description: "Updates the application status of a MySQL database without starting or stopping the container.",
|
||||
},
|
||||
})
|
||||
.input(apiChangeMySqlStatus)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.mysqlId, {
|
||||
@@ -288,6 +330,12 @@ export const mysqlRouter = createTRPCRouter({
|
||||
return mongo;
|
||||
}),
|
||||
reload: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Reload a MySQL database",
|
||||
description: "Restarts the MySQL database by stopping and then starting its Docker container.",
|
||||
},
|
||||
})
|
||||
.input(apiResetMysql)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.mysqlId, {
|
||||
@@ -319,6 +367,12 @@ export const mysqlRouter = createTRPCRouter({
|
||||
return true;
|
||||
}),
|
||||
remove: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Delete a MySQL database",
|
||||
description: "Removes the MySQL database service, its Docker container, cancels associated backup jobs, and deletes the database record.",
|
||||
},
|
||||
})
|
||||
.input(apiFindOneMySql)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServiceAccess(ctx, input.mysqlId, "delete");
|
||||
@@ -355,6 +409,12 @@ export const mysqlRouter = createTRPCRouter({
|
||||
return mongo;
|
||||
}),
|
||||
saveEnvironment: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Save environment variables for a MySQL database",
|
||||
description: "Updates the environment variables for the specified MySQL database service.",
|
||||
},
|
||||
})
|
||||
.input(apiSaveEnvironmentVariablesMySql)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.mysqlId, {
|
||||
@@ -379,6 +439,12 @@ export const mysqlRouter = createTRPCRouter({
|
||||
return true;
|
||||
}),
|
||||
update: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Update a MySQL database",
|
||||
description: "Updates the configuration of an existing MySQL database service.",
|
||||
},
|
||||
})
|
||||
.input(apiUpdateMySql)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const { mysqlId, ...rest } = input;
|
||||
@@ -405,6 +471,12 @@ export const mysqlRouter = createTRPCRouter({
|
||||
return true;
|
||||
}),
|
||||
changePassword: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Change MySQL database password",
|
||||
description: "Changes the password for a MySQL user or root account by executing ALTER USER inside the running container and updating the stored password.",
|
||||
},
|
||||
})
|
||||
.input(
|
||||
z.object({
|
||||
mysqlId: z.string().min(1),
|
||||
@@ -462,6 +534,12 @@ export const mysqlRouter = createTRPCRouter({
|
||||
return true;
|
||||
}),
|
||||
move: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Move a MySQL database to another environment",
|
||||
description: "Moves the MySQL database to a different environment within the same project.",
|
||||
},
|
||||
})
|
||||
.input(
|
||||
z.object({
|
||||
mysqlId: z.string(),
|
||||
@@ -498,6 +576,12 @@ export const mysqlRouter = createTRPCRouter({
|
||||
return updatedMysql;
|
||||
}),
|
||||
rebuild: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Rebuild a MySQL database",
|
||||
description: "Rebuilds the MySQL database Docker container from scratch, pulling the latest image and recreating the service.",
|
||||
},
|
||||
})
|
||||
.input(apiRebuildMysql)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.mysqlId, {
|
||||
@@ -514,6 +598,12 @@ export const mysqlRouter = createTRPCRouter({
|
||||
return true;
|
||||
}),
|
||||
search: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Search MySQL databases",
|
||||
description: "Returns a paginated list of MySQL databases matching the given filters. Supports searching by name, appName, description, project, and environment.",
|
||||
},
|
||||
})
|
||||
.input(
|
||||
z.object({
|
||||
q: z.string().optional(),
|
||||
@@ -607,6 +697,12 @@ export const mysqlRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
readLogs: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Read MySQL container logs",
|
||||
description: "Retrieves the Docker container logs for the specified MySQL database, with support for tail count, time-based filtering, and text search.",
|
||||
},
|
||||
})
|
||||
.input(
|
||||
apiFindOneMySql.extend({
|
||||
tail: z.number().int().min(1).max(10000).default(100),
|
||||
|
||||
@@ -95,6 +95,12 @@ import {
|
||||
|
||||
export const notificationRouter = createTRPCRouter({
|
||||
createSlack: withPermission("notification", "create")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Create Slack notification",
|
||||
description: "Creates a new Slack notification provider for the current organization and logs an audit event.",
|
||||
},
|
||||
})
|
||||
.input(apiCreateSlack)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
@@ -114,6 +120,12 @@ export const notificationRouter = createTRPCRouter({
|
||||
}
|
||||
}),
|
||||
updateSlack: withPermission("notification", "update")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Update Slack notification",
|
||||
description: "Updates an existing Slack notification provider. Verifies organization ownership before applying changes.",
|
||||
},
|
||||
})
|
||||
.input(apiUpdateSlack)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
@@ -140,6 +152,12 @@ export const notificationRouter = createTRPCRouter({
|
||||
}
|
||||
}),
|
||||
testSlackConnection: withPermission("notification", "create")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Test Slack connection",
|
||||
description: "Sends a test message to the configured Slack channel to verify the webhook connection works.",
|
||||
},
|
||||
})
|
||||
.input(apiTestSlackConnection)
|
||||
.mutation(async ({ input }) => {
|
||||
try {
|
||||
@@ -157,6 +175,12 @@ export const notificationRouter = createTRPCRouter({
|
||||
}
|
||||
}),
|
||||
createTelegram: withPermission("notification", "create")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Create Telegram notification",
|
||||
description: "Creates a new Telegram notification provider for the current organization and logs an audit event.",
|
||||
},
|
||||
})
|
||||
.input(apiCreateTelegram)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
@@ -179,6 +203,12 @@ export const notificationRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
updateTelegram: withPermission("notification", "update")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Update Telegram notification",
|
||||
description: "Updates an existing Telegram notification provider. Verifies organization ownership before applying changes.",
|
||||
},
|
||||
})
|
||||
.input(apiUpdateTelegram)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
@@ -209,6 +239,12 @@ export const notificationRouter = createTRPCRouter({
|
||||
}
|
||||
}),
|
||||
testTelegramConnection: withPermission("notification", "create")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Test Telegram connection",
|
||||
description: "Sends a test message to the configured Telegram chat to verify the bot token and chat ID work.",
|
||||
},
|
||||
})
|
||||
.input(apiTestTelegramConnection)
|
||||
.mutation(async ({ input }) => {
|
||||
try {
|
||||
@@ -223,6 +259,12 @@ export const notificationRouter = createTRPCRouter({
|
||||
}
|
||||
}),
|
||||
createDiscord: withPermission("notification", "create")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Create Discord notification",
|
||||
description: "Creates a new Discord notification provider for the current organization and logs an audit event.",
|
||||
},
|
||||
})
|
||||
.input(apiCreateDiscord)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
@@ -245,6 +287,12 @@ export const notificationRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
updateDiscord: withPermission("notification", "update")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Update Discord notification",
|
||||
description: "Updates an existing Discord notification provider. Verifies organization ownership before applying changes.",
|
||||
},
|
||||
})
|
||||
.input(apiUpdateDiscord)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
@@ -276,6 +324,12 @@ export const notificationRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
testDiscordConnection: withPermission("notification", "create")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Test Discord connection",
|
||||
description: "Sends a test embed message to the configured Discord webhook to verify the connection works.",
|
||||
},
|
||||
})
|
||||
.input(apiTestDiscordConnection)
|
||||
.mutation(async ({ input }) => {
|
||||
try {
|
||||
@@ -298,6 +352,12 @@ export const notificationRouter = createTRPCRouter({
|
||||
}
|
||||
}),
|
||||
createEmail: withPermission("notification", "create")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Create Email notification",
|
||||
description: "Creates a new SMTP email notification provider for the current organization and logs an audit event.",
|
||||
},
|
||||
})
|
||||
.input(apiCreateEmail)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
@@ -316,6 +376,12 @@ export const notificationRouter = createTRPCRouter({
|
||||
}
|
||||
}),
|
||||
updateEmail: withPermission("notification", "update")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Update Email notification",
|
||||
description: "Updates an existing SMTP email notification provider. Verifies organization ownership before applying changes.",
|
||||
},
|
||||
})
|
||||
.input(apiUpdateEmail)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
@@ -346,6 +412,12 @@ export const notificationRouter = createTRPCRouter({
|
||||
}
|
||||
}),
|
||||
testEmailConnection: withPermission("notification", "create")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Test Email connection",
|
||||
description: "Sends a test email via the configured SMTP settings to verify the connection works.",
|
||||
},
|
||||
})
|
||||
.input(apiTestEmailConnection)
|
||||
.mutation(async ({ input }) => {
|
||||
try {
|
||||
@@ -364,6 +436,12 @@ export const notificationRouter = createTRPCRouter({
|
||||
}
|
||||
}),
|
||||
createResend: withPermission("notification", "create")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Create Resend notification",
|
||||
description: "Creates a new Resend email notification provider for the current organization and logs an audit event.",
|
||||
},
|
||||
})
|
||||
.input(apiCreateResend)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
@@ -382,6 +460,12 @@ export const notificationRouter = createTRPCRouter({
|
||||
}
|
||||
}),
|
||||
updateResend: withPermission("notification", "update")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Update Resend notification",
|
||||
description: "Updates an existing Resend email notification provider. Verifies organization ownership before applying changes.",
|
||||
},
|
||||
})
|
||||
.input(apiUpdateResend)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
@@ -412,6 +496,12 @@ export const notificationRouter = createTRPCRouter({
|
||||
}
|
||||
}),
|
||||
testResendConnection: withPermission("notification", "create")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Test Resend connection",
|
||||
description: "Sends a test email via Resend to verify the API key and configuration work.",
|
||||
},
|
||||
})
|
||||
.input(apiTestResendConnection)
|
||||
.mutation(async ({ input }) => {
|
||||
try {
|
||||
@@ -430,6 +520,12 @@ export const notificationRouter = createTRPCRouter({
|
||||
}
|
||||
}),
|
||||
remove: withPermission("notification", "delete")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Delete notification",
|
||||
description: "Removes a notification provider by ID. Verifies organization ownership and logs an audit event before deletion.",
|
||||
},
|
||||
})
|
||||
.input(apiFindOneNotification)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
@@ -458,6 +554,12 @@ export const notificationRouter = createTRPCRouter({
|
||||
}
|
||||
}),
|
||||
one: withPermission("notification", "read")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Get notification",
|
||||
description: "Returns a single notification provider by ID. Verifies the caller belongs to the same organization.",
|
||||
},
|
||||
})
|
||||
.input(apiFindOneNotification)
|
||||
.query(async ({ input, ctx }) => {
|
||||
const notification = await findNotificationById(input.notificationId);
|
||||
@@ -469,7 +571,14 @@ export const notificationRouter = createTRPCRouter({
|
||||
}
|
||||
return notification;
|
||||
}),
|
||||
all: withPermission("notification", "read").query(async ({ ctx }) => {
|
||||
all: withPermission("notification", "read")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "List all notifications",
|
||||
description: "Returns all notification providers for the current organization, including all provider-specific details (Slack, Telegram, Discord, etc.).",
|
||||
},
|
||||
})
|
||||
.query(async ({ ctx }) => {
|
||||
return await db.query.notifications.findMany({
|
||||
with: {
|
||||
slack: true,
|
||||
@@ -490,6 +599,12 @@ export const notificationRouter = createTRPCRouter({
|
||||
});
|
||||
}),
|
||||
receiveNotification: publicProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Receive server threshold notification",
|
||||
description: "Public endpoint that receives CPU/memory threshold alerts from Dokploy or remote servers. Validates the token and dispatches notifications to all configured providers.",
|
||||
},
|
||||
})
|
||||
.input(
|
||||
z.object({
|
||||
ServerType: z.enum(["Dokploy", "Remote"]).default("Dokploy"),
|
||||
@@ -551,6 +666,12 @@ export const notificationRouter = createTRPCRouter({
|
||||
}
|
||||
}),
|
||||
createGotify: withPermission("notification", "create")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Create Gotify notification",
|
||||
description: "Creates a new Gotify notification provider for the current organization and logs an audit event.",
|
||||
},
|
||||
})
|
||||
.input(apiCreateGotify)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
@@ -569,6 +690,12 @@ export const notificationRouter = createTRPCRouter({
|
||||
}
|
||||
}),
|
||||
updateGotify: withPermission("notification", "update")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Update Gotify notification",
|
||||
description: "Updates an existing Gotify notification provider. Verifies organization ownership before applying changes.",
|
||||
},
|
||||
})
|
||||
.input(apiUpdateGotify)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
@@ -598,6 +725,12 @@ export const notificationRouter = createTRPCRouter({
|
||||
}
|
||||
}),
|
||||
testGotifyConnection: withPermission("notification", "create")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Test Gotify connection",
|
||||
description: "Sends a test notification to the configured Gotify server to verify the connection works.",
|
||||
},
|
||||
})
|
||||
.input(apiTestGotifyConnection)
|
||||
.mutation(async ({ input }) => {
|
||||
try {
|
||||
@@ -616,6 +749,12 @@ export const notificationRouter = createTRPCRouter({
|
||||
}
|
||||
}),
|
||||
createNtfy: withPermission("notification", "create")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Create ntfy notification",
|
||||
description: "Creates a new ntfy notification provider for the current organization and logs an audit event.",
|
||||
},
|
||||
})
|
||||
.input(apiCreateNtfy)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
@@ -634,6 +773,12 @@ export const notificationRouter = createTRPCRouter({
|
||||
}
|
||||
}),
|
||||
updateNtfy: withPermission("notification", "update")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Update ntfy notification",
|
||||
description: "Updates an existing ntfy notification provider. Verifies organization ownership before applying changes.",
|
||||
},
|
||||
})
|
||||
.input(apiUpdateNtfy)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
@@ -663,6 +808,12 @@ export const notificationRouter = createTRPCRouter({
|
||||
}
|
||||
}),
|
||||
testNtfyConnection: withPermission("notification", "create")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Test ntfy connection",
|
||||
description: "Sends a test notification to the configured ntfy topic to verify the connection works.",
|
||||
},
|
||||
})
|
||||
.input(apiTestNtfyConnection)
|
||||
.mutation(async ({ input }) => {
|
||||
try {
|
||||
@@ -686,6 +837,12 @@ export const notificationRouter = createTRPCRouter({
|
||||
}
|
||||
}),
|
||||
createMattermost: withPermission("notification", "create")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Create Mattermost notification",
|
||||
description: "Creates a new Mattermost notification provider for the current organization and logs an audit event.",
|
||||
},
|
||||
})
|
||||
.input(apiCreateMattermost)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
@@ -707,6 +864,12 @@ export const notificationRouter = createTRPCRouter({
|
||||
}
|
||||
}),
|
||||
updateMattermost: withPermission("notification", "update")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Update Mattermost notification",
|
||||
description: "Updates an existing Mattermost notification provider. Verifies organization ownership before applying changes.",
|
||||
},
|
||||
})
|
||||
.input(apiUpdateMattermost)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
@@ -736,6 +899,12 @@ export const notificationRouter = createTRPCRouter({
|
||||
}
|
||||
}),
|
||||
testMattermostConnection: withPermission("notification", "create")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Test Mattermost connection",
|
||||
description: "Sends a test message to the configured Mattermost webhook to verify the connection works.",
|
||||
},
|
||||
})
|
||||
.input(apiTestMattermostConnection)
|
||||
.mutation(async ({ input }) => {
|
||||
try {
|
||||
@@ -754,6 +923,12 @@ export const notificationRouter = createTRPCRouter({
|
||||
}
|
||||
}),
|
||||
createCustom: withPermission("notification", "create")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Create custom webhook notification",
|
||||
description: "Creates a new custom webhook notification provider for the current organization and logs an audit event.",
|
||||
},
|
||||
})
|
||||
.input(apiCreateCustom)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
@@ -772,6 +947,12 @@ export const notificationRouter = createTRPCRouter({
|
||||
}
|
||||
}),
|
||||
updateCustom: withPermission("notification", "update")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Update custom webhook notification",
|
||||
description: "Updates an existing custom webhook notification provider. Verifies organization ownership before applying changes.",
|
||||
},
|
||||
})
|
||||
.input(apiUpdateCustom)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
@@ -798,6 +979,12 @@ export const notificationRouter = createTRPCRouter({
|
||||
}
|
||||
}),
|
||||
testCustomConnection: withPermission("notification", "create")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Test custom webhook connection",
|
||||
description: "Sends a test payload to the configured custom webhook URL to verify the connection works.",
|
||||
},
|
||||
})
|
||||
.input(apiTestCustomConnection)
|
||||
.mutation(async ({ input }) => {
|
||||
try {
|
||||
@@ -816,6 +1003,12 @@ export const notificationRouter = createTRPCRouter({
|
||||
}
|
||||
}),
|
||||
createLark: withPermission("notification", "create")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Create Lark notification",
|
||||
description: "Creates a new Lark notification provider for the current organization and logs an audit event.",
|
||||
},
|
||||
})
|
||||
.input(apiCreateLark)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
@@ -834,6 +1027,12 @@ export const notificationRouter = createTRPCRouter({
|
||||
}
|
||||
}),
|
||||
updateLark: withPermission("notification", "update")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Update Lark notification",
|
||||
description: "Updates an existing Lark notification provider. Verifies organization ownership before applying changes.",
|
||||
},
|
||||
})
|
||||
.input(apiUpdateLark)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
@@ -863,6 +1062,12 @@ export const notificationRouter = createTRPCRouter({
|
||||
}
|
||||
}),
|
||||
testLarkConnection: withPermission("notification", "create")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Test Lark connection",
|
||||
description: "Sends a test message to the configured Lark webhook to verify the connection works.",
|
||||
},
|
||||
})
|
||||
.input(apiTestLarkConnection)
|
||||
.mutation(async ({ input }) => {
|
||||
try {
|
||||
@@ -882,6 +1087,12 @@ export const notificationRouter = createTRPCRouter({
|
||||
}
|
||||
}),
|
||||
createTeams: withPermission("notification", "create")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Create Teams notification",
|
||||
description: "Creates a new Microsoft Teams notification provider for the current organization and logs an audit event.",
|
||||
},
|
||||
})
|
||||
.input(apiCreateTeams)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
@@ -900,6 +1111,12 @@ export const notificationRouter = createTRPCRouter({
|
||||
}
|
||||
}),
|
||||
updateTeams: withPermission("notification", "update")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Update Teams notification",
|
||||
description: "Updates an existing Microsoft Teams notification provider. Verifies organization ownership before applying changes.",
|
||||
},
|
||||
})
|
||||
.input(apiUpdateTeams)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
@@ -929,6 +1146,12 @@ export const notificationRouter = createTRPCRouter({
|
||||
}
|
||||
}),
|
||||
testTeamsConnection: withPermission("notification", "create")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Test Teams connection",
|
||||
description: "Sends a test message to the configured Microsoft Teams webhook to verify the connection works.",
|
||||
},
|
||||
})
|
||||
.input(apiTestTeamsConnection)
|
||||
.mutation(async ({ input }) => {
|
||||
try {
|
||||
@@ -946,6 +1169,12 @@ export const notificationRouter = createTRPCRouter({
|
||||
}
|
||||
}),
|
||||
createPushover: withPermission("notification", "create")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Create Pushover notification",
|
||||
description: "Creates a new Pushover notification provider for the current organization and logs an audit event.",
|
||||
},
|
||||
})
|
||||
.input(apiCreatePushover)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
@@ -967,6 +1196,12 @@ export const notificationRouter = createTRPCRouter({
|
||||
}
|
||||
}),
|
||||
updatePushover: withPermission("notification", "update")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Update Pushover notification",
|
||||
description: "Updates an existing Pushover notification provider. Verifies organization ownership before applying changes.",
|
||||
},
|
||||
})
|
||||
.input(apiUpdatePushover)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
@@ -996,6 +1231,12 @@ export const notificationRouter = createTRPCRouter({
|
||||
}
|
||||
}),
|
||||
testPushoverConnection: withPermission("notification", "create")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Test Pushover connection",
|
||||
description: "Sends a test notification to the configured Pushover account to verify the connection works.",
|
||||
},
|
||||
})
|
||||
.input(apiTestPushoverConnection)
|
||||
.mutation(async ({ input }) => {
|
||||
try {
|
||||
@@ -1013,7 +1254,14 @@ export const notificationRouter = createTRPCRouter({
|
||||
});
|
||||
}
|
||||
}),
|
||||
getEmailProviders: withPermission("notification", "read").query(
|
||||
getEmailProviders: withPermission("notification", "read")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "List email notification providers",
|
||||
description: "Returns all notification providers that support email (SMTP and Resend) for the current organization.",
|
||||
},
|
||||
})
|
||||
.query(
|
||||
async ({ ctx }) => {
|
||||
return await db.query.notifications.findMany({
|
||||
where: eq(
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { db } from "@dokploy/server/db";
|
||||
import { IS_CLOUD, sendInvitationEmail } from "@dokploy/server/index";
|
||||
import { IS_CLOUD } from "@dokploy/server/index";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { and, desc, eq, exists } from "drizzle-orm";
|
||||
import { nanoid } from "nanoid";
|
||||
@@ -15,6 +15,12 @@ import {
|
||||
import { createTRPCRouter, protectedProcedure, withPermission } from "../trpc";
|
||||
export const organizationRouter = createTRPCRouter({
|
||||
create: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Create an organization",
|
||||
description: "Create a new organization and add the current user as the owner. Only owners and admins can create organizations in self-hosted mode.",
|
||||
},
|
||||
})
|
||||
.input(
|
||||
z.object({
|
||||
name: z.string(),
|
||||
@@ -65,7 +71,14 @@ export const organizationRouter = createTRPCRouter({
|
||||
});
|
||||
return result;
|
||||
}),
|
||||
all: protectedProcedure.query(async ({ ctx }) => {
|
||||
all: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "List all organizations",
|
||||
description: "Retrieve all organizations the current user is a member of, including their membership details.",
|
||||
},
|
||||
})
|
||||
.query(async ({ ctx }) => {
|
||||
const memberResult = await db.query.organization.findMany({
|
||||
where: (organization) =>
|
||||
exists(
|
||||
@@ -88,6 +101,12 @@ export const organizationRouter = createTRPCRouter({
|
||||
return memberResult;
|
||||
}),
|
||||
one: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Get an organization by ID",
|
||||
description: "Retrieve a single organization by its ID. The current user must be a member of the organization.",
|
||||
},
|
||||
})
|
||||
.input(
|
||||
z.object({
|
||||
organizationId: z.string(),
|
||||
@@ -114,6 +133,12 @@ export const organizationRouter = createTRPCRouter({
|
||||
});
|
||||
}),
|
||||
update: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Update an organization",
|
||||
description: "Update the name and logo of an organization. Only the organization owner can perform this action.",
|
||||
},
|
||||
})
|
||||
.input(
|
||||
z.object({
|
||||
organizationId: z.string(),
|
||||
@@ -178,6 +203,12 @@ export const organizationRouter = createTRPCRouter({
|
||||
return result[0];
|
||||
}),
|
||||
delete: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Delete an organization",
|
||||
description: "Delete an organization by ID. Only the owner can delete it, and they must retain at least one organization.",
|
||||
},
|
||||
})
|
||||
.input(
|
||||
z.object({
|
||||
organizationId: z.string(),
|
||||
@@ -248,6 +279,12 @@ export const organizationRouter = createTRPCRouter({
|
||||
return result;
|
||||
}),
|
||||
inviteMember: withPermission("member", "create")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Invite a member to organization",
|
||||
description: "Create a pending invitation for a user by email to join the active organization with the specified role. Checks for existing membership and pending invitations. Supports custom roles.",
|
||||
},
|
||||
})
|
||||
.input(
|
||||
z.object({
|
||||
email: z.string().email(),
|
||||
@@ -325,24 +362,6 @@ export const organizationRouter = createTRPCRouter({
|
||||
})
|
||||
.returning();
|
||||
|
||||
if (IS_CLOUD && created) {
|
||||
const host =
|
||||
process.env.NODE_ENV === "development"
|
||||
? "http://localhost:3000"
|
||||
: "https://app.dokploy.com";
|
||||
const inviteLink = `${host}/invitation?token=${created.id}`;
|
||||
|
||||
const org = await db.query.organization.findFirst({
|
||||
where: eq(organization.id, orgId),
|
||||
});
|
||||
|
||||
await sendInvitationEmail({
|
||||
email,
|
||||
inviteLink,
|
||||
organizationName: org?.name || "organization",
|
||||
});
|
||||
}
|
||||
|
||||
await audit(ctx, {
|
||||
action: "create",
|
||||
resourceType: "organization",
|
||||
@@ -353,13 +372,26 @@ export const organizationRouter = createTRPCRouter({
|
||||
return created;
|
||||
}),
|
||||
|
||||
allInvitations: withPermission("member", "create").query(async ({ ctx }) => {
|
||||
allInvitations: withPermission("member", "create")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "List all organization invitations",
|
||||
description: "Retrieve all invitations for the active organization, ordered by status and expiration date.",
|
||||
},
|
||||
})
|
||||
.query(async ({ ctx }) => {
|
||||
return await db.query.invitation.findMany({
|
||||
where: eq(invitation.organizationId, ctx.session.activeOrganizationId),
|
||||
orderBy: [desc(invitation.status), desc(invitation.expiresAt)],
|
||||
});
|
||||
}),
|
||||
removeInvitation: withPermission("member", "create")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Remove an invitation",
|
||||
description: "Delete a pending invitation by ID. Only invitations belonging to the active organization can be removed.",
|
||||
},
|
||||
})
|
||||
.input(z.object({ invitationId: z.string() }))
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const invitationResult = await db.query.invitation.findFirst({
|
||||
@@ -395,6 +427,12 @@ export const organizationRouter = createTRPCRouter({
|
||||
return result;
|
||||
}),
|
||||
updateMemberRole: withPermission("member", "update")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Update member role",
|
||||
description: "Change the role of a member in the active organization. Users cannot change their own role, and the owner role is nontransferable. Only owners can change admin roles. Supports custom roles.",
|
||||
},
|
||||
})
|
||||
.input(
|
||||
z.object({
|
||||
memberId: z.string(),
|
||||
@@ -481,6 +519,12 @@ export const organizationRouter = createTRPCRouter({
|
||||
return true;
|
||||
}),
|
||||
setDefault: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Set default organization",
|
||||
description: "Set an organization as the default for the current user. Unsets any previous default and marks the specified organization as the new default.",
|
||||
},
|
||||
})
|
||||
.input(
|
||||
z.object({
|
||||
organizationId: z.string().min(1),
|
||||
@@ -527,7 +571,14 @@ export const organizationRouter = createTRPCRouter({
|
||||
});
|
||||
return { success: true };
|
||||
}),
|
||||
active: protectedProcedure.query(async ({ ctx }) => {
|
||||
active: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Get active organization",
|
||||
description: "Retrieve the organization that is currently active in the user's session. Returns null if no organization is active.",
|
||||
},
|
||||
})
|
||||
.query(async ({ ctx }) => {
|
||||
if (!ctx.session.activeOrganizationId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -50,6 +50,12 @@ const resolvePatchServiceId = (patch: {
|
||||
|
||||
export const patchRouter = createTRPCRouter({
|
||||
create: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Create patch",
|
||||
description: "Creates a new file patch for an application or compose service. Checks service-level permissions and logs an audit event.",
|
||||
},
|
||||
})
|
||||
.input(apiCreatePatch)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const serviceId = input.applicationId ?? input.composeId;
|
||||
@@ -73,7 +79,15 @@ export const patchRouter = createTRPCRouter({
|
||||
return result;
|
||||
}),
|
||||
|
||||
one: protectedProcedure.input(apiFindPatch).query(async ({ input, ctx }) => {
|
||||
one: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Get patch",
|
||||
description: "Returns a single patch by ID. Resolves the associated service to verify read permissions.",
|
||||
},
|
||||
})
|
||||
.input(apiFindPatch)
|
||||
.query(async ({ input, ctx }) => {
|
||||
const patch = await findPatchById(input.patchId);
|
||||
const serviceId = resolvePatchServiceId(patch);
|
||||
await checkServicePermissionAndAccess(ctx, serviceId, {
|
||||
@@ -83,6 +97,12 @@ export const patchRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
byEntityId: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "List patches by entity",
|
||||
description: "Returns all patches associated with a given application or compose service.",
|
||||
},
|
||||
})
|
||||
.input(
|
||||
z.object({ id: z.string(), type: z.enum(["application", "compose"]) }),
|
||||
)
|
||||
@@ -94,6 +114,12 @@ export const patchRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
update: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Update patch",
|
||||
description: "Updates the content or configuration of an existing patch. Resolves the associated service to verify permissions and logs an audit event.",
|
||||
},
|
||||
})
|
||||
.input(apiUpdatePatch)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const patch = await findPatchById(input.patchId);
|
||||
@@ -114,6 +140,12 @@ export const patchRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
delete: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Delete patch",
|
||||
description: "Deletes a patch by ID. Resolves the associated service to verify delete permissions and logs an audit event.",
|
||||
},
|
||||
})
|
||||
.input(apiDeletePatch)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const patch = await findPatchById(input.patchId);
|
||||
@@ -133,6 +165,12 @@ export const patchRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
toggleEnabled: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Toggle patch enabled state",
|
||||
description: "Enables or disables a patch without deleting it. Resolves the associated service to verify permissions and logs an audit event.",
|
||||
},
|
||||
})
|
||||
.input(apiTogglePatchEnabled)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const patch = await findPatchById(input.patchId);
|
||||
@@ -155,6 +193,12 @@ export const patchRouter = createTRPCRouter({
|
||||
|
||||
// Repository Operations
|
||||
ensureRepo: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Ensure patch repository exists",
|
||||
description: "Ensures a patch repository is initialized for the given application or compose service. Creates the repo if it does not exist and logs an audit event.",
|
||||
},
|
||||
})
|
||||
.input(
|
||||
z.object({
|
||||
id: z.string(),
|
||||
@@ -179,6 +223,12 @@ export const patchRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
readRepoDirectories: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "List patch repository directories",
|
||||
description: "Reads the directory listing at a given path inside the patch repository for an application or compose service.",
|
||||
},
|
||||
})
|
||||
.input(
|
||||
z.object({
|
||||
id: z.string().min(1),
|
||||
@@ -202,6 +252,12 @@ export const patchRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
readRepoFile: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Read patch repository file",
|
||||
description: "Reads a file from the patch repository. For delete-type patches it returns the current repo content; otherwise returns the patch content if available, falling back to the repo file.",
|
||||
},
|
||||
})
|
||||
.input(
|
||||
z.object({
|
||||
id: z.string().min(1),
|
||||
@@ -241,6 +297,12 @@ export const patchRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
saveFileAsPatch: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Save file as patch",
|
||||
description: "Creates or updates a patch record from file content. If a patch already exists for the file path, it updates the existing patch; otherwise creates a new one.",
|
||||
},
|
||||
})
|
||||
.input(
|
||||
z.object({
|
||||
id: z.string().min(1),
|
||||
@@ -291,6 +353,12 @@ export const patchRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
markFileForDeletion: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Mark file for deletion",
|
||||
description: "Creates a delete-type patch that will remove the specified file from the service on next deployment. Logs an audit event.",
|
||||
},
|
||||
})
|
||||
.input(
|
||||
z.object({
|
||||
id: z.string().min(1),
|
||||
@@ -318,6 +386,12 @@ export const patchRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
cleanPatchRepos: adminProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Clean patch repositories",
|
||||
description: "Removes all patch repository working directories on the local or a specified remote server. Admin-only operation that logs an audit event.",
|
||||
},
|
||||
})
|
||||
.input(z.object({ serverId: z.string().optional() }))
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await cleanPatchRepos(input.serverId);
|
||||
|
||||
@@ -16,6 +16,12 @@ import {
|
||||
|
||||
export const portRouter = createTRPCRouter({
|
||||
create: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Create a port",
|
||||
description: "Creates a new port mapping for an application, binding a published port to a target port. Logs an audit entry.",
|
||||
},
|
||||
})
|
||||
.input(apiCreatePort)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
@@ -39,6 +45,12 @@ export const portRouter = createTRPCRouter({
|
||||
}
|
||||
}),
|
||||
one: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Get a port",
|
||||
description: "Returns a single port mapping by its ID, including the associated application details.",
|
||||
},
|
||||
})
|
||||
.input(apiFindOnePort)
|
||||
.query(async ({ input, ctx }) => {
|
||||
try {
|
||||
@@ -58,6 +70,12 @@ export const portRouter = createTRPCRouter({
|
||||
}
|
||||
}),
|
||||
delete: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Delete a port",
|
||||
description: "Deletes a port mapping by its ID and logs an audit entry with the published and target port details.",
|
||||
},
|
||||
})
|
||||
.input(apiFindOnePort)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const port = await finPortById(input.portId);
|
||||
@@ -85,6 +103,12 @@ export const portRouter = createTRPCRouter({
|
||||
}
|
||||
}),
|
||||
update: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Update a port",
|
||||
description: "Updates an existing port mapping's configuration and logs an audit entry.",
|
||||
},
|
||||
})
|
||||
.input(apiUpdatePort)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const port = await finPortById(input.portId);
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
findEnvironmentById,
|
||||
findPostgresById,
|
||||
findProjectById,
|
||||
getAccessibleServerIds,
|
||||
getContainerLogs,
|
||||
getMountPath,
|
||||
getServiceContainerCommand,
|
||||
@@ -21,7 +22,6 @@ import {
|
||||
stopService,
|
||||
stopServiceRemote,
|
||||
updatePostgresById,
|
||||
getAccessibleServerIds,
|
||||
} from "@dokploy/server";
|
||||
import { db } from "@dokploy/server/db";
|
||||
import {
|
||||
@@ -55,6 +55,12 @@ import { cancelJobs } from "@/server/utils/backup";
|
||||
|
||||
export const postgresRouter = createTRPCRouter({
|
||||
create: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Create a PostgreSQL database",
|
||||
description: "Creates a new PostgreSQL database service with the specified configuration, sets up a persistent data volume, and registers it in the project.",
|
||||
},
|
||||
})
|
||||
.input(apiCreatePostgres)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
@@ -121,6 +127,12 @@ export const postgresRouter = createTRPCRouter({
|
||||
}
|
||||
}),
|
||||
one: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Get a PostgreSQL database by ID",
|
||||
description: "Returns the full details of a PostgreSQL database service, including its environment and project configuration.",
|
||||
},
|
||||
})
|
||||
.input(apiFindOnePostgres)
|
||||
.query(async ({ input, ctx }) => {
|
||||
await checkServiceAccess(ctx, input.postgresId, "read");
|
||||
@@ -139,6 +151,12 @@ export const postgresRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
start: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Start a PostgreSQL database",
|
||||
description: "Starts the Docker container for the specified PostgreSQL database and sets its status to done.",
|
||||
},
|
||||
})
|
||||
.input(apiFindOnePostgres)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.postgresId, {
|
||||
@@ -164,6 +182,12 @@ export const postgresRouter = createTRPCRouter({
|
||||
return service;
|
||||
}),
|
||||
stop: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Stop a PostgreSQL database",
|
||||
description: "Stops the Docker container for the specified PostgreSQL database and sets its status to idle.",
|
||||
},
|
||||
})
|
||||
.input(apiFindOnePostgres)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.postgresId, {
|
||||
@@ -188,6 +212,12 @@ export const postgresRouter = createTRPCRouter({
|
||||
return postgres;
|
||||
}),
|
||||
saveExternalPort: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Save the external port for a PostgreSQL database",
|
||||
description: "Updates the external port mapping for the PostgreSQL database and triggers a redeployment. Validates that the port is not already in use.",
|
||||
},
|
||||
})
|
||||
.input(apiSaveExternalPortPostgres)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.postgresId, {
|
||||
@@ -221,6 +251,12 @@ export const postgresRouter = createTRPCRouter({
|
||||
return postgres;
|
||||
}),
|
||||
deploy: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Deploy a PostgreSQL database",
|
||||
description: "Triggers a deployment for the specified PostgreSQL database, rebuilding and restarting its Docker container with the current configuration.",
|
||||
},
|
||||
})
|
||||
.input(apiDeployPostgres)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.postgresId, {
|
||||
@@ -276,6 +312,12 @@ export const postgresRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
changeStatus: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Change PostgreSQL database status",
|
||||
description: "Updates the application status of a PostgreSQL database without starting or stopping the container.",
|
||||
},
|
||||
})
|
||||
.input(apiChangePostgresStatus)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.postgresId, {
|
||||
@@ -294,6 +336,12 @@ export const postgresRouter = createTRPCRouter({
|
||||
return postgres;
|
||||
}),
|
||||
remove: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Delete a PostgreSQL database",
|
||||
description: "Removes the PostgreSQL database service, its Docker container, cancels associated backup jobs, and deletes the database record.",
|
||||
},
|
||||
})
|
||||
.input(apiFindOnePostgres)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServiceAccess(ctx, input.postgresId, "delete");
|
||||
@@ -332,6 +380,12 @@ export const postgresRouter = createTRPCRouter({
|
||||
return postgres;
|
||||
}),
|
||||
saveEnvironment: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Save environment variables for a PostgreSQL database",
|
||||
description: "Updates the environment variables for the specified PostgreSQL database service.",
|
||||
},
|
||||
})
|
||||
.input(apiSaveEnvironmentVariablesPostgres)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.postgresId, {
|
||||
@@ -356,6 +410,12 @@ export const postgresRouter = createTRPCRouter({
|
||||
return true;
|
||||
}),
|
||||
reload: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Reload a PostgreSQL database",
|
||||
description: "Restarts the PostgreSQL database by stopping and then starting its Docker container.",
|
||||
},
|
||||
})
|
||||
.input(apiResetPostgres)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.postgresId, {
|
||||
@@ -388,6 +448,12 @@ export const postgresRouter = createTRPCRouter({
|
||||
return true;
|
||||
}),
|
||||
update: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Update a PostgreSQL database",
|
||||
description: "Updates the configuration of an existing PostgreSQL database service.",
|
||||
},
|
||||
})
|
||||
.input(apiUpdatePostgres)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const { postgresId, ...rest } = input;
|
||||
@@ -415,6 +481,12 @@ export const postgresRouter = createTRPCRouter({
|
||||
return true;
|
||||
}),
|
||||
changePassword: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Change PostgreSQL database password",
|
||||
description: "Changes the password for the PostgreSQL database user by executing ALTER USER inside the running container and updating the stored password.",
|
||||
},
|
||||
})
|
||||
.input(
|
||||
z.object({
|
||||
postgresId: z.string().min(1),
|
||||
@@ -465,6 +537,12 @@ export const postgresRouter = createTRPCRouter({
|
||||
return true;
|
||||
}),
|
||||
move: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Move a PostgreSQL database to another environment",
|
||||
description: "Moves the PostgreSQL database to a different environment within the same project.",
|
||||
},
|
||||
})
|
||||
.input(
|
||||
z.object({
|
||||
postgresId: z.string(),
|
||||
@@ -501,6 +579,12 @@ export const postgresRouter = createTRPCRouter({
|
||||
return updatedPostgres;
|
||||
}),
|
||||
rebuild: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Rebuild a PostgreSQL database",
|
||||
description: "Rebuilds the PostgreSQL database Docker container from scratch, pulling the latest image and recreating the service.",
|
||||
},
|
||||
})
|
||||
.input(apiRebuildPostgres)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.postgresId, {
|
||||
@@ -517,6 +601,12 @@ export const postgresRouter = createTRPCRouter({
|
||||
return true;
|
||||
}),
|
||||
search: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Search PostgreSQL databases",
|
||||
description: "Returns a paginated list of PostgreSQL databases matching the given filters. Supports searching by name, appName, description, project, and environment.",
|
||||
},
|
||||
})
|
||||
.input(
|
||||
z.object({
|
||||
q: z.string().optional(),
|
||||
@@ -617,6 +707,12 @@ export const postgresRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
readLogs: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Read PostgreSQL container logs",
|
||||
description: "Retrieves the Docker container logs for the specified PostgreSQL database, with support for tail count, time-based filtering, and text search.",
|
||||
},
|
||||
})
|
||||
.input(
|
||||
apiFindOnePostgres.extend({
|
||||
tail: z.number().int().min(1).max(10000).default(100),
|
||||
|
||||
@@ -16,6 +16,12 @@ import { createTRPCRouter, protectedProcedure } from "../trpc";
|
||||
|
||||
export const previewDeploymentRouter = createTRPCRouter({
|
||||
all: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "List preview deployments",
|
||||
description: "Returns all preview deployments associated with the given application.",
|
||||
},
|
||||
})
|
||||
.input(apiFindAllByApplication)
|
||||
.query(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.applicationId, {
|
||||
@@ -25,6 +31,12 @@ export const previewDeploymentRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
one: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Get a preview deployment",
|
||||
description: "Returns the details of a specific preview deployment by its ID.",
|
||||
},
|
||||
})
|
||||
.input(z.object({ previewDeploymentId: z.string() }))
|
||||
.query(async ({ input, ctx }) => {
|
||||
const previewDeployment = await findPreviewDeploymentById(
|
||||
@@ -39,6 +51,12 @@ export const previewDeploymentRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
delete: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Delete a preview deployment",
|
||||
description: "Permanently removes a preview deployment and its associated resources.",
|
||||
},
|
||||
})
|
||||
.input(z.object({ previewDeploymentId: z.string() }))
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const previewDeployment = await findPreviewDeploymentById(
|
||||
@@ -59,6 +77,12 @@ export const previewDeploymentRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
redeploy: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Redeploy a preview deployment",
|
||||
description: "Triggers a rebuild of an existing preview deployment by adding a new job to the deployment queue.",
|
||||
},
|
||||
})
|
||||
.input(
|
||||
z.object({
|
||||
previewDeploymentId: z.string(),
|
||||
|
||||
@@ -67,6 +67,12 @@ import {
|
||||
|
||||
export const projectRouter = createTRPCRouter({
|
||||
create: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Create a project",
|
||||
description: "Creates a new project in the current organization with a default environment. Validates server availability for cloud deployments.",
|
||||
},
|
||||
})
|
||||
.input(apiCreateProject)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
try {
|
||||
@@ -106,6 +112,12 @@ export const projectRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
one: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Get a project",
|
||||
description: "Retrieves a project by its ID with all environments and services. Filters services based on the user's access permissions.",
|
||||
},
|
||||
})
|
||||
.input(apiFindOneProject)
|
||||
.query(async ({ input, ctx }) => {
|
||||
if (ctx.user.role !== "owner" && ctx.user.role !== "admin") {
|
||||
@@ -193,7 +205,14 @@ export const projectRouter = createTRPCRouter({
|
||||
}
|
||||
return project;
|
||||
}),
|
||||
all: protectedProcedure.query(async ({ ctx }) => {
|
||||
all: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "List all projects",
|
||||
description: "Returns all projects in the current organization with their environments and services. Filters results based on the user's access permissions.",
|
||||
},
|
||||
})
|
||||
.query(async ({ ctx }) => {
|
||||
if (ctx.user.role !== "owner" && ctx.user.role !== "admin") {
|
||||
const { accessedProjects, accessedEnvironments, accessedServices } =
|
||||
await findMemberByUserId(ctx.user.id, ctx.session.activeOrganizationId);
|
||||
@@ -375,7 +394,14 @@ export const projectRouter = createTRPCRouter({
|
||||
});
|
||||
}),
|
||||
|
||||
allForPermissions: withPermission("member", "update").query(
|
||||
allForPermissions: withPermission("member", "update")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "List all projects for permissions",
|
||||
description: "Returns all projects with their environments and services for the permissions management UI. Requires member update permission.",
|
||||
},
|
||||
})
|
||||
.query(
|
||||
async ({ ctx }) => {
|
||||
return await db.query.projects.findMany({
|
||||
where: eq(projects.organizationId, ctx.session.activeOrganizationId),
|
||||
@@ -487,149 +513,13 @@ export const projectRouter = createTRPCRouter({
|
||||
},
|
||||
),
|
||||
|
||||
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
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Search projects",
|
||||
description: "Searches projects by name or description with pagination. Respects project-level access control for non-admin users.",
|
||||
},
|
||||
})
|
||||
.input(
|
||||
z.object({
|
||||
q: z.string().optional(),
|
||||
@@ -707,6 +597,12 @@ export const projectRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
remove: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Delete a project",
|
||||
description: "Permanently deletes a project and all its associated environments, services, and resources.",
|
||||
},
|
||||
})
|
||||
.input(apiRemoveProject)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
@@ -734,6 +630,12 @@ export const projectRouter = createTRPCRouter({
|
||||
}
|
||||
}),
|
||||
update: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Update a project",
|
||||
description: "Updates a project's name, description, or environment variables. Validates organization ownership and project-level access permissions.",
|
||||
},
|
||||
})
|
||||
.input(apiUpdateProject)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
@@ -782,6 +684,12 @@ export const projectRouter = createTRPCRouter({
|
||||
}
|
||||
}),
|
||||
duplicate: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Duplicate a project or environment",
|
||||
description: "Duplicates services from a source environment into a new project or into the same project. Copies applications, compose services, databases, and their associated domains, mounts, ports, and backups.",
|
||||
},
|
||||
})
|
||||
.input(
|
||||
z.object({
|
||||
sourceEnvironmentId: z.string(),
|
||||
|
||||
@@ -15,6 +15,12 @@ import {
|
||||
|
||||
export const redirectsRouter = createTRPCRouter({
|
||||
create: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Create a redirect",
|
||||
description: "Creates a new redirect rule for an application using a regex pattern. Logs an audit entry.",
|
||||
},
|
||||
})
|
||||
.input(apiCreateRedirect)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.applicationId, {
|
||||
@@ -31,6 +37,12 @@ export const redirectsRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
one: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Get a redirect",
|
||||
description: "Returns a single redirect rule by its ID.",
|
||||
},
|
||||
})
|
||||
.input(apiFindOneRedirect)
|
||||
.query(async ({ input, ctx }) => {
|
||||
const redirect = await findRedirectById(input.redirectId);
|
||||
@@ -41,6 +53,12 @@ export const redirectsRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
delete: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Delete a redirect",
|
||||
description: "Deletes a redirect rule by its ID and logs an audit entry.",
|
||||
},
|
||||
})
|
||||
.input(apiFindOneRedirect)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const redirect = await findRedirectById(input.redirectId);
|
||||
@@ -57,6 +75,12 @@ export const redirectsRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
update: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Update a redirect",
|
||||
description: "Updates an existing redirect rule's configuration and logs an audit entry.",
|
||||
},
|
||||
})
|
||||
.input(apiUpdateRedirect)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const redirect = await findRedirectById(input.redirectId);
|
||||
|
||||
@@ -51,6 +51,12 @@ import {
|
||||
} from "@/server/db/schema";
|
||||
export const redisRouter = createTRPCRouter({
|
||||
create: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Create a Redis database",
|
||||
description: "Creates a new Redis database service with the specified configuration, sets up a persistent data volume, and registers it in the project.",
|
||||
},
|
||||
})
|
||||
.input(apiCreateRedis)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
@@ -108,6 +114,12 @@ export const redisRouter = createTRPCRouter({
|
||||
}
|
||||
}),
|
||||
one: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Get a Redis database by ID",
|
||||
description: "Returns the full details of a Redis database service, including its environment and project configuration.",
|
||||
},
|
||||
})
|
||||
.input(apiFindOneRedis)
|
||||
.query(async ({ input, ctx }) => {
|
||||
await checkServiceAccess(ctx, input.redisId, "read");
|
||||
@@ -126,6 +138,12 @@ export const redisRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
start: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Start a Redis database",
|
||||
description: "Starts the Docker container for the specified Redis database and sets its status to done.",
|
||||
},
|
||||
})
|
||||
.input(apiFindOneRedis)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.redisId, {
|
||||
@@ -151,6 +169,12 @@ export const redisRouter = createTRPCRouter({
|
||||
return redis;
|
||||
}),
|
||||
reload: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Reload a Redis database",
|
||||
description: "Restarts the Redis database by stopping and then starting its Docker container.",
|
||||
},
|
||||
})
|
||||
.input(apiResetRedis)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.redisId, {
|
||||
@@ -184,6 +208,12 @@ export const redisRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
stop: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Stop a Redis database",
|
||||
description: "Stops the Docker container for the specified Redis database and sets its status to idle.",
|
||||
},
|
||||
})
|
||||
.input(apiFindOneRedis)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.redisId, {
|
||||
@@ -208,6 +238,12 @@ export const redisRouter = createTRPCRouter({
|
||||
return redis;
|
||||
}),
|
||||
saveExternalPort: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Save the external port for a Redis database",
|
||||
description: "Updates the external port mapping for the Redis database and triggers a redeployment. Validates that the port is not already in use.",
|
||||
},
|
||||
})
|
||||
.input(apiSaveExternalPortRedis)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.redisId, {
|
||||
@@ -241,6 +277,12 @@ export const redisRouter = createTRPCRouter({
|
||||
return redis;
|
||||
}),
|
||||
deploy: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Deploy a Redis database",
|
||||
description: "Triggers a deployment for the specified Redis database, rebuilding and restarting its Docker container with the current configuration.",
|
||||
},
|
||||
})
|
||||
.input(apiDeployRedis)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.redisId, {
|
||||
@@ -293,6 +335,12 @@ export const redisRouter = createTRPCRouter({
|
||||
}
|
||||
}),
|
||||
changeStatus: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Change Redis database status",
|
||||
description: "Updates the application status of a Redis database without starting or stopping the container.",
|
||||
},
|
||||
})
|
||||
.input(apiChangeRedisStatus)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.redisId, {
|
||||
@@ -311,6 +359,12 @@ export const redisRouter = createTRPCRouter({
|
||||
return mongo;
|
||||
}),
|
||||
remove: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Delete a Redis database",
|
||||
description: "Removes the Redis database service, its Docker container, and deletes the database record.",
|
||||
},
|
||||
})
|
||||
.input(apiFindOneRedis)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServiceAccess(ctx, input.redisId, "delete");
|
||||
@@ -346,6 +400,12 @@ export const redisRouter = createTRPCRouter({
|
||||
return redis;
|
||||
}),
|
||||
saveEnvironment: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Save environment variables for a Redis database",
|
||||
description: "Updates the environment variables for the specified Redis database service.",
|
||||
},
|
||||
})
|
||||
.input(apiSaveEnvironmentVariablesRedis)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.redisId, {
|
||||
@@ -370,6 +430,12 @@ export const redisRouter = createTRPCRouter({
|
||||
return true;
|
||||
}),
|
||||
update: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Update a Redis database",
|
||||
description: "Updates the configuration of an existing Redis database service.",
|
||||
},
|
||||
})
|
||||
.input(apiUpdateRedis)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const { redisId, ...rest } = input;
|
||||
@@ -396,6 +462,12 @@ export const redisRouter = createTRPCRouter({
|
||||
return true;
|
||||
}),
|
||||
changePassword: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Change Redis database password",
|
||||
description: "Changes the password for the Redis database by executing CONFIG SET requirepass inside the running container and updating the stored password.",
|
||||
},
|
||||
})
|
||||
.input(
|
||||
z.object({
|
||||
redisId: z.string().min(1),
|
||||
@@ -446,6 +518,12 @@ export const redisRouter = createTRPCRouter({
|
||||
return true;
|
||||
}),
|
||||
move: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Move a Redis database to another environment",
|
||||
description: "Moves the Redis database to a different environment within the same project.",
|
||||
},
|
||||
})
|
||||
.input(
|
||||
z.object({
|
||||
redisId: z.string(),
|
||||
@@ -482,6 +560,12 @@ export const redisRouter = createTRPCRouter({
|
||||
return updatedRedis;
|
||||
}),
|
||||
rebuild: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Rebuild a Redis database",
|
||||
description: "Rebuilds the Redis database Docker container from scratch, pulling the latest image and recreating the service.",
|
||||
},
|
||||
})
|
||||
.input(apiRebuildRedis)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.redisId, {
|
||||
@@ -497,6 +581,12 @@ export const redisRouter = createTRPCRouter({
|
||||
return true;
|
||||
}),
|
||||
search: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Search Redis databases",
|
||||
description: "Returns a paginated list of Redis databases matching the given filters. Supports searching by name, appName, description, project, and environment.",
|
||||
},
|
||||
})
|
||||
.input(
|
||||
z.object({
|
||||
q: z.string().optional(),
|
||||
@@ -590,6 +680,12 @@ export const redisRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
readLogs: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Read Redis container logs",
|
||||
description: "Retrieves the Docker container logs for the specified Redis database, with support for tail count, time-based filtering, and text search.",
|
||||
},
|
||||
})
|
||||
.input(
|
||||
apiFindOneRedis.extend({
|
||||
tail: z.number().int().min(1).max(10000).default(100),
|
||||
|
||||
@@ -23,6 +23,12 @@ import {
|
||||
import { createTRPCRouter, withPermission } from "../trpc";
|
||||
export const registryRouter = createTRPCRouter({
|
||||
create: withPermission("registry", "create")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Create registry",
|
||||
description: "Creates a new Docker registry entry for the current organization and logs an audit event.",
|
||||
},
|
||||
})
|
||||
.input(apiCreateRegistry)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const reg = await createRegistry(input, ctx.session.activeOrganizationId);
|
||||
@@ -35,6 +41,12 @@ export const registryRouter = createTRPCRouter({
|
||||
return reg;
|
||||
}),
|
||||
remove: withPermission("registry", "delete")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Delete registry",
|
||||
description: "Removes a Docker registry entry by ID. Verifies organization ownership and logs an audit event before deletion.",
|
||||
},
|
||||
})
|
||||
.input(apiRemoveRegistry)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const registry = await findRegistryById(input.registryId);
|
||||
@@ -53,6 +65,12 @@ export const registryRouter = createTRPCRouter({
|
||||
return await removeRegistry(input.registryId);
|
||||
}),
|
||||
update: withPermission("registry", "create")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Update registry",
|
||||
description: "Updates an existing Docker registry entry. Verifies organization ownership before applying changes and logs an audit event.",
|
||||
},
|
||||
})
|
||||
.input(apiUpdateRegistry)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const { registryId, ...rest } = input;
|
||||
@@ -82,13 +100,26 @@ export const registryRouter = createTRPCRouter({
|
||||
});
|
||||
return true;
|
||||
}),
|
||||
all: withPermission("registry", "read").query(async ({ ctx }) => {
|
||||
all: withPermission("registry", "read")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "List all registries",
|
||||
description: "Returns all Docker registry entries for the current organization.",
|
||||
},
|
||||
})
|
||||
.query(async ({ ctx }) => {
|
||||
const registryResponse = await db.query.registry.findMany({
|
||||
where: eq(registry.organizationId, ctx.session.activeOrganizationId),
|
||||
});
|
||||
return registryResponse;
|
||||
}),
|
||||
one: withPermission("registry", "read")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Get registry",
|
||||
description: "Returns a single Docker registry entry by ID. Verifies the caller belongs to the same organization.",
|
||||
},
|
||||
})
|
||||
.input(apiFindOneRegistry)
|
||||
.query(async ({ input, ctx }) => {
|
||||
const registry = await findRegistryById(input.registryId);
|
||||
@@ -101,6 +132,12 @@ export const registryRouter = createTRPCRouter({
|
||||
return registry;
|
||||
}),
|
||||
testRegistry: withPermission("registry", "read")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Test registry credentials",
|
||||
description: "Attempts a docker login with the provided credentials to verify the registry URL, username, and password are valid. Can run locally or on a remote server.",
|
||||
},
|
||||
})
|
||||
.input(apiTestRegistry)
|
||||
.mutation(async ({ input }) => {
|
||||
try {
|
||||
@@ -143,6 +180,12 @@ export const registryRouter = createTRPCRouter({
|
||||
}
|
||||
}),
|
||||
testRegistryById: withPermission("registry", "read")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Test registry connection by ID",
|
||||
description: "Looks up a saved registry by ID and attempts a docker login with its stored credentials. Verifies organization ownership before testing.",
|
||||
},
|
||||
})
|
||||
.input(apiTestRegistryById)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
|
||||
@@ -11,6 +11,12 @@ import { createTRPCRouter, protectedProcedure } from "../trpc";
|
||||
|
||||
export const rollbackRouter = createTRPCRouter({
|
||||
delete: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Delete a rollback",
|
||||
description: "Permanently removes a rollback record by its ID.",
|
||||
},
|
||||
})
|
||||
.input(apiFindOneRollback)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
@@ -40,6 +46,12 @@ export const rollbackRouter = createTRPCRouter({
|
||||
}
|
||||
}),
|
||||
rollback: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Perform a rollback",
|
||||
description: "Rolls back an application to a previous deployment by restoring its Docker image and redeploying.",
|
||||
},
|
||||
})
|
||||
.input(apiFindOneRollback)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
|
||||
@@ -7,27 +7,27 @@ import {
|
||||
updateScheduleSchema,
|
||||
} from "@dokploy/server/db/schema/schedule";
|
||||
import { runCommand } from "@dokploy/server/index";
|
||||
import {
|
||||
checkPermission,
|
||||
checkServicePermissionAndAccess,
|
||||
findMemberByUserId,
|
||||
} from "@dokploy/server/services/permission";
|
||||
import { checkServicePermissionAndAccess } from "@dokploy/server/services/permission";
|
||||
import {
|
||||
createSchedule,
|
||||
deleteSchedule,
|
||||
findScheduleById,
|
||||
updateSchedule,
|
||||
} from "@dokploy/server/services/schedule";
|
||||
import { findServerById } from "@dokploy/server/services/server";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { asc, desc, eq } from "drizzle-orm";
|
||||
import { z } from "zod";
|
||||
import { audit } from "@/server/api/utils/audit";
|
||||
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;
|
||||
@@ -35,45 +35,6 @@ export const scheduleRouter = createTRPCRouter({
|
||||
await checkServicePermissionAndAccess(ctx, serviceId, {
|
||||
schedule: ["create"],
|
||||
});
|
||||
} else {
|
||||
if (input.scheduleType === "dokploy-server" && IS_CLOUD) {
|
||||
throw new TRPCError({
|
||||
code: "FORBIDDEN",
|
||||
message:
|
||||
"Host-level schedules are not available in the cloud version.",
|
||||
});
|
||||
}
|
||||
|
||||
await checkPermission(ctx, { schedule: ["create"] });
|
||||
|
||||
if (
|
||||
input.scheduleType === "server" ||
|
||||
input.scheduleType === "dokploy-server"
|
||||
) {
|
||||
const member = await findMemberByUserId(
|
||||
ctx.user.id,
|
||||
ctx.session.activeOrganizationId,
|
||||
);
|
||||
if (member.role !== "owner" && member.role !== "admin") {
|
||||
throw new TRPCError({
|
||||
code: "FORBIDDEN",
|
||||
message:
|
||||
"Only owners and admins can manage server-level schedules.",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (input.scheduleType === "server" && input.serverId) {
|
||||
const targetServer = await findServerById(input.serverId);
|
||||
if (
|
||||
targetServer.organizationId !== ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You don't have access to this server.",
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
const newSchedule = await createSchedule(input);
|
||||
|
||||
@@ -99,80 +60,21 @@ 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);
|
||||
|
||||
if (
|
||||
IS_CLOUD &&
|
||||
input.scheduleType &&
|
||||
input.scheduleType !== existingSchedule.scheduleType
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "FORBIDDEN",
|
||||
message: "Changing scheduleType is not allowed in the cloud version.",
|
||||
});
|
||||
}
|
||||
|
||||
const serviceId =
|
||||
existingSchedule.applicationId || existingSchedule.composeId;
|
||||
if (serviceId) {
|
||||
await checkServicePermissionAndAccess(ctx, serviceId, {
|
||||
schedule: ["update"],
|
||||
});
|
||||
} else {
|
||||
if (existingSchedule.scheduleType === "dokploy-server" && IS_CLOUD) {
|
||||
throw new TRPCError({
|
||||
code: "FORBIDDEN",
|
||||
message:
|
||||
"Host-level schedules are not available in the cloud version.",
|
||||
});
|
||||
}
|
||||
|
||||
await checkPermission(ctx, { schedule: ["update"] });
|
||||
|
||||
if (
|
||||
existingSchedule.scheduleType === "server" ||
|
||||
existingSchedule.scheduleType === "dokploy-server"
|
||||
) {
|
||||
const member = await findMemberByUserId(
|
||||
ctx.user.id,
|
||||
ctx.session.activeOrganizationId,
|
||||
);
|
||||
if (member.role !== "owner" && member.role !== "admin") {
|
||||
throw new TRPCError({
|
||||
code: "FORBIDDEN",
|
||||
message:
|
||||
"Only owners and admins can manage server-level schedules.",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
existingSchedule.scheduleType === "server" &&
|
||||
existingSchedule.serverId
|
||||
) {
|
||||
const targetServer = await findServerById(existingSchedule.serverId);
|
||||
if (
|
||||
targetServer.organizationId !== ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You don't have access to this server.",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
existingSchedule.scheduleType === "dokploy-server" &&
|
||||
existingSchedule.userId &&
|
||||
existingSchedule.userId !== ctx.user.id
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You can only manage your own host-level schedules.",
|
||||
});
|
||||
}
|
||||
}
|
||||
const updatedSchedule = await updateSchedule(input);
|
||||
|
||||
@@ -209,6 +111,12 @@ export const scheduleRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
delete: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Delete a scheduled job",
|
||||
description: "Permanently removes a scheduled job and unschedules any associated cron job.",
|
||||
},
|
||||
})
|
||||
.input(z.object({ scheduleId: z.string() }))
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const scheduleItem = await findScheduleById(input.scheduleId);
|
||||
@@ -217,56 +125,6 @@ export const scheduleRouter = createTRPCRouter({
|
||||
await checkServicePermissionAndAccess(ctx, serviceId, {
|
||||
schedule: ["delete"],
|
||||
});
|
||||
} else {
|
||||
if (scheduleItem.scheduleType === "dokploy-server" && IS_CLOUD) {
|
||||
throw new TRPCError({
|
||||
code: "FORBIDDEN",
|
||||
message:
|
||||
"Host-level schedules are not available in the cloud version.",
|
||||
});
|
||||
}
|
||||
|
||||
await checkPermission(ctx, { schedule: ["delete"] });
|
||||
|
||||
if (
|
||||
scheduleItem.scheduleType === "server" ||
|
||||
scheduleItem.scheduleType === "dokploy-server"
|
||||
) {
|
||||
const member = await findMemberByUserId(
|
||||
ctx.user.id,
|
||||
ctx.session.activeOrganizationId,
|
||||
);
|
||||
if (member.role !== "owner" && member.role !== "admin") {
|
||||
throw new TRPCError({
|
||||
code: "FORBIDDEN",
|
||||
message:
|
||||
"Only owners and admins can manage server-level schedules.",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (scheduleItem.scheduleType === "server" && scheduleItem.serverId) {
|
||||
const targetServer = await findServerById(scheduleItem.serverId);
|
||||
if (
|
||||
targetServer.organizationId !== ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You don't have access to this server.",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
scheduleItem.scheduleType === "dokploy-server" &&
|
||||
scheduleItem.userId &&
|
||||
scheduleItem.userId !== ctx.user.id
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You can only manage your own host-level schedules.",
|
||||
});
|
||||
}
|
||||
}
|
||||
await deleteSchedule(input.scheduleId);
|
||||
|
||||
@@ -289,6 +147,12 @@ export const scheduleRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
list: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "List scheduled jobs",
|
||||
description: "Returns all scheduled jobs for a given service (application, compose, server, or dokploy-server), including their deployment history.",
|
||||
},
|
||||
})
|
||||
.input(
|
||||
z.object({
|
||||
id: z.string(),
|
||||
@@ -308,30 +172,6 @@ export const scheduleRouter = createTRPCRouter({
|
||||
await checkServicePermissionAndAccess(ctx, input.id, {
|
||||
schedule: ["read"],
|
||||
});
|
||||
} else {
|
||||
await checkPermission(ctx, { schedule: ["read"] });
|
||||
|
||||
if (input.scheduleType === "server") {
|
||||
const targetServer = await findServerById(input.id);
|
||||
if (
|
||||
targetServer.organizationId !== ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You don't have access to this server.",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
input.scheduleType === "dokploy-server" &&
|
||||
input.id !== ctx.user.id
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You can only list your own host-level schedules.",
|
||||
});
|
||||
}
|
||||
}
|
||||
const where = {
|
||||
application: eq(schedules.applicationId, input.id),
|
||||
@@ -354,6 +194,12 @@ export const scheduleRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
one: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Get a scheduled job",
|
||||
description: "Returns the details of a specific scheduled job by its ID.",
|
||||
},
|
||||
})
|
||||
.input(z.object({ scheduleId: z.string() }))
|
||||
.query(async ({ input, ctx }) => {
|
||||
const schedule = await findScheduleById(input.scheduleId);
|
||||
@@ -362,36 +208,17 @@ export const scheduleRouter = createTRPCRouter({
|
||||
await checkServicePermissionAndAccess(ctx, serviceId, {
|
||||
schedule: ["read"],
|
||||
});
|
||||
} else {
|
||||
await checkPermission(ctx, { schedule: ["read"] });
|
||||
|
||||
if (schedule.scheduleType === "server" && schedule.serverId) {
|
||||
const targetServer = await findServerById(schedule.serverId);
|
||||
if (
|
||||
targetServer.organizationId !== ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You don't have access to this schedule.",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
schedule.scheduleType === "dokploy-server" &&
|
||||
schedule.userId &&
|
||||
schedule.userId !== ctx.user.id
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You don't have access to this schedule.",
|
||||
});
|
||||
}
|
||||
}
|
||||
return schedule;
|
||||
}),
|
||||
|
||||
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);
|
||||
@@ -400,56 +227,6 @@ export const scheduleRouter = createTRPCRouter({
|
||||
await checkServicePermissionAndAccess(ctx, serviceId, {
|
||||
schedule: ["create"],
|
||||
});
|
||||
} else {
|
||||
if (scheduleItem.scheduleType === "dokploy-server" && IS_CLOUD) {
|
||||
throw new TRPCError({
|
||||
code: "FORBIDDEN",
|
||||
message:
|
||||
"Host-level schedules are not available in the cloud version.",
|
||||
});
|
||||
}
|
||||
|
||||
await checkPermission(ctx, { schedule: ["create"] });
|
||||
|
||||
if (
|
||||
scheduleItem.scheduleType === "server" ||
|
||||
scheduleItem.scheduleType === "dokploy-server"
|
||||
) {
|
||||
const member = await findMemberByUserId(
|
||||
ctx.user.id,
|
||||
ctx.session.activeOrganizationId,
|
||||
);
|
||||
if (member.role !== "owner" && member.role !== "admin") {
|
||||
throw new TRPCError({
|
||||
code: "FORBIDDEN",
|
||||
message:
|
||||
"Only owners and admins can manage server-level schedules.",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (scheduleItem.scheduleType === "server" && scheduleItem.serverId) {
|
||||
const targetServer = await findServerById(scheduleItem.serverId);
|
||||
if (
|
||||
targetServer.organizationId !== ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You don't have access to this server.",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
scheduleItem.scheduleType === "dokploy-server" &&
|
||||
scheduleItem.userId &&
|
||||
scheduleItem.userId !== ctx.user.id
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You can only manage your own host-level schedules.",
|
||||
});
|
||||
}
|
||||
}
|
||||
try {
|
||||
await runCommand(input.scheduleId);
|
||||
|
||||
@@ -15,6 +15,12 @@ import {
|
||||
|
||||
export const securityRouter = createTRPCRouter({
|
||||
create: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Create a security entry",
|
||||
description: "Creates a new HTTP basic auth security entry for an application with the provided username and password. Logs an audit entry.",
|
||||
},
|
||||
})
|
||||
.input(apiCreateSecurity)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.applicationId, {
|
||||
@@ -31,6 +37,12 @@ export const securityRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
one: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Get a security entry",
|
||||
description: "Returns a single HTTP basic auth security entry by its ID.",
|
||||
},
|
||||
})
|
||||
.input(apiFindOneSecurity)
|
||||
.query(async ({ input, ctx }) => {
|
||||
const security = await findSecurityById(input.securityId);
|
||||
@@ -41,6 +53,12 @@ export const securityRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
delete: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Delete a security entry",
|
||||
description: "Deletes an HTTP basic auth security entry by its ID and logs an audit entry.",
|
||||
},
|
||||
})
|
||||
.input(apiFindOneSecurity)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const security = await findSecurityById(input.securityId);
|
||||
@@ -57,6 +75,12 @@ export const securityRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
update: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Update a security entry",
|
||||
description: "Updates an existing HTTP basic auth security entry's configuration and logs an audit entry.",
|
||||
},
|
||||
})
|
||||
.input(apiUpdateSecurity)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const security = await findSecurityById(input.securityId);
|
||||
|
||||
@@ -48,6 +48,12 @@ import {
|
||||
|
||||
export const serverRouter = createTRPCRouter({
|
||||
create: withPermission("server", "create")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Create a server",
|
||||
description: "Creates a new server in the organization. In cloud mode, enforces the user's server quantity limit. Returns the newly created server.",
|
||||
},
|
||||
})
|
||||
.input(apiCreateServer)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
try {
|
||||
@@ -80,6 +86,12 @@ export const serverRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
one: withPermission("server", "read")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Get a server",
|
||||
description: "Retrieves a single server by its ID. Validates that the user has access to the server within their organization.",
|
||||
},
|
||||
})
|
||||
.input(apiFindOneServer)
|
||||
.query(async ({ input, ctx }) => {
|
||||
const server = await findServerById(input.serverId);
|
||||
@@ -101,13 +113,26 @@ export const serverRouter = createTRPCRouter({
|
||||
return server;
|
||||
}),
|
||||
getDefaultCommand: withPermission("server", "read")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Get default server command",
|
||||
description: "Returns the default setup command for a server. The command varies depending on whether the server is a build server or a deploy server.",
|
||||
},
|
||||
})
|
||||
.input(apiFindOneServer)
|
||||
.query(async ({ input }) => {
|
||||
const server = await findServerById(input.serverId);
|
||||
const isBuildServer = server.serverType === "build";
|
||||
return defaultCommand(isBuildServer);
|
||||
}),
|
||||
all: withPermission("server", "read").query(async ({ ctx }) => {
|
||||
all: withPermission("server", "read")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "List all servers",
|
||||
description: "Returns all servers in the current organization along with a count of associated services (applications, compose, databases). Results are filtered by the user's accessible server permissions.",
|
||||
},
|
||||
})
|
||||
.query(async ({ ctx }) => {
|
||||
const accessibleIds = await getAccessibleServerIds(ctx.session);
|
||||
|
||||
const result = await db
|
||||
@@ -130,6 +155,12 @@ export const serverRouter = createTRPCRouter({
|
||||
return result.filter((s) => accessibleIds.has(s.serverId));
|
||||
}),
|
||||
allForPermissions: withPermission("member", "update")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "List all servers for permissions",
|
||||
description: "Returns a minimal list of servers (ID, name, IP, type) used for configuring member permissions. Requires a valid enterprise license.",
|
||||
},
|
||||
})
|
||||
.use(async ({ ctx, next }) => {
|
||||
const licensed = await hasValidLicense(ctx.session.activeOrganizationId);
|
||||
if (!licensed) {
|
||||
@@ -152,7 +183,14 @@ export const serverRouter = createTRPCRouter({
|
||||
where: eq(server.organizationId, ctx.session.activeOrganizationId),
|
||||
});
|
||||
}),
|
||||
count: protectedProcedure.query(async ({ ctx }) => {
|
||||
count: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Get server count",
|
||||
description: "Returns the total number of servers across all organizations owned by the current user.",
|
||||
},
|
||||
})
|
||||
.query(async ({ ctx }) => {
|
||||
const organizations = await db.query.organization.findMany({
|
||||
where: eq(organization.ownerId, ctx.user.id),
|
||||
with: {
|
||||
@@ -164,7 +202,14 @@ export const serverRouter = createTRPCRouter({
|
||||
|
||||
return servers.length ?? 0;
|
||||
}),
|
||||
withSSHKey: withPermission("server", "read").query(async ({ ctx }) => {
|
||||
withSSHKey: withPermission("server", "read")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "List servers with SSH keys",
|
||||
description: "Returns all deploy-type servers that have an SSH key configured. In cloud mode, only active servers are included. Results are filtered by the user's accessible server permissions.",
|
||||
},
|
||||
})
|
||||
.query(async ({ ctx }) => {
|
||||
const accessibleIds = await getAccessibleServerIds(ctx.session);
|
||||
|
||||
const result = await db.query.server.findMany({
|
||||
@@ -184,7 +229,14 @@ export const serverRouter = createTRPCRouter({
|
||||
});
|
||||
return result.filter((s) => accessibleIds.has(s.serverId));
|
||||
}),
|
||||
buildServers: withPermission("server", "read").query(async ({ ctx }) => {
|
||||
buildServers: withPermission("server", "read")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "List build servers",
|
||||
description: "Returns all build-type servers that have an SSH key configured. In cloud mode, only active servers are included. Results are filtered by the user's accessible server permissions.",
|
||||
},
|
||||
})
|
||||
.query(async ({ ctx }) => {
|
||||
const accessibleIds = await getAccessibleServerIds(ctx.session);
|
||||
|
||||
const result = await db.query.server.findMany({
|
||||
@@ -205,6 +257,12 @@ export const serverRouter = createTRPCRouter({
|
||||
return result.filter((s) => accessibleIds.has(s.serverId));
|
||||
}),
|
||||
setup: withPermission("server", "create")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Setup a server",
|
||||
description: "Runs the initial setup process on a remote server, installing required dependencies and configuring Docker. An audit log entry is created.",
|
||||
},
|
||||
})
|
||||
.input(apiFindOneServer)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
@@ -256,6 +314,12 @@ export const serverRouter = createTRPCRouter({
|
||||
}
|
||||
}),
|
||||
validate: withPermission("server", "read")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Validate server configuration",
|
||||
description: "Checks the server for required tools and configuration including Docker, Rclone, Nixpacks, Buildpacks, Railpack, Swarm mode, network setup, and privilege mode.",
|
||||
},
|
||||
})
|
||||
.input(apiFindOneServer)
|
||||
.query(async ({ input, ctx }) => {
|
||||
try {
|
||||
@@ -304,6 +368,12 @@ export const serverRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
security: withPermission("server", "read")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Get server security audit",
|
||||
description: "Performs a security audit on the server, checking UFW firewall, SSH configuration, non-root user setup, unattended upgrades, and Fail2Ban status.",
|
||||
},
|
||||
})
|
||||
.input(apiFindOneServer)
|
||||
.query(async ({ input, ctx }) => {
|
||||
try {
|
||||
@@ -354,6 +424,12 @@ export const serverRouter = createTRPCRouter({
|
||||
}
|
||||
}),
|
||||
setupMonitoring: withPermission("server", "create")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Setup server monitoring",
|
||||
description: "Configures and deploys the monitoring agent on a server with the specified metrics configuration including refresh rates, retention, thresholds, and container service filters.",
|
||||
},
|
||||
})
|
||||
.input(apiUpdateServerMonitoring)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
@@ -402,6 +478,12 @@ export const serverRouter = createTRPCRouter({
|
||||
}
|
||||
}),
|
||||
remove: withPermission("server", "delete")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Remove a server",
|
||||
description: "Deletes a server and removes all associated deployments. Fails if the server has active services. In cloud mode, updates the user's server quantity allocation.",
|
||||
},
|
||||
})
|
||||
.input(apiRemoveServer)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
@@ -435,6 +517,12 @@ export const serverRouter = createTRPCRouter({
|
||||
}
|
||||
}),
|
||||
update: withPermission("server", "create")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Update a server",
|
||||
description: "Updates the configuration of an existing server. Fails if the server is inactive. An audit log entry is created for the update.",
|
||||
},
|
||||
})
|
||||
.input(apiUpdateServer)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
@@ -467,14 +555,28 @@ export const serverRouter = createTRPCRouter({
|
||||
throw error;
|
||||
}
|
||||
}),
|
||||
publicIp: protectedProcedure.query(async () => {
|
||||
publicIp: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Get public IP address",
|
||||
description: "Returns the public IP address of the local server. Returns an empty string in cloud mode.",
|
||||
},
|
||||
})
|
||||
.query(async () => {
|
||||
if (IS_CLOUD) {
|
||||
return "";
|
||||
}
|
||||
const ip = await getPublicIpWithFallback();
|
||||
return ip;
|
||||
}),
|
||||
getServerTime: protectedProcedure.query(() => {
|
||||
getServerTime: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Get server time",
|
||||
description: "Returns the current server time and timezone. Returns null in cloud mode.",
|
||||
},
|
||||
})
|
||||
.query(() => {
|
||||
if (IS_CLOUD) {
|
||||
return null;
|
||||
}
|
||||
@@ -484,6 +586,12 @@ export const serverRouter = createTRPCRouter({
|
||||
};
|
||||
}),
|
||||
getServerMetrics: withPermission("monitoring", "read")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Get server metrics",
|
||||
description: "Fetches monitoring metrics (CPU, memory, disk, network) from the server's monitoring agent endpoint. Requires the monitoring service to be configured and running.",
|
||||
},
|
||||
})
|
||||
.input(
|
||||
z.object({
|
||||
url: z.string(),
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -25,6 +25,12 @@ import {
|
||||
|
||||
export const sshRouter = createTRPCRouter({
|
||||
create: withPermission("sshKeys", "create")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Create SSH key",
|
||||
description: "Stores a new SSH key for the current organization and logs an audit event.",
|
||||
},
|
||||
})
|
||||
.input(apiCreateSshKey)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
@@ -46,6 +52,12 @@ export const sshRouter = createTRPCRouter({
|
||||
}
|
||||
}),
|
||||
remove: withPermission("sshKeys", "delete")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Delete SSH key",
|
||||
description: "Removes an SSH key by ID. Verifies organization ownership and logs an audit event before deletion.",
|
||||
},
|
||||
})
|
||||
.input(apiRemoveSshKey)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
@@ -69,6 +81,12 @@ export const sshRouter = createTRPCRouter({
|
||||
}
|
||||
}),
|
||||
one: withPermission("sshKeys", "read")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Get SSH key",
|
||||
description: "Returns a single SSH key by ID. Verifies the caller belongs to the same organization.",
|
||||
},
|
||||
})
|
||||
.input(apiFindOneSshKey)
|
||||
.query(async ({ input, ctx }) => {
|
||||
const sshKey = await findSSHKeyById(input.sshKeyId);
|
||||
@@ -81,13 +99,27 @@ export const sshRouter = createTRPCRouter({
|
||||
}
|
||||
return sshKey;
|
||||
}),
|
||||
all: withPermission("sshKeys", "read").query(async ({ ctx }) => {
|
||||
all: withPermission("sshKeys", "read")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "List all SSH keys",
|
||||
description: "Returns all SSH keys for the current organization, ordered by creation date descending.",
|
||||
},
|
||||
})
|
||||
.query(async ({ ctx }) => {
|
||||
return await db.query.sshKeys.findMany({
|
||||
where: eq(sshKeys.organizationId, ctx.session.activeOrganizationId),
|
||||
orderBy: desc(sshKeys.createdAt),
|
||||
});
|
||||
}),
|
||||
allForApps: protectedProcedure.query(async ({ ctx }) => {
|
||||
allForApps: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "List SSH keys for app selection",
|
||||
description: "Returns a lightweight list of SSH keys (ID and name only) for the current organization, suitable for dropdown selectors in application forms.",
|
||||
},
|
||||
})
|
||||
.query(async ({ ctx }) => {
|
||||
return await db.query.sshKeys.findMany({
|
||||
columns: {
|
||||
sshKeyId: true,
|
||||
@@ -98,11 +130,23 @@ export const sshRouter = createTRPCRouter({
|
||||
});
|
||||
}),
|
||||
generate: withPermission("sshKeys", "read")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Generate SSH key pair",
|
||||
description: "Generates a new SSH key pair of the specified type (RSA, ED25519, etc.) and returns both public and private keys.",
|
||||
},
|
||||
})
|
||||
.input(apiGenerateSSHKey)
|
||||
.mutation(async ({ input }) => {
|
||||
return await generateSSHKey(input.type);
|
||||
}),
|
||||
update: withPermission("sshKeys", "create")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Update SSH key",
|
||||
description: "Updates an existing SSH key. Verifies organization ownership before applying changes and logs an audit event.",
|
||||
},
|
||||
})
|
||||
.input(apiUpdateSshKey)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
|
||||
@@ -30,7 +30,14 @@ import {
|
||||
|
||||
export const stripeRouter = createTRPCRouter({
|
||||
/** Returns the current billing plan for the user's organization. Used to gate features like chat (Startup only). */
|
||||
getCurrentPlan: protectedProcedure.query(async ({ ctx }) => {
|
||||
getCurrentPlan: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Get current billing plan",
|
||||
description: "Returns the active Stripe billing plan (hobby, startup, or legacy) for the caller's organization owner. Returns null if not on cloud or no subscription exists.",
|
||||
},
|
||||
})
|
||||
.query(async ({ ctx }) => {
|
||||
if (!IS_CLOUD) return null;
|
||||
const owner = await findUserById(ctx.user.ownerId);
|
||||
if (!owner?.stripeCustomerId) return null;
|
||||
@@ -71,7 +78,14 @@ export const stripeRouter = createTRPCRouter({
|
||||
return null;
|
||||
}),
|
||||
|
||||
getProducts: adminProcedure.query(async ({ ctx }) => {
|
||||
getProducts: adminProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "List Stripe products and subscriptions",
|
||||
description: "Returns available Stripe products, the user's active subscriptions, current plan tier, billing interval, and price amount.",
|
||||
},
|
||||
})
|
||||
.query(async ({ ctx }) => {
|
||||
const user = await findUserById(ctx.user.ownerId);
|
||||
const stripeCustomerId = user.stripeCustomerId;
|
||||
|
||||
@@ -162,6 +176,12 @@ export const stripeRouter = createTRPCRouter({
|
||||
};
|
||||
}),
|
||||
createCheckoutSession: adminProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Create Stripe checkout session",
|
||||
description: "Creates a Stripe checkout session for subscribing to a billing plan. Handles customer creation or reuse and returns the session ID for redirect.",
|
||||
},
|
||||
})
|
||||
.input(
|
||||
z
|
||||
.object({
|
||||
@@ -222,7 +242,14 @@ export const stripeRouter = createTRPCRouter({
|
||||
|
||||
return { sessionId: session.id };
|
||||
}),
|
||||
createCustomerPortalSession: adminProcedure.mutation(async ({ ctx }) => {
|
||||
createCustomerPortalSession: adminProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Create Stripe customer portal session",
|
||||
description: "Creates a Stripe billing portal session URL so the user can manage their subscription, payment methods, and invoices.",
|
||||
},
|
||||
})
|
||||
.mutation(async ({ ctx }) => {
|
||||
// Use the organization's owner account for billing portal
|
||||
const owner = await findUserById(ctx.user.ownerId);
|
||||
|
||||
@@ -253,6 +280,12 @@ export const stripeRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
upgradeSubscription: adminProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Upgrade subscription",
|
||||
description: "Upgrades or changes the current Stripe subscription to a different tier or server quantity. Applies prorated charges for the billing period change.",
|
||||
},
|
||||
})
|
||||
.input(
|
||||
z
|
||||
.object({
|
||||
@@ -324,7 +357,14 @@ export const stripeRouter = createTRPCRouter({
|
||||
return { ok: true };
|
||||
}),
|
||||
|
||||
canCreateMoreServers: withPermission("server", "create").query(
|
||||
canCreateMoreServers: withPermission("server", "create")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Check server creation quota",
|
||||
description: "Returns whether the organization can create more servers based on their subscription's server quantity limit. Always returns true for self-hosted instances.",
|
||||
},
|
||||
})
|
||||
.query(
|
||||
async ({ ctx }) => {
|
||||
const user = await findUserById(ctx.user.ownerId);
|
||||
const servers = await findServersByUserId(user.id);
|
||||
@@ -338,6 +378,12 @@ export const stripeRouter = createTRPCRouter({
|
||||
),
|
||||
|
||||
updateInvoiceNotifications: adminProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Update invoice notification preference",
|
||||
description: "Enables or disables email notifications for invoice events. Only available on Dokploy Cloud.",
|
||||
},
|
||||
})
|
||||
.input(z.object({ enabled: z.boolean() }))
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
if (!IS_CLOUD) {
|
||||
@@ -353,7 +399,14 @@ export const stripeRouter = createTRPCRouter({
|
||||
return { ok: true };
|
||||
}),
|
||||
|
||||
getInvoices: adminProcedure.query(async ({ ctx }) => {
|
||||
getInvoices: adminProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "List invoices",
|
||||
description: "Returns up to 100 Stripe invoices for the organization owner, including status, amounts, and download links.",
|
||||
},
|
||||
})
|
||||
.query(async ({ ctx }) => {
|
||||
const user = await findUserById(ctx.user.ownerId);
|
||||
const stripeCustomerId = user.stripeCustomerId;
|
||||
|
||||
|
||||
@@ -13,6 +13,12 @@ import { containerIdRegex } from "./docker";
|
||||
|
||||
export const swarmRouter = createTRPCRouter({
|
||||
getNodes: withPermission("server", "read")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Get Swarm nodes",
|
||||
description: "Retrieves all nodes in the Docker Swarm. Optionally targets a remote server by ID.",
|
||||
},
|
||||
})
|
||||
.input(
|
||||
z.object({
|
||||
serverId: z.string().optional(),
|
||||
@@ -22,11 +28,23 @@ export const swarmRouter = createTRPCRouter({
|
||||
return await getSwarmNodes(input.serverId);
|
||||
}),
|
||||
getNodeInfo: withPermission("server", "read")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Get Swarm node info",
|
||||
description: "Retrieves detailed information about a specific Docker Swarm node by its node ID. Optionally targets a remote server.",
|
||||
},
|
||||
})
|
||||
.input(z.object({ nodeId: z.string(), serverId: z.string().optional() }))
|
||||
.query(async ({ input }) => {
|
||||
return await getNodeInfo(input.nodeId, input.serverId);
|
||||
}),
|
||||
getNodeApps: withPermission("server", "read")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Get Swarm node applications",
|
||||
description: "Retrieves all applications (services) running across Docker Swarm nodes. Optionally targets a remote server.",
|
||||
},
|
||||
})
|
||||
.input(
|
||||
z.object({
|
||||
serverId: z.string().optional(),
|
||||
@@ -58,6 +76,12 @@ export const swarmRouter = createTRPCRouter({
|
||||
return await getApplicationInfo(input.appName, input.serverId);
|
||||
}),
|
||||
getContainerStats: withPermission("server", "read")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Get container stats",
|
||||
description: "Retrieves resource usage statistics for all containers. Optionally targets a remote server and validates organization access.",
|
||||
},
|
||||
})
|
||||
.input(
|
||||
z.object({
|
||||
serverId: z.string().optional(),
|
||||
|
||||
@@ -16,6 +16,12 @@ import { createTRPCRouter, protectedProcedure, withPermission } from "../trpc";
|
||||
|
||||
export const tagRouter = createTRPCRouter({
|
||||
create: withPermission("tag", "create")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Create tag",
|
||||
description: "Creates a new tag with a name and color for the current organization. Tag names must be unique within the organization.",
|
||||
},
|
||||
})
|
||||
.input(apiCreateTag)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
@@ -47,7 +53,14 @@ export const tagRouter = createTRPCRouter({
|
||||
}
|
||||
}),
|
||||
|
||||
all: protectedProcedure.query(async ({ ctx }) => {
|
||||
all: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "List all tags",
|
||||
description: "Returns all tags for the current organization, ordered alphabetically by name.",
|
||||
},
|
||||
})
|
||||
.query(async ({ ctx }) => {
|
||||
try {
|
||||
const organizationTags = await db.query.tags.findMany({
|
||||
where: eq(tags.organizationId, ctx.session.activeOrganizationId),
|
||||
@@ -64,7 +77,15 @@ export const tagRouter = createTRPCRouter({
|
||||
}
|
||||
}),
|
||||
|
||||
one: protectedProcedure.input(apiFindOneTag).query(async ({ input, ctx }) => {
|
||||
one: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Get tag",
|
||||
description: "Returns a single tag by ID. Only returns tags belonging to the caller's organization.",
|
||||
},
|
||||
})
|
||||
.input(apiFindOneTag)
|
||||
.query(async ({ input, ctx }) => {
|
||||
try {
|
||||
const tag = await db.query.tags.findFirst({
|
||||
where: and(
|
||||
@@ -94,6 +115,12 @@ export const tagRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
update: withPermission("tag", "update")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Update tag",
|
||||
description: "Updates an existing tag's name and/or color. Verifies the tag belongs to the caller's organization. Tag names must remain unique.",
|
||||
},
|
||||
})
|
||||
.input(apiUpdateTag)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
@@ -144,6 +171,12 @@ export const tagRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
remove: withPermission("tag", "delete")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Delete tag",
|
||||
description: "Deletes a tag by ID. Cascade-deletes all project-tag associations. Verifies the tag belongs to the caller's organization.",
|
||||
},
|
||||
})
|
||||
.input(apiRemoveTag)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
@@ -179,6 +212,12 @@ export const tagRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
assignToProject: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Assign tag to project",
|
||||
description: "Associates a tag with a project. Verifies that both the tag and project belong to the caller's organization and that the caller has project access.",
|
||||
},
|
||||
})
|
||||
.input(
|
||||
z.object({
|
||||
projectId: z.string().min(1),
|
||||
@@ -267,6 +306,12 @@ export const tagRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
removeFromProject: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Remove tag from project",
|
||||
description: "Removes a tag-project association. Verifies that both the tag and project belong to the caller's organization and that the caller has project access.",
|
||||
},
|
||||
})
|
||||
.input(
|
||||
z.object({
|
||||
projectId: z.string().min(1),
|
||||
@@ -347,6 +392,12 @@ export const tagRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
bulkAssign: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Bulk assign tags to project",
|
||||
description: "Replaces all tag associations for a project with the provided list of tag IDs. Removes existing associations first, then inserts the new set.",
|
||||
},
|
||||
})
|
||||
.input(
|
||||
z.object({
|
||||
projectId: z.string().min(1),
|
||||
|
||||
@@ -9,12 +9,12 @@ import {
|
||||
getWebServerSettings,
|
||||
IS_CLOUD,
|
||||
removeUserById,
|
||||
renderInvitationEmail,
|
||||
sendEmailNotification,
|
||||
sendResendNotification,
|
||||
updateUser,
|
||||
} from "@dokploy/server";
|
||||
import { db } from "@dokploy/server/db";
|
||||
import { hasValidLicense } from "@dokploy/server/services/proprietary/license-key";
|
||||
import {
|
||||
account,
|
||||
apiAssignPermissions,
|
||||
@@ -29,7 +29,6 @@ import {
|
||||
hasPermission,
|
||||
resolvePermissions,
|
||||
} from "@dokploy/server/services/permission";
|
||||
import { hasValidLicense } from "@dokploy/server/services/proprietary/license-key";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import * as bcrypt from "bcrypt";
|
||||
import { and, asc, eq, gt } from "drizzle-orm";
|
||||
@@ -61,7 +60,14 @@ const apiCreateApiKey = z.object({
|
||||
});
|
||||
|
||||
export const userRouter = createTRPCRouter({
|
||||
all: withPermission("member", "read").query(async ({ ctx }) => {
|
||||
all: withPermission("member", "read")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "List all organization members",
|
||||
description: "Retrieve all members of the current active organization, including their associated user data, ordered by creation date.",
|
||||
},
|
||||
})
|
||||
.query(async ({ ctx }) => {
|
||||
return await db.query.member.findMany({
|
||||
where: eq(member.organizationId, ctx.session.activeOrganizationId),
|
||||
with: {
|
||||
@@ -71,6 +77,12 @@ export const userRouter = createTRPCRouter({
|
||||
});
|
||||
}),
|
||||
one: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Get a user by ID",
|
||||
description: "Retrieve a specific user's membership and profile within the active organization. Users can view their own data; admins and owners can view any member. Requires member.update permission for non-self lookups.",
|
||||
},
|
||||
})
|
||||
.input(
|
||||
z.object({
|
||||
userId: z.string(),
|
||||
@@ -115,7 +127,14 @@ export const userRouter = createTRPCRouter({
|
||||
|
||||
return memberResult;
|
||||
}),
|
||||
session: publicProcedure.query(async ({ ctx }) => {
|
||||
session: publicProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Get current session",
|
||||
description: "Return the current user's ID and active organization ID from the session. Returns null if no valid session exists.",
|
||||
},
|
||||
})
|
||||
.query(async ({ ctx }) => {
|
||||
if (!ctx.user || !ctx.session || !ctx.session.activeOrganizationId) {
|
||||
return null;
|
||||
}
|
||||
@@ -128,7 +147,14 @@ export const userRouter = createTRPCRouter({
|
||||
},
|
||||
};
|
||||
}),
|
||||
get: protectedProcedure.query(async ({ ctx }) => {
|
||||
get: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Get current user profile",
|
||||
description: "Retrieve the current authenticated user's membership record including user profile and API keys for the active organization.",
|
||||
},
|
||||
})
|
||||
.query(async ({ ctx }) => {
|
||||
const memberResult = await db.query.member.findFirst({
|
||||
where: and(
|
||||
eq(member.userId, ctx.user.id),
|
||||
@@ -145,10 +171,24 @@ export const userRouter = createTRPCRouter({
|
||||
|
||||
return memberResult;
|
||||
}),
|
||||
getPermissions: protectedProcedure.query(async ({ ctx }) => {
|
||||
getPermissions: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Get resolved permissions",
|
||||
description: "Return the fully resolved permissions for the current user in the active organization, combining role-based and custom permissions.",
|
||||
},
|
||||
})
|
||||
.query(async ({ ctx }) => {
|
||||
return resolvePermissions(ctx);
|
||||
}),
|
||||
haveRootAccess: protectedProcedure.query(async ({ ctx }) => {
|
||||
haveRootAccess: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Check root access",
|
||||
description: "Check whether the current user has root admin access. Only returns true in cloud mode for the designated admin user or impersonating sessions.",
|
||||
},
|
||||
})
|
||||
.query(async ({ ctx }) => {
|
||||
if (!IS_CLOUD) {
|
||||
return false;
|
||||
}
|
||||
@@ -160,7 +200,14 @@ export const userRouter = createTRPCRouter({
|
||||
}
|
||||
return false;
|
||||
}),
|
||||
getBackups: adminProcedure.query(async ({ ctx }) => {
|
||||
getBackups: adminProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Get user backups",
|
||||
description: "Retrieve the current admin user's backup configurations including destinations, deployments, and API keys.",
|
||||
},
|
||||
})
|
||||
.query(async ({ ctx }) => {
|
||||
const memberResult = await db.query.member.findFirst({
|
||||
where: and(
|
||||
eq(member.userId, ctx.user.id),
|
||||
@@ -183,8 +230,14 @@ export const userRouter = createTRPCRouter({
|
||||
|
||||
return memberResult?.user;
|
||||
}),
|
||||
getServerMetrics: withPermission("monitoring", "read").query(
|
||||
async ({ ctx }) => {
|
||||
getServerMetrics: withPermission("monitoring", "read")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Get server metrics user",
|
||||
description: "Retrieve the user record associated with server metrics access for the current organization membership.",
|
||||
},
|
||||
})
|
||||
.query(async ({ ctx }) => {
|
||||
const memberResult = await db.query.member.findFirst({
|
||||
where: and(
|
||||
eq(member.userId, ctx.user.id),
|
||||
@@ -199,6 +252,12 @@ export const userRouter = createTRPCRouter({
|
||||
},
|
||||
),
|
||||
update: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Update current user",
|
||||
description: "Update the current user's profile. If changing the password, the current password must be provided and verified. Logs an audit event on success.",
|
||||
},
|
||||
})
|
||||
.input(apiUpdateUser)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
if (input.password || input.currentPassword) {
|
||||
@@ -249,12 +308,24 @@ export const userRouter = createTRPCRouter({
|
||||
}
|
||||
}),
|
||||
getUserByToken: publicProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Get user by token",
|
||||
description: "Look up a user by their authentication token. This is a public endpoint that does not require an active session.",
|
||||
},
|
||||
})
|
||||
.input(apiFindOneToken)
|
||||
.query(async ({ input }) => {
|
||||
return await getUserByToken(input.token);
|
||||
}),
|
||||
getMetricsToken: withPermission("monitoring", "read").query(
|
||||
async ({ ctx }) => {
|
||||
getMetricsToken: withPermission("monitoring", "read")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Get metrics token and configuration",
|
||||
description: "Retrieve the server IP, paid features flag, and monitoring configuration needed for metrics collection.",
|
||||
},
|
||||
})
|
||||
.query(async ({ ctx }) => {
|
||||
const user = await findUserById(ctx.user.ownerId);
|
||||
const settings = await getWebServerSettings();
|
||||
return {
|
||||
@@ -265,6 +336,12 @@ export const userRouter = createTRPCRouter({
|
||||
},
|
||||
),
|
||||
remove: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Remove a user",
|
||||
description: "Delete a user from the organization. Only owners and admins can remove users; owners cannot be removed, and admins cannot remove themselves or other admins. Disabled on cloud.",
|
||||
},
|
||||
})
|
||||
.input(
|
||||
z.object({
|
||||
userId: z.string(),
|
||||
@@ -334,6 +411,12 @@ export const userRouter = createTRPCRouter({
|
||||
return result;
|
||||
}),
|
||||
assignPermissions: withPermission("member", "update")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Assign member permissions",
|
||||
description: "Update permissions for a specific member in the organization. Only the organization owner can assign permissions. Git provider and server access restrictions require a valid license.",
|
||||
},
|
||||
})
|
||||
.input(apiAssignPermissions)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
@@ -384,7 +467,14 @@ export const userRouter = createTRPCRouter({
|
||||
throw error;
|
||||
}
|
||||
}),
|
||||
getInvitations: protectedProcedure.query(async ({ ctx }) => {
|
||||
getInvitations: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Get pending invitations for current user",
|
||||
description: "Retrieve all pending organization invitations for the current user's email that have not yet expired.",
|
||||
},
|
||||
})
|
||||
.query(async ({ ctx }) => {
|
||||
return await db.query.invitation.findMany({
|
||||
where: and(
|
||||
eq(invitation.email, ctx.user.email),
|
||||
@@ -398,6 +488,12 @@ export const userRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
getContainerMetrics: withPermission("monitoring", "read")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Get container metrics",
|
||||
description: "Fetch monitoring metrics for a specific container by querying the metrics endpoint. Requires an application name, metrics URL, and authentication token.",
|
||||
},
|
||||
})
|
||||
.input(
|
||||
z.object({
|
||||
url: z.string(),
|
||||
@@ -456,11 +552,24 @@ export const userRouter = createTRPCRouter({
|
||||
}
|
||||
}),
|
||||
|
||||
generateToken: protectedProcedure.mutation(async () => {
|
||||
generateToken: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Generate authentication token",
|
||||
description: "Generate a new authentication token for the current user.",
|
||||
},
|
||||
})
|
||||
.mutation(async () => {
|
||||
return "token";
|
||||
}),
|
||||
|
||||
deleteApiKey: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Delete an API key",
|
||||
description: "Delete an API key by ID. Only the owner of the API key can delete it. Logs an audit event on success.",
|
||||
},
|
||||
})
|
||||
.input(
|
||||
z.object({
|
||||
apiKeyId: z.string(),
|
||||
@@ -500,6 +609,12 @@ export const userRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
createApiKey: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Create an API key",
|
||||
description: "Create a new API key for the current user, scoped to a specific organization. Supports optional rate limiting and request limiting configuration.",
|
||||
},
|
||||
})
|
||||
.input(apiCreateApiKey)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
// Verify user is a member of the organization specified in metadata
|
||||
@@ -530,6 +645,12 @@ export const userRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
checkUserOrganizations: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Check user organization count",
|
||||
description: "Return the number of organizations a user belongs to. Users can check their own count; admins and owners can check counts for members in the active organization.",
|
||||
},
|
||||
})
|
||||
.input(
|
||||
z.object({
|
||||
userId: z.string(),
|
||||
@@ -571,6 +692,12 @@ export const userRouter = createTRPCRouter({
|
||||
return organizations.length;
|
||||
}),
|
||||
createUserWithCredentials: withPermission("member", "create")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Create user with credentials",
|
||||
description: "Create a new user with email and password and add them to the active organization with the specified role. Only available in self-hosted mode.",
|
||||
},
|
||||
})
|
||||
.input(
|
||||
z.object({
|
||||
email: z.string().email(),
|
||||
@@ -602,6 +729,12 @@ export const userRouter = createTRPCRouter({
|
||||
});
|
||||
}),
|
||||
sendInvitation: withPermission("member", "create")
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Send invitation email",
|
||||
description: "Send an invitation email to a pending invitee using a configured email or Resend notification provider. Returns the generated invite link. Disabled on cloud.",
|
||||
},
|
||||
})
|
||||
.input(
|
||||
z.object({
|
||||
invitationId: z.string().min(1),
|
||||
@@ -640,26 +773,27 @@ export const userRouter = createTRPCRouter({
|
||||
);
|
||||
|
||||
try {
|
||||
const toEmail = currentInvitation?.email || "";
|
||||
const orgName = organization?.name || "organization";
|
||||
const subject = `You've been invited to join ${orgName} on Dokploy`;
|
||||
const html = await renderInvitationEmail({
|
||||
email: toEmail,
|
||||
inviteLink,
|
||||
organizationName: orgName,
|
||||
});
|
||||
const htmlContent = `
|
||||
\t\t\t\t<p>You are invited to join ${organization?.name || "organization"} on Dokploy. Click the link to accept the invitation: <a href="${inviteLink}">Accept Invitation</a></p>
|
||||
\t\t\t\t`;
|
||||
|
||||
if (email) {
|
||||
await sendEmailNotification(
|
||||
{ ...email, toAddresses: [toEmail] },
|
||||
subject,
|
||||
html,
|
||||
{
|
||||
...email,
|
||||
toAddresses: [currentInvitation?.email || ""],
|
||||
},
|
||||
"Invitation to join organization",
|
||||
htmlContent,
|
||||
);
|
||||
} else if (resend) {
|
||||
await sendResendNotification(
|
||||
{ ...resend, toAddresses: [toEmail] },
|
||||
subject,
|
||||
html,
|
||||
{
|
||||
...resend,
|
||||
toAddresses: [currentInvitation?.email || ""],
|
||||
},
|
||||
"Invitation to join organization",
|
||||
htmlContent,
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -676,7 +810,14 @@ export const userRouter = createTRPCRouter({
|
||||
return inviteLink;
|
||||
}),
|
||||
|
||||
getBookmarkedTemplates: protectedProcedure.query(async ({ ctx }) => {
|
||||
getBookmarkedTemplates: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Get bookmarked templates",
|
||||
description: "Retrieve the list of template IDs that the current user has bookmarked.",
|
||||
},
|
||||
})
|
||||
.query(async ({ ctx }) => {
|
||||
const result = await db.query.user.findFirst({
|
||||
where: eq(user.id, ctx.user.id),
|
||||
columns: { bookmarkedTemplates: true },
|
||||
@@ -686,6 +827,12 @@ export const userRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
toggleTemplateBookmark: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Toggle template bookmark",
|
||||
description: "Add or remove a template from the current user's bookmarks. Returns whether the template is now bookmarked.",
|
||||
},
|
||||
})
|
||||
.input(
|
||||
z.object({
|
||||
templateId: z.string().min(1),
|
||||
|
||||
@@ -15,9 +15,7 @@ import {
|
||||
updateVolumeBackupSchema,
|
||||
volumeBackups,
|
||||
} from "@dokploy/server/db/schema";
|
||||
import { findDestinationById } from "@dokploy/server/services/destination";
|
||||
import { checkServicePermissionAndAccess } from "@dokploy/server/services/permission";
|
||||
import { findServerById } from "@dokploy/server/services/server";
|
||||
import {
|
||||
execAsyncRemote,
|
||||
execAsyncStream,
|
||||
@@ -32,6 +30,12 @@ import { createTRPCRouter, protectedProcedure, withPermission } from "../trpc";
|
||||
|
||||
export const volumeBackupsRouter = createTRPCRouter({
|
||||
list: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "List volume backups",
|
||||
description: "Returns all volume backup configurations for a given service, including related service details.",
|
||||
},
|
||||
})
|
||||
.input(
|
||||
z.object({
|
||||
id: z.string().min(1),
|
||||
@@ -67,6 +71,12 @@ export const volumeBackupsRouter = createTRPCRouter({
|
||||
});
|
||||
}),
|
||||
create: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Create a volume backup",
|
||||
description: "Creates a new volume backup configuration for a service. If enabled, automatically schedules the backup using the provided cron expression.",
|
||||
},
|
||||
})
|
||||
.input(createVolumeBackupSchema)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const serviceId =
|
||||
@@ -104,6 +114,12 @@ export const volumeBackupsRouter = createTRPCRouter({
|
||||
return newVolumeBackup;
|
||||
}),
|
||||
one: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Get a volume backup",
|
||||
description: "Returns the details of a specific volume backup configuration by its ID.",
|
||||
},
|
||||
})
|
||||
.input(
|
||||
z.object({
|
||||
volumeBackupId: z.string().min(1),
|
||||
@@ -128,6 +144,12 @@ export const volumeBackupsRouter = createTRPCRouter({
|
||||
return vb;
|
||||
}),
|
||||
delete: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Delete a volume backup",
|
||||
description: "Permanently removes a volume backup configuration by its ID.",
|
||||
},
|
||||
})
|
||||
.input(
|
||||
z.object({
|
||||
volumeBackupId: z.string().min(1),
|
||||
@@ -158,6 +180,12 @@ export const volumeBackupsRouter = createTRPCRouter({
|
||||
return result;
|
||||
}),
|
||||
update: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Update a volume backup",
|
||||
description: "Updates an existing volume backup configuration. Reschedules or removes the backup job depending on the enabled state.",
|
||||
},
|
||||
})
|
||||
.input(updateVolumeBackupSchema)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const existingVb = await findVolumeBackupById(input.volumeBackupId);
|
||||
@@ -218,6 +246,12 @@ export const volumeBackupsRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
runManually: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Run a volume backup manually",
|
||||
description: "Immediately executes a volume backup outside of its normal cron schedule.",
|
||||
},
|
||||
})
|
||||
.input(z.object({ volumeBackupId: z.string().min(1) }))
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const vb = await findVolumeBackupById(input.volumeBackupId);
|
||||
@@ -267,23 +301,7 @@ export const volumeBackupsRouter = createTRPCRouter({
|
||||
serverId: z.string().optional(),
|
||||
}),
|
||||
)
|
||||
.subscription(async ({ input, ctx }) => {
|
||||
const destination = await findDestinationById(input.destinationId);
|
||||
if (destination.organizationId !== ctx.session.activeOrganizationId) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You don't have access to this destination.",
|
||||
});
|
||||
}
|
||||
if (input.serverId) {
|
||||
const targetServer = await findServerById(input.serverId);
|
||||
if (targetServer.organizationId !== ctx.session.activeOrganizationId) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You don't have access to this server.",
|
||||
});
|
||||
}
|
||||
}
|
||||
.subscription(async ({ input }) => {
|
||||
return observable<string>((emit) => {
|
||||
const runRestore = async () => {
|
||||
try {
|
||||
|
||||
@@ -13,7 +13,12 @@ import { hasValidLicense } from "@dokploy/server/index";
|
||||
import type { statements } from "@dokploy/server/lib/access-control";
|
||||
import { validateRequest } from "@dokploy/server/lib/auth";
|
||||
import { checkPermission } from "@dokploy/server/services/permission";
|
||||
import type { OpenApiMeta } from "@dokploy/trpc-openapi";
|
||||
import type { OpenApiMeta as _OpenApiMeta } from "@dokploy/trpc-openapi";
|
||||
|
||||
// method and path are auto-generated by @dokploy/trpc-openapi, make them optional
|
||||
type OpenApiMeta = {
|
||||
openapi?: Partial<NonNullable<_OpenApiMeta["openapi"]>>;
|
||||
};
|
||||
import { initTRPC, TRPCError } from "@trpc/server";
|
||||
import type { CreateNextContextOptions } from "@trpc/server/adapters/next";
|
||||
import type { Session, User } from "better-auth";
|
||||
|
||||
@@ -85,11 +85,6 @@ export const setupDockerContainerLogsWebSocketServer = (
|
||||
if (serverId) {
|
||||
const server = await findServerById(serverId);
|
||||
|
||||
if (server.organizationId !== session.activeOrganizationId) {
|
||||
ws.close();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!server.sshKeyId) return;
|
||||
const client = new Client();
|
||||
client
|
||||
|
||||
@@ -61,12 +61,6 @@ export const setupDockerContainerTerminalWebSocketServer = (
|
||||
try {
|
||||
if (serverId) {
|
||||
const server = await findServerById(serverId);
|
||||
|
||||
if (server.organizationId !== session.activeOrganizationId) {
|
||||
ws.close();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!server.sshKeyId)
|
||||
throw new Error("No SSH key available for this server");
|
||||
|
||||
|
||||
@@ -57,11 +57,6 @@ export const setupDeploymentLogsWebSocketServer = (
|
||||
if (serverId) {
|
||||
const server = await findServerById(serverId);
|
||||
|
||||
if (server.organizationId !== session.activeOrganizationId) {
|
||||
ws.close();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!server.sshKeyId) {
|
||||
ws.close();
|
||||
return;
|
||||
|
||||
@@ -154,11 +154,6 @@ export const setupTerminalWebSocketServer = (
|
||||
return;
|
||||
}
|
||||
|
||||
if (server.organizationId !== session.activeOrganizationId) {
|
||||
ws.close();
|
||||
return;
|
||||
}
|
||||
|
||||
const { ipAddress: host, port, username, sshKey, sshKeyId } = server;
|
||||
|
||||
if (!sshKeyId) {
|
||||
|
||||
@@ -104,10 +104,7 @@ 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");
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user