mirror of
https://github.com/Dokploy/dokploy.git
synced 2026-07-06 14:35:26 +02:00
Compare commits
1 Commits
v0.25.2
...
feat/concu
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2da2b2dd39 |
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:
|
Before submitting this PR, please make sure that:
|
||||||
|
|
||||||
- [] You created a dedicated branch based on the `canary` branch.
|
- [ ] 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 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 have tested this PR in your local instance.
|
||||||
|
|
||||||
## Issues related (if applicable)
|
## 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)
|
## 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 |
17
README.md
17
README.md
@@ -11,25 +11,8 @@
|
|||||||
</div>
|
</div>
|
||||||
<br />
|
<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.
|
Dokploy is a free, self-hostable Platform as a Service (PaaS) that simplifies the deployment and management of applications and databases.
|
||||||
|
|
||||||
|
|
||||||
## ✨ Features
|
## ✨ Features
|
||||||
|
|
||||||
Dokploy includes multiple features to make your life easier.
|
Dokploy includes multiple features to make your life easier.
|
||||||
|
|||||||
@@ -5,11 +5,7 @@ import { zValidator } from "@hono/zod-validator";
|
|||||||
import { Inngest } from "inngest";
|
import { Inngest } from "inngest";
|
||||||
import { serve as serveInngest } from "inngest/hono";
|
import { serve as serveInngest } from "inngest/hono";
|
||||||
import { logger } from "./logger.js";
|
import { logger } from "./logger.js";
|
||||||
import {
|
import { type DeployJob, deployJobSchema } from "./schema.js";
|
||||||
cancelDeploymentSchema,
|
|
||||||
type DeployJob,
|
|
||||||
deployJobSchema,
|
|
||||||
} from "./schema.js";
|
|
||||||
import { deploy } from "./utils.js";
|
import { deploy } from "./utils.js";
|
||||||
|
|
||||||
const app = new Hono();
|
const app = new Hono();
|
||||||
@@ -31,13 +27,6 @@ export const deploymentFunction = inngest.createFunction(
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
retries: 0,
|
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" },
|
{ 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) => {
|
app.get("/health", async (c) => {
|
||||||
return c.json({ status: "ok" });
|
return c.json({ status: "ok" });
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ import { z } from "zod";
|
|||||||
export const deployJobSchema = z.discriminatedUnion("applicationType", [
|
export const deployJobSchema = z.discriminatedUnion("applicationType", [
|
||||||
z.object({
|
z.object({
|
||||||
applicationId: z.string(),
|
applicationId: z.string(),
|
||||||
titleLog: z.string().optional(),
|
titleLog: z.string(),
|
||||||
descriptionLog: z.string().optional(),
|
descriptionLog: z.string(),
|
||||||
server: z.boolean().optional(),
|
server: z.boolean().optional(),
|
||||||
type: z.enum(["deploy", "redeploy"]),
|
type: z.enum(["deploy", "redeploy"]),
|
||||||
applicationType: z.literal("application"),
|
applicationType: z.literal("application"),
|
||||||
@@ -12,8 +12,8 @@ export const deployJobSchema = z.discriminatedUnion("applicationType", [
|
|||||||
}),
|
}),
|
||||||
z.object({
|
z.object({
|
||||||
composeId: z.string(),
|
composeId: z.string(),
|
||||||
titleLog: z.string().optional(),
|
titleLog: z.string(),
|
||||||
descriptionLog: z.string().optional(),
|
descriptionLog: z.string(),
|
||||||
server: z.boolean().optional(),
|
server: z.boolean().optional(),
|
||||||
type: z.enum(["deploy", "redeploy"]),
|
type: z.enum(["deploy", "redeploy"]),
|
||||||
applicationType: z.literal("compose"),
|
applicationType: z.literal("compose"),
|
||||||
@@ -22,8 +22,8 @@ export const deployJobSchema = z.discriminatedUnion("applicationType", [
|
|||||||
z.object({
|
z.object({
|
||||||
applicationId: z.string(),
|
applicationId: z.string(),
|
||||||
previewDeploymentId: z.string(),
|
previewDeploymentId: z.string(),
|
||||||
titleLog: z.string().optional(),
|
titleLog: z.string(),
|
||||||
descriptionLog: z.string().optional(),
|
descriptionLog: z.string(),
|
||||||
server: z.boolean().optional(),
|
server: z.boolean().optional(),
|
||||||
type: z.enum(["deploy"]),
|
type: z.enum(["deploy"]),
|
||||||
applicationType: z.literal("application-preview"),
|
applicationType: z.literal("application-preview"),
|
||||||
@@ -32,16 +32,3 @@ export const deployJobSchema = z.discriminatedUnion("applicationType", [
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
export type DeployJob = z.infer<typeof deployJobSchema>;
|
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") {
|
if (job.type === "redeploy") {
|
||||||
await rebuildRemoteApplication({
|
await rebuildRemoteApplication({
|
||||||
applicationId: job.applicationId,
|
applicationId: job.applicationId,
|
||||||
titleLog: job.titleLog || "Rebuild deployment",
|
titleLog: job.titleLog,
|
||||||
descriptionLog: job.descriptionLog || "",
|
descriptionLog: job.descriptionLog,
|
||||||
});
|
});
|
||||||
} else if (job.type === "deploy") {
|
} else if (job.type === "deploy") {
|
||||||
await deployRemoteApplication({
|
await deployRemoteApplication({
|
||||||
applicationId: job.applicationId,
|
applicationId: job.applicationId,
|
||||||
titleLog: job.titleLog || "Manual deployment",
|
titleLog: job.titleLog,
|
||||||
descriptionLog: job.descriptionLog || "",
|
descriptionLog: job.descriptionLog,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -38,14 +38,14 @@ export const deploy = async (job: DeployJob) => {
|
|||||||
if (job.type === "redeploy") {
|
if (job.type === "redeploy") {
|
||||||
await rebuildRemoteCompose({
|
await rebuildRemoteCompose({
|
||||||
composeId: job.composeId,
|
composeId: job.composeId,
|
||||||
titleLog: job.titleLog || "Rebuild deployment",
|
titleLog: job.titleLog,
|
||||||
descriptionLog: job.descriptionLog || "",
|
descriptionLog: job.descriptionLog,
|
||||||
});
|
});
|
||||||
} else if (job.type === "deploy") {
|
} else if (job.type === "deploy") {
|
||||||
await deployRemoteCompose({
|
await deployRemoteCompose({
|
||||||
composeId: job.composeId,
|
composeId: job.composeId,
|
||||||
titleLog: job.titleLog || "Manual deployment",
|
titleLog: job.titleLog,
|
||||||
descriptionLog: job.descriptionLog || "",
|
descriptionLog: job.descriptionLog,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -57,8 +57,8 @@ export const deploy = async (job: DeployJob) => {
|
|||||||
if (job.type === "deploy") {
|
if (job.type === "deploy") {
|
||||||
await deployRemotePreviewApplication({
|
await deployRemotePreviewApplication({
|
||||||
applicationId: job.applicationId,
|
applicationId: job.applicationId,
|
||||||
titleLog: job.titleLog || "Preview Deployment",
|
titleLog: job.titleLog,
|
||||||
descriptionLog: job.descriptionLog || "",
|
descriptionLog: job.descriptionLog,
|
||||||
previewDeploymentId: job.previewDeploymentId,
|
previewDeploymentId: job.previewDeploymentId,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -56,21 +56,13 @@ const baseApp: ApplicationNested = {
|
|||||||
previewPort: 3000,
|
previewPort: 3000,
|
||||||
previewLimit: 0,
|
previewLimit: 0,
|
||||||
previewWildcard: "",
|
previewWildcard: "",
|
||||||
environment: {
|
project: {
|
||||||
env: "",
|
env: "",
|
||||||
environmentId: "",
|
organizationId: "",
|
||||||
name: "",
|
name: "",
|
||||||
createdAt: "",
|
|
||||||
description: "",
|
description: "",
|
||||||
|
createdAt: "",
|
||||||
projectId: "",
|
projectId: "",
|
||||||
project: {
|
|
||||||
env: "",
|
|
||||||
organizationId: "",
|
|
||||||
name: "",
|
|
||||||
description: "",
|
|
||||||
createdAt: "",
|
|
||||||
projectId: "",
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
buildArgs: null,
|
buildArgs: null,
|
||||||
buildPath: "/",
|
buildPath: "/",
|
||||||
@@ -100,7 +92,6 @@ const baseApp: ApplicationNested = {
|
|||||||
dockerfile: null,
|
dockerfile: null,
|
||||||
dockerImage: null,
|
dockerImage: null,
|
||||||
dropBuildPath: null,
|
dropBuildPath: null,
|
||||||
environmentId: "",
|
|
||||||
enabled: null,
|
enabled: null,
|
||||||
env: null,
|
env: null,
|
||||||
healthCheckSwarm: null,
|
healthCheckSwarm: null,
|
||||||
@@ -115,6 +106,7 @@ const baseApp: ApplicationNested = {
|
|||||||
password: null,
|
password: null,
|
||||||
placementSwarm: null,
|
placementSwarm: null,
|
||||||
ports: [],
|
ports: [],
|
||||||
|
projectId: "",
|
||||||
publishDirectory: null,
|
publishDirectory: null,
|
||||||
isStaticSpa: null,
|
isStaticSpa: null,
|
||||||
redirects: [],
|
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,
|
previewLimit: 0,
|
||||||
previewCustomCertResolver: null,
|
previewCustomCertResolver: null,
|
||||||
previewWildcard: "",
|
previewWildcard: "",
|
||||||
environmentId: "",
|
project: {
|
||||||
environment: {
|
|
||||||
env: "",
|
env: "",
|
||||||
environmentId: "",
|
organizationId: "",
|
||||||
name: "",
|
name: "",
|
||||||
createdAt: "",
|
|
||||||
description: "",
|
description: "",
|
||||||
|
createdAt: "",
|
||||||
projectId: "",
|
projectId: "",
|
||||||
project: {
|
|
||||||
env: "",
|
|
||||||
organizationId: "",
|
|
||||||
name: "",
|
|
||||||
description: "",
|
|
||||||
createdAt: "",
|
|
||||||
projectId: "",
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
buildPath: "/",
|
buildPath: "/",
|
||||||
gitlabPathNamespace: "",
|
gitlabPathNamespace: "",
|
||||||
@@ -94,6 +85,7 @@ const baseApp: ApplicationNested = {
|
|||||||
password: null,
|
password: null,
|
||||||
placementSwarm: null,
|
placementSwarm: null,
|
||||||
ports: [],
|
ports: [],
|
||||||
|
projectId: "",
|
||||||
publishDirectory: null,
|
publishDirectory: null,
|
||||||
isStaticSpa: null,
|
isStaticSpa: null,
|
||||||
redirects: [],
|
redirects: [],
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { Clock, Loader2, RefreshCcw, RocketIcon, Settings } from "lucide-react";
|
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 { toast } from "sonner";
|
||||||
import { AlertBlock } from "@/components/shared/alert-block";
|
|
||||||
import { DateTooltip } from "@/components/shared/date-tooltip";
|
import { DateTooltip } from "@/components/shared/date-tooltip";
|
||||||
import { DialogAction } from "@/components/shared/dialog-action";
|
import { DialogAction } from "@/components/shared/dialog-action";
|
||||||
import { StatusTooltip } from "@/components/shared/status-tooltip";
|
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 } =
|
const { mutateAsync: rollback, isLoading: isRollingBack } =
|
||||||
api.rollback.rollback.useMutation();
|
api.rollback.rollback.useMutation();
|
||||||
const { mutateAsync: killProcess, isLoading: isKillingProcess } =
|
const { mutateAsync: killProcess, isLoading: isKillingProcess } =
|
||||||
api.deployment.killProcess.useMutation();
|
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("");
|
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(() => {
|
useEffect(() => {
|
||||||
setUrl(document.location.origin);
|
setUrl(document.location.origin);
|
||||||
}, []);
|
}, []);
|
||||||
@@ -114,7 +77,7 @@ export const ShowDeployments = ({
|
|||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<CardTitle className="text-xl">Deployments</CardTitle>
|
<CardTitle className="text-xl">Deployments</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription>
|
||||||
See the last 10 deployments for this {type}
|
See all the 10 last deployments for this {type}
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-row items-center gap-2">
|
<div className="flex flex-row items-center gap-2">
|
||||||
@@ -131,54 +94,6 @@ export const ShowDeployments = ({
|
|||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="flex flex-col gap-4">
|
<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 && (
|
{refreshToken && (
|
||||||
<div className="flex flex-col gap-2 text-sm">
|
<div className="flex flex-col gap-2 text-sm">
|
||||||
<span>
|
<span>
|
||||||
@@ -189,9 +104,7 @@ export const ShowDeployments = ({
|
|||||||
<span>Webhook URL: </span>
|
<span>Webhook URL: </span>
|
||||||
<div className="flex flex-row items-center gap-2">
|
<div className="flex flex-row items-center gap-2">
|
||||||
<span className="break-all text-muted-foreground">
|
<span className="break-all text-muted-foreground">
|
||||||
{`${url}/api/deploy${
|
{`${url}/api/deploy${type === "compose" ? "/compose" : ""}/${refreshToken}`}
|
||||||
type === "compose" ? "/compose" : ""
|
|
||||||
}/${refreshToken}`}
|
|
||||||
</span>
|
</span>
|
||||||
{(type === "application" || type === "compose") && (
|
{(type === "application" || type === "compose") && (
|
||||||
<RefreshToken id={id} type={type} />
|
<RefreshToken id={id} type={type} />
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ export const ShowGeneralApplication = ({ applicationId }: Props) => {
|
|||||||
toast.success("Application deployed successfully");
|
toast.success("Application deployed successfully");
|
||||||
refetch();
|
refetch();
|
||||||
router.push(
|
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(() => {
|
.catch(() => {
|
||||||
@@ -79,7 +79,7 @@ export const ShowGeneralApplication = ({ applicationId }: Props) => {
|
|||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
variant="default"
|
variant="default"
|
||||||
isLoading={data?.applicationStatus === "running"}
|
// isLoading={data?.applicationStatus === "running"}
|
||||||
className="flex items-center gap-1.5 group focus-visible:ring-2 focus-visible:ring-offset-2"
|
className="flex items-center gap-1.5 group focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||||
>
|
>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ export const ShowSchedules = ({ id, scheduleType = "application" }: Props) => {
|
|||||||
return (
|
return (
|
||||||
<Card className="border px-6 shadow-none bg-transparent h-full min-h-[50vh]">
|
<Card className="border px-6 shadow-none bg-transparent h-full min-h-[50vh]">
|
||||||
<CardHeader className="px-0">
|
<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">
|
<div className="flex flex-col gap-2">
|
||||||
<CardTitle className="text-xl font-bold flex items-center gap-2">
|
<CardTitle className="text-xl font-bold flex items-center gap-2">
|
||||||
Scheduled Tasks
|
Scheduled Tasks
|
||||||
@@ -91,15 +91,15 @@ export const ShowSchedules = ({ id, scheduleType = "application" }: Props) => {
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={schedule.scheduleId}
|
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 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" />
|
<Clock className="size-4 text-primary/70" />
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-1.5">
|
<div className="space-y-1.5">
|
||||||
<div className="flex items-center gap-2 flex-wrap">
|
<div className="flex items-center gap-2">
|
||||||
<h3 className="text-sm font-medium leading-none [overflow-wrap:anywhere] line-clamp-3">
|
<h3 className="text-sm font-medium leading-none">
|
||||||
{schedule.name}
|
{schedule.name}
|
||||||
</h3>
|
</h3>
|
||||||
<Badge
|
<Badge
|
||||||
@@ -109,7 +109,7 @@ export const ShowSchedules = ({ id, scheduleType = "application" }: Props) => {
|
|||||||
{schedule.enabled ? "Enabled" : "Disabled"}
|
{schedule.enabled ? "Enabled" : "Disabled"}
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</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
|
<Badge
|
||||||
variant="outline"
|
variant="outline"
|
||||||
className="font-mono text-[10px] bg-transparent"
|
className="font-mono text-[10px] bg-transparent"
|
||||||
@@ -142,7 +142,7 @@ export const ShowSchedules = ({ id, scheduleType = "application" }: Props) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center gap-0.5 md:gap-1.5">
|
<div className="flex items-center gap-1.5">
|
||||||
<ShowDeploymentsModal
|
<ShowDeploymentsModal
|
||||||
id={schedule.scheduleId}
|
id={schedule.scheduleId}
|
||||||
type="schedule"
|
type="schedule"
|
||||||
@@ -226,7 +226,7 @@ export const ShowSchedules = ({ id, scheduleType = "application" }: Props) => {
|
|||||||
})}
|
})}
|
||||||
</div>
|
</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" />
|
<Clock className="size-8 mb-4 text-muted-foreground" />
|
||||||
<p className="text-lg font-medium text-muted-foreground">
|
<p className="text-lg font-medium text-muted-foreground">
|
||||||
No scheduled tasks
|
No scheduled tasks
|
||||||
|
|||||||
@@ -101,9 +101,7 @@ export const DeleteService = ({ id, type }: Props) => {
|
|||||||
deleteVolumes,
|
deleteVolumes,
|
||||||
})
|
})
|
||||||
.then((result) => {
|
.then((result) => {
|
||||||
push(
|
push(`/dashboard/project/${result?.projectId}`);
|
||||||
`/dashboard/project/${result?.environment?.projectId}/environment/${result?.environment?.environmentId}`,
|
|
||||||
);
|
|
||||||
toast.success("deleted successfully");
|
toast.success("deleted successfully");
|
||||||
setIsOpen(false);
|
setIsOpen(false);
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ export const ComposeActions = ({ composeId }: Props) => {
|
|||||||
toast.success("Compose deployed successfully");
|
toast.success("Compose deployed successfully");
|
||||||
refetch();
|
refetch();
|
||||||
router.push(
|
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(() => {
|
.catch(() => {
|
||||||
|
|||||||
@@ -102,9 +102,9 @@ export const ShowExternalMariadbCredentials = ({ mariadbId }: Props) => {
|
|||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="text-xl">External Credentials</CardTitle>
|
<CardTitle className="text-xl">External Credentials</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription>
|
||||||
In order to make the database reachable through the internet, you
|
In order to make the database reachable trought internet is
|
||||||
must set a port and ensure that the port is not being used by
|
required to set a port, make sure the port is not used by another
|
||||||
another application or database
|
application or database
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="flex w-full flex-col gap-4">
|
<CardContent className="flex w-full flex-col gap-4">
|
||||||
|
|||||||
@@ -102,9 +102,9 @@ export const ShowExternalMongoCredentials = ({ mongoId }: Props) => {
|
|||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="text-xl">External Credentials</CardTitle>
|
<CardTitle className="text-xl">External Credentials</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription>
|
||||||
In order to make the database reachable through the internet, you
|
In order to make the database reachable trought internet is
|
||||||
must set a port and ensure that the port is not being used by
|
required to set a port, make sure the port is not used by another
|
||||||
another application or database
|
application or database
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="flex w-full flex-col gap-4">
|
<CardContent className="flex w-full flex-col gap-4">
|
||||||
|
|||||||
@@ -102,9 +102,9 @@ export const ShowExternalMysqlCredentials = ({ mysqlId }: Props) => {
|
|||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="text-xl">External Credentials</CardTitle>
|
<CardTitle className="text-xl">External Credentials</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription>
|
||||||
In order to make the database reachable through the internet, you
|
In order to make the database reachable trought internet is
|
||||||
must set a port and ensure that the port is not being used by
|
required to set a port, make sure the port is not used by another
|
||||||
another application or database
|
application or database
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="flex w-full flex-col gap-4">
|
<CardContent className="flex w-full flex-col gap-4">
|
||||||
|
|||||||
@@ -104,9 +104,9 @@ export const ShowExternalPostgresCredentials = ({ postgresId }: Props) => {
|
|||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="text-xl">External Credentials</CardTitle>
|
<CardTitle className="text-xl">External Credentials</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription>
|
||||||
In order to make the database reachable through the internet, you
|
In order to make the database reachable trought internet is
|
||||||
must set a port and ensure that the port is not being used by
|
required to set a port, make sure the port is not used by another
|
||||||
another application or database
|
application or database
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="flex w-full flex-col gap-4">
|
<CardContent className="flex w-full flex-col gap-4">
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { TemplateGenerator } from "@/components/dashboard/project/ai/template-generator";
|
import { TemplateGenerator } from "@/components/dashboard/project/ai/template-generator";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
environmentId: string;
|
projectId: string;
|
||||||
projectName?: string;
|
projectName?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const AddAiAssistant = ({ environmentId }: Props) => {
|
export const AddAiAssistant = ({ projectId }: Props) => {
|
||||||
return <TemplateGenerator environmentId={environmentId} />;
|
return <TemplateGenerator projectId={projectId} />;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -64,11 +64,11 @@ const AddTemplateSchema = z.object({
|
|||||||
type AddTemplate = z.infer<typeof AddTemplateSchema>;
|
type AddTemplate = z.infer<typeof AddTemplateSchema>;
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
environmentId: string;
|
projectId: string;
|
||||||
projectName?: string;
|
projectName?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const AddApplication = ({ environmentId, projectName }: Props) => {
|
export const AddApplication = ({ projectId, projectName }: Props) => {
|
||||||
const utils = api.useUtils();
|
const utils = api.useUtils();
|
||||||
const { data: isCloud } = api.settings.isCloud.useQuery();
|
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||||
const [visible, setVisible] = useState(false);
|
const [visible, setVisible] = useState(false);
|
||||||
@@ -76,10 +76,6 @@ export const AddApplication = ({ environmentId, projectName }: Props) => {
|
|||||||
const { data: servers } = api.server.withSSHKey.useQuery();
|
const { data: servers } = api.server.withSSHKey.useQuery();
|
||||||
|
|
||||||
const hasServers = servers && servers.length > 0;
|
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 } =
|
const { mutateAsync, isLoading, error, isError } =
|
||||||
api.application.create.useMutation();
|
api.application.create.useMutation();
|
||||||
@@ -98,15 +94,15 @@ export const AddApplication = ({ environmentId, projectName }: Props) => {
|
|||||||
name: data.name,
|
name: data.name,
|
||||||
appName: data.appName,
|
appName: data.appName,
|
||||||
description: data.description,
|
description: data.description,
|
||||||
serverId: data.serverId === "dokploy" ? undefined : data.serverId,
|
projectId,
|
||||||
environmentId,
|
serverId: data.serverId,
|
||||||
})
|
})
|
||||||
.then(async () => {
|
.then(async () => {
|
||||||
toast.success("Service Created");
|
toast.success("Service Created");
|
||||||
form.reset();
|
form.reset();
|
||||||
setVisible(false);
|
setVisible(false);
|
||||||
await utils.environment.one.invalidate({
|
await utils.project.one.invalidate({
|
||||||
environmentId,
|
projectId,
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
@@ -161,7 +157,7 @@ export const AddApplication = ({ environmentId, projectName }: Props) => {
|
|||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
{shouldShowServerDropdown && (
|
{hasServers && (
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="serverId"
|
name="serverId"
|
||||||
@@ -190,27 +186,13 @@ export const AddApplication = ({ environmentId, projectName }: Props) => {
|
|||||||
|
|
||||||
<Select
|
<Select
|
||||||
onValueChange={field.onChange}
|
onValueChange={field.onChange}
|
||||||
defaultValue={
|
defaultValue={field.value}
|
||||||
field.value || (!isCloud ? "dokploy" : undefined)
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<SelectTrigger>
|
<SelectTrigger>
|
||||||
<SelectValue
|
<SelectValue placeholder="Select a Server" />
|
||||||
placeholder={!isCloud ? "Dokploy" : "Select a Server"}
|
|
||||||
/>
|
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectGroup>
|
<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) => (
|
{servers?.map((server) => (
|
||||||
<SelectItem
|
<SelectItem
|
||||||
key={server.serverId}
|
key={server.serverId}
|
||||||
@@ -224,9 +206,7 @@ export const AddApplication = ({ environmentId, projectName }: Props) => {
|
|||||||
</span>
|
</span>
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
))}
|
))}
|
||||||
<SelectLabel>
|
<SelectLabel>Servers ({servers?.length})</SelectLabel>
|
||||||
Servers ({servers?.length + (!isCloud ? 1 : 0)})
|
|
||||||
</SelectLabel>
|
|
||||||
</SelectGroup>
|
</SelectGroup>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
|
|||||||
@@ -65,11 +65,11 @@ const AddComposeSchema = z.object({
|
|||||||
type AddCompose = z.infer<typeof AddComposeSchema>;
|
type AddCompose = z.infer<typeof AddComposeSchema>;
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
environmentId: string;
|
projectId: string;
|
||||||
projectName?: string;
|
projectName?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const AddCompose = ({ environmentId, projectName }: Props) => {
|
export const AddCompose = ({ projectId, projectName }: Props) => {
|
||||||
const utils = api.useUtils();
|
const utils = api.useUtils();
|
||||||
const [visible, setVisible] = useState(false);
|
const [visible, setVisible] = useState(false);
|
||||||
const slug = slugify(projectName);
|
const slug = slugify(projectName);
|
||||||
@@ -78,14 +78,7 @@ export const AddCompose = ({ environmentId, projectName }: Props) => {
|
|||||||
const { mutateAsync, isLoading, error, isError } =
|
const { mutateAsync, isLoading, error, isError } =
|
||||||
api.compose.create.useMutation();
|
api.compose.create.useMutation();
|
||||||
|
|
||||||
// Get environment data to extract projectId
|
|
||||||
const { data: environment } = api.environment.one.useQuery({ environmentId });
|
|
||||||
|
|
||||||
const hasServers = servers && servers.length > 0;
|
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>({
|
const form = useForm<AddCompose>({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
@@ -105,17 +98,16 @@ export const AddCompose = ({ environmentId, projectName }: Props) => {
|
|||||||
await mutateAsync({
|
await mutateAsync({
|
||||||
name: data.name,
|
name: data.name,
|
||||||
description: data.description,
|
description: data.description,
|
||||||
environmentId,
|
projectId,
|
||||||
composeType: data.composeType,
|
composeType: data.composeType,
|
||||||
appName: data.appName,
|
appName: data.appName,
|
||||||
serverId: data.serverId === "dokploy" ? undefined : data.serverId,
|
serverId: data.serverId,
|
||||||
})
|
})
|
||||||
.then(async () => {
|
.then(async () => {
|
||||||
toast.success("Compose Created");
|
toast.success("Compose Created");
|
||||||
setVisible(false);
|
setVisible(false);
|
||||||
// Invalidate the project query to refresh the environment data
|
await utils.project.one.invalidate({
|
||||||
await utils.environment.one.invalidate({
|
projectId,
|
||||||
environmentId,
|
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
@@ -173,7 +165,7 @@ export const AddCompose = ({ environmentId, projectName }: Props) => {
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{shouldShowServerDropdown && (
|
{hasServers && (
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="serverId"
|
name="serverId"
|
||||||
@@ -202,27 +194,13 @@ export const AddCompose = ({ environmentId, projectName }: Props) => {
|
|||||||
|
|
||||||
<Select
|
<Select
|
||||||
onValueChange={field.onChange}
|
onValueChange={field.onChange}
|
||||||
defaultValue={
|
defaultValue={field.value}
|
||||||
field.value || (!isCloud ? "dokploy" : undefined)
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<SelectTrigger>
|
<SelectTrigger>
|
||||||
<SelectValue
|
<SelectValue placeholder="Select a Server" />
|
||||||
placeholder={!isCloud ? "Dokploy" : "Select a Server"}
|
|
||||||
/>
|
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectGroup>
|
<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) => (
|
{servers?.map((server) => (
|
||||||
<SelectItem
|
<SelectItem
|
||||||
key={server.serverId}
|
key={server.serverId}
|
||||||
@@ -236,9 +214,7 @@ export const AddCompose = ({ environmentId, projectName }: Props) => {
|
|||||||
</span>
|
</span>
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
))}
|
))}
|
||||||
<SelectLabel>
|
<SelectLabel>Servers ({servers?.length})</SelectLabel>
|
||||||
Servers ({servers?.length + (!isCloud ? 1 : 0)})
|
|
||||||
</SelectLabel>
|
|
||||||
</SelectGroup>
|
</SelectGroup>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
|
|||||||
@@ -83,12 +83,7 @@ const baseDatabaseSchema = z.object({
|
|||||||
message:
|
message:
|
||||||
"App name supports lowercase letters, numbers, '-' and can only start and end letters, and does not support continuous '-'",
|
"App name supports lowercase letters, numbers, '-' and can only start and end letters, and does not support continuous '-'",
|
||||||
}),
|
}),
|
||||||
databasePassword: z
|
databasePassword: z.string(),
|
||||||
.string()
|
|
||||||
.regex(/^[a-zA-Z0-9@#%^&*()_+\-=[\]{}|;:,.<>?~`]*$/, {
|
|
||||||
message:
|
|
||||||
"Password contains invalid characters. Please avoid: $ ! ' \" \\ / and space characters for database compatibility",
|
|
||||||
}),
|
|
||||||
dockerImage: z.string(),
|
dockerImage: z.string(),
|
||||||
description: z.string().nullable(),
|
description: z.string().nullable(),
|
||||||
serverId: z.string().nullable(),
|
serverId: z.string().nullable(),
|
||||||
@@ -117,13 +112,7 @@ const mySchema = z.discriminatedUnion("type", [
|
|||||||
z
|
z
|
||||||
.object({
|
.object({
|
||||||
type: z.literal("mysql"),
|
type: z.literal("mysql"),
|
||||||
databaseRootPassword: z
|
databaseRootPassword: z.string().default(""),
|
||||||
.string()
|
|
||||||
.regex(/^[a-zA-Z0-9@#%^&*()_+\-=[\]{}|;:,.<>?~`]*$/, {
|
|
||||||
message:
|
|
||||||
"Password contains invalid characters. Please avoid: $ ! ' \" \\ / and space characters for database compatibility",
|
|
||||||
})
|
|
||||||
.optional(),
|
|
||||||
databaseUser: z.string().default("mysql"),
|
databaseUser: z.string().default("mysql"),
|
||||||
databaseName: z.string().default("mysql"),
|
databaseName: z.string().default("mysql"),
|
||||||
})
|
})
|
||||||
@@ -132,13 +121,7 @@ const mySchema = z.discriminatedUnion("type", [
|
|||||||
.object({
|
.object({
|
||||||
type: z.literal("mariadb"),
|
type: z.literal("mariadb"),
|
||||||
dockerImage: z.string().default("mariadb:4"),
|
dockerImage: z.string().default("mariadb:4"),
|
||||||
databaseRootPassword: z
|
databaseRootPassword: z.string().default(""),
|
||||||
.string()
|
|
||||||
.regex(/^[a-zA-Z0-9@#%^&*()_+\-=[\]{}|;:,.<>?~`]*$/, {
|
|
||||||
message:
|
|
||||||
"Password contains invalid characters. Please avoid: $ ! ' \" \\ / and space characters for database compatibility",
|
|
||||||
})
|
|
||||||
.optional(),
|
|
||||||
databaseUser: z.string().default("mariadb"),
|
databaseUser: z.string().default("mariadb"),
|
||||||
databaseName: z.string().default("mariadb"),
|
databaseName: z.string().default("mariadb"),
|
||||||
})
|
})
|
||||||
@@ -171,15 +154,14 @@ const databasesMap = {
|
|||||||
type AddDatabase = z.infer<typeof mySchema>;
|
type AddDatabase = z.infer<typeof mySchema>;
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
environmentId: string;
|
projectId: string;
|
||||||
projectName?: string;
|
projectName?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const AddDatabase = ({ environmentId, projectName }: Props) => {
|
export const AddDatabase = ({ projectId, projectName }: Props) => {
|
||||||
const utils = api.useUtils();
|
const utils = api.useUtils();
|
||||||
const [visible, setVisible] = useState(false);
|
const [visible, setVisible] = useState(false);
|
||||||
const slug = slugify(projectName);
|
const slug = slugify(projectName);
|
||||||
const { data: isCloud } = api.settings.isCloud.useQuery();
|
|
||||||
const { data: servers } = api.server.withSSHKey.useQuery();
|
const { data: servers } = api.server.withSSHKey.useQuery();
|
||||||
const postgresMutation = api.postgres.create.useMutation();
|
const postgresMutation = api.postgres.create.useMutation();
|
||||||
const mongoMutation = api.mongo.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 mariadbMutation = api.mariadb.create.useMutation();
|
||||||
const mysqlMutation = api.mysql.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;
|
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>({
|
const form = useForm<AddDatabase>({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
@@ -228,8 +203,8 @@ export const AddDatabase = ({ environmentId, projectName }: Props) => {
|
|||||||
name: data.name,
|
name: data.name,
|
||||||
appName: data.appName,
|
appName: data.appName,
|
||||||
dockerImage: defaultDockerImage,
|
dockerImage: defaultDockerImage,
|
||||||
serverId: data.serverId === "dokploy" ? undefined : data.serverId,
|
projectId,
|
||||||
environmentId,
|
serverId: data.serverId,
|
||||||
description: data.description,
|
description: data.description,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -241,7 +216,7 @@ export const AddDatabase = ({ environmentId, projectName }: Props) => {
|
|||||||
|
|
||||||
databaseUser:
|
databaseUser:
|
||||||
data.databaseUser || databasesUserDefaultPlaceholder[data.type],
|
data.databaseUser || databasesUserDefaultPlaceholder[data.type],
|
||||||
serverId: data.serverId === "dokploy" ? null : data.serverId,
|
serverId: data.serverId,
|
||||||
});
|
});
|
||||||
} else if (data.type === "mongo") {
|
} else if (data.type === "mongo") {
|
||||||
promise = mongoMutation.mutateAsync({
|
promise = mongoMutation.mutateAsync({
|
||||||
@@ -249,24 +224,25 @@ export const AddDatabase = ({ environmentId, projectName }: Props) => {
|
|||||||
databasePassword: data.databasePassword,
|
databasePassword: data.databasePassword,
|
||||||
databaseUser:
|
databaseUser:
|
||||||
data.databaseUser || databasesUserDefaultPlaceholder[data.type],
|
data.databaseUser || databasesUserDefaultPlaceholder[data.type],
|
||||||
serverId: data.serverId === "dokploy" ? null : data.serverId,
|
serverId: data.serverId,
|
||||||
replicaSets: data.replicaSets,
|
replicaSets: data.replicaSets,
|
||||||
});
|
});
|
||||||
} else if (data.type === "redis") {
|
} else if (data.type === "redis") {
|
||||||
promise = redisMutation.mutateAsync({
|
promise = redisMutation.mutateAsync({
|
||||||
...commonParams,
|
...commonParams,
|
||||||
databasePassword: data.databasePassword,
|
databasePassword: data.databasePassword,
|
||||||
serverId: data.serverId === "dokploy" ? null : data.serverId,
|
serverId: data.serverId,
|
||||||
|
projectId,
|
||||||
});
|
});
|
||||||
} else if (data.type === "mariadb") {
|
} else if (data.type === "mariadb") {
|
||||||
promise = mariadbMutation.mutateAsync({
|
promise = mariadbMutation.mutateAsync({
|
||||||
...commonParams,
|
...commonParams,
|
||||||
databasePassword: data.databasePassword,
|
databasePassword: data.databasePassword,
|
||||||
databaseRootPassword: data.databaseRootPassword || "",
|
databaseRootPassword: data.databaseRootPassword,
|
||||||
databaseName: data.databaseName || "mariadb",
|
databaseName: data.databaseName || "mariadb",
|
||||||
databaseUser:
|
databaseUser:
|
||||||
data.databaseUser || databasesUserDefaultPlaceholder[data.type],
|
data.databaseUser || databasesUserDefaultPlaceholder[data.type],
|
||||||
serverId: data.serverId === "dokploy" ? null : data.serverId,
|
serverId: data.serverId,
|
||||||
});
|
});
|
||||||
} else if (data.type === "mysql") {
|
} else if (data.type === "mysql") {
|
||||||
promise = mysqlMutation.mutateAsync({
|
promise = mysqlMutation.mutateAsync({
|
||||||
@@ -275,8 +251,8 @@ export const AddDatabase = ({ environmentId, projectName }: Props) => {
|
|||||||
databaseName: data.databaseName || "mysql",
|
databaseName: data.databaseName || "mysql",
|
||||||
databaseUser:
|
databaseUser:
|
||||||
data.databaseUser || databasesUserDefaultPlaceholder[data.type],
|
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: "",
|
databaseUser: "",
|
||||||
});
|
});
|
||||||
setVisible(false);
|
setVisible(false);
|
||||||
// Invalidate the project query to refresh the environment data
|
await utils.project.one.invalidate({
|
||||||
await utils.environment.one.invalidate({
|
projectId,
|
||||||
environmentId,
|
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
@@ -407,7 +382,7 @@ export const AddDatabase = ({ environmentId, projectName }: Props) => {
|
|||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
{shouldShowServerDropdown && (
|
{hasServers && (
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="serverId"
|
name="serverId"
|
||||||
@@ -416,29 +391,13 @@ export const AddDatabase = ({ environmentId, projectName }: Props) => {
|
|||||||
<FormLabel>Select a Server</FormLabel>
|
<FormLabel>Select a Server</FormLabel>
|
||||||
<Select
|
<Select
|
||||||
onValueChange={field.onChange}
|
onValueChange={field.onChange}
|
||||||
defaultValue={
|
defaultValue={field.value || ""}
|
||||||
field.value || (!isCloud ? "dokploy" : undefined)
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<SelectTrigger>
|
<SelectTrigger>
|
||||||
<SelectValue
|
<SelectValue placeholder="Select a Server" />
|
||||||
placeholder={
|
|
||||||
!isCloud ? "Dokploy" : "Select a Server"
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectGroup>
|
<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) => (
|
{servers?.map((server) => (
|
||||||
<SelectItem
|
<SelectItem
|
||||||
key={server.serverId}
|
key={server.serverId}
|
||||||
@@ -448,7 +407,7 @@ export const AddDatabase = ({ environmentId, projectName }: Props) => {
|
|||||||
</SelectItem>
|
</SelectItem>
|
||||||
))}
|
))}
|
||||||
<SelectLabel>
|
<SelectLabel>
|
||||||
Servers ({servers?.length + (!isCloud ? 1 : 0)})
|
Servers ({servers?.length})
|
||||||
</SelectLabel>
|
</SelectLabel>
|
||||||
</SelectGroup>
|
</SelectGroup>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
|
|||||||
@@ -73,11 +73,11 @@ import { api } from "@/utils/api";
|
|||||||
const TEMPLATE_BASE_URL_KEY = "dokploy_template_base_url";
|
const TEMPLATE_BASE_URL_KEY = "dokploy_template_base_url";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
environmentId: string;
|
projectId: string;
|
||||||
baseUrl?: string;
|
baseUrl?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const AddTemplate = ({ environmentId, baseUrl }: Props) => {
|
export const AddTemplate = ({ projectId, baseUrl }: Props) => {
|
||||||
const [query, setQuery] = useState("");
|
const [query, setQuery] = useState("");
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const [viewMode, setViewMode] = useState<"detailed" | "icon">("detailed");
|
const [viewMode, setViewMode] = useState<"detailed" | "icon">("detailed");
|
||||||
@@ -91,9 +91,6 @@ export const AddTemplate = ({ environmentId, baseUrl }: Props) => {
|
|||||||
return undefined;
|
return undefined;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Get environment data to extract projectId
|
|
||||||
const { data: environment } = api.environment.one.useQuery({ environmentId });
|
|
||||||
|
|
||||||
// Save to localStorage when customBaseUrl changes
|
// Save to localStorage when customBaseUrl changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (customBaseUrl) {
|
if (customBaseUrl) {
|
||||||
@@ -141,10 +138,6 @@ export const AddTemplate = ({ environmentId, baseUrl }: Props) => {
|
|||||||
}) || [];
|
}) || [];
|
||||||
|
|
||||||
const hasServers = servers && servers.length > 0;
|
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 (
|
return (
|
||||||
<Dialog open={open} onOpenChange={setOpen}>
|
<Dialog open={open} onOpenChange={setOpen}>
|
||||||
@@ -434,7 +427,7 @@ export const AddTemplate = ({ environmentId, baseUrl }: Props) => {
|
|||||||
project.
|
project.
|
||||||
</AlertDialogDescription>
|
</AlertDialogDescription>
|
||||||
|
|
||||||
{shouldShowServerDropdown && (
|
{hasServers && (
|
||||||
<div>
|
<div>
|
||||||
<TooltipProvider delayDuration={0}>
|
<TooltipProvider delayDuration={0}>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
@@ -463,29 +456,12 @@ export const AddTemplate = ({ environmentId, baseUrl }: Props) => {
|
|||||||
onValueChange={(e) => {
|
onValueChange={(e) => {
|
||||||
setServerId(e);
|
setServerId(e);
|
||||||
}}
|
}}
|
||||||
defaultValue={
|
|
||||||
!isCloud ? "dokploy" : undefined
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<SelectTrigger>
|
<SelectTrigger>
|
||||||
<SelectValue
|
<SelectValue placeholder="Select a Server" />
|
||||||
placeholder={
|
|
||||||
!isCloud ? "Dokploy" : "Select a Server"
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectGroup>
|
<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) => (
|
{servers?.map((server) => (
|
||||||
<SelectItem
|
<SelectItem
|
||||||
key={server.serverId}
|
key={server.serverId}
|
||||||
@@ -500,8 +476,7 @@ export const AddTemplate = ({ environmentId, baseUrl }: Props) => {
|
|||||||
</SelectItem>
|
</SelectItem>
|
||||||
))}
|
))}
|
||||||
<SelectLabel>
|
<SelectLabel>
|
||||||
Servers (
|
Servers ({servers?.length})
|
||||||
{servers?.length + (!isCloud ? 1 : 0)})
|
|
||||||
</SelectLabel>
|
</SelectLabel>
|
||||||
</SelectGroup>
|
</SelectGroup>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
@@ -515,20 +490,16 @@ export const AddTemplate = ({ environmentId, baseUrl }: Props) => {
|
|||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
const promise = mutateAsync({
|
const promise = mutateAsync({
|
||||||
serverId:
|
projectId,
|
||||||
serverId === "dokploy"
|
serverId: serverId || undefined,
|
||||||
? undefined
|
|
||||||
: serverId,
|
|
||||||
environmentId,
|
|
||||||
id: template.id,
|
id: template.id,
|
||||||
baseUrl: customBaseUrl,
|
baseUrl: customBaseUrl,
|
||||||
});
|
});
|
||||||
toast.promise(promise, {
|
toast.promise(promise, {
|
||||||
loading: "Setting up...",
|
loading: "Setting up...",
|
||||||
success: () => {
|
success: () => {
|
||||||
// Invalidate the project query to refresh the environment data
|
utils.project.one.invalidate({
|
||||||
utils.environment.one.invalidate({
|
projectId,
|
||||||
environmentId,
|
|
||||||
});
|
});
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
return `${template.name} template created successfully`;
|
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) => {
|
export const StepOne = ({ setTemplateInfo, templateInfo }: any) => {
|
||||||
// Get servers from the API
|
// Get servers from the API
|
||||||
const { data: servers } = api.server.withSSHKey.useQuery();
|
const { data: servers } = api.server.withSSHKey.useQuery();
|
||||||
const { data: isCloud } = api.settings.isCloud.useQuery();
|
|
||||||
const hasServers = servers && servers.length > 0;
|
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) => {
|
const handleExampleClick = (example: string) => {
|
||||||
setTemplateInfo({ ...templateInfo, userInput: example });
|
setTemplateInfo({ ...templateInfo, userInput: example });
|
||||||
@@ -53,58 +48,34 @@ export const StepOne = ({ setTemplateInfo, templateInfo }: any) => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{shouldShowServerDropdown && (
|
{hasServers && (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="server-deploy">
|
<Label htmlFor="server-deploy">
|
||||||
Select the server where you want to deploy (optional)
|
Select the server where you want to deploy (optional)
|
||||||
</Label>
|
</Label>
|
||||||
<Select
|
<Select
|
||||||
value={
|
value={templateInfo.server?.serverId}
|
||||||
templateInfo.server?.serverId ||
|
|
||||||
(!isCloud ? "dokploy" : undefined)
|
|
||||||
}
|
|
||||||
onValueChange={(value) => {
|
onValueChange={(value) => {
|
||||||
if (value === "dokploy") {
|
const server = servers?.find((s) => s.serverId === value);
|
||||||
|
if (server) {
|
||||||
setTemplateInfo({
|
setTemplateInfo({
|
||||||
...templateInfo,
|
...templateInfo,
|
||||||
server: undefined,
|
server: server,
|
||||||
});
|
});
|
||||||
} else {
|
|
||||||
const server = servers?.find((s) => s.serverId === value);
|
|
||||||
if (server) {
|
|
||||||
setTemplateInfo({
|
|
||||||
...templateInfo,
|
|
||||||
server: server,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<SelectTrigger className="w-full">
|
<SelectTrigger className="w-full">
|
||||||
<SelectValue
|
<SelectValue placeholder="Select a server" />
|
||||||
placeholder={!isCloud ? "Dokploy" : "Select a Server"}
|
|
||||||
/>
|
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectGroup>
|
<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) => (
|
{servers?.map((server) => (
|
||||||
<SelectItem key={server.serverId} value={server.serverId}>
|
<SelectItem key={server.serverId} value={server.serverId}>
|
||||||
{server.name}
|
{server.name}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
))}
|
))}
|
||||||
<SelectLabel>
|
<SelectLabel>Servers ({servers?.length})</SelectLabel>
|
||||||
Servers ({servers?.length + (!isCloud ? 1 : 0)})
|
|
||||||
</SelectLabel>
|
|
||||||
</SelectGroup>
|
</SelectGroup>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
|
|||||||
@@ -90,11 +90,11 @@ export const { useStepper, steps, Scoped } = defineStepper(
|
|||||||
);
|
);
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
environmentId: string;
|
projectId: string;
|
||||||
projectName?: string;
|
projectName?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const TemplateGenerator = ({ environmentId }: Props) => {
|
export const TemplateGenerator = ({ projectId }: Props) => {
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const stepper = useStepper();
|
const stepper = useStepper();
|
||||||
const { data: aiSettings } = api.ai.getAll.useQuery();
|
const { data: aiSettings } = api.ai.getAll.useQuery();
|
||||||
@@ -121,7 +121,7 @@ export const TemplateGenerator = ({ environmentId }: Props) => {
|
|||||||
|
|
||||||
const onSubmit = async () => {
|
const onSubmit = async () => {
|
||||||
await mutateAsync({
|
await mutateAsync({
|
||||||
environmentId: environmentId,
|
projectId,
|
||||||
id: templateInfo.details?.id || "",
|
id: templateInfo.details?.id || "",
|
||||||
name: templateInfo?.details?.name || "",
|
name: templateInfo?.details?.name || "",
|
||||||
description: templateInfo?.details?.shortDescription || "",
|
description: templateInfo?.details?.shortDescription || "",
|
||||||
@@ -138,9 +138,8 @@ export const TemplateGenerator = ({ environmentId }: Props) => {
|
|||||||
.then(async () => {
|
.then(async () => {
|
||||||
toast.success("Compose Created");
|
toast.success("Compose Created");
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
// Invalidate the project query to refresh the environment data
|
await utils.project.one.invalidate({
|
||||||
await utils.environment.one.invalidate({
|
projectId,
|
||||||
environmentId,
|
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
|
|||||||
@@ -15,13 +15,6 @@ import {
|
|||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
|
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
|
||||||
import {
|
|
||||||
Select,
|
|
||||||
SelectContent,
|
|
||||||
SelectItem,
|
|
||||||
SelectTrigger,
|
|
||||||
SelectValue,
|
|
||||||
} from "@/components/ui/select";
|
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
|
|
||||||
export type Services = {
|
export type Services = {
|
||||||
@@ -43,35 +36,23 @@ export type Services = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
interface DuplicateProjectProps {
|
interface DuplicateProjectProps {
|
||||||
environmentId: string;
|
projectId: string;
|
||||||
services: Services[];
|
services: Services[];
|
||||||
selectedServiceIds: string[];
|
selectedServiceIds: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DuplicateProject = ({
|
export const DuplicateProject = ({
|
||||||
environmentId,
|
projectId,
|
||||||
services,
|
services,
|
||||||
selectedServiceIds,
|
selectedServiceIds,
|
||||||
}: DuplicateProjectProps) => {
|
}: DuplicateProjectProps) => {
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const [name, setName] = useState("");
|
const [name, setName] = useState("");
|
||||||
const [description, setDescription] = useState("");
|
const [description, setDescription] = useState("");
|
||||||
const [duplicateType, setDuplicateType] = useState("new-project"); // "new-project" or "existing-environment"
|
const [duplicateType, setDuplicateType] = useState("new-project"); // "new-project" or "same-project"
|
||||||
const [selectedTargetProject, setSelectedTargetProject] =
|
|
||||||
useState<string>("");
|
|
||||||
const [selectedTargetEnvironment, setSelectedTargetEnvironment] =
|
|
||||||
useState<string>("");
|
|
||||||
const utils = api.useUtils();
|
const utils = api.useUtils();
|
||||||
const router = useRouter();
|
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) =>
|
const selectedServices = services.filter((service) =>
|
||||||
selectedServiceIds.includes(service.id),
|
selectedServiceIds.includes(service.id),
|
||||||
);
|
);
|
||||||
@@ -80,29 +61,6 @@ export const DuplicateProject = ({
|
|||||||
api.project.duplicate.useMutation({
|
api.project.duplicate.useMutation({
|
||||||
onSuccess: async (newProject) => {
|
onSuccess: async (newProject) => {
|
||||||
await utils.project.all.invalidate();
|
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(
|
toast.success(
|
||||||
duplicateType === "new-project"
|
duplicateType === "new-project"
|
||||||
? "Project duplicated successfully"
|
? "Project duplicated successfully"
|
||||||
@@ -110,9 +68,7 @@ export const DuplicateProject = ({
|
|||||||
);
|
);
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
if (duplicateType === "new-project") {
|
if (duplicateType === "new-project") {
|
||||||
router.push(
|
router.push(`/dashboard/project/${newProject.projectId}`);
|
||||||
`/dashboard/project/${newProject?.projectId}/environment/${newProject?.environmentId}`,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
@@ -126,20 +82,8 @@ export const DuplicateProject = ({
|
|||||||
return;
|
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({
|
await duplicateProject({
|
||||||
sourceEnvironmentId: selectedTargetEnvironment,
|
sourceProjectId: projectId,
|
||||||
name,
|
name,
|
||||||
description,
|
description,
|
||||||
includeServices: true,
|
includeServices: true,
|
||||||
@@ -147,7 +91,7 @@ export const DuplicateProject = ({
|
|||||||
id: service.id,
|
id: service.id,
|
||||||
type: service.type,
|
type: service.type,
|
||||||
})),
|
})),
|
||||||
duplicateInSameProject: duplicateType === "existing-environment",
|
duplicateInSameProject: duplicateType === "same-project",
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -161,8 +105,6 @@ export const DuplicateProject = ({
|
|||||||
setName("");
|
setName("");
|
||||||
setDescription("");
|
setDescription("");
|
||||||
setDuplicateType("new-project");
|
setDuplicateType("new-project");
|
||||||
setSelectedTargetProject("");
|
|
||||||
setSelectedTargetEnvironment("");
|
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -185,14 +127,7 @@ export const DuplicateProject = ({
|
|||||||
<Label>Duplicate to</Label>
|
<Label>Duplicate to</Label>
|
||||||
<RadioGroup
|
<RadioGroup
|
||||||
value={duplicateType}
|
value={duplicateType}
|
||||||
onValueChange={(value) => {
|
onValueChange={setDuplicateType}
|
||||||
setDuplicateType(value);
|
|
||||||
// Reset selections when changing type
|
|
||||||
if (value !== "existing-environment") {
|
|
||||||
setSelectedTargetProject("");
|
|
||||||
setSelectedTargetEnvironment("");
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
className="grid gap-2"
|
className="grid gap-2"
|
||||||
>
|
>
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
@@ -200,13 +135,8 @@ export const DuplicateProject = ({
|
|||||||
<Label htmlFor="new-project">New project</Label>
|
<Label htmlFor="new-project">New project</Label>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<RadioGroupItem
|
<RadioGroupItem value="same-project" id="same-project" />
|
||||||
value="existing-environment"
|
<Label htmlFor="same-project">Same project</Label>
|
||||||
id="existing-environment"
|
|
||||||
/>
|
|
||||||
<Label htmlFor="existing-environment">
|
|
||||||
Existing environment
|
|
||||||
</Label>
|
|
||||||
</div>
|
</div>
|
||||||
</RadioGroup>
|
</RadioGroup>
|
||||||
</div>
|
</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">
|
<div className="grid gap-2">
|
||||||
<Label>Selected services to duplicate</Label>
|
<Label>Selected services to duplicate</Label>
|
||||||
<div className="space-y-2 max-h-[200px] overflow-y-auto border rounded-md p-4">
|
<div className="space-y-2 max-h-[200px] overflow-y-auto border rounded-md p-4">
|
||||||
@@ -325,26 +187,18 @@ export const DuplicateProject = ({
|
|||||||
>
|
>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button onClick={handleDuplicate} disabled={isLoading}>
|
||||||
onClick={handleDuplicate}
|
|
||||||
disabled={
|
|
||||||
isLoading ||
|
|
||||||
(duplicateType === "new-project" && !name) ||
|
|
||||||
(duplicateType === "existing-environment" &&
|
|
||||||
(!selectedTargetProject || !selectedTargetEnvironment))
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<>
|
<>
|
||||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||||
{duplicateType === "new-project"
|
{duplicateType === "new-project"
|
||||||
? "Duplicating to new project..."
|
? "Duplicating project..."
|
||||||
: "Duplicating to environment..."}
|
: "Duplicating services..."}
|
||||||
</>
|
</>
|
||||||
) : duplicateType === "new-project" ? (
|
) : duplicateType === "new-project" ? (
|
||||||
"Duplicate to new project"
|
"Duplicate project"
|
||||||
) : (
|
) : (
|
||||||
"Duplicate to environment"
|
"Duplicate services"
|
||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogFooter>
|
</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");
|
toast.success(projectId ? "Project Updated" : "Project Created");
|
||||||
setIsOpen(false);
|
setIsOpen(false);
|
||||||
if (!projectId) {
|
if (!projectId) {
|
||||||
const projectIdToUse =
|
router.push(`/dashboard/project/${data?.projectId}`);
|
||||||
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}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
refetch();
|
refetch();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ import {
|
|||||||
DropdownMenuSeparator,
|
DropdownMenuSeparator,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from "@/components/ui/dropdown-menu";
|
} from "@/components/ui/dropdown-menu";
|
||||||
import { FocusShortcutInput } from "@/components/shared/focus-shortcut-input";
|
import { Input } from "@/components/ui/input";
|
||||||
import {
|
import {
|
||||||
Select,
|
Select,
|
||||||
SelectContent,
|
SelectContent,
|
||||||
@@ -96,8 +96,22 @@ export const ShowProjects = () => {
|
|||||||
new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime();
|
new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime();
|
||||||
break;
|
break;
|
||||||
case "services": {
|
case "services": {
|
||||||
const aTotalServices = a.environments.length;
|
const aTotalServices =
|
||||||
const bTotalServices = b.environments.length;
|
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;
|
comparison = aTotalServices - bTotalServices;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -144,13 +158,12 @@ export const ShowProjects = () => {
|
|||||||
<>
|
<>
|
||||||
<div className="flex max-sm:flex-col gap-4 items-center w-full">
|
<div className="flex max-sm:flex-col gap-4 items-center w-full">
|
||||||
<div className="flex-1 relative max-sm:w-full">
|
<div className="flex-1 relative max-sm:w-full">
|
||||||
<FocusShortcutInput
|
<Input
|
||||||
placeholder="Filter projects..."
|
placeholder="Filter projects..."
|
||||||
value={searchQuery}
|
value={searchQuery}
|
||||||
onChange={(e) => setSearchQuery(e.target.value)}
|
onChange={(e) => setSearchQuery(e.target.value)}
|
||||||
className="pr-10"
|
className="pr-10"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Search className="absolute right-3 top-1/2 -translate-y-1/2 size-4 text-muted-foreground" />
|
<Search className="absolute right-3 top-1/2 -translate-y-1/2 size-4 text-muted-foreground" />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2 min-w-48 max-sm:w-full">
|
<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">
|
<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) => {
|
{filteredProjects?.map((project) => {
|
||||||
const emptyServices = project?.environments
|
const emptyServices =
|
||||||
.map(
|
project?.mariadb.length === 0 &&
|
||||||
(env) =>
|
project?.mongo.length === 0 &&
|
||||||
env.applications.length === 0 &&
|
project?.mysql.length === 0 &&
|
||||||
env.mariadb.length === 0 &&
|
project?.postgres.length === 0 &&
|
||||||
env.mongo.length === 0 &&
|
project?.redis.length === 0 &&
|
||||||
env.mysql.length === 0 &&
|
project?.applications.length === 0 &&
|
||||||
env.postgres.length === 0 &&
|
project?.compose.length === 0;
|
||||||
env.redis.length === 0 &&
|
|
||||||
env.applications.length === 0 &&
|
|
||||||
env.compose.length === 0,
|
|
||||||
)
|
|
||||||
.every(Boolean);
|
|
||||||
|
|
||||||
const totalServices = project?.environments
|
const totalServices =
|
||||||
.map(
|
project?.mariadb.length +
|
||||||
(env) =>
|
project?.mongo.length +
|
||||||
env.mariadb.length +
|
project?.mysql.length +
|
||||||
env.mongo.length +
|
project?.postgres.length +
|
||||||
env.mysql.length +
|
project?.redis.length +
|
||||||
env.postgres.length +
|
project?.applications.length +
|
||||||
env.redis.length +
|
project?.compose.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);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -229,10 +225,11 @@ export const ShowProjects = () => {
|
|||||||
className="w-full lg:max-w-md"
|
className="w-full lg:max-w-md"
|
||||||
>
|
>
|
||||||
<Link
|
<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">
|
<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>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<Button
|
<Button
|
||||||
@@ -247,51 +244,44 @@ export const ShowProjects = () => {
|
|||||||
className="w-[200px] space-y-2 overflow-y-auto max-h-[400px]"
|
className="w-[200px] space-y-2 overflow-y-auto max-h-[400px]"
|
||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
>
|
>
|
||||||
{project.environments.some(
|
{project.applications.length > 0 && (
|
||||||
(env) => env.applications.length > 0,
|
|
||||||
) && (
|
|
||||||
<DropdownMenuGroup>
|
<DropdownMenuGroup>
|
||||||
<DropdownMenuLabel>
|
<DropdownMenuLabel>
|
||||||
Applications
|
Applications
|
||||||
</DropdownMenuLabel>
|
</DropdownMenuLabel>
|
||||||
{project.environments.map((env) =>
|
{project.applications.map((app) => (
|
||||||
env.applications.map((app) => (
|
<div key={app.applicationId}>
|
||||||
<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 />
|
<DropdownMenuSeparator />
|
||||||
<DropdownMenuGroup>
|
{app.domains.map((domain) => (
|
||||||
<DropdownMenuLabel className="font-normal capitalize text-xs flex items-center justify-between">
|
<DropdownMenuItem
|
||||||
{app.name}
|
key={domain.domainId}
|
||||||
<StatusTooltip
|
asChild
|
||||||
status={
|
>
|
||||||
app.applicationStatus
|
<Link
|
||||||
}
|
className="space-x-4 text-xs cursor-pointer justify-between"
|
||||||
/>
|
target="_blank"
|
||||||
</DropdownMenuLabel>
|
href={`${domain.https ? "https" : "http"}://${domain.host}${domain.path}`}
|
||||||
<DropdownMenuSeparator />
|
|
||||||
{app.domains.map((domain) => (
|
|
||||||
<DropdownMenuItem
|
|
||||||
key={domain.domainId}
|
|
||||||
asChild
|
|
||||||
>
|
>
|
||||||
<Link
|
<span className="truncate">
|
||||||
className="space-x-4 text-xs cursor-pointer justify-between"
|
{domain.host}
|
||||||
target="_blank"
|
</span>
|
||||||
href={`${domain.https ? "https" : "http"}://${domain.host}${domain.path}`}
|
<ExternalLinkIcon className="size-4 shrink-0" />
|
||||||
>
|
</Link>
|
||||||
<span className="truncate">
|
</DropdownMenuItem>
|
||||||
{domain.host}
|
))}
|
||||||
</span>
|
</DropdownMenuGroup>
|
||||||
<ExternalLinkIcon className="size-4 shrink-0" />
|
</div>
|
||||||
</Link>
|
))}
|
||||||
</DropdownMenuItem>
|
|
||||||
))}
|
|
||||||
</DropdownMenuGroup>
|
|
||||||
</div>
|
|
||||||
)),
|
|
||||||
)}
|
|
||||||
</DropdownMenuGroup>
|
</DropdownMenuGroup>
|
||||||
)}
|
)}
|
||||||
{/*
|
|
||||||
{project.compose.length > 0 && (
|
{project.compose.length > 0 && (
|
||||||
<DropdownMenuGroup>
|
<DropdownMenuGroup>
|
||||||
<DropdownMenuLabel>
|
<DropdownMenuLabel>
|
||||||
@@ -329,7 +319,7 @@ export const ShowProjects = () => {
|
|||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</DropdownMenuGroup>
|
</DropdownMenuGroup>
|
||||||
)} */}
|
)}
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
) : null}
|
) : null}
|
||||||
|
|||||||
@@ -96,9 +96,9 @@ export const ShowExternalRedisCredentials = ({ redisId }: Props) => {
|
|||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="text-xl">External Credentials</CardTitle>
|
<CardTitle className="text-xl">External Credentials</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription>
|
||||||
In order to make the database reachable through the internet, you
|
In order to make the database reachable trought internet is
|
||||||
must set a port and ensure that the port is not being used by
|
required to set a port, make sure the port is not used by another
|
||||||
another application or database
|
application or database
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="flex w-full flex-col gap-4">
|
<CardContent className="flex w-full flex-col gap-4">
|
||||||
|
|||||||
@@ -3,10 +3,6 @@
|
|||||||
import { BookIcon, CircuitBoard, GlobeIcon } from "lucide-react";
|
import { BookIcon, CircuitBoard, GlobeIcon } from "lucide-react";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import {
|
|
||||||
extractServices,
|
|
||||||
type Services,
|
|
||||||
} from "@/components/dashboard/settings/users/add-permissions";
|
|
||||||
import {
|
import {
|
||||||
MariadbIcon,
|
MariadbIcon,
|
||||||
MongodbIcon,
|
MongodbIcon,
|
||||||
@@ -24,34 +20,13 @@ import {
|
|||||||
CommandSeparator,
|
CommandSeparator,
|
||||||
} from "@/components/ui/command";
|
} from "@/components/ui/command";
|
||||||
import { authClient } from "@/lib/auth-client";
|
import { authClient } from "@/lib/auth-client";
|
||||||
|
import {
|
||||||
|
extractServices,
|
||||||
|
type Services,
|
||||||
|
} from "@/pages/dashboard/project/[projectId]";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { StatusTooltip } from "../shared/status-tooltip";
|
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 = () => {
|
export const SearchCommand = () => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [open, setOpen] = React.useState(false);
|
const [open, setOpen] = React.useState(false);
|
||||||
@@ -88,42 +63,31 @@ export const SearchCommand = () => {
|
|||||||
</CommandEmpty>
|
</CommandEmpty>
|
||||||
<CommandGroup heading={"Projects"}>
|
<CommandGroup heading={"Projects"}>
|
||||||
<CommandList>
|
<CommandList>
|
||||||
{data?.map((project) => {
|
{data?.map((project) => (
|
||||||
const productionEnvironment = project.environments.find(
|
<CommandItem
|
||||||
(environment) => environment.name === "production",
|
key={project.projectId}
|
||||||
);
|
onSelect={() => {
|
||||||
|
router.push(`/dashboard/project/${project.projectId}`);
|
||||||
if (!productionEnvironment) return null;
|
setOpen(false);
|
||||||
|
}}
|
||||||
return (
|
>
|
||||||
<CommandItem
|
<BookIcon className="size-4 text-muted-foreground mr-2" />
|
||||||
key={project.projectId}
|
{project.name}
|
||||||
onSelect={() => {
|
</CommandItem>
|
||||||
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>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</CommandList>
|
</CommandList>
|
||||||
</CommandGroup>
|
</CommandGroup>
|
||||||
<CommandSeparator />
|
<CommandSeparator />
|
||||||
<CommandGroup heading={"Services"}>
|
<CommandGroup heading={"Services"}>
|
||||||
<CommandList>
|
<CommandList>
|
||||||
{data?.map((project) => {
|
{data?.map((project) => {
|
||||||
const applications: SearchServices[] =
|
const applications: Services[] = extractServices(project);
|
||||||
extractAllServicesFromProject(project);
|
|
||||||
return applications.map((application) => (
|
return applications.map((application) => (
|
||||||
<CommandItem
|
<CommandItem
|
||||||
key={application.id}
|
key={application.id}
|
||||||
onSelect={() => {
|
onSelect={() => {
|
||||||
router.push(
|
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);
|
setOpen(false);
|
||||||
}}
|
}}
|
||||||
@@ -150,8 +114,7 @@ export const SearchCommand = () => {
|
|||||||
<CircuitBoard className="h-6 w-6 mr-2" />
|
<CircuitBoard className="h-6 w-6 mr-2" />
|
||||||
)}
|
)}
|
||||||
<span className="flex-grow">
|
<span className="flex-grow">
|
||||||
{project.name} / {application.environmentName} /{" "}
|
{project.name} / {application.name}{" "}
|
||||||
{application.name}{" "}
|
|
||||||
<div style={{ display: "none" }}>{application.id}</div>
|
<div style={{ display: "none" }}>{application.id}</div>
|
||||||
</span>
|
</span>
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -65,11 +65,6 @@ export const AddCertificate = () => {
|
|||||||
const { mutateAsync, isError, error, isLoading } =
|
const { mutateAsync, isError, error, isLoading } =
|
||||||
api.certificates.create.useMutation();
|
api.certificates.create.useMutation();
|
||||||
const { data: servers } = api.server.withSSHKey.useQuery();
|
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>({
|
const form = useForm<AddCertificate>({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
@@ -90,7 +85,7 @@ export const AddCertificate = () => {
|
|||||||
certificateData: data.certificateData,
|
certificateData: data.certificateData,
|
||||||
privateKey: data.privateKey,
|
privateKey: data.privateKey,
|
||||||
autoRenew: data.autoRenew,
|
autoRenew: data.autoRenew,
|
||||||
serverId: data.serverId === "dokploy" ? undefined : data.serverId,
|
serverId: data.serverId,
|
||||||
organizationId: "",
|
organizationId: "",
|
||||||
})
|
})
|
||||||
.then(async () => {
|
.then(async () => {
|
||||||
@@ -179,70 +174,52 @@ export const AddCertificate = () => {
|
|||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
{shouldShowServerDropdown && (
|
<FormField
|
||||||
<FormField
|
control={form.control}
|
||||||
control={form.control}
|
name="serverId"
|
||||||
name="serverId"
|
render={({ field }) => (
|
||||||
render={({ field }) => (
|
<FormItem>
|
||||||
<FormItem>
|
<TooltipProvider delayDuration={0}>
|
||||||
<TooltipProvider delayDuration={0}>
|
<Tooltip>
|
||||||
<Tooltip>
|
<TooltipTrigger asChild>
|
||||||
<TooltipTrigger asChild>
|
<FormLabel className="break-all w-fit flex flex-row gap-1 items-center">
|
||||||
<FormLabel className="break-all w-fit flex flex-row gap-1 items-center">
|
Select a Server {!isCloud && "(Optional)"}
|
||||||
Select a Server {!isCloud && "(Optional)"}
|
<HelpCircle className="size-4 text-muted-foreground" />
|
||||||
<HelpCircle className="size-4 text-muted-foreground" />
|
</FormLabel>
|
||||||
</FormLabel>
|
</TooltipTrigger>
|
||||||
</TooltipTrigger>
|
</Tooltip>
|
||||||
</Tooltip>
|
</TooltipProvider>
|
||||||
</TooltipProvider>
|
|
||||||
|
|
||||||
<Select
|
<Select
|
||||||
onValueChange={field.onChange}
|
onValueChange={field.onChange}
|
||||||
defaultValue={
|
defaultValue={field.value}
|
||||||
field.value || (!isCloud ? "dokploy" : undefined)
|
>
|
||||||
}
|
<SelectTrigger>
|
||||||
>
|
<SelectValue placeholder="Select a Server" />
|
||||||
<SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectValue
|
<SelectContent>
|
||||||
placeholder={!isCloud ? "Dokploy" : "Select a Server"}
|
<SelectGroup>
|
||||||
/>
|
{servers?.map((server) => (
|
||||||
</SelectTrigger>
|
<SelectItem
|
||||||
<SelectContent>
|
key={server.serverId}
|
||||||
<SelectGroup>
|
value={server.serverId}
|
||||||
{!isCloud && (
|
>
|
||||||
<SelectItem value="dokploy">
|
<span className="flex items-center gap-2 justify-between w-full">
|
||||||
<span className="flex items-center gap-2 justify-between w-full">
|
<span>{server.name}</span>
|
||||||
<span>Dokploy</span>
|
<span className="text-muted-foreground text-xs self-center">
|
||||||
<span className="text-muted-foreground text-xs self-center">
|
{server.ipAddress}
|
||||||
Default
|
|
||||||
</span>
|
|
||||||
</span>
|
</span>
|
||||||
</SelectItem>
|
</span>
|
||||||
)}
|
</SelectItem>
|
||||||
{servers?.map((server) => (
|
))}
|
||||||
<SelectItem
|
<SelectLabel>Servers ({servers?.length})</SelectLabel>
|
||||||
key={server.serverId}
|
</SelectGroup>
|
||||||
value={server.serverId}
|
</SelectContent>
|
||||||
>
|
</Select>
|
||||||
<span className="flex items-center gap-2 justify-between w-full">
|
<FormMessage />
|
||||||
<span>{server.name}</span>
|
</FormItem>
|
||||||
<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>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<DialogFooter className="flex w-full flex-row !justify-end">
|
<DialogFooter className="flex w-full flex-row !justify-end">
|
||||||
|
|||||||
@@ -101,15 +101,6 @@ export const notificationSchema = z.discriminatedUnion("type", [
|
|||||||
decoration: z.boolean().default(true),
|
decoration: z.boolean().default(true),
|
||||||
})
|
})
|
||||||
.merge(notificationBaseSchema),
|
.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 = {
|
export const notificationsMap = {
|
||||||
@@ -133,10 +124,6 @@ export const notificationsMap = {
|
|||||||
icon: <MessageCircleMore size={29} className="text-muted-foreground" />,
|
icon: <MessageCircleMore size={29} className="text-muted-foreground" />,
|
||||||
label: "Gotify",
|
label: "Gotify",
|
||||||
},
|
},
|
||||||
ntfy: {
|
|
||||||
icon: <MessageCircleMore size={29} className="text-muted-foreground" />,
|
|
||||||
label: "ntfy",
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type NotificationSchema = z.infer<typeof notificationSchema>;
|
export type NotificationSchema = z.infer<typeof notificationSchema>;
|
||||||
@@ -168,8 +155,6 @@ export const HandleNotifications = ({ notificationId }: Props) => {
|
|||||||
api.notification.testEmailConnection.useMutation();
|
api.notification.testEmailConnection.useMutation();
|
||||||
const { mutateAsync: testGotifyConnection, isLoading: isLoadingGotify } =
|
const { mutateAsync: testGotifyConnection, isLoading: isLoadingGotify } =
|
||||||
api.notification.testGotifyConnection.useMutation();
|
api.notification.testGotifyConnection.useMutation();
|
||||||
const { mutateAsync: testNtfyConnection, isLoading: isLoadingNtfy } =
|
|
||||||
api.notification.testNtfyConnection.useMutation();
|
|
||||||
const slackMutation = notificationId
|
const slackMutation = notificationId
|
||||||
? api.notification.updateSlack.useMutation()
|
? api.notification.updateSlack.useMutation()
|
||||||
: api.notification.createSlack.useMutation();
|
: api.notification.createSlack.useMutation();
|
||||||
@@ -185,9 +170,6 @@ export const HandleNotifications = ({ notificationId }: Props) => {
|
|||||||
const gotifyMutation = notificationId
|
const gotifyMutation = notificationId
|
||||||
? api.notification.updateGotify.useMutation()
|
? api.notification.updateGotify.useMutation()
|
||||||
: api.notification.createGotify.useMutation();
|
: api.notification.createGotify.useMutation();
|
||||||
const ntfyMutation = notificationId
|
|
||||||
? api.notification.updateNtfy.useMutation()
|
|
||||||
: api.notification.createNtfy.useMutation();
|
|
||||||
|
|
||||||
const form = useForm<NotificationSchema>({
|
const form = useForm<NotificationSchema>({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
@@ -284,20 +266,6 @@ export const HandleNotifications = ({ notificationId }: Props) => {
|
|||||||
name: notification.name,
|
name: notification.name,
|
||||||
dockerCleanup: notification.dockerCleanup,
|
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 {
|
} else {
|
||||||
form.reset();
|
form.reset();
|
||||||
@@ -310,7 +278,6 @@ export const HandleNotifications = ({ notificationId }: Props) => {
|
|||||||
discord: discordMutation,
|
discord: discordMutation,
|
||||||
email: emailMutation,
|
email: emailMutation,
|
||||||
gotify: gotifyMutation,
|
gotify: gotifyMutation,
|
||||||
ntfy: ntfyMutation,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const onSubmit = async (data: NotificationSchema) => {
|
const onSubmit = async (data: NotificationSchema) => {
|
||||||
@@ -399,21 +366,6 @@ export const HandleNotifications = ({ notificationId }: Props) => {
|
|||||||
notificationId: notificationId || "",
|
notificationId: notificationId || "",
|
||||||
gotifyId: notification?.gotifyId || "",
|
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) {
|
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>
|
</div>
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
@@ -1149,8 +1024,7 @@ export const HandleNotifications = ({ notificationId }: Props) => {
|
|||||||
isLoadingTelegram ||
|
isLoadingTelegram ||
|
||||||
isLoadingDiscord ||
|
isLoadingDiscord ||
|
||||||
isLoadingEmail ||
|
isLoadingEmail ||
|
||||||
isLoadingGotify ||
|
isLoadingGotify
|
||||||
isLoadingNtfy
|
|
||||||
}
|
}
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
@@ -1187,13 +1061,6 @@ export const HandleNotifications = ({ notificationId }: Props) => {
|
|||||||
priority: form.getValues("priority"),
|
priority: form.getValues("priority"),
|
||||||
decoration: form.getValues("decoration"),
|
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");
|
toast.success("Connection Success");
|
||||||
} catch {
|
} catch {
|
||||||
|
|||||||
@@ -88,11 +88,6 @@ export const ShowNotifications = () => {
|
|||||||
<MessageCircleMore className="size-6 text-muted-foreground" />
|
<MessageCircleMore className="size-6 text-muted-foreground" />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{notification.notificationType === "ntfy" && (
|
|
||||||
<div className="flex items-center justify-center rounded-lg ">
|
|
||||||
<MessageCircleMore className="size-6 text-muted-foreground" />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{notification.name}
|
{notification.name}
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -97,7 +97,11 @@ export const ShowTraefikActions = ({ serverId }: Props) => {
|
|||||||
);
|
);
|
||||||
refetchDashboard();
|
refetchDashboard();
|
||||||
})
|
})
|
||||||
.catch(() => {});
|
.catch(() => {
|
||||||
|
toast.error(
|
||||||
|
`${haveTraefikDashboardPortEnabled ? "Disabled" : "Enabled"} Dashboard`,
|
||||||
|
);
|
||||||
|
});
|
||||||
}}
|
}}
|
||||||
className="w-full cursor-pointer space-x-3"
|
className="w-full cursor-pointer space-x-3"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -35,9 +35,9 @@ import {
|
|||||||
PopoverContent,
|
PopoverContent,
|
||||||
PopoverTrigger,
|
PopoverTrigger,
|
||||||
} from "@/components/ui/popover";
|
} from "@/components/ui/popover";
|
||||||
|
import { extractServices } from "@/pages/dashboard/project/[projectId]";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { useUrl } from "@/utils/hooks/use-url";
|
import { useUrl } from "@/utils/hooks/use-url";
|
||||||
import { extractServices } from "../users/add-permissions";
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
serverId?: string;
|
serverId?: string;
|
||||||
@@ -95,13 +95,11 @@ export const SetupMonitoring = ({ serverId }: Props) => {
|
|||||||
|
|
||||||
const { data: projects } = api.project.all.useQuery();
|
const { data: projects } = api.project.all.useQuery();
|
||||||
|
|
||||||
const extractServicesFromProjects = () => {
|
const extractServicesFromProjects = (projects: any[] | undefined) => {
|
||||||
if (!projects) return [];
|
if (!projects) return [];
|
||||||
|
|
||||||
const allServices = projects.flatMap((project) => {
|
const allServices = projects.flatMap((project) => {
|
||||||
const services = project.environments.flatMap((env) =>
|
const services = extractServices(project);
|
||||||
extractServices(env),
|
|
||||||
);
|
|
||||||
return serverId
|
return serverId
|
||||||
? services
|
? services
|
||||||
.filter((service) => service.serverId === serverId)
|
.filter((service) => service.serverId === serverId)
|
||||||
@@ -112,7 +110,7 @@ export const SetupMonitoring = ({ serverId }: Props) => {
|
|||||||
return [...new Set(allServices)];
|
return [...new Set(allServices)];
|
||||||
};
|
};
|
||||||
|
|
||||||
const services = extractServicesFromProjects();
|
const services = extractServicesFromProjects(projects);
|
||||||
|
|
||||||
const form = useForm<Schema>({
|
const form = useForm<Schema>({
|
||||||
resolver: zodResolver(Schema),
|
resolver: zodResolver(Schema),
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import type { findEnvironmentById } from "@dokploy/server/index";
|
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
@@ -27,135 +26,11 @@ import {
|
|||||||
FormMessage,
|
FormMessage,
|
||||||
} from "@/components/ui/form";
|
} from "@/components/ui/form";
|
||||||
import { Switch } from "@/components/ui/switch";
|
import { Switch } from "@/components/ui/switch";
|
||||||
|
import { extractServices } from "@/pages/dashboard/project/[projectId]";
|
||||||
import { api } from "@/utils/api";
|
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({
|
const addPermissions = z.object({
|
||||||
accessedProjects: z.array(z.string()).optional(),
|
accessedProjects: z.array(z.string()).optional(),
|
||||||
accessedEnvironments: z.array(z.string()).optional(),
|
|
||||||
accessedServices: z.array(z.string()).optional(),
|
accessedServices: z.array(z.string()).optional(),
|
||||||
canCreateProjects: z.boolean().optional().default(false),
|
canCreateProjects: z.boolean().optional().default(false),
|
||||||
canCreateServices: z.boolean().optional().default(false),
|
canCreateServices: z.boolean().optional().default(false),
|
||||||
@@ -201,7 +76,6 @@ export const AddUserPermissions = ({ userId }: Props) => {
|
|||||||
if (data) {
|
if (data) {
|
||||||
form.reset({
|
form.reset({
|
||||||
accessedProjects: data.accessedProjects || [],
|
accessedProjects: data.accessedProjects || [],
|
||||||
accessedEnvironments: data.accessedEnvironments || [],
|
|
||||||
accessedServices: data.accessedServices || [],
|
accessedServices: data.accessedServices || [],
|
||||||
canCreateProjects: data.canCreateProjects,
|
canCreateProjects: data.canCreateProjects,
|
||||||
canCreateServices: data.canCreateServices,
|
canCreateServices: data.canCreateServices,
|
||||||
@@ -225,7 +99,6 @@ export const AddUserPermissions = ({ userId }: Props) => {
|
|||||||
canDeleteProjects: data.canDeleteProjects,
|
canDeleteProjects: data.canDeleteProjects,
|
||||||
canAccessToTraefikFiles: data.canAccessToTraefikFiles,
|
canAccessToTraefikFiles: data.canAccessToTraefikFiles,
|
||||||
accessedProjects: data.accessedProjects || [],
|
accessedProjects: data.accessedProjects || [],
|
||||||
accessedEnvironments: data.accessedEnvironments || [],
|
|
||||||
accessedServices: data.accessedServices || [],
|
accessedServices: data.accessedServices || [],
|
||||||
canAccessToDocker: data.canAccessToDocker,
|
canAccessToDocker: data.canAccessToDocker,
|
||||||
canAccessToAPI: data.canAccessToAPI,
|
canAccessToAPI: data.canAccessToAPI,
|
||||||
@@ -459,317 +332,89 @@ export const AddUserPermissions = ({ userId }: Props) => {
|
|||||||
No projects found
|
No projects found
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
<div className="grid md:grid-cols-1 gap-4">
|
<div className="grid md:grid-cols-2 gap-4">
|
||||||
{projects?.map((project, projectIndex) => {
|
{projects?.map((item, index) => {
|
||||||
|
const applications = extractServices(item);
|
||||||
return (
|
return (
|
||||||
<FormField
|
<FormField
|
||||||
key={`project-${projectIndex}`}
|
key={`project-${index}`}
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="accessedProjects"
|
name="accessedProjects"
|
||||||
render={({ field }) => {
|
render={({ field }) => {
|
||||||
return (
|
return (
|
||||||
<FormItem
|
<FormItem
|
||||||
key={project.projectId}
|
key={item.projectId}
|
||||||
className="flex flex-col items-start rounded-lg p-4 border"
|
className="flex flex-col items-start space-x-4 rounded-lg p-4 border"
|
||||||
>
|
>
|
||||||
{/* Project Header */}
|
<div className="flex flex-row gap-4">
|
||||||
<div className="flex flex-row gap-4 items-center w-full">
|
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={field.value?.includes(
|
checked={field.value?.includes(
|
||||||
project.projectId,
|
item.projectId,
|
||||||
)}
|
)}
|
||||||
onCheckedChange={(checked) => {
|
onCheckedChange={(checked) => {
|
||||||
if (checked) {
|
return checked
|
||||||
// Add the project
|
? field.onChange([
|
||||||
field.onChange([
|
...(field.value || []),
|
||||||
...(field.value || []),
|
item.projectId,
|
||||||
project.projectId,
|
])
|
||||||
]);
|
: field.onChange(
|
||||||
} else {
|
field.value?.filter(
|
||||||
// Remove the project
|
(value) =>
|
||||||
field.onChange(
|
value !== item.projectId,
|
||||||
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,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// 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>
|
</FormControl>
|
||||||
<FormLabel className="text-base font-semibold text-primary">
|
<FormLabel className="text-sm font-medium text-primary">
|
||||||
{project.name}
|
{item.name}
|
||||||
</FormLabel>
|
</FormLabel>
|
||||||
</div>
|
</div>
|
||||||
|
{applications.length === 0 && (
|
||||||
{/* Environments */}
|
<p className="text-sm text-muted-foreground">
|
||||||
<div className="ml-6 w-full space-y-3">
|
No services found
|
||||||
{project.environments.length === 0 && (
|
</p>
|
||||||
<p className="text-sm text-muted-foreground">
|
)}
|
||||||
No environments found
|
{applications?.map((item, index) => (
|
||||||
</p>
|
<FormField
|
||||||
)}
|
key={`project-${index}`}
|
||||||
{project.environments.map(
|
control={form.control}
|
||||||
(environment, envIndex) => {
|
name="accessedServices"
|
||||||
const services =
|
render={({ field }) => {
|
||||||
extractServices(environment);
|
|
||||||
return (
|
return (
|
||||||
<div
|
<FormItem
|
||||||
key={`env-${envIndex}`}
|
key={item.id}
|
||||||
className="border-l-2 border-muted pl-4"
|
className="flex flex-row items-start space-x-3 space-y-0"
|
||||||
>
|
>
|
||||||
{/* Environment Header with Checkbox */}
|
<FormControl>
|
||||||
<FormField
|
<Checkbox
|
||||||
key={`env-${envIndex}`}
|
checked={field.value?.includes(
|
||||||
control={form.control}
|
item.id,
|
||||||
name="accessedEnvironments"
|
)}
|
||||||
render={({ field: envField }) => (
|
onCheckedChange={(checked) => {
|
||||||
<FormItem className="flex flex-row items-center space-x-3 space-y-0 mb-2">
|
return checked
|
||||||
<FormControl>
|
? field.onChange([
|
||||||
<Checkbox
|
...(field.value || []),
|
||||||
checked={envField.value?.includes(
|
item.id,
|
||||||
environment.environmentId,
|
])
|
||||||
)}
|
: field.onChange(
|
||||||
onCheckedChange={(
|
field.value?.filter(
|
||||||
checked,
|
(value) =>
|
||||||
) => {
|
value !== item.id,
|
||||||
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>
|
||||||
)}
|
<FormLabel className="text-sm text-muted-foreground">
|
||||||
</div>
|
{item.name}
|
||||||
</div>
|
</FormLabel>
|
||||||
|
</FormItem>
|
||||||
);
|
);
|
||||||
},
|
}}
|
||||||
)}
|
/>
|
||||||
</div>
|
))}
|
||||||
</FormItem>
|
</FormItem>
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import { SidebarTrigger } from "@/components/ui/sidebar";
|
|||||||
interface Props {
|
interface Props {
|
||||||
list: {
|
list: {
|
||||||
name: string;
|
name: string;
|
||||||
href?: string;
|
href: string;
|
||||||
}[];
|
}[];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -29,11 +29,11 @@ export const BreadcrumbSidebar = ({ list }: Props) => {
|
|||||||
{list.map((item, index) => (
|
{list.map((item, index) => (
|
||||||
<Fragment key={item.name}>
|
<Fragment key={item.name}>
|
||||||
<BreadcrumbItem className="block">
|
<BreadcrumbItem className="block">
|
||||||
<BreadcrumbLink href={item?.href} asChild={!!item?.href}>
|
<BreadcrumbLink href={item.href} asChild={!!item.href}>
|
||||||
{item.href ? (
|
{item.href ? (
|
||||||
<Link href={item?.href}>{item?.name}</Link>
|
<Link href={item.href}>{item.name}</Link>
|
||||||
) : (
|
) : (
|
||||||
item?.name
|
item.name
|
||||||
)}
|
)}
|
||||||
</BreadcrumbLink>
|
</BreadcrumbLink>
|
||||||
</BreadcrumbItem>
|
</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 copy from "copy-to-clipboard";
|
||||||
import { Clipboard } from "lucide-react";
|
import { Clipboard, EyeIcon, EyeOffIcon } from "lucide-react";
|
||||||
import { useRef } from "react";
|
import { useRef, useState } from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { Button } from "../ui/button";
|
import { Button } from "../ui/button";
|
||||||
import { Input, type InputProps } from "../ui/input";
|
import { Input, type InputProps } from "../ui/input";
|
||||||
|
|
||||||
export const ToggleVisibilityInput = ({ ...props }: InputProps) => {
|
export const ToggleVisibilityInput = ({ ...props }: InputProps) => {
|
||||||
|
const [isPasswordVisible, setIsPasswordVisible] = useState(false);
|
||||||
const inputRef = useRef<HTMLInputElement>(null);
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
|
const togglePasswordVisibility = () => {
|
||||||
|
setIsPasswordVisible((prevVisibility) => !prevVisibility);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex w-full items-center space-x-2">
|
<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
|
<Button
|
||||||
variant={"secondary"}
|
variant={"secondary"}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@@ -20,13 +29,13 @@ export const ToggleVisibilityInput = ({ ...props }: InputProps) => {
|
|||||||
>
|
>
|
||||||
<Clipboard className="size-4 text-muted-foreground" />
|
<Clipboard className="size-4 text-muted-foreground" />
|
||||||
</Button>
|
</Button>
|
||||||
{/* <Button onClick={togglePasswordVisibility} variant={"secondary"}>
|
<Button onClick={togglePasswordVisibility} variant={"secondary"}>
|
||||||
{isPasswordVisible ? (
|
{isPasswordVisible ? (
|
||||||
<EyeOffIcon className="size-4 text-muted-foreground" />
|
<EyeOffIcon className="size-4 text-muted-foreground" />
|
||||||
) : (
|
) : (
|
||||||
<EyeIcon className="size-4 text-muted-foreground" />
|
<EyeIcon className="size-4 text-muted-foreground" />
|
||||||
)}
|
)}
|
||||||
</Button> */}
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { EyeIcon, EyeOffIcon } from "lucide-react";
|
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
@@ -9,39 +8,18 @@ export interface InputProps
|
|||||||
|
|
||||||
const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
||||||
({ className, errorMessage, type, ...props }, ref) => {
|
({ className, errorMessage, type, ...props }, ref) => {
|
||||||
const [showPassword, setShowPassword] = React.useState(false);
|
|
||||||
const isPassword = type === "password";
|
|
||||||
const inputType = isPassword ? (showPassword ? "text" : "password") : type;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="relative w-full">
|
<input
|
||||||
<input
|
type={type}
|
||||||
type={inputType}
|
className={cn(
|
||||||
className={cn(
|
// bg-gray
|
||||||
// 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",
|
||||||
"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,
|
||||||
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>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
ref={ref}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
{errorMessage && (
|
{errorMessage && (
|
||||||
<span className="text-sm text-red-600 text-secondary-foreground">
|
<span className="text-sm text-red-600 text-secondary-foreground">
|
||||||
{errorMessage}
|
{errorMessage}
|
||||||
|
|||||||
2
apps/dokploy/drizzle/0107_calm_power_pack.sql
Normal file
2
apps/dokploy/drizzle/0107_calm_power_pack.sql
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
ALTER TABLE "user_temp" ADD COLUMN "serverConcurrency" integer DEFAULT 1 NOT NULL;--> statement-breakpoint
|
||||||
|
ALTER TABLE "server" ADD COLUMN "concurrency" integer DEFAULT 1 NOT NULL;
|
||||||
@@ -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;
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"id": "9b77fa3f-52d5-4488-930b-1d7ef304af19",
|
"id": "c1520c6a-965f-4977-9e0e-ec8e402af35c",
|
||||||
"prevId": "5568024c-5daa-4554-a224-8a005a53f97c",
|
"prevId": "5568024c-5daa-4554-a224-8a005a53f97c",
|
||||||
"version": "7",
|
"version": "7",
|
||||||
"dialect": "postgresql",
|
"dialect": "postgresql",
|
||||||
@@ -1303,8 +1303,8 @@
|
|||||||
"primaryKey": false,
|
"primaryKey": false,
|
||||||
"notNull": false
|
"notNull": false
|
||||||
},
|
},
|
||||||
"environmentId": {
|
"projectId": {
|
||||||
"name": "environmentId",
|
"name": "projectId",
|
||||||
"type": "text",
|
"type": "text",
|
||||||
"primaryKey": false,
|
"primaryKey": false,
|
||||||
"notNull": true
|
"notNull": true
|
||||||
@@ -1368,15 +1368,15 @@
|
|||||||
"onDelete": "set null",
|
"onDelete": "set null",
|
||||||
"onUpdate": "no action"
|
"onUpdate": "no action"
|
||||||
},
|
},
|
||||||
"application_environmentId_environment_environmentId_fk": {
|
"application_projectId_project_projectId_fk": {
|
||||||
"name": "application_environmentId_environment_environmentId_fk",
|
"name": "application_projectId_project_projectId_fk",
|
||||||
"tableFrom": "application",
|
"tableFrom": "application",
|
||||||
"tableTo": "environment",
|
"tableTo": "project",
|
||||||
"columnsFrom": [
|
"columnsFrom": [
|
||||||
"environmentId"
|
"projectId"
|
||||||
],
|
],
|
||||||
"columnsTo": [
|
"columnsTo": [
|
||||||
"environmentId"
|
"projectId"
|
||||||
],
|
],
|
||||||
"onDelete": "cascade",
|
"onDelete": "cascade",
|
||||||
"onUpdate": "no action"
|
"onUpdate": "no action"
|
||||||
@@ -2074,8 +2074,8 @@
|
|||||||
"notNull": true,
|
"notNull": true,
|
||||||
"default": "'idle'"
|
"default": "'idle'"
|
||||||
},
|
},
|
||||||
"environmentId": {
|
"projectId": {
|
||||||
"name": "environmentId",
|
"name": "projectId",
|
||||||
"type": "text",
|
"type": "text",
|
||||||
"primaryKey": false,
|
"primaryKey": false,
|
||||||
"notNull": true
|
"notNull": true
|
||||||
@@ -2138,15 +2138,15 @@
|
|||||||
"onDelete": "set null",
|
"onDelete": "set null",
|
||||||
"onUpdate": "no action"
|
"onUpdate": "no action"
|
||||||
},
|
},
|
||||||
"compose_environmentId_environment_environmentId_fk": {
|
"compose_projectId_project_projectId_fk": {
|
||||||
"name": "compose_environmentId_environment_environmentId_fk",
|
"name": "compose_projectId_project_projectId_fk",
|
||||||
"tableFrom": "compose",
|
"tableFrom": "compose",
|
||||||
"tableTo": "environment",
|
"tableTo": "project",
|
||||||
"columnsFrom": [
|
"columnsFrom": [
|
||||||
"environmentId"
|
"projectId"
|
||||||
],
|
],
|
||||||
"columnsTo": [
|
"columnsTo": [
|
||||||
"environmentId"
|
"projectId"
|
||||||
],
|
],
|
||||||
"onDelete": "cascade",
|
"onDelete": "cascade",
|
||||||
"onUpdate": "no action"
|
"onUpdate": "no action"
|
||||||
@@ -2704,63 +2704,6 @@
|
|||||||
"checkConstraints": {},
|
"checkConstraints": {},
|
||||||
"isRLSEnabled": false
|
"isRLSEnabled": false
|
||||||
},
|
},
|
||||||
"public.environment": {
|
|
||||||
"name": "environment",
|
|
||||||
"schema": "",
|
|
||||||
"columns": {
|
|
||||||
"environmentId": {
|
|
||||||
"name": "environmentId",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": true,
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
"name": {
|
|
||||||
"name": "name",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
"description": {
|
|
||||||
"name": "description",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
"createdAt": {
|
|
||||||
"name": "createdAt",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
"projectId": {
|
|
||||||
"name": "projectId",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"indexes": {},
|
|
||||||
"foreignKeys": {
|
|
||||||
"environment_projectId_project_projectId_fk": {
|
|
||||||
"name": "environment_projectId_project_projectId_fk",
|
|
||||||
"tableFrom": "environment",
|
|
||||||
"tableTo": "project",
|
|
||||||
"columnsFrom": [
|
|
||||||
"projectId"
|
|
||||||
],
|
|
||||||
"columnsTo": [
|
|
||||||
"projectId"
|
|
||||||
],
|
|
||||||
"onDelete": "cascade",
|
|
||||||
"onUpdate": "no action"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"compositePrimaryKeys": {},
|
|
||||||
"uniqueConstraints": {},
|
|
||||||
"policies": {},
|
|
||||||
"checkConstraints": {},
|
|
||||||
"isRLSEnabled": false
|
|
||||||
},
|
|
||||||
"public.git_provider": {
|
"public.git_provider": {
|
||||||
"name": "git_provider",
|
"name": "git_provider",
|
||||||
"schema": "",
|
"schema": "",
|
||||||
@@ -3272,8 +3215,8 @@
|
|||||||
"primaryKey": false,
|
"primaryKey": false,
|
||||||
"notNull": true
|
"notNull": true
|
||||||
},
|
},
|
||||||
"environmentId": {
|
"projectId": {
|
||||||
"name": "environmentId",
|
"name": "projectId",
|
||||||
"type": "text",
|
"type": "text",
|
||||||
"primaryKey": false,
|
"primaryKey": false,
|
||||||
"notNull": true
|
"notNull": true
|
||||||
@@ -3287,15 +3230,15 @@
|
|||||||
},
|
},
|
||||||
"indexes": {},
|
"indexes": {},
|
||||||
"foreignKeys": {
|
"foreignKeys": {
|
||||||
"mariadb_environmentId_environment_environmentId_fk": {
|
"mariadb_projectId_project_projectId_fk": {
|
||||||
"name": "mariadb_environmentId_environment_environmentId_fk",
|
"name": "mariadb_projectId_project_projectId_fk",
|
||||||
"tableFrom": "mariadb",
|
"tableFrom": "mariadb",
|
||||||
"tableTo": "environment",
|
"tableTo": "project",
|
||||||
"columnsFrom": [
|
"columnsFrom": [
|
||||||
"environmentId"
|
"projectId"
|
||||||
],
|
],
|
||||||
"columnsTo": [
|
"columnsTo": [
|
||||||
"environmentId"
|
"projectId"
|
||||||
],
|
],
|
||||||
"onDelete": "cascade",
|
"onDelete": "cascade",
|
||||||
"onUpdate": "no action"
|
"onUpdate": "no action"
|
||||||
@@ -3485,8 +3428,8 @@
|
|||||||
"primaryKey": false,
|
"primaryKey": false,
|
||||||
"notNull": true
|
"notNull": true
|
||||||
},
|
},
|
||||||
"environmentId": {
|
"projectId": {
|
||||||
"name": "environmentId",
|
"name": "projectId",
|
||||||
"type": "text",
|
"type": "text",
|
||||||
"primaryKey": false,
|
"primaryKey": false,
|
||||||
"notNull": true
|
"notNull": true
|
||||||
@@ -3507,15 +3450,15 @@
|
|||||||
},
|
},
|
||||||
"indexes": {},
|
"indexes": {},
|
||||||
"foreignKeys": {
|
"foreignKeys": {
|
||||||
"mongo_environmentId_environment_environmentId_fk": {
|
"mongo_projectId_project_projectId_fk": {
|
||||||
"name": "mongo_environmentId_environment_environmentId_fk",
|
"name": "mongo_projectId_project_projectId_fk",
|
||||||
"tableFrom": "mongo",
|
"tableFrom": "mongo",
|
||||||
"tableTo": "environment",
|
"tableTo": "project",
|
||||||
"columnsFrom": [
|
"columnsFrom": [
|
||||||
"environmentId"
|
"projectId"
|
||||||
],
|
],
|
||||||
"columnsTo": [
|
"columnsTo": [
|
||||||
"environmentId"
|
"projectId"
|
||||||
],
|
],
|
||||||
"onDelete": "cascade",
|
"onDelete": "cascade",
|
||||||
"onUpdate": "no action"
|
"onUpdate": "no action"
|
||||||
@@ -3915,8 +3858,8 @@
|
|||||||
"primaryKey": false,
|
"primaryKey": false,
|
||||||
"notNull": true
|
"notNull": true
|
||||||
},
|
},
|
||||||
"environmentId": {
|
"projectId": {
|
||||||
"name": "environmentId",
|
"name": "projectId",
|
||||||
"type": "text",
|
"type": "text",
|
||||||
"primaryKey": false,
|
"primaryKey": false,
|
||||||
"notNull": true
|
"notNull": true
|
||||||
@@ -3930,15 +3873,15 @@
|
|||||||
},
|
},
|
||||||
"indexes": {},
|
"indexes": {},
|
||||||
"foreignKeys": {
|
"foreignKeys": {
|
||||||
"mysql_environmentId_environment_environmentId_fk": {
|
"mysql_projectId_project_projectId_fk": {
|
||||||
"name": "mysql_environmentId_environment_environmentId_fk",
|
"name": "mysql_projectId_project_projectId_fk",
|
||||||
"tableFrom": "mysql",
|
"tableFrom": "mysql",
|
||||||
"tableTo": "environment",
|
"tableTo": "project",
|
||||||
"columnsFrom": [
|
"columnsFrom": [
|
||||||
"environmentId"
|
"projectId"
|
||||||
],
|
],
|
||||||
"columnsTo": [
|
"columnsTo": [
|
||||||
"environmentId"
|
"projectId"
|
||||||
],
|
],
|
||||||
"onDelete": "cascade",
|
"onDelete": "cascade",
|
||||||
"onUpdate": "no action"
|
"onUpdate": "no action"
|
||||||
@@ -4593,8 +4536,8 @@
|
|||||||
"primaryKey": false,
|
"primaryKey": false,
|
||||||
"notNull": true
|
"notNull": true
|
||||||
},
|
},
|
||||||
"environmentId": {
|
"projectId": {
|
||||||
"name": "environmentId",
|
"name": "projectId",
|
||||||
"type": "text",
|
"type": "text",
|
||||||
"primaryKey": false,
|
"primaryKey": false,
|
||||||
"notNull": true
|
"notNull": true
|
||||||
@@ -4608,15 +4551,15 @@
|
|||||||
},
|
},
|
||||||
"indexes": {},
|
"indexes": {},
|
||||||
"foreignKeys": {
|
"foreignKeys": {
|
||||||
"postgres_environmentId_environment_environmentId_fk": {
|
"postgres_projectId_project_projectId_fk": {
|
||||||
"name": "postgres_environmentId_environment_environmentId_fk",
|
"name": "postgres_projectId_project_projectId_fk",
|
||||||
"tableFrom": "postgres",
|
"tableFrom": "postgres",
|
||||||
"tableTo": "environment",
|
"tableTo": "project",
|
||||||
"columnsFrom": [
|
"columnsFrom": [
|
||||||
"environmentId"
|
"projectId"
|
||||||
],
|
],
|
||||||
"columnsTo": [
|
"columnsTo": [
|
||||||
"environmentId"
|
"projectId"
|
||||||
],
|
],
|
||||||
"onDelete": "cascade",
|
"onDelete": "cascade",
|
||||||
"onUpdate": "no action"
|
"onUpdate": "no action"
|
||||||
@@ -5062,8 +5005,8 @@
|
|||||||
"notNull": true,
|
"notNull": true,
|
||||||
"default": 1
|
"default": 1
|
||||||
},
|
},
|
||||||
"environmentId": {
|
"projectId": {
|
||||||
"name": "environmentId",
|
"name": "projectId",
|
||||||
"type": "text",
|
"type": "text",
|
||||||
"primaryKey": false,
|
"primaryKey": false,
|
||||||
"notNull": true
|
"notNull": true
|
||||||
@@ -5077,15 +5020,15 @@
|
|||||||
},
|
},
|
||||||
"indexes": {},
|
"indexes": {},
|
||||||
"foreignKeys": {
|
"foreignKeys": {
|
||||||
"redis_environmentId_environment_environmentId_fk": {
|
"redis_projectId_project_projectId_fk": {
|
||||||
"name": "redis_environmentId_environment_environmentId_fk",
|
"name": "redis_projectId_project_projectId_fk",
|
||||||
"tableFrom": "redis",
|
"tableFrom": "redis",
|
||||||
"tableTo": "environment",
|
"tableTo": "project",
|
||||||
"columnsFrom": [
|
"columnsFrom": [
|
||||||
"environmentId"
|
"projectId"
|
||||||
],
|
],
|
||||||
"columnsTo": [
|
"columnsTo": [
|
||||||
"environmentId"
|
"projectId"
|
||||||
],
|
],
|
||||||
"onDelete": "cascade",
|
"onDelete": "cascade",
|
||||||
"onUpdate": "no action"
|
"onUpdate": "no action"
|
||||||
@@ -5579,6 +5522,13 @@
|
|||||||
"primaryKey": false,
|
"primaryKey": false,
|
||||||
"notNull": false
|
"notNull": false
|
||||||
},
|
},
|
||||||
|
"concurrency": {
|
||||||
|
"name": "concurrency",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": 1
|
||||||
|
},
|
||||||
"metricsConfig": {
|
"metricsConfig": {
|
||||||
"name": "metricsConfig",
|
"name": "metricsConfig",
|
||||||
"type": "jsonb",
|
"type": "jsonb",
|
||||||
@@ -5958,6 +5908,13 @@
|
|||||||
"notNull": true,
|
"notNull": true,
|
||||||
"default": false
|
"default": false
|
||||||
},
|
},
|
||||||
|
"serverConcurrency": {
|
||||||
|
"name": "serverConcurrency",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": 1
|
||||||
|
},
|
||||||
"metricsConfig": {
|
"metricsConfig": {
|
||||||
"name": "metricsConfig",
|
"name": "metricsConfig",
|
||||||
"type": "jsonb",
|
"type": "jsonb",
|
||||||
|
|||||||
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
@@ -754,29 +754,8 @@
|
|||||||
{
|
{
|
||||||
"idx": 107,
|
"idx": 107,
|
||||||
"version": "7",
|
"version": "7",
|
||||||
"when": 1756793713380,
|
"when": 1756436825081,
|
||||||
"tag": "0107_loud_kang",
|
"tag": "0107_calm_power_pack",
|
||||||
"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
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "dokploy",
|
"name": "dokploy",
|
||||||
"version": "v0.25.2",
|
"version": "v0.25.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
@@ -97,7 +97,6 @@
|
|||||||
"better-auth": "v1.2.8-beta.7",
|
"better-auth": "v1.2.8-beta.7",
|
||||||
"bl": "6.0.11",
|
"bl": "6.0.11",
|
||||||
"boxen": "^7.1.1",
|
"boxen": "^7.1.1",
|
||||||
"bullmq": "5.4.2",
|
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"cmdk": "^0.2.1",
|
"cmdk": "^0.2.1",
|
||||||
@@ -126,6 +125,7 @@
|
|||||||
"nodemailer": "6.9.14",
|
"nodemailer": "6.9.14",
|
||||||
"octokit": "3.1.2",
|
"octokit": "3.1.2",
|
||||||
"otpauth": "^9.4.0",
|
"otpauth": "^9.4.0",
|
||||||
|
"p-limit": "^7.1.1",
|
||||||
"pino": "9.4.0",
|
"pino": "9.4.0",
|
||||||
"pino-pretty": "11.2.2",
|
"pino-pretty": "11.2.2",
|
||||||
"postgres": "3.4.4",
|
"postgres": "3.4.4",
|
||||||
|
|||||||
@@ -20,11 +20,7 @@ export default async function handler(
|
|||||||
const application = await db.query.applications.findFirst({
|
const application = await db.query.applications.findFirst({
|
||||||
where: eq(applications.refreshToken, refreshToken as string),
|
where: eq(applications.refreshToken, refreshToken as string),
|
||||||
with: {
|
with: {
|
||||||
environment: {
|
project: true,
|
||||||
with: {
|
|
||||||
project: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
bitbucket: true,
|
bitbucket: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -27,11 +27,7 @@ export default async function handler(
|
|||||||
const composeResult = await db.query.compose.findFirst({
|
const composeResult = await db.query.compose.findFirst({
|
||||||
where: eq(compose.refreshToken, refreshToken as string),
|
where: eq(compose.refreshToken, refreshToken as string),
|
||||||
with: {
|
with: {
|
||||||
environment: {
|
project: true,
|
||||||
with: {
|
|
||||||
project: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
bitbucket: 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 [_toggleMonitoring, _setToggleMonitoring] = useState(false);
|
||||||
const { applicationId, activeTab } = props;
|
const { applicationId, activeTab } = props;
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { projectId, environmentId } = router.query;
|
const { projectId } = router.query;
|
||||||
const [tab, setTab] = useState<TabState>(activeTab);
|
const [tab, setTab] = useState<TabState>(activeTab);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -97,20 +97,18 @@ const Service = (
|
|||||||
list={[
|
list={[
|
||||||
{ name: "Projects", href: "/dashboard/projects" },
|
{ name: "Projects", href: "/dashboard/projects" },
|
||||||
{
|
{
|
||||||
name: data?.environment.project.name || "",
|
name: data?.project?.name || "",
|
||||||
},
|
href: `/dashboard/project/${projectId}`,
|
||||||
{
|
|
||||||
name: data?.environment?.name || "",
|
|
||||||
href: `/dashboard/project/${projectId}/environment/${environmentId}`,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: data?.name || "",
|
name: data?.name || "",
|
||||||
|
href: `/dashboard/project/${projectId}/services/application/${applicationId}`,
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
<Head>
|
<Head>
|
||||||
<title>
|
<title>
|
||||||
Application: {data?.name} - {data?.environment.project.name} | Dokploy
|
Application: {data?.name} - {data?.project.name} | Dokploy
|
||||||
</title>
|
</title>
|
||||||
</Head>
|
</Head>
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
@@ -217,7 +215,7 @@ const Service = (
|
|||||||
className="w-full"
|
className="w-full"
|
||||||
onValueChange={(e) => {
|
onValueChange={(e) => {
|
||||||
setTab(e as TabState);
|
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);
|
router.push(newPath);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -381,7 +379,6 @@ export async function getServerSideProps(
|
|||||||
ctx: GetServerSidePropsContext<{
|
ctx: GetServerSidePropsContext<{
|
||||||
applicationId: string;
|
applicationId: string;
|
||||||
activeTab: TabState;
|
activeTab: TabState;
|
||||||
environmentId: string;
|
|
||||||
}>,
|
}>,
|
||||||
) {
|
) {
|
||||||
const { query, params, req, res } = ctx;
|
const { query, params, req, res } = ctx;
|
||||||
@@ -423,7 +420,6 @@ export async function getServerSideProps(
|
|||||||
trpcState: helpers.dehydrate(),
|
trpcState: helpers.dehydrate(),
|
||||||
applicationId: params?.applicationId,
|
applicationId: params?.applicationId,
|
||||||
activeTab: (activeTab || "general") as TabState,
|
activeTab: (activeTab || "general") as TabState,
|
||||||
environmentId: params?.environmentId,
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
} catch {
|
} catch {
|
||||||
@@ -67,7 +67,7 @@ const Service = (
|
|||||||
const [_toggleMonitoring, _setToggleMonitoring] = useState(false);
|
const [_toggleMonitoring, _setToggleMonitoring] = useState(false);
|
||||||
const { composeId, activeTab } = props;
|
const { composeId, activeTab } = props;
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { projectId, environmentId } = router.query;
|
const { projectId } = router.query;
|
||||||
const [tab, setTab] = useState<TabState>(activeTab);
|
const [tab, setTab] = useState<TabState>(activeTab);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -88,20 +88,18 @@ const Service = (
|
|||||||
list={[
|
list={[
|
||||||
{ name: "Projects", href: "/dashboard/projects" },
|
{ name: "Projects", href: "/dashboard/projects" },
|
||||||
{
|
{
|
||||||
name: data?.environment?.project?.name || "",
|
name: data?.project?.name || "",
|
||||||
},
|
href: `/dashboard/project/${projectId}`,
|
||||||
{
|
|
||||||
name: data?.environment?.name || "",
|
|
||||||
href: `/dashboard/project/${projectId}/environment/${environmentId}`,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: data?.name || "",
|
name: data?.name || "",
|
||||||
|
href: `/dashboard/project/${projectId}/services/compose/${composeId}`,
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
<Head>
|
<Head>
|
||||||
<title>
|
<title>
|
||||||
Compose: {data?.name} - {data?.environment?.project?.name} | Dokploy
|
Compose: {data?.name} - {data?.project.name} | Dokploy
|
||||||
</title>
|
</title>
|
||||||
</Head>
|
</Head>
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
@@ -210,7 +208,7 @@ const Service = (
|
|||||||
className="w-full"
|
className="w-full"
|
||||||
onValueChange={(e) => {
|
onValueChange={(e) => {
|
||||||
setTab(e as TabState);
|
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);
|
router.push(newPath);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -377,7 +375,6 @@ export async function getServerSideProps(
|
|||||||
ctx: GetServerSidePropsContext<{
|
ctx: GetServerSidePropsContext<{
|
||||||
composeId: string;
|
composeId: string;
|
||||||
activeTab: TabState;
|
activeTab: TabState;
|
||||||
environmentId: string;
|
|
||||||
}>,
|
}>,
|
||||||
) {
|
) {
|
||||||
const { query, params, req, res } = ctx;
|
const { query, params, req, res } = ctx;
|
||||||
@@ -417,7 +414,6 @@ export async function getServerSideProps(
|
|||||||
trpcState: helpers.dehydrate(),
|
trpcState: helpers.dehydrate(),
|
||||||
composeId: params?.composeId,
|
composeId: params?.composeId,
|
||||||
activeTab: (activeTab || "general") as TabState,
|
activeTab: (activeTab || "general") as TabState,
|
||||||
environmentId: params?.environmentId,
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
} catch {
|
} catch {
|
||||||
@@ -55,7 +55,7 @@ const Mariadb = (
|
|||||||
|
|
||||||
const { mariadbId, activeTab } = props;
|
const { mariadbId, activeTab } = props;
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { projectId, environmentId } = router.query;
|
const { projectId } = router.query;
|
||||||
const [tab, setSab] = useState<TabState>(activeTab);
|
const [tab, setSab] = useState<TabState>(activeTab);
|
||||||
const { data } = api.mariadb.one.useQuery({ mariadbId });
|
const { data } = api.mariadb.one.useQuery({ mariadbId });
|
||||||
const { data: auth } = api.user.get.useQuery();
|
const { data: auth } = api.user.get.useQuery();
|
||||||
@@ -69,22 +69,19 @@ const Mariadb = (
|
|||||||
list={[
|
list={[
|
||||||
{ name: "Projects", href: "/dashboard/projects" },
|
{ name: "Projects", href: "/dashboard/projects" },
|
||||||
{
|
{
|
||||||
name: data?.environment?.project?.name || "",
|
name: data?.project?.name || "",
|
||||||
},
|
href: `/dashboard/project/${projectId}`,
|
||||||
{
|
|
||||||
name: data?.environment?.name || "",
|
|
||||||
href: `/dashboard/project/${projectId}/environment/${environmentId}`,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: data?.name || "",
|
name: data?.name || "",
|
||||||
|
href: `/dashboard/project/${projectId}/services/mariadb/${mariadbId}`,
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
<Head>
|
<Head>
|
||||||
<title>
|
<title>
|
||||||
Database: {data?.name} - {data?.environment?.project?.name} |
|
Database: {data?.name} - {data?.project.name} | Dokploy
|
||||||
Dokploy
|
|
||||||
</title>
|
</title>
|
||||||
</Head>
|
</Head>
|
||||||
<Card className="h-full bg-sidebar p-2.5 rounded-xl w-full">
|
<Card className="h-full bg-sidebar p-2.5 rounded-xl w-full">
|
||||||
@@ -182,7 +179,7 @@ const Mariadb = (
|
|||||||
className="w-full"
|
className="w-full"
|
||||||
onValueChange={(e) => {
|
onValueChange={(e) => {
|
||||||
setSab(e as TabState);
|
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 });
|
router.push(newPath, undefined, { shallow: true });
|
||||||
}}
|
}}
|
||||||
@@ -303,11 +300,7 @@ Mariadb.getLayout = (page: ReactElement) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export async function getServerSideProps(
|
export async function getServerSideProps(
|
||||||
ctx: GetServerSidePropsContext<{
|
ctx: GetServerSidePropsContext<{ mariadbId: string; activeTab: TabState }>,
|
||||||
mariadbId: string;
|
|
||||||
activeTab: TabState;
|
|
||||||
environmentId: string;
|
|
||||||
}>,
|
|
||||||
) {
|
) {
|
||||||
const { query, params, req, res } = ctx;
|
const { query, params, req, res } = ctx;
|
||||||
const activeTab = query.tab;
|
const activeTab = query.tab;
|
||||||
@@ -345,7 +338,6 @@ export async function getServerSideProps(
|
|||||||
trpcState: helpers.dehydrate(),
|
trpcState: helpers.dehydrate(),
|
||||||
mariadbId: params?.mariadbId,
|
mariadbId: params?.mariadbId,
|
||||||
activeTab: (activeTab || "general") as TabState,
|
activeTab: (activeTab || "general") as TabState,
|
||||||
environmentId: params?.environmentId,
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
} catch {
|
} catch {
|
||||||
@@ -54,7 +54,7 @@ const Mongo = (
|
|||||||
const [_toggleMonitoring, _setToggleMonitoring] = useState(false);
|
const [_toggleMonitoring, _setToggleMonitoring] = useState(false);
|
||||||
const { mongoId, activeTab } = props;
|
const { mongoId, activeTab } = props;
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { projectId, environmentId } = router.query;
|
const { projectId } = router.query;
|
||||||
const [tab, setSab] = useState<TabState>(activeTab);
|
const [tab, setSab] = useState<TabState>(activeTab);
|
||||||
const { data } = api.mongo.one.useQuery({ mongoId });
|
const { data } = api.mongo.one.useQuery({ mongoId });
|
||||||
|
|
||||||
@@ -69,20 +69,18 @@ const Mongo = (
|
|||||||
list={[
|
list={[
|
||||||
{ name: "Projects", href: "/dashboard/projects" },
|
{ name: "Projects", href: "/dashboard/projects" },
|
||||||
{
|
{
|
||||||
name: data?.environment?.project?.name || "",
|
name: data?.project?.name || "",
|
||||||
},
|
href: `/dashboard/project/${projectId}`,
|
||||||
{
|
|
||||||
name: data?.environment?.name || "",
|
|
||||||
href: `/dashboard/project/${projectId}/environment/${environmentId}`,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: data?.name || "",
|
name: data?.name || "",
|
||||||
|
href: `/dashboard/project/${projectId}/services/mongo/${mongoId}`,
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
<Head>
|
<Head>
|
||||||
<title>
|
<title>
|
||||||
Database: {data?.name} - {data?.environment?.project?.name} | Dokploy
|
Database: {data?.name} - {data?.project.name} | Dokploy
|
||||||
</title>
|
</title>
|
||||||
</Head>
|
</Head>
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
@@ -182,7 +180,7 @@ const Mongo = (
|
|||||||
className="w-full"
|
className="w-full"
|
||||||
onValueChange={(e) => {
|
onValueChange={(e) => {
|
||||||
setSab(e as TabState);
|
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 });
|
router.push(newPath, undefined, { shallow: true });
|
||||||
}}
|
}}
|
||||||
@@ -304,11 +302,7 @@ Mongo.getLayout = (page: ReactElement) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export async function getServerSideProps(
|
export async function getServerSideProps(
|
||||||
ctx: GetServerSidePropsContext<{
|
ctx: GetServerSidePropsContext<{ mongoId: string; activeTab: TabState }>,
|
||||||
mongoId: string;
|
|
||||||
activeTab: TabState;
|
|
||||||
environmentId: string;
|
|
||||||
}>,
|
|
||||||
) {
|
) {
|
||||||
const { query, params, req, res } = ctx;
|
const { query, params, req, res } = ctx;
|
||||||
const activeTab = query.tab;
|
const activeTab = query.tab;
|
||||||
@@ -346,7 +340,6 @@ export async function getServerSideProps(
|
|||||||
trpcState: helpers.dehydrate(),
|
trpcState: helpers.dehydrate(),
|
||||||
mongoId: params?.mongoId,
|
mongoId: params?.mongoId,
|
||||||
activeTab: (activeTab || "general") as TabState,
|
activeTab: (activeTab || "general") as TabState,
|
||||||
environmentId: params?.environmentId,
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
} catch {
|
} catch {
|
||||||
@@ -54,7 +54,7 @@ const MySql = (
|
|||||||
const [_toggleMonitoring, _setToggleMonitoring] = useState(false);
|
const [_toggleMonitoring, _setToggleMonitoring] = useState(false);
|
||||||
const { mysqlId, activeTab } = props;
|
const { mysqlId, activeTab } = props;
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { projectId, environmentId } = router.query;
|
const { projectId } = router.query;
|
||||||
const [tab, setSab] = useState<TabState>(activeTab);
|
const [tab, setSab] = useState<TabState>(activeTab);
|
||||||
const { data } = api.mysql.one.useQuery({ mysqlId });
|
const { data } = api.mysql.one.useQuery({ mysqlId });
|
||||||
const { data: auth } = api.user.get.useQuery();
|
const { data: auth } = api.user.get.useQuery();
|
||||||
@@ -68,22 +68,19 @@ const MySql = (
|
|||||||
list={[
|
list={[
|
||||||
{ name: "Projects", href: "/dashboard/projects" },
|
{ name: "Projects", href: "/dashboard/projects" },
|
||||||
{
|
{
|
||||||
name: data?.environment?.project?.name || "",
|
name: data?.project?.name || "",
|
||||||
},
|
href: `/dashboard/project/${projectId}`,
|
||||||
{
|
|
||||||
name: data?.environment?.name || "",
|
|
||||||
href: `/dashboard/project/${projectId}/environment/${environmentId}`,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: data?.name || "",
|
name: data?.name || "",
|
||||||
|
href: `/dashboard/project/${projectId}/services/mysql/${mysqlId}`,
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
<Head>
|
<Head>
|
||||||
<title>
|
<title>
|
||||||
Database: {data?.name} - {data?.environment?.project?.name} |
|
Database: {data?.name} - {data?.project.name} | Dokploy
|
||||||
Dokploy
|
|
||||||
</title>
|
</title>
|
||||||
</Head>
|
</Head>
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
@@ -183,7 +180,7 @@ const MySql = (
|
|||||||
className="w-full"
|
className="w-full"
|
||||||
onValueChange={(e) => {
|
onValueChange={(e) => {
|
||||||
setSab(e as TabState);
|
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 });
|
router.push(newPath, undefined, { shallow: true });
|
||||||
}}
|
}}
|
||||||
@@ -289,11 +286,7 @@ MySql.getLayout = (page: ReactElement) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export async function getServerSideProps(
|
export async function getServerSideProps(
|
||||||
ctx: GetServerSidePropsContext<{
|
ctx: GetServerSidePropsContext<{ mysqlId: string; activeTab: TabState }>,
|
||||||
mysqlId: string;
|
|
||||||
activeTab: TabState;
|
|
||||||
environmentId: string;
|
|
||||||
}>,
|
|
||||||
) {
|
) {
|
||||||
const { query, params, req, res } = ctx;
|
const { query, params, req, res } = ctx;
|
||||||
const activeTab = query.tab;
|
const activeTab = query.tab;
|
||||||
@@ -54,7 +54,7 @@ const Postgresql = (
|
|||||||
const [_toggleMonitoring, _setToggleMonitoring] = useState(false);
|
const [_toggleMonitoring, _setToggleMonitoring] = useState(false);
|
||||||
const { postgresId, activeTab } = props;
|
const { postgresId, activeTab } = props;
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { projectId, environmentId } = router.query;
|
const { projectId } = router.query;
|
||||||
const [tab, setSab] = useState<TabState>(activeTab);
|
const [tab, setSab] = useState<TabState>(activeTab);
|
||||||
const { data } = api.postgres.one.useQuery({ postgresId });
|
const { data } = api.postgres.one.useQuery({ postgresId });
|
||||||
const { data: auth } = api.user.get.useQuery();
|
const { data: auth } = api.user.get.useQuery();
|
||||||
@@ -68,20 +68,18 @@ const Postgresql = (
|
|||||||
list={[
|
list={[
|
||||||
{ name: "Projects", href: "/dashboard/projects" },
|
{ name: "Projects", href: "/dashboard/projects" },
|
||||||
{
|
{
|
||||||
name: data?.environment?.project?.name || "",
|
name: data?.project?.name || "",
|
||||||
},
|
href: `/dashboard/project/${projectId}`,
|
||||||
{
|
|
||||||
name: data?.environment?.name || "",
|
|
||||||
href: `/dashboard/project/${projectId}/environment/${environmentId}`,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: data?.name || "",
|
name: data?.name || "",
|
||||||
|
href: `/dashboard/project/${projectId}/services/postgres/${postgresId}`,
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
<Head>
|
<Head>
|
||||||
<title>
|
<title>
|
||||||
Database: {data?.name} - {data?.environment?.project?.name} | Dokploy
|
Database: {data?.name} - {data?.project.name} | Dokploy
|
||||||
</title>
|
</title>
|
||||||
</Head>
|
</Head>
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
@@ -181,11 +179,9 @@ const Postgresql = (
|
|||||||
className="w-full"
|
className="w-full"
|
||||||
onValueChange={(e) => {
|
onValueChange={(e) => {
|
||||||
setSab(e as TabState);
|
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, {
|
router.push(newPath, undefined, { shallow: true });
|
||||||
shallow: true,
|
|
||||||
});
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="flex flex-row items-center justify-between w-full gap-4 overflow-x-scroll">
|
<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 ? (
|
{data?.serverId && isCloud ? (
|
||||||
<ContainerPaidMonitoring
|
<ContainerPaidMonitoring
|
||||||
appName={data?.appName || ""}
|
appName={data?.appName || ""}
|
||||||
baseUrl={`${
|
baseUrl={`${data?.serverId ? `http://${data?.server?.ipAddress}:${data?.server?.metricsConfig?.server?.port}` : "http://localhost:4500"}`}
|
||||||
data?.serverId
|
|
||||||
? `http://${data?.server?.ipAddress}:${data?.server?.metricsConfig?.server?.port}`
|
|
||||||
: "http://localhost:4500"
|
|
||||||
}`}
|
|
||||||
token={
|
token={
|
||||||
data?.server?.metricsConfig?.server?.token || ""
|
data?.server?.metricsConfig?.server?.token || ""
|
||||||
}
|
}
|
||||||
@@ -292,11 +284,7 @@ Postgresql.getLayout = (page: ReactElement) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export async function getServerSideProps(
|
export async function getServerSideProps(
|
||||||
ctx: GetServerSidePropsContext<{
|
ctx: GetServerSidePropsContext<{ postgresId: string; activeTab: TabState }>,
|
||||||
postgresId: string;
|
|
||||||
activeTab: TabState;
|
|
||||||
environmentId: string;
|
|
||||||
}>,
|
|
||||||
) {
|
) {
|
||||||
const { query, params, req, res } = ctx;
|
const { query, params, req, res } = ctx;
|
||||||
const activeTab = query.tab;
|
const activeTab = query.tab;
|
||||||
@@ -53,7 +53,7 @@ const Redis = (
|
|||||||
const [_toggleMonitoring, _setToggleMonitoring] = useState(false);
|
const [_toggleMonitoring, _setToggleMonitoring] = useState(false);
|
||||||
const { redisId, activeTab } = props;
|
const { redisId, activeTab } = props;
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { projectId, environmentId } = router.query;
|
const { projectId } = router.query;
|
||||||
const [tab, setSab] = useState<TabState>(activeTab);
|
const [tab, setSab] = useState<TabState>(activeTab);
|
||||||
const { data } = api.redis.one.useQuery({ redisId });
|
const { data } = api.redis.one.useQuery({ redisId });
|
||||||
|
|
||||||
@@ -68,20 +68,18 @@ const Redis = (
|
|||||||
list={[
|
list={[
|
||||||
{ name: "Projects", href: "/dashboard/projects" },
|
{ name: "Projects", href: "/dashboard/projects" },
|
||||||
{
|
{
|
||||||
name: data?.environment?.project?.name || "",
|
name: data?.project?.name || "",
|
||||||
},
|
href: `/dashboard/project/${projectId}`,
|
||||||
{
|
|
||||||
name: data?.environment?.name || "",
|
|
||||||
href: `/dashboard/project/${projectId}/environment/${environmentId}`,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: data?.name || "",
|
name: data?.name || "",
|
||||||
|
href: `/dashboard/project/${projectId}/services/redis/${redisId}`,
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
<Head>
|
<Head>
|
||||||
<title>
|
<title>
|
||||||
Database: {data?.name} - {data?.environment?.project?.name} | Dokploy
|
Database: {data?.name} - {data?.project.name} | Dokploy
|
||||||
</title>
|
</title>
|
||||||
</Head>
|
</Head>
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
@@ -181,7 +179,7 @@ const Redis = (
|
|||||||
className="w-full"
|
className="w-full"
|
||||||
onValueChange={(e) => {
|
onValueChange={(e) => {
|
||||||
setSab(e as TabState);
|
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 });
|
router.push(newPath, undefined, { shallow: true });
|
||||||
}}
|
}}
|
||||||
@@ -293,11 +291,7 @@ Redis.getLayout = (page: ReactElement) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export async function getServerSideProps(
|
export async function getServerSideProps(
|
||||||
ctx: GetServerSidePropsContext<{
|
ctx: GetServerSidePropsContext<{ redisId: string; activeTab: TabState }>,
|
||||||
redisId: string;
|
|
||||||
activeTab: TabState;
|
|
||||||
environmentId: string;
|
|
||||||
}>,
|
|
||||||
) {
|
) {
|
||||||
const { query, params, req, res } = ctx;
|
const { query, params, req, res } = ctx;
|
||||||
const activeTab = query.tab;
|
const activeTab = query.tab;
|
||||||
@@ -11,7 +11,6 @@ import { deploymentRouter } from "./routers/deployment";
|
|||||||
import { destinationRouter } from "./routers/destination";
|
import { destinationRouter } from "./routers/destination";
|
||||||
import { dockerRouter } from "./routers/docker";
|
import { dockerRouter } from "./routers/docker";
|
||||||
import { domainRouter } from "./routers/domain";
|
import { domainRouter } from "./routers/domain";
|
||||||
import { environmentRouter } from "./routers/environment";
|
|
||||||
import { gitProviderRouter } from "./routers/git-provider";
|
import { gitProviderRouter } from "./routers/git-provider";
|
||||||
import { giteaRouter } from "./routers/gitea";
|
import { giteaRouter } from "./routers/gitea";
|
||||||
import { githubRouter } from "./routers/github";
|
import { githubRouter } from "./routers/github";
|
||||||
@@ -85,7 +84,6 @@ export const appRouter = createTRPCRouter({
|
|||||||
schedule: scheduleRouter,
|
schedule: scheduleRouter,
|
||||||
rollback: rollbackRouter,
|
rollback: rollbackRouter,
|
||||||
volumeBackups: volumeBackupsRouter,
|
volumeBackups: volumeBackupsRouter,
|
||||||
environment: environmentRouter,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// export type definition of API
|
// export type definition of API
|
||||||
|
|||||||
@@ -4,11 +4,7 @@ import {
|
|||||||
apiUpdateAi,
|
apiUpdateAi,
|
||||||
deploySuggestionSchema,
|
deploySuggestionSchema,
|
||||||
} from "@dokploy/server/db/schema/ai";
|
} from "@dokploy/server/db/schema/ai";
|
||||||
import {
|
import { createDomain, createMount } from "@dokploy/server/index";
|
||||||
createDomain,
|
|
||||||
createMount,
|
|
||||||
findEnvironmentById,
|
|
||||||
} from "@dokploy/server/index";
|
|
||||||
import {
|
import {
|
||||||
deleteAiSettings,
|
deleteAiSettings,
|
||||||
getAiSettingById,
|
getAiSettingById,
|
||||||
@@ -181,12 +177,10 @@ export const aiRouter = createTRPCRouter({
|
|||||||
deploy: protectedProcedure
|
deploy: protectedProcedure
|
||||||
.input(deploySuggestionSchema)
|
.input(deploySuggestionSchema)
|
||||||
.mutation(async ({ ctx, input }) => {
|
.mutation(async ({ ctx, input }) => {
|
||||||
const environment = await findEnvironmentById(input.environmentId);
|
|
||||||
const project = await findProjectById(environment.projectId);
|
|
||||||
if (ctx.user.role === "member") {
|
if (ctx.user.role === "member") {
|
||||||
await checkServiceAccess(
|
await checkServiceAccess(
|
||||||
ctx.session.activeOrganizationId,
|
ctx.session.activeOrganizationId,
|
||||||
environment.projectId,
|
input.projectId,
|
||||||
"create",
|
"create",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -198,6 +192,8 @@ export const aiRouter = createTRPCRouter({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const project = await findProjectById(input.projectId);
|
||||||
|
|
||||||
const projectName = slugify(`${project.name} ${input.id}`);
|
const projectName = slugify(`${project.name} ${input.id}`);
|
||||||
|
|
||||||
const compose = await createComposeByTemplate({
|
const compose = await createComposeByTemplate({
|
||||||
@@ -209,7 +205,6 @@ export const aiRouter = createTRPCRouter({
|
|||||||
sourceType: "raw",
|
sourceType: "raw",
|
||||||
appName: `${projectName}-${generatePassword(6)}`,
|
appName: `${projectName}-${generatePassword(6)}`,
|
||||||
isolatedDeployment: true,
|
isolatedDeployment: true,
|
||||||
environmentId: input.environmentId,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (input.domains && input.domains?.length > 0) {
|
if (input.domains && input.domains?.length > 0) {
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import {
|
|||||||
createApplication,
|
createApplication,
|
||||||
deleteAllMiddlewares,
|
deleteAllMiddlewares,
|
||||||
findApplicationById,
|
findApplicationById,
|
||||||
findEnvironmentById,
|
|
||||||
findGitProviderById,
|
findGitProviderById,
|
||||||
findProjectById,
|
findProjectById,
|
||||||
getApplicationStats,
|
getApplicationStats,
|
||||||
@@ -24,7 +23,6 @@ import {
|
|||||||
unzipDrop,
|
unzipDrop,
|
||||||
updateApplication,
|
updateApplication,
|
||||||
updateApplicationStatus,
|
updateApplicationStatus,
|
||||||
updateDeploymentStatus,
|
|
||||||
writeConfig,
|
writeConfig,
|
||||||
writeConfigRemote,
|
writeConfigRemote,
|
||||||
// uploadFileSchema
|
// uploadFileSchema
|
||||||
@@ -41,10 +39,8 @@ import {
|
|||||||
import { db } from "@/server/db";
|
import { db } from "@/server/db";
|
||||||
import {
|
import {
|
||||||
apiCreateApplication,
|
apiCreateApplication,
|
||||||
apiDeployApplication,
|
|
||||||
apiFindMonitoringStats,
|
apiFindMonitoringStats,
|
||||||
apiFindOneApplication,
|
apiFindOneApplication,
|
||||||
apiRedeployApplication,
|
|
||||||
apiReloadApplication,
|
apiReloadApplication,
|
||||||
apiSaveBitbucketProvider,
|
apiSaveBitbucketProvider,
|
||||||
apiSaveBuildType,
|
apiSaveBuildType,
|
||||||
@@ -58,8 +54,12 @@ import {
|
|||||||
applications,
|
applications,
|
||||||
} from "@/server/db/schema";
|
} from "@/server/db/schema";
|
||||||
import type { DeploymentJob } from "@/server/queues/queue-types";
|
import type { DeploymentJob } from "@/server/queues/queue-types";
|
||||||
import { cleanQueuesByApplication, myQueue } from "@/server/queues/queueSetup";
|
import {
|
||||||
import { cancelDeployment, deploy } from "@/server/utils/deploy";
|
addJobWithUserContext,
|
||||||
|
cleanQueuesByApplication,
|
||||||
|
myQueue,
|
||||||
|
} from "@/server/queues/queueSetup";
|
||||||
|
import { deploy } from "@/server/utils/deploy";
|
||||||
import { uploadFileSchema } from "@/utils/schema";
|
import { uploadFileSchema } from "@/utils/schema";
|
||||||
|
|
||||||
export const applicationRouter = createTRPCRouter({
|
export const applicationRouter = createTRPCRouter({
|
||||||
@@ -67,14 +67,10 @@ export const applicationRouter = createTRPCRouter({
|
|||||||
.input(apiCreateApplication)
|
.input(apiCreateApplication)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
try {
|
try {
|
||||||
// Get project from environment
|
|
||||||
const environment = await findEnvironmentById(input.environmentId);
|
|
||||||
const project = await findProjectById(environment.projectId);
|
|
||||||
|
|
||||||
if (ctx.user.role === "member") {
|
if (ctx.user.role === "member") {
|
||||||
await checkServiceAccess(
|
await checkServiceAccess(
|
||||||
ctx.user.id,
|
ctx.user.id,
|
||||||
project.projectId,
|
input.projectId,
|
||||||
ctx.session.activeOrganizationId,
|
ctx.session.activeOrganizationId,
|
||||||
"create",
|
"create",
|
||||||
);
|
);
|
||||||
@@ -87,13 +83,13 @@ export const applicationRouter = createTRPCRouter({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const project = await findProjectById(input.projectId);
|
||||||
if (project.organizationId !== ctx.session.activeOrganizationId) {
|
if (project.organizationId !== ctx.session.activeOrganizationId) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
message: "You are not authorized to access this project",
|
message: "You are not authorized to access this project",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const newApplication = await createApplication(input);
|
const newApplication = await createApplication(input);
|
||||||
|
|
||||||
if (ctx.user.role === "member") {
|
if (ctx.user.role === "member") {
|
||||||
@@ -105,7 +101,6 @@ export const applicationRouter = createTRPCRouter({
|
|||||||
}
|
}
|
||||||
return newApplication;
|
return newApplication;
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
console.log("error", error);
|
|
||||||
if (error instanceof TRPCError) {
|
if (error instanceof TRPCError) {
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
@@ -129,8 +124,7 @@ export const applicationRouter = createTRPCRouter({
|
|||||||
}
|
}
|
||||||
const application = await findApplicationById(input.applicationId);
|
const application = await findApplicationById(input.applicationId);
|
||||||
if (
|
if (
|
||||||
application.environment.project.organizationId !==
|
application.project.organizationId !== ctx.session.activeOrganizationId
|
||||||
ctx.session.activeOrganizationId
|
|
||||||
) {
|
) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
@@ -185,7 +179,7 @@ export const applicationRouter = createTRPCRouter({
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
if (
|
if (
|
||||||
application.environment.project.organizationId !==
|
application.project.organizationId !==
|
||||||
ctx.session.activeOrganizationId
|
ctx.session.activeOrganizationId
|
||||||
) {
|
) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
@@ -222,8 +216,7 @@ export const applicationRouter = createTRPCRouter({
|
|||||||
const application = await findApplicationById(input.applicationId);
|
const application = await findApplicationById(input.applicationId);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
application.environment.project.organizationId !==
|
application.project.organizationId !== ctx.session.activeOrganizationId
|
||||||
ctx.session.activeOrganizationId
|
|
||||||
) {
|
) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
@@ -258,17 +251,14 @@ export const applicationRouter = createTRPCRouter({
|
|||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
return application;
|
return result[0];
|
||||||
}),
|
}),
|
||||||
|
|
||||||
stop: protectedProcedure
|
stop: protectedProcedure
|
||||||
.input(apiFindOneApplication)
|
.input(apiFindOneApplication)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
const service = await findApplicationById(input.applicationId);
|
const service = await findApplicationById(input.applicationId);
|
||||||
if (
|
if (service.project.organizationId !== ctx.session.activeOrganizationId) {
|
||||||
service.environment.project.organizationId !==
|
|
||||||
ctx.session.activeOrganizationId
|
|
||||||
) {
|
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
message: "You are not authorized to stop this application",
|
message: "You are not authorized to stop this application",
|
||||||
@@ -288,10 +278,7 @@ export const applicationRouter = createTRPCRouter({
|
|||||||
.input(apiFindOneApplication)
|
.input(apiFindOneApplication)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
const service = await findApplicationById(input.applicationId);
|
const service = await findApplicationById(input.applicationId);
|
||||||
if (
|
if (service.project.organizationId !== ctx.session.activeOrganizationId) {
|
||||||
service.environment.project.organizationId !==
|
|
||||||
ctx.session.activeOrganizationId
|
|
||||||
) {
|
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
message: "You are not authorized to start this application",
|
message: "You are not authorized to start this application",
|
||||||
@@ -309,12 +296,11 @@ export const applicationRouter = createTRPCRouter({
|
|||||||
}),
|
}),
|
||||||
|
|
||||||
redeploy: protectedProcedure
|
redeploy: protectedProcedure
|
||||||
.input(apiRedeployApplication)
|
.input(apiFindOneApplication)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
const application = await findApplicationById(input.applicationId);
|
const application = await findApplicationById(input.applicationId);
|
||||||
if (
|
if (
|
||||||
application.environment.project.organizationId !==
|
application.project.organizationId !== ctx.session.activeOrganizationId
|
||||||
ctx.session.activeOrganizationId
|
|
||||||
) {
|
) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
@@ -323,8 +309,8 @@ export const applicationRouter = createTRPCRouter({
|
|||||||
}
|
}
|
||||||
const jobData: DeploymentJob = {
|
const jobData: DeploymentJob = {
|
||||||
applicationId: input.applicationId,
|
applicationId: input.applicationId,
|
||||||
titleLog: input.title || "Rebuild deployment",
|
titleLog: "Rebuild deployment",
|
||||||
descriptionLog: input.description || "",
|
descriptionLog: "",
|
||||||
type: "redeploy",
|
type: "redeploy",
|
||||||
applicationType: "application",
|
applicationType: "application",
|
||||||
server: !!application.serverId,
|
server: !!application.serverId,
|
||||||
@@ -349,8 +335,7 @@ export const applicationRouter = createTRPCRouter({
|
|||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
const application = await findApplicationById(input.applicationId);
|
const application = await findApplicationById(input.applicationId);
|
||||||
if (
|
if (
|
||||||
application.environment.project.organizationId !==
|
application.project.organizationId !== ctx.session.activeOrganizationId
|
||||||
ctx.session.activeOrganizationId
|
|
||||||
) {
|
) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
@@ -368,8 +353,7 @@ export const applicationRouter = createTRPCRouter({
|
|||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
const application = await findApplicationById(input.applicationId);
|
const application = await findApplicationById(input.applicationId);
|
||||||
if (
|
if (
|
||||||
application.environment.project.organizationId !==
|
application.project.organizationId !== ctx.session.activeOrganizationId
|
||||||
ctx.session.activeOrganizationId
|
|
||||||
) {
|
) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
@@ -394,8 +378,7 @@ export const applicationRouter = createTRPCRouter({
|
|||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
const application = await findApplicationById(input.applicationId);
|
const application = await findApplicationById(input.applicationId);
|
||||||
if (
|
if (
|
||||||
application.environment.project.organizationId !==
|
application.project.organizationId !== ctx.session.activeOrganizationId
|
||||||
ctx.session.activeOrganizationId
|
|
||||||
) {
|
) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
@@ -422,8 +405,7 @@ export const applicationRouter = createTRPCRouter({
|
|||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
const application = await findApplicationById(input.applicationId);
|
const application = await findApplicationById(input.applicationId);
|
||||||
if (
|
if (
|
||||||
application.environment.project.organizationId !==
|
application.project.organizationId !== ctx.session.activeOrganizationId
|
||||||
ctx.session.activeOrganizationId
|
|
||||||
) {
|
) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
@@ -451,8 +433,7 @@ export const applicationRouter = createTRPCRouter({
|
|||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
const application = await findApplicationById(input.applicationId);
|
const application = await findApplicationById(input.applicationId);
|
||||||
if (
|
if (
|
||||||
application.environment.project.organizationId !==
|
application.project.organizationId !== ctx.session.activeOrganizationId
|
||||||
ctx.session.activeOrganizationId
|
|
||||||
) {
|
) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
@@ -478,8 +459,7 @@ export const applicationRouter = createTRPCRouter({
|
|||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
const application = await findApplicationById(input.applicationId);
|
const application = await findApplicationById(input.applicationId);
|
||||||
if (
|
if (
|
||||||
application.environment.project.organizationId !==
|
application.project.organizationId !== ctx.session.activeOrganizationId
|
||||||
ctx.session.activeOrganizationId
|
|
||||||
) {
|
) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
@@ -505,8 +485,7 @@ export const applicationRouter = createTRPCRouter({
|
|||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
const application = await findApplicationById(input.applicationId);
|
const application = await findApplicationById(input.applicationId);
|
||||||
if (
|
if (
|
||||||
application.environment.project.organizationId !==
|
application.project.organizationId !== ctx.session.activeOrganizationId
|
||||||
ctx.session.activeOrganizationId
|
|
||||||
) {
|
) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
@@ -529,8 +508,7 @@ export const applicationRouter = createTRPCRouter({
|
|||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
const application = await findApplicationById(input.applicationId);
|
const application = await findApplicationById(input.applicationId);
|
||||||
if (
|
if (
|
||||||
application.environment.project.organizationId !==
|
application.project.organizationId !== ctx.session.activeOrganizationId
|
||||||
ctx.session.activeOrganizationId
|
|
||||||
) {
|
) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
@@ -555,8 +533,7 @@ export const applicationRouter = createTRPCRouter({
|
|||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
const application = await findApplicationById(input.applicationId);
|
const application = await findApplicationById(input.applicationId);
|
||||||
if (
|
if (
|
||||||
application.environment.project.organizationId !==
|
application.project.organizationId !== ctx.session.activeOrganizationId
|
||||||
ctx.session.activeOrganizationId
|
|
||||||
) {
|
) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
@@ -617,8 +594,7 @@ export const applicationRouter = createTRPCRouter({
|
|||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
const application = await findApplicationById(input.applicationId);
|
const application = await findApplicationById(input.applicationId);
|
||||||
if (
|
if (
|
||||||
application.environment.project.organizationId !==
|
application.project.organizationId !== ctx.session.activeOrganizationId
|
||||||
ctx.session.activeOrganizationId
|
|
||||||
) {
|
) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
@@ -632,8 +608,7 @@ export const applicationRouter = createTRPCRouter({
|
|||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
const application = await findApplicationById(input.applicationId);
|
const application = await findApplicationById(input.applicationId);
|
||||||
if (
|
if (
|
||||||
application.environment.project.organizationId !==
|
application.project.organizationId !== ctx.session.activeOrganizationId
|
||||||
ctx.session.activeOrganizationId
|
|
||||||
) {
|
) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
@@ -659,8 +634,7 @@ export const applicationRouter = createTRPCRouter({
|
|||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
const application = await findApplicationById(input.applicationId);
|
const application = await findApplicationById(input.applicationId);
|
||||||
if (
|
if (
|
||||||
application.environment.project.organizationId !==
|
application.project.organizationId !== ctx.session.activeOrganizationId
|
||||||
ctx.session.activeOrganizationId
|
|
||||||
) {
|
) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
@@ -673,12 +647,11 @@ export const applicationRouter = createTRPCRouter({
|
|||||||
return true;
|
return true;
|
||||||
}),
|
}),
|
||||||
deploy: protectedProcedure
|
deploy: protectedProcedure
|
||||||
.input(apiDeployApplication)
|
.input(apiFindOneApplication)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
const application = await findApplicationById(input.applicationId);
|
const application = await findApplicationById(input.applicationId);
|
||||||
if (
|
if (
|
||||||
application.environment.project.organizationId !==
|
application.project.organizationId !== ctx.session.activeOrganizationId
|
||||||
ctx.session.activeOrganizationId
|
|
||||||
) {
|
) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
@@ -687,8 +660,8 @@ export const applicationRouter = createTRPCRouter({
|
|||||||
}
|
}
|
||||||
const jobData: DeploymentJob = {
|
const jobData: DeploymentJob = {
|
||||||
applicationId: input.applicationId,
|
applicationId: input.applicationId,
|
||||||
titleLog: input.title || "Manual deployment",
|
titleLog: "Manual deployment",
|
||||||
descriptionLog: input.description || "",
|
descriptionLog: "",
|
||||||
type: "deploy",
|
type: "deploy",
|
||||||
applicationType: "application",
|
applicationType: "application",
|
||||||
server: !!application.serverId,
|
server: !!application.serverId,
|
||||||
@@ -699,14 +672,7 @@ export const applicationRouter = createTRPCRouter({
|
|||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
await myQueue.add(
|
await addJobWithUserContext({ ...jobData }, ctx.user.id);
|
||||||
"deployments",
|
|
||||||
{ ...jobData },
|
|
||||||
{
|
|
||||||
removeOnComplete: true,
|
|
||||||
removeOnFail: true,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}),
|
}),
|
||||||
|
|
||||||
cleanQueues: protectedProcedure
|
cleanQueues: protectedProcedure
|
||||||
@@ -714,8 +680,7 @@ export const applicationRouter = createTRPCRouter({
|
|||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
const application = await findApplicationById(input.applicationId);
|
const application = await findApplicationById(input.applicationId);
|
||||||
if (
|
if (
|
||||||
application.environment.project.organizationId !==
|
application.project.organizationId !== ctx.session.activeOrganizationId
|
||||||
ctx.session.activeOrganizationId
|
|
||||||
) {
|
) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
@@ -730,8 +695,7 @@ export const applicationRouter = createTRPCRouter({
|
|||||||
.query(async ({ input, ctx }) => {
|
.query(async ({ input, ctx }) => {
|
||||||
const application = await findApplicationById(input.applicationId);
|
const application = await findApplicationById(input.applicationId);
|
||||||
if (
|
if (
|
||||||
application.environment.project.organizationId !==
|
application.project.organizationId !== ctx.session.activeOrganizationId
|
||||||
ctx.session.activeOrganizationId
|
|
||||||
) {
|
) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
@@ -767,10 +731,7 @@ export const applicationRouter = createTRPCRouter({
|
|||||||
|
|
||||||
const app = await findApplicationById(input.applicationId as string);
|
const app = await findApplicationById(input.applicationId as string);
|
||||||
|
|
||||||
if (
|
if (app.project.organizationId !== ctx.session.activeOrganizationId) {
|
||||||
app.environment.project.organizationId !==
|
|
||||||
ctx.session.activeOrganizationId
|
|
||||||
) {
|
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
message: "You are not authorized to deploy this application",
|
message: "You are not authorized to deploy this application",
|
||||||
@@ -813,8 +774,7 @@ export const applicationRouter = createTRPCRouter({
|
|||||||
const application = await findApplicationById(input.applicationId);
|
const application = await findApplicationById(input.applicationId);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
application.environment.project.organizationId !==
|
application.project.organizationId !== ctx.session.activeOrganizationId
|
||||||
ctx.session.activeOrganizationId
|
|
||||||
) {
|
) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
@@ -850,14 +810,13 @@ export const applicationRouter = createTRPCRouter({
|
|||||||
.input(
|
.input(
|
||||||
z.object({
|
z.object({
|
||||||
applicationId: z.string(),
|
applicationId: z.string(),
|
||||||
targetEnvironmentId: z.string(),
|
targetProjectId: z.string(),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
const application = await findApplicationById(input.applicationId);
|
const application = await findApplicationById(input.applicationId);
|
||||||
if (
|
if (
|
||||||
application.environment.project.organizationId !==
|
application.project.organizationId !== ctx.session.activeOrganizationId
|
||||||
ctx.session.activeOrganizationId
|
|
||||||
) {
|
) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
@@ -865,16 +824,11 @@ export const applicationRouter = createTRPCRouter({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const targetEnvironment = await findEnvironmentById(
|
const targetProject = await findProjectById(input.targetProjectId);
|
||||||
input.targetEnvironmentId,
|
if (targetProject.organizationId !== ctx.session.activeOrganizationId) {
|
||||||
);
|
|
||||||
if (
|
|
||||||
targetEnvironment.project.organizationId !==
|
|
||||||
ctx.session.activeOrganizationId
|
|
||||||
) {
|
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
message: "You are not authorized to move to this environment",
|
message: "You are not authorized to move to this project",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -882,7 +836,7 @@ export const applicationRouter = createTRPCRouter({
|
|||||||
const updatedApplication = await db
|
const updatedApplication = await db
|
||||||
.update(applications)
|
.update(applications)
|
||||||
.set({
|
.set({
|
||||||
environmentId: input.targetEnvironmentId,
|
projectId: input.targetProjectId,
|
||||||
})
|
})
|
||||||
.where(eq(applications.applicationId, input.applicationId))
|
.where(eq(applications.applicationId, input.applicationId))
|
||||||
.returning()
|
.returning()
|
||||||
@@ -897,55 +851,4 @@ export const applicationRouter = createTRPCRouter({
|
|||||||
|
|
||||||
return updatedApplication;
|
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,
|
deleteMount,
|
||||||
findComposeById,
|
findComposeById,
|
||||||
findDomainsByComposeId,
|
findDomainsByComposeId,
|
||||||
findEnvironmentById,
|
|
||||||
findGitProviderById,
|
findGitProviderById,
|
||||||
findProjectById,
|
findProjectById,
|
||||||
findServerById,
|
findServerById,
|
||||||
@@ -29,7 +28,6 @@ import {
|
|||||||
startCompose,
|
startCompose,
|
||||||
stopCompose,
|
stopCompose,
|
||||||
updateCompose,
|
updateCompose,
|
||||||
updateDeploymentStatus,
|
|
||||||
} from "@dokploy/server";
|
} from "@dokploy/server";
|
||||||
import {
|
import {
|
||||||
type CompleteTemplate,
|
type CompleteTemplate,
|
||||||
@@ -49,17 +47,15 @@ import { db } from "@/server/db";
|
|||||||
import {
|
import {
|
||||||
apiCreateCompose,
|
apiCreateCompose,
|
||||||
apiDeleteCompose,
|
apiDeleteCompose,
|
||||||
apiDeployCompose,
|
|
||||||
apiFetchServices,
|
apiFetchServices,
|
||||||
apiFindCompose,
|
apiFindCompose,
|
||||||
apiRandomizeCompose,
|
apiRandomizeCompose,
|
||||||
apiRedeployCompose,
|
|
||||||
apiUpdateCompose,
|
apiUpdateCompose,
|
||||||
compose as composeTable,
|
compose as composeTable,
|
||||||
} from "@/server/db/schema";
|
} from "@/server/db/schema";
|
||||||
import type { DeploymentJob } from "@/server/queues/queue-types";
|
import type { DeploymentJob } from "@/server/queues/queue-types";
|
||||||
import { cleanQueuesByCompose, myQueue } from "@/server/queues/queueSetup";
|
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 { generatePassword } from "@/templates/utils";
|
||||||
import { createTRPCRouter, protectedProcedure, publicProcedure } from "../trpc";
|
import { createTRPCRouter, protectedProcedure, publicProcedure } from "../trpc";
|
||||||
|
|
||||||
@@ -68,14 +64,10 @@ export const composeRouter = createTRPCRouter({
|
|||||||
.input(apiCreateCompose)
|
.input(apiCreateCompose)
|
||||||
.mutation(async ({ ctx, input }) => {
|
.mutation(async ({ ctx, input }) => {
|
||||||
try {
|
try {
|
||||||
// Get project from environment
|
|
||||||
const environment = await findEnvironmentById(input.environmentId);
|
|
||||||
const project = await findProjectById(environment.projectId);
|
|
||||||
|
|
||||||
if (ctx.user.role === "member") {
|
if (ctx.user.role === "member") {
|
||||||
await checkServiceAccess(
|
await checkServiceAccess(
|
||||||
ctx.user.id,
|
ctx.user.id,
|
||||||
project.projectId,
|
input.projectId,
|
||||||
ctx.session.activeOrganizationId,
|
ctx.session.activeOrganizationId,
|
||||||
"create",
|
"create",
|
||||||
);
|
);
|
||||||
@@ -87,15 +79,14 @@ export const composeRouter = createTRPCRouter({
|
|||||||
message: "You need to use a server to create a compose",
|
message: "You need to use a server to create a compose",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
const project = await findProjectById(input.projectId);
|
||||||
if (project.organizationId !== ctx.session.activeOrganizationId) {
|
if (project.organizationId !== ctx.session.activeOrganizationId) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
message: "You are not authorized to access this project",
|
message: "You are not authorized to access this project",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
const newService = await createCompose({
|
const newService = await createCompose(input);
|
||||||
...input,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (ctx.user.role === "member") {
|
if (ctx.user.role === "member") {
|
||||||
await addNewService(
|
await addNewService(
|
||||||
@@ -124,10 +115,7 @@ export const composeRouter = createTRPCRouter({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const compose = await findComposeById(input.composeId);
|
const compose = await findComposeById(input.composeId);
|
||||||
if (
|
if (compose.project.organizationId !== ctx.session.activeOrganizationId) {
|
||||||
compose.environment.project.organizationId !==
|
|
||||||
ctx.session.activeOrganizationId
|
|
||||||
) {
|
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
message: "You are not authorized to access this compose",
|
message: "You are not authorized to access this compose",
|
||||||
@@ -178,10 +166,7 @@ export const composeRouter = createTRPCRouter({
|
|||||||
.input(apiUpdateCompose)
|
.input(apiUpdateCompose)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
const compose = await findComposeById(input.composeId);
|
const compose = await findComposeById(input.composeId);
|
||||||
if (
|
if (compose.project.organizationId !== ctx.session.activeOrganizationId) {
|
||||||
compose.environment.project.organizationId !==
|
|
||||||
ctx.session.activeOrganizationId
|
|
||||||
) {
|
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
message: "You are not authorized to update this compose",
|
message: "You are not authorized to update this compose",
|
||||||
@@ -203,7 +188,7 @@ export const composeRouter = createTRPCRouter({
|
|||||||
const composeResult = await findComposeById(input.composeId);
|
const composeResult = await findComposeById(input.composeId);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
composeResult.environment.project.organizationId !==
|
composeResult.project.organizationId !==
|
||||||
ctx.session.activeOrganizationId
|
ctx.session.activeOrganizationId
|
||||||
) {
|
) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
@@ -211,6 +196,7 @@ export const composeRouter = createTRPCRouter({
|
|||||||
message: "You are not authorized to delete this compose",
|
message: "You are not authorized to delete this compose",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
4;
|
||||||
|
|
||||||
const result = await db
|
const result = await db
|
||||||
.delete(composeTable)
|
.delete(composeTable)
|
||||||
@@ -229,16 +215,13 @@ export const composeRouter = createTRPCRouter({
|
|||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
return composeResult;
|
return result[0];
|
||||||
}),
|
}),
|
||||||
cleanQueues: protectedProcedure
|
cleanQueues: protectedProcedure
|
||||||
.input(apiFindCompose)
|
.input(apiFindCompose)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
const compose = await findComposeById(input.composeId);
|
const compose = await findComposeById(input.composeId);
|
||||||
if (
|
if (compose.project.organizationId !== ctx.session.activeOrganizationId) {
|
||||||
compose.environment.project.organizationId !==
|
|
||||||
ctx.session.activeOrganizationId
|
|
||||||
) {
|
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
message: "You are not authorized to clean this compose",
|
message: "You are not authorized to clean this compose",
|
||||||
@@ -251,10 +234,7 @@ export const composeRouter = createTRPCRouter({
|
|||||||
.input(apiFetchServices)
|
.input(apiFetchServices)
|
||||||
.query(async ({ input, ctx }) => {
|
.query(async ({ input, ctx }) => {
|
||||||
const compose = await findComposeById(input.composeId);
|
const compose = await findComposeById(input.composeId);
|
||||||
if (
|
if (compose.project.organizationId !== ctx.session.activeOrganizationId) {
|
||||||
compose.environment.project.organizationId !==
|
|
||||||
ctx.session.activeOrganizationId
|
|
||||||
) {
|
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
message: "You are not authorized to load this compose",
|
message: "You are not authorized to load this compose",
|
||||||
@@ -271,10 +251,7 @@ export const composeRouter = createTRPCRouter({
|
|||||||
)
|
)
|
||||||
.query(async ({ input, ctx }) => {
|
.query(async ({ input, ctx }) => {
|
||||||
const compose = await findComposeById(input.composeId);
|
const compose = await findComposeById(input.composeId);
|
||||||
if (
|
if (compose.project.organizationId !== ctx.session.activeOrganizationId) {
|
||||||
compose.environment.project.organizationId !==
|
|
||||||
ctx.session.activeOrganizationId
|
|
||||||
) {
|
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
message: "You are not authorized to load this compose",
|
message: "You are not authorized to load this compose",
|
||||||
@@ -293,8 +270,7 @@ export const composeRouter = createTRPCRouter({
|
|||||||
const compose = await findComposeById(input.composeId);
|
const compose = await findComposeById(input.composeId);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
compose.environment.project.organizationId !==
|
compose.project.organizationId !== ctx.session.activeOrganizationId
|
||||||
ctx.session.activeOrganizationId
|
|
||||||
) {
|
) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
@@ -320,10 +296,7 @@ export const composeRouter = createTRPCRouter({
|
|||||||
.input(apiRandomizeCompose)
|
.input(apiRandomizeCompose)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
const compose = await findComposeById(input.composeId);
|
const compose = await findComposeById(input.composeId);
|
||||||
if (
|
if (compose.project.organizationId !== ctx.session.activeOrganizationId) {
|
||||||
compose.environment.project.organizationId !==
|
|
||||||
ctx.session.activeOrganizationId
|
|
||||||
) {
|
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
message: "You are not authorized to randomize this compose",
|
message: "You are not authorized to randomize this compose",
|
||||||
@@ -335,10 +308,7 @@ export const composeRouter = createTRPCRouter({
|
|||||||
.input(apiRandomizeCompose)
|
.input(apiRandomizeCompose)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
const compose = await findComposeById(input.composeId);
|
const compose = await findComposeById(input.composeId);
|
||||||
if (
|
if (compose.project.organizationId !== ctx.session.activeOrganizationId) {
|
||||||
compose.environment.project.organizationId !==
|
|
||||||
ctx.session.activeOrganizationId
|
|
||||||
) {
|
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
message: "You are not authorized to randomize this compose",
|
message: "You are not authorized to randomize this compose",
|
||||||
@@ -353,10 +323,7 @@ export const composeRouter = createTRPCRouter({
|
|||||||
.input(apiFindCompose)
|
.input(apiFindCompose)
|
||||||
.query(async ({ input, ctx }) => {
|
.query(async ({ input, ctx }) => {
|
||||||
const compose = await findComposeById(input.composeId);
|
const compose = await findComposeById(input.composeId);
|
||||||
if (
|
if (compose.project.organizationId !== ctx.session.activeOrganizationId) {
|
||||||
compose.environment.project.organizationId !==
|
|
||||||
ctx.session.activeOrganizationId
|
|
||||||
) {
|
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
message: "You are not authorized to get this compose",
|
message: "You are not authorized to get this compose",
|
||||||
@@ -370,14 +337,11 @@ export const composeRouter = createTRPCRouter({
|
|||||||
}),
|
}),
|
||||||
|
|
||||||
deploy: protectedProcedure
|
deploy: protectedProcedure
|
||||||
.input(apiDeployCompose)
|
.input(apiFindCompose)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
const compose = await findComposeById(input.composeId);
|
const compose = await findComposeById(input.composeId);
|
||||||
|
|
||||||
if (
|
if (compose.project.organizationId !== ctx.session.activeOrganizationId) {
|
||||||
compose.environment.project.organizationId !==
|
|
||||||
ctx.session.activeOrganizationId
|
|
||||||
) {
|
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
message: "You are not authorized to deploy this compose",
|
message: "You are not authorized to deploy this compose",
|
||||||
@@ -385,10 +349,10 @@ export const composeRouter = createTRPCRouter({
|
|||||||
}
|
}
|
||||||
const jobData: DeploymentJob = {
|
const jobData: DeploymentJob = {
|
||||||
composeId: input.composeId,
|
composeId: input.composeId,
|
||||||
titleLog: input.title || "Manual deployment",
|
titleLog: "Manual deployment",
|
||||||
type: "deploy",
|
type: "deploy",
|
||||||
applicationType: "compose",
|
applicationType: "compose",
|
||||||
descriptionLog: input.description || "",
|
descriptionLog: "",
|
||||||
server: !!compose.serverId,
|
server: !!compose.serverId,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -407,13 +371,10 @@ export const composeRouter = createTRPCRouter({
|
|||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
redeploy: protectedProcedure
|
redeploy: protectedProcedure
|
||||||
.input(apiRedeployCompose)
|
.input(apiFindCompose)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
const compose = await findComposeById(input.composeId);
|
const compose = await findComposeById(input.composeId);
|
||||||
if (
|
if (compose.project.organizationId !== ctx.session.activeOrganizationId) {
|
||||||
compose.environment.project.organizationId !==
|
|
||||||
ctx.session.activeOrganizationId
|
|
||||||
) {
|
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
message: "You are not authorized to redeploy this compose",
|
message: "You are not authorized to redeploy this compose",
|
||||||
@@ -421,10 +382,10 @@ export const composeRouter = createTRPCRouter({
|
|||||||
}
|
}
|
||||||
const jobData: DeploymentJob = {
|
const jobData: DeploymentJob = {
|
||||||
composeId: input.composeId,
|
composeId: input.composeId,
|
||||||
titleLog: input.title || "Rebuild deployment",
|
titleLog: "Rebuild deployment",
|
||||||
type: "redeploy",
|
type: "redeploy",
|
||||||
applicationType: "compose",
|
applicationType: "compose",
|
||||||
descriptionLog: input.description || "",
|
descriptionLog: "",
|
||||||
server: !!compose.serverId,
|
server: !!compose.serverId,
|
||||||
};
|
};
|
||||||
if (IS_CLOUD && compose.serverId) {
|
if (IS_CLOUD && compose.serverId) {
|
||||||
@@ -445,10 +406,7 @@ export const composeRouter = createTRPCRouter({
|
|||||||
.input(apiFindCompose)
|
.input(apiFindCompose)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
const compose = await findComposeById(input.composeId);
|
const compose = await findComposeById(input.composeId);
|
||||||
if (
|
if (compose.project.organizationId !== ctx.session.activeOrganizationId) {
|
||||||
compose.environment.project.organizationId !==
|
|
||||||
ctx.session.activeOrganizationId
|
|
||||||
) {
|
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
message: "You are not authorized to stop this compose",
|
message: "You are not authorized to stop this compose",
|
||||||
@@ -462,10 +420,7 @@ export const composeRouter = createTRPCRouter({
|
|||||||
.input(apiFindCompose)
|
.input(apiFindCompose)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
const compose = await findComposeById(input.composeId);
|
const compose = await findComposeById(input.composeId);
|
||||||
if (
|
if (compose.project.organizationId !== ctx.session.activeOrganizationId) {
|
||||||
compose.environment.project.organizationId !==
|
|
||||||
ctx.session.activeOrganizationId
|
|
||||||
) {
|
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
message: "You are not authorized to stop this compose",
|
message: "You are not authorized to stop this compose",
|
||||||
@@ -480,10 +435,7 @@ export const composeRouter = createTRPCRouter({
|
|||||||
.query(async ({ input, ctx }) => {
|
.query(async ({ input, ctx }) => {
|
||||||
const compose = await findComposeById(input.composeId);
|
const compose = await findComposeById(input.composeId);
|
||||||
|
|
||||||
if (
|
if (compose.project.organizationId !== ctx.session.activeOrganizationId) {
|
||||||
compose.environment.project.organizationId !==
|
|
||||||
ctx.session.activeOrganizationId
|
|
||||||
) {
|
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
message: "You are not authorized to get this compose",
|
message: "You are not authorized to get this compose",
|
||||||
@@ -496,10 +448,7 @@ export const composeRouter = createTRPCRouter({
|
|||||||
.input(apiFindCompose)
|
.input(apiFindCompose)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
const compose = await findComposeById(input.composeId);
|
const compose = await findComposeById(input.composeId);
|
||||||
if (
|
if (compose.project.organizationId !== ctx.session.activeOrganizationId) {
|
||||||
compose.environment.project.organizationId !==
|
|
||||||
ctx.session.activeOrganizationId
|
|
||||||
) {
|
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
message: "You are not authorized to refresh this compose",
|
message: "You are not authorized to refresh this compose",
|
||||||
@@ -513,19 +462,17 @@ export const composeRouter = createTRPCRouter({
|
|||||||
deployTemplate: protectedProcedure
|
deployTemplate: protectedProcedure
|
||||||
.input(
|
.input(
|
||||||
z.object({
|
z.object({
|
||||||
environmentId: z.string(),
|
projectId: z.string(),
|
||||||
serverId: z.string().optional(),
|
serverId: z.string().optional(),
|
||||||
id: z.string(),
|
id: z.string(),
|
||||||
baseUrl: z.string().optional(),
|
baseUrl: z.string().optional(),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.mutation(async ({ ctx, input }) => {
|
.mutation(async ({ ctx, input }) => {
|
||||||
const environment = await findEnvironmentById(input.environmentId);
|
|
||||||
|
|
||||||
if (ctx.user.role === "member") {
|
if (ctx.user.role === "member") {
|
||||||
await checkServiceAccess(
|
await checkServiceAccess(
|
||||||
ctx.user.id,
|
ctx.user.id,
|
||||||
environment.projectId,
|
input.projectId,
|
||||||
ctx.session.activeOrganizationId,
|
ctx.session.activeOrganizationId,
|
||||||
"create",
|
"create",
|
||||||
);
|
);
|
||||||
@@ -543,7 +490,7 @@ export const composeRouter = createTRPCRouter({
|
|||||||
const admin = await findUserById(ctx.user.ownerId);
|
const admin = await findUserById(ctx.user.ownerId);
|
||||||
let serverIp = admin.serverIp || "127.0.0.1";
|
let serverIp = admin.serverIp || "127.0.0.1";
|
||||||
|
|
||||||
const project = await findProjectById(environment.projectId);
|
const project = await findProjectById(input.projectId);
|
||||||
|
|
||||||
if (input.serverId) {
|
if (input.serverId) {
|
||||||
const server = await findServerById(input.serverId);
|
const server = await findServerById(input.serverId);
|
||||||
@@ -644,10 +591,7 @@ export const composeRouter = createTRPCRouter({
|
|||||||
.input(apiFindCompose)
|
.input(apiFindCompose)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
const compose = await findComposeById(input.composeId);
|
const compose = await findComposeById(input.composeId);
|
||||||
if (
|
if (compose.project.organizationId !== ctx.session.activeOrganizationId) {
|
||||||
compose.environment.project.organizationId !==
|
|
||||||
ctx.session.activeOrganizationId
|
|
||||||
) {
|
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
message: "You are not authorized to disconnect this git provider",
|
message: "You are not authorized to disconnect this git provider",
|
||||||
@@ -703,38 +647,30 @@ export const composeRouter = createTRPCRouter({
|
|||||||
.input(
|
.input(
|
||||||
z.object({
|
z.object({
|
||||||
composeId: z.string(),
|
composeId: z.string(),
|
||||||
targetEnvironmentId: z.string(),
|
targetProjectId: z.string(),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
const compose = await findComposeById(input.composeId);
|
const compose = await findComposeById(input.composeId);
|
||||||
if (
|
if (compose.project.organizationId !== ctx.session.activeOrganizationId) {
|
||||||
compose.environment.project.organizationId !==
|
|
||||||
ctx.session.activeOrganizationId
|
|
||||||
) {
|
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
message: "You are not authorized to move this compose",
|
message: "You are not authorized to move this compose",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const targetEnvironment = await findEnvironmentById(
|
const targetProject = await findProjectById(input.targetProjectId);
|
||||||
input.targetEnvironmentId,
|
if (targetProject.organizationId !== ctx.session.activeOrganizationId) {
|
||||||
);
|
|
||||||
if (
|
|
||||||
targetEnvironment.project.organizationId !==
|
|
||||||
ctx.session.activeOrganizationId
|
|
||||||
) {
|
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
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
|
const updatedCompose = await db
|
||||||
.update(composeTable)
|
.update(composeTable)
|
||||||
.set({
|
.set({
|
||||||
environmentId: input.targetEnvironmentId,
|
projectId: input.targetProjectId,
|
||||||
})
|
})
|
||||||
.where(eq(composeTable.composeId, input.composeId))
|
.where(eq(composeTable.composeId, input.composeId))
|
||||||
.returning()
|
.returning()
|
||||||
@@ -762,8 +698,7 @@ export const composeRouter = createTRPCRouter({
|
|||||||
const compose = await findComposeById(input.composeId);
|
const compose = await findComposeById(input.composeId);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
compose.environment.project.organizationId !==
|
compose.project.organizationId !== ctx.session.activeOrganizationId
|
||||||
ctx.session.activeOrganizationId
|
|
||||||
) {
|
) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
@@ -834,8 +769,7 @@ export const composeRouter = createTRPCRouter({
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
compose.environment.project.organizationId !==
|
compose.project.organizationId !== ctx.session.activeOrganizationId
|
||||||
ctx.session.activeOrganizationId
|
|
||||||
) {
|
) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
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 }) => {
|
.query(async ({ input, ctx }) => {
|
||||||
const application = await findApplicationById(input.applicationId);
|
const application = await findApplicationById(input.applicationId);
|
||||||
if (
|
if (
|
||||||
application.environment.project.organizationId !==
|
application.project.organizationId !== ctx.session.activeOrganizationId
|
||||||
ctx.session.activeOrganizationId
|
|
||||||
) {
|
) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
@@ -44,10 +43,7 @@ export const deploymentRouter = createTRPCRouter({
|
|||||||
.input(apiFindAllByCompose)
|
.input(apiFindAllByCompose)
|
||||||
.query(async ({ input, ctx }) => {
|
.query(async ({ input, ctx }) => {
|
||||||
const compose = await findComposeById(input.composeId);
|
const compose = await findComposeById(input.composeId);
|
||||||
if (
|
if (compose.project.organizationId !== ctx.session.activeOrganizationId) {
|
||||||
compose.environment.project.organizationId !==
|
|
||||||
ctx.session.activeOrganizationId
|
|
||||||
) {
|
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
message: "You are not authorized to access this compose",
|
message: "You are not authorized to access this compose",
|
||||||
|
|||||||
@@ -34,8 +34,7 @@ export const domainRouter = createTRPCRouter({
|
|||||||
if (input.domainType === "compose" && input.composeId) {
|
if (input.domainType === "compose" && input.composeId) {
|
||||||
const compose = await findComposeById(input.composeId);
|
const compose = await findComposeById(input.composeId);
|
||||||
if (
|
if (
|
||||||
compose.environment.project.organizationId !==
|
compose.project.organizationId !== ctx.session.activeOrganizationId
|
||||||
ctx.session.activeOrganizationId
|
|
||||||
) {
|
) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
@@ -45,7 +44,7 @@ export const domainRouter = createTRPCRouter({
|
|||||||
} else if (input.domainType === "application" && input.applicationId) {
|
} else if (input.domainType === "application" && input.applicationId) {
|
||||||
const application = await findApplicationById(input.applicationId);
|
const application = await findApplicationById(input.applicationId);
|
||||||
if (
|
if (
|
||||||
application.environment.project.organizationId !==
|
application.project.organizationId !==
|
||||||
ctx.session.activeOrganizationId
|
ctx.session.activeOrganizationId
|
||||||
) {
|
) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
@@ -71,8 +70,7 @@ export const domainRouter = createTRPCRouter({
|
|||||||
.query(async ({ input, ctx }) => {
|
.query(async ({ input, ctx }) => {
|
||||||
const application = await findApplicationById(input.applicationId);
|
const application = await findApplicationById(input.applicationId);
|
||||||
if (
|
if (
|
||||||
application.environment.project.organizationId !==
|
application.project.organizationId !== ctx.session.activeOrganizationId
|
||||||
ctx.session.activeOrganizationId
|
|
||||||
) {
|
) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
@@ -85,10 +83,7 @@ export const domainRouter = createTRPCRouter({
|
|||||||
.input(apiFindCompose)
|
.input(apiFindCompose)
|
||||||
.query(async ({ input, ctx }) => {
|
.query(async ({ input, ctx }) => {
|
||||||
const compose = await findComposeById(input.composeId);
|
const compose = await findComposeById(input.composeId);
|
||||||
if (
|
if (compose.project.organizationId !== ctx.session.activeOrganizationId) {
|
||||||
compose.environment.project.organizationId !==
|
|
||||||
ctx.session.activeOrganizationId
|
|
||||||
) {
|
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
message: "You are not authorized to access this compose",
|
message: "You are not authorized to access this compose",
|
||||||
@@ -127,8 +122,7 @@ export const domainRouter = createTRPCRouter({
|
|||||||
if (currentDomain.applicationId) {
|
if (currentDomain.applicationId) {
|
||||||
const newApp = await findApplicationById(currentDomain.applicationId);
|
const newApp = await findApplicationById(currentDomain.applicationId);
|
||||||
if (
|
if (
|
||||||
newApp.environment.project.organizationId !==
|
newApp.project.organizationId !== ctx.session.activeOrganizationId
|
||||||
ctx.session.activeOrganizationId
|
|
||||||
) {
|
) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
@@ -138,8 +132,7 @@ export const domainRouter = createTRPCRouter({
|
|||||||
} else if (currentDomain.composeId) {
|
} else if (currentDomain.composeId) {
|
||||||
const newCompose = await findComposeById(currentDomain.composeId);
|
const newCompose = await findComposeById(currentDomain.composeId);
|
||||||
if (
|
if (
|
||||||
newCompose.environment.project.organizationId !==
|
newCompose.project.organizationId !== ctx.session.activeOrganizationId
|
||||||
ctx.session.activeOrganizationId
|
|
||||||
) {
|
) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
@@ -151,8 +144,8 @@ export const domainRouter = createTRPCRouter({
|
|||||||
currentDomain.previewDeploymentId,
|
currentDomain.previewDeploymentId,
|
||||||
);
|
);
|
||||||
if (
|
if (
|
||||||
newPreviewDeployment.application.environment.project
|
newPreviewDeployment.application.project.organizationId !==
|
||||||
.organizationId !== ctx.session.activeOrganizationId
|
ctx.session.activeOrganizationId
|
||||||
) {
|
) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
@@ -182,8 +175,7 @@ export const domainRouter = createTRPCRouter({
|
|||||||
if (domain.applicationId) {
|
if (domain.applicationId) {
|
||||||
const application = await findApplicationById(domain.applicationId);
|
const application = await findApplicationById(domain.applicationId);
|
||||||
if (
|
if (
|
||||||
application.environment.project.organizationId !==
|
application.project.organizationId !== ctx.session.activeOrganizationId
|
||||||
ctx.session.activeOrganizationId
|
|
||||||
) {
|
) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
@@ -192,10 +184,7 @@ export const domainRouter = createTRPCRouter({
|
|||||||
}
|
}
|
||||||
} else if (domain.composeId) {
|
} else if (domain.composeId) {
|
||||||
const compose = await findComposeById(domain.composeId);
|
const compose = await findComposeById(domain.composeId);
|
||||||
if (
|
if (compose.project.organizationId !== ctx.session.activeOrganizationId) {
|
||||||
compose.environment.project.organizationId !==
|
|
||||||
ctx.session.activeOrganizationId
|
|
||||||
) {
|
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
message: "You are not authorized to access this compose",
|
message: "You are not authorized to access this compose",
|
||||||
@@ -211,7 +200,7 @@ export const domainRouter = createTRPCRouter({
|
|||||||
if (domain.applicationId) {
|
if (domain.applicationId) {
|
||||||
const application = await findApplicationById(domain.applicationId);
|
const application = await findApplicationById(domain.applicationId);
|
||||||
if (
|
if (
|
||||||
application.environment.project.organizationId !==
|
application.project.organizationId !==
|
||||||
ctx.session.activeOrganizationId
|
ctx.session.activeOrganizationId
|
||||||
) {
|
) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
@@ -222,8 +211,7 @@ export const domainRouter = createTRPCRouter({
|
|||||||
} else if (domain.composeId) {
|
} else if (domain.composeId) {
|
||||||
const compose = await findComposeById(domain.composeId);
|
const compose = await findComposeById(domain.composeId);
|
||||||
if (
|
if (
|
||||||
compose.environment.project.organizationId !==
|
compose.project.organizationId !== ctx.session.activeOrganizationId
|
||||||
ctx.session.activeOrganizationId
|
|
||||||
) {
|
) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
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,
|
deployMariadb,
|
||||||
findBackupsByDbId,
|
findBackupsByDbId,
|
||||||
findMariadbById,
|
findMariadbById,
|
||||||
findEnvironmentById,
|
|
||||||
findProjectById,
|
findProjectById,
|
||||||
IS_CLOUD,
|
IS_CLOUD,
|
||||||
rebuildDatabase,
|
rebuildDatabase,
|
||||||
@@ -42,14 +41,10 @@ export const mariadbRouter = createTRPCRouter({
|
|||||||
.input(apiCreateMariaDB)
|
.input(apiCreateMariaDB)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
try {
|
try {
|
||||||
// Get project from environment
|
|
||||||
const environment = await findEnvironmentById(input.environmentId);
|
|
||||||
const project = await findProjectById(environment.projectId);
|
|
||||||
|
|
||||||
if (ctx.user.role === "member") {
|
if (ctx.user.role === "member") {
|
||||||
await checkServiceAccess(
|
await checkServiceAccess(
|
||||||
ctx.user.id,
|
ctx.user.id,
|
||||||
project.projectId,
|
input.projectId,
|
||||||
ctx.session.activeOrganizationId,
|
ctx.session.activeOrganizationId,
|
||||||
"create",
|
"create",
|
||||||
);
|
);
|
||||||
@@ -62,15 +57,14 @@ export const mariadbRouter = createTRPCRouter({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const project = await findProjectById(input.projectId);
|
||||||
if (project.organizationId !== ctx.session.activeOrganizationId) {
|
if (project.organizationId !== ctx.session.activeOrganizationId) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
message: "You are not authorized to access this project",
|
message: "You are not authorized to access this project",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
const newMariadb = await createMariadb({
|
const newMariadb = await createMariadb(input);
|
||||||
...input,
|
|
||||||
});
|
|
||||||
if (ctx.user.role === "member") {
|
if (ctx.user.role === "member") {
|
||||||
await addNewService(
|
await addNewService(
|
||||||
ctx.user.id,
|
ctx.user.id,
|
||||||
@@ -107,10 +101,7 @@ export const mariadbRouter = createTRPCRouter({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
const mariadb = await findMariadbById(input.mariadbId);
|
const mariadb = await findMariadbById(input.mariadbId);
|
||||||
if (
|
if (mariadb.project.organizationId !== ctx.session.activeOrganizationId) {
|
||||||
mariadb.environment.project.organizationId !==
|
|
||||||
ctx.session.activeOrganizationId
|
|
||||||
) {
|
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
message: "You are not authorized to access this Mariadb",
|
message: "You are not authorized to access this Mariadb",
|
||||||
@@ -123,10 +114,7 @@ export const mariadbRouter = createTRPCRouter({
|
|||||||
.input(apiFindOneMariaDB)
|
.input(apiFindOneMariaDB)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
const service = await findMariadbById(input.mariadbId);
|
const service = await findMariadbById(input.mariadbId);
|
||||||
if (
|
if (service.project.organizationId !== ctx.session.activeOrganizationId) {
|
||||||
service.environment.project.organizationId !==
|
|
||||||
ctx.session.activeOrganizationId
|
|
||||||
) {
|
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
message: "You are not authorized to start this Mariadb",
|
message: "You are not authorized to start this Mariadb",
|
||||||
@@ -163,10 +151,7 @@ export const mariadbRouter = createTRPCRouter({
|
|||||||
.input(apiSaveExternalPortMariaDB)
|
.input(apiSaveExternalPortMariaDB)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
const mongo = await findMariadbById(input.mariadbId);
|
const mongo = await findMariadbById(input.mariadbId);
|
||||||
if (
|
if (mongo.project.organizationId !== ctx.session.activeOrganizationId) {
|
||||||
mongo.environment.project.organizationId !==
|
|
||||||
ctx.session.activeOrganizationId
|
|
||||||
) {
|
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
message: "You are not authorized to save this external port",
|
message: "You are not authorized to save this external port",
|
||||||
@@ -182,10 +167,7 @@ export const mariadbRouter = createTRPCRouter({
|
|||||||
.input(apiDeployMariaDB)
|
.input(apiDeployMariaDB)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
const mariadb = await findMariadbById(input.mariadbId);
|
const mariadb = await findMariadbById(input.mariadbId);
|
||||||
if (
|
if (mariadb.project.organizationId !== ctx.session.activeOrganizationId) {
|
||||||
mariadb.environment.project.organizationId !==
|
|
||||||
ctx.session.activeOrganizationId
|
|
||||||
) {
|
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
message: "You are not authorized to deploy this Mariadb",
|
message: "You are not authorized to deploy this Mariadb",
|
||||||
@@ -206,10 +188,7 @@ export const mariadbRouter = createTRPCRouter({
|
|||||||
.input(apiDeployMariaDB)
|
.input(apiDeployMariaDB)
|
||||||
.subscription(async ({ input, ctx }) => {
|
.subscription(async ({ input, ctx }) => {
|
||||||
const mariadb = await findMariadbById(input.mariadbId);
|
const mariadb = await findMariadbById(input.mariadbId);
|
||||||
if (
|
if (mariadb.project.organizationId !== ctx.session.activeOrganizationId) {
|
||||||
mariadb.environment.project.organizationId !==
|
|
||||||
ctx.session.activeOrganizationId
|
|
||||||
) {
|
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
message: "You are not authorized to deploy this Mariadb",
|
message: "You are not authorized to deploy this Mariadb",
|
||||||
@@ -226,10 +205,7 @@ export const mariadbRouter = createTRPCRouter({
|
|||||||
.input(apiChangeMariaDBStatus)
|
.input(apiChangeMariaDBStatus)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
const mongo = await findMariadbById(input.mariadbId);
|
const mongo = await findMariadbById(input.mariadbId);
|
||||||
if (
|
if (mongo.project.organizationId !== ctx.session.activeOrganizationId) {
|
||||||
mongo.environment.project.organizationId !==
|
|
||||||
ctx.session.activeOrganizationId
|
|
||||||
) {
|
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
message: "You are not authorized to change this Mariadb status",
|
message: "You are not authorized to change this Mariadb status",
|
||||||
@@ -253,10 +229,7 @@ export const mariadbRouter = createTRPCRouter({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const mongo = await findMariadbById(input.mariadbId);
|
const mongo = await findMariadbById(input.mariadbId);
|
||||||
if (
|
if (mongo.project.organizationId !== ctx.session.activeOrganizationId) {
|
||||||
mongo.environment.project.organizationId !==
|
|
||||||
ctx.session.activeOrganizationId
|
|
||||||
) {
|
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
message: "You are not authorized to delete this Mariadb",
|
message: "You are not authorized to delete this Mariadb",
|
||||||
@@ -282,10 +255,7 @@ export const mariadbRouter = createTRPCRouter({
|
|||||||
.input(apiSaveEnvironmentVariablesMariaDB)
|
.input(apiSaveEnvironmentVariablesMariaDB)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
const mariadb = await findMariadbById(input.mariadbId);
|
const mariadb = await findMariadbById(input.mariadbId);
|
||||||
if (
|
if (mariadb.project.organizationId !== ctx.session.activeOrganizationId) {
|
||||||
mariadb.environment.project.organizationId !==
|
|
||||||
ctx.session.activeOrganizationId
|
|
||||||
) {
|
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
message: "You are not authorized to save this environment",
|
message: "You are not authorized to save this environment",
|
||||||
@@ -308,10 +278,7 @@ export const mariadbRouter = createTRPCRouter({
|
|||||||
.input(apiResetMariadb)
|
.input(apiResetMariadb)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
const mariadb = await findMariadbById(input.mariadbId);
|
const mariadb = await findMariadbById(input.mariadbId);
|
||||||
if (
|
if (mariadb.project.organizationId !== ctx.session.activeOrganizationId) {
|
||||||
mariadb.environment.project.organizationId !==
|
|
||||||
ctx.session.activeOrganizationId
|
|
||||||
) {
|
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
message: "You are not authorized to reload this Mariadb",
|
message: "You are not authorized to reload this Mariadb",
|
||||||
@@ -341,10 +308,7 @@ export const mariadbRouter = createTRPCRouter({
|
|||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
const { mariadbId, ...rest } = input;
|
const { mariadbId, ...rest } = input;
|
||||||
const mariadb = await findMariadbById(mariadbId);
|
const mariadb = await findMariadbById(mariadbId);
|
||||||
if (
|
if (mariadb.project.organizationId !== ctx.session.activeOrganizationId) {
|
||||||
mariadb.environment.project.organizationId !==
|
|
||||||
ctx.session.activeOrganizationId
|
|
||||||
) {
|
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
message: "You are not authorized to update this Mariadb",
|
message: "You are not authorized to update this Mariadb",
|
||||||
@@ -367,31 +331,23 @@ export const mariadbRouter = createTRPCRouter({
|
|||||||
.input(
|
.input(
|
||||||
z.object({
|
z.object({
|
||||||
mariadbId: z.string(),
|
mariadbId: z.string(),
|
||||||
targetEnvironmentId: z.string(),
|
targetProjectId: z.string(),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
const mariadb = await findMariadbById(input.mariadbId);
|
const mariadb = await findMariadbById(input.mariadbId);
|
||||||
if (
|
if (mariadb.project.organizationId !== ctx.session.activeOrganizationId) {
|
||||||
mariadb.environment.project.organizationId !==
|
|
||||||
ctx.session.activeOrganizationId
|
|
||||||
) {
|
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
message: "You are not authorized to move this mariadb",
|
message: "You are not authorized to move this mariadb",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const targetEnvironment = await findEnvironmentById(
|
const targetProject = await findProjectById(input.targetProjectId);
|
||||||
input.targetEnvironmentId,
|
if (targetProject.organizationId !== ctx.session.activeOrganizationId) {
|
||||||
);
|
|
||||||
if (
|
|
||||||
targetEnvironment.project.organizationId !==
|
|
||||||
ctx.session.activeOrganizationId
|
|
||||||
) {
|
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
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
|
const updatedMariadb = await db
|
||||||
.update(mariadbTable)
|
.update(mariadbTable)
|
||||||
.set({
|
.set({
|
||||||
environmentId: input.targetEnvironmentId,
|
projectId: input.targetProjectId,
|
||||||
})
|
})
|
||||||
.where(eq(mariadbTable.mariadbId, input.mariadbId))
|
.where(eq(mariadbTable.mariadbId, input.mariadbId))
|
||||||
.returning()
|
.returning()
|
||||||
@@ -418,10 +374,7 @@ export const mariadbRouter = createTRPCRouter({
|
|||||||
.input(apiRebuildMariadb)
|
.input(apiRebuildMariadb)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
const mariadb = await findMariadbById(input.mariadbId);
|
const mariadb = await findMariadbById(input.mariadbId);
|
||||||
if (
|
if (mariadb.project.organizationId !== ctx.session.activeOrganizationId) {
|
||||||
mariadb.environment.project.organizationId !==
|
|
||||||
ctx.session.activeOrganizationId
|
|
||||||
) {
|
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
message: "You are not authorized to rebuild this MariaDB database",
|
message: "You are not authorized to rebuild this MariaDB database",
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import {
|
|||||||
deployMongo,
|
deployMongo,
|
||||||
findBackupsByDbId,
|
findBackupsByDbId,
|
||||||
findMongoById,
|
findMongoById,
|
||||||
findEnvironmentById,
|
|
||||||
findProjectById,
|
findProjectById,
|
||||||
IS_CLOUD,
|
IS_CLOUD,
|
||||||
rebuildDatabase,
|
rebuildDatabase,
|
||||||
@@ -42,14 +41,10 @@ export const mongoRouter = createTRPCRouter({
|
|||||||
.input(apiCreateMongo)
|
.input(apiCreateMongo)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
try {
|
try {
|
||||||
// Get project from environment
|
|
||||||
const environment = await findEnvironmentById(input.environmentId);
|
|
||||||
const project = await findProjectById(environment.projectId);
|
|
||||||
|
|
||||||
if (ctx.user.role === "member") {
|
if (ctx.user.role === "member") {
|
||||||
await checkServiceAccess(
|
await checkServiceAccess(
|
||||||
ctx.user.id,
|
ctx.user.id,
|
||||||
project.projectId,
|
input.projectId,
|
||||||
ctx.session.activeOrganizationId,
|
ctx.session.activeOrganizationId,
|
||||||
"create",
|
"create",
|
||||||
);
|
);
|
||||||
@@ -62,15 +57,14 @@ export const mongoRouter = createTRPCRouter({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const project = await findProjectById(input.projectId);
|
||||||
if (project.organizationId !== ctx.session.activeOrganizationId) {
|
if (project.organizationId !== ctx.session.activeOrganizationId) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
message: "You are not authorized to access this project",
|
message: "You are not authorized to access this project",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
const newMongo = await createMongo({
|
const newMongo = await createMongo(input);
|
||||||
...input,
|
|
||||||
});
|
|
||||||
if (ctx.user.role === "member") {
|
if (ctx.user.role === "member") {
|
||||||
await addNewService(
|
await addNewService(
|
||||||
ctx.user.id,
|
ctx.user.id,
|
||||||
@@ -112,10 +106,7 @@ export const mongoRouter = createTRPCRouter({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const mongo = await findMongoById(input.mongoId);
|
const mongo = await findMongoById(input.mongoId);
|
||||||
if (
|
if (mongo.project.organizationId !== ctx.session.activeOrganizationId) {
|
||||||
mongo.environment.project.organizationId !==
|
|
||||||
ctx.session.activeOrganizationId
|
|
||||||
) {
|
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
message: "You are not authorized to access this mongo",
|
message: "You are not authorized to access this mongo",
|
||||||
@@ -129,10 +120,7 @@ export const mongoRouter = createTRPCRouter({
|
|||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
const service = await findMongoById(input.mongoId);
|
const service = await findMongoById(input.mongoId);
|
||||||
|
|
||||||
if (
|
if (service.project.organizationId !== ctx.session.activeOrganizationId) {
|
||||||
service.environment.project.organizationId !==
|
|
||||||
ctx.session.activeOrganizationId
|
|
||||||
) {
|
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
message: "You are not authorized to start this mongo",
|
message: "You are not authorized to start this mongo",
|
||||||
@@ -155,10 +143,7 @@ export const mongoRouter = createTRPCRouter({
|
|||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
const mongo = await findMongoById(input.mongoId);
|
const mongo = await findMongoById(input.mongoId);
|
||||||
|
|
||||||
if (
|
if (mongo.project.organizationId !== ctx.session.activeOrganizationId) {
|
||||||
mongo.environment.project.organizationId !==
|
|
||||||
ctx.session.activeOrganizationId
|
|
||||||
) {
|
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
message: "You are not authorized to stop this mongo",
|
message: "You are not authorized to stop this mongo",
|
||||||
@@ -180,10 +165,7 @@ export const mongoRouter = createTRPCRouter({
|
|||||||
.input(apiSaveExternalPortMongo)
|
.input(apiSaveExternalPortMongo)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
const mongo = await findMongoById(input.mongoId);
|
const mongo = await findMongoById(input.mongoId);
|
||||||
if (
|
if (mongo.project.organizationId !== ctx.session.activeOrganizationId) {
|
||||||
mongo.environment.project.organizationId !==
|
|
||||||
ctx.session.activeOrganizationId
|
|
||||||
) {
|
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
message: "You are not authorized to save this external port",
|
message: "You are not authorized to save this external port",
|
||||||
@@ -199,10 +181,7 @@ export const mongoRouter = createTRPCRouter({
|
|||||||
.input(apiDeployMongo)
|
.input(apiDeployMongo)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
const mongo = await findMongoById(input.mongoId);
|
const mongo = await findMongoById(input.mongoId);
|
||||||
if (
|
if (mongo.project.organizationId !== ctx.session.activeOrganizationId) {
|
||||||
mongo.environment.project.organizationId !==
|
|
||||||
ctx.session.activeOrganizationId
|
|
||||||
) {
|
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
message: "You are not authorized to deploy this mongo",
|
message: "You are not authorized to deploy this mongo",
|
||||||
@@ -222,10 +201,7 @@ export const mongoRouter = createTRPCRouter({
|
|||||||
.input(apiDeployMongo)
|
.input(apiDeployMongo)
|
||||||
.subscription(async ({ input, ctx }) => {
|
.subscription(async ({ input, ctx }) => {
|
||||||
const mongo = await findMongoById(input.mongoId);
|
const mongo = await findMongoById(input.mongoId);
|
||||||
if (
|
if (mongo.project.organizationId !== ctx.session.activeOrganizationId) {
|
||||||
mongo.environment.project.organizationId !==
|
|
||||||
ctx.session.activeOrganizationId
|
|
||||||
) {
|
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
message: "You are not authorized to deploy this mongo",
|
message: "You are not authorized to deploy this mongo",
|
||||||
@@ -242,10 +218,7 @@ export const mongoRouter = createTRPCRouter({
|
|||||||
.input(apiChangeMongoStatus)
|
.input(apiChangeMongoStatus)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
const mongo = await findMongoById(input.mongoId);
|
const mongo = await findMongoById(input.mongoId);
|
||||||
if (
|
if (mongo.project.organizationId !== ctx.session.activeOrganizationId) {
|
||||||
mongo.environment.project.organizationId !==
|
|
||||||
ctx.session.activeOrganizationId
|
|
||||||
) {
|
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
message: "You are not authorized to change this mongo status",
|
message: "You are not authorized to change this mongo status",
|
||||||
@@ -260,10 +233,7 @@ export const mongoRouter = createTRPCRouter({
|
|||||||
.input(apiResetMongo)
|
.input(apiResetMongo)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
const mongo = await findMongoById(input.mongoId);
|
const mongo = await findMongoById(input.mongoId);
|
||||||
if (
|
if (mongo.project.organizationId !== ctx.session.activeOrganizationId) {
|
||||||
mongo.environment.project.organizationId !==
|
|
||||||
ctx.session.activeOrganizationId
|
|
||||||
) {
|
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
message: "You are not authorized to reload this mongo",
|
message: "You are not authorized to reload this mongo",
|
||||||
@@ -302,10 +272,7 @@ export const mongoRouter = createTRPCRouter({
|
|||||||
|
|
||||||
const mongo = await findMongoById(input.mongoId);
|
const mongo = await findMongoById(input.mongoId);
|
||||||
|
|
||||||
if (
|
if (mongo.project.organizationId !== ctx.session.activeOrganizationId) {
|
||||||
mongo.environment.project.organizationId !==
|
|
||||||
ctx.session.activeOrganizationId
|
|
||||||
) {
|
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
message: "You are not authorized to delete this mongo",
|
message: "You are not authorized to delete this mongo",
|
||||||
@@ -331,10 +298,7 @@ export const mongoRouter = createTRPCRouter({
|
|||||||
.input(apiSaveEnvironmentVariablesMongo)
|
.input(apiSaveEnvironmentVariablesMongo)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
const mongo = await findMongoById(input.mongoId);
|
const mongo = await findMongoById(input.mongoId);
|
||||||
if (
|
if (mongo.project.organizationId !== ctx.session.activeOrganizationId) {
|
||||||
mongo.environment.project.organizationId !==
|
|
||||||
ctx.session.activeOrganizationId
|
|
||||||
) {
|
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
message: "You are not authorized to save this environment",
|
message: "You are not authorized to save this environment",
|
||||||
@@ -358,10 +322,7 @@ export const mongoRouter = createTRPCRouter({
|
|||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
const { mongoId, ...rest } = input;
|
const { mongoId, ...rest } = input;
|
||||||
const mongo = await findMongoById(mongoId);
|
const mongo = await findMongoById(mongoId);
|
||||||
if (
|
if (mongo.project.organizationId !== ctx.session.activeOrganizationId) {
|
||||||
mongo.environment.project.organizationId !==
|
|
||||||
ctx.session.activeOrganizationId
|
|
||||||
) {
|
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
message: "You are not authorized to update this mongo",
|
message: "You are not authorized to update this mongo",
|
||||||
@@ -384,31 +345,23 @@ export const mongoRouter = createTRPCRouter({
|
|||||||
.input(
|
.input(
|
||||||
z.object({
|
z.object({
|
||||||
mongoId: z.string(),
|
mongoId: z.string(),
|
||||||
targetEnvironmentId: z.string(),
|
targetProjectId: z.string(),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
const mongo = await findMongoById(input.mongoId);
|
const mongo = await findMongoById(input.mongoId);
|
||||||
if (
|
if (mongo.project.organizationId !== ctx.session.activeOrganizationId) {
|
||||||
mongo.environment.project.organizationId !==
|
|
||||||
ctx.session.activeOrganizationId
|
|
||||||
) {
|
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
message: "You are not authorized to move this mongo",
|
message: "You are not authorized to move this mongo",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const targetEnvironment = await findEnvironmentById(
|
const targetProject = await findProjectById(input.targetProjectId);
|
||||||
input.targetEnvironmentId,
|
if (targetProject.organizationId !== ctx.session.activeOrganizationId) {
|
||||||
);
|
|
||||||
if (
|
|
||||||
targetEnvironment.project.organizationId !==
|
|
||||||
ctx.session.activeOrganizationId
|
|
||||||
) {
|
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
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
|
const updatedMongo = await db
|
||||||
.update(mongoTable)
|
.update(mongoTable)
|
||||||
.set({
|
.set({
|
||||||
environmentId: input.targetEnvironmentId,
|
projectId: input.targetProjectId,
|
||||||
})
|
})
|
||||||
.where(eq(mongoTable.mongoId, input.mongoId))
|
.where(eq(mongoTable.mongoId, input.mongoId))
|
||||||
.returning()
|
.returning()
|
||||||
@@ -435,10 +388,7 @@ export const mongoRouter = createTRPCRouter({
|
|||||||
.input(apiRebuildMongo)
|
.input(apiRebuildMongo)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
const mongo = await findMongoById(input.mongoId);
|
const mongo = await findMongoById(input.mongoId);
|
||||||
if (
|
if (mongo.project.organizationId !== ctx.session.activeOrganizationId) {
|
||||||
mongo.environment.project.organizationId !==
|
|
||||||
ctx.session.activeOrganizationId
|
|
||||||
) {
|
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
message: "You are not authorized to rebuild this MongoDB database",
|
message: "You are not authorized to rebuild this MongoDB database",
|
||||||
|
|||||||
@@ -3,11 +3,9 @@ import {
|
|||||||
deleteMount,
|
deleteMount,
|
||||||
findApplicationById,
|
findApplicationById,
|
||||||
findMountById,
|
findMountById,
|
||||||
findMountOrganizationId,
|
|
||||||
getServiceContainer,
|
getServiceContainer,
|
||||||
updateMount,
|
updateMount,
|
||||||
} from "@dokploy/server";
|
} from "@dokploy/server";
|
||||||
import { TRPCError } from "@trpc/server";
|
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import {
|
import {
|
||||||
apiCreateMount,
|
apiCreateMount,
|
||||||
@@ -26,39 +24,16 @@ export const mountRouter = createTRPCRouter({
|
|||||||
}),
|
}),
|
||||||
remove: protectedProcedure
|
remove: protectedProcedure
|
||||||
.input(apiRemoveMount)
|
.input(apiRemoveMount)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input }) => {
|
||||||
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",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return await deleteMount(input.mountId);
|
return await deleteMount(input.mountId);
|
||||||
}),
|
}),
|
||||||
|
|
||||||
one: protectedProcedure
|
one: protectedProcedure.input(apiFindOneMount).query(async ({ input }) => {
|
||||||
.input(apiFindOneMount)
|
return await findMountById(input.mountId);
|
||||||
.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);
|
|
||||||
}),
|
|
||||||
update: protectedProcedure
|
update: protectedProcedure
|
||||||
.input(apiUpdateMount)
|
.input(apiUpdateMount)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input }) => {
|
||||||
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",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return await updateMount(input.mountId, input);
|
return await updateMount(input.mountId, input);
|
||||||
}),
|
}),
|
||||||
allNamedByApplicationId: protectedProcedure
|
allNamedByApplicationId: protectedProcedure
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import {
|
|||||||
createMysql,
|
createMysql,
|
||||||
deployMySql,
|
deployMySql,
|
||||||
findBackupsByDbId,
|
findBackupsByDbId,
|
||||||
findEnvironmentById,
|
|
||||||
findMySqlById,
|
findMySqlById,
|
||||||
findProjectById,
|
findProjectById,
|
||||||
IS_CLOUD,
|
IS_CLOUD,
|
||||||
@@ -43,14 +42,10 @@ export const mysqlRouter = createTRPCRouter({
|
|||||||
.input(apiCreateMySql)
|
.input(apiCreateMySql)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
try {
|
try {
|
||||||
// Get project from environment
|
|
||||||
const environment = await findEnvironmentById(input.environmentId);
|
|
||||||
const project = await findProjectById(environment.projectId);
|
|
||||||
|
|
||||||
if (ctx.user.role === "member") {
|
if (ctx.user.role === "member") {
|
||||||
await checkServiceAccess(
|
await checkServiceAccess(
|
||||||
ctx.user.id,
|
ctx.user.id,
|
||||||
project.projectId,
|
input.projectId,
|
||||||
ctx.session.activeOrganizationId,
|
ctx.session.activeOrganizationId,
|
||||||
"create",
|
"create",
|
||||||
);
|
);
|
||||||
@@ -62,7 +57,8 @@ export const mysqlRouter = createTRPCRouter({
|
|||||||
message: "You need to use a server to create a MySQL",
|
message: "You need to use a server to create a MySQL",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
1;
|
||||||
|
const project = await findProjectById(input.projectId);
|
||||||
if (project.organizationId !== ctx.session.activeOrganizationId) {
|
if (project.organizationId !== ctx.session.activeOrganizationId) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
@@ -70,9 +66,7 @@ export const mysqlRouter = createTRPCRouter({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const newMysql = await createMysql({
|
const newMysql = await createMysql(input);
|
||||||
...input,
|
|
||||||
});
|
|
||||||
if (ctx.user.role === "member") {
|
if (ctx.user.role === "member") {
|
||||||
await addNewService(
|
await addNewService(
|
||||||
ctx.user.id,
|
ctx.user.id,
|
||||||
@@ -113,10 +107,7 @@ export const mysqlRouter = createTRPCRouter({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
const mysql = await findMySqlById(input.mysqlId);
|
const mysql = await findMySqlById(input.mysqlId);
|
||||||
if (
|
if (mysql.project.organizationId !== ctx.session.activeOrganizationId) {
|
||||||
mysql.environment.project.organizationId !==
|
|
||||||
ctx.session.activeOrganizationId
|
|
||||||
) {
|
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
message: "You are not authorized to access this MySQL",
|
message: "You are not authorized to access this MySQL",
|
||||||
@@ -129,10 +120,7 @@ export const mysqlRouter = createTRPCRouter({
|
|||||||
.input(apiFindOneMySql)
|
.input(apiFindOneMySql)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
const service = await findMySqlById(input.mysqlId);
|
const service = await findMySqlById(input.mysqlId);
|
||||||
if (
|
if (service.project.organizationId !== ctx.session.activeOrganizationId) {
|
||||||
service.environment.project.organizationId !==
|
|
||||||
ctx.session.activeOrganizationId
|
|
||||||
) {
|
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
message: "You are not authorized to start this MySQL",
|
message: "You are not authorized to start this MySQL",
|
||||||
@@ -154,10 +142,7 @@ export const mysqlRouter = createTRPCRouter({
|
|||||||
.input(apiFindOneMySql)
|
.input(apiFindOneMySql)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
const mongo = await findMySqlById(input.mysqlId);
|
const mongo = await findMySqlById(input.mysqlId);
|
||||||
if (
|
if (mongo.project.organizationId !== ctx.session.activeOrganizationId) {
|
||||||
mongo.environment.project.organizationId !==
|
|
||||||
ctx.session.activeOrganizationId
|
|
||||||
) {
|
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
message: "You are not authorized to stop this MySQL",
|
message: "You are not authorized to stop this MySQL",
|
||||||
@@ -178,10 +163,7 @@ export const mysqlRouter = createTRPCRouter({
|
|||||||
.input(apiSaveExternalPortMySql)
|
.input(apiSaveExternalPortMySql)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
const mongo = await findMySqlById(input.mysqlId);
|
const mongo = await findMySqlById(input.mysqlId);
|
||||||
if (
|
if (mongo.project.organizationId !== ctx.session.activeOrganizationId) {
|
||||||
mongo.environment.project.organizationId !==
|
|
||||||
ctx.session.activeOrganizationId
|
|
||||||
) {
|
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
message: "You are not authorized to save this external port",
|
message: "You are not authorized to save this external port",
|
||||||
@@ -197,10 +179,7 @@ export const mysqlRouter = createTRPCRouter({
|
|||||||
.input(apiDeployMySql)
|
.input(apiDeployMySql)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
const mysql = await findMySqlById(input.mysqlId);
|
const mysql = await findMySqlById(input.mysqlId);
|
||||||
if (
|
if (mysql.project.organizationId !== ctx.session.activeOrganizationId) {
|
||||||
mysql.environment.project.organizationId !==
|
|
||||||
ctx.session.activeOrganizationId
|
|
||||||
) {
|
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
message: "You are not authorized to deploy this MySQL",
|
message: "You are not authorized to deploy this MySQL",
|
||||||
@@ -220,10 +199,7 @@ export const mysqlRouter = createTRPCRouter({
|
|||||||
.input(apiDeployMySql)
|
.input(apiDeployMySql)
|
||||||
.subscription(async ({ input, ctx }) => {
|
.subscription(async ({ input, ctx }) => {
|
||||||
const mysql = await findMySqlById(input.mysqlId);
|
const mysql = await findMySqlById(input.mysqlId);
|
||||||
if (
|
if (mysql.project.organizationId !== ctx.session.activeOrganizationId) {
|
||||||
mysql.environment.project.organizationId !==
|
|
||||||
ctx.session.activeOrganizationId
|
|
||||||
) {
|
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
message: "You are not authorized to deploy this MySQL",
|
message: "You are not authorized to deploy this MySQL",
|
||||||
@@ -240,10 +216,7 @@ export const mysqlRouter = createTRPCRouter({
|
|||||||
.input(apiChangeMySqlStatus)
|
.input(apiChangeMySqlStatus)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
const mongo = await findMySqlById(input.mysqlId);
|
const mongo = await findMySqlById(input.mysqlId);
|
||||||
if (
|
if (mongo.project.organizationId !== ctx.session.activeOrganizationId) {
|
||||||
mongo.environment.project.organizationId !==
|
|
||||||
ctx.session.activeOrganizationId
|
|
||||||
) {
|
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
message: "You are not authorized to change this MySQL status",
|
message: "You are not authorized to change this MySQL status",
|
||||||
@@ -258,10 +231,7 @@ export const mysqlRouter = createTRPCRouter({
|
|||||||
.input(apiResetMysql)
|
.input(apiResetMysql)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
const mysql = await findMySqlById(input.mysqlId);
|
const mysql = await findMySqlById(input.mysqlId);
|
||||||
if (
|
if (mysql.project.organizationId !== ctx.session.activeOrganizationId) {
|
||||||
mysql.environment.project.organizationId !==
|
|
||||||
ctx.session.activeOrganizationId
|
|
||||||
) {
|
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
message: "You are not authorized to reload this MySQL",
|
message: "You are not authorized to reload this MySQL",
|
||||||
@@ -297,10 +267,7 @@ export const mysqlRouter = createTRPCRouter({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
const mongo = await findMySqlById(input.mysqlId);
|
const mongo = await findMySqlById(input.mysqlId);
|
||||||
if (
|
if (mongo.project.organizationId !== ctx.session.activeOrganizationId) {
|
||||||
mongo.environment.project.organizationId !==
|
|
||||||
ctx.session.activeOrganizationId
|
|
||||||
) {
|
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
message: "You are not authorized to delete this MySQL",
|
message: "You are not authorized to delete this MySQL",
|
||||||
@@ -326,10 +293,7 @@ export const mysqlRouter = createTRPCRouter({
|
|||||||
.input(apiSaveEnvironmentVariablesMySql)
|
.input(apiSaveEnvironmentVariablesMySql)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
const mysql = await findMySqlById(input.mysqlId);
|
const mysql = await findMySqlById(input.mysqlId);
|
||||||
if (
|
if (mysql.project.organizationId !== ctx.session.activeOrganizationId) {
|
||||||
mysql.environment.project.organizationId !==
|
|
||||||
ctx.session.activeOrganizationId
|
|
||||||
) {
|
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
message: "You are not authorized to save this environment",
|
message: "You are not authorized to save this environment",
|
||||||
@@ -353,10 +317,7 @@ export const mysqlRouter = createTRPCRouter({
|
|||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
const { mysqlId, ...rest } = input;
|
const { mysqlId, ...rest } = input;
|
||||||
const mysql = await findMySqlById(mysqlId);
|
const mysql = await findMySqlById(mysqlId);
|
||||||
if (
|
if (mysql.project.organizationId !== ctx.session.activeOrganizationId) {
|
||||||
mysql.environment.project.organizationId !==
|
|
||||||
ctx.session.activeOrganizationId
|
|
||||||
) {
|
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
message: "You are not authorized to update this MySQL",
|
message: "You are not authorized to update this MySQL",
|
||||||
@@ -379,31 +340,23 @@ export const mysqlRouter = createTRPCRouter({
|
|||||||
.input(
|
.input(
|
||||||
z.object({
|
z.object({
|
||||||
mysqlId: z.string(),
|
mysqlId: z.string(),
|
||||||
targetEnvironmentId: z.string(),
|
targetProjectId: z.string(),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
const mysql = await findMySqlById(input.mysqlId);
|
const mysql = await findMySqlById(input.mysqlId);
|
||||||
if (
|
if (mysql.project.organizationId !== ctx.session.activeOrganizationId) {
|
||||||
mysql.environment.project.organizationId !==
|
|
||||||
ctx.session.activeOrganizationId
|
|
||||||
) {
|
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
message: "You are not authorized to move this mysql",
|
message: "You are not authorized to move this mysql",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const targetEnvironment = await findEnvironmentById(
|
const targetProject = await findProjectById(input.targetProjectId);
|
||||||
input.targetEnvironmentId,
|
if (targetProject.organizationId !== ctx.session.activeOrganizationId) {
|
||||||
);
|
|
||||||
if (
|
|
||||||
targetEnvironment.project.organizationId !==
|
|
||||||
ctx.session.activeOrganizationId
|
|
||||||
) {
|
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
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
|
const updatedMysql = await db
|
||||||
.update(mysqlTable)
|
.update(mysqlTable)
|
||||||
.set({
|
.set({
|
||||||
environmentId: input.targetEnvironmentId,
|
projectId: input.targetProjectId,
|
||||||
})
|
})
|
||||||
.where(eq(mysqlTable.mysqlId, input.mysqlId))
|
.where(eq(mysqlTable.mysqlId, input.mysqlId))
|
||||||
.returning()
|
.returning()
|
||||||
@@ -430,10 +383,7 @@ export const mysqlRouter = createTRPCRouter({
|
|||||||
.input(apiRebuildMysql)
|
.input(apiRebuildMysql)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
const mysql = await findMySqlById(input.mysqlId);
|
const mysql = await findMySqlById(input.mysqlId);
|
||||||
if (
|
if (mysql.project.organizationId !== ctx.session.activeOrganizationId) {
|
||||||
mysql.environment.project.organizationId !==
|
|
||||||
ctx.session.activeOrganizationId
|
|
||||||
) {
|
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
message: "You are not authorized to rebuild this MySQL database",
|
message: "You are not authorized to rebuild this MySQL database",
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import {
|
|||||||
createDiscordNotification,
|
createDiscordNotification,
|
||||||
createEmailNotification,
|
createEmailNotification,
|
||||||
createGotifyNotification,
|
createGotifyNotification,
|
||||||
createNtfyNotification,
|
|
||||||
createSlackNotification,
|
createSlackNotification,
|
||||||
createTelegramNotification,
|
createTelegramNotification,
|
||||||
findNotificationById,
|
findNotificationById,
|
||||||
@@ -11,14 +10,12 @@ import {
|
|||||||
sendDiscordNotification,
|
sendDiscordNotification,
|
||||||
sendEmailNotification,
|
sendEmailNotification,
|
||||||
sendGotifyNotification,
|
sendGotifyNotification,
|
||||||
sendNtfyNotification,
|
|
||||||
sendServerThresholdNotifications,
|
sendServerThresholdNotifications,
|
||||||
sendSlackNotification,
|
sendSlackNotification,
|
||||||
sendTelegramNotification,
|
sendTelegramNotification,
|
||||||
updateDiscordNotification,
|
updateDiscordNotification,
|
||||||
updateEmailNotification,
|
updateEmailNotification,
|
||||||
updateGotifyNotification,
|
updateGotifyNotification,
|
||||||
updateNtfyNotification,
|
|
||||||
updateSlackNotification,
|
updateSlackNotification,
|
||||||
updateTelegramNotification,
|
updateTelegramNotification,
|
||||||
} from "@dokploy/server";
|
} from "@dokploy/server";
|
||||||
@@ -36,20 +33,17 @@ import {
|
|||||||
apiCreateDiscord,
|
apiCreateDiscord,
|
||||||
apiCreateEmail,
|
apiCreateEmail,
|
||||||
apiCreateGotify,
|
apiCreateGotify,
|
||||||
apiCreateNtfy,
|
|
||||||
apiCreateSlack,
|
apiCreateSlack,
|
||||||
apiCreateTelegram,
|
apiCreateTelegram,
|
||||||
apiFindOneNotification,
|
apiFindOneNotification,
|
||||||
apiTestDiscordConnection,
|
apiTestDiscordConnection,
|
||||||
apiTestEmailConnection,
|
apiTestEmailConnection,
|
||||||
apiTestGotifyConnection,
|
apiTestGotifyConnection,
|
||||||
apiTestNtfyConnection,
|
|
||||||
apiTestSlackConnection,
|
apiTestSlackConnection,
|
||||||
apiTestTelegramConnection,
|
apiTestTelegramConnection,
|
||||||
apiUpdateDiscord,
|
apiUpdateDiscord,
|
||||||
apiUpdateEmail,
|
apiUpdateEmail,
|
||||||
apiUpdateGotify,
|
apiUpdateGotify,
|
||||||
apiUpdateNtfy,
|
|
||||||
apiUpdateSlack,
|
apiUpdateSlack,
|
||||||
apiUpdateTelegram,
|
apiUpdateTelegram,
|
||||||
notifications,
|
notifications,
|
||||||
@@ -327,7 +321,6 @@ export const notificationRouter = createTRPCRouter({
|
|||||||
discord: true,
|
discord: true,
|
||||||
email: true,
|
email: true,
|
||||||
gotify: true,
|
gotify: true,
|
||||||
ntfy: true,
|
|
||||||
},
|
},
|
||||||
orderBy: desc(notifications.createdAt),
|
orderBy: desc(notifications.createdAt),
|
||||||
where: eq(notifications.organizationId, ctx.session.activeOrganizationId),
|
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 }) => {
|
getEmailProviders: adminProcedure.query(async ({ ctx }) => {
|
||||||
return await db.query.notifications.findMany({
|
return await db.query.notifications.findMany({
|
||||||
where: eq(notifications.organizationId, ctx.session.activeOrganizationId),
|
where: eq(notifications.organizationId, ctx.session.activeOrganizationId),
|
||||||
|
|||||||
@@ -27,44 +27,22 @@ export const portRouter = createTRPCRouter({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
one: protectedProcedure
|
one: protectedProcedure.input(apiFindOnePort).query(async ({ input }) => {
|
||||||
.input(apiFindOnePort)
|
try {
|
||||||
.query(async ({ input, ctx }) => {
|
return await finPortById(input.portId);
|
||||||
try {
|
} catch (error) {
|
||||||
const port = await finPortById(input.portId);
|
throw new TRPCError({
|
||||||
if (
|
code: "BAD_REQUEST",
|
||||||
port.application.environment.project.organizationId !==
|
message: "Port not found",
|
||||||
ctx.session.activeOrganizationId
|
cause: error,
|
||||||
) {
|
});
|
||||||
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,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
delete: protectedProcedure
|
delete: protectedProcedure
|
||||||
.input(apiFindOnePort)
|
.input(apiFindOnePort)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input }) => {
|
||||||
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",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
try {
|
try {
|
||||||
return await removePortById(input.portId);
|
return removePortById(input.portId);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const message =
|
const message =
|
||||||
error instanceof Error ? error.message : "Error input: Deleting port";
|
error instanceof Error ? error.message : "Error input: Deleting port";
|
||||||
@@ -76,19 +54,9 @@ export const portRouter = createTRPCRouter({
|
|||||||
}),
|
}),
|
||||||
update: protectedProcedure
|
update: protectedProcedure
|
||||||
.input(apiUpdatePort)
|
.input(apiUpdatePort)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input }) => {
|
||||||
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",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
try {
|
try {
|
||||||
return await updatePortById(input.portId, input);
|
return updatePortById(input.portId, input);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const message =
|
const message =
|
||||||
error instanceof Error ? error.message : "Error updating the port";
|
error instanceof Error ? error.message : "Error updating the port";
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import {
|
|||||||
createPostgres,
|
createPostgres,
|
||||||
deployPostgres,
|
deployPostgres,
|
||||||
findBackupsByDbId,
|
findBackupsByDbId,
|
||||||
findEnvironmentById,
|
|
||||||
findPostgresById,
|
findPostgresById,
|
||||||
findProjectById,
|
findProjectById,
|
||||||
IS_CLOUD,
|
IS_CLOUD,
|
||||||
@@ -42,14 +41,10 @@ export const postgresRouter = createTRPCRouter({
|
|||||||
.input(apiCreatePostgres)
|
.input(apiCreatePostgres)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
try {
|
try {
|
||||||
// Get project from environment
|
|
||||||
const environment = await findEnvironmentById(input.environmentId);
|
|
||||||
const project = await findProjectById(environment.projectId);
|
|
||||||
|
|
||||||
if (ctx.user.role === "member") {
|
if (ctx.user.role === "member") {
|
||||||
await checkServiceAccess(
|
await checkServiceAccess(
|
||||||
ctx.user.id,
|
ctx.user.id,
|
||||||
project.projectId,
|
input.projectId,
|
||||||
ctx.session.activeOrganizationId,
|
ctx.session.activeOrganizationId,
|
||||||
"create",
|
"create",
|
||||||
);
|
);
|
||||||
@@ -62,15 +57,14 @@ export const postgresRouter = createTRPCRouter({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const project = await findProjectById(input.projectId);
|
||||||
if (project.organizationId !== ctx.session.activeOrganizationId) {
|
if (project.organizationId !== ctx.session.activeOrganizationId) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
message: "You are not authorized to access this project",
|
message: "You are not authorized to access this project",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
const newPostgres = await createPostgres({
|
const newPostgres = await createPostgres(input);
|
||||||
...input,
|
|
||||||
});
|
|
||||||
if (ctx.user.role === "member") {
|
if (ctx.user.role === "member") {
|
||||||
await addNewService(
|
await addNewService(
|
||||||
ctx.user.id,
|
ctx.user.id,
|
||||||
@@ -113,8 +107,7 @@ export const postgresRouter = createTRPCRouter({
|
|||||||
|
|
||||||
const postgres = await findPostgresById(input.postgresId);
|
const postgres = await findPostgresById(input.postgresId);
|
||||||
if (
|
if (
|
||||||
postgres.environment.project.organizationId !==
|
postgres.project.organizationId !== ctx.session.activeOrganizationId
|
||||||
ctx.session.activeOrganizationId
|
|
||||||
) {
|
) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
@@ -129,10 +122,7 @@ export const postgresRouter = createTRPCRouter({
|
|||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
const service = await findPostgresById(input.postgresId);
|
const service = await findPostgresById(input.postgresId);
|
||||||
|
|
||||||
if (
|
if (service.project.organizationId !== ctx.session.activeOrganizationId) {
|
||||||
service.environment.project.organizationId !==
|
|
||||||
ctx.session.activeOrganizationId
|
|
||||||
) {
|
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
message: "You are not authorized to start this Postgres",
|
message: "You are not authorized to start this Postgres",
|
||||||
@@ -155,8 +145,7 @@ export const postgresRouter = createTRPCRouter({
|
|||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
const postgres = await findPostgresById(input.postgresId);
|
const postgres = await findPostgresById(input.postgresId);
|
||||||
if (
|
if (
|
||||||
postgres.environment.project.organizationId !==
|
postgres.project.organizationId !== ctx.session.activeOrganizationId
|
||||||
ctx.session.activeOrganizationId
|
|
||||||
) {
|
) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
@@ -180,8 +169,7 @@ export const postgresRouter = createTRPCRouter({
|
|||||||
const postgres = await findPostgresById(input.postgresId);
|
const postgres = await findPostgresById(input.postgresId);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
postgres.environment.project.organizationId !==
|
postgres.project.organizationId !== ctx.session.activeOrganizationId
|
||||||
ctx.session.activeOrganizationId
|
|
||||||
) {
|
) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
@@ -199,8 +187,7 @@ export const postgresRouter = createTRPCRouter({
|
|||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
const postgres = await findPostgresById(input.postgresId);
|
const postgres = await findPostgresById(input.postgresId);
|
||||||
if (
|
if (
|
||||||
postgres.environment.project.organizationId !==
|
postgres.project.organizationId !== ctx.session.activeOrganizationId
|
||||||
ctx.session.activeOrganizationId
|
|
||||||
) {
|
) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
@@ -223,8 +210,7 @@ export const postgresRouter = createTRPCRouter({
|
|||||||
.subscription(async ({ input, ctx }) => {
|
.subscription(async ({ input, ctx }) => {
|
||||||
const postgres = await findPostgresById(input.postgresId);
|
const postgres = await findPostgresById(input.postgresId);
|
||||||
if (
|
if (
|
||||||
postgres.environment.project.organizationId !==
|
postgres.project.organizationId !== ctx.session.activeOrganizationId
|
||||||
ctx.session.activeOrganizationId
|
|
||||||
) {
|
) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
@@ -243,8 +229,7 @@ export const postgresRouter = createTRPCRouter({
|
|||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
const postgres = await findPostgresById(input.postgresId);
|
const postgres = await findPostgresById(input.postgresId);
|
||||||
if (
|
if (
|
||||||
postgres.environment.project.organizationId !==
|
postgres.project.organizationId !== ctx.session.activeOrganizationId
|
||||||
ctx.session.activeOrganizationId
|
|
||||||
) {
|
) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
@@ -270,8 +255,7 @@ export const postgresRouter = createTRPCRouter({
|
|||||||
const postgres = await findPostgresById(input.postgresId);
|
const postgres = await findPostgresById(input.postgresId);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
postgres.environment.project.organizationId !==
|
postgres.project.organizationId !== ctx.session.activeOrganizationId
|
||||||
ctx.session.activeOrganizationId
|
|
||||||
) {
|
) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
@@ -296,8 +280,7 @@ export const postgresRouter = createTRPCRouter({
|
|||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
const postgres = await findPostgresById(input.postgresId);
|
const postgres = await findPostgresById(input.postgresId);
|
||||||
if (
|
if (
|
||||||
postgres.environment.project.organizationId !==
|
postgres.project.organizationId !== ctx.session.activeOrganizationId
|
||||||
ctx.session.activeOrganizationId
|
|
||||||
) {
|
) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
@@ -322,8 +305,7 @@ export const postgresRouter = createTRPCRouter({
|
|||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
const postgres = await findPostgresById(input.postgresId);
|
const postgres = await findPostgresById(input.postgresId);
|
||||||
if (
|
if (
|
||||||
postgres.environment.project.organizationId !==
|
postgres.project.organizationId !== ctx.session.activeOrganizationId
|
||||||
ctx.session.activeOrganizationId
|
|
||||||
) {
|
) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
@@ -355,8 +337,7 @@ export const postgresRouter = createTRPCRouter({
|
|||||||
const { postgresId, ...rest } = input;
|
const { postgresId, ...rest } = input;
|
||||||
const postgres = await findPostgresById(postgresId);
|
const postgres = await findPostgresById(postgresId);
|
||||||
if (
|
if (
|
||||||
postgres.environment.project.organizationId !==
|
postgres.project.organizationId !== ctx.session.activeOrganizationId
|
||||||
ctx.session.activeOrganizationId
|
|
||||||
) {
|
) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
@@ -380,14 +361,13 @@ export const postgresRouter = createTRPCRouter({
|
|||||||
.input(
|
.input(
|
||||||
z.object({
|
z.object({
|
||||||
postgresId: z.string(),
|
postgresId: z.string(),
|
||||||
targetEnvironmentId: z.string(),
|
targetProjectId: z.string(),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
const postgres = await findPostgresById(input.postgresId);
|
const postgres = await findPostgresById(input.postgresId);
|
||||||
if (
|
if (
|
||||||
postgres.environment.project.organizationId !==
|
postgres.project.organizationId !== ctx.session.activeOrganizationId
|
||||||
ctx.session.activeOrganizationId
|
|
||||||
) {
|
) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
@@ -395,16 +375,11 @@ export const postgresRouter = createTRPCRouter({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const targetEnvironment = await findEnvironmentById(
|
const targetProject = await findProjectById(input.targetProjectId);
|
||||||
input.targetEnvironmentId,
|
if (targetProject.organizationId !== ctx.session.activeOrganizationId) {
|
||||||
);
|
|
||||||
if (
|
|
||||||
targetEnvironment.project.organizationId !==
|
|
||||||
ctx.session.activeOrganizationId
|
|
||||||
) {
|
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
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
|
const updatedPostgres = await db
|
||||||
.update(postgresTable)
|
.update(postgresTable)
|
||||||
.set({
|
.set({
|
||||||
environmentId: input.targetEnvironmentId,
|
projectId: input.targetProjectId,
|
||||||
})
|
})
|
||||||
.where(eq(postgresTable.postgresId, input.postgresId))
|
.where(eq(postgresTable.postgresId, input.postgresId))
|
||||||
.returning()
|
.returning()
|
||||||
@@ -432,8 +407,7 @@ export const postgresRouter = createTRPCRouter({
|
|||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
const postgres = await findPostgresById(input.postgresId);
|
const postgres = await findPostgresById(input.postgresId);
|
||||||
if (
|
if (
|
||||||
postgres.environment.project.organizationId !==
|
postgres.project.organizationId !== ctx.session.activeOrganizationId
|
||||||
ctx.session.activeOrganizationId
|
|
||||||
) {
|
) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
|
|||||||
@@ -15,8 +15,7 @@ export const previewDeploymentRouter = createTRPCRouter({
|
|||||||
.query(async ({ input, ctx }) => {
|
.query(async ({ input, ctx }) => {
|
||||||
const application = await findApplicationById(input.applicationId);
|
const application = await findApplicationById(input.applicationId);
|
||||||
if (
|
if (
|
||||||
application.environment.project.organizationId !==
|
application.project.organizationId !== ctx.session.activeOrganizationId
|
||||||
ctx.session.activeOrganizationId
|
|
||||||
) {
|
) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
@@ -32,7 +31,7 @@ export const previewDeploymentRouter = createTRPCRouter({
|
|||||||
input.previewDeploymentId,
|
input.previewDeploymentId,
|
||||||
);
|
);
|
||||||
if (
|
if (
|
||||||
previewDeployment.application.environment.project.organizationId !==
|
previewDeployment.application.project.organizationId !==
|
||||||
ctx.session.activeOrganizationId
|
ctx.session.activeOrganizationId
|
||||||
) {
|
) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
@@ -50,7 +49,7 @@ export const previewDeploymentRouter = createTRPCRouter({
|
|||||||
input.previewDeploymentId,
|
input.previewDeploymentId,
|
||||||
);
|
);
|
||||||
if (
|
if (
|
||||||
previewDeployment.application.environment.project.organizationId !==
|
previewDeployment.application.project.organizationId !==
|
||||||
ctx.session.activeOrganizationId
|
ctx.session.activeOrganizationId
|
||||||
) {
|
) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ import {
|
|||||||
deleteProject,
|
deleteProject,
|
||||||
findApplicationById,
|
findApplicationById,
|
||||||
findComposeById,
|
findComposeById,
|
||||||
findEnvironmentById,
|
|
||||||
findMariadbById,
|
findMariadbById,
|
||||||
findMemberById,
|
findMemberById,
|
||||||
findMongoById,
|
findMongoById,
|
||||||
@@ -44,7 +43,6 @@ import {
|
|||||||
apiUpdateProject,
|
apiUpdateProject,
|
||||||
applications,
|
applications,
|
||||||
compose,
|
compose,
|
||||||
environments,
|
|
||||||
mariadb,
|
mariadb,
|
||||||
mongo,
|
mongo,
|
||||||
mysql,
|
mysql,
|
||||||
@@ -82,7 +80,7 @@ export const projectRouter = createTRPCRouter({
|
|||||||
if (ctx.user.role === "member") {
|
if (ctx.user.role === "member") {
|
||||||
await addNewProject(
|
await addNewProject(
|
||||||
ctx.user.id,
|
ctx.user.id,
|
||||||
project.project.projectId,
|
project.projectId,
|
||||||
ctx.session.activeOrganizationId,
|
ctx.session.activeOrganizationId,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -119,42 +117,29 @@ export const projectRouter = createTRPCRouter({
|
|||||||
eq(projects.organizationId, ctx.session.activeOrganizationId),
|
eq(projects.organizationId, ctx.session.activeOrganizationId),
|
||||||
),
|
),
|
||||||
with: {
|
with: {
|
||||||
environments: {
|
applications: {
|
||||||
with: {
|
where: buildServiceFilter(
|
||||||
applications: {
|
applications.applicationId,
|
||||||
where: buildServiceFilter(
|
accessedServices,
|
||||||
applications.applicationId,
|
),
|
||||||
accessedServices,
|
},
|
||||||
),
|
compose: {
|
||||||
},
|
where: buildServiceFilter(compose.composeId, accessedServices),
|
||||||
compose: {
|
},
|
||||||
where: buildServiceFilter(
|
mariadb: {
|
||||||
compose.composeId,
|
where: buildServiceFilter(mariadb.mariadbId, accessedServices),
|
||||||
accessedServices,
|
},
|
||||||
),
|
mongo: {
|
||||||
},
|
where: buildServiceFilter(mongo.mongoId, accessedServices),
|
||||||
mariadb: {
|
},
|
||||||
where: buildServiceFilter(
|
mysql: {
|
||||||
mariadb.mariadbId,
|
where: buildServiceFilter(mysql.mysqlId, accessedServices),
|
||||||
accessedServices,
|
},
|
||||||
),
|
postgres: {
|
||||||
},
|
where: buildServiceFilter(postgres.postgresId, accessedServices),
|
||||||
mongo: {
|
},
|
||||||
where: buildServiceFilter(mongo.mongoId, accessedServices),
|
redis: {
|
||||||
},
|
where: buildServiceFilter(redis.redisId, 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 }) => {
|
all: protectedProcedure.query(async ({ ctx }) => {
|
||||||
if (ctx.user.role === "member") {
|
if (ctx.user.role === "member") {
|
||||||
const { accessedProjects, accessedEnvironments, accessedServices } =
|
const { accessedProjects, accessedServices } = await findMemberById(
|
||||||
await findMemberById(ctx.user.id, ctx.session.activeOrganizationId);
|
ctx.user.id,
|
||||||
|
ctx.session.activeOrganizationId,
|
||||||
|
);
|
||||||
|
|
||||||
if (accessedProjects.length === 0) {
|
if (accessedProjects.length === 0) {
|
||||||
return [];
|
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({
|
return await db.query.projects.findMany({
|
||||||
where: and(
|
where: and(
|
||||||
sql`${projects.projectId} IN (${sql.join(
|
sql`${projects.projectId} IN (${sql.join(
|
||||||
@@ -204,39 +182,31 @@ export const projectRouter = createTRPCRouter({
|
|||||||
eq(projects.organizationId, ctx.session.activeOrganizationId),
|
eq(projects.organizationId, ctx.session.activeOrganizationId),
|
||||||
),
|
),
|
||||||
with: {
|
with: {
|
||||||
environments: {
|
applications: {
|
||||||
where: environmentFilter,
|
where: buildServiceFilter(
|
||||||
with: {
|
applications.applicationId,
|
||||||
applications: {
|
accessedServices,
|
||||||
where: buildServiceFilter(
|
),
|
||||||
applications.applicationId,
|
with: { domains: true },
|
||||||
accessedServices,
|
},
|
||||||
),
|
mariadb: {
|
||||||
with: { domains: true },
|
where: buildServiceFilter(mariadb.mariadbId, accessedServices),
|
||||||
},
|
},
|
||||||
mariadb: {
|
mongo: {
|
||||||
where: buildServiceFilter(mariadb.mariadbId, accessedServices),
|
where: buildServiceFilter(mongo.mongoId, accessedServices),
|
||||||
},
|
},
|
||||||
mongo: {
|
mysql: {
|
||||||
where: buildServiceFilter(mongo.mongoId, accessedServices),
|
where: buildServiceFilter(mysql.mysqlId, accessedServices),
|
||||||
},
|
},
|
||||||
mysql: {
|
postgres: {
|
||||||
where: buildServiceFilter(mysql.mysqlId, accessedServices),
|
where: buildServiceFilter(postgres.postgresId, accessedServices),
|
||||||
},
|
},
|
||||||
postgres: {
|
redis: {
|
||||||
where: buildServiceFilter(
|
where: buildServiceFilter(redis.redisId, accessedServices),
|
||||||
postgres.postgresId,
|
},
|
||||||
accessedServices,
|
compose: {
|
||||||
),
|
where: buildServiceFilter(compose.composeId, accessedServices),
|
||||||
},
|
with: { domains: true },
|
||||||
redis: {
|
|
||||||
where: buildServiceFilter(redis.redisId, accessedServices),
|
|
||||||
},
|
|
||||||
compose: {
|
|
||||||
where: buildServiceFilter(compose.composeId, accessedServices),
|
|
||||||
with: { domains: true },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
orderBy: desc(projects.createdAt),
|
orderBy: desc(projects.createdAt),
|
||||||
@@ -245,23 +215,19 @@ export const projectRouter = createTRPCRouter({
|
|||||||
|
|
||||||
return await db.query.projects.findMany({
|
return await db.query.projects.findMany({
|
||||||
with: {
|
with: {
|
||||||
environments: {
|
applications: {
|
||||||
with: {
|
with: {
|
||||||
applications: {
|
domains: true,
|
||||||
with: {
|
},
|
||||||
domains: true,
|
},
|
||||||
},
|
mariadb: true,
|
||||||
},
|
mongo: true,
|
||||||
mariadb: true,
|
mysql: true,
|
||||||
mongo: true,
|
postgres: true,
|
||||||
mysql: true,
|
redis: true,
|
||||||
postgres: true,
|
compose: {
|
||||||
redis: true,
|
with: {
|
||||||
compose: {
|
domains: true,
|
||||||
with: {
|
|
||||||
domains: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -322,7 +288,7 @@ export const projectRouter = createTRPCRouter({
|
|||||||
duplicate: protectedProcedure
|
duplicate: protectedProcedure
|
||||||
.input(
|
.input(
|
||||||
z.object({
|
z.object({
|
||||||
sourceEnvironmentId: z.string(),
|
sourceProjectId: z.string(),
|
||||||
name: z.string(),
|
name: z.string(),
|
||||||
description: z.string().optional(),
|
description: z.string().optional(),
|
||||||
includeServices: z.boolean().default(true),
|
includeServices: z.boolean().default(true),
|
||||||
@@ -356,15 +322,9 @@ export const projectRouter = createTRPCRouter({
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Get source project
|
// Get source project
|
||||||
const sourceEnvironment = input.duplicateInSameProject
|
const sourceProject = await findProjectById(input.sourceProjectId);
|
||||||
? await findEnvironmentById(input.sourceEnvironmentId)
|
|
||||||
: null;
|
|
||||||
|
|
||||||
if (
|
if (sourceProject.organizationId !== ctx.session.activeOrganizationId) {
|
||||||
input.duplicateInSameProject &&
|
|
||||||
sourceEnvironment?.project.organizationId !==
|
|
||||||
ctx.session.activeOrganizationId
|
|
||||||
) {
|
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
message: "You are not authorized to access this project",
|
message: "You are not authorized to access this project",
|
||||||
@@ -373,17 +333,15 @@ export const projectRouter = createTRPCRouter({
|
|||||||
|
|
||||||
// Create new project or use existing one
|
// Create new project or use existing one
|
||||||
const targetProject = input.duplicateInSameProject
|
const targetProject = input.duplicateInSameProject
|
||||||
? sourceEnvironment
|
? sourceProject
|
||||||
: await createProject(
|
: await createProject(
|
||||||
{
|
{
|
||||||
name: input.name,
|
name: input.name,
|
||||||
description: input.description,
|
description: input.description,
|
||||||
env: sourceEnvironment?.project.env,
|
env: sourceProject.env,
|
||||||
},
|
},
|
||||||
ctx.session.activeOrganizationId,
|
ctx.session.activeOrganizationId,
|
||||||
).then((value) => value.environment);
|
);
|
||||||
|
|
||||||
console.log("targetProject", targetProject);
|
|
||||||
|
|
||||||
if (input.includeServices) {
|
if (input.includeServices) {
|
||||||
const servicesToDuplicate = input.selectedServices || [];
|
const servicesToDuplicate = input.selectedServices || [];
|
||||||
@@ -416,7 +374,7 @@ export const projectRouter = createTRPCRouter({
|
|||||||
name: input.duplicateInSameProject
|
name: input.duplicateInSameProject
|
||||||
? `${application.name} (copy)`
|
? `${application.name} (copy)`
|
||||||
: application.name,
|
: application.name,
|
||||||
environmentId: targetProject?.environmentId || "",
|
projectId: targetProject.projectId,
|
||||||
});
|
});
|
||||||
|
|
||||||
for (const domain of domains) {
|
for (const domain of domains) {
|
||||||
@@ -486,7 +444,7 @@ export const projectRouter = createTRPCRouter({
|
|||||||
name: input.duplicateInSameProject
|
name: input.duplicateInSameProject
|
||||||
? `${postgres.name} (copy)`
|
? `${postgres.name} (copy)`
|
||||||
: postgres.name,
|
: postgres.name,
|
||||||
environmentId: targetProject?.environmentId || "",
|
projectId: targetProject.projectId,
|
||||||
});
|
});
|
||||||
|
|
||||||
for (const mount of mounts) {
|
for (const mount of mounts) {
|
||||||
@@ -522,7 +480,7 @@ export const projectRouter = createTRPCRouter({
|
|||||||
name: input.duplicateInSameProject
|
name: input.duplicateInSameProject
|
||||||
? `${mariadb.name} (copy)`
|
? `${mariadb.name} (copy)`
|
||||||
: mariadb.name,
|
: mariadb.name,
|
||||||
environmentId: targetProject?.environmentId || "",
|
projectId: targetProject.projectId,
|
||||||
});
|
});
|
||||||
|
|
||||||
for (const mount of mounts) {
|
for (const mount of mounts) {
|
||||||
@@ -558,7 +516,7 @@ export const projectRouter = createTRPCRouter({
|
|||||||
name: input.duplicateInSameProject
|
name: input.duplicateInSameProject
|
||||||
? `${mongo.name} (copy)`
|
? `${mongo.name} (copy)`
|
||||||
: mongo.name,
|
: mongo.name,
|
||||||
environmentId: targetProject?.environmentId || "",
|
projectId: targetProject.projectId,
|
||||||
});
|
});
|
||||||
|
|
||||||
for (const mount of mounts) {
|
for (const mount of mounts) {
|
||||||
@@ -594,7 +552,7 @@ export const projectRouter = createTRPCRouter({
|
|||||||
name: input.duplicateInSameProject
|
name: input.duplicateInSameProject
|
||||||
? `${mysql.name} (copy)`
|
? `${mysql.name} (copy)`
|
||||||
: mysql.name,
|
: mysql.name,
|
||||||
environmentId: targetProject?.environmentId || "",
|
projectId: targetProject.projectId,
|
||||||
});
|
});
|
||||||
|
|
||||||
for (const mount of mounts) {
|
for (const mount of mounts) {
|
||||||
@@ -630,7 +588,7 @@ export const projectRouter = createTRPCRouter({
|
|||||||
name: input.duplicateInSameProject
|
name: input.duplicateInSameProject
|
||||||
? `${redis.name} (copy)`
|
? `${redis.name} (copy)`
|
||||||
: redis.name,
|
: redis.name,
|
||||||
environmentId: targetProject?.environmentId || "",
|
projectId: targetProject.projectId,
|
||||||
});
|
});
|
||||||
|
|
||||||
for (const mount of mounts) {
|
for (const mount of mounts) {
|
||||||
@@ -665,7 +623,7 @@ export const projectRouter = createTRPCRouter({
|
|||||||
name: input.duplicateInSameProject
|
name: input.duplicateInSameProject
|
||||||
? `${compose.name} (copy)`
|
? `${compose.name} (copy)`
|
||||||
: compose.name,
|
: compose.name,
|
||||||
environmentId: targetProject?.environmentId || "",
|
projectId: targetProject.projectId,
|
||||||
});
|
});
|
||||||
|
|
||||||
for (const mount of mounts) {
|
for (const mount of mounts) {
|
||||||
@@ -700,7 +658,7 @@ export const projectRouter = createTRPCRouter({
|
|||||||
if (!input.duplicateInSameProject && ctx.user.role === "member") {
|
if (!input.duplicateInSameProject && ctx.user.role === "member") {
|
||||||
await addNewProject(
|
await addNewProject(
|
||||||
ctx.user.id,
|
ctx.user.id,
|
||||||
targetProject?.projectId || "",
|
targetProject.projectId,
|
||||||
ctx.session.activeOrganizationId,
|
ctx.session.activeOrganizationId,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,8 +19,7 @@ export const redirectsRouter = createTRPCRouter({
|
|||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
const application = await findApplicationById(input.applicationId);
|
const application = await findApplicationById(input.applicationId);
|
||||||
if (
|
if (
|
||||||
application.environment.project.organizationId !==
|
application.project.organizationId !== ctx.session.activeOrganizationId
|
||||||
ctx.session.activeOrganizationId
|
|
||||||
) {
|
) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
@@ -35,8 +34,7 @@ export const redirectsRouter = createTRPCRouter({
|
|||||||
const redirect = await findRedirectById(input.redirectId);
|
const redirect = await findRedirectById(input.redirectId);
|
||||||
const application = await findApplicationById(redirect.applicationId);
|
const application = await findApplicationById(redirect.applicationId);
|
||||||
if (
|
if (
|
||||||
application.environment.project.organizationId !==
|
application.project.organizationId !== ctx.session.activeOrganizationId
|
||||||
ctx.session.activeOrganizationId
|
|
||||||
) {
|
) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
@@ -51,8 +49,7 @@ export const redirectsRouter = createTRPCRouter({
|
|||||||
const redirect = await findRedirectById(input.redirectId);
|
const redirect = await findRedirectById(input.redirectId);
|
||||||
const application = await findApplicationById(redirect.applicationId);
|
const application = await findApplicationById(redirect.applicationId);
|
||||||
if (
|
if (
|
||||||
application.environment.project.organizationId !==
|
application.project.organizationId !== ctx.session.activeOrganizationId
|
||||||
ctx.session.activeOrganizationId
|
|
||||||
) {
|
) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
@@ -67,8 +64,7 @@ export const redirectsRouter = createTRPCRouter({
|
|||||||
const redirect = await findRedirectById(input.redirectId);
|
const redirect = await findRedirectById(input.redirectId);
|
||||||
const application = await findApplicationById(redirect.applicationId);
|
const application = await findApplicationById(redirect.applicationId);
|
||||||
if (
|
if (
|
||||||
application.environment.project.organizationId !==
|
application.project.organizationId !== ctx.session.activeOrganizationId
|
||||||
ctx.session.activeOrganizationId
|
|
||||||
) {
|
) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import {
|
|||||||
createMount,
|
createMount,
|
||||||
createRedis,
|
createRedis,
|
||||||
deployRedis,
|
deployRedis,
|
||||||
findEnvironmentById,
|
|
||||||
findProjectById,
|
findProjectById,
|
||||||
findRedisById,
|
findRedisById,
|
||||||
IS_CLOUD,
|
IS_CLOUD,
|
||||||
@@ -41,14 +40,10 @@ export const redisRouter = createTRPCRouter({
|
|||||||
.input(apiCreateRedis)
|
.input(apiCreateRedis)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
try {
|
try {
|
||||||
// Get project from environment
|
|
||||||
const environment = await findEnvironmentById(input.environmentId);
|
|
||||||
const project = await findProjectById(environment.projectId);
|
|
||||||
|
|
||||||
if (ctx.user.role === "member") {
|
if (ctx.user.role === "member") {
|
||||||
await checkServiceAccess(
|
await checkServiceAccess(
|
||||||
ctx.user.id,
|
ctx.user.id,
|
||||||
project.projectId,
|
input.projectId,
|
||||||
ctx.session.activeOrganizationId,
|
ctx.session.activeOrganizationId,
|
||||||
"create",
|
"create",
|
||||||
);
|
);
|
||||||
@@ -61,15 +56,14 @@ export const redisRouter = createTRPCRouter({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const project = await findProjectById(input.projectId);
|
||||||
if (project.organizationId !== ctx.session.activeOrganizationId) {
|
if (project.organizationId !== ctx.session.activeOrganizationId) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
message: "You are not authorized to access this project",
|
message: "You are not authorized to access this project",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
const newRedis = await createRedis({
|
const newRedis = await createRedis(input);
|
||||||
...input,
|
|
||||||
});
|
|
||||||
if (ctx.user.role === "member") {
|
if (ctx.user.role === "member") {
|
||||||
await addNewService(
|
await addNewService(
|
||||||
ctx.user.id,
|
ctx.user.id,
|
||||||
@@ -104,10 +98,7 @@ export const redisRouter = createTRPCRouter({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const redis = await findRedisById(input.redisId);
|
const redis = await findRedisById(input.redisId);
|
||||||
if (
|
if (redis.project.organizationId !== ctx.session.activeOrganizationId) {
|
||||||
redis.environment.project.organizationId !==
|
|
||||||
ctx.session.activeOrganizationId
|
|
||||||
) {
|
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
message: "You are not authorized to access this Redis",
|
message: "You are not authorized to access this Redis",
|
||||||
@@ -120,10 +111,7 @@ export const redisRouter = createTRPCRouter({
|
|||||||
.input(apiFindOneRedis)
|
.input(apiFindOneRedis)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
const redis = await findRedisById(input.redisId);
|
const redis = await findRedisById(input.redisId);
|
||||||
if (
|
if (redis.project.organizationId !== ctx.session.activeOrganizationId) {
|
||||||
redis.environment.project.organizationId !==
|
|
||||||
ctx.session.activeOrganizationId
|
|
||||||
) {
|
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
message: "You are not authorized to start this Redis",
|
message: "You are not authorized to start this Redis",
|
||||||
@@ -145,10 +133,7 @@ export const redisRouter = createTRPCRouter({
|
|||||||
.input(apiResetRedis)
|
.input(apiResetRedis)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
const redis = await findRedisById(input.redisId);
|
const redis = await findRedisById(input.redisId);
|
||||||
if (
|
if (redis.project.organizationId !== ctx.session.activeOrganizationId) {
|
||||||
redis.environment.project.organizationId !==
|
|
||||||
ctx.session.activeOrganizationId
|
|
||||||
) {
|
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
message: "You are not authorized to reload this Redis",
|
message: "You are not authorized to reload this Redis",
|
||||||
@@ -178,10 +163,7 @@ export const redisRouter = createTRPCRouter({
|
|||||||
.input(apiFindOneRedis)
|
.input(apiFindOneRedis)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
const redis = await findRedisById(input.redisId);
|
const redis = await findRedisById(input.redisId);
|
||||||
if (
|
if (redis.project.organizationId !== ctx.session.activeOrganizationId) {
|
||||||
redis.environment.project.organizationId !==
|
|
||||||
ctx.session.activeOrganizationId
|
|
||||||
) {
|
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
message: "You are not authorized to stop this Redis",
|
message: "You are not authorized to stop this Redis",
|
||||||
@@ -202,10 +184,7 @@ export const redisRouter = createTRPCRouter({
|
|||||||
.input(apiSaveExternalPortRedis)
|
.input(apiSaveExternalPortRedis)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
const mongo = await findRedisById(input.redisId);
|
const mongo = await findRedisById(input.redisId);
|
||||||
if (
|
if (mongo.project.organizationId !== ctx.session.activeOrganizationId) {
|
||||||
mongo.environment.project.organizationId !==
|
|
||||||
ctx.session.activeOrganizationId
|
|
||||||
) {
|
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
message: "You are not authorized to save this external port",
|
message: "You are not authorized to save this external port",
|
||||||
@@ -221,10 +200,7 @@ export const redisRouter = createTRPCRouter({
|
|||||||
.input(apiDeployRedis)
|
.input(apiDeployRedis)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
const redis = await findRedisById(input.redisId);
|
const redis = await findRedisById(input.redisId);
|
||||||
if (
|
if (redis.project.organizationId !== ctx.session.activeOrganizationId) {
|
||||||
redis.environment.project.organizationId !==
|
|
||||||
ctx.session.activeOrganizationId
|
|
||||||
) {
|
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
message: "You are not authorized to deploy this Redis",
|
message: "You are not authorized to deploy this Redis",
|
||||||
@@ -244,10 +220,7 @@ export const redisRouter = createTRPCRouter({
|
|||||||
.input(apiDeployRedis)
|
.input(apiDeployRedis)
|
||||||
.subscription(async ({ input, ctx }) => {
|
.subscription(async ({ input, ctx }) => {
|
||||||
const redis = await findRedisById(input.redisId);
|
const redis = await findRedisById(input.redisId);
|
||||||
if (
|
if (redis.project.organizationId !== ctx.session.activeOrganizationId) {
|
||||||
redis.environment.project.organizationId !==
|
|
||||||
ctx.session.activeOrganizationId
|
|
||||||
) {
|
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
message: "You are not authorized to deploy this Redis",
|
message: "You are not authorized to deploy this Redis",
|
||||||
@@ -263,10 +236,7 @@ export const redisRouter = createTRPCRouter({
|
|||||||
.input(apiChangeRedisStatus)
|
.input(apiChangeRedisStatus)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
const mongo = await findRedisById(input.redisId);
|
const mongo = await findRedisById(input.redisId);
|
||||||
if (
|
if (mongo.project.organizationId !== ctx.session.activeOrganizationId) {
|
||||||
mongo.environment.project.organizationId !==
|
|
||||||
ctx.session.activeOrganizationId
|
|
||||||
) {
|
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
message: "You are not authorized to change this Redis status",
|
message: "You are not authorized to change this Redis status",
|
||||||
@@ -291,10 +261,7 @@ export const redisRouter = createTRPCRouter({
|
|||||||
|
|
||||||
const redis = await findRedisById(input.redisId);
|
const redis = await findRedisById(input.redisId);
|
||||||
|
|
||||||
if (
|
if (redis.project.organizationId !== ctx.session.activeOrganizationId) {
|
||||||
redis.environment.project.organizationId !==
|
|
||||||
ctx.session.activeOrganizationId
|
|
||||||
) {
|
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
message: "You are not authorized to delete this Redis",
|
message: "You are not authorized to delete this Redis",
|
||||||
@@ -317,10 +284,7 @@ export const redisRouter = createTRPCRouter({
|
|||||||
.input(apiSaveEnvironmentVariablesRedis)
|
.input(apiSaveEnvironmentVariablesRedis)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
const redis = await findRedisById(input.redisId);
|
const redis = await findRedisById(input.redisId);
|
||||||
if (
|
if (redis.project.organizationId !== ctx.session.activeOrganizationId) {
|
||||||
redis.environment.project.organizationId !==
|
|
||||||
ctx.session.activeOrganizationId
|
|
||||||
) {
|
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
message: "You are not authorized to save this environment",
|
message: "You are not authorized to save this environment",
|
||||||
@@ -360,31 +324,23 @@ export const redisRouter = createTRPCRouter({
|
|||||||
.input(
|
.input(
|
||||||
z.object({
|
z.object({
|
||||||
redisId: z.string(),
|
redisId: z.string(),
|
||||||
targetEnvironmentId: z.string(),
|
targetProjectId: z.string(),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
const redis = await findRedisById(input.redisId);
|
const redis = await findRedisById(input.redisId);
|
||||||
if (
|
if (redis.project.organizationId !== ctx.session.activeOrganizationId) {
|
||||||
redis.environment.project.organizationId !==
|
|
||||||
ctx.session.activeOrganizationId
|
|
||||||
) {
|
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
message: "You are not authorized to move this redis",
|
message: "You are not authorized to move this redis",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const targetEnvironment = await findEnvironmentById(
|
const targetProject = await findProjectById(input.targetProjectId);
|
||||||
input.targetEnvironmentId,
|
if (targetProject.organizationId !== ctx.session.activeOrganizationId) {
|
||||||
);
|
|
||||||
if (
|
|
||||||
targetEnvironment.project.organizationId !==
|
|
||||||
ctx.session.activeOrganizationId
|
|
||||||
) {
|
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
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
|
const updatedRedis = await db
|
||||||
.update(redisTable)
|
.update(redisTable)
|
||||||
.set({
|
.set({
|
||||||
environmentId: input.targetEnvironmentId,
|
projectId: input.targetProjectId,
|
||||||
})
|
})
|
||||||
.where(eq(redisTable.redisId, input.redisId))
|
.where(eq(redisTable.redisId, input.redisId))
|
||||||
.returning()
|
.returning()
|
||||||
@@ -411,10 +367,7 @@ export const redisRouter = createTRPCRouter({
|
|||||||
.input(apiRebuildRedis)
|
.input(apiRebuildRedis)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
const redis = await findRedisById(input.redisId);
|
const redis = await findRedisById(input.redisId);
|
||||||
if (
|
if (redis.project.organizationId !== ctx.session.activeOrganizationId) {
|
||||||
redis.environment.project.organizationId !==
|
|
||||||
ctx.session.activeOrganizationId
|
|
||||||
) {
|
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
message: "You are not authorized to rebuild this Redis database",
|
message: "You are not authorized to rebuild this Redis database",
|
||||||
|
|||||||
@@ -1,8 +1,4 @@
|
|||||||
import {
|
import { removeRollbackById, rollback } from "@dokploy/server";
|
||||||
findRollbackById,
|
|
||||||
removeRollbackById,
|
|
||||||
rollback,
|
|
||||||
} from "@dokploy/server";
|
|
||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc";
|
import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc";
|
||||||
import { apiFindOneRollback } from "@/server/db/schema";
|
import { apiFindOneRollback } from "@/server/db/schema";
|
||||||
@@ -26,18 +22,8 @@ export const rollbackRouter = createTRPCRouter({
|
|||||||
}),
|
}),
|
||||||
rollback: protectedProcedure
|
rollback: protectedProcedure
|
||||||
.input(apiFindOneRollback)
|
.input(apiFindOneRollback)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input }) => {
|
||||||
try {
|
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);
|
return await rollback(input.rollbackId);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
|
|||||||
@@ -19,8 +19,7 @@ export const securityRouter = createTRPCRouter({
|
|||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
const application = await findApplicationById(input.applicationId);
|
const application = await findApplicationById(input.applicationId);
|
||||||
if (
|
if (
|
||||||
application.environment.project.organizationId !==
|
application.project.organizationId !== ctx.session.activeOrganizationId
|
||||||
ctx.session.activeOrganizationId
|
|
||||||
) {
|
) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
@@ -35,8 +34,7 @@ export const securityRouter = createTRPCRouter({
|
|||||||
const security = await findSecurityById(input.securityId);
|
const security = await findSecurityById(input.securityId);
|
||||||
const application = await findApplicationById(security.applicationId);
|
const application = await findApplicationById(security.applicationId);
|
||||||
if (
|
if (
|
||||||
application.environment.project.organizationId !==
|
application.project.organizationId !== ctx.session.activeOrganizationId
|
||||||
ctx.session.activeOrganizationId
|
|
||||||
) {
|
) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
@@ -51,8 +49,7 @@ export const securityRouter = createTRPCRouter({
|
|||||||
const security = await findSecurityById(input.securityId);
|
const security = await findSecurityById(input.securityId);
|
||||||
const application = await findApplicationById(security.applicationId);
|
const application = await findApplicationById(security.applicationId);
|
||||||
if (
|
if (
|
||||||
application.environment.project.organizationId !==
|
application.project.organizationId !== ctx.session.activeOrganizationId
|
||||||
ctx.session.activeOrganizationId
|
|
||||||
) {
|
) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
@@ -67,8 +64,7 @@ export const securityRouter = createTRPCRouter({
|
|||||||
const security = await findSecurityById(input.securityId);
|
const security = await findSecurityById(input.securityId);
|
||||||
const application = await findApplicationById(security.applicationId);
|
const application = await findApplicationById(security.applicationId);
|
||||||
if (
|
if (
|
||||||
application.environment.project.organizationId !==
|
application.project.organizationId !== ctx.session.activeOrganizationId
|
||||||
ctx.session.activeOrganizationId
|
|
||||||
) {
|
) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
|
|||||||
104
apps/dokploy/server/queues/README.md
Normal file
104
apps/dokploy/server/queues/README.md
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
# Queue System Migration - BullMQ to p-limit
|
||||||
|
|
||||||
|
This directory contains the new queue system that replaces BullMQ with [p-limit](https://github.com/sindresorhus/p-limit) for deployment queues.
|
||||||
|
|
||||||
|
## Why the Migration?
|
||||||
|
|
||||||
|
- **Resource Issues**: Users experienced freezing during builds due to resource constraints
|
||||||
|
- **Cancellation Problems**: BullMQ workers couldn't be properly canceled when Docker processes restart
|
||||||
|
- **Retry Loops**: Unwanted automatic retries when processes are killed
|
||||||
|
|
||||||
|
## New Architecture
|
||||||
|
|
||||||
|
### Key Features
|
||||||
|
|
||||||
|
1. **Per-Server Queues**: Deployments are grouped by server (local "dokploy-server" or remote servers)
|
||||||
|
2. **Ordered Processing**: Within each server, deployments are processed based on server concurrency settings
|
||||||
|
3. **Global User Concurrency**: User's `serverConcurrency` controls total deployments across all servers
|
||||||
|
4. **Proper Cancellation**: Jobs can be canceled using AbortController
|
||||||
|
5. **No Redis Dependency**: In-memory queues eliminate Redis dependency issues
|
||||||
|
|
||||||
|
### Files
|
||||||
|
|
||||||
|
- `service-queue.ts` - New p-limit based queue implementation
|
||||||
|
- `queueSetup.ts` - Compatibility layer for existing code
|
||||||
|
- `deployments-queue.ts` - Legacy compatibility exports
|
||||||
|
- `queue-types.ts` - Shared type definitions
|
||||||
|
|
||||||
|
## Usage Examples
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { addJobWithUserContext, cancelDeploymentJobs, getDeploymentQueueStatus } from './queueSetup';
|
||||||
|
|
||||||
|
// Add a deployment job with user context (recommended for API routes)
|
||||||
|
const result = await addJobWithUserContext({
|
||||||
|
applicationType: 'application',
|
||||||
|
applicationId: '123',
|
||||||
|
type: 'deploy',
|
||||||
|
titleLog: 'Deploying app',
|
||||||
|
descriptionLog: 'Starting deployment',
|
||||||
|
serverId: 'server-456' // Optional - for remote deployments
|
||||||
|
}, 'user-id-789'); // User ID for concurrency settings
|
||||||
|
|
||||||
|
// Cancel jobs for a service
|
||||||
|
const cancelled = cancelDeploymentJobs('app-123');
|
||||||
|
|
||||||
|
// Get queue status
|
||||||
|
const status = getDeploymentQueueStatus('app-123');
|
||||||
|
```
|
||||||
|
|
||||||
|
### Database-Driven Concurrency
|
||||||
|
|
||||||
|
The system now automatically reads concurrency settings from the database:
|
||||||
|
|
||||||
|
1. **Global User Concurrency**: From `users_temp.serverConcurrency` field
|
||||||
|
- Controls the **TOTAL** number of deployments that can run simultaneously for a user
|
||||||
|
- Example: If `serverConcurrency = 1`, only 1 deployment across ALL services at a time
|
||||||
|
- Example: If `serverConcurrency = 3`, maximum 3 deployments can run simultaneously across all services
|
||||||
|
|
||||||
|
2. **Server Concurrency**: From `server.concurrency` field
|
||||||
|
- Controls how many deployments can run simultaneously **on a specific server**
|
||||||
|
- Only applies when deploying to remote servers (`serverId` is present)
|
||||||
|
- Example: Server A can handle 2 concurrent deployments, Server B can handle 1
|
||||||
|
|
||||||
|
### Concurrency Hierarchy
|
||||||
|
|
||||||
|
```
|
||||||
|
User Global Limit (users_temp.serverConcurrency)
|
||||||
|
├── dokploy-server (local deployments)
|
||||||
|
│ ├── App A deployment
|
||||||
|
│ ├── App B deployment
|
||||||
|
│ └── Compose C deployment
|
||||||
|
├── remote-server-1 (server.concurrency = 2)
|
||||||
|
│ ├── App D deployment
|
||||||
|
│ └── App E deployment
|
||||||
|
└── remote-server-2 (server.concurrency = 1)
|
||||||
|
└── App F deployment
|
||||||
|
```
|
||||||
|
|
||||||
|
**Example Scenarios:**
|
||||||
|
|
||||||
|
- **User has `serverConcurrency = 1`**: Only 1 deployment total across ALL servers
|
||||||
|
- **User has `serverConcurrency = 3`**: Maximum 3 deployments simultaneously across all servers
|
||||||
|
- **Local server**: All local apps/compose share the "dokploy-server" queue
|
||||||
|
- **Remote server with `concurrency = 2`**: That server can handle up to 2 concurrent deployments
|
||||||
|
- **Queue grouping**: `app-123` and `app-456` on same server share the same queue
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
- **Global Concurrency**: Set how many services can deploy simultaneously
|
||||||
|
- **Service Concurrency**: Each service processes 1 deployment at a time (FIFO)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { setGlobalConcurrency } from './service-queue';
|
||||||
|
|
||||||
|
// Allow 5 services to deploy simultaneously
|
||||||
|
setGlobalConcurrency(5);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Migration Notes
|
||||||
|
|
||||||
|
- The schedules app still uses BullMQ for cron/repeatable jobs (different use case)
|
||||||
|
- All existing API endpoints work unchanged due to compatibility layer
|
||||||
|
- No breaking changes to existing functionality
|
||||||
|
- Improved resource usage and cancellation capabilities
|
||||||
@@ -1,122 +1,58 @@
|
|||||||
import {
|
// This file is kept for backward compatibility but now uses the new service-queue system
|
||||||
deployApplication,
|
// The actual queue logic has been moved to service-queue.ts using p-limit
|
||||||
deployCompose,
|
|
||||||
deployPreviewApplication,
|
|
||||||
deployRemoteApplication,
|
|
||||||
deployRemoteCompose,
|
|
||||||
deployRemotePreviewApplication,
|
|
||||||
rebuildApplication,
|
|
||||||
rebuildCompose,
|
|
||||||
rebuildRemoteApplication,
|
|
||||||
rebuildRemoteCompose,
|
|
||||||
updateApplicationStatus,
|
|
||||||
updateCompose,
|
|
||||||
updatePreviewDeployment,
|
|
||||||
} from "@dokploy/server";
|
|
||||||
import { type Job, Worker } from "bullmq";
|
|
||||||
import type { DeploymentJob } from "./queue-types";
|
|
||||||
import { redisConfig } from "./redis-connection";
|
|
||||||
|
|
||||||
export const deploymentWorker = new Worker(
|
import { serviceQueueManager } from "./service-queue";
|
||||||
"deployments",
|
|
||||||
async (job: Job<DeploymentJob>) => {
|
|
||||||
try {
|
|
||||||
if (job.data.applicationType === "application") {
|
|
||||||
await updateApplicationStatus(job.data.applicationId, "running");
|
|
||||||
|
|
||||||
if (job.data.server) {
|
// Legacy compatibility - this is no longer used but kept to avoid breaking imports
|
||||||
if (job.data.type === "redeploy") {
|
export const deploymentWorker = {
|
||||||
await rebuildRemoteApplication({
|
run: async () => {
|
||||||
applicationId: job.data.applicationId,
|
console.log(
|
||||||
titleLog: job.data.titleLog,
|
"Legacy deploymentWorker.run() called - now using service-queue system",
|
||||||
descriptionLog: job.data.descriptionLog,
|
);
|
||||||
});
|
// The service queue manager starts automatically, no need to do anything
|
||||||
} else if (job.data.type === "deploy") {
|
return Promise.resolve();
|
||||||
await deployRemoteApplication({
|
|
||||||
applicationId: job.data.applicationId,
|
|
||||||
titleLog: job.data.titleLog,
|
|
||||||
descriptionLog: job.data.descriptionLog,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (job.data.type === "redeploy") {
|
|
||||||
await rebuildApplication({
|
|
||||||
applicationId: job.data.applicationId,
|
|
||||||
titleLog: job.data.titleLog,
|
|
||||||
descriptionLog: job.data.descriptionLog,
|
|
||||||
});
|
|
||||||
} else if (job.data.type === "deploy") {
|
|
||||||
await deployApplication({
|
|
||||||
applicationId: job.data.applicationId,
|
|
||||||
titleLog: job.data.titleLog,
|
|
||||||
descriptionLog: job.data.descriptionLog,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (job.data.applicationType === "compose") {
|
|
||||||
await updateCompose(job.data.composeId, {
|
|
||||||
composeStatus: "running",
|
|
||||||
});
|
|
||||||
|
|
||||||
if (job.data.server) {
|
|
||||||
if (job.data.type === "redeploy") {
|
|
||||||
await rebuildRemoteCompose({
|
|
||||||
composeId: job.data.composeId,
|
|
||||||
titleLog: job.data.titleLog,
|
|
||||||
descriptionLog: job.data.descriptionLog,
|
|
||||||
});
|
|
||||||
} else if (job.data.type === "deploy") {
|
|
||||||
await deployRemoteCompose({
|
|
||||||
composeId: job.data.composeId,
|
|
||||||
titleLog: job.data.titleLog,
|
|
||||||
descriptionLog: job.data.descriptionLog,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (job.data.type === "deploy") {
|
|
||||||
await deployCompose({
|
|
||||||
composeId: job.data.composeId,
|
|
||||||
titleLog: job.data.titleLog,
|
|
||||||
descriptionLog: job.data.descriptionLog,
|
|
||||||
});
|
|
||||||
} else if (job.data.type === "redeploy") {
|
|
||||||
await rebuildCompose({
|
|
||||||
composeId: job.data.composeId,
|
|
||||||
titleLog: job.data.titleLog,
|
|
||||||
descriptionLog: job.data.descriptionLog,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (job.data.applicationType === "application-preview") {
|
|
||||||
await updatePreviewDeployment(job.data.previewDeploymentId, {
|
|
||||||
previewStatus: "running",
|
|
||||||
});
|
|
||||||
if (job.data.server) {
|
|
||||||
if (job.data.type === "deploy") {
|
|
||||||
await deployRemotePreviewApplication({
|
|
||||||
applicationId: job.data.applicationId,
|
|
||||||
titleLog: job.data.titleLog,
|
|
||||||
descriptionLog: job.data.descriptionLog,
|
|
||||||
previewDeploymentId: job.data.previewDeploymentId,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (job.data.type === "deploy") {
|
|
||||||
await deployPreviewApplication({
|
|
||||||
applicationId: job.data.applicationId,
|
|
||||||
titleLog: job.data.titleLog,
|
|
||||||
descriptionLog: job.data.descriptionLog,
|
|
||||||
previewDeploymentId: job.data.previewDeploymentId,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.log("Error", error);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
{
|
close: async () => {
|
||||||
autorun: false,
|
console.log("Legacy deploymentWorker.close() called");
|
||||||
connection: redisConfig,
|
return Promise.resolve();
|
||||||
},
|
},
|
||||||
);
|
};
|
||||||
|
|
||||||
|
// Legacy exports for backward compatibility
|
||||||
|
export const getWorkersMap = () => {
|
||||||
|
console.warn(
|
||||||
|
"getWorkersMap() is deprecated - use serviceQueueManager instead",
|
||||||
|
);
|
||||||
|
return {};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getWorker = (_serverId?: string) => {
|
||||||
|
console.warn("getWorker() is deprecated - use serviceQueueManager instead");
|
||||||
|
return undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const createDeploymentWorker = (defaultConcurrency = 1) => {
|
||||||
|
console.warn(
|
||||||
|
"createDeploymentWorker() is deprecated - use serviceQueueManager instead",
|
||||||
|
);
|
||||||
|
serviceQueueManager.setGlobalConcurrency(defaultConcurrency);
|
||||||
|
return deploymentWorker;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const createServerDeploymentWorker = (
|
||||||
|
_serverId: string,
|
||||||
|
_concurrency = 1,
|
||||||
|
) => {
|
||||||
|
console.warn(
|
||||||
|
"createServerDeploymentWorker() is deprecated - use serviceQueueManager instead",
|
||||||
|
);
|
||||||
|
// The new system automatically creates queues per service, no need for explicit worker creation
|
||||||
|
return deploymentWorker;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const removeServerDeploymentWorker = (serverId: string) => {
|
||||||
|
console.warn(
|
||||||
|
"removeServerDeploymentWorker() is deprecated - use removeServiceQueue instead",
|
||||||
|
);
|
||||||
|
serviceQueueManager.removeServiceQueue(serverId);
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,44 +1,101 @@
|
|||||||
import { Queue } from "bullmq";
|
import type { DeploymentJob } from "./queue-types";
|
||||||
import { redisConfig } from "./redis-connection";
|
import {
|
||||||
|
addDeploymentJob,
|
||||||
|
cancelDeploymentJobs,
|
||||||
|
getDeploymentQueueStatus,
|
||||||
|
setGlobalConcurrency,
|
||||||
|
} from "./service-queue";
|
||||||
|
|
||||||
const myQueue = new Queue("deployments", {
|
// Default queue name for local deployments
|
||||||
connection: redisConfig,
|
export const DEFAULT_QUEUE = "default";
|
||||||
});
|
|
||||||
|
|
||||||
process.on("SIGTERM", () => {
|
// Initialize with default concurrency of 3 services
|
||||||
myQueue.close();
|
setGlobalConcurrency(3);
|
||||||
process.exit(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
myQueue.on("error", (error) => {
|
// Helper function to determine service ID from job data
|
||||||
if ((error as any).code === "ECONNREFUSED") {
|
// Groups deployments by SERVER, not by individual application/compose
|
||||||
console.error(
|
const getServiceId = (jobData: DeploymentJob): string => {
|
||||||
"Make sure you have installed Redis and it is running.",
|
// If it has a serverId, group by that server
|
||||||
error,
|
if (jobData.serverId) {
|
||||||
);
|
return jobData.serverId;
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
// For local deployments (no serverId), group all under the main Dokploy server
|
||||||
|
return "dokploy-server";
|
||||||
|
};
|
||||||
|
|
||||||
|
// Compatibility functions to replace BullMQ usage
|
||||||
|
export const myQueue = {
|
||||||
|
add: async (
|
||||||
|
_name: string,
|
||||||
|
jobData: DeploymentJob,
|
||||||
|
_options?: any,
|
||||||
|
userId?: string,
|
||||||
|
) => {
|
||||||
|
const serviceId = getServiceId(jobData);
|
||||||
|
const jobId = await addDeploymentJob(serviceId, jobData, userId);
|
||||||
|
console.log(`Added deployment job ${jobId} to service ${serviceId}`);
|
||||||
|
return { id: jobId };
|
||||||
|
},
|
||||||
|
|
||||||
|
close: () => {
|
||||||
|
console.log("Service queue manager shutdown initiated");
|
||||||
|
return Promise.resolve();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
export const cleanQueuesByApplication = async (applicationId: string) => {
|
export const cleanQueuesByApplication = async (applicationId: string) => {
|
||||||
const jobs = await myQueue.getJobs(["waiting", "delayed"]);
|
// Cancel jobs for this specific application across all servers
|
||||||
|
let totalCancelled = 0;
|
||||||
|
|
||||||
for (const job of jobs) {
|
// Check the local Dokploy server
|
||||||
if (job?.data?.applicationId === applicationId) {
|
const localCancelled = cancelDeploymentJobs(
|
||||||
await job.remove();
|
"dokploy-server",
|
||||||
console.log(`Removed job ${job.id} for application ${applicationId}`);
|
applicationId,
|
||||||
}
|
undefined,
|
||||||
}
|
);
|
||||||
|
totalCancelled += localCancelled;
|
||||||
|
|
||||||
|
// TODO: Also check remote servers if we need to track which servers have this application
|
||||||
|
// For now, we only clean from the local server queue
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`Cancelled ${totalCancelled} jobs for application ${applicationId}`,
|
||||||
|
);
|
||||||
|
return totalCancelled;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const cleanQueuesByCompose = async (composeId: string) => {
|
export const cleanQueuesByCompose = async (composeId: string) => {
|
||||||
const jobs = await myQueue.getJobs(["waiting", "delayed"]);
|
// Cancel jobs for this specific compose across all servers
|
||||||
|
let totalCancelled = 0;
|
||||||
|
|
||||||
for (const job of jobs) {
|
// Check the local Dokploy server
|
||||||
if (job?.data?.composeId === composeId) {
|
const localCancelled = cancelDeploymentJobs(
|
||||||
await job.remove();
|
"dokploy-server",
|
||||||
console.log(`Removed job ${job.id} for compose ${composeId}`);
|
undefined,
|
||||||
}
|
composeId,
|
||||||
}
|
);
|
||||||
|
totalCancelled += localCancelled;
|
||||||
|
|
||||||
|
// TODO: Also check remote servers if we need to track which servers have this compose
|
||||||
|
// For now, we only clean from the local server queue
|
||||||
|
|
||||||
|
console.log(`Cancelled ${totalCancelled} jobs for compose ${composeId}`);
|
||||||
|
return totalCancelled;
|
||||||
};
|
};
|
||||||
|
|
||||||
export { myQueue };
|
// Export queue status for monitoring
|
||||||
|
export const getQueueStatus = getDeploymentQueueStatus;
|
||||||
|
|
||||||
|
// New function to add jobs with user context (for API routes)
|
||||||
|
export const addJobWithUserContext = async (
|
||||||
|
jobData: DeploymentJob,
|
||||||
|
userId?: string,
|
||||||
|
): Promise<{ id: string }> => {
|
||||||
|
const serviceId = getServiceId(jobData);
|
||||||
|
const jobId = await addDeploymentJob(serviceId, jobData, userId);
|
||||||
|
console.log(
|
||||||
|
`Added deployment job ${jobId} to service ${serviceId} with user context ${userId || "none"}`,
|
||||||
|
);
|
||||||
|
return { id: jobId };
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,8 +0,0 @@
|
|||||||
import type { ConnectionOptions } from "bullmq";
|
|
||||||
|
|
||||||
export const redisConfig: ConnectionOptions = {
|
|
||||||
host:
|
|
||||||
process.env.NODE_ENV === "production"
|
|
||||||
? process.env.REDIS_HOST || "dokploy-redis"
|
|
||||||
: "127.0.0.1",
|
|
||||||
};
|
|
||||||
500
apps/dokploy/server/queues/service-queue.ts
Normal file
500
apps/dokploy/server/queues/service-queue.ts
Normal file
@@ -0,0 +1,500 @@
|
|||||||
|
import {
|
||||||
|
deployApplication,
|
||||||
|
deployCompose,
|
||||||
|
deployPreviewApplication,
|
||||||
|
deployRemoteApplication,
|
||||||
|
deployRemoteCompose,
|
||||||
|
deployRemotePreviewApplication,
|
||||||
|
findServerById,
|
||||||
|
rebuildApplication,
|
||||||
|
rebuildCompose,
|
||||||
|
rebuildRemoteApplication,
|
||||||
|
rebuildRemoteCompose,
|
||||||
|
updateApplicationStatus,
|
||||||
|
updateCompose,
|
||||||
|
updatePreviewDeployment,
|
||||||
|
} from "@dokploy/server";
|
||||||
|
import { db } from "@dokploy/server/db";
|
||||||
|
import { users_temp } from "@dokploy/server/db/schema";
|
||||||
|
import { eq } from "drizzle-orm";
|
||||||
|
import pLimit from "p-limit";
|
||||||
|
import type { DeploymentJob } from "./queue-types";
|
||||||
|
|
||||||
|
// Types for our p-limit based queue system
|
||||||
|
export interface QueueJob {
|
||||||
|
id: string;
|
||||||
|
data: DeploymentJob;
|
||||||
|
createdAt: Date;
|
||||||
|
status: "waiting" | "processing" | "completed" | "failed" | "cancelled";
|
||||||
|
abortController: AbortController;
|
||||||
|
promise?: Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ServiceQueue {
|
||||||
|
serviceId: string;
|
||||||
|
jobs: QueueJob[];
|
||||||
|
limit: ReturnType<typeof pLimit>; // p-limit instance with concurrency 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// Global queue management using p-limit
|
||||||
|
class ServiceQueueManager {
|
||||||
|
private queues: Map<string, ServiceQueue> = new Map();
|
||||||
|
private globalLimit: ReturnType<typeof pLimit>;
|
||||||
|
private isShuttingDown = false;
|
||||||
|
|
||||||
|
constructor(globalConcurrency = 3) {
|
||||||
|
// Global limit controls how many services can deploy simultaneously
|
||||||
|
this.globalLimit = pLimit(globalConcurrency);
|
||||||
|
this.setupShutdownHandlers();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set global concurrency (how many services can deploy simultaneously)
|
||||||
|
setGlobalConcurrency(concurrency: number) {
|
||||||
|
this.globalLimit = pLimit(concurrency);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get concurrency settings from database
|
||||||
|
private async getConcurrencySettings(jobData: DeploymentJob): Promise<{
|
||||||
|
serviceConcurrency: number;
|
||||||
|
}> {
|
||||||
|
try {
|
||||||
|
// Default: Each service processes 1 deployment at a time (FIFO within service)
|
||||||
|
let serviceConcurrency = 1;
|
||||||
|
|
||||||
|
// If it's a server deployment, get server-specific concurrency
|
||||||
|
// This controls how many deployments can run simultaneously ON THAT SERVER
|
||||||
|
if (jobData.serverId) {
|
||||||
|
try {
|
||||||
|
const serverData = await findServerById(jobData.serverId);
|
||||||
|
serviceConcurrency = serverData.concurrency || 1;
|
||||||
|
console.log(
|
||||||
|
`Server ${jobData.serverId} can handle ${serviceConcurrency} concurrent deployments`,
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(
|
||||||
|
`Could not get server concurrency for ${jobData.serverId}, using default: 1`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
serviceConcurrency,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(
|
||||||
|
"Error getting concurrency settings, using defaults:",
|
||||||
|
error,
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
serviceConcurrency: 1,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get or create a queue for a service with dynamic concurrency
|
||||||
|
private async getOrCreateQueue(
|
||||||
|
serviceId: string,
|
||||||
|
jobData?: DeploymentJob,
|
||||||
|
): Promise<ServiceQueue> {
|
||||||
|
if (!this.queues.has(serviceId)) {
|
||||||
|
let serviceConcurrency = 1; // Default
|
||||||
|
|
||||||
|
// Get concurrency from database if we have job data
|
||||||
|
if (jobData) {
|
||||||
|
const settings = await this.getConcurrencySettings(jobData);
|
||||||
|
serviceConcurrency = settings.serviceConcurrency;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.queues.set(serviceId, {
|
||||||
|
serviceId,
|
||||||
|
jobs: [],
|
||||||
|
// Service concurrency from database or default to 1
|
||||||
|
limit: pLimit(serviceConcurrency),
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`Created queue for service ${serviceId} with concurrency: ${serviceConcurrency}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return this.queues.get(serviceId)!;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add a job to a service queue
|
||||||
|
async addJob(
|
||||||
|
serviceId: string,
|
||||||
|
jobData: DeploymentJob,
|
||||||
|
userId?: string,
|
||||||
|
): Promise<string> {
|
||||||
|
if (this.isShuttingDown) {
|
||||||
|
throw new Error("Queue manager is shutting down");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update global concurrency based on user settings if provided
|
||||||
|
// This controls the TOTAL number of deployments across ALL services for this user
|
||||||
|
if (userId) {
|
||||||
|
try {
|
||||||
|
const userData = await db.query.users_temp.findFirst({
|
||||||
|
where: eq(users_temp.id, userId),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (userData?.serverConcurrency) {
|
||||||
|
// This is GLOBAL concurrency - total deployments across all services
|
||||||
|
this.globalLimit = pLimit(userData.serverConcurrency);
|
||||||
|
console.log(
|
||||||
|
`Set GLOBAL concurrency to ${userData.serverConcurrency} deployments total for user ${userId}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(
|
||||||
|
`Could not get user concurrency settings for ${userId}:`,
|
||||||
|
error,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const queue = await this.getOrCreateQueue(serviceId, jobData);
|
||||||
|
const jobId = `${serviceId}-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
||||||
|
|
||||||
|
const job: QueueJob = {
|
||||||
|
id: jobId,
|
||||||
|
data: jobData,
|
||||||
|
createdAt: new Date(),
|
||||||
|
status: "waiting",
|
||||||
|
abortController: new AbortController(),
|
||||||
|
};
|
||||||
|
|
||||||
|
queue.jobs.push(job);
|
||||||
|
console.log(
|
||||||
|
`Added job ${jobId} to service ${serviceId} queue. Queue length: ${queue.jobs.length}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Start processing the job using p-limit
|
||||||
|
this.processJob(queue, job);
|
||||||
|
|
||||||
|
return jobId;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process a job using both global and service-level p-limit
|
||||||
|
private processJob(queue: ServiceQueue, job: QueueJob) {
|
||||||
|
// Use global limit to control cross-service concurrency
|
||||||
|
job.promise = this.globalLimit(() =>
|
||||||
|
// Use service limit to ensure ordered processing within service
|
||||||
|
queue.limit(async () => {
|
||||||
|
if (job.status === "cancelled" || this.isShuttingDown) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
job.status = "processing";
|
||||||
|
console.log(`Processing job ${job.id} for service ${queue.serviceId}`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.executeJob(job);
|
||||||
|
job.status = "completed";
|
||||||
|
console.log(`Completed job ${job.id} for service ${queue.serviceId}`);
|
||||||
|
} catch (error) {
|
||||||
|
if (job.abortController.signal.aborted) {
|
||||||
|
job.status = "cancelled";
|
||||||
|
console.log(
|
||||||
|
`Job ${job.id} was cancelled for service ${queue.serviceId}`,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
job.status = "failed";
|
||||||
|
console.error(
|
||||||
|
`Job ${job.id} failed for service ${queue.serviceId}:`,
|
||||||
|
error,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
// Clean up completed/failed jobs after a delay
|
||||||
|
setTimeout(() => {
|
||||||
|
queue.jobs = queue.jobs.filter((j) => j.id !== job.id);
|
||||||
|
}, 5000);
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove/cancel jobs for a specific service
|
||||||
|
cancelJobsByService(
|
||||||
|
serviceId: string,
|
||||||
|
applicationId?: string,
|
||||||
|
composeId?: string,
|
||||||
|
): number {
|
||||||
|
const queue = this.queues.get(serviceId);
|
||||||
|
if (!queue) return 0;
|
||||||
|
|
||||||
|
let cancelledCount = 0;
|
||||||
|
|
||||||
|
// Cancel waiting and processing jobs
|
||||||
|
for (const job of queue.jobs) {
|
||||||
|
if (job.status === "waiting" || job.status === "processing") {
|
||||||
|
// Check if this job matches the filter criteria
|
||||||
|
const matchesApplication = applicationId
|
||||||
|
? (job.data.applicationType === "application" ||
|
||||||
|
job.data.applicationType === "application-preview") &&
|
||||||
|
job.data.applicationId === applicationId
|
||||||
|
: true;
|
||||||
|
const matchesCompose = composeId
|
||||||
|
? job.data.applicationType === "compose" &&
|
||||||
|
job.data.composeId === composeId
|
||||||
|
: true;
|
||||||
|
|
||||||
|
if (matchesApplication && matchesCompose) {
|
||||||
|
job.status = "cancelled";
|
||||||
|
job.abortController.abort();
|
||||||
|
cancelledCount++;
|
||||||
|
console.log(`Cancelled job ${job.id} for service ${serviceId}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove cancelled jobs from queue immediately
|
||||||
|
queue.jobs = queue.jobs.filter((job) => job.status !== "cancelled");
|
||||||
|
|
||||||
|
return cancelledCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get queue status for a service
|
||||||
|
getQueueStatus(serviceId: string) {
|
||||||
|
const queue = this.queues.get(serviceId);
|
||||||
|
if (!queue) return null;
|
||||||
|
|
||||||
|
return {
|
||||||
|
serviceId,
|
||||||
|
totalJobs: queue.jobs.length,
|
||||||
|
waitingJobs: queue.jobs.filter((j) => j.status === "waiting").length,
|
||||||
|
processingJobs: queue.jobs.filter((j) => j.status === "processing")
|
||||||
|
.length,
|
||||||
|
completedJobs: queue.jobs.filter((j) => j.status === "completed").length,
|
||||||
|
failedJobs: queue.jobs.filter((j) => j.status === "failed").length,
|
||||||
|
// p-limit queue status
|
||||||
|
activeCount: queue.limit.activeCount,
|
||||||
|
pendingCount: queue.limit.pendingCount,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get all queues status
|
||||||
|
getAllQueuesStatus() {
|
||||||
|
const status: Record<string, any> = {};
|
||||||
|
for (const [serviceId] of this.queues) {
|
||||||
|
status[serviceId] = this.getQueueStatus(serviceId);
|
||||||
|
}
|
||||||
|
status.global = {
|
||||||
|
activeCount: this.globalLimit.activeCount,
|
||||||
|
pendingCount: this.globalLimit.pendingCount,
|
||||||
|
concurrency: this.globalLimit.concurrency,
|
||||||
|
};
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear pending jobs from a service queue using p-limit's clearQueue
|
||||||
|
clearServiceQueue(serviceId: string) {
|
||||||
|
const queue = this.queues.get(serviceId);
|
||||||
|
if (queue) {
|
||||||
|
// Cancel all waiting jobs
|
||||||
|
for (const job of queue.jobs) {
|
||||||
|
if (job.status === "waiting") {
|
||||||
|
job.status = "cancelled";
|
||||||
|
job.abortController.abort();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear p-limit's internal queue
|
||||||
|
queue.limit.clearQueue();
|
||||||
|
|
||||||
|
// Remove cancelled jobs
|
||||||
|
queue.jobs = queue.jobs.filter((job) => job.status !== "cancelled");
|
||||||
|
|
||||||
|
console.log(`Cleared service queue for ${serviceId}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async executeJob(job: QueueJob): Promise<void> {
|
||||||
|
const { data } = job;
|
||||||
|
|
||||||
|
// Check if job was cancelled before execution
|
||||||
|
if (job.abortController.signal.aborted) {
|
||||||
|
throw new Error("Job was cancelled");
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (data.applicationType === "application") {
|
||||||
|
await updateApplicationStatus(data.applicationId, "running");
|
||||||
|
|
||||||
|
if (data.server) {
|
||||||
|
if (data.type === "redeploy") {
|
||||||
|
await rebuildRemoteApplication({
|
||||||
|
applicationId: data.applicationId,
|
||||||
|
titleLog: data.titleLog,
|
||||||
|
descriptionLog: data.descriptionLog,
|
||||||
|
});
|
||||||
|
} else if (data.type === "deploy") {
|
||||||
|
await deployRemoteApplication({
|
||||||
|
applicationId: data.applicationId,
|
||||||
|
titleLog: data.titleLog,
|
||||||
|
descriptionLog: data.descriptionLog,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (data.type === "redeploy") {
|
||||||
|
await rebuildApplication({
|
||||||
|
applicationId: data.applicationId,
|
||||||
|
titleLog: data.titleLog,
|
||||||
|
descriptionLog: data.descriptionLog,
|
||||||
|
});
|
||||||
|
} else if (data.type === "deploy") {
|
||||||
|
await deployApplication({
|
||||||
|
applicationId: data.applicationId,
|
||||||
|
titleLog: data.titleLog,
|
||||||
|
descriptionLog: data.descriptionLog,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (data.applicationType === "compose") {
|
||||||
|
await updateCompose(data.composeId, {
|
||||||
|
composeStatus: "running",
|
||||||
|
});
|
||||||
|
|
||||||
|
if (data.server) {
|
||||||
|
if (data.type === "redeploy") {
|
||||||
|
await rebuildRemoteCompose({
|
||||||
|
composeId: data.composeId,
|
||||||
|
titleLog: data.titleLog,
|
||||||
|
descriptionLog: data.descriptionLog,
|
||||||
|
});
|
||||||
|
} else if (data.type === "deploy") {
|
||||||
|
await deployRemoteCompose({
|
||||||
|
composeId: data.composeId,
|
||||||
|
titleLog: data.titleLog,
|
||||||
|
descriptionLog: data.descriptionLog,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (data.type === "deploy") {
|
||||||
|
await deployCompose({
|
||||||
|
composeId: data.composeId,
|
||||||
|
titleLog: data.titleLog,
|
||||||
|
descriptionLog: data.descriptionLog,
|
||||||
|
});
|
||||||
|
} else if (data.type === "redeploy") {
|
||||||
|
await rebuildCompose({
|
||||||
|
composeId: data.composeId,
|
||||||
|
titleLog: data.titleLog,
|
||||||
|
descriptionLog: data.descriptionLog,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (data.applicationType === "application-preview") {
|
||||||
|
await updatePreviewDeployment(data.previewDeploymentId, {
|
||||||
|
previewStatus: "running",
|
||||||
|
});
|
||||||
|
if (data.server) {
|
||||||
|
if (data.type === "deploy") {
|
||||||
|
await deployRemotePreviewApplication({
|
||||||
|
applicationId: data.applicationId,
|
||||||
|
titleLog: data.titleLog,
|
||||||
|
descriptionLog: data.descriptionLog,
|
||||||
|
previewDeploymentId: data.previewDeploymentId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (data.type === "deploy") {
|
||||||
|
await deployPreviewApplication({
|
||||||
|
applicationId: data.applicationId,
|
||||||
|
titleLog: data.titleLog,
|
||||||
|
descriptionLog: data.descriptionLog,
|
||||||
|
previewDeploymentId: data.previewDeploymentId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.log("Deployment Error", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private setupShutdownHandlers() {
|
||||||
|
const gracefulShutdown = async () => {
|
||||||
|
console.log("Shutting down service queue manager...");
|
||||||
|
this.isShuttingDown = true;
|
||||||
|
|
||||||
|
// Cancel all jobs
|
||||||
|
for (const queue of this.queues.values()) {
|
||||||
|
for (const job of queue.jobs) {
|
||||||
|
job.abortController.abort();
|
||||||
|
}
|
||||||
|
// Clear p-limit queues
|
||||||
|
queue.limit.clearQueue();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear global queue
|
||||||
|
this.globalLimit.clearQueue();
|
||||||
|
|
||||||
|
// Wait a bit for jobs to finish cancelling
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
||||||
|
process.exit(0);
|
||||||
|
};
|
||||||
|
|
||||||
|
process.on("SIGTERM", gracefulShutdown);
|
||||||
|
process.on("SIGINT", gracefulShutdown);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove a specific service queue entirely
|
||||||
|
removeServiceQueue(serviceId: string) {
|
||||||
|
const queue = this.queues.get(serviceId);
|
||||||
|
if (queue) {
|
||||||
|
// Cancel all jobs in the queue
|
||||||
|
for (const job of queue.jobs) {
|
||||||
|
job.abortController.abort();
|
||||||
|
}
|
||||||
|
// Clear p-limit queue
|
||||||
|
queue.limit.clearQueue();
|
||||||
|
this.queues.delete(serviceId);
|
||||||
|
console.log(`Removed service queue for ${serviceId}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Global instance
|
||||||
|
export const serviceQueueManager = new ServiceQueueManager();
|
||||||
|
|
||||||
|
// Helper functions to maintain compatibility with existing code
|
||||||
|
export const addDeploymentJob = async (
|
||||||
|
serviceId: string,
|
||||||
|
jobData: DeploymentJob,
|
||||||
|
userId?: string,
|
||||||
|
): Promise<string> => {
|
||||||
|
return await serviceQueueManager.addJob(serviceId, jobData, userId);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const cancelDeploymentJobs = (
|
||||||
|
serviceId: string,
|
||||||
|
applicationId?: string,
|
||||||
|
composeId?: string,
|
||||||
|
): number => {
|
||||||
|
return serviceQueueManager.cancelJobsByService(
|
||||||
|
serviceId,
|
||||||
|
applicationId,
|
||||||
|
composeId,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getDeploymentQueueStatus = (serviceId?: string) => {
|
||||||
|
if (serviceId) {
|
||||||
|
return serviceQueueManager.getQueueStatus(serviceId);
|
||||||
|
}
|
||||||
|
return serviceQueueManager.getAllQueuesStatus();
|
||||||
|
};
|
||||||
|
|
||||||
|
export const setGlobalConcurrency = (concurrency: number) => {
|
||||||
|
serviceQueueManager.setGlobalConcurrency(concurrency);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const removeServiceQueue = (serviceId: string) => {
|
||||||
|
serviceQueueManager.removeServiceQueue(serviceId);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const clearServiceQueue = (serviceId: string) => {
|
||||||
|
serviceQueueManager.clearServiceQueue(serviceId);
|
||||||
|
};
|
||||||
@@ -23,30 +23,3 @@ export const deploy = async (jobData: DeploymentJob) => {
|
|||||||
throw error;
|
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";
|
} from "./queue.js";
|
||||||
import { jobQueueSchema } from "./schema.js";
|
import { jobQueueSchema } from "./schema.js";
|
||||||
import { initializeJobs } from "./utils.js";
|
import { initializeJobs } from "./utils.js";
|
||||||
import { firstWorker, secondWorker, thirdWorker } from "./workers.js";
|
import { firstWorker, secondWorker } from "./workers.js";
|
||||||
|
|
||||||
const app = new Hono();
|
const app = new Hono();
|
||||||
|
|
||||||
@@ -91,7 +91,6 @@ export const gracefulShutdown = async (signal: string) => {
|
|||||||
logger.warn(`Received ${signal}, closing server...`);
|
logger.warn(`Received ${signal}, closing server...`);
|
||||||
await firstWorker.close();
|
await firstWorker.close();
|
||||||
await secondWorker.close();
|
await secondWorker.close();
|
||||||
await thirdWorker.close();
|
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -7,34 +7,22 @@ import { runJobs } from "./utils.js";
|
|||||||
export const firstWorker = new Worker(
|
export const firstWorker = new Worker(
|
||||||
"backupQueue",
|
"backupQueue",
|
||||||
async (job: Job<QueueJob>) => {
|
async (job: Job<QueueJob>) => {
|
||||||
logger.info({ data: job.data }, "Running job first worker");
|
logger.info({ data: job.data }, "Running job");
|
||||||
await runJobs(job.data);
|
await runJobs(job.data);
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
concurrency: 100,
|
concurrency: 50,
|
||||||
connection,
|
connection,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
export const secondWorker = new Worker(
|
export const secondWorker = new Worker(
|
||||||
"backupQueue",
|
"backupQueue",
|
||||||
async (job: Job<QueueJob>) => {
|
async (job: Job<QueueJob>) => {
|
||||||
logger.info({ data: job.data }, "Running job second worker");
|
logger.info({ data: job.data }, "Running job");
|
||||||
await runJobs(job.data);
|
await runJobs(job.data);
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
concurrency: 100,
|
concurrency: 50,
|
||||||
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,
|
|
||||||
connection,
|
connection,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -112,10 +112,6 @@ export const member = pgTable("member", {
|
|||||||
.array()
|
.array()
|
||||||
.notNull()
|
.notNull()
|
||||||
.default(sql`ARRAY[]::text[]`),
|
.default(sql`ARRAY[]::text[]`),
|
||||||
accessedEnvironments: text("accessedEnvironments")
|
|
||||||
.array()
|
|
||||||
.notNull()
|
|
||||||
.default(sql`ARRAY[]::text[]`),
|
|
||||||
accessedServices: text("accesedServices")
|
accessedServices: text("accesedServices")
|
||||||
.array()
|
.array()
|
||||||
.notNull()
|
.notNull()
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ export const apiUpdateAi = createSchema
|
|||||||
.omit({ organizationId: true });
|
.omit({ organizationId: true });
|
||||||
|
|
||||||
export const deploySuggestionSchema = z.object({
|
export const deploySuggestionSchema = z.object({
|
||||||
environmentId: z.string().min(1),
|
projectId: z.string().min(1),
|
||||||
id: z.string().min(1),
|
id: z.string().min(1),
|
||||||
dockerCompose: z.string().min(1),
|
dockerCompose: z.string().min(1),
|
||||||
envVariables: z.string(),
|
envVariables: z.string(),
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ import { z } from "zod";
|
|||||||
import { bitbucket } from "./bitbucket";
|
import { bitbucket } from "./bitbucket";
|
||||||
import { deployments } from "./deployment";
|
import { deployments } from "./deployment";
|
||||||
import { domains } from "./domain";
|
import { domains } from "./domain";
|
||||||
import { environments } from "./environment";
|
|
||||||
import { gitea } from "./gitea";
|
import { gitea } from "./gitea";
|
||||||
import { github } from "./github";
|
import { github } from "./github";
|
||||||
import { gitlab } from "./gitlab";
|
import { gitlab } from "./gitlab";
|
||||||
@@ -180,9 +179,9 @@ export const applications = pgTable("application", {
|
|||||||
registryId: text("registryId").references(() => registry.registryId, {
|
registryId: text("registryId").references(() => registry.registryId, {
|
||||||
onDelete: "set null",
|
onDelete: "set null",
|
||||||
}),
|
}),
|
||||||
environmentId: text("environmentId")
|
projectId: text("projectId")
|
||||||
.notNull()
|
.notNull()
|
||||||
.references(() => environments.environmentId, { onDelete: "cascade" }),
|
.references(() => projects.projectId, { onDelete: "cascade" }),
|
||||||
githubId: text("githubId").references(() => github.githubId, {
|
githubId: text("githubId").references(() => github.githubId, {
|
||||||
onDelete: "set null",
|
onDelete: "set null",
|
||||||
}),
|
}),
|
||||||
@@ -203,9 +202,9 @@ export const applications = pgTable("application", {
|
|||||||
export const applicationsRelations = relations(
|
export const applicationsRelations = relations(
|
||||||
applications,
|
applications,
|
||||||
({ one, many }) => ({
|
({ one, many }) => ({
|
||||||
environment: one(environments, {
|
project: one(projects, {
|
||||||
fields: [applications.environmentId],
|
fields: [applications.projectId],
|
||||||
references: [environments.environmentId],
|
references: [projects.projectId],
|
||||||
}),
|
}),
|
||||||
deployments: many(deployments),
|
deployments: many(deployments),
|
||||||
customGitSSHKey: one(sshKeys, {
|
customGitSSHKey: one(sshKeys, {
|
||||||
@@ -274,7 +273,7 @@ const createSchema = createInsertSchema(applications, {
|
|||||||
customGitBuildPath: z.string().optional(),
|
customGitBuildPath: z.string().optional(),
|
||||||
customGitUrl: z.string().optional(),
|
customGitUrl: z.string().optional(),
|
||||||
buildPath: z.string().optional(),
|
buildPath: z.string().optional(),
|
||||||
environmentId: z.string(),
|
projectId: z.string(),
|
||||||
sourceType: z
|
sourceType: z
|
||||||
.enum(["github", "docker", "git", "gitlab", "bitbucket", "gitea", "drop"])
|
.enum(["github", "docker", "git", "gitlab", "bitbucket", "gitea", "drop"])
|
||||||
.optional(),
|
.optional(),
|
||||||
@@ -318,7 +317,7 @@ export const apiCreateApplication = createSchema.pick({
|
|||||||
name: true,
|
name: true,
|
||||||
appName: true,
|
appName: true,
|
||||||
description: true,
|
description: true,
|
||||||
environmentId: true,
|
projectId: true,
|
||||||
serverId: true,
|
serverId: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -328,26 +327,6 @@ export const apiFindOneApplication = createSchema
|
|||||||
})
|
})
|
||||||
.required();
|
.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
|
export const apiReloadApplication = createSchema
|
||||||
.pick({
|
.pick({
|
||||||
appName: true,
|
appName: true,
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import { backups } from "./backups";
|
|||||||
import { bitbucket } from "./bitbucket";
|
import { bitbucket } from "./bitbucket";
|
||||||
import { deployments } from "./deployment";
|
import { deployments } from "./deployment";
|
||||||
import { domains } from "./domain";
|
import { domains } from "./domain";
|
||||||
import { environments } from "./environment";
|
|
||||||
import { gitea } from "./gitea";
|
import { gitea } from "./gitea";
|
||||||
import { github } from "./github";
|
import { github } from "./github";
|
||||||
import { gitlab } from "./gitlab";
|
import { gitlab } from "./gitlab";
|
||||||
@@ -85,9 +84,9 @@ export const compose = pgTable("compose", {
|
|||||||
.default(false),
|
.default(false),
|
||||||
triggerType: triggerType("triggerType").default("push"),
|
triggerType: triggerType("triggerType").default("push"),
|
||||||
composeStatus: applicationStatus("composeStatus").notNull().default("idle"),
|
composeStatus: applicationStatus("composeStatus").notNull().default("idle"),
|
||||||
environmentId: text("environmentId")
|
projectId: text("projectId")
|
||||||
.notNull()
|
.notNull()
|
||||||
.references(() => environments.environmentId, { onDelete: "cascade" }),
|
.references(() => projects.projectId, { onDelete: "cascade" }),
|
||||||
createdAt: text("createdAt")
|
createdAt: text("createdAt")
|
||||||
.notNull()
|
.notNull()
|
||||||
.$defaultFn(() => new Date().toISOString()),
|
.$defaultFn(() => new Date().toISOString()),
|
||||||
@@ -110,9 +109,9 @@ export const compose = pgTable("compose", {
|
|||||||
});
|
});
|
||||||
|
|
||||||
export const composeRelations = relations(compose, ({ one, many }) => ({
|
export const composeRelations = relations(compose, ({ one, many }) => ({
|
||||||
environment: one(environments, {
|
project: one(projects, {
|
||||||
fields: [compose.environmentId],
|
fields: [compose.projectId],
|
||||||
references: [environments.environmentId],
|
references: [projects.projectId],
|
||||||
}),
|
}),
|
||||||
deployments: many(deployments),
|
deployments: many(deployments),
|
||||||
mounts: many(mounts),
|
mounts: many(mounts),
|
||||||
@@ -150,7 +149,7 @@ const createSchema = createInsertSchema(compose, {
|
|||||||
description: z.string(),
|
description: z.string(),
|
||||||
env: z.string().optional(),
|
env: z.string().optional(),
|
||||||
composeFile: z.string().optional(),
|
composeFile: z.string().optional(),
|
||||||
environmentId: z.string(),
|
projectId: z.string(),
|
||||||
customGitSSHKeyId: z.string().optional(),
|
customGitSSHKeyId: z.string().optional(),
|
||||||
command: z.string().optional(),
|
command: z.string().optional(),
|
||||||
composePath: z.string().min(1),
|
composePath: z.string().min(1),
|
||||||
@@ -161,7 +160,7 @@ const createSchema = createInsertSchema(compose, {
|
|||||||
export const apiCreateCompose = createSchema.pick({
|
export const apiCreateCompose = createSchema.pick({
|
||||||
name: true,
|
name: true,
|
||||||
description: true,
|
description: true,
|
||||||
environmentId: true,
|
projectId: true,
|
||||||
composeType: true,
|
composeType: true,
|
||||||
appName: true,
|
appName: true,
|
||||||
serverId: true,
|
serverId: true,
|
||||||
@@ -170,7 +169,7 @@ export const apiCreateCompose = createSchema.pick({
|
|||||||
|
|
||||||
export const apiCreateComposeByTemplate = createSchema
|
export const apiCreateComposeByTemplate = createSchema
|
||||||
.pick({
|
.pick({
|
||||||
environmentId: true,
|
projectId: true,
|
||||||
})
|
})
|
||||||
.extend({
|
.extend({
|
||||||
id: z.string().min(1),
|
id: z.string().min(1),
|
||||||
@@ -181,18 +180,6 @@ export const apiFindCompose = z.object({
|
|||||||
composeId: z.string().min(1),
|
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({
|
export const apiDeleteCompose = z.object({
|
||||||
composeId: z.string().min(1),
|
composeId: z.string().min(1),
|
||||||
deleteVolumes: z.boolean(),
|
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 "./deployment";
|
||||||
export * from "./destination";
|
export * from "./destination";
|
||||||
export * from "./domain";
|
export * from "./domain";
|
||||||
export * from "./environment";
|
|
||||||
export * from "./git-provider";
|
export * from "./git-provider";
|
||||||
export * from "./gitea";
|
export * from "./gitea";
|
||||||
export * from "./github";
|
export * from "./github";
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ import { createInsertSchema } from "drizzle-zod";
|
|||||||
import { nanoid } from "nanoid";
|
import { nanoid } from "nanoid";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { backups } from "./backups";
|
import { backups } from "./backups";
|
||||||
import { environments } from "./environment";
|
|
||||||
import { mounts } from "./mount";
|
import { mounts } from "./mount";
|
||||||
|
import { projects } from "./project";
|
||||||
import { server } from "./server";
|
import { server } from "./server";
|
||||||
import {
|
import {
|
||||||
applicationStatus,
|
applicationStatus,
|
||||||
@@ -66,19 +66,18 @@ export const mariadb = pgTable("mariadb", {
|
|||||||
createdAt: text("createdAt")
|
createdAt: text("createdAt")
|
||||||
.notNull()
|
.notNull()
|
||||||
.$defaultFn(() => new Date().toISOString()),
|
.$defaultFn(() => new Date().toISOString()),
|
||||||
|
projectId: text("projectId")
|
||||||
environmentId: text("environmentId")
|
|
||||||
.notNull()
|
.notNull()
|
||||||
.references(() => environments.environmentId, { onDelete: "cascade" }),
|
.references(() => projects.projectId, { onDelete: "cascade" }),
|
||||||
serverId: text("serverId").references(() => server.serverId, {
|
serverId: text("serverId").references(() => server.serverId, {
|
||||||
onDelete: "cascade",
|
onDelete: "cascade",
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const mariadbRelations = relations(mariadb, ({ one, many }) => ({
|
export const mariadbRelations = relations(mariadb, ({ one, many }) => ({
|
||||||
environment: one(environments, {
|
project: one(projects, {
|
||||||
fields: [mariadb.environmentId],
|
fields: [mariadb.projectId],
|
||||||
references: [environments.environmentId],
|
references: [projects.projectId],
|
||||||
}),
|
}),
|
||||||
backups: many(backups),
|
backups: many(backups),
|
||||||
mounts: many(mounts),
|
mounts: many(mounts),
|
||||||
@@ -95,19 +94,8 @@ const createSchema = createInsertSchema(mariadb, {
|
|||||||
createdAt: z.string(),
|
createdAt: z.string(),
|
||||||
databaseName: z.string().min(1),
|
databaseName: z.string().min(1),
|
||||||
databaseUser: z.string().min(1),
|
databaseUser: z.string().min(1),
|
||||||
databasePassword: z
|
databasePassword: z.string(),
|
||||||
.string()
|
databaseRootPassword: z.string().optional(),
|
||||||
.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(),
|
|
||||||
dockerImage: z.string().default("mariadb:6"),
|
dockerImage: z.string().default("mariadb:6"),
|
||||||
command: z.string().optional(),
|
command: z.string().optional(),
|
||||||
env: z.string().optional(),
|
env: z.string().optional(),
|
||||||
@@ -115,7 +103,7 @@ const createSchema = createInsertSchema(mariadb, {
|
|||||||
memoryLimit: z.string().optional(),
|
memoryLimit: z.string().optional(),
|
||||||
cpuReservation: z.string().optional(),
|
cpuReservation: z.string().optional(),
|
||||||
cpuLimit: z.string().optional(),
|
cpuLimit: z.string().optional(),
|
||||||
environmentId: z.string(),
|
projectId: z.string(),
|
||||||
applicationStatus: z.enum(["idle", "running", "done", "error"]),
|
applicationStatus: z.enum(["idle", "running", "done", "error"]),
|
||||||
externalPort: z.number(),
|
externalPort: z.number(),
|
||||||
description: z.string().optional(),
|
description: z.string().optional(),
|
||||||
@@ -136,7 +124,7 @@ export const apiCreateMariaDB = createSchema
|
|||||||
appName: true,
|
appName: true,
|
||||||
dockerImage: true,
|
dockerImage: true,
|
||||||
databaseRootPassword: true,
|
databaseRootPassword: true,
|
||||||
environmentId: true,
|
projectId: true,
|
||||||
description: true,
|
description: true,
|
||||||
databaseName: true,
|
databaseName: true,
|
||||||
databaseUser: true,
|
databaseUser: true,
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ import { createInsertSchema } from "drizzle-zod";
|
|||||||
import { nanoid } from "nanoid";
|
import { nanoid } from "nanoid";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { backups } from "./backups";
|
import { backups } from "./backups";
|
||||||
import { environments } from "./environment";
|
|
||||||
import { mounts } from "./mount";
|
import { mounts } from "./mount";
|
||||||
|
import { projects } from "./project";
|
||||||
import { server } from "./server";
|
import { server } from "./server";
|
||||||
import {
|
import {
|
||||||
applicationStatus,
|
applicationStatus,
|
||||||
@@ -62,10 +62,9 @@ export const mongo = pgTable("mongo", {
|
|||||||
createdAt: text("createdAt")
|
createdAt: text("createdAt")
|
||||||
.notNull()
|
.notNull()
|
||||||
.$defaultFn(() => new Date().toISOString()),
|
.$defaultFn(() => new Date().toISOString()),
|
||||||
|
projectId: text("projectId")
|
||||||
environmentId: text("environmentId")
|
|
||||||
.notNull()
|
.notNull()
|
||||||
.references(() => environments.environmentId, { onDelete: "cascade" }),
|
.references(() => projects.projectId, { onDelete: "cascade" }),
|
||||||
serverId: text("serverId").references(() => server.serverId, {
|
serverId: text("serverId").references(() => server.serverId, {
|
||||||
onDelete: "cascade",
|
onDelete: "cascade",
|
||||||
}),
|
}),
|
||||||
@@ -73,9 +72,9 @@ export const mongo = pgTable("mongo", {
|
|||||||
});
|
});
|
||||||
|
|
||||||
export const mongoRelations = relations(mongo, ({ one, many }) => ({
|
export const mongoRelations = relations(mongo, ({ one, many }) => ({
|
||||||
environment: one(environments, {
|
project: one(projects, {
|
||||||
fields: [mongo.environmentId],
|
fields: [mongo.projectId],
|
||||||
references: [environments.environmentId],
|
references: [projects.projectId],
|
||||||
}),
|
}),
|
||||||
backups: many(backups),
|
backups: many(backups),
|
||||||
mounts: many(mounts),
|
mounts: many(mounts),
|
||||||
@@ -90,12 +89,7 @@ const createSchema = createInsertSchema(mongo, {
|
|||||||
createdAt: z.string(),
|
createdAt: z.string(),
|
||||||
mongoId: z.string(),
|
mongoId: z.string(),
|
||||||
name: z.string().min(1),
|
name: z.string().min(1),
|
||||||
databasePassword: z
|
databasePassword: z.string(),
|
||||||
.string()
|
|
||||||
.regex(/^[a-zA-Z0-9@#%^&*()_+\-=[\]{}|;:,.<>?~`]*$/, {
|
|
||||||
message:
|
|
||||||
"Password contains invalid characters. Please avoid: $ ! ' \" \\ / and space characters for database compatibility",
|
|
||||||
}),
|
|
||||||
databaseUser: z.string().min(1),
|
databaseUser: z.string().min(1),
|
||||||
dockerImage: z.string().default("mongo:15"),
|
dockerImage: z.string().default("mongo:15"),
|
||||||
command: z.string().optional(),
|
command: z.string().optional(),
|
||||||
@@ -104,7 +98,7 @@ const createSchema = createInsertSchema(mongo, {
|
|||||||
memoryLimit: z.string().optional(),
|
memoryLimit: z.string().optional(),
|
||||||
cpuReservation: z.string().optional(),
|
cpuReservation: z.string().optional(),
|
||||||
cpuLimit: z.string().optional(),
|
cpuLimit: z.string().optional(),
|
||||||
environmentId: z.string(),
|
projectId: z.string(),
|
||||||
applicationStatus: z.enum(["idle", "running", "done", "error"]),
|
applicationStatus: z.enum(["idle", "running", "done", "error"]),
|
||||||
externalPort: z.number(),
|
externalPort: z.number(),
|
||||||
description: z.string().optional(),
|
description: z.string().optional(),
|
||||||
@@ -125,7 +119,7 @@ export const apiCreateMongo = createSchema
|
|||||||
name: true,
|
name: true,
|
||||||
appName: true,
|
appName: true,
|
||||||
dockerImage: true,
|
dockerImage: true,
|
||||||
environmentId: true,
|
projectId: true,
|
||||||
description: true,
|
description: true,
|
||||||
databaseUser: true,
|
databaseUser: true,
|
||||||
databasePassword: true,
|
databasePassword: true,
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user