Merge branch 'canary' into feat/ntfy

This commit is contained in:
Mauricio Siu
2025-09-06 14:12:06 -06:00
132 changed files with 25182 additions and 2589 deletions

View File

@@ -6,16 +6,13 @@ Please describe in a short paragraph what this PR is about.
Before submitting this PR, please make sure that:
- [ ] You created a dedicated branch based on the `canary` branch.
- [ ] You have read the suggestions in the CONTRIBUTING.md file https://github.com/Dokploy/dokploy/blob/canary/CONTRIBUTING.md#pull-request
- [ ] You have tested this PR in your local instance.
- [] You created a dedicated branch based on the `canary` branch.
- [] You have read the suggestions in the CONTRIBUTING.md file https://github.com/Dokploy/dokploy/blob/canary/CONTRIBUTING.md#pull-request
- [] You have tested this PR in your local instance.
## Issues related (if applicable)
Close automatically the related issues using the keywords: `closes #ISSUE_NUMBER`, `fixes #ISSUE_NUMBER`, `resolves #ISSUE_NUMBER`
Example: `closes #123`
closes #123
## Screenshots (if applicable)
If you include a video or screenshot, would be awesome so we can see the changes in action.

BIN
.github/sponsors/tuple.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 264 KiB

View File

@@ -2,7 +2,7 @@ name: Dokploy Docker Build
on:
push:
branches: [main, canary]
branches: [main, canary, "fix/re-apply-database-migration-fix"]
workflow_dispatch:
env:

View File

@@ -11,8 +11,25 @@
</div>
<br />
<div align="center" markdown="1">
<sup>Special thanks to:</sup>
<br>
<br>
<a href="https://tuple.app/dokploy">
<img src=".github/sponsors/tuple.png" alt="Tuple's sponsorship image" width="400"/>
</a>
### [Tuple, the premier screen sharing app for developers](https://tuple.app/dokploy)
[Available for MacOS & Windows](https://tuple.app/dokploy)<br>
</div>
Dokploy is a free, self-hostable Platform as a Service (PaaS) that simplifies the deployment and management of applications and databases.
## ✨ Features
Dokploy includes multiple features to make your life easier.

View File

@@ -3,8 +3,8 @@ import { z } from "zod";
export const deployJobSchema = z.discriminatedUnion("applicationType", [
z.object({
applicationId: z.string(),
titleLog: z.string(),
descriptionLog: z.string(),
titleLog: z.string().optional(),
descriptionLog: z.string().optional(),
server: z.boolean().optional(),
type: z.enum(["deploy", "redeploy"]),
applicationType: z.literal("application"),
@@ -12,8 +12,8 @@ export const deployJobSchema = z.discriminatedUnion("applicationType", [
}),
z.object({
composeId: z.string(),
titleLog: z.string(),
descriptionLog: z.string(),
titleLog: z.string().optional(),
descriptionLog: z.string().optional(),
server: z.boolean().optional(),
type: z.enum(["deploy", "redeploy"]),
applicationType: z.literal("compose"),
@@ -22,8 +22,8 @@ export const deployJobSchema = z.discriminatedUnion("applicationType", [
z.object({
applicationId: z.string(),
previewDeploymentId: z.string(),
titleLog: z.string(),
descriptionLog: z.string(),
titleLog: z.string().optional(),
descriptionLog: z.string().optional(),
server: z.boolean().optional(),
type: z.enum(["deploy"]),
applicationType: z.literal("application-preview"),

View File

@@ -18,14 +18,14 @@ export const deploy = async (job: DeployJob) => {
if (job.type === "redeploy") {
await rebuildRemoteApplication({
applicationId: job.applicationId,
titleLog: job.titleLog,
descriptionLog: job.descriptionLog,
titleLog: job.titleLog || "Rebuild deployment",
descriptionLog: job.descriptionLog || "",
});
} else if (job.type === "deploy") {
await deployRemoteApplication({
applicationId: job.applicationId,
titleLog: job.titleLog,
descriptionLog: job.descriptionLog,
titleLog: job.titleLog || "Manual deployment",
descriptionLog: job.descriptionLog || "",
});
}
}
@@ -38,14 +38,14 @@ export const deploy = async (job: DeployJob) => {
if (job.type === "redeploy") {
await rebuildRemoteCompose({
composeId: job.composeId,
titleLog: job.titleLog,
descriptionLog: job.descriptionLog,
titleLog: job.titleLog || "Rebuild deployment",
descriptionLog: job.descriptionLog || "",
});
} else if (job.type === "deploy") {
await deployRemoteCompose({
composeId: job.composeId,
titleLog: job.titleLog,
descriptionLog: job.descriptionLog,
titleLog: job.titleLog || "Manual deployment",
descriptionLog: job.descriptionLog || "",
});
}
}
@@ -57,8 +57,8 @@ export const deploy = async (job: DeployJob) => {
if (job.type === "deploy") {
await deployRemotePreviewApplication({
applicationId: job.applicationId,
titleLog: job.titleLog,
descriptionLog: job.descriptionLog,
titleLog: job.titleLog || "Preview Deployment",
descriptionLog: job.descriptionLog || "",
previewDeploymentId: job.previewDeploymentId,
});
}

View File

@@ -56,13 +56,21 @@ const baseApp: ApplicationNested = {
previewPort: 3000,
previewLimit: 0,
previewWildcard: "",
project: {
environment: {
env: "",
organizationId: "",
environmentId: "",
name: "",
description: "",
createdAt: "",
description: "",
projectId: "",
project: {
env: "",
organizationId: "",
name: "",
description: "",
createdAt: "",
projectId: "",
},
},
buildArgs: null,
buildPath: "/",
@@ -92,6 +100,7 @@ const baseApp: ApplicationNested = {
dockerfile: null,
dockerImage: null,
dropBuildPath: null,
environmentId: "",
enabled: null,
env: null,
healthCheckSwarm: null,
@@ -106,7 +115,6 @@ const baseApp: ApplicationNested = {
password: null,
placementSwarm: null,
ports: [],
projectId: "",
publishDirectory: null,
isStaticSpa: null,
redirects: [],

View File

@@ -0,0 +1,335 @@
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",
]);
});
});

View File

@@ -36,13 +36,22 @@ const baseApp: ApplicationNested = {
previewLimit: 0,
previewCustomCertResolver: null,
previewWildcard: "",
project: {
environmentId: "",
environment: {
env: "",
organizationId: "",
environmentId: "",
name: "",
description: "",
createdAt: "",
description: "",
projectId: "",
project: {
env: "",
organizationId: "",
name: "",
description: "",
createdAt: "",
projectId: "",
},
},
buildPath: "/",
gitlabPathNamespace: "",
@@ -85,7 +94,6 @@ const baseApp: ApplicationNested = {
password: null,
placementSwarm: null,
ports: [],
projectId: "",
publishDirectory: null,
isStaticSpa: null,
redirects: [],

View File

@@ -77,7 +77,7 @@ export const ShowDeployments = ({
<div className="flex flex-col gap-2">
<CardTitle className="text-xl">Deployments</CardTitle>
<CardDescription>
See all the 10 last deployments for this {type}
See the last 10 deployments for this {type}
</CardDescription>
</div>
<div className="flex flex-row items-center gap-2">
@@ -104,7 +104,9 @@ export const ShowDeployments = ({
<span>Webhook URL: </span>
<div className="flex flex-row items-center gap-2">
<span className="break-all text-muted-foreground">
{`${url}/api/deploy${type === "compose" ? "/compose" : ""}/${refreshToken}`}
{`${url}/api/deploy${
type === "compose" ? "/compose" : ""
}/${refreshToken}`}
</span>
{(type === "application" || type === "compose") && (
<RefreshToken id={id} type={type} />

View File

@@ -69,7 +69,7 @@ export const ShowGeneralApplication = ({ applicationId }: Props) => {
toast.success("Application deployed successfully");
refetch();
router.push(
`/dashboard/project/${data?.projectId}/services/application/${applicationId}?tab=deployments`,
`/dashboard/project/${data?.environment.projectId}/environment/${data?.environmentId}/services/application/${applicationId}?tab=deployments`,
);
})
.catch(() => {

View File

@@ -58,7 +58,7 @@ export const ShowSchedules = ({ id, scheduleType = "application" }: Props) => {
return (
<Card className="border px-6 shadow-none bg-transparent h-full min-h-[50vh]">
<CardHeader className="px-0">
<div className="flex justify-between items-center">
<div className="flex justify-between items-center gap-y-2 flex-wrap">
<div className="flex flex-col gap-2">
<CardTitle className="text-xl font-bold flex items-center gap-2">
Scheduled Tasks
@@ -91,15 +91,15 @@ export const ShowSchedules = ({ id, scheduleType = "application" }: Props) => {
return (
<div
key={schedule.scheduleId}
className="flex items-center justify-between rounded-lg border p-3 transition-colors bg-muted/50"
className="flex items-center flex-wrap sm:flex-nowrap gap-y-2 justify-between rounded-lg border p-3 transition-colors bg-muted/50"
>
<div className="flex items-start gap-3">
<div className="flex h-9 w-9 items-center justify-center rounded-full bg-primary/5">
<div className="flex flex-shrink-0 h-9 w-9 items-center justify-center rounded-full bg-primary/5">
<Clock className="size-4 text-primary/70" />
</div>
<div className="space-y-1.5">
<div className="flex items-center gap-2">
<h3 className="text-sm font-medium leading-none">
<div className="flex items-center gap-2 flex-wrap">
<h3 className="text-sm font-medium leading-none [overflow-wrap:anywhere] line-clamp-3">
{schedule.name}
</h3>
<Badge
@@ -109,7 +109,7 @@ export const ShowSchedules = ({ id, scheduleType = "application" }: Props) => {
{schedule.enabled ? "Enabled" : "Disabled"}
</Badge>
</div>
<div className="flex items-center gap-2 text-sm text-muted-foreground">
<div className="flex items-center gap-2 text-sm text-muted-foreground flex-wrap">
<Badge
variant="outline"
className="font-mono text-[10px] bg-transparent"
@@ -142,7 +142,7 @@ export const ShowSchedules = ({ id, scheduleType = "application" }: Props) => {
</div>
</div>
<div className="flex items-center gap-1.5">
<div className="flex items-center gap-0.5 md:gap-1.5">
<ShowDeploymentsModal
id={schedule.scheduleId}
type="schedule"
@@ -226,7 +226,7 @@ export const ShowSchedules = ({ id, scheduleType = "application" }: Props) => {
})}
</div>
) : (
<div className="flex flex-col gap-2 items-center justify-center py-12 rounded-lg">
<div className="flex flex-col gap-2 items-center justify-center py-12 rounded-lg">
<Clock className="size-8 mb-4 text-muted-foreground" />
<p className="text-lg font-medium text-muted-foreground">
No scheduled tasks

View File

@@ -101,7 +101,9 @@ export const DeleteService = ({ id, type }: Props) => {
deleteVolumes,
})
.then((result) => {
push(`/dashboard/project/${result?.projectId}`);
push(
`/dashboard/project/${result?.environment?.projectId}/environment/${result?.environment?.environmentId}`,
);
toast.success("deleted successfully");
setIsOpen(false);
})

View File

@@ -47,7 +47,7 @@ export const ComposeActions = ({ composeId }: Props) => {
toast.success("Compose deployed successfully");
refetch();
router.push(
`/dashboard/project/${data?.project.projectId}/services/compose/${composeId}?tab=deployments`,
`/dashboard/project/${data?.environment.projectId}/environment/${data?.environmentId}/services/compose/${composeId}?tab=deployments`,
);
})
.catch(() => {

View File

@@ -1,10 +1,10 @@
import { TemplateGenerator } from "@/components/dashboard/project/ai/template-generator";
interface Props {
projectId: string;
environmentId: string;
projectName?: string;
}
export const AddAiAssistant = ({ projectId }: Props) => {
return <TemplateGenerator projectId={projectId} />;
export const AddAiAssistant = ({ environmentId }: Props) => {
return <TemplateGenerator environmentId={environmentId} />;
};

View File

@@ -64,11 +64,11 @@ const AddTemplateSchema = z.object({
type AddTemplate = z.infer<typeof AddTemplateSchema>;
interface Props {
projectId: string;
environmentId: string;
projectName?: string;
}
export const AddApplication = ({ projectId, projectName }: Props) => {
export const AddApplication = ({ environmentId, projectName }: Props) => {
const utils = api.useUtils();
const { data: isCloud } = api.settings.isCloud.useQuery();
const [visible, setVisible] = useState(false);
@@ -76,6 +76,10 @@ export const AddApplication = ({ projectId, projectName }: Props) => {
const { data: servers } = api.server.withSSHKey.useQuery();
const hasServers = servers && servers.length > 0;
// Show dropdown logic based on cloud environment
// Cloud: show only if there are remote servers (no Dokploy option)
// Self-hosted: show only if there are remote servers (Dokploy is default, hide if no remote servers)
const shouldShowServerDropdown = hasServers;
const { mutateAsync, isLoading, error, isError } =
api.application.create.useMutation();
@@ -94,15 +98,15 @@ export const AddApplication = ({ projectId, projectName }: Props) => {
name: data.name,
appName: data.appName,
description: data.description,
projectId,
serverId: data.serverId,
serverId: data.serverId === "dokploy" ? undefined : data.serverId,
environmentId,
})
.then(async () => {
toast.success("Service Created");
form.reset();
setVisible(false);
await utils.project.one.invalidate({
projectId,
await utils.environment.one.invalidate({
environmentId,
});
})
.catch(() => {
@@ -157,7 +161,7 @@ export const AddApplication = ({ projectId, projectName }: Props) => {
</FormItem>
)}
/>
{hasServers && (
{shouldShowServerDropdown && (
<FormField
control={form.control}
name="serverId"
@@ -186,13 +190,27 @@ export const AddApplication = ({ projectId, projectName }: Props) => {
<Select
onValueChange={field.onChange}
defaultValue={field.value}
defaultValue={
field.value || (!isCloud ? "dokploy" : undefined)
}
>
<SelectTrigger>
<SelectValue placeholder="Select a Server" />
<SelectValue
placeholder={!isCloud ? "Dokploy" : "Select a Server"}
/>
</SelectTrigger>
<SelectContent>
<SelectGroup>
{!isCloud && (
<SelectItem value="dokploy">
<span className="flex items-center gap-2 justify-between w-full">
<span>Dokploy</span>
<span className="text-muted-foreground text-xs self-center">
Default
</span>
</span>
</SelectItem>
)}
{servers?.map((server) => (
<SelectItem
key={server.serverId}
@@ -206,7 +224,9 @@ export const AddApplication = ({ projectId, projectName }: Props) => {
</span>
</SelectItem>
))}
<SelectLabel>Servers ({servers?.length})</SelectLabel>
<SelectLabel>
Servers ({servers?.length + (!isCloud ? 1 : 0)})
</SelectLabel>
</SelectGroup>
</SelectContent>
</Select>

View File

@@ -65,11 +65,11 @@ const AddComposeSchema = z.object({
type AddCompose = z.infer<typeof AddComposeSchema>;
interface Props {
projectId: string;
environmentId: string;
projectName?: string;
}
export const AddCompose = ({ projectId, projectName }: Props) => {
export const AddCompose = ({ environmentId, projectName }: Props) => {
const utils = api.useUtils();
const [visible, setVisible] = useState(false);
const slug = slugify(projectName);
@@ -78,7 +78,14 @@ export const AddCompose = ({ projectId, projectName }: Props) => {
const { mutateAsync, isLoading, error, isError } =
api.compose.create.useMutation();
// Get environment data to extract projectId
const { data: environment } = api.environment.one.useQuery({ environmentId });
const hasServers = servers && servers.length > 0;
// Show dropdown logic based on cloud environment
// Cloud: show only if there are remote servers (no Dokploy option)
// Self-hosted: show only if there are remote servers (Dokploy is default, hide if no remote servers)
const shouldShowServerDropdown = hasServers;
const form = useForm<AddCompose>({
defaultValues: {
@@ -98,16 +105,17 @@ export const AddCompose = ({ projectId, projectName }: Props) => {
await mutateAsync({
name: data.name,
description: data.description,
projectId,
environmentId,
composeType: data.composeType,
appName: data.appName,
serverId: data.serverId,
serverId: data.serverId === "dokploy" ? undefined : data.serverId,
})
.then(async () => {
toast.success("Compose Created");
setVisible(false);
await utils.project.one.invalidate({
projectId,
// Invalidate the project query to refresh the environment data
await utils.environment.one.invalidate({
environmentId,
});
})
.catch(() => {
@@ -165,7 +173,7 @@ export const AddCompose = ({ projectId, projectName }: Props) => {
)}
/>
</div>
{hasServers && (
{shouldShowServerDropdown && (
<FormField
control={form.control}
name="serverId"
@@ -194,13 +202,27 @@ export const AddCompose = ({ projectId, projectName }: Props) => {
<Select
onValueChange={field.onChange}
defaultValue={field.value}
defaultValue={
field.value || (!isCloud ? "dokploy" : undefined)
}
>
<SelectTrigger>
<SelectValue placeholder="Select a Server" />
<SelectValue
placeholder={!isCloud ? "Dokploy" : "Select a Server"}
/>
</SelectTrigger>
<SelectContent>
<SelectGroup>
{!isCloud && (
<SelectItem value="dokploy">
<span className="flex items-center gap-2 justify-between w-full">
<span>Dokploy</span>
<span className="text-muted-foreground text-xs self-center">
Default
</span>
</span>
</SelectItem>
)}
{servers?.map((server) => (
<SelectItem
key={server.serverId}
@@ -214,7 +236,9 @@ export const AddCompose = ({ projectId, projectName }: Props) => {
</span>
</SelectItem>
))}
<SelectLabel>Servers ({servers?.length})</SelectLabel>
<SelectLabel>
Servers ({servers?.length + (!isCloud ? 1 : 0)})
</SelectLabel>
</SelectGroup>
</SelectContent>
</Select>

View File

@@ -83,7 +83,12 @@ const baseDatabaseSchema = z.object({
message:
"App name supports lowercase letters, numbers, '-' and can only start and end letters, and does not support continuous '-'",
}),
databasePassword: z.string(),
databasePassword: z
.string()
.regex(/^[a-zA-Z0-9@#%^&*()_+\-=[\]{}|;:,.<>?~`]*$/, {
message:
"Password contains invalid characters. Please avoid: $ ! ' \" \\ / and space characters for database compatibility",
}),
dockerImage: z.string(),
description: z.string().nullable(),
serverId: z.string().nullable(),
@@ -112,7 +117,13 @@ const mySchema = z.discriminatedUnion("type", [
z
.object({
type: z.literal("mysql"),
databaseRootPassword: z.string().default(""),
databaseRootPassword: z
.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"),
databaseName: z.string().default("mysql"),
})
@@ -121,7 +132,13 @@ const mySchema = z.discriminatedUnion("type", [
.object({
type: z.literal("mariadb"),
dockerImage: z.string().default("mariadb:4"),
databaseRootPassword: z.string().default(""),
databaseRootPassword: z
.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"),
databaseName: z.string().default("mariadb"),
})
@@ -154,14 +171,15 @@ const databasesMap = {
type AddDatabase = z.infer<typeof mySchema>;
interface Props {
projectId: string;
environmentId: string;
projectName?: string;
}
export const AddDatabase = ({ projectId, projectName }: Props) => {
export const AddDatabase = ({ environmentId, projectName }: Props) => {
const utils = api.useUtils();
const [visible, setVisible] = useState(false);
const slug = slugify(projectName);
const { data: isCloud } = api.settings.isCloud.useQuery();
const { data: servers } = api.server.withSSHKey.useQuery();
const postgresMutation = api.postgres.create.useMutation();
const mongoMutation = api.mongo.create.useMutation();
@@ -169,7 +187,14 @@ export const AddDatabase = ({ projectId, projectName }: Props) => {
const mariadbMutation = api.mariadb.create.useMutation();
const mysqlMutation = api.mysql.create.useMutation();
// Get environment data to extract projectId
const { data: environment } = api.environment.one.useQuery({ environmentId });
const hasServers = servers && servers.length > 0;
// Show dropdown logic based on cloud environment
// Cloud: show only if there are remote servers (no Dokploy option)
// Self-hosted: show only if there are remote servers (Dokploy is default, hide if no remote servers)
const shouldShowServerDropdown = hasServers;
const form = useForm<AddDatabase>({
defaultValues: {
@@ -203,8 +228,8 @@ export const AddDatabase = ({ projectId, projectName }: Props) => {
name: data.name,
appName: data.appName,
dockerImage: defaultDockerImage,
projectId,
serverId: data.serverId,
serverId: data.serverId === "dokploy" ? undefined : data.serverId,
environmentId,
description: data.description,
};
@@ -216,7 +241,7 @@ export const AddDatabase = ({ projectId, projectName }: Props) => {
databaseUser:
data.databaseUser || databasesUserDefaultPlaceholder[data.type],
serverId: data.serverId,
serverId: data.serverId === "dokploy" ? null : data.serverId,
});
} else if (data.type === "mongo") {
promise = mongoMutation.mutateAsync({
@@ -224,25 +249,24 @@ export const AddDatabase = ({ projectId, projectName }: Props) => {
databasePassword: data.databasePassword,
databaseUser:
data.databaseUser || databasesUserDefaultPlaceholder[data.type],
serverId: data.serverId,
serverId: data.serverId === "dokploy" ? null : data.serverId,
replicaSets: data.replicaSets,
});
} else if (data.type === "redis") {
promise = redisMutation.mutateAsync({
...commonParams,
databasePassword: data.databasePassword,
serverId: data.serverId,
projectId,
serverId: data.serverId === "dokploy" ? null : data.serverId,
});
} else if (data.type === "mariadb") {
promise = mariadbMutation.mutateAsync({
...commonParams,
databasePassword: data.databasePassword,
databaseRootPassword: data.databaseRootPassword,
databaseRootPassword: data.databaseRootPassword || "",
databaseName: data.databaseName || "mariadb",
databaseUser:
data.databaseUser || databasesUserDefaultPlaceholder[data.type],
serverId: data.serverId,
serverId: data.serverId === "dokploy" ? null : data.serverId,
});
} else if (data.type === "mysql") {
promise = mysqlMutation.mutateAsync({
@@ -251,8 +275,8 @@ export const AddDatabase = ({ projectId, projectName }: Props) => {
databaseName: data.databaseName || "mysql",
databaseUser:
data.databaseUser || databasesUserDefaultPlaceholder[data.type],
databaseRootPassword: data.databaseRootPassword,
serverId: data.serverId,
serverId: data.serverId === "dokploy" ? null : data.serverId,
databaseRootPassword: data.databaseRootPassword || "",
});
}
@@ -271,8 +295,9 @@ export const AddDatabase = ({ projectId, projectName }: Props) => {
databaseUser: "",
});
setVisible(false);
await utils.project.one.invalidate({
projectId,
// Invalidate the project query to refresh the environment data
await utils.environment.one.invalidate({
environmentId,
});
})
.catch(() => {
@@ -382,7 +407,7 @@ export const AddDatabase = ({ projectId, projectName }: Props) => {
</FormItem>
)}
/>
{hasServers && (
{shouldShowServerDropdown && (
<FormField
control={form.control}
name="serverId"
@@ -391,13 +416,29 @@ export const AddDatabase = ({ projectId, projectName }: Props) => {
<FormLabel>Select a Server</FormLabel>
<Select
onValueChange={field.onChange}
defaultValue={field.value || ""}
defaultValue={
field.value || (!isCloud ? "dokploy" : undefined)
}
>
<SelectTrigger>
<SelectValue placeholder="Select a Server" />
<SelectValue
placeholder={
!isCloud ? "Dokploy" : "Select a Server"
}
/>
</SelectTrigger>
<SelectContent>
<SelectGroup>
{!isCloud && (
<SelectItem value="dokploy">
<span className="flex items-center gap-2 justify-between w-full">
<span>Dokploy</span>
<span className="text-muted-foreground text-xs self-center">
Default
</span>
</span>
</SelectItem>
)}
{servers?.map((server) => (
<SelectItem
key={server.serverId}
@@ -407,7 +448,7 @@ export const AddDatabase = ({ projectId, projectName }: Props) => {
</SelectItem>
))}
<SelectLabel>
Servers ({servers?.length})
Servers ({servers?.length + (!isCloud ? 1 : 0)})
</SelectLabel>
</SelectGroup>
</SelectContent>

View File

@@ -73,11 +73,11 @@ import { api } from "@/utils/api";
const TEMPLATE_BASE_URL_KEY = "dokploy_template_base_url";
interface Props {
projectId: string;
environmentId: string;
baseUrl?: string;
}
export const AddTemplate = ({ projectId, baseUrl }: Props) => {
export const AddTemplate = ({ environmentId, baseUrl }: Props) => {
const [query, setQuery] = useState("");
const [open, setOpen] = useState(false);
const [viewMode, setViewMode] = useState<"detailed" | "icon">("detailed");
@@ -91,6 +91,9 @@ export const AddTemplate = ({ projectId, baseUrl }: Props) => {
return undefined;
});
// Get environment data to extract projectId
const { data: environment } = api.environment.one.useQuery({ environmentId });
// Save to localStorage when customBaseUrl changes
useEffect(() => {
if (customBaseUrl) {
@@ -138,6 +141,10 @@ export const AddTemplate = ({ projectId, baseUrl }: Props) => {
}) || [];
const hasServers = servers && servers.length > 0;
// Show dropdown logic based on cloud environment
// Cloud: show only if there are remote servers (no Dokploy option)
// Self-hosted: show only if there are remote servers (Dokploy is default, hide if no remote servers)
const shouldShowServerDropdown = hasServers;
return (
<Dialog open={open} onOpenChange={setOpen}>
@@ -427,7 +434,7 @@ export const AddTemplate = ({ projectId, baseUrl }: Props) => {
project.
</AlertDialogDescription>
{hasServers && (
{shouldShowServerDropdown && (
<div>
<TooltipProvider delayDuration={0}>
<Tooltip>
@@ -456,12 +463,29 @@ export const AddTemplate = ({ projectId, baseUrl }: Props) => {
onValueChange={(e) => {
setServerId(e);
}}
defaultValue={
!isCloud ? "dokploy" : undefined
}
>
<SelectTrigger>
<SelectValue placeholder="Select a Server" />
<SelectValue
placeholder={
!isCloud ? "Dokploy" : "Select a Server"
}
/>
</SelectTrigger>
<SelectContent>
<SelectGroup>
{!isCloud && (
<SelectItem value="dokploy">
<span className="flex items-center gap-2 justify-between w-full">
<span>Dokploy</span>
<span className="text-muted-foreground text-xs self-center">
Default
</span>
</span>
</SelectItem>
)}
{servers?.map((server) => (
<SelectItem
key={server.serverId}
@@ -476,7 +500,8 @@ export const AddTemplate = ({ projectId, baseUrl }: Props) => {
</SelectItem>
))}
<SelectLabel>
Servers ({servers?.length})
Servers (
{servers?.length + (!isCloud ? 1 : 0)})
</SelectLabel>
</SelectGroup>
</SelectContent>
@@ -490,16 +515,20 @@ export const AddTemplate = ({ projectId, baseUrl }: Props) => {
disabled={isLoading}
onClick={async () => {
const promise = mutateAsync({
projectId,
serverId: serverId || undefined,
serverId:
serverId === "dokploy"
? undefined
: serverId,
environmentId,
id: template.id,
baseUrl: customBaseUrl,
});
toast.promise(promise, {
loading: "Setting up...",
success: () => {
utils.project.one.invalidate({
projectId,
// Invalidate the project query to refresh the environment data
utils.environment.one.invalidate({
environmentId,
});
setOpen(false);
return `${template.name} template created successfully`;

View File

@@ -0,0 +1,446 @@
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>
</>
);
};

View File

@@ -25,7 +25,12 @@ const examples = [
export const StepOne = ({ setTemplateInfo, templateInfo }: any) => {
// Get servers from the API
const { data: servers } = api.server.withSSHKey.useQuery();
const { data: isCloud } = api.settings.isCloud.useQuery();
const hasServers = servers && servers.length > 0;
// Show dropdown logic based on cloud environment
// Cloud: show only if there are remote servers (no Dokploy option)
// Self-hosted: show only if there are remote servers (Dokploy is default, hide if no remote servers)
const shouldShowServerDropdown = hasServers;
const handleExampleClick = (example: string) => {
setTemplateInfo({ ...templateInfo, userInput: example });
@@ -48,34 +53,58 @@ export const StepOne = ({ setTemplateInfo, templateInfo }: any) => {
/>
</div>
{hasServers && (
{shouldShowServerDropdown && (
<div className="space-y-2">
<Label htmlFor="server-deploy">
Select the server where you want to deploy (optional)
</Label>
<Select
value={templateInfo.server?.serverId}
value={
templateInfo.server?.serverId ||
(!isCloud ? "dokploy" : undefined)
}
onValueChange={(value) => {
const server = servers?.find((s) => s.serverId === value);
if (server) {
if (value === "dokploy") {
setTemplateInfo({
...templateInfo,
server: server,
server: undefined,
});
} else {
const server = servers?.find((s) => s.serverId === value);
if (server) {
setTemplateInfo({
...templateInfo,
server: server,
});
}
}
}}
>
<SelectTrigger className="w-full">
<SelectValue placeholder="Select a server" />
<SelectValue
placeholder={!isCloud ? "Dokploy" : "Select a Server"}
/>
</SelectTrigger>
<SelectContent>
<SelectGroup>
{!isCloud && (
<SelectItem value="dokploy">
<span className="flex items-center gap-2 justify-between w-full">
<span>Dokploy</span>
<span className="text-muted-foreground text-xs self-center">
Default
</span>
</span>
</SelectItem>
)}
{servers?.map((server) => (
<SelectItem key={server.serverId} value={server.serverId}>
{server.name}
</SelectItem>
))}
<SelectLabel>Servers ({servers?.length})</SelectLabel>
<SelectLabel>
Servers ({servers?.length + (!isCloud ? 1 : 0)})
</SelectLabel>
</SelectGroup>
</SelectContent>
</Select>

View File

@@ -90,11 +90,11 @@ export const { useStepper, steps, Scoped } = defineStepper(
);
interface Props {
projectId: string;
environmentId: string;
projectName?: string;
}
export const TemplateGenerator = ({ projectId }: Props) => {
export const TemplateGenerator = ({ environmentId }: Props) => {
const [open, setOpen] = useState(false);
const stepper = useStepper();
const { data: aiSettings } = api.ai.getAll.useQuery();
@@ -121,7 +121,7 @@ export const TemplateGenerator = ({ projectId }: Props) => {
const onSubmit = async () => {
await mutateAsync({
projectId,
environmentId: environmentId,
id: templateInfo.details?.id || "",
name: templateInfo?.details?.name || "",
description: templateInfo?.details?.shortDescription || "",
@@ -138,8 +138,9 @@ export const TemplateGenerator = ({ projectId }: Props) => {
.then(async () => {
toast.success("Compose Created");
setOpen(false);
await utils.project.one.invalidate({
projectId,
// Invalidate the project query to refresh the environment data
await utils.environment.one.invalidate({
environmentId,
});
})
.catch(() => {

View File

@@ -15,6 +15,13 @@ import {
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { api } from "@/utils/api";
export type Services = {
@@ -36,23 +43,35 @@ export type Services = {
};
interface DuplicateProjectProps {
projectId: string;
environmentId: string;
services: Services[];
selectedServiceIds: string[];
}
export const DuplicateProject = ({
projectId,
environmentId,
services,
selectedServiceIds,
}: DuplicateProjectProps) => {
const [open, setOpen] = useState(false);
const [name, setName] = useState("");
const [description, setDescription] = useState("");
const [duplicateType, setDuplicateType] = useState("new-project"); // "new-project" or "same-project"
const [duplicateType, setDuplicateType] = useState("new-project"); // "new-project" or "existing-environment"
const [selectedTargetProject, setSelectedTargetProject] =
useState<string>("");
const [selectedTargetEnvironment, setSelectedTargetEnvironment] =
useState<string>("");
const utils = api.useUtils();
const router = useRouter();
// Queries for project and environment selection
const { data: allProjects } = api.project.all.useQuery();
const { data: selectedProjectEnvironments } =
api.environment.byProjectId.useQuery(
{ projectId: selectedTargetProject },
{ enabled: !!selectedTargetProject },
);
const selectedServices = services.filter((service) =>
selectedServiceIds.includes(service.id),
);
@@ -68,7 +87,9 @@ export const DuplicateProject = ({
);
setOpen(false);
if (duplicateType === "new-project") {
router.push(`/dashboard/project/${newProject.projectId}`);
router.push(
`/dashboard/project/${newProject?.projectId}/environment/${newProject?.environmentId}`,
);
}
},
onError: (error) => {
@@ -82,8 +103,20 @@ export const DuplicateProject = ({
return;
}
if (duplicateType === "existing-environment") {
if (!selectedTargetProject) {
toast.error("Please select a target project");
return;
}
if (!selectedTargetEnvironment) {
toast.error("Please select a target environment");
return;
}
}
// TODO: Update duplicate API to support targetProjectId and targetEnvironmentId
await duplicateProject({
sourceProjectId: projectId,
sourceEnvironmentId: selectedTargetEnvironment,
name,
description,
includeServices: true,
@@ -91,7 +124,7 @@ export const DuplicateProject = ({
id: service.id,
type: service.type,
})),
duplicateInSameProject: duplicateType === "same-project",
duplicateInSameProject: duplicateType === "existing-environment",
});
};
@@ -105,6 +138,8 @@ export const DuplicateProject = ({
setName("");
setDescription("");
setDuplicateType("new-project");
setSelectedTargetProject("");
setSelectedTargetEnvironment("");
}
}}
>
@@ -127,7 +162,14 @@ export const DuplicateProject = ({
<Label>Duplicate to</Label>
<RadioGroup
value={duplicateType}
onValueChange={setDuplicateType}
onValueChange={(value) => {
setDuplicateType(value);
// Reset selections when changing type
if (value !== "existing-environment") {
setSelectedTargetProject("");
setSelectedTargetEnvironment("");
}
}}
className="grid gap-2"
>
<div className="flex items-center space-x-2">
@@ -135,8 +177,13 @@ export const DuplicateProject = ({
<Label htmlFor="new-project">New project</Label>
</div>
<div className="flex items-center space-x-2">
<RadioGroupItem value="same-project" id="same-project" />
<Label htmlFor="same-project">Same project</Label>
<RadioGroupItem
value="existing-environment"
id="existing-environment"
/>
<Label htmlFor="existing-environment">
Existing environment
</Label>
</div>
</RadioGroup>
</div>
@@ -165,6 +212,74 @@ export const DuplicateProject = ({
</>
)}
{duplicateType === "existing-environment" && (
<>
{allProjects?.filter((p) => p.projectId !== environmentId)
.length === 0 ? (
<div className="flex flex-col items-center justify-center gap-2 py-4 text-center">
<p className="text-sm text-muted-foreground">
No other projects available. Create a new project first.
</p>
</div>
) : (
<>
{/* Step 1: Select Project */}
<div className="grid gap-2">
<Label>Target Project</Label>
<Select
value={selectedTargetProject}
onValueChange={(value) => {
setSelectedTargetProject(value);
setSelectedTargetEnvironment(""); // Reset environment when project changes
}}
>
<SelectTrigger>
<SelectValue placeholder="Select target project" />
</SelectTrigger>
<SelectContent>
{allProjects
?.filter((p) => p.projectId !== environmentId)
.map((project) => (
<SelectItem
key={project.projectId}
value={project.projectId}
>
{project.name}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
{/* Step 2: Select Environment (only show if project is selected) */}
{selectedTargetProject && (
<div className="grid gap-2">
<Label>Target Environment</Label>
<Select
value={selectedTargetEnvironment}
onValueChange={setSelectedTargetEnvironment}
>
<SelectTrigger>
<SelectValue placeholder="Select target environment" />
</SelectTrigger>
<SelectContent>
{selectedProjectEnvironments?.map((env) => (
<SelectItem
key={env.environmentId}
value={env.environmentId}
>
{env.name}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
)}
</>
)}
</>
)}
<div className="grid gap-2">
<Label>Selected services to duplicate</Label>
<div className="space-y-2 max-h-[200px] overflow-y-auto border rounded-md p-4">
@@ -187,18 +302,26 @@ export const DuplicateProject = ({
>
Cancel
</Button>
<Button onClick={handleDuplicate} disabled={isLoading}>
<Button
onClick={handleDuplicate}
disabled={
isLoading ||
(duplicateType === "new-project" && !name) ||
(duplicateType === "existing-environment" &&
(!selectedTargetProject || !selectedTargetEnvironment))
}
>
{isLoading ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
{duplicateType === "new-project"
? "Duplicating project..."
: "Duplicating services..."}
? "Duplicating to new project..."
: "Duplicating to environment..."}
</>
) : duplicateType === "new-project" ? (
"Duplicate project"
"Duplicate to new project"
) : (
"Duplicate services"
"Duplicate to environment"
)}
</Button>
</DialogFooter>

View File

@@ -0,0 +1,157 @@
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>
);
};

View File

@@ -101,7 +101,18 @@ export const HandleProject = ({ projectId }: Props) => {
toast.success(projectId ? "Project Updated" : "Project Created");
setIsOpen(false);
if (!projectId) {
router.push(`/dashboard/project/${data?.projectId}`);
const projectIdToUse =
data && "project" in data ? data.project.projectId : undefined;
const environmentIdToUse =
data && "environment" in data
? data.environment.environmentId
: undefined;
if (environmentIdToUse && projectIdToUse) {
router.push(
`/dashboard/project/${projectIdToUse}/environment/${environmentIdToUse}`,
);
}
} else {
refetch();
}

View File

@@ -96,22 +96,8 @@ export const ShowProjects = () => {
new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime();
break;
case "services": {
const aTotalServices =
a.mariadb.length +
a.mongo.length +
a.mysql.length +
a.postgres.length +
a.redis.length +
a.applications.length +
a.compose.length;
const bTotalServices =
b.mariadb.length +
b.mongo.length +
b.mysql.length +
b.postgres.length +
b.redis.length +
b.applications.length +
b.compose.length;
const aTotalServices = a.environments.length;
const bTotalServices = b.environments.length;
comparison = aTotalServices - bTotalServices;
break;
}
@@ -201,23 +187,40 @@ export const ShowProjects = () => {
)}
<div className="w-full grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4 3xl:grid-cols-5 flex-wrap gap-5">
{filteredProjects?.map((project) => {
const emptyServices =
project?.mariadb.length === 0 &&
project?.mongo.length === 0 &&
project?.mysql.length === 0 &&
project?.postgres.length === 0 &&
project?.redis.length === 0 &&
project?.applications.length === 0 &&
project?.compose.length === 0;
const emptyServices = project?.environments
.map(
(env) =>
env.applications.length === 0 &&
env.mariadb.length === 0 &&
env.mongo.length === 0 &&
env.mysql.length === 0 &&
env.postgres.length === 0 &&
env.redis.length === 0 &&
env.applications.length === 0 &&
env.compose.length === 0,
)
.every(Boolean);
const totalServices =
project?.mariadb.length +
project?.mongo.length +
project?.mysql.length +
project?.postgres.length +
project?.redis.length +
project?.applications.length +
project?.compose.length;
const totalServices = project?.environments
.map(
(env) =>
env.mariadb.length +
env.mongo.length +
env.mysql.length +
env.postgres.length +
env.redis.length +
env.applications.length +
env.compose.length,
)
.reduce((acc, curr) => acc + curr, 0);
const haveServicesWithDomains = project?.environments
.map(
(env) =>
env.applications.length > 0 ||
env.compose.length > 0,
)
.some(Boolean);
return (
<div
@@ -225,11 +228,10 @@ export const ShowProjects = () => {
className="w-full lg:max-w-md"
>
<Link
href={`/dashboard/project/${project.projectId}`}
href={`/dashboard/project/${project.projectId}/environment/${project?.environments?.[0]?.environmentId}`}
>
<Card className="group relative w-full h-full bg-transparent transition-colors hover:bg-border">
{project.applications.length > 0 ||
project.compose.length > 0 ? (
{haveServicesWithDomains ? (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
@@ -244,44 +246,51 @@ export const ShowProjects = () => {
className="w-[200px] space-y-2 overflow-y-auto max-h-[400px]"
onClick={(e) => e.stopPropagation()}
>
{project.applications.length > 0 && (
{project.environments.some(
(env) => env.applications.length > 0,
) && (
<DropdownMenuGroup>
<DropdownMenuLabel>
Applications
</DropdownMenuLabel>
{project.applications.map((app) => (
<div key={app.applicationId}>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuLabel className="font-normal capitalize text-xs flex items-center justify-between">
{app.name}
<StatusTooltip
status={app.applicationStatus}
/>
</DropdownMenuLabel>
{project.environments.map((env) =>
env.applications.map((app) => (
<div key={app.applicationId}>
<DropdownMenuSeparator />
{app.domains.map((domain) => (
<DropdownMenuItem
key={domain.domainId}
asChild
>
<Link
className="space-x-4 text-xs cursor-pointer justify-between"
target="_blank"
href={`${domain.https ? "https" : "http"}://${domain.host}${domain.path}`}
<DropdownMenuGroup>
<DropdownMenuLabel className="font-normal capitalize text-xs flex items-center justify-between">
{app.name}
<StatusTooltip
status={
app.applicationStatus
}
/>
</DropdownMenuLabel>
<DropdownMenuSeparator />
{app.domains.map((domain) => (
<DropdownMenuItem
key={domain.domainId}
asChild
>
<span className="truncate">
{domain.host}
</span>
<ExternalLinkIcon className="size-4 shrink-0" />
</Link>
</DropdownMenuItem>
))}
</DropdownMenuGroup>
</div>
))}
<Link
className="space-x-4 text-xs cursor-pointer justify-between"
target="_blank"
href={`${domain.https ? "https" : "http"}://${domain.host}${domain.path}`}
>
<span className="truncate">
{domain.host}
</span>
<ExternalLinkIcon className="size-4 shrink-0" />
</Link>
</DropdownMenuItem>
))}
</DropdownMenuGroup>
</div>
)),
)}
</DropdownMenuGroup>
)}
{/*
{project.compose.length > 0 && (
<DropdownMenuGroup>
<DropdownMenuLabel>
@@ -319,7 +328,7 @@ export const ShowProjects = () => {
</div>
))}
</DropdownMenuGroup>
)}
)} */}
</DropdownMenuContent>
</DropdownMenu>
) : null}

View File

@@ -20,10 +20,10 @@ import {
CommandSeparator,
} from "@/components/ui/command";
import { authClient } from "@/lib/auth-client";
import {
extractServices,
type Services,
} from "@/pages/dashboard/project/[projectId]";
// import {
// extractServices,
// type Services,
// } from "@/pages/dashboard/project/[projectId]";
import { api } from "@/utils/api";
import { StatusTooltip } from "../shared/status-tooltip";
@@ -51,7 +51,7 @@ export const SearchCommand = () => {
return (
<div>
<CommandDialog open={open} onOpenChange={setOpen}>
{/* <CommandDialog open={open} onOpenChange={setOpen}>
<CommandInput
placeholder={"Search projects or settings"}
value={search}
@@ -87,7 +87,7 @@ export const SearchCommand = () => {
key={application.id}
onSelect={() => {
router.push(
`/dashboard/project/${project.projectId}/services/${application.type}/${application.id}`,
`/dashboard/project/${project.projectId}/environment/${application.environmentId}/services/${application.type}/${application.id}`,
);
setOpen(false);
}}
@@ -181,7 +181,7 @@ export const SearchCommand = () => {
</CommandItem>
</CommandGroup>
</CommandList>
</CommandDialog>
</CommandDialog> */}
</div>
);
};

View File

@@ -65,6 +65,11 @@ export const AddCertificate = () => {
const { mutateAsync, isError, error, isLoading } =
api.certificates.create.useMutation();
const { data: servers } = api.server.withSSHKey.useQuery();
const hasServers = servers && servers.length > 0;
// Show dropdown logic based on cloud environment
// Cloud: show only if there are remote servers (no Dokploy option)
// Self-hosted: show only if there are remote servers (Dokploy is default, hide if no remote servers)
const shouldShowServerDropdown = hasServers;
const form = useForm<AddCertificate>({
defaultValues: {
@@ -85,7 +90,7 @@ export const AddCertificate = () => {
certificateData: data.certificateData,
privateKey: data.privateKey,
autoRenew: data.autoRenew,
serverId: data.serverId,
serverId: data.serverId === "dokploy" ? undefined : data.serverId,
organizationId: "",
})
.then(async () => {
@@ -174,52 +179,70 @@ export const AddCertificate = () => {
</FormItem>
)}
/>
<FormField
control={form.control}
name="serverId"
render={({ field }) => (
<FormItem>
<TooltipProvider delayDuration={0}>
<Tooltip>
<TooltipTrigger asChild>
<FormLabel className="break-all w-fit flex flex-row gap-1 items-center">
Select a Server {!isCloud && "(Optional)"}
<HelpCircle className="size-4 text-muted-foreground" />
</FormLabel>
</TooltipTrigger>
</Tooltip>
</TooltipProvider>
{shouldShowServerDropdown && (
<FormField
control={form.control}
name="serverId"
render={({ field }) => (
<FormItem>
<TooltipProvider delayDuration={0}>
<Tooltip>
<TooltipTrigger asChild>
<FormLabel className="break-all w-fit flex flex-row gap-1 items-center">
Select a Server {!isCloud && "(Optional)"}
<HelpCircle className="size-4 text-muted-foreground" />
</FormLabel>
</TooltipTrigger>
</Tooltip>
</TooltipProvider>
<Select
onValueChange={field.onChange}
defaultValue={field.value}
>
<SelectTrigger>
<SelectValue placeholder="Select a Server" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
{servers?.map((server) => (
<SelectItem
key={server.serverId}
value={server.serverId}
>
<span className="flex items-center gap-2 justify-between w-full">
<span>{server.name}</span>
<span className="text-muted-foreground text-xs self-center">
{server.ipAddress}
<Select
onValueChange={field.onChange}
defaultValue={
field.value || (!isCloud ? "dokploy" : undefined)
}
>
<SelectTrigger>
<SelectValue
placeholder={!isCloud ? "Dokploy" : "Select a Server"}
/>
</SelectTrigger>
<SelectContent>
<SelectGroup>
{!isCloud && (
<SelectItem value="dokploy">
<span className="flex items-center gap-2 justify-between w-full">
<span>Dokploy</span>
<span className="text-muted-foreground text-xs self-center">
Default
</span>
</span>
</span>
</SelectItem>
))}
<SelectLabel>Servers ({servers?.length})</SelectLabel>
</SelectGroup>
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
</SelectItem>
)}
{servers?.map((server) => (
<SelectItem
key={server.serverId}
value={server.serverId}
>
<span className="flex items-center gap-2 justify-between w-full">
<span>{server.name}</span>
<span className="text-muted-foreground text-xs self-center">
{server.ipAddress}
</span>
</span>
</SelectItem>
))}
<SelectLabel>
Servers ({servers?.length + (!isCloud ? 1 : 0)})
</SelectLabel>
</SelectGroup>
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
)}
</form>
<DialogFooter className="flex w-full flex-row !justify-end">

View File

@@ -35,9 +35,9 @@ import {
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import { extractServices } from "@/pages/dashboard/project/[projectId]";
import { api } from "@/utils/api";
import { useUrl } from "@/utils/hooks/use-url";
import { extractServices } from "../users/add-permissions";
interface Props {
serverId?: string;
@@ -95,11 +95,13 @@ export const SetupMonitoring = ({ serverId }: Props) => {
const { data: projects } = api.project.all.useQuery();
const extractServicesFromProjects = (projects: any[] | undefined) => {
const extractServicesFromProjects = () => {
if (!projects) return [];
const allServices = projects.flatMap((project) => {
const services = extractServices(project);
const services = project.environments.flatMap((env) =>
extractServices(env),
);
return serverId
? services
.filter((service) => service.serverId === serverId)
@@ -110,7 +112,7 @@ export const SetupMonitoring = ({ serverId }: Props) => {
return [...new Set(allServices)];
};
const services = extractServicesFromProjects(projects);
const services = extractServicesFromProjects();
const form = useForm<Schema>({
resolver: zodResolver(Schema),

View File

@@ -1,3 +1,4 @@
import type { findEnvironmentById } from "@dokploy/server/index";
import { zodResolver } from "@hookform/resolvers/zod";
import { useEffect } from "react";
import { useForm } from "react-hook-form";
@@ -26,11 +27,135 @@ import {
FormMessage,
} from "@/components/ui/form";
import { Switch } from "@/components/ui/switch";
import { extractServices } from "@/pages/dashboard/project/[projectId]";
import { api } from "@/utils/api";
type Environment = Omit<
Awaited<ReturnType<typeof findEnvironmentById>>,
"project"
>;
export type Services = {
appName: string;
serverId?: string | null;
name: string;
type:
| "mariadb"
| "application"
| "postgres"
| "mysql"
| "mongo"
| "redis"
| "compose";
description?: string | null;
id: string;
createdAt: string;
status?: "idle" | "running" | "done" | "error";
};
export const extractServices = (data: Environment | undefined) => {
const applications: Services[] =
data?.applications.map((item) => ({
appName: item.appName,
name: item.name,
type: "application",
id: item.applicationId,
createdAt: item.createdAt,
status: item.applicationStatus,
description: item.description,
serverId: item.serverId,
})) || [];
const mariadb: Services[] =
data?.mariadb.map((item) => ({
appName: item.appName,
name: item.name,
type: "mariadb",
id: item.mariadbId,
createdAt: item.createdAt,
status: item.applicationStatus,
description: item.description,
serverId: item.serverId,
})) || [];
const postgres: Services[] =
data?.postgres.map((item) => ({
appName: item.appName,
name: item.name,
type: "postgres",
id: item.postgresId,
createdAt: item.createdAt,
status: item.applicationStatus,
description: item.description,
serverId: item.serverId,
})) || [];
const mongo: Services[] =
data?.mongo.map((item) => ({
appName: item.appName,
name: item.name,
type: "mongo",
id: item.mongoId,
createdAt: item.createdAt,
status: item.applicationStatus,
description: item.description,
serverId: item.serverId,
})) || [];
const redis: Services[] =
data?.redis.map((item) => ({
appName: item.appName,
name: item.name,
type: "redis",
id: item.redisId,
createdAt: item.createdAt,
status: item.applicationStatus,
description: item.description,
serverId: item.serverId,
})) || [];
const mysql: Services[] =
data?.mysql.map((item) => ({
appName: item.appName,
name: item.name,
type: "mysql",
id: item.mysqlId,
createdAt: item.createdAt,
status: item.applicationStatus,
description: item.description,
serverId: item.serverId,
})) || [];
const compose: Services[] =
data?.compose.map((item) => ({
appName: item.appName,
name: item.name,
type: "compose",
id: item.composeId,
createdAt: item.createdAt,
status: item.composeStatus,
description: item.description,
serverId: item.serverId,
})) || [];
applications.push(
...mysql,
...redis,
...mongo,
...postgres,
...mariadb,
...compose,
);
applications.sort((a, b) => {
return new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime();
});
return applications;
};
const addPermissions = z.object({
accessedProjects: z.array(z.string()).optional(),
accessedEnvironments: z.array(z.string()).optional(),
accessedServices: z.array(z.string()).optional(),
canCreateProjects: z.boolean().optional().default(false),
canCreateServices: z.boolean().optional().default(false),
@@ -76,6 +201,7 @@ export const AddUserPermissions = ({ userId }: Props) => {
if (data) {
form.reset({
accessedProjects: data.accessedProjects || [],
accessedEnvironments: data.accessedEnvironments || [],
accessedServices: data.accessedServices || [],
canCreateProjects: data.canCreateProjects,
canCreateServices: data.canCreateServices,
@@ -99,6 +225,7 @@ export const AddUserPermissions = ({ userId }: Props) => {
canDeleteProjects: data.canDeleteProjects,
canAccessToTraefikFiles: data.canAccessToTraefikFiles,
accessedProjects: data.accessedProjects || [],
accessedEnvironments: data.accessedEnvironments || [],
accessedServices: data.accessedServices || [],
canAccessToDocker: data.canAccessToDocker,
canAccessToAPI: data.canAccessToAPI,
@@ -332,89 +459,317 @@ export const AddUserPermissions = ({ userId }: Props) => {
No projects found
</p>
)}
<div className="grid md:grid-cols-2 gap-4">
{projects?.map((item, index) => {
const applications = extractServices(item);
<div className="grid md:grid-cols-1 gap-4">
{projects?.map((project, projectIndex) => {
return (
<FormField
key={`project-${index}`}
key={`project-${projectIndex}`}
control={form.control}
name="accessedProjects"
render={({ field }) => {
return (
<FormItem
key={item.projectId}
className="flex flex-col items-start space-x-4 rounded-lg p-4 border"
key={project.projectId}
className="flex flex-col items-start rounded-lg p-4 border"
>
<div className="flex flex-row gap-4">
{/* Project Header */}
<div className="flex flex-row gap-4 items-center w-full">
<FormControl>
<Checkbox
checked={field.value?.includes(
item.projectId,
project.projectId,
)}
onCheckedChange={(checked) => {
return checked
? field.onChange([
...(field.value || []),
item.projectId,
])
: field.onChange(
field.value?.filter(
(value) =>
value !== item.projectId,
),
if (checked) {
// Add the project
field.onChange([
...(field.value || []),
project.projectId,
]);
} else {
// Remove the project
field.onChange(
field.value?.filter(
(value) =>
value !== project.projectId,
),
);
// Also remove all environments and services from this project
const currentEnvs =
form.getValues(
"accessedEnvironments",
) || [];
const currentServices =
form.getValues(
"accessedServices",
) || [];
// Get all environment IDs from this project
const projectEnvIds =
project.environments.map(
(env) => env.environmentId,
);
// Get all service IDs from this project
const projectServiceIds =
project.environments.flatMap(
(env) =>
extractServices(env).map(
(service) => service.id,
),
);
// Remove environments and services from this project
form.setValue(
"accessedEnvironments",
currentEnvs.filter(
(envId) =>
!projectEnvIds.includes(envId),
),
);
form.setValue(
"accessedServices",
currentServices.filter(
(serviceId) =>
!projectServiceIds.includes(
serviceId,
),
),
);
}
}}
/>
</FormControl>
<FormLabel className="text-sm font-medium text-primary">
{item.name}
<FormLabel className="text-base font-semibold text-primary">
{project.name}
</FormLabel>
</div>
{applications.length === 0 && (
<p className="text-sm text-muted-foreground">
No services found
</p>
)}
{applications?.map((item, index) => (
<FormField
key={`project-${index}`}
control={form.control}
name="accessedServices"
render={({ field }) => {
{/* Environments */}
<div className="ml-6 w-full space-y-3">
{project.environments.length === 0 && (
<p className="text-sm text-muted-foreground">
No environments found
</p>
)}
{project.environments.map(
(environment, envIndex) => {
const services =
extractServices(environment);
return (
<FormItem
key={item.id}
className="flex flex-row items-start space-x-3 space-y-0"
<div
key={`env-${envIndex}`}
className="border-l-2 border-muted pl-4"
>
<FormControl>
<Checkbox
checked={field.value?.includes(
item.id,
)}
onCheckedChange={(checked) => {
return checked
? field.onChange([
...(field.value || []),
item.id,
])
: field.onChange(
field.value?.filter(
(value) =>
value !== item.id,
),
{/* Environment Header with Checkbox */}
<FormField
key={`env-${envIndex}`}
control={form.control}
name="accessedEnvironments"
render={({ field: envField }) => (
<FormItem className="flex flex-row items-center space-x-3 space-y-0 mb-2">
<FormControl>
<Checkbox
checked={envField.value?.includes(
environment.environmentId,
)}
onCheckedChange={(
checked,
) => {
if (checked) {
// Add the environment
envField.onChange([
...(envField.value ||
[]),
environment.environmentId,
]);
// Auto-select the project if not already selected
const currentProjects =
form.getValues(
"accessedProjects",
) || [];
if (
!currentProjects.includes(
project.projectId,
)
) {
form.setValue(
"accessedProjects",
[
...currentProjects,
project.projectId,
],
);
}
} else {
// Remove the environment
envField.onChange(
envField.value?.filter(
(value) =>
value !==
environment.environmentId,
),
);
// Also remove all services from this environment
const currentServices =
form.getValues(
"accessedServices",
) || [];
const environmentServiceIds =
services.map(
(service) =>
service.id,
);
form.setValue(
"accessedServices",
currentServices.filter(
(serviceId) =>
!environmentServiceIds.includes(
serviceId,
),
),
);
}
}}
/>
</FormControl>
<div className="flex items-center gap-2">
<div className="w-2 h-2 bg-blue-500 rounded-full" />
<FormLabel className="text-sm font-medium text-foreground cursor-pointer">
{environment.name}
</FormLabel>
<span className="text-xs text-muted-foreground">
({services.length} services)
</span>
</div>
</FormItem>
)}
/>
{/* Services */}
<div className="ml-4 space-y-2">
{services.length === 0 && (
<p className="text-xs text-muted-foreground">
No services found
</p>
)}
{services.map(
(service, serviceIndex) => (
<FormField
key={`service-${serviceIndex}`}
control={form.control}
name="accessedServices"
render={({
field: serviceField,
}) => {
return (
<FormItem
key={service.id}
className="flex flex-row items-center space-x-3 space-y-0"
>
<FormControl>
<Checkbox
checked={serviceField.value?.includes(
service.id,
)}
onCheckedChange={(
checked,
) => {
if (checked) {
// Add the service
serviceField.onChange(
[
...(serviceField.value ||
[]),
service.id,
],
);
// Auto-select the environment if not already selected
const currentEnvs =
form.getValues(
"accessedEnvironments",
) || [];
if (
!currentEnvs.includes(
environment.environmentId,
)
) {
form.setValue(
"accessedEnvironments",
[
...currentEnvs,
environment.environmentId,
],
);
}
// Auto-select the project if not already selected
const currentProjects =
form.getValues(
"accessedProjects",
) || [];
if (
!currentProjects.includes(
project.projectId,
)
) {
form.setValue(
"accessedProjects",
[
...currentProjects,
project.projectId,
],
);
}
} else {
// Remove the service
serviceField.onChange(
serviceField.value?.filter(
(value) =>
value !==
service.id,
),
);
}
}}
/>
</FormControl>
<div className="flex items-center gap-2">
<div
className={`w-1.5 h-1.5 rounded-full ${
service.type ===
"application"
? "bg-green-500"
: service.type ===
"compose"
? "bg-purple-500"
: "bg-orange-500"
}`}
/>
<FormLabel className="text-sm text-muted-foreground cursor-pointer">
{service.name}
</FormLabel>
<span className="text-xs text-muted-foreground/70 capitalize">
({service.type})
</span>
</div>
</FormItem>
);
}}
/>
</FormControl>
<FormLabel className="text-sm text-muted-foreground">
{item.name}
</FormLabel>
</FormItem>
}}
/>
),
)}
</div>
</div>
);
}}
/>
))}
},
)}
</div>
</FormItem>
);
}}

View File

@@ -13,7 +13,7 @@ import { SidebarTrigger } from "@/components/ui/sidebar";
interface Props {
list: {
name: string;
href: string;
href?: string;
}[];
}
@@ -29,11 +29,11 @@ export const BreadcrumbSidebar = ({ list }: Props) => {
{list.map((item, index) => (
<Fragment key={item.name}>
<BreadcrumbItem className="block">
<BreadcrumbLink href={item.href} asChild={!!item.href}>
<BreadcrumbLink href={item?.href} asChild={!!item?.href}>
{item.href ? (
<Link href={item.href}>{item.name}</Link>
<Link href={item?.href}>{item?.name}</Link>
) : (
item.name
item?.name
)}
</BreadcrumbLink>
</BreadcrumbItem>

View File

@@ -1,25 +1,16 @@
import copy from "copy-to-clipboard";
import { Clipboard, EyeIcon, EyeOffIcon } from "lucide-react";
import { useRef, useState } from "react";
import { Clipboard } from "lucide-react";
import { useRef } from "react";
import { toast } from "sonner";
import { Button } from "../ui/button";
import { Input, type InputProps } from "../ui/input";
export const ToggleVisibilityInput = ({ ...props }: InputProps) => {
const [isPasswordVisible, setIsPasswordVisible] = useState(false);
const inputRef = useRef<HTMLInputElement>(null);
const togglePasswordVisibility = () => {
setIsPasswordVisible((prevVisibility) => !prevVisibility);
};
return (
<div className="flex w-full items-center space-x-2">
<Input
ref={inputRef}
type={isPasswordVisible ? "text" : "password"}
{...props}
/>
<Input ref={inputRef} type={"password"} {...props} />
<Button
variant={"secondary"}
onClick={() => {
@@ -29,13 +20,13 @@ export const ToggleVisibilityInput = ({ ...props }: InputProps) => {
>
<Clipboard className="size-4 text-muted-foreground" />
</Button>
<Button onClick={togglePasswordVisibility} variant={"secondary"}>
{/* <Button onClick={togglePasswordVisibility} variant={"secondary"}>
{isPasswordVisible ? (
<EyeOffIcon className="size-4 text-muted-foreground" />
) : (
<EyeIcon className="size-4 text-muted-foreground" />
)}
</Button>
</Button> */}
</div>
);
};

View File

@@ -1,3 +1,4 @@
import { EyeIcon, EyeOffIcon } from "lucide-react";
import * as React from "react";
import { cn } from "@/lib/utils";
@@ -8,18 +9,39 @@ export interface InputProps
const Input = React.forwardRef<HTMLInputElement, InputProps>(
({ className, errorMessage, type, ...props }, ref) => {
const [showPassword, setShowPassword] = React.useState(false);
const isPassword = type === "password";
const inputType = isPassword ? (showPassword ? "text" : "password") : type;
return (
<>
<input
type={type}
className={cn(
// bg-gray
"flex h-10 w-full rounded-md bg-input px-3 py-2 text-sm file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-border disabled:cursor-not-allowed disabled:opacity-50",
className,
<div className="relative w-full">
<input
type={inputType}
className={cn(
// bg-gray
"flex h-10 w-full rounded-md bg-input px-3 py-2 text-sm file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-border disabled:cursor-not-allowed disabled:opacity-50",
isPassword && "pr-10", // Add padding for the eye icon
className,
)}
ref={ref}
{...props}
/>
{isPassword && (
<button
type="button"
className="absolute inset-y-0 right-0 flex items-center pr-3 text-muted-foreground hover:text-foreground focus:outline-none"
onClick={() => setShowPassword(!showPassword)}
tabIndex={-1}
>
{showPassword ? (
<EyeOffIcon className="h-4 w-4" />
) : (
<EyeIcon className="h-4 w-4" />
)}
</button>
)}
ref={ref}
{...props}
/>
</div>
{errorMessage && (
<span className="text-sm text-red-600 text-secondary-foreground">
{errorMessage}

View File

@@ -0,0 +1,147 @@
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";

View File

@@ -0,0 +1 @@
ALTER TABLE "environment" ADD COLUMN "env" text DEFAULT '' NOT NULL;

View File

@@ -0,0 +1 @@
ALTER TABLE "member" ADD COLUMN "accessedEnvironments" text[] DEFAULT ARRAY[]::text[] NOT NULL;

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

View File

@@ -750,6 +750,27 @@
"when": 1754912062243,
"tag": "0106_purple_maggott",
"breakpoints": true
},
{
"idx": 107,
"version": "7",
"when": 1756793713380,
"tag": "0107_loud_kang",
"breakpoints": true
},
{
"idx": 108,
"version": "7",
"when": 1756955718127,
"tag": "0108_lazy_next_avengers",
"breakpoints": true
},
{
"idx": 109,
"version": "7",
"when": 1757052053574,
"tag": "0109_remarkable_sauron",
"breakpoints": true
}
]
}

View File

@@ -7,6 +7,10 @@ function prepareDefine(config: DotenvParseOutput | undefined) {
const define = {};
// @ts-ignore
for (const [key, value] of Object.entries(config)) {
// Skip DATABASE_URL to allow runtime environment variable override
if (key === "DATABASE_URL") {
continue;
}
// @ts-ignore
define[`process.env.${key}`] = JSON.stringify(value);
}
@@ -14,6 +18,7 @@ function prepareDefine(config: DotenvParseOutput | undefined) {
}
const define = prepareDefine(result.parsed);
try {
esbuild
.build({

View File

@@ -1,149 +0,0 @@
// import { drizzle } from "drizzle-orm/postgres-js";
// import { nanoid } from "nanoid";
// import postgres from "postgres";
// import * as schema from "./server/db/schema";
// const connectionString = process.env.DATABASE_URL!;
// const sql = postgres(connectionString, { max: 1 });
// const db = drizzle(sql, {
// schema,
// });
// await db
// .transaction(async (db) => {
// const admins = await db.query.admins.findMany({
// with: {
// auth: true,
// users: {
// with: {
// auth: true,
// },
// },
// },
// });
// for (const admin of admins) {
// const user = await db
// .insert(schema.users_temp)
// .values({
// id: admin.adminId,
// email: admin.auth.email,
// token: admin.auth.token || "",
// emailVerified: true,
// updatedAt: new Date(),
// role: "admin",
// serverIp: admin.serverIp,
// image: admin.auth.image,
// certificateType: admin.certificateType,
// host: admin.host,
// letsEncryptEmail: admin.letsEncryptEmail,
// sshPrivateKey: admin.sshPrivateKey,
// enableDockerCleanup: admin.enableDockerCleanup,
// enableLogRotation: admin.enableLogRotation,
// enablePaidFeatures: admin.enablePaidFeatures,
// metricsConfig: admin.metricsConfig,
// cleanupCacheApplications: admin.cleanupCacheApplications,
// cleanupCacheOnPreviews: admin.cleanupCacheOnPreviews,
// cleanupCacheOnCompose: admin.cleanupCacheOnCompose,
// stripeCustomerId: admin.stripeCustomerId,
// stripeSubscriptionId: admin.stripeSubscriptionId,
// serversQuantity: admin.serversQuantity,
// })
// .returning()
// .then((user) => user[0]);
// await db.insert(schema.account).values({
// providerId: "credential",
// userId: user?.id || "",
// password: admin.auth.password,
// is2FAEnabled: admin.auth.is2FAEnabled || false,
// createdAt: new Date(admin.auth.createdAt) || new Date(),
// updatedAt: new Date(admin.auth.createdAt) || new Date(),
// });
// const organization = await db
// .insert(schema.organization)
// .values({
// name: "My Organization",
// slug: nanoid(),
// ownerId: user?.id || "",
// createdAt: new Date(admin.createdAt) || new Date(),
// })
// .returning()
// .then((organization) => organization[0]);
// for (const member of admin.users) {
// const userTemp = await db
// .insert(schema.users_temp)
// .values({
// id: member.userId,
// email: member.auth.email,
// token: member.token || "",
// emailVerified: true,
// updatedAt: new Date(admin.createdAt) || new Date(),
// role: "user",
// image: member.auth.image,
// createdAt: admin.createdAt,
// canAccessToAPI: member.canAccessToAPI || false,
// canAccessToDocker: member.canAccessToDocker || false,
// canAccessToGitProviders: member.canAccessToGitProviders || false,
// canAccessToSSHKeys: member.canAccessToSSHKeys || false,
// canAccessToTraefikFiles: member.canAccessToTraefikFiles || false,
// canCreateProjects: member.canCreateProjects || false,
// canCreateServices: member.canCreateServices || false,
// canDeleteProjects: member.canDeleteProjects || false,
// canDeleteServices: member.canDeleteServices || false,
// accessedProjects: member.accessedProjects || [],
// accessedServices: member.accessedServices || [],
// })
// .returning()
// .then((userTemp) => userTemp[0]);
// await db.insert(schema.account).values({
// providerId: "credential",
// userId: member?.userId || "",
// password: member.auth.password,
// is2FAEnabled: member.auth.is2FAEnabled || false,
// createdAt: new Date(member.auth.createdAt) || new Date(),
// updatedAt: new Date(member.auth.createdAt) || new Date(),
// });
// await db.insert(schema.member).values({
// organizationId: organization?.id || "",
// userId: userTemp?.id || "",
// role: "admin",
// createdAt: new Date(member.createdAt) || new Date(),
// });
// }
// }
// })
// .then(() => {
// console.log("Migration finished");
// })
// .catch((error) => {
// console.error(error);
// });
// await db
// .transaction(async (db) => {
// const projects = await db.query.projects.findMany({
// with: {
// user: {
// with: {
// organizations: true,
// },
// },
// },
// });
// for (const project of projects) {
// const _user = await db.update(schema.projects).set({
// organizationId: project.user.organizations[0]?.id || "",
// });
// }
// })
// .then(() => {
// console.log("Migration finished");
// })
// .catch((error) => {
// console.error(error);
// });

View File

@@ -20,7 +20,11 @@ export default async function handler(
const application = await db.query.applications.findFirst({
where: eq(applications.refreshToken, refreshToken as string),
with: {
project: true,
environment: {
with: {
project: true,
},
},
bitbucket: true,
},
});

View File

@@ -27,7 +27,11 @@ export default async function handler(
const composeResult = await db.query.compose.findFirst({
where: eq(compose.refreshToken, refreshToken as string),
with: {
project: true,
environment: {
with: {
project: true,
},
},
bitbucket: true,
},
});

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -71,7 +71,7 @@ const Service = (
const [_toggleMonitoring, _setToggleMonitoring] = useState(false);
const { applicationId, activeTab } = props;
const router = useRouter();
const { projectId } = router.query;
const { projectId, environmentId } = router.query;
const [tab, setTab] = useState<TabState>(activeTab);
useEffect(() => {
@@ -97,18 +97,20 @@ const Service = (
list={[
{ name: "Projects", href: "/dashboard/projects" },
{
name: data?.project?.name || "",
href: `/dashboard/project/${projectId}`,
name: data?.environment.project.name || "",
},
{
name: data?.environment?.name || "",
href: `/dashboard/project/${projectId}/environment/${environmentId}`,
},
{
name: data?.name || "",
href: `/dashboard/project/${projectId}/services/application/${applicationId}`,
},
]}
/>
<Head>
<title>
Application: {data?.name} - {data?.project.name} | Dokploy
Application: {data?.name} - {data?.environment.project.name} | Dokploy
</title>
</Head>
<div className="w-full">
@@ -215,7 +217,7 @@ const Service = (
className="w-full"
onValueChange={(e) => {
setTab(e as TabState);
const newPath = `/dashboard/project/${projectId}/services/application/${applicationId}?tab=${e}`;
const newPath = `/dashboard/project/${projectId}/environment/${environmentId}/services/application/${applicationId}?tab=${e}`;
router.push(newPath);
}}
>
@@ -379,6 +381,7 @@ export async function getServerSideProps(
ctx: GetServerSidePropsContext<{
applicationId: string;
activeTab: TabState;
environmentId: string;
}>,
) {
const { query, params, req, res } = ctx;
@@ -420,6 +423,7 @@ export async function getServerSideProps(
trpcState: helpers.dehydrate(),
applicationId: params?.applicationId,
activeTab: (activeTab || "general") as TabState,
environmentId: params?.environmentId,
},
};
} catch {

View File

@@ -67,7 +67,7 @@ const Service = (
const [_toggleMonitoring, _setToggleMonitoring] = useState(false);
const { composeId, activeTab } = props;
const router = useRouter();
const { projectId } = router.query;
const { projectId, environmentId } = router.query;
const [tab, setTab] = useState<TabState>(activeTab);
useEffect(() => {
@@ -88,18 +88,20 @@ const Service = (
list={[
{ name: "Projects", href: "/dashboard/projects" },
{
name: data?.project?.name || "",
href: `/dashboard/project/${projectId}`,
name: data?.environment?.project?.name || "",
},
{
name: data?.environment?.name || "",
href: `/dashboard/project/${projectId}/environment/${environmentId}`,
},
{
name: data?.name || "",
href: `/dashboard/project/${projectId}/services/compose/${composeId}`,
},
]}
/>
<Head>
<title>
Compose: {data?.name} - {data?.project.name} | Dokploy
Compose: {data?.name} - {data?.environment?.project?.name} | Dokploy
</title>
</Head>
<div className="w-full">
@@ -208,7 +210,7 @@ const Service = (
className="w-full"
onValueChange={(e) => {
setTab(e as TabState);
const newPath = `/dashboard/project/${projectId}/services/compose/${composeId}?tab=${e}`;
const newPath = `/dashboard/project/${projectId}/environment/${environmentId}/services/compose/${composeId}?tab=${e}`;
router.push(newPath);
}}
>
@@ -375,6 +377,7 @@ export async function getServerSideProps(
ctx: GetServerSidePropsContext<{
composeId: string;
activeTab: TabState;
environmentId: string;
}>,
) {
const { query, params, req, res } = ctx;
@@ -414,6 +417,7 @@ export async function getServerSideProps(
trpcState: helpers.dehydrate(),
composeId: params?.composeId,
activeTab: (activeTab || "general") as TabState,
environmentId: params?.environmentId,
},
};
} catch {

View File

@@ -55,7 +55,7 @@ const Mariadb = (
const { mariadbId, activeTab } = props;
const router = useRouter();
const { projectId } = router.query;
const { projectId, environmentId } = router.query;
const [tab, setSab] = useState<TabState>(activeTab);
const { data } = api.mariadb.one.useQuery({ mariadbId });
const { data: auth } = api.user.get.useQuery();
@@ -69,19 +69,22 @@ const Mariadb = (
list={[
{ name: "Projects", href: "/dashboard/projects" },
{
name: data?.project?.name || "",
href: `/dashboard/project/${projectId}`,
name: data?.environment?.project?.name || "",
},
{
name: data?.environment?.name || "",
href: `/dashboard/project/${projectId}/environment/${environmentId}`,
},
{
name: data?.name || "",
href: `/dashboard/project/${projectId}/services/mariadb/${mariadbId}`,
},
]}
/>
<div className="flex flex-col gap-4">
<Head>
<title>
Database: {data?.name} - {data?.project.name} | Dokploy
Database: {data?.name} - {data?.environment?.project?.name} |
Dokploy
</title>
</Head>
<Card className="h-full bg-sidebar p-2.5 rounded-xl w-full">
@@ -179,7 +182,7 @@ const Mariadb = (
className="w-full"
onValueChange={(e) => {
setSab(e as TabState);
const newPath = `/dashboard/project/${projectId}/services/mariadb/${mariadbId}?tab=${e}`;
const newPath = `/dashboard/project/${projectId}/environment/${environmentId}/services/mariadb/${mariadbId}?tab=${e}`;
router.push(newPath, undefined, { shallow: true });
}}
@@ -300,7 +303,11 @@ Mariadb.getLayout = (page: ReactElement) => {
};
export async function getServerSideProps(
ctx: GetServerSidePropsContext<{ mariadbId: string; activeTab: TabState }>,
ctx: GetServerSidePropsContext<{
mariadbId: string;
activeTab: TabState;
environmentId: string;
}>,
) {
const { query, params, req, res } = ctx;
const activeTab = query.tab;
@@ -338,6 +345,7 @@ export async function getServerSideProps(
trpcState: helpers.dehydrate(),
mariadbId: params?.mariadbId,
activeTab: (activeTab || "general") as TabState,
environmentId: params?.environmentId,
},
};
} catch {

View File

@@ -54,7 +54,7 @@ const Mongo = (
const [_toggleMonitoring, _setToggleMonitoring] = useState(false);
const { mongoId, activeTab } = props;
const router = useRouter();
const { projectId } = router.query;
const { projectId, environmentId } = router.query;
const [tab, setSab] = useState<TabState>(activeTab);
const { data } = api.mongo.one.useQuery({ mongoId });
@@ -69,18 +69,20 @@ const Mongo = (
list={[
{ name: "Projects", href: "/dashboard/projects" },
{
name: data?.project?.name || "",
href: `/dashboard/project/${projectId}`,
name: data?.environment?.project?.name || "",
},
{
name: data?.environment?.name || "",
href: `/dashboard/project/${projectId}/environment/${environmentId}`,
},
{
name: data?.name || "",
href: `/dashboard/project/${projectId}/services/mongo/${mongoId}`,
},
]}
/>
<Head>
<title>
Database: {data?.name} - {data?.project.name} | Dokploy
Database: {data?.name} - {data?.environment?.project?.name} | Dokploy
</title>
</Head>
<div className="w-full">
@@ -180,7 +182,7 @@ const Mongo = (
className="w-full"
onValueChange={(e) => {
setSab(e as TabState);
const newPath = `/dashboard/project/${projectId}/services/mongo/${mongoId}?tab=${e}`;
const newPath = `/dashboard/project/${projectId}/environment/${environmentId}/services/mongo/${mongoId}?tab=${e}`;
router.push(newPath, undefined, { shallow: true });
}}
@@ -302,7 +304,11 @@ Mongo.getLayout = (page: ReactElement) => {
};
export async function getServerSideProps(
ctx: GetServerSidePropsContext<{ mongoId: string; activeTab: TabState }>,
ctx: GetServerSidePropsContext<{
mongoId: string;
activeTab: TabState;
environmentId: string;
}>,
) {
const { query, params, req, res } = ctx;
const activeTab = query.tab;
@@ -340,6 +346,7 @@ export async function getServerSideProps(
trpcState: helpers.dehydrate(),
mongoId: params?.mongoId,
activeTab: (activeTab || "general") as TabState,
environmentId: params?.environmentId,
},
};
} catch {

View File

@@ -54,7 +54,7 @@ const MySql = (
const [_toggleMonitoring, _setToggleMonitoring] = useState(false);
const { mysqlId, activeTab } = props;
const router = useRouter();
const { projectId } = router.query;
const { projectId, environmentId } = router.query;
const [tab, setSab] = useState<TabState>(activeTab);
const { data } = api.mysql.one.useQuery({ mysqlId });
const { data: auth } = api.user.get.useQuery();
@@ -68,19 +68,22 @@ const MySql = (
list={[
{ name: "Projects", href: "/dashboard/projects" },
{
name: data?.project?.name || "",
href: `/dashboard/project/${projectId}`,
name: data?.environment?.project?.name || "",
},
{
name: data?.environment?.name || "",
href: `/dashboard/project/${projectId}/environment/${environmentId}`,
},
{
name: data?.name || "",
href: `/dashboard/project/${projectId}/services/mysql/${mysqlId}`,
},
]}
/>
<div className="flex flex-col gap-4">
<Head>
<title>
Database: {data?.name} - {data?.project.name} | Dokploy
Database: {data?.name} - {data?.environment?.project?.name} |
Dokploy
</title>
</Head>
<div className="w-full">
@@ -180,7 +183,7 @@ const MySql = (
className="w-full"
onValueChange={(e) => {
setSab(e as TabState);
const newPath = `/dashboard/project/${projectId}/services/mysql/${mysqlId}?tab=${e}`;
const newPath = `/dashboard/project/${projectId}/environment/${environmentId}/services/mysql/${mysqlId}?tab=${e}`;
router.push(newPath, undefined, { shallow: true });
}}
@@ -286,7 +289,11 @@ MySql.getLayout = (page: ReactElement) => {
};
export async function getServerSideProps(
ctx: GetServerSidePropsContext<{ mysqlId: string; activeTab: TabState }>,
ctx: GetServerSidePropsContext<{
mysqlId: string;
activeTab: TabState;
environmentId: string;
}>,
) {
const { query, params, req, res } = ctx;
const activeTab = query.tab;

View File

@@ -54,7 +54,7 @@ const Postgresql = (
const [_toggleMonitoring, _setToggleMonitoring] = useState(false);
const { postgresId, activeTab } = props;
const router = useRouter();
const { projectId } = router.query;
const { projectId, environmentId } = router.query;
const [tab, setSab] = useState<TabState>(activeTab);
const { data } = api.postgres.one.useQuery({ postgresId });
const { data: auth } = api.user.get.useQuery();
@@ -68,18 +68,20 @@ const Postgresql = (
list={[
{ name: "Projects", href: "/dashboard/projects" },
{
name: data?.project?.name || "",
href: `/dashboard/project/${projectId}`,
name: data?.environment?.project?.name || "",
},
{
name: data?.environment?.name || "",
href: `/dashboard/project/${projectId}/environment/${environmentId}`,
},
{
name: data?.name || "",
href: `/dashboard/project/${projectId}/services/postgres/${postgresId}`,
},
]}
/>
<Head>
<title>
Database: {data?.name} - {data?.project.name} | Dokploy
Database: {data?.name} - {data?.environment?.project?.name} | Dokploy
</title>
</Head>
<div className="w-full">
@@ -179,9 +181,11 @@ const Postgresql = (
className="w-full"
onValueChange={(e) => {
setSab(e as TabState);
const newPath = `/dashboard/project/${projectId}/services/postgres/${postgresId}?tab=${e}`;
const newPath = `/dashboard/project/${projectId}/environment/${environmentId}/services/postgres/${postgresId}?tab=${e}`;
router.push(newPath, undefined, { shallow: true });
router.push(newPath, undefined, {
shallow: true,
});
}}
>
<div className="flex flex-row items-center justify-between w-full gap-4 overflow-x-scroll">
@@ -228,7 +232,11 @@ const Postgresql = (
{data?.serverId && isCloud ? (
<ContainerPaidMonitoring
appName={data?.appName || ""}
baseUrl={`${data?.serverId ? `http://${data?.server?.ipAddress}:${data?.server?.metricsConfig?.server?.port}` : "http://localhost:4500"}`}
baseUrl={`${
data?.serverId
? `http://${data?.server?.ipAddress}:${data?.server?.metricsConfig?.server?.port}`
: "http://localhost:4500"
}`}
token={
data?.server?.metricsConfig?.server?.token || ""
}
@@ -284,7 +292,11 @@ Postgresql.getLayout = (page: ReactElement) => {
};
export async function getServerSideProps(
ctx: GetServerSidePropsContext<{ postgresId: string; activeTab: TabState }>,
ctx: GetServerSidePropsContext<{
postgresId: string;
activeTab: TabState;
environmentId: string;
}>,
) {
const { query, params, req, res } = ctx;
const activeTab = query.tab;

View File

@@ -53,7 +53,7 @@ const Redis = (
const [_toggleMonitoring, _setToggleMonitoring] = useState(false);
const { redisId, activeTab } = props;
const router = useRouter();
const { projectId } = router.query;
const { projectId, environmentId } = router.query;
const [tab, setSab] = useState<TabState>(activeTab);
const { data } = api.redis.one.useQuery({ redisId });
@@ -68,18 +68,20 @@ const Redis = (
list={[
{ name: "Projects", href: "/dashboard/projects" },
{
name: data?.project?.name || "",
href: `/dashboard/project/${projectId}`,
name: data?.environment?.project?.name || "",
},
{
name: data?.environment?.name || "",
href: `/dashboard/project/${projectId}/environment/${environmentId}`,
},
{
name: data?.name || "",
href: `/dashboard/project/${projectId}/services/redis/${redisId}`,
},
]}
/>
<Head>
<title>
Database: {data?.name} - {data?.project.name} | Dokploy
Database: {data?.name} - {data?.environment?.project?.name} | Dokploy
</title>
</Head>
<div className="w-full">
@@ -179,7 +181,7 @@ const Redis = (
className="w-full"
onValueChange={(e) => {
setSab(e as TabState);
const newPath = `/dashboard/project/${projectId}/services/redis/${redisId}?tab=${e}`;
const newPath = `/dashboard/project/${projectId}/environment/${environmentId}/services/redis/${redisId}?tab=${e}`;
router.push(newPath, undefined, { shallow: true });
}}
@@ -291,7 +293,11 @@ Redis.getLayout = (page: ReactElement) => {
};
export async function getServerSideProps(
ctx: GetServerSidePropsContext<{ redisId: string; activeTab: TabState }>,
ctx: GetServerSidePropsContext<{
redisId: string;
activeTab: TabState;
environmentId: string;
}>,
) {
const { query, params, req, res } = ctx;
const activeTab = query.tab;

View File

@@ -11,6 +11,7 @@ import { deploymentRouter } from "./routers/deployment";
import { destinationRouter } from "./routers/destination";
import { dockerRouter } from "./routers/docker";
import { domainRouter } from "./routers/domain";
import { environmentRouter } from "./routers/environment";
import { gitProviderRouter } from "./routers/git-provider";
import { giteaRouter } from "./routers/gitea";
import { githubRouter } from "./routers/github";
@@ -84,6 +85,7 @@ export const appRouter = createTRPCRouter({
schedule: scheduleRouter,
rollback: rollbackRouter,
volumeBackups: volumeBackupsRouter,
environment: environmentRouter,
});
// export type definition of API

View File

@@ -4,7 +4,11 @@ import {
apiUpdateAi,
deploySuggestionSchema,
} from "@dokploy/server/db/schema/ai";
import { createDomain, createMount } from "@dokploy/server/index";
import {
createDomain,
createMount,
findEnvironmentById,
} from "@dokploy/server/index";
import {
deleteAiSettings,
getAiSettingById,
@@ -177,10 +181,12 @@ export const aiRouter = createTRPCRouter({
deploy: protectedProcedure
.input(deploySuggestionSchema)
.mutation(async ({ ctx, input }) => {
const environment = await findEnvironmentById(input.environmentId);
const project = await findProjectById(environment.projectId);
if (ctx.user.role === "member") {
await checkServiceAccess(
ctx.session.activeOrganizationId,
input.projectId,
environment.projectId,
"create",
);
}
@@ -192,8 +198,6 @@ export const aiRouter = createTRPCRouter({
});
}
const project = await findProjectById(input.projectId);
const projectName = slugify(`${project.name} ${input.id}`);
const compose = await createComposeByTemplate({
@@ -205,6 +209,7 @@ export const aiRouter = createTRPCRouter({
sourceType: "raw",
appName: `${projectName}-${generatePassword(6)}`,
isolatedDeployment: true,
environmentId: input.environmentId,
});
if (input.domains && input.domains?.length > 0) {

View File

@@ -4,6 +4,7 @@ import {
createApplication,
deleteAllMiddlewares,
findApplicationById,
findEnvironmentById,
findGitProviderById,
findProjectById,
getApplicationStats,
@@ -39,8 +40,10 @@ import {
import { db } from "@/server/db";
import {
apiCreateApplication,
apiDeployApplication,
apiFindMonitoringStats,
apiFindOneApplication,
apiRedeployApplication,
apiReloadApplication,
apiSaveBitbucketProvider,
apiSaveBuildType,
@@ -63,10 +66,14 @@ export const applicationRouter = createTRPCRouter({
.input(apiCreateApplication)
.mutation(async ({ input, ctx }) => {
try {
// Get project from environment
const environment = await findEnvironmentById(input.environmentId);
const project = await findProjectById(environment.projectId);
if (ctx.user.role === "member") {
await checkServiceAccess(
ctx.user.id,
input.projectId,
project.projectId,
ctx.session.activeOrganizationId,
"create",
);
@@ -79,13 +86,13 @@ export const applicationRouter = createTRPCRouter({
});
}
const project = await findProjectById(input.projectId);
if (project.organizationId !== ctx.session.activeOrganizationId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to access this project",
});
}
const newApplication = await createApplication(input);
if (ctx.user.role === "member") {
@@ -97,6 +104,7 @@ export const applicationRouter = createTRPCRouter({
}
return newApplication;
} catch (error: unknown) {
console.log("error", error);
if (error instanceof TRPCError) {
throw error;
}
@@ -120,7 +128,8 @@ export const applicationRouter = createTRPCRouter({
}
const application = await findApplicationById(input.applicationId);
if (
application.project.organizationId !== ctx.session.activeOrganizationId
application.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
@@ -175,7 +184,7 @@ export const applicationRouter = createTRPCRouter({
try {
if (
application.project.organizationId !==
application.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
@@ -212,7 +221,8 @@ export const applicationRouter = createTRPCRouter({
const application = await findApplicationById(input.applicationId);
if (
application.project.organizationId !== ctx.session.activeOrganizationId
application.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
@@ -247,14 +257,17 @@ export const applicationRouter = createTRPCRouter({
} catch (_) {}
}
return result[0];
return application;
}),
stop: protectedProcedure
.input(apiFindOneApplication)
.mutation(async ({ input, ctx }) => {
const service = await findApplicationById(input.applicationId);
if (service.project.organizationId !== ctx.session.activeOrganizationId) {
if (
service.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to stop this application",
@@ -274,7 +287,10 @@ export const applicationRouter = createTRPCRouter({
.input(apiFindOneApplication)
.mutation(async ({ input, ctx }) => {
const service = await findApplicationById(input.applicationId);
if (service.project.organizationId !== ctx.session.activeOrganizationId) {
if (
service.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to start this application",
@@ -292,11 +308,12 @@ export const applicationRouter = createTRPCRouter({
}),
redeploy: protectedProcedure
.input(apiFindOneApplication)
.input(apiRedeployApplication)
.mutation(async ({ input, ctx }) => {
const application = await findApplicationById(input.applicationId);
if (
application.project.organizationId !== ctx.session.activeOrganizationId
application.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
@@ -305,8 +322,8 @@ export const applicationRouter = createTRPCRouter({
}
const jobData: DeploymentJob = {
applicationId: input.applicationId,
titleLog: "Rebuild deployment",
descriptionLog: "",
titleLog: input.title || "Rebuild deployment",
descriptionLog: input.description || "",
type: "redeploy",
applicationType: "application",
server: !!application.serverId,
@@ -331,7 +348,8 @@ export const applicationRouter = createTRPCRouter({
.mutation(async ({ input, ctx }) => {
const application = await findApplicationById(input.applicationId);
if (
application.project.organizationId !== ctx.session.activeOrganizationId
application.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
@@ -349,7 +367,8 @@ export const applicationRouter = createTRPCRouter({
.mutation(async ({ input, ctx }) => {
const application = await findApplicationById(input.applicationId);
if (
application.project.organizationId !== ctx.session.activeOrganizationId
application.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
@@ -374,7 +393,8 @@ export const applicationRouter = createTRPCRouter({
.mutation(async ({ input, ctx }) => {
const application = await findApplicationById(input.applicationId);
if (
application.project.organizationId !== ctx.session.activeOrganizationId
application.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
@@ -401,7 +421,8 @@ export const applicationRouter = createTRPCRouter({
.mutation(async ({ input, ctx }) => {
const application = await findApplicationById(input.applicationId);
if (
application.project.organizationId !== ctx.session.activeOrganizationId
application.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
@@ -429,7 +450,8 @@ export const applicationRouter = createTRPCRouter({
.mutation(async ({ input, ctx }) => {
const application = await findApplicationById(input.applicationId);
if (
application.project.organizationId !== ctx.session.activeOrganizationId
application.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
@@ -455,7 +477,8 @@ export const applicationRouter = createTRPCRouter({
.mutation(async ({ input, ctx }) => {
const application = await findApplicationById(input.applicationId);
if (
application.project.organizationId !== ctx.session.activeOrganizationId
application.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
@@ -481,7 +504,8 @@ export const applicationRouter = createTRPCRouter({
.mutation(async ({ input, ctx }) => {
const application = await findApplicationById(input.applicationId);
if (
application.project.organizationId !== ctx.session.activeOrganizationId
application.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
@@ -504,7 +528,8 @@ export const applicationRouter = createTRPCRouter({
.mutation(async ({ input, ctx }) => {
const application = await findApplicationById(input.applicationId);
if (
application.project.organizationId !== ctx.session.activeOrganizationId
application.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
@@ -529,7 +554,8 @@ export const applicationRouter = createTRPCRouter({
.mutation(async ({ input, ctx }) => {
const application = await findApplicationById(input.applicationId);
if (
application.project.organizationId !== ctx.session.activeOrganizationId
application.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
@@ -590,7 +616,8 @@ export const applicationRouter = createTRPCRouter({
.mutation(async ({ input, ctx }) => {
const application = await findApplicationById(input.applicationId);
if (
application.project.organizationId !== ctx.session.activeOrganizationId
application.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
@@ -604,7 +631,8 @@ export const applicationRouter = createTRPCRouter({
.mutation(async ({ input, ctx }) => {
const application = await findApplicationById(input.applicationId);
if (
application.project.organizationId !== ctx.session.activeOrganizationId
application.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
@@ -630,7 +658,8 @@ export const applicationRouter = createTRPCRouter({
.mutation(async ({ input, ctx }) => {
const application = await findApplicationById(input.applicationId);
if (
application.project.organizationId !== ctx.session.activeOrganizationId
application.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
@@ -643,11 +672,12 @@ export const applicationRouter = createTRPCRouter({
return true;
}),
deploy: protectedProcedure
.input(apiFindOneApplication)
.input(apiDeployApplication)
.mutation(async ({ input, ctx }) => {
const application = await findApplicationById(input.applicationId);
if (
application.project.organizationId !== ctx.session.activeOrganizationId
application.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
@@ -656,8 +686,8 @@ export const applicationRouter = createTRPCRouter({
}
const jobData: DeploymentJob = {
applicationId: input.applicationId,
titleLog: "Manual deployment",
descriptionLog: "",
titleLog: input.title || "Manual deployment",
descriptionLog: input.description || "",
type: "deploy",
applicationType: "application",
server: !!application.serverId,
@@ -683,7 +713,8 @@ export const applicationRouter = createTRPCRouter({
.mutation(async ({ input, ctx }) => {
const application = await findApplicationById(input.applicationId);
if (
application.project.organizationId !== ctx.session.activeOrganizationId
application.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
@@ -698,7 +729,8 @@ export const applicationRouter = createTRPCRouter({
.query(async ({ input, ctx }) => {
const application = await findApplicationById(input.applicationId);
if (
application.project.organizationId !== ctx.session.activeOrganizationId
application.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
@@ -734,7 +766,10 @@ export const applicationRouter = createTRPCRouter({
const app = await findApplicationById(input.applicationId as string);
if (app.project.organizationId !== ctx.session.activeOrganizationId) {
if (
app.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to deploy this application",
@@ -777,7 +812,8 @@ export const applicationRouter = createTRPCRouter({
const application = await findApplicationById(input.applicationId);
if (
application.project.organizationId !== ctx.session.activeOrganizationId
application.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
@@ -813,13 +849,14 @@ export const applicationRouter = createTRPCRouter({
.input(
z.object({
applicationId: z.string(),
targetProjectId: z.string(),
targetEnvironmentId: z.string(),
}),
)
.mutation(async ({ input, ctx }) => {
const application = await findApplicationById(input.applicationId);
if (
application.project.organizationId !== ctx.session.activeOrganizationId
application.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
@@ -827,11 +864,16 @@ export const applicationRouter = createTRPCRouter({
});
}
const targetProject = await findProjectById(input.targetProjectId);
if (targetProject.organizationId !== ctx.session.activeOrganizationId) {
const targetEnvironment = await findEnvironmentById(
input.targetEnvironmentId,
);
if (
targetEnvironment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to move to this project",
message: "You are not authorized to move to this environment",
});
}
@@ -839,7 +881,7 @@ export const applicationRouter = createTRPCRouter({
const updatedApplication = await db
.update(applications)
.set({
projectId: input.targetProjectId,
environmentId: input.targetEnvironmentId,
})
.where(eq(applications.applicationId, input.applicationId))
.returning()

View File

@@ -12,6 +12,7 @@ import {
deleteMount,
findComposeById,
findDomainsByComposeId,
findEnvironmentById,
findGitProviderById,
findProjectById,
findServerById,
@@ -47,9 +48,11 @@ import { db } from "@/server/db";
import {
apiCreateCompose,
apiDeleteCompose,
apiDeployCompose,
apiFetchServices,
apiFindCompose,
apiRandomizeCompose,
apiRedeployCompose,
apiUpdateCompose,
compose as composeTable,
} from "@/server/db/schema";
@@ -64,10 +67,14 @@ export const composeRouter = createTRPCRouter({
.input(apiCreateCompose)
.mutation(async ({ ctx, input }) => {
try {
// Get project from environment
const environment = await findEnvironmentById(input.environmentId);
const project = await findProjectById(environment.projectId);
if (ctx.user.role === "member") {
await checkServiceAccess(
ctx.user.id,
input.projectId,
project.projectId,
ctx.session.activeOrganizationId,
"create",
);
@@ -79,14 +86,15 @@ export const composeRouter = createTRPCRouter({
message: "You need to use a server to create a compose",
});
}
const project = await findProjectById(input.projectId);
if (project.organizationId !== ctx.session.activeOrganizationId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to access this project",
});
}
const newService = await createCompose(input);
const newService = await createCompose({
...input,
});
if (ctx.user.role === "member") {
await addNewService(
@@ -115,7 +123,10 @@ export const composeRouter = createTRPCRouter({
}
const compose = await findComposeById(input.composeId);
if (compose.project.organizationId !== ctx.session.activeOrganizationId) {
if (
compose.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to access this compose",
@@ -166,7 +177,10 @@ export const composeRouter = createTRPCRouter({
.input(apiUpdateCompose)
.mutation(async ({ input, ctx }) => {
const compose = await findComposeById(input.composeId);
if (compose.project.organizationId !== ctx.session.activeOrganizationId) {
if (
compose.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to update this compose",
@@ -188,7 +202,7 @@ export const composeRouter = createTRPCRouter({
const composeResult = await findComposeById(input.composeId);
if (
composeResult.project.organizationId !==
composeResult.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
@@ -196,7 +210,6 @@ export const composeRouter = createTRPCRouter({
message: "You are not authorized to delete this compose",
});
}
4;
const result = await db
.delete(composeTable)
@@ -215,13 +228,16 @@ export const composeRouter = createTRPCRouter({
} catch (_) {}
}
return result[0];
return composeResult;
}),
cleanQueues: protectedProcedure
.input(apiFindCompose)
.mutation(async ({ input, ctx }) => {
const compose = await findComposeById(input.composeId);
if (compose.project.organizationId !== ctx.session.activeOrganizationId) {
if (
compose.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to clean this compose",
@@ -234,7 +250,10 @@ export const composeRouter = createTRPCRouter({
.input(apiFetchServices)
.query(async ({ input, ctx }) => {
const compose = await findComposeById(input.composeId);
if (compose.project.organizationId !== ctx.session.activeOrganizationId) {
if (
compose.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to load this compose",
@@ -251,7 +270,10 @@ export const composeRouter = createTRPCRouter({
)
.query(async ({ input, ctx }) => {
const compose = await findComposeById(input.composeId);
if (compose.project.organizationId !== ctx.session.activeOrganizationId) {
if (
compose.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to load this compose",
@@ -270,7 +292,8 @@ export const composeRouter = createTRPCRouter({
const compose = await findComposeById(input.composeId);
if (
compose.project.organizationId !== ctx.session.activeOrganizationId
compose.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
@@ -296,7 +319,10 @@ export const composeRouter = createTRPCRouter({
.input(apiRandomizeCompose)
.mutation(async ({ input, ctx }) => {
const compose = await findComposeById(input.composeId);
if (compose.project.organizationId !== ctx.session.activeOrganizationId) {
if (
compose.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to randomize this compose",
@@ -308,7 +334,10 @@ export const composeRouter = createTRPCRouter({
.input(apiRandomizeCompose)
.mutation(async ({ input, ctx }) => {
const compose = await findComposeById(input.composeId);
if (compose.project.organizationId !== ctx.session.activeOrganizationId) {
if (
compose.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to randomize this compose",
@@ -323,7 +352,10 @@ export const composeRouter = createTRPCRouter({
.input(apiFindCompose)
.query(async ({ input, ctx }) => {
const compose = await findComposeById(input.composeId);
if (compose.project.organizationId !== ctx.session.activeOrganizationId) {
if (
compose.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to get this compose",
@@ -337,11 +369,14 @@ export const composeRouter = createTRPCRouter({
}),
deploy: protectedProcedure
.input(apiFindCompose)
.input(apiDeployCompose)
.mutation(async ({ input, ctx }) => {
const compose = await findComposeById(input.composeId);
if (compose.project.organizationId !== ctx.session.activeOrganizationId) {
if (
compose.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to deploy this compose",
@@ -349,10 +384,10 @@ export const composeRouter = createTRPCRouter({
}
const jobData: DeploymentJob = {
composeId: input.composeId,
titleLog: "Manual deployment",
titleLog: input.title || "Manual deployment",
type: "deploy",
applicationType: "compose",
descriptionLog: "",
descriptionLog: input.description || "",
server: !!compose.serverId,
};
@@ -371,10 +406,13 @@ export const composeRouter = createTRPCRouter({
);
}),
redeploy: protectedProcedure
.input(apiFindCompose)
.input(apiRedeployCompose)
.mutation(async ({ input, ctx }) => {
const compose = await findComposeById(input.composeId);
if (compose.project.organizationId !== ctx.session.activeOrganizationId) {
if (
compose.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to redeploy this compose",
@@ -382,10 +420,10 @@ export const composeRouter = createTRPCRouter({
}
const jobData: DeploymentJob = {
composeId: input.composeId,
titleLog: "Rebuild deployment",
titleLog: input.title || "Rebuild deployment",
type: "redeploy",
applicationType: "compose",
descriptionLog: "",
descriptionLog: input.description || "",
server: !!compose.serverId,
};
if (IS_CLOUD && compose.serverId) {
@@ -406,7 +444,10 @@ export const composeRouter = createTRPCRouter({
.input(apiFindCompose)
.mutation(async ({ input, ctx }) => {
const compose = await findComposeById(input.composeId);
if (compose.project.organizationId !== ctx.session.activeOrganizationId) {
if (
compose.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to stop this compose",
@@ -420,7 +461,10 @@ export const composeRouter = createTRPCRouter({
.input(apiFindCompose)
.mutation(async ({ input, ctx }) => {
const compose = await findComposeById(input.composeId);
if (compose.project.organizationId !== ctx.session.activeOrganizationId) {
if (
compose.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to stop this compose",
@@ -435,7 +479,10 @@ export const composeRouter = createTRPCRouter({
.query(async ({ input, ctx }) => {
const compose = await findComposeById(input.composeId);
if (compose.project.organizationId !== ctx.session.activeOrganizationId) {
if (
compose.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to get this compose",
@@ -448,7 +495,10 @@ export const composeRouter = createTRPCRouter({
.input(apiFindCompose)
.mutation(async ({ input, ctx }) => {
const compose = await findComposeById(input.composeId);
if (compose.project.organizationId !== ctx.session.activeOrganizationId) {
if (
compose.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to refresh this compose",
@@ -462,17 +512,19 @@ export const composeRouter = createTRPCRouter({
deployTemplate: protectedProcedure
.input(
z.object({
projectId: z.string(),
environmentId: z.string(),
serverId: z.string().optional(),
id: z.string(),
baseUrl: z.string().optional(),
}),
)
.mutation(async ({ ctx, input }) => {
const environment = await findEnvironmentById(input.environmentId);
if (ctx.user.role === "member") {
await checkServiceAccess(
ctx.user.id,
input.projectId,
environment.projectId,
ctx.session.activeOrganizationId,
"create",
);
@@ -490,7 +542,7 @@ export const composeRouter = createTRPCRouter({
const admin = await findUserById(ctx.user.ownerId);
let serverIp = admin.serverIp || "127.0.0.1";
const project = await findProjectById(input.projectId);
const project = await findProjectById(environment.projectId);
if (input.serverId) {
const server = await findServerById(input.serverId);
@@ -591,7 +643,10 @@ export const composeRouter = createTRPCRouter({
.input(apiFindCompose)
.mutation(async ({ input, ctx }) => {
const compose = await findComposeById(input.composeId);
if (compose.project.organizationId !== ctx.session.activeOrganizationId) {
if (
compose.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to disconnect this git provider",
@@ -647,30 +702,38 @@ export const composeRouter = createTRPCRouter({
.input(
z.object({
composeId: z.string(),
targetProjectId: z.string(),
targetEnvironmentId: z.string(),
}),
)
.mutation(async ({ input, ctx }) => {
const compose = await findComposeById(input.composeId);
if (compose.project.organizationId !== ctx.session.activeOrganizationId) {
if (
compose.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to move this compose",
});
}
const targetProject = await findProjectById(input.targetProjectId);
if (targetProject.organizationId !== ctx.session.activeOrganizationId) {
const targetEnvironment = await findEnvironmentById(
input.targetEnvironmentId,
);
if (
targetEnvironment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to move to this project",
message: "You are not authorized to move to this environment",
});
}
const updatedCompose = await db
.update(composeTable)
.set({
projectId: input.targetProjectId,
environmentId: input.targetEnvironmentId,
})
.where(eq(composeTable.composeId, input.composeId))
.returning()
@@ -698,7 +761,8 @@ export const composeRouter = createTRPCRouter({
const compose = await findComposeById(input.composeId);
if (
compose.project.organizationId !== ctx.session.activeOrganizationId
compose.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
@@ -769,7 +833,8 @@ export const composeRouter = createTRPCRouter({
);
if (
compose.project.organizationId !== ctx.session.activeOrganizationId
compose.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",

View File

@@ -29,7 +29,8 @@ export const deploymentRouter = createTRPCRouter({
.query(async ({ input, ctx }) => {
const application = await findApplicationById(input.applicationId);
if (
application.project.organizationId !== ctx.session.activeOrganizationId
application.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
@@ -43,7 +44,10 @@ export const deploymentRouter = createTRPCRouter({
.input(apiFindAllByCompose)
.query(async ({ input, ctx }) => {
const compose = await findComposeById(input.composeId);
if (compose.project.organizationId !== ctx.session.activeOrganizationId) {
if (
compose.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to access this compose",

View File

@@ -34,7 +34,8 @@ export const domainRouter = createTRPCRouter({
if (input.domainType === "compose" && input.composeId) {
const compose = await findComposeById(input.composeId);
if (
compose.project.organizationId !== ctx.session.activeOrganizationId
compose.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
@@ -44,7 +45,7 @@ export const domainRouter = createTRPCRouter({
} else if (input.domainType === "application" && input.applicationId) {
const application = await findApplicationById(input.applicationId);
if (
application.project.organizationId !==
application.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
@@ -70,7 +71,8 @@ export const domainRouter = createTRPCRouter({
.query(async ({ input, ctx }) => {
const application = await findApplicationById(input.applicationId);
if (
application.project.organizationId !== ctx.session.activeOrganizationId
application.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
@@ -83,7 +85,10 @@ export const domainRouter = createTRPCRouter({
.input(apiFindCompose)
.query(async ({ input, ctx }) => {
const compose = await findComposeById(input.composeId);
if (compose.project.organizationId !== ctx.session.activeOrganizationId) {
if (
compose.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to access this compose",
@@ -122,7 +127,8 @@ export const domainRouter = createTRPCRouter({
if (currentDomain.applicationId) {
const newApp = await findApplicationById(currentDomain.applicationId);
if (
newApp.project.organizationId !== ctx.session.activeOrganizationId
newApp.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
@@ -132,7 +138,8 @@ export const domainRouter = createTRPCRouter({
} else if (currentDomain.composeId) {
const newCompose = await findComposeById(currentDomain.composeId);
if (
newCompose.project.organizationId !== ctx.session.activeOrganizationId
newCompose.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
@@ -144,8 +151,8 @@ export const domainRouter = createTRPCRouter({
currentDomain.previewDeploymentId,
);
if (
newPreviewDeployment.application.project.organizationId !==
ctx.session.activeOrganizationId
newPreviewDeployment.application.environment.project
.organizationId !== ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
@@ -175,7 +182,8 @@ export const domainRouter = createTRPCRouter({
if (domain.applicationId) {
const application = await findApplicationById(domain.applicationId);
if (
application.project.organizationId !== ctx.session.activeOrganizationId
application.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
@@ -184,7 +192,10 @@ export const domainRouter = createTRPCRouter({
}
} else if (domain.composeId) {
const compose = await findComposeById(domain.composeId);
if (compose.project.organizationId !== ctx.session.activeOrganizationId) {
if (
compose.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to access this compose",
@@ -200,7 +211,7 @@ export const domainRouter = createTRPCRouter({
if (domain.applicationId) {
const application = await findApplicationById(domain.applicationId);
if (
application.project.organizationId !==
application.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
@@ -211,7 +222,8 @@ export const domainRouter = createTRPCRouter({
} else if (domain.composeId) {
const compose = await findComposeById(domain.composeId);
if (
compose.project.organizationId !== ctx.session.activeOrganizationId
compose.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",

View File

@@ -0,0 +1,343 @@
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}`,
});
}
}),
});

View File

@@ -6,6 +6,7 @@ import {
deployMariadb,
findBackupsByDbId,
findMariadbById,
findEnvironmentById,
findProjectById,
IS_CLOUD,
rebuildDatabase,
@@ -41,10 +42,14 @@ export const mariadbRouter = createTRPCRouter({
.input(apiCreateMariaDB)
.mutation(async ({ input, ctx }) => {
try {
// Get project from environment
const environment = await findEnvironmentById(input.environmentId);
const project = await findProjectById(environment.projectId);
if (ctx.user.role === "member") {
await checkServiceAccess(
ctx.user.id,
input.projectId,
project.projectId,
ctx.session.activeOrganizationId,
"create",
);
@@ -57,14 +62,15 @@ export const mariadbRouter = createTRPCRouter({
});
}
const project = await findProjectById(input.projectId);
if (project.organizationId !== ctx.session.activeOrganizationId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to access this project",
});
}
const newMariadb = await createMariadb(input);
const newMariadb = await createMariadb({
...input,
});
if (ctx.user.role === "member") {
await addNewService(
ctx.user.id,
@@ -101,7 +107,10 @@ export const mariadbRouter = createTRPCRouter({
);
}
const mariadb = await findMariadbById(input.mariadbId);
if (mariadb.project.organizationId !== ctx.session.activeOrganizationId) {
if (
mariadb.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to access this Mariadb",
@@ -114,7 +123,10 @@ export const mariadbRouter = createTRPCRouter({
.input(apiFindOneMariaDB)
.mutation(async ({ input, ctx }) => {
const service = await findMariadbById(input.mariadbId);
if (service.project.organizationId !== ctx.session.activeOrganizationId) {
if (
service.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to start this Mariadb",
@@ -151,7 +163,10 @@ export const mariadbRouter = createTRPCRouter({
.input(apiSaveExternalPortMariaDB)
.mutation(async ({ input, ctx }) => {
const mongo = await findMariadbById(input.mariadbId);
if (mongo.project.organizationId !== ctx.session.activeOrganizationId) {
if (
mongo.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to save this external port",
@@ -167,7 +182,10 @@ export const mariadbRouter = createTRPCRouter({
.input(apiDeployMariaDB)
.mutation(async ({ input, ctx }) => {
const mariadb = await findMariadbById(input.mariadbId);
if (mariadb.project.organizationId !== ctx.session.activeOrganizationId) {
if (
mariadb.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to deploy this Mariadb",
@@ -188,7 +206,10 @@ export const mariadbRouter = createTRPCRouter({
.input(apiDeployMariaDB)
.subscription(async ({ input, ctx }) => {
const mariadb = await findMariadbById(input.mariadbId);
if (mariadb.project.organizationId !== ctx.session.activeOrganizationId) {
if (
mariadb.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to deploy this Mariadb",
@@ -205,7 +226,10 @@ export const mariadbRouter = createTRPCRouter({
.input(apiChangeMariaDBStatus)
.mutation(async ({ input, ctx }) => {
const mongo = await findMariadbById(input.mariadbId);
if (mongo.project.organizationId !== ctx.session.activeOrganizationId) {
if (
mongo.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to change this Mariadb status",
@@ -229,7 +253,10 @@ export const mariadbRouter = createTRPCRouter({
}
const mongo = await findMariadbById(input.mariadbId);
if (mongo.project.organizationId !== ctx.session.activeOrganizationId) {
if (
mongo.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to delete this Mariadb",
@@ -255,7 +282,10 @@ export const mariadbRouter = createTRPCRouter({
.input(apiSaveEnvironmentVariablesMariaDB)
.mutation(async ({ input, ctx }) => {
const mariadb = await findMariadbById(input.mariadbId);
if (mariadb.project.organizationId !== ctx.session.activeOrganizationId) {
if (
mariadb.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to save this environment",
@@ -278,7 +308,10 @@ export const mariadbRouter = createTRPCRouter({
.input(apiResetMariadb)
.mutation(async ({ input, ctx }) => {
const mariadb = await findMariadbById(input.mariadbId);
if (mariadb.project.organizationId !== ctx.session.activeOrganizationId) {
if (
mariadb.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to reload this Mariadb",
@@ -308,7 +341,10 @@ export const mariadbRouter = createTRPCRouter({
.mutation(async ({ input, ctx }) => {
const { mariadbId, ...rest } = input;
const mariadb = await findMariadbById(mariadbId);
if (mariadb.project.organizationId !== ctx.session.activeOrganizationId) {
if (
mariadb.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to update this Mariadb",
@@ -331,23 +367,31 @@ export const mariadbRouter = createTRPCRouter({
.input(
z.object({
mariadbId: z.string(),
targetProjectId: z.string(),
targetEnvironmentId: z.string(),
}),
)
.mutation(async ({ input, ctx }) => {
const mariadb = await findMariadbById(input.mariadbId);
if (mariadb.project.organizationId !== ctx.session.activeOrganizationId) {
if (
mariadb.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to move this mariadb",
});
}
const targetProject = await findProjectById(input.targetProjectId);
if (targetProject.organizationId !== ctx.session.activeOrganizationId) {
const targetEnvironment = await findEnvironmentById(
input.targetEnvironmentId,
);
if (
targetEnvironment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to move to this project",
message: "You are not authorized to move to this environment",
});
}
@@ -355,7 +399,7 @@ export const mariadbRouter = createTRPCRouter({
const updatedMariadb = await db
.update(mariadbTable)
.set({
projectId: input.targetProjectId,
environmentId: input.targetEnvironmentId,
})
.where(eq(mariadbTable.mariadbId, input.mariadbId))
.returning()
@@ -374,7 +418,10 @@ export const mariadbRouter = createTRPCRouter({
.input(apiRebuildMariadb)
.mutation(async ({ input, ctx }) => {
const mariadb = await findMariadbById(input.mariadbId);
if (mariadb.project.organizationId !== ctx.session.activeOrganizationId) {
if (
mariadb.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to rebuild this MariaDB database",

View File

@@ -6,6 +6,7 @@ import {
deployMongo,
findBackupsByDbId,
findMongoById,
findEnvironmentById,
findProjectById,
IS_CLOUD,
rebuildDatabase,
@@ -41,10 +42,14 @@ export const mongoRouter = createTRPCRouter({
.input(apiCreateMongo)
.mutation(async ({ input, ctx }) => {
try {
// Get project from environment
const environment = await findEnvironmentById(input.environmentId);
const project = await findProjectById(environment.projectId);
if (ctx.user.role === "member") {
await checkServiceAccess(
ctx.user.id,
input.projectId,
project.projectId,
ctx.session.activeOrganizationId,
"create",
);
@@ -57,14 +62,15 @@ export const mongoRouter = createTRPCRouter({
});
}
const project = await findProjectById(input.projectId);
if (project.organizationId !== ctx.session.activeOrganizationId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to access this project",
});
}
const newMongo = await createMongo(input);
const newMongo = await createMongo({
...input,
});
if (ctx.user.role === "member") {
await addNewService(
ctx.user.id,
@@ -106,7 +112,10 @@ export const mongoRouter = createTRPCRouter({
}
const mongo = await findMongoById(input.mongoId);
if (mongo.project.organizationId !== ctx.session.activeOrganizationId) {
if (
mongo.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to access this mongo",
@@ -120,7 +129,10 @@ export const mongoRouter = createTRPCRouter({
.mutation(async ({ input, ctx }) => {
const service = await findMongoById(input.mongoId);
if (service.project.organizationId !== ctx.session.activeOrganizationId) {
if (
service.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to start this mongo",
@@ -143,7 +155,10 @@ export const mongoRouter = createTRPCRouter({
.mutation(async ({ input, ctx }) => {
const mongo = await findMongoById(input.mongoId);
if (mongo.project.organizationId !== ctx.session.activeOrganizationId) {
if (
mongo.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to stop this mongo",
@@ -165,7 +180,10 @@ export const mongoRouter = createTRPCRouter({
.input(apiSaveExternalPortMongo)
.mutation(async ({ input, ctx }) => {
const mongo = await findMongoById(input.mongoId);
if (mongo.project.organizationId !== ctx.session.activeOrganizationId) {
if (
mongo.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to save this external port",
@@ -181,7 +199,10 @@ export const mongoRouter = createTRPCRouter({
.input(apiDeployMongo)
.mutation(async ({ input, ctx }) => {
const mongo = await findMongoById(input.mongoId);
if (mongo.project.organizationId !== ctx.session.activeOrganizationId) {
if (
mongo.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to deploy this mongo",
@@ -201,7 +222,10 @@ export const mongoRouter = createTRPCRouter({
.input(apiDeployMongo)
.subscription(async ({ input, ctx }) => {
const mongo = await findMongoById(input.mongoId);
if (mongo.project.organizationId !== ctx.session.activeOrganizationId) {
if (
mongo.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to deploy this mongo",
@@ -218,7 +242,10 @@ export const mongoRouter = createTRPCRouter({
.input(apiChangeMongoStatus)
.mutation(async ({ input, ctx }) => {
const mongo = await findMongoById(input.mongoId);
if (mongo.project.organizationId !== ctx.session.activeOrganizationId) {
if (
mongo.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to change this mongo status",
@@ -233,7 +260,10 @@ export const mongoRouter = createTRPCRouter({
.input(apiResetMongo)
.mutation(async ({ input, ctx }) => {
const mongo = await findMongoById(input.mongoId);
if (mongo.project.organizationId !== ctx.session.activeOrganizationId) {
if (
mongo.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to reload this mongo",
@@ -272,7 +302,10 @@ export const mongoRouter = createTRPCRouter({
const mongo = await findMongoById(input.mongoId);
if (mongo.project.organizationId !== ctx.session.activeOrganizationId) {
if (
mongo.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to delete this mongo",
@@ -298,7 +331,10 @@ export const mongoRouter = createTRPCRouter({
.input(apiSaveEnvironmentVariablesMongo)
.mutation(async ({ input, ctx }) => {
const mongo = await findMongoById(input.mongoId);
if (mongo.project.organizationId !== ctx.session.activeOrganizationId) {
if (
mongo.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to save this environment",
@@ -322,7 +358,10 @@ export const mongoRouter = createTRPCRouter({
.mutation(async ({ input, ctx }) => {
const { mongoId, ...rest } = input;
const mongo = await findMongoById(mongoId);
if (mongo.project.organizationId !== ctx.session.activeOrganizationId) {
if (
mongo.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to update this mongo",
@@ -345,23 +384,31 @@ export const mongoRouter = createTRPCRouter({
.input(
z.object({
mongoId: z.string(),
targetProjectId: z.string(),
targetEnvironmentId: z.string(),
}),
)
.mutation(async ({ input, ctx }) => {
const mongo = await findMongoById(input.mongoId);
if (mongo.project.organizationId !== ctx.session.activeOrganizationId) {
if (
mongo.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to move this mongo",
});
}
const targetProject = await findProjectById(input.targetProjectId);
if (targetProject.organizationId !== ctx.session.activeOrganizationId) {
const targetEnvironment = await findEnvironmentById(
input.targetEnvironmentId,
);
if (
targetEnvironment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to move to this project",
message: "You are not authorized to move to this environment",
});
}
@@ -369,7 +416,7 @@ export const mongoRouter = createTRPCRouter({
const updatedMongo = await db
.update(mongoTable)
.set({
projectId: input.targetProjectId,
environmentId: input.targetEnvironmentId,
})
.where(eq(mongoTable.mongoId, input.mongoId))
.returning()
@@ -388,7 +435,10 @@ export const mongoRouter = createTRPCRouter({
.input(apiRebuildMongo)
.mutation(async ({ input, ctx }) => {
const mongo = await findMongoById(input.mongoId);
if (mongo.project.organizationId !== ctx.session.activeOrganizationId) {
if (
mongo.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to rebuild this MongoDB database",

View File

@@ -3,9 +3,11 @@ import {
deleteMount,
findApplicationById,
findMountById,
findMountOrganizationId,
getServiceContainer,
updateMount,
} from "@dokploy/server";
import { TRPCError } from "@trpc/server";
import { z } from "zod";
import {
apiCreateMount,
@@ -24,16 +26,39 @@ export const mountRouter = createTRPCRouter({
}),
remove: protectedProcedure
.input(apiRemoveMount)
.mutation(async ({ input }) => {
.mutation(async ({ input, ctx }) => {
const organizationId = await findMountOrganizationId(input.mountId);
if (organizationId !== ctx.session.activeOrganizationId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to delete this mount",
});
}
return await deleteMount(input.mountId);
}),
one: protectedProcedure.input(apiFindOneMount).query(async ({ input }) => {
return await findMountById(input.mountId);
}),
one: protectedProcedure
.input(apiFindOneMount)
.query(async ({ input, ctx }) => {
const organizationId = await findMountOrganizationId(input.mountId);
if (organizationId !== ctx.session.activeOrganizationId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to access this mount",
});
}
return await findMountById(input.mountId);
}),
update: protectedProcedure
.input(apiUpdateMount)
.mutation(async ({ input }) => {
.mutation(async ({ input, ctx }) => {
const organizationId = await findMountOrganizationId(input.mountId);
if (organizationId !== ctx.session.activeOrganizationId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to update this mount",
});
}
return await updateMount(input.mountId, input);
}),
allNamedByApplicationId: protectedProcedure

View File

@@ -5,6 +5,7 @@ import {
createMysql,
deployMySql,
findBackupsByDbId,
findEnvironmentById,
findMySqlById,
findProjectById,
IS_CLOUD,
@@ -42,10 +43,14 @@ export const mysqlRouter = createTRPCRouter({
.input(apiCreateMySql)
.mutation(async ({ input, ctx }) => {
try {
// Get project from environment
const environment = await findEnvironmentById(input.environmentId);
const project = await findProjectById(environment.projectId);
if (ctx.user.role === "member") {
await checkServiceAccess(
ctx.user.id,
input.projectId,
project.projectId,
ctx.session.activeOrganizationId,
"create",
);
@@ -57,8 +62,7 @@ export const mysqlRouter = createTRPCRouter({
message: "You need to use a server to create a MySQL",
});
}
1;
const project = await findProjectById(input.projectId);
if (project.organizationId !== ctx.session.activeOrganizationId) {
throw new TRPCError({
code: "UNAUTHORIZED",
@@ -66,7 +70,9 @@ export const mysqlRouter = createTRPCRouter({
});
}
const newMysql = await createMysql(input);
const newMysql = await createMysql({
...input,
});
if (ctx.user.role === "member") {
await addNewService(
ctx.user.id,
@@ -107,7 +113,10 @@ export const mysqlRouter = createTRPCRouter({
);
}
const mysql = await findMySqlById(input.mysqlId);
if (mysql.project.organizationId !== ctx.session.activeOrganizationId) {
if (
mysql.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to access this MySQL",
@@ -120,7 +129,10 @@ export const mysqlRouter = createTRPCRouter({
.input(apiFindOneMySql)
.mutation(async ({ input, ctx }) => {
const service = await findMySqlById(input.mysqlId);
if (service.project.organizationId !== ctx.session.activeOrganizationId) {
if (
service.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to start this MySQL",
@@ -142,7 +154,10 @@ export const mysqlRouter = createTRPCRouter({
.input(apiFindOneMySql)
.mutation(async ({ input, ctx }) => {
const mongo = await findMySqlById(input.mysqlId);
if (mongo.project.organizationId !== ctx.session.activeOrganizationId) {
if (
mongo.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to stop this MySQL",
@@ -163,7 +178,10 @@ export const mysqlRouter = createTRPCRouter({
.input(apiSaveExternalPortMySql)
.mutation(async ({ input, ctx }) => {
const mongo = await findMySqlById(input.mysqlId);
if (mongo.project.organizationId !== ctx.session.activeOrganizationId) {
if (
mongo.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to save this external port",
@@ -179,7 +197,10 @@ export const mysqlRouter = createTRPCRouter({
.input(apiDeployMySql)
.mutation(async ({ input, ctx }) => {
const mysql = await findMySqlById(input.mysqlId);
if (mysql.project.organizationId !== ctx.session.activeOrganizationId) {
if (
mysql.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to deploy this MySQL",
@@ -199,7 +220,10 @@ export const mysqlRouter = createTRPCRouter({
.input(apiDeployMySql)
.subscription(async ({ input, ctx }) => {
const mysql = await findMySqlById(input.mysqlId);
if (mysql.project.organizationId !== ctx.session.activeOrganizationId) {
if (
mysql.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to deploy this MySQL",
@@ -216,7 +240,10 @@ export const mysqlRouter = createTRPCRouter({
.input(apiChangeMySqlStatus)
.mutation(async ({ input, ctx }) => {
const mongo = await findMySqlById(input.mysqlId);
if (mongo.project.organizationId !== ctx.session.activeOrganizationId) {
if (
mongo.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to change this MySQL status",
@@ -231,7 +258,10 @@ export const mysqlRouter = createTRPCRouter({
.input(apiResetMysql)
.mutation(async ({ input, ctx }) => {
const mysql = await findMySqlById(input.mysqlId);
if (mysql.project.organizationId !== ctx.session.activeOrganizationId) {
if (
mysql.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to reload this MySQL",
@@ -267,7 +297,10 @@ export const mysqlRouter = createTRPCRouter({
);
}
const mongo = await findMySqlById(input.mysqlId);
if (mongo.project.organizationId !== ctx.session.activeOrganizationId) {
if (
mongo.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to delete this MySQL",
@@ -293,7 +326,10 @@ export const mysqlRouter = createTRPCRouter({
.input(apiSaveEnvironmentVariablesMySql)
.mutation(async ({ input, ctx }) => {
const mysql = await findMySqlById(input.mysqlId);
if (mysql.project.organizationId !== ctx.session.activeOrganizationId) {
if (
mysql.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to save this environment",
@@ -317,7 +353,10 @@ export const mysqlRouter = createTRPCRouter({
.mutation(async ({ input, ctx }) => {
const { mysqlId, ...rest } = input;
const mysql = await findMySqlById(mysqlId);
if (mysql.project.organizationId !== ctx.session.activeOrganizationId) {
if (
mysql.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to update this MySQL",
@@ -340,23 +379,31 @@ export const mysqlRouter = createTRPCRouter({
.input(
z.object({
mysqlId: z.string(),
targetProjectId: z.string(),
targetEnvironmentId: z.string(),
}),
)
.mutation(async ({ input, ctx }) => {
const mysql = await findMySqlById(input.mysqlId);
if (mysql.project.organizationId !== ctx.session.activeOrganizationId) {
if (
mysql.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to move this mysql",
});
}
const targetProject = await findProjectById(input.targetProjectId);
if (targetProject.organizationId !== ctx.session.activeOrganizationId) {
const targetEnvironment = await findEnvironmentById(
input.targetEnvironmentId,
);
if (
targetEnvironment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to move to this project",
message: "You are not authorized to move to this environment",
});
}
@@ -364,7 +411,7 @@ export const mysqlRouter = createTRPCRouter({
const updatedMysql = await db
.update(mysqlTable)
.set({
projectId: input.targetProjectId,
environmentId: input.targetEnvironmentId,
})
.where(eq(mysqlTable.mysqlId, input.mysqlId))
.returning()
@@ -383,7 +430,10 @@ export const mysqlRouter = createTRPCRouter({
.input(apiRebuildMysql)
.mutation(async ({ input, ctx }) => {
const mysql = await findMySqlById(input.mysqlId);
if (mysql.project.organizationId !== ctx.session.activeOrganizationId) {
if (
mysql.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to rebuild this MySQL database",

View File

@@ -27,22 +27,44 @@ export const portRouter = createTRPCRouter({
});
}
}),
one: protectedProcedure.input(apiFindOnePort).query(async ({ input }) => {
try {
return await finPortById(input.portId);
} catch (error) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Port not found",
cause: error,
});
}
}),
one: protectedProcedure
.input(apiFindOnePort)
.query(async ({ input, ctx }) => {
try {
const port = await finPortById(input.portId);
if (
port.application.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to access this port",
});
}
return port;
} catch (error) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Port not found",
cause: error,
});
}
}),
delete: protectedProcedure
.input(apiFindOnePort)
.mutation(async ({ input }) => {
.mutation(async ({ input, ctx }) => {
const port = await finPortById(input.portId);
if (
port.application.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to delete this port",
});
}
try {
return removePortById(input.portId);
return await removePortById(input.portId);
} catch (error) {
const message =
error instanceof Error ? error.message : "Error input: Deleting port";
@@ -54,9 +76,19 @@ export const portRouter = createTRPCRouter({
}),
update: protectedProcedure
.input(apiUpdatePort)
.mutation(async ({ input }) => {
.mutation(async ({ input, ctx }) => {
const port = await finPortById(input.portId);
if (
port.application.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to update this port",
});
}
try {
return updatePortById(input.portId, input);
return await updatePortById(input.portId, input);
} catch (error) {
const message =
error instanceof Error ? error.message : "Error updating the port";

View File

@@ -5,6 +5,7 @@ import {
createPostgres,
deployPostgres,
findBackupsByDbId,
findEnvironmentById,
findPostgresById,
findProjectById,
IS_CLOUD,
@@ -41,10 +42,14 @@ export const postgresRouter = createTRPCRouter({
.input(apiCreatePostgres)
.mutation(async ({ input, ctx }) => {
try {
// Get project from environment
const environment = await findEnvironmentById(input.environmentId);
const project = await findProjectById(environment.projectId);
if (ctx.user.role === "member") {
await checkServiceAccess(
ctx.user.id,
input.projectId,
project.projectId,
ctx.session.activeOrganizationId,
"create",
);
@@ -57,14 +62,15 @@ export const postgresRouter = createTRPCRouter({
});
}
const project = await findProjectById(input.projectId);
if (project.organizationId !== ctx.session.activeOrganizationId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to access this project",
});
}
const newPostgres = await createPostgres(input);
const newPostgres = await createPostgres({
...input,
});
if (ctx.user.role === "member") {
await addNewService(
ctx.user.id,
@@ -107,7 +113,8 @@ export const postgresRouter = createTRPCRouter({
const postgres = await findPostgresById(input.postgresId);
if (
postgres.project.organizationId !== ctx.session.activeOrganizationId
postgres.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
@@ -122,7 +129,10 @@ export const postgresRouter = createTRPCRouter({
.mutation(async ({ input, ctx }) => {
const service = await findPostgresById(input.postgresId);
if (service.project.organizationId !== ctx.session.activeOrganizationId) {
if (
service.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to start this Postgres",
@@ -145,7 +155,8 @@ export const postgresRouter = createTRPCRouter({
.mutation(async ({ input, ctx }) => {
const postgres = await findPostgresById(input.postgresId);
if (
postgres.project.organizationId !== ctx.session.activeOrganizationId
postgres.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
@@ -169,7 +180,8 @@ export const postgresRouter = createTRPCRouter({
const postgres = await findPostgresById(input.postgresId);
if (
postgres.project.organizationId !== ctx.session.activeOrganizationId
postgres.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
@@ -187,7 +199,8 @@ export const postgresRouter = createTRPCRouter({
.mutation(async ({ input, ctx }) => {
const postgres = await findPostgresById(input.postgresId);
if (
postgres.project.organizationId !== ctx.session.activeOrganizationId
postgres.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
@@ -210,7 +223,8 @@ export const postgresRouter = createTRPCRouter({
.subscription(async ({ input, ctx }) => {
const postgres = await findPostgresById(input.postgresId);
if (
postgres.project.organizationId !== ctx.session.activeOrganizationId
postgres.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
@@ -229,7 +243,8 @@ export const postgresRouter = createTRPCRouter({
.mutation(async ({ input, ctx }) => {
const postgres = await findPostgresById(input.postgresId);
if (
postgres.project.organizationId !== ctx.session.activeOrganizationId
postgres.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
@@ -255,7 +270,8 @@ export const postgresRouter = createTRPCRouter({
const postgres = await findPostgresById(input.postgresId);
if (
postgres.project.organizationId !== ctx.session.activeOrganizationId
postgres.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
@@ -280,7 +296,8 @@ export const postgresRouter = createTRPCRouter({
.mutation(async ({ input, ctx }) => {
const postgres = await findPostgresById(input.postgresId);
if (
postgres.project.organizationId !== ctx.session.activeOrganizationId
postgres.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
@@ -305,7 +322,8 @@ export const postgresRouter = createTRPCRouter({
.mutation(async ({ input, ctx }) => {
const postgres = await findPostgresById(input.postgresId);
if (
postgres.project.organizationId !== ctx.session.activeOrganizationId
postgres.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
@@ -337,7 +355,8 @@ export const postgresRouter = createTRPCRouter({
const { postgresId, ...rest } = input;
const postgres = await findPostgresById(postgresId);
if (
postgres.project.organizationId !== ctx.session.activeOrganizationId
postgres.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
@@ -361,13 +380,14 @@ export const postgresRouter = createTRPCRouter({
.input(
z.object({
postgresId: z.string(),
targetProjectId: z.string(),
targetEnvironmentId: z.string(),
}),
)
.mutation(async ({ input, ctx }) => {
const postgres = await findPostgresById(input.postgresId);
if (
postgres.project.organizationId !== ctx.session.activeOrganizationId
postgres.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
@@ -375,11 +395,16 @@ export const postgresRouter = createTRPCRouter({
});
}
const targetProject = await findProjectById(input.targetProjectId);
if (targetProject.organizationId !== ctx.session.activeOrganizationId) {
const targetEnvironment = await findEnvironmentById(
input.targetEnvironmentId,
);
if (
targetEnvironment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to move to this project",
message: "You are not authorized to move to this environment",
});
}
@@ -387,7 +412,7 @@ export const postgresRouter = createTRPCRouter({
const updatedPostgres = await db
.update(postgresTable)
.set({
projectId: input.targetProjectId,
environmentId: input.targetEnvironmentId,
})
.where(eq(postgresTable.postgresId, input.postgresId))
.returning()
@@ -407,7 +432,8 @@ export const postgresRouter = createTRPCRouter({
.mutation(async ({ input, ctx }) => {
const postgres = await findPostgresById(input.postgresId);
if (
postgres.project.organizationId !== ctx.session.activeOrganizationId
postgres.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",

View File

@@ -15,7 +15,8 @@ export const previewDeploymentRouter = createTRPCRouter({
.query(async ({ input, ctx }) => {
const application = await findApplicationById(input.applicationId);
if (
application.project.organizationId !== ctx.session.activeOrganizationId
application.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
@@ -31,7 +32,7 @@ export const previewDeploymentRouter = createTRPCRouter({
input.previewDeploymentId,
);
if (
previewDeployment.application.project.organizationId !==
previewDeployment.application.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
@@ -49,7 +50,7 @@ export const previewDeploymentRouter = createTRPCRouter({
input.previewDeploymentId,
);
if (
previewDeployment.application.project.organizationId !==
previewDeployment.application.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({

View File

@@ -19,6 +19,7 @@ import {
deleteProject,
findApplicationById,
findComposeById,
findEnvironmentById,
findMariadbById,
findMemberById,
findMongoById,
@@ -43,6 +44,7 @@ import {
apiUpdateProject,
applications,
compose,
environments,
mariadb,
mongo,
mysql,
@@ -80,7 +82,7 @@ export const projectRouter = createTRPCRouter({
if (ctx.user.role === "member") {
await addNewProject(
ctx.user.id,
project.projectId,
project.project.projectId,
ctx.session.activeOrganizationId,
);
}
@@ -117,29 +119,42 @@ export const projectRouter = createTRPCRouter({
eq(projects.organizationId, ctx.session.activeOrganizationId),
),
with: {
applications: {
where: buildServiceFilter(
applications.applicationId,
accessedServices,
),
},
compose: {
where: buildServiceFilter(compose.composeId, accessedServices),
},
mariadb: {
where: buildServiceFilter(mariadb.mariadbId, accessedServices),
},
mongo: {
where: buildServiceFilter(mongo.mongoId, accessedServices),
},
mysql: {
where: buildServiceFilter(mysql.mysqlId, accessedServices),
},
postgres: {
where: buildServiceFilter(postgres.postgresId, accessedServices),
},
redis: {
where: buildServiceFilter(redis.redisId, accessedServices),
environments: {
with: {
applications: {
where: buildServiceFilter(
applications.applicationId,
accessedServices,
),
},
compose: {
where: buildServiceFilter(
compose.composeId,
accessedServices,
),
},
mariadb: {
where: buildServiceFilter(
mariadb.mariadbId,
accessedServices,
),
},
mongo: {
where: buildServiceFilter(mongo.mongoId, accessedServices),
},
mysql: {
where: buildServiceFilter(mysql.mysqlId, accessedServices),
},
postgres: {
where: buildServiceFilter(
postgres.postgresId,
accessedServices,
),
},
redis: {
where: buildServiceFilter(redis.redisId, accessedServices),
},
},
},
},
});
@@ -164,15 +179,22 @@ export const projectRouter = createTRPCRouter({
}),
all: protectedProcedure.query(async ({ ctx }) => {
if (ctx.user.role === "member") {
const { accessedProjects, accessedServices } = await findMemberById(
ctx.user.id,
ctx.session.activeOrganizationId,
);
const { accessedProjects, accessedEnvironments, accessedServices } =
await findMemberById(ctx.user.id, ctx.session.activeOrganizationId);
if (accessedProjects.length === 0) {
return [];
}
// Build environment filter
const environmentFilter =
accessedEnvironments.length === 0
? sql`false`
: sql`${environments.environmentId} IN (${sql.join(
accessedEnvironments.map((envId) => sql`${envId}`),
sql`, `,
)})`;
return await db.query.projects.findMany({
where: and(
sql`${projects.projectId} IN (${sql.join(
@@ -182,31 +204,39 @@ export const projectRouter = createTRPCRouter({
eq(projects.organizationId, ctx.session.activeOrganizationId),
),
with: {
applications: {
where: buildServiceFilter(
applications.applicationId,
accessedServices,
),
with: { domains: true },
},
mariadb: {
where: buildServiceFilter(mariadb.mariadbId, accessedServices),
},
mongo: {
where: buildServiceFilter(mongo.mongoId, accessedServices),
},
mysql: {
where: buildServiceFilter(mysql.mysqlId, accessedServices),
},
postgres: {
where: buildServiceFilter(postgres.postgresId, accessedServices),
},
redis: {
where: buildServiceFilter(redis.redisId, accessedServices),
},
compose: {
where: buildServiceFilter(compose.composeId, accessedServices),
with: { domains: true },
environments: {
where: environmentFilter,
with: {
applications: {
where: buildServiceFilter(
applications.applicationId,
accessedServices,
),
with: { domains: true },
},
mariadb: {
where: buildServiceFilter(mariadb.mariadbId, accessedServices),
},
mongo: {
where: buildServiceFilter(mongo.mongoId, accessedServices),
},
mysql: {
where: buildServiceFilter(mysql.mysqlId, accessedServices),
},
postgres: {
where: buildServiceFilter(
postgres.postgresId,
accessedServices,
),
},
redis: {
where: buildServiceFilter(redis.redisId, accessedServices),
},
compose: {
where: buildServiceFilter(compose.composeId, accessedServices),
with: { domains: true },
},
},
},
},
orderBy: desc(projects.createdAt),
@@ -215,19 +245,23 @@ export const projectRouter = createTRPCRouter({
return await db.query.projects.findMany({
with: {
applications: {
environments: {
with: {
domains: true,
},
},
mariadb: true,
mongo: true,
mysql: true,
postgres: true,
redis: true,
compose: {
with: {
domains: true,
applications: {
with: {
domains: true,
},
},
mariadb: true,
mongo: true,
mysql: true,
postgres: true,
redis: true,
compose: {
with: {
domains: true,
},
},
},
},
},
@@ -288,7 +322,7 @@ export const projectRouter = createTRPCRouter({
duplicate: protectedProcedure
.input(
z.object({
sourceProjectId: z.string(),
sourceEnvironmentId: z.string(),
name: z.string(),
description: z.string().optional(),
includeServices: z.boolean().default(true),
@@ -322,9 +356,15 @@ export const projectRouter = createTRPCRouter({
}
// Get source project
const sourceProject = await findProjectById(input.sourceProjectId);
const sourceEnvironment = input.duplicateInSameProject
? await findEnvironmentById(input.sourceEnvironmentId)
: null;
if (sourceProject.organizationId !== ctx.session.activeOrganizationId) {
if (
input.duplicateInSameProject &&
sourceEnvironment?.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to access this project",
@@ -333,15 +373,17 @@ export const projectRouter = createTRPCRouter({
// Create new project or use existing one
const targetProject = input.duplicateInSameProject
? sourceProject
? sourceEnvironment
: await createProject(
{
name: input.name,
description: input.description,
env: sourceProject.env,
env: sourceEnvironment?.project.env,
},
ctx.session.activeOrganizationId,
);
).then((value) => value.environment);
console.log("targetProject", targetProject);
if (input.includeServices) {
const servicesToDuplicate = input.selectedServices || [];
@@ -374,7 +416,7 @@ export const projectRouter = createTRPCRouter({
name: input.duplicateInSameProject
? `${application.name} (copy)`
: application.name,
projectId: targetProject.projectId,
environmentId: targetProject?.environmentId || "",
});
for (const domain of domains) {
@@ -444,7 +486,7 @@ export const projectRouter = createTRPCRouter({
name: input.duplicateInSameProject
? `${postgres.name} (copy)`
: postgres.name,
projectId: targetProject.projectId,
environmentId: targetProject?.environmentId || "",
});
for (const mount of mounts) {
@@ -480,7 +522,7 @@ export const projectRouter = createTRPCRouter({
name: input.duplicateInSameProject
? `${mariadb.name} (copy)`
: mariadb.name,
projectId: targetProject.projectId,
environmentId: targetProject?.environmentId || "",
});
for (const mount of mounts) {
@@ -516,7 +558,7 @@ export const projectRouter = createTRPCRouter({
name: input.duplicateInSameProject
? `${mongo.name} (copy)`
: mongo.name,
projectId: targetProject.projectId,
environmentId: targetProject?.environmentId || "",
});
for (const mount of mounts) {
@@ -552,7 +594,7 @@ export const projectRouter = createTRPCRouter({
name: input.duplicateInSameProject
? `${mysql.name} (copy)`
: mysql.name,
projectId: targetProject.projectId,
environmentId: targetProject?.environmentId || "",
});
for (const mount of mounts) {
@@ -588,7 +630,7 @@ export const projectRouter = createTRPCRouter({
name: input.duplicateInSameProject
? `${redis.name} (copy)`
: redis.name,
projectId: targetProject.projectId,
environmentId: targetProject?.environmentId || "",
});
for (const mount of mounts) {
@@ -623,7 +665,7 @@ export const projectRouter = createTRPCRouter({
name: input.duplicateInSameProject
? `${compose.name} (copy)`
: compose.name,
projectId: targetProject.projectId,
environmentId: targetProject?.environmentId || "",
});
for (const mount of mounts) {
@@ -658,7 +700,7 @@ export const projectRouter = createTRPCRouter({
if (!input.duplicateInSameProject && ctx.user.role === "member") {
await addNewProject(
ctx.user.id,
targetProject.projectId,
targetProject?.projectId || "",
ctx.session.activeOrganizationId,
);
}

View File

@@ -19,7 +19,8 @@ export const redirectsRouter = createTRPCRouter({
.mutation(async ({ input, ctx }) => {
const application = await findApplicationById(input.applicationId);
if (
application.project.organizationId !== ctx.session.activeOrganizationId
application.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
@@ -34,7 +35,8 @@ export const redirectsRouter = createTRPCRouter({
const redirect = await findRedirectById(input.redirectId);
const application = await findApplicationById(redirect.applicationId);
if (
application.project.organizationId !== ctx.session.activeOrganizationId
application.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
@@ -49,7 +51,8 @@ export const redirectsRouter = createTRPCRouter({
const redirect = await findRedirectById(input.redirectId);
const application = await findApplicationById(redirect.applicationId);
if (
application.project.organizationId !== ctx.session.activeOrganizationId
application.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
@@ -64,7 +67,8 @@ export const redirectsRouter = createTRPCRouter({
const redirect = await findRedirectById(input.redirectId);
const application = await findApplicationById(redirect.applicationId);
if (
application.project.organizationId !== ctx.session.activeOrganizationId
application.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",

View File

@@ -4,6 +4,7 @@ import {
createMount,
createRedis,
deployRedis,
findEnvironmentById,
findProjectById,
findRedisById,
IS_CLOUD,
@@ -40,10 +41,14 @@ export const redisRouter = createTRPCRouter({
.input(apiCreateRedis)
.mutation(async ({ input, ctx }) => {
try {
// Get project from environment
const environment = await findEnvironmentById(input.environmentId);
const project = await findProjectById(environment.projectId);
if (ctx.user.role === "member") {
await checkServiceAccess(
ctx.user.id,
input.projectId,
project.projectId,
ctx.session.activeOrganizationId,
"create",
);
@@ -56,14 +61,15 @@ export const redisRouter = createTRPCRouter({
});
}
const project = await findProjectById(input.projectId);
if (project.organizationId !== ctx.session.activeOrganizationId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to access this project",
});
}
const newRedis = await createRedis(input);
const newRedis = await createRedis({
...input,
});
if (ctx.user.role === "member") {
await addNewService(
ctx.user.id,
@@ -98,7 +104,10 @@ export const redisRouter = createTRPCRouter({
}
const redis = await findRedisById(input.redisId);
if (redis.project.organizationId !== ctx.session.activeOrganizationId) {
if (
redis.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to access this Redis",
@@ -111,7 +120,10 @@ export const redisRouter = createTRPCRouter({
.input(apiFindOneRedis)
.mutation(async ({ input, ctx }) => {
const redis = await findRedisById(input.redisId);
if (redis.project.organizationId !== ctx.session.activeOrganizationId) {
if (
redis.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to start this Redis",
@@ -133,7 +145,10 @@ export const redisRouter = createTRPCRouter({
.input(apiResetRedis)
.mutation(async ({ input, ctx }) => {
const redis = await findRedisById(input.redisId);
if (redis.project.organizationId !== ctx.session.activeOrganizationId) {
if (
redis.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to reload this Redis",
@@ -163,7 +178,10 @@ export const redisRouter = createTRPCRouter({
.input(apiFindOneRedis)
.mutation(async ({ input, ctx }) => {
const redis = await findRedisById(input.redisId);
if (redis.project.organizationId !== ctx.session.activeOrganizationId) {
if (
redis.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to stop this Redis",
@@ -184,7 +202,10 @@ export const redisRouter = createTRPCRouter({
.input(apiSaveExternalPortRedis)
.mutation(async ({ input, ctx }) => {
const mongo = await findRedisById(input.redisId);
if (mongo.project.organizationId !== ctx.session.activeOrganizationId) {
if (
mongo.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to save this external port",
@@ -200,7 +221,10 @@ export const redisRouter = createTRPCRouter({
.input(apiDeployRedis)
.mutation(async ({ input, ctx }) => {
const redis = await findRedisById(input.redisId);
if (redis.project.organizationId !== ctx.session.activeOrganizationId) {
if (
redis.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to deploy this Redis",
@@ -220,7 +244,10 @@ export const redisRouter = createTRPCRouter({
.input(apiDeployRedis)
.subscription(async ({ input, ctx }) => {
const redis = await findRedisById(input.redisId);
if (redis.project.organizationId !== ctx.session.activeOrganizationId) {
if (
redis.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to deploy this Redis",
@@ -236,7 +263,10 @@ export const redisRouter = createTRPCRouter({
.input(apiChangeRedisStatus)
.mutation(async ({ input, ctx }) => {
const mongo = await findRedisById(input.redisId);
if (mongo.project.organizationId !== ctx.session.activeOrganizationId) {
if (
mongo.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to change this Redis status",
@@ -261,7 +291,10 @@ export const redisRouter = createTRPCRouter({
const redis = await findRedisById(input.redisId);
if (redis.project.organizationId !== ctx.session.activeOrganizationId) {
if (
redis.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to delete this Redis",
@@ -284,7 +317,10 @@ export const redisRouter = createTRPCRouter({
.input(apiSaveEnvironmentVariablesRedis)
.mutation(async ({ input, ctx }) => {
const redis = await findRedisById(input.redisId);
if (redis.project.organizationId !== ctx.session.activeOrganizationId) {
if (
redis.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to save this environment",
@@ -324,23 +360,31 @@ export const redisRouter = createTRPCRouter({
.input(
z.object({
redisId: z.string(),
targetProjectId: z.string(),
targetEnvironmentId: z.string(),
}),
)
.mutation(async ({ input, ctx }) => {
const redis = await findRedisById(input.redisId);
if (redis.project.organizationId !== ctx.session.activeOrganizationId) {
if (
redis.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to move this redis",
});
}
const targetProject = await findProjectById(input.targetProjectId);
if (targetProject.organizationId !== ctx.session.activeOrganizationId) {
const targetEnvironment = await findEnvironmentById(
input.targetEnvironmentId,
);
if (
targetEnvironment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to move to this project",
message: "You are not authorized to move to this environment",
});
}
@@ -348,7 +392,7 @@ export const redisRouter = createTRPCRouter({
const updatedRedis = await db
.update(redisTable)
.set({
projectId: input.targetProjectId,
environmentId: input.targetEnvironmentId,
})
.where(eq(redisTable.redisId, input.redisId))
.returning()
@@ -367,7 +411,10 @@ export const redisRouter = createTRPCRouter({
.input(apiRebuildRedis)
.mutation(async ({ input, ctx }) => {
const redis = await findRedisById(input.redisId);
if (redis.project.organizationId !== ctx.session.activeOrganizationId) {
if (
redis.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to rebuild this Redis database",

View File

@@ -1,4 +1,8 @@
import { removeRollbackById, rollback } from "@dokploy/server";
import {
findRollbackById,
removeRollbackById,
rollback,
} from "@dokploy/server";
import { TRPCError } from "@trpc/server";
import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc";
import { apiFindOneRollback } from "@/server/db/schema";
@@ -22,8 +26,18 @@ export const rollbackRouter = createTRPCRouter({
}),
rollback: protectedProcedure
.input(apiFindOneRollback)
.mutation(async ({ input }) => {
.mutation(async ({ input, ctx }) => {
try {
const currentRollback = await findRollbackById(input.rollbackId);
if (
currentRollback?.deployment?.application?.environment?.project
.organizationId !== ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to rollback this deployment",
});
}
return await rollback(input.rollbackId);
} catch (error) {
console.error(error);

View File

@@ -19,7 +19,8 @@ export const securityRouter = createTRPCRouter({
.mutation(async ({ input, ctx }) => {
const application = await findApplicationById(input.applicationId);
if (
application.project.organizationId !== ctx.session.activeOrganizationId
application.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
@@ -34,7 +35,8 @@ export const securityRouter = createTRPCRouter({
const security = await findSecurityById(input.securityId);
const application = await findApplicationById(security.applicationId);
if (
application.project.organizationId !== ctx.session.activeOrganizationId
application.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
@@ -49,7 +51,8 @@ export const securityRouter = createTRPCRouter({
const security = await findSecurityById(input.securityId);
const application = await findApplicationById(security.applicationId);
if (
application.project.organizationId !== ctx.session.activeOrganizationId
application.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
@@ -64,7 +67,8 @@ export const securityRouter = createTRPCRouter({
const security = await findSecurityById(input.securityId);
const application = await findApplicationById(security.applicationId);
if (
application.project.organizationId !== ctx.session.activeOrganizationId
application.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",

View File

@@ -6,14 +6,18 @@ declare global {
var db: PostgresJsDatabase<typeof schema> | undefined;
}
const dbUrl =
process.env.DATABASE_URL ||
"postgres://dokploy:amukds4wi9001583845717ad2@dokploy-postgres:5432/dokploy";
export let db: PostgresJsDatabase<typeof schema>;
if (process.env.NODE_ENV === "production") {
db = drizzle(postgres(process.env.DATABASE_URL!), {
db = drizzle(postgres(dbUrl!), {
schema,
});
} else {
if (!global.db)
global.db = drizzle(postgres(process.env.DATABASE_URL!), {
global.db = drizzle(postgres(dbUrl!), {
schema,
});

View File

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

View File

@@ -111,4 +111,4 @@
"node": "^20.16.0",
"pnpm": ">=9.12.0"
}
}
}

View File

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

View File

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

View File

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

View File

@@ -112,6 +112,10 @@ export const member = pgTable("member", {
.array()
.notNull()
.default(sql`ARRAY[]::text[]`),
accessedEnvironments: text("accessedEnvironments")
.array()
.notNull()
.default(sql`ARRAY[]::text[]`),
accessedServices: text("accesedServices")
.array()
.notNull()

View File

@@ -55,7 +55,7 @@ export const apiUpdateAi = createSchema
.omit({ organizationId: true });
export const deploySuggestionSchema = z.object({
projectId: z.string().min(1),
environmentId: z.string().min(1),
id: z.string().min(1),
dockerCompose: z.string().min(1),
envVariables: z.string(),

View File

@@ -13,6 +13,7 @@ import { z } from "zod";
import { bitbucket } from "./bitbucket";
import { deployments } from "./deployment";
import { domains } from "./domain";
import { environments } from "./environment";
import { gitea } from "./gitea";
import { github } from "./github";
import { gitlab } from "./gitlab";
@@ -179,9 +180,9 @@ export const applications = pgTable("application", {
registryId: text("registryId").references(() => registry.registryId, {
onDelete: "set null",
}),
projectId: text("projectId")
environmentId: text("environmentId")
.notNull()
.references(() => projects.projectId, { onDelete: "cascade" }),
.references(() => environments.environmentId, { onDelete: "cascade" }),
githubId: text("githubId").references(() => github.githubId, {
onDelete: "set null",
}),
@@ -202,9 +203,9 @@ export const applications = pgTable("application", {
export const applicationsRelations = relations(
applications,
({ one, many }) => ({
project: one(projects, {
fields: [applications.projectId],
references: [projects.projectId],
environment: one(environments, {
fields: [applications.environmentId],
references: [environments.environmentId],
}),
deployments: many(deployments),
customGitSSHKey: one(sshKeys, {
@@ -273,7 +274,7 @@ const createSchema = createInsertSchema(applications, {
customGitBuildPath: z.string().optional(),
customGitUrl: z.string().optional(),
buildPath: z.string().optional(),
projectId: z.string(),
environmentId: z.string(),
sourceType: z
.enum(["github", "docker", "git", "gitlab", "bitbucket", "gitea", "drop"])
.optional(),
@@ -317,7 +318,7 @@ export const apiCreateApplication = createSchema.pick({
name: true,
appName: true,
description: true,
projectId: true,
environmentId: true,
serverId: true,
});
@@ -327,6 +328,26 @@ export const apiFindOneApplication = createSchema
})
.required();
export const apiDeployApplication = createSchema
.pick({
applicationId: true,
})
.extend({
applicationId: z.string().min(1),
title: z.string().optional(),
description: z.string().optional(),
});
export const apiRedeployApplication = createSchema
.pick({
applicationId: true,
})
.extend({
applicationId: z.string().min(1),
title: z.string().optional(),
description: z.string().optional(),
});
export const apiReloadApplication = createSchema
.pick({
appName: true,

View File

@@ -7,6 +7,7 @@ import { backups } from "./backups";
import { bitbucket } from "./bitbucket";
import { deployments } from "./deployment";
import { domains } from "./domain";
import { environments } from "./environment";
import { gitea } from "./gitea";
import { github } from "./github";
import { gitlab } from "./gitlab";
@@ -84,9 +85,9 @@ export const compose = pgTable("compose", {
.default(false),
triggerType: triggerType("triggerType").default("push"),
composeStatus: applicationStatus("composeStatus").notNull().default("idle"),
projectId: text("projectId")
environmentId: text("environmentId")
.notNull()
.references(() => projects.projectId, { onDelete: "cascade" }),
.references(() => environments.environmentId, { onDelete: "cascade" }),
createdAt: text("createdAt")
.notNull()
.$defaultFn(() => new Date().toISOString()),
@@ -109,9 +110,9 @@ export const compose = pgTable("compose", {
});
export const composeRelations = relations(compose, ({ one, many }) => ({
project: one(projects, {
fields: [compose.projectId],
references: [projects.projectId],
environment: one(environments, {
fields: [compose.environmentId],
references: [environments.environmentId],
}),
deployments: many(deployments),
mounts: many(mounts),
@@ -149,7 +150,7 @@ const createSchema = createInsertSchema(compose, {
description: z.string(),
env: z.string().optional(),
composeFile: z.string().optional(),
projectId: z.string(),
environmentId: z.string(),
customGitSSHKeyId: z.string().optional(),
command: z.string().optional(),
composePath: z.string().min(1),
@@ -160,7 +161,7 @@ const createSchema = createInsertSchema(compose, {
export const apiCreateCompose = createSchema.pick({
name: true,
description: true,
projectId: true,
environmentId: true,
composeType: true,
appName: true,
serverId: true,
@@ -169,7 +170,7 @@ export const apiCreateCompose = createSchema.pick({
export const apiCreateComposeByTemplate = createSchema
.pick({
projectId: true,
environmentId: true,
})
.extend({
id: z.string().min(1),
@@ -180,6 +181,18 @@ export const apiFindCompose = z.object({
composeId: z.string().min(1),
});
export const apiDeployCompose = z.object({
composeId: z.string().min(1),
title: z.string().optional(),
description: z.string().optional(),
});
export const apiRedeployCompose = z.object({
composeId: z.string().min(1),
title: z.string().optional(),
description: z.string().optional(),
});
export const apiDeleteCompose = z.object({
composeId: z.string().min(1),
deleteVolumes: z.boolean(),

View File

@@ -0,0 +1,85 @@
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,
});

View File

@@ -8,6 +8,7 @@ export * from "./compose";
export * from "./deployment";
export * from "./destination";
export * from "./domain";
export * from "./environment";
export * from "./git-provider";
export * from "./gitea";
export * from "./github";

View File

@@ -4,8 +4,8 @@ import { createInsertSchema } from "drizzle-zod";
import { nanoid } from "nanoid";
import { z } from "zod";
import { backups } from "./backups";
import { environments } from "./environment";
import { mounts } from "./mount";
import { projects } from "./project";
import { server } from "./server";
import {
applicationStatus,
@@ -66,18 +66,19 @@ export const mariadb = pgTable("mariadb", {
createdAt: text("createdAt")
.notNull()
.$defaultFn(() => new Date().toISOString()),
projectId: text("projectId")
environmentId: text("environmentId")
.notNull()
.references(() => projects.projectId, { onDelete: "cascade" }),
.references(() => environments.environmentId, { onDelete: "cascade" }),
serverId: text("serverId").references(() => server.serverId, {
onDelete: "cascade",
}),
});
export const mariadbRelations = relations(mariadb, ({ one, many }) => ({
project: one(projects, {
fields: [mariadb.projectId],
references: [projects.projectId],
environment: one(environments, {
fields: [mariadb.environmentId],
references: [environments.environmentId],
}),
backups: many(backups),
mounts: many(mounts),
@@ -94,8 +95,19 @@ const createSchema = createInsertSchema(mariadb, {
createdAt: z.string(),
databaseName: z.string().min(1),
databaseUser: z.string().min(1),
databasePassword: z.string(),
databaseRootPassword: z.string().optional(),
databasePassword: z
.string()
.regex(/^[a-zA-Z0-9@#%^&*()_+\-=[\]{}|;:,.<>?~`]*$/, {
message:
"Password contains invalid characters. Please avoid: $ ! ' \" \\ / and space characters for database compatibility",
}),
databaseRootPassword: z
.string()
.regex(/^[a-zA-Z0-9@#%^&*()_+\-=[\]{}|;:,.<>?~`]*$/, {
message:
"Password contains invalid characters. Please avoid: $ ! ' \" \\ / and space characters for database compatibility",
})
.optional(),
dockerImage: z.string().default("mariadb:6"),
command: z.string().optional(),
env: z.string().optional(),
@@ -103,7 +115,7 @@ const createSchema = createInsertSchema(mariadb, {
memoryLimit: z.string().optional(),
cpuReservation: z.string().optional(),
cpuLimit: z.string().optional(),
projectId: z.string(),
environmentId: z.string(),
applicationStatus: z.enum(["idle", "running", "done", "error"]),
externalPort: z.number(),
description: z.string().optional(),
@@ -124,7 +136,7 @@ export const apiCreateMariaDB = createSchema
appName: true,
dockerImage: true,
databaseRootPassword: true,
projectId: true,
environmentId: true,
description: true,
databaseName: true,
databaseUser: true,

View File

@@ -4,8 +4,8 @@ import { createInsertSchema } from "drizzle-zod";
import { nanoid } from "nanoid";
import { z } from "zod";
import { backups } from "./backups";
import { environments } from "./environment";
import { mounts } from "./mount";
import { projects } from "./project";
import { server } from "./server";
import {
applicationStatus,
@@ -62,9 +62,10 @@ export const mongo = pgTable("mongo", {
createdAt: text("createdAt")
.notNull()
.$defaultFn(() => new Date().toISOString()),
projectId: text("projectId")
environmentId: text("environmentId")
.notNull()
.references(() => projects.projectId, { onDelete: "cascade" }),
.references(() => environments.environmentId, { onDelete: "cascade" }),
serverId: text("serverId").references(() => server.serverId, {
onDelete: "cascade",
}),
@@ -72,9 +73,9 @@ export const mongo = pgTable("mongo", {
});
export const mongoRelations = relations(mongo, ({ one, many }) => ({
project: one(projects, {
fields: [mongo.projectId],
references: [projects.projectId],
environment: one(environments, {
fields: [mongo.environmentId],
references: [environments.environmentId],
}),
backups: many(backups),
mounts: many(mounts),
@@ -89,7 +90,12 @@ const createSchema = createInsertSchema(mongo, {
createdAt: z.string(),
mongoId: z.string(),
name: z.string().min(1),
databasePassword: z.string(),
databasePassword: z
.string()
.regex(/^[a-zA-Z0-9@#%^&*()_+\-=[\]{}|;:,.<>?~`]*$/, {
message:
"Password contains invalid characters. Please avoid: $ ! ' \" \\ / and space characters for database compatibility",
}),
databaseUser: z.string().min(1),
dockerImage: z.string().default("mongo:15"),
command: z.string().optional(),
@@ -98,7 +104,7 @@ const createSchema = createInsertSchema(mongo, {
memoryLimit: z.string().optional(),
cpuReservation: z.string().optional(),
cpuLimit: z.string().optional(),
projectId: z.string(),
environmentId: z.string(),
applicationStatus: z.enum(["idle", "running", "done", "error"]),
externalPort: z.number(),
description: z.string().optional(),
@@ -119,7 +125,7 @@ export const apiCreateMongo = createSchema
name: true,
appName: true,
dockerImage: true,
projectId: true,
environmentId: true,
description: true,
databaseUser: true,
databasePassword: true,

View File

@@ -4,8 +4,8 @@ import { createInsertSchema } from "drizzle-zod";
import { nanoid } from "nanoid";
import { z } from "zod";
import { backups } from "./backups";
import { environments } from "./environment";
import { mounts } from "./mount";
import { projects } from "./project";
import { server } from "./server";
import {
applicationStatus,
@@ -64,18 +64,19 @@ export const mysql = pgTable("mysql", {
createdAt: text("createdAt")
.notNull()
.$defaultFn(() => new Date().toISOString()),
projectId: text("projectId")
environmentId: text("environmentId")
.notNull()
.references(() => projects.projectId, { onDelete: "cascade" }),
.references(() => environments.environmentId, { onDelete: "cascade" }),
serverId: text("serverId").references(() => server.serverId, {
onDelete: "cascade",
}),
});
export const mysqlRelations = relations(mysql, ({ one, many }) => ({
project: one(projects, {
fields: [mysql.projectId],
references: [projects.projectId],
environment: one(environments, {
fields: [mysql.environmentId],
references: [environments.environmentId],
}),
backups: many(backups),
mounts: many(mounts),
@@ -92,8 +93,19 @@ const createSchema = createInsertSchema(mysql, {
name: z.string().min(1),
databaseName: z.string().min(1),
databaseUser: z.string().min(1),
databasePassword: z.string(),
databaseRootPassword: z.string().optional(),
databasePassword: z
.string()
.regex(/^[a-zA-Z0-9@#%^&*()_+\-=[\]{}|;:,.<>?~`]*$/, {
message:
"Password contains invalid characters. Please avoid: $ ! ' \" \\ / and space characters for database compatibility",
}),
databaseRootPassword: z
.string()
.regex(/^[a-zA-Z0-9@#%^&*()_+\-=[\]{}|;:,.<>?~`]*$/, {
message:
"Password contains invalid characters. Please avoid: $ ! ' \" \\ / and space characters for database compatibility",
})
.optional(),
dockerImage: z.string().default("mysql:8"),
command: z.string().optional(),
env: z.string().optional(),
@@ -101,7 +113,6 @@ const createSchema = createInsertSchema(mysql, {
memoryLimit: z.string().optional(),
cpuReservation: z.string().optional(),
cpuLimit: z.string().optional(),
projectId: z.string(),
applicationStatus: z.enum(["idle", "running", "done", "error"]),
externalPort: z.number(),
description: z.string().optional(),
@@ -121,7 +132,7 @@ export const apiCreateMySql = createSchema
name: true,
appName: true,
dockerImage: true,
projectId: true,
environmentId: true,
description: true,
databaseName: true,
databaseUser: true,

View File

@@ -4,8 +4,8 @@ import { createInsertSchema } from "drizzle-zod";
import { nanoid } from "nanoid";
import { z } from "zod";
import { backups } from "./backups";
import { environments } from "./environment";
import { mounts } from "./mount";
import { projects } from "./project";
import { server } from "./server";
import {
applicationStatus,
@@ -64,18 +64,19 @@ export const postgres = pgTable("postgres", {
createdAt: text("createdAt")
.notNull()
.$defaultFn(() => new Date().toISOString()),
projectId: text("projectId")
environmentId: text("environmentId")
.notNull()
.references(() => projects.projectId, { onDelete: "cascade" }),
.references(() => environments.environmentId, { onDelete: "cascade" }),
serverId: text("serverId").references(() => server.serverId, {
onDelete: "cascade",
}),
});
export const postgresRelations = relations(postgres, ({ one, many }) => ({
project: one(projects, {
fields: [postgres.projectId],
references: [projects.projectId],
environment: one(environments, {
fields: [postgres.environmentId],
references: [environments.environmentId],
}),
backups: many(backups),
mounts: many(mounts),
@@ -88,7 +89,12 @@ export const postgresRelations = relations(postgres, ({ one, many }) => ({
const createSchema = createInsertSchema(postgres, {
postgresId: z.string(),
name: z.string().min(1),
databasePassword: z.string(),
databasePassword: z
.string()
.regex(/^[a-zA-Z0-9@#%^&*()_+\-=[\]{}|;:,.<>?~`]*$/, {
message:
"Password contains invalid characters. Please avoid: $ ! ' \" \\ / and space characters for database compatibility",
}),
databaseName: z.string().min(1),
databaseUser: z.string().min(1),
dockerImage: z.string().default("postgres:15"),
@@ -98,7 +104,7 @@ const createSchema = createInsertSchema(postgres, {
memoryLimit: z.string().optional(),
cpuReservation: z.string().optional(),
cpuLimit: z.string().optional(),
projectId: z.string(),
environmentId: z.string(),
applicationStatus: z.enum(["idle", "running", "done", "error"]),
externalPort: z.number(),
createdAt: z.string(),
@@ -122,7 +128,7 @@ export const apiCreatePostgres = createSchema
databaseUser: true,
databasePassword: true,
dockerImage: true,
projectId: true,
environmentId: true,
description: true,
serverId: true,
})

View File

@@ -4,13 +4,7 @@ import { createInsertSchema } from "drizzle-zod";
import { nanoid } from "nanoid";
import { z } from "zod";
import { organization } from "./account";
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 { redis } from "./redis";
import { environments } from "./environment";
export const projects = pgTable("project", {
projectId: text("projectId")
@@ -30,13 +24,7 @@ export const projects = pgTable("project", {
});
export const projectRelations = relations(projects, ({ many, one }) => ({
mysql: many(mysql),
postgres: many(postgres),
mariadb: many(mariadb),
applications: many(applications),
mongo: many(mongo),
redis: many(redis),
compose: many(compose),
environments: many(environments),
organization: one(organization, {
fields: [projects.organizationId],
references: [organization.id],

View File

@@ -3,6 +3,7 @@ import { integer, json, pgTable, text } from "drizzle-orm/pg-core";
import { createInsertSchema } from "drizzle-zod";
import { nanoid } from "nanoid";
import { z } from "zod";
import { environments } from "./environment";
import { mounts } from "./mount";
import { projects } from "./project";
import { server } from "./server";
@@ -60,18 +61,19 @@ export const redis = pgTable("redis", {
labelsSwarm: json("labelsSwarm").$type<LabelsSwarm>(),
networkSwarm: json("networkSwarm").$type<NetworkSwarm[]>(),
replicas: integer("replicas").default(1).notNull(),
projectId: text("projectId")
environmentId: text("environmentId")
.notNull()
.references(() => projects.projectId, { onDelete: "cascade" }),
.references(() => environments.environmentId, { onDelete: "cascade" }),
serverId: text("serverId").references(() => server.serverId, {
onDelete: "cascade",
}),
});
export const redisRelations = relations(redis, ({ one, many }) => ({
project: one(projects, {
fields: [redis.projectId],
references: [projects.projectId],
environment: one(environments, {
fields: [redis.environmentId],
references: [environments.environmentId],
}),
mounts: many(mounts),
server: one(server, {
@@ -93,7 +95,7 @@ const createSchema = createInsertSchema(redis, {
memoryLimit: z.string().optional(),
cpuReservation: z.string().optional(),
cpuLimit: z.string().optional(),
projectId: z.string(),
environmentId: z.string(),
applicationStatus: z.enum(["idle", "running", "done", "error"]),
externalPort: z.number(),
description: z.string().optional(),
@@ -114,7 +116,7 @@ export const apiCreateRedis = createSchema
appName: true,
databasePassword: true,
dockerImage: true,
projectId: true,
environmentId: true,
description: true,
serverId: true,
})

View File

@@ -27,7 +27,9 @@ export const rollbacks = pgTable("rollback", {
.$defaultFn(() => new Date().toISOString()),
fullContext: jsonb("fullContext").$type<
Application & {
project: Project;
environment: {
project: Project;
};
mounts: Mount[];
ports: Port[];
registry?: Registry | null;

View File

@@ -175,6 +175,7 @@ export const apiAssignPermissions = createSchema
})
.extend({
accessedProjects: z.array(z.string()).optional(),
accessedEnvironments: z.array(z.string()).optional(),
accessedServices: z.array(z.string()).optional(),
canCreateProjects: z.boolean().optional(),
canCreateServices: z.boolean().optional(),

View File

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

View File

@@ -16,6 +16,7 @@ export * from "./services/deployment";
export * from "./services/destination";
export * from "./services/docker";
export * from "./services/domain";
export * from "./services/environment";
export * from "./services/git-provider";
export * from "./services/gitea";
export * from "./services/github";

View File

@@ -105,7 +105,11 @@ export const findApplicationById = async (applicationId: string) => {
const application = await db.query.applications.findFirst({
where: eq(applications.applicationId, applicationId),
with: {
project: true,
environment: {
with: {
project: true,
},
},
domains: true,
deployments: true,
mounts: true,
@@ -180,7 +184,7 @@ export const deployApplication = async ({
}) => {
const application = await findApplicationById(applicationId);
const buildLink = `${await getDokployUrl()}/dashboard/project/${application.projectId}/services/application/${application.applicationId}?tab=deployments`;
const buildLink = `${await getDokployUrl()}/dashboard/project/${application.environment.projectId}/environment/${application.environmentId}/services/application/${application.applicationId}?tab=deployments`;
const deployment = await createDeployment({
applicationId: applicationId,
title: titleLog,
@@ -227,11 +231,11 @@ export const deployApplication = async ({
}
await sendBuildSuccessNotifications({
projectName: application.project.name,
projectName: application.environment.project.name,
applicationName: application.name,
applicationType: "application",
buildLink,
organizationId: application.project.organizationId,
organizationId: application.environment.project.organizationId,
domains: application.domains,
});
} catch (error) {
@@ -239,13 +243,13 @@ export const deployApplication = async ({
await updateApplicationStatus(applicationId, "error");
await sendBuildErrorNotifications({
projectName: application.project.name,
projectName: application.environment.project.name,
applicationName: application.name,
applicationType: "application",
// @ts-ignore
errorMessage: error?.message || "Error building",
buildLink,
organizationId: application.project.organizationId,
organizationId: application.environment.project.organizationId,
});
throw error;
@@ -307,7 +311,7 @@ export const deployRemoteApplication = async ({
}) => {
const application = await findApplicationById(applicationId);
const buildLink = `${await getDokployUrl()}/dashboard/project/${application.projectId}/services/application/${application.applicationId}?tab=deployments`;
const buildLink = `${await getDokployUrl()}/dashboard/project/${application.environment.projectId}/environment/${application.environmentId}/services/application/${application.applicationId}?tab=deployments`;
const deployment = await createDeployment({
applicationId: applicationId,
title: titleLog,
@@ -363,11 +367,11 @@ export const deployRemoteApplication = async ({
}
await sendBuildSuccessNotifications({
projectName: application.project.name,
projectName: application.environment.project.name,
applicationName: application.name,
applicationType: "application",
buildLink,
organizationId: application.project.organizationId,
organizationId: application.environment.project.organizationId,
domains: application.domains,
});
} catch (error) {
@@ -387,12 +391,12 @@ export const deployRemoteApplication = async ({
await updateApplicationStatus(applicationId, "error");
await sendBuildErrorNotifications({
projectName: application.project.name,
projectName: application.environment.project.name,
applicationName: application.name,
applicationType: "application",
errorMessage: `Please check the logs for details: ${errorMessage}`,
buildLink,
organizationId: application.project.organizationId,
organizationId: application.environment.project.organizationId,
});
throw error;

View File

@@ -126,7 +126,11 @@ export const findComposeById = async (composeId: string) => {
const result = await db.query.compose.findFirst({
where: eq(compose.composeId, composeId),
with: {
project: true,
environment: {
with: {
project: true,
},
},
deployments: true,
mounts: true,
domains: true,
@@ -222,7 +226,7 @@ export const deployCompose = async ({
const compose = await findComposeById(composeId);
const buildLink = `${await getDokployUrl()}/dashboard/project/${
compose.projectId
compose.environment.projectId
}/services/compose/${compose.composeId}?tab=deployments`;
const deployment = await createDeploymentCompose({
composeId: composeId,
@@ -255,11 +259,11 @@ export const deployCompose = async ({
});
await sendBuildSuccessNotifications({
projectName: compose.project.name,
projectName: compose.environment.project.name,
applicationName: compose.name,
applicationType: "compose",
buildLink,
organizationId: compose.project.organizationId,
organizationId: compose.environment.project.organizationId,
domains: compose.domains,
});
} catch (error) {
@@ -268,13 +272,13 @@ export const deployCompose = async ({
composeStatus: "error",
});
await sendBuildErrorNotifications({
projectName: compose.project.name,
projectName: compose.environment.project.name,
applicationName: compose.name,
applicationType: "compose",
// @ts-ignore
errorMessage: error?.message || "Error building",
buildLink,
organizationId: compose.project.organizationId,
organizationId: compose.environment.project.organizationId,
});
throw error;
}
@@ -330,7 +334,7 @@ export const deployRemoteCompose = async ({
const compose = await findComposeById(composeId);
const buildLink = `${await getDokployUrl()}/dashboard/project/${
compose.projectId
compose.environment.projectId
}/services/compose/${compose.composeId}?tab=deployments`;
const deployment = await createDeploymentCompose({
composeId: composeId,
@@ -387,11 +391,11 @@ export const deployRemoteCompose = async ({
});
await sendBuildSuccessNotifications({
projectName: compose.project.name,
projectName: compose.environment.project.name,
applicationName: compose.name,
applicationType: "compose",
buildLink,
organizationId: compose.project.organizationId,
organizationId: compose.environment.project.organizationId,
domains: compose.domains,
});
} catch (error) {
@@ -410,13 +414,13 @@ export const deployRemoteCompose = async ({
composeStatus: "error",
});
await sendBuildErrorNotifications({
projectName: compose.project.name,
projectName: compose.environment.project.name,
applicationName: compose.name,
applicationType: "compose",
// @ts-ignore
errorMessage: error?.message || "Error building",
buildLink,
organizationId: compose.project.organizationId,
organizationId: compose.environment.project.organizationId,
});
throw error;
}

View File

@@ -0,0 +1,140 @@
import { db } from "@dokploy/server/db";
import {
type apiCreateEnvironment,
type apiDuplicateEnvironment,
environments,
} from "@dokploy/server/db/schema";
import { TRPCError } from "@trpc/server";
import { asc, eq } from "drizzle-orm";
export type Environment = typeof environments.$inferSelect;
export const createEnvironment = async (
input: typeof apiCreateEnvironment._type,
) => {
const newEnvironment = await db
.insert(environments)
.values({
...input,
})
.returning()
.then((value) => value[0]);
if (!newEnvironment) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Error creating the environment",
});
}
return newEnvironment;
};
export const findEnvironmentById = async (environmentId: string) => {
const environment = await db.query.environments.findFirst({
where: eq(environments.environmentId, environmentId),
with: {
applications: true,
mariadb: true,
mongo: true,
mysql: true,
postgres: true,
redis: true,
compose: true,
project: true,
},
});
if (!environment) {
throw new TRPCError({
code: "NOT_FOUND",
message: "Environment not found",
});
}
return environment;
};
export const findEnvironmentsByProjectId = async (projectId: string) => {
const projectEnvironments = await db.query.environments.findMany({
where: eq(environments.projectId, projectId),
orderBy: asc(environments.createdAt),
with: {
applications: true,
mariadb: true,
mongo: true,
mysql: true,
postgres: true,
redis: true,
compose: true,
project: true,
},
});
return projectEnvironments;
};
export const deleteEnvironment = async (environmentId: string) => {
const currentEnvironment = await findEnvironmentById(environmentId);
if (currentEnvironment.name === "production") {
throw new TRPCError({
code: "BAD_REQUEST",
message: "You cannot delete the production environment",
});
}
const deletedEnvironment = await db
.delete(environments)
.where(eq(environments.environmentId, environmentId))
.returning()
.then((value) => value[0]);
return deletedEnvironment;
};
export const updateEnvironmentById = async (
environmentId: string,
environmentData: Partial<Environment>,
) => {
const result = await db
.update(environments)
.set({
...environmentData,
})
.where(eq(environments.environmentId, environmentId))
.returning()
.then((res) => res[0]);
return result;
};
export const duplicateEnvironment = async (
input: typeof apiDuplicateEnvironment._type,
) => {
// Find the original environment
const originalEnvironment = await findEnvironmentById(input.environmentId);
// Create a new environment with the provided name and description
const newEnvironment = await db
.insert(environments)
.values({
name: input.name,
description: input.description || originalEnvironment.description,
projectId: originalEnvironment.projectId,
})
.returning()
.then((value) => value[0]);
if (!newEnvironment) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Error duplicating the environment",
});
}
return newEnvironment;
};
export const createProductionEnvironment = async (projectId: string) => {
return createEnvironment({
name: "production",
description: "Production environment",
projectId,
});
};

View File

@@ -56,7 +56,11 @@ export const findMariadbById = async (mariadbId: string) => {
const result = await db.query.mariadb.findFirst({
where: eq(mariadb.mariadbId, mariadbId),
with: {
project: true,
environment: {
with: {
project: true,
},
},
mounts: true,
server: true,
backups: {

View File

@@ -53,7 +53,11 @@ export const findMongoById = async (mongoId: string) => {
const result = await db.query.mongo.findFirst({
where: eq(mongo.mongoId, mongoId),
with: {
project: true,
environment: {
with: {
project: true,
},
},
mounts: true,
server: true,
backups: {

View File

@@ -105,13 +105,69 @@ export const findMountById = async (mountId: string) => {
const mount = await db.query.mounts.findFirst({
where: eq(mounts.mountId, mountId),
with: {
application: true,
postgres: true,
mariadb: true,
mongo: true,
mysql: true,
redis: true,
compose: true,
application: {
with: {
environment: {
with: {
project: true,
},
},
},
},
postgres: {
with: {
environment: {
with: {
project: true,
},
},
},
},
mariadb: {
with: {
environment: {
with: {
project: true,
},
},
},
},
mongo: {
with: {
environment: {
with: {
project: true,
},
},
},
},
mysql: {
with: {
environment: {
with: {
project: true,
},
},
},
},
redis: {
with: {
environment: {
with: {
project: true,
},
},
},
},
compose: {
with: {
environment: {
with: {
project: true,
},
},
},
},
},
});
if (!mount) {
@@ -123,6 +179,34 @@ export const findMountById = async (mountId: string) => {
return mount;
};
export const findMountOrganizationId = async (mountId: string) => {
const mount = await findMountById(mountId);
if (mount.application) {
return mount.application.environment.project.organizationId;
}
if (mount.postgres) {
return mount.postgres.environment.project.organizationId;
}
if (mount.mariadb) {
return mount.mariadb.environment.project.organizationId;
}
if (mount.mongo) {
return mount.mongo.environment.project.organizationId;
}
if (mount.mysql) {
return mount.mysql.environment.project.organizationId;
}
if (mount.redis) {
return mount.redis.environment.project.organizationId;
}
if (mount.compose) {
return mount.compose.environment.project.organizationId;
}
return null;
};
export const updateMount = async (
mountId: string,
mountData: Partial<Mount>,

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