mirror of
https://github.com/Dokploy/dokploy.git
synced 2026-06-18 21:55:24 +02:00
Compare commits
1 Commits
v0.25.2
...
2234-datab
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
51c81968e9 |
11
.github/pull_request_template.md
vendored
11
.github/pull_request_template.md
vendored
@@ -6,13 +6,16 @@ Please describe in a short paragraph what this PR is about.
|
||||
|
||||
Before submitting this PR, please make sure that:
|
||||
|
||||
- [] You created a dedicated branch based on the `canary` branch.
|
||||
- [] You have read the suggestions in the CONTRIBUTING.md file https://github.com/Dokploy/dokploy/blob/canary/CONTRIBUTING.md#pull-request
|
||||
- [] You have tested this PR in your local instance.
|
||||
- [ ] You created a dedicated branch based on the `canary` branch.
|
||||
- [ ] You have read the suggestions in the CONTRIBUTING.md file https://github.com/Dokploy/dokploy/blob/canary/CONTRIBUTING.md#pull-request
|
||||
- [ ] You have tested this PR in your local instance.
|
||||
|
||||
## Issues related (if applicable)
|
||||
|
||||
closes #123
|
||||
Close automatically the related issues using the keywords: `closes #ISSUE_NUMBER`, `fixes #ISSUE_NUMBER`, `resolves #ISSUE_NUMBER`
|
||||
|
||||
Example: `closes #123`
|
||||
|
||||
## Screenshots (if applicable)
|
||||
|
||||
If you include a video or screenshot, would be awesome so we can see the changes in action.
|
||||
BIN
.github/sponsors/tuple.png
vendored
BIN
.github/sponsors/tuple.png
vendored
Binary file not shown.
|
Before Width: | Height: | Size: 264 KiB |
2
.github/workflows/dokploy.yml
vendored
2
.github/workflows/dokploy.yml
vendored
@@ -2,7 +2,7 @@ name: Dokploy Docker Build
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main, canary, "fix/re-apply-database-migration-fix"]
|
||||
branches: [main, canary]
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
|
||||
17
README.md
17
README.md
@@ -11,25 +11,8 @@
|
||||
</div>
|
||||
<br />
|
||||
|
||||
|
||||
|
||||
<div align="center" markdown="1">
|
||||
<sup>Special thanks to:</sup>
|
||||
<br>
|
||||
<br>
|
||||
<a href="https://tuple.app/dokploy">
|
||||
<img src=".github/sponsors/tuple.png" alt="Tuple's sponsorship image" width="400"/>
|
||||
</a>
|
||||
|
||||
### [Tuple, the premier screen sharing app for developers](https://tuple.app/dokploy)
|
||||
[Available for MacOS & Windows](https://tuple.app/dokploy)<br>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
Dokploy is a free, self-hostable Platform as a Service (PaaS) that simplifies the deployment and management of applications and databases.
|
||||
|
||||
|
||||
## ✨ Features
|
||||
|
||||
Dokploy includes multiple features to make your life easier.
|
||||
|
||||
@@ -5,11 +5,7 @@ import { zValidator } from "@hono/zod-validator";
|
||||
import { Inngest } from "inngest";
|
||||
import { serve as serveInngest } from "inngest/hono";
|
||||
import { logger } from "./logger.js";
|
||||
import {
|
||||
cancelDeploymentSchema,
|
||||
type DeployJob,
|
||||
deployJobSchema,
|
||||
} from "./schema.js";
|
||||
import { type DeployJob, deployJobSchema } from "./schema.js";
|
||||
import { deploy } from "./utils.js";
|
||||
|
||||
const app = new Hono();
|
||||
@@ -31,13 +27,6 @@ export const deploymentFunction = inngest.createFunction(
|
||||
},
|
||||
],
|
||||
retries: 0,
|
||||
cancelOn: [
|
||||
{
|
||||
event: "deployment/cancelled",
|
||||
if: "async.data.applicationId == event.data.applicationId || async.data.composeId == event.data.composeId",
|
||||
timeout: "1h", // Allow cancellation for up to 1 hour
|
||||
},
|
||||
],
|
||||
},
|
||||
{ event: "deployment/requested" },
|
||||
|
||||
@@ -130,48 +119,6 @@ app.post("/deploy", zValidator("json", deployJobSchema), async (c) => {
|
||||
}
|
||||
});
|
||||
|
||||
app.post(
|
||||
"/cancel-deployment",
|
||||
zValidator("json", cancelDeploymentSchema),
|
||||
async (c) => {
|
||||
const data = c.req.valid("json");
|
||||
logger.info("Received cancel deployment request", data);
|
||||
|
||||
try {
|
||||
// Send cancellation event to Inngest
|
||||
|
||||
await inngest.send({
|
||||
name: "deployment/cancelled",
|
||||
data,
|
||||
});
|
||||
|
||||
const identifier =
|
||||
data.applicationType === "application"
|
||||
? `applicationId: ${data.applicationId}`
|
||||
: `composeId: ${data.composeId}`;
|
||||
|
||||
logger.info("Deployment cancellation event sent", {
|
||||
...data,
|
||||
identifier,
|
||||
});
|
||||
|
||||
return c.json({
|
||||
message: "Deployment cancellation requested",
|
||||
applicationType: data.applicationType,
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error("Failed to send deployment cancellation event", error);
|
||||
return c.json(
|
||||
{
|
||||
message: "Failed to cancel deployment",
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
},
|
||||
500,
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
app.get("/health", async (c) => {
|
||||
return c.json({ status: "ok" });
|
||||
});
|
||||
|
||||
@@ -3,8 +3,8 @@ import { z } from "zod";
|
||||
export const deployJobSchema = z.discriminatedUnion("applicationType", [
|
||||
z.object({
|
||||
applicationId: z.string(),
|
||||
titleLog: z.string().optional(),
|
||||
descriptionLog: z.string().optional(),
|
||||
titleLog: z.string(),
|
||||
descriptionLog: z.string(),
|
||||
server: z.boolean().optional(),
|
||||
type: z.enum(["deploy", "redeploy"]),
|
||||
applicationType: z.literal("application"),
|
||||
@@ -12,8 +12,8 @@ export const deployJobSchema = z.discriminatedUnion("applicationType", [
|
||||
}),
|
||||
z.object({
|
||||
composeId: z.string(),
|
||||
titleLog: z.string().optional(),
|
||||
descriptionLog: z.string().optional(),
|
||||
titleLog: z.string(),
|
||||
descriptionLog: z.string(),
|
||||
server: z.boolean().optional(),
|
||||
type: z.enum(["deploy", "redeploy"]),
|
||||
applicationType: z.literal("compose"),
|
||||
@@ -22,8 +22,8 @@ export const deployJobSchema = z.discriminatedUnion("applicationType", [
|
||||
z.object({
|
||||
applicationId: z.string(),
|
||||
previewDeploymentId: z.string(),
|
||||
titleLog: z.string().optional(),
|
||||
descriptionLog: z.string().optional(),
|
||||
titleLog: z.string(),
|
||||
descriptionLog: z.string(),
|
||||
server: z.boolean().optional(),
|
||||
type: z.enum(["deploy"]),
|
||||
applicationType: z.literal("application-preview"),
|
||||
@@ -32,16 +32,3 @@ export const deployJobSchema = z.discriminatedUnion("applicationType", [
|
||||
]);
|
||||
|
||||
export type DeployJob = z.infer<typeof deployJobSchema>;
|
||||
|
||||
export const cancelDeploymentSchema = z.discriminatedUnion("applicationType", [
|
||||
z.object({
|
||||
applicationId: z.string(),
|
||||
applicationType: z.literal("application"),
|
||||
}),
|
||||
z.object({
|
||||
composeId: z.string(),
|
||||
applicationType: z.literal("compose"),
|
||||
}),
|
||||
]);
|
||||
|
||||
export type CancelDeploymentJob = z.infer<typeof cancelDeploymentSchema>;
|
||||
|
||||
@@ -18,14 +18,14 @@ export const deploy = async (job: DeployJob) => {
|
||||
if (job.type === "redeploy") {
|
||||
await rebuildRemoteApplication({
|
||||
applicationId: job.applicationId,
|
||||
titleLog: job.titleLog || "Rebuild deployment",
|
||||
descriptionLog: job.descriptionLog || "",
|
||||
titleLog: job.titleLog,
|
||||
descriptionLog: job.descriptionLog,
|
||||
});
|
||||
} else if (job.type === "deploy") {
|
||||
await deployRemoteApplication({
|
||||
applicationId: job.applicationId,
|
||||
titleLog: job.titleLog || "Manual deployment",
|
||||
descriptionLog: job.descriptionLog || "",
|
||||
titleLog: job.titleLog,
|
||||
descriptionLog: job.descriptionLog,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -38,14 +38,14 @@ export const deploy = async (job: DeployJob) => {
|
||||
if (job.type === "redeploy") {
|
||||
await rebuildRemoteCompose({
|
||||
composeId: job.composeId,
|
||||
titleLog: job.titleLog || "Rebuild deployment",
|
||||
descriptionLog: job.descriptionLog || "",
|
||||
titleLog: job.titleLog,
|
||||
descriptionLog: job.descriptionLog,
|
||||
});
|
||||
} else if (job.type === "deploy") {
|
||||
await deployRemoteCompose({
|
||||
composeId: job.composeId,
|
||||
titleLog: job.titleLog || "Manual deployment",
|
||||
descriptionLog: job.descriptionLog || "",
|
||||
titleLog: job.titleLog,
|
||||
descriptionLog: job.descriptionLog,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -57,8 +57,8 @@ export const deploy = async (job: DeployJob) => {
|
||||
if (job.type === "deploy") {
|
||||
await deployRemotePreviewApplication({
|
||||
applicationId: job.applicationId,
|
||||
titleLog: job.titleLog || "Preview Deployment",
|
||||
descriptionLog: job.descriptionLog || "",
|
||||
titleLog: job.titleLog,
|
||||
descriptionLog: job.descriptionLog,
|
||||
previewDeploymentId: job.previewDeploymentId,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -56,21 +56,13 @@ const baseApp: ApplicationNested = {
|
||||
previewPort: 3000,
|
||||
previewLimit: 0,
|
||||
previewWildcard: "",
|
||||
environment: {
|
||||
project: {
|
||||
env: "",
|
||||
environmentId: "",
|
||||
organizationId: "",
|
||||
name: "",
|
||||
createdAt: "",
|
||||
description: "",
|
||||
createdAt: "",
|
||||
projectId: "",
|
||||
project: {
|
||||
env: "",
|
||||
organizationId: "",
|
||||
name: "",
|
||||
description: "",
|
||||
createdAt: "",
|
||||
projectId: "",
|
||||
},
|
||||
},
|
||||
buildArgs: null,
|
||||
buildPath: "/",
|
||||
@@ -100,7 +92,6 @@ const baseApp: ApplicationNested = {
|
||||
dockerfile: null,
|
||||
dockerImage: null,
|
||||
dropBuildPath: null,
|
||||
environmentId: "",
|
||||
enabled: null,
|
||||
env: null,
|
||||
healthCheckSwarm: null,
|
||||
@@ -115,6 +106,7 @@ const baseApp: ApplicationNested = {
|
||||
password: null,
|
||||
placementSwarm: null,
|
||||
ports: [],
|
||||
projectId: "",
|
||||
publishDirectory: null,
|
||||
isStaticSpa: null,
|
||||
redirects: [],
|
||||
|
||||
335
apps/dokploy/__test__/env/environment.test.ts
vendored
335
apps/dokploy/__test__/env/environment.test.ts
vendored
@@ -1,335 +0,0 @@
|
||||
import { prepareEnvironmentVariables } from "@dokploy/server/index";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
const projectEnv = `
|
||||
ENVIRONMENT=staging
|
||||
DATABASE_URL=postgres://postgres:postgres@localhost:5432/project_db
|
||||
PORT=3000
|
||||
`;
|
||||
|
||||
const environmentEnv = `
|
||||
NODE_ENV=development
|
||||
API_URL=https://api.dev.example.com
|
||||
REDIS_URL=redis://localhost:6379
|
||||
DATABASE_NAME=dev_database
|
||||
SECRET_KEY=env-secret-123
|
||||
`;
|
||||
|
||||
describe("prepareEnvironmentVariables (environment variables)", () => {
|
||||
it("resolves environment variables correctly", () => {
|
||||
const serviceWithEnvVars = `
|
||||
NODE_ENV=\${{environment.NODE_ENV}}
|
||||
API_URL=\${{environment.API_URL}}
|
||||
SERVICE_PORT=4000
|
||||
`;
|
||||
|
||||
const resolved = prepareEnvironmentVariables(
|
||||
serviceWithEnvVars,
|
||||
"",
|
||||
environmentEnv,
|
||||
);
|
||||
|
||||
expect(resolved).toEqual([
|
||||
"NODE_ENV=development",
|
||||
"API_URL=https://api.dev.example.com",
|
||||
"SERVICE_PORT=4000",
|
||||
]);
|
||||
});
|
||||
|
||||
it("resolves both project and environment variables", () => {
|
||||
const serviceWithBoth = `
|
||||
ENVIRONMENT=\${{project.ENVIRONMENT}}
|
||||
NODE_ENV=\${{environment.NODE_ENV}}
|
||||
API_URL=\${{environment.API_URL}}
|
||||
DATABASE_URL=\${{project.DATABASE_URL}}
|
||||
SERVICE_PORT=4000
|
||||
`;
|
||||
|
||||
const resolved = prepareEnvironmentVariables(
|
||||
serviceWithBoth,
|
||||
projectEnv,
|
||||
environmentEnv,
|
||||
);
|
||||
|
||||
expect(resolved).toEqual([
|
||||
"ENVIRONMENT=staging",
|
||||
"NODE_ENV=development",
|
||||
"API_URL=https://api.dev.example.com",
|
||||
"DATABASE_URL=postgres://postgres:postgres@localhost:5432/project_db",
|
||||
"SERVICE_PORT=4000",
|
||||
]);
|
||||
});
|
||||
|
||||
it("handles undefined environment variables", () => {
|
||||
const serviceWithUndefined = `
|
||||
UNDEFINED_VAR=\${{environment.UNDEFINED_VAR}}
|
||||
`;
|
||||
|
||||
expect(() =>
|
||||
prepareEnvironmentVariables(serviceWithUndefined, "", environmentEnv),
|
||||
).toThrow("Invalid environment variable: environment.UNDEFINED_VAR");
|
||||
});
|
||||
|
||||
it("allows service variables to override environment variables", () => {
|
||||
const serviceOverrideEnv = `
|
||||
NODE_ENV=production
|
||||
API_URL=\${{environment.API_URL}}
|
||||
`;
|
||||
|
||||
const resolved = prepareEnvironmentVariables(
|
||||
serviceOverrideEnv,
|
||||
"",
|
||||
environmentEnv,
|
||||
);
|
||||
|
||||
expect(resolved).toEqual([
|
||||
"NODE_ENV=production", // Overrides environment variable
|
||||
"API_URL=https://api.dev.example.com",
|
||||
]);
|
||||
});
|
||||
|
||||
it("resolves complex references with project, environment, and service variables", () => {
|
||||
const complexServiceEnv = `
|
||||
FULL_DATABASE_URL=\${{project.DATABASE_URL}}/\${{environment.DATABASE_NAME}}
|
||||
API_ENDPOINT=\${{environment.API_URL}}/\${{project.ENVIRONMENT}}/api
|
||||
SERVICE_NAME=my-service
|
||||
COMPLEX_VAR=\${{SERVICE_NAME}}-\${{environment.NODE_ENV}}-\${{project.ENVIRONMENT}}
|
||||
`;
|
||||
|
||||
const resolved = prepareEnvironmentVariables(
|
||||
complexServiceEnv,
|
||||
projectEnv,
|
||||
environmentEnv,
|
||||
);
|
||||
|
||||
expect(resolved).toEqual([
|
||||
"FULL_DATABASE_URL=postgres://postgres:postgres@localhost:5432/project_db/dev_database",
|
||||
"API_ENDPOINT=https://api.dev.example.com/staging/api",
|
||||
"SERVICE_NAME=my-service",
|
||||
"COMPLEX_VAR=my-service-development-staging",
|
||||
]);
|
||||
});
|
||||
|
||||
it("handles environment variables with special characters", () => {
|
||||
const specialEnvVars = `
|
||||
SPECIAL_URL=https://special.com
|
||||
COMPLEX_KEY="key-with-@#$%^&*()"
|
||||
JWT_SECRET="secret-with-spaces and symbols!@#"
|
||||
`;
|
||||
|
||||
const serviceWithSpecial = `
|
||||
FULL_URL=\${{environment.SPECIAL_URL}}/path?key=\${{environment.COMPLEX_KEY}}
|
||||
AUTH_SECRET=\${{environment.JWT_SECRET}}
|
||||
`;
|
||||
|
||||
const resolved = prepareEnvironmentVariables(
|
||||
serviceWithSpecial,
|
||||
"",
|
||||
specialEnvVars,
|
||||
);
|
||||
|
||||
expect(resolved).toEqual([
|
||||
"FULL_URL=https://special.com/path?key=key-with-@#$%^&*()",
|
||||
"AUTH_SECRET=secret-with-spaces and symbols!@#",
|
||||
]);
|
||||
});
|
||||
|
||||
it("maintains precedence: service > environment > project", () => {
|
||||
const conflictingProjectEnv = `
|
||||
NODE_ENV=production-project
|
||||
API_URL=https://project.api.com
|
||||
DATABASE_NAME=project_db
|
||||
`;
|
||||
|
||||
const conflictingEnvironmentEnv = `
|
||||
NODE_ENV=development-environment
|
||||
API_URL=https://environment.api.com
|
||||
DATABASE_NAME=env_db
|
||||
`;
|
||||
|
||||
const serviceWithConflicts = `
|
||||
NODE_ENV=service-override
|
||||
PROJECT_ENV=\${{project.NODE_ENV}}
|
||||
ENV_VAR=\${{environment.API_URL}}
|
||||
DB_NAME=\${{environment.DATABASE_NAME}}
|
||||
`;
|
||||
|
||||
const resolved = prepareEnvironmentVariables(
|
||||
serviceWithConflicts,
|
||||
conflictingProjectEnv,
|
||||
conflictingEnvironmentEnv,
|
||||
);
|
||||
|
||||
expect(resolved).toEqual([
|
||||
"NODE_ENV=service-override", // Service wins
|
||||
"PROJECT_ENV=production-project", // Project reference
|
||||
"ENV_VAR=https://environment.api.com", // Environment reference
|
||||
"DB_NAME=env_db", // Environment reference
|
||||
]);
|
||||
});
|
||||
|
||||
it("handles empty environment variables", () => {
|
||||
const serviceWithEmpty = `
|
||||
SERVICE_VAR=test
|
||||
PROJECT_VAR=\${{project.ENVIRONMENT}}
|
||||
`;
|
||||
|
||||
const resolved = prepareEnvironmentVariables(
|
||||
serviceWithEmpty,
|
||||
projectEnv,
|
||||
"",
|
||||
);
|
||||
|
||||
expect(resolved).toEqual(["SERVICE_VAR=test", "PROJECT_VAR=staging"]);
|
||||
});
|
||||
|
||||
it("handles mixed quotes and environment variables", () => {
|
||||
const envWithQuotes = `
|
||||
QUOTED_VAR="development"
|
||||
SINGLE_QUOTED='https://api.dev.example.com'
|
||||
MIXED_VAR="value with 'single' quotes"
|
||||
`;
|
||||
|
||||
const serviceWithQuotes = `
|
||||
NODE_ENV=\${{environment.QUOTED_VAR}}
|
||||
API_URL=\${{environment.SINGLE_QUOTED}}
|
||||
COMPLEX="Prefix-\${{environment.MIXED_VAR}}-Suffix"
|
||||
`;
|
||||
|
||||
const resolved = prepareEnvironmentVariables(
|
||||
serviceWithQuotes,
|
||||
"",
|
||||
envWithQuotes,
|
||||
);
|
||||
|
||||
expect(resolved).toEqual([
|
||||
"NODE_ENV=development",
|
||||
"API_URL=https://api.dev.example.com",
|
||||
"COMPLEX=Prefix-value with 'single' quotes-Suffix",
|
||||
]);
|
||||
});
|
||||
|
||||
it("resolves multiple environment references in single value", () => {
|
||||
const multiRefEnv = `
|
||||
HOST=localhost
|
||||
PORT=5432
|
||||
USERNAME=postgres
|
||||
PASSWORD=secret123
|
||||
`;
|
||||
|
||||
const serviceWithMultiRefs = `
|
||||
DATABASE_URL=postgresql://\${{environment.USERNAME}}:\${{environment.PASSWORD}}@\${{environment.HOST}}:\${{environment.PORT}}/mydb
|
||||
CONNECTION_STRING=\${{environment.HOST}}:\${{environment.PORT}}
|
||||
`;
|
||||
|
||||
const resolved = prepareEnvironmentVariables(
|
||||
serviceWithMultiRefs,
|
||||
"",
|
||||
multiRefEnv,
|
||||
);
|
||||
|
||||
expect(resolved).toEqual([
|
||||
"DATABASE_URL=postgresql://postgres:secret123@localhost:5432/mydb",
|
||||
"CONNECTION_STRING=localhost:5432",
|
||||
]);
|
||||
});
|
||||
|
||||
it("handles nested references with environment and project variables", () => {
|
||||
const nestedProjectEnv = `
|
||||
BASE_DOMAIN=example.com
|
||||
PROTOCOL=https
|
||||
`;
|
||||
|
||||
const nestedEnvironmentEnv = `
|
||||
SUBDOMAIN=api.dev
|
||||
PATH_PREFIX=/v1
|
||||
`;
|
||||
|
||||
const serviceWithNested = `
|
||||
FULL_URL=\${{project.PROTOCOL}}://\${{environment.SUBDOMAIN}}.\${{project.BASE_DOMAIN}}\${{environment.PATH_PREFIX}}/endpoint
|
||||
API_BASE=\${{project.PROTOCOL}}://\${{environment.SUBDOMAIN}}.\${{project.BASE_DOMAIN}}
|
||||
`;
|
||||
|
||||
const resolved = prepareEnvironmentVariables(
|
||||
serviceWithNested,
|
||||
nestedProjectEnv,
|
||||
nestedEnvironmentEnv,
|
||||
);
|
||||
|
||||
expect(resolved).toEqual([
|
||||
"FULL_URL=https://api.dev.example.com/v1/endpoint",
|
||||
"API_BASE=https://api.dev.example.com",
|
||||
]);
|
||||
});
|
||||
|
||||
it("throws error for malformed environment variable references", () => {
|
||||
const serviceWithMalformed = `
|
||||
MALFORMED1=\${{environment.}}
|
||||
MALFORMED2=\${{environment}}
|
||||
VALID=\${{environment.NODE_ENV}}
|
||||
`;
|
||||
|
||||
// Should throw error for empty variable name after environment.
|
||||
expect(() =>
|
||||
prepareEnvironmentVariables(serviceWithMalformed, "", environmentEnv),
|
||||
).toThrow("Invalid environment variable: environment.");
|
||||
});
|
||||
|
||||
it("handles environment variables with numeric values", () => {
|
||||
const numericEnv = `
|
||||
PORT=8080
|
||||
TIMEOUT=30
|
||||
RETRY_COUNT=3
|
||||
PERCENTAGE=99.5
|
||||
`;
|
||||
|
||||
const serviceWithNumeric = `
|
||||
SERVER_PORT=\${{environment.PORT}}
|
||||
REQUEST_TIMEOUT=\${{environment.TIMEOUT}}
|
||||
MAX_RETRIES=\${{environment.RETRY_COUNT}}
|
||||
SUCCESS_RATE=\${{environment.PERCENTAGE}}
|
||||
`;
|
||||
|
||||
const resolved = prepareEnvironmentVariables(
|
||||
serviceWithNumeric,
|
||||
"",
|
||||
numericEnv,
|
||||
);
|
||||
|
||||
expect(resolved).toEqual([
|
||||
"SERVER_PORT=8080",
|
||||
"REQUEST_TIMEOUT=30",
|
||||
"MAX_RETRIES=3",
|
||||
"SUCCESS_RATE=99.5",
|
||||
]);
|
||||
});
|
||||
|
||||
it("handles boolean-like environment variables", () => {
|
||||
const booleanEnv = `
|
||||
DEBUG=true
|
||||
ENABLED=false
|
||||
PRODUCTION=1
|
||||
DEVELOPMENT=0
|
||||
`;
|
||||
|
||||
const serviceWithBoolean = `
|
||||
DEBUG_MODE=\${{environment.DEBUG}}
|
||||
FEATURE_ENABLED=\${{environment.ENABLED}}
|
||||
IS_PROD=\${{environment.PRODUCTION}}
|
||||
IS_DEV=\${{environment.DEVELOPMENT}}
|
||||
`;
|
||||
|
||||
const resolved = prepareEnvironmentVariables(
|
||||
serviceWithBoolean,
|
||||
"",
|
||||
booleanEnv,
|
||||
);
|
||||
|
||||
expect(resolved).toEqual([
|
||||
"DEBUG_MODE=true",
|
||||
"FEATURE_ENABLED=false",
|
||||
"IS_PROD=1",
|
||||
"IS_DEV=0",
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -36,22 +36,13 @@ const baseApp: ApplicationNested = {
|
||||
previewLimit: 0,
|
||||
previewCustomCertResolver: null,
|
||||
previewWildcard: "",
|
||||
environmentId: "",
|
||||
environment: {
|
||||
project: {
|
||||
env: "",
|
||||
environmentId: "",
|
||||
organizationId: "",
|
||||
name: "",
|
||||
createdAt: "",
|
||||
description: "",
|
||||
createdAt: "",
|
||||
projectId: "",
|
||||
project: {
|
||||
env: "",
|
||||
organizationId: "",
|
||||
name: "",
|
||||
description: "",
|
||||
createdAt: "",
|
||||
projectId: "",
|
||||
},
|
||||
},
|
||||
buildPath: "/",
|
||||
gitlabPathNamespace: "",
|
||||
@@ -94,6 +85,7 @@ const baseApp: ApplicationNested = {
|
||||
password: null,
|
||||
placementSwarm: null,
|
||||
ports: [],
|
||||
projectId: "",
|
||||
publishDirectory: null,
|
||||
isStaticSpa: null,
|
||||
redirects: [],
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { Clock, Loader2, RefreshCcw, RocketIcon, Settings } from "lucide-react";
|
||||
import React, { useEffect, useMemo, useState } from "react";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { AlertBlock } from "@/components/shared/alert-block";
|
||||
import { DateTooltip } from "@/components/shared/date-tooltip";
|
||||
import { DialogAction } from "@/components/shared/dialog-action";
|
||||
import { StatusTooltip } from "@/components/shared/status-tooltip";
|
||||
@@ -62,48 +61,12 @@ export const ShowDeployments = ({
|
||||
},
|
||||
);
|
||||
|
||||
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||
|
||||
const { mutateAsync: rollback, isLoading: isRollingBack } =
|
||||
api.rollback.rollback.useMutation();
|
||||
const { mutateAsync: killProcess, isLoading: isKillingProcess } =
|
||||
api.deployment.killProcess.useMutation();
|
||||
|
||||
// Cancel deployment mutations
|
||||
const {
|
||||
mutateAsync: cancelApplicationDeployment,
|
||||
isLoading: isCancellingApp,
|
||||
} = api.application.cancelDeployment.useMutation();
|
||||
const {
|
||||
mutateAsync: cancelComposeDeployment,
|
||||
isLoading: isCancellingCompose,
|
||||
} = api.compose.cancelDeployment.useMutation();
|
||||
|
||||
const [url, setUrl] = React.useState("");
|
||||
|
||||
// Check for stuck deployment (more than 9 minutes) - only for the most recent deployment
|
||||
const stuckDeployment = useMemo(() => {
|
||||
if (!isCloud || !deployments || deployments.length === 0) return null;
|
||||
|
||||
const now = Date.now();
|
||||
const NINE_MINUTES = 10 * 60 * 1000; // 9 minutes in milliseconds
|
||||
|
||||
// Get the most recent deployment (first in the list since they're sorted by date)
|
||||
const mostRecentDeployment = deployments[0];
|
||||
|
||||
if (
|
||||
!mostRecentDeployment ||
|
||||
mostRecentDeployment.status !== "running" ||
|
||||
!mostRecentDeployment.startedAt
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const startTime = new Date(mostRecentDeployment.startedAt).getTime();
|
||||
const elapsed = now - startTime;
|
||||
|
||||
return elapsed > NINE_MINUTES ? mostRecentDeployment : null;
|
||||
}, [isCloud, deployments]);
|
||||
useEffect(() => {
|
||||
setUrl(document.location.origin);
|
||||
}, []);
|
||||
@@ -114,7 +77,7 @@ export const ShowDeployments = ({
|
||||
<div className="flex flex-col gap-2">
|
||||
<CardTitle className="text-xl">Deployments</CardTitle>
|
||||
<CardDescription>
|
||||
See the last 10 deployments for this {type}
|
||||
See all the 10 last deployments for this {type}
|
||||
</CardDescription>
|
||||
</div>
|
||||
<div className="flex flex-row items-center gap-2">
|
||||
@@ -131,54 +94,6 @@ export const ShowDeployments = ({
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-col gap-4">
|
||||
{stuckDeployment && (type === "application" || type === "compose") && (
|
||||
<AlertBlock
|
||||
type="warning"
|
||||
className="flex-col items-start w-full p-4"
|
||||
>
|
||||
<div className="flex flex-col gap-3">
|
||||
<div>
|
||||
<div className="font-medium text-sm mb-1">
|
||||
Build appears to be stuck
|
||||
</div>
|
||||
<p className="text-sm">
|
||||
Hey! Looks like the build has been running for more than 10
|
||||
minutes. Would you like to cancel this deployment?
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
variant="destructive"
|
||||
size="sm"
|
||||
className="w-fit"
|
||||
isLoading={
|
||||
type === "application" ? isCancellingApp : isCancellingCompose
|
||||
}
|
||||
onClick={async () => {
|
||||
try {
|
||||
if (type === "application") {
|
||||
await cancelApplicationDeployment({
|
||||
applicationId: id,
|
||||
});
|
||||
} else if (type === "compose") {
|
||||
await cancelComposeDeployment({
|
||||
composeId: id,
|
||||
});
|
||||
}
|
||||
toast.success("Deployment cancellation requested");
|
||||
} catch (error) {
|
||||
toast.error(
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: "Failed to cancel deployment",
|
||||
);
|
||||
}
|
||||
}}
|
||||
>
|
||||
Cancel Deployment
|
||||
</Button>
|
||||
</div>
|
||||
</AlertBlock>
|
||||
)}
|
||||
{refreshToken && (
|
||||
<div className="flex flex-col gap-2 text-sm">
|
||||
<span>
|
||||
@@ -189,9 +104,7 @@ export const ShowDeployments = ({
|
||||
<span>Webhook URL: </span>
|
||||
<div className="flex flex-row items-center gap-2">
|
||||
<span className="break-all text-muted-foreground">
|
||||
{`${url}/api/deploy${
|
||||
type === "compose" ? "/compose" : ""
|
||||
}/${refreshToken}`}
|
||||
{`${url}/api/deploy${type === "compose" ? "/compose" : ""}/${refreshToken}`}
|
||||
</span>
|
||||
{(type === "application" || type === "compose") && (
|
||||
<RefreshToken id={id} type={type} />
|
||||
|
||||
@@ -69,7 +69,7 @@ export const ShowGeneralApplication = ({ applicationId }: Props) => {
|
||||
toast.success("Application deployed successfully");
|
||||
refetch();
|
||||
router.push(
|
||||
`/dashboard/project/${data?.environment.projectId}/environment/${data?.environmentId}/services/application/${applicationId}?tab=deployments`,
|
||||
`/dashboard/project/${data?.projectId}/services/application/${applicationId}?tab=deployments`,
|
||||
);
|
||||
})
|
||||
.catch(() => {
|
||||
|
||||
@@ -58,7 +58,7 @@ export const ShowSchedules = ({ id, scheduleType = "application" }: Props) => {
|
||||
return (
|
||||
<Card className="border px-6 shadow-none bg-transparent h-full min-h-[50vh]">
|
||||
<CardHeader className="px-0">
|
||||
<div className="flex justify-between items-center gap-y-2 flex-wrap">
|
||||
<div className="flex justify-between items-center">
|
||||
<div className="flex flex-col gap-2">
|
||||
<CardTitle className="text-xl font-bold flex items-center gap-2">
|
||||
Scheduled Tasks
|
||||
@@ -91,15 +91,15 @@ export const ShowSchedules = ({ id, scheduleType = "application" }: Props) => {
|
||||
return (
|
||||
<div
|
||||
key={schedule.scheduleId}
|
||||
className="flex items-center flex-wrap sm:flex-nowrap gap-y-2 justify-between rounded-lg border p-3 transition-colors bg-muted/50"
|
||||
className="flex items-center justify-between rounded-lg border p-3 transition-colors bg-muted/50"
|
||||
>
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="flex flex-shrink-0 h-9 w-9 items-center justify-center rounded-full bg-primary/5">
|
||||
<div className="flex h-9 w-9 items-center justify-center rounded-full bg-primary/5">
|
||||
<Clock className="size-4 text-primary/70" />
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<h3 className="text-sm font-medium leading-none [overflow-wrap:anywhere] line-clamp-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<h3 className="text-sm font-medium leading-none">
|
||||
{schedule.name}
|
||||
</h3>
|
||||
<Badge
|
||||
@@ -109,7 +109,7 @@ export const ShowSchedules = ({ id, scheduleType = "application" }: Props) => {
|
||||
{schedule.enabled ? "Enabled" : "Disabled"}
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-sm text-muted-foreground flex-wrap">
|
||||
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="font-mono text-[10px] bg-transparent"
|
||||
@@ -142,7 +142,7 @@ export const ShowSchedules = ({ id, scheduleType = "application" }: Props) => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-0.5 md:gap-1.5">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<ShowDeploymentsModal
|
||||
id={schedule.scheduleId}
|
||||
type="schedule"
|
||||
@@ -226,7 +226,7 @@ export const ShowSchedules = ({ id, scheduleType = "application" }: Props) => {
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col gap-2 items-center justify-center py-12 rounded-lg">
|
||||
<div className="flex flex-col gap-2 items-center justify-center py-12 rounded-lg">
|
||||
<Clock className="size-8 mb-4 text-muted-foreground" />
|
||||
<p className="text-lg font-medium text-muted-foreground">
|
||||
No scheduled tasks
|
||||
|
||||
@@ -101,9 +101,7 @@ export const DeleteService = ({ id, type }: Props) => {
|
||||
deleteVolumes,
|
||||
})
|
||||
.then((result) => {
|
||||
push(
|
||||
`/dashboard/project/${result?.environment?.projectId}/environment/${result?.environment?.environmentId}`,
|
||||
);
|
||||
push(`/dashboard/project/${result?.projectId}`);
|
||||
toast.success("deleted successfully");
|
||||
setIsOpen(false);
|
||||
})
|
||||
|
||||
@@ -47,7 +47,7 @@ export const ComposeActions = ({ composeId }: Props) => {
|
||||
toast.success("Compose deployed successfully");
|
||||
refetch();
|
||||
router.push(
|
||||
`/dashboard/project/${data?.environment.projectId}/environment/${data?.environmentId}/services/compose/${composeId}?tab=deployments`,
|
||||
`/dashboard/project/${data?.project.projectId}/services/compose/${composeId}?tab=deployments`,
|
||||
);
|
||||
})
|
||||
.catch(() => {
|
||||
|
||||
@@ -102,9 +102,9 @@ export const ShowExternalMariadbCredentials = ({ mariadbId }: Props) => {
|
||||
<CardHeader>
|
||||
<CardTitle className="text-xl">External Credentials</CardTitle>
|
||||
<CardDescription>
|
||||
In order to make the database reachable through the internet, you
|
||||
must set a port and ensure that the port is not being used by
|
||||
another application or database
|
||||
In order to make the database reachable trought internet is
|
||||
required to set a port, make sure the port is not used by another
|
||||
application or database
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="flex w-full flex-col gap-4">
|
||||
|
||||
@@ -102,9 +102,9 @@ export const ShowExternalMongoCredentials = ({ mongoId }: Props) => {
|
||||
<CardHeader>
|
||||
<CardTitle className="text-xl">External Credentials</CardTitle>
|
||||
<CardDescription>
|
||||
In order to make the database reachable through the internet, you
|
||||
must set a port and ensure that the port is not being used by
|
||||
another application or database
|
||||
In order to make the database reachable trought internet is
|
||||
required to set a port, make sure the port is not used by another
|
||||
application or database
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="flex w-full flex-col gap-4">
|
||||
|
||||
@@ -102,9 +102,9 @@ export const ShowExternalMysqlCredentials = ({ mysqlId }: Props) => {
|
||||
<CardHeader>
|
||||
<CardTitle className="text-xl">External Credentials</CardTitle>
|
||||
<CardDescription>
|
||||
In order to make the database reachable through the internet, you
|
||||
must set a port and ensure that the port is not being used by
|
||||
another application or database
|
||||
In order to make the database reachable trought internet is
|
||||
required to set a port, make sure the port is not used by another
|
||||
application or database
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="flex w-full flex-col gap-4">
|
||||
|
||||
@@ -104,9 +104,9 @@ export const ShowExternalPostgresCredentials = ({ postgresId }: Props) => {
|
||||
<CardHeader>
|
||||
<CardTitle className="text-xl">External Credentials</CardTitle>
|
||||
<CardDescription>
|
||||
In order to make the database reachable through the internet, you
|
||||
must set a port and ensure that the port is not being used by
|
||||
another application or database
|
||||
In order to make the database reachable trought internet is
|
||||
required to set a port, make sure the port is not used by another
|
||||
application or database
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="flex w-full flex-col gap-4">
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { TemplateGenerator } from "@/components/dashboard/project/ai/template-generator";
|
||||
|
||||
interface Props {
|
||||
environmentId: string;
|
||||
projectId: string;
|
||||
projectName?: string;
|
||||
}
|
||||
|
||||
export const AddAiAssistant = ({ environmentId }: Props) => {
|
||||
return <TemplateGenerator environmentId={environmentId} />;
|
||||
export const AddAiAssistant = ({ projectId }: Props) => {
|
||||
return <TemplateGenerator projectId={projectId} />;
|
||||
};
|
||||
|
||||
@@ -64,11 +64,11 @@ const AddTemplateSchema = z.object({
|
||||
type AddTemplate = z.infer<typeof AddTemplateSchema>;
|
||||
|
||||
interface Props {
|
||||
environmentId: string;
|
||||
projectId: string;
|
||||
projectName?: string;
|
||||
}
|
||||
|
||||
export const AddApplication = ({ environmentId, projectName }: Props) => {
|
||||
export const AddApplication = ({ projectId, projectName }: Props) => {
|
||||
const utils = api.useUtils();
|
||||
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||
const [visible, setVisible] = useState(false);
|
||||
@@ -76,10 +76,6 @@ export const AddApplication = ({ environmentId, projectName }: Props) => {
|
||||
const { data: servers } = api.server.withSSHKey.useQuery();
|
||||
|
||||
const hasServers = servers && servers.length > 0;
|
||||
// Show dropdown logic based on cloud environment
|
||||
// Cloud: show only if there are remote servers (no Dokploy option)
|
||||
// Self-hosted: show only if there are remote servers (Dokploy is default, hide if no remote servers)
|
||||
const shouldShowServerDropdown = hasServers;
|
||||
|
||||
const { mutateAsync, isLoading, error, isError } =
|
||||
api.application.create.useMutation();
|
||||
@@ -98,15 +94,15 @@ export const AddApplication = ({ environmentId, projectName }: Props) => {
|
||||
name: data.name,
|
||||
appName: data.appName,
|
||||
description: data.description,
|
||||
serverId: data.serverId === "dokploy" ? undefined : data.serverId,
|
||||
environmentId,
|
||||
projectId,
|
||||
serverId: data.serverId,
|
||||
})
|
||||
.then(async () => {
|
||||
toast.success("Service Created");
|
||||
form.reset();
|
||||
setVisible(false);
|
||||
await utils.environment.one.invalidate({
|
||||
environmentId,
|
||||
await utils.project.one.invalidate({
|
||||
projectId,
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
@@ -161,7 +157,7 @@ export const AddApplication = ({ environmentId, projectName }: Props) => {
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
{shouldShowServerDropdown && (
|
||||
{hasServers && (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="serverId"
|
||||
@@ -190,27 +186,13 @@ export const AddApplication = ({ environmentId, projectName }: Props) => {
|
||||
|
||||
<Select
|
||||
onValueChange={field.onChange}
|
||||
defaultValue={
|
||||
field.value || (!isCloud ? "dokploy" : undefined)
|
||||
}
|
||||
defaultValue={field.value}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue
|
||||
placeholder={!isCloud ? "Dokploy" : "Select a Server"}
|
||||
/>
|
||||
<SelectValue placeholder="Select a Server" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
{!isCloud && (
|
||||
<SelectItem value="dokploy">
|
||||
<span className="flex items-center gap-2 justify-between w-full">
|
||||
<span>Dokploy</span>
|
||||
<span className="text-muted-foreground text-xs self-center">
|
||||
Default
|
||||
</span>
|
||||
</span>
|
||||
</SelectItem>
|
||||
)}
|
||||
{servers?.map((server) => (
|
||||
<SelectItem
|
||||
key={server.serverId}
|
||||
@@ -224,9 +206,7 @@ export const AddApplication = ({ environmentId, projectName }: Props) => {
|
||||
</span>
|
||||
</SelectItem>
|
||||
))}
|
||||
<SelectLabel>
|
||||
Servers ({servers?.length + (!isCloud ? 1 : 0)})
|
||||
</SelectLabel>
|
||||
<SelectLabel>Servers ({servers?.length})</SelectLabel>
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
@@ -65,11 +65,11 @@ const AddComposeSchema = z.object({
|
||||
type AddCompose = z.infer<typeof AddComposeSchema>;
|
||||
|
||||
interface Props {
|
||||
environmentId: string;
|
||||
projectId: string;
|
||||
projectName?: string;
|
||||
}
|
||||
|
||||
export const AddCompose = ({ environmentId, projectName }: Props) => {
|
||||
export const AddCompose = ({ projectId, projectName }: Props) => {
|
||||
const utils = api.useUtils();
|
||||
const [visible, setVisible] = useState(false);
|
||||
const slug = slugify(projectName);
|
||||
@@ -78,14 +78,7 @@ export const AddCompose = ({ environmentId, projectName }: Props) => {
|
||||
const { mutateAsync, isLoading, error, isError } =
|
||||
api.compose.create.useMutation();
|
||||
|
||||
// Get environment data to extract projectId
|
||||
const { data: environment } = api.environment.one.useQuery({ environmentId });
|
||||
|
||||
const hasServers = servers && servers.length > 0;
|
||||
// Show dropdown logic based on cloud environment
|
||||
// Cloud: show only if there are remote servers (no Dokploy option)
|
||||
// Self-hosted: show only if there are remote servers (Dokploy is default, hide if no remote servers)
|
||||
const shouldShowServerDropdown = hasServers;
|
||||
|
||||
const form = useForm<AddCompose>({
|
||||
defaultValues: {
|
||||
@@ -105,17 +98,16 @@ export const AddCompose = ({ environmentId, projectName }: Props) => {
|
||||
await mutateAsync({
|
||||
name: data.name,
|
||||
description: data.description,
|
||||
environmentId,
|
||||
projectId,
|
||||
composeType: data.composeType,
|
||||
appName: data.appName,
|
||||
serverId: data.serverId === "dokploy" ? undefined : data.serverId,
|
||||
serverId: data.serverId,
|
||||
})
|
||||
.then(async () => {
|
||||
toast.success("Compose Created");
|
||||
setVisible(false);
|
||||
// Invalidate the project query to refresh the environment data
|
||||
await utils.environment.one.invalidate({
|
||||
environmentId,
|
||||
await utils.project.one.invalidate({
|
||||
projectId,
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
@@ -173,7 +165,7 @@ export const AddCompose = ({ environmentId, projectName }: Props) => {
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
{shouldShowServerDropdown && (
|
||||
{hasServers && (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="serverId"
|
||||
@@ -202,27 +194,13 @@ export const AddCompose = ({ environmentId, projectName }: Props) => {
|
||||
|
||||
<Select
|
||||
onValueChange={field.onChange}
|
||||
defaultValue={
|
||||
field.value || (!isCloud ? "dokploy" : undefined)
|
||||
}
|
||||
defaultValue={field.value}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue
|
||||
placeholder={!isCloud ? "Dokploy" : "Select a Server"}
|
||||
/>
|
||||
<SelectValue placeholder="Select a Server" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
{!isCloud && (
|
||||
<SelectItem value="dokploy">
|
||||
<span className="flex items-center gap-2 justify-between w-full">
|
||||
<span>Dokploy</span>
|
||||
<span className="text-muted-foreground text-xs self-center">
|
||||
Default
|
||||
</span>
|
||||
</span>
|
||||
</SelectItem>
|
||||
)}
|
||||
{servers?.map((server) => (
|
||||
<SelectItem
|
||||
key={server.serverId}
|
||||
@@ -236,9 +214,7 @@ export const AddCompose = ({ environmentId, projectName }: Props) => {
|
||||
</span>
|
||||
</SelectItem>
|
||||
))}
|
||||
<SelectLabel>
|
||||
Servers ({servers?.length + (!isCloud ? 1 : 0)})
|
||||
</SelectLabel>
|
||||
<SelectLabel>Servers ({servers?.length})</SelectLabel>
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
@@ -83,12 +83,7 @@ const baseDatabaseSchema = z.object({
|
||||
message:
|
||||
"App name supports lowercase letters, numbers, '-' and can only start and end letters, and does not support continuous '-'",
|
||||
}),
|
||||
databasePassword: z
|
||||
.string()
|
||||
.regex(/^[a-zA-Z0-9@#%^&*()_+\-=[\]{}|;:,.<>?~`]*$/, {
|
||||
message:
|
||||
"Password contains invalid characters. Please avoid: $ ! ' \" \\ / and space characters for database compatibility",
|
||||
}),
|
||||
databasePassword: z.string(),
|
||||
dockerImage: z.string(),
|
||||
description: z.string().nullable(),
|
||||
serverId: z.string().nullable(),
|
||||
@@ -117,13 +112,7 @@ const mySchema = z.discriminatedUnion("type", [
|
||||
z
|
||||
.object({
|
||||
type: z.literal("mysql"),
|
||||
databaseRootPassword: z
|
||||
.string()
|
||||
.regex(/^[a-zA-Z0-9@#%^&*()_+\-=[\]{}|;:,.<>?~`]*$/, {
|
||||
message:
|
||||
"Password contains invalid characters. Please avoid: $ ! ' \" \\ / and space characters for database compatibility",
|
||||
})
|
||||
.optional(),
|
||||
databaseRootPassword: z.string().default(""),
|
||||
databaseUser: z.string().default("mysql"),
|
||||
databaseName: z.string().default("mysql"),
|
||||
})
|
||||
@@ -132,13 +121,7 @@ const mySchema = z.discriminatedUnion("type", [
|
||||
.object({
|
||||
type: z.literal("mariadb"),
|
||||
dockerImage: z.string().default("mariadb:4"),
|
||||
databaseRootPassword: z
|
||||
.string()
|
||||
.regex(/^[a-zA-Z0-9@#%^&*()_+\-=[\]{}|;:,.<>?~`]*$/, {
|
||||
message:
|
||||
"Password contains invalid characters. Please avoid: $ ! ' \" \\ / and space characters for database compatibility",
|
||||
})
|
||||
.optional(),
|
||||
databaseRootPassword: z.string().default(""),
|
||||
databaseUser: z.string().default("mariadb"),
|
||||
databaseName: z.string().default("mariadb"),
|
||||
})
|
||||
@@ -171,15 +154,14 @@ const databasesMap = {
|
||||
type AddDatabase = z.infer<typeof mySchema>;
|
||||
|
||||
interface Props {
|
||||
environmentId: string;
|
||||
projectId: string;
|
||||
projectName?: string;
|
||||
}
|
||||
|
||||
export const AddDatabase = ({ environmentId, projectName }: Props) => {
|
||||
export const AddDatabase = ({ projectId, projectName }: Props) => {
|
||||
const utils = api.useUtils();
|
||||
const [visible, setVisible] = useState(false);
|
||||
const slug = slugify(projectName);
|
||||
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||
const { data: servers } = api.server.withSSHKey.useQuery();
|
||||
const postgresMutation = api.postgres.create.useMutation();
|
||||
const mongoMutation = api.mongo.create.useMutation();
|
||||
@@ -187,14 +169,7 @@ export const AddDatabase = ({ environmentId, projectName }: Props) => {
|
||||
const mariadbMutation = api.mariadb.create.useMutation();
|
||||
const mysqlMutation = api.mysql.create.useMutation();
|
||||
|
||||
// Get environment data to extract projectId
|
||||
const { data: environment } = api.environment.one.useQuery({ environmentId });
|
||||
|
||||
const hasServers = servers && servers.length > 0;
|
||||
// Show dropdown logic based on cloud environment
|
||||
// Cloud: show only if there are remote servers (no Dokploy option)
|
||||
// Self-hosted: show only if there are remote servers (Dokploy is default, hide if no remote servers)
|
||||
const shouldShowServerDropdown = hasServers;
|
||||
|
||||
const form = useForm<AddDatabase>({
|
||||
defaultValues: {
|
||||
@@ -228,8 +203,8 @@ export const AddDatabase = ({ environmentId, projectName }: Props) => {
|
||||
name: data.name,
|
||||
appName: data.appName,
|
||||
dockerImage: defaultDockerImage,
|
||||
serverId: data.serverId === "dokploy" ? undefined : data.serverId,
|
||||
environmentId,
|
||||
projectId,
|
||||
serverId: data.serverId,
|
||||
description: data.description,
|
||||
};
|
||||
|
||||
@@ -241,7 +216,7 @@ export const AddDatabase = ({ environmentId, projectName }: Props) => {
|
||||
|
||||
databaseUser:
|
||||
data.databaseUser || databasesUserDefaultPlaceholder[data.type],
|
||||
serverId: data.serverId === "dokploy" ? null : data.serverId,
|
||||
serverId: data.serverId,
|
||||
});
|
||||
} else if (data.type === "mongo") {
|
||||
promise = mongoMutation.mutateAsync({
|
||||
@@ -249,24 +224,25 @@ export const AddDatabase = ({ environmentId, projectName }: Props) => {
|
||||
databasePassword: data.databasePassword,
|
||||
databaseUser:
|
||||
data.databaseUser || databasesUserDefaultPlaceholder[data.type],
|
||||
serverId: data.serverId === "dokploy" ? null : data.serverId,
|
||||
serverId: data.serverId,
|
||||
replicaSets: data.replicaSets,
|
||||
});
|
||||
} else if (data.type === "redis") {
|
||||
promise = redisMutation.mutateAsync({
|
||||
...commonParams,
|
||||
databasePassword: data.databasePassword,
|
||||
serverId: data.serverId === "dokploy" ? null : data.serverId,
|
||||
serverId: data.serverId,
|
||||
projectId,
|
||||
});
|
||||
} else if (data.type === "mariadb") {
|
||||
promise = mariadbMutation.mutateAsync({
|
||||
...commonParams,
|
||||
databasePassword: data.databasePassword,
|
||||
databaseRootPassword: data.databaseRootPassword || "",
|
||||
databaseRootPassword: data.databaseRootPassword,
|
||||
databaseName: data.databaseName || "mariadb",
|
||||
databaseUser:
|
||||
data.databaseUser || databasesUserDefaultPlaceholder[data.type],
|
||||
serverId: data.serverId === "dokploy" ? null : data.serverId,
|
||||
serverId: data.serverId,
|
||||
});
|
||||
} else if (data.type === "mysql") {
|
||||
promise = mysqlMutation.mutateAsync({
|
||||
@@ -275,8 +251,8 @@ export const AddDatabase = ({ environmentId, projectName }: Props) => {
|
||||
databaseName: data.databaseName || "mysql",
|
||||
databaseUser:
|
||||
data.databaseUser || databasesUserDefaultPlaceholder[data.type],
|
||||
serverId: data.serverId === "dokploy" ? null : data.serverId,
|
||||
databaseRootPassword: data.databaseRootPassword || "",
|
||||
databaseRootPassword: data.databaseRootPassword,
|
||||
serverId: data.serverId,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -295,9 +271,8 @@ export const AddDatabase = ({ environmentId, projectName }: Props) => {
|
||||
databaseUser: "",
|
||||
});
|
||||
setVisible(false);
|
||||
// Invalidate the project query to refresh the environment data
|
||||
await utils.environment.one.invalidate({
|
||||
environmentId,
|
||||
await utils.project.one.invalidate({
|
||||
projectId,
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
@@ -407,7 +382,7 @@ export const AddDatabase = ({ environmentId, projectName }: Props) => {
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
{shouldShowServerDropdown && (
|
||||
{hasServers && (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="serverId"
|
||||
@@ -416,29 +391,13 @@ export const AddDatabase = ({ environmentId, projectName }: Props) => {
|
||||
<FormLabel>Select a Server</FormLabel>
|
||||
<Select
|
||||
onValueChange={field.onChange}
|
||||
defaultValue={
|
||||
field.value || (!isCloud ? "dokploy" : undefined)
|
||||
}
|
||||
defaultValue={field.value || ""}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue
|
||||
placeholder={
|
||||
!isCloud ? "Dokploy" : "Select a Server"
|
||||
}
|
||||
/>
|
||||
<SelectValue placeholder="Select a Server" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
{!isCloud && (
|
||||
<SelectItem value="dokploy">
|
||||
<span className="flex items-center gap-2 justify-between w-full">
|
||||
<span>Dokploy</span>
|
||||
<span className="text-muted-foreground text-xs self-center">
|
||||
Default
|
||||
</span>
|
||||
</span>
|
||||
</SelectItem>
|
||||
)}
|
||||
{servers?.map((server) => (
|
||||
<SelectItem
|
||||
key={server.serverId}
|
||||
@@ -448,7 +407,7 @@ export const AddDatabase = ({ environmentId, projectName }: Props) => {
|
||||
</SelectItem>
|
||||
))}
|
||||
<SelectLabel>
|
||||
Servers ({servers?.length + (!isCloud ? 1 : 0)})
|
||||
Servers ({servers?.length})
|
||||
</SelectLabel>
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
|
||||
@@ -73,11 +73,11 @@ import { api } from "@/utils/api";
|
||||
const TEMPLATE_BASE_URL_KEY = "dokploy_template_base_url";
|
||||
|
||||
interface Props {
|
||||
environmentId: string;
|
||||
projectId: string;
|
||||
baseUrl?: string;
|
||||
}
|
||||
|
||||
export const AddTemplate = ({ environmentId, baseUrl }: Props) => {
|
||||
export const AddTemplate = ({ projectId, baseUrl }: Props) => {
|
||||
const [query, setQuery] = useState("");
|
||||
const [open, setOpen] = useState(false);
|
||||
const [viewMode, setViewMode] = useState<"detailed" | "icon">("detailed");
|
||||
@@ -91,9 +91,6 @@ export const AddTemplate = ({ environmentId, baseUrl }: Props) => {
|
||||
return undefined;
|
||||
});
|
||||
|
||||
// Get environment data to extract projectId
|
||||
const { data: environment } = api.environment.one.useQuery({ environmentId });
|
||||
|
||||
// Save to localStorage when customBaseUrl changes
|
||||
useEffect(() => {
|
||||
if (customBaseUrl) {
|
||||
@@ -141,10 +138,6 @@ export const AddTemplate = ({ environmentId, baseUrl }: Props) => {
|
||||
}) || [];
|
||||
|
||||
const hasServers = servers && servers.length > 0;
|
||||
// Show dropdown logic based on cloud environment
|
||||
// Cloud: show only if there are remote servers (no Dokploy option)
|
||||
// Self-hosted: show only if there are remote servers (Dokploy is default, hide if no remote servers)
|
||||
const shouldShowServerDropdown = hasServers;
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
@@ -434,7 +427,7 @@ export const AddTemplate = ({ environmentId, baseUrl }: Props) => {
|
||||
project.
|
||||
</AlertDialogDescription>
|
||||
|
||||
{shouldShowServerDropdown && (
|
||||
{hasServers && (
|
||||
<div>
|
||||
<TooltipProvider delayDuration={0}>
|
||||
<Tooltip>
|
||||
@@ -463,29 +456,12 @@ export const AddTemplate = ({ environmentId, baseUrl }: Props) => {
|
||||
onValueChange={(e) => {
|
||||
setServerId(e);
|
||||
}}
|
||||
defaultValue={
|
||||
!isCloud ? "dokploy" : undefined
|
||||
}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue
|
||||
placeholder={
|
||||
!isCloud ? "Dokploy" : "Select a Server"
|
||||
}
|
||||
/>
|
||||
<SelectValue placeholder="Select a Server" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
{!isCloud && (
|
||||
<SelectItem value="dokploy">
|
||||
<span className="flex items-center gap-2 justify-between w-full">
|
||||
<span>Dokploy</span>
|
||||
<span className="text-muted-foreground text-xs self-center">
|
||||
Default
|
||||
</span>
|
||||
</span>
|
||||
</SelectItem>
|
||||
)}
|
||||
{servers?.map((server) => (
|
||||
<SelectItem
|
||||
key={server.serverId}
|
||||
@@ -500,8 +476,7 @@ export const AddTemplate = ({ environmentId, baseUrl }: Props) => {
|
||||
</SelectItem>
|
||||
))}
|
||||
<SelectLabel>
|
||||
Servers (
|
||||
{servers?.length + (!isCloud ? 1 : 0)})
|
||||
Servers ({servers?.length})
|
||||
</SelectLabel>
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
@@ -515,20 +490,16 @@ export const AddTemplate = ({ environmentId, baseUrl }: Props) => {
|
||||
disabled={isLoading}
|
||||
onClick={async () => {
|
||||
const promise = mutateAsync({
|
||||
serverId:
|
||||
serverId === "dokploy"
|
||||
? undefined
|
||||
: serverId,
|
||||
environmentId,
|
||||
projectId,
|
||||
serverId: serverId || undefined,
|
||||
id: template.id,
|
||||
baseUrl: customBaseUrl,
|
||||
});
|
||||
toast.promise(promise, {
|
||||
loading: "Setting up...",
|
||||
success: () => {
|
||||
// Invalidate the project query to refresh the environment data
|
||||
utils.environment.one.invalidate({
|
||||
environmentId,
|
||||
utils.project.one.invalidate({
|
||||
projectId,
|
||||
});
|
||||
setOpen(false);
|
||||
return `${template.name} template created successfully`;
|
||||
|
||||
@@ -1,446 +0,0 @@
|
||||
import type { findEnvironmentsByProjectId } from "@dokploy/server";
|
||||
import {
|
||||
ChevronDownIcon,
|
||||
PencilIcon,
|
||||
PlusIcon,
|
||||
Terminal,
|
||||
TrashIcon,
|
||||
} from "lucide-react";
|
||||
import { useRouter } from "next/router";
|
||||
import { useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { EnvironmentVariables } from "@/components/dashboard/project/environment-variables";
|
||||
import { AlertBlock } from "@/components/shared/alert-block";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { api } from "@/utils/api";
|
||||
|
||||
type Environment = Awaited<
|
||||
ReturnType<typeof findEnvironmentsByProjectId>
|
||||
>[number];
|
||||
interface AdvancedEnvironmentSelectorProps {
|
||||
projectId: string;
|
||||
currentEnvironmentId?: string;
|
||||
}
|
||||
|
||||
export const AdvancedEnvironmentSelector = ({
|
||||
projectId,
|
||||
currentEnvironmentId,
|
||||
}: AdvancedEnvironmentSelectorProps) => {
|
||||
const router = useRouter();
|
||||
const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false);
|
||||
const [isEditDialogOpen, setIsEditDialogOpen] = useState(false);
|
||||
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);
|
||||
const [selectedEnvironment, setSelectedEnvironment] =
|
||||
useState<Environment | null>(null);
|
||||
|
||||
const { data: environments } = api.environment.byProjectId.useQuery(
|
||||
{ projectId: projectId },
|
||||
{
|
||||
enabled: !!projectId,
|
||||
},
|
||||
);
|
||||
|
||||
// Form states
|
||||
const [name, setName] = useState("");
|
||||
const [description, setDescription] = useState("");
|
||||
|
||||
// API mutations
|
||||
const { data: environment } = api.environment.one.useQuery(
|
||||
{ environmentId: currentEnvironmentId || "" },
|
||||
{
|
||||
enabled: !!currentEnvironmentId,
|
||||
},
|
||||
);
|
||||
|
||||
const haveServices =
|
||||
selectedEnvironment &&
|
||||
((selectedEnvironment?.mariadb?.length || 0) > 0 ||
|
||||
(selectedEnvironment?.mongo?.length || 0) > 0 ||
|
||||
(selectedEnvironment?.mysql?.length || 0) > 0 ||
|
||||
(selectedEnvironment?.postgres?.length || 0) > 0 ||
|
||||
(selectedEnvironment?.redis?.length || 0) > 0 ||
|
||||
(selectedEnvironment?.applications?.length || 0) > 0 ||
|
||||
(selectedEnvironment?.compose?.length || 0) > 0);
|
||||
const createEnvironment = api.environment.create.useMutation();
|
||||
const updateEnvironment = api.environment.update.useMutation();
|
||||
const deleteEnvironment = api.environment.remove.useMutation();
|
||||
const duplicateEnvironment = api.environment.duplicate.useMutation();
|
||||
|
||||
// Refetch project data
|
||||
const utils = api.useUtils();
|
||||
|
||||
const handleCreateEnvironment = async () => {
|
||||
try {
|
||||
await createEnvironment.mutateAsync({
|
||||
projectId,
|
||||
name: name.trim(),
|
||||
description: description.trim() || null,
|
||||
});
|
||||
|
||||
toast.success("Environment created successfully");
|
||||
utils.environment.byProjectId.invalidate({ projectId });
|
||||
setIsCreateDialogOpen(false);
|
||||
setName("");
|
||||
setDescription("");
|
||||
} catch (error) {
|
||||
toast.error("Failed to create environment");
|
||||
}
|
||||
};
|
||||
|
||||
const handleUpdateEnvironment = async () => {
|
||||
if (!selectedEnvironment) return;
|
||||
|
||||
try {
|
||||
await updateEnvironment.mutateAsync({
|
||||
environmentId: selectedEnvironment.environmentId,
|
||||
name: name.trim(),
|
||||
description: description.trim() || null,
|
||||
});
|
||||
|
||||
toast.success("Environment updated successfully");
|
||||
utils.environment.byProjectId.invalidate({ projectId });
|
||||
setIsEditDialogOpen(false);
|
||||
setSelectedEnvironment(null);
|
||||
setName("");
|
||||
setDescription("");
|
||||
} catch (error) {
|
||||
toast.error("Failed to update environment");
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteEnvironment = async () => {
|
||||
if (!selectedEnvironment) return;
|
||||
|
||||
try {
|
||||
await deleteEnvironment.mutateAsync({
|
||||
environmentId: selectedEnvironment.environmentId,
|
||||
});
|
||||
|
||||
toast.success("Environment deleted successfully");
|
||||
utils.environment.byProjectId.invalidate({ projectId });
|
||||
setIsDeleteDialogOpen(false);
|
||||
setSelectedEnvironment(null);
|
||||
|
||||
// Redirect to production if we deleted the current environment
|
||||
if (selectedEnvironment.environmentId === currentEnvironmentId) {
|
||||
const productionEnv = environments?.find(
|
||||
(env) => env.name === "production",
|
||||
);
|
||||
if (productionEnv) {
|
||||
router.push(
|
||||
`/dashboard/project/${projectId}/environment/${productionEnv.environmentId}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
toast.error("Failed to delete environment");
|
||||
}
|
||||
};
|
||||
|
||||
const handleDuplicateEnvironment = async (environment: Environment) => {
|
||||
try {
|
||||
const result = await duplicateEnvironment.mutateAsync({
|
||||
environmentId: environment.environmentId,
|
||||
name: `${environment.name}-copy`,
|
||||
description: environment.description,
|
||||
});
|
||||
|
||||
toast.success("Environment duplicated successfully");
|
||||
utils.project.one.invalidate({ projectId });
|
||||
|
||||
// Navigate to the new duplicated environment
|
||||
router.push(
|
||||
`/dashboard/project/${projectId}/environment/${result.environmentId}`,
|
||||
);
|
||||
} catch (error) {
|
||||
toast.error("Failed to duplicate environment");
|
||||
}
|
||||
};
|
||||
|
||||
const openEditDialog = (environment: Environment) => {
|
||||
setSelectedEnvironment(environment);
|
||||
setName(environment.name);
|
||||
setDescription(environment.description || "");
|
||||
setIsEditDialogOpen(true);
|
||||
};
|
||||
|
||||
const openDeleteDialog = (environment: Environment) => {
|
||||
setSelectedEnvironment(environment);
|
||||
setIsDeleteDialogOpen(true);
|
||||
};
|
||||
|
||||
const currentEnv = environments?.find(
|
||||
(env) => env.environmentId === currentEnvironmentId,
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" className="h-auto p-2 font-normal">
|
||||
<div className="flex items-center gap-1">
|
||||
<span className="text-muted-foreground">/</span>
|
||||
<span>{currentEnv?.name || "Select Environment"}</span>
|
||||
<ChevronDownIcon className="h-4 w-4 text-muted-foreground" />
|
||||
</div>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className="w-[300px]" align="start">
|
||||
<DropdownMenuLabel>Environments</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
|
||||
{environments?.map((environment) => {
|
||||
const servicesCount =
|
||||
environment.mariadb.length +
|
||||
environment.mongo.length +
|
||||
environment.mysql.length +
|
||||
environment.postgres.length +
|
||||
environment.redis.length +
|
||||
environment.applications.length +
|
||||
environment.compose.length;
|
||||
return (
|
||||
<div
|
||||
key={environment.environmentId}
|
||||
className="flex items-center"
|
||||
>
|
||||
<DropdownMenuItem
|
||||
className="flex-1 cursor-pointer"
|
||||
onClick={() => {
|
||||
router.push(
|
||||
`/dashboard/project/${projectId}/environment/${environment.environmentId}`,
|
||||
);
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center justify-between w-full">
|
||||
<span>
|
||||
{environment.name} ({servicesCount})
|
||||
</span>
|
||||
{environment.environmentId === currentEnvironmentId && (
|
||||
<div className="w-2 h-2 bg-blue-500 rounded-full" />
|
||||
)}
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
|
||||
{/* Action buttons for non-production environments */}
|
||||
<EnvironmentVariables environmentId={environment.environmentId}>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-6 w-6 p-0"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
>
|
||||
<Terminal className="h-3 w-3" />
|
||||
</Button>
|
||||
</EnvironmentVariables>
|
||||
{environment.name !== "production" && (
|
||||
<div className="flex items-center gap-1 px-2">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-6 w-6 p-0"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
openEditDialog(environment);
|
||||
}}
|
||||
>
|
||||
<PencilIcon className="h-3 w-3" />
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-6 w-6 p-0 text-red-600 hover:text-red-700"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
openDeleteDialog(environment);
|
||||
}}
|
||||
>
|
||||
<TrashIcon className="h-3 w-3" />
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem
|
||||
className="cursor-pointer"
|
||||
onClick={() => setIsCreateDialogOpen(true)}
|
||||
>
|
||||
<PlusIcon className="h-4 w-4 mr-2" />
|
||||
Create Environment
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
||||
<Dialog open={isCreateDialogOpen} onOpenChange={setIsCreateDialogOpen}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Create Environment</DialogTitle>
|
||||
<DialogDescription>
|
||||
Create a new environment for your project.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-1">
|
||||
<Label htmlFor="name">Name</Label>
|
||||
<Input
|
||||
id="name"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
placeholder="Environment name"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label htmlFor="description">Description (optional)</Label>
|
||||
<Textarea
|
||||
id="description"
|
||||
value={description}
|
||||
onChange={(e) => setDescription(e.target.value)}
|
||||
placeholder="Environment description"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
setIsCreateDialogOpen(false);
|
||||
setName("");
|
||||
setDescription("");
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleCreateEnvironment}
|
||||
disabled={!name.trim() || createEnvironment.isLoading}
|
||||
>
|
||||
{createEnvironment.isLoading ? "Creating..." : "Create"}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* Edit Environment Dialog */}
|
||||
<Dialog open={isEditDialogOpen} onOpenChange={setIsEditDialogOpen}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Edit Environment</DialogTitle>
|
||||
<DialogDescription>
|
||||
Update the environment details.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-1">
|
||||
<Label htmlFor="edit-name">Name</Label>
|
||||
<Input
|
||||
id="edit-name"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
placeholder="Environment name"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label htmlFor="edit-description">Description (optional)</Label>
|
||||
<Textarea
|
||||
id="edit-description"
|
||||
value={description}
|
||||
onChange={(e) => setDescription(e.target.value)}
|
||||
placeholder="Environment description"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
setIsEditDialogOpen(false);
|
||||
setSelectedEnvironment(null);
|
||||
setName("");
|
||||
setDescription("");
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleUpdateEnvironment}
|
||||
disabled={!name.trim() || updateEnvironment.isLoading}
|
||||
>
|
||||
{updateEnvironment.isLoading ? "Updating..." : "Update"}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* Delete Environment Dialog */}
|
||||
<Dialog open={isDeleteDialogOpen} onOpenChange={setIsDeleteDialogOpen}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Delete Environment</DialogTitle>
|
||||
<DialogDescription>
|
||||
Are you sure you want to delete the environment "
|
||||
{selectedEnvironment?.name}"? This action cannot be undone and
|
||||
will also delete all services in this environment.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
{haveServices && (
|
||||
<AlertBlock type="warning">
|
||||
This environment have active services, please delete them first.
|
||||
</AlertBlock>
|
||||
)}
|
||||
|
||||
<DialogFooter>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
setIsDeleteDialogOpen(false);
|
||||
setSelectedEnvironment(null);
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
variant="destructive"
|
||||
onClick={handleDeleteEnvironment}
|
||||
disabled={
|
||||
deleteEnvironment.isLoading ||
|
||||
haveServices ||
|
||||
!selectedEnvironment
|
||||
}
|
||||
>
|
||||
{deleteEnvironment.isLoading ? "Deleting..." : "Delete"}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -25,12 +25,7 @@ const examples = [
|
||||
export const StepOne = ({ setTemplateInfo, templateInfo }: any) => {
|
||||
// Get servers from the API
|
||||
const { data: servers } = api.server.withSSHKey.useQuery();
|
||||
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||
const hasServers = servers && servers.length > 0;
|
||||
// Show dropdown logic based on cloud environment
|
||||
// Cloud: show only if there are remote servers (no Dokploy option)
|
||||
// Self-hosted: show only if there are remote servers (Dokploy is default, hide if no remote servers)
|
||||
const shouldShowServerDropdown = hasServers;
|
||||
|
||||
const handleExampleClick = (example: string) => {
|
||||
setTemplateInfo({ ...templateInfo, userInput: example });
|
||||
@@ -53,58 +48,34 @@ export const StepOne = ({ setTemplateInfo, templateInfo }: any) => {
|
||||
/>
|
||||
</div>
|
||||
|
||||
{shouldShowServerDropdown && (
|
||||
{hasServers && (
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="server-deploy">
|
||||
Select the server where you want to deploy (optional)
|
||||
</Label>
|
||||
<Select
|
||||
value={
|
||||
templateInfo.server?.serverId ||
|
||||
(!isCloud ? "dokploy" : undefined)
|
||||
}
|
||||
value={templateInfo.server?.serverId}
|
||||
onValueChange={(value) => {
|
||||
if (value === "dokploy") {
|
||||
const server = servers?.find((s) => s.serverId === value);
|
||||
if (server) {
|
||||
setTemplateInfo({
|
||||
...templateInfo,
|
||||
server: undefined,
|
||||
server: server,
|
||||
});
|
||||
} else {
|
||||
const server = servers?.find((s) => s.serverId === value);
|
||||
if (server) {
|
||||
setTemplateInfo({
|
||||
...templateInfo,
|
||||
server: server,
|
||||
});
|
||||
}
|
||||
}
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="w-full">
|
||||
<SelectValue
|
||||
placeholder={!isCloud ? "Dokploy" : "Select a Server"}
|
||||
/>
|
||||
<SelectValue placeholder="Select a server" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
{!isCloud && (
|
||||
<SelectItem value="dokploy">
|
||||
<span className="flex items-center gap-2 justify-between w-full">
|
||||
<span>Dokploy</span>
|
||||
<span className="text-muted-foreground text-xs self-center">
|
||||
Default
|
||||
</span>
|
||||
</span>
|
||||
</SelectItem>
|
||||
)}
|
||||
{servers?.map((server) => (
|
||||
<SelectItem key={server.serverId} value={server.serverId}>
|
||||
{server.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
<SelectLabel>
|
||||
Servers ({servers?.length + (!isCloud ? 1 : 0)})
|
||||
</SelectLabel>
|
||||
<SelectLabel>Servers ({servers?.length})</SelectLabel>
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
@@ -90,11 +90,11 @@ export const { useStepper, steps, Scoped } = defineStepper(
|
||||
);
|
||||
|
||||
interface Props {
|
||||
environmentId: string;
|
||||
projectId: string;
|
||||
projectName?: string;
|
||||
}
|
||||
|
||||
export const TemplateGenerator = ({ environmentId }: Props) => {
|
||||
export const TemplateGenerator = ({ projectId }: Props) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
const stepper = useStepper();
|
||||
const { data: aiSettings } = api.ai.getAll.useQuery();
|
||||
@@ -121,7 +121,7 @@ export const TemplateGenerator = ({ environmentId }: Props) => {
|
||||
|
||||
const onSubmit = async () => {
|
||||
await mutateAsync({
|
||||
environmentId: environmentId,
|
||||
projectId,
|
||||
id: templateInfo.details?.id || "",
|
||||
name: templateInfo?.details?.name || "",
|
||||
description: templateInfo?.details?.shortDescription || "",
|
||||
@@ -138,9 +138,8 @@ export const TemplateGenerator = ({ environmentId }: Props) => {
|
||||
.then(async () => {
|
||||
toast.success("Compose Created");
|
||||
setOpen(false);
|
||||
// Invalidate the project query to refresh the environment data
|
||||
await utils.environment.one.invalidate({
|
||||
environmentId,
|
||||
await utils.project.one.invalidate({
|
||||
projectId,
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
|
||||
@@ -15,13 +15,6 @@ import {
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { api } from "@/utils/api";
|
||||
|
||||
export type Services = {
|
||||
@@ -43,35 +36,23 @@ export type Services = {
|
||||
};
|
||||
|
||||
interface DuplicateProjectProps {
|
||||
environmentId: string;
|
||||
projectId: string;
|
||||
services: Services[];
|
||||
selectedServiceIds: string[];
|
||||
}
|
||||
|
||||
export const DuplicateProject = ({
|
||||
environmentId,
|
||||
projectId,
|
||||
services,
|
||||
selectedServiceIds,
|
||||
}: DuplicateProjectProps) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [name, setName] = useState("");
|
||||
const [description, setDescription] = useState("");
|
||||
const [duplicateType, setDuplicateType] = useState("new-project"); // "new-project" or "existing-environment"
|
||||
const [selectedTargetProject, setSelectedTargetProject] =
|
||||
useState<string>("");
|
||||
const [selectedTargetEnvironment, setSelectedTargetEnvironment] =
|
||||
useState<string>("");
|
||||
const [duplicateType, setDuplicateType] = useState("new-project"); // "new-project" or "same-project"
|
||||
const utils = api.useUtils();
|
||||
const router = useRouter();
|
||||
|
||||
// Queries for project and environment selection
|
||||
const { data: allProjects } = api.project.all.useQuery();
|
||||
const { data: selectedProjectEnvironments } =
|
||||
api.environment.byProjectId.useQuery(
|
||||
{ projectId: selectedTargetProject },
|
||||
{ enabled: !!selectedTargetProject },
|
||||
);
|
||||
|
||||
const selectedServices = services.filter((service) =>
|
||||
selectedServiceIds.includes(service.id),
|
||||
);
|
||||
@@ -80,29 +61,6 @@ export const DuplicateProject = ({
|
||||
api.project.duplicate.useMutation({
|
||||
onSuccess: async (newProject) => {
|
||||
await utils.project.all.invalidate();
|
||||
|
||||
// If duplicating to same project+environment, invalidate the environment query
|
||||
// to refresh the services list
|
||||
if (duplicateType === "existing-environment") {
|
||||
await utils.environment.one.invalidate({
|
||||
environmentId: selectedTargetEnvironment,
|
||||
});
|
||||
await utils.environment.byProjectId.invalidate({
|
||||
projectId: selectedTargetProject,
|
||||
});
|
||||
|
||||
// If duplicating to the same environment we're currently viewing,
|
||||
// also invalidate the current environment to refresh the services list
|
||||
if (selectedTargetEnvironment === environmentId) {
|
||||
await utils.environment.one.invalidate({ environmentId });
|
||||
// Also invalidate the project query to refresh the project data
|
||||
const projectId = router.query.projectId as string;
|
||||
if (projectId) {
|
||||
await utils.project.one.invalidate({ projectId });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
toast.success(
|
||||
duplicateType === "new-project"
|
||||
? "Project duplicated successfully"
|
||||
@@ -110,9 +68,7 @@ export const DuplicateProject = ({
|
||||
);
|
||||
setOpen(false);
|
||||
if (duplicateType === "new-project") {
|
||||
router.push(
|
||||
`/dashboard/project/${newProject?.projectId}/environment/${newProject?.environmentId}`,
|
||||
);
|
||||
router.push(`/dashboard/project/${newProject.projectId}`);
|
||||
}
|
||||
},
|
||||
onError: (error) => {
|
||||
@@ -126,20 +82,8 @@ export const DuplicateProject = ({
|
||||
return;
|
||||
}
|
||||
|
||||
if (duplicateType === "existing-environment") {
|
||||
if (!selectedTargetProject) {
|
||||
toast.error("Please select a target project");
|
||||
return;
|
||||
}
|
||||
if (!selectedTargetEnvironment) {
|
||||
toast.error("Please select a target environment");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Update duplicate API to support targetProjectId and targetEnvironmentId
|
||||
await duplicateProject({
|
||||
sourceEnvironmentId: selectedTargetEnvironment,
|
||||
sourceProjectId: projectId,
|
||||
name,
|
||||
description,
|
||||
includeServices: true,
|
||||
@@ -147,7 +91,7 @@ export const DuplicateProject = ({
|
||||
id: service.id,
|
||||
type: service.type,
|
||||
})),
|
||||
duplicateInSameProject: duplicateType === "existing-environment",
|
||||
duplicateInSameProject: duplicateType === "same-project",
|
||||
});
|
||||
};
|
||||
|
||||
@@ -161,8 +105,6 @@ export const DuplicateProject = ({
|
||||
setName("");
|
||||
setDescription("");
|
||||
setDuplicateType("new-project");
|
||||
setSelectedTargetProject("");
|
||||
setSelectedTargetEnvironment("");
|
||||
}
|
||||
}}
|
||||
>
|
||||
@@ -185,14 +127,7 @@ export const DuplicateProject = ({
|
||||
<Label>Duplicate to</Label>
|
||||
<RadioGroup
|
||||
value={duplicateType}
|
||||
onValueChange={(value) => {
|
||||
setDuplicateType(value);
|
||||
// Reset selections when changing type
|
||||
if (value !== "existing-environment") {
|
||||
setSelectedTargetProject("");
|
||||
setSelectedTargetEnvironment("");
|
||||
}
|
||||
}}
|
||||
onValueChange={setDuplicateType}
|
||||
className="grid gap-2"
|
||||
>
|
||||
<div className="flex items-center space-x-2">
|
||||
@@ -200,13 +135,8 @@ export const DuplicateProject = ({
|
||||
<Label htmlFor="new-project">New project</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem
|
||||
value="existing-environment"
|
||||
id="existing-environment"
|
||||
/>
|
||||
<Label htmlFor="existing-environment">
|
||||
Existing environment
|
||||
</Label>
|
||||
<RadioGroupItem value="same-project" id="same-project" />
|
||||
<Label htmlFor="same-project">Same project</Label>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</div>
|
||||
@@ -235,74 +165,6 @@ export const DuplicateProject = ({
|
||||
</>
|
||||
)}
|
||||
|
||||
{duplicateType === "existing-environment" && (
|
||||
<>
|
||||
{allProjects?.filter((p) => p.projectId !== environmentId)
|
||||
.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center gap-2 py-4 text-center">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
No other projects available. Create a new project first.
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{/* Step 1: Select Project */}
|
||||
<div className="grid gap-2">
|
||||
<Label>Target Project</Label>
|
||||
<Select
|
||||
value={selectedTargetProject}
|
||||
onValueChange={(value) => {
|
||||
setSelectedTargetProject(value);
|
||||
setSelectedTargetEnvironment(""); // Reset environment when project changes
|
||||
}}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select target project" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{allProjects
|
||||
?.filter((p) => p.projectId !== environmentId)
|
||||
.map((project) => (
|
||||
<SelectItem
|
||||
key={project.projectId}
|
||||
value={project.projectId}
|
||||
>
|
||||
{project.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{/* Step 2: Select Environment (only show if project is selected) */}
|
||||
{selectedTargetProject && (
|
||||
<div className="grid gap-2">
|
||||
<Label>Target Environment</Label>
|
||||
<Select
|
||||
value={selectedTargetEnvironment}
|
||||
onValueChange={setSelectedTargetEnvironment}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select target environment" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{selectedProjectEnvironments?.map((env) => (
|
||||
<SelectItem
|
||||
key={env.environmentId}
|
||||
value={env.environmentId}
|
||||
>
|
||||
{env.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
<div className="grid gap-2">
|
||||
<Label>Selected services to duplicate</Label>
|
||||
<div className="space-y-2 max-h-[200px] overflow-y-auto border rounded-md p-4">
|
||||
@@ -325,26 +187,18 @@ export const DuplicateProject = ({
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleDuplicate}
|
||||
disabled={
|
||||
isLoading ||
|
||||
(duplicateType === "new-project" && !name) ||
|
||||
(duplicateType === "existing-environment" &&
|
||||
(!selectedTargetProject || !selectedTargetEnvironment))
|
||||
}
|
||||
>
|
||||
<Button onClick={handleDuplicate} disabled={isLoading}>
|
||||
{isLoading ? (
|
||||
<>
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
{duplicateType === "new-project"
|
||||
? "Duplicating to new project..."
|
||||
: "Duplicating to environment..."}
|
||||
? "Duplicating project..."
|
||||
: "Duplicating services..."}
|
||||
</>
|
||||
) : duplicateType === "new-project" ? (
|
||||
"Duplicate to new project"
|
||||
"Duplicate project"
|
||||
) : (
|
||||
"Duplicate to environment"
|
||||
"Duplicate services"
|
||||
)}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
|
||||
@@ -1,157 +0,0 @@
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { Terminal } from "lucide-react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
import { AlertBlock } from "@/components/shared/alert-block";
|
||||
import { CodeEditor } from "@/components/shared/code-editor";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import { DropdownMenuItem } from "@/components/ui/dropdown-menu";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
import { api } from "@/utils/api";
|
||||
|
||||
const updateEnvironmentSchema = z.object({
|
||||
env: z.string().optional(),
|
||||
});
|
||||
|
||||
type UpdateEnvironment = z.infer<typeof updateEnvironmentSchema>;
|
||||
|
||||
interface Props {
|
||||
environmentId: string;
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
export const EnvironmentVariables = ({ environmentId, children }: Props) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const utils = api.useUtils();
|
||||
const { mutateAsync, error, isError, isLoading } =
|
||||
api.environment.update.useMutation();
|
||||
const { data } = api.environment.one.useQuery(
|
||||
{
|
||||
environmentId,
|
||||
},
|
||||
{
|
||||
enabled: !!environmentId,
|
||||
},
|
||||
);
|
||||
|
||||
const form = useForm<UpdateEnvironment>({
|
||||
defaultValues: {
|
||||
env: data?.env ?? "",
|
||||
},
|
||||
resolver: zodResolver(updateEnvironmentSchema),
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
form.reset({
|
||||
env: data.env ?? "",
|
||||
});
|
||||
}
|
||||
}, [data, form, form.reset]);
|
||||
|
||||
const onSubmit = async (formData: UpdateEnvironment) => {
|
||||
await mutateAsync({
|
||||
env: formData.env || "",
|
||||
environmentId: environmentId,
|
||||
})
|
||||
.then(() => {
|
||||
toast.success("Environment variables updated successfully");
|
||||
utils.environment.one.invalidate({ environmentId });
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error updating the environment variables");
|
||||
})
|
||||
.finally(() => {});
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||
<DialogTrigger asChild>
|
||||
{children ?? (
|
||||
<DropdownMenuItem
|
||||
className="w-full cursor-pointer space-x-3"
|
||||
onSelect={(e) => e.preventDefault()}
|
||||
>
|
||||
<Terminal className="size-4" />
|
||||
<span>Environment Variables</span>
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</DialogTrigger>
|
||||
<DialogContent className="sm:max-w-6xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Environment Variables</DialogTitle>
|
||||
<DialogDescription>
|
||||
Update the environment variables that are accessible to all services
|
||||
in this environment.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
||||
<AlertBlock type="info">
|
||||
Use this syntax to reference environment-level variables in your
|
||||
service environments:{" "}
|
||||
<code>API_URL=${"{{environment.API_URL}}"}</code>
|
||||
</AlertBlock>
|
||||
<div className="grid gap-4">
|
||||
<div className="grid items-center gap-4">
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="grid w-full gap-4 "
|
||||
>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="env"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Environment variables</FormLabel>
|
||||
<FormControl>
|
||||
<CodeEditor
|
||||
lineWrapping
|
||||
language="properties"
|
||||
wrapperClassName="h-[35rem] font-mono"
|
||||
placeholder={`NODE_ENV=development
|
||||
DATABASE_URL=postgresql://localhost:5432/mydb
|
||||
API_KEY=your-api-key-here
|
||||
|
||||
`}
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<pre>
|
||||
<FormMessage />
|
||||
</pre>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<DialogFooter>
|
||||
<Button isLoading={isLoading} type="submit">
|
||||
Update
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
</Form>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
@@ -101,18 +101,7 @@ export const HandleProject = ({ projectId }: Props) => {
|
||||
toast.success(projectId ? "Project Updated" : "Project Created");
|
||||
setIsOpen(false);
|
||||
if (!projectId) {
|
||||
const projectIdToUse =
|
||||
data && "project" in data ? data.project.projectId : undefined;
|
||||
const environmentIdToUse =
|
||||
data && "environment" in data
|
||||
? data.environment.environmentId
|
||||
: undefined;
|
||||
|
||||
if (environmentIdToUse && projectIdToUse) {
|
||||
router.push(
|
||||
`/dashboard/project/${projectIdToUse}/environment/${environmentIdToUse}`,
|
||||
);
|
||||
}
|
||||
router.push(`/dashboard/project/${data?.projectId}`);
|
||||
} else {
|
||||
refetch();
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@ import {
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { FocusShortcutInput } from "@/components/shared/focus-shortcut-input";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
@@ -96,8 +96,22 @@ export const ShowProjects = () => {
|
||||
new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime();
|
||||
break;
|
||||
case "services": {
|
||||
const aTotalServices = a.environments.length;
|
||||
const bTotalServices = b.environments.length;
|
||||
const aTotalServices =
|
||||
a.mariadb.length +
|
||||
a.mongo.length +
|
||||
a.mysql.length +
|
||||
a.postgres.length +
|
||||
a.redis.length +
|
||||
a.applications.length +
|
||||
a.compose.length;
|
||||
const bTotalServices =
|
||||
b.mariadb.length +
|
||||
b.mongo.length +
|
||||
b.mysql.length +
|
||||
b.postgres.length +
|
||||
b.redis.length +
|
||||
b.applications.length +
|
||||
b.compose.length;
|
||||
comparison = aTotalServices - bTotalServices;
|
||||
break;
|
||||
}
|
||||
@@ -144,13 +158,12 @@ export const ShowProjects = () => {
|
||||
<>
|
||||
<div className="flex max-sm:flex-col gap-4 items-center w-full">
|
||||
<div className="flex-1 relative max-sm:w-full">
|
||||
<FocusShortcutInput
|
||||
<Input
|
||||
placeholder="Filter projects..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="pr-10"
|
||||
/>
|
||||
|
||||
<Search className="absolute right-3 top-1/2 -translate-y-1/2 size-4 text-muted-foreground" />
|
||||
</div>
|
||||
<div className="flex items-center gap-2 min-w-48 max-sm:w-full">
|
||||
@@ -188,40 +201,23 @@ export const ShowProjects = () => {
|
||||
)}
|
||||
<div className="w-full grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4 3xl:grid-cols-5 flex-wrap gap-5">
|
||||
{filteredProjects?.map((project) => {
|
||||
const emptyServices = project?.environments
|
||||
.map(
|
||||
(env) =>
|
||||
env.applications.length === 0 &&
|
||||
env.mariadb.length === 0 &&
|
||||
env.mongo.length === 0 &&
|
||||
env.mysql.length === 0 &&
|
||||
env.postgres.length === 0 &&
|
||||
env.redis.length === 0 &&
|
||||
env.applications.length === 0 &&
|
||||
env.compose.length === 0,
|
||||
)
|
||||
.every(Boolean);
|
||||
const emptyServices =
|
||||
project?.mariadb.length === 0 &&
|
||||
project?.mongo.length === 0 &&
|
||||
project?.mysql.length === 0 &&
|
||||
project?.postgres.length === 0 &&
|
||||
project?.redis.length === 0 &&
|
||||
project?.applications.length === 0 &&
|
||||
project?.compose.length === 0;
|
||||
|
||||
const totalServices = project?.environments
|
||||
.map(
|
||||
(env) =>
|
||||
env.mariadb.length +
|
||||
env.mongo.length +
|
||||
env.mysql.length +
|
||||
env.postgres.length +
|
||||
env.redis.length +
|
||||
env.applications.length +
|
||||
env.compose.length,
|
||||
)
|
||||
.reduce((acc, curr) => acc + curr, 0);
|
||||
|
||||
const haveServicesWithDomains = project?.environments
|
||||
.map(
|
||||
(env) =>
|
||||
env.applications.length > 0 ||
|
||||
env.compose.length > 0,
|
||||
)
|
||||
.some(Boolean);
|
||||
const totalServices =
|
||||
project?.mariadb.length +
|
||||
project?.mongo.length +
|
||||
project?.mysql.length +
|
||||
project?.postgres.length +
|
||||
project?.redis.length +
|
||||
project?.applications.length +
|
||||
project?.compose.length;
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -229,10 +225,11 @@ export const ShowProjects = () => {
|
||||
className="w-full lg:max-w-md"
|
||||
>
|
||||
<Link
|
||||
href={`/dashboard/project/${project.projectId}/environment/${project?.environments?.[0]?.environmentId}`}
|
||||
href={`/dashboard/project/${project.projectId}`}
|
||||
>
|
||||
<Card className="group relative w-full h-full bg-transparent transition-colors hover:bg-border">
|
||||
{haveServicesWithDomains ? (
|
||||
{project.applications.length > 0 ||
|
||||
project.compose.length > 0 ? (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
@@ -247,51 +244,44 @@ export const ShowProjects = () => {
|
||||
className="w-[200px] space-y-2 overflow-y-auto max-h-[400px]"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{project.environments.some(
|
||||
(env) => env.applications.length > 0,
|
||||
) && (
|
||||
{project.applications.length > 0 && (
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuLabel>
|
||||
Applications
|
||||
</DropdownMenuLabel>
|
||||
{project.environments.map((env) =>
|
||||
env.applications.map((app) => (
|
||||
<div key={app.applicationId}>
|
||||
{project.applications.map((app) => (
|
||||
<div key={app.applicationId}>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuLabel className="font-normal capitalize text-xs flex items-center justify-between">
|
||||
{app.name}
|
||||
<StatusTooltip
|
||||
status={app.applicationStatus}
|
||||
/>
|
||||
</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuLabel className="font-normal capitalize text-xs flex items-center justify-between">
|
||||
{app.name}
|
||||
<StatusTooltip
|
||||
status={
|
||||
app.applicationStatus
|
||||
}
|
||||
/>
|
||||
</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
{app.domains.map((domain) => (
|
||||
<DropdownMenuItem
|
||||
key={domain.domainId}
|
||||
asChild
|
||||
{app.domains.map((domain) => (
|
||||
<DropdownMenuItem
|
||||
key={domain.domainId}
|
||||
asChild
|
||||
>
|
||||
<Link
|
||||
className="space-x-4 text-xs cursor-pointer justify-between"
|
||||
target="_blank"
|
||||
href={`${domain.https ? "https" : "http"}://${domain.host}${domain.path}`}
|
||||
>
|
||||
<Link
|
||||
className="space-x-4 text-xs cursor-pointer justify-between"
|
||||
target="_blank"
|
||||
href={`${domain.https ? "https" : "http"}://${domain.host}${domain.path}`}
|
||||
>
|
||||
<span className="truncate">
|
||||
{domain.host}
|
||||
</span>
|
||||
<ExternalLinkIcon className="size-4 shrink-0" />
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuGroup>
|
||||
</div>
|
||||
)),
|
||||
)}
|
||||
<span className="truncate">
|
||||
{domain.host}
|
||||
</span>
|
||||
<ExternalLinkIcon className="size-4 shrink-0" />
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuGroup>
|
||||
</div>
|
||||
))}
|
||||
</DropdownMenuGroup>
|
||||
)}
|
||||
{/*
|
||||
{project.compose.length > 0 && (
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuLabel>
|
||||
@@ -329,7 +319,7 @@ export const ShowProjects = () => {
|
||||
</div>
|
||||
))}
|
||||
</DropdownMenuGroup>
|
||||
)} */}
|
||||
)}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
) : null}
|
||||
|
||||
@@ -96,9 +96,9 @@ export const ShowExternalRedisCredentials = ({ redisId }: Props) => {
|
||||
<CardHeader>
|
||||
<CardTitle className="text-xl">External Credentials</CardTitle>
|
||||
<CardDescription>
|
||||
In order to make the database reachable through the internet, you
|
||||
must set a port and ensure that the port is not being used by
|
||||
another application or database
|
||||
In order to make the database reachable trought internet is
|
||||
required to set a port, make sure the port is not used by another
|
||||
application or database
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="flex w-full flex-col gap-4">
|
||||
|
||||
@@ -3,10 +3,6 @@
|
||||
import { BookIcon, CircuitBoard, GlobeIcon } from "lucide-react";
|
||||
import { useRouter } from "next/router";
|
||||
import React from "react";
|
||||
import {
|
||||
extractServices,
|
||||
type Services,
|
||||
} from "@/components/dashboard/settings/users/add-permissions";
|
||||
import {
|
||||
MariadbIcon,
|
||||
MongodbIcon,
|
||||
@@ -24,34 +20,13 @@ import {
|
||||
CommandSeparator,
|
||||
} from "@/components/ui/command";
|
||||
import { authClient } from "@/lib/auth-client";
|
||||
import {
|
||||
extractServices,
|
||||
type Services,
|
||||
} from "@/pages/dashboard/project/[projectId]";
|
||||
import { api } from "@/utils/api";
|
||||
import { StatusTooltip } from "../shared/status-tooltip";
|
||||
|
||||
// Extended Services type to include environmentId and environmentName for search navigation
|
||||
type SearchServices = Services & {
|
||||
environmentId: string;
|
||||
environmentName: string;
|
||||
};
|
||||
|
||||
const extractAllServicesFromProject = (project: any): SearchServices[] => {
|
||||
const allServices: SearchServices[] = [];
|
||||
|
||||
// Iterate through all environments in the project
|
||||
project.environments?.forEach((environment: any) => {
|
||||
const environmentServices = extractServices(environment);
|
||||
const servicesWithEnvironmentId: SearchServices[] = environmentServices.map(
|
||||
(service) => ({
|
||||
...service,
|
||||
environmentId: environment.environmentId,
|
||||
environmentName: environment.name,
|
||||
}),
|
||||
);
|
||||
allServices.push(...servicesWithEnvironmentId);
|
||||
});
|
||||
|
||||
return allServices;
|
||||
};
|
||||
|
||||
export const SearchCommand = () => {
|
||||
const router = useRouter();
|
||||
const [open, setOpen] = React.useState(false);
|
||||
@@ -88,42 +63,31 @@ export const SearchCommand = () => {
|
||||
</CommandEmpty>
|
||||
<CommandGroup heading={"Projects"}>
|
||||
<CommandList>
|
||||
{data?.map((project) => {
|
||||
const productionEnvironment = project.environments.find(
|
||||
(environment) => environment.name === "production",
|
||||
);
|
||||
|
||||
if (!productionEnvironment) return null;
|
||||
|
||||
return (
|
||||
<CommandItem
|
||||
key={project.projectId}
|
||||
onSelect={() => {
|
||||
router.push(
|
||||
`/dashboard/project/${project.projectId}/environment/${productionEnvironment!.environmentId}`,
|
||||
);
|
||||
setOpen(false);
|
||||
}}
|
||||
>
|
||||
<BookIcon className="size-4 text-muted-foreground mr-2" />
|
||||
{project.name} / {productionEnvironment!.name}
|
||||
</CommandItem>
|
||||
);
|
||||
})}
|
||||
{data?.map((project) => (
|
||||
<CommandItem
|
||||
key={project.projectId}
|
||||
onSelect={() => {
|
||||
router.push(`/dashboard/project/${project.projectId}`);
|
||||
setOpen(false);
|
||||
}}
|
||||
>
|
||||
<BookIcon className="size-4 text-muted-foreground mr-2" />
|
||||
{project.name}
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandList>
|
||||
</CommandGroup>
|
||||
<CommandSeparator />
|
||||
<CommandGroup heading={"Services"}>
|
||||
<CommandList>
|
||||
{data?.map((project) => {
|
||||
const applications: SearchServices[] =
|
||||
extractAllServicesFromProject(project);
|
||||
const applications: Services[] = extractServices(project);
|
||||
return applications.map((application) => (
|
||||
<CommandItem
|
||||
key={application.id}
|
||||
onSelect={() => {
|
||||
router.push(
|
||||
`/dashboard/project/${project.projectId}/environment/${application.environmentId}/services/${application.type}/${application.id}`,
|
||||
`/dashboard/project/${project.projectId}/services/${application.type}/${application.id}`,
|
||||
);
|
||||
setOpen(false);
|
||||
}}
|
||||
@@ -150,8 +114,7 @@ export const SearchCommand = () => {
|
||||
<CircuitBoard className="h-6 w-6 mr-2" />
|
||||
)}
|
||||
<span className="flex-grow">
|
||||
{project.name} / {application.environmentName} /{" "}
|
||||
{application.name}{" "}
|
||||
{project.name} / {application.name}{" "}
|
||||
<div style={{ display: "none" }}>{application.id}</div>
|
||||
</span>
|
||||
<div>
|
||||
|
||||
@@ -65,11 +65,6 @@ export const AddCertificate = () => {
|
||||
const { mutateAsync, isError, error, isLoading } =
|
||||
api.certificates.create.useMutation();
|
||||
const { data: servers } = api.server.withSSHKey.useQuery();
|
||||
const hasServers = servers && servers.length > 0;
|
||||
// Show dropdown logic based on cloud environment
|
||||
// Cloud: show only if there are remote servers (no Dokploy option)
|
||||
// Self-hosted: show only if there are remote servers (Dokploy is default, hide if no remote servers)
|
||||
const shouldShowServerDropdown = hasServers;
|
||||
|
||||
const form = useForm<AddCertificate>({
|
||||
defaultValues: {
|
||||
@@ -90,7 +85,7 @@ export const AddCertificate = () => {
|
||||
certificateData: data.certificateData,
|
||||
privateKey: data.privateKey,
|
||||
autoRenew: data.autoRenew,
|
||||
serverId: data.serverId === "dokploy" ? undefined : data.serverId,
|
||||
serverId: data.serverId,
|
||||
organizationId: "",
|
||||
})
|
||||
.then(async () => {
|
||||
@@ -179,70 +174,52 @@ export const AddCertificate = () => {
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
{shouldShowServerDropdown && (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="serverId"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<TooltipProvider delayDuration={0}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<FormLabel className="break-all w-fit flex flex-row gap-1 items-center">
|
||||
Select a Server {!isCloud && "(Optional)"}
|
||||
<HelpCircle className="size-4 text-muted-foreground" />
|
||||
</FormLabel>
|
||||
</TooltipTrigger>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="serverId"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<TooltipProvider delayDuration={0}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<FormLabel className="break-all w-fit flex flex-row gap-1 items-center">
|
||||
Select a Server {!isCloud && "(Optional)"}
|
||||
<HelpCircle className="size-4 text-muted-foreground" />
|
||||
</FormLabel>
|
||||
</TooltipTrigger>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
|
||||
<Select
|
||||
onValueChange={field.onChange}
|
||||
defaultValue={
|
||||
field.value || (!isCloud ? "dokploy" : undefined)
|
||||
}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue
|
||||
placeholder={!isCloud ? "Dokploy" : "Select a Server"}
|
||||
/>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
{!isCloud && (
|
||||
<SelectItem value="dokploy">
|
||||
<span className="flex items-center gap-2 justify-between w-full">
|
||||
<span>Dokploy</span>
|
||||
<span className="text-muted-foreground text-xs self-center">
|
||||
Default
|
||||
</span>
|
||||
<Select
|
||||
onValueChange={field.onChange}
|
||||
defaultValue={field.value}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select a Server" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
{servers?.map((server) => (
|
||||
<SelectItem
|
||||
key={server.serverId}
|
||||
value={server.serverId}
|
||||
>
|
||||
<span className="flex items-center gap-2 justify-between w-full">
|
||||
<span>{server.name}</span>
|
||||
<span className="text-muted-foreground text-xs self-center">
|
||||
{server.ipAddress}
|
||||
</span>
|
||||
</SelectItem>
|
||||
)}
|
||||
{servers?.map((server) => (
|
||||
<SelectItem
|
||||
key={server.serverId}
|
||||
value={server.serverId}
|
||||
>
|
||||
<span className="flex items-center gap-2 justify-between w-full">
|
||||
<span>{server.name}</span>
|
||||
<span className="text-muted-foreground text-xs self-center">
|
||||
{server.ipAddress}
|
||||
</span>
|
||||
</span>
|
||||
</SelectItem>
|
||||
))}
|
||||
<SelectLabel>
|
||||
Servers ({servers?.length + (!isCloud ? 1 : 0)})
|
||||
</SelectLabel>
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</span>
|
||||
</SelectItem>
|
||||
))}
|
||||
<SelectLabel>Servers ({servers?.length})</SelectLabel>
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</form>
|
||||
|
||||
<DialogFooter className="flex w-full flex-row !justify-end">
|
||||
|
||||
@@ -101,15 +101,6 @@ export const notificationSchema = z.discriminatedUnion("type", [
|
||||
decoration: z.boolean().default(true),
|
||||
})
|
||||
.merge(notificationBaseSchema),
|
||||
z
|
||||
.object({
|
||||
type: z.literal("ntfy"),
|
||||
serverUrl: z.string().min(1, { message: "Server URL is required" }),
|
||||
topic: z.string().min(1, { message: "Topic is required" }),
|
||||
accessToken: z.string().min(1, { message: "Access Token is required" }),
|
||||
priority: z.number().min(1).max(5).default(3),
|
||||
})
|
||||
.merge(notificationBaseSchema),
|
||||
]);
|
||||
|
||||
export const notificationsMap = {
|
||||
@@ -133,10 +124,6 @@ export const notificationsMap = {
|
||||
icon: <MessageCircleMore size={29} className="text-muted-foreground" />,
|
||||
label: "Gotify",
|
||||
},
|
||||
ntfy: {
|
||||
icon: <MessageCircleMore size={29} className="text-muted-foreground" />,
|
||||
label: "ntfy",
|
||||
},
|
||||
};
|
||||
|
||||
export type NotificationSchema = z.infer<typeof notificationSchema>;
|
||||
@@ -168,8 +155,6 @@ export const HandleNotifications = ({ notificationId }: Props) => {
|
||||
api.notification.testEmailConnection.useMutation();
|
||||
const { mutateAsync: testGotifyConnection, isLoading: isLoadingGotify } =
|
||||
api.notification.testGotifyConnection.useMutation();
|
||||
const { mutateAsync: testNtfyConnection, isLoading: isLoadingNtfy } =
|
||||
api.notification.testNtfyConnection.useMutation();
|
||||
const slackMutation = notificationId
|
||||
? api.notification.updateSlack.useMutation()
|
||||
: api.notification.createSlack.useMutation();
|
||||
@@ -185,9 +170,6 @@ export const HandleNotifications = ({ notificationId }: Props) => {
|
||||
const gotifyMutation = notificationId
|
||||
? api.notification.updateGotify.useMutation()
|
||||
: api.notification.createGotify.useMutation();
|
||||
const ntfyMutation = notificationId
|
||||
? api.notification.updateNtfy.useMutation()
|
||||
: api.notification.createNtfy.useMutation();
|
||||
|
||||
const form = useForm<NotificationSchema>({
|
||||
defaultValues: {
|
||||
@@ -284,20 +266,6 @@ export const HandleNotifications = ({ notificationId }: Props) => {
|
||||
name: notification.name,
|
||||
dockerCleanup: notification.dockerCleanup,
|
||||
});
|
||||
} else if (notification.notificationType === "ntfy") {
|
||||
form.reset({
|
||||
appBuildError: notification.appBuildError,
|
||||
appDeploy: notification.appDeploy,
|
||||
dokployRestart: notification.dokployRestart,
|
||||
databaseBackup: notification.databaseBackup,
|
||||
type: notification.notificationType,
|
||||
accessToken: notification.ntfy?.accessToken,
|
||||
topic: notification.ntfy?.topic,
|
||||
priority: notification.ntfy?.priority,
|
||||
serverUrl: notification.ntfy?.serverUrl,
|
||||
name: notification.name,
|
||||
dockerCleanup: notification.dockerCleanup,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
form.reset();
|
||||
@@ -310,7 +278,6 @@ export const HandleNotifications = ({ notificationId }: Props) => {
|
||||
discord: discordMutation,
|
||||
email: emailMutation,
|
||||
gotify: gotifyMutation,
|
||||
ntfy: ntfyMutation,
|
||||
};
|
||||
|
||||
const onSubmit = async (data: NotificationSchema) => {
|
||||
@@ -399,21 +366,6 @@ export const HandleNotifications = ({ notificationId }: Props) => {
|
||||
notificationId: notificationId || "",
|
||||
gotifyId: notification?.gotifyId || "",
|
||||
});
|
||||
} else if (data.type === "ntfy") {
|
||||
promise = ntfyMutation.mutateAsync({
|
||||
appBuildError: appBuildError,
|
||||
appDeploy: appDeploy,
|
||||
dokployRestart: dokployRestart,
|
||||
databaseBackup: databaseBackup,
|
||||
serverUrl: data.serverUrl,
|
||||
accessToken: data.accessToken,
|
||||
topic: data.topic,
|
||||
priority: data.priority,
|
||||
name: data.name,
|
||||
dockerCleanup: dockerCleanup,
|
||||
notificationId: notificationId || "",
|
||||
ntfyId: notification?.ntfyId || "",
|
||||
});
|
||||
}
|
||||
|
||||
if (promise) {
|
||||
@@ -923,83 +875,6 @@ export const HandleNotifications = ({ notificationId }: Props) => {
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{type === "ntfy" && (
|
||||
<>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="serverUrl"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Server URL</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="https://ntfy.sh" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="topic"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Topic</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="deployments" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="accessToken"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Access Token</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="AzxcvbnmKjhgfdsa..."
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="priority"
|
||||
defaultValue={3}
|
||||
render={({ field }) => (
|
||||
<FormItem className="w-full">
|
||||
<FormLabel>Priority</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="3"
|
||||
{...field}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value;
|
||||
if (value) {
|
||||
const port = Number.parseInt(value);
|
||||
if (port > 0 && port <= 5) {
|
||||
field.onChange(port);
|
||||
}
|
||||
}
|
||||
}}
|
||||
type="number"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
Message priority (1-5, default: 3)
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-4">
|
||||
@@ -1149,8 +1024,7 @@ export const HandleNotifications = ({ notificationId }: Props) => {
|
||||
isLoadingTelegram ||
|
||||
isLoadingDiscord ||
|
||||
isLoadingEmail ||
|
||||
isLoadingGotify ||
|
||||
isLoadingNtfy
|
||||
isLoadingGotify
|
||||
}
|
||||
variant="secondary"
|
||||
onClick={async () => {
|
||||
@@ -1187,13 +1061,6 @@ export const HandleNotifications = ({ notificationId }: Props) => {
|
||||
priority: form.getValues("priority"),
|
||||
decoration: form.getValues("decoration"),
|
||||
});
|
||||
} else if (type === "ntfy") {
|
||||
await testNtfyConnection({
|
||||
serverUrl: form.getValues("serverUrl"),
|
||||
topic: form.getValues("topic"),
|
||||
accessToken: form.getValues("accessToken"),
|
||||
priority: form.getValues("priority"),
|
||||
});
|
||||
}
|
||||
toast.success("Connection Success");
|
||||
} catch {
|
||||
|
||||
@@ -88,11 +88,6 @@ export const ShowNotifications = () => {
|
||||
<MessageCircleMore className="size-6 text-muted-foreground" />
|
||||
</div>
|
||||
)}
|
||||
{notification.notificationType === "ntfy" && (
|
||||
<div className="flex items-center justify-center rounded-lg ">
|
||||
<MessageCircleMore className="size-6 text-muted-foreground" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{notification.name}
|
||||
</span>
|
||||
|
||||
@@ -97,7 +97,11 @@ export const ShowTraefikActions = ({ serverId }: Props) => {
|
||||
);
|
||||
refetchDashboard();
|
||||
})
|
||||
.catch(() => {});
|
||||
.catch(() => {
|
||||
toast.error(
|
||||
`${haveTraefikDashboardPortEnabled ? "Disabled" : "Enabled"} Dashboard`,
|
||||
);
|
||||
});
|
||||
}}
|
||||
className="w-full cursor-pointer space-x-3"
|
||||
>
|
||||
|
||||
@@ -35,9 +35,9 @@ import {
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/components/ui/popover";
|
||||
import { extractServices } from "@/pages/dashboard/project/[projectId]";
|
||||
import { api } from "@/utils/api";
|
||||
import { useUrl } from "@/utils/hooks/use-url";
|
||||
import { extractServices } from "../users/add-permissions";
|
||||
|
||||
interface Props {
|
||||
serverId?: string;
|
||||
@@ -95,13 +95,11 @@ export const SetupMonitoring = ({ serverId }: Props) => {
|
||||
|
||||
const { data: projects } = api.project.all.useQuery();
|
||||
|
||||
const extractServicesFromProjects = () => {
|
||||
const extractServicesFromProjects = (projects: any[] | undefined) => {
|
||||
if (!projects) return [];
|
||||
|
||||
const allServices = projects.flatMap((project) => {
|
||||
const services = project.environments.flatMap((env) =>
|
||||
extractServices(env),
|
||||
);
|
||||
const services = extractServices(project);
|
||||
return serverId
|
||||
? services
|
||||
.filter((service) => service.serverId === serverId)
|
||||
@@ -112,7 +110,7 @@ export const SetupMonitoring = ({ serverId }: Props) => {
|
||||
return [...new Set(allServices)];
|
||||
};
|
||||
|
||||
const services = extractServicesFromProjects();
|
||||
const services = extractServicesFromProjects(projects);
|
||||
|
||||
const form = useForm<Schema>({
|
||||
resolver: zodResolver(Schema),
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import type { findEnvironmentById } from "@dokploy/server/index";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useEffect } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
@@ -27,135 +26,11 @@ import {
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { extractServices } from "@/pages/dashboard/project/[projectId]";
|
||||
import { api } from "@/utils/api";
|
||||
|
||||
type Environment = Omit<
|
||||
Awaited<ReturnType<typeof findEnvironmentById>>,
|
||||
"project"
|
||||
>;
|
||||
|
||||
export type Services = {
|
||||
appName: string;
|
||||
serverId?: string | null;
|
||||
name: string;
|
||||
type:
|
||||
| "mariadb"
|
||||
| "application"
|
||||
| "postgres"
|
||||
| "mysql"
|
||||
| "mongo"
|
||||
| "redis"
|
||||
| "compose";
|
||||
description?: string | null;
|
||||
id: string;
|
||||
createdAt: string;
|
||||
status?: "idle" | "running" | "done" | "error";
|
||||
};
|
||||
|
||||
export const extractServices = (data: Environment | undefined) => {
|
||||
const applications: Services[] =
|
||||
data?.applications.map((item) => ({
|
||||
appName: item.appName,
|
||||
name: item.name,
|
||||
type: "application",
|
||||
id: item.applicationId,
|
||||
createdAt: item.createdAt,
|
||||
status: item.applicationStatus,
|
||||
description: item.description,
|
||||
serverId: item.serverId,
|
||||
})) || [];
|
||||
|
||||
const mariadb: Services[] =
|
||||
data?.mariadb.map((item) => ({
|
||||
appName: item.appName,
|
||||
name: item.name,
|
||||
type: "mariadb",
|
||||
id: item.mariadbId,
|
||||
createdAt: item.createdAt,
|
||||
status: item.applicationStatus,
|
||||
description: item.description,
|
||||
serverId: item.serverId,
|
||||
})) || [];
|
||||
|
||||
const postgres: Services[] =
|
||||
data?.postgres.map((item) => ({
|
||||
appName: item.appName,
|
||||
name: item.name,
|
||||
type: "postgres",
|
||||
id: item.postgresId,
|
||||
createdAt: item.createdAt,
|
||||
status: item.applicationStatus,
|
||||
description: item.description,
|
||||
serverId: item.serverId,
|
||||
})) || [];
|
||||
|
||||
const mongo: Services[] =
|
||||
data?.mongo.map((item) => ({
|
||||
appName: item.appName,
|
||||
name: item.name,
|
||||
type: "mongo",
|
||||
id: item.mongoId,
|
||||
createdAt: item.createdAt,
|
||||
status: item.applicationStatus,
|
||||
description: item.description,
|
||||
serverId: item.serverId,
|
||||
})) || [];
|
||||
|
||||
const redis: Services[] =
|
||||
data?.redis.map((item) => ({
|
||||
appName: item.appName,
|
||||
name: item.name,
|
||||
type: "redis",
|
||||
id: item.redisId,
|
||||
createdAt: item.createdAt,
|
||||
status: item.applicationStatus,
|
||||
description: item.description,
|
||||
serverId: item.serverId,
|
||||
})) || [];
|
||||
|
||||
const mysql: Services[] =
|
||||
data?.mysql.map((item) => ({
|
||||
appName: item.appName,
|
||||
name: item.name,
|
||||
type: "mysql",
|
||||
id: item.mysqlId,
|
||||
createdAt: item.createdAt,
|
||||
status: item.applicationStatus,
|
||||
description: item.description,
|
||||
serverId: item.serverId,
|
||||
})) || [];
|
||||
|
||||
const compose: Services[] =
|
||||
data?.compose.map((item) => ({
|
||||
appName: item.appName,
|
||||
name: item.name,
|
||||
type: "compose",
|
||||
id: item.composeId,
|
||||
createdAt: item.createdAt,
|
||||
status: item.composeStatus,
|
||||
description: item.description,
|
||||
serverId: item.serverId,
|
||||
})) || [];
|
||||
|
||||
applications.push(
|
||||
...mysql,
|
||||
...redis,
|
||||
...mongo,
|
||||
...postgres,
|
||||
...mariadb,
|
||||
...compose,
|
||||
);
|
||||
|
||||
applications.sort((a, b) => {
|
||||
return new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime();
|
||||
});
|
||||
|
||||
return applications;
|
||||
};
|
||||
|
||||
const addPermissions = z.object({
|
||||
accessedProjects: z.array(z.string()).optional(),
|
||||
accessedEnvironments: z.array(z.string()).optional(),
|
||||
accessedServices: z.array(z.string()).optional(),
|
||||
canCreateProjects: z.boolean().optional().default(false),
|
||||
canCreateServices: z.boolean().optional().default(false),
|
||||
@@ -201,7 +76,6 @@ export const AddUserPermissions = ({ userId }: Props) => {
|
||||
if (data) {
|
||||
form.reset({
|
||||
accessedProjects: data.accessedProjects || [],
|
||||
accessedEnvironments: data.accessedEnvironments || [],
|
||||
accessedServices: data.accessedServices || [],
|
||||
canCreateProjects: data.canCreateProjects,
|
||||
canCreateServices: data.canCreateServices,
|
||||
@@ -225,7 +99,6 @@ export const AddUserPermissions = ({ userId }: Props) => {
|
||||
canDeleteProjects: data.canDeleteProjects,
|
||||
canAccessToTraefikFiles: data.canAccessToTraefikFiles,
|
||||
accessedProjects: data.accessedProjects || [],
|
||||
accessedEnvironments: data.accessedEnvironments || [],
|
||||
accessedServices: data.accessedServices || [],
|
||||
canAccessToDocker: data.canAccessToDocker,
|
||||
canAccessToAPI: data.canAccessToAPI,
|
||||
@@ -459,317 +332,89 @@ export const AddUserPermissions = ({ userId }: Props) => {
|
||||
No projects found
|
||||
</p>
|
||||
)}
|
||||
<div className="grid md:grid-cols-1 gap-4">
|
||||
{projects?.map((project, projectIndex) => {
|
||||
<div className="grid md:grid-cols-2 gap-4">
|
||||
{projects?.map((item, index) => {
|
||||
const applications = extractServices(item);
|
||||
return (
|
||||
<FormField
|
||||
key={`project-${projectIndex}`}
|
||||
key={`project-${index}`}
|
||||
control={form.control}
|
||||
name="accessedProjects"
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<FormItem
|
||||
key={project.projectId}
|
||||
className="flex flex-col items-start rounded-lg p-4 border"
|
||||
key={item.projectId}
|
||||
className="flex flex-col items-start space-x-4 rounded-lg p-4 border"
|
||||
>
|
||||
{/* Project Header */}
|
||||
<div className="flex flex-row gap-4 items-center w-full">
|
||||
<div className="flex flex-row gap-4">
|
||||
<FormControl>
|
||||
<Checkbox
|
||||
checked={field.value?.includes(
|
||||
project.projectId,
|
||||
item.projectId,
|
||||
)}
|
||||
onCheckedChange={(checked) => {
|
||||
if (checked) {
|
||||
// Add the project
|
||||
field.onChange([
|
||||
...(field.value || []),
|
||||
project.projectId,
|
||||
]);
|
||||
} else {
|
||||
// Remove the project
|
||||
field.onChange(
|
||||
field.value?.filter(
|
||||
(value) =>
|
||||
value !== project.projectId,
|
||||
),
|
||||
);
|
||||
|
||||
// Also remove all environments and services from this project
|
||||
const currentEnvs =
|
||||
form.getValues(
|
||||
"accessedEnvironments",
|
||||
) || [];
|
||||
const currentServices =
|
||||
form.getValues(
|
||||
"accessedServices",
|
||||
) || [];
|
||||
|
||||
// Get all environment IDs from this project
|
||||
const projectEnvIds =
|
||||
project.environments.map(
|
||||
(env) => env.environmentId,
|
||||
return checked
|
||||
? field.onChange([
|
||||
...(field.value || []),
|
||||
item.projectId,
|
||||
])
|
||||
: field.onChange(
|
||||
field.value?.filter(
|
||||
(value) =>
|
||||
value !== item.projectId,
|
||||
),
|
||||
);
|
||||
|
||||
// Get all service IDs from this project
|
||||
const projectServiceIds =
|
||||
project.environments.flatMap(
|
||||
(env) =>
|
||||
extractServices(env).map(
|
||||
(service) => service.id,
|
||||
),
|
||||
);
|
||||
|
||||
// Remove environments and services from this project
|
||||
form.setValue(
|
||||
"accessedEnvironments",
|
||||
currentEnvs.filter(
|
||||
(envId) =>
|
||||
!projectEnvIds.includes(envId),
|
||||
),
|
||||
);
|
||||
form.setValue(
|
||||
"accessedServices",
|
||||
currentServices.filter(
|
||||
(serviceId) =>
|
||||
!projectServiceIds.includes(
|
||||
serviceId,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormLabel className="text-base font-semibold text-primary">
|
||||
{project.name}
|
||||
<FormLabel className="text-sm font-medium text-primary">
|
||||
{item.name}
|
||||
</FormLabel>
|
||||
</div>
|
||||
|
||||
{/* Environments */}
|
||||
<div className="ml-6 w-full space-y-3">
|
||||
{project.environments.length === 0 && (
|
||||
<p className="text-sm text-muted-foreground">
|
||||
No environments found
|
||||
</p>
|
||||
)}
|
||||
{project.environments.map(
|
||||
(environment, envIndex) => {
|
||||
const services =
|
||||
extractServices(environment);
|
||||
{applications.length === 0 && (
|
||||
<p className="text-sm text-muted-foreground">
|
||||
No services found
|
||||
</p>
|
||||
)}
|
||||
{applications?.map((item, index) => (
|
||||
<FormField
|
||||
key={`project-${index}`}
|
||||
control={form.control}
|
||||
name="accessedServices"
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<div
|
||||
key={`env-${envIndex}`}
|
||||
className="border-l-2 border-muted pl-4"
|
||||
<FormItem
|
||||
key={item.id}
|
||||
className="flex flex-row items-start space-x-3 space-y-0"
|
||||
>
|
||||
{/* Environment Header with Checkbox */}
|
||||
<FormField
|
||||
key={`env-${envIndex}`}
|
||||
control={form.control}
|
||||
name="accessedEnvironments"
|
||||
render={({ field: envField }) => (
|
||||
<FormItem className="flex flex-row items-center space-x-3 space-y-0 mb-2">
|
||||
<FormControl>
|
||||
<Checkbox
|
||||
checked={envField.value?.includes(
|
||||
environment.environmentId,
|
||||
)}
|
||||
onCheckedChange={(
|
||||
checked,
|
||||
) => {
|
||||
if (checked) {
|
||||
// Add the environment
|
||||
envField.onChange([
|
||||
...(envField.value ||
|
||||
[]),
|
||||
environment.environmentId,
|
||||
]);
|
||||
|
||||
// Auto-select the project if not already selected
|
||||
const currentProjects =
|
||||
form.getValues(
|
||||
"accessedProjects",
|
||||
) || [];
|
||||
if (
|
||||
!currentProjects.includes(
|
||||
project.projectId,
|
||||
)
|
||||
) {
|
||||
form.setValue(
|
||||
"accessedProjects",
|
||||
[
|
||||
...currentProjects,
|
||||
project.projectId,
|
||||
],
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// Remove the environment
|
||||
envField.onChange(
|
||||
envField.value?.filter(
|
||||
(value) =>
|
||||
value !==
|
||||
environment.environmentId,
|
||||
),
|
||||
);
|
||||
|
||||
// Also remove all services from this environment
|
||||
const currentServices =
|
||||
form.getValues(
|
||||
"accessedServices",
|
||||
) || [];
|
||||
const environmentServiceIds =
|
||||
services.map(
|
||||
(service) =>
|
||||
service.id,
|
||||
);
|
||||
|
||||
form.setValue(
|
||||
"accessedServices",
|
||||
currentServices.filter(
|
||||
(serviceId) =>
|
||||
!environmentServiceIds.includes(
|
||||
serviceId,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-2 h-2 bg-blue-500 rounded-full" />
|
||||
<FormLabel className="text-sm font-medium text-foreground cursor-pointer">
|
||||
{environment.name}
|
||||
</FormLabel>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
({services.length} services)
|
||||
</span>
|
||||
</div>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
{/* Services */}
|
||||
<div className="ml-4 space-y-2">
|
||||
{services.length === 0 && (
|
||||
<p className="text-xs text-muted-foreground">
|
||||
No services found
|
||||
</p>
|
||||
)}
|
||||
{services.map(
|
||||
(service, serviceIndex) => (
|
||||
<FormField
|
||||
key={`service-${serviceIndex}`}
|
||||
control={form.control}
|
||||
name="accessedServices"
|
||||
render={({
|
||||
field: serviceField,
|
||||
}) => {
|
||||
return (
|
||||
<FormItem
|
||||
key={service.id}
|
||||
className="flex flex-row items-center space-x-3 space-y-0"
|
||||
>
|
||||
<FormControl>
|
||||
<Checkbox
|
||||
checked={serviceField.value?.includes(
|
||||
service.id,
|
||||
)}
|
||||
onCheckedChange={(
|
||||
checked,
|
||||
) => {
|
||||
if (checked) {
|
||||
// Add the service
|
||||
serviceField.onChange(
|
||||
[
|
||||
...(serviceField.value ||
|
||||
[]),
|
||||
service.id,
|
||||
],
|
||||
);
|
||||
|
||||
// Auto-select the environment if not already selected
|
||||
const currentEnvs =
|
||||
form.getValues(
|
||||
"accessedEnvironments",
|
||||
) || [];
|
||||
if (
|
||||
!currentEnvs.includes(
|
||||
environment.environmentId,
|
||||
)
|
||||
) {
|
||||
form.setValue(
|
||||
"accessedEnvironments",
|
||||
[
|
||||
...currentEnvs,
|
||||
environment.environmentId,
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
// Auto-select the project if not already selected
|
||||
const currentProjects =
|
||||
form.getValues(
|
||||
"accessedProjects",
|
||||
) || [];
|
||||
if (
|
||||
!currentProjects.includes(
|
||||
project.projectId,
|
||||
)
|
||||
) {
|
||||
form.setValue(
|
||||
"accessedProjects",
|
||||
[
|
||||
...currentProjects,
|
||||
project.projectId,
|
||||
],
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// Remove the service
|
||||
serviceField.onChange(
|
||||
serviceField.value?.filter(
|
||||
(value) =>
|
||||
value !==
|
||||
service.id,
|
||||
),
|
||||
);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
<div className="flex items-center gap-2">
|
||||
<div
|
||||
className={`w-1.5 h-1.5 rounded-full ${
|
||||
service.type ===
|
||||
"application"
|
||||
? "bg-green-500"
|
||||
: service.type ===
|
||||
"compose"
|
||||
? "bg-purple-500"
|
||||
: "bg-orange-500"
|
||||
}`}
|
||||
/>
|
||||
<FormLabel className="text-sm text-muted-foreground cursor-pointer">
|
||||
{service.name}
|
||||
</FormLabel>
|
||||
<span className="text-xs text-muted-foreground/70 capitalize">
|
||||
({service.type})
|
||||
</span>
|
||||
</div>
|
||||
</FormItem>
|
||||
<FormControl>
|
||||
<Checkbox
|
||||
checked={field.value?.includes(
|
||||
item.id,
|
||||
)}
|
||||
onCheckedChange={(checked) => {
|
||||
return checked
|
||||
? field.onChange([
|
||||
...(field.value || []),
|
||||
item.id,
|
||||
])
|
||||
: field.onChange(
|
||||
field.value?.filter(
|
||||
(value) =>
|
||||
value !== item.id,
|
||||
),
|
||||
);
|
||||
}}
|
||||
/>
|
||||
),
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormLabel className="text-sm text-muted-foreground">
|
||||
{item.name}
|
||||
</FormLabel>
|
||||
</FormItem>
|
||||
);
|
||||
},
|
||||
)}
|
||||
</div>
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</FormItem>
|
||||
);
|
||||
}}
|
||||
|
||||
@@ -11,20 +11,11 @@ interface Props {
|
||||
export const DashboardLayout = ({ children }: Props) => {
|
||||
const { data: haveRootAccess } = api.user.haveRootAccess.useQuery();
|
||||
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||
const { data: isUserSubscribed } = api.settings.isUserSubscribed.useQuery(
|
||||
undefined,
|
||||
{
|
||||
enabled: isCloud === true,
|
||||
refetchOnWindowFocus: false,
|
||||
refetchOnMount: false,
|
||||
refetchOnReconnect: false,
|
||||
},
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Page>{children}</Page>
|
||||
{isCloud === true && isUserSubscribed === true && (
|
||||
{isCloud === true && (
|
||||
<ChatwootWidget websiteToken="USCpQRKzHvFMssf3p6Eacae5" />
|
||||
)}
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ import { SidebarTrigger } from "@/components/ui/sidebar";
|
||||
interface Props {
|
||||
list: {
|
||||
name: string;
|
||||
href?: string;
|
||||
href: string;
|
||||
}[];
|
||||
}
|
||||
|
||||
@@ -29,11 +29,11 @@ export const BreadcrumbSidebar = ({ list }: Props) => {
|
||||
{list.map((item, index) => (
|
||||
<Fragment key={item.name}>
|
||||
<BreadcrumbItem className="block">
|
||||
<BreadcrumbLink href={item?.href} asChild={!!item?.href}>
|
||||
<BreadcrumbLink href={item.href} asChild={!!item.href}>
|
||||
{item.href ? (
|
||||
<Link href={item?.href}>{item?.name}</Link>
|
||||
<Link href={item.href}>{item.name}</Link>
|
||||
) : (
|
||||
item?.name
|
||||
item.name
|
||||
)}
|
||||
</BreadcrumbLink>
|
||||
</BreadcrumbItem>
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
import { useEffect, useRef } from "react";
|
||||
import { Input } from "@/components/ui/input";
|
||||
|
||||
type Props = React.ComponentPropsWithoutRef<typeof Input>;
|
||||
|
||||
export const FocusShortcutInput = (props: Props) => {
|
||||
const inputRef = useRef<HTMLInputElement | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const onKeyDown = (e: KeyboardEvent) => {
|
||||
const isMod = e.metaKey || e.ctrlKey;
|
||||
if (!isMod || e.key.toLowerCase() !== "k") return;
|
||||
|
||||
const target = e.target as HTMLElement | null;
|
||||
if (target) {
|
||||
const tag = target.tagName;
|
||||
if (
|
||||
target.isContentEditable ||
|
||||
tag === "INPUT" ||
|
||||
tag === "TEXTAREA" ||
|
||||
tag === "SELECT" ||
|
||||
target.getAttribute("role") === "textbox"
|
||||
)
|
||||
return;
|
||||
}
|
||||
|
||||
e.preventDefault();
|
||||
inputRef.current?.focus();
|
||||
};
|
||||
|
||||
window.addEventListener("keydown", onKeyDown);
|
||||
return () => window.removeEventListener("keydown", onKeyDown);
|
||||
}, []);
|
||||
|
||||
return <Input {...props} ref={inputRef} />;
|
||||
};
|
||||
@@ -1,16 +1,25 @@
|
||||
import copy from "copy-to-clipboard";
|
||||
import { Clipboard } from "lucide-react";
|
||||
import { useRef } from "react";
|
||||
import { Clipboard, EyeIcon, EyeOffIcon } from "lucide-react";
|
||||
import { useRef, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { Button } from "../ui/button";
|
||||
import { Input, type InputProps } from "../ui/input";
|
||||
|
||||
export const ToggleVisibilityInput = ({ ...props }: InputProps) => {
|
||||
const [isPasswordVisible, setIsPasswordVisible] = useState(false);
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const togglePasswordVisibility = () => {
|
||||
setIsPasswordVisible((prevVisibility) => !prevVisibility);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex w-full items-center space-x-2">
|
||||
<Input ref={inputRef} type={"password"} {...props} />
|
||||
<Input
|
||||
ref={inputRef}
|
||||
type={isPasswordVisible ? "text" : "password"}
|
||||
{...props}
|
||||
/>
|
||||
<Button
|
||||
variant={"secondary"}
|
||||
onClick={() => {
|
||||
@@ -20,13 +29,13 @@ export const ToggleVisibilityInput = ({ ...props }: InputProps) => {
|
||||
>
|
||||
<Clipboard className="size-4 text-muted-foreground" />
|
||||
</Button>
|
||||
{/* <Button onClick={togglePasswordVisibility} variant={"secondary"}>
|
||||
<Button onClick={togglePasswordVisibility} variant={"secondary"}>
|
||||
{isPasswordVisible ? (
|
||||
<EyeOffIcon className="size-4 text-muted-foreground" />
|
||||
) : (
|
||||
<EyeIcon className="size-4 text-muted-foreground" />
|
||||
)}
|
||||
</Button> */}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { EyeIcon, EyeOffIcon } from "lucide-react";
|
||||
import * as React from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
@@ -9,39 +8,18 @@ export interface InputProps
|
||||
|
||||
const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
||||
({ className, errorMessage, type, ...props }, ref) => {
|
||||
const [showPassword, setShowPassword] = React.useState(false);
|
||||
const isPassword = type === "password";
|
||||
const inputType = isPassword ? (showPassword ? "text" : "password") : type;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="relative w-full">
|
||||
<input
|
||||
type={inputType}
|
||||
className={cn(
|
||||
// bg-gray
|
||||
"flex h-10 w-full rounded-md bg-input px-3 py-2 text-sm file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-border disabled:cursor-not-allowed disabled:opacity-50",
|
||||
isPassword && "pr-10", // Add padding for the eye icon
|
||||
className,
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
{isPassword && (
|
||||
<button
|
||||
type="button"
|
||||
className="absolute inset-y-0 right-0 flex items-center pr-3 text-muted-foreground hover:text-foreground focus:outline-none"
|
||||
onClick={() => setShowPassword(!showPassword)}
|
||||
tabIndex={-1}
|
||||
>
|
||||
{showPassword ? (
|
||||
<EyeOffIcon className="h-4 w-4" />
|
||||
) : (
|
||||
<EyeIcon className="h-4 w-4" />
|
||||
)}
|
||||
</button>
|
||||
<input
|
||||
type={type}
|
||||
className={cn(
|
||||
// bg-gray
|
||||
"flex h-10 w-full rounded-md bg-input px-3 py-2 text-sm file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-border disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className,
|
||||
)}
|
||||
</div>
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
{errorMessage && (
|
||||
<span className="text-sm text-red-600 text-secondary-foreground">
|
||||
{errorMessage}
|
||||
|
||||
@@ -1,147 +0,0 @@
|
||||
CREATE TABLE "environment" (
|
||||
"environmentId" text PRIMARY KEY NOT NULL,
|
||||
"name" text NOT NULL,
|
||||
"description" text,
|
||||
"createdAt" text NOT NULL,
|
||||
"projectId" text NOT NULL
|
||||
);
|
||||
ALTER TABLE "environment" ADD CONSTRAINT "environment_projectId_project_projectId_fk" FOREIGN KEY ("projectId") REFERENCES "public"."project"("projectId") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||
|
||||
-- Insertar un ambiente "production" para cada proyecto existente
|
||||
INSERT INTO "environment" ("environmentId", "name", "description", "createdAt", "projectId")
|
||||
SELECT
|
||||
-- Generar un ID único para cada ambiente usando el projectId como base
|
||||
'env_prod_' || "projectId" || '_' || EXTRACT(EPOCH FROM NOW())::text,
|
||||
'production',
|
||||
'Production environment',
|
||||
NOW()::text,
|
||||
"projectId"
|
||||
FROM "project"
|
||||
WHERE "projectId" NOT IN (
|
||||
SELECT DISTINCT "projectId"
|
||||
FROM "environment"
|
||||
WHERE "name" = 'production'
|
||||
);
|
||||
|
||||
ALTER TABLE "application" ADD COLUMN "environmentId" text;--> statement-breakpoint
|
||||
ALTER TABLE "postgres" ADD COLUMN "environmentId" text;--> statement-breakpoint
|
||||
ALTER TABLE "mariadb" ADD COLUMN "environmentId" text;--> statement-breakpoint
|
||||
ALTER TABLE "mongo" ADD COLUMN "environmentId" text;--> statement-breakpoint
|
||||
ALTER TABLE "mysql" ADD COLUMN "environmentId" text;--> statement-breakpoint
|
||||
ALTER TABLE "redis" ADD COLUMN "environmentId" text;--> statement-breakpoint
|
||||
ALTER TABLE "compose" ADD COLUMN "environmentId" text;--> statement-breakpoint
|
||||
|
||||
|
||||
-- Step 3: Update all services to point to their project's production environment
|
||||
-- Update applications
|
||||
UPDATE "application"
|
||||
SET "environmentId" = (
|
||||
SELECT e."environmentId"
|
||||
FROM "environment" e
|
||||
WHERE e."projectId" = "application"."projectId"
|
||||
AND e."name" = 'production'
|
||||
LIMIT 1
|
||||
);--> statement-breakpoint
|
||||
|
||||
-- Update compose
|
||||
UPDATE "compose"
|
||||
SET "environmentId" = (
|
||||
SELECT e."environmentId"
|
||||
FROM "environment" e
|
||||
WHERE e."projectId" = "compose"."projectId"
|
||||
AND e."name" = 'production'
|
||||
LIMIT 1
|
||||
);--> statement-breakpoint
|
||||
|
||||
-- Update mariadb
|
||||
UPDATE "mariadb"
|
||||
SET "environmentId" = (
|
||||
SELECT e."environmentId"
|
||||
FROM "environment" e
|
||||
WHERE e."projectId" = "mariadb"."projectId"
|
||||
AND e."name" = 'production'
|
||||
LIMIT 1
|
||||
);--> statement-breakpoint
|
||||
|
||||
-- Update mongo
|
||||
UPDATE "mongo"
|
||||
SET "environmentId" = (
|
||||
SELECT e."environmentId"
|
||||
FROM "environment" e
|
||||
WHERE e."projectId" = "mongo"."projectId"
|
||||
AND e."name" = 'production'
|
||||
LIMIT 1
|
||||
);--> statement-breakpoint
|
||||
|
||||
-- Update mysql
|
||||
UPDATE "mysql"
|
||||
SET "environmentId" = (
|
||||
SELECT e."environmentId"
|
||||
FROM "environment" e
|
||||
WHERE e."projectId" = "mysql"."projectId"
|
||||
AND e."name" = 'production'
|
||||
LIMIT 1
|
||||
);--> statement-breakpoint
|
||||
|
||||
-- Update postgres
|
||||
UPDATE "postgres"
|
||||
SET "environmentId" = (
|
||||
SELECT e."environmentId"
|
||||
FROM "environment" e
|
||||
WHERE e."projectId" = "postgres"."projectId"
|
||||
AND e."name" = 'production'
|
||||
LIMIT 1
|
||||
);--> statement-breakpoint
|
||||
|
||||
-- Update redis
|
||||
UPDATE "redis"
|
||||
SET "environmentId" = (
|
||||
SELECT e."environmentId"
|
||||
FROM "environment" e
|
||||
WHERE e."projectId" = "redis"."projectId"
|
||||
AND e."name" = 'production'
|
||||
LIMIT 1
|
||||
);--> statement-breakpoint
|
||||
|
||||
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "application" DROP CONSTRAINT "application_projectId_project_projectId_fk";
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "postgres" DROP CONSTRAINT "postgres_projectId_project_projectId_fk";
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "mariadb" DROP CONSTRAINT "mariadb_projectId_project_projectId_fk";
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "mongo" DROP CONSTRAINT "mongo_projectId_project_projectId_fk";
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "mysql" DROP CONSTRAINT "mysql_projectId_project_projectId_fk";
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "redis" DROP CONSTRAINT "redis_projectId_project_projectId_fk";
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "compose" DROP CONSTRAINT "compose_projectId_project_projectId_fk";
|
||||
--> statement-breakpoint
|
||||
|
||||
-- Step 4: Make environmentId columns NOT NULL
|
||||
ALTER TABLE "application" ALTER COLUMN "environmentId" SET NOT NULL;--> statement-breakpoint
|
||||
ALTER TABLE "compose" ALTER COLUMN "environmentId" SET NOT NULL;--> statement-breakpoint
|
||||
ALTER TABLE "mariadb" ALTER COLUMN "environmentId" SET NOT NULL;--> statement-breakpoint
|
||||
ALTER TABLE "mongo" ALTER COLUMN "environmentId" SET NOT NULL;--> statement-breakpoint
|
||||
ALTER TABLE "mysql" ALTER COLUMN "environmentId" SET NOT NULL;--> statement-breakpoint
|
||||
ALTER TABLE "postgres" ALTER COLUMN "environmentId" SET NOT NULL;--> statement-breakpoint
|
||||
ALTER TABLE "redis" ALTER COLUMN "environmentId" SET NOT NULL;--> statement-breakpoint
|
||||
|
||||
|
||||
|
||||
ALTER TABLE "application" ADD CONSTRAINT "application_environmentId_environment_environmentId_fk" FOREIGN KEY ("environmentId") REFERENCES "public"."environment"("environmentId") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "postgres" ADD CONSTRAINT "postgres_environmentId_environment_environmentId_fk" FOREIGN KEY ("environmentId") REFERENCES "public"."environment"("environmentId") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "mariadb" ADD CONSTRAINT "mariadb_environmentId_environment_environmentId_fk" FOREIGN KEY ("environmentId") REFERENCES "public"."environment"("environmentId") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "mongo" ADD CONSTRAINT "mongo_environmentId_environment_environmentId_fk" FOREIGN KEY ("environmentId") REFERENCES "public"."environment"("environmentId") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "mysql" ADD CONSTRAINT "mysql_environmentId_environment_environmentId_fk" FOREIGN KEY ("environmentId") REFERENCES "public"."environment"("environmentId") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "redis" ADD CONSTRAINT "redis_environmentId_environment_environmentId_fk" FOREIGN KEY ("environmentId") REFERENCES "public"."environment"("environmentId") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "compose" ADD CONSTRAINT "compose_environmentId_environment_environmentId_fk" FOREIGN KEY ("environmentId") REFERENCES "public"."environment"("environmentId") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "application" DROP COLUMN "projectId";--> statement-breakpoint
|
||||
ALTER TABLE "postgres" DROP COLUMN "projectId";--> statement-breakpoint
|
||||
ALTER TABLE "mariadb" DROP COLUMN "projectId";--> statement-breakpoint
|
||||
ALTER TABLE "mongo" DROP COLUMN "projectId";--> statement-breakpoint
|
||||
ALTER TABLE "mysql" DROP COLUMN "projectId";--> statement-breakpoint
|
||||
ALTER TABLE "redis" DROP COLUMN "projectId";--> statement-breakpoint
|
||||
ALTER TABLE "compose" DROP COLUMN "projectId";
|
||||
@@ -1 +0,0 @@
|
||||
ALTER TABLE "environment" ADD COLUMN "env" text DEFAULT '' NOT NULL;
|
||||
@@ -1 +0,0 @@
|
||||
ALTER TABLE "member" ADD COLUMN "accessedEnvironments" text[] DEFAULT ARRAY[]::text[] NOT NULL;
|
||||
@@ -1,11 +0,0 @@
|
||||
ALTER TYPE "public"."notificationType" ADD VALUE 'ntfy';--> statement-breakpoint
|
||||
CREATE TABLE "ntfy" (
|
||||
"ntfyId" text PRIMARY KEY NOT NULL,
|
||||
"serverUrl" text NOT NULL,
|
||||
"topic" text NOT NULL,
|
||||
"accessToken" text NOT NULL,
|
||||
"priority" integer DEFAULT 3 NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "notification" ADD COLUMN "ntfyId" text;--> statement-breakpoint
|
||||
ALTER TABLE "notification" ADD CONSTRAINT "notification_ntfyId_ntfy_ntfyId_fk" FOREIGN KEY ("ntfyId") REFERENCES "public"."ntfy"("ntfyId") ON DELETE cascade ON UPDATE no action;
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -750,34 +750,6 @@
|
||||
"when": 1754912062243,
|
||||
"tag": "0106_purple_maggott",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 107,
|
||||
"version": "7",
|
||||
"when": 1756793713380,
|
||||
"tag": "0107_loud_kang",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 108,
|
||||
"version": "7",
|
||||
"when": 1756955718127,
|
||||
"tag": "0108_lazy_next_avengers",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 109,
|
||||
"version": "7",
|
||||
"when": 1757052053574,
|
||||
"tag": "0109_remarkable_sauron",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 110,
|
||||
"version": "7",
|
||||
"when": 1757189541734,
|
||||
"tag": "0110_red_psynapse",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "dokploy",
|
||||
"version": "v0.25.2",
|
||||
"version": "v0.24.12",
|
||||
"private": true,
|
||||
"license": "Apache-2.0",
|
||||
"type": "module",
|
||||
|
||||
@@ -20,11 +20,7 @@ export default async function handler(
|
||||
const application = await db.query.applications.findFirst({
|
||||
where: eq(applications.refreshToken, refreshToken as string),
|
||||
with: {
|
||||
environment: {
|
||||
with: {
|
||||
project: true,
|
||||
},
|
||||
},
|
||||
project: true,
|
||||
bitbucket: true,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -27,11 +27,7 @@ export default async function handler(
|
||||
const composeResult = await db.query.compose.findFirst({
|
||||
where: eq(compose.refreshToken, refreshToken as string),
|
||||
with: {
|
||||
environment: {
|
||||
with: {
|
||||
project: true,
|
||||
},
|
||||
},
|
||||
project: true,
|
||||
bitbucket: true,
|
||||
},
|
||||
});
|
||||
|
||||
1372
apps/dokploy/pages/dashboard/project/[projectId].tsx
Normal file
1372
apps/dokploy/pages/dashboard/project/[projectId].tsx
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -71,7 +71,7 @@ const Service = (
|
||||
const [_toggleMonitoring, _setToggleMonitoring] = useState(false);
|
||||
const { applicationId, activeTab } = props;
|
||||
const router = useRouter();
|
||||
const { projectId, environmentId } = router.query;
|
||||
const { projectId } = router.query;
|
||||
const [tab, setTab] = useState<TabState>(activeTab);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -97,20 +97,18 @@ const Service = (
|
||||
list={[
|
||||
{ name: "Projects", href: "/dashboard/projects" },
|
||||
{
|
||||
name: data?.environment.project.name || "",
|
||||
},
|
||||
{
|
||||
name: data?.environment?.name || "",
|
||||
href: `/dashboard/project/${projectId}/environment/${environmentId}`,
|
||||
name: data?.project?.name || "",
|
||||
href: `/dashboard/project/${projectId}`,
|
||||
},
|
||||
{
|
||||
name: data?.name || "",
|
||||
href: `/dashboard/project/${projectId}/services/application/${applicationId}`,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<Head>
|
||||
<title>
|
||||
Application: {data?.name} - {data?.environment.project.name} | Dokploy
|
||||
Application: {data?.name} - {data?.project.name} | Dokploy
|
||||
</title>
|
||||
</Head>
|
||||
<div className="w-full">
|
||||
@@ -217,7 +215,7 @@ const Service = (
|
||||
className="w-full"
|
||||
onValueChange={(e) => {
|
||||
setTab(e as TabState);
|
||||
const newPath = `/dashboard/project/${projectId}/environment/${environmentId}/services/application/${applicationId}?tab=${e}`;
|
||||
const newPath = `/dashboard/project/${projectId}/services/application/${applicationId}?tab=${e}`;
|
||||
router.push(newPath);
|
||||
}}
|
||||
>
|
||||
@@ -381,7 +379,6 @@ export async function getServerSideProps(
|
||||
ctx: GetServerSidePropsContext<{
|
||||
applicationId: string;
|
||||
activeTab: TabState;
|
||||
environmentId: string;
|
||||
}>,
|
||||
) {
|
||||
const { query, params, req, res } = ctx;
|
||||
@@ -423,7 +420,6 @@ export async function getServerSideProps(
|
||||
trpcState: helpers.dehydrate(),
|
||||
applicationId: params?.applicationId,
|
||||
activeTab: (activeTab || "general") as TabState,
|
||||
environmentId: params?.environmentId,
|
||||
},
|
||||
};
|
||||
} catch {
|
||||
@@ -67,7 +67,7 @@ const Service = (
|
||||
const [_toggleMonitoring, _setToggleMonitoring] = useState(false);
|
||||
const { composeId, activeTab } = props;
|
||||
const router = useRouter();
|
||||
const { projectId, environmentId } = router.query;
|
||||
const { projectId } = router.query;
|
||||
const [tab, setTab] = useState<TabState>(activeTab);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -88,20 +88,18 @@ const Service = (
|
||||
list={[
|
||||
{ name: "Projects", href: "/dashboard/projects" },
|
||||
{
|
||||
name: data?.environment?.project?.name || "",
|
||||
},
|
||||
{
|
||||
name: data?.environment?.name || "",
|
||||
href: `/dashboard/project/${projectId}/environment/${environmentId}`,
|
||||
name: data?.project?.name || "",
|
||||
href: `/dashboard/project/${projectId}`,
|
||||
},
|
||||
{
|
||||
name: data?.name || "",
|
||||
href: `/dashboard/project/${projectId}/services/compose/${composeId}`,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<Head>
|
||||
<title>
|
||||
Compose: {data?.name} - {data?.environment?.project?.name} | Dokploy
|
||||
Compose: {data?.name} - {data?.project.name} | Dokploy
|
||||
</title>
|
||||
</Head>
|
||||
<div className="w-full">
|
||||
@@ -210,7 +208,7 @@ const Service = (
|
||||
className="w-full"
|
||||
onValueChange={(e) => {
|
||||
setTab(e as TabState);
|
||||
const newPath = `/dashboard/project/${projectId}/environment/${environmentId}/services/compose/${composeId}?tab=${e}`;
|
||||
const newPath = `/dashboard/project/${projectId}/services/compose/${composeId}?tab=${e}`;
|
||||
router.push(newPath);
|
||||
}}
|
||||
>
|
||||
@@ -377,7 +375,6 @@ export async function getServerSideProps(
|
||||
ctx: GetServerSidePropsContext<{
|
||||
composeId: string;
|
||||
activeTab: TabState;
|
||||
environmentId: string;
|
||||
}>,
|
||||
) {
|
||||
const { query, params, req, res } = ctx;
|
||||
@@ -417,7 +414,6 @@ export async function getServerSideProps(
|
||||
trpcState: helpers.dehydrate(),
|
||||
composeId: params?.composeId,
|
||||
activeTab: (activeTab || "general") as TabState,
|
||||
environmentId: params?.environmentId,
|
||||
},
|
||||
};
|
||||
} catch {
|
||||
@@ -55,7 +55,7 @@ const Mariadb = (
|
||||
|
||||
const { mariadbId, activeTab } = props;
|
||||
const router = useRouter();
|
||||
const { projectId, environmentId } = router.query;
|
||||
const { projectId } = router.query;
|
||||
const [tab, setSab] = useState<TabState>(activeTab);
|
||||
const { data } = api.mariadb.one.useQuery({ mariadbId });
|
||||
const { data: auth } = api.user.get.useQuery();
|
||||
@@ -69,22 +69,19 @@ const Mariadb = (
|
||||
list={[
|
||||
{ name: "Projects", href: "/dashboard/projects" },
|
||||
{
|
||||
name: data?.environment?.project?.name || "",
|
||||
},
|
||||
{
|
||||
name: data?.environment?.name || "",
|
||||
href: `/dashboard/project/${projectId}/environment/${environmentId}`,
|
||||
name: data?.project?.name || "",
|
||||
href: `/dashboard/project/${projectId}`,
|
||||
},
|
||||
{
|
||||
name: data?.name || "",
|
||||
href: `/dashboard/project/${projectId}/services/mariadb/${mariadbId}`,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<div className="flex flex-col gap-4">
|
||||
<Head>
|
||||
<title>
|
||||
Database: {data?.name} - {data?.environment?.project?.name} |
|
||||
Dokploy
|
||||
Database: {data?.name} - {data?.project.name} | Dokploy
|
||||
</title>
|
||||
</Head>
|
||||
<Card className="h-full bg-sidebar p-2.5 rounded-xl w-full">
|
||||
@@ -182,7 +179,7 @@ const Mariadb = (
|
||||
className="w-full"
|
||||
onValueChange={(e) => {
|
||||
setSab(e as TabState);
|
||||
const newPath = `/dashboard/project/${projectId}/environment/${environmentId}/services/mariadb/${mariadbId}?tab=${e}`;
|
||||
const newPath = `/dashboard/project/${projectId}/services/mariadb/${mariadbId}?tab=${e}`;
|
||||
|
||||
router.push(newPath, undefined, { shallow: true });
|
||||
}}
|
||||
@@ -303,11 +300,7 @@ Mariadb.getLayout = (page: ReactElement) => {
|
||||
};
|
||||
|
||||
export async function getServerSideProps(
|
||||
ctx: GetServerSidePropsContext<{
|
||||
mariadbId: string;
|
||||
activeTab: TabState;
|
||||
environmentId: string;
|
||||
}>,
|
||||
ctx: GetServerSidePropsContext<{ mariadbId: string; activeTab: TabState }>,
|
||||
) {
|
||||
const { query, params, req, res } = ctx;
|
||||
const activeTab = query.tab;
|
||||
@@ -345,7 +338,6 @@ export async function getServerSideProps(
|
||||
trpcState: helpers.dehydrate(),
|
||||
mariadbId: params?.mariadbId,
|
||||
activeTab: (activeTab || "general") as TabState,
|
||||
environmentId: params?.environmentId,
|
||||
},
|
||||
};
|
||||
} catch {
|
||||
@@ -54,7 +54,7 @@ const Mongo = (
|
||||
const [_toggleMonitoring, _setToggleMonitoring] = useState(false);
|
||||
const { mongoId, activeTab } = props;
|
||||
const router = useRouter();
|
||||
const { projectId, environmentId } = router.query;
|
||||
const { projectId } = router.query;
|
||||
const [tab, setSab] = useState<TabState>(activeTab);
|
||||
const { data } = api.mongo.one.useQuery({ mongoId });
|
||||
|
||||
@@ -69,20 +69,18 @@ const Mongo = (
|
||||
list={[
|
||||
{ name: "Projects", href: "/dashboard/projects" },
|
||||
{
|
||||
name: data?.environment?.project?.name || "",
|
||||
},
|
||||
{
|
||||
name: data?.environment?.name || "",
|
||||
href: `/dashboard/project/${projectId}/environment/${environmentId}`,
|
||||
name: data?.project?.name || "",
|
||||
href: `/dashboard/project/${projectId}`,
|
||||
},
|
||||
{
|
||||
name: data?.name || "",
|
||||
href: `/dashboard/project/${projectId}/services/mongo/${mongoId}`,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<Head>
|
||||
<title>
|
||||
Database: {data?.name} - {data?.environment?.project?.name} | Dokploy
|
||||
Database: {data?.name} - {data?.project.name} | Dokploy
|
||||
</title>
|
||||
</Head>
|
||||
<div className="w-full">
|
||||
@@ -182,7 +180,7 @@ const Mongo = (
|
||||
className="w-full"
|
||||
onValueChange={(e) => {
|
||||
setSab(e as TabState);
|
||||
const newPath = `/dashboard/project/${projectId}/environment/${environmentId}/services/mongo/${mongoId}?tab=${e}`;
|
||||
const newPath = `/dashboard/project/${projectId}/services/mongo/${mongoId}?tab=${e}`;
|
||||
|
||||
router.push(newPath, undefined, { shallow: true });
|
||||
}}
|
||||
@@ -304,11 +302,7 @@ Mongo.getLayout = (page: ReactElement) => {
|
||||
};
|
||||
|
||||
export async function getServerSideProps(
|
||||
ctx: GetServerSidePropsContext<{
|
||||
mongoId: string;
|
||||
activeTab: TabState;
|
||||
environmentId: string;
|
||||
}>,
|
||||
ctx: GetServerSidePropsContext<{ mongoId: string; activeTab: TabState }>,
|
||||
) {
|
||||
const { query, params, req, res } = ctx;
|
||||
const activeTab = query.tab;
|
||||
@@ -346,7 +340,6 @@ export async function getServerSideProps(
|
||||
trpcState: helpers.dehydrate(),
|
||||
mongoId: params?.mongoId,
|
||||
activeTab: (activeTab || "general") as TabState,
|
||||
environmentId: params?.environmentId,
|
||||
},
|
||||
};
|
||||
} catch {
|
||||
@@ -54,7 +54,7 @@ const MySql = (
|
||||
const [_toggleMonitoring, _setToggleMonitoring] = useState(false);
|
||||
const { mysqlId, activeTab } = props;
|
||||
const router = useRouter();
|
||||
const { projectId, environmentId } = router.query;
|
||||
const { projectId } = router.query;
|
||||
const [tab, setSab] = useState<TabState>(activeTab);
|
||||
const { data } = api.mysql.one.useQuery({ mysqlId });
|
||||
const { data: auth } = api.user.get.useQuery();
|
||||
@@ -68,22 +68,19 @@ const MySql = (
|
||||
list={[
|
||||
{ name: "Projects", href: "/dashboard/projects" },
|
||||
{
|
||||
name: data?.environment?.project?.name || "",
|
||||
},
|
||||
{
|
||||
name: data?.environment?.name || "",
|
||||
href: `/dashboard/project/${projectId}/environment/${environmentId}`,
|
||||
name: data?.project?.name || "",
|
||||
href: `/dashboard/project/${projectId}`,
|
||||
},
|
||||
{
|
||||
name: data?.name || "",
|
||||
href: `/dashboard/project/${projectId}/services/mysql/${mysqlId}`,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<div className="flex flex-col gap-4">
|
||||
<Head>
|
||||
<title>
|
||||
Database: {data?.name} - {data?.environment?.project?.name} |
|
||||
Dokploy
|
||||
Database: {data?.name} - {data?.project.name} | Dokploy
|
||||
</title>
|
||||
</Head>
|
||||
<div className="w-full">
|
||||
@@ -183,7 +180,7 @@ const MySql = (
|
||||
className="w-full"
|
||||
onValueChange={(e) => {
|
||||
setSab(e as TabState);
|
||||
const newPath = `/dashboard/project/${projectId}/environment/${environmentId}/services/mysql/${mysqlId}?tab=${e}`;
|
||||
const newPath = `/dashboard/project/${projectId}/services/mysql/${mysqlId}?tab=${e}`;
|
||||
|
||||
router.push(newPath, undefined, { shallow: true });
|
||||
}}
|
||||
@@ -289,11 +286,7 @@ MySql.getLayout = (page: ReactElement) => {
|
||||
};
|
||||
|
||||
export async function getServerSideProps(
|
||||
ctx: GetServerSidePropsContext<{
|
||||
mysqlId: string;
|
||||
activeTab: TabState;
|
||||
environmentId: string;
|
||||
}>,
|
||||
ctx: GetServerSidePropsContext<{ mysqlId: string; activeTab: TabState }>,
|
||||
) {
|
||||
const { query, params, req, res } = ctx;
|
||||
const activeTab = query.tab;
|
||||
@@ -54,7 +54,7 @@ const Postgresql = (
|
||||
const [_toggleMonitoring, _setToggleMonitoring] = useState(false);
|
||||
const { postgresId, activeTab } = props;
|
||||
const router = useRouter();
|
||||
const { projectId, environmentId } = router.query;
|
||||
const { projectId } = router.query;
|
||||
const [tab, setSab] = useState<TabState>(activeTab);
|
||||
const { data } = api.postgres.one.useQuery({ postgresId });
|
||||
const { data: auth } = api.user.get.useQuery();
|
||||
@@ -68,20 +68,18 @@ const Postgresql = (
|
||||
list={[
|
||||
{ name: "Projects", href: "/dashboard/projects" },
|
||||
{
|
||||
name: data?.environment?.project?.name || "",
|
||||
},
|
||||
{
|
||||
name: data?.environment?.name || "",
|
||||
href: `/dashboard/project/${projectId}/environment/${environmentId}`,
|
||||
name: data?.project?.name || "",
|
||||
href: `/dashboard/project/${projectId}`,
|
||||
},
|
||||
{
|
||||
name: data?.name || "",
|
||||
href: `/dashboard/project/${projectId}/services/postgres/${postgresId}`,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<Head>
|
||||
<title>
|
||||
Database: {data?.name} - {data?.environment?.project?.name} | Dokploy
|
||||
Database: {data?.name} - {data?.project.name} | Dokploy
|
||||
</title>
|
||||
</Head>
|
||||
<div className="w-full">
|
||||
@@ -181,11 +179,9 @@ const Postgresql = (
|
||||
className="w-full"
|
||||
onValueChange={(e) => {
|
||||
setSab(e as TabState);
|
||||
const newPath = `/dashboard/project/${projectId}/environment/${environmentId}/services/postgres/${postgresId}?tab=${e}`;
|
||||
const newPath = `/dashboard/project/${projectId}/services/postgres/${postgresId}?tab=${e}`;
|
||||
|
||||
router.push(newPath, undefined, {
|
||||
shallow: true,
|
||||
});
|
||||
router.push(newPath, undefined, { shallow: true });
|
||||
}}
|
||||
>
|
||||
<div className="flex flex-row items-center justify-between w-full gap-4 overflow-x-scroll">
|
||||
@@ -232,11 +228,7 @@ const Postgresql = (
|
||||
{data?.serverId && isCloud ? (
|
||||
<ContainerPaidMonitoring
|
||||
appName={data?.appName || ""}
|
||||
baseUrl={`${
|
||||
data?.serverId
|
||||
? `http://${data?.server?.ipAddress}:${data?.server?.metricsConfig?.server?.port}`
|
||||
: "http://localhost:4500"
|
||||
}`}
|
||||
baseUrl={`${data?.serverId ? `http://${data?.server?.ipAddress}:${data?.server?.metricsConfig?.server?.port}` : "http://localhost:4500"}`}
|
||||
token={
|
||||
data?.server?.metricsConfig?.server?.token || ""
|
||||
}
|
||||
@@ -292,11 +284,7 @@ Postgresql.getLayout = (page: ReactElement) => {
|
||||
};
|
||||
|
||||
export async function getServerSideProps(
|
||||
ctx: GetServerSidePropsContext<{
|
||||
postgresId: string;
|
||||
activeTab: TabState;
|
||||
environmentId: string;
|
||||
}>,
|
||||
ctx: GetServerSidePropsContext<{ postgresId: string; activeTab: TabState }>,
|
||||
) {
|
||||
const { query, params, req, res } = ctx;
|
||||
const activeTab = query.tab;
|
||||
@@ -53,7 +53,7 @@ const Redis = (
|
||||
const [_toggleMonitoring, _setToggleMonitoring] = useState(false);
|
||||
const { redisId, activeTab } = props;
|
||||
const router = useRouter();
|
||||
const { projectId, environmentId } = router.query;
|
||||
const { projectId } = router.query;
|
||||
const [tab, setSab] = useState<TabState>(activeTab);
|
||||
const { data } = api.redis.one.useQuery({ redisId });
|
||||
|
||||
@@ -68,20 +68,18 @@ const Redis = (
|
||||
list={[
|
||||
{ name: "Projects", href: "/dashboard/projects" },
|
||||
{
|
||||
name: data?.environment?.project?.name || "",
|
||||
},
|
||||
{
|
||||
name: data?.environment?.name || "",
|
||||
href: `/dashboard/project/${projectId}/environment/${environmentId}`,
|
||||
name: data?.project?.name || "",
|
||||
href: `/dashboard/project/${projectId}`,
|
||||
},
|
||||
{
|
||||
name: data?.name || "",
|
||||
href: `/dashboard/project/${projectId}/services/redis/${redisId}`,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<Head>
|
||||
<title>
|
||||
Database: {data?.name} - {data?.environment?.project?.name} | Dokploy
|
||||
Database: {data?.name} - {data?.project.name} | Dokploy
|
||||
</title>
|
||||
</Head>
|
||||
<div className="w-full">
|
||||
@@ -181,7 +179,7 @@ const Redis = (
|
||||
className="w-full"
|
||||
onValueChange={(e) => {
|
||||
setSab(e as TabState);
|
||||
const newPath = `/dashboard/project/${projectId}/environment/${environmentId}/services/redis/${redisId}?tab=${e}`;
|
||||
const newPath = `/dashboard/project/${projectId}/services/redis/${redisId}?tab=${e}`;
|
||||
|
||||
router.push(newPath, undefined, { shallow: true });
|
||||
}}
|
||||
@@ -293,11 +291,7 @@ Redis.getLayout = (page: ReactElement) => {
|
||||
};
|
||||
|
||||
export async function getServerSideProps(
|
||||
ctx: GetServerSidePropsContext<{
|
||||
redisId: string;
|
||||
activeTab: TabState;
|
||||
environmentId: string;
|
||||
}>,
|
||||
ctx: GetServerSidePropsContext<{ redisId: string; activeTab: TabState }>,
|
||||
) {
|
||||
const { query, params, req, res } = ctx;
|
||||
const activeTab = query.tab;
|
||||
@@ -11,7 +11,6 @@ import { deploymentRouter } from "./routers/deployment";
|
||||
import { destinationRouter } from "./routers/destination";
|
||||
import { dockerRouter } from "./routers/docker";
|
||||
import { domainRouter } from "./routers/domain";
|
||||
import { environmentRouter } from "./routers/environment";
|
||||
import { gitProviderRouter } from "./routers/git-provider";
|
||||
import { giteaRouter } from "./routers/gitea";
|
||||
import { githubRouter } from "./routers/github";
|
||||
@@ -85,7 +84,6 @@ export const appRouter = createTRPCRouter({
|
||||
schedule: scheduleRouter,
|
||||
rollback: rollbackRouter,
|
||||
volumeBackups: volumeBackupsRouter,
|
||||
environment: environmentRouter,
|
||||
});
|
||||
|
||||
// export type definition of API
|
||||
|
||||
@@ -4,11 +4,7 @@ import {
|
||||
apiUpdateAi,
|
||||
deploySuggestionSchema,
|
||||
} from "@dokploy/server/db/schema/ai";
|
||||
import {
|
||||
createDomain,
|
||||
createMount,
|
||||
findEnvironmentById,
|
||||
} from "@dokploy/server/index";
|
||||
import { createDomain, createMount } from "@dokploy/server/index";
|
||||
import {
|
||||
deleteAiSettings,
|
||||
getAiSettingById,
|
||||
@@ -181,12 +177,10 @@ export const aiRouter = createTRPCRouter({
|
||||
deploy: protectedProcedure
|
||||
.input(deploySuggestionSchema)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const environment = await findEnvironmentById(input.environmentId);
|
||||
const project = await findProjectById(environment.projectId);
|
||||
if (ctx.user.role === "member") {
|
||||
await checkServiceAccess(
|
||||
ctx.session.activeOrganizationId,
|
||||
environment.projectId,
|
||||
input.projectId,
|
||||
"create",
|
||||
);
|
||||
}
|
||||
@@ -198,6 +192,8 @@ export const aiRouter = createTRPCRouter({
|
||||
});
|
||||
}
|
||||
|
||||
const project = await findProjectById(input.projectId);
|
||||
|
||||
const projectName = slugify(`${project.name} ${input.id}`);
|
||||
|
||||
const compose = await createComposeByTemplate({
|
||||
@@ -209,7 +205,6 @@ export const aiRouter = createTRPCRouter({
|
||||
sourceType: "raw",
|
||||
appName: `${projectName}-${generatePassword(6)}`,
|
||||
isolatedDeployment: true,
|
||||
environmentId: input.environmentId,
|
||||
});
|
||||
|
||||
if (input.domains && input.domains?.length > 0) {
|
||||
|
||||
@@ -4,7 +4,6 @@ import {
|
||||
createApplication,
|
||||
deleteAllMiddlewares,
|
||||
findApplicationById,
|
||||
findEnvironmentById,
|
||||
findGitProviderById,
|
||||
findProjectById,
|
||||
getApplicationStats,
|
||||
@@ -24,7 +23,6 @@ import {
|
||||
unzipDrop,
|
||||
updateApplication,
|
||||
updateApplicationStatus,
|
||||
updateDeploymentStatus,
|
||||
writeConfig,
|
||||
writeConfigRemote,
|
||||
// uploadFileSchema
|
||||
@@ -41,10 +39,8 @@ import {
|
||||
import { db } from "@/server/db";
|
||||
import {
|
||||
apiCreateApplication,
|
||||
apiDeployApplication,
|
||||
apiFindMonitoringStats,
|
||||
apiFindOneApplication,
|
||||
apiRedeployApplication,
|
||||
apiReloadApplication,
|
||||
apiSaveBitbucketProvider,
|
||||
apiSaveBuildType,
|
||||
@@ -59,7 +55,7 @@ import {
|
||||
} from "@/server/db/schema";
|
||||
import type { DeploymentJob } from "@/server/queues/queue-types";
|
||||
import { cleanQueuesByApplication, myQueue } from "@/server/queues/queueSetup";
|
||||
import { cancelDeployment, deploy } from "@/server/utils/deploy";
|
||||
import { deploy } from "@/server/utils/deploy";
|
||||
import { uploadFileSchema } from "@/utils/schema";
|
||||
|
||||
export const applicationRouter = createTRPCRouter({
|
||||
@@ -67,14 +63,10 @@ export const applicationRouter = createTRPCRouter({
|
||||
.input(apiCreateApplication)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
// Get project from environment
|
||||
const environment = await findEnvironmentById(input.environmentId);
|
||||
const project = await findProjectById(environment.projectId);
|
||||
|
||||
if (ctx.user.role === "member") {
|
||||
await checkServiceAccess(
|
||||
ctx.user.id,
|
||||
project.projectId,
|
||||
input.projectId,
|
||||
ctx.session.activeOrganizationId,
|
||||
"create",
|
||||
);
|
||||
@@ -87,13 +79,13 @@ export const applicationRouter = createTRPCRouter({
|
||||
});
|
||||
}
|
||||
|
||||
const project = await findProjectById(input.projectId);
|
||||
if (project.organizationId !== ctx.session.activeOrganizationId) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to access this project",
|
||||
});
|
||||
}
|
||||
|
||||
const newApplication = await createApplication(input);
|
||||
|
||||
if (ctx.user.role === "member") {
|
||||
@@ -105,7 +97,6 @@ export const applicationRouter = createTRPCRouter({
|
||||
}
|
||||
return newApplication;
|
||||
} catch (error: unknown) {
|
||||
console.log("error", error);
|
||||
if (error instanceof TRPCError) {
|
||||
throw error;
|
||||
}
|
||||
@@ -129,8 +120,7 @@ export const applicationRouter = createTRPCRouter({
|
||||
}
|
||||
const application = await findApplicationById(input.applicationId);
|
||||
if (
|
||||
application.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
application.project.organizationId !== ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
@@ -185,7 +175,7 @@ export const applicationRouter = createTRPCRouter({
|
||||
|
||||
try {
|
||||
if (
|
||||
application.environment.project.organizationId !==
|
||||
application.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
@@ -222,8 +212,7 @@ export const applicationRouter = createTRPCRouter({
|
||||
const application = await findApplicationById(input.applicationId);
|
||||
|
||||
if (
|
||||
application.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
application.project.organizationId !== ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
@@ -258,17 +247,14 @@ export const applicationRouter = createTRPCRouter({
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
return application;
|
||||
return result[0];
|
||||
}),
|
||||
|
||||
stop: protectedProcedure
|
||||
.input(apiFindOneApplication)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const service = await findApplicationById(input.applicationId);
|
||||
if (
|
||||
service.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
if (service.project.organizationId !== ctx.session.activeOrganizationId) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to stop this application",
|
||||
@@ -288,10 +274,7 @@ export const applicationRouter = createTRPCRouter({
|
||||
.input(apiFindOneApplication)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const service = await findApplicationById(input.applicationId);
|
||||
if (
|
||||
service.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
if (service.project.organizationId !== ctx.session.activeOrganizationId) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to start this application",
|
||||
@@ -309,12 +292,11 @@ export const applicationRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
redeploy: protectedProcedure
|
||||
.input(apiRedeployApplication)
|
||||
.input(apiFindOneApplication)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const application = await findApplicationById(input.applicationId);
|
||||
if (
|
||||
application.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
application.project.organizationId !== ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
@@ -323,8 +305,8 @@ export const applicationRouter = createTRPCRouter({
|
||||
}
|
||||
const jobData: DeploymentJob = {
|
||||
applicationId: input.applicationId,
|
||||
titleLog: input.title || "Rebuild deployment",
|
||||
descriptionLog: input.description || "",
|
||||
titleLog: "Rebuild deployment",
|
||||
descriptionLog: "",
|
||||
type: "redeploy",
|
||||
applicationType: "application",
|
||||
server: !!application.serverId,
|
||||
@@ -349,8 +331,7 @@ export const applicationRouter = createTRPCRouter({
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const application = await findApplicationById(input.applicationId);
|
||||
if (
|
||||
application.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
application.project.organizationId !== ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
@@ -368,8 +349,7 @@ export const applicationRouter = createTRPCRouter({
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const application = await findApplicationById(input.applicationId);
|
||||
if (
|
||||
application.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
application.project.organizationId !== ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
@@ -394,8 +374,7 @@ export const applicationRouter = createTRPCRouter({
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const application = await findApplicationById(input.applicationId);
|
||||
if (
|
||||
application.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
application.project.organizationId !== ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
@@ -422,8 +401,7 @@ export const applicationRouter = createTRPCRouter({
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const application = await findApplicationById(input.applicationId);
|
||||
if (
|
||||
application.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
application.project.organizationId !== ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
@@ -451,8 +429,7 @@ export const applicationRouter = createTRPCRouter({
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const application = await findApplicationById(input.applicationId);
|
||||
if (
|
||||
application.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
application.project.organizationId !== ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
@@ -478,8 +455,7 @@ export const applicationRouter = createTRPCRouter({
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const application = await findApplicationById(input.applicationId);
|
||||
if (
|
||||
application.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
application.project.organizationId !== ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
@@ -505,8 +481,7 @@ export const applicationRouter = createTRPCRouter({
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const application = await findApplicationById(input.applicationId);
|
||||
if (
|
||||
application.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
application.project.organizationId !== ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
@@ -529,8 +504,7 @@ export const applicationRouter = createTRPCRouter({
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const application = await findApplicationById(input.applicationId);
|
||||
if (
|
||||
application.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
application.project.organizationId !== ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
@@ -555,8 +529,7 @@ export const applicationRouter = createTRPCRouter({
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const application = await findApplicationById(input.applicationId);
|
||||
if (
|
||||
application.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
application.project.organizationId !== ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
@@ -617,8 +590,7 @@ export const applicationRouter = createTRPCRouter({
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const application = await findApplicationById(input.applicationId);
|
||||
if (
|
||||
application.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
application.project.organizationId !== ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
@@ -632,8 +604,7 @@ export const applicationRouter = createTRPCRouter({
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const application = await findApplicationById(input.applicationId);
|
||||
if (
|
||||
application.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
application.project.organizationId !== ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
@@ -659,8 +630,7 @@ export const applicationRouter = createTRPCRouter({
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const application = await findApplicationById(input.applicationId);
|
||||
if (
|
||||
application.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
application.project.organizationId !== ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
@@ -673,12 +643,11 @@ export const applicationRouter = createTRPCRouter({
|
||||
return true;
|
||||
}),
|
||||
deploy: protectedProcedure
|
||||
.input(apiDeployApplication)
|
||||
.input(apiFindOneApplication)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const application = await findApplicationById(input.applicationId);
|
||||
if (
|
||||
application.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
application.project.organizationId !== ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
@@ -687,8 +656,8 @@ export const applicationRouter = createTRPCRouter({
|
||||
}
|
||||
const jobData: DeploymentJob = {
|
||||
applicationId: input.applicationId,
|
||||
titleLog: input.title || "Manual deployment",
|
||||
descriptionLog: input.description || "",
|
||||
titleLog: "Manual deployment",
|
||||
descriptionLog: "",
|
||||
type: "deploy",
|
||||
applicationType: "application",
|
||||
server: !!application.serverId,
|
||||
@@ -714,8 +683,7 @@ export const applicationRouter = createTRPCRouter({
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const application = await findApplicationById(input.applicationId);
|
||||
if (
|
||||
application.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
application.project.organizationId !== ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
@@ -730,8 +698,7 @@ export const applicationRouter = createTRPCRouter({
|
||||
.query(async ({ input, ctx }) => {
|
||||
const application = await findApplicationById(input.applicationId);
|
||||
if (
|
||||
application.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
application.project.organizationId !== ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
@@ -767,10 +734,7 @@ export const applicationRouter = createTRPCRouter({
|
||||
|
||||
const app = await findApplicationById(input.applicationId as string);
|
||||
|
||||
if (
|
||||
app.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
if (app.project.organizationId !== ctx.session.activeOrganizationId) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to deploy this application",
|
||||
@@ -813,8 +777,7 @@ export const applicationRouter = createTRPCRouter({
|
||||
const application = await findApplicationById(input.applicationId);
|
||||
|
||||
if (
|
||||
application.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
application.project.organizationId !== ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
@@ -850,14 +813,13 @@ export const applicationRouter = createTRPCRouter({
|
||||
.input(
|
||||
z.object({
|
||||
applicationId: z.string(),
|
||||
targetEnvironmentId: z.string(),
|
||||
targetProjectId: z.string(),
|
||||
}),
|
||||
)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const application = await findApplicationById(input.applicationId);
|
||||
if (
|
||||
application.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
application.project.organizationId !== ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
@@ -865,16 +827,11 @@ export const applicationRouter = createTRPCRouter({
|
||||
});
|
||||
}
|
||||
|
||||
const targetEnvironment = await findEnvironmentById(
|
||||
input.targetEnvironmentId,
|
||||
);
|
||||
if (
|
||||
targetEnvironment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
const targetProject = await findProjectById(input.targetProjectId);
|
||||
if (targetProject.organizationId !== ctx.session.activeOrganizationId) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to move to this environment",
|
||||
message: "You are not authorized to move to this project",
|
||||
});
|
||||
}
|
||||
|
||||
@@ -882,7 +839,7 @@ export const applicationRouter = createTRPCRouter({
|
||||
const updatedApplication = await db
|
||||
.update(applications)
|
||||
.set({
|
||||
environmentId: input.targetEnvironmentId,
|
||||
projectId: input.targetProjectId,
|
||||
})
|
||||
.where(eq(applications.applicationId, input.applicationId))
|
||||
.returning()
|
||||
@@ -897,55 +854,4 @@ export const applicationRouter = createTRPCRouter({
|
||||
|
||||
return updatedApplication;
|
||||
}),
|
||||
|
||||
cancelDeployment: protectedProcedure
|
||||
.input(apiFindOneApplication)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const application = await findApplicationById(input.applicationId);
|
||||
if (
|
||||
application.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to cancel this deployment",
|
||||
});
|
||||
}
|
||||
|
||||
if (IS_CLOUD && application.serverId) {
|
||||
try {
|
||||
await updateApplicationStatus(input.applicationId, "idle");
|
||||
|
||||
if (application.deployments[0]) {
|
||||
await updateDeploymentStatus(
|
||||
application.deployments[0].deploymentId,
|
||||
"done",
|
||||
);
|
||||
}
|
||||
|
||||
await cancelDeployment({
|
||||
applicationId: input.applicationId,
|
||||
applicationType: "application",
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: "Deployment cancellation requested",
|
||||
};
|
||||
} catch (error) {
|
||||
throw new TRPCError({
|
||||
code: "INTERNAL_SERVER_ERROR",
|
||||
message:
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: "Failed to cancel deployment",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Deployment cancellation only available in cloud version",
|
||||
});
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -12,7 +12,6 @@ import {
|
||||
deleteMount,
|
||||
findComposeById,
|
||||
findDomainsByComposeId,
|
||||
findEnvironmentById,
|
||||
findGitProviderById,
|
||||
findProjectById,
|
||||
findServerById,
|
||||
@@ -29,7 +28,6 @@ import {
|
||||
startCompose,
|
||||
stopCompose,
|
||||
updateCompose,
|
||||
updateDeploymentStatus,
|
||||
} from "@dokploy/server";
|
||||
import {
|
||||
type CompleteTemplate,
|
||||
@@ -49,17 +47,15 @@ import { db } from "@/server/db";
|
||||
import {
|
||||
apiCreateCompose,
|
||||
apiDeleteCompose,
|
||||
apiDeployCompose,
|
||||
apiFetchServices,
|
||||
apiFindCompose,
|
||||
apiRandomizeCompose,
|
||||
apiRedeployCompose,
|
||||
apiUpdateCompose,
|
||||
compose as composeTable,
|
||||
} from "@/server/db/schema";
|
||||
import type { DeploymentJob } from "@/server/queues/queue-types";
|
||||
import { cleanQueuesByCompose, myQueue } from "@/server/queues/queueSetup";
|
||||
import { cancelDeployment, deploy } from "@/server/utils/deploy";
|
||||
import { deploy } from "@/server/utils/deploy";
|
||||
import { generatePassword } from "@/templates/utils";
|
||||
import { createTRPCRouter, protectedProcedure, publicProcedure } from "../trpc";
|
||||
|
||||
@@ -68,14 +64,10 @@ export const composeRouter = createTRPCRouter({
|
||||
.input(apiCreateCompose)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
try {
|
||||
// Get project from environment
|
||||
const environment = await findEnvironmentById(input.environmentId);
|
||||
const project = await findProjectById(environment.projectId);
|
||||
|
||||
if (ctx.user.role === "member") {
|
||||
await checkServiceAccess(
|
||||
ctx.user.id,
|
||||
project.projectId,
|
||||
input.projectId,
|
||||
ctx.session.activeOrganizationId,
|
||||
"create",
|
||||
);
|
||||
@@ -87,15 +79,14 @@ export const composeRouter = createTRPCRouter({
|
||||
message: "You need to use a server to create a compose",
|
||||
});
|
||||
}
|
||||
const project = await findProjectById(input.projectId);
|
||||
if (project.organizationId !== ctx.session.activeOrganizationId) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to access this project",
|
||||
});
|
||||
}
|
||||
const newService = await createCompose({
|
||||
...input,
|
||||
});
|
||||
const newService = await createCompose(input);
|
||||
|
||||
if (ctx.user.role === "member") {
|
||||
await addNewService(
|
||||
@@ -124,10 +115,7 @@ export const composeRouter = createTRPCRouter({
|
||||
}
|
||||
|
||||
const compose = await findComposeById(input.composeId);
|
||||
if (
|
||||
compose.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
if (compose.project.organizationId !== ctx.session.activeOrganizationId) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to access this compose",
|
||||
@@ -178,10 +166,7 @@ export const composeRouter = createTRPCRouter({
|
||||
.input(apiUpdateCompose)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const compose = await findComposeById(input.composeId);
|
||||
if (
|
||||
compose.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
if (compose.project.organizationId !== ctx.session.activeOrganizationId) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to update this compose",
|
||||
@@ -203,7 +188,7 @@ export const composeRouter = createTRPCRouter({
|
||||
const composeResult = await findComposeById(input.composeId);
|
||||
|
||||
if (
|
||||
composeResult.environment.project.organizationId !==
|
||||
composeResult.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
@@ -211,6 +196,7 @@ export const composeRouter = createTRPCRouter({
|
||||
message: "You are not authorized to delete this compose",
|
||||
});
|
||||
}
|
||||
4;
|
||||
|
||||
const result = await db
|
||||
.delete(composeTable)
|
||||
@@ -229,16 +215,13 @@ export const composeRouter = createTRPCRouter({
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
return composeResult;
|
||||
return result[0];
|
||||
}),
|
||||
cleanQueues: protectedProcedure
|
||||
.input(apiFindCompose)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const compose = await findComposeById(input.composeId);
|
||||
if (
|
||||
compose.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
if (compose.project.organizationId !== ctx.session.activeOrganizationId) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to clean this compose",
|
||||
@@ -251,10 +234,7 @@ export const composeRouter = createTRPCRouter({
|
||||
.input(apiFetchServices)
|
||||
.query(async ({ input, ctx }) => {
|
||||
const compose = await findComposeById(input.composeId);
|
||||
if (
|
||||
compose.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
if (compose.project.organizationId !== ctx.session.activeOrganizationId) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to load this compose",
|
||||
@@ -271,10 +251,7 @@ export const composeRouter = createTRPCRouter({
|
||||
)
|
||||
.query(async ({ input, ctx }) => {
|
||||
const compose = await findComposeById(input.composeId);
|
||||
if (
|
||||
compose.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
if (compose.project.organizationId !== ctx.session.activeOrganizationId) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to load this compose",
|
||||
@@ -293,8 +270,7 @@ export const composeRouter = createTRPCRouter({
|
||||
const compose = await findComposeById(input.composeId);
|
||||
|
||||
if (
|
||||
compose.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
compose.project.organizationId !== ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
@@ -320,10 +296,7 @@ export const composeRouter = createTRPCRouter({
|
||||
.input(apiRandomizeCompose)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const compose = await findComposeById(input.composeId);
|
||||
if (
|
||||
compose.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
if (compose.project.organizationId !== ctx.session.activeOrganizationId) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to randomize this compose",
|
||||
@@ -335,10 +308,7 @@ export const composeRouter = createTRPCRouter({
|
||||
.input(apiRandomizeCompose)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const compose = await findComposeById(input.composeId);
|
||||
if (
|
||||
compose.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
if (compose.project.organizationId !== ctx.session.activeOrganizationId) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to randomize this compose",
|
||||
@@ -353,10 +323,7 @@ export const composeRouter = createTRPCRouter({
|
||||
.input(apiFindCompose)
|
||||
.query(async ({ input, ctx }) => {
|
||||
const compose = await findComposeById(input.composeId);
|
||||
if (
|
||||
compose.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
if (compose.project.organizationId !== ctx.session.activeOrganizationId) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to get this compose",
|
||||
@@ -370,14 +337,11 @@ export const composeRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
deploy: protectedProcedure
|
||||
.input(apiDeployCompose)
|
||||
.input(apiFindCompose)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const compose = await findComposeById(input.composeId);
|
||||
|
||||
if (
|
||||
compose.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
if (compose.project.organizationId !== ctx.session.activeOrganizationId) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to deploy this compose",
|
||||
@@ -385,10 +349,10 @@ export const composeRouter = createTRPCRouter({
|
||||
}
|
||||
const jobData: DeploymentJob = {
|
||||
composeId: input.composeId,
|
||||
titleLog: input.title || "Manual deployment",
|
||||
titleLog: "Manual deployment",
|
||||
type: "deploy",
|
||||
applicationType: "compose",
|
||||
descriptionLog: input.description || "",
|
||||
descriptionLog: "",
|
||||
server: !!compose.serverId,
|
||||
};
|
||||
|
||||
@@ -407,13 +371,10 @@ export const composeRouter = createTRPCRouter({
|
||||
);
|
||||
}),
|
||||
redeploy: protectedProcedure
|
||||
.input(apiRedeployCompose)
|
||||
.input(apiFindCompose)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const compose = await findComposeById(input.composeId);
|
||||
if (
|
||||
compose.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
if (compose.project.organizationId !== ctx.session.activeOrganizationId) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to redeploy this compose",
|
||||
@@ -421,10 +382,10 @@ export const composeRouter = createTRPCRouter({
|
||||
}
|
||||
const jobData: DeploymentJob = {
|
||||
composeId: input.composeId,
|
||||
titleLog: input.title || "Rebuild deployment",
|
||||
titleLog: "Rebuild deployment",
|
||||
type: "redeploy",
|
||||
applicationType: "compose",
|
||||
descriptionLog: input.description || "",
|
||||
descriptionLog: "",
|
||||
server: !!compose.serverId,
|
||||
};
|
||||
if (IS_CLOUD && compose.serverId) {
|
||||
@@ -445,10 +406,7 @@ export const composeRouter = createTRPCRouter({
|
||||
.input(apiFindCompose)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const compose = await findComposeById(input.composeId);
|
||||
if (
|
||||
compose.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
if (compose.project.organizationId !== ctx.session.activeOrganizationId) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to stop this compose",
|
||||
@@ -462,10 +420,7 @@ export const composeRouter = createTRPCRouter({
|
||||
.input(apiFindCompose)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const compose = await findComposeById(input.composeId);
|
||||
if (
|
||||
compose.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
if (compose.project.organizationId !== ctx.session.activeOrganizationId) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to stop this compose",
|
||||
@@ -480,10 +435,7 @@ export const composeRouter = createTRPCRouter({
|
||||
.query(async ({ input, ctx }) => {
|
||||
const compose = await findComposeById(input.composeId);
|
||||
|
||||
if (
|
||||
compose.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
if (compose.project.organizationId !== ctx.session.activeOrganizationId) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to get this compose",
|
||||
@@ -496,10 +448,7 @@ export const composeRouter = createTRPCRouter({
|
||||
.input(apiFindCompose)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const compose = await findComposeById(input.composeId);
|
||||
if (
|
||||
compose.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
if (compose.project.organizationId !== ctx.session.activeOrganizationId) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to refresh this compose",
|
||||
@@ -513,19 +462,17 @@ export const composeRouter = createTRPCRouter({
|
||||
deployTemplate: protectedProcedure
|
||||
.input(
|
||||
z.object({
|
||||
environmentId: z.string(),
|
||||
projectId: z.string(),
|
||||
serverId: z.string().optional(),
|
||||
id: z.string(),
|
||||
baseUrl: z.string().optional(),
|
||||
}),
|
||||
)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const environment = await findEnvironmentById(input.environmentId);
|
||||
|
||||
if (ctx.user.role === "member") {
|
||||
await checkServiceAccess(
|
||||
ctx.user.id,
|
||||
environment.projectId,
|
||||
input.projectId,
|
||||
ctx.session.activeOrganizationId,
|
||||
"create",
|
||||
);
|
||||
@@ -543,7 +490,7 @@ export const composeRouter = createTRPCRouter({
|
||||
const admin = await findUserById(ctx.user.ownerId);
|
||||
let serverIp = admin.serverIp || "127.0.0.1";
|
||||
|
||||
const project = await findProjectById(environment.projectId);
|
||||
const project = await findProjectById(input.projectId);
|
||||
|
||||
if (input.serverId) {
|
||||
const server = await findServerById(input.serverId);
|
||||
@@ -644,10 +591,7 @@ export const composeRouter = createTRPCRouter({
|
||||
.input(apiFindCompose)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const compose = await findComposeById(input.composeId);
|
||||
if (
|
||||
compose.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
if (compose.project.organizationId !== ctx.session.activeOrganizationId) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to disconnect this git provider",
|
||||
@@ -703,38 +647,30 @@ export const composeRouter = createTRPCRouter({
|
||||
.input(
|
||||
z.object({
|
||||
composeId: z.string(),
|
||||
targetEnvironmentId: z.string(),
|
||||
targetProjectId: z.string(),
|
||||
}),
|
||||
)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const compose = await findComposeById(input.composeId);
|
||||
if (
|
||||
compose.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
if (compose.project.organizationId !== ctx.session.activeOrganizationId) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to move this compose",
|
||||
});
|
||||
}
|
||||
|
||||
const targetEnvironment = await findEnvironmentById(
|
||||
input.targetEnvironmentId,
|
||||
);
|
||||
if (
|
||||
targetEnvironment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
const targetProject = await findProjectById(input.targetProjectId);
|
||||
if (targetProject.organizationId !== ctx.session.activeOrganizationId) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to move to this environment",
|
||||
message: "You are not authorized to move to this project",
|
||||
});
|
||||
}
|
||||
|
||||
const updatedCompose = await db
|
||||
.update(composeTable)
|
||||
.set({
|
||||
environmentId: input.targetEnvironmentId,
|
||||
projectId: input.targetProjectId,
|
||||
})
|
||||
.where(eq(composeTable.composeId, input.composeId))
|
||||
.returning()
|
||||
@@ -762,8 +698,7 @@ export const composeRouter = createTRPCRouter({
|
||||
const compose = await findComposeById(input.composeId);
|
||||
|
||||
if (
|
||||
compose.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
compose.project.organizationId !== ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
@@ -834,8 +769,7 @@ export const composeRouter = createTRPCRouter({
|
||||
);
|
||||
|
||||
if (
|
||||
compose.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
compose.project.organizationId !== ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
@@ -929,57 +863,4 @@ export const composeRouter = createTRPCRouter({
|
||||
});
|
||||
}
|
||||
}),
|
||||
|
||||
cancelDeployment: protectedProcedure
|
||||
.input(apiFindCompose)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const compose = await findComposeById(input.composeId);
|
||||
if (
|
||||
compose.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to cancel this deployment",
|
||||
});
|
||||
}
|
||||
|
||||
if (IS_CLOUD && compose.serverId) {
|
||||
try {
|
||||
await updateCompose(input.composeId, {
|
||||
composeStatus: "idle",
|
||||
});
|
||||
|
||||
if (compose.deployments[0]) {
|
||||
await updateDeploymentStatus(
|
||||
compose.deployments[0].deploymentId,
|
||||
"done",
|
||||
);
|
||||
}
|
||||
|
||||
await cancelDeployment({
|
||||
composeId: input.composeId,
|
||||
applicationType: "compose",
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: "Deployment cancellation requested",
|
||||
};
|
||||
} catch (error) {
|
||||
throw new TRPCError({
|
||||
code: "INTERNAL_SERVER_ERROR",
|
||||
message:
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: "Failed to cancel deployment",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Deployment cancellation only available in cloud version",
|
||||
});
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -29,8 +29,7 @@ export const deploymentRouter = createTRPCRouter({
|
||||
.query(async ({ input, ctx }) => {
|
||||
const application = await findApplicationById(input.applicationId);
|
||||
if (
|
||||
application.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
application.project.organizationId !== ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
@@ -44,10 +43,7 @@ export const deploymentRouter = createTRPCRouter({
|
||||
.input(apiFindAllByCompose)
|
||||
.query(async ({ input, ctx }) => {
|
||||
const compose = await findComposeById(input.composeId);
|
||||
if (
|
||||
compose.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
if (compose.project.organizationId !== ctx.session.activeOrganizationId) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to access this compose",
|
||||
|
||||
@@ -34,8 +34,7 @@ export const domainRouter = createTRPCRouter({
|
||||
if (input.domainType === "compose" && input.composeId) {
|
||||
const compose = await findComposeById(input.composeId);
|
||||
if (
|
||||
compose.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
compose.project.organizationId !== ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
@@ -45,7 +44,7 @@ export const domainRouter = createTRPCRouter({
|
||||
} else if (input.domainType === "application" && input.applicationId) {
|
||||
const application = await findApplicationById(input.applicationId);
|
||||
if (
|
||||
application.environment.project.organizationId !==
|
||||
application.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
@@ -71,8 +70,7 @@ export const domainRouter = createTRPCRouter({
|
||||
.query(async ({ input, ctx }) => {
|
||||
const application = await findApplicationById(input.applicationId);
|
||||
if (
|
||||
application.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
application.project.organizationId !== ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
@@ -85,10 +83,7 @@ export const domainRouter = createTRPCRouter({
|
||||
.input(apiFindCompose)
|
||||
.query(async ({ input, ctx }) => {
|
||||
const compose = await findComposeById(input.composeId);
|
||||
if (
|
||||
compose.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
if (compose.project.organizationId !== ctx.session.activeOrganizationId) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to access this compose",
|
||||
@@ -127,8 +122,7 @@ export const domainRouter = createTRPCRouter({
|
||||
if (currentDomain.applicationId) {
|
||||
const newApp = await findApplicationById(currentDomain.applicationId);
|
||||
if (
|
||||
newApp.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
newApp.project.organizationId !== ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
@@ -138,8 +132,7 @@ export const domainRouter = createTRPCRouter({
|
||||
} else if (currentDomain.composeId) {
|
||||
const newCompose = await findComposeById(currentDomain.composeId);
|
||||
if (
|
||||
newCompose.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
newCompose.project.organizationId !== ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
@@ -151,8 +144,8 @@ export const domainRouter = createTRPCRouter({
|
||||
currentDomain.previewDeploymentId,
|
||||
);
|
||||
if (
|
||||
newPreviewDeployment.application.environment.project
|
||||
.organizationId !== ctx.session.activeOrganizationId
|
||||
newPreviewDeployment.application.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
@@ -182,8 +175,7 @@ export const domainRouter = createTRPCRouter({
|
||||
if (domain.applicationId) {
|
||||
const application = await findApplicationById(domain.applicationId);
|
||||
if (
|
||||
application.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
application.project.organizationId !== ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
@@ -192,10 +184,7 @@ export const domainRouter = createTRPCRouter({
|
||||
}
|
||||
} else if (domain.composeId) {
|
||||
const compose = await findComposeById(domain.composeId);
|
||||
if (
|
||||
compose.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
if (compose.project.organizationId !== ctx.session.activeOrganizationId) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to access this compose",
|
||||
@@ -211,7 +200,7 @@ export const domainRouter = createTRPCRouter({
|
||||
if (domain.applicationId) {
|
||||
const application = await findApplicationById(domain.applicationId);
|
||||
if (
|
||||
application.environment.project.organizationId !==
|
||||
application.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
@@ -222,8 +211,7 @@ export const domainRouter = createTRPCRouter({
|
||||
} else if (domain.composeId) {
|
||||
const compose = await findComposeById(domain.composeId);
|
||||
if (
|
||||
compose.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
compose.project.organizationId !== ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
|
||||
@@ -1,343 +0,0 @@
|
||||
import {
|
||||
addNewEnvironment,
|
||||
checkEnvironmentAccess,
|
||||
createEnvironment,
|
||||
deleteEnvironment,
|
||||
duplicateEnvironment,
|
||||
findEnvironmentById,
|
||||
findEnvironmentsByProjectId,
|
||||
findMemberById,
|
||||
updateEnvironmentById,
|
||||
} from "@dokploy/server";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { z } from "zod";
|
||||
import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc";
|
||||
import {
|
||||
apiCreateEnvironment,
|
||||
apiDuplicateEnvironment,
|
||||
apiFindOneEnvironment,
|
||||
apiRemoveEnvironment,
|
||||
apiUpdateEnvironment,
|
||||
} from "@/server/db/schema";
|
||||
|
||||
// Helper function to filter services within an environment based on user permissions
|
||||
const filterEnvironmentServices = (
|
||||
environment: any,
|
||||
accessedServices: string[],
|
||||
) => ({
|
||||
...environment,
|
||||
applications: environment.applications.filter((app: any) =>
|
||||
accessedServices.includes(app.applicationId),
|
||||
),
|
||||
mariadb: environment.mariadb.filter((db: any) =>
|
||||
accessedServices.includes(db.mariadbId),
|
||||
),
|
||||
mongo: environment.mongo.filter((db: any) =>
|
||||
accessedServices.includes(db.mongoId),
|
||||
),
|
||||
mysql: environment.mysql.filter((db: any) =>
|
||||
accessedServices.includes(db.mysqlId),
|
||||
),
|
||||
postgres: environment.postgres.filter((db: any) =>
|
||||
accessedServices.includes(db.postgresId),
|
||||
),
|
||||
redis: environment.redis.filter((db: any) =>
|
||||
accessedServices.includes(db.redisId),
|
||||
),
|
||||
compose: environment.compose.filter((comp: any) =>
|
||||
accessedServices.includes(comp.composeId),
|
||||
),
|
||||
});
|
||||
|
||||
export const environmentRouter = createTRPCRouter({
|
||||
create: protectedProcedure
|
||||
.input(apiCreateEnvironment)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
// Check if user has access to the project
|
||||
// This would typically involve checking project ownership/membership
|
||||
// For now, we'll use a basic organization check
|
||||
|
||||
if (input.name === "production") {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Environment name cannot be production",
|
||||
});
|
||||
}
|
||||
|
||||
const environment = await createEnvironment(input);
|
||||
|
||||
if (ctx.user.role === "member") {
|
||||
await addNewEnvironment(
|
||||
ctx.user.id,
|
||||
environment.environmentId,
|
||||
ctx.session.activeOrganizationId,
|
||||
);
|
||||
}
|
||||
return environment;
|
||||
} catch (error) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: `Error creating the environment: ${error instanceof Error ? error.message : error}`,
|
||||
cause: error,
|
||||
});
|
||||
}
|
||||
}),
|
||||
|
||||
one: protectedProcedure
|
||||
.input(apiFindOneEnvironment)
|
||||
.query(async ({ input, ctx }) => {
|
||||
try {
|
||||
if (ctx.user.role === "member") {
|
||||
await checkEnvironmentAccess(
|
||||
ctx.user.id,
|
||||
input.environmentId,
|
||||
ctx.session.activeOrganizationId,
|
||||
"access",
|
||||
);
|
||||
}
|
||||
const environment = await findEnvironmentById(input.environmentId);
|
||||
if (
|
||||
environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "FORBIDDEN",
|
||||
message: "You are not allowed to access this environment",
|
||||
});
|
||||
}
|
||||
|
||||
// Check environment access and filter services for members
|
||||
if (ctx.user.role === "member") {
|
||||
const { accessedEnvironments, accessedServices } =
|
||||
await findMemberById(ctx.user.id, ctx.session.activeOrganizationId);
|
||||
|
||||
if (!accessedEnvironments.includes(environment.environmentId)) {
|
||||
throw new TRPCError({
|
||||
code: "FORBIDDEN",
|
||||
message: "You are not allowed to access this environment",
|
||||
});
|
||||
}
|
||||
|
||||
// Filter services based on member permissions
|
||||
const filteredEnvironment = filterEnvironmentServices(
|
||||
environment,
|
||||
accessedServices,
|
||||
);
|
||||
|
||||
return filteredEnvironment;
|
||||
}
|
||||
|
||||
return environment;
|
||||
} catch (error) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Environment not found",
|
||||
});
|
||||
}
|
||||
}),
|
||||
|
||||
byProjectId: protectedProcedure
|
||||
.input(z.object({ projectId: z.string() }))
|
||||
.query(async ({ input, ctx }) => {
|
||||
try {
|
||||
const environments = await findEnvironmentsByProjectId(input.projectId);
|
||||
|
||||
// Check organization access
|
||||
if (
|
||||
environments.some(
|
||||
(environment) =>
|
||||
environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId,
|
||||
)
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "FORBIDDEN",
|
||||
message: "You are not allowed to access this environment",
|
||||
});
|
||||
}
|
||||
|
||||
// Filter environments for members based on their permissions
|
||||
if (ctx.user.role === "member") {
|
||||
const { accessedEnvironments, accessedServices } =
|
||||
await findMemberById(ctx.user.id, ctx.session.activeOrganizationId);
|
||||
|
||||
// Filter environments to only show those the member has access to
|
||||
const filteredEnvironments = environments
|
||||
.filter((environment) =>
|
||||
accessedEnvironments.includes(environment.environmentId),
|
||||
)
|
||||
.map((environment) =>
|
||||
filterEnvironmentServices(environment, accessedServices),
|
||||
);
|
||||
|
||||
return filteredEnvironments;
|
||||
}
|
||||
|
||||
return environments;
|
||||
} catch (error) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: `Error fetching environments: ${error instanceof Error ? error.message : error}`,
|
||||
});
|
||||
}
|
||||
}),
|
||||
|
||||
remove: protectedProcedure
|
||||
.input(apiRemoveEnvironment)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
if (ctx.user.role === "member") {
|
||||
await checkEnvironmentAccess(
|
||||
ctx.user.id,
|
||||
input.environmentId,
|
||||
ctx.session.activeOrganizationId,
|
||||
"access",
|
||||
);
|
||||
}
|
||||
const environment = await findEnvironmentById(input.environmentId);
|
||||
if (
|
||||
environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "FORBIDDEN",
|
||||
message: "You are not allowed to access this environment",
|
||||
});
|
||||
}
|
||||
|
||||
// Check environment access for members
|
||||
if (ctx.user.role === "member") {
|
||||
const { accessedEnvironments } = await findMemberById(
|
||||
ctx.user.id,
|
||||
ctx.session.activeOrganizationId,
|
||||
);
|
||||
|
||||
if (!accessedEnvironments.includes(environment.environmentId)) {
|
||||
throw new TRPCError({
|
||||
code: "FORBIDDEN",
|
||||
message: "You are not allowed to delete this environment",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const deletedEnvironment = await deleteEnvironment(input.environmentId);
|
||||
return deletedEnvironment;
|
||||
} catch (error) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: `Error deleting the environment: ${error instanceof Error ? error.message : error}`,
|
||||
});
|
||||
}
|
||||
}),
|
||||
|
||||
update: protectedProcedure
|
||||
.input(apiUpdateEnvironment)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
const { environmentId, ...updateData } = input;
|
||||
|
||||
if (updateData.name === "production") {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Environment name cannot be production",
|
||||
});
|
||||
}
|
||||
|
||||
if (ctx.user.role === "member") {
|
||||
await checkEnvironmentAccess(
|
||||
ctx.user.id,
|
||||
environmentId,
|
||||
ctx.session.activeOrganizationId,
|
||||
"access",
|
||||
);
|
||||
}
|
||||
const currentEnvironment = await findEnvironmentById(environmentId);
|
||||
if (
|
||||
currentEnvironment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "FORBIDDEN",
|
||||
message: "You are not allowed to access this environment",
|
||||
});
|
||||
}
|
||||
|
||||
// Check environment access for members
|
||||
if (ctx.user.role === "member") {
|
||||
const { accessedEnvironments } = await findMemberById(
|
||||
ctx.user.id,
|
||||
ctx.session.activeOrganizationId,
|
||||
);
|
||||
|
||||
if (
|
||||
!accessedEnvironments.includes(currentEnvironment.environmentId)
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "FORBIDDEN",
|
||||
message: "You are not allowed to update this environment",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const environment = await updateEnvironmentById(
|
||||
environmentId,
|
||||
updateData,
|
||||
);
|
||||
return environment;
|
||||
} catch (error) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: `Error updating the environment: ${error instanceof Error ? error.message : error}`,
|
||||
});
|
||||
}
|
||||
}),
|
||||
|
||||
duplicate: protectedProcedure
|
||||
.input(apiDuplicateEnvironment)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
if (ctx.user.role === "member") {
|
||||
await checkEnvironmentAccess(
|
||||
ctx.user.id,
|
||||
input.environmentId,
|
||||
ctx.session.activeOrganizationId,
|
||||
"access",
|
||||
);
|
||||
}
|
||||
const environment = await findEnvironmentById(input.environmentId);
|
||||
if (
|
||||
environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "FORBIDDEN",
|
||||
message: "You are not allowed to access this environment",
|
||||
});
|
||||
}
|
||||
|
||||
// Check environment access for members
|
||||
if (ctx.user.role === "member") {
|
||||
const { accessedEnvironments } = await findMemberById(
|
||||
ctx.user.id,
|
||||
ctx.session.activeOrganizationId,
|
||||
);
|
||||
|
||||
if (!accessedEnvironments.includes(environment.environmentId)) {
|
||||
throw new TRPCError({
|
||||
code: "FORBIDDEN",
|
||||
message: "You are not allowed to duplicate this environment",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const duplicatedEnvironment = await duplicateEnvironment(input);
|
||||
return duplicatedEnvironment;
|
||||
} catch (error) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: `Error duplicating the environment: ${error instanceof Error ? error.message : error}`,
|
||||
});
|
||||
}
|
||||
}),
|
||||
});
|
||||
@@ -6,7 +6,6 @@ import {
|
||||
deployMariadb,
|
||||
findBackupsByDbId,
|
||||
findMariadbById,
|
||||
findEnvironmentById,
|
||||
findProjectById,
|
||||
IS_CLOUD,
|
||||
rebuildDatabase,
|
||||
@@ -42,14 +41,10 @@ export const mariadbRouter = createTRPCRouter({
|
||||
.input(apiCreateMariaDB)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
// Get project from environment
|
||||
const environment = await findEnvironmentById(input.environmentId);
|
||||
const project = await findProjectById(environment.projectId);
|
||||
|
||||
if (ctx.user.role === "member") {
|
||||
await checkServiceAccess(
|
||||
ctx.user.id,
|
||||
project.projectId,
|
||||
input.projectId,
|
||||
ctx.session.activeOrganizationId,
|
||||
"create",
|
||||
);
|
||||
@@ -62,15 +57,14 @@ export const mariadbRouter = createTRPCRouter({
|
||||
});
|
||||
}
|
||||
|
||||
const project = await findProjectById(input.projectId);
|
||||
if (project.organizationId !== ctx.session.activeOrganizationId) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to access this project",
|
||||
});
|
||||
}
|
||||
const newMariadb = await createMariadb({
|
||||
...input,
|
||||
});
|
||||
const newMariadb = await createMariadb(input);
|
||||
if (ctx.user.role === "member") {
|
||||
await addNewService(
|
||||
ctx.user.id,
|
||||
@@ -107,10 +101,7 @@ export const mariadbRouter = createTRPCRouter({
|
||||
);
|
||||
}
|
||||
const mariadb = await findMariadbById(input.mariadbId);
|
||||
if (
|
||||
mariadb.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
if (mariadb.project.organizationId !== ctx.session.activeOrganizationId) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to access this Mariadb",
|
||||
@@ -123,10 +114,7 @@ export const mariadbRouter = createTRPCRouter({
|
||||
.input(apiFindOneMariaDB)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const service = await findMariadbById(input.mariadbId);
|
||||
if (
|
||||
service.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
if (service.project.organizationId !== ctx.session.activeOrganizationId) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to start this Mariadb",
|
||||
@@ -163,10 +151,7 @@ export const mariadbRouter = createTRPCRouter({
|
||||
.input(apiSaveExternalPortMariaDB)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const mongo = await findMariadbById(input.mariadbId);
|
||||
if (
|
||||
mongo.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
if (mongo.project.organizationId !== ctx.session.activeOrganizationId) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to save this external port",
|
||||
@@ -182,10 +167,7 @@ export const mariadbRouter = createTRPCRouter({
|
||||
.input(apiDeployMariaDB)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const mariadb = await findMariadbById(input.mariadbId);
|
||||
if (
|
||||
mariadb.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
if (mariadb.project.organizationId !== ctx.session.activeOrganizationId) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to deploy this Mariadb",
|
||||
@@ -206,10 +188,7 @@ export const mariadbRouter = createTRPCRouter({
|
||||
.input(apiDeployMariaDB)
|
||||
.subscription(async ({ input, ctx }) => {
|
||||
const mariadb = await findMariadbById(input.mariadbId);
|
||||
if (
|
||||
mariadb.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
if (mariadb.project.organizationId !== ctx.session.activeOrganizationId) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to deploy this Mariadb",
|
||||
@@ -226,10 +205,7 @@ export const mariadbRouter = createTRPCRouter({
|
||||
.input(apiChangeMariaDBStatus)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const mongo = await findMariadbById(input.mariadbId);
|
||||
if (
|
||||
mongo.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
if (mongo.project.organizationId !== ctx.session.activeOrganizationId) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to change this Mariadb status",
|
||||
@@ -253,10 +229,7 @@ export const mariadbRouter = createTRPCRouter({
|
||||
}
|
||||
|
||||
const mongo = await findMariadbById(input.mariadbId);
|
||||
if (
|
||||
mongo.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
if (mongo.project.organizationId !== ctx.session.activeOrganizationId) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to delete this Mariadb",
|
||||
@@ -282,10 +255,7 @@ export const mariadbRouter = createTRPCRouter({
|
||||
.input(apiSaveEnvironmentVariablesMariaDB)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const mariadb = await findMariadbById(input.mariadbId);
|
||||
if (
|
||||
mariadb.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
if (mariadb.project.organizationId !== ctx.session.activeOrganizationId) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to save this environment",
|
||||
@@ -308,10 +278,7 @@ export const mariadbRouter = createTRPCRouter({
|
||||
.input(apiResetMariadb)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const mariadb = await findMariadbById(input.mariadbId);
|
||||
if (
|
||||
mariadb.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
if (mariadb.project.organizationId !== ctx.session.activeOrganizationId) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to reload this Mariadb",
|
||||
@@ -341,10 +308,7 @@ export const mariadbRouter = createTRPCRouter({
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const { mariadbId, ...rest } = input;
|
||||
const mariadb = await findMariadbById(mariadbId);
|
||||
if (
|
||||
mariadb.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
if (mariadb.project.organizationId !== ctx.session.activeOrganizationId) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to update this Mariadb",
|
||||
@@ -367,31 +331,23 @@ export const mariadbRouter = createTRPCRouter({
|
||||
.input(
|
||||
z.object({
|
||||
mariadbId: z.string(),
|
||||
targetEnvironmentId: z.string(),
|
||||
targetProjectId: z.string(),
|
||||
}),
|
||||
)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const mariadb = await findMariadbById(input.mariadbId);
|
||||
if (
|
||||
mariadb.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
if (mariadb.project.organizationId !== ctx.session.activeOrganizationId) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to move this mariadb",
|
||||
});
|
||||
}
|
||||
|
||||
const targetEnvironment = await findEnvironmentById(
|
||||
input.targetEnvironmentId,
|
||||
);
|
||||
if (
|
||||
targetEnvironment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
const targetProject = await findProjectById(input.targetProjectId);
|
||||
if (targetProject.organizationId !== ctx.session.activeOrganizationId) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to move to this environment",
|
||||
message: "You are not authorized to move to this project",
|
||||
});
|
||||
}
|
||||
|
||||
@@ -399,7 +355,7 @@ export const mariadbRouter = createTRPCRouter({
|
||||
const updatedMariadb = await db
|
||||
.update(mariadbTable)
|
||||
.set({
|
||||
environmentId: input.targetEnvironmentId,
|
||||
projectId: input.targetProjectId,
|
||||
})
|
||||
.where(eq(mariadbTable.mariadbId, input.mariadbId))
|
||||
.returning()
|
||||
@@ -418,10 +374,7 @@ export const mariadbRouter = createTRPCRouter({
|
||||
.input(apiRebuildMariadb)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const mariadb = await findMariadbById(input.mariadbId);
|
||||
if (
|
||||
mariadb.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
if (mariadb.project.organizationId !== ctx.session.activeOrganizationId) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to rebuild this MariaDB database",
|
||||
|
||||
@@ -6,7 +6,6 @@ import {
|
||||
deployMongo,
|
||||
findBackupsByDbId,
|
||||
findMongoById,
|
||||
findEnvironmentById,
|
||||
findProjectById,
|
||||
IS_CLOUD,
|
||||
rebuildDatabase,
|
||||
@@ -42,14 +41,10 @@ export const mongoRouter = createTRPCRouter({
|
||||
.input(apiCreateMongo)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
// Get project from environment
|
||||
const environment = await findEnvironmentById(input.environmentId);
|
||||
const project = await findProjectById(environment.projectId);
|
||||
|
||||
if (ctx.user.role === "member") {
|
||||
await checkServiceAccess(
|
||||
ctx.user.id,
|
||||
project.projectId,
|
||||
input.projectId,
|
||||
ctx.session.activeOrganizationId,
|
||||
"create",
|
||||
);
|
||||
@@ -62,15 +57,14 @@ export const mongoRouter = createTRPCRouter({
|
||||
});
|
||||
}
|
||||
|
||||
const project = await findProjectById(input.projectId);
|
||||
if (project.organizationId !== ctx.session.activeOrganizationId) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to access this project",
|
||||
});
|
||||
}
|
||||
const newMongo = await createMongo({
|
||||
...input,
|
||||
});
|
||||
const newMongo = await createMongo(input);
|
||||
if (ctx.user.role === "member") {
|
||||
await addNewService(
|
||||
ctx.user.id,
|
||||
@@ -112,10 +106,7 @@ export const mongoRouter = createTRPCRouter({
|
||||
}
|
||||
|
||||
const mongo = await findMongoById(input.mongoId);
|
||||
if (
|
||||
mongo.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
if (mongo.project.organizationId !== ctx.session.activeOrganizationId) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to access this mongo",
|
||||
@@ -129,10 +120,7 @@ export const mongoRouter = createTRPCRouter({
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const service = await findMongoById(input.mongoId);
|
||||
|
||||
if (
|
||||
service.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
if (service.project.organizationId !== ctx.session.activeOrganizationId) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to start this mongo",
|
||||
@@ -155,10 +143,7 @@ export const mongoRouter = createTRPCRouter({
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const mongo = await findMongoById(input.mongoId);
|
||||
|
||||
if (
|
||||
mongo.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
if (mongo.project.organizationId !== ctx.session.activeOrganizationId) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to stop this mongo",
|
||||
@@ -180,10 +165,7 @@ export const mongoRouter = createTRPCRouter({
|
||||
.input(apiSaveExternalPortMongo)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const mongo = await findMongoById(input.mongoId);
|
||||
if (
|
||||
mongo.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
if (mongo.project.organizationId !== ctx.session.activeOrganizationId) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to save this external port",
|
||||
@@ -199,10 +181,7 @@ export const mongoRouter = createTRPCRouter({
|
||||
.input(apiDeployMongo)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const mongo = await findMongoById(input.mongoId);
|
||||
if (
|
||||
mongo.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
if (mongo.project.organizationId !== ctx.session.activeOrganizationId) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to deploy this mongo",
|
||||
@@ -222,10 +201,7 @@ export const mongoRouter = createTRPCRouter({
|
||||
.input(apiDeployMongo)
|
||||
.subscription(async ({ input, ctx }) => {
|
||||
const mongo = await findMongoById(input.mongoId);
|
||||
if (
|
||||
mongo.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
if (mongo.project.organizationId !== ctx.session.activeOrganizationId) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to deploy this mongo",
|
||||
@@ -242,10 +218,7 @@ export const mongoRouter = createTRPCRouter({
|
||||
.input(apiChangeMongoStatus)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const mongo = await findMongoById(input.mongoId);
|
||||
if (
|
||||
mongo.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
if (mongo.project.organizationId !== ctx.session.activeOrganizationId) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to change this mongo status",
|
||||
@@ -260,10 +233,7 @@ export const mongoRouter = createTRPCRouter({
|
||||
.input(apiResetMongo)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const mongo = await findMongoById(input.mongoId);
|
||||
if (
|
||||
mongo.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
if (mongo.project.organizationId !== ctx.session.activeOrganizationId) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to reload this mongo",
|
||||
@@ -302,10 +272,7 @@ export const mongoRouter = createTRPCRouter({
|
||||
|
||||
const mongo = await findMongoById(input.mongoId);
|
||||
|
||||
if (
|
||||
mongo.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
if (mongo.project.organizationId !== ctx.session.activeOrganizationId) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to delete this mongo",
|
||||
@@ -331,10 +298,7 @@ export const mongoRouter = createTRPCRouter({
|
||||
.input(apiSaveEnvironmentVariablesMongo)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const mongo = await findMongoById(input.mongoId);
|
||||
if (
|
||||
mongo.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
if (mongo.project.organizationId !== ctx.session.activeOrganizationId) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to save this environment",
|
||||
@@ -358,10 +322,7 @@ export const mongoRouter = createTRPCRouter({
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const { mongoId, ...rest } = input;
|
||||
const mongo = await findMongoById(mongoId);
|
||||
if (
|
||||
mongo.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
if (mongo.project.organizationId !== ctx.session.activeOrganizationId) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to update this mongo",
|
||||
@@ -384,31 +345,23 @@ export const mongoRouter = createTRPCRouter({
|
||||
.input(
|
||||
z.object({
|
||||
mongoId: z.string(),
|
||||
targetEnvironmentId: z.string(),
|
||||
targetProjectId: z.string(),
|
||||
}),
|
||||
)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const mongo = await findMongoById(input.mongoId);
|
||||
if (
|
||||
mongo.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
if (mongo.project.organizationId !== ctx.session.activeOrganizationId) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to move this mongo",
|
||||
});
|
||||
}
|
||||
|
||||
const targetEnvironment = await findEnvironmentById(
|
||||
input.targetEnvironmentId,
|
||||
);
|
||||
if (
|
||||
targetEnvironment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
const targetProject = await findProjectById(input.targetProjectId);
|
||||
if (targetProject.organizationId !== ctx.session.activeOrganizationId) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to move to this environment",
|
||||
message: "You are not authorized to move to this project",
|
||||
});
|
||||
}
|
||||
|
||||
@@ -416,7 +369,7 @@ export const mongoRouter = createTRPCRouter({
|
||||
const updatedMongo = await db
|
||||
.update(mongoTable)
|
||||
.set({
|
||||
environmentId: input.targetEnvironmentId,
|
||||
projectId: input.targetProjectId,
|
||||
})
|
||||
.where(eq(mongoTable.mongoId, input.mongoId))
|
||||
.returning()
|
||||
@@ -435,10 +388,7 @@ export const mongoRouter = createTRPCRouter({
|
||||
.input(apiRebuildMongo)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const mongo = await findMongoById(input.mongoId);
|
||||
if (
|
||||
mongo.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
if (mongo.project.organizationId !== ctx.session.activeOrganizationId) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to rebuild this MongoDB database",
|
||||
|
||||
@@ -3,11 +3,9 @@ import {
|
||||
deleteMount,
|
||||
findApplicationById,
|
||||
findMountById,
|
||||
findMountOrganizationId,
|
||||
getServiceContainer,
|
||||
updateMount,
|
||||
} from "@dokploy/server";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { z } from "zod";
|
||||
import {
|
||||
apiCreateMount,
|
||||
@@ -26,39 +24,16 @@ export const mountRouter = createTRPCRouter({
|
||||
}),
|
||||
remove: protectedProcedure
|
||||
.input(apiRemoveMount)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const organizationId = await findMountOrganizationId(input.mountId);
|
||||
if (organizationId !== ctx.session.activeOrganizationId) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to delete this mount",
|
||||
});
|
||||
}
|
||||
.mutation(async ({ input }) => {
|
||||
return await deleteMount(input.mountId);
|
||||
}),
|
||||
|
||||
one: protectedProcedure
|
||||
.input(apiFindOneMount)
|
||||
.query(async ({ input, ctx }) => {
|
||||
const organizationId = await findMountOrganizationId(input.mountId);
|
||||
if (organizationId !== ctx.session.activeOrganizationId) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to access this mount",
|
||||
});
|
||||
}
|
||||
return await findMountById(input.mountId);
|
||||
}),
|
||||
one: protectedProcedure.input(apiFindOneMount).query(async ({ input }) => {
|
||||
return await findMountById(input.mountId);
|
||||
}),
|
||||
update: protectedProcedure
|
||||
.input(apiUpdateMount)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const organizationId = await findMountOrganizationId(input.mountId);
|
||||
if (organizationId !== ctx.session.activeOrganizationId) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to update this mount",
|
||||
});
|
||||
}
|
||||
.mutation(async ({ input }) => {
|
||||
return await updateMount(input.mountId, input);
|
||||
}),
|
||||
allNamedByApplicationId: protectedProcedure
|
||||
|
||||
@@ -5,7 +5,6 @@ import {
|
||||
createMysql,
|
||||
deployMySql,
|
||||
findBackupsByDbId,
|
||||
findEnvironmentById,
|
||||
findMySqlById,
|
||||
findProjectById,
|
||||
IS_CLOUD,
|
||||
@@ -43,14 +42,10 @@ export const mysqlRouter = createTRPCRouter({
|
||||
.input(apiCreateMySql)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
// Get project from environment
|
||||
const environment = await findEnvironmentById(input.environmentId);
|
||||
const project = await findProjectById(environment.projectId);
|
||||
|
||||
if (ctx.user.role === "member") {
|
||||
await checkServiceAccess(
|
||||
ctx.user.id,
|
||||
project.projectId,
|
||||
input.projectId,
|
||||
ctx.session.activeOrganizationId,
|
||||
"create",
|
||||
);
|
||||
@@ -62,7 +57,8 @@ export const mysqlRouter = createTRPCRouter({
|
||||
message: "You need to use a server to create a MySQL",
|
||||
});
|
||||
}
|
||||
|
||||
1;
|
||||
const project = await findProjectById(input.projectId);
|
||||
if (project.organizationId !== ctx.session.activeOrganizationId) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
@@ -70,9 +66,7 @@ export const mysqlRouter = createTRPCRouter({
|
||||
});
|
||||
}
|
||||
|
||||
const newMysql = await createMysql({
|
||||
...input,
|
||||
});
|
||||
const newMysql = await createMysql(input);
|
||||
if (ctx.user.role === "member") {
|
||||
await addNewService(
|
||||
ctx.user.id,
|
||||
@@ -113,10 +107,7 @@ export const mysqlRouter = createTRPCRouter({
|
||||
);
|
||||
}
|
||||
const mysql = await findMySqlById(input.mysqlId);
|
||||
if (
|
||||
mysql.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
if (mysql.project.organizationId !== ctx.session.activeOrganizationId) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to access this MySQL",
|
||||
@@ -129,10 +120,7 @@ export const mysqlRouter = createTRPCRouter({
|
||||
.input(apiFindOneMySql)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const service = await findMySqlById(input.mysqlId);
|
||||
if (
|
||||
service.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
if (service.project.organizationId !== ctx.session.activeOrganizationId) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to start this MySQL",
|
||||
@@ -154,10 +142,7 @@ export const mysqlRouter = createTRPCRouter({
|
||||
.input(apiFindOneMySql)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const mongo = await findMySqlById(input.mysqlId);
|
||||
if (
|
||||
mongo.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
if (mongo.project.organizationId !== ctx.session.activeOrganizationId) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to stop this MySQL",
|
||||
@@ -178,10 +163,7 @@ export const mysqlRouter = createTRPCRouter({
|
||||
.input(apiSaveExternalPortMySql)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const mongo = await findMySqlById(input.mysqlId);
|
||||
if (
|
||||
mongo.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
if (mongo.project.organizationId !== ctx.session.activeOrganizationId) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to save this external port",
|
||||
@@ -197,10 +179,7 @@ export const mysqlRouter = createTRPCRouter({
|
||||
.input(apiDeployMySql)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const mysql = await findMySqlById(input.mysqlId);
|
||||
if (
|
||||
mysql.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
if (mysql.project.organizationId !== ctx.session.activeOrganizationId) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to deploy this MySQL",
|
||||
@@ -220,10 +199,7 @@ export const mysqlRouter = createTRPCRouter({
|
||||
.input(apiDeployMySql)
|
||||
.subscription(async ({ input, ctx }) => {
|
||||
const mysql = await findMySqlById(input.mysqlId);
|
||||
if (
|
||||
mysql.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
if (mysql.project.organizationId !== ctx.session.activeOrganizationId) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to deploy this MySQL",
|
||||
@@ -240,10 +216,7 @@ export const mysqlRouter = createTRPCRouter({
|
||||
.input(apiChangeMySqlStatus)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const mongo = await findMySqlById(input.mysqlId);
|
||||
if (
|
||||
mongo.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
if (mongo.project.organizationId !== ctx.session.activeOrganizationId) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to change this MySQL status",
|
||||
@@ -258,10 +231,7 @@ export const mysqlRouter = createTRPCRouter({
|
||||
.input(apiResetMysql)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const mysql = await findMySqlById(input.mysqlId);
|
||||
if (
|
||||
mysql.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
if (mysql.project.organizationId !== ctx.session.activeOrganizationId) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to reload this MySQL",
|
||||
@@ -297,10 +267,7 @@ export const mysqlRouter = createTRPCRouter({
|
||||
);
|
||||
}
|
||||
const mongo = await findMySqlById(input.mysqlId);
|
||||
if (
|
||||
mongo.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
if (mongo.project.organizationId !== ctx.session.activeOrganizationId) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to delete this MySQL",
|
||||
@@ -326,10 +293,7 @@ export const mysqlRouter = createTRPCRouter({
|
||||
.input(apiSaveEnvironmentVariablesMySql)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const mysql = await findMySqlById(input.mysqlId);
|
||||
if (
|
||||
mysql.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
if (mysql.project.organizationId !== ctx.session.activeOrganizationId) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to save this environment",
|
||||
@@ -353,10 +317,7 @@ export const mysqlRouter = createTRPCRouter({
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const { mysqlId, ...rest } = input;
|
||||
const mysql = await findMySqlById(mysqlId);
|
||||
if (
|
||||
mysql.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
if (mysql.project.organizationId !== ctx.session.activeOrganizationId) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to update this MySQL",
|
||||
@@ -379,31 +340,23 @@ export const mysqlRouter = createTRPCRouter({
|
||||
.input(
|
||||
z.object({
|
||||
mysqlId: z.string(),
|
||||
targetEnvironmentId: z.string(),
|
||||
targetProjectId: z.string(),
|
||||
}),
|
||||
)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const mysql = await findMySqlById(input.mysqlId);
|
||||
if (
|
||||
mysql.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
if (mysql.project.organizationId !== ctx.session.activeOrganizationId) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to move this mysql",
|
||||
});
|
||||
}
|
||||
|
||||
const targetEnvironment = await findEnvironmentById(
|
||||
input.targetEnvironmentId,
|
||||
);
|
||||
if (
|
||||
targetEnvironment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
const targetProject = await findProjectById(input.targetProjectId);
|
||||
if (targetProject.organizationId !== ctx.session.activeOrganizationId) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to move to this environment",
|
||||
message: "You are not authorized to move to this project",
|
||||
});
|
||||
}
|
||||
|
||||
@@ -411,7 +364,7 @@ export const mysqlRouter = createTRPCRouter({
|
||||
const updatedMysql = await db
|
||||
.update(mysqlTable)
|
||||
.set({
|
||||
environmentId: input.targetEnvironmentId,
|
||||
projectId: input.targetProjectId,
|
||||
})
|
||||
.where(eq(mysqlTable.mysqlId, input.mysqlId))
|
||||
.returning()
|
||||
@@ -430,10 +383,7 @@ export const mysqlRouter = createTRPCRouter({
|
||||
.input(apiRebuildMysql)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const mysql = await findMySqlById(input.mysqlId);
|
||||
if (
|
||||
mysql.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
if (mysql.project.organizationId !== ctx.session.activeOrganizationId) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to rebuild this MySQL database",
|
||||
|
||||
@@ -2,7 +2,6 @@ import {
|
||||
createDiscordNotification,
|
||||
createEmailNotification,
|
||||
createGotifyNotification,
|
||||
createNtfyNotification,
|
||||
createSlackNotification,
|
||||
createTelegramNotification,
|
||||
findNotificationById,
|
||||
@@ -11,14 +10,12 @@ import {
|
||||
sendDiscordNotification,
|
||||
sendEmailNotification,
|
||||
sendGotifyNotification,
|
||||
sendNtfyNotification,
|
||||
sendServerThresholdNotifications,
|
||||
sendSlackNotification,
|
||||
sendTelegramNotification,
|
||||
updateDiscordNotification,
|
||||
updateEmailNotification,
|
||||
updateGotifyNotification,
|
||||
updateNtfyNotification,
|
||||
updateSlackNotification,
|
||||
updateTelegramNotification,
|
||||
} from "@dokploy/server";
|
||||
@@ -36,20 +33,17 @@ import {
|
||||
apiCreateDiscord,
|
||||
apiCreateEmail,
|
||||
apiCreateGotify,
|
||||
apiCreateNtfy,
|
||||
apiCreateSlack,
|
||||
apiCreateTelegram,
|
||||
apiFindOneNotification,
|
||||
apiTestDiscordConnection,
|
||||
apiTestEmailConnection,
|
||||
apiTestGotifyConnection,
|
||||
apiTestNtfyConnection,
|
||||
apiTestSlackConnection,
|
||||
apiTestTelegramConnection,
|
||||
apiUpdateDiscord,
|
||||
apiUpdateEmail,
|
||||
apiUpdateGotify,
|
||||
apiUpdateNtfy,
|
||||
apiUpdateSlack,
|
||||
apiUpdateTelegram,
|
||||
notifications,
|
||||
@@ -327,7 +321,6 @@ export const notificationRouter = createTRPCRouter({
|
||||
discord: true,
|
||||
email: true,
|
||||
gotify: true,
|
||||
ntfy: true,
|
||||
},
|
||||
orderBy: desc(notifications.createdAt),
|
||||
where: eq(notifications.organizationId, ctx.session.activeOrganizationId),
|
||||
@@ -453,64 +446,6 @@ export const notificationRouter = createTRPCRouter({
|
||||
});
|
||||
}
|
||||
}),
|
||||
createNtfy: adminProcedure
|
||||
.input(apiCreateNtfy)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
return await createNtfyNotification(
|
||||
input,
|
||||
ctx.session.activeOrganizationId,
|
||||
);
|
||||
} catch (error) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error creating the notification",
|
||||
cause: error,
|
||||
});
|
||||
}
|
||||
}),
|
||||
updateNtfy: adminProcedure
|
||||
.input(apiUpdateNtfy)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
const notification = await findNotificationById(input.notificationId);
|
||||
if (
|
||||
IS_CLOUD &&
|
||||
notification.organizationId !== ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to update this notification",
|
||||
});
|
||||
}
|
||||
return await updateNtfyNotification({
|
||||
...input,
|
||||
organizationId: ctx.session.activeOrganizationId,
|
||||
});
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
}),
|
||||
testNtfyConnection: adminProcedure
|
||||
.input(apiTestNtfyConnection)
|
||||
.mutation(async ({ input }) => {
|
||||
try {
|
||||
await sendNtfyNotification(
|
||||
input,
|
||||
"Test Notification",
|
||||
"",
|
||||
"view, visit Dokploy on Github, https://github.com/dokploy/dokploy, clear=true;",
|
||||
"Hi, From Dokploy 👋",
|
||||
);
|
||||
return true;
|
||||
} catch (error) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error testing the notification",
|
||||
cause: error,
|
||||
});
|
||||
}
|
||||
}),
|
||||
getEmailProviders: adminProcedure.query(async ({ ctx }) => {
|
||||
return await db.query.notifications.findMany({
|
||||
where: eq(notifications.organizationId, ctx.session.activeOrganizationId),
|
||||
|
||||
@@ -27,44 +27,22 @@ export const portRouter = createTRPCRouter({
|
||||
});
|
||||
}
|
||||
}),
|
||||
one: protectedProcedure
|
||||
.input(apiFindOnePort)
|
||||
.query(async ({ input, ctx }) => {
|
||||
try {
|
||||
const port = await finPortById(input.portId);
|
||||
if (
|
||||
port.application.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to access this port",
|
||||
});
|
||||
}
|
||||
return port;
|
||||
} catch (error) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Port not found",
|
||||
cause: error,
|
||||
});
|
||||
}
|
||||
}),
|
||||
one: protectedProcedure.input(apiFindOnePort).query(async ({ input }) => {
|
||||
try {
|
||||
return await finPortById(input.portId);
|
||||
} catch (error) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Port not found",
|
||||
cause: error,
|
||||
});
|
||||
}
|
||||
}),
|
||||
delete: protectedProcedure
|
||||
.input(apiFindOnePort)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const port = await finPortById(input.portId);
|
||||
if (
|
||||
port.application.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to delete this port",
|
||||
});
|
||||
}
|
||||
.mutation(async ({ input }) => {
|
||||
try {
|
||||
return await removePortById(input.portId);
|
||||
return removePortById(input.portId);
|
||||
} catch (error) {
|
||||
const message =
|
||||
error instanceof Error ? error.message : "Error input: Deleting port";
|
||||
@@ -76,19 +54,9 @@ export const portRouter = createTRPCRouter({
|
||||
}),
|
||||
update: protectedProcedure
|
||||
.input(apiUpdatePort)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const port = await finPortById(input.portId);
|
||||
if (
|
||||
port.application.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to update this port",
|
||||
});
|
||||
}
|
||||
.mutation(async ({ input }) => {
|
||||
try {
|
||||
return await updatePortById(input.portId, input);
|
||||
return updatePortById(input.portId, input);
|
||||
} catch (error) {
|
||||
const message =
|
||||
error instanceof Error ? error.message : "Error updating the port";
|
||||
|
||||
@@ -5,7 +5,6 @@ import {
|
||||
createPostgres,
|
||||
deployPostgres,
|
||||
findBackupsByDbId,
|
||||
findEnvironmentById,
|
||||
findPostgresById,
|
||||
findProjectById,
|
||||
IS_CLOUD,
|
||||
@@ -42,14 +41,10 @@ export const postgresRouter = createTRPCRouter({
|
||||
.input(apiCreatePostgres)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
// Get project from environment
|
||||
const environment = await findEnvironmentById(input.environmentId);
|
||||
const project = await findProjectById(environment.projectId);
|
||||
|
||||
if (ctx.user.role === "member") {
|
||||
await checkServiceAccess(
|
||||
ctx.user.id,
|
||||
project.projectId,
|
||||
input.projectId,
|
||||
ctx.session.activeOrganizationId,
|
||||
"create",
|
||||
);
|
||||
@@ -62,15 +57,14 @@ export const postgresRouter = createTRPCRouter({
|
||||
});
|
||||
}
|
||||
|
||||
const project = await findProjectById(input.projectId);
|
||||
if (project.organizationId !== ctx.session.activeOrganizationId) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to access this project",
|
||||
});
|
||||
}
|
||||
const newPostgres = await createPostgres({
|
||||
...input,
|
||||
});
|
||||
const newPostgres = await createPostgres(input);
|
||||
if (ctx.user.role === "member") {
|
||||
await addNewService(
|
||||
ctx.user.id,
|
||||
@@ -113,8 +107,7 @@ export const postgresRouter = createTRPCRouter({
|
||||
|
||||
const postgres = await findPostgresById(input.postgresId);
|
||||
if (
|
||||
postgres.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
postgres.project.organizationId !== ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
@@ -129,10 +122,7 @@ export const postgresRouter = createTRPCRouter({
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const service = await findPostgresById(input.postgresId);
|
||||
|
||||
if (
|
||||
service.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
if (service.project.organizationId !== ctx.session.activeOrganizationId) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to start this Postgres",
|
||||
@@ -155,8 +145,7 @@ export const postgresRouter = createTRPCRouter({
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const postgres = await findPostgresById(input.postgresId);
|
||||
if (
|
||||
postgres.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
postgres.project.organizationId !== ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
@@ -180,8 +169,7 @@ export const postgresRouter = createTRPCRouter({
|
||||
const postgres = await findPostgresById(input.postgresId);
|
||||
|
||||
if (
|
||||
postgres.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
postgres.project.organizationId !== ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
@@ -199,8 +187,7 @@ export const postgresRouter = createTRPCRouter({
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const postgres = await findPostgresById(input.postgresId);
|
||||
if (
|
||||
postgres.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
postgres.project.organizationId !== ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
@@ -223,8 +210,7 @@ export const postgresRouter = createTRPCRouter({
|
||||
.subscription(async ({ input, ctx }) => {
|
||||
const postgres = await findPostgresById(input.postgresId);
|
||||
if (
|
||||
postgres.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
postgres.project.organizationId !== ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
@@ -243,8 +229,7 @@ export const postgresRouter = createTRPCRouter({
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const postgres = await findPostgresById(input.postgresId);
|
||||
if (
|
||||
postgres.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
postgres.project.organizationId !== ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
@@ -270,8 +255,7 @@ export const postgresRouter = createTRPCRouter({
|
||||
const postgres = await findPostgresById(input.postgresId);
|
||||
|
||||
if (
|
||||
postgres.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
postgres.project.organizationId !== ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
@@ -296,8 +280,7 @@ export const postgresRouter = createTRPCRouter({
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const postgres = await findPostgresById(input.postgresId);
|
||||
if (
|
||||
postgres.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
postgres.project.organizationId !== ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
@@ -322,8 +305,7 @@ export const postgresRouter = createTRPCRouter({
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const postgres = await findPostgresById(input.postgresId);
|
||||
if (
|
||||
postgres.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
postgres.project.organizationId !== ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
@@ -355,8 +337,7 @@ export const postgresRouter = createTRPCRouter({
|
||||
const { postgresId, ...rest } = input;
|
||||
const postgres = await findPostgresById(postgresId);
|
||||
if (
|
||||
postgres.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
postgres.project.organizationId !== ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
@@ -380,14 +361,13 @@ export const postgresRouter = createTRPCRouter({
|
||||
.input(
|
||||
z.object({
|
||||
postgresId: z.string(),
|
||||
targetEnvironmentId: z.string(),
|
||||
targetProjectId: z.string(),
|
||||
}),
|
||||
)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const postgres = await findPostgresById(input.postgresId);
|
||||
if (
|
||||
postgres.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
postgres.project.organizationId !== ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
@@ -395,16 +375,11 @@ export const postgresRouter = createTRPCRouter({
|
||||
});
|
||||
}
|
||||
|
||||
const targetEnvironment = await findEnvironmentById(
|
||||
input.targetEnvironmentId,
|
||||
);
|
||||
if (
|
||||
targetEnvironment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
const targetProject = await findProjectById(input.targetProjectId);
|
||||
if (targetProject.organizationId !== ctx.session.activeOrganizationId) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to move to this environment",
|
||||
message: "You are not authorized to move to this project",
|
||||
});
|
||||
}
|
||||
|
||||
@@ -412,7 +387,7 @@ export const postgresRouter = createTRPCRouter({
|
||||
const updatedPostgres = await db
|
||||
.update(postgresTable)
|
||||
.set({
|
||||
environmentId: input.targetEnvironmentId,
|
||||
projectId: input.targetProjectId,
|
||||
})
|
||||
.where(eq(postgresTable.postgresId, input.postgresId))
|
||||
.returning()
|
||||
@@ -432,8 +407,7 @@ export const postgresRouter = createTRPCRouter({
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const postgres = await findPostgresById(input.postgresId);
|
||||
if (
|
||||
postgres.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
postgres.project.organizationId !== ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
|
||||
@@ -15,8 +15,7 @@ export const previewDeploymentRouter = createTRPCRouter({
|
||||
.query(async ({ input, ctx }) => {
|
||||
const application = await findApplicationById(input.applicationId);
|
||||
if (
|
||||
application.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
application.project.organizationId !== ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
@@ -32,7 +31,7 @@ export const previewDeploymentRouter = createTRPCRouter({
|
||||
input.previewDeploymentId,
|
||||
);
|
||||
if (
|
||||
previewDeployment.application.environment.project.organizationId !==
|
||||
previewDeployment.application.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
@@ -50,7 +49,7 @@ export const previewDeploymentRouter = createTRPCRouter({
|
||||
input.previewDeploymentId,
|
||||
);
|
||||
if (
|
||||
previewDeployment.application.environment.project.organizationId !==
|
||||
previewDeployment.application.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
|
||||
@@ -19,7 +19,6 @@ import {
|
||||
deleteProject,
|
||||
findApplicationById,
|
||||
findComposeById,
|
||||
findEnvironmentById,
|
||||
findMariadbById,
|
||||
findMemberById,
|
||||
findMongoById,
|
||||
@@ -44,7 +43,6 @@ import {
|
||||
apiUpdateProject,
|
||||
applications,
|
||||
compose,
|
||||
environments,
|
||||
mariadb,
|
||||
mongo,
|
||||
mysql,
|
||||
@@ -82,7 +80,7 @@ export const projectRouter = createTRPCRouter({
|
||||
if (ctx.user.role === "member") {
|
||||
await addNewProject(
|
||||
ctx.user.id,
|
||||
project.project.projectId,
|
||||
project.projectId,
|
||||
ctx.session.activeOrganizationId,
|
||||
);
|
||||
}
|
||||
@@ -119,42 +117,29 @@ export const projectRouter = createTRPCRouter({
|
||||
eq(projects.organizationId, ctx.session.activeOrganizationId),
|
||||
),
|
||||
with: {
|
||||
environments: {
|
||||
with: {
|
||||
applications: {
|
||||
where: buildServiceFilter(
|
||||
applications.applicationId,
|
||||
accessedServices,
|
||||
),
|
||||
},
|
||||
compose: {
|
||||
where: buildServiceFilter(
|
||||
compose.composeId,
|
||||
accessedServices,
|
||||
),
|
||||
},
|
||||
mariadb: {
|
||||
where: buildServiceFilter(
|
||||
mariadb.mariadbId,
|
||||
accessedServices,
|
||||
),
|
||||
},
|
||||
mongo: {
|
||||
where: buildServiceFilter(mongo.mongoId, accessedServices),
|
||||
},
|
||||
mysql: {
|
||||
where: buildServiceFilter(mysql.mysqlId, accessedServices),
|
||||
},
|
||||
postgres: {
|
||||
where: buildServiceFilter(
|
||||
postgres.postgresId,
|
||||
accessedServices,
|
||||
),
|
||||
},
|
||||
redis: {
|
||||
where: buildServiceFilter(redis.redisId, accessedServices),
|
||||
},
|
||||
},
|
||||
applications: {
|
||||
where: buildServiceFilter(
|
||||
applications.applicationId,
|
||||
accessedServices,
|
||||
),
|
||||
},
|
||||
compose: {
|
||||
where: buildServiceFilter(compose.composeId, accessedServices),
|
||||
},
|
||||
mariadb: {
|
||||
where: buildServiceFilter(mariadb.mariadbId, accessedServices),
|
||||
},
|
||||
mongo: {
|
||||
where: buildServiceFilter(mongo.mongoId, accessedServices),
|
||||
},
|
||||
mysql: {
|
||||
where: buildServiceFilter(mysql.mysqlId, accessedServices),
|
||||
},
|
||||
postgres: {
|
||||
where: buildServiceFilter(postgres.postgresId, accessedServices),
|
||||
},
|
||||
redis: {
|
||||
where: buildServiceFilter(redis.redisId, accessedServices),
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -179,22 +164,15 @@ export const projectRouter = createTRPCRouter({
|
||||
}),
|
||||
all: protectedProcedure.query(async ({ ctx }) => {
|
||||
if (ctx.user.role === "member") {
|
||||
const { accessedProjects, accessedEnvironments, accessedServices } =
|
||||
await findMemberById(ctx.user.id, ctx.session.activeOrganizationId);
|
||||
const { accessedProjects, accessedServices } = await findMemberById(
|
||||
ctx.user.id,
|
||||
ctx.session.activeOrganizationId,
|
||||
);
|
||||
|
||||
if (accessedProjects.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Build environment filter
|
||||
const environmentFilter =
|
||||
accessedEnvironments.length === 0
|
||||
? sql`false`
|
||||
: sql`${environments.environmentId} IN (${sql.join(
|
||||
accessedEnvironments.map((envId) => sql`${envId}`),
|
||||
sql`, `,
|
||||
)})`;
|
||||
|
||||
return await db.query.projects.findMany({
|
||||
where: and(
|
||||
sql`${projects.projectId} IN (${sql.join(
|
||||
@@ -204,39 +182,31 @@ export const projectRouter = createTRPCRouter({
|
||||
eq(projects.organizationId, ctx.session.activeOrganizationId),
|
||||
),
|
||||
with: {
|
||||
environments: {
|
||||
where: environmentFilter,
|
||||
with: {
|
||||
applications: {
|
||||
where: buildServiceFilter(
|
||||
applications.applicationId,
|
||||
accessedServices,
|
||||
),
|
||||
with: { domains: true },
|
||||
},
|
||||
mariadb: {
|
||||
where: buildServiceFilter(mariadb.mariadbId, accessedServices),
|
||||
},
|
||||
mongo: {
|
||||
where: buildServiceFilter(mongo.mongoId, accessedServices),
|
||||
},
|
||||
mysql: {
|
||||
where: buildServiceFilter(mysql.mysqlId, accessedServices),
|
||||
},
|
||||
postgres: {
|
||||
where: buildServiceFilter(
|
||||
postgres.postgresId,
|
||||
accessedServices,
|
||||
),
|
||||
},
|
||||
redis: {
|
||||
where: buildServiceFilter(redis.redisId, accessedServices),
|
||||
},
|
||||
compose: {
|
||||
where: buildServiceFilter(compose.composeId, accessedServices),
|
||||
with: { domains: true },
|
||||
},
|
||||
},
|
||||
applications: {
|
||||
where: buildServiceFilter(
|
||||
applications.applicationId,
|
||||
accessedServices,
|
||||
),
|
||||
with: { domains: true },
|
||||
},
|
||||
mariadb: {
|
||||
where: buildServiceFilter(mariadb.mariadbId, accessedServices),
|
||||
},
|
||||
mongo: {
|
||||
where: buildServiceFilter(mongo.mongoId, accessedServices),
|
||||
},
|
||||
mysql: {
|
||||
where: buildServiceFilter(mysql.mysqlId, accessedServices),
|
||||
},
|
||||
postgres: {
|
||||
where: buildServiceFilter(postgres.postgresId, accessedServices),
|
||||
},
|
||||
redis: {
|
||||
where: buildServiceFilter(redis.redisId, accessedServices),
|
||||
},
|
||||
compose: {
|
||||
where: buildServiceFilter(compose.composeId, accessedServices),
|
||||
with: { domains: true },
|
||||
},
|
||||
},
|
||||
orderBy: desc(projects.createdAt),
|
||||
@@ -245,23 +215,19 @@ export const projectRouter = createTRPCRouter({
|
||||
|
||||
return await db.query.projects.findMany({
|
||||
with: {
|
||||
environments: {
|
||||
applications: {
|
||||
with: {
|
||||
applications: {
|
||||
with: {
|
||||
domains: true,
|
||||
},
|
||||
},
|
||||
mariadb: true,
|
||||
mongo: true,
|
||||
mysql: true,
|
||||
postgres: true,
|
||||
redis: true,
|
||||
compose: {
|
||||
with: {
|
||||
domains: true,
|
||||
},
|
||||
},
|
||||
domains: true,
|
||||
},
|
||||
},
|
||||
mariadb: true,
|
||||
mongo: true,
|
||||
mysql: true,
|
||||
postgres: true,
|
||||
redis: true,
|
||||
compose: {
|
||||
with: {
|
||||
domains: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -322,7 +288,7 @@ export const projectRouter = createTRPCRouter({
|
||||
duplicate: protectedProcedure
|
||||
.input(
|
||||
z.object({
|
||||
sourceEnvironmentId: z.string(),
|
||||
sourceProjectId: z.string(),
|
||||
name: z.string(),
|
||||
description: z.string().optional(),
|
||||
includeServices: z.boolean().default(true),
|
||||
@@ -356,15 +322,9 @@ export const projectRouter = createTRPCRouter({
|
||||
}
|
||||
|
||||
// Get source project
|
||||
const sourceEnvironment = input.duplicateInSameProject
|
||||
? await findEnvironmentById(input.sourceEnvironmentId)
|
||||
: null;
|
||||
const sourceProject = await findProjectById(input.sourceProjectId);
|
||||
|
||||
if (
|
||||
input.duplicateInSameProject &&
|
||||
sourceEnvironment?.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
if (sourceProject.organizationId !== ctx.session.activeOrganizationId) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to access this project",
|
||||
@@ -373,17 +333,15 @@ export const projectRouter = createTRPCRouter({
|
||||
|
||||
// Create new project or use existing one
|
||||
const targetProject = input.duplicateInSameProject
|
||||
? sourceEnvironment
|
||||
? sourceProject
|
||||
: await createProject(
|
||||
{
|
||||
name: input.name,
|
||||
description: input.description,
|
||||
env: sourceEnvironment?.project.env,
|
||||
env: sourceProject.env,
|
||||
},
|
||||
ctx.session.activeOrganizationId,
|
||||
).then((value) => value.environment);
|
||||
|
||||
console.log("targetProject", targetProject);
|
||||
);
|
||||
|
||||
if (input.includeServices) {
|
||||
const servicesToDuplicate = input.selectedServices || [];
|
||||
@@ -416,7 +374,7 @@ export const projectRouter = createTRPCRouter({
|
||||
name: input.duplicateInSameProject
|
||||
? `${application.name} (copy)`
|
||||
: application.name,
|
||||
environmentId: targetProject?.environmentId || "",
|
||||
projectId: targetProject.projectId,
|
||||
});
|
||||
|
||||
for (const domain of domains) {
|
||||
@@ -486,7 +444,7 @@ export const projectRouter = createTRPCRouter({
|
||||
name: input.duplicateInSameProject
|
||||
? `${postgres.name} (copy)`
|
||||
: postgres.name,
|
||||
environmentId: targetProject?.environmentId || "",
|
||||
projectId: targetProject.projectId,
|
||||
});
|
||||
|
||||
for (const mount of mounts) {
|
||||
@@ -522,7 +480,7 @@ export const projectRouter = createTRPCRouter({
|
||||
name: input.duplicateInSameProject
|
||||
? `${mariadb.name} (copy)`
|
||||
: mariadb.name,
|
||||
environmentId: targetProject?.environmentId || "",
|
||||
projectId: targetProject.projectId,
|
||||
});
|
||||
|
||||
for (const mount of mounts) {
|
||||
@@ -558,7 +516,7 @@ export const projectRouter = createTRPCRouter({
|
||||
name: input.duplicateInSameProject
|
||||
? `${mongo.name} (copy)`
|
||||
: mongo.name,
|
||||
environmentId: targetProject?.environmentId || "",
|
||||
projectId: targetProject.projectId,
|
||||
});
|
||||
|
||||
for (const mount of mounts) {
|
||||
@@ -594,7 +552,7 @@ export const projectRouter = createTRPCRouter({
|
||||
name: input.duplicateInSameProject
|
||||
? `${mysql.name} (copy)`
|
||||
: mysql.name,
|
||||
environmentId: targetProject?.environmentId || "",
|
||||
projectId: targetProject.projectId,
|
||||
});
|
||||
|
||||
for (const mount of mounts) {
|
||||
@@ -630,7 +588,7 @@ export const projectRouter = createTRPCRouter({
|
||||
name: input.duplicateInSameProject
|
||||
? `${redis.name} (copy)`
|
||||
: redis.name,
|
||||
environmentId: targetProject?.environmentId || "",
|
||||
projectId: targetProject.projectId,
|
||||
});
|
||||
|
||||
for (const mount of mounts) {
|
||||
@@ -665,7 +623,7 @@ export const projectRouter = createTRPCRouter({
|
||||
name: input.duplicateInSameProject
|
||||
? `${compose.name} (copy)`
|
||||
: compose.name,
|
||||
environmentId: targetProject?.environmentId || "",
|
||||
projectId: targetProject.projectId,
|
||||
});
|
||||
|
||||
for (const mount of mounts) {
|
||||
@@ -700,7 +658,7 @@ export const projectRouter = createTRPCRouter({
|
||||
if (!input.duplicateInSameProject && ctx.user.role === "member") {
|
||||
await addNewProject(
|
||||
ctx.user.id,
|
||||
targetProject?.projectId || "",
|
||||
targetProject.projectId,
|
||||
ctx.session.activeOrganizationId,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -19,8 +19,7 @@ export const redirectsRouter = createTRPCRouter({
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const application = await findApplicationById(input.applicationId);
|
||||
if (
|
||||
application.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
application.project.organizationId !== ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
@@ -35,8 +34,7 @@ export const redirectsRouter = createTRPCRouter({
|
||||
const redirect = await findRedirectById(input.redirectId);
|
||||
const application = await findApplicationById(redirect.applicationId);
|
||||
if (
|
||||
application.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
application.project.organizationId !== ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
@@ -51,8 +49,7 @@ export const redirectsRouter = createTRPCRouter({
|
||||
const redirect = await findRedirectById(input.redirectId);
|
||||
const application = await findApplicationById(redirect.applicationId);
|
||||
if (
|
||||
application.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
application.project.organizationId !== ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
@@ -67,8 +64,7 @@ export const redirectsRouter = createTRPCRouter({
|
||||
const redirect = await findRedirectById(input.redirectId);
|
||||
const application = await findApplicationById(redirect.applicationId);
|
||||
if (
|
||||
application.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
application.project.organizationId !== ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
|
||||
@@ -4,7 +4,6 @@ import {
|
||||
createMount,
|
||||
createRedis,
|
||||
deployRedis,
|
||||
findEnvironmentById,
|
||||
findProjectById,
|
||||
findRedisById,
|
||||
IS_CLOUD,
|
||||
@@ -41,14 +40,10 @@ export const redisRouter = createTRPCRouter({
|
||||
.input(apiCreateRedis)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
// Get project from environment
|
||||
const environment = await findEnvironmentById(input.environmentId);
|
||||
const project = await findProjectById(environment.projectId);
|
||||
|
||||
if (ctx.user.role === "member") {
|
||||
await checkServiceAccess(
|
||||
ctx.user.id,
|
||||
project.projectId,
|
||||
input.projectId,
|
||||
ctx.session.activeOrganizationId,
|
||||
"create",
|
||||
);
|
||||
@@ -61,15 +56,14 @@ export const redisRouter = createTRPCRouter({
|
||||
});
|
||||
}
|
||||
|
||||
const project = await findProjectById(input.projectId);
|
||||
if (project.organizationId !== ctx.session.activeOrganizationId) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to access this project",
|
||||
});
|
||||
}
|
||||
const newRedis = await createRedis({
|
||||
...input,
|
||||
});
|
||||
const newRedis = await createRedis(input);
|
||||
if (ctx.user.role === "member") {
|
||||
await addNewService(
|
||||
ctx.user.id,
|
||||
@@ -86,7 +80,7 @@ export const redisRouter = createTRPCRouter({
|
||||
type: "volume",
|
||||
});
|
||||
|
||||
return newRedis;
|
||||
return true;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
@@ -104,10 +98,7 @@ export const redisRouter = createTRPCRouter({
|
||||
}
|
||||
|
||||
const redis = await findRedisById(input.redisId);
|
||||
if (
|
||||
redis.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
if (redis.project.organizationId !== ctx.session.activeOrganizationId) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to access this Redis",
|
||||
@@ -120,10 +111,7 @@ export const redisRouter = createTRPCRouter({
|
||||
.input(apiFindOneRedis)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const redis = await findRedisById(input.redisId);
|
||||
if (
|
||||
redis.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
if (redis.project.organizationId !== ctx.session.activeOrganizationId) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to start this Redis",
|
||||
@@ -145,10 +133,7 @@ export const redisRouter = createTRPCRouter({
|
||||
.input(apiResetRedis)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const redis = await findRedisById(input.redisId);
|
||||
if (
|
||||
redis.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
if (redis.project.organizationId !== ctx.session.activeOrganizationId) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to reload this Redis",
|
||||
@@ -178,10 +163,7 @@ export const redisRouter = createTRPCRouter({
|
||||
.input(apiFindOneRedis)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const redis = await findRedisById(input.redisId);
|
||||
if (
|
||||
redis.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
if (redis.project.organizationId !== ctx.session.activeOrganizationId) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to stop this Redis",
|
||||
@@ -202,10 +184,7 @@ export const redisRouter = createTRPCRouter({
|
||||
.input(apiSaveExternalPortRedis)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const mongo = await findRedisById(input.redisId);
|
||||
if (
|
||||
mongo.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
if (mongo.project.organizationId !== ctx.session.activeOrganizationId) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to save this external port",
|
||||
@@ -221,10 +200,7 @@ export const redisRouter = createTRPCRouter({
|
||||
.input(apiDeployRedis)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const redis = await findRedisById(input.redisId);
|
||||
if (
|
||||
redis.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
if (redis.project.organizationId !== ctx.session.activeOrganizationId) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to deploy this Redis",
|
||||
@@ -244,10 +220,7 @@ export const redisRouter = createTRPCRouter({
|
||||
.input(apiDeployRedis)
|
||||
.subscription(async ({ input, ctx }) => {
|
||||
const redis = await findRedisById(input.redisId);
|
||||
if (
|
||||
redis.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
if (redis.project.organizationId !== ctx.session.activeOrganizationId) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to deploy this Redis",
|
||||
@@ -263,10 +236,7 @@ export const redisRouter = createTRPCRouter({
|
||||
.input(apiChangeRedisStatus)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const mongo = await findRedisById(input.redisId);
|
||||
if (
|
||||
mongo.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
if (mongo.project.organizationId !== ctx.session.activeOrganizationId) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to change this Redis status",
|
||||
@@ -291,10 +261,7 @@ export const redisRouter = createTRPCRouter({
|
||||
|
||||
const redis = await findRedisById(input.redisId);
|
||||
|
||||
if (
|
||||
redis.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
if (redis.project.organizationId !== ctx.session.activeOrganizationId) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to delete this Redis",
|
||||
@@ -317,10 +284,7 @@ export const redisRouter = createTRPCRouter({
|
||||
.input(apiSaveEnvironmentVariablesRedis)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const redis = await findRedisById(input.redisId);
|
||||
if (
|
||||
redis.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
if (redis.project.organizationId !== ctx.session.activeOrganizationId) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to save this environment",
|
||||
@@ -360,31 +324,23 @@ export const redisRouter = createTRPCRouter({
|
||||
.input(
|
||||
z.object({
|
||||
redisId: z.string(),
|
||||
targetEnvironmentId: z.string(),
|
||||
targetProjectId: z.string(),
|
||||
}),
|
||||
)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const redis = await findRedisById(input.redisId);
|
||||
if (
|
||||
redis.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
if (redis.project.organizationId !== ctx.session.activeOrganizationId) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to move this redis",
|
||||
});
|
||||
}
|
||||
|
||||
const targetEnvironment = await findEnvironmentById(
|
||||
input.targetEnvironmentId,
|
||||
);
|
||||
if (
|
||||
targetEnvironment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
const targetProject = await findProjectById(input.targetProjectId);
|
||||
if (targetProject.organizationId !== ctx.session.activeOrganizationId) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to move to this environment",
|
||||
message: "You are not authorized to move to this project",
|
||||
});
|
||||
}
|
||||
|
||||
@@ -392,7 +348,7 @@ export const redisRouter = createTRPCRouter({
|
||||
const updatedRedis = await db
|
||||
.update(redisTable)
|
||||
.set({
|
||||
environmentId: input.targetEnvironmentId,
|
||||
projectId: input.targetProjectId,
|
||||
})
|
||||
.where(eq(redisTable.redisId, input.redisId))
|
||||
.returning()
|
||||
@@ -411,10 +367,7 @@ export const redisRouter = createTRPCRouter({
|
||||
.input(apiRebuildRedis)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const redis = await findRedisById(input.redisId);
|
||||
if (
|
||||
redis.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
if (redis.project.organizationId !== ctx.session.activeOrganizationId) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to rebuild this Redis database",
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
import {
|
||||
findRollbackById,
|
||||
removeRollbackById,
|
||||
rollback,
|
||||
} from "@dokploy/server";
|
||||
import { removeRollbackById, rollback } from "@dokploy/server";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc";
|
||||
import { apiFindOneRollback } from "@/server/db/schema";
|
||||
@@ -26,18 +22,8 @@ export const rollbackRouter = createTRPCRouter({
|
||||
}),
|
||||
rollback: protectedProcedure
|
||||
.input(apiFindOneRollback)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
.mutation(async ({ input }) => {
|
||||
try {
|
||||
const currentRollback = await findRollbackById(input.rollbackId);
|
||||
if (
|
||||
currentRollback?.deployment?.application?.environment?.project
|
||||
.organizationId !== ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to rollback this deployment",
|
||||
});
|
||||
}
|
||||
return await rollback(input.rollbackId);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
|
||||
@@ -19,8 +19,7 @@ export const securityRouter = createTRPCRouter({
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const application = await findApplicationById(input.applicationId);
|
||||
if (
|
||||
application.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
application.project.organizationId !== ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
@@ -35,8 +34,7 @@ export const securityRouter = createTRPCRouter({
|
||||
const security = await findSecurityById(input.securityId);
|
||||
const application = await findApplicationById(security.applicationId);
|
||||
if (
|
||||
application.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
application.project.organizationId !== ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
@@ -51,8 +49,7 @@ export const securityRouter = createTRPCRouter({
|
||||
const security = await findSecurityById(input.securityId);
|
||||
const application = await findApplicationById(security.applicationId);
|
||||
if (
|
||||
application.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
application.project.organizationId !== ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
@@ -67,8 +64,7 @@ export const securityRouter = createTRPCRouter({
|
||||
const security = await findSecurityById(input.securityId);
|
||||
const application = await findApplicationById(security.applicationId);
|
||||
if (
|
||||
application.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
application.project.organizationId !== ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
|
||||
@@ -45,7 +45,7 @@ import {
|
||||
} from "@dokploy/server";
|
||||
import { generateOpenApiDocument } from "@dokploy/trpc-openapi";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { eq, sql } from "drizzle-orm";
|
||||
import { sql } from "drizzle-orm";
|
||||
import { dump, load } from "js-yaml";
|
||||
import { scheduledJobs, scheduleJob } from "node-schedule";
|
||||
import { z } from "zod";
|
||||
@@ -60,8 +60,6 @@ import {
|
||||
apiServerSchema,
|
||||
apiTraefikConfig,
|
||||
apiUpdateDockerCleanup,
|
||||
projects,
|
||||
server,
|
||||
} from "@/server/db/schema";
|
||||
import { removeJob, schedule } from "@/server/utils/backup";
|
||||
import packageInfo from "../../../package.json";
|
||||
@@ -708,18 +706,6 @@ export const settingsRouter = createTRPCRouter({
|
||||
isCloud: publicProcedure.query(async () => {
|
||||
return IS_CLOUD;
|
||||
}),
|
||||
isUserSubscribed: protectedProcedure.query(async ({ ctx }) => {
|
||||
const haveServers = await db.query.server.findMany({
|
||||
where: eq(server.organizationId, ctx.session?.activeOrganizationId || ""),
|
||||
});
|
||||
const haveProjects = await db.query.projects.findMany({
|
||||
where: eq(
|
||||
projects.organizationId,
|
||||
ctx.session?.activeOrganizationId || "",
|
||||
),
|
||||
});
|
||||
return haveServers.length > 0 || haveProjects.length > 0;
|
||||
}),
|
||||
health: publicProcedure.query(async () => {
|
||||
if (IS_CLOUD) {
|
||||
try {
|
||||
|
||||
@@ -5,7 +5,6 @@ import * as schema from "./schema";
|
||||
declare global {
|
||||
var db: PostgresJsDatabase<typeof schema> | undefined;
|
||||
}
|
||||
|
||||
const dbUrl =
|
||||
process.env.DATABASE_URL ||
|
||||
"postgres://dokploy:amukds4wi9001583845717ad2@dokploy-postgres:5432/dokploy";
|
||||
|
||||
@@ -23,30 +23,3 @@ export const deploy = async (jobData: DeploymentJob) => {
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
type CancelDeploymentData =
|
||||
| { applicationId: string; applicationType: "application" }
|
||||
| { composeId: string; applicationType: "compose" };
|
||||
|
||||
export const cancelDeployment = async (cancelData: CancelDeploymentData) => {
|
||||
try {
|
||||
const result = await fetch(`${process.env.SERVER_URL}/cancel-deployment`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"X-API-Key": process.env.API_KEY || "NO-DEFINED",
|
||||
},
|
||||
body: JSON.stringify(cancelData),
|
||||
});
|
||||
|
||||
if (!result.ok) {
|
||||
const errorData = await result.json().catch(() => ({}));
|
||||
throw new Error(errorData.message || "Failed to cancel deployment");
|
||||
}
|
||||
|
||||
const data = await result.json();
|
||||
return data;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
} from "./queue.js";
|
||||
import { jobQueueSchema } from "./schema.js";
|
||||
import { initializeJobs } from "./utils.js";
|
||||
import { firstWorker, secondWorker, thirdWorker } from "./workers.js";
|
||||
import { firstWorker, secondWorker } from "./workers.js";
|
||||
|
||||
const app = new Hono();
|
||||
|
||||
@@ -91,7 +91,6 @@ export const gracefulShutdown = async (signal: string) => {
|
||||
logger.warn(`Received ${signal}, closing server...`);
|
||||
await firstWorker.close();
|
||||
await secondWorker.close();
|
||||
await thirdWorker.close();
|
||||
process.exit(0);
|
||||
};
|
||||
|
||||
|
||||
@@ -7,34 +7,22 @@ import { runJobs } from "./utils.js";
|
||||
export const firstWorker = new Worker(
|
||||
"backupQueue",
|
||||
async (job: Job<QueueJob>) => {
|
||||
logger.info({ data: job.data }, "Running job first worker");
|
||||
logger.info({ data: job.data }, "Running job");
|
||||
await runJobs(job.data);
|
||||
},
|
||||
{
|
||||
concurrency: 100,
|
||||
concurrency: 50,
|
||||
connection,
|
||||
},
|
||||
);
|
||||
export const secondWorker = new Worker(
|
||||
"backupQueue",
|
||||
async (job: Job<QueueJob>) => {
|
||||
logger.info({ data: job.data }, "Running job second worker");
|
||||
logger.info({ data: job.data }, "Running job");
|
||||
await runJobs(job.data);
|
||||
},
|
||||
{
|
||||
concurrency: 100,
|
||||
connection,
|
||||
},
|
||||
);
|
||||
|
||||
export const thirdWorker = new Worker(
|
||||
"backupQueue",
|
||||
async (job: Job<QueueJob>) => {
|
||||
logger.info({ data: job.data }, "Running job third worker");
|
||||
await runJobs(job.data);
|
||||
},
|
||||
{
|
||||
concurrency: 100,
|
||||
concurrency: 50,
|
||||
connection,
|
||||
},
|
||||
);
|
||||
|
||||
@@ -112,10 +112,6 @@ export const member = pgTable("member", {
|
||||
.array()
|
||||
.notNull()
|
||||
.default(sql`ARRAY[]::text[]`),
|
||||
accessedEnvironments: text("accessedEnvironments")
|
||||
.array()
|
||||
.notNull()
|
||||
.default(sql`ARRAY[]::text[]`),
|
||||
accessedServices: text("accesedServices")
|
||||
.array()
|
||||
.notNull()
|
||||
|
||||
@@ -55,7 +55,7 @@ export const apiUpdateAi = createSchema
|
||||
.omit({ organizationId: true });
|
||||
|
||||
export const deploySuggestionSchema = z.object({
|
||||
environmentId: z.string().min(1),
|
||||
projectId: z.string().min(1),
|
||||
id: z.string().min(1),
|
||||
dockerCompose: z.string().min(1),
|
||||
envVariables: z.string(),
|
||||
|
||||
@@ -13,7 +13,6 @@ import { z } from "zod";
|
||||
import { bitbucket } from "./bitbucket";
|
||||
import { deployments } from "./deployment";
|
||||
import { domains } from "./domain";
|
||||
import { environments } from "./environment";
|
||||
import { gitea } from "./gitea";
|
||||
import { github } from "./github";
|
||||
import { gitlab } from "./gitlab";
|
||||
@@ -180,9 +179,9 @@ export const applications = pgTable("application", {
|
||||
registryId: text("registryId").references(() => registry.registryId, {
|
||||
onDelete: "set null",
|
||||
}),
|
||||
environmentId: text("environmentId")
|
||||
projectId: text("projectId")
|
||||
.notNull()
|
||||
.references(() => environments.environmentId, { onDelete: "cascade" }),
|
||||
.references(() => projects.projectId, { onDelete: "cascade" }),
|
||||
githubId: text("githubId").references(() => github.githubId, {
|
||||
onDelete: "set null",
|
||||
}),
|
||||
@@ -203,9 +202,9 @@ export const applications = pgTable("application", {
|
||||
export const applicationsRelations = relations(
|
||||
applications,
|
||||
({ one, many }) => ({
|
||||
environment: one(environments, {
|
||||
fields: [applications.environmentId],
|
||||
references: [environments.environmentId],
|
||||
project: one(projects, {
|
||||
fields: [applications.projectId],
|
||||
references: [projects.projectId],
|
||||
}),
|
||||
deployments: many(deployments),
|
||||
customGitSSHKey: one(sshKeys, {
|
||||
@@ -274,7 +273,7 @@ const createSchema = createInsertSchema(applications, {
|
||||
customGitBuildPath: z.string().optional(),
|
||||
customGitUrl: z.string().optional(),
|
||||
buildPath: z.string().optional(),
|
||||
environmentId: z.string(),
|
||||
projectId: z.string(),
|
||||
sourceType: z
|
||||
.enum(["github", "docker", "git", "gitlab", "bitbucket", "gitea", "drop"])
|
||||
.optional(),
|
||||
@@ -318,7 +317,7 @@ export const apiCreateApplication = createSchema.pick({
|
||||
name: true,
|
||||
appName: true,
|
||||
description: true,
|
||||
environmentId: true,
|
||||
projectId: true,
|
||||
serverId: true,
|
||||
});
|
||||
|
||||
@@ -328,26 +327,6 @@ export const apiFindOneApplication = createSchema
|
||||
})
|
||||
.required();
|
||||
|
||||
export const apiDeployApplication = createSchema
|
||||
.pick({
|
||||
applicationId: true,
|
||||
})
|
||||
.extend({
|
||||
applicationId: z.string().min(1),
|
||||
title: z.string().optional(),
|
||||
description: z.string().optional(),
|
||||
});
|
||||
|
||||
export const apiRedeployApplication = createSchema
|
||||
.pick({
|
||||
applicationId: true,
|
||||
})
|
||||
.extend({
|
||||
applicationId: z.string().min(1),
|
||||
title: z.string().optional(),
|
||||
description: z.string().optional(),
|
||||
});
|
||||
|
||||
export const apiReloadApplication = createSchema
|
||||
.pick({
|
||||
appName: true,
|
||||
|
||||
@@ -7,7 +7,6 @@ import { backups } from "./backups";
|
||||
import { bitbucket } from "./bitbucket";
|
||||
import { deployments } from "./deployment";
|
||||
import { domains } from "./domain";
|
||||
import { environments } from "./environment";
|
||||
import { gitea } from "./gitea";
|
||||
import { github } from "./github";
|
||||
import { gitlab } from "./gitlab";
|
||||
@@ -85,9 +84,9 @@ export const compose = pgTable("compose", {
|
||||
.default(false),
|
||||
triggerType: triggerType("triggerType").default("push"),
|
||||
composeStatus: applicationStatus("composeStatus").notNull().default("idle"),
|
||||
environmentId: text("environmentId")
|
||||
projectId: text("projectId")
|
||||
.notNull()
|
||||
.references(() => environments.environmentId, { onDelete: "cascade" }),
|
||||
.references(() => projects.projectId, { onDelete: "cascade" }),
|
||||
createdAt: text("createdAt")
|
||||
.notNull()
|
||||
.$defaultFn(() => new Date().toISOString()),
|
||||
@@ -110,9 +109,9 @@ export const compose = pgTable("compose", {
|
||||
});
|
||||
|
||||
export const composeRelations = relations(compose, ({ one, many }) => ({
|
||||
environment: one(environments, {
|
||||
fields: [compose.environmentId],
|
||||
references: [environments.environmentId],
|
||||
project: one(projects, {
|
||||
fields: [compose.projectId],
|
||||
references: [projects.projectId],
|
||||
}),
|
||||
deployments: many(deployments),
|
||||
mounts: many(mounts),
|
||||
@@ -150,7 +149,7 @@ const createSchema = createInsertSchema(compose, {
|
||||
description: z.string(),
|
||||
env: z.string().optional(),
|
||||
composeFile: z.string().optional(),
|
||||
environmentId: z.string(),
|
||||
projectId: z.string(),
|
||||
customGitSSHKeyId: z.string().optional(),
|
||||
command: z.string().optional(),
|
||||
composePath: z.string().min(1),
|
||||
@@ -161,7 +160,7 @@ const createSchema = createInsertSchema(compose, {
|
||||
export const apiCreateCompose = createSchema.pick({
|
||||
name: true,
|
||||
description: true,
|
||||
environmentId: true,
|
||||
projectId: true,
|
||||
composeType: true,
|
||||
appName: true,
|
||||
serverId: true,
|
||||
@@ -170,7 +169,7 @@ export const apiCreateCompose = createSchema.pick({
|
||||
|
||||
export const apiCreateComposeByTemplate = createSchema
|
||||
.pick({
|
||||
environmentId: true,
|
||||
projectId: true,
|
||||
})
|
||||
.extend({
|
||||
id: z.string().min(1),
|
||||
@@ -181,18 +180,6 @@ export const apiFindCompose = z.object({
|
||||
composeId: z.string().min(1),
|
||||
});
|
||||
|
||||
export const apiDeployCompose = z.object({
|
||||
composeId: z.string().min(1),
|
||||
title: z.string().optional(),
|
||||
description: z.string().optional(),
|
||||
});
|
||||
|
||||
export const apiRedeployCompose = z.object({
|
||||
composeId: z.string().min(1),
|
||||
title: z.string().optional(),
|
||||
description: z.string().optional(),
|
||||
});
|
||||
|
||||
export const apiDeleteCompose = z.object({
|
||||
composeId: z.string().min(1),
|
||||
deleteVolumes: z.boolean(),
|
||||
|
||||
@@ -1,85 +0,0 @@
|
||||
import { relations } from "drizzle-orm";
|
||||
import { pgTable, text } from "drizzle-orm/pg-core";
|
||||
import { createInsertSchema } from "drizzle-zod";
|
||||
import { nanoid } from "nanoid";
|
||||
import { z } from "zod";
|
||||
import { applications } from "./application";
|
||||
import { compose } from "./compose";
|
||||
import { mariadb } from "./mariadb";
|
||||
import { mongo } from "./mongo";
|
||||
import { mysql } from "./mysql";
|
||||
import { postgres } from "./postgres";
|
||||
import { projects } from "./project";
|
||||
import { redis } from "./redis";
|
||||
|
||||
export const environments = pgTable("environment", {
|
||||
environmentId: text("environmentId")
|
||||
.notNull()
|
||||
.primaryKey()
|
||||
.$defaultFn(() => nanoid()),
|
||||
name: text("name").notNull(),
|
||||
description: text("description"),
|
||||
createdAt: text("createdAt")
|
||||
.notNull()
|
||||
.$defaultFn(() => new Date().toISOString()),
|
||||
env: text("env").notNull().default(""),
|
||||
projectId: text("projectId")
|
||||
.notNull()
|
||||
.references(() => projects.projectId, { onDelete: "cascade" }),
|
||||
});
|
||||
|
||||
export const environmentRelations = relations(
|
||||
environments,
|
||||
({ one, many }) => ({
|
||||
project: one(projects, {
|
||||
fields: [environments.projectId],
|
||||
references: [projects.projectId],
|
||||
}),
|
||||
applications: many(applications),
|
||||
mariadb: many(mariadb),
|
||||
postgres: many(postgres),
|
||||
mysql: many(mysql),
|
||||
redis: many(redis),
|
||||
mongo: many(mongo),
|
||||
compose: many(compose),
|
||||
}),
|
||||
);
|
||||
|
||||
const createSchema = createInsertSchema(environments, {
|
||||
environmentId: z.string().min(1),
|
||||
name: z.string().min(1),
|
||||
description: z.string().optional(),
|
||||
});
|
||||
|
||||
export const apiCreateEnvironment = createSchema.pick({
|
||||
name: true,
|
||||
description: true,
|
||||
projectId: true,
|
||||
});
|
||||
|
||||
export const apiFindOneEnvironment = createSchema
|
||||
.pick({
|
||||
environmentId: true,
|
||||
})
|
||||
.required();
|
||||
|
||||
export const apiRemoveEnvironment = createSchema
|
||||
.pick({
|
||||
environmentId: true,
|
||||
})
|
||||
.required();
|
||||
|
||||
export const apiUpdateEnvironment = createSchema.partial().extend({
|
||||
environmentId: z.string().min(1),
|
||||
});
|
||||
|
||||
export const apiDuplicateEnvironment = createSchema
|
||||
.pick({
|
||||
environmentId: true,
|
||||
name: true,
|
||||
description: true,
|
||||
})
|
||||
.required({
|
||||
environmentId: true,
|
||||
name: true,
|
||||
});
|
||||
@@ -8,7 +8,6 @@ export * from "./compose";
|
||||
export * from "./deployment";
|
||||
export * from "./destination";
|
||||
export * from "./domain";
|
||||
export * from "./environment";
|
||||
export * from "./git-provider";
|
||||
export * from "./gitea";
|
||||
export * from "./github";
|
||||
|
||||
@@ -4,8 +4,8 @@ import { createInsertSchema } from "drizzle-zod";
|
||||
import { nanoid } from "nanoid";
|
||||
import { z } from "zod";
|
||||
import { backups } from "./backups";
|
||||
import { environments } from "./environment";
|
||||
import { mounts } from "./mount";
|
||||
import { projects } from "./project";
|
||||
import { server } from "./server";
|
||||
import {
|
||||
applicationStatus,
|
||||
@@ -66,19 +66,18 @@ export const mariadb = pgTable("mariadb", {
|
||||
createdAt: text("createdAt")
|
||||
.notNull()
|
||||
.$defaultFn(() => new Date().toISOString()),
|
||||
|
||||
environmentId: text("environmentId")
|
||||
projectId: text("projectId")
|
||||
.notNull()
|
||||
.references(() => environments.environmentId, { onDelete: "cascade" }),
|
||||
.references(() => projects.projectId, { onDelete: "cascade" }),
|
||||
serverId: text("serverId").references(() => server.serverId, {
|
||||
onDelete: "cascade",
|
||||
}),
|
||||
});
|
||||
|
||||
export const mariadbRelations = relations(mariadb, ({ one, many }) => ({
|
||||
environment: one(environments, {
|
||||
fields: [mariadb.environmentId],
|
||||
references: [environments.environmentId],
|
||||
project: one(projects, {
|
||||
fields: [mariadb.projectId],
|
||||
references: [projects.projectId],
|
||||
}),
|
||||
backups: many(backups),
|
||||
mounts: many(mounts),
|
||||
@@ -95,19 +94,8 @@ const createSchema = createInsertSchema(mariadb, {
|
||||
createdAt: z.string(),
|
||||
databaseName: z.string().min(1),
|
||||
databaseUser: z.string().min(1),
|
||||
databasePassword: z
|
||||
.string()
|
||||
.regex(/^[a-zA-Z0-9@#%^&*()_+\-=[\]{}|;:,.<>?~`]*$/, {
|
||||
message:
|
||||
"Password contains invalid characters. Please avoid: $ ! ' \" \\ / and space characters for database compatibility",
|
||||
}),
|
||||
databaseRootPassword: z
|
||||
.string()
|
||||
.regex(/^[a-zA-Z0-9@#%^&*()_+\-=[\]{}|;:,.<>?~`]*$/, {
|
||||
message:
|
||||
"Password contains invalid characters. Please avoid: $ ! ' \" \\ / and space characters for database compatibility",
|
||||
})
|
||||
.optional(),
|
||||
databasePassword: z.string(),
|
||||
databaseRootPassword: z.string().optional(),
|
||||
dockerImage: z.string().default("mariadb:6"),
|
||||
command: z.string().optional(),
|
||||
env: z.string().optional(),
|
||||
@@ -115,7 +103,7 @@ const createSchema = createInsertSchema(mariadb, {
|
||||
memoryLimit: z.string().optional(),
|
||||
cpuReservation: z.string().optional(),
|
||||
cpuLimit: z.string().optional(),
|
||||
environmentId: z.string(),
|
||||
projectId: z.string(),
|
||||
applicationStatus: z.enum(["idle", "running", "done", "error"]),
|
||||
externalPort: z.number(),
|
||||
description: z.string().optional(),
|
||||
@@ -136,7 +124,7 @@ export const apiCreateMariaDB = createSchema
|
||||
appName: true,
|
||||
dockerImage: true,
|
||||
databaseRootPassword: true,
|
||||
environmentId: true,
|
||||
projectId: true,
|
||||
description: true,
|
||||
databaseName: true,
|
||||
databaseUser: true,
|
||||
|
||||
@@ -4,8 +4,8 @@ import { createInsertSchema } from "drizzle-zod";
|
||||
import { nanoid } from "nanoid";
|
||||
import { z } from "zod";
|
||||
import { backups } from "./backups";
|
||||
import { environments } from "./environment";
|
||||
import { mounts } from "./mount";
|
||||
import { projects } from "./project";
|
||||
import { server } from "./server";
|
||||
import {
|
||||
applicationStatus,
|
||||
@@ -62,10 +62,9 @@ export const mongo = pgTable("mongo", {
|
||||
createdAt: text("createdAt")
|
||||
.notNull()
|
||||
.$defaultFn(() => new Date().toISOString()),
|
||||
|
||||
environmentId: text("environmentId")
|
||||
projectId: text("projectId")
|
||||
.notNull()
|
||||
.references(() => environments.environmentId, { onDelete: "cascade" }),
|
||||
.references(() => projects.projectId, { onDelete: "cascade" }),
|
||||
serverId: text("serverId").references(() => server.serverId, {
|
||||
onDelete: "cascade",
|
||||
}),
|
||||
@@ -73,9 +72,9 @@ export const mongo = pgTable("mongo", {
|
||||
});
|
||||
|
||||
export const mongoRelations = relations(mongo, ({ one, many }) => ({
|
||||
environment: one(environments, {
|
||||
fields: [mongo.environmentId],
|
||||
references: [environments.environmentId],
|
||||
project: one(projects, {
|
||||
fields: [mongo.projectId],
|
||||
references: [projects.projectId],
|
||||
}),
|
||||
backups: many(backups),
|
||||
mounts: many(mounts),
|
||||
@@ -90,12 +89,7 @@ const createSchema = createInsertSchema(mongo, {
|
||||
createdAt: z.string(),
|
||||
mongoId: z.string(),
|
||||
name: z.string().min(1),
|
||||
databasePassword: z
|
||||
.string()
|
||||
.regex(/^[a-zA-Z0-9@#%^&*()_+\-=[\]{}|;:,.<>?~`]*$/, {
|
||||
message:
|
||||
"Password contains invalid characters. Please avoid: $ ! ' \" \\ / and space characters for database compatibility",
|
||||
}),
|
||||
databasePassword: z.string(),
|
||||
databaseUser: z.string().min(1),
|
||||
dockerImage: z.string().default("mongo:15"),
|
||||
command: z.string().optional(),
|
||||
@@ -104,7 +98,7 @@ const createSchema = createInsertSchema(mongo, {
|
||||
memoryLimit: z.string().optional(),
|
||||
cpuReservation: z.string().optional(),
|
||||
cpuLimit: z.string().optional(),
|
||||
environmentId: z.string(),
|
||||
projectId: z.string(),
|
||||
applicationStatus: z.enum(["idle", "running", "done", "error"]),
|
||||
externalPort: z.number(),
|
||||
description: z.string().optional(),
|
||||
@@ -125,7 +119,7 @@ export const apiCreateMongo = createSchema
|
||||
name: true,
|
||||
appName: true,
|
||||
dockerImage: true,
|
||||
environmentId: true,
|
||||
projectId: true,
|
||||
description: true,
|
||||
databaseUser: true,
|
||||
databasePassword: true,
|
||||
|
||||
@@ -4,8 +4,8 @@ import { createInsertSchema } from "drizzle-zod";
|
||||
import { nanoid } from "nanoid";
|
||||
import { z } from "zod";
|
||||
import { backups } from "./backups";
|
||||
import { environments } from "./environment";
|
||||
import { mounts } from "./mount";
|
||||
import { projects } from "./project";
|
||||
import { server } from "./server";
|
||||
import {
|
||||
applicationStatus,
|
||||
@@ -64,19 +64,18 @@ export const mysql = pgTable("mysql", {
|
||||
createdAt: text("createdAt")
|
||||
.notNull()
|
||||
.$defaultFn(() => new Date().toISOString()),
|
||||
|
||||
environmentId: text("environmentId")
|
||||
projectId: text("projectId")
|
||||
.notNull()
|
||||
.references(() => environments.environmentId, { onDelete: "cascade" }),
|
||||
.references(() => projects.projectId, { onDelete: "cascade" }),
|
||||
serverId: text("serverId").references(() => server.serverId, {
|
||||
onDelete: "cascade",
|
||||
}),
|
||||
});
|
||||
|
||||
export const mysqlRelations = relations(mysql, ({ one, many }) => ({
|
||||
environment: one(environments, {
|
||||
fields: [mysql.environmentId],
|
||||
references: [environments.environmentId],
|
||||
project: one(projects, {
|
||||
fields: [mysql.projectId],
|
||||
references: [projects.projectId],
|
||||
}),
|
||||
backups: many(backups),
|
||||
mounts: many(mounts),
|
||||
@@ -93,19 +92,8 @@ const createSchema = createInsertSchema(mysql, {
|
||||
name: z.string().min(1),
|
||||
databaseName: z.string().min(1),
|
||||
databaseUser: z.string().min(1),
|
||||
databasePassword: z
|
||||
.string()
|
||||
.regex(/^[a-zA-Z0-9@#%^&*()_+\-=[\]{}|;:,.<>?~`]*$/, {
|
||||
message:
|
||||
"Password contains invalid characters. Please avoid: $ ! ' \" \\ / and space characters for database compatibility",
|
||||
}),
|
||||
databaseRootPassword: z
|
||||
.string()
|
||||
.regex(/^[a-zA-Z0-9@#%^&*()_+\-=[\]{}|;:,.<>?~`]*$/, {
|
||||
message:
|
||||
"Password contains invalid characters. Please avoid: $ ! ' \" \\ / and space characters for database compatibility",
|
||||
})
|
||||
.optional(),
|
||||
databasePassword: z.string(),
|
||||
databaseRootPassword: z.string().optional(),
|
||||
dockerImage: z.string().default("mysql:8"),
|
||||
command: z.string().optional(),
|
||||
env: z.string().optional(),
|
||||
@@ -113,6 +101,7 @@ const createSchema = createInsertSchema(mysql, {
|
||||
memoryLimit: z.string().optional(),
|
||||
cpuReservation: z.string().optional(),
|
||||
cpuLimit: z.string().optional(),
|
||||
projectId: z.string(),
|
||||
applicationStatus: z.enum(["idle", "running", "done", "error"]),
|
||||
externalPort: z.number(),
|
||||
description: z.string().optional(),
|
||||
@@ -132,7 +121,7 @@ export const apiCreateMySql = createSchema
|
||||
name: true,
|
||||
appName: true,
|
||||
dockerImage: true,
|
||||
environmentId: true,
|
||||
projectId: true,
|
||||
description: true,
|
||||
databaseName: true,
|
||||
databaseUser: true,
|
||||
|
||||
@@ -11,7 +11,6 @@ export const notificationType = pgEnum("notificationType", [
|
||||
"discord",
|
||||
"email",
|
||||
"gotify",
|
||||
"ntfy",
|
||||
]);
|
||||
|
||||
export const notifications = pgTable("notification", {
|
||||
@@ -45,9 +44,6 @@ export const notifications = pgTable("notification", {
|
||||
gotifyId: text("gotifyId").references(() => gotify.gotifyId, {
|
||||
onDelete: "cascade",
|
||||
}),
|
||||
ntfyId: text("ntfyId").references(() => ntfy.ntfyId, {
|
||||
onDelete: "cascade",
|
||||
}),
|
||||
organizationId: text("organizationId")
|
||||
.notNull()
|
||||
.references(() => organization.id, { onDelete: "cascade" }),
|
||||
@@ -105,17 +101,6 @@ export const gotify = pgTable("gotify", {
|
||||
decoration: boolean("decoration"),
|
||||
});
|
||||
|
||||
export const ntfy = pgTable("ntfy", {
|
||||
ntfyId: text("ntfyId")
|
||||
.notNull()
|
||||
.primaryKey()
|
||||
.$defaultFn(() => nanoid()),
|
||||
serverUrl: text("serverUrl").notNull(),
|
||||
topic: text("topic").notNull(),
|
||||
accessToken: text("accessToken").notNull(),
|
||||
priority: integer("priority").notNull().default(3),
|
||||
});
|
||||
|
||||
export const notificationsRelations = relations(notifications, ({ one }) => ({
|
||||
slack: one(slack, {
|
||||
fields: [notifications.slackId],
|
||||
@@ -137,10 +122,6 @@ export const notificationsRelations = relations(notifications, ({ one }) => ({
|
||||
fields: [notifications.gotifyId],
|
||||
references: [gotify.gotifyId],
|
||||
}),
|
||||
ntfy: one(ntfy, {
|
||||
fields: [notifications.ntfyId],
|
||||
references: [ntfy.ntfyId],
|
||||
}),
|
||||
organization: one(organization, {
|
||||
fields: [notifications.organizationId],
|
||||
references: [organization.id],
|
||||
@@ -303,36 +284,6 @@ export const apiTestGotifyConnection = apiCreateGotify
|
||||
decoration: z.boolean().optional(),
|
||||
});
|
||||
|
||||
export const apiCreateNtfy = notificationsSchema
|
||||
.pick({
|
||||
appBuildError: true,
|
||||
databaseBackup: true,
|
||||
dokployRestart: true,
|
||||
name: true,
|
||||
appDeploy: true,
|
||||
dockerCleanup: true,
|
||||
})
|
||||
.extend({
|
||||
serverUrl: z.string().min(1),
|
||||
topic: z.string().min(1),
|
||||
accessToken: z.string().min(1),
|
||||
priority: z.number().min(1),
|
||||
})
|
||||
.required();
|
||||
|
||||
export const apiUpdateNtfy = apiCreateNtfy.partial().extend({
|
||||
notificationId: z.string().min(1),
|
||||
ntfyId: z.string().min(1),
|
||||
organizationId: z.string().optional(),
|
||||
});
|
||||
|
||||
export const apiTestNtfyConnection = apiCreateNtfy.pick({
|
||||
serverUrl: true,
|
||||
topic: true,
|
||||
accessToken: true,
|
||||
priority: true,
|
||||
});
|
||||
|
||||
export const apiFindOneNotification = notificationsSchema
|
||||
.pick({
|
||||
notificationId: true,
|
||||
@@ -352,9 +303,7 @@ export const apiSendTest = notificationsSchema
|
||||
password: z.string(),
|
||||
toAddresses: z.array(z.string()),
|
||||
serverUrl: z.string(),
|
||||
topic: z.string(),
|
||||
appToken: z.string(),
|
||||
accessToken: z.string(),
|
||||
priority: z.number(),
|
||||
})
|
||||
.partial();
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user