Compare commits

...

2 Commits
v0.16.1 ... wip

Author SHA1 Message Date
Mauricio Siu
823dbe608f refactor: migration 2024-10-24 21:25:45 -06:00
Mauricio Siu
4309939fd5 refactor: update 2024-10-24 21:07:36 -06:00
158 changed files with 12614 additions and 284 deletions

View File

@@ -11,7 +11,7 @@
"build-next": "next build",
"setup": "tsx -r dotenv/config setup.ts && sleep 5 && pnpm run migration:run",
"reset-password": "node -r dotenv/config dist/reset-password.mjs",
"dev": "tsx -r dotenv/config ./server/server.ts --project tsconfig.server.json ",
"dev": "TURBOPACK=1 tsx -r dotenv/config ./server/server.ts --project tsconfig.server.json ",
"studio": "drizzle-kit studio --config ./server/db/drizzle.config.ts",
"migration:generate": "drizzle-kit generate --config ./server/db/drizzle.config.ts",
"migration:run": "tsx -r dotenv/config migration.ts",
@@ -90,7 +90,7 @@
"lucia": "^3.0.1",
"lucide-react": "^0.312.0",
"nanoid": "3",
"next": "^14.1.3",
"next": "^15.0.1",
"next-themes": "^0.2.1",
"node-pty": "1.0.0",
"node-schedule": "2.1.1",

View File

@@ -1 +1 @@
export * from "@dokploy/server/dist/db/schema";
export * from "@dokploy/server/db/schema";

View File

@@ -23,11 +23,12 @@ import {
getPublicIpWithFallback,
setupTerminalWebSocketServer,
} from "./wss/terminal";
// Holman Oniel Guevara Ayala
// GET /dashboard/settings/server 200 in 6640ms
config({ path: ".env" });
const PORT = Number.parseInt(process.env.PORT || "3000", 10);
const dev = process.env.NODE_ENV !== "production";
const app = next({ dev });
const app = next({ dev, turbopack: true });
const handle = app.getRequestHandler();
void app.prepare().then(async () => {
try {

View File

@@ -3,15 +3,12 @@ import {
createDefaultServerTraefikConfig,
createDefaultTraefikConfig,
initializeTraefik,
} from "@dokploy/server/dist/setup/traefik-setup";
} from "@dokploy/server";
import { setupDirectories } from "@dokploy/server/dist/setup/config-paths";
import { initializePostgres } from "@dokploy/server/dist/setup/postgres-setup";
import { initializeRedis } from "@dokploy/server/dist/setup/redis-setup";
import {
initializeNetwork,
initializeSwarm,
} from "@dokploy/server/dist/setup/setup";
import { setupDirectories } from "@dokploy/server";
import { initializePostgres } from "@dokploy/server";
import { initializeRedis } from "@dokploy/server";
import { initializeNetwork, initializeSwarm } from "@dokploy/server";
(async () => {
try {
setupDirectories();

View File

@@ -26,7 +26,8 @@
/* Path Aliases */
"baseUrl": ".",
"paths": {
"@/*": ["./*"]
"@/*": ["./*"],
"@dokploy/server/*": ["../../packages/server/src/*"]
}
},

5
apps/mig/.gitignore vendored Normal file
View File

@@ -0,0 +1,5 @@
node_modules
/.cache
/build
.env

40
apps/mig/README.md Normal file
View File

@@ -0,0 +1,40 @@
# Welcome to Remix!
- 📖 [Remix docs](https://remix.run/docs)
## Development
Run the dev server:
```shellscript
npm run dev
```
## Deployment
First, build your app for production:
```sh
npm run build
```
Then run the app in production mode:
```sh
npm start
```
Now you'll need to pick a host to deploy it to.
### DIY
If you're familiar with deploying Node applications, the built-in Remix app server is production-ready.
Make sure to deploy the output of `npm run build`
- `build/server`
- `build/client`
## Styling
This template comes with [Tailwind CSS](https://tailwindcss.com/) already configured for a simple default starting experience. You can use whatever css framework you prefer. See the [Vite docs on css](https://vitejs.dev/guide/features.html#css) for more information.

View File

@@ -0,0 +1,18 @@
/**
* By default, Remix will handle hydrating your app on the client for you.
* You are free to delete this file if you'd like to, but if you ever want it revealed again, you can run `npx remix reveal` ✨
* For more information, see https://remix.run/file-conventions/entry.client
*/
import { RemixBrowser } from "@remix-run/react";
import { StrictMode, startTransition } from "react";
import { hydrateRoot } from "react-dom/client";
startTransition(() => {
hydrateRoot(
document,
<StrictMode>
<RemixBrowser />
</StrictMode>,
);
});

View File

@@ -0,0 +1,141 @@
/**
* By default, Remix will handle generating the HTTP Response for you.
* You are free to delete this file if you'd like to, but if you ever want it revealed again, you can run `npx remix reveal` ✨
* For more information, see https://remix.run/file-conventions/entry.server
*/
import { PassThrough } from "node:stream";
import type { AppLoadContext, EntryContext } from "@remix-run/node";
import { createReadableStreamFromReadable } from "@remix-run/node";
import { RemixServer } from "@remix-run/react";
import { isbot } from "isbot";
import { renderToPipeableStream } from "react-dom/server";
const ABORT_DELAY = 5_000;
export default function handleRequest(
request: Request,
responseStatusCode: number,
responseHeaders: Headers,
remixContext: EntryContext,
// This is ignored so we can keep it in the template for visibility. Feel
// free to delete this parameter in your app if you're not using it!
// eslint-disable-next-line @typescript-eslint/no-unused-vars
loadContext: AppLoadContext,
) {
return isbot(request.headers.get("user-agent") || "")
? handleBotRequest(
request,
responseStatusCode,
responseHeaders,
remixContext,
)
: handleBrowserRequest(
request,
responseStatusCode,
responseHeaders,
remixContext,
);
}
function handleBotRequest(
request: Request,
responseStatusCode: number,
responseHeaders: Headers,
remixContext: EntryContext,
) {
return new Promise((resolve, reject) => {
let shellRendered = false;
const { pipe, abort } = renderToPipeableStream(
<RemixServer
context={remixContext}
url={request.url}
abortDelay={ABORT_DELAY}
/>,
{
onAllReady() {
shellRendered = true;
const body = new PassThrough();
const stream = createReadableStreamFromReadable(body);
responseHeaders.set("Content-Type", "text/html");
resolve(
new Response(stream, {
headers: responseHeaders,
status: responseStatusCode,
}),
);
pipe(body);
},
onShellError(error: unknown) {
reject(error);
},
onError(error: unknown) {
// responseStatusCode = 500;
// Log streaming rendering errors from inside the shell. Don't log
// errors encountered during initial shell rendering since they'll
// reject and get logged in handleDocumentRequest.
if (shellRendered) {
console.error(error);
}
},
},
);
setTimeout(abort, ABORT_DELAY);
});
}
function handleBrowserRequest(
request: Request,
responseStatusCode: number,
responseHeaders: Headers,
remixContext: EntryContext,
) {
return new Promise((resolve, reject) => {
let shellRendered = false;
const { pipe, abort } = renderToPipeableStream(
<RemixServer
context={remixContext}
url={request.url}
abortDelay={ABORT_DELAY}
/>,
{
onShellReady() {
shellRendered = true;
const body = new PassThrough();
const stream = createReadableStreamFromReadable(body);
responseHeaders.set("Content-Type", "text/html");
resolve(
new Response(stream, {
headers: responseHeaders,
status: responseStatusCode,
}),
);
pipe(body);
},
onShellError(error: unknown) {
reject(error);
},
onError(error: unknown) {
// responseStatusCode = 500;
// Log streaming rendering errors from inside the shell. Don't log
// errors encountered during initial shell rendering since they'll
// reject and get logged in handleDocumentRequest.
if (shellRendered) {
console.error(error);
}
},
},
);
setTimeout(abort, ABORT_DELAY);
});
}

45
apps/mig/app/root.tsx Normal file
View File

@@ -0,0 +1,45 @@
import type { LinksFunction } from "@remix-run/node";
import {
Links,
Meta,
Outlet,
Scripts,
ScrollRestoration,
} from "@remix-run/react";
import "./tailwind.css";
export const links: LinksFunction = () => [
{ rel: "preconnect", href: "https://fonts.googleapis.com" },
{
rel: "preconnect",
href: "https://fonts.gstatic.com",
crossOrigin: "anonymous",
},
{
rel: "stylesheet",
href: "https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap",
},
];
export function Layout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<head>
<meta charSet="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<Meta />
<Links />
</head>
<body>
{children}
<ScrollRestoration />
<Scripts />
</body>
</html>
);
}
export default function App() {
return <Outlet />;
}

View File

@@ -0,0 +1,138 @@
import type { MetaFunction } from "@remix-run/node";
export const meta: MetaFunction = () => {
return [
{ title: "New Remix App" },
{ name: "description", content: "Welcome to Remix!" },
];
};
export default function Index() {
return (
<div className="flex h-screen items-center justify-center">
<div className="flex flex-col items-center gap-16">
<header className="flex flex-col items-center gap-9">
<h1 className="leading text-2xl font-bold text-gray-800 dark:text-gray-100">
Welcome to <span className="sr-only">Dokploy</span>
</h1>
<div className="h-[144px] w-[434px]">
<img
src="/logo-light.png"
alt="Remix"
className="block w-full dark:hidden"
/>
<img
src="/logo-dark.png"
alt="Remix"
className="hidden w-full dark:block"
/>
</div>
</header>
<nav className="flex flex-col items-center justify-center gap-4 rounded-3xl border border-gray-200 p-6 dark:border-gray-700">
<p className="leading-6 text-gray-700 dark:text-gray-200">
What&apos;s
</p>
<ul>
{resources.map(({ href, text, icon }) => (
<li key={href}>
<a
className="group flex items-center gap-3 self-stretch p-3 leading-normal text-blue-700 hover:underline dark:text-blue-500"
href={href}
target="_blank"
rel="noreferrer"
>
{icon}
{text}
</a>
</li>
))}
</ul>
</nav>
</div>
</div>
);
}
const resources = [
{
href: "https://remix.run/start/quickstart",
text: "Quick Start (5 min)",
icon: (
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="20"
viewBox="0 0 20 20"
fill="none"
className="stroke-gray-600 group-hover:stroke-current dark:stroke-gray-300"
>
<path
d="M8.51851 12.0741L7.92592 18L15.6296 9.7037L11.4815 7.33333L12.0741 2L4.37036 10.2963L8.51851 12.0741Z"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
),
},
{
href: "https://remix.run/start/tutorial",
text: "Tutorial (30 min)",
icon: (
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="20"
viewBox="0 0 20 20"
fill="none"
className="stroke-gray-600 group-hover:stroke-current dark:stroke-gray-300"
>
<path
d="M4.561 12.749L3.15503 14.1549M3.00811 8.99944H1.01978M3.15503 3.84489L4.561 5.2508M8.3107 1.70923L8.3107 3.69749M13.4655 3.84489L12.0595 5.2508M18.1868 17.0974L16.635 18.6491C16.4636 18.8205 16.1858 18.8205 16.0144 18.6491L13.568 16.2028C13.383 16.0178 13.0784 16.0347 12.915 16.239L11.2697 18.2956C11.047 18.5739 10.6029 18.4847 10.505 18.142L7.85215 8.85711C7.75756 8.52603 8.06365 8.21994 8.39472 8.31453L17.6796 10.9673C18.0223 11.0653 18.1115 11.5094 17.8332 11.7321L15.7766 13.3773C15.5723 13.5408 15.5554 13.8454 15.7404 14.0304L18.1868 16.4767C18.3582 16.6481 18.3582 16.926 18.1868 17.0974Z"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
),
},
{
href: "https://remix.run/docs",
text: "Remix Docs",
icon: (
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="20"
viewBox="0 0 20 20"
fill="none"
className="stroke-gray-600 group-hover:stroke-current dark:stroke-gray-300"
>
<path
d="M9.99981 10.0751V9.99992M17.4688 17.4688C15.889 19.0485 11.2645 16.9853 7.13958 12.8604C3.01467 8.73546 0.951405 4.11091 2.53116 2.53116C4.11091 0.951405 8.73546 3.01467 12.8604 7.13958C16.9853 11.2645 19.0485 15.889 17.4688 17.4688ZM2.53132 17.4688C0.951566 15.8891 3.01483 11.2645 7.13974 7.13963C11.2647 3.01471 15.8892 0.951453 17.469 2.53121C19.0487 4.11096 16.9854 8.73551 12.8605 12.8604C8.73562 16.9853 4.11107 19.0486 2.53132 17.4688Z"
strokeWidth="1.5"
strokeLinecap="round"
/>
</svg>
),
},
{
href: "https://rmx.as/discord",
text: "Join Discord",
icon: (
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="20"
viewBox="0 0 24 20"
fill="none"
className="stroke-gray-600 group-hover:stroke-current dark:stroke-gray-300"
>
<path
d="M15.0686 1.25995L14.5477 1.17423L14.2913 1.63578C14.1754 1.84439 14.0545 2.08275 13.9422 2.31963C12.6461 2.16488 11.3406 2.16505 10.0445 2.32014C9.92822 2.08178 9.80478 1.84975 9.67412 1.62413L9.41449 1.17584L8.90333 1.25995C7.33547 1.51794 5.80717 1.99419 4.37748 2.66939L4.19 2.75793L4.07461 2.93019C1.23864 7.16437 0.46302 11.3053 0.838165 15.3924L0.868838 15.7266L1.13844 15.9264C2.81818 17.1714 4.68053 18.1233 6.68582 18.719L7.18892 18.8684L7.50166 18.4469C7.96179 17.8268 8.36504 17.1824 8.709 16.4944L8.71099 16.4904C10.8645 17.0471 13.128 17.0485 15.2821 16.4947C15.6261 17.1826 16.0293 17.8269 16.4892 18.4469L16.805 18.8725L17.3116 18.717C19.3056 18.105 21.1876 17.1751 22.8559 15.9238L23.1224 15.724L23.1528 15.3923C23.5873 10.6524 22.3579 6.53306 19.8947 2.90714L19.7759 2.73227L19.5833 2.64518C18.1437 1.99439 16.6386 1.51826 15.0686 1.25995ZM16.6074 10.7755L16.6074 10.7756C16.5934 11.6409 16.0212 12.1444 15.4783 12.1444C14.9297 12.1444 14.3493 11.6173 14.3493 10.7877C14.3493 9.94885 14.9378 9.41192 15.4783 9.41192C16.0471 9.41192 16.6209 9.93851 16.6074 10.7755ZM8.49373 12.1444C7.94513 12.1444 7.36471 11.6173 7.36471 10.7877C7.36471 9.94885 7.95323 9.41192 8.49373 9.41192C9.06038 9.41192 9.63892 9.93712 9.6417 10.7815C9.62517 11.6239 9.05462 12.1444 8.49373 12.1444Z"
strokeWidth="1.5"
/>
</svg>
),
},
];

12
apps/mig/app/tailwind.css Normal file
View File

@@ -0,0 +1,12 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
html,
body {
@apply bg-white dark:bg-gray-950;
@media (prefers-color-scheme: dark) {
color-scheme: dark;
}
}

20
apps/mig/migration.ts Normal file
View File

@@ -0,0 +1,20 @@
import { drizzle } from "drizzle-orm/postgres-js";
import { migrate } from "drizzle-orm/postgres-js/migrator";
import postgres from "postgres";
const connectionString = process.env.DATABASE_URL || "";
const sql = postgres(connectionString, { max: 1 });
const db = drizzle(sql);
await migrate(db, { migrationsFolder: "drizzle" })
.then(() => {
console.log("Migration complete");
sql.end();
})
.catch((error) => {
console.log("Migration failed", error);
})
.finally(() => {
sql.end();
});

90
apps/mig/package.json Normal file
View File

@@ -0,0 +1,90 @@
{
"name": "my-remix-app",
"private": true,
"sideEffects": false,
"type": "module",
"scripts": {
"build": "remix vite:build",
"dev2": "tsx --watch -r dotenv/config server/server.ts",
"start3": "cross-env NODE_ENV=production node -r dotenv/config ./build/server/index.js",
"start2": "cross-env NODE_ENV=production node ./server.js",
"dev": "remix vite:dev",
"start": "remix-serve ./build/server/index.js",
"typecheck": "tsc"
},
"dependencies": {
"@dokploy/server": "workspace:*",
"@remix-run/express": "2.13.1",
"@remix-run/node": "^2.13.1",
"@remix-run/react": "^2.13.1",
"cross-env": "7.0.3",
"isbot": "^4.1.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"bullmq": "5.4.2",
"rotating-file-stream": "3.2.3",
"@faker-js/faker": "^8.4.1",
"@lucia-auth/adapter-drizzle": "1.0.7",
"@octokit/auth-app": "^6.0.4",
"@react-email/components": "^0.0.21",
"@trpc/server": "^10.43.6",
"adm-zip": "^0.5.14",
"bcrypt": "5.1.1",
"bl": "6.0.11",
"boxen": "^7.1.1",
"date-fns": "3.6.0",
"dockerode": "4.0.2",
"dotenv": "16.4.5",
"drizzle-orm": "^0.30.8",
"drizzle-zod": "0.5.1",
"hi-base32": "^0.5.1",
"js-yaml": "4.1.0",
"lodash": "4.17.21",
"lucia": "^3.0.1",
"nanoid": "3",
"node-os-utils": "1.3.7",
"node-pty": "1.0.0",
"node-schedule": "2.1.1",
"nodemailer": "6.9.14",
"octokit": "3.1.2",
"otpauth": "^9.2.3",
"postgres": "3.4.4",
"public-ip": "6.0.2",
"qrcode": "^1.5.3",
"slugify": "^1.6.6",
"ws": "8.16.0",
"zod": "^3.23.4",
"ssh2": "1.15.0"
},
"devDependencies": {
"esbuild": "0.24.0",
"@remix-run/dev": "^2.13.1",
"@types/react": "^18.2.20",
"@types/react-dom": "^18.2.7",
"autoprefixer": "^10.4.19",
"postcss": "^8.4.38",
"tailwindcss": "^3.4.4",
"typescript": "^5.1.6",
"vite": "^5.1.0",
"vite-tsconfig-paths": "^4.2.1",
"tsx": "^4.7.1",
"tsc-alias": "1.8.10",
"@types/adm-zip": "^0.5.5",
"@types/bcrypt": "5.0.2",
"@types/dockerode": "3.3.23",
"@types/js-yaml": "4.0.9",
"@types/lodash": "4.17.4",
"@types/node": "^18.17.0",
"@types/node-os-utils": "1.3.4",
"@types/node-schedule": "2.1.6",
"@types/nodemailer": "^6.4.15",
"@types/qrcode": "^1.5.5",
"@types/ws": "8.5.10",
"drizzle-kit": "^0.21.1",
"@types/ssh2": "1.15.1"
},
"engines": {
"node": ">=18.18.0"
}
}

View File

@@ -0,0 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};

BIN
apps/mig/public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

View File

@@ -0,0 +1,77 @@
import { authRouter } from "@/server/api/routers/auth";
import { createTRPCRouter } from "../api/trpc";
import { adminRouter } from "./routers/admin";
import { applicationRouter } from "./routers/application";
import { backupRouter } from "./routers/backup";
import { bitbucketRouter } from "./routers/bitbucket";
import { certificateRouter } from "./routers/certificate";
import { clusterRouter } from "./routers/cluster";
import { composeRouter } from "./routers/compose";
import { deploymentRouter } from "./routers/deployment";
import { destinationRouter } from "./routers/destination";
import { dockerRouter } from "./routers/docker";
import { domainRouter } from "./routers/domain";
import { gitProviderRouter } from "./routers/git-provider";
import { githubRouter } from "./routers/github";
import { gitlabRouter } from "./routers/gitlab";
import { mariadbRouter } from "./routers/mariadb";
import { mongoRouter } from "./routers/mongo";
import { mountRouter } from "./routers/mount";
import { mysqlRouter } from "./routers/mysql";
import { notificationRouter } from "./routers/notification";
import { portRouter } from "./routers/port";
import { postgresRouter } from "./routers/postgres";
import { projectRouter } from "./routers/project";
import { redirectsRouter } from "./routers/redirects";
import { redisRouter } from "./routers/redis";
import { registryRouter } from "./routers/registry";
import { securityRouter } from "./routers/security";
import { serverRouter } from "./routers/server";
import { settingsRouter } from "./routers/settings";
import { sshRouter } from "./routers/ssh-key";
import { stripeRouter } from "./routers/stripe";
import { userRouter } from "./routers/user";
/**
* This is the primary router for your server.
*
* All routers added in /api/routers should be manually added here.
*/
export const appRouter = createTRPCRouter({
admin: adminRouter,
docker: dockerRouter,
auth: authRouter,
project: projectRouter,
application: applicationRouter,
mysql: mysqlRouter,
postgres: postgresRouter,
redis: redisRouter,
mongo: mongoRouter,
mariadb: mariadbRouter,
compose: composeRouter,
user: userRouter,
domain: domainRouter,
destination: destinationRouter,
backup: backupRouter,
deployment: deploymentRouter,
mounts: mountRouter,
certificates: certificateRouter,
settings: settingsRouter,
security: securityRouter,
redirects: redirectsRouter,
port: portRouter,
registry: registryRouter,
cluster: clusterRouter,
notification: notificationRouter,
sshKey: sshRouter,
gitProvider: gitProviderRouter,
bitbucket: bitbucketRouter,
gitlab: gitlabRouter,
github: githubRouter,
server: serverRouter,
stripe: stripeRouter,
});
// export type definition of API
export type AppRouter = typeof appRouter;

View File

@@ -0,0 +1,92 @@
import { db } from "@/server/db";
import {
apiAssignPermissions,
apiCreateUserInvitation,
apiFindOneToken,
apiRemoveUser,
users,
} from "@/server/db/schema";
import {
createInvitation,
findAdminById,
findUserByAuthId,
findUserById,
getUserByToken,
removeUserByAuthId,
} from "@dokploy/server";
import { TRPCError } from "@trpc/server";
import { eq } from "drizzle-orm";
import { adminProcedure, createTRPCRouter, publicProcedure } from "../trpc";
export const adminRouter = createTRPCRouter({
one: adminProcedure.query(async ({ ctx }) => {
const { sshPrivateKey, ...rest } = await findAdminById(ctx.user.adminId);
return {
haveSSH: !!sshPrivateKey,
...rest,
};
}),
createUserInvitation: adminProcedure
.input(apiCreateUserInvitation)
.mutation(async ({ input, ctx }) => {
try {
await createInvitation(input, ctx.user.adminId);
} catch (error) {
throw new TRPCError({
code: "BAD_REQUEST",
message:
"Error to create this user\ncheck if the email is not registered",
cause: error,
});
}
}),
removeUser: adminProcedure
.input(apiRemoveUser)
.mutation(async ({ input, ctx }) => {
try {
const user = await findUserByAuthId(input.authId);
if (user.adminId !== ctx.user.adminId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not allowed to delete this user",
});
}
return await removeUserByAuthId(input.authId);
} catch (error) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Error to delete this user",
cause: error,
});
}
}),
getUserByToken: publicProcedure
.input(apiFindOneToken)
.query(async ({ input }) => {
return await getUserByToken(input.token);
}),
assignPermissions: adminProcedure
.input(apiAssignPermissions)
.mutation(async ({ input, ctx }) => {
try {
const user = await findUserById(input.userId);
if (user.adminId !== ctx.user.adminId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not allowed to assign permissions",
});
}
await db
.update(users)
.set({
...input,
})
.where(eq(users.userId, input.userId));
} catch (error) {
throw error;
}
}),
});

View File

@@ -0,0 +1,625 @@
import {
createTRPCRouter,
protectedProcedure,
uploadProcedure,
} from "@/server/api/trpc";
import { db } from "@/server/db";
import {
apiCreateApplication,
apiFindMonitoringStats,
apiFindOneApplication,
apiReloadApplication,
apiSaveBitbucketProvider,
apiSaveBuildType,
apiSaveDockerProvider,
apiSaveEnvironmentVariables,
apiSaveGitProvider,
apiSaveGithubProvider,
apiSaveGitlabProvider,
apiUpdateApplication,
applications,
} from "@/server/db/schema";
import {
type DeploymentJob,
cleanQueuesByApplication,
} from "@/server/queues/deployments-queue";
import { myQueue } from "@/server/queues/queueSetup";
import { deploy } from "@/server/utils/deploy";
import { uploadFileSchema } from "@/utils/schema";
import {
IS_CLOUD,
addNewService,
checkServiceAccess,
createApplication,
deleteAllMiddlewares,
findApplicationById,
findProjectById,
getApplicationStats,
readConfig,
readRemoteConfig,
removeDeployments,
removeDirectoryCode,
removeMonitoringDirectory,
removeService,
removeTraefikConfig,
startService,
startServiceRemote,
stopService,
stopServiceRemote,
unzipDrop,
updateApplication,
updateApplicationStatus,
writeConfig,
writeConfigRemote,
// uploadFileSchema
} from "@dokploy/server";
import { TRPCError } from "@trpc/server";
import { eq } from "drizzle-orm";
import { nanoid } from "nanoid";
import { z } from "zod";
export const applicationRouter = createTRPCRouter({
create: protectedProcedure
.input(apiCreateApplication)
.mutation(async ({ input, ctx }) => {
try {
if (ctx.user.rol === "user") {
await checkServiceAccess(ctx.user.authId, input.projectId, "create");
}
if (IS_CLOUD && !input.serverId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You need to use a server to create an application",
});
}
const project = await findProjectById(input.projectId);
if (project.adminId !== ctx.user.adminId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to access this project",
});
}
const newApplication = await createApplication(input);
if (ctx.user.rol === "user") {
await addNewService(ctx.user.authId, newApplication.applicationId);
}
return newApplication;
} catch (error: unknown) {
if (error instanceof TRPCError) {
throw error;
}
throw new TRPCError({
code: "BAD_REQUEST",
message: "Error to create the application",
cause: error,
});
}
}),
one: protectedProcedure
.input(apiFindOneApplication)
.query(async ({ input, ctx }) => {
if (ctx.user.rol === "user") {
await checkServiceAccess(
ctx.user.authId,
input.applicationId,
"access",
);
}
const application = await findApplicationById(input.applicationId);
if (application.project.adminId !== ctx.user.adminId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to access this application",
});
}
return application;
}),
reload: protectedProcedure
.input(apiReloadApplication)
.mutation(async ({ input, ctx }) => {
const application = await findApplicationById(input.applicationId);
if (application.project.adminId !== ctx.user.adminId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to reload this application",
});
}
if (application.serverId) {
await stopServiceRemote(application.serverId, input.appName);
} else {
await stopService(input.appName);
}
await updateApplicationStatus(input.applicationId, "idle");
if (application.serverId) {
await startServiceRemote(application.serverId, input.appName);
} else {
await startService(input.appName);
}
await updateApplicationStatus(input.applicationId, "done");
return true;
}),
delete: protectedProcedure
.input(apiFindOneApplication)
.mutation(async ({ input, ctx }) => {
if (ctx.user.rol === "user") {
await checkServiceAccess(
ctx.user.authId,
input.applicationId,
"delete",
);
}
const application = await findApplicationById(input.applicationId);
if (application.project.adminId !== ctx.user.adminId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to delete this application",
});
}
const result = await db
.delete(applications)
.where(eq(applications.applicationId, input.applicationId))
.returning();
const cleanupOperations = [
async () => await deleteAllMiddlewares(application),
async () => await removeDeployments(application),
async () =>
await removeDirectoryCode(application.appName, application.serverId),
async () =>
await removeMonitoringDirectory(
application.appName,
application.serverId,
),
async () =>
await removeTraefikConfig(application.appName, application.serverId),
async () =>
await removeService(application?.appName, application.serverId),
];
for (const operation of cleanupOperations) {
try {
await operation();
} catch (error) {}
}
return result[0];
}),
stop: protectedProcedure
.input(apiFindOneApplication)
.mutation(async ({ input, ctx }) => {
const service = await findApplicationById(input.applicationId);
if (service.project.adminId !== ctx.user.adminId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to stop this application",
});
}
if (service.serverId) {
await stopServiceRemote(service.serverId, service.appName);
} else {
await stopService(service.appName);
}
await updateApplicationStatus(input.applicationId, "idle");
return service;
}),
start: protectedProcedure
.input(apiFindOneApplication)
.mutation(async ({ input, ctx }) => {
const service = await findApplicationById(input.applicationId);
if (service.project.adminId !== ctx.user.adminId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to start this application",
});
}
if (service.serverId) {
await startServiceRemote(service.serverId, service.appName);
} else {
await startService(service.appName);
}
await updateApplicationStatus(input.applicationId, "done");
return service;
}),
redeploy: protectedProcedure
.input(apiFindOneApplication)
.mutation(async ({ input, ctx }) => {
const application = await findApplicationById(input.applicationId);
if (application.project.adminId !== ctx.user.adminId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to redeploy this application",
});
}
const jobData: DeploymentJob = {
applicationId: input.applicationId,
titleLog: "Rebuild deployment",
descriptionLog: "",
type: "redeploy",
applicationType: "application",
server: !!application.serverId,
};
if (IS_CLOUD && application.serverId) {
jobData.serverId = application.serverId;
await deploy(jobData);
return true;
}
await myQueue.add(
"deployments",
{ ...jobData },
{
removeOnComplete: true,
removeOnFail: true,
},
);
}),
saveEnvironment: protectedProcedure
.input(apiSaveEnvironmentVariables)
.mutation(async ({ input, ctx }) => {
const application = await findApplicationById(input.applicationId);
if (application.project.adminId !== ctx.user.adminId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to save this environment",
});
}
await updateApplication(input.applicationId, {
env: input.env,
buildArgs: input.buildArgs,
});
return true;
}),
saveBuildType: protectedProcedure
.input(apiSaveBuildType)
.mutation(async ({ input, ctx }) => {
const application = await findApplicationById(input.applicationId);
if (application.project.adminId !== ctx.user.adminId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to save this build type",
});
}
await updateApplication(input.applicationId, {
buildType: input.buildType,
dockerfile: input.dockerfile,
publishDirectory: input.publishDirectory,
dockerContextPath: input.dockerContextPath,
dockerBuildStage: input.dockerBuildStage,
});
return true;
}),
saveGithubProvider: protectedProcedure
.input(apiSaveGithubProvider)
.mutation(async ({ input, ctx }) => {
const application = await findApplicationById(input.applicationId);
if (application.project.adminId !== ctx.user.adminId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to save this github provider",
});
}
await updateApplication(input.applicationId, {
repository: input.repository,
branch: input.branch,
sourceType: "github",
owner: input.owner,
buildPath: input.buildPath,
applicationStatus: "idle",
githubId: input.githubId,
});
return true;
}),
saveGitlabProvider: protectedProcedure
.input(apiSaveGitlabProvider)
.mutation(async ({ input, ctx }) => {
const application = await findApplicationById(input.applicationId);
if (application.project.adminId !== ctx.user.adminId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to save this gitlab provider",
});
}
await updateApplication(input.applicationId, {
gitlabRepository: input.gitlabRepository,
gitlabOwner: input.gitlabOwner,
gitlabBranch: input.gitlabBranch,
gitlabBuildPath: input.gitlabBuildPath,
sourceType: "gitlab",
applicationStatus: "idle",
gitlabId: input.gitlabId,
gitlabProjectId: input.gitlabProjectId,
gitlabPathNamespace: input.gitlabPathNamespace,
});
return true;
}),
saveBitbucketProvider: protectedProcedure
.input(apiSaveBitbucketProvider)
.mutation(async ({ input, ctx }) => {
const application = await findApplicationById(input.applicationId);
if (application.project.adminId !== ctx.user.adminId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to save this bitbucket provider",
});
}
await updateApplication(input.applicationId, {
bitbucketRepository: input.bitbucketRepository,
bitbucketOwner: input.bitbucketOwner,
bitbucketBranch: input.bitbucketBranch,
bitbucketBuildPath: input.bitbucketBuildPath,
sourceType: "bitbucket",
applicationStatus: "idle",
bitbucketId: input.bitbucketId,
});
return true;
}),
saveDockerProvider: protectedProcedure
.input(apiSaveDockerProvider)
.mutation(async ({ input, ctx }) => {
const application = await findApplicationById(input.applicationId);
if (application.project.adminId !== ctx.user.adminId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to save this docker provider",
});
}
await updateApplication(input.applicationId, {
dockerImage: input.dockerImage,
username: input.username,
password: input.password,
sourceType: "docker",
applicationStatus: "idle",
});
return true;
}),
saveGitProdiver: protectedProcedure
.input(apiSaveGitProvider)
.mutation(async ({ input, ctx }) => {
const application = await findApplicationById(input.applicationId);
if (application.project.adminId !== ctx.user.adminId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to save this git provider",
});
}
await updateApplication(input.applicationId, {
customGitBranch: input.customGitBranch,
customGitBuildPath: input.customGitBuildPath,
customGitUrl: input.customGitUrl,
customGitSSHKeyId: input.customGitSSHKeyId,
sourceType: "git",
applicationStatus: "idle",
});
return true;
}),
markRunning: protectedProcedure
.input(apiFindOneApplication)
.mutation(async ({ input, ctx }) => {
const application = await findApplicationById(input.applicationId);
if (application.project.adminId !== ctx.user.adminId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to mark this application as running",
});
}
await updateApplicationStatus(input.applicationId, "running");
}),
update: protectedProcedure
.input(apiUpdateApplication)
.mutation(async ({ input, ctx }) => {
const application = await findApplicationById(input.applicationId);
if (application.project.adminId !== ctx.user.adminId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to update this application",
});
}
const { applicationId, ...rest } = input;
const updateApp = await updateApplication(applicationId, {
...rest,
});
if (!updateApp) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Update: Error to update application",
});
}
return true;
}),
refreshToken: protectedProcedure
.input(apiFindOneApplication)
.mutation(async ({ input, ctx }) => {
const application = await findApplicationById(input.applicationId);
if (application.project.adminId !== ctx.user.adminId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to refresh this application",
});
}
await updateApplication(input.applicationId, {
refreshToken: nanoid(),
});
return true;
}),
deploy: protectedProcedure
.input(apiFindOneApplication)
.mutation(async ({ input, ctx }) => {
const application = await findApplicationById(input.applicationId);
if (application.project.adminId !== ctx.user.adminId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to deploy this application",
});
}
const jobData: DeploymentJob = {
applicationId: input.applicationId,
titleLog: "Manual deployment",
descriptionLog: "",
type: "deploy",
applicationType: "application",
server: !!application.serverId,
};
if (IS_CLOUD && application.serverId) {
jobData.serverId = application.serverId;
await deploy(jobData);
return true;
}
await myQueue.add(
"deployments",
{ ...jobData },
{
removeOnComplete: true,
removeOnFail: true,
},
);
}),
cleanQueues: protectedProcedure
.input(apiFindOneApplication)
.mutation(async ({ input, ctx }) => {
const application = await findApplicationById(input.applicationId);
if (application.project.adminId !== ctx.user.adminId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to clean this application",
});
}
await cleanQueuesByApplication(input.applicationId);
}),
readTraefikConfig: protectedProcedure
.input(apiFindOneApplication)
.query(async ({ input, ctx }) => {
const application = await findApplicationById(input.applicationId);
if (application.project.adminId !== ctx.user.adminId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to read this application",
});
}
let traefikConfig = null;
if (application.serverId) {
traefikConfig = await readRemoteConfig(
application.serverId,
application.appName,
);
} else {
traefikConfig = readConfig(application.appName);
}
return traefikConfig;
}),
dropDeployment: protectedProcedure
.meta({
openapi: {
path: "/drop-deployment",
method: "POST",
override: true,
enabled: false,
},
})
.use(uploadProcedure)
.input(uploadFileSchema)
.mutation(async ({ input, ctx }) => {
const zipFile = input.zip;
const app = await findApplicationById(input.applicationId as string);
if (app.project.adminId !== ctx.user.adminId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to deploy this application",
});
}
updateApplication(input.applicationId as string, {
sourceType: "drop",
dropBuildPath: input.dropBuildPath,
});
await unzipDrop(zipFile, app);
const jobData: DeploymentJob = {
applicationId: app.applicationId,
titleLog: "Manual deployment",
descriptionLog: "",
type: "deploy",
applicationType: "application",
server: !!app.serverId,
};
if (IS_CLOUD && app.serverId) {
jobData.serverId = app.serverId;
await deploy(jobData);
return true;
}
await myQueue.add(
"deployments",
{ ...jobData },
{
removeOnComplete: true,
removeOnFail: true,
},
);
return true;
}),
updateTraefikConfig: protectedProcedure
.input(z.object({ applicationId: z.string(), traefikConfig: z.string() }))
.mutation(async ({ input, ctx }) => {
const application = await findApplicationById(input.applicationId);
if (application.project.adminId !== ctx.user.adminId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to update this application",
});
}
if (application.serverId) {
await writeConfigRemote(
application.serverId,
application.appName,
input.traefikConfig,
);
} else {
writeConfig(application.appName, input.traefikConfig);
}
return true;
}),
readAppMonitoring: protectedProcedure
.input(apiFindMonitoringStats)
.query(async ({ input, ctx }) => {
if (IS_CLOUD) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "Functionality not available in cloud version",
});
}
const stats = await getApplicationStats(input.appName);
return stats;
}),
});

View File

@@ -0,0 +1,339 @@
import {
apiCreateAdmin,
apiCreateUser,
apiFindOneAuth,
apiLogin,
apiUpdateAuth,
apiUpdateAuthByAdmin,
apiVerify2FA,
apiVerifyLogin2FA,
auth,
} from "@/server/db/schema";
import {
IS_CLOUD,
createAdmin,
createUser,
findAuthByEmail,
findAuthById,
generate2FASecret,
getUserByToken,
lucia,
luciaToken,
sendEmailNotification,
updateAuthById,
validateRequest,
verify2FA,
} from "@dokploy/server";
import { TRPCError } from "@trpc/server";
import * as bcrypt from "bcrypt";
import { isBefore } from "date-fns";
import { eq } from "drizzle-orm";
import { nanoid } from "nanoid";
import { z } from "zod";
import { db } from "../../db";
import {
adminProcedure,
createTRPCRouter,
protectedProcedure,
publicProcedure,
} from "../trpc";
export const authRouter = createTRPCRouter({
createAdmin: publicProcedure
.input(apiCreateAdmin)
.mutation(async ({ ctx, input }) => {
try {
if (!IS_CLOUD) {
const admin = await db.query.admins.findFirst({});
if (admin) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Admin already exists",
});
}
}
const newAdmin = await createAdmin(input);
const session = await lucia.createSession(newAdmin.id || "", {});
ctx.res.appendHeader(
"Set-Cookie",
lucia.createSessionCookie(session.id).serialize(),
);
return true;
} catch (error) {
throw error;
}
}),
createUser: publicProcedure
.input(apiCreateUser)
.mutation(async ({ ctx, input }) => {
try {
const token = await getUserByToken(input.token);
if (token.isExpired) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Invalid token",
});
}
const newUser = await createUser(input);
const session = await lucia.createSession(newUser?.authId || "", {});
ctx.res.appendHeader(
"Set-Cookie",
lucia.createSessionCookie(session.id).serialize(),
);
return true;
} catch (error) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Error to create the user",
cause: error,
});
}
}),
login: publicProcedure.input(apiLogin).mutation(async ({ ctx, input }) => {
try {
const auth = await findAuthByEmail(input.email);
const correctPassword = bcrypt.compareSync(
input.password,
auth?.password || "",
);
if (!correctPassword) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Credentials do not match",
});
}
if (auth?.is2FAEnabled) {
return {
is2FAEnabled: true,
authId: auth.id,
};
}
const session = await lucia.createSession(auth?.id || "", {});
ctx.res.appendHeader(
"Set-Cookie",
lucia.createSessionCookie(session.id).serialize(),
);
return {
is2FAEnabled: false,
authId: auth?.id,
};
} catch (error) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Credentials do not match",
cause: error,
});
}
}),
get: protectedProcedure.query(async ({ ctx }) => {
const auth = await findAuthById(ctx.user.authId);
return auth;
}),
logout: protectedProcedure.mutation(async ({ ctx }) => {
const { req, res } = ctx;
const { session } = await validateRequest(req, res);
if (!session) return false;
await lucia.invalidateSession(session.id);
res.setHeader("Set-Cookie", lucia.createBlankSessionCookie().serialize());
return true;
}),
update: protectedProcedure
.input(apiUpdateAuth)
.mutation(async ({ ctx, input }) => {
const auth = await updateAuthById(ctx.user.authId, {
...(input.email && { email: input.email }),
...(input.password && {
password: bcrypt.hashSync(input.password, 10),
}),
...(input.image && { image: input.image }),
});
return auth;
}),
generateToken: protectedProcedure.mutation(async ({ ctx, input }) => {
const auth = await findAuthById(ctx.user.authId);
if (auth.token) {
await luciaToken.invalidateSession(auth.token);
}
const session = await luciaToken.createSession(auth?.id || "", {
expiresIn: 60 * 60 * 24 * 30,
});
await updateAuthById(auth.id, {
token: session.id,
});
return auth;
}),
one: adminProcedure.input(apiFindOneAuth).query(async ({ input }) => {
const auth = await findAuthById(input.id);
return auth;
}),
updateByAdmin: protectedProcedure
.input(apiUpdateAuthByAdmin)
.mutation(async ({ input }) => {
const auth = await updateAuthById(input.id, {
...(input.email && { email: input.email }),
...(input.password && {
password: bcrypt.hashSync(input.password, 10),
}),
...(input.image && { image: input.image }),
});
return auth;
}),
generate2FASecret: protectedProcedure.query(async ({ ctx }) => {
return await generate2FASecret(ctx.user.authId);
}),
verify2FASetup: protectedProcedure
.input(apiVerify2FA)
.mutation(async ({ ctx, input }) => {
const auth = await findAuthById(ctx.user.authId);
await verify2FA(auth, input.secret, input.pin);
await updateAuthById(auth.id, {
is2FAEnabled: true,
secret: input.secret,
});
return auth;
}),
verifyLogin2FA: publicProcedure
.input(apiVerifyLogin2FA)
.mutation(async ({ ctx, input }) => {
const auth = await findAuthById(input.id);
await verify2FA(auth, auth.secret || "", input.pin);
const session = await lucia.createSession(auth.id, {});
ctx.res.appendHeader(
"Set-Cookie",
lucia.createSessionCookie(session.id).serialize(),
);
return true;
}),
disable2FA: protectedProcedure.mutation(async ({ ctx }) => {
const auth = await findAuthById(ctx.user.authId);
await updateAuthById(auth.id, {
is2FAEnabled: false,
secret: null,
});
return auth;
}),
verifyToken: protectedProcedure.mutation(async () => {
return true;
}),
sendResetPasswordEmail: publicProcedure
.input(
z.object({
email: z.string().min(1).email(),
}),
)
.mutation(async ({ ctx, input }) => {
if (!IS_CLOUD) {
throw new TRPCError({
code: "NOT_FOUND",
message: "This feature is only available in the cloud version",
});
}
const authR = await db.query.auth.findFirst({
where: eq(auth.email, input.email),
});
if (!authR) {
throw new TRPCError({
code: "NOT_FOUND",
message: "User not found",
});
}
const token = nanoid();
await updateAuthById(authR.id, {
resetPasswordToken: token,
// Make resetPassword in 24 hours
resetPasswordExpiresAt: new Date(
new Date().getTime() + 24 * 60 * 60 * 1000,
).toISOString(),
});
const email = await sendEmailNotification(
{
fromAddress: process.env.SMTP_FROM_ADDRESS || "",
toAddresses: [authR.email],
smtpServer: process.env.SMTP_SERVER || "",
smtpPort: Number(process.env.SMTP_PORT),
username: process.env.SMTP_USERNAME || "",
password: process.env.SMTP_PASSWORD || "",
},
"Reset Password",
`
Reset your password by clicking the link below:
The link will expire in 24 hours.
<a href="http://localhost:3000/reset-password?token=${token}">
Reset Password
</a>
`,
);
}),
resetPassword: publicProcedure
.input(
z.object({
resetPasswordToken: z.string().min(1),
password: z.string().min(1),
}),
)
.mutation(async ({ ctx, input }) => {
if (!IS_CLOUD) {
throw new TRPCError({
code: "NOT_FOUND",
message: "This feature is only available in the cloud version",
});
}
const authR = await db.query.auth.findFirst({
where: eq(auth.resetPasswordToken, input.resetPasswordToken),
});
if (!authR || authR.resetPasswordExpiresAt === null) {
throw new TRPCError({
code: "NOT_FOUND",
message: "Token not found",
});
}
const isExpired = isBefore(
new Date(authR.resetPasswordExpiresAt),
new Date(),
);
if (isExpired) {
throw new TRPCError({
code: "NOT_FOUND",
message: "Token expired",
});
}
await updateAuthById(authR.id, {
resetPasswordExpiresAt: null,
resetPasswordToken: null,
password: bcrypt.hashSync(input.password, 10),
});
return true;
}),
});

View File

@@ -0,0 +1,232 @@
import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc";
import {
apiCreateBackup,
apiFindOneBackup,
apiRemoveBackup,
apiUpdateBackup,
} from "@/server/db/schema";
import { removeJob, schedule, updateJob } from "@/server/utils/backup";
import {
IS_CLOUD,
createBackup,
findBackupById,
findMariadbByBackupId,
findMongoByBackupId,
findMySqlByBackupId,
findPostgresByBackupId,
findServerById,
removeBackupById,
removeScheduleBackup,
runMariadbBackup,
runMongoBackup,
runMySqlBackup,
runPostgresBackup,
scheduleBackup,
updateBackupById,
} from "@dokploy/server";
import { TRPCError } from "@trpc/server";
export const backupRouter = createTRPCRouter({
create: protectedProcedure
.input(apiCreateBackup)
.mutation(async ({ input, ctx }) => {
try {
const newBackup = await createBackup(input);
const backup = await findBackupById(newBackup.backupId);
if (IS_CLOUD && backup.enabled) {
const databaseType = backup.databaseType;
let serverId = "";
if (databaseType === "postgres" && backup.postgres?.serverId) {
serverId = backup.postgres.serverId;
} else if (databaseType === "mysql" && backup.mysql?.serverId) {
serverId = backup.mysql.serverId;
} else if (databaseType === "mongo" && backup.mongo?.serverId) {
serverId = backup.mongo.serverId;
} else if (databaseType === "mariadb" && backup.mariadb?.serverId) {
serverId = backup.mariadb.serverId;
}
const server = await findServerById(serverId);
if (server.serverStatus === "inactive") {
throw new TRPCError({
code: "NOT_FOUND",
message: "Server is inactive",
});
}
await schedule({
cronSchedule: backup.schedule,
backupId: backup.backupId,
type: "backup",
});
} else {
if (backup.enabled) {
scheduleBackup(backup);
}
}
} catch (error) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Error to create the Backup",
cause: error,
});
}
}),
one: protectedProcedure
.input(apiFindOneBackup)
.query(async ({ input, ctx }) => {
const backup = await findBackupById(input.backupId);
return backup;
}),
update: protectedProcedure
.input(apiUpdateBackup)
.mutation(async ({ input, ctx }) => {
try {
await updateBackupById(input.backupId, input);
const backup = await findBackupById(input.backupId);
if (IS_CLOUD) {
if (backup.enabled) {
await updateJob({
cronSchedule: backup.schedule,
backupId: backup.backupId,
type: "backup",
});
} else {
await removeJob({
cronSchedule: backup.schedule,
backupId: backup.backupId,
type: "backup",
});
}
} else {
if (backup.enabled) {
removeScheduleBackup(input.backupId);
scheduleBackup(backup);
} else {
removeScheduleBackup(input.backupId);
}
}
} catch (error) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Error to update this Backup",
});
}
}),
remove: protectedProcedure
.input(apiRemoveBackup)
.mutation(async ({ input, ctx }) => {
try {
const value = await removeBackupById(input.backupId);
if (IS_CLOUD && value) {
removeJob({
backupId: input.backupId,
cronSchedule: value.schedule,
type: "backup",
});
} else if (!IS_CLOUD) {
removeScheduleBackup(input.backupId);
}
return value;
} catch (error) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Error to delete this Backup",
cause: error,
});
}
}),
manualBackupPostgres: protectedProcedure
.input(apiFindOneBackup)
.mutation(async ({ input }) => {
try {
const backup = await findBackupById(input.backupId);
const postgres = await findPostgresByBackupId(backup.backupId);
await runPostgresBackup(postgres, backup);
return true;
} catch (error) {
console.log(error);
throw new TRPCError({
code: "BAD_REQUEST",
message: "Error to run manual postgres backup ",
cause: error,
});
}
}),
manualBackupMySql: protectedProcedure
.input(apiFindOneBackup)
.mutation(async ({ input }) => {
try {
const backup = await findBackupById(input.backupId);
const mysql = await findMySqlByBackupId(backup.backupId);
await runMySqlBackup(mysql, backup);
return true;
} catch (error) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Error to run manual mysql backup ",
cause: error,
});
}
}),
manualBackupMariadb: protectedProcedure
.input(apiFindOneBackup)
.mutation(async ({ input }) => {
try {
const backup = await findBackupById(input.backupId);
const mariadb = await findMariadbByBackupId(backup.backupId);
await runMariadbBackup(mariadb, backup);
return true;
} catch (error) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Error to run manual mariadb backup ",
cause: error,
});
}
}),
manualBackupMongo: protectedProcedure
.input(apiFindOneBackup)
.mutation(async ({ input }) => {
try {
const backup = await findBackupById(input.backupId);
const mongo = await findMongoByBackupId(backup.backupId);
await runMongoBackup(mongo, backup);
return true;
} catch (error) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Error to run manual mongo backup ",
cause: error,
});
}
}),
});
// export const getAdminId = async (backupId: string) => {
// const backup = await findBackupById(backupId);
// if (backup.databaseType === "postgres" && backup.postgresId) {
// const postgres = await findPostgresById(backup.postgresId);
// return postgres.project.adminId;
// }
// if (backup.databaseType === "mariadb" && backup.mariadbId) {
// const mariadb = await findMariadbById(backup.mariadbId);
// return mariadb.project.adminId;
// }
// if (backup.databaseType === "mysql" && backup.mysqlId) {
// const mysql = await findMySqlById(backup.mysqlId);
// return mysql.project.adminId;
// }
// if (backup.databaseType === "mongo" && backup.mongoId) {
// const mongo = await findMongoById(backup.mongoId);
// return mongo.project.adminId;
// }
// return null;
// };

View File

@@ -0,0 +1,148 @@
import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc";
import { db } from "@/server/db";
import {
apiBitbucketTestConnection,
apiCreateBitbucket,
apiFindBitbucketBranches,
apiFindOneBitbucket,
apiUpdateBitbucket,
} from "@/server/db/schema";
import {
IS_CLOUD,
createBitbucket,
findBitbucketById,
getBitbucketBranches,
getBitbucketRepositories,
testBitbucketConnection,
updateBitbucket,
} from "@dokploy/server";
import { TRPCError } from "@trpc/server";
export const bitbucketRouter = createTRPCRouter({
create: protectedProcedure
.input(apiCreateBitbucket)
.mutation(async ({ input, ctx }) => {
try {
return await createBitbucket(input, ctx.user.adminId);
} catch (error) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Error to create this bitbucket provider",
cause: error,
});
}
}),
one: protectedProcedure
.input(apiFindOneBitbucket)
.query(async ({ input, ctx }) => {
const bitbucketProvider = await findBitbucketById(input.bitbucketId);
if (
IS_CLOUD &&
bitbucketProvider.gitProvider.adminId !== ctx.user.adminId
) {
//TODO: Remove this line when the cloud version is ready
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not allowed to access this bitbucket provider",
});
}
return bitbucketProvider;
}),
bitbucketProviders: protectedProcedure.query(async ({ ctx }) => {
let result = await db.query.bitbucket.findMany({
with: {
gitProvider: true,
},
columns: {
bitbucketId: true,
},
});
if (IS_CLOUD) {
// TODO: mAyBe a rEfaCtoR 🤫
result = result.filter(
(provider) => provider.gitProvider.adminId === ctx.user.adminId,
);
}
return result;
}),
getBitbucketRepositories: protectedProcedure
.input(apiFindOneBitbucket)
.query(async ({ input, ctx }) => {
const bitbucketProvider = await findBitbucketById(input.bitbucketId);
if (
IS_CLOUD &&
bitbucketProvider.gitProvider.adminId !== ctx.user.adminId
) {
//TODO: Remove this line when the cloud version is ready
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not allowed to access this bitbucket provider",
});
}
return await getBitbucketRepositories(input.bitbucketId);
}),
getBitbucketBranches: protectedProcedure
.input(apiFindBitbucketBranches)
.query(async ({ input, ctx }) => {
const bitbucketProvider = await findBitbucketById(
input.bitbucketId || "",
);
if (
IS_CLOUD &&
bitbucketProvider.gitProvider.adminId !== ctx.user.adminId
) {
//TODO: Remove this line when the cloud version is ready
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not allowed to access this bitbucket provider",
});
}
return await getBitbucketBranches(input);
}),
testConnection: protectedProcedure
.input(apiBitbucketTestConnection)
.mutation(async ({ input, ctx }) => {
try {
const bitbucketProvider = await findBitbucketById(input.bitbucketId);
if (
IS_CLOUD &&
bitbucketProvider.gitProvider.adminId !== ctx.user.adminId
) {
//TODO: Remove this line when the cloud version is ready
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not allowed to access this bitbucket provider",
});
}
const result = await testBitbucketConnection(input);
return `Found ${result} repositories`;
} catch (error) {
throw new TRPCError({
code: "BAD_REQUEST",
message: error instanceof Error ? error?.message : `Error: ${error}`,
});
}
}),
update: protectedProcedure
.input(apiUpdateBitbucket)
.mutation(async ({ input, ctx }) => {
const bitbucketProvider = await findBitbucketById(input.bitbucketId);
if (
IS_CLOUD &&
bitbucketProvider.gitProvider.adminId !== ctx.user.adminId
) {
//TODO: Remove this line when the cloud version is ready
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not allowed to access this bitbucket provider",
});
}
return await updateBitbucket(input.bitbucketId, {
...input,
adminId: ctx.user.adminId,
});
}),
});

View File

@@ -0,0 +1,62 @@
import { adminProcedure, createTRPCRouter } from "@/server/api/trpc";
import {
apiCreateCertificate,
apiFindCertificate,
certificates,
} from "@/server/db/schema";
import { db } from "@/server/db";
import {
IS_CLOUD,
createCertificate,
findCertificateById,
removeCertificateById,
} from "@dokploy/server";
import { TRPCError } from "@trpc/server";
import { eq } from "drizzle-orm";
export const certificateRouter = createTRPCRouter({
create: adminProcedure
.input(apiCreateCertificate)
.mutation(async ({ input, ctx }) => {
if (IS_CLOUD && !input.serverId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "Please set a server to create a certificate",
});
}
return await createCertificate(input, ctx.user.adminId);
}),
one: adminProcedure
.input(apiFindCertificate)
.query(async ({ input, ctx }) => {
const certificates = await findCertificateById(input.certificateId);
if (IS_CLOUD && certificates.adminId !== ctx.user.adminId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not allowed to access this certificate",
});
}
return certificates;
}),
remove: adminProcedure
.input(apiFindCertificate)
.mutation(async ({ input, ctx }) => {
const certificates = await findCertificateById(input.certificateId);
if (IS_CLOUD && certificates.adminId !== ctx.user.adminId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not allowed to delete this certificate",
});
}
await removeCertificateById(input.certificateId);
return true;
}),
all: adminProcedure.query(async ({ ctx }) => {
return await db.query.certificates.findMany({
// TODO: Remove this line when the cloud version is ready
...(IS_CLOUD && { where: eq(certificates.adminId, ctx.user.adminId) }),
});
}),
});

View File

@@ -0,0 +1,76 @@
import { getPublicIpWithFallback } from "@/server/wss/terminal";
import { type DockerNode, IS_CLOUD, docker, execAsync } from "@dokploy/server";
import { TRPCError } from "@trpc/server";
import { z } from "zod";
import { createTRPCRouter, protectedProcedure } from "../trpc";
export const clusterRouter = createTRPCRouter({
getNodes: protectedProcedure.query(async () => {
if (IS_CLOUD) {
return [];
}
const workers: DockerNode[] = await docker.listNodes();
return workers;
}),
removeWorker: protectedProcedure
.input(
z.object({
nodeId: z.string(),
}),
)
.mutation(async ({ input }) => {
if (IS_CLOUD) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "Functionality not available in cloud version",
});
}
try {
await execAsync(
`docker node update --availability drain ${input.nodeId}`,
);
await execAsync(`docker node rm ${input.nodeId} --force`);
return true;
} catch (error) {
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: "Error to remove the node",
cause: error,
});
}
}),
addWorker: protectedProcedure.query(async ({ input }) => {
if (IS_CLOUD) {
return {
command: "",
version: "",
};
}
const result = await docker.swarmInspect();
const docker_version = await docker.version();
return {
command: `docker swarm join --token ${
result.JoinTokens.Worker
} ${await getPublicIpWithFallback()}:2377`,
version: docker_version.Version,
};
}),
addManager: protectedProcedure.query(async ({ input }) => {
if (IS_CLOUD) {
return {
command: "",
version: "",
};
}
const result = await docker.swarmInspect();
const docker_version = await docker.version();
return {
command: `docker swarm join --token ${
result.JoinTokens.Manager
} ${await getPublicIpWithFallback()}:2377`,
version: docker_version.Version,
};
}),
});

View File

@@ -0,0 +1,448 @@
import { slugify } from "@/lib/slug";
import { db } from "@/server/db";
import {
apiCreateCompose,
apiCreateComposeByTemplate,
apiFetchServices,
apiFindCompose,
apiRandomizeCompose,
apiUpdateCompose,
compose,
} from "@/server/db/schema";
import {
type DeploymentJob,
cleanQueuesByCompose,
} from "@/server/queues/deployments-queue";
import { myQueue } from "@/server/queues/queueSetup";
import { templates } from "@/templates/templates";
import type { TemplatesKeys } from "@/templates/types/templates-data.type";
import {
generatePassword,
loadTemplateModule,
readTemplateComposeFile,
} from "@/templates/utils";
import { TRPCError } from "@trpc/server";
import { eq } from "drizzle-orm";
import { dump } from "js-yaml";
import _ from "lodash";
import { nanoid } from "nanoid";
import { createTRPCRouter, protectedProcedure } from "../trpc";
import { deploy } from "@/server/utils/deploy";
import {
IS_CLOUD,
addDomainToCompose,
addNewService,
checkServiceAccess,
cloneCompose,
cloneComposeRemote,
createCommand,
createCompose,
createComposeByTemplate,
createDomain,
createMount,
findAdmin,
findAdminById,
findComposeById,
findDomainsByComposeId,
findProjectById,
findServerById,
loadServices,
randomizeComposeFile,
removeCompose,
removeComposeDirectory,
removeDeploymentsByComposeId,
stopCompose,
updateCompose,
} from "@dokploy/server";
export const composeRouter = createTRPCRouter({
create: protectedProcedure
.input(apiCreateCompose)
.mutation(async ({ ctx, input }) => {
try {
if (ctx.user.rol === "user") {
await checkServiceAccess(ctx.user.authId, input.projectId, "create");
}
if (IS_CLOUD && !input.serverId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You need to use a server to create a compose",
});
}
const project = await findProjectById(input.projectId);
if (project.adminId !== ctx.user.adminId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to access this project",
});
}
const newService = await createCompose(input);
if (ctx.user.rol === "user") {
await addNewService(ctx.user.authId, newService.composeId);
}
} catch (error) {
throw error;
}
}),
one: protectedProcedure
.input(apiFindCompose)
.query(async ({ input, ctx }) => {
if (ctx.user.rol === "user") {
await checkServiceAccess(ctx.user.authId, input.composeId, "access");
}
const compose = await findComposeById(input.composeId);
if (compose.project.adminId !== ctx.user.adminId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to access this compose",
});
}
return compose;
}),
update: protectedProcedure
.input(apiUpdateCompose)
.mutation(async ({ input, ctx }) => {
const compose = await findComposeById(input.composeId);
if (compose.project.adminId !== ctx.user.adminId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to update this compose",
});
}
return updateCompose(input.composeId, input);
}),
delete: protectedProcedure
.input(apiFindCompose)
.mutation(async ({ input, ctx }) => {
if (ctx.user.rol === "user") {
await checkServiceAccess(ctx.user.authId, input.composeId, "delete");
}
const composeResult = await findComposeById(input.composeId);
if (composeResult.project.adminId !== ctx.user.adminId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to delete this compose",
});
}
4;
const result = await db
.delete(compose)
.where(eq(compose.composeId, input.composeId))
.returning();
const cleanupOperations = [
async () => await removeCompose(composeResult),
async () => await removeDeploymentsByComposeId(composeResult),
async () => await removeComposeDirectory(composeResult.appName),
];
for (const operation of cleanupOperations) {
try {
await operation();
} catch (error) {}
}
return result[0];
}),
cleanQueues: protectedProcedure
.input(apiFindCompose)
.mutation(async ({ input, ctx }) => {
const compose = await findComposeById(input.composeId);
if (compose.project.adminId !== ctx.user.adminId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to clean this compose",
});
}
await cleanQueuesByCompose(input.composeId);
}),
loadServices: protectedProcedure
.input(apiFetchServices)
.query(async ({ input, ctx }) => {
const compose = await findComposeById(input.composeId);
if (compose.project.adminId !== ctx.user.adminId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to load this compose",
});
}
return await loadServices(input.composeId, input.type);
}),
fetchSourceType: protectedProcedure
.input(apiFindCompose)
.mutation(async ({ input, ctx }) => {
try {
const compose = await findComposeById(input.composeId);
if (compose.project.adminId !== ctx.user.adminId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to fetch this compose",
});
}
if (compose.serverId) {
await cloneComposeRemote(compose);
} else {
await cloneCompose(compose);
}
return compose.sourceType;
} catch (err) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Error to fetch source type",
cause: err,
});
}
}),
randomizeCompose: protectedProcedure
.input(apiRandomizeCompose)
.mutation(async ({ input, ctx }) => {
const compose = await findComposeById(input.composeId);
if (compose.project.adminId !== ctx.user.adminId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to randomize this compose",
});
}
return await randomizeComposeFile(input.composeId, input.suffix);
}),
getConvertedCompose: protectedProcedure
.input(apiFindCompose)
.query(async ({ input, ctx }) => {
const compose = await findComposeById(input.composeId);
if (compose.project.adminId !== ctx.user.adminId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to get this compose",
});
}
const domains = await findDomainsByComposeId(input.composeId);
const composeFile = await addDomainToCompose(compose, domains);
return dump(composeFile, {
lineWidth: 1000,
});
}),
deploy: protectedProcedure
.input(apiFindCompose)
.mutation(async ({ input, ctx }) => {
const compose = await findComposeById(input.composeId);
if (compose.project.adminId !== ctx.user.adminId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to deploy this compose",
});
}
const jobData: DeploymentJob = {
composeId: input.composeId,
titleLog: "Manual deployment",
type: "deploy",
applicationType: "compose",
descriptionLog: "",
server: !!compose.serverId,
};
if (IS_CLOUD && compose.serverId) {
jobData.serverId = compose.serverId;
await deploy(jobData);
return true;
}
await myQueue.add(
"deployments",
{ ...jobData },
{
removeOnComplete: true,
removeOnFail: true,
},
);
}),
redeploy: protectedProcedure
.input(apiFindCompose)
.mutation(async ({ input, ctx }) => {
const compose = await findComposeById(input.composeId);
if (compose.project.adminId !== ctx.user.adminId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to redeploy this compose",
});
}
const jobData: DeploymentJob = {
composeId: input.composeId,
titleLog: "Rebuild deployment",
type: "redeploy",
applicationType: "compose",
descriptionLog: "",
server: !!compose.serverId,
};
if (IS_CLOUD && compose.serverId) {
jobData.serverId = compose.serverId;
await deploy(jobData);
return true;
}
await myQueue.add(
"deployments",
{ ...jobData },
{
removeOnComplete: true,
removeOnFail: true,
},
);
}),
stop: protectedProcedure
.input(apiFindCompose)
.mutation(async ({ input, ctx }) => {
const compose = await findComposeById(input.composeId);
if (compose.project.adminId !== ctx.user.adminId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to stop this compose",
});
}
await stopCompose(input.composeId);
return true;
}),
getDefaultCommand: protectedProcedure
.input(apiFindCompose)
.query(async ({ input, ctx }) => {
const compose = await findComposeById(input.composeId);
if (compose.project.adminId !== ctx.user.adminId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to get this compose",
});
}
const command = createCommand(compose);
return `docker ${command}`;
}),
refreshToken: protectedProcedure
.input(apiFindCompose)
.mutation(async ({ input, ctx }) => {
const compose = await findComposeById(input.composeId);
if (compose.project.adminId !== ctx.user.adminId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to refresh this compose",
});
}
await updateCompose(input.composeId, {
refreshToken: nanoid(),
});
return true;
}),
deployTemplate: protectedProcedure
.input(apiCreateComposeByTemplate)
.mutation(async ({ ctx, input }) => {
if (ctx.user.rol === "user") {
await checkServiceAccess(ctx.user.authId, input.projectId, "create");
}
if (IS_CLOUD && !input.serverId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You need to use a server to create a compose",
});
}
const composeFile = await readTemplateComposeFile(input.id);
const generate = await loadTemplateModule(input.id as TemplatesKeys);
const admin = await findAdminById(ctx.user.adminId);
let serverIp = admin.serverIp;
if (!admin.serverIp) {
throw new TRPCError({
code: "NOT_FOUND",
message:
"You need to have a server IP to deploy this template in order to generate domains",
});
}
const project = await findProjectById(input.projectId);
if (input.serverId) {
const server = await findServerById(input.serverId);
serverIp = server.ipAddress;
} else if (process.env.NODE_ENV === "development") {
serverIp = "127.0.0.1";
}
const projectName = slugify(`${project.name} ${input.id}`);
const { envs, mounts, domains } = generate({
serverIp: serverIp || "",
projectName: projectName,
});
const compose = await createComposeByTemplate({
...input,
composeFile: composeFile,
env: envs?.join("\n"),
serverId: input.serverId,
name: input.id,
sourceType: "raw",
appName: `${projectName}-${generatePassword(6)}`,
});
if (ctx.user.rol === "user") {
await addNewService(ctx.user.authId, compose.composeId);
}
if (mounts && mounts?.length > 0) {
for (const mount of mounts) {
await createMount({
filePath: mount.filePath,
mountPath: "",
content: mount.content,
serviceId: compose.composeId,
serviceType: "compose",
type: "file",
});
}
}
if (domains && domains?.length > 0) {
for (const domain of domains) {
await createDomain({
...domain,
domainType: "compose",
certificateType: "none",
composeId: compose.composeId,
});
}
}
return null;
}),
templates: protectedProcedure.query(async () => {
const templatesData = templates.map((t) => ({
name: t.name,
description: t.description,
id: t.id,
links: t.links,
tags: t.tags,
logo: t.logo,
version: t.version,
}));
return templatesData;
}),
getTags: protectedProcedure.query(async ({ input }) => {
const allTags = templates.flatMap((template) => template.tags);
const uniqueTags = _.uniq(allTags);
return uniqueTags;
}),
});

View File

@@ -0,0 +1,55 @@
import {
apiFindAllByApplication,
apiFindAllByCompose,
apiFindAllByServer,
} from "@/server/db/schema";
import {
findAllDeploymentsByApplicationId,
findAllDeploymentsByComposeId,
findAllDeploymentsByServerId,
findApplicationById,
findComposeById,
findServerById,
} from "@dokploy/server";
import { TRPCError } from "@trpc/server";
import { createTRPCRouter, protectedProcedure } from "../trpc";
export const deploymentRouter = createTRPCRouter({
all: protectedProcedure
.input(apiFindAllByApplication)
.query(async ({ input, ctx }) => {
const application = await findApplicationById(input.applicationId);
if (application.project.adminId !== ctx.user.adminId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to access this application",
});
}
return await findAllDeploymentsByApplicationId(input.applicationId);
}),
allByCompose: protectedProcedure
.input(apiFindAllByCompose)
.query(async ({ input, ctx }) => {
const compose = await findComposeById(input.composeId);
if (compose.project.adminId !== ctx.user.adminId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to access this compose",
});
}
return await findAllDeploymentsByComposeId(input.composeId);
}),
allByServer: protectedProcedure
.input(apiFindAllByServer)
.query(async ({ input, ctx }) => {
const server = await findServerById(input.serverId);
if (server.adminId !== ctx.user.adminId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to access this server",
});
}
return await findAllDeploymentsByServerId(input.serverId);
}),
});

View File

@@ -0,0 +1,137 @@
import {
adminProcedure,
createTRPCRouter,
protectedProcedure,
} from "@/server/api/trpc";
import { db } from "@/server/db";
import {
apiCreateDestination,
apiFindOneDestination,
apiRemoveDestination,
apiUpdateDestination,
destinations,
} from "@/server/db/schema";
import {
IS_CLOUD,
createDestintation,
execAsync,
execAsyncRemote,
findDestinationById,
removeDestinationById,
updateDestinationById,
} from "@dokploy/server";
import { TRPCError } from "@trpc/server";
import { eq } from "drizzle-orm";
export const destinationRouter = createTRPCRouter({
create: adminProcedure
.input(apiCreateDestination)
.mutation(async ({ input, ctx }) => {
try {
return await createDestintation(input, ctx.user.adminId);
} catch (error) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Error to create the destination",
cause: error,
});
}
}),
testConnection: adminProcedure
.input(apiCreateDestination)
.mutation(async ({ input }) => {
const { secretAccessKey, bucket, region, endpoint, accessKey } = input;
try {
const rcloneFlags = [
// `--s3-provider=Cloudflare`,
`--s3-access-key-id=${accessKey}`,
`--s3-secret-access-key=${secretAccessKey}`,
`--s3-region=${region}`,
`--s3-endpoint=${endpoint}`,
"--s3-no-check-bucket",
"--s3-force-path-style",
];
const rcloneDestination = `:s3:${bucket}`;
const rcloneCommand = `rclone ls ${rcloneFlags.join(" ")} "${rcloneDestination}"`;
if (IS_CLOUD && !input.serverId) {
throw new TRPCError({
code: "NOT_FOUND",
message: "Server not found",
});
}
if (IS_CLOUD) {
await execAsyncRemote(input.serverId || "", rcloneCommand);
} else {
await execAsync(rcloneCommand);
}
} catch (error) {
throw new TRPCError({
code: "BAD_REQUEST",
message:
error instanceof Error
? error?.message
: "Error to connect to bucket",
cause: error,
});
}
}),
one: protectedProcedure
.input(apiFindOneDestination)
.query(async ({ input, ctx }) => {
const destination = await findDestinationById(input.destinationId);
if (destination.adminId !== ctx.user.adminId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not allowed to access this destination",
});
}
return destination;
}),
all: protectedProcedure.query(async ({ ctx }) => {
return await db.query.destinations.findMany({
where: eq(destinations.adminId, ctx.user.adminId),
});
}),
remove: adminProcedure
.input(apiRemoveDestination)
.mutation(async ({ input, ctx }) => {
try {
const destination = await findDestinationById(input.destinationId);
if (destination.adminId !== ctx.user.adminId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not allowed to delete this destination",
});
}
return await removeDestinationById(
input.destinationId,
ctx.user.adminId,
);
} catch (error) {
throw error;
}
}),
update: adminProcedure
.input(apiUpdateDestination)
.mutation(async ({ input, ctx }) => {
try {
const destination = await findDestinationById(input.destinationId);
if (destination.adminId !== ctx.user.adminId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not allowed to update this destination",
});
}
return await updateDestinationById(input.destinationId, {
...input,
adminId: ctx.user.adminId,
});
} catch (error) {
throw error;
}
}),
});

View File

@@ -0,0 +1,71 @@
import {
containerRestart,
getConfig,
getContainers,
getContainersByAppLabel,
getContainersByAppNameMatch,
} from "@dokploy/server";
import { z } from "zod";
import { createTRPCRouter, protectedProcedure } from "../trpc";
export const dockerRouter = createTRPCRouter({
getContainers: protectedProcedure
.input(
z.object({
serverId: z.string().optional(),
}),
)
.query(async ({ input }) => {
return await getContainers(input.serverId);
}),
restartContainer: protectedProcedure
.input(
z.object({
containerId: z.string().min(1),
}),
)
.mutation(async ({ input }) => {
return await containerRestart(input.containerId);
}),
getConfig: protectedProcedure
.input(
z.object({
containerId: z.string().min(1),
serverId: z.string().optional(),
}),
)
.query(async ({ input }) => {
return await getConfig(input.containerId, input.serverId);
}),
getContainersByAppNameMatch: protectedProcedure
.input(
z.object({
appType: z
.union([z.literal("stack"), z.literal("docker-compose")])
.optional(),
appName: z.string().min(1),
serverId: z.string().optional(),
}),
)
.query(async ({ input }) => {
return await getContainersByAppNameMatch(
input.appName,
input.appType,
input.serverId,
);
}),
getContainersByAppLabel: protectedProcedure
.input(
z.object({
appName: z.string().min(1),
serverId: z.string().optional(),
}),
)
.query(async ({ input }) => {
return await getContainersByAppLabel(input.appName, input.serverId);
}),
});

View File

@@ -0,0 +1,171 @@
import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc";
import {
apiCreateDomain,
apiFindCompose,
apiFindDomain,
apiFindOneApplication,
apiUpdateDomain,
} from "@/server/db/schema";
import {
createDomain,
findApplicationById,
findComposeById,
findDomainById,
findDomainsByApplicationId,
findDomainsByComposeId,
generateTraefikMeDomain,
manageDomain,
removeDomain,
removeDomainById,
updateDomainById,
} from "@dokploy/server";
import { TRPCError } from "@trpc/server";
import { z } from "zod";
export const domainRouter = createTRPCRouter({
create: protectedProcedure
.input(apiCreateDomain)
.mutation(async ({ input, ctx }) => {
try {
if (input.domainType === "compose" && input.composeId) {
const compose = await findComposeById(input.composeId);
if (compose.project.adminId !== ctx.user.adminId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to access this compose",
});
}
} else if (input.domainType === "application" && input.applicationId) {
const application = await findApplicationById(input.applicationId);
if (application.project.adminId !== ctx.user.adminId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to access this application",
});
}
}
return await createDomain(input);
} catch (error) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Error to create the domain",
cause: error,
});
}
}),
byApplicationId: protectedProcedure
.input(apiFindOneApplication)
.query(async ({ input, ctx }) => {
const application = await findApplicationById(input.applicationId);
if (application.project.adminId !== ctx.user.adminId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to access this application",
});
}
return await findDomainsByApplicationId(input.applicationId);
}),
byComposeId: protectedProcedure
.input(apiFindCompose)
.query(async ({ input, ctx }) => {
const compose = await findComposeById(input.composeId);
if (compose.project.adminId !== ctx.user.adminId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to access this compose",
});
}
return await findDomainsByComposeId(input.composeId);
}),
generateDomain: protectedProcedure
.input(z.object({ appName: z.string(), serverId: z.string().optional() }))
.mutation(async ({ input, ctx }) => {
return generateTraefikMeDomain(
input.appName,
ctx.user.adminId,
input.serverId,
);
}),
update: protectedProcedure
.input(apiUpdateDomain)
.mutation(async ({ input, ctx }) => {
const currentDomain = await findDomainById(input.domainId);
if (currentDomain.applicationId) {
const newApp = await findApplicationById(currentDomain.applicationId);
if (newApp.project.adminId !== ctx.user.adminId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to access this application",
});
}
} else if (currentDomain.composeId) {
const newCompose = await findComposeById(currentDomain.composeId);
if (newCompose.project.adminId !== ctx.user.adminId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to access this compose",
});
}
}
const result = await updateDomainById(input.domainId, input);
const domain = await findDomainById(input.domainId);
if (domain.applicationId) {
const application = await findApplicationById(domain.applicationId);
await manageDomain(application, domain);
}
return result;
}),
one: protectedProcedure.input(apiFindDomain).query(async ({ input, ctx }) => {
const domain = await findDomainById(input.domainId);
if (domain.applicationId) {
const application = await findApplicationById(domain.applicationId);
if (application.project.adminId !== ctx.user.adminId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to access this application",
});
}
} else if (domain.composeId) {
const compose = await findComposeById(domain.composeId);
if (compose.project.adminId !== ctx.user.adminId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to access this compose",
});
}
}
return await findDomainById(input.domainId);
}),
delete: protectedProcedure
.input(apiFindDomain)
.mutation(async ({ input, ctx }) => {
const domain = await findDomainById(input.domainId);
if (domain.applicationId) {
const application = await findApplicationById(domain.applicationId);
if (application.project.adminId !== ctx.user.adminId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to access this application",
});
}
} else if (domain.composeId) {
const compose = await findComposeById(domain.composeId);
if (compose.project.adminId !== ctx.user.adminId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to access this compose",
});
}
}
const result = await removeDomainById(input.domainId);
if (domain.applicationId) {
const application = await findApplicationById(domain.applicationId);
await removeDomain(application, domain.uniqueConfigKey);
}
return result;
}),
});

View File

@@ -0,0 +1,46 @@
import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc";
import { db } from "@/server/db";
import { apiRemoveGitProvider, gitProvider } from "@/server/db/schema";
import {
IS_CLOUD,
findGitProviderById,
removeGitProvider,
} from "@dokploy/server";
import { TRPCError } from "@trpc/server";
import { desc, eq } from "drizzle-orm";
export const gitProviderRouter = createTRPCRouter({
getAll: protectedProcedure.query(async ({ ctx }) => {
return await db.query.gitProvider.findMany({
with: {
gitlab: true,
bitbucket: true,
github: true,
},
orderBy: desc(gitProvider.createdAt),
...(IS_CLOUD && { where: eq(gitProvider.adminId, ctx.user.adminId) }),
//TODO: Remove this line when the cloud version is ready
});
}),
remove: protectedProcedure
.input(apiRemoveGitProvider)
.mutation(async ({ input, ctx }) => {
try {
const gitProvider = await findGitProviderById(input.gitProviderId);
if (IS_CLOUD && gitProvider.adminId !== ctx.user.adminId) {
// TODO: Remove isCloud in the next versions of dokploy
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not allowed to delete this git provider",
});
}
return await removeGitProvider(input.gitProviderId);
} catch (error) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Error to delete this git provider",
});
}
}),
});

View File

@@ -0,0 +1,126 @@
import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc";
import { db } from "@/server/db";
import {
apiFindGithubBranches,
apiFindOneGithub,
apiUpdateGithub,
} from "@/server/db/schema";
import {
IS_CLOUD,
findGithubById,
getGithubBranches,
getGithubRepositories,
haveGithubRequirements,
updateGitProvider,
} from "@dokploy/server";
import { TRPCError } from "@trpc/server";
export const githubRouter = createTRPCRouter({
one: protectedProcedure
.input(apiFindOneGithub)
.query(async ({ input, ctx }) => {
const githubProvider = await findGithubById(input.githubId);
if (IS_CLOUD && githubProvider.gitProvider.adminId !== ctx.user.adminId) {
//TODO: Remove this line when the cloud version is ready
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not allowed to access this github provider",
});
}
return githubProvider;
}),
getGithubRepositories: protectedProcedure
.input(apiFindOneGithub)
.query(async ({ input, ctx }) => {
const githubProvider = await findGithubById(input.githubId);
if (IS_CLOUD && githubProvider.gitProvider.adminId !== ctx.user.adminId) {
//TODO: Remove this line when the cloud version is ready
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not allowed to access this github provider",
});
}
return await getGithubRepositories(input.githubId);
}),
getGithubBranches: protectedProcedure
.input(apiFindGithubBranches)
.query(async ({ input, ctx }) => {
const githubProvider = await findGithubById(input.githubId || "");
if (IS_CLOUD && githubProvider.gitProvider.adminId !== ctx.user.adminId) {
//TODO: Remove this line when the cloud version is ready
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not allowed to access this github provider",
});
}
return await getGithubBranches(input);
}),
githubProviders: protectedProcedure.query(async ({ ctx }) => {
let result = await db.query.github.findMany({
with: {
gitProvider: true,
},
});
if (IS_CLOUD) {
// TODO: mAyBe a rEfaCtoR 🤫
result = result.filter(
(provider) => provider.gitProvider.adminId === ctx.user.adminId,
);
}
const filtered = result
.filter((provider) => haveGithubRequirements(provider))
.map((provider) => {
return {
githubId: provider.githubId,
gitProvider: {
...provider.gitProvider,
},
};
});
return filtered;
}),
testConnection: protectedProcedure
.input(apiFindOneGithub)
.mutation(async ({ input, ctx }) => {
try {
const githubProvider = await findGithubById(input.githubId);
if (
IS_CLOUD &&
githubProvider.gitProvider.adminId !== ctx.user.adminId
) {
//TODO: Remove this line when the cloud version is ready
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not allowed to access this github provider",
});
}
const result = await getGithubRepositories(input.githubId);
return `Found ${result.length} repositories`;
} catch (err) {
throw new TRPCError({
code: "BAD_REQUEST",
message: err instanceof Error ? err?.message : `Error: ${err}`,
});
}
}),
update: protectedProcedure
.input(apiUpdateGithub)
.mutation(async ({ input, ctx }) => {
const githubProvider = await findGithubById(input.githubId);
if (IS_CLOUD && githubProvider.gitProvider.adminId !== ctx.user.adminId) {
//TODO: Remove this line when the cloud version is ready
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not allowed to access this github provider",
});
}
await updateGitProvider(input.gitProviderId, {
name: input.name,
adminId: ctx.user.adminId,
});
}),
});

View File

@@ -0,0 +1,151 @@
import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc";
import {
apiCreateGitlab,
apiFindGitlabBranches,
apiFindOneGitlab,
apiGitlabTestConnection,
apiUpdateGitlab,
} from "@/server/db/schema";
import { db } from "@/server/db";
import {
IS_CLOUD,
createGitlab,
findGitlabById,
getGitlabBranches,
getGitlabRepositories,
haveGitlabRequirements,
testGitlabConnection,
updateGitProvider,
updateGitlab,
} from "@dokploy/server";
import { TRPCError } from "@trpc/server";
export const gitlabRouter = createTRPCRouter({
create: protectedProcedure
.input(apiCreateGitlab)
.mutation(async ({ input, ctx }) => {
try {
return await createGitlab(input, ctx.user.adminId);
} catch (error) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Error to create this gitlab provider",
cause: error,
});
}
}),
one: protectedProcedure
.input(apiFindOneGitlab)
.query(async ({ input, ctx }) => {
const gitlabProvider = await findGitlabById(input.gitlabId);
if (IS_CLOUD && gitlabProvider.gitProvider.adminId !== ctx.user.adminId) {
//TODO: Remove this line when the cloud version is ready
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not allowed to access this gitlab provider",
});
}
return gitlabProvider;
}),
gitlabProviders: protectedProcedure.query(async ({ ctx }) => {
let result = await db.query.gitlab.findMany({
with: {
gitProvider: true,
},
});
if (IS_CLOUD) {
// TODO: mAyBe a rEfaCtoR 🤫
result = result.filter(
(provider) => provider.gitProvider.adminId === ctx.user.adminId,
);
}
const filtered = result
.filter((provider) => haveGitlabRequirements(provider))
.map((provider) => {
return {
gitlabId: provider.gitlabId,
gitProvider: {
...provider.gitProvider,
},
};
});
return filtered;
}),
getGitlabRepositories: protectedProcedure
.input(apiFindOneGitlab)
.query(async ({ input, ctx }) => {
const gitlabProvider = await findGitlabById(input.gitlabId);
if (IS_CLOUD && gitlabProvider.gitProvider.adminId !== ctx.user.adminId) {
//TODO: Remove this line when the cloud version is ready
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not allowed to access this gitlab provider",
});
}
return await getGitlabRepositories(input.gitlabId);
}),
getGitlabBranches: protectedProcedure
.input(apiFindGitlabBranches)
.query(async ({ input, ctx }) => {
const gitlabProvider = await findGitlabById(input.gitlabId || "");
if (IS_CLOUD && gitlabProvider.gitProvider.adminId !== ctx.user.adminId) {
//TODO: Remove this line when the cloud version is ready
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not allowed to access this gitlab provider",
});
}
return await getGitlabBranches(input);
}),
testConnection: protectedProcedure
.input(apiGitlabTestConnection)
.mutation(async ({ input, ctx }) => {
try {
const gitlabProvider = await findGitlabById(input.gitlabId || "");
if (
IS_CLOUD &&
gitlabProvider.gitProvider.adminId !== ctx.user.adminId
) {
//TODO: Remove this line when the cloud version is ready
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not allowed to access this gitlab provider",
});
}
const result = await testGitlabConnection(input);
return `Found ${result} repositories`;
} catch (error) {
throw new TRPCError({
code: "BAD_REQUEST",
message: error instanceof Error ? error?.message : `Error: ${error}`,
});
}
}),
update: protectedProcedure
.input(apiUpdateGitlab)
.mutation(async ({ input, ctx }) => {
const gitlabProvider = await findGitlabById(input.gitlabId);
if (IS_CLOUD && gitlabProvider.gitProvider.adminId !== ctx.user.adminId) {
//TODO: Remove this line when the cloud version is ready
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not allowed to access this gitlab provider",
});
}
if (input.name) {
await updateGitProvider(input.gitProviderId, {
name: input.name,
adminId: ctx.user.adminId,
});
} else {
await updateGitlab(input.gitlabId, {
...input,
});
}
}),
});

View File

@@ -0,0 +1,277 @@
import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc";
import {
apiChangeMariaDBStatus,
apiCreateMariaDB,
apiDeployMariaDB,
apiFindOneMariaDB,
apiResetMariadb,
apiSaveEnvironmentVariablesMariaDB,
apiSaveExternalPortMariaDB,
apiUpdateMariaDB,
} from "@/server/db/schema";
import {
IS_CLOUD,
addNewService,
checkServiceAccess,
createMariadb,
createMount,
deployMariadb,
findMariadbById,
findProjectById,
findServerById,
removeMariadbById,
removeService,
startService,
startServiceRemote,
stopService,
stopServiceRemote,
updateMariadbById,
} from "@dokploy/server";
import { TRPCError } from "@trpc/server";
export const mariadbRouter = createTRPCRouter({
create: protectedProcedure
.input(apiCreateMariaDB)
.mutation(async ({ input, ctx }) => {
try {
if (ctx.user.rol === "user") {
await checkServiceAccess(ctx.user.authId, input.projectId, "create");
}
if (IS_CLOUD && !input.serverId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You need to use a server to create a mariadb",
});
}
const project = await findProjectById(input.projectId);
if (project.adminId !== ctx.user.adminId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to access this project",
});
}
const newMariadb = await createMariadb(input);
if (ctx.user.rol === "user") {
await addNewService(ctx.user.authId, newMariadb.mariadbId);
}
await createMount({
serviceId: newMariadb.mariadbId,
serviceType: "mariadb",
volumeName: `${newMariadb.appName}-data`,
mountPath: "/var/lib/mysql",
type: "volume",
});
return true;
} catch (error) {
if (error instanceof TRPCError) {
throw error;
}
throw error;
}
}),
one: protectedProcedure
.input(apiFindOneMariaDB)
.query(async ({ input, ctx }) => {
if (ctx.user.rol === "user") {
await checkServiceAccess(ctx.user.authId, input.mariadbId, "access");
}
const mariadb = await findMariadbById(input.mariadbId);
if (mariadb.project.adminId !== ctx.user.adminId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to access this mariadb",
});
}
return mariadb;
}),
start: protectedProcedure
.input(apiFindOneMariaDB)
.mutation(async ({ input, ctx }) => {
const service = await findMariadbById(input.mariadbId);
if (service.project.adminId !== ctx.user.adminId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to start this mariadb",
});
}
if (service.serverId) {
await startServiceRemote(service.serverId, service.appName);
} else {
await startService(service.appName);
}
await updateMariadbById(input.mariadbId, {
applicationStatus: "done",
});
return service;
}),
stop: protectedProcedure
.input(apiFindOneMariaDB)
.mutation(async ({ input }) => {
const mariadb = await findMariadbById(input.mariadbId);
if (mariadb.serverId) {
await stopServiceRemote(mariadb.serverId, mariadb.appName);
} else {
await stopService(mariadb.appName);
}
await updateMariadbById(input.mariadbId, {
applicationStatus: "idle",
});
return mariadb;
}),
saveExternalPort: protectedProcedure
.input(apiSaveExternalPortMariaDB)
.mutation(async ({ input, ctx }) => {
const mongo = await findMariadbById(input.mariadbId);
if (mongo.project.adminId !== ctx.user.adminId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to save this external port",
});
}
await updateMariadbById(input.mariadbId, {
externalPort: input.externalPort,
});
await deployMariadb(input.mariadbId);
return mongo;
}),
deploy: protectedProcedure
.input(apiDeployMariaDB)
.mutation(async ({ input, ctx }) => {
const mariadb = await findMariadbById(input.mariadbId);
if (mariadb.project.adminId !== ctx.user.adminId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to deploy this mariadb",
});
}
return deployMariadb(input.mariadbId);
}),
changeStatus: protectedProcedure
.input(apiChangeMariaDBStatus)
.mutation(async ({ input, ctx }) => {
const mongo = await findMariadbById(input.mariadbId);
if (mongo.project.adminId !== ctx.user.adminId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to change this mariadb status",
});
}
await updateMariadbById(input.mariadbId, {
applicationStatus: input.applicationStatus,
});
return mongo;
}),
remove: protectedProcedure
.input(apiFindOneMariaDB)
.mutation(async ({ input, ctx }) => {
if (ctx.user.rol === "user") {
await checkServiceAccess(ctx.user.authId, input.mariadbId, "delete");
}
const mongo = await findMariadbById(input.mariadbId);
if (mongo.project.adminId !== ctx.user.adminId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to delete this mariadb",
});
}
const cleanupOperations = [
async () => await removeService(mongo?.appName, mongo.serverId),
async () => await removeMariadbById(input.mariadbId),
];
for (const operation of cleanupOperations) {
try {
await operation();
} catch (error) {}
}
return mongo;
}),
saveEnvironment: protectedProcedure
.input(apiSaveEnvironmentVariablesMariaDB)
.mutation(async ({ input, ctx }) => {
const mariadb = await findMariadbById(input.mariadbId);
if (mariadb.project.adminId !== ctx.user.adminId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to save this environment",
});
}
const service = await updateMariadbById(input.mariadbId, {
env: input.env,
});
if (!service) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Update: Error to add environment variables",
});
}
return true;
}),
reload: protectedProcedure
.input(apiResetMariadb)
.mutation(async ({ input, ctx }) => {
const mariadb = await findMariadbById(input.mariadbId);
if (mariadb.project.adminId !== ctx.user.adminId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to reload this mariadb",
});
}
if (mariadb.serverId) {
await stopServiceRemote(mariadb.serverId, mariadb.appName);
} else {
await stopService(mariadb.appName);
}
await updateMariadbById(input.mariadbId, {
applicationStatus: "idle",
});
if (mariadb.serverId) {
await startServiceRemote(mariadb.serverId, mariadb.appName);
} else {
await startService(mariadb.appName);
}
await updateMariadbById(input.mariadbId, {
applicationStatus: "done",
});
return true;
}),
update: protectedProcedure
.input(apiUpdateMariaDB)
.mutation(async ({ input, ctx }) => {
const { mariadbId, ...rest } = input;
const mariadb = await findMariadbById(mariadbId);
if (mariadb.project.adminId !== ctx.user.adminId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to update this mariadb",
});
}
const service = await updateMariadbById(mariadbId, {
...rest,
});
if (!service) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Update: Error to update mariadb",
});
}
return true;
}),
});

View File

@@ -0,0 +1,290 @@
import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc";
import {
apiChangeMongoStatus,
apiCreateMongo,
apiDeployMongo,
apiFindOneMongo,
apiResetMongo,
apiSaveEnvironmentVariablesMongo,
apiSaveExternalPortMongo,
apiUpdateMongo,
} from "@/server/db/schema";
import {
IS_CLOUD,
addNewService,
checkServiceAccess,
createMongo,
createMount,
deployMongo,
findMongoById,
findProjectById,
removeMongoById,
removeService,
startService,
startServiceRemote,
stopService,
stopServiceRemote,
updateMongoById,
} from "@dokploy/server";
import { TRPCError } from "@trpc/server";
export const mongoRouter = createTRPCRouter({
create: protectedProcedure
.input(apiCreateMongo)
.mutation(async ({ input, ctx }) => {
try {
if (ctx.user.rol === "user") {
await checkServiceAccess(ctx.user.authId, input.projectId, "create");
}
if (IS_CLOUD && !input.serverId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You need to use a server to create a mongo",
});
}
const project = await findProjectById(input.projectId);
if (project.adminId !== ctx.user.adminId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to access this project",
});
}
const newMongo = await createMongo(input);
if (ctx.user.rol === "user") {
await addNewService(ctx.user.authId, newMongo.mongoId);
}
await createMount({
serviceId: newMongo.mongoId,
serviceType: "mongo",
volumeName: `${newMongo.appName}-data`,
mountPath: "/data/db",
type: "volume",
});
return true;
} catch (error) {
if (error instanceof TRPCError) {
throw error;
}
throw new TRPCError({
code: "BAD_REQUEST",
message: "Error input: Inserting mongo database",
cause: error,
});
}
}),
one: protectedProcedure
.input(apiFindOneMongo)
.query(async ({ input, ctx }) => {
if (ctx.user.rol === "user") {
await checkServiceAccess(ctx.user.authId, input.mongoId, "access");
}
const mongo = await findMongoById(input.mongoId);
if (mongo.project.adminId !== ctx.user.adminId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to access this mongo",
});
}
return mongo;
}),
start: protectedProcedure
.input(apiFindOneMongo)
.mutation(async ({ input, ctx }) => {
const service = await findMongoById(input.mongoId);
if (service.project.adminId !== ctx.user.adminId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to start this mongo",
});
}
if (service.serverId) {
await startServiceRemote(service.serverId, service.appName);
} else {
await startService(service.appName);
}
await updateMongoById(input.mongoId, {
applicationStatus: "done",
});
return service;
}),
stop: protectedProcedure
.input(apiFindOneMongo)
.mutation(async ({ input, ctx }) => {
const mongo = await findMongoById(input.mongoId);
if (mongo.project.adminId !== ctx.user.adminId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to stop this mongo",
});
}
if (mongo.serverId) {
await stopServiceRemote(mongo.serverId, mongo.appName);
} else {
await stopService(mongo.appName);
}
await updateMongoById(input.mongoId, {
applicationStatus: "idle",
});
return mongo;
}),
saveExternalPort: protectedProcedure
.input(apiSaveExternalPortMongo)
.mutation(async ({ input, ctx }) => {
const mongo = await findMongoById(input.mongoId);
if (mongo.project.adminId !== ctx.user.adminId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to save this external port",
});
}
await updateMongoById(input.mongoId, {
externalPort: input.externalPort,
});
await deployMongo(input.mongoId);
return mongo;
}),
deploy: protectedProcedure
.input(apiDeployMongo)
.mutation(async ({ input, ctx }) => {
const mongo = await findMongoById(input.mongoId);
if (mongo.project.adminId !== ctx.user.adminId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to deploy this mongo",
});
}
return deployMongo(input.mongoId);
}),
changeStatus: protectedProcedure
.input(apiChangeMongoStatus)
.mutation(async ({ input, ctx }) => {
const mongo = await findMongoById(input.mongoId);
if (mongo.project.adminId !== ctx.user.adminId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to change this mongo status",
});
}
await updateMongoById(input.mongoId, {
applicationStatus: input.applicationStatus,
});
return mongo;
}),
reload: protectedProcedure
.input(apiResetMongo)
.mutation(async ({ input, ctx }) => {
const mongo = await findMongoById(input.mongoId);
if (mongo.project.adminId !== ctx.user.adminId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to reload this mongo",
});
}
if (mongo.serverId) {
await stopServiceRemote(mongo.serverId, mongo.appName);
} else {
await stopService(mongo.appName);
}
await updateMongoById(input.mongoId, {
applicationStatus: "idle",
});
if (mongo.serverId) {
await startServiceRemote(mongo.serverId, mongo.appName);
} else {
await startService(mongo.appName);
}
await updateMongoById(input.mongoId, {
applicationStatus: "done",
});
return true;
}),
remove: protectedProcedure
.input(apiFindOneMongo)
.mutation(async ({ input, ctx }) => {
if (ctx.user.rol === "user") {
await checkServiceAccess(ctx.user.authId, input.mongoId, "delete");
}
const mongo = await findMongoById(input.mongoId);
if (mongo.project.adminId !== ctx.user.adminId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to delete this mongo",
});
}
const cleanupOperations = [
async () => await removeService(mongo?.appName, mongo.serverId),
async () => await removeMongoById(input.mongoId),
];
for (const operation of cleanupOperations) {
try {
await operation();
} catch (error) {}
}
return mongo;
}),
saveEnvironment: protectedProcedure
.input(apiSaveEnvironmentVariablesMongo)
.mutation(async ({ input, ctx }) => {
const mongo = await findMongoById(input.mongoId);
if (mongo.project.adminId !== ctx.user.adminId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to save this environment",
});
}
const service = await updateMongoById(input.mongoId, {
env: input.env,
});
if (!service) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Update: Error to add environment variables",
});
}
return true;
}),
update: protectedProcedure
.input(apiUpdateMongo)
.mutation(async ({ input, ctx }) => {
const { mongoId, ...rest } = input;
const mongo = await findMongoById(mongoId);
if (mongo.project.adminId !== ctx.user.adminId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to update this mongo",
});
}
const service = await updateMongoById(mongoId, {
...rest,
});
if (!service) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Update: Error to update mongo",
});
}
return true;
}),
});

View File

@@ -0,0 +1,37 @@
import {
apiCreateMount,
apiFindOneMount,
apiRemoveMount,
apiUpdateMount,
} from "@/server/db/schema";
import {
createMount,
deleteMount,
findMountById,
updateMount,
} from "@dokploy/server";
import { createTRPCRouter, protectedProcedure } from "../trpc";
export const mountRouter = createTRPCRouter({
create: protectedProcedure
.input(apiCreateMount)
.mutation(async ({ input }) => {
await createMount(input);
return true;
}),
remove: protectedProcedure
.input(apiRemoveMount)
.mutation(async ({ input }) => {
return await deleteMount(input.mountId);
}),
one: protectedProcedure.input(apiFindOneMount).query(async ({ input }) => {
return await findMountById(input.mountId);
}),
update: protectedProcedure
.input(apiUpdateMount)
.mutation(async ({ input }) => {
await updateMount(input.mountId, input);
return true;
}),
});

View File

@@ -0,0 +1,286 @@
import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc";
import {
apiChangeMySqlStatus,
apiCreateMySql,
apiDeployMySql,
apiFindOneMySql,
apiResetMysql,
apiSaveEnvironmentVariablesMySql,
apiSaveExternalPortMySql,
apiUpdateMySql,
} from "@/server/db/schema";
import { TRPCError } from "@trpc/server";
import {
IS_CLOUD,
addNewService,
checkServiceAccess,
createMount,
createMysql,
deployMySql,
findMySqlById,
findProjectById,
removeMySqlById,
removeService,
startService,
startServiceRemote,
stopService,
stopServiceRemote,
updateMySqlById,
} from "@dokploy/server";
export const mysqlRouter = createTRPCRouter({
create: protectedProcedure
.input(apiCreateMySql)
.mutation(async ({ input, ctx }) => {
try {
if (ctx.user.rol === "user") {
await checkServiceAccess(ctx.user.authId, input.projectId, "create");
}
if (IS_CLOUD && !input.serverId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You need to use a server to create a mysql",
});
}
1;
const project = await findProjectById(input.projectId);
if (project.adminId !== ctx.user.adminId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to access this project",
});
}
const newMysql = await createMysql(input);
if (ctx.user.rol === "user") {
await addNewService(ctx.user.authId, newMysql.mysqlId);
}
await createMount({
serviceId: newMysql.mysqlId,
serviceType: "mysql",
volumeName: `${newMysql.appName}-data`,
mountPath: "/var/lib/mysql",
type: "volume",
});
return true;
} catch (error) {
if (error instanceof TRPCError) {
throw error;
}
throw new TRPCError({
code: "BAD_REQUEST",
message: "Error input: Inserting mysql database",
cause: error,
});
}
}),
one: protectedProcedure
.input(apiFindOneMySql)
.query(async ({ input, ctx }) => {
if (ctx.user.rol === "user") {
await checkServiceAccess(ctx.user.authId, input.mysqlId, "access");
}
const mysql = await findMySqlById(input.mysqlId);
if (mysql.project.adminId !== ctx.user.adminId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to access this mysql",
});
}
return mysql;
}),
start: protectedProcedure
.input(apiFindOneMySql)
.mutation(async ({ input, ctx }) => {
const service = await findMySqlById(input.mysqlId);
if (service.project.adminId !== ctx.user.adminId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to start this mysql",
});
}
if (service.serverId) {
await startServiceRemote(service.serverId, service.appName);
} else {
await startService(service.appName);
}
await updateMySqlById(input.mysqlId, {
applicationStatus: "done",
});
return service;
}),
stop: protectedProcedure
.input(apiFindOneMySql)
.mutation(async ({ input, ctx }) => {
const mongo = await findMySqlById(input.mysqlId);
if (mongo.project.adminId !== ctx.user.adminId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to stop this mysql",
});
}
if (mongo.serverId) {
await stopServiceRemote(mongo.serverId, mongo.appName);
} else {
await stopService(mongo.appName);
}
await updateMySqlById(input.mysqlId, {
applicationStatus: "idle",
});
return mongo;
}),
saveExternalPort: protectedProcedure
.input(apiSaveExternalPortMySql)
.mutation(async ({ input, ctx }) => {
const mongo = await findMySqlById(input.mysqlId);
if (mongo.project.adminId !== ctx.user.adminId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to save this external port",
});
}
await updateMySqlById(input.mysqlId, {
externalPort: input.externalPort,
});
await deployMySql(input.mysqlId);
return mongo;
}),
deploy: protectedProcedure
.input(apiDeployMySql)
.mutation(async ({ input, ctx }) => {
const mysql = await findMySqlById(input.mysqlId);
if (mysql.project.adminId !== ctx.user.adminId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to deploy this mysql",
});
}
return deployMySql(input.mysqlId);
}),
changeStatus: protectedProcedure
.input(apiChangeMySqlStatus)
.mutation(async ({ input, ctx }) => {
const mongo = await findMySqlById(input.mysqlId);
if (mongo.project.adminId !== ctx.user.adminId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to change this mysql status",
});
}
await updateMySqlById(input.mysqlId, {
applicationStatus: input.applicationStatus,
});
return mongo;
}),
reload: protectedProcedure
.input(apiResetMysql)
.mutation(async ({ input, ctx }) => {
const mysql = await findMySqlById(input.mysqlId);
if (mysql.project.adminId !== ctx.user.adminId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to reload this mysql",
});
}
if (mysql.serverId) {
await stopServiceRemote(mysql.serverId, mysql.appName);
} else {
await stopService(mysql.appName);
}
await updateMySqlById(input.mysqlId, {
applicationStatus: "idle",
});
if (mysql.serverId) {
await startServiceRemote(mysql.serverId, mysql.appName);
} else {
await startService(mysql.appName);
}
await updateMySqlById(input.mysqlId, {
applicationStatus: "done",
});
return true;
}),
remove: protectedProcedure
.input(apiFindOneMySql)
.mutation(async ({ input, ctx }) => {
if (ctx.user.rol === "user") {
await checkServiceAccess(ctx.user.authId, input.mysqlId, "delete");
}
const mongo = await findMySqlById(input.mysqlId);
if (mongo.project.adminId !== ctx.user.adminId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to delete this mysql",
});
}
const cleanupOperations = [
async () => await removeService(mongo?.appName, mongo.serverId),
async () => await removeMySqlById(input.mysqlId),
];
for (const operation of cleanupOperations) {
try {
await operation();
} catch (error) {}
}
return mongo;
}),
saveEnvironment: protectedProcedure
.input(apiSaveEnvironmentVariablesMySql)
.mutation(async ({ input, ctx }) => {
const mysql = await findMySqlById(input.mysqlId);
if (mysql.project.adminId !== ctx.user.adminId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to save this environment",
});
}
const service = await updateMySqlById(input.mysqlId, {
env: input.env,
});
if (!service) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Update: Error to add environment variables",
});
}
return true;
}),
update: protectedProcedure
.input(apiUpdateMySql)
.mutation(async ({ input, ctx }) => {
const { mysqlId, ...rest } = input;
const mysql = await findMySqlById(mysqlId);
if (mysql.project.adminId !== ctx.user.adminId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to update this mysql",
});
}
const service = await updateMySqlById(mysqlId, {
...rest,
});
if (!service) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Update: Error to update mysql",
});
}
return true;
}),
});

View File

@@ -0,0 +1,304 @@
import {
adminProcedure,
createTRPCRouter,
protectedProcedure,
} from "@/server/api/trpc";
import { db } from "@/server/db";
import {
apiCreateDiscord,
apiCreateEmail,
apiCreateSlack,
apiCreateTelegram,
apiFindOneNotification,
apiTestDiscordConnection,
apiTestEmailConnection,
apiTestSlackConnection,
apiTestTelegramConnection,
apiUpdateDiscord,
apiUpdateEmail,
apiUpdateSlack,
apiUpdateTelegram,
notifications,
} from "@/server/db/schema";
import {
IS_CLOUD,
createDiscordNotification,
createEmailNotification,
createSlackNotification,
createTelegramNotification,
findNotificationById,
removeNotificationById,
sendDiscordNotification,
sendEmailNotification,
sendSlackNotification,
sendTelegramNotification,
updateDiscordNotification,
updateEmailNotification,
updateSlackNotification,
updateTelegramNotification,
} from "@dokploy/server";
import { TRPCError } from "@trpc/server";
import { desc, eq } from "drizzle-orm";
// TODO: Uncomment the validations when is cloud ready
export const notificationRouter = createTRPCRouter({
createSlack: adminProcedure
.input(apiCreateSlack)
.mutation(async ({ input, ctx }) => {
try {
return await createSlackNotification(input, ctx.user.adminId);
} catch (error) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Error to create the notification",
cause: error,
});
}
}),
updateSlack: adminProcedure
.input(apiUpdateSlack)
.mutation(async ({ input, ctx }) => {
try {
const notification = await findNotificationById(input.notificationId);
if (IS_CLOUD && notification.adminId !== ctx.user.adminId) {
// TODO: Remove isCloud in the next versions of dokploy
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to update this notification",
});
}
return await updateSlackNotification({
...input,
adminId: ctx.user.adminId,
});
} catch (error) {
throw error;
}
}),
testSlackConnection: adminProcedure
.input(apiTestSlackConnection)
.mutation(async ({ input }) => {
try {
await sendSlackNotification(input, {
channel: input.channel,
text: "Hi, From Dokploy 👋",
});
return true;
} catch (error) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Error to test the notification",
cause: error,
});
}
}),
createTelegram: adminProcedure
.input(apiCreateTelegram)
.mutation(async ({ input, ctx }) => {
try {
return await createTelegramNotification(input, ctx.user.adminId);
} catch (error) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Error to create the notification",
cause: error,
});
}
}),
updateTelegram: adminProcedure
.input(apiUpdateTelegram)
.mutation(async ({ input, ctx }) => {
try {
const notification = await findNotificationById(input.notificationId);
if (IS_CLOUD && notification.adminId !== ctx.user.adminId) {
// TODO: Remove isCloud in the next versions of dokploy
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to update this notification",
});
}
return await updateTelegramNotification({
...input,
adminId: ctx.user.adminId,
});
} catch (error) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Error to update the notification",
cause: error,
});
}
}),
testTelegramConnection: adminProcedure
.input(apiTestTelegramConnection)
.mutation(async ({ input }) => {
try {
await sendTelegramNotification(input, "Hi, From Dokploy 👋");
return true;
} catch (error) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Error to test the notification",
cause: error,
});
}
}),
createDiscord: adminProcedure
.input(apiCreateDiscord)
.mutation(async ({ input, ctx }) => {
try {
return await createDiscordNotification(input, ctx.user.adminId);
} catch (error) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Error to create the notification",
cause: error,
});
}
}),
updateDiscord: adminProcedure
.input(apiUpdateDiscord)
.mutation(async ({ input, ctx }) => {
try {
const notification = await findNotificationById(input.notificationId);
if (IS_CLOUD && notification.adminId !== ctx.user.adminId) {
// TODO: Remove isCloud in the next versions of dokploy
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to update this notification",
});
}
return await updateDiscordNotification({
...input,
adminId: ctx.user.adminId,
});
} catch (error) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Error to update the notification",
cause: error,
});
}
}),
testDiscordConnection: adminProcedure
.input(apiTestDiscordConnection)
.mutation(async ({ input }) => {
try {
await sendDiscordNotification(input, {
title: "Test Notification",
description: "Hi, From Dokploy 👋",
});
return true;
} catch (error) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Error to test the notification",
cause: error,
});
}
}),
createEmail: adminProcedure
.input(apiCreateEmail)
.mutation(async ({ input, ctx }) => {
try {
return await createEmailNotification(input, ctx.user.adminId);
} catch (error) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Error to create the notification",
cause: error,
});
}
}),
updateEmail: adminProcedure
.input(apiUpdateEmail)
.mutation(async ({ input, ctx }) => {
try {
const notification = await findNotificationById(input.notificationId);
if (IS_CLOUD && notification.adminId !== ctx.user.adminId) {
// TODO: Remove isCloud in the next versions of dokploy
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to update this notification",
});
}
return await updateEmailNotification({
...input,
adminId: ctx.user.adminId,
});
} catch (error) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Error to update the notification",
cause: error,
});
}
}),
testEmailConnection: adminProcedure
.input(apiTestEmailConnection)
.mutation(async ({ input }) => {
try {
await sendEmailNotification(
input,
"Test Email",
"<p>Hi, From Dokploy 👋</p>",
);
return true;
} catch (error) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Error to test the notification",
cause: error,
});
}
}),
remove: adminProcedure
.input(apiFindOneNotification)
.mutation(async ({ input, ctx }) => {
try {
const notification = await findNotificationById(input.notificationId);
if (IS_CLOUD && notification.adminId !== ctx.user.adminId) {
// TODO: Remove isCloud in the next versions of dokploy
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to delete this notification",
});
}
return await removeNotificationById(input.notificationId);
} catch (error) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Error to delete this notification",
});
}
}),
one: protectedProcedure
.input(apiFindOneNotification)
.query(async ({ input, ctx }) => {
const notification = await findNotificationById(input.notificationId);
if (IS_CLOUD && notification.adminId !== ctx.user.adminId) {
// TODO: Remove isCloud in the next versions of dokploy
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to access this notification",
});
}
return notification;
}),
all: adminProcedure.query(async ({ ctx }) => {
return await db.query.notifications.findMany({
with: {
slack: true,
telegram: true,
discord: true,
email: true,
},
orderBy: desc(notifications.createdAt),
...(IS_CLOUD && { where: eq(notifications.adminId, ctx.user.adminId) }),
// TODO: Remove this line when the cloud version is ready
});
}),
});

View File

@@ -0,0 +1,65 @@
import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc";
import {
apiCreatePort,
apiFindOnePort,
apiUpdatePort,
} from "@/server/db/schema";
import {
createPort,
finPortById,
removePortById,
updatePortById,
} from "@dokploy/server";
import { TRPCError } from "@trpc/server";
export const portRouter = createTRPCRouter({
create: protectedProcedure
.input(apiCreatePort)
.mutation(async ({ input }) => {
try {
await createPort(input);
return true;
} catch (error) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Error input: Inserting port",
cause: error,
});
}
}),
one: protectedProcedure.input(apiFindOnePort).query(async ({ input }) => {
try {
return await finPortById(input.portId);
} catch (error) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Port not found",
cause: error,
});
}
}),
delete: protectedProcedure
.input(apiFindOnePort)
.mutation(async ({ input }) => {
try {
return removePortById(input.portId);
} catch (error) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Error input: Deleting port",
});
}
}),
update: protectedProcedure
.input(apiUpdatePort)
.mutation(async ({ input }) => {
try {
return updatePortById(input.portId, input);
} catch (error) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Error to updating port",
});
}
}),
});

View File

@@ -0,0 +1,284 @@
import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc";
import {
apiChangePostgresStatus,
apiCreatePostgres,
apiDeployPostgres,
apiFindOnePostgres,
apiResetPostgres,
apiSaveEnvironmentVariablesPostgres,
apiSaveExternalPortPostgres,
apiUpdatePostgres,
} from "@/server/db/schema";
import {
IS_CLOUD,
addNewService,
checkServiceAccess,
createMount,
createPostgres,
deployPostgres,
findPostgresById,
findProjectById,
removePostgresById,
removeService,
startService,
startServiceRemote,
stopService,
stopServiceRemote,
updatePostgresById,
} from "@dokploy/server";
import { TRPCError } from "@trpc/server";
export const postgresRouter = createTRPCRouter({
create: protectedProcedure
.input(apiCreatePostgres)
.mutation(async ({ input, ctx }) => {
try {
if (ctx.user.rol === "user") {
await checkServiceAccess(ctx.user.authId, input.projectId, "create");
}
if (IS_CLOUD && !input.serverId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You need to use a server to create a postgres",
});
}
const project = await findProjectById(input.projectId);
if (project.adminId !== ctx.user.adminId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to access this project",
});
}
const newPostgres = await createPostgres(input);
if (ctx.user.rol === "user") {
await addNewService(ctx.user.authId, newPostgres.postgresId);
}
await createMount({
serviceId: newPostgres.postgresId,
serviceType: "postgres",
volumeName: `${newPostgres.appName}-data`,
mountPath: "/var/lib/postgresql/data",
type: "volume",
});
return true;
} catch (error) {
if (error instanceof TRPCError) {
throw error;
}
throw new TRPCError({
code: "BAD_REQUEST",
message: "Error input: Inserting postgresql database",
cause: error,
});
}
}),
one: protectedProcedure
.input(apiFindOnePostgres)
.query(async ({ input, ctx }) => {
if (ctx.user.rol === "user") {
await checkServiceAccess(ctx.user.authId, input.postgresId, "access");
}
const postgres = await findPostgresById(input.postgresId);
if (postgres.project.adminId !== ctx.user.adminId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to access this postgres",
});
}
return postgres;
}),
start: protectedProcedure
.input(apiFindOnePostgres)
.mutation(async ({ input, ctx }) => {
const service = await findPostgresById(input.postgresId);
if (service.project.adminId !== ctx.user.adminId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to start this postgres",
});
}
if (service.serverId) {
await startServiceRemote(service.serverId, service.appName);
} else {
await startService(service.appName);
}
await updatePostgresById(input.postgresId, {
applicationStatus: "done",
});
return service;
}),
stop: protectedProcedure
.input(apiFindOnePostgres)
.mutation(async ({ input, ctx }) => {
const postgres = await findPostgresById(input.postgresId);
if (postgres.project.adminId !== ctx.user.adminId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to stop this postgres",
});
}
if (postgres.serverId) {
await stopServiceRemote(postgres.serverId, postgres.appName);
} else {
await stopService(postgres.appName);
}
await updatePostgresById(input.postgresId, {
applicationStatus: "idle",
});
return postgres;
}),
saveExternalPort: protectedProcedure
.input(apiSaveExternalPortPostgres)
.mutation(async ({ input, ctx }) => {
const postgres = await findPostgresById(input.postgresId);
if (postgres.project.adminId !== ctx.user.adminId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to save this external port",
});
}
await updatePostgresById(input.postgresId, {
externalPort: input.externalPort,
});
await deployPostgres(input.postgresId);
return postgres;
}),
deploy: protectedProcedure
.input(apiDeployPostgres)
.mutation(async ({ input, ctx }) => {
const postgres = await findPostgresById(input.postgresId);
if (postgres.project.adminId !== ctx.user.adminId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to deploy this postgres",
});
}
return deployPostgres(input.postgresId);
}),
changeStatus: protectedProcedure
.input(apiChangePostgresStatus)
.mutation(async ({ input, ctx }) => {
const postgres = await findPostgresById(input.postgresId);
if (postgres.project.adminId !== ctx.user.adminId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to change this postgres status",
});
}
await updatePostgresById(input.postgresId, {
applicationStatus: input.applicationStatus,
});
return postgres;
}),
remove: protectedProcedure
.input(apiFindOnePostgres)
.mutation(async ({ input, ctx }) => {
if (ctx.user.rol === "user") {
await checkServiceAccess(ctx.user.authId, input.postgresId, "delete");
}
const postgres = await findPostgresById(input.postgresId);
if (postgres.project.adminId !== ctx.user.adminId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to delete this postgres",
});
}
const cleanupOperations = [
removeService(postgres.appName, postgres.serverId),
removePostgresById(input.postgresId),
];
await Promise.allSettled(cleanupOperations);
return postgres;
}),
saveEnvironment: protectedProcedure
.input(apiSaveEnvironmentVariablesPostgres)
.mutation(async ({ input, ctx }) => {
const postgres = await findPostgresById(input.postgresId);
if (postgres.project.adminId !== ctx.user.adminId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to save this environment",
});
}
const service = await updatePostgresById(input.postgresId, {
env: input.env,
});
if (!service) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Update: Error to add environment variables",
});
}
return true;
}),
reload: protectedProcedure
.input(apiResetPostgres)
.mutation(async ({ input, ctx }) => {
const postgres = await findPostgresById(input.postgresId);
if (postgres.project.adminId !== ctx.user.adminId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to reload this postgres",
});
}
if (postgres.serverId) {
await stopServiceRemote(postgres.serverId, postgres.appName);
} else {
await stopService(postgres.appName);
}
await updatePostgresById(input.postgresId, {
applicationStatus: "idle",
});
if (postgres.serverId) {
await startServiceRemote(postgres.serverId, postgres.appName);
} else {
await startService(postgres.appName);
}
await updatePostgresById(input.postgresId, {
applicationStatus: "done",
});
return true;
}),
update: protectedProcedure
.input(apiUpdatePostgres)
.mutation(async ({ input, ctx }) => {
const { postgresId, ...rest } = input;
const postgres = await findPostgresById(postgresId);
if (postgres.project.adminId !== ctx.user.adminId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to update this postgres",
});
}
const service = await updatePostgresById(postgresId, {
...rest,
});
if (!service) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Update: Error to update postgres",
});
}
return true;
}),
});

View File

@@ -0,0 +1,234 @@
import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc";
import { db } from "@/server/db";
import {
apiCreateProject,
apiFindOneProject,
apiRemoveProject,
apiUpdateProject,
applications,
compose,
mariadb,
mongo,
mysql,
postgres,
projects,
redis,
} from "@/server/db/schema";
import { TRPCError } from "@trpc/server";
import { and, desc, eq, sql } from "drizzle-orm";
import type { AnyPgColumn } from "drizzle-orm/pg-core";
import {
addNewProject,
checkProjectAccess,
createProject,
deleteProject,
findProjectById,
findUserByAuthId,
updateProjectById,
} from "@dokploy/server";
export const projectRouter = createTRPCRouter({
create: protectedProcedure
.input(apiCreateProject)
.mutation(async ({ ctx, input }) => {
try {
if (ctx.user.rol === "user") {
await checkProjectAccess(ctx.user.authId, "create");
}
const project = await createProject(input, ctx.user.adminId);
if (ctx.user.rol === "user") {
await addNewProject(ctx.user.authId, project.projectId);
}
return project;
} catch (error) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Error to create the project",
cause: error,
});
}
}),
one: protectedProcedure
.input(apiFindOneProject)
.query(async ({ input, ctx }) => {
if (ctx.user.rol === "user") {
const { accesedServices } = await findUserByAuthId(ctx.user.authId);
await checkProjectAccess(ctx.user.authId, "access", input.projectId);
const project = await db.query.projects.findFirst({
where: and(
eq(projects.projectId, input.projectId),
eq(projects.adminId, ctx.user.adminId),
),
with: {
compose: {
where: buildServiceFilter(compose.composeId, accesedServices),
},
applications: {
where: buildServiceFilter(
applications.applicationId,
accesedServices,
),
},
mariadb: {
where: buildServiceFilter(mariadb.mariadbId, accesedServices),
},
mongo: {
where: buildServiceFilter(mongo.mongoId, accesedServices),
},
mysql: {
where: buildServiceFilter(mysql.mysqlId, accesedServices),
},
postgres: {
where: buildServiceFilter(postgres.postgresId, accesedServices),
},
redis: {
where: buildServiceFilter(redis.redisId, accesedServices),
},
},
});
if (!project) {
throw new TRPCError({
code: "NOT_FOUND",
message: "Project not found",
});
}
return project;
}
const project = await findProjectById(input.projectId);
if (project.adminId !== ctx.user.adminId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to access this project",
});
}
return project;
}),
all: protectedProcedure.query(async ({ ctx }) => {
if (ctx.user.rol === "user") {
const { accesedProjects, accesedServices } = await findUserByAuthId(
ctx.user.authId,
);
if (accesedProjects.length === 0) {
return [];
}
const query = await db.query.projects.findMany({
where: sql`${projects.projectId} IN (${sql.join(
accesedProjects.map((projectId) => sql`${projectId}`),
sql`, `,
)})`,
with: {
applications: {
where: buildServiceFilter(
applications.applicationId,
accesedServices,
),
with: { domains: true },
},
mariadb: {
where: buildServiceFilter(mariadb.mariadbId, accesedServices),
},
mongo: {
where: buildServiceFilter(mongo.mongoId, accesedServices),
},
mysql: {
where: buildServiceFilter(mysql.mysqlId, accesedServices),
},
postgres: {
where: buildServiceFilter(postgres.postgresId, accesedServices),
},
redis: {
where: buildServiceFilter(redis.redisId, accesedServices),
},
compose: {
where: buildServiceFilter(compose.composeId, accesedServices),
with: { domains: true },
},
},
orderBy: desc(projects.createdAt),
});
return query;
}
return await db.query.projects.findMany({
with: {
applications: {
with: {
domains: true,
},
},
mariadb: true,
mongo: true,
mysql: true,
postgres: true,
redis: true,
compose: {
with: {
domains: true,
},
},
},
where: eq(projects.adminId, ctx.user.adminId),
orderBy: desc(projects.createdAt),
});
}),
remove: protectedProcedure
.input(apiRemoveProject)
.mutation(async ({ input, ctx }) => {
try {
if (ctx.user.rol === "user") {
await checkProjectAccess(ctx.user.authId, "delete");
}
const currentProject = await findProjectById(input.projectId);
if (currentProject.adminId !== ctx.user.adminId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to delete this project",
});
}
const deletedProject = await deleteProject(input.projectId);
return deletedProject;
} catch (error) {
throw error;
}
}),
update: protectedProcedure
.input(apiUpdateProject)
.mutation(async ({ input, ctx }) => {
try {
const currentProject = await findProjectById(input.projectId);
if (currentProject.adminId !== ctx.user.adminId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to update this project",
});
}
const project = await updateProjectById(input.projectId, {
...input,
});
return project;
} catch (error) {
throw error;
}
}),
});
function buildServiceFilter(fieldName: AnyPgColumn, accesedServices: string[]) {
return accesedServices.length > 0
? sql`${fieldName} IN (${sql.join(
accesedServices.map((serviceId) => sql`${serviceId}`),
sql`, `,
)})`
: sql`1 = 0`; // Always false condition
}

View File

@@ -0,0 +1,68 @@
import {
apiCreateRedirect,
apiFindOneRedirect,
apiUpdateRedirect,
} from "@/server/db/schema";
import {
createRedirect,
findApplicationById,
findRedirectById,
removeRedirectById,
updateRedirectById,
} from "@dokploy/server";
import { TRPCError } from "@trpc/server";
import { createTRPCRouter, protectedProcedure } from "../trpc";
export const redirectsRouter = createTRPCRouter({
create: protectedProcedure
.input(apiCreateRedirect)
.mutation(async ({ input, ctx }) => {
const application = await findApplicationById(input.applicationId);
if (application.project.adminId !== ctx.user.adminId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to access this application",
});
}
return await createRedirect(input);
}),
one: protectedProcedure
.input(apiFindOneRedirect)
.query(async ({ input, ctx }) => {
const redirect = await findRedirectById(input.redirectId);
const application = await findApplicationById(redirect.applicationId);
if (application.project.adminId !== ctx.user.adminId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to access this application",
});
}
return findRedirectById(input.redirectId);
}),
delete: protectedProcedure
.input(apiFindOneRedirect)
.mutation(async ({ input, ctx }) => {
const redirect = await findRedirectById(input.redirectId);
const application = await findApplicationById(redirect.applicationId);
if (application.project.adminId !== ctx.user.adminId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to access this application",
});
}
return removeRedirectById(input.redirectId);
}),
update: protectedProcedure
.input(apiUpdateRedirect)
.mutation(async ({ input, ctx }) => {
const redirect = await findRedirectById(input.redirectId);
const application = await findApplicationById(redirect.applicationId);
if (application.project.adminId !== ctx.user.adminId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to access this application",
});
}
return updateRedirectById(input.redirectId, input);
}),
});

View File

@@ -0,0 +1,276 @@
import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc";
import {
apiChangeRedisStatus,
apiCreateRedis,
apiDeployRedis,
apiFindOneRedis,
apiResetRedis,
apiSaveEnvironmentVariablesRedis,
apiSaveExternalPortRedis,
apiUpdateRedis,
} from "@/server/db/schema";
import { TRPCError } from "@trpc/server";
import {
IS_CLOUD,
addNewService,
checkServiceAccess,
createMount,
createRedis,
deployRedis,
findProjectById,
findRedisById,
removeRedisById,
removeService,
startService,
startServiceRemote,
stopService,
stopServiceRemote,
updateRedisById,
} from "@dokploy/server";
export const redisRouter = createTRPCRouter({
create: protectedProcedure
.input(apiCreateRedis)
.mutation(async ({ input, ctx }) => {
try {
if (ctx.user.rol === "user") {
await checkServiceAccess(ctx.user.authId, input.projectId, "create");
}
if (IS_CLOUD && !input.serverId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You need to use a server to create a redis",
});
}
const project = await findProjectById(input.projectId);
if (project.adminId !== ctx.user.adminId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to access this project",
});
}
const newRedis = await createRedis(input);
if (ctx.user.rol === "user") {
await addNewService(ctx.user.authId, newRedis.redisId);
}
await createMount({
serviceId: newRedis.redisId,
serviceType: "redis",
volumeName: `${newRedis.appName}-data`,
mountPath: "/data",
type: "volume",
});
return true;
} catch (error) {
throw error;
}
}),
one: protectedProcedure
.input(apiFindOneRedis)
.query(async ({ input, ctx }) => {
if (ctx.user.rol === "user") {
await checkServiceAccess(ctx.user.authId, input.redisId, "access");
}
const redis = await findRedisById(input.redisId);
if (redis.project.adminId !== ctx.user.adminId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to access this redis",
});
}
return redis;
}),
start: protectedProcedure
.input(apiFindOneRedis)
.mutation(async ({ input, ctx }) => {
const redis = await findRedisById(input.redisId);
if (redis.project.adminId !== ctx.user.adminId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to start this redis",
});
}
if (redis.serverId) {
await startServiceRemote(redis.serverId, redis.appName);
} else {
await startService(redis.appName);
}
await updateRedisById(input.redisId, {
applicationStatus: "done",
});
return redis;
}),
reload: protectedProcedure
.input(apiResetRedis)
.mutation(async ({ input, ctx }) => {
const redis = await findRedisById(input.redisId);
if (redis.project.adminId !== ctx.user.adminId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to reload this redis",
});
}
if (redis.serverId) {
await stopServiceRemote(redis.serverId, redis.appName);
} else {
await stopService(redis.appName);
}
await updateRedisById(input.redisId, {
applicationStatus: "idle",
});
if (redis.serverId) {
await startServiceRemote(redis.serverId, redis.appName);
} else {
await startService(redis.appName);
}
await updateRedisById(input.redisId, {
applicationStatus: "done",
});
return true;
}),
stop: protectedProcedure
.input(apiFindOneRedis)
.mutation(async ({ input, ctx }) => {
const redis = await findRedisById(input.redisId);
if (redis.project.adminId !== ctx.user.adminId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to stop this redis",
});
}
if (redis.serverId) {
await stopServiceRemote(redis.serverId, redis.appName);
} else {
await stopService(redis.appName);
}
await updateRedisById(input.redisId, {
applicationStatus: "idle",
});
return redis;
}),
saveExternalPort: protectedProcedure
.input(apiSaveExternalPortRedis)
.mutation(async ({ input, ctx }) => {
const mongo = await findRedisById(input.redisId);
if (mongo.project.adminId !== ctx.user.adminId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to save this external port",
});
}
await updateRedisById(input.redisId, {
externalPort: input.externalPort,
});
await deployRedis(input.redisId);
return mongo;
}),
deploy: protectedProcedure
.input(apiDeployRedis)
.mutation(async ({ input, ctx }) => {
const redis = await findRedisById(input.redisId);
if (redis.project.adminId !== ctx.user.adminId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to deploy this redis",
});
}
return deployRedis(input.redisId);
}),
changeStatus: protectedProcedure
.input(apiChangeRedisStatus)
.mutation(async ({ input, ctx }) => {
const mongo = await findRedisById(input.redisId);
if (mongo.project.adminId !== ctx.user.adminId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to change this redis status",
});
}
await updateRedisById(input.redisId, {
applicationStatus: input.applicationStatus,
});
return mongo;
}),
remove: protectedProcedure
.input(apiFindOneRedis)
.mutation(async ({ input, ctx }) => {
if (ctx.user.rol === "user") {
await checkServiceAccess(ctx.user.authId, input.redisId, "delete");
}
const redis = await findRedisById(input.redisId);
if (redis.project.adminId !== ctx.user.adminId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to delete this redis",
});
}
const cleanupOperations = [
async () => await removeService(redis?.appName, redis.serverId),
async () => await removeRedisById(input.redisId),
];
for (const operation of cleanupOperations) {
try {
await operation();
} catch (error) {}
}
return redis;
}),
saveEnvironment: protectedProcedure
.input(apiSaveEnvironmentVariablesRedis)
.mutation(async ({ input, ctx }) => {
const redis = await findRedisById(input.redisId);
if (redis.project.adminId !== ctx.user.adminId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to save this environment",
});
}
const updatedRedis = await updateRedisById(input.redisId, {
env: input.env,
});
if (!updatedRedis) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Update: Error to add environment variables",
});
}
return true;
}),
update: protectedProcedure
.input(apiUpdateRedis)
.mutation(async ({ input }) => {
const { redisId, ...rest } = input;
const redis = await updateRedisById(redisId, {
...rest,
});
if (!redis) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Update: Error to update redis",
});
}
return true;
}),
});

View File

@@ -0,0 +1,103 @@
import {
apiCreateRegistry,
apiFindOneRegistry,
apiRemoveRegistry,
apiTestRegistry,
apiUpdateRegistry,
} from "@/server/db/schema";
import {
IS_CLOUD,
createRegistry,
execAsync,
execAsyncRemote,
findAllRegistryByAdminId,
findRegistryById,
removeRegistry,
updateRegistry,
} from "@dokploy/server";
import { TRPCError } from "@trpc/server";
import { adminProcedure, createTRPCRouter, protectedProcedure } from "../trpc";
export const registryRouter = createTRPCRouter({
create: adminProcedure
.input(apiCreateRegistry)
.mutation(async ({ ctx, input }) => {
return await createRegistry(input, ctx.user.adminId);
}),
remove: adminProcedure
.input(apiRemoveRegistry)
.mutation(async ({ ctx, input }) => {
const registry = await findRegistryById(input.registryId);
if (registry.adminId !== ctx.user.adminId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not allowed to delete this registry",
});
}
return await removeRegistry(input.registryId);
}),
update: protectedProcedure
.input(apiUpdateRegistry)
.mutation(async ({ input, ctx }) => {
const { registryId, ...rest } = input;
const registry = await findRegistryById(registryId);
if (registry.adminId !== ctx.user.adminId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not allowed to update this registry",
});
}
const application = await updateRegistry(registryId, {
...rest,
});
if (!application) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Update: Error to update registry",
});
}
return true;
}),
all: protectedProcedure.query(async ({ ctx }) => {
return await findAllRegistryByAdminId(ctx.user.adminId);
}),
one: adminProcedure
.input(apiFindOneRegistry)
.query(async ({ input, ctx }) => {
const registry = await findRegistryById(input.registryId);
if (registry.adminId !== ctx.user.adminId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not allowed to access this registry",
});
}
return registry;
}),
testRegistry: protectedProcedure
.input(apiTestRegistry)
.mutation(async ({ input }) => {
try {
const loginCommand = `echo ${input.password} | docker login ${input.registryUrl} --username ${input.username} --password-stdin`;
if (IS_CLOUD && !input.serverId) {
throw new TRPCError({
code: "NOT_FOUND",
message: "Select a server to test the registry",
});
}
if (input.serverId && input.serverId !== "none") {
await execAsyncRemote(input.serverId, loginCommand);
} else {
await execAsync(loginCommand);
}
return true;
} catch (error) {
console.log("Error Registry:", error);
return false;
}
}),
});

View File

@@ -0,0 +1,68 @@
import {
apiCreateSecurity,
apiFindOneSecurity,
apiUpdateSecurity,
} from "@/server/db/schema";
import {
createSecurity,
deleteSecurityById,
findApplicationById,
findSecurityById,
updateSecurityById,
} from "@dokploy/server";
import { TRPCError } from "@trpc/server";
import { createTRPCRouter, protectedProcedure } from "../trpc";
export const securityRouter = createTRPCRouter({
create: protectedProcedure
.input(apiCreateSecurity)
.mutation(async ({ input, ctx }) => {
const application = await findApplicationById(input.applicationId);
if (application.project.adminId !== ctx.user.adminId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to access this application",
});
}
return await createSecurity(input);
}),
one: protectedProcedure
.input(apiFindOneSecurity)
.query(async ({ input, ctx }) => {
const security = await findSecurityById(input.securityId);
const application = await findApplicationById(security.applicationId);
if (application.project.adminId !== ctx.user.adminId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to access this application",
});
}
return await findSecurityById(input.securityId);
}),
delete: protectedProcedure
.input(apiFindOneSecurity)
.mutation(async ({ input, ctx }) => {
const security = await findSecurityById(input.securityId);
const application = await findApplicationById(security.applicationId);
if (application.project.adminId !== ctx.user.adminId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to access this application",
});
}
return await deleteSecurityById(input.securityId);
}),
update: protectedProcedure
.input(apiUpdateSecurity)
.mutation(async ({ input, ctx }) => {
const security = await findSecurityById(input.securityId);
const application = await findApplicationById(security.applicationId);
if (application.project.adminId !== ctx.user.adminId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to access this application",
});
}
return await updateSecurityById(input.securityId, input);
}),
});

View File

@@ -0,0 +1,184 @@
import { updateServersBasedOnQuantity } from "@/pages/api/stripe/webhook";
import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc";
import { db } from "@/server/db";
import {
apiCreateServer,
apiFindOneServer,
apiRemoveServer,
apiUpdateServer,
applications,
compose,
mariadb,
mongo,
mysql,
postgres,
redis,
server,
} from "@/server/db/schema";
import {
IS_CLOUD,
createServer,
deleteServer,
findAdminById,
findServerById,
findServersByAdminId,
haveActiveServices,
removeDeploymentsByServerId,
serverSetup,
updateServerById,
} from "@dokploy/server";
import { TRPCError } from "@trpc/server";
import { and, desc, eq, getTableColumns, isNotNull, sql } from "drizzle-orm";
export const serverRouter = createTRPCRouter({
create: protectedProcedure
.input(apiCreateServer)
.mutation(async ({ ctx, input }) => {
try {
const admin = await findAdminById(ctx.user.adminId);
const servers = await findServersByAdminId(admin.adminId);
if (IS_CLOUD && servers.length >= admin.serversQuantity) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "You cannot create more servers",
});
}
const project = await createServer(input, ctx.user.adminId);
return project;
} catch (error) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Error to create the server",
cause: error,
});
}
}),
one: protectedProcedure
.input(apiFindOneServer)
.query(async ({ input, ctx }) => {
const server = await findServerById(input.serverId);
if (server.adminId !== ctx.user.adminId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to access this server",
});
}
return server;
}),
all: protectedProcedure.query(async ({ ctx }) => {
const result = await db
.select({
...getTableColumns(server),
totalSum: sql<number>`cast(count(${applications.applicationId}) + count(${compose.composeId}) + count(${redis.redisId}) + count(${mariadb.mariadbId}) + count(${mongo.mongoId}) + count(${mysql.mysqlId}) + count(${postgres.postgresId}) as integer)`,
})
.from(server)
.leftJoin(applications, eq(applications.serverId, server.serverId))
.leftJoin(compose, eq(compose.serverId, server.serverId))
.leftJoin(redis, eq(redis.serverId, server.serverId))
.leftJoin(mariadb, eq(mariadb.serverId, server.serverId))
.leftJoin(mongo, eq(mongo.serverId, server.serverId))
.leftJoin(mysql, eq(mysql.serverId, server.serverId))
.leftJoin(postgres, eq(postgres.serverId, server.serverId))
.where(eq(server.adminId, ctx.user.adminId))
.orderBy(desc(server.createdAt))
.groupBy(server.serverId);
return result;
}),
withSSHKey: protectedProcedure.query(async ({ ctx }) => {
const result = await db.query.server.findMany({
orderBy: desc(server.createdAt),
where: IS_CLOUD
? and(
isNotNull(server.sshKeyId),
eq(server.adminId, ctx.user.adminId),
eq(server.serverStatus, "active"),
)
: and(isNotNull(server.sshKeyId), eq(server.adminId, ctx.user.adminId)),
});
return result;
}),
setup: protectedProcedure
.input(apiFindOneServer)
.mutation(async ({ input, ctx }) => {
try {
const server = await findServerById(input.serverId);
if (server.adminId !== ctx.user.adminId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to setup this server",
});
}
const currentServer = await serverSetup(input.serverId);
return currentServer;
} catch (error) {
throw error;
}
}),
remove: protectedProcedure
.input(apiRemoveServer)
.mutation(async ({ input, ctx }) => {
try {
const server = await findServerById(input.serverId);
if (server.adminId !== ctx.user.adminId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to delete this server",
});
}
const activeServers = await haveActiveServices(input.serverId);
if (activeServers) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Server has active services, please delete them first",
});
}
const currentServer = await findServerById(input.serverId);
await removeDeploymentsByServerId(currentServer);
await deleteServer(input.serverId);
if (IS_CLOUD) {
const admin = await findAdminById(ctx.user.adminId);
await updateServersBasedOnQuantity(
admin.adminId,
admin.serversQuantity,
);
}
return currentServer;
} catch (error) {
throw error;
}
}),
update: protectedProcedure
.input(apiUpdateServer)
.mutation(async ({ input, ctx }) => {
try {
const server = await findServerById(input.serverId);
if (server.adminId !== ctx.user.adminId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to update this server",
});
}
if (server.serverStatus === "inactive") {
throw new TRPCError({
code: "NOT_FOUND",
message: "Server is inactive",
});
}
const currentServer = await updateServerById(input.serverId, {
...input,
});
return currentServer;
} catch (error) {
throw error;
}
}),
});

View File

@@ -0,0 +1,666 @@
import { db } from "@/server/db";
import {
apiAssignDomain,
apiEnableDashboard,
apiModifyTraefikConfig,
apiReadStatsLogs,
apiReadTraefikConfig,
apiSaveSSHKey,
apiServerSchema,
apiTraefikConfig,
apiUpdateDockerCleanup,
} from "@/server/db/schema";
import { removeJob, schedule } from "@/server/utils/backup";
import {
IS_CLOUD,
canAccessToTraefikFiles,
cleanStoppedContainers,
cleanUpDockerBuilder,
cleanUpSystemPrune,
cleanUpUnusedImages,
cleanUpUnusedVolumes,
execAsync,
execAsyncRemote,
findAdmin,
findAdminById,
findServerById,
getDokployImage,
initializeTraefik,
logRotationManager,
parseRawConfig,
paths,
prepareEnvironmentVariables,
processLogs,
pullLatestRelease,
readConfig,
readConfigInPath,
readDirectory,
readMainConfig,
readMonitoringConfig,
recreateDirectory,
sendDockerCleanupNotifications,
spawnAsync,
startService,
startServiceRemote,
stopService,
stopServiceRemote,
updateAdmin,
updateLetsEncryptEmail,
updateServerById,
updateServerTraefik,
writeConfig,
writeMainConfig,
writeTraefikConfigInPath,
} from "@dokploy/server";
import { generateOpenApiDocument } from "@dokploy/trpc-openapi";
import { TRPCError } from "@trpc/server";
import { sql } from "drizzle-orm";
import { dump, load } from "js-yaml";
import { scheduleJob, scheduledJobs } from "node-schedule";
import { z } from "zod";
import packageInfo from "../../../package.json";
import { appRouter } from "../root";
import {
adminProcedure,
createTRPCRouter,
protectedProcedure,
publicProcedure,
} from "../trpc";
export const settingsRouter = createTRPCRouter({
reloadServer: adminProcedure.mutation(async () => {
if (IS_CLOUD) {
return true;
}
const { stdout } = await execAsync(
"docker service inspect dokploy --format '{{.ID}}'",
);
await execAsync(`docker service update --force ${stdout.trim()}`);
return true;
}),
reloadTraefik: adminProcedure
.input(apiServerSchema)
.mutation(async ({ input }) => {
try {
if (input?.serverId) {
await stopServiceRemote(input.serverId, "dokploy-traefik");
await startServiceRemote(input.serverId, "dokploy-traefik");
} else if (!IS_CLOUD) {
await stopService("dokploy-traefik");
await startService("dokploy-traefik");
}
} catch (err) {
console.error(err);
}
return true;
}),
toggleDashboard: adminProcedure
.input(apiEnableDashboard)
.mutation(async ({ input }) => {
await initializeTraefik({
enableDashboard: input.enableDashboard,
serverId: input.serverId,
});
return true;
}),
cleanUnusedImages: adminProcedure
.input(apiServerSchema)
.mutation(async ({ input }) => {
await cleanUpUnusedImages(input?.serverId);
return true;
}),
cleanUnusedVolumes: adminProcedure
.input(apiServerSchema)
.mutation(async ({ input }) => {
await cleanUpUnusedVolumes(input?.serverId);
return true;
}),
cleanStoppedContainers: adminProcedure
.input(apiServerSchema)
.mutation(async ({ input }) => {
await cleanStoppedContainers(input?.serverId);
return true;
}),
cleanDockerBuilder: adminProcedure
.input(apiServerSchema)
.mutation(async ({ input }) => {
await cleanUpDockerBuilder(input?.serverId);
}),
cleanDockerPrune: adminProcedure
.input(apiServerSchema)
.mutation(async ({ input }) => {
await cleanUpSystemPrune(input?.serverId);
await cleanUpDockerBuilder(input?.serverId);
return true;
}),
cleanAll: adminProcedure
.input(apiServerSchema)
.mutation(async ({ input }) => {
await cleanUpUnusedImages(input?.serverId);
await cleanStoppedContainers(input?.serverId);
await cleanUpDockerBuilder(input?.serverId);
await cleanUpSystemPrune(input?.serverId);
return true;
}),
cleanMonitoring: adminProcedure.mutation(async () => {
if (IS_CLOUD) {
return true;
}
const { MONITORING_PATH } = paths();
await recreateDirectory(MONITORING_PATH);
return true;
}),
saveSSHPrivateKey: adminProcedure
.input(apiSaveSSHKey)
.mutation(async ({ input, ctx }) => {
if (IS_CLOUD) {
return true;
}
await updateAdmin(ctx.user.authId, {
sshPrivateKey: input.sshPrivateKey,
});
return true;
}),
assignDomainServer: adminProcedure
.input(apiAssignDomain)
.mutation(async ({ ctx, input }) => {
if (IS_CLOUD) {
return true;
}
const admin = await updateAdmin(ctx.user.authId, {
host: input.host,
...(input.letsEncryptEmail && {
letsEncryptEmail: input.letsEncryptEmail,
}),
certificateType: input.certificateType,
});
if (!admin) {
throw new TRPCError({
code: "NOT_FOUND",
message: "Admin not found",
});
}
updateServerTraefik(admin, input.host);
if (input.letsEncryptEmail) {
updateLetsEncryptEmail(input.letsEncryptEmail);
}
return admin;
}),
cleanSSHPrivateKey: adminProcedure.mutation(async ({ ctx }) => {
if (IS_CLOUD) {
return true;
}
await updateAdmin(ctx.user.authId, {
sshPrivateKey: null,
});
return true;
}),
updateDockerCleanup: adminProcedure
.input(apiUpdateDockerCleanup)
.mutation(async ({ input, ctx }) => {
if (input.serverId) {
await updateServerById(input.serverId, {
enableDockerCleanup: input.enableDockerCleanup,
});
const server = await findServerById(input.serverId);
if (server.adminId !== ctx.user.adminId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to access this server",
});
}
if (server.enableDockerCleanup) {
const server = await findServerById(input.serverId);
if (server.serverStatus === "inactive") {
throw new TRPCError({
code: "NOT_FOUND",
message: "Server is inactive",
});
}
if (IS_CLOUD) {
await schedule({
cronSchedule: "0 0 * * *",
serverId: input.serverId,
type: "server",
});
} else {
scheduleJob(server.serverId, "0 0 * * *", async () => {
console.log(
`Docker Cleanup ${new Date().toLocaleString()}] Running...`,
);
await cleanUpUnusedImages(server.serverId);
await cleanUpDockerBuilder(server.serverId);
await cleanUpSystemPrune(server.serverId);
await sendDockerCleanupNotifications();
});
}
} else {
if (IS_CLOUD) {
await removeJob({
cronSchedule: "0 0 * * *",
serverId: input.serverId,
type: "server",
});
} else {
const currentJob = scheduledJobs[server.serverId];
currentJob?.cancel();
}
}
} else if (!IS_CLOUD) {
const admin = await findAdminById(ctx.user.adminId);
if (admin.adminId !== ctx.user.adminId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to access this admin",
});
}
await updateAdmin(ctx.user.authId, {
enableDockerCleanup: input.enableDockerCleanup,
});
if (admin.enableDockerCleanup) {
scheduleJob("docker-cleanup", "0 0 * * *", async () => {
console.log(
`Docker Cleanup ${new Date().toLocaleString()}] Running...`,
);
await cleanUpUnusedImages();
await cleanUpDockerBuilder();
await cleanUpSystemPrune();
await sendDockerCleanupNotifications();
});
} else {
const currentJob = scheduledJobs["docker-cleanup"];
currentJob?.cancel();
}
}
return true;
}),
readTraefikConfig: adminProcedure.query(() => {
if (IS_CLOUD) {
return true;
}
const traefikConfig = readMainConfig();
return traefikConfig;
}),
updateTraefikConfig: adminProcedure
.input(apiTraefikConfig)
.mutation(async ({ input }) => {
if (IS_CLOUD) {
return true;
}
writeMainConfig(input.traefikConfig);
return true;
}),
readWebServerTraefikConfig: adminProcedure.query(() => {
if (IS_CLOUD) {
return true;
}
const traefikConfig = readConfig("dokploy");
return traefikConfig;
}),
updateWebServerTraefikConfig: adminProcedure
.input(apiTraefikConfig)
.mutation(async ({ input }) => {
if (IS_CLOUD) {
return true;
}
writeConfig("dokploy", input.traefikConfig);
return true;
}),
readMiddlewareTraefikConfig: adminProcedure.query(() => {
if (IS_CLOUD) {
return true;
}
const traefikConfig = readConfig("middlewares");
return traefikConfig;
}),
updateMiddlewareTraefikConfig: adminProcedure
.input(apiTraefikConfig)
.mutation(async ({ input }) => {
if (IS_CLOUD) {
return true;
}
writeConfig("middlewares", input.traefikConfig);
return true;
}),
checkAndUpdateImage: adminProcedure.mutation(async () => {
if (IS_CLOUD) {
return true;
}
return await pullLatestRelease();
}),
updateServer: adminProcedure.mutation(async () => {
if (IS_CLOUD) {
return true;
}
await spawnAsync("docker", [
"service",
"update",
"--force",
"--image",
getDokployImage(),
"dokploy",
]);
return true;
}),
getDokployVersion: adminProcedure.query(() => {
return packageInfo.version;
}),
readDirectories: protectedProcedure
.input(apiServerSchema)
.query(async ({ ctx, input }) => {
try {
if (ctx.user.rol === "user") {
const canAccess = await canAccessToTraefikFiles(ctx.user.authId);
if (!canAccess) {
throw new TRPCError({ code: "UNAUTHORIZED" });
}
}
const { MAIN_TRAEFIK_PATH } = paths(!!input?.serverId);
const result = await readDirectory(MAIN_TRAEFIK_PATH, input?.serverId);
return result || [];
} catch (error) {
throw error;
}
}),
updateTraefikFile: protectedProcedure
.input(apiModifyTraefikConfig)
.mutation(async ({ input, ctx }) => {
if (ctx.user.rol === "user") {
const canAccess = await canAccessToTraefikFiles(ctx.user.authId);
if (!canAccess) {
throw new TRPCError({ code: "UNAUTHORIZED" });
}
}
await writeTraefikConfigInPath(
input.path,
input.traefikConfig,
input?.serverId,
);
return true;
}),
readTraefikFile: protectedProcedure
.input(apiReadTraefikConfig)
.query(async ({ input, ctx }) => {
if (ctx.user.rol === "user") {
const canAccess = await canAccessToTraefikFiles(ctx.user.authId);
if (!canAccess) {
throw new TRPCError({ code: "UNAUTHORIZED" });
}
}
return readConfigInPath(input.path, input.serverId);
}),
getIp: protectedProcedure.query(async () => {
if (IS_CLOUD) {
return true;
}
const admin = await findAdmin();
return admin.serverIp;
}),
getOpenApiDocument: protectedProcedure.query(
async ({ ctx }): Promise<unknown> => {
const protocol = ctx.req.headers["x-forwarded-proto"];
const url = `${protocol}://${ctx.req.headers.host}/api`;
const openApiDocument = generateOpenApiDocument(appRouter, {
title: "tRPC OpenAPI",
version: "1.0.0",
baseUrl: url,
docsUrl: `${url}/settings.getOpenApiDocument`,
tags: [
"admin",
"docker",
"compose",
"registry",
"cluster",
"user",
"domain",
"destination",
"backup",
"deployment",
"mounts",
"certificates",
"settings",
"security",
"redirects",
"port",
"project",
"application",
"mysql",
"postgres",
"redis",
"mongo",
"mariadb",
"sshRouter",
"gitProvider",
"bitbucket",
"github",
"gitlab",
],
});
openApiDocument.info = {
title: "Dokploy API",
description: "Endpoints for dokploy",
// TODO: get version from package.json
version: "1.0.0",
};
return openApiDocument;
},
),
readTraefikEnv: adminProcedure
.input(apiServerSchema)
.query(async ({ input }) => {
const command =
"docker service inspect --format='{{range .Spec.TaskTemplate.ContainerSpec.Env}}{{println .}}{{end}}' dokploy-traefik";
if (input?.serverId) {
const result = await execAsyncRemote(input.serverId, command);
return result.stdout.trim();
}
if (!IS_CLOUD) {
const result = await execAsync(command);
return result.stdout.trim();
}
}),
writeTraefikEnv: adminProcedure
.input(z.object({ env: z.string(), serverId: z.string().optional() }))
.mutation(async ({ input }) => {
const envs = prepareEnvironmentVariables(input.env);
await initializeTraefik({
env: envs,
serverId: input.serverId,
});
return true;
}),
haveTraefikDashboardPortEnabled: adminProcedure
.input(apiServerSchema)
.query(async ({ input }) => {
const command = `docker service inspect --format='{{json .Endpoint.Ports}}' dokploy-traefik`;
let stdout = "";
if (input?.serverId) {
const result = await execAsyncRemote(input.serverId, command);
stdout = result.stdout;
} else if (!IS_CLOUD) {
const result = await execAsync(
"docker service inspect --format='{{json .Endpoint.Ports}}' dokploy-traefik",
);
stdout = result.stdout;
}
const parsed: any[] = JSON.parse(stdout.trim());
for (const port of parsed) {
if (port.PublishedPort === 8080) {
return true;
}
}
return false;
}),
readStatsLogs: adminProcedure
.meta({
openapi: {
path: "/read-stats-logs",
method: "POST",
override: true,
enabled: false,
},
})
.input(apiReadStatsLogs)
.query(({ input }) => {
if (IS_CLOUD) {
return {
data: [],
totalCount: 0,
};
}
const rawConfig = readMonitoringConfig();
const parsedConfig = parseRawConfig(
rawConfig as string,
input.page,
input.sort,
input.search,
input.status,
);
return parsedConfig;
}),
readStats: adminProcedure.query(() => {
if (IS_CLOUD) {
return [];
}
const rawConfig = readMonitoringConfig();
const processedLogs = processLogs(rawConfig as string);
return processedLogs || [];
}),
getLogRotateStatus: adminProcedure.query(async () => {
if (IS_CLOUD) {
return true;
}
return await logRotationManager.getStatus();
}),
toggleLogRotate: adminProcedure
.input(
z.object({
enable: z.boolean(),
}),
)
.mutation(async ({ input }) => {
if (IS_CLOUD) {
return true;
}
if (input.enable) {
await logRotationManager.activate();
} else {
await logRotationManager.deactivate();
}
return true;
}),
haveActivateRequests: adminProcedure.query(async () => {
if (IS_CLOUD) {
return true;
}
const config = readMainConfig();
if (!config) return false;
const parsedConfig = load(config) as {
accessLog?: {
filePath: string;
};
};
return !!parsedConfig?.accessLog?.filePath;
}),
toggleRequests: adminProcedure
.input(
z.object({
enable: z.boolean(),
}),
)
.mutation(async ({ input }) => {
if (IS_CLOUD) {
return true;
}
const mainConfig = readMainConfig();
if (!mainConfig) return false;
const currentConfig = load(mainConfig) as {
accessLog?: {
filePath: string;
};
};
if (input.enable) {
const config = {
accessLog: {
filePath: "/etc/dokploy/traefik/dynamic/access.log",
format: "json",
bufferingSize: 100,
filters: {
retryAttempts: true,
minDuration: "10ms",
},
},
};
currentConfig.accessLog = config.accessLog;
} else {
currentConfig.accessLog = undefined;
}
writeMainConfig(dump(currentConfig));
return true;
}),
isCloud: protectedProcedure.query(async () => {
return IS_CLOUD;
}),
health: publicProcedure.query(async () => {
if (IS_CLOUD) {
try {
await db.execute(sql`SELECT 1`);
return { status: "ok" };
} catch (error) {
console.error("Database connection error:", error);
throw error;
}
}
return { status: "not_cloud" };
}),
});
// {
// "Parallelism": 1,
// "Delay": 10000000000,
// "FailureAction": "rollback",
// "Order": "start-first"
// }

View File

@@ -0,0 +1,103 @@
import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc";
import { db } from "@/server/db";
import {
apiCreateSshKey,
apiFindOneSshKey,
apiGenerateSSHKey,
apiRemoveSshKey,
apiUpdateSshKey,
sshKeys,
} from "@/server/db/schema";
import {
IS_CLOUD,
createSshKey,
findSSHKeyById,
generateSSHKey,
removeSSHKeyById,
updateSSHKeyById,
} from "@dokploy/server";
import { TRPCError } from "@trpc/server";
import { eq } from "drizzle-orm";
export const sshRouter = createTRPCRouter({
create: protectedProcedure
.input(apiCreateSshKey)
.mutation(async ({ input, ctx }) => {
try {
await createSshKey({
...input,
adminId: ctx.user.adminId,
});
} catch (error) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Error to create the ssh key",
cause: error,
});
}
}),
remove: protectedProcedure
.input(apiRemoveSshKey)
.mutation(async ({ input, ctx }) => {
try {
const sshKey = await findSSHKeyById(input.sshKeyId);
if (IS_CLOUD && sshKey.adminId !== ctx.user.adminId) {
// TODO: Remove isCloud in the next versions of dokploy
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not allowed to delete this ssh key",
});
}
return await removeSSHKeyById(input.sshKeyId);
} catch (error) {
throw error;
}
}),
one: protectedProcedure
.input(apiFindOneSshKey)
.query(async ({ input, ctx }) => {
const sshKey = await findSSHKeyById(input.sshKeyId);
if (IS_CLOUD && sshKey.adminId !== ctx.user.adminId) {
// TODO: Remove isCloud in the next versions of dokploy
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not allowed to access this ssh key",
});
}
return sshKey;
}),
all: protectedProcedure.query(async ({ ctx }) => {
return await db.query.sshKeys.findMany({
...(IS_CLOUD && { where: eq(sshKeys.adminId, ctx.user.adminId) }),
});
// TODO: Remove this line when the cloud version is ready
}),
generate: protectedProcedure
.input(apiGenerateSSHKey)
.mutation(async ({ input }) => {
return await generateSSHKey(input.type);
}),
update: protectedProcedure
.input(apiUpdateSshKey)
.mutation(async ({ input, ctx }) => {
try {
const sshKey = await findSSHKeyById(input.sshKeyId);
if (IS_CLOUD && sshKey.adminId !== ctx.user.adminId) {
// TODO: Remove isCloud in the next versions of dokploy
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not allowed to update this ssh key",
});
}
return await updateSSHKeyById(input);
} catch (error) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Error to update this ssh key",
cause: error,
});
}
}),
});

View File

@@ -0,0 +1,130 @@
import { WEBSITE_URL, getStripeItems } from "@/server/utils/stripe";
import {
IS_CLOUD,
findAdminById,
findServersByAdminId,
updateAdmin,
} from "@dokploy/server";
import { TRPCError } from "@trpc/server";
import Stripe from "stripe";
import { z } from "zod";
import { adminProcedure, createTRPCRouter } from "../trpc";
export const stripeRouter = createTRPCRouter({
getProducts: adminProcedure.query(async ({ ctx }) => {
const admin = await findAdminById(ctx.user.adminId);
const stripeCustomerId = admin.stripeCustomerId;
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY || "", {
apiVersion: "2024-09-30.acacia",
});
const products = await stripe.products.list({
expand: ["data.default_price"],
active: true,
});
if (!stripeCustomerId) {
return {
products: products.data,
subscriptions: [],
};
}
const subscriptions = await stripe.subscriptions.list({
customer: stripeCustomerId,
status: "active",
expand: ["data.items.data.price"],
});
return {
products: products.data,
subscriptions: subscriptions.data,
};
}),
createCheckoutSession: adminProcedure
.input(
z.object({
productId: z.string(),
serverQuantity: z.number().min(1),
isAnnual: z.boolean(),
}),
)
.mutation(async ({ ctx, input }) => {
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY || "", {
apiVersion: "2024-09-30.acacia",
});
const items = getStripeItems(input.serverQuantity, input.isAnnual);
const admin = await findAdminById(ctx.user.adminId);
let stripeCustomerId = admin.stripeCustomerId;
if (stripeCustomerId) {
const customer = await stripe.customers.retrieve(stripeCustomerId);
if (customer.deleted) {
await updateAdmin(admin.authId, {
stripeCustomerId: null,
});
stripeCustomerId = null;
}
}
const session = await stripe.checkout.sessions.create({
mode: "subscription",
line_items: items,
...(stripeCustomerId && {
customer: stripeCustomerId,
}),
metadata: {
adminId: admin.adminId,
},
success_url: `${WEBSITE_URL}/dashboard/settings/billing`,
cancel_url: `${WEBSITE_URL}/dashboard/settings/billing`,
});
return { sessionId: session.id };
}),
createCustomerPortalSession: adminProcedure.mutation(
async ({ ctx, input }) => {
const admin = await findAdminById(ctx.user.adminId);
if (!admin.stripeCustomerId) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Stripe Customer ID not found",
});
}
const stripeCustomerId = admin.stripeCustomerId;
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY || "", {
apiVersion: "2024-09-30.acacia",
});
try {
const session = await stripe.billingPortal.sessions.create({
customer: stripeCustomerId,
return_url: `${WEBSITE_URL}/dashboard/settings/billing`,
});
return { url: session.url };
} catch (error) {
return {
url: "",
};
}
},
),
canCreateMoreServers: adminProcedure.query(async ({ ctx }) => {
const admin = await findAdminById(ctx.user.adminId);
const servers = await findServersByAdminId(admin.adminId);
if (!IS_CLOUD) {
return true;
}
return servers.length < admin.serversQuantity;
}),
});

View File

@@ -0,0 +1,34 @@
import { apiFindOneUser, apiFindOneUserByAuth } from "@/server/db/schema";
import { findUserByAuthId, findUserById, findUsers } from "@dokploy/server";
import { TRPCError } from "@trpc/server";
import { adminProcedure, createTRPCRouter, protectedProcedure } from "../trpc";
export const userRouter = createTRPCRouter({
all: adminProcedure.query(async ({ ctx }) => {
return await findUsers(ctx.user.adminId);
}),
byAuthId: protectedProcedure
.input(apiFindOneUserByAuth)
.query(async ({ input, ctx }) => {
const user = await findUserByAuthId(input.authId);
if (user.adminId !== ctx.user.adminId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not allowed to access this user",
});
}
return user;
}),
byUserId: protectedProcedure
.input(apiFindOneUser)
.query(async ({ input, ctx }) => {
const user = await findUserById(input.userId);
if (user.adminId !== ctx.user.adminId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not allowed to access this user",
});
}
return user;
}),
});

210
apps/mig/server/api/trpc.ts Normal file
View File

@@ -0,0 +1,210 @@
/**
* YOU PROBABLY DON'T NEED TO EDIT THIS FILE, UNLESS:
* 1. You want to modify request context (see Part 1).
* 2. You want to create a new middleware or type of procedure (see Part 3).
*
* TL;DR - This is where all the tRPC server stuff is created and plugged in. The pieces you will
* need to use are documented accordingly near the end.
*/
// import { getServerAuthSession } from "@/server/auth";
import { db } from "@/server/db";
import { validateBearerToken, validateRequest } from "@dokploy/server";
import type { OpenApiMeta } from "@dokploy/trpc-openapi";
import { TRPCError, initTRPC } from "@trpc/server";
import type { CreateNextContextOptions } from "@trpc/server/adapters/next";
import {
experimental_createMemoryUploadHandler,
experimental_isMultipartFormDataRequest,
experimental_parseMultipartFormData,
} from "@trpc/server/adapters/node-http/content-type/form-data";
import type { Session, User } from "lucia";
import superjson from "superjson";
import { ZodError } from "zod";
/**
* 1. CONTEXT
*
* This section defines the "contexts" that are available in the backend API.
*
* These allow you to access things when processing a request, like the database, the session, etc.
*/
interface CreateContextOptions {
user: (User & { authId: string; adminId: string }) | null;
session: Session | null;
req: CreateNextContextOptions["req"];
res: CreateNextContextOptions["res"];
}
/**
* This helper generates the "internals" for a tRPC context. If you need to use it, you can export
* it from here.
*
* Examples of things you may need it for:
* - testing, so we don't have to mock Next.js' req/res
* - tRPC's `createSSGHelpers`, where we don't have req/res
*
* @see https://create.t3.gg/en/usage/trpc#-serverapitrpcts
*/
const createInnerTRPCContext = (opts: CreateContextOptions) => {
return {
session: opts.session,
db,
req: opts.req,
res: opts.res,
user: opts.user,
};
};
/**
* This is the actual context you will use in your router. It will be used to process every request
* that goes through your tRPC endpoint.
*
* @see https://trpc.io/docs/context
*/
export const createTRPCContext = async (opts: CreateNextContextOptions) => {
const { req, res } = opts;
let { session, user } = await validateBearerToken(req);
if (!session) {
const cookieResult = await validateRequest(req, res);
session = cookieResult.session;
user = cookieResult.user;
}
return createInnerTRPCContext({
req,
res,
session: session,
...((user && {
user: {
authId: user.id,
email: user.email,
rol: user.rol,
id: user.id,
secret: user.secret,
adminId: user.adminId,
},
}) || {
user: null,
}),
});
};
/**
* 2. INITIALIZATION
*
* This is where the tRPC API is initialized, connecting the context and transformer. We also parse
* ZodErrors so that you get typesafety on the frontend if your procedure fails due to validation
* errors on the backend.
*/
const t = initTRPC
.meta<OpenApiMeta>()
.context<typeof createTRPCContext>()
.create({
transformer: superjson,
errorFormatter({ shape, error }) {
return {
...shape,
data: {
...shape.data,
zodError:
error.cause instanceof ZodError ? error.cause.flatten() : null,
},
};
},
});
/**
* 3. ROUTER & PROCEDURE (THE IMPORTANT BIT)
*
* These are the pieces you use to build your tRPC API. You should import these a lot in the
* "/src/server/api/routers" directory.
*/
/**
* This is how you create new routers and sub-routers in your tRPC API.
*
* @see https://trpc.io/docs/router
*/
export const createTRPCRouter = t.router;
/**
* Public (unauthenticated) procedure
*
* This is the base piece you use to build new queries and mutations on your tRPC API. It does not
* guarantee that a user querying is authorized, but you can still access user session data if they
* are logged in.
*/
export const publicProcedure = t.procedure;
/**
* Protected (authenticated) procedure
*
* If you want a query or mutation to ONLY be accessible to logged in users, use this. It verifies
* the session is valid and guarantees `ctx.session.user` is not null.
*
* @see https://trpc.io/docs/procedures
*/
export const protectedProcedure = t.procedure.use(({ ctx, next }) => {
if (!ctx.session || !ctx.user) {
throw new TRPCError({ code: "UNAUTHORIZED" });
}
return next({
ctx: {
// infers the `session` as non-nullable
session: ctx.session,
user: ctx.user,
// session: { ...ctx.session, user: ctx.user },
},
});
});
export const uploadProcedure = async (opts: any) => {
if (!experimental_isMultipartFormDataRequest(opts.ctx.req)) {
return opts.next();
}
const formData = await experimental_parseMultipartFormData(
opts.ctx.req,
experimental_createMemoryUploadHandler({
// 2GB
maxPartSize: 1024 * 1024 * 1024 * 2,
}),
);
return opts.next({
rawInput: formData,
});
};
export const cliProcedure = t.procedure.use(({ ctx, next }) => {
if (!ctx.session || !ctx.user || ctx.user.rol !== "admin") {
throw new TRPCError({ code: "UNAUTHORIZED" });
}
return next({
ctx: {
// infers the `session` as non-nullable
session: ctx.session,
user: ctx.user,
// session: { ...ctx.session, user: ctx.user },
},
});
});
export const adminProcedure = t.procedure.use(({ ctx, next }) => {
if (!ctx.session || !ctx.user || ctx.user.rol !== "admin") {
throw new TRPCError({ code: "UNAUTHORIZED" });
}
return next({
ctx: {
// infers the `session` as non-nullable
session: ctx.session,
user: ctx.user,
// session: { ...ctx.session, user: ctx.user },
},
});
});

View File

@@ -0,0 +1,14 @@
import { defineConfig } from "drizzle-kit";
export default defineConfig({
schema: "./server/db/schema/index.ts",
dialect: "postgresql",
dbCredentials: {
url: process.env.DATABASE_URL || "",
},
out: "drizzle",
migrations: {
table: "migrations",
schema: "public",
},
});

View File

@@ -0,0 +1,21 @@
import { type PostgresJsDatabase, drizzle } from "drizzle-orm/postgres-js";
import postgres from "postgres";
import * as schema from "./schema";
declare global {
var db: PostgresJsDatabase<typeof schema> | undefined;
}
export let db: PostgresJsDatabase<typeof schema>;
if (process.env.NODE_ENV === "production") {
db = drizzle(postgres(process.env.DATABASE_URL || ""), {
schema,
});
} else {
if (!global.db)
global.db = drizzle(postgres(process.env.DATABASE_URL || ""), {
schema,
});
db = global.db;
}

View File

@@ -0,0 +1,21 @@
import { drizzle } from "drizzle-orm/postgres-js";
import { migrate } from "drizzle-orm/postgres-js/migrator";
import postgres from "postgres";
const connectionString = process.env.DATABASE_URL || "";
const sql = postgres(connectionString, { max: 1 });
const db = drizzle(sql);
export const migration = async () =>
await migrate(db, { migrationsFolder: "drizzle" })
.then(() => {
console.log("Migration complete");
sql.end();
})
.catch((error) => {
console.log("Migration failed", error);
})
.finally(() => {
sql.end();
});

View File

@@ -0,0 +1,23 @@
import { sql } from "drizzle-orm";
// Credits to Louistiti from Drizzle Discord: https://discord.com/channels/1043890932593987624/1130802621750448160/1143083373535973406
import { drizzle } from "drizzle-orm/postgres-js";
import postgres from "postgres";
const connectionString = process.env.DATABASE_URL || "";
const pg = postgres(connectionString, { max: 1 });
const db = drizzle(pg);
const clearDb = async (): Promise<void> => {
try {
const tablesQuery = sql<string>`DROP SCHEMA public CASCADE; CREATE SCHEMA public; DROP schema drizzle CASCADE;`;
const tables = await db.execute(tablesQuery);
console.log(tables);
await pg.end();
} catch (error) {
console.error("Error to clean database", error);
} finally {
}
};
clearDb();

View File

@@ -0,0 +1 @@
export * from "@dokploy/server/db/schema";

View File

@@ -0,0 +1,35 @@
import bc from "bcrypt";
import { drizzle } from "drizzle-orm/postgres-js";
import postgres from "postgres";
import { users } from "./schema";
const connectionString = process.env.DATABASE_URL || "";
const pg = postgres(connectionString, { max: 1 });
const db = drizzle(pg);
function password(txt: string) {
return bc.hashSync(txt, 10);
}
async function seed() {
console.log("> Seed:", process.env.DATABASE_PATH, "\n");
// const authenticationR = await db
// .insert(users)
// .values([
// {
// email: "user1@hotmail.com",
// password: password("12345671"),
// },
// ])
// .onConflictDoNothing()
// .returning();
// console.log("\nSemillas Update:", authenticationR.length);
}
seed().catch((e) => {
console.error(e);
process.exit(1);
});

View File

@@ -0,0 +1,46 @@
import { z } from "zod";
export const domain = z
.object({
host: z.string().min(1, { message: "Add a hostname" }),
path: z.string().min(1).optional(),
port: z
.number()
.min(1, { message: "Port must be at least 1" })
.max(65535, { message: "Port must be 65535 or below" })
.optional(),
https: z.boolean().optional(),
certificateType: z.enum(["letsencrypt", "none"]).optional(),
})
.superRefine((input, ctx) => {
if (input.https && !input.certificateType) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
path: ["certificateType"],
message: "Required",
});
}
});
export const domainCompose = z
.object({
host: z.string().min(1, { message: "Host is required" }),
path: z.string().min(1).optional(),
port: z
.number()
.min(1, { message: "Port must be at least 1" })
.max(65535, { message: "Port must be 65535 or below" })
.optional(),
https: z.boolean().optional(),
certificateType: z.enum(["letsencrypt", "none"]).optional(),
serviceName: z.string().min(1, { message: "Service name is required" }),
})
.superRefine((input, ctx) => {
if (input.https && !input.certificateType) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
path: ["certificateType"],
message: "Required",
});
}
});

View File

@@ -0,0 +1,37 @@
import { z } from "zod";
export const sshKeyCreate = z.object({
name: z.string().min(1),
description: z.string().optional(),
publicKey: z.string().refine(
(key) => {
const rsaPubPattern = /^ssh-rsa\s+([A-Za-z0-9+/=]+)\s*(.*)?\s*$/;
const ed25519PubPattern = /^ssh-ed25519\s+([A-Za-z0-9+/=]+)\s*(.*)?\s*$/;
return rsaPubPattern.test(key) || ed25519PubPattern.test(key);
},
{
message: "Invalid public key format",
},
),
privateKey: z.string().refine(
(key) => {
const rsaPrivPattern =
/^-----BEGIN RSA PRIVATE KEY-----\n([A-Za-z0-9+/=\n]+)-----END RSA PRIVATE KEY-----\s*$/;
const ed25519PrivPattern =
/^-----BEGIN OPENSSH PRIVATE KEY-----\n([A-Za-z0-9+/=\n]+)-----END OPENSSH PRIVATE KEY-----\s*$/;
return rsaPrivPattern.test(key) || ed25519PrivPattern.test(key);
},
{
message: "Invalid private key format",
},
),
});
export const sshKeyUpdate = sshKeyCreate.pick({
name: true,
description: true,
});
export const sshKeyType = z.object({
type: z.enum(["rsa", "ed25519"]).optional(),
});

View File

@@ -0,0 +1,138 @@
import {
deployApplication,
deployCompose,
deployRemoteApplication,
deployRemoteCompose,
rebuildApplication,
rebuildCompose,
rebuildRemoteApplication,
rebuildRemoteCompose,
updateApplicationStatus,
updateCompose,
} from "@dokploy/server";
import { type Job, Worker } from "bullmq";
import { myQueue, redisConfig } from "./queueSetup";
type DeployJob =
| {
applicationId: string;
titleLog: string;
descriptionLog: string;
server?: boolean;
type: "deploy" | "redeploy";
applicationType: "application";
serverId?: string;
}
| {
composeId: string;
titleLog: string;
descriptionLog: string;
server?: boolean;
type: "deploy" | "redeploy";
applicationType: "compose";
serverId?: string;
};
export type DeploymentJob = DeployJob;
export const deploymentWorker = new Worker(
"deployments",
async (job: Job<DeploymentJob>) => {
try {
if (job.data.applicationType === "application") {
await updateApplicationStatus(job.data.applicationId, "running");
if (job.data.server) {
if (job.data.type === "redeploy") {
await rebuildRemoteApplication({
applicationId: job.data.applicationId,
titleLog: job.data.titleLog,
descriptionLog: job.data.descriptionLog,
});
} else if (job.data.type === "deploy") {
await deployRemoteApplication({
applicationId: job.data.applicationId,
titleLog: job.data.titleLog,
descriptionLog: job.data.descriptionLog,
});
}
} else {
if (job.data.type === "redeploy") {
await rebuildApplication({
applicationId: job.data.applicationId,
titleLog: job.data.titleLog,
descriptionLog: job.data.descriptionLog,
});
} else if (job.data.type === "deploy") {
await deployApplication({
applicationId: job.data.applicationId,
titleLog: job.data.titleLog,
descriptionLog: job.data.descriptionLog,
});
}
}
} else if (job.data.applicationType === "compose") {
await updateCompose(job.data.composeId, {
composeStatus: "running",
});
if (job.data.server) {
if (job.data.type === "redeploy") {
await rebuildRemoteCompose({
composeId: job.data.composeId,
titleLog: job.data.titleLog,
descriptionLog: job.data.descriptionLog,
});
} else if (job.data.type === "deploy") {
await deployRemoteCompose({
composeId: job.data.composeId,
titleLog: job.data.titleLog,
descriptionLog: job.data.descriptionLog,
});
}
} else {
if (job.data.type === "deploy") {
await deployCompose({
composeId: job.data.composeId,
titleLog: job.data.titleLog,
descriptionLog: job.data.descriptionLog,
});
} else if (job.data.type === "redeploy") {
await rebuildCompose({
composeId: job.data.composeId,
titleLog: job.data.titleLog,
descriptionLog: job.data.descriptionLog,
});
}
}
}
} catch (error) {
console.log("Error", error);
}
},
{
autorun: false,
connection: redisConfig,
},
);
export const cleanQueuesByApplication = async (applicationId: string) => {
const jobs = await myQueue.getJobs(["waiting", "delayed"]);
for (const job of jobs) {
if (job?.data?.applicationId === applicationId) {
await job.remove();
console.log(`Removed job ${job.id} for application ${applicationId}`);
}
}
};
export const cleanQueuesByCompose = async (composeId: string) => {
const jobs = await myQueue.getJobs(["waiting", "delayed"]);
for (const job of jobs) {
if (job?.data?.composeId === composeId) {
await job.remove();
console.log(`Removed job ${job.id} for compose ${composeId}`);
}
}
};

View File

@@ -0,0 +1,24 @@
import { type ConnectionOptions, Queue } from "bullmq";
export const redisConfig: ConnectionOptions = {
host: process.env.NODE_ENV === "production" ? "dokploy-redis" : "127.0.0.1",
};
const myQueue = new Queue("deployments", {
connection: redisConfig,
});
process.on("SIGTERM", () => {
myQueue.close();
process.exit(0);
});
myQueue.on("error", (error) => {
if ((error as any).code === "ECONNREFUSED") {
console.error(
"Make sure you have installed Redis and it is running.",
error,
);
}
});
export { myQueue };

91
apps/mig/server/server.ts Normal file
View File

@@ -0,0 +1,91 @@
import {
IS_CLOUD,
createDefaultMiddlewares,
createDefaultServerTraefikConfig,
createDefaultTraefikConfig,
getPublicIpWithFallback,
initCronJobs,
initializeNetwork,
initializePostgres,
initializeRedis,
initializeTraefik,
sendDokployRestartNotifications,
setupDirectories,
} from "@dokploy/server";
import { createRequestHandler } from "@remix-run/express";
import { installGlobals } from "@remix-run/node";
import express from "express";
import { migration } from "./db/migration";
installGlobals();
const viteDevServer =
process.env.NODE_ENV === "production"
? undefined
: await import("vite").then((vite) =>
vite.createServer({
server: { middlewareMode: true },
}),
);
const app = express();
// handle asset requests
if (viteDevServer) {
app.use(viteDevServer.middlewares);
} else {
app.use(
"/assets",
express.static("build/client/assets", {
immutable: true,
maxAge: "1y",
}),
);
}
app.use(express.static("build/client", { maxAge: "1h" }));
// handle SSR requests
app.all(
"*",
createRequestHandler({
build: viteDevServer
? () => viteDevServer.ssrLoadModule("virtual:remix/server-build")
: await import("../build/server/index.js"),
}),
);
const port = 3000;
app.listen(port, async () => {
if (!IS_CLOUD) {
// setupDockerStatsMonitoringSocketServer(server);
}
if (process.env.NODE_ENV === "production" && !IS_CLOUD) {
setupDirectories();
createDefaultMiddlewares();
await initializeNetwork();
createDefaultTraefikConfig();
createDefaultServerTraefikConfig();
await initializePostgres();
await initializeTraefik();
await initializeRedis();
initCronJobs();
// Timeout to wait for the database to be ready
await new Promise((resolve) => setTimeout(resolve, 7000));
// await migration();
await sendDokployRestartNotifications();
}
if (IS_CLOUD && process.env.NODE_ENV === "production") {
await migration();
}
console.log("Server Started:", port);
if (!IS_CLOUD) {
console.log("Starting Deployment Worker");
const { deploymentWorker } = await import("./queues/deployments-queue");
await deploymentWorker.run();
}
});

View File

@@ -0,0 +1,67 @@
type QueueJob =
| {
type: "backup";
cronSchedule: string;
backupId: string;
}
| {
type: "server";
cronSchedule: string;
serverId: string;
};
export const schedule = async (job: QueueJob) => {
try {
const result = await fetch(`${process.env.JOBS_URL}/create-backup`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-API-Key": process.env.API_KEY || "NO-DEFINED",
},
body: JSON.stringify(job),
});
const data = await result.json();
console.log(data);
return data;
} catch (error) {
console.log(error);
throw error;
}
};
export const removeJob = async (job: QueueJob) => {
try {
const result = await fetch(`${process.env.JOBS_URL}/remove-job`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-API-Key": process.env.API_KEY || "NO-DEFINED",
},
body: JSON.stringify(job),
});
const data = await result.json();
console.log(data);
return data;
} catch (error) {
console.log(error);
throw error;
}
};
export const updateJob = async (job: QueueJob) => {
try {
const result = await fetch(`${process.env.JOBS_URL}/update-backup`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-API-Key": process.env.API_KEY || "NO-DEFINED",
},
body: JSON.stringify(job),
});
const data = await result.json();
console.log(data);
return data;
} catch (error) {
console.log(error);
throw error;
}
};

View File

@@ -0,0 +1,25 @@
import { findServerById } from "@dokploy/server";
import type { DeploymentJob } from "../queues/deployments-queue";
export const deploy = async (jobData: DeploymentJob) => {
try {
const server = await findServerById(jobData.serverId as string);
if (server.serverStatus === "inactive") {
throw new Error("Server is inactive");
}
const result = await fetch(`${process.env.SERVER_URL}/deploy`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-API-Key": process.env.API_KEY || "NO-DEFINED",
},
body: JSON.stringify(jobData),
});
const data = await result.json();
console.log(data);
return data;
} catch (error) {
console.log(error);
throw error;
}
};

View File

@@ -0,0 +1,27 @@
export const WEBSITE_URL =
process.env.NODE_ENV === "development"
? "http://localhost:3000"
: "https://app.dokploy.com";
const BASE_PRICE_MONTHLY_ID = process.env.BASE_PRICE_MONTHLY_ID || ""; // $4.00
const BASE_ANNUAL_MONTHLY_ID = process.env.BASE_ANNUAL_MONTHLY_ID || ""; // $7.99
export const getStripeItems = (serverQuantity: number, isAnnual: boolean) => {
const items = [];
if (isAnnual) {
items.push({
price: BASE_ANNUAL_MONTHLY_ID,
quantity: serverQuantity,
});
return items;
}
items.push({
price: BASE_PRICE_MONTHLY_ID,
quantity: serverQuantity,
});
return items;
};

View File

@@ -0,0 +1,141 @@
import type http from "node:http";
import { findServerById, validateWebSocketRequest } from "@dokploy/server";
import { spawn } from "node-pty";
import { Client } from "ssh2";
import { WebSocketServer } from "ws";
import { getShell } from "./utils";
export const setupDockerContainerLogsWebSocketServer = (
server: http.Server<typeof http.IncomingMessage, typeof http.ServerResponse>,
) => {
const wssTerm = new WebSocketServer({
noServer: true,
path: "/docker-container-logs",
});
server.on("upgrade", (req, socket, head) => {
const { pathname } = new URL(req.url || "", `http://${req.headers.host}`);
if (pathname === "/_next/webpack-hmr") {
return;
}
if (pathname === "/docker-container-logs") {
wssTerm.handleUpgrade(req, socket, head, function done(ws) {
wssTerm.emit("connection", ws, req);
});
}
});
// eslint-disable-next-line @typescript-eslint/no-misused-promises
wssTerm.on("connection", async (ws, req) => {
const url = new URL(req.url || "", `http://${req.headers.host}`);
const containerId = url.searchParams.get("containerId");
const tail = url.searchParams.get("tail");
const serverId = url.searchParams.get("serverId");
const { user, session } = await validateWebSocketRequest(req);
if (!containerId) {
ws.close(4000, "containerId no provided");
return;
}
if (!user || !session) {
ws.close();
return;
}
try {
if (serverId) {
const server = await findServerById(serverId);
if (!server.sshKeyId) return;
const client = new Client();
client
.once("ready", () => {
const command = `
bash -c "docker container logs --tail ${tail} --follow ${containerId}"
`;
client.exec(command, (err, stream) => {
if (err) {
console.error("Execution error:", err);
ws.close();
client.end();
return;
}
stream
.on("close", () => {
console.log("Connection closed ✅ Container Logs");
client.end();
ws.close();
})
.on("data", (data: string) => {
ws.send(data.toString());
})
.stderr.on("data", (data) => {
ws.send(data.toString());
});
});
})
.on("error", (err) => {
console.error("SSH connection error:", err);
ws.send(`SSH error: ${err.message}`);
ws.close(); // Cierra el WebSocket si hay un error con SSH
client.end();
})
.connect({
host: server.ipAddress,
port: server.port,
username: server.username,
privateKey: server.sshKey?.privateKey,
});
ws.on("close", () => {
console.log("Connection closed ✅, From Container Logs WS");
client.end();
});
} else {
const shell = getShell();
const ptyProcess = spawn(
shell,
[
"-c",
`docker container logs --tail ${tail} --follow ${containerId}`,
],
{
name: "xterm-256color",
cwd: process.env.HOME,
env: process.env,
encoding: "utf8",
cols: 80,
rows: 30,
},
);
ptyProcess.onData((data) => {
ws.send(data);
});
ws.on("close", () => {
ptyProcess.kill();
});
ws.on("message", (message) => {
try {
let command: string | Buffer[] | Buffer | ArrayBuffer;
if (Buffer.isBuffer(message)) {
command = message.toString("utf8");
} else {
command = message;
}
ptyProcess.write(command.toString());
} catch (error) {
// @ts-ignore
const errorMessage = error?.message as unknown as string;
ws.send(errorMessage);
}
});
}
} catch (error) {
// @ts-ignore
const errorMessage = error?.message as unknown as string;
ws.send(errorMessage);
}
});
};

View File

@@ -0,0 +1,154 @@
import type http from "node:http";
import { findServerById, validateWebSocketRequest } from "@dokploy/server";
import { spawn } from "node-pty";
import { Client } from "ssh2";
import { WebSocketServer } from "ws";
import { getShell } from "./utils";
export const setupDockerContainerTerminalWebSocketServer = (
server: http.Server<typeof http.IncomingMessage, typeof http.ServerResponse>,
) => {
const wssTerm = new WebSocketServer({
noServer: true,
path: "/docker-container-terminal",
});
server.on("upgrade", (req, socket, head) => {
const { pathname } = new URL(req.url || "", `http://${req.headers.host}`);
if (pathname === "/_next/webpack-hmr") {
return;
}
if (pathname === "/docker-container-terminal") {
wssTerm.handleUpgrade(req, socket, head, function done(ws) {
wssTerm.emit("connection", ws, req);
});
}
});
// eslint-disable-next-line @typescript-eslint/no-misused-promises
wssTerm.on("connection", async (ws, req) => {
const url = new URL(req.url || "", `http://${req.headers.host}`);
const containerId = url.searchParams.get("containerId");
const activeWay = url.searchParams.get("activeWay");
const serverId = url.searchParams.get("serverId");
const { user, session } = await validateWebSocketRequest(req);
if (!containerId) {
ws.close(4000, "containerId no provided");
return;
}
if (!user || !session) {
ws.close();
return;
}
try {
if (serverId) {
const server = await findServerById(serverId);
if (!server.sshKeyId)
throw new Error("No SSH key available for this server");
const conn = new Client();
let stdout = "";
let stderr = "";
conn
.once("ready", () => {
conn.exec(
`docker exec -it ${containerId} ${activeWay}`,
{ pty: true },
(err, stream) => {
if (err) throw err;
stream
.on("close", (code: number, signal: string) => {
console.log(
`Stream :: close :: code: ${code}, signal: ${signal}`,
);
ws.send(`\nContainer closed with code: ${code}\n`);
conn.end();
})
.on("data", (data: string) => {
stdout += data.toString();
ws.send(data.toString());
})
.stderr.on("data", (data) => {
stderr += data.toString();
ws.send(data.toString());
console.error("Error: ", data.toString());
});
ws.on("message", (message) => {
try {
let command: string | Buffer[] | Buffer | ArrayBuffer;
if (Buffer.isBuffer(message)) {
command = message.toString("utf8");
} else {
command = message;
}
stream.write(command.toString());
} catch (error) {
// @ts-ignore
const errorMessage = error?.message as unknown as string;
ws.send(errorMessage);
}
});
ws.on("close", () => {
stream.end();
});
},
);
})
.connect({
host: server.ipAddress,
port: server.port,
username: server.username,
privateKey: server.sshKey?.privateKey,
timeout: 99999,
});
} else {
const shell = getShell();
const ptyProcess = spawn(
shell,
["-c", `docker exec -it ${containerId} ${activeWay}`],
{
name: "xterm-256color",
cwd: process.env.HOME,
env: process.env,
encoding: "utf8",
cols: 80,
rows: 30,
},
);
ptyProcess.onData((data) => {
ws.send(data);
});
ws.on("close", () => {
ptyProcess.kill();
});
ws.on("message", (message) => {
try {
let command: string | Buffer[] | Buffer | ArrayBuffer;
if (Buffer.isBuffer(message)) {
command = message.toString("utf8");
} else {
command = message;
}
ptyProcess.write(command.toString());
} catch (error) {
// @ts-ignore
const errorMessage = error?.message as unknown as string;
ws.send(errorMessage);
}
});
}
} catch (error) {
// @ts-ignore
const errorMessage = error?.message as unknown as string;
ws.send(errorMessage);
}
});
};

View File

@@ -0,0 +1,96 @@
import type http from "node:http";
import {
docker,
getLastAdvancedStatsFile,
recordAdvancedStats,
validateWebSocketRequest,
} from "@dokploy/server";
import { WebSocketServer } from "ws";
export const setupDockerStatsMonitoringSocketServer = (
server: http.Server<typeof http.IncomingMessage, typeof http.ServerResponse>,
) => {
const wssTerm = new WebSocketServer({
noServer: true,
path: "/listen-docker-stats-monitoring",
});
server.on("upgrade", (req, socket, head) => {
const { pathname } = new URL(req.url || "", `http://${req.headers.host}`);
if (pathname === "/_next/webpack-hmr") {
return;
}
if (pathname === "/listen-docker-stats-monitoring") {
wssTerm.handleUpgrade(req, socket, head, function done(ws) {
wssTerm.emit("connection", ws, req);
});
}
});
wssTerm.on("connection", async (ws, req) => {
const url = new URL(req.url || "", `http://${req.headers.host}`);
const appName = url.searchParams.get("appName");
const appType = (url.searchParams.get("appType") || "application") as
| "application"
| "stack"
| "docker-compose";
const { user, session } = await validateWebSocketRequest(req);
if (!appName) {
ws.close(4000, "appName no provided");
return;
}
if (!user || !session) {
ws.close();
return;
}
const intervalId = setInterval(async () => {
try {
const filter = {
status: ["running"],
...(appType === "application" && {
label: [`com.docker.swarm.service.name=${appName}`],
}),
...(appType === "stack" && {
label: [`com.docker.swarm.task.name=${appName}`],
}),
...(appType === "docker-compose" && {
name: [appName],
}),
};
const containers = await docker.listContainers({
filters: JSON.stringify(filter),
});
const container = containers[0];
if (!container || container?.State !== "running") {
ws.close(4000, "Container not running");
return;
}
const stats = await docker.getContainer(container.Id).stats({
stream: false,
});
await recordAdvancedStats(stats, appName);
const data = await getLastAdvancedStatsFile(appName);
ws.send(
JSON.stringify({
data,
}),
);
} catch (error) {
// @ts-ignore
ws.close(4000, `Error: ${error.message}`);
}
}, 1300);
ws.on("close", () => {
clearInterval(intervalId);
});
});
};

View File

@@ -0,0 +1,112 @@
import { spawn } from "node:child_process";
import type http from "node:http";
import { findServerById, validateWebSocketRequest } from "@dokploy/server";
import { Client } from "ssh2";
import { WebSocketServer } from "ws";
export const setupDeploymentLogsWebSocketServer = (
server: http.Server<typeof http.IncomingMessage, typeof http.ServerResponse>,
) => {
const wssTerm = new WebSocketServer({
noServer: true,
path: "/listen-deployment",
});
server.on("upgrade", (req, socket, head) => {
const { pathname } = new URL(req.url || "", `http://${req.headers.host}`);
if (pathname === "/_next/webpack-hmr") {
return;
}
if (pathname === "/listen-deployment") {
wssTerm.handleUpgrade(req, socket, head, function done(ws) {
wssTerm.emit("connection", ws, req);
});
}
});
wssTerm.on("connection", async (ws, req) => {
const url = new URL(req.url || "", `http://${req.headers.host}`);
const logPath = url.searchParams.get("logPath");
const serverId = url.searchParams.get("serverId");
const { user, session } = await validateWebSocketRequest(req);
if (!logPath) {
console.log("logPath no provided");
ws.close(4000, "logPath no provided");
return;
}
if (!user || !session) {
ws.close();
return;
}
try {
if (serverId) {
const server = await findServerById(serverId);
if (!server.sshKeyId) return;
const client = new Client();
client
.on("ready", () => {
const command = `
tail -n +1 -f ${logPath};
`;
client.exec(command, (err, stream) => {
if (err) {
console.error("Execution error:", err);
ws.close();
return;
}
stream
.on("close", () => {
console.log("Connection closed ✅");
client.end();
ws.close();
})
.on("data", (data: string) => {
ws.send(data.toString());
})
.stderr.on("data", (data) => {
ws.send(data.toString());
});
});
})
.on("error", (err) => {
console.error("SSH connection error:", err);
ws.send(`SSH error: ${err.message}`);
ws.close(); // Cierra el WebSocket si hay un error con SSH
})
.connect({
host: server.ipAddress,
port: server.port,
username: server.username,
privateKey: server.sshKey?.privateKey,
});
ws.on("close", () => {
console.log("Connection closed ✅, From WS");
client.end();
});
} else {
const tail = spawn("tail", ["-n", "+1", "-f", logPath]);
tail.stdout.on("data", (data) => {
ws.send(data.toString());
});
tail.stderr.on("data", (data) => {
ws.send(new Error(`tail error: ${data.toString()}`).message);
});
tail.on("close", () => {
ws.close();
});
}
} catch (error) {
// @ts-ignore
// const errorMessage = error?.message as unknown as string;
// ws.send(errorMessage);
}
});
};

View File

@@ -0,0 +1,140 @@
import type http from "node:http";
import { findServerById, validateWebSocketRequest } from "@dokploy/server";
import { publicIpv4, publicIpv6 } from "public-ip";
import { Client } from "ssh2";
import { WebSocketServer } from "ws";
export const getPublicIpWithFallback = async () => {
// @ts-ignore
let ip = null;
try {
ip = await publicIpv4();
} catch (error) {
console.log(
"Error to obtain public IPv4 address, falling back to IPv6",
// @ts-ignore
error.message,
);
try {
ip = await publicIpv6();
} catch (error) {
// @ts-ignore
console.error("Error to obtain public IPv6 address", error.message);
ip = null;
}
}
return ip;
};
export const setupTerminalWebSocketServer = (
server: http.Server<typeof http.IncomingMessage, typeof http.ServerResponse>,
) => {
const wssTerm = new WebSocketServer({
noServer: true,
path: "/terminal",
});
server.on("upgrade", (req, socket, head) => {
const { pathname } = new URL(req.url || "", `http://${req.headers.host}`);
if (pathname === "/_next/webpack-hmr") {
return;
}
if (pathname === "/terminal") {
wssTerm.handleUpgrade(req, socket, head, function done(ws) {
wssTerm.emit("connection", ws, req);
});
}
});
wssTerm.on("connection", async (ws, req) => {
const url = new URL(req.url || "", `http://${req.headers.host}`);
const serverId = url.searchParams.get("serverId");
const { user, session } = await validateWebSocketRequest(req);
if (!user || !session || !serverId) {
ws.close();
return;
}
const server = await findServerById(serverId);
if (!server) {
ws.close();
return;
}
if (!server.sshKeyId)
throw new Error("No SSH key available for this server");
const conn = new Client();
let stdout = "";
let stderr = "";
conn
.once("ready", () => {
conn.shell(
{
term: "terminal",
cols: 80,
rows: 30,
height: 30,
width: 80,
},
(err, stream) => {
if (err) throw err;
stream
.on("close", (code: number, signal: string) => {
ws.send(`\nContainer closed with code: ${code}\n`);
conn.end();
})
.on("data", (data: string) => {
stdout += data.toString();
ws.send(data.toString());
})
.stderr.on("data", (data) => {
stderr += data.toString();
ws.send(data.toString());
console.error("Error: ", data.toString());
});
ws.on("message", (message) => {
try {
let command: string | Buffer[] | Buffer | ArrayBuffer;
if (Buffer.isBuffer(message)) {
command = message.toString("utf8");
} else {
command = message;
}
stream.write(command.toString());
} catch (error) {
// @ts-ignore
const errorMessage = error?.message as unknown as string;
ws.send(errorMessage);
}
});
ws.on("close", () => {
console.log("Connection closed ✅");
stream.end();
});
},
);
})
.on("error", (err) => {
if (err.level === "client-authentication") {
ws.send(
`Authentication failed: Invalid SSH private key. ❌ Error: ${err.message} ${err.level}`,
);
} else {
ws.send(`SSH connection error: ${err.message}`);
}
conn.end();
})
.connect({
host: server.ipAddress,
port: server.port,
username: server.username,
privateKey: server.sshKey?.privateKey,
timeout: 99999,
});
});
};

View File

@@ -0,0 +1,12 @@
import os from "node:os";
export const getShell = () => {
switch (os.platform()) {
case "win32":
return "powershell.exe";
case "darwin":
return "zsh";
default:
return "bash";
}
};

View File

@@ -0,0 +1,22 @@
import type { Config } from "tailwindcss";
export default {
content: ["./app/**/{**,.client,.server}/**/*.{js,jsx,ts,tsx}"],
theme: {
extend: {
fontFamily: {
sans: [
'"Inter"',
"ui-sans-serif",
"system-ui",
"sans-serif",
'"Apple Color Emoji"',
'"Segoe UI Emoji"',
'"Segoe UI Symbol"',
'"Noto Color Emoji"',
],
},
},
},
plugins: [],
} satisfies Config;

34
apps/mig/tsconfig.json Normal file
View File

@@ -0,0 +1,34 @@
{
"include": [
"**/*.ts",
"**/*.tsx",
"**/.server/**/*.ts",
"**/.server/**/*.tsx",
"**/.client/**/*.ts",
"**/.client/**/*.tsx"
],
"compilerOptions": {
"lib": ["DOM", "DOM.Iterable", "ES2022"],
"types": ["@remix-run/node", "vite/client"],
"isolatedModules": true,
"esModuleInterop": true,
"jsx": "react-jsx",
"module": "ESNext",
"moduleResolution": "Bundler",
"resolveJsonModule": true,
"target": "ES2022",
"strict": true,
"allowJs": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"baseUrl": ".",
"paths": {
"~/*": ["./app/*"],
"@dokploy/server/*": ["../../packages/server/src/*"],
"@/server/*": ["./server/*"]
},
// Vite takes care of building everything, not tsc.
"noEmit": true
}
}

53
apps/mig/vite.config.ts Normal file
View File

@@ -0,0 +1,53 @@
import path from "node:path";
import { vitePlugin as remix } from "@remix-run/dev";
import esbuild from "esbuild";
import { defineConfig } from "vite";
import tsconfigPaths from "vite-tsconfig-paths";
declare module "@remix-run/node" {
interface Future {
v3_singleFetch: true;
}
}
export default defineConfig({
plugins: [
remix({
future: {
v3_fetcherPersist: true,
v3_relativeSplatPath: true,
v3_throwAbortReason: true,
v3_singleFetch: true,
v3_lazyRouteDiscovery: true,
},
serverBuildFile: "remix.js",
buildEnd: async () => {
await esbuild
.build({
alias: {
"~": "./app",
"@dokploy/server": "../../packages/server/src",
},
outfile: "build/server/index.js",
entryPoints: ["server/server.ts"],
external: ["./build/server/*"],
platform: "node",
format: "esm",
packages: "external",
bundle: true,
logLevel: "info",
})
.catch((error: unknown) => {
console.error("Error building server:", error);
process.exit(1);
});
},
}),
tsconfigPaths(),
],
resolve: {
alias: {
"@dokploy/server": path.resolve(__dirname, "../../packages/server/src"),
},
},
});

View File

@@ -1,7 +1,14 @@
{
"$schema": "./node_modules/@biomejs/biome/configuration_schema.json",
"files": {
"ignore": ["node_modules/**", ".next/**", "drizzle/**", ".docker", "dist"]
"ignore": [
"node_modules/**",
".next/**",
"drizzle/**",
".docker",
"dist",
"build"
]
},
"organizeImports": {
"enabled": true

View File

@@ -1,9 +1,15 @@
{
"name": "@dokploy/server",
"version": "1.0.0",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"main": "./src/index.ts",
"type": "module",
"exports": {
".": "./src/index.ts",
"./db": {
"import": "./src/db/index.ts",
"require": "./dist/db/index.cjs.js"
}
},
"scripts": {
"build": "rm -rf ./dist && tsc --project tsconfig.server.json && tsc-alias -p tsconfig.server.json",
"build:types": "tsc --emitDeclarationOnly --experimenta-dts",

View File

@@ -1,7 +1,7 @@
import { webcrypto } from "node:crypto";
import type { IncomingMessage, ServerResponse } from "node:http";
import { findAdminByAuthId } from "@/server/services/admin";
import { findUserByAuthId } from "@/server/services/user";
import { findAdminByAuthId } from "@dokploy/server/services/admin";
import { findUserByAuthId } from "@dokploy/server/services/user";
import { DrizzlePostgreSQLAdapter } from "@lucia-auth/adapter-drizzle";
import { TimeSpan } from "lucia";
import { Lucia } from "lucia/dist/core.js";

View File

@@ -1,11 +1,11 @@
import { randomBytes } from "node:crypto";
import { db } from "@/server/db";
import { db } from "@dokploy/server/db";
import {
admins,
type apiCreateUserInvitation,
auth,
users,
} from "@/server/db/schema";
} from "@dokploy/server/db/schema";
import { TRPCError } from "@trpc/server";
import * as bcrypt from "bcrypt";
import { eq } from "drizzle-orm";

View File

@@ -1,38 +1,41 @@
import { docker } from "@/server/constants";
import { db } from "@/server/db";
import { type apiCreateApplication, applications } from "@/server/db/schema";
import { generateAppName } from "@/server/db/schema";
import { getAdvancedStats } from "@/server/monitoring/utilts";
import { generatePassword } from "@/server/templates/utils";
import { docker } from "@dokploy/server/constants";
import { db } from "@dokploy/server/db";
import {
type apiCreateApplication,
applications,
} from "@dokploy/server/db/schema";
import { generateAppName } from "@dokploy/server/db/schema";
import { getAdvancedStats } from "@dokploy/server/monitoring/utilts";
import { generatePassword } from "@dokploy/server/templates/utils";
import {
buildApplication,
getBuildCommand,
mechanizeDockerContainer,
} from "@/server/utils/builders";
import { sendBuildErrorNotifications } from "@/server/utils/notifications/build-error";
import { sendBuildSuccessNotifications } from "@/server/utils/notifications/build-success";
import { execAsyncRemote } from "@/server/utils/process/execAsync";
} from "@dokploy/server/utils/builders";
import { sendBuildErrorNotifications } from "@dokploy/server/utils/notifications/build-error";
import { sendBuildSuccessNotifications } from "@dokploy/server/utils/notifications/build-success";
import { execAsyncRemote } from "@dokploy/server/utils/process/execAsync";
import {
cloneBitbucketRepository,
getBitbucketCloneCommand,
} from "@/server/utils/providers/bitbucket";
} from "@dokploy/server/utils/providers/bitbucket";
import {
buildDocker,
buildRemoteDocker,
} from "@/server/utils/providers/docker";
} from "@dokploy/server/utils/providers/docker";
import {
cloneGitRepository,
getCustomGitCloneCommand,
} from "@/server/utils/providers/git";
} from "@dokploy/server/utils/providers/git";
import {
cloneGithubRepository,
getGithubCloneCommand,
} from "@/server/utils/providers/github";
} from "@dokploy/server/utils/providers/github";
import {
cloneGitlabRepository,
getGitlabCloneCommand,
} from "@/server/utils/providers/gitlab";
import { createTraefikConfig } from "@/server/utils/traefik/application";
} from "@dokploy/server/utils/providers/gitlab";
import { createTraefikConfig } from "@dokploy/server/utils/traefik/application";
import { TRPCError } from "@trpc/server";
import { eq } from "drizzle-orm";
import { encodeBase64 } from "../utils/docker/utils";

View File

@@ -1,13 +1,13 @@
import { randomBytes } from "node:crypto";
import { db } from "@/server/db";
import { db } from "@dokploy/server/db";
import {
admins,
type apiCreateAdmin,
type apiCreateUser,
auth,
users,
} from "@/server/db/schema";
import { getPublicIpWithFallback } from "@/server/wss/terminal";
} from "@dokploy/server/db/schema";
import { getPublicIpWithFallback } from "@dokploy/server/wss/terminal";
import { TRPCError } from "@trpc/server";
import * as bcrypt from "bcrypt";
import { eq } from "drizzle-orm";

View File

@@ -1,5 +1,5 @@
import { db } from "@/server/db";
import { type apiCreateBackup, backups } from "@/server/db/schema";
import { db } from "@dokploy/server/db";
import { type apiCreateBackup, backups } from "@dokploy/server/db/schema";
import { TRPCError } from "@trpc/server";
import { eq } from "drizzle-orm";

View File

@@ -1,10 +1,10 @@
import { db } from "@/server/db";
import { db } from "@dokploy/server/db";
import {
type apiCreateBitbucket,
type apiUpdateBitbucket,
bitbucket,
gitProvider,
} from "@/server/db/schema";
} from "@dokploy/server/db/schema";
import { TRPCError } from "@trpc/server";
import { eq } from "drizzle-orm";

View File

@@ -1,9 +1,12 @@
import fs from "node:fs";
import path from "node:path";
import { paths } from "@/server/constants";
import { db } from "@/server/db";
import { type apiCreateCertificate, certificates } from "@/server/db/schema";
import { removeDirectoryIfExistsContent } from "@/server/utils/filesystem/directory";
import { paths } from "@dokploy/server/constants";
import { db } from "@dokploy/server/db";
import {
type apiCreateCertificate,
certificates,
} from "@dokploy/server/db/schema";
import { removeDirectoryIfExistsContent } from "@dokploy/server/utils/filesystem/directory";
import { TRPCError } from "@trpc/server";
import { eq } from "drizzle-orm";
import { dump } from "js-yaml";

View File

@@ -1,44 +1,47 @@
import { join } from "node:path";
import { paths } from "@/server/constants";
import { db } from "@/server/db";
import { type apiCreateCompose, compose } from "@/server/db/schema";
import { generateAppName } from "@/server/db/schema";
import { generatePassword } from "@/server/templates/utils";
import { paths } from "@dokploy/server/constants";
import { db } from "@dokploy/server/db";
import { type apiCreateCompose, compose } from "@dokploy/server/db/schema";
import { generateAppName } from "@dokploy/server/db/schema";
import { generatePassword } from "@dokploy/server/templates/utils";
import {
buildCompose,
getBuildComposeCommand,
} from "@/server/utils/builders/compose";
import { randomizeSpecificationFile } from "@/server/utils/docker/compose";
} from "@dokploy/server/utils/builders/compose";
import { randomizeSpecificationFile } from "@dokploy/server/utils/docker/compose";
import {
cloneCompose,
cloneComposeRemote,
loadDockerCompose,
loadDockerComposeRemote,
} from "@/server/utils/docker/domain";
import type { ComposeSpecification } from "@/server/utils/docker/types";
import { sendBuildErrorNotifications } from "@/server/utils/notifications/build-error";
import { sendBuildSuccessNotifications } from "@/server/utils/notifications/build-success";
import { execAsync, execAsyncRemote } from "@/server/utils/process/execAsync";
} from "@dokploy/server/utils/docker/domain";
import type { ComposeSpecification } from "@dokploy/server/utils/docker/types";
import { sendBuildErrorNotifications } from "@dokploy/server/utils/notifications/build-error";
import { sendBuildSuccessNotifications } from "@dokploy/server/utils/notifications/build-success";
import {
execAsync,
execAsyncRemote,
} from "@dokploy/server/utils/process/execAsync";
import {
cloneBitbucketRepository,
getBitbucketCloneCommand,
} from "@/server/utils/providers/bitbucket";
} from "@dokploy/server/utils/providers/bitbucket";
import {
cloneGitRepository,
getCustomGitCloneCommand,
} from "@/server/utils/providers/git";
} from "@dokploy/server/utils/providers/git";
import {
cloneGithubRepository,
getGithubCloneCommand,
} from "@/server/utils/providers/github";
} from "@dokploy/server/utils/providers/github";
import {
cloneGitlabRepository,
getGitlabCloneCommand,
} from "@/server/utils/providers/gitlab";
} from "@dokploy/server/utils/providers/gitlab";
import {
createComposeFile,
getCreateComposeFileCommand,
} from "@/server/utils/providers/raw";
} from "@dokploy/server/utils/providers/raw";
import { TRPCError } from "@trpc/server";
import { eq } from "drizzle-orm";
import { encodeBase64 } from "../utils/docker/utils";

View File

@@ -1,14 +1,14 @@
import { existsSync, promises as fsPromises } from "node:fs";
import path from "node:path";
import { paths } from "@/server/constants";
import { db } from "@/server/db";
import { paths } from "@dokploy/server/constants";
import { db } from "@dokploy/server/db";
import {
type apiCreateDeployment,
type apiCreateDeploymentCompose,
type apiCreateDeploymentServer,
deployments,
} from "@/server/db/schema";
import { removeDirectoryIfExistsContent } from "@/server/utils/filesystem/directory";
} from "@dokploy/server/db/schema";
import { removeDirectoryIfExistsContent } from "@dokploy/server/utils/filesystem/directory";
import { TRPCError } from "@trpc/server";
import { format } from "date-fns";
import { desc, eq } from "drizzle-orm";
@@ -20,7 +20,7 @@ import {
import { type Compose, findComposeById, updateCompose } from "./compose";
import { type Server, findServerById } from "./server";
import { execAsyncRemote } from "@/server/utils/process/execAsync";
import { execAsyncRemote } from "@dokploy/server/utils/process/execAsync";
export type Deployment = typeof deployments.$inferSelect;

View File

@@ -1,5 +1,8 @@
import { db } from "@/server/db";
import { type apiCreateDestination, destinations } from "@/server/db/schema";
import { db } from "@dokploy/server/db";
import {
type apiCreateDestination,
destinations,
} from "@dokploy/server/db/schema";
import { TRPCError } from "@trpc/server";
import { and, eq } from "drizzle-orm";

View File

@@ -1,4 +1,7 @@
import { execAsync, execAsyncRemote } from "@/server/utils/process/execAsync";
import {
execAsync,
execAsyncRemote,
} from "@dokploy/server/utils/process/execAsync";
export const getContainers = async (serverId?: string | null) => {
try {

View File

@@ -1,6 +1,6 @@
import { db } from "@/server/db";
import { generateRandomDomain } from "@/server/templates/utils";
import { manageDomain } from "@/server/utils/traefik/domain";
import { db } from "@dokploy/server/db";
import { generateRandomDomain } from "@dokploy/server/templates/utils";
import { manageDomain } from "@dokploy/server/utils/traefik/domain";
import { TRPCError } from "@trpc/server";
import { eq } from "drizzle-orm";
import { type apiCreateDomain, domains } from "../db/schema";

View File

@@ -1,5 +1,5 @@
import { db } from "@/server/db";
import { gitProvider } from "@/server/db/schema";
import { db } from "@dokploy/server/db";
import { gitProvider } from "@dokploy/server/db/schema";
import { TRPCError } from "@trpc/server";
import { eq } from "drizzle-orm";

View File

@@ -1,5 +1,9 @@
import { db } from "@/server/db";
import { type apiCreateGithub, gitProvider, github } from "@/server/db/schema";
import { db } from "@dokploy/server/db";
import {
type apiCreateGithub,
gitProvider,
github,
} from "@dokploy/server/db/schema";
import { TRPCError } from "@trpc/server";
import { eq } from "drizzle-orm";

View File

@@ -1,11 +1,11 @@
import { db } from "@/server/db";
import { db } from "@dokploy/server/db";
import {
type apiCreateGitlab,
type bitbucket,
gitProvider,
type github,
gitlab,
} from "@/server/db/schema";
} from "@dokploy/server/db/schema";
import { TRPCError } from "@trpc/server";
import { eq } from "drizzle-orm";

View File

@@ -1,14 +1,18 @@
import { db } from "@/server/db";
import { type apiCreateMariaDB, backups, mariadb } from "@/server/db/schema";
import { generateAppName } from "@/server/db/schema";
import { generatePassword } from "@/server/templates/utils";
import { buildMariadb } from "@/server/utils/databases/mariadb";
import { pullImage } from "@/server/utils/docker/utils";
import { db } from "@dokploy/server/db";
import {
type apiCreateMariaDB,
backups,
mariadb,
} from "@dokploy/server/db/schema";
import { generateAppName } from "@dokploy/server/db/schema";
import { generatePassword } from "@dokploy/server/templates/utils";
import { buildMariadb } from "@dokploy/server/utils/databases/mariadb";
import { pullImage } from "@dokploy/server/utils/docker/utils";
import { TRPCError } from "@trpc/server";
import { eq, getTableColumns } from "drizzle-orm";
import { validUniqueServerAppName } from "./project";
import { execAsyncRemote } from "@/server/utils/process/execAsync";
import { execAsyncRemote } from "@dokploy/server/utils/process/execAsync";
export type Mariadb = typeof mariadb.$inferSelect;

View File

@@ -1,14 +1,14 @@
import { db } from "@/server/db";
import { type apiCreateMongo, backups, mongo } from "@/server/db/schema";
import { generateAppName } from "@/server/db/schema";
import { generatePassword } from "@/server/templates/utils";
import { buildMongo } from "@/server/utils/databases/mongo";
import { pullImage } from "@/server/utils/docker/utils";
import { db } from "@dokploy/server/db";
import { type apiCreateMongo, backups, mongo } from "@dokploy/server/db/schema";
import { generateAppName } from "@dokploy/server/db/schema";
import { generatePassword } from "@dokploy/server/templates/utils";
import { buildMongo } from "@dokploy/server/utils/databases/mongo";
import { pullImage } from "@dokploy/server/utils/docker/utils";
import { TRPCError } from "@trpc/server";
import { eq, getTableColumns } from "drizzle-orm";
import { validUniqueServerAppName } from "./project";
import { execAsyncRemote } from "@/server/utils/process/execAsync";
import { execAsyncRemote } from "@dokploy/server/utils/process/execAsync";
export type Mongo = typeof mongo.$inferSelect;

View File

@@ -1,14 +1,17 @@
import path from "node:path";
import { paths } from "@/server/constants";
import { db } from "@/server/db";
import { paths } from "@dokploy/server/constants";
import { db } from "@dokploy/server/db";
import {
type ServiceType,
type apiCreateMount,
mounts,
} from "@/server/db/schema";
import { createFile, getCreateFileCommand } from "@/server/utils/docker/utils";
import { removeFileOrDirectory } from "@/server/utils/filesystem/directory";
import { execAsyncRemote } from "@/server/utils/process/execAsync";
} from "@dokploy/server/db/schema";
import {
createFile,
getCreateFileCommand,
} from "@dokploy/server/utils/docker/utils";
import { removeFileOrDirectory } from "@dokploy/server/utils/filesystem/directory";
import { execAsyncRemote } from "@dokploy/server/utils/process/execAsync";
import { TRPCError } from "@trpc/server";
import { type SQL, eq, sql } from "drizzle-orm";

View File

@@ -1,14 +1,14 @@
import { db } from "@/server/db";
import { type apiCreateMySql, backups, mysql } from "@/server/db/schema";
import { generateAppName } from "@/server/db/schema";
import { generatePassword } from "@/server/templates/utils";
import { buildMysql } from "@/server/utils/databases/mysql";
import { pullImage } from "@/server/utils/docker/utils";
import { db } from "@dokploy/server/db";
import { type apiCreateMySql, backups, mysql } from "@dokploy/server/db/schema";
import { generateAppName } from "@dokploy/server/db/schema";
import { generatePassword } from "@dokploy/server/templates/utils";
import { buildMysql } from "@dokploy/server/utils/databases/mysql";
import { pullImage } from "@dokploy/server/utils/docker/utils";
import { TRPCError } from "@trpc/server";
import { eq, getTableColumns } from "drizzle-orm";
import { validUniqueServerAppName } from "./project";
import { execAsyncRemote } from "@/server/utils/process/execAsync";
import { execAsyncRemote } from "@dokploy/server/utils/process/execAsync";
export type MySql = typeof mysql.$inferSelect;

View File

@@ -1,4 +1,4 @@
import { db } from "@/server/db";
import { db } from "@dokploy/server/db";
import {
type apiCreateDiscord,
type apiCreateEmail,
@@ -13,7 +13,7 @@ import {
notifications,
slack,
telegram,
} from "@/server/db/schema";
} from "@dokploy/server/db/schema";
import { TRPCError } from "@trpc/server";
import { eq } from "drizzle-orm";

View File

@@ -1,5 +1,5 @@
import { db } from "@/server/db";
import { type apiCreatePort, ports } from "@/server/db/schema";
import { db } from "@dokploy/server/db";
import { type apiCreatePort, ports } from "@dokploy/server/db/schema";
import { TRPCError } from "@trpc/server";
import { eq } from "drizzle-orm";

View File

@@ -1,14 +1,18 @@
import { db } from "@/server/db";
import { type apiCreatePostgres, backups, postgres } from "@/server/db/schema";
import { generateAppName } from "@/server/db/schema";
import { generatePassword } from "@/server/templates/utils";
import { buildPostgres } from "@/server/utils/databases/postgres";
import { pullImage } from "@/server/utils/docker/utils";
import { db } from "@dokploy/server/db";
import {
type apiCreatePostgres,
backups,
postgres,
} from "@dokploy/server/db/schema";
import { generateAppName } from "@dokploy/server/db/schema";
import { generatePassword } from "@dokploy/server/templates/utils";
import { buildPostgres } from "@dokploy/server/utils/databases/postgres";
import { pullImage } from "@dokploy/server/utils/docker/utils";
import { TRPCError } from "@trpc/server";
import { eq, getTableColumns } from "drizzle-orm";
import { validUniqueServerAppName } from "./project";
import { execAsyncRemote } from "@/server/utils/process/execAsync";
import { execAsyncRemote } from "@dokploy/server/utils/process/execAsync";
export type Postgres = typeof postgres.$inferSelect;

View File

@@ -1,4 +1,4 @@
import { db } from "@/server/db";
import { db } from "@dokploy/server/db";
import {
type apiCreateProject,
applications,
@@ -8,7 +8,7 @@ import {
postgres,
projects,
redis,
} from "@/server/db/schema";
} from "@dokploy/server/db/schema";
import { TRPCError } from "@trpc/server";
import { eq } from "drizzle-orm";

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