mirror of
https://github.com/Dokploy/dokploy.git
synced 2026-06-16 04:35:24 +02:00
Compare commits
40 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d0c92d84ef | ||
|
|
72974e00a6 | ||
|
|
d96e2bbeb7 | ||
|
|
a45d8ee8f4 | ||
|
|
9067452a38 | ||
|
|
1fa4d5b2ba | ||
|
|
bade36ea9d | ||
|
|
0c22041623 | ||
|
|
cccee05173 | ||
|
|
9f9c8fccf2 | ||
|
|
ad2e53a67a | ||
|
|
00f3853bd7 | ||
|
|
2880327e94 | ||
|
|
827b84f57e | ||
|
|
11aa8fe0c5 | ||
|
|
b9ac720d99 | ||
|
|
77b0ff7bbf | ||
|
|
e7af2c0ebd | ||
|
|
6a1bedb90f | ||
|
|
a2f142174b | ||
|
|
f4ce304a04 | ||
|
|
bb521f3e7e | ||
|
|
baaa470234 | ||
|
|
4871520dbb | ||
|
|
dad49ec96f | ||
|
|
ce4e37c75b | ||
|
|
c317ec39cb | ||
|
|
a4e9c6e890 | ||
|
|
72fb85f616 | ||
|
|
1e7a6f2071 | ||
|
|
5ffd664570 | ||
|
|
947100c041 | ||
|
|
5410a56638 | ||
|
|
8127dc4536 | ||
|
|
2f37235aea | ||
|
|
290267bca4 | ||
|
|
8eace173b9 | ||
|
|
c9a9ed8164 | ||
|
|
30428053e8 | ||
|
|
1c0dbbcfd6 |
144
apps/dokploy/__test__/permissions/check-permission.test.ts
Normal file
144
apps/dokploy/__test__/permissions/check-permission.test.ts
Normal file
@@ -0,0 +1,144 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const mockMemberData = (
|
||||
role: string,
|
||||
overrides: Record<string, boolean> = {},
|
||||
) => ({
|
||||
id: "member-1",
|
||||
role,
|
||||
userId: "user-1",
|
||||
organizationId: "org-1",
|
||||
accessedProjects: [] as string[],
|
||||
accessedServices: [] as string[],
|
||||
accessedEnvironments: [] as string[],
|
||||
canCreateProjects: overrides.canCreateProjects ?? false,
|
||||
canDeleteProjects: overrides.canDeleteProjects ?? false,
|
||||
canCreateServices: overrides.canCreateServices ?? false,
|
||||
canDeleteServices: overrides.canDeleteServices ?? false,
|
||||
canCreateEnvironments: overrides.canCreateEnvironments ?? false,
|
||||
canDeleteEnvironments: overrides.canDeleteEnvironments ?? false,
|
||||
canAccessToTraefikFiles: overrides.canAccessToTraefikFiles ?? false,
|
||||
canAccessToDocker: overrides.canAccessToDocker ?? false,
|
||||
canAccessToAPI: overrides.canAccessToAPI ?? false,
|
||||
canAccessToSSHKeys: overrides.canAccessToSSHKeys ?? false,
|
||||
canAccessToGitProviders: overrides.canAccessToGitProviders ?? false,
|
||||
user: { id: "user-1", email: "test@test.com" },
|
||||
});
|
||||
|
||||
let memberToReturn: ReturnType<typeof mockMemberData> =
|
||||
mockMemberData("member");
|
||||
|
||||
vi.mock("@dokploy/server/db", () => ({
|
||||
db: {
|
||||
query: {
|
||||
member: {
|
||||
findFirst: vi.fn(() => Promise.resolve(memberToReturn)),
|
||||
findMany: vi.fn(() => Promise.resolve([])),
|
||||
},
|
||||
organizationRole: {
|
||||
findFirst: vi.fn(),
|
||||
findMany: vi.fn(() => Promise.resolve([])),
|
||||
},
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("@dokploy/server/services/proprietary/license-key", () => ({
|
||||
hasValidLicense: vi.fn(() => Promise.resolve(false)),
|
||||
}));
|
||||
|
||||
const { checkPermission } = await import("@dokploy/server/services/permission");
|
||||
|
||||
const ctx = {
|
||||
user: { id: "user-1" },
|
||||
session: { activeOrganizationId: "org-1" },
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe("static roles bypass enterprise resources", () => {
|
||||
it("owner bypasses deployment.read", async () => {
|
||||
memberToReturn = mockMemberData("owner");
|
||||
await expect(
|
||||
checkPermission(ctx, { deployment: ["read"] }),
|
||||
).resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
it("admin bypasses backup.create", async () => {
|
||||
memberToReturn = mockMemberData("admin");
|
||||
await expect(
|
||||
checkPermission(ctx, { backup: ["create"] }),
|
||||
).resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
it("member bypasses schedule.delete", async () => {
|
||||
memberToReturn = mockMemberData("member");
|
||||
await expect(
|
||||
checkPermission(ctx, { schedule: ["delete"] }),
|
||||
).resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
it("member bypasses multiple enterprise permissions at once", async () => {
|
||||
memberToReturn = mockMemberData("member");
|
||||
await expect(
|
||||
checkPermission(ctx, {
|
||||
deployment: ["read"],
|
||||
backup: ["create"],
|
||||
domain: ["delete"],
|
||||
}),
|
||||
).resolves.toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("static roles validate free-tier resources", () => {
|
||||
it("owner passes project.create", async () => {
|
||||
memberToReturn = mockMemberData("owner");
|
||||
await expect(
|
||||
checkPermission(ctx, { project: ["create"] }),
|
||||
).resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
it("member fails project.create (no legacy override)", async () => {
|
||||
memberToReturn = mockMemberData("member");
|
||||
await expect(
|
||||
checkPermission(ctx, { project: ["create"] }),
|
||||
).rejects.toThrow();
|
||||
});
|
||||
|
||||
it("member passes service.read", async () => {
|
||||
memberToReturn = mockMemberData("member");
|
||||
await expect(
|
||||
checkPermission(ctx, { service: ["read"] }),
|
||||
).resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
it("member fails service.create", async () => {
|
||||
memberToReturn = mockMemberData("member");
|
||||
await expect(
|
||||
checkPermission(ctx, { service: ["create"] }),
|
||||
).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe("legacy boolean overrides for member", () => {
|
||||
it("member passes project.create with canCreateProjects=true", async () => {
|
||||
memberToReturn = mockMemberData("member", { canCreateProjects: true });
|
||||
await expect(
|
||||
checkPermission(ctx, { project: ["create"] }),
|
||||
).resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
it("member passes docker.read with canAccessToDocker=true", async () => {
|
||||
memberToReturn = mockMemberData("member", { canAccessToDocker: true });
|
||||
await expect(
|
||||
checkPermission(ctx, { docker: ["read"] }),
|
||||
).resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
it("member fails docker.read with canAccessToDocker=false", async () => {
|
||||
memberToReturn = mockMemberData("member");
|
||||
await expect(checkPermission(ctx, { docker: ["read"] })).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,78 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import {
|
||||
enterpriseOnlyResources,
|
||||
statements,
|
||||
} from "@dokploy/server/lib/access-control";
|
||||
|
||||
const FREE_TIER_RESOURCES = [
|
||||
"organization",
|
||||
"member",
|
||||
"invitation",
|
||||
"team",
|
||||
"ac",
|
||||
"project",
|
||||
"service",
|
||||
"environment",
|
||||
"docker",
|
||||
"sshKeys",
|
||||
"gitProviders",
|
||||
"traefikFiles",
|
||||
"api",
|
||||
];
|
||||
|
||||
const ENTERPRISE_RESOURCES = [
|
||||
"volume",
|
||||
"deployment",
|
||||
"envVars",
|
||||
"projectEnvVars",
|
||||
"environmentEnvVars",
|
||||
"server",
|
||||
"registry",
|
||||
"certificate",
|
||||
"backup",
|
||||
"volumeBackup",
|
||||
"schedule",
|
||||
"domain",
|
||||
"destination",
|
||||
"notification",
|
||||
"logs",
|
||||
"monitoring",
|
||||
"auditLog",
|
||||
];
|
||||
|
||||
describe("enterpriseOnlyResources set", () => {
|
||||
it("contains all enterprise resources", () => {
|
||||
for (const resource of ENTERPRISE_RESOURCES) {
|
||||
expect(enterpriseOnlyResources.has(resource)).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
it("does NOT contain free-tier resources", () => {
|
||||
for (const resource of FREE_TIER_RESOURCES) {
|
||||
expect(enterpriseOnlyResources.has(resource)).toBe(false);
|
||||
}
|
||||
});
|
||||
|
||||
it("every resource in statements is either free or enterprise", () => {
|
||||
const allResources = Object.keys(statements);
|
||||
for (const resource of allResources) {
|
||||
const isFree = FREE_TIER_RESOURCES.includes(resource);
|
||||
const isEnterprise = enterpriseOnlyResources.has(resource);
|
||||
expect(isFree || isEnterprise).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
it("free and enterprise sets don't overlap", () => {
|
||||
for (const resource of FREE_TIER_RESOURCES) {
|
||||
expect(enterpriseOnlyResources.has(resource)).toBe(false);
|
||||
}
|
||||
});
|
||||
|
||||
it("all statement resources are accounted for", () => {
|
||||
const allResources = Object.keys(statements);
|
||||
const categorized = [...FREE_TIER_RESOURCES, ...ENTERPRISE_RESOURCES];
|
||||
for (const resource of allResources) {
|
||||
expect(categorized).toContain(resource);
|
||||
}
|
||||
});
|
||||
});
|
||||
161
apps/dokploy/__test__/permissions/resolve-permissions.test.ts
Normal file
161
apps/dokploy/__test__/permissions/resolve-permissions.test.ts
Normal file
@@ -0,0 +1,161 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const mockMemberData = (
|
||||
role: string,
|
||||
overrides: Record<string, boolean> = {},
|
||||
) => ({
|
||||
id: "member-1",
|
||||
role,
|
||||
userId: "user-1",
|
||||
organizationId: "org-1",
|
||||
accessedProjects: [] as string[],
|
||||
accessedServices: [] as string[],
|
||||
accessedEnvironments: [] as string[],
|
||||
canCreateProjects: overrides.canCreateProjects ?? false,
|
||||
canDeleteProjects: overrides.canDeleteProjects ?? false,
|
||||
canCreateServices: overrides.canCreateServices ?? false,
|
||||
canDeleteServices: overrides.canDeleteServices ?? false,
|
||||
canCreateEnvironments: overrides.canCreateEnvironments ?? false,
|
||||
canDeleteEnvironments: overrides.canDeleteEnvironments ?? false,
|
||||
canAccessToTraefikFiles: overrides.canAccessToTraefikFiles ?? false,
|
||||
canAccessToDocker: overrides.canAccessToDocker ?? false,
|
||||
canAccessToAPI: overrides.canAccessToAPI ?? false,
|
||||
canAccessToSSHKeys: overrides.canAccessToSSHKeys ?? false,
|
||||
canAccessToGitProviders: overrides.canAccessToGitProviders ?? false,
|
||||
user: { id: "user-1", email: "test@test.com" },
|
||||
});
|
||||
|
||||
let memberToReturn: ReturnType<typeof mockMemberData> =
|
||||
mockMemberData("member");
|
||||
|
||||
vi.mock("@dokploy/server/db", () => ({
|
||||
db: {
|
||||
query: {
|
||||
member: {
|
||||
findFirst: vi.fn(() => Promise.resolve(memberToReturn)),
|
||||
findMany: vi.fn(() => Promise.resolve([])),
|
||||
},
|
||||
organizationRole: {
|
||||
findFirst: vi.fn(),
|
||||
findMany: vi.fn(() => Promise.resolve([])),
|
||||
},
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("@dokploy/server/services/proprietary/license-key", () => ({
|
||||
hasValidLicense: vi.fn(() => Promise.resolve(false)),
|
||||
}));
|
||||
|
||||
const { resolvePermissions } = await import(
|
||||
"@dokploy/server/services/permission"
|
||||
);
|
||||
const { enterpriseOnlyResources, statements } = await import(
|
||||
"@dokploy/server/lib/access-control"
|
||||
);
|
||||
|
||||
const ctx = {
|
||||
user: { id: "user-1" },
|
||||
session: { activeOrganizationId: "org-1" },
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe("enterprise resources for static roles", () => {
|
||||
it("owner gets true for all enterprise resources", async () => {
|
||||
memberToReturn = mockMemberData("owner");
|
||||
const perms = await resolvePermissions(ctx);
|
||||
|
||||
for (const resource of enterpriseOnlyResources) {
|
||||
const actions = statements[resource as keyof typeof statements];
|
||||
for (const action of actions) {
|
||||
expect((perms as any)[resource][action]).toBe(true);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it("admin gets true for all enterprise resources", async () => {
|
||||
memberToReturn = mockMemberData("admin");
|
||||
const perms = await resolvePermissions(ctx);
|
||||
|
||||
for (const resource of enterpriseOnlyResources) {
|
||||
const actions = statements[resource as keyof typeof statements];
|
||||
for (const action of actions) {
|
||||
expect((perms as any)[resource][action]).toBe(true);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it("member gets true for service-level enterprise resources", async () => {
|
||||
memberToReturn = mockMemberData("member");
|
||||
const perms = await resolvePermissions(ctx);
|
||||
|
||||
expect(perms.deployment.read).toBe(true);
|
||||
expect(perms.deployment.create).toBe(true);
|
||||
expect(perms.domain.read).toBe(true);
|
||||
expect(perms.backup.read).toBe(true);
|
||||
expect(perms.logs.read).toBe(true);
|
||||
expect(perms.monitoring.read).toBe(true);
|
||||
});
|
||||
|
||||
it("member gets false for org-level enterprise resources", async () => {
|
||||
memberToReturn = mockMemberData("member");
|
||||
const perms = await resolvePermissions(ctx);
|
||||
|
||||
expect(perms.server.read).toBe(false);
|
||||
expect(perms.registry.read).toBe(false);
|
||||
expect(perms.certificate.read).toBe(false);
|
||||
expect(perms.destination.read).toBe(false);
|
||||
expect(perms.notification.read).toBe(false);
|
||||
expect(perms.auditLog.read).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("free-tier resources for member", () => {
|
||||
it("member gets service.read=true", async () => {
|
||||
memberToReturn = mockMemberData("member");
|
||||
const perms = await resolvePermissions(ctx);
|
||||
expect(perms.service.read).toBe(true);
|
||||
});
|
||||
|
||||
it("member gets project.create=false without legacy override", async () => {
|
||||
memberToReturn = mockMemberData("member");
|
||||
const perms = await resolvePermissions(ctx);
|
||||
expect(perms.project.create).toBe(false);
|
||||
});
|
||||
|
||||
it("member gets project.create=true with canCreateProjects", async () => {
|
||||
memberToReturn = mockMemberData("member", { canCreateProjects: true });
|
||||
const perms = await resolvePermissions(ctx);
|
||||
expect(perms.project.create).toBe(true);
|
||||
});
|
||||
|
||||
it("member gets docker.read=false without legacy override", async () => {
|
||||
memberToReturn = mockMemberData("member");
|
||||
const perms = await resolvePermissions(ctx);
|
||||
expect(perms.docker.read).toBe(false);
|
||||
});
|
||||
|
||||
it("member gets docker.read=true with canAccessToDocker", async () => {
|
||||
memberToReturn = mockMemberData("member", { canAccessToDocker: true });
|
||||
const perms = await resolvePermissions(ctx);
|
||||
expect(perms.docker.read).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("free-tier resources for owner", () => {
|
||||
it("owner gets all free-tier permissions as true", async () => {
|
||||
memberToReturn = mockMemberData("owner");
|
||||
const perms = await resolvePermissions(ctx);
|
||||
expect(perms.project.create).toBe(true);
|
||||
expect(perms.project.delete).toBe(true);
|
||||
expect(perms.service.create).toBe(true);
|
||||
expect(perms.service.read).toBe(true);
|
||||
expect(perms.service.delete).toBe(true);
|
||||
expect(perms.docker.read).toBe(true);
|
||||
expect(perms.traefikFiles.read).toBe(true);
|
||||
expect(perms.traefikFiles.write).toBe(true);
|
||||
});
|
||||
});
|
||||
132
apps/dokploy/__test__/permissions/service-access.test.ts
Normal file
132
apps/dokploy/__test__/permissions/service-access.test.ts
Normal file
@@ -0,0 +1,132 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const mockMemberData = (
|
||||
role: string,
|
||||
accessedServices: string[] = [],
|
||||
accessedProjects: string[] = [],
|
||||
) => ({
|
||||
id: "member-1",
|
||||
role,
|
||||
userId: "user-1",
|
||||
organizationId: "org-1",
|
||||
accessedProjects,
|
||||
accessedServices,
|
||||
accessedEnvironments: [] as string[],
|
||||
canCreateProjects: false,
|
||||
canDeleteProjects: false,
|
||||
canCreateServices: false,
|
||||
canDeleteServices: false,
|
||||
canCreateEnvironments: false,
|
||||
canDeleteEnvironments: false,
|
||||
canAccessToTraefikFiles: false,
|
||||
canAccessToDocker: false,
|
||||
canAccessToAPI: false,
|
||||
canAccessToSSHKeys: false,
|
||||
canAccessToGitProviders: false,
|
||||
user: { id: "user-1", email: "test@test.com" },
|
||||
});
|
||||
|
||||
let memberToReturn: ReturnType<typeof mockMemberData> =
|
||||
mockMemberData("member");
|
||||
|
||||
vi.mock("@dokploy/server/db", () => ({
|
||||
db: {
|
||||
query: {
|
||||
member: {
|
||||
findFirst: vi.fn(() => Promise.resolve(memberToReturn)),
|
||||
findMany: vi.fn(() => Promise.resolve([])),
|
||||
},
|
||||
organizationRole: {
|
||||
findFirst: vi.fn(),
|
||||
findMany: vi.fn(() => Promise.resolve([])),
|
||||
},
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("@dokploy/server/services/proprietary/license-key", () => ({
|
||||
hasValidLicense: vi.fn(() => Promise.resolve(false)),
|
||||
}));
|
||||
|
||||
const { checkServicePermissionAndAccess, checkServiceAccess } = await import(
|
||||
"@dokploy/server/services/permission"
|
||||
);
|
||||
|
||||
const ctx = {
|
||||
user: { id: "user-1" },
|
||||
session: { activeOrganizationId: "org-1" },
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe("checkServicePermissionAndAccess", () => {
|
||||
it("owner bypasses accessedServices check", async () => {
|
||||
memberToReturn = mockMemberData("owner", []);
|
||||
await expect(
|
||||
checkServicePermissionAndAccess(ctx, "service-123", {
|
||||
deployment: ["read"],
|
||||
}),
|
||||
).resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
it("admin bypasses accessedServices check", async () => {
|
||||
memberToReturn = mockMemberData("admin", []);
|
||||
await expect(
|
||||
checkServicePermissionAndAccess(ctx, "service-123", {
|
||||
backup: ["create"],
|
||||
}),
|
||||
).resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
it("member with access to service passes", async () => {
|
||||
memberToReturn = mockMemberData("member", ["service-123"]);
|
||||
await expect(
|
||||
checkServicePermissionAndAccess(ctx, "service-123", {
|
||||
deployment: ["read"],
|
||||
}),
|
||||
).resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
it("member WITHOUT access to service fails", async () => {
|
||||
memberToReturn = mockMemberData("member", ["other-service"]);
|
||||
await expect(
|
||||
checkServicePermissionAndAccess(ctx, "service-123", {
|
||||
deployment: ["read"],
|
||||
}),
|
||||
).rejects.toThrow("You don't have access to this service");
|
||||
});
|
||||
|
||||
it("member with empty accessedServices fails", async () => {
|
||||
memberToReturn = mockMemberData("member", []);
|
||||
await expect(
|
||||
checkServicePermissionAndAccess(ctx, "service-123", {
|
||||
domain: ["delete"],
|
||||
}),
|
||||
).rejects.toThrow("You don't have access to this service");
|
||||
});
|
||||
});
|
||||
|
||||
describe("checkServiceAccess", () => {
|
||||
it("member with service access passes read check", async () => {
|
||||
memberToReturn = mockMemberData("member", ["app-1"]);
|
||||
await expect(
|
||||
checkServiceAccess(ctx, "app-1", "read"),
|
||||
).resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
it("member without service access fails read check", async () => {
|
||||
memberToReturn = mockMemberData("member", []);
|
||||
await expect(checkServiceAccess(ctx, "app-1", "read")).rejects.toThrow(
|
||||
"You don't have access to this service",
|
||||
);
|
||||
});
|
||||
|
||||
it("owner bypasses all access checks", async () => {
|
||||
memberToReturn = mockMemberData("owner", [], []);
|
||||
await expect(
|
||||
checkServiceAccess(ctx, "project-1", "create"),
|
||||
).resolves.toBeUndefined();
|
||||
});
|
||||
});
|
||||
@@ -15,13 +15,17 @@ interface Props {
|
||||
}
|
||||
|
||||
export const ShowTraefikConfig = ({ applicationId }: Props) => {
|
||||
const { data: permissions } = api.user.getPermissions.useQuery();
|
||||
const canRead = permissions?.traefikFiles.read ?? false;
|
||||
const { data, isPending } = api.application.readTraefikConfig.useQuery(
|
||||
{
|
||||
applicationId,
|
||||
},
|
||||
{ enabled: !!applicationId },
|
||||
{ enabled: !!applicationId && canRead },
|
||||
);
|
||||
|
||||
if (!canRead) return null;
|
||||
|
||||
return (
|
||||
<Card className="bg-background">
|
||||
<CardHeader className="flex flex-row justify-between">
|
||||
|
||||
@@ -60,6 +60,8 @@ export const validateAndFormatYAML = (yamlText: string) => {
|
||||
};
|
||||
|
||||
export const UpdateTraefikConfig = ({ applicationId }: Props) => {
|
||||
const { data: permissions } = api.user.getPermissions.useQuery();
|
||||
const canWrite = permissions?.traefikFiles.write ?? false;
|
||||
const [open, setOpen] = useState(false);
|
||||
const [skipYamlValidation, setSkipYamlValidation] = useState(false);
|
||||
const { data, refetch } = api.application.readTraefikConfig.useQuery(
|
||||
@@ -125,9 +127,11 @@ export const UpdateTraefikConfig = ({ applicationId }: Props) => {
|
||||
}
|
||||
}}
|
||||
>
|
||||
<DialogTrigger asChild>
|
||||
<Button isLoading={isPending}>Modify</Button>
|
||||
</DialogTrigger>
|
||||
{canWrite && (
|
||||
<DialogTrigger asChild>
|
||||
<Button isLoading={isPending}>Modify</Button>
|
||||
</DialogTrigger>
|
||||
)}
|
||||
<DialogContent className="sm:max-w-4xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Update traefik config</DialogTitle>
|
||||
|
||||
@@ -21,6 +21,13 @@ interface Props {
|
||||
}
|
||||
|
||||
export const ShowVolumes = ({ id, type }: Props) => {
|
||||
const { data: permissions } = api.user.getPermissions.useQuery();
|
||||
const canRead = permissions?.volume.read ?? false;
|
||||
const canCreate = permissions?.volume.create ?? false;
|
||||
const canDelete = permissions?.volume.delete ?? false;
|
||||
|
||||
if (!canRead) return null;
|
||||
|
||||
const queryMap = {
|
||||
postgres: () =>
|
||||
api.postgres.one.useQuery({ postgresId: id }, { enabled: !!id }),
|
||||
@@ -50,7 +57,7 @@ export const ShowVolumes = ({ id, type }: Props) => {
|
||||
</CardDescription>
|
||||
</div>
|
||||
|
||||
{data && data?.mounts.length > 0 && (
|
||||
{canCreate && data && data?.mounts.length > 0 && (
|
||||
<AddVolumes serviceId={id} refetch={refetch} serviceType={type}>
|
||||
Add Volume
|
||||
</AddVolumes>
|
||||
@@ -63,9 +70,11 @@ export const ShowVolumes = ({ id, type }: Props) => {
|
||||
<span className="text-base text-muted-foreground">
|
||||
No volumes/mounts configured
|
||||
</span>
|
||||
<AddVolumes serviceId={id} refetch={refetch} serviceType={type}>
|
||||
Add Volume
|
||||
</AddVolumes>
|
||||
{canCreate && (
|
||||
<AddVolumes serviceId={id} refetch={refetch} serviceType={type}>
|
||||
Add Volume
|
||||
</AddVolumes>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col pt-2 gap-4">
|
||||
@@ -130,38 +139,42 @@ export const ShowVolumes = ({ id, type }: Props) => {
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-row gap-1">
|
||||
<UpdateVolume
|
||||
mountId={mount.mountId}
|
||||
type={mount.type}
|
||||
refetch={refetch}
|
||||
serviceType={type}
|
||||
/>
|
||||
<DialogAction
|
||||
title="Delete Volume"
|
||||
description="Are you sure you want to delete this volume?"
|
||||
type="destructive"
|
||||
onClick={async () => {
|
||||
await deleteVolume({
|
||||
mountId: mount.mountId,
|
||||
})
|
||||
.then(() => {
|
||||
refetch();
|
||||
toast.success("Volume deleted successfully");
|
||||
{canCreate && (
|
||||
<UpdateVolume
|
||||
mountId={mount.mountId}
|
||||
type={mount.type}
|
||||
refetch={refetch}
|
||||
serviceType={type}
|
||||
/>
|
||||
)}
|
||||
{canDelete && (
|
||||
<DialogAction
|
||||
title="Delete Volume"
|
||||
description="Are you sure you want to delete this volume?"
|
||||
type="destructive"
|
||||
onClick={async () => {
|
||||
await deleteVolume({
|
||||
mountId: mount.mountId,
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error deleting volume");
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="group hover:bg-red-500/10"
|
||||
isLoading={isRemoving}
|
||||
.then(() => {
|
||||
refetch();
|
||||
toast.success("Volume deleted successfully");
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error deleting volume");
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Trash2 className="size-4 text-primary group-hover:text-red-500" />
|
||||
</Button>
|
||||
</DialogAction>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="group hover:bg-red-500/10"
|
||||
isLoading={isRemoving}
|
||||
>
|
||||
<Trash2 className="size-4 text-primary group-hover:text-red-500" />
|
||||
</Button>
|
||||
</DialogAction>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -50,6 +50,9 @@ interface Props {
|
||||
}
|
||||
|
||||
export const ShowDomains = ({ id, type }: Props) => {
|
||||
const { data: permissions } = api.user.getPermissions.useQuery();
|
||||
const canCreateDomain = permissions?.domain.create ?? false;
|
||||
const canDeleteDomain = permissions?.domain.delete ?? false;
|
||||
const { data: application } =
|
||||
type === "application"
|
||||
? api.application.one.useQuery(
|
||||
@@ -149,7 +152,7 @@ export const ShowDomains = ({ id, type }: Props) => {
|
||||
</div>
|
||||
|
||||
<div className="flex flex-row gap-4 flex-wrap">
|
||||
{data && data?.length > 0 && (
|
||||
{canCreateDomain && data && data?.length > 0 && (
|
||||
<AddDomain id={id} type={type}>
|
||||
<Button>
|
||||
<GlobeIcon className="size-4" /> Add Domain
|
||||
@@ -173,13 +176,15 @@ export const ShowDomains = ({ id, type }: Props) => {
|
||||
To access the application it is required to set at least 1
|
||||
domain
|
||||
</span>
|
||||
<div className="flex flex-row gap-4 flex-wrap">
|
||||
<AddDomain id={id} type={type}>
|
||||
<Button>
|
||||
<GlobeIcon className="size-4" /> Add Domain
|
||||
</Button>
|
||||
</AddDomain>
|
||||
</div>
|
||||
{canCreateDomain && (
|
||||
<div className="flex flex-row gap-4 flex-wrap">
|
||||
<AddDomain id={id} type={type}>
|
||||
<Button>
|
||||
<GlobeIcon className="size-4" /> Add Domain
|
||||
</Button>
|
||||
</AddDomain>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 gap-4 xl:grid-cols-2 w-full min-h-[40vh] ">
|
||||
@@ -214,47 +219,51 @@ export const ShowDomains = ({ id, type }: Props) => {
|
||||
}
|
||||
/>
|
||||
)}
|
||||
<AddDomain
|
||||
id={id}
|
||||
type={type}
|
||||
domainId={item.domainId}
|
||||
>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="group hover:bg-blue-500/10"
|
||||
{canCreateDomain && (
|
||||
<AddDomain
|
||||
id={id}
|
||||
type={type}
|
||||
domainId={item.domainId}
|
||||
>
|
||||
<PenBoxIcon className="size-3.5 text-primary group-hover:text-blue-500" />
|
||||
</Button>
|
||||
</AddDomain>
|
||||
<DialogAction
|
||||
title="Delete Domain"
|
||||
description="Are you sure you want to delete this domain?"
|
||||
type="destructive"
|
||||
onClick={async () => {
|
||||
await deleteDomain({
|
||||
domainId: item.domainId,
|
||||
})
|
||||
.then((_data) => {
|
||||
refetch();
|
||||
toast.success(
|
||||
"Domain deleted successfully",
|
||||
);
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="group hover:bg-blue-500/10"
|
||||
>
|
||||
<PenBoxIcon className="size-3.5 text-primary group-hover:text-blue-500" />
|
||||
</Button>
|
||||
</AddDomain>
|
||||
)}
|
||||
{canDeleteDomain && (
|
||||
<DialogAction
|
||||
title="Delete Domain"
|
||||
description="Are you sure you want to delete this domain?"
|
||||
type="destructive"
|
||||
onClick={async () => {
|
||||
await deleteDomain({
|
||||
domainId: item.domainId,
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error deleting domain");
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="group hover:bg-red-500/10"
|
||||
isLoading={isRemoving}
|
||||
.then((_data) => {
|
||||
refetch();
|
||||
toast.success(
|
||||
"Domain deleted successfully",
|
||||
);
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error deleting domain");
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Trash2 className="size-4 text-primary group-hover:text-red-500" />
|
||||
</Button>
|
||||
</DialogAction>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="group hover:bg-red-500/10"
|
||||
isLoading={isRemoving}
|
||||
>
|
||||
<Trash2 className="size-4 text-primary group-hover:text-red-500" />
|
||||
</Button>
|
||||
</DialogAction>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-full break-all">
|
||||
|
||||
@@ -36,6 +36,8 @@ interface Props {
|
||||
}
|
||||
|
||||
export const ShowEnvironment = ({ id, type }: Props) => {
|
||||
const { data: permissions } = api.user.getPermissions.useQuery();
|
||||
const canWrite = permissions?.envVars.write ?? false;
|
||||
const queryMap = {
|
||||
postgres: () =>
|
||||
api.postgres.one.useQuery({ postgresId: id }, { enabled: !!id }),
|
||||
@@ -185,25 +187,27 @@ PORT=3000
|
||||
)}
|
||||
/>
|
||||
|
||||
<div className="flex flex-row justify-end gap-2">
|
||||
{hasChanges && (
|
||||
{canWrite && (
|
||||
<div className="flex flex-row justify-end gap-2">
|
||||
{hasChanges && (
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={handleCancel}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={handleCancel}
|
||||
isLoading={isPending}
|
||||
className="w-fit"
|
||||
type="submit"
|
||||
disabled={!hasChanges}
|
||||
>
|
||||
Cancel
|
||||
Save
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
isLoading={isPending}
|
||||
className="w-fit"
|
||||
type="submit"
|
||||
disabled={!hasChanges}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</form>
|
||||
</Form>
|
||||
</CardContent>
|
||||
|
||||
@@ -31,6 +31,8 @@ interface Props {
|
||||
}
|
||||
|
||||
export const ShowEnvironment = ({ applicationId }: Props) => {
|
||||
const { data: permissions } = api.user.getPermissions.useQuery();
|
||||
const canWrite = permissions?.envVars.write ?? false;
|
||||
const { mutateAsync, isPending } =
|
||||
api.application.saveEnvironment.useMutation();
|
||||
|
||||
@@ -201,27 +203,30 @@ export const ShowEnvironment = ({ applicationId }: Props) => {
|
||||
<Switch
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
disabled={!canWrite}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
<div className="flex flex-row justify-end gap-2">
|
||||
{hasChanges && (
|
||||
<Button type="button" variant="outline" onClick={handleCancel}>
|
||||
Cancel
|
||||
{canWrite && (
|
||||
<div className="flex flex-row justify-end gap-2">
|
||||
{hasChanges && (
|
||||
<Button type="button" variant="outline" onClick={handleCancel}>
|
||||
Cancel
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
isLoading={isPending}
|
||||
className="w-fit"
|
||||
type="submit"
|
||||
disabled={!hasChanges}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
isLoading={isPending}
|
||||
className="w-fit"
|
||||
type="submit"
|
||||
disabled={!hasChanges}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</form>
|
||||
</Form>
|
||||
</Card>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
|
||||
import { CheckIcon, ChevronsUpDown, X } from "lucide-react";
|
||||
import { CheckIcon, ChevronsUpDown, HelpCircle, X } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useEffect } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
@@ -416,10 +416,8 @@ export const SaveBitbucketProvider = ({ applicationId }: Props) => {
|
||||
<FormLabel>Watch Paths</FormLabel>
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<div className="size-4 rounded-full bg-muted flex items-center justify-center text-[10px] font-bold">
|
||||
?
|
||||
</div>
|
||||
<TooltipTrigger asChild>
|
||||
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
|
||||
import { KeyRoundIcon, LockIcon, X } from "lucide-react";
|
||||
import { HelpCircle, KeyRoundIcon, LockIcon, X } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import { useEffect } from "react";
|
||||
@@ -228,10 +228,8 @@ export const SaveGitProvider = ({ applicationId }: Props) => {
|
||||
<FormLabel>Watch Paths</FormLabel>
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<div className="size-4 rounded-full bg-muted flex items-center justify-center text-[10px] font-bold">
|
||||
?
|
||||
</div>
|
||||
<TooltipTrigger asChild>
|
||||
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className="max-w-[300px]">
|
||||
<p>
|
||||
|
||||
@@ -30,6 +30,9 @@ interface Props {
|
||||
|
||||
export const ShowGeneralApplication = ({ applicationId }: Props) => {
|
||||
const router = useRouter();
|
||||
const { data: permissions } = api.user.getPermissions.useQuery();
|
||||
const canDeploy = permissions?.deployment.create ?? false;
|
||||
const canUpdateService = permissions?.service.create ?? false;
|
||||
const { data, refetch } = api.application.one.useQuery(
|
||||
{
|
||||
applicationId,
|
||||
@@ -57,128 +60,135 @@ export const ShowGeneralApplication = ({ applicationId }: Props) => {
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-row gap-4 flex-wrap">
|
||||
<TooltipProvider delayDuration={0} disableHoverableContent={false}>
|
||||
<DialogAction
|
||||
title="Deploy Application"
|
||||
description="Are you sure you want to deploy this application?"
|
||||
type="default"
|
||||
onClick={async () => {
|
||||
await deploy({
|
||||
applicationId: applicationId,
|
||||
})
|
||||
.then(() => {
|
||||
toast.success("Application deployed successfully");
|
||||
refetch();
|
||||
router.push(
|
||||
`/dashboard/project/${data?.environment.projectId}/environment/${data?.environmentId}/services/application/${applicationId}?tab=deployments`,
|
||||
);
|
||||
{canDeploy && (
|
||||
<DialogAction
|
||||
title="Deploy Application"
|
||||
description="Are you sure you want to deploy this application?"
|
||||
type="default"
|
||||
onClick={async () => {
|
||||
await deploy({
|
||||
applicationId: applicationId,
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error deploying application");
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
variant="default"
|
||||
isLoading={data?.applicationStatus === "running"}
|
||||
className="flex items-center gap-1.5 group focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||
.then(() => {
|
||||
toast.success("Application deployed successfully");
|
||||
refetch();
|
||||
router.push(
|
||||
`/dashboard/project/${data?.environment.projectId}/environment/${data?.environmentId}/services/application/${applicationId}?tab=deployments`,
|
||||
);
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error deploying application");
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div className="flex items-center">
|
||||
<Rocket className="size-4 mr-1" />
|
||||
Deploy
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipPrimitive.Portal>
|
||||
<TooltipContent sideOffset={5} className="z-[60]">
|
||||
<p>
|
||||
Downloads the source code and performs a complete build
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</TooltipPrimitive.Portal>
|
||||
</Tooltip>
|
||||
</Button>
|
||||
</DialogAction>
|
||||
<DialogAction
|
||||
title="Reload Application"
|
||||
description="Are you sure you want to reload this application?"
|
||||
type="default"
|
||||
onClick={async () => {
|
||||
await reload({
|
||||
applicationId: applicationId,
|
||||
appName: data?.appName || "",
|
||||
})
|
||||
.then(() => {
|
||||
toast.success("Application reloaded successfully");
|
||||
refetch();
|
||||
<Button
|
||||
variant="default"
|
||||
isLoading={data?.applicationStatus === "running"}
|
||||
className="flex items-center gap-1.5 group focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||
>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div className="flex items-center">
|
||||
<Rocket className="size-4 mr-1" />
|
||||
Deploy
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipPrimitive.Portal>
|
||||
<TooltipContent sideOffset={5} className="z-[60]">
|
||||
<p>
|
||||
Downloads the source code and performs a complete
|
||||
build
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</TooltipPrimitive.Portal>
|
||||
</Tooltip>
|
||||
</Button>
|
||||
</DialogAction>
|
||||
)}
|
||||
{canDeploy && (
|
||||
<DialogAction
|
||||
title="Reload Application"
|
||||
description="Are you sure you want to reload this application?"
|
||||
type="default"
|
||||
onClick={async () => {
|
||||
await reload({
|
||||
applicationId: applicationId,
|
||||
appName: data?.appName || "",
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error reloading application");
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
variant="secondary"
|
||||
isLoading={isReloading}
|
||||
className="flex items-center gap-1.5 group focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||
.then(() => {
|
||||
toast.success("Application reloaded successfully");
|
||||
refetch();
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error reloading application");
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div className="flex items-center">
|
||||
<RefreshCcw className="size-4 mr-1" />
|
||||
Reload
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipPrimitive.Portal>
|
||||
<TooltipContent sideOffset={5} className="z-[60]">
|
||||
<p>Reload the application without rebuilding it</p>
|
||||
</TooltipContent>
|
||||
</TooltipPrimitive.Portal>
|
||||
</Tooltip>
|
||||
</Button>
|
||||
</DialogAction>
|
||||
<DialogAction
|
||||
title="Rebuild Application"
|
||||
description="Are you sure you want to rebuild this application?"
|
||||
type="default"
|
||||
onClick={async () => {
|
||||
await redeploy({
|
||||
applicationId: applicationId,
|
||||
})
|
||||
.then(() => {
|
||||
toast.success("Application rebuilt successfully");
|
||||
refetch();
|
||||
<Button
|
||||
variant="secondary"
|
||||
isLoading={isReloading}
|
||||
className="flex items-center gap-1.5 group focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||
>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div className="flex items-center">
|
||||
<RefreshCcw className="size-4 mr-1" />
|
||||
Reload
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipPrimitive.Portal>
|
||||
<TooltipContent sideOffset={5} className="z-[60]">
|
||||
<p>Reload the application without rebuilding it</p>
|
||||
</TooltipContent>
|
||||
</TooltipPrimitive.Portal>
|
||||
</Tooltip>
|
||||
</Button>
|
||||
</DialogAction>
|
||||
)}
|
||||
{canDeploy && (
|
||||
<DialogAction
|
||||
title="Rebuild Application"
|
||||
description="Are you sure you want to rebuild this application?"
|
||||
type="default"
|
||||
onClick={async () => {
|
||||
await redeploy({
|
||||
applicationId: applicationId,
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error rebuilding application");
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
variant="secondary"
|
||||
isLoading={data?.applicationStatus === "running"}
|
||||
className="flex items-center gap-1.5 group focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||
.then(() => {
|
||||
toast.success("Application rebuilt successfully");
|
||||
refetch();
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error rebuilding application");
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div className="flex items-center">
|
||||
<Hammer className="size-4 mr-1" />
|
||||
Rebuild
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipPrimitive.Portal>
|
||||
<TooltipContent sideOffset={5} className="z-[60]">
|
||||
<p>
|
||||
Only rebuilds the application without downloading new
|
||||
code
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</TooltipPrimitive.Portal>
|
||||
</Tooltip>
|
||||
</Button>
|
||||
</DialogAction>
|
||||
<Button
|
||||
variant="secondary"
|
||||
isLoading={data?.applicationStatus === "running"}
|
||||
className="flex items-center gap-1.5 group focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||
>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div className="flex items-center">
|
||||
<Hammer className="size-4 mr-1" />
|
||||
Rebuild
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipPrimitive.Portal>
|
||||
<TooltipContent sideOffset={5} className="z-[60]">
|
||||
<p>
|
||||
Only rebuilds the application without downloading new
|
||||
code
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</TooltipPrimitive.Portal>
|
||||
</Tooltip>
|
||||
</Button>
|
||||
</DialogAction>
|
||||
)}
|
||||
|
||||
{data?.applicationStatus === "idle" ? (
|
||||
{canDeploy && data?.applicationStatus === "idle" ? (
|
||||
<DialogAction
|
||||
title="Start Application"
|
||||
description="Are you sure you want to start this application?"
|
||||
@@ -219,7 +229,7 @@ export const ShowGeneralApplication = ({ applicationId }: Props) => {
|
||||
</Tooltip>
|
||||
</Button>
|
||||
</DialogAction>
|
||||
) : (
|
||||
) : canDeploy ? (
|
||||
<DialogAction
|
||||
title="Stop Application"
|
||||
description="Are you sure you want to stop this application?"
|
||||
@@ -256,7 +266,7 @@ export const ShowGeneralApplication = ({ applicationId }: Props) => {
|
||||
</Tooltip>
|
||||
</Button>
|
||||
</DialogAction>
|
||||
)}
|
||||
) : null}
|
||||
</TooltipProvider>
|
||||
<DockerTerminalModal
|
||||
appName={data?.appName || ""}
|
||||
@@ -270,49 +280,53 @@ export const ShowGeneralApplication = ({ applicationId }: Props) => {
|
||||
Open Terminal
|
||||
</Button>
|
||||
</DockerTerminalModal>
|
||||
<div className="flex flex-row items-center gap-2 rounded-md px-4 py-2 border">
|
||||
<span className="text-sm font-medium">Autodeploy</span>
|
||||
<Switch
|
||||
aria-label="Toggle autodeploy"
|
||||
checked={data?.autoDeploy || false}
|
||||
onCheckedChange={async (enabled) => {
|
||||
await update({
|
||||
applicationId,
|
||||
autoDeploy: enabled,
|
||||
})
|
||||
.then(async () => {
|
||||
toast.success("Auto Deploy Updated");
|
||||
await refetch();
|
||||
{canUpdateService && (
|
||||
<div className="flex flex-row items-center gap-2 rounded-md px-4 py-2 border">
|
||||
<span className="text-sm font-medium">Autodeploy</span>
|
||||
<Switch
|
||||
aria-label="Toggle autodeploy"
|
||||
checked={data?.autoDeploy || false}
|
||||
onCheckedChange={async (enabled) => {
|
||||
await update({
|
||||
applicationId,
|
||||
autoDeploy: enabled,
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error updating Auto Deploy");
|
||||
});
|
||||
}}
|
||||
className="flex flex-row gap-2 items-center data-[state=checked]:bg-primary"
|
||||
/>
|
||||
</div>
|
||||
.then(async () => {
|
||||
toast.success("Auto Deploy Updated");
|
||||
await refetch();
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error updating Auto Deploy");
|
||||
});
|
||||
}}
|
||||
className="flex flex-row gap-2 items-center data-[state=checked]:bg-primary"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex flex-row items-center gap-2 rounded-md px-4 py-2 border">
|
||||
<span className="text-sm font-medium">Clean Cache</span>
|
||||
<Switch
|
||||
aria-label="Toggle clean cache"
|
||||
checked={data?.cleanCache || false}
|
||||
onCheckedChange={async (enabled) => {
|
||||
await update({
|
||||
applicationId,
|
||||
cleanCache: enabled,
|
||||
})
|
||||
.then(async () => {
|
||||
toast.success("Clean Cache Updated");
|
||||
await refetch();
|
||||
{canUpdateService && (
|
||||
<div className="flex flex-row items-center gap-2 rounded-md px-4 py-2 border">
|
||||
<span className="text-sm font-medium">Clean Cache</span>
|
||||
<Switch
|
||||
aria-label="Toggle clean cache"
|
||||
checked={data?.cleanCache || false}
|
||||
onCheckedChange={async (enabled) => {
|
||||
await update({
|
||||
applicationId,
|
||||
cleanCache: enabled,
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error updating Clean Cache");
|
||||
});
|
||||
}}
|
||||
className="flex flex-row gap-2 items-center data-[state=checked]:bg-primary"
|
||||
/>
|
||||
</div>
|
||||
.then(async () => {
|
||||
toast.success("Clean Cache Updated");
|
||||
await refetch();
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error updating Clean Cache");
|
||||
});
|
||||
}}
|
||||
className="flex flex-row gap-2 items-center data-[state=checked]:bg-primary"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
<ShowProviderForm applicationId={applicationId} />
|
||||
|
||||
@@ -46,6 +46,8 @@ interface Props {
|
||||
}
|
||||
|
||||
export const DeleteService = ({ id, type }: Props) => {
|
||||
const { data: permissions } = api.user.getPermissions.useQuery();
|
||||
const canDelete = permissions?.service.delete ?? false;
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
const queryMap = {
|
||||
@@ -123,6 +125,8 @@ export const DeleteService = ({ id, type }: Props) => {
|
||||
data?.applicationStatus === "running") ||
|
||||
(data && "composeStatus" in data && data?.composeStatus === "running");
|
||||
|
||||
if (!canDelete) return null;
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||
<DialogTrigger asChild>
|
||||
|
||||
@@ -19,6 +19,9 @@ interface Props {
|
||||
}
|
||||
export const ComposeActions = ({ composeId }: Props) => {
|
||||
const router = useRouter();
|
||||
const { data: permissions } = api.user.getPermissions.useQuery();
|
||||
const canDeploy = permissions?.deployment.create ?? false;
|
||||
const canUpdateService = permissions?.service.create ?? false;
|
||||
const { data, refetch } = api.compose.one.useQuery(
|
||||
{
|
||||
composeId,
|
||||
@@ -35,162 +38,169 @@ export const ComposeActions = ({ composeId }: Props) => {
|
||||
return (
|
||||
<div className="flex flex-row gap-4 w-full flex-wrap ">
|
||||
<TooltipProvider delayDuration={0} disableHoverableContent={false}>
|
||||
<DialogAction
|
||||
title="Deploy Compose"
|
||||
description="Are you sure you want to deploy this compose?"
|
||||
type="default"
|
||||
onClick={async () => {
|
||||
await deploy({
|
||||
composeId: composeId,
|
||||
})
|
||||
.then(() => {
|
||||
toast.success("Compose deployed successfully");
|
||||
refetch();
|
||||
router.push(
|
||||
`/dashboard/project/${data?.environment.projectId}/environment/${data?.environmentId}/services/compose/${composeId}?tab=deployments`,
|
||||
);
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error deploying compose");
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
variant="default"
|
||||
isLoading={data?.composeStatus === "running"}
|
||||
className="flex items-center gap-1.5 group focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||
>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div className="flex items-center">
|
||||
<Rocket className="size-4 mr-1" />
|
||||
Deploy
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipPrimitive.Portal>
|
||||
<TooltipContent sideOffset={5} className="z-[60]">
|
||||
<p>Downloads the source code and performs a complete build</p>
|
||||
</TooltipContent>
|
||||
</TooltipPrimitive.Portal>
|
||||
</Tooltip>
|
||||
</Button>
|
||||
</DialogAction>
|
||||
<DialogAction
|
||||
title="Reload Compose"
|
||||
description="Are you sure you want to reload this compose?"
|
||||
type="default"
|
||||
onClick={async () => {
|
||||
await redeploy({
|
||||
composeId: composeId,
|
||||
})
|
||||
.then(() => {
|
||||
toast.success("Compose reloaded successfully");
|
||||
refetch();
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error reloading compose");
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
variant="secondary"
|
||||
isLoading={data?.composeStatus === "running"}
|
||||
className="flex items-center gap-1.5 group focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||
>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div className="flex items-center">
|
||||
<RefreshCcw className="size-4 mr-1" />
|
||||
Reload
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipPrimitive.Portal>
|
||||
<TooltipContent sideOffset={5} className="z-[60]">
|
||||
<p>Reload the compose without rebuilding it</p>
|
||||
</TooltipContent>
|
||||
</TooltipPrimitive.Portal>
|
||||
</Tooltip>
|
||||
</Button>
|
||||
</DialogAction>
|
||||
{data?.composeType === "docker-compose" &&
|
||||
data?.composeStatus === "idle" ? (
|
||||
{canDeploy && (
|
||||
<DialogAction
|
||||
title="Start Compose"
|
||||
description="Are you sure you want to start this compose?"
|
||||
title="Deploy Compose"
|
||||
description="Are you sure you want to deploy this compose?"
|
||||
type="default"
|
||||
onClick={async () => {
|
||||
await start({
|
||||
await deploy({
|
||||
composeId: composeId,
|
||||
})
|
||||
.then(() => {
|
||||
toast.success("Compose started successfully");
|
||||
toast.success("Compose deployed successfully");
|
||||
refetch();
|
||||
router.push(
|
||||
`/dashboard/project/${data?.environment.projectId}/environment/${data?.environmentId}/services/compose/${composeId}?tab=deployments`,
|
||||
);
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error starting compose");
|
||||
toast.error("Error deploying compose");
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
variant="secondary"
|
||||
isLoading={isStarting}
|
||||
variant="default"
|
||||
isLoading={data?.composeStatus === "running"}
|
||||
className="flex items-center gap-1.5 group focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||
>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div className="flex items-center">
|
||||
<CheckCircle2 className="size-4 mr-1" />
|
||||
Start
|
||||
<Rocket className="size-4 mr-1" />
|
||||
Deploy
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipPrimitive.Portal>
|
||||
<TooltipContent sideOffset={5} className="z-[60]">
|
||||
<p>
|
||||
Start the compose (requires a previous successful build)
|
||||
Downloads the source code and performs a complete build
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</TooltipPrimitive.Portal>
|
||||
</Tooltip>
|
||||
</Button>
|
||||
</DialogAction>
|
||||
) : (
|
||||
)}
|
||||
{canDeploy && (
|
||||
<DialogAction
|
||||
title="Stop Compose"
|
||||
description="Are you sure you want to stop this compose?"
|
||||
title="Reload Compose"
|
||||
description="Are you sure you want to reload this compose?"
|
||||
type="default"
|
||||
onClick={async () => {
|
||||
await stop({
|
||||
await redeploy({
|
||||
composeId: composeId,
|
||||
})
|
||||
.then(() => {
|
||||
toast.success("Compose stopped successfully");
|
||||
toast.success("Compose reloaded successfully");
|
||||
refetch();
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error stopping compose");
|
||||
toast.error("Error reloading compose");
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
variant="destructive"
|
||||
isLoading={isStopping}
|
||||
variant="secondary"
|
||||
isLoading={data?.composeStatus === "running"}
|
||||
className="flex items-center gap-1.5 group focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||
>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div className="flex items-center">
|
||||
<Ban className="size-4 mr-1" />
|
||||
Stop
|
||||
<RefreshCcw className="size-4 mr-1" />
|
||||
Reload
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipPrimitive.Portal>
|
||||
<TooltipContent sideOffset={5} className="z-[60]">
|
||||
<p>Stop the currently running compose</p>
|
||||
<p>Reload the compose without rebuilding it</p>
|
||||
</TooltipContent>
|
||||
</TooltipPrimitive.Portal>
|
||||
</Tooltip>
|
||||
</Button>
|
||||
</DialogAction>
|
||||
)}
|
||||
{canDeploy &&
|
||||
(data?.composeType === "docker-compose" &&
|
||||
data?.composeStatus === "idle" ? (
|
||||
<DialogAction
|
||||
title="Start Compose"
|
||||
description="Are you sure you want to start this compose?"
|
||||
type="default"
|
||||
onClick={async () => {
|
||||
await start({
|
||||
composeId: composeId,
|
||||
})
|
||||
.then(() => {
|
||||
toast.success("Compose started successfully");
|
||||
refetch();
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error starting compose");
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
variant="secondary"
|
||||
isLoading={isStarting}
|
||||
className="flex items-center gap-1.5 group focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||
>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div className="flex items-center">
|
||||
<CheckCircle2 className="size-4 mr-1" />
|
||||
Start
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipPrimitive.Portal>
|
||||
<TooltipContent sideOffset={5} className="z-[60]">
|
||||
<p>
|
||||
Start the compose (requires a previous successful build)
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</TooltipPrimitive.Portal>
|
||||
</Tooltip>
|
||||
</Button>
|
||||
</DialogAction>
|
||||
) : (
|
||||
<DialogAction
|
||||
title="Stop Compose"
|
||||
description="Are you sure you want to stop this compose?"
|
||||
onClick={async () => {
|
||||
await stop({
|
||||
composeId: composeId,
|
||||
})
|
||||
.then(() => {
|
||||
toast.success("Compose stopped successfully");
|
||||
refetch();
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error stopping compose");
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
variant="destructive"
|
||||
isLoading={isStopping}
|
||||
className="flex items-center gap-1.5 group focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||
>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div className="flex items-center">
|
||||
<Ban className="size-4 mr-1" />
|
||||
Stop
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipPrimitive.Portal>
|
||||
<TooltipContent sideOffset={5} className="z-[60]">
|
||||
<p>Stop the currently running compose</p>
|
||||
</TooltipContent>
|
||||
</TooltipPrimitive.Portal>
|
||||
</Tooltip>
|
||||
</Button>
|
||||
</DialogAction>
|
||||
))}
|
||||
</TooltipProvider>
|
||||
<DockerTerminalModal
|
||||
appName={data?.appName || ""}
|
||||
@@ -205,27 +215,29 @@ export const ComposeActions = ({ composeId }: Props) => {
|
||||
Open Terminal
|
||||
</Button>
|
||||
</DockerTerminalModal>
|
||||
<div className="flex flex-row items-center gap-2 rounded-md px-4 py-2 border">
|
||||
<span className="text-sm font-medium">Autodeploy</span>
|
||||
<Switch
|
||||
aria-label="Toggle autodeploy"
|
||||
checked={data?.autoDeploy || false}
|
||||
onCheckedChange={async (enabled) => {
|
||||
await update({
|
||||
composeId,
|
||||
autoDeploy: enabled,
|
||||
})
|
||||
.then(async () => {
|
||||
toast.success("Auto Deploy Updated");
|
||||
await refetch();
|
||||
{canUpdateService && (
|
||||
<div className="flex flex-row items-center gap-2 rounded-md px-4 py-2 border">
|
||||
<span className="text-sm font-medium">Autodeploy</span>
|
||||
<Switch
|
||||
aria-label="Toggle autodeploy"
|
||||
checked={data?.autoDeploy || false}
|
||||
onCheckedChange={async (enabled) => {
|
||||
await update({
|
||||
composeId,
|
||||
autoDeploy: enabled,
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error updating Auto Deploy");
|
||||
});
|
||||
}}
|
||||
className="flex flex-row gap-2 items-center data-[state=checked]:bg-primary"
|
||||
/>
|
||||
</div>
|
||||
.then(async () => {
|
||||
toast.success("Auto Deploy Updated");
|
||||
await refetch();
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error updating Auto Deploy");
|
||||
});
|
||||
}}
|
||||
className="flex flex-row gap-2 items-center data-[state=checked]:bg-primary"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -26,6 +26,8 @@ const AddComposeFile = z.object({
|
||||
type AddComposeFile = z.infer<typeof AddComposeFile>;
|
||||
|
||||
export const ComposeFileEditor = ({ composeId }: Props) => {
|
||||
const { data: permissions } = api.user.getPermissions.useQuery();
|
||||
const canUpdate = permissions?.service.create ?? false;
|
||||
const utils = api.useUtils();
|
||||
const { data, refetch } = api.compose.one.useQuery(
|
||||
{
|
||||
@@ -164,14 +166,16 @@ services:
|
||||
</Form>
|
||||
<div className="flex justify-between flex-col lg:flex-row gap-2">
|
||||
<div className="w-full flex flex-col lg:flex-row gap-4 items-end" />
|
||||
<Button
|
||||
type="submit"
|
||||
form="hook-form-save-compose-file"
|
||||
isLoading={isPending}
|
||||
className="lg:w-fit w-full"
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
{canUpdate && (
|
||||
<Button
|
||||
type="submit"
|
||||
form="hook-form-save-compose-file"
|
||||
isLoading={isPending}
|
||||
className="lg:w-fit w-full"
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
|
||||
import { KeyRoundIcon, LockIcon, X } from "lucide-react";
|
||||
import { HelpCircle, KeyRoundIcon, LockIcon, X } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import { useEffect } from "react";
|
||||
@@ -230,10 +230,8 @@ export const SaveGitProviderCompose = ({ composeId }: Props) => {
|
||||
<FormLabel>Watch Paths</FormLabel>
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<div className="size-4 rounded-full bg-muted flex items-center justify-center text-[10px] font-bold">
|
||||
?
|
||||
</div>
|
||||
<TooltipTrigger asChild>
|
||||
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className="max-w-[300px]">
|
||||
<p>
|
||||
|
||||
@@ -21,6 +21,8 @@ interface Props {
|
||||
}
|
||||
|
||||
export const ShowGeneralMariadb = ({ mariadbId }: Props) => {
|
||||
const { data: permissions } = api.user.getPermissions.useQuery();
|
||||
const canDeploy = permissions?.deployment.create ?? false;
|
||||
const { data, refetch } = api.mariadb.one.useQuery(
|
||||
{
|
||||
mariadbId,
|
||||
@@ -72,154 +74,33 @@ export const ShowGeneralMariadb = ({ mariadbId }: Props) => {
|
||||
<CardTitle className="text-xl">Deploy Settings</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-row gap-4 flex-wrap">
|
||||
<TooltipProvider delayDuration={0}>
|
||||
<DialogAction
|
||||
title="Deploy Mariadb"
|
||||
description="Are you sure you want to deploy this mariadb?"
|
||||
type="default"
|
||||
onClick={async () => {
|
||||
setIsDeploying(true);
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
refetch();
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
variant="default"
|
||||
isLoading={data?.applicationStatus === "running"}
|
||||
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||
>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div className="flex items-center">
|
||||
<Rocket className="size-4 mr-1" />
|
||||
Deploy
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipPrimitive.Portal>
|
||||
<TooltipContent sideOffset={5} className="z-[60]">
|
||||
<p>Downloads and sets up the MariaDB database</p>
|
||||
</TooltipContent>
|
||||
</TooltipPrimitive.Portal>
|
||||
</Tooltip>
|
||||
</Button>
|
||||
</DialogAction>
|
||||
</TooltipProvider>
|
||||
<TooltipProvider delayDuration={0}>
|
||||
<DialogAction
|
||||
title="Reload Mariadb"
|
||||
description="Are you sure you want to reload this mariadb?"
|
||||
type="default"
|
||||
onClick={async () => {
|
||||
await reload({
|
||||
mariadbId: mariadbId,
|
||||
appName: data?.appName || "",
|
||||
})
|
||||
.then(() => {
|
||||
toast.success("Mariadb reloaded successfully");
|
||||
refetch();
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error reloading Mariadb");
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
variant="secondary"
|
||||
isLoading={isReloading}
|
||||
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||
>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div className="flex items-center">
|
||||
<RefreshCcw className="size-4 mr-1" />
|
||||
Reload
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipPrimitive.Portal>
|
||||
<TooltipContent sideOffset={5} className="z-[60]">
|
||||
<p>Restart the MariaDB service without rebuilding</p>
|
||||
</TooltipContent>
|
||||
</TooltipPrimitive.Portal>
|
||||
</Tooltip>
|
||||
</Button>
|
||||
</DialogAction>
|
||||
</TooltipProvider>
|
||||
{data?.applicationStatus === "idle" ? (
|
||||
{canDeploy && (
|
||||
<TooltipProvider delayDuration={0}>
|
||||
<DialogAction
|
||||
title="Start Mariadb"
|
||||
description="Are you sure you want to start this mariadb?"
|
||||
title="Deploy Mariadb"
|
||||
description="Are you sure you want to deploy this mariadb?"
|
||||
type="default"
|
||||
onClick={async () => {
|
||||
await start({
|
||||
mariadbId: mariadbId,
|
||||
})
|
||||
.then(() => {
|
||||
toast.success("Mariadb started successfully");
|
||||
refetch();
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error starting Mariadb");
|
||||
});
|
||||
setIsDeploying(true);
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
refetch();
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
variant="secondary"
|
||||
isLoading={isStarting}
|
||||
variant="default"
|
||||
isLoading={data?.applicationStatus === "running"}
|
||||
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||
>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div className="flex items-center">
|
||||
<CheckCircle2 className="size-4 mr-1" />
|
||||
Start
|
||||
<Rocket className="size-4 mr-1" />
|
||||
Deploy
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipPrimitive.Portal>
|
||||
<TooltipContent sideOffset={5} className="z-[60]">
|
||||
<p>
|
||||
Start the MariaDB database (requires a previous
|
||||
successful setup)
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</TooltipPrimitive.Portal>
|
||||
</Tooltip>
|
||||
</Button>
|
||||
</DialogAction>
|
||||
</TooltipProvider>
|
||||
) : (
|
||||
<TooltipProvider delayDuration={0}>
|
||||
<DialogAction
|
||||
title="Stop Mariadb"
|
||||
description="Are you sure you want to stop this mariadb?"
|
||||
onClick={async () => {
|
||||
await stop({
|
||||
mariadbId: mariadbId,
|
||||
})
|
||||
.then(() => {
|
||||
toast.success("Mariadb stopped successfully");
|
||||
refetch();
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error stopping Mariadb");
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
variant="destructive"
|
||||
isLoading={isStopping}
|
||||
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||
>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div className="flex items-center">
|
||||
<Ban className="size-4 mr-1" />
|
||||
Stop
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipPrimitive.Portal>
|
||||
<TooltipContent sideOffset={5} className="z-[60]">
|
||||
<p>Stop the currently running MariaDB database</p>
|
||||
<p>Downloads and sets up the MariaDB database</p>
|
||||
</TooltipContent>
|
||||
</TooltipPrimitive.Portal>
|
||||
</Tooltip>
|
||||
@@ -227,6 +108,132 @@ export const ShowGeneralMariadb = ({ mariadbId }: Props) => {
|
||||
</DialogAction>
|
||||
</TooltipProvider>
|
||||
)}
|
||||
{canDeploy && (
|
||||
<TooltipProvider delayDuration={0}>
|
||||
<DialogAction
|
||||
title="Reload Mariadb"
|
||||
description="Are you sure you want to reload this mariadb?"
|
||||
type="default"
|
||||
onClick={async () => {
|
||||
await reload({
|
||||
mariadbId: mariadbId,
|
||||
appName: data?.appName || "",
|
||||
})
|
||||
.then(() => {
|
||||
toast.success("Mariadb reloaded successfully");
|
||||
refetch();
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error reloading Mariadb");
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
variant="secondary"
|
||||
isLoading={isReloading}
|
||||
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||
>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div className="flex items-center">
|
||||
<RefreshCcw className="size-4 mr-1" />
|
||||
Reload
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipPrimitive.Portal>
|
||||
<TooltipContent sideOffset={5} className="z-[60]">
|
||||
<p>Restart the MariaDB service without rebuilding</p>
|
||||
</TooltipContent>
|
||||
</TooltipPrimitive.Portal>
|
||||
</Tooltip>
|
||||
</Button>
|
||||
</DialogAction>
|
||||
</TooltipProvider>
|
||||
)}
|
||||
{canDeploy &&
|
||||
(data?.applicationStatus === "idle" ? (
|
||||
<TooltipProvider delayDuration={0}>
|
||||
<DialogAction
|
||||
title="Start Mariadb"
|
||||
description="Are you sure you want to start this mariadb?"
|
||||
type="default"
|
||||
onClick={async () => {
|
||||
await start({
|
||||
mariadbId: mariadbId,
|
||||
})
|
||||
.then(() => {
|
||||
toast.success("Mariadb started successfully");
|
||||
refetch();
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error starting Mariadb");
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
variant="secondary"
|
||||
isLoading={isStarting}
|
||||
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||
>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div className="flex items-center">
|
||||
<CheckCircle2 className="size-4 mr-1" />
|
||||
Start
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipPrimitive.Portal>
|
||||
<TooltipContent sideOffset={5} className="z-[60]">
|
||||
<p>
|
||||
Start the MariaDB database (requires a previous
|
||||
successful setup)
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</TooltipPrimitive.Portal>
|
||||
</Tooltip>
|
||||
</Button>
|
||||
</DialogAction>
|
||||
</TooltipProvider>
|
||||
) : (
|
||||
<TooltipProvider delayDuration={0}>
|
||||
<DialogAction
|
||||
title="Stop Mariadb"
|
||||
description="Are you sure you want to stop this mariadb?"
|
||||
onClick={async () => {
|
||||
await stop({
|
||||
mariadbId: mariadbId,
|
||||
})
|
||||
.then(() => {
|
||||
toast.success("Mariadb stopped successfully");
|
||||
refetch();
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error stopping Mariadb");
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
variant="destructive"
|
||||
isLoading={isStopping}
|
||||
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||
>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div className="flex items-center">
|
||||
<Ban className="size-4 mr-1" />
|
||||
Stop
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipPrimitive.Portal>
|
||||
<TooltipContent sideOffset={5} className="z-[60]">
|
||||
<p>Stop the currently running MariaDB database</p>
|
||||
</TooltipContent>
|
||||
</TooltipPrimitive.Portal>
|
||||
</Tooltip>
|
||||
</Button>
|
||||
</DialogAction>
|
||||
</TooltipProvider>
|
||||
))}
|
||||
<DockerTerminalModal
|
||||
appName={data?.appName || ""}
|
||||
serverId={data?.serverId || ""}
|
||||
|
||||
@@ -21,6 +21,8 @@ interface Props {
|
||||
}
|
||||
|
||||
export const ShowGeneralMongo = ({ mongoId }: Props) => {
|
||||
const { data: permissions } = api.user.getPermissions.useQuery();
|
||||
const canDeploy = permissions?.deployment.create ?? false;
|
||||
const { data, refetch } = api.mongo.one.useQuery(
|
||||
{
|
||||
mongoId,
|
||||
@@ -73,153 +75,158 @@ export const ShowGeneralMongo = ({ mongoId }: Props) => {
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-row gap-4 flex-wrap">
|
||||
<TooltipProvider delayDuration={0}>
|
||||
<DialogAction
|
||||
title="Deploy Mongo"
|
||||
description="Are you sure you want to deploy this mongo?"
|
||||
type="default"
|
||||
onClick={async () => {
|
||||
setIsDeploying(true);
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
refetch();
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
variant="default"
|
||||
isLoading={data?.applicationStatus === "running"}
|
||||
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||
>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div className="flex items-center">
|
||||
<Rocket className="size-4 mr-1" />
|
||||
Deploy
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipPrimitive.Portal>
|
||||
<TooltipContent sideOffset={5} className="z-[60]">
|
||||
<p>Downloads and sets up the MongoDB database</p>
|
||||
</TooltipContent>
|
||||
</TooltipPrimitive.Portal>
|
||||
</Tooltip>
|
||||
</Button>
|
||||
</DialogAction>
|
||||
<DialogAction
|
||||
title="Reload Mongo"
|
||||
description="Are you sure you want to reload this mongo?"
|
||||
type="default"
|
||||
onClick={async () => {
|
||||
await reload({
|
||||
mongoId: mongoId,
|
||||
appName: data?.appName || "",
|
||||
})
|
||||
.then(() => {
|
||||
toast.success("Mongo reloaded successfully");
|
||||
refetch();
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error reloading Mongo");
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
variant="secondary"
|
||||
isLoading={isReloading}
|
||||
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||
>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div className="flex items-center">
|
||||
<RefreshCcw className="size-4 mr-1" />
|
||||
Reload
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipPrimitive.Portal>
|
||||
<TooltipContent sideOffset={5} className="z-[60]">
|
||||
<p>Restart the MongoDB service without rebuilding</p>
|
||||
</TooltipContent>
|
||||
</TooltipPrimitive.Portal>
|
||||
</Tooltip>
|
||||
</Button>
|
||||
</DialogAction>
|
||||
{data?.applicationStatus === "idle" ? (
|
||||
{canDeploy && (
|
||||
<DialogAction
|
||||
title="Start Mongo"
|
||||
description="Are you sure you want to start this mongo?"
|
||||
title="Deploy Mongo"
|
||||
description="Are you sure you want to deploy this mongo?"
|
||||
type="default"
|
||||
onClick={async () => {
|
||||
await start({
|
||||
mongoId: mongoId,
|
||||
})
|
||||
.then(() => {
|
||||
toast.success("Mongo started successfully");
|
||||
refetch();
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error starting Mongo");
|
||||
});
|
||||
setIsDeploying(true);
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
refetch();
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
variant="secondary"
|
||||
isLoading={isStarting}
|
||||
variant="default"
|
||||
isLoading={data?.applicationStatus === "running"}
|
||||
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||
>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div className="flex items-center">
|
||||
<CheckCircle2 className="size-4 mr-1" />
|
||||
Start
|
||||
<Rocket className="size-4 mr-1" />
|
||||
Deploy
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipPrimitive.Portal>
|
||||
<TooltipContent sideOffset={5} className="z-[60]">
|
||||
<p>
|
||||
Start the MongoDB database (requires a previous
|
||||
successful setup)
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</TooltipPrimitive.Portal>
|
||||
</Tooltip>
|
||||
</Button>
|
||||
</DialogAction>
|
||||
) : (
|
||||
<DialogAction
|
||||
title="Stop Mongo"
|
||||
description="Are you sure you want to stop this mongo?"
|
||||
onClick={async () => {
|
||||
await stop({
|
||||
mongoId: mongoId,
|
||||
})
|
||||
.then(() => {
|
||||
toast.success("Mongo stopped successfully");
|
||||
refetch();
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error stopping Mongo");
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
variant="destructive"
|
||||
isLoading={isStopping}
|
||||
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||
>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div className="flex items-center">
|
||||
<Ban className="size-4 mr-1" />
|
||||
Stop
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipPrimitive.Portal>
|
||||
<TooltipContent sideOffset={5} className="z-[60]">
|
||||
<p>Stop the currently running MongoDB database</p>
|
||||
<p>Downloads and sets up the MongoDB database</p>
|
||||
</TooltipContent>
|
||||
</TooltipPrimitive.Portal>
|
||||
</Tooltip>
|
||||
</Button>
|
||||
</DialogAction>
|
||||
)}
|
||||
{canDeploy && (
|
||||
<DialogAction
|
||||
title="Reload Mongo"
|
||||
description="Are you sure you want to reload this mongo?"
|
||||
type="default"
|
||||
onClick={async () => {
|
||||
await reload({
|
||||
mongoId: mongoId,
|
||||
appName: data?.appName || "",
|
||||
})
|
||||
.then(() => {
|
||||
toast.success("Mongo reloaded successfully");
|
||||
refetch();
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error reloading Mongo");
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
variant="secondary"
|
||||
isLoading={isReloading}
|
||||
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||
>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div className="flex items-center">
|
||||
<RefreshCcw className="size-4 mr-1" />
|
||||
Reload
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipPrimitive.Portal>
|
||||
<TooltipContent sideOffset={5} className="z-[60]">
|
||||
<p>Restart the MongoDB service without rebuilding</p>
|
||||
</TooltipContent>
|
||||
</TooltipPrimitive.Portal>
|
||||
</Tooltip>
|
||||
</Button>
|
||||
</DialogAction>
|
||||
)}
|
||||
{canDeploy &&
|
||||
(data?.applicationStatus === "idle" ? (
|
||||
<DialogAction
|
||||
title="Start Mongo"
|
||||
description="Are you sure you want to start this mongo?"
|
||||
type="default"
|
||||
onClick={async () => {
|
||||
await start({
|
||||
mongoId: mongoId,
|
||||
})
|
||||
.then(() => {
|
||||
toast.success("Mongo started successfully");
|
||||
refetch();
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error starting Mongo");
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
variant="secondary"
|
||||
isLoading={isStarting}
|
||||
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||
>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div className="flex items-center">
|
||||
<CheckCircle2 className="size-4 mr-1" />
|
||||
Start
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipPrimitive.Portal>
|
||||
<TooltipContent sideOffset={5} className="z-[60]">
|
||||
<p>
|
||||
Start the MongoDB database (requires a previous
|
||||
successful setup)
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</TooltipPrimitive.Portal>
|
||||
</Tooltip>
|
||||
</Button>
|
||||
</DialogAction>
|
||||
) : (
|
||||
<DialogAction
|
||||
title="Stop Mongo"
|
||||
description="Are you sure you want to stop this mongo?"
|
||||
onClick={async () => {
|
||||
await stop({
|
||||
mongoId: mongoId,
|
||||
})
|
||||
.then(() => {
|
||||
toast.success("Mongo stopped successfully");
|
||||
refetch();
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error stopping Mongo");
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
variant="destructive"
|
||||
isLoading={isStopping}
|
||||
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||
>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div className="flex items-center">
|
||||
<Ban className="size-4 mr-1" />
|
||||
Stop
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipPrimitive.Portal>
|
||||
<TooltipContent sideOffset={5} className="z-[60]">
|
||||
<p>Stop the currently running MongoDB database</p>
|
||||
</TooltipContent>
|
||||
</TooltipPrimitive.Portal>
|
||||
</Tooltip>
|
||||
</Button>
|
||||
</DialogAction>
|
||||
))}
|
||||
</TooltipProvider>
|
||||
<DockerTerminalModal
|
||||
appName={data?.appName || ""}
|
||||
|
||||
@@ -21,6 +21,8 @@ interface Props {
|
||||
}
|
||||
|
||||
export const ShowGeneralMysql = ({ mysqlId }: Props) => {
|
||||
const { data: permissions } = api.user.getPermissions.useQuery();
|
||||
const canDeploy = permissions?.deployment.create ?? false;
|
||||
const { data, refetch } = api.mysql.one.useQuery(
|
||||
{
|
||||
mysqlId,
|
||||
@@ -71,153 +73,158 @@ export const ShowGeneralMysql = ({ mysqlId }: Props) => {
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-row gap-4 flex-wrap">
|
||||
<TooltipProvider delayDuration={0}>
|
||||
<DialogAction
|
||||
title="Deploy MySQL"
|
||||
description="Are you sure you want to deploy this mysql?"
|
||||
type="default"
|
||||
onClick={async () => {
|
||||
setIsDeploying(true);
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
refetch();
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
variant="default"
|
||||
isLoading={data?.applicationStatus === "running"}
|
||||
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||
>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div className="flex items-center">
|
||||
<Rocket className="size-4 mr-1" />
|
||||
Deploy
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipPrimitive.Portal>
|
||||
<TooltipContent sideOffset={5} className="z-[60]">
|
||||
<p>Downloads and sets up the MySQL database</p>
|
||||
</TooltipContent>
|
||||
</TooltipPrimitive.Portal>
|
||||
</Tooltip>
|
||||
</Button>
|
||||
</DialogAction>
|
||||
<DialogAction
|
||||
title="Reload MySQL"
|
||||
description="Are you sure you want to reload this mysql?"
|
||||
type="default"
|
||||
onClick={async () => {
|
||||
await reload({
|
||||
mysqlId: mysqlId,
|
||||
appName: data?.appName || "",
|
||||
})
|
||||
.then(() => {
|
||||
toast.success("MySQL reloaded successfully");
|
||||
refetch();
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error reloading MySQL");
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
variant="secondary"
|
||||
isLoading={isReloading}
|
||||
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||
>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div className="flex items-center">
|
||||
<RefreshCcw className="size-4 mr-1" />
|
||||
Reload
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipPrimitive.Portal>
|
||||
<TooltipContent sideOffset={5} className="z-[60]">
|
||||
<p>Restart the MySQL service without rebuilding</p>
|
||||
</TooltipContent>
|
||||
</TooltipPrimitive.Portal>
|
||||
</Tooltip>
|
||||
</Button>
|
||||
</DialogAction>
|
||||
{data?.applicationStatus === "idle" ? (
|
||||
{canDeploy && (
|
||||
<DialogAction
|
||||
title="Start MySQL"
|
||||
description="Are you sure you want to start this mysql?"
|
||||
title="Deploy MySQL"
|
||||
description="Are you sure you want to deploy this mysql?"
|
||||
type="default"
|
||||
onClick={async () => {
|
||||
await start({
|
||||
mysqlId: mysqlId,
|
||||
})
|
||||
.then(() => {
|
||||
toast.success("MySQL started successfully");
|
||||
refetch();
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error starting MySQL");
|
||||
});
|
||||
setIsDeploying(true);
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
refetch();
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
variant="secondary"
|
||||
isLoading={isStarting}
|
||||
variant="default"
|
||||
isLoading={data?.applicationStatus === "running"}
|
||||
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||
>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div className="flex items-center">
|
||||
<CheckCircle2 className="size-4 mr-1" />
|
||||
Start
|
||||
<Rocket className="size-4 mr-1" />
|
||||
Deploy
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipPrimitive.Portal>
|
||||
<TooltipContent sideOffset={5} className="z-[60]">
|
||||
<p>
|
||||
Start the MySQL database (requires a previous
|
||||
successful setup)
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</TooltipPrimitive.Portal>
|
||||
</Tooltip>
|
||||
</Button>
|
||||
</DialogAction>
|
||||
) : (
|
||||
<DialogAction
|
||||
title="Stop MySQL"
|
||||
description="Are you sure you want to stop this mysql?"
|
||||
onClick={async () => {
|
||||
await stop({
|
||||
mysqlId: mysqlId,
|
||||
})
|
||||
.then(() => {
|
||||
toast.success("MySQL stopped successfully");
|
||||
refetch();
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error stopping MySQL");
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
variant="destructive"
|
||||
isLoading={isStopping}
|
||||
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||
>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div className="flex items-center">
|
||||
<Ban className="size-4 mr-1" />
|
||||
Stop
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipPrimitive.Portal>
|
||||
<TooltipContent sideOffset={5} className="z-[60]">
|
||||
<p>Stop the currently running MySQL database</p>
|
||||
<p>Downloads and sets up the MySQL database</p>
|
||||
</TooltipContent>
|
||||
</TooltipPrimitive.Portal>
|
||||
</Tooltip>
|
||||
</Button>
|
||||
</DialogAction>
|
||||
)}
|
||||
{canDeploy && (
|
||||
<DialogAction
|
||||
title="Reload MySQL"
|
||||
description="Are you sure you want to reload this mysql?"
|
||||
type="default"
|
||||
onClick={async () => {
|
||||
await reload({
|
||||
mysqlId: mysqlId,
|
||||
appName: data?.appName || "",
|
||||
})
|
||||
.then(() => {
|
||||
toast.success("MySQL reloaded successfully");
|
||||
refetch();
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error reloading MySQL");
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
variant="secondary"
|
||||
isLoading={isReloading}
|
||||
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||
>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div className="flex items-center">
|
||||
<RefreshCcw className="size-4 mr-1" />
|
||||
Reload
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipPrimitive.Portal>
|
||||
<TooltipContent sideOffset={5} className="z-[60]">
|
||||
<p>Restart the MySQL service without rebuilding</p>
|
||||
</TooltipContent>
|
||||
</TooltipPrimitive.Portal>
|
||||
</Tooltip>
|
||||
</Button>
|
||||
</DialogAction>
|
||||
)}
|
||||
{canDeploy &&
|
||||
(data?.applicationStatus === "idle" ? (
|
||||
<DialogAction
|
||||
title="Start MySQL"
|
||||
description="Are you sure you want to start this mysql?"
|
||||
type="default"
|
||||
onClick={async () => {
|
||||
await start({
|
||||
mysqlId: mysqlId,
|
||||
})
|
||||
.then(() => {
|
||||
toast.success("MySQL started successfully");
|
||||
refetch();
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error starting MySQL");
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
variant="secondary"
|
||||
isLoading={isStarting}
|
||||
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||
>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div className="flex items-center">
|
||||
<CheckCircle2 className="size-4 mr-1" />
|
||||
Start
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipPrimitive.Portal>
|
||||
<TooltipContent sideOffset={5} className="z-[60]">
|
||||
<p>
|
||||
Start the MySQL database (requires a previous
|
||||
successful setup)
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</TooltipPrimitive.Portal>
|
||||
</Tooltip>
|
||||
</Button>
|
||||
</DialogAction>
|
||||
) : (
|
||||
<DialogAction
|
||||
title="Stop MySQL"
|
||||
description="Are you sure you want to stop this mysql?"
|
||||
onClick={async () => {
|
||||
await stop({
|
||||
mysqlId: mysqlId,
|
||||
})
|
||||
.then(() => {
|
||||
toast.success("MySQL stopped successfully");
|
||||
refetch();
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error stopping MySQL");
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
variant="destructive"
|
||||
isLoading={isStopping}
|
||||
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||
>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div className="flex items-center">
|
||||
<Ban className="size-4 mr-1" />
|
||||
Stop
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipPrimitive.Portal>
|
||||
<TooltipContent sideOffset={5} className="z-[60]">
|
||||
<p>Stop the currently running MySQL database</p>
|
||||
</TooltipContent>
|
||||
</TooltipPrimitive.Portal>
|
||||
</Tooltip>
|
||||
</Button>
|
||||
</DialogAction>
|
||||
))}
|
||||
</TooltipProvider>
|
||||
<DockerTerminalModal
|
||||
appName={data?.appName || ""}
|
||||
|
||||
@@ -21,6 +21,8 @@ interface Props {
|
||||
}
|
||||
|
||||
export const ShowGeneralPostgres = ({ postgresId }: Props) => {
|
||||
const { data: permissions } = api.user.getPermissions.useQuery();
|
||||
const canDeploy = permissions?.deployment.create ?? false;
|
||||
const { data, refetch } = api.postgres.one.useQuery(
|
||||
{
|
||||
postgresId: postgresId,
|
||||
@@ -73,153 +75,162 @@ export const ShowGeneralPostgres = ({ postgresId }: Props) => {
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-row gap-4 flex-wrap">
|
||||
<TooltipProvider disableHoverableContent={false}>
|
||||
<DialogAction
|
||||
title="Deploy PostgreSQL"
|
||||
description="Are you sure you want to deploy this postgres?"
|
||||
type="default"
|
||||
onClick={async () => {
|
||||
setIsDeploying(true);
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
refetch();
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
variant="default"
|
||||
isLoading={data?.applicationStatus === "running"}
|
||||
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||
>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div className="flex items-center">
|
||||
<Rocket className="size-4 mr-1" />
|
||||
Deploy
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipPrimitive.Portal>
|
||||
<TooltipContent sideOffset={5} className="z-[60]">
|
||||
<p>Downloads and sets up the PostgreSQL database</p>
|
||||
</TooltipContent>
|
||||
</TooltipPrimitive.Portal>
|
||||
</Tooltip>
|
||||
</Button>
|
||||
</DialogAction>
|
||||
<DialogAction
|
||||
title="Reload PostgreSQL"
|
||||
description="Are you sure you want to reload this postgres?"
|
||||
type="default"
|
||||
onClick={async () => {
|
||||
await reload({
|
||||
postgresId: postgresId,
|
||||
appName: data?.appName || "",
|
||||
})
|
||||
.then(() => {
|
||||
toast.success("PostgreSQL reloaded successfully");
|
||||
refetch();
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error reloading PostgreSQL");
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
variant="secondary"
|
||||
isLoading={isReloading}
|
||||
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||
>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div className="flex items-center">
|
||||
<RefreshCcw className="size-4 mr-1" />
|
||||
Reload
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipPrimitive.Portal>
|
||||
<TooltipContent sideOffset={5} className="z-[60]">
|
||||
<p>Restart the PostgreSQL service without rebuilding</p>
|
||||
</TooltipContent>
|
||||
</TooltipPrimitive.Portal>
|
||||
</Tooltip>
|
||||
</Button>
|
||||
</DialogAction>
|
||||
{data?.applicationStatus === "idle" ? (
|
||||
{canDeploy && (
|
||||
<DialogAction
|
||||
title="Start PostgreSQL"
|
||||
description="Are you sure you want to start this postgres?"
|
||||
title="Deploy PostgreSQL"
|
||||
description="Are you sure you want to deploy this postgres?"
|
||||
type="default"
|
||||
onClick={async () => {
|
||||
await start({
|
||||
postgresId: postgresId,
|
||||
})
|
||||
.then(() => {
|
||||
toast.success("PostgreSQL started successfully");
|
||||
refetch();
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error starting PostgreSQL");
|
||||
});
|
||||
setIsDeploying(true);
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
refetch();
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
variant="secondary"
|
||||
isLoading={isStarting}
|
||||
variant="default"
|
||||
isLoading={data?.applicationStatus === "running"}
|
||||
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||
>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div className="flex items-center">
|
||||
<CheckCircle2 className="size-4 mr-1" />
|
||||
Start
|
||||
<Rocket className="size-4 mr-1" />
|
||||
Deploy
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipPrimitive.Portal>
|
||||
<TooltipContent sideOffset={5} className="z-[60]">
|
||||
<p>
|
||||
Start the PostgreSQL database (requires a previous
|
||||
successful setup)
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</TooltipPrimitive.Portal>
|
||||
</Tooltip>
|
||||
</Button>
|
||||
</DialogAction>
|
||||
) : (
|
||||
<DialogAction
|
||||
title="Stop PostgreSQL"
|
||||
description="Are you sure you want to stop this postgres?"
|
||||
onClick={async () => {
|
||||
await stop({
|
||||
postgresId: postgresId,
|
||||
})
|
||||
.then(() => {
|
||||
toast.success("PostgreSQL stopped successfully");
|
||||
refetch();
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error stopping PostgreSQL");
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
variant="destructive"
|
||||
isLoading={isStopping}
|
||||
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||
>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div className="flex items-center">
|
||||
<Ban className="size-4 mr-1" />
|
||||
Stop
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipPrimitive.Portal>
|
||||
<TooltipContent sideOffset={5} className="z-[60]">
|
||||
<p>Stop the currently running PostgreSQL database</p>
|
||||
<p>Downloads and sets up the PostgreSQL database</p>
|
||||
</TooltipContent>
|
||||
</TooltipPrimitive.Portal>
|
||||
</Tooltip>
|
||||
</Button>
|
||||
</DialogAction>
|
||||
)}
|
||||
{canDeploy && (
|
||||
<DialogAction
|
||||
title="Reload PostgreSQL"
|
||||
description="Are you sure you want to reload this postgres?"
|
||||
type="default"
|
||||
onClick={async () => {
|
||||
await reload({
|
||||
postgresId: postgresId,
|
||||
appName: data?.appName || "",
|
||||
})
|
||||
.then(() => {
|
||||
toast.success("PostgreSQL reloaded successfully");
|
||||
refetch();
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error reloading PostgreSQL");
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
variant="secondary"
|
||||
isLoading={isReloading}
|
||||
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||
>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div className="flex items-center">
|
||||
<RefreshCcw className="size-4 mr-1" />
|
||||
Reload
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipPrimitive.Portal>
|
||||
<TooltipContent sideOffset={5} className="z-[60]">
|
||||
<p>
|
||||
Restart the PostgreSQL service without rebuilding
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</TooltipPrimitive.Portal>
|
||||
</Tooltip>
|
||||
</Button>
|
||||
</DialogAction>
|
||||
)}
|
||||
{canDeploy &&
|
||||
(data?.applicationStatus === "idle" ? (
|
||||
<DialogAction
|
||||
title="Start PostgreSQL"
|
||||
description="Are you sure you want to start this postgres?"
|
||||
type="default"
|
||||
onClick={async () => {
|
||||
await start({
|
||||
postgresId: postgresId,
|
||||
})
|
||||
.then(() => {
|
||||
toast.success("PostgreSQL started successfully");
|
||||
refetch();
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error starting PostgreSQL");
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
variant="secondary"
|
||||
isLoading={isStarting}
|
||||
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||
>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div className="flex items-center">
|
||||
<CheckCircle2 className="size-4 mr-1" />
|
||||
Start
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipPrimitive.Portal>
|
||||
<TooltipContent sideOffset={5} className="z-[60]">
|
||||
<p>
|
||||
Start the PostgreSQL database (requires a previous
|
||||
successful setup)
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</TooltipPrimitive.Portal>
|
||||
</Tooltip>
|
||||
</Button>
|
||||
</DialogAction>
|
||||
) : (
|
||||
<DialogAction
|
||||
title="Stop PostgreSQL"
|
||||
description="Are you sure you want to stop this postgres?"
|
||||
onClick={async () => {
|
||||
await stop({
|
||||
postgresId: postgresId,
|
||||
})
|
||||
.then(() => {
|
||||
toast.success("PostgreSQL stopped successfully");
|
||||
refetch();
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error stopping PostgreSQL");
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
variant="destructive"
|
||||
isLoading={isStopping}
|
||||
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||
>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div className="flex items-center">
|
||||
<Ban className="size-4 mr-1" />
|
||||
Stop
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipPrimitive.Portal>
|
||||
<TooltipContent sideOffset={5} className="z-[60]">
|
||||
<p>
|
||||
Stop the currently running PostgreSQL database
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</TooltipPrimitive.Portal>
|
||||
</Tooltip>
|
||||
</Button>
|
||||
</DialogAction>
|
||||
))}
|
||||
</TooltipProvider>
|
||||
<DockerTerminalModal
|
||||
appName={data?.appName || ""}
|
||||
|
||||
@@ -57,19 +57,13 @@ export const AdvancedEnvironmentSelector = ({
|
||||
const [description, setDescription] = useState("");
|
||||
|
||||
// Get current user's permissions
|
||||
const { data: currentUser } = api.user.get.useQuery();
|
||||
const { data: permissions } = api.user.getPermissions.useQuery();
|
||||
|
||||
// Check if user can create environments
|
||||
const canCreateEnvironments =
|
||||
currentUser?.role === "owner" ||
|
||||
currentUser?.role === "admin" ||
|
||||
currentUser?.canCreateEnvironments === true;
|
||||
const canCreateEnvironments = !!permissions?.environment.create;
|
||||
|
||||
// Check if user can delete environments
|
||||
const canDeleteEnvironments =
|
||||
currentUser?.role === "owner" ||
|
||||
currentUser?.role === "admin" ||
|
||||
currentUser?.canDeleteEnvironments === true;
|
||||
const canDeleteEnvironments = !!permissions?.environment.delete;
|
||||
|
||||
const haveServices =
|
||||
selectedEnvironment &&
|
||||
|
||||
@@ -39,6 +39,9 @@ interface Props {
|
||||
}
|
||||
|
||||
export const EnvironmentVariables = ({ environmentId, children }: Props) => {
|
||||
const { data: permissions } = api.user.getPermissions.useQuery();
|
||||
const canRead = permissions?.environmentEnvVars.read ?? false;
|
||||
const canWrite = permissions?.environmentEnvVars.write ?? false;
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const utils = api.useUtils();
|
||||
const { mutateAsync, error, isError, isPending } =
|
||||
@@ -97,6 +100,10 @@ export const EnvironmentVariables = ({ environmentId, children }: Props) => {
|
||||
};
|
||||
}, [form, onSubmit, isPending, isOpen]);
|
||||
|
||||
if (!canRead) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||
<DialogTrigger asChild>
|
||||
@@ -141,6 +148,7 @@ export const EnvironmentVariables = ({ environmentId, children }: Props) => {
|
||||
<CodeEditor
|
||||
lineWrapping
|
||||
language="properties"
|
||||
readOnly={!canWrite}
|
||||
wrapperClassName="h-[35rem] font-mono"
|
||||
placeholder={`NODE_ENV=development
|
||||
DATABASE_URL=postgresql://localhost:5432/mydb
|
||||
@@ -157,11 +165,13 @@ API_KEY=your-api-key-here
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<DialogFooter>
|
||||
<Button isLoading={isPending} type="submit">
|
||||
Update
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
{canWrite && (
|
||||
<DialogFooter>
|
||||
<Button isLoading={isPending} type="submit">
|
||||
Update
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
)}
|
||||
</form>
|
||||
</Form>
|
||||
</div>
|
||||
|
||||
@@ -39,6 +39,9 @@ interface Props {
|
||||
}
|
||||
|
||||
export const ProjectEnvironment = ({ projectId, children }: Props) => {
|
||||
const { data: permissions } = api.user.getPermissions.useQuery();
|
||||
const canRead = permissions?.projectEnvVars.read ?? false;
|
||||
const canWrite = permissions?.projectEnvVars.write ?? false;
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const utils = api.useUtils();
|
||||
const { mutateAsync, error, isError, isPending } =
|
||||
@@ -96,6 +99,10 @@ export const ProjectEnvironment = ({ projectId, children }: Props) => {
|
||||
};
|
||||
}, [form, onSubmit, isPending, isOpen]);
|
||||
|
||||
if (!canRead) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||
<DialogTrigger asChild>
|
||||
@@ -139,6 +146,7 @@ export const ProjectEnvironment = ({ projectId, children }: Props) => {
|
||||
<CodeEditor
|
||||
lineWrapping
|
||||
language="properties"
|
||||
readOnly={!canWrite}
|
||||
wrapperClassName="h-[35rem] font-mono"
|
||||
placeholder={`NODE_ENV=production
|
||||
PORT=3000
|
||||
@@ -154,11 +162,13 @@ PORT=3000
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<DialogFooter>
|
||||
<Button isLoading={isPending} type="submit">
|
||||
Update
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
{canWrite && (
|
||||
<DialogFooter>
|
||||
<Button isLoading={isPending} type="submit">
|
||||
Update
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
)}
|
||||
</form>
|
||||
</Form>
|
||||
</div>
|
||||
|
||||
@@ -61,6 +61,7 @@ export const ShowProjects = () => {
|
||||
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||
const { data, isPending } = api.project.all.useQuery();
|
||||
const { data: auth } = api.user.get.useQuery();
|
||||
const { data: permissions } = api.user.getPermissions.useQuery();
|
||||
const { mutateAsync } = api.project.remove.useMutation();
|
||||
|
||||
const [searchQuery, setSearchQuery] = useState(
|
||||
@@ -168,11 +169,6 @@ export const ShowProjects = () => {
|
||||
<BreadcrumbSidebar
|
||||
list={[{ name: "Projects", href: "/dashboard/projects" }]}
|
||||
/>
|
||||
{!isCloud && (
|
||||
<div className="absolute top-4 right-4">
|
||||
<TimeBadge />
|
||||
</div>
|
||||
)}
|
||||
<div className="w-full">
|
||||
<Card className="h-full bg-sidebar p-2.5 rounded-xl ">
|
||||
<div className="rounded-xl bg-background shadow-md ">
|
||||
@@ -186,9 +182,7 @@ export const ShowProjects = () => {
|
||||
Create and manage your projects
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
{(auth?.role === "owner" ||
|
||||
auth?.role === "admin" ||
|
||||
auth?.canCreateProjects) && (
|
||||
{permissions?.project.create && (
|
||||
<div className="">
|
||||
<HandleProject />
|
||||
</div>
|
||||
@@ -361,8 +355,7 @@ export const ShowProjects = () => {
|
||||
<div
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{(auth?.role === "owner" ||
|
||||
auth?.canDeleteProjects) && (
|
||||
{permissions?.project.delete && (
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger className="w-full">
|
||||
<DropdownMenuItem
|
||||
|
||||
@@ -21,6 +21,8 @@ interface Props {
|
||||
}
|
||||
|
||||
export const ShowGeneralRedis = ({ redisId }: Props) => {
|
||||
const { data: permissions } = api.user.getPermissions.useQuery();
|
||||
const canDeploy = permissions?.deployment.create ?? false;
|
||||
const { data, refetch } = api.redis.one.useQuery(
|
||||
{
|
||||
redisId,
|
||||
@@ -72,153 +74,158 @@ export const ShowGeneralRedis = ({ redisId }: Props) => {
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-row gap-4 flex-wrap">
|
||||
<TooltipProvider delayDuration={0}>
|
||||
<DialogAction
|
||||
title="Deploy Redis"
|
||||
description="Are you sure you want to deploy this redis?"
|
||||
type="default"
|
||||
onClick={async () => {
|
||||
setIsDeploying(true);
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
refetch();
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
variant="default"
|
||||
isLoading={data?.applicationStatus === "running"}
|
||||
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||
>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div className="flex items-center">
|
||||
<Rocket className="size-4 mr-1" />
|
||||
Deploy
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipPrimitive.Portal>
|
||||
<TooltipContent sideOffset={5} className="z-[60]">
|
||||
<p>Downloads and sets up the Redis database</p>
|
||||
</TooltipContent>
|
||||
</TooltipPrimitive.Portal>
|
||||
</Tooltip>
|
||||
</Button>
|
||||
</DialogAction>
|
||||
<DialogAction
|
||||
title="Reload Redis"
|
||||
description="Are you sure you want to reload this redis?"
|
||||
type="default"
|
||||
onClick={async () => {
|
||||
await reload({
|
||||
redisId: redisId,
|
||||
appName: data?.appName || "",
|
||||
})
|
||||
.then(() => {
|
||||
toast.success("Redis reloaded successfully");
|
||||
refetch();
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error reloading Redis");
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
variant="secondary"
|
||||
isLoading={isReloading}
|
||||
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||
>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div className="flex items-center">
|
||||
<RefreshCcw className="size-4 mr-1" />
|
||||
Reload
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipPrimitive.Portal>
|
||||
<TooltipContent sideOffset={5} className="z-[60]">
|
||||
<p>Restart the Redis service without rebuilding</p>
|
||||
</TooltipContent>
|
||||
</TooltipPrimitive.Portal>
|
||||
</Tooltip>
|
||||
</Button>
|
||||
</DialogAction>
|
||||
{data?.applicationStatus === "idle" ? (
|
||||
{canDeploy && (
|
||||
<DialogAction
|
||||
title="Start Redis"
|
||||
description="Are you sure you want to start this redis?"
|
||||
title="Deploy Redis"
|
||||
description="Are you sure you want to deploy this redis?"
|
||||
type="default"
|
||||
onClick={async () => {
|
||||
await start({
|
||||
redisId: redisId,
|
||||
})
|
||||
.then(() => {
|
||||
toast.success("Redis started successfully");
|
||||
refetch();
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error starting Redis");
|
||||
});
|
||||
setIsDeploying(true);
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
refetch();
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
variant="secondary"
|
||||
isLoading={isStarting}
|
||||
variant="default"
|
||||
isLoading={data?.applicationStatus === "running"}
|
||||
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||
>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div className="flex items-center">
|
||||
<CheckCircle2 className="size-4 mr-1" />
|
||||
Start
|
||||
<Rocket className="size-4 mr-1" />
|
||||
Deploy
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipPrimitive.Portal>
|
||||
<TooltipContent sideOffset={5} className="z-[60]">
|
||||
<p>
|
||||
Start the Redis database (requires a previous
|
||||
successful setup)
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</TooltipPrimitive.Portal>
|
||||
</Tooltip>
|
||||
</Button>
|
||||
</DialogAction>
|
||||
) : (
|
||||
<DialogAction
|
||||
title="Stop Redis"
|
||||
description="Are you sure you want to stop this redis?"
|
||||
onClick={async () => {
|
||||
await stop({
|
||||
redisId: redisId,
|
||||
})
|
||||
.then(() => {
|
||||
toast.success("Redis stopped successfully");
|
||||
refetch();
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error stopping Redis");
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
variant="destructive"
|
||||
isLoading={isStopping}
|
||||
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||
>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div className="flex items-center">
|
||||
<Ban className="size-4 mr-1" />
|
||||
Stop
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipPrimitive.Portal>
|
||||
<TooltipContent sideOffset={5} className="z-[60]">
|
||||
<p>Stop the currently running Redis database</p>
|
||||
<p>Downloads and sets up the Redis database</p>
|
||||
</TooltipContent>
|
||||
</TooltipPrimitive.Portal>
|
||||
</Tooltip>
|
||||
</Button>
|
||||
</DialogAction>
|
||||
)}
|
||||
{canDeploy && (
|
||||
<DialogAction
|
||||
title="Reload Redis"
|
||||
description="Are you sure you want to reload this redis?"
|
||||
type="default"
|
||||
onClick={async () => {
|
||||
await reload({
|
||||
redisId: redisId,
|
||||
appName: data?.appName || "",
|
||||
})
|
||||
.then(() => {
|
||||
toast.success("Redis reloaded successfully");
|
||||
refetch();
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error reloading Redis");
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
variant="secondary"
|
||||
isLoading={isReloading}
|
||||
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||
>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div className="flex items-center">
|
||||
<RefreshCcw className="size-4 mr-1" />
|
||||
Reload
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipPrimitive.Portal>
|
||||
<TooltipContent sideOffset={5} className="z-[60]">
|
||||
<p>Restart the Redis service without rebuilding</p>
|
||||
</TooltipContent>
|
||||
</TooltipPrimitive.Portal>
|
||||
</Tooltip>
|
||||
</Button>
|
||||
</DialogAction>
|
||||
)}
|
||||
{canDeploy &&
|
||||
(data?.applicationStatus === "idle" ? (
|
||||
<DialogAction
|
||||
title="Start Redis"
|
||||
description="Are you sure you want to start this redis?"
|
||||
type="default"
|
||||
onClick={async () => {
|
||||
await start({
|
||||
redisId: redisId,
|
||||
})
|
||||
.then(() => {
|
||||
toast.success("Redis started successfully");
|
||||
refetch();
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error starting Redis");
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
variant="secondary"
|
||||
isLoading={isStarting}
|
||||
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||
>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div className="flex items-center">
|
||||
<CheckCircle2 className="size-4 mr-1" />
|
||||
Start
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipPrimitive.Portal>
|
||||
<TooltipContent sideOffset={5} className="z-[60]">
|
||||
<p>
|
||||
Start the Redis database (requires a previous
|
||||
successful setup)
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</TooltipPrimitive.Portal>
|
||||
</Tooltip>
|
||||
</Button>
|
||||
</DialogAction>
|
||||
) : (
|
||||
<DialogAction
|
||||
title="Stop Redis"
|
||||
description="Are you sure you want to stop this redis?"
|
||||
onClick={async () => {
|
||||
await stop({
|
||||
redisId: redisId,
|
||||
})
|
||||
.then(() => {
|
||||
toast.success("Redis stopped successfully");
|
||||
refetch();
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error stopping Redis");
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
variant="destructive"
|
||||
isLoading={isStopping}
|
||||
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||
>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div className="flex items-center">
|
||||
<Ban className="size-4 mr-1" />
|
||||
Stop
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipPrimitive.Portal>
|
||||
<TooltipContent sideOffset={5} className="z-[60]">
|
||||
<p>Stop the currently running Redis database</p>
|
||||
</TooltipContent>
|
||||
</TooltipPrimitive.Portal>
|
||||
</Tooltip>
|
||||
</Button>
|
||||
</DialogAction>
|
||||
))}
|
||||
</TooltipProvider>
|
||||
<DockerTerminalModal
|
||||
appName={data?.appName || ""}
|
||||
|
||||
@@ -91,7 +91,10 @@ export const ShowBilling = () => {
|
||||
api.stripe.upgradeSubscription.useMutation();
|
||||
const utils = api.useUtils();
|
||||
|
||||
const [serverQuantity, setServerQuantity] = useState(3);
|
||||
const [hobbyServerQuantity, setHobbyServerQuantity] = useState(1);
|
||||
const [startupServerQuantity, setStartupServerQuantity] = useState(
|
||||
STARTUP_SERVERS_INCLUDED,
|
||||
);
|
||||
const [isAnnual, setIsAnnual] = useState(false);
|
||||
const [upgradeTier, setUpgradeTier] = useState<"hobby" | "startup" | null>(
|
||||
null,
|
||||
@@ -111,6 +114,12 @@ export const ShowBilling = () => {
|
||||
productId: string,
|
||||
) => {
|
||||
const stripe = await stripePromise;
|
||||
const serverQuantity =
|
||||
tier === "startup"
|
||||
? startupServerQuantity
|
||||
: tier === "hobby"
|
||||
? hobbyServerQuantity
|
||||
: hobbyServerQuantity;
|
||||
if (data && data.subscriptions.length === 0) {
|
||||
createCheckoutSession({
|
||||
tier,
|
||||
@@ -679,7 +688,7 @@ export const ShowBilling = () => {
|
||||
<p className="text-2xl font-semibold text-foreground">
|
||||
$
|
||||
{calculatePriceHobby(
|
||||
serverQuantity,
|
||||
hobbyServerQuantity,
|
||||
isAnnual,
|
||||
).toFixed(2)}
|
||||
/{isAnnual ? "yr" : "mo"}
|
||||
@@ -692,7 +701,8 @@ export const ShowBilling = () => {
|
||||
<p className="text-xs text-muted-foreground mt-2">
|
||||
$
|
||||
{(
|
||||
calculatePriceHobby(serverQuantity, true) / 12
|
||||
calculatePriceHobby(hobbyServerQuantity, true) /
|
||||
12
|
||||
).toFixed(2)}
|
||||
/mo
|
||||
</p>
|
||||
@@ -724,19 +734,19 @@ export const ShowBilling = () => {
|
||||
Servers:
|
||||
</span>
|
||||
<Button
|
||||
disabled={serverQuantity <= 1}
|
||||
disabled={hobbyServerQuantity <= 1}
|
||||
variant="outline"
|
||||
size="icon"
|
||||
onClick={() =>
|
||||
setServerQuantity((q) => Math.max(1, q - 1))
|
||||
setHobbyServerQuantity((q) => Math.max(1, q - 1))
|
||||
}
|
||||
>
|
||||
<MinusIcon className="h-4 w-4" />
|
||||
</Button>
|
||||
<NumberInput
|
||||
value={serverQuantity}
|
||||
value={hobbyServerQuantity}
|
||||
onChange={(e) =>
|
||||
setServerQuantity(
|
||||
setHobbyServerQuantity(
|
||||
Math.max(
|
||||
1,
|
||||
Number(
|
||||
@@ -750,7 +760,7 @@ export const ShowBilling = () => {
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
onClick={() => setServerQuantity((q) => q + 1)}
|
||||
onClick={() => setHobbyServerQuantity((q) => q + 1)}
|
||||
>
|
||||
<PlusIcon className="h-4 w-4" />
|
||||
</Button>
|
||||
@@ -775,7 +785,7 @@ export const ShowBilling = () => {
|
||||
onClick={() =>
|
||||
handleCheckout("hobby", data!.hobbyProductId!)
|
||||
}
|
||||
disabled={serverQuantity < 1}
|
||||
disabled={hobbyServerQuantity < 1}
|
||||
>
|
||||
Get Started
|
||||
</Button>
|
||||
@@ -806,7 +816,7 @@ export const ShowBilling = () => {
|
||||
<p className="text-2xl font-semibold text-foreground">
|
||||
$
|
||||
{calculatePriceStartup(
|
||||
serverQuantity,
|
||||
startupServerQuantity,
|
||||
isAnnual,
|
||||
).toFixed(2)}
|
||||
/{isAnnual ? "yr" : "mo"}
|
||||
@@ -819,7 +829,10 @@ export const ShowBilling = () => {
|
||||
<p className="text-xs text-muted-foreground mt-2">
|
||||
$
|
||||
{(
|
||||
calculatePriceStartup(serverQuantity, true) / 12
|
||||
calculatePriceStartup(
|
||||
startupServerQuantity,
|
||||
true,
|
||||
) / 12
|
||||
).toFixed(2)}
|
||||
/mo
|
||||
</p>
|
||||
@@ -856,13 +869,14 @@ export const ShowBilling = () => {
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
disabled={
|
||||
serverQuantity <= STARTUP_SERVERS_INCLUDED
|
||||
startupServerQuantity <=
|
||||
STARTUP_SERVERS_INCLUDED
|
||||
}
|
||||
variant="outline"
|
||||
size="icon"
|
||||
className="h-8 w-8"
|
||||
onClick={() =>
|
||||
setServerQuantity((q) =>
|
||||
setStartupServerQuantity((q) =>
|
||||
Math.max(STARTUP_SERVERS_INCLUDED, q - 1),
|
||||
)
|
||||
}
|
||||
@@ -870,9 +884,9 @@ export const ShowBilling = () => {
|
||||
<MinusIcon className="h-4 w-4" />
|
||||
</Button>
|
||||
<NumberInput
|
||||
value={serverQuantity}
|
||||
value={startupServerQuantity}
|
||||
onChange={(e) =>
|
||||
setServerQuantity(
|
||||
setStartupServerQuantity(
|
||||
Math.max(
|
||||
STARTUP_SERVERS_INCLUDED,
|
||||
Number(
|
||||
@@ -887,7 +901,9 @@ export const ShowBilling = () => {
|
||||
variant="outline"
|
||||
size="icon"
|
||||
className="h-8 w-8"
|
||||
onClick={() => setServerQuantity((q) => q + 1)}
|
||||
onClick={() =>
|
||||
setStartupServerQuantity((q) => q + 1)
|
||||
}
|
||||
>
|
||||
<PlusIcon className="h-4 w-4" />
|
||||
</Button>
|
||||
@@ -917,7 +933,7 @@ export const ShowBilling = () => {
|
||||
)
|
||||
}
|
||||
disabled={
|
||||
serverQuantity < STARTUP_SERVERS_INCLUDED
|
||||
startupServerQuantity < STARTUP_SERVERS_INCLUDED
|
||||
}
|
||||
>
|
||||
Get Started
|
||||
@@ -1009,7 +1025,7 @@ export const ShowBilling = () => {
|
||||
<p className="text-2xl font-semibold tracking-tight text-primary ">
|
||||
${" "}
|
||||
{calculatePrice(
|
||||
serverQuantity,
|
||||
hobbyServerQuantity,
|
||||
isAnnual,
|
||||
).toFixed(2)}{" "}
|
||||
USD
|
||||
@@ -1018,7 +1034,10 @@ export const ShowBilling = () => {
|
||||
<p className="text-base font-semibold tracking-tight text-muted-foreground">
|
||||
${" "}
|
||||
{(
|
||||
calculatePrice(serverQuantity, isAnnual) / 12
|
||||
calculatePrice(
|
||||
hobbyServerQuantity,
|
||||
isAnnual,
|
||||
) / 12
|
||||
).toFixed(2)}{" "}
|
||||
/ Month USD
|
||||
</p>
|
||||
@@ -1026,9 +1045,10 @@ export const ShowBilling = () => {
|
||||
) : (
|
||||
<p className="text-2xl font-semibold tracking-tight text-primary ">
|
||||
${" "}
|
||||
{calculatePrice(serverQuantity, isAnnual).toFixed(
|
||||
2,
|
||||
)}{" "}
|
||||
{calculatePrice(
|
||||
hobbyServerQuantity,
|
||||
isAnnual,
|
||||
).toFixed(2)}{" "}
|
||||
USD
|
||||
</p>
|
||||
)}
|
||||
@@ -1071,26 +1091,28 @@ export const ShowBilling = () => {
|
||||
<div className="flex flex-col gap-2 mt-4">
|
||||
<div className="flex items-center gap-2 justify-center">
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{serverQuantity} Servers
|
||||
{hobbyServerQuantity} Servers
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-2">
|
||||
<Button
|
||||
disabled={serverQuantity <= 1}
|
||||
disabled={hobbyServerQuantity <= 1}
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
if (serverQuantity <= 1) return;
|
||||
if (hobbyServerQuantity <= 1) return;
|
||||
|
||||
setServerQuantity(serverQuantity - 1);
|
||||
setHobbyServerQuantity(
|
||||
hobbyServerQuantity - 1,
|
||||
);
|
||||
}}
|
||||
>
|
||||
<MinusIcon className="h-4 w-4" />
|
||||
</Button>
|
||||
<NumberInput
|
||||
value={serverQuantity}
|
||||
value={hobbyServerQuantity}
|
||||
onChange={(e) => {
|
||||
setServerQuantity(
|
||||
setHobbyServerQuantity(
|
||||
e.target.value as unknown as number,
|
||||
);
|
||||
}}
|
||||
@@ -1099,7 +1121,9 @@ export const ShowBilling = () => {
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
setServerQuantity(serverQuantity + 1);
|
||||
setHobbyServerQuantity(
|
||||
hobbyServerQuantity + 1,
|
||||
);
|
||||
}}
|
||||
>
|
||||
<PlusIcon className="h-4 w-4" />
|
||||
@@ -1125,7 +1149,7 @@ export const ShowBilling = () => {
|
||||
onClick={async () => {
|
||||
handleCheckout("legacy", product.id);
|
||||
}}
|
||||
disabled={serverQuantity < 1}
|
||||
disabled={hobbyServerQuantity < 1}
|
||||
>
|
||||
Subscribe
|
||||
</Button>
|
||||
|
||||
@@ -18,6 +18,7 @@ export const ShowCertificates = () => {
|
||||
const { mutateAsync, isPending: isRemoving } =
|
||||
api.certificates.remove.useMutation();
|
||||
const { data, isPending, refetch } = api.certificates.all.useQuery();
|
||||
const { data: permissions } = api.user.getPermissions.useQuery();
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
@@ -53,7 +54,7 @@ export const ShowCertificates = () => {
|
||||
<span className="text-base text-muted-foreground text-center">
|
||||
You don't have any certificates created
|
||||
</span>
|
||||
<AddCertificate />
|
||||
{permissions?.certificate.create && <AddCertificate />}
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col gap-4 min-h-[25vh]">
|
||||
@@ -101,47 +102,52 @@ export const ShowCertificates = () => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-row gap-1">
|
||||
<DialogAction
|
||||
title="Delete Certificate"
|
||||
description="Are you sure you want to delete this certificate?"
|
||||
type="destructive"
|
||||
onClick={async () => {
|
||||
await mutateAsync({
|
||||
certificateId: certificate.certificateId,
|
||||
})
|
||||
.then(() => {
|
||||
toast.success(
|
||||
"Certificate deleted successfully",
|
||||
);
|
||||
refetch();
|
||||
{permissions?.certificate.delete && (
|
||||
<div className="flex flex-row gap-1">
|
||||
<DialogAction
|
||||
title="Delete Certificate"
|
||||
description="Are you sure you want to delete this certificate?"
|
||||
type="destructive"
|
||||
onClick={async () => {
|
||||
await mutateAsync({
|
||||
certificateId:
|
||||
certificate.certificateId,
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error(
|
||||
"Error deleting certificate",
|
||||
);
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="group hover:bg-red-500/10 "
|
||||
isLoading={isRemoving}
|
||||
.then(() => {
|
||||
toast.success(
|
||||
"Certificate deleted successfully",
|
||||
);
|
||||
refetch();
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error(
|
||||
"Error deleting certificate",
|
||||
);
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Trash2 className="size-4 text-primary group-hover:text-red-500" />
|
||||
</Button>
|
||||
</DialogAction>
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="group hover:bg-red-500/10 "
|
||||
isLoading={isRemoving}
|
||||
>
|
||||
<Trash2 className="size-4 text-primary group-hover:text-red-500" />
|
||||
</Button>
|
||||
</DialogAction>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-row gap-2 flex-wrap w-full justify-end mr-4">
|
||||
<AddCertificate />
|
||||
</div>
|
||||
{permissions?.certificate.create && (
|
||||
<div className="flex flex-row gap-2 flex-wrap w-full justify-end mr-4">
|
||||
<AddCertificate />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -16,6 +16,7 @@ export const ShowRegistry = () => {
|
||||
const { mutateAsync, isPending: isRemoving } =
|
||||
api.registry.remove.useMutation();
|
||||
const { data, isPending, refetch } = api.registry.all.useQuery();
|
||||
const { data: permissions } = api.user.getPermissions.useQuery();
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
@@ -44,7 +45,7 @@ export const ShowRegistry = () => {
|
||||
<span className="text-base text-muted-foreground text-center">
|
||||
You don't have any registry configurations
|
||||
</span>
|
||||
<HandleRegistry />
|
||||
{permissions?.registry.create && <HandleRegistry />}
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col gap-4 min-h-[25vh]">
|
||||
@@ -73,45 +74,49 @@ export const ShowRegistry = () => {
|
||||
registryId={registry.registryId}
|
||||
/>
|
||||
|
||||
<DialogAction
|
||||
title="Delete Registry"
|
||||
description="Are you sure you want to delete this registry configuration?"
|
||||
type="destructive"
|
||||
onClick={async () => {
|
||||
await mutateAsync({
|
||||
registryId: registry.registryId,
|
||||
})
|
||||
.then(() => {
|
||||
toast.success(
|
||||
"Registry configuration deleted successfully",
|
||||
);
|
||||
refetch();
|
||||
{permissions?.registry.delete && (
|
||||
<DialogAction
|
||||
title="Delete Registry"
|
||||
description="Are you sure you want to delete this registry configuration?"
|
||||
type="destructive"
|
||||
onClick={async () => {
|
||||
await mutateAsync({
|
||||
registryId: registry.registryId,
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error(
|
||||
"Error deleting registry configuration",
|
||||
);
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="group hover:bg-red-500/10 "
|
||||
isLoading={isRemoving}
|
||||
.then(() => {
|
||||
toast.success(
|
||||
"Registry configuration deleted successfully",
|
||||
);
|
||||
refetch();
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error(
|
||||
"Error deleting registry configuration",
|
||||
);
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Trash2 className="size-4 text-primary group-hover:text-red-500" />
|
||||
</Button>
|
||||
</DialogAction>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="group hover:bg-red-500/10 "
|
||||
isLoading={isRemoving}
|
||||
>
|
||||
<Trash2 className="size-4 text-primary group-hover:text-red-500" />
|
||||
</Button>
|
||||
</DialogAction>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-row gap-2 flex-wrap w-full justify-end mr-4">
|
||||
<HandleRegistry />
|
||||
</div>
|
||||
{permissions?.registry.create && (
|
||||
<div className="flex flex-row gap-2 flex-wrap w-full justify-end mr-4">
|
||||
<HandleRegistry />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -16,6 +16,7 @@ export const ShowDestinations = () => {
|
||||
const { data, isPending, refetch } = api.destination.all.useQuery();
|
||||
const { mutateAsync, isPending: isRemoving } =
|
||||
api.destination.remove.useMutation();
|
||||
const { data: permissions } = api.user.getPermissions.useQuery();
|
||||
return (
|
||||
<div className="w-full">
|
||||
<Card className="h-full bg-sidebar p-2.5 rounded-xl max-w-5xl mx-auto">
|
||||
@@ -45,7 +46,7 @@ export const ShowDestinations = () => {
|
||||
To create a backup it is required to set at least 1
|
||||
provider.
|
||||
</span>
|
||||
<HandleDestinations />
|
||||
{permissions?.destination.create && <HandleDestinations />}
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col gap-4 min-h-[25vh]">
|
||||
@@ -71,43 +72,49 @@ export const ShowDestinations = () => {
|
||||
<HandleDestinations
|
||||
destinationId={destination.destinationId}
|
||||
/>
|
||||
<DialogAction
|
||||
title="Delete Destination"
|
||||
description="Are you sure you want to delete this destination?"
|
||||
type="destructive"
|
||||
onClick={async () => {
|
||||
await mutateAsync({
|
||||
destinationId: destination.destinationId,
|
||||
})
|
||||
.then(() => {
|
||||
toast.success(
|
||||
"Destination deleted successfully",
|
||||
);
|
||||
refetch();
|
||||
{permissions?.destination.delete && (
|
||||
<DialogAction
|
||||
title="Delete Destination"
|
||||
description="Are you sure you want to delete this destination?"
|
||||
type="destructive"
|
||||
onClick={async () => {
|
||||
await mutateAsync({
|
||||
destinationId: destination.destinationId,
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error deleting destination");
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="group hover:bg-red-500/10 "
|
||||
isLoading={isRemoving}
|
||||
.then(() => {
|
||||
toast.success(
|
||||
"Destination deleted successfully",
|
||||
);
|
||||
refetch();
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error(
|
||||
"Error deleting destination",
|
||||
);
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Trash2 className="size-4 text-primary group-hover:text-red-500" />
|
||||
</Button>
|
||||
</DialogAction>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="group hover:bg-red-500/10 "
|
||||
isLoading={isRemoving}
|
||||
>
|
||||
<Trash2 className="size-4 text-primary group-hover:text-red-500" />
|
||||
</Button>
|
||||
</DialogAction>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-row gap-2 flex-wrap w-full justify-end mr-4">
|
||||
<HandleDestinations />
|
||||
</div>
|
||||
{permissions?.destination.create && (
|
||||
<div className="flex flex-row gap-2 flex-wrap w-full justify-end mr-4">
|
||||
<HandleDestinations />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -737,6 +737,9 @@ export const HandleNotifications = ({ notificationId }: Props) => {
|
||||
});
|
||||
setVisible(false);
|
||||
await utils.notification.all.invalidate();
|
||||
if (notificationId) {
|
||||
await utils.notification.one.invalidate({ notificationId });
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error(
|
||||
|
||||
@@ -26,6 +26,7 @@ export const ShowNotifications = () => {
|
||||
const { data, isPending, refetch } = api.notification.all.useQuery();
|
||||
const { mutateAsync, isPending: isRemoving } =
|
||||
api.notification.remove.useMutation();
|
||||
const { data: permissions } = api.user.getPermissions.useQuery();
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
@@ -56,7 +57,9 @@ export const ShowNotifications = () => {
|
||||
To send notifications it is required to set at least 1
|
||||
provider.
|
||||
</span>
|
||||
<HandleNotifications />
|
||||
{permissions?.notification.create && (
|
||||
<HandleNotifications />
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col gap-4 min-h-[25vh]">
|
||||
@@ -126,45 +129,50 @@ export const ShowNotifications = () => {
|
||||
notificationId={notification.notificationId}
|
||||
/>
|
||||
|
||||
<DialogAction
|
||||
title="Delete Notification"
|
||||
description="Are you sure you want to delete this notification?"
|
||||
type="destructive"
|
||||
onClick={async () => {
|
||||
await mutateAsync({
|
||||
notificationId: notification.notificationId,
|
||||
})
|
||||
.then(() => {
|
||||
toast.success(
|
||||
"Notification deleted successfully",
|
||||
);
|
||||
refetch();
|
||||
{permissions?.notification.delete && (
|
||||
<DialogAction
|
||||
title="Delete Notification"
|
||||
description="Are you sure you want to delete this notification?"
|
||||
type="destructive"
|
||||
onClick={async () => {
|
||||
await mutateAsync({
|
||||
notificationId:
|
||||
notification.notificationId,
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error(
|
||||
"Error deleting notification",
|
||||
);
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="group hover:bg-red-500/10 "
|
||||
isLoading={isRemoving}
|
||||
.then(() => {
|
||||
toast.success(
|
||||
"Notification deleted successfully",
|
||||
);
|
||||
refetch();
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error(
|
||||
"Error deleting notification",
|
||||
);
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Trash2 className="size-4 text-primary group-hover:text-red-500" />
|
||||
</Button>
|
||||
</DialogAction>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="group hover:bg-red-500/10 "
|
||||
isLoading={isRemoving}
|
||||
>
|
||||
<Trash2 className="size-4 text-primary group-hover:text-red-500" />
|
||||
</Button>
|
||||
</DialogAction>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-row gap-2 flex-wrap w-full justify-end mr-4">
|
||||
<HandleNotifications />
|
||||
</div>
|
||||
{permissions?.notification.create && (
|
||||
<div className="flex flex-row gap-2 flex-wrap w-full justify-end mr-4">
|
||||
<HandleNotifications />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -59,6 +59,7 @@ export const ShowServers = () => {
|
||||
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||
const { data: canCreateMoreServers } =
|
||||
api.stripe.canCreateMoreServers.useQuery();
|
||||
const { data: permissions } = api.user.getPermissions.useQuery();
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
@@ -115,7 +116,7 @@ export const ShowServers = () => {
|
||||
Start adding servers to deploy your applications
|
||||
remotely.
|
||||
</span>
|
||||
<HandleServers />
|
||||
{permissions?.server.create && <HandleServers />}
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col gap-4 min-h-[25vh]">
|
||||
@@ -362,66 +363,71 @@ export const ShowServers = () => {
|
||||
|
||||
<div className="flex-1" />
|
||||
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div>
|
||||
<DialogAction
|
||||
disabled={!canDelete}
|
||||
title={
|
||||
canDelete
|
||||
? "Delete Server"
|
||||
: "Server has active services"
|
||||
}
|
||||
description={
|
||||
canDelete ? (
|
||||
"This will delete the server and all associated data"
|
||||
) : (
|
||||
<div className="flex flex-col gap-2">
|
||||
You can not delete this
|
||||
server because it has
|
||||
active services.
|
||||
<AlertBlock type="warning">
|
||||
You have active services
|
||||
associated with this
|
||||
server, please delete
|
||||
them first.
|
||||
</AlertBlock>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
onClick={async () => {
|
||||
await mutateAsync({
|
||||
serverId: server.serverId,
|
||||
})
|
||||
.then(() => {
|
||||
refetch();
|
||||
toast.success(
|
||||
`Server ${server.name} deleted successfully`,
|
||||
);
|
||||
{permissions?.server.delete && (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div>
|
||||
<DialogAction
|
||||
disabled={!canDelete}
|
||||
title={
|
||||
canDelete
|
||||
? "Delete Server"
|
||||
: "Server has active services"
|
||||
}
|
||||
description={
|
||||
canDelete ? (
|
||||
"This will delete the server and all associated data"
|
||||
) : (
|
||||
<div className="flex flex-col gap-2">
|
||||
You can not delete this
|
||||
server because it has
|
||||
active services.
|
||||
<AlertBlock type="warning">
|
||||
You have active
|
||||
services associated
|
||||
with this server,
|
||||
please delete them
|
||||
first.
|
||||
</AlertBlock>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
onClick={async () => {
|
||||
await mutateAsync({
|
||||
serverId: server.serverId,
|
||||
})
|
||||
.catch((err) => {
|
||||
toast.error(err.message);
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className={`h-9 w-9 ${canDelete ? "text-destructive hover:text-destructive hover:bg-destructive/10" : "text-muted-foreground hover:bg-muted"}`}
|
||||
.then(() => {
|
||||
refetch();
|
||||
toast.success(
|
||||
`Server ${server.name} deleted successfully`,
|
||||
);
|
||||
})
|
||||
.catch((err) => {
|
||||
toast.error(
|
||||
err.message,
|
||||
);
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
</DialogAction>
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>
|
||||
{canDelete
|
||||
? "Delete Server"
|
||||
: "Cannot delete - has active services"}
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className={`h-9 w-9 ${canDelete ? "text-destructive hover:text-destructive hover:bg-destructive/10" : "text-muted-foreground hover:bg-muted"}`}
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
</DialogAction>
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>
|
||||
{canDelete
|
||||
? "Delete Server"
|
||||
: "Cannot delete - has active services"}
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
)}
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
)}
|
||||
@@ -431,13 +437,15 @@ export const ShowServers = () => {
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-row gap-2 flex-wrap w-full justify-end mt-4">
|
||||
{data && data?.length > 0 && (
|
||||
<div>
|
||||
<HandleServers />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{permissions?.server.create && (
|
||||
<div className="flex flex-row gap-2 flex-wrap w-full justify-end mt-4">
|
||||
{data && data?.length > 0 && (
|
||||
<div>
|
||||
<HandleServers />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -17,6 +17,7 @@ export const ShowDestinations = () => {
|
||||
const { data, isPending, refetch } = api.sshKey.all.useQuery();
|
||||
const { mutateAsync, isPending: isRemoving } =
|
||||
api.sshKey.remove.useMutation();
|
||||
const { data: permissions } = api.user.getPermissions.useQuery();
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
@@ -46,7 +47,7 @@ export const ShowDestinations = () => {
|
||||
<span className="text-base text-muted-foreground text-center">
|
||||
You don't have any SSH keys
|
||||
</span>
|
||||
<HandleSSHKeys />
|
||||
{permissions?.sshKeys.create && <HandleSSHKeys />}
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col gap-4 min-h-[25vh]">
|
||||
@@ -84,43 +85,47 @@ export const ShowDestinations = () => {
|
||||
<div className="flex flex-row gap-1">
|
||||
<HandleSSHKeys sshKeyId={sshKey.sshKeyId} />
|
||||
|
||||
<DialogAction
|
||||
title="Delete SSH Key"
|
||||
description="Are you sure you want to delete this SSH Key?"
|
||||
type="destructive"
|
||||
onClick={async () => {
|
||||
await mutateAsync({
|
||||
sshKeyId: sshKey.sshKeyId,
|
||||
})
|
||||
.then(() => {
|
||||
toast.success(
|
||||
"SSH Key deleted successfully",
|
||||
);
|
||||
refetch();
|
||||
{permissions?.sshKeys.delete && (
|
||||
<DialogAction
|
||||
title="Delete SSH Key"
|
||||
description="Are you sure you want to delete this SSH Key?"
|
||||
type="destructive"
|
||||
onClick={async () => {
|
||||
await mutateAsync({
|
||||
sshKeyId: sshKey.sshKeyId,
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error deleting SSH Key");
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="group hover:bg-red-500/10 "
|
||||
isLoading={isRemoving}
|
||||
.then(() => {
|
||||
toast.success(
|
||||
"SSH Key deleted successfully",
|
||||
);
|
||||
refetch();
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error deleting SSH Key");
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Trash2 className="size-4 text-primary group-hover:text-red-500" />
|
||||
</Button>
|
||||
</DialogAction>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="group hover:bg-red-500/10 "
|
||||
isLoading={isRemoving}
|
||||
>
|
||||
<Trash2 className="size-4 text-primary group-hover:text-red-500" />
|
||||
</Button>
|
||||
</DialogAction>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-row gap-2 flex-wrap w-full justify-end mr-4">
|
||||
<HandleSSHKeys />
|
||||
</div>
|
||||
{permissions?.sshKeys.create && (
|
||||
<div className="flex flex-row gap-2 flex-wrap w-full justify-end mr-4">
|
||||
<HandleSSHKeys />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -32,7 +32,6 @@ import {
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { authClient } from "@/lib/auth-client";
|
||||
import { api } from "@/utils/api";
|
||||
|
||||
const addInvitation = z.object({
|
||||
@@ -40,7 +39,7 @@ const addInvitation = z.object({
|
||||
.string()
|
||||
.min(1, "Email is required")
|
||||
.email({ message: "Invalid email" }),
|
||||
role: z.enum(["member", "admin"]),
|
||||
role: z.string().min(1, "Role is required"),
|
||||
notificationId: z.string().optional(),
|
||||
});
|
||||
|
||||
@@ -49,13 +48,14 @@ type AddInvitation = z.infer<typeof addInvitation>;
|
||||
export const AddInvitation = () => {
|
||||
const [open, setOpen] = useState(false);
|
||||
const utils = api.useUtils();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||
const { data: emailProviders } =
|
||||
api.notification.getEmailProviders.useQuery();
|
||||
const { mutateAsync: inviteMember, isPending: isInviting } =
|
||||
api.organization.inviteMember.useMutation();
|
||||
const { mutateAsync: sendInvitation } = api.user.sendInvitation.useMutation();
|
||||
const { data: customRoles } = api.customRole.all.useQuery();
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const { data: activeOrganization } = api.organization.active.useQuery();
|
||||
|
||||
const form = useForm<AddInvitation>({
|
||||
defaultValues: {
|
||||
@@ -70,19 +70,15 @@ export const AddInvitation = () => {
|
||||
}, [form, form.formState.isSubmitSuccessful, form.reset]);
|
||||
|
||||
const onSubmit = async (data: AddInvitation) => {
|
||||
setIsLoading(true);
|
||||
const result = await authClient.organization.inviteMember({
|
||||
email: data.email.toLowerCase(),
|
||||
role: data.role,
|
||||
organizationId: activeOrganization?.id,
|
||||
});
|
||||
try {
|
||||
const result = await inviteMember({
|
||||
email: data.email.toLowerCase(),
|
||||
role: data.role,
|
||||
});
|
||||
|
||||
if (result.error) {
|
||||
setError(result.error.message || "");
|
||||
} else {
|
||||
if (!isCloud && data.notificationId) {
|
||||
await sendInvitation({
|
||||
invitationId: result.data.id,
|
||||
invitationId: result!.id,
|
||||
notificationId: data.notificationId || "",
|
||||
})
|
||||
.then(() => {
|
||||
@@ -96,10 +92,11 @@ export const AddInvitation = () => {
|
||||
}
|
||||
setError(null);
|
||||
setOpen(false);
|
||||
} catch (error: any) {
|
||||
setError(error.message || "Failed to create invitation");
|
||||
}
|
||||
|
||||
utils.organization.allInvitations.invalidate();
|
||||
setIsLoading(false);
|
||||
};
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
@@ -159,6 +156,11 @@ export const AddInvitation = () => {
|
||||
<SelectContent>
|
||||
<SelectItem value="member">Member</SelectItem>
|
||||
<SelectItem value="admin">Admin</SelectItem>
|
||||
{customRoles?.map((role) => (
|
||||
<SelectItem key={role.role} value={role.role}>
|
||||
{role.role}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormDescription>
|
||||
@@ -212,7 +214,7 @@ export const AddInvitation = () => {
|
||||
)}
|
||||
<DialogFooter className="flex w-full flex-row">
|
||||
<Button
|
||||
isLoading={isLoading}
|
||||
isLoading={isInviting}
|
||||
form="hook-form-add-invitation"
|
||||
type="submit"
|
||||
>
|
||||
|
||||
@@ -173,9 +173,11 @@ type AddPermissions = z.infer<typeof addPermissions>;
|
||||
|
||||
interface Props {
|
||||
userId: string;
|
||||
role?: string;
|
||||
}
|
||||
|
||||
export const AddUserPermissions = ({ userId }: Props) => {
|
||||
export const AddUserPermissions = ({ userId, role }: Props) => {
|
||||
const isCustomRole = !!role && !["owner", "admin", "member"].includes(role);
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const { data: projects } = api.project.allForPermissions.useQuery(undefined, {
|
||||
enabled: isOpen,
|
||||
@@ -284,226 +286,237 @@ export const AddUserPermissions = ({ userId }: Props) => {
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="grid grid-cols-1 md:grid-cols-2 w-full gap-4"
|
||||
>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="canCreateProjects"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm">
|
||||
<div className="space-y-0.5">
|
||||
<FormLabel>Create Projects</FormLabel>
|
||||
<FormDescription>
|
||||
Allow the user to create projects
|
||||
</FormDescription>
|
||||
</div>
|
||||
<FormControl>
|
||||
<Switch
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="canDeleteProjects"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm">
|
||||
<div className="space-y-0.5">
|
||||
<FormLabel>Delete Projects</FormLabel>
|
||||
<FormDescription>
|
||||
Allow the user to delete projects
|
||||
</FormDescription>
|
||||
</div>
|
||||
<FormControl>
|
||||
<Switch
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="canCreateServices"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm">
|
||||
<div className="space-y-0.5">
|
||||
<FormLabel>Create Services</FormLabel>
|
||||
<FormDescription>
|
||||
Allow the user to create services
|
||||
</FormDescription>
|
||||
</div>
|
||||
<FormControl>
|
||||
<Switch
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="canDeleteServices"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm">
|
||||
<div className="space-y-0.5">
|
||||
<FormLabel>Delete Services</FormLabel>
|
||||
<FormDescription>
|
||||
Allow the user to delete services
|
||||
</FormDescription>
|
||||
</div>
|
||||
<FormControl>
|
||||
<Switch
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="canCreateEnvironments"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm">
|
||||
<div className="space-y-0.5">
|
||||
<FormLabel>Create Environments</FormLabel>
|
||||
<FormDescription>
|
||||
Allow the user to create environments
|
||||
</FormDescription>
|
||||
</div>
|
||||
<FormControl>
|
||||
<Switch
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="canDeleteEnvironments"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm">
|
||||
<div className="space-y-0.5">
|
||||
<FormLabel>Delete Environments</FormLabel>
|
||||
<FormDescription>
|
||||
Allow the user to delete environments
|
||||
</FormDescription>
|
||||
</div>
|
||||
<FormControl>
|
||||
<Switch
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="canAccessToTraefikFiles"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm">
|
||||
<div className="space-y-0.5">
|
||||
<FormLabel>Access to Traefik Files</FormLabel>
|
||||
<FormDescription>
|
||||
Allow the user to access to the Traefik Tab Files
|
||||
</FormDescription>
|
||||
</div>
|
||||
<FormControl>
|
||||
<Switch
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="canAccessToDocker"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm">
|
||||
<div className="space-y-0.5">
|
||||
<FormLabel>Access to Docker</FormLabel>
|
||||
<FormDescription>
|
||||
Allow the user to access to the Docker Tab
|
||||
</FormDescription>
|
||||
</div>
|
||||
<FormControl>
|
||||
<Switch
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="canAccessToAPI"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm">
|
||||
<div className="space-y-0.5">
|
||||
<FormLabel>Access to API/CLI</FormLabel>
|
||||
<FormDescription>
|
||||
Allow the user to access to the API/CLI
|
||||
</FormDescription>
|
||||
</div>
|
||||
<FormControl>
|
||||
<Switch
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="canAccessToSSHKeys"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm">
|
||||
<div className="space-y-0.5">
|
||||
<FormLabel>Access to SSH Keys</FormLabel>
|
||||
<FormDescription>
|
||||
Allow to users to access to the SSH Keys section
|
||||
</FormDescription>
|
||||
</div>
|
||||
<FormControl>
|
||||
<Switch
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="canAccessToGitProviders"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm">
|
||||
<div className="space-y-0.5">
|
||||
<FormLabel>Access to Git Providers</FormLabel>
|
||||
<FormDescription>
|
||||
Allow to users to access to the Git Providers section
|
||||
</FormDescription>
|
||||
</div>
|
||||
<FormControl>
|
||||
<Switch
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
{isCustomRole && (
|
||||
<div className="md:col-span-2 rounded-lg border p-3 bg-muted/50 text-sm text-muted-foreground">
|
||||
This user has a custom role assigned. Capabilities are defined
|
||||
by the role. You can still manage which projects, environments,
|
||||
and services they can access below.
|
||||
</div>
|
||||
)}
|
||||
{!isCustomRole && (
|
||||
<>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="canCreateProjects"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm">
|
||||
<div className="space-y-0.5">
|
||||
<FormLabel>Create Projects</FormLabel>
|
||||
<FormDescription>
|
||||
Allow the user to create projects
|
||||
</FormDescription>
|
||||
</div>
|
||||
<FormControl>
|
||||
<Switch
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="canDeleteProjects"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm">
|
||||
<div className="space-y-0.5">
|
||||
<FormLabel>Delete Projects</FormLabel>
|
||||
<FormDescription>
|
||||
Allow the user to delete projects
|
||||
</FormDescription>
|
||||
</div>
|
||||
<FormControl>
|
||||
<Switch
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="canCreateServices"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm">
|
||||
<div className="space-y-0.5">
|
||||
<FormLabel>Create Services</FormLabel>
|
||||
<FormDescription>
|
||||
Allow the user to create services
|
||||
</FormDescription>
|
||||
</div>
|
||||
<FormControl>
|
||||
<Switch
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="canDeleteServices"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm">
|
||||
<div className="space-y-0.5">
|
||||
<FormLabel>Delete Services</FormLabel>
|
||||
<FormDescription>
|
||||
Allow the user to delete services
|
||||
</FormDescription>
|
||||
</div>
|
||||
<FormControl>
|
||||
<Switch
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="canCreateEnvironments"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm">
|
||||
<div className="space-y-0.5">
|
||||
<FormLabel>Create Environments</FormLabel>
|
||||
<FormDescription>
|
||||
Allow the user to create environments
|
||||
</FormDescription>
|
||||
</div>
|
||||
<FormControl>
|
||||
<Switch
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="canDeleteEnvironments"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm">
|
||||
<div className="space-y-0.5">
|
||||
<FormLabel>Delete Environments</FormLabel>
|
||||
<FormDescription>
|
||||
Allow the user to delete environments
|
||||
</FormDescription>
|
||||
</div>
|
||||
<FormControl>
|
||||
<Switch
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="canAccessToTraefikFiles"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm">
|
||||
<div className="space-y-0.5">
|
||||
<FormLabel>Access to Traefik Files</FormLabel>
|
||||
<FormDescription>
|
||||
Allow the user to access to the Traefik Tab Files
|
||||
</FormDescription>
|
||||
</div>
|
||||
<FormControl>
|
||||
<Switch
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="canAccessToDocker"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm">
|
||||
<div className="space-y-0.5">
|
||||
<FormLabel>Access to Docker</FormLabel>
|
||||
<FormDescription>
|
||||
Allow the user to access to the Docker Tab
|
||||
</FormDescription>
|
||||
</div>
|
||||
<FormControl>
|
||||
<Switch
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="canAccessToAPI"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm">
|
||||
<div className="space-y-0.5">
|
||||
<FormLabel>Access to API/CLI</FormLabel>
|
||||
<FormDescription>
|
||||
Allow the user to access to the API/CLI
|
||||
</FormDescription>
|
||||
</div>
|
||||
<FormControl>
|
||||
<Switch
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="canAccessToSSHKeys"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm">
|
||||
<div className="space-y-0.5">
|
||||
<FormLabel>Access to SSH Keys</FormLabel>
|
||||
<FormDescription>
|
||||
Allow to users to access to the SSH Keys section
|
||||
</FormDescription>
|
||||
</div>
|
||||
<FormControl>
|
||||
<Switch
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="canAccessToGitProviders"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm">
|
||||
<div className="space-y-0.5">
|
||||
<FormLabel>Access to Git Providers</FormLabel>
|
||||
<FormDescription>
|
||||
Allow to users to access to the Git Providers section
|
||||
</FormDescription>
|
||||
</div>
|
||||
<FormControl>
|
||||
<Switch
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="accessedProjects"
|
||||
|
||||
@@ -34,14 +34,14 @@ import {
|
||||
import { api } from "@/utils/api";
|
||||
|
||||
const changeRoleSchema = z.object({
|
||||
role: z.enum(["admin", "member"]),
|
||||
role: z.string().min(1),
|
||||
});
|
||||
|
||||
type ChangeRoleSchema = z.infer<typeof changeRoleSchema>;
|
||||
|
||||
interface Props {
|
||||
memberId: string;
|
||||
currentRole: "admin" | "member";
|
||||
currentRole: string;
|
||||
userEmail: string;
|
||||
}
|
||||
|
||||
@@ -49,6 +49,10 @@ export const ChangeRole = ({ memberId, currentRole, userEmail }: Props) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const utils = api.useUtils();
|
||||
|
||||
const { data: customRoles } = api.customRole.all.useQuery(undefined, {
|
||||
enabled: isOpen,
|
||||
});
|
||||
|
||||
const { mutateAsync, isError, error, isPending } =
|
||||
api.organization.updateMemberRole.useMutation();
|
||||
|
||||
@@ -125,6 +129,14 @@ export const ChangeRole = ({ memberId, currentRole, userEmail }: Props) => {
|
||||
<SelectContent>
|
||||
<SelectItem value="admin">Admin</SelectItem>
|
||||
<SelectItem value="member">Member</SelectItem>
|
||||
{customRoles?.map((customRole) => (
|
||||
<SelectItem
|
||||
key={customRole.role}
|
||||
value={customRole.role}
|
||||
>
|
||||
{customRole.role}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormDescription>
|
||||
@@ -132,6 +144,13 @@ export const ChangeRole = ({ memberId, currentRole, userEmail }: Props) => {
|
||||
<br />
|
||||
<strong>Member:</strong> Limited permissions, can be
|
||||
customized.
|
||||
{customRoles && customRoles.length > 0 && (
|
||||
<>
|
||||
<br />
|
||||
<strong>Custom roles:</strong> Enterprise-defined
|
||||
permissions.
|
||||
</>
|
||||
)}
|
||||
<br />
|
||||
<em className="text-muted-foreground text-xs">
|
||||
Note: Owner role is intransferible.
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { format } from "date-fns";
|
||||
import { Loader2, MoreHorizontal, Users } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
import { AlertBlock } from "@/components/shared/alert-block";
|
||||
import { DialogAction } from "@/components/shared/dialog-action";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
@@ -35,10 +36,20 @@ export const ShowUsers = () => {
|
||||
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||
const { data, isPending, refetch } = api.user.all.useQuery();
|
||||
const { mutateAsync } = api.user.remove.useMutation();
|
||||
const { data: permissions } = api.user.getPermissions.useQuery();
|
||||
const { data: hasValidLicense } =
|
||||
api.licenseKey.haveValidLicenseKey.useQuery();
|
||||
|
||||
const utils = api.useUtils();
|
||||
const { data: session } = api.user.session.useQuery();
|
||||
|
||||
const FREE_ROLES = ["owner", "admin", "member"];
|
||||
const membersWithCustomRoles = data?.filter(
|
||||
(member) => !FREE_ROLES.includes(member.role),
|
||||
);
|
||||
const hasCustomRolesWithoutLicense =
|
||||
!hasValidLicense && (membersWithCustomRoles?.length ?? 0) > 0;
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
<Card className="h-full bg-sidebar p-2.5 rounded-xl max-w-5xl mx-auto">
|
||||
@@ -69,6 +80,18 @@ export const ShowUsers = () => {
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col gap-4 min-h-[25vh]">
|
||||
{hasCustomRolesWithoutLicense && (
|
||||
<AlertBlock type="warning">
|
||||
You have{" "}
|
||||
{membersWithCustomRoles?.length === 1
|
||||
? "1 user"
|
||||
: `${membersWithCustomRoles?.length} users`}{" "}
|
||||
assigned to custom roles. Custom roles will not work
|
||||
without a valid Enterprise license. Please activate your
|
||||
license or change these users to a free role (Admin or
|
||||
Member).
|
||||
</AlertBlock>
|
||||
)}
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
@@ -89,40 +112,39 @@ export const ShowUsers = () => {
|
||||
)?.role;
|
||||
|
||||
// Owner never has "Edit Permissions" (they're absolute owner)
|
||||
// Other users can edit permissions if target is not themselves and target is a member
|
||||
// Other users can edit permissions if target is not themselves and target is a member/custom role
|
||||
const isStaticAdminOrOwner =
|
||||
member.role === "owner" || member.role === "admin";
|
||||
const canEditPermissions =
|
||||
member.role !== "owner" &&
|
||||
member.role === "member" &&
|
||||
!isStaticAdminOrOwner &&
|
||||
member.user.id !== session?.user?.id;
|
||||
|
||||
// Can change role based on hierarchy:
|
||||
// - Owner: Can change anyone's role (except themselves and other owners)
|
||||
// - Admin: Can only change member roles (not other admins or owners)
|
||||
// - Admin: Can only change member/custom roles (not other admins or owners)
|
||||
// - Owner role is intransferible
|
||||
const canChangeRole =
|
||||
member.role !== "owner" &&
|
||||
member.user.id !== session?.user?.id &&
|
||||
(currentUserRole === "owner" ||
|
||||
(currentUserRole === "admin" &&
|
||||
member.role === "member"));
|
||||
member.role !== "admin"));
|
||||
|
||||
// Delete/Unlink follow same hierarchy as role changes
|
||||
// - Owner: Can delete/unlink anyone (except themselves and owner can't be deleted)
|
||||
// - Admin: Can only delete/unlink members (not other admins or owner)
|
||||
const canDelete =
|
||||
member.role !== "owner" &&
|
||||
!isCloud &&
|
||||
member.user.id !== session?.user?.id &&
|
||||
(currentUserRole === "owner" ||
|
||||
(currentUserRole === "admin" &&
|
||||
member.role === "member"));
|
||||
const canDeleteMember =
|
||||
permissions?.member.delete ?? false;
|
||||
|
||||
const canUnlink =
|
||||
// Self-hosted: "Delete User" removes the user entirely
|
||||
// Cloud: "Unlink User" removes from the organization only
|
||||
const canRemove =
|
||||
member.role !== "owner" &&
|
||||
member.user.id !== session?.user?.id &&
|
||||
(currentUserRole === "owner" ||
|
||||
(currentUserRole === "admin" &&
|
||||
member.role === "member"));
|
||||
member.role !== "admin") ||
|
||||
(canDeleteMember && !isStaticAdminOrOwner));
|
||||
|
||||
const canDelete = canRemove && !isCloud;
|
||||
const canUnlink = canRemove && !!isCloud;
|
||||
|
||||
const hasAnyAction =
|
||||
canEditPermissions ||
|
||||
@@ -134,6 +156,11 @@ export const ShowUsers = () => {
|
||||
<TableRow key={member.id}>
|
||||
<TableCell className="w-[100px]">
|
||||
{member.user.email}
|
||||
{member.user.id === session?.user?.id && (
|
||||
<span className="text-muted-foreground ml-1">
|
||||
(You)
|
||||
</span>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell className="text-center">
|
||||
<Badge
|
||||
@@ -179,9 +206,7 @@ export const ShowUsers = () => {
|
||||
{canChangeRole && (
|
||||
<ChangeRole
|
||||
memberId={member.id}
|
||||
currentRole={
|
||||
member.role as "admin" | "member"
|
||||
}
|
||||
currentRole={member.role}
|
||||
userEmail={member.user.email}
|
||||
/>
|
||||
)}
|
||||
@@ -189,6 +214,7 @@ export const ShowUsers = () => {
|
||||
{canEditPermissions && (
|
||||
<AddUserPermissions
|
||||
userId={member.user.id}
|
||||
role={member.role}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
ChevronRight,
|
||||
ChevronsUpDown,
|
||||
CircleHelp,
|
||||
ClipboardList,
|
||||
Clock,
|
||||
CreditCard,
|
||||
Database,
|
||||
@@ -92,13 +93,21 @@ import { UserNav } from "./user-nav";
|
||||
|
||||
// The types of the queries we are going to use
|
||||
type AuthQueryOutput = inferRouterOutputs<AppRouter>["user"]["get"];
|
||||
type PermissionsOutput =
|
||||
inferRouterOutputs<AppRouter>["user"]["getPermissions"];
|
||||
|
||||
type EnabledOpts = {
|
||||
auth?: AuthQueryOutput;
|
||||
permissions?: PermissionsOutput;
|
||||
isCloud: boolean;
|
||||
};
|
||||
|
||||
type SingleNavItem = {
|
||||
isSingle?: true;
|
||||
title: string;
|
||||
url: string;
|
||||
icon?: LucideIcon;
|
||||
isEnabled?: (opts: { auth?: AuthQueryOutput; isCloud: boolean }) => boolean;
|
||||
isEnabled?: (opts: EnabledOpts) => boolean;
|
||||
};
|
||||
|
||||
// NavItem type
|
||||
@@ -112,10 +121,7 @@ type NavItem =
|
||||
title: string;
|
||||
icon: LucideIcon;
|
||||
items: SingleNavItem[];
|
||||
isEnabled?: (opts: {
|
||||
auth?: AuthQueryOutput;
|
||||
isCloud: boolean;
|
||||
}) => boolean;
|
||||
isEnabled?: (opts: EnabledOpts) => boolean;
|
||||
};
|
||||
|
||||
// ExternalLink type
|
||||
@@ -124,7 +130,7 @@ type ExternalLink = {
|
||||
name: string;
|
||||
url: string;
|
||||
icon: React.ComponentType<{ className?: string }>;
|
||||
isEnabled?: (opts: { auth?: AuthQueryOutput; isCloud: boolean }) => boolean;
|
||||
isEnabled?: (opts: EnabledOpts) => boolean;
|
||||
};
|
||||
|
||||
// Menu type
|
||||
@@ -152,14 +158,16 @@ const MENU: Menu = {
|
||||
title: "Deployments",
|
||||
url: "/dashboard/deployments",
|
||||
icon: Rocket,
|
||||
isEnabled: ({ permissions }) => !!permissions?.deployment.read,
|
||||
},
|
||||
{
|
||||
isSingle: true,
|
||||
title: "Monitoring",
|
||||
url: "/dashboard/monitoring",
|
||||
icon: BarChartHorizontalBigIcon,
|
||||
// Only enabled in non-cloud environments
|
||||
isEnabled: ({ isCloud }) => !isCloud,
|
||||
// Only enabled in non-cloud environments and if user has monitoring.read
|
||||
isEnabled: ({ isCloud, permissions }) =>
|
||||
!isCloud && !!permissions?.monitoring.read,
|
||||
},
|
||||
{
|
||||
isSingle: true,
|
||||
@@ -167,64 +175,44 @@ const MENU: Menu = {
|
||||
url: "/dashboard/schedules",
|
||||
icon: Clock,
|
||||
// Only enabled in non-cloud environments
|
||||
isEnabled: ({ isCloud, auth }) =>
|
||||
!isCloud && (auth?.role === "owner" || auth?.role === "admin"),
|
||||
isEnabled: ({ isCloud, permissions }) =>
|
||||
!isCloud && !!permissions?.organization.update,
|
||||
},
|
||||
{
|
||||
isSingle: true,
|
||||
title: "Traefik File System",
|
||||
url: "/dashboard/traefik",
|
||||
icon: GalleryVerticalEnd,
|
||||
// Only enabled for admins and users with access to Traefik files in non-cloud environments
|
||||
isEnabled: ({ auth, isCloud }) =>
|
||||
!!(
|
||||
(auth?.role === "owner" ||
|
||||
auth?.role === "admin" ||
|
||||
auth?.canAccessToTraefikFiles) &&
|
||||
!isCloud
|
||||
),
|
||||
// Only enabled for users with access to Traefik files in non-cloud environments
|
||||
isEnabled: ({ permissions, isCloud }) =>
|
||||
!!(permissions?.traefikFiles.read && !isCloud),
|
||||
},
|
||||
{
|
||||
isSingle: true,
|
||||
title: "Docker",
|
||||
url: "/dashboard/docker",
|
||||
icon: BlocksIcon,
|
||||
// Only enabled for admins and users with access to Docker in non-cloud environments
|
||||
isEnabled: ({ auth, isCloud }) =>
|
||||
!!(
|
||||
(auth?.role === "owner" ||
|
||||
auth?.role === "admin" ||
|
||||
auth?.canAccessToDocker) &&
|
||||
!isCloud
|
||||
),
|
||||
// Only enabled for users with access to Docker in non-cloud environments
|
||||
isEnabled: ({ permissions, isCloud }) =>
|
||||
!!(permissions?.docker.read && !isCloud),
|
||||
},
|
||||
{
|
||||
isSingle: true,
|
||||
title: "Swarm",
|
||||
url: "/dashboard/swarm",
|
||||
icon: PieChart,
|
||||
// Only enabled for admins and users with access to Docker in non-cloud environments
|
||||
isEnabled: ({ auth, isCloud }) =>
|
||||
!!(
|
||||
(auth?.role === "owner" ||
|
||||
auth?.role === "admin" ||
|
||||
auth?.canAccessToDocker) &&
|
||||
!isCloud
|
||||
),
|
||||
// Only enabled for users with access to Docker in non-cloud environments
|
||||
isEnabled: ({ permissions, isCloud }) =>
|
||||
!!(permissions?.docker.read && !isCloud),
|
||||
},
|
||||
{
|
||||
isSingle: true,
|
||||
title: "Requests",
|
||||
url: "/dashboard/requests",
|
||||
icon: Forward,
|
||||
// Only enabled for admins and users with access to Docker in non-cloud environments
|
||||
isEnabled: ({ auth, isCloud }) =>
|
||||
!!(
|
||||
(auth?.role === "owner" ||
|
||||
auth?.role === "admin" ||
|
||||
auth?.canAccessToDocker) &&
|
||||
!isCloud
|
||||
),
|
||||
// Only enabled for users with access to Docker in non-cloud environments
|
||||
isEnabled: ({ permissions, isCloud }) =>
|
||||
!!(permissions?.docker.read && !isCloud),
|
||||
},
|
||||
|
||||
// Legacy unused menu, adjusted to the new structure
|
||||
@@ -291,8 +279,8 @@ const MENU: Menu = {
|
||||
url: "/dashboard/settings/server",
|
||||
icon: Activity,
|
||||
// Only enabled for admins in non-cloud environments
|
||||
isEnabled: ({ auth, isCloud }) =>
|
||||
!!((auth?.role === "owner" || auth?.role === "admin") && !isCloud),
|
||||
isEnabled: ({ permissions, isCloud }) =>
|
||||
!!(permissions?.organization.update && !isCloud),
|
||||
},
|
||||
{
|
||||
isSingle: true,
|
||||
@@ -305,70 +293,59 @@ const MENU: Menu = {
|
||||
title: "Remote Servers",
|
||||
url: "/dashboard/settings/servers",
|
||||
icon: Server,
|
||||
// Only enabled for admins
|
||||
isEnabled: ({ auth }) =>
|
||||
!!(auth?.role === "owner" || auth?.role === "admin"),
|
||||
isEnabled: ({ permissions }) => !!permissions?.server.read,
|
||||
},
|
||||
{
|
||||
isSingle: true,
|
||||
title: "Users",
|
||||
icon: Users,
|
||||
url: "/dashboard/settings/users",
|
||||
// Only enabled for admins
|
||||
isEnabled: ({ auth }) =>
|
||||
!!(auth?.role === "owner" || auth?.role === "admin"),
|
||||
// Only enabled for users with member.read permission
|
||||
isEnabled: ({ permissions }) => !!permissions?.member.read,
|
||||
},
|
||||
{
|
||||
isSingle: true,
|
||||
title: "Audit Logs",
|
||||
icon: ClipboardList,
|
||||
url: "/dashboard/settings/audit-logs",
|
||||
isEnabled: ({ permissions }) => !!permissions?.auditLog.read,
|
||||
},
|
||||
{
|
||||
isSingle: true,
|
||||
title: "SSH Keys",
|
||||
icon: KeyRound,
|
||||
url: "/dashboard/settings/ssh-keys",
|
||||
// Only enabled for admins and users with access to SSH keys
|
||||
isEnabled: ({ auth }) =>
|
||||
!!(
|
||||
auth?.role === "owner" ||
|
||||
auth?.canAccessToSSHKeys ||
|
||||
auth?.role === "admin"
|
||||
),
|
||||
// Only enabled for users with access to SSH keys
|
||||
isEnabled: ({ permissions }) => !!permissions?.sshKeys.read,
|
||||
},
|
||||
{
|
||||
title: "AI",
|
||||
icon: BotIcon,
|
||||
url: "/dashboard/settings/ai",
|
||||
isSingle: true,
|
||||
isEnabled: ({ auth }) =>
|
||||
!!(auth?.role === "owner" || auth?.role === "admin"),
|
||||
isEnabled: ({ permissions }) => !!permissions?.organization.update,
|
||||
},
|
||||
{
|
||||
isSingle: true,
|
||||
title: "Git",
|
||||
url: "/dashboard/settings/git-providers",
|
||||
icon: GitBranch,
|
||||
// Only enabled for admins and users with access to Git providers
|
||||
isEnabled: ({ auth }) =>
|
||||
!!(
|
||||
auth?.role === "owner" ||
|
||||
auth?.canAccessToGitProviders ||
|
||||
auth?.role === "admin"
|
||||
),
|
||||
// Only enabled for users with access to Git providers
|
||||
isEnabled: ({ permissions }) => !!permissions?.gitProviders.read,
|
||||
},
|
||||
{
|
||||
isSingle: true,
|
||||
title: "Registry",
|
||||
url: "/dashboard/settings/registry",
|
||||
icon: Package,
|
||||
// Only enabled for admins
|
||||
isEnabled: ({ auth }) =>
|
||||
!!(auth?.role === "owner" || auth?.role === "admin"),
|
||||
isEnabled: ({ permissions }) => !!permissions?.registry.read,
|
||||
},
|
||||
{
|
||||
isSingle: true,
|
||||
title: "S3 Destinations",
|
||||
url: "/dashboard/settings/destinations",
|
||||
icon: Database,
|
||||
// Only enabled for admins
|
||||
isEnabled: ({ auth }) =>
|
||||
!!(auth?.role === "owner" || auth?.role === "admin"),
|
||||
isEnabled: ({ permissions }) => !!permissions?.destination.read,
|
||||
},
|
||||
|
||||
{
|
||||
@@ -376,9 +353,7 @@ const MENU: Menu = {
|
||||
title: "Certificates",
|
||||
url: "/dashboard/settings/certificates",
|
||||
icon: ShieldCheck,
|
||||
// Only enabled for admins
|
||||
isEnabled: ({ auth }) =>
|
||||
!!(auth?.role === "owner" || auth?.role === "admin"),
|
||||
isEnabled: ({ permissions }) => !!permissions?.certificate.read,
|
||||
},
|
||||
{
|
||||
isSingle: true,
|
||||
@@ -386,24 +361,23 @@ const MENU: Menu = {
|
||||
url: "/dashboard/settings/cluster",
|
||||
icon: Boxes,
|
||||
// Only enabled for admins in non-cloud environments
|
||||
isEnabled: ({ auth, isCloud }) =>
|
||||
!!((auth?.role === "owner" || auth?.role === "admin") && !isCloud),
|
||||
isEnabled: ({ permissions, isCloud }) =>
|
||||
!!(permissions?.organization.update && !isCloud),
|
||||
},
|
||||
{
|
||||
isSingle: true,
|
||||
title: "Notifications",
|
||||
url: "/dashboard/settings/notifications",
|
||||
icon: Bell,
|
||||
// Only enabled for admins
|
||||
isEnabled: ({ auth }) =>
|
||||
!!(auth?.role === "owner" || auth?.role === "admin"),
|
||||
// Only enabled for users with access to notifications
|
||||
isEnabled: ({ permissions }) => !!permissions?.notification.read,
|
||||
},
|
||||
{
|
||||
isSingle: true,
|
||||
title: "Billing",
|
||||
url: "/dashboard/settings/billing",
|
||||
icon: CreditCard,
|
||||
// Only enabled for admins in cloud environments
|
||||
// Only enabled for owners in cloud environments
|
||||
isEnabled: ({ auth, isCloud }) => !!(auth?.role === "owner" && isCloud),
|
||||
},
|
||||
{
|
||||
@@ -411,7 +385,7 @@ const MENU: Menu = {
|
||||
title: "License",
|
||||
url: "/dashboard/settings/license",
|
||||
icon: Key,
|
||||
// Only enabled for admins in non-cloud environments
|
||||
// Only enabled for owners
|
||||
isEnabled: ({ auth }) => !!(auth?.role === "owner"),
|
||||
},
|
||||
{
|
||||
@@ -420,8 +394,7 @@ const MENU: Menu = {
|
||||
url: "/dashboard/settings/sso",
|
||||
icon: LogIn,
|
||||
// Enabled for admins in both cloud and self-hosted (enterprise)
|
||||
isEnabled: ({ auth }) =>
|
||||
!!(auth?.role === "owner" || auth?.role === "admin"),
|
||||
isEnabled: ({ permissions }) => !!permissions?.organization.update,
|
||||
},
|
||||
{
|
||||
isSingle: true,
|
||||
@@ -453,6 +426,7 @@ const MENU: Menu = {
|
||||
*/
|
||||
function createMenuForAuthUser(opts: {
|
||||
auth?: AuthQueryOutput;
|
||||
permissions?: PermissionsOutput;
|
||||
isCloud: boolean;
|
||||
whitelabeling?: {
|
||||
docsUrl?: string | null;
|
||||
@@ -461,7 +435,7 @@ function createMenuForAuthUser(opts: {
|
||||
}): Menu {
|
||||
const filterEnabled = <
|
||||
T extends {
|
||||
isEnabled?: (o: { auth?: AuthQueryOutput; isCloud: boolean }) => boolean;
|
||||
isEnabled?: (o: EnabledOpts) => boolean;
|
||||
},
|
||||
>(
|
||||
items: readonly T[],
|
||||
@@ -469,7 +443,11 @@ function createMenuForAuthUser(opts: {
|
||||
items.filter((item) =>
|
||||
!item.isEnabled
|
||||
? true
|
||||
: item.isEnabled({ auth: opts.auth, isCloud: opts.isCloud }),
|
||||
: item.isEnabled({
|
||||
auth: opts.auth,
|
||||
permissions: opts.permissions,
|
||||
isCloud: opts.isCloud,
|
||||
}),
|
||||
) as T[];
|
||||
|
||||
// Apply whitelabeling URL overrides to help items
|
||||
@@ -567,6 +545,7 @@ function SidebarLogo() {
|
||||
const { mutateAsync: setDefaultOrganization, isPending: isSettingDefault } =
|
||||
api.organization.setDefault.useMutation();
|
||||
const { isMobile } = useSidebar();
|
||||
const isCollapsed = state === "collapsed" && !isMobile;
|
||||
const { data: activeOrganization } = api.organization.active.useQuery();
|
||||
|
||||
const { data: invitations, refetch: refetchInvitations } =
|
||||
@@ -592,9 +571,7 @@ function SidebarLogo() {
|
||||
<SidebarMenu
|
||||
className={cn(
|
||||
"flex gap-2",
|
||||
state === "collapsed"
|
||||
? "flex-col"
|
||||
: "flex-row justify-between items-center",
|
||||
isCollapsed ? "flex-col" : "flex-row justify-between items-center",
|
||||
)}
|
||||
>
|
||||
{/* Organization Logo and Selector */}
|
||||
@@ -602,17 +579,17 @@ function SidebarLogo() {
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<SidebarMenuButton
|
||||
size={state === "collapsed" ? "sm" : "lg"}
|
||||
size={isCollapsed ? "sm" : "lg"}
|
||||
className={cn(
|
||||
"data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground",
|
||||
state === "collapsed" &&
|
||||
isCollapsed &&
|
||||
"flex justify-center items-center p-2 h-10 w-10 mx-auto",
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
"flex items-center gap-2",
|
||||
state === "collapsed" && "justify-center",
|
||||
isCollapsed && "justify-center",
|
||||
)}
|
||||
>
|
||||
<div
|
||||
@@ -624,7 +601,7 @@ function SidebarLogo() {
|
||||
<Logo
|
||||
className={cn(
|
||||
"transition-all",
|
||||
state === "collapsed" ? "size-4" : "size-5",
|
||||
isCollapsed ? "size-4" : "size-5",
|
||||
)}
|
||||
logoUrl={activeOrganization?.logo || undefined}
|
||||
/>
|
||||
@@ -632,7 +609,7 @@ function SidebarLogo() {
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col items-start",
|
||||
state === "collapsed" && "hidden",
|
||||
isCollapsed && "hidden",
|
||||
)}
|
||||
>
|
||||
<p className="text-sm font-medium leading-none">
|
||||
@@ -641,7 +618,7 @@ function SidebarLogo() {
|
||||
</div>
|
||||
</div>
|
||||
<ChevronsUpDown
|
||||
className={cn("ml-auto", state === "collapsed" && "hidden")}
|
||||
className={cn("ml-auto", isCollapsed && "hidden")}
|
||||
/>
|
||||
</SidebarMenuButton>
|
||||
</DropdownMenuTrigger>
|
||||
@@ -790,7 +767,7 @@ function SidebarLogo() {
|
||||
</SidebarMenuItem>
|
||||
|
||||
{/* Notification Bell */}
|
||||
<SidebarMenuItem className={cn(state === "collapsed" && "mt-2")}>
|
||||
<SidebarMenuItem className={cn(isCollapsed && "mt-2")}>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
@@ -798,7 +775,7 @@ function SidebarLogo() {
|
||||
size="icon"
|
||||
className={cn(
|
||||
"relative",
|
||||
state === "collapsed" && "h-8 w-8 p-1.5 mx-auto",
|
||||
isCollapsed && "h-8 w-8 p-1.5 mx-auto",
|
||||
)}
|
||||
>
|
||||
<Bell className="size-4" />
|
||||
@@ -894,6 +871,7 @@ export default function Page({ children }: Props) {
|
||||
|
||||
const pathname = usePathname();
|
||||
const { data: auth } = api.user.get.useQuery();
|
||||
const { data: permissions } = api.user.getPermissions.useQuery();
|
||||
const { data: dokployVersion } = api.settings.getDokployVersion.useQuery();
|
||||
const { data: whitelabeling } = api.whitelabeling.get.useQuery(undefined, {
|
||||
staleTime: 5 * 60 * 1000,
|
||||
@@ -907,7 +885,12 @@ export default function Page({ children }: Props) {
|
||||
home: filteredHome,
|
||||
settings: filteredSettings,
|
||||
help,
|
||||
} = createMenuForAuthUser({ auth, isCloud: !!isCloud, whitelabeling });
|
||||
} = createMenuForAuthUser({
|
||||
auth,
|
||||
permissions,
|
||||
isCloud: !!isCloud,
|
||||
whitelabeling,
|
||||
});
|
||||
|
||||
const activeItem = findActiveNavItem(
|
||||
[...filteredHome, ...filteredSettings],
|
||||
@@ -1147,7 +1130,7 @@ export default function Page({ children }: Props) {
|
||||
</SidebarContent>
|
||||
<SidebarFooter>
|
||||
<SidebarMenu className="flex flex-col gap-2">
|
||||
{!isCloud && (auth?.role === "owner" || auth?.role === "admin") && (
|
||||
{!isCloud && permissions?.organization.update && (
|
||||
<SidebarMenuItem>
|
||||
<UpdateServerButton />
|
||||
</SidebarMenuItem>
|
||||
@@ -1161,14 +1144,9 @@ export default function Page({ children }: Props) {
|
||||
</div>
|
||||
)}
|
||||
{dokployVersion && (
|
||||
<>
|
||||
<div className="px-3 text-xs text-muted-foreground text-center group-data-[collapsible=icon]:hidden">
|
||||
Version {dokployVersion}
|
||||
</div>
|
||||
<div className="hidden text-xs text-muted-foreground text-center group-data-[collapsible=icon]:block">
|
||||
{dokployVersion}
|
||||
</div>
|
||||
</>
|
||||
<div className="px-3 text-xs text-muted-foreground text-center group-data-[collapsible=icon]:hidden">
|
||||
Version {dokployVersion}
|
||||
</div>
|
||||
)}
|
||||
</SidebarMenu>
|
||||
</SidebarFooter>
|
||||
|
||||
@@ -21,6 +21,7 @@ const _AUTO_CHECK_UPDATES_INTERVAL_MINUTES = 7;
|
||||
export const UserNav = () => {
|
||||
const router = useRouter();
|
||||
const { data } = api.user.get.useQuery();
|
||||
const { data: permissions } = api.user.getPermissions.useQuery();
|
||||
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||
|
||||
// const { mutateAsync } = api.auth.logout.useMutation();
|
||||
@@ -94,9 +95,7 @@ export const UserNav = () => {
|
||||
>
|
||||
Monitoring
|
||||
</DropdownMenuItem>
|
||||
{(data?.role === "owner" ||
|
||||
data?.role === "admin" ||
|
||||
data?.canAccessToTraefikFiles) && (
|
||||
{permissions?.traefikFiles.read && (
|
||||
<DropdownMenuItem
|
||||
className="cursor-pointer"
|
||||
onClick={() => {
|
||||
@@ -106,9 +105,7 @@ export const UserNav = () => {
|
||||
Traefik
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
{(data?.role === "owner" ||
|
||||
data?.role === "admin" ||
|
||||
data?.canAccessToDocker) && (
|
||||
{permissions?.docker.read && (
|
||||
<DropdownMenuItem
|
||||
className="cursor-pointer"
|
||||
onClick={() => {
|
||||
@@ -122,7 +119,7 @@ export const UserNav = () => {
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
(data?.role === "owner" || data?.role === "admin") && (
|
||||
permissions?.organization.update && (
|
||||
<DropdownMenuItem
|
||||
className="cursor-pointer"
|
||||
onClick={() => {
|
||||
|
||||
230
apps/dokploy/components/proprietary/audit-logs/columns.tsx
Normal file
230
apps/dokploy/components/proprietary/audit-logs/columns.tsx
Normal file
@@ -0,0 +1,230 @@
|
||||
"use client";
|
||||
|
||||
import type { AuditLog } from "@dokploy/server/db/schema";
|
||||
import type { ColumnDef } from "@tanstack/react-table";
|
||||
import { format } from "date-fns";
|
||||
import {
|
||||
ArrowUpDown,
|
||||
FileJson,
|
||||
LogIn,
|
||||
LogOut,
|
||||
PlusCircle,
|
||||
RefreshCw,
|
||||
RotateCcw,
|
||||
Trash2,
|
||||
Upload,
|
||||
XCircle,
|
||||
} from "lucide-react";
|
||||
import React from "react";
|
||||
import { CodeEditor } from "@/components/shared/code-editor";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
|
||||
const ACTION_CONFIG: Record<
|
||||
string,
|
||||
{ label: string; icon: React.ElementType; className: string }
|
||||
> = {
|
||||
create: {
|
||||
label: "Created",
|
||||
icon: PlusCircle,
|
||||
className:
|
||||
"bg-emerald-500/10 text-emerald-600 dark:text-emerald-400 border-emerald-500/20",
|
||||
},
|
||||
update: {
|
||||
label: "Updated",
|
||||
icon: RefreshCw,
|
||||
className:
|
||||
"bg-blue-500/10 text-blue-600 dark:text-blue-400 border-blue-500/20",
|
||||
},
|
||||
delete: {
|
||||
label: "Deleted",
|
||||
icon: Trash2,
|
||||
className: "bg-red-500/10 text-red-600 dark:text-red-400 border-red-500/20",
|
||||
},
|
||||
deploy: {
|
||||
label: "Deployed",
|
||||
icon: Upload,
|
||||
className:
|
||||
"bg-violet-500/10 text-violet-600 dark:text-violet-400 border-violet-500/20",
|
||||
},
|
||||
cancel: {
|
||||
label: "Cancelled",
|
||||
icon: XCircle,
|
||||
className:
|
||||
"bg-orange-500/10 text-orange-600 dark:text-orange-400 border-orange-500/20",
|
||||
},
|
||||
redeploy: {
|
||||
label: "Redeployed",
|
||||
icon: RotateCcw,
|
||||
className:
|
||||
"bg-violet-500/10 text-violet-600 dark:text-violet-400 border-violet-500/20",
|
||||
},
|
||||
login: {
|
||||
label: "Login",
|
||||
icon: LogIn,
|
||||
className:
|
||||
"bg-teal-500/10 text-teal-600 dark:text-teal-400 border-teal-500/20",
|
||||
},
|
||||
logout: {
|
||||
label: "Logout",
|
||||
icon: LogOut,
|
||||
className:
|
||||
"bg-slate-500/10 text-slate-600 dark:text-slate-400 border-slate-500/20",
|
||||
},
|
||||
};
|
||||
|
||||
const RESOURCE_LABELS: Record<string, string> = {
|
||||
project: "Project",
|
||||
service: "Service",
|
||||
environment: "Environment",
|
||||
deployment: "Deployment",
|
||||
user: "User",
|
||||
customRole: "Custom Role",
|
||||
domain: "Domain",
|
||||
certificate: "Certificate",
|
||||
registry: "Registry",
|
||||
server: "Server",
|
||||
sshKey: "SSH Key",
|
||||
gitProvider: "Git Provider",
|
||||
notification: "Notification",
|
||||
settings: "Settings",
|
||||
session: "Session",
|
||||
};
|
||||
|
||||
function MetadataCell({ metadata }: { metadata: string | null }) {
|
||||
if (!metadata)
|
||||
return <span className="text-muted-foreground text-sm">—</span>;
|
||||
|
||||
const formatted = React.useMemo(() => {
|
||||
try {
|
||||
return JSON.stringify(JSON.parse(metadata), null, 2);
|
||||
} catch {
|
||||
return metadata;
|
||||
}
|
||||
}, [metadata]);
|
||||
|
||||
return (
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="ghost" size="sm" className="h-7 gap-1.5 text-xs">
|
||||
<FileJson className="h-3.5 w-3.5" />
|
||||
View
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-w-lg">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Metadata</DialogTitle>
|
||||
</DialogHeader>
|
||||
<CodeEditor
|
||||
value={formatted}
|
||||
language="json"
|
||||
lineNumbers={false}
|
||||
readOnly
|
||||
className="min-h-[200px] max-h-[400px] overflow-auto rounded-md"
|
||||
/>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
export const columns: ColumnDef<AuditLog>[] = [
|
||||
{
|
||||
accessorKey: "createdAt",
|
||||
header: ({ column }) => (
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
|
||||
>
|
||||
Date
|
||||
<ArrowUpDown className="ml-2 h-4 w-4" />
|
||||
</Button>
|
||||
),
|
||||
cell: ({ row }) => (
|
||||
<span className="text-sm text-muted-foreground whitespace-nowrap">
|
||||
{format(new Date(row.getValue("createdAt")), "MMM d, yyyy HH:mm")}
|
||||
</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: "userEmail",
|
||||
header: ({ column }) => (
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
|
||||
>
|
||||
User
|
||||
<ArrowUpDown className="ml-2 h-4 w-4" />
|
||||
</Button>
|
||||
),
|
||||
cell: ({ row }) => (
|
||||
<span className="text-sm">{row.getValue("userEmail")}</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: "action",
|
||||
header: ({ column }) => (
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
|
||||
>
|
||||
Action
|
||||
<ArrowUpDown className="ml-2 h-4 w-4" />
|
||||
</Button>
|
||||
),
|
||||
cell: ({ row }) => {
|
||||
const action = row.getValue("action") as string;
|
||||
const config = ACTION_CONFIG[action];
|
||||
if (!config) {
|
||||
return <span className="text-xs text-muted-foreground">{action}</span>;
|
||||
}
|
||||
const Icon = config.icon;
|
||||
return (
|
||||
<span
|
||||
className={`inline-flex items-center gap-1.5 rounded-full border px-2.5 py-0.5 text-xs font-medium ${config.className}`}
|
||||
>
|
||||
<Icon className="size-3" />
|
||||
{config.label}
|
||||
</span>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "resourceType",
|
||||
header: "Resource",
|
||||
cell: ({ row }) => (
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{RESOURCE_LABELS[row.getValue("resourceType") as string] ??
|
||||
row.getValue("resourceType")}
|
||||
</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: "resourceName",
|
||||
header: "Name",
|
||||
cell: ({ row }) => (
|
||||
<span className="text-sm font-medium">
|
||||
{(row.getValue("resourceName") as string) ?? "—"}
|
||||
</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: "userRole",
|
||||
header: "Role",
|
||||
cell: ({ row }) => (
|
||||
<span className="text-sm text-muted-foreground capitalize">
|
||||
{row.getValue("userRole")}
|
||||
</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: "metadata",
|
||||
header: "Metadata",
|
||||
cell: ({ row }) => <MetadataCell metadata={row.getValue("metadata")} />,
|
||||
},
|
||||
];
|
||||
400
apps/dokploy/components/proprietary/audit-logs/data-table.tsx
Normal file
400
apps/dokploy/components/proprietary/audit-logs/data-table.tsx
Normal file
@@ -0,0 +1,400 @@
|
||||
"use client";
|
||||
|
||||
import type { AuditLog } from "@dokploy/server/db/schema";
|
||||
import { DropdownMenuTrigger } from "@radix-ui/react-dropdown-menu";
|
||||
import {
|
||||
type ColumnDef,
|
||||
flexRender,
|
||||
getCoreRowModel,
|
||||
getSortedRowModel,
|
||||
type SortingState,
|
||||
useReactTable,
|
||||
type VisibilityState,
|
||||
} from "@tanstack/react-table";
|
||||
import { format } from "date-fns";
|
||||
import { CalendarIcon, ChevronDown, X } from "lucide-react";
|
||||
import React from "react";
|
||||
import type { DateRange } from "react-day-picker";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Calendar } from "@/components/ui/calendar";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuCheckboxItem,
|
||||
DropdownMenuContent,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/components/ui/popover";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
|
||||
const ACTION_OPTIONS = [
|
||||
{ value: "create", label: "Created" },
|
||||
{ value: "update", label: "Updated" },
|
||||
{ value: "delete", label: "Deleted" },
|
||||
{ value: "deploy", label: "Deployed" },
|
||||
{ value: "cancel", label: "Cancelled" },
|
||||
{ value: "redeploy", label: "Redeployed" },
|
||||
{ value: "login", label: "Login" },
|
||||
{ value: "logout", label: "Logout" },
|
||||
];
|
||||
|
||||
const RESOURCE_OPTIONS = [
|
||||
{ value: "project", label: "Projects" },
|
||||
{ value: "service", label: "Applications / Services" },
|
||||
{ value: "environment", label: "Environments" },
|
||||
{ value: "deployment", label: "Deployments" },
|
||||
{ value: "user", label: "Users" },
|
||||
{ value: "customRole", label: "Custom Roles" },
|
||||
{ value: "domain", label: "Domains" },
|
||||
{ value: "certificate", label: "Certificates" },
|
||||
{ value: "registry", label: "Registries" },
|
||||
{ value: "server", label: "Remote Servers" },
|
||||
{ value: "sshKey", label: "SSH Keys" },
|
||||
{ value: "gitProvider", label: "Git Providers" },
|
||||
{ value: "notification", label: "Notifications" },
|
||||
{ value: "settings", label: "Settings" },
|
||||
{ value: "session", label: "Sessions (Login/Logout)" },
|
||||
];
|
||||
|
||||
const PAGE_SIZE_OPTIONS = [25, 50, 100, 200];
|
||||
|
||||
type AuditAction =
|
||||
| "create"
|
||||
| "update"
|
||||
| "delete"
|
||||
| "deploy"
|
||||
| "cancel"
|
||||
| "redeploy"
|
||||
| "login"
|
||||
| "logout";
|
||||
type AuditResourceType =
|
||||
| "project"
|
||||
| "service"
|
||||
| "environment"
|
||||
| "deployment"
|
||||
| "user"
|
||||
| "customRole"
|
||||
| "domain"
|
||||
| "certificate"
|
||||
| "registry"
|
||||
| "server"
|
||||
| "sshKey"
|
||||
| "gitProvider"
|
||||
| "notification"
|
||||
| "settings"
|
||||
| "session";
|
||||
|
||||
export interface AuditLogFilters {
|
||||
userEmail: string;
|
||||
resourceName: string;
|
||||
action: AuditAction | "";
|
||||
resourceType: AuditResourceType | "";
|
||||
dateRange: DateRange | undefined;
|
||||
}
|
||||
|
||||
interface DataTableProps {
|
||||
columns: ColumnDef<AuditLog>[];
|
||||
data: AuditLog[];
|
||||
total: number;
|
||||
pageIndex: number;
|
||||
pageSize: number;
|
||||
filters: AuditLogFilters;
|
||||
onPageChange: (page: number) => void;
|
||||
onPageSizeChange: (size: number) => void;
|
||||
onFilterChange: <K extends keyof AuditLogFilters>(
|
||||
key: K,
|
||||
value: AuditLogFilters[K],
|
||||
) => void;
|
||||
isLoading?: boolean;
|
||||
}
|
||||
|
||||
export function DataTable({
|
||||
columns,
|
||||
data,
|
||||
total,
|
||||
pageIndex,
|
||||
pageSize,
|
||||
filters,
|
||||
onPageChange,
|
||||
onPageSizeChange,
|
||||
onFilterChange,
|
||||
isLoading,
|
||||
}: DataTableProps) {
|
||||
const [sorting, setSorting] = React.useState<SortingState>([
|
||||
{ id: "createdAt", desc: true },
|
||||
]);
|
||||
const [columnVisibility, setColumnVisibility] =
|
||||
React.useState<VisibilityState>({});
|
||||
|
||||
const table = useReactTable({
|
||||
data,
|
||||
columns,
|
||||
onSortingChange: setSorting,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
getSortedRowModel: getSortedRowModel(),
|
||||
onColumnVisibilityChange: setColumnVisibility,
|
||||
manualPagination: true,
|
||||
manualFiltering: true,
|
||||
rowCount: total,
|
||||
state: {
|
||||
sorting,
|
||||
columnVisibility,
|
||||
},
|
||||
});
|
||||
|
||||
const pageCount = Math.ceil(total / pageSize);
|
||||
const hasFilters =
|
||||
filters.userEmail ||
|
||||
filters.resourceName ||
|
||||
filters.action ||
|
||||
filters.resourceType ||
|
||||
filters.dateRange;
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4 w-full">
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<Input
|
||||
placeholder="Filter by user..."
|
||||
value={filters.userEmail}
|
||||
onChange={(e) => onFilterChange("userEmail", e.target.value)}
|
||||
className="max-w-xs"
|
||||
/>
|
||||
<Input
|
||||
placeholder="Filter by name..."
|
||||
value={filters.resourceName}
|
||||
onChange={(e) => onFilterChange("resourceName", e.target.value)}
|
||||
className="max-w-xs"
|
||||
/>
|
||||
<Select
|
||||
value={filters.action || "__all__"}
|
||||
onValueChange={(value) =>
|
||||
onFilterChange(
|
||||
"action",
|
||||
value === "__all__" ? "" : (value as AuditAction),
|
||||
)
|
||||
}
|
||||
>
|
||||
<SelectTrigger className="w-[160px]">
|
||||
<SelectValue placeholder="All actions" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="__all__">All actions</SelectItem>
|
||||
{ACTION_OPTIONS.map((opt) => (
|
||||
<SelectItem key={opt.value} value={opt.value}>
|
||||
{opt.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Select
|
||||
value={filters.resourceType || "__all__"}
|
||||
onValueChange={(value) =>
|
||||
onFilterChange(
|
||||
"resourceType",
|
||||
value === "__all__" ? "" : (value as AuditResourceType),
|
||||
)
|
||||
}
|
||||
>
|
||||
<SelectTrigger className="w-[200px]">
|
||||
<SelectValue placeholder="All resources" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="__all__">All resources</SelectItem>
|
||||
{RESOURCE_OPTIONS.map((opt) => (
|
||||
<SelectItem key={opt.value} value={opt.value}>
|
||||
{opt.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="h-9 gap-1.5 text-sm font-normal"
|
||||
>
|
||||
<CalendarIcon className="h-4 w-4" />
|
||||
{filters.dateRange?.from ? (
|
||||
filters.dateRange.to ? (
|
||||
`${format(filters.dateRange.from, "MMM d")} – ${format(filters.dateRange.to, "MMM d, yyyy")}`
|
||||
) : (
|
||||
format(filters.dateRange.from, "MMM d, yyyy")
|
||||
)
|
||||
) : (
|
||||
<span className="text-muted-foreground">Date range</span>
|
||||
)}
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-auto p-0" align="start">
|
||||
<Calendar
|
||||
mode="range"
|
||||
selected={filters.dateRange}
|
||||
onSelect={(range) => onFilterChange("dateRange", range)}
|
||||
numberOfMonths={2}
|
||||
initialFocus
|
||||
/>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
{hasFilters && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
onFilterChange("userEmail", "");
|
||||
onFilterChange("resourceName", "");
|
||||
onFilterChange("action", "");
|
||||
onFilterChange("resourceType", "");
|
||||
onFilterChange("dateRange", undefined);
|
||||
}}
|
||||
className="text-muted-foreground"
|
||||
>
|
||||
<X className="h-4 w-4 mr-1" />
|
||||
Clear
|
||||
</Button>
|
||||
)}
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="outline" className="ml-auto">
|
||||
Columns <ChevronDown className="ml-2 h-4 w-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
{table
|
||||
.getAllColumns()
|
||||
.filter((col) => col.getCanHide())
|
||||
.map((col) => (
|
||||
<DropdownMenuCheckboxItem
|
||||
key={col.id}
|
||||
className="capitalize"
|
||||
checked={col.getIsVisible()}
|
||||
onCheckedChange={(value) => col.toggleVisibility(!!value)}
|
||||
>
|
||||
{col.id}
|
||||
</DropdownMenuCheckboxItem>
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
|
||||
<div className="rounded-md border overflow-auto">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
{table.getHeaderGroups().map((headerGroup) => (
|
||||
<TableRow key={headerGroup.id}>
|
||||
{headerGroup.headers.map((header) => (
|
||||
<TableHead key={header.id}>
|
||||
{header.isPlaceholder
|
||||
? null
|
||||
: flexRender(
|
||||
header.column.columnDef.header,
|
||||
header.getContext(),
|
||||
)}
|
||||
</TableHead>
|
||||
))}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{isLoading ? (
|
||||
<TableRow>
|
||||
<TableCell
|
||||
colSpan={columns.length}
|
||||
className="h-24 text-center text-muted-foreground"
|
||||
>
|
||||
Loading...
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : table.getRowModel().rows.length ? (
|
||||
table.getRowModel().rows.map((row) => (
|
||||
<TableRow key={row.id}>
|
||||
{row.getVisibleCells().map((cell) => (
|
||||
<TableCell key={cell.id}>
|
||||
{flexRender(
|
||||
cell.column.columnDef.cell,
|
||||
cell.getContext(),
|
||||
)}
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
))
|
||||
) : (
|
||||
<TableRow>
|
||||
<TableCell
|
||||
colSpan={columns.length}
|
||||
className="h-24 text-center text-muted-foreground"
|
||||
>
|
||||
No audit logs found.
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between text-sm text-muted-foreground">
|
||||
<span>
|
||||
{total} {total === 1 ? "entry" : "entries"} total
|
||||
</span>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm whitespace-nowrap">Rows per page</span>
|
||||
<Select
|
||||
value={String(pageSize)}
|
||||
onValueChange={(value) => onPageSizeChange(Number(value))}
|
||||
>
|
||||
<SelectTrigger className="w-[80px] h-8">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{PAGE_SIZE_OPTIONS.map((size) => (
|
||||
<SelectItem key={size} value={String(size)}>
|
||||
{size}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<span className="whitespace-nowrap">
|
||||
Page {pageIndex + 1} of {Math.max(1, pageCount)}
|
||||
</span>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => onPageChange(pageIndex - 1)}
|
||||
disabled={pageIndex === 0}
|
||||
>
|
||||
Previous
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => onPageChange(pageIndex + 1)}
|
||||
disabled={pageIndex + 1 >= pageCount}
|
||||
>
|
||||
Next
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
import { ClipboardList } from "lucide-react";
|
||||
import React from "react";
|
||||
import { EnterpriseFeatureGate } from "@/components/proprietary/enterprise-feature-gate";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import { api } from "@/utils/api";
|
||||
import { columns } from "./columns";
|
||||
import { type AuditLogFilters, DataTable } from "./data-table";
|
||||
|
||||
function AuditLogsContent() {
|
||||
const [pageIndex, setPageIndex] = React.useState(0);
|
||||
const [pageSize, setPageSize] = React.useState(50);
|
||||
const [filters, setFilters] = React.useState<AuditLogFilters>({
|
||||
userEmail: "",
|
||||
resourceName: "",
|
||||
action: "",
|
||||
resourceType: "",
|
||||
dateRange: undefined,
|
||||
});
|
||||
|
||||
const [debouncedText, setDebouncedText] = React.useState({
|
||||
userEmail: "",
|
||||
resourceName: "",
|
||||
});
|
||||
|
||||
React.useEffect(() => {
|
||||
const t = setTimeout(() => {
|
||||
setDebouncedText({
|
||||
userEmail: filters.userEmail,
|
||||
resourceName: filters.resourceName,
|
||||
});
|
||||
setPageIndex(0);
|
||||
}, 400);
|
||||
return () => clearTimeout(t);
|
||||
}, [filters.userEmail, filters.resourceName]);
|
||||
|
||||
const handleFilterChange = <K extends keyof AuditLogFilters>(
|
||||
key: K,
|
||||
value: AuditLogFilters[K],
|
||||
) => {
|
||||
setFilters((prev) => ({ ...prev, [key]: value }));
|
||||
if (key !== "userEmail" && key !== "resourceName") {
|
||||
setPageIndex(0);
|
||||
}
|
||||
};
|
||||
|
||||
const handlePageSizeChange = (size: number) => {
|
||||
setPageSize(size);
|
||||
setPageIndex(0);
|
||||
};
|
||||
|
||||
const { data, isLoading } = api.auditLog.all.useQuery({
|
||||
userEmail: debouncedText.userEmail || undefined,
|
||||
resourceName: debouncedText.resourceName || undefined,
|
||||
action: filters.action || undefined,
|
||||
resourceType: filters.resourceType || undefined,
|
||||
from: filters.dateRange?.from,
|
||||
to: filters.dateRange?.to,
|
||||
limit: pageSize,
|
||||
offset: pageIndex * pageSize,
|
||||
});
|
||||
|
||||
return (
|
||||
<DataTable
|
||||
columns={columns}
|
||||
data={data?.logs ?? []}
|
||||
total={data?.total ?? 0}
|
||||
pageIndex={pageIndex}
|
||||
pageSize={pageSize}
|
||||
filters={filters}
|
||||
onPageChange={setPageIndex}
|
||||
onPageSizeChange={handlePageSizeChange}
|
||||
onFilterChange={handleFilterChange}
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function ShowAuditLogs() {
|
||||
return (
|
||||
<Card className="h-full bg-sidebar p-2.5 rounded-xl max-w-6xl w-full mx-auto">
|
||||
<div className="rounded-xl bg-background shadow-md ">
|
||||
<EnterpriseFeatureGate
|
||||
lockedProps={{
|
||||
title: "Audit Logs",
|
||||
description:
|
||||
"Get full visibility into every action performed across your organization. Audit logs are available as part of Dokploy Enterprise.",
|
||||
ctaLabel: "Manage License",
|
||||
}}
|
||||
>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-xl flex flex-row gap-2">
|
||||
<ClipboardList className="h-5 w-5 text-muted-foreground self-center" />
|
||||
Audit Logs
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Track all actions performed by members in your organization.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-2 py-8 border-t">
|
||||
<AuditLogsContent />
|
||||
</CardContent>
|
||||
</EnterpriseFeatureGate>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
1032
apps/dokploy/components/proprietary/roles/manage-custom-roles.tsx
Normal file
1032
apps/dokploy/components/proprietary/roles/manage-custom-roles.tsx
Normal file
File diff suppressed because it is too large
Load Diff
@@ -17,6 +17,8 @@ import {
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { SidebarTrigger } from "@/components/ui/sidebar";
|
||||
import { TimeBadge } from "@/components/ui/time-badge";
|
||||
import { api } from "@/utils/api";
|
||||
|
||||
interface BreadcrumbEntry {
|
||||
name: string;
|
||||
@@ -32,9 +34,11 @@ interface Props {
|
||||
}
|
||||
|
||||
export const BreadcrumbSidebar = ({ list }: Props) => {
|
||||
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||
|
||||
return (
|
||||
<header className="flex h-16 shrink-0 items-center gap-2 transition-[width,height] ease-linear group-has-[[data-collapsible=icon]]/sidebar-wrapper:h-12">
|
||||
<div className="flex items-center justify-between w-full">
|
||||
<div className="flex items-center justify-between w-full px-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<SidebarTrigger className="-ml-1" />
|
||||
<Separator orientation="vertical" className="mr-2 h-4" />
|
||||
@@ -75,6 +79,7 @@ export const BreadcrumbSidebar = ({ list }: Props) => {
|
||||
</BreadcrumbList>
|
||||
</Breadcrumb>
|
||||
</div>
|
||||
{!isCloud && <TimeBadge />}
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
|
||||
@@ -213,7 +213,9 @@ const Sidebar = React.forwardRef<
|
||||
}
|
||||
side={side}
|
||||
>
|
||||
<div className="flex h-full w-full flex-col">{children}</div>
|
||||
<div className="flex h-full w-full flex-col overflow-hidden">
|
||||
{children}
|
||||
</div>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
);
|
||||
@@ -412,7 +414,7 @@ const SidebarContent = React.forwardRef<
|
||||
ref={ref}
|
||||
data-sidebar="content"
|
||||
className={cn(
|
||||
"flex min-h-0 flex-1 flex-col gap-2 overflow-auto group-data-[collapsible=icon]:overflow-hidden",
|
||||
"flex min-h-0 flex-1 flex-col gap-2 overflow-auto group-data-[collapsible=icon]:overflow-y-auto",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
|
||||
31
apps/dokploy/drizzle/0149_rare_radioactive_man.sql
Normal file
31
apps/dokploy/drizzle/0149_rare_radioactive_man.sql
Normal file
@@ -0,0 +1,31 @@
|
||||
CREATE TABLE "organization_role" (
|
||||
"id" text PRIMARY KEY NOT NULL,
|
||||
"organization_id" text NOT NULL,
|
||||
"role" text NOT NULL,
|
||||
"permission" text NOT NULL,
|
||||
"created_at" timestamp DEFAULT now() NOT NULL,
|
||||
"updated_at" timestamp
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "audit_log" (
|
||||
"id" text PRIMARY KEY NOT NULL,
|
||||
"organization_id" text,
|
||||
"user_id" text,
|
||||
"user_email" text NOT NULL,
|
||||
"user_role" text NOT NULL,
|
||||
"action" text NOT NULL,
|
||||
"resource_type" text NOT NULL,
|
||||
"resource_id" text,
|
||||
"resource_name" text,
|
||||
"metadata" text,
|
||||
"created_at" timestamp DEFAULT now() NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "organization_role" ADD CONSTRAINT "organization_role_organization_id_organization_id_fk" FOREIGN KEY ("organization_id") REFERENCES "public"."organization"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "audit_log" ADD CONSTRAINT "audit_log_organization_id_organization_id_fk" FOREIGN KEY ("organization_id") REFERENCES "public"."organization"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "audit_log" ADD CONSTRAINT "audit_log_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint
|
||||
CREATE INDEX "organizationRole_organizationId_idx" ON "organization_role" USING btree ("organization_id");--> statement-breakpoint
|
||||
CREATE INDEX "organizationRole_role_idx" ON "organization_role" USING btree ("role");--> statement-breakpoint
|
||||
CREATE INDEX "auditLog_organizationId_idx" ON "audit_log" USING btree ("organization_id");--> statement-breakpoint
|
||||
CREATE INDEX "auditLog_userId_idx" ON "audit_log" USING btree ("user_id");--> statement-breakpoint
|
||||
CREATE INDEX "auditLog_createdAt_idx" ON "audit_log" USING btree ("created_at");
|
||||
5
apps/dokploy/drizzle/0150_nappy_blue_blade.sql
Normal file
5
apps/dokploy/drizzle/0150_nappy_blue_blade.sql
Normal file
@@ -0,0 +1,5 @@
|
||||
ALTER TABLE "apikey" ALTER COLUMN "user_id" DROP NOT NULL;--> statement-breakpoint
|
||||
ALTER TABLE "apikey" ADD COLUMN "config_id" text DEFAULT 'default' NOT NULL;--> statement-breakpoint
|
||||
ALTER TABLE "apikey" ADD COLUMN "reference_id" text;--> statement-breakpoint
|
||||
UPDATE "apikey" SET "reference_id" = "user_id" WHERE "reference_id" IS NULL;--> statement-breakpoint
|
||||
ALTER TABLE "apikey" ALTER COLUMN "reference_id" SET NOT NULL;
|
||||
4
apps/dokploy/drizzle/0151_modern_sunfire.sql
Normal file
4
apps/dokploy/drizzle/0151_modern_sunfire.sql
Normal file
@@ -0,0 +1,4 @@
|
||||
ALTER TABLE "apikey" DROP CONSTRAINT "apikey_user_id_user_id_fk";
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "apikey" ADD CONSTRAINT "apikey_reference_id_user_id_fk" FOREIGN KEY ("reference_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "apikey" DROP COLUMN "user_id";
|
||||
7715
apps/dokploy/drizzle/meta/0149_snapshot.json
Normal file
7715
apps/dokploy/drizzle/meta/0149_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
7728
apps/dokploy/drizzle/meta/0150_snapshot.json
Normal file
7728
apps/dokploy/drizzle/meta/0150_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
7722
apps/dokploy/drizzle/meta/0151_snapshot.json
Normal file
7722
apps/dokploy/drizzle/meta/0151_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1044,6 +1044,27 @@
|
||||
"when": 1773129798212,
|
||||
"tag": "0148_futuristic_bullseye",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 149,
|
||||
"version": "7",
|
||||
"when": 1773637297592,
|
||||
"tag": "0149_rare_radioactive_man",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 150,
|
||||
"version": "7",
|
||||
"when": 1773870095817,
|
||||
"tag": "0150_nappy_blue_blade",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 151,
|
||||
"version": "7",
|
||||
"when": 1773872561300,
|
||||
"tag": "0151_modern_sunfire",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import { ssoClient } from "@better-auth/sso/client";
|
||||
import { apiKeyClient } from "@better-auth/api-key/client";
|
||||
import {
|
||||
adminClient,
|
||||
apiKeyClient,
|
||||
inferAdditionalFields,
|
||||
organizationClient,
|
||||
twoFactorClient,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "dokploy",
|
||||
"version": "v0.28.6",
|
||||
"version": "v0.28.8",
|
||||
"private": true,
|
||||
"license": "Apache-2.0",
|
||||
"type": "module",
|
||||
@@ -46,7 +46,8 @@
|
||||
"@ai-sdk/mistral": "^3.0.20",
|
||||
"@ai-sdk/openai": "^3.0.29",
|
||||
"@ai-sdk/openai-compatible": "^2.0.30",
|
||||
"@better-auth/sso": "1.5.0-beta.16",
|
||||
"@better-auth/api-key": "1.5.4",
|
||||
"@better-auth/sso": "1.5.4",
|
||||
"@codemirror/autocomplete": "^6.18.6",
|
||||
"@codemirror/lang-css": "^6.3.1",
|
||||
"@codemirror/lang-json": "^6.0.1",
|
||||
@@ -99,7 +100,7 @@
|
||||
"ai": "^6.0.86",
|
||||
"ai-sdk-ollama": "^3.7.0",
|
||||
"bcrypt": "5.1.1",
|
||||
"better-auth": "1.5.0-beta.16",
|
||||
"better-auth": "1.5.4",
|
||||
"bl": "6.0.11",
|
||||
"boxen": "^7.1.1",
|
||||
"bullmq": "5.67.3",
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { validateRequest } from "@dokploy/server/lib/auth";
|
||||
import { hasPermission } from "@dokploy/server/services/permission";
|
||||
import { Rocket } from "lucide-react";
|
||||
import type { GetServerSidePropsContext } from "next";
|
||||
import { useRouter } from "next/router";
|
||||
@@ -79,7 +80,7 @@ DeploymentsPage.getLayout = (page: ReactElement) => {
|
||||
};
|
||||
|
||||
export async function getServerSideProps(ctx: GetServerSidePropsContext) {
|
||||
const { user } = await validateRequest(ctx.req);
|
||||
const { user, session } = await validateRequest(ctx.req);
|
||||
if (!user) {
|
||||
return {
|
||||
redirect: {
|
||||
@@ -88,6 +89,24 @@ export async function getServerSideProps(ctx: GetServerSidePropsContext) {
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const canView = await hasPermission(
|
||||
{
|
||||
user: { id: user.id },
|
||||
session: { activeOrganizationId: session?.activeOrganizationId || "" },
|
||||
},
|
||||
{ deployment: ["read"] },
|
||||
);
|
||||
|
||||
if (!canView) {
|
||||
return {
|
||||
redirect: {
|
||||
permanent: false,
|
||||
destination: "/dashboard/projects",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
props: {},
|
||||
};
|
||||
|
||||
@@ -53,19 +53,15 @@ export async function getServerSideProps(
|
||||
try {
|
||||
await helpers.project.all.prefetch();
|
||||
|
||||
if (user.role === "member") {
|
||||
const userR = await helpers.user.one.fetch({
|
||||
userId: user.id,
|
||||
});
|
||||
const userPermissions = await helpers.user.getPermissions.fetch();
|
||||
|
||||
if (!userR?.canAccessToDocker) {
|
||||
return {
|
||||
redirect: {
|
||||
permanent: true,
|
||||
destination: "/",
|
||||
},
|
||||
};
|
||||
}
|
||||
if (!userPermissions?.docker.read) {
|
||||
return {
|
||||
redirect: {
|
||||
permanent: true,
|
||||
destination: "/",
|
||||
},
|
||||
};
|
||||
}
|
||||
return {
|
||||
props: {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { IS_CLOUD } from "@dokploy/server/constants";
|
||||
import { validateRequest } from "@dokploy/server/lib/auth";
|
||||
import { hasPermission } from "@dokploy/server/services/permission";
|
||||
import { Loader2 } from "lucide-react";
|
||||
import type { GetServerSidePropsContext } from "next";
|
||||
import type { ReactElement } from "react";
|
||||
@@ -99,7 +100,7 @@ export async function getServerSideProps(
|
||||
},
|
||||
};
|
||||
}
|
||||
const { user } = await validateRequest(ctx.req);
|
||||
const { user, session } = await validateRequest(ctx.req);
|
||||
if (!user) {
|
||||
return {
|
||||
redirect: {
|
||||
@@ -109,6 +110,23 @@ export async function getServerSideProps(
|
||||
};
|
||||
}
|
||||
|
||||
const canView = await hasPermission(
|
||||
{
|
||||
user: { id: user.id },
|
||||
session: { activeOrganizationId: session?.activeOrganizationId || "" },
|
||||
},
|
||||
{ monitoring: ["read"] },
|
||||
);
|
||||
|
||||
if (!canView) {
|
||||
return {
|
||||
redirect: {
|
||||
permanent: false,
|
||||
destination: "/dashboard/projects",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
props: {},
|
||||
};
|
||||
|
||||
@@ -272,6 +272,7 @@ const EnvironmentPage = (
|
||||
const [isBulkActionLoading, setIsBulkActionLoading] = useState(false);
|
||||
const { projectId, environmentId } = props;
|
||||
const { data: auth } = api.user.get.useQuery();
|
||||
const { data: permissions } = api.user.getPermissions.useQuery();
|
||||
|
||||
const { data: environments } = api.environment.byProjectId.useQuery({
|
||||
projectId: projectId,
|
||||
@@ -905,9 +906,7 @@ const EnvironmentPage = (
|
||||
<ProjectEnvironment projectId={projectId}>
|
||||
<Button variant="outline">Project Environment</Button>
|
||||
</ProjectEnvironment>
|
||||
{(auth?.role === "owner" ||
|
||||
auth?.role === "admin" ||
|
||||
auth?.canCreateServices) && (
|
||||
{permissions?.service.create && (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button>
|
||||
@@ -1029,9 +1028,7 @@ const EnvironmentPage = (
|
||||
Stop
|
||||
</Button>
|
||||
</DialogAction>
|
||||
{(auth?.role === "owner" ||
|
||||
auth?.role === "admin" ||
|
||||
auth?.canDeleteServices) && (
|
||||
{permissions?.service.delete && (
|
||||
<>
|
||||
<DialogAction
|
||||
title="Delete Services"
|
||||
@@ -1624,6 +1621,7 @@ export async function getServerSideProps(
|
||||
environmentId: params.environmentId,
|
||||
});
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
// If user doesn't have access to requested environment, redirect to accessible one
|
||||
const accessibleEnvironments =
|
||||
await helpers.environment.byProjectId.fetch({
|
||||
@@ -1643,11 +1641,11 @@ export async function getServerSideProps(
|
||||
},
|
||||
};
|
||||
}
|
||||
// No accessible environments, redirect to home
|
||||
// No accessible environments, redirect to projects
|
||||
return {
|
||||
redirect: {
|
||||
permanent: false,
|
||||
destination: "/",
|
||||
destination: "/dashboard/projects",
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -1663,7 +1661,8 @@ export async function getServerSideProps(
|
||||
environmentId: params.environmentId,
|
||||
},
|
||||
};
|
||||
} catch {
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
return {
|
||||
redirect: {
|
||||
permanent: false,
|
||||
|
||||
@@ -92,6 +92,7 @@ const Service = (
|
||||
|
||||
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||
const { data: auth } = api.user.get.useQuery();
|
||||
const { data: permissions } = api.user.getPermissions.useQuery();
|
||||
|
||||
const { data: environments } = api.environment.byProjectId.useQuery({
|
||||
projectId: data?.environment?.project?.projectId || "",
|
||||
@@ -197,10 +198,10 @@ const Service = (
|
||||
</div>
|
||||
|
||||
<div className="flex flex-row gap-2 justify-end">
|
||||
<UpdateApplication applicationId={applicationId} />
|
||||
{(auth?.role === "owner" ||
|
||||
auth?.role === "admin" ||
|
||||
auth?.canDeleteServices) && (
|
||||
{permissions?.service.create && (
|
||||
<UpdateApplication applicationId={applicationId} />
|
||||
)}
|
||||
{permissions?.service.delete && (
|
||||
<DeleteService id={applicationId} type="application" />
|
||||
)}
|
||||
</div>
|
||||
@@ -242,24 +243,47 @@ const Service = (
|
||||
<div className="flex flex-row items-center justify-between w-full overflow-auto">
|
||||
<TabsList className="flex gap-8 max-md:gap-4 justify-start">
|
||||
<TabsTrigger value="general">General</TabsTrigger>
|
||||
<TabsTrigger value="environment">Environment</TabsTrigger>
|
||||
<TabsTrigger value="domains">Domains</TabsTrigger>
|
||||
<TabsTrigger value="deployments">Deployments</TabsTrigger>
|
||||
<TabsTrigger value="preview-deployments">
|
||||
Preview Deployments
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="schedules">Schedules</TabsTrigger>
|
||||
<TabsTrigger value="volume-backups">
|
||||
Volume Backups
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="logs">Logs</TabsTrigger>
|
||||
{permissions?.envVars.read && (
|
||||
<TabsTrigger value="environment">
|
||||
Environment
|
||||
</TabsTrigger>
|
||||
)}
|
||||
{permissions?.domain.read && (
|
||||
<TabsTrigger value="domains">Domains</TabsTrigger>
|
||||
)}
|
||||
{permissions?.deployment.read && (
|
||||
<TabsTrigger value="deployments">
|
||||
Deployments
|
||||
</TabsTrigger>
|
||||
)}
|
||||
{permissions?.deployment.read && (
|
||||
<TabsTrigger value="preview-deployments">
|
||||
Preview Deployments
|
||||
</TabsTrigger>
|
||||
)}
|
||||
{permissions?.schedule.read && (
|
||||
<TabsTrigger value="schedules">Schedules</TabsTrigger>
|
||||
)}
|
||||
{permissions?.volumeBackup.read && (
|
||||
<TabsTrigger value="volume-backups">
|
||||
Volume Backups
|
||||
</TabsTrigger>
|
||||
)}
|
||||
{permissions?.logs.read && (
|
||||
<TabsTrigger value="logs">Logs</TabsTrigger>
|
||||
)}
|
||||
{data?.sourceType !== "docker" && (
|
||||
<TabsTrigger value="patches">Patches</TabsTrigger>
|
||||
)}
|
||||
{((data?.serverId && isCloud) || !data?.server) && (
|
||||
<TabsTrigger value="monitoring">Monitoring</TabsTrigger>
|
||||
{permissions?.monitoring.read &&
|
||||
((data?.serverId && isCloud) || !data?.server) && (
|
||||
<TabsTrigger value="monitoring">
|
||||
Monitoring
|
||||
</TabsTrigger>
|
||||
)}
|
||||
{permissions?.service.create && (
|
||||
<TabsTrigger value="advanced">Advanced</TabsTrigger>
|
||||
)}
|
||||
<TabsTrigger value="advanced">Advanced</TabsTrigger>
|
||||
</TabsList>
|
||||
</div>
|
||||
|
||||
@@ -268,26 +292,29 @@ const Service = (
|
||||
<ShowGeneralApplication applicationId={applicationId} />
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent value="environment">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<ShowEnvironment applicationId={applicationId} />
|
||||
</div>
|
||||
</TabsContent>
|
||||
{permissions?.envVars.read && (
|
||||
<TabsContent value="environment">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<ShowEnvironment applicationId={applicationId} />
|
||||
</div>
|
||||
</TabsContent>
|
||||
)}
|
||||
|
||||
<TabsContent value="monitoring">
|
||||
<div className="pt-2.5">
|
||||
<div className="flex flex-col gap-4 border rounded-lg p-6">
|
||||
{data?.serverId && isCloud ? (
|
||||
<ContainerPaidMonitoring
|
||||
appName={data?.appName || ""}
|
||||
baseUrl={`${data?.serverId ? `http://${data?.server?.ipAddress}:${data?.server?.metricsConfig?.server?.port}` : "http://localhost:4500"}`}
|
||||
token={
|
||||
data?.server?.metricsConfig?.server?.token || ""
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
{/* {monitoring?.enabledFeatures &&
|
||||
{permissions?.monitoring.read && (
|
||||
<TabsContent value="monitoring">
|
||||
<div className="pt-2.5">
|
||||
<div className="flex flex-col gap-4 border rounded-lg p-6">
|
||||
{data?.serverId && isCloud ? (
|
||||
<ContainerPaidMonitoring
|
||||
appName={data?.appName || ""}
|
||||
baseUrl={`${data?.serverId ? `http://${data?.server?.ipAddress}:${data?.server?.metricsConfig?.server?.port}` : "http://localhost:4500"}`}
|
||||
token={
|
||||
data?.server?.metricsConfig?.server?.token || ""
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
{/* {monitoring?.enabledFeatures &&
|
||||
isCloud &&
|
||||
data?.serverId && (
|
||||
<div className="flex flex-row border w-fit p-4 rounded-lg items-center gap-2">
|
||||
@@ -301,7 +328,7 @@ const Service = (
|
||||
</div>
|
||||
)} */}
|
||||
|
||||
{/* {toggleMonitoring ? (
|
||||
{/* {toggleMonitoring ? (
|
||||
<ContainerPaidMonitoring
|
||||
appName={data?.appName || ""}
|
||||
baseUrl={`http://${monitoring?.serverIp}:${monitoring?.metricsConfig?.server?.port}`}
|
||||
@@ -310,84 +337,102 @@ const Service = (
|
||||
}
|
||||
/>
|
||||
) : ( */}
|
||||
<div>
|
||||
<ContainerFreeMonitoring
|
||||
appName={data?.appName || ""}
|
||||
/>
|
||||
</div>
|
||||
{/* )} */}
|
||||
</>
|
||||
)}
|
||||
<div>
|
||||
<ContainerFreeMonitoring
|
||||
appName={data?.appName || ""}
|
||||
/>
|
||||
</div>
|
||||
{/* )} */}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</TabsContent>
|
||||
</TabsContent>
|
||||
)}
|
||||
|
||||
<TabsContent value="logs">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<ShowDockerLogs
|
||||
appName={data?.appName || ""}
|
||||
serverId={data?.serverId || ""}
|
||||
/>
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent value="schedules">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<ShowSchedules
|
||||
id={applicationId}
|
||||
scheduleType="application"
|
||||
/>
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent value="deployments" className="w-full pt-2.5">
|
||||
<div className="flex flex-col gap-4 border rounded-lg">
|
||||
<ShowDeployments
|
||||
id={applicationId}
|
||||
type="application"
|
||||
serverId={data?.serverId || ""}
|
||||
refreshToken={data?.refreshToken || ""}
|
||||
/>
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent value="volume-backups" className="w-full pt-2.5">
|
||||
<div className="flex flex-col gap-4 border rounded-lg">
|
||||
<ShowVolumeBackups
|
||||
id={applicationId}
|
||||
type="application"
|
||||
serverId={data?.serverId || ""}
|
||||
/>
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent value="preview-deployments" className="w-full">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<ShowPreviewDeployments applicationId={applicationId} />
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent value="domains" className="w-full">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<ShowDomains id={applicationId} type="application" />
|
||||
</div>
|
||||
</TabsContent>
|
||||
{permissions?.logs.read && (
|
||||
<TabsContent value="logs">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<ShowDockerLogs
|
||||
appName={data?.appName || ""}
|
||||
serverId={data?.serverId || ""}
|
||||
/>
|
||||
</div>
|
||||
</TabsContent>
|
||||
)}
|
||||
{permissions?.schedule.read && (
|
||||
<TabsContent value="schedules">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<ShowSchedules
|
||||
id={applicationId}
|
||||
scheduleType="application"
|
||||
/>
|
||||
</div>
|
||||
</TabsContent>
|
||||
)}
|
||||
{permissions?.deployment.read && (
|
||||
<TabsContent value="deployments" className="w-full pt-2.5">
|
||||
<div className="flex flex-col gap-4 border rounded-lg">
|
||||
<ShowDeployments
|
||||
id={applicationId}
|
||||
type="application"
|
||||
serverId={data?.serverId || ""}
|
||||
refreshToken={data?.refreshToken || ""}
|
||||
/>
|
||||
</div>
|
||||
</TabsContent>
|
||||
)}
|
||||
{permissions?.volumeBackup.read && (
|
||||
<TabsContent
|
||||
value="volume-backups"
|
||||
className="w-full pt-2.5"
|
||||
>
|
||||
<div className="flex flex-col gap-4 border rounded-lg">
|
||||
<ShowVolumeBackups
|
||||
id={applicationId}
|
||||
type="application"
|
||||
serverId={data?.serverId || ""}
|
||||
/>
|
||||
</div>
|
||||
</TabsContent>
|
||||
)}
|
||||
{permissions?.deployment.read && (
|
||||
<TabsContent value="preview-deployments" className="w-full">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<ShowPreviewDeployments applicationId={applicationId} />
|
||||
</div>
|
||||
</TabsContent>
|
||||
)}
|
||||
{permissions?.domain.read && (
|
||||
<TabsContent value="domains" className="w-full">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<ShowDomains id={applicationId} type="application" />
|
||||
</div>
|
||||
</TabsContent>
|
||||
)}
|
||||
<TabsContent value="patches" className="w-full">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<ShowPatches id={applicationId} type="application" />
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent value="advanced">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<AddCommand applicationId={applicationId} />
|
||||
<ShowClusterSettings
|
||||
id={applicationId}
|
||||
type="application"
|
||||
/>
|
||||
<ShowBuildServer applicationId={applicationId} />
|
||||
<ShowResources id={applicationId} type="application" />
|
||||
<ShowVolumes id={applicationId} type="application" />
|
||||
<ShowRedirects applicationId={applicationId} />
|
||||
<ShowSecurity applicationId={applicationId} />
|
||||
<ShowPorts applicationId={applicationId} />
|
||||
<ShowTraefikConfig applicationId={applicationId} />
|
||||
</div>
|
||||
</TabsContent>
|
||||
{permissions?.service.create && (
|
||||
<TabsContent value="advanced">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<AddCommand applicationId={applicationId} />
|
||||
<ShowClusterSettings
|
||||
id={applicationId}
|
||||
type="application"
|
||||
/>
|
||||
<ShowBuildServer applicationId={applicationId} />
|
||||
<ShowResources id={applicationId} type="application" />
|
||||
<ShowVolumes id={applicationId} type="application" />
|
||||
<ShowRedirects applicationId={applicationId} />
|
||||
<ShowSecurity applicationId={applicationId} />
|
||||
<ShowPorts applicationId={applicationId} />
|
||||
<ShowTraefikConfig applicationId={applicationId} />
|
||||
</div>
|
||||
</TabsContent>
|
||||
)}
|
||||
</Tabs>
|
||||
)}
|
||||
</CardContent>
|
||||
|
||||
@@ -81,6 +81,7 @@ const Service = (
|
||||
const { data } = api.compose.one.useQuery({ composeId });
|
||||
|
||||
const { data: auth } = api.user.get.useQuery();
|
||||
const { data: permissions } = api.user.getPermissions.useQuery();
|
||||
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||
const { data: environments } = api.environment.byProjectId.useQuery({
|
||||
projectId: data?.environment?.projectId || "",
|
||||
@@ -185,11 +186,11 @@ const Service = (
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-row gap-2 justify-end">
|
||||
<UpdateCompose composeId={composeId} />
|
||||
{permissions?.service.create && (
|
||||
<UpdateCompose composeId={composeId} />
|
||||
)}
|
||||
|
||||
{(auth?.role === "owner" ||
|
||||
auth?.role === "admin" ||
|
||||
auth?.canDeleteServices) && (
|
||||
{permissions?.service.delete && (
|
||||
<DeleteService id={composeId} type="compose" />
|
||||
)}
|
||||
</div>
|
||||
@@ -232,22 +233,45 @@ const Service = (
|
||||
<div className="flex flex-row items-center w-full overflow-auto">
|
||||
<TabsList className="flex gap-8 max-md:gap-4 justify-start">
|
||||
<TabsTrigger value="general">General</TabsTrigger>
|
||||
<TabsTrigger value="environment">Environment</TabsTrigger>
|
||||
<TabsTrigger value="domains">Domains</TabsTrigger>
|
||||
<TabsTrigger value="deployments">Deployments</TabsTrigger>
|
||||
<TabsTrigger value="backups">Backups</TabsTrigger>
|
||||
<TabsTrigger value="schedules">Schedules</TabsTrigger>
|
||||
<TabsTrigger value="volumeBackups">
|
||||
Volume Backups
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="logs">Logs</TabsTrigger>
|
||||
{permissions?.envVars.read && (
|
||||
<TabsTrigger value="environment">
|
||||
Environment
|
||||
</TabsTrigger>
|
||||
)}
|
||||
{permissions?.domain.read && (
|
||||
<TabsTrigger value="domains">Domains</TabsTrigger>
|
||||
)}
|
||||
{permissions?.deployment.read && (
|
||||
<TabsTrigger value="deployments">
|
||||
Deployments
|
||||
</TabsTrigger>
|
||||
)}
|
||||
{permissions?.service.create && (
|
||||
<TabsTrigger value="backups">Backups</TabsTrigger>
|
||||
)}
|
||||
{permissions?.schedule.read && (
|
||||
<TabsTrigger value="schedules">Schedules</TabsTrigger>
|
||||
)}
|
||||
{permissions?.volumeBackup.read && (
|
||||
<TabsTrigger value="volumeBackups">
|
||||
Volume Backups
|
||||
</TabsTrigger>
|
||||
)}
|
||||
{permissions?.logs.read && (
|
||||
<TabsTrigger value="logs">Logs</TabsTrigger>
|
||||
)}
|
||||
{data?.sourceType !== "raw" && (
|
||||
<TabsTrigger value="patches">Patches</TabsTrigger>
|
||||
)}
|
||||
{((data?.serverId && isCloud) || !data?.server) && (
|
||||
<TabsTrigger value="monitoring">Monitoring</TabsTrigger>
|
||||
{permissions?.monitoring.read &&
|
||||
((data?.serverId && isCloud) || !data?.server) && (
|
||||
<TabsTrigger value="monitoring">
|
||||
Monitoring
|
||||
</TabsTrigger>
|
||||
)}
|
||||
{permissions?.service.create && (
|
||||
<TabsTrigger value="advanced">Advanced</TabsTrigger>
|
||||
)}
|
||||
<TabsTrigger value="advanced">Advanced</TabsTrigger>
|
||||
</TabsList>
|
||||
</div>
|
||||
|
||||
@@ -256,47 +280,56 @@ const Service = (
|
||||
<ShowGeneralCompose composeId={composeId} />
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent value="environment">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<ShowEnvironment id={composeId} type="compose" />
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent value="backups">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<ShowBackups id={composeId} backupType="compose" />
|
||||
</div>
|
||||
</TabsContent>
|
||||
{permissions?.envVars.read && (
|
||||
<TabsContent value="environment">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<ShowEnvironment id={composeId} type="compose" />
|
||||
</div>
|
||||
</TabsContent>
|
||||
)}
|
||||
{permissions?.service.create && (
|
||||
<TabsContent value="backups">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<ShowBackups id={composeId} backupType="compose" />
|
||||
</div>
|
||||
</TabsContent>
|
||||
)}
|
||||
|
||||
<TabsContent value="schedules">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<ShowSchedules id={composeId} scheduleType="compose" />
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent value="volumeBackups">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<ShowVolumeBackups
|
||||
id={composeId}
|
||||
type="compose"
|
||||
serverId={data?.serverId || ""}
|
||||
/>
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent value="monitoring">
|
||||
<div className="pt-2.5">
|
||||
<div className="flex flex-col border rounded-lg ">
|
||||
{data?.serverId && isCloud ? (
|
||||
<ComposePaidMonitoring
|
||||
serverId={data?.serverId || ""}
|
||||
baseUrl={`${data?.serverId ? `http://${data?.server?.ipAddress}:${data?.server?.metricsConfig?.server?.port}` : "http://localhost:4500"}`}
|
||||
appName={data?.appName || ""}
|
||||
token={
|
||||
data?.server?.metricsConfig?.server?.token || ""
|
||||
}
|
||||
appType={data?.composeType || "docker-compose"}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
{/* {monitoring?.enabledFeatures &&
|
||||
{permissions?.schedule.read && (
|
||||
<TabsContent value="schedules">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<ShowSchedules id={composeId} scheduleType="compose" />
|
||||
</div>
|
||||
</TabsContent>
|
||||
)}
|
||||
{permissions?.volumeBackup.read && (
|
||||
<TabsContent value="volumeBackups">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<ShowVolumeBackups
|
||||
id={composeId}
|
||||
type="compose"
|
||||
serverId={data?.serverId || ""}
|
||||
/>
|
||||
</div>
|
||||
</TabsContent>
|
||||
)}
|
||||
{permissions?.monitoring.read && (
|
||||
<TabsContent value="monitoring">
|
||||
<div className="pt-2.5">
|
||||
<div className="flex flex-col border rounded-lg ">
|
||||
{data?.serverId && isCloud ? (
|
||||
<ComposePaidMonitoring
|
||||
serverId={data?.serverId || ""}
|
||||
baseUrl={`${data?.serverId ? `http://${data?.server?.ipAddress}:${data?.server?.metricsConfig?.server?.port}` : "http://localhost:4500"}`}
|
||||
appName={data?.appName || ""}
|
||||
token={
|
||||
data?.server?.metricsConfig?.server?.token || ""
|
||||
}
|
||||
appType={data?.composeType || "docker-compose"}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
{/* {monitoring?.enabledFeatures &&
|
||||
isCloud &&
|
||||
data?.serverId && (
|
||||
<div className="flex flex-row border w-fit p-4 rounded-lg items-center gap-2 m-4">
|
||||
@@ -320,53 +353,60 @@ const Service = (
|
||||
appType={data?.composeType || "docker-compose"}
|
||||
/>
|
||||
) : ( */}
|
||||
{/* <div> */}
|
||||
<ComposeFreeMonitoring
|
||||
serverId={data?.serverId || ""}
|
||||
appName={data?.appName || ""}
|
||||
appType={data?.composeType || "docker-compose"}
|
||||
/>
|
||||
{/* </div> */}
|
||||
{/* )} */}
|
||||
</>
|
||||
{/* <div> */}
|
||||
<ComposeFreeMonitoring
|
||||
serverId={data?.serverId || ""}
|
||||
appName={data?.appName || ""}
|
||||
appType={data?.composeType || "docker-compose"}
|
||||
/>
|
||||
{/* </div> */}
|
||||
{/* )} */}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</TabsContent>
|
||||
)}
|
||||
|
||||
{permissions?.logs.read && (
|
||||
<TabsContent value="logs">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
{data?.composeType === "docker-compose" ? (
|
||||
<ShowDockerLogsCompose
|
||||
serverId={data?.serverId || ""}
|
||||
appName={data?.appName || ""}
|
||||
appType={data?.composeType || "docker-compose"}
|
||||
/>
|
||||
) : (
|
||||
<ShowDockerLogsStack
|
||||
serverId={data?.serverId || ""}
|
||||
appName={data?.appName || ""}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</TabsContent>
|
||||
</TabsContent>
|
||||
)}
|
||||
|
||||
<TabsContent value="logs">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
{data?.composeType === "docker-compose" ? (
|
||||
<ShowDockerLogsCompose
|
||||
{permissions?.deployment.read && (
|
||||
<TabsContent value="deployments" className="w-full pt-2.5">
|
||||
<div className="flex flex-col gap-4 border rounded-lg">
|
||||
<ShowDeployments
|
||||
id={composeId}
|
||||
type="compose"
|
||||
serverId={data?.serverId || ""}
|
||||
appName={data?.appName || ""}
|
||||
appType={data?.composeType || "docker-compose"}
|
||||
refreshToken={data?.refreshToken || ""}
|
||||
/>
|
||||
) : (
|
||||
<ShowDockerLogsStack
|
||||
serverId={data?.serverId || ""}
|
||||
appName={data?.appName || ""}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</TabsContent>
|
||||
</div>
|
||||
</TabsContent>
|
||||
)}
|
||||
|
||||
<TabsContent value="deployments" className="w-full pt-2.5">
|
||||
<div className="flex flex-col gap-4 border rounded-lg">
|
||||
<ShowDeployments
|
||||
id={composeId}
|
||||
type="compose"
|
||||
serverId={data?.serverId || ""}
|
||||
refreshToken={data?.refreshToken || ""}
|
||||
/>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="domains">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<ShowDomains id={composeId} type="compose" />
|
||||
</div>
|
||||
</TabsContent>
|
||||
{permissions?.domain.read && (
|
||||
<TabsContent value="domains">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<ShowDomains id={composeId} type="compose" />
|
||||
</div>
|
||||
</TabsContent>
|
||||
)}
|
||||
|
||||
<TabsContent value="patches" className="w-full">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
@@ -374,14 +414,16 @@ const Service = (
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="advanced">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<AddCommandCompose composeId={composeId} />
|
||||
<ShowVolumes id={composeId} type="compose" />
|
||||
<ShowImport composeId={composeId} />
|
||||
<IsolatedDeploymentTab composeId={composeId} />
|
||||
</div>
|
||||
</TabsContent>
|
||||
{permissions?.service.create && (
|
||||
<TabsContent value="advanced">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<AddCommandCompose composeId={composeId} />
|
||||
<ShowVolumes id={composeId} type="compose" />
|
||||
<ShowImport composeId={composeId} />
|
||||
<IsolatedDeploymentTab composeId={composeId} />
|
||||
</div>
|
||||
</TabsContent>
|
||||
)}
|
||||
</Tabs>
|
||||
)}
|
||||
</CardContent>
|
||||
|
||||
@@ -60,6 +60,7 @@ const Mariadb = (
|
||||
const [tab, setSab] = useState<TabState>(activeTab);
|
||||
const { data } = api.mariadb.one.useQuery({ mariadbId });
|
||||
const { data: auth } = api.user.get.useQuery();
|
||||
const { data: permissions } = api.user.getPermissions.useQuery();
|
||||
|
||||
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||
|
||||
@@ -159,10 +160,10 @@ const Mariadb = (
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-row gap-2 justify-end">
|
||||
<UpdateMariadb mariadbId={mariadbId} />
|
||||
{(auth?.role === "owner" ||
|
||||
auth?.role === "admin" ||
|
||||
auth?.canDeleteServices) && (
|
||||
{permissions?.service.create && (
|
||||
<UpdateMariadb mariadbId={mariadbId} />
|
||||
)}
|
||||
{permissions?.service.delete && (
|
||||
<DeleteService id={mariadbId} type="mariadb" />
|
||||
)}
|
||||
</div>
|
||||
@@ -214,13 +215,24 @@ const Mariadb = (
|
||||
)}
|
||||
>
|
||||
<TabsTrigger value="general">General</TabsTrigger>
|
||||
<TabsTrigger value="environment">Environment</TabsTrigger>
|
||||
<TabsTrigger value="logs">Logs</TabsTrigger>
|
||||
{((data?.serverId && isCloud) || !data?.server) && (
|
||||
<TabsTrigger value="monitoring">Monitoring</TabsTrigger>
|
||||
{permissions?.envVars.read && (
|
||||
<TabsTrigger value="environment">
|
||||
Environment
|
||||
</TabsTrigger>
|
||||
)}
|
||||
{permissions?.logs.read && (
|
||||
<TabsTrigger value="logs">Logs</TabsTrigger>
|
||||
)}
|
||||
{permissions?.monitoring.read &&
|
||||
((data?.serverId && isCloud) || !data?.server) && (
|
||||
<TabsTrigger value="monitoring">
|
||||
Monitoring
|
||||
</TabsTrigger>
|
||||
)}
|
||||
<TabsTrigger value="backups">Backups</TabsTrigger>
|
||||
<TabsTrigger value="advanced">Advanced</TabsTrigger>
|
||||
{permissions?.service.create && (
|
||||
<TabsTrigger value="advanced">Advanced</TabsTrigger>
|
||||
)}
|
||||
</TabsList>
|
||||
</div>
|
||||
|
||||
@@ -231,25 +243,28 @@ const Mariadb = (
|
||||
<ShowExternalMariadbCredentials mariadbId={mariadbId} />
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent value="environment">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<ShowEnvironment id={mariadbId} type="mariadb" />
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent value="monitoring">
|
||||
<div className="pt-2.5">
|
||||
<div className="flex flex-col gap-4 border rounded-lg p-6">
|
||||
{data?.serverId && isCloud ? (
|
||||
<ContainerPaidMonitoring
|
||||
appName={data?.appName || ""}
|
||||
baseUrl={`${data?.serverId ? `http://${data?.server?.ipAddress}:${data?.server?.metricsConfig?.server?.port}` : "http://localhost:4500"}`}
|
||||
token={
|
||||
data?.server?.metricsConfig?.server?.token || ""
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
{/* {monitoring?.enabledFeatures && (
|
||||
{permissions?.envVars.read && (
|
||||
<TabsContent value="environment">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<ShowEnvironment id={mariadbId} type="mariadb" />
|
||||
</div>
|
||||
</TabsContent>
|
||||
)}
|
||||
{permissions?.monitoring.read && (
|
||||
<TabsContent value="monitoring">
|
||||
<div className="pt-2.5">
|
||||
<div className="flex flex-col gap-4 border rounded-lg p-6">
|
||||
{data?.serverId && isCloud ? (
|
||||
<ContainerPaidMonitoring
|
||||
appName={data?.appName || ""}
|
||||
baseUrl={`${data?.serverId ? `http://${data?.server?.ipAddress}:${data?.server?.metricsConfig?.server?.port}` : "http://localhost:4500"}`}
|
||||
token={
|
||||
data?.server?.metricsConfig?.server?.token || ""
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
{/* {monitoring?.enabledFeatures && (
|
||||
<div className="flex flex-row border w-fit p-4 rounded-lg items-center gap-2">
|
||||
<Label className="text-muted-foreground">
|
||||
Change Monitoring
|
||||
@@ -271,37 +286,42 @@ const Mariadb = (
|
||||
/>
|
||||
) : (
|
||||
<div> */}
|
||||
<ContainerFreeMonitoring
|
||||
appName={data?.appName || ""}
|
||||
/>
|
||||
{/* </div> */}
|
||||
{/* )} */}
|
||||
</>
|
||||
)}
|
||||
<ContainerFreeMonitoring
|
||||
appName={data?.appName || ""}
|
||||
/>
|
||||
{/* </div> */}
|
||||
{/* )} */}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent value="logs">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<ShowDockerLogs
|
||||
serverId={data?.serverId || ""}
|
||||
appName={data?.appName || ""}
|
||||
/>
|
||||
</div>
|
||||
</TabsContent>
|
||||
</TabsContent>
|
||||
)}
|
||||
{permissions?.logs.read && (
|
||||
<TabsContent value="logs">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<ShowDockerLogs
|
||||
serverId={data?.serverId || ""}
|
||||
appName={data?.appName || ""}
|
||||
/>
|
||||
</div>
|
||||
</TabsContent>
|
||||
)}
|
||||
<TabsContent value="backups">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<ShowBackups id={mariadbId} databaseType="mariadb" />
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent value="advanced">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<ShowDatabaseAdvancedSettings
|
||||
id={mariadbId}
|
||||
type="mariadb"
|
||||
/>
|
||||
</div>
|
||||
</TabsContent>
|
||||
{permissions?.service.create && (
|
||||
<TabsContent value="advanced">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<ShowDatabaseAdvancedSettings
|
||||
id={mariadbId}
|
||||
type="mariadb"
|
||||
/>
|
||||
</div>
|
||||
</TabsContent>
|
||||
)}
|
||||
</Tabs>
|
||||
)}
|
||||
</CardContent>
|
||||
|
||||
@@ -60,6 +60,7 @@ const Mongo = (
|
||||
const { data } = api.mongo.one.useQuery({ mongoId });
|
||||
|
||||
const { data: auth } = api.user.get.useQuery();
|
||||
const { data: permissions } = api.user.getPermissions.useQuery();
|
||||
|
||||
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||
const { data: environments } = api.environment.byProjectId.useQuery({
|
||||
@@ -159,10 +160,10 @@ const Mongo = (
|
||||
</div>
|
||||
|
||||
<div className="flex flex-row gap-2 justify-end">
|
||||
<UpdateMongo mongoId={mongoId} />
|
||||
{(auth?.role === "owner" ||
|
||||
auth?.role === "admin" ||
|
||||
auth?.canDeleteServices) && (
|
||||
{permissions?.service.create && (
|
||||
<UpdateMongo mongoId={mongoId} />
|
||||
)}
|
||||
{permissions?.service.delete && (
|
||||
<DeleteService id={mongoId} type="mongo" />
|
||||
)}
|
||||
</div>
|
||||
@@ -214,13 +215,24 @@ const Mongo = (
|
||||
)}
|
||||
>
|
||||
<TabsTrigger value="general">General</TabsTrigger>
|
||||
<TabsTrigger value="environment">Environment</TabsTrigger>
|
||||
<TabsTrigger value="logs">Logs</TabsTrigger>
|
||||
{((data?.serverId && isCloud) || !data?.server) && (
|
||||
<TabsTrigger value="monitoring">Monitoring</TabsTrigger>
|
||||
{permissions?.envVars.read && (
|
||||
<TabsTrigger value="environment">
|
||||
Environment
|
||||
</TabsTrigger>
|
||||
)}
|
||||
{permissions?.logs.read && (
|
||||
<TabsTrigger value="logs">Logs</TabsTrigger>
|
||||
)}
|
||||
{permissions?.monitoring.read &&
|
||||
((data?.serverId && isCloud) || !data?.server) && (
|
||||
<TabsTrigger value="monitoring">
|
||||
Monitoring
|
||||
</TabsTrigger>
|
||||
)}
|
||||
<TabsTrigger value="backups">Backups</TabsTrigger>
|
||||
<TabsTrigger value="advanced">Advanced</TabsTrigger>
|
||||
{permissions?.service.create && (
|
||||
<TabsTrigger value="advanced">Advanced</TabsTrigger>
|
||||
)}
|
||||
</TabsList>
|
||||
</div>
|
||||
|
||||
@@ -231,25 +243,28 @@ const Mongo = (
|
||||
<ShowExternalMongoCredentials mongoId={mongoId} />
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent value="environment">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<ShowEnvironment id={mongoId} type="mongo" />
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent value="monitoring">
|
||||
<div className="pt-2.5">
|
||||
<div className="flex flex-col gap-4 border rounded-lg p-6">
|
||||
{data?.serverId && isCloud ? (
|
||||
<ContainerPaidMonitoring
|
||||
appName={data?.appName || ""}
|
||||
baseUrl={`${data?.serverId ? `http://${data?.server?.ipAddress}:${data?.server?.metricsConfig?.server?.port}` : "http://localhost:4500"}`}
|
||||
token={
|
||||
data?.server?.metricsConfig?.server?.token || ""
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
{/* {monitoring?.enabledFeatures && (
|
||||
{permissions?.envVars.read && (
|
||||
<TabsContent value="environment">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<ShowEnvironment id={mongoId} type="mongo" />
|
||||
</div>
|
||||
</TabsContent>
|
||||
)}
|
||||
{permissions?.monitoring.read && (
|
||||
<TabsContent value="monitoring">
|
||||
<div className="pt-2.5">
|
||||
<div className="flex flex-col gap-4 border rounded-lg p-6">
|
||||
{data?.serverId && isCloud ? (
|
||||
<ContainerPaidMonitoring
|
||||
appName={data?.appName || ""}
|
||||
baseUrl={`${data?.serverId ? `http://${data?.server?.ipAddress}:${data?.server?.metricsConfig?.server?.port}` : "http://localhost:4500"}`}
|
||||
token={
|
||||
data?.server?.metricsConfig?.server?.token || ""
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
{/* {monitoring?.enabledFeatures && (
|
||||
<div className="flex flex-row border w-fit p-4 rounded-lg items-center gap-2">
|
||||
<Label className="text-muted-foreground">
|
||||
Change Monitoring
|
||||
@@ -271,24 +286,27 @@ const Mongo = (
|
||||
/>
|
||||
) : (
|
||||
<div> */}
|
||||
<ContainerFreeMonitoring
|
||||
appName={data?.appName || ""}
|
||||
/>
|
||||
{/* </div> */}
|
||||
{/* )} */}
|
||||
</>
|
||||
)}
|
||||
<ContainerFreeMonitoring
|
||||
appName={data?.appName || ""}
|
||||
/>
|
||||
{/* </div> */}
|
||||
{/* )} */}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent value="logs">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<ShowDockerLogs
|
||||
serverId={data?.serverId || ""}
|
||||
appName={data?.appName || ""}
|
||||
/>
|
||||
</div>
|
||||
</TabsContent>
|
||||
</TabsContent>
|
||||
)}
|
||||
{permissions?.logs.read && (
|
||||
<TabsContent value="logs">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<ShowDockerLogs
|
||||
serverId={data?.serverId || ""}
|
||||
appName={data?.appName || ""}
|
||||
/>
|
||||
</div>
|
||||
</TabsContent>
|
||||
)}
|
||||
<TabsContent value="backups">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<ShowBackups
|
||||
@@ -298,11 +316,16 @@ const Mongo = (
|
||||
/>
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent value="advanced">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<ShowDatabaseAdvancedSettings id={mongoId} type="mongo" />
|
||||
</div>
|
||||
</TabsContent>
|
||||
{permissions?.service.create && (
|
||||
<TabsContent value="advanced">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<ShowDatabaseAdvancedSettings
|
||||
id={mongoId}
|
||||
type="mongo"
|
||||
/>
|
||||
</div>
|
||||
</TabsContent>
|
||||
)}
|
||||
</Tabs>
|
||||
)}
|
||||
</CardContent>
|
||||
|
||||
@@ -59,6 +59,7 @@ const MySql = (
|
||||
const [tab, setSab] = useState<TabState>(activeTab);
|
||||
const { data } = api.mysql.one.useQuery({ mysqlId });
|
||||
const { data: auth } = api.user.get.useQuery();
|
||||
const { data: permissions } = api.user.getPermissions.useQuery();
|
||||
|
||||
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||
const { data: environments } = api.environment.byProjectId.useQuery({
|
||||
@@ -159,10 +160,10 @@ const MySql = (
|
||||
</div>
|
||||
|
||||
<div className="flex flex-row gap-2 justify-end">
|
||||
<UpdateMysql mysqlId={mysqlId} />
|
||||
{(auth?.role === "owner" ||
|
||||
auth?.role === "admin" ||
|
||||
auth?.canDeleteServices) && (
|
||||
{permissions?.service.create && (
|
||||
<UpdateMysql mysqlId={mysqlId} />
|
||||
)}
|
||||
{permissions?.service.delete && (
|
||||
<DeleteService id={mysqlId} type="mysql" />
|
||||
)}
|
||||
</div>
|
||||
@@ -214,17 +215,24 @@ const MySql = (
|
||||
)}
|
||||
>
|
||||
<TabsTrigger value="general">General</TabsTrigger>
|
||||
<TabsTrigger value="environment">
|
||||
Environment
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="logs">Logs</TabsTrigger>
|
||||
{((data?.serverId && isCloud) || !data?.server) && (
|
||||
<TabsTrigger value="monitoring">
|
||||
Monitoring
|
||||
{permissions?.envVars.read && (
|
||||
<TabsTrigger value="environment">
|
||||
Environment
|
||||
</TabsTrigger>
|
||||
)}
|
||||
{permissions?.logs.read && (
|
||||
<TabsTrigger value="logs">Logs</TabsTrigger>
|
||||
)}
|
||||
{permissions?.monitoring.read &&
|
||||
((data?.serverId && isCloud) || !data?.server) && (
|
||||
<TabsTrigger value="monitoring">
|
||||
Monitoring
|
||||
</TabsTrigger>
|
||||
)}
|
||||
<TabsTrigger value="backups">Backups</TabsTrigger>
|
||||
<TabsTrigger value="advanced">Advanced</TabsTrigger>
|
||||
{permissions?.service.create && (
|
||||
<TabsTrigger value="advanced">Advanced</TabsTrigger>
|
||||
)}
|
||||
</TabsList>
|
||||
</div>
|
||||
|
||||
@@ -235,40 +243,47 @@ const MySql = (
|
||||
<ShowExternalMysqlCredentials mysqlId={mysqlId} />
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent value="environment" className="w-full">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<ShowEnvironment id={mysqlId} type="mysql" />
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent value="monitoring">
|
||||
<div className="pt-2.5">
|
||||
<div className="flex flex-col gap-4 border rounded-lg p-6">
|
||||
{data?.serverId && isCloud ? (
|
||||
<ContainerPaidMonitoring
|
||||
appName={data?.appName || ""}
|
||||
baseUrl={`${data?.serverId ? `http://${data?.server?.ipAddress}:${data?.server?.metricsConfig?.server?.port}` : "http://localhost:4500"}`}
|
||||
token={
|
||||
data?.server?.metricsConfig?.server?.token || ""
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<ContainerFreeMonitoring
|
||||
appName={data?.appName || ""}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{permissions?.envVars.read && (
|
||||
<TabsContent value="environment" className="w-full">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<ShowEnvironment id={mysqlId} type="mysql" />
|
||||
</div>
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent value="logs">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<ShowDockerLogs
|
||||
serverId={data?.serverId || ""}
|
||||
appName={data?.appName || ""}
|
||||
/>
|
||||
</div>
|
||||
</TabsContent>
|
||||
</TabsContent>
|
||||
)}
|
||||
{permissions?.monitoring.read && (
|
||||
<TabsContent value="monitoring">
|
||||
<div className="pt-2.5">
|
||||
<div className="flex flex-col gap-4 border rounded-lg p-6">
|
||||
{data?.serverId && isCloud ? (
|
||||
<ContainerPaidMonitoring
|
||||
appName={data?.appName || ""}
|
||||
baseUrl={`${data?.serverId ? `http://${data?.server?.ipAddress}:${data?.server?.metricsConfig?.server?.port}` : "http://localhost:4500"}`}
|
||||
token={
|
||||
data?.server?.metricsConfig?.server?.token ||
|
||||
""
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<ContainerFreeMonitoring
|
||||
appName={data?.appName || ""}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</TabsContent>
|
||||
)}
|
||||
{permissions?.logs.read && (
|
||||
<TabsContent value="logs">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<ShowDockerLogs
|
||||
serverId={data?.serverId || ""}
|
||||
appName={data?.appName || ""}
|
||||
/>
|
||||
</div>
|
||||
</TabsContent>
|
||||
)}
|
||||
<TabsContent value="backups">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<ShowBackups
|
||||
@@ -278,14 +293,16 @@ const MySql = (
|
||||
/>
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent value="advanced">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<ShowDatabaseAdvancedSettings
|
||||
id={mysqlId}
|
||||
type="mysql"
|
||||
/>
|
||||
</div>
|
||||
</TabsContent>
|
||||
{permissions?.service.create && (
|
||||
<TabsContent value="advanced">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<ShowDatabaseAdvancedSettings
|
||||
id={mysqlId}
|
||||
type="mysql"
|
||||
/>
|
||||
</div>
|
||||
</TabsContent>
|
||||
)}
|
||||
</Tabs>
|
||||
)}
|
||||
</CardContent>
|
||||
|
||||
@@ -59,6 +59,7 @@ const Postgresql = (
|
||||
const [tab, setSab] = useState<TabState>(activeTab);
|
||||
const { data } = api.postgres.one.useQuery({ postgresId });
|
||||
const { data: auth } = api.user.get.useQuery();
|
||||
const { data: permissions } = api.user.getPermissions.useQuery();
|
||||
|
||||
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||
const { data: environments } = api.environment.byProjectId.useQuery({
|
||||
@@ -158,10 +159,10 @@ const Postgresql = (
|
||||
</div>
|
||||
|
||||
<div className="flex flex-row gap-2 justify-end">
|
||||
<UpdatePostgres postgresId={postgresId} />
|
||||
{(auth?.role === "owner" ||
|
||||
auth?.role === "admin" ||
|
||||
auth?.canDeleteServices) && (
|
||||
{permissions?.service.create && (
|
||||
<UpdatePostgres postgresId={postgresId} />
|
||||
)}
|
||||
{permissions?.service.delete && (
|
||||
<DeleteService id={postgresId} type="postgres" />
|
||||
)}
|
||||
</div>
|
||||
@@ -215,13 +216,24 @@ const Postgresql = (
|
||||
)}
|
||||
>
|
||||
<TabsTrigger value="general">General</TabsTrigger>
|
||||
<TabsTrigger value="environment">Environment</TabsTrigger>
|
||||
<TabsTrigger value="logs">Logs</TabsTrigger>
|
||||
{((data?.serverId && isCloud) || !data?.server) && (
|
||||
<TabsTrigger value="monitoring">Monitoring</TabsTrigger>
|
||||
{permissions?.envVars.read && (
|
||||
<TabsTrigger value="environment">
|
||||
Environment
|
||||
</TabsTrigger>
|
||||
)}
|
||||
{permissions?.logs.read && (
|
||||
<TabsTrigger value="logs">Logs</TabsTrigger>
|
||||
)}
|
||||
{permissions?.monitoring.read &&
|
||||
((data?.serverId && isCloud) || !data?.server) && (
|
||||
<TabsTrigger value="monitoring">
|
||||
Monitoring
|
||||
</TabsTrigger>
|
||||
)}
|
||||
<TabsTrigger value="backups">Backups</TabsTrigger>
|
||||
<TabsTrigger value="advanced">Advanced</TabsTrigger>
|
||||
{permissions?.service.create && (
|
||||
<TabsTrigger value="advanced">Advanced</TabsTrigger>
|
||||
)}
|
||||
</TabsList>
|
||||
</div>
|
||||
|
||||
@@ -236,44 +248,50 @@ const Postgresql = (
|
||||
/>
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent value="environment">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<ShowEnvironment id={postgresId} type="postgres" />
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent value="monitoring">
|
||||
<div className="pt-2.5">
|
||||
<div className="flex flex-col gap-4 border rounded-lg p-6">
|
||||
{data?.serverId && isCloud ? (
|
||||
<ContainerPaidMonitoring
|
||||
appName={data?.appName || ""}
|
||||
baseUrl={`${
|
||||
data?.serverId
|
||||
? `http://${data?.server?.ipAddress}:${data?.server?.metricsConfig?.server?.port}`
|
||||
: "http://localhost:4500"
|
||||
}`}
|
||||
token={
|
||||
data?.server?.metricsConfig?.server?.token || ""
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<ContainerFreeMonitoring
|
||||
appName={data?.appName || ""}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{permissions?.envVars.read && (
|
||||
<TabsContent value="environment">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<ShowEnvironment id={postgresId} type="postgres" />
|
||||
</div>
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent value="logs">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<ShowDockerLogs
|
||||
serverId={data?.serverId || ""}
|
||||
appName={data?.appName || ""}
|
||||
/>
|
||||
</div>
|
||||
</TabsContent>
|
||||
</TabsContent>
|
||||
)}
|
||||
{permissions?.monitoring.read && (
|
||||
<TabsContent value="monitoring">
|
||||
<div className="pt-2.5">
|
||||
<div className="flex flex-col gap-4 border rounded-lg p-6">
|
||||
{data?.serverId && isCloud ? (
|
||||
<ContainerPaidMonitoring
|
||||
appName={data?.appName || ""}
|
||||
baseUrl={`${
|
||||
data?.serverId
|
||||
? `http://${data?.server?.ipAddress}:${data?.server?.metricsConfig?.server?.port}`
|
||||
: "http://localhost:4500"
|
||||
}`}
|
||||
token={
|
||||
data?.server?.metricsConfig?.server?.token || ""
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<ContainerFreeMonitoring
|
||||
appName={data?.appName || ""}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</TabsContent>
|
||||
)}
|
||||
{permissions?.logs.read && (
|
||||
<TabsContent value="logs">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<ShowDockerLogs
|
||||
serverId={data?.serverId || ""}
|
||||
appName={data?.appName || ""}
|
||||
/>
|
||||
</div>
|
||||
</TabsContent>
|
||||
)}
|
||||
<TabsContent value="backups">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<ShowBackups
|
||||
@@ -283,14 +301,16 @@ const Postgresql = (
|
||||
/>
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent value="advanced">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<ShowDatabaseAdvancedSettings
|
||||
id={postgresId}
|
||||
type="postgres"
|
||||
/>
|
||||
</div>
|
||||
</TabsContent>
|
||||
{permissions?.service.create && (
|
||||
<TabsContent value="advanced">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<ShowDatabaseAdvancedSettings
|
||||
id={postgresId}
|
||||
type="postgres"
|
||||
/>
|
||||
</div>
|
||||
</TabsContent>
|
||||
)}
|
||||
</Tabs>
|
||||
)}
|
||||
</CardContent>
|
||||
|
||||
@@ -59,6 +59,7 @@ const Redis = (
|
||||
const { data } = api.redis.one.useQuery({ redisId });
|
||||
|
||||
const { data: auth } = api.user.get.useQuery();
|
||||
const { data: permissions } = api.user.getPermissions.useQuery();
|
||||
|
||||
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||
const { data: environments } = api.environment.byProjectId.useQuery({
|
||||
@@ -158,10 +159,10 @@ const Redis = (
|
||||
</div>
|
||||
|
||||
<div className="flex flex-row gap-2 justify-end">
|
||||
<UpdateRedis redisId={redisId} />
|
||||
{(auth?.role === "owner" ||
|
||||
auth?.role === "admin" ||
|
||||
auth?.canDeleteServices) && (
|
||||
{permissions?.service.create && (
|
||||
<UpdateRedis redisId={redisId} />
|
||||
)}
|
||||
{permissions?.service.delete && (
|
||||
<DeleteService id={redisId} type="redis" />
|
||||
)}
|
||||
</div>
|
||||
@@ -213,12 +214,23 @@ const Redis = (
|
||||
)}
|
||||
>
|
||||
<TabsTrigger value="general">General</TabsTrigger>
|
||||
<TabsTrigger value="environment">Environment</TabsTrigger>
|
||||
<TabsTrigger value="logs">Logs</TabsTrigger>
|
||||
{((data?.serverId && isCloud) || !data?.server) && (
|
||||
<TabsTrigger value="monitoring">Monitoring</TabsTrigger>
|
||||
{permissions?.envVars.read && (
|
||||
<TabsTrigger value="environment">
|
||||
Environment
|
||||
</TabsTrigger>
|
||||
)}
|
||||
{permissions?.logs.read && (
|
||||
<TabsTrigger value="logs">Logs</TabsTrigger>
|
||||
)}
|
||||
{permissions?.monitoring.read &&
|
||||
((data?.serverId && isCloud) || !data?.server) && (
|
||||
<TabsTrigger value="monitoring">
|
||||
Monitoring
|
||||
</TabsTrigger>
|
||||
)}
|
||||
{permissions?.service.create && (
|
||||
<TabsTrigger value="advanced">Advanced</TabsTrigger>
|
||||
)}
|
||||
<TabsTrigger value="advanced">Advanced</TabsTrigger>
|
||||
</TabsList>
|
||||
</div>
|
||||
|
||||
@@ -229,25 +241,28 @@ const Redis = (
|
||||
<ShowExternalRedisCredentials redisId={redisId} />
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent value="environment">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<ShowEnvironment id={redisId} type="redis" />
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent value="monitoring">
|
||||
<div className="pt-2.5">
|
||||
<div className="flex flex-col gap-4 border rounded-lg p-6">
|
||||
{data?.serverId && isCloud ? (
|
||||
<ContainerPaidMonitoring
|
||||
appName={data?.appName || ""}
|
||||
baseUrl={`${data?.serverId ? `http://${data?.server?.ipAddress}:${data?.server?.metricsConfig?.server?.port}` : "http://localhost:4500"}`}
|
||||
token={
|
||||
data?.server?.metricsConfig?.server?.token || ""
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
{/* {monitoring?.enabledFeatures && (
|
||||
{permissions?.envVars.read && (
|
||||
<TabsContent value="environment">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<ShowEnvironment id={redisId} type="redis" />
|
||||
</div>
|
||||
</TabsContent>
|
||||
)}
|
||||
{permissions?.monitoring.read && (
|
||||
<TabsContent value="monitoring">
|
||||
<div className="pt-2.5">
|
||||
<div className="flex flex-col gap-4 border rounded-lg p-6">
|
||||
{data?.serverId && isCloud ? (
|
||||
<ContainerPaidMonitoring
|
||||
appName={data?.appName || ""}
|
||||
baseUrl={`${data?.serverId ? `http://${data?.server?.ipAddress}:${data?.server?.metricsConfig?.server?.port}` : "http://localhost:4500"}`}
|
||||
token={
|
||||
data?.server?.metricsConfig?.server?.token || ""
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
{/* {monitoring?.enabledFeatures && (
|
||||
<div className="flex flex-row border w-fit p-4 rounded-lg items-center gap-2">
|
||||
<Label className="text-muted-foreground">
|
||||
Change Monitoring
|
||||
@@ -269,29 +284,37 @@ const Redis = (
|
||||
/>
|
||||
) : (
|
||||
<div> */}
|
||||
<ContainerFreeMonitoring
|
||||
appName={data?.appName || ""}
|
||||
/>
|
||||
{/* </div> */}
|
||||
{/* )} */}
|
||||
</>
|
||||
)}
|
||||
<ContainerFreeMonitoring
|
||||
appName={data?.appName || ""}
|
||||
/>
|
||||
{/* </div> */}
|
||||
{/* )} */}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent value="logs">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<ShowDockerLogs
|
||||
serverId={data?.serverId || ""}
|
||||
appName={data?.appName || ""}
|
||||
/>
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent value="advanced">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<ShowDatabaseAdvancedSettings id={redisId} type="redis" />
|
||||
</div>
|
||||
</TabsContent>
|
||||
</TabsContent>
|
||||
)}
|
||||
{permissions?.logs.read && (
|
||||
<TabsContent value="logs">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<ShowDockerLogs
|
||||
serverId={data?.serverId || ""}
|
||||
appName={data?.appName || ""}
|
||||
/>
|
||||
</div>
|
||||
</TabsContent>
|
||||
)}
|
||||
{permissions?.service.create && (
|
||||
<TabsContent value="advanced">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<ShowDatabaseAdvancedSettings
|
||||
id={redisId}
|
||||
type="redis"
|
||||
/>
|
||||
</div>
|
||||
</TabsContent>
|
||||
)}
|
||||
</Tabs>
|
||||
)}
|
||||
</CardContent>
|
||||
|
||||
66
apps/dokploy/pages/dashboard/settings/audit-logs.tsx
Normal file
66
apps/dokploy/pages/dashboard/settings/audit-logs.tsx
Normal file
@@ -0,0 +1,66 @@
|
||||
import { validateRequest } from "@dokploy/server";
|
||||
import { createServerSideHelpers } from "@trpc/react-query/server";
|
||||
import type { GetServerSidePropsContext } from "next";
|
||||
import type { ReactElement } from "react";
|
||||
import superjson from "superjson";
|
||||
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
||||
import { ShowAuditLogs } from "@/components/proprietary/audit-logs/show-audit-logs";
|
||||
import { appRouter } from "@/server/api/root";
|
||||
|
||||
const Page = () => {
|
||||
return (
|
||||
<div className="flex flex-col gap-4 w-full">
|
||||
<ShowAuditLogs />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Page;
|
||||
|
||||
Page.getLayout = (page: ReactElement) => {
|
||||
return <DashboardLayout metaName="Audit Logs">{page}</DashboardLayout>;
|
||||
};
|
||||
|
||||
export async function getServerSideProps(ctx: GetServerSidePropsContext) {
|
||||
const { req, res } = ctx;
|
||||
const { user, session } = await validateRequest(req);
|
||||
|
||||
if (!user) {
|
||||
return {
|
||||
redirect: { destination: "/", permanent: true },
|
||||
};
|
||||
}
|
||||
|
||||
const helpers = createServerSideHelpers({
|
||||
router: appRouter,
|
||||
ctx: {
|
||||
req: req as any,
|
||||
res: res as any,
|
||||
db: null as any,
|
||||
session: session as any,
|
||||
user: user as any,
|
||||
},
|
||||
transformer: superjson,
|
||||
});
|
||||
|
||||
try {
|
||||
const userPermissions = await helpers.user.getPermissions.fetch();
|
||||
|
||||
if (!userPermissions?.auditLog.read) {
|
||||
return {
|
||||
redirect: {
|
||||
destination: "/dashboard/settings/profile",
|
||||
permanent: false,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
props: {
|
||||
trpcState: helpers.dehydrate(),
|
||||
},
|
||||
};
|
||||
} catch {
|
||||
return { props: {} };
|
||||
}
|
||||
}
|
||||
@@ -48,19 +48,15 @@ export async function getServerSideProps(
|
||||
try {
|
||||
await helpers.project.all.prefetch();
|
||||
await helpers.settings.isCloud.prefetch();
|
||||
if (user.role === "member") {
|
||||
const userR = await helpers.user.one.fetch({
|
||||
userId: user.id,
|
||||
});
|
||||
const userPermissions = await helpers.user.getPermissions.fetch();
|
||||
|
||||
if (!userR?.canAccessToGitProviders) {
|
||||
return {
|
||||
redirect: {
|
||||
permanent: true,
|
||||
destination: "/",
|
||||
},
|
||||
};
|
||||
}
|
||||
if (!userPermissions?.gitProviders.read) {
|
||||
return {
|
||||
redirect: {
|
||||
permanent: true,
|
||||
destination: "/",
|
||||
},
|
||||
};
|
||||
}
|
||||
return {
|
||||
props: {
|
||||
|
||||
@@ -11,7 +11,7 @@ import { appRouter } from "@/server/api/root";
|
||||
import { api } from "@/utils/api";
|
||||
|
||||
const Page = () => {
|
||||
const { data } = api.user.get.useQuery();
|
||||
const { data: permissions } = api.user.getPermissions.useQuery();
|
||||
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||
|
||||
return (
|
||||
@@ -19,9 +19,7 @@ const Page = () => {
|
||||
<div className="h-full rounded-xl max-w-5xl mx-auto flex flex-col gap-4">
|
||||
<ProfileForm />
|
||||
{isCloud && <LinkingAccount />}
|
||||
{(data?.canAccessToAPI ||
|
||||
data?.role === "owner" ||
|
||||
data?.role === "admin") && <ShowApiKeys />}
|
||||
{permissions?.api.read && <ShowApiKeys />}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -49,19 +49,15 @@ export async function getServerSideProps(
|
||||
await helpers.project.all.prefetch();
|
||||
await helpers.settings.isCloud.prefetch();
|
||||
|
||||
if (user.role === "member") {
|
||||
const userR = await helpers.user.one.fetch({
|
||||
userId: user.id,
|
||||
});
|
||||
const userPermissions = await helpers.user.getPermissions.fetch();
|
||||
|
||||
if (!userR?.canAccessToSSHKeys) {
|
||||
return {
|
||||
redirect: {
|
||||
permanent: true,
|
||||
destination: "/",
|
||||
},
|
||||
};
|
||||
}
|
||||
if (!userPermissions?.sshKeys.read) {
|
||||
return {
|
||||
redirect: {
|
||||
permanent: true,
|
||||
destination: "/",
|
||||
},
|
||||
};
|
||||
}
|
||||
return {
|
||||
props: {
|
||||
|
||||
@@ -3,16 +3,24 @@ import { createServerSideHelpers } from "@trpc/react-query/server";
|
||||
import type { GetServerSidePropsContext } from "next";
|
||||
import type { ReactElement } from "react";
|
||||
import superjson from "superjson";
|
||||
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
||||
import { ManageCustomRoles } from "@/components/proprietary/roles/manage-custom-roles";
|
||||
import { ShowInvitations } from "@/components/dashboard/settings/users/show-invitations";
|
||||
import { ShowUsers } from "@/components/dashboard/settings/users/show-users";
|
||||
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
||||
import { appRouter } from "@/server/api/root";
|
||||
import { api } from "@/utils/api";
|
||||
|
||||
const Page = () => {
|
||||
const { data: auth } = api.user.get.useQuery();
|
||||
const { data: permissions } = api.user.getPermissions.useQuery();
|
||||
const isOwnerOrAdmin = auth?.role === "owner" || auth?.role === "admin";
|
||||
const canCreateMembers = permissions?.member.create ?? false;
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4 w-full">
|
||||
<ShowUsers />
|
||||
<ShowInvitations />
|
||||
{canCreateMembers && <ShowInvitations />}
|
||||
{isOwnerOrAdmin && <ManageCustomRoles />}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -28,7 +36,7 @@ export async function getServerSideProps(
|
||||
const { req, res } = ctx;
|
||||
const { user, session } = await validateRequest(req);
|
||||
|
||||
if (!user || user.role === "member") {
|
||||
if (!user) {
|
||||
return {
|
||||
redirect: {
|
||||
permanent: true,
|
||||
@@ -48,12 +56,30 @@ export async function getServerSideProps(
|
||||
},
|
||||
transformer: superjson,
|
||||
});
|
||||
await helpers.user.get.prefetch();
|
||||
await helpers.settings.isCloud.prefetch();
|
||||
|
||||
return {
|
||||
props: {
|
||||
trpcState: helpers.dehydrate(),
|
||||
},
|
||||
};
|
||||
try {
|
||||
await helpers.user.get.prefetch();
|
||||
await helpers.settings.isCloud.prefetch();
|
||||
|
||||
const userPermissions = await helpers.user.getPermissions.fetch();
|
||||
|
||||
if (!userPermissions?.member.read) {
|
||||
return {
|
||||
redirect: {
|
||||
permanent: true,
|
||||
destination: "/",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
props: {
|
||||
trpcState: helpers.dehydrate(),
|
||||
},
|
||||
};
|
||||
} catch {
|
||||
return {
|
||||
props: {},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,19 +53,15 @@ export async function getServerSideProps(
|
||||
try {
|
||||
await helpers.project.all.prefetch();
|
||||
|
||||
if (user.role === "member") {
|
||||
const userR = await helpers.user.one.fetch({
|
||||
userId: user.id,
|
||||
});
|
||||
const userPermissions = await helpers.user.getPermissions.fetch();
|
||||
|
||||
if (!userR?.canAccessToDocker) {
|
||||
return {
|
||||
redirect: {
|
||||
permanent: true,
|
||||
destination: "/",
|
||||
},
|
||||
};
|
||||
}
|
||||
if (!userPermissions?.docker.read) {
|
||||
return {
|
||||
redirect: {
|
||||
permanent: true,
|
||||
destination: "/",
|
||||
},
|
||||
};
|
||||
}
|
||||
return {
|
||||
props: {
|
||||
|
||||
@@ -53,19 +53,15 @@ export async function getServerSideProps(
|
||||
try {
|
||||
await helpers.project.all.prefetch();
|
||||
|
||||
if (user.role === "member") {
|
||||
const userR = await helpers.user.one.fetch({
|
||||
userId: user.id,
|
||||
});
|
||||
const userPermissions = await helpers.user.getPermissions.fetch();
|
||||
|
||||
if (!userR?.canAccessToTraefikFiles) {
|
||||
return {
|
||||
redirect: {
|
||||
permanent: true,
|
||||
destination: "/",
|
||||
},
|
||||
};
|
||||
}
|
||||
if (!userPermissions?.traefikFiles.read) {
|
||||
return {
|
||||
redirect: {
|
||||
permanent: true,
|
||||
destination: "/",
|
||||
},
|
||||
};
|
||||
}
|
||||
return {
|
||||
props: {
|
||||
|
||||
@@ -98,19 +98,15 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
||||
},
|
||||
transformer: superjson,
|
||||
});
|
||||
if (user.role === "member") {
|
||||
const userR = await helpers.user.one.fetch({
|
||||
userId: user.id,
|
||||
});
|
||||
const userPermissions = await helpers.user.getPermissions.fetch();
|
||||
|
||||
if (!userR?.canAccessToAPI) {
|
||||
return {
|
||||
redirect: {
|
||||
permanent: true,
|
||||
destination: "/",
|
||||
},
|
||||
};
|
||||
}
|
||||
if (!userPermissions?.api.read) {
|
||||
return {
|
||||
redirect: {
|
||||
permanent: true,
|
||||
destination: "/",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
@@ -23,6 +23,8 @@ import { mysqlRouter } from "./routers/mysql";
|
||||
import { notificationRouter } from "./routers/notification";
|
||||
import { organizationRouter } from "./routers/organization";
|
||||
import { patchRouter } from "./routers/patch";
|
||||
import { auditLogRouter } from "./routers/proprietary/audit-log";
|
||||
import { customRoleRouter } from "./routers/proprietary/custom-role";
|
||||
import { licenseKeyRouter } from "./routers/proprietary/license-key";
|
||||
import { ssoRouter } from "./routers/proprietary/sso";
|
||||
import { whitelabelingRouter } from "./routers/proprietary/whitelabeling";
|
||||
@@ -89,6 +91,8 @@ export const appRouter = createTRPCRouter({
|
||||
licenseKey: licenseKeyRouter,
|
||||
sso: ssoRouter,
|
||||
whitelabeling: whitelabelingRouter,
|
||||
customRole: customRoleRouter,
|
||||
auditLog: auditLogRouter,
|
||||
schedule: scheduleRouter,
|
||||
rollback: rollbackRouter,
|
||||
volumeBackups: volumeBackupsRouter,
|
||||
|
||||
@@ -21,7 +21,7 @@ import { findProjectById } from "@dokploy/server/services/project";
|
||||
import {
|
||||
addNewService,
|
||||
checkServiceAccess,
|
||||
} from "@dokploy/server/services/user";
|
||||
} from "@dokploy/server/services/permission";
|
||||
import {
|
||||
getProviderHeaders,
|
||||
getProviderName,
|
||||
@@ -38,17 +38,10 @@ import {
|
||||
import { generatePassword } from "@/templates/utils";
|
||||
|
||||
export const aiRouter = createTRPCRouter({
|
||||
one: protectedProcedure
|
||||
one: adminProcedure
|
||||
.input(z.object({ aiId: z.string() }))
|
||||
.query(async ({ ctx, input }) => {
|
||||
const aiSetting = await getAiSettingById(input.aiId);
|
||||
if (aiSetting.organizationId !== ctx.session.activeOrganizationId) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You don't have access to this AI configuration",
|
||||
});
|
||||
}
|
||||
return aiSetting;
|
||||
.query(async ({ input }) => {
|
||||
return await getAiSettingById(input.aiId);
|
||||
}),
|
||||
|
||||
getModels: protectedProcedure
|
||||
@@ -159,11 +152,9 @@ export const aiRouter = createTRPCRouter({
|
||||
return await saveAiSettings(ctx.session.activeOrganizationId, input);
|
||||
}),
|
||||
|
||||
update: protectedProcedure
|
||||
.input(apiUpdateAi)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
return await saveAiSettings(ctx.session.activeOrganizationId, input);
|
||||
}),
|
||||
update: adminProcedure.input(apiUpdateAi).mutation(async ({ ctx, input }) => {
|
||||
return await saveAiSettings(ctx.session.activeOrganizationId, input);
|
||||
}),
|
||||
|
||||
getAll: adminProcedure.query(async ({ ctx }) => {
|
||||
return await getAiSettingsByOrganizationId(
|
||||
@@ -171,29 +162,15 @@ export const aiRouter = createTRPCRouter({
|
||||
);
|
||||
}),
|
||||
|
||||
get: protectedProcedure
|
||||
get: adminProcedure
|
||||
.input(z.object({ aiId: z.string() }))
|
||||
.query(async ({ ctx, input }) => {
|
||||
const aiSetting = await getAiSettingById(input.aiId);
|
||||
if (aiSetting.organizationId !== ctx.session.activeOrganizationId) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You don't have access to this AI configuration",
|
||||
});
|
||||
}
|
||||
return aiSetting;
|
||||
.query(async ({ input }) => {
|
||||
return await getAiSettingById(input.aiId);
|
||||
}),
|
||||
|
||||
delete: protectedProcedure
|
||||
delete: adminProcedure
|
||||
.input(z.object({ aiId: z.string() }))
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const aiSetting = await getAiSettingById(input.aiId);
|
||||
if (aiSetting.organizationId !== ctx.session.activeOrganizationId) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You don't have access to this AI configuration",
|
||||
});
|
||||
}
|
||||
.mutation(async ({ input }) => {
|
||||
return await deleteAiSettings(input.aiId);
|
||||
}),
|
||||
|
||||
@@ -223,13 +200,7 @@ export const aiRouter = createTRPCRouter({
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const environment = await findEnvironmentById(input.environmentId);
|
||||
const project = await findProjectById(environment.projectId);
|
||||
if (ctx.user.role === "member") {
|
||||
await checkServiceAccess(
|
||||
ctx.session.activeOrganizationId,
|
||||
environment.projectId,
|
||||
"create",
|
||||
);
|
||||
}
|
||||
await checkServiceAccess(ctx, environment.projectId, "create");
|
||||
|
||||
if (IS_CLOUD && !input.serverId) {
|
||||
throw new TRPCError({
|
||||
@@ -275,13 +246,7 @@ export const aiRouter = createTRPCRouter({
|
||||
}
|
||||
}
|
||||
|
||||
if (ctx.user.role === "member") {
|
||||
await addNewService(
|
||||
ctx.session.activeOrganizationId,
|
||||
ctx.user.ownerId,
|
||||
compose.composeId,
|
||||
);
|
||||
}
|
||||
await addNewService(ctx, compose.composeId);
|
||||
|
||||
return null;
|
||||
}),
|
||||
|
||||
@@ -1,13 +1,10 @@
|
||||
import {
|
||||
addNewService,
|
||||
checkServiceAccess,
|
||||
clearOldDeployments,
|
||||
createApplication,
|
||||
deleteAllMiddlewares,
|
||||
findApplicationById,
|
||||
findEnvironmentById,
|
||||
findGitProviderById,
|
||||
findMemberById,
|
||||
findProjectById,
|
||||
getApplicationStats,
|
||||
IS_CLOUD,
|
||||
@@ -29,14 +26,24 @@ import {
|
||||
updateDeploymentStatus,
|
||||
writeConfig,
|
||||
writeConfigRemote,
|
||||
// uploadFileSchema
|
||||
} from "@dokploy/server";
|
||||
import { db } from "@dokploy/server/db";
|
||||
import {
|
||||
addNewService,
|
||||
checkServiceAccess,
|
||||
checkServicePermissionAndAccess,
|
||||
findMemberByUserId,
|
||||
} from "@dokploy/server/services/permission";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { and, desc, eq, ilike, or, sql } from "drizzle-orm";
|
||||
import { nanoid } from "nanoid";
|
||||
import { z } from "zod";
|
||||
import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc";
|
||||
import {
|
||||
createTRPCRouter,
|
||||
protectedProcedure,
|
||||
withPermission,
|
||||
} from "@/server/api/trpc";
|
||||
import { audit } from "@/server/api/utils/audit";
|
||||
import {
|
||||
apiCreateApplication,
|
||||
apiDeployApplication,
|
||||
@@ -72,18 +79,10 @@ export const applicationRouter = createTRPCRouter({
|
||||
.input(apiCreateApplication)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
// Get project from environment
|
||||
const environment = await findEnvironmentById(input.environmentId);
|
||||
const project = await findProjectById(environment.projectId);
|
||||
|
||||
if (ctx.user.role === "member") {
|
||||
await checkServiceAccess(
|
||||
ctx.user.id,
|
||||
project.projectId,
|
||||
ctx.session.activeOrganizationId,
|
||||
"create",
|
||||
);
|
||||
}
|
||||
await checkServiceAccess(ctx, project.projectId, "create");
|
||||
|
||||
if (IS_CLOUD && !input.serverId) {
|
||||
throw new TRPCError({
|
||||
@@ -101,13 +100,13 @@ export const applicationRouter = createTRPCRouter({
|
||||
|
||||
const newApplication = await createApplication(input);
|
||||
|
||||
if (ctx.user.role === "member") {
|
||||
await addNewService(
|
||||
ctx.user.id,
|
||||
newApplication.applicationId,
|
||||
project.organizationId,
|
||||
);
|
||||
}
|
||||
await addNewService(ctx, newApplication.applicationId);
|
||||
await audit(ctx, {
|
||||
action: "create",
|
||||
resourceType: "service",
|
||||
resourceId: newApplication.applicationId,
|
||||
resourceName: newApplication.appName,
|
||||
});
|
||||
return newApplication;
|
||||
} catch (error: unknown) {
|
||||
console.log("error", error);
|
||||
@@ -124,14 +123,7 @@ export const applicationRouter = createTRPCRouter({
|
||||
one: protectedProcedure
|
||||
.input(apiFindOneApplication)
|
||||
.query(async ({ input, ctx }) => {
|
||||
if (ctx.user.role === "member") {
|
||||
await checkServiceAccess(
|
||||
ctx.user.id,
|
||||
input.applicationId,
|
||||
ctx.session.activeOrganizationId,
|
||||
"access",
|
||||
);
|
||||
}
|
||||
await checkServiceAccess(ctx, input.applicationId, "read");
|
||||
const application = await findApplicationById(input.applicationId);
|
||||
if (
|
||||
application.environment.project.organizationId !==
|
||||
@@ -186,22 +178,21 @@ export const applicationRouter = createTRPCRouter({
|
||||
reload: protectedProcedure
|
||||
.input(apiReloadApplication)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.applicationId, {
|
||||
deployment: ["create"],
|
||||
});
|
||||
const application = await findApplicationById(input.applicationId);
|
||||
|
||||
try {
|
||||
if (
|
||||
application.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to reload this application",
|
||||
});
|
||||
}
|
||||
|
||||
await updateApplicationStatus(input.applicationId, "idle");
|
||||
await mechanizeDockerContainer(application);
|
||||
await updateApplicationStatus(input.applicationId, "done");
|
||||
await audit(ctx, {
|
||||
action: "reload",
|
||||
resourceType: "application",
|
||||
resourceId: application.applicationId,
|
||||
resourceName: application.appName,
|
||||
});
|
||||
return true;
|
||||
} catch (error) {
|
||||
await updateApplicationStatus(input.applicationId, "error");
|
||||
@@ -216,14 +207,7 @@ export const applicationRouter = createTRPCRouter({
|
||||
delete: protectedProcedure
|
||||
.input(apiFindOneApplication)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
if (ctx.user.role === "member") {
|
||||
await checkServiceAccess(
|
||||
ctx.user.id,
|
||||
input.applicationId,
|
||||
ctx.session.activeOrganizationId,
|
||||
"delete",
|
||||
);
|
||||
}
|
||||
await checkServiceAccess(ctx, input.applicationId, "delete");
|
||||
const application = await findApplicationById(input.applicationId);
|
||||
|
||||
if (
|
||||
@@ -272,69 +256,66 @@ export const applicationRouter = createTRPCRouter({
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
await audit(ctx, {
|
||||
action: "delete",
|
||||
resourceType: "service",
|
||||
resourceId: application.applicationId,
|
||||
resourceName: application.appName,
|
||||
});
|
||||
return application;
|
||||
}),
|
||||
|
||||
stop: protectedProcedure
|
||||
.input(apiFindOneApplication)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.applicationId, {
|
||||
deployment: ["create"],
|
||||
});
|
||||
const service = await findApplicationById(input.applicationId);
|
||||
if (
|
||||
service.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to stop this application",
|
||||
});
|
||||
}
|
||||
if (service.serverId) {
|
||||
await stopServiceRemote(service.serverId, service.appName);
|
||||
} else {
|
||||
await stopService(service.appName);
|
||||
}
|
||||
await updateApplicationStatus(input.applicationId, "idle");
|
||||
|
||||
await audit(ctx, {
|
||||
action: "stop",
|
||||
resourceType: "application",
|
||||
resourceId: service.applicationId,
|
||||
resourceName: service.appName,
|
||||
});
|
||||
return service;
|
||||
}),
|
||||
|
||||
start: protectedProcedure
|
||||
.input(apiFindOneApplication)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.applicationId, {
|
||||
deployment: ["create"],
|
||||
});
|
||||
const service = await findApplicationById(input.applicationId);
|
||||
if (
|
||||
service.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to start this application",
|
||||
});
|
||||
}
|
||||
|
||||
if (service.serverId) {
|
||||
await startServiceRemote(service.serverId, service.appName);
|
||||
} else {
|
||||
await startService(service.appName);
|
||||
}
|
||||
await updateApplicationStatus(input.applicationId, "done");
|
||||
|
||||
await audit(ctx, {
|
||||
action: "start",
|
||||
resourceType: "application",
|
||||
resourceId: service.applicationId,
|
||||
resourceName: service.appName,
|
||||
});
|
||||
return service;
|
||||
}),
|
||||
|
||||
redeploy: protectedProcedure
|
||||
.input(apiRedeployApplication)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.applicationId, {
|
||||
deployment: ["create"],
|
||||
});
|
||||
const application = await findApplicationById(input.applicationId);
|
||||
if (
|
||||
application.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to redeploy this application",
|
||||
});
|
||||
}
|
||||
const jobData: DeploymentJob = {
|
||||
applicationId: input.applicationId,
|
||||
titleLog: input.title || "Rebuild deployment",
|
||||
@@ -349,6 +330,12 @@ export const applicationRouter = createTRPCRouter({
|
||||
deploy(jobData).catch((error) => {
|
||||
console.error("Background deployment failed:", error);
|
||||
});
|
||||
await audit(ctx, {
|
||||
action: "rebuild",
|
||||
resourceType: "application",
|
||||
resourceId: application.applicationId,
|
||||
resourceName: application.appName,
|
||||
});
|
||||
return true;
|
||||
}
|
||||
await myQueue.add(
|
||||
@@ -359,41 +346,40 @@ export const applicationRouter = createTRPCRouter({
|
||||
removeOnFail: true,
|
||||
},
|
||||
);
|
||||
await audit(ctx, {
|
||||
action: "rebuild",
|
||||
resourceType: "application",
|
||||
resourceId: application.applicationId,
|
||||
resourceName: application.appName,
|
||||
});
|
||||
}),
|
||||
saveEnvironment: protectedProcedure
|
||||
.input(apiSaveEnvironmentVariables)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const application = await findApplicationById(input.applicationId);
|
||||
if (
|
||||
application.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to save this environment",
|
||||
});
|
||||
}
|
||||
await checkServicePermissionAndAccess(ctx, input.applicationId, {
|
||||
envVars: ["write"],
|
||||
});
|
||||
await updateApplication(input.applicationId, {
|
||||
env: input.env,
|
||||
buildArgs: input.buildArgs,
|
||||
buildSecrets: input.buildSecrets,
|
||||
createEnvFile: input.createEnvFile,
|
||||
});
|
||||
const application = await findApplicationById(input.applicationId);
|
||||
await audit(ctx, {
|
||||
action: "update",
|
||||
resourceType: "application",
|
||||
resourceId: application.applicationId,
|
||||
resourceName: application.appName,
|
||||
});
|
||||
return true;
|
||||
}),
|
||||
saveBuildType: protectedProcedure
|
||||
.input(apiSaveBuildType)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const application = await findApplicationById(input.applicationId);
|
||||
if (
|
||||
application.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to save this build type",
|
||||
});
|
||||
}
|
||||
await checkServicePermissionAndAccess(ctx, input.applicationId, {
|
||||
service: ["create"],
|
||||
});
|
||||
await updateApplication(input.applicationId, {
|
||||
buildType: input.buildType,
|
||||
dockerfile: input.dockerfile,
|
||||
@@ -404,22 +390,21 @@ export const applicationRouter = createTRPCRouter({
|
||||
isStaticSpa: input.isStaticSpa,
|
||||
railpackVersion: input.railpackVersion,
|
||||
});
|
||||
|
||||
const application = await findApplicationById(input.applicationId);
|
||||
await audit(ctx, {
|
||||
action: "update",
|
||||
resourceType: "application",
|
||||
resourceId: application.applicationId,
|
||||
resourceName: application.appName,
|
||||
});
|
||||
return true;
|
||||
}),
|
||||
saveGithubProvider: protectedProcedure
|
||||
.input(apiSaveGithubProvider)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const application = await findApplicationById(input.applicationId);
|
||||
if (
|
||||
application.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to save this github provider",
|
||||
});
|
||||
}
|
||||
await checkServicePermissionAndAccess(ctx, input.applicationId, {
|
||||
service: ["create"],
|
||||
});
|
||||
await updateApplication(input.applicationId, {
|
||||
repository: input.repository,
|
||||
branch: input.branch,
|
||||
@@ -432,22 +417,21 @@ export const applicationRouter = createTRPCRouter({
|
||||
triggerType: input.triggerType,
|
||||
enableSubmodules: input.enableSubmodules,
|
||||
});
|
||||
|
||||
const application = await findApplicationById(input.applicationId);
|
||||
await audit(ctx, {
|
||||
action: "update",
|
||||
resourceType: "application",
|
||||
resourceId: application.applicationId,
|
||||
resourceName: application.appName,
|
||||
});
|
||||
return true;
|
||||
}),
|
||||
saveGitlabProvider: protectedProcedure
|
||||
.input(apiSaveGitlabProvider)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const application = await findApplicationById(input.applicationId);
|
||||
if (
|
||||
application.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to save this gitlab provider",
|
||||
});
|
||||
}
|
||||
await checkServicePermissionAndAccess(ctx, input.applicationId, {
|
||||
service: ["create"],
|
||||
});
|
||||
await updateApplication(input.applicationId, {
|
||||
gitlabRepository: input.gitlabRepository,
|
||||
gitlabOwner: input.gitlabOwner,
|
||||
@@ -461,22 +445,21 @@ export const applicationRouter = createTRPCRouter({
|
||||
watchPaths: input.watchPaths,
|
||||
enableSubmodules: input.enableSubmodules,
|
||||
});
|
||||
|
||||
const application = await findApplicationById(input.applicationId);
|
||||
await audit(ctx, {
|
||||
action: "update",
|
||||
resourceType: "application",
|
||||
resourceId: application.applicationId,
|
||||
resourceName: application.appName,
|
||||
});
|
||||
return true;
|
||||
}),
|
||||
saveBitbucketProvider: protectedProcedure
|
||||
.input(apiSaveBitbucketProvider)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const application = await findApplicationById(input.applicationId);
|
||||
if (
|
||||
application.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to save this bitbucket provider",
|
||||
});
|
||||
}
|
||||
await checkServicePermissionAndAccess(ctx, input.applicationId, {
|
||||
service: ["create"],
|
||||
});
|
||||
await updateApplication(input.applicationId, {
|
||||
bitbucketRepository: input.bitbucketRepository,
|
||||
bitbucketRepositorySlug: input.bitbucketRepositorySlug,
|
||||
@@ -489,22 +472,21 @@ export const applicationRouter = createTRPCRouter({
|
||||
watchPaths: input.watchPaths,
|
||||
enableSubmodules: input.enableSubmodules,
|
||||
});
|
||||
|
||||
const application = await findApplicationById(input.applicationId);
|
||||
await audit(ctx, {
|
||||
action: "update",
|
||||
resourceType: "application",
|
||||
resourceId: application.applicationId,
|
||||
resourceName: application.appName,
|
||||
});
|
||||
return true;
|
||||
}),
|
||||
saveGiteaProvider: protectedProcedure
|
||||
.input(apiSaveGiteaProvider)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const application = await findApplicationById(input.applicationId);
|
||||
if (
|
||||
application.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to save this gitea provider",
|
||||
});
|
||||
}
|
||||
await checkServicePermissionAndAccess(ctx, input.applicationId, {
|
||||
service: ["create"],
|
||||
});
|
||||
await updateApplication(input.applicationId, {
|
||||
giteaRepository: input.giteaRepository,
|
||||
giteaOwner: input.giteaOwner,
|
||||
@@ -516,22 +498,21 @@ export const applicationRouter = createTRPCRouter({
|
||||
watchPaths: input.watchPaths,
|
||||
enableSubmodules: input.enableSubmodules,
|
||||
});
|
||||
|
||||
const application = await findApplicationById(input.applicationId);
|
||||
await audit(ctx, {
|
||||
action: "update",
|
||||
resourceType: "application",
|
||||
resourceId: application.applicationId,
|
||||
resourceName: application.appName,
|
||||
});
|
||||
return true;
|
||||
}),
|
||||
saveDockerProvider: protectedProcedure
|
||||
.input(apiSaveDockerProvider)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const application = await findApplicationById(input.applicationId);
|
||||
if (
|
||||
application.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to save this docker provider",
|
||||
});
|
||||
}
|
||||
await checkServicePermissionAndAccess(ctx, input.applicationId, {
|
||||
service: ["create"],
|
||||
});
|
||||
await updateApplication(input.applicationId, {
|
||||
dockerImage: input.dockerImage,
|
||||
username: input.username,
|
||||
@@ -540,22 +521,21 @@ export const applicationRouter = createTRPCRouter({
|
||||
applicationStatus: "idle",
|
||||
registryUrl: input.registryUrl,
|
||||
});
|
||||
|
||||
const application = await findApplicationById(input.applicationId);
|
||||
await audit(ctx, {
|
||||
action: "update",
|
||||
resourceType: "application",
|
||||
resourceId: application.applicationId,
|
||||
resourceName: application.appName,
|
||||
});
|
||||
return true;
|
||||
}),
|
||||
saveGitProvider: protectedProcedure
|
||||
.input(apiSaveGitProvider)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const application = await findApplicationById(input.applicationId);
|
||||
if (
|
||||
application.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to save this git provider",
|
||||
});
|
||||
}
|
||||
await checkServicePermissionAndAccess(ctx, input.applicationId, {
|
||||
service: ["create"],
|
||||
});
|
||||
await updateApplication(input.applicationId, {
|
||||
customGitBranch: input.customGitBranch,
|
||||
customGitBuildPath: input.customGitBuildPath,
|
||||
@@ -566,26 +546,22 @@ export const applicationRouter = createTRPCRouter({
|
||||
watchPaths: input.watchPaths,
|
||||
enableSubmodules: input.enableSubmodules,
|
||||
});
|
||||
|
||||
const application = await findApplicationById(input.applicationId);
|
||||
await audit(ctx, {
|
||||
action: "update",
|
||||
resourceType: "application",
|
||||
resourceId: application.applicationId,
|
||||
resourceName: application.appName,
|
||||
});
|
||||
return true;
|
||||
}),
|
||||
disconnectGitProvider: protectedProcedure
|
||||
.input(apiFindOneApplication)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const application = await findApplicationById(input.applicationId);
|
||||
if (
|
||||
application.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to disconnect this git provider",
|
||||
});
|
||||
}
|
||||
|
||||
// Reset all git provider related fields
|
||||
await checkServicePermissionAndAccess(ctx, input.applicationId, {
|
||||
service: ["create"],
|
||||
});
|
||||
await updateApplication(input.applicationId, {
|
||||
// GitHub fields
|
||||
repository: null,
|
||||
branch: null,
|
||||
owner: null,
|
||||
@@ -593,7 +569,6 @@ export const applicationRouter = createTRPCRouter({
|
||||
githubId: null,
|
||||
triggerType: "push",
|
||||
|
||||
// GitLab fields
|
||||
gitlabRepository: null,
|
||||
gitlabOwner: null,
|
||||
gitlabBranch: null,
|
||||
@@ -602,63 +577,58 @@ export const applicationRouter = createTRPCRouter({
|
||||
gitlabProjectId: null,
|
||||
gitlabPathNamespace: null,
|
||||
|
||||
// Bitbucket fields
|
||||
bitbucketRepository: null,
|
||||
bitbucketOwner: null,
|
||||
bitbucketBranch: null,
|
||||
bitbucketBuildPath: null,
|
||||
bitbucketId: null,
|
||||
|
||||
// Gitea fields
|
||||
giteaRepository: null,
|
||||
giteaOwner: null,
|
||||
giteaBranch: null,
|
||||
giteaBuildPath: null,
|
||||
giteaId: null,
|
||||
|
||||
// Custom Git fields
|
||||
customGitBranch: null,
|
||||
customGitBuildPath: null,
|
||||
customGitUrl: null,
|
||||
customGitSSHKeyId: null,
|
||||
|
||||
// Common fields
|
||||
sourceType: "github", // Reset to default
|
||||
applicationStatus: "idle",
|
||||
watchPaths: null,
|
||||
enableSubmodules: false,
|
||||
});
|
||||
|
||||
const application = await findApplicationById(input.applicationId);
|
||||
await audit(ctx, {
|
||||
action: "update",
|
||||
resourceType: "application",
|
||||
resourceId: application.applicationId,
|
||||
resourceName: application.appName,
|
||||
});
|
||||
return true;
|
||||
}),
|
||||
markRunning: protectedProcedure
|
||||
.input(apiFindOneApplication)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const application = await findApplicationById(input.applicationId);
|
||||
if (
|
||||
application.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to mark this application as running",
|
||||
});
|
||||
}
|
||||
await checkServicePermissionAndAccess(ctx, input.applicationId, {
|
||||
deployment: ["create"],
|
||||
});
|
||||
await updateApplicationStatus(input.applicationId, "running");
|
||||
const application = await findApplicationById(input.applicationId);
|
||||
await audit(ctx, {
|
||||
action: "deploy",
|
||||
resourceType: "application",
|
||||
resourceId: application.applicationId,
|
||||
resourceName: application.appName,
|
||||
});
|
||||
}),
|
||||
update: protectedProcedure
|
||||
.input(apiUpdateApplication)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const application = await findApplicationById(input.applicationId);
|
||||
if (
|
||||
application.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to update this application",
|
||||
});
|
||||
}
|
||||
await checkServicePermissionAndAccess(ctx, input.applicationId, {
|
||||
service: ["create"],
|
||||
});
|
||||
const { applicationId, ...rest } = input;
|
||||
const updateApp = await updateApplication(applicationId, {
|
||||
...rest,
|
||||
@@ -670,40 +640,39 @@ export const applicationRouter = createTRPCRouter({
|
||||
message: "Error updating application",
|
||||
});
|
||||
}
|
||||
|
||||
await audit(ctx, {
|
||||
action: "update",
|
||||
resourceType: "application",
|
||||
resourceId: updateApp.applicationId,
|
||||
resourceName: updateApp.appName,
|
||||
});
|
||||
return true;
|
||||
}),
|
||||
refreshToken: protectedProcedure
|
||||
.input(apiFindOneApplication)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const application = await findApplicationById(input.applicationId);
|
||||
if (
|
||||
application.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to refresh this application",
|
||||
});
|
||||
}
|
||||
await checkServicePermissionAndAccess(ctx, input.applicationId, {
|
||||
service: ["create"],
|
||||
});
|
||||
await updateApplication(input.applicationId, {
|
||||
refreshToken: nanoid(),
|
||||
});
|
||||
const application = await findApplicationById(input.applicationId);
|
||||
await audit(ctx, {
|
||||
action: "update",
|
||||
resourceType: "application",
|
||||
resourceId: application.applicationId,
|
||||
resourceName: application.appName,
|
||||
});
|
||||
return true;
|
||||
}),
|
||||
deploy: protectedProcedure
|
||||
.input(apiDeployApplication)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.applicationId, {
|
||||
deployment: ["create"],
|
||||
});
|
||||
const application = await findApplicationById(input.applicationId);
|
||||
if (
|
||||
application.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to deploy this application",
|
||||
});
|
||||
}
|
||||
const jobData: DeploymentJob = {
|
||||
applicationId: input.applicationId,
|
||||
titleLog: input.title || "Manual deployment",
|
||||
@@ -717,7 +686,12 @@ export const applicationRouter = createTRPCRouter({
|
||||
deploy(jobData).catch((error) => {
|
||||
console.error("Background deployment failed:", error);
|
||||
});
|
||||
|
||||
await audit(ctx, {
|
||||
action: "deploy",
|
||||
resourceType: "application",
|
||||
resourceId: application.applicationId,
|
||||
resourceName: application.appName,
|
||||
});
|
||||
return true;
|
||||
}
|
||||
await myQueue.add(
|
||||
@@ -728,69 +702,60 @@ export const applicationRouter = createTRPCRouter({
|
||||
removeOnFail: true,
|
||||
},
|
||||
);
|
||||
await audit(ctx, {
|
||||
action: "deploy",
|
||||
resourceType: "application",
|
||||
resourceId: application.applicationId,
|
||||
resourceName: application.appName,
|
||||
});
|
||||
}),
|
||||
|
||||
cleanQueues: protectedProcedure
|
||||
.input(apiFindOneApplication)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const application = await findApplicationById(input.applicationId);
|
||||
if (
|
||||
application.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to clean this application",
|
||||
});
|
||||
}
|
||||
await checkServicePermissionAndAccess(ctx, input.applicationId, {
|
||||
deployment: ["cancel"],
|
||||
});
|
||||
await cleanQueuesByApplication(input.applicationId);
|
||||
}),
|
||||
clearDeployments: protectedProcedure
|
||||
.input(apiFindOneApplication)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.applicationId, {
|
||||
deployment: ["create"],
|
||||
});
|
||||
const application = await findApplicationById(input.applicationId);
|
||||
if (
|
||||
application.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message:
|
||||
"You are not authorized to clear deployments for this application",
|
||||
});
|
||||
}
|
||||
await clearOldDeployments(application.appName, application.serverId);
|
||||
await audit(ctx, {
|
||||
action: "delete",
|
||||
resourceType: "application",
|
||||
resourceId: application.applicationId,
|
||||
resourceName: application.appName,
|
||||
});
|
||||
return true;
|
||||
}),
|
||||
killBuild: protectedProcedure
|
||||
.input(apiFindOneApplication)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.applicationId, {
|
||||
deployment: ["cancel"],
|
||||
});
|
||||
const application = await findApplicationById(input.applicationId);
|
||||
if (
|
||||
application.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to kill this build",
|
||||
});
|
||||
}
|
||||
await killDockerBuild("application", application.serverId);
|
||||
await audit(ctx, {
|
||||
action: "stop",
|
||||
resourceType: "application",
|
||||
resourceId: application.applicationId,
|
||||
resourceName: application.appName,
|
||||
});
|
||||
}),
|
||||
readTraefikConfig: protectedProcedure
|
||||
.input(apiFindOneApplication)
|
||||
.query(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.applicationId, {
|
||||
traefikFiles: ["read"],
|
||||
});
|
||||
const application = await findApplicationById(input.applicationId);
|
||||
if (
|
||||
application.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to read this application",
|
||||
});
|
||||
}
|
||||
|
||||
let traefikConfig = null;
|
||||
if (application.serverId) {
|
||||
traefikConfig = await readRemoteConfig(
|
||||
@@ -820,18 +785,11 @@ export const applicationRouter = createTRPCRouter({
|
||||
const applicationId = formData.get("applicationId") as string;
|
||||
const dropBuildPath = formData.get("dropBuildPath") as string | null;
|
||||
|
||||
await checkServicePermissionAndAccess(ctx, applicationId, {
|
||||
deployment: ["create"],
|
||||
});
|
||||
const app = await findApplicationById(applicationId);
|
||||
|
||||
if (
|
||||
app.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to deploy this application",
|
||||
});
|
||||
}
|
||||
|
||||
await updateApplication(applicationId, {
|
||||
sourceType: "drop",
|
||||
dropBuildPath: dropBuildPath || "",
|
||||
@@ -862,23 +820,21 @@ export const applicationRouter = createTRPCRouter({
|
||||
removeOnFail: true,
|
||||
},
|
||||
);
|
||||
await audit(ctx, {
|
||||
action: "deploy",
|
||||
resourceType: "application",
|
||||
resourceId: app.applicationId,
|
||||
resourceName: app.appName,
|
||||
});
|
||||
return true;
|
||||
}),
|
||||
updateTraefikConfig: protectedProcedure
|
||||
.input(z.object({ applicationId: z.string(), traefikConfig: z.string() }))
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.applicationId, {
|
||||
traefikFiles: ["write"],
|
||||
});
|
||||
const application = await findApplicationById(input.applicationId);
|
||||
|
||||
if (
|
||||
application.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to update this application",
|
||||
});
|
||||
}
|
||||
|
||||
if (application.serverId) {
|
||||
await writeConfigRemote(
|
||||
application.serverId,
|
||||
@@ -888,9 +844,15 @@ export const applicationRouter = createTRPCRouter({
|
||||
} else {
|
||||
writeConfig(application.appName, input.traefikConfig);
|
||||
}
|
||||
await audit(ctx, {
|
||||
action: "update",
|
||||
resourceType: "application",
|
||||
resourceId: application.applicationId,
|
||||
resourceName: application.appName,
|
||||
});
|
||||
return true;
|
||||
}),
|
||||
readAppMonitoring: protectedProcedure
|
||||
readAppMonitoring: withPermission("monitoring", "read")
|
||||
.input(apiFindMonitoringStats)
|
||||
.query(async ({ input }) => {
|
||||
if (IS_CLOUD) {
|
||||
@@ -911,31 +873,10 @@ export const applicationRouter = createTRPCRouter({
|
||||
}),
|
||||
)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const application = await findApplicationById(input.applicationId);
|
||||
if (
|
||||
application.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to move this application",
|
||||
});
|
||||
}
|
||||
await checkServicePermissionAndAccess(ctx, input.applicationId, {
|
||||
service: ["create"],
|
||||
});
|
||||
|
||||
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 environment",
|
||||
});
|
||||
}
|
||||
|
||||
// Update the application's projectId
|
||||
const updatedApplication = await db
|
||||
.update(applications)
|
||||
.set({
|
||||
@@ -951,23 +892,22 @@ export const applicationRouter = createTRPCRouter({
|
||||
message: "Failed to move application",
|
||||
});
|
||||
}
|
||||
|
||||
await audit(ctx, {
|
||||
action: "update",
|
||||
resourceType: "application",
|
||||
resourceId: updatedApplication.applicationId,
|
||||
resourceName: updatedApplication.appName,
|
||||
});
|
||||
return updatedApplication;
|
||||
}),
|
||||
|
||||
cancelDeployment: protectedProcedure
|
||||
.input(apiFindOneApplication)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.applicationId, {
|
||||
deployment: ["cancel"],
|
||||
});
|
||||
const application = await findApplicationById(input.applicationId);
|
||||
if (
|
||||
application.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to cancel this deployment",
|
||||
});
|
||||
}
|
||||
|
||||
if (IS_CLOUD && application.serverId) {
|
||||
try {
|
||||
@@ -984,7 +924,12 @@ export const applicationRouter = createTRPCRouter({
|
||||
applicationId: input.applicationId,
|
||||
applicationType: "application",
|
||||
});
|
||||
|
||||
await audit(ctx, {
|
||||
action: "stop",
|
||||
resourceType: "application",
|
||||
resourceId: application.applicationId,
|
||||
resourceName: application.appName,
|
||||
});
|
||||
return {
|
||||
success: true,
|
||||
message: "Deployment cancellation requested",
|
||||
@@ -1085,19 +1030,17 @@ export const applicationRouter = createTRPCRouter({
|
||||
);
|
||||
}
|
||||
|
||||
if (ctx.user.role === "member") {
|
||||
const { accessedServices } = await findMemberById(
|
||||
ctx.user.id,
|
||||
ctx.session.activeOrganizationId,
|
||||
);
|
||||
if (accessedServices.length === 0) return { items: [], total: 0 };
|
||||
baseConditions.push(
|
||||
sql`${applications.applicationId} IN (${sql.join(
|
||||
accessedServices.map((id) => sql`${id}`),
|
||||
sql`, `,
|
||||
)})`,
|
||||
);
|
||||
}
|
||||
const { accessedServices } = await findMemberByUserId(
|
||||
ctx.user.id,
|
||||
ctx.session.activeOrganizationId,
|
||||
);
|
||||
if (accessedServices.length === 0) return { items: [], total: 0 };
|
||||
baseConditions.push(
|
||||
sql`${applications.applicationId} IN (${sql.join(
|
||||
accessedServices.map((id) => sql`${id}`),
|
||||
sql`, `,
|
||||
)})`,
|
||||
);
|
||||
|
||||
const where = and(...baseConditions);
|
||||
|
||||
|
||||
@@ -44,7 +44,13 @@ import {
|
||||
} from "@dokploy/server/utils/restore";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { z } from "zod";
|
||||
import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc";
|
||||
import {
|
||||
createTRPCRouter,
|
||||
protectedProcedure,
|
||||
withPermission,
|
||||
} from "@/server/api/trpc";
|
||||
import { checkServicePermissionAndAccess } from "@dokploy/server/services/permission";
|
||||
import { audit } from "@/server/api/utils/audit";
|
||||
import {
|
||||
apiCreateBackup,
|
||||
apiFindOneBackup,
|
||||
@@ -69,10 +75,21 @@ interface RcloneFile {
|
||||
export const backupRouter = createTRPCRouter({
|
||||
create: protectedProcedure
|
||||
.input(apiCreateBackup)
|
||||
.mutation(async ({ input }) => {
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
const newBackup = await createBackup(input);
|
||||
const serviceId =
|
||||
input.postgresId ||
|
||||
input.mysqlId ||
|
||||
input.mariadbId ||
|
||||
input.mongoId ||
|
||||
input.composeId;
|
||||
if (serviceId) {
|
||||
await checkServicePermissionAndAccess(ctx, serviceId, {
|
||||
backup: ["create"],
|
||||
});
|
||||
}
|
||||
|
||||
const newBackup = await createBackup(input);
|
||||
const backup = await findBackupById(newBackup.backupId);
|
||||
|
||||
if (IS_CLOUD && backup.enabled) {
|
||||
@@ -110,6 +127,11 @@ export const backupRouter = createTRPCRouter({
|
||||
scheduleBackup(backup);
|
||||
}
|
||||
}
|
||||
await audit(ctx, {
|
||||
action: "create",
|
||||
resourceType: "backup",
|
||||
resourceId: backup.backupId,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
throw new TRPCError({
|
||||
@@ -122,15 +144,42 @@ export const backupRouter = createTRPCRouter({
|
||||
});
|
||||
}
|
||||
}),
|
||||
one: protectedProcedure.input(apiFindOneBackup).query(async ({ input }) => {
|
||||
const backup = await findBackupById(input.backupId);
|
||||
one: protectedProcedure
|
||||
.input(apiFindOneBackup)
|
||||
.query(async ({ input, ctx }) => {
|
||||
const backup = await findBackupById(input.backupId);
|
||||
|
||||
return backup;
|
||||
}),
|
||||
const serviceId =
|
||||
backup.postgresId ||
|
||||
backup.mysqlId ||
|
||||
backup.mariadbId ||
|
||||
backup.mongoId ||
|
||||
backup.composeId;
|
||||
if (serviceId) {
|
||||
await checkServicePermissionAndAccess(ctx, serviceId, {
|
||||
backup: ["read"],
|
||||
});
|
||||
}
|
||||
|
||||
return backup;
|
||||
}),
|
||||
update: protectedProcedure
|
||||
.input(apiUpdateBackup)
|
||||
.mutation(async ({ input }) => {
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
const existing = await findBackupById(input.backupId);
|
||||
const serviceId =
|
||||
existing.postgresId ||
|
||||
existing.mysqlId ||
|
||||
existing.mariadbId ||
|
||||
existing.mongoId ||
|
||||
existing.composeId;
|
||||
if (serviceId) {
|
||||
await checkServicePermissionAndAccess(ctx, serviceId, {
|
||||
backup: ["update"],
|
||||
});
|
||||
}
|
||||
|
||||
await updateBackupById(input.backupId, input);
|
||||
const backup = await findBackupById(input.backupId);
|
||||
|
||||
@@ -156,6 +205,11 @@ export const backupRouter = createTRPCRouter({
|
||||
removeScheduleBackup(input.backupId);
|
||||
}
|
||||
}
|
||||
await audit(ctx, {
|
||||
action: "update",
|
||||
resourceType: "backup",
|
||||
resourceId: backup.backupId,
|
||||
});
|
||||
} catch (error) {
|
||||
const message =
|
||||
error instanceof Error ? error.message : "Error updating this Backup";
|
||||
@@ -167,8 +221,21 @@ export const backupRouter = createTRPCRouter({
|
||||
}),
|
||||
remove: protectedProcedure
|
||||
.input(apiRemoveBackup)
|
||||
.mutation(async ({ input }) => {
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
const backup = await findBackupById(input.backupId);
|
||||
const serviceId =
|
||||
backup.postgresId ||
|
||||
backup.mysqlId ||
|
||||
backup.mariadbId ||
|
||||
backup.mongoId ||
|
||||
backup.composeId;
|
||||
if (serviceId) {
|
||||
await checkServicePermissionAndAccess(ctx, serviceId, {
|
||||
backup: ["delete"],
|
||||
});
|
||||
}
|
||||
|
||||
const value = await removeBackupById(input.backupId);
|
||||
if (IS_CLOUD && value) {
|
||||
removeJob({
|
||||
@@ -179,6 +246,11 @@ export const backupRouter = createTRPCRouter({
|
||||
} else if (!IS_CLOUD) {
|
||||
removeScheduleBackup(input.backupId);
|
||||
}
|
||||
await audit(ctx, {
|
||||
action: "delete",
|
||||
resourceType: "backup",
|
||||
resourceId: input.backupId,
|
||||
});
|
||||
return value;
|
||||
} catch (error) {
|
||||
const message =
|
||||
@@ -191,13 +263,22 @@ export const backupRouter = createTRPCRouter({
|
||||
}),
|
||||
manualBackupPostgres: protectedProcedure
|
||||
.input(apiFindOneBackup)
|
||||
.mutation(async ({ input }) => {
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
const backup = await findBackupById(input.backupId);
|
||||
if (backup.postgresId) {
|
||||
await checkServicePermissionAndAccess(ctx, backup.postgresId, {
|
||||
backup: ["create"],
|
||||
});
|
||||
}
|
||||
const postgres = await findPostgresByBackupId(backup.backupId);
|
||||
await runPostgresBackup(postgres, backup);
|
||||
|
||||
await keepLatestNBackups(backup, postgres?.serverId);
|
||||
await audit(ctx, {
|
||||
action: "run",
|
||||
resourceType: "backup",
|
||||
resourceId: backup.backupId,
|
||||
});
|
||||
return true;
|
||||
} catch (error) {
|
||||
const message =
|
||||
@@ -213,12 +294,22 @@ export const backupRouter = createTRPCRouter({
|
||||
|
||||
manualBackupMySql: protectedProcedure
|
||||
.input(apiFindOneBackup)
|
||||
.mutation(async ({ input }) => {
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
const backup = await findBackupById(input.backupId);
|
||||
if (backup.mysqlId) {
|
||||
await checkServicePermissionAndAccess(ctx, backup.mysqlId, {
|
||||
backup: ["create"],
|
||||
});
|
||||
}
|
||||
const mysql = await findMySqlByBackupId(backup.backupId);
|
||||
await runMySqlBackup(mysql, backup);
|
||||
await keepLatestNBackups(backup, mysql?.serverId);
|
||||
await audit(ctx, {
|
||||
action: "run",
|
||||
resourceType: "backup",
|
||||
resourceId: backup.backupId,
|
||||
});
|
||||
return true;
|
||||
} catch (error) {
|
||||
throw new TRPCError({
|
||||
@@ -230,12 +321,22 @@ export const backupRouter = createTRPCRouter({
|
||||
}),
|
||||
manualBackupMariadb: protectedProcedure
|
||||
.input(apiFindOneBackup)
|
||||
.mutation(async ({ input }) => {
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
const backup = await findBackupById(input.backupId);
|
||||
if (backup.mariadbId) {
|
||||
await checkServicePermissionAndAccess(ctx, backup.mariadbId, {
|
||||
backup: ["create"],
|
||||
});
|
||||
}
|
||||
const mariadb = await findMariadbByBackupId(backup.backupId);
|
||||
await runMariadbBackup(mariadb, backup);
|
||||
await keepLatestNBackups(backup, mariadb?.serverId);
|
||||
await audit(ctx, {
|
||||
action: "run",
|
||||
resourceType: "backup",
|
||||
resourceId: backup.backupId,
|
||||
});
|
||||
return true;
|
||||
} catch (error) {
|
||||
throw new TRPCError({
|
||||
@@ -247,12 +348,22 @@ export const backupRouter = createTRPCRouter({
|
||||
}),
|
||||
manualBackupCompose: protectedProcedure
|
||||
.input(apiFindOneBackup)
|
||||
.mutation(async ({ input }) => {
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
const backup = await findBackupById(input.backupId);
|
||||
if (backup.composeId) {
|
||||
await checkServicePermissionAndAccess(ctx, backup.composeId, {
|
||||
backup: ["create"],
|
||||
});
|
||||
}
|
||||
const compose = await findComposeByBackupId(backup.backupId);
|
||||
await runComposeBackup(compose, backup);
|
||||
await keepLatestNBackups(backup, compose?.serverId);
|
||||
await audit(ctx, {
|
||||
action: "run",
|
||||
resourceType: "backup",
|
||||
resourceId: backup.backupId,
|
||||
});
|
||||
return true;
|
||||
} catch (error) {
|
||||
throw new TRPCError({
|
||||
@@ -264,12 +375,22 @@ export const backupRouter = createTRPCRouter({
|
||||
}),
|
||||
manualBackupMongo: protectedProcedure
|
||||
.input(apiFindOneBackup)
|
||||
.mutation(async ({ input }) => {
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
const backup = await findBackupById(input.backupId);
|
||||
if (backup.mongoId) {
|
||||
await checkServicePermissionAndAccess(ctx, backup.mongoId, {
|
||||
backup: ["create"],
|
||||
});
|
||||
}
|
||||
const mongo = await findMongoByBackupId(backup.backupId);
|
||||
await runMongoBackup(mongo, backup);
|
||||
await keepLatestNBackups(backup, mongo?.serverId);
|
||||
await audit(ctx, {
|
||||
action: "run",
|
||||
resourceType: "backup",
|
||||
resourceId: backup.backupId,
|
||||
});
|
||||
return true;
|
||||
} catch (error) {
|
||||
throw new TRPCError({
|
||||
@@ -279,15 +400,20 @@ export const backupRouter = createTRPCRouter({
|
||||
});
|
||||
}
|
||||
}),
|
||||
manualBackupWebServer: protectedProcedure
|
||||
manualBackupWebServer: withPermission("backup", "create")
|
||||
.input(apiFindOneBackup)
|
||||
.mutation(async ({ input }) => {
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const backup = await findBackupById(input.backupId);
|
||||
await runWebServerBackup(backup);
|
||||
await keepLatestNBackups(backup);
|
||||
await audit(ctx, {
|
||||
action: "run",
|
||||
resourceType: "backup",
|
||||
resourceId: backup.backupId,
|
||||
});
|
||||
return true;
|
||||
}),
|
||||
listBackupFiles: protectedProcedure
|
||||
listBackupFiles: withPermission("backup", "read")
|
||||
.input(
|
||||
z.object({
|
||||
destinationId: z.string(),
|
||||
@@ -374,7 +500,12 @@ export const backupRouter = createTRPCRouter({
|
||||
},
|
||||
})
|
||||
.input(apiRestoreBackup)
|
||||
.subscription(async function* ({ input, signal }) {
|
||||
.subscription(async function* ({ input, ctx, signal }) {
|
||||
if (input.databaseId) {
|
||||
await checkServicePermissionAndAccess(ctx, input.databaseId, {
|
||||
backup: ["restore"],
|
||||
});
|
||||
}
|
||||
const destination = await findDestinationById(input.destinationId);
|
||||
const queue: string[] = [];
|
||||
const done = false;
|
||||
|
||||
@@ -8,7 +8,12 @@ import {
|
||||
} from "@dokploy/server";
|
||||
import { db } from "@dokploy/server/db";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc";
|
||||
import {
|
||||
createTRPCRouter,
|
||||
protectedProcedure,
|
||||
withPermission,
|
||||
} from "@/server/api/trpc";
|
||||
import { audit } from "@/server/api/utils/audit";
|
||||
import {
|
||||
apiBitbucketTestConnection,
|
||||
apiCreateBitbucket,
|
||||
@@ -18,15 +23,23 @@ import {
|
||||
} from "@/server/db/schema";
|
||||
|
||||
export const bitbucketRouter = createTRPCRouter({
|
||||
create: protectedProcedure
|
||||
create: withPermission("gitProviders", "create")
|
||||
.input(apiCreateBitbucket)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
return await createBitbucket(
|
||||
const result = await createBitbucket(
|
||||
input,
|
||||
ctx.session.activeOrganizationId,
|
||||
ctx.session.userId,
|
||||
);
|
||||
|
||||
await audit(ctx, {
|
||||
action: "create",
|
||||
resourceType: "gitProvider",
|
||||
resourceName: input.name,
|
||||
});
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
@@ -37,19 +50,8 @@ export const bitbucketRouter = createTRPCRouter({
|
||||
}),
|
||||
one: protectedProcedure
|
||||
.input(apiFindOneBitbucket)
|
||||
.query(async ({ input, ctx }) => {
|
||||
const bitbucketProvider = await findBitbucketById(input.bitbucketId);
|
||||
if (
|
||||
bitbucketProvider.gitProvider.organizationId !==
|
||||
ctx.session.activeOrganizationId &&
|
||||
bitbucketProvider.gitProvider.userId !== ctx.session.userId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not allowed to access this bitbucket provider",
|
||||
});
|
||||
}
|
||||
return bitbucketProvider;
|
||||
.query(async ({ input }) => {
|
||||
return await findBitbucketById(input.bitbucketId);
|
||||
}),
|
||||
bitbucketProviders: protectedProcedure.query(async ({ ctx }) => {
|
||||
let result = await db.query.bitbucket.findMany({
|
||||
@@ -73,53 +75,18 @@ export const bitbucketRouter = createTRPCRouter({
|
||||
|
||||
getBitbucketRepositories: protectedProcedure
|
||||
.input(apiFindOneBitbucket)
|
||||
.query(async ({ input, ctx }) => {
|
||||
const bitbucketProvider = await findBitbucketById(input.bitbucketId);
|
||||
if (
|
||||
bitbucketProvider.gitProvider.organizationId !==
|
||||
ctx.session.activeOrganizationId &&
|
||||
bitbucketProvider.gitProvider.userId !== ctx.session.userId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not allowed to access this bitbucket provider",
|
||||
});
|
||||
}
|
||||
.query(async ({ input }) => {
|
||||
return await getBitbucketRepositories(input.bitbucketId);
|
||||
}),
|
||||
getBitbucketBranches: protectedProcedure
|
||||
.input(apiFindBitbucketBranches)
|
||||
.query(async ({ input, ctx }) => {
|
||||
const bitbucketProvider = await findBitbucketById(
|
||||
input.bitbucketId || "",
|
||||
);
|
||||
if (
|
||||
bitbucketProvider.gitProvider.organizationId !==
|
||||
ctx.session.activeOrganizationId &&
|
||||
bitbucketProvider.gitProvider.userId !== ctx.session.userId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not allowed to access this bitbucket provider",
|
||||
});
|
||||
}
|
||||
.query(async ({ input }) => {
|
||||
return await getBitbucketBranches(input);
|
||||
}),
|
||||
testConnection: protectedProcedure
|
||||
.input(apiBitbucketTestConnection)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
.mutation(async ({ input }) => {
|
||||
try {
|
||||
const bitbucketProvider = await findBitbucketById(input.bitbucketId);
|
||||
if (
|
||||
bitbucketProvider.gitProvider.organizationId !==
|
||||
ctx.session.activeOrganizationId &&
|
||||
bitbucketProvider.gitProvider.userId !== ctx.session.userId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not allowed to access this bitbucket provider",
|
||||
});
|
||||
}
|
||||
const result = await testBitbucketConnection(input);
|
||||
|
||||
return `Found ${result} repositories`;
|
||||
@@ -130,23 +97,21 @@ export const bitbucketRouter = createTRPCRouter({
|
||||
});
|
||||
}
|
||||
}),
|
||||
update: protectedProcedure
|
||||
update: withPermission("gitProviders", "create")
|
||||
.input(apiUpdateBitbucket)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const bitbucketProvider = await findBitbucketById(input.bitbucketId);
|
||||
if (
|
||||
bitbucketProvider.gitProvider.organizationId !==
|
||||
ctx.session.activeOrganizationId &&
|
||||
bitbucketProvider.gitProvider.userId !== ctx.session.userId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not allowed to access this bitbucket provider",
|
||||
});
|
||||
}
|
||||
return await updateBitbucket(input.bitbucketId, {
|
||||
const result = await updateBitbucket(input.bitbucketId, {
|
||||
...input,
|
||||
organizationId: ctx.session.activeOrganizationId,
|
||||
});
|
||||
|
||||
await audit(ctx, {
|
||||
action: "update",
|
||||
resourceType: "gitProvider",
|
||||
resourceId: input.bitbucketId,
|
||||
resourceName: input.name,
|
||||
});
|
||||
|
||||
return result;
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -7,7 +7,8 @@ import {
|
||||
import { db } from "@dokploy/server/db";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { adminProcedure, createTRPCRouter } from "@/server/api/trpc";
|
||||
import { audit } from "@/server/api/utils/audit";
|
||||
import { createTRPCRouter, withPermission } from "@/server/api/trpc";
|
||||
import {
|
||||
apiCreateCertificate,
|
||||
apiFindCertificate,
|
||||
@@ -15,7 +16,7 @@ import {
|
||||
} from "@/server/db/schema";
|
||||
|
||||
export const certificateRouter = createTRPCRouter({
|
||||
create: adminProcedure
|
||||
create: withPermission("certificate", "create")
|
||||
.input(apiCreateCertificate)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
if (IS_CLOUD && !input.serverId) {
|
||||
@@ -24,10 +25,20 @@ export const certificateRouter = createTRPCRouter({
|
||||
message: "Please set a server to create a certificate",
|
||||
});
|
||||
}
|
||||
return await createCertificate(input, ctx.session.activeOrganizationId);
|
||||
const cert = await createCertificate(
|
||||
input,
|
||||
ctx.session.activeOrganizationId,
|
||||
);
|
||||
await audit(ctx, {
|
||||
action: "create",
|
||||
resourceType: "certificate",
|
||||
resourceId: cert.certificateId,
|
||||
resourceName: cert.name,
|
||||
});
|
||||
return cert;
|
||||
}),
|
||||
|
||||
one: adminProcedure
|
||||
one: withPermission("certificate", "read")
|
||||
.input(apiFindCertificate)
|
||||
.query(async ({ input, ctx }) => {
|
||||
const certificates = await findCertificateById(input.certificateId);
|
||||
@@ -39,7 +50,7 @@ export const certificateRouter = createTRPCRouter({
|
||||
}
|
||||
return certificates;
|
||||
}),
|
||||
remove: adminProcedure
|
||||
remove: withPermission("certificate", "delete")
|
||||
.input(apiFindCertificate)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const certificates = await findCertificateById(input.certificateId);
|
||||
@@ -49,10 +60,16 @@ export const certificateRouter = createTRPCRouter({
|
||||
message: "You are not allowed to delete this certificate",
|
||||
});
|
||||
}
|
||||
await audit(ctx, {
|
||||
action: "delete",
|
||||
resourceType: "certificate",
|
||||
resourceId: certificates.certificateId,
|
||||
resourceName: certificates.name,
|
||||
});
|
||||
await removeCertificateById(input.certificateId);
|
||||
return true;
|
||||
}),
|
||||
all: adminProcedure.query(async ({ ctx }) => {
|
||||
all: withPermission("certificate", "read").query(async ({ ctx }) => {
|
||||
return await db.query.certificates.findMany({
|
||||
where: eq(certificates.organizationId, ctx.session.activeOrganizationId),
|
||||
});
|
||||
|
||||
@@ -7,10 +7,12 @@ import {
|
||||
} from "@dokploy/server";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { z } from "zod";
|
||||
import { audit } from "@/server/api/utils/audit";
|
||||
import { getLocalServerIp } from "@/server/wss/terminal";
|
||||
import { createTRPCRouter, protectedProcedure } from "../trpc";
|
||||
import { createTRPCRouter, withPermission } from "../trpc";
|
||||
|
||||
export const clusterRouter = createTRPCRouter({
|
||||
getNodes: protectedProcedure
|
||||
getNodes: withPermission("server", "read")
|
||||
.input(
|
||||
z.object({
|
||||
serverId: z.string().optional(),
|
||||
@@ -19,17 +21,17 @@ export const clusterRouter = createTRPCRouter({
|
||||
.query(async ({ input }) => {
|
||||
const docker = await getRemoteDocker(input.serverId);
|
||||
const workers: DockerNode[] = await docker.listNodes();
|
||||
|
||||
return workers;
|
||||
}),
|
||||
removeWorker: protectedProcedure
|
||||
|
||||
removeWorker: withPermission("server", "delete")
|
||||
.input(
|
||||
z.object({
|
||||
nodeId: z.string(),
|
||||
serverId: z.string().optional(),
|
||||
}),
|
||||
)
|
||||
.mutation(async ({ input }) => {
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
const drainCommand = `docker node update --availability drain ${input.nodeId}`;
|
||||
const removeCommand = `docker node rm ${input.nodeId} --force`;
|
||||
@@ -41,6 +43,12 @@ export const clusterRouter = createTRPCRouter({
|
||||
await execAsync(drainCommand);
|
||||
await execAsync(removeCommand);
|
||||
}
|
||||
await audit(ctx, {
|
||||
action: "delete",
|
||||
resourceType: "cluster",
|
||||
resourceId: input.nodeId,
|
||||
resourceName: input.nodeId,
|
||||
});
|
||||
return true;
|
||||
} catch (error) {
|
||||
throw new TRPCError({
|
||||
@@ -50,7 +58,8 @@ export const clusterRouter = createTRPCRouter({
|
||||
});
|
||||
}
|
||||
}),
|
||||
addWorker: protectedProcedure
|
||||
|
||||
addWorker: withPermission("server", "create")
|
||||
.input(
|
||||
z.object({
|
||||
serverId: z.string().optional(),
|
||||
@@ -68,13 +77,12 @@ export const clusterRouter = createTRPCRouter({
|
||||
}
|
||||
|
||||
return {
|
||||
command: `docker swarm join --token ${
|
||||
result.JoinTokens.Worker
|
||||
} ${ip}:2377`,
|
||||
command: `docker swarm join --token ${result.JoinTokens.Worker} ${ip}:2377`,
|
||||
version: docker_version.Version,
|
||||
};
|
||||
}),
|
||||
addManager: protectedProcedure
|
||||
|
||||
addManager: withPermission("server", "create")
|
||||
.input(
|
||||
z.object({
|
||||
serverId: z.string().optional(),
|
||||
@@ -91,9 +99,7 @@ export const clusterRouter = createTRPCRouter({
|
||||
ip = server?.ipAddress;
|
||||
}
|
||||
return {
|
||||
command: `docker swarm join --token ${
|
||||
result.JoinTokens.Manager
|
||||
} ${ip}:2377`,
|
||||
command: `docker swarm join --token ${result.JoinTokens.Manager} ${ip}:2377`,
|
||||
version: docker_version.Version,
|
||||
};
|
||||
}),
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import {
|
||||
addDomainToCompose,
|
||||
addNewService,
|
||||
checkServiceAccess,
|
||||
clearOldDeployments,
|
||||
cloneCompose,
|
||||
createCommand,
|
||||
@@ -16,7 +14,6 @@ import {
|
||||
findDomainsByComposeId,
|
||||
findEnvironmentById,
|
||||
findGitProviderById,
|
||||
findMemberById,
|
||||
findProjectById,
|
||||
findServerById,
|
||||
getComposeContainer,
|
||||
@@ -34,6 +31,12 @@ import {
|
||||
updateCompose,
|
||||
updateDeploymentStatus,
|
||||
} from "@dokploy/server";
|
||||
import {
|
||||
addNewService,
|
||||
checkServiceAccess,
|
||||
checkServicePermissionAndAccess,
|
||||
findMemberByUserId,
|
||||
} from "@dokploy/server/services/permission";
|
||||
import { db } from "@dokploy/server/db";
|
||||
import {
|
||||
type CompleteTemplate,
|
||||
@@ -72,6 +75,7 @@ import {
|
||||
} from "@/server/queues/queueSetup";
|
||||
import { cancelDeployment, deploy } from "@/server/utils/deploy";
|
||||
import { generatePassword } from "@/templates/utils";
|
||||
import { audit } from "../utils/audit";
|
||||
import { createTRPCRouter, protectedProcedure, publicProcedure } from "../trpc";
|
||||
|
||||
export const composeRouter = createTRPCRouter({
|
||||
@@ -79,18 +83,10 @@ export const composeRouter = createTRPCRouter({
|
||||
.input(apiCreateCompose)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
try {
|
||||
// Get project from environment
|
||||
const environment = await findEnvironmentById(input.environmentId);
|
||||
const project = await findProjectById(environment.projectId);
|
||||
|
||||
if (ctx.user.role === "member") {
|
||||
await checkServiceAccess(
|
||||
ctx.user.id,
|
||||
project.projectId,
|
||||
ctx.session.activeOrganizationId,
|
||||
"create",
|
||||
);
|
||||
}
|
||||
await checkServiceAccess(ctx, project.projectId, "create");
|
||||
|
||||
if (IS_CLOUD && !input.serverId) {
|
||||
throw new TRPCError({
|
||||
@@ -108,14 +104,14 @@ export const composeRouter = createTRPCRouter({
|
||||
...input,
|
||||
});
|
||||
|
||||
if (ctx.user.role === "member") {
|
||||
await addNewService(
|
||||
ctx.user.id,
|
||||
newService.composeId,
|
||||
project.organizationId,
|
||||
);
|
||||
}
|
||||
await addNewService(ctx, newService.composeId);
|
||||
|
||||
await audit(ctx, {
|
||||
action: "create",
|
||||
resourceType: "service",
|
||||
resourceId: newService.composeId,
|
||||
resourceName: newService.appName,
|
||||
});
|
||||
return newService;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
@@ -125,14 +121,7 @@ export const composeRouter = createTRPCRouter({
|
||||
one: protectedProcedure
|
||||
.input(apiFindCompose)
|
||||
.query(async ({ input, ctx }) => {
|
||||
if (ctx.user.role === "member") {
|
||||
await checkServiceAccess(
|
||||
ctx.user.id,
|
||||
input.composeId,
|
||||
ctx.session.activeOrganizationId,
|
||||
"access",
|
||||
);
|
||||
}
|
||||
await checkServiceAccess(ctx, input.composeId, "read");
|
||||
|
||||
const compose = await findComposeById(input.composeId);
|
||||
if (
|
||||
@@ -188,29 +177,22 @@ export const composeRouter = createTRPCRouter({
|
||||
update: protectedProcedure
|
||||
.input(apiUpdateCompose)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const compose = await findComposeById(input.composeId);
|
||||
if (
|
||||
compose.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to update this compose",
|
||||
});
|
||||
}
|
||||
return updateCompose(input.composeId, input);
|
||||
await checkServicePermissionAndAccess(ctx, input.composeId, {
|
||||
service: ["create"],
|
||||
});
|
||||
const updated = await updateCompose(input.composeId, input);
|
||||
await audit(ctx, {
|
||||
action: "update",
|
||||
resourceType: "compose",
|
||||
resourceId: input.composeId,
|
||||
resourceName: updated?.name,
|
||||
});
|
||||
return updated;
|
||||
}),
|
||||
delete: protectedProcedure
|
||||
.input(apiDeleteCompose)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
if (ctx.user.role === "member") {
|
||||
await checkServiceAccess(
|
||||
ctx.user.id,
|
||||
input.composeId,
|
||||
ctx.session.activeOrganizationId,
|
||||
"delete",
|
||||
);
|
||||
}
|
||||
await checkServiceAccess(ctx, input.composeId, "delete");
|
||||
const composeResult = await findComposeById(input.composeId);
|
||||
|
||||
if (
|
||||
@@ -249,70 +231,55 @@ export const composeRouter = createTRPCRouter({
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
await audit(ctx, {
|
||||
action: "delete",
|
||||
resourceType: "service",
|
||||
resourceId: composeResult.composeId,
|
||||
resourceName: composeResult.appName,
|
||||
});
|
||||
return composeResult;
|
||||
}),
|
||||
cleanQueues: protectedProcedure
|
||||
.input(apiFindCompose)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const compose = await findComposeById(input.composeId);
|
||||
if (
|
||||
compose.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to clean this compose",
|
||||
});
|
||||
}
|
||||
await checkServicePermissionAndAccess(ctx, input.composeId, {
|
||||
deployment: ["create"],
|
||||
});
|
||||
await cleanQueuesByCompose(input.composeId);
|
||||
return { success: true, message: "Queues cleaned successfully" };
|
||||
}),
|
||||
clearDeployments: protectedProcedure
|
||||
.input(apiFindCompose)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.composeId, {
|
||||
deployment: ["create"],
|
||||
});
|
||||
const compose = await findComposeById(input.composeId);
|
||||
if (
|
||||
compose.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message:
|
||||
"You are not authorized to clear deployments for this compose",
|
||||
});
|
||||
}
|
||||
await clearOldDeployments(compose.appName, compose.serverId);
|
||||
await audit(ctx, {
|
||||
action: "update",
|
||||
resourceType: "compose",
|
||||
resourceId: input.composeId,
|
||||
resourceName: compose.name,
|
||||
});
|
||||
return true;
|
||||
}),
|
||||
killBuild: protectedProcedure
|
||||
.input(apiFindCompose)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.composeId, {
|
||||
deployment: ["cancel"],
|
||||
});
|
||||
const compose = await findComposeById(input.composeId);
|
||||
if (
|
||||
compose.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to kill this build",
|
||||
});
|
||||
}
|
||||
await killDockerBuild("compose", compose.serverId);
|
||||
}),
|
||||
|
||||
loadServices: protectedProcedure
|
||||
.input(apiFetchServices)
|
||||
.query(async ({ input, ctx }) => {
|
||||
const compose = await findComposeById(input.composeId);
|
||||
if (
|
||||
compose.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to load this compose",
|
||||
});
|
||||
}
|
||||
await checkServicePermissionAndAccess(ctx, input.composeId, {
|
||||
service: ["create"],
|
||||
});
|
||||
return await loadServices(input.composeId, input.type);
|
||||
}),
|
||||
loadMountsByService: protectedProcedure
|
||||
@@ -323,16 +290,10 @@ export const composeRouter = createTRPCRouter({
|
||||
}),
|
||||
)
|
||||
.query(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.composeId, {
|
||||
service: ["create"],
|
||||
});
|
||||
const compose = await findComposeById(input.composeId);
|
||||
if (
|
||||
compose.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to load this compose",
|
||||
});
|
||||
}
|
||||
const container = await getComposeContainer(compose, input.serviceName);
|
||||
const mounts = container?.Mounts.filter(
|
||||
(mount) => mount.Type === "volume" && mount.Source !== "",
|
||||
@@ -343,18 +304,11 @@ export const composeRouter = createTRPCRouter({
|
||||
.input(apiFindCompose)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
await checkServicePermissionAndAccess(ctx, input.composeId, {
|
||||
service: ["create"],
|
||||
});
|
||||
const compose = await findComposeById(input.composeId);
|
||||
|
||||
if (
|
||||
compose.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to fetch this compose",
|
||||
});
|
||||
}
|
||||
|
||||
const command = await cloneCompose(compose);
|
||||
if (compose.serverId) {
|
||||
await execAsyncRemote(compose.serverId, command);
|
||||
@@ -374,49 +328,45 @@ export const composeRouter = createTRPCRouter({
|
||||
randomizeCompose: protectedProcedure
|
||||
.input(apiRandomizeCompose)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.composeId, {
|
||||
service: ["create"],
|
||||
});
|
||||
const result = await randomizeComposeFile(input.composeId, input.suffix);
|
||||
const compose = await findComposeById(input.composeId);
|
||||
if (
|
||||
compose.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to randomize this compose",
|
||||
});
|
||||
}
|
||||
return await randomizeComposeFile(input.composeId, input.suffix);
|
||||
await audit(ctx, {
|
||||
action: "update",
|
||||
resourceType: "compose",
|
||||
resourceId: input.composeId,
|
||||
resourceName: compose.name,
|
||||
});
|
||||
return result;
|
||||
}),
|
||||
isolatedDeployment: protectedProcedure
|
||||
.input(apiRandomizeCompose)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const compose = await findComposeById(input.composeId);
|
||||
if (
|
||||
compose.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to randomize this compose",
|
||||
});
|
||||
}
|
||||
return await randomizeIsolatedDeploymentComposeFile(
|
||||
await checkServicePermissionAndAccess(ctx, input.composeId, {
|
||||
service: ["create"],
|
||||
});
|
||||
const result = await randomizeIsolatedDeploymentComposeFile(
|
||||
input.composeId,
|
||||
input.suffix,
|
||||
);
|
||||
const compose = await findComposeById(input.composeId);
|
||||
await audit(ctx, {
|
||||
action: "update",
|
||||
resourceType: "compose",
|
||||
resourceId: input.composeId,
|
||||
resourceName: compose.name,
|
||||
});
|
||||
return result;
|
||||
}),
|
||||
getConvertedCompose: protectedProcedure
|
||||
.input(apiFindCompose)
|
||||
.query(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.composeId, {
|
||||
service: ["create"],
|
||||
});
|
||||
const compose = await findComposeById(input.composeId);
|
||||
if (
|
||||
compose.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to get this compose",
|
||||
});
|
||||
}
|
||||
const domains = await findDomainsByComposeId(input.composeId);
|
||||
const composeFile = await addDomainToCompose(compose, domains);
|
||||
return stringify(composeFile, {
|
||||
@@ -427,17 +377,11 @@ export const composeRouter = createTRPCRouter({
|
||||
deploy: protectedProcedure
|
||||
.input(apiDeployCompose)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.composeId, {
|
||||
deployment: ["create"],
|
||||
});
|
||||
const compose = await findComposeById(input.composeId);
|
||||
|
||||
if (
|
||||
compose.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to deploy this compose",
|
||||
});
|
||||
}
|
||||
const jobData: DeploymentJob = {
|
||||
composeId: input.composeId,
|
||||
titleLog: input.title || "Manual deployment",
|
||||
@@ -452,6 +396,12 @@ export const composeRouter = createTRPCRouter({
|
||||
deploy(jobData).catch((error) => {
|
||||
console.error("Background deployment failed:", error);
|
||||
});
|
||||
await audit(ctx, {
|
||||
action: "deploy",
|
||||
resourceType: "compose",
|
||||
resourceId: input.composeId,
|
||||
resourceName: compose.name,
|
||||
});
|
||||
return true;
|
||||
}
|
||||
await myQueue.add(
|
||||
@@ -462,6 +412,12 @@ export const composeRouter = createTRPCRouter({
|
||||
removeOnFail: true,
|
||||
},
|
||||
);
|
||||
await audit(ctx, {
|
||||
action: "deploy",
|
||||
resourceType: "compose",
|
||||
resourceId: input.composeId,
|
||||
resourceName: compose.name,
|
||||
});
|
||||
return {
|
||||
success: true,
|
||||
message: "Deployment queued",
|
||||
@@ -471,16 +427,10 @@ export const composeRouter = createTRPCRouter({
|
||||
redeploy: protectedProcedure
|
||||
.input(apiRedeployCompose)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.composeId, {
|
||||
deployment: ["create"],
|
||||
});
|
||||
const compose = await findComposeById(input.composeId);
|
||||
if (
|
||||
compose.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to redeploy this compose",
|
||||
});
|
||||
}
|
||||
const jobData: DeploymentJob = {
|
||||
composeId: input.composeId,
|
||||
titleLog: input.title || "Rebuild deployment",
|
||||
@@ -494,6 +444,12 @@ export const composeRouter = createTRPCRouter({
|
||||
deploy(jobData).catch((error) => {
|
||||
console.error("Background deployment failed:", error);
|
||||
});
|
||||
await audit(ctx, {
|
||||
action: "deploy",
|
||||
resourceType: "compose",
|
||||
resourceId: input.composeId,
|
||||
resourceName: compose.name,
|
||||
});
|
||||
return true;
|
||||
}
|
||||
await myQueue.add(
|
||||
@@ -504,6 +460,12 @@ export const composeRouter = createTRPCRouter({
|
||||
removeOnFail: true,
|
||||
},
|
||||
);
|
||||
await audit(ctx, {
|
||||
action: "deploy",
|
||||
resourceType: "compose",
|
||||
resourceId: input.composeId,
|
||||
resourceName: compose.name,
|
||||
});
|
||||
return {
|
||||
success: true,
|
||||
message: "Redeployment queued",
|
||||
@@ -513,70 +475,61 @@ export const composeRouter = createTRPCRouter({
|
||||
stop: protectedProcedure
|
||||
.input(apiFindCompose)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const compose = await findComposeById(input.composeId);
|
||||
if (
|
||||
compose.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to stop this compose",
|
||||
});
|
||||
}
|
||||
await checkServicePermissionAndAccess(ctx, input.composeId, {
|
||||
deployment: ["create"],
|
||||
});
|
||||
await stopCompose(input.composeId);
|
||||
|
||||
const composeForStop = await findComposeById(input.composeId);
|
||||
await audit(ctx, {
|
||||
action: "stop",
|
||||
resourceType: "compose",
|
||||
resourceId: input.composeId,
|
||||
resourceName: composeForStop.name,
|
||||
});
|
||||
return true;
|
||||
}),
|
||||
start: protectedProcedure
|
||||
.input(apiFindCompose)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const compose = await findComposeById(input.composeId);
|
||||
if (
|
||||
compose.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to stop this compose",
|
||||
});
|
||||
}
|
||||
await checkServicePermissionAndAccess(ctx, input.composeId, {
|
||||
deployment: ["create"],
|
||||
});
|
||||
await startCompose(input.composeId);
|
||||
|
||||
const composeForStart = await findComposeById(input.composeId);
|
||||
await audit(ctx, {
|
||||
action: "start",
|
||||
resourceType: "compose",
|
||||
resourceId: input.composeId,
|
||||
resourceName: composeForStart.name,
|
||||
});
|
||||
return true;
|
||||
}),
|
||||
getDefaultCommand: protectedProcedure
|
||||
.input(apiFindCompose)
|
||||
.query(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.composeId, {
|
||||
service: ["create"],
|
||||
});
|
||||
const compose = await findComposeById(input.composeId);
|
||||
|
||||
if (
|
||||
compose.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to get this compose",
|
||||
});
|
||||
}
|
||||
const command = createCommand(compose);
|
||||
return `docker ${command}`;
|
||||
}),
|
||||
refreshToken: protectedProcedure
|
||||
.input(apiFindCompose)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const compose = await findComposeById(input.composeId);
|
||||
if (
|
||||
compose.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to refresh this compose",
|
||||
});
|
||||
}
|
||||
await checkServicePermissionAndAccess(ctx, input.composeId, {
|
||||
service: ["create"],
|
||||
});
|
||||
await updateCompose(input.composeId, {
|
||||
refreshToken: nanoid(),
|
||||
});
|
||||
const composeForToken = await findComposeById(input.composeId);
|
||||
await audit(ctx, {
|
||||
action: "update",
|
||||
resourceType: "compose",
|
||||
resourceId: input.composeId,
|
||||
resourceName: composeForToken.name,
|
||||
});
|
||||
return true;
|
||||
}),
|
||||
deployTemplate: protectedProcedure
|
||||
@@ -591,14 +544,7 @@ export const composeRouter = createTRPCRouter({
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const environment = await findEnvironmentById(input.environmentId);
|
||||
|
||||
if (ctx.user.role === "member") {
|
||||
await checkServiceAccess(
|
||||
ctx.user.id,
|
||||
environment.projectId,
|
||||
ctx.session.activeOrganizationId,
|
||||
"create",
|
||||
);
|
||||
}
|
||||
await checkServiceAccess(ctx, environment.projectId, "create");
|
||||
|
||||
if (IS_CLOUD && !input.serverId) {
|
||||
throw new TRPCError({
|
||||
@@ -648,13 +594,7 @@ export const composeRouter = createTRPCRouter({
|
||||
isolatedDeployment: true,
|
||||
});
|
||||
|
||||
if (ctx.user.role === "member") {
|
||||
await addNewService(
|
||||
ctx.user.id,
|
||||
compose.composeId,
|
||||
ctx.session.activeOrganizationId,
|
||||
);
|
||||
}
|
||||
await addNewService(ctx, compose.composeId);
|
||||
|
||||
if (generate.mounts && generate.mounts?.length > 0) {
|
||||
for (const mount of generate.mounts) {
|
||||
@@ -681,6 +621,12 @@ export const composeRouter = createTRPCRouter({
|
||||
}
|
||||
}
|
||||
|
||||
await audit(ctx, {
|
||||
action: "create",
|
||||
resourceType: "compose",
|
||||
resourceId: compose.composeId,
|
||||
resourceName: compose.name,
|
||||
});
|
||||
return compose;
|
||||
}),
|
||||
|
||||
@@ -714,20 +660,11 @@ export const composeRouter = createTRPCRouter({
|
||||
disconnectGitProvider: protectedProcedure
|
||||
.input(apiFindCompose)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const compose = await findComposeById(input.composeId);
|
||||
if (
|
||||
compose.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to disconnect this git provider",
|
||||
});
|
||||
}
|
||||
await checkServicePermissionAndAccess(ctx, input.composeId, {
|
||||
service: ["create"],
|
||||
});
|
||||
|
||||
// Reset all git provider related fields
|
||||
await updateCompose(input.composeId, {
|
||||
// GitHub fields
|
||||
repository: null,
|
||||
branch: null,
|
||||
owner: null,
|
||||
@@ -735,7 +672,6 @@ export const composeRouter = createTRPCRouter({
|
||||
githubId: null,
|
||||
triggerType: "push",
|
||||
|
||||
// GitLab fields
|
||||
gitlabRepository: null,
|
||||
gitlabOwner: null,
|
||||
gitlabBranch: null,
|
||||
@@ -743,30 +679,33 @@ export const composeRouter = createTRPCRouter({
|
||||
gitlabProjectId: null,
|
||||
gitlabPathNamespace: null,
|
||||
|
||||
// Bitbucket fields
|
||||
bitbucketRepository: null,
|
||||
bitbucketOwner: null,
|
||||
bitbucketBranch: null,
|
||||
bitbucketId: null,
|
||||
|
||||
// Gitea fields
|
||||
giteaRepository: null,
|
||||
giteaOwner: null,
|
||||
giteaBranch: null,
|
||||
giteaId: null,
|
||||
|
||||
// Custom Git fields
|
||||
customGitBranch: null,
|
||||
customGitUrl: null,
|
||||
customGitSSHKeyId: null,
|
||||
|
||||
// Common fields
|
||||
sourceType: "github", // Reset to default
|
||||
composeStatus: "idle",
|
||||
watchPaths: null,
|
||||
enableSubmodules: false,
|
||||
});
|
||||
|
||||
const composeForDisconnect = await findComposeById(input.composeId);
|
||||
await audit(ctx, {
|
||||
action: "update",
|
||||
resourceType: "compose",
|
||||
resourceId: input.composeId,
|
||||
resourceName: composeForDisconnect.name,
|
||||
});
|
||||
return true;
|
||||
}),
|
||||
|
||||
@@ -778,29 +717,9 @@ export const composeRouter = createTRPCRouter({
|
||||
}),
|
||||
)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const compose = await findComposeById(input.composeId);
|
||||
if (
|
||||
compose.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to move this compose",
|
||||
});
|
||||
}
|
||||
|
||||
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 environment",
|
||||
});
|
||||
}
|
||||
await checkServicePermissionAndAccess(ctx, input.composeId, {
|
||||
service: ["create"],
|
||||
});
|
||||
|
||||
const updatedCompose = await db
|
||||
.update(composeTable)
|
||||
@@ -818,6 +737,12 @@ export const composeRouter = createTRPCRouter({
|
||||
});
|
||||
}
|
||||
|
||||
await audit(ctx, {
|
||||
action: "update",
|
||||
resourceType: "compose",
|
||||
resourceId: input.composeId,
|
||||
resourceName: updatedCompose.name,
|
||||
});
|
||||
return updatedCompose;
|
||||
}),
|
||||
|
||||
@@ -830,18 +755,11 @@ export const composeRouter = createTRPCRouter({
|
||||
)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
await checkServicePermissionAndAccess(ctx, input.composeId, {
|
||||
service: ["create"],
|
||||
});
|
||||
const compose = await findComposeById(input.composeId);
|
||||
|
||||
if (
|
||||
compose.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to update this compose",
|
||||
});
|
||||
}
|
||||
|
||||
const decodedData = Buffer.from(input.base64, "base64").toString(
|
||||
"utf-8",
|
||||
);
|
||||
@@ -901,21 +819,14 @@ export const composeRouter = createTRPCRouter({
|
||||
)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
await checkServicePermissionAndAccess(ctx, input.composeId, {
|
||||
service: ["create"],
|
||||
});
|
||||
const compose = await findComposeById(input.composeId);
|
||||
const decodedData = Buffer.from(input.base64, "base64").toString(
|
||||
"utf-8",
|
||||
);
|
||||
|
||||
if (
|
||||
compose.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to update this compose",
|
||||
});
|
||||
}
|
||||
|
||||
for (const mount of compose.mounts) {
|
||||
await deleteMount(mount.mountId);
|
||||
}
|
||||
@@ -993,6 +904,12 @@ export const composeRouter = createTRPCRouter({
|
||||
}
|
||||
}
|
||||
|
||||
await audit(ctx, {
|
||||
action: "update",
|
||||
resourceType: "compose",
|
||||
resourceId: input.composeId,
|
||||
resourceName: compose.appName,
|
||||
});
|
||||
return {
|
||||
success: true,
|
||||
message: "Template imported successfully",
|
||||
@@ -1008,16 +925,10 @@ export const composeRouter = createTRPCRouter({
|
||||
cancelDeployment: protectedProcedure
|
||||
.input(apiFindCompose)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.composeId, {
|
||||
deployment: ["cancel"],
|
||||
});
|
||||
const compose = await findComposeById(input.composeId);
|
||||
if (
|
||||
compose.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to cancel this deployment",
|
||||
});
|
||||
}
|
||||
|
||||
if (IS_CLOUD && compose.serverId) {
|
||||
try {
|
||||
@@ -1037,6 +948,12 @@ export const composeRouter = createTRPCRouter({
|
||||
applicationType: "compose",
|
||||
});
|
||||
|
||||
await audit(ctx, {
|
||||
action: "stop",
|
||||
resourceType: "compose",
|
||||
resourceId: input.composeId,
|
||||
resourceName: compose.name,
|
||||
});
|
||||
return {
|
||||
success: true,
|
||||
message: "Deployment cancellation requested",
|
||||
@@ -1113,19 +1030,17 @@ export const composeRouter = createTRPCRouter({
|
||||
);
|
||||
}
|
||||
|
||||
if (ctx.user.role === "member") {
|
||||
const { accessedServices } = await findMemberById(
|
||||
ctx.user.id,
|
||||
ctx.session.activeOrganizationId,
|
||||
);
|
||||
if (accessedServices.length === 0) return { items: [], total: 0 };
|
||||
baseConditions.push(
|
||||
sql`${composeTable.composeId} IN (${sql.join(
|
||||
accessedServices.map((id) => sql`${id}`),
|
||||
sql`, `,
|
||||
)})`,
|
||||
);
|
||||
}
|
||||
const { accessedServices } = await findMemberByUserId(
|
||||
ctx.user.id,
|
||||
ctx.session.activeOrganizationId,
|
||||
);
|
||||
if (accessedServices.length === 0) return { items: [], total: 0 };
|
||||
baseConditions.push(
|
||||
sql`${composeTable.composeId} IN (${sql.join(
|
||||
accessedServices.map((id) => sql`${id}`),
|
||||
sql`, `,
|
||||
)})`,
|
||||
);
|
||||
|
||||
const where = and(...baseConditions);
|
||||
|
||||
|
||||
@@ -5,20 +5,21 @@ import {
|
||||
findAllDeploymentsByComposeId,
|
||||
findAllDeploymentsByServerId,
|
||||
findAllDeploymentsCentralized,
|
||||
findApplicationById,
|
||||
findComposeById,
|
||||
findDeploymentById,
|
||||
findMemberById,
|
||||
findServerById,
|
||||
IS_CLOUD,
|
||||
removeDeployment,
|
||||
resolveServicePath,
|
||||
updateDeploymentStatus,
|
||||
} from "@dokploy/server";
|
||||
import { db } from "@dokploy/server/db";
|
||||
import {
|
||||
checkServicePermissionAndAccess,
|
||||
findMemberByUserId,
|
||||
} from "@dokploy/server/services/permission";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { desc, eq } from "drizzle-orm";
|
||||
import { z } from "zod";
|
||||
import { audit } from "@/server/api/utils/audit";
|
||||
import {
|
||||
apiFindAllByApplication,
|
||||
apiFindAllByCompose,
|
||||
@@ -29,65 +30,46 @@ import {
|
||||
} from "@/server/db/schema";
|
||||
import { myQueue } from "@/server/queues/queueSetup";
|
||||
import { fetchDeployApiJobs, type QueueJobRow } from "@/server/utils/deploy";
|
||||
import { createTRPCRouter, protectedProcedure } from "../trpc";
|
||||
import { createTRPCRouter, protectedProcedure, withPermission } from "../trpc";
|
||||
|
||||
export const deploymentRouter = createTRPCRouter({
|
||||
all: protectedProcedure
|
||||
.input(apiFindAllByApplication)
|
||||
.query(async ({ input, ctx }) => {
|
||||
const application = await findApplicationById(input.applicationId);
|
||||
if (
|
||||
application.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to access this application",
|
||||
});
|
||||
}
|
||||
await checkServicePermissionAndAccess(ctx, input.applicationId, {
|
||||
deployment: ["read"],
|
||||
});
|
||||
return await findAllDeploymentsByApplicationId(input.applicationId);
|
||||
}),
|
||||
|
||||
allByCompose: protectedProcedure
|
||||
.input(apiFindAllByCompose)
|
||||
.query(async ({ input, ctx }) => {
|
||||
const compose = await findComposeById(input.composeId);
|
||||
if (
|
||||
compose.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to access this compose",
|
||||
});
|
||||
}
|
||||
await checkServicePermissionAndAccess(ctx, input.composeId, {
|
||||
deployment: ["read"],
|
||||
});
|
||||
return await findAllDeploymentsByComposeId(input.composeId);
|
||||
}),
|
||||
allByServer: protectedProcedure
|
||||
allByServer: withPermission("deployment", "read")
|
||||
.input(apiFindAllByServer)
|
||||
.query(async ({ input, ctx }) => {
|
||||
const server = await findServerById(input.serverId);
|
||||
if (server.organizationId !== ctx.session.activeOrganizationId) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to access this server",
|
||||
});
|
||||
}
|
||||
.query(async ({ input }) => {
|
||||
return await findAllDeploymentsByServerId(input.serverId);
|
||||
}),
|
||||
allCentralized: protectedProcedure.query(async ({ ctx }) => {
|
||||
const orgId = ctx.session.activeOrganizationId;
|
||||
const accessedServices =
|
||||
ctx.user.role === "member"
|
||||
? (await findMemberById(ctx.user.id, orgId)).accessedServices
|
||||
: null;
|
||||
if (accessedServices !== null && accessedServices.length === 0) {
|
||||
return [];
|
||||
}
|
||||
return findAllDeploymentsCentralized(orgId, accessedServices);
|
||||
}),
|
||||
allCentralized: withPermission("deployment", "read").query(
|
||||
async ({ ctx }) => {
|
||||
const orgId = ctx.session.activeOrganizationId;
|
||||
const accessedServices =
|
||||
ctx.user.role !== "owner" && ctx.user.role !== "admin"
|
||||
? (await findMemberByUserId(ctx.user.id, orgId)).accessedServices
|
||||
: null;
|
||||
if (accessedServices !== null && accessedServices.length === 0) {
|
||||
return [];
|
||||
}
|
||||
return findAllDeploymentsCentralized(orgId, accessedServices);
|
||||
},
|
||||
),
|
||||
|
||||
queueList: protectedProcedure.query(async ({ ctx }) => {
|
||||
queueList: withPermission("deployment", "read").query(async ({ ctx }) => {
|
||||
const orgId = ctx.session.activeOrganizationId;
|
||||
let rows: QueueJobRow[];
|
||||
|
||||
@@ -135,7 +117,10 @@ export const deploymentRouter = createTRPCRouter({
|
||||
|
||||
allByType: protectedProcedure
|
||||
.input(apiFindAllByType)
|
||||
.query(async ({ input }) => {
|
||||
.query(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.id, {
|
||||
deployment: ["read"],
|
||||
});
|
||||
const deploymentsList = await db.query.deployments.findMany({
|
||||
where: eq(deployments[`${input.type}Id`], input.id),
|
||||
orderBy: desc(deployments.createdAt),
|
||||
@@ -151,8 +136,14 @@ export const deploymentRouter = createTRPCRouter({
|
||||
deploymentId: z.string().min(1),
|
||||
}),
|
||||
)
|
||||
.mutation(async ({ input }) => {
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const deployment = await findDeploymentById(input.deploymentId);
|
||||
const serviceId = deployment.applicationId || deployment.composeId;
|
||||
if (serviceId) {
|
||||
await checkServicePermissionAndAccess(ctx, serviceId, {
|
||||
deployment: ["cancel"],
|
||||
});
|
||||
}
|
||||
|
||||
if (!deployment.pid) {
|
||||
throw new TRPCError({
|
||||
@@ -169,6 +160,11 @@ export const deploymentRouter = createTRPCRouter({
|
||||
}
|
||||
|
||||
await updateDeploymentStatus(deployment.deploymentId, "error");
|
||||
await audit(ctx, {
|
||||
action: "cancel",
|
||||
resourceType: "deployment",
|
||||
resourceId: deployment.deploymentId,
|
||||
});
|
||||
}),
|
||||
|
||||
removeDeployment: protectedProcedure
|
||||
@@ -177,7 +173,20 @@ export const deploymentRouter = createTRPCRouter({
|
||||
deploymentId: z.string().min(1),
|
||||
}),
|
||||
)
|
||||
.mutation(async ({ input }) => {
|
||||
return await removeDeployment(input.deploymentId);
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const deployment = await findDeploymentById(input.deploymentId);
|
||||
const serviceId = deployment.applicationId || deployment.composeId;
|
||||
if (serviceId) {
|
||||
await checkServicePermissionAndAccess(ctx, serviceId, {
|
||||
deployment: ["cancel"],
|
||||
});
|
||||
}
|
||||
const result = await removeDeployment(input.deploymentId);
|
||||
await audit(ctx, {
|
||||
action: "delete",
|
||||
resourceType: "deployment",
|
||||
resourceId: deployment.deploymentId,
|
||||
});
|
||||
return result;
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -10,11 +10,8 @@ import {
|
||||
import { db } from "@dokploy/server/db";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { desc, eq } from "drizzle-orm";
|
||||
import {
|
||||
adminProcedure,
|
||||
createTRPCRouter,
|
||||
protectedProcedure,
|
||||
} from "@/server/api/trpc";
|
||||
import { createTRPCRouter, withPermission } from "@/server/api/trpc";
|
||||
import { audit } from "@/server/api/utils/audit";
|
||||
import {
|
||||
apiCreateDestination,
|
||||
apiFindOneDestination,
|
||||
@@ -24,14 +21,21 @@ import {
|
||||
} from "@/server/db/schema";
|
||||
|
||||
export const destinationRouter = createTRPCRouter({
|
||||
create: adminProcedure
|
||||
create: withPermission("destination", "create")
|
||||
.input(apiCreateDestination)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
return await createDestintation(
|
||||
const result = await createDestintation(
|
||||
input,
|
||||
ctx.session.activeOrganizationId,
|
||||
);
|
||||
await audit(ctx, {
|
||||
action: "create",
|
||||
resourceType: "destination",
|
||||
resourceId: result.destinationId,
|
||||
resourceName: input.name,
|
||||
});
|
||||
return result;
|
||||
} catch (error) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
@@ -40,7 +44,7 @@ export const destinationRouter = createTRPCRouter({
|
||||
});
|
||||
}
|
||||
}),
|
||||
testConnection: adminProcedure
|
||||
testConnection: withPermission("destination", "create")
|
||||
.input(apiCreateDestination)
|
||||
.mutation(async ({ input }) => {
|
||||
const { secretAccessKey, bucket, region, endpoint, accessKey, provider } =
|
||||
@@ -87,7 +91,7 @@ export const destinationRouter = createTRPCRouter({
|
||||
});
|
||||
}
|
||||
}),
|
||||
one: protectedProcedure
|
||||
one: withPermission("destination", "read")
|
||||
.input(apiFindOneDestination)
|
||||
.query(async ({ input, ctx }) => {
|
||||
const destination = await findDestinationById(input.destinationId);
|
||||
@@ -99,13 +103,13 @@ export const destinationRouter = createTRPCRouter({
|
||||
}
|
||||
return destination;
|
||||
}),
|
||||
all: protectedProcedure.query(async ({ ctx }) => {
|
||||
all: withPermission("destination", "read").query(async ({ ctx }) => {
|
||||
return await db.query.destinations.findMany({
|
||||
where: eq(destinations.organizationId, ctx.session.activeOrganizationId),
|
||||
orderBy: [desc(destinations.createdAt)],
|
||||
});
|
||||
}),
|
||||
remove: adminProcedure
|
||||
remove: withPermission("destination", "delete")
|
||||
.input(apiRemoveDestination)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
@@ -117,15 +121,22 @@ export const destinationRouter = createTRPCRouter({
|
||||
message: "You are not allowed to delete this destination",
|
||||
});
|
||||
}
|
||||
return await removeDestinationById(
|
||||
const result = await removeDestinationById(
|
||||
input.destinationId,
|
||||
ctx.session.activeOrganizationId,
|
||||
);
|
||||
await audit(ctx, {
|
||||
action: "delete",
|
||||
resourceType: "destination",
|
||||
resourceId: input.destinationId,
|
||||
resourceName: destination.name,
|
||||
});
|
||||
return result;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
}),
|
||||
update: adminProcedure
|
||||
update: withPermission("destination", "create")
|
||||
.input(apiUpdateDestination)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
@@ -136,10 +147,17 @@ export const destinationRouter = createTRPCRouter({
|
||||
message: "You are not allowed to update this destination",
|
||||
});
|
||||
}
|
||||
return await updateDestinationById(input.destinationId, {
|
||||
const result = await updateDestinationById(input.destinationId, {
|
||||
...input,
|
||||
organizationId: ctx.session.activeOrganizationId,
|
||||
});
|
||||
await audit(ctx, {
|
||||
action: "update",
|
||||
resourceType: "destination",
|
||||
resourceId: input.destinationId,
|
||||
resourceName: input.name,
|
||||
});
|
||||
return result;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
@@ -10,12 +10,13 @@ import {
|
||||
} from "@dokploy/server";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { z } from "zod";
|
||||
import { createTRPCRouter, protectedProcedure } from "../trpc";
|
||||
import { audit } from "@/server/api/utils/audit";
|
||||
import { createTRPCRouter, withPermission } from "../trpc";
|
||||
|
||||
export const containerIdRegex = /^[a-zA-Z0-9.\-_]+$/;
|
||||
|
||||
export const dockerRouter = createTRPCRouter({
|
||||
getContainers: protectedProcedure
|
||||
getContainers: withPermission("docker", "read")
|
||||
.input(
|
||||
z.object({
|
||||
serverId: z.string().optional(),
|
||||
@@ -31,7 +32,7 @@ export const dockerRouter = createTRPCRouter({
|
||||
return await getContainers(input.serverId);
|
||||
}),
|
||||
|
||||
restartContainer: protectedProcedure
|
||||
restartContainer: withPermission("docker", "read")
|
||||
.input(
|
||||
z.object({
|
||||
containerId: z
|
||||
@@ -40,11 +41,18 @@ export const dockerRouter = createTRPCRouter({
|
||||
.regex(containerIdRegex, "Invalid container id."),
|
||||
}),
|
||||
)
|
||||
.mutation(async ({ input }) => {
|
||||
return await containerRestart(input.containerId);
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const result = await containerRestart(input.containerId);
|
||||
await audit(ctx, {
|
||||
action: "start",
|
||||
resourceType: "docker",
|
||||
resourceId: input.containerId,
|
||||
resourceName: input.containerId,
|
||||
});
|
||||
return result;
|
||||
}),
|
||||
|
||||
getConfig: protectedProcedure
|
||||
getConfig: withPermission("docker", "read")
|
||||
.input(
|
||||
z.object({
|
||||
containerId: z
|
||||
@@ -64,7 +72,7 @@ export const dockerRouter = createTRPCRouter({
|
||||
return await getConfig(input.containerId, input.serverId);
|
||||
}),
|
||||
|
||||
getContainersByAppNameMatch: protectedProcedure
|
||||
getContainersByAppNameMatch: withPermission("service", "read")
|
||||
.input(
|
||||
z.object({
|
||||
appType: z.enum(["stack", "docker-compose"]).optional(),
|
||||
@@ -86,7 +94,7 @@ export const dockerRouter = createTRPCRouter({
|
||||
);
|
||||
}),
|
||||
|
||||
getContainersByAppLabel: protectedProcedure
|
||||
getContainersByAppLabel: withPermission("docker", "read")
|
||||
.input(
|
||||
z.object({
|
||||
appName: z.string().min(1).regex(containerIdRegex, "Invalid app name."),
|
||||
@@ -108,7 +116,7 @@ export const dockerRouter = createTRPCRouter({
|
||||
);
|
||||
}),
|
||||
|
||||
getStackContainersByAppName: protectedProcedure
|
||||
getStackContainersByAppName: withPermission("docker", "read")
|
||||
.input(
|
||||
z.object({
|
||||
appName: z.string().min(1).regex(containerIdRegex, "Invalid app name."),
|
||||
@@ -125,7 +133,7 @@ export const dockerRouter = createTRPCRouter({
|
||||
return await getStackContainersByAppName(input.appName, input.serverId);
|
||||
}),
|
||||
|
||||
getServiceContainersByAppName: protectedProcedure
|
||||
getServiceContainersByAppName: withPermission("docker", "read")
|
||||
.input(
|
||||
z.object({
|
||||
appName: z.string().min(1).regex(containerIdRegex, "Invalid app name."),
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import {
|
||||
createDomain,
|
||||
findApplicationById,
|
||||
findComposeById,
|
||||
findDomainById,
|
||||
findDomainsByApplicationId,
|
||||
findDomainsByComposeId,
|
||||
@@ -15,9 +14,15 @@ import {
|
||||
updateDomainById,
|
||||
validateDomain,
|
||||
} from "@dokploy/server";
|
||||
import { checkServicePermissionAndAccess } from "@dokploy/server/services/permission";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { z } from "zod";
|
||||
import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc";
|
||||
import {
|
||||
createTRPCRouter,
|
||||
protectedProcedure,
|
||||
withPermission,
|
||||
} from "@/server/api/trpc";
|
||||
import { audit } from "@/server/api/utils/audit";
|
||||
import {
|
||||
apiCreateDomain,
|
||||
apiFindCompose,
|
||||
@@ -32,29 +37,22 @@ export const domainRouter = createTRPCRouter({
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
if (input.domainType === "compose" && input.composeId) {
|
||||
const compose = await findComposeById(input.composeId);
|
||||
if (
|
||||
compose.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to access this compose",
|
||||
});
|
||||
}
|
||||
await checkServicePermissionAndAccess(ctx, input.composeId, {
|
||||
domain: ["create"],
|
||||
});
|
||||
} else if (input.domainType === "application" && input.applicationId) {
|
||||
const application = await findApplicationById(input.applicationId);
|
||||
if (
|
||||
application.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to access this application",
|
||||
});
|
||||
}
|
||||
await checkServicePermissionAndAccess(ctx, input.applicationId, {
|
||||
domain: ["create"],
|
||||
});
|
||||
}
|
||||
return await createDomain(input);
|
||||
const domain = await createDomain(input);
|
||||
await audit(ctx, {
|
||||
action: "create",
|
||||
resourceType: "domain",
|
||||
resourceId: domain.domainId,
|
||||
resourceName: domain.host,
|
||||
});
|
||||
return domain;
|
||||
} catch (error) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
@@ -69,34 +67,20 @@ export const domainRouter = createTRPCRouter({
|
||||
byApplicationId: protectedProcedure
|
||||
.input(apiFindOneApplication)
|
||||
.query(async ({ input, ctx }) => {
|
||||
const application = await findApplicationById(input.applicationId);
|
||||
if (
|
||||
application.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to access this application",
|
||||
});
|
||||
}
|
||||
await checkServicePermissionAndAccess(ctx, input.applicationId, {
|
||||
domain: ["read"],
|
||||
});
|
||||
return await findDomainsByApplicationId(input.applicationId);
|
||||
}),
|
||||
byComposeId: protectedProcedure
|
||||
.input(apiFindCompose)
|
||||
.query(async ({ input, ctx }) => {
|
||||
const compose = await findComposeById(input.composeId);
|
||||
if (
|
||||
compose.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to access this compose",
|
||||
});
|
||||
}
|
||||
await checkServicePermissionAndAccess(ctx, input.composeId, {
|
||||
domain: ["read"],
|
||||
});
|
||||
return await findDomainsByComposeId(input.composeId);
|
||||
}),
|
||||
generateDomain: protectedProcedure
|
||||
generateDomain: withPermission("domain", "create")
|
||||
.input(z.object({ appName: z.string(), serverId: z.string().optional() }))
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
return generateTraefikMeDomain(
|
||||
@@ -105,7 +89,7 @@ export const domainRouter = createTRPCRouter({
|
||||
input.serverId,
|
||||
);
|
||||
}),
|
||||
canGenerateTraefikMeDomains: protectedProcedure
|
||||
canGenerateTraefikMeDomains: withPermission("domain", "read")
|
||||
.input(z.object({ serverId: z.string() }))
|
||||
.query(async ({ input }) => {
|
||||
if (input.serverId) {
|
||||
@@ -120,45 +104,28 @@ export const domainRouter = createTRPCRouter({
|
||||
.input(apiUpdateDomain)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const currentDomain = await findDomainById(input.domainId);
|
||||
|
||||
if (currentDomain.applicationId) {
|
||||
const newApp = await findApplicationById(currentDomain.applicationId);
|
||||
if (
|
||||
newApp.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to access this application",
|
||||
});
|
||||
}
|
||||
} else if (currentDomain.composeId) {
|
||||
const newCompose = await findComposeById(currentDomain.composeId);
|
||||
if (
|
||||
newCompose.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to access this compose",
|
||||
});
|
||||
}
|
||||
const serviceId = currentDomain.applicationId || currentDomain.composeId;
|
||||
if (serviceId) {
|
||||
await checkServicePermissionAndAccess(ctx, serviceId, {
|
||||
domain: ["create"],
|
||||
});
|
||||
} else if (currentDomain.previewDeploymentId) {
|
||||
const newPreviewDeployment = await findPreviewDeploymentById(
|
||||
const preview = await findPreviewDeploymentById(
|
||||
currentDomain.previewDeploymentId,
|
||||
);
|
||||
if (
|
||||
newPreviewDeployment.application.environment.project
|
||||
.organizationId !== ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to access this preview deployment",
|
||||
});
|
||||
}
|
||||
await checkServicePermissionAndAccess(ctx, preview.applicationId, {
|
||||
domain: ["create"],
|
||||
});
|
||||
}
|
||||
|
||||
const result = await updateDomainById(input.domainId, input);
|
||||
const domain = await findDomainById(input.domainId);
|
||||
await audit(ctx, {
|
||||
action: "update",
|
||||
resourceType: "domain",
|
||||
resourceId: domain.domainId,
|
||||
resourceName: domain.host,
|
||||
});
|
||||
if (domain.applicationId) {
|
||||
const application = await findApplicationById(domain.applicationId);
|
||||
await manageDomain(application, domain);
|
||||
@@ -176,59 +143,46 @@ export const domainRouter = createTRPCRouter({
|
||||
}),
|
||||
one: protectedProcedure.input(apiFindDomain).query(async ({ input, ctx }) => {
|
||||
const domain = await findDomainById(input.domainId);
|
||||
if (domain.applicationId) {
|
||||
const application = await findApplicationById(domain.applicationId);
|
||||
if (
|
||||
application.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to access this application",
|
||||
});
|
||||
}
|
||||
} else if (domain.composeId) {
|
||||
const compose = await findComposeById(domain.composeId);
|
||||
if (
|
||||
compose.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to access this compose",
|
||||
});
|
||||
}
|
||||
const serviceId = domain.applicationId || domain.composeId;
|
||||
if (serviceId) {
|
||||
await checkServicePermissionAndAccess(ctx, serviceId, {
|
||||
domain: ["read"],
|
||||
});
|
||||
} else if (domain.previewDeploymentId) {
|
||||
const preview = await findPreviewDeploymentById(
|
||||
domain.previewDeploymentId,
|
||||
);
|
||||
await checkServicePermissionAndAccess(ctx, preview.applicationId, {
|
||||
domain: ["read"],
|
||||
});
|
||||
}
|
||||
return await findDomainById(input.domainId);
|
||||
return domain;
|
||||
}),
|
||||
delete: protectedProcedure
|
||||
.input(apiFindDomain)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const domain = await findDomainById(input.domainId);
|
||||
if (domain.applicationId) {
|
||||
const application = await findApplicationById(domain.applicationId);
|
||||
if (
|
||||
application.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to access this application",
|
||||
});
|
||||
}
|
||||
} else if (domain.composeId) {
|
||||
const compose = await findComposeById(domain.composeId);
|
||||
if (
|
||||
compose.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to access this compose",
|
||||
});
|
||||
}
|
||||
const serviceId = domain.applicationId || domain.composeId;
|
||||
if (serviceId) {
|
||||
await checkServicePermissionAndAccess(ctx, serviceId, {
|
||||
domain: ["delete"],
|
||||
});
|
||||
} else if (domain.previewDeploymentId) {
|
||||
const preview = await findPreviewDeploymentById(
|
||||
domain.previewDeploymentId,
|
||||
);
|
||||
await checkServicePermissionAndAccess(ctx, preview.applicationId, {
|
||||
domain: ["delete"],
|
||||
});
|
||||
}
|
||||
|
||||
const result = await removeDomainById(input.domainId);
|
||||
await audit(ctx, {
|
||||
action: "delete",
|
||||
resourceType: "domain",
|
||||
resourceId: domain.domainId,
|
||||
resourceName: domain.host,
|
||||
});
|
||||
|
||||
if (domain.applicationId) {
|
||||
const application = await findApplicationById(domain.applicationId);
|
||||
@@ -238,7 +192,7 @@ export const domainRouter = createTRPCRouter({
|
||||
return result;
|
||||
}),
|
||||
|
||||
validateDomain: protectedProcedure
|
||||
validateDomain: withPermission("domain", "read")
|
||||
.input(
|
||||
z.object({
|
||||
domain: z.string(),
|
||||
|
||||
@@ -1,31 +1,35 @@
|
||||
import {
|
||||
addNewEnvironment,
|
||||
checkEnvironmentAccess,
|
||||
checkEnvironmentCreationPermission,
|
||||
checkEnvironmentDeletionPermission,
|
||||
createEnvironment,
|
||||
deleteEnvironment,
|
||||
duplicateEnvironment,
|
||||
findEnvironmentById,
|
||||
findEnvironmentsByProjectId,
|
||||
findMemberById,
|
||||
updateEnvironmentById,
|
||||
} from "@dokploy/server";
|
||||
import { db } from "@dokploy/server/db";
|
||||
import {
|
||||
addNewEnvironment,
|
||||
checkEnvironmentAccess,
|
||||
checkEnvironmentCreationPermission,
|
||||
checkEnvironmentDeletionPermission,
|
||||
checkPermission,
|
||||
findMemberByUserId,
|
||||
} from "@dokploy/server/services/permission";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { and, desc, eq, ilike, or, sql } from "drizzle-orm";
|
||||
import { z } from "zod";
|
||||
import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc";
|
||||
import { audit } from "@/server/api/utils/audit";
|
||||
import {
|
||||
apiCreateEnvironment,
|
||||
apiDuplicateEnvironment,
|
||||
apiFindOneEnvironment,
|
||||
apiRemoveEnvironment,
|
||||
apiUpdateEnvironment,
|
||||
environments,
|
||||
projects,
|
||||
} from "@/server/db/schema";
|
||||
import { environments, projects } from "@/server/db/schema";
|
||||
|
||||
// Helper function to filter services within an environment based on user permissions
|
||||
const filterEnvironmentServices = (
|
||||
environment: any,
|
||||
accessedServices: string[],
|
||||
@@ -59,12 +63,7 @@ export const environmentRouter = createTRPCRouter({
|
||||
.input(apiCreateEnvironment)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
// Check if user has permission to create environments
|
||||
await checkEnvironmentCreationPermission(
|
||||
ctx.user.id,
|
||||
input.projectId,
|
||||
ctx.session.activeOrganizationId,
|
||||
);
|
||||
await checkEnvironmentCreationPermission(ctx, input.projectId);
|
||||
|
||||
if (input.name === "production") {
|
||||
throw new TRPCError({
|
||||
@@ -74,16 +73,15 @@ export const environmentRouter = createTRPCRouter({
|
||||
});
|
||||
}
|
||||
|
||||
// Allow users to create environments with any name, including "production"
|
||||
const environment = await createEnvironment(input);
|
||||
|
||||
if (ctx.user.role === "member") {
|
||||
await addNewEnvironment(
|
||||
ctx.user.id,
|
||||
environment.environmentId,
|
||||
ctx.session.activeOrganizationId,
|
||||
);
|
||||
}
|
||||
await addNewEnvironment(ctx, environment.environmentId);
|
||||
await audit(ctx, {
|
||||
action: "create",
|
||||
resourceType: "environment",
|
||||
resourceId: environment.environmentId,
|
||||
resourceName: environment.name,
|
||||
});
|
||||
return environment;
|
||||
} catch (error) {
|
||||
if (error instanceof TRPCError) {
|
||||
@@ -100,54 +98,39 @@ export const environmentRouter = createTRPCRouter({
|
||||
one: protectedProcedure
|
||||
.input(apiFindOneEnvironment)
|
||||
.query(async ({ input, ctx }) => {
|
||||
try {
|
||||
if (ctx.user.role === "member") {
|
||||
await checkEnvironmentAccess(
|
||||
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",
|
||||
});
|
||||
}
|
||||
|
||||
if (ctx.user.role !== "owner" && ctx.user.role !== "admin") {
|
||||
const { accessedEnvironments, accessedServices } =
|
||||
await findMemberByUserId(
|
||||
ctx.user.id,
|
||||
input.environmentId,
|
||||
ctx.session.activeOrganizationId,
|
||||
"access",
|
||||
);
|
||||
}
|
||||
const environment = await findEnvironmentById(input.environmentId);
|
||||
if (
|
||||
environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
|
||||
if (!accessedEnvironments.includes(environment.environmentId)) {
|
||||
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);
|
||||
const filteredEnvironment = filterEnvironmentServices(
|
||||
environment,
|
||||
accessedServices,
|
||||
);
|
||||
|
||||
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",
|
||||
});
|
||||
return filteredEnvironment;
|
||||
}
|
||||
|
||||
return environment;
|
||||
}),
|
||||
|
||||
byProjectId: protectedProcedure
|
||||
@@ -156,7 +139,6 @@ export const environmentRouter = createTRPCRouter({
|
||||
try {
|
||||
const environments = await findEnvironmentsByProjectId(input.projectId);
|
||||
|
||||
// Check organization access
|
||||
if (
|
||||
environments.some(
|
||||
(environment) =>
|
||||
@@ -170,12 +152,13 @@ export const environmentRouter = createTRPCRouter({
|
||||
});
|
||||
}
|
||||
|
||||
// Filter environments for members based on their permissions
|
||||
if (ctx.user.role === "member") {
|
||||
if (ctx.user.role !== "owner" && ctx.user.role !== "admin") {
|
||||
const { accessedEnvironments, accessedServices } =
|
||||
await findMemberById(ctx.user.id, ctx.session.activeOrganizationId);
|
||||
await findMemberByUserId(
|
||||
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),
|
||||
@@ -211,7 +194,6 @@ export const environmentRouter = createTRPCRouter({
|
||||
});
|
||||
}
|
||||
|
||||
// Prevent deletion of the default environment
|
||||
if (environment.isDefault) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
@@ -219,24 +201,17 @@ export const environmentRouter = createTRPCRouter({
|
||||
});
|
||||
}
|
||||
|
||||
// Check environment deletion permission
|
||||
await checkEnvironmentDeletionPermission(
|
||||
ctx.user.id,
|
||||
environment.projectId,
|
||||
ctx.session.activeOrganizationId,
|
||||
);
|
||||
await checkEnvironmentDeletionPermission(ctx, environment.projectId);
|
||||
|
||||
// Additional check for environment access for members
|
||||
if (ctx.user.role === "member") {
|
||||
await checkEnvironmentAccess(
|
||||
ctx.user.id,
|
||||
input.environmentId,
|
||||
ctx.session.activeOrganizationId,
|
||||
"access",
|
||||
);
|
||||
}
|
||||
await checkEnvironmentAccess(ctx, input.environmentId, "read");
|
||||
|
||||
const deletedEnvironment = await deleteEnvironment(input.environmentId);
|
||||
await audit(ctx, {
|
||||
action: "delete",
|
||||
resourceType: "environment",
|
||||
resourceId: deletedEnvironment?.environmentId,
|
||||
resourceName: deletedEnvironment?.name,
|
||||
});
|
||||
return deletedEnvironment;
|
||||
} catch (error) {
|
||||
if (error instanceof TRPCError) {
|
||||
@@ -256,18 +231,14 @@ export const environmentRouter = createTRPCRouter({
|
||||
try {
|
||||
const { environmentId, ...updateData } = input;
|
||||
|
||||
// Allow users to rename environments to any name, including "production"
|
||||
if (ctx.user.role === "member") {
|
||||
await checkEnvironmentAccess(
|
||||
ctx.user.id,
|
||||
environmentId,
|
||||
ctx.session.activeOrganizationId,
|
||||
"access",
|
||||
);
|
||||
await checkEnvironmentAccess(ctx, environmentId, "read");
|
||||
|
||||
if (updateData.env !== undefined) {
|
||||
await checkPermission(ctx, { environmentEnvVars: ["write"] });
|
||||
}
|
||||
|
||||
const currentEnvironment = await findEnvironmentById(environmentId);
|
||||
|
||||
// Prevent renaming the default environment, but allow updating env and description
|
||||
if (currentEnvironment.isDefault && updateData.name !== undefined) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
@@ -284,9 +255,8 @@ export const environmentRouter = createTRPCRouter({
|
||||
});
|
||||
}
|
||||
|
||||
// Check environment access for members
|
||||
if (ctx.user.role === "member") {
|
||||
const { accessedEnvironments } = await findMemberById(
|
||||
if (ctx.user.role !== "owner" && ctx.user.role !== "admin") {
|
||||
const { accessedEnvironments } = await findMemberByUserId(
|
||||
ctx.user.id,
|
||||
ctx.session.activeOrganizationId,
|
||||
);
|
||||
@@ -305,6 +275,14 @@ export const environmentRouter = createTRPCRouter({
|
||||
environmentId,
|
||||
updateData,
|
||||
);
|
||||
if (environment) {
|
||||
await audit(ctx, {
|
||||
action: "update",
|
||||
resourceType: "environment",
|
||||
resourceId: environment.environmentId,
|
||||
resourceName: environment.name,
|
||||
});
|
||||
}
|
||||
return environment;
|
||||
} catch (error) {
|
||||
throw new TRPCError({
|
||||
@@ -318,14 +296,7 @@ export const environmentRouter = createTRPCRouter({
|
||||
.input(apiDuplicateEnvironment)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
if (ctx.user.role === "member") {
|
||||
await checkEnvironmentAccess(
|
||||
ctx.user.id,
|
||||
input.environmentId,
|
||||
ctx.session.activeOrganizationId,
|
||||
"access",
|
||||
);
|
||||
}
|
||||
await checkEnvironmentAccess(ctx, input.environmentId, "read");
|
||||
const environment = await findEnvironmentById(input.environmentId);
|
||||
if (
|
||||
environment.project.organizationId !==
|
||||
@@ -337,9 +308,8 @@ export const environmentRouter = createTRPCRouter({
|
||||
});
|
||||
}
|
||||
|
||||
// Check environment access for members
|
||||
if (ctx.user.role === "member") {
|
||||
const { accessedEnvironments } = await findMemberById(
|
||||
if (ctx.user.role !== "owner" && ctx.user.role !== "admin") {
|
||||
const { accessedEnvironments } = await findMemberByUserId(
|
||||
ctx.user.id,
|
||||
ctx.session.activeOrganizationId,
|
||||
);
|
||||
@@ -353,6 +323,13 @@ export const environmentRouter = createTRPCRouter({
|
||||
}
|
||||
|
||||
const duplicatedEnvironment = await duplicateEnvironment(input);
|
||||
await audit(ctx, {
|
||||
action: "create",
|
||||
resourceType: "environment",
|
||||
resourceId: duplicatedEnvironment.environmentId,
|
||||
resourceName: duplicatedEnvironment.name,
|
||||
metadata: { duplicatedFrom: input.environmentId },
|
||||
});
|
||||
return duplicatedEnvironment;
|
||||
} catch (error) {
|
||||
throw new TRPCError({
|
||||
@@ -404,8 +381,8 @@ export const environmentRouter = createTRPCRouter({
|
||||
);
|
||||
}
|
||||
|
||||
if (ctx.user.role === "member") {
|
||||
const { accessedEnvironments } = await findMemberById(
|
||||
if (ctx.user.role !== "owner" && ctx.user.role !== "admin") {
|
||||
const { accessedEnvironments } = await findMemberByUserId(
|
||||
ctx.user.id,
|
||||
ctx.session.activeOrganizationId,
|
||||
);
|
||||
|
||||
@@ -2,7 +2,12 @@ import { findGitProviderById, removeGitProvider } from "@dokploy/server";
|
||||
import { db } from "@dokploy/server/db";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { and, desc, eq } from "drizzle-orm";
|
||||
import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc";
|
||||
import { audit } from "@/server/api/utils/audit";
|
||||
import {
|
||||
createTRPCRouter,
|
||||
protectedProcedure,
|
||||
withPermission,
|
||||
} from "@/server/api/trpc";
|
||||
import { apiRemoveGitProvider, gitProvider } from "@/server/db/schema";
|
||||
|
||||
export const gitProviderRouter = createTRPCRouter({
|
||||
@@ -21,7 +26,7 @@ export const gitProviderRouter = createTRPCRouter({
|
||||
),
|
||||
});
|
||||
}),
|
||||
remove: protectedProcedure
|
||||
remove: withPermission("gitProviders", "delete")
|
||||
.input(apiRemoveGitProvider)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
@@ -33,6 +38,12 @@ export const gitProviderRouter = createTRPCRouter({
|
||||
message: "You are not allowed to delete this Git provider",
|
||||
});
|
||||
}
|
||||
await audit(ctx, {
|
||||
action: "delete",
|
||||
resourceType: "gitProvider",
|
||||
resourceId: gitProvider.gitProviderId,
|
||||
resourceName: gitProvider.name ?? gitProvider.gitProviderId,
|
||||
});
|
||||
return await removeGitProvider(input.gitProviderId);
|
||||
} catch (error) {
|
||||
const message =
|
||||
|
||||
@@ -10,7 +10,12 @@ import {
|
||||
} from "@dokploy/server";
|
||||
import { db } from "@dokploy/server/db";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc";
|
||||
import {
|
||||
createTRPCRouter,
|
||||
protectedProcedure,
|
||||
withPermission,
|
||||
} from "@/server/api/trpc";
|
||||
import { audit } from "@/server/api/utils/audit";
|
||||
import {
|
||||
apiCreateGitea,
|
||||
apiFindGiteaBranches,
|
||||
@@ -20,15 +25,24 @@ import {
|
||||
} from "@/server/db/schema";
|
||||
|
||||
export const giteaRouter = createTRPCRouter({
|
||||
create: protectedProcedure
|
||||
create: withPermission("gitProviders", "create")
|
||||
.input(apiCreateGitea)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
return await createGitea(
|
||||
const result = await createGitea(
|
||||
input,
|
||||
ctx.session.activeOrganizationId,
|
||||
ctx.session.userId,
|
||||
);
|
||||
|
||||
await audit(ctx, {
|
||||
action: "create",
|
||||
resourceType: "gitProvider",
|
||||
resourceId: result.giteaId,
|
||||
resourceName: input.name,
|
||||
});
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
@@ -38,24 +52,11 @@ export const giteaRouter = createTRPCRouter({
|
||||
}
|
||||
}),
|
||||
|
||||
one: protectedProcedure
|
||||
.input(apiFindOneGitea)
|
||||
.query(async ({ input, ctx }) => {
|
||||
const giteaProvider = await findGiteaById(input.giteaId);
|
||||
if (
|
||||
giteaProvider.gitProvider.organizationId !==
|
||||
ctx.session.activeOrganizationId &&
|
||||
giteaProvider.gitProvider.userId !== ctx.session.userId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not allowed to access this Gitea provider",
|
||||
});
|
||||
}
|
||||
return giteaProvider;
|
||||
}),
|
||||
one: protectedProcedure.input(apiFindOneGitea).query(async ({ input }) => {
|
||||
return await findGiteaById(input.giteaId);
|
||||
}),
|
||||
|
||||
giteaProviders: protectedProcedure.query(async ({ ctx }: { ctx: any }) => {
|
||||
giteaProviders: protectedProcedure.query(async ({ ctx }) => {
|
||||
let result = await db.query.gitea.findMany({
|
||||
with: {
|
||||
gitProvider: true,
|
||||
@@ -85,7 +86,7 @@ export const giteaRouter = createTRPCRouter({
|
||||
|
||||
getGiteaRepositories: protectedProcedure
|
||||
.input(apiFindOneGitea)
|
||||
.query(async ({ input, ctx }) => {
|
||||
.query(async ({ input }) => {
|
||||
const { giteaId } = input;
|
||||
|
||||
if (!giteaId) {
|
||||
@@ -95,18 +96,6 @@ export const giteaRouter = createTRPCRouter({
|
||||
});
|
||||
}
|
||||
|
||||
const giteaProvider = await findGiteaById(giteaId);
|
||||
if (
|
||||
giteaProvider.gitProvider.organizationId !==
|
||||
ctx.session.activeOrganizationId &&
|
||||
giteaProvider.gitProvider.userId !== ctx.session.userId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not allowed to access this Gitea provider",
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
const repositories = await getGiteaRepositories(giteaId);
|
||||
return repositories;
|
||||
@@ -121,7 +110,7 @@ export const giteaRouter = createTRPCRouter({
|
||||
|
||||
getGiteaBranches: protectedProcedure
|
||||
.input(apiFindGiteaBranches)
|
||||
.query(async ({ input, ctx }) => {
|
||||
.query(async ({ input }) => {
|
||||
const { giteaId, owner, repositoryName } = input;
|
||||
|
||||
if (!giteaId || !owner || !repositoryName) {
|
||||
@@ -132,18 +121,6 @@ export const giteaRouter = createTRPCRouter({
|
||||
});
|
||||
}
|
||||
|
||||
const giteaProvider = await findGiteaById(giteaId);
|
||||
if (
|
||||
giteaProvider.gitProvider.organizationId !==
|
||||
ctx.session.activeOrganizationId &&
|
||||
giteaProvider.gitProvider.userId !== ctx.session.userId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not allowed to access this Gitea provider",
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
return await getGiteaBranches({
|
||||
giteaId,
|
||||
@@ -161,22 +138,10 @@ export const giteaRouter = createTRPCRouter({
|
||||
|
||||
testConnection: protectedProcedure
|
||||
.input(apiGiteaTestConnection)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
.mutation(async ({ input }) => {
|
||||
const giteaId = input.giteaId ?? "";
|
||||
|
||||
try {
|
||||
const giteaProvider = await findGiteaById(giteaId);
|
||||
if (
|
||||
giteaProvider.gitProvider.organizationId !==
|
||||
ctx.session.activeOrganizationId &&
|
||||
giteaProvider.gitProvider.userId !== ctx.session.userId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not allowed to access this Gitea provider",
|
||||
});
|
||||
}
|
||||
|
||||
const result = await testGiteaConnection({
|
||||
giteaId,
|
||||
});
|
||||
@@ -191,21 +156,9 @@ export const giteaRouter = createTRPCRouter({
|
||||
}
|
||||
}),
|
||||
|
||||
update: protectedProcedure
|
||||
update: withPermission("gitProviders", "create")
|
||||
.input(apiUpdateGitea)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const giteaProvider = await findGiteaById(input.giteaId);
|
||||
if (
|
||||
giteaProvider.gitProvider.organizationId !==
|
||||
ctx.session.activeOrganizationId &&
|
||||
giteaProvider.gitProvider.userId !== ctx.session.userId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not allowed to access this Gitea provider",
|
||||
});
|
||||
}
|
||||
|
||||
if (input.name) {
|
||||
await updateGitProvider(input.gitProviderId, {
|
||||
name: input.name,
|
||||
@@ -221,12 +174,19 @@ export const giteaRouter = createTRPCRouter({
|
||||
});
|
||||
}
|
||||
|
||||
await audit(ctx, {
|
||||
action: "update",
|
||||
resourceType: "gitProvider",
|
||||
resourceId: input.giteaId,
|
||||
resourceName: input.name,
|
||||
});
|
||||
|
||||
return { success: true };
|
||||
}),
|
||||
|
||||
getGiteaUrl: protectedProcedure
|
||||
.input(apiFindOneGitea)
|
||||
.query(async ({ input, ctx }) => {
|
||||
.query(async ({ input }) => {
|
||||
const { giteaId } = input;
|
||||
|
||||
if (!giteaId) {
|
||||
@@ -237,16 +197,6 @@ export const giteaRouter = createTRPCRouter({
|
||||
}
|
||||
|
||||
const giteaProvider = await findGiteaById(giteaId);
|
||||
if (
|
||||
giteaProvider.gitProvider.organizationId !==
|
||||
ctx.session.activeOrganizationId &&
|
||||
giteaProvider.gitProvider.userId !== ctx.session.userId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not allowed to access this Gitea provider",
|
||||
});
|
||||
}
|
||||
|
||||
// Return the base URL of the Gitea instance
|
||||
return giteaProvider.giteaUrl;
|
||||
|
||||
@@ -8,7 +8,12 @@ import {
|
||||
} from "@dokploy/server";
|
||||
import { db } from "@dokploy/server/db";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc";
|
||||
import {
|
||||
createTRPCRouter,
|
||||
protectedProcedure,
|
||||
withPermission,
|
||||
} from "@/server/api/trpc";
|
||||
import { audit } from "@/server/api/utils/audit";
|
||||
import {
|
||||
apiFindGithubBranches,
|
||||
apiFindOneGithub,
|
||||
@@ -16,53 +21,17 @@ import {
|
||||
} from "@/server/db/schema";
|
||||
|
||||
export const githubRouter = createTRPCRouter({
|
||||
one: protectedProcedure
|
||||
.input(apiFindOneGithub)
|
||||
.query(async ({ input, ctx }) => {
|
||||
const githubProvider = await findGithubById(input.githubId);
|
||||
if (
|
||||
githubProvider.gitProvider.organizationId !==
|
||||
ctx.session.activeOrganizationId &&
|
||||
githubProvider.gitProvider.userId === ctx.session.userId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not allowed to access this github provider",
|
||||
});
|
||||
}
|
||||
return githubProvider;
|
||||
}),
|
||||
one: protectedProcedure.input(apiFindOneGithub).query(async ({ input }) => {
|
||||
return await findGithubById(input.githubId);
|
||||
}),
|
||||
getGithubRepositories: protectedProcedure
|
||||
.input(apiFindOneGithub)
|
||||
.query(async ({ input, ctx }) => {
|
||||
const githubProvider = await findGithubById(input.githubId);
|
||||
if (
|
||||
githubProvider.gitProvider.organizationId !==
|
||||
ctx.session.activeOrganizationId &&
|
||||
githubProvider.gitProvider.userId === ctx.session.userId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not allowed to access this github provider",
|
||||
});
|
||||
}
|
||||
.query(async ({ input }) => {
|
||||
return await getGithubRepositories(input.githubId);
|
||||
}),
|
||||
getGithubBranches: protectedProcedure
|
||||
.input(apiFindGithubBranches)
|
||||
.query(async ({ input, ctx }) => {
|
||||
const githubProvider = await findGithubById(input.githubId || "");
|
||||
if (
|
||||
githubProvider.gitProvider.organizationId !==
|
||||
ctx.session.activeOrganizationId &&
|
||||
githubProvider.gitProvider.userId === ctx.session.userId
|
||||
) {
|
||||
//TODO: Remove this line when the cloud version is ready
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not allowed to access this github provider",
|
||||
});
|
||||
}
|
||||
.query(async ({ input }) => {
|
||||
return await getGithubBranches(input);
|
||||
}),
|
||||
githubProviders: protectedProcedure.query(async ({ ctx }) => {
|
||||
@@ -95,19 +64,8 @@ export const githubRouter = createTRPCRouter({
|
||||
|
||||
testConnection: protectedProcedure
|
||||
.input(apiFindOneGithub)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
.mutation(async ({ input }) => {
|
||||
try {
|
||||
const githubProvider = await findGithubById(input.githubId);
|
||||
if (
|
||||
githubProvider.gitProvider.organizationId !==
|
||||
ctx.session.activeOrganizationId &&
|
||||
githubProvider.gitProvider.userId === ctx.session.userId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not allowed to access this github provider",
|
||||
});
|
||||
}
|
||||
const result = await getGithubRepositories(input.githubId);
|
||||
return `Found ${result.length} repositories`;
|
||||
} catch (err) {
|
||||
@@ -117,20 +75,9 @@ export const githubRouter = createTRPCRouter({
|
||||
});
|
||||
}
|
||||
}),
|
||||
update: protectedProcedure
|
||||
update: withPermission("gitProviders", "create")
|
||||
.input(apiUpdateGithub)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const githubProvider = await findGithubById(input.githubId);
|
||||
if (
|
||||
githubProvider.gitProvider.organizationId !==
|
||||
ctx.session.activeOrganizationId &&
|
||||
githubProvider.gitProvider.userId === ctx.session.userId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not allowed to access this github provider",
|
||||
});
|
||||
}
|
||||
await updateGitProvider(input.gitProviderId, {
|
||||
name: input.name,
|
||||
organizationId: ctx.session.activeOrganizationId,
|
||||
@@ -139,5 +86,12 @@ export const githubRouter = createTRPCRouter({
|
||||
await updateGithub(input.githubId, {
|
||||
...input,
|
||||
});
|
||||
|
||||
await audit(ctx, {
|
||||
action: "update",
|
||||
resourceType: "gitProvider",
|
||||
resourceId: input.gitProviderId,
|
||||
resourceName: input.name,
|
||||
});
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -10,7 +10,12 @@ import {
|
||||
} from "@dokploy/server";
|
||||
import { db } from "@dokploy/server/db";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc";
|
||||
import {
|
||||
createTRPCRouter,
|
||||
protectedProcedure,
|
||||
withPermission,
|
||||
} from "@/server/api/trpc";
|
||||
import { audit } from "@/server/api/utils/audit";
|
||||
import {
|
||||
apiCreateGitlab,
|
||||
apiFindGitlabBranches,
|
||||
@@ -20,15 +25,23 @@ import {
|
||||
} from "@/server/db/schema";
|
||||
|
||||
export const gitlabRouter = createTRPCRouter({
|
||||
create: protectedProcedure
|
||||
create: withPermission("gitProviders", "create")
|
||||
.input(apiCreateGitlab)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
return await createGitlab(
|
||||
const result = await createGitlab(
|
||||
input,
|
||||
ctx.session.activeOrganizationId,
|
||||
ctx.session.userId,
|
||||
);
|
||||
|
||||
await audit(ctx, {
|
||||
action: "create",
|
||||
resourceType: "gitProvider",
|
||||
resourceName: input.name,
|
||||
});
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
@@ -37,22 +50,9 @@ export const gitlabRouter = createTRPCRouter({
|
||||
});
|
||||
}
|
||||
}),
|
||||
one: protectedProcedure
|
||||
.input(apiFindOneGitlab)
|
||||
.query(async ({ input, ctx }) => {
|
||||
const gitlabProvider = await findGitlabById(input.gitlabId);
|
||||
if (
|
||||
gitlabProvider.gitProvider.organizationId !==
|
||||
ctx.session.activeOrganizationId &&
|
||||
gitlabProvider.gitProvider.userId !== ctx.session.userId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not allowed to access this Gitlab provider",
|
||||
});
|
||||
}
|
||||
return gitlabProvider;
|
||||
}),
|
||||
one: protectedProcedure.input(apiFindOneGitlab).query(async ({ input }) => {
|
||||
return await findGitlabById(input.gitlabId);
|
||||
}),
|
||||
gitlabProviders: protectedProcedure.query(async ({ ctx }) => {
|
||||
let result = await db.query.gitlab.findMany({
|
||||
with: {
|
||||
@@ -83,52 +83,19 @@ export const gitlabRouter = createTRPCRouter({
|
||||
}),
|
||||
getGitlabRepositories: protectedProcedure
|
||||
.input(apiFindOneGitlab)
|
||||
.query(async ({ input, ctx }) => {
|
||||
const gitlabProvider = await findGitlabById(input.gitlabId);
|
||||
if (
|
||||
gitlabProvider.gitProvider.organizationId !==
|
||||
ctx.session.activeOrganizationId &&
|
||||
gitlabProvider.gitProvider.userId !== ctx.session.userId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not allowed to access this Gitlab provider",
|
||||
});
|
||||
}
|
||||
.query(async ({ input }) => {
|
||||
return await getGitlabRepositories(input.gitlabId);
|
||||
}),
|
||||
|
||||
getGitlabBranches: protectedProcedure
|
||||
.input(apiFindGitlabBranches)
|
||||
.query(async ({ input, ctx }) => {
|
||||
const gitlabProvider = await findGitlabById(input.gitlabId || "");
|
||||
if (
|
||||
gitlabProvider.gitProvider.organizationId !==
|
||||
ctx.session.activeOrganizationId &&
|
||||
gitlabProvider.gitProvider.userId !== ctx.session.userId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not allowed to access this Gitlab provider",
|
||||
});
|
||||
}
|
||||
.query(async ({ input }) => {
|
||||
return await getGitlabBranches(input);
|
||||
}),
|
||||
testConnection: protectedProcedure
|
||||
.input(apiGitlabTestConnection)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
.mutation(async ({ input }) => {
|
||||
try {
|
||||
const gitlabProvider = await findGitlabById(input.gitlabId || "");
|
||||
if (
|
||||
gitlabProvider.gitProvider.organizationId !==
|
||||
ctx.session.activeOrganizationId &&
|
||||
gitlabProvider.gitProvider.userId !== ctx.session.userId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not allowed to access this Gitlab provider",
|
||||
});
|
||||
}
|
||||
const result = await testGitlabConnection(input);
|
||||
|
||||
return `Found ${result} repositories`;
|
||||
@@ -139,20 +106,9 @@ export const gitlabRouter = createTRPCRouter({
|
||||
});
|
||||
}
|
||||
}),
|
||||
update: protectedProcedure
|
||||
update: withPermission("gitProviders", "create")
|
||||
.input(apiUpdateGitlab)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const gitlabProvider = await findGitlabById(input.gitlabId);
|
||||
if (
|
||||
gitlabProvider.gitProvider.organizationId !==
|
||||
ctx.session.activeOrganizationId &&
|
||||
gitlabProvider.gitProvider.userId !== ctx.session.userId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not allowed to access this Gitlab provider",
|
||||
});
|
||||
}
|
||||
if (input.name) {
|
||||
await updateGitProvider(input.gitProviderId, {
|
||||
name: input.name,
|
||||
@@ -167,5 +123,12 @@ export const gitlabRouter = createTRPCRouter({
|
||||
...input,
|
||||
});
|
||||
}
|
||||
|
||||
await audit(ctx, {
|
||||
action: "update",
|
||||
resourceType: "gitProvider",
|
||||
resourceId: input.gitProviderId,
|
||||
resourceName: input.name,
|
||||
});
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -1,14 +1,11 @@
|
||||
import {
|
||||
addNewService,
|
||||
checkPortInUse,
|
||||
checkServiceAccess,
|
||||
createMariadb,
|
||||
createMount,
|
||||
deployMariadb,
|
||||
findBackupsByDbId,
|
||||
findEnvironmentById,
|
||||
findMariadbById,
|
||||
findMemberById,
|
||||
findProjectById,
|
||||
IS_CLOUD,
|
||||
rebuildDatabase,
|
||||
@@ -21,11 +18,18 @@ import {
|
||||
updateMariadbById,
|
||||
} from "@dokploy/server";
|
||||
import { db } from "@dokploy/server/db";
|
||||
import {
|
||||
addNewService,
|
||||
checkServiceAccess,
|
||||
checkServicePermissionAndAccess,
|
||||
findMemberByUserId,
|
||||
} from "@dokploy/server/services/permission";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { observable } from "@trpc/server/observable";
|
||||
import { and, desc, eq, ilike, or, sql } from "drizzle-orm";
|
||||
import { z } from "zod";
|
||||
import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc";
|
||||
import { audit } from "@/server/api/utils/audit";
|
||||
import {
|
||||
apiChangeMariaDBStatus,
|
||||
apiCreateMariaDB,
|
||||
@@ -36,27 +40,20 @@ import {
|
||||
apiSaveEnvironmentVariablesMariaDB,
|
||||
apiSaveExternalPortMariaDB,
|
||||
apiUpdateMariaDB,
|
||||
environments,
|
||||
mariadb as mariadbTable,
|
||||
projects,
|
||||
} from "@/server/db/schema";
|
||||
import { environments, projects } from "@/server/db/schema";
|
||||
import { cancelJobs } from "@/server/utils/backup";
|
||||
export const mariadbRouter = createTRPCRouter({
|
||||
create: protectedProcedure
|
||||
.input(apiCreateMariaDB)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
// Get project from environment
|
||||
const environment = await findEnvironmentById(input.environmentId);
|
||||
const project = await findProjectById(environment.projectId);
|
||||
|
||||
if (ctx.user.role === "member") {
|
||||
await checkServiceAccess(
|
||||
ctx.user.id,
|
||||
project.projectId,
|
||||
ctx.session.activeOrganizationId,
|
||||
"create",
|
||||
);
|
||||
}
|
||||
await checkServiceAccess(ctx, project.projectId, "create");
|
||||
|
||||
if (IS_CLOUD && !input.serverId) {
|
||||
throw new TRPCError({
|
||||
@@ -74,13 +71,7 @@ export const mariadbRouter = createTRPCRouter({
|
||||
const newMariadb = await createMariadb({
|
||||
...input,
|
||||
});
|
||||
if (ctx.user.role === "member") {
|
||||
await addNewService(
|
||||
ctx.user.id,
|
||||
newMariadb.mariadbId,
|
||||
project.organizationId,
|
||||
);
|
||||
}
|
||||
await addNewService(ctx, newMariadb.mariadbId);
|
||||
|
||||
await createMount({
|
||||
serviceId: newMariadb.mariadbId,
|
||||
@@ -90,6 +81,12 @@ export const mariadbRouter = createTRPCRouter({
|
||||
type: "volume",
|
||||
});
|
||||
|
||||
await audit(ctx, {
|
||||
action: "create",
|
||||
resourceType: "service",
|
||||
resourceId: newMariadb.mariadbId,
|
||||
resourceName: newMariadb.appName,
|
||||
});
|
||||
return newMariadb;
|
||||
} catch (error) {
|
||||
if (error instanceof TRPCError) {
|
||||
@@ -101,14 +98,7 @@ export const mariadbRouter = createTRPCRouter({
|
||||
one: protectedProcedure
|
||||
.input(apiFindOneMariaDB)
|
||||
.query(async ({ input, ctx }) => {
|
||||
if (ctx.user.role === "member") {
|
||||
await checkServiceAccess(
|
||||
ctx.user.id,
|
||||
input.mariadbId,
|
||||
ctx.session.activeOrganizationId,
|
||||
"access",
|
||||
);
|
||||
}
|
||||
await checkServiceAccess(ctx, input.mariadbId, "read");
|
||||
const mariadb = await findMariadbById(input.mariadbId);
|
||||
if (
|
||||
mariadb.environment.project.organizationId !==
|
||||
@@ -125,16 +115,10 @@ export const mariadbRouter = createTRPCRouter({
|
||||
start: protectedProcedure
|
||||
.input(apiFindOneMariaDB)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.mariadbId, {
|
||||
deployment: ["create"],
|
||||
});
|
||||
const service = await findMariadbById(input.mariadbId);
|
||||
if (
|
||||
service.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to start this Mariadb",
|
||||
});
|
||||
}
|
||||
if (service.serverId) {
|
||||
await startServiceRemote(service.serverId, service.appName);
|
||||
} else {
|
||||
@@ -144,11 +128,20 @@ export const mariadbRouter = createTRPCRouter({
|
||||
applicationStatus: "done",
|
||||
});
|
||||
|
||||
await audit(ctx, {
|
||||
action: "start",
|
||||
resourceType: "service",
|
||||
resourceId: service.mariadbId,
|
||||
resourceName: service.appName,
|
||||
});
|
||||
return service;
|
||||
}),
|
||||
stop: protectedProcedure
|
||||
.input(apiFindOneMariaDB)
|
||||
.mutation(async ({ input }) => {
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.mariadbId, {
|
||||
deployment: ["create"],
|
||||
});
|
||||
const mariadb = await findMariadbById(input.mariadbId);
|
||||
|
||||
if (mariadb.serverId) {
|
||||
@@ -160,21 +153,21 @@ export const mariadbRouter = createTRPCRouter({
|
||||
applicationStatus: "idle",
|
||||
});
|
||||
|
||||
await audit(ctx, {
|
||||
action: "stop",
|
||||
resourceType: "service",
|
||||
resourceId: mariadb.mariadbId,
|
||||
resourceName: mariadb.appName,
|
||||
});
|
||||
return mariadb;
|
||||
}),
|
||||
saveExternalPort: protectedProcedure
|
||||
.input(apiSaveExternalPortMariaDB)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.mariadbId, {
|
||||
service: ["create"],
|
||||
});
|
||||
const mariadb = await findMariadbById(input.mariadbId);
|
||||
if (
|
||||
mariadb.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to save this external port",
|
||||
});
|
||||
}
|
||||
|
||||
if (input.externalPort) {
|
||||
const portCheck = await checkPortInUse(
|
||||
@@ -193,22 +186,28 @@ export const mariadbRouter = createTRPCRouter({
|
||||
externalPort: input.externalPort,
|
||||
});
|
||||
await deployMariadb(input.mariadbId);
|
||||
await audit(ctx, {
|
||||
action: "update",
|
||||
resourceType: "service",
|
||||
resourceId: mariadb.mariadbId,
|
||||
resourceName: mariadb.appName,
|
||||
});
|
||||
return mariadb;
|
||||
}),
|
||||
deploy: protectedProcedure
|
||||
.input(apiDeployMariaDB)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.mariadbId, {
|
||||
deployment: ["create"],
|
||||
});
|
||||
const mariadb = await findMariadbById(input.mariadbId);
|
||||
if (
|
||||
mariadb.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to deploy this Mariadb",
|
||||
});
|
||||
}
|
||||
|
||||
await audit(ctx, {
|
||||
action: "deploy",
|
||||
resourceType: "service",
|
||||
resourceId: mariadb.mariadbId,
|
||||
resourceName: mariadb.appName,
|
||||
});
|
||||
return deployMariadb(input.mariadbId);
|
||||
}),
|
||||
deployWithLogs: protectedProcedure
|
||||
@@ -222,16 +221,9 @@ export const mariadbRouter = createTRPCRouter({
|
||||
})
|
||||
.input(apiDeployMariaDB)
|
||||
.subscription(async ({ input, ctx }) => {
|
||||
const mariadb = await findMariadbById(input.mariadbId);
|
||||
if (
|
||||
mariadb.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to deploy this Mariadb",
|
||||
});
|
||||
}
|
||||
await checkServicePermissionAndAccess(ctx, input.mariadbId, {
|
||||
deployment: ["create"],
|
||||
});
|
||||
|
||||
return observable<string>((emit) => {
|
||||
deployMariadb(input.mariadbId, (log) => {
|
||||
@@ -242,32 +234,25 @@ export const mariadbRouter = createTRPCRouter({
|
||||
changeStatus: protectedProcedure
|
||||
.input(apiChangeMariaDBStatus)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.mariadbId, {
|
||||
deployment: ["create"],
|
||||
});
|
||||
const mongo = await findMariadbById(input.mariadbId);
|
||||
if (
|
||||
mongo.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to change this Mariadb status",
|
||||
});
|
||||
}
|
||||
await updateMariadbById(input.mariadbId, {
|
||||
applicationStatus: input.applicationStatus,
|
||||
});
|
||||
await audit(ctx, {
|
||||
action: "update",
|
||||
resourceType: "service",
|
||||
resourceId: mongo.mariadbId,
|
||||
resourceName: mongo.appName,
|
||||
});
|
||||
return mongo;
|
||||
}),
|
||||
remove: protectedProcedure
|
||||
.input(apiFindOneMariaDB)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
if (ctx.user.role === "member") {
|
||||
await checkServiceAccess(
|
||||
ctx.user.id,
|
||||
input.mariadbId,
|
||||
ctx.session.activeOrganizationId,
|
||||
"delete",
|
||||
);
|
||||
}
|
||||
await checkServiceAccess(ctx, input.mariadbId, "delete");
|
||||
|
||||
const mongo = await findMariadbById(input.mariadbId);
|
||||
if (
|
||||
@@ -280,6 +265,12 @@ export const mariadbRouter = createTRPCRouter({
|
||||
});
|
||||
}
|
||||
|
||||
await audit(ctx, {
|
||||
action: "delete",
|
||||
resourceType: "service",
|
||||
resourceId: mongo.mariadbId,
|
||||
resourceName: mongo.appName,
|
||||
});
|
||||
const backups = await findBackupsByDbId(input.mariadbId, "mariadb");
|
||||
const cleanupOperations = [
|
||||
async () => await removeService(mongo?.appName, mongo.serverId),
|
||||
@@ -298,16 +289,9 @@ export const mariadbRouter = createTRPCRouter({
|
||||
saveEnvironment: protectedProcedure
|
||||
.input(apiSaveEnvironmentVariablesMariaDB)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const mariadb = await findMariadbById(input.mariadbId);
|
||||
if (
|
||||
mariadb.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to save this environment",
|
||||
});
|
||||
}
|
||||
await checkServicePermissionAndAccess(ctx, input.mariadbId, {
|
||||
envVars: ["write"],
|
||||
});
|
||||
const service = await updateMariadbById(input.mariadbId, {
|
||||
env: input.env,
|
||||
});
|
||||
@@ -319,21 +303,20 @@ export const mariadbRouter = createTRPCRouter({
|
||||
});
|
||||
}
|
||||
|
||||
await audit(ctx, {
|
||||
action: "update",
|
||||
resourceType: "service",
|
||||
resourceId: input.mariadbId,
|
||||
});
|
||||
return true;
|
||||
}),
|
||||
reload: protectedProcedure
|
||||
.input(apiResetMariadb)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.mariadbId, {
|
||||
deployment: ["create"],
|
||||
});
|
||||
const mariadb = await findMariadbById(input.mariadbId);
|
||||
if (
|
||||
mariadb.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to reload this Mariadb",
|
||||
});
|
||||
}
|
||||
if (mariadb.serverId) {
|
||||
await stopServiceRemote(mariadb.serverId, mariadb.appName);
|
||||
} else {
|
||||
@@ -351,22 +334,21 @@ export const mariadbRouter = createTRPCRouter({
|
||||
await updateMariadbById(input.mariadbId, {
|
||||
applicationStatus: "done",
|
||||
});
|
||||
await audit(ctx, {
|
||||
action: "reload",
|
||||
resourceType: "service",
|
||||
resourceId: mariadb.mariadbId,
|
||||
resourceName: mariadb.appName,
|
||||
});
|
||||
return true;
|
||||
}),
|
||||
update: protectedProcedure
|
||||
.input(apiUpdateMariaDB)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const { mariadbId, ...rest } = input;
|
||||
const mariadb = await findMariadbById(mariadbId);
|
||||
if (
|
||||
mariadb.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to update this Mariadb",
|
||||
});
|
||||
}
|
||||
await checkServicePermissionAndAccess(ctx, mariadbId, {
|
||||
service: ["create"],
|
||||
});
|
||||
const service = await updateMariadbById(mariadbId, {
|
||||
...rest,
|
||||
});
|
||||
@@ -378,6 +360,12 @@ export const mariadbRouter = createTRPCRouter({
|
||||
});
|
||||
}
|
||||
|
||||
await audit(ctx, {
|
||||
action: "update",
|
||||
resourceType: "service",
|
||||
resourceId: mariadbId,
|
||||
resourceName: service.appName,
|
||||
});
|
||||
return true;
|
||||
}),
|
||||
move: protectedProcedure
|
||||
@@ -388,31 +376,10 @@ export const mariadbRouter = createTRPCRouter({
|
||||
}),
|
||||
)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const mariadb = await findMariadbById(input.mariadbId);
|
||||
if (
|
||||
mariadb.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to move this mariadb",
|
||||
});
|
||||
}
|
||||
await checkServicePermissionAndAccess(ctx, input.mariadbId, {
|
||||
service: ["create"],
|
||||
});
|
||||
|
||||
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 environment",
|
||||
});
|
||||
}
|
||||
|
||||
// Update the mariadb's projectId
|
||||
const updatedMariadb = await db
|
||||
.update(mariadbTable)
|
||||
.set({
|
||||
@@ -429,23 +396,27 @@ export const mariadbRouter = createTRPCRouter({
|
||||
});
|
||||
}
|
||||
|
||||
await audit(ctx, {
|
||||
action: "move",
|
||||
resourceType: "service",
|
||||
resourceId: updatedMariadb.mariadbId,
|
||||
resourceName: updatedMariadb.appName,
|
||||
});
|
||||
return updatedMariadb;
|
||||
}),
|
||||
rebuild: protectedProcedure
|
||||
.input(apiRebuildMariadb)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const mariadb = await findMariadbById(input.mariadbId);
|
||||
if (
|
||||
mariadb.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to rebuild this MariaDB database",
|
||||
});
|
||||
}
|
||||
await checkServicePermissionAndAccess(ctx, input.mariadbId, {
|
||||
deployment: ["create"],
|
||||
});
|
||||
|
||||
await rebuildDatabase(mariadb.mariadbId, "mariadb");
|
||||
await rebuildDatabase(input.mariadbId, "mariadb");
|
||||
await audit(ctx, {
|
||||
action: "rebuild",
|
||||
resourceType: "service",
|
||||
resourceId: input.mariadbId,
|
||||
});
|
||||
return true;
|
||||
}),
|
||||
search: protectedProcedure
|
||||
@@ -499,19 +470,18 @@ export const mariadbRouter = createTRPCRouter({
|
||||
),
|
||||
);
|
||||
}
|
||||
if (ctx.user.role === "member") {
|
||||
const { accessedServices } = await findMemberById(
|
||||
ctx.user.id,
|
||||
ctx.session.activeOrganizationId,
|
||||
);
|
||||
if (accessedServices.length === 0) return { items: [], total: 0 };
|
||||
baseConditions.push(
|
||||
sql`${mariadbTable.mariadbId} IN (${sql.join(
|
||||
accessedServices.map((id) => sql`${id}`),
|
||||
sql`, `,
|
||||
)})`,
|
||||
);
|
||||
}
|
||||
const { accessedServices } = await findMemberByUserId(
|
||||
ctx.user.id,
|
||||
ctx.session.activeOrganizationId,
|
||||
);
|
||||
if (accessedServices.length === 0) return { items: [], total: 0 };
|
||||
baseConditions.push(
|
||||
sql`${mariadbTable.mariadbId} IN (${sql.join(
|
||||
accessedServices.map((id) => sql`${id}`),
|
||||
sql`, `,
|
||||
)})`,
|
||||
);
|
||||
|
||||
const where = and(...baseConditions);
|
||||
const [items, countResult] = await Promise.all([
|
||||
db
|
||||
|
||||
@@ -1,13 +1,10 @@
|
||||
import {
|
||||
addNewService,
|
||||
checkPortInUse,
|
||||
checkServiceAccess,
|
||||
createMongo,
|
||||
createMount,
|
||||
deployMongo,
|
||||
findBackupsByDbId,
|
||||
findEnvironmentById,
|
||||
findMemberById,
|
||||
findMongoById,
|
||||
findProjectById,
|
||||
IS_CLOUD,
|
||||
@@ -20,10 +17,17 @@ import {
|
||||
stopServiceRemote,
|
||||
updateMongoById,
|
||||
} from "@dokploy/server";
|
||||
import {
|
||||
addNewService,
|
||||
checkServiceAccess,
|
||||
checkServicePermissionAndAccess,
|
||||
findMemberByUserId,
|
||||
} from "@dokploy/server/services/permission";
|
||||
import { db } from "@dokploy/server/db";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { and, desc, eq, ilike, or, sql } from "drizzle-orm";
|
||||
import { z } from "zod";
|
||||
import { audit } from "@/server/api/utils/audit";
|
||||
import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc";
|
||||
import {
|
||||
apiChangeMongoStatus,
|
||||
@@ -44,18 +48,10 @@ export const mongoRouter = createTRPCRouter({
|
||||
.input(apiCreateMongo)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
// Get project from environment
|
||||
const environment = await findEnvironmentById(input.environmentId);
|
||||
const project = await findProjectById(environment.projectId);
|
||||
|
||||
if (ctx.user.role === "member") {
|
||||
await checkServiceAccess(
|
||||
ctx.user.id,
|
||||
project.projectId,
|
||||
ctx.session.activeOrganizationId,
|
||||
"create",
|
||||
);
|
||||
}
|
||||
await checkServiceAccess(ctx, project.projectId, "create");
|
||||
|
||||
if (IS_CLOUD && !input.serverId) {
|
||||
throw new TRPCError({
|
||||
@@ -73,13 +69,7 @@ export const mongoRouter = createTRPCRouter({
|
||||
const newMongo = await createMongo({
|
||||
...input,
|
||||
});
|
||||
if (ctx.user.role === "member") {
|
||||
await addNewService(
|
||||
ctx.user.id,
|
||||
newMongo.mongoId,
|
||||
project.organizationId,
|
||||
);
|
||||
}
|
||||
await addNewService(ctx, newMongo.mongoId);
|
||||
|
||||
await createMount({
|
||||
serviceId: newMongo.mongoId,
|
||||
@@ -89,6 +79,12 @@ export const mongoRouter = createTRPCRouter({
|
||||
type: "volume",
|
||||
});
|
||||
|
||||
await audit(ctx, {
|
||||
action: "create",
|
||||
resourceType: "service",
|
||||
resourceId: newMongo.mongoId,
|
||||
resourceName: newMongo.appName,
|
||||
});
|
||||
return newMongo;
|
||||
} catch (error) {
|
||||
if (error instanceof TRPCError) {
|
||||
@@ -104,14 +100,7 @@ export const mongoRouter = createTRPCRouter({
|
||||
one: protectedProcedure
|
||||
.input(apiFindOneMongo)
|
||||
.query(async ({ input, ctx }) => {
|
||||
if (ctx.user.role === "member") {
|
||||
await checkServiceAccess(
|
||||
ctx.user.id,
|
||||
input.mongoId,
|
||||
ctx.session.activeOrganizationId,
|
||||
"access",
|
||||
);
|
||||
}
|
||||
await checkServiceAccess(ctx, input.mongoId, "read");
|
||||
|
||||
const mongo = await findMongoById(input.mongoId);
|
||||
if (
|
||||
@@ -129,18 +118,11 @@ export const mongoRouter = createTRPCRouter({
|
||||
start: protectedProcedure
|
||||
.input(apiFindOneMongo)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.mongoId, {
|
||||
deployment: ["create"],
|
||||
});
|
||||
const service = await findMongoById(input.mongoId);
|
||||
|
||||
if (
|
||||
service.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to start this mongo",
|
||||
});
|
||||
}
|
||||
|
||||
if (service.serverId) {
|
||||
await startServiceRemote(service.serverId, service.appName);
|
||||
} else {
|
||||
@@ -150,23 +132,22 @@ export const mongoRouter = createTRPCRouter({
|
||||
applicationStatus: "done",
|
||||
});
|
||||
|
||||
await audit(ctx, {
|
||||
action: "start",
|
||||
resourceType: "service",
|
||||
resourceId: service.mongoId,
|
||||
resourceName: service.appName,
|
||||
});
|
||||
return service;
|
||||
}),
|
||||
stop: protectedProcedure
|
||||
.input(apiFindOneMongo)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.mongoId, {
|
||||
deployment: ["create"],
|
||||
});
|
||||
const mongo = await findMongoById(input.mongoId);
|
||||
|
||||
if (
|
||||
mongo.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to stop this mongo",
|
||||
});
|
||||
}
|
||||
|
||||
if (mongo.serverId) {
|
||||
await stopServiceRemote(mongo.serverId, mongo.appName);
|
||||
} else {
|
||||
@@ -176,21 +157,21 @@ export const mongoRouter = createTRPCRouter({
|
||||
applicationStatus: "idle",
|
||||
});
|
||||
|
||||
await audit(ctx, {
|
||||
action: "stop",
|
||||
resourceType: "service",
|
||||
resourceId: mongo.mongoId,
|
||||
resourceName: mongo.appName,
|
||||
});
|
||||
return mongo;
|
||||
}),
|
||||
saveExternalPort: protectedProcedure
|
||||
.input(apiSaveExternalPortMongo)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.mongoId, {
|
||||
service: ["create"],
|
||||
});
|
||||
const mongo = await findMongoById(input.mongoId);
|
||||
if (
|
||||
mongo.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to save this external port",
|
||||
});
|
||||
}
|
||||
|
||||
if (input.externalPort) {
|
||||
const portCheck = await checkPortInUse(
|
||||
@@ -209,21 +190,27 @@ export const mongoRouter = createTRPCRouter({
|
||||
externalPort: input.externalPort,
|
||||
});
|
||||
await deployMongo(input.mongoId);
|
||||
await audit(ctx, {
|
||||
action: "update",
|
||||
resourceType: "service",
|
||||
resourceId: mongo.mongoId,
|
||||
resourceName: mongo.appName,
|
||||
});
|
||||
return mongo;
|
||||
}),
|
||||
deploy: protectedProcedure
|
||||
.input(apiDeployMongo)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.mongoId, {
|
||||
deployment: ["create"],
|
||||
});
|
||||
const mongo = await findMongoById(input.mongoId);
|
||||
if (
|
||||
mongo.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to deploy this mongo",
|
||||
});
|
||||
}
|
||||
await audit(ctx, {
|
||||
action: "deploy",
|
||||
resourceType: "service",
|
||||
resourceId: mongo.mongoId,
|
||||
resourceName: mongo.appName,
|
||||
});
|
||||
return deployMongo(input.mongoId);
|
||||
}),
|
||||
deployWithLogs: protectedProcedure
|
||||
@@ -237,16 +224,9 @@ export const mongoRouter = createTRPCRouter({
|
||||
})
|
||||
.input(apiDeployMongo)
|
||||
.subscription(async function* ({ input, ctx, signal }) {
|
||||
const mongo = await findMongoById(input.mongoId);
|
||||
if (
|
||||
mongo.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to deploy this mongo",
|
||||
});
|
||||
}
|
||||
await checkServicePermissionAndAccess(ctx, input.mongoId, {
|
||||
deployment: ["create"],
|
||||
});
|
||||
const queue: string[] = [];
|
||||
const done = false;
|
||||
|
||||
@@ -270,34 +250,28 @@ export const mongoRouter = createTRPCRouter({
|
||||
changeStatus: protectedProcedure
|
||||
.input(apiChangeMongoStatus)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.mongoId, {
|
||||
deployment: ["create"],
|
||||
});
|
||||
const mongo = await findMongoById(input.mongoId);
|
||||
if (
|
||||
mongo.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to change this mongo status",
|
||||
});
|
||||
}
|
||||
await updateMongoById(input.mongoId, {
|
||||
applicationStatus: input.applicationStatus,
|
||||
});
|
||||
await audit(ctx, {
|
||||
action: "update",
|
||||
resourceType: "service",
|
||||
resourceId: mongo.mongoId,
|
||||
resourceName: mongo.appName,
|
||||
});
|
||||
return mongo;
|
||||
}),
|
||||
reload: protectedProcedure
|
||||
.input(apiResetMongo)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.mongoId, {
|
||||
deployment: ["create"],
|
||||
});
|
||||
const mongo = await findMongoById(input.mongoId);
|
||||
if (
|
||||
mongo.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to reload this mongo",
|
||||
});
|
||||
}
|
||||
if (mongo.serverId) {
|
||||
await stopServiceRemote(mongo.serverId, mongo.appName);
|
||||
} else {
|
||||
@@ -315,19 +289,18 @@ export const mongoRouter = createTRPCRouter({
|
||||
await updateMongoById(input.mongoId, {
|
||||
applicationStatus: "done",
|
||||
});
|
||||
await audit(ctx, {
|
||||
action: "reload",
|
||||
resourceType: "service",
|
||||
resourceId: mongo.mongoId,
|
||||
resourceName: mongo.appName,
|
||||
});
|
||||
return true;
|
||||
}),
|
||||
remove: protectedProcedure
|
||||
.input(apiFindOneMongo)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
if (ctx.user.role === "member") {
|
||||
await checkServiceAccess(
|
||||
ctx.user.id,
|
||||
input.mongoId,
|
||||
ctx.session.activeOrganizationId,
|
||||
"delete",
|
||||
);
|
||||
}
|
||||
await checkServiceAccess(ctx, input.mongoId, "delete");
|
||||
|
||||
const mongo = await findMongoById(input.mongoId);
|
||||
|
||||
@@ -340,6 +313,12 @@ export const mongoRouter = createTRPCRouter({
|
||||
message: "You are not authorized to delete this mongo",
|
||||
});
|
||||
}
|
||||
await audit(ctx, {
|
||||
action: "delete",
|
||||
resourceType: "service",
|
||||
resourceId: mongo.mongoId,
|
||||
resourceName: mongo.appName,
|
||||
});
|
||||
const backups = await findBackupsByDbId(input.mongoId, "mongo");
|
||||
|
||||
const cleanupOperations = [
|
||||
@@ -359,16 +338,9 @@ export const mongoRouter = createTRPCRouter({
|
||||
saveEnvironment: protectedProcedure
|
||||
.input(apiSaveEnvironmentVariablesMongo)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const mongo = await findMongoById(input.mongoId);
|
||||
if (
|
||||
mongo.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to save this environment",
|
||||
});
|
||||
}
|
||||
await checkServicePermissionAndAccess(ctx, input.mongoId, {
|
||||
envVars: ["write"],
|
||||
});
|
||||
const service = await updateMongoById(input.mongoId, {
|
||||
env: input.env,
|
||||
});
|
||||
@@ -380,22 +352,20 @@ export const mongoRouter = createTRPCRouter({
|
||||
});
|
||||
}
|
||||
|
||||
await audit(ctx, {
|
||||
action: "update",
|
||||
resourceType: "service",
|
||||
resourceId: input.mongoId,
|
||||
});
|
||||
return true;
|
||||
}),
|
||||
update: protectedProcedure
|
||||
.input(apiUpdateMongo)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const { mongoId, ...rest } = input;
|
||||
const mongo = await findMongoById(mongoId);
|
||||
if (
|
||||
mongo.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to update this mongo",
|
||||
});
|
||||
}
|
||||
await checkServicePermissionAndAccess(ctx, mongoId, {
|
||||
service: ["create"],
|
||||
});
|
||||
const service = await updateMongoById(mongoId, {
|
||||
...rest,
|
||||
});
|
||||
@@ -407,6 +377,12 @@ export const mongoRouter = createTRPCRouter({
|
||||
});
|
||||
}
|
||||
|
||||
await audit(ctx, {
|
||||
action: "update",
|
||||
resourceType: "service",
|
||||
resourceId: mongoId,
|
||||
resourceName: service.appName,
|
||||
});
|
||||
return true;
|
||||
}),
|
||||
move: protectedProcedure
|
||||
@@ -417,31 +393,10 @@ export const mongoRouter = createTRPCRouter({
|
||||
}),
|
||||
)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const mongo = await findMongoById(input.mongoId);
|
||||
if (
|
||||
mongo.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to move this mongo",
|
||||
});
|
||||
}
|
||||
await checkServicePermissionAndAccess(ctx, input.mongoId, {
|
||||
service: ["create"],
|
||||
});
|
||||
|
||||
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 environment",
|
||||
});
|
||||
}
|
||||
|
||||
// Update the mongo's projectId
|
||||
const updatedMongo = await db
|
||||
.update(mongoTable)
|
||||
.set({
|
||||
@@ -458,24 +413,28 @@ export const mongoRouter = createTRPCRouter({
|
||||
});
|
||||
}
|
||||
|
||||
await audit(ctx, {
|
||||
action: "move",
|
||||
resourceType: "service",
|
||||
resourceId: updatedMongo.mongoId,
|
||||
resourceName: updatedMongo.appName,
|
||||
});
|
||||
return updatedMongo;
|
||||
}),
|
||||
rebuild: protectedProcedure
|
||||
.input(apiRebuildMongo)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const mongo = await findMongoById(input.mongoId);
|
||||
if (
|
||||
mongo.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to rebuild this MongoDB database",
|
||||
});
|
||||
}
|
||||
await checkServicePermissionAndAccess(ctx, input.mongoId, {
|
||||
deployment: ["create"],
|
||||
});
|
||||
|
||||
await rebuildDatabase(mongo.mongoId, "mongo");
|
||||
await rebuildDatabase(input.mongoId, "mongo");
|
||||
|
||||
await audit(ctx, {
|
||||
action: "rebuild",
|
||||
resourceType: "service",
|
||||
resourceId: input.mongoId,
|
||||
});
|
||||
return true;
|
||||
}),
|
||||
search: protectedProcedure
|
||||
@@ -524,19 +483,18 @@ export const mongoRouter = createTRPCRouter({
|
||||
ilike(mongoTable.description ?? "", `%${input.description.trim()}%`),
|
||||
);
|
||||
}
|
||||
if (ctx.user.role === "member") {
|
||||
const { accessedServices } = await findMemberById(
|
||||
ctx.user.id,
|
||||
ctx.session.activeOrganizationId,
|
||||
);
|
||||
if (accessedServices.length === 0) return { items: [], total: 0 };
|
||||
baseConditions.push(
|
||||
sql`${mongoTable.mongoId} IN (${sql.join(
|
||||
accessedServices.map((id) => sql`${id}`),
|
||||
sql`, `,
|
||||
)})`,
|
||||
);
|
||||
}
|
||||
const { accessedServices } = await findMemberByUserId(
|
||||
ctx.user.id,
|
||||
ctx.session.activeOrganizationId,
|
||||
);
|
||||
if (accessedServices.length === 0) return { items: [], total: 0 };
|
||||
baseConditions.push(
|
||||
sql`${mongoTable.mongoId} IN (${sql.join(
|
||||
accessedServices.map((id) => sql`${id}`),
|
||||
sql`, `,
|
||||
)})`,
|
||||
);
|
||||
|
||||
const where = and(...baseConditions);
|
||||
const [items, countResult] = await Promise.all([
|
||||
db
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import {
|
||||
checkServiceAccess,
|
||||
createMount,
|
||||
deleteMount,
|
||||
findApplicationById,
|
||||
@@ -7,7 +6,6 @@ import {
|
||||
findMariadbById,
|
||||
findMongoById,
|
||||
findMountById,
|
||||
findMountOrganizationId,
|
||||
findMountsByApplicationId,
|
||||
findMySqlById,
|
||||
findPostgresById,
|
||||
@@ -15,6 +13,10 @@ import {
|
||||
getServiceContainer,
|
||||
updateMount,
|
||||
} from "@dokploy/server";
|
||||
import {
|
||||
checkServiceAccess,
|
||||
checkServicePermissionAndAccess,
|
||||
} from "@dokploy/server/services/permission";
|
||||
import type { ServiceType } from "@dokploy/server/db/schema/mount";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { z } from "zod";
|
||||
@@ -26,6 +28,7 @@ import {
|
||||
apiUpdateMount,
|
||||
} from "@/server/db/schema";
|
||||
import { createTRPCRouter, protectedProcedure } from "../trpc";
|
||||
import { audit } from "@/server/api/utils/audit";
|
||||
|
||||
async function getServiceOrganizationId(
|
||||
serviceId: string,
|
||||
@@ -68,49 +71,94 @@ async function getServiceOrganizationId(
|
||||
export const mountRouter = createTRPCRouter({
|
||||
create: protectedProcedure
|
||||
.input(apiCreateMount)
|
||||
.mutation(async ({ input }) => {
|
||||
return await createMount(input);
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.serviceId, {
|
||||
volume: ["create"],
|
||||
});
|
||||
const mount = await createMount(input);
|
||||
await audit(ctx, {
|
||||
action: "create",
|
||||
resourceType: "mount",
|
||||
resourceId: mount.mountId,
|
||||
resourceName: input.mountPath,
|
||||
});
|
||||
return mount;
|
||||
}),
|
||||
remove: protectedProcedure
|
||||
.input(apiRemoveMount)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const organizationId = await findMountOrganizationId(input.mountId);
|
||||
if (organizationId !== ctx.session.activeOrganizationId) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to delete this mount",
|
||||
const mount = await findMountById(input.mountId);
|
||||
const serviceId =
|
||||
mount.applicationId ||
|
||||
mount.postgresId ||
|
||||
mount.mariadbId ||
|
||||
mount.mongoId ||
|
||||
mount.mysqlId ||
|
||||
mount.redisId ||
|
||||
mount.composeId;
|
||||
if (serviceId) {
|
||||
await checkServicePermissionAndAccess(ctx, serviceId, {
|
||||
volume: ["delete"],
|
||||
});
|
||||
}
|
||||
await audit(ctx, {
|
||||
action: "delete",
|
||||
resourceType: "mount",
|
||||
resourceId: input.mountId,
|
||||
});
|
||||
return await deleteMount(input.mountId);
|
||||
}),
|
||||
|
||||
one: protectedProcedure
|
||||
.input(apiFindOneMount)
|
||||
.query(async ({ input, ctx }) => {
|
||||
const organizationId = await findMountOrganizationId(input.mountId);
|
||||
if (organizationId !== ctx.session.activeOrganizationId) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to access this mount",
|
||||
const mount = await findMountById(input.mountId);
|
||||
const serviceId =
|
||||
mount.applicationId ||
|
||||
mount.postgresId ||
|
||||
mount.mariadbId ||
|
||||
mount.mongoId ||
|
||||
mount.mysqlId ||
|
||||
mount.redisId ||
|
||||
mount.composeId;
|
||||
if (serviceId) {
|
||||
await checkServicePermissionAndAccess(ctx, serviceId, {
|
||||
volume: ["read"],
|
||||
});
|
||||
}
|
||||
return await findMountById(input.mountId);
|
||||
return mount;
|
||||
}),
|
||||
update: protectedProcedure
|
||||
.input(apiUpdateMount)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const organizationId = await findMountOrganizationId(input.mountId);
|
||||
if (organizationId !== ctx.session.activeOrganizationId) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to update this mount",
|
||||
const mount = await findMountById(input.mountId);
|
||||
const serviceId =
|
||||
mount.applicationId ||
|
||||
mount.postgresId ||
|
||||
mount.mariadbId ||
|
||||
mount.mongoId ||
|
||||
mount.mysqlId ||
|
||||
mount.redisId ||
|
||||
mount.composeId;
|
||||
if (serviceId) {
|
||||
await checkServicePermissionAndAccess(ctx, serviceId, {
|
||||
volume: ["create"],
|
||||
});
|
||||
}
|
||||
await audit(ctx, {
|
||||
action: "update",
|
||||
resourceType: "mount",
|
||||
resourceId: input.mountId,
|
||||
resourceName: input.mountPath,
|
||||
});
|
||||
return await updateMount(input.mountId, input);
|
||||
}),
|
||||
allNamedByApplicationId: protectedProcedure
|
||||
.input(z.object({ applicationId: z.string().min(1) }))
|
||||
.query(async ({ input }) => {
|
||||
.query(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.applicationId, {
|
||||
volume: ["read"],
|
||||
});
|
||||
const app = await findApplicationById(input.applicationId);
|
||||
const container = await getServiceContainer(app.appName, app.serverId);
|
||||
const mounts = container?.Mounts.filter(
|
||||
@@ -122,14 +170,7 @@ export const mountRouter = createTRPCRouter({
|
||||
.input(apiFindMountByApplicationId)
|
||||
.query(async ({ input, ctx }) => {
|
||||
console.log("input", input);
|
||||
if (ctx.user.role === "member") {
|
||||
await checkServiceAccess(
|
||||
ctx.user.id,
|
||||
input.serviceId,
|
||||
ctx.session.activeOrganizationId,
|
||||
"access",
|
||||
);
|
||||
}
|
||||
await checkServiceAccess(ctx, input.serviceId, "read");
|
||||
const organizationId = await getServiceOrganizationId(
|
||||
input.serviceId,
|
||||
input.serviceType,
|
||||
|
||||
@@ -1,13 +1,10 @@
|
||||
import {
|
||||
addNewService,
|
||||
checkPortInUse,
|
||||
checkServiceAccess,
|
||||
createMount,
|
||||
createMysql,
|
||||
deployMySql,
|
||||
findBackupsByDbId,
|
||||
findEnvironmentById,
|
||||
findMemberById,
|
||||
findMySqlById,
|
||||
findProjectById,
|
||||
IS_CLOUD,
|
||||
@@ -20,10 +17,17 @@ import {
|
||||
stopServiceRemote,
|
||||
updateMySqlById,
|
||||
} from "@dokploy/server";
|
||||
import {
|
||||
addNewService,
|
||||
checkServiceAccess,
|
||||
checkServicePermissionAndAccess,
|
||||
findMemberByUserId,
|
||||
} from "@dokploy/server/services/permission";
|
||||
import { db } from "@dokploy/server/db";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { and, desc, eq, ilike, or, sql } from "drizzle-orm";
|
||||
import { z } from "zod";
|
||||
import { audit } from "@/server/api/utils/audit";
|
||||
import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc";
|
||||
import {
|
||||
apiChangeMySqlStatus,
|
||||
@@ -46,18 +50,10 @@ export const mysqlRouter = createTRPCRouter({
|
||||
.input(apiCreateMySql)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
// Get project from environment
|
||||
const environment = await findEnvironmentById(input.environmentId);
|
||||
const project = await findProjectById(environment.projectId);
|
||||
|
||||
if (ctx.user.role === "member") {
|
||||
await checkServiceAccess(
|
||||
ctx.user.id,
|
||||
project.projectId,
|
||||
ctx.session.activeOrganizationId,
|
||||
"create",
|
||||
);
|
||||
}
|
||||
await checkServiceAccess(ctx, project.projectId, "create");
|
||||
|
||||
if (IS_CLOUD && !input.serverId) {
|
||||
throw new TRPCError({
|
||||
@@ -76,13 +72,7 @@ export const mysqlRouter = createTRPCRouter({
|
||||
const newMysql = await createMysql({
|
||||
...input,
|
||||
});
|
||||
if (ctx.user.role === "member") {
|
||||
await addNewService(
|
||||
ctx.user.id,
|
||||
newMysql.mysqlId,
|
||||
project.organizationId,
|
||||
);
|
||||
}
|
||||
await addNewService(ctx, newMysql.mysqlId);
|
||||
|
||||
await createMount({
|
||||
serviceId: newMysql.mysqlId,
|
||||
@@ -92,6 +82,12 @@ export const mysqlRouter = createTRPCRouter({
|
||||
type: "volume",
|
||||
});
|
||||
|
||||
await audit(ctx, {
|
||||
action: "create",
|
||||
resourceType: "service",
|
||||
resourceId: newMysql.mysqlId,
|
||||
resourceName: newMysql.appName,
|
||||
});
|
||||
return newMysql;
|
||||
} catch (error) {
|
||||
if (error instanceof TRPCError) {
|
||||
@@ -107,14 +103,7 @@ export const mysqlRouter = createTRPCRouter({
|
||||
one: protectedProcedure
|
||||
.input(apiFindOneMySql)
|
||||
.query(async ({ input, ctx }) => {
|
||||
if (ctx.user.role === "member") {
|
||||
await checkServiceAccess(
|
||||
ctx.user.id,
|
||||
input.mysqlId,
|
||||
ctx.session.activeOrganizationId,
|
||||
"access",
|
||||
);
|
||||
}
|
||||
await checkServiceAccess(ctx, input.mysqlId, "read");
|
||||
const mysql = await findMySqlById(input.mysqlId);
|
||||
if (
|
||||
mysql.environment.project.organizationId !==
|
||||
@@ -131,16 +120,10 @@ export const mysqlRouter = createTRPCRouter({
|
||||
start: protectedProcedure
|
||||
.input(apiFindOneMySql)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.mysqlId, {
|
||||
deployment: ["create"],
|
||||
});
|
||||
const service = await findMySqlById(input.mysqlId);
|
||||
if (
|
||||
service.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to start this MySQL",
|
||||
});
|
||||
}
|
||||
|
||||
if (service.serverId) {
|
||||
await startServiceRemote(service.serverId, service.appName);
|
||||
@@ -151,21 +134,21 @@ export const mysqlRouter = createTRPCRouter({
|
||||
applicationStatus: "done",
|
||||
});
|
||||
|
||||
await audit(ctx, {
|
||||
action: "start",
|
||||
resourceType: "service",
|
||||
resourceId: service.mysqlId,
|
||||
resourceName: service.appName,
|
||||
});
|
||||
return service;
|
||||
}),
|
||||
stop: protectedProcedure
|
||||
.input(apiFindOneMySql)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.mysqlId, {
|
||||
deployment: ["create"],
|
||||
});
|
||||
const mongo = await findMySqlById(input.mysqlId);
|
||||
if (
|
||||
mongo.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to stop this MySQL",
|
||||
});
|
||||
}
|
||||
if (mongo.serverId) {
|
||||
await stopServiceRemote(mongo.serverId, mongo.appName);
|
||||
} else {
|
||||
@@ -175,21 +158,21 @@ export const mysqlRouter = createTRPCRouter({
|
||||
applicationStatus: "idle",
|
||||
});
|
||||
|
||||
await audit(ctx, {
|
||||
action: "stop",
|
||||
resourceType: "service",
|
||||
resourceId: mongo.mysqlId,
|
||||
resourceName: mongo.appName,
|
||||
});
|
||||
return mongo;
|
||||
}),
|
||||
saveExternalPort: protectedProcedure
|
||||
.input(apiSaveExternalPortMySql)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.mysqlId, {
|
||||
service: ["create"],
|
||||
});
|
||||
const mysql = await findMySqlById(input.mysqlId);
|
||||
if (
|
||||
mysql.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to save this external port",
|
||||
});
|
||||
}
|
||||
|
||||
if (input.externalPort) {
|
||||
const portCheck = await checkPortInUse(
|
||||
@@ -208,21 +191,27 @@ export const mysqlRouter = createTRPCRouter({
|
||||
externalPort: input.externalPort,
|
||||
});
|
||||
await deployMySql(input.mysqlId);
|
||||
await audit(ctx, {
|
||||
action: "update",
|
||||
resourceType: "service",
|
||||
resourceId: mysql.mysqlId,
|
||||
resourceName: mysql.appName,
|
||||
});
|
||||
return mysql;
|
||||
}),
|
||||
deploy: protectedProcedure
|
||||
.input(apiDeployMySql)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.mysqlId, {
|
||||
deployment: ["create"],
|
||||
});
|
||||
const mysql = await findMySqlById(input.mysqlId);
|
||||
if (
|
||||
mysql.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to deploy this MySQL",
|
||||
});
|
||||
}
|
||||
await audit(ctx, {
|
||||
action: "deploy",
|
||||
resourceType: "service",
|
||||
resourceId: mysql.mysqlId,
|
||||
resourceName: mysql.appName,
|
||||
});
|
||||
return deployMySql(input.mysqlId);
|
||||
}),
|
||||
deployWithLogs: protectedProcedure
|
||||
@@ -236,16 +225,9 @@ export const mysqlRouter = createTRPCRouter({
|
||||
})
|
||||
.input(apiDeployMySql)
|
||||
.subscription(async function* ({ input, ctx, signal }) {
|
||||
const mysql = await findMySqlById(input.mysqlId);
|
||||
if (
|
||||
mysql.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to deploy this MySQL",
|
||||
});
|
||||
}
|
||||
await checkServicePermissionAndAccess(ctx, input.mysqlId, {
|
||||
deployment: ["create"],
|
||||
});
|
||||
|
||||
const queue: string[] = [];
|
||||
const done = false;
|
||||
@@ -269,34 +251,28 @@ export const mysqlRouter = createTRPCRouter({
|
||||
changeStatus: protectedProcedure
|
||||
.input(apiChangeMySqlStatus)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.mysqlId, {
|
||||
deployment: ["create"],
|
||||
});
|
||||
const mongo = await findMySqlById(input.mysqlId);
|
||||
if (
|
||||
mongo.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to change this MySQL status",
|
||||
});
|
||||
}
|
||||
await updateMySqlById(input.mysqlId, {
|
||||
applicationStatus: input.applicationStatus,
|
||||
});
|
||||
await audit(ctx, {
|
||||
action: "update",
|
||||
resourceType: "service",
|
||||
resourceId: mongo.mysqlId,
|
||||
resourceName: mongo.appName,
|
||||
});
|
||||
return mongo;
|
||||
}),
|
||||
reload: protectedProcedure
|
||||
.input(apiResetMysql)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.mysqlId, {
|
||||
deployment: ["create"],
|
||||
});
|
||||
const mysql = await findMySqlById(input.mysqlId);
|
||||
if (
|
||||
mysql.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to reload this MySQL",
|
||||
});
|
||||
}
|
||||
if (mysql.serverId) {
|
||||
await stopServiceRemote(mysql.serverId, mysql.appName);
|
||||
} else {
|
||||
@@ -313,19 +289,18 @@ export const mysqlRouter = createTRPCRouter({
|
||||
await updateMySqlById(input.mysqlId, {
|
||||
applicationStatus: "done",
|
||||
});
|
||||
await audit(ctx, {
|
||||
action: "reload",
|
||||
resourceType: "service",
|
||||
resourceId: mysql.mysqlId,
|
||||
resourceName: mysql.appName,
|
||||
});
|
||||
return true;
|
||||
}),
|
||||
remove: protectedProcedure
|
||||
.input(apiFindOneMySql)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
if (ctx.user.role === "member") {
|
||||
await checkServiceAccess(
|
||||
ctx.user.id,
|
||||
input.mysqlId,
|
||||
ctx.session.activeOrganizationId,
|
||||
"delete",
|
||||
);
|
||||
}
|
||||
await checkServiceAccess(ctx, input.mysqlId, "delete");
|
||||
const mongo = await findMySqlById(input.mysqlId);
|
||||
if (
|
||||
mongo.environment.project.organizationId !==
|
||||
@@ -337,6 +312,12 @@ export const mysqlRouter = createTRPCRouter({
|
||||
});
|
||||
}
|
||||
|
||||
await audit(ctx, {
|
||||
action: "delete",
|
||||
resourceType: "service",
|
||||
resourceId: mongo.mysqlId,
|
||||
resourceName: mongo.appName,
|
||||
});
|
||||
const backups = await findBackupsByDbId(input.mysqlId, "mysql");
|
||||
const cleanupOperations = [
|
||||
async () => await removeService(mongo?.appName, mongo.serverId),
|
||||
@@ -355,16 +336,9 @@ export const mysqlRouter = createTRPCRouter({
|
||||
saveEnvironment: protectedProcedure
|
||||
.input(apiSaveEnvironmentVariablesMySql)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const mysql = await findMySqlById(input.mysqlId);
|
||||
if (
|
||||
mysql.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to save this environment",
|
||||
});
|
||||
}
|
||||
await checkServicePermissionAndAccess(ctx, input.mysqlId, {
|
||||
envVars: ["write"],
|
||||
});
|
||||
const service = await updateMySqlById(input.mysqlId, {
|
||||
env: input.env,
|
||||
});
|
||||
@@ -376,22 +350,20 @@ export const mysqlRouter = createTRPCRouter({
|
||||
});
|
||||
}
|
||||
|
||||
await audit(ctx, {
|
||||
action: "update",
|
||||
resourceType: "service",
|
||||
resourceId: input.mysqlId,
|
||||
});
|
||||
return true;
|
||||
}),
|
||||
update: protectedProcedure
|
||||
.input(apiUpdateMySql)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const { mysqlId, ...rest } = input;
|
||||
const mysql = await findMySqlById(mysqlId);
|
||||
if (
|
||||
mysql.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to update this MySQL",
|
||||
});
|
||||
}
|
||||
await checkServicePermissionAndAccess(ctx, mysqlId, {
|
||||
service: ["create"],
|
||||
});
|
||||
const service = await updateMySqlById(mysqlId, {
|
||||
...rest,
|
||||
});
|
||||
@@ -403,6 +375,12 @@ export const mysqlRouter = createTRPCRouter({
|
||||
});
|
||||
}
|
||||
|
||||
await audit(ctx, {
|
||||
action: "update",
|
||||
resourceType: "service",
|
||||
resourceId: mysqlId,
|
||||
resourceName: service.appName,
|
||||
});
|
||||
return true;
|
||||
}),
|
||||
move: protectedProcedure
|
||||
@@ -413,31 +391,10 @@ export const mysqlRouter = createTRPCRouter({
|
||||
}),
|
||||
)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const mysql = await findMySqlById(input.mysqlId);
|
||||
if (
|
||||
mysql.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to move this mysql",
|
||||
});
|
||||
}
|
||||
await checkServicePermissionAndAccess(ctx, input.mysqlId, {
|
||||
service: ["create"],
|
||||
});
|
||||
|
||||
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 environment",
|
||||
});
|
||||
}
|
||||
|
||||
// Update the mysql's projectId
|
||||
const updatedMysql = await db
|
||||
.update(mysqlTable)
|
||||
.set({
|
||||
@@ -454,24 +411,28 @@ export const mysqlRouter = createTRPCRouter({
|
||||
});
|
||||
}
|
||||
|
||||
await audit(ctx, {
|
||||
action: "move",
|
||||
resourceType: "service",
|
||||
resourceId: updatedMysql.mysqlId,
|
||||
resourceName: updatedMysql.appName,
|
||||
});
|
||||
return updatedMysql;
|
||||
}),
|
||||
rebuild: protectedProcedure
|
||||
.input(apiRebuildMysql)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const mysql = await findMySqlById(input.mysqlId);
|
||||
if (
|
||||
mysql.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to rebuild this MySQL database",
|
||||
});
|
||||
}
|
||||
await checkServicePermissionAndAccess(ctx, input.mysqlId, {
|
||||
deployment: ["create"],
|
||||
});
|
||||
|
||||
await rebuildDatabase(mysql.mysqlId, "mysql");
|
||||
await rebuildDatabase(input.mysqlId, "mysql");
|
||||
|
||||
await audit(ctx, {
|
||||
action: "rebuild",
|
||||
resourceType: "service",
|
||||
resourceId: input.mysqlId,
|
||||
});
|
||||
return true;
|
||||
}),
|
||||
search: protectedProcedure
|
||||
@@ -520,19 +481,18 @@ export const mysqlRouter = createTRPCRouter({
|
||||
ilike(mysqlTable.description ?? "", `%${input.description.trim()}%`),
|
||||
);
|
||||
}
|
||||
if (ctx.user.role === "member") {
|
||||
const { accessedServices } = await findMemberById(
|
||||
ctx.user.id,
|
||||
ctx.session.activeOrganizationId,
|
||||
);
|
||||
if (accessedServices.length === 0) return { items: [], total: 0 };
|
||||
baseConditions.push(
|
||||
sql`${mysqlTable.mysqlId} IN (${sql.join(
|
||||
accessedServices.map((id) => sql`${id}`),
|
||||
sql`, `,
|
||||
)})`,
|
||||
);
|
||||
}
|
||||
const { accessedServices } = await findMemberByUserId(
|
||||
ctx.user.id,
|
||||
ctx.session.activeOrganizationId,
|
||||
);
|
||||
if (accessedServices.length === 0) return { items: [], total: 0 };
|
||||
baseConditions.push(
|
||||
sql`${mysqlTable.mysqlId} IN (${sql.join(
|
||||
accessedServices.map((id) => sql`${id}`),
|
||||
sql`, `,
|
||||
)})`,
|
||||
);
|
||||
|
||||
const where = and(...baseConditions);
|
||||
const [items, countResult] = await Promise.all([
|
||||
db
|
||||
|
||||
@@ -43,11 +43,11 @@ import { TRPCError } from "@trpc/server";
|
||||
import { desc, eq, sql } from "drizzle-orm";
|
||||
import { z } from "zod";
|
||||
import {
|
||||
adminProcedure,
|
||||
createTRPCRouter,
|
||||
protectedProcedure,
|
||||
publicProcedure,
|
||||
withPermission,
|
||||
} from "@/server/api/trpc";
|
||||
import { audit } from "@/server/api/utils/audit";
|
||||
import {
|
||||
apiCreateCustom,
|
||||
apiCreateDiscord,
|
||||
@@ -88,15 +88,18 @@ import {
|
||||
} from "@/server/db/schema";
|
||||
|
||||
export const notificationRouter = createTRPCRouter({
|
||||
createSlack: adminProcedure
|
||||
createSlack: withPermission("notification", "create")
|
||||
.input(apiCreateSlack)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
return await createSlackNotification(
|
||||
input,
|
||||
ctx.session.activeOrganizationId,
|
||||
);
|
||||
await createSlackNotification(input, ctx.session.activeOrganizationId);
|
||||
await audit(ctx, {
|
||||
action: "create",
|
||||
resourceType: "notification",
|
||||
resourceName: input.name,
|
||||
});
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error creating the notification",
|
||||
@@ -104,7 +107,7 @@ export const notificationRouter = createTRPCRouter({
|
||||
});
|
||||
}
|
||||
}),
|
||||
updateSlack: adminProcedure
|
||||
updateSlack: withPermission("notification", "update")
|
||||
.input(apiUpdateSlack)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
@@ -115,15 +118,22 @@ export const notificationRouter = createTRPCRouter({
|
||||
message: "You are not authorized to update this notification",
|
||||
});
|
||||
}
|
||||
return await updateSlackNotification({
|
||||
const result = await updateSlackNotification({
|
||||
...input,
|
||||
organizationId: ctx.session.activeOrganizationId,
|
||||
});
|
||||
await audit(ctx, {
|
||||
action: "update",
|
||||
resourceType: "notification",
|
||||
resourceId: input.notificationId,
|
||||
resourceName: notification.name,
|
||||
});
|
||||
return result;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
}),
|
||||
testSlackConnection: adminProcedure
|
||||
testSlackConnection: withPermission("notification", "create")
|
||||
.input(apiTestSlackConnection)
|
||||
.mutation(async ({ input }) => {
|
||||
try {
|
||||
@@ -140,14 +150,19 @@ export const notificationRouter = createTRPCRouter({
|
||||
});
|
||||
}
|
||||
}),
|
||||
createTelegram: adminProcedure
|
||||
createTelegram: withPermission("notification", "create")
|
||||
.input(apiCreateTelegram)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
return await createTelegramNotification(
|
||||
await createTelegramNotification(
|
||||
input,
|
||||
ctx.session.activeOrganizationId,
|
||||
);
|
||||
await audit(ctx, {
|
||||
action: "create",
|
||||
resourceType: "notification",
|
||||
resourceName: input.name,
|
||||
});
|
||||
} catch (error) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
@@ -157,7 +172,7 @@ export const notificationRouter = createTRPCRouter({
|
||||
}
|
||||
}),
|
||||
|
||||
updateTelegram: adminProcedure
|
||||
updateTelegram: withPermission("notification", "update")
|
||||
.input(apiUpdateTelegram)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
@@ -168,10 +183,17 @@ export const notificationRouter = createTRPCRouter({
|
||||
message: "You are not authorized to update this notification",
|
||||
});
|
||||
}
|
||||
return await updateTelegramNotification({
|
||||
const result = await updateTelegramNotification({
|
||||
...input,
|
||||
organizationId: ctx.session.activeOrganizationId,
|
||||
});
|
||||
await audit(ctx, {
|
||||
action: "update",
|
||||
resourceType: "notification",
|
||||
resourceId: input.notificationId,
|
||||
resourceName: notification.name,
|
||||
});
|
||||
return result;
|
||||
} catch (error) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
@@ -180,7 +202,7 @@ export const notificationRouter = createTRPCRouter({
|
||||
});
|
||||
}
|
||||
}),
|
||||
testTelegramConnection: adminProcedure
|
||||
testTelegramConnection: withPermission("notification", "create")
|
||||
.input(apiTestTelegramConnection)
|
||||
.mutation(async ({ input }) => {
|
||||
try {
|
||||
@@ -194,14 +216,19 @@ export const notificationRouter = createTRPCRouter({
|
||||
});
|
||||
}
|
||||
}),
|
||||
createDiscord: adminProcedure
|
||||
createDiscord: withPermission("notification", "create")
|
||||
.input(apiCreateDiscord)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
return await createDiscordNotification(
|
||||
await createDiscordNotification(
|
||||
input,
|
||||
ctx.session.activeOrganizationId,
|
||||
);
|
||||
await audit(ctx, {
|
||||
action: "create",
|
||||
resourceType: "notification",
|
||||
resourceName: input.name,
|
||||
});
|
||||
} catch (error) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
@@ -211,7 +238,7 @@ export const notificationRouter = createTRPCRouter({
|
||||
}
|
||||
}),
|
||||
|
||||
updateDiscord: adminProcedure
|
||||
updateDiscord: withPermission("notification", "update")
|
||||
.input(apiUpdateDiscord)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
@@ -222,10 +249,17 @@ export const notificationRouter = createTRPCRouter({
|
||||
message: "You are not authorized to update this notification",
|
||||
});
|
||||
}
|
||||
return await updateDiscordNotification({
|
||||
const result = await updateDiscordNotification({
|
||||
...input,
|
||||
organizationId: ctx.session.activeOrganizationId,
|
||||
});
|
||||
await audit(ctx, {
|
||||
action: "update",
|
||||
resourceType: "notification",
|
||||
resourceId: input.notificationId,
|
||||
resourceName: notification.name,
|
||||
});
|
||||
return result;
|
||||
} catch (error) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
@@ -235,7 +269,7 @@ export const notificationRouter = createTRPCRouter({
|
||||
}
|
||||
}),
|
||||
|
||||
testDiscordConnection: adminProcedure
|
||||
testDiscordConnection: withPermission("notification", "create")
|
||||
.input(apiTestDiscordConnection)
|
||||
.mutation(async ({ input }) => {
|
||||
try {
|
||||
@@ -257,14 +291,16 @@ export const notificationRouter = createTRPCRouter({
|
||||
});
|
||||
}
|
||||
}),
|
||||
createEmail: adminProcedure
|
||||
createEmail: withPermission("notification", "create")
|
||||
.input(apiCreateEmail)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
return await createEmailNotification(
|
||||
input,
|
||||
ctx.session.activeOrganizationId,
|
||||
);
|
||||
await createEmailNotification(input, ctx.session.activeOrganizationId);
|
||||
await audit(ctx, {
|
||||
action: "create",
|
||||
resourceType: "notification",
|
||||
resourceName: input.name,
|
||||
});
|
||||
} catch (error) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
@@ -273,7 +309,7 @@ export const notificationRouter = createTRPCRouter({
|
||||
});
|
||||
}
|
||||
}),
|
||||
updateEmail: adminProcedure
|
||||
updateEmail: withPermission("notification", "update")
|
||||
.input(apiUpdateEmail)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
@@ -284,10 +320,17 @@ export const notificationRouter = createTRPCRouter({
|
||||
message: "You are not authorized to update this notification",
|
||||
});
|
||||
}
|
||||
return await updateEmailNotification({
|
||||
const result = await updateEmailNotification({
|
||||
...input,
|
||||
organizationId: ctx.session.activeOrganizationId,
|
||||
});
|
||||
await audit(ctx, {
|
||||
action: "update",
|
||||
resourceType: "notification",
|
||||
resourceId: input.notificationId,
|
||||
resourceName: notification.name,
|
||||
});
|
||||
return result;
|
||||
} catch (error) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
@@ -296,7 +339,7 @@ export const notificationRouter = createTRPCRouter({
|
||||
});
|
||||
}
|
||||
}),
|
||||
testEmailConnection: adminProcedure
|
||||
testEmailConnection: withPermission("notification", "create")
|
||||
.input(apiTestEmailConnection)
|
||||
.mutation(async ({ input }) => {
|
||||
try {
|
||||
@@ -314,14 +357,16 @@ export const notificationRouter = createTRPCRouter({
|
||||
});
|
||||
}
|
||||
}),
|
||||
createResend: adminProcedure
|
||||
createResend: withPermission("notification", "create")
|
||||
.input(apiCreateResend)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
return await createResendNotification(
|
||||
input,
|
||||
ctx.session.activeOrganizationId,
|
||||
);
|
||||
await createResendNotification(input, ctx.session.activeOrganizationId);
|
||||
await audit(ctx, {
|
||||
action: "create",
|
||||
resourceType: "notification",
|
||||
resourceName: input.name,
|
||||
});
|
||||
} catch (error) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
@@ -330,7 +375,7 @@ export const notificationRouter = createTRPCRouter({
|
||||
});
|
||||
}
|
||||
}),
|
||||
updateResend: adminProcedure
|
||||
updateResend: withPermission("notification", "update")
|
||||
.input(apiUpdateResend)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
@@ -341,10 +386,17 @@ export const notificationRouter = createTRPCRouter({
|
||||
message: "You are not authorized to update this notification",
|
||||
});
|
||||
}
|
||||
return await updateResendNotification({
|
||||
const result = await updateResendNotification({
|
||||
...input,
|
||||
organizationId: ctx.session.activeOrganizationId,
|
||||
});
|
||||
await audit(ctx, {
|
||||
action: "update",
|
||||
resourceType: "notification",
|
||||
resourceId: input.notificationId,
|
||||
resourceName: notification.name,
|
||||
});
|
||||
return result;
|
||||
} catch (error) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
@@ -353,7 +405,7 @@ export const notificationRouter = createTRPCRouter({
|
||||
});
|
||||
}
|
||||
}),
|
||||
testResendConnection: adminProcedure
|
||||
testResendConnection: withPermission("notification", "create")
|
||||
.input(apiTestResendConnection)
|
||||
.mutation(async ({ input }) => {
|
||||
try {
|
||||
@@ -371,7 +423,7 @@ export const notificationRouter = createTRPCRouter({
|
||||
});
|
||||
}
|
||||
}),
|
||||
remove: adminProcedure
|
||||
remove: withPermission("notification", "delete")
|
||||
.input(apiFindOneNotification)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
@@ -382,6 +434,11 @@ export const notificationRouter = createTRPCRouter({
|
||||
message: "You are not authorized to delete this notification",
|
||||
});
|
||||
}
|
||||
await audit(ctx, {
|
||||
action: "delete",
|
||||
resourceType: "notification",
|
||||
resourceName: notification.name,
|
||||
});
|
||||
return await removeNotificationById(input.notificationId);
|
||||
} catch (error) {
|
||||
const message =
|
||||
@@ -394,7 +451,7 @@ export const notificationRouter = createTRPCRouter({
|
||||
});
|
||||
}
|
||||
}),
|
||||
one: protectedProcedure
|
||||
one: withPermission("notification", "read")
|
||||
.input(apiFindOneNotification)
|
||||
.query(async ({ input, ctx }) => {
|
||||
const notification = await findNotificationById(input.notificationId);
|
||||
@@ -406,7 +463,7 @@ export const notificationRouter = createTRPCRouter({
|
||||
}
|
||||
return notification;
|
||||
}),
|
||||
all: adminProcedure.query(async ({ ctx }) => {
|
||||
all: withPermission("notification", "read").query(async ({ ctx }) => {
|
||||
return await db.query.notifications.findMany({
|
||||
with: {
|
||||
slack: true,
|
||||
@@ -453,8 +510,6 @@ export const notificationRouter = createTRPCRouter({
|
||||
});
|
||||
}
|
||||
|
||||
// For Dokploy server type, we don't have a specific organizationId
|
||||
// This might need to be adjusted based on your business logic
|
||||
organizationId = "";
|
||||
ServerName = "Dokploy";
|
||||
} else {
|
||||
@@ -488,14 +543,16 @@ export const notificationRouter = createTRPCRouter({
|
||||
});
|
||||
}
|
||||
}),
|
||||
createGotify: adminProcedure
|
||||
createGotify: withPermission("notification", "create")
|
||||
.input(apiCreateGotify)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
return await createGotifyNotification(
|
||||
input,
|
||||
ctx.session.activeOrganizationId,
|
||||
);
|
||||
await createGotifyNotification(input, ctx.session.activeOrganizationId);
|
||||
await audit(ctx, {
|
||||
action: "create",
|
||||
resourceType: "notification",
|
||||
resourceName: input.name,
|
||||
});
|
||||
} catch (error) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
@@ -504,7 +561,7 @@ export const notificationRouter = createTRPCRouter({
|
||||
});
|
||||
}
|
||||
}),
|
||||
updateGotify: adminProcedure
|
||||
updateGotify: withPermission("notification", "update")
|
||||
.input(apiUpdateGotify)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
@@ -518,15 +575,22 @@ export const notificationRouter = createTRPCRouter({
|
||||
message: "You are not authorized to update this notification",
|
||||
});
|
||||
}
|
||||
return await updateGotifyNotification({
|
||||
const result = await updateGotifyNotification({
|
||||
...input,
|
||||
organizationId: ctx.session.activeOrganizationId,
|
||||
});
|
||||
await audit(ctx, {
|
||||
action: "update",
|
||||
resourceType: "notification",
|
||||
resourceId: input.notificationId,
|
||||
resourceName: notification.name,
|
||||
});
|
||||
return result;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
}),
|
||||
testGotifyConnection: adminProcedure
|
||||
testGotifyConnection: withPermission("notification", "create")
|
||||
.input(apiTestGotifyConnection)
|
||||
.mutation(async ({ input }) => {
|
||||
try {
|
||||
@@ -544,14 +608,16 @@ export const notificationRouter = createTRPCRouter({
|
||||
});
|
||||
}
|
||||
}),
|
||||
createNtfy: adminProcedure
|
||||
createNtfy: withPermission("notification", "create")
|
||||
.input(apiCreateNtfy)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
return await createNtfyNotification(
|
||||
input,
|
||||
ctx.session.activeOrganizationId,
|
||||
);
|
||||
await createNtfyNotification(input, ctx.session.activeOrganizationId);
|
||||
await audit(ctx, {
|
||||
action: "create",
|
||||
resourceType: "notification",
|
||||
resourceName: input.name,
|
||||
});
|
||||
} catch (error) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
@@ -560,7 +626,7 @@ export const notificationRouter = createTRPCRouter({
|
||||
});
|
||||
}
|
||||
}),
|
||||
updateNtfy: adminProcedure
|
||||
updateNtfy: withPermission("notification", "update")
|
||||
.input(apiUpdateNtfy)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
@@ -574,15 +640,22 @@ export const notificationRouter = createTRPCRouter({
|
||||
message: "You are not authorized to update this notification",
|
||||
});
|
||||
}
|
||||
return await updateNtfyNotification({
|
||||
const result = await updateNtfyNotification({
|
||||
...input,
|
||||
organizationId: ctx.session.activeOrganizationId,
|
||||
});
|
||||
await audit(ctx, {
|
||||
action: "update",
|
||||
resourceType: "notification",
|
||||
resourceId: input.notificationId,
|
||||
resourceName: notification.name,
|
||||
});
|
||||
return result;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
}),
|
||||
testNtfyConnection: adminProcedure
|
||||
testNtfyConnection: withPermission("notification", "create")
|
||||
.input(apiTestNtfyConnection)
|
||||
.mutation(async ({ input }) => {
|
||||
try {
|
||||
@@ -602,14 +675,16 @@ export const notificationRouter = createTRPCRouter({
|
||||
});
|
||||
}
|
||||
}),
|
||||
createCustom: adminProcedure
|
||||
createCustom: withPermission("notification", "create")
|
||||
.input(apiCreateCustom)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
return await createCustomNotification(
|
||||
input,
|
||||
ctx.session.activeOrganizationId,
|
||||
);
|
||||
await createCustomNotification(input, ctx.session.activeOrganizationId);
|
||||
await audit(ctx, {
|
||||
action: "create",
|
||||
resourceType: "notification",
|
||||
resourceName: input.name,
|
||||
});
|
||||
} catch (error) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
@@ -618,7 +693,7 @@ export const notificationRouter = createTRPCRouter({
|
||||
});
|
||||
}
|
||||
}),
|
||||
updateCustom: adminProcedure
|
||||
updateCustom: withPermission("notification", "update")
|
||||
.input(apiUpdateCustom)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
@@ -629,15 +704,22 @@ export const notificationRouter = createTRPCRouter({
|
||||
message: "You are not authorized to update this notification",
|
||||
});
|
||||
}
|
||||
return await updateCustomNotification({
|
||||
const result = await updateCustomNotification({
|
||||
...input,
|
||||
organizationId: ctx.session.activeOrganizationId,
|
||||
});
|
||||
await audit(ctx, {
|
||||
action: "update",
|
||||
resourceType: "notification",
|
||||
resourceId: input.notificationId,
|
||||
resourceName: notification.name,
|
||||
});
|
||||
return result;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
}),
|
||||
testCustomConnection: adminProcedure
|
||||
testCustomConnection: withPermission("notification", "create")
|
||||
.input(apiTestCustomConnection)
|
||||
.mutation(async ({ input }) => {
|
||||
try {
|
||||
@@ -655,14 +737,16 @@ export const notificationRouter = createTRPCRouter({
|
||||
});
|
||||
}
|
||||
}),
|
||||
createLark: adminProcedure
|
||||
createLark: withPermission("notification", "create")
|
||||
.input(apiCreateLark)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
return await createLarkNotification(
|
||||
input,
|
||||
ctx.session.activeOrganizationId,
|
||||
);
|
||||
await createLarkNotification(input, ctx.session.activeOrganizationId);
|
||||
await audit(ctx, {
|
||||
action: "create",
|
||||
resourceType: "notification",
|
||||
resourceName: input.name,
|
||||
});
|
||||
} catch (error) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
@@ -671,7 +755,7 @@ export const notificationRouter = createTRPCRouter({
|
||||
});
|
||||
}
|
||||
}),
|
||||
updateLark: adminProcedure
|
||||
updateLark: withPermission("notification", "update")
|
||||
.input(apiUpdateLark)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
@@ -685,15 +769,22 @@ export const notificationRouter = createTRPCRouter({
|
||||
message: "You are not authorized to update this notification",
|
||||
});
|
||||
}
|
||||
return await updateLarkNotification({
|
||||
const result = await updateLarkNotification({
|
||||
...input,
|
||||
organizationId: ctx.session.activeOrganizationId,
|
||||
});
|
||||
await audit(ctx, {
|
||||
action: "update",
|
||||
resourceType: "notification",
|
||||
resourceId: input.notificationId,
|
||||
resourceName: notification.name,
|
||||
});
|
||||
return result;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
}),
|
||||
testLarkConnection: adminProcedure
|
||||
testLarkConnection: withPermission("notification", "create")
|
||||
.input(apiTestLarkConnection)
|
||||
.mutation(async ({ input }) => {
|
||||
try {
|
||||
@@ -712,14 +803,16 @@ export const notificationRouter = createTRPCRouter({
|
||||
});
|
||||
}
|
||||
}),
|
||||
createTeams: adminProcedure
|
||||
createTeams: withPermission("notification", "create")
|
||||
.input(apiCreateTeams)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
return await createTeamsNotification(
|
||||
input,
|
||||
ctx.session.activeOrganizationId,
|
||||
);
|
||||
await createTeamsNotification(input, ctx.session.activeOrganizationId);
|
||||
await audit(ctx, {
|
||||
action: "create",
|
||||
resourceType: "notification",
|
||||
resourceName: input.name,
|
||||
});
|
||||
} catch (error) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
@@ -728,7 +821,7 @@ export const notificationRouter = createTRPCRouter({
|
||||
});
|
||||
}
|
||||
}),
|
||||
updateTeams: adminProcedure
|
||||
updateTeams: withPermission("notification", "update")
|
||||
.input(apiUpdateTeams)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
@@ -742,15 +835,22 @@ export const notificationRouter = createTRPCRouter({
|
||||
message: "You are not authorized to update this notification",
|
||||
});
|
||||
}
|
||||
return await updateTeamsNotification({
|
||||
const result = await updateTeamsNotification({
|
||||
...input,
|
||||
organizationId: ctx.session.activeOrganizationId,
|
||||
});
|
||||
await audit(ctx, {
|
||||
action: "update",
|
||||
resourceType: "notification",
|
||||
resourceId: input.notificationId,
|
||||
resourceName: notification.name,
|
||||
});
|
||||
return result;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
}),
|
||||
testTeamsConnection: adminProcedure
|
||||
testTeamsConnection: withPermission("notification", "create")
|
||||
.input(apiTestTeamsConnection)
|
||||
.mutation(async ({ input }) => {
|
||||
try {
|
||||
@@ -767,14 +867,19 @@ export const notificationRouter = createTRPCRouter({
|
||||
});
|
||||
}
|
||||
}),
|
||||
createPushover: adminProcedure
|
||||
createPushover: withPermission("notification", "create")
|
||||
.input(apiCreatePushover)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
return await createPushoverNotification(
|
||||
await createPushoverNotification(
|
||||
input,
|
||||
ctx.session.activeOrganizationId,
|
||||
);
|
||||
await audit(ctx, {
|
||||
action: "create",
|
||||
resourceType: "notification",
|
||||
resourceName: input.name,
|
||||
});
|
||||
} catch (error) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
@@ -783,7 +888,7 @@ export const notificationRouter = createTRPCRouter({
|
||||
});
|
||||
}
|
||||
}),
|
||||
updatePushover: adminProcedure
|
||||
updatePushover: withPermission("notification", "update")
|
||||
.input(apiUpdatePushover)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
@@ -797,15 +902,22 @@ export const notificationRouter = createTRPCRouter({
|
||||
message: "You are not authorized to update this notification",
|
||||
});
|
||||
}
|
||||
return await updatePushoverNotification({
|
||||
const result = await updatePushoverNotification({
|
||||
...input,
|
||||
organizationId: ctx.session.activeOrganizationId,
|
||||
});
|
||||
await audit(ctx, {
|
||||
action: "update",
|
||||
resourceType: "notification",
|
||||
resourceId: input.notificationId,
|
||||
resourceName: notification.name,
|
||||
});
|
||||
return result;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
}),
|
||||
testPushoverConnection: adminProcedure
|
||||
testPushoverConnection: withPermission("notification", "create")
|
||||
.input(apiTestPushoverConnection)
|
||||
.mutation(async ({ input }) => {
|
||||
try {
|
||||
@@ -823,13 +935,18 @@ export const notificationRouter = createTRPCRouter({
|
||||
});
|
||||
}
|
||||
}),
|
||||
getEmailProviders: adminProcedure.query(async ({ ctx }) => {
|
||||
return await db.query.notifications.findMany({
|
||||
where: eq(notifications.organizationId, ctx.session.activeOrganizationId),
|
||||
with: {
|
||||
email: true,
|
||||
resend: true,
|
||||
},
|
||||
});
|
||||
}),
|
||||
getEmailProviders: withPermission("notification", "read").query(
|
||||
async ({ ctx }) => {
|
||||
return await db.query.notifications.findMany({
|
||||
where: eq(
|
||||
notifications.organizationId,
|
||||
ctx.session.activeOrganizationId,
|
||||
),
|
||||
with: {
|
||||
email: true,
|
||||
resend: true,
|
||||
},
|
||||
});
|
||||
},
|
||||
),
|
||||
});
|
||||
|
||||
@@ -1,11 +1,18 @@
|
||||
import { db } from "@dokploy/server/db";
|
||||
import { IS_CLOUD } from "@dokploy/server/index";
|
||||
import { audit } from "@/server/api/utils/audit";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { and, desc, eq, exists } from "drizzle-orm";
|
||||
import { nanoid } from "nanoid";
|
||||
import { z } from "zod";
|
||||
import { invitation, member, organization } from "@/server/db/schema";
|
||||
import { adminProcedure, createTRPCRouter, protectedProcedure } from "../trpc";
|
||||
import {
|
||||
invitation,
|
||||
member,
|
||||
organization,
|
||||
organizationRole,
|
||||
user,
|
||||
} from "@/server/db/schema";
|
||||
import { createTRPCRouter, protectedProcedure, withPermission } from "../trpc";
|
||||
export const organizationRouter = createTRPCRouter({
|
||||
create: protectedProcedure
|
||||
.input(
|
||||
@@ -50,6 +57,12 @@ export const organizationRouter = createTRPCRouter({
|
||||
createdAt: new Date(),
|
||||
userId: ctx.user.id,
|
||||
});
|
||||
await audit(ctx, {
|
||||
action: "create",
|
||||
resourceType: "organization",
|
||||
resourceId: result.id,
|
||||
resourceName: result.name,
|
||||
});
|
||||
return result;
|
||||
}),
|
||||
all: protectedProcedure.query(async ({ ctx }) => {
|
||||
@@ -156,6 +169,12 @@ export const organizationRouter = createTRPCRouter({
|
||||
})
|
||||
.where(eq(organization.id, input.organizationId))
|
||||
.returning();
|
||||
await audit(ctx, {
|
||||
action: "update",
|
||||
resourceType: "organization",
|
||||
resourceId: input.organizationId,
|
||||
resourceName: input.name,
|
||||
});
|
||||
return result[0];
|
||||
}),
|
||||
delete: protectedProcedure
|
||||
@@ -220,15 +239,109 @@ export const organizationRouter = createTRPCRouter({
|
||||
.delete(organization)
|
||||
.where(eq(organization.id, input.organizationId));
|
||||
|
||||
await audit(ctx, {
|
||||
action: "delete",
|
||||
resourceType: "organization",
|
||||
resourceId: input.organizationId,
|
||||
resourceName: org.name,
|
||||
});
|
||||
return result;
|
||||
}),
|
||||
allInvitations: adminProcedure.query(async ({ ctx }) => {
|
||||
inviteMember: withPermission("member", "create")
|
||||
.input(
|
||||
z.object({
|
||||
email: z.string().email(),
|
||||
role: z.string().min(1),
|
||||
}),
|
||||
)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const orgId = ctx.session.activeOrganizationId;
|
||||
const email = input.email.toLowerCase();
|
||||
|
||||
// Check if user is already a member
|
||||
const existingUser = await db.query.user.findFirst({
|
||||
where: eq(user.email, email),
|
||||
});
|
||||
|
||||
if (existingUser) {
|
||||
const existingMember = await db.query.member.findFirst({
|
||||
where: and(
|
||||
eq(member.organizationId, orgId),
|
||||
eq(member.userId, existingUser.id),
|
||||
),
|
||||
});
|
||||
|
||||
if (existingMember) {
|
||||
throw new TRPCError({
|
||||
code: "CONFLICT",
|
||||
message: "User is already a member of this organization",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Check for pending invitation
|
||||
const existingInvitation = await db.query.invitation.findFirst({
|
||||
where: and(
|
||||
eq(invitation.organizationId, orgId),
|
||||
eq(invitation.email, email),
|
||||
eq(invitation.status, "pending"),
|
||||
),
|
||||
});
|
||||
|
||||
if (existingInvitation) {
|
||||
throw new TRPCError({
|
||||
code: "CONFLICT",
|
||||
message: "An invitation has already been sent to this email",
|
||||
});
|
||||
}
|
||||
|
||||
// If assigning a custom role, verify it exists
|
||||
if (!["owner", "admin", "member"].includes(input.role)) {
|
||||
const customRole = await db.query.organizationRole.findFirst({
|
||||
where: and(
|
||||
eq(organizationRole.organizationId, orgId),
|
||||
eq(organizationRole.role, input.role),
|
||||
),
|
||||
});
|
||||
|
||||
if (!customRole) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: `Role "${input.role}" not found`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const [created] = await db
|
||||
.insert(invitation)
|
||||
.values({
|
||||
id: nanoid(),
|
||||
organizationId: orgId,
|
||||
email,
|
||||
role: input.role as any,
|
||||
status: "pending",
|
||||
expiresAt: new Date(Date.now() + 48 * 60 * 60 * 1000),
|
||||
inviterId: ctx.user.id,
|
||||
})
|
||||
.returning();
|
||||
|
||||
await audit(ctx, {
|
||||
action: "create",
|
||||
resourceType: "organization",
|
||||
resourceId: created?.id,
|
||||
resourceName: email,
|
||||
metadata: { type: "inviteMember", role: input.role },
|
||||
});
|
||||
return created;
|
||||
}),
|
||||
|
||||
allInvitations: withPermission("member", "create").query(async ({ ctx }) => {
|
||||
return await db.query.invitation.findMany({
|
||||
where: eq(invitation.organizationId, ctx.session.activeOrganizationId),
|
||||
orderBy: [desc(invitation.status), desc(invitation.expiresAt)],
|
||||
});
|
||||
}),
|
||||
removeInvitation: adminProcedure
|
||||
removeInvitation: withPermission("member", "create")
|
||||
.input(z.object({ invitationId: z.string() }))
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const invitationResult = await db.query.invitation.findFirst({
|
||||
@@ -251,15 +364,23 @@ export const organizationRouter = createTRPCRouter({
|
||||
});
|
||||
}
|
||||
|
||||
return await db
|
||||
const result = await db
|
||||
.delete(invitation)
|
||||
.where(eq(invitation.id, input.invitationId));
|
||||
await audit(ctx, {
|
||||
action: "delete",
|
||||
resourceType: "organization",
|
||||
resourceId: input.invitationId,
|
||||
resourceName: invitationResult.email,
|
||||
metadata: { type: "removeInvitation" },
|
||||
});
|
||||
return result;
|
||||
}),
|
||||
updateMemberRole: adminProcedure
|
||||
updateMemberRole: withPermission("member", "update")
|
||||
.input(
|
||||
z.object({
|
||||
memberId: z.string(),
|
||||
role: z.enum(["admin", "member"]),
|
||||
role: z.string().min(1),
|
||||
}),
|
||||
)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
@@ -289,7 +410,7 @@ export const organizationRouter = createTRPCRouter({
|
||||
}
|
||||
|
||||
// Owner role is intransferible - cannot change to or from owner
|
||||
if (target.role === "owner") {
|
||||
if (target.role === "owner" || input.role === "owner") {
|
||||
throw new TRPCError({
|
||||
code: "FORBIDDEN",
|
||||
message: "The owner role is intransferible",
|
||||
@@ -306,12 +427,39 @@ export const organizationRouter = createTRPCRouter({
|
||||
});
|
||||
}
|
||||
|
||||
// If assigning a custom role (not admin/member), verify it exists
|
||||
if (input.role !== "admin" && input.role !== "member") {
|
||||
const customRole = await db.query.organizationRole.findFirst({
|
||||
where: and(
|
||||
eq(
|
||||
organizationRole.organizationId,
|
||||
ctx.session.activeOrganizationId,
|
||||
),
|
||||
eq(organizationRole.role, input.role),
|
||||
),
|
||||
});
|
||||
|
||||
if (!customRole) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: `Custom role "${input.role}" not found`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Update the target member's role
|
||||
await db
|
||||
.update(member)
|
||||
.set({ role: input.role })
|
||||
.where(eq(member.id, input.memberId));
|
||||
|
||||
await audit(ctx, {
|
||||
action: "update",
|
||||
resourceType: "user",
|
||||
resourceId: target.userId,
|
||||
resourceName: target.user.email,
|
||||
metadata: { before: target.role, after: input.role },
|
||||
});
|
||||
return true;
|
||||
}),
|
||||
setDefault: protectedProcedure
|
||||
@@ -353,6 +501,12 @@ export const organizationRouter = createTRPCRouter({
|
||||
),
|
||||
);
|
||||
|
||||
await audit(ctx, {
|
||||
action: "update",
|
||||
resourceType: "organization",
|
||||
resourceId: input.organizationId,
|
||||
metadata: { type: "setDefault" },
|
||||
});
|
||||
return { success: true };
|
||||
}),
|
||||
active: protectedProcedure.query(async ({ ctx }) => {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import {
|
||||
checkServiceAccess,
|
||||
cleanPatchRepos,
|
||||
createPatch,
|
||||
deletePatch,
|
||||
@@ -14,6 +13,7 @@ import {
|
||||
readPatchRepoFile,
|
||||
updatePatch,
|
||||
} from "@dokploy/server";
|
||||
import { checkServicePermissionAndAccess } from "@dokploy/server/services/permission";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { z } from "zod";
|
||||
import {
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
createTRPCRouter,
|
||||
protectedProcedure,
|
||||
} from "@/server/api/trpc";
|
||||
import { audit } from "@/server/api/utils/audit";
|
||||
import {
|
||||
apiCreatePatch,
|
||||
apiDeletePatch,
|
||||
@@ -29,47 +30,56 @@ import {
|
||||
apiUpdatePatch,
|
||||
} from "@/server/db/schema";
|
||||
|
||||
/**
|
||||
* Resolves the serviceId from a patch record (applicationId or composeId).
|
||||
* Throws if neither is set.
|
||||
*/
|
||||
const resolvePatchServiceId = (patch: {
|
||||
applicationId: string | null;
|
||||
composeId: string | null;
|
||||
}): string => {
|
||||
const serviceId = patch.applicationId ?? patch.composeId;
|
||||
if (!serviceId) {
|
||||
throw new TRPCError({
|
||||
code: "INTERNAL_SERVER_ERROR",
|
||||
message: "Patch has no associated service",
|
||||
});
|
||||
}
|
||||
return serviceId;
|
||||
};
|
||||
|
||||
export const patchRouter = createTRPCRouter({
|
||||
create: protectedProcedure
|
||||
.input(apiCreatePatch)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
if (input.applicationId) {
|
||||
const app = await findApplicationById(input.applicationId);
|
||||
if (
|
||||
app.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to access this application",
|
||||
});
|
||||
}
|
||||
if (ctx.user.role === "member") {
|
||||
await checkServiceAccess(
|
||||
ctx.user.id,
|
||||
input.applicationId,
|
||||
ctx.session.activeOrganizationId,
|
||||
"access",
|
||||
);
|
||||
}
|
||||
} else if (input.composeId) {
|
||||
const compose = await findComposeById(input.composeId);
|
||||
if (
|
||||
compose.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to access this compose",
|
||||
});
|
||||
}
|
||||
const serviceId = input.applicationId ?? input.composeId;
|
||||
if (!serviceId) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Either applicationId or composeId must be provided",
|
||||
});
|
||||
}
|
||||
|
||||
return await createPatch(input);
|
||||
await checkServicePermissionAndAccess(ctx, serviceId, {
|
||||
service: ["create"],
|
||||
});
|
||||
const result = await createPatch(input);
|
||||
await audit(ctx, {
|
||||
action: "create",
|
||||
resourceType: "settings",
|
||||
resourceId: result.patchId,
|
||||
resourceName: result.filePath,
|
||||
metadata: { type: "patch" },
|
||||
});
|
||||
return result;
|
||||
}),
|
||||
|
||||
one: protectedProcedure.input(apiFindPatch).query(async ({ input }) => {
|
||||
return await findPatchById(input.patchId);
|
||||
one: protectedProcedure.input(apiFindPatch).query(async ({ input, ctx }) => {
|
||||
const patch = await findPatchById(input.patchId);
|
||||
const serviceId = resolvePatchServiceId(patch);
|
||||
await checkServicePermissionAndAccess(ctx, serviceId, {
|
||||
service: ["read"],
|
||||
});
|
||||
return patch;
|
||||
}),
|
||||
|
||||
byEntityId: protectedProcedure
|
||||
@@ -77,51 +87,70 @@ export const patchRouter = createTRPCRouter({
|
||||
z.object({ id: z.string(), type: z.enum(["application", "compose"]) }),
|
||||
)
|
||||
.query(async ({ input, ctx }) => {
|
||||
if (input.type === "application") {
|
||||
const app = await findApplicationById(input.id);
|
||||
if (
|
||||
app.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to access this application",
|
||||
});
|
||||
}
|
||||
} else if (input.type === "compose") {
|
||||
const compose = await findComposeById(input.id);
|
||||
if (
|
||||
compose.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to access this compose",
|
||||
});
|
||||
}
|
||||
}
|
||||
const result = await findPatchesByEntityId(input.id, input.type);
|
||||
|
||||
return result;
|
||||
await checkServicePermissionAndAccess(ctx, input.id, {
|
||||
service: ["read"],
|
||||
});
|
||||
return await findPatchesByEntityId(input.id, input.type);
|
||||
}),
|
||||
|
||||
update: protectedProcedure
|
||||
.input(apiUpdatePatch)
|
||||
.mutation(async ({ input }) => {
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const patch = await findPatchById(input.patchId);
|
||||
const serviceId = resolvePatchServiceId(patch);
|
||||
await checkServicePermissionAndAccess(ctx, serviceId, {
|
||||
service: ["create"],
|
||||
});
|
||||
const { patchId, ...data } = input;
|
||||
return await updatePatch(patchId, data);
|
||||
const result = await updatePatch(patchId, data);
|
||||
await audit(ctx, {
|
||||
action: "update",
|
||||
resourceType: "settings",
|
||||
resourceId: patchId,
|
||||
resourceName: patch.filePath,
|
||||
metadata: { type: "patch" },
|
||||
});
|
||||
return result;
|
||||
}),
|
||||
|
||||
delete: protectedProcedure
|
||||
.input(apiDeletePatch)
|
||||
.mutation(async ({ input }) => {
|
||||
return await deletePatch(input.patchId);
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const patch = await findPatchById(input.patchId);
|
||||
const serviceId = resolvePatchServiceId(patch);
|
||||
await checkServicePermissionAndAccess(ctx, serviceId, {
|
||||
service: ["delete"],
|
||||
});
|
||||
const result = await deletePatch(input.patchId);
|
||||
await audit(ctx, {
|
||||
action: "delete",
|
||||
resourceType: "settings",
|
||||
resourceId: input.patchId,
|
||||
resourceName: patch.filePath,
|
||||
metadata: { type: "patch" },
|
||||
});
|
||||
return result;
|
||||
}),
|
||||
|
||||
toggleEnabled: protectedProcedure
|
||||
.input(apiTogglePatchEnabled)
|
||||
.mutation(async ({ input }) => {
|
||||
return await updatePatch(input.patchId, { enabled: input.enabled });
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const patch = await findPatchById(input.patchId);
|
||||
const serviceId = resolvePatchServiceId(patch);
|
||||
await checkServicePermissionAndAccess(ctx, serviceId, {
|
||||
service: ["create"],
|
||||
});
|
||||
const result = await updatePatch(input.patchId, {
|
||||
enabled: input.enabled,
|
||||
});
|
||||
await audit(ctx, {
|
||||
action: "update",
|
||||
resourceType: "settings",
|
||||
resourceId: input.patchId,
|
||||
resourceName: patch.filePath,
|
||||
metadata: { type: "patch", enabled: input.enabled },
|
||||
});
|
||||
return result;
|
||||
}),
|
||||
|
||||
// Repository Operations
|
||||
@@ -132,11 +161,21 @@ export const patchRouter = createTRPCRouter({
|
||||
type: z.enum(["application", "compose"]),
|
||||
}),
|
||||
)
|
||||
.mutation(async ({ input }) => {
|
||||
return await ensurePatchRepo({
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.id, {
|
||||
service: ["create"],
|
||||
});
|
||||
const result = await ensurePatchRepo({
|
||||
type: input.type,
|
||||
id: input.id,
|
||||
});
|
||||
await audit(ctx, {
|
||||
action: "create",
|
||||
resourceType: "settings",
|
||||
resourceId: input.id,
|
||||
metadata: { type: "ensurePatchRepo", serviceType: input.type },
|
||||
});
|
||||
return result;
|
||||
}),
|
||||
|
||||
readRepoDirectories: protectedProcedure
|
||||
@@ -148,36 +187,17 @@ export const patchRouter = createTRPCRouter({
|
||||
}),
|
||||
)
|
||||
.query(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.id, {
|
||||
service: ["read"],
|
||||
});
|
||||
let serverId: string | null = null;
|
||||
|
||||
if (input.type === "application") {
|
||||
const app = await findApplicationById(input.id);
|
||||
if (
|
||||
app.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to access this application",
|
||||
});
|
||||
}
|
||||
serverId = app.serverId;
|
||||
}
|
||||
|
||||
if (input.type === "compose") {
|
||||
} else {
|
||||
const compose = await findComposeById(input.id);
|
||||
if (
|
||||
compose.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to access this compose",
|
||||
});
|
||||
}
|
||||
serverId = compose.serverId;
|
||||
}
|
||||
|
||||
return await readPatchRepoDirectory(input.repoPath, serverId);
|
||||
}),
|
||||
|
||||
@@ -190,44 +210,22 @@ export const patchRouter = createTRPCRouter({
|
||||
}),
|
||||
)
|
||||
.query(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.id, {
|
||||
service: ["read"],
|
||||
});
|
||||
let serverId: string | null = null;
|
||||
|
||||
if (input.type === "application") {
|
||||
const app = await findApplicationById(input.id);
|
||||
if (
|
||||
app.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to access this application",
|
||||
});
|
||||
}
|
||||
serverId = app.serverId;
|
||||
} else if (input.type === "compose") {
|
||||
const compose = await findComposeById(input.id);
|
||||
if (
|
||||
compose.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to access this compose",
|
||||
});
|
||||
}
|
||||
serverId = compose.serverId;
|
||||
} else {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Either applicationId or composeId must be provided",
|
||||
});
|
||||
const compose = await findComposeById(input.id);
|
||||
serverId = compose.serverId;
|
||||
}
|
||||
const existingPatch = await findPatchByFilePath(
|
||||
input.filePath,
|
||||
input.id,
|
||||
input.type,
|
||||
);
|
||||
|
||||
// For delete patches, show current file content from repo (what will be deleted)
|
||||
if (existingPatch?.type === "delete") {
|
||||
try {
|
||||
@@ -253,55 +251,43 @@ export const patchRouter = createTRPCRouter({
|
||||
}),
|
||||
)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
if (input.type === "application") {
|
||||
const app = await findApplicationById(input.id);
|
||||
if (
|
||||
app.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to access this application",
|
||||
});
|
||||
}
|
||||
} else if (input.type === "compose") {
|
||||
const compose = await findComposeById(input.id);
|
||||
if (
|
||||
compose.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to access this compose",
|
||||
});
|
||||
}
|
||||
} else {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Either application or compose must be provided",
|
||||
});
|
||||
}
|
||||
|
||||
await checkServicePermissionAndAccess(ctx, input.id, {
|
||||
service: ["create"],
|
||||
});
|
||||
const existingPatch = await findPatchByFilePath(
|
||||
input.filePath,
|
||||
input.id,
|
||||
input.type,
|
||||
);
|
||||
|
||||
if (!existingPatch) {
|
||||
return await createPatch({
|
||||
const result = await createPatch({
|
||||
filePath: input.filePath,
|
||||
content: input.content,
|
||||
type: input.patchType,
|
||||
applicationId: input.type === "application" ? input.id : undefined,
|
||||
composeId: input.type === "compose" ? input.id : undefined,
|
||||
});
|
||||
await audit(ctx, {
|
||||
action: "create",
|
||||
resourceType: "settings",
|
||||
resourceId: result.patchId,
|
||||
resourceName: input.filePath,
|
||||
metadata: { type: "saveFileAsPatch" },
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
return await updatePatch(existingPatch.patchId, {
|
||||
const result = await updatePatch(existingPatch.patchId, {
|
||||
content: input.content,
|
||||
type: input.patchType,
|
||||
});
|
||||
await audit(ctx, {
|
||||
action: "update",
|
||||
resourceType: "settings",
|
||||
resourceId: existingPatch.patchId,
|
||||
resourceName: input.filePath,
|
||||
metadata: { type: "saveFileAsPatch" },
|
||||
});
|
||||
return result;
|
||||
}),
|
||||
|
||||
markFileForDeletion: protectedProcedure
|
||||
@@ -313,36 +299,34 @@ export const patchRouter = createTRPCRouter({
|
||||
}),
|
||||
)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
if (input.type === "application") {
|
||||
const app = await findApplicationById(input.id);
|
||||
if (
|
||||
app.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to access this application",
|
||||
});
|
||||
}
|
||||
} else if (input.type === "compose") {
|
||||
const compose = await findComposeById(input.id);
|
||||
if (
|
||||
compose.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to access this compose",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return await markPatchForDeletion(input.filePath, input.id, input.type);
|
||||
await checkServicePermissionAndAccess(ctx, input.id, {
|
||||
service: ["create"],
|
||||
});
|
||||
const result = await markPatchForDeletion(
|
||||
input.filePath,
|
||||
input.id,
|
||||
input.type,
|
||||
);
|
||||
await audit(ctx, {
|
||||
action: "delete",
|
||||
resourceType: "settings",
|
||||
resourceId: input.id,
|
||||
resourceName: input.filePath,
|
||||
metadata: { type: "markFileForDeletion" },
|
||||
});
|
||||
return result;
|
||||
}),
|
||||
|
||||
cleanPatchRepos: adminProcedure
|
||||
.input(z.object({ serverId: z.string().optional() }))
|
||||
.mutation(async ({ input }) => {
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await cleanPatchRepos(input.serverId);
|
||||
await audit(ctx, {
|
||||
action: "delete",
|
||||
resourceType: "settings",
|
||||
resourceId: input.serverId || "local",
|
||||
metadata: { type: "cleanPatchRepos" },
|
||||
});
|
||||
return true;
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -4,8 +4,10 @@ import {
|
||||
removePortById,
|
||||
updatePortById,
|
||||
} from "@dokploy/server";
|
||||
import { checkServicePermissionAndAccess } from "@dokploy/server/services/permission";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc";
|
||||
import { audit } from "@/server/api/utils/audit";
|
||||
import {
|
||||
apiCreatePort,
|
||||
apiFindOnePort,
|
||||
@@ -15,10 +17,19 @@ import {
|
||||
export const portRouter = createTRPCRouter({
|
||||
create: protectedProcedure
|
||||
.input(apiCreatePort)
|
||||
.mutation(async ({ input }) => {
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
await createPort(input);
|
||||
return true;
|
||||
await checkServicePermissionAndAccess(ctx, input.applicationId, {
|
||||
service: ["create"],
|
||||
});
|
||||
const port = await createPort(input);
|
||||
await audit(ctx, {
|
||||
action: "create",
|
||||
resourceType: "port",
|
||||
resourceId: port.portId,
|
||||
resourceName: `${port.publishedPort}:${port.targetPort}`,
|
||||
});
|
||||
return port;
|
||||
} catch (error) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
@@ -32,15 +43,11 @@ export const portRouter = createTRPCRouter({
|
||||
.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",
|
||||
});
|
||||
}
|
||||
await checkServicePermissionAndAccess(
|
||||
ctx,
|
||||
port.application.applicationId,
|
||||
{ service: ["read"] },
|
||||
);
|
||||
return port;
|
||||
} catch (error) {
|
||||
throw new TRPCError({
|
||||
@@ -54,17 +61,20 @@ export const portRouter = createTRPCRouter({
|
||||
.input(apiFindOnePort)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const port = await finPortById(input.portId);
|
||||
if (
|
||||
port.application.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to delete this port",
|
||||
});
|
||||
}
|
||||
await checkServicePermissionAndAccess(
|
||||
ctx,
|
||||
port.application.applicationId,
|
||||
{ service: ["delete"] },
|
||||
);
|
||||
try {
|
||||
return await removePortById(input.portId);
|
||||
const result = await removePortById(input.portId);
|
||||
await audit(ctx, {
|
||||
action: "delete",
|
||||
resourceType: "port",
|
||||
resourceId: port.portId,
|
||||
resourceName: `${port.publishedPort}:${port.targetPort}`,
|
||||
});
|
||||
return result;
|
||||
} catch (error) {
|
||||
const message =
|
||||
error instanceof Error ? error.message : "Error input: Deleting port";
|
||||
@@ -78,17 +88,20 @@ export const portRouter = createTRPCRouter({
|
||||
.input(apiUpdatePort)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const port = await finPortById(input.portId);
|
||||
if (
|
||||
port.application.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to update this port",
|
||||
});
|
||||
}
|
||||
await checkServicePermissionAndAccess(
|
||||
ctx,
|
||||
port.application.applicationId,
|
||||
{ service: ["create"] },
|
||||
);
|
||||
try {
|
||||
return await updatePortById(input.portId, input);
|
||||
const result = await updatePortById(input.portId, input);
|
||||
await audit(ctx, {
|
||||
action: "update",
|
||||
resourceType: "port",
|
||||
resourceId: port.portId,
|
||||
resourceName: `${port.publishedPort}:${port.targetPort}`,
|
||||
});
|
||||
return result;
|
||||
} catch (error) {
|
||||
const message =
|
||||
error instanceof Error ? error.message : "Error updating the port";
|
||||
|
||||
@@ -1,13 +1,10 @@
|
||||
import {
|
||||
addNewService,
|
||||
checkPortInUse,
|
||||
checkServiceAccess,
|
||||
createMount,
|
||||
createPostgres,
|
||||
deployPostgres,
|
||||
findBackupsByDbId,
|
||||
findEnvironmentById,
|
||||
findMemberById,
|
||||
findPostgresById,
|
||||
findProjectById,
|
||||
getMountPath,
|
||||
@@ -21,10 +18,17 @@ import {
|
||||
stopServiceRemote,
|
||||
updatePostgresById,
|
||||
} from "@dokploy/server";
|
||||
import {
|
||||
addNewService,
|
||||
checkServiceAccess,
|
||||
checkServicePermissionAndAccess,
|
||||
findMemberByUserId,
|
||||
} from "@dokploy/server/services/permission";
|
||||
import { db } from "@dokploy/server/db";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { and, desc, eq, ilike, or, sql } from "drizzle-orm";
|
||||
import { z } from "zod";
|
||||
import { audit } from "@/server/api/utils/audit";
|
||||
import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc";
|
||||
import {
|
||||
apiChangePostgresStatus,
|
||||
@@ -46,18 +50,10 @@ export const postgresRouter = createTRPCRouter({
|
||||
.input(apiCreatePostgres)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
// Get project from environment
|
||||
const environment = await findEnvironmentById(input.environmentId);
|
||||
const project = await findProjectById(environment.projectId);
|
||||
|
||||
if (ctx.user.role === "member") {
|
||||
await checkServiceAccess(
|
||||
ctx.user.id,
|
||||
project.projectId,
|
||||
ctx.session.activeOrganizationId,
|
||||
"create",
|
||||
);
|
||||
}
|
||||
await checkServiceAccess(ctx, project.projectId, "create");
|
||||
|
||||
if (IS_CLOUD && !input.serverId) {
|
||||
throw new TRPCError({
|
||||
@@ -75,13 +71,7 @@ export const postgresRouter = createTRPCRouter({
|
||||
const newPostgres = await createPostgres({
|
||||
...input,
|
||||
});
|
||||
if (ctx.user.role === "member") {
|
||||
await addNewService(
|
||||
ctx.user.id,
|
||||
newPostgres.postgresId,
|
||||
project.organizationId,
|
||||
);
|
||||
}
|
||||
await addNewService(ctx, newPostgres.postgresId);
|
||||
|
||||
const mountPath = getMountPath(input.dockerImage);
|
||||
|
||||
@@ -93,6 +83,12 @@ export const postgresRouter = createTRPCRouter({
|
||||
type: "volume",
|
||||
});
|
||||
|
||||
await audit(ctx, {
|
||||
action: "create",
|
||||
resourceType: "service",
|
||||
resourceId: newPostgres.postgresId,
|
||||
resourceName: newPostgres.appName,
|
||||
});
|
||||
return newPostgres;
|
||||
} catch (error) {
|
||||
if (error instanceof TRPCError) {
|
||||
@@ -108,14 +104,7 @@ export const postgresRouter = createTRPCRouter({
|
||||
one: protectedProcedure
|
||||
.input(apiFindOnePostgres)
|
||||
.query(async ({ input, ctx }) => {
|
||||
if (ctx.user.role === "member") {
|
||||
await checkServiceAccess(
|
||||
ctx.user.id,
|
||||
input.postgresId,
|
||||
ctx.session.activeOrganizationId,
|
||||
"access",
|
||||
);
|
||||
}
|
||||
await checkServiceAccess(ctx, input.postgresId, "read");
|
||||
|
||||
const postgres = await findPostgresById(input.postgresId);
|
||||
if (
|
||||
@@ -133,18 +122,11 @@ export const postgresRouter = createTRPCRouter({
|
||||
start: protectedProcedure
|
||||
.input(apiFindOnePostgres)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.postgresId, {
|
||||
deployment: ["create"],
|
||||
});
|
||||
const service = await findPostgresById(input.postgresId);
|
||||
|
||||
if (
|
||||
service.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to start this Postgres",
|
||||
});
|
||||
}
|
||||
|
||||
if (service.serverId) {
|
||||
await startServiceRemote(service.serverId, service.appName);
|
||||
} else {
|
||||
@@ -154,21 +136,21 @@ export const postgresRouter = createTRPCRouter({
|
||||
applicationStatus: "done",
|
||||
});
|
||||
|
||||
await audit(ctx, {
|
||||
action: "start",
|
||||
resourceType: "service",
|
||||
resourceId: service.postgresId,
|
||||
resourceName: service.appName,
|
||||
});
|
||||
return service;
|
||||
}),
|
||||
stop: protectedProcedure
|
||||
.input(apiFindOnePostgres)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.postgresId, {
|
||||
deployment: ["create"],
|
||||
});
|
||||
const postgres = await findPostgresById(input.postgresId);
|
||||
if (
|
||||
postgres.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to stop this Postgres",
|
||||
});
|
||||
}
|
||||
if (postgres.serverId) {
|
||||
await stopServiceRemote(postgres.serverId, postgres.appName);
|
||||
} else {
|
||||
@@ -178,23 +160,22 @@ export const postgresRouter = createTRPCRouter({
|
||||
applicationStatus: "idle",
|
||||
});
|
||||
|
||||
await audit(ctx, {
|
||||
action: "stop",
|
||||
resourceType: "service",
|
||||
resourceId: postgres.postgresId,
|
||||
resourceName: postgres.appName,
|
||||
});
|
||||
return postgres;
|
||||
}),
|
||||
saveExternalPort: protectedProcedure
|
||||
.input(apiSaveExternalPortPostgres)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.postgresId, {
|
||||
service: ["create"],
|
||||
});
|
||||
const postgres = await findPostgresById(input.postgresId);
|
||||
|
||||
if (
|
||||
postgres.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to save this external port",
|
||||
});
|
||||
}
|
||||
|
||||
if (input.externalPort) {
|
||||
const portCheck = await checkPortInUse(
|
||||
input.externalPort,
|
||||
@@ -212,21 +193,27 @@ export const postgresRouter = createTRPCRouter({
|
||||
externalPort: input.externalPort,
|
||||
});
|
||||
await deployPostgres(input.postgresId);
|
||||
await audit(ctx, {
|
||||
action: "update",
|
||||
resourceType: "service",
|
||||
resourceId: postgres.postgresId,
|
||||
resourceName: postgres.appName,
|
||||
});
|
||||
return postgres;
|
||||
}),
|
||||
deploy: protectedProcedure
|
||||
.input(apiDeployPostgres)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.postgresId, {
|
||||
deployment: ["create"],
|
||||
});
|
||||
const postgres = await findPostgresById(input.postgresId);
|
||||
if (
|
||||
postgres.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to deploy this Postgres",
|
||||
});
|
||||
}
|
||||
await audit(ctx, {
|
||||
action: "deploy",
|
||||
resourceType: "service",
|
||||
resourceId: postgres.postgresId,
|
||||
resourceName: postgres.appName,
|
||||
});
|
||||
return deployPostgres(input.postgresId);
|
||||
}),
|
||||
|
||||
@@ -241,17 +228,9 @@ export const postgresRouter = createTRPCRouter({
|
||||
})
|
||||
.input(apiDeployPostgres)
|
||||
.subscription(async function* ({ input, ctx, signal }) {
|
||||
const postgres = await findPostgresById(input.postgresId);
|
||||
|
||||
if (
|
||||
postgres.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to deploy this Postgres",
|
||||
});
|
||||
}
|
||||
await checkServicePermissionAndAccess(ctx, input.postgresId, {
|
||||
deployment: ["create"],
|
||||
});
|
||||
|
||||
const queue: string[] = [];
|
||||
const done = false;
|
||||
@@ -276,32 +255,25 @@ export const postgresRouter = createTRPCRouter({
|
||||
changeStatus: protectedProcedure
|
||||
.input(apiChangePostgresStatus)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.postgresId, {
|
||||
deployment: ["create"],
|
||||
});
|
||||
const postgres = await findPostgresById(input.postgresId);
|
||||
if (
|
||||
postgres.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to change this Postgres status",
|
||||
});
|
||||
}
|
||||
await updatePostgresById(input.postgresId, {
|
||||
applicationStatus: input.applicationStatus,
|
||||
});
|
||||
await audit(ctx, {
|
||||
action: "update",
|
||||
resourceType: "service",
|
||||
resourceId: postgres.postgresId,
|
||||
resourceName: postgres.appName,
|
||||
});
|
||||
return postgres;
|
||||
}),
|
||||
remove: protectedProcedure
|
||||
.input(apiFindOnePostgres)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
if (ctx.user.role === "member") {
|
||||
await checkServiceAccess(
|
||||
ctx.user.id,
|
||||
input.postgresId,
|
||||
ctx.session.activeOrganizationId,
|
||||
"delete",
|
||||
);
|
||||
}
|
||||
await checkServiceAccess(ctx, input.postgresId, "delete");
|
||||
const postgres = await findPostgresById(input.postgresId);
|
||||
|
||||
if (
|
||||
@@ -314,6 +286,12 @@ export const postgresRouter = createTRPCRouter({
|
||||
});
|
||||
}
|
||||
|
||||
await audit(ctx, {
|
||||
action: "delete",
|
||||
resourceType: "service",
|
||||
resourceId: postgres.postgresId,
|
||||
resourceName: postgres.appName,
|
||||
});
|
||||
const backups = await findBackupsByDbId(input.postgresId, "postgres");
|
||||
|
||||
const cleanupOperations = [
|
||||
@@ -333,16 +311,9 @@ export const postgresRouter = createTRPCRouter({
|
||||
saveEnvironment: protectedProcedure
|
||||
.input(apiSaveEnvironmentVariablesPostgres)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const postgres = await findPostgresById(input.postgresId);
|
||||
if (
|
||||
postgres.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to save this environment",
|
||||
});
|
||||
}
|
||||
await checkServicePermissionAndAccess(ctx, input.postgresId, {
|
||||
envVars: ["write"],
|
||||
});
|
||||
const service = await updatePostgresById(input.postgresId, {
|
||||
env: input.env,
|
||||
});
|
||||
@@ -354,21 +325,20 @@ export const postgresRouter = createTRPCRouter({
|
||||
});
|
||||
}
|
||||
|
||||
await audit(ctx, {
|
||||
action: "update",
|
||||
resourceType: "service",
|
||||
resourceId: input.postgresId,
|
||||
});
|
||||
return true;
|
||||
}),
|
||||
reload: protectedProcedure
|
||||
.input(apiResetPostgres)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.postgresId, {
|
||||
deployment: ["create"],
|
||||
});
|
||||
const postgres = await findPostgresById(input.postgresId);
|
||||
if (
|
||||
postgres.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to reload this Postgres",
|
||||
});
|
||||
}
|
||||
if (postgres.serverId) {
|
||||
await stopServiceRemote(postgres.serverId, postgres.appName);
|
||||
} else {
|
||||
@@ -386,22 +356,21 @@ export const postgresRouter = createTRPCRouter({
|
||||
await updatePostgresById(input.postgresId, {
|
||||
applicationStatus: "done",
|
||||
});
|
||||
await audit(ctx, {
|
||||
action: "reload",
|
||||
resourceType: "service",
|
||||
resourceId: postgres.postgresId,
|
||||
resourceName: postgres.appName,
|
||||
});
|
||||
return true;
|
||||
}),
|
||||
update: protectedProcedure
|
||||
.input(apiUpdatePostgres)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const { postgresId, ...rest } = input;
|
||||
const postgres = await findPostgresById(postgresId);
|
||||
if (
|
||||
postgres.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to update this Postgres",
|
||||
});
|
||||
}
|
||||
await checkServicePermissionAndAccess(ctx, postgresId, {
|
||||
service: ["create"],
|
||||
});
|
||||
|
||||
const service = await updatePostgresById(postgresId, {
|
||||
...rest,
|
||||
@@ -414,6 +383,12 @@ export const postgresRouter = createTRPCRouter({
|
||||
});
|
||||
}
|
||||
|
||||
await audit(ctx, {
|
||||
action: "update",
|
||||
resourceType: "service",
|
||||
resourceId: postgresId,
|
||||
resourceName: service.appName,
|
||||
});
|
||||
return true;
|
||||
}),
|
||||
move: protectedProcedure
|
||||
@@ -424,31 +399,10 @@ export const postgresRouter = createTRPCRouter({
|
||||
}),
|
||||
)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const postgres = await findPostgresById(input.postgresId);
|
||||
if (
|
||||
postgres.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to move this postgres",
|
||||
});
|
||||
}
|
||||
await checkServicePermissionAndAccess(ctx, input.postgresId, {
|
||||
service: ["create"],
|
||||
});
|
||||
|
||||
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 environment",
|
||||
});
|
||||
}
|
||||
|
||||
// Update the postgres's projectId
|
||||
const updatedPostgres = await db
|
||||
.update(postgresTable)
|
||||
.set({
|
||||
@@ -465,24 +419,28 @@ export const postgresRouter = createTRPCRouter({
|
||||
});
|
||||
}
|
||||
|
||||
await audit(ctx, {
|
||||
action: "move",
|
||||
resourceType: "service",
|
||||
resourceId: updatedPostgres.postgresId,
|
||||
resourceName: updatedPostgres.appName,
|
||||
});
|
||||
return updatedPostgres;
|
||||
}),
|
||||
rebuild: protectedProcedure
|
||||
.input(apiRebuildPostgres)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const postgres = await findPostgresById(input.postgresId);
|
||||
if (
|
||||
postgres.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to rebuild this Postgres database",
|
||||
});
|
||||
}
|
||||
await checkServicePermissionAndAccess(ctx, input.postgresId, {
|
||||
deployment: ["create"],
|
||||
});
|
||||
|
||||
await rebuildDatabase(postgres.postgresId, "postgres");
|
||||
await rebuildDatabase(input.postgresId, "postgres");
|
||||
|
||||
await audit(ctx, {
|
||||
action: "rebuild",
|
||||
resourceType: "service",
|
||||
resourceId: input.postgresId,
|
||||
});
|
||||
return true;
|
||||
}),
|
||||
search: protectedProcedure
|
||||
@@ -538,19 +496,18 @@ export const postgresRouter = createTRPCRouter({
|
||||
),
|
||||
);
|
||||
}
|
||||
if (ctx.user.role === "member") {
|
||||
const { accessedServices } = await findMemberById(
|
||||
ctx.user.id,
|
||||
ctx.session.activeOrganizationId,
|
||||
);
|
||||
if (accessedServices.length === 0) return { items: [], total: 0 };
|
||||
baseConditions.push(
|
||||
sql`${postgresTable.postgresId} IN (${sql.join(
|
||||
accessedServices.map((id) => sql`${id}`),
|
||||
sql`, `,
|
||||
)})`,
|
||||
);
|
||||
}
|
||||
const { accessedServices } = await findMemberByUserId(
|
||||
ctx.user.id,
|
||||
ctx.session.activeOrganizationId,
|
||||
);
|
||||
if (accessedServices.length === 0) return { items: [], total: 0 };
|
||||
baseConditions.push(
|
||||
sql`${postgresTable.postgresId} IN (${sql.join(
|
||||
accessedServices.map((id) => sql`${id}`),
|
||||
sql`, `,
|
||||
)})`,
|
||||
);
|
||||
|
||||
const where = and(...baseConditions);
|
||||
const [items, countResult] = await Promise.all([
|
||||
db
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user