Merge pull request #531 from Dokploy/feat/cloud

Feat/cloud
This commit is contained in:
Mauricio Siu
2024-10-07 01:44:00 -06:00
committed by GitHub
337 changed files with 9994 additions and 2304 deletions

View File

@@ -11,6 +11,7 @@ jobs:
command: | command: |
cp apps/dokploy/.env.production.example .env.production cp apps/dokploy/.env.production.example .env.production
cp apps/dokploy/.env.production.example apps/dokploy/.env.production cp apps/dokploy/.env.production.example apps/dokploy/.env.production
- run: - run:
name: Build and push AMD64 image name: Build and push AMD64 image
command: | command: |

View File

@@ -48,3 +48,74 @@ jobs:
push: true push: true
tags: dokploy/website:latest tags: dokploy/website:latest
platforms: linux/amd64 platforms: linux/amd64
build-and-push-cloud-image:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v3
- name: Log in to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push Docker image
uses: docker/build-push-action@v4
with:
context: .
file: ./Dockerfile.cloud
push: true
tags: |
siumauricio/cloud:${{ github.ref_name == 'main' && 'main' || 'canary' }}
platforms: linux/amd64
build-and-push-schedule-image:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v3
- name: Log in to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push Docker image
uses: docker/build-push-action@v4
with:
context: .
file: ./Dockerfile.schedule
push: true
tags: |
siumauricio/schedule:${{ github.ref_name == 'main' && 'main' || 'canary' }}
platforms: linux/amd64
build-and-push-server-image:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v3
- name: Log in to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push Docker image
uses: docker/build-push-action@v4
with:
context: .
file: ./Dockerfile.server
push: true
tags: |
siumauricio/server:${{ github.ref_name == 'main' && 'main' || 'canary' }}
platforms: linux/amd64

View File

@@ -18,6 +18,7 @@ jobs:
node-version: 18.18.0 node-version: 18.18.0
cache: "pnpm" cache: "pnpm"
- run: pnpm install --frozen-lockfile - run: pnpm install --frozen-lockfile
- run: pnpm run server:build
- run: pnpm biome ci - run: pnpm biome ci
- run: pnpm typecheck - run: pnpm typecheck
@@ -32,6 +33,7 @@ jobs:
node-version: 18.18.0 node-version: 18.18.0
cache: "pnpm" cache: "pnpm"
- run: pnpm install --frozen-lockfile - run: pnpm install --frozen-lockfile
- run: pnpm run server:build
- run: pnpm build - run: pnpm build
parallel-tests: parallel-tests:
@@ -44,4 +46,5 @@ jobs:
node-version: 18.18.0 node-version: 18.18.0
cache: "pnpm" cache: "pnpm"
- run: pnpm install --frozen-lockfile - run: pnpm install --frozen-lockfile
- run: pnpm run server:build
- run: pnpm test - run: pnpm test

View File

@@ -15,7 +15,9 @@ RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --frozen-lockfile
# Deploy only the dokploy app # Deploy only the dokploy app
ENV NODE_ENV=production ENV NODE_ENV=production
RUN pnpm --filter=@dokploy/server build
RUN pnpm --filter=./apps/dokploy run build RUN pnpm --filter=./apps/dokploy run build
RUN pnpm --filter=./apps/dokploy --prod deploy /prod/dokploy RUN pnpm --filter=./apps/dokploy --prod deploy /prod/dokploy
RUN cp -R /usr/src/app/apps/dokploy/.next /prod/dokploy/.next RUN cp -R /usr/src/app/apps/dokploy/.next /prod/dokploy/.next

53
Dockerfile.cloud Normal file
View File

@@ -0,0 +1,53 @@
FROM node:18-slim AS base
ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH"
RUN corepack enable
FROM base AS build
COPY . /usr/src/app
WORKDIR /usr/src/app
RUN apt-get update && apt-get install -y python3 make g++ git && rm -rf /var/lib/apt/lists/*
# Install dependencies
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm --filter=@dokploy/server --filter=./apps/dokploy install --frozen-lockfile
# Deploy only the dokploy app
ENV NODE_ENV=production
RUN pnpm --filter=@dokploy/server build
RUN pnpm --filter=./apps/dokploy run build
RUN pnpm --filter=./apps/dokploy --prod deploy /prod/dokploy
RUN cp -R /usr/src/app/apps/dokploy/.next /prod/dokploy/.next
RUN cp -R /usr/src/app/apps/dokploy/dist /prod/dokploy/dist
FROM base AS dokploy
WORKDIR /app
# Set production
ENV NODE_ENV=production
RUN apt-get update && apt-get install -y curl unzip apache2-utils && rm -rf /var/lib/apt/lists/*
# Copy only the necessary files
COPY --from=build /prod/dokploy/.next ./.next
COPY --from=build /prod/dokploy/dist ./dist
COPY --from=build /prod/dokploy/next.config.mjs ./next.config.mjs
COPY --from=build /prod/dokploy/public ./public
COPY --from=build /prod/dokploy/package.json ./package.json
COPY --from=build /prod/dokploy/drizzle ./drizzle
COPY .env ./.env
COPY --from=build /prod/dokploy/components.json ./components.json
COPY --from=build /prod/dokploy/node_modules ./node_modules
# Install RCLONE
RUN curl https://rclone.org/install.sh | bash
# tsx
RUN pnpm install -g tsx
EXPOSE 3000
CMD [ "pnpm", "start" ]

36
Dockerfile.schedule Normal file
View File

@@ -0,0 +1,36 @@
FROM node:18-slim AS base
ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH"
RUN corepack enable
FROM base AS build
COPY . /usr/src/app
WORKDIR /usr/src/app
RUN apt-get update && apt-get install -y python3 make g++ git && rm -rf /var/lib/apt/lists/*
# Install dependencies
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm --filter=@dokploy/server --filter=./apps/schedules install --frozen-lockfile
# Deploy only the dokploy app
ENV NODE_ENV=production
RUN pnpm --filter=@dokploy/server build
RUN pnpm --filter=./apps/schedules run build
RUN pnpm --filter=./apps/schedules --prod deploy /prod/schedules
RUN cp -R /usr/src/app/apps/schedules/dist /prod/schedules/dist
FROM base AS dokploy
WORKDIR /app
# Set production
ENV NODE_ENV=production
# Copy only the necessary files
COPY --from=build /prod/schedules/dist ./dist
COPY --from=build /prod/schedules/package.json ./package.json
COPY --from=build /prod/schedules/node_modules ./node_modules
CMD HOSTNAME=0.0.0.0 && pnpm start

36
Dockerfile.server Normal file
View File

@@ -0,0 +1,36 @@
FROM node:18-slim AS base
ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH"
RUN corepack enable
FROM base AS build
COPY . /usr/src/app
WORKDIR /usr/src/app
RUN apt-get update && apt-get install -y python3 make g++ git && rm -rf /var/lib/apt/lists/*
# Install dependencies
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm --filter=@dokploy/server --filter=./apps/api install --frozen-lockfile
# Deploy only the dokploy app
ENV NODE_ENV=production
RUN pnpm --filter=@dokploy/server build
RUN pnpm --filter=./apps/api run build
RUN pnpm --filter=./apps/api --prod deploy /prod/api
RUN cp -R /usr/src/app/apps/api/dist /prod/api/dist
FROM base AS dokploy
WORKDIR /app
# Set production
ENV NODE_ENV=production
# Copy only the necessary files
COPY --from=build /prod/api/dist ./dist
COPY --from=build /prod/api/package.json ./package.json
COPY --from=build /prod/api/node_modules ./node_modules
CMD HOSTNAME=0.0.0.0 && pnpm start

View File

@@ -1,15 +1,33 @@
{ {
"name": "my-app", "name": "@dokploy/api",
"version": "0.0.1",
"type": "module",
"scripts": { "scripts": {
"dev": "tsx watch src/index.ts" "dev": "PORT=4000 tsx watch src/index.ts",
"build": "tsc --project tsconfig.json",
"start": "node --experimental-specifier-resolution=node dist/index.js",
"typecheck": "tsc --noEmit"
}, },
"dependencies": { "dependencies": {
"pino": "9.4.0",
"pino-pretty": "11.2.2",
"@hono/zod-validator": "0.3.0",
"zod": "^3.23.4",
"react": "18.2.0",
"react-dom": "18.2.0",
"@dokploy/server": "workspace:*",
"@hono/node-server": "^1.12.1", "@hono/node-server": "^1.12.1",
"hono": "^4.5.8", "hono": "^4.5.8",
"dotenv": "^16.3.1" "dotenv": "^16.3.1",
"redis": "4.7.0",
"@nerimity/mimiqueue": "1.2.3"
}, },
"devDependencies": { "devDependencies": {
"typescript": "^5.4.2",
"@types/react": "^18.2.37",
"@types/react-dom": "^18.2.15",
"@types/node": "^20.11.17", "@types/node": "^20.11.17",
"tsx": "^4.7.1" "tsx": "^4.7.1"
} },
"packageManager": "pnpm@9.5.0"
} }

View File

@@ -1,66 +1,61 @@
import { serve } from "@hono/node-server"; import { serve } from "@hono/node-server";
import { config } from "dotenv";
import { Hono } from "hono"; import { Hono } from "hono";
import { cors } from "hono/cors"; import "dotenv/config";
import { validateLemonSqueezyLicense } from "./utils"; import { zValidator } from "@hono/zod-validator";
import { Queue } from "@nerimity/mimiqueue";
config(); import { createClient } from "redis";
import { logger } from "./logger";
import { type DeployJob, deployJobSchema } from "./schema";
import { deploy } from "./utils";
const app = new Hono(); const app = new Hono();
const redisClient = createClient({
app.use( url: process.env.REDIS_URL,
"/*",
cors({
origin: ["http://localhost:3000", "http://localhost:3001"], // Ajusta esto a los orígenes de tu aplicación Next.js
allowMethods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"],
allowHeaders: ["Content-Type", "Authorization"],
exposeHeaders: ["Content-Length", "X-Kuma-Revision"],
maxAge: 600,
credentials: true,
}),
);
export const LEMON_SQUEEZY_API_KEY = process.env.LEMON_SQUEEZY_API_KEY;
export const LEMON_SQUEEZY_STORE_ID = process.env.LEMON_SQUEEZY_STORE_ID;
app.get("/v1/health", (c) => {
return c.text("Hello Hono!");
}); });
app.post("/v1/validate-license", async (c) => { app.use(async (c, next) => {
const { licenseKey } = await c.req.json(); if (c.req.path === "/health") {
return next();
}
const authHeader = c.req.header("X-API-Key");
if (!licenseKey) { if (process.env.API_KEY !== authHeader) {
return c.json({ error: "License key is required" }, 400); return c.json({ message: "Invalid API Key" }, 403);
} }
try { return next();
const licenseValidation = await validateLemonSqueezyLicense(licenseKey);
if (licenseValidation.valid) {
return c.json({
valid: true,
message: "License is valid",
metadata: licenseValidation.meta,
}); });
}
app.post("/deploy", zValidator("json", deployJobSchema), (c) => {
const data = c.req.valid("json");
const res = queue.add(data, { groupName: data.serverId });
return c.json( return c.json(
{ {
valid: false, message: "Deployment Added",
message: licenseValidation.error || "Invalid license",
}, },
400, 200,
); );
} catch (error) {
console.error("Error during license validation:", error);
return c.json({ error: "Internal server error" }, 500);
}
}); });
const port = 4000; app.get("/health", async (c) => {
console.log(`Server is running on port ${port}`); return c.json({ status: "ok" });
serve({
fetch: app.fetch,
port,
}); });
const queue = new Queue({
name: "deployments",
process: async (job: DeployJob) => {
logger.info("Deploying job", job);
return await deploy(job);
},
redisClient,
});
(async () => {
await redisClient.connect();
await redisClient.flushAll();
logger.info("Redis Cleaned");
})();
const port = Number.parseInt(process.env.PORT || "3000");
logger.info("Starting Deployments Server ✅", port);
serve({ fetch: app.fetch, port });

10
apps/api/src/logger.ts Normal file
View File

@@ -0,0 +1,10 @@
import pino from "pino";
export const logger = pino({
transport: {
target: "pino-pretty",
options: {
colorize: true,
},
},
});

24
apps/api/src/schema.ts Normal file
View File

@@ -0,0 +1,24 @@
import { z } from "zod";
export const deployJobSchema = z.discriminatedUnion("applicationType", [
z.object({
applicationId: z.string(),
titleLog: z.string(),
descriptionLog: z.string(),
server: z.boolean().optional(),
type: z.enum(["deploy", "redeploy"]),
applicationType: z.literal("application"),
serverId: z.string(),
}),
z.object({
composeId: z.string(),
titleLog: z.string(),
descriptionLog: z.string(),
server: z.boolean().optional(),
type: z.enum(["deploy", "redeploy"]),
applicationType: z.literal("compose"),
serverId: z.string(),
}),
]);
export type DeployJob = z.infer<typeof deployJobSchema>;

View File

@@ -1,28 +1,96 @@
import { LEMON_SQUEEZY_API_KEY, LEMON_SQUEEZY_STORE_ID } from "."; import {
deployApplication,
deployCompose,
deployRemoteApplication,
deployRemoteCompose,
rebuildApplication,
rebuildCompose,
rebuildRemoteApplication,
rebuildRemoteCompose,
updateApplicationStatus,
updateCompose,
} from "@dokploy/server";
import type { DeployJob } from "./schema";
import type { LemonSqueezyLicenseResponse } from "./types"; import type { LemonSqueezyLicenseResponse } from "./types";
export const validateLemonSqueezyLicense = async ( // const LEMON_SQUEEZY_API_KEY = process.env.LEMON_SQUEEZY_API_KEY;
licenseKey: string, // const LEMON_SQUEEZY_STORE_ID = process.env.LEMON_SQUEEZY_STORE_ID;
): Promise<LemonSqueezyLicenseResponse> => { // export const validateLemonSqueezyLicense = async (
try { // licenseKey: string,
const response = await fetch( // ): Promise<LemonSqueezyLicenseResponse> => {
"https://api.lemonsqueezy.com/v1/licenses/validate", // try {
{ // const response = await fetch(
method: "POST", // "https://api.lemonsqueezy.com/v1/licenses/validate",
headers: { // {
"Content-Type": "application/json", // method: "POST",
"x-api-key": LEMON_SQUEEZY_API_KEY as string, // headers: {
}, // "Content-Type": "application/json",
body: JSON.stringify({ // "x-api-key": LEMON_SQUEEZY_API_KEY as string,
license_key: licenseKey, // },
store_id: LEMON_SQUEEZY_STORE_ID as string, // body: JSON.stringify({
}), // license_key: licenseKey,
}, // store_id: LEMON_SQUEEZY_STORE_ID as string,
); // }),
// },
// );
return response.json(); // return response.json();
} catch (error) { // } catch (error) {
console.error("Error validating license:", error); // console.error("Error validating license:", error);
return { valid: false, error: "Error validating license" }; // return { valid: false, error: "Error validating license" };
// }
// };
export const deploy = async (job: DeployJob) => {
try {
if (job.applicationType === "application") {
await updateApplicationStatus(job.applicationId, "running");
if (job.server) {
if (job.type === "redeploy") {
await rebuildRemoteApplication({
applicationId: job.applicationId,
titleLog: job.titleLog,
descriptionLog: job.descriptionLog,
});
} else if (job.type === "deploy") {
await deployRemoteApplication({
applicationId: job.applicationId,
titleLog: job.titleLog,
descriptionLog: job.descriptionLog,
});
} }
}
} else if (job.applicationType === "compose") {
await updateCompose(job.composeId, {
composeStatus: "running",
});
if (job.server) {
if (job.type === "redeploy") {
await rebuildRemoteCompose({
composeId: job.composeId,
titleLog: job.titleLog,
descriptionLog: job.descriptionLog,
});
} else if (job.type === "deploy") {
await deployRemoteCompose({
composeId: job.composeId,
titleLog: job.titleLog,
descriptionLog: job.descriptionLog,
});
}
}
}
} catch (error) {
console.log(error);
if (job.applicationType === "application") {
await updateApplicationStatus(job.applicationId, "error");
} else if (job.applicationType === "compose") {
await updateCompose(job.composeId, {
composeStatus: "error",
});
}
}
return true;
}; };

View File

@@ -2,11 +2,12 @@
"compilerOptions": { "compilerOptions": {
"target": "ESNext", "target": "ESNext",
"module": "ESNext", "module": "ESNext",
"moduleResolution": "Bundler", "moduleResolution": "Node",
"strict": true, "strict": true,
"skipLibCheck": true, "skipLibCheck": true,
"types": ["node"], "outDir": "dist",
"jsx": "react-jsx", "jsx": "react-jsx",
"jsxImportSource": "hono/jsx" "jsxImportSource": "hono/jsx"
} },
"exclude": ["node_modules", "dist"]
} }

View File

@@ -21,15 +21,10 @@
"react-ga4": "^2.1.0" "react-ga4": "^2.1.0"
}, },
"devDependencies": { "devDependencies": {
"tsx": "4.15.7",
"@biomejs/biome": "1.8.1",
"@types/mdx": "^2.0.13", "@types/mdx": "^2.0.13",
"@types/node": "^20.14.2",
"@types/react": "^18.3.3", "@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
"autoprefixer": "^10.4.19",
"postcss": "^8.4.38", "postcss": "^8.4.38",
"tailwindcss": "^3.4.4", "tailwindcss": "^3.4.1",
"typescript": "^5.4.5" "typescript": "^5.4.5"
} }
} }

View File

@@ -1,3 +1,5 @@
DATABASE_URL="postgres://dokploy:amukds4wi9001583845717ad2@localhost:5432/dokploy" DATABASE_URL="postgres://dokploy:amukds4wi9001583845717ad2@localhost:5432/dokploy"
PORT=3000 PORT=3000
NODE_ENV=development NODE_ENV=development
IS_CLOUD="true"
SERVER_URL="http://localhost:4000"

View File

@@ -1,5 +1,5 @@
import { addSuffixToAllProperties } from "@/server/utils/docker/compose"; import { addSuffixToAllProperties } from "@dokploy/server";
import type { ComposeSpecification } from "@/server/utils/docker/types"; import type { ComposeSpecification } from "@dokploy/server";
import { load } from "js-yaml"; import { load } from "js-yaml";
import { expect, test } from "vitest"; import { expect, test } from "vitest";

View File

@@ -1,6 +1,6 @@
import { generateRandomHash } from "@/server/utils/docker/compose"; import { generateRandomHash } from "@dokploy/server";
import { addSuffixToConfigsRoot } from "@/server/utils/docker/compose/configs"; import { addSuffixToConfigsRoot } from "@dokploy/server";
import type { ComposeSpecification } from "@/server/utils/docker/types"; import type { ComposeSpecification } from "@dokploy/server";
import { load } from "js-yaml"; import { load } from "js-yaml";
import { expect, test } from "vitest"; import { expect, test } from "vitest";

View File

@@ -1,6 +1,6 @@
import { generateRandomHash } from "@/server/utils/docker/compose"; import { generateRandomHash } from "@dokploy/server";
import { addSuffixToConfigsInServices } from "@/server/utils/docker/compose/configs"; import { addSuffixToConfigsInServices } from "@dokploy/server";
import type { ComposeSpecification } from "@/server/utils/docker/types"; import type { ComposeSpecification } from "@dokploy/server";
import { load } from "js-yaml"; import { load } from "js-yaml";
import { expect, test } from "vitest"; import { expect, test } from "vitest";

View File

@@ -1,9 +1,6 @@
import { generateRandomHash } from "@/server/utils/docker/compose"; import { generateRandomHash } from "@dokploy/server";
import { import { addSuffixToAllConfigs, addSuffixToConfigsRoot } from "@dokploy/server";
addSuffixToAllConfigs, import type { ComposeSpecification } from "@dokploy/server";
addSuffixToConfigsRoot,
} from "@/server/utils/docker/compose/configs";
import type { ComposeSpecification } from "@/server/utils/docker/types";
import { load } from "js-yaml"; import { load } from "js-yaml";
import { expect, test } from "vitest"; import { expect, test } from "vitest";

View File

@@ -1,5 +1,5 @@
import type { Domain } from "@/server/api/services/domain"; import type { Domain } from "@dokploy/server";
import { createDomainLabels } from "@/server/utils/docker/domain"; import { createDomainLabels } from "@dokploy/server";
import { describe, expect, it } from "vitest"; import { describe, expect, it } from "vitest";
describe("createDomainLabels", () => { describe("createDomainLabels", () => {

View File

@@ -1,4 +1,4 @@
import { addDokployNetworkToRoot } from "@/server/utils/docker/domain"; import { addDokployNetworkToRoot } from "@dokploy/server";
import { describe, expect, it } from "vitest"; import { describe, expect, it } from "vitest";
describe("addDokployNetworkToRoot", () => { describe("addDokployNetworkToRoot", () => {

View File

@@ -1,4 +1,4 @@
import { addDokployNetworkToService } from "@/server/utils/docker/domain"; import { addDokployNetworkToService } from "@dokploy/server";
import { describe, expect, it } from "vitest"; import { describe, expect, it } from "vitest";
describe("addDokployNetworkToService", () => { describe("addDokployNetworkToService", () => {

View File

@@ -1,6 +1,6 @@
import { generateRandomHash } from "@/server/utils/docker/compose"; import { generateRandomHash } from "@dokploy/server";
import { addSuffixToNetworksRoot } from "@/server/utils/docker/compose/network"; import { addSuffixToNetworksRoot } from "@dokploy/server";
import type { ComposeSpecification } from "@/server/utils/docker/types"; import type { ComposeSpecification } from "@dokploy/server";
import { load } from "js-yaml"; import { load } from "js-yaml";
import { expect, test } from "vitest"; import { expect, test } from "vitest";

View File

@@ -1,6 +1,6 @@
import { generateRandomHash } from "@/server/utils/docker/compose"; import { generateRandomHash } from "@dokploy/server";
import { addSuffixToServiceNetworks } from "@/server/utils/docker/compose/network"; import { addSuffixToServiceNetworks } from "@dokploy/server";
import type { ComposeSpecification } from "@/server/utils/docker/types"; import type { ComposeSpecification } from "@dokploy/server";
import { load } from "js-yaml"; import { load } from "js-yaml";
import { expect, test } from "vitest"; import { expect, test } from "vitest";

View File

@@ -1,10 +1,10 @@
import { generateRandomHash } from "@/server/utils/docker/compose"; import { generateRandomHash } from "@dokploy/server";
import { import {
addSuffixToAllNetworks, addSuffixToAllNetworks,
addSuffixToServiceNetworks, addSuffixToServiceNetworks,
} from "@/server/utils/docker/compose/network"; } from "@dokploy/server";
import { addSuffixToNetworksRoot } from "@/server/utils/docker/compose/network"; import { addSuffixToNetworksRoot } from "@dokploy/server";
import type { ComposeSpecification } from "@/server/utils/docker/types"; import type { ComposeSpecification } from "@dokploy/server";
import { load } from "js-yaml"; import { load } from "js-yaml";
import { expect, test } from "vitest"; import { expect, test } from "vitest";

View File

@@ -1,6 +1,6 @@
import { generateRandomHash } from "@/server/utils/docker/compose"; import { generateRandomHash } from "@dokploy/server";
import { addSuffixToSecretsRoot } from "@/server/utils/docker/compose/secrets"; import { addSuffixToSecretsRoot } from "@dokploy/server";
import type { ComposeSpecification } from "@/server/utils/docker/types"; import type { ComposeSpecification } from "@dokploy/server";
import { dump, load } from "js-yaml"; import { dump, load } from "js-yaml";
import { expect, test } from "vitest"; import { expect, test } from "vitest";

View File

@@ -1,6 +1,6 @@
import { generateRandomHash } from "@/server/utils/docker/compose"; import { generateRandomHash } from "@dokploy/server";
import { addSuffixToSecretsInServices } from "@/server/utils/docker/compose/secrets"; import { addSuffixToSecretsInServices } from "@dokploy/server";
import type { ComposeSpecification } from "@/server/utils/docker/types"; import type { ComposeSpecification } from "@dokploy/server";
import { load } from "js-yaml"; import { load } from "js-yaml";
import { expect, test } from "vitest"; import { expect, test } from "vitest";

View File

@@ -1,5 +1,5 @@
import { addSuffixToAllSecrets } from "@/server/utils/docker/compose/secrets"; import { addSuffixToAllSecrets } from "@dokploy/server";
import type { ComposeSpecification } from "@/server/utils/docker/types"; import type { ComposeSpecification } from "@dokploy/server";
import { load } from "js-yaml"; import { load } from "js-yaml";
import { expect, test } from "vitest"; import { expect, test } from "vitest";

View File

@@ -1,6 +1,6 @@
import { generateRandomHash } from "@/server/utils/docker/compose"; import { generateRandomHash } from "@dokploy/server";
import { addSuffixToServiceNames } from "@/server/utils/docker/compose/service"; import { addSuffixToServiceNames } from "@dokploy/server";
import type { ComposeSpecification } from "@/server/utils/docker/types"; import type { ComposeSpecification } from "@dokploy/server";
import { load } from "js-yaml"; import { load } from "js-yaml";
import { expect, test } from "vitest"; import { expect, test } from "vitest";

View File

@@ -1,6 +1,6 @@
import { generateRandomHash } from "@/server/utils/docker/compose"; import { generateRandomHash } from "@dokploy/server";
import { addSuffixToServiceNames } from "@/server/utils/docker/compose/service"; import { addSuffixToServiceNames } from "@dokploy/server";
import type { ComposeSpecification } from "@/server/utils/docker/types"; import type { ComposeSpecification } from "@dokploy/server";
import { load } from "js-yaml"; import { load } from "js-yaml";
import { expect, test } from "vitest"; import { expect, test } from "vitest";

View File

@@ -1,6 +1,6 @@
import { generateRandomHash } from "@/server/utils/docker/compose"; import { generateRandomHash } from "@dokploy/server";
import { addSuffixToServiceNames } from "@/server/utils/docker/compose/service"; import { addSuffixToServiceNames } from "@dokploy/server";
import type { ComposeSpecification } from "@/server/utils/docker/types"; import type { ComposeSpecification } from "@dokploy/server";
import { load } from "js-yaml"; import { load } from "js-yaml";
import { expect, test } from "vitest"; import { expect, test } from "vitest";

View File

@@ -1,6 +1,6 @@
import { generateRandomHash } from "@/server/utils/docker/compose"; import { generateRandomHash } from "@dokploy/server";
import { addSuffixToServiceNames } from "@/server/utils/docker/compose/service"; import { addSuffixToServiceNames } from "@dokploy/server";
import type { ComposeSpecification } from "@/server/utils/docker/types"; import type { ComposeSpecification } from "@dokploy/server";
import { load } from "js-yaml"; import { load } from "js-yaml";
import { expect, test } from "vitest"; import { expect, test } from "vitest";

View File

@@ -1,6 +1,6 @@
import { generateRandomHash } from "@/server/utils/docker/compose"; import { generateRandomHash } from "@dokploy/server";
import { addSuffixToServiceNames } from "@/server/utils/docker/compose/service"; import { addSuffixToServiceNames } from "@dokploy/server";
import type { ComposeSpecification } from "@/server/utils/docker/types"; import type { ComposeSpecification } from "@dokploy/server";
import { load } from "js-yaml"; import { load } from "js-yaml";
import { expect, test } from "vitest"; import { expect, test } from "vitest";

View File

@@ -1,8 +1,8 @@
import { import {
addSuffixToAllServiceNames, addSuffixToAllServiceNames,
addSuffixToServiceNames, addSuffixToServiceNames,
} from "@/server/utils/docker/compose/service"; } from "@dokploy/server";
import type { ComposeSpecification } from "@/server/utils/docker/types"; import type { ComposeSpecification } from "@dokploy/server";
import { load } from "js-yaml"; import { load } from "js-yaml";
import { expect, test } from "vitest"; import { expect, test } from "vitest";

View File

@@ -1,6 +1,6 @@
import { generateRandomHash } from "@/server/utils/docker/compose"; import { generateRandomHash } from "@dokploy/server";
import { addSuffixToServiceNames } from "@/server/utils/docker/compose/service"; import { addSuffixToServiceNames } from "@dokploy/server";
import type { ComposeSpecification } from "@/server/utils/docker/types"; import type { ComposeSpecification } from "@dokploy/server";
import { load } from "js-yaml"; import { load } from "js-yaml";
import { expect, test } from "vitest"; import { expect, test } from "vitest";

View File

@@ -1,9 +1,6 @@
import { generateRandomHash } from "@/server/utils/docker/compose"; import { generateRandomHash } from "@dokploy/server";
import { import { addSuffixToAllVolumes, addSuffixToVolumesRoot } from "@dokploy/server";
addSuffixToAllVolumes, import type { ComposeSpecification } from "@dokploy/server";
addSuffixToVolumesRoot,
} from "@/server/utils/docker/compose/volume";
import type { ComposeSpecification } from "@/server/utils/docker/types";
import { load } from "js-yaml"; import { load } from "js-yaml";
import { expect, test } from "vitest"; import { expect, test } from "vitest";

View File

@@ -1,6 +1,6 @@
import { generateRandomHash } from "@/server/utils/docker/compose"; import { generateRandomHash } from "@dokploy/server";
import { addSuffixToVolumesRoot } from "@/server/utils/docker/compose/volume"; import { addSuffixToVolumesRoot } from "@dokploy/server";
import type { ComposeSpecification } from "@/server/utils/docker/types"; import type { ComposeSpecification } from "@dokploy/server";
import { load } from "js-yaml"; import { load } from "js-yaml";
import { expect, test } from "vitest"; import { expect, test } from "vitest";

View File

@@ -1,6 +1,6 @@
import { generateRandomHash } from "@/server/utils/docker/compose"; import { generateRandomHash } from "@dokploy/server";
import { addSuffixToVolumesInServices } from "@/server/utils/docker/compose/volume"; import { addSuffixToVolumesInServices } from "@dokploy/server";
import type { ComposeSpecification } from "@/server/utils/docker/types"; import type { ComposeSpecification } from "@dokploy/server";
import { load } from "js-yaml"; import { load } from "js-yaml";
import { expect, test } from "vitest"; import { expect, test } from "vitest";

View File

@@ -1,9 +1,9 @@
import { generateRandomHash } from "@/server/utils/docker/compose"; import { generateRandomHash } from "@dokploy/server";
import { import {
addSuffixToAllVolumes, addSuffixToAllVolumes,
addSuffixToVolumesInServices, addSuffixToVolumesInServices,
} from "@/server/utils/docker/compose/volume"; } from "@dokploy/server";
import type { ComposeSpecification } from "@/server/utils/docker/types"; import type { ComposeSpecification } from "@dokploy/server";
import { load } from "js-yaml"; import { load } from "js-yaml";
import { expect, test } from "vitest"; import { expect, test } from "vitest";

View File

@@ -1,9 +1,9 @@
import fs from "node:fs/promises"; import fs from "node:fs/promises";
import path from "node:path"; import path from "node:path";
import { paths } from "@/server/constants"; import { paths } from "@dokploy/server/dist/constants";
const { APPLICATIONS_PATH } = paths(); const { APPLICATIONS_PATH } = paths();
import type { ApplicationNested } from "@/server/utils/builders"; import type { ApplicationNested } from "@dokploy/server";
import { unzipDrop } from "@/server/utils/builders/drop"; import { unzipDrop } from "@dokploy/server";
import AdmZip from "adm-zip"; import AdmZip from "adm-zip";
import { afterAll, beforeAll, describe, expect, it, vi } from "vitest"; import { afterAll, beforeAll, describe, expect, it, vi } from "vitest";
@@ -81,14 +81,17 @@ const baseApp: ApplicationNested = {
username: null, username: null,
dockerContextPath: null, dockerContextPath: null,
}; };
//
vi.mock("@/server/constants", () => ({ vi.mock("@dokploy/server/dist/constants", async (importOriginal) => {
const actual = await importOriginal();
return {
// @ts-ignore
...actual,
paths: () => ({ paths: () => ({
APPLICATIONS_PATH: "./__test__/drop/zips/output", APPLICATIONS_PATH: "./__test__/drop/zips/output",
}), }),
// APPLICATIONS_PATH: "./__test__/drop/zips/output", };
})); });
describe("unzipDrop using real zip files", () => { describe("unzipDrop using real zip files", () => {
// const { APPLICATIONS_PATH } = paths(); // const { APPLICATIONS_PATH } = paths();
beforeAll(async () => { beforeAll(async () => {
@@ -102,15 +105,19 @@ describe("unzipDrop using real zip files", () => {
it("should correctly extract a zip with a single root folder", async () => { it("should correctly extract a zip with a single root folder", async () => {
baseApp.appName = "single-file"; baseApp.appName = "single-file";
// const appName = "single-file"; // const appName = "single-file";
try {
const outputPath = path.join(APPLICATIONS_PATH, baseApp.appName, "code"); const outputPath = path.join(APPLICATIONS_PATH, baseApp.appName, "code");
const zip = new AdmZip("./__test__/drop/zips/single-file.zip"); const zip = new AdmZip("./__test__/drop/zips/single-file.zip");
console.log(`Output Path: ${outputPath}`);
const zipBuffer = zip.toBuffer(); const zipBuffer = zip.toBuffer();
const file = new File([zipBuffer], "single.zip"); const file = new File([zipBuffer], "single.zip");
await unzipDrop(file, baseApp); await unzipDrop(file, baseApp);
const files = await fs.readdir(outputPath, { withFileTypes: true }); const files = await fs.readdir(outputPath, { withFileTypes: true });
expect(files.some((f) => f.name === "test.txt")).toBe(true); expect(files.some((f) => f.name === "test.txt")).toBe(true);
} catch (err) {
console.log(err);
} finally {
}
}); });
it("should correctly extract a zip with a single root folder and a subfolder", async () => { it("should correctly extract a zip with a single root folder and a subfolder", async () => {

View File

@@ -1,4 +1,4 @@
import { parseRawConfig, processLogs } from "@/server/utils/access-log/utils"; import { parseRawConfig, processLogs } from "@dokploy/server";
import { describe, expect, it } from "vitest"; import { describe, expect, it } from "vitest";
const sampleLogEntry = `{"ClientAddr":"172.19.0.1:56732","ClientHost":"172.19.0.1","ClientPort":"56732","ClientUsername":"-","DownstreamContentSize":0,"DownstreamStatus":304,"Duration":14729375,"OriginContentSize":0,"OriginDuration":14051833,"OriginStatus":304,"Overhead":677542,"RequestAddr":"s222-umami-c381af.traefik.me","RequestContentSize":0,"RequestCount":122,"RequestHost":"s222-umami-c381af.traefik.me","RequestMethod":"GET","RequestPath":"/dashboard?_rsc=1rugv","RequestPort":"-","RequestProtocol":"HTTP/1.1","RequestScheme":"http","RetryAttempts":0,"RouterName":"s222-umami-60e104-47-web@docker","ServiceAddr":"10.0.1.15:3000","ServiceName":"s222-umami-60e104-47-web@docker","ServiceURL":{"Scheme":"http","Opaque":"","User":null,"Host":"10.0.1.15:3000","Path":"","RawPath":"","ForceQuery":false,"RawQuery":"","Fragment":"","RawFragment":""},"StartLocal":"2024-08-25T04:34:37.306691884Z","StartUTC":"2024-08-25T04:34:37.306691884Z","entryPointName":"web","level":"info","msg":"","time":"2024-08-25T04:34:37Z"}`; const sampleLogEntry = `{"ClientAddr":"172.19.0.1:56732","ClientHost":"172.19.0.1","ClientPort":"56732","ClientUsername":"-","DownstreamContentSize":0,"DownstreamStatus":304,"Duration":14729375,"OriginContentSize":0,"OriginDuration":14051833,"OriginStatus":304,"Overhead":677542,"RequestAddr":"s222-umami-c381af.traefik.me","RequestContentSize":0,"RequestCount":122,"RequestHost":"s222-umami-c381af.traefik.me","RequestMethod":"GET","RequestPath":"/dashboard?_rsc=1rugv","RequestPort":"-","RequestProtocol":"HTTP/1.1","RequestScheme":"http","RetryAttempts":0,"RouterName":"s222-umami-60e104-47-web@docker","ServiceAddr":"10.0.1.15:3000","ServiceName":"s222-umami-60e104-47-web@docker","ServiceURL":{"Scheme":"http","Opaque":"","User":null,"Host":"10.0.1.15:3000","Path":"","RawPath":"","ForceQuery":false,"RawQuery":"","Fragment":"","RawFragment":""},"StartLocal":"2024-08-25T04:34:37.306691884Z","StartUTC":"2024-08-25T04:34:37.306691884Z","entryPointName":"web","level":"info","msg":"","time":"2024-08-25T04:34:37Z"}`;

View File

@@ -5,11 +5,12 @@ vi.mock("node:fs", () => ({
default: fs, default: fs,
})); }));
import type { Admin } from "@/server/api/services/admin"; import type { Admin, FileConfig } from "@dokploy/server";
import { createDefaultServerTraefikConfig } from "@/server/setup/traefik-setup"; import {
import { loadOrCreateConfig } from "@/server/utils/traefik/application"; createDefaultServerTraefikConfig,
import type { FileConfig } from "@/server/utils/traefik/file-types"; loadOrCreateConfig,
import { updateServerTraefik } from "@/server/utils/traefik/web-server"; updateServerTraefik,
} from "@dokploy/server";
import { beforeEach, expect, test, vi } from "vitest"; import { beforeEach, expect, test, vi } from "vitest";
const baseAdmin: Admin = { const baseAdmin: Admin = {

View File

@@ -1,7 +1,7 @@
import type { Domain } from "@/server/api/services/domain"; import type { Domain } from "@dokploy/server";
import type { Redirect } from "@/server/api/services/redirect"; import type { Redirect } from "@dokploy/server";
import type { ApplicationNested } from "@/server/utils/builders"; import type { ApplicationNested } from "@dokploy/server";
import { createRouterConfig } from "@/server/utils/traefik/domain"; import { createRouterConfig } from "@dokploy/server";
import { expect, test } from "vitest"; import { expect, test } from "vitest";
const baseApp: ApplicationNested = { const baseApp: ApplicationNested = {

View File

@@ -13,4 +13,9 @@ export default defineConfig({
exclude: ["**/node_modules/**", "**/dist/**", "**/.docker/**"], exclude: ["**/node_modules/**", "**/dist/**", "**/.docker/**"],
pool: "forks", pool: "forks",
}, },
define: {
"process.env": {
NODE: "test",
},
},
}); });

View File

@@ -16,20 +16,37 @@ interface Props {
export const ShowDeployment = ({ logPath, open, onClose, serverId }: Props) => { export const ShowDeployment = ({ logPath, open, onClose, serverId }: Props) => {
const [data, setData] = useState(""); const [data, setData] = useState("");
const endOfLogsRef = useRef<HTMLDivElement>(null); const endOfLogsRef = useRef<HTMLDivElement>(null);
const wsRef = useRef<WebSocket | null>(null); // Ref to hold WebSocket instance
useEffect(() => { useEffect(() => {
if (!open || !logPath) return; if (!open || !logPath) return;
setData("");
const protocol = window.location.protocol === "https:" ? "wss:" : "ws:"; const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
const wsUrl = `${protocol}//${window.location.host}/listen-deployment?logPath=${logPath}${serverId ? `&serverId=${serverId}` : ""}`; const wsUrl = `${protocol}//${window.location.host}/listen-deployment?logPath=${logPath}${serverId ? `&serverId=${serverId}` : ""}`;
const ws = new WebSocket(wsUrl); const ws = new WebSocket(wsUrl);
wsRef.current = ws; // Store WebSocket instance in ref
ws.onmessage = (e) => { ws.onmessage = (e) => {
setData((currentData) => currentData + e.data); setData((currentData) => currentData + e.data);
}; };
return () => ws.close(); ws.onerror = (error) => {
console.error("WebSocket error: ", error);
};
ws.onclose = () => {
console.log("WebSocket connection closed");
wsRef.current = null; // Clear reference on close
};
return () => {
if (wsRef.current?.readyState === WebSocket.OPEN) {
ws.close();
wsRef.current = null;
}
};
}, [logPath, open]); }, [logPath, open]);
const scrollToBottom = () => { const scrollToBottom = () => {
@@ -45,7 +62,15 @@ export const ShowDeployment = ({ logPath, open, onClose, serverId }: Props) => {
open={open} open={open}
onOpenChange={(e) => { onOpenChange={(e) => {
onClose(); onClose();
if (!e) setData(""); if (!e) {
setData("");
}
if (wsRef.current) {
if (wsRef.current.readyState === WebSocket.OPEN) {
wsRef.current.close();
}
}
}} }}
> >
<DialogContent className={"sm:max-w-5xl overflow-y-auto max-h-screen"}> <DialogContent className={"sm:max-w-5xl overflow-y-auto max-h-screen"}>

View File

@@ -21,20 +21,38 @@ export const ShowDeploymentCompose = ({
}: Props) => { }: Props) => {
const [data, setData] = useState(""); const [data, setData] = useState("");
const endOfLogsRef = useRef<HTMLDivElement>(null); const endOfLogsRef = useRef<HTMLDivElement>(null);
const wsRef = useRef<WebSocket | null>(null); // Ref to hold WebSocket instance
useEffect(() => { useEffect(() => {
if (!open || !logPath) return; if (!open || !logPath) return;
setData("");
const protocol = window.location.protocol === "https:" ? "wss:" : "ws:"; const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
const wsUrl = `${protocol}//${window.location.host}/listen-deployment?logPath=${logPath}&serverId=${serverId}`; const wsUrl = `${protocol}//${window.location.host}/listen-deployment?logPath=${logPath}&serverId=${serverId}`;
const ws = new WebSocket(wsUrl); const ws = new WebSocket(wsUrl);
wsRef.current = ws; // Store WebSocket instance in ref
ws.onmessage = (e) => { ws.onmessage = (e) => {
setData((currentData) => currentData + e.data); setData((currentData) => currentData + e.data);
}; };
return () => ws.close(); ws.onerror = (error) => {
console.error("WebSocket error: ", error);
};
ws.onclose = () => {
console.log("WebSocket connection closed");
wsRef.current = null;
};
return () => {
if (wsRef.current?.readyState === WebSocket.OPEN) {
ws.close();
wsRef.current = null;
}
};
}, [logPath, open]); }, [logPath, open]);
const scrollToBottom = () => { const scrollToBottom = () => {
@@ -50,7 +68,15 @@ export const ShowDeploymentCompose = ({
open={open} open={open}
onOpenChange={(e) => { onOpenChange={(e) => {
onClose(); onClose();
if (!e) setData(""); if (!e) {
setData("");
}
if (wsRef.current) {
if (wsRef.current.readyState === WebSocket.OPEN) {
wsRef.current.close();
}
}
}} }}
> >
<DialogContent className={"sm:max-w-5xl overflow-y-auto max-h-screen"}> <DialogContent className={"sm:max-w-5xl overflow-y-auto max-h-screen"}>

View File

@@ -77,7 +77,6 @@ export const ComposeFileEditor = ({ composeId }: Props) => {
}); });
}) })
.catch((e) => { .catch((e) => {
console.log(e);
toast.error("Error to update the compose config"); toast.error("Error to update the compose config");
}); });
}; };

View File

@@ -1,7 +1,7 @@
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
import { Terminal } from "@xterm/xterm"; import { Terminal } from "@xterm/xterm";
import React, { useEffect } from "react"; import React, { useEffect, useRef } from "react";
import { FitAddon } from "xterm-addon-fit"; import { FitAddon } from "xterm-addon-fit";
import "@xterm/xterm/css/xterm.css"; import "@xterm/xterm/css/xterm.css";
@@ -18,12 +18,24 @@ export const DockerLogsId: React.FC<Props> = ({
}) => { }) => {
const [term, setTerm] = React.useState<Terminal>(); const [term, setTerm] = React.useState<Terminal>();
const [lines, setLines] = React.useState<number>(40); const [lines, setLines] = React.useState<number>(40);
const wsRef = useRef<WebSocket | null>(null); // Ref to hold WebSocket instance
const createTerminal = (): Terminal => { useEffect(() => {
// if (containerId === "select-a-container") {
// return;
// }
const container = document.getElementById(id); const container = document.getElementById(id);
if (container) { if (container) {
container.innerHTML = ""; container.innerHTML = "";
} }
if (wsRef.current) {
console.log(wsRef.current);
if (wsRef.current.readyState === WebSocket.OPEN) {
wsRef.current.close();
}
wsRef.current = null;
}
const termi = new Terminal({ const termi = new Terminal({
cursorBlink: true, cursorBlink: true,
cols: 80, cols: 80,
@@ -45,7 +57,7 @@ export const DockerLogsId: React.FC<Props> = ({
const wsUrl = `${protocol}//${window.location.host}/docker-container-logs?containerId=${containerId}&tail=${lines}${serverId ? `&serverId=${serverId}` : ""}`; const wsUrl = `${protocol}//${window.location.host}/docker-container-logs?containerId=${containerId}&tail=${lines}${serverId ? `&serverId=${serverId}` : ""}`;
const ws = new WebSocket(wsUrl); const ws = new WebSocket(wsUrl);
wsRef.current = ws;
const fitAddon = new FitAddon(); const fitAddon = new FitAddon();
termi.loadAddon(fitAddon); termi.loadAddon(fitAddon);
// @ts-ignore // @ts-ignore
@@ -54,6 +66,10 @@ export const DockerLogsId: React.FC<Props> = ({
termi.focus(); termi.focus();
setTerm(termi); setTerm(termi);
ws.onerror = (error) => {
console.error("WebSocket error: ", error);
};
ws.onmessage = (e) => { ws.onmessage = (e) => {
termi.write(e.data); termi.write(e.data);
}; };
@@ -62,12 +78,14 @@ export const DockerLogsId: React.FC<Props> = ({
console.log(e.reason); console.log(e.reason);
termi.write(`Connection closed!\nReason: ${e.reason}\n`); termi.write(`Connection closed!\nReason: ${e.reason}\n`);
wsRef.current = null;
}; };
return termi; return () => {
if (wsRef.current?.readyState === WebSocket.OPEN) {
ws.close();
wsRef.current = null;
}
}; };
useEffect(() => {
createTerminal();
}, [lines, containerId]); }, [lines, containerId]);
useEffect(() => { useEffect(() => {

View File

@@ -11,13 +11,11 @@ import {
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { Switch } from "@/components/ui/switch"; import { Switch } from "@/components/ui/switch";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { useUrl } from "@/utils/hooks/use-url";
import { format } from "date-fns"; import { format } from "date-fns";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
export const AddGithubProvider = () => { export const AddGithubProvider = () => {
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
const url = useUrl();
const { data } = api.auth.get.useQuery(); const { data } = api.auth.get.useQuery();
const [manifest, setManifest] = useState(""); const [manifest, setManifest] = useState("");
const [isOrganization, setIsOrganization] = useState(false); const [isOrganization, setIsOrganization] = useState(false);

View File

@@ -145,7 +145,6 @@ export const ProfileForm = () => {
<FormControl> <FormControl>
<RadioGroup <RadioGroup
onValueChange={(e) => { onValueChange={(e) => {
console.log(e);
field.onChange(e); field.onChange(e);
}} }}
defaultValue={field.value} defaultValue={field.value}

View File

@@ -19,7 +19,6 @@ import {
DialogTrigger, DialogTrigger,
} from "@/components/ui/dialog"; } from "@/components/ui/dialog";
import { DropdownMenuItem } from "@/components/ui/dropdown-menu"; import { DropdownMenuItem } from "@/components/ui/dropdown-menu";
import { Separator } from "@/components/ui/separator";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import copy from "copy-to-clipboard"; import copy from "copy-to-clipboard";

View File

@@ -1,7 +1,7 @@
import { AddProject } from "@/components/dashboard/projects/add"; import { AddProject } from "@/components/dashboard/projects/add";
import type { Auth } from "@/server/api/services/auth";
import type { User } from "@/server/api/services/user";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import type { Auth, IS_CLOUD, User } from "@dokploy/server";
import { is } from "drizzle-orm";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { useEffect, useMemo, useState } from "react"; import { useEffect, useMemo, useState } from "react";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "../ui/tabs"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "../ui/tabs";
@@ -11,6 +11,7 @@ interface TabInfo {
tabLabel?: string; tabLabel?: string;
description: string; description: string;
index: string; index: string;
type: TabState;
isShow?: ({ rol, user }: { rol?: Auth["rol"]; user?: User }) => boolean; isShow?: ({ rol, user }: { rol?: Auth["rol"]; user?: User }) => boolean;
} }
@@ -22,18 +23,25 @@ export type TabState =
| "requests" | "requests"
| "docker"; | "docker";
const tabMap: Record<TabState, TabInfo> = { const getTabMaps = (isCloud: boolean) => {
projects: { const elements: TabInfo[] = [
{
label: "Projects", label: "Projects",
description: "Manage your projects", description: "Manage your projects",
index: "/dashboard/projects", index: "/dashboard/projects",
type: "projects",
}, },
monitoring: { ];
if (!isCloud) {
elements.push(
{
label: "Monitoring", label: "Monitoring",
description: "Monitor your projects", description: "Monitor your projects",
index: "/dashboard/monitoring", index: "/dashboard/monitoring",
type: "monitoring",
}, },
traefik: { {
label: "Traefik", label: "Traefik",
tabLabel: "Traefik File System", tabLabel: "Traefik File System",
description: "Manage your traefik", description: "Manage your traefik",
@@ -41,28 +49,39 @@ const tabMap: Record<TabState, TabInfo> = {
isShow: ({ rol, user }) => { isShow: ({ rol, user }) => {
return Boolean(rol === "admin" || user?.canAccessToTraefikFiles); return Boolean(rol === "admin" || user?.canAccessToTraefikFiles);
}, },
type: "traefik",
}, },
docker: { {
label: "Docker", label: "Docker",
description: "Manage your docker", description: "Manage your docker",
index: "/dashboard/docker", index: "/dashboard/docker",
isShow: ({ rol, user }) => { isShow: ({ rol, user }) => {
return Boolean(rol === "admin" || user?.canAccessToDocker); return Boolean(rol === "admin" || user?.canAccessToDocker);
}, },
type: "docker",
}, },
requests: { {
label: "Requests", label: "Requests",
description: "Manage your requests", description: "Manage your requests",
index: "/dashboard/requests", index: "/dashboard/requests",
isShow: ({ rol, user }) => { isShow: ({ rol, user }) => {
return Boolean(rol === "admin" || user?.canAccessToDocker); return Boolean(rol === "admin" || user?.canAccessToDocker);
}, },
type: "requests",
}, },
settings: { );
}
elements.push({
label: "Settings", label: "Settings",
description: "Manage your settings", description: "Manage your settings",
index: "/dashboard/settings/server", type: "settings",
}, index: isCloud
? "/dashboard/settings/profile"
: "/dashboard/settings/server",
});
return elements;
}; };
interface Props { interface Props {
@@ -72,9 +91,10 @@ interface Props {
export const NavigationTabs = ({ tab, children }: Props) => { export const NavigationTabs = ({ tab, children }: Props) => {
const router = useRouter(); const router = useRouter();
const { data } = api.auth.get.useQuery(); const { data } = api.auth.get.useQuery();
const [activeTab, setActiveTab] = useState<TabState>(tab); const [activeTab, setActiveTab] = useState<TabState>(tab);
const { data: isCloud } = api.settings.isCloud.useQuery();
const tabMap = useMemo(() => getTabMaps(isCloud ?? false), [isCloud]);
const { data: user } = api.user.byAuthId.useQuery( const { data: user } = api.user.byAuthId.useQuery(
{ {
authId: data?.id || "", authId: data?.id || "",
@@ -89,7 +109,7 @@ export const NavigationTabs = ({ tab, children }: Props) => {
}, [tab]); }, [tab]);
const activeTabInfo = useMemo(() => { const activeTabInfo = useMemo(() => {
return tabMap[activeTab]; return tabMap.find((tab) => tab.type === activeTab);
}, [activeTab]); }, [activeTab]);
return ( return (
@@ -97,10 +117,10 @@ export const NavigationTabs = ({ tab, children }: Props) => {
<header className="mb-6 flex w-full items-center gap-2 justify-between flex-wrap"> <header className="mb-6 flex w-full items-center gap-2 justify-between flex-wrap">
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<h1 className="text-xl font-bold lg:text-3xl"> <h1 className="text-xl font-bold lg:text-3xl">
{activeTabInfo.label} {activeTabInfo?.label}
</h1> </h1>
<p className="lg:text-medium text-muted-foreground"> <p className="lg:text-medium text-muted-foreground">
{activeTabInfo.description} {activeTabInfo?.description}
</p> </p>
</div> </div>
{tab === "projects" && {tab === "projects" &&
@@ -112,27 +132,26 @@ export const NavigationTabs = ({ tab, children }: Props) => {
className="w-full" className="w-full"
onValueChange={async (e) => { onValueChange={async (e) => {
setActiveTab(e as TabState); setActiveTab(e as TabState);
router.push(tabMap[e as TabState].index); const tab = tabMap.find((tab) => tab.type === e);
router.push(tab?.index || "");
}} }}
> >
{/* className="grid w-fit grid-cols-4 bg-transparent" */}
<div className="flex flex-row items-center justify-between w-full gap-4 max-sm:overflow-x-auto border-b border-b-divider pb-1"> <div className="flex flex-row items-center justify-between w-full gap-4 max-sm:overflow-x-auto border-b border-b-divider pb-1">
<TabsList className="bg-transparent relative px-0"> <TabsList className="bg-transparent relative px-0">
{Object.keys(tabMap).map((key) => { {tabMap.map((tab, index) => {
const tab = tabMap[key as TabState]; if (tab?.isShow && !tab?.isShow?.({ rol: data?.rol, user })) {
if (tab.isShow && !tab.isShow?.({ rol: data?.rol, user })) {
return null; return null;
} }
return ( return (
<TabsTrigger <TabsTrigger
key={key} key={tab.type}
value={key} value={tab.type}
className="relative py-2.5 md:px-5 data-[state=active]:shadow-none data-[state=active]:bg-transparent rounded-md hover:bg-zinc-100 hover:dark:bg-zinc-800 data-[state=active]:hover:bg-zinc-100 data-[state=active]:hover:dark:bg-zinc-800" className="relative py-2.5 md:px-5 data-[state=active]:shadow-none data-[state=active]:bg-transparent rounded-md hover:bg-zinc-100 hover:dark:bg-zinc-800 data-[state=active]:hover:bg-zinc-100 data-[state=active]:hover:dark:bg-zinc-800"
> >
<span className="relative z-[1] w-full"> <span className="relative z-[1] w-full">
{tab.tabLabel || tab.label} {tab?.tabLabel || tab?.label}
</span> </span>
{key === activeTab && ( {tab.type === activeTab && (
<div className="absolute -bottom-[5.5px] w-full"> <div className="absolute -bottom-[5.5px] w-full">
<div className="h-0.5 bg-foreground rounded-t-md" /> <div className="h-0.5 bg-foreground rounded-t-md" />
</div> </div>

View File

@@ -4,6 +4,7 @@ interface Props {
export const SettingsLayout = ({ children }: Props) => { export const SettingsLayout = ({ children }: Props) => {
const { data } = api.auth.get.useQuery(); const { data } = api.auth.get.useQuery();
const { data: isCloud } = api.settings.isCloud.useQuery();
const { data: user } = api.user.byAuthId.useQuery( const { data: user } = api.user.byAuthId.useQuery(
{ {
authId: data?.id || "", authId: data?.id || "",
@@ -17,7 +18,7 @@ export const SettingsLayout = ({ children }: Props) => {
<div className="md:max-w-[18rem] w-full"> <div className="md:max-w-[18rem] w-full">
<Nav <Nav
links={[ links={[
...(data?.rol === "admin" ...(data?.rol === "admin" && !isCloud
? [ ? [
{ {
title: "Server", title: "Server",
@@ -47,12 +48,16 @@ export const SettingsLayout = ({ children }: Props) => {
icon: Database, icon: Database,
href: "/dashboard/settings/destinations", href: "/dashboard/settings/destinations",
}, },
...(!isCloud
? [
{ {
title: "Certificates", title: "Certificates",
label: "", label: "",
icon: ShieldCheck, icon: ShieldCheck,
href: "/dashboard/settings/certificates", href: "/dashboard/settings/certificates",
}, },
]
: []),
{ {
title: "SSH Keys", title: "SSH Keys",
label: "", label: "",
@@ -71,12 +76,23 @@ export const SettingsLayout = ({ children }: Props) => {
icon: Users, icon: Users,
href: "/dashboard/settings/users", href: "/dashboard/settings/users",
}, },
{
title: "Registry",
label: "",
icon: ListMusic,
href: "/dashboard/settings/registry",
},
...(!isCloud
? [
{ {
title: "Cluster", title: "Cluster",
label: "", label: "",
icon: BoxesIcon, icon: BoxesIcon,
href: "/dashboard/settings/cluster", href: "/dashboard/settings/cluster",
}, },
]
: []),
{ {
title: "Notifications", title: "Notifications",
label: "", label: "",
@@ -128,6 +144,7 @@ import {
GitBranch, GitBranch,
KeyIcon, KeyIcon,
KeyRound, KeyRound,
ListMusic,
type LucideIcon, type LucideIcon,
Route, Route,
Server, Server,

View File

@@ -0,0 +1,25 @@
ALTER TABLE "git_provider" DROP CONSTRAINT "git_provider_authId_auth_id_fk";
--> statement-breakpoint
ALTER TABLE "notification" ADD COLUMN "adminId" text;--> statement-breakpoint
ALTER TABLE "ssh-key" ADD COLUMN "privateKey" text DEFAULT '' NOT NULL;--> statement-breakpoint
ALTER TABLE "ssh-key" ADD COLUMN "adminId" text;--> statement-breakpoint
ALTER TABLE "git_provider" ADD COLUMN "adminId" text;--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "notification" ADD CONSTRAINT "notification_adminId_admin_adminId_fk" FOREIGN KEY ("adminId") REFERENCES "public"."admin"("adminId") ON DELETE cascade ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "ssh-key" ADD CONSTRAINT "ssh-key_adminId_admin_adminId_fk" FOREIGN KEY ("adminId") REFERENCES "public"."admin"("adminId") ON DELETE cascade ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "git_provider" ADD CONSTRAINT "git_provider_adminId_admin_adminId_fk" FOREIGN KEY ("adminId") REFERENCES "public"."admin"("adminId") ON DELETE cascade ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
--> statement-breakpoint
ALTER TABLE "git_provider" DROP COLUMN IF EXISTS "authId";

File diff suppressed because it is too large Load Diff

View File

@@ -274,6 +274,13 @@
"when": 1727942090102, "when": 1727942090102,
"tag": "0038_rapid_landau", "tag": "0038_rapid_landau",
"breakpoints": true "breakpoints": true
},
{
"idx": 39,
"version": "6",
"when": 1728021127765,
"tag": "0039_many_tiger_shark",
"breakpoints": true
} }
] ]
} }

View File

@@ -18,6 +18,7 @@ const nextConfig = {
typescript: { typescript: {
ignoreBuildErrors: true, ignoreBuildErrors: true,
}, },
transpilePackages: ["@dokploy/server"],
webpack: (config) => { webpack: (config) => {
config.plugins.push( config.plugins.push(
new CopyWebpackPlugin({ new CopyWebpackPlugin({

View File

@@ -6,7 +6,7 @@
"type": "module", "type": "module",
"scripts": { "scripts": {
"build": "npm run build-server && npm run build-next", "build": "npm run build-server && npm run build-next",
"start": "node dist/server.mjs", "start": "node -r dotenv/config dist/server.mjs",
"build-server": "tsx esbuild.config.ts", "build-server": "tsx esbuild.config.ts",
"build-next": "next build", "build-next": "next build",
"setup": "tsx -r dotenv/config setup.ts && sleep 5 && pnpm run migration:run", "setup": "tsx -r dotenv/config setup.ts && sleep 5 && pnpm run migration:run",
@@ -34,17 +34,14 @@
"test": "vitest --config __test__/vitest.config.ts" "test": "vitest --config __test__/vitest.config.ts"
}, },
"dependencies": { "dependencies": {
"rotating-file-stream": "3.2.3", "@dokploy/server": "workspace:*",
"@codemirror/lang-json": "^6.0.1", "@codemirror/lang-json": "^6.0.1",
"@codemirror/lang-yaml": "^6.1.1", "@codemirror/lang-yaml": "^6.1.1",
"@codemirror/language": "^6.10.1", "@codemirror/language": "^6.10.1",
"@codemirror/legacy-modes": "6.4.0", "@codemirror/legacy-modes": "6.4.0",
"@codemirror/view": "6.29.0", "@codemirror/view": "6.29.0",
"@dokploy/trpc-openapi": "0.0.4", "@dokploy/trpc-openapi": "0.0.4",
"@faker-js/faker": "^8.4.1",
"@hookform/resolvers": "^3.3.4", "@hookform/resolvers": "^3.3.4",
"@lucia-auth/adapter-drizzle": "1.0.7",
"@octokit/auth-app": "^6.0.4",
"@octokit/webhooks": "^13.2.7", "@octokit/webhooks": "^13.2.7",
"@radix-ui/react-accordion": "1.1.2", "@radix-ui/react-accordion": "1.1.2",
"@radix-ui/react-alert-dialog": "^1.0.5", "@radix-ui/react-alert-dialog": "^1.0.5",
@@ -64,7 +61,6 @@
"@radix-ui/react-tabs": "^1.0.4", "@radix-ui/react-tabs": "^1.0.4",
"@radix-ui/react-toggle": "^1.0.3", "@radix-ui/react-toggle": "^1.0.3",
"@radix-ui/react-tooltip": "^1.0.7", "@radix-ui/react-tooltip": "^1.0.7",
"@react-email/components": "^0.0.21",
"@tanstack/react-query": "^4.36.1", "@tanstack/react-query": "^4.36.1",
"@tanstack/react-table": "^8.16.0", "@tanstack/react-table": "^8.16.0",
"@trpc/client": "^10.43.6", "@trpc/client": "^10.43.6",
@@ -77,8 +73,6 @@
"@xterm/xterm": "^5.4.0", "@xterm/xterm": "^5.4.0",
"adm-zip": "^0.5.14", "adm-zip": "^0.5.14",
"bcrypt": "5.1.1", "bcrypt": "5.1.1",
"bl": "6.0.11",
"boxen": "^7.1.1",
"bullmq": "5.4.2", "bullmq": "5.4.2",
"class-variance-authority": "^0.7.0", "class-variance-authority": "^0.7.0",
"clsx": "^2.1.0", "clsx": "^2.1.0",
@@ -86,31 +80,22 @@
"copy-to-clipboard": "^3.3.3", "copy-to-clipboard": "^3.3.3",
"copy-webpack-plugin": "^12.0.2", "copy-webpack-plugin": "^12.0.2",
"date-fns": "3.6.0", "date-fns": "3.6.0",
"dockerode": "4.0.2",
"dockerode-compose": "^1.4.0",
"dockerstats": "2.4.2",
"dotenv": "16.4.5", "dotenv": "16.4.5",
"drizzle-orm": "^0.30.8", "drizzle-orm": "^0.30.8",
"drizzle-zod": "0.5.1", "drizzle-zod": "0.5.1",
"hi-base32": "^0.5.1",
"input-otp": "^1.2.4", "input-otp": "^1.2.4",
"js-yaml": "4.1.0", "js-yaml": "4.1.0",
"k6": "^0.0.0",
"lodash": "4.17.21", "lodash": "4.17.21",
"lucia": "^3.0.1", "lucia": "^3.0.1",
"lucide-react": "^0.312.0", "lucide-react": "^0.312.0",
"nanoid": "3", "nanoid": "3",
"next": "^14.1.3", "next": "^14.1.3",
"next-themes": "^0.2.1", "next-themes": "^0.2.1",
"node-os-utils": "1.3.7",
"node-pty": "1.0.0", "node-pty": "1.0.0",
"node-schedule": "2.1.1", "node-schedule": "2.1.1",
"nodemailer": "6.9.14",
"octokit": "3.1.2", "octokit": "3.1.2",
"otpauth": "^9.2.3",
"postgres": "3.4.4", "postgres": "3.4.4",
"public-ip": "6.0.2", "public-ip": "6.0.2",
"qrcode": "^1.5.3",
"react": "18.2.0", "react": "18.2.0",
"react-dom": "18.2.0", "react-dom": "18.2.0",
"react-hook-form": "^7.49.3", "react-hook-form": "^7.49.3",
@@ -121,53 +106,35 @@
"swagger-ui-react": "^5.17.14", "swagger-ui-react": "^5.17.14",
"tailwind-merge": "^2.2.0", "tailwind-merge": "^2.2.0",
"tailwindcss-animate": "^1.0.7", "tailwindcss-animate": "^1.0.7",
"tar-fs": "3.0.5",
"undici": "^6.19.2", "undici": "^6.19.2",
"use-resize-observer": "9.1.0", "use-resize-observer": "9.1.0",
"ws": "8.16.0", "ws": "8.16.0",
"xterm-addon-fit": "^0.8.0", "xterm-addon-fit": "^0.8.0",
"zod": "^3.23.4", "zod": "^3.23.4",
"zod-form-data": "^2.0.2", "zod-form-data": "^2.0.2",
"@radix-ui/react-primitive": "2.0.0",
"@radix-ui/react-use-controllable-state": "1.1.0",
"ssh2": "1.15.0" "ssh2": "1.15.0"
}, },
"devDependencies": { "devDependencies": {
"@biomejs/biome": "1.8.3", "autoprefixer": "10.4.12",
"@commitlint/cli": "^19.3.0",
"@commitlint/config-conventional": "^19.2.2",
"@types/adm-zip": "^0.5.5", "@types/adm-zip": "^0.5.5",
"@types/bcrypt": "5.0.2", "@types/bcrypt": "5.0.2",
"@types/dockerode": "3.3.23",
"@types/js-yaml": "4.0.9", "@types/js-yaml": "4.0.9",
"@types/lodash": "4.17.4", "@types/lodash": "4.17.4",
"@types/node": "^18.17.0", "@types/node": "^18.17.0",
"@types/node-os-utils": "1.3.4",
"@types/node-schedule": "2.1.6", "@types/node-schedule": "2.1.6",
"@types/nodemailer": "^6.4.15",
"@types/qrcode": "^1.5.5",
"@types/react": "^18.2.37", "@types/react": "^18.2.37",
"@types/react-dom": "^18.2.15", "@types/react-dom": "^18.2.15",
"@types/swagger-ui-react": "^4.18.3", "@types/swagger-ui-react": "^4.18.3",
"@types/tar-fs": "2.0.4",
"@types/ws": "8.5.10", "@types/ws": "8.5.10",
"autoprefixer": "^10.4.14",
"drizzle-kit": "^0.21.1", "drizzle-kit": "^0.21.1",
"esbuild": "0.20.2", "esbuild": "0.20.2",
"husky": "^9.0.11",
"lint-staged": "^15.2.7", "lint-staged": "^15.2.7",
"localtunnel": "2.0.2",
"memfs": "^4.11.0", "memfs": "^4.11.0",
"postcss": "^8.4.31",
"prettier": "^3.2.4",
"prettier-plugin-tailwindcss": "^0.5.11",
"tailwindcss": "^3.4.1", "tailwindcss": "^3.4.1",
"tsconfig-paths": "4.2.0",
"tsx": "^4.7.0", "tsx": "^4.7.0",
"typescript": "^5.4.2", "typescript": "^5.4.2",
"vite-tsconfig-paths": "4.3.2", "vite-tsconfig-paths": "4.3.2",
"vitest": "^1.6.0", "vitest": "^1.6.0",
"xterm-readline": "1.1.1",
"@types/ssh2": "1.15.1" "@types/ssh2": "1.15.1"
}, },
"ct3aMetadata": { "ct3aMetadata": {

View File

@@ -10,7 +10,6 @@ interface Props {
export default function Custom404({ statusCode, error }: Props) { export default function Custom404({ statusCode, error }: Props) {
const displayStatusCode = statusCode || 400; const displayStatusCode = statusCode || 400;
console.log(error, statusCode);
return ( return (
<div className="h-screen"> <div className="h-screen">
<div className="max-w-[50rem] flex flex-col mx-auto size-full"> <div className="max-w-[50rem] flex flex-col mx-auto size-full">
@@ -92,7 +91,6 @@ export default function Custom404({ statusCode, error }: Props) {
// @ts-ignore // @ts-ignore
Error.getInitialProps = ({ res, err, ...rest }: NextPageContext) => { Error.getInitialProps = ({ res, err, ...rest }: NextPageContext) => {
console.log(err, rest);
const statusCode = res ? res.statusCode : err ? err.statusCode : 404; const statusCode = res ? res.statusCode : err ? err.statusCode : 404;
return { statusCode, error: err }; return { statusCode, error: err };
}; };

View File

@@ -1,7 +1,6 @@
import { appRouter } from "@/server/api/root"; import { appRouter } from "@/server/api/root";
import { createTRPCContext } from "@/server/api/trpc"; import { createTRPCContext } from "@/server/api/trpc";
import { validateRequest } from "@/server/auth/auth"; import { validateBearerToken, validateRequest } from "@dokploy/server";
import { validateBearerToken } from "@/server/auth/token";
import { createOpenApiNextHandler } from "@dokploy/trpc-openapi"; import { createOpenApiNextHandler } from "@dokploy/trpc-openapi";
import type { NextApiRequest, NextApiResponse } from "next"; import type { NextApiRequest, NextApiResponse } from "next";
@@ -18,6 +17,7 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
res.status(401).json({ message: "Unauthorized" }); res.status(401).json({ message: "Unauthorized" });
return; return;
} }
// @ts-ignore // @ts-ignore
return createOpenApiNextHandler({ return createOpenApiNextHandler({
router: appRouter, router: appRouter,

View File

@@ -2,6 +2,8 @@ import { db } from "@/server/db";
import { applications } from "@/server/db/schema"; import { applications } from "@/server/db/schema";
import type { DeploymentJob } from "@/server/queues/deployments-queue"; import type { DeploymentJob } from "@/server/queues/deployments-queue";
import { myQueue } from "@/server/queues/queueSetup"; import { myQueue } from "@/server/queues/queueSetup";
import { deploy } from "@/server/utils/deploy";
import { IS_CLOUD } from "@dokploy/server";
import { eq } from "drizzle-orm"; import { eq } from "drizzle-orm";
import type { NextApiRequest, NextApiResponse } from "next"; import type { NextApiRequest, NextApiResponse } from "next";
@@ -89,6 +91,12 @@ export default async function handler(
applicationType: "application", applicationType: "application",
server: !!application.serverId, server: !!application.serverId,
}; };
if (IS_CLOUD && application.serverId) {
jobData.serverId = application.serverId;
await deploy(jobData);
return true;
}
await myQueue.add( await myQueue.add(
"deployments", "deployments",
{ ...jobData }, { ...jobData },

View File

@@ -2,6 +2,8 @@ import { db } from "@/server/db";
import { compose } from "@/server/db/schema"; import { compose } from "@/server/db/schema";
import type { DeploymentJob } from "@/server/queues/deployments-queue"; import type { DeploymentJob } from "@/server/queues/deployments-queue";
import { myQueue } from "@/server/queues/queueSetup"; import { myQueue } from "@/server/queues/queueSetup";
import { deploy } from "@/server/utils/deploy";
import { IS_CLOUD } from "@dokploy/server";
import { eq } from "drizzle-orm"; import { eq } from "drizzle-orm";
import type { NextApiRequest, NextApiResponse } from "next"; import type { NextApiRequest, NextApiResponse } from "next";
import { import {
@@ -65,6 +67,12 @@ export default async function handler(
descriptionLog: `Hash: ${deploymentHash}`, descriptionLog: `Hash: ${deploymentHash}`,
server: !!composeResult.serverId, server: !!composeResult.serverId,
}; };
if (IS_CLOUD && composeResult.serverId) {
jobData.serverId = composeResult.serverId;
await deploy(jobData);
return true;
}
await myQueue.add( await myQueue.add(
"deployments", "deployments",
{ ...jobData }, { ...jobData },

View File

@@ -1,8 +1,9 @@
import { findAdmin } from "@/server/api/services/admin";
import { db } from "@/server/db"; import { db } from "@/server/db";
import { applications, compose, github } from "@/server/db/schema"; import { applications, compose, github } from "@/server/db/schema";
import type { DeploymentJob } from "@/server/queues/deployments-queue"; import type { DeploymentJob } from "@/server/queues/deployments-queue";
import { myQueue } from "@/server/queues/queueSetup"; import { myQueue } from "@/server/queues/queueSetup";
import { deploy } from "@/server/utils/deploy";
import { IS_CLOUD } from "@dokploy/server";
import { Webhooks } from "@octokit/webhooks"; import { Webhooks } from "@octokit/webhooks";
import { and, eq } from "drizzle-orm"; import { and, eq } from "drizzle-orm";
import type { NextApiRequest, NextApiResponse } from "next"; import type { NextApiRequest, NextApiResponse } from "next";
@@ -12,17 +13,10 @@ export default async function handler(
req: NextApiRequest, req: NextApiRequest,
res: NextApiResponse, res: NextApiResponse,
) { ) {
const admin = await findAdmin();
if (!admin) {
res.status(200).json({ message: "Could not find admin" });
return;
}
const signature = req.headers["x-hub-signature-256"]; const signature = req.headers["x-hub-signature-256"];
const githubBody = req.body; const githubBody = req.body;
if (!githubBody?.installation.id) { if (!githubBody?.installation?.id) {
res.status(400).json({ message: "Github Installation not found" }); res.status(400).json({ message: "Github Installation not found" });
return; return;
} }
@@ -88,6 +82,12 @@ export default async function handler(
applicationType: "application", applicationType: "application",
server: !!app.serverId, server: !!app.serverId,
}; };
if (IS_CLOUD && app.serverId) {
jobData.serverId = app.serverId;
await deploy(jobData);
return true;
}
await myQueue.add( await myQueue.add(
"deployments", "deployments",
{ ...jobData }, { ...jobData },
@@ -116,6 +116,12 @@ export default async function handler(
descriptionLog: `Hash: ${deploymentHash}`, descriptionLog: `Hash: ${deploymentHash}`,
}; };
if (IS_CLOUD && composeApp.serverId) {
jobData.serverId = composeApp.serverId;
await deploy(jobData);
return true;
}
await myQueue.add( await myQueue.add(
"deployments", "deployments",
{ ...jobData }, { ...jobData },

View File

@@ -1,6 +1,11 @@
import { createGithub } from "@/server/api/services/github";
import { db } from "@/server/db"; import { db } from "@/server/db";
import { github } from "@/server/db/schema"; import { github } from "@/server/db/schema";
import {
createGithub,
findAdminByAuthId,
findAuthById,
findUserByAuthId,
} from "@dokploy/server";
import { eq } from "drizzle-orm"; import { eq } from "drizzle-orm";
import type { NextApiRequest, NextApiResponse } from "next"; import type { NextApiRequest, NextApiResponse } from "next";
import { Octokit } from "octokit"; import { Octokit } from "octokit";
@@ -34,7 +39,19 @@ export default async function handler(
}, },
); );
await createGithub({ const auth = await findAuthById(value as string);
let adminId = "";
if (auth.rol === "admin") {
const admin = await findAdminByAuthId(auth.id);
adminId = admin.adminId;
} else {
const user = await findUserByAuthId(auth.id);
adminId = user.adminId;
}
await createGithub(
{
name: data.name, name: data.name,
githubAppName: data.html_url, githubAppName: data.html_url,
githubAppId: data.id, githubAppId: data.id,
@@ -42,8 +59,9 @@ export default async function handler(
githubClientSecret: data.client_secret, githubClientSecret: data.client_secret,
githubWebhookSecret: data.webhook_secret, githubWebhookSecret: data.webhook_secret,
githubPrivateKey: data.pem, githubPrivateKey: data.pem,
authId: value as string, },
}); adminId,
);
} else if (action === "gh_setup") { } else if (action === "gh_setup") {
await db await db
.update(github) .update(github)

View File

@@ -1,4 +1,4 @@
import { findGitlabById, updateGitlab } from "@/server/api/services/gitlab"; import { findGitlabById, updateGitlab } from "@dokploy/server";
import type { NextApiRequest, NextApiResponse } from "next"; import type { NextApiRequest, NextApiResponse } from "next";
export default async function handler( export default async function handler(

View File

@@ -1,18 +0,0 @@
import type { NextRequest } from "next/server";
import { renderToString } from "react-dom/server";
import Page418 from "../hola"; // Importa la página 418
export const GET = async (req: NextRequest) => {
// Renderiza el componente de la página 418 como HTML
const htmlContent = renderToString(Page418());
// Devuelve la respuesta con el código de estado HTTP 418
return new Response(htmlContent, {
headers: {
"Content-Type": "text/html",
},
status: 418,
});
};
export default GET;

View File

@@ -1,7 +1,7 @@
import { ShowContainers } from "@/components/dashboard/docker/show/show-containers"; import { ShowContainers } from "@/components/dashboard/docker/show/show-containers";
import { DashboardLayout } from "@/components/layouts/dashboard-layout"; import { DashboardLayout } from "@/components/layouts/dashboard-layout";
import { appRouter } from "@/server/api/root"; import { appRouter } from "@/server/api/root";
import { validateRequest } from "@/server/auth/auth"; import { IS_CLOUD, validateRequest } from "@dokploy/server";
import { createServerSideHelpers } from "@trpc/react-query/server"; import { createServerSideHelpers } from "@trpc/react-query/server";
import type { GetServerSidePropsContext } from "next"; import type { GetServerSidePropsContext } from "next";
import React, { type ReactElement } from "react"; import React, { type ReactElement } from "react";
@@ -19,6 +19,14 @@ Dashboard.getLayout = (page: ReactElement) => {
export async function getServerSideProps( export async function getServerSideProps(
ctx: GetServerSidePropsContext<{ serviceId: string }>, ctx: GetServerSidePropsContext<{ serviceId: string }>,
) { ) {
if (IS_CLOUD) {
return {
redirect: {
permanent: true,
destination: "/dashboard/projects",
},
};
}
const { user, session } = await validateRequest(ctx.req, ctx.res); const { user, session } = await validateRequest(ctx.req, ctx.res);
if (!user) { if (!user) {
return { return {
@@ -28,7 +36,7 @@ export async function getServerSideProps(
}, },
}; };
} }
const { req, res, resolvedUrl } = ctx; const { req, res } = ctx;
const helpers = createServerSideHelpers({ const helpers = createServerSideHelpers({
router: appRouter, router: appRouter,

View File

@@ -1,6 +1,6 @@
import { ShowMonitoring } from "@/components/dashboard/monitoring/web-server/show"; import { ShowMonitoring } from "@/components/dashboard/monitoring/web-server/show";
import { DashboardLayout } from "@/components/layouts/dashboard-layout"; import { DashboardLayout } from "@/components/layouts/dashboard-layout";
import { validateRequest } from "@/server/auth/auth"; import { IS_CLOUD, validateRequest } from "@dokploy/server";
import type { GetServerSidePropsContext } from "next"; import type { GetServerSidePropsContext } from "next";
import React, { type ReactElement } from "react"; import React, { type ReactElement } from "react";
@@ -16,6 +16,14 @@ Dashboard.getLayout = (page: ReactElement) => {
export async function getServerSideProps( export async function getServerSideProps(
ctx: GetServerSidePropsContext<{ serviceId: string }>, ctx: GetServerSidePropsContext<{ serviceId: string }>,
) { ) {
if (IS_CLOUD) {
return {
redirect: {
permanent: true,
destination: "/dashboard/projects",
},
};
}
const { user } = await validateRequest(ctx.req, ctx.res); const { user } = await validateRequest(ctx.req, ctx.res);
if (!user) { if (!user) {
return { return {

View File

@@ -29,9 +29,9 @@ import {
DropdownMenuTrigger, DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"; } from "@/components/ui/dropdown-menu";
import { appRouter } from "@/server/api/root"; import { appRouter } from "@/server/api/root";
import type { findProjectById } from "@/server/api/services/project";
import { validateRequest } from "@/server/auth/auth";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import type { findProjectById } from "@dokploy/server";
import { validateRequest } from "@dokploy/server";
import { createServerSideHelpers } from "@trpc/react-query/server"; import { createServerSideHelpers } from "@trpc/react-query/server";
import { CircuitBoard, FolderInput, GlobeIcon, PlusIcon } from "lucide-react"; import { CircuitBoard, FolderInput, GlobeIcon, PlusIcon } from "lucide-react";
import type { import type {

View File

@@ -25,8 +25,8 @@ import {
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { appRouter } from "@/server/api/root"; import { appRouter } from "@/server/api/root";
import { validateRequest } from "@/server/auth/auth";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { validateRequest } from "@dokploy/server";
import { createServerSideHelpers } from "@trpc/react-query/server"; import { createServerSideHelpers } from "@trpc/react-query/server";
import { GlobeIcon } from "lucide-react"; import { GlobeIcon } from "lucide-react";
import type { import type {

View File

@@ -19,8 +19,8 @@ import {
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { appRouter } from "@/server/api/root"; import { appRouter } from "@/server/api/root";
import { validateRequest } from "@/server/auth/auth";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { validateRequest } from "@dokploy/server";
import { createServerSideHelpers } from "@trpc/react-query/server"; import { createServerSideHelpers } from "@trpc/react-query/server";
import { CircuitBoard } from "lucide-react"; import { CircuitBoard } from "lucide-react";
import type { import type {

View File

@@ -20,8 +20,8 @@ import {
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { appRouter } from "@/server/api/root"; import { appRouter } from "@/server/api/root";
import { validateRequest } from "@/server/auth/auth";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { validateRequest } from "@dokploy/server";
import { createServerSideHelpers } from "@trpc/react-query/server"; import { createServerSideHelpers } from "@trpc/react-query/server";
import type { import type {
GetServerSidePropsContext, GetServerSidePropsContext,

View File

@@ -20,8 +20,8 @@ import {
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { appRouter } from "@/server/api/root"; import { appRouter } from "@/server/api/root";
import { validateRequest } from "@/server/auth/auth";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { validateRequest } from "@dokploy/server";
import { createServerSideHelpers } from "@trpc/react-query/server"; import { createServerSideHelpers } from "@trpc/react-query/server";
import type { import type {
GetServerSidePropsContext, GetServerSidePropsContext,

View File

@@ -20,8 +20,8 @@ import {
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { appRouter } from "@/server/api/root"; import { appRouter } from "@/server/api/root";
import { validateRequest } from "@/server/auth/auth";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { validateRequest } from "@dokploy/server";
import { createServerSideHelpers } from "@trpc/react-query/server"; import { createServerSideHelpers } from "@trpc/react-query/server";
import type { import type {
GetServerSidePropsContext, GetServerSidePropsContext,

View File

@@ -20,8 +20,8 @@ import {
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { appRouter } from "@/server/api/root"; import { appRouter } from "@/server/api/root";
import { validateRequest } from "@/server/auth/auth";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { validateRequest } from "@dokploy/server";
import { createServerSideHelpers } from "@trpc/react-query/server"; import { createServerSideHelpers } from "@trpc/react-query/server";
import type { import type {
GetServerSidePropsContext, GetServerSidePropsContext,

View File

@@ -19,8 +19,8 @@ import {
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { appRouter } from "@/server/api/root"; import { appRouter } from "@/server/api/root";
import { validateRequest } from "@/server/auth/auth";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { validateRequest } from "@dokploy/server";
import { createServerSideHelpers } from "@trpc/react-query/server"; import { createServerSideHelpers } from "@trpc/react-query/server";
import type { import type {
GetServerSidePropsContext, GetServerSidePropsContext,

View File

@@ -1,6 +1,6 @@
import { ShowProjects } from "@/components/dashboard/projects/show"; import { ShowProjects } from "@/components/dashboard/projects/show";
import { DashboardLayout } from "@/components/layouts/dashboard-layout"; import { DashboardLayout } from "@/components/layouts/dashboard-layout";
import { validateRequest } from "@/server/auth/auth"; import { validateRequest } from "@dokploy/server";
import type { GetServerSidePropsContext } from "next"; import type { GetServerSidePropsContext } from "next";
import React, { type ReactElement } from "react"; import React, { type ReactElement } from "react";

View File

@@ -1,6 +1,6 @@
import { ShowRequests } from "@/components/dashboard/requests/show-requests"; import { ShowRequests } from "@/components/dashboard/requests/show-requests";
import { DashboardLayout } from "@/components/layouts/dashboard-layout"; import { DashboardLayout } from "@/components/layouts/dashboard-layout";
import { validateRequest } from "@/server/auth/auth"; import { IS_CLOUD, validateRequest } from "@dokploy/server";
import type { GetServerSidePropsContext } from "next"; import type { GetServerSidePropsContext } from "next";
import type { ReactElement } from "react"; import type { ReactElement } from "react";
import * as React from "react"; import * as React from "react";
@@ -14,6 +14,14 @@ Requests.getLayout = (page: ReactElement) => {
export async function getServerSideProps( export async function getServerSideProps(
ctx: GetServerSidePropsContext<{ serviceId: string }>, ctx: GetServerSidePropsContext<{ serviceId: string }>,
) { ) {
if (IS_CLOUD) {
return {
redirect: {
permanent: true,
destination: "/dashboard/projects",
},
};
}
const { user } = await validateRequest(ctx.req, ctx.res); const { user } = await validateRequest(ctx.req, ctx.res);
if (!user) { if (!user) {
return { return {

View File

@@ -1,7 +1,7 @@
import { AppearanceForm } from "@/components/dashboard/settings/appearance-form"; import { AppearanceForm } from "@/components/dashboard/settings/appearance-form";
import { DashboardLayout } from "@/components/layouts/dashboard-layout"; import { DashboardLayout } from "@/components/layouts/dashboard-layout";
import { SettingsLayout } from "@/components/layouts/settings-layout"; import { SettingsLayout } from "@/components/layouts/settings-layout";
import { validateRequest } from "@/server/auth/auth"; import { validateRequest } from "@dokploy/server";
import type { GetServerSidePropsContext } from "next"; import type { GetServerSidePropsContext } from "next";
import React, { type ReactElement } from "react"; import React, { type ReactElement } from "react";

View File

@@ -1,7 +1,7 @@
import { ShowCertificates } from "@/components/dashboard/settings/certificates/show-certificates"; import { ShowCertificates } from "@/components/dashboard/settings/certificates/show-certificates";
import { DashboardLayout } from "@/components/layouts/dashboard-layout"; import { DashboardLayout } from "@/components/layouts/dashboard-layout";
import { SettingsLayout } from "@/components/layouts/settings-layout"; import { SettingsLayout } from "@/components/layouts/settings-layout";
import { validateRequest } from "@/server/auth/auth"; import { validateRequest } from "@dokploy/server";
import type { GetServerSidePropsContext } from "next"; import type { GetServerSidePropsContext } from "next";
import React, { type ReactElement } from "react"; import React, { type ReactElement } from "react";

View File

@@ -2,14 +2,13 @@ import { ShowNodes } from "@/components/dashboard/settings/cluster/nodes/show-no
import { ShowRegistry } from "@/components/dashboard/settings/cluster/registry/show-registry"; import { ShowRegistry } from "@/components/dashboard/settings/cluster/registry/show-registry";
import { DashboardLayout } from "@/components/layouts/dashboard-layout"; import { DashboardLayout } from "@/components/layouts/dashboard-layout";
import { SettingsLayout } from "@/components/layouts/settings-layout"; import { SettingsLayout } from "@/components/layouts/settings-layout";
import { validateRequest } from "@/server/auth/auth"; import { IS_CLOUD, validateRequest } from "@dokploy/server";
import type { GetServerSidePropsContext } from "next"; import type { GetServerSidePropsContext } from "next";
import React, { type ReactElement } from "react"; import React, { type ReactElement } from "react";
const Page = () => { const Page = () => {
return ( return (
<div className="flex flex-col gap-4 w-full"> <div className="flex flex-col gap-4 w-full">
<ShowRegistry />
<ShowNodes /> <ShowNodes />
</div> </div>
); );
@@ -27,6 +26,14 @@ Page.getLayout = (page: ReactElement) => {
export async function getServerSideProps( export async function getServerSideProps(
ctx: GetServerSidePropsContext<{ serviceId: string }>, ctx: GetServerSidePropsContext<{ serviceId: string }>,
) { ) {
if (IS_CLOUD) {
return {
redirect: {
permanent: true,
destination: "/dashboard/projects",
},
};
}
const { user, session } = await validateRequest(ctx.req, ctx.res); const { user, session } = await validateRequest(ctx.req, ctx.res);
if (!user || user.rol === "user") { if (!user || user.rol === "user") {
return { return {

View File

@@ -1,7 +1,7 @@
import { ShowDestinations } from "@/components/dashboard/settings/destination/show-destinations"; import { ShowDestinations } from "@/components/dashboard/settings/destination/show-destinations";
import { DashboardLayout } from "@/components/layouts/dashboard-layout"; import { DashboardLayout } from "@/components/layouts/dashboard-layout";
import { SettingsLayout } from "@/components/layouts/settings-layout"; import { SettingsLayout } from "@/components/layouts/settings-layout";
import { validateRequest } from "@/server/auth/auth"; import { validateRequest } from "@dokploy/server";
import type { GetServerSidePropsContext } from "next"; import type { GetServerSidePropsContext } from "next";
import React, { type ReactElement } from "react"; import React, { type ReactElement } from "react";

View File

@@ -2,7 +2,7 @@ import { ShowGitProviders } from "@/components/dashboard/settings/git/show-git-p
import { DashboardLayout } from "@/components/layouts/dashboard-layout"; import { DashboardLayout } from "@/components/layouts/dashboard-layout";
import { SettingsLayout } from "@/components/layouts/settings-layout"; import { SettingsLayout } from "@/components/layouts/settings-layout";
import { appRouter } from "@/server/api/root"; import { appRouter } from "@/server/api/root";
import { validateRequest } from "@/server/auth/auth"; import { validateRequest } from "@dokploy/server";
import { createServerSideHelpers } from "@trpc/react-query/server"; import { createServerSideHelpers } from "@trpc/react-query/server";
import type { GetServerSidePropsContext } from "next"; import type { GetServerSidePropsContext } from "next";
import React, { type ReactElement } from "react"; import React, { type ReactElement } from "react";

View File

@@ -2,7 +2,7 @@ import { ShowDestinations } from "@/components/dashboard/settings/destination/sh
import { ShowNotifications } from "@/components/dashboard/settings/notifications/show-notifications"; import { ShowNotifications } from "@/components/dashboard/settings/notifications/show-notifications";
import { DashboardLayout } from "@/components/layouts/dashboard-layout"; import { DashboardLayout } from "@/components/layouts/dashboard-layout";
import { SettingsLayout } from "@/components/layouts/settings-layout"; import { SettingsLayout } from "@/components/layouts/settings-layout";
import { validateRequest } from "@/server/auth/auth"; import { validateRequest } from "@dokploy/server";
import type { GetServerSidePropsContext } from "next"; import type { GetServerSidePropsContext } from "next";
import React, { type ReactElement } from "react"; import React, { type ReactElement } from "react";

View File

@@ -2,8 +2,8 @@ import { GenerateToken } from "@/components/dashboard/settings/profile/generate-
import { ProfileForm } from "@/components/dashboard/settings/profile/profile-form"; import { ProfileForm } from "@/components/dashboard/settings/profile/profile-form";
import { DashboardLayout } from "@/components/layouts/dashboard-layout"; import { DashboardLayout } from "@/components/layouts/dashboard-layout";
import { SettingsLayout } from "@/components/layouts/settings-layout"; import { SettingsLayout } from "@/components/layouts/settings-layout";
import { validateRequest } from "@/server/auth/auth";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { validateRequest } from "@dokploy/server";
import type { GetServerSidePropsContext } from "next"; import type { GetServerSidePropsContext } from "next";
import React, { type ReactElement } from "react"; import React, { type ReactElement } from "react";

View File

@@ -0,0 +1,41 @@
import { ShowRegistry } from "@/components/dashboard/settings/cluster/registry/show-registry";
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
import { SettingsLayout } from "@/components/layouts/settings-layout";
import { validateRequest } from "@dokploy/server";
import type { GetServerSidePropsContext } from "next";
import React, { type ReactElement } from "react";
const Page = () => {
return (
<div className="flex flex-col gap-4 w-full">
<ShowRegistry />
</div>
);
};
export default Page;
Page.getLayout = (page: ReactElement) => {
return (
<DashboardLayout tab={"settings"}>
<SettingsLayout>{page}</SettingsLayout>
</DashboardLayout>
);
};
export async function getServerSideProps(
ctx: GetServerSidePropsContext<{ serviceId: string }>,
) {
const { user, session } = await validateRequest(ctx.req, ctx.res);
if (!user || user.rol === "user") {
return {
redirect: {
permanent: true,
destination: "/",
},
};
}
return {
props: {},
};
}

View File

@@ -2,7 +2,7 @@ import { WebDomain } from "@/components/dashboard/settings/web-domain";
import { WebServer } from "@/components/dashboard/settings/web-server"; import { WebServer } from "@/components/dashboard/settings/web-server";
import { DashboardLayout } from "@/components/layouts/dashboard-layout"; import { DashboardLayout } from "@/components/layouts/dashboard-layout";
import { SettingsLayout } from "@/components/layouts/settings-layout"; import { SettingsLayout } from "@/components/layouts/settings-layout";
import { validateRequest } from "@/server/auth/auth"; import { IS_CLOUD, validateRequest } from "@dokploy/server";
import type { GetServerSidePropsContext } from "next"; import type { GetServerSidePropsContext } from "next";
import React, { type ReactElement } from "react"; import React, { type ReactElement } from "react";
@@ -27,6 +27,14 @@ Page.getLayout = (page: ReactElement) => {
export async function getServerSideProps( export async function getServerSideProps(
ctx: GetServerSidePropsContext<{ serviceId: string }>, ctx: GetServerSidePropsContext<{ serviceId: string }>,
) { ) {
if (IS_CLOUD) {
return {
redirect: {
permanent: true,
destination: "/dashboard/projects",
},
};
}
const { user } = await validateRequest(ctx.req, ctx.res); const { user } = await validateRequest(ctx.req, ctx.res);
if (!user) { if (!user) {
return { return {

View File

@@ -1,7 +1,7 @@
import { ShowServers } from "@/components/dashboard/settings/servers/show-servers"; import { ShowServers } from "@/components/dashboard/settings/servers/show-servers";
import { DashboardLayout } from "@/components/layouts/dashboard-layout"; import { DashboardLayout } from "@/components/layouts/dashboard-layout";
import { SettingsLayout } from "@/components/layouts/settings-layout"; import { SettingsLayout } from "@/components/layouts/settings-layout";
import { validateRequest } from "@/server/auth/auth"; import { validateRequest } from "@dokploy/server";
import type { GetServerSidePropsContext } from "next"; import type { GetServerSidePropsContext } from "next";
import React, { type ReactElement } from "react"; import React, { type ReactElement } from "react";

View File

@@ -2,7 +2,7 @@ import { ShowDestinations } from "@/components/dashboard/settings/ssh-keys/show-
import { DashboardLayout } from "@/components/layouts/dashboard-layout"; import { DashboardLayout } from "@/components/layouts/dashboard-layout";
import { SettingsLayout } from "@/components/layouts/settings-layout"; import { SettingsLayout } from "@/components/layouts/settings-layout";
import { appRouter } from "@/server/api/root"; import { appRouter } from "@/server/api/root";
import { validateRequest } from "@/server/auth/auth"; import { validateRequest } from "@dokploy/server";
import { createServerSideHelpers } from "@trpc/react-query/server"; import { createServerSideHelpers } from "@trpc/react-query/server";
import type { GetServerSidePropsContext } from "next"; import type { GetServerSidePropsContext } from "next";
import React, { type ReactElement } from "react"; import React, { type ReactElement } from "react";

View File

@@ -1,7 +1,7 @@
import { ShowUsers } from "@/components/dashboard/settings/users/show-users"; import { ShowUsers } from "@/components/dashboard/settings/users/show-users";
import { DashboardLayout } from "@/components/layouts/dashboard-layout"; import { DashboardLayout } from "@/components/layouts/dashboard-layout";
import { SettingsLayout } from "@/components/layouts/settings-layout"; import { SettingsLayout } from "@/components/layouts/settings-layout";
import { validateRequest } from "@/server/auth/auth"; import { validateRequest } from "@dokploy/server";
import type { GetServerSidePropsContext } from "next"; import type { GetServerSidePropsContext } from "next";
import React, { type ReactElement } from "react"; import React, { type ReactElement } from "react";

View File

@@ -1,7 +1,7 @@
import { ShowTraefikSystem } from "@/components/dashboard/file-system/show-traefik-system"; import { ShowTraefikSystem } from "@/components/dashboard/file-system/show-traefik-system";
import { DashboardLayout } from "@/components/layouts/dashboard-layout"; import { DashboardLayout } from "@/components/layouts/dashboard-layout";
import { appRouter } from "@/server/api/root"; import { appRouter } from "@/server/api/root";
import { validateRequest } from "@/server/auth/auth"; import { IS_CLOUD, validateRequest } from "@dokploy/server";
import { createServerSideHelpers } from "@trpc/react-query/server"; import { createServerSideHelpers } from "@trpc/react-query/server";
import type { GetServerSidePropsContext } from "next"; import type { GetServerSidePropsContext } from "next";
import React, { type ReactElement } from "react"; import React, { type ReactElement } from "react";
@@ -19,6 +19,14 @@ Dashboard.getLayout = (page: ReactElement) => {
export async function getServerSideProps( export async function getServerSideProps(
ctx: GetServerSidePropsContext<{ serviceId: string }>, ctx: GetServerSidePropsContext<{ serviceId: string }>,
) { ) {
if (IS_CLOUD) {
return {
redirect: {
permanent: true,
destination: "/dashboard/projects",
},
};
}
const { user, session } = await validateRequest(ctx.req, ctx.res); const { user, session } = await validateRequest(ctx.req, ctx.res);
if (!user) { if (!user) {
return { return {

View File

@@ -1,3 +0,0 @@
export default function hola() {
return <div>hola</div>;
}

View File

@@ -17,9 +17,8 @@ import {
FormMessage, FormMessage,
} from "@/components/ui/form"; } from "@/components/ui/form";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { isAdminPresent } from "@/server/api/services/admin";
import { validateRequest } from "@/server/auth/auth";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { IS_CLOUD, isAdminPresent, validateRequest } from "@dokploy/server";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import type { GetServerSidePropsContext } from "next"; import type { GetServerSidePropsContext } from "next";
import Link from "next/link"; import Link from "next/link";
@@ -51,16 +50,12 @@ const loginSchema = z.object({
type Login = z.infer<typeof loginSchema>; type Login = z.infer<typeof loginSchema>;
interface Props {
hasAdmin: boolean;
}
type AuthResponse = { type AuthResponse = {
is2FAEnabled: boolean; is2FAEnabled: boolean;
authId: string; authId: string;
}; };
export default function Home({ hasAdmin }: Props) { export default function Home() {
const [temp, setTemp] = useState<AuthResponse>({ const [temp, setTemp] = useState<AuthResponse>({
is2FAEnabled: false, is2FAEnabled: false,
authId: "", authId: "",
@@ -170,23 +165,13 @@ export default function Home({ hasAdmin }: Props) {
<Login2FA authId={temp.authId} /> <Login2FA authId={temp.authId} />
)} )}
{!hasAdmin && (
<div className="mt-4 text-center text-sm">
Dont have an account?
<Link className="underline" href="/register">
Sign up
</Link>
</div>
)}
<div className="flex flex-row justify-between flex-wrap"> <div className="flex flex-row justify-between flex-wrap">
<div className="mt-4 text-center text-sm flex flex-row justify-center gap-2"> <div className="mt-4 text-center text-sm flex flex-row justify-center gap-2">
Need help?
<Link <Link
className="underline" className="hover:underline text-muted-foreground"
href="https://dokploy.com" href="/register"
target="_blank"
> >
Contact us Create an account
</Link> </Link>
</div> </div>
@@ -212,6 +197,24 @@ Home.getLayout = (page: ReactElement) => {
return <OnboardingLayout>{page}</OnboardingLayout>; return <OnboardingLayout>{page}</OnboardingLayout>;
}; };
export async function getServerSideProps(context: GetServerSidePropsContext) { export async function getServerSideProps(context: GetServerSidePropsContext) {
if (IS_CLOUD) {
try {
const { user } = await validateRequest(context.req, context.res);
if (user) {
return {
redirect: {
permanent: true,
destination: "/dashboard/projects",
},
};
}
} catch (error) {}
return {
props: {},
};
}
const hasAdmin = await isAdminPresent(); const hasAdmin = await isAdminPresent();
if (!hasAdmin) { if (!hasAdmin) {
@@ -233,6 +236,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
}, },
}; };
} }
return { return {
props: { props: {
hasAdmin, hasAdmin,

View File

@@ -15,8 +15,8 @@ import {
FormMessage, FormMessage,
} from "@/components/ui/form"; } from "@/components/ui/form";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { getUserByToken } from "@/server/api/services/admin";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { getUserByToken } from "@dokploy/server";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { AlertTriangle } from "lucide-react"; import { AlertTriangle } from "lucide-react";
import type { GetServerSidePropsContext } from "next"; import type { GetServerSidePropsContext } from "next";

View File

@@ -15,11 +15,11 @@ import {
FormMessage, FormMessage,
} from "@/components/ui/form"; } from "@/components/ui/form";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { isAdminPresent } from "@/server/api/services/admin";
// import { IS_CLOUD } from "@/server/constants";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { IS_CLOUD, isAdminPresent, validateRequest } from "@dokploy/server";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { AlertTriangle } from "lucide-react"; import { AlertTriangle } from "lucide-react";
import type { GetServerSidePropsContext } from "next";
import Link from "next/link"; import Link from "next/link";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { useEffect } from "react"; import { useEffect } from "react";
@@ -67,9 +67,10 @@ type Register = z.infer<typeof registerSchema>;
interface Props { interface Props {
hasAdmin: boolean; hasAdmin: boolean;
isCloud: boolean;
} }
const Register = ({ hasAdmin }: Props) => { const Register = ({ isCloud }: Props) => {
const router = useRouter(); const router = useRouter();
const { mutateAsync, error, isError } = api.auth.createAdmin.useMutation(); const { mutateAsync, error, isError } = api.auth.createAdmin.useMutation();
@@ -112,9 +113,12 @@ const Register = ({ hasAdmin }: Props) => {
<span className="font-medium text-sm">Dokploy</span> <span className="font-medium text-sm">Dokploy</span>
</Link> </Link>
<CardTitle className="text-2xl font-bold">Setup the server</CardTitle> <CardTitle className="text-2xl font-bold">
{isCloud ? "Create an account" : "Setup the server"}
</CardTitle>
<CardDescription> <CardDescription>
Enter your email and password to setup the server Enter your email and password to{" "}
{isCloud ? "create an account" : "setup the server"}
</CardDescription> </CardDescription>
<Card className="mx-auto w-full max-w-lg bg-transparent"> <Card className="mx-auto w-full max-w-lg bg-transparent">
<div className="p-3" /> <div className="p-3" />
@@ -192,8 +196,9 @@ const Register = ({ hasAdmin }: Props) => {
</div> </div>
</form> </form>
</Form> </Form>
{hasAdmin && ( <div className="flex flex-row justify-between flex-wrap">
<div className="mt-4 text-center text-sm"> {isCloud && (
<div className="mt-4 text-center text-sm flex gap-2">
Already have account? Already have account?
<Link className="underline" href="/"> <Link className="underline" href="/">
Sign in Sign in
@@ -211,6 +216,7 @@ const Register = ({ hasAdmin }: Props) => {
Contact us Contact us
</Link> </Link>
</div> </div>
</div>
</CardContent> </CardContent>
</Card> </Card>
</div> </div>
@@ -220,12 +226,24 @@ const Register = ({ hasAdmin }: Props) => {
}; };
export default Register; export default Register;
export async function getServerSideProps() { export async function getServerSideProps(context: GetServerSidePropsContext) {
// if (IS_CLOUD) { if (IS_CLOUD) {
// return { const { user } = await validateRequest(context.req, context.res);
// props: {},
// }; if (user) {
// } return {
redirect: {
permanent: true,
destination: "/dashboard/projects",
},
};
}
return {
props: {
isCloud: true,
},
};
}
const hasAdmin = await isAdminPresent(); const hasAdmin = await isAdminPresent();
if (hasAdmin) { if (hasAdmin) {
@@ -238,7 +256,7 @@ export async function getServerSideProps() {
} }
return { return {
props: { props: {
hasAdmin, isCloud: false,
}, },
}; };
} }

View File

@@ -1,6 +1,6 @@
import { appRouter } from "@/server/api/root"; import { appRouter } from "@/server/api/root";
import { validateRequest } from "@/server/auth/auth";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { validateRequest } from "@dokploy/server";
import { createServerSideHelpers } from "@trpc/react-query/server"; import { createServerSideHelpers } from "@trpc/react-query/server";
import type { GetServerSidePropsContext, NextPage } from "next"; import type { GetServerSidePropsContext, NextPage } from "next";
import dynamic from "next/dynamic"; import dynamic from "next/dynamic";

View File

@@ -1,6 +1,6 @@
import { findAdmin } from "./server/api/services/admin"; import { findAdmin } from "@dokploy/server";
import { updateAuthById } from "./server/api/services/auth"; import { updateAuthById } from "@dokploy/server";
import { generateRandomPassword } from "./server/auth/random-password"; import { generateRandomPassword } from "@dokploy/server";
(async () => { (async () => {
try { try {

View File

@@ -6,19 +6,22 @@ import {
apiRemoveUser, apiRemoveUser,
users, users,
} from "@/server/db/schema"; } from "@/server/db/schema";
import { TRPCError } from "@trpc/server";
import { eq } from "drizzle-orm";
import { import {
createInvitation, createInvitation,
findAdmin, findAdminById,
findUserByAuthId,
findUserById,
getUserByToken, getUserByToken,
removeUserByAuthId, removeUserByAuthId,
} from "../services/admin"; } from "@dokploy/server";
import { TRPCError } from "@trpc/server";
import { eq } from "drizzle-orm";
import { adminProcedure, createTRPCRouter, publicProcedure } from "../trpc"; import { adminProcedure, createTRPCRouter, publicProcedure } from "../trpc";
export const adminRouter = createTRPCRouter({ export const adminRouter = createTRPCRouter({
one: adminProcedure.query(async ({ ctx }) => { one: adminProcedure.query(async ({ ctx }) => {
const { sshPrivateKey, ...rest } = await findAdmin(); const { sshPrivateKey, ...rest } = await findAdminById(ctx.user.adminId);
return { return {
haveSSH: !!sshPrivateKey, haveSSH: !!sshPrivateKey,
...rest, ...rest,
@@ -26,9 +29,9 @@ export const adminRouter = createTRPCRouter({
}), }),
createUserInvitation: adminProcedure createUserInvitation: adminProcedure
.input(apiCreateUserInvitation) .input(apiCreateUserInvitation)
.mutation(async ({ input }) => { .mutation(async ({ input, ctx }) => {
try { try {
await createInvitation(input); await createInvitation(input, ctx.user.adminId);
} catch (error) { } catch (error) {
throw new TRPCError({ throw new TRPCError({
code: "BAD_REQUEST", code: "BAD_REQUEST",
@@ -40,8 +43,16 @@ export const adminRouter = createTRPCRouter({
}), }),
removeUser: adminProcedure removeUser: adminProcedure
.input(apiRemoveUser) .input(apiRemoveUser)
.mutation(async ({ input }) => { .mutation(async ({ input, ctx }) => {
try { 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); return await removeUserByAuthId(input.authId);
} catch (error) { } catch (error) {
throw new TRPCError({ throw new TRPCError({
@@ -58,8 +69,16 @@ export const adminRouter = createTRPCRouter({
}), }),
assignPermissions: adminProcedure assignPermissions: adminProcedure
.input(apiAssignPermissions) .input(apiAssignPermissions)
.mutation(async ({ input }) => { .mutation(async ({ input, ctx }) => {
try { 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 await db
.update(users) .update(users)
.set({ .set({
@@ -67,10 +86,7 @@ export const adminRouter = createTRPCRouter({
}) })
.where(eq(users.userId, input.userId)); .where(eq(users.userId, input.userId));
} catch (error) { } catch (error) {
throw new TRPCError({ throw error;
code: "BAD_REQUEST",
message: "Error to assign permissions",
});
} }
}), }),
}); });

View File

@@ -18,46 +18,45 @@ import {
apiSaveGitlabProvider, apiSaveGitlabProvider,
apiUpdateApplication, apiUpdateApplication,
applications, applications,
} from "@/server/db/schema/application"; } from "@/server/db/schema";
import { import {
type DeploymentJob, type DeploymentJob,
cleanQueuesByApplication, cleanQueuesByApplication,
} from "@/server/queues/deployments-queue"; } from "@/server/queues/deployments-queue";
import { myQueue } from "@/server/queues/queueSetup"; import { myQueue } from "@/server/queues/queueSetup";
import { unzipDrop } from "@/server/utils/builders/drop"; import { deploy } from "@/server/utils/deploy";
import { uploadFileSchema } from "@/utils/schema";
import { import {
IS_CLOUD,
addNewService,
checkServiceAccess,
createApplication,
deleteAllMiddlewares,
findApplicationById,
findProjectById,
getApplicationStats,
readConfig,
readRemoteConfig,
removeDeployments,
removeDirectoryCode,
removeMonitoringDirectory,
removeService, removeService,
removeTraefikConfig,
startService, startService,
startServiceRemote, startServiceRemote,
stopService, stopService,
stopServiceRemote, stopServiceRemote,
} from "@/server/utils/docker/utils"; unzipDrop,
import { updateApplication,
removeDirectoryCode, updateApplicationStatus,
removeMonitoringDirectory,
} from "@/server/utils/filesystem/directory";
import {
readConfig,
readRemoteConfig,
removeTraefikConfig,
writeConfig, writeConfig,
writeConfigRemote, writeConfigRemote,
} from "@/server/utils/traefik/application"; // uploadFileSchema
import { deleteAllMiddlewares } from "@/server/utils/traefik/middleware"; } from "@dokploy/server";
import { uploadFileSchema } from "@/utils/schema";
import { TRPCError } from "@trpc/server"; import { TRPCError } from "@trpc/server";
import { eq } from "drizzle-orm"; import { eq } from "drizzle-orm";
import { nanoid } from "nanoid"; import { nanoid } from "nanoid";
import { z } from "zod"; import { z } from "zod";
import {
createApplication,
findApplicationById,
getApplicationStats,
updateApplication,
updateApplicationStatus,
} from "../services/application";
import { removeDeployments } from "../services/deployment";
import { addNewService, checkServiceAccess } from "../services/user";
export const applicationRouter = createTRPCRouter({ export const applicationRouter = createTRPCRouter({
create: protectedProcedure create: protectedProcedure
@@ -67,6 +66,21 @@ export const applicationRouter = createTRPCRouter({
if (ctx.user.rol === "user") { if (ctx.user.rol === "user") {
await checkServiceAccess(ctx.user.authId, input.projectId, "create"); 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); const newApplication = await createApplication(input);
if (ctx.user.rol === "user") { if (ctx.user.rol === "user") {
@@ -94,13 +108,26 @@ export const applicationRouter = createTRPCRouter({
"access", "access",
); );
} }
return await findApplicationById(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 application;
}), }),
reload: protectedProcedure reload: protectedProcedure
.input(apiReloadApplication) .input(apiReloadApplication)
.mutation(async ({ input }) => { .mutation(async ({ input, ctx }) => {
const application = await findApplicationById(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 reload this application",
});
}
if (application.serverId) { if (application.serverId) {
await stopServiceRemote(application.serverId, input.appName); await stopServiceRemote(application.serverId, input.appName);
} else { } else {
@@ -129,6 +156,13 @@ export const applicationRouter = createTRPCRouter({
} }
const application = await findApplicationById(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 delete this application",
});
}
const result = await db const result = await db
.delete(applications) .delete(applications)
.where(eq(applications.applicationId, input.applicationId)) .where(eq(applications.applicationId, input.applicationId))
@@ -161,8 +195,14 @@ export const applicationRouter = createTRPCRouter({
stop: protectedProcedure stop: protectedProcedure
.input(apiFindOneApplication) .input(apiFindOneApplication)
.mutation(async ({ input }) => { .mutation(async ({ input, ctx }) => {
const service = await findApplicationById(input.applicationId); 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) { if (service.serverId) {
await stopServiceRemote(service.serverId, service.appName); await stopServiceRemote(service.serverId, service.appName);
} else { } else {
@@ -175,8 +215,15 @@ export const applicationRouter = createTRPCRouter({
start: protectedProcedure start: protectedProcedure
.input(apiFindOneApplication) .input(apiFindOneApplication)
.mutation(async ({ input }) => { .mutation(async ({ input, ctx }) => {
const service = await findApplicationById(input.applicationId); 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) { if (service.serverId) {
await startServiceRemote(service.serverId, service.appName); await startServiceRemote(service.serverId, service.appName);
} else { } else {
@@ -189,8 +236,14 @@ export const applicationRouter = createTRPCRouter({
redeploy: protectedProcedure redeploy: protectedProcedure
.input(apiFindOneApplication) .input(apiFindOneApplication)
.mutation(async ({ input }) => { .mutation(async ({ input, ctx }) => {
const application = await findApplicationById(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 redeploy this application",
});
}
const jobData: DeploymentJob = { const jobData: DeploymentJob = {
applicationId: input.applicationId, applicationId: input.applicationId,
titleLog: "Rebuild deployment", titleLog: "Rebuild deployment",
@@ -199,6 +252,12 @@ export const applicationRouter = createTRPCRouter({
applicationType: "application", applicationType: "application",
server: !!application.serverId, server: !!application.serverId,
}; };
if (IS_CLOUD && application.serverId) {
jobData.serverId = application.serverId;
await deploy(jobData);
return true;
}
await myQueue.add( await myQueue.add(
"deployments", "deployments",
{ ...jobData }, { ...jobData },
@@ -210,7 +269,14 @@ export const applicationRouter = createTRPCRouter({
}), }),
saveEnvironment: protectedProcedure saveEnvironment: protectedProcedure
.input(apiSaveEnvironmentVariables) .input(apiSaveEnvironmentVariables)
.mutation(async ({ input }) => { .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, { await updateApplication(input.applicationId, {
env: input.env, env: input.env,
buildArgs: input.buildArgs, buildArgs: input.buildArgs,
@@ -219,7 +285,14 @@ export const applicationRouter = createTRPCRouter({
}), }),
saveBuildType: protectedProcedure saveBuildType: protectedProcedure
.input(apiSaveBuildType) .input(apiSaveBuildType)
.mutation(async ({ input }) => { .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, { await updateApplication(input.applicationId, {
buildType: input.buildType, buildType: input.buildType,
dockerfile: input.dockerfile, dockerfile: input.dockerfile,
@@ -232,7 +305,14 @@ export const applicationRouter = createTRPCRouter({
}), }),
saveGithubProvider: protectedProcedure saveGithubProvider: protectedProcedure
.input(apiSaveGithubProvider) .input(apiSaveGithubProvider)
.mutation(async ({ input }) => { .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, { await updateApplication(input.applicationId, {
repository: input.repository, repository: input.repository,
branch: input.branch, branch: input.branch,
@@ -247,7 +327,14 @@ export const applicationRouter = createTRPCRouter({
}), }),
saveGitlabProvider: protectedProcedure saveGitlabProvider: protectedProcedure
.input(apiSaveGitlabProvider) .input(apiSaveGitlabProvider)
.mutation(async ({ input }) => { .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, { await updateApplication(input.applicationId, {
gitlabRepository: input.gitlabRepository, gitlabRepository: input.gitlabRepository,
gitlabOwner: input.gitlabOwner, gitlabOwner: input.gitlabOwner,
@@ -264,7 +351,14 @@ export const applicationRouter = createTRPCRouter({
}), }),
saveBitbucketProvider: protectedProcedure saveBitbucketProvider: protectedProcedure
.input(apiSaveBitbucketProvider) .input(apiSaveBitbucketProvider)
.mutation(async ({ input }) => { .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, { await updateApplication(input.applicationId, {
bitbucketRepository: input.bitbucketRepository, bitbucketRepository: input.bitbucketRepository,
bitbucketOwner: input.bitbucketOwner, bitbucketOwner: input.bitbucketOwner,
@@ -279,7 +373,14 @@ export const applicationRouter = createTRPCRouter({
}), }),
saveDockerProvider: protectedProcedure saveDockerProvider: protectedProcedure
.input(apiSaveDockerProvider) .input(apiSaveDockerProvider)
.mutation(async ({ input }) => { .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, { await updateApplication(input.applicationId, {
dockerImage: input.dockerImage, dockerImage: input.dockerImage,
username: input.username, username: input.username,
@@ -292,7 +393,14 @@ export const applicationRouter = createTRPCRouter({
}), }),
saveGitProdiver: protectedProcedure saveGitProdiver: protectedProcedure
.input(apiSaveGitProvider) .input(apiSaveGitProvider)
.mutation(async ({ input }) => { .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, { await updateApplication(input.applicationId, {
customGitBranch: input.customGitBranch, customGitBranch: input.customGitBranch,
customGitBuildPath: input.customGitBuildPath, customGitBuildPath: input.customGitBuildPath,
@@ -306,18 +414,32 @@ export const applicationRouter = createTRPCRouter({
}), }),
markRunning: protectedProcedure markRunning: protectedProcedure
.input(apiFindOneApplication) .input(apiFindOneApplication)
.mutation(async ({ input }) => { .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"); await updateApplicationStatus(input.applicationId, "running");
}), }),
update: protectedProcedure update: protectedProcedure
.input(apiUpdateApplication) .input(apiUpdateApplication)
.mutation(async ({ input }) => { .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 { applicationId, ...rest } = input;
const application = await updateApplication(applicationId, { const updateApp = await updateApplication(applicationId, {
...rest, ...rest,
}); });
if (!application) { if (!updateApp) {
throw new TRPCError({ throw new TRPCError({
code: "BAD_REQUEST", code: "BAD_REQUEST",
message: "Update: Error to update application", message: "Update: Error to update application",
@@ -328,7 +450,14 @@ export const applicationRouter = createTRPCRouter({
}), }),
refreshToken: protectedProcedure refreshToken: protectedProcedure
.input(apiFindOneApplication) .input(apiFindOneApplication)
.mutation(async ({ input }) => { .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, { await updateApplication(input.applicationId, {
refreshToken: nanoid(), refreshToken: nanoid(),
}); });
@@ -338,6 +467,12 @@ export const applicationRouter = createTRPCRouter({
.input(apiFindOneApplication) .input(apiFindOneApplication)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
const application = await findApplicationById(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 deploy this application",
});
}
const jobData: DeploymentJob = { const jobData: DeploymentJob = {
applicationId: input.applicationId, applicationId: input.applicationId,
titleLog: "Manual deployment", titleLog: "Manual deployment",
@@ -346,6 +481,12 @@ export const applicationRouter = createTRPCRouter({
applicationType: "application", applicationType: "application",
server: !!application.serverId, server: !!application.serverId,
}; };
if (IS_CLOUD && application.serverId) {
jobData.serverId = application.serverId;
await deploy(jobData);
return true;
}
await myQueue.add( await myQueue.add(
"deployments", "deployments",
{ ...jobData }, { ...jobData },
@@ -358,14 +499,28 @@ export const applicationRouter = createTRPCRouter({
cleanQueues: protectedProcedure cleanQueues: protectedProcedure
.input(apiFindOneApplication) .input(apiFindOneApplication)
.mutation(async ({ input }) => { .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); await cleanQueuesByApplication(input.applicationId);
}), }),
readTraefikConfig: protectedProcedure readTraefikConfig: protectedProcedure
.input(apiFindOneApplication) .input(apiFindOneApplication)
.query(async ({ input }) => { .query(async ({ input, ctx }) => {
const application = await findApplicationById(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 read this application",
});
}
let traefikConfig = null; let traefikConfig = null;
if (application.serverId) { if (application.serverId) {
traefikConfig = await readRemoteConfig( traefikConfig = await readRemoteConfig(
@@ -389,17 +544,24 @@ export const applicationRouter = createTRPCRouter({
}) })
.use(uploadProcedure) .use(uploadProcedure)
.input(uploadFileSchema) .input(uploadFileSchema)
.mutation(async ({ input }) => { .mutation(async ({ input, ctx }) => {
const zipFile = input.zip; 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, { updateApplication(input.applicationId as string, {
sourceType: "drop", sourceType: "drop",
dropBuildPath: input.dropBuildPath, dropBuildPath: input.dropBuildPath,
}); });
const app = await findApplicationById(input.applicationId as string);
await unzipDrop(zipFile, app); await unzipDrop(zipFile, app);
const jobData: DeploymentJob = { const jobData: DeploymentJob = {
applicationId: app.applicationId, applicationId: app.applicationId,
titleLog: "Manual deployment", titleLog: "Manual deployment",
@@ -408,6 +570,12 @@ export const applicationRouter = createTRPCRouter({
applicationType: "application", applicationType: "application",
server: !!app.serverId, server: !!app.serverId,
}; };
if (IS_CLOUD && app.serverId) {
jobData.serverId = app.serverId;
await deploy(jobData);
return true;
}
await myQueue.add( await myQueue.add(
"deployments", "deployments",
{ ...jobData }, { ...jobData },
@@ -420,9 +588,16 @@ export const applicationRouter = createTRPCRouter({
}), }),
updateTraefikConfig: protectedProcedure updateTraefikConfig: protectedProcedure
.input(z.object({ applicationId: z.string(), traefikConfig: z.string() })) .input(z.object({ applicationId: z.string(), traefikConfig: z.string() }))
.mutation(async ({ input }) => { .mutation(async ({ input, ctx }) => {
const application = await findApplicationById(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 update this application",
});
}
if (application.serverId) { if (application.serverId) {
await writeConfigRemote( await writeConfigRemote(
application.serverId, application.serverId,
@@ -436,14 +611,15 @@ export const applicationRouter = createTRPCRouter({
}), }),
readAppMonitoring: protectedProcedure readAppMonitoring: protectedProcedure
.input(apiFindMonitoringStats) .input(apiFindMonitoringStats)
.query(async ({ input }) => { .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); const stats = await getApplicationStats(input.appName);
return stats; return stats;
}), }),
}); });
// Paketo Buildpacks: paketobuildpacks/builder-jammy-full Ubuntu 22.04 Jammy Jellyfish full image with buildpacks for Apache HTTPD, Go, Java, Java Native Image, .NET, NGINX, Node.js, PHP, Procfile, Python, and Ruby
// Heroku: heroku/builder:22 Heroku-22 (Ubuntu 22.04) base image with buildpacks for Go, Java, Node.js, PHP, Python, Ruby & Scala.
// pack build imageName --path ./ --builder paketobuildpacks/builder-jammy-full
// pack build prueba-pack --path ./ --builder heroku/builder:22

View File

@@ -1,6 +1,3 @@
import { lucia, validateRequest } from "@/server/auth/auth";
import { luciaToken } from "@/server/auth/token";
// import { IS_CLOUD } from "@/server/constants";
import { import {
apiCreateAdmin, apiCreateAdmin,
apiCreateUser, apiCreateUser,
@@ -11,19 +8,23 @@ import {
apiVerify2FA, apiVerify2FA,
apiVerifyLogin2FA, apiVerifyLogin2FA,
} from "@/server/db/schema"; } from "@/server/db/schema";
import { TRPCError } from "@trpc/server";
import * as bcrypt from "bcrypt";
import { db } from "../../db";
import { getUserByToken } from "../services/admin";
import { import {
IS_CLOUD,
createAdmin, createAdmin,
createUser, createUser,
findAuthByEmail, findAuthByEmail,
findAuthById, findAuthById,
generate2FASecret, generate2FASecret,
getUserByToken,
lucia,
luciaToken,
updateAuthById, updateAuthById,
validateRequest,
verify2FA, verify2FA,
} from "../services/auth"; } from "@dokploy/server";
import { TRPCError } from "@trpc/server";
import * as bcrypt from "bcrypt";
import { db } from "../../db";
import { import {
adminProcedure, adminProcedure,
createTRPCRouter, createTRPCRouter,
@@ -36,7 +37,7 @@ export const authRouter = createTRPCRouter({
.input(apiCreateAdmin) .input(apiCreateAdmin)
.mutation(async ({ ctx, input }) => { .mutation(async ({ ctx, input }) => {
try { try {
// if (!IS_CLOUD) { if (!IS_CLOUD) {
const admin = await db.query.admins.findFirst({}); const admin = await db.query.admins.findFirst({});
if (admin) { if (admin) {
throw new TRPCError({ throw new TRPCError({
@@ -44,8 +45,7 @@ export const authRouter = createTRPCRouter({
message: "Admin already exists", message: "Admin already exists",
}); });
} }
// } }
const newAdmin = await createAdmin(input); const newAdmin = await createAdmin(input);
const session = await lucia.createSession(newAdmin.id || "", {}); const session = await lucia.createSession(newAdmin.id || "", {});
ctx.res.appendHeader( ctx.res.appendHeader(
@@ -54,12 +54,7 @@ export const authRouter = createTRPCRouter({
); );
return true; return true;
} catch (error) { } catch (error) {
console.log(error); throw error;
throw new TRPCError({
code: "BAD_REQUEST",
message: "Error to create the main admin",
cause: error,
});
} }
}), }),
createUser: publicProcedure createUser: publicProcedure
@@ -225,7 +220,7 @@ export const authRouter = createTRPCRouter({
lucia.createSessionCookie(session.id).serialize(), lucia.createSessionCookie(session.id).serialize(),
); );
return auth; return true;
}), }),
disable2FA: protectedProcedure.mutation(async ({ ctx }) => { disable2FA: protectedProcedure.mutation(async ({ ctx }) => {
const auth = await findAuthById(ctx.user.authId); const auth = await findAuthById(ctx.user.authId);

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