Compare commits

..

63 Commits

Author SHA1 Message Date
Mauricio Siu
fb6b06f064 chore: add push trigger for version sync on tag creation 2026-04-24 22:46:18 -06:00
Mauricio Siu
09824facf8 refactor: improve Badge component formatting in requests table 2026-04-24 22:34:48 -06:00
Mauricio Siu
bd46eaec5c Merge pull request #4303 from Dokploy/fix/requests-status-fallback-downstream
fix: fallback to DownstreamStatus when OriginStatus is 0 in requests table
2026-04-24 22:33:52 -06:00
Mauricio Siu
e9fdc19b96 fix: fallback to DownstreamStatus when OriginStatus is 0 in requests table
Closes #4250
2026-04-24 22:33:24 -06:00
Mauricio Siu
3e81cdac4d Merge pull request #4255 from manalkaff/fix/requests-filter-by-hostname
fix: filter requests by hostname instead of path
2026-04-24 22:01:35 -06:00
Mauricio Siu
e72c51444c Merge pull request #4281 from sajdakabir/fix/4276-sanitize-webhook-error-responses
fix: stop leaking Drizzle SQL queries in webhook error responses (#4276)
2026-04-24 21:59:50 -06:00
Mauricio Siu
940d18ad25 Merge pull request #4302 from Dokploy/fix/send-email-cloud-version
feat: implement invitation email functionality for organization creation
2026-04-24 21:51:53 -06:00
autofix-ci[bot]
c41b69c925 [autofix.ci] apply automated fixes 2026-04-25 03:40:50 +00:00
Mauricio Siu
b610f7aeff feat: implement invitation email functionality for organization creation
- Added `sendInvitationEmail` function to send invitation emails when a new organization is created in the cloud environment.
- Updated email template to enhance the invitation message and included a direct link for users to accept the invitation.
- Refactored email sending logic in the user router to utilize the new invitation email rendering function.
- Improved organization invitation email design for better user experience.
2026-04-24 21:40:08 -06:00
Mauricio Siu
cdd77a04dc Merge pull request #4129 from NomisCZ/fix/ssh2-isdate-nodejs23
fix: drop .zip deployment - isDate is not a function
2026-04-24 12:58:03 -06:00
Mauricio Siu
05f22edfe5 chore: bump version to v0.29.2 in package.json 2026-04-24 12:53:03 -06:00
Mauricio Siu
29480cde90 Merge pull request #4298 from Dokploy/fix/GHSA-f8wj-5c4w-frhg-cross-org-idor
Fix/ghsa f8wj 5c4w frhg cross org idor
2026-04-24 12:49:24 -06:00
Mauricio Siu
232ccc9139 feat: add organization-level authorization checks to WebSocket servers
- Implemented checks in the WebSocket server setups for Docker container logs, terminal, and deployment logs to ensure users can only access resources associated with their active organization.
- Enhanced security by closing WebSocket connections if the organization ID does not match the session's active organization ID.
2026-04-24 12:47:51 -06:00
Mauricio Siu
018e2b153e fix: add cross-org ownership checks to cluster, deployment, backup, and WebSocket endpoints
Prevents owner/admin users of one organization from accessing servers,
destinations, and Docker Swarm join tokens belonging to other organizations
by validating organizationId on all endpoints that accept serverId or
destinationId as direct input.

- cluster: validate serverId org on getNodes, addWorker, addManager, removeWorker
- deployment: validate serverId org on allByServer
- backup: validate destinationId + serverId org on listBackupFiles
- volume-backups: validate destinationId + serverId org on restoreVolumeBackupWithLogs
- wss: validate server org on docker-container-logs, docker-container-terminal,
  listen-deployment, and terminal WebSocket handlers
- auth: fix TypeScript type for API key metadata parsing
2026-04-24 12:44:42 -06:00
sajdakabir
f8c6c8f7cc fix: stop leaking Drizzle SQL queries in webhook error responses (#4276) 2026-04-22 13:06:22 +05:30
Mauricio Siu
d7af82731c Merge pull request #4279 from Dokploy/fix/GHSA-7wmr-57mg-h5q6-schedule-authz
fix(schedule): add authz checks for server and host-level schedules
2026-04-21 21:38:26 -06:00
Mauricio Siu
c3fa638a56 feat: enhance schedule management with permission checks and cloud restrictions
- Added comprehensive permission checks for creating, updating, and deleting schedules based on user roles (owner/admin) and schedule types (server/dokploy-server).
- Implemented restrictions for cloud users to prevent managing host-level schedules and changing schedule types.
- Improved access control for server-level schedules to ensure users can only manage schedules associated with their organization.
2026-04-21 21:36:44 -06:00
Mauricio Siu
98a586478e chore: bump version to v0.29.1 in package.json 2026-04-19 12:07:02 -06:00
Mauricio Siu
13248c8d8a Merge pull request #4257 from colocated/fix/4256-preview-deployment-too-many-args
fix: preview deployments broken on v0.29.0 — postgres 100-arg limit
2026-04-19 12:06:17 -06:00
Jack
54417ca8e7 fix: limit application columns in findPreviewDeploymentById to avoid postgres 100-arg limit
Closes #4256
2026-04-19 11:14:47 +01:00
manalkaff
598fae0e92 fix: filter requests by hostname instead of path
The search filter on the Requests tab was incorrectly filtering by
RequestPath instead of RequestHost, causing "filter by name" to match
URL paths rather than hostnames. Updated the placeholder text to
reflect the correct field being searched.

Fixes #4249
2026-04-19 17:30:42 +08:00
Mauricio Siu
b392e58001 Merge pull request #4244 from Dokploy/feat/dashboard-home
feat: add dashboard home page
2026-04-17 22:40:50 -06:00
Mauricio Siu
d9945c0a4f style: update ShowHome component layout for improved responsiveness
- Adjusted the Card component to have a minimum height of 85vh for better visual consistency.
- Ensured the inner div has a full height to enhance the layout structure.
2026-04-17 22:24:08 -06:00
autofix-ci[bot]
f6e2c033ba [autofix.ci] apply automated fixes 2026-04-18 04:18:44 +00:00
Mauricio Siu
5c787adae1 feat: implement homeStats query for dashboard overview
- Replace individual project and server queries with a consolidated homeStats query to streamline data retrieval for the dashboard.
- Update the ShowHome component to utilize homeStats for displaying project, environment, application, and service counts, along with their status breakdown.
- Enhance data handling for user permissions to ensure accurate statistics based on user access levels.
2026-04-17 22:18:14 -06:00
Mauricio Siu
2ba1df1eaa feat: refine home page and fix libsql in bulk actions
- Home: 4 KPI cards (projects, services, deploys/7d, status list),
  server column with icon in recent deployments, empty state with
  icon, dashboard card frame to match other pages.
- Include libsql in project services count sort.
- Fix bulk actions in environment page: libsql was missing from
  start, stop, move, delete and deploy handlers.
2026-04-17 22:11:04 -06:00
autofix-ci[bot]
e7859395b1 [autofix.ci] apply automated fixes 2026-04-18 03:37:12 +00:00
Mauricio Siu
6f0ed89ce7 feat: add dashboard home page with overview and recent deployments
Adds a new /dashboard/home landing with welcome header, KPI cards
(deploys/24h, build, CPU, memory) and a recent deployments list.

Home is now the post-login landing and the destination for permission
fallback redirects across the app. Projects remains accessible from
the sidebar.
2026-04-17 21:36:37 -06:00
Mauricio Siu
4277a509b2 Merge pull request #4241 from sancho1952007/patch-1
style: Fix typo in custom entrypoint description
2026-04-17 21:06:48 -06:00
Sancho Godinho
f7b576cbf3 Fix typo in custom entrypoint description 2026-04-18 04:23:15 +05:30
Mauricio Siu
425fef6e28 fix: remove 'v' prefix from version in synchronization workflow
Update the version retrieval command in the GitHub Actions workflow to strip the 'v' prefix from the version number in package.json. This change ensures that the version format is consistent for downstream processes.
2026-04-17 14:49:14 -06:00
Mauricio Siu
958372c5f9 chore: update paths in version synchronization workflow for MCP and CLI repositories
Modify the GitHub Actions workflow to clone the MCP and CLI repositories into temporary directories instead of the current directory. This change improves the organization of the workflow and ensures that the latest OpenAPI specification is correctly referenced during the synchronization process.
2026-04-17 14:46:20 -06:00
Mauricio Siu
e7c581476e feat: add workflow dispatch trigger to version synchronization workflow
Enhance the GitHub Actions workflow by adding a workflow_dispatch trigger, allowing manual execution of the version synchronization process. This provides greater flexibility in managing version updates for MCP and CLI repositories.
2026-04-17 14:44:04 -06:00
Mauricio Siu
0cae8330e2 chore: adjust version bump timing in synchronization workflow
Update the GitHub Actions workflow to bump the version in package.json after installing dependencies, ensuring that the version is not overwritten by pnpm install. This change enhances the reliability of version synchronization for both MCP and CLI repositories.
2026-04-17 14:42:14 -06:00
Mauricio Siu
4a271c11e7 Merge pull request #4239 from Dokploy/feat/resend-verification-email-on-signin
feat: resend verification email on sign-in and improve template
2026-04-17 14:02:01 -06:00
Mauricio Siu
fda367b2c5 fix: update logger configuration to disable in production environment
Change the logger's disabled property to be dependent on the NODE_ENV variable, ensuring logging is disabled in production for improved performance and security.
2026-04-17 14:01:46 -06:00
Mauricio Siu
ea1238b1d1 feat: resend verification email on sign-in and improve email template
- Add `sendOnSignIn: true` to emailVerification config so unverified users
  receive a new verification email when they attempt to sign in
- Create styled verification email template matching the invoice email design
- Extract `sendVerificationEmail` helper to keep auth.ts clean
- Show friendly message on login when email is not verified
2026-04-17 13:59:50 -06:00
Mauricio Siu
b060f80932 feat: add no tags message to tag selector component
Enhance the TagSelector component to display a message when no tags are created, prompting users to add tags. This improves user experience by providing clear feedback in the UI.
2026-04-16 12:21:17 -06:00
Mauricio Siu
04b9f56333 chore: enhance version synchronization workflow for MCP and CLI repositories
Update the GitHub Actions workflow to include regeneration of tools from the latest OpenAPI specification and ensure the latest openapi.json is copied to the CLI repository. This improves the consistency and accuracy of the versioning and API documentation across both repositories.
2026-04-15 20:55:37 -06:00
Mauricio Siu
599b97da51 feat: add version synchronization workflow for MCP and CLI repositories
Implement a GitHub Actions workflow to automatically sync the version from the Dokploy repository to the MCP and CLI repositories upon release. This includes cloning the repositories, updating the package.json version, and committing the changes with relevant metadata, ensuring consistent versioning across platforms.
2026-04-15 18:50:54 -06:00
Mauricio Siu
415298fddb feat: add OpenAPI sync to MCP and CLI repositories
Implement workflows to sync the OpenAPI specification to both the MCP and CLI repositories. This includes cloning the repositories, updating the openapi.json file, and committing the changes with relevant metadata. The process ensures that the OpenAPI documentation is consistently updated across multiple platforms.
2026-04-15 18:32:20 -06:00
Mauricio Siu
ddff8b9de7 feat: add container networks view to dashboard
Integrate a new component, ShowContainerNetworks, to display network details for each container in the dashboard. This includes a dialog that shows network information such as IP address, gateway, and MAC address, enhancing the container management capabilities.
2026-04-13 22:04:46 -06:00
Mauricio Siu
90f97912a4 Merge pull request #4221 from Dokploy/feat/container-view-mounts
feat: add view mounts, config, and terminal to container actions
2026-04-13 21:58:20 -06:00
Mauricio Siu
9af745ce67 feat: add view mounts, view config, and terminal to container actions
Add a new "View Mounts" action to the container dropdown that displays
volume and bind mounts in a formatted table (type, source, destination,
mode, read/write). Also add "View Config" and "Terminal" actions to the
compose containers tab which previously only had logs and lifecycle actions.
2026-04-13 21:56:53 -06:00
Mauricio Siu
d99f2cd460 Merge pull request #4216 from nizepart/fix/server-ip-override-on-user-creation
fix: prevent serverIp from being overwritten on every user registration
2026-04-13 20:59:26 -06:00
Mauricio Siu
d234558822 Merge pull request #4219 from Dokploy/feat/service-cards-context-menu
feat: add context menu to service cards
2026-04-13 20:52:22 -06:00
Mauricio Siu
7f25ddca44 fix: add loading feedback and invalidation to context menu actions
Use toast.promise for loading/success/error states and invalidate
environment query after actions complete to update service status.
2026-04-13 20:51:22 -06:00
Mauricio Siu
638b3dd546 feat: add context menu to service cards
Right-click on service cards to quickly Start, Deploy, Stop, or Delete
a service without navigating into it. Uses shadcn/ui ContextMenu
component built on @radix-ui/react-context-menu. Delete action shows
a confirmation dialog. LibSQL services are excluded since they lack
standard mutation endpoints.
2026-04-13 20:48:17 -06:00
Mauricio Siu
1a8fd8396d Merge pull request #4218 from Dokploy/feat/compose-containers-tab
feat: add containers tab to compose services
2026-04-13 20:36:19 -06:00
Mauricio Siu
385850f354 fix: update audit action for container termination
Change the audit action from "kill" to "stop" for the containerKill function to better reflect the operation being performed. This aligns the logging with the intended action and improves clarity in audit records.
2026-04-13 20:36:04 -06:00
Mauricio Siu
a48306a2c6 fix: address PR review feedback
- Use "kill" audit action for killContainer instead of "stop"
- Pass undefined instead of empty string for optional serverId
2026-04-13 20:34:06 -06:00
Mauricio Siu
89737e7b65 refactor: remove duplicate import of ShowComposeContainers component
Eliminate redundant import statement for ShowComposeContainers in the compose service page, streamlining the code and improving readability.
2026-04-13 20:32:11 -06:00
Mauricio Siu
00c708483e fix: use service.read permission for compose container actions
Change restartContainer, startContainer, stopContainer, and killContainer
endpoints to use service.read instead of docker.read so members with
access to the compose can use container lifecycle actions.
2026-04-13 20:31:58 -06:00
autofix-ci[bot]
ddf570a807 [autofix.ci] apply automated fixes 2026-04-14 02:15:37 +00:00
Mauricio Siu
f8eb2ba4ba feat: add containers tab to compose services
Add a Containers tab to the compose service page that lists all
containers with their state, status, and container ID. Each container
has a dropdown menu with lifecycle actions: View Logs, Restart, Start,
Stop, and Kill.

- Add containerStart, containerStop, containerKill functions to docker service
- Add corresponding tRPC procedures with server ownership checks and audit logging
- Update containerRestart to support remote servers via serverId
- Create ShowComposeContainers component with table view and action menu
- Add Containers tab between Deployments and Backups, gated by docker.read permission
2026-04-13 20:11:21 -06:00
Трапезин Андрей Александрович
9f07f8e9e1 fix: prevent serverIp from being overwritten on every user registration 2026-04-13 19:57:31 +03:00
Mauricio Siu
3cefa43a21 Merge pull request #4031 from difagume/style/deployments-remove-max-w-8xl
style(dashboard): remove max-width constraint from deployments card
2026-04-12 14:00:46 -06:00
autofix-ci[bot]
0941ec9f3e [autofix.ci] apply automated fixes 2026-04-12 20:00:08 +00:00
Mauricio Siu
879218a8b1 Merge branch 'canary' into style/deployments-remove-max-w-8xl 2026-04-12 13:59:24 -06:00
Šimon Orság
eafbd0353e fix: strictly use ssh2 1.16.0 package 2026-04-04 17:18:03 +02:00
Šimon Orság
91ebf3b6f5 fix: upgrade ssh2 from 1.15.0 to ^1.16.0 (util.isDate removed in Node.js v23+) 2026-04-03 01:09:28 +02:00
Diego Fabricio
4ef8c94340 Apply suggestion from @greptile-apps[bot]
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
2026-03-18 21:21:47 -05:00
Diego Fabricio
ff369c9d3a style(dashboard): remove max-width constraint from deployments card
- Deleted max-w-8xl class to allow card width to adapt freely
2026-03-18 21:09:08 -05:00
116 changed files with 24342 additions and 36696 deletions

View File

@@ -68,3 +68,45 @@ jobs:
echo "✅ OpenAPI synced to website successfully" 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 Normal file
View File

@@ -0,0 +1,83 @@
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 }}"

File diff suppressed because one or more lines are too long

View File

@@ -1,629 +0,0 @@
"use client";
import { useChat } from "@ai-sdk/react";
import type { ChatContext } from "@dokploy/server/utils/ai/chat-tools";
import { DefaultChatTransport } from "ai";
import {
Bot,
Check,
ChevronDown,
Loader2,
Send,
Trash2,
Wrench,
X,
} from "lucide-react";
import { useRouter } from "next/router";
import { useEffect, useMemo, useRef, useState } from "react";
import ReactMarkdown from "react-markdown";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from "@/components/ui/collapsible";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import {
Sheet,
SheetContent,
SheetDescription,
SheetHeader,
SheetTitle,
} from "@/components/ui/sheet";
import { Textarea } from "@/components/ui/textarea";
import { api } from "@/utils/api";
function useChatContext(): ChatContext {
const router = useRouter();
const { query, pathname } = router;
return useMemo(() => {
const projectId =
typeof query.projectId === "string" ? query.projectId : undefined;
const environmentId =
typeof query.environmentId === "string"
? query.environmentId
: undefined;
const serverId =
typeof query.serverId === "string" ? query.serverId : undefined;
const serviceParams = [
{ key: "applicationId", type: "application" },
{ key: "composeId", type: "compose" },
{ key: "postgresId", type: "postgres" },
{ key: "mysqlId", type: "mysql" },
{ key: "redisId", type: "redis" },
{ key: "mongoId", type: "mongo" },
{ key: "mariadbId", type: "mariadb" },
{ key: "libsqlId", type: "libsql" },
] as const;
for (const { key, type } of serviceParams) {
if (query[key] && typeof query[key] === "string") {
return {
type,
id: query[key] as string,
projectId,
environmentId,
serverId,
};
}
}
if (query.projectId && typeof query.projectId === "string") {
return {
type: "project" as const,
id: query.projectId,
projectId,
environmentId,
serverId,
};
}
return {
type: "general" as const,
id: "",
projectId,
environmentId,
serverId,
};
}, [
query.applicationId,
query.composeId,
query.postgresId,
query.mysqlId,
query.redisId,
query.mongoId,
query.mariadbId,
query.libsqlId,
query.projectId,
query.environmentId,
query.serverId,
pathname,
]);
}
export function ChatPanel() {
const [open, setOpen] = useState(false);
const [aiId, setAiId] = useState<string>("");
const [input, setInput] = useState("");
const scrollRef = useRef<HTMLDivElement>(null);
const inputRef = useRef<HTMLTextAreaElement>(null);
const context = useChatContext();
const aiIdRef = useRef(aiId);
const contextRef = useRef(context);
aiIdRef.current = aiId;
contextRef.current = context;
const { data: isCloud } = api.settings.isCloud.useQuery(undefined, {
refetchOnWindowFocus: false,
});
const { data: providers } = api.ai.getEnabledProviders.useQuery(undefined, {
refetchOnWindowFocus: false,
enabled: !isCloud,
});
const enabledProviders = providers ?? [];
const STORAGE_KEY = "dokploy-chat-messages";
const restoredRef = useRef(false);
const { messages, sendMessage, status, setMessages, addToolApprovalResponse } = useChat({
id: "dokploy-chat",
transport: new DefaultChatTransport({
api: "/api/ai/chat",
body: () => ({
...(isCloud ? {} : { aiId: aiIdRef.current }),
context: contextRef.current,
}),
}),
});
const isLoading = status === "streaming" || status === "submitted";
// Restore messages from localStorage on mount
useEffect(() => {
if (restoredRef.current) return;
restoredRef.current = true;
try {
const stored = localStorage.getItem(STORAGE_KEY);
if (stored) {
const parsed = JSON.parse(stored);
if (Array.isArray(parsed) && parsed.length > 0) {
setMessages(parsed);
}
}
} catch {
// ignore
}
}, [setMessages]);
// Persist messages to localStorage
useEffect(() => {
if (!restoredRef.current) return;
if (messages.length > 0) {
try {
const toStore = messages.slice(-50);
localStorage.setItem(STORAGE_KEY, JSON.stringify(toStore));
} catch {
// localStorage full or unavailable — ignore
}
}
}, [messages]);
useEffect(() => {
if (!isCloud && !aiId && enabledProviders.length > 0 && enabledProviders[0]) {
setAiId(enabledProviders[0].aiId);
}
}, [enabledProviders, aiId, isCloud]);
useEffect(() => {
if (scrollRef.current) {
scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
}
}, [messages, status]);
if (!isCloud && enabledProviders.length === 0) return null;
const handleSend = () => {
if (!input.trim() || isLoading) return;
if (!isCloud && !aiId) return;
sendMessage({ text: input });
setInput("");
setTimeout(() => inputRef.current?.focus(), 0);
};
const contextLabel =
context.type === "general" ? "General" : context.type;
const lastMessage = messages[messages.length - 1];
return (
<>
<Button
onClick={() => setOpen(true)}
variant="outline"
className="fixed bottom-6 right-6 z-50 h-11 w-11 rounded-full shadow-md border"
size="icon"
>
<Bot className="h-5 w-5" />
</Button>
<Sheet open={open} onOpenChange={setOpen}>
<SheetContent
side="right"
className="w-full sm:w-[480px] p-0 flex flex-col border-l outline-none"
>
<SheetHeader className="px-4 py-3 border-b shrink-0">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<Bot className="h-4 w-4 text-muted-foreground" />
<SheetTitle className="text-sm font-medium">
{isCloud ? "Dokploy Agent" : "AI Assistant"}
</SheetTitle>
{isLoading && (
<span className="text-xs text-muted-foreground animate-pulse">
working...
</span>
)}
</div>
</div>
<SheetDescription className="sr-only">
Chat with AI to manage your infrastructure
</SheetDescription>
<div className="flex items-center gap-2 pt-1">
{!isCloud && (
<Select value={aiId} onValueChange={setAiId}>
<SelectTrigger className="h-8 text-xs flex-1">
<SelectValue placeholder="Select provider..." />
</SelectTrigger>
<SelectContent>
{enabledProviders.map((p) => (
<SelectItem key={p.aiId} value={p.aiId}>
{p.name} ({p.model})
</SelectItem>
))}
</SelectContent>
</Select>
)}
<Badge
variant="outline"
className="text-xs shrink-0 capitalize font-normal"
>
{contextLabel}
</Badge>
{messages.length > 0 && (
<Button
variant="ghost"
size="icon"
className="h-8 w-8 shrink-0"
onClick={() => { setMessages([]); localStorage.removeItem(STORAGE_KEY); }}
title="Clear chat"
>
<Trash2 className="h-3.5 w-3.5" />
</Button>
)}
</div>
</SheetHeader>
<div
ref={scrollRef}
className="flex-1 overflow-y-auto p-4 space-y-3"
>
{messages.length === 0 && (
<div className="flex flex-col items-center justify-center h-full text-muted-foreground gap-3">
<Bot className="h-8 w-8 opacity-30" />
<p className="text-sm text-center">
Ask me anything about your{" "}
{context.type === "general"
? "infrastructure"
: context.type}
</p>
<div className="flex flex-wrap gap-1.5 justify-center">
{(context.type === "application"
? [
"What's the status of this app?",
"Why did the last build fail?",
"Show me recent deployments",
"Redeploy this app",
]
: context.type === "compose"
? [
"Show compose service status",
"Why did the last deploy fail?",
"Show me the domains",
"Redeploy this service",
]
: context.type === "postgres" ||
context.type === "mysql" ||
context.type === "redis" ||
context.type === "mongo" ||
context.type === "mariadb" ||
context.type === "libsql"
? [
`Show ${context.type} status`,
"What's the connection info?",
"Show recent deployments",
"Restart this database",
]
: context.type === "project"
? [
"How many services do I have?",
"Show me all environments",
"Which services are failing?",
]
: [
"List all my projects",
"Show project overview",
"What servers do I have?",
]
).map((suggestion) => (
<Button
key={suggestion}
variant="outline"
size="sm"
className="text-xs h-7 font-normal"
onClick={() => setInput(suggestion)}
>
{suggestion}
</Button>
))}
</div>
</div>
)}
{messages.map((message) => {
if (message.role === "user") {
return (
<div key={message.id} className="flex justify-end">
<div className="max-w-[85%] rounded-lg px-3 py-2 text-sm bg-muted">
<p className="whitespace-pre-wrap">
{message.parts
.filter(
(p): p is { type: "text"; text: string } =>
p.type === "text",
)
.map((p) => p.text)
.join("")}
</p>
</div>
</div>
);
}
return (
<div key={message.id} className="flex justify-start">
<div className="max-w-[90%] space-y-2">
{message.parts.map((part, i) => {
if (
part.type === "text" &&
(part as { text?: string }).text?.trim()
) {
return (
<div
key={`text-${message.id}-${i}`}
className="rounded-lg border px-3 py-2 text-sm prose prose-sm dark:prose-invert max-w-none break-words"
>
<ReactMarkdown>
{(part as { text: string }).text}
</ReactMarkdown>
</div>
);
}
if (part.type === "dynamic-tool") {
return (
<div
key={part.toolCallId}
className="rounded-lg border px-3 py-2"
>
<ToolCallDisplay
toolCallId={part.toolCallId}
toolName={part.toolName}
state={part.state}
input={(part as any).input}
output={
part.state === "output-available"
? part.output
: undefined
}
onApprove={(id) =>
addToolApprovalResponse({
id,
approved: true,
})
}
onDeny={(id) =>
addToolApprovalResponse({
id,
approved: false,
reason: "User denied",
})
}
/>
</div>
);
}
if (part.type === "reasoning") {
return (
<Collapsible key={`reasoning-${message.id}-${i}`}>
<CollapsibleTrigger asChild>
<button
type="button"
className="flex items-center gap-1.5 text-xs text-muted-foreground hover:text-foreground transition-colors"
>
<Bot className="h-3 w-3" />
<span>Thinking...</span>
<ChevronDown className="h-3 w-3" />
</button>
</CollapsibleTrigger>
<CollapsibleContent>
<div className="mt-1 rounded-lg border px-3 py-2 text-xs text-muted-foreground italic">
{(part as any).text ||
(part as any).reasoning}
</div>
</CollapsibleContent>
</Collapsible>
);
}
return null;
})}
</div>
</div>
);
})}
{isLoading && lastMessage?.role === "user" && (
<div className="flex justify-start">
<div className="rounded-lg border px-3 py-2 flex items-center gap-2 text-xs text-muted-foreground">
<Loader2 className="h-3 w-3 animate-spin" />
Investigating...
</div>
</div>
)}
</div>
<div className="border-t p-3 shrink-0 flex gap-2">
<Textarea
ref={inputRef}
value={input}
onChange={(e) => setInput(e.target.value)}
placeholder={
!isCloud && !aiId
? "Select a provider first..."
: "Ask anything..."
}
disabled={(!isCloud && !aiId) || isLoading}
className="min-h-[40px] max-h-[120px] resize-none text-sm"
rows={1}
onKeyDown={(e) => {
if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault();
handleSend();
}
}}
/>
<Button
type="button"
size="icon"
variant="outline"
disabled={
(!isCloud && !aiId) || !input.trim() || isLoading
}
className="shrink-0 h-10 w-10"
onClick={handleSend}
>
<Send className="h-4 w-4" />
</Button>
</div>
</SheetContent>
</Sheet>
</>
);
}
function ToolCallDisplay({
toolCallId,
toolName,
state,
input,
output,
onApprove,
onDeny,
}: {
toolCallId: string;
toolName: string;
state: string;
input?: unknown;
output?: unknown;
onApprove?: (id: string) => void;
onDeny?: (id: string) => void;
}) {
const [isOpen, setIsOpen] = useState(false);
const isRunning =
state === "input-streaming" || state === "input-available";
const isDone = state === "output-available";
const isError = state === "output-error";
const needsApproval = state === "requires-approval";
const outputText = output
? typeof output === "string"
? output
: JSON.stringify(output, null, 2)
: null;
// Extract operationId and params from input
const inputData = input as { operationId?: string; params?: Record<string, unknown> } | undefined;
const operationId = inputData?.operationId;
const params = inputData?.params;
// Format: "compose-one" → "compose → one"
const displayLabel = operationId
? operationId.replace("-", " → ")
: toolName;
// Determine HTTP method hint from operationId
const isReadOp = operationId?.match(/^(.*-)?(one|all|get|list|read|search|by)/i);
const StatusIcon = isRunning
? () => <Loader2 className="h-3.5 w-3.5 animate-spin text-blue-500 shrink-0" />
: isDone
? () => <Check className="h-3.5 w-3.5 text-green-500 shrink-0" />
: isError
? () => <X className="h-3.5 w-3.5 text-red-500 shrink-0" />
: () => <Wrench className="h-3.5 w-3.5 text-muted-foreground shrink-0" />;
if (needsApproval) {
return (
<div className="space-y-2">
<div className="flex items-center gap-2 text-xs">
<Wrench className="h-3.5 w-3.5 text-yellow-500 shrink-0" />
<code className="font-mono text-xs font-medium">{displayLabel}</code>
<Badge variant="outline" className="text-[10px] px-1 py-0 h-4 font-normal">
write
</Badge>
</div>
{params && Object.keys(params).length > 0 && (
<div className="ml-5.5 flex flex-wrap gap-1">
{Object.entries(params).map(([key, value]) => (
<span key={key} className="text-[10px] bg-muted px-1.5 py-0.5 rounded font-mono">
{key}={typeof value === "string" ? `"${value}"` : String(value)}
</span>
))}
</div>
)}
<div className="flex gap-1.5 ml-5.5">
<Button
variant="outline"
size="sm"
className="h-6 text-xs px-2"
onClick={() => onApprove?.(toolCallId)}
>
<Check className="h-3 w-3 mr-1" />
Approve
</Button>
<Button
variant="ghost"
size="sm"
className="h-6 text-xs px-2"
onClick={() => onDeny?.(toolCallId)}
>
<X className="h-3 w-3 mr-1" />
Deny
</Button>
</div>
</div>
);
}
return (
<div className="space-y-1">
<Collapsible open={isOpen} onOpenChange={setIsOpen}>
<CollapsibleTrigger asChild>
<button
type="button"
className="flex items-center gap-2 text-xs w-full hover:bg-muted/50 rounded -mx-1 px-1 py-0.5 transition-colors"
>
<StatusIcon />
<code className="font-mono text-xs font-medium">{displayLabel}</code>
{isReadOp && (
<Badge variant="secondary" className="text-[10px] px-1 py-0 h-4 font-normal">
read
</Badge>
)}
{params && Object.keys(params).length > 0 && (
<span className="text-[10px] text-muted-foreground truncate">
{Object.entries(params)
.slice(0, 3)
.map(([k, v]) => `${k}=${typeof v === "string" ? `"${String(v).slice(0, 20)}"` : String(v)}`)
.join(", ")}
{Object.keys(params).length > 3 ? ` +${Object.keys(params).length - 3}` : ""}
</span>
)}
{(outputText || isRunning) && (
<ChevronDown
className={`h-3 w-3 ml-auto text-muted-foreground transition-transform shrink-0 ${isOpen ? "rotate-180" : ""}`}
/>
)}
</button>
</CollapsibleTrigger>
{outputText && (
<CollapsibleContent>
<pre className="mt-1 ml-5.5 p-2 bg-muted/50 rounded text-[10px] overflow-x-auto max-h-[200px] overflow-y-auto leading-tight whitespace-pre-wrap break-words">
{outputText}
</pre>
</CollapsibleContent>
)}
</Collapsible>
</div>
);
}

View File

@@ -666,7 +666,7 @@ export const AddDomain = ({ id, type, domainId = "", children }: Props) => {
<div className="space-y-0.5"> <div className="space-y-0.5">
<FormLabel>Custom Entrypoint</FormLabel> <FormLabel>Custom Entrypoint</FormLabel>
<FormDescription> <FormDescription>
Use custom entrypoint for domina Use custom entrypoint for domain
<br /> <br />
"web" and/or "websecure" is used by default. "web" and/or "websecure" is used by default.
</FormDescription> </FormDescription>

View File

@@ -0,0 +1,290 @@
import { Loader2, MoreHorizontal, RefreshCw } from "lucide-react";
import dynamic from "next/dynamic";
import { useState } from "react";
import { toast } from "sonner";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import { api } from "@/utils/api";
import { ShowContainerConfig } from "@/components/dashboard/docker/config/show-container-config";
import { ShowContainerMounts } from "@/components/dashboard/docker/mounts/show-container-mounts";
import { ShowContainerNetworks } from "@/components/dashboard/docker/networks/show-container-networks";
import { DockerTerminalModal } from "@/components/dashboard/docker/terminal/docker-terminal-modal";
const DockerLogsId = dynamic(
() =>
import("@/components/dashboard/docker/logs/docker-logs-id").then(
(e) => e.DockerLogsId,
),
{
ssr: false,
},
);
interface Props {
appName: string;
serverId?: string;
appType: "stack" | "docker-compose";
}
export const ShowComposeContainers = ({
appName,
appType,
serverId,
}: Props) => {
const { data, isPending, refetch } =
api.docker.getContainersByAppNameMatch.useQuery(
{
appName,
appType,
serverId,
},
{
enabled: !!appName,
},
);
return (
<Card className="bg-background">
<CardHeader className="flex flex-row items-center justify-between">
<div>
<CardTitle className="text-xl">Containers</CardTitle>
<CardDescription>
Inspect each container in this compose and run basic lifecycle
actions.
</CardDescription>
</div>
<Button
variant="outline"
size="icon"
onClick={() => refetch()}
disabled={isPending}
>
<RefreshCw className={`h-4 w-4 ${isPending ? "animate-spin" : ""}`} />
</Button>
</CardHeader>
<CardContent>
{isPending ? (
<div className="flex items-center justify-center h-[20vh]">
<Loader2 className="animate-spin h-6 w-6 text-muted-foreground" />
</div>
) : !data || data.length === 0 ? (
<div className="flex items-center justify-center h-[20vh]">
<span className="text-muted-foreground">
No containers found. Deploy the compose to see containers here.
</span>
</div>
) : (
<div className="rounded-md border">
<Table>
<TableHeader>
<TableRow>
<TableHead>Name</TableHead>
<TableHead>State</TableHead>
<TableHead>Status</TableHead>
<TableHead>Container ID</TableHead>
<TableHead className="text-right" />
</TableRow>
</TableHeader>
<TableBody>
{data.map((container) => (
<ContainerRow
key={container.containerId}
container={container}
serverId={serverId}
onActionComplete={() => refetch()}
/>
))}
</TableBody>
</Table>
</div>
)}
</CardContent>
</Card>
);
};
interface ContainerRowProps {
container: {
containerId: string;
name: string;
state: string;
status: string;
};
serverId?: string;
onActionComplete: () => void;
}
const ContainerRow = ({
container,
serverId,
onActionComplete,
}: ContainerRowProps) => {
const [logsOpen, setLogsOpen] = useState(false);
const [actionLoading, setActionLoading] = useState<string | null>(null);
const restartMutation = api.docker.restartContainer.useMutation();
const startMutation = api.docker.startContainer.useMutation();
const stopMutation = api.docker.stopContainer.useMutation();
const killMutation = api.docker.killContainer.useMutation();
const handleAction = async (
action: string,
mutationFn: typeof restartMutation,
) => {
setActionLoading(action);
try {
await mutationFn.mutateAsync({
containerId: container.containerId,
serverId,
});
toast.success(`Container ${action} successfully`);
onActionComplete();
} catch (error) {
toast.error(
`Failed to ${action} container: ${error instanceof Error ? error.message : "Unknown error"}`,
);
} finally {
setActionLoading(null);
}
};
return (
<TableRow>
<TableCell className="font-medium">{container.name}</TableCell>
<TableCell>
<Badge
variant={
container.state === "running"
? "default"
: container.state === "exited"
? "secondary"
: "destructive"
}
>
{container.state}
</Badge>
</TableCell>
<TableCell>{container.status}</TableCell>
<TableCell className="font-mono text-sm text-muted-foreground">
{container.containerId}
</TableCell>
<TableCell className="text-right">
<Dialog open={logsOpen} onOpenChange={setLogsOpen}>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" className="h-8 w-8 p-0">
{actionLoading ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : (
<MoreHorizontal className="h-4 w-4" />
)}
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuLabel>Actions</DropdownMenuLabel>
<DialogTrigger asChild>
<DropdownMenuItem
className="cursor-pointer"
onSelect={(e) => e.preventDefault()}
>
View Logs
</DropdownMenuItem>
</DialogTrigger>
<ShowContainerConfig
containerId={container.containerId}
serverId={serverId || ""}
/>
<ShowContainerMounts
containerId={container.containerId}
serverId={serverId || ""}
/>
<ShowContainerNetworks
containerId={container.containerId}
serverId={serverId || ""}
/>
<DockerTerminalModal
containerId={container.containerId}
serverId={serverId || ""}
>
Terminal
</DockerTerminalModal>
<DropdownMenuSeparator />
<DropdownMenuItem
className="cursor-pointer"
disabled={actionLoading !== null}
onClick={() => handleAction("restart", restartMutation)}
>
Restart
</DropdownMenuItem>
<DropdownMenuItem
className="cursor-pointer"
disabled={actionLoading !== null}
onClick={() => handleAction("start", startMutation)}
>
Start
</DropdownMenuItem>
<DropdownMenuItem
className="cursor-pointer"
disabled={actionLoading !== null}
onClick={() => handleAction("stop", stopMutation)}
>
Stop
</DropdownMenuItem>
<DropdownMenuItem
className="cursor-pointer text-red-500 focus:text-red-600"
disabled={actionLoading !== null}
onClick={() => handleAction("kill", killMutation)}
>
Kill
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
<DialogContent className="sm:max-w-7xl">
<DialogHeader>
<DialogTitle>View Logs</DialogTitle>
<DialogDescription>Logs for {container.name}</DialogDescription>
</DialogHeader>
<div className="flex flex-col gap-4 pt-2.5">
<DockerLogsId
containerId={container.containerId}
serverId={serverId}
runType="native"
/>
</div>
</DialogContent>
</Dialog>
</TableCell>
</TableRow>
);
};

View File

@@ -0,0 +1,112 @@
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { DropdownMenuItem } from "@/components/ui/dropdown-menu";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import { Badge } from "@/components/ui/badge";
import { api } from "@/utils/api";
interface Props {
containerId: string;
serverId?: string;
}
interface Mount {
Type: string;
Source: string;
Destination: string;
Mode: string;
RW: boolean;
Propagation: string;
Name?: string;
Driver?: string;
}
export const ShowContainerMounts = ({ containerId, serverId }: Props) => {
const { data } = api.docker.getConfig.useQuery(
{
containerId,
serverId,
},
{
enabled: !!containerId,
},
);
const mounts: Mount[] = data?.Mounts ?? [];
return (
<Dialog>
<DialogTrigger asChild>
<DropdownMenuItem
className="w-full cursor-pointer"
onSelect={(e) => e.preventDefault()}
>
View Mounts
</DropdownMenuItem>
</DialogTrigger>
<DialogContent className="w-full md:w-[70vw] min-w-[70vw]">
<DialogHeader>
<DialogTitle>Container Mounts</DialogTitle>
<DialogDescription>
Volume and bind mounts for this container
</DialogDescription>
</DialogHeader>
<div className="overflow-auto max-h-[70vh]">
{mounts.length === 0 ? (
<div className="text-center text-muted-foreground py-8">
No mounts found for this container.
</div>
) : (
<Table>
<TableHeader>
<TableRow>
<TableHead>Type</TableHead>
<TableHead>Source</TableHead>
<TableHead>Destination</TableHead>
<TableHead>Mode</TableHead>
<TableHead>Read/Write</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{mounts.map((mount, index) => (
<TableRow key={index}>
<TableCell>
<Badge variant="outline">{mount.Type}</Badge>
</TableCell>
<TableCell className="font-mono text-xs max-w-[250px] truncate">
{mount.Name || mount.Source}
</TableCell>
<TableCell className="font-mono text-xs max-w-[250px] truncate">
{mount.Destination}
</TableCell>
<TableCell className="text-xs">
{mount.Mode || "-"}
</TableCell>
<TableCell>
<Badge variant={mount.RW ? "default" : "secondary"}>
{mount.RW ? "RW" : "RO"}
</Badge>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
)}
</div>
</DialogContent>
</Dialog>
);
};

View File

@@ -0,0 +1,119 @@
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { DropdownMenuItem } from "@/components/ui/dropdown-menu";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import { Badge } from "@/components/ui/badge";
import { api } from "@/utils/api";
interface Props {
containerId: string;
serverId?: string;
}
interface Network {
IPAMConfig: unknown;
Links: unknown;
Aliases: string[] | null;
MacAddress: string;
NetworkID: string;
EndpointID: string;
Gateway: string;
IPAddress: string;
IPPrefixLen: number;
IPv6Gateway: string;
GlobalIPv6Address: string;
GlobalIPv6PrefixLen: number;
DriverOpts: unknown;
}
export const ShowContainerNetworks = ({ containerId, serverId }: Props) => {
const { data } = api.docker.getConfig.useQuery(
{
containerId,
serverId,
},
{
enabled: !!containerId,
},
);
const networks: Record<string, Network> =
data?.NetworkSettings?.Networks ?? {};
const entries = Object.entries(networks);
return (
<Dialog>
<DialogTrigger asChild>
<DropdownMenuItem
className="w-full cursor-pointer"
onSelect={(e) => e.preventDefault()}
>
View Networks
</DropdownMenuItem>
</DialogTrigger>
<DialogContent className="w-full md:w-[70vw] min-w-[70vw]">
<DialogHeader>
<DialogTitle>Container Networks</DialogTitle>
<DialogDescription>
Networks attached to this container
</DialogDescription>
</DialogHeader>
<div className="overflow-auto max-h-[70vh]">
{entries.length === 0 ? (
<div className="text-center text-muted-foreground py-8">
No networks found for this container.
</div>
) : (
<Table>
<TableHeader>
<TableRow>
<TableHead>Network</TableHead>
<TableHead>IP Address</TableHead>
<TableHead>Gateway</TableHead>
<TableHead>MAC Address</TableHead>
<TableHead>Aliases</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{entries.map(([name, network]) => (
<TableRow key={name}>
<TableCell>
<Badge variant="outline">{name}</Badge>
</TableCell>
<TableCell className="font-mono text-xs">
{network.IPAddress
? `${network.IPAddress}/${network.IPPrefixLen}`
: "-"}
</TableCell>
<TableCell className="font-mono text-xs">
{network.Gateway || "-"}
</TableCell>
<TableCell className="font-mono text-xs">
{network.MacAddress || "-"}
</TableCell>
<TableCell className="text-xs">
{network.Aliases?.join(", ") || "-"}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
)}
</div>
</DialogContent>
</Dialog>
);
};

View File

@@ -10,6 +10,8 @@ import {
} from "@/components/ui/dropdown-menu"; } from "@/components/ui/dropdown-menu";
import { ShowContainerConfig } from "../config/show-container-config"; import { ShowContainerConfig } from "../config/show-container-config";
import { ShowDockerModalLogs } from "../logs/show-docker-modal-logs"; 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 { RemoveContainerDialog } from "../remove/remove-container";
import { DockerTerminalModal } from "../terminal/docker-terminal-modal"; import { DockerTerminalModal } from "../terminal/docker-terminal-modal";
import { UploadFileModal } from "../upload/upload-file-modal"; import { UploadFileModal } from "../upload/upload-file-modal";
@@ -123,6 +125,14 @@ export const columns: ColumnDef<Container>[] = [
containerId={container.containerId} containerId={container.containerId}
serverId={container.serverId || ""} serverId={container.serverId || ""}
/> />
<ShowContainerMounts
containerId={container.containerId}
serverId={container.serverId || ""}
/>
<ShowContainerNetworks
containerId={container.containerId}
serverId={container.serverId || ""}
/>
<DockerTerminalModal <DockerTerminalModal
containerId={container.containerId} containerId={container.containerId}
serverId={container.serverId || ""} serverId={container.serverId || ""}

View File

@@ -0,0 +1,291 @@
import { formatDistanceToNow } from "date-fns";
import { ArrowRight, Rocket, Server } from "lucide-react";
import Link from "next/link";
import { useMemo } from "react";
import { Button } from "@/components/ui/button";
import { Card } from "@/components/ui/card";
import { api } from "@/utils/api";
type DeploymentStatus = "idle" | "running" | "done" | "error";
const statusDotClass: Record<string, string> = {
done: "bg-emerald-500",
running: "bg-amber-500",
error: "bg-red-500",
idle: "bg-muted-foreground/40",
};
function getServiceInfo(d: any) {
const app = d.application;
const comp = d.compose;
const serverName: string =
d.server?.name ?? app?.server?.name ?? comp?.server?.name ?? "Dokploy";
if (app?.environment?.project && app.environment) {
return {
name: app.name as string,
environment: app.environment.name as string,
projectName: app.environment.project.name as string,
serverName,
href: `/dashboard/project/${app.environment.project.projectId}/environment/${app.environment.environmentId}/services/application/${app.applicationId}`,
};
}
if (comp?.environment?.project && comp.environment) {
return {
name: comp.name as string,
environment: comp.environment.name as string,
projectName: comp.environment.project.name as string,
serverName,
href: `/dashboard/project/${comp.environment.project.projectId}/environment/${comp.environment.environmentId}/services/compose/${comp.composeId}`,
};
}
return null;
}
function StatCard({
label,
value,
delta,
}: {
label: string;
value: string;
delta?: string;
}) {
return (
<div className="rounded-xl border bg-background p-5 min-h-[140px] flex flex-col justify-between">
<span className="text-xs uppercase tracking-wider text-muted-foreground">
{label}
</span>
<div className="flex flex-col gap-1">
<span className="text-3xl font-semibold tracking-tight">{value}</span>
{delta && (
<span className="text-xs text-muted-foreground">{delta}</span>
)}
</div>
</div>
);
}
function StatusListCard({
label,
items,
}: {
label: string;
items: { dotClass: string; label: string; count: number }[];
}) {
return (
<div className="rounded-xl border bg-background p-5 min-h-[140px] flex flex-col gap-3">
<span className="text-xs uppercase tracking-wider text-muted-foreground">
{label}
</span>
<ul className="flex flex-col gap-1.5">
{items.map((item) => (
<li key={item.label} className="flex items-center gap-2.5 text-sm">
<span
className={`size-2 rounded-full shrink-0 ${item.dotClass}`}
aria-hidden
/>
<span className="font-semibold tabular-nums w-8">{item.count}</span>
<span className="text-muted-foreground">{item.label}</span>
</li>
))}
</ul>
</div>
);
}
export const ShowHome = () => {
const { data: auth } = api.user.get.useQuery();
const { data: homeStats } = api.project.homeStats.useQuery();
const { data: permissions } = api.user.getPermissions.useQuery();
const canReadDeployments = !!permissions?.deployment.read;
const { data: deployments } = api.deployment.allCentralized.useQuery(
undefined,
{
enabled: canReadDeployments,
refetchInterval: 10000,
},
);
const firstName = auth?.user?.firstName?.trim();
const totals = homeStats ?? {
projects: 0,
environments: 0,
applications: 0,
compose: 0,
databases: 0,
services: 0,
};
const statusBreakdown = homeStats?.status ?? {
running: 0,
error: 0,
idle: 0,
};
const recentDeployments = useMemo(() => {
if (!deployments) return [];
return [...deployments]
.sort(
(a, b) =>
new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime(),
)
.slice(0, 10);
}, [deployments]);
const deployStats = useMemo(() => {
const now = Date.now();
const weekMs = 7 * 24 * 60 * 60 * 1000;
const lastStart = now - weekMs;
const prevStart = now - 2 * weekMs;
const last: NonNullable<typeof deployments> = [];
const prev: NonNullable<typeof deployments> = [];
for (const d of deployments ?? []) {
const t = new Date(d.createdAt).getTime();
if (t >= lastStart) last.push(d);
else if (t >= prevStart) prev.push(d);
}
const lastCount = last.length;
const prevCount = prev.length;
let delta: string | undefined;
if (prevCount > 0) {
const pct = Math.round(((lastCount - prevCount) / prevCount) * 100);
delta = `${pct >= 0 ? "+" : ""}${pct}% vs prev 7d`;
} else if (lastCount > 0) {
delta = "no prior data";
} else {
delta = "no activity yet";
}
return { value: String(lastCount), delta };
}, [deployments]);
return (
<div className="w-full">
<Card className="h-full bg-sidebar p-2.5 rounded-xl min-h-[85vh]">
<div className="rounded-xl bg-background shadow-md p-6 flex flex-col gap-6 h-full">
<div className="flex flex-col gap-6 sm:flex-row sm:items-end sm:justify-between">
<h1 className="text-3xl font-semibold tracking-tight">
{firstName ? `Welcome back, ${firstName}` : "Welcome back"}
</h1>
<Button asChild variant="secondary" className="w-fit">
<Link href="/dashboard/projects">
Go to projects
<ArrowRight className="size-4" />
</Link>
</Button>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
<StatCard
label="Projects"
value={String(totals.projects)}
delta={`${totals.environments} ${totals.environments === 1 ? "environment" : "environments"}`}
/>
<StatCard
label="Services"
value={String(totals.services)}
delta={`${totals.applications} apps · ${totals.compose} compose · ${totals.databases} db`}
/>
<StatCard
label="Deploys / 7d"
value={deployStats.value}
delta={deployStats.delta}
/>
<StatusListCard
label="Status"
items={[
{
dotClass: "bg-emerald-500",
label: "running",
count: statusBreakdown.running,
},
{
dotClass: "bg-red-500",
label: "errored",
count: statusBreakdown.error,
},
{
dotClass: "bg-muted-foreground/40",
label: "idle",
count: statusBreakdown.idle,
},
]}
/>
</div>
<div className="rounded-xl border bg-background">
<div className="flex items-center justify-between px-5 py-4 border-b">
<div className="flex items-center gap-2">
<Rocket className="size-4 text-muted-foreground" />
<h2 className="text-sm font-semibold">Recent deployments</h2>
</div>
{canReadDeployments && (
<Link
href="/dashboard/deployments"
className="text-xs text-muted-foreground hover:text-foreground transition-colors"
>
view all
</Link>
)}
</div>
{!canReadDeployments ? (
<div className="min-h-[400px] flex flex-col items-center justify-center gap-3 text-center text-sm text-muted-foreground p-10">
<Rocket className="size-8 opacity-40" />
<span>You do not have permission to view deployments.</span>
</div>
) : recentDeployments.length === 0 ? (
<div className="min-h-[400px] flex flex-col items-center justify-center gap-3 text-center text-sm text-muted-foreground p-10">
<Rocket className="size-8 opacity-40" />
<span>No deployments yet.</span>
</div>
) : (
<ul className="divide-y">
{recentDeployments.map((d) => {
const info = getServiceInfo(d);
if (!info) return null;
const status = (d.status ?? "idle") as DeploymentStatus;
return (
<li key={d.deploymentId}>
<Link
href={info.href}
className="flex items-center gap-4 px-5 py-4 hover:bg-muted/40 transition-colors"
>
<span
className={`size-2 rounded-full shrink-0 ${statusDotClass[status] ?? statusDotClass.idle}`}
aria-hidden
/>
<div className="flex flex-col min-w-0 flex-1">
<span className="text-sm truncate">{info.name}</span>
<span className="text-xs text-muted-foreground truncate">
{info.projectName} · {info.environment}
</span>
</div>
<span className="text-xs text-muted-foreground w-36 hidden lg:flex items-center justify-end gap-1.5 truncate">
<Server className="size-3 shrink-0" />
<span className="truncate">{info.serverName}</span>
</span>
<span className="text-xs text-muted-foreground w-20 text-right hidden sm:inline">
{status}
</span>
<span className="text-xs text-muted-foreground w-24 text-right hidden md:inline">
{formatDistanceToNow(new Date(d.createdAt), {
addSuffix: true,
})}
</span>
<span className="text-xs text-muted-foreground hover:text-foreground transition-colors">
logs
</span>
</Link>
</li>
);
})}
</ul>
)}
</div>
</div>
</Card>
</div>
);
};

View File

@@ -166,6 +166,7 @@ export const ShowProjects = () => {
return ( return (
total + total +
(env.applications?.length || 0) + (env.applications?.length || 0) +
(env.libsql?.length || 0) +
(env.mariadb?.length || 0) + (env.mariadb?.length || 0) +
(env.mongo?.length || 0) + (env.mongo?.length || 0) +
(env.mysql?.length || 0) + (env.mysql?.length || 0) +
@@ -178,6 +179,7 @@ export const ShowProjects = () => {
return ( return (
total + total +
(env.applications?.length || 0) + (env.applications?.length || 0) +
(env.libsql?.length || 0) +
(env.mariadb?.length || 0) + (env.mariadb?.length || 0) +
(env.mongo?.length || 0) + (env.mongo?.length || 0) +
(env.mysql?.length || 0) + (env.mysql?.length || 0) +

View File

@@ -79,8 +79,11 @@ export const columns: ColumnDef<LogEntry>[] = [
: log.RequestPath} : log.RequestPath}
</div> </div>
<div className="flex flex-row gap-3 w-full"> <div className="flex flex-row gap-3 w-full">
<Badge variant={getStatusColor(log.OriginStatus)}> <Badge
Status: {formatStatusLabel(log.OriginStatus)} variant={getStatusColor(log.OriginStatus || log.DownstreamStatus)}
>
Status:{" "}
{formatStatusLabel(log.OriginStatus || log.DownstreamStatus)}
</Badge> </Badge>
<Badge variant={"secondary"}> <Badge variant={"secondary"}>
Exec Time: {formatDuration(log.Duration)} Exec Time: {formatDuration(log.Duration)}

View File

@@ -185,7 +185,7 @@ export const RequestsTable = ({ dateRange }: RequestsTableProps) => {
<div className="flex flex-col gap-4 w-full overflow-auto"> <div className="flex flex-col gap-4 w-full overflow-auto">
<div className="flex items-center gap-2 max-sm:flex-wrap"> <div className="flex items-center gap-2 max-sm:flex-wrap">
<Input <Input
placeholder="Filter by name..." placeholder="Filter by hostname..."
value={search} value={search}
onChange={(event) => setSearch(event.target.value)} onChange={(event) => setSearch(event.target.value)}
className="md:max-w-sm" className="md:max-w-sm"

View File

@@ -167,7 +167,7 @@ export const SearchCommand = () => {
<CommandGroup heading={"Application"} hidden={true}> <CommandGroup heading={"Application"} hidden={true}>
<CommandItem <CommandItem
onSelect={() => { onSelect={() => {
router.push("/dashboard/projects"); router.push("/dashboard/home");
setOpen(false); setOpen(false);
}} }}
> >

View File

@@ -425,7 +425,7 @@ export const WelcomeSubscription = () => {
onClick={() => { onClick={() => {
if (stepper.isLast) { if (stepper.isLast) {
setIsOpen(false); setIsOpen(false);
push("/dashboard/projects"); push("/dashboard/home");
} else { } else {
stepper.next(); stepper.next();
} }

View File

@@ -1,5 +1,4 @@
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { ChatPanel } from "../dashboard/ai-chat/chat-panel";
import { ImpersonationBar } from "../dashboard/impersonation/impersonation-bar"; import { ImpersonationBar } from "../dashboard/impersonation/impersonation-bar";
import { HubSpotWidget } from "../shared/HubSpotWidget"; import { HubSpotWidget } from "../shared/HubSpotWidget";
import Page from "./side"; import Page from "./side";
@@ -24,7 +23,6 @@ export const DashboardLayout = ({ children }: Props) => {
return ( return (
<> <>
<Page>{children}</Page> <Page>{children}</Page>
<ChatPanel />
{isChatEnabled && ( {isChatEnabled && (
<> <>
<HubSpotWidget /> <HubSpotWidget />

View File

@@ -19,6 +19,7 @@ import {
Forward, Forward,
GalleryVerticalEnd, GalleryVerticalEnd,
GitBranch, GitBranch,
House,
Key, Key,
KeyRound, KeyRound,
Loader2, Loader2,
@@ -148,6 +149,12 @@ type Menu = {
// The `isEnabled` function is called to determine if the item should be displayed // The `isEnabled` function is called to determine if the item should be displayed
const MENU: Menu = { const MENU: Menu = {
home: [ home: [
{
isSingle: true,
title: "Home",
url: "/dashboard/home",
icon: House,
},
{ {
isSingle: true, isSingle: true,
title: "Projects", title: "Projects",

View File

@@ -80,7 +80,7 @@ export const UserNav = () => {
<DropdownMenuItem <DropdownMenuItem
className="cursor-pointer" className="cursor-pointer"
onClick={() => { onClick={() => {
router.push("/dashboard/projects"); router.push("/dashboard/home");
}} }}
> >
Projects Projects

View File

@@ -44,7 +44,7 @@ export function SignInWithSSO({ children }: SignInWithSSOProps) {
try { try {
const { data, error } = await authClient.signIn.sso({ const { data, error } = await authClient.signIn.sso({
email: values.email, email: values.email,
callbackURL: "/dashboard/projects", callbackURL: "/dashboard/home",
}); });
if (error) { if (error) {
toast.error(error.message ?? "Failed to sign in with SSO"); toast.error(error.message ?? "Failed to sign in with SSO");

View File

@@ -116,6 +116,14 @@ export function TagSelector({
<HandleTag /> <HandleTag />
</div> </div>
</CommandEmpty> </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> <CommandGroup>
{tags.map((tag) => { {tags.map((tag) => {
const isSelected = selectedTags.includes(tag.id); const isSelected = selectedTags.includes(tag.id);

View File

@@ -0,0 +1,198 @@
import * as ContextMenuPrimitive from "@radix-ui/react-context-menu";
import { Check, ChevronRight, Circle } from "lucide-react";
import * as React from "react";
import { cn } from "@/lib/utils";
const ContextMenu = ContextMenuPrimitive.Root;
const ContextMenuTrigger = ContextMenuPrimitive.Trigger;
const ContextMenuGroup = ContextMenuPrimitive.Group;
const ContextMenuPortal = ContextMenuPrimitive.Portal;
const ContextMenuSub = ContextMenuPrimitive.Sub;
const ContextMenuRadioGroup = ContextMenuPrimitive.RadioGroup;
const ContextMenuSubTrigger = React.forwardRef<
React.ElementRef<typeof ContextMenuPrimitive.SubTrigger>,
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.SubTrigger> & {
inset?: boolean;
}
>(({ className, inset, children, ...props }, ref) => (
<ContextMenuPrimitive.SubTrigger
ref={ref}
className={cn(
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground",
inset && "pl-8",
className,
)}
{...props}
>
{children}
<ChevronRight className="ml-auto h-4 w-4" />
</ContextMenuPrimitive.SubTrigger>
));
ContextMenuSubTrigger.displayName = ContextMenuPrimitive.SubTrigger.displayName;
const ContextMenuSubContent = React.forwardRef<
React.ElementRef<typeof ContextMenuPrimitive.SubContent>,
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.SubContent>
>(({ className, ...props }, ref) => (
<ContextMenuPrimitive.SubContent
ref={ref}
className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className,
)}
{...props}
/>
));
ContextMenuSubContent.displayName = ContextMenuPrimitive.SubContent.displayName;
const ContextMenuContent = React.forwardRef<
React.ElementRef<typeof ContextMenuPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Content>
>(({ className, ...props }, ref) => (
<ContextMenuPrimitive.Portal>
<ContextMenuPrimitive.Content
ref={ref}
className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md animate-in fade-in-80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className,
)}
{...props}
/>
</ContextMenuPrimitive.Portal>
));
ContextMenuContent.displayName = ContextMenuPrimitive.Content.displayName;
const ContextMenuItem = React.forwardRef<
React.ElementRef<typeof ContextMenuPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Item> & {
inset?: boolean;
}
>(({ className, inset, ...props }, ref) => (
<ContextMenuPrimitive.Item
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
inset && "pl-8",
className,
)}
{...props}
/>
));
ContextMenuItem.displayName = ContextMenuPrimitive.Item.displayName;
const ContextMenuCheckboxItem = React.forwardRef<
React.ElementRef<typeof ContextMenuPrimitive.CheckboxItem>,
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.CheckboxItem>
>(({ className, children, checked, ...props }, ref) => (
<ContextMenuPrimitive.CheckboxItem
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className,
)}
checked={checked}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<ContextMenuPrimitive.ItemIndicator>
<Check className="h-4 w-4" />
</ContextMenuPrimitive.ItemIndicator>
</span>
{children}
</ContextMenuPrimitive.CheckboxItem>
));
ContextMenuCheckboxItem.displayName =
ContextMenuPrimitive.CheckboxItem.displayName;
const ContextMenuRadioItem = React.forwardRef<
React.ElementRef<typeof ContextMenuPrimitive.RadioItem>,
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.RadioItem>
>(({ className, children, ...props }, ref) => (
<ContextMenuPrimitive.RadioItem
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className,
)}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<ContextMenuPrimitive.ItemIndicator>
<Circle className="h-2 w-2 fill-current" />
</ContextMenuPrimitive.ItemIndicator>
</span>
{children}
</ContextMenuPrimitive.RadioItem>
));
ContextMenuRadioItem.displayName = ContextMenuPrimitive.RadioItem.displayName;
const ContextMenuLabel = React.forwardRef<
React.ElementRef<typeof ContextMenuPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Label> & {
inset?: boolean;
}
>(({ className, inset, ...props }, ref) => (
<ContextMenuPrimitive.Label
ref={ref}
className={cn(
"px-2 py-1.5 text-sm font-semibold text-foreground",
inset && "pl-8",
className,
)}
{...props}
/>
));
ContextMenuLabel.displayName = ContextMenuPrimitive.Label.displayName;
const ContextMenuSeparator = React.forwardRef<
React.ElementRef<typeof ContextMenuPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Separator>
>(({ className, ...props }, ref) => (
<ContextMenuPrimitive.Separator
ref={ref}
className={cn("-mx-1 my-1 h-px bg-border", className)}
{...props}
/>
));
ContextMenuSeparator.displayName = ContextMenuPrimitive.Separator.displayName;
const ContextMenuShortcut = ({
className,
...props
}: React.HTMLAttributes<HTMLSpanElement>) => {
return (
<span
className={cn(
"ml-auto text-xs tracking-widest text-muted-foreground",
className,
)}
{...props}
/>
);
};
ContextMenuShortcut.displayName = "ContextMenuShortcut";
export {
ContextMenu,
ContextMenuTrigger,
ContextMenuContent,
ContextMenuItem,
ContextMenuCheckboxItem,
ContextMenuRadioItem,
ContextMenuLabel,
ContextMenuSeparator,
ContextMenuShortcut,
ContextMenuGroup,
ContextMenuPortal,
ContextMenuSub,
ContextMenuSubContent,
ContextMenuSubTrigger,
ContextMenuRadioGroup,
};

View File

@@ -1,6 +1,6 @@
{ {
"name": "dokploy", "name": "dokploy",
"version": "v0.29.0", "version": "v0.29.2",
"private": true, "private": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"type": "module", "type": "module",
@@ -46,7 +46,6 @@
"@ai-sdk/mistral": "^3.0.20", "@ai-sdk/mistral": "^3.0.20",
"@ai-sdk/openai": "^3.0.29", "@ai-sdk/openai": "^3.0.29",
"@ai-sdk/openai-compatible": "^2.0.30", "@ai-sdk/openai-compatible": "^2.0.30",
"@ai-sdk/react": "^3.0.156",
"@better-auth/api-key": "1.5.4", "@better-auth/api-key": "1.5.4",
"@better-auth/sso": "1.5.4", "@better-auth/sso": "1.5.4",
"@codemirror/autocomplete": "^6.18.6", "@codemirror/autocomplete": "^6.18.6",
@@ -58,7 +57,7 @@
"@codemirror/search": "^6.6.0", "@codemirror/search": "^6.6.0",
"@codemirror/view": "^6.39.15", "@codemirror/view": "^6.39.15",
"@dokploy/server": "workspace:*", "@dokploy/server": "workspace:*",
"@dokploy/trpc-openapi": "0.0.19", "@dokploy/trpc-openapi": "0.0.18",
"@faker-js/faker": "^8.4.1", "@faker-js/faker": "^8.4.1",
"@hookform/resolvers": "^5.2.2", "@hookform/resolvers": "^5.2.2",
"@octokit/auth-app": "^6.1.3", "@octokit/auth-app": "^6.1.3",
@@ -68,6 +67,7 @@
"@radix-ui/react-avatar": "^1.1.10", "@radix-ui/react-avatar": "^1.1.10",
"@radix-ui/react-checkbox": "^1.3.2", "@radix-ui/react-checkbox": "^1.3.2",
"@radix-ui/react-collapsible": "^1.1.11", "@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-dialog": "^1.1.14",
"@radix-ui/react-dropdown-menu": "^2.1.15", "@radix-ui/react-dropdown-menu": "^2.1.15",
"@radix-ui/react-label": "^2.1.7", "@radix-ui/react-label": "^2.1.7",
@@ -147,7 +147,7 @@
"shell-quote": "^1.8.1", "shell-quote": "^1.8.1",
"slugify": "^1.6.6", "slugify": "^1.6.6",
"sonner": "^1.7.4", "sonner": "^1.7.4",
"ssh2": "1.15.0", "ssh2": "~1.16.0",
"stripe": "17.2.0", "stripe": "17.2.0",
"superjson": "^2.2.2", "superjson": "^2.2.2",
"swagger-ui-react": "^5.31.2", "swagger-ui-react": "^5.31.2",

View File

@@ -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"> <div className="mt-5 flex flex-col justify-center items-center gap-2 sm:flex-row sm:gap-3">
<Link <Link
href="/dashboard/projects" href="/dashboard/home"
className={buttonVariants({ className={buttonVariants({
variant: "secondary", variant: "secondary",
className: "flex flex-row gap-2", className: "flex flex-row gap-2",

View File

@@ -1,251 +0,0 @@
import { readFileSync } from "node:fs";
import { join } from "node:path";
import { IS_CLOUD, validateRequest } from "@dokploy/server";
import { getAiSettingById } from "@dokploy/server/services/ai";
import {
type ChatContext,
getAllTools,
} from "@dokploy/server/utils/ai/chat-tools";
import {
buildEndpointCatalog,
createApiTool,
} from "@dokploy/server/utils/ai/api-tool";
import {
getOrCreateEmbeddings,
retrieveRelevantEndpoints,
} from "@dokploy/server/utils/ai/tool-retrieval";
import { selectAIProvider } from "@dokploy/server/utils/ai/select-ai-provider";
import { createAnthropic } from "@ai-sdk/anthropic";
import { convertToModelMessages, stepCountIs, streamText } from "ai";
import type { NextApiRequest, NextApiResponse } from "next";
let cachedSpec: any = null;
function getOpenApiSpec() {
if (!cachedSpec) {
try {
const specPath = join(process.cwd(), "../../openapi.json");
cachedSpec = JSON.parse(readFileSync(specPath, "utf-8"));
} catch {
cachedSpec = null;
}
}
return cachedSpec;
}
function buildContextBlock(context: ChatContext): string {
if (context.type === "general") {
return "CONTEXT: The user is on the general dashboard (no specific resource selected). Use project-all to list their projects if needed.";
}
const lines: string[] = [];
lines.push(
`CONTEXT: The user is currently viewing a specific ${context.type}. The ${context.type}Id is "${context.id}".`,
);
lines.push(
`When the user says "this app", "this service", "this database", "add env var", etc., they ALWAYS mean this ${context.type} (ID: "${context.id}"). NEVER ask which service they mean.`,
);
if (context.projectId) {
lines.push(`- projectId: "${context.projectId}"`);
}
if (context.environmentId) {
lines.push(`- environmentId: "${context.environmentId}"`);
}
if (context.serverId) {
lines.push(`- serverId: "${context.serverId}"`);
}
lines.push(
"Use these IDs directly when calling tools — do NOT ask the user for them. You already know exactly which resource the user is talking about.",
);
return lines.join("\n");
}
function buildSystemPrompt(context: ChatContext, catalog: string | null, endpointCount?: number) {
const contextBlock = buildContextBlock(context);
return `You are an autonomous DevOps agent inside Dokploy (Docker-based PaaS). You take action immediately — you don't explain, you don't ask, you DO.
${contextBlock}
THINKING PROCESS (do this before EVERY action):
1. Scan ALL section headers (## tag — description) in the ENDPOINT CATALOG to find which sections are relevant
2. Read the endpoint descriptions in those sections to pick the right operationId
3. Call the endpoint with the correct params — use the IDs from the context above
BEHAVIOR:
- When the user asks you to do something → DO IT. Call the API right away.
- When you need information → call the endpoint to get it. Never say "I can't access" or "I don't have the ability to".
- When something fails → read the error, figure out the fix, and apply it. Don't stop to explain the error — fix it.
- EVERY capability you need is in the ENDPOINT CATALOG below. If you think you can't do something, you're wrong — scan ALL sections again.
- You already have all the IDs you need from the context above. NEVER ask the user for IDs, paths, or information you can discover by calling endpoints.
- NEVER ask for confirmation or permission. The only exception is deleting a service entirely. For everything else (read, update, deploy, stop, start, restart) → just do it immediately.
KEY PATTERN: When you need to explore files, find paths, or check repository structure → use the "patch" section endpoints to browse directories and read files. NEVER ask the user for file paths.
DATA MODEL: Project → Environment → Services (application, compose, postgres, mysql, redis, mongo, mariadb, libsql). Each service has deployments with build logs.
TOOL: You have one tool "call_api". Pass operationId + params from the catalog.
- ALWAYS pass required params (*) in the "params" object. Example: { "operationId": "domain-byComposeId", "params": { "composeId": "abc123" } }
- Params: * = required, ? = optional, [a|b|c] = allowed values
- GET = read-only (auto-executed). POST/PUT/DELETE = write (user approves).
- If a call fails, read the error message and fix the params. NEVER retry the same call with the same params.
RESPONSE STYLE:
- 2-3 sentences max. No walls of text.
- Never explain limitations — find the right endpoint and act.
- Answer in the user's language.
${catalog ? `ENDPOINT CATALOG (${endpointCount} endpoints):\n${catalog}` : ""}`;
}
function getUserMessages(messages: any[]): string {
const texts: string[] = [];
for (const msg of messages) {
if (msg.role !== "user") continue;
if (typeof msg.content === "string") {
texts.push(msg.content);
} else if (Array.isArray(msg.content)) {
texts.push(
msg.content
.filter((p: any) => p.type === "text")
.map((p: any) => p.text)
.join(" "),
);
}
}
return texts.slice(-3).join(". ");
}
export default async function handler(
req: NextApiRequest,
res: NextApiResponse,
) {
if (req.method !== "POST") {
return res.status(405).json({ error: "Method not allowed" });
}
try {
const { session, user } = await validateRequest(req);
if (!user || !session) {
return res.status(401).json({ error: "Unauthorized" });
}
const body = req.body;
const messages = body.messages;
const aiId = body.aiId;
const context = (body.context as ChatContext) || {
type: "general" as const,
id: "",
};
// ─── Resolve model ────────────────────────────────────────
let model: any;
if (IS_CLOUD && process.env.CLOUD_ANTHROPIC_API_KEY) {
const anthropic = createAnthropic({
apiKey: process.env.CLOUD_ANTHROPIC_API_KEY,
});
model = anthropic("claude-haiku-4-5-20251001");
} else {
if (!aiId || !messages) {
return res
.status(400)
.json({ error: "Missing aiId or messages" });
}
const aiSettings = await getAiSettingById(aiId);
if (!aiSettings || !aiSettings.isEnabled) {
return res
.status(400)
.json({ error: "AI provider not enabled" });
}
const provider = selectAIProvider(aiSettings);
model = provider(aiSettings.model);
}
if (!messages) {
return res.status(400).json({ error: "Missing messages" });
}
// ─── Resolve tools ────────────────────────────────────────
const protocol = req.headers["x-forwarded-proto"] || "http";
const host = req.headers.host || "localhost:3000";
const toolConfig = {
baseUrl: `${protocol}://${host}/api`,
cookie: req.headers.cookie || "",
};
let tools: Record<string, any>;
let catalogText: string | null = null;
let endpointCount = 0;
const spec = getOpenApiSpec();
if (spec) {
const voyageApiKey = process.env.VOYAGE_API_KEY;
if (!voyageApiKey) {
return res.status(400).json({ error: "VOYAGE_API_KEY is required" });
}
const embeddingsPath = join(process.cwd(), ".tool-embeddings.json");
const allEmbeddings = await getOrCreateEmbeddings(
spec,
voyageApiKey,
embeddingsPath,
);
const userQuery = getUserMessages(messages).trim();
const { operationIds: tagFilteredIds } = buildEndpointCatalog(spec, context.type);
let relevantIds: Set<string> | undefined;
if (userQuery && allEmbeddings.length > 0) {
const topIds = await retrieveRelevantEndpoints(
userQuery,
allEmbeddings,
voyageApiKey,
{ allowedOperationIds: tagFilteredIds, topK: 25 },
);
if (topIds.length > 0) {
relevantIds = new Set(topIds);
}
}
const { catalog, count, operationIds } = buildEndpointCatalog(
spec,
context.type,
relevantIds,
);
catalogText = catalog;
endpointCount = count;
tools = createApiTool(spec, toolConfig, operationIds, 8000);
} else {
tools = getAllTools(context, toolConfig);
}
// ─── Stream response ──────────────────────────────────────
const modelMessages = await convertToModelMessages(messages);
const result = streamText({
model,
system: buildSystemPrompt(context, catalogText, endpointCount),
messages: modelMessages,
tools,
stopWhen: stepCountIs(12),
});
// Disable buffering for streaming
res.setHeader("X-Accel-Buffering", "no");
res.setHeader("Cache-Control", "no-cache, no-transform");
result.pipeUIMessageStreamToResponse(res);
} catch (error) {
console.error("AI chat error:", error);
return res.status(500).json({
error:
error instanceof Error ? error.message : "Internal server error",
});
}
}

View File

@@ -12,6 +12,15 @@ import type { DeploymentJob } from "@/server/queues/queue-types";
import { myQueue } from "@/server/queues/queueSetup"; import { myQueue } from "@/server/queues/queueSetup";
import { deploy } from "@/server/utils/deploy"; 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 * Helper function to get package_version from registry_package events
*/ */
@@ -262,14 +271,15 @@ export default async function handler(
); );
} }
} catch (error) { } catch (error) {
res.status(400).json({ message: "Error deploying Application", error }); logWebhookError("Error deploying Application:", error);
res.status(400).json({ message: "Error deploying Application" });
return; return;
} }
res.status(200).json({ message: "Application deployed successfully" }); res.status(200).json({ message: "Application deployed successfully" });
} catch (error) { } catch (error) {
console.log(error); logWebhookError("Error deploying Application:", error);
res.status(400).json({ message: "Error deploying Application", error }); res.status(400).json({ message: "Error deploying Application" });
} }
} }

View File

@@ -12,6 +12,7 @@ import {
extractCommittedPaths, extractCommittedPaths,
extractHash, extractHash,
getProviderByHeader, getProviderByHeader,
logWebhookError,
} from "../[refreshToken]"; } from "../[refreshToken]";
export default async function handler( export default async function handler(
@@ -195,13 +196,14 @@ export default async function handler(
); );
} }
} catch (error) { } catch (error) {
res.status(400).json({ message: "Error deploying Compose", error }); logWebhookError("Error deploying Compose:", error);
res.status(400).json({ message: "Error deploying Compose" });
return; return;
} }
res.status(200).json({ message: "Compose deployed successfully" }); res.status(200).json({ message: "Compose deployed successfully" });
} catch (error) { } catch (error) {
console.log(error); logWebhookError("Error deploying Compose:", error);
res.status(400).json({ message: "Error deploying Compose", error }); res.status(400).json({ message: "Error deploying Compose" });
} }
} }

View File

@@ -17,7 +17,11 @@ import { applications, compose, github } from "@/server/db/schema";
import type { DeploymentJob } from "@/server/queues/queue-types"; import type { DeploymentJob } from "@/server/queues/queue-types";
import { myQueue } from "@/server/queues/queueSetup"; import { myQueue } from "@/server/queues/queueSetup";
import { deploy } from "@/server/utils/deploy"; import { deploy } from "@/server/utils/deploy";
import { extractCommitMessage, extractHash } from "./[refreshToken]"; import {
extractCommitMessage,
extractHash,
logWebhookError,
} from "./[refreshToken]";
export default async function handler( export default async function handler(
req: NextApiRequest, req: NextApiRequest,
@@ -197,10 +201,8 @@ export default async function handler(
}); });
return; return;
} catch (error) { } catch (error) {
console.error("Error deploying applications on tag:", error); logWebhookError("Error deploying applications on tag:", error);
res res.status(400).json({ message: "Error deploying applications on tag" });
.status(400)
.json({ message: "Error deploying applications on tag", error });
return; return;
} }
} }
@@ -322,7 +324,8 @@ export default async function handler(
} }
res.status(200).json({ message: `Deployed ${totalApps} apps` }); res.status(200).json({ message: `Deployed ${totalApps} apps` });
} catch (error) { } catch (error) {
res.status(400).json({ message: "Error deploying Application", error }); logWebhookError("Error deploying Application:", error);
res.status(400).json({ message: "Error deploying Application" });
} }
} else if (req.headers["x-github-event"] === "pull_request") { } else if (req.headers["x-github-event"] === "pull_request") {
const prId = githubBody?.pull_request?.id; const prId = githubBody?.pull_request?.id;

View File

@@ -40,7 +40,7 @@ function DeploymentsPage() {
return ( return (
<div className="w-full"> <div className="w-full">
<Card className="h-full bg-sidebar p-2.5 rounded-xl max-w-8xl mx-auto min-h-[45vh]"> <Card className="h-full bg-sidebar p-2.5 rounded-xl min-h-[45vh]">
<div className="rounded-xl bg-background shadow-md h-full"> <div className="rounded-xl bg-background shadow-md h-full">
<CardHeader> <CardHeader>
<div className="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between"> <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 { return {
redirect: { redirect: {
permanent: false, permanent: false,
destination: "/dashboard/projects", destination: "/dashboard/home",
}, },
}; };
} }

View File

@@ -24,7 +24,7 @@ export async function getServerSideProps(
return { return {
redirect: { redirect: {
permanent: true, permanent: true,
destination: "/dashboard/projects", destination: "/dashboard/home",
}, },
}; };
} }

View File

@@ -0,0 +1,53 @@
import { validateRequest } from "@dokploy/server/lib/auth";
import { createServerSideHelpers } from "@trpc/react-query/server";
import type { GetServerSidePropsContext } from "next";
import type { ReactElement } from "react";
import superjson from "superjson";
import { ShowHome } from "@/components/dashboard/home/show-home";
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
import { appRouter } from "@/server/api/root";
const Home = () => {
return <ShowHome />;
};
export default Home;
Home.getLayout = (page: ReactElement) => {
return <DashboardLayout>{page}</DashboardLayout>;
};
export async function getServerSideProps(ctx: GetServerSidePropsContext) {
const { req, res } = ctx;
const { user, session } = await validateRequest(req);
if (!user) {
return {
redirect: {
permanent: true,
destination: "/",
},
};
}
const helpers = createServerSideHelpers({
router: appRouter,
ctx: {
req: req as any,
res: res as any,
db: null as any,
session: session as any,
user: user as any,
},
transformer: superjson,
});
await helpers.settings.isCloud.prefetch();
await helpers.user.get.prefetch();
return {
props: {
trpcState: helpers.dehydrate(),
},
};
}

View File

@@ -96,7 +96,7 @@ export async function getServerSideProps(
return { return {
redirect: { redirect: {
permanent: true, permanent: true,
destination: "/dashboard/projects", destination: "/dashboard/home",
}, },
}; };
} }
@@ -122,7 +122,7 @@ export async function getServerSideProps(
return { return {
redirect: { redirect: {
permanent: false, permanent: false,
destination: "/dashboard/projects", destination: "/dashboard/home",
}, },
}; };
} }

View File

@@ -12,6 +12,7 @@ import {
Loader2, Loader2,
Play, Play,
PlusIcon, PlusIcon,
RefreshCw,
Search, Search,
ServerIcon, ServerIcon,
SquareTerminal, SquareTerminal,
@@ -68,6 +69,14 @@ import {
CommandInput, CommandInput,
CommandItem, CommandItem,
} from "@/components/ui/command"; } from "@/components/ui/command";
import {
ContextMenu,
ContextMenuContent,
ContextMenuItem,
ContextMenuLabel,
ContextMenuSeparator,
ContextMenuTrigger,
} from "@/components/ui/context-menu";
import { import {
Dialog, Dialog,
DialogContent, DialogContent,
@@ -424,6 +433,7 @@ const EnvironmentPage = (
const [isBulkDeleteDialogOpen, setIsBulkDeleteDialogOpen] = useState(false); const [isBulkDeleteDialogOpen, setIsBulkDeleteDialogOpen] = useState(false);
const [deleteVolumes, setDeleteVolumes] = useState(false); const [deleteVolumes, setDeleteVolumes] = useState(false);
const [selectedServerId, setSelectedServerId] = useState<string>("all"); const [selectedServerId, setSelectedServerId] = useState<string>("all");
const [serviceToDelete, setServiceToDelete] = useState<Services | null>(null);
const handleSelectAll = () => { const handleSelectAll = () => {
if (selectedServices.length === filteredServices.length) { if (selectedServices.length === filteredServices.length) {
@@ -499,6 +509,14 @@ const EnvironmentPage = (
deploy: api.mongo.deploy.useMutation(), 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 () => { const handleBulkStart = async () => {
let success = 0; let success = 0;
setIsBulkActionLoading(true); setIsBulkActionLoading(true);
@@ -531,6 +549,9 @@ const EnvironmentPage = (
case "mongo": case "mongo":
await mongoActions.start.mutateAsync({ mongoId: serviceId }); await mongoActions.start.mutateAsync({ mongoId: serviceId });
break; break;
case "libsql":
await libsqlActions.start.mutateAsync({ libsqlId: serviceId });
break;
} }
success++; success++;
} catch { } catch {
@@ -578,6 +599,9 @@ const EnvironmentPage = (
case "mongo": case "mongo":
await mongoActions.stop.mutateAsync({ mongoId: serviceId }); await mongoActions.stop.mutateAsync({ mongoId: serviceId });
break; break;
case "libsql":
await libsqlActions.stop.mutateAsync({ libsqlId: serviceId });
break;
} }
success++; success++;
} catch { } catch {
@@ -654,6 +678,12 @@ const EnvironmentPage = (
targetEnvironmentId: selectedTargetEnvironment, targetEnvironmentId: selectedTargetEnvironment,
}); });
break; break;
case "libsql":
await libsqlActions.move.mutateAsync({
libsqlId: serviceId,
targetEnvironmentId: selectedTargetEnvironment,
});
break;
} }
await utils.environment.one.invalidate({ await utils.environment.one.invalidate({
environmentId, environmentId,
@@ -723,6 +753,11 @@ const EnvironmentPage = (
mongoId: serviceId, mongoId: serviceId,
}); });
break; break;
case "libsql":
await libsqlActions.delete.mutateAsync({
libsqlId: serviceId,
});
break;
} }
await utils.environment.one.invalidate({ await utils.environment.one.invalidate({
environmentId, environmentId,
@@ -789,6 +824,11 @@ const EnvironmentPage = (
mongoId: serviceId, mongoId: serviceId,
}); });
break; break;
case "libsql":
await libsqlActions.deploy.mutateAsync({
libsqlId: serviceId,
});
break;
} }
success++; success++;
} catch (error) { } catch (error) {
@@ -814,6 +854,110 @@ const EnvironmentPage = (
setIsBulkActionLoading(false); 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 // Get unique servers from services
const availableServers = useMemo(() => { const availableServers = useMemo(() => {
if (!applications) return []; if (!applications) return [];
@@ -1472,110 +1616,156 @@ const EnvironmentPage = (
<div className="flex w-full flex-col gap-4"> <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"> <div className="gap-5 pb-10 grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-3">
{filteredServices?.map((service) => ( {filteredServices?.map((service) => (
<Link <ContextMenu key={service.id}>
key={service.id} <ContextMenuTrigger asChild>
href={`/dashboard/project/${projectId}/environment/${environmentId}/services/${service.type}/${service.id}`} <Link
className="block" 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)
}
> >
<div className="h-full w-full flex items-center justify-center"> <Card className="flex flex-col group relative cursor-pointer bg-transparent transition-colors hover:bg-border">
<Checkbox {service.serverId && (
checked={selectedServices.includes( <div className="absolute -left-1 -top-2">
service.id, <ServerIcon className="size-4 text-muted-foreground" />
)}
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>
)} )}
<DateTooltip date={service.createdAt}> <div className="absolute -right-1 -top-2">
Created <StatusTooltip status={service.status} />
</DateTooltip> </div>
</div>
</CardFooter> <div
</Card> className={cn(
</Link> "absolute -left-3 -bottom-3 size-9 translate-y-1 rounded-full p-0 transition-all duration-200 z-10 bg-background border",
selectedServices.includes(service.id)
? "opacity-100 translate-y-0"
: "opacity-0 group-hover:translate-y-0 group-hover:opacity-100",
)}
onClick={(e) =>
handleServiceSelect(service.id, e)
}
>
<div className="h-full w-full flex items-center justify-center">
<Checkbox
checked={selectedServices.includes(
service.id,
)}
className="data-[state=checked]:bg-primary"
/>
</div>
</div>
<CardHeader>
<CardTitle className="flex items-center justify-between">
<div className="flex flex-row items-center gap-2 justify-between w-full">
<div className="flex flex-col gap-2">
<span className="text-base flex items-center gap-2 font-medium leading-none flex-wrap">
{service.name}
</span>
{service.description && (
<span className="text-sm font-medium text-muted-foreground">
{service.description}
</span>
)}
</div>
<span className="text-sm font-medium text-muted-foreground self-start">
{service.type === "postgres" && (
<PostgresqlIcon className="h-7 w-7" />
)}
{service.type === "redis" && (
<RedisIcon className="h-7 w-7" />
)}
{service.type === "mariadb" && (
<MariadbIcon className="h-7 w-7" />
)}
{service.type === "mongo" && (
<MongodbIcon className="h-7 w-7" />
)}
{service.type === "mysql" && (
<MysqlIcon className="h-7 w-7" />
)}
{service.type === "application" &&
(service.icon ? (
// biome-ignore lint/performance/noImgElement: application icon is data URL
<img
src={service.icon}
alt={service.name}
className="size-7 object-contain"
/>
) : (
<GlobeIcon className="h-6 w-6" />
))}
{service.type === "compose" && (
<CircuitBoard className="h-6 w-6" />
)}
{service.type === "libsql" && (
<LibsqlIcon className="h-6 w-6" />
)}
</span>
</div>
</CardTitle>
</CardHeader>
<CardFooter className="mt-auto">
<div className="space-y-1 text-sm w-full">
{service.serverName && (
<div className="flex items-center gap-1.5 text-xs text-muted-foreground mb-1">
<ServerIcon className="size-3" />
<span className="truncate">
{service.serverName}
</span>
</div>
)}
<DateTooltip date={service.createdAt}>
Created
</DateTooltip>
</div>
</CardFooter>
</Card>
</Link>
</ContextMenuTrigger>
{service.type !== "libsql" && (
<ContextMenuContent className="w-48">
<ContextMenuLabel className="truncate">
{service.name}
</ContextMenuLabel>
<ContextMenuSeparator />
<ContextMenuItem
className="flex items-center gap-2"
onClick={() =>
handleServiceAction(service, "start")
}
>
<Play className="size-4" />
Start
</ContextMenuItem>
<ContextMenuItem
className="flex items-center gap-2"
onClick={() =>
handleServiceAction(service, "deploy")
}
>
<RefreshCw className="size-4" />
Deploy
</ContextMenuItem>
<ContextMenuItem
className="flex items-center gap-2 text-orange-500 focus:text-orange-500"
onClick={() =>
handleServiceAction(service, "stop")
}
>
<Ban className="size-4" />
Stop
</ContextMenuItem>
<ContextMenuSeparator />
<ContextMenuItem
className="flex items-center gap-2 text-red-500 focus:text-red-500"
onClick={() => setServiceToDelete(service)}
>
<Trash2 className="size-4" />
Delete
</ContextMenuItem>
</ContextMenuContent>
)}
</ContextMenu>
))} ))}
</div> </div>
</div> </div>
@@ -1586,6 +1776,38 @@ const EnvironmentPage = (
</div> </div>
</Card> </Card>
</div> </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> </div>
); );
}; };
@@ -1664,7 +1886,7 @@ export async function getServerSideProps(
return { return {
redirect: { redirect: {
permanent: false, permanent: false,
destination: "/dashboard/projects", destination: "/dashboard/home",
}, },
}; };
} }

View File

@@ -490,7 +490,7 @@ export async function getServerSideProps(
return { return {
redirect: { redirect: {
permanent: false, permanent: false,
destination: "/dashboard/projects", destination: "/dashboard/home",
}, },
}; };
} }

View File

@@ -22,6 +22,7 @@ import { ShowSchedules } from "@/components/dashboard/application/schedules/show
import { ShowVolumeBackups } from "@/components/dashboard/application/volume-backups/show-volume-backups"; import { ShowVolumeBackups } from "@/components/dashboard/application/volume-backups/show-volume-backups";
import { AddCommandCompose } from "@/components/dashboard/compose/advanced/add-command"; import { AddCommandCompose } from "@/components/dashboard/compose/advanced/add-command";
import { IsolatedDeploymentTab } from "@/components/dashboard/compose/advanced/add-isolation"; 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 { DeleteService } from "@/components/dashboard/compose/delete-service";
import { ShowGeneralCompose } from "@/components/dashboard/compose/general/show"; import { ShowGeneralCompose } from "@/components/dashboard/compose/general/show";
import { ShowDockerLogsCompose } from "@/components/dashboard/compose/logs/show"; import { ShowDockerLogsCompose } from "@/components/dashboard/compose/logs/show";
@@ -60,6 +61,7 @@ type TabState =
| "advanced" | "advanced"
| "deployments" | "deployments"
| "domains" | "domains"
| "containers"
| "monitoring" | "monitoring"
| "volumeBackups"; | "volumeBackups";
@@ -231,6 +233,9 @@ const Service = (
Deployments Deployments
</TabsTrigger> </TabsTrigger>
)} )}
{permissions?.service.read && (
<TabsTrigger value="containers">Containers</TabsTrigger>
)}
{permissions?.service.create && ( {permissions?.service.create && (
<TabsTrigger value="backups">Backups</TabsTrigger> <TabsTrigger value="backups">Backups</TabsTrigger>
)} )}
@@ -298,6 +303,18 @@ const Service = (
</div> </div>
</TabsContent> </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 && ( {permissions?.monitoring.read && (
<TabsContent value="monitoring"> <TabsContent value="monitoring">
<div className="pt-2.5"> <div className="pt-2.5">
@@ -475,7 +492,7 @@ export async function getServerSideProps(
return { return {
redirect: { redirect: {
permanent: false, permanent: false,
destination: "/dashboard/projects", destination: "/dashboard/home",
}, },
}; };
} }

View File

@@ -343,7 +343,7 @@ export async function getServerSideProps(
return { return {
redirect: { redirect: {
permanent: false, permanent: false,
destination: "/dashboard/projects", destination: "/dashboard/home",
}, },
}; };
} }

View File

@@ -372,7 +372,7 @@ export async function getServerSideProps(
return { return {
redirect: { redirect: {
permanent: false, permanent: false,
destination: "/dashboard/projects", destination: "/dashboard/home",
}, },
}; };
} }

View File

@@ -376,7 +376,7 @@ export async function getServerSideProps(
return { return {
redirect: { redirect: {
permanent: false, permanent: false,
destination: "/dashboard/projects", destination: "/dashboard/home",
}, },
}; };
} }

View File

@@ -353,7 +353,7 @@ export async function getServerSideProps(
return { return {
redirect: { redirect: {
permanent: false, permanent: false,
destination: "/dashboard/projects", destination: "/dashboard/home",
}, },
}; };
} }

View File

@@ -360,7 +360,7 @@ export async function getServerSideProps(
return { return {
redirect: { redirect: {
permanent: false, permanent: false,
destination: "/dashboard/projects", destination: "/dashboard/home",
}, },
}; };
} }

View File

@@ -363,7 +363,7 @@ export async function getServerSideProps(
return { return {
redirect: { redirect: {
permanent: false, permanent: false,
destination: "/dashboard/projects", destination: "/dashboard/home",
}, },
}; };
} }

View File

@@ -18,7 +18,7 @@ export async function getServerSideProps(
return { return {
redirect: { redirect: {
permanent: true, permanent: true,
destination: "/dashboard/projects", destination: "/dashboard/home",
}, },
}; };
} }

View File

@@ -35,7 +35,7 @@ export async function getServerSideProps(
return { return {
redirect: { redirect: {
permanent: true, permanent: true,
destination: "/dashboard/projects", destination: "/dashboard/home",
}, },
}; };
} }

View File

@@ -24,7 +24,7 @@ export async function getServerSideProps(
return { return {
redirect: { redirect: {
permanent: true, permanent: true,
destination: "/dashboard/projects", destination: "/dashboard/home",
}, },
}; };
} }

View File

@@ -28,7 +28,7 @@ export async function getServerSideProps(
return { return {
redirect: { redirect: {
permanent: true, permanent: true,
destination: "/dashboard/projects", destination: "/dashboard/home",
}, },
}; };
} }

View File

@@ -24,7 +24,7 @@ export async function getServerSideProps(
return { return {
redirect: { redirect: {
permanent: true, permanent: true,
destination: "/dashboard/projects", destination: "/dashboard/home",
}, },
}; };
} }

View File

@@ -45,7 +45,7 @@ export async function getServerSideProps(
return { return {
redirect: { redirect: {
permanent: true, permanent: true,
destination: "/dashboard/projects", destination: "/dashboard/home",
}, },
}; };
} }

View File

@@ -46,7 +46,7 @@ export async function getServerSideProps(
return { return {
redirect: { redirect: {
permanent: true, permanent: true,
destination: "/dashboard/projects", destination: "/dashboard/home",
}, },
}; };
} }

View File

@@ -24,7 +24,7 @@ export async function getServerSideProps(
return { return {
redirect: { redirect: {
permanent: true, permanent: true,
destination: "/dashboard/projects", destination: "/dashboard/home",
}, },
}; };
} }

View File

@@ -82,6 +82,16 @@ export default function Home({ IS_CLOUD }: Props) {
}); });
if (error) { 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); toast.error(error.message);
setError(error.message || "An error occurred while logging in"); setError(error.message || "An error occurred while logging in");
return; return;
@@ -96,7 +106,7 @@ export default function Home({ IS_CLOUD }: Props) {
} }
toast.success("Logged in successfully"); toast.success("Logged in successfully");
router.push("/dashboard/projects"); router.push("/dashboard/home");
} catch { } catch {
toast.error("An error occurred while logging in"); toast.error("An error occurred while logging in");
} finally { } finally {
@@ -123,7 +133,7 @@ export default function Home({ IS_CLOUD }: Props) {
} }
toast.success("Logged in successfully"); toast.success("Logged in successfully");
router.push("/dashboard/projects"); router.push("/dashboard/home");
} catch { } catch {
toast.error("An error occurred while verifying 2FA code"); toast.error("An error occurred while verifying 2FA code");
} finally { } finally {
@@ -153,7 +163,7 @@ export default function Home({ IS_CLOUD }: Props) {
} }
toast.success("Logged in successfully"); toast.success("Logged in successfully");
router.push("/dashboard/projects"); router.push("/dashboard/home");
} catch { } catch {
toast.error("An error occurred while verifying backup code"); toast.error("An error occurred while verifying backup code");
} finally { } finally {
@@ -398,7 +408,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
return { return {
redirect: { redirect: {
permanent: true, permanent: true,
destination: "/dashboard/projects", destination: "/dashboard/home",
}, },
}; };
} }
@@ -427,7 +437,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
return { return {
redirect: { redirect: {
permanent: true, permanent: true,
destination: "/dashboard/projects", destination: "/dashboard/home",
}, },
}; };
} }

View File

@@ -139,7 +139,7 @@ const Invitation = ({
}); });
toast.success("Account created successfully"); toast.success("Account created successfully");
router.push("/dashboard/projects"); router.push("/dashboard/home");
} catch { } catch {
toast.error("An error occurred while creating your account"); toast.error("An error occurred while creating your account");
} }

View File

@@ -303,7 +303,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
return { return {
redirect: { redirect: {
permanent: true, permanent: true,
destination: "/dashboard/projects", destination: "/dashboard/home",
}, },
}; };
} }

View File

@@ -10,12 +10,6 @@ import { adminProcedure, createTRPCRouter } from "../trpc";
export const adminRouter = createTRPCRouter({ export const adminRouter = createTRPCRouter({
setupMonitoring: adminProcedure 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) .input(apiUpdateWebServerMonitoring)
.mutation(async ({ input }) => { .mutation(async ({ input }) => {
try { try {

View File

@@ -41,24 +41,12 @@ import { generatePassword } from "@/templates/utils";
export const aiRouter = createTRPCRouter({ export const aiRouter = createTRPCRouter({
one: adminProcedure 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() })) .input(z.object({ aiId: z.string() }))
.query(async ({ input }) => { .query(async ({ input }) => {
return await getAiSettingById(input.aiId); return await getAiSettingById(input.aiId);
}), }),
getModels: protectedProcedure 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() })) .input(z.object({ apiUrl: z.string().min(1), apiKey: z.string() }))
.query(async ({ input }) => { .query(async ({ input }) => {
try { try {
@@ -186,75 +174,33 @@ export const aiRouter = createTRPCRouter({
}); });
} }
}), }),
create: adminProcedure create: adminProcedure.input(apiCreateAi).mutation(async ({ ctx, input }) => {
.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); return await saveAiSettings(ctx.session.activeOrganizationId, input);
}), }),
update: adminProcedure update: adminProcedure.input(apiUpdateAi).mutation(async ({ ctx, input }) => {
.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); return await saveAiSettings(ctx.session.activeOrganizationId, input);
}), }),
getAll: adminProcedure getAll: adminProcedure.query(async ({ ctx }) => {
.meta({
openapi: {
summary: "List all AI providers",
description: "Returns all AI provider configurations for the current organization.",
},
})
.query(async ({ ctx }) => {
return await getAiSettingsByOrganizationId( return await getAiSettingsByOrganizationId(
ctx.session.activeOrganizationId, ctx.session.activeOrganizationId,
); );
}), }),
get: adminProcedure get: adminProcedure
.meta({
openapi: {
summary: "Get AI provider",
description: "Returns a single AI provider configuration by its ID.",
},
})
.input(z.object({ aiId: z.string() })) .input(z.object({ aiId: z.string() }))
.query(async ({ input }) => { .query(async ({ input }) => {
return await getAiSettingById(input.aiId); return await getAiSettingById(input.aiId);
}), }),
delete: adminProcedure delete: adminProcedure
.meta({
openapi: {
summary: "Delete AI provider",
description: "Removes an AI provider configuration by its ID.",
},
})
.input(z.object({ aiId: z.string() })) .input(z.object({ aiId: z.string() }))
.mutation(async ({ input }) => { .mutation(async ({ input }) => {
return await deleteAiSettings(input.aiId); return await deleteAiSettings(input.aiId);
}), }),
getEnabledProviders: protectedProcedure getEnabledProviders: protectedProcedure.query(async ({ ctx }) => {
.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( const settings = await getAiSettingsByOrganizationId(
ctx.session.activeOrganizationId, ctx.session.activeOrganizationId,
); );
@@ -264,12 +210,6 @@ export const aiRouter = createTRPCRouter({
}), }),
analyzeLogs: protectedProcedure 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( .input(
z.object({ z.object({
aiId: z.string().min(1), aiId: z.string().min(1),
@@ -328,12 +268,6 @@ ${input.logs}`,
}), }),
testConnection: protectedProcedure 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( .input(
z.object({ z.object({
apiUrl: z.string().min(1), apiUrl: z.string().min(1),
@@ -368,12 +302,6 @@ ${input.logs}`,
}), }),
suggest: protectedProcedure 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( .input(
z.object({ z.object({
aiId: z.string(), aiId: z.string(),
@@ -395,12 +323,6 @@ ${input.logs}`,
} }
}), }),
deploy: protectedProcedure 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) .input(deploySuggestionSchema)
.mutation(async ({ ctx, input }) => { .mutation(async ({ ctx, input }) => {
const environment = await findEnvironmentById(input.environmentId); const environment = await findEnvironmentById(input.environmentId);

View File

@@ -79,12 +79,6 @@ import { cancelDeployment, deploy } from "@/server/utils/deploy";
export const applicationRouter = createTRPCRouter({ export const applicationRouter = createTRPCRouter({
create: protectedProcedure 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) .input(apiCreateApplication)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
try { try {
@@ -140,12 +134,6 @@ export const applicationRouter = createTRPCRouter({
} }
}), }),
one: protectedProcedure 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) .input(apiFindOneApplication)
.query(async ({ input, ctx }) => { .query(async ({ input, ctx }) => {
await checkServiceAccess(ctx, input.applicationId, "read"); await checkServiceAccess(ctx, input.applicationId, "read");
@@ -201,12 +189,6 @@ export const applicationRouter = createTRPCRouter({
}), }),
reload: protectedProcedure 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) .input(apiReloadApplication)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.applicationId, { await checkServicePermissionAndAccess(ctx, input.applicationId, {
@@ -236,12 +218,6 @@ export const applicationRouter = createTRPCRouter({
}), }),
delete: protectedProcedure 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) .input(apiFindOneApplication)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
await checkServiceAccess(ctx, input.applicationId, "delete"); await checkServiceAccess(ctx, input.applicationId, "delete");
@@ -303,12 +279,6 @@ export const applicationRouter = createTRPCRouter({
}), }),
stop: protectedProcedure 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) .input(apiFindOneApplication)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.applicationId, { await checkServicePermissionAndAccess(ctx, input.applicationId, {
@@ -331,12 +301,6 @@ export const applicationRouter = createTRPCRouter({
}), }),
start: protectedProcedure start: protectedProcedure
.meta({
openapi: {
summary: "Start an application",
description: "Starts the Docker service for the application and sets its status to done.",
},
})
.input(apiFindOneApplication) .input(apiFindOneApplication)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.applicationId, { await checkServicePermissionAndAccess(ctx, input.applicationId, {
@@ -359,12 +323,6 @@ export const applicationRouter = createTRPCRouter({
}), }),
redeploy: protectedProcedure 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) .input(apiRedeployApplication)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.applicationId, { await checkServicePermissionAndAccess(ctx, input.applicationId, {
@@ -409,12 +367,6 @@ export const applicationRouter = createTRPCRouter({
}); });
}), }),
saveEnvironment: protectedProcedure saveEnvironment: protectedProcedure
.meta({
openapi: {
summary: "Save environment variables",
description: "Updates the environment variables, build arguments, and build secrets for an application.",
},
})
.input(apiSaveEnvironmentVariables) .input(apiSaveEnvironmentVariables)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.applicationId, { await checkServicePermissionAndAccess(ctx, input.applicationId, {
@@ -436,12 +388,6 @@ export const applicationRouter = createTRPCRouter({
return true; return true;
}), }),
saveBuildType: protectedProcedure 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) .input(apiSaveBuildType)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.applicationId, { await checkServicePermissionAndAccess(ctx, input.applicationId, {
@@ -467,12 +413,6 @@ export const applicationRouter = createTRPCRouter({
return true; return true;
}), }),
saveGithubProvider: protectedProcedure 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) .input(apiSaveGithubProvider)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.applicationId, { await checkServicePermissionAndAccess(ctx, input.applicationId, {
@@ -500,12 +440,6 @@ export const applicationRouter = createTRPCRouter({
return true; return true;
}), }),
saveGitlabProvider: protectedProcedure 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) .input(apiSaveGitlabProvider)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.applicationId, { await checkServicePermissionAndAccess(ctx, input.applicationId, {
@@ -534,12 +468,6 @@ export const applicationRouter = createTRPCRouter({
return true; return true;
}), }),
saveBitbucketProvider: protectedProcedure 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) .input(apiSaveBitbucketProvider)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.applicationId, { await checkServicePermissionAndAccess(ctx, input.applicationId, {
@@ -567,12 +495,6 @@ export const applicationRouter = createTRPCRouter({
return true; return true;
}), }),
saveGiteaProvider: protectedProcedure 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) .input(apiSaveGiteaProvider)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.applicationId, { await checkServicePermissionAndAccess(ctx, input.applicationId, {
@@ -599,12 +521,6 @@ export const applicationRouter = createTRPCRouter({
return true; return true;
}), }),
saveDockerProvider: protectedProcedure 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) .input(apiSaveDockerProvider)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.applicationId, { await checkServicePermissionAndAccess(ctx, input.applicationId, {
@@ -628,12 +544,6 @@ export const applicationRouter = createTRPCRouter({
return true; return true;
}), }),
saveGitProvider: protectedProcedure 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) .input(apiSaveGitProvider)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.applicationId, { await checkServicePermissionAndAccess(ctx, input.applicationId, {
@@ -659,12 +569,6 @@ export const applicationRouter = createTRPCRouter({
return true; return true;
}), }),
disconnectGitProvider: protectedProcedure 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) .input(apiFindOneApplication)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.applicationId, { await checkServicePermissionAndAccess(ctx, input.applicationId, {
@@ -718,12 +622,6 @@ export const applicationRouter = createTRPCRouter({
return true; return true;
}), }),
markRunning: protectedProcedure 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) .input(apiFindOneApplication)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.applicationId, { await checkServicePermissionAndAccess(ctx, input.applicationId, {
@@ -739,12 +637,6 @@ export const applicationRouter = createTRPCRouter({
}); });
}), }),
update: protectedProcedure 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) .input(apiUpdateApplication)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.applicationId, { await checkServicePermissionAndAccess(ctx, input.applicationId, {
@@ -781,12 +673,6 @@ export const applicationRouter = createTRPCRouter({
return true; return true;
}), }),
refreshToken: protectedProcedure 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) .input(apiFindOneApplication)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.applicationId, { await checkServicePermissionAndAccess(ctx, input.applicationId, {
@@ -805,12 +691,6 @@ export const applicationRouter = createTRPCRouter({
return true; return true;
}), }),
deploy: protectedProcedure 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) .input(apiDeployApplication)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.applicationId, { await checkServicePermissionAndAccess(ctx, input.applicationId, {
@@ -855,12 +735,6 @@ export const applicationRouter = createTRPCRouter({
}), }),
cleanQueues: protectedProcedure cleanQueues: protectedProcedure
.meta({
openapi: {
summary: "Clean deployment queues",
description: "Removes all pending deployment jobs from the queue for the specified application.",
},
})
.input(apiFindOneApplication) .input(apiFindOneApplication)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.applicationId, { await checkServicePermissionAndAccess(ctx, input.applicationId, {
@@ -869,12 +743,6 @@ export const applicationRouter = createTRPCRouter({
await cleanQueuesByApplication(input.applicationId); await cleanQueuesByApplication(input.applicationId);
}), }),
clearDeployments: protectedProcedure 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) .input(apiFindOneApplication)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.applicationId, { await checkServicePermissionAndAccess(ctx, input.applicationId, {
@@ -891,12 +759,6 @@ export const applicationRouter = createTRPCRouter({
return true; return true;
}), }),
killBuild: protectedProcedure killBuild: protectedProcedure
.meta({
openapi: {
summary: "Kill active build",
description: "Forcefully terminates the currently running Docker build process for the application.",
},
})
.input(apiFindOneApplication) .input(apiFindOneApplication)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.applicationId, { await checkServicePermissionAndAccess(ctx, input.applicationId, {
@@ -912,12 +774,6 @@ export const applicationRouter = createTRPCRouter({
}); });
}), }),
readTraefikConfig: protectedProcedure 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) .input(apiFindOneApplication)
.query(async ({ input, ctx }) => { .query(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.applicationId, { await checkServicePermissionAndAccess(ctx, input.applicationId, {
@@ -937,12 +793,6 @@ export const applicationRouter = createTRPCRouter({
}), }),
dropDeployment: protectedProcedure 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( .input(
zfd.formData({ zfd.formData({
applicationId: z.string(), applicationId: z.string(),
@@ -999,12 +849,6 @@ export const applicationRouter = createTRPCRouter({
return true; return true;
}), }),
updateTraefikConfig: protectedProcedure 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() })) .input(z.object({ applicationId: z.string(), traefikConfig: z.string() }))
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.applicationId, { await checkServicePermissionAndAccess(ctx, input.applicationId, {
@@ -1029,12 +873,6 @@ export const applicationRouter = createTRPCRouter({
return true; return true;
}), }),
readAppMonitoring: withPermission("monitoring", "read") 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) .input(apiFindMonitoringStats)
.query(async ({ input }) => { .query(async ({ input }) => {
if (IS_CLOUD) { if (IS_CLOUD) {
@@ -1048,12 +886,6 @@ export const applicationRouter = createTRPCRouter({
return stats; return stats;
}), }),
move: protectedProcedure 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( .input(
z.object({ z.object({
applicationId: z.string(), applicationId: z.string(),
@@ -1090,12 +922,6 @@ export const applicationRouter = createTRPCRouter({
}), }),
cancelDeployment: protectedProcedure 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) .input(apiFindOneApplication)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.applicationId, { await checkServicePermissionAndAccess(ctx, input.applicationId, {
@@ -1146,12 +972,6 @@ export const applicationRouter = createTRPCRouter({
}), }),
search: protectedProcedure 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( .input(
z.object({ z.object({
q: z.string().optional(), q: z.string().optional(),
@@ -1284,12 +1104,6 @@ export const applicationRouter = createTRPCRouter({
}), }),
readLogs: protectedProcedure 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( .input(
apiFindOneApplication.extend({ apiFindOneApplication.extend({
tail: z.number().int().min(1).max(10000).default(100), tail: z.number().int().min(1).max(10000).default(100),

View File

@@ -78,12 +78,6 @@ interface RcloneFile {
export const backupRouter = createTRPCRouter({ export const backupRouter = createTRPCRouter({
create: protectedProcedure 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) .input(apiCreateBackup)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
try { try {
@@ -158,12 +152,6 @@ export const backupRouter = createTRPCRouter({
} }
}), }),
one: protectedProcedure one: protectedProcedure
.meta({
openapi: {
summary: "Get a backup",
description: "Returns the details of a specific backup configuration by its ID.",
},
})
.input(apiFindOneBackup) .input(apiFindOneBackup)
.query(async ({ input, ctx }) => { .query(async ({ input, ctx }) => {
const backup = await findBackupById(input.backupId); const backup = await findBackupById(input.backupId);
@@ -184,12 +172,6 @@ export const backupRouter = createTRPCRouter({
return backup; return backup;
}), }),
update: protectedProcedure 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) .input(apiUpdateBackup)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
try { try {
@@ -247,12 +229,6 @@ export const backupRouter = createTRPCRouter({
} }
}), }),
remove: protectedProcedure remove: protectedProcedure
.meta({
openapi: {
summary: "Delete a backup",
description: "Permanently removes a backup configuration and unschedules any associated backup job.",
},
})
.input(apiRemoveBackup) .input(apiRemoveBackup)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
try { try {
@@ -296,12 +272,6 @@ export const backupRouter = createTRPCRouter({
} }
}), }),
manualBackupPostgres: protectedProcedure 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) .input(apiFindOneBackup)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
try { try {
@@ -333,12 +303,6 @@ export const backupRouter = createTRPCRouter({
}), }),
manualBackupMySql: protectedProcedure 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) .input(apiFindOneBackup)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
try { try {
@@ -366,12 +330,6 @@ export const backupRouter = createTRPCRouter({
} }
}), }),
manualBackupMariadb: protectedProcedure 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) .input(apiFindOneBackup)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
try { try {
@@ -399,12 +357,6 @@ export const backupRouter = createTRPCRouter({
} }
}), }),
manualBackupCompose: protectedProcedure 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) .input(apiFindOneBackup)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
try { try {
@@ -432,12 +384,6 @@ export const backupRouter = createTRPCRouter({
} }
}), }),
manualBackupMongo: protectedProcedure 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) .input(apiFindOneBackup)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
try { try {
@@ -465,12 +411,6 @@ export const backupRouter = createTRPCRouter({
} }
}), }),
manualBackupLibsql: protectedProcedure 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) .input(apiFindOneBackup)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
try { try {
@@ -498,12 +438,6 @@ export const backupRouter = createTRPCRouter({
} }
}), }),
manualBackupWebServer: withPermission("backup", "create") 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) .input(apiFindOneBackup)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
const backup = await findBackupById(input.backupId); const backup = await findBackupById(input.backupId);
@@ -517,12 +451,6 @@ export const backupRouter = createTRPCRouter({
return true; return true;
}), }),
listBackupFiles: withPermission("backup", "read") 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( .input(
z.object({ z.object({
destinationId: z.string(), destinationId: z.string(),
@@ -530,9 +458,26 @@ export const backupRouter = createTRPCRouter({
serverId: z.string().optional(), serverId: z.string().optional(),
}), }),
) )
.query(async ({ input }) => { .query(async ({ input, ctx }) => {
try { try {
const destination = await findDestinationById(input.destinationId); 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 rcloneFlags = getS3Credentials(destination);
const bucketPath = `:s3:${destination.bucket}`; const bucketPath = `:s3:${destination.bucket}`;

View File

@@ -25,12 +25,6 @@ import {
export const bitbucketRouter = createTRPCRouter({ export const bitbucketRouter = createTRPCRouter({
create: withPermission("gitProviders", "create") 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) .input(apiCreateBitbucket)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
try { try {
@@ -56,24 +50,11 @@ export const bitbucketRouter = createTRPCRouter({
} }
}), }),
one: protectedProcedure one: protectedProcedure
.meta({
openapi: {
summary: "Get Bitbucket provider",
description: "Returns a single Bitbucket provider configuration by its ID.",
},
})
.input(apiFindOneBitbucket) .input(apiFindOneBitbucket)
.query(async ({ input }) => { .query(async ({ input }) => {
return await findBitbucketById(input.bitbucketId); return await findBitbucketById(input.bitbucketId);
}), }),
bitbucketProviders: protectedProcedure bitbucketProviders: protectedProcedure.query(async ({ ctx }) => {
.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); const accessibleIds = await getAccessibleGitProviderIds(ctx.session);
let result = await db.query.bitbucket.findMany({ let result = await db.query.bitbucket.findMany({
@@ -96,34 +77,16 @@ export const bitbucketRouter = createTRPCRouter({
}), }),
getBitbucketRepositories: protectedProcedure 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) .input(apiFindOneBitbucket)
.query(async ({ input }) => { .query(async ({ input }) => {
return await getBitbucketRepositories(input.bitbucketId); return await getBitbucketRepositories(input.bitbucketId);
}), }),
getBitbucketBranches: protectedProcedure 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) .input(apiFindBitbucketBranches)
.query(async ({ input }) => { .query(async ({ input }) => {
return await getBitbucketBranches(input); return await getBitbucketBranches(input);
}), }),
testConnection: protectedProcedure 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) .input(apiBitbucketTestConnection)
.mutation(async ({ input }) => { .mutation(async ({ input }) => {
try { try {
@@ -138,12 +101,6 @@ export const bitbucketRouter = createTRPCRouter({
} }
}), }),
update: withPermission("gitProviders", "create") update: withPermission("gitProviders", "create")
.meta({
openapi: {
summary: "Update Bitbucket provider",
description: "Updates a Bitbucket provider configuration. Requires gitProviders create permission.",
},
})
.input(apiUpdateBitbucket) .input(apiUpdateBitbucket)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
const result = await updateBitbucket(input.bitbucketId, { const result = await updateBitbucket(input.bitbucketId, {

View File

@@ -19,12 +19,6 @@ import {
export const certificateRouter = createTRPCRouter({ export const certificateRouter = createTRPCRouter({
create: withPermission("certificate", "create") 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) .input(apiCreateCertificate)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
if (IS_CLOUD && !input.serverId) { if (IS_CLOUD && !input.serverId) {
@@ -47,12 +41,6 @@ export const certificateRouter = createTRPCRouter({
}), }),
one: withPermission("certificate", "read") 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) .input(apiFindCertificate)
.query(async ({ input, ctx }) => { .query(async ({ input, ctx }) => {
const certificates = await findCertificateById(input.certificateId); const certificates = await findCertificateById(input.certificateId);
@@ -65,12 +53,6 @@ export const certificateRouter = createTRPCRouter({
return certificates; return certificates;
}), }),
remove: withPermission("certificate", "delete") 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) .input(apiFindCertificate)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
const certificates = await findCertificateById(input.certificateId); const certificates = await findCertificateById(input.certificateId);
@@ -89,14 +71,7 @@ export const certificateRouter = createTRPCRouter({
await removeCertificateById(input.certificateId); await removeCertificateById(input.certificateId);
return true; return true;
}), }),
all: withPermission("certificate", "read") all: withPermission("certificate", "read").query(async ({ ctx }) => {
.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({ return await db.query.certificates.findMany({
where: eq(certificates.organizationId, ctx.session.activeOrganizationId), where: eq(certificates.organizationId, ctx.session.activeOrganizationId),
with: { with: {
@@ -105,12 +80,6 @@ export const certificateRouter = createTRPCRouter({
}); });
}), }),
update: withPermission("certificate", "update") 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) .input(apiUpdateCertificate)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
const certificate = await findCertificateById(input.certificateId); const certificate = await findCertificateById(input.certificateId);

View File

@@ -13,30 +13,27 @@ import { createTRPCRouter, withPermission } from "../trpc";
export const clusterRouter = createTRPCRouter({ export const clusterRouter = createTRPCRouter({
getNodes: withPermission("server", "read") 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( .input(
z.object({ z.object({
serverId: z.string().optional(), serverId: z.string().optional(),
}), }),
) )
.query(async ({ input }) => { .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.",
});
}
}
const docker = await getRemoteDocker(input.serverId); const docker = await getRemoteDocker(input.serverId);
const workers: DockerNode[] = await docker.listNodes(); const workers: DockerNode[] = await docker.listNodes();
return workers; return workers;
}), }),
removeWorker: withPermission("server", "delete") 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( .input(
z.object({ z.object({
nodeId: z.string(), nodeId: z.string(),
@@ -44,6 +41,15 @@ export const clusterRouter = createTRPCRouter({
}), }),
) )
.mutation(async ({ input, ctx }) => { .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 { try {
const drainCommand = `docker node update --availability drain ${input.nodeId}`; const drainCommand = `docker node update --availability drain ${input.nodeId}`;
const removeCommand = `docker node rm ${input.nodeId} --force`; const removeCommand = `docker node rm ${input.nodeId} --force`;
@@ -72,18 +78,21 @@ export const clusterRouter = createTRPCRouter({
}), }),
addWorker: withPermission("server", "create") 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( .input(
z.object({ z.object({
serverId: z.string().optional(), serverId: z.string().optional(),
}), }),
) )
.query(async ({ input }) => { .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.",
});
}
}
const docker = await getRemoteDocker(input.serverId); const docker = await getRemoteDocker(input.serverId);
const result = await docker.swarmInspect(); const result = await docker.swarmInspect();
const docker_version = await docker.version(); const docker_version = await docker.version();
@@ -101,18 +110,21 @@ export const clusterRouter = createTRPCRouter({
}), }),
addManager: withPermission("server", "create") 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( .input(
z.object({ z.object({
serverId: z.string().optional(), serverId: z.string().optional(),
}), }),
) )
.query(async ({ input }) => { .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.",
});
}
}
const docker = await getRemoteDocker(input.serverId); const docker = await getRemoteDocker(input.serverId);
const result = await docker.swarmInspect(); const result = await docker.swarmInspect();
const docker_version = await docker.version(); const docker_version = await docker.version();

View File

@@ -83,12 +83,6 @@ import { audit } from "../utils/audit";
export const composeRouter = createTRPCRouter({ export const composeRouter = createTRPCRouter({
create: protectedProcedure 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) .input(apiCreateCompose)
.mutation(async ({ ctx, input }) => { .mutation(async ({ ctx, input }) => {
try { try {
@@ -139,12 +133,6 @@ export const composeRouter = createTRPCRouter({
}), }),
one: protectedProcedure 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) .input(apiFindCompose)
.query(async ({ input, ctx }) => { .query(async ({ input, ctx }) => {
await checkServiceAccess(ctx, input.composeId, "read"); await checkServiceAccess(ctx, input.composeId, "read");
@@ -201,12 +189,6 @@ export const composeRouter = createTRPCRouter({
}), }),
update: protectedProcedure 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) .input(apiUpdateCompose)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.composeId, { await checkServicePermissionAndAccess(ctx, input.composeId, {
@@ -222,12 +204,6 @@ export const composeRouter = createTRPCRouter({
return updated; return updated;
}), }),
saveEnvironment: protectedProcedure saveEnvironment: protectedProcedure
.meta({
openapi: {
summary: "Save compose environment variables",
description: "Updates the environment variables for a compose service.",
},
})
.input(apiSaveEnvironmentVariablesCompose) .input(apiSaveEnvironmentVariablesCompose)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.composeId, { await checkServicePermissionAndAccess(ctx, input.composeId, {
@@ -253,12 +229,6 @@ export const composeRouter = createTRPCRouter({
return true; return true;
}), }),
delete: protectedProcedure 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) .input(apiDeleteCompose)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
await checkServiceAccess(ctx, input.composeId, "delete"); await checkServiceAccess(ctx, input.composeId, "delete");
@@ -309,12 +279,6 @@ export const composeRouter = createTRPCRouter({
return composeResult; return composeResult;
}), }),
cleanQueues: protectedProcedure cleanQueues: protectedProcedure
.meta({
openapi: {
summary: "Clean deployment queues",
description: "Removes all pending deployment jobs from the queue for the specified compose service.",
},
})
.input(apiFindCompose) .input(apiFindCompose)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.composeId, { await checkServicePermissionAndAccess(ctx, input.composeId, {
@@ -324,12 +288,6 @@ export const composeRouter = createTRPCRouter({
return { success: true, message: "Queues cleaned successfully" }; return { success: true, message: "Queues cleaned successfully" };
}), }),
clearDeployments: protectedProcedure 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) .input(apiFindCompose)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.composeId, { await checkServicePermissionAndAccess(ctx, input.composeId, {
@@ -346,12 +304,6 @@ export const composeRouter = createTRPCRouter({
return true; return true;
}), }),
killBuild: protectedProcedure killBuild: protectedProcedure
.meta({
openapi: {
summary: "Kill active build",
description: "Forcefully terminates the currently running Docker build process for the compose service.",
},
})
.input(apiFindCompose) .input(apiFindCompose)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.composeId, { await checkServicePermissionAndAccess(ctx, input.composeId, {
@@ -362,12 +314,6 @@ export const composeRouter = createTRPCRouter({
}), }),
loadServices: protectedProcedure 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) .input(apiFetchServices)
.query(async ({ input, ctx }) => { .query(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.composeId, { await checkServicePermissionAndAccess(ctx, input.composeId, {
@@ -376,12 +322,6 @@ export const composeRouter = createTRPCRouter({
return await loadServices(input.composeId, input.type); return await loadServices(input.composeId, input.type);
}), }),
loadMountsByService: protectedProcedure 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( .input(
z.object({ z.object({
composeId: z.string().min(1), composeId: z.string().min(1),
@@ -400,12 +340,6 @@ export const composeRouter = createTRPCRouter({
return mounts; return mounts;
}), }),
fetchSourceType: protectedProcedure 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) .input(apiFindCompose)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
try { try {
@@ -431,12 +365,6 @@ export const composeRouter = createTRPCRouter({
}), }),
randomizeCompose: protectedProcedure 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) .input(apiRandomizeCompose)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.composeId, { await checkServicePermissionAndAccess(ctx, input.composeId, {
@@ -453,12 +381,6 @@ export const composeRouter = createTRPCRouter({
return result; return result;
}), }),
isolatedDeployment: protectedProcedure 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) .input(apiRandomizeCompose)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.composeId, { await checkServicePermissionAndAccess(ctx, input.composeId, {
@@ -478,12 +400,6 @@ export const composeRouter = createTRPCRouter({
return result; return result;
}), }),
getConvertedCompose: protectedProcedure 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) .input(apiFindCompose)
.query(async ({ input, ctx }) => { .query(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.composeId, { await checkServicePermissionAndAccess(ctx, input.composeId, {
@@ -498,12 +414,6 @@ export const composeRouter = createTRPCRouter({
}), }),
deploy: protectedProcedure 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) .input(apiDeployCompose)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.composeId, { await checkServicePermissionAndAccess(ctx, input.composeId, {
@@ -554,12 +464,6 @@ export const composeRouter = createTRPCRouter({
}; };
}), }),
redeploy: protectedProcedure 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) .input(apiRedeployCompose)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.composeId, { await checkServicePermissionAndAccess(ctx, input.composeId, {
@@ -608,12 +512,6 @@ export const composeRouter = createTRPCRouter({
}; };
}), }),
stop: protectedProcedure stop: protectedProcedure
.meta({
openapi: {
summary: "Stop a compose service",
description: "Stops all running containers for the compose service using docker compose stop.",
},
})
.input(apiFindCompose) .input(apiFindCompose)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.composeId, { await checkServicePermissionAndAccess(ctx, input.composeId, {
@@ -630,12 +528,6 @@ export const composeRouter = createTRPCRouter({
return true; return true;
}), }),
start: protectedProcedure start: protectedProcedure
.meta({
openapi: {
summary: "Start a compose service",
description: "Starts all containers for the compose service using docker compose start.",
},
})
.input(apiFindCompose) .input(apiFindCompose)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.composeId, { await checkServicePermissionAndAccess(ctx, input.composeId, {
@@ -652,12 +544,6 @@ export const composeRouter = createTRPCRouter({
return true; return true;
}), }),
getDefaultCommand: protectedProcedure 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) .input(apiFindCompose)
.query(async ({ input, ctx }) => { .query(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.composeId, { await checkServicePermissionAndAccess(ctx, input.composeId, {
@@ -668,12 +554,6 @@ export const composeRouter = createTRPCRouter({
return `docker ${command}`; return `docker ${command}`;
}), }),
refreshToken: protectedProcedure 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) .input(apiFindCompose)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.composeId, { await checkServicePermissionAndAccess(ctx, input.composeId, {
@@ -692,12 +572,6 @@ export const composeRouter = createTRPCRouter({
return true; return true;
}), }),
deployTemplate: protectedProcedure 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( .input(
z.object({ z.object({
environmentId: z.string(), environmentId: z.string(),
@@ -806,12 +680,6 @@ export const composeRouter = createTRPCRouter({
}), }),
templates: protectedProcedure 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() })) .input(z.object({ baseUrl: z.string().optional() }))
.query(async ({ input }) => { .query(async ({ input }) => {
try { try {
@@ -830,12 +698,6 @@ export const composeRouter = createTRPCRouter({
}), }),
getTags: protectedProcedure 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() })) .input(z.object({ baseUrl: z.string().optional() }))
.query(async ({ input }) => { .query(async ({ input }) => {
const githubTemplates = await fetchTemplatesList(input.baseUrl); const githubTemplates = await fetchTemplatesList(input.baseUrl);
@@ -845,12 +707,6 @@ export const composeRouter = createTRPCRouter({
return uniqueTags; return uniqueTags;
}), }),
disconnectGitProvider: protectedProcedure 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) .input(apiFindCompose)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.composeId, { await checkServicePermissionAndAccess(ctx, input.composeId, {
@@ -903,12 +759,6 @@ export const composeRouter = createTRPCRouter({
}), }),
move: protectedProcedure 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( .input(
z.object({ z.object({
composeId: z.string(), composeId: z.string(),
@@ -946,12 +796,6 @@ export const composeRouter = createTRPCRouter({
}), }),
processTemplate: protectedProcedure 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( .input(
z.object({ z.object({
base64: z.string(), base64: z.string(),
@@ -1016,12 +860,6 @@ export const composeRouter = createTRPCRouter({
}), }),
import: protectedProcedure 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( .input(
z.object({ z.object({
base64: z.string(), base64: z.string(),
@@ -1134,12 +972,6 @@ export const composeRouter = createTRPCRouter({
}), }),
cancelDeployment: protectedProcedure 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) .input(apiFindCompose)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.composeId, { await checkServicePermissionAndAccess(ctx, input.composeId, {
@@ -1193,12 +1025,6 @@ export const composeRouter = createTRPCRouter({
}), }),
search: protectedProcedure 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( .input(
z.object({ z.object({
q: z.string().optional(), q: z.string().optional(),
@@ -1307,12 +1133,6 @@ export const composeRouter = createTRPCRouter({
}), }),
readLogs: protectedProcedure 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( .input(
apiFindCompose.extend({ apiFindCompose.extend({
containerId: z containerId: z

View File

@@ -16,6 +16,7 @@ import {
checkServicePermissionAndAccess, checkServicePermissionAndAccess,
findMemberByUserId, findMemberByUserId,
} from "@dokploy/server/services/permission"; } from "@dokploy/server/services/permission";
import { findServerById } from "@dokploy/server/services/server";
import { TRPCError } from "@trpc/server"; import { TRPCError } from "@trpc/server";
import { desc, eq } from "drizzle-orm"; import { desc, eq } from "drizzle-orm";
import { z } from "zod"; import { z } from "zod";
@@ -34,12 +35,6 @@ import { createTRPCRouter, protectedProcedure, withPermission } from "../trpc";
export const deploymentRouter = createTRPCRouter({ export const deploymentRouter = createTRPCRouter({
all: protectedProcedure all: protectedProcedure
.meta({
openapi: {
summary: "List deployments by application",
description: "Returns all deployments associated with the given application, ordered by creation date.",
},
})
.input(apiFindAllByApplication) .input(apiFindAllByApplication)
.query(async ({ input, ctx }) => { .query(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.applicationId, { await checkServicePermissionAndAccess(ctx, input.applicationId, {
@@ -49,12 +44,6 @@ export const deploymentRouter = createTRPCRouter({
}), }),
allByCompose: protectedProcedure allByCompose: protectedProcedure
.meta({
openapi: {
summary: "List deployments by compose",
description: "Returns all deployments associated with the given compose service.",
},
})
.input(apiFindAllByCompose) .input(apiFindAllByCompose)
.query(async ({ input, ctx }) => { .query(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.composeId, { await checkServicePermissionAndAccess(ctx, input.composeId, {
@@ -63,24 +52,18 @@ export const deploymentRouter = createTRPCRouter({
return await findAllDeploymentsByComposeId(input.composeId); return await findAllDeploymentsByComposeId(input.composeId);
}), }),
allByServer: withPermission("deployment", "read") allByServer: withPermission("deployment", "read")
.meta({
openapi: {
summary: "List deployments by server",
description: "Returns all deployments associated with the given server.",
},
})
.input(apiFindAllByServer) .input(apiFindAllByServer)
.query(async ({ input }) => { .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.",
});
}
return await findAllDeploymentsByServerId(input.serverId); return await findAllDeploymentsByServerId(input.serverId);
}), }),
allCentralized: withPermission("deployment", "read") allCentralized: withPermission("deployment", "read").query(
.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 }) => { async ({ ctx }) => {
const orgId = ctx.session.activeOrganizationId; const orgId = ctx.session.activeOrganizationId;
const accessedServices = const accessedServices =
@@ -94,14 +77,7 @@ export const deploymentRouter = createTRPCRouter({
}, },
), ),
queueList: withPermission("deployment", "read") queueList: withPermission("deployment", "read").query(async ({ ctx }) => {
.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; const orgId = ctx.session.activeOrganizationId;
let rows: QueueJobRow[]; let rows: QueueJobRow[];
@@ -148,12 +124,6 @@ export const deploymentRouter = createTRPCRouter({
}), }),
allByType: protectedProcedure 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) .input(apiFindAllByType)
.query(async ({ input, ctx }) => { .query(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.id, { await checkServicePermissionAndAccess(ctx, input.id, {
@@ -169,12 +139,6 @@ export const deploymentRouter = createTRPCRouter({
return deploymentsList; return deploymentsList;
}), }),
killProcess: protectedProcedure 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( .input(
z.object({ z.object({
deploymentId: z.string().min(1), deploymentId: z.string().min(1),
@@ -212,12 +176,6 @@ export const deploymentRouter = createTRPCRouter({
}), }),
removeDeployment: protectedProcedure removeDeployment: protectedProcedure
.meta({
openapi: {
summary: "Delete a deployment",
description: "Permanently removes a deployment record and its associated data.",
},
})
.input( .input(
z.object({ z.object({
deploymentId: z.string().min(1), deploymentId: z.string().min(1),
@@ -239,43 +197,4 @@ export const deploymentRouter = createTRPCRouter({
}); });
return result; 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,
};
}),
}); });

View File

@@ -22,12 +22,6 @@ import {
export const destinationRouter = createTRPCRouter({ export const destinationRouter = createTRPCRouter({
create: withPermission("destination", "create") 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) .input(apiCreateDestination)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
try { try {
@@ -51,12 +45,6 @@ export const destinationRouter = createTRPCRouter({
} }
}), }),
testConnection: withPermission("destination", "create") 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) .input(apiCreateDestination)
.mutation(async ({ input }) => { .mutation(async ({ input }) => {
const { const {
@@ -114,12 +102,6 @@ export const destinationRouter = createTRPCRouter({
} }
}), }),
one: withPermission("destination", "read") 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) .input(apiFindOneDestination)
.query(async ({ input, ctx }) => { .query(async ({ input, ctx }) => {
const destination = await findDestinationById(input.destinationId); const destination = await findDestinationById(input.destinationId);
@@ -131,26 +113,13 @@ export const destinationRouter = createTRPCRouter({
} }
return destination; return destination;
}), }),
all: withPermission("destination", "read") all: withPermission("destination", "read").query(async ({ ctx }) => {
.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({ return await db.query.destinations.findMany({
where: eq(destinations.organizationId, ctx.session.activeOrganizationId), where: eq(destinations.organizationId, ctx.session.activeOrganizationId),
orderBy: [desc(destinations.createdAt)], orderBy: [desc(destinations.createdAt)],
}); });
}), }),
remove: withPermission("destination", "delete") 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) .input(apiRemoveDestination)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
try { try {
@@ -178,12 +147,6 @@ export const destinationRouter = createTRPCRouter({
} }
}), }),
update: withPermission("destination", "create") 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) .input(apiUpdateDestination)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
try { try {

View File

@@ -1,6 +1,9 @@
import { import {
containerKill,
containerRemove, containerRemove,
containerRestart, containerRestart,
containerStart,
containerStop,
findServerById, findServerById,
getConfig, getConfig,
getContainers, getContainers,
@@ -20,12 +23,6 @@ export const containerIdRegex = /^[a-zA-Z0-9.\-_]+$/;
export const dockerRouter = createTRPCRouter({ export const dockerRouter = createTRPCRouter({
getContainers: withPermission("docker", "read") 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( .input(
z.object({ z.object({
serverId: z.string().optional(), serverId: z.string().optional(),
@@ -41,39 +38,111 @@ export const dockerRouter = createTRPCRouter({
return await getContainers(input.serverId); return await getContainers(input.serverId);
}), }),
restartContainer: withPermission("docker", "read") restartContainer: withPermission("service", "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( .input(
z.object({ z.object({
containerId: z containerId: z
.string() .string()
.min(1) .min(1)
.regex(containerIdRegex, "Invalid container id."), .regex(containerIdRegex, "Invalid container id."),
serverId: z.string().optional(),
}), }),
) )
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
const result = await containerRestart(input.containerId); if (input.serverId) {
const server = await findServerById(input.serverId);
if (server.organizationId !== ctx.session?.activeOrganizationId) {
throw new TRPCError({ code: "UNAUTHORIZED" });
}
}
await containerRestart(input.containerId, input.serverId);
await audit(ctx, { await audit(ctx, {
action: "start", action: "start",
resourceType: "docker", resourceType: "docker",
resourceId: input.containerId, resourceId: input.containerId,
resourceName: input.containerId, resourceName: input.containerId,
}); });
return result; }),
startContainer: withPermission("service", "read")
.input(
z.object({
containerId: z
.string()
.min(1)
.regex(containerIdRegex, "Invalid container id."),
serverId: z.string().optional(),
}),
)
.mutation(async ({ input, ctx }) => {
if (input.serverId) {
const server = await findServerById(input.serverId);
if (server.organizationId !== ctx.session?.activeOrganizationId) {
throw new TRPCError({ code: "UNAUTHORIZED" });
}
}
await containerStart(input.containerId, input.serverId);
await audit(ctx, {
action: "start",
resourceType: "docker",
resourceId: input.containerId,
resourceName: input.containerId,
});
}),
stopContainer: withPermission("service", "read")
.input(
z.object({
containerId: z
.string()
.min(1)
.regex(containerIdRegex, "Invalid container id."),
serverId: z.string().optional(),
}),
)
.mutation(async ({ input, ctx }) => {
if (input.serverId) {
const server = await findServerById(input.serverId);
if (server.organizationId !== ctx.session?.activeOrganizationId) {
throw new TRPCError({ code: "UNAUTHORIZED" });
}
}
await containerStop(input.containerId, input.serverId);
await audit(ctx, {
action: "stop",
resourceType: "docker",
resourceId: input.containerId,
resourceName: input.containerId,
});
}),
killContainer: withPermission("service", "read")
.input(
z.object({
containerId: z
.string()
.min(1)
.regex(containerIdRegex, "Invalid container id."),
serverId: z.string().optional(),
}),
)
.mutation(async ({ input, ctx }) => {
if (input.serverId) {
const server = await findServerById(input.serverId);
if (server.organizationId !== ctx.session?.activeOrganizationId) {
throw new TRPCError({ code: "UNAUTHORIZED" });
}
}
await containerKill(input.containerId, input.serverId);
await audit(ctx, {
action: "stop",
resourceType: "docker",
resourceId: input.containerId,
resourceName: input.containerId,
});
}), }),
removeContainer: withPermission("docker", "read") 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( .input(
z.object({ z.object({
containerId: z containerId: z
@@ -100,12 +169,6 @@ export const dockerRouter = createTRPCRouter({
}), }),
getConfig: withPermission("docker", "read") 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( .input(
z.object({ z.object({
containerId: z containerId: z
@@ -126,12 +189,6 @@ export const dockerRouter = createTRPCRouter({
}), }),
getContainersByAppNameMatch: withPermission("service", "read") 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( .input(
z.object({ z.object({
appType: z.enum(["stack", "docker-compose"]).optional(), appType: z.enum(["stack", "docker-compose"]).optional(),
@@ -154,12 +211,6 @@ export const dockerRouter = createTRPCRouter({
}), }),
getContainersByAppLabel: withPermission("docker", "read") 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( .input(
z.object({ z.object({
appName: z.string().min(1).regex(containerIdRegex, "Invalid app name."), appName: z.string().min(1).regex(containerIdRegex, "Invalid app name."),
@@ -182,12 +233,6 @@ export const dockerRouter = createTRPCRouter({
}), }),
getStackContainersByAppName: withPermission("docker", "read") 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( .input(
z.object({ z.object({
appName: z.string().min(1).regex(containerIdRegex, "Invalid app name."), appName: z.string().min(1).regex(containerIdRegex, "Invalid app name."),
@@ -205,12 +250,6 @@ export const dockerRouter = createTRPCRouter({
}), }),
getServiceContainersByAppName: withPermission("docker", "read") 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( .input(
z.object({ z.object({
appName: z.string().min(1).regex(containerIdRegex, "Invalid app name."), appName: z.string().min(1).regex(containerIdRegex, "Invalid app name."),
@@ -228,12 +267,6 @@ export const dockerRouter = createTRPCRouter({
}), }),
uploadFileToContainer: withPermission("docker", "read") 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) .input(uploadFileToContainerSchema)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
if (input.serverId) { if (input.serverId) {

View File

@@ -33,12 +33,6 @@ import {
export const domainRouter = createTRPCRouter({ export const domainRouter = createTRPCRouter({
create: protectedProcedure 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) .input(apiCreateDomain)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
try { try {
@@ -71,12 +65,6 @@ export const domainRouter = createTRPCRouter({
} }
}), }),
byApplicationId: protectedProcedure byApplicationId: protectedProcedure
.meta({
openapi: {
summary: "List domains by application",
description: "Returns all domains associated with a given application ID.",
},
})
.input(apiFindOneApplication) .input(apiFindOneApplication)
.query(async ({ input, ctx }) => { .query(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.applicationId, { await checkServicePermissionAndAccess(ctx, input.applicationId, {
@@ -85,12 +73,6 @@ export const domainRouter = createTRPCRouter({
return await findDomainsByApplicationId(input.applicationId); return await findDomainsByApplicationId(input.applicationId);
}), }),
byComposeId: protectedProcedure byComposeId: protectedProcedure
.meta({
openapi: {
summary: "List domains by compose service",
description: "Returns all domains associated with a given compose service ID.",
},
})
.input(apiFindCompose) .input(apiFindCompose)
.query(async ({ input, ctx }) => { .query(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.composeId, { await checkServicePermissionAndAccess(ctx, input.composeId, {
@@ -99,12 +81,6 @@ export const domainRouter = createTRPCRouter({
return await findDomainsByComposeId(input.composeId); return await findDomainsByComposeId(input.composeId);
}), }),
generateDomain: withPermission("domain", "create") 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() })) .input(z.object({ appName: z.string(), serverId: z.string().optional() }))
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
return generateTraefikMeDomain( return generateTraefikMeDomain(
@@ -114,12 +90,6 @@ export const domainRouter = createTRPCRouter({
); );
}), }),
canGenerateTraefikMeDomains: withPermission("domain", "read") 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() })) .input(z.object({ serverId: z.string() }))
.query(async ({ input }) => { .query(async ({ input }) => {
if (input.serverId) { if (input.serverId) {
@@ -131,12 +101,6 @@ export const domainRouter = createTRPCRouter({
}), }),
update: protectedProcedure 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) .input(apiUpdateDomain)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
const currentDomain = await findDomainById(input.domainId); const currentDomain = await findDomainById(input.domainId);
@@ -177,15 +141,7 @@ export const domainRouter = createTRPCRouter({
} }
return result; return result;
}), }),
one: protectedProcedure one: protectedProcedure.input(apiFindDomain).query(async ({ input, ctx }) => {
.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 domain = await findDomainById(input.domainId);
const serviceId = domain.applicationId || domain.composeId; const serviceId = domain.applicationId || domain.composeId;
if (serviceId) { if (serviceId) {
@@ -203,12 +159,6 @@ export const domainRouter = createTRPCRouter({
return domain; return domain;
}), }),
delete: protectedProcedure 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) .input(apiFindDomain)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
const domain = await findDomainById(input.domainId); const domain = await findDomainById(input.domainId);
@@ -243,12 +193,6 @@ export const domainRouter = createTRPCRouter({
}), }),
validateDomain: withPermission("domain", "read") 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( .input(
z.object({ z.object({
domain: z.string(), domain: z.string(),

View File

@@ -63,12 +63,6 @@ const filterEnvironmentServices = (
export const environmentRouter = createTRPCRouter({ export const environmentRouter = createTRPCRouter({
create: protectedProcedure 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) .input(apiCreateEnvironment)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
try { try {
@@ -105,12 +99,6 @@ export const environmentRouter = createTRPCRouter({
}), }),
one: protectedProcedure 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) .input(apiFindOneEnvironment)
.query(async ({ input, ctx }) => { .query(async ({ input, ctx }) => {
const environment = await findEnvironmentById(input.environmentId); const environment = await findEnvironmentById(input.environmentId);
@@ -149,12 +137,6 @@ export const environmentRouter = createTRPCRouter({
}), }),
byProjectId: protectedProcedure 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() })) .input(z.object({ projectId: z.string() }))
.query(async ({ input, ctx }) => { .query(async ({ input, ctx }) => {
try { try {
@@ -201,12 +183,6 @@ export const environmentRouter = createTRPCRouter({
}), }),
remove: protectedProcedure 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) .input(apiRemoveEnvironment)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
try { try {
@@ -253,12 +229,6 @@ export const environmentRouter = createTRPCRouter({
}), }),
update: protectedProcedure 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) .input(apiUpdateEnvironment)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
try { try {
@@ -326,12 +296,6 @@ export const environmentRouter = createTRPCRouter({
}), }),
duplicate: protectedProcedure 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) .input(apiDuplicateEnvironment)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
try { try {
@@ -379,12 +343,6 @@ export const environmentRouter = createTRPCRouter({
}), }),
search: protectedProcedure 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( .input(
z.object({ z.object({
q: z.string().optional(), q: z.string().optional(),

View File

@@ -21,14 +21,7 @@ import {
} from "@/server/db/schema"; } from "@/server/db/schema";
export const gitProviderRouter = createTRPCRouter({ export const gitProviderRouter = createTRPCRouter({
getAll: protectedProcedure getAll: protectedProcedure.query(async ({ ctx }) => {
.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); const accessibleIds = await getAccessibleGitProviderIds(ctx.session);
if (accessibleIds.size === 0) { if (accessibleIds.size === 0) {
@@ -53,12 +46,6 @@ export const gitProviderRouter = createTRPCRouter({
}), }),
toggleShare: protectedProcedure 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) .input(apiToggleShareGitProvider)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
const provider = await findGitProviderById(input.gitProviderId); const provider = await findGitProviderById(input.gitProviderId);
@@ -86,12 +73,6 @@ export const gitProviderRouter = createTRPCRouter({
}), }),
allForPermissions: withPermission("member", "update") 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 }) => { .use(async ({ ctx, next }) => {
const licensed = await hasValidLicense(ctx.session.activeOrganizationId); const licensed = await hasValidLicense(ctx.session.activeOrganizationId);
if (!licensed) { if (!licensed) {
@@ -115,12 +96,6 @@ export const gitProviderRouter = createTRPCRouter({
}), }),
remove: withPermission("gitProviders", "delete") 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) .input(apiRemoveGitProvider)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
try { try {

View File

@@ -27,12 +27,6 @@ import {
export const giteaRouter = createTRPCRouter({ export const giteaRouter = createTRPCRouter({
create: withPermission("gitProviders", "create") 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) .input(apiCreateGitea)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
try { try {
@@ -59,26 +53,11 @@ export const giteaRouter = createTRPCRouter({
} }
}), }),
one: protectedProcedure one: protectedProcedure.input(apiFindOneGitea).query(async ({ input }) => {
.meta({ return await findGiteaById(input.giteaId);
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 giteaProviders: protectedProcedure.query(async ({ ctx }) => {
.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); const accessibleIds = await getAccessibleGitProviderIds(ctx.session);
let result = await db.query.gitea.findMany({ let result = await db.query.gitea.findMany({
@@ -109,12 +88,6 @@ export const giteaRouter = createTRPCRouter({
}), }),
getGiteaRepositories: protectedProcedure 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) .input(apiFindOneGitea)
.query(async ({ input }) => { .query(async ({ input }) => {
const { giteaId } = input; const { giteaId } = input;
@@ -139,12 +112,6 @@ export const giteaRouter = createTRPCRouter({
}), }),
getGiteaBranches: protectedProcedure 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) .input(apiFindGiteaBranches)
.query(async ({ input }) => { .query(async ({ input }) => {
const { giteaId, owner, repositoryName } = input; const { giteaId, owner, repositoryName } = input;
@@ -173,12 +140,6 @@ export const giteaRouter = createTRPCRouter({
}), }),
testConnection: protectedProcedure 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) .input(apiGiteaTestConnection)
.mutation(async ({ input }) => { .mutation(async ({ input }) => {
const giteaId = input.giteaId ?? ""; const giteaId = input.giteaId ?? "";
@@ -199,12 +160,6 @@ export const giteaRouter = createTRPCRouter({
}), }),
update: withPermission("gitProviders", "create") 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) .input(apiUpdateGitea)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
if (input.name) { if (input.name) {
@@ -233,12 +188,6 @@ export const giteaRouter = createTRPCRouter({
}), }),
getGiteaUrl: protectedProcedure 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) .input(apiFindOneGitea)
.query(async ({ input }) => { .query(async ({ input }) => {
const { giteaId } = input; const { giteaId } = input;

View File

@@ -22,47 +22,20 @@ import {
} from "@/server/db/schema"; } from "@/server/db/schema";
export const githubRouter = createTRPCRouter({ export const githubRouter = createTRPCRouter({
one: protectedProcedure one: protectedProcedure.input(apiFindOneGithub).query(async ({ input }) => {
.meta({ return await findGithubById(input.githubId);
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 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) .input(apiFindOneGithub)
.query(async ({ input }) => { .query(async ({ input }) => {
return await getGithubRepositories(input.githubId); return await getGithubRepositories(input.githubId);
}), }),
getGithubBranches: protectedProcedure 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) .input(apiFindGithubBranches)
.query(async ({ input }) => { .query(async ({ input }) => {
return await getGithubBranches(input); return await getGithubBranches(input);
}), }),
githubProviders: protectedProcedure githubProviders: protectedProcedure.query(async ({ ctx }) => {
.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); const accessibleIds = await getAccessibleGitProviderIds(ctx.session);
let result = await db.query.github.findMany({ let result = await db.query.github.findMany({
@@ -93,12 +66,6 @@ export const githubRouter = createTRPCRouter({
}), }),
testConnection: protectedProcedure 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) .input(apiFindOneGithub)
.mutation(async ({ input }) => { .mutation(async ({ input }) => {
try { try {
@@ -112,12 +79,6 @@ export const githubRouter = createTRPCRouter({
} }
}), }),
update: withPermission("gitProviders", "create") 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) .input(apiUpdateGithub)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
await updateGitProvider(input.gitProviderId, { await updateGitProvider(input.gitProviderId, {

View File

@@ -27,12 +27,6 @@ import {
export const gitlabRouter = createTRPCRouter({ export const gitlabRouter = createTRPCRouter({
create: withPermission("gitProviders", "create") 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) .input(apiCreateGitlab)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
try { try {
@@ -57,25 +51,10 @@ export const gitlabRouter = createTRPCRouter({
}); });
} }
}), }),
one: protectedProcedure one: protectedProcedure.input(apiFindOneGitlab).query(async ({ input }) => {
.meta({ return await findGitlabById(input.gitlabId);
openapi: { }),
summary: "Get GitLab provider", gitlabProviders: protectedProcedure.query(async ({ ctx }) => {
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); const accessibleIds = await getAccessibleGitProviderIds(ctx.session);
let result = await db.query.gitlab.findMany({ let result = await db.query.gitlab.findMany({
@@ -106,35 +85,17 @@ export const gitlabRouter = createTRPCRouter({
return filtered; return filtered;
}), }),
getGitlabRepositories: protectedProcedure 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) .input(apiFindOneGitlab)
.query(async ({ input }) => { .query(async ({ input }) => {
return await getGitlabRepositories(input.gitlabId); return await getGitlabRepositories(input.gitlabId);
}), }),
getGitlabBranches: protectedProcedure 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) .input(apiFindGitlabBranches)
.query(async ({ input }) => { .query(async ({ input }) => {
return await getGitlabBranches(input); return await getGitlabBranches(input);
}), }),
testConnection: protectedProcedure 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) .input(apiGitlabTestConnection)
.mutation(async ({ input }) => { .mutation(async ({ input }) => {
try { try {
@@ -149,12 +110,6 @@ export const gitlabRouter = createTRPCRouter({
} }
}), }),
update: withPermission("gitProviders", "create") 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) .input(apiUpdateGitlab)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
if (input.name) { if (input.name) {

View File

@@ -6,7 +6,6 @@ import {
findEnvironmentById, findEnvironmentById,
findLibsqlById, findLibsqlById,
findProjectById, findProjectById,
getAccessibleServerIds,
getContainerLogs, getContainerLogs,
IS_CLOUD, IS_CLOUD,
rebuildDatabase, rebuildDatabase,
@@ -17,6 +16,7 @@ import {
stopService, stopService,
stopServiceRemote, stopServiceRemote,
updateLibsqlById, updateLibsqlById,
getAccessibleServerIds,
} from "@dokploy/server"; } from "@dokploy/server";
import { import {
addNewService, addNewService,
@@ -43,12 +43,6 @@ import {
} from "@/server/db/schema"; } from "@/server/db/schema";
export const libsqlRouter = createTRPCRouter({ export const libsqlRouter = createTRPCRouter({
create: protectedProcedure 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) .input(apiCreateLibsql)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
try { try {
@@ -106,12 +100,6 @@ export const libsqlRouter = createTRPCRouter({
} }
}), }),
one: protectedProcedure 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) .input(apiFindOneLibsql)
.query(async ({ input, ctx }) => { .query(async ({ input, ctx }) => {
await checkServiceAccess(ctx, input.libsqlId, "read"); await checkServiceAccess(ctx, input.libsqlId, "read");
@@ -130,12 +118,6 @@ export const libsqlRouter = createTRPCRouter({
}), }),
start: protectedProcedure 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) .input(apiFindOneLibsql)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.libsqlId, { await checkServicePermissionAndAccess(ctx, input.libsqlId, {
@@ -161,12 +143,6 @@ export const libsqlRouter = createTRPCRouter({
return libsql; return libsql;
}), }),
stop: protectedProcedure 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) .input(apiFindOneLibsql)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.libsqlId, { await checkServicePermissionAndAccess(ctx, input.libsqlId, {
@@ -192,12 +168,6 @@ export const libsqlRouter = createTRPCRouter({
return libsql; return libsql;
}), }),
saveExternalPorts: protectedProcedure 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) .input(apiSaveExternalPortsLibsql)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.libsqlId, { await checkServicePermissionAndAccess(ctx, input.libsqlId, {
@@ -260,12 +230,6 @@ export const libsqlRouter = createTRPCRouter({
return libsql; return libsql;
}), }),
deploy: protectedProcedure 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) .input(apiDeployLibsql)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.libsqlId, { await checkServicePermissionAndAccess(ctx, input.libsqlId, {
@@ -318,12 +282,6 @@ export const libsqlRouter = createTRPCRouter({
} }
}), }),
changeStatus: protectedProcedure 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) .input(apiChangeLibsqlStatus)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.libsqlId, { await checkServicePermissionAndAccess(ctx, input.libsqlId, {
@@ -342,12 +300,6 @@ export const libsqlRouter = createTRPCRouter({
return libsql; return libsql;
}), }),
remove: protectedProcedure 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) .input(apiFindOneLibsql)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
await checkServiceAccess(ctx, input.libsqlId, "delete"); await checkServiceAccess(ctx, input.libsqlId, "delete");
@@ -383,12 +335,6 @@ export const libsqlRouter = createTRPCRouter({
return libsql; return libsql;
}), }),
saveEnvironment: protectedProcedure 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) .input(apiSaveEnvironmentVariablesLibsql)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.libsqlId, { await checkServicePermissionAndAccess(ctx, input.libsqlId, {
@@ -413,12 +359,6 @@ export const libsqlRouter = createTRPCRouter({
return true; return true;
}), }),
reload: protectedProcedure reload: protectedProcedure
.meta({
openapi: {
summary: "Reload a LibSQL database",
description: "Restarts the LibSQL database by stopping and then starting its Docker container.",
},
})
.input(apiResetLibsql) .input(apiResetLibsql)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.libsqlId, { await checkServicePermissionAndAccess(ctx, input.libsqlId, {
@@ -451,12 +391,6 @@ export const libsqlRouter = createTRPCRouter({
return true; return true;
}), }),
update: protectedProcedure update: protectedProcedure
.meta({
openapi: {
summary: "Update a LibSQL database",
description: "Updates the configuration of an existing LibSQL database service.",
},
})
.input(apiUpdateLibsql) .input(apiUpdateLibsql)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
const { libsqlId, ...rest } = input; const { libsqlId, ...rest } = input;
@@ -483,12 +417,6 @@ export const libsqlRouter = createTRPCRouter({
return true; return true;
}), }),
move: protectedProcedure 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( .input(
z.object({ z.object({
libsqlId: z.string(), libsqlId: z.string(),
@@ -525,12 +453,6 @@ export const libsqlRouter = createTRPCRouter({
return updatedLibsql; return updatedLibsql;
}), }),
rebuild: protectedProcedure 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) .input(apiRebuildLibsql)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.libsqlId, { await checkServicePermissionAndAccess(ctx, input.libsqlId, {
@@ -547,12 +469,6 @@ export const libsqlRouter = createTRPCRouter({
}), }),
readLogs: protectedProcedure 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( .input(
apiFindOneLibsql.extend({ apiFindOneLibsql.extend({
tail: z.number().int().min(1).max(10000).default(100), tail: z.number().int().min(1).max(10000).default(100),

View File

@@ -54,12 +54,6 @@ import {
import { cancelJobs } from "@/server/utils/backup"; import { cancelJobs } from "@/server/utils/backup";
export const mariadbRouter = createTRPCRouter({ export const mariadbRouter = createTRPCRouter({
create: protectedProcedure 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) .input(apiCreateMariaDB)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
try { try {
@@ -120,12 +114,6 @@ export const mariadbRouter = createTRPCRouter({
} }
}), }),
one: protectedProcedure 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) .input(apiFindOneMariaDB)
.query(async ({ input, ctx }) => { .query(async ({ input, ctx }) => {
await checkServiceAccess(ctx, input.mariadbId, "read"); await checkServiceAccess(ctx, input.mariadbId, "read");
@@ -143,12 +131,6 @@ export const mariadbRouter = createTRPCRouter({
}), }),
start: protectedProcedure 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) .input(apiFindOneMariaDB)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.mariadbId, { await checkServicePermissionAndAccess(ctx, input.mariadbId, {
@@ -173,12 +155,6 @@ export const mariadbRouter = createTRPCRouter({
return service; return service;
}), }),
stop: protectedProcedure 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) .input(apiFindOneMariaDB)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.mariadbId, { await checkServicePermissionAndAccess(ctx, input.mariadbId, {
@@ -204,12 +180,6 @@ export const mariadbRouter = createTRPCRouter({
return mariadb; return mariadb;
}), }),
saveExternalPort: protectedProcedure 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) .input(apiSaveExternalPortMariaDB)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.mariadbId, { await checkServicePermissionAndAccess(ctx, input.mariadbId, {
@@ -243,12 +213,6 @@ export const mariadbRouter = createTRPCRouter({
return mariadb; return mariadb;
}), }),
deploy: protectedProcedure 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) .input(apiDeployMariaDB)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.mariadbId, { await checkServicePermissionAndAccess(ctx, input.mariadbId, {
@@ -286,12 +250,6 @@ export const mariadbRouter = createTRPCRouter({
}); });
}), }),
changeStatus: protectedProcedure 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) .input(apiChangeMariaDBStatus)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.mariadbId, { await checkServicePermissionAndAccess(ctx, input.mariadbId, {
@@ -310,12 +268,6 @@ export const mariadbRouter = createTRPCRouter({
return mongo; return mongo;
}), }),
remove: protectedProcedure 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) .input(apiFindOneMariaDB)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
await checkServiceAccess(ctx, input.mariadbId, "delete"); await checkServiceAccess(ctx, input.mariadbId, "delete");
@@ -353,12 +305,6 @@ export const mariadbRouter = createTRPCRouter({
return mongo; return mongo;
}), }),
saveEnvironment: protectedProcedure 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) .input(apiSaveEnvironmentVariablesMariaDB)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.mariadbId, { await checkServicePermissionAndAccess(ctx, input.mariadbId, {
@@ -383,12 +329,6 @@ export const mariadbRouter = createTRPCRouter({
return true; return true;
}), }),
reload: protectedProcedure reload: protectedProcedure
.meta({
openapi: {
summary: "Reload a MariaDB database",
description: "Restarts the MariaDB database by stopping and then starting its Docker container.",
},
})
.input(apiResetMariadb) .input(apiResetMariadb)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.mariadbId, { await checkServicePermissionAndAccess(ctx, input.mariadbId, {
@@ -421,12 +361,6 @@ export const mariadbRouter = createTRPCRouter({
return true; return true;
}), }),
update: protectedProcedure update: protectedProcedure
.meta({
openapi: {
summary: "Update a MariaDB database",
description: "Updates the configuration of an existing MariaDB database service.",
},
})
.input(apiUpdateMariaDB) .input(apiUpdateMariaDB)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
const { mariadbId, ...rest } = input; const { mariadbId, ...rest } = input;
@@ -453,12 +387,6 @@ export const mariadbRouter = createTRPCRouter({
return true; return true;
}), }),
changePassword: protectedProcedure 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( .input(
z.object({ z.object({
mariadbId: z.string().min(1), mariadbId: z.string().min(1),
@@ -516,12 +444,6 @@ export const mariadbRouter = createTRPCRouter({
return true; return true;
}), }),
move: protectedProcedure 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( .input(
z.object({ z.object({
mariadbId: z.string(), mariadbId: z.string(),
@@ -558,12 +480,6 @@ export const mariadbRouter = createTRPCRouter({
return updatedMariadb; return updatedMariadb;
}), }),
rebuild: protectedProcedure 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) .input(apiRebuildMariadb)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.mariadbId, { await checkServicePermissionAndAccess(ctx, input.mariadbId, {
@@ -579,12 +495,6 @@ export const mariadbRouter = createTRPCRouter({
return true; return true;
}), }),
search: protectedProcedure 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( .input(
z.object({ z.object({
q: z.string().optional(), q: z.string().optional(),
@@ -683,12 +593,6 @@ export const mariadbRouter = createTRPCRouter({
}), }),
readLogs: protectedProcedure 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( .input(
apiFindOneMariaDB.extend({ apiFindOneMariaDB.extend({
tail: z.number().int().min(1).max(10000).default(100), tail: z.number().int().min(1).max(10000).default(100),

View File

@@ -53,12 +53,6 @@ import {
import { cancelJobs } from "@/server/utils/backup"; import { cancelJobs } from "@/server/utils/backup";
export const mongoRouter = createTRPCRouter({ export const mongoRouter = createTRPCRouter({
create: protectedProcedure 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) .input(apiCreateMongo)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
try { try {
@@ -123,12 +117,6 @@ export const mongoRouter = createTRPCRouter({
} }
}), }),
one: protectedProcedure 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) .input(apiFindOneMongo)
.query(async ({ input, ctx }) => { .query(async ({ input, ctx }) => {
await checkServiceAccess(ctx, input.mongoId, "read"); await checkServiceAccess(ctx, input.mongoId, "read");
@@ -147,12 +135,6 @@ export const mongoRouter = createTRPCRouter({
}), }),
start: protectedProcedure 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) .input(apiFindOneMongo)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.mongoId, { await checkServicePermissionAndAccess(ctx, input.mongoId, {
@@ -178,12 +160,6 @@ export const mongoRouter = createTRPCRouter({
return service; return service;
}), }),
stop: protectedProcedure 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) .input(apiFindOneMongo)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.mongoId, { await checkServicePermissionAndAccess(ctx, input.mongoId, {
@@ -209,12 +185,6 @@ export const mongoRouter = createTRPCRouter({
return mongo; return mongo;
}), }),
saveExternalPort: protectedProcedure 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) .input(apiSaveExternalPortMongo)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.mongoId, { await checkServicePermissionAndAccess(ctx, input.mongoId, {
@@ -248,12 +218,6 @@ export const mongoRouter = createTRPCRouter({
return mongo; return mongo;
}), }),
deploy: protectedProcedure 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) .input(apiDeployMongo)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.mongoId, { await checkServicePermissionAndAccess(ctx, input.mongoId, {
@@ -307,12 +271,6 @@ export const mongoRouter = createTRPCRouter({
}), }),
changeStatus: protectedProcedure 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) .input(apiChangeMongoStatus)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.mongoId, { await checkServicePermissionAndAccess(ctx, input.mongoId, {
@@ -331,12 +289,6 @@ export const mongoRouter = createTRPCRouter({
return mongo; return mongo;
}), }),
reload: protectedProcedure reload: protectedProcedure
.meta({
openapi: {
summary: "Reload a MongoDB database",
description: "Restarts the MongoDB database by stopping and then starting its Docker container.",
},
})
.input(apiResetMongo) .input(apiResetMongo)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.mongoId, { await checkServicePermissionAndAccess(ctx, input.mongoId, {
@@ -369,12 +321,6 @@ export const mongoRouter = createTRPCRouter({
return true; return true;
}), }),
remove: protectedProcedure 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) .input(apiFindOneMongo)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
await checkServiceAccess(ctx, input.mongoId, "delete"); await checkServiceAccess(ctx, input.mongoId, "delete");
@@ -413,12 +359,6 @@ export const mongoRouter = createTRPCRouter({
return mongo; return mongo;
}), }),
saveEnvironment: protectedProcedure 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) .input(apiSaveEnvironmentVariablesMongo)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.mongoId, { await checkServicePermissionAndAccess(ctx, input.mongoId, {
@@ -443,12 +383,6 @@ export const mongoRouter = createTRPCRouter({
return true; return true;
}), }),
update: protectedProcedure update: protectedProcedure
.meta({
openapi: {
summary: "Update a MongoDB database",
description: "Updates the configuration of an existing MongoDB database service.",
},
})
.input(apiUpdateMongo) .input(apiUpdateMongo)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
const { mongoId, ...rest } = input; const { mongoId, ...rest } = input;
@@ -475,12 +409,6 @@ export const mongoRouter = createTRPCRouter({
return true; return true;
}), }),
changePassword: protectedProcedure 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( .input(
z.object({ z.object({
mongoId: z.string().min(1), mongoId: z.string().min(1),
@@ -531,12 +459,6 @@ export const mongoRouter = createTRPCRouter({
return true; return true;
}), }),
move: protectedProcedure 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( .input(
z.object({ z.object({
mongoId: z.string(), mongoId: z.string(),
@@ -573,12 +495,6 @@ export const mongoRouter = createTRPCRouter({
return updatedMongo; return updatedMongo;
}), }),
rebuild: protectedProcedure 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) .input(apiRebuildMongo)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.mongoId, { await checkServicePermissionAndAccess(ctx, input.mongoId, {
@@ -595,12 +511,6 @@ export const mongoRouter = createTRPCRouter({
return true; return true;
}), }),
search: protectedProcedure 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( .input(
z.object({ z.object({
q: z.string().optional(), q: z.string().optional(),
@@ -694,12 +604,6 @@ export const mongoRouter = createTRPCRouter({
}), }),
readLogs: protectedProcedure 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( .input(
apiFindOneMongo.extend({ apiFindOneMongo.extend({
tail: z.number().int().min(1).max(10000).default(100), tail: z.number().int().min(1).max(10000).default(100),

View File

@@ -75,12 +75,6 @@ async function getServiceOrganizationId(
export const mountRouter = createTRPCRouter({ export const mountRouter = createTRPCRouter({
create: protectedProcedure 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) .input(apiCreateMount)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.serviceId, { await checkServicePermissionAndAccess(ctx, input.serviceId, {
@@ -96,12 +90,6 @@ export const mountRouter = createTRPCRouter({
return mount; return mount;
}), }),
remove: protectedProcedure 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) .input(apiRemoveMount)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
const mount = await findMountById(input.mountId); const mount = await findMountById(input.mountId);
@@ -128,12 +116,6 @@ export const mountRouter = createTRPCRouter({
}), }),
one: protectedProcedure 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) .input(apiFindOneMount)
.query(async ({ input, ctx }) => { .query(async ({ input, ctx }) => {
const mount = await findMountById(input.mountId); const mount = await findMountById(input.mountId);
@@ -154,12 +136,6 @@ export const mountRouter = createTRPCRouter({
return mount; return mount;
}), }),
update: protectedProcedure 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) .input(apiUpdateMount)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
const mount = await findMountById(input.mountId); const mount = await findMountById(input.mountId);
@@ -186,12 +162,6 @@ export const mountRouter = createTRPCRouter({
return await updateMount(input.mountId, input); return await updateMount(input.mountId, input);
}), }),
allNamedByApplicationId: protectedProcedure 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) })) .input(z.object({ applicationId: z.string().min(1) }))
.query(async ({ input, ctx }) => { .query(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.applicationId, { await checkServicePermissionAndAccess(ctx, input.applicationId, {
@@ -205,12 +175,6 @@ export const mountRouter = createTRPCRouter({
return mounts; return mounts;
}), }),
listByServiceId: protectedProcedure 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) .input(apiFindMountByApplicationId)
.query(async ({ input, ctx }) => { .query(async ({ input, ctx }) => {
await checkServiceAccess(ctx, input.serviceId, "read"); await checkServiceAccess(ctx, input.serviceId, "read");

View File

@@ -54,12 +54,6 @@ import { cancelJobs } from "@/server/utils/backup";
export const mysqlRouter = createTRPCRouter({ export const mysqlRouter = createTRPCRouter({
create: protectedProcedure 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) .input(apiCreateMySql)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
try { try {
@@ -124,12 +118,6 @@ export const mysqlRouter = createTRPCRouter({
} }
}), }),
one: protectedProcedure 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) .input(apiFindOneMySql)
.query(async ({ input, ctx }) => { .query(async ({ input, ctx }) => {
await checkServiceAccess(ctx, input.mysqlId, "read"); await checkServiceAccess(ctx, input.mysqlId, "read");
@@ -147,12 +135,6 @@ export const mysqlRouter = createTRPCRouter({
}), }),
start: protectedProcedure 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) .input(apiFindOneMySql)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.mysqlId, { await checkServicePermissionAndAccess(ctx, input.mysqlId, {
@@ -178,12 +160,6 @@ export const mysqlRouter = createTRPCRouter({
return service; return service;
}), }),
stop: protectedProcedure 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) .input(apiFindOneMySql)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.mysqlId, { await checkServicePermissionAndAccess(ctx, input.mysqlId, {
@@ -208,12 +184,6 @@ export const mysqlRouter = createTRPCRouter({
return mongo; return mongo;
}), }),
saveExternalPort: protectedProcedure 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) .input(apiSaveExternalPortMySql)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.mysqlId, { await checkServicePermissionAndAccess(ctx, input.mysqlId, {
@@ -247,12 +217,6 @@ export const mysqlRouter = createTRPCRouter({
return mysql; return mysql;
}), }),
deploy: protectedProcedure 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) .input(apiDeployMySql)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.mysqlId, { await checkServicePermissionAndAccess(ctx, input.mysqlId, {
@@ -306,12 +270,6 @@ export const mysqlRouter = createTRPCRouter({
} }
}), }),
changeStatus: protectedProcedure 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) .input(apiChangeMySqlStatus)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.mysqlId, { await checkServicePermissionAndAccess(ctx, input.mysqlId, {
@@ -330,12 +288,6 @@ export const mysqlRouter = createTRPCRouter({
return mongo; return mongo;
}), }),
reload: protectedProcedure reload: protectedProcedure
.meta({
openapi: {
summary: "Reload a MySQL database",
description: "Restarts the MySQL database by stopping and then starting its Docker container.",
},
})
.input(apiResetMysql) .input(apiResetMysql)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.mysqlId, { await checkServicePermissionAndAccess(ctx, input.mysqlId, {
@@ -367,12 +319,6 @@ export const mysqlRouter = createTRPCRouter({
return true; return true;
}), }),
remove: protectedProcedure 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) .input(apiFindOneMySql)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
await checkServiceAccess(ctx, input.mysqlId, "delete"); await checkServiceAccess(ctx, input.mysqlId, "delete");
@@ -409,12 +355,6 @@ export const mysqlRouter = createTRPCRouter({
return mongo; return mongo;
}), }),
saveEnvironment: protectedProcedure 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) .input(apiSaveEnvironmentVariablesMySql)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.mysqlId, { await checkServicePermissionAndAccess(ctx, input.mysqlId, {
@@ -439,12 +379,6 @@ export const mysqlRouter = createTRPCRouter({
return true; return true;
}), }),
update: protectedProcedure update: protectedProcedure
.meta({
openapi: {
summary: "Update a MySQL database",
description: "Updates the configuration of an existing MySQL database service.",
},
})
.input(apiUpdateMySql) .input(apiUpdateMySql)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
const { mysqlId, ...rest } = input; const { mysqlId, ...rest } = input;
@@ -471,12 +405,6 @@ export const mysqlRouter = createTRPCRouter({
return true; return true;
}), }),
changePassword: protectedProcedure 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( .input(
z.object({ z.object({
mysqlId: z.string().min(1), mysqlId: z.string().min(1),
@@ -534,12 +462,6 @@ export const mysqlRouter = createTRPCRouter({
return true; return true;
}), }),
move: protectedProcedure 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( .input(
z.object({ z.object({
mysqlId: z.string(), mysqlId: z.string(),
@@ -576,12 +498,6 @@ export const mysqlRouter = createTRPCRouter({
return updatedMysql; return updatedMysql;
}), }),
rebuild: protectedProcedure 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) .input(apiRebuildMysql)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.mysqlId, { await checkServicePermissionAndAccess(ctx, input.mysqlId, {
@@ -598,12 +514,6 @@ export const mysqlRouter = createTRPCRouter({
return true; return true;
}), }),
search: protectedProcedure 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( .input(
z.object({ z.object({
q: z.string().optional(), q: z.string().optional(),
@@ -697,12 +607,6 @@ export const mysqlRouter = createTRPCRouter({
}), }),
readLogs: protectedProcedure 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( .input(
apiFindOneMySql.extend({ apiFindOneMySql.extend({
tail: z.number().int().min(1).max(10000).default(100), tail: z.number().int().min(1).max(10000).default(100),

View File

@@ -95,12 +95,6 @@ import {
export const notificationRouter = createTRPCRouter({ export const notificationRouter = createTRPCRouter({
createSlack: withPermission("notification", "create") 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) .input(apiCreateSlack)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
try { try {
@@ -120,12 +114,6 @@ export const notificationRouter = createTRPCRouter({
} }
}), }),
updateSlack: withPermission("notification", "update") 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) .input(apiUpdateSlack)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
try { try {
@@ -152,12 +140,6 @@ export const notificationRouter = createTRPCRouter({
} }
}), }),
testSlackConnection: withPermission("notification", "create") 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) .input(apiTestSlackConnection)
.mutation(async ({ input }) => { .mutation(async ({ input }) => {
try { try {
@@ -175,12 +157,6 @@ export const notificationRouter = createTRPCRouter({
} }
}), }),
createTelegram: withPermission("notification", "create") 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) .input(apiCreateTelegram)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
try { try {
@@ -203,12 +179,6 @@ export const notificationRouter = createTRPCRouter({
}), }),
updateTelegram: withPermission("notification", "update") 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) .input(apiUpdateTelegram)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
try { try {
@@ -239,12 +209,6 @@ export const notificationRouter = createTRPCRouter({
} }
}), }),
testTelegramConnection: withPermission("notification", "create") 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) .input(apiTestTelegramConnection)
.mutation(async ({ input }) => { .mutation(async ({ input }) => {
try { try {
@@ -259,12 +223,6 @@ export const notificationRouter = createTRPCRouter({
} }
}), }),
createDiscord: withPermission("notification", "create") 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) .input(apiCreateDiscord)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
try { try {
@@ -287,12 +245,6 @@ export const notificationRouter = createTRPCRouter({
}), }),
updateDiscord: withPermission("notification", "update") 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) .input(apiUpdateDiscord)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
try { try {
@@ -324,12 +276,6 @@ export const notificationRouter = createTRPCRouter({
}), }),
testDiscordConnection: withPermission("notification", "create") 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) .input(apiTestDiscordConnection)
.mutation(async ({ input }) => { .mutation(async ({ input }) => {
try { try {
@@ -352,12 +298,6 @@ export const notificationRouter = createTRPCRouter({
} }
}), }),
createEmail: withPermission("notification", "create") 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) .input(apiCreateEmail)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
try { try {
@@ -376,12 +316,6 @@ export const notificationRouter = createTRPCRouter({
} }
}), }),
updateEmail: withPermission("notification", "update") 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) .input(apiUpdateEmail)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
try { try {
@@ -412,12 +346,6 @@ export const notificationRouter = createTRPCRouter({
} }
}), }),
testEmailConnection: withPermission("notification", "create") 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) .input(apiTestEmailConnection)
.mutation(async ({ input }) => { .mutation(async ({ input }) => {
try { try {
@@ -436,12 +364,6 @@ export const notificationRouter = createTRPCRouter({
} }
}), }),
createResend: withPermission("notification", "create") 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) .input(apiCreateResend)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
try { try {
@@ -460,12 +382,6 @@ export const notificationRouter = createTRPCRouter({
} }
}), }),
updateResend: withPermission("notification", "update") 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) .input(apiUpdateResend)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
try { try {
@@ -496,12 +412,6 @@ export const notificationRouter = createTRPCRouter({
} }
}), }),
testResendConnection: withPermission("notification", "create") 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) .input(apiTestResendConnection)
.mutation(async ({ input }) => { .mutation(async ({ input }) => {
try { try {
@@ -520,12 +430,6 @@ export const notificationRouter = createTRPCRouter({
} }
}), }),
remove: withPermission("notification", "delete") 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) .input(apiFindOneNotification)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
try { try {
@@ -554,12 +458,6 @@ export const notificationRouter = createTRPCRouter({
} }
}), }),
one: withPermission("notification", "read") 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) .input(apiFindOneNotification)
.query(async ({ input, ctx }) => { .query(async ({ input, ctx }) => {
const notification = await findNotificationById(input.notificationId); const notification = await findNotificationById(input.notificationId);
@@ -571,14 +469,7 @@ export const notificationRouter = createTRPCRouter({
} }
return notification; return notification;
}), }),
all: withPermission("notification", "read") all: withPermission("notification", "read").query(async ({ ctx }) => {
.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({ return await db.query.notifications.findMany({
with: { with: {
slack: true, slack: true,
@@ -599,12 +490,6 @@ export const notificationRouter = createTRPCRouter({
}); });
}), }),
receiveNotification: publicProcedure 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( .input(
z.object({ z.object({
ServerType: z.enum(["Dokploy", "Remote"]).default("Dokploy"), ServerType: z.enum(["Dokploy", "Remote"]).default("Dokploy"),
@@ -666,12 +551,6 @@ export const notificationRouter = createTRPCRouter({
} }
}), }),
createGotify: withPermission("notification", "create") 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) .input(apiCreateGotify)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
try { try {
@@ -690,12 +569,6 @@ export const notificationRouter = createTRPCRouter({
} }
}), }),
updateGotify: withPermission("notification", "update") 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) .input(apiUpdateGotify)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
try { try {
@@ -725,12 +598,6 @@ export const notificationRouter = createTRPCRouter({
} }
}), }),
testGotifyConnection: withPermission("notification", "create") 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) .input(apiTestGotifyConnection)
.mutation(async ({ input }) => { .mutation(async ({ input }) => {
try { try {
@@ -749,12 +616,6 @@ export const notificationRouter = createTRPCRouter({
} }
}), }),
createNtfy: withPermission("notification", "create") 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) .input(apiCreateNtfy)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
try { try {
@@ -773,12 +634,6 @@ export const notificationRouter = createTRPCRouter({
} }
}), }),
updateNtfy: withPermission("notification", "update") 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) .input(apiUpdateNtfy)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
try { try {
@@ -808,12 +663,6 @@ export const notificationRouter = createTRPCRouter({
} }
}), }),
testNtfyConnection: withPermission("notification", "create") 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) .input(apiTestNtfyConnection)
.mutation(async ({ input }) => { .mutation(async ({ input }) => {
try { try {
@@ -837,12 +686,6 @@ export const notificationRouter = createTRPCRouter({
} }
}), }),
createMattermost: withPermission("notification", "create") 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) .input(apiCreateMattermost)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
try { try {
@@ -864,12 +707,6 @@ export const notificationRouter = createTRPCRouter({
} }
}), }),
updateMattermost: withPermission("notification", "update") 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) .input(apiUpdateMattermost)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
try { try {
@@ -899,12 +736,6 @@ export const notificationRouter = createTRPCRouter({
} }
}), }),
testMattermostConnection: withPermission("notification", "create") 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) .input(apiTestMattermostConnection)
.mutation(async ({ input }) => { .mutation(async ({ input }) => {
try { try {
@@ -923,12 +754,6 @@ export const notificationRouter = createTRPCRouter({
} }
}), }),
createCustom: withPermission("notification", "create") 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) .input(apiCreateCustom)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
try { try {
@@ -947,12 +772,6 @@ export const notificationRouter = createTRPCRouter({
} }
}), }),
updateCustom: withPermission("notification", "update") 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) .input(apiUpdateCustom)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
try { try {
@@ -979,12 +798,6 @@ export const notificationRouter = createTRPCRouter({
} }
}), }),
testCustomConnection: withPermission("notification", "create") 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) .input(apiTestCustomConnection)
.mutation(async ({ input }) => { .mutation(async ({ input }) => {
try { try {
@@ -1003,12 +816,6 @@ export const notificationRouter = createTRPCRouter({
} }
}), }),
createLark: withPermission("notification", "create") 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) .input(apiCreateLark)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
try { try {
@@ -1027,12 +834,6 @@ export const notificationRouter = createTRPCRouter({
} }
}), }),
updateLark: withPermission("notification", "update") 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) .input(apiUpdateLark)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
try { try {
@@ -1062,12 +863,6 @@ export const notificationRouter = createTRPCRouter({
} }
}), }),
testLarkConnection: withPermission("notification", "create") 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) .input(apiTestLarkConnection)
.mutation(async ({ input }) => { .mutation(async ({ input }) => {
try { try {
@@ -1087,12 +882,6 @@ export const notificationRouter = createTRPCRouter({
} }
}), }),
createTeams: withPermission("notification", "create") 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) .input(apiCreateTeams)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
try { try {
@@ -1111,12 +900,6 @@ export const notificationRouter = createTRPCRouter({
} }
}), }),
updateTeams: withPermission("notification", "update") 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) .input(apiUpdateTeams)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
try { try {
@@ -1146,12 +929,6 @@ export const notificationRouter = createTRPCRouter({
} }
}), }),
testTeamsConnection: withPermission("notification", "create") 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) .input(apiTestTeamsConnection)
.mutation(async ({ input }) => { .mutation(async ({ input }) => {
try { try {
@@ -1169,12 +946,6 @@ export const notificationRouter = createTRPCRouter({
} }
}), }),
createPushover: withPermission("notification", "create") 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) .input(apiCreatePushover)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
try { try {
@@ -1196,12 +967,6 @@ export const notificationRouter = createTRPCRouter({
} }
}), }),
updatePushover: withPermission("notification", "update") 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) .input(apiUpdatePushover)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
try { try {
@@ -1231,12 +996,6 @@ export const notificationRouter = createTRPCRouter({
} }
}), }),
testPushoverConnection: withPermission("notification", "create") 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) .input(apiTestPushoverConnection)
.mutation(async ({ input }) => { .mutation(async ({ input }) => {
try { try {
@@ -1254,14 +1013,7 @@ export const notificationRouter = createTRPCRouter({
}); });
} }
}), }),
getEmailProviders: withPermission("notification", "read") getEmailProviders: withPermission("notification", "read").query(
.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 }) => { async ({ ctx }) => {
return await db.query.notifications.findMany({ return await db.query.notifications.findMany({
where: eq( where: eq(

View File

@@ -1,5 +1,5 @@
import { db } from "@dokploy/server/db"; import { db } from "@dokploy/server/db";
import { IS_CLOUD } from "@dokploy/server/index"; import { IS_CLOUD, sendInvitationEmail } from "@dokploy/server/index";
import { TRPCError } from "@trpc/server"; import { TRPCError } from "@trpc/server";
import { and, desc, eq, exists } from "drizzle-orm"; import { and, desc, eq, exists } from "drizzle-orm";
import { nanoid } from "nanoid"; import { nanoid } from "nanoid";
@@ -15,12 +15,6 @@ import {
import { createTRPCRouter, protectedProcedure, withPermission } from "../trpc"; import { createTRPCRouter, protectedProcedure, withPermission } from "../trpc";
export const organizationRouter = createTRPCRouter({ export const organizationRouter = createTRPCRouter({
create: protectedProcedure 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( .input(
z.object({ z.object({
name: z.string(), name: z.string(),
@@ -71,14 +65,7 @@ export const organizationRouter = createTRPCRouter({
}); });
return result; return result;
}), }),
all: protectedProcedure all: protectedProcedure.query(async ({ ctx }) => {
.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({ const memberResult = await db.query.organization.findMany({
where: (organization) => where: (organization) =>
exists( exists(
@@ -101,12 +88,6 @@ export const organizationRouter = createTRPCRouter({
return memberResult; return memberResult;
}), }),
one: protectedProcedure 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( .input(
z.object({ z.object({
organizationId: z.string(), organizationId: z.string(),
@@ -133,12 +114,6 @@ export const organizationRouter = createTRPCRouter({
}); });
}), }),
update: protectedProcedure 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( .input(
z.object({ z.object({
organizationId: z.string(), organizationId: z.string(),
@@ -203,12 +178,6 @@ export const organizationRouter = createTRPCRouter({
return result[0]; return result[0];
}), }),
delete: protectedProcedure 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( .input(
z.object({ z.object({
organizationId: z.string(), organizationId: z.string(),
@@ -279,12 +248,6 @@ export const organizationRouter = createTRPCRouter({
return result; return result;
}), }),
inviteMember: withPermission("member", "create") 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( .input(
z.object({ z.object({
email: z.string().email(), email: z.string().email(),
@@ -362,6 +325,24 @@ export const organizationRouter = createTRPCRouter({
}) })
.returning(); .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, { await audit(ctx, {
action: "create", action: "create",
resourceType: "organization", resourceType: "organization",
@@ -372,26 +353,13 @@ export const organizationRouter = createTRPCRouter({
return created; return created;
}), }),
allInvitations: withPermission("member", "create") allInvitations: withPermission("member", "create").query(async ({ ctx }) => {
.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({ return await db.query.invitation.findMany({
where: eq(invitation.organizationId, ctx.session.activeOrganizationId), where: eq(invitation.organizationId, ctx.session.activeOrganizationId),
orderBy: [desc(invitation.status), desc(invitation.expiresAt)], orderBy: [desc(invitation.status), desc(invitation.expiresAt)],
}); });
}), }),
removeInvitation: withPermission("member", "create") 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() })) .input(z.object({ invitationId: z.string() }))
.mutation(async ({ ctx, input }) => { .mutation(async ({ ctx, input }) => {
const invitationResult = await db.query.invitation.findFirst({ const invitationResult = await db.query.invitation.findFirst({
@@ -427,12 +395,6 @@ export const organizationRouter = createTRPCRouter({
return result; return result;
}), }),
updateMemberRole: withPermission("member", "update") 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( .input(
z.object({ z.object({
memberId: z.string(), memberId: z.string(),
@@ -519,12 +481,6 @@ export const organizationRouter = createTRPCRouter({
return true; return true;
}), }),
setDefault: protectedProcedure 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( .input(
z.object({ z.object({
organizationId: z.string().min(1), organizationId: z.string().min(1),
@@ -571,14 +527,7 @@ export const organizationRouter = createTRPCRouter({
}); });
return { success: true }; return { success: true };
}), }),
active: protectedProcedure active: protectedProcedure.query(async ({ ctx }) => {
.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) { if (!ctx.session.activeOrganizationId) {
return null; return null;
} }

View File

@@ -50,12 +50,6 @@ const resolvePatchServiceId = (patch: {
export const patchRouter = createTRPCRouter({ export const patchRouter = createTRPCRouter({
create: protectedProcedure 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) .input(apiCreatePatch)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
const serviceId = input.applicationId ?? input.composeId; const serviceId = input.applicationId ?? input.composeId;
@@ -79,15 +73,7 @@ export const patchRouter = createTRPCRouter({
return result; return result;
}), }),
one: protectedProcedure one: protectedProcedure.input(apiFindPatch).query(async ({ input, ctx }) => {
.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 patch = await findPatchById(input.patchId);
const serviceId = resolvePatchServiceId(patch); const serviceId = resolvePatchServiceId(patch);
await checkServicePermissionAndAccess(ctx, serviceId, { await checkServicePermissionAndAccess(ctx, serviceId, {
@@ -97,12 +83,6 @@ export const patchRouter = createTRPCRouter({
}), }),
byEntityId: protectedProcedure byEntityId: protectedProcedure
.meta({
openapi: {
summary: "List patches by entity",
description: "Returns all patches associated with a given application or compose service.",
},
})
.input( .input(
z.object({ id: z.string(), type: z.enum(["application", "compose"]) }), z.object({ id: z.string(), type: z.enum(["application", "compose"]) }),
) )
@@ -114,12 +94,6 @@ export const patchRouter = createTRPCRouter({
}), }),
update: protectedProcedure 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) .input(apiUpdatePatch)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
const patch = await findPatchById(input.patchId); const patch = await findPatchById(input.patchId);
@@ -140,12 +114,6 @@ export const patchRouter = createTRPCRouter({
}), }),
delete: protectedProcedure 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) .input(apiDeletePatch)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
const patch = await findPatchById(input.patchId); const patch = await findPatchById(input.patchId);
@@ -165,12 +133,6 @@ export const patchRouter = createTRPCRouter({
}), }),
toggleEnabled: protectedProcedure 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) .input(apiTogglePatchEnabled)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
const patch = await findPatchById(input.patchId); const patch = await findPatchById(input.patchId);
@@ -193,12 +155,6 @@ export const patchRouter = createTRPCRouter({
// Repository Operations // Repository Operations
ensureRepo: protectedProcedure 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( .input(
z.object({ z.object({
id: z.string(), id: z.string(),
@@ -223,12 +179,6 @@ export const patchRouter = createTRPCRouter({
}), }),
readRepoDirectories: protectedProcedure 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( .input(
z.object({ z.object({
id: z.string().min(1), id: z.string().min(1),
@@ -252,12 +202,6 @@ export const patchRouter = createTRPCRouter({
}), }),
readRepoFile: protectedProcedure 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( .input(
z.object({ z.object({
id: z.string().min(1), id: z.string().min(1),
@@ -297,12 +241,6 @@ export const patchRouter = createTRPCRouter({
}), }),
saveFileAsPatch: protectedProcedure 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( .input(
z.object({ z.object({
id: z.string().min(1), id: z.string().min(1),
@@ -353,12 +291,6 @@ export const patchRouter = createTRPCRouter({
}), }),
markFileForDeletion: protectedProcedure 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( .input(
z.object({ z.object({
id: z.string().min(1), id: z.string().min(1),
@@ -386,12 +318,6 @@ export const patchRouter = createTRPCRouter({
}), }),
cleanPatchRepos: adminProcedure 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() })) .input(z.object({ serverId: z.string().optional() }))
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
await cleanPatchRepos(input.serverId); await cleanPatchRepos(input.serverId);

View File

@@ -16,12 +16,6 @@ import {
export const portRouter = createTRPCRouter({ export const portRouter = createTRPCRouter({
create: protectedProcedure 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) .input(apiCreatePort)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
try { try {
@@ -45,12 +39,6 @@ export const portRouter = createTRPCRouter({
} }
}), }),
one: protectedProcedure one: protectedProcedure
.meta({
openapi: {
summary: "Get a port",
description: "Returns a single port mapping by its ID, including the associated application details.",
},
})
.input(apiFindOnePort) .input(apiFindOnePort)
.query(async ({ input, ctx }) => { .query(async ({ input, ctx }) => {
try { try {
@@ -70,12 +58,6 @@ export const portRouter = createTRPCRouter({
} }
}), }),
delete: protectedProcedure 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) .input(apiFindOnePort)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
const port = await finPortById(input.portId); const port = await finPortById(input.portId);
@@ -103,12 +85,6 @@ export const portRouter = createTRPCRouter({
} }
}), }),
update: protectedProcedure update: protectedProcedure
.meta({
openapi: {
summary: "Update a port",
description: "Updates an existing port mapping's configuration and logs an audit entry.",
},
})
.input(apiUpdatePort) .input(apiUpdatePort)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
const port = await finPortById(input.portId); const port = await finPortById(input.portId);

View File

@@ -9,7 +9,6 @@ import {
findEnvironmentById, findEnvironmentById,
findPostgresById, findPostgresById,
findProjectById, findProjectById,
getAccessibleServerIds,
getContainerLogs, getContainerLogs,
getMountPath, getMountPath,
getServiceContainerCommand, getServiceContainerCommand,
@@ -22,6 +21,7 @@ import {
stopService, stopService,
stopServiceRemote, stopServiceRemote,
updatePostgresById, updatePostgresById,
getAccessibleServerIds,
} from "@dokploy/server"; } from "@dokploy/server";
import { db } from "@dokploy/server/db"; import { db } from "@dokploy/server/db";
import { import {
@@ -55,12 +55,6 @@ import { cancelJobs } from "@/server/utils/backup";
export const postgresRouter = createTRPCRouter({ export const postgresRouter = createTRPCRouter({
create: protectedProcedure 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) .input(apiCreatePostgres)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
try { try {
@@ -127,12 +121,6 @@ export const postgresRouter = createTRPCRouter({
} }
}), }),
one: protectedProcedure 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) .input(apiFindOnePostgres)
.query(async ({ input, ctx }) => { .query(async ({ input, ctx }) => {
await checkServiceAccess(ctx, input.postgresId, "read"); await checkServiceAccess(ctx, input.postgresId, "read");
@@ -151,12 +139,6 @@ export const postgresRouter = createTRPCRouter({
}), }),
start: protectedProcedure 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) .input(apiFindOnePostgres)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.postgresId, { await checkServicePermissionAndAccess(ctx, input.postgresId, {
@@ -182,12 +164,6 @@ export const postgresRouter = createTRPCRouter({
return service; return service;
}), }),
stop: protectedProcedure 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) .input(apiFindOnePostgres)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.postgresId, { await checkServicePermissionAndAccess(ctx, input.postgresId, {
@@ -212,12 +188,6 @@ export const postgresRouter = createTRPCRouter({
return postgres; return postgres;
}), }),
saveExternalPort: protectedProcedure 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) .input(apiSaveExternalPortPostgres)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.postgresId, { await checkServicePermissionAndAccess(ctx, input.postgresId, {
@@ -251,12 +221,6 @@ export const postgresRouter = createTRPCRouter({
return postgres; return postgres;
}), }),
deploy: protectedProcedure 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) .input(apiDeployPostgres)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.postgresId, { await checkServicePermissionAndAccess(ctx, input.postgresId, {
@@ -312,12 +276,6 @@ export const postgresRouter = createTRPCRouter({
}), }),
changeStatus: protectedProcedure 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) .input(apiChangePostgresStatus)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.postgresId, { await checkServicePermissionAndAccess(ctx, input.postgresId, {
@@ -336,12 +294,6 @@ export const postgresRouter = createTRPCRouter({
return postgres; return postgres;
}), }),
remove: protectedProcedure 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) .input(apiFindOnePostgres)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
await checkServiceAccess(ctx, input.postgresId, "delete"); await checkServiceAccess(ctx, input.postgresId, "delete");
@@ -380,12 +332,6 @@ export const postgresRouter = createTRPCRouter({
return postgres; return postgres;
}), }),
saveEnvironment: protectedProcedure 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) .input(apiSaveEnvironmentVariablesPostgres)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.postgresId, { await checkServicePermissionAndAccess(ctx, input.postgresId, {
@@ -410,12 +356,6 @@ export const postgresRouter = createTRPCRouter({
return true; return true;
}), }),
reload: protectedProcedure reload: protectedProcedure
.meta({
openapi: {
summary: "Reload a PostgreSQL database",
description: "Restarts the PostgreSQL database by stopping and then starting its Docker container.",
},
})
.input(apiResetPostgres) .input(apiResetPostgres)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.postgresId, { await checkServicePermissionAndAccess(ctx, input.postgresId, {
@@ -448,12 +388,6 @@ export const postgresRouter = createTRPCRouter({
return true; return true;
}), }),
update: protectedProcedure update: protectedProcedure
.meta({
openapi: {
summary: "Update a PostgreSQL database",
description: "Updates the configuration of an existing PostgreSQL database service.",
},
})
.input(apiUpdatePostgres) .input(apiUpdatePostgres)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
const { postgresId, ...rest } = input; const { postgresId, ...rest } = input;
@@ -481,12 +415,6 @@ export const postgresRouter = createTRPCRouter({
return true; return true;
}), }),
changePassword: protectedProcedure 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( .input(
z.object({ z.object({
postgresId: z.string().min(1), postgresId: z.string().min(1),
@@ -537,12 +465,6 @@ export const postgresRouter = createTRPCRouter({
return true; return true;
}), }),
move: protectedProcedure 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( .input(
z.object({ z.object({
postgresId: z.string(), postgresId: z.string(),
@@ -579,12 +501,6 @@ export const postgresRouter = createTRPCRouter({
return updatedPostgres; return updatedPostgres;
}), }),
rebuild: protectedProcedure 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) .input(apiRebuildPostgres)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.postgresId, { await checkServicePermissionAndAccess(ctx, input.postgresId, {
@@ -601,12 +517,6 @@ export const postgresRouter = createTRPCRouter({
return true; return true;
}), }),
search: protectedProcedure 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( .input(
z.object({ z.object({
q: z.string().optional(), q: z.string().optional(),
@@ -707,12 +617,6 @@ export const postgresRouter = createTRPCRouter({
}), }),
readLogs: protectedProcedure 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( .input(
apiFindOnePostgres.extend({ apiFindOnePostgres.extend({
tail: z.number().int().min(1).max(10000).default(100), tail: z.number().int().min(1).max(10000).default(100),

View File

@@ -16,12 +16,6 @@ import { createTRPCRouter, protectedProcedure } from "../trpc";
export const previewDeploymentRouter = createTRPCRouter({ export const previewDeploymentRouter = createTRPCRouter({
all: protectedProcedure all: protectedProcedure
.meta({
openapi: {
summary: "List preview deployments",
description: "Returns all preview deployments associated with the given application.",
},
})
.input(apiFindAllByApplication) .input(apiFindAllByApplication)
.query(async ({ input, ctx }) => { .query(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.applicationId, { await checkServicePermissionAndAccess(ctx, input.applicationId, {
@@ -31,12 +25,6 @@ export const previewDeploymentRouter = createTRPCRouter({
}), }),
one: protectedProcedure 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() })) .input(z.object({ previewDeploymentId: z.string() }))
.query(async ({ input, ctx }) => { .query(async ({ input, ctx }) => {
const previewDeployment = await findPreviewDeploymentById( const previewDeployment = await findPreviewDeploymentById(
@@ -51,12 +39,6 @@ export const previewDeploymentRouter = createTRPCRouter({
}), }),
delete: protectedProcedure 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() })) .input(z.object({ previewDeploymentId: z.string() }))
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
const previewDeployment = await findPreviewDeploymentById( const previewDeployment = await findPreviewDeploymentById(
@@ -77,12 +59,6 @@ export const previewDeploymentRouter = createTRPCRouter({
}), }),
redeploy: protectedProcedure 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( .input(
z.object({ z.object({
previewDeploymentId: z.string(), previewDeploymentId: z.string(),

View File

@@ -67,12 +67,6 @@ import {
export const projectRouter = createTRPCRouter({ export const projectRouter = createTRPCRouter({
create: protectedProcedure 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) .input(apiCreateProject)
.mutation(async ({ ctx, input }) => { .mutation(async ({ ctx, input }) => {
try { try {
@@ -112,12 +106,6 @@ export const projectRouter = createTRPCRouter({
}), }),
one: protectedProcedure 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) .input(apiFindOneProject)
.query(async ({ input, ctx }) => { .query(async ({ input, ctx }) => {
if (ctx.user.role !== "owner" && ctx.user.role !== "admin") { if (ctx.user.role !== "owner" && ctx.user.role !== "admin") {
@@ -205,14 +193,7 @@ export const projectRouter = createTRPCRouter({
} }
return project; return project;
}), }),
all: protectedProcedure all: protectedProcedure.query(async ({ ctx }) => {
.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") { if (ctx.user.role !== "owner" && ctx.user.role !== "admin") {
const { accessedProjects, accessedEnvironments, accessedServices } = const { accessedProjects, accessedEnvironments, accessedServices } =
await findMemberByUserId(ctx.user.id, ctx.session.activeOrganizationId); await findMemberByUserId(ctx.user.id, ctx.session.activeOrganizationId);
@@ -394,14 +375,7 @@ export const projectRouter = createTRPCRouter({
}); });
}), }),
allForPermissions: withPermission("member", "update") allForPermissions: withPermission("member", "update").query(
.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 }) => { async ({ ctx }) => {
return await db.query.projects.findMany({ return await db.query.projects.findMany({
where: eq(projects.organizationId, ctx.session.activeOrganizationId), where: eq(projects.organizationId, ctx.session.activeOrganizationId),
@@ -513,13 +487,149 @@ export const projectRouter = createTRPCRouter({
}, },
), ),
search: protectedProcedure homeStats: protectedProcedure.query(async ({ ctx }) => {
.meta({ const isPrivileged = ctx.user.role === "owner" || ctx.user.role === "admin";
openapi: {
summary: "Search projects", let accessedProjects: string[] = [];
description: "Searches projects by name or description with pagination. Respects project-level access control for non-admin users.", let accessedEnvironments: string[] = [];
let accessedServices: string[] = [];
if (!isPrivileged) {
const member = await findMemberByUserId(
ctx.user.id,
ctx.session.activeOrganizationId,
);
accessedProjects = member.accessedProjects;
accessedEnvironments = member.accessedEnvironments;
accessedServices = member.accessedServices;
if (accessedProjects.length === 0) {
return {
projects: 0,
environments: 0,
applications: 0,
compose: 0,
databases: 0,
services: 0,
status: { running: 0, error: 0, idle: 0 },
};
}
}
const projectIdFilter = isPrivileged
? eq(projects.organizationId, ctx.session.activeOrganizationId)
: and(
sql`${projects.projectId} IN (${sql.join(
accessedProjects.map((id) => sql`${id}`),
sql`, `,
)})`,
eq(projects.organizationId, ctx.session.activeOrganizationId),
);
const environmentFilter = isPrivileged
? undefined
: accessedEnvironments.length === 0
? sql`false`
: sql`${environments.environmentId} IN (${sql.join(
accessedEnvironments.map((envId) => sql`${envId}`),
sql`, `,
)})`;
const applyFilter = (col: AnyPgColumn) =>
isPrivileged ? undefined : buildServiceFilter(col, accessedServices);
const rows = await db.query.projects.findMany({
where: projectIdFilter,
columns: { projectId: true },
with: {
environments: {
where: environmentFilter,
columns: { environmentId: true },
with: {
applications: {
where: applyFilter(applications.applicationId),
columns: { applicationStatus: true },
},
compose: {
where: applyFilter(compose.composeId),
columns: { composeStatus: true },
},
libsql: {
where: applyFilter(libsql.libsqlId),
columns: { applicationStatus: true },
},
mariadb: {
where: applyFilter(mariadb.mariadbId),
columns: { applicationStatus: true },
},
mongo: {
where: applyFilter(mongo.mongoId),
columns: { applicationStatus: true },
},
mysql: {
where: applyFilter(mysql.mysqlId),
columns: { applicationStatus: true },
},
postgres: {
where: applyFilter(postgres.postgresId),
columns: { applicationStatus: true },
},
redis: {
where: applyFilter(redis.redisId),
columns: { applicationStatus: true },
},
},
},
}, },
}) });
let applicationsCount = 0;
let composeCount = 0;
let databasesCount = 0;
let environmentsCount = 0;
const status = { running: 0, error: 0, idle: 0 };
const bump = (s?: string | null) => {
if (s === "done") status.running++;
else if (s === "error") status.error++;
else status.idle++;
};
for (const project of rows) {
for (const env of project.environments) {
environmentsCount++;
applicationsCount += env.applications.length;
composeCount += env.compose.length;
databasesCount +=
env.libsql.length +
env.mariadb.length +
env.mongo.length +
env.mysql.length +
env.postgres.length +
env.redis.length;
for (const a of env.applications) bump(a.applicationStatus);
for (const c of env.compose) bump(c.composeStatus);
for (const s of env.libsql) bump(s.applicationStatus);
for (const s of env.mariadb) bump(s.applicationStatus);
for (const s of env.mongo) bump(s.applicationStatus);
for (const s of env.mysql) bump(s.applicationStatus);
for (const s of env.postgres) bump(s.applicationStatus);
for (const s of env.redis) bump(s.applicationStatus);
}
}
return {
projects: rows.length,
environments: environmentsCount,
applications: applicationsCount,
compose: composeCount,
databases: databasesCount,
services: applicationsCount + composeCount + databasesCount,
status,
};
}),
search: protectedProcedure
.input( .input(
z.object({ z.object({
q: z.string().optional(), q: z.string().optional(),
@@ -597,12 +707,6 @@ export const projectRouter = createTRPCRouter({
}), }),
remove: protectedProcedure remove: protectedProcedure
.meta({
openapi: {
summary: "Delete a project",
description: "Permanently deletes a project and all its associated environments, services, and resources.",
},
})
.input(apiRemoveProject) .input(apiRemoveProject)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
try { try {
@@ -630,12 +734,6 @@ export const projectRouter = createTRPCRouter({
} }
}), }),
update: protectedProcedure 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) .input(apiUpdateProject)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
try { try {
@@ -684,12 +782,6 @@ export const projectRouter = createTRPCRouter({
} }
}), }),
duplicate: protectedProcedure 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( .input(
z.object({ z.object({
sourceEnvironmentId: z.string(), sourceEnvironmentId: z.string(),

View File

@@ -15,12 +15,6 @@ import {
export const redirectsRouter = createTRPCRouter({ export const redirectsRouter = createTRPCRouter({
create: protectedProcedure 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) .input(apiCreateRedirect)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.applicationId, { await checkServicePermissionAndAccess(ctx, input.applicationId, {
@@ -37,12 +31,6 @@ export const redirectsRouter = createTRPCRouter({
}), }),
one: protectedProcedure one: protectedProcedure
.meta({
openapi: {
summary: "Get a redirect",
description: "Returns a single redirect rule by its ID.",
},
})
.input(apiFindOneRedirect) .input(apiFindOneRedirect)
.query(async ({ input, ctx }) => { .query(async ({ input, ctx }) => {
const redirect = await findRedirectById(input.redirectId); const redirect = await findRedirectById(input.redirectId);
@@ -53,12 +41,6 @@ export const redirectsRouter = createTRPCRouter({
}), }),
delete: protectedProcedure delete: protectedProcedure
.meta({
openapi: {
summary: "Delete a redirect",
description: "Deletes a redirect rule by its ID and logs an audit entry.",
},
})
.input(apiFindOneRedirect) .input(apiFindOneRedirect)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
const redirect = await findRedirectById(input.redirectId); const redirect = await findRedirectById(input.redirectId);
@@ -75,12 +57,6 @@ export const redirectsRouter = createTRPCRouter({
}), }),
update: protectedProcedure update: protectedProcedure
.meta({
openapi: {
summary: "Update a redirect",
description: "Updates an existing redirect rule's configuration and logs an audit entry.",
},
})
.input(apiUpdateRedirect) .input(apiUpdateRedirect)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
const redirect = await findRedirectById(input.redirectId); const redirect = await findRedirectById(input.redirectId);

View File

@@ -51,12 +51,6 @@ import {
} from "@/server/db/schema"; } from "@/server/db/schema";
export const redisRouter = createTRPCRouter({ export const redisRouter = createTRPCRouter({
create: protectedProcedure 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) .input(apiCreateRedis)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
try { try {
@@ -114,12 +108,6 @@ export const redisRouter = createTRPCRouter({
} }
}), }),
one: protectedProcedure 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) .input(apiFindOneRedis)
.query(async ({ input, ctx }) => { .query(async ({ input, ctx }) => {
await checkServiceAccess(ctx, input.redisId, "read"); await checkServiceAccess(ctx, input.redisId, "read");
@@ -138,12 +126,6 @@ export const redisRouter = createTRPCRouter({
}), }),
start: protectedProcedure 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) .input(apiFindOneRedis)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.redisId, { await checkServicePermissionAndAccess(ctx, input.redisId, {
@@ -169,12 +151,6 @@ export const redisRouter = createTRPCRouter({
return redis; return redis;
}), }),
reload: protectedProcedure reload: protectedProcedure
.meta({
openapi: {
summary: "Reload a Redis database",
description: "Restarts the Redis database by stopping and then starting its Docker container.",
},
})
.input(apiResetRedis) .input(apiResetRedis)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.redisId, { await checkServicePermissionAndAccess(ctx, input.redisId, {
@@ -208,12 +184,6 @@ export const redisRouter = createTRPCRouter({
}), }),
stop: protectedProcedure 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) .input(apiFindOneRedis)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.redisId, { await checkServicePermissionAndAccess(ctx, input.redisId, {
@@ -238,12 +208,6 @@ export const redisRouter = createTRPCRouter({
return redis; return redis;
}), }),
saveExternalPort: protectedProcedure 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) .input(apiSaveExternalPortRedis)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.redisId, { await checkServicePermissionAndAccess(ctx, input.redisId, {
@@ -277,12 +241,6 @@ export const redisRouter = createTRPCRouter({
return redis; return redis;
}), }),
deploy: protectedProcedure 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) .input(apiDeployRedis)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.redisId, { await checkServicePermissionAndAccess(ctx, input.redisId, {
@@ -335,12 +293,6 @@ export const redisRouter = createTRPCRouter({
} }
}), }),
changeStatus: protectedProcedure 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) .input(apiChangeRedisStatus)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.redisId, { await checkServicePermissionAndAccess(ctx, input.redisId, {
@@ -359,12 +311,6 @@ export const redisRouter = createTRPCRouter({
return mongo; return mongo;
}), }),
remove: protectedProcedure 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) .input(apiFindOneRedis)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
await checkServiceAccess(ctx, input.redisId, "delete"); await checkServiceAccess(ctx, input.redisId, "delete");
@@ -400,12 +346,6 @@ export const redisRouter = createTRPCRouter({
return redis; return redis;
}), }),
saveEnvironment: protectedProcedure 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) .input(apiSaveEnvironmentVariablesRedis)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.redisId, { await checkServicePermissionAndAccess(ctx, input.redisId, {
@@ -430,12 +370,6 @@ export const redisRouter = createTRPCRouter({
return true; return true;
}), }),
update: protectedProcedure update: protectedProcedure
.meta({
openapi: {
summary: "Update a Redis database",
description: "Updates the configuration of an existing Redis database service.",
},
})
.input(apiUpdateRedis) .input(apiUpdateRedis)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
const { redisId, ...rest } = input; const { redisId, ...rest } = input;
@@ -462,12 +396,6 @@ export const redisRouter = createTRPCRouter({
return true; return true;
}), }),
changePassword: protectedProcedure 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( .input(
z.object({ z.object({
redisId: z.string().min(1), redisId: z.string().min(1),
@@ -518,12 +446,6 @@ export const redisRouter = createTRPCRouter({
return true; return true;
}), }),
move: protectedProcedure 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( .input(
z.object({ z.object({
redisId: z.string(), redisId: z.string(),
@@ -560,12 +482,6 @@ export const redisRouter = createTRPCRouter({
return updatedRedis; return updatedRedis;
}), }),
rebuild: protectedProcedure 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) .input(apiRebuildRedis)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.redisId, { await checkServicePermissionAndAccess(ctx, input.redisId, {
@@ -581,12 +497,6 @@ export const redisRouter = createTRPCRouter({
return true; return true;
}), }),
search: protectedProcedure 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( .input(
z.object({ z.object({
q: z.string().optional(), q: z.string().optional(),
@@ -680,12 +590,6 @@ export const redisRouter = createTRPCRouter({
}), }),
readLogs: protectedProcedure 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( .input(
apiFindOneRedis.extend({ apiFindOneRedis.extend({
tail: z.number().int().min(1).max(10000).default(100), tail: z.number().int().min(1).max(10000).default(100),

View File

@@ -23,12 +23,6 @@ import {
import { createTRPCRouter, withPermission } from "../trpc"; import { createTRPCRouter, withPermission } from "../trpc";
export const registryRouter = createTRPCRouter({ export const registryRouter = createTRPCRouter({
create: withPermission("registry", "create") 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) .input(apiCreateRegistry)
.mutation(async ({ ctx, input }) => { .mutation(async ({ ctx, input }) => {
const reg = await createRegistry(input, ctx.session.activeOrganizationId); const reg = await createRegistry(input, ctx.session.activeOrganizationId);
@@ -41,12 +35,6 @@ export const registryRouter = createTRPCRouter({
return reg; return reg;
}), }),
remove: withPermission("registry", "delete") 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) .input(apiRemoveRegistry)
.mutation(async ({ ctx, input }) => { .mutation(async ({ ctx, input }) => {
const registry = await findRegistryById(input.registryId); const registry = await findRegistryById(input.registryId);
@@ -65,12 +53,6 @@ export const registryRouter = createTRPCRouter({
return await removeRegistry(input.registryId); return await removeRegistry(input.registryId);
}), }),
update: withPermission("registry", "create") 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) .input(apiUpdateRegistry)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
const { registryId, ...rest } = input; const { registryId, ...rest } = input;
@@ -100,26 +82,13 @@ export const registryRouter = createTRPCRouter({
}); });
return true; return true;
}), }),
all: withPermission("registry", "read") all: withPermission("registry", "read").query(async ({ ctx }) => {
.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({ const registryResponse = await db.query.registry.findMany({
where: eq(registry.organizationId, ctx.session.activeOrganizationId), where: eq(registry.organizationId, ctx.session.activeOrganizationId),
}); });
return registryResponse; return registryResponse;
}), }),
one: withPermission("registry", "read") 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) .input(apiFindOneRegistry)
.query(async ({ input, ctx }) => { .query(async ({ input, ctx }) => {
const registry = await findRegistryById(input.registryId); const registry = await findRegistryById(input.registryId);
@@ -132,12 +101,6 @@ export const registryRouter = createTRPCRouter({
return registry; return registry;
}), }),
testRegistry: withPermission("registry", "read") 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) .input(apiTestRegistry)
.mutation(async ({ input }) => { .mutation(async ({ input }) => {
try { try {
@@ -180,12 +143,6 @@ export const registryRouter = createTRPCRouter({
} }
}), }),
testRegistryById: withPermission("registry", "read") 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) .input(apiTestRegistryById)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
try { try {

View File

@@ -11,12 +11,6 @@ import { createTRPCRouter, protectedProcedure } from "../trpc";
export const rollbackRouter = createTRPCRouter({ export const rollbackRouter = createTRPCRouter({
delete: protectedProcedure delete: protectedProcedure
.meta({
openapi: {
summary: "Delete a rollback",
description: "Permanently removes a rollback record by its ID.",
},
})
.input(apiFindOneRollback) .input(apiFindOneRollback)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
try { try {
@@ -46,12 +40,6 @@ export const rollbackRouter = createTRPCRouter({
} }
}), }),
rollback: protectedProcedure 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) .input(apiFindOneRollback)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
try { try {

View File

@@ -7,27 +7,27 @@ import {
updateScheduleSchema, updateScheduleSchema,
} from "@dokploy/server/db/schema/schedule"; } from "@dokploy/server/db/schema/schedule";
import { runCommand } from "@dokploy/server/index"; import { runCommand } from "@dokploy/server/index";
import { checkServicePermissionAndAccess } from "@dokploy/server/services/permission"; import {
checkPermission,
checkServicePermissionAndAccess,
findMemberByUserId,
} from "@dokploy/server/services/permission";
import { import {
createSchedule, createSchedule,
deleteSchedule, deleteSchedule,
findScheduleById, findScheduleById,
updateSchedule, updateSchedule,
} from "@dokploy/server/services/schedule"; } from "@dokploy/server/services/schedule";
import { findServerById } from "@dokploy/server/services/server";
import { TRPCError } from "@trpc/server"; import { TRPCError } from "@trpc/server";
import { asc, desc, eq } from "drizzle-orm"; import { asc, desc, eq } from "drizzle-orm";
import { z } from "zod"; import { z } from "zod";
import { audit } from "@/server/api/utils/audit"; import { audit } from "@/server/api/utils/audit";
import { removeJob, schedule } from "@/server/utils/backup"; import { removeJob, schedule } from "@/server/utils/backup";
import { createTRPCRouter, protectedProcedure } from "../trpc"; import { createTRPCRouter, protectedProcedure } from "../trpc";
export const scheduleRouter = createTRPCRouter({ export const scheduleRouter = createTRPCRouter({
create: protectedProcedure 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) .input(createScheduleSchema)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
const serviceId = input.applicationId || input.composeId; const serviceId = input.applicationId || input.composeId;
@@ -35,6 +35,45 @@ export const scheduleRouter = createTRPCRouter({
await checkServicePermissionAndAccess(ctx, serviceId, { await checkServicePermissionAndAccess(ctx, serviceId, {
schedule: ["create"], 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); const newSchedule = await createSchedule(input);
@@ -60,21 +99,80 @@ export const scheduleRouter = createTRPCRouter({
}), }),
update: protectedProcedure 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) .input(updateScheduleSchema)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
const existingSchedule = await findScheduleById(input.scheduleId); 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 = const serviceId =
existingSchedule.applicationId || existingSchedule.composeId; existingSchedule.applicationId || existingSchedule.composeId;
if (serviceId) { if (serviceId) {
await checkServicePermissionAndAccess(ctx, serviceId, { await checkServicePermissionAndAccess(ctx, serviceId, {
schedule: ["update"], 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); const updatedSchedule = await updateSchedule(input);
@@ -111,12 +209,6 @@ export const scheduleRouter = createTRPCRouter({
}), }),
delete: protectedProcedure 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() })) .input(z.object({ scheduleId: z.string() }))
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
const scheduleItem = await findScheduleById(input.scheduleId); const scheduleItem = await findScheduleById(input.scheduleId);
@@ -125,6 +217,56 @@ export const scheduleRouter = createTRPCRouter({
await checkServicePermissionAndAccess(ctx, serviceId, { await checkServicePermissionAndAccess(ctx, serviceId, {
schedule: ["delete"], 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); await deleteSchedule(input.scheduleId);
@@ -147,12 +289,6 @@ export const scheduleRouter = createTRPCRouter({
}), }),
list: protectedProcedure 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( .input(
z.object({ z.object({
id: z.string(), id: z.string(),
@@ -172,6 +308,30 @@ export const scheduleRouter = createTRPCRouter({
await checkServicePermissionAndAccess(ctx, input.id, { await checkServicePermissionAndAccess(ctx, input.id, {
schedule: ["read"], 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 = { const where = {
application: eq(schedules.applicationId, input.id), application: eq(schedules.applicationId, input.id),
@@ -194,12 +354,6 @@ export const scheduleRouter = createTRPCRouter({
}), }),
one: protectedProcedure 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() })) .input(z.object({ scheduleId: z.string() }))
.query(async ({ input, ctx }) => { .query(async ({ input, ctx }) => {
const schedule = await findScheduleById(input.scheduleId); const schedule = await findScheduleById(input.scheduleId);
@@ -208,17 +362,36 @@ export const scheduleRouter = createTRPCRouter({
await checkServicePermissionAndAccess(ctx, serviceId, { await checkServicePermissionAndAccess(ctx, serviceId, {
schedule: ["read"], 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; return schedule;
}), }),
runManually: protectedProcedure 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) })) .input(z.object({ scheduleId: z.string().min(1) }))
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
const scheduleItem = await findScheduleById(input.scheduleId); const scheduleItem = await findScheduleById(input.scheduleId);
@@ -227,6 +400,56 @@ export const scheduleRouter = createTRPCRouter({
await checkServicePermissionAndAccess(ctx, serviceId, { await checkServicePermissionAndAccess(ctx, serviceId, {
schedule: ["create"], 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 { try {
await runCommand(input.scheduleId); await runCommand(input.scheduleId);

View File

@@ -15,12 +15,6 @@ import {
export const securityRouter = createTRPCRouter({ export const securityRouter = createTRPCRouter({
create: protectedProcedure 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) .input(apiCreateSecurity)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.applicationId, { await checkServicePermissionAndAccess(ctx, input.applicationId, {
@@ -37,12 +31,6 @@ export const securityRouter = createTRPCRouter({
}), }),
one: protectedProcedure one: protectedProcedure
.meta({
openapi: {
summary: "Get a security entry",
description: "Returns a single HTTP basic auth security entry by its ID.",
},
})
.input(apiFindOneSecurity) .input(apiFindOneSecurity)
.query(async ({ input, ctx }) => { .query(async ({ input, ctx }) => {
const security = await findSecurityById(input.securityId); const security = await findSecurityById(input.securityId);
@@ -53,12 +41,6 @@ export const securityRouter = createTRPCRouter({
}), }),
delete: protectedProcedure 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) .input(apiFindOneSecurity)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
const security = await findSecurityById(input.securityId); const security = await findSecurityById(input.securityId);
@@ -75,12 +57,6 @@ export const securityRouter = createTRPCRouter({
}), }),
update: protectedProcedure 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) .input(apiUpdateSecurity)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
const security = await findSecurityById(input.securityId); const security = await findSecurityById(input.securityId);

View File

@@ -48,12 +48,6 @@ import {
export const serverRouter = createTRPCRouter({ export const serverRouter = createTRPCRouter({
create: withPermission("server", "create") 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) .input(apiCreateServer)
.mutation(async ({ ctx, input }) => { .mutation(async ({ ctx, input }) => {
try { try {
@@ -86,12 +80,6 @@ export const serverRouter = createTRPCRouter({
}), }),
one: withPermission("server", "read") 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) .input(apiFindOneServer)
.query(async ({ input, ctx }) => { .query(async ({ input, ctx }) => {
const server = await findServerById(input.serverId); const server = await findServerById(input.serverId);
@@ -113,26 +101,13 @@ export const serverRouter = createTRPCRouter({
return server; return server;
}), }),
getDefaultCommand: withPermission("server", "read") 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) .input(apiFindOneServer)
.query(async ({ input }) => { .query(async ({ input }) => {
const server = await findServerById(input.serverId); const server = await findServerById(input.serverId);
const isBuildServer = server.serverType === "build"; const isBuildServer = server.serverType === "build";
return defaultCommand(isBuildServer); return defaultCommand(isBuildServer);
}), }),
all: withPermission("server", "read") all: withPermission("server", "read").query(async ({ ctx }) => {
.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 accessibleIds = await getAccessibleServerIds(ctx.session);
const result = await db const result = await db
@@ -155,12 +130,6 @@ export const serverRouter = createTRPCRouter({
return result.filter((s) => accessibleIds.has(s.serverId)); return result.filter((s) => accessibleIds.has(s.serverId));
}), }),
allForPermissions: withPermission("member", "update") 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 }) => { .use(async ({ ctx, next }) => {
const licensed = await hasValidLicense(ctx.session.activeOrganizationId); const licensed = await hasValidLicense(ctx.session.activeOrganizationId);
if (!licensed) { if (!licensed) {
@@ -183,14 +152,7 @@ export const serverRouter = createTRPCRouter({
where: eq(server.organizationId, ctx.session.activeOrganizationId), where: eq(server.organizationId, ctx.session.activeOrganizationId),
}); });
}), }),
count: protectedProcedure count: protectedProcedure.query(async ({ ctx }) => {
.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({ const organizations = await db.query.organization.findMany({
where: eq(organization.ownerId, ctx.user.id), where: eq(organization.ownerId, ctx.user.id),
with: { with: {
@@ -202,14 +164,7 @@ export const serverRouter = createTRPCRouter({
return servers.length ?? 0; return servers.length ?? 0;
}), }),
withSSHKey: withPermission("server", "read") withSSHKey: withPermission("server", "read").query(async ({ ctx }) => {
.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 accessibleIds = await getAccessibleServerIds(ctx.session);
const result = await db.query.server.findMany({ const result = await db.query.server.findMany({
@@ -229,14 +184,7 @@ export const serverRouter = createTRPCRouter({
}); });
return result.filter((s) => accessibleIds.has(s.serverId)); return result.filter((s) => accessibleIds.has(s.serverId));
}), }),
buildServers: withPermission("server", "read") buildServers: withPermission("server", "read").query(async ({ ctx }) => {
.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 accessibleIds = await getAccessibleServerIds(ctx.session);
const result = await db.query.server.findMany({ const result = await db.query.server.findMany({
@@ -257,12 +205,6 @@ export const serverRouter = createTRPCRouter({
return result.filter((s) => accessibleIds.has(s.serverId)); return result.filter((s) => accessibleIds.has(s.serverId));
}), }),
setup: withPermission("server", "create") 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) .input(apiFindOneServer)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
try { try {
@@ -314,12 +256,6 @@ export const serverRouter = createTRPCRouter({
} }
}), }),
validate: withPermission("server", "read") 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) .input(apiFindOneServer)
.query(async ({ input, ctx }) => { .query(async ({ input, ctx }) => {
try { try {
@@ -368,12 +304,6 @@ export const serverRouter = createTRPCRouter({
}), }),
security: withPermission("server", "read") 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) .input(apiFindOneServer)
.query(async ({ input, ctx }) => { .query(async ({ input, ctx }) => {
try { try {
@@ -424,12 +354,6 @@ export const serverRouter = createTRPCRouter({
} }
}), }),
setupMonitoring: withPermission("server", "create") 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) .input(apiUpdateServerMonitoring)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
try { try {
@@ -478,12 +402,6 @@ export const serverRouter = createTRPCRouter({
} }
}), }),
remove: withPermission("server", "delete") 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) .input(apiRemoveServer)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
try { try {
@@ -517,12 +435,6 @@ export const serverRouter = createTRPCRouter({
} }
}), }),
update: withPermission("server", "create") 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) .input(apiUpdateServer)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
try { try {
@@ -555,28 +467,14 @@ export const serverRouter = createTRPCRouter({
throw error; throw error;
} }
}), }),
publicIp: protectedProcedure publicIp: protectedProcedure.query(async () => {
.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) { if (IS_CLOUD) {
return ""; return "";
} }
const ip = await getPublicIpWithFallback(); const ip = await getPublicIpWithFallback();
return ip; return ip;
}), }),
getServerTime: protectedProcedure getServerTime: protectedProcedure.query(() => {
.meta({
openapi: {
summary: "Get server time",
description: "Returns the current server time and timezone. Returns null in cloud mode.",
},
})
.query(() => {
if (IS_CLOUD) { if (IS_CLOUD) {
return null; return null;
} }
@@ -586,12 +484,6 @@ export const serverRouter = createTRPCRouter({
}; };
}), }),
getServerMetrics: withPermission("monitoring", "read") 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( .input(
z.object({ z.object({
url: z.string(), url: z.string(),

File diff suppressed because it is too large Load Diff

View File

@@ -25,12 +25,6 @@ import {
export const sshRouter = createTRPCRouter({ export const sshRouter = createTRPCRouter({
create: withPermission("sshKeys", "create") 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) .input(apiCreateSshKey)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
try { try {
@@ -52,12 +46,6 @@ export const sshRouter = createTRPCRouter({
} }
}), }),
remove: withPermission("sshKeys", "delete") 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) .input(apiRemoveSshKey)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
try { try {
@@ -81,12 +69,6 @@ export const sshRouter = createTRPCRouter({
} }
}), }),
one: withPermission("sshKeys", "read") 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) .input(apiFindOneSshKey)
.query(async ({ input, ctx }) => { .query(async ({ input, ctx }) => {
const sshKey = await findSSHKeyById(input.sshKeyId); const sshKey = await findSSHKeyById(input.sshKeyId);
@@ -99,27 +81,13 @@ export const sshRouter = createTRPCRouter({
} }
return sshKey; return sshKey;
}), }),
all: withPermission("sshKeys", "read") all: withPermission("sshKeys", "read").query(async ({ ctx }) => {
.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({ return await db.query.sshKeys.findMany({
where: eq(sshKeys.organizationId, ctx.session.activeOrganizationId), where: eq(sshKeys.organizationId, ctx.session.activeOrganizationId),
orderBy: desc(sshKeys.createdAt), orderBy: desc(sshKeys.createdAt),
}); });
}), }),
allForApps: protectedProcedure allForApps: protectedProcedure.query(async ({ ctx }) => {
.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({ return await db.query.sshKeys.findMany({
columns: { columns: {
sshKeyId: true, sshKeyId: true,
@@ -130,23 +98,11 @@ export const sshRouter = createTRPCRouter({
}); });
}), }),
generate: withPermission("sshKeys", "read") 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) .input(apiGenerateSSHKey)
.mutation(async ({ input }) => { .mutation(async ({ input }) => {
return await generateSSHKey(input.type); return await generateSSHKey(input.type);
}), }),
update: withPermission("sshKeys", "create") 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) .input(apiUpdateSshKey)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
try { try {

View File

@@ -30,14 +30,7 @@ import {
export const stripeRouter = createTRPCRouter({ export const stripeRouter = createTRPCRouter({
/** Returns the current billing plan for the user's organization. Used to gate features like chat (Startup only). */ /** Returns the current billing plan for the user's organization. Used to gate features like chat (Startup only). */
getCurrentPlan: protectedProcedure getCurrentPlan: protectedProcedure.query(async ({ ctx }) => {
.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; if (!IS_CLOUD) return null;
const owner = await findUserById(ctx.user.ownerId); const owner = await findUserById(ctx.user.ownerId);
if (!owner?.stripeCustomerId) return null; if (!owner?.stripeCustomerId) return null;
@@ -78,14 +71,7 @@ export const stripeRouter = createTRPCRouter({
return null; return null;
}), }),
getProducts: adminProcedure getProducts: adminProcedure.query(async ({ ctx }) => {
.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 user = await findUserById(ctx.user.ownerId);
const stripeCustomerId = user.stripeCustomerId; const stripeCustomerId = user.stripeCustomerId;
@@ -176,12 +162,6 @@ export const stripeRouter = createTRPCRouter({
}; };
}), }),
createCheckoutSession: adminProcedure 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( .input(
z z
.object({ .object({
@@ -242,14 +222,7 @@ export const stripeRouter = createTRPCRouter({
return { sessionId: session.id }; return { sessionId: session.id };
}), }),
createCustomerPortalSession: adminProcedure createCustomerPortalSession: adminProcedure.mutation(async ({ ctx }) => {
.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 // Use the organization's owner account for billing portal
const owner = await findUserById(ctx.user.ownerId); const owner = await findUserById(ctx.user.ownerId);
@@ -280,12 +253,6 @@ export const stripeRouter = createTRPCRouter({
}), }),
upgradeSubscription: adminProcedure 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( .input(
z z
.object({ .object({
@@ -357,14 +324,7 @@ export const stripeRouter = createTRPCRouter({
return { ok: true }; return { ok: true };
}), }),
canCreateMoreServers: withPermission("server", "create") canCreateMoreServers: withPermission("server", "create").query(
.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 }) => { async ({ ctx }) => {
const user = await findUserById(ctx.user.ownerId); const user = await findUserById(ctx.user.ownerId);
const servers = await findServersByUserId(user.id); const servers = await findServersByUserId(user.id);
@@ -378,12 +338,6 @@ export const stripeRouter = createTRPCRouter({
), ),
updateInvoiceNotifications: adminProcedure 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() })) .input(z.object({ enabled: z.boolean() }))
.mutation(async ({ ctx, input }) => { .mutation(async ({ ctx, input }) => {
if (!IS_CLOUD) { if (!IS_CLOUD) {
@@ -399,14 +353,7 @@ export const stripeRouter = createTRPCRouter({
return { ok: true }; return { ok: true };
}), }),
getInvoices: adminProcedure getInvoices: adminProcedure.query(async ({ ctx }) => {
.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 user = await findUserById(ctx.user.ownerId);
const stripeCustomerId = user.stripeCustomerId; const stripeCustomerId = user.stripeCustomerId;

View File

@@ -13,12 +13,6 @@ import { containerIdRegex } from "./docker";
export const swarmRouter = createTRPCRouter({ export const swarmRouter = createTRPCRouter({
getNodes: withPermission("server", "read") 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( .input(
z.object({ z.object({
serverId: z.string().optional(), serverId: z.string().optional(),
@@ -28,23 +22,11 @@ export const swarmRouter = createTRPCRouter({
return await getSwarmNodes(input.serverId); return await getSwarmNodes(input.serverId);
}), }),
getNodeInfo: withPermission("server", "read") 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() })) .input(z.object({ nodeId: z.string(), serverId: z.string().optional() }))
.query(async ({ input }) => { .query(async ({ input }) => {
return await getNodeInfo(input.nodeId, input.serverId); return await getNodeInfo(input.nodeId, input.serverId);
}), }),
getNodeApps: withPermission("server", "read") 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( .input(
z.object({ z.object({
serverId: z.string().optional(), serverId: z.string().optional(),
@@ -76,12 +58,6 @@ export const swarmRouter = createTRPCRouter({
return await getApplicationInfo(input.appName, input.serverId); return await getApplicationInfo(input.appName, input.serverId);
}), }),
getContainerStats: withPermission("server", "read") 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( .input(
z.object({ z.object({
serverId: z.string().optional(), serverId: z.string().optional(),

View File

@@ -16,12 +16,6 @@ import { createTRPCRouter, protectedProcedure, withPermission } from "../trpc";
export const tagRouter = createTRPCRouter({ export const tagRouter = createTRPCRouter({
create: withPermission("tag", "create") 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) .input(apiCreateTag)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
try { try {
@@ -53,14 +47,7 @@ export const tagRouter = createTRPCRouter({
} }
}), }),
all: protectedProcedure all: protectedProcedure.query(async ({ ctx }) => {
.meta({
openapi: {
summary: "List all tags",
description: "Returns all tags for the current organization, ordered alphabetically by name.",
},
})
.query(async ({ ctx }) => {
try { try {
const organizationTags = await db.query.tags.findMany({ const organizationTags = await db.query.tags.findMany({
where: eq(tags.organizationId, ctx.session.activeOrganizationId), where: eq(tags.organizationId, ctx.session.activeOrganizationId),
@@ -77,15 +64,7 @@ export const tagRouter = createTRPCRouter({
} }
}), }),
one: protectedProcedure one: protectedProcedure.input(apiFindOneTag).query(async ({ input, ctx }) => {
.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 { try {
const tag = await db.query.tags.findFirst({ const tag = await db.query.tags.findFirst({
where: and( where: and(
@@ -115,12 +94,6 @@ export const tagRouter = createTRPCRouter({
}), }),
update: withPermission("tag", "update") 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) .input(apiUpdateTag)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
try { try {
@@ -171,12 +144,6 @@ export const tagRouter = createTRPCRouter({
}), }),
remove: withPermission("tag", "delete") 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) .input(apiRemoveTag)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
try { try {
@@ -212,12 +179,6 @@ export const tagRouter = createTRPCRouter({
}), }),
assignToProject: protectedProcedure 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( .input(
z.object({ z.object({
projectId: z.string().min(1), projectId: z.string().min(1),
@@ -306,12 +267,6 @@ export const tagRouter = createTRPCRouter({
}), }),
removeFromProject: protectedProcedure 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( .input(
z.object({ z.object({
projectId: z.string().min(1), projectId: z.string().min(1),
@@ -392,12 +347,6 @@ export const tagRouter = createTRPCRouter({
}), }),
bulkAssign: protectedProcedure 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( .input(
z.object({ z.object({
projectId: z.string().min(1), projectId: z.string().min(1),

View File

@@ -9,12 +9,12 @@ import {
getWebServerSettings, getWebServerSettings,
IS_CLOUD, IS_CLOUD,
removeUserById, removeUserById,
renderInvitationEmail,
sendEmailNotification, sendEmailNotification,
sendResendNotification, sendResendNotification,
updateUser, updateUser,
} from "@dokploy/server"; } from "@dokploy/server";
import { db } from "@dokploy/server/db"; import { db } from "@dokploy/server/db";
import { hasValidLicense } from "@dokploy/server/services/proprietary/license-key";
import { import {
account, account,
apiAssignPermissions, apiAssignPermissions,
@@ -29,6 +29,7 @@ import {
hasPermission, hasPermission,
resolvePermissions, resolvePermissions,
} from "@dokploy/server/services/permission"; } from "@dokploy/server/services/permission";
import { hasValidLicense } from "@dokploy/server/services/proprietary/license-key";
import { TRPCError } from "@trpc/server"; import { TRPCError } from "@trpc/server";
import * as bcrypt from "bcrypt"; import * as bcrypt from "bcrypt";
import { and, asc, eq, gt } from "drizzle-orm"; import { and, asc, eq, gt } from "drizzle-orm";
@@ -60,14 +61,7 @@ const apiCreateApiKey = z.object({
}); });
export const userRouter = createTRPCRouter({ export const userRouter = createTRPCRouter({
all: withPermission("member", "read") all: withPermission("member", "read").query(async ({ ctx }) => {
.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({ return await db.query.member.findMany({
where: eq(member.organizationId, ctx.session.activeOrganizationId), where: eq(member.organizationId, ctx.session.activeOrganizationId),
with: { with: {
@@ -77,12 +71,6 @@ export const userRouter = createTRPCRouter({
}); });
}), }),
one: protectedProcedure 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( .input(
z.object({ z.object({
userId: z.string(), userId: z.string(),
@@ -127,14 +115,7 @@ export const userRouter = createTRPCRouter({
return memberResult; return memberResult;
}), }),
session: publicProcedure session: publicProcedure.query(async ({ ctx }) => {
.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) { if (!ctx.user || !ctx.session || !ctx.session.activeOrganizationId) {
return null; return null;
} }
@@ -147,14 +128,7 @@ export const userRouter = createTRPCRouter({
}, },
}; };
}), }),
get: protectedProcedure get: protectedProcedure.query(async ({ ctx }) => {
.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({ const memberResult = await db.query.member.findFirst({
where: and( where: and(
eq(member.userId, ctx.user.id), eq(member.userId, ctx.user.id),
@@ -171,24 +145,10 @@ export const userRouter = createTRPCRouter({
return memberResult; return memberResult;
}), }),
getPermissions: protectedProcedure getPermissions: protectedProcedure.query(async ({ ctx }) => {
.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); return resolvePermissions(ctx);
}), }),
haveRootAccess: protectedProcedure haveRootAccess: protectedProcedure.query(async ({ ctx }) => {
.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) { if (!IS_CLOUD) {
return false; return false;
} }
@@ -200,14 +160,7 @@ export const userRouter = createTRPCRouter({
} }
return false; return false;
}), }),
getBackups: adminProcedure getBackups: adminProcedure.query(async ({ ctx }) => {
.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({ const memberResult = await db.query.member.findFirst({
where: and( where: and(
eq(member.userId, ctx.user.id), eq(member.userId, ctx.user.id),
@@ -230,14 +183,8 @@ export const userRouter = createTRPCRouter({
return memberResult?.user; return memberResult?.user;
}), }),
getServerMetrics: withPermission("monitoring", "read") getServerMetrics: withPermission("monitoring", "read").query(
.meta({ async ({ ctx }) => {
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({ const memberResult = await db.query.member.findFirst({
where: and( where: and(
eq(member.userId, ctx.user.id), eq(member.userId, ctx.user.id),
@@ -252,12 +199,6 @@ export const userRouter = createTRPCRouter({
}, },
), ),
update: protectedProcedure 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) .input(apiUpdateUser)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
if (input.password || input.currentPassword) { if (input.password || input.currentPassword) {
@@ -308,24 +249,12 @@ export const userRouter = createTRPCRouter({
} }
}), }),
getUserByToken: publicProcedure 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) .input(apiFindOneToken)
.query(async ({ input }) => { .query(async ({ input }) => {
return await getUserByToken(input.token); return await getUserByToken(input.token);
}), }),
getMetricsToken: withPermission("monitoring", "read") getMetricsToken: withPermission("monitoring", "read").query(
.meta({ async ({ ctx }) => {
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 user = await findUserById(ctx.user.ownerId);
const settings = await getWebServerSettings(); const settings = await getWebServerSettings();
return { return {
@@ -336,12 +265,6 @@ export const userRouter = createTRPCRouter({
}, },
), ),
remove: protectedProcedure 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( .input(
z.object({ z.object({
userId: z.string(), userId: z.string(),
@@ -411,12 +334,6 @@ export const userRouter = createTRPCRouter({
return result; return result;
}), }),
assignPermissions: withPermission("member", "update") 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) .input(apiAssignPermissions)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
try { try {
@@ -467,14 +384,7 @@ export const userRouter = createTRPCRouter({
throw error; throw error;
} }
}), }),
getInvitations: protectedProcedure getInvitations: protectedProcedure.query(async ({ ctx }) => {
.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({ return await db.query.invitation.findMany({
where: and( where: and(
eq(invitation.email, ctx.user.email), eq(invitation.email, ctx.user.email),
@@ -488,12 +398,6 @@ export const userRouter = createTRPCRouter({
}), }),
getContainerMetrics: withPermission("monitoring", "read") 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( .input(
z.object({ z.object({
url: z.string(), url: z.string(),
@@ -552,24 +456,11 @@ export const userRouter = createTRPCRouter({
} }
}), }),
generateToken: protectedProcedure generateToken: protectedProcedure.mutation(async () => {
.meta({
openapi: {
summary: "Generate authentication token",
description: "Generate a new authentication token for the current user.",
},
})
.mutation(async () => {
return "token"; return "token";
}), }),
deleteApiKey: protectedProcedure 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( .input(
z.object({ z.object({
apiKeyId: z.string(), apiKeyId: z.string(),
@@ -609,12 +500,6 @@ export const userRouter = createTRPCRouter({
}), }),
createApiKey: protectedProcedure 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) .input(apiCreateApiKey)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
// Verify user is a member of the organization specified in metadata // Verify user is a member of the organization specified in metadata
@@ -645,12 +530,6 @@ export const userRouter = createTRPCRouter({
}), }),
checkUserOrganizations: protectedProcedure 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( .input(
z.object({ z.object({
userId: z.string(), userId: z.string(),
@@ -692,12 +571,6 @@ export const userRouter = createTRPCRouter({
return organizations.length; return organizations.length;
}), }),
createUserWithCredentials: withPermission("member", "create") 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( .input(
z.object({ z.object({
email: z.string().email(), email: z.string().email(),
@@ -729,12 +602,6 @@ export const userRouter = createTRPCRouter({
}); });
}), }),
sendInvitation: withPermission("member", "create") 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( .input(
z.object({ z.object({
invitationId: z.string().min(1), invitationId: z.string().min(1),
@@ -773,27 +640,26 @@ export const userRouter = createTRPCRouter({
); );
try { try {
const htmlContent = ` const toEmail = currentInvitation?.email || "";
\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> const orgName = organization?.name || "organization";
\t\t\t\t`; const subject = `You've been invited to join ${orgName} on Dokploy`;
const html = await renderInvitationEmail({
email: toEmail,
inviteLink,
organizationName: orgName,
});
if (email) { if (email) {
await sendEmailNotification( await sendEmailNotification(
{ { ...email, toAddresses: [toEmail] },
...email, subject,
toAddresses: [currentInvitation?.email || ""], html,
},
"Invitation to join organization",
htmlContent,
); );
} else if (resend) { } else if (resend) {
await sendResendNotification( await sendResendNotification(
{ { ...resend, toAddresses: [toEmail] },
...resend, subject,
toAddresses: [currentInvitation?.email || ""], html,
},
"Invitation to join organization",
htmlContent,
); );
} }
} catch (error) { } catch (error) {
@@ -810,14 +676,7 @@ export const userRouter = createTRPCRouter({
return inviteLink; return inviteLink;
}), }),
getBookmarkedTemplates: protectedProcedure getBookmarkedTemplates: protectedProcedure.query(async ({ ctx }) => {
.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({ const result = await db.query.user.findFirst({
where: eq(user.id, ctx.user.id), where: eq(user.id, ctx.user.id),
columns: { bookmarkedTemplates: true }, columns: { bookmarkedTemplates: true },
@@ -827,12 +686,6 @@ export const userRouter = createTRPCRouter({
}), }),
toggleTemplateBookmark: protectedProcedure 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( .input(
z.object({ z.object({
templateId: z.string().min(1), templateId: z.string().min(1),

View File

@@ -15,7 +15,9 @@ import {
updateVolumeBackupSchema, updateVolumeBackupSchema,
volumeBackups, volumeBackups,
} from "@dokploy/server/db/schema"; } from "@dokploy/server/db/schema";
import { findDestinationById } from "@dokploy/server/services/destination";
import { checkServicePermissionAndAccess } from "@dokploy/server/services/permission"; import { checkServicePermissionAndAccess } from "@dokploy/server/services/permission";
import { findServerById } from "@dokploy/server/services/server";
import { import {
execAsyncRemote, execAsyncRemote,
execAsyncStream, execAsyncStream,
@@ -30,12 +32,6 @@ import { createTRPCRouter, protectedProcedure, withPermission } from "../trpc";
export const volumeBackupsRouter = createTRPCRouter({ export const volumeBackupsRouter = createTRPCRouter({
list: protectedProcedure list: protectedProcedure
.meta({
openapi: {
summary: "List volume backups",
description: "Returns all volume backup configurations for a given service, including related service details.",
},
})
.input( .input(
z.object({ z.object({
id: z.string().min(1), id: z.string().min(1),
@@ -71,12 +67,6 @@ export const volumeBackupsRouter = createTRPCRouter({
}); });
}), }),
create: protectedProcedure 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) .input(createVolumeBackupSchema)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
const serviceId = const serviceId =
@@ -114,12 +104,6 @@ export const volumeBackupsRouter = createTRPCRouter({
return newVolumeBackup; return newVolumeBackup;
}), }),
one: protectedProcedure one: protectedProcedure
.meta({
openapi: {
summary: "Get a volume backup",
description: "Returns the details of a specific volume backup configuration by its ID.",
},
})
.input( .input(
z.object({ z.object({
volumeBackupId: z.string().min(1), volumeBackupId: z.string().min(1),
@@ -144,12 +128,6 @@ export const volumeBackupsRouter = createTRPCRouter({
return vb; return vb;
}), }),
delete: protectedProcedure delete: protectedProcedure
.meta({
openapi: {
summary: "Delete a volume backup",
description: "Permanently removes a volume backup configuration by its ID.",
},
})
.input( .input(
z.object({ z.object({
volumeBackupId: z.string().min(1), volumeBackupId: z.string().min(1),
@@ -180,12 +158,6 @@ export const volumeBackupsRouter = createTRPCRouter({
return result; return result;
}), }),
update: protectedProcedure 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) .input(updateVolumeBackupSchema)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
const existingVb = await findVolumeBackupById(input.volumeBackupId); const existingVb = await findVolumeBackupById(input.volumeBackupId);
@@ -246,12 +218,6 @@ export const volumeBackupsRouter = createTRPCRouter({
}), }),
runManually: protectedProcedure 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) })) .input(z.object({ volumeBackupId: z.string().min(1) }))
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
const vb = await findVolumeBackupById(input.volumeBackupId); const vb = await findVolumeBackupById(input.volumeBackupId);
@@ -301,7 +267,23 @@ export const volumeBackupsRouter = createTRPCRouter({
serverId: z.string().optional(), serverId: z.string().optional(),
}), }),
) )
.subscription(async ({ input }) => { .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.",
});
}
}
return observable<string>((emit) => { return observable<string>((emit) => {
const runRestore = async () => { const runRestore = async () => {
try { try {

View File

@@ -13,12 +13,7 @@ import { hasValidLicense } from "@dokploy/server/index";
import type { statements } from "@dokploy/server/lib/access-control"; import type { statements } from "@dokploy/server/lib/access-control";
import { validateRequest } from "@dokploy/server/lib/auth"; import { validateRequest } from "@dokploy/server/lib/auth";
import { checkPermission } from "@dokploy/server/services/permission"; import { checkPermission } from "@dokploy/server/services/permission";
import type { OpenApiMeta as _OpenApiMeta } from "@dokploy/trpc-openapi"; import type { 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 { initTRPC, TRPCError } from "@trpc/server";
import type { CreateNextContextOptions } from "@trpc/server/adapters/next"; import type { CreateNextContextOptions } from "@trpc/server/adapters/next";
import type { Session, User } from "better-auth"; import type { Session, User } from "better-auth";

View File

@@ -85,6 +85,11 @@ export const setupDockerContainerLogsWebSocketServer = (
if (serverId) { if (serverId) {
const server = await findServerById(serverId); const server = await findServerById(serverId);
if (server.organizationId !== session.activeOrganizationId) {
ws.close();
return;
}
if (!server.sshKeyId) return; if (!server.sshKeyId) return;
const client = new Client(); const client = new Client();
client client

View File

@@ -61,6 +61,12 @@ export const setupDockerContainerTerminalWebSocketServer = (
try { try {
if (serverId) { if (serverId) {
const server = await findServerById(serverId); const server = await findServerById(serverId);
if (server.organizationId !== session.activeOrganizationId) {
ws.close();
return;
}
if (!server.sshKeyId) if (!server.sshKeyId)
throw new Error("No SSH key available for this server"); throw new Error("No SSH key available for this server");

View File

@@ -57,6 +57,11 @@ export const setupDeploymentLogsWebSocketServer = (
if (serverId) { if (serverId) {
const server = await findServerById(serverId); const server = await findServerById(serverId);
if (server.organizationId !== session.activeOrganizationId) {
ws.close();
return;
}
if (!server.sshKeyId) { if (!server.sshKeyId) {
ws.close(); ws.close();
return; return;

View File

@@ -154,6 +154,11 @@ export const setupTerminalWebSocketServer = (
return; return;
} }
if (server.organizationId !== session.activeOrganizationId) {
ws.close();
return;
}
const { ipAddress: host, port, username, sshKey, sshKeyId } = server; const { ipAddress: host, port, username, sshKey, sshKeyId } = server;
if (!sshKeyId) { if (!sshKeyId) {

View File

@@ -104,7 +104,10 @@ process.on("uncaughtException", (err) => {
}); });
process.on("unhandledRejection", (reason, _promise) => { 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"); const port = Number.parseInt(process.env.PORT || "3000");

Some files were not shown because too many files have changed in this diff Show More