mirror of
https://github.com/Dokploy/dokploy.git
synced 2026-07-10 08:25:22 +02:00
Compare commits
36 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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) => {
|
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(
|
const { data, isPending } = api.application.readTraefikConfig.useQuery(
|
||||||
{
|
{
|
||||||
applicationId,
|
applicationId,
|
||||||
},
|
},
|
||||||
{ enabled: !!applicationId },
|
{ enabled: !!applicationId && canRead },
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (!canRead) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className="bg-background">
|
<Card className="bg-background">
|
||||||
<CardHeader className="flex flex-row justify-between">
|
<CardHeader className="flex flex-row justify-between">
|
||||||
|
|||||||
@@ -60,6 +60,8 @@ export const validateAndFormatYAML = (yamlText: string) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const UpdateTraefikConfig = ({ applicationId }: Props) => {
|
export const UpdateTraefikConfig = ({ applicationId }: Props) => {
|
||||||
|
const { data: permissions } = api.user.getPermissions.useQuery();
|
||||||
|
const canWrite = permissions?.traefikFiles.write ?? false;
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const [skipYamlValidation, setSkipYamlValidation] = useState(false);
|
const [skipYamlValidation, setSkipYamlValidation] = useState(false);
|
||||||
const { data, refetch } = api.application.readTraefikConfig.useQuery(
|
const { data, refetch } = api.application.readTraefikConfig.useQuery(
|
||||||
@@ -125,9 +127,11 @@ export const UpdateTraefikConfig = ({ applicationId }: Props) => {
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<DialogTrigger asChild>
|
{canWrite && (
|
||||||
<Button isLoading={isPending}>Modify</Button>
|
<DialogTrigger asChild>
|
||||||
</DialogTrigger>
|
<Button isLoading={isPending}>Modify</Button>
|
||||||
|
</DialogTrigger>
|
||||||
|
)}
|
||||||
<DialogContent className="sm:max-w-4xl">
|
<DialogContent className="sm:max-w-4xl">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Update traefik config</DialogTitle>
|
<DialogTitle>Update traefik config</DialogTitle>
|
||||||
|
|||||||
@@ -21,6 +21,13 @@ interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const ShowVolumes = ({ id, type }: 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 = {
|
const queryMap = {
|
||||||
postgres: () =>
|
postgres: () =>
|
||||||
api.postgres.one.useQuery({ postgresId: id }, { enabled: !!id }),
|
api.postgres.one.useQuery({ postgresId: id }, { enabled: !!id }),
|
||||||
@@ -50,7 +57,7 @@ export const ShowVolumes = ({ id, type }: Props) => {
|
|||||||
</CardDescription>
|
</CardDescription>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{data && data?.mounts.length > 0 && (
|
{canCreate && data && data?.mounts.length > 0 && (
|
||||||
<AddVolumes serviceId={id} refetch={refetch} serviceType={type}>
|
<AddVolumes serviceId={id} refetch={refetch} serviceType={type}>
|
||||||
Add Volume
|
Add Volume
|
||||||
</AddVolumes>
|
</AddVolumes>
|
||||||
@@ -63,9 +70,11 @@ export const ShowVolumes = ({ id, type }: Props) => {
|
|||||||
<span className="text-base text-muted-foreground">
|
<span className="text-base text-muted-foreground">
|
||||||
No volumes/mounts configured
|
No volumes/mounts configured
|
||||||
</span>
|
</span>
|
||||||
<AddVolumes serviceId={id} refetch={refetch} serviceType={type}>
|
{canCreate && (
|
||||||
Add Volume
|
<AddVolumes serviceId={id} refetch={refetch} serviceType={type}>
|
||||||
</AddVolumes>
|
Add Volume
|
||||||
|
</AddVolumes>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex flex-col pt-2 gap-4">
|
<div className="flex flex-col pt-2 gap-4">
|
||||||
@@ -130,38 +139,42 @@ export const ShowVolumes = ({ id, type }: Props) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-row gap-1">
|
<div className="flex flex-row gap-1">
|
||||||
<UpdateVolume
|
{canCreate && (
|
||||||
mountId={mount.mountId}
|
<UpdateVolume
|
||||||
type={mount.type}
|
mountId={mount.mountId}
|
||||||
refetch={refetch}
|
type={mount.type}
|
||||||
serviceType={type}
|
refetch={refetch}
|
||||||
/>
|
serviceType={type}
|
||||||
<DialogAction
|
/>
|
||||||
title="Delete Volume"
|
)}
|
||||||
description="Are you sure you want to delete this volume?"
|
{canDelete && (
|
||||||
type="destructive"
|
<DialogAction
|
||||||
onClick={async () => {
|
title="Delete Volume"
|
||||||
await deleteVolume({
|
description="Are you sure you want to delete this volume?"
|
||||||
mountId: mount.mountId,
|
type="destructive"
|
||||||
})
|
onClick={async () => {
|
||||||
.then(() => {
|
await deleteVolume({
|
||||||
refetch();
|
mountId: mount.mountId,
|
||||||
toast.success("Volume deleted successfully");
|
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.then(() => {
|
||||||
toast.error("Error deleting volume");
|
refetch();
|
||||||
});
|
toast.success("Volume deleted successfully");
|
||||||
}}
|
})
|
||||||
>
|
.catch(() => {
|
||||||
<Button
|
toast.error("Error deleting volume");
|
||||||
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
|
||||||
</Button>
|
variant="ghost"
|
||||||
</DialogAction>
|
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>
|
</div>
|
||||||
|
|||||||
@@ -50,6 +50,9 @@ interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const ShowDomains = ({ id, type }: 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 } =
|
const { data: application } =
|
||||||
type === "application"
|
type === "application"
|
||||||
? api.application.one.useQuery(
|
? api.application.one.useQuery(
|
||||||
@@ -149,7 +152,7 @@ export const ShowDomains = ({ id, type }: Props) => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-row gap-4 flex-wrap">
|
<div className="flex flex-row gap-4 flex-wrap">
|
||||||
{data && data?.length > 0 && (
|
{canCreateDomain && data && data?.length > 0 && (
|
||||||
<AddDomain id={id} type={type}>
|
<AddDomain id={id} type={type}>
|
||||||
<Button>
|
<Button>
|
||||||
<GlobeIcon className="size-4" /> Add Domain
|
<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
|
To access the application it is required to set at least 1
|
||||||
domain
|
domain
|
||||||
</span>
|
</span>
|
||||||
<div className="flex flex-row gap-4 flex-wrap">
|
{canCreateDomain && (
|
||||||
<AddDomain id={id} type={type}>
|
<div className="flex flex-row gap-4 flex-wrap">
|
||||||
<Button>
|
<AddDomain id={id} type={type}>
|
||||||
<GlobeIcon className="size-4" /> Add Domain
|
<Button>
|
||||||
</Button>
|
<GlobeIcon className="size-4" /> Add Domain
|
||||||
</AddDomain>
|
</Button>
|
||||||
</div>
|
</AddDomain>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="grid grid-cols-1 gap-4 xl:grid-cols-2 w-full min-h-[40vh] ">
|
<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
|
{canCreateDomain && (
|
||||||
id={id}
|
<AddDomain
|
||||||
type={type}
|
id={id}
|
||||||
domainId={item.domainId}
|
type={type}
|
||||||
>
|
domainId={item.domainId}
|
||||||
<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
|
||||||
</Button>
|
variant="ghost"
|
||||||
</AddDomain>
|
size="icon"
|
||||||
<DialogAction
|
className="group hover:bg-blue-500/10"
|
||||||
title="Delete Domain"
|
>
|
||||||
description="Are you sure you want to delete this domain?"
|
<PenBoxIcon className="size-3.5 text-primary group-hover:text-blue-500" />
|
||||||
type="destructive"
|
</Button>
|
||||||
onClick={async () => {
|
</AddDomain>
|
||||||
await deleteDomain({
|
)}
|
||||||
domainId: item.domainId,
|
{canDeleteDomain && (
|
||||||
})
|
<DialogAction
|
||||||
.then((_data) => {
|
title="Delete Domain"
|
||||||
refetch();
|
description="Are you sure you want to delete this domain?"
|
||||||
toast.success(
|
type="destructive"
|
||||||
"Domain deleted successfully",
|
onClick={async () => {
|
||||||
);
|
await deleteDomain({
|
||||||
|
domainId: item.domainId,
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.then((_data) => {
|
||||||
toast.error("Error deleting domain");
|
refetch();
|
||||||
});
|
toast.success(
|
||||||
}}
|
"Domain deleted successfully",
|
||||||
>
|
);
|
||||||
<Button
|
})
|
||||||
variant="ghost"
|
.catch(() => {
|
||||||
size="icon"
|
toast.error("Error deleting domain");
|
||||||
className="group hover:bg-red-500/10"
|
});
|
||||||
isLoading={isRemoving}
|
}}
|
||||||
>
|
>
|
||||||
<Trash2 className="size-4 text-primary group-hover:text-red-500" />
|
<Button
|
||||||
</Button>
|
variant="ghost"
|
||||||
</DialogAction>
|
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="w-full break-all">
|
<div className="w-full break-all">
|
||||||
|
|||||||
@@ -36,6 +36,8 @@ interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const ShowEnvironment = ({ id, type }: Props) => {
|
export const ShowEnvironment = ({ id, type }: Props) => {
|
||||||
|
const { data: permissions } = api.user.getPermissions.useQuery();
|
||||||
|
const canWrite = permissions?.envVars.write ?? false;
|
||||||
const queryMap = {
|
const queryMap = {
|
||||||
postgres: () =>
|
postgres: () =>
|
||||||
api.postgres.one.useQuery({ postgresId: id }, { enabled: !!id }),
|
api.postgres.one.useQuery({ postgresId: id }, { enabled: !!id }),
|
||||||
@@ -185,25 +187,27 @@ PORT=3000
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="flex flex-row justify-end gap-2">
|
{canWrite && (
|
||||||
{hasChanges && (
|
<div className="flex flex-row justify-end gap-2">
|
||||||
|
{hasChanges && (
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
onClick={handleCancel}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
isLoading={isPending}
|
||||||
variant="outline"
|
className="w-fit"
|
||||||
onClick={handleCancel}
|
type="submit"
|
||||||
|
disabled={!hasChanges}
|
||||||
>
|
>
|
||||||
Cancel
|
Save
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
</div>
|
||||||
<Button
|
)}
|
||||||
isLoading={isPending}
|
|
||||||
className="w-fit"
|
|
||||||
type="submit"
|
|
||||||
disabled={!hasChanges}
|
|
||||||
>
|
|
||||||
Save
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</form>
|
</form>
|
||||||
</Form>
|
</Form>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|||||||
@@ -31,6 +31,8 @@ interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const ShowEnvironment = ({ applicationId }: Props) => {
|
export const ShowEnvironment = ({ applicationId }: Props) => {
|
||||||
|
const { data: permissions } = api.user.getPermissions.useQuery();
|
||||||
|
const canWrite = permissions?.envVars.write ?? false;
|
||||||
const { mutateAsync, isPending } =
|
const { mutateAsync, isPending } =
|
||||||
api.application.saveEnvironment.useMutation();
|
api.application.saveEnvironment.useMutation();
|
||||||
|
|
||||||
@@ -201,27 +203,30 @@ export const ShowEnvironment = ({ applicationId }: Props) => {
|
|||||||
<Switch
|
<Switch
|
||||||
checked={field.value}
|
checked={field.value}
|
||||||
onCheckedChange={field.onChange}
|
onCheckedChange={field.onChange}
|
||||||
|
disabled={!canWrite}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<div className="flex flex-row justify-end gap-2">
|
{canWrite && (
|
||||||
{hasChanges && (
|
<div className="flex flex-row justify-end gap-2">
|
||||||
<Button type="button" variant="outline" onClick={handleCancel}>
|
{hasChanges && (
|
||||||
Cancel
|
<Button type="button" variant="outline" onClick={handleCancel}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
<Button
|
||||||
|
isLoading={isPending}
|
||||||
|
className="w-fit"
|
||||||
|
type="submit"
|
||||||
|
disabled={!hasChanges}
|
||||||
|
>
|
||||||
|
Save
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
</div>
|
||||||
<Button
|
)}
|
||||||
isLoading={isPending}
|
|
||||||
className="w-fit"
|
|
||||||
type="submit"
|
|
||||||
disabled={!hasChanges}
|
|
||||||
>
|
|
||||||
Save
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</form>
|
</form>
|
||||||
</Form>
|
</Form>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
|
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 Link from "next/link";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
@@ -416,10 +416,8 @@ export const SaveBitbucketProvider = ({ applicationId }: Props) => {
|
|||||||
<FormLabel>Watch Paths</FormLabel>
|
<FormLabel>Watch Paths</FormLabel>
|
||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger>
|
<TooltipTrigger asChild>
|
||||||
<div className="size-4 rounded-full bg-muted flex items-center justify-center text-[10px] font-bold">
|
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
|
||||||
?
|
|
||||||
</div>
|
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent>
|
<TooltipContent>
|
||||||
<p>
|
<p>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
|
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 Link from "next/link";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
@@ -228,10 +228,8 @@ export const SaveGitProvider = ({ applicationId }: Props) => {
|
|||||||
<FormLabel>Watch Paths</FormLabel>
|
<FormLabel>Watch Paths</FormLabel>
|
||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger>
|
<TooltipTrigger asChild>
|
||||||
<div className="size-4 rounded-full bg-muted flex items-center justify-center text-[10px] font-bold">
|
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
|
||||||
?
|
|
||||||
</div>
|
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent className="max-w-[300px]">
|
<TooltipContent className="max-w-[300px]">
|
||||||
<p>
|
<p>
|
||||||
|
|||||||
@@ -30,6 +30,9 @@ interface Props {
|
|||||||
|
|
||||||
export const ShowGeneralApplication = ({ applicationId }: Props) => {
|
export const ShowGeneralApplication = ({ applicationId }: Props) => {
|
||||||
const router = useRouter();
|
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(
|
const { data, refetch } = api.application.one.useQuery(
|
||||||
{
|
{
|
||||||
applicationId,
|
applicationId,
|
||||||
@@ -57,128 +60,135 @@ export const ShowGeneralApplication = ({ applicationId }: Props) => {
|
|||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="flex flex-row gap-4 flex-wrap">
|
<CardContent className="flex flex-row gap-4 flex-wrap">
|
||||||
<TooltipProvider delayDuration={0} disableHoverableContent={false}>
|
<TooltipProvider delayDuration={0} disableHoverableContent={false}>
|
||||||
<DialogAction
|
{canDeploy && (
|
||||||
title="Deploy Application"
|
<DialogAction
|
||||||
description="Are you sure you want to deploy this application?"
|
title="Deploy Application"
|
||||||
type="default"
|
description="Are you sure you want to deploy this application?"
|
||||||
onClick={async () => {
|
type="default"
|
||||||
await deploy({
|
onClick={async () => {
|
||||||
applicationId: applicationId,
|
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`,
|
|
||||||
);
|
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.then(() => {
|
||||||
toast.error("Error deploying application");
|
toast.success("Application deployed successfully");
|
||||||
});
|
refetch();
|
||||||
}}
|
router.push(
|
||||||
>
|
`/dashboard/project/${data?.environment.projectId}/environment/${data?.environmentId}/services/application/${applicationId}?tab=deployments`,
|
||||||
<Button
|
);
|
||||||
variant="default"
|
})
|
||||||
isLoading={data?.applicationStatus === "running"}
|
.catch(() => {
|
||||||
className="flex items-center gap-1.5 group focus-visible:ring-2 focus-visible:ring-offset-2"
|
toast.error("Error deploying application");
|
||||||
|
});
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<Tooltip>
|
<Button
|
||||||
<TooltipTrigger asChild>
|
variant="default"
|
||||||
<div className="flex items-center">
|
isLoading={data?.applicationStatus === "running"}
|
||||||
<Rocket className="size-4 mr-1" />
|
className="flex items-center gap-1.5 group focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||||
Deploy
|
>
|
||||||
</div>
|
<Tooltip>
|
||||||
</TooltipTrigger>
|
<TooltipTrigger asChild>
|
||||||
<TooltipPrimitive.Portal>
|
<div className="flex items-center">
|
||||||
<TooltipContent sideOffset={5} className="z-[60]">
|
<Rocket className="size-4 mr-1" />
|
||||||
<p>
|
Deploy
|
||||||
Downloads the source code and performs a complete build
|
</div>
|
||||||
</p>
|
</TooltipTrigger>
|
||||||
</TooltipContent>
|
<TooltipPrimitive.Portal>
|
||||||
</TooltipPrimitive.Portal>
|
<TooltipContent sideOffset={5} className="z-[60]">
|
||||||
</Tooltip>
|
<p>
|
||||||
</Button>
|
Downloads the source code and performs a complete
|
||||||
</DialogAction>
|
build
|
||||||
<DialogAction
|
</p>
|
||||||
title="Reload Application"
|
</TooltipContent>
|
||||||
description="Are you sure you want to reload this application?"
|
</TooltipPrimitive.Portal>
|
||||||
type="default"
|
</Tooltip>
|
||||||
onClick={async () => {
|
</Button>
|
||||||
await reload({
|
</DialogAction>
|
||||||
applicationId: applicationId,
|
)}
|
||||||
appName: data?.appName || "",
|
{canDeploy && (
|
||||||
})
|
<DialogAction
|
||||||
.then(() => {
|
title="Reload Application"
|
||||||
toast.success("Application reloaded successfully");
|
description="Are you sure you want to reload this application?"
|
||||||
refetch();
|
type="default"
|
||||||
|
onClick={async () => {
|
||||||
|
await reload({
|
||||||
|
applicationId: applicationId,
|
||||||
|
appName: data?.appName || "",
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.then(() => {
|
||||||
toast.error("Error reloading application");
|
toast.success("Application reloaded successfully");
|
||||||
});
|
refetch();
|
||||||
}}
|
})
|
||||||
>
|
.catch(() => {
|
||||||
<Button
|
toast.error("Error reloading application");
|
||||||
variant="secondary"
|
});
|
||||||
isLoading={isReloading}
|
}}
|
||||||
className="flex items-center gap-1.5 group focus-visible:ring-2 focus-visible:ring-offset-2"
|
|
||||||
>
|
>
|
||||||
<Tooltip>
|
<Button
|
||||||
<TooltipTrigger asChild>
|
variant="secondary"
|
||||||
<div className="flex items-center">
|
isLoading={isReloading}
|
||||||
<RefreshCcw className="size-4 mr-1" />
|
className="flex items-center gap-1.5 group focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||||
Reload
|
>
|
||||||
</div>
|
<Tooltip>
|
||||||
</TooltipTrigger>
|
<TooltipTrigger asChild>
|
||||||
<TooltipPrimitive.Portal>
|
<div className="flex items-center">
|
||||||
<TooltipContent sideOffset={5} className="z-[60]">
|
<RefreshCcw className="size-4 mr-1" />
|
||||||
<p>Reload the application without rebuilding it</p>
|
Reload
|
||||||
</TooltipContent>
|
</div>
|
||||||
</TooltipPrimitive.Portal>
|
</TooltipTrigger>
|
||||||
</Tooltip>
|
<TooltipPrimitive.Portal>
|
||||||
</Button>
|
<TooltipContent sideOffset={5} className="z-[60]">
|
||||||
</DialogAction>
|
<p>Reload the application without rebuilding it</p>
|
||||||
<DialogAction
|
</TooltipContent>
|
||||||
title="Rebuild Application"
|
</TooltipPrimitive.Portal>
|
||||||
description="Are you sure you want to rebuild this application?"
|
</Tooltip>
|
||||||
type="default"
|
</Button>
|
||||||
onClick={async () => {
|
</DialogAction>
|
||||||
await redeploy({
|
)}
|
||||||
applicationId: applicationId,
|
{canDeploy && (
|
||||||
})
|
<DialogAction
|
||||||
.then(() => {
|
title="Rebuild Application"
|
||||||
toast.success("Application rebuilt successfully");
|
description="Are you sure you want to rebuild this application?"
|
||||||
refetch();
|
type="default"
|
||||||
|
onClick={async () => {
|
||||||
|
await redeploy({
|
||||||
|
applicationId: applicationId,
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.then(() => {
|
||||||
toast.error("Error rebuilding application");
|
toast.success("Application rebuilt successfully");
|
||||||
});
|
refetch();
|
||||||
}}
|
})
|
||||||
>
|
.catch(() => {
|
||||||
<Button
|
toast.error("Error rebuilding application");
|
||||||
variant="secondary"
|
});
|
||||||
isLoading={data?.applicationStatus === "running"}
|
}}
|
||||||
className="flex items-center gap-1.5 group focus-visible:ring-2 focus-visible:ring-offset-2"
|
|
||||||
>
|
>
|
||||||
<Tooltip>
|
<Button
|
||||||
<TooltipTrigger asChild>
|
variant="secondary"
|
||||||
<div className="flex items-center">
|
isLoading={data?.applicationStatus === "running"}
|
||||||
<Hammer className="size-4 mr-1" />
|
className="flex items-center gap-1.5 group focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||||
Rebuild
|
>
|
||||||
</div>
|
<Tooltip>
|
||||||
</TooltipTrigger>
|
<TooltipTrigger asChild>
|
||||||
<TooltipPrimitive.Portal>
|
<div className="flex items-center">
|
||||||
<TooltipContent sideOffset={5} className="z-[60]">
|
<Hammer className="size-4 mr-1" />
|
||||||
<p>
|
Rebuild
|
||||||
Only rebuilds the application without downloading new
|
</div>
|
||||||
code
|
</TooltipTrigger>
|
||||||
</p>
|
<TooltipPrimitive.Portal>
|
||||||
</TooltipContent>
|
<TooltipContent sideOffset={5} className="z-[60]">
|
||||||
</TooltipPrimitive.Portal>
|
<p>
|
||||||
</Tooltip>
|
Only rebuilds the application without downloading new
|
||||||
</Button>
|
code
|
||||||
</DialogAction>
|
</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</TooltipPrimitive.Portal>
|
||||||
|
</Tooltip>
|
||||||
|
</Button>
|
||||||
|
</DialogAction>
|
||||||
|
)}
|
||||||
|
|
||||||
{data?.applicationStatus === "idle" ? (
|
{canDeploy && data?.applicationStatus === "idle" ? (
|
||||||
<DialogAction
|
<DialogAction
|
||||||
title="Start Application"
|
title="Start Application"
|
||||||
description="Are you sure you want to start this application?"
|
description="Are you sure you want to start this application?"
|
||||||
@@ -219,7 +229,7 @@ export const ShowGeneralApplication = ({ applicationId }: Props) => {
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Button>
|
</Button>
|
||||||
</DialogAction>
|
</DialogAction>
|
||||||
) : (
|
) : canDeploy ? (
|
||||||
<DialogAction
|
<DialogAction
|
||||||
title="Stop Application"
|
title="Stop Application"
|
||||||
description="Are you sure you want to stop this application?"
|
description="Are you sure you want to stop this application?"
|
||||||
@@ -256,7 +266,7 @@ export const ShowGeneralApplication = ({ applicationId }: Props) => {
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Button>
|
</Button>
|
||||||
</DialogAction>
|
</DialogAction>
|
||||||
)}
|
) : null}
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
<DockerTerminalModal
|
<DockerTerminalModal
|
||||||
appName={data?.appName || ""}
|
appName={data?.appName || ""}
|
||||||
@@ -270,49 +280,53 @@ export const ShowGeneralApplication = ({ applicationId }: Props) => {
|
|||||||
Open Terminal
|
Open Terminal
|
||||||
</Button>
|
</Button>
|
||||||
</DockerTerminalModal>
|
</DockerTerminalModal>
|
||||||
<div className="flex flex-row items-center gap-2 rounded-md px-4 py-2 border">
|
{canUpdateService && (
|
||||||
<span className="text-sm font-medium">Autodeploy</span>
|
<div className="flex flex-row items-center gap-2 rounded-md px-4 py-2 border">
|
||||||
<Switch
|
<span className="text-sm font-medium">Autodeploy</span>
|
||||||
aria-label="Toggle autodeploy"
|
<Switch
|
||||||
checked={data?.autoDeploy || false}
|
aria-label="Toggle autodeploy"
|
||||||
onCheckedChange={async (enabled) => {
|
checked={data?.autoDeploy || false}
|
||||||
await update({
|
onCheckedChange={async (enabled) => {
|
||||||
applicationId,
|
await update({
|
||||||
autoDeploy: enabled,
|
applicationId,
|
||||||
})
|
autoDeploy: enabled,
|
||||||
.then(async () => {
|
|
||||||
toast.success("Auto Deploy Updated");
|
|
||||||
await refetch();
|
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.then(async () => {
|
||||||
toast.error("Error updating Auto Deploy");
|
toast.success("Auto Deploy Updated");
|
||||||
});
|
await refetch();
|
||||||
}}
|
})
|
||||||
className="flex flex-row gap-2 items-center data-[state=checked]:bg-primary"
|
.catch(() => {
|
||||||
/>
|
toast.error("Error updating Auto Deploy");
|
||||||
</div>
|
});
|
||||||
|
}}
|
||||||
|
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">
|
{canUpdateService && (
|
||||||
<span className="text-sm font-medium">Clean Cache</span>
|
<div className="flex flex-row items-center gap-2 rounded-md px-4 py-2 border">
|
||||||
<Switch
|
<span className="text-sm font-medium">Clean Cache</span>
|
||||||
aria-label="Toggle clean cache"
|
<Switch
|
||||||
checked={data?.cleanCache || false}
|
aria-label="Toggle clean cache"
|
||||||
onCheckedChange={async (enabled) => {
|
checked={data?.cleanCache || false}
|
||||||
await update({
|
onCheckedChange={async (enabled) => {
|
||||||
applicationId,
|
await update({
|
||||||
cleanCache: enabled,
|
applicationId,
|
||||||
})
|
cleanCache: enabled,
|
||||||
.then(async () => {
|
|
||||||
toast.success("Clean Cache Updated");
|
|
||||||
await refetch();
|
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.then(async () => {
|
||||||
toast.error("Error updating Clean Cache");
|
toast.success("Clean Cache Updated");
|
||||||
});
|
await refetch();
|
||||||
}}
|
})
|
||||||
className="flex flex-row gap-2 items-center data-[state=checked]:bg-primary"
|
.catch(() => {
|
||||||
/>
|
toast.error("Error updating Clean Cache");
|
||||||
</div>
|
});
|
||||||
|
}}
|
||||||
|
className="flex flex-row gap-2 items-center data-[state=checked]:bg-primary"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
<ShowProviderForm applicationId={applicationId} />
|
<ShowProviderForm applicationId={applicationId} />
|
||||||
|
|||||||
@@ -46,6 +46,8 @@ interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const DeleteService = ({ id, type }: 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 [isOpen, setIsOpen] = useState(false);
|
||||||
|
|
||||||
const queryMap = {
|
const queryMap = {
|
||||||
@@ -123,6 +125,8 @@ export const DeleteService = ({ id, type }: Props) => {
|
|||||||
data?.applicationStatus === "running") ||
|
data?.applicationStatus === "running") ||
|
||||||
(data && "composeStatus" in data && data?.composeStatus === "running");
|
(data && "composeStatus" in data && data?.composeStatus === "running");
|
||||||
|
|
||||||
|
if (!canDelete) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
|
|||||||
@@ -19,6 +19,9 @@ interface Props {
|
|||||||
}
|
}
|
||||||
export const ComposeActions = ({ composeId }: Props) => {
|
export const ComposeActions = ({ composeId }: Props) => {
|
||||||
const router = useRouter();
|
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(
|
const { data, refetch } = api.compose.one.useQuery(
|
||||||
{
|
{
|
||||||
composeId,
|
composeId,
|
||||||
@@ -35,162 +38,169 @@ export const ComposeActions = ({ composeId }: Props) => {
|
|||||||
return (
|
return (
|
||||||
<div className="flex flex-row gap-4 w-full flex-wrap ">
|
<div className="flex flex-row gap-4 w-full flex-wrap ">
|
||||||
<TooltipProvider delayDuration={0} disableHoverableContent={false}>
|
<TooltipProvider delayDuration={0} disableHoverableContent={false}>
|
||||||
<DialogAction
|
{canDeploy && (
|
||||||
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" ? (
|
|
||||||
<DialogAction
|
<DialogAction
|
||||||
title="Start Compose"
|
title="Deploy Compose"
|
||||||
description="Are you sure you want to start this compose?"
|
description="Are you sure you want to deploy this compose?"
|
||||||
type="default"
|
type="default"
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
await start({
|
await deploy({
|
||||||
composeId: composeId,
|
composeId: composeId,
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
toast.success("Compose started successfully");
|
toast.success("Compose deployed successfully");
|
||||||
refetch();
|
refetch();
|
||||||
|
router.push(
|
||||||
|
`/dashboard/project/${data?.environment.projectId}/environment/${data?.environmentId}/services/compose/${composeId}?tab=deployments`,
|
||||||
|
);
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error starting compose");
|
toast.error("Error deploying compose");
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
variant="secondary"
|
variant="default"
|
||||||
isLoading={isStarting}
|
isLoading={data?.composeStatus === "running"}
|
||||||
className="flex items-center gap-1.5 group focus-visible:ring-2 focus-visible:ring-offset-2"
|
className="flex items-center gap-1.5 group focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||||
>
|
>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<CheckCircle2 className="size-4 mr-1" />
|
<Rocket className="size-4 mr-1" />
|
||||||
Start
|
Deploy
|
||||||
</div>
|
</div>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipPrimitive.Portal>
|
<TooltipPrimitive.Portal>
|
||||||
<TooltipContent sideOffset={5} className="z-[60]">
|
<TooltipContent sideOffset={5} className="z-[60]">
|
||||||
<p>
|
<p>
|
||||||
Start the compose (requires a previous successful build)
|
Downloads the source code and performs a complete build
|
||||||
</p>
|
</p>
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</TooltipPrimitive.Portal>
|
</TooltipPrimitive.Portal>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Button>
|
</Button>
|
||||||
</DialogAction>
|
</DialogAction>
|
||||||
) : (
|
)}
|
||||||
|
{canDeploy && (
|
||||||
<DialogAction
|
<DialogAction
|
||||||
title="Stop Compose"
|
title="Reload Compose"
|
||||||
description="Are you sure you want to stop this compose?"
|
description="Are you sure you want to reload this compose?"
|
||||||
|
type="default"
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
await stop({
|
await redeploy({
|
||||||
composeId: composeId,
|
composeId: composeId,
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
toast.success("Compose stopped successfully");
|
toast.success("Compose reloaded successfully");
|
||||||
refetch();
|
refetch();
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error stopping compose");
|
toast.error("Error reloading compose");
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
variant="destructive"
|
variant="secondary"
|
||||||
isLoading={isStopping}
|
isLoading={data?.composeStatus === "running"}
|
||||||
className="flex items-center gap-1.5 group focus-visible:ring-2 focus-visible:ring-offset-2"
|
className="flex items-center gap-1.5 group focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||||
>
|
>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<Ban className="size-4 mr-1" />
|
<RefreshCcw className="size-4 mr-1" />
|
||||||
Stop
|
Reload
|
||||||
</div>
|
</div>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipPrimitive.Portal>
|
<TooltipPrimitive.Portal>
|
||||||
<TooltipContent sideOffset={5} className="z-[60]">
|
<TooltipContent sideOffset={5} className="z-[60]">
|
||||||
<p>Stop the currently running compose</p>
|
<p>Reload the compose without rebuilding it</p>
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</TooltipPrimitive.Portal>
|
</TooltipPrimitive.Portal>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Button>
|
</Button>
|
||||||
</DialogAction>
|
</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>
|
</TooltipProvider>
|
||||||
<DockerTerminalModal
|
<DockerTerminalModal
|
||||||
appName={data?.appName || ""}
|
appName={data?.appName || ""}
|
||||||
@@ -205,27 +215,29 @@ export const ComposeActions = ({ composeId }: Props) => {
|
|||||||
Open Terminal
|
Open Terminal
|
||||||
</Button>
|
</Button>
|
||||||
</DockerTerminalModal>
|
</DockerTerminalModal>
|
||||||
<div className="flex flex-row items-center gap-2 rounded-md px-4 py-2 border">
|
{canUpdateService && (
|
||||||
<span className="text-sm font-medium">Autodeploy</span>
|
<div className="flex flex-row items-center gap-2 rounded-md px-4 py-2 border">
|
||||||
<Switch
|
<span className="text-sm font-medium">Autodeploy</span>
|
||||||
aria-label="Toggle autodeploy"
|
<Switch
|
||||||
checked={data?.autoDeploy || false}
|
aria-label="Toggle autodeploy"
|
||||||
onCheckedChange={async (enabled) => {
|
checked={data?.autoDeploy || false}
|
||||||
await update({
|
onCheckedChange={async (enabled) => {
|
||||||
composeId,
|
await update({
|
||||||
autoDeploy: enabled,
|
composeId,
|
||||||
})
|
autoDeploy: enabled,
|
||||||
.then(async () => {
|
|
||||||
toast.success("Auto Deploy Updated");
|
|
||||||
await refetch();
|
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.then(async () => {
|
||||||
toast.error("Error updating Auto Deploy");
|
toast.success("Auto Deploy Updated");
|
||||||
});
|
await refetch();
|
||||||
}}
|
})
|
||||||
className="flex flex-row gap-2 items-center data-[state=checked]:bg-primary"
|
.catch(() => {
|
||||||
/>
|
toast.error("Error updating Auto Deploy");
|
||||||
</div>
|
});
|
||||||
|
}}
|
||||||
|
className="flex flex-row gap-2 items-center data-[state=checked]:bg-primary"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -26,6 +26,8 @@ const AddComposeFile = z.object({
|
|||||||
type AddComposeFile = z.infer<typeof AddComposeFile>;
|
type AddComposeFile = z.infer<typeof AddComposeFile>;
|
||||||
|
|
||||||
export const ComposeFileEditor = ({ composeId }: Props) => {
|
export const ComposeFileEditor = ({ composeId }: Props) => {
|
||||||
|
const { data: permissions } = api.user.getPermissions.useQuery();
|
||||||
|
const canUpdate = permissions?.service.create ?? false;
|
||||||
const utils = api.useUtils();
|
const utils = api.useUtils();
|
||||||
const { data, refetch } = api.compose.one.useQuery(
|
const { data, refetch } = api.compose.one.useQuery(
|
||||||
{
|
{
|
||||||
@@ -164,14 +166,16 @@ services:
|
|||||||
</Form>
|
</Form>
|
||||||
<div className="flex justify-between flex-col lg:flex-row gap-2">
|
<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" />
|
<div className="w-full flex flex-col lg:flex-row gap-4 items-end" />
|
||||||
<Button
|
{canUpdate && (
|
||||||
type="submit"
|
<Button
|
||||||
form="hook-form-save-compose-file"
|
type="submit"
|
||||||
isLoading={isPending}
|
form="hook-form-save-compose-file"
|
||||||
className="lg:w-fit w-full"
|
isLoading={isPending}
|
||||||
>
|
className="lg:w-fit w-full"
|
||||||
Save
|
>
|
||||||
</Button>
|
Save
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
|
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 Link from "next/link";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
@@ -230,10 +230,8 @@ export const SaveGitProviderCompose = ({ composeId }: Props) => {
|
|||||||
<FormLabel>Watch Paths</FormLabel>
|
<FormLabel>Watch Paths</FormLabel>
|
||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger>
|
<TooltipTrigger asChild>
|
||||||
<div className="size-4 rounded-full bg-muted flex items-center justify-center text-[10px] font-bold">
|
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
|
||||||
?
|
|
||||||
</div>
|
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent className="max-w-[300px]">
|
<TooltipContent className="max-w-[300px]">
|
||||||
<p>
|
<p>
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const ShowGeneralMariadb = ({ mariadbId }: 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(
|
const { data, refetch } = api.mariadb.one.useQuery(
|
||||||
{
|
{
|
||||||
mariadbId,
|
mariadbId,
|
||||||
@@ -72,154 +74,33 @@ export const ShowGeneralMariadb = ({ mariadbId }: Props) => {
|
|||||||
<CardTitle className="text-xl">Deploy Settings</CardTitle>
|
<CardTitle className="text-xl">Deploy Settings</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="flex flex-row gap-4 flex-wrap">
|
<CardContent className="flex flex-row gap-4 flex-wrap">
|
||||||
<TooltipProvider delayDuration={0}>
|
{canDeploy && (
|
||||||
<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" ? (
|
|
||||||
<TooltipProvider delayDuration={0}>
|
<TooltipProvider delayDuration={0}>
|
||||||
<DialogAction
|
<DialogAction
|
||||||
title="Start Mariadb"
|
title="Deploy Mariadb"
|
||||||
description="Are you sure you want to start this mariadb?"
|
description="Are you sure you want to deploy this mariadb?"
|
||||||
type="default"
|
type="default"
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
await start({
|
setIsDeploying(true);
|
||||||
mariadbId: mariadbId,
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||||
})
|
refetch();
|
||||||
.then(() => {
|
|
||||||
toast.success("Mariadb started successfully");
|
|
||||||
refetch();
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
toast.error("Error starting Mariadb");
|
|
||||||
});
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
variant="secondary"
|
variant="default"
|
||||||
isLoading={isStarting}
|
isLoading={data?.applicationStatus === "running"}
|
||||||
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||||
>
|
>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<CheckCircle2 className="size-4 mr-1" />
|
<Rocket className="size-4 mr-1" />
|
||||||
Start
|
Deploy
|
||||||
</div>
|
</div>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipPrimitive.Portal>
|
<TooltipPrimitive.Portal>
|
||||||
<TooltipContent sideOffset={5} className="z-[60]">
|
<TooltipContent sideOffset={5} className="z-[60]">
|
||||||
<p>
|
<p>Downloads and sets up the MariaDB database</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>
|
</TooltipContent>
|
||||||
</TooltipPrimitive.Portal>
|
</TooltipPrimitive.Portal>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
@@ -227,6 +108,132 @@ export const ShowGeneralMariadb = ({ mariadbId }: Props) => {
|
|||||||
</DialogAction>
|
</DialogAction>
|
||||||
</TooltipProvider>
|
</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
|
<DockerTerminalModal
|
||||||
appName={data?.appName || ""}
|
appName={data?.appName || ""}
|
||||||
serverId={data?.serverId || ""}
|
serverId={data?.serverId || ""}
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const ShowGeneralMongo = ({ mongoId }: 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(
|
const { data, refetch } = api.mongo.one.useQuery(
|
||||||
{
|
{
|
||||||
mongoId,
|
mongoId,
|
||||||
@@ -73,153 +75,158 @@ export const ShowGeneralMongo = ({ mongoId }: Props) => {
|
|||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="flex flex-row gap-4 flex-wrap">
|
<CardContent className="flex flex-row gap-4 flex-wrap">
|
||||||
<TooltipProvider delayDuration={0}>
|
<TooltipProvider delayDuration={0}>
|
||||||
<DialogAction
|
{canDeploy && (
|
||||||
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" ? (
|
|
||||||
<DialogAction
|
<DialogAction
|
||||||
title="Start Mongo"
|
title="Deploy Mongo"
|
||||||
description="Are you sure you want to start this mongo?"
|
description="Are you sure you want to deploy this mongo?"
|
||||||
type="default"
|
type="default"
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
await start({
|
setIsDeploying(true);
|
||||||
mongoId: mongoId,
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||||
})
|
refetch();
|
||||||
.then(() => {
|
|
||||||
toast.success("Mongo started successfully");
|
|
||||||
refetch();
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
toast.error("Error starting Mongo");
|
|
||||||
});
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
variant="secondary"
|
variant="default"
|
||||||
isLoading={isStarting}
|
isLoading={data?.applicationStatus === "running"}
|
||||||
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||||
>
|
>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<CheckCircle2 className="size-4 mr-1" />
|
<Rocket className="size-4 mr-1" />
|
||||||
Start
|
Deploy
|
||||||
</div>
|
</div>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipPrimitive.Portal>
|
<TooltipPrimitive.Portal>
|
||||||
<TooltipContent sideOffset={5} className="z-[60]">
|
<TooltipContent sideOffset={5} className="z-[60]">
|
||||||
<p>
|
<p>Downloads and sets up the MongoDB database</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>
|
</TooltipContent>
|
||||||
</TooltipPrimitive.Portal>
|
</TooltipPrimitive.Portal>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Button>
|
</Button>
|
||||||
</DialogAction>
|
</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>
|
</TooltipProvider>
|
||||||
<DockerTerminalModal
|
<DockerTerminalModal
|
||||||
appName={data?.appName || ""}
|
appName={data?.appName || ""}
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const ShowGeneralMysql = ({ mysqlId }: 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(
|
const { data, refetch } = api.mysql.one.useQuery(
|
||||||
{
|
{
|
||||||
mysqlId,
|
mysqlId,
|
||||||
@@ -71,153 +73,158 @@ export const ShowGeneralMysql = ({ mysqlId }: Props) => {
|
|||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="flex flex-row gap-4 flex-wrap">
|
<CardContent className="flex flex-row gap-4 flex-wrap">
|
||||||
<TooltipProvider delayDuration={0}>
|
<TooltipProvider delayDuration={0}>
|
||||||
<DialogAction
|
{canDeploy && (
|
||||||
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" ? (
|
|
||||||
<DialogAction
|
<DialogAction
|
||||||
title="Start MySQL"
|
title="Deploy MySQL"
|
||||||
description="Are you sure you want to start this mysql?"
|
description="Are you sure you want to deploy this mysql?"
|
||||||
type="default"
|
type="default"
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
await start({
|
setIsDeploying(true);
|
||||||
mysqlId: mysqlId,
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||||
})
|
refetch();
|
||||||
.then(() => {
|
|
||||||
toast.success("MySQL started successfully");
|
|
||||||
refetch();
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
toast.error("Error starting MySQL");
|
|
||||||
});
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
variant="secondary"
|
variant="default"
|
||||||
isLoading={isStarting}
|
isLoading={data?.applicationStatus === "running"}
|
||||||
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||||
>
|
>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<CheckCircle2 className="size-4 mr-1" />
|
<Rocket className="size-4 mr-1" />
|
||||||
Start
|
Deploy
|
||||||
</div>
|
</div>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipPrimitive.Portal>
|
<TooltipPrimitive.Portal>
|
||||||
<TooltipContent sideOffset={5} className="z-[60]">
|
<TooltipContent sideOffset={5} className="z-[60]">
|
||||||
<p>
|
<p>Downloads and sets up the MySQL database</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>
|
</TooltipContent>
|
||||||
</TooltipPrimitive.Portal>
|
</TooltipPrimitive.Portal>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Button>
|
</Button>
|
||||||
</DialogAction>
|
</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>
|
</TooltipProvider>
|
||||||
<DockerTerminalModal
|
<DockerTerminalModal
|
||||||
appName={data?.appName || ""}
|
appName={data?.appName || ""}
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const ShowGeneralPostgres = ({ postgresId }: 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(
|
const { data, refetch } = api.postgres.one.useQuery(
|
||||||
{
|
{
|
||||||
postgresId: postgresId,
|
postgresId: postgresId,
|
||||||
@@ -73,153 +75,162 @@ export const ShowGeneralPostgres = ({ postgresId }: Props) => {
|
|||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="flex flex-row gap-4 flex-wrap">
|
<CardContent className="flex flex-row gap-4 flex-wrap">
|
||||||
<TooltipProvider disableHoverableContent={false}>
|
<TooltipProvider disableHoverableContent={false}>
|
||||||
<DialogAction
|
{canDeploy && (
|
||||||
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" ? (
|
|
||||||
<DialogAction
|
<DialogAction
|
||||||
title="Start PostgreSQL"
|
title="Deploy PostgreSQL"
|
||||||
description="Are you sure you want to start this postgres?"
|
description="Are you sure you want to deploy this postgres?"
|
||||||
type="default"
|
type="default"
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
await start({
|
setIsDeploying(true);
|
||||||
postgresId: postgresId,
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||||
})
|
refetch();
|
||||||
.then(() => {
|
|
||||||
toast.success("PostgreSQL started successfully");
|
|
||||||
refetch();
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
toast.error("Error starting PostgreSQL");
|
|
||||||
});
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
variant="secondary"
|
variant="default"
|
||||||
isLoading={isStarting}
|
isLoading={data?.applicationStatus === "running"}
|
||||||
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||||
>
|
>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<CheckCircle2 className="size-4 mr-1" />
|
<Rocket className="size-4 mr-1" />
|
||||||
Start
|
Deploy
|
||||||
</div>
|
</div>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipPrimitive.Portal>
|
<TooltipPrimitive.Portal>
|
||||||
<TooltipContent sideOffset={5} className="z-[60]">
|
<TooltipContent sideOffset={5} className="z-[60]">
|
||||||
<p>
|
<p>Downloads and sets up the PostgreSQL database</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>
|
</TooltipContent>
|
||||||
</TooltipPrimitive.Portal>
|
</TooltipPrimitive.Portal>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Button>
|
</Button>
|
||||||
</DialogAction>
|
</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>
|
</TooltipProvider>
|
||||||
<DockerTerminalModal
|
<DockerTerminalModal
|
||||||
appName={data?.appName || ""}
|
appName={data?.appName || ""}
|
||||||
|
|||||||
@@ -57,19 +57,13 @@ export const AdvancedEnvironmentSelector = ({
|
|||||||
const [description, setDescription] = useState("");
|
const [description, setDescription] = useState("");
|
||||||
|
|
||||||
// Get current user's permissions
|
// 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
|
// Check if user can create environments
|
||||||
const canCreateEnvironments =
|
const canCreateEnvironments = !!permissions?.environment.create;
|
||||||
currentUser?.role === "owner" ||
|
|
||||||
currentUser?.role === "admin" ||
|
|
||||||
currentUser?.canCreateEnvironments === true;
|
|
||||||
|
|
||||||
// Check if user can delete environments
|
// Check if user can delete environments
|
||||||
const canDeleteEnvironments =
|
const canDeleteEnvironments = !!permissions?.environment.delete;
|
||||||
currentUser?.role === "owner" ||
|
|
||||||
currentUser?.role === "admin" ||
|
|
||||||
currentUser?.canDeleteEnvironments === true;
|
|
||||||
|
|
||||||
const haveServices =
|
const haveServices =
|
||||||
selectedEnvironment &&
|
selectedEnvironment &&
|
||||||
|
|||||||
@@ -39,6 +39,9 @@ interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const EnvironmentVariables = ({ environmentId, children }: 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 [isOpen, setIsOpen] = useState(false);
|
||||||
const utils = api.useUtils();
|
const utils = api.useUtils();
|
||||||
const { mutateAsync, error, isError, isPending } =
|
const { mutateAsync, error, isError, isPending } =
|
||||||
@@ -97,6 +100,10 @@ export const EnvironmentVariables = ({ environmentId, children }: Props) => {
|
|||||||
};
|
};
|
||||||
}, [form, onSubmit, isPending, isOpen]);
|
}, [form, onSubmit, isPending, isOpen]);
|
||||||
|
|
||||||
|
if (!canRead) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
@@ -141,6 +148,7 @@ export const EnvironmentVariables = ({ environmentId, children }: Props) => {
|
|||||||
<CodeEditor
|
<CodeEditor
|
||||||
lineWrapping
|
lineWrapping
|
||||||
language="properties"
|
language="properties"
|
||||||
|
readOnly={!canWrite}
|
||||||
wrapperClassName="h-[35rem] font-mono"
|
wrapperClassName="h-[35rem] font-mono"
|
||||||
placeholder={`NODE_ENV=development
|
placeholder={`NODE_ENV=development
|
||||||
DATABASE_URL=postgresql://localhost:5432/mydb
|
DATABASE_URL=postgresql://localhost:5432/mydb
|
||||||
@@ -157,11 +165,13 @@ API_KEY=your-api-key-here
|
|||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<DialogFooter>
|
{canWrite && (
|
||||||
<Button isLoading={isPending} type="submit">
|
<DialogFooter>
|
||||||
Update
|
<Button isLoading={isPending} type="submit">
|
||||||
</Button>
|
Update
|
||||||
</DialogFooter>
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
)}
|
||||||
</form>
|
</form>
|
||||||
</Form>
|
</Form>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -39,6 +39,9 @@ interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const ProjectEnvironment = ({ projectId, children }: 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 [isOpen, setIsOpen] = useState(false);
|
||||||
const utils = api.useUtils();
|
const utils = api.useUtils();
|
||||||
const { mutateAsync, error, isError, isPending } =
|
const { mutateAsync, error, isError, isPending } =
|
||||||
@@ -96,6 +99,10 @@ export const ProjectEnvironment = ({ projectId, children }: Props) => {
|
|||||||
};
|
};
|
||||||
}, [form, onSubmit, isPending, isOpen]);
|
}, [form, onSubmit, isPending, isOpen]);
|
||||||
|
|
||||||
|
if (!canRead) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
@@ -139,6 +146,7 @@ export const ProjectEnvironment = ({ projectId, children }: Props) => {
|
|||||||
<CodeEditor
|
<CodeEditor
|
||||||
lineWrapping
|
lineWrapping
|
||||||
language="properties"
|
language="properties"
|
||||||
|
readOnly={!canWrite}
|
||||||
wrapperClassName="h-[35rem] font-mono"
|
wrapperClassName="h-[35rem] font-mono"
|
||||||
placeholder={`NODE_ENV=production
|
placeholder={`NODE_ENV=production
|
||||||
PORT=3000
|
PORT=3000
|
||||||
@@ -154,11 +162,13 @@ PORT=3000
|
|||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<DialogFooter>
|
{canWrite && (
|
||||||
<Button isLoading={isPending} type="submit">
|
<DialogFooter>
|
||||||
Update
|
<Button isLoading={isPending} type="submit">
|
||||||
</Button>
|
Update
|
||||||
</DialogFooter>
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
)}
|
||||||
</form>
|
</form>
|
||||||
</Form>
|
</Form>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -61,6 +61,7 @@ export const ShowProjects = () => {
|
|||||||
const { data: isCloud } = api.settings.isCloud.useQuery();
|
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||||
const { data, isPending } = api.project.all.useQuery();
|
const { data, isPending } = api.project.all.useQuery();
|
||||||
const { data: auth } = api.user.get.useQuery();
|
const { data: auth } = api.user.get.useQuery();
|
||||||
|
const { data: permissions } = api.user.getPermissions.useQuery();
|
||||||
const { mutateAsync } = api.project.remove.useMutation();
|
const { mutateAsync } = api.project.remove.useMutation();
|
||||||
|
|
||||||
const [searchQuery, setSearchQuery] = useState(
|
const [searchQuery, setSearchQuery] = useState(
|
||||||
@@ -168,11 +169,6 @@ export const ShowProjects = () => {
|
|||||||
<BreadcrumbSidebar
|
<BreadcrumbSidebar
|
||||||
list={[{ name: "Projects", href: "/dashboard/projects" }]}
|
list={[{ name: "Projects", href: "/dashboard/projects" }]}
|
||||||
/>
|
/>
|
||||||
{!isCloud && (
|
|
||||||
<div className="absolute top-4 right-4">
|
|
||||||
<TimeBadge />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
<Card className="h-full bg-sidebar p-2.5 rounded-xl ">
|
<Card className="h-full bg-sidebar p-2.5 rounded-xl ">
|
||||||
<div className="rounded-xl bg-background shadow-md ">
|
<div className="rounded-xl bg-background shadow-md ">
|
||||||
@@ -186,9 +182,7 @@ export const ShowProjects = () => {
|
|||||||
Create and manage your projects
|
Create and manage your projects
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
{(auth?.role === "owner" ||
|
{permissions?.project.create && (
|
||||||
auth?.role === "admin" ||
|
|
||||||
auth?.canCreateProjects) && (
|
|
||||||
<div className="">
|
<div className="">
|
||||||
<HandleProject />
|
<HandleProject />
|
||||||
</div>
|
</div>
|
||||||
@@ -361,8 +355,7 @@ export const ShowProjects = () => {
|
|||||||
<div
|
<div
|
||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
>
|
>
|
||||||
{(auth?.role === "owner" ||
|
{permissions?.project.delete && (
|
||||||
auth?.canDeleteProjects) && (
|
|
||||||
<AlertDialog>
|
<AlertDialog>
|
||||||
<AlertDialogTrigger className="w-full">
|
<AlertDialogTrigger className="w-full">
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const ShowGeneralRedis = ({ redisId }: 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(
|
const { data, refetch } = api.redis.one.useQuery(
|
||||||
{
|
{
|
||||||
redisId,
|
redisId,
|
||||||
@@ -72,153 +74,158 @@ export const ShowGeneralRedis = ({ redisId }: Props) => {
|
|||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="flex flex-row gap-4 flex-wrap">
|
<CardContent className="flex flex-row gap-4 flex-wrap">
|
||||||
<TooltipProvider delayDuration={0}>
|
<TooltipProvider delayDuration={0}>
|
||||||
<DialogAction
|
{canDeploy && (
|
||||||
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" ? (
|
|
||||||
<DialogAction
|
<DialogAction
|
||||||
title="Start Redis"
|
title="Deploy Redis"
|
||||||
description="Are you sure you want to start this redis?"
|
description="Are you sure you want to deploy this redis?"
|
||||||
type="default"
|
type="default"
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
await start({
|
setIsDeploying(true);
|
||||||
redisId: redisId,
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||||
})
|
refetch();
|
||||||
.then(() => {
|
|
||||||
toast.success("Redis started successfully");
|
|
||||||
refetch();
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
toast.error("Error starting Redis");
|
|
||||||
});
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
variant="secondary"
|
variant="default"
|
||||||
isLoading={isStarting}
|
isLoading={data?.applicationStatus === "running"}
|
||||||
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||||
>
|
>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<CheckCircle2 className="size-4 mr-1" />
|
<Rocket className="size-4 mr-1" />
|
||||||
Start
|
Deploy
|
||||||
</div>
|
</div>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipPrimitive.Portal>
|
<TooltipPrimitive.Portal>
|
||||||
<TooltipContent sideOffset={5} className="z-[60]">
|
<TooltipContent sideOffset={5} className="z-[60]">
|
||||||
<p>
|
<p>Downloads and sets up the Redis database</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>
|
</TooltipContent>
|
||||||
</TooltipPrimitive.Portal>
|
</TooltipPrimitive.Portal>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Button>
|
</Button>
|
||||||
</DialogAction>
|
</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>
|
</TooltipProvider>
|
||||||
<DockerTerminalModal
|
<DockerTerminalModal
|
||||||
appName={data?.appName || ""}
|
appName={data?.appName || ""}
|
||||||
|
|||||||
@@ -91,7 +91,10 @@ export const ShowBilling = () => {
|
|||||||
api.stripe.upgradeSubscription.useMutation();
|
api.stripe.upgradeSubscription.useMutation();
|
||||||
const utils = api.useUtils();
|
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 [isAnnual, setIsAnnual] = useState(false);
|
||||||
const [upgradeTier, setUpgradeTier] = useState<"hobby" | "startup" | null>(
|
const [upgradeTier, setUpgradeTier] = useState<"hobby" | "startup" | null>(
|
||||||
null,
|
null,
|
||||||
@@ -111,6 +114,12 @@ export const ShowBilling = () => {
|
|||||||
productId: string,
|
productId: string,
|
||||||
) => {
|
) => {
|
||||||
const stripe = await stripePromise;
|
const stripe = await stripePromise;
|
||||||
|
const serverQuantity =
|
||||||
|
tier === "startup"
|
||||||
|
? startupServerQuantity
|
||||||
|
: tier === "hobby"
|
||||||
|
? hobbyServerQuantity
|
||||||
|
: hobbyServerQuantity;
|
||||||
if (data && data.subscriptions.length === 0) {
|
if (data && data.subscriptions.length === 0) {
|
||||||
createCheckoutSession({
|
createCheckoutSession({
|
||||||
tier,
|
tier,
|
||||||
@@ -679,7 +688,7 @@ export const ShowBilling = () => {
|
|||||||
<p className="text-2xl font-semibold text-foreground">
|
<p className="text-2xl font-semibold text-foreground">
|
||||||
$
|
$
|
||||||
{calculatePriceHobby(
|
{calculatePriceHobby(
|
||||||
serverQuantity,
|
hobbyServerQuantity,
|
||||||
isAnnual,
|
isAnnual,
|
||||||
).toFixed(2)}
|
).toFixed(2)}
|
||||||
/{isAnnual ? "yr" : "mo"}
|
/{isAnnual ? "yr" : "mo"}
|
||||||
@@ -692,7 +701,8 @@ export const ShowBilling = () => {
|
|||||||
<p className="text-xs text-muted-foreground mt-2">
|
<p className="text-xs text-muted-foreground mt-2">
|
||||||
$
|
$
|
||||||
{(
|
{(
|
||||||
calculatePriceHobby(serverQuantity, true) / 12
|
calculatePriceHobby(hobbyServerQuantity, true) /
|
||||||
|
12
|
||||||
).toFixed(2)}
|
).toFixed(2)}
|
||||||
/mo
|
/mo
|
||||||
</p>
|
</p>
|
||||||
@@ -724,19 +734,19 @@ export const ShowBilling = () => {
|
|||||||
Servers:
|
Servers:
|
||||||
</span>
|
</span>
|
||||||
<Button
|
<Button
|
||||||
disabled={serverQuantity <= 1}
|
disabled={hobbyServerQuantity <= 1}
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="icon"
|
size="icon"
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
setServerQuantity((q) => Math.max(1, q - 1))
|
setHobbyServerQuantity((q) => Math.max(1, q - 1))
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<MinusIcon className="h-4 w-4" />
|
<MinusIcon className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
<NumberInput
|
<NumberInput
|
||||||
value={serverQuantity}
|
value={hobbyServerQuantity}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
setServerQuantity(
|
setHobbyServerQuantity(
|
||||||
Math.max(
|
Math.max(
|
||||||
1,
|
1,
|
||||||
Number(
|
Number(
|
||||||
@@ -750,7 +760,7 @@ export const ShowBilling = () => {
|
|||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="icon"
|
size="icon"
|
||||||
onClick={() => setServerQuantity((q) => q + 1)}
|
onClick={() => setHobbyServerQuantity((q) => q + 1)}
|
||||||
>
|
>
|
||||||
<PlusIcon className="h-4 w-4" />
|
<PlusIcon className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
@@ -775,7 +785,7 @@ export const ShowBilling = () => {
|
|||||||
onClick={() =>
|
onClick={() =>
|
||||||
handleCheckout("hobby", data!.hobbyProductId!)
|
handleCheckout("hobby", data!.hobbyProductId!)
|
||||||
}
|
}
|
||||||
disabled={serverQuantity < 1}
|
disabled={hobbyServerQuantity < 1}
|
||||||
>
|
>
|
||||||
Get Started
|
Get Started
|
||||||
</Button>
|
</Button>
|
||||||
@@ -806,7 +816,7 @@ export const ShowBilling = () => {
|
|||||||
<p className="text-2xl font-semibold text-foreground">
|
<p className="text-2xl font-semibold text-foreground">
|
||||||
$
|
$
|
||||||
{calculatePriceStartup(
|
{calculatePriceStartup(
|
||||||
serverQuantity,
|
startupServerQuantity,
|
||||||
isAnnual,
|
isAnnual,
|
||||||
).toFixed(2)}
|
).toFixed(2)}
|
||||||
/{isAnnual ? "yr" : "mo"}
|
/{isAnnual ? "yr" : "mo"}
|
||||||
@@ -819,7 +829,10 @@ export const ShowBilling = () => {
|
|||||||
<p className="text-xs text-muted-foreground mt-2">
|
<p className="text-xs text-muted-foreground mt-2">
|
||||||
$
|
$
|
||||||
{(
|
{(
|
||||||
calculatePriceStartup(serverQuantity, true) / 12
|
calculatePriceStartup(
|
||||||
|
startupServerQuantity,
|
||||||
|
true,
|
||||||
|
) / 12
|
||||||
).toFixed(2)}
|
).toFixed(2)}
|
||||||
/mo
|
/mo
|
||||||
</p>
|
</p>
|
||||||
@@ -856,13 +869,14 @@ export const ShowBilling = () => {
|
|||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Button
|
<Button
|
||||||
disabled={
|
disabled={
|
||||||
serverQuantity <= STARTUP_SERVERS_INCLUDED
|
startupServerQuantity <=
|
||||||
|
STARTUP_SERVERS_INCLUDED
|
||||||
}
|
}
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="icon"
|
size="icon"
|
||||||
className="h-8 w-8"
|
className="h-8 w-8"
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
setServerQuantity((q) =>
|
setStartupServerQuantity((q) =>
|
||||||
Math.max(STARTUP_SERVERS_INCLUDED, q - 1),
|
Math.max(STARTUP_SERVERS_INCLUDED, q - 1),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -870,9 +884,9 @@ export const ShowBilling = () => {
|
|||||||
<MinusIcon className="h-4 w-4" />
|
<MinusIcon className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
<NumberInput
|
<NumberInput
|
||||||
value={serverQuantity}
|
value={startupServerQuantity}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
setServerQuantity(
|
setStartupServerQuantity(
|
||||||
Math.max(
|
Math.max(
|
||||||
STARTUP_SERVERS_INCLUDED,
|
STARTUP_SERVERS_INCLUDED,
|
||||||
Number(
|
Number(
|
||||||
@@ -887,7 +901,9 @@ export const ShowBilling = () => {
|
|||||||
variant="outline"
|
variant="outline"
|
||||||
size="icon"
|
size="icon"
|
||||||
className="h-8 w-8"
|
className="h-8 w-8"
|
||||||
onClick={() => setServerQuantity((q) => q + 1)}
|
onClick={() =>
|
||||||
|
setStartupServerQuantity((q) => q + 1)
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<PlusIcon className="h-4 w-4" />
|
<PlusIcon className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
@@ -917,7 +933,7 @@ export const ShowBilling = () => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
disabled={
|
disabled={
|
||||||
serverQuantity < STARTUP_SERVERS_INCLUDED
|
startupServerQuantity < STARTUP_SERVERS_INCLUDED
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
Get Started
|
Get Started
|
||||||
@@ -1009,7 +1025,7 @@ export const ShowBilling = () => {
|
|||||||
<p className="text-2xl font-semibold tracking-tight text-primary ">
|
<p className="text-2xl font-semibold tracking-tight text-primary ">
|
||||||
${" "}
|
${" "}
|
||||||
{calculatePrice(
|
{calculatePrice(
|
||||||
serverQuantity,
|
hobbyServerQuantity,
|
||||||
isAnnual,
|
isAnnual,
|
||||||
).toFixed(2)}{" "}
|
).toFixed(2)}{" "}
|
||||||
USD
|
USD
|
||||||
@@ -1018,7 +1034,10 @@ export const ShowBilling = () => {
|
|||||||
<p className="text-base font-semibold tracking-tight text-muted-foreground">
|
<p className="text-base font-semibold tracking-tight text-muted-foreground">
|
||||||
${" "}
|
${" "}
|
||||||
{(
|
{(
|
||||||
calculatePrice(serverQuantity, isAnnual) / 12
|
calculatePrice(
|
||||||
|
hobbyServerQuantity,
|
||||||
|
isAnnual,
|
||||||
|
) / 12
|
||||||
).toFixed(2)}{" "}
|
).toFixed(2)}{" "}
|
||||||
/ Month USD
|
/ Month USD
|
||||||
</p>
|
</p>
|
||||||
@@ -1026,9 +1045,10 @@ export const ShowBilling = () => {
|
|||||||
) : (
|
) : (
|
||||||
<p className="text-2xl font-semibold tracking-tight text-primary ">
|
<p className="text-2xl font-semibold tracking-tight text-primary ">
|
||||||
${" "}
|
${" "}
|
||||||
{calculatePrice(serverQuantity, isAnnual).toFixed(
|
{calculatePrice(
|
||||||
2,
|
hobbyServerQuantity,
|
||||||
)}{" "}
|
isAnnual,
|
||||||
|
).toFixed(2)}{" "}
|
||||||
USD
|
USD
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
@@ -1071,26 +1091,28 @@ export const ShowBilling = () => {
|
|||||||
<div className="flex flex-col gap-2 mt-4">
|
<div className="flex flex-col gap-2 mt-4">
|
||||||
<div className="flex items-center gap-2 justify-center">
|
<div className="flex items-center gap-2 justify-center">
|
||||||
<span className="text-sm text-muted-foreground">
|
<span className="text-sm text-muted-foreground">
|
||||||
{serverQuantity} Servers
|
{hobbyServerQuantity} Servers
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<Button
|
<Button
|
||||||
disabled={serverQuantity <= 1}
|
disabled={hobbyServerQuantity <= 1}
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (serverQuantity <= 1) return;
|
if (hobbyServerQuantity <= 1) return;
|
||||||
|
|
||||||
setServerQuantity(serverQuantity - 1);
|
setHobbyServerQuantity(
|
||||||
|
hobbyServerQuantity - 1,
|
||||||
|
);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<MinusIcon className="h-4 w-4" />
|
<MinusIcon className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
<NumberInput
|
<NumberInput
|
||||||
value={serverQuantity}
|
value={hobbyServerQuantity}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
setServerQuantity(
|
setHobbyServerQuantity(
|
||||||
e.target.value as unknown as number,
|
e.target.value as unknown as number,
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
@@ -1099,7 +1121,9 @@ export const ShowBilling = () => {
|
|||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setServerQuantity(serverQuantity + 1);
|
setHobbyServerQuantity(
|
||||||
|
hobbyServerQuantity + 1,
|
||||||
|
);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<PlusIcon className="h-4 w-4" />
|
<PlusIcon className="h-4 w-4" />
|
||||||
@@ -1125,7 +1149,7 @@ export const ShowBilling = () => {
|
|||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
handleCheckout("legacy", product.id);
|
handleCheckout("legacy", product.id);
|
||||||
}}
|
}}
|
||||||
disabled={serverQuantity < 1}
|
disabled={hobbyServerQuantity < 1}
|
||||||
>
|
>
|
||||||
Subscribe
|
Subscribe
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ export const ShowCertificates = () => {
|
|||||||
const { mutateAsync, isPending: isRemoving } =
|
const { mutateAsync, isPending: isRemoving } =
|
||||||
api.certificates.remove.useMutation();
|
api.certificates.remove.useMutation();
|
||||||
const { data, isPending, refetch } = api.certificates.all.useQuery();
|
const { data, isPending, refetch } = api.certificates.all.useQuery();
|
||||||
|
const { data: permissions } = api.user.getPermissions.useQuery();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
@@ -53,7 +54,7 @@ export const ShowCertificates = () => {
|
|||||||
<span className="text-base text-muted-foreground text-center">
|
<span className="text-base text-muted-foreground text-center">
|
||||||
You don't have any certificates created
|
You don't have any certificates created
|
||||||
</span>
|
</span>
|
||||||
<AddCertificate />
|
{permissions?.certificate.create && <AddCertificate />}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex flex-col gap-4 min-h-[25vh]">
|
<div className="flex flex-col gap-4 min-h-[25vh]">
|
||||||
@@ -101,47 +102,52 @@ export const ShowCertificates = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-row gap-1">
|
{permissions?.certificate.delete && (
|
||||||
<DialogAction
|
<div className="flex flex-row gap-1">
|
||||||
title="Delete Certificate"
|
<DialogAction
|
||||||
description="Are you sure you want to delete this certificate?"
|
title="Delete Certificate"
|
||||||
type="destructive"
|
description="Are you sure you want to delete this certificate?"
|
||||||
onClick={async () => {
|
type="destructive"
|
||||||
await mutateAsync({
|
onClick={async () => {
|
||||||
certificateId: certificate.certificateId,
|
await mutateAsync({
|
||||||
})
|
certificateId:
|
||||||
.then(() => {
|
certificate.certificateId,
|
||||||
toast.success(
|
|
||||||
"Certificate deleted successfully",
|
|
||||||
);
|
|
||||||
refetch();
|
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.then(() => {
|
||||||
toast.error(
|
toast.success(
|
||||||
"Error deleting certificate",
|
"Certificate deleted successfully",
|
||||||
);
|
);
|
||||||
});
|
refetch();
|
||||||
}}
|
})
|
||||||
>
|
.catch(() => {
|
||||||
<Button
|
toast.error(
|
||||||
variant="ghost"
|
"Error deleting certificate",
|
||||||
size="icon"
|
);
|
||||||
className="group hover:bg-red-500/10 "
|
});
|
||||||
isLoading={isRemoving}
|
}}
|
||||||
>
|
>
|
||||||
<Trash2 className="size-4 text-primary group-hover:text-red-500" />
|
<Button
|
||||||
</Button>
|
variant="ghost"
|
||||||
</DialogAction>
|
size="icon"
|
||||||
</div>
|
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>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-row gap-2 flex-wrap w-full justify-end mr-4">
|
{permissions?.certificate.create && (
|
||||||
<AddCertificate />
|
<div className="flex flex-row gap-2 flex-wrap w-full justify-end mr-4">
|
||||||
</div>
|
<AddCertificate />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ export const ShowRegistry = () => {
|
|||||||
const { mutateAsync, isPending: isRemoving } =
|
const { mutateAsync, isPending: isRemoving } =
|
||||||
api.registry.remove.useMutation();
|
api.registry.remove.useMutation();
|
||||||
const { data, isPending, refetch } = api.registry.all.useQuery();
|
const { data, isPending, refetch } = api.registry.all.useQuery();
|
||||||
|
const { data: permissions } = api.user.getPermissions.useQuery();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
@@ -44,7 +45,7 @@ export const ShowRegistry = () => {
|
|||||||
<span className="text-base text-muted-foreground text-center">
|
<span className="text-base text-muted-foreground text-center">
|
||||||
You don't have any registry configurations
|
You don't have any registry configurations
|
||||||
</span>
|
</span>
|
||||||
<HandleRegistry />
|
{permissions?.registry.create && <HandleRegistry />}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex flex-col gap-4 min-h-[25vh]">
|
<div className="flex flex-col gap-4 min-h-[25vh]">
|
||||||
@@ -73,45 +74,49 @@ export const ShowRegistry = () => {
|
|||||||
registryId={registry.registryId}
|
registryId={registry.registryId}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<DialogAction
|
{permissions?.registry.delete && (
|
||||||
title="Delete Registry"
|
<DialogAction
|
||||||
description="Are you sure you want to delete this registry configuration?"
|
title="Delete Registry"
|
||||||
type="destructive"
|
description="Are you sure you want to delete this registry configuration?"
|
||||||
onClick={async () => {
|
type="destructive"
|
||||||
await mutateAsync({
|
onClick={async () => {
|
||||||
registryId: registry.registryId,
|
await mutateAsync({
|
||||||
})
|
registryId: registry.registryId,
|
||||||
.then(() => {
|
|
||||||
toast.success(
|
|
||||||
"Registry configuration deleted successfully",
|
|
||||||
);
|
|
||||||
refetch();
|
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.then(() => {
|
||||||
toast.error(
|
toast.success(
|
||||||
"Error deleting registry configuration",
|
"Registry configuration deleted successfully",
|
||||||
);
|
);
|
||||||
});
|
refetch();
|
||||||
}}
|
})
|
||||||
>
|
.catch(() => {
|
||||||
<Button
|
toast.error(
|
||||||
variant="ghost"
|
"Error deleting registry configuration",
|
||||||
size="icon"
|
);
|
||||||
className="group hover:bg-red-500/10 "
|
});
|
||||||
isLoading={isRemoving}
|
}}
|
||||||
>
|
>
|
||||||
<Trash2 className="size-4 text-primary group-hover:text-red-500" />
|
<Button
|
||||||
</Button>
|
variant="ghost"
|
||||||
</DialogAction>
|
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>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-row gap-2 flex-wrap w-full justify-end mr-4">
|
{permissions?.registry.create && (
|
||||||
<HandleRegistry />
|
<div className="flex flex-row gap-2 flex-wrap w-full justify-end mr-4">
|
||||||
</div>
|
<HandleRegistry />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ export const ShowDestinations = () => {
|
|||||||
const { data, isPending, refetch } = api.destination.all.useQuery();
|
const { data, isPending, refetch } = api.destination.all.useQuery();
|
||||||
const { mutateAsync, isPending: isRemoving } =
|
const { mutateAsync, isPending: isRemoving } =
|
||||||
api.destination.remove.useMutation();
|
api.destination.remove.useMutation();
|
||||||
|
const { data: permissions } = api.user.getPermissions.useQuery();
|
||||||
return (
|
return (
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
<Card className="h-full bg-sidebar p-2.5 rounded-xl max-w-5xl mx-auto">
|
<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
|
To create a backup it is required to set at least 1
|
||||||
provider.
|
provider.
|
||||||
</span>
|
</span>
|
||||||
<HandleDestinations />
|
{permissions?.destination.create && <HandleDestinations />}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex flex-col gap-4 min-h-[25vh]">
|
<div className="flex flex-col gap-4 min-h-[25vh]">
|
||||||
@@ -71,43 +72,49 @@ export const ShowDestinations = () => {
|
|||||||
<HandleDestinations
|
<HandleDestinations
|
||||||
destinationId={destination.destinationId}
|
destinationId={destination.destinationId}
|
||||||
/>
|
/>
|
||||||
<DialogAction
|
{permissions?.destination.delete && (
|
||||||
title="Delete Destination"
|
<DialogAction
|
||||||
description="Are you sure you want to delete this destination?"
|
title="Delete Destination"
|
||||||
type="destructive"
|
description="Are you sure you want to delete this destination?"
|
||||||
onClick={async () => {
|
type="destructive"
|
||||||
await mutateAsync({
|
onClick={async () => {
|
||||||
destinationId: destination.destinationId,
|
await mutateAsync({
|
||||||
})
|
destinationId: destination.destinationId,
|
||||||
.then(() => {
|
|
||||||
toast.success(
|
|
||||||
"Destination deleted successfully",
|
|
||||||
);
|
|
||||||
refetch();
|
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.then(() => {
|
||||||
toast.error("Error deleting destination");
|
toast.success(
|
||||||
});
|
"Destination deleted successfully",
|
||||||
}}
|
);
|
||||||
>
|
refetch();
|
||||||
<Button
|
})
|
||||||
variant="ghost"
|
.catch(() => {
|
||||||
size="icon"
|
toast.error(
|
||||||
className="group hover:bg-red-500/10 "
|
"Error deleting destination",
|
||||||
isLoading={isRemoving}
|
);
|
||||||
|
});
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<Trash2 className="size-4 text-primary group-hover:text-red-500" />
|
<Button
|
||||||
</Button>
|
variant="ghost"
|
||||||
</DialogAction>
|
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>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-row gap-2 flex-wrap w-full justify-end mr-4">
|
{permissions?.destination.create && (
|
||||||
<HandleDestinations />
|
<div className="flex flex-row gap-2 flex-wrap w-full justify-end mr-4">
|
||||||
</div>
|
<HandleDestinations />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -737,6 +737,9 @@ export const HandleNotifications = ({ notificationId }: Props) => {
|
|||||||
});
|
});
|
||||||
setVisible(false);
|
setVisible(false);
|
||||||
await utils.notification.all.invalidate();
|
await utils.notification.all.invalidate();
|
||||||
|
if (notificationId) {
|
||||||
|
await utils.notification.one.invalidate({ notificationId });
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error(
|
toast.error(
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ export const ShowNotifications = () => {
|
|||||||
const { data, isPending, refetch } = api.notification.all.useQuery();
|
const { data, isPending, refetch } = api.notification.all.useQuery();
|
||||||
const { mutateAsync, isPending: isRemoving } =
|
const { mutateAsync, isPending: isRemoving } =
|
||||||
api.notification.remove.useMutation();
|
api.notification.remove.useMutation();
|
||||||
|
const { data: permissions } = api.user.getPermissions.useQuery();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
@@ -56,7 +57,9 @@ export const ShowNotifications = () => {
|
|||||||
To send notifications it is required to set at least 1
|
To send notifications it is required to set at least 1
|
||||||
provider.
|
provider.
|
||||||
</span>
|
</span>
|
||||||
<HandleNotifications />
|
{permissions?.notification.create && (
|
||||||
|
<HandleNotifications />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex flex-col gap-4 min-h-[25vh]">
|
<div className="flex flex-col gap-4 min-h-[25vh]">
|
||||||
@@ -126,45 +129,50 @@ export const ShowNotifications = () => {
|
|||||||
notificationId={notification.notificationId}
|
notificationId={notification.notificationId}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<DialogAction
|
{permissions?.notification.delete && (
|
||||||
title="Delete Notification"
|
<DialogAction
|
||||||
description="Are you sure you want to delete this notification?"
|
title="Delete Notification"
|
||||||
type="destructive"
|
description="Are you sure you want to delete this notification?"
|
||||||
onClick={async () => {
|
type="destructive"
|
||||||
await mutateAsync({
|
onClick={async () => {
|
||||||
notificationId: notification.notificationId,
|
await mutateAsync({
|
||||||
})
|
notificationId:
|
||||||
.then(() => {
|
notification.notificationId,
|
||||||
toast.success(
|
|
||||||
"Notification deleted successfully",
|
|
||||||
);
|
|
||||||
refetch();
|
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.then(() => {
|
||||||
toast.error(
|
toast.success(
|
||||||
"Error deleting notification",
|
"Notification deleted successfully",
|
||||||
);
|
);
|
||||||
});
|
refetch();
|
||||||
}}
|
})
|
||||||
>
|
.catch(() => {
|
||||||
<Button
|
toast.error(
|
||||||
variant="ghost"
|
"Error deleting notification",
|
||||||
size="icon"
|
);
|
||||||
className="group hover:bg-red-500/10 "
|
});
|
||||||
isLoading={isRemoving}
|
}}
|
||||||
>
|
>
|
||||||
<Trash2 className="size-4 text-primary group-hover:text-red-500" />
|
<Button
|
||||||
</Button>
|
variant="ghost"
|
||||||
</DialogAction>
|
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>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-row gap-2 flex-wrap w-full justify-end mr-4">
|
{permissions?.notification.create && (
|
||||||
<HandleNotifications />
|
<div className="flex flex-row gap-2 flex-wrap w-full justify-end mr-4">
|
||||||
</div>
|
<HandleNotifications />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -59,6 +59,7 @@ export const ShowServers = () => {
|
|||||||
const { data: isCloud } = api.settings.isCloud.useQuery();
|
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||||
const { data: canCreateMoreServers } =
|
const { data: canCreateMoreServers } =
|
||||||
api.stripe.canCreateMoreServers.useQuery();
|
api.stripe.canCreateMoreServers.useQuery();
|
||||||
|
const { data: permissions } = api.user.getPermissions.useQuery();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
@@ -115,7 +116,7 @@ export const ShowServers = () => {
|
|||||||
Start adding servers to deploy your applications
|
Start adding servers to deploy your applications
|
||||||
remotely.
|
remotely.
|
||||||
</span>
|
</span>
|
||||||
<HandleServers />
|
{permissions?.server.create && <HandleServers />}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex flex-col gap-4 min-h-[25vh]">
|
<div className="flex flex-col gap-4 min-h-[25vh]">
|
||||||
@@ -362,66 +363,71 @@ export const ShowServers = () => {
|
|||||||
|
|
||||||
<div className="flex-1" />
|
<div className="flex-1" />
|
||||||
|
|
||||||
<Tooltip>
|
{permissions?.server.delete && (
|
||||||
<TooltipTrigger asChild>
|
<Tooltip>
|
||||||
<div>
|
<TooltipTrigger asChild>
|
||||||
<DialogAction
|
<div>
|
||||||
disabled={!canDelete}
|
<DialogAction
|
||||||
title={
|
disabled={!canDelete}
|
||||||
canDelete
|
title={
|
||||||
? "Delete Server"
|
canDelete
|
||||||
: "Server has active services"
|
? "Delete Server"
|
||||||
}
|
: "Server has active services"
|
||||||
description={
|
}
|
||||||
canDelete ? (
|
description={
|
||||||
"This will delete the server and all associated data"
|
canDelete ? (
|
||||||
) : (
|
"This will delete the server and all associated data"
|
||||||
<div className="flex flex-col gap-2">
|
) : (
|
||||||
You can not delete this
|
<div className="flex flex-col gap-2">
|
||||||
server because it has
|
You can not delete this
|
||||||
active services.
|
server because it has
|
||||||
<AlertBlock type="warning">
|
active services.
|
||||||
You have active services
|
<AlertBlock type="warning">
|
||||||
associated with this
|
You have active
|
||||||
server, please delete
|
services associated
|
||||||
them first.
|
with this server,
|
||||||
</AlertBlock>
|
please delete them
|
||||||
</div>
|
first.
|
||||||
)
|
</AlertBlock>
|
||||||
}
|
</div>
|
||||||
onClick={async () => {
|
)
|
||||||
await mutateAsync({
|
}
|
||||||
serverId: server.serverId,
|
onClick={async () => {
|
||||||
})
|
await mutateAsync({
|
||||||
.then(() => {
|
serverId: server.serverId,
|
||||||
refetch();
|
|
||||||
toast.success(
|
|
||||||
`Server ${server.name} deleted successfully`,
|
|
||||||
);
|
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.then(() => {
|
||||||
toast.error(err.message);
|
refetch();
|
||||||
});
|
toast.success(
|
||||||
}}
|
`Server ${server.name} deleted successfully`,
|
||||||
>
|
);
|
||||||
<Button
|
})
|
||||||
variant="ghost"
|
.catch((err) => {
|
||||||
size="icon"
|
toast.error(
|
||||||
className={`h-9 w-9 ${canDelete ? "text-destructive hover:text-destructive hover:bg-destructive/10" : "text-muted-foreground hover:bg-muted"}`}
|
err.message,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<Trash2 className="h-4 w-4" />
|
<Button
|
||||||
</Button>
|
variant="ghost"
|
||||||
</DialogAction>
|
size="icon"
|
||||||
</div>
|
className={`h-9 w-9 ${canDelete ? "text-destructive hover:text-destructive hover:bg-destructive/10" : "text-muted-foreground hover:bg-muted"}`}
|
||||||
</TooltipTrigger>
|
>
|
||||||
<TooltipContent>
|
<Trash2 className="h-4 w-4" />
|
||||||
<p>
|
</Button>
|
||||||
{canDelete
|
</DialogAction>
|
||||||
? "Delete Server"
|
</div>
|
||||||
: "Cannot delete - has active services"}
|
</TooltipTrigger>
|
||||||
</p>
|
<TooltipContent>
|
||||||
</TooltipContent>
|
<p>
|
||||||
</Tooltip>
|
{canDelete
|
||||||
|
? "Delete Server"
|
||||||
|
: "Cannot delete - has active services"}
|
||||||
|
</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -431,13 +437,15 @@ export const ShowServers = () => {
|
|||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-row gap-2 flex-wrap w-full justify-end mt-4">
|
{permissions?.server.create && (
|
||||||
{data && data?.length > 0 && (
|
<div className="flex flex-row gap-2 flex-wrap w-full justify-end mt-4">
|
||||||
<div>
|
{data && data?.length > 0 && (
|
||||||
<HandleServers />
|
<div>
|
||||||
</div>
|
<HandleServers />
|
||||||
)}
|
</div>
|
||||||
</div>
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ export const ShowDestinations = () => {
|
|||||||
const { data, isPending, refetch } = api.sshKey.all.useQuery();
|
const { data, isPending, refetch } = api.sshKey.all.useQuery();
|
||||||
const { mutateAsync, isPending: isRemoving } =
|
const { mutateAsync, isPending: isRemoving } =
|
||||||
api.sshKey.remove.useMutation();
|
api.sshKey.remove.useMutation();
|
||||||
|
const { data: permissions } = api.user.getPermissions.useQuery();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
@@ -46,7 +47,7 @@ export const ShowDestinations = () => {
|
|||||||
<span className="text-base text-muted-foreground text-center">
|
<span className="text-base text-muted-foreground text-center">
|
||||||
You don't have any SSH keys
|
You don't have any SSH keys
|
||||||
</span>
|
</span>
|
||||||
<HandleSSHKeys />
|
{permissions?.sshKeys.create && <HandleSSHKeys />}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex flex-col gap-4 min-h-[25vh]">
|
<div className="flex flex-col gap-4 min-h-[25vh]">
|
||||||
@@ -84,43 +85,47 @@ export const ShowDestinations = () => {
|
|||||||
<div className="flex flex-row gap-1">
|
<div className="flex flex-row gap-1">
|
||||||
<HandleSSHKeys sshKeyId={sshKey.sshKeyId} />
|
<HandleSSHKeys sshKeyId={sshKey.sshKeyId} />
|
||||||
|
|
||||||
<DialogAction
|
{permissions?.sshKeys.delete && (
|
||||||
title="Delete SSH Key"
|
<DialogAction
|
||||||
description="Are you sure you want to delete this SSH Key?"
|
title="Delete SSH Key"
|
||||||
type="destructive"
|
description="Are you sure you want to delete this SSH Key?"
|
||||||
onClick={async () => {
|
type="destructive"
|
||||||
await mutateAsync({
|
onClick={async () => {
|
||||||
sshKeyId: sshKey.sshKeyId,
|
await mutateAsync({
|
||||||
})
|
sshKeyId: sshKey.sshKeyId,
|
||||||
.then(() => {
|
|
||||||
toast.success(
|
|
||||||
"SSH Key deleted successfully",
|
|
||||||
);
|
|
||||||
refetch();
|
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.then(() => {
|
||||||
toast.error("Error deleting SSH Key");
|
toast.success(
|
||||||
});
|
"SSH Key deleted successfully",
|
||||||
}}
|
);
|
||||||
>
|
refetch();
|
||||||
<Button
|
})
|
||||||
variant="ghost"
|
.catch(() => {
|
||||||
size="icon"
|
toast.error("Error deleting SSH Key");
|
||||||
className="group hover:bg-red-500/10 "
|
});
|
||||||
isLoading={isRemoving}
|
}}
|
||||||
>
|
>
|
||||||
<Trash2 className="size-4 text-primary group-hover:text-red-500" />
|
<Button
|
||||||
</Button>
|
variant="ghost"
|
||||||
</DialogAction>
|
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>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-row gap-2 flex-wrap w-full justify-end mr-4">
|
{permissions?.sshKeys.create && (
|
||||||
<HandleSSHKeys />
|
<div className="flex flex-row gap-2 flex-wrap w-full justify-end mr-4">
|
||||||
</div>
|
<HandleSSHKeys />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -32,7 +32,6 @@ import {
|
|||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from "@/components/ui/select";
|
} from "@/components/ui/select";
|
||||||
import { authClient } from "@/lib/auth-client";
|
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
|
|
||||||
const addInvitation = z.object({
|
const addInvitation = z.object({
|
||||||
@@ -40,7 +39,7 @@ const addInvitation = z.object({
|
|||||||
.string()
|
.string()
|
||||||
.min(1, "Email is required")
|
.min(1, "Email is required")
|
||||||
.email({ message: "Invalid email" }),
|
.email({ message: "Invalid email" }),
|
||||||
role: z.enum(["member", "admin"]),
|
role: z.string().min(1, "Role is required"),
|
||||||
notificationId: z.string().optional(),
|
notificationId: z.string().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -49,13 +48,14 @@ type AddInvitation = z.infer<typeof addInvitation>;
|
|||||||
export const AddInvitation = () => {
|
export const AddInvitation = () => {
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const utils = api.useUtils();
|
const utils = api.useUtils();
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
|
||||||
const { data: isCloud } = api.settings.isCloud.useQuery();
|
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||||
const { data: emailProviders } =
|
const { data: emailProviders } =
|
||||||
api.notification.getEmailProviders.useQuery();
|
api.notification.getEmailProviders.useQuery();
|
||||||
|
const { mutateAsync: inviteMember, isPending: isInviting } =
|
||||||
|
api.organization.inviteMember.useMutation();
|
||||||
const { mutateAsync: sendInvitation } = api.user.sendInvitation.useMutation();
|
const { mutateAsync: sendInvitation } = api.user.sendInvitation.useMutation();
|
||||||
|
const { data: customRoles } = api.customRole.all.useQuery();
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const { data: activeOrganization } = api.organization.active.useQuery();
|
|
||||||
|
|
||||||
const form = useForm<AddInvitation>({
|
const form = useForm<AddInvitation>({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
@@ -70,19 +70,15 @@ export const AddInvitation = () => {
|
|||||||
}, [form, form.formState.isSubmitSuccessful, form.reset]);
|
}, [form, form.formState.isSubmitSuccessful, form.reset]);
|
||||||
|
|
||||||
const onSubmit = async (data: AddInvitation) => {
|
const onSubmit = async (data: AddInvitation) => {
|
||||||
setIsLoading(true);
|
try {
|
||||||
const result = await authClient.organization.inviteMember({
|
const result = await inviteMember({
|
||||||
email: data.email.toLowerCase(),
|
email: data.email.toLowerCase(),
|
||||||
role: data.role,
|
role: data.role,
|
||||||
organizationId: activeOrganization?.id,
|
});
|
||||||
});
|
|
||||||
|
|
||||||
if (result.error) {
|
|
||||||
setError(result.error.message || "");
|
|
||||||
} else {
|
|
||||||
if (!isCloud && data.notificationId) {
|
if (!isCloud && data.notificationId) {
|
||||||
await sendInvitation({
|
await sendInvitation({
|
||||||
invitationId: result.data.id,
|
invitationId: result!.id,
|
||||||
notificationId: data.notificationId || "",
|
notificationId: data.notificationId || "",
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
@@ -96,10 +92,11 @@ export const AddInvitation = () => {
|
|||||||
}
|
}
|
||||||
setError(null);
|
setError(null);
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
|
} catch (error: any) {
|
||||||
|
setError(error.message || "Failed to create invitation");
|
||||||
}
|
}
|
||||||
|
|
||||||
utils.organization.allInvitations.invalidate();
|
utils.organization.allInvitations.invalidate();
|
||||||
setIsLoading(false);
|
|
||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={setOpen}>
|
<Dialog open={open} onOpenChange={setOpen}>
|
||||||
@@ -159,6 +156,11 @@ export const AddInvitation = () => {
|
|||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="member">Member</SelectItem>
|
<SelectItem value="member">Member</SelectItem>
|
||||||
<SelectItem value="admin">Admin</SelectItem>
|
<SelectItem value="admin">Admin</SelectItem>
|
||||||
|
{customRoles?.map((role) => (
|
||||||
|
<SelectItem key={role.role} value={role.role}>
|
||||||
|
{role.role}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
<FormDescription>
|
<FormDescription>
|
||||||
@@ -212,7 +214,7 @@ export const AddInvitation = () => {
|
|||||||
)}
|
)}
|
||||||
<DialogFooter className="flex w-full flex-row">
|
<DialogFooter className="flex w-full flex-row">
|
||||||
<Button
|
<Button
|
||||||
isLoading={isLoading}
|
isLoading={isInviting}
|
||||||
form="hook-form-add-invitation"
|
form="hook-form-add-invitation"
|
||||||
type="submit"
|
type="submit"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -173,9 +173,11 @@ type AddPermissions = z.infer<typeof addPermissions>;
|
|||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
userId: string;
|
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 [isOpen, setIsOpen] = useState(false);
|
||||||
const { data: projects } = api.project.allForPermissions.useQuery(undefined, {
|
const { data: projects } = api.project.allForPermissions.useQuery(undefined, {
|
||||||
enabled: isOpen,
|
enabled: isOpen,
|
||||||
@@ -284,226 +286,237 @@ export const AddUserPermissions = ({ userId }: Props) => {
|
|||||||
onSubmit={form.handleSubmit(onSubmit)}
|
onSubmit={form.handleSubmit(onSubmit)}
|
||||||
className="grid grid-cols-1 md:grid-cols-2 w-full gap-4"
|
className="grid grid-cols-1 md:grid-cols-2 w-full gap-4"
|
||||||
>
|
>
|
||||||
<FormField
|
{isCustomRole && (
|
||||||
control={form.control}
|
<div className="md:col-span-2 rounded-lg border p-3 bg-muted/50 text-sm text-muted-foreground">
|
||||||
name="canCreateProjects"
|
This user has a custom role assigned. Capabilities are defined
|
||||||
render={({ field }) => (
|
by the role. You can still manage which projects, environments,
|
||||||
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm">
|
and services they can access below.
|
||||||
<div className="space-y-0.5">
|
</div>
|
||||||
<FormLabel>Create Projects</FormLabel>
|
)}
|
||||||
<FormDescription>
|
{!isCustomRole && (
|
||||||
Allow the user to create projects
|
<>
|
||||||
</FormDescription>
|
<FormField
|
||||||
</div>
|
control={form.control}
|
||||||
<FormControl>
|
name="canCreateProjects"
|
||||||
<Switch
|
render={({ field }) => (
|
||||||
checked={field.value}
|
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm">
|
||||||
onCheckedChange={field.onChange}
|
<div className="space-y-0.5">
|
||||||
/>
|
<FormLabel>Create Projects</FormLabel>
|
||||||
</FormControl>
|
<FormDescription>
|
||||||
</FormItem>
|
Allow the user to create projects
|
||||||
)}
|
</FormDescription>
|
||||||
/>
|
</div>
|
||||||
<FormField
|
<FormControl>
|
||||||
control={form.control}
|
<Switch
|
||||||
name="canDeleteProjects"
|
checked={field.value}
|
||||||
render={({ field }) => (
|
onCheckedChange={field.onChange}
|
||||||
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm">
|
/>
|
||||||
<div className="space-y-0.5">
|
</FormControl>
|
||||||
<FormLabel>Delete Projects</FormLabel>
|
</FormItem>
|
||||||
<FormDescription>
|
)}
|
||||||
Allow the user to delete projects
|
/>
|
||||||
</FormDescription>
|
<FormField
|
||||||
</div>
|
control={form.control}
|
||||||
<FormControl>
|
name="canDeleteProjects"
|
||||||
<Switch
|
render={({ field }) => (
|
||||||
checked={field.value}
|
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm">
|
||||||
onCheckedChange={field.onChange}
|
<div className="space-y-0.5">
|
||||||
/>
|
<FormLabel>Delete Projects</FormLabel>
|
||||||
</FormControl>
|
<FormDescription>
|
||||||
</FormItem>
|
Allow the user to delete projects
|
||||||
)}
|
</FormDescription>
|
||||||
/>
|
</div>
|
||||||
<FormField
|
<FormControl>
|
||||||
control={form.control}
|
<Switch
|
||||||
name="canCreateServices"
|
checked={field.value}
|
||||||
render={({ field }) => (
|
onCheckedChange={field.onChange}
|
||||||
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm">
|
/>
|
||||||
<div className="space-y-0.5">
|
</FormControl>
|
||||||
<FormLabel>Create Services</FormLabel>
|
</FormItem>
|
||||||
<FormDescription>
|
)}
|
||||||
Allow the user to create services
|
/>
|
||||||
</FormDescription>
|
<FormField
|
||||||
</div>
|
control={form.control}
|
||||||
<FormControl>
|
name="canCreateServices"
|
||||||
<Switch
|
render={({ field }) => (
|
||||||
checked={field.value}
|
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm">
|
||||||
onCheckedChange={field.onChange}
|
<div className="space-y-0.5">
|
||||||
/>
|
<FormLabel>Create Services</FormLabel>
|
||||||
</FormControl>
|
<FormDescription>
|
||||||
</FormItem>
|
Allow the user to create services
|
||||||
)}
|
</FormDescription>
|
||||||
/>
|
</div>
|
||||||
<FormField
|
<FormControl>
|
||||||
control={form.control}
|
<Switch
|
||||||
name="canDeleteServices"
|
checked={field.value}
|
||||||
render={({ field }) => (
|
onCheckedChange={field.onChange}
|
||||||
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm">
|
/>
|
||||||
<div className="space-y-0.5">
|
</FormControl>
|
||||||
<FormLabel>Delete Services</FormLabel>
|
</FormItem>
|
||||||
<FormDescription>
|
)}
|
||||||
Allow the user to delete services
|
/>
|
||||||
</FormDescription>
|
<FormField
|
||||||
</div>
|
control={form.control}
|
||||||
<FormControl>
|
name="canDeleteServices"
|
||||||
<Switch
|
render={({ field }) => (
|
||||||
checked={field.value}
|
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm">
|
||||||
onCheckedChange={field.onChange}
|
<div className="space-y-0.5">
|
||||||
/>
|
<FormLabel>Delete Services</FormLabel>
|
||||||
</FormControl>
|
<FormDescription>
|
||||||
</FormItem>
|
Allow the user to delete services
|
||||||
)}
|
</FormDescription>
|
||||||
/>
|
</div>
|
||||||
<FormField
|
<FormControl>
|
||||||
control={form.control}
|
<Switch
|
||||||
name="canCreateEnvironments"
|
checked={field.value}
|
||||||
render={({ field }) => (
|
onCheckedChange={field.onChange}
|
||||||
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm">
|
/>
|
||||||
<div className="space-y-0.5">
|
</FormControl>
|
||||||
<FormLabel>Create Environments</FormLabel>
|
</FormItem>
|
||||||
<FormDescription>
|
)}
|
||||||
Allow the user to create environments
|
/>
|
||||||
</FormDescription>
|
<FormField
|
||||||
</div>
|
control={form.control}
|
||||||
<FormControl>
|
name="canCreateEnvironments"
|
||||||
<Switch
|
render={({ field }) => (
|
||||||
checked={field.value}
|
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm">
|
||||||
onCheckedChange={field.onChange}
|
<div className="space-y-0.5">
|
||||||
/>
|
<FormLabel>Create Environments</FormLabel>
|
||||||
</FormControl>
|
<FormDescription>
|
||||||
</FormItem>
|
Allow the user to create environments
|
||||||
)}
|
</FormDescription>
|
||||||
/>
|
</div>
|
||||||
<FormField
|
<FormControl>
|
||||||
control={form.control}
|
<Switch
|
||||||
name="canDeleteEnvironments"
|
checked={field.value}
|
||||||
render={({ field }) => (
|
onCheckedChange={field.onChange}
|
||||||
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm">
|
/>
|
||||||
<div className="space-y-0.5">
|
</FormControl>
|
||||||
<FormLabel>Delete Environments</FormLabel>
|
</FormItem>
|
||||||
<FormDescription>
|
)}
|
||||||
Allow the user to delete environments
|
/>
|
||||||
</FormDescription>
|
<FormField
|
||||||
</div>
|
control={form.control}
|
||||||
<FormControl>
|
name="canDeleteEnvironments"
|
||||||
<Switch
|
render={({ field }) => (
|
||||||
checked={field.value}
|
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm">
|
||||||
onCheckedChange={field.onChange}
|
<div className="space-y-0.5">
|
||||||
/>
|
<FormLabel>Delete Environments</FormLabel>
|
||||||
</FormControl>
|
<FormDescription>
|
||||||
</FormItem>
|
Allow the user to delete environments
|
||||||
)}
|
</FormDescription>
|
||||||
/>
|
</div>
|
||||||
<FormField
|
<FormControl>
|
||||||
control={form.control}
|
<Switch
|
||||||
name="canAccessToTraefikFiles"
|
checked={field.value}
|
||||||
render={({ field }) => (
|
onCheckedChange={field.onChange}
|
||||||
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm">
|
/>
|
||||||
<div className="space-y-0.5">
|
</FormControl>
|
||||||
<FormLabel>Access to Traefik Files</FormLabel>
|
</FormItem>
|
||||||
<FormDescription>
|
)}
|
||||||
Allow the user to access to the Traefik Tab Files
|
/>
|
||||||
</FormDescription>
|
<FormField
|
||||||
</div>
|
control={form.control}
|
||||||
<FormControl>
|
name="canAccessToTraefikFiles"
|
||||||
<Switch
|
render={({ field }) => (
|
||||||
checked={field.value}
|
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm">
|
||||||
onCheckedChange={field.onChange}
|
<div className="space-y-0.5">
|
||||||
/>
|
<FormLabel>Access to Traefik Files</FormLabel>
|
||||||
</FormControl>
|
<FormDescription>
|
||||||
</FormItem>
|
Allow the user to access to the Traefik Tab Files
|
||||||
)}
|
</FormDescription>
|
||||||
/>
|
</div>
|
||||||
<FormField
|
<FormControl>
|
||||||
control={form.control}
|
<Switch
|
||||||
name="canAccessToDocker"
|
checked={field.value}
|
||||||
render={({ field }) => (
|
onCheckedChange={field.onChange}
|
||||||
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm">
|
/>
|
||||||
<div className="space-y-0.5">
|
</FormControl>
|
||||||
<FormLabel>Access to Docker</FormLabel>
|
</FormItem>
|
||||||
<FormDescription>
|
)}
|
||||||
Allow the user to access to the Docker Tab
|
/>
|
||||||
</FormDescription>
|
<FormField
|
||||||
</div>
|
control={form.control}
|
||||||
<FormControl>
|
name="canAccessToDocker"
|
||||||
<Switch
|
render={({ field }) => (
|
||||||
checked={field.value}
|
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm">
|
||||||
onCheckedChange={field.onChange}
|
<div className="space-y-0.5">
|
||||||
/>
|
<FormLabel>Access to Docker</FormLabel>
|
||||||
</FormControl>
|
<FormDescription>
|
||||||
</FormItem>
|
Allow the user to access to the Docker Tab
|
||||||
)}
|
</FormDescription>
|
||||||
/>
|
</div>
|
||||||
<FormField
|
<FormControl>
|
||||||
control={form.control}
|
<Switch
|
||||||
name="canAccessToAPI"
|
checked={field.value}
|
||||||
render={({ field }) => (
|
onCheckedChange={field.onChange}
|
||||||
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm">
|
/>
|
||||||
<div className="space-y-0.5">
|
</FormControl>
|
||||||
<FormLabel>Access to API/CLI</FormLabel>
|
</FormItem>
|
||||||
<FormDescription>
|
)}
|
||||||
Allow the user to access to the API/CLI
|
/>
|
||||||
</FormDescription>
|
<FormField
|
||||||
</div>
|
control={form.control}
|
||||||
<FormControl>
|
name="canAccessToAPI"
|
||||||
<Switch
|
render={({ field }) => (
|
||||||
checked={field.value}
|
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm">
|
||||||
onCheckedChange={field.onChange}
|
<div className="space-y-0.5">
|
||||||
/>
|
<FormLabel>Access to API/CLI</FormLabel>
|
||||||
</FormControl>
|
<FormDescription>
|
||||||
</FormItem>
|
Allow the user to access to the API/CLI
|
||||||
)}
|
</FormDescription>
|
||||||
/>
|
</div>
|
||||||
<FormField
|
<FormControl>
|
||||||
control={form.control}
|
<Switch
|
||||||
name="canAccessToSSHKeys"
|
checked={field.value}
|
||||||
render={({ field }) => (
|
onCheckedChange={field.onChange}
|
||||||
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm">
|
/>
|
||||||
<div className="space-y-0.5">
|
</FormControl>
|
||||||
<FormLabel>Access to SSH Keys</FormLabel>
|
</FormItem>
|
||||||
<FormDescription>
|
)}
|
||||||
Allow to users to access to the SSH Keys section
|
/>
|
||||||
</FormDescription>
|
<FormField
|
||||||
</div>
|
control={form.control}
|
||||||
<FormControl>
|
name="canAccessToSSHKeys"
|
||||||
<Switch
|
render={({ field }) => (
|
||||||
checked={field.value}
|
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm">
|
||||||
onCheckedChange={field.onChange}
|
<div className="space-y-0.5">
|
||||||
/>
|
<FormLabel>Access to SSH Keys</FormLabel>
|
||||||
</FormControl>
|
<FormDescription>
|
||||||
</FormItem>
|
Allow to users to access to the SSH Keys section
|
||||||
)}
|
</FormDescription>
|
||||||
/>
|
</div>
|
||||||
<FormField
|
<FormControl>
|
||||||
control={form.control}
|
<Switch
|
||||||
name="canAccessToGitProviders"
|
checked={field.value}
|
||||||
render={({ field }) => (
|
onCheckedChange={field.onChange}
|
||||||
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm">
|
/>
|
||||||
<div className="space-y-0.5">
|
</FormControl>
|
||||||
<FormLabel>Access to Git Providers</FormLabel>
|
</FormItem>
|
||||||
<FormDescription>
|
)}
|
||||||
Allow to users to access to the Git Providers section
|
/>
|
||||||
</FormDescription>
|
<FormField
|
||||||
</div>
|
control={form.control}
|
||||||
<FormControl>
|
name="canAccessToGitProviders"
|
||||||
<Switch
|
render={({ field }) => (
|
||||||
checked={field.value}
|
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm">
|
||||||
onCheckedChange={field.onChange}
|
<div className="space-y-0.5">
|
||||||
/>
|
<FormLabel>Access to Git Providers</FormLabel>
|
||||||
</FormControl>
|
<FormDescription>
|
||||||
</FormItem>
|
Allow to users to access to the Git Providers section
|
||||||
)}
|
</FormDescription>
|
||||||
/>
|
</div>
|
||||||
|
<FormControl>
|
||||||
|
<Switch
|
||||||
|
checked={field.value}
|
||||||
|
onCheckedChange={field.onChange}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="accessedProjects"
|
name="accessedProjects"
|
||||||
|
|||||||
@@ -34,14 +34,14 @@ import {
|
|||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
|
|
||||||
const changeRoleSchema = z.object({
|
const changeRoleSchema = z.object({
|
||||||
role: z.enum(["admin", "member"]),
|
role: z.string().min(1),
|
||||||
});
|
});
|
||||||
|
|
||||||
type ChangeRoleSchema = z.infer<typeof changeRoleSchema>;
|
type ChangeRoleSchema = z.infer<typeof changeRoleSchema>;
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
memberId: string;
|
memberId: string;
|
||||||
currentRole: "admin" | "member";
|
currentRole: string;
|
||||||
userEmail: string;
|
userEmail: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -49,6 +49,10 @@ export const ChangeRole = ({ memberId, currentRole, userEmail }: Props) => {
|
|||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const utils = api.useUtils();
|
const utils = api.useUtils();
|
||||||
|
|
||||||
|
const { data: customRoles } = api.customRole.all.useQuery(undefined, {
|
||||||
|
enabled: isOpen,
|
||||||
|
});
|
||||||
|
|
||||||
const { mutateAsync, isError, error, isPending } =
|
const { mutateAsync, isError, error, isPending } =
|
||||||
api.organization.updateMemberRole.useMutation();
|
api.organization.updateMemberRole.useMutation();
|
||||||
|
|
||||||
@@ -125,6 +129,14 @@ export const ChangeRole = ({ memberId, currentRole, userEmail }: Props) => {
|
|||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="admin">Admin</SelectItem>
|
<SelectItem value="admin">Admin</SelectItem>
|
||||||
<SelectItem value="member">Member</SelectItem>
|
<SelectItem value="member">Member</SelectItem>
|
||||||
|
{customRoles?.map((customRole) => (
|
||||||
|
<SelectItem
|
||||||
|
key={customRole.role}
|
||||||
|
value={customRole.role}
|
||||||
|
>
|
||||||
|
{customRole.role}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
<FormDescription>
|
<FormDescription>
|
||||||
@@ -132,6 +144,13 @@ export const ChangeRole = ({ memberId, currentRole, userEmail }: Props) => {
|
|||||||
<br />
|
<br />
|
||||||
<strong>Member:</strong> Limited permissions, can be
|
<strong>Member:</strong> Limited permissions, can be
|
||||||
customized.
|
customized.
|
||||||
|
{customRoles && customRoles.length > 0 && (
|
||||||
|
<>
|
||||||
|
<br />
|
||||||
|
<strong>Custom roles:</strong> Enterprise-defined
|
||||||
|
permissions.
|
||||||
|
</>
|
||||||
|
)}
|
||||||
<br />
|
<br />
|
||||||
<em className="text-muted-foreground text-xs">
|
<em className="text-muted-foreground text-xs">
|
||||||
Note: Owner role is intransferible.
|
Note: Owner role is intransferible.
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { format } from "date-fns";
|
import { format } from "date-fns";
|
||||||
import { Loader2, MoreHorizontal, Users } from "lucide-react";
|
import { Loader2, MoreHorizontal, Users } from "lucide-react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
import { AlertBlock } from "@/components/shared/alert-block";
|
||||||
import { DialogAction } from "@/components/shared/dialog-action";
|
import { DialogAction } from "@/components/shared/dialog-action";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
@@ -35,10 +36,20 @@ export const ShowUsers = () => {
|
|||||||
const { data: isCloud } = api.settings.isCloud.useQuery();
|
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||||
const { data, isPending, refetch } = api.user.all.useQuery();
|
const { data, isPending, refetch } = api.user.all.useQuery();
|
||||||
const { mutateAsync } = api.user.remove.useMutation();
|
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 utils = api.useUtils();
|
||||||
const { data: session } = api.user.session.useQuery();
|
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 (
|
return (
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
<Card className="h-full bg-sidebar p-2.5 rounded-xl max-w-5xl mx-auto">
|
<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>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex flex-col gap-4 min-h-[25vh]">
|
<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>
|
<Table>
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
@@ -89,40 +112,39 @@ export const ShowUsers = () => {
|
|||||||
)?.role;
|
)?.role;
|
||||||
|
|
||||||
// Owner never has "Edit Permissions" (they're absolute owner)
|
// 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 =
|
const canEditPermissions =
|
||||||
member.role !== "owner" &&
|
!isStaticAdminOrOwner &&
|
||||||
member.role === "member" &&
|
|
||||||
member.user.id !== session?.user?.id;
|
member.user.id !== session?.user?.id;
|
||||||
|
|
||||||
// Can change role based on hierarchy:
|
// Can change role based on hierarchy:
|
||||||
// - Owner: Can change anyone's role (except themselves and other owners)
|
// - 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
|
// - Owner role is intransferible
|
||||||
const canChangeRole =
|
const canChangeRole =
|
||||||
member.role !== "owner" &&
|
member.role !== "owner" &&
|
||||||
member.user.id !== session?.user?.id &&
|
member.user.id !== session?.user?.id &&
|
||||||
(currentUserRole === "owner" ||
|
(currentUserRole === "owner" ||
|
||||||
(currentUserRole === "admin" &&
|
(currentUserRole === "admin" &&
|
||||||
member.role === "member"));
|
member.role !== "admin"));
|
||||||
|
|
||||||
// Delete/Unlink follow same hierarchy as role changes
|
const canDeleteMember =
|
||||||
// - Owner: Can delete/unlink anyone (except themselves and owner can't be deleted)
|
permissions?.member.delete ?? false;
|
||||||
// - 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 canUnlink =
|
// Self-hosted: "Delete User" removes the user entirely
|
||||||
|
// Cloud: "Unlink User" removes from the organization only
|
||||||
|
const canRemove =
|
||||||
member.role !== "owner" &&
|
member.role !== "owner" &&
|
||||||
member.user.id !== session?.user?.id &&
|
member.user.id !== session?.user?.id &&
|
||||||
(currentUserRole === "owner" ||
|
(currentUserRole === "owner" ||
|
||||||
(currentUserRole === "admin" &&
|
(currentUserRole === "admin" &&
|
||||||
member.role === "member"));
|
member.role !== "admin") ||
|
||||||
|
(canDeleteMember && !isStaticAdminOrOwner));
|
||||||
|
|
||||||
|
const canDelete = canRemove && !isCloud;
|
||||||
|
const canUnlink = canRemove && !!isCloud;
|
||||||
|
|
||||||
const hasAnyAction =
|
const hasAnyAction =
|
||||||
canEditPermissions ||
|
canEditPermissions ||
|
||||||
@@ -134,6 +156,11 @@ export const ShowUsers = () => {
|
|||||||
<TableRow key={member.id}>
|
<TableRow key={member.id}>
|
||||||
<TableCell className="w-[100px]">
|
<TableCell className="w-[100px]">
|
||||||
{member.user.email}
|
{member.user.email}
|
||||||
|
{member.user.id === session?.user?.id && (
|
||||||
|
<span className="text-muted-foreground ml-1">
|
||||||
|
(You)
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="text-center">
|
<TableCell className="text-center">
|
||||||
<Badge
|
<Badge
|
||||||
@@ -179,9 +206,7 @@ export const ShowUsers = () => {
|
|||||||
{canChangeRole && (
|
{canChangeRole && (
|
||||||
<ChangeRole
|
<ChangeRole
|
||||||
memberId={member.id}
|
memberId={member.id}
|
||||||
currentRole={
|
currentRole={member.role}
|
||||||
member.role as "admin" | "member"
|
|
||||||
}
|
|
||||||
userEmail={member.user.email}
|
userEmail={member.user.email}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@@ -189,6 +214,7 @@ export const ShowUsers = () => {
|
|||||||
{canEditPermissions && (
|
{canEditPermissions && (
|
||||||
<AddUserPermissions
|
<AddUserPermissions
|
||||||
userId={member.user.id}
|
userId={member.user.id}
|
||||||
|
role={member.role}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
ChevronRight,
|
ChevronRight,
|
||||||
ChevronsUpDown,
|
ChevronsUpDown,
|
||||||
CircleHelp,
|
CircleHelp,
|
||||||
|
ClipboardList,
|
||||||
Clock,
|
Clock,
|
||||||
CreditCard,
|
CreditCard,
|
||||||
Database,
|
Database,
|
||||||
@@ -92,13 +93,21 @@ import { UserNav } from "./user-nav";
|
|||||||
|
|
||||||
// The types of the queries we are going to use
|
// The types of the queries we are going to use
|
||||||
type AuthQueryOutput = inferRouterOutputs<AppRouter>["user"]["get"];
|
type AuthQueryOutput = inferRouterOutputs<AppRouter>["user"]["get"];
|
||||||
|
type PermissionsOutput =
|
||||||
|
inferRouterOutputs<AppRouter>["user"]["getPermissions"];
|
||||||
|
|
||||||
|
type EnabledOpts = {
|
||||||
|
auth?: AuthQueryOutput;
|
||||||
|
permissions?: PermissionsOutput;
|
||||||
|
isCloud: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
type SingleNavItem = {
|
type SingleNavItem = {
|
||||||
isSingle?: true;
|
isSingle?: true;
|
||||||
title: string;
|
title: string;
|
||||||
url: string;
|
url: string;
|
||||||
icon?: LucideIcon;
|
icon?: LucideIcon;
|
||||||
isEnabled?: (opts: { auth?: AuthQueryOutput; isCloud: boolean }) => boolean;
|
isEnabled?: (opts: EnabledOpts) => boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
// NavItem type
|
// NavItem type
|
||||||
@@ -112,10 +121,7 @@ type NavItem =
|
|||||||
title: string;
|
title: string;
|
||||||
icon: LucideIcon;
|
icon: LucideIcon;
|
||||||
items: SingleNavItem[];
|
items: SingleNavItem[];
|
||||||
isEnabled?: (opts: {
|
isEnabled?: (opts: EnabledOpts) => boolean;
|
||||||
auth?: AuthQueryOutput;
|
|
||||||
isCloud: boolean;
|
|
||||||
}) => boolean;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// ExternalLink type
|
// ExternalLink type
|
||||||
@@ -124,7 +130,7 @@ type ExternalLink = {
|
|||||||
name: string;
|
name: string;
|
||||||
url: string;
|
url: string;
|
||||||
icon: React.ComponentType<{ className?: string }>;
|
icon: React.ComponentType<{ className?: string }>;
|
||||||
isEnabled?: (opts: { auth?: AuthQueryOutput; isCloud: boolean }) => boolean;
|
isEnabled?: (opts: EnabledOpts) => boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Menu type
|
// Menu type
|
||||||
@@ -152,14 +158,16 @@ const MENU: Menu = {
|
|||||||
title: "Deployments",
|
title: "Deployments",
|
||||||
url: "/dashboard/deployments",
|
url: "/dashboard/deployments",
|
||||||
icon: Rocket,
|
icon: Rocket,
|
||||||
|
isEnabled: ({ permissions }) => !!permissions?.deployment.read,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
isSingle: true,
|
isSingle: true,
|
||||||
title: "Monitoring",
|
title: "Monitoring",
|
||||||
url: "/dashboard/monitoring",
|
url: "/dashboard/monitoring",
|
||||||
icon: BarChartHorizontalBigIcon,
|
icon: BarChartHorizontalBigIcon,
|
||||||
// Only enabled in non-cloud environments
|
// Only enabled in non-cloud environments and if user has monitoring.read
|
||||||
isEnabled: ({ isCloud }) => !isCloud,
|
isEnabled: ({ isCloud, permissions }) =>
|
||||||
|
!isCloud && !!permissions?.monitoring.read,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
isSingle: true,
|
isSingle: true,
|
||||||
@@ -167,64 +175,44 @@ const MENU: Menu = {
|
|||||||
url: "/dashboard/schedules",
|
url: "/dashboard/schedules",
|
||||||
icon: Clock,
|
icon: Clock,
|
||||||
// Only enabled in non-cloud environments
|
// Only enabled in non-cloud environments
|
||||||
isEnabled: ({ isCloud, auth }) =>
|
isEnabled: ({ isCloud, permissions }) =>
|
||||||
!isCloud && (auth?.role === "owner" || auth?.role === "admin"),
|
!isCloud && !!permissions?.organization.update,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
isSingle: true,
|
isSingle: true,
|
||||||
title: "Traefik File System",
|
title: "Traefik File System",
|
||||||
url: "/dashboard/traefik",
|
url: "/dashboard/traefik",
|
||||||
icon: GalleryVerticalEnd,
|
icon: GalleryVerticalEnd,
|
||||||
// Only enabled for admins and users with access to Traefik files in non-cloud environments
|
// Only enabled for users with access to Traefik files in non-cloud environments
|
||||||
isEnabled: ({ auth, isCloud }) =>
|
isEnabled: ({ permissions, isCloud }) =>
|
||||||
!!(
|
!!(permissions?.traefikFiles.read && !isCloud),
|
||||||
(auth?.role === "owner" ||
|
|
||||||
auth?.role === "admin" ||
|
|
||||||
auth?.canAccessToTraefikFiles) &&
|
|
||||||
!isCloud
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
isSingle: true,
|
isSingle: true,
|
||||||
title: "Docker",
|
title: "Docker",
|
||||||
url: "/dashboard/docker",
|
url: "/dashboard/docker",
|
||||||
icon: BlocksIcon,
|
icon: BlocksIcon,
|
||||||
// Only enabled for admins and users with access to Docker in non-cloud environments
|
// Only enabled for users with access to Docker in non-cloud environments
|
||||||
isEnabled: ({ auth, isCloud }) =>
|
isEnabled: ({ permissions, isCloud }) =>
|
||||||
!!(
|
!!(permissions?.docker.read && !isCloud),
|
||||||
(auth?.role === "owner" ||
|
|
||||||
auth?.role === "admin" ||
|
|
||||||
auth?.canAccessToDocker) &&
|
|
||||||
!isCloud
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
isSingle: true,
|
isSingle: true,
|
||||||
title: "Swarm",
|
title: "Swarm",
|
||||||
url: "/dashboard/swarm",
|
url: "/dashboard/swarm",
|
||||||
icon: PieChart,
|
icon: PieChart,
|
||||||
// Only enabled for admins and users with access to Docker in non-cloud environments
|
// Only enabled for users with access to Docker in non-cloud environments
|
||||||
isEnabled: ({ auth, isCloud }) =>
|
isEnabled: ({ permissions, isCloud }) =>
|
||||||
!!(
|
!!(permissions?.docker.read && !isCloud),
|
||||||
(auth?.role === "owner" ||
|
|
||||||
auth?.role === "admin" ||
|
|
||||||
auth?.canAccessToDocker) &&
|
|
||||||
!isCloud
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
isSingle: true,
|
isSingle: true,
|
||||||
title: "Requests",
|
title: "Requests",
|
||||||
url: "/dashboard/requests",
|
url: "/dashboard/requests",
|
||||||
icon: Forward,
|
icon: Forward,
|
||||||
// Only enabled for admins and users with access to Docker in non-cloud environments
|
// Only enabled for users with access to Docker in non-cloud environments
|
||||||
isEnabled: ({ auth, isCloud }) =>
|
isEnabled: ({ permissions, isCloud }) =>
|
||||||
!!(
|
!!(permissions?.docker.read && !isCloud),
|
||||||
(auth?.role === "owner" ||
|
|
||||||
auth?.role === "admin" ||
|
|
||||||
auth?.canAccessToDocker) &&
|
|
||||||
!isCloud
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
|
|
||||||
// Legacy unused menu, adjusted to the new structure
|
// Legacy unused menu, adjusted to the new structure
|
||||||
@@ -291,8 +279,8 @@ const MENU: Menu = {
|
|||||||
url: "/dashboard/settings/server",
|
url: "/dashboard/settings/server",
|
||||||
icon: Activity,
|
icon: Activity,
|
||||||
// Only enabled for admins in non-cloud environments
|
// Only enabled for admins in non-cloud environments
|
||||||
isEnabled: ({ auth, isCloud }) =>
|
isEnabled: ({ permissions, isCloud }) =>
|
||||||
!!((auth?.role === "owner" || auth?.role === "admin") && !isCloud),
|
!!(permissions?.organization.update && !isCloud),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
isSingle: true,
|
isSingle: true,
|
||||||
@@ -305,70 +293,59 @@ const MENU: Menu = {
|
|||||||
title: "Remote Servers",
|
title: "Remote Servers",
|
||||||
url: "/dashboard/settings/servers",
|
url: "/dashboard/settings/servers",
|
||||||
icon: Server,
|
icon: Server,
|
||||||
// Only enabled for admins
|
isEnabled: ({ permissions }) => !!permissions?.server.read,
|
||||||
isEnabled: ({ auth }) =>
|
|
||||||
!!(auth?.role === "owner" || auth?.role === "admin"),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
isSingle: true,
|
isSingle: true,
|
||||||
title: "Users",
|
title: "Users",
|
||||||
icon: Users,
|
icon: Users,
|
||||||
url: "/dashboard/settings/users",
|
url: "/dashboard/settings/users",
|
||||||
// Only enabled for admins
|
// Only enabled for users with member.read permission
|
||||||
isEnabled: ({ auth }) =>
|
isEnabled: ({ permissions }) => !!permissions?.member.read,
|
||||||
!!(auth?.role === "owner" || auth?.role === "admin"),
|
},
|
||||||
|
{
|
||||||
|
isSingle: true,
|
||||||
|
title: "Audit Logs",
|
||||||
|
icon: ClipboardList,
|
||||||
|
url: "/dashboard/settings/audit-logs",
|
||||||
|
isEnabled: ({ permissions }) => !!permissions?.auditLog.read,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
isSingle: true,
|
isSingle: true,
|
||||||
title: "SSH Keys",
|
title: "SSH Keys",
|
||||||
icon: KeyRound,
|
icon: KeyRound,
|
||||||
url: "/dashboard/settings/ssh-keys",
|
url: "/dashboard/settings/ssh-keys",
|
||||||
// Only enabled for admins and users with access to SSH keys
|
// Only enabled for users with access to SSH keys
|
||||||
isEnabled: ({ auth }) =>
|
isEnabled: ({ permissions }) => !!permissions?.sshKeys.read,
|
||||||
!!(
|
|
||||||
auth?.role === "owner" ||
|
|
||||||
auth?.canAccessToSSHKeys ||
|
|
||||||
auth?.role === "admin"
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "AI",
|
title: "AI",
|
||||||
icon: BotIcon,
|
icon: BotIcon,
|
||||||
url: "/dashboard/settings/ai",
|
url: "/dashboard/settings/ai",
|
||||||
isSingle: true,
|
isSingle: true,
|
||||||
isEnabled: ({ auth }) =>
|
isEnabled: ({ permissions }) => !!permissions?.organization.update,
|
||||||
!!(auth?.role === "owner" || auth?.role === "admin"),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
isSingle: true,
|
isSingle: true,
|
||||||
title: "Git",
|
title: "Git",
|
||||||
url: "/dashboard/settings/git-providers",
|
url: "/dashboard/settings/git-providers",
|
||||||
icon: GitBranch,
|
icon: GitBranch,
|
||||||
// Only enabled for admins and users with access to Git providers
|
// Only enabled for users with access to Git providers
|
||||||
isEnabled: ({ auth }) =>
|
isEnabled: ({ permissions }) => !!permissions?.gitProviders.read,
|
||||||
!!(
|
|
||||||
auth?.role === "owner" ||
|
|
||||||
auth?.canAccessToGitProviders ||
|
|
||||||
auth?.role === "admin"
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
isSingle: true,
|
isSingle: true,
|
||||||
title: "Registry",
|
title: "Registry",
|
||||||
url: "/dashboard/settings/registry",
|
url: "/dashboard/settings/registry",
|
||||||
icon: Package,
|
icon: Package,
|
||||||
// Only enabled for admins
|
isEnabled: ({ permissions }) => !!permissions?.registry.read,
|
||||||
isEnabled: ({ auth }) =>
|
|
||||||
!!(auth?.role === "owner" || auth?.role === "admin"),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
isSingle: true,
|
isSingle: true,
|
||||||
title: "S3 Destinations",
|
title: "S3 Destinations",
|
||||||
url: "/dashboard/settings/destinations",
|
url: "/dashboard/settings/destinations",
|
||||||
icon: Database,
|
icon: Database,
|
||||||
// Only enabled for admins
|
isEnabled: ({ permissions }) => !!permissions?.destination.read,
|
||||||
isEnabled: ({ auth }) =>
|
|
||||||
!!(auth?.role === "owner" || auth?.role === "admin"),
|
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
@@ -376,9 +353,7 @@ const MENU: Menu = {
|
|||||||
title: "Certificates",
|
title: "Certificates",
|
||||||
url: "/dashboard/settings/certificates",
|
url: "/dashboard/settings/certificates",
|
||||||
icon: ShieldCheck,
|
icon: ShieldCheck,
|
||||||
// Only enabled for admins
|
isEnabled: ({ permissions }) => !!permissions?.certificate.read,
|
||||||
isEnabled: ({ auth }) =>
|
|
||||||
!!(auth?.role === "owner" || auth?.role === "admin"),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
isSingle: true,
|
isSingle: true,
|
||||||
@@ -386,24 +361,23 @@ const MENU: Menu = {
|
|||||||
url: "/dashboard/settings/cluster",
|
url: "/dashboard/settings/cluster",
|
||||||
icon: Boxes,
|
icon: Boxes,
|
||||||
// Only enabled for admins in non-cloud environments
|
// Only enabled for admins in non-cloud environments
|
||||||
isEnabled: ({ auth, isCloud }) =>
|
isEnabled: ({ permissions, isCloud }) =>
|
||||||
!!((auth?.role === "owner" || auth?.role === "admin") && !isCloud),
|
!!(permissions?.organization.update && !isCloud),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
isSingle: true,
|
isSingle: true,
|
||||||
title: "Notifications",
|
title: "Notifications",
|
||||||
url: "/dashboard/settings/notifications",
|
url: "/dashboard/settings/notifications",
|
||||||
icon: Bell,
|
icon: Bell,
|
||||||
// Only enabled for admins
|
// Only enabled for users with access to notifications
|
||||||
isEnabled: ({ auth }) =>
|
isEnabled: ({ permissions }) => !!permissions?.notification.read,
|
||||||
!!(auth?.role === "owner" || auth?.role === "admin"),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
isSingle: true,
|
isSingle: true,
|
||||||
title: "Billing",
|
title: "Billing",
|
||||||
url: "/dashboard/settings/billing",
|
url: "/dashboard/settings/billing",
|
||||||
icon: CreditCard,
|
icon: CreditCard,
|
||||||
// Only enabled for admins in cloud environments
|
// Only enabled for owners in cloud environments
|
||||||
isEnabled: ({ auth, isCloud }) => !!(auth?.role === "owner" && isCloud),
|
isEnabled: ({ auth, isCloud }) => !!(auth?.role === "owner" && isCloud),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -411,7 +385,7 @@ const MENU: Menu = {
|
|||||||
title: "License",
|
title: "License",
|
||||||
url: "/dashboard/settings/license",
|
url: "/dashboard/settings/license",
|
||||||
icon: Key,
|
icon: Key,
|
||||||
// Only enabled for admins in non-cloud environments
|
// Only enabled for owners
|
||||||
isEnabled: ({ auth }) => !!(auth?.role === "owner"),
|
isEnabled: ({ auth }) => !!(auth?.role === "owner"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -420,8 +394,7 @@ const MENU: Menu = {
|
|||||||
url: "/dashboard/settings/sso",
|
url: "/dashboard/settings/sso",
|
||||||
icon: LogIn,
|
icon: LogIn,
|
||||||
// Enabled for admins in both cloud and self-hosted (enterprise)
|
// Enabled for admins in both cloud and self-hosted (enterprise)
|
||||||
isEnabled: ({ auth }) =>
|
isEnabled: ({ permissions }) => !!permissions?.organization.update,
|
||||||
!!(auth?.role === "owner" || auth?.role === "admin"),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
isSingle: true,
|
isSingle: true,
|
||||||
@@ -453,6 +426,7 @@ const MENU: Menu = {
|
|||||||
*/
|
*/
|
||||||
function createMenuForAuthUser(opts: {
|
function createMenuForAuthUser(opts: {
|
||||||
auth?: AuthQueryOutput;
|
auth?: AuthQueryOutput;
|
||||||
|
permissions?: PermissionsOutput;
|
||||||
isCloud: boolean;
|
isCloud: boolean;
|
||||||
whitelabeling?: {
|
whitelabeling?: {
|
||||||
docsUrl?: string | null;
|
docsUrl?: string | null;
|
||||||
@@ -461,7 +435,7 @@ function createMenuForAuthUser(opts: {
|
|||||||
}): Menu {
|
}): Menu {
|
||||||
const filterEnabled = <
|
const filterEnabled = <
|
||||||
T extends {
|
T extends {
|
||||||
isEnabled?: (o: { auth?: AuthQueryOutput; isCloud: boolean }) => boolean;
|
isEnabled?: (o: EnabledOpts) => boolean;
|
||||||
},
|
},
|
||||||
>(
|
>(
|
||||||
items: readonly T[],
|
items: readonly T[],
|
||||||
@@ -469,7 +443,11 @@ function createMenuForAuthUser(opts: {
|
|||||||
items.filter((item) =>
|
items.filter((item) =>
|
||||||
!item.isEnabled
|
!item.isEnabled
|
||||||
? true
|
? true
|
||||||
: item.isEnabled({ auth: opts.auth, isCloud: opts.isCloud }),
|
: item.isEnabled({
|
||||||
|
auth: opts.auth,
|
||||||
|
permissions: opts.permissions,
|
||||||
|
isCloud: opts.isCloud,
|
||||||
|
}),
|
||||||
) as T[];
|
) as T[];
|
||||||
|
|
||||||
// Apply whitelabeling URL overrides to help items
|
// Apply whitelabeling URL overrides to help items
|
||||||
@@ -567,6 +545,7 @@ function SidebarLogo() {
|
|||||||
const { mutateAsync: setDefaultOrganization, isPending: isSettingDefault } =
|
const { mutateAsync: setDefaultOrganization, isPending: isSettingDefault } =
|
||||||
api.organization.setDefault.useMutation();
|
api.organization.setDefault.useMutation();
|
||||||
const { isMobile } = useSidebar();
|
const { isMobile } = useSidebar();
|
||||||
|
const isCollapsed = state === "collapsed" && !isMobile;
|
||||||
const { data: activeOrganization } = api.organization.active.useQuery();
|
const { data: activeOrganization } = api.organization.active.useQuery();
|
||||||
|
|
||||||
const { data: invitations, refetch: refetchInvitations } =
|
const { data: invitations, refetch: refetchInvitations } =
|
||||||
@@ -592,9 +571,7 @@ function SidebarLogo() {
|
|||||||
<SidebarMenu
|
<SidebarMenu
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex gap-2",
|
"flex gap-2",
|
||||||
state === "collapsed"
|
isCollapsed ? "flex-col" : "flex-row justify-between items-center",
|
||||||
? "flex-col"
|
|
||||||
: "flex-row justify-between items-center",
|
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{/* Organization Logo and Selector */}
|
{/* Organization Logo and Selector */}
|
||||||
@@ -602,17 +579,17 @@ function SidebarLogo() {
|
|||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<SidebarMenuButton
|
<SidebarMenuButton
|
||||||
size={state === "collapsed" ? "sm" : "lg"}
|
size={isCollapsed ? "sm" : "lg"}
|
||||||
className={cn(
|
className={cn(
|
||||||
"data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground",
|
"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",
|
"flex justify-center items-center p-2 h-10 w-10 mx-auto",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex items-center gap-2",
|
"flex items-center gap-2",
|
||||||
state === "collapsed" && "justify-center",
|
isCollapsed && "justify-center",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
@@ -624,7 +601,7 @@ function SidebarLogo() {
|
|||||||
<Logo
|
<Logo
|
||||||
className={cn(
|
className={cn(
|
||||||
"transition-all",
|
"transition-all",
|
||||||
state === "collapsed" ? "size-4" : "size-5",
|
isCollapsed ? "size-4" : "size-5",
|
||||||
)}
|
)}
|
||||||
logoUrl={activeOrganization?.logo || undefined}
|
logoUrl={activeOrganization?.logo || undefined}
|
||||||
/>
|
/>
|
||||||
@@ -632,7 +609,7 @@ function SidebarLogo() {
|
|||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex flex-col items-start",
|
"flex flex-col items-start",
|
||||||
state === "collapsed" && "hidden",
|
isCollapsed && "hidden",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<p className="text-sm font-medium leading-none">
|
<p className="text-sm font-medium leading-none">
|
||||||
@@ -641,7 +618,7 @@ function SidebarLogo() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ChevronsUpDown
|
<ChevronsUpDown
|
||||||
className={cn("ml-auto", state === "collapsed" && "hidden")}
|
className={cn("ml-auto", isCollapsed && "hidden")}
|
||||||
/>
|
/>
|
||||||
</SidebarMenuButton>
|
</SidebarMenuButton>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
@@ -790,7 +767,7 @@ function SidebarLogo() {
|
|||||||
</SidebarMenuItem>
|
</SidebarMenuItem>
|
||||||
|
|
||||||
{/* Notification Bell */}
|
{/* Notification Bell */}
|
||||||
<SidebarMenuItem className={cn(state === "collapsed" && "mt-2")}>
|
<SidebarMenuItem className={cn(isCollapsed && "mt-2")}>
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<Button
|
<Button
|
||||||
@@ -798,7 +775,7 @@ function SidebarLogo() {
|
|||||||
size="icon"
|
size="icon"
|
||||||
className={cn(
|
className={cn(
|
||||||
"relative",
|
"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" />
|
<Bell className="size-4" />
|
||||||
@@ -894,6 +871,7 @@ export default function Page({ children }: Props) {
|
|||||||
|
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
const { data: auth } = api.user.get.useQuery();
|
const { data: auth } = api.user.get.useQuery();
|
||||||
|
const { data: permissions } = api.user.getPermissions.useQuery();
|
||||||
const { data: dokployVersion } = api.settings.getDokployVersion.useQuery();
|
const { data: dokployVersion } = api.settings.getDokployVersion.useQuery();
|
||||||
const { data: whitelabeling } = api.whitelabeling.get.useQuery(undefined, {
|
const { data: whitelabeling } = api.whitelabeling.get.useQuery(undefined, {
|
||||||
staleTime: 5 * 60 * 1000,
|
staleTime: 5 * 60 * 1000,
|
||||||
@@ -907,7 +885,12 @@ export default function Page({ children }: Props) {
|
|||||||
home: filteredHome,
|
home: filteredHome,
|
||||||
settings: filteredSettings,
|
settings: filteredSettings,
|
||||||
help,
|
help,
|
||||||
} = createMenuForAuthUser({ auth, isCloud: !!isCloud, whitelabeling });
|
} = createMenuForAuthUser({
|
||||||
|
auth,
|
||||||
|
permissions,
|
||||||
|
isCloud: !!isCloud,
|
||||||
|
whitelabeling,
|
||||||
|
});
|
||||||
|
|
||||||
const activeItem = findActiveNavItem(
|
const activeItem = findActiveNavItem(
|
||||||
[...filteredHome, ...filteredSettings],
|
[...filteredHome, ...filteredSettings],
|
||||||
@@ -1147,7 +1130,7 @@ export default function Page({ children }: Props) {
|
|||||||
</SidebarContent>
|
</SidebarContent>
|
||||||
<SidebarFooter>
|
<SidebarFooter>
|
||||||
<SidebarMenu className="flex flex-col gap-2">
|
<SidebarMenu className="flex flex-col gap-2">
|
||||||
{!isCloud && (auth?.role === "owner" || auth?.role === "admin") && (
|
{!isCloud && permissions?.organization.update && (
|
||||||
<SidebarMenuItem>
|
<SidebarMenuItem>
|
||||||
<UpdateServerButton />
|
<UpdateServerButton />
|
||||||
</SidebarMenuItem>
|
</SidebarMenuItem>
|
||||||
@@ -1161,14 +1144,9 @@ export default function Page({ children }: Props) {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{dokployVersion && (
|
{dokployVersion && (
|
||||||
<>
|
<div className="px-3 text-xs text-muted-foreground text-center group-data-[collapsible=icon]:hidden">
|
||||||
<div className="px-3 text-xs text-muted-foreground text-center group-data-[collapsible=icon]:hidden">
|
Version {dokployVersion}
|
||||||
Version {dokployVersion}
|
</div>
|
||||||
</div>
|
|
||||||
<div className="hidden text-xs text-muted-foreground text-center group-data-[collapsible=icon]:block">
|
|
||||||
{dokployVersion}
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
</SidebarMenu>
|
</SidebarMenu>
|
||||||
</SidebarFooter>
|
</SidebarFooter>
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ const _AUTO_CHECK_UPDATES_INTERVAL_MINUTES = 7;
|
|||||||
export const UserNav = () => {
|
export const UserNav = () => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { data } = api.user.get.useQuery();
|
const { data } = api.user.get.useQuery();
|
||||||
|
const { data: permissions } = api.user.getPermissions.useQuery();
|
||||||
const { data: isCloud } = api.settings.isCloud.useQuery();
|
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||||
|
|
||||||
// const { mutateAsync } = api.auth.logout.useMutation();
|
// const { mutateAsync } = api.auth.logout.useMutation();
|
||||||
@@ -94,9 +95,7 @@ export const UserNav = () => {
|
|||||||
>
|
>
|
||||||
Monitoring
|
Monitoring
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
{(data?.role === "owner" ||
|
{permissions?.traefikFiles.read && (
|
||||||
data?.role === "admin" ||
|
|
||||||
data?.canAccessToTraefikFiles) && (
|
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
className="cursor-pointer"
|
className="cursor-pointer"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@@ -106,9 +105,7 @@ export const UserNav = () => {
|
|||||||
Traefik
|
Traefik
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
)}
|
)}
|
||||||
{(data?.role === "owner" ||
|
{permissions?.docker.read && (
|
||||||
data?.role === "admin" ||
|
|
||||||
data?.canAccessToDocker) && (
|
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
className="cursor-pointer"
|
className="cursor-pointer"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@@ -122,7 +119,7 @@ export const UserNav = () => {
|
|||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
(data?.role === "owner" || data?.role === "admin") && (
|
permissions?.organization.update && (
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
className="cursor-pointer"
|
className="cursor-pointer"
|
||||||
onClick={() => {
|
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";
|
} from "@/components/ui/dropdown-menu";
|
||||||
import { Separator } from "@/components/ui/separator";
|
import { Separator } from "@/components/ui/separator";
|
||||||
import { SidebarTrigger } from "@/components/ui/sidebar";
|
import { SidebarTrigger } from "@/components/ui/sidebar";
|
||||||
|
import { TimeBadge } from "@/components/ui/time-badge";
|
||||||
|
import { api } from "@/utils/api";
|
||||||
|
|
||||||
interface BreadcrumbEntry {
|
interface BreadcrumbEntry {
|
||||||
name: string;
|
name: string;
|
||||||
@@ -32,9 +34,11 @@ interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const BreadcrumbSidebar = ({ list }: Props) => {
|
export const BreadcrumbSidebar = ({ list }: Props) => {
|
||||||
|
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||||
|
|
||||||
return (
|
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">
|
<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">
|
<div className="flex items-center gap-2">
|
||||||
<SidebarTrigger className="-ml-1" />
|
<SidebarTrigger className="-ml-1" />
|
||||||
<Separator orientation="vertical" className="mr-2 h-4" />
|
<Separator orientation="vertical" className="mr-2 h-4" />
|
||||||
@@ -75,6 +79,7 @@ export const BreadcrumbSidebar = ({ list }: Props) => {
|
|||||||
</BreadcrumbList>
|
</BreadcrumbList>
|
||||||
</Breadcrumb>
|
</Breadcrumb>
|
||||||
</div>
|
</div>
|
||||||
|
{!isCloud && <TimeBadge />}
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -213,7 +213,9 @@ const Sidebar = React.forwardRef<
|
|||||||
}
|
}
|
||||||
side={side}
|
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>
|
</SheetContent>
|
||||||
</Sheet>
|
</Sheet>
|
||||||
);
|
);
|
||||||
@@ -412,7 +414,7 @@ const SidebarContent = React.forwardRef<
|
|||||||
ref={ref}
|
ref={ref}
|
||||||
data-sidebar="content"
|
data-sidebar="content"
|
||||||
className={cn(
|
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,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...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");
|
||||||
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
@@ -1044,6 +1044,13 @@
|
|||||||
"when": 1773129798212,
|
"when": 1773129798212,
|
||||||
"tag": "0148_futuristic_bullseye",
|
"tag": "0148_futuristic_bullseye",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 149,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1773637297592,
|
||||||
|
"tag": "0149_rare_radioactive_man",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import { ssoClient } from "@better-auth/sso/client";
|
import { ssoClient } from "@better-auth/sso/client";
|
||||||
|
import { apiKeyClient } from "@better-auth/api-key/client";
|
||||||
import {
|
import {
|
||||||
adminClient,
|
adminClient,
|
||||||
apiKeyClient,
|
|
||||||
inferAdditionalFields,
|
inferAdditionalFields,
|
||||||
organizationClient,
|
organizationClient,
|
||||||
twoFactorClient,
|
twoFactorClient,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "dokploy",
|
"name": "dokploy",
|
||||||
"version": "v0.28.6",
|
"version": "v0.28.7",
|
||||||
"private": true,
|
"private": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
@@ -46,7 +46,8 @@
|
|||||||
"@ai-sdk/mistral": "^3.0.20",
|
"@ai-sdk/mistral": "^3.0.20",
|
||||||
"@ai-sdk/openai": "^3.0.29",
|
"@ai-sdk/openai": "^3.0.29",
|
||||||
"@ai-sdk/openai-compatible": "^2.0.30",
|
"@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/autocomplete": "^6.18.6",
|
||||||
"@codemirror/lang-css": "^6.3.1",
|
"@codemirror/lang-css": "^6.3.1",
|
||||||
"@codemirror/lang-json": "^6.0.1",
|
"@codemirror/lang-json": "^6.0.1",
|
||||||
@@ -99,7 +100,7 @@
|
|||||||
"ai": "^6.0.86",
|
"ai": "^6.0.86",
|
||||||
"ai-sdk-ollama": "^3.7.0",
|
"ai-sdk-ollama": "^3.7.0",
|
||||||
"bcrypt": "5.1.1",
|
"bcrypt": "5.1.1",
|
||||||
"better-auth": "1.5.0-beta.16",
|
"better-auth": "1.5.4",
|
||||||
"bl": "6.0.11",
|
"bl": "6.0.11",
|
||||||
"boxen": "^7.1.1",
|
"boxen": "^7.1.1",
|
||||||
"bullmq": "5.67.3",
|
"bullmq": "5.67.3",
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { validateRequest } from "@dokploy/server/lib/auth";
|
import { validateRequest } from "@dokploy/server/lib/auth";
|
||||||
|
import { hasPermission } from "@dokploy/server/services/permission";
|
||||||
import { Rocket } from "lucide-react";
|
import { Rocket } from "lucide-react";
|
||||||
import type { GetServerSidePropsContext } from "next";
|
import type { GetServerSidePropsContext } from "next";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
@@ -79,7 +80,7 @@ DeploymentsPage.getLayout = (page: ReactElement) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export async function getServerSideProps(ctx: GetServerSidePropsContext) {
|
export async function getServerSideProps(ctx: GetServerSidePropsContext) {
|
||||||
const { user } = await validateRequest(ctx.req);
|
const { user, session } = await validateRequest(ctx.req);
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return {
|
return {
|
||||||
redirect: {
|
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 {
|
return {
|
||||||
props: {},
|
props: {},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -53,19 +53,15 @@ export async function getServerSideProps(
|
|||||||
try {
|
try {
|
||||||
await helpers.project.all.prefetch();
|
await helpers.project.all.prefetch();
|
||||||
|
|
||||||
if (user.role === "member") {
|
const userPermissions = await helpers.user.getPermissions.fetch();
|
||||||
const userR = await helpers.user.one.fetch({
|
|
||||||
userId: user.id,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!userR?.canAccessToDocker) {
|
if (!userPermissions?.docker.read) {
|
||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
permanent: true,
|
permanent: true,
|
||||||
destination: "/",
|
destination: "/",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
props: {
|
props: {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { IS_CLOUD } from "@dokploy/server/constants";
|
import { IS_CLOUD } from "@dokploy/server/constants";
|
||||||
import { validateRequest } from "@dokploy/server/lib/auth";
|
import { validateRequest } from "@dokploy/server/lib/auth";
|
||||||
|
import { hasPermission } from "@dokploy/server/services/permission";
|
||||||
import { Loader2 } from "lucide-react";
|
import { Loader2 } from "lucide-react";
|
||||||
import type { GetServerSidePropsContext } from "next";
|
import type { GetServerSidePropsContext } from "next";
|
||||||
import type { ReactElement } from "react";
|
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) {
|
if (!user) {
|
||||||
return {
|
return {
|
||||||
redirect: {
|
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 {
|
return {
|
||||||
props: {},
|
props: {},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -272,6 +272,7 @@ const EnvironmentPage = (
|
|||||||
const [isBulkActionLoading, setIsBulkActionLoading] = useState(false);
|
const [isBulkActionLoading, setIsBulkActionLoading] = useState(false);
|
||||||
const { projectId, environmentId } = props;
|
const { projectId, environmentId } = props;
|
||||||
const { data: auth } = api.user.get.useQuery();
|
const { data: auth } = api.user.get.useQuery();
|
||||||
|
const { data: permissions } = api.user.getPermissions.useQuery();
|
||||||
|
|
||||||
const { data: environments } = api.environment.byProjectId.useQuery({
|
const { data: environments } = api.environment.byProjectId.useQuery({
|
||||||
projectId: projectId,
|
projectId: projectId,
|
||||||
@@ -905,9 +906,7 @@ const EnvironmentPage = (
|
|||||||
<ProjectEnvironment projectId={projectId}>
|
<ProjectEnvironment projectId={projectId}>
|
||||||
<Button variant="outline">Project Environment</Button>
|
<Button variant="outline">Project Environment</Button>
|
||||||
</ProjectEnvironment>
|
</ProjectEnvironment>
|
||||||
{(auth?.role === "owner" ||
|
{permissions?.service.create && (
|
||||||
auth?.role === "admin" ||
|
|
||||||
auth?.canCreateServices) && (
|
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<Button>
|
<Button>
|
||||||
@@ -1029,9 +1028,7 @@ const EnvironmentPage = (
|
|||||||
Stop
|
Stop
|
||||||
</Button>
|
</Button>
|
||||||
</DialogAction>
|
</DialogAction>
|
||||||
{(auth?.role === "owner" ||
|
{permissions?.service.delete && (
|
||||||
auth?.role === "admin" ||
|
|
||||||
auth?.canDeleteServices) && (
|
|
||||||
<>
|
<>
|
||||||
<DialogAction
|
<DialogAction
|
||||||
title="Delete Services"
|
title="Delete Services"
|
||||||
@@ -1624,6 +1621,7 @@ export async function getServerSideProps(
|
|||||||
environmentId: params.environmentId,
|
environmentId: params.environmentId,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
// If user doesn't have access to requested environment, redirect to accessible one
|
// If user doesn't have access to requested environment, redirect to accessible one
|
||||||
const accessibleEnvironments =
|
const accessibleEnvironments =
|
||||||
await helpers.environment.byProjectId.fetch({
|
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 {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
permanent: false,
|
permanent: false,
|
||||||
destination: "/",
|
destination: "/dashboard/projects",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -1663,7 +1661,8 @@ export async function getServerSideProps(
|
|||||||
environmentId: params.environmentId,
|
environmentId: params.environmentId,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
} catch {
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
permanent: false,
|
permanent: false,
|
||||||
|
|||||||
@@ -92,6 +92,7 @@ const Service = (
|
|||||||
|
|
||||||
const { data: isCloud } = api.settings.isCloud.useQuery();
|
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||||
const { data: auth } = api.user.get.useQuery();
|
const { data: auth } = api.user.get.useQuery();
|
||||||
|
const { data: permissions } = api.user.getPermissions.useQuery();
|
||||||
|
|
||||||
const { data: environments } = api.environment.byProjectId.useQuery({
|
const { data: environments } = api.environment.byProjectId.useQuery({
|
||||||
projectId: data?.environment?.project?.projectId || "",
|
projectId: data?.environment?.project?.projectId || "",
|
||||||
@@ -197,10 +198,10 @@ const Service = (
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-row gap-2 justify-end">
|
<div className="flex flex-row gap-2 justify-end">
|
||||||
<UpdateApplication applicationId={applicationId} />
|
{permissions?.service.create && (
|
||||||
{(auth?.role === "owner" ||
|
<UpdateApplication applicationId={applicationId} />
|
||||||
auth?.role === "admin" ||
|
)}
|
||||||
auth?.canDeleteServices) && (
|
{permissions?.service.delete && (
|
||||||
<DeleteService id={applicationId} type="application" />
|
<DeleteService id={applicationId} type="application" />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -242,24 +243,47 @@ const Service = (
|
|||||||
<div className="flex flex-row items-center justify-between w-full overflow-auto">
|
<div className="flex flex-row items-center justify-between w-full overflow-auto">
|
||||||
<TabsList className="flex gap-8 max-md:gap-4 justify-start">
|
<TabsList className="flex gap-8 max-md:gap-4 justify-start">
|
||||||
<TabsTrigger value="general">General</TabsTrigger>
|
<TabsTrigger value="general">General</TabsTrigger>
|
||||||
<TabsTrigger value="environment">Environment</TabsTrigger>
|
{permissions?.envVars.read && (
|
||||||
<TabsTrigger value="domains">Domains</TabsTrigger>
|
<TabsTrigger value="environment">
|
||||||
<TabsTrigger value="deployments">Deployments</TabsTrigger>
|
Environment
|
||||||
<TabsTrigger value="preview-deployments">
|
</TabsTrigger>
|
||||||
Preview Deployments
|
)}
|
||||||
</TabsTrigger>
|
{permissions?.domain.read && (
|
||||||
<TabsTrigger value="schedules">Schedules</TabsTrigger>
|
<TabsTrigger value="domains">Domains</TabsTrigger>
|
||||||
<TabsTrigger value="volume-backups">
|
)}
|
||||||
Volume Backups
|
{permissions?.deployment.read && (
|
||||||
</TabsTrigger>
|
<TabsTrigger value="deployments">
|
||||||
<TabsTrigger value="logs">Logs</TabsTrigger>
|
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" && (
|
{data?.sourceType !== "docker" && (
|
||||||
<TabsTrigger value="patches">Patches</TabsTrigger>
|
<TabsTrigger value="patches">Patches</TabsTrigger>
|
||||||
)}
|
)}
|
||||||
{((data?.serverId && isCloud) || !data?.server) && (
|
{permissions?.monitoring.read &&
|
||||||
<TabsTrigger value="monitoring">Monitoring</TabsTrigger>
|
((data?.serverId && isCloud) || !data?.server) && (
|
||||||
|
<TabsTrigger value="monitoring">
|
||||||
|
Monitoring
|
||||||
|
</TabsTrigger>
|
||||||
|
)}
|
||||||
|
{permissions?.service.create && (
|
||||||
|
<TabsTrigger value="advanced">Advanced</TabsTrigger>
|
||||||
)}
|
)}
|
||||||
<TabsTrigger value="advanced">Advanced</TabsTrigger>
|
|
||||||
</TabsList>
|
</TabsList>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -268,26 +292,29 @@ const Service = (
|
|||||||
<ShowGeneralApplication applicationId={applicationId} />
|
<ShowGeneralApplication applicationId={applicationId} />
|
||||||
</div>
|
</div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
<TabsContent value="environment">
|
{permissions?.envVars.read && (
|
||||||
<div className="flex flex-col gap-4 pt-2.5">
|
<TabsContent value="environment">
|
||||||
<ShowEnvironment applicationId={applicationId} />
|
<div className="flex flex-col gap-4 pt-2.5">
|
||||||
</div>
|
<ShowEnvironment applicationId={applicationId} />
|
||||||
</TabsContent>
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
)}
|
||||||
|
|
||||||
<TabsContent value="monitoring">
|
{permissions?.monitoring.read && (
|
||||||
<div className="pt-2.5">
|
<TabsContent value="monitoring">
|
||||||
<div className="flex flex-col gap-4 border rounded-lg p-6">
|
<div className="pt-2.5">
|
||||||
{data?.serverId && isCloud ? (
|
<div className="flex flex-col gap-4 border rounded-lg p-6">
|
||||||
<ContainerPaidMonitoring
|
{data?.serverId && isCloud ? (
|
||||||
appName={data?.appName || ""}
|
<ContainerPaidMonitoring
|
||||||
baseUrl={`${data?.serverId ? `http://${data?.server?.ipAddress}:${data?.server?.metricsConfig?.server?.port}` : "http://localhost:4500"}`}
|
appName={data?.appName || ""}
|
||||||
token={
|
baseUrl={`${data?.serverId ? `http://${data?.server?.ipAddress}:${data?.server?.metricsConfig?.server?.port}` : "http://localhost:4500"}`}
|
||||||
data?.server?.metricsConfig?.server?.token || ""
|
token={
|
||||||
}
|
data?.server?.metricsConfig?.server?.token || ""
|
||||||
/>
|
}
|
||||||
) : (
|
/>
|
||||||
<>
|
) : (
|
||||||
{/* {monitoring?.enabledFeatures &&
|
<>
|
||||||
|
{/* {monitoring?.enabledFeatures &&
|
||||||
isCloud &&
|
isCloud &&
|
||||||
data?.serverId && (
|
data?.serverId && (
|
||||||
<div className="flex flex-row border w-fit p-4 rounded-lg items-center gap-2">
|
<div className="flex flex-row border w-fit p-4 rounded-lg items-center gap-2">
|
||||||
@@ -301,7 +328,7 @@ const Service = (
|
|||||||
</div>
|
</div>
|
||||||
)} */}
|
)} */}
|
||||||
|
|
||||||
{/* {toggleMonitoring ? (
|
{/* {toggleMonitoring ? (
|
||||||
<ContainerPaidMonitoring
|
<ContainerPaidMonitoring
|
||||||
appName={data?.appName || ""}
|
appName={data?.appName || ""}
|
||||||
baseUrl={`http://${monitoring?.serverIp}:${monitoring?.metricsConfig?.server?.port}`}
|
baseUrl={`http://${monitoring?.serverIp}:${monitoring?.metricsConfig?.server?.port}`}
|
||||||
@@ -310,84 +337,102 @@ const Service = (
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
) : ( */}
|
) : ( */}
|
||||||
<div>
|
<div>
|
||||||
<ContainerFreeMonitoring
|
<ContainerFreeMonitoring
|
||||||
appName={data?.appName || ""}
|
appName={data?.appName || ""}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{/* )} */}
|
{/* )} */}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</TabsContent>
|
||||||
</TabsContent>
|
)}
|
||||||
|
|
||||||
<TabsContent value="logs">
|
{permissions?.logs.read && (
|
||||||
<div className="flex flex-col gap-4 pt-2.5">
|
<TabsContent value="logs">
|
||||||
<ShowDockerLogs
|
<div className="flex flex-col gap-4 pt-2.5">
|
||||||
appName={data?.appName || ""}
|
<ShowDockerLogs
|
||||||
serverId={data?.serverId || ""}
|
appName={data?.appName || ""}
|
||||||
/>
|
serverId={data?.serverId || ""}
|
||||||
</div>
|
/>
|
||||||
</TabsContent>
|
</div>
|
||||||
<TabsContent value="schedules">
|
</TabsContent>
|
||||||
<div className="flex flex-col gap-4 pt-2.5">
|
)}
|
||||||
<ShowSchedules
|
{permissions?.schedule.read && (
|
||||||
id={applicationId}
|
<TabsContent value="schedules">
|
||||||
scheduleType="application"
|
<div className="flex flex-col gap-4 pt-2.5">
|
||||||
/>
|
<ShowSchedules
|
||||||
</div>
|
id={applicationId}
|
||||||
</TabsContent>
|
scheduleType="application"
|
||||||
<TabsContent value="deployments" className="w-full pt-2.5">
|
/>
|
||||||
<div className="flex flex-col gap-4 border rounded-lg">
|
</div>
|
||||||
<ShowDeployments
|
</TabsContent>
|
||||||
id={applicationId}
|
)}
|
||||||
type="application"
|
{permissions?.deployment.read && (
|
||||||
serverId={data?.serverId || ""}
|
<TabsContent value="deployments" className="w-full pt-2.5">
|
||||||
refreshToken={data?.refreshToken || ""}
|
<div className="flex flex-col gap-4 border rounded-lg">
|
||||||
/>
|
<ShowDeployments
|
||||||
</div>
|
id={applicationId}
|
||||||
</TabsContent>
|
type="application"
|
||||||
<TabsContent value="volume-backups" className="w-full pt-2.5">
|
serverId={data?.serverId || ""}
|
||||||
<div className="flex flex-col gap-4 border rounded-lg">
|
refreshToken={data?.refreshToken || ""}
|
||||||
<ShowVolumeBackups
|
/>
|
||||||
id={applicationId}
|
</div>
|
||||||
type="application"
|
</TabsContent>
|
||||||
serverId={data?.serverId || ""}
|
)}
|
||||||
/>
|
{permissions?.volumeBackup.read && (
|
||||||
</div>
|
<TabsContent
|
||||||
</TabsContent>
|
value="volume-backups"
|
||||||
<TabsContent value="preview-deployments" className="w-full">
|
className="w-full pt-2.5"
|
||||||
<div className="flex flex-col gap-4 pt-2.5">
|
>
|
||||||
<ShowPreviewDeployments applicationId={applicationId} />
|
<div className="flex flex-col gap-4 border rounded-lg">
|
||||||
</div>
|
<ShowVolumeBackups
|
||||||
</TabsContent>
|
id={applicationId}
|
||||||
<TabsContent value="domains" className="w-full">
|
type="application"
|
||||||
<div className="flex flex-col gap-4 pt-2.5">
|
serverId={data?.serverId || ""}
|
||||||
<ShowDomains id={applicationId} type="application" />
|
/>
|
||||||
</div>
|
</div>
|
||||||
</TabsContent>
|
</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">
|
<TabsContent value="patches" className="w-full">
|
||||||
<div className="flex flex-col gap-4 pt-2.5">
|
<div className="flex flex-col gap-4 pt-2.5">
|
||||||
<ShowPatches id={applicationId} type="application" />
|
<ShowPatches id={applicationId} type="application" />
|
||||||
</div>
|
</div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
<TabsContent value="advanced">
|
{permissions?.service.create && (
|
||||||
<div className="flex flex-col gap-4 pt-2.5">
|
<TabsContent value="advanced">
|
||||||
<AddCommand applicationId={applicationId} />
|
<div className="flex flex-col gap-4 pt-2.5">
|
||||||
<ShowClusterSettings
|
<AddCommand applicationId={applicationId} />
|
||||||
id={applicationId}
|
<ShowClusterSettings
|
||||||
type="application"
|
id={applicationId}
|
||||||
/>
|
type="application"
|
||||||
<ShowBuildServer applicationId={applicationId} />
|
/>
|
||||||
<ShowResources id={applicationId} type="application" />
|
<ShowBuildServer applicationId={applicationId} />
|
||||||
<ShowVolumes id={applicationId} type="application" />
|
<ShowResources id={applicationId} type="application" />
|
||||||
<ShowRedirects applicationId={applicationId} />
|
<ShowVolumes id={applicationId} type="application" />
|
||||||
<ShowSecurity applicationId={applicationId} />
|
<ShowRedirects applicationId={applicationId} />
|
||||||
<ShowPorts applicationId={applicationId} />
|
<ShowSecurity applicationId={applicationId} />
|
||||||
<ShowTraefikConfig applicationId={applicationId} />
|
<ShowPorts applicationId={applicationId} />
|
||||||
</div>
|
<ShowTraefikConfig applicationId={applicationId} />
|
||||||
</TabsContent>
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
)}
|
||||||
</Tabs>
|
</Tabs>
|
||||||
)}
|
)}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|||||||
@@ -81,6 +81,7 @@ const Service = (
|
|||||||
const { data } = api.compose.one.useQuery({ composeId });
|
const { data } = api.compose.one.useQuery({ composeId });
|
||||||
|
|
||||||
const { data: auth } = api.user.get.useQuery();
|
const { data: auth } = api.user.get.useQuery();
|
||||||
|
const { data: permissions } = api.user.getPermissions.useQuery();
|
||||||
const { data: isCloud } = api.settings.isCloud.useQuery();
|
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||||
const { data: environments } = api.environment.byProjectId.useQuery({
|
const { data: environments } = api.environment.byProjectId.useQuery({
|
||||||
projectId: data?.environment?.projectId || "",
|
projectId: data?.environment?.projectId || "",
|
||||||
@@ -185,11 +186,11 @@ const Service = (
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-row gap-2 justify-end">
|
<div className="flex flex-row gap-2 justify-end">
|
||||||
<UpdateCompose composeId={composeId} />
|
{permissions?.service.create && (
|
||||||
|
<UpdateCompose composeId={composeId} />
|
||||||
|
)}
|
||||||
|
|
||||||
{(auth?.role === "owner" ||
|
{permissions?.service.delete && (
|
||||||
auth?.role === "admin" ||
|
|
||||||
auth?.canDeleteServices) && (
|
|
||||||
<DeleteService id={composeId} type="compose" />
|
<DeleteService id={composeId} type="compose" />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -232,22 +233,45 @@ const Service = (
|
|||||||
<div className="flex flex-row items-center w-full overflow-auto">
|
<div className="flex flex-row items-center w-full overflow-auto">
|
||||||
<TabsList className="flex gap-8 max-md:gap-4 justify-start">
|
<TabsList className="flex gap-8 max-md:gap-4 justify-start">
|
||||||
<TabsTrigger value="general">General</TabsTrigger>
|
<TabsTrigger value="general">General</TabsTrigger>
|
||||||
<TabsTrigger value="environment">Environment</TabsTrigger>
|
{permissions?.envVars.read && (
|
||||||
<TabsTrigger value="domains">Domains</TabsTrigger>
|
<TabsTrigger value="environment">
|
||||||
<TabsTrigger value="deployments">Deployments</TabsTrigger>
|
Environment
|
||||||
<TabsTrigger value="backups">Backups</TabsTrigger>
|
</TabsTrigger>
|
||||||
<TabsTrigger value="schedules">Schedules</TabsTrigger>
|
)}
|
||||||
<TabsTrigger value="volumeBackups">
|
{permissions?.domain.read && (
|
||||||
Volume Backups
|
<TabsTrigger value="domains">Domains</TabsTrigger>
|
||||||
</TabsTrigger>
|
)}
|
||||||
<TabsTrigger value="logs">Logs</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" && (
|
{data?.sourceType !== "raw" && (
|
||||||
<TabsTrigger value="patches">Patches</TabsTrigger>
|
<TabsTrigger value="patches">Patches</TabsTrigger>
|
||||||
)}
|
)}
|
||||||
{((data?.serverId && isCloud) || !data?.server) && (
|
{permissions?.monitoring.read &&
|
||||||
<TabsTrigger value="monitoring">Monitoring</TabsTrigger>
|
((data?.serverId && isCloud) || !data?.server) && (
|
||||||
|
<TabsTrigger value="monitoring">
|
||||||
|
Monitoring
|
||||||
|
</TabsTrigger>
|
||||||
|
)}
|
||||||
|
{permissions?.service.create && (
|
||||||
|
<TabsTrigger value="advanced">Advanced</TabsTrigger>
|
||||||
)}
|
)}
|
||||||
<TabsTrigger value="advanced">Advanced</TabsTrigger>
|
|
||||||
</TabsList>
|
</TabsList>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -256,47 +280,56 @@ const Service = (
|
|||||||
<ShowGeneralCompose composeId={composeId} />
|
<ShowGeneralCompose composeId={composeId} />
|
||||||
</div>
|
</div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
<TabsContent value="environment">
|
{permissions?.envVars.read && (
|
||||||
<div className="flex flex-col gap-4 pt-2.5">
|
<TabsContent value="environment">
|
||||||
<ShowEnvironment id={composeId} type="compose" />
|
<div className="flex flex-col gap-4 pt-2.5">
|
||||||
</div>
|
<ShowEnvironment id={composeId} type="compose" />
|
||||||
</TabsContent>
|
</div>
|
||||||
<TabsContent value="backups">
|
</TabsContent>
|
||||||
<div className="flex flex-col gap-4 pt-2.5">
|
)}
|
||||||
<ShowBackups id={composeId} backupType="compose" />
|
{permissions?.service.create && (
|
||||||
</div>
|
<TabsContent value="backups">
|
||||||
</TabsContent>
|
<div className="flex flex-col gap-4 pt-2.5">
|
||||||
|
<ShowBackups id={composeId} backupType="compose" />
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
)}
|
||||||
|
|
||||||
<TabsContent value="schedules">
|
{permissions?.schedule.read && (
|
||||||
<div className="flex flex-col gap-4 pt-2.5">
|
<TabsContent value="schedules">
|
||||||
<ShowSchedules id={composeId} scheduleType="compose" />
|
<div className="flex flex-col gap-4 pt-2.5">
|
||||||
</div>
|
<ShowSchedules id={composeId} scheduleType="compose" />
|
||||||
</TabsContent>
|
</div>
|
||||||
<TabsContent value="volumeBackups">
|
</TabsContent>
|
||||||
<div className="flex flex-col gap-4 pt-2.5">
|
)}
|
||||||
<ShowVolumeBackups
|
{permissions?.volumeBackup.read && (
|
||||||
id={composeId}
|
<TabsContent value="volumeBackups">
|
||||||
type="compose"
|
<div className="flex flex-col gap-4 pt-2.5">
|
||||||
serverId={data?.serverId || ""}
|
<ShowVolumeBackups
|
||||||
/>
|
id={composeId}
|
||||||
</div>
|
type="compose"
|
||||||
</TabsContent>
|
serverId={data?.serverId || ""}
|
||||||
<TabsContent value="monitoring">
|
/>
|
||||||
<div className="pt-2.5">
|
</div>
|
||||||
<div className="flex flex-col border rounded-lg ">
|
</TabsContent>
|
||||||
{data?.serverId && isCloud ? (
|
)}
|
||||||
<ComposePaidMonitoring
|
{permissions?.monitoring.read && (
|
||||||
serverId={data?.serverId || ""}
|
<TabsContent value="monitoring">
|
||||||
baseUrl={`${data?.serverId ? `http://${data?.server?.ipAddress}:${data?.server?.metricsConfig?.server?.port}` : "http://localhost:4500"}`}
|
<div className="pt-2.5">
|
||||||
appName={data?.appName || ""}
|
<div className="flex flex-col border rounded-lg ">
|
||||||
token={
|
{data?.serverId && isCloud ? (
|
||||||
data?.server?.metricsConfig?.server?.token || ""
|
<ComposePaidMonitoring
|
||||||
}
|
serverId={data?.serverId || ""}
|
||||||
appType={data?.composeType || "docker-compose"}
|
baseUrl={`${data?.serverId ? `http://${data?.server?.ipAddress}:${data?.server?.metricsConfig?.server?.port}` : "http://localhost:4500"}`}
|
||||||
/>
|
appName={data?.appName || ""}
|
||||||
) : (
|
token={
|
||||||
<>
|
data?.server?.metricsConfig?.server?.token || ""
|
||||||
{/* {monitoring?.enabledFeatures &&
|
}
|
||||||
|
appType={data?.composeType || "docker-compose"}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{/* {monitoring?.enabledFeatures &&
|
||||||
isCloud &&
|
isCloud &&
|
||||||
data?.serverId && (
|
data?.serverId && (
|
||||||
<div className="flex flex-row border w-fit p-4 rounded-lg items-center gap-2 m-4">
|
<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"}
|
appType={data?.composeType || "docker-compose"}
|
||||||
/>
|
/>
|
||||||
) : ( */}
|
) : ( */}
|
||||||
{/* <div> */}
|
{/* <div> */}
|
||||||
<ComposeFreeMonitoring
|
<ComposeFreeMonitoring
|
||||||
serverId={data?.serverId || ""}
|
serverId={data?.serverId || ""}
|
||||||
appName={data?.appName || ""}
|
appName={data?.appName || ""}
|
||||||
appType={data?.composeType || "docker-compose"}
|
appType={data?.composeType || "docker-compose"}
|
||||||
/>
|
/>
|
||||||
{/* </div> */}
|
{/* </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>
|
||||||
</div>
|
</TabsContent>
|
||||||
</TabsContent>
|
)}
|
||||||
|
|
||||||
<TabsContent value="logs">
|
{permissions?.deployment.read && (
|
||||||
<div className="flex flex-col gap-4 pt-2.5">
|
<TabsContent value="deployments" className="w-full pt-2.5">
|
||||||
{data?.composeType === "docker-compose" ? (
|
<div className="flex flex-col gap-4 border rounded-lg">
|
||||||
<ShowDockerLogsCompose
|
<ShowDeployments
|
||||||
|
id={composeId}
|
||||||
|
type="compose"
|
||||||
serverId={data?.serverId || ""}
|
serverId={data?.serverId || ""}
|
||||||
appName={data?.appName || ""}
|
refreshToken={data?.refreshToken || ""}
|
||||||
appType={data?.composeType || "docker-compose"}
|
|
||||||
/>
|
/>
|
||||||
) : (
|
</div>
|
||||||
<ShowDockerLogsStack
|
</TabsContent>
|
||||||
serverId={data?.serverId || ""}
|
)}
|
||||||
appName={data?.appName || ""}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</TabsContent>
|
|
||||||
|
|
||||||
<TabsContent value="deployments" className="w-full pt-2.5">
|
{permissions?.domain.read && (
|
||||||
<div className="flex flex-col gap-4 border rounded-lg">
|
<TabsContent value="domains">
|
||||||
<ShowDeployments
|
<div className="flex flex-col gap-4 pt-2.5">
|
||||||
id={composeId}
|
<ShowDomains id={composeId} type="compose" />
|
||||||
type="compose"
|
</div>
|
||||||
serverId={data?.serverId || ""}
|
</TabsContent>
|
||||||
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>
|
|
||||||
|
|
||||||
<TabsContent value="patches" className="w-full">
|
<TabsContent value="patches" className="w-full">
|
||||||
<div className="flex flex-col gap-4 pt-2.5">
|
<div className="flex flex-col gap-4 pt-2.5">
|
||||||
@@ -374,14 +414,16 @@ const Service = (
|
|||||||
</div>
|
</div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
<TabsContent value="advanced">
|
{permissions?.service.create && (
|
||||||
<div className="flex flex-col gap-4 pt-2.5">
|
<TabsContent value="advanced">
|
||||||
<AddCommandCompose composeId={composeId} />
|
<div className="flex flex-col gap-4 pt-2.5">
|
||||||
<ShowVolumes id={composeId} type="compose" />
|
<AddCommandCompose composeId={composeId} />
|
||||||
<ShowImport composeId={composeId} />
|
<ShowVolumes id={composeId} type="compose" />
|
||||||
<IsolatedDeploymentTab composeId={composeId} />
|
<ShowImport composeId={composeId} />
|
||||||
</div>
|
<IsolatedDeploymentTab composeId={composeId} />
|
||||||
</TabsContent>
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
)}
|
||||||
</Tabs>
|
</Tabs>
|
||||||
)}
|
)}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|||||||
@@ -60,6 +60,7 @@ const Mariadb = (
|
|||||||
const [tab, setSab] = useState<TabState>(activeTab);
|
const [tab, setSab] = useState<TabState>(activeTab);
|
||||||
const { data } = api.mariadb.one.useQuery({ mariadbId });
|
const { data } = api.mariadb.one.useQuery({ mariadbId });
|
||||||
const { data: auth } = api.user.get.useQuery();
|
const { data: auth } = api.user.get.useQuery();
|
||||||
|
const { data: permissions } = api.user.getPermissions.useQuery();
|
||||||
|
|
||||||
const { data: isCloud } = api.settings.isCloud.useQuery();
|
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||||
|
|
||||||
@@ -159,10 +160,10 @@ const Mariadb = (
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-row gap-2 justify-end">
|
<div className="flex flex-row gap-2 justify-end">
|
||||||
<UpdateMariadb mariadbId={mariadbId} />
|
{permissions?.service.create && (
|
||||||
{(auth?.role === "owner" ||
|
<UpdateMariadb mariadbId={mariadbId} />
|
||||||
auth?.role === "admin" ||
|
)}
|
||||||
auth?.canDeleteServices) && (
|
{permissions?.service.delete && (
|
||||||
<DeleteService id={mariadbId} type="mariadb" />
|
<DeleteService id={mariadbId} type="mariadb" />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -214,13 +215,24 @@ const Mariadb = (
|
|||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<TabsTrigger value="general">General</TabsTrigger>
|
<TabsTrigger value="general">General</TabsTrigger>
|
||||||
<TabsTrigger value="environment">Environment</TabsTrigger>
|
{permissions?.envVars.read && (
|
||||||
<TabsTrigger value="logs">Logs</TabsTrigger>
|
<TabsTrigger value="environment">
|
||||||
{((data?.serverId && isCloud) || !data?.server) && (
|
Environment
|
||||||
<TabsTrigger value="monitoring">Monitoring</TabsTrigger>
|
</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="backups">Backups</TabsTrigger>
|
||||||
<TabsTrigger value="advanced">Advanced</TabsTrigger>
|
{permissions?.service.create && (
|
||||||
|
<TabsTrigger value="advanced">Advanced</TabsTrigger>
|
||||||
|
)}
|
||||||
</TabsList>
|
</TabsList>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -231,25 +243,28 @@ const Mariadb = (
|
|||||||
<ShowExternalMariadbCredentials mariadbId={mariadbId} />
|
<ShowExternalMariadbCredentials mariadbId={mariadbId} />
|
||||||
</div>
|
</div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
<TabsContent value="environment">
|
{permissions?.envVars.read && (
|
||||||
<div className="flex flex-col gap-4 pt-2.5">
|
<TabsContent value="environment">
|
||||||
<ShowEnvironment id={mariadbId} type="mariadb" />
|
<div className="flex flex-col gap-4 pt-2.5">
|
||||||
</div>
|
<ShowEnvironment id={mariadbId} type="mariadb" />
|
||||||
</TabsContent>
|
</div>
|
||||||
<TabsContent value="monitoring">
|
</TabsContent>
|
||||||
<div className="pt-2.5">
|
)}
|
||||||
<div className="flex flex-col gap-4 border rounded-lg p-6">
|
{permissions?.monitoring.read && (
|
||||||
{data?.serverId && isCloud ? (
|
<TabsContent value="monitoring">
|
||||||
<ContainerPaidMonitoring
|
<div className="pt-2.5">
|
||||||
appName={data?.appName || ""}
|
<div className="flex flex-col gap-4 border rounded-lg p-6">
|
||||||
baseUrl={`${data?.serverId ? `http://${data?.server?.ipAddress}:${data?.server?.metricsConfig?.server?.port}` : "http://localhost:4500"}`}
|
{data?.serverId && isCloud ? (
|
||||||
token={
|
<ContainerPaidMonitoring
|
||||||
data?.server?.metricsConfig?.server?.token || ""
|
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 && (
|
/>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{/* {monitoring?.enabledFeatures && (
|
||||||
<div className="flex flex-row border w-fit p-4 rounded-lg items-center gap-2">
|
<div className="flex flex-row border w-fit p-4 rounded-lg items-center gap-2">
|
||||||
<Label className="text-muted-foreground">
|
<Label className="text-muted-foreground">
|
||||||
Change Monitoring
|
Change Monitoring
|
||||||
@@ -271,37 +286,42 @@ const Mariadb = (
|
|||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<div> */}
|
<div> */}
|
||||||
<ContainerFreeMonitoring
|
<ContainerFreeMonitoring
|
||||||
appName={data?.appName || ""}
|
appName={data?.appName || ""}
|
||||||
/>
|
/>
|
||||||
{/* </div> */}
|
{/* </div> */}
|
||||||
{/* )} */}
|
{/* )} */}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</TabsContent>
|
||||||
</TabsContent>
|
)}
|
||||||
<TabsContent value="logs">
|
{permissions?.logs.read && (
|
||||||
<div className="flex flex-col gap-4 pt-2.5">
|
<TabsContent value="logs">
|
||||||
<ShowDockerLogs
|
<div className="flex flex-col gap-4 pt-2.5">
|
||||||
serverId={data?.serverId || ""}
|
<ShowDockerLogs
|
||||||
appName={data?.appName || ""}
|
serverId={data?.serverId || ""}
|
||||||
/>
|
appName={data?.appName || ""}
|
||||||
</div>
|
/>
|
||||||
</TabsContent>
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
)}
|
||||||
<TabsContent value="backups">
|
<TabsContent value="backups">
|
||||||
<div className="flex flex-col gap-4 pt-2.5">
|
<div className="flex flex-col gap-4 pt-2.5">
|
||||||
<ShowBackups id={mariadbId} databaseType="mariadb" />
|
<ShowBackups id={mariadbId} databaseType="mariadb" />
|
||||||
</div>
|
</div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
<TabsContent value="advanced">
|
{permissions?.service.create && (
|
||||||
<div className="flex flex-col gap-4 pt-2.5">
|
<TabsContent value="advanced">
|
||||||
<ShowDatabaseAdvancedSettings
|
<div className="flex flex-col gap-4 pt-2.5">
|
||||||
id={mariadbId}
|
<ShowDatabaseAdvancedSettings
|
||||||
type="mariadb"
|
id={mariadbId}
|
||||||
/>
|
type="mariadb"
|
||||||
</div>
|
/>
|
||||||
</TabsContent>
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
)}
|
||||||
</Tabs>
|
</Tabs>
|
||||||
)}
|
)}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|||||||
@@ -60,6 +60,7 @@ const Mongo = (
|
|||||||
const { data } = api.mongo.one.useQuery({ mongoId });
|
const { data } = api.mongo.one.useQuery({ mongoId });
|
||||||
|
|
||||||
const { data: auth } = api.user.get.useQuery();
|
const { data: auth } = api.user.get.useQuery();
|
||||||
|
const { data: permissions } = api.user.getPermissions.useQuery();
|
||||||
|
|
||||||
const { data: isCloud } = api.settings.isCloud.useQuery();
|
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||||
const { data: environments } = api.environment.byProjectId.useQuery({
|
const { data: environments } = api.environment.byProjectId.useQuery({
|
||||||
@@ -159,10 +160,10 @@ const Mongo = (
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-row gap-2 justify-end">
|
<div className="flex flex-row gap-2 justify-end">
|
||||||
<UpdateMongo mongoId={mongoId} />
|
{permissions?.service.create && (
|
||||||
{(auth?.role === "owner" ||
|
<UpdateMongo mongoId={mongoId} />
|
||||||
auth?.role === "admin" ||
|
)}
|
||||||
auth?.canDeleteServices) && (
|
{permissions?.service.delete && (
|
||||||
<DeleteService id={mongoId} type="mongo" />
|
<DeleteService id={mongoId} type="mongo" />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -214,13 +215,24 @@ const Mongo = (
|
|||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<TabsTrigger value="general">General</TabsTrigger>
|
<TabsTrigger value="general">General</TabsTrigger>
|
||||||
<TabsTrigger value="environment">Environment</TabsTrigger>
|
{permissions?.envVars.read && (
|
||||||
<TabsTrigger value="logs">Logs</TabsTrigger>
|
<TabsTrigger value="environment">
|
||||||
{((data?.serverId && isCloud) || !data?.server) && (
|
Environment
|
||||||
<TabsTrigger value="monitoring">Monitoring</TabsTrigger>
|
</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="backups">Backups</TabsTrigger>
|
||||||
<TabsTrigger value="advanced">Advanced</TabsTrigger>
|
{permissions?.service.create && (
|
||||||
|
<TabsTrigger value="advanced">Advanced</TabsTrigger>
|
||||||
|
)}
|
||||||
</TabsList>
|
</TabsList>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -231,25 +243,28 @@ const Mongo = (
|
|||||||
<ShowExternalMongoCredentials mongoId={mongoId} />
|
<ShowExternalMongoCredentials mongoId={mongoId} />
|
||||||
</div>
|
</div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
<TabsContent value="environment">
|
{permissions?.envVars.read && (
|
||||||
<div className="flex flex-col gap-4 pt-2.5">
|
<TabsContent value="environment">
|
||||||
<ShowEnvironment id={mongoId} type="mongo" />
|
<div className="flex flex-col gap-4 pt-2.5">
|
||||||
</div>
|
<ShowEnvironment id={mongoId} type="mongo" />
|
||||||
</TabsContent>
|
</div>
|
||||||
<TabsContent value="monitoring">
|
</TabsContent>
|
||||||
<div className="pt-2.5">
|
)}
|
||||||
<div className="flex flex-col gap-4 border rounded-lg p-6">
|
{permissions?.monitoring.read && (
|
||||||
{data?.serverId && isCloud ? (
|
<TabsContent value="monitoring">
|
||||||
<ContainerPaidMonitoring
|
<div className="pt-2.5">
|
||||||
appName={data?.appName || ""}
|
<div className="flex flex-col gap-4 border rounded-lg p-6">
|
||||||
baseUrl={`${data?.serverId ? `http://${data?.server?.ipAddress}:${data?.server?.metricsConfig?.server?.port}` : "http://localhost:4500"}`}
|
{data?.serverId && isCloud ? (
|
||||||
token={
|
<ContainerPaidMonitoring
|
||||||
data?.server?.metricsConfig?.server?.token || ""
|
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 && (
|
/>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{/* {monitoring?.enabledFeatures && (
|
||||||
<div className="flex flex-row border w-fit p-4 rounded-lg items-center gap-2">
|
<div className="flex flex-row border w-fit p-4 rounded-lg items-center gap-2">
|
||||||
<Label className="text-muted-foreground">
|
<Label className="text-muted-foreground">
|
||||||
Change Monitoring
|
Change Monitoring
|
||||||
@@ -271,24 +286,27 @@ const Mongo = (
|
|||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<div> */}
|
<div> */}
|
||||||
<ContainerFreeMonitoring
|
<ContainerFreeMonitoring
|
||||||
appName={data?.appName || ""}
|
appName={data?.appName || ""}
|
||||||
/>
|
/>
|
||||||
{/* </div> */}
|
{/* </div> */}
|
||||||
{/* )} */}
|
{/* )} */}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</TabsContent>
|
||||||
</TabsContent>
|
)}
|
||||||
<TabsContent value="logs">
|
{permissions?.logs.read && (
|
||||||
<div className="flex flex-col gap-4 pt-2.5">
|
<TabsContent value="logs">
|
||||||
<ShowDockerLogs
|
<div className="flex flex-col gap-4 pt-2.5">
|
||||||
serverId={data?.serverId || ""}
|
<ShowDockerLogs
|
||||||
appName={data?.appName || ""}
|
serverId={data?.serverId || ""}
|
||||||
/>
|
appName={data?.appName || ""}
|
||||||
</div>
|
/>
|
||||||
</TabsContent>
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
)}
|
||||||
<TabsContent value="backups">
|
<TabsContent value="backups">
|
||||||
<div className="flex flex-col gap-4 pt-2.5">
|
<div className="flex flex-col gap-4 pt-2.5">
|
||||||
<ShowBackups
|
<ShowBackups
|
||||||
@@ -298,11 +316,16 @@ const Mongo = (
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
<TabsContent value="advanced">
|
{permissions?.service.create && (
|
||||||
<div className="flex flex-col gap-4 pt-2.5">
|
<TabsContent value="advanced">
|
||||||
<ShowDatabaseAdvancedSettings id={mongoId} type="mongo" />
|
<div className="flex flex-col gap-4 pt-2.5">
|
||||||
</div>
|
<ShowDatabaseAdvancedSettings
|
||||||
</TabsContent>
|
id={mongoId}
|
||||||
|
type="mongo"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
)}
|
||||||
</Tabs>
|
</Tabs>
|
||||||
)}
|
)}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|||||||
@@ -59,6 +59,7 @@ const MySql = (
|
|||||||
const [tab, setSab] = useState<TabState>(activeTab);
|
const [tab, setSab] = useState<TabState>(activeTab);
|
||||||
const { data } = api.mysql.one.useQuery({ mysqlId });
|
const { data } = api.mysql.one.useQuery({ mysqlId });
|
||||||
const { data: auth } = api.user.get.useQuery();
|
const { data: auth } = api.user.get.useQuery();
|
||||||
|
const { data: permissions } = api.user.getPermissions.useQuery();
|
||||||
|
|
||||||
const { data: isCloud } = api.settings.isCloud.useQuery();
|
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||||
const { data: environments } = api.environment.byProjectId.useQuery({
|
const { data: environments } = api.environment.byProjectId.useQuery({
|
||||||
@@ -159,10 +160,10 @@ const MySql = (
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-row gap-2 justify-end">
|
<div className="flex flex-row gap-2 justify-end">
|
||||||
<UpdateMysql mysqlId={mysqlId} />
|
{permissions?.service.create && (
|
||||||
{(auth?.role === "owner" ||
|
<UpdateMysql mysqlId={mysqlId} />
|
||||||
auth?.role === "admin" ||
|
)}
|
||||||
auth?.canDeleteServices) && (
|
{permissions?.service.delete && (
|
||||||
<DeleteService id={mysqlId} type="mysql" />
|
<DeleteService id={mysqlId} type="mysql" />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -214,17 +215,24 @@ const MySql = (
|
|||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<TabsTrigger value="general">General</TabsTrigger>
|
<TabsTrigger value="general">General</TabsTrigger>
|
||||||
<TabsTrigger value="environment">
|
{permissions?.envVars.read && (
|
||||||
Environment
|
<TabsTrigger value="environment">
|
||||||
</TabsTrigger>
|
Environment
|
||||||
<TabsTrigger value="logs">Logs</TabsTrigger>
|
|
||||||
{((data?.serverId && isCloud) || !data?.server) && (
|
|
||||||
<TabsTrigger value="monitoring">
|
|
||||||
Monitoring
|
|
||||||
</TabsTrigger>
|
</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="backups">Backups</TabsTrigger>
|
||||||
<TabsTrigger value="advanced">Advanced</TabsTrigger>
|
{permissions?.service.create && (
|
||||||
|
<TabsTrigger value="advanced">Advanced</TabsTrigger>
|
||||||
|
)}
|
||||||
</TabsList>
|
</TabsList>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -235,40 +243,47 @@ const MySql = (
|
|||||||
<ShowExternalMysqlCredentials mysqlId={mysqlId} />
|
<ShowExternalMysqlCredentials mysqlId={mysqlId} />
|
||||||
</div>
|
</div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
<TabsContent value="environment" className="w-full">
|
{permissions?.envVars.read && (
|
||||||
<div className="flex flex-col gap-4 pt-2.5">
|
<TabsContent value="environment" className="w-full">
|
||||||
<ShowEnvironment id={mysqlId} type="mysql" />
|
<div className="flex flex-col gap-4 pt-2.5">
|
||||||
</div>
|
<ShowEnvironment id={mysqlId} type="mysql" />
|
||||||
</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 || ""}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</TabsContent>
|
||||||
</TabsContent>
|
)}
|
||||||
<TabsContent value="logs">
|
{permissions?.monitoring.read && (
|
||||||
<div className="flex flex-col gap-4 pt-2.5">
|
<TabsContent value="monitoring">
|
||||||
<ShowDockerLogs
|
<div className="pt-2.5">
|
||||||
serverId={data?.serverId || ""}
|
<div className="flex flex-col gap-4 border rounded-lg p-6">
|
||||||
appName={data?.appName || ""}
|
{data?.serverId && isCloud ? (
|
||||||
/>
|
<ContainerPaidMonitoring
|
||||||
</div>
|
appName={data?.appName || ""}
|
||||||
</TabsContent>
|
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">
|
<TabsContent value="backups">
|
||||||
<div className="flex flex-col gap-4 pt-2.5">
|
<div className="flex flex-col gap-4 pt-2.5">
|
||||||
<ShowBackups
|
<ShowBackups
|
||||||
@@ -278,14 +293,16 @@ const MySql = (
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
<TabsContent value="advanced">
|
{permissions?.service.create && (
|
||||||
<div className="flex flex-col gap-4 pt-2.5">
|
<TabsContent value="advanced">
|
||||||
<ShowDatabaseAdvancedSettings
|
<div className="flex flex-col gap-4 pt-2.5">
|
||||||
id={mysqlId}
|
<ShowDatabaseAdvancedSettings
|
||||||
type="mysql"
|
id={mysqlId}
|
||||||
/>
|
type="mysql"
|
||||||
</div>
|
/>
|
||||||
</TabsContent>
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
)}
|
||||||
</Tabs>
|
</Tabs>
|
||||||
)}
|
)}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|||||||
@@ -59,6 +59,7 @@ const Postgresql = (
|
|||||||
const [tab, setSab] = useState<TabState>(activeTab);
|
const [tab, setSab] = useState<TabState>(activeTab);
|
||||||
const { data } = api.postgres.one.useQuery({ postgresId });
|
const { data } = api.postgres.one.useQuery({ postgresId });
|
||||||
const { data: auth } = api.user.get.useQuery();
|
const { data: auth } = api.user.get.useQuery();
|
||||||
|
const { data: permissions } = api.user.getPermissions.useQuery();
|
||||||
|
|
||||||
const { data: isCloud } = api.settings.isCloud.useQuery();
|
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||||
const { data: environments } = api.environment.byProjectId.useQuery({
|
const { data: environments } = api.environment.byProjectId.useQuery({
|
||||||
@@ -158,10 +159,10 @@ const Postgresql = (
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-row gap-2 justify-end">
|
<div className="flex flex-row gap-2 justify-end">
|
||||||
<UpdatePostgres postgresId={postgresId} />
|
{permissions?.service.create && (
|
||||||
{(auth?.role === "owner" ||
|
<UpdatePostgres postgresId={postgresId} />
|
||||||
auth?.role === "admin" ||
|
)}
|
||||||
auth?.canDeleteServices) && (
|
{permissions?.service.delete && (
|
||||||
<DeleteService id={postgresId} type="postgres" />
|
<DeleteService id={postgresId} type="postgres" />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -215,13 +216,24 @@ const Postgresql = (
|
|||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<TabsTrigger value="general">General</TabsTrigger>
|
<TabsTrigger value="general">General</TabsTrigger>
|
||||||
<TabsTrigger value="environment">Environment</TabsTrigger>
|
{permissions?.envVars.read && (
|
||||||
<TabsTrigger value="logs">Logs</TabsTrigger>
|
<TabsTrigger value="environment">
|
||||||
{((data?.serverId && isCloud) || !data?.server) && (
|
Environment
|
||||||
<TabsTrigger value="monitoring">Monitoring</TabsTrigger>
|
</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="backups">Backups</TabsTrigger>
|
||||||
<TabsTrigger value="advanced">Advanced</TabsTrigger>
|
{permissions?.service.create && (
|
||||||
|
<TabsTrigger value="advanced">Advanced</TabsTrigger>
|
||||||
|
)}
|
||||||
</TabsList>
|
</TabsList>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -236,44 +248,50 @@ const Postgresql = (
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
<TabsContent value="environment">
|
{permissions?.envVars.read && (
|
||||||
<div className="flex flex-col gap-4 pt-2.5">
|
<TabsContent value="environment">
|
||||||
<ShowEnvironment id={postgresId} type="postgres" />
|
<div className="flex flex-col gap-4 pt-2.5">
|
||||||
</div>
|
<ShowEnvironment id={postgresId} type="postgres" />
|
||||||
</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 || ""}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</TabsContent>
|
||||||
</TabsContent>
|
)}
|
||||||
<TabsContent value="logs">
|
{permissions?.monitoring.read && (
|
||||||
<div className="flex flex-col gap-4 pt-2.5">
|
<TabsContent value="monitoring">
|
||||||
<ShowDockerLogs
|
<div className="pt-2.5">
|
||||||
serverId={data?.serverId || ""}
|
<div className="flex flex-col gap-4 border rounded-lg p-6">
|
||||||
appName={data?.appName || ""}
|
{data?.serverId && isCloud ? (
|
||||||
/>
|
<ContainerPaidMonitoring
|
||||||
</div>
|
appName={data?.appName || ""}
|
||||||
</TabsContent>
|
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">
|
<TabsContent value="backups">
|
||||||
<div className="flex flex-col gap-4 pt-2.5">
|
<div className="flex flex-col gap-4 pt-2.5">
|
||||||
<ShowBackups
|
<ShowBackups
|
||||||
@@ -283,14 +301,16 @@ const Postgresql = (
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
<TabsContent value="advanced">
|
{permissions?.service.create && (
|
||||||
<div className="flex flex-col gap-4 pt-2.5">
|
<TabsContent value="advanced">
|
||||||
<ShowDatabaseAdvancedSettings
|
<div className="flex flex-col gap-4 pt-2.5">
|
||||||
id={postgresId}
|
<ShowDatabaseAdvancedSettings
|
||||||
type="postgres"
|
id={postgresId}
|
||||||
/>
|
type="postgres"
|
||||||
</div>
|
/>
|
||||||
</TabsContent>
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
)}
|
||||||
</Tabs>
|
</Tabs>
|
||||||
)}
|
)}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|||||||
@@ -59,6 +59,7 @@ const Redis = (
|
|||||||
const { data } = api.redis.one.useQuery({ redisId });
|
const { data } = api.redis.one.useQuery({ redisId });
|
||||||
|
|
||||||
const { data: auth } = api.user.get.useQuery();
|
const { data: auth } = api.user.get.useQuery();
|
||||||
|
const { data: permissions } = api.user.getPermissions.useQuery();
|
||||||
|
|
||||||
const { data: isCloud } = api.settings.isCloud.useQuery();
|
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||||
const { data: environments } = api.environment.byProjectId.useQuery({
|
const { data: environments } = api.environment.byProjectId.useQuery({
|
||||||
@@ -158,10 +159,10 @@ const Redis = (
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-row gap-2 justify-end">
|
<div className="flex flex-row gap-2 justify-end">
|
||||||
<UpdateRedis redisId={redisId} />
|
{permissions?.service.create && (
|
||||||
{(auth?.role === "owner" ||
|
<UpdateRedis redisId={redisId} />
|
||||||
auth?.role === "admin" ||
|
)}
|
||||||
auth?.canDeleteServices) && (
|
{permissions?.service.delete && (
|
||||||
<DeleteService id={redisId} type="redis" />
|
<DeleteService id={redisId} type="redis" />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -213,12 +214,23 @@ const Redis = (
|
|||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<TabsTrigger value="general">General</TabsTrigger>
|
<TabsTrigger value="general">General</TabsTrigger>
|
||||||
<TabsTrigger value="environment">Environment</TabsTrigger>
|
{permissions?.envVars.read && (
|
||||||
<TabsTrigger value="logs">Logs</TabsTrigger>
|
<TabsTrigger value="environment">
|
||||||
{((data?.serverId && isCloud) || !data?.server) && (
|
Environment
|
||||||
<TabsTrigger value="monitoring">Monitoring</TabsTrigger>
|
</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>
|
</TabsList>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -229,25 +241,28 @@ const Redis = (
|
|||||||
<ShowExternalRedisCredentials redisId={redisId} />
|
<ShowExternalRedisCredentials redisId={redisId} />
|
||||||
</div>
|
</div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
<TabsContent value="environment">
|
{permissions?.envVars.read && (
|
||||||
<div className="flex flex-col gap-4 pt-2.5">
|
<TabsContent value="environment">
|
||||||
<ShowEnvironment id={redisId} type="redis" />
|
<div className="flex flex-col gap-4 pt-2.5">
|
||||||
</div>
|
<ShowEnvironment id={redisId} type="redis" />
|
||||||
</TabsContent>
|
</div>
|
||||||
<TabsContent value="monitoring">
|
</TabsContent>
|
||||||
<div className="pt-2.5">
|
)}
|
||||||
<div className="flex flex-col gap-4 border rounded-lg p-6">
|
{permissions?.monitoring.read && (
|
||||||
{data?.serverId && isCloud ? (
|
<TabsContent value="monitoring">
|
||||||
<ContainerPaidMonitoring
|
<div className="pt-2.5">
|
||||||
appName={data?.appName || ""}
|
<div className="flex flex-col gap-4 border rounded-lg p-6">
|
||||||
baseUrl={`${data?.serverId ? `http://${data?.server?.ipAddress}:${data?.server?.metricsConfig?.server?.port}` : "http://localhost:4500"}`}
|
{data?.serverId && isCloud ? (
|
||||||
token={
|
<ContainerPaidMonitoring
|
||||||
data?.server?.metricsConfig?.server?.token || ""
|
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 && (
|
/>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{/* {monitoring?.enabledFeatures && (
|
||||||
<div className="flex flex-row border w-fit p-4 rounded-lg items-center gap-2">
|
<div className="flex flex-row border w-fit p-4 rounded-lg items-center gap-2">
|
||||||
<Label className="text-muted-foreground">
|
<Label className="text-muted-foreground">
|
||||||
Change Monitoring
|
Change Monitoring
|
||||||
@@ -269,29 +284,37 @@ const Redis = (
|
|||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<div> */}
|
<div> */}
|
||||||
<ContainerFreeMonitoring
|
<ContainerFreeMonitoring
|
||||||
appName={data?.appName || ""}
|
appName={data?.appName || ""}
|
||||||
/>
|
/>
|
||||||
{/* </div> */}
|
{/* </div> */}
|
||||||
{/* )} */}
|
{/* )} */}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</TabsContent>
|
||||||
</TabsContent>
|
)}
|
||||||
<TabsContent value="logs">
|
{permissions?.logs.read && (
|
||||||
<div className="flex flex-col gap-4 pt-2.5">
|
<TabsContent value="logs">
|
||||||
<ShowDockerLogs
|
<div className="flex flex-col gap-4 pt-2.5">
|
||||||
serverId={data?.serverId || ""}
|
<ShowDockerLogs
|
||||||
appName={data?.appName || ""}
|
serverId={data?.serverId || ""}
|
||||||
/>
|
appName={data?.appName || ""}
|
||||||
</div>
|
/>
|
||||||
</TabsContent>
|
</div>
|
||||||
<TabsContent value="advanced">
|
</TabsContent>
|
||||||
<div className="flex flex-col gap-4 pt-2.5">
|
)}
|
||||||
<ShowDatabaseAdvancedSettings id={redisId} type="redis" />
|
{permissions?.service.create && (
|
||||||
</div>
|
<TabsContent value="advanced">
|
||||||
</TabsContent>
|
<div className="flex flex-col gap-4 pt-2.5">
|
||||||
|
<ShowDatabaseAdvancedSettings
|
||||||
|
id={redisId}
|
||||||
|
type="redis"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
)}
|
||||||
</Tabs>
|
</Tabs>
|
||||||
)}
|
)}
|
||||||
</CardContent>
|
</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 {
|
try {
|
||||||
await helpers.project.all.prefetch();
|
await helpers.project.all.prefetch();
|
||||||
await helpers.settings.isCloud.prefetch();
|
await helpers.settings.isCloud.prefetch();
|
||||||
if (user.role === "member") {
|
const userPermissions = await helpers.user.getPermissions.fetch();
|
||||||
const userR = await helpers.user.one.fetch({
|
|
||||||
userId: user.id,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!userR?.canAccessToGitProviders) {
|
if (!userPermissions?.gitProviders.read) {
|
||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
permanent: true,
|
permanent: true,
|
||||||
destination: "/",
|
destination: "/",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
props: {
|
props: {
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import { appRouter } from "@/server/api/root";
|
|||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
|
|
||||||
const Page = () => {
|
const Page = () => {
|
||||||
const { data } = api.user.get.useQuery();
|
const { data: permissions } = api.user.getPermissions.useQuery();
|
||||||
const { data: isCloud } = api.settings.isCloud.useQuery();
|
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -19,9 +19,7 @@ const Page = () => {
|
|||||||
<div className="h-full rounded-xl max-w-5xl mx-auto flex flex-col gap-4">
|
<div className="h-full rounded-xl max-w-5xl mx-auto flex flex-col gap-4">
|
||||||
<ProfileForm />
|
<ProfileForm />
|
||||||
{isCloud && <LinkingAccount />}
|
{isCloud && <LinkingAccount />}
|
||||||
{(data?.canAccessToAPI ||
|
{permissions?.api.read && <ShowApiKeys />}
|
||||||
data?.role === "owner" ||
|
|
||||||
data?.role === "admin") && <ShowApiKeys />}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -49,19 +49,15 @@ export async function getServerSideProps(
|
|||||||
await helpers.project.all.prefetch();
|
await helpers.project.all.prefetch();
|
||||||
await helpers.settings.isCloud.prefetch();
|
await helpers.settings.isCloud.prefetch();
|
||||||
|
|
||||||
if (user.role === "member") {
|
const userPermissions = await helpers.user.getPermissions.fetch();
|
||||||
const userR = await helpers.user.one.fetch({
|
|
||||||
userId: user.id,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!userR?.canAccessToSSHKeys) {
|
if (!userPermissions?.sshKeys.read) {
|
||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
permanent: true,
|
permanent: true,
|
||||||
destination: "/",
|
destination: "/",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
props: {
|
props: {
|
||||||
|
|||||||
@@ -3,16 +3,24 @@ import { createServerSideHelpers } from "@trpc/react-query/server";
|
|||||||
import type { GetServerSidePropsContext } from "next";
|
import type { GetServerSidePropsContext } from "next";
|
||||||
import type { ReactElement } from "react";
|
import type { ReactElement } from "react";
|
||||||
import superjson from "superjson";
|
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 { ShowInvitations } from "@/components/dashboard/settings/users/show-invitations";
|
||||||
import { ShowUsers } from "@/components/dashboard/settings/users/show-users";
|
import { ShowUsers } from "@/components/dashboard/settings/users/show-users";
|
||||||
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
|
||||||
import { appRouter } from "@/server/api/root";
|
import { appRouter } from "@/server/api/root";
|
||||||
|
import { api } from "@/utils/api";
|
||||||
|
|
||||||
const Page = () => {
|
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 (
|
return (
|
||||||
<div className="flex flex-col gap-4 w-full">
|
<div className="flex flex-col gap-4 w-full">
|
||||||
<ShowUsers />
|
<ShowUsers />
|
||||||
<ShowInvitations />
|
{canCreateMembers && <ShowInvitations />}
|
||||||
|
{isOwnerOrAdmin && <ManageCustomRoles />}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -28,7 +36,7 @@ export async function getServerSideProps(
|
|||||||
const { req, res } = ctx;
|
const { req, res } = ctx;
|
||||||
const { user, session } = await validateRequest(req);
|
const { user, session } = await validateRequest(req);
|
||||||
|
|
||||||
if (!user || user.role === "member") {
|
if (!user) {
|
||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
permanent: true,
|
permanent: true,
|
||||||
@@ -48,12 +56,30 @@ export async function getServerSideProps(
|
|||||||
},
|
},
|
||||||
transformer: superjson,
|
transformer: superjson,
|
||||||
});
|
});
|
||||||
await helpers.user.get.prefetch();
|
|
||||||
await helpers.settings.isCloud.prefetch();
|
|
||||||
|
|
||||||
return {
|
try {
|
||||||
props: {
|
await helpers.user.get.prefetch();
|
||||||
trpcState: helpers.dehydrate(),
|
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 {
|
try {
|
||||||
await helpers.project.all.prefetch();
|
await helpers.project.all.prefetch();
|
||||||
|
|
||||||
if (user.role === "member") {
|
const userPermissions = await helpers.user.getPermissions.fetch();
|
||||||
const userR = await helpers.user.one.fetch({
|
|
||||||
userId: user.id,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!userR?.canAccessToDocker) {
|
if (!userPermissions?.docker.read) {
|
||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
permanent: true,
|
permanent: true,
|
||||||
destination: "/",
|
destination: "/",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
props: {
|
props: {
|
||||||
|
|||||||
@@ -53,19 +53,15 @@ export async function getServerSideProps(
|
|||||||
try {
|
try {
|
||||||
await helpers.project.all.prefetch();
|
await helpers.project.all.prefetch();
|
||||||
|
|
||||||
if (user.role === "member") {
|
const userPermissions = await helpers.user.getPermissions.fetch();
|
||||||
const userR = await helpers.user.one.fetch({
|
|
||||||
userId: user.id,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!userR?.canAccessToTraefikFiles) {
|
if (!userPermissions?.traefikFiles.read) {
|
||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
permanent: true,
|
permanent: true,
|
||||||
destination: "/",
|
destination: "/",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
props: {
|
props: {
|
||||||
|
|||||||
@@ -98,19 +98,15 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
|||||||
},
|
},
|
||||||
transformer: superjson,
|
transformer: superjson,
|
||||||
});
|
});
|
||||||
if (user.role === "member") {
|
const userPermissions = await helpers.user.getPermissions.fetch();
|
||||||
const userR = await helpers.user.one.fetch({
|
|
||||||
userId: user.id,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!userR?.canAccessToAPI) {
|
if (!userPermissions?.api.read) {
|
||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
permanent: true,
|
permanent: true,
|
||||||
destination: "/",
|
destination: "/",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -23,6 +23,8 @@ import { mysqlRouter } from "./routers/mysql";
|
|||||||
import { notificationRouter } from "./routers/notification";
|
import { notificationRouter } from "./routers/notification";
|
||||||
import { organizationRouter } from "./routers/organization";
|
import { organizationRouter } from "./routers/organization";
|
||||||
import { patchRouter } from "./routers/patch";
|
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 { licenseKeyRouter } from "./routers/proprietary/license-key";
|
||||||
import { ssoRouter } from "./routers/proprietary/sso";
|
import { ssoRouter } from "./routers/proprietary/sso";
|
||||||
import { whitelabelingRouter } from "./routers/proprietary/whitelabeling";
|
import { whitelabelingRouter } from "./routers/proprietary/whitelabeling";
|
||||||
@@ -89,6 +91,8 @@ export const appRouter = createTRPCRouter({
|
|||||||
licenseKey: licenseKeyRouter,
|
licenseKey: licenseKeyRouter,
|
||||||
sso: ssoRouter,
|
sso: ssoRouter,
|
||||||
whitelabeling: whitelabelingRouter,
|
whitelabeling: whitelabelingRouter,
|
||||||
|
customRole: customRoleRouter,
|
||||||
|
auditLog: auditLogRouter,
|
||||||
schedule: scheduleRouter,
|
schedule: scheduleRouter,
|
||||||
rollback: rollbackRouter,
|
rollback: rollbackRouter,
|
||||||
volumeBackups: volumeBackupsRouter,
|
volumeBackups: volumeBackupsRouter,
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ import { findProjectById } from "@dokploy/server/services/project";
|
|||||||
import {
|
import {
|
||||||
addNewService,
|
addNewService,
|
||||||
checkServiceAccess,
|
checkServiceAccess,
|
||||||
} from "@dokploy/server/services/user";
|
} from "@dokploy/server/services/permission";
|
||||||
import {
|
import {
|
||||||
getProviderHeaders,
|
getProviderHeaders,
|
||||||
getProviderName,
|
getProviderName,
|
||||||
@@ -38,17 +38,10 @@ import {
|
|||||||
import { generatePassword } from "@/templates/utils";
|
import { generatePassword } from "@/templates/utils";
|
||||||
|
|
||||||
export const aiRouter = createTRPCRouter({
|
export const aiRouter = createTRPCRouter({
|
||||||
one: protectedProcedure
|
one: adminProcedure
|
||||||
.input(z.object({ aiId: z.string() }))
|
.input(z.object({ aiId: z.string() }))
|
||||||
.query(async ({ ctx, input }) => {
|
.query(async ({ input }) => {
|
||||||
const aiSetting = await getAiSettingById(input.aiId);
|
return 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;
|
|
||||||
}),
|
}),
|
||||||
|
|
||||||
getModels: protectedProcedure
|
getModels: protectedProcedure
|
||||||
@@ -159,11 +152,9 @@ export const aiRouter = createTRPCRouter({
|
|||||||
return await saveAiSettings(ctx.session.activeOrganizationId, input);
|
return await saveAiSettings(ctx.session.activeOrganizationId, input);
|
||||||
}),
|
}),
|
||||||
|
|
||||||
update: protectedProcedure
|
update: adminProcedure.input(apiUpdateAi).mutation(async ({ ctx, input }) => {
|
||||||
.input(apiUpdateAi)
|
return await saveAiSettings(ctx.session.activeOrganizationId, input);
|
||||||
.mutation(async ({ ctx, input }) => {
|
}),
|
||||||
return await saveAiSettings(ctx.session.activeOrganizationId, input);
|
|
||||||
}),
|
|
||||||
|
|
||||||
getAll: adminProcedure.query(async ({ ctx }) => {
|
getAll: adminProcedure.query(async ({ ctx }) => {
|
||||||
return await getAiSettingsByOrganizationId(
|
return await getAiSettingsByOrganizationId(
|
||||||
@@ -171,29 +162,15 @@ export const aiRouter = createTRPCRouter({
|
|||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
|
|
||||||
get: protectedProcedure
|
get: adminProcedure
|
||||||
.input(z.object({ aiId: z.string() }))
|
.input(z.object({ aiId: z.string() }))
|
||||||
.query(async ({ ctx, input }) => {
|
.query(async ({ input }) => {
|
||||||
const aiSetting = await getAiSettingById(input.aiId);
|
return 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;
|
|
||||||
}),
|
}),
|
||||||
|
|
||||||
delete: protectedProcedure
|
delete: adminProcedure
|
||||||
.input(z.object({ aiId: z.string() }))
|
.input(z.object({ aiId: z.string() }))
|
||||||
.mutation(async ({ ctx, input }) => {
|
.mutation(async ({ 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 await deleteAiSettings(input.aiId);
|
return await deleteAiSettings(input.aiId);
|
||||||
}),
|
}),
|
||||||
|
|
||||||
@@ -223,13 +200,7 @@ export const aiRouter = createTRPCRouter({
|
|||||||
.mutation(async ({ ctx, input }) => {
|
.mutation(async ({ ctx, input }) => {
|
||||||
const environment = await findEnvironmentById(input.environmentId);
|
const environment = await findEnvironmentById(input.environmentId);
|
||||||
const project = await findProjectById(environment.projectId);
|
const project = await findProjectById(environment.projectId);
|
||||||
if (ctx.user.role === "member") {
|
await checkServiceAccess(ctx, environment.projectId, "create");
|
||||||
await checkServiceAccess(
|
|
||||||
ctx.session.activeOrganizationId,
|
|
||||||
environment.projectId,
|
|
||||||
"create",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (IS_CLOUD && !input.serverId) {
|
if (IS_CLOUD && !input.serverId) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
@@ -275,13 +246,7 @@ export const aiRouter = createTRPCRouter({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ctx.user.role === "member") {
|
await addNewService(ctx, compose.composeId);
|
||||||
await addNewService(
|
|
||||||
ctx.session.activeOrganizationId,
|
|
||||||
ctx.user.ownerId,
|
|
||||||
compose.composeId,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -1,13 +1,10 @@
|
|||||||
import {
|
import {
|
||||||
addNewService,
|
|
||||||
checkServiceAccess,
|
|
||||||
clearOldDeployments,
|
clearOldDeployments,
|
||||||
createApplication,
|
createApplication,
|
||||||
deleteAllMiddlewares,
|
deleteAllMiddlewares,
|
||||||
findApplicationById,
|
findApplicationById,
|
||||||
findEnvironmentById,
|
findEnvironmentById,
|
||||||
findGitProviderById,
|
findGitProviderById,
|
||||||
findMemberById,
|
|
||||||
findProjectById,
|
findProjectById,
|
||||||
getApplicationStats,
|
getApplicationStats,
|
||||||
IS_CLOUD,
|
IS_CLOUD,
|
||||||
@@ -29,14 +26,24 @@ import {
|
|||||||
updateDeploymentStatus,
|
updateDeploymentStatus,
|
||||||
writeConfig,
|
writeConfig,
|
||||||
writeConfigRemote,
|
writeConfigRemote,
|
||||||
// uploadFileSchema
|
|
||||||
} from "@dokploy/server";
|
} from "@dokploy/server";
|
||||||
import { db } from "@dokploy/server/db";
|
import { db } from "@dokploy/server/db";
|
||||||
|
import {
|
||||||
|
addNewService,
|
||||||
|
checkServiceAccess,
|
||||||
|
checkServicePermissionAndAccess,
|
||||||
|
findMemberByUserId,
|
||||||
|
} from "@dokploy/server/services/permission";
|
||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
import { and, desc, eq, ilike, or, sql } from "drizzle-orm";
|
import { and, desc, eq, ilike, or, sql } from "drizzle-orm";
|
||||||
import { nanoid } from "nanoid";
|
import { nanoid } from "nanoid";
|
||||||
import { z } from "zod";
|
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 {
|
import {
|
||||||
apiCreateApplication,
|
apiCreateApplication,
|
||||||
apiDeployApplication,
|
apiDeployApplication,
|
||||||
@@ -72,18 +79,10 @@ export const applicationRouter = createTRPCRouter({
|
|||||||
.input(apiCreateApplication)
|
.input(apiCreateApplication)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
try {
|
try {
|
||||||
// Get project from environment
|
|
||||||
const environment = await findEnvironmentById(input.environmentId);
|
const environment = await findEnvironmentById(input.environmentId);
|
||||||
const project = await findProjectById(environment.projectId);
|
const project = await findProjectById(environment.projectId);
|
||||||
|
|
||||||
if (ctx.user.role === "member") {
|
await checkServiceAccess(ctx, project.projectId, "create");
|
||||||
await checkServiceAccess(
|
|
||||||
ctx.user.id,
|
|
||||||
project.projectId,
|
|
||||||
ctx.session.activeOrganizationId,
|
|
||||||
"create",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (IS_CLOUD && !input.serverId) {
|
if (IS_CLOUD && !input.serverId) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
@@ -101,13 +100,13 @@ export const applicationRouter = createTRPCRouter({
|
|||||||
|
|
||||||
const newApplication = await createApplication(input);
|
const newApplication = await createApplication(input);
|
||||||
|
|
||||||
if (ctx.user.role === "member") {
|
await addNewService(ctx, newApplication.applicationId);
|
||||||
await addNewService(
|
await audit(ctx, {
|
||||||
ctx.user.id,
|
action: "create",
|
||||||
newApplication.applicationId,
|
resourceType: "service",
|
||||||
project.organizationId,
|
resourceId: newApplication.applicationId,
|
||||||
);
|
resourceName: newApplication.appName,
|
||||||
}
|
});
|
||||||
return newApplication;
|
return newApplication;
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
console.log("error", error);
|
console.log("error", error);
|
||||||
@@ -124,14 +123,7 @@ export const applicationRouter = createTRPCRouter({
|
|||||||
one: protectedProcedure
|
one: protectedProcedure
|
||||||
.input(apiFindOneApplication)
|
.input(apiFindOneApplication)
|
||||||
.query(async ({ input, ctx }) => {
|
.query(async ({ input, ctx }) => {
|
||||||
if (ctx.user.role === "member") {
|
await checkServiceAccess(ctx, input.applicationId, "read");
|
||||||
await checkServiceAccess(
|
|
||||||
ctx.user.id,
|
|
||||||
input.applicationId,
|
|
||||||
ctx.session.activeOrganizationId,
|
|
||||||
"access",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
const application = await findApplicationById(input.applicationId);
|
const application = await findApplicationById(input.applicationId);
|
||||||
if (
|
if (
|
||||||
application.environment.project.organizationId !==
|
application.environment.project.organizationId !==
|
||||||
@@ -186,22 +178,21 @@ export const applicationRouter = createTRPCRouter({
|
|||||||
reload: protectedProcedure
|
reload: protectedProcedure
|
||||||
.input(apiReloadApplication)
|
.input(apiReloadApplication)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
|
await checkServicePermissionAndAccess(ctx, input.applicationId, {
|
||||||
|
deployment: ["create"],
|
||||||
|
});
|
||||||
const application = await findApplicationById(input.applicationId);
|
const application = await findApplicationById(input.applicationId);
|
||||||
|
|
||||||
try {
|
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 updateApplicationStatus(input.applicationId, "idle");
|
||||||
await mechanizeDockerContainer(application);
|
await mechanizeDockerContainer(application);
|
||||||
await updateApplicationStatus(input.applicationId, "done");
|
await updateApplicationStatus(input.applicationId, "done");
|
||||||
|
await audit(ctx, {
|
||||||
|
action: "reload",
|
||||||
|
resourceType: "application",
|
||||||
|
resourceId: application.applicationId,
|
||||||
|
resourceName: application.appName,
|
||||||
|
});
|
||||||
return true;
|
return true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
await updateApplicationStatus(input.applicationId, "error");
|
await updateApplicationStatus(input.applicationId, "error");
|
||||||
@@ -216,14 +207,7 @@ export const applicationRouter = createTRPCRouter({
|
|||||||
delete: protectedProcedure
|
delete: protectedProcedure
|
||||||
.input(apiFindOneApplication)
|
.input(apiFindOneApplication)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
if (ctx.user.role === "member") {
|
await checkServiceAccess(ctx, input.applicationId, "delete");
|
||||||
await checkServiceAccess(
|
|
||||||
ctx.user.id,
|
|
||||||
input.applicationId,
|
|
||||||
ctx.session.activeOrganizationId,
|
|
||||||
"delete",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
const application = await findApplicationById(input.applicationId);
|
const application = await findApplicationById(input.applicationId);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
@@ -272,69 +256,66 @@ export const applicationRouter = createTRPCRouter({
|
|||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await audit(ctx, {
|
||||||
|
action: "delete",
|
||||||
|
resourceType: "service",
|
||||||
|
resourceId: application.applicationId,
|
||||||
|
resourceName: application.appName,
|
||||||
|
});
|
||||||
return application;
|
return application;
|
||||||
}),
|
}),
|
||||||
|
|
||||||
stop: protectedProcedure
|
stop: protectedProcedure
|
||||||
.input(apiFindOneApplication)
|
.input(apiFindOneApplication)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
|
await checkServicePermissionAndAccess(ctx, input.applicationId, {
|
||||||
|
deployment: ["create"],
|
||||||
|
});
|
||||||
const service = await findApplicationById(input.applicationId);
|
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) {
|
if (service.serverId) {
|
||||||
await stopServiceRemote(service.serverId, service.appName);
|
await stopServiceRemote(service.serverId, service.appName);
|
||||||
} else {
|
} else {
|
||||||
await stopService(service.appName);
|
await stopService(service.appName);
|
||||||
}
|
}
|
||||||
await updateApplicationStatus(input.applicationId, "idle");
|
await updateApplicationStatus(input.applicationId, "idle");
|
||||||
|
await audit(ctx, {
|
||||||
|
action: "stop",
|
||||||
|
resourceType: "application",
|
||||||
|
resourceId: service.applicationId,
|
||||||
|
resourceName: service.appName,
|
||||||
|
});
|
||||||
return service;
|
return service;
|
||||||
}),
|
}),
|
||||||
|
|
||||||
start: protectedProcedure
|
start: protectedProcedure
|
||||||
.input(apiFindOneApplication)
|
.input(apiFindOneApplication)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
|
await checkServicePermissionAndAccess(ctx, input.applicationId, {
|
||||||
|
deployment: ["create"],
|
||||||
|
});
|
||||||
const service = await findApplicationById(input.applicationId);
|
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) {
|
if (service.serverId) {
|
||||||
await startServiceRemote(service.serverId, service.appName);
|
await startServiceRemote(service.serverId, service.appName);
|
||||||
} else {
|
} else {
|
||||||
await startService(service.appName);
|
await startService(service.appName);
|
||||||
}
|
}
|
||||||
await updateApplicationStatus(input.applicationId, "done");
|
await updateApplicationStatus(input.applicationId, "done");
|
||||||
|
await audit(ctx, {
|
||||||
|
action: "start",
|
||||||
|
resourceType: "application",
|
||||||
|
resourceId: service.applicationId,
|
||||||
|
resourceName: service.appName,
|
||||||
|
});
|
||||||
return service;
|
return service;
|
||||||
}),
|
}),
|
||||||
|
|
||||||
redeploy: protectedProcedure
|
redeploy: protectedProcedure
|
||||||
.input(apiRedeployApplication)
|
.input(apiRedeployApplication)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
|
await checkServicePermissionAndAccess(ctx, input.applicationId, {
|
||||||
|
deployment: ["create"],
|
||||||
|
});
|
||||||
const application = await findApplicationById(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 redeploy this application",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
const jobData: DeploymentJob = {
|
const jobData: DeploymentJob = {
|
||||||
applicationId: input.applicationId,
|
applicationId: input.applicationId,
|
||||||
titleLog: input.title || "Rebuild deployment",
|
titleLog: input.title || "Rebuild deployment",
|
||||||
@@ -349,6 +330,12 @@ export const applicationRouter = createTRPCRouter({
|
|||||||
deploy(jobData).catch((error) => {
|
deploy(jobData).catch((error) => {
|
||||||
console.error("Background deployment failed:", error);
|
console.error("Background deployment failed:", error);
|
||||||
});
|
});
|
||||||
|
await audit(ctx, {
|
||||||
|
action: "rebuild",
|
||||||
|
resourceType: "application",
|
||||||
|
resourceId: application.applicationId,
|
||||||
|
resourceName: application.appName,
|
||||||
|
});
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
await myQueue.add(
|
await myQueue.add(
|
||||||
@@ -359,41 +346,40 @@ export const applicationRouter = createTRPCRouter({
|
|||||||
removeOnFail: true,
|
removeOnFail: true,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
await audit(ctx, {
|
||||||
|
action: "rebuild",
|
||||||
|
resourceType: "application",
|
||||||
|
resourceId: application.applicationId,
|
||||||
|
resourceName: application.appName,
|
||||||
|
});
|
||||||
}),
|
}),
|
||||||
saveEnvironment: protectedProcedure
|
saveEnvironment: protectedProcedure
|
||||||
.input(apiSaveEnvironmentVariables)
|
.input(apiSaveEnvironmentVariables)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
const application = await findApplicationById(input.applicationId);
|
await checkServicePermissionAndAccess(ctx, input.applicationId, {
|
||||||
if (
|
envVars: ["write"],
|
||||||
application.environment.project.organizationId !==
|
});
|
||||||
ctx.session.activeOrganizationId
|
|
||||||
) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "UNAUTHORIZED",
|
|
||||||
message: "You are not authorized to save this environment",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
await updateApplication(input.applicationId, {
|
await updateApplication(input.applicationId, {
|
||||||
env: input.env,
|
env: input.env,
|
||||||
buildArgs: input.buildArgs,
|
buildArgs: input.buildArgs,
|
||||||
buildSecrets: input.buildSecrets,
|
buildSecrets: input.buildSecrets,
|
||||||
createEnvFile: input.createEnvFile,
|
createEnvFile: input.createEnvFile,
|
||||||
});
|
});
|
||||||
|
const application = await findApplicationById(input.applicationId);
|
||||||
|
await audit(ctx, {
|
||||||
|
action: "update",
|
||||||
|
resourceType: "application",
|
||||||
|
resourceId: application.applicationId,
|
||||||
|
resourceName: application.appName,
|
||||||
|
});
|
||||||
return true;
|
return true;
|
||||||
}),
|
}),
|
||||||
saveBuildType: protectedProcedure
|
saveBuildType: protectedProcedure
|
||||||
.input(apiSaveBuildType)
|
.input(apiSaveBuildType)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
const application = await findApplicationById(input.applicationId);
|
await checkServicePermissionAndAccess(ctx, input.applicationId, {
|
||||||
if (
|
service: ["create"],
|
||||||
application.environment.project.organizationId !==
|
});
|
||||||
ctx.session.activeOrganizationId
|
|
||||||
) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "UNAUTHORIZED",
|
|
||||||
message: "You are not authorized to save this build type",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
await updateApplication(input.applicationId, {
|
await updateApplication(input.applicationId, {
|
||||||
buildType: input.buildType,
|
buildType: input.buildType,
|
||||||
dockerfile: input.dockerfile,
|
dockerfile: input.dockerfile,
|
||||||
@@ -404,22 +390,21 @@ export const applicationRouter = createTRPCRouter({
|
|||||||
isStaticSpa: input.isStaticSpa,
|
isStaticSpa: input.isStaticSpa,
|
||||||
railpackVersion: input.railpackVersion,
|
railpackVersion: input.railpackVersion,
|
||||||
});
|
});
|
||||||
|
const application = await findApplicationById(input.applicationId);
|
||||||
|
await audit(ctx, {
|
||||||
|
action: "update",
|
||||||
|
resourceType: "application",
|
||||||
|
resourceId: application.applicationId,
|
||||||
|
resourceName: application.appName,
|
||||||
|
});
|
||||||
return true;
|
return true;
|
||||||
}),
|
}),
|
||||||
saveGithubProvider: protectedProcedure
|
saveGithubProvider: protectedProcedure
|
||||||
.input(apiSaveGithubProvider)
|
.input(apiSaveGithubProvider)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
const application = await findApplicationById(input.applicationId);
|
await checkServicePermissionAndAccess(ctx, input.applicationId, {
|
||||||
if (
|
service: ["create"],
|
||||||
application.environment.project.organizationId !==
|
});
|
||||||
ctx.session.activeOrganizationId
|
|
||||||
) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "UNAUTHORIZED",
|
|
||||||
message: "You are not authorized to save this github provider",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
await updateApplication(input.applicationId, {
|
await updateApplication(input.applicationId, {
|
||||||
repository: input.repository,
|
repository: input.repository,
|
||||||
branch: input.branch,
|
branch: input.branch,
|
||||||
@@ -432,22 +417,21 @@ export const applicationRouter = createTRPCRouter({
|
|||||||
triggerType: input.triggerType,
|
triggerType: input.triggerType,
|
||||||
enableSubmodules: input.enableSubmodules,
|
enableSubmodules: input.enableSubmodules,
|
||||||
});
|
});
|
||||||
|
const application = await findApplicationById(input.applicationId);
|
||||||
|
await audit(ctx, {
|
||||||
|
action: "update",
|
||||||
|
resourceType: "application",
|
||||||
|
resourceId: application.applicationId,
|
||||||
|
resourceName: application.appName,
|
||||||
|
});
|
||||||
return true;
|
return true;
|
||||||
}),
|
}),
|
||||||
saveGitlabProvider: protectedProcedure
|
saveGitlabProvider: protectedProcedure
|
||||||
.input(apiSaveGitlabProvider)
|
.input(apiSaveGitlabProvider)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
const application = await findApplicationById(input.applicationId);
|
await checkServicePermissionAndAccess(ctx, input.applicationId, {
|
||||||
if (
|
service: ["create"],
|
||||||
application.environment.project.organizationId !==
|
});
|
||||||
ctx.session.activeOrganizationId
|
|
||||||
) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "UNAUTHORIZED",
|
|
||||||
message: "You are not authorized to save this gitlab provider",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
await updateApplication(input.applicationId, {
|
await updateApplication(input.applicationId, {
|
||||||
gitlabRepository: input.gitlabRepository,
|
gitlabRepository: input.gitlabRepository,
|
||||||
gitlabOwner: input.gitlabOwner,
|
gitlabOwner: input.gitlabOwner,
|
||||||
@@ -461,22 +445,21 @@ export const applicationRouter = createTRPCRouter({
|
|||||||
watchPaths: input.watchPaths,
|
watchPaths: input.watchPaths,
|
||||||
enableSubmodules: input.enableSubmodules,
|
enableSubmodules: input.enableSubmodules,
|
||||||
});
|
});
|
||||||
|
const application = await findApplicationById(input.applicationId);
|
||||||
|
await audit(ctx, {
|
||||||
|
action: "update",
|
||||||
|
resourceType: "application",
|
||||||
|
resourceId: application.applicationId,
|
||||||
|
resourceName: application.appName,
|
||||||
|
});
|
||||||
return true;
|
return true;
|
||||||
}),
|
}),
|
||||||
saveBitbucketProvider: protectedProcedure
|
saveBitbucketProvider: protectedProcedure
|
||||||
.input(apiSaveBitbucketProvider)
|
.input(apiSaveBitbucketProvider)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
const application = await findApplicationById(input.applicationId);
|
await checkServicePermissionAndAccess(ctx, input.applicationId, {
|
||||||
if (
|
service: ["create"],
|
||||||
application.environment.project.organizationId !==
|
});
|
||||||
ctx.session.activeOrganizationId
|
|
||||||
) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "UNAUTHORIZED",
|
|
||||||
message: "You are not authorized to save this bitbucket provider",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
await updateApplication(input.applicationId, {
|
await updateApplication(input.applicationId, {
|
||||||
bitbucketRepository: input.bitbucketRepository,
|
bitbucketRepository: input.bitbucketRepository,
|
||||||
bitbucketRepositorySlug: input.bitbucketRepositorySlug,
|
bitbucketRepositorySlug: input.bitbucketRepositorySlug,
|
||||||
@@ -489,22 +472,21 @@ export const applicationRouter = createTRPCRouter({
|
|||||||
watchPaths: input.watchPaths,
|
watchPaths: input.watchPaths,
|
||||||
enableSubmodules: input.enableSubmodules,
|
enableSubmodules: input.enableSubmodules,
|
||||||
});
|
});
|
||||||
|
const application = await findApplicationById(input.applicationId);
|
||||||
|
await audit(ctx, {
|
||||||
|
action: "update",
|
||||||
|
resourceType: "application",
|
||||||
|
resourceId: application.applicationId,
|
||||||
|
resourceName: application.appName,
|
||||||
|
});
|
||||||
return true;
|
return true;
|
||||||
}),
|
}),
|
||||||
saveGiteaProvider: protectedProcedure
|
saveGiteaProvider: protectedProcedure
|
||||||
.input(apiSaveGiteaProvider)
|
.input(apiSaveGiteaProvider)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
const application = await findApplicationById(input.applicationId);
|
await checkServicePermissionAndAccess(ctx, input.applicationId, {
|
||||||
if (
|
service: ["create"],
|
||||||
application.environment.project.organizationId !==
|
});
|
||||||
ctx.session.activeOrganizationId
|
|
||||||
) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "UNAUTHORIZED",
|
|
||||||
message: "You are not authorized to save this gitea provider",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
await updateApplication(input.applicationId, {
|
await updateApplication(input.applicationId, {
|
||||||
giteaRepository: input.giteaRepository,
|
giteaRepository: input.giteaRepository,
|
||||||
giteaOwner: input.giteaOwner,
|
giteaOwner: input.giteaOwner,
|
||||||
@@ -516,22 +498,21 @@ export const applicationRouter = createTRPCRouter({
|
|||||||
watchPaths: input.watchPaths,
|
watchPaths: input.watchPaths,
|
||||||
enableSubmodules: input.enableSubmodules,
|
enableSubmodules: input.enableSubmodules,
|
||||||
});
|
});
|
||||||
|
const application = await findApplicationById(input.applicationId);
|
||||||
|
await audit(ctx, {
|
||||||
|
action: "update",
|
||||||
|
resourceType: "application",
|
||||||
|
resourceId: application.applicationId,
|
||||||
|
resourceName: application.appName,
|
||||||
|
});
|
||||||
return true;
|
return true;
|
||||||
}),
|
}),
|
||||||
saveDockerProvider: protectedProcedure
|
saveDockerProvider: protectedProcedure
|
||||||
.input(apiSaveDockerProvider)
|
.input(apiSaveDockerProvider)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
const application = await findApplicationById(input.applicationId);
|
await checkServicePermissionAndAccess(ctx, input.applicationId, {
|
||||||
if (
|
service: ["create"],
|
||||||
application.environment.project.organizationId !==
|
});
|
||||||
ctx.session.activeOrganizationId
|
|
||||||
) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "UNAUTHORIZED",
|
|
||||||
message: "You are not authorized to save this docker provider",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
await updateApplication(input.applicationId, {
|
await updateApplication(input.applicationId, {
|
||||||
dockerImage: input.dockerImage,
|
dockerImage: input.dockerImage,
|
||||||
username: input.username,
|
username: input.username,
|
||||||
@@ -540,22 +521,21 @@ export const applicationRouter = createTRPCRouter({
|
|||||||
applicationStatus: "idle",
|
applicationStatus: "idle",
|
||||||
registryUrl: input.registryUrl,
|
registryUrl: input.registryUrl,
|
||||||
});
|
});
|
||||||
|
const application = await findApplicationById(input.applicationId);
|
||||||
|
await audit(ctx, {
|
||||||
|
action: "update",
|
||||||
|
resourceType: "application",
|
||||||
|
resourceId: application.applicationId,
|
||||||
|
resourceName: application.appName,
|
||||||
|
});
|
||||||
return true;
|
return true;
|
||||||
}),
|
}),
|
||||||
saveGitProvider: protectedProcedure
|
saveGitProvider: protectedProcedure
|
||||||
.input(apiSaveGitProvider)
|
.input(apiSaveGitProvider)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
const application = await findApplicationById(input.applicationId);
|
await checkServicePermissionAndAccess(ctx, input.applicationId, {
|
||||||
if (
|
service: ["create"],
|
||||||
application.environment.project.organizationId !==
|
});
|
||||||
ctx.session.activeOrganizationId
|
|
||||||
) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "UNAUTHORIZED",
|
|
||||||
message: "You are not authorized to save this git provider",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
await updateApplication(input.applicationId, {
|
await updateApplication(input.applicationId, {
|
||||||
customGitBranch: input.customGitBranch,
|
customGitBranch: input.customGitBranch,
|
||||||
customGitBuildPath: input.customGitBuildPath,
|
customGitBuildPath: input.customGitBuildPath,
|
||||||
@@ -566,26 +546,22 @@ export const applicationRouter = createTRPCRouter({
|
|||||||
watchPaths: input.watchPaths,
|
watchPaths: input.watchPaths,
|
||||||
enableSubmodules: input.enableSubmodules,
|
enableSubmodules: input.enableSubmodules,
|
||||||
});
|
});
|
||||||
|
const application = await findApplicationById(input.applicationId);
|
||||||
|
await audit(ctx, {
|
||||||
|
action: "update",
|
||||||
|
resourceType: "application",
|
||||||
|
resourceId: application.applicationId,
|
||||||
|
resourceName: application.appName,
|
||||||
|
});
|
||||||
return true;
|
return true;
|
||||||
}),
|
}),
|
||||||
disconnectGitProvider: protectedProcedure
|
disconnectGitProvider: protectedProcedure
|
||||||
.input(apiFindOneApplication)
|
.input(apiFindOneApplication)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
const application = await findApplicationById(input.applicationId);
|
await checkServicePermissionAndAccess(ctx, input.applicationId, {
|
||||||
if (
|
service: ["create"],
|
||||||
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 updateApplication(input.applicationId, {
|
await updateApplication(input.applicationId, {
|
||||||
// GitHub fields
|
|
||||||
repository: null,
|
repository: null,
|
||||||
branch: null,
|
branch: null,
|
||||||
owner: null,
|
owner: null,
|
||||||
@@ -593,7 +569,6 @@ export const applicationRouter = createTRPCRouter({
|
|||||||
githubId: null,
|
githubId: null,
|
||||||
triggerType: "push",
|
triggerType: "push",
|
||||||
|
|
||||||
// GitLab fields
|
|
||||||
gitlabRepository: null,
|
gitlabRepository: null,
|
||||||
gitlabOwner: null,
|
gitlabOwner: null,
|
||||||
gitlabBranch: null,
|
gitlabBranch: null,
|
||||||
@@ -602,63 +577,58 @@ export const applicationRouter = createTRPCRouter({
|
|||||||
gitlabProjectId: null,
|
gitlabProjectId: null,
|
||||||
gitlabPathNamespace: null,
|
gitlabPathNamespace: null,
|
||||||
|
|
||||||
// Bitbucket fields
|
|
||||||
bitbucketRepository: null,
|
bitbucketRepository: null,
|
||||||
bitbucketOwner: null,
|
bitbucketOwner: null,
|
||||||
bitbucketBranch: null,
|
bitbucketBranch: null,
|
||||||
bitbucketBuildPath: null,
|
bitbucketBuildPath: null,
|
||||||
bitbucketId: null,
|
bitbucketId: null,
|
||||||
|
|
||||||
// Gitea fields
|
|
||||||
giteaRepository: null,
|
giteaRepository: null,
|
||||||
giteaOwner: null,
|
giteaOwner: null,
|
||||||
giteaBranch: null,
|
giteaBranch: null,
|
||||||
giteaBuildPath: null,
|
giteaBuildPath: null,
|
||||||
giteaId: null,
|
giteaId: null,
|
||||||
|
|
||||||
// Custom Git fields
|
|
||||||
customGitBranch: null,
|
customGitBranch: null,
|
||||||
customGitBuildPath: null,
|
customGitBuildPath: null,
|
||||||
customGitUrl: null,
|
customGitUrl: null,
|
||||||
customGitSSHKeyId: null,
|
customGitSSHKeyId: null,
|
||||||
|
|
||||||
// Common fields
|
|
||||||
sourceType: "github", // Reset to default
|
sourceType: "github", // Reset to default
|
||||||
applicationStatus: "idle",
|
applicationStatus: "idle",
|
||||||
watchPaths: null,
|
watchPaths: null,
|
||||||
enableSubmodules: false,
|
enableSubmodules: false,
|
||||||
});
|
});
|
||||||
|
const application = await findApplicationById(input.applicationId);
|
||||||
|
await audit(ctx, {
|
||||||
|
action: "update",
|
||||||
|
resourceType: "application",
|
||||||
|
resourceId: application.applicationId,
|
||||||
|
resourceName: application.appName,
|
||||||
|
});
|
||||||
return true;
|
return true;
|
||||||
}),
|
}),
|
||||||
markRunning: protectedProcedure
|
markRunning: protectedProcedure
|
||||||
.input(apiFindOneApplication)
|
.input(apiFindOneApplication)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
const application = await findApplicationById(input.applicationId);
|
await checkServicePermissionAndAccess(ctx, input.applicationId, {
|
||||||
if (
|
deployment: ["create"],
|
||||||
application.environment.project.organizationId !==
|
});
|
||||||
ctx.session.activeOrganizationId
|
|
||||||
) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "UNAUTHORIZED",
|
|
||||||
message: "You are not authorized to mark this application as running",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
await updateApplicationStatus(input.applicationId, "running");
|
await updateApplicationStatus(input.applicationId, "running");
|
||||||
|
const application = await findApplicationById(input.applicationId);
|
||||||
|
await audit(ctx, {
|
||||||
|
action: "deploy",
|
||||||
|
resourceType: "application",
|
||||||
|
resourceId: application.applicationId,
|
||||||
|
resourceName: application.appName,
|
||||||
|
});
|
||||||
}),
|
}),
|
||||||
update: protectedProcedure
|
update: protectedProcedure
|
||||||
.input(apiUpdateApplication)
|
.input(apiUpdateApplication)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
const application = await findApplicationById(input.applicationId);
|
await checkServicePermissionAndAccess(ctx, input.applicationId, {
|
||||||
if (
|
service: ["create"],
|
||||||
application.environment.project.organizationId !==
|
});
|
||||||
ctx.session.activeOrganizationId
|
|
||||||
) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "UNAUTHORIZED",
|
|
||||||
message: "You are not authorized to update this application",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
const { applicationId, ...rest } = input;
|
const { applicationId, ...rest } = input;
|
||||||
const updateApp = await updateApplication(applicationId, {
|
const updateApp = await updateApplication(applicationId, {
|
||||||
...rest,
|
...rest,
|
||||||
@@ -670,40 +640,39 @@ export const applicationRouter = createTRPCRouter({
|
|||||||
message: "Error updating application",
|
message: "Error updating application",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
await audit(ctx, {
|
||||||
|
action: "update",
|
||||||
|
resourceType: "application",
|
||||||
|
resourceId: updateApp.applicationId,
|
||||||
|
resourceName: updateApp.appName,
|
||||||
|
});
|
||||||
return true;
|
return true;
|
||||||
}),
|
}),
|
||||||
refreshToken: protectedProcedure
|
refreshToken: protectedProcedure
|
||||||
.input(apiFindOneApplication)
|
.input(apiFindOneApplication)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
const application = await findApplicationById(input.applicationId);
|
await checkServicePermissionAndAccess(ctx, input.applicationId, {
|
||||||
if (
|
service: ["create"],
|
||||||
application.environment.project.organizationId !==
|
});
|
||||||
ctx.session.activeOrganizationId
|
|
||||||
) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "UNAUTHORIZED",
|
|
||||||
message: "You are not authorized to refresh this application",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
await updateApplication(input.applicationId, {
|
await updateApplication(input.applicationId, {
|
||||||
refreshToken: nanoid(),
|
refreshToken: nanoid(),
|
||||||
});
|
});
|
||||||
|
const application = await findApplicationById(input.applicationId);
|
||||||
|
await audit(ctx, {
|
||||||
|
action: "update",
|
||||||
|
resourceType: "application",
|
||||||
|
resourceId: application.applicationId,
|
||||||
|
resourceName: application.appName,
|
||||||
|
});
|
||||||
return true;
|
return true;
|
||||||
}),
|
}),
|
||||||
deploy: protectedProcedure
|
deploy: protectedProcedure
|
||||||
.input(apiDeployApplication)
|
.input(apiDeployApplication)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
|
await checkServicePermissionAndAccess(ctx, input.applicationId, {
|
||||||
|
deployment: ["create"],
|
||||||
|
});
|
||||||
const application = await findApplicationById(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 deploy this application",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
const jobData: DeploymentJob = {
|
const jobData: DeploymentJob = {
|
||||||
applicationId: input.applicationId,
|
applicationId: input.applicationId,
|
||||||
titleLog: input.title || "Manual deployment",
|
titleLog: input.title || "Manual deployment",
|
||||||
@@ -717,7 +686,12 @@ export const applicationRouter = createTRPCRouter({
|
|||||||
deploy(jobData).catch((error) => {
|
deploy(jobData).catch((error) => {
|
||||||
console.error("Background deployment failed:", error);
|
console.error("Background deployment failed:", error);
|
||||||
});
|
});
|
||||||
|
await audit(ctx, {
|
||||||
|
action: "deploy",
|
||||||
|
resourceType: "application",
|
||||||
|
resourceId: application.applicationId,
|
||||||
|
resourceName: application.appName,
|
||||||
|
});
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
await myQueue.add(
|
await myQueue.add(
|
||||||
@@ -728,69 +702,60 @@ export const applicationRouter = createTRPCRouter({
|
|||||||
removeOnFail: true,
|
removeOnFail: true,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
await audit(ctx, {
|
||||||
|
action: "deploy",
|
||||||
|
resourceType: "application",
|
||||||
|
resourceId: application.applicationId,
|
||||||
|
resourceName: application.appName,
|
||||||
|
});
|
||||||
}),
|
}),
|
||||||
|
|
||||||
cleanQueues: protectedProcedure
|
cleanQueues: protectedProcedure
|
||||||
.input(apiFindOneApplication)
|
.input(apiFindOneApplication)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
const application = await findApplicationById(input.applicationId);
|
await checkServicePermissionAndAccess(ctx, input.applicationId, {
|
||||||
if (
|
deployment: ["cancel"],
|
||||||
application.environment.project.organizationId !==
|
});
|
||||||
ctx.session.activeOrganizationId
|
|
||||||
) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "UNAUTHORIZED",
|
|
||||||
message: "You are not authorized to clean this application",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
await cleanQueuesByApplication(input.applicationId);
|
await cleanQueuesByApplication(input.applicationId);
|
||||||
}),
|
}),
|
||||||
clearDeployments: protectedProcedure
|
clearDeployments: protectedProcedure
|
||||||
.input(apiFindOneApplication)
|
.input(apiFindOneApplication)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
|
await checkServicePermissionAndAccess(ctx, input.applicationId, {
|
||||||
|
deployment: ["create"],
|
||||||
|
});
|
||||||
const application = await findApplicationById(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 clear deployments for this application",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
await clearOldDeployments(application.appName, application.serverId);
|
await clearOldDeployments(application.appName, application.serverId);
|
||||||
|
await audit(ctx, {
|
||||||
|
action: "delete",
|
||||||
|
resourceType: "application",
|
||||||
|
resourceId: application.applicationId,
|
||||||
|
resourceName: application.appName,
|
||||||
|
});
|
||||||
return true;
|
return true;
|
||||||
}),
|
}),
|
||||||
killBuild: protectedProcedure
|
killBuild: protectedProcedure
|
||||||
.input(apiFindOneApplication)
|
.input(apiFindOneApplication)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
|
await checkServicePermissionAndAccess(ctx, input.applicationId, {
|
||||||
|
deployment: ["cancel"],
|
||||||
|
});
|
||||||
const application = await findApplicationById(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 kill this build",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
await killDockerBuild("application", application.serverId);
|
await killDockerBuild("application", application.serverId);
|
||||||
|
await audit(ctx, {
|
||||||
|
action: "stop",
|
||||||
|
resourceType: "application",
|
||||||
|
resourceId: application.applicationId,
|
||||||
|
resourceName: application.appName,
|
||||||
|
});
|
||||||
}),
|
}),
|
||||||
readTraefikConfig: protectedProcedure
|
readTraefikConfig: protectedProcedure
|
||||||
.input(apiFindOneApplication)
|
.input(apiFindOneApplication)
|
||||||
.query(async ({ input, ctx }) => {
|
.query(async ({ input, ctx }) => {
|
||||||
|
await checkServicePermissionAndAccess(ctx, input.applicationId, {
|
||||||
|
traefikFiles: ["read"],
|
||||||
|
});
|
||||||
const application = await findApplicationById(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 read this application",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
let traefikConfig = null;
|
let traefikConfig = null;
|
||||||
if (application.serverId) {
|
if (application.serverId) {
|
||||||
traefikConfig = await readRemoteConfig(
|
traefikConfig = await readRemoteConfig(
|
||||||
@@ -820,18 +785,11 @@ export const applicationRouter = createTRPCRouter({
|
|||||||
const applicationId = formData.get("applicationId") as string;
|
const applicationId = formData.get("applicationId") as string;
|
||||||
const dropBuildPath = formData.get("dropBuildPath") as string | null;
|
const dropBuildPath = formData.get("dropBuildPath") as string | null;
|
||||||
|
|
||||||
|
await checkServicePermissionAndAccess(ctx, applicationId, {
|
||||||
|
deployment: ["create"],
|
||||||
|
});
|
||||||
const app = await findApplicationById(applicationId);
|
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, {
|
await updateApplication(applicationId, {
|
||||||
sourceType: "drop",
|
sourceType: "drop",
|
||||||
dropBuildPath: dropBuildPath || "",
|
dropBuildPath: dropBuildPath || "",
|
||||||
@@ -862,23 +820,21 @@ export const applicationRouter = createTRPCRouter({
|
|||||||
removeOnFail: true,
|
removeOnFail: true,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
await audit(ctx, {
|
||||||
|
action: "deploy",
|
||||||
|
resourceType: "application",
|
||||||
|
resourceId: app.applicationId,
|
||||||
|
resourceName: app.appName,
|
||||||
|
});
|
||||||
return true;
|
return true;
|
||||||
}),
|
}),
|
||||||
updateTraefikConfig: protectedProcedure
|
updateTraefikConfig: protectedProcedure
|
||||||
.input(z.object({ applicationId: z.string(), traefikConfig: z.string() }))
|
.input(z.object({ applicationId: z.string(), traefikConfig: z.string() }))
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
|
await checkServicePermissionAndAccess(ctx, input.applicationId, {
|
||||||
|
traefikFiles: ["write"],
|
||||||
|
});
|
||||||
const application = await findApplicationById(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 update this application",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (application.serverId) {
|
if (application.serverId) {
|
||||||
await writeConfigRemote(
|
await writeConfigRemote(
|
||||||
application.serverId,
|
application.serverId,
|
||||||
@@ -888,9 +844,15 @@ export const applicationRouter = createTRPCRouter({
|
|||||||
} else {
|
} else {
|
||||||
writeConfig(application.appName, input.traefikConfig);
|
writeConfig(application.appName, input.traefikConfig);
|
||||||
}
|
}
|
||||||
|
await audit(ctx, {
|
||||||
|
action: "update",
|
||||||
|
resourceType: "application",
|
||||||
|
resourceId: application.applicationId,
|
||||||
|
resourceName: application.appName,
|
||||||
|
});
|
||||||
return true;
|
return true;
|
||||||
}),
|
}),
|
||||||
readAppMonitoring: protectedProcedure
|
readAppMonitoring: withPermission("monitoring", "read")
|
||||||
.input(apiFindMonitoringStats)
|
.input(apiFindMonitoringStats)
|
||||||
.query(async ({ input }) => {
|
.query(async ({ input }) => {
|
||||||
if (IS_CLOUD) {
|
if (IS_CLOUD) {
|
||||||
@@ -911,31 +873,10 @@ export const applicationRouter = createTRPCRouter({
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
const application = await findApplicationById(input.applicationId);
|
await checkServicePermissionAndAccess(ctx, input.applicationId, {
|
||||||
if (
|
service: ["create"],
|
||||||
application.environment.project.organizationId !==
|
});
|
||||||
ctx.session.activeOrganizationId
|
|
||||||
) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "UNAUTHORIZED",
|
|
||||||
message: "You are not authorized to move this application",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
const updatedApplication = await db
|
||||||
.update(applications)
|
.update(applications)
|
||||||
.set({
|
.set({
|
||||||
@@ -951,23 +892,22 @@ export const applicationRouter = createTRPCRouter({
|
|||||||
message: "Failed to move application",
|
message: "Failed to move application",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
await audit(ctx, {
|
||||||
|
action: "update",
|
||||||
|
resourceType: "application",
|
||||||
|
resourceId: updatedApplication.applicationId,
|
||||||
|
resourceName: updatedApplication.appName,
|
||||||
|
});
|
||||||
return updatedApplication;
|
return updatedApplication;
|
||||||
}),
|
}),
|
||||||
|
|
||||||
cancelDeployment: protectedProcedure
|
cancelDeployment: protectedProcedure
|
||||||
.input(apiFindOneApplication)
|
.input(apiFindOneApplication)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
|
await checkServicePermissionAndAccess(ctx, input.applicationId, {
|
||||||
|
deployment: ["cancel"],
|
||||||
|
});
|
||||||
const application = await findApplicationById(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 cancel this deployment",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (IS_CLOUD && application.serverId) {
|
if (IS_CLOUD && application.serverId) {
|
||||||
try {
|
try {
|
||||||
@@ -984,7 +924,12 @@ export const applicationRouter = createTRPCRouter({
|
|||||||
applicationId: input.applicationId,
|
applicationId: input.applicationId,
|
||||||
applicationType: "application",
|
applicationType: "application",
|
||||||
});
|
});
|
||||||
|
await audit(ctx, {
|
||||||
|
action: "stop",
|
||||||
|
resourceType: "application",
|
||||||
|
resourceId: application.applicationId,
|
||||||
|
resourceName: application.appName,
|
||||||
|
});
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
message: "Deployment cancellation requested",
|
message: "Deployment cancellation requested",
|
||||||
@@ -1085,19 +1030,17 @@ export const applicationRouter = createTRPCRouter({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ctx.user.role === "member") {
|
const { accessedServices } = await findMemberByUserId(
|
||||||
const { accessedServices } = await findMemberById(
|
ctx.user.id,
|
||||||
ctx.user.id,
|
ctx.session.activeOrganizationId,
|
||||||
ctx.session.activeOrganizationId,
|
);
|
||||||
);
|
if (accessedServices.length === 0) return { items: [], total: 0 };
|
||||||
if (accessedServices.length === 0) return { items: [], total: 0 };
|
baseConditions.push(
|
||||||
baseConditions.push(
|
sql`${applications.applicationId} IN (${sql.join(
|
||||||
sql`${applications.applicationId} IN (${sql.join(
|
accessedServices.map((id) => sql`${id}`),
|
||||||
accessedServices.map((id) => sql`${id}`),
|
sql`, `,
|
||||||
sql`, `,
|
)})`,
|
||||||
)})`,
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const where = and(...baseConditions);
|
const where = and(...baseConditions);
|
||||||
|
|
||||||
|
|||||||
@@ -44,7 +44,13 @@ import {
|
|||||||
} from "@dokploy/server/utils/restore";
|
} from "@dokploy/server/utils/restore";
|
||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
import { z } from "zod";
|
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 {
|
import {
|
||||||
apiCreateBackup,
|
apiCreateBackup,
|
||||||
apiFindOneBackup,
|
apiFindOneBackup,
|
||||||
@@ -69,10 +75,21 @@ interface RcloneFile {
|
|||||||
export const backupRouter = createTRPCRouter({
|
export const backupRouter = createTRPCRouter({
|
||||||
create: protectedProcedure
|
create: protectedProcedure
|
||||||
.input(apiCreateBackup)
|
.input(apiCreateBackup)
|
||||||
.mutation(async ({ input }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
try {
|
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);
|
const backup = await findBackupById(newBackup.backupId);
|
||||||
|
|
||||||
if (IS_CLOUD && backup.enabled) {
|
if (IS_CLOUD && backup.enabled) {
|
||||||
@@ -110,6 +127,11 @@ export const backupRouter = createTRPCRouter({
|
|||||||
scheduleBackup(backup);
|
scheduleBackup(backup);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
await audit(ctx, {
|
||||||
|
action: "create",
|
||||||
|
resourceType: "backup",
|
||||||
|
resourceId: backup.backupId,
|
||||||
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
@@ -122,15 +144,42 @@ export const backupRouter = createTRPCRouter({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
one: protectedProcedure.input(apiFindOneBackup).query(async ({ input }) => {
|
one: protectedProcedure
|
||||||
const backup = await findBackupById(input.backupId);
|
.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
|
update: protectedProcedure
|
||||||
.input(apiUpdateBackup)
|
.input(apiUpdateBackup)
|
||||||
.mutation(async ({ input }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
try {
|
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);
|
await updateBackupById(input.backupId, input);
|
||||||
const backup = await findBackupById(input.backupId);
|
const backup = await findBackupById(input.backupId);
|
||||||
|
|
||||||
@@ -156,6 +205,11 @@ export const backupRouter = createTRPCRouter({
|
|||||||
removeScheduleBackup(input.backupId);
|
removeScheduleBackup(input.backupId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
await audit(ctx, {
|
||||||
|
action: "update",
|
||||||
|
resourceType: "backup",
|
||||||
|
resourceId: backup.backupId,
|
||||||
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const message =
|
const message =
|
||||||
error instanceof Error ? error.message : "Error updating this Backup";
|
error instanceof Error ? error.message : "Error updating this Backup";
|
||||||
@@ -167,8 +221,21 @@ export const backupRouter = createTRPCRouter({
|
|||||||
}),
|
}),
|
||||||
remove: protectedProcedure
|
remove: protectedProcedure
|
||||||
.input(apiRemoveBackup)
|
.input(apiRemoveBackup)
|
||||||
.mutation(async ({ input }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
try {
|
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);
|
const value = await removeBackupById(input.backupId);
|
||||||
if (IS_CLOUD && value) {
|
if (IS_CLOUD && value) {
|
||||||
removeJob({
|
removeJob({
|
||||||
@@ -179,6 +246,11 @@ export const backupRouter = createTRPCRouter({
|
|||||||
} else if (!IS_CLOUD) {
|
} else if (!IS_CLOUD) {
|
||||||
removeScheduleBackup(input.backupId);
|
removeScheduleBackup(input.backupId);
|
||||||
}
|
}
|
||||||
|
await audit(ctx, {
|
||||||
|
action: "delete",
|
||||||
|
resourceType: "backup",
|
||||||
|
resourceId: input.backupId,
|
||||||
|
});
|
||||||
return value;
|
return value;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const message =
|
const message =
|
||||||
@@ -191,13 +263,22 @@ export const backupRouter = createTRPCRouter({
|
|||||||
}),
|
}),
|
||||||
manualBackupPostgres: protectedProcedure
|
manualBackupPostgres: protectedProcedure
|
||||||
.input(apiFindOneBackup)
|
.input(apiFindOneBackup)
|
||||||
.mutation(async ({ input }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
try {
|
try {
|
||||||
const backup = await findBackupById(input.backupId);
|
const backup = await findBackupById(input.backupId);
|
||||||
|
if (backup.postgresId) {
|
||||||
|
await checkServicePermissionAndAccess(ctx, backup.postgresId, {
|
||||||
|
backup: ["create"],
|
||||||
|
});
|
||||||
|
}
|
||||||
const postgres = await findPostgresByBackupId(backup.backupId);
|
const postgres = await findPostgresByBackupId(backup.backupId);
|
||||||
await runPostgresBackup(postgres, backup);
|
await runPostgresBackup(postgres, backup);
|
||||||
|
|
||||||
await keepLatestNBackups(backup, postgres?.serverId);
|
await keepLatestNBackups(backup, postgres?.serverId);
|
||||||
|
await audit(ctx, {
|
||||||
|
action: "run",
|
||||||
|
resourceType: "backup",
|
||||||
|
resourceId: backup.backupId,
|
||||||
|
});
|
||||||
return true;
|
return true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const message =
|
const message =
|
||||||
@@ -213,12 +294,22 @@ export const backupRouter = createTRPCRouter({
|
|||||||
|
|
||||||
manualBackupMySql: protectedProcedure
|
manualBackupMySql: protectedProcedure
|
||||||
.input(apiFindOneBackup)
|
.input(apiFindOneBackup)
|
||||||
.mutation(async ({ input }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
try {
|
try {
|
||||||
const backup = await findBackupById(input.backupId);
|
const backup = await findBackupById(input.backupId);
|
||||||
|
if (backup.mysqlId) {
|
||||||
|
await checkServicePermissionAndAccess(ctx, backup.mysqlId, {
|
||||||
|
backup: ["create"],
|
||||||
|
});
|
||||||
|
}
|
||||||
const mysql = await findMySqlByBackupId(backup.backupId);
|
const mysql = await findMySqlByBackupId(backup.backupId);
|
||||||
await runMySqlBackup(mysql, backup);
|
await runMySqlBackup(mysql, backup);
|
||||||
await keepLatestNBackups(backup, mysql?.serverId);
|
await keepLatestNBackups(backup, mysql?.serverId);
|
||||||
|
await audit(ctx, {
|
||||||
|
action: "run",
|
||||||
|
resourceType: "backup",
|
||||||
|
resourceId: backup.backupId,
|
||||||
|
});
|
||||||
return true;
|
return true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
@@ -230,12 +321,22 @@ export const backupRouter = createTRPCRouter({
|
|||||||
}),
|
}),
|
||||||
manualBackupMariadb: protectedProcedure
|
manualBackupMariadb: protectedProcedure
|
||||||
.input(apiFindOneBackup)
|
.input(apiFindOneBackup)
|
||||||
.mutation(async ({ input }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
try {
|
try {
|
||||||
const backup = await findBackupById(input.backupId);
|
const backup = await findBackupById(input.backupId);
|
||||||
|
if (backup.mariadbId) {
|
||||||
|
await checkServicePermissionAndAccess(ctx, backup.mariadbId, {
|
||||||
|
backup: ["create"],
|
||||||
|
});
|
||||||
|
}
|
||||||
const mariadb = await findMariadbByBackupId(backup.backupId);
|
const mariadb = await findMariadbByBackupId(backup.backupId);
|
||||||
await runMariadbBackup(mariadb, backup);
|
await runMariadbBackup(mariadb, backup);
|
||||||
await keepLatestNBackups(backup, mariadb?.serverId);
|
await keepLatestNBackups(backup, mariadb?.serverId);
|
||||||
|
await audit(ctx, {
|
||||||
|
action: "run",
|
||||||
|
resourceType: "backup",
|
||||||
|
resourceId: backup.backupId,
|
||||||
|
});
|
||||||
return true;
|
return true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
@@ -247,12 +348,22 @@ export const backupRouter = createTRPCRouter({
|
|||||||
}),
|
}),
|
||||||
manualBackupCompose: protectedProcedure
|
manualBackupCompose: protectedProcedure
|
||||||
.input(apiFindOneBackup)
|
.input(apiFindOneBackup)
|
||||||
.mutation(async ({ input }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
try {
|
try {
|
||||||
const backup = await findBackupById(input.backupId);
|
const backup = await findBackupById(input.backupId);
|
||||||
|
if (backup.composeId) {
|
||||||
|
await checkServicePermissionAndAccess(ctx, backup.composeId, {
|
||||||
|
backup: ["create"],
|
||||||
|
});
|
||||||
|
}
|
||||||
const compose = await findComposeByBackupId(backup.backupId);
|
const compose = await findComposeByBackupId(backup.backupId);
|
||||||
await runComposeBackup(compose, backup);
|
await runComposeBackup(compose, backup);
|
||||||
await keepLatestNBackups(backup, compose?.serverId);
|
await keepLatestNBackups(backup, compose?.serverId);
|
||||||
|
await audit(ctx, {
|
||||||
|
action: "run",
|
||||||
|
resourceType: "backup",
|
||||||
|
resourceId: backup.backupId,
|
||||||
|
});
|
||||||
return true;
|
return true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
@@ -264,12 +375,22 @@ export const backupRouter = createTRPCRouter({
|
|||||||
}),
|
}),
|
||||||
manualBackupMongo: protectedProcedure
|
manualBackupMongo: protectedProcedure
|
||||||
.input(apiFindOneBackup)
|
.input(apiFindOneBackup)
|
||||||
.mutation(async ({ input }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
try {
|
try {
|
||||||
const backup = await findBackupById(input.backupId);
|
const backup = await findBackupById(input.backupId);
|
||||||
|
if (backup.mongoId) {
|
||||||
|
await checkServicePermissionAndAccess(ctx, backup.mongoId, {
|
||||||
|
backup: ["create"],
|
||||||
|
});
|
||||||
|
}
|
||||||
const mongo = await findMongoByBackupId(backup.backupId);
|
const mongo = await findMongoByBackupId(backup.backupId);
|
||||||
await runMongoBackup(mongo, backup);
|
await runMongoBackup(mongo, backup);
|
||||||
await keepLatestNBackups(backup, mongo?.serverId);
|
await keepLatestNBackups(backup, mongo?.serverId);
|
||||||
|
await audit(ctx, {
|
||||||
|
action: "run",
|
||||||
|
resourceType: "backup",
|
||||||
|
resourceId: backup.backupId,
|
||||||
|
});
|
||||||
return true;
|
return true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
@@ -279,15 +400,20 @@ export const backupRouter = createTRPCRouter({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
manualBackupWebServer: protectedProcedure
|
manualBackupWebServer: withPermission("backup", "create")
|
||||||
.input(apiFindOneBackup)
|
.input(apiFindOneBackup)
|
||||||
.mutation(async ({ input }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
const backup = await findBackupById(input.backupId);
|
const backup = await findBackupById(input.backupId);
|
||||||
await runWebServerBackup(backup);
|
await runWebServerBackup(backup);
|
||||||
await keepLatestNBackups(backup);
|
await keepLatestNBackups(backup);
|
||||||
|
await audit(ctx, {
|
||||||
|
action: "run",
|
||||||
|
resourceType: "backup",
|
||||||
|
resourceId: backup.backupId,
|
||||||
|
});
|
||||||
return true;
|
return true;
|
||||||
}),
|
}),
|
||||||
listBackupFiles: protectedProcedure
|
listBackupFiles: withPermission("backup", "read")
|
||||||
.input(
|
.input(
|
||||||
z.object({
|
z.object({
|
||||||
destinationId: z.string(),
|
destinationId: z.string(),
|
||||||
@@ -374,7 +500,12 @@ export const backupRouter = createTRPCRouter({
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
.input(apiRestoreBackup)
|
.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 destination = await findDestinationById(input.destinationId);
|
||||||
const queue: string[] = [];
|
const queue: string[] = [];
|
||||||
const done = false;
|
const done = false;
|
||||||
|
|||||||
@@ -8,7 +8,12 @@ import {
|
|||||||
} from "@dokploy/server";
|
} from "@dokploy/server";
|
||||||
import { db } from "@dokploy/server/db";
|
import { db } from "@dokploy/server/db";
|
||||||
import { TRPCError } from "@trpc/server";
|
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 {
|
import {
|
||||||
apiBitbucketTestConnection,
|
apiBitbucketTestConnection,
|
||||||
apiCreateBitbucket,
|
apiCreateBitbucket,
|
||||||
@@ -18,15 +23,23 @@ import {
|
|||||||
} from "@/server/db/schema";
|
} from "@/server/db/schema";
|
||||||
|
|
||||||
export const bitbucketRouter = createTRPCRouter({
|
export const bitbucketRouter = createTRPCRouter({
|
||||||
create: protectedProcedure
|
create: withPermission("gitProviders", "create")
|
||||||
.input(apiCreateBitbucket)
|
.input(apiCreateBitbucket)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
try {
|
try {
|
||||||
return await createBitbucket(
|
const result = await createBitbucket(
|
||||||
input,
|
input,
|
||||||
ctx.session.activeOrganizationId,
|
ctx.session.activeOrganizationId,
|
||||||
ctx.session.userId,
|
ctx.session.userId,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
await audit(ctx, {
|
||||||
|
action: "create",
|
||||||
|
resourceType: "gitProvider",
|
||||||
|
resourceName: input.name,
|
||||||
|
});
|
||||||
|
|
||||||
|
return result;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "BAD_REQUEST",
|
code: "BAD_REQUEST",
|
||||||
@@ -37,19 +50,8 @@ export const bitbucketRouter = createTRPCRouter({
|
|||||||
}),
|
}),
|
||||||
one: protectedProcedure
|
one: protectedProcedure
|
||||||
.input(apiFindOneBitbucket)
|
.input(apiFindOneBitbucket)
|
||||||
.query(async ({ input, ctx }) => {
|
.query(async ({ input }) => {
|
||||||
const bitbucketProvider = await findBitbucketById(input.bitbucketId);
|
return 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;
|
|
||||||
}),
|
}),
|
||||||
bitbucketProviders: protectedProcedure.query(async ({ ctx }) => {
|
bitbucketProviders: protectedProcedure.query(async ({ ctx }) => {
|
||||||
let result = await db.query.bitbucket.findMany({
|
let result = await db.query.bitbucket.findMany({
|
||||||
@@ -73,53 +75,18 @@ export const bitbucketRouter = createTRPCRouter({
|
|||||||
|
|
||||||
getBitbucketRepositories: protectedProcedure
|
getBitbucketRepositories: protectedProcedure
|
||||||
.input(apiFindOneBitbucket)
|
.input(apiFindOneBitbucket)
|
||||||
.query(async ({ input, ctx }) => {
|
.query(async ({ input }) => {
|
||||||
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 getBitbucketRepositories(input.bitbucketId);
|
return await getBitbucketRepositories(input.bitbucketId);
|
||||||
}),
|
}),
|
||||||
getBitbucketBranches: protectedProcedure
|
getBitbucketBranches: protectedProcedure
|
||||||
.input(apiFindBitbucketBranches)
|
.input(apiFindBitbucketBranches)
|
||||||
.query(async ({ input, ctx }) => {
|
.query(async ({ input }) => {
|
||||||
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 getBitbucketBranches(input);
|
return await getBitbucketBranches(input);
|
||||||
}),
|
}),
|
||||||
testConnection: protectedProcedure
|
testConnection: protectedProcedure
|
||||||
.input(apiBitbucketTestConnection)
|
.input(apiBitbucketTestConnection)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input }) => {
|
||||||
try {
|
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);
|
const result = await testBitbucketConnection(input);
|
||||||
|
|
||||||
return `Found ${result} repositories`;
|
return `Found ${result} repositories`;
|
||||||
@@ -130,23 +97,21 @@ export const bitbucketRouter = createTRPCRouter({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
update: protectedProcedure
|
update: withPermission("gitProviders", "create")
|
||||||
.input(apiUpdateBitbucket)
|
.input(apiUpdateBitbucket)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
const bitbucketProvider = await findBitbucketById(input.bitbucketId);
|
const result = await updateBitbucket(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, {
|
|
||||||
...input,
|
...input,
|
||||||
organizationId: ctx.session.activeOrganizationId,
|
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 { db } from "@dokploy/server/db";
|
||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
import { eq } from "drizzle-orm";
|
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 {
|
import {
|
||||||
apiCreateCertificate,
|
apiCreateCertificate,
|
||||||
apiFindCertificate,
|
apiFindCertificate,
|
||||||
@@ -15,7 +16,7 @@ import {
|
|||||||
} from "@/server/db/schema";
|
} from "@/server/db/schema";
|
||||||
|
|
||||||
export const certificateRouter = createTRPCRouter({
|
export const certificateRouter = createTRPCRouter({
|
||||||
create: adminProcedure
|
create: withPermission("certificate", "create")
|
||||||
.input(apiCreateCertificate)
|
.input(apiCreateCertificate)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
if (IS_CLOUD && !input.serverId) {
|
if (IS_CLOUD && !input.serverId) {
|
||||||
@@ -24,10 +25,20 @@ export const certificateRouter = createTRPCRouter({
|
|||||||
message: "Please set a server to create a certificate",
|
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)
|
.input(apiFindCertificate)
|
||||||
.query(async ({ input, ctx }) => {
|
.query(async ({ input, ctx }) => {
|
||||||
const certificates = await findCertificateById(input.certificateId);
|
const certificates = await findCertificateById(input.certificateId);
|
||||||
@@ -39,7 +50,7 @@ export const certificateRouter = createTRPCRouter({
|
|||||||
}
|
}
|
||||||
return certificates;
|
return certificates;
|
||||||
}),
|
}),
|
||||||
remove: adminProcedure
|
remove: withPermission("certificate", "delete")
|
||||||
.input(apiFindCertificate)
|
.input(apiFindCertificate)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
const certificates = await findCertificateById(input.certificateId);
|
const certificates = await findCertificateById(input.certificateId);
|
||||||
@@ -49,10 +60,16 @@ export const certificateRouter = createTRPCRouter({
|
|||||||
message: "You are not allowed to delete this certificate",
|
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);
|
await removeCertificateById(input.certificateId);
|
||||||
return true;
|
return true;
|
||||||
}),
|
}),
|
||||||
all: adminProcedure.query(async ({ ctx }) => {
|
all: withPermission("certificate", "read").query(async ({ ctx }) => {
|
||||||
return await db.query.certificates.findMany({
|
return await db.query.certificates.findMany({
|
||||||
where: eq(certificates.organizationId, ctx.session.activeOrganizationId),
|
where: eq(certificates.organizationId, ctx.session.activeOrganizationId),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -7,10 +7,12 @@ import {
|
|||||||
} from "@dokploy/server";
|
} from "@dokploy/server";
|
||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
import { audit } from "@/server/api/utils/audit";
|
||||||
import { getLocalServerIp } from "@/server/wss/terminal";
|
import { getLocalServerIp } from "@/server/wss/terminal";
|
||||||
import { createTRPCRouter, protectedProcedure } from "../trpc";
|
import { createTRPCRouter, withPermission } from "../trpc";
|
||||||
|
|
||||||
export const clusterRouter = createTRPCRouter({
|
export const clusterRouter = createTRPCRouter({
|
||||||
getNodes: protectedProcedure
|
getNodes: withPermission("server", "read")
|
||||||
.input(
|
.input(
|
||||||
z.object({
|
z.object({
|
||||||
serverId: z.string().optional(),
|
serverId: z.string().optional(),
|
||||||
@@ -19,17 +21,17 @@ export const clusterRouter = createTRPCRouter({
|
|||||||
.query(async ({ input }) => {
|
.query(async ({ input }) => {
|
||||||
const docker = await getRemoteDocker(input.serverId);
|
const docker = await getRemoteDocker(input.serverId);
|
||||||
const workers: DockerNode[] = await docker.listNodes();
|
const workers: DockerNode[] = await docker.listNodes();
|
||||||
|
|
||||||
return workers;
|
return workers;
|
||||||
}),
|
}),
|
||||||
removeWorker: protectedProcedure
|
|
||||||
|
removeWorker: withPermission("server", "delete")
|
||||||
.input(
|
.input(
|
||||||
z.object({
|
z.object({
|
||||||
nodeId: z.string(),
|
nodeId: z.string(),
|
||||||
serverId: z.string().optional(),
|
serverId: z.string().optional(),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.mutation(async ({ input }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
try {
|
try {
|
||||||
const drainCommand = `docker node update --availability drain ${input.nodeId}`;
|
const drainCommand = `docker node update --availability drain ${input.nodeId}`;
|
||||||
const removeCommand = `docker node rm ${input.nodeId} --force`;
|
const removeCommand = `docker node rm ${input.nodeId} --force`;
|
||||||
@@ -41,6 +43,12 @@ export const clusterRouter = createTRPCRouter({
|
|||||||
await execAsync(drainCommand);
|
await execAsync(drainCommand);
|
||||||
await execAsync(removeCommand);
|
await execAsync(removeCommand);
|
||||||
}
|
}
|
||||||
|
await audit(ctx, {
|
||||||
|
action: "delete",
|
||||||
|
resourceType: "cluster",
|
||||||
|
resourceId: input.nodeId,
|
||||||
|
resourceName: input.nodeId,
|
||||||
|
});
|
||||||
return true;
|
return true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
@@ -50,7 +58,8 @@ export const clusterRouter = createTRPCRouter({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
addWorker: protectedProcedure
|
|
||||||
|
addWorker: withPermission("server", "create")
|
||||||
.input(
|
.input(
|
||||||
z.object({
|
z.object({
|
||||||
serverId: z.string().optional(),
|
serverId: z.string().optional(),
|
||||||
@@ -68,13 +77,12 @@ export const clusterRouter = createTRPCRouter({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
command: `docker swarm join --token ${
|
command: `docker swarm join --token ${result.JoinTokens.Worker} ${ip}:2377`,
|
||||||
result.JoinTokens.Worker
|
|
||||||
} ${ip}:2377`,
|
|
||||||
version: docker_version.Version,
|
version: docker_version.Version,
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
addManager: protectedProcedure
|
|
||||||
|
addManager: withPermission("server", "create")
|
||||||
.input(
|
.input(
|
||||||
z.object({
|
z.object({
|
||||||
serverId: z.string().optional(),
|
serverId: z.string().optional(),
|
||||||
@@ -91,9 +99,7 @@ export const clusterRouter = createTRPCRouter({
|
|||||||
ip = server?.ipAddress;
|
ip = server?.ipAddress;
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
command: `docker swarm join --token ${
|
command: `docker swarm join --token ${result.JoinTokens.Manager} ${ip}:2377`,
|
||||||
result.JoinTokens.Manager
|
|
||||||
} ${ip}:2377`,
|
|
||||||
version: docker_version.Version,
|
version: docker_version.Version,
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
import {
|
import {
|
||||||
addDomainToCompose,
|
addDomainToCompose,
|
||||||
addNewService,
|
|
||||||
checkServiceAccess,
|
|
||||||
clearOldDeployments,
|
clearOldDeployments,
|
||||||
cloneCompose,
|
cloneCompose,
|
||||||
createCommand,
|
createCommand,
|
||||||
@@ -16,7 +14,6 @@ import {
|
|||||||
findDomainsByComposeId,
|
findDomainsByComposeId,
|
||||||
findEnvironmentById,
|
findEnvironmentById,
|
||||||
findGitProviderById,
|
findGitProviderById,
|
||||||
findMemberById,
|
|
||||||
findProjectById,
|
findProjectById,
|
||||||
findServerById,
|
findServerById,
|
||||||
getComposeContainer,
|
getComposeContainer,
|
||||||
@@ -34,6 +31,12 @@ import {
|
|||||||
updateCompose,
|
updateCompose,
|
||||||
updateDeploymentStatus,
|
updateDeploymentStatus,
|
||||||
} from "@dokploy/server";
|
} from "@dokploy/server";
|
||||||
|
import {
|
||||||
|
addNewService,
|
||||||
|
checkServiceAccess,
|
||||||
|
checkServicePermissionAndAccess,
|
||||||
|
findMemberByUserId,
|
||||||
|
} from "@dokploy/server/services/permission";
|
||||||
import { db } from "@dokploy/server/db";
|
import { db } from "@dokploy/server/db";
|
||||||
import {
|
import {
|
||||||
type CompleteTemplate,
|
type CompleteTemplate,
|
||||||
@@ -72,6 +75,7 @@ import {
|
|||||||
} from "@/server/queues/queueSetup";
|
} from "@/server/queues/queueSetup";
|
||||||
import { cancelDeployment, deploy } from "@/server/utils/deploy";
|
import { cancelDeployment, deploy } from "@/server/utils/deploy";
|
||||||
import { generatePassword } from "@/templates/utils";
|
import { generatePassword } from "@/templates/utils";
|
||||||
|
import { audit } from "../utils/audit";
|
||||||
import { createTRPCRouter, protectedProcedure, publicProcedure } from "../trpc";
|
import { createTRPCRouter, protectedProcedure, publicProcedure } from "../trpc";
|
||||||
|
|
||||||
export const composeRouter = createTRPCRouter({
|
export const composeRouter = createTRPCRouter({
|
||||||
@@ -79,18 +83,10 @@ export const composeRouter = createTRPCRouter({
|
|||||||
.input(apiCreateCompose)
|
.input(apiCreateCompose)
|
||||||
.mutation(async ({ ctx, input }) => {
|
.mutation(async ({ ctx, input }) => {
|
||||||
try {
|
try {
|
||||||
// Get project from environment
|
|
||||||
const environment = await findEnvironmentById(input.environmentId);
|
const environment = await findEnvironmentById(input.environmentId);
|
||||||
const project = await findProjectById(environment.projectId);
|
const project = await findProjectById(environment.projectId);
|
||||||
|
|
||||||
if (ctx.user.role === "member") {
|
await checkServiceAccess(ctx, project.projectId, "create");
|
||||||
await checkServiceAccess(
|
|
||||||
ctx.user.id,
|
|
||||||
project.projectId,
|
|
||||||
ctx.session.activeOrganizationId,
|
|
||||||
"create",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (IS_CLOUD && !input.serverId) {
|
if (IS_CLOUD && !input.serverId) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
@@ -108,14 +104,14 @@ export const composeRouter = createTRPCRouter({
|
|||||||
...input,
|
...input,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (ctx.user.role === "member") {
|
await addNewService(ctx, newService.composeId);
|
||||||
await addNewService(
|
|
||||||
ctx.user.id,
|
|
||||||
newService.composeId,
|
|
||||||
project.organizationId,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
await audit(ctx, {
|
||||||
|
action: "create",
|
||||||
|
resourceType: "service",
|
||||||
|
resourceId: newService.composeId,
|
||||||
|
resourceName: newService.appName,
|
||||||
|
});
|
||||||
return newService;
|
return newService;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw error;
|
throw error;
|
||||||
@@ -125,14 +121,7 @@ export const composeRouter = createTRPCRouter({
|
|||||||
one: protectedProcedure
|
one: protectedProcedure
|
||||||
.input(apiFindCompose)
|
.input(apiFindCompose)
|
||||||
.query(async ({ input, ctx }) => {
|
.query(async ({ input, ctx }) => {
|
||||||
if (ctx.user.role === "member") {
|
await checkServiceAccess(ctx, input.composeId, "read");
|
||||||
await checkServiceAccess(
|
|
||||||
ctx.user.id,
|
|
||||||
input.composeId,
|
|
||||||
ctx.session.activeOrganizationId,
|
|
||||||
"access",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const compose = await findComposeById(input.composeId);
|
const compose = await findComposeById(input.composeId);
|
||||||
if (
|
if (
|
||||||
@@ -188,29 +177,22 @@ export const composeRouter = createTRPCRouter({
|
|||||||
update: protectedProcedure
|
update: protectedProcedure
|
||||||
.input(apiUpdateCompose)
|
.input(apiUpdateCompose)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
const compose = await findComposeById(input.composeId);
|
await checkServicePermissionAndAccess(ctx, input.composeId, {
|
||||||
if (
|
service: ["create"],
|
||||||
compose.environment.project.organizationId !==
|
});
|
||||||
ctx.session.activeOrganizationId
|
const updated = await updateCompose(input.composeId, input);
|
||||||
) {
|
await audit(ctx, {
|
||||||
throw new TRPCError({
|
action: "update",
|
||||||
code: "UNAUTHORIZED",
|
resourceType: "compose",
|
||||||
message: "You are not authorized to update this compose",
|
resourceId: input.composeId,
|
||||||
});
|
resourceName: updated?.name,
|
||||||
}
|
});
|
||||||
return updateCompose(input.composeId, input);
|
return updated;
|
||||||
}),
|
}),
|
||||||
delete: protectedProcedure
|
delete: protectedProcedure
|
||||||
.input(apiDeleteCompose)
|
.input(apiDeleteCompose)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
if (ctx.user.role === "member") {
|
await checkServiceAccess(ctx, input.composeId, "delete");
|
||||||
await checkServiceAccess(
|
|
||||||
ctx.user.id,
|
|
||||||
input.composeId,
|
|
||||||
ctx.session.activeOrganizationId,
|
|
||||||
"delete",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
const composeResult = await findComposeById(input.composeId);
|
const composeResult = await findComposeById(input.composeId);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
@@ -249,70 +231,55 @@ export const composeRouter = createTRPCRouter({
|
|||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await audit(ctx, {
|
||||||
|
action: "delete",
|
||||||
|
resourceType: "service",
|
||||||
|
resourceId: composeResult.composeId,
|
||||||
|
resourceName: composeResult.appName,
|
||||||
|
});
|
||||||
return composeResult;
|
return composeResult;
|
||||||
}),
|
}),
|
||||||
cleanQueues: protectedProcedure
|
cleanQueues: protectedProcedure
|
||||||
.input(apiFindCompose)
|
.input(apiFindCompose)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
const compose = await findComposeById(input.composeId);
|
await checkServicePermissionAndAccess(ctx, input.composeId, {
|
||||||
if (
|
deployment: ["create"],
|
||||||
compose.environment.project.organizationId !==
|
});
|
||||||
ctx.session.activeOrganizationId
|
|
||||||
) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "UNAUTHORIZED",
|
|
||||||
message: "You are not authorized to clean this compose",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
await cleanQueuesByCompose(input.composeId);
|
await cleanQueuesByCompose(input.composeId);
|
||||||
return { success: true, message: "Queues cleaned successfully" };
|
return { success: true, message: "Queues cleaned successfully" };
|
||||||
}),
|
}),
|
||||||
clearDeployments: protectedProcedure
|
clearDeployments: protectedProcedure
|
||||||
.input(apiFindCompose)
|
.input(apiFindCompose)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
|
await checkServicePermissionAndAccess(ctx, input.composeId, {
|
||||||
|
deployment: ["create"],
|
||||||
|
});
|
||||||
const compose = await findComposeById(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 clear deployments for this compose",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
await clearOldDeployments(compose.appName, compose.serverId);
|
await clearOldDeployments(compose.appName, compose.serverId);
|
||||||
|
await audit(ctx, {
|
||||||
|
action: "update",
|
||||||
|
resourceType: "compose",
|
||||||
|
resourceId: input.composeId,
|
||||||
|
resourceName: compose.name,
|
||||||
|
});
|
||||||
return true;
|
return true;
|
||||||
}),
|
}),
|
||||||
killBuild: protectedProcedure
|
killBuild: protectedProcedure
|
||||||
.input(apiFindCompose)
|
.input(apiFindCompose)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
|
await checkServicePermissionAndAccess(ctx, input.composeId, {
|
||||||
|
deployment: ["cancel"],
|
||||||
|
});
|
||||||
const compose = await findComposeById(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 kill this build",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
await killDockerBuild("compose", compose.serverId);
|
await killDockerBuild("compose", compose.serverId);
|
||||||
}),
|
}),
|
||||||
|
|
||||||
loadServices: protectedProcedure
|
loadServices: protectedProcedure
|
||||||
.input(apiFetchServices)
|
.input(apiFetchServices)
|
||||||
.query(async ({ input, ctx }) => {
|
.query(async ({ input, ctx }) => {
|
||||||
const compose = await findComposeById(input.composeId);
|
await checkServicePermissionAndAccess(ctx, input.composeId, {
|
||||||
if (
|
service: ["create"],
|
||||||
compose.environment.project.organizationId !==
|
});
|
||||||
ctx.session.activeOrganizationId
|
|
||||||
) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "UNAUTHORIZED",
|
|
||||||
message: "You are not authorized to load this compose",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return await loadServices(input.composeId, input.type);
|
return await loadServices(input.composeId, input.type);
|
||||||
}),
|
}),
|
||||||
loadMountsByService: protectedProcedure
|
loadMountsByService: protectedProcedure
|
||||||
@@ -323,16 +290,10 @@ export const composeRouter = createTRPCRouter({
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.query(async ({ input, ctx }) => {
|
.query(async ({ input, ctx }) => {
|
||||||
|
await checkServicePermissionAndAccess(ctx, input.composeId, {
|
||||||
|
service: ["create"],
|
||||||
|
});
|
||||||
const compose = await findComposeById(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 load this compose",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
const container = await getComposeContainer(compose, input.serviceName);
|
const container = await getComposeContainer(compose, input.serviceName);
|
||||||
const mounts = container?.Mounts.filter(
|
const mounts = container?.Mounts.filter(
|
||||||
(mount) => mount.Type === "volume" && mount.Source !== "",
|
(mount) => mount.Type === "volume" && mount.Source !== "",
|
||||||
@@ -343,18 +304,11 @@ export const composeRouter = createTRPCRouter({
|
|||||||
.input(apiFindCompose)
|
.input(apiFindCompose)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
try {
|
try {
|
||||||
|
await checkServicePermissionAndAccess(ctx, input.composeId, {
|
||||||
|
service: ["create"],
|
||||||
|
});
|
||||||
const compose = await findComposeById(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 fetch this compose",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const command = await cloneCompose(compose);
|
const command = await cloneCompose(compose);
|
||||||
if (compose.serverId) {
|
if (compose.serverId) {
|
||||||
await execAsyncRemote(compose.serverId, command);
|
await execAsyncRemote(compose.serverId, command);
|
||||||
@@ -374,49 +328,45 @@ export const composeRouter = createTRPCRouter({
|
|||||||
randomizeCompose: protectedProcedure
|
randomizeCompose: protectedProcedure
|
||||||
.input(apiRandomizeCompose)
|
.input(apiRandomizeCompose)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.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);
|
const compose = await findComposeById(input.composeId);
|
||||||
if (
|
await audit(ctx, {
|
||||||
compose.environment.project.organizationId !==
|
action: "update",
|
||||||
ctx.session.activeOrganizationId
|
resourceType: "compose",
|
||||||
) {
|
resourceId: input.composeId,
|
||||||
throw new TRPCError({
|
resourceName: compose.name,
|
||||||
code: "UNAUTHORIZED",
|
});
|
||||||
message: "You are not authorized to randomize this compose",
|
return result;
|
||||||
});
|
|
||||||
}
|
|
||||||
return await randomizeComposeFile(input.composeId, input.suffix);
|
|
||||||
}),
|
}),
|
||||||
isolatedDeployment: protectedProcedure
|
isolatedDeployment: protectedProcedure
|
||||||
.input(apiRandomizeCompose)
|
.input(apiRandomizeCompose)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
const compose = await findComposeById(input.composeId);
|
await checkServicePermissionAndAccess(ctx, input.composeId, {
|
||||||
if (
|
service: ["create"],
|
||||||
compose.environment.project.organizationId !==
|
});
|
||||||
ctx.session.activeOrganizationId
|
const result = await randomizeIsolatedDeploymentComposeFile(
|
||||||
) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "UNAUTHORIZED",
|
|
||||||
message: "You are not authorized to randomize this compose",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return await randomizeIsolatedDeploymentComposeFile(
|
|
||||||
input.composeId,
|
input.composeId,
|
||||||
input.suffix,
|
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
|
getConvertedCompose: protectedProcedure
|
||||||
.input(apiFindCompose)
|
.input(apiFindCompose)
|
||||||
.query(async ({ input, ctx }) => {
|
.query(async ({ input, ctx }) => {
|
||||||
|
await checkServicePermissionAndAccess(ctx, input.composeId, {
|
||||||
|
service: ["create"],
|
||||||
|
});
|
||||||
const compose = await findComposeById(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 get this compose",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
const domains = await findDomainsByComposeId(input.composeId);
|
const domains = await findDomainsByComposeId(input.composeId);
|
||||||
const composeFile = await addDomainToCompose(compose, domains);
|
const composeFile = await addDomainToCompose(compose, domains);
|
||||||
return stringify(composeFile, {
|
return stringify(composeFile, {
|
||||||
@@ -427,17 +377,11 @@ export const composeRouter = createTRPCRouter({
|
|||||||
deploy: protectedProcedure
|
deploy: protectedProcedure
|
||||||
.input(apiDeployCompose)
|
.input(apiDeployCompose)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
|
await checkServicePermissionAndAccess(ctx, input.composeId, {
|
||||||
|
deployment: ["create"],
|
||||||
|
});
|
||||||
const compose = await findComposeById(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 deploy this compose",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
const jobData: DeploymentJob = {
|
const jobData: DeploymentJob = {
|
||||||
composeId: input.composeId,
|
composeId: input.composeId,
|
||||||
titleLog: input.title || "Manual deployment",
|
titleLog: input.title || "Manual deployment",
|
||||||
@@ -452,6 +396,12 @@ export const composeRouter = createTRPCRouter({
|
|||||||
deploy(jobData).catch((error) => {
|
deploy(jobData).catch((error) => {
|
||||||
console.error("Background deployment failed:", error);
|
console.error("Background deployment failed:", error);
|
||||||
});
|
});
|
||||||
|
await audit(ctx, {
|
||||||
|
action: "deploy",
|
||||||
|
resourceType: "compose",
|
||||||
|
resourceId: input.composeId,
|
||||||
|
resourceName: compose.name,
|
||||||
|
});
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
await myQueue.add(
|
await myQueue.add(
|
||||||
@@ -462,6 +412,12 @@ export const composeRouter = createTRPCRouter({
|
|||||||
removeOnFail: true,
|
removeOnFail: true,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
await audit(ctx, {
|
||||||
|
action: "deploy",
|
||||||
|
resourceType: "compose",
|
||||||
|
resourceId: input.composeId,
|
||||||
|
resourceName: compose.name,
|
||||||
|
});
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
message: "Deployment queued",
|
message: "Deployment queued",
|
||||||
@@ -471,16 +427,10 @@ export const composeRouter = createTRPCRouter({
|
|||||||
redeploy: protectedProcedure
|
redeploy: protectedProcedure
|
||||||
.input(apiRedeployCompose)
|
.input(apiRedeployCompose)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
|
await checkServicePermissionAndAccess(ctx, input.composeId, {
|
||||||
|
deployment: ["create"],
|
||||||
|
});
|
||||||
const compose = await findComposeById(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 redeploy this compose",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
const jobData: DeploymentJob = {
|
const jobData: DeploymentJob = {
|
||||||
composeId: input.composeId,
|
composeId: input.composeId,
|
||||||
titleLog: input.title || "Rebuild deployment",
|
titleLog: input.title || "Rebuild deployment",
|
||||||
@@ -494,6 +444,12 @@ export const composeRouter = createTRPCRouter({
|
|||||||
deploy(jobData).catch((error) => {
|
deploy(jobData).catch((error) => {
|
||||||
console.error("Background deployment failed:", error);
|
console.error("Background deployment failed:", error);
|
||||||
});
|
});
|
||||||
|
await audit(ctx, {
|
||||||
|
action: "deploy",
|
||||||
|
resourceType: "compose",
|
||||||
|
resourceId: input.composeId,
|
||||||
|
resourceName: compose.name,
|
||||||
|
});
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
await myQueue.add(
|
await myQueue.add(
|
||||||
@@ -504,6 +460,12 @@ export const composeRouter = createTRPCRouter({
|
|||||||
removeOnFail: true,
|
removeOnFail: true,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
await audit(ctx, {
|
||||||
|
action: "deploy",
|
||||||
|
resourceType: "compose",
|
||||||
|
resourceId: input.composeId,
|
||||||
|
resourceName: compose.name,
|
||||||
|
});
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
message: "Redeployment queued",
|
message: "Redeployment queued",
|
||||||
@@ -513,70 +475,61 @@ export const composeRouter = createTRPCRouter({
|
|||||||
stop: protectedProcedure
|
stop: protectedProcedure
|
||||||
.input(apiFindCompose)
|
.input(apiFindCompose)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
const compose = await findComposeById(input.composeId);
|
await checkServicePermissionAndAccess(ctx, input.composeId, {
|
||||||
if (
|
deployment: ["create"],
|
||||||
compose.environment.project.organizationId !==
|
});
|
||||||
ctx.session.activeOrganizationId
|
|
||||||
) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "UNAUTHORIZED",
|
|
||||||
message: "You are not authorized to stop this compose",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
await stopCompose(input.composeId);
|
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;
|
return true;
|
||||||
}),
|
}),
|
||||||
start: protectedProcedure
|
start: protectedProcedure
|
||||||
.input(apiFindCompose)
|
.input(apiFindCompose)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
const compose = await findComposeById(input.composeId);
|
await checkServicePermissionAndAccess(ctx, input.composeId, {
|
||||||
if (
|
deployment: ["create"],
|
||||||
compose.environment.project.organizationId !==
|
});
|
||||||
ctx.session.activeOrganizationId
|
|
||||||
) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "UNAUTHORIZED",
|
|
||||||
message: "You are not authorized to stop this compose",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
await startCompose(input.composeId);
|
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;
|
return true;
|
||||||
}),
|
}),
|
||||||
getDefaultCommand: protectedProcedure
|
getDefaultCommand: protectedProcedure
|
||||||
.input(apiFindCompose)
|
.input(apiFindCompose)
|
||||||
.query(async ({ input, ctx }) => {
|
.query(async ({ input, ctx }) => {
|
||||||
|
await checkServicePermissionAndAccess(ctx, input.composeId, {
|
||||||
|
service: ["create"],
|
||||||
|
});
|
||||||
const compose = await findComposeById(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 get this compose",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
const command = createCommand(compose);
|
const command = createCommand(compose);
|
||||||
return `docker ${command}`;
|
return `docker ${command}`;
|
||||||
}),
|
}),
|
||||||
refreshToken: protectedProcedure
|
refreshToken: protectedProcedure
|
||||||
.input(apiFindCompose)
|
.input(apiFindCompose)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
const compose = await findComposeById(input.composeId);
|
await checkServicePermissionAndAccess(ctx, input.composeId, {
|
||||||
if (
|
service: ["create"],
|
||||||
compose.environment.project.organizationId !==
|
});
|
||||||
ctx.session.activeOrganizationId
|
|
||||||
) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "UNAUTHORIZED",
|
|
||||||
message: "You are not authorized to refresh this compose",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
await updateCompose(input.composeId, {
|
await updateCompose(input.composeId, {
|
||||||
refreshToken: nanoid(),
|
refreshToken: nanoid(),
|
||||||
});
|
});
|
||||||
|
const composeForToken = await findComposeById(input.composeId);
|
||||||
|
await audit(ctx, {
|
||||||
|
action: "update",
|
||||||
|
resourceType: "compose",
|
||||||
|
resourceId: input.composeId,
|
||||||
|
resourceName: composeForToken.name,
|
||||||
|
});
|
||||||
return true;
|
return true;
|
||||||
}),
|
}),
|
||||||
deployTemplate: protectedProcedure
|
deployTemplate: protectedProcedure
|
||||||
@@ -591,14 +544,7 @@ export const composeRouter = createTRPCRouter({
|
|||||||
.mutation(async ({ ctx, input }) => {
|
.mutation(async ({ ctx, input }) => {
|
||||||
const environment = await findEnvironmentById(input.environmentId);
|
const environment = await findEnvironmentById(input.environmentId);
|
||||||
|
|
||||||
if (ctx.user.role === "member") {
|
await checkServiceAccess(ctx, environment.projectId, "create");
|
||||||
await checkServiceAccess(
|
|
||||||
ctx.user.id,
|
|
||||||
environment.projectId,
|
|
||||||
ctx.session.activeOrganizationId,
|
|
||||||
"create",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (IS_CLOUD && !input.serverId) {
|
if (IS_CLOUD && !input.serverId) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
@@ -648,13 +594,7 @@ export const composeRouter = createTRPCRouter({
|
|||||||
isolatedDeployment: true,
|
isolatedDeployment: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (ctx.user.role === "member") {
|
await addNewService(ctx, compose.composeId);
|
||||||
await addNewService(
|
|
||||||
ctx.user.id,
|
|
||||||
compose.composeId,
|
|
||||||
ctx.session.activeOrganizationId,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (generate.mounts && generate.mounts?.length > 0) {
|
if (generate.mounts && generate.mounts?.length > 0) {
|
||||||
for (const mount of generate.mounts) {
|
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;
|
return compose;
|
||||||
}),
|
}),
|
||||||
|
|
||||||
@@ -714,20 +660,11 @@ export const composeRouter = createTRPCRouter({
|
|||||||
disconnectGitProvider: protectedProcedure
|
disconnectGitProvider: protectedProcedure
|
||||||
.input(apiFindCompose)
|
.input(apiFindCompose)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
const compose = await findComposeById(input.composeId);
|
await checkServicePermissionAndAccess(ctx, input.composeId, {
|
||||||
if (
|
service: ["create"],
|
||||||
compose.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 updateCompose(input.composeId, {
|
await updateCompose(input.composeId, {
|
||||||
// GitHub fields
|
|
||||||
repository: null,
|
repository: null,
|
||||||
branch: null,
|
branch: null,
|
||||||
owner: null,
|
owner: null,
|
||||||
@@ -735,7 +672,6 @@ export const composeRouter = createTRPCRouter({
|
|||||||
githubId: null,
|
githubId: null,
|
||||||
triggerType: "push",
|
triggerType: "push",
|
||||||
|
|
||||||
// GitLab fields
|
|
||||||
gitlabRepository: null,
|
gitlabRepository: null,
|
||||||
gitlabOwner: null,
|
gitlabOwner: null,
|
||||||
gitlabBranch: null,
|
gitlabBranch: null,
|
||||||
@@ -743,30 +679,33 @@ export const composeRouter = createTRPCRouter({
|
|||||||
gitlabProjectId: null,
|
gitlabProjectId: null,
|
||||||
gitlabPathNamespace: null,
|
gitlabPathNamespace: null,
|
||||||
|
|
||||||
// Bitbucket fields
|
|
||||||
bitbucketRepository: null,
|
bitbucketRepository: null,
|
||||||
bitbucketOwner: null,
|
bitbucketOwner: null,
|
||||||
bitbucketBranch: null,
|
bitbucketBranch: null,
|
||||||
bitbucketId: null,
|
bitbucketId: null,
|
||||||
|
|
||||||
// Gitea fields
|
|
||||||
giteaRepository: null,
|
giteaRepository: null,
|
||||||
giteaOwner: null,
|
giteaOwner: null,
|
||||||
giteaBranch: null,
|
giteaBranch: null,
|
||||||
giteaId: null,
|
giteaId: null,
|
||||||
|
|
||||||
// Custom Git fields
|
|
||||||
customGitBranch: null,
|
customGitBranch: null,
|
||||||
customGitUrl: null,
|
customGitUrl: null,
|
||||||
customGitSSHKeyId: null,
|
customGitSSHKeyId: null,
|
||||||
|
|
||||||
// Common fields
|
|
||||||
sourceType: "github", // Reset to default
|
sourceType: "github", // Reset to default
|
||||||
composeStatus: "idle",
|
composeStatus: "idle",
|
||||||
watchPaths: null,
|
watchPaths: null,
|
||||||
enableSubmodules: false,
|
enableSubmodules: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const composeForDisconnect = await findComposeById(input.composeId);
|
||||||
|
await audit(ctx, {
|
||||||
|
action: "update",
|
||||||
|
resourceType: "compose",
|
||||||
|
resourceId: input.composeId,
|
||||||
|
resourceName: composeForDisconnect.name,
|
||||||
|
});
|
||||||
return true;
|
return true;
|
||||||
}),
|
}),
|
||||||
|
|
||||||
@@ -778,29 +717,9 @@ export const composeRouter = createTRPCRouter({
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
const compose = await findComposeById(input.composeId);
|
await checkServicePermissionAndAccess(ctx, input.composeId, {
|
||||||
if (
|
service: ["create"],
|
||||||
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",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const updatedCompose = await db
|
const updatedCompose = await db
|
||||||
.update(composeTable)
|
.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;
|
return updatedCompose;
|
||||||
}),
|
}),
|
||||||
|
|
||||||
@@ -830,18 +755,11 @@ export const composeRouter = createTRPCRouter({
|
|||||||
)
|
)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
try {
|
try {
|
||||||
|
await checkServicePermissionAndAccess(ctx, input.composeId, {
|
||||||
|
service: ["create"],
|
||||||
|
});
|
||||||
const compose = await findComposeById(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 update this compose",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const decodedData = Buffer.from(input.base64, "base64").toString(
|
const decodedData = Buffer.from(input.base64, "base64").toString(
|
||||||
"utf-8",
|
"utf-8",
|
||||||
);
|
);
|
||||||
@@ -901,21 +819,14 @@ export const composeRouter = createTRPCRouter({
|
|||||||
)
|
)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
try {
|
try {
|
||||||
|
await checkServicePermissionAndAccess(ctx, input.composeId, {
|
||||||
|
service: ["create"],
|
||||||
|
});
|
||||||
const compose = await findComposeById(input.composeId);
|
const compose = await findComposeById(input.composeId);
|
||||||
const decodedData = Buffer.from(input.base64, "base64").toString(
|
const decodedData = Buffer.from(input.base64, "base64").toString(
|
||||||
"utf-8",
|
"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) {
|
for (const mount of compose.mounts) {
|
||||||
await deleteMount(mount.mountId);
|
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 {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
message: "Template imported successfully",
|
message: "Template imported successfully",
|
||||||
@@ -1008,16 +925,10 @@ export const composeRouter = createTRPCRouter({
|
|||||||
cancelDeployment: protectedProcedure
|
cancelDeployment: protectedProcedure
|
||||||
.input(apiFindCompose)
|
.input(apiFindCompose)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
|
await checkServicePermissionAndAccess(ctx, input.composeId, {
|
||||||
|
deployment: ["cancel"],
|
||||||
|
});
|
||||||
const compose = await findComposeById(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 cancel this deployment",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (IS_CLOUD && compose.serverId) {
|
if (IS_CLOUD && compose.serverId) {
|
||||||
try {
|
try {
|
||||||
@@ -1037,6 +948,12 @@ export const composeRouter = createTRPCRouter({
|
|||||||
applicationType: "compose",
|
applicationType: "compose",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await audit(ctx, {
|
||||||
|
action: "stop",
|
||||||
|
resourceType: "compose",
|
||||||
|
resourceId: input.composeId,
|
||||||
|
resourceName: compose.name,
|
||||||
|
});
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
message: "Deployment cancellation requested",
|
message: "Deployment cancellation requested",
|
||||||
@@ -1113,19 +1030,17 @@ export const composeRouter = createTRPCRouter({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ctx.user.role === "member") {
|
const { accessedServices } = await findMemberByUserId(
|
||||||
const { accessedServices } = await findMemberById(
|
ctx.user.id,
|
||||||
ctx.user.id,
|
ctx.session.activeOrganizationId,
|
||||||
ctx.session.activeOrganizationId,
|
);
|
||||||
);
|
if (accessedServices.length === 0) return { items: [], total: 0 };
|
||||||
if (accessedServices.length === 0) return { items: [], total: 0 };
|
baseConditions.push(
|
||||||
baseConditions.push(
|
sql`${composeTable.composeId} IN (${sql.join(
|
||||||
sql`${composeTable.composeId} IN (${sql.join(
|
accessedServices.map((id) => sql`${id}`),
|
||||||
accessedServices.map((id) => sql`${id}`),
|
sql`, `,
|
||||||
sql`, `,
|
)})`,
|
||||||
)})`,
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const where = and(...baseConditions);
|
const where = and(...baseConditions);
|
||||||
|
|
||||||
|
|||||||
@@ -5,20 +5,21 @@ import {
|
|||||||
findAllDeploymentsByComposeId,
|
findAllDeploymentsByComposeId,
|
||||||
findAllDeploymentsByServerId,
|
findAllDeploymentsByServerId,
|
||||||
findAllDeploymentsCentralized,
|
findAllDeploymentsCentralized,
|
||||||
findApplicationById,
|
|
||||||
findComposeById,
|
|
||||||
findDeploymentById,
|
findDeploymentById,
|
||||||
findMemberById,
|
|
||||||
findServerById,
|
|
||||||
IS_CLOUD,
|
IS_CLOUD,
|
||||||
removeDeployment,
|
removeDeployment,
|
||||||
resolveServicePath,
|
resolveServicePath,
|
||||||
updateDeploymentStatus,
|
updateDeploymentStatus,
|
||||||
} from "@dokploy/server";
|
} from "@dokploy/server";
|
||||||
import { db } from "@dokploy/server/db";
|
import { db } from "@dokploy/server/db";
|
||||||
|
import {
|
||||||
|
checkServicePermissionAndAccess,
|
||||||
|
findMemberByUserId,
|
||||||
|
} from "@dokploy/server/services/permission";
|
||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
import { desc, eq } from "drizzle-orm";
|
import { desc, eq } from "drizzle-orm";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
import { audit } from "@/server/api/utils/audit";
|
||||||
import {
|
import {
|
||||||
apiFindAllByApplication,
|
apiFindAllByApplication,
|
||||||
apiFindAllByCompose,
|
apiFindAllByCompose,
|
||||||
@@ -29,65 +30,46 @@ import {
|
|||||||
} from "@/server/db/schema";
|
} from "@/server/db/schema";
|
||||||
import { myQueue } from "@/server/queues/queueSetup";
|
import { myQueue } from "@/server/queues/queueSetup";
|
||||||
import { fetchDeployApiJobs, type QueueJobRow } from "@/server/utils/deploy";
|
import { fetchDeployApiJobs, type QueueJobRow } from "@/server/utils/deploy";
|
||||||
import { createTRPCRouter, protectedProcedure } from "../trpc";
|
import { createTRPCRouter, protectedProcedure, withPermission } from "../trpc";
|
||||||
|
|
||||||
export const deploymentRouter = createTRPCRouter({
|
export const deploymentRouter = createTRPCRouter({
|
||||||
all: protectedProcedure
|
all: protectedProcedure
|
||||||
.input(apiFindAllByApplication)
|
.input(apiFindAllByApplication)
|
||||||
.query(async ({ input, ctx }) => {
|
.query(async ({ input, ctx }) => {
|
||||||
const application = await findApplicationById(input.applicationId);
|
await checkServicePermissionAndAccess(ctx, input.applicationId, {
|
||||||
if (
|
deployment: ["read"],
|
||||||
application.environment.project.organizationId !==
|
});
|
||||||
ctx.session.activeOrganizationId
|
|
||||||
) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "UNAUTHORIZED",
|
|
||||||
message: "You are not authorized to access this application",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return await findAllDeploymentsByApplicationId(input.applicationId);
|
return await findAllDeploymentsByApplicationId(input.applicationId);
|
||||||
}),
|
}),
|
||||||
|
|
||||||
allByCompose: protectedProcedure
|
allByCompose: protectedProcedure
|
||||||
.input(apiFindAllByCompose)
|
.input(apiFindAllByCompose)
|
||||||
.query(async ({ input, ctx }) => {
|
.query(async ({ input, ctx }) => {
|
||||||
const compose = await findComposeById(input.composeId);
|
await checkServicePermissionAndAccess(ctx, input.composeId, {
|
||||||
if (
|
deployment: ["read"],
|
||||||
compose.environment.project.organizationId !==
|
});
|
||||||
ctx.session.activeOrganizationId
|
|
||||||
) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "UNAUTHORIZED",
|
|
||||||
message: "You are not authorized to access this compose",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return await findAllDeploymentsByComposeId(input.composeId);
|
return await findAllDeploymentsByComposeId(input.composeId);
|
||||||
}),
|
}),
|
||||||
allByServer: protectedProcedure
|
allByServer: withPermission("deployment", "read")
|
||||||
.input(apiFindAllByServer)
|
.input(apiFindAllByServer)
|
||||||
.query(async ({ input, ctx }) => {
|
.query(async ({ input }) => {
|
||||||
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",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return await findAllDeploymentsByServerId(input.serverId);
|
return await findAllDeploymentsByServerId(input.serverId);
|
||||||
}),
|
}),
|
||||||
allCentralized: protectedProcedure.query(async ({ ctx }) => {
|
allCentralized: withPermission("deployment", "read").query(
|
||||||
const orgId = ctx.session.activeOrganizationId;
|
async ({ ctx }) => {
|
||||||
const accessedServices =
|
const orgId = ctx.session.activeOrganizationId;
|
||||||
ctx.user.role === "member"
|
const accessedServices =
|
||||||
? (await findMemberById(ctx.user.id, orgId)).accessedServices
|
ctx.user.role !== "owner" && ctx.user.role !== "admin"
|
||||||
: null;
|
? (await findMemberByUserId(ctx.user.id, orgId)).accessedServices
|
||||||
if (accessedServices !== null && accessedServices.length === 0) {
|
: null;
|
||||||
return [];
|
if (accessedServices !== null && accessedServices.length === 0) {
|
||||||
}
|
return [];
|
||||||
return findAllDeploymentsCentralized(orgId, accessedServices);
|
}
|
||||||
}),
|
return findAllDeploymentsCentralized(orgId, accessedServices);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
|
||||||
queueList: protectedProcedure.query(async ({ ctx }) => {
|
queueList: withPermission("deployment", "read").query(async ({ ctx }) => {
|
||||||
const orgId = ctx.session.activeOrganizationId;
|
const orgId = ctx.session.activeOrganizationId;
|
||||||
let rows: QueueJobRow[];
|
let rows: QueueJobRow[];
|
||||||
|
|
||||||
@@ -135,7 +117,10 @@ export const deploymentRouter = createTRPCRouter({
|
|||||||
|
|
||||||
allByType: protectedProcedure
|
allByType: protectedProcedure
|
||||||
.input(apiFindAllByType)
|
.input(apiFindAllByType)
|
||||||
.query(async ({ input }) => {
|
.query(async ({ input, ctx }) => {
|
||||||
|
await checkServicePermissionAndAccess(ctx, input.id, {
|
||||||
|
deployment: ["read"],
|
||||||
|
});
|
||||||
const deploymentsList = await db.query.deployments.findMany({
|
const deploymentsList = await db.query.deployments.findMany({
|
||||||
where: eq(deployments[`${input.type}Id`], input.id),
|
where: eq(deployments[`${input.type}Id`], input.id),
|
||||||
orderBy: desc(deployments.createdAt),
|
orderBy: desc(deployments.createdAt),
|
||||||
@@ -151,8 +136,14 @@ export const deploymentRouter = createTRPCRouter({
|
|||||||
deploymentId: z.string().min(1),
|
deploymentId: z.string().min(1),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.mutation(async ({ input }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
const deployment = await findDeploymentById(input.deploymentId);
|
const deployment = await findDeploymentById(input.deploymentId);
|
||||||
|
const serviceId = deployment.applicationId || deployment.composeId;
|
||||||
|
if (serviceId) {
|
||||||
|
await checkServicePermissionAndAccess(ctx, serviceId, {
|
||||||
|
deployment: ["cancel"],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (!deployment.pid) {
|
if (!deployment.pid) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
@@ -169,6 +160,11 @@ export const deploymentRouter = createTRPCRouter({
|
|||||||
}
|
}
|
||||||
|
|
||||||
await updateDeploymentStatus(deployment.deploymentId, "error");
|
await updateDeploymentStatus(deployment.deploymentId, "error");
|
||||||
|
await audit(ctx, {
|
||||||
|
action: "cancel",
|
||||||
|
resourceType: "deployment",
|
||||||
|
resourceId: deployment.deploymentId,
|
||||||
|
});
|
||||||
}),
|
}),
|
||||||
|
|
||||||
removeDeployment: protectedProcedure
|
removeDeployment: protectedProcedure
|
||||||
@@ -177,7 +173,20 @@ export const deploymentRouter = createTRPCRouter({
|
|||||||
deploymentId: z.string().min(1),
|
deploymentId: z.string().min(1),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.mutation(async ({ input }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
return await removeDeployment(input.deploymentId);
|
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 { db } from "@dokploy/server/db";
|
||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
import { desc, eq } from "drizzle-orm";
|
import { desc, eq } from "drizzle-orm";
|
||||||
import {
|
import { createTRPCRouter, withPermission } from "@/server/api/trpc";
|
||||||
adminProcedure,
|
import { audit } from "@/server/api/utils/audit";
|
||||||
createTRPCRouter,
|
|
||||||
protectedProcedure,
|
|
||||||
} from "@/server/api/trpc";
|
|
||||||
import {
|
import {
|
||||||
apiCreateDestination,
|
apiCreateDestination,
|
||||||
apiFindOneDestination,
|
apiFindOneDestination,
|
||||||
@@ -24,14 +21,21 @@ import {
|
|||||||
} from "@/server/db/schema";
|
} from "@/server/db/schema";
|
||||||
|
|
||||||
export const destinationRouter = createTRPCRouter({
|
export const destinationRouter = createTRPCRouter({
|
||||||
create: adminProcedure
|
create: withPermission("destination", "create")
|
||||||
.input(apiCreateDestination)
|
.input(apiCreateDestination)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
try {
|
try {
|
||||||
return await createDestintation(
|
const result = await createDestintation(
|
||||||
input,
|
input,
|
||||||
ctx.session.activeOrganizationId,
|
ctx.session.activeOrganizationId,
|
||||||
);
|
);
|
||||||
|
await audit(ctx, {
|
||||||
|
action: "create",
|
||||||
|
resourceType: "destination",
|
||||||
|
resourceId: result.destinationId,
|
||||||
|
resourceName: input.name,
|
||||||
|
});
|
||||||
|
return result;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "BAD_REQUEST",
|
code: "BAD_REQUEST",
|
||||||
@@ -40,7 +44,7 @@ export const destinationRouter = createTRPCRouter({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
testConnection: adminProcedure
|
testConnection: withPermission("destination", "create")
|
||||||
.input(apiCreateDestination)
|
.input(apiCreateDestination)
|
||||||
.mutation(async ({ input }) => {
|
.mutation(async ({ input }) => {
|
||||||
const { secretAccessKey, bucket, region, endpoint, accessKey, provider } =
|
const { secretAccessKey, bucket, region, endpoint, accessKey, provider } =
|
||||||
@@ -87,7 +91,7 @@ export const destinationRouter = createTRPCRouter({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
one: protectedProcedure
|
one: withPermission("destination", "read")
|
||||||
.input(apiFindOneDestination)
|
.input(apiFindOneDestination)
|
||||||
.query(async ({ input, ctx }) => {
|
.query(async ({ input, ctx }) => {
|
||||||
const destination = await findDestinationById(input.destinationId);
|
const destination = await findDestinationById(input.destinationId);
|
||||||
@@ -99,13 +103,13 @@ export const destinationRouter = createTRPCRouter({
|
|||||||
}
|
}
|
||||||
return destination;
|
return destination;
|
||||||
}),
|
}),
|
||||||
all: protectedProcedure.query(async ({ ctx }) => {
|
all: withPermission("destination", "read").query(async ({ ctx }) => {
|
||||||
return await db.query.destinations.findMany({
|
return await db.query.destinations.findMany({
|
||||||
where: eq(destinations.organizationId, ctx.session.activeOrganizationId),
|
where: eq(destinations.organizationId, ctx.session.activeOrganizationId),
|
||||||
orderBy: [desc(destinations.createdAt)],
|
orderBy: [desc(destinations.createdAt)],
|
||||||
});
|
});
|
||||||
}),
|
}),
|
||||||
remove: adminProcedure
|
remove: withPermission("destination", "delete")
|
||||||
.input(apiRemoveDestination)
|
.input(apiRemoveDestination)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
try {
|
try {
|
||||||
@@ -117,15 +121,22 @@ export const destinationRouter = createTRPCRouter({
|
|||||||
message: "You are not allowed to delete this destination",
|
message: "You are not allowed to delete this destination",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return await removeDestinationById(
|
const result = await removeDestinationById(
|
||||||
input.destinationId,
|
input.destinationId,
|
||||||
ctx.session.activeOrganizationId,
|
ctx.session.activeOrganizationId,
|
||||||
);
|
);
|
||||||
|
await audit(ctx, {
|
||||||
|
action: "delete",
|
||||||
|
resourceType: "destination",
|
||||||
|
resourceId: input.destinationId,
|
||||||
|
resourceName: destination.name,
|
||||||
|
});
|
||||||
|
return result;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
update: adminProcedure
|
update: withPermission("destination", "create")
|
||||||
.input(apiUpdateDestination)
|
.input(apiUpdateDestination)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
try {
|
try {
|
||||||
@@ -136,10 +147,17 @@ export const destinationRouter = createTRPCRouter({
|
|||||||
message: "You are not allowed to update this destination",
|
message: "You are not allowed to update this destination",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return await updateDestinationById(input.destinationId, {
|
const result = await updateDestinationById(input.destinationId, {
|
||||||
...input,
|
...input,
|
||||||
organizationId: ctx.session.activeOrganizationId,
|
organizationId: ctx.session.activeOrganizationId,
|
||||||
});
|
});
|
||||||
|
await audit(ctx, {
|
||||||
|
action: "update",
|
||||||
|
resourceType: "destination",
|
||||||
|
resourceId: input.destinationId,
|
||||||
|
resourceName: input.name,
|
||||||
|
});
|
||||||
|
return result;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,12 +10,13 @@ import {
|
|||||||
} from "@dokploy/server";
|
} from "@dokploy/server";
|
||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
import { z } from "zod";
|
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 containerIdRegex = /^[a-zA-Z0-9.\-_]+$/;
|
||||||
|
|
||||||
export const dockerRouter = createTRPCRouter({
|
export const dockerRouter = createTRPCRouter({
|
||||||
getContainers: protectedProcedure
|
getContainers: withPermission("docker", "read")
|
||||||
.input(
|
.input(
|
||||||
z.object({
|
z.object({
|
||||||
serverId: z.string().optional(),
|
serverId: z.string().optional(),
|
||||||
@@ -31,7 +32,7 @@ export const dockerRouter = createTRPCRouter({
|
|||||||
return await getContainers(input.serverId);
|
return await getContainers(input.serverId);
|
||||||
}),
|
}),
|
||||||
|
|
||||||
restartContainer: protectedProcedure
|
restartContainer: withPermission("docker", "read")
|
||||||
.input(
|
.input(
|
||||||
z.object({
|
z.object({
|
||||||
containerId: z
|
containerId: z
|
||||||
@@ -40,11 +41,18 @@ export const dockerRouter = createTRPCRouter({
|
|||||||
.regex(containerIdRegex, "Invalid container id."),
|
.regex(containerIdRegex, "Invalid container id."),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.mutation(async ({ input }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
return await containerRestart(input.containerId);
|
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(
|
.input(
|
||||||
z.object({
|
z.object({
|
||||||
containerId: z
|
containerId: z
|
||||||
@@ -64,7 +72,7 @@ export const dockerRouter = createTRPCRouter({
|
|||||||
return await getConfig(input.containerId, input.serverId);
|
return await getConfig(input.containerId, input.serverId);
|
||||||
}),
|
}),
|
||||||
|
|
||||||
getContainersByAppNameMatch: protectedProcedure
|
getContainersByAppNameMatch: withPermission("service", "read")
|
||||||
.input(
|
.input(
|
||||||
z.object({
|
z.object({
|
||||||
appType: z.enum(["stack", "docker-compose"]).optional(),
|
appType: z.enum(["stack", "docker-compose"]).optional(),
|
||||||
@@ -86,7 +94,7 @@ export const dockerRouter = createTRPCRouter({
|
|||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
|
|
||||||
getContainersByAppLabel: protectedProcedure
|
getContainersByAppLabel: withPermission("docker", "read")
|
||||||
.input(
|
.input(
|
||||||
z.object({
|
z.object({
|
||||||
appName: z.string().min(1).regex(containerIdRegex, "Invalid app name."),
|
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(
|
.input(
|
||||||
z.object({
|
z.object({
|
||||||
appName: z.string().min(1).regex(containerIdRegex, "Invalid app name."),
|
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);
|
return await getStackContainersByAppName(input.appName, input.serverId);
|
||||||
}),
|
}),
|
||||||
|
|
||||||
getServiceContainersByAppName: protectedProcedure
|
getServiceContainersByAppName: withPermission("docker", "read")
|
||||||
.input(
|
.input(
|
||||||
z.object({
|
z.object({
|
||||||
appName: z.string().min(1).regex(containerIdRegex, "Invalid app name."),
|
appName: z.string().min(1).regex(containerIdRegex, "Invalid app name."),
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import {
|
import {
|
||||||
createDomain,
|
createDomain,
|
||||||
findApplicationById,
|
findApplicationById,
|
||||||
findComposeById,
|
|
||||||
findDomainById,
|
findDomainById,
|
||||||
findDomainsByApplicationId,
|
findDomainsByApplicationId,
|
||||||
findDomainsByComposeId,
|
findDomainsByComposeId,
|
||||||
@@ -15,9 +14,15 @@ import {
|
|||||||
updateDomainById,
|
updateDomainById,
|
||||||
validateDomain,
|
validateDomain,
|
||||||
} from "@dokploy/server";
|
} from "@dokploy/server";
|
||||||
|
import { checkServicePermissionAndAccess } from "@dokploy/server/services/permission";
|
||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
import { z } from "zod";
|
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 {
|
import {
|
||||||
apiCreateDomain,
|
apiCreateDomain,
|
||||||
apiFindCompose,
|
apiFindCompose,
|
||||||
@@ -32,29 +37,22 @@ export const domainRouter = createTRPCRouter({
|
|||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
try {
|
try {
|
||||||
if (input.domainType === "compose" && input.composeId) {
|
if (input.domainType === "compose" && input.composeId) {
|
||||||
const compose = await findComposeById(input.composeId);
|
await checkServicePermissionAndAccess(ctx, input.composeId, {
|
||||||
if (
|
domain: ["create"],
|
||||||
compose.environment.project.organizationId !==
|
});
|
||||||
ctx.session.activeOrganizationId
|
|
||||||
) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "UNAUTHORIZED",
|
|
||||||
message: "You are not authorized to access this compose",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else if (input.domainType === "application" && input.applicationId) {
|
} else if (input.domainType === "application" && input.applicationId) {
|
||||||
const application = await findApplicationById(input.applicationId);
|
await checkServicePermissionAndAccess(ctx, input.applicationId, {
|
||||||
if (
|
domain: ["create"],
|
||||||
application.environment.project.organizationId !==
|
});
|
||||||
ctx.session.activeOrganizationId
|
|
||||||
) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "UNAUTHORIZED",
|
|
||||||
message: "You are not authorized to access this application",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
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) {
|
} catch (error) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "BAD_REQUEST",
|
code: "BAD_REQUEST",
|
||||||
@@ -69,34 +67,20 @@ export const domainRouter = createTRPCRouter({
|
|||||||
byApplicationId: protectedProcedure
|
byApplicationId: protectedProcedure
|
||||||
.input(apiFindOneApplication)
|
.input(apiFindOneApplication)
|
||||||
.query(async ({ input, ctx }) => {
|
.query(async ({ input, ctx }) => {
|
||||||
const application = await findApplicationById(input.applicationId);
|
await checkServicePermissionAndAccess(ctx, input.applicationId, {
|
||||||
if (
|
domain: ["read"],
|
||||||
application.environment.project.organizationId !==
|
});
|
||||||
ctx.session.activeOrganizationId
|
|
||||||
) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "UNAUTHORIZED",
|
|
||||||
message: "You are not authorized to access this application",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return await findDomainsByApplicationId(input.applicationId);
|
return await findDomainsByApplicationId(input.applicationId);
|
||||||
}),
|
}),
|
||||||
byComposeId: protectedProcedure
|
byComposeId: protectedProcedure
|
||||||
.input(apiFindCompose)
|
.input(apiFindCompose)
|
||||||
.query(async ({ input, ctx }) => {
|
.query(async ({ input, ctx }) => {
|
||||||
const compose = await findComposeById(input.composeId);
|
await checkServicePermissionAndAccess(ctx, input.composeId, {
|
||||||
if (
|
domain: ["read"],
|
||||||
compose.environment.project.organizationId !==
|
});
|
||||||
ctx.session.activeOrganizationId
|
|
||||||
) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "UNAUTHORIZED",
|
|
||||||
message: "You are not authorized to access this compose",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return await findDomainsByComposeId(input.composeId);
|
return await findDomainsByComposeId(input.composeId);
|
||||||
}),
|
}),
|
||||||
generateDomain: protectedProcedure
|
generateDomain: withPermission("domain", "create")
|
||||||
.input(z.object({ appName: z.string(), serverId: z.string().optional() }))
|
.input(z.object({ appName: z.string(), serverId: z.string().optional() }))
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
return generateTraefikMeDomain(
|
return generateTraefikMeDomain(
|
||||||
@@ -105,7 +89,7 @@ export const domainRouter = createTRPCRouter({
|
|||||||
input.serverId,
|
input.serverId,
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
canGenerateTraefikMeDomains: protectedProcedure
|
canGenerateTraefikMeDomains: withPermission("domain", "read")
|
||||||
.input(z.object({ serverId: z.string() }))
|
.input(z.object({ serverId: z.string() }))
|
||||||
.query(async ({ input }) => {
|
.query(async ({ input }) => {
|
||||||
if (input.serverId) {
|
if (input.serverId) {
|
||||||
@@ -120,45 +104,28 @@ export const domainRouter = createTRPCRouter({
|
|||||||
.input(apiUpdateDomain)
|
.input(apiUpdateDomain)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
const currentDomain = await findDomainById(input.domainId);
|
const currentDomain = await findDomainById(input.domainId);
|
||||||
|
const serviceId = currentDomain.applicationId || currentDomain.composeId;
|
||||||
if (currentDomain.applicationId) {
|
if (serviceId) {
|
||||||
const newApp = await findApplicationById(currentDomain.applicationId);
|
await checkServicePermissionAndAccess(ctx, serviceId, {
|
||||||
if (
|
domain: ["create"],
|
||||||
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",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else if (currentDomain.previewDeploymentId) {
|
} else if (currentDomain.previewDeploymentId) {
|
||||||
const newPreviewDeployment = await findPreviewDeploymentById(
|
const preview = await findPreviewDeploymentById(
|
||||||
currentDomain.previewDeploymentId,
|
currentDomain.previewDeploymentId,
|
||||||
);
|
);
|
||||||
if (
|
await checkServicePermissionAndAccess(ctx, preview.applicationId, {
|
||||||
newPreviewDeployment.application.environment.project
|
domain: ["create"],
|
||||||
.organizationId !== ctx.session.activeOrganizationId
|
});
|
||||||
) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "UNAUTHORIZED",
|
|
||||||
message: "You are not authorized to access this preview deployment",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await updateDomainById(input.domainId, input);
|
const result = await updateDomainById(input.domainId, input);
|
||||||
const domain = await findDomainById(input.domainId);
|
const domain = await findDomainById(input.domainId);
|
||||||
|
await audit(ctx, {
|
||||||
|
action: "update",
|
||||||
|
resourceType: "domain",
|
||||||
|
resourceId: domain.domainId,
|
||||||
|
resourceName: domain.host,
|
||||||
|
});
|
||||||
if (domain.applicationId) {
|
if (domain.applicationId) {
|
||||||
const application = await findApplicationById(domain.applicationId);
|
const application = await findApplicationById(domain.applicationId);
|
||||||
await manageDomain(application, domain);
|
await manageDomain(application, domain);
|
||||||
@@ -176,59 +143,46 @@ export const domainRouter = createTRPCRouter({
|
|||||||
}),
|
}),
|
||||||
one: protectedProcedure.input(apiFindDomain).query(async ({ input, ctx }) => {
|
one: protectedProcedure.input(apiFindDomain).query(async ({ input, ctx }) => {
|
||||||
const domain = await findDomainById(input.domainId);
|
const domain = await findDomainById(input.domainId);
|
||||||
if (domain.applicationId) {
|
const serviceId = domain.applicationId || domain.composeId;
|
||||||
const application = await findApplicationById(domain.applicationId);
|
if (serviceId) {
|
||||||
if (
|
await checkServicePermissionAndAccess(ctx, serviceId, {
|
||||||
application.environment.project.organizationId !==
|
domain: ["read"],
|
||||||
ctx.session.activeOrganizationId
|
});
|
||||||
) {
|
} else if (domain.previewDeploymentId) {
|
||||||
throw new TRPCError({
|
const preview = await findPreviewDeploymentById(
|
||||||
code: "UNAUTHORIZED",
|
domain.previewDeploymentId,
|
||||||
message: "You are not authorized to access this application",
|
);
|
||||||
});
|
await checkServicePermissionAndAccess(ctx, preview.applicationId, {
|
||||||
}
|
domain: ["read"],
|
||||||
} 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",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return await findDomainById(input.domainId);
|
return domain;
|
||||||
}),
|
}),
|
||||||
delete: protectedProcedure
|
delete: protectedProcedure
|
||||||
.input(apiFindDomain)
|
.input(apiFindDomain)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
const domain = await findDomainById(input.domainId);
|
const domain = await findDomainById(input.domainId);
|
||||||
if (domain.applicationId) {
|
const serviceId = domain.applicationId || domain.composeId;
|
||||||
const application = await findApplicationById(domain.applicationId);
|
if (serviceId) {
|
||||||
if (
|
await checkServicePermissionAndAccess(ctx, serviceId, {
|
||||||
application.environment.project.organizationId !==
|
domain: ["delete"],
|
||||||
ctx.session.activeOrganizationId
|
});
|
||||||
) {
|
} else if (domain.previewDeploymentId) {
|
||||||
throw new TRPCError({
|
const preview = await findPreviewDeploymentById(
|
||||||
code: "UNAUTHORIZED",
|
domain.previewDeploymentId,
|
||||||
message: "You are not authorized to access this application",
|
);
|
||||||
});
|
await checkServicePermissionAndAccess(ctx, preview.applicationId, {
|
||||||
}
|
domain: ["delete"],
|
||||||
} 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 result = await removeDomainById(input.domainId);
|
const result = await removeDomainById(input.domainId);
|
||||||
|
await audit(ctx, {
|
||||||
|
action: "delete",
|
||||||
|
resourceType: "domain",
|
||||||
|
resourceId: domain.domainId,
|
||||||
|
resourceName: domain.host,
|
||||||
|
});
|
||||||
|
|
||||||
if (domain.applicationId) {
|
if (domain.applicationId) {
|
||||||
const application = await findApplicationById(domain.applicationId);
|
const application = await findApplicationById(domain.applicationId);
|
||||||
@@ -238,7 +192,7 @@ export const domainRouter = createTRPCRouter({
|
|||||||
return result;
|
return result;
|
||||||
}),
|
}),
|
||||||
|
|
||||||
validateDomain: protectedProcedure
|
validateDomain: withPermission("domain", "read")
|
||||||
.input(
|
.input(
|
||||||
z.object({
|
z.object({
|
||||||
domain: z.string(),
|
domain: z.string(),
|
||||||
|
|||||||
@@ -1,31 +1,35 @@
|
|||||||
import {
|
import {
|
||||||
addNewEnvironment,
|
|
||||||
checkEnvironmentAccess,
|
|
||||||
checkEnvironmentCreationPermission,
|
|
||||||
checkEnvironmentDeletionPermission,
|
|
||||||
createEnvironment,
|
createEnvironment,
|
||||||
deleteEnvironment,
|
deleteEnvironment,
|
||||||
duplicateEnvironment,
|
duplicateEnvironment,
|
||||||
findEnvironmentById,
|
findEnvironmentById,
|
||||||
findEnvironmentsByProjectId,
|
findEnvironmentsByProjectId,
|
||||||
findMemberById,
|
|
||||||
updateEnvironmentById,
|
updateEnvironmentById,
|
||||||
} from "@dokploy/server";
|
} from "@dokploy/server";
|
||||||
import { db } from "@dokploy/server/db";
|
import { db } from "@dokploy/server/db";
|
||||||
|
import {
|
||||||
|
addNewEnvironment,
|
||||||
|
checkEnvironmentAccess,
|
||||||
|
checkEnvironmentCreationPermission,
|
||||||
|
checkEnvironmentDeletionPermission,
|
||||||
|
checkPermission,
|
||||||
|
findMemberByUserId,
|
||||||
|
} from "@dokploy/server/services/permission";
|
||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
import { and, desc, eq, ilike, or, sql } from "drizzle-orm";
|
import { and, desc, eq, ilike, or, sql } from "drizzle-orm";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc";
|
import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc";
|
||||||
|
import { audit } from "@/server/api/utils/audit";
|
||||||
import {
|
import {
|
||||||
apiCreateEnvironment,
|
apiCreateEnvironment,
|
||||||
apiDuplicateEnvironment,
|
apiDuplicateEnvironment,
|
||||||
apiFindOneEnvironment,
|
apiFindOneEnvironment,
|
||||||
apiRemoveEnvironment,
|
apiRemoveEnvironment,
|
||||||
apiUpdateEnvironment,
|
apiUpdateEnvironment,
|
||||||
|
environments,
|
||||||
|
projects,
|
||||||
} from "@/server/db/schema";
|
} 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 = (
|
const filterEnvironmentServices = (
|
||||||
environment: any,
|
environment: any,
|
||||||
accessedServices: string[],
|
accessedServices: string[],
|
||||||
@@ -59,12 +63,7 @@ export const environmentRouter = createTRPCRouter({
|
|||||||
.input(apiCreateEnvironment)
|
.input(apiCreateEnvironment)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
try {
|
try {
|
||||||
// Check if user has permission to create environments
|
await checkEnvironmentCreationPermission(ctx, input.projectId);
|
||||||
await checkEnvironmentCreationPermission(
|
|
||||||
ctx.user.id,
|
|
||||||
input.projectId,
|
|
||||||
ctx.session.activeOrganizationId,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (input.name === "production") {
|
if (input.name === "production") {
|
||||||
throw new TRPCError({
|
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);
|
const environment = await createEnvironment(input);
|
||||||
|
|
||||||
if (ctx.user.role === "member") {
|
await addNewEnvironment(ctx, environment.environmentId);
|
||||||
await addNewEnvironment(
|
await audit(ctx, {
|
||||||
ctx.user.id,
|
action: "create",
|
||||||
environment.environmentId,
|
resourceType: "environment",
|
||||||
ctx.session.activeOrganizationId,
|
resourceId: environment.environmentId,
|
||||||
);
|
resourceName: environment.name,
|
||||||
}
|
});
|
||||||
return environment;
|
return environment;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof TRPCError) {
|
if (error instanceof TRPCError) {
|
||||||
@@ -100,54 +98,39 @@ export const environmentRouter = createTRPCRouter({
|
|||||||
one: protectedProcedure
|
one: protectedProcedure
|
||||||
.input(apiFindOneEnvironment)
|
.input(apiFindOneEnvironment)
|
||||||
.query(async ({ input, ctx }) => {
|
.query(async ({ input, ctx }) => {
|
||||||
try {
|
const environment = await findEnvironmentById(input.environmentId);
|
||||||
if (ctx.user.role === "member") {
|
if (
|
||||||
await checkEnvironmentAccess(
|
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,
|
ctx.user.id,
|
||||||
input.environmentId,
|
|
||||||
ctx.session.activeOrganizationId,
|
ctx.session.activeOrganizationId,
|
||||||
"access",
|
|
||||||
);
|
);
|
||||||
}
|
|
||||||
const environment = await findEnvironmentById(input.environmentId);
|
if (!accessedEnvironments.includes(environment.environmentId)) {
|
||||||
if (
|
|
||||||
environment.project.organizationId !==
|
|
||||||
ctx.session.activeOrganizationId
|
|
||||||
) {
|
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "FORBIDDEN",
|
code: "FORBIDDEN",
|
||||||
message: "You are not allowed to access this environment",
|
message: "You are not allowed to access this environment",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check environment access and filter services for members
|
const filteredEnvironment = filterEnvironmentServices(
|
||||||
if (ctx.user.role === "member") {
|
environment,
|
||||||
const { accessedEnvironments, accessedServices } =
|
accessedServices,
|
||||||
await findMemberById(ctx.user.id, ctx.session.activeOrganizationId);
|
);
|
||||||
|
|
||||||
if (!accessedEnvironments.includes(environment.environmentId)) {
|
return filteredEnvironment;
|
||||||
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 environment;
|
||||||
}),
|
}),
|
||||||
|
|
||||||
byProjectId: protectedProcedure
|
byProjectId: protectedProcedure
|
||||||
@@ -156,7 +139,6 @@ export const environmentRouter = createTRPCRouter({
|
|||||||
try {
|
try {
|
||||||
const environments = await findEnvironmentsByProjectId(input.projectId);
|
const environments = await findEnvironmentsByProjectId(input.projectId);
|
||||||
|
|
||||||
// Check organization access
|
|
||||||
if (
|
if (
|
||||||
environments.some(
|
environments.some(
|
||||||
(environment) =>
|
(environment) =>
|
||||||
@@ -170,12 +152,13 @@ export const environmentRouter = createTRPCRouter({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Filter environments for members based on their permissions
|
if (ctx.user.role !== "owner" && ctx.user.role !== "admin") {
|
||||||
if (ctx.user.role === "member") {
|
|
||||||
const { accessedEnvironments, accessedServices } =
|
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
|
const filteredEnvironments = environments
|
||||||
.filter((environment) =>
|
.filter((environment) =>
|
||||||
accessedEnvironments.includes(environment.environmentId),
|
accessedEnvironments.includes(environment.environmentId),
|
||||||
@@ -211,7 +194,6 @@ export const environmentRouter = createTRPCRouter({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prevent deletion of the default environment
|
|
||||||
if (environment.isDefault) {
|
if (environment.isDefault) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "BAD_REQUEST",
|
code: "BAD_REQUEST",
|
||||||
@@ -219,24 +201,17 @@ export const environmentRouter = createTRPCRouter({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check environment deletion permission
|
await checkEnvironmentDeletionPermission(ctx, environment.projectId);
|
||||||
await checkEnvironmentDeletionPermission(
|
|
||||||
ctx.user.id,
|
|
||||||
environment.projectId,
|
|
||||||
ctx.session.activeOrganizationId,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Additional check for environment access for members
|
await checkEnvironmentAccess(ctx, input.environmentId, "read");
|
||||||
if (ctx.user.role === "member") {
|
|
||||||
await checkEnvironmentAccess(
|
|
||||||
ctx.user.id,
|
|
||||||
input.environmentId,
|
|
||||||
ctx.session.activeOrganizationId,
|
|
||||||
"access",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const deletedEnvironment = await deleteEnvironment(input.environmentId);
|
const deletedEnvironment = await deleteEnvironment(input.environmentId);
|
||||||
|
await audit(ctx, {
|
||||||
|
action: "delete",
|
||||||
|
resourceType: "environment",
|
||||||
|
resourceId: deletedEnvironment?.environmentId,
|
||||||
|
resourceName: deletedEnvironment?.name,
|
||||||
|
});
|
||||||
return deletedEnvironment;
|
return deletedEnvironment;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof TRPCError) {
|
if (error instanceof TRPCError) {
|
||||||
@@ -256,18 +231,14 @@ export const environmentRouter = createTRPCRouter({
|
|||||||
try {
|
try {
|
||||||
const { environmentId, ...updateData } = input;
|
const { environmentId, ...updateData } = input;
|
||||||
|
|
||||||
// Allow users to rename environments to any name, including "production"
|
await checkEnvironmentAccess(ctx, environmentId, "read");
|
||||||
if (ctx.user.role === "member") {
|
|
||||||
await checkEnvironmentAccess(
|
if (updateData.env !== undefined) {
|
||||||
ctx.user.id,
|
await checkPermission(ctx, { environmentEnvVars: ["write"] });
|
||||||
environmentId,
|
|
||||||
ctx.session.activeOrganizationId,
|
|
||||||
"access",
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const currentEnvironment = await findEnvironmentById(environmentId);
|
const currentEnvironment = await findEnvironmentById(environmentId);
|
||||||
|
|
||||||
// Prevent renaming the default environment, but allow updating env and description
|
|
||||||
if (currentEnvironment.isDefault && updateData.name !== undefined) {
|
if (currentEnvironment.isDefault && updateData.name !== undefined) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "BAD_REQUEST",
|
code: "BAD_REQUEST",
|
||||||
@@ -284,9 +255,8 @@ export const environmentRouter = createTRPCRouter({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check environment access for members
|
if (ctx.user.role !== "owner" && ctx.user.role !== "admin") {
|
||||||
if (ctx.user.role === "member") {
|
const { accessedEnvironments } = await findMemberByUserId(
|
||||||
const { accessedEnvironments } = await findMemberById(
|
|
||||||
ctx.user.id,
|
ctx.user.id,
|
||||||
ctx.session.activeOrganizationId,
|
ctx.session.activeOrganizationId,
|
||||||
);
|
);
|
||||||
@@ -305,6 +275,14 @@ export const environmentRouter = createTRPCRouter({
|
|||||||
environmentId,
|
environmentId,
|
||||||
updateData,
|
updateData,
|
||||||
);
|
);
|
||||||
|
if (environment) {
|
||||||
|
await audit(ctx, {
|
||||||
|
action: "update",
|
||||||
|
resourceType: "environment",
|
||||||
|
resourceId: environment.environmentId,
|
||||||
|
resourceName: environment.name,
|
||||||
|
});
|
||||||
|
}
|
||||||
return environment;
|
return environment;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
@@ -318,14 +296,7 @@ export const environmentRouter = createTRPCRouter({
|
|||||||
.input(apiDuplicateEnvironment)
|
.input(apiDuplicateEnvironment)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
try {
|
try {
|
||||||
if (ctx.user.role === "member") {
|
await checkEnvironmentAccess(ctx, input.environmentId, "read");
|
||||||
await checkEnvironmentAccess(
|
|
||||||
ctx.user.id,
|
|
||||||
input.environmentId,
|
|
||||||
ctx.session.activeOrganizationId,
|
|
||||||
"access",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
const environment = await findEnvironmentById(input.environmentId);
|
const environment = await findEnvironmentById(input.environmentId);
|
||||||
if (
|
if (
|
||||||
environment.project.organizationId !==
|
environment.project.organizationId !==
|
||||||
@@ -337,9 +308,8 @@ export const environmentRouter = createTRPCRouter({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check environment access for members
|
if (ctx.user.role !== "owner" && ctx.user.role !== "admin") {
|
||||||
if (ctx.user.role === "member") {
|
const { accessedEnvironments } = await findMemberByUserId(
|
||||||
const { accessedEnvironments } = await findMemberById(
|
|
||||||
ctx.user.id,
|
ctx.user.id,
|
||||||
ctx.session.activeOrganizationId,
|
ctx.session.activeOrganizationId,
|
||||||
);
|
);
|
||||||
@@ -353,6 +323,13 @@ export const environmentRouter = createTRPCRouter({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const duplicatedEnvironment = await duplicateEnvironment(input);
|
const duplicatedEnvironment = await duplicateEnvironment(input);
|
||||||
|
await audit(ctx, {
|
||||||
|
action: "create",
|
||||||
|
resourceType: "environment",
|
||||||
|
resourceId: duplicatedEnvironment.environmentId,
|
||||||
|
resourceName: duplicatedEnvironment.name,
|
||||||
|
metadata: { duplicatedFrom: input.environmentId },
|
||||||
|
});
|
||||||
return duplicatedEnvironment;
|
return duplicatedEnvironment;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
@@ -404,8 +381,8 @@ export const environmentRouter = createTRPCRouter({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ctx.user.role === "member") {
|
if (ctx.user.role !== "owner" && ctx.user.role !== "admin") {
|
||||||
const { accessedEnvironments } = await findMemberById(
|
const { accessedEnvironments } = await findMemberByUserId(
|
||||||
ctx.user.id,
|
ctx.user.id,
|
||||||
ctx.session.activeOrganizationId,
|
ctx.session.activeOrganizationId,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -2,7 +2,12 @@ import { findGitProviderById, removeGitProvider } from "@dokploy/server";
|
|||||||
import { db } from "@dokploy/server/db";
|
import { db } from "@dokploy/server/db";
|
||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
import { and, desc, eq } from "drizzle-orm";
|
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";
|
import { apiRemoveGitProvider, gitProvider } from "@/server/db/schema";
|
||||||
|
|
||||||
export const gitProviderRouter = createTRPCRouter({
|
export const gitProviderRouter = createTRPCRouter({
|
||||||
@@ -21,7 +26,7 @@ export const gitProviderRouter = createTRPCRouter({
|
|||||||
),
|
),
|
||||||
});
|
});
|
||||||
}),
|
}),
|
||||||
remove: protectedProcedure
|
remove: withPermission("gitProviders", "delete")
|
||||||
.input(apiRemoveGitProvider)
|
.input(apiRemoveGitProvider)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
try {
|
try {
|
||||||
@@ -33,6 +38,12 @@ export const gitProviderRouter = createTRPCRouter({
|
|||||||
message: "You are not allowed to delete this Git provider",
|
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);
|
return await removeGitProvider(input.gitProviderId);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const message =
|
const message =
|
||||||
|
|||||||
@@ -10,7 +10,12 @@ import {
|
|||||||
} from "@dokploy/server";
|
} from "@dokploy/server";
|
||||||
import { db } from "@dokploy/server/db";
|
import { db } from "@dokploy/server/db";
|
||||||
import { TRPCError } from "@trpc/server";
|
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 {
|
import {
|
||||||
apiCreateGitea,
|
apiCreateGitea,
|
||||||
apiFindGiteaBranches,
|
apiFindGiteaBranches,
|
||||||
@@ -20,15 +25,24 @@ import {
|
|||||||
} from "@/server/db/schema";
|
} from "@/server/db/schema";
|
||||||
|
|
||||||
export const giteaRouter = createTRPCRouter({
|
export const giteaRouter = createTRPCRouter({
|
||||||
create: protectedProcedure
|
create: withPermission("gitProviders", "create")
|
||||||
.input(apiCreateGitea)
|
.input(apiCreateGitea)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
try {
|
try {
|
||||||
return await createGitea(
|
const result = await createGitea(
|
||||||
input,
|
input,
|
||||||
ctx.session.activeOrganizationId,
|
ctx.session.activeOrganizationId,
|
||||||
ctx.session.userId,
|
ctx.session.userId,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
await audit(ctx, {
|
||||||
|
action: "create",
|
||||||
|
resourceType: "gitProvider",
|
||||||
|
resourceId: result.giteaId,
|
||||||
|
resourceName: input.name,
|
||||||
|
});
|
||||||
|
|
||||||
|
return result;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "BAD_REQUEST",
|
code: "BAD_REQUEST",
|
||||||
@@ -38,24 +52,11 @@ export const giteaRouter = createTRPCRouter({
|
|||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
|
||||||
one: protectedProcedure
|
one: protectedProcedure.input(apiFindOneGitea).query(async ({ input }) => {
|
||||||
.input(apiFindOneGitea)
|
return await findGiteaById(input.giteaId);
|
||||||
.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;
|
|
||||||
}),
|
|
||||||
|
|
||||||
giteaProviders: protectedProcedure.query(async ({ ctx }: { ctx: any }) => {
|
giteaProviders: protectedProcedure.query(async ({ ctx }) => {
|
||||||
let result = await db.query.gitea.findMany({
|
let result = await db.query.gitea.findMany({
|
||||||
with: {
|
with: {
|
||||||
gitProvider: true,
|
gitProvider: true,
|
||||||
@@ -85,7 +86,7 @@ export const giteaRouter = createTRPCRouter({
|
|||||||
|
|
||||||
getGiteaRepositories: protectedProcedure
|
getGiteaRepositories: protectedProcedure
|
||||||
.input(apiFindOneGitea)
|
.input(apiFindOneGitea)
|
||||||
.query(async ({ input, ctx }) => {
|
.query(async ({ input }) => {
|
||||||
const { giteaId } = input;
|
const { giteaId } = input;
|
||||||
|
|
||||||
if (!giteaId) {
|
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 {
|
try {
|
||||||
const repositories = await getGiteaRepositories(giteaId);
|
const repositories = await getGiteaRepositories(giteaId);
|
||||||
return repositories;
|
return repositories;
|
||||||
@@ -121,7 +110,7 @@ export const giteaRouter = createTRPCRouter({
|
|||||||
|
|
||||||
getGiteaBranches: protectedProcedure
|
getGiteaBranches: protectedProcedure
|
||||||
.input(apiFindGiteaBranches)
|
.input(apiFindGiteaBranches)
|
||||||
.query(async ({ input, ctx }) => {
|
.query(async ({ input }) => {
|
||||||
const { giteaId, owner, repositoryName } = input;
|
const { giteaId, owner, repositoryName } = input;
|
||||||
|
|
||||||
if (!giteaId || !owner || !repositoryName) {
|
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 {
|
try {
|
||||||
return await getGiteaBranches({
|
return await getGiteaBranches({
|
||||||
giteaId,
|
giteaId,
|
||||||
@@ -161,22 +138,10 @@ export const giteaRouter = createTRPCRouter({
|
|||||||
|
|
||||||
testConnection: protectedProcedure
|
testConnection: protectedProcedure
|
||||||
.input(apiGiteaTestConnection)
|
.input(apiGiteaTestConnection)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input }) => {
|
||||||
const giteaId = input.giteaId ?? "";
|
const giteaId = input.giteaId ?? "";
|
||||||
|
|
||||||
try {
|
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({
|
const result = await testGiteaConnection({
|
||||||
giteaId,
|
giteaId,
|
||||||
});
|
});
|
||||||
@@ -191,21 +156,9 @@ export const giteaRouter = createTRPCRouter({
|
|||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
|
||||||
update: protectedProcedure
|
update: withPermission("gitProviders", "create")
|
||||||
.input(apiUpdateGitea)
|
.input(apiUpdateGitea)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.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) {
|
if (input.name) {
|
||||||
await updateGitProvider(input.gitProviderId, {
|
await updateGitProvider(input.gitProviderId, {
|
||||||
name: input.name,
|
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 };
|
return { success: true };
|
||||||
}),
|
}),
|
||||||
|
|
||||||
getGiteaUrl: protectedProcedure
|
getGiteaUrl: protectedProcedure
|
||||||
.input(apiFindOneGitea)
|
.input(apiFindOneGitea)
|
||||||
.query(async ({ input, ctx }) => {
|
.query(async ({ input }) => {
|
||||||
const { giteaId } = input;
|
const { giteaId } = input;
|
||||||
|
|
||||||
if (!giteaId) {
|
if (!giteaId) {
|
||||||
@@ -237,16 +197,6 @@ export const giteaRouter = createTRPCRouter({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const giteaProvider = await findGiteaById(giteaId);
|
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 the base URL of the Gitea instance
|
||||||
return giteaProvider.giteaUrl;
|
return giteaProvider.giteaUrl;
|
||||||
|
|||||||
@@ -8,7 +8,12 @@ import {
|
|||||||
} from "@dokploy/server";
|
} from "@dokploy/server";
|
||||||
import { db } from "@dokploy/server/db";
|
import { db } from "@dokploy/server/db";
|
||||||
import { TRPCError } from "@trpc/server";
|
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 {
|
import {
|
||||||
apiFindGithubBranches,
|
apiFindGithubBranches,
|
||||||
apiFindOneGithub,
|
apiFindOneGithub,
|
||||||
@@ -16,53 +21,17 @@ import {
|
|||||||
} from "@/server/db/schema";
|
} from "@/server/db/schema";
|
||||||
|
|
||||||
export const githubRouter = createTRPCRouter({
|
export const githubRouter = createTRPCRouter({
|
||||||
one: protectedProcedure
|
one: protectedProcedure.input(apiFindOneGithub).query(async ({ input }) => {
|
||||||
.input(apiFindOneGithub)
|
return await findGithubById(input.githubId);
|
||||||
.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;
|
|
||||||
}),
|
|
||||||
getGithubRepositories: protectedProcedure
|
getGithubRepositories: protectedProcedure
|
||||||
.input(apiFindOneGithub)
|
.input(apiFindOneGithub)
|
||||||
.query(async ({ input, ctx }) => {
|
.query(async ({ input }) => {
|
||||||
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 await getGithubRepositories(input.githubId);
|
return await getGithubRepositories(input.githubId);
|
||||||
}),
|
}),
|
||||||
getGithubBranches: protectedProcedure
|
getGithubBranches: protectedProcedure
|
||||||
.input(apiFindGithubBranches)
|
.input(apiFindGithubBranches)
|
||||||
.query(async ({ input, ctx }) => {
|
.query(async ({ input }) => {
|
||||||
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",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return await getGithubBranches(input);
|
return await getGithubBranches(input);
|
||||||
}),
|
}),
|
||||||
githubProviders: protectedProcedure.query(async ({ ctx }) => {
|
githubProviders: protectedProcedure.query(async ({ ctx }) => {
|
||||||
@@ -95,19 +64,8 @@ export const githubRouter = createTRPCRouter({
|
|||||||
|
|
||||||
testConnection: protectedProcedure
|
testConnection: protectedProcedure
|
||||||
.input(apiFindOneGithub)
|
.input(apiFindOneGithub)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input }) => {
|
||||||
try {
|
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);
|
const result = await getGithubRepositories(input.githubId);
|
||||||
return `Found ${result.length} repositories`;
|
return `Found ${result.length} repositories`;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -117,20 +75,9 @@ export const githubRouter = createTRPCRouter({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
update: protectedProcedure
|
update: withPermission("gitProviders", "create")
|
||||||
.input(apiUpdateGithub)
|
.input(apiUpdateGithub)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.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, {
|
await updateGitProvider(input.gitProviderId, {
|
||||||
name: input.name,
|
name: input.name,
|
||||||
organizationId: ctx.session.activeOrganizationId,
|
organizationId: ctx.session.activeOrganizationId,
|
||||||
@@ -139,5 +86,12 @@ export const githubRouter = createTRPCRouter({
|
|||||||
await updateGithub(input.githubId, {
|
await updateGithub(input.githubId, {
|
||||||
...input,
|
...input,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await audit(ctx, {
|
||||||
|
action: "update",
|
||||||
|
resourceType: "gitProvider",
|
||||||
|
resourceId: input.gitProviderId,
|
||||||
|
resourceName: input.name,
|
||||||
|
});
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -10,7 +10,12 @@ import {
|
|||||||
} from "@dokploy/server";
|
} from "@dokploy/server";
|
||||||
import { db } from "@dokploy/server/db";
|
import { db } from "@dokploy/server/db";
|
||||||
import { TRPCError } from "@trpc/server";
|
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 {
|
import {
|
||||||
apiCreateGitlab,
|
apiCreateGitlab,
|
||||||
apiFindGitlabBranches,
|
apiFindGitlabBranches,
|
||||||
@@ -20,15 +25,23 @@ import {
|
|||||||
} from "@/server/db/schema";
|
} from "@/server/db/schema";
|
||||||
|
|
||||||
export const gitlabRouter = createTRPCRouter({
|
export const gitlabRouter = createTRPCRouter({
|
||||||
create: protectedProcedure
|
create: withPermission("gitProviders", "create")
|
||||||
.input(apiCreateGitlab)
|
.input(apiCreateGitlab)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
try {
|
try {
|
||||||
return await createGitlab(
|
const result = await createGitlab(
|
||||||
input,
|
input,
|
||||||
ctx.session.activeOrganizationId,
|
ctx.session.activeOrganizationId,
|
||||||
ctx.session.userId,
|
ctx.session.userId,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
await audit(ctx, {
|
||||||
|
action: "create",
|
||||||
|
resourceType: "gitProvider",
|
||||||
|
resourceName: input.name,
|
||||||
|
});
|
||||||
|
|
||||||
|
return result;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "BAD_REQUEST",
|
code: "BAD_REQUEST",
|
||||||
@@ -37,22 +50,9 @@ export const gitlabRouter = createTRPCRouter({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
one: protectedProcedure
|
one: protectedProcedure.input(apiFindOneGitlab).query(async ({ input }) => {
|
||||||
.input(apiFindOneGitlab)
|
return await findGitlabById(input.gitlabId);
|
||||||
.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;
|
|
||||||
}),
|
|
||||||
gitlabProviders: protectedProcedure.query(async ({ ctx }) => {
|
gitlabProviders: protectedProcedure.query(async ({ ctx }) => {
|
||||||
let result = await db.query.gitlab.findMany({
|
let result = await db.query.gitlab.findMany({
|
||||||
with: {
|
with: {
|
||||||
@@ -83,52 +83,19 @@ export const gitlabRouter = createTRPCRouter({
|
|||||||
}),
|
}),
|
||||||
getGitlabRepositories: protectedProcedure
|
getGitlabRepositories: protectedProcedure
|
||||||
.input(apiFindOneGitlab)
|
.input(apiFindOneGitlab)
|
||||||
.query(async ({ input, ctx }) => {
|
.query(async ({ input }) => {
|
||||||
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 await getGitlabRepositories(input.gitlabId);
|
return await getGitlabRepositories(input.gitlabId);
|
||||||
}),
|
}),
|
||||||
|
|
||||||
getGitlabBranches: protectedProcedure
|
getGitlabBranches: protectedProcedure
|
||||||
.input(apiFindGitlabBranches)
|
.input(apiFindGitlabBranches)
|
||||||
.query(async ({ input, ctx }) => {
|
.query(async ({ input }) => {
|
||||||
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 await getGitlabBranches(input);
|
return await getGitlabBranches(input);
|
||||||
}),
|
}),
|
||||||
testConnection: protectedProcedure
|
testConnection: protectedProcedure
|
||||||
.input(apiGitlabTestConnection)
|
.input(apiGitlabTestConnection)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input }) => {
|
||||||
try {
|
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);
|
const result = await testGitlabConnection(input);
|
||||||
|
|
||||||
return `Found ${result} repositories`;
|
return `Found ${result} repositories`;
|
||||||
@@ -139,20 +106,9 @@ export const gitlabRouter = createTRPCRouter({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
update: protectedProcedure
|
update: withPermission("gitProviders", "create")
|
||||||
.input(apiUpdateGitlab)
|
.input(apiUpdateGitlab)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.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) {
|
if (input.name) {
|
||||||
await updateGitProvider(input.gitProviderId, {
|
await updateGitProvider(input.gitProviderId, {
|
||||||
name: input.name,
|
name: input.name,
|
||||||
@@ -167,5 +123,12 @@ export const gitlabRouter = createTRPCRouter({
|
|||||||
...input,
|
...input,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await audit(ctx, {
|
||||||
|
action: "update",
|
||||||
|
resourceType: "gitProvider",
|
||||||
|
resourceId: input.gitProviderId,
|
||||||
|
resourceName: input.name,
|
||||||
|
});
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,14 +1,11 @@
|
|||||||
import {
|
import {
|
||||||
addNewService,
|
|
||||||
checkPortInUse,
|
checkPortInUse,
|
||||||
checkServiceAccess,
|
|
||||||
createMariadb,
|
createMariadb,
|
||||||
createMount,
|
createMount,
|
||||||
deployMariadb,
|
deployMariadb,
|
||||||
findBackupsByDbId,
|
findBackupsByDbId,
|
||||||
findEnvironmentById,
|
findEnvironmentById,
|
||||||
findMariadbById,
|
findMariadbById,
|
||||||
findMemberById,
|
|
||||||
findProjectById,
|
findProjectById,
|
||||||
IS_CLOUD,
|
IS_CLOUD,
|
||||||
rebuildDatabase,
|
rebuildDatabase,
|
||||||
@@ -21,11 +18,18 @@ import {
|
|||||||
updateMariadbById,
|
updateMariadbById,
|
||||||
} from "@dokploy/server";
|
} from "@dokploy/server";
|
||||||
import { db } from "@dokploy/server/db";
|
import { db } from "@dokploy/server/db";
|
||||||
|
import {
|
||||||
|
addNewService,
|
||||||
|
checkServiceAccess,
|
||||||
|
checkServicePermissionAndAccess,
|
||||||
|
findMemberByUserId,
|
||||||
|
} from "@dokploy/server/services/permission";
|
||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
import { observable } from "@trpc/server/observable";
|
import { observable } from "@trpc/server/observable";
|
||||||
import { and, desc, eq, ilike, or, sql } from "drizzle-orm";
|
import { and, desc, eq, ilike, or, sql } from "drizzle-orm";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc";
|
import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc";
|
||||||
|
import { audit } from "@/server/api/utils/audit";
|
||||||
import {
|
import {
|
||||||
apiChangeMariaDBStatus,
|
apiChangeMariaDBStatus,
|
||||||
apiCreateMariaDB,
|
apiCreateMariaDB,
|
||||||
@@ -36,27 +40,20 @@ import {
|
|||||||
apiSaveEnvironmentVariablesMariaDB,
|
apiSaveEnvironmentVariablesMariaDB,
|
||||||
apiSaveExternalPortMariaDB,
|
apiSaveExternalPortMariaDB,
|
||||||
apiUpdateMariaDB,
|
apiUpdateMariaDB,
|
||||||
|
environments,
|
||||||
mariadb as mariadbTable,
|
mariadb as mariadbTable,
|
||||||
|
projects,
|
||||||
} from "@/server/db/schema";
|
} from "@/server/db/schema";
|
||||||
import { environments, projects } from "@/server/db/schema";
|
|
||||||
import { cancelJobs } from "@/server/utils/backup";
|
import { cancelJobs } from "@/server/utils/backup";
|
||||||
export const mariadbRouter = createTRPCRouter({
|
export const mariadbRouter = createTRPCRouter({
|
||||||
create: protectedProcedure
|
create: protectedProcedure
|
||||||
.input(apiCreateMariaDB)
|
.input(apiCreateMariaDB)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
try {
|
try {
|
||||||
// Get project from environment
|
|
||||||
const environment = await findEnvironmentById(input.environmentId);
|
const environment = await findEnvironmentById(input.environmentId);
|
||||||
const project = await findProjectById(environment.projectId);
|
const project = await findProjectById(environment.projectId);
|
||||||
|
|
||||||
if (ctx.user.role === "member") {
|
await checkServiceAccess(ctx, project.projectId, "create");
|
||||||
await checkServiceAccess(
|
|
||||||
ctx.user.id,
|
|
||||||
project.projectId,
|
|
||||||
ctx.session.activeOrganizationId,
|
|
||||||
"create",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (IS_CLOUD && !input.serverId) {
|
if (IS_CLOUD && !input.serverId) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
@@ -74,13 +71,7 @@ export const mariadbRouter = createTRPCRouter({
|
|||||||
const newMariadb = await createMariadb({
|
const newMariadb = await createMariadb({
|
||||||
...input,
|
...input,
|
||||||
});
|
});
|
||||||
if (ctx.user.role === "member") {
|
await addNewService(ctx, newMariadb.mariadbId);
|
||||||
await addNewService(
|
|
||||||
ctx.user.id,
|
|
||||||
newMariadb.mariadbId,
|
|
||||||
project.organizationId,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
await createMount({
|
await createMount({
|
||||||
serviceId: newMariadb.mariadbId,
|
serviceId: newMariadb.mariadbId,
|
||||||
@@ -90,6 +81,12 @@ export const mariadbRouter = createTRPCRouter({
|
|||||||
type: "volume",
|
type: "volume",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await audit(ctx, {
|
||||||
|
action: "create",
|
||||||
|
resourceType: "service",
|
||||||
|
resourceId: newMariadb.mariadbId,
|
||||||
|
resourceName: newMariadb.appName,
|
||||||
|
});
|
||||||
return newMariadb;
|
return newMariadb;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof TRPCError) {
|
if (error instanceof TRPCError) {
|
||||||
@@ -101,14 +98,7 @@ export const mariadbRouter = createTRPCRouter({
|
|||||||
one: protectedProcedure
|
one: protectedProcedure
|
||||||
.input(apiFindOneMariaDB)
|
.input(apiFindOneMariaDB)
|
||||||
.query(async ({ input, ctx }) => {
|
.query(async ({ input, ctx }) => {
|
||||||
if (ctx.user.role === "member") {
|
await checkServiceAccess(ctx, input.mariadbId, "read");
|
||||||
await checkServiceAccess(
|
|
||||||
ctx.user.id,
|
|
||||||
input.mariadbId,
|
|
||||||
ctx.session.activeOrganizationId,
|
|
||||||
"access",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
const mariadb = await findMariadbById(input.mariadbId);
|
const mariadb = await findMariadbById(input.mariadbId);
|
||||||
if (
|
if (
|
||||||
mariadb.environment.project.organizationId !==
|
mariadb.environment.project.organizationId !==
|
||||||
@@ -125,16 +115,10 @@ export const mariadbRouter = createTRPCRouter({
|
|||||||
start: protectedProcedure
|
start: protectedProcedure
|
||||||
.input(apiFindOneMariaDB)
|
.input(apiFindOneMariaDB)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
|
await checkServicePermissionAndAccess(ctx, input.mariadbId, {
|
||||||
|
deployment: ["create"],
|
||||||
|
});
|
||||||
const service = await findMariadbById(input.mariadbId);
|
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) {
|
if (service.serverId) {
|
||||||
await startServiceRemote(service.serverId, service.appName);
|
await startServiceRemote(service.serverId, service.appName);
|
||||||
} else {
|
} else {
|
||||||
@@ -144,11 +128,20 @@ export const mariadbRouter = createTRPCRouter({
|
|||||||
applicationStatus: "done",
|
applicationStatus: "done",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await audit(ctx, {
|
||||||
|
action: "start",
|
||||||
|
resourceType: "service",
|
||||||
|
resourceId: service.mariadbId,
|
||||||
|
resourceName: service.appName,
|
||||||
|
});
|
||||||
return service;
|
return service;
|
||||||
}),
|
}),
|
||||||
stop: protectedProcedure
|
stop: protectedProcedure
|
||||||
.input(apiFindOneMariaDB)
|
.input(apiFindOneMariaDB)
|
||||||
.mutation(async ({ input }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
|
await checkServicePermissionAndAccess(ctx, input.mariadbId, {
|
||||||
|
deployment: ["create"],
|
||||||
|
});
|
||||||
const mariadb = await findMariadbById(input.mariadbId);
|
const mariadb = await findMariadbById(input.mariadbId);
|
||||||
|
|
||||||
if (mariadb.serverId) {
|
if (mariadb.serverId) {
|
||||||
@@ -160,21 +153,21 @@ export const mariadbRouter = createTRPCRouter({
|
|||||||
applicationStatus: "idle",
|
applicationStatus: "idle",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await audit(ctx, {
|
||||||
|
action: "stop",
|
||||||
|
resourceType: "service",
|
||||||
|
resourceId: mariadb.mariadbId,
|
||||||
|
resourceName: mariadb.appName,
|
||||||
|
});
|
||||||
return mariadb;
|
return mariadb;
|
||||||
}),
|
}),
|
||||||
saveExternalPort: protectedProcedure
|
saveExternalPort: protectedProcedure
|
||||||
.input(apiSaveExternalPortMariaDB)
|
.input(apiSaveExternalPortMariaDB)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
|
await checkServicePermissionAndAccess(ctx, input.mariadbId, {
|
||||||
|
service: ["create"],
|
||||||
|
});
|
||||||
const mariadb = await findMariadbById(input.mariadbId);
|
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) {
|
if (input.externalPort) {
|
||||||
const portCheck = await checkPortInUse(
|
const portCheck = await checkPortInUse(
|
||||||
@@ -193,22 +186,28 @@ export const mariadbRouter = createTRPCRouter({
|
|||||||
externalPort: input.externalPort,
|
externalPort: input.externalPort,
|
||||||
});
|
});
|
||||||
await deployMariadb(input.mariadbId);
|
await deployMariadb(input.mariadbId);
|
||||||
|
await audit(ctx, {
|
||||||
|
action: "update",
|
||||||
|
resourceType: "service",
|
||||||
|
resourceId: mariadb.mariadbId,
|
||||||
|
resourceName: mariadb.appName,
|
||||||
|
});
|
||||||
return mariadb;
|
return mariadb;
|
||||||
}),
|
}),
|
||||||
deploy: protectedProcedure
|
deploy: protectedProcedure
|
||||||
.input(apiDeployMariaDB)
|
.input(apiDeployMariaDB)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
|
await checkServicePermissionAndAccess(ctx, input.mariadbId, {
|
||||||
|
deployment: ["create"],
|
||||||
|
});
|
||||||
const mariadb = await findMariadbById(input.mariadbId);
|
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);
|
return deployMariadb(input.mariadbId);
|
||||||
}),
|
}),
|
||||||
deployWithLogs: protectedProcedure
|
deployWithLogs: protectedProcedure
|
||||||
@@ -222,16 +221,9 @@ export const mariadbRouter = createTRPCRouter({
|
|||||||
})
|
})
|
||||||
.input(apiDeployMariaDB)
|
.input(apiDeployMariaDB)
|
||||||
.subscription(async ({ input, ctx }) => {
|
.subscription(async ({ input, ctx }) => {
|
||||||
const mariadb = await findMariadbById(input.mariadbId);
|
await checkServicePermissionAndAccess(ctx, input.mariadbId, {
|
||||||
if (
|
deployment: ["create"],
|
||||||
mariadb.environment.project.organizationId !==
|
});
|
||||||
ctx.session.activeOrganizationId
|
|
||||||
) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "UNAUTHORIZED",
|
|
||||||
message: "You are not authorized to deploy this Mariadb",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return observable<string>((emit) => {
|
return observable<string>((emit) => {
|
||||||
deployMariadb(input.mariadbId, (log) => {
|
deployMariadb(input.mariadbId, (log) => {
|
||||||
@@ -242,32 +234,25 @@ export const mariadbRouter = createTRPCRouter({
|
|||||||
changeStatus: protectedProcedure
|
changeStatus: protectedProcedure
|
||||||
.input(apiChangeMariaDBStatus)
|
.input(apiChangeMariaDBStatus)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
|
await checkServicePermissionAndAccess(ctx, input.mariadbId, {
|
||||||
|
deployment: ["create"],
|
||||||
|
});
|
||||||
const mongo = await findMariadbById(input.mariadbId);
|
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, {
|
await updateMariadbById(input.mariadbId, {
|
||||||
applicationStatus: input.applicationStatus,
|
applicationStatus: input.applicationStatus,
|
||||||
});
|
});
|
||||||
|
await audit(ctx, {
|
||||||
|
action: "update",
|
||||||
|
resourceType: "service",
|
||||||
|
resourceId: mongo.mariadbId,
|
||||||
|
resourceName: mongo.appName,
|
||||||
|
});
|
||||||
return mongo;
|
return mongo;
|
||||||
}),
|
}),
|
||||||
remove: protectedProcedure
|
remove: protectedProcedure
|
||||||
.input(apiFindOneMariaDB)
|
.input(apiFindOneMariaDB)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
if (ctx.user.role === "member") {
|
await checkServiceAccess(ctx, input.mariadbId, "delete");
|
||||||
await checkServiceAccess(
|
|
||||||
ctx.user.id,
|
|
||||||
input.mariadbId,
|
|
||||||
ctx.session.activeOrganizationId,
|
|
||||||
"delete",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const mongo = await findMariadbById(input.mariadbId);
|
const mongo = await findMariadbById(input.mariadbId);
|
||||||
if (
|
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 backups = await findBackupsByDbId(input.mariadbId, "mariadb");
|
||||||
const cleanupOperations = [
|
const cleanupOperations = [
|
||||||
async () => await removeService(mongo?.appName, mongo.serverId),
|
async () => await removeService(mongo?.appName, mongo.serverId),
|
||||||
@@ -298,16 +289,9 @@ export const mariadbRouter = createTRPCRouter({
|
|||||||
saveEnvironment: protectedProcedure
|
saveEnvironment: protectedProcedure
|
||||||
.input(apiSaveEnvironmentVariablesMariaDB)
|
.input(apiSaveEnvironmentVariablesMariaDB)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
const mariadb = await findMariadbById(input.mariadbId);
|
await checkServicePermissionAndAccess(ctx, input.mariadbId, {
|
||||||
if (
|
envVars: ["write"],
|
||||||
mariadb.environment.project.organizationId !==
|
});
|
||||||
ctx.session.activeOrganizationId
|
|
||||||
) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "UNAUTHORIZED",
|
|
||||||
message: "You are not authorized to save this environment",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
const service = await updateMariadbById(input.mariadbId, {
|
const service = await updateMariadbById(input.mariadbId, {
|
||||||
env: input.env,
|
env: input.env,
|
||||||
});
|
});
|
||||||
@@ -319,21 +303,20 @@ export const mariadbRouter = createTRPCRouter({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await audit(ctx, {
|
||||||
|
action: "update",
|
||||||
|
resourceType: "service",
|
||||||
|
resourceId: input.mariadbId,
|
||||||
|
});
|
||||||
return true;
|
return true;
|
||||||
}),
|
}),
|
||||||
reload: protectedProcedure
|
reload: protectedProcedure
|
||||||
.input(apiResetMariadb)
|
.input(apiResetMariadb)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
|
await checkServicePermissionAndAccess(ctx, input.mariadbId, {
|
||||||
|
deployment: ["create"],
|
||||||
|
});
|
||||||
const mariadb = await findMariadbById(input.mariadbId);
|
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) {
|
if (mariadb.serverId) {
|
||||||
await stopServiceRemote(mariadb.serverId, mariadb.appName);
|
await stopServiceRemote(mariadb.serverId, mariadb.appName);
|
||||||
} else {
|
} else {
|
||||||
@@ -351,22 +334,21 @@ export const mariadbRouter = createTRPCRouter({
|
|||||||
await updateMariadbById(input.mariadbId, {
|
await updateMariadbById(input.mariadbId, {
|
||||||
applicationStatus: "done",
|
applicationStatus: "done",
|
||||||
});
|
});
|
||||||
|
await audit(ctx, {
|
||||||
|
action: "reload",
|
||||||
|
resourceType: "service",
|
||||||
|
resourceId: mariadb.mariadbId,
|
||||||
|
resourceName: mariadb.appName,
|
||||||
|
});
|
||||||
return true;
|
return true;
|
||||||
}),
|
}),
|
||||||
update: protectedProcedure
|
update: protectedProcedure
|
||||||
.input(apiUpdateMariaDB)
|
.input(apiUpdateMariaDB)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
const { mariadbId, ...rest } = input;
|
const { mariadbId, ...rest } = input;
|
||||||
const mariadb = await findMariadbById(mariadbId);
|
await checkServicePermissionAndAccess(ctx, mariadbId, {
|
||||||
if (
|
service: ["create"],
|
||||||
mariadb.environment.project.organizationId !==
|
});
|
||||||
ctx.session.activeOrganizationId
|
|
||||||
) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "UNAUTHORIZED",
|
|
||||||
message: "You are not authorized to update this Mariadb",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
const service = await updateMariadbById(mariadbId, {
|
const service = await updateMariadbById(mariadbId, {
|
||||||
...rest,
|
...rest,
|
||||||
});
|
});
|
||||||
@@ -378,6 +360,12 @@ export const mariadbRouter = createTRPCRouter({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await audit(ctx, {
|
||||||
|
action: "update",
|
||||||
|
resourceType: "service",
|
||||||
|
resourceId: mariadbId,
|
||||||
|
resourceName: service.appName,
|
||||||
|
});
|
||||||
return true;
|
return true;
|
||||||
}),
|
}),
|
||||||
move: protectedProcedure
|
move: protectedProcedure
|
||||||
@@ -388,31 +376,10 @@ export const mariadbRouter = createTRPCRouter({
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
const mariadb = await findMariadbById(input.mariadbId);
|
await checkServicePermissionAndAccess(ctx, input.mariadbId, {
|
||||||
if (
|
service: ["create"],
|
||||||
mariadb.environment.project.organizationId !==
|
});
|
||||||
ctx.session.activeOrganizationId
|
|
||||||
) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "UNAUTHORIZED",
|
|
||||||
message: "You are not authorized to move this mariadb",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const targetEnvironment = await findEnvironmentById(
|
|
||||||
input.targetEnvironmentId,
|
|
||||||
);
|
|
||||||
if (
|
|
||||||
targetEnvironment.project.organizationId !==
|
|
||||||
ctx.session.activeOrganizationId
|
|
||||||
) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "UNAUTHORIZED",
|
|
||||||
message: "You are not authorized to move to this environment",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update the mariadb's projectId
|
|
||||||
const updatedMariadb = await db
|
const updatedMariadb = await db
|
||||||
.update(mariadbTable)
|
.update(mariadbTable)
|
||||||
.set({
|
.set({
|
||||||
@@ -429,23 +396,27 @@ export const mariadbRouter = createTRPCRouter({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await audit(ctx, {
|
||||||
|
action: "move",
|
||||||
|
resourceType: "service",
|
||||||
|
resourceId: updatedMariadb.mariadbId,
|
||||||
|
resourceName: updatedMariadb.appName,
|
||||||
|
});
|
||||||
return updatedMariadb;
|
return updatedMariadb;
|
||||||
}),
|
}),
|
||||||
rebuild: protectedProcedure
|
rebuild: protectedProcedure
|
||||||
.input(apiRebuildMariadb)
|
.input(apiRebuildMariadb)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
const mariadb = await findMariadbById(input.mariadbId);
|
await checkServicePermissionAndAccess(ctx, input.mariadbId, {
|
||||||
if (
|
deployment: ["create"],
|
||||||
mariadb.environment.project.organizationId !==
|
});
|
||||||
ctx.session.activeOrganizationId
|
|
||||||
) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "UNAUTHORIZED",
|
|
||||||
message: "You are not authorized to rebuild this MariaDB database",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
await rebuildDatabase(mariadb.mariadbId, "mariadb");
|
await rebuildDatabase(input.mariadbId, "mariadb");
|
||||||
|
await audit(ctx, {
|
||||||
|
action: "rebuild",
|
||||||
|
resourceType: "service",
|
||||||
|
resourceId: input.mariadbId,
|
||||||
|
});
|
||||||
return true;
|
return true;
|
||||||
}),
|
}),
|
||||||
search: protectedProcedure
|
search: protectedProcedure
|
||||||
@@ -499,19 +470,18 @@ export const mariadbRouter = createTRPCRouter({
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (ctx.user.role === "member") {
|
const { accessedServices } = await findMemberByUserId(
|
||||||
const { accessedServices } = await findMemberById(
|
ctx.user.id,
|
||||||
ctx.user.id,
|
ctx.session.activeOrganizationId,
|
||||||
ctx.session.activeOrganizationId,
|
);
|
||||||
);
|
if (accessedServices.length === 0) return { items: [], total: 0 };
|
||||||
if (accessedServices.length === 0) return { items: [], total: 0 };
|
baseConditions.push(
|
||||||
baseConditions.push(
|
sql`${mariadbTable.mariadbId} IN (${sql.join(
|
||||||
sql`${mariadbTable.mariadbId} IN (${sql.join(
|
accessedServices.map((id) => sql`${id}`),
|
||||||
accessedServices.map((id) => sql`${id}`),
|
sql`, `,
|
||||||
sql`, `,
|
)})`,
|
||||||
)})`,
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
const where = and(...baseConditions);
|
const where = and(...baseConditions);
|
||||||
const [items, countResult] = await Promise.all([
|
const [items, countResult] = await Promise.all([
|
||||||
db
|
db
|
||||||
|
|||||||
@@ -1,13 +1,10 @@
|
|||||||
import {
|
import {
|
||||||
addNewService,
|
|
||||||
checkPortInUse,
|
checkPortInUse,
|
||||||
checkServiceAccess,
|
|
||||||
createMongo,
|
createMongo,
|
||||||
createMount,
|
createMount,
|
||||||
deployMongo,
|
deployMongo,
|
||||||
findBackupsByDbId,
|
findBackupsByDbId,
|
||||||
findEnvironmentById,
|
findEnvironmentById,
|
||||||
findMemberById,
|
|
||||||
findMongoById,
|
findMongoById,
|
||||||
findProjectById,
|
findProjectById,
|
||||||
IS_CLOUD,
|
IS_CLOUD,
|
||||||
@@ -20,10 +17,17 @@ import {
|
|||||||
stopServiceRemote,
|
stopServiceRemote,
|
||||||
updateMongoById,
|
updateMongoById,
|
||||||
} from "@dokploy/server";
|
} from "@dokploy/server";
|
||||||
|
import {
|
||||||
|
addNewService,
|
||||||
|
checkServiceAccess,
|
||||||
|
checkServicePermissionAndAccess,
|
||||||
|
findMemberByUserId,
|
||||||
|
} from "@dokploy/server/services/permission";
|
||||||
import { db } from "@dokploy/server/db";
|
import { db } from "@dokploy/server/db";
|
||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
import { and, desc, eq, ilike, or, sql } from "drizzle-orm";
|
import { and, desc, eq, ilike, or, sql } from "drizzle-orm";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
import { audit } from "@/server/api/utils/audit";
|
||||||
import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc";
|
import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc";
|
||||||
import {
|
import {
|
||||||
apiChangeMongoStatus,
|
apiChangeMongoStatus,
|
||||||
@@ -44,18 +48,10 @@ export const mongoRouter = createTRPCRouter({
|
|||||||
.input(apiCreateMongo)
|
.input(apiCreateMongo)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
try {
|
try {
|
||||||
// Get project from environment
|
|
||||||
const environment = await findEnvironmentById(input.environmentId);
|
const environment = await findEnvironmentById(input.environmentId);
|
||||||
const project = await findProjectById(environment.projectId);
|
const project = await findProjectById(environment.projectId);
|
||||||
|
|
||||||
if (ctx.user.role === "member") {
|
await checkServiceAccess(ctx, project.projectId, "create");
|
||||||
await checkServiceAccess(
|
|
||||||
ctx.user.id,
|
|
||||||
project.projectId,
|
|
||||||
ctx.session.activeOrganizationId,
|
|
||||||
"create",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (IS_CLOUD && !input.serverId) {
|
if (IS_CLOUD && !input.serverId) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
@@ -73,13 +69,7 @@ export const mongoRouter = createTRPCRouter({
|
|||||||
const newMongo = await createMongo({
|
const newMongo = await createMongo({
|
||||||
...input,
|
...input,
|
||||||
});
|
});
|
||||||
if (ctx.user.role === "member") {
|
await addNewService(ctx, newMongo.mongoId);
|
||||||
await addNewService(
|
|
||||||
ctx.user.id,
|
|
||||||
newMongo.mongoId,
|
|
||||||
project.organizationId,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
await createMount({
|
await createMount({
|
||||||
serviceId: newMongo.mongoId,
|
serviceId: newMongo.mongoId,
|
||||||
@@ -89,6 +79,12 @@ export const mongoRouter = createTRPCRouter({
|
|||||||
type: "volume",
|
type: "volume",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await audit(ctx, {
|
||||||
|
action: "create",
|
||||||
|
resourceType: "service",
|
||||||
|
resourceId: newMongo.mongoId,
|
||||||
|
resourceName: newMongo.appName,
|
||||||
|
});
|
||||||
return newMongo;
|
return newMongo;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof TRPCError) {
|
if (error instanceof TRPCError) {
|
||||||
@@ -104,14 +100,7 @@ export const mongoRouter = createTRPCRouter({
|
|||||||
one: protectedProcedure
|
one: protectedProcedure
|
||||||
.input(apiFindOneMongo)
|
.input(apiFindOneMongo)
|
||||||
.query(async ({ input, ctx }) => {
|
.query(async ({ input, ctx }) => {
|
||||||
if (ctx.user.role === "member") {
|
await checkServiceAccess(ctx, input.mongoId, "read");
|
||||||
await checkServiceAccess(
|
|
||||||
ctx.user.id,
|
|
||||||
input.mongoId,
|
|
||||||
ctx.session.activeOrganizationId,
|
|
||||||
"access",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const mongo = await findMongoById(input.mongoId);
|
const mongo = await findMongoById(input.mongoId);
|
||||||
if (
|
if (
|
||||||
@@ -129,18 +118,11 @@ export const mongoRouter = createTRPCRouter({
|
|||||||
start: protectedProcedure
|
start: protectedProcedure
|
||||||
.input(apiFindOneMongo)
|
.input(apiFindOneMongo)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
|
await checkServicePermissionAndAccess(ctx, input.mongoId, {
|
||||||
|
deployment: ["create"],
|
||||||
|
});
|
||||||
const service = await findMongoById(input.mongoId);
|
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) {
|
if (service.serverId) {
|
||||||
await startServiceRemote(service.serverId, service.appName);
|
await startServiceRemote(service.serverId, service.appName);
|
||||||
} else {
|
} else {
|
||||||
@@ -150,23 +132,22 @@ export const mongoRouter = createTRPCRouter({
|
|||||||
applicationStatus: "done",
|
applicationStatus: "done",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await audit(ctx, {
|
||||||
|
action: "start",
|
||||||
|
resourceType: "service",
|
||||||
|
resourceId: service.mongoId,
|
||||||
|
resourceName: service.appName,
|
||||||
|
});
|
||||||
return service;
|
return service;
|
||||||
}),
|
}),
|
||||||
stop: protectedProcedure
|
stop: protectedProcedure
|
||||||
.input(apiFindOneMongo)
|
.input(apiFindOneMongo)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
|
await checkServicePermissionAndAccess(ctx, input.mongoId, {
|
||||||
|
deployment: ["create"],
|
||||||
|
});
|
||||||
const mongo = await findMongoById(input.mongoId);
|
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) {
|
if (mongo.serverId) {
|
||||||
await stopServiceRemote(mongo.serverId, mongo.appName);
|
await stopServiceRemote(mongo.serverId, mongo.appName);
|
||||||
} else {
|
} else {
|
||||||
@@ -176,21 +157,21 @@ export const mongoRouter = createTRPCRouter({
|
|||||||
applicationStatus: "idle",
|
applicationStatus: "idle",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await audit(ctx, {
|
||||||
|
action: "stop",
|
||||||
|
resourceType: "service",
|
||||||
|
resourceId: mongo.mongoId,
|
||||||
|
resourceName: mongo.appName,
|
||||||
|
});
|
||||||
return mongo;
|
return mongo;
|
||||||
}),
|
}),
|
||||||
saveExternalPort: protectedProcedure
|
saveExternalPort: protectedProcedure
|
||||||
.input(apiSaveExternalPortMongo)
|
.input(apiSaveExternalPortMongo)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
|
await checkServicePermissionAndAccess(ctx, input.mongoId, {
|
||||||
|
service: ["create"],
|
||||||
|
});
|
||||||
const mongo = await findMongoById(input.mongoId);
|
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) {
|
if (input.externalPort) {
|
||||||
const portCheck = await checkPortInUse(
|
const portCheck = await checkPortInUse(
|
||||||
@@ -209,21 +190,27 @@ export const mongoRouter = createTRPCRouter({
|
|||||||
externalPort: input.externalPort,
|
externalPort: input.externalPort,
|
||||||
});
|
});
|
||||||
await deployMongo(input.mongoId);
|
await deployMongo(input.mongoId);
|
||||||
|
await audit(ctx, {
|
||||||
|
action: "update",
|
||||||
|
resourceType: "service",
|
||||||
|
resourceId: mongo.mongoId,
|
||||||
|
resourceName: mongo.appName,
|
||||||
|
});
|
||||||
return mongo;
|
return mongo;
|
||||||
}),
|
}),
|
||||||
deploy: protectedProcedure
|
deploy: protectedProcedure
|
||||||
.input(apiDeployMongo)
|
.input(apiDeployMongo)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
|
await checkServicePermissionAndAccess(ctx, input.mongoId, {
|
||||||
|
deployment: ["create"],
|
||||||
|
});
|
||||||
const mongo = await findMongoById(input.mongoId);
|
const mongo = await findMongoById(input.mongoId);
|
||||||
if (
|
await audit(ctx, {
|
||||||
mongo.environment.project.organizationId !==
|
action: "deploy",
|
||||||
ctx.session.activeOrganizationId
|
resourceType: "service",
|
||||||
) {
|
resourceId: mongo.mongoId,
|
||||||
throw new TRPCError({
|
resourceName: mongo.appName,
|
||||||
code: "UNAUTHORIZED",
|
});
|
||||||
message: "You are not authorized to deploy this mongo",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return deployMongo(input.mongoId);
|
return deployMongo(input.mongoId);
|
||||||
}),
|
}),
|
||||||
deployWithLogs: protectedProcedure
|
deployWithLogs: protectedProcedure
|
||||||
@@ -237,16 +224,9 @@ export const mongoRouter = createTRPCRouter({
|
|||||||
})
|
})
|
||||||
.input(apiDeployMongo)
|
.input(apiDeployMongo)
|
||||||
.subscription(async function* ({ input, ctx, signal }) {
|
.subscription(async function* ({ input, ctx, signal }) {
|
||||||
const mongo = await findMongoById(input.mongoId);
|
await checkServicePermissionAndAccess(ctx, input.mongoId, {
|
||||||
if (
|
deployment: ["create"],
|
||||||
mongo.environment.project.organizationId !==
|
});
|
||||||
ctx.session.activeOrganizationId
|
|
||||||
) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "UNAUTHORIZED",
|
|
||||||
message: "You are not authorized to deploy this mongo",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
const queue: string[] = [];
|
const queue: string[] = [];
|
||||||
const done = false;
|
const done = false;
|
||||||
|
|
||||||
@@ -270,34 +250,28 @@ export const mongoRouter = createTRPCRouter({
|
|||||||
changeStatus: protectedProcedure
|
changeStatus: protectedProcedure
|
||||||
.input(apiChangeMongoStatus)
|
.input(apiChangeMongoStatus)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
|
await checkServicePermissionAndAccess(ctx, input.mongoId, {
|
||||||
|
deployment: ["create"],
|
||||||
|
});
|
||||||
const mongo = await findMongoById(input.mongoId);
|
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, {
|
await updateMongoById(input.mongoId, {
|
||||||
applicationStatus: input.applicationStatus,
|
applicationStatus: input.applicationStatus,
|
||||||
});
|
});
|
||||||
|
await audit(ctx, {
|
||||||
|
action: "update",
|
||||||
|
resourceType: "service",
|
||||||
|
resourceId: mongo.mongoId,
|
||||||
|
resourceName: mongo.appName,
|
||||||
|
});
|
||||||
return mongo;
|
return mongo;
|
||||||
}),
|
}),
|
||||||
reload: protectedProcedure
|
reload: protectedProcedure
|
||||||
.input(apiResetMongo)
|
.input(apiResetMongo)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
|
await checkServicePermissionAndAccess(ctx, input.mongoId, {
|
||||||
|
deployment: ["create"],
|
||||||
|
});
|
||||||
const mongo = await findMongoById(input.mongoId);
|
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) {
|
if (mongo.serverId) {
|
||||||
await stopServiceRemote(mongo.serverId, mongo.appName);
|
await stopServiceRemote(mongo.serverId, mongo.appName);
|
||||||
} else {
|
} else {
|
||||||
@@ -315,19 +289,18 @@ export const mongoRouter = createTRPCRouter({
|
|||||||
await updateMongoById(input.mongoId, {
|
await updateMongoById(input.mongoId, {
|
||||||
applicationStatus: "done",
|
applicationStatus: "done",
|
||||||
});
|
});
|
||||||
|
await audit(ctx, {
|
||||||
|
action: "reload",
|
||||||
|
resourceType: "service",
|
||||||
|
resourceId: mongo.mongoId,
|
||||||
|
resourceName: mongo.appName,
|
||||||
|
});
|
||||||
return true;
|
return true;
|
||||||
}),
|
}),
|
||||||
remove: protectedProcedure
|
remove: protectedProcedure
|
||||||
.input(apiFindOneMongo)
|
.input(apiFindOneMongo)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
if (ctx.user.role === "member") {
|
await checkServiceAccess(ctx, input.mongoId, "delete");
|
||||||
await checkServiceAccess(
|
|
||||||
ctx.user.id,
|
|
||||||
input.mongoId,
|
|
||||||
ctx.session.activeOrganizationId,
|
|
||||||
"delete",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const mongo = await findMongoById(input.mongoId);
|
const mongo = await findMongoById(input.mongoId);
|
||||||
|
|
||||||
@@ -340,6 +313,12 @@ export const mongoRouter = createTRPCRouter({
|
|||||||
message: "You are not authorized to delete this mongo",
|
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 backups = await findBackupsByDbId(input.mongoId, "mongo");
|
||||||
|
|
||||||
const cleanupOperations = [
|
const cleanupOperations = [
|
||||||
@@ -359,16 +338,9 @@ export const mongoRouter = createTRPCRouter({
|
|||||||
saveEnvironment: protectedProcedure
|
saveEnvironment: protectedProcedure
|
||||||
.input(apiSaveEnvironmentVariablesMongo)
|
.input(apiSaveEnvironmentVariablesMongo)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
const mongo = await findMongoById(input.mongoId);
|
await checkServicePermissionAndAccess(ctx, input.mongoId, {
|
||||||
if (
|
envVars: ["write"],
|
||||||
mongo.environment.project.organizationId !==
|
});
|
||||||
ctx.session.activeOrganizationId
|
|
||||||
) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "UNAUTHORIZED",
|
|
||||||
message: "You are not authorized to save this environment",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
const service = await updateMongoById(input.mongoId, {
|
const service = await updateMongoById(input.mongoId, {
|
||||||
env: input.env,
|
env: input.env,
|
||||||
});
|
});
|
||||||
@@ -380,22 +352,20 @@ export const mongoRouter = createTRPCRouter({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await audit(ctx, {
|
||||||
|
action: "update",
|
||||||
|
resourceType: "service",
|
||||||
|
resourceId: input.mongoId,
|
||||||
|
});
|
||||||
return true;
|
return true;
|
||||||
}),
|
}),
|
||||||
update: protectedProcedure
|
update: protectedProcedure
|
||||||
.input(apiUpdateMongo)
|
.input(apiUpdateMongo)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
const { mongoId, ...rest } = input;
|
const { mongoId, ...rest } = input;
|
||||||
const mongo = await findMongoById(mongoId);
|
await checkServicePermissionAndAccess(ctx, mongoId, {
|
||||||
if (
|
service: ["create"],
|
||||||
mongo.environment.project.organizationId !==
|
});
|
||||||
ctx.session.activeOrganizationId
|
|
||||||
) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "UNAUTHORIZED",
|
|
||||||
message: "You are not authorized to update this mongo",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
const service = await updateMongoById(mongoId, {
|
const service = await updateMongoById(mongoId, {
|
||||||
...rest,
|
...rest,
|
||||||
});
|
});
|
||||||
@@ -407,6 +377,12 @@ export const mongoRouter = createTRPCRouter({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await audit(ctx, {
|
||||||
|
action: "update",
|
||||||
|
resourceType: "service",
|
||||||
|
resourceId: mongoId,
|
||||||
|
resourceName: service.appName,
|
||||||
|
});
|
||||||
return true;
|
return true;
|
||||||
}),
|
}),
|
||||||
move: protectedProcedure
|
move: protectedProcedure
|
||||||
@@ -417,31 +393,10 @@ export const mongoRouter = createTRPCRouter({
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
const mongo = await findMongoById(input.mongoId);
|
await checkServicePermissionAndAccess(ctx, input.mongoId, {
|
||||||
if (
|
service: ["create"],
|
||||||
mongo.environment.project.organizationId !==
|
});
|
||||||
ctx.session.activeOrganizationId
|
|
||||||
) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "UNAUTHORIZED",
|
|
||||||
message: "You are not authorized to move this mongo",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const targetEnvironment = await findEnvironmentById(
|
|
||||||
input.targetEnvironmentId,
|
|
||||||
);
|
|
||||||
if (
|
|
||||||
targetEnvironment.project.organizationId !==
|
|
||||||
ctx.session.activeOrganizationId
|
|
||||||
) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "UNAUTHORIZED",
|
|
||||||
message: "You are not authorized to move to this environment",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update the mongo's projectId
|
|
||||||
const updatedMongo = await db
|
const updatedMongo = await db
|
||||||
.update(mongoTable)
|
.update(mongoTable)
|
||||||
.set({
|
.set({
|
||||||
@@ -458,24 +413,28 @@ export const mongoRouter = createTRPCRouter({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await audit(ctx, {
|
||||||
|
action: "move",
|
||||||
|
resourceType: "service",
|
||||||
|
resourceId: updatedMongo.mongoId,
|
||||||
|
resourceName: updatedMongo.appName,
|
||||||
|
});
|
||||||
return updatedMongo;
|
return updatedMongo;
|
||||||
}),
|
}),
|
||||||
rebuild: protectedProcedure
|
rebuild: protectedProcedure
|
||||||
.input(apiRebuildMongo)
|
.input(apiRebuildMongo)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
const mongo = await findMongoById(input.mongoId);
|
await checkServicePermissionAndAccess(ctx, input.mongoId, {
|
||||||
if (
|
deployment: ["create"],
|
||||||
mongo.environment.project.organizationId !==
|
});
|
||||||
ctx.session.activeOrganizationId
|
|
||||||
) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "UNAUTHORIZED",
|
|
||||||
message: "You are not authorized to rebuild this MongoDB database",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
await rebuildDatabase(mongo.mongoId, "mongo");
|
await rebuildDatabase(input.mongoId, "mongo");
|
||||||
|
|
||||||
|
await audit(ctx, {
|
||||||
|
action: "rebuild",
|
||||||
|
resourceType: "service",
|
||||||
|
resourceId: input.mongoId,
|
||||||
|
});
|
||||||
return true;
|
return true;
|
||||||
}),
|
}),
|
||||||
search: protectedProcedure
|
search: protectedProcedure
|
||||||
@@ -524,19 +483,18 @@ export const mongoRouter = createTRPCRouter({
|
|||||||
ilike(mongoTable.description ?? "", `%${input.description.trim()}%`),
|
ilike(mongoTable.description ?? "", `%${input.description.trim()}%`),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (ctx.user.role === "member") {
|
const { accessedServices } = await findMemberByUserId(
|
||||||
const { accessedServices } = await findMemberById(
|
ctx.user.id,
|
||||||
ctx.user.id,
|
ctx.session.activeOrganizationId,
|
||||||
ctx.session.activeOrganizationId,
|
);
|
||||||
);
|
if (accessedServices.length === 0) return { items: [], total: 0 };
|
||||||
if (accessedServices.length === 0) return { items: [], total: 0 };
|
baseConditions.push(
|
||||||
baseConditions.push(
|
sql`${mongoTable.mongoId} IN (${sql.join(
|
||||||
sql`${mongoTable.mongoId} IN (${sql.join(
|
accessedServices.map((id) => sql`${id}`),
|
||||||
accessedServices.map((id) => sql`${id}`),
|
sql`, `,
|
||||||
sql`, `,
|
)})`,
|
||||||
)})`,
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
const where = and(...baseConditions);
|
const where = and(...baseConditions);
|
||||||
const [items, countResult] = await Promise.all([
|
const [items, countResult] = await Promise.all([
|
||||||
db
|
db
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import {
|
import {
|
||||||
checkServiceAccess,
|
|
||||||
createMount,
|
createMount,
|
||||||
deleteMount,
|
deleteMount,
|
||||||
findApplicationById,
|
findApplicationById,
|
||||||
@@ -7,7 +6,6 @@ import {
|
|||||||
findMariadbById,
|
findMariadbById,
|
||||||
findMongoById,
|
findMongoById,
|
||||||
findMountById,
|
findMountById,
|
||||||
findMountOrganizationId,
|
|
||||||
findMountsByApplicationId,
|
findMountsByApplicationId,
|
||||||
findMySqlById,
|
findMySqlById,
|
||||||
findPostgresById,
|
findPostgresById,
|
||||||
@@ -15,6 +13,10 @@ import {
|
|||||||
getServiceContainer,
|
getServiceContainer,
|
||||||
updateMount,
|
updateMount,
|
||||||
} from "@dokploy/server";
|
} from "@dokploy/server";
|
||||||
|
import {
|
||||||
|
checkServiceAccess,
|
||||||
|
checkServicePermissionAndAccess,
|
||||||
|
} from "@dokploy/server/services/permission";
|
||||||
import type { ServiceType } from "@dokploy/server/db/schema/mount";
|
import type { ServiceType } from "@dokploy/server/db/schema/mount";
|
||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
@@ -26,6 +28,7 @@ import {
|
|||||||
apiUpdateMount,
|
apiUpdateMount,
|
||||||
} from "@/server/db/schema";
|
} from "@/server/db/schema";
|
||||||
import { createTRPCRouter, protectedProcedure } from "../trpc";
|
import { createTRPCRouter, protectedProcedure } from "../trpc";
|
||||||
|
import { audit } from "@/server/api/utils/audit";
|
||||||
|
|
||||||
async function getServiceOrganizationId(
|
async function getServiceOrganizationId(
|
||||||
serviceId: string,
|
serviceId: string,
|
||||||
@@ -68,49 +71,94 @@ async function getServiceOrganizationId(
|
|||||||
export const mountRouter = createTRPCRouter({
|
export const mountRouter = createTRPCRouter({
|
||||||
create: protectedProcedure
|
create: protectedProcedure
|
||||||
.input(apiCreateMount)
|
.input(apiCreateMount)
|
||||||
.mutation(async ({ input }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
return await createMount(input);
|
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
|
remove: protectedProcedure
|
||||||
.input(apiRemoveMount)
|
.input(apiRemoveMount)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
const organizationId = await findMountOrganizationId(input.mountId);
|
const mount = await findMountById(input.mountId);
|
||||||
if (organizationId !== ctx.session.activeOrganizationId) {
|
const serviceId =
|
||||||
throw new TRPCError({
|
mount.applicationId ||
|
||||||
code: "UNAUTHORIZED",
|
mount.postgresId ||
|
||||||
message: "You are not authorized to delete this mount",
|
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);
|
return await deleteMount(input.mountId);
|
||||||
}),
|
}),
|
||||||
|
|
||||||
one: protectedProcedure
|
one: protectedProcedure
|
||||||
.input(apiFindOneMount)
|
.input(apiFindOneMount)
|
||||||
.query(async ({ input, ctx }) => {
|
.query(async ({ input, ctx }) => {
|
||||||
const organizationId = await findMountOrganizationId(input.mountId);
|
const mount = await findMountById(input.mountId);
|
||||||
if (organizationId !== ctx.session.activeOrganizationId) {
|
const serviceId =
|
||||||
throw new TRPCError({
|
mount.applicationId ||
|
||||||
code: "UNAUTHORIZED",
|
mount.postgresId ||
|
||||||
message: "You are not authorized to access this mount",
|
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
|
update: protectedProcedure
|
||||||
.input(apiUpdateMount)
|
.input(apiUpdateMount)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
const organizationId = await findMountOrganizationId(input.mountId);
|
const mount = await findMountById(input.mountId);
|
||||||
if (organizationId !== ctx.session.activeOrganizationId) {
|
const serviceId =
|
||||||
throw new TRPCError({
|
mount.applicationId ||
|
||||||
code: "UNAUTHORIZED",
|
mount.postgresId ||
|
||||||
message: "You are not authorized to update this mount",
|
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);
|
return await updateMount(input.mountId, input);
|
||||||
}),
|
}),
|
||||||
allNamedByApplicationId: protectedProcedure
|
allNamedByApplicationId: protectedProcedure
|
||||||
.input(z.object({ applicationId: z.string().min(1) }))
|
.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 app = await findApplicationById(input.applicationId);
|
||||||
const container = await getServiceContainer(app.appName, app.serverId);
|
const container = await getServiceContainer(app.appName, app.serverId);
|
||||||
const mounts = container?.Mounts.filter(
|
const mounts = container?.Mounts.filter(
|
||||||
@@ -122,14 +170,7 @@ export const mountRouter = createTRPCRouter({
|
|||||||
.input(apiFindMountByApplicationId)
|
.input(apiFindMountByApplicationId)
|
||||||
.query(async ({ input, ctx }) => {
|
.query(async ({ input, ctx }) => {
|
||||||
console.log("input", input);
|
console.log("input", input);
|
||||||
if (ctx.user.role === "member") {
|
await checkServiceAccess(ctx, input.serviceId, "read");
|
||||||
await checkServiceAccess(
|
|
||||||
ctx.user.id,
|
|
||||||
input.serviceId,
|
|
||||||
ctx.session.activeOrganizationId,
|
|
||||||
"access",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
const organizationId = await getServiceOrganizationId(
|
const organizationId = await getServiceOrganizationId(
|
||||||
input.serviceId,
|
input.serviceId,
|
||||||
input.serviceType,
|
input.serviceType,
|
||||||
|
|||||||
@@ -1,13 +1,10 @@
|
|||||||
import {
|
import {
|
||||||
addNewService,
|
|
||||||
checkPortInUse,
|
checkPortInUse,
|
||||||
checkServiceAccess,
|
|
||||||
createMount,
|
createMount,
|
||||||
createMysql,
|
createMysql,
|
||||||
deployMySql,
|
deployMySql,
|
||||||
findBackupsByDbId,
|
findBackupsByDbId,
|
||||||
findEnvironmentById,
|
findEnvironmentById,
|
||||||
findMemberById,
|
|
||||||
findMySqlById,
|
findMySqlById,
|
||||||
findProjectById,
|
findProjectById,
|
||||||
IS_CLOUD,
|
IS_CLOUD,
|
||||||
@@ -20,10 +17,17 @@ import {
|
|||||||
stopServiceRemote,
|
stopServiceRemote,
|
||||||
updateMySqlById,
|
updateMySqlById,
|
||||||
} from "@dokploy/server";
|
} from "@dokploy/server";
|
||||||
|
import {
|
||||||
|
addNewService,
|
||||||
|
checkServiceAccess,
|
||||||
|
checkServicePermissionAndAccess,
|
||||||
|
findMemberByUserId,
|
||||||
|
} from "@dokploy/server/services/permission";
|
||||||
import { db } from "@dokploy/server/db";
|
import { db } from "@dokploy/server/db";
|
||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
import { and, desc, eq, ilike, or, sql } from "drizzle-orm";
|
import { and, desc, eq, ilike, or, sql } from "drizzle-orm";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
import { audit } from "@/server/api/utils/audit";
|
||||||
import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc";
|
import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc";
|
||||||
import {
|
import {
|
||||||
apiChangeMySqlStatus,
|
apiChangeMySqlStatus,
|
||||||
@@ -46,18 +50,10 @@ export const mysqlRouter = createTRPCRouter({
|
|||||||
.input(apiCreateMySql)
|
.input(apiCreateMySql)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
try {
|
try {
|
||||||
// Get project from environment
|
|
||||||
const environment = await findEnvironmentById(input.environmentId);
|
const environment = await findEnvironmentById(input.environmentId);
|
||||||
const project = await findProjectById(environment.projectId);
|
const project = await findProjectById(environment.projectId);
|
||||||
|
|
||||||
if (ctx.user.role === "member") {
|
await checkServiceAccess(ctx, project.projectId, "create");
|
||||||
await checkServiceAccess(
|
|
||||||
ctx.user.id,
|
|
||||||
project.projectId,
|
|
||||||
ctx.session.activeOrganizationId,
|
|
||||||
"create",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (IS_CLOUD && !input.serverId) {
|
if (IS_CLOUD && !input.serverId) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
@@ -76,13 +72,7 @@ export const mysqlRouter = createTRPCRouter({
|
|||||||
const newMysql = await createMysql({
|
const newMysql = await createMysql({
|
||||||
...input,
|
...input,
|
||||||
});
|
});
|
||||||
if (ctx.user.role === "member") {
|
await addNewService(ctx, newMysql.mysqlId);
|
||||||
await addNewService(
|
|
||||||
ctx.user.id,
|
|
||||||
newMysql.mysqlId,
|
|
||||||
project.organizationId,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
await createMount({
|
await createMount({
|
||||||
serviceId: newMysql.mysqlId,
|
serviceId: newMysql.mysqlId,
|
||||||
@@ -92,6 +82,12 @@ export const mysqlRouter = createTRPCRouter({
|
|||||||
type: "volume",
|
type: "volume",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await audit(ctx, {
|
||||||
|
action: "create",
|
||||||
|
resourceType: "service",
|
||||||
|
resourceId: newMysql.mysqlId,
|
||||||
|
resourceName: newMysql.appName,
|
||||||
|
});
|
||||||
return newMysql;
|
return newMysql;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof TRPCError) {
|
if (error instanceof TRPCError) {
|
||||||
@@ -107,14 +103,7 @@ export const mysqlRouter = createTRPCRouter({
|
|||||||
one: protectedProcedure
|
one: protectedProcedure
|
||||||
.input(apiFindOneMySql)
|
.input(apiFindOneMySql)
|
||||||
.query(async ({ input, ctx }) => {
|
.query(async ({ input, ctx }) => {
|
||||||
if (ctx.user.role === "member") {
|
await checkServiceAccess(ctx, input.mysqlId, "read");
|
||||||
await checkServiceAccess(
|
|
||||||
ctx.user.id,
|
|
||||||
input.mysqlId,
|
|
||||||
ctx.session.activeOrganizationId,
|
|
||||||
"access",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
const mysql = await findMySqlById(input.mysqlId);
|
const mysql = await findMySqlById(input.mysqlId);
|
||||||
if (
|
if (
|
||||||
mysql.environment.project.organizationId !==
|
mysql.environment.project.organizationId !==
|
||||||
@@ -131,16 +120,10 @@ export const mysqlRouter = createTRPCRouter({
|
|||||||
start: protectedProcedure
|
start: protectedProcedure
|
||||||
.input(apiFindOneMySql)
|
.input(apiFindOneMySql)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
|
await checkServicePermissionAndAccess(ctx, input.mysqlId, {
|
||||||
|
deployment: ["create"],
|
||||||
|
});
|
||||||
const service = await findMySqlById(input.mysqlId);
|
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) {
|
if (service.serverId) {
|
||||||
await startServiceRemote(service.serverId, service.appName);
|
await startServiceRemote(service.serverId, service.appName);
|
||||||
@@ -151,21 +134,21 @@ export const mysqlRouter = createTRPCRouter({
|
|||||||
applicationStatus: "done",
|
applicationStatus: "done",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await audit(ctx, {
|
||||||
|
action: "start",
|
||||||
|
resourceType: "service",
|
||||||
|
resourceId: service.mysqlId,
|
||||||
|
resourceName: service.appName,
|
||||||
|
});
|
||||||
return service;
|
return service;
|
||||||
}),
|
}),
|
||||||
stop: protectedProcedure
|
stop: protectedProcedure
|
||||||
.input(apiFindOneMySql)
|
.input(apiFindOneMySql)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
|
await checkServicePermissionAndAccess(ctx, input.mysqlId, {
|
||||||
|
deployment: ["create"],
|
||||||
|
});
|
||||||
const mongo = await findMySqlById(input.mysqlId);
|
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) {
|
if (mongo.serverId) {
|
||||||
await stopServiceRemote(mongo.serverId, mongo.appName);
|
await stopServiceRemote(mongo.serverId, mongo.appName);
|
||||||
} else {
|
} else {
|
||||||
@@ -175,21 +158,21 @@ export const mysqlRouter = createTRPCRouter({
|
|||||||
applicationStatus: "idle",
|
applicationStatus: "idle",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await audit(ctx, {
|
||||||
|
action: "stop",
|
||||||
|
resourceType: "service",
|
||||||
|
resourceId: mongo.mysqlId,
|
||||||
|
resourceName: mongo.appName,
|
||||||
|
});
|
||||||
return mongo;
|
return mongo;
|
||||||
}),
|
}),
|
||||||
saveExternalPort: protectedProcedure
|
saveExternalPort: protectedProcedure
|
||||||
.input(apiSaveExternalPortMySql)
|
.input(apiSaveExternalPortMySql)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
|
await checkServicePermissionAndAccess(ctx, input.mysqlId, {
|
||||||
|
service: ["create"],
|
||||||
|
});
|
||||||
const mysql = await findMySqlById(input.mysqlId);
|
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) {
|
if (input.externalPort) {
|
||||||
const portCheck = await checkPortInUse(
|
const portCheck = await checkPortInUse(
|
||||||
@@ -208,21 +191,27 @@ export const mysqlRouter = createTRPCRouter({
|
|||||||
externalPort: input.externalPort,
|
externalPort: input.externalPort,
|
||||||
});
|
});
|
||||||
await deployMySql(input.mysqlId);
|
await deployMySql(input.mysqlId);
|
||||||
|
await audit(ctx, {
|
||||||
|
action: "update",
|
||||||
|
resourceType: "service",
|
||||||
|
resourceId: mysql.mysqlId,
|
||||||
|
resourceName: mysql.appName,
|
||||||
|
});
|
||||||
return mysql;
|
return mysql;
|
||||||
}),
|
}),
|
||||||
deploy: protectedProcedure
|
deploy: protectedProcedure
|
||||||
.input(apiDeployMySql)
|
.input(apiDeployMySql)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
|
await checkServicePermissionAndAccess(ctx, input.mysqlId, {
|
||||||
|
deployment: ["create"],
|
||||||
|
});
|
||||||
const mysql = await findMySqlById(input.mysqlId);
|
const mysql = await findMySqlById(input.mysqlId);
|
||||||
if (
|
await audit(ctx, {
|
||||||
mysql.environment.project.organizationId !==
|
action: "deploy",
|
||||||
ctx.session.activeOrganizationId
|
resourceType: "service",
|
||||||
) {
|
resourceId: mysql.mysqlId,
|
||||||
throw new TRPCError({
|
resourceName: mysql.appName,
|
||||||
code: "UNAUTHORIZED",
|
});
|
||||||
message: "You are not authorized to deploy this MySQL",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return deployMySql(input.mysqlId);
|
return deployMySql(input.mysqlId);
|
||||||
}),
|
}),
|
||||||
deployWithLogs: protectedProcedure
|
deployWithLogs: protectedProcedure
|
||||||
@@ -236,16 +225,9 @@ export const mysqlRouter = createTRPCRouter({
|
|||||||
})
|
})
|
||||||
.input(apiDeployMySql)
|
.input(apiDeployMySql)
|
||||||
.subscription(async function* ({ input, ctx, signal }) {
|
.subscription(async function* ({ input, ctx, signal }) {
|
||||||
const mysql = await findMySqlById(input.mysqlId);
|
await checkServicePermissionAndAccess(ctx, input.mysqlId, {
|
||||||
if (
|
deployment: ["create"],
|
||||||
mysql.environment.project.organizationId !==
|
});
|
||||||
ctx.session.activeOrganizationId
|
|
||||||
) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "UNAUTHORIZED",
|
|
||||||
message: "You are not authorized to deploy this MySQL",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const queue: string[] = [];
|
const queue: string[] = [];
|
||||||
const done = false;
|
const done = false;
|
||||||
@@ -269,34 +251,28 @@ export const mysqlRouter = createTRPCRouter({
|
|||||||
changeStatus: protectedProcedure
|
changeStatus: protectedProcedure
|
||||||
.input(apiChangeMySqlStatus)
|
.input(apiChangeMySqlStatus)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
|
await checkServicePermissionAndAccess(ctx, input.mysqlId, {
|
||||||
|
deployment: ["create"],
|
||||||
|
});
|
||||||
const mongo = await findMySqlById(input.mysqlId);
|
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, {
|
await updateMySqlById(input.mysqlId, {
|
||||||
applicationStatus: input.applicationStatus,
|
applicationStatus: input.applicationStatus,
|
||||||
});
|
});
|
||||||
|
await audit(ctx, {
|
||||||
|
action: "update",
|
||||||
|
resourceType: "service",
|
||||||
|
resourceId: mongo.mysqlId,
|
||||||
|
resourceName: mongo.appName,
|
||||||
|
});
|
||||||
return mongo;
|
return mongo;
|
||||||
}),
|
}),
|
||||||
reload: protectedProcedure
|
reload: protectedProcedure
|
||||||
.input(apiResetMysql)
|
.input(apiResetMysql)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
|
await checkServicePermissionAndAccess(ctx, input.mysqlId, {
|
||||||
|
deployment: ["create"],
|
||||||
|
});
|
||||||
const mysql = await findMySqlById(input.mysqlId);
|
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) {
|
if (mysql.serverId) {
|
||||||
await stopServiceRemote(mysql.serverId, mysql.appName);
|
await stopServiceRemote(mysql.serverId, mysql.appName);
|
||||||
} else {
|
} else {
|
||||||
@@ -313,19 +289,18 @@ export const mysqlRouter = createTRPCRouter({
|
|||||||
await updateMySqlById(input.mysqlId, {
|
await updateMySqlById(input.mysqlId, {
|
||||||
applicationStatus: "done",
|
applicationStatus: "done",
|
||||||
});
|
});
|
||||||
|
await audit(ctx, {
|
||||||
|
action: "reload",
|
||||||
|
resourceType: "service",
|
||||||
|
resourceId: mysql.mysqlId,
|
||||||
|
resourceName: mysql.appName,
|
||||||
|
});
|
||||||
return true;
|
return true;
|
||||||
}),
|
}),
|
||||||
remove: protectedProcedure
|
remove: protectedProcedure
|
||||||
.input(apiFindOneMySql)
|
.input(apiFindOneMySql)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
if (ctx.user.role === "member") {
|
await checkServiceAccess(ctx, input.mysqlId, "delete");
|
||||||
await checkServiceAccess(
|
|
||||||
ctx.user.id,
|
|
||||||
input.mysqlId,
|
|
||||||
ctx.session.activeOrganizationId,
|
|
||||||
"delete",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
const mongo = await findMySqlById(input.mysqlId);
|
const mongo = await findMySqlById(input.mysqlId);
|
||||||
if (
|
if (
|
||||||
mongo.environment.project.organizationId !==
|
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 backups = await findBackupsByDbId(input.mysqlId, "mysql");
|
||||||
const cleanupOperations = [
|
const cleanupOperations = [
|
||||||
async () => await removeService(mongo?.appName, mongo.serverId),
|
async () => await removeService(mongo?.appName, mongo.serverId),
|
||||||
@@ -355,16 +336,9 @@ export const mysqlRouter = createTRPCRouter({
|
|||||||
saveEnvironment: protectedProcedure
|
saveEnvironment: protectedProcedure
|
||||||
.input(apiSaveEnvironmentVariablesMySql)
|
.input(apiSaveEnvironmentVariablesMySql)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
const mysql = await findMySqlById(input.mysqlId);
|
await checkServicePermissionAndAccess(ctx, input.mysqlId, {
|
||||||
if (
|
envVars: ["write"],
|
||||||
mysql.environment.project.organizationId !==
|
});
|
||||||
ctx.session.activeOrganizationId
|
|
||||||
) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "UNAUTHORIZED",
|
|
||||||
message: "You are not authorized to save this environment",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
const service = await updateMySqlById(input.mysqlId, {
|
const service = await updateMySqlById(input.mysqlId, {
|
||||||
env: input.env,
|
env: input.env,
|
||||||
});
|
});
|
||||||
@@ -376,22 +350,20 @@ export const mysqlRouter = createTRPCRouter({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await audit(ctx, {
|
||||||
|
action: "update",
|
||||||
|
resourceType: "service",
|
||||||
|
resourceId: input.mysqlId,
|
||||||
|
});
|
||||||
return true;
|
return true;
|
||||||
}),
|
}),
|
||||||
update: protectedProcedure
|
update: protectedProcedure
|
||||||
.input(apiUpdateMySql)
|
.input(apiUpdateMySql)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
const { mysqlId, ...rest } = input;
|
const { mysqlId, ...rest } = input;
|
||||||
const mysql = await findMySqlById(mysqlId);
|
await checkServicePermissionAndAccess(ctx, mysqlId, {
|
||||||
if (
|
service: ["create"],
|
||||||
mysql.environment.project.organizationId !==
|
});
|
||||||
ctx.session.activeOrganizationId
|
|
||||||
) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "UNAUTHORIZED",
|
|
||||||
message: "You are not authorized to update this MySQL",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
const service = await updateMySqlById(mysqlId, {
|
const service = await updateMySqlById(mysqlId, {
|
||||||
...rest,
|
...rest,
|
||||||
});
|
});
|
||||||
@@ -403,6 +375,12 @@ export const mysqlRouter = createTRPCRouter({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await audit(ctx, {
|
||||||
|
action: "update",
|
||||||
|
resourceType: "service",
|
||||||
|
resourceId: mysqlId,
|
||||||
|
resourceName: service.appName,
|
||||||
|
});
|
||||||
return true;
|
return true;
|
||||||
}),
|
}),
|
||||||
move: protectedProcedure
|
move: protectedProcedure
|
||||||
@@ -413,31 +391,10 @@ export const mysqlRouter = createTRPCRouter({
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
const mysql = await findMySqlById(input.mysqlId);
|
await checkServicePermissionAndAccess(ctx, input.mysqlId, {
|
||||||
if (
|
service: ["create"],
|
||||||
mysql.environment.project.organizationId !==
|
});
|
||||||
ctx.session.activeOrganizationId
|
|
||||||
) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "UNAUTHORIZED",
|
|
||||||
message: "You are not authorized to move this mysql",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const targetEnvironment = await findEnvironmentById(
|
|
||||||
input.targetEnvironmentId,
|
|
||||||
);
|
|
||||||
if (
|
|
||||||
targetEnvironment.project.organizationId !==
|
|
||||||
ctx.session.activeOrganizationId
|
|
||||||
) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "UNAUTHORIZED",
|
|
||||||
message: "You are not authorized to move to this environment",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update the mysql's projectId
|
|
||||||
const updatedMysql = await db
|
const updatedMysql = await db
|
||||||
.update(mysqlTable)
|
.update(mysqlTable)
|
||||||
.set({
|
.set({
|
||||||
@@ -454,24 +411,28 @@ export const mysqlRouter = createTRPCRouter({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await audit(ctx, {
|
||||||
|
action: "move",
|
||||||
|
resourceType: "service",
|
||||||
|
resourceId: updatedMysql.mysqlId,
|
||||||
|
resourceName: updatedMysql.appName,
|
||||||
|
});
|
||||||
return updatedMysql;
|
return updatedMysql;
|
||||||
}),
|
}),
|
||||||
rebuild: protectedProcedure
|
rebuild: protectedProcedure
|
||||||
.input(apiRebuildMysql)
|
.input(apiRebuildMysql)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
const mysql = await findMySqlById(input.mysqlId);
|
await checkServicePermissionAndAccess(ctx, input.mysqlId, {
|
||||||
if (
|
deployment: ["create"],
|
||||||
mysql.environment.project.organizationId !==
|
});
|
||||||
ctx.session.activeOrganizationId
|
|
||||||
) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "UNAUTHORIZED",
|
|
||||||
message: "You are not authorized to rebuild this MySQL database",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
await rebuildDatabase(mysql.mysqlId, "mysql");
|
await rebuildDatabase(input.mysqlId, "mysql");
|
||||||
|
|
||||||
|
await audit(ctx, {
|
||||||
|
action: "rebuild",
|
||||||
|
resourceType: "service",
|
||||||
|
resourceId: input.mysqlId,
|
||||||
|
});
|
||||||
return true;
|
return true;
|
||||||
}),
|
}),
|
||||||
search: protectedProcedure
|
search: protectedProcedure
|
||||||
@@ -520,19 +481,18 @@ export const mysqlRouter = createTRPCRouter({
|
|||||||
ilike(mysqlTable.description ?? "", `%${input.description.trim()}%`),
|
ilike(mysqlTable.description ?? "", `%${input.description.trim()}%`),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (ctx.user.role === "member") {
|
const { accessedServices } = await findMemberByUserId(
|
||||||
const { accessedServices } = await findMemberById(
|
ctx.user.id,
|
||||||
ctx.user.id,
|
ctx.session.activeOrganizationId,
|
||||||
ctx.session.activeOrganizationId,
|
);
|
||||||
);
|
if (accessedServices.length === 0) return { items: [], total: 0 };
|
||||||
if (accessedServices.length === 0) return { items: [], total: 0 };
|
baseConditions.push(
|
||||||
baseConditions.push(
|
sql`${mysqlTable.mysqlId} IN (${sql.join(
|
||||||
sql`${mysqlTable.mysqlId} IN (${sql.join(
|
accessedServices.map((id) => sql`${id}`),
|
||||||
accessedServices.map((id) => sql`${id}`),
|
sql`, `,
|
||||||
sql`, `,
|
)})`,
|
||||||
)})`,
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
const where = and(...baseConditions);
|
const where = and(...baseConditions);
|
||||||
const [items, countResult] = await Promise.all([
|
const [items, countResult] = await Promise.all([
|
||||||
db
|
db
|
||||||
|
|||||||
@@ -43,11 +43,11 @@ import { TRPCError } from "@trpc/server";
|
|||||||
import { desc, eq, sql } from "drizzle-orm";
|
import { desc, eq, sql } from "drizzle-orm";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import {
|
import {
|
||||||
adminProcedure,
|
|
||||||
createTRPCRouter,
|
createTRPCRouter,
|
||||||
protectedProcedure,
|
|
||||||
publicProcedure,
|
publicProcedure,
|
||||||
|
withPermission,
|
||||||
} from "@/server/api/trpc";
|
} from "@/server/api/trpc";
|
||||||
|
import { audit } from "@/server/api/utils/audit";
|
||||||
import {
|
import {
|
||||||
apiCreateCustom,
|
apiCreateCustom,
|
||||||
apiCreateDiscord,
|
apiCreateDiscord,
|
||||||
@@ -88,15 +88,18 @@ import {
|
|||||||
} from "@/server/db/schema";
|
} from "@/server/db/schema";
|
||||||
|
|
||||||
export const notificationRouter = createTRPCRouter({
|
export const notificationRouter = createTRPCRouter({
|
||||||
createSlack: adminProcedure
|
createSlack: withPermission("notification", "create")
|
||||||
.input(apiCreateSlack)
|
.input(apiCreateSlack)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
try {
|
try {
|
||||||
return await createSlackNotification(
|
await createSlackNotification(input, ctx.session.activeOrganizationId);
|
||||||
input,
|
await audit(ctx, {
|
||||||
ctx.session.activeOrganizationId,
|
action: "create",
|
||||||
);
|
resourceType: "notification",
|
||||||
|
resourceName: input.name,
|
||||||
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "BAD_REQUEST",
|
code: "BAD_REQUEST",
|
||||||
message: "Error creating the notification",
|
message: "Error creating the notification",
|
||||||
@@ -104,7 +107,7 @@ export const notificationRouter = createTRPCRouter({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
updateSlack: adminProcedure
|
updateSlack: withPermission("notification", "update")
|
||||||
.input(apiUpdateSlack)
|
.input(apiUpdateSlack)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
try {
|
try {
|
||||||
@@ -115,15 +118,22 @@ export const notificationRouter = createTRPCRouter({
|
|||||||
message: "You are not authorized to update this notification",
|
message: "You are not authorized to update this notification",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return await updateSlackNotification({
|
const result = await updateSlackNotification({
|
||||||
...input,
|
...input,
|
||||||
organizationId: ctx.session.activeOrganizationId,
|
organizationId: ctx.session.activeOrganizationId,
|
||||||
});
|
});
|
||||||
|
await audit(ctx, {
|
||||||
|
action: "update",
|
||||||
|
resourceType: "notification",
|
||||||
|
resourceId: input.notificationId,
|
||||||
|
resourceName: notification.name,
|
||||||
|
});
|
||||||
|
return result;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
testSlackConnection: adminProcedure
|
testSlackConnection: withPermission("notification", "create")
|
||||||
.input(apiTestSlackConnection)
|
.input(apiTestSlackConnection)
|
||||||
.mutation(async ({ input }) => {
|
.mutation(async ({ input }) => {
|
||||||
try {
|
try {
|
||||||
@@ -140,14 +150,19 @@ export const notificationRouter = createTRPCRouter({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
createTelegram: adminProcedure
|
createTelegram: withPermission("notification", "create")
|
||||||
.input(apiCreateTelegram)
|
.input(apiCreateTelegram)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
try {
|
try {
|
||||||
return await createTelegramNotification(
|
await createTelegramNotification(
|
||||||
input,
|
input,
|
||||||
ctx.session.activeOrganizationId,
|
ctx.session.activeOrganizationId,
|
||||||
);
|
);
|
||||||
|
await audit(ctx, {
|
||||||
|
action: "create",
|
||||||
|
resourceType: "notification",
|
||||||
|
resourceName: input.name,
|
||||||
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "BAD_REQUEST",
|
code: "BAD_REQUEST",
|
||||||
@@ -157,7 +172,7 @@ export const notificationRouter = createTRPCRouter({
|
|||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
|
||||||
updateTelegram: adminProcedure
|
updateTelegram: withPermission("notification", "update")
|
||||||
.input(apiUpdateTelegram)
|
.input(apiUpdateTelegram)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
try {
|
try {
|
||||||
@@ -168,10 +183,17 @@ export const notificationRouter = createTRPCRouter({
|
|||||||
message: "You are not authorized to update this notification",
|
message: "You are not authorized to update this notification",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return await updateTelegramNotification({
|
const result = await updateTelegramNotification({
|
||||||
...input,
|
...input,
|
||||||
organizationId: ctx.session.activeOrganizationId,
|
organizationId: ctx.session.activeOrganizationId,
|
||||||
});
|
});
|
||||||
|
await audit(ctx, {
|
||||||
|
action: "update",
|
||||||
|
resourceType: "notification",
|
||||||
|
resourceId: input.notificationId,
|
||||||
|
resourceName: notification.name,
|
||||||
|
});
|
||||||
|
return result;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "BAD_REQUEST",
|
code: "BAD_REQUEST",
|
||||||
@@ -180,7 +202,7 @@ export const notificationRouter = createTRPCRouter({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
testTelegramConnection: adminProcedure
|
testTelegramConnection: withPermission("notification", "create")
|
||||||
.input(apiTestTelegramConnection)
|
.input(apiTestTelegramConnection)
|
||||||
.mutation(async ({ input }) => {
|
.mutation(async ({ input }) => {
|
||||||
try {
|
try {
|
||||||
@@ -194,14 +216,19 @@ export const notificationRouter = createTRPCRouter({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
createDiscord: adminProcedure
|
createDiscord: withPermission("notification", "create")
|
||||||
.input(apiCreateDiscord)
|
.input(apiCreateDiscord)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
try {
|
try {
|
||||||
return await createDiscordNotification(
|
await createDiscordNotification(
|
||||||
input,
|
input,
|
||||||
ctx.session.activeOrganizationId,
|
ctx.session.activeOrganizationId,
|
||||||
);
|
);
|
||||||
|
await audit(ctx, {
|
||||||
|
action: "create",
|
||||||
|
resourceType: "notification",
|
||||||
|
resourceName: input.name,
|
||||||
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "BAD_REQUEST",
|
code: "BAD_REQUEST",
|
||||||
@@ -211,7 +238,7 @@ export const notificationRouter = createTRPCRouter({
|
|||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
|
||||||
updateDiscord: adminProcedure
|
updateDiscord: withPermission("notification", "update")
|
||||||
.input(apiUpdateDiscord)
|
.input(apiUpdateDiscord)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
try {
|
try {
|
||||||
@@ -222,10 +249,17 @@ export const notificationRouter = createTRPCRouter({
|
|||||||
message: "You are not authorized to update this notification",
|
message: "You are not authorized to update this notification",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return await updateDiscordNotification({
|
const result = await updateDiscordNotification({
|
||||||
...input,
|
...input,
|
||||||
organizationId: ctx.session.activeOrganizationId,
|
organizationId: ctx.session.activeOrganizationId,
|
||||||
});
|
});
|
||||||
|
await audit(ctx, {
|
||||||
|
action: "update",
|
||||||
|
resourceType: "notification",
|
||||||
|
resourceId: input.notificationId,
|
||||||
|
resourceName: notification.name,
|
||||||
|
});
|
||||||
|
return result;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "BAD_REQUEST",
|
code: "BAD_REQUEST",
|
||||||
@@ -235,7 +269,7 @@ export const notificationRouter = createTRPCRouter({
|
|||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
|
||||||
testDiscordConnection: adminProcedure
|
testDiscordConnection: withPermission("notification", "create")
|
||||||
.input(apiTestDiscordConnection)
|
.input(apiTestDiscordConnection)
|
||||||
.mutation(async ({ input }) => {
|
.mutation(async ({ input }) => {
|
||||||
try {
|
try {
|
||||||
@@ -257,14 +291,16 @@ export const notificationRouter = createTRPCRouter({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
createEmail: adminProcedure
|
createEmail: withPermission("notification", "create")
|
||||||
.input(apiCreateEmail)
|
.input(apiCreateEmail)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
try {
|
try {
|
||||||
return await createEmailNotification(
|
await createEmailNotification(input, ctx.session.activeOrganizationId);
|
||||||
input,
|
await audit(ctx, {
|
||||||
ctx.session.activeOrganizationId,
|
action: "create",
|
||||||
);
|
resourceType: "notification",
|
||||||
|
resourceName: input.name,
|
||||||
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "BAD_REQUEST",
|
code: "BAD_REQUEST",
|
||||||
@@ -273,7 +309,7 @@ export const notificationRouter = createTRPCRouter({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
updateEmail: adminProcedure
|
updateEmail: withPermission("notification", "update")
|
||||||
.input(apiUpdateEmail)
|
.input(apiUpdateEmail)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
try {
|
try {
|
||||||
@@ -284,10 +320,17 @@ export const notificationRouter = createTRPCRouter({
|
|||||||
message: "You are not authorized to update this notification",
|
message: "You are not authorized to update this notification",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return await updateEmailNotification({
|
const result = await updateEmailNotification({
|
||||||
...input,
|
...input,
|
||||||
organizationId: ctx.session.activeOrganizationId,
|
organizationId: ctx.session.activeOrganizationId,
|
||||||
});
|
});
|
||||||
|
await audit(ctx, {
|
||||||
|
action: "update",
|
||||||
|
resourceType: "notification",
|
||||||
|
resourceId: input.notificationId,
|
||||||
|
resourceName: notification.name,
|
||||||
|
});
|
||||||
|
return result;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "BAD_REQUEST",
|
code: "BAD_REQUEST",
|
||||||
@@ -296,7 +339,7 @@ export const notificationRouter = createTRPCRouter({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
testEmailConnection: adminProcedure
|
testEmailConnection: withPermission("notification", "create")
|
||||||
.input(apiTestEmailConnection)
|
.input(apiTestEmailConnection)
|
||||||
.mutation(async ({ input }) => {
|
.mutation(async ({ input }) => {
|
||||||
try {
|
try {
|
||||||
@@ -314,14 +357,16 @@ export const notificationRouter = createTRPCRouter({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
createResend: adminProcedure
|
createResend: withPermission("notification", "create")
|
||||||
.input(apiCreateResend)
|
.input(apiCreateResend)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
try {
|
try {
|
||||||
return await createResendNotification(
|
await createResendNotification(input, ctx.session.activeOrganizationId);
|
||||||
input,
|
await audit(ctx, {
|
||||||
ctx.session.activeOrganizationId,
|
action: "create",
|
||||||
);
|
resourceType: "notification",
|
||||||
|
resourceName: input.name,
|
||||||
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "BAD_REQUEST",
|
code: "BAD_REQUEST",
|
||||||
@@ -330,7 +375,7 @@ export const notificationRouter = createTRPCRouter({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
updateResend: adminProcedure
|
updateResend: withPermission("notification", "update")
|
||||||
.input(apiUpdateResend)
|
.input(apiUpdateResend)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
try {
|
try {
|
||||||
@@ -341,10 +386,17 @@ export const notificationRouter = createTRPCRouter({
|
|||||||
message: "You are not authorized to update this notification",
|
message: "You are not authorized to update this notification",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return await updateResendNotification({
|
const result = await updateResendNotification({
|
||||||
...input,
|
...input,
|
||||||
organizationId: ctx.session.activeOrganizationId,
|
organizationId: ctx.session.activeOrganizationId,
|
||||||
});
|
});
|
||||||
|
await audit(ctx, {
|
||||||
|
action: "update",
|
||||||
|
resourceType: "notification",
|
||||||
|
resourceId: input.notificationId,
|
||||||
|
resourceName: notification.name,
|
||||||
|
});
|
||||||
|
return result;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "BAD_REQUEST",
|
code: "BAD_REQUEST",
|
||||||
@@ -353,7 +405,7 @@ export const notificationRouter = createTRPCRouter({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
testResendConnection: adminProcedure
|
testResendConnection: withPermission("notification", "create")
|
||||||
.input(apiTestResendConnection)
|
.input(apiTestResendConnection)
|
||||||
.mutation(async ({ input }) => {
|
.mutation(async ({ input }) => {
|
||||||
try {
|
try {
|
||||||
@@ -371,7 +423,7 @@ export const notificationRouter = createTRPCRouter({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
remove: adminProcedure
|
remove: withPermission("notification", "delete")
|
||||||
.input(apiFindOneNotification)
|
.input(apiFindOneNotification)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
try {
|
try {
|
||||||
@@ -382,6 +434,11 @@ export const notificationRouter = createTRPCRouter({
|
|||||||
message: "You are not authorized to delete this notification",
|
message: "You are not authorized to delete this notification",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
await audit(ctx, {
|
||||||
|
action: "delete",
|
||||||
|
resourceType: "notification",
|
||||||
|
resourceName: notification.name,
|
||||||
|
});
|
||||||
return await removeNotificationById(input.notificationId);
|
return await removeNotificationById(input.notificationId);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const message =
|
const message =
|
||||||
@@ -394,7 +451,7 @@ export const notificationRouter = createTRPCRouter({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
one: protectedProcedure
|
one: withPermission("notification", "read")
|
||||||
.input(apiFindOneNotification)
|
.input(apiFindOneNotification)
|
||||||
.query(async ({ input, ctx }) => {
|
.query(async ({ input, ctx }) => {
|
||||||
const notification = await findNotificationById(input.notificationId);
|
const notification = await findNotificationById(input.notificationId);
|
||||||
@@ -406,7 +463,7 @@ export const notificationRouter = createTRPCRouter({
|
|||||||
}
|
}
|
||||||
return notification;
|
return notification;
|
||||||
}),
|
}),
|
||||||
all: adminProcedure.query(async ({ ctx }) => {
|
all: withPermission("notification", "read").query(async ({ ctx }) => {
|
||||||
return await db.query.notifications.findMany({
|
return await db.query.notifications.findMany({
|
||||||
with: {
|
with: {
|
||||||
slack: true,
|
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 = "";
|
organizationId = "";
|
||||||
ServerName = "Dokploy";
|
ServerName = "Dokploy";
|
||||||
} else {
|
} else {
|
||||||
@@ -488,14 +543,16 @@ export const notificationRouter = createTRPCRouter({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
createGotify: adminProcedure
|
createGotify: withPermission("notification", "create")
|
||||||
.input(apiCreateGotify)
|
.input(apiCreateGotify)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
try {
|
try {
|
||||||
return await createGotifyNotification(
|
await createGotifyNotification(input, ctx.session.activeOrganizationId);
|
||||||
input,
|
await audit(ctx, {
|
||||||
ctx.session.activeOrganizationId,
|
action: "create",
|
||||||
);
|
resourceType: "notification",
|
||||||
|
resourceName: input.name,
|
||||||
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "BAD_REQUEST",
|
code: "BAD_REQUEST",
|
||||||
@@ -504,7 +561,7 @@ export const notificationRouter = createTRPCRouter({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
updateGotify: adminProcedure
|
updateGotify: withPermission("notification", "update")
|
||||||
.input(apiUpdateGotify)
|
.input(apiUpdateGotify)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
try {
|
try {
|
||||||
@@ -518,15 +575,22 @@ export const notificationRouter = createTRPCRouter({
|
|||||||
message: "You are not authorized to update this notification",
|
message: "You are not authorized to update this notification",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return await updateGotifyNotification({
|
const result = await updateGotifyNotification({
|
||||||
...input,
|
...input,
|
||||||
organizationId: ctx.session.activeOrganizationId,
|
organizationId: ctx.session.activeOrganizationId,
|
||||||
});
|
});
|
||||||
|
await audit(ctx, {
|
||||||
|
action: "update",
|
||||||
|
resourceType: "notification",
|
||||||
|
resourceId: input.notificationId,
|
||||||
|
resourceName: notification.name,
|
||||||
|
});
|
||||||
|
return result;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
testGotifyConnection: adminProcedure
|
testGotifyConnection: withPermission("notification", "create")
|
||||||
.input(apiTestGotifyConnection)
|
.input(apiTestGotifyConnection)
|
||||||
.mutation(async ({ input }) => {
|
.mutation(async ({ input }) => {
|
||||||
try {
|
try {
|
||||||
@@ -544,14 +608,16 @@ export const notificationRouter = createTRPCRouter({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
createNtfy: adminProcedure
|
createNtfy: withPermission("notification", "create")
|
||||||
.input(apiCreateNtfy)
|
.input(apiCreateNtfy)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
try {
|
try {
|
||||||
return await createNtfyNotification(
|
await createNtfyNotification(input, ctx.session.activeOrganizationId);
|
||||||
input,
|
await audit(ctx, {
|
||||||
ctx.session.activeOrganizationId,
|
action: "create",
|
||||||
);
|
resourceType: "notification",
|
||||||
|
resourceName: input.name,
|
||||||
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "BAD_REQUEST",
|
code: "BAD_REQUEST",
|
||||||
@@ -560,7 +626,7 @@ export const notificationRouter = createTRPCRouter({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
updateNtfy: adminProcedure
|
updateNtfy: withPermission("notification", "update")
|
||||||
.input(apiUpdateNtfy)
|
.input(apiUpdateNtfy)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
try {
|
try {
|
||||||
@@ -574,15 +640,22 @@ export const notificationRouter = createTRPCRouter({
|
|||||||
message: "You are not authorized to update this notification",
|
message: "You are not authorized to update this notification",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return await updateNtfyNotification({
|
const result = await updateNtfyNotification({
|
||||||
...input,
|
...input,
|
||||||
organizationId: ctx.session.activeOrganizationId,
|
organizationId: ctx.session.activeOrganizationId,
|
||||||
});
|
});
|
||||||
|
await audit(ctx, {
|
||||||
|
action: "update",
|
||||||
|
resourceType: "notification",
|
||||||
|
resourceId: input.notificationId,
|
||||||
|
resourceName: notification.name,
|
||||||
|
});
|
||||||
|
return result;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
testNtfyConnection: adminProcedure
|
testNtfyConnection: withPermission("notification", "create")
|
||||||
.input(apiTestNtfyConnection)
|
.input(apiTestNtfyConnection)
|
||||||
.mutation(async ({ input }) => {
|
.mutation(async ({ input }) => {
|
||||||
try {
|
try {
|
||||||
@@ -602,14 +675,16 @@ export const notificationRouter = createTRPCRouter({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
createCustom: adminProcedure
|
createCustom: withPermission("notification", "create")
|
||||||
.input(apiCreateCustom)
|
.input(apiCreateCustom)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
try {
|
try {
|
||||||
return await createCustomNotification(
|
await createCustomNotification(input, ctx.session.activeOrganizationId);
|
||||||
input,
|
await audit(ctx, {
|
||||||
ctx.session.activeOrganizationId,
|
action: "create",
|
||||||
);
|
resourceType: "notification",
|
||||||
|
resourceName: input.name,
|
||||||
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "BAD_REQUEST",
|
code: "BAD_REQUEST",
|
||||||
@@ -618,7 +693,7 @@ export const notificationRouter = createTRPCRouter({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
updateCustom: adminProcedure
|
updateCustom: withPermission("notification", "update")
|
||||||
.input(apiUpdateCustom)
|
.input(apiUpdateCustom)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
try {
|
try {
|
||||||
@@ -629,15 +704,22 @@ export const notificationRouter = createTRPCRouter({
|
|||||||
message: "You are not authorized to update this notification",
|
message: "You are not authorized to update this notification",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return await updateCustomNotification({
|
const result = await updateCustomNotification({
|
||||||
...input,
|
...input,
|
||||||
organizationId: ctx.session.activeOrganizationId,
|
organizationId: ctx.session.activeOrganizationId,
|
||||||
});
|
});
|
||||||
|
await audit(ctx, {
|
||||||
|
action: "update",
|
||||||
|
resourceType: "notification",
|
||||||
|
resourceId: input.notificationId,
|
||||||
|
resourceName: notification.name,
|
||||||
|
});
|
||||||
|
return result;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
testCustomConnection: adminProcedure
|
testCustomConnection: withPermission("notification", "create")
|
||||||
.input(apiTestCustomConnection)
|
.input(apiTestCustomConnection)
|
||||||
.mutation(async ({ input }) => {
|
.mutation(async ({ input }) => {
|
||||||
try {
|
try {
|
||||||
@@ -655,14 +737,16 @@ export const notificationRouter = createTRPCRouter({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
createLark: adminProcedure
|
createLark: withPermission("notification", "create")
|
||||||
.input(apiCreateLark)
|
.input(apiCreateLark)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
try {
|
try {
|
||||||
return await createLarkNotification(
|
await createLarkNotification(input, ctx.session.activeOrganizationId);
|
||||||
input,
|
await audit(ctx, {
|
||||||
ctx.session.activeOrganizationId,
|
action: "create",
|
||||||
);
|
resourceType: "notification",
|
||||||
|
resourceName: input.name,
|
||||||
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "BAD_REQUEST",
|
code: "BAD_REQUEST",
|
||||||
@@ -671,7 +755,7 @@ export const notificationRouter = createTRPCRouter({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
updateLark: adminProcedure
|
updateLark: withPermission("notification", "update")
|
||||||
.input(apiUpdateLark)
|
.input(apiUpdateLark)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
try {
|
try {
|
||||||
@@ -685,15 +769,22 @@ export const notificationRouter = createTRPCRouter({
|
|||||||
message: "You are not authorized to update this notification",
|
message: "You are not authorized to update this notification",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return await updateLarkNotification({
|
const result = await updateLarkNotification({
|
||||||
...input,
|
...input,
|
||||||
organizationId: ctx.session.activeOrganizationId,
|
organizationId: ctx.session.activeOrganizationId,
|
||||||
});
|
});
|
||||||
|
await audit(ctx, {
|
||||||
|
action: "update",
|
||||||
|
resourceType: "notification",
|
||||||
|
resourceId: input.notificationId,
|
||||||
|
resourceName: notification.name,
|
||||||
|
});
|
||||||
|
return result;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
testLarkConnection: adminProcedure
|
testLarkConnection: withPermission("notification", "create")
|
||||||
.input(apiTestLarkConnection)
|
.input(apiTestLarkConnection)
|
||||||
.mutation(async ({ input }) => {
|
.mutation(async ({ input }) => {
|
||||||
try {
|
try {
|
||||||
@@ -712,14 +803,16 @@ export const notificationRouter = createTRPCRouter({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
createTeams: adminProcedure
|
createTeams: withPermission("notification", "create")
|
||||||
.input(apiCreateTeams)
|
.input(apiCreateTeams)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
try {
|
try {
|
||||||
return await createTeamsNotification(
|
await createTeamsNotification(input, ctx.session.activeOrganizationId);
|
||||||
input,
|
await audit(ctx, {
|
||||||
ctx.session.activeOrganizationId,
|
action: "create",
|
||||||
);
|
resourceType: "notification",
|
||||||
|
resourceName: input.name,
|
||||||
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "BAD_REQUEST",
|
code: "BAD_REQUEST",
|
||||||
@@ -728,7 +821,7 @@ export const notificationRouter = createTRPCRouter({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
updateTeams: adminProcedure
|
updateTeams: withPermission("notification", "update")
|
||||||
.input(apiUpdateTeams)
|
.input(apiUpdateTeams)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
try {
|
try {
|
||||||
@@ -742,15 +835,22 @@ export const notificationRouter = createTRPCRouter({
|
|||||||
message: "You are not authorized to update this notification",
|
message: "You are not authorized to update this notification",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return await updateTeamsNotification({
|
const result = await updateTeamsNotification({
|
||||||
...input,
|
...input,
|
||||||
organizationId: ctx.session.activeOrganizationId,
|
organizationId: ctx.session.activeOrganizationId,
|
||||||
});
|
});
|
||||||
|
await audit(ctx, {
|
||||||
|
action: "update",
|
||||||
|
resourceType: "notification",
|
||||||
|
resourceId: input.notificationId,
|
||||||
|
resourceName: notification.name,
|
||||||
|
});
|
||||||
|
return result;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
testTeamsConnection: adminProcedure
|
testTeamsConnection: withPermission("notification", "create")
|
||||||
.input(apiTestTeamsConnection)
|
.input(apiTestTeamsConnection)
|
||||||
.mutation(async ({ input }) => {
|
.mutation(async ({ input }) => {
|
||||||
try {
|
try {
|
||||||
@@ -767,14 +867,19 @@ export const notificationRouter = createTRPCRouter({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
createPushover: adminProcedure
|
createPushover: withPermission("notification", "create")
|
||||||
.input(apiCreatePushover)
|
.input(apiCreatePushover)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
try {
|
try {
|
||||||
return await createPushoverNotification(
|
await createPushoverNotification(
|
||||||
input,
|
input,
|
||||||
ctx.session.activeOrganizationId,
|
ctx.session.activeOrganizationId,
|
||||||
);
|
);
|
||||||
|
await audit(ctx, {
|
||||||
|
action: "create",
|
||||||
|
resourceType: "notification",
|
||||||
|
resourceName: input.name,
|
||||||
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "BAD_REQUEST",
|
code: "BAD_REQUEST",
|
||||||
@@ -783,7 +888,7 @@ export const notificationRouter = createTRPCRouter({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
updatePushover: adminProcedure
|
updatePushover: withPermission("notification", "update")
|
||||||
.input(apiUpdatePushover)
|
.input(apiUpdatePushover)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
try {
|
try {
|
||||||
@@ -797,15 +902,22 @@ export const notificationRouter = createTRPCRouter({
|
|||||||
message: "You are not authorized to update this notification",
|
message: "You are not authorized to update this notification",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return await updatePushoverNotification({
|
const result = await updatePushoverNotification({
|
||||||
...input,
|
...input,
|
||||||
organizationId: ctx.session.activeOrganizationId,
|
organizationId: ctx.session.activeOrganizationId,
|
||||||
});
|
});
|
||||||
|
await audit(ctx, {
|
||||||
|
action: "update",
|
||||||
|
resourceType: "notification",
|
||||||
|
resourceId: input.notificationId,
|
||||||
|
resourceName: notification.name,
|
||||||
|
});
|
||||||
|
return result;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
testPushoverConnection: adminProcedure
|
testPushoverConnection: withPermission("notification", "create")
|
||||||
.input(apiTestPushoverConnection)
|
.input(apiTestPushoverConnection)
|
||||||
.mutation(async ({ input }) => {
|
.mutation(async ({ input }) => {
|
||||||
try {
|
try {
|
||||||
@@ -823,13 +935,18 @@ export const notificationRouter = createTRPCRouter({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
getEmailProviders: adminProcedure.query(async ({ ctx }) => {
|
getEmailProviders: withPermission("notification", "read").query(
|
||||||
return await db.query.notifications.findMany({
|
async ({ ctx }) => {
|
||||||
where: eq(notifications.organizationId, ctx.session.activeOrganizationId),
|
return await db.query.notifications.findMany({
|
||||||
with: {
|
where: eq(
|
||||||
email: true,
|
notifications.organizationId,
|
||||||
resend: true,
|
ctx.session.activeOrganizationId,
|
||||||
},
|
),
|
||||||
});
|
with: {
|
||||||
}),
|
email: true,
|
||||||
|
resend: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,11 +1,18 @@
|
|||||||
import { db } from "@dokploy/server/db";
|
import { db } from "@dokploy/server/db";
|
||||||
import { IS_CLOUD } from "@dokploy/server/index";
|
import { IS_CLOUD } from "@dokploy/server/index";
|
||||||
|
import { audit } from "@/server/api/utils/audit";
|
||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
import { and, desc, eq, exists } from "drizzle-orm";
|
import { and, desc, eq, exists } from "drizzle-orm";
|
||||||
import { nanoid } from "nanoid";
|
import { nanoid } from "nanoid";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { invitation, member, organization } from "@/server/db/schema";
|
import {
|
||||||
import { adminProcedure, createTRPCRouter, protectedProcedure } from "../trpc";
|
invitation,
|
||||||
|
member,
|
||||||
|
organization,
|
||||||
|
organizationRole,
|
||||||
|
user,
|
||||||
|
} from "@/server/db/schema";
|
||||||
|
import { createTRPCRouter, protectedProcedure, withPermission } from "../trpc";
|
||||||
export const organizationRouter = createTRPCRouter({
|
export const organizationRouter = createTRPCRouter({
|
||||||
create: protectedProcedure
|
create: protectedProcedure
|
||||||
.input(
|
.input(
|
||||||
@@ -50,6 +57,12 @@ export const organizationRouter = createTRPCRouter({
|
|||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
userId: ctx.user.id,
|
userId: ctx.user.id,
|
||||||
});
|
});
|
||||||
|
await audit(ctx, {
|
||||||
|
action: "create",
|
||||||
|
resourceType: "organization",
|
||||||
|
resourceId: result.id,
|
||||||
|
resourceName: result.name,
|
||||||
|
});
|
||||||
return result;
|
return result;
|
||||||
}),
|
}),
|
||||||
all: protectedProcedure.query(async ({ ctx }) => {
|
all: protectedProcedure.query(async ({ ctx }) => {
|
||||||
@@ -156,6 +169,12 @@ export const organizationRouter = createTRPCRouter({
|
|||||||
})
|
})
|
||||||
.where(eq(organization.id, input.organizationId))
|
.where(eq(organization.id, input.organizationId))
|
||||||
.returning();
|
.returning();
|
||||||
|
await audit(ctx, {
|
||||||
|
action: "update",
|
||||||
|
resourceType: "organization",
|
||||||
|
resourceId: input.organizationId,
|
||||||
|
resourceName: input.name,
|
||||||
|
});
|
||||||
return result[0];
|
return result[0];
|
||||||
}),
|
}),
|
||||||
delete: protectedProcedure
|
delete: protectedProcedure
|
||||||
@@ -220,15 +239,109 @@ export const organizationRouter = createTRPCRouter({
|
|||||||
.delete(organization)
|
.delete(organization)
|
||||||
.where(eq(organization.id, input.organizationId));
|
.where(eq(organization.id, input.organizationId));
|
||||||
|
|
||||||
|
await audit(ctx, {
|
||||||
|
action: "delete",
|
||||||
|
resourceType: "organization",
|
||||||
|
resourceId: input.organizationId,
|
||||||
|
resourceName: org.name,
|
||||||
|
});
|
||||||
return result;
|
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({
|
return await db.query.invitation.findMany({
|
||||||
where: eq(invitation.organizationId, ctx.session.activeOrganizationId),
|
where: eq(invitation.organizationId, ctx.session.activeOrganizationId),
|
||||||
orderBy: [desc(invitation.status), desc(invitation.expiresAt)],
|
orderBy: [desc(invitation.status), desc(invitation.expiresAt)],
|
||||||
});
|
});
|
||||||
}),
|
}),
|
||||||
removeInvitation: adminProcedure
|
removeInvitation: withPermission("member", "create")
|
||||||
.input(z.object({ invitationId: z.string() }))
|
.input(z.object({ invitationId: z.string() }))
|
||||||
.mutation(async ({ ctx, input }) => {
|
.mutation(async ({ ctx, input }) => {
|
||||||
const invitationResult = await db.query.invitation.findFirst({
|
const invitationResult = await db.query.invitation.findFirst({
|
||||||
@@ -251,15 +364,23 @@ export const organizationRouter = createTRPCRouter({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return await db
|
const result = await db
|
||||||
.delete(invitation)
|
.delete(invitation)
|
||||||
.where(eq(invitation.id, input.invitationId));
|
.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(
|
.input(
|
||||||
z.object({
|
z.object({
|
||||||
memberId: z.string(),
|
memberId: z.string(),
|
||||||
role: z.enum(["admin", "member"]),
|
role: z.string().min(1),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.mutation(async ({ ctx, input }) => {
|
.mutation(async ({ ctx, input }) => {
|
||||||
@@ -289,7 +410,7 @@ export const organizationRouter = createTRPCRouter({
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Owner role is intransferible - cannot change to or from owner
|
// Owner role is intransferible - cannot change to or from owner
|
||||||
if (target.role === "owner") {
|
if (target.role === "owner" || input.role === "owner") {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "FORBIDDEN",
|
code: "FORBIDDEN",
|
||||||
message: "The owner role is intransferible",
|
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
|
// Update the target member's role
|
||||||
await db
|
await db
|
||||||
.update(member)
|
.update(member)
|
||||||
.set({ role: input.role })
|
.set({ role: input.role })
|
||||||
.where(eq(member.id, input.memberId));
|
.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;
|
return true;
|
||||||
}),
|
}),
|
||||||
setDefault: protectedProcedure
|
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 };
|
return { success: true };
|
||||||
}),
|
}),
|
||||||
active: protectedProcedure.query(async ({ ctx }) => {
|
active: protectedProcedure.query(async ({ ctx }) => {
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import {
|
import {
|
||||||
checkServiceAccess,
|
|
||||||
cleanPatchRepos,
|
cleanPatchRepos,
|
||||||
createPatch,
|
createPatch,
|
||||||
deletePatch,
|
deletePatch,
|
||||||
@@ -14,6 +13,7 @@ import {
|
|||||||
readPatchRepoFile,
|
readPatchRepoFile,
|
||||||
updatePatch,
|
updatePatch,
|
||||||
} from "@dokploy/server";
|
} from "@dokploy/server";
|
||||||
|
import { checkServicePermissionAndAccess } from "@dokploy/server/services/permission";
|
||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import {
|
import {
|
||||||
@@ -21,6 +21,7 @@ import {
|
|||||||
createTRPCRouter,
|
createTRPCRouter,
|
||||||
protectedProcedure,
|
protectedProcedure,
|
||||||
} from "@/server/api/trpc";
|
} from "@/server/api/trpc";
|
||||||
|
import { audit } from "@/server/api/utils/audit";
|
||||||
import {
|
import {
|
||||||
apiCreatePatch,
|
apiCreatePatch,
|
||||||
apiDeletePatch,
|
apiDeletePatch,
|
||||||
@@ -29,47 +30,56 @@ import {
|
|||||||
apiUpdatePatch,
|
apiUpdatePatch,
|
||||||
} from "@/server/db/schema";
|
} 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({
|
export const patchRouter = createTRPCRouter({
|
||||||
create: protectedProcedure
|
create: protectedProcedure
|
||||||
.input(apiCreatePatch)
|
.input(apiCreatePatch)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
if (input.applicationId) {
|
const serviceId = input.applicationId ?? input.composeId;
|
||||||
const app = await findApplicationById(input.applicationId);
|
if (!serviceId) {
|
||||||
if (
|
throw new TRPCError({
|
||||||
app.environment.project.organizationId !==
|
code: "BAD_REQUEST",
|
||||||
ctx.session.activeOrganizationId
|
message: "Either applicationId or composeId must be provided",
|
||||||
) {
|
});
|
||||||
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",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
await checkServicePermissionAndAccess(ctx, serviceId, {
|
||||||
return await createPatch(input);
|
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 }) => {
|
one: protectedProcedure.input(apiFindPatch).query(async ({ input, ctx }) => {
|
||||||
return await findPatchById(input.patchId);
|
const patch = await findPatchById(input.patchId);
|
||||||
|
const serviceId = resolvePatchServiceId(patch);
|
||||||
|
await checkServicePermissionAndAccess(ctx, serviceId, {
|
||||||
|
service: ["read"],
|
||||||
|
});
|
||||||
|
return patch;
|
||||||
}),
|
}),
|
||||||
|
|
||||||
byEntityId: protectedProcedure
|
byEntityId: protectedProcedure
|
||||||
@@ -77,51 +87,70 @@ export const patchRouter = createTRPCRouter({
|
|||||||
z.object({ id: z.string(), type: z.enum(["application", "compose"]) }),
|
z.object({ id: z.string(), type: z.enum(["application", "compose"]) }),
|
||||||
)
|
)
|
||||||
.query(async ({ input, ctx }) => {
|
.query(async ({ input, ctx }) => {
|
||||||
if (input.type === "application") {
|
await checkServicePermissionAndAccess(ctx, input.id, {
|
||||||
const app = await findApplicationById(input.id);
|
service: ["read"],
|
||||||
if (
|
});
|
||||||
app.environment.project.organizationId !==
|
return await findPatchesByEntityId(input.id, input.type);
|
||||||
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;
|
|
||||||
}),
|
}),
|
||||||
|
|
||||||
update: protectedProcedure
|
update: protectedProcedure
|
||||||
.input(apiUpdatePatch)
|
.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;
|
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
|
delete: protectedProcedure
|
||||||
.input(apiDeletePatch)
|
.input(apiDeletePatch)
|
||||||
.mutation(async ({ input }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
return await deletePatch(input.patchId);
|
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
|
toggleEnabled: protectedProcedure
|
||||||
.input(apiTogglePatchEnabled)
|
.input(apiTogglePatchEnabled)
|
||||||
.mutation(async ({ input }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
return await updatePatch(input.patchId, { enabled: input.enabled });
|
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
|
// Repository Operations
|
||||||
@@ -132,11 +161,21 @@ export const patchRouter = createTRPCRouter({
|
|||||||
type: z.enum(["application", "compose"]),
|
type: z.enum(["application", "compose"]),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.mutation(async ({ input }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
return await ensurePatchRepo({
|
await checkServicePermissionAndAccess(ctx, input.id, {
|
||||||
|
service: ["create"],
|
||||||
|
});
|
||||||
|
const result = await ensurePatchRepo({
|
||||||
type: input.type,
|
type: input.type,
|
||||||
id: input.id,
|
id: input.id,
|
||||||
});
|
});
|
||||||
|
await audit(ctx, {
|
||||||
|
action: "create",
|
||||||
|
resourceType: "settings",
|
||||||
|
resourceId: input.id,
|
||||||
|
metadata: { type: "ensurePatchRepo", serviceType: input.type },
|
||||||
|
});
|
||||||
|
return result;
|
||||||
}),
|
}),
|
||||||
|
|
||||||
readRepoDirectories: protectedProcedure
|
readRepoDirectories: protectedProcedure
|
||||||
@@ -148,36 +187,17 @@ export const patchRouter = createTRPCRouter({
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.query(async ({ input, ctx }) => {
|
.query(async ({ input, ctx }) => {
|
||||||
|
await checkServicePermissionAndAccess(ctx, input.id, {
|
||||||
|
service: ["read"],
|
||||||
|
});
|
||||||
let serverId: string | null = null;
|
let serverId: string | null = null;
|
||||||
|
|
||||||
if (input.type === "application") {
|
if (input.type === "application") {
|
||||||
const app = await findApplicationById(input.id);
|
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;
|
serverId = app.serverId;
|
||||||
}
|
} else {
|
||||||
|
|
||||||
if (input.type === "compose") {
|
|
||||||
const compose = await findComposeById(input.id);
|
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;
|
serverId = compose.serverId;
|
||||||
}
|
}
|
||||||
|
|
||||||
return await readPatchRepoDirectory(input.repoPath, serverId);
|
return await readPatchRepoDirectory(input.repoPath, serverId);
|
||||||
}),
|
}),
|
||||||
|
|
||||||
@@ -190,44 +210,22 @@ export const patchRouter = createTRPCRouter({
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.query(async ({ input, ctx }) => {
|
.query(async ({ input, ctx }) => {
|
||||||
|
await checkServicePermissionAndAccess(ctx, input.id, {
|
||||||
|
service: ["read"],
|
||||||
|
});
|
||||||
let serverId: string | null = null;
|
let serverId: string | null = null;
|
||||||
|
|
||||||
if (input.type === "application") {
|
if (input.type === "application") {
|
||||||
const app = await findApplicationById(input.id);
|
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;
|
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 {
|
} else {
|
||||||
throw new TRPCError({
|
const compose = await findComposeById(input.id);
|
||||||
code: "BAD_REQUEST",
|
serverId = compose.serverId;
|
||||||
message: "Either applicationId or composeId must be provided",
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
const existingPatch = await findPatchByFilePath(
|
const existingPatch = await findPatchByFilePath(
|
||||||
input.filePath,
|
input.filePath,
|
||||||
input.id,
|
input.id,
|
||||||
input.type,
|
input.type,
|
||||||
);
|
);
|
||||||
|
|
||||||
// For delete patches, show current file content from repo (what will be deleted)
|
// For delete patches, show current file content from repo (what will be deleted)
|
||||||
if (existingPatch?.type === "delete") {
|
if (existingPatch?.type === "delete") {
|
||||||
try {
|
try {
|
||||||
@@ -253,55 +251,43 @@ export const patchRouter = createTRPCRouter({
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
if (input.type === "application") {
|
await checkServicePermissionAndAccess(ctx, input.id, {
|
||||||
const app = await findApplicationById(input.id);
|
service: ["create"],
|
||||||
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",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const existingPatch = await findPatchByFilePath(
|
const existingPatch = await findPatchByFilePath(
|
||||||
input.filePath,
|
input.filePath,
|
||||||
input.id,
|
input.id,
|
||||||
input.type,
|
input.type,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!existingPatch) {
|
if (!existingPatch) {
|
||||||
return await createPatch({
|
const result = await createPatch({
|
||||||
filePath: input.filePath,
|
filePath: input.filePath,
|
||||||
content: input.content,
|
content: input.content,
|
||||||
type: input.patchType,
|
type: input.patchType,
|
||||||
applicationId: input.type === "application" ? input.id : undefined,
|
applicationId: input.type === "application" ? input.id : undefined,
|
||||||
composeId: input.type === "compose" ? 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;
|
||||||
}
|
}
|
||||||
|
const result = await updatePatch(existingPatch.patchId, {
|
||||||
return await updatePatch(existingPatch.patchId, {
|
|
||||||
content: input.content,
|
content: input.content,
|
||||||
type: input.patchType,
|
type: input.patchType,
|
||||||
});
|
});
|
||||||
|
await audit(ctx, {
|
||||||
|
action: "update",
|
||||||
|
resourceType: "settings",
|
||||||
|
resourceId: existingPatch.patchId,
|
||||||
|
resourceName: input.filePath,
|
||||||
|
metadata: { type: "saveFileAsPatch" },
|
||||||
|
});
|
||||||
|
return result;
|
||||||
}),
|
}),
|
||||||
|
|
||||||
markFileForDeletion: protectedProcedure
|
markFileForDeletion: protectedProcedure
|
||||||
@@ -313,36 +299,34 @@ export const patchRouter = createTRPCRouter({
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
if (input.type === "application") {
|
await checkServicePermissionAndAccess(ctx, input.id, {
|
||||||
const app = await findApplicationById(input.id);
|
service: ["create"],
|
||||||
if (
|
});
|
||||||
app.environment.project.organizationId !==
|
const result = await markPatchForDeletion(
|
||||||
ctx.session.activeOrganizationId
|
input.filePath,
|
||||||
) {
|
input.id,
|
||||||
throw new TRPCError({
|
input.type,
|
||||||
code: "UNAUTHORIZED",
|
);
|
||||||
message: "You are not authorized to access this application",
|
await audit(ctx, {
|
||||||
});
|
action: "delete",
|
||||||
}
|
resourceType: "settings",
|
||||||
} else if (input.type === "compose") {
|
resourceId: input.id,
|
||||||
const compose = await findComposeById(input.id);
|
resourceName: input.filePath,
|
||||||
if (
|
metadata: { type: "markFileForDeletion" },
|
||||||
compose.environment.project.organizationId !==
|
});
|
||||||
ctx.session.activeOrganizationId
|
return result;
|
||||||
) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "UNAUTHORIZED",
|
|
||||||
message: "You are not authorized to access this compose",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return await markPatchForDeletion(input.filePath, input.id, input.type);
|
|
||||||
}),
|
}),
|
||||||
|
|
||||||
cleanPatchRepos: adminProcedure
|
cleanPatchRepos: adminProcedure
|
||||||
.input(z.object({ serverId: z.string().optional() }))
|
.input(z.object({ serverId: z.string().optional() }))
|
||||||
.mutation(async ({ input }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
await cleanPatchRepos(input.serverId);
|
await cleanPatchRepos(input.serverId);
|
||||||
|
await audit(ctx, {
|
||||||
|
action: "delete",
|
||||||
|
resourceType: "settings",
|
||||||
|
resourceId: input.serverId || "local",
|
||||||
|
metadata: { type: "cleanPatchRepos" },
|
||||||
|
});
|
||||||
return true;
|
return true;
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -4,8 +4,10 @@ import {
|
|||||||
removePortById,
|
removePortById,
|
||||||
updatePortById,
|
updatePortById,
|
||||||
} from "@dokploy/server";
|
} from "@dokploy/server";
|
||||||
|
import { checkServicePermissionAndAccess } from "@dokploy/server/services/permission";
|
||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc";
|
import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc";
|
||||||
|
import { audit } from "@/server/api/utils/audit";
|
||||||
import {
|
import {
|
||||||
apiCreatePort,
|
apiCreatePort,
|
||||||
apiFindOnePort,
|
apiFindOnePort,
|
||||||
@@ -15,10 +17,19 @@ import {
|
|||||||
export const portRouter = createTRPCRouter({
|
export const portRouter = createTRPCRouter({
|
||||||
create: protectedProcedure
|
create: protectedProcedure
|
||||||
.input(apiCreatePort)
|
.input(apiCreatePort)
|
||||||
.mutation(async ({ input }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
try {
|
try {
|
||||||
await createPort(input);
|
await checkServicePermissionAndAccess(ctx, input.applicationId, {
|
||||||
return true;
|
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) {
|
} catch (error) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "BAD_REQUEST",
|
code: "BAD_REQUEST",
|
||||||
@@ -32,15 +43,11 @@ export const portRouter = createTRPCRouter({
|
|||||||
.query(async ({ input, ctx }) => {
|
.query(async ({ input, ctx }) => {
|
||||||
try {
|
try {
|
||||||
const port = await finPortById(input.portId);
|
const port = await finPortById(input.portId);
|
||||||
if (
|
await checkServicePermissionAndAccess(
|
||||||
port.application.environment.project.organizationId !==
|
ctx,
|
||||||
ctx.session.activeOrganizationId
|
port.application.applicationId,
|
||||||
) {
|
{ service: ["read"] },
|
||||||
throw new TRPCError({
|
);
|
||||||
code: "UNAUTHORIZED",
|
|
||||||
message: "You are not authorized to access this port",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return port;
|
return port;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
@@ -54,17 +61,20 @@ export const portRouter = createTRPCRouter({
|
|||||||
.input(apiFindOnePort)
|
.input(apiFindOnePort)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
const port = await finPortById(input.portId);
|
const port = await finPortById(input.portId);
|
||||||
if (
|
await checkServicePermissionAndAccess(
|
||||||
port.application.environment.project.organizationId !==
|
ctx,
|
||||||
ctx.session.activeOrganizationId
|
port.application.applicationId,
|
||||||
) {
|
{ service: ["delete"] },
|
||||||
throw new TRPCError({
|
);
|
||||||
code: "UNAUTHORIZED",
|
|
||||||
message: "You are not authorized to delete this port",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
try {
|
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) {
|
} catch (error) {
|
||||||
const message =
|
const message =
|
||||||
error instanceof Error ? error.message : "Error input: Deleting port";
|
error instanceof Error ? error.message : "Error input: Deleting port";
|
||||||
@@ -78,17 +88,20 @@ export const portRouter = createTRPCRouter({
|
|||||||
.input(apiUpdatePort)
|
.input(apiUpdatePort)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
const port = await finPortById(input.portId);
|
const port = await finPortById(input.portId);
|
||||||
if (
|
await checkServicePermissionAndAccess(
|
||||||
port.application.environment.project.organizationId !==
|
ctx,
|
||||||
ctx.session.activeOrganizationId
|
port.application.applicationId,
|
||||||
) {
|
{ service: ["create"] },
|
||||||
throw new TRPCError({
|
);
|
||||||
code: "UNAUTHORIZED",
|
|
||||||
message: "You are not authorized to update this port",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
try {
|
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) {
|
} catch (error) {
|
||||||
const message =
|
const message =
|
||||||
error instanceof Error ? error.message : "Error updating the port";
|
error instanceof Error ? error.message : "Error updating the port";
|
||||||
|
|||||||
@@ -1,13 +1,10 @@
|
|||||||
import {
|
import {
|
||||||
addNewService,
|
|
||||||
checkPortInUse,
|
checkPortInUse,
|
||||||
checkServiceAccess,
|
|
||||||
createMount,
|
createMount,
|
||||||
createPostgres,
|
createPostgres,
|
||||||
deployPostgres,
|
deployPostgres,
|
||||||
findBackupsByDbId,
|
findBackupsByDbId,
|
||||||
findEnvironmentById,
|
findEnvironmentById,
|
||||||
findMemberById,
|
|
||||||
findPostgresById,
|
findPostgresById,
|
||||||
findProjectById,
|
findProjectById,
|
||||||
getMountPath,
|
getMountPath,
|
||||||
@@ -21,10 +18,17 @@ import {
|
|||||||
stopServiceRemote,
|
stopServiceRemote,
|
||||||
updatePostgresById,
|
updatePostgresById,
|
||||||
} from "@dokploy/server";
|
} from "@dokploy/server";
|
||||||
|
import {
|
||||||
|
addNewService,
|
||||||
|
checkServiceAccess,
|
||||||
|
checkServicePermissionAndAccess,
|
||||||
|
findMemberByUserId,
|
||||||
|
} from "@dokploy/server/services/permission";
|
||||||
import { db } from "@dokploy/server/db";
|
import { db } from "@dokploy/server/db";
|
||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
import { and, desc, eq, ilike, or, sql } from "drizzle-orm";
|
import { and, desc, eq, ilike, or, sql } from "drizzle-orm";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
import { audit } from "@/server/api/utils/audit";
|
||||||
import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc";
|
import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc";
|
||||||
import {
|
import {
|
||||||
apiChangePostgresStatus,
|
apiChangePostgresStatus,
|
||||||
@@ -46,18 +50,10 @@ export const postgresRouter = createTRPCRouter({
|
|||||||
.input(apiCreatePostgres)
|
.input(apiCreatePostgres)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
try {
|
try {
|
||||||
// Get project from environment
|
|
||||||
const environment = await findEnvironmentById(input.environmentId);
|
const environment = await findEnvironmentById(input.environmentId);
|
||||||
const project = await findProjectById(environment.projectId);
|
const project = await findProjectById(environment.projectId);
|
||||||
|
|
||||||
if (ctx.user.role === "member") {
|
await checkServiceAccess(ctx, project.projectId, "create");
|
||||||
await checkServiceAccess(
|
|
||||||
ctx.user.id,
|
|
||||||
project.projectId,
|
|
||||||
ctx.session.activeOrganizationId,
|
|
||||||
"create",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (IS_CLOUD && !input.serverId) {
|
if (IS_CLOUD && !input.serverId) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
@@ -75,13 +71,7 @@ export const postgresRouter = createTRPCRouter({
|
|||||||
const newPostgres = await createPostgres({
|
const newPostgres = await createPostgres({
|
||||||
...input,
|
...input,
|
||||||
});
|
});
|
||||||
if (ctx.user.role === "member") {
|
await addNewService(ctx, newPostgres.postgresId);
|
||||||
await addNewService(
|
|
||||||
ctx.user.id,
|
|
||||||
newPostgres.postgresId,
|
|
||||||
project.organizationId,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const mountPath = getMountPath(input.dockerImage);
|
const mountPath = getMountPath(input.dockerImage);
|
||||||
|
|
||||||
@@ -93,6 +83,12 @@ export const postgresRouter = createTRPCRouter({
|
|||||||
type: "volume",
|
type: "volume",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await audit(ctx, {
|
||||||
|
action: "create",
|
||||||
|
resourceType: "service",
|
||||||
|
resourceId: newPostgres.postgresId,
|
||||||
|
resourceName: newPostgres.appName,
|
||||||
|
});
|
||||||
return newPostgres;
|
return newPostgres;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof TRPCError) {
|
if (error instanceof TRPCError) {
|
||||||
@@ -108,14 +104,7 @@ export const postgresRouter = createTRPCRouter({
|
|||||||
one: protectedProcedure
|
one: protectedProcedure
|
||||||
.input(apiFindOnePostgres)
|
.input(apiFindOnePostgres)
|
||||||
.query(async ({ input, ctx }) => {
|
.query(async ({ input, ctx }) => {
|
||||||
if (ctx.user.role === "member") {
|
await checkServiceAccess(ctx, input.postgresId, "read");
|
||||||
await checkServiceAccess(
|
|
||||||
ctx.user.id,
|
|
||||||
input.postgresId,
|
|
||||||
ctx.session.activeOrganizationId,
|
|
||||||
"access",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const postgres = await findPostgresById(input.postgresId);
|
const postgres = await findPostgresById(input.postgresId);
|
||||||
if (
|
if (
|
||||||
@@ -133,18 +122,11 @@ export const postgresRouter = createTRPCRouter({
|
|||||||
start: protectedProcedure
|
start: protectedProcedure
|
||||||
.input(apiFindOnePostgres)
|
.input(apiFindOnePostgres)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
|
await checkServicePermissionAndAccess(ctx, input.postgresId, {
|
||||||
|
deployment: ["create"],
|
||||||
|
});
|
||||||
const service = await findPostgresById(input.postgresId);
|
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) {
|
if (service.serverId) {
|
||||||
await startServiceRemote(service.serverId, service.appName);
|
await startServiceRemote(service.serverId, service.appName);
|
||||||
} else {
|
} else {
|
||||||
@@ -154,21 +136,21 @@ export const postgresRouter = createTRPCRouter({
|
|||||||
applicationStatus: "done",
|
applicationStatus: "done",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await audit(ctx, {
|
||||||
|
action: "start",
|
||||||
|
resourceType: "service",
|
||||||
|
resourceId: service.postgresId,
|
||||||
|
resourceName: service.appName,
|
||||||
|
});
|
||||||
return service;
|
return service;
|
||||||
}),
|
}),
|
||||||
stop: protectedProcedure
|
stop: protectedProcedure
|
||||||
.input(apiFindOnePostgres)
|
.input(apiFindOnePostgres)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
|
await checkServicePermissionAndAccess(ctx, input.postgresId, {
|
||||||
|
deployment: ["create"],
|
||||||
|
});
|
||||||
const postgres = await findPostgresById(input.postgresId);
|
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) {
|
if (postgres.serverId) {
|
||||||
await stopServiceRemote(postgres.serverId, postgres.appName);
|
await stopServiceRemote(postgres.serverId, postgres.appName);
|
||||||
} else {
|
} else {
|
||||||
@@ -178,23 +160,22 @@ export const postgresRouter = createTRPCRouter({
|
|||||||
applicationStatus: "idle",
|
applicationStatus: "idle",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await audit(ctx, {
|
||||||
|
action: "stop",
|
||||||
|
resourceType: "service",
|
||||||
|
resourceId: postgres.postgresId,
|
||||||
|
resourceName: postgres.appName,
|
||||||
|
});
|
||||||
return postgres;
|
return postgres;
|
||||||
}),
|
}),
|
||||||
saveExternalPort: protectedProcedure
|
saveExternalPort: protectedProcedure
|
||||||
.input(apiSaveExternalPortPostgres)
|
.input(apiSaveExternalPortPostgres)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
|
await checkServicePermissionAndAccess(ctx, input.postgresId, {
|
||||||
|
service: ["create"],
|
||||||
|
});
|
||||||
const postgres = await findPostgresById(input.postgresId);
|
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) {
|
if (input.externalPort) {
|
||||||
const portCheck = await checkPortInUse(
|
const portCheck = await checkPortInUse(
|
||||||
input.externalPort,
|
input.externalPort,
|
||||||
@@ -212,21 +193,27 @@ export const postgresRouter = createTRPCRouter({
|
|||||||
externalPort: input.externalPort,
|
externalPort: input.externalPort,
|
||||||
});
|
});
|
||||||
await deployPostgres(input.postgresId);
|
await deployPostgres(input.postgresId);
|
||||||
|
await audit(ctx, {
|
||||||
|
action: "update",
|
||||||
|
resourceType: "service",
|
||||||
|
resourceId: postgres.postgresId,
|
||||||
|
resourceName: postgres.appName,
|
||||||
|
});
|
||||||
return postgres;
|
return postgres;
|
||||||
}),
|
}),
|
||||||
deploy: protectedProcedure
|
deploy: protectedProcedure
|
||||||
.input(apiDeployPostgres)
|
.input(apiDeployPostgres)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
|
await checkServicePermissionAndAccess(ctx, input.postgresId, {
|
||||||
|
deployment: ["create"],
|
||||||
|
});
|
||||||
const postgres = await findPostgresById(input.postgresId);
|
const postgres = await findPostgresById(input.postgresId);
|
||||||
if (
|
await audit(ctx, {
|
||||||
postgres.environment.project.organizationId !==
|
action: "deploy",
|
||||||
ctx.session.activeOrganizationId
|
resourceType: "service",
|
||||||
) {
|
resourceId: postgres.postgresId,
|
||||||
throw new TRPCError({
|
resourceName: postgres.appName,
|
||||||
code: "UNAUTHORIZED",
|
});
|
||||||
message: "You are not authorized to deploy this Postgres",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return deployPostgres(input.postgresId);
|
return deployPostgres(input.postgresId);
|
||||||
}),
|
}),
|
||||||
|
|
||||||
@@ -241,17 +228,9 @@ export const postgresRouter = createTRPCRouter({
|
|||||||
})
|
})
|
||||||
.input(apiDeployPostgres)
|
.input(apiDeployPostgres)
|
||||||
.subscription(async function* ({ input, ctx, signal }) {
|
.subscription(async function* ({ input, ctx, signal }) {
|
||||||
const postgres = await findPostgresById(input.postgresId);
|
await checkServicePermissionAndAccess(ctx, input.postgresId, {
|
||||||
|
deployment: ["create"],
|
||||||
if (
|
});
|
||||||
postgres.environment.project.organizationId !==
|
|
||||||
ctx.session.activeOrganizationId
|
|
||||||
) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "UNAUTHORIZED",
|
|
||||||
message: "You are not authorized to deploy this Postgres",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const queue: string[] = [];
|
const queue: string[] = [];
|
||||||
const done = false;
|
const done = false;
|
||||||
@@ -276,32 +255,25 @@ export const postgresRouter = createTRPCRouter({
|
|||||||
changeStatus: protectedProcedure
|
changeStatus: protectedProcedure
|
||||||
.input(apiChangePostgresStatus)
|
.input(apiChangePostgresStatus)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
|
await checkServicePermissionAndAccess(ctx, input.postgresId, {
|
||||||
|
deployment: ["create"],
|
||||||
|
});
|
||||||
const postgres = await findPostgresById(input.postgresId);
|
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, {
|
await updatePostgresById(input.postgresId, {
|
||||||
applicationStatus: input.applicationStatus,
|
applicationStatus: input.applicationStatus,
|
||||||
});
|
});
|
||||||
|
await audit(ctx, {
|
||||||
|
action: "update",
|
||||||
|
resourceType: "service",
|
||||||
|
resourceId: postgres.postgresId,
|
||||||
|
resourceName: postgres.appName,
|
||||||
|
});
|
||||||
return postgres;
|
return postgres;
|
||||||
}),
|
}),
|
||||||
remove: protectedProcedure
|
remove: protectedProcedure
|
||||||
.input(apiFindOnePostgres)
|
.input(apiFindOnePostgres)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
if (ctx.user.role === "member") {
|
await checkServiceAccess(ctx, input.postgresId, "delete");
|
||||||
await checkServiceAccess(
|
|
||||||
ctx.user.id,
|
|
||||||
input.postgresId,
|
|
||||||
ctx.session.activeOrganizationId,
|
|
||||||
"delete",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
const postgres = await findPostgresById(input.postgresId);
|
const postgres = await findPostgresById(input.postgresId);
|
||||||
|
|
||||||
if (
|
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 backups = await findBackupsByDbId(input.postgresId, "postgres");
|
||||||
|
|
||||||
const cleanupOperations = [
|
const cleanupOperations = [
|
||||||
@@ -333,16 +311,9 @@ export const postgresRouter = createTRPCRouter({
|
|||||||
saveEnvironment: protectedProcedure
|
saveEnvironment: protectedProcedure
|
||||||
.input(apiSaveEnvironmentVariablesPostgres)
|
.input(apiSaveEnvironmentVariablesPostgres)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
const postgres = await findPostgresById(input.postgresId);
|
await checkServicePermissionAndAccess(ctx, input.postgresId, {
|
||||||
if (
|
envVars: ["write"],
|
||||||
postgres.environment.project.organizationId !==
|
});
|
||||||
ctx.session.activeOrganizationId
|
|
||||||
) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "UNAUTHORIZED",
|
|
||||||
message: "You are not authorized to save this environment",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
const service = await updatePostgresById(input.postgresId, {
|
const service = await updatePostgresById(input.postgresId, {
|
||||||
env: input.env,
|
env: input.env,
|
||||||
});
|
});
|
||||||
@@ -354,21 +325,20 @@ export const postgresRouter = createTRPCRouter({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await audit(ctx, {
|
||||||
|
action: "update",
|
||||||
|
resourceType: "service",
|
||||||
|
resourceId: input.postgresId,
|
||||||
|
});
|
||||||
return true;
|
return true;
|
||||||
}),
|
}),
|
||||||
reload: protectedProcedure
|
reload: protectedProcedure
|
||||||
.input(apiResetPostgres)
|
.input(apiResetPostgres)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
|
await checkServicePermissionAndAccess(ctx, input.postgresId, {
|
||||||
|
deployment: ["create"],
|
||||||
|
});
|
||||||
const postgres = await findPostgresById(input.postgresId);
|
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) {
|
if (postgres.serverId) {
|
||||||
await stopServiceRemote(postgres.serverId, postgres.appName);
|
await stopServiceRemote(postgres.serverId, postgres.appName);
|
||||||
} else {
|
} else {
|
||||||
@@ -386,22 +356,21 @@ export const postgresRouter = createTRPCRouter({
|
|||||||
await updatePostgresById(input.postgresId, {
|
await updatePostgresById(input.postgresId, {
|
||||||
applicationStatus: "done",
|
applicationStatus: "done",
|
||||||
});
|
});
|
||||||
|
await audit(ctx, {
|
||||||
|
action: "reload",
|
||||||
|
resourceType: "service",
|
||||||
|
resourceId: postgres.postgresId,
|
||||||
|
resourceName: postgres.appName,
|
||||||
|
});
|
||||||
return true;
|
return true;
|
||||||
}),
|
}),
|
||||||
update: protectedProcedure
|
update: protectedProcedure
|
||||||
.input(apiUpdatePostgres)
|
.input(apiUpdatePostgres)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
const { postgresId, ...rest } = input;
|
const { postgresId, ...rest } = input;
|
||||||
const postgres = await findPostgresById(postgresId);
|
await checkServicePermissionAndAccess(ctx, postgresId, {
|
||||||
if (
|
service: ["create"],
|
||||||
postgres.environment.project.organizationId !==
|
});
|
||||||
ctx.session.activeOrganizationId
|
|
||||||
) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "UNAUTHORIZED",
|
|
||||||
message: "You are not authorized to update this Postgres",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const service = await updatePostgresById(postgresId, {
|
const service = await updatePostgresById(postgresId, {
|
||||||
...rest,
|
...rest,
|
||||||
@@ -414,6 +383,12 @@ export const postgresRouter = createTRPCRouter({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await audit(ctx, {
|
||||||
|
action: "update",
|
||||||
|
resourceType: "service",
|
||||||
|
resourceId: postgresId,
|
||||||
|
resourceName: service.appName,
|
||||||
|
});
|
||||||
return true;
|
return true;
|
||||||
}),
|
}),
|
||||||
move: protectedProcedure
|
move: protectedProcedure
|
||||||
@@ -424,31 +399,10 @@ export const postgresRouter = createTRPCRouter({
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
const postgres = await findPostgresById(input.postgresId);
|
await checkServicePermissionAndAccess(ctx, input.postgresId, {
|
||||||
if (
|
service: ["create"],
|
||||||
postgres.environment.project.organizationId !==
|
});
|
||||||
ctx.session.activeOrganizationId
|
|
||||||
) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "UNAUTHORIZED",
|
|
||||||
message: "You are not authorized to move this postgres",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
const updatedPostgres = await db
|
||||||
.update(postgresTable)
|
.update(postgresTable)
|
||||||
.set({
|
.set({
|
||||||
@@ -465,24 +419,28 @@ export const postgresRouter = createTRPCRouter({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await audit(ctx, {
|
||||||
|
action: "move",
|
||||||
|
resourceType: "service",
|
||||||
|
resourceId: updatedPostgres.postgresId,
|
||||||
|
resourceName: updatedPostgres.appName,
|
||||||
|
});
|
||||||
return updatedPostgres;
|
return updatedPostgres;
|
||||||
}),
|
}),
|
||||||
rebuild: protectedProcedure
|
rebuild: protectedProcedure
|
||||||
.input(apiRebuildPostgres)
|
.input(apiRebuildPostgres)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
const postgres = await findPostgresById(input.postgresId);
|
await checkServicePermissionAndAccess(ctx, input.postgresId, {
|
||||||
if (
|
deployment: ["create"],
|
||||||
postgres.environment.project.organizationId !==
|
});
|
||||||
ctx.session.activeOrganizationId
|
|
||||||
) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "UNAUTHORIZED",
|
|
||||||
message: "You are not authorized to rebuild this Postgres database",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
await rebuildDatabase(postgres.postgresId, "postgres");
|
await rebuildDatabase(input.postgresId, "postgres");
|
||||||
|
|
||||||
|
await audit(ctx, {
|
||||||
|
action: "rebuild",
|
||||||
|
resourceType: "service",
|
||||||
|
resourceId: input.postgresId,
|
||||||
|
});
|
||||||
return true;
|
return true;
|
||||||
}),
|
}),
|
||||||
search: protectedProcedure
|
search: protectedProcedure
|
||||||
@@ -538,19 +496,18 @@ export const postgresRouter = createTRPCRouter({
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (ctx.user.role === "member") {
|
const { accessedServices } = await findMemberByUserId(
|
||||||
const { accessedServices } = await findMemberById(
|
ctx.user.id,
|
||||||
ctx.user.id,
|
ctx.session.activeOrganizationId,
|
||||||
ctx.session.activeOrganizationId,
|
);
|
||||||
);
|
if (accessedServices.length === 0) return { items: [], total: 0 };
|
||||||
if (accessedServices.length === 0) return { items: [], total: 0 };
|
baseConditions.push(
|
||||||
baseConditions.push(
|
sql`${postgresTable.postgresId} IN (${sql.join(
|
||||||
sql`${postgresTable.postgresId} IN (${sql.join(
|
accessedServices.map((id) => sql`${id}`),
|
||||||
accessedServices.map((id) => sql`${id}`),
|
sql`, `,
|
||||||
sql`, `,
|
)})`,
|
||||||
)})`,
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
const where = and(...baseConditions);
|
const where = and(...baseConditions);
|
||||||
const [items, countResult] = await Promise.all([
|
const [items, countResult] = await Promise.all([
|
||||||
db
|
db
|
||||||
|
|||||||
@@ -5,8 +5,9 @@ import {
|
|||||||
IS_CLOUD,
|
IS_CLOUD,
|
||||||
removePreviewDeployment,
|
removePreviewDeployment,
|
||||||
} from "@dokploy/server";
|
} from "@dokploy/server";
|
||||||
import { TRPCError } from "@trpc/server";
|
import { checkServicePermissionAndAccess } from "@dokploy/server/services/permission";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
import { audit } from "@/server/api/utils/audit";
|
||||||
import { apiFindAllByApplication } from "@/server/db/schema";
|
import { apiFindAllByApplication } from "@/server/db/schema";
|
||||||
import type { DeploymentJob } from "@/server/queues/queue-types";
|
import type { DeploymentJob } from "@/server/queues/queue-types";
|
||||||
import { myQueue } from "@/server/queues/queueSetup";
|
import { myQueue } from "@/server/queues/queueSetup";
|
||||||
@@ -17,53 +18,46 @@ export const previewDeploymentRouter = createTRPCRouter({
|
|||||||
all: protectedProcedure
|
all: protectedProcedure
|
||||||
.input(apiFindAllByApplication)
|
.input(apiFindAllByApplication)
|
||||||
.query(async ({ input, ctx }) => {
|
.query(async ({ input, ctx }) => {
|
||||||
const application = await findApplicationById(input.applicationId);
|
await checkServicePermissionAndAccess(ctx, input.applicationId, {
|
||||||
if (
|
deployment: ["read"],
|
||||||
application.environment.project.organizationId !==
|
});
|
||||||
ctx.session.activeOrganizationId
|
|
||||||
) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "UNAUTHORIZED",
|
|
||||||
message: "You are not authorized to access this application",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return await findPreviewDeploymentsByApplicationId(input.applicationId);
|
return await findPreviewDeploymentsByApplicationId(input.applicationId);
|
||||||
}),
|
}),
|
||||||
delete: protectedProcedure
|
|
||||||
.input(z.object({ previewDeploymentId: z.string() }))
|
|
||||||
.mutation(async ({ input, ctx }) => {
|
|
||||||
const previewDeployment = await findPreviewDeploymentById(
|
|
||||||
input.previewDeploymentId,
|
|
||||||
);
|
|
||||||
if (
|
|
||||||
previewDeployment.application.environment.project.organizationId !==
|
|
||||||
ctx.session.activeOrganizationId
|
|
||||||
) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "UNAUTHORIZED",
|
|
||||||
message: "You are not authorized to delete this preview deployment",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
await removePreviewDeployment(input.previewDeploymentId);
|
|
||||||
return true;
|
|
||||||
}),
|
|
||||||
one: protectedProcedure
|
one: protectedProcedure
|
||||||
.input(z.object({ previewDeploymentId: z.string() }))
|
.input(z.object({ previewDeploymentId: z.string() }))
|
||||||
.query(async ({ input, ctx }) => {
|
.query(async ({ input, ctx }) => {
|
||||||
const previewDeployment = await findPreviewDeploymentById(
|
const previewDeployment = await findPreviewDeploymentById(
|
||||||
input.previewDeploymentId,
|
input.previewDeploymentId,
|
||||||
);
|
);
|
||||||
if (
|
await checkServicePermissionAndAccess(
|
||||||
previewDeployment.application.environment.project.organizationId !==
|
ctx,
|
||||||
ctx.session.activeOrganizationId
|
previewDeployment.applicationId,
|
||||||
) {
|
{ deployment: ["read"] },
|
||||||
throw new TRPCError({
|
);
|
||||||
code: "UNAUTHORIZED",
|
|
||||||
message: "You are not authorized to access this preview deployment",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return previewDeployment;
|
return previewDeployment;
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
delete: protectedProcedure
|
||||||
|
.input(z.object({ previewDeploymentId: z.string() }))
|
||||||
|
.mutation(async ({ input, ctx }) => {
|
||||||
|
const previewDeployment = await findPreviewDeploymentById(
|
||||||
|
input.previewDeploymentId,
|
||||||
|
);
|
||||||
|
await checkServicePermissionAndAccess(
|
||||||
|
ctx,
|
||||||
|
previewDeployment.applicationId,
|
||||||
|
{ deployment: ["cancel"] },
|
||||||
|
);
|
||||||
|
await removePreviewDeployment(input.previewDeploymentId);
|
||||||
|
await audit(ctx, {
|
||||||
|
action: "delete",
|
||||||
|
resourceType: "previewDeployment",
|
||||||
|
resourceId: input.previewDeploymentId,
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
}),
|
||||||
|
|
||||||
redeploy: protectedProcedure
|
redeploy: protectedProcedure
|
||||||
.input(
|
.input(
|
||||||
z.object({
|
z.object({
|
||||||
@@ -76,15 +70,11 @@ export const previewDeploymentRouter = createTRPCRouter({
|
|||||||
const previewDeployment = await findPreviewDeploymentById(
|
const previewDeployment = await findPreviewDeploymentById(
|
||||||
input.previewDeploymentId,
|
input.previewDeploymentId,
|
||||||
);
|
);
|
||||||
if (
|
await checkServicePermissionAndAccess(
|
||||||
previewDeployment.application.environment.project.organizationId !==
|
ctx,
|
||||||
ctx.session.activeOrganizationId
|
previewDeployment.applicationId,
|
||||||
) {
|
{ deployment: ["create"] },
|
||||||
throw new TRPCError({
|
);
|
||||||
code: "UNAUTHORIZED",
|
|
||||||
message: "You are not authorized to redeploy this preview deployment",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
const application = await findApplicationById(
|
const application = await findApplicationById(
|
||||||
previewDeployment.applicationId,
|
previewDeployment.applicationId,
|
||||||
);
|
);
|
||||||
@@ -103,6 +93,11 @@ export const previewDeploymentRouter = createTRPCRouter({
|
|||||||
deploy(jobData).catch((error) => {
|
deploy(jobData).catch((error) => {
|
||||||
console.error("Background deployment failed:", error);
|
console.error("Background deployment failed:", error);
|
||||||
});
|
});
|
||||||
|
await audit(ctx, {
|
||||||
|
action: "redeploy",
|
||||||
|
resourceType: "previewDeployment",
|
||||||
|
resourceId: input.previewDeploymentId,
|
||||||
|
});
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
await myQueue.add(
|
await myQueue.add(
|
||||||
@@ -113,6 +108,11 @@ export const previewDeploymentRouter = createTRPCRouter({
|
|||||||
removeOnFail: true,
|
removeOnFail: true,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
await audit(ctx, {
|
||||||
|
action: "redeploy",
|
||||||
|
resourceType: "previewDeployment",
|
||||||
|
resourceId: input.previewDeploymentId,
|
||||||
|
});
|
||||||
return true;
|
return true;
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,7 +1,4 @@
|
|||||||
import {
|
import {
|
||||||
addNewEnvironment,
|
|
||||||
addNewProject,
|
|
||||||
checkProjectAccess,
|
|
||||||
createApplication,
|
createApplication,
|
||||||
createBackup,
|
createBackup,
|
||||||
createCompose,
|
createCompose,
|
||||||
@@ -22,7 +19,6 @@ import {
|
|||||||
findComposeById,
|
findComposeById,
|
||||||
findEnvironmentById,
|
findEnvironmentById,
|
||||||
findMariadbById,
|
findMariadbById,
|
||||||
findMemberById,
|
|
||||||
findMongoById,
|
findMongoById,
|
||||||
findMySqlById,
|
findMySqlById,
|
||||||
findPostgresById,
|
findPostgresById,
|
||||||
@@ -32,15 +28,23 @@ import {
|
|||||||
IS_CLOUD,
|
IS_CLOUD,
|
||||||
updateProjectById,
|
updateProjectById,
|
||||||
} from "@dokploy/server";
|
} from "@dokploy/server";
|
||||||
|
import {
|
||||||
|
addNewEnvironment,
|
||||||
|
addNewProject,
|
||||||
|
checkPermission,
|
||||||
|
checkProjectAccess,
|
||||||
|
findMemberByUserId,
|
||||||
|
} from "@dokploy/server/services/permission";
|
||||||
import { db } from "@dokploy/server/db";
|
import { db } from "@dokploy/server/db";
|
||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
import { and, desc, eq, ilike, or, sql } from "drizzle-orm";
|
import { and, desc, eq, ilike, or, sql } from "drizzle-orm";
|
||||||
import type { AnyPgColumn } from "drizzle-orm/pg-core";
|
import type { AnyPgColumn } from "drizzle-orm/pg-core";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
import { audit } from "@/server/api/utils/audit";
|
||||||
import {
|
import {
|
||||||
adminProcedure,
|
|
||||||
createTRPCRouter,
|
createTRPCRouter,
|
||||||
protectedProcedure,
|
protectedProcedure,
|
||||||
|
withPermission,
|
||||||
} from "@/server/api/trpc";
|
} from "@/server/api/trpc";
|
||||||
import {
|
import {
|
||||||
apiCreateProject,
|
apiCreateProject,
|
||||||
@@ -63,13 +67,7 @@ export const projectRouter = createTRPCRouter({
|
|||||||
.input(apiCreateProject)
|
.input(apiCreateProject)
|
||||||
.mutation(async ({ ctx, input }) => {
|
.mutation(async ({ ctx, input }) => {
|
||||||
try {
|
try {
|
||||||
if (ctx.user.role === "member") {
|
await checkProjectAccess(ctx, "create");
|
||||||
await checkProjectAccess(
|
|
||||||
ctx.user.id,
|
|
||||||
"create",
|
|
||||||
ctx.session.activeOrganizationId,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const admin = await findUserById(ctx.user.ownerId);
|
const admin = await findUserById(ctx.user.ownerId);
|
||||||
|
|
||||||
@@ -84,20 +82,16 @@ export const projectRouter = createTRPCRouter({
|
|||||||
input,
|
input,
|
||||||
ctx.session.activeOrganizationId,
|
ctx.session.activeOrganizationId,
|
||||||
);
|
);
|
||||||
if (ctx.user.role === "member") {
|
await addNewProject(ctx, project.project.projectId);
|
||||||
await addNewProject(
|
|
||||||
ctx.user.id,
|
|
||||||
project.project.projectId,
|
|
||||||
ctx.session.activeOrganizationId,
|
|
||||||
);
|
|
||||||
|
|
||||||
await addNewEnvironment(
|
await addNewEnvironment(ctx, project?.environment?.environmentId || "");
|
||||||
ctx.user.id,
|
|
||||||
project?.environment?.environmentId || "",
|
|
||||||
ctx.session.activeOrganizationId,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
await audit(ctx, {
|
||||||
|
action: "create",
|
||||||
|
resourceType: "project",
|
||||||
|
resourceId: project.project.projectId,
|
||||||
|
resourceName: project.project.name,
|
||||||
|
});
|
||||||
return project;
|
return project;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
@@ -111,18 +105,18 @@ export const projectRouter = createTRPCRouter({
|
|||||||
one: protectedProcedure
|
one: protectedProcedure
|
||||||
.input(apiFindOneProject)
|
.input(apiFindOneProject)
|
||||||
.query(async ({ input, ctx }) => {
|
.query(async ({ input, ctx }) => {
|
||||||
if (ctx.user.role === "member") {
|
if (ctx.user.role !== "owner" && ctx.user.role !== "admin") {
|
||||||
const { accessedServices } = await findMemberById(
|
const { accessedServices, accessedProjects } = await findMemberByUserId(
|
||||||
ctx.user.id,
|
ctx.user.id,
|
||||||
ctx.session.activeOrganizationId,
|
ctx.session.activeOrganizationId,
|
||||||
);
|
);
|
||||||
|
|
||||||
await checkProjectAccess(
|
if (!accessedProjects.includes(input.projectId)) {
|
||||||
ctx.user.id,
|
throw new TRPCError({
|
||||||
"access",
|
code: "UNAUTHORIZED",
|
||||||
ctx.session.activeOrganizationId,
|
message: "You don't have access to this project",
|
||||||
input.projectId,
|
});
|
||||||
);
|
}
|
||||||
|
|
||||||
const project = await db.query.projects.findFirst({
|
const project = await db.query.projects.findFirst({
|
||||||
where: and(
|
where: and(
|
||||||
@@ -189,15 +183,14 @@ export const projectRouter = createTRPCRouter({
|
|||||||
return project;
|
return project;
|
||||||
}),
|
}),
|
||||||
all: protectedProcedure.query(async ({ ctx }) => {
|
all: protectedProcedure.query(async ({ ctx }) => {
|
||||||
if (ctx.user.role === "member") {
|
if (ctx.user.role !== "owner" && ctx.user.role !== "admin") {
|
||||||
const { accessedProjects, accessedEnvironments, accessedServices } =
|
const { accessedProjects, accessedEnvironments, accessedServices } =
|
||||||
await findMemberById(ctx.user.id, ctx.session.activeOrganizationId);
|
await findMemberByUserId(ctx.user.id, ctx.session.activeOrganizationId);
|
||||||
|
|
||||||
if (accessedProjects.length === 0) {
|
if (accessedProjects.length === 0) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build environment filter
|
|
||||||
const environmentFilter =
|
const environmentFilter =
|
||||||
accessedEnvironments.length === 0
|
accessedEnvironments.length === 0
|
||||||
? sql`false`
|
? sql`false`
|
||||||
@@ -348,105 +341,106 @@ export const projectRouter = createTRPCRouter({
|
|||||||
});
|
});
|
||||||
}),
|
}),
|
||||||
|
|
||||||
/** All projects with full environments and services for the admin permissions UI. Admin only. */
|
allForPermissions: withPermission("member", "update").query(
|
||||||
allForPermissions: adminProcedure.query(async ({ ctx }) => {
|
async ({ ctx }) => {
|
||||||
return await db.query.projects.findMany({
|
return await db.query.projects.findMany({
|
||||||
where: eq(projects.organizationId, ctx.session.activeOrganizationId),
|
where: eq(projects.organizationId, ctx.session.activeOrganizationId),
|
||||||
orderBy: desc(projects.createdAt),
|
orderBy: desc(projects.createdAt),
|
||||||
columns: {
|
columns: {
|
||||||
projectId: true,
|
projectId: true,
|
||||||
name: true,
|
name: true,
|
||||||
},
|
},
|
||||||
with: {
|
with: {
|
||||||
environments: {
|
environments: {
|
||||||
columns: {
|
columns: {
|
||||||
environmentId: true,
|
environmentId: true,
|
||||||
name: true,
|
name: true,
|
||||||
isDefault: true,
|
isDefault: true,
|
||||||
},
|
|
||||||
with: {
|
|
||||||
applications: {
|
|
||||||
columns: {
|
|
||||||
applicationId: true,
|
|
||||||
appName: true,
|
|
||||||
name: true,
|
|
||||||
createdAt: true,
|
|
||||||
applicationStatus: true,
|
|
||||||
description: true,
|
|
||||||
serverId: true,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
mariadb: {
|
with: {
|
||||||
columns: {
|
applications: {
|
||||||
mariadbId: true,
|
columns: {
|
||||||
appName: true,
|
applicationId: true,
|
||||||
name: true,
|
appName: true,
|
||||||
createdAt: true,
|
name: true,
|
||||||
applicationStatus: true,
|
createdAt: true,
|
||||||
description: true,
|
applicationStatus: true,
|
||||||
serverId: true,
|
description: true,
|
||||||
|
serverId: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
mariadb: {
|
||||||
postgres: {
|
columns: {
|
||||||
columns: {
|
mariadbId: true,
|
||||||
postgresId: true,
|
appName: true,
|
||||||
appName: true,
|
name: true,
|
||||||
name: true,
|
createdAt: true,
|
||||||
createdAt: true,
|
applicationStatus: true,
|
||||||
applicationStatus: true,
|
description: true,
|
||||||
description: true,
|
serverId: true,
|
||||||
serverId: true,
|
},
|
||||||
},
|
},
|
||||||
},
|
postgres: {
|
||||||
mysql: {
|
columns: {
|
||||||
columns: {
|
postgresId: true,
|
||||||
mysqlId: true,
|
appName: true,
|
||||||
appName: true,
|
name: true,
|
||||||
name: true,
|
createdAt: true,
|
||||||
createdAt: true,
|
applicationStatus: true,
|
||||||
applicationStatus: true,
|
description: true,
|
||||||
description: true,
|
serverId: true,
|
||||||
serverId: true,
|
},
|
||||||
},
|
},
|
||||||
},
|
mysql: {
|
||||||
mongo: {
|
columns: {
|
||||||
columns: {
|
mysqlId: true,
|
||||||
mongoId: true,
|
appName: true,
|
||||||
appName: true,
|
name: true,
|
||||||
name: true,
|
createdAt: true,
|
||||||
createdAt: true,
|
applicationStatus: true,
|
||||||
applicationStatus: true,
|
description: true,
|
||||||
description: true,
|
serverId: true,
|
||||||
serverId: true,
|
},
|
||||||
},
|
},
|
||||||
},
|
mongo: {
|
||||||
redis: {
|
columns: {
|
||||||
columns: {
|
mongoId: true,
|
||||||
redisId: true,
|
appName: true,
|
||||||
appName: true,
|
name: true,
|
||||||
name: true,
|
createdAt: true,
|
||||||
createdAt: true,
|
applicationStatus: true,
|
||||||
applicationStatus: true,
|
description: true,
|
||||||
description: true,
|
serverId: true,
|
||||||
serverId: true,
|
},
|
||||||
},
|
},
|
||||||
},
|
redis: {
|
||||||
compose: {
|
columns: {
|
||||||
columns: {
|
redisId: true,
|
||||||
composeId: true,
|
appName: true,
|
||||||
appName: true,
|
name: true,
|
||||||
name: true,
|
createdAt: true,
|
||||||
createdAt: true,
|
applicationStatus: true,
|
||||||
composeStatus: true,
|
description: true,
|
||||||
description: true,
|
serverId: true,
|
||||||
serverId: true,
|
},
|
||||||
|
},
|
||||||
|
compose: {
|
||||||
|
columns: {
|
||||||
|
composeId: true,
|
||||||
|
appName: true,
|
||||||
|
name: true,
|
||||||
|
createdAt: true,
|
||||||
|
composeStatus: true,
|
||||||
|
description: true,
|
||||||
|
serverId: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
});
|
||||||
});
|
},
|
||||||
}),
|
),
|
||||||
|
|
||||||
search: protectedProcedure
|
search: protectedProcedure
|
||||||
.input(
|
.input(
|
||||||
@@ -482,8 +476,8 @@ export const projectRouter = createTRPCRouter({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ctx.user.role === "member") {
|
if (ctx.user.role !== "owner" && ctx.user.role !== "admin") {
|
||||||
const { accessedProjects } = await findMemberById(
|
const { accessedProjects } = await findMemberByUserId(
|
||||||
ctx.user.id,
|
ctx.user.id,
|
||||||
ctx.session.activeOrganizationId,
|
ctx.session.activeOrganizationId,
|
||||||
);
|
);
|
||||||
@@ -529,13 +523,6 @@ export const projectRouter = createTRPCRouter({
|
|||||||
.input(apiRemoveProject)
|
.input(apiRemoveProject)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
try {
|
try {
|
||||||
if (ctx.user.role === "member") {
|
|
||||||
await checkProjectAccess(
|
|
||||||
ctx.user.id,
|
|
||||||
"delete",
|
|
||||||
ctx.session.activeOrganizationId,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
const currentProject = await findProjectById(input.projectId);
|
const currentProject = await findProjectById(input.projectId);
|
||||||
if (
|
if (
|
||||||
currentProject.organizationId !== ctx.session.activeOrganizationId
|
currentProject.organizationId !== ctx.session.activeOrganizationId
|
||||||
@@ -545,8 +532,15 @@ export const projectRouter = createTRPCRouter({
|
|||||||
message: "You are not authorized to delete this project",
|
message: "You are not authorized to delete this project",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
await checkProjectAccess(ctx, "delete", input.projectId);
|
||||||
const deletedProject = await deleteProject(input.projectId);
|
const deletedProject = await deleteProject(input.projectId);
|
||||||
|
|
||||||
|
await audit(ctx, {
|
||||||
|
action: "delete",
|
||||||
|
resourceType: "project",
|
||||||
|
resourceId: currentProject.projectId,
|
||||||
|
resourceName: currentProject.name,
|
||||||
|
});
|
||||||
return deletedProject;
|
return deletedProject;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw error;
|
throw error;
|
||||||
@@ -565,10 +559,36 @@ export const projectRouter = createTRPCRouter({
|
|||||||
message: "You are not authorized to update this project",
|
message: "You are not authorized to update this project",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (ctx.user.role !== "owner" && ctx.user.role !== "admin") {
|
||||||
|
const { accessedProjects } = await findMemberByUserId(
|
||||||
|
ctx.user.id,
|
||||||
|
ctx.session.activeOrganizationId,
|
||||||
|
);
|
||||||
|
if (!accessedProjects.includes(input.projectId)) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "UNAUTHORIZED",
|
||||||
|
message: "You don't have access to this project",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (input.env !== undefined) {
|
||||||
|
await checkPermission(ctx, { projectEnvVars: ["write"] });
|
||||||
|
}
|
||||||
|
|
||||||
const project = await updateProjectById(input.projectId, {
|
const project = await updateProjectById(input.projectId, {
|
||||||
...input,
|
...input,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (project) {
|
||||||
|
await audit(ctx, {
|
||||||
|
action: "update",
|
||||||
|
resourceType: "project",
|
||||||
|
resourceId: input.projectId,
|
||||||
|
resourceName: project.name,
|
||||||
|
});
|
||||||
|
}
|
||||||
return project;
|
return project;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw error;
|
throw error;
|
||||||
@@ -602,15 +622,8 @@ export const projectRouter = createTRPCRouter({
|
|||||||
)
|
)
|
||||||
.mutation(async ({ ctx, input }) => {
|
.mutation(async ({ ctx, input }) => {
|
||||||
try {
|
try {
|
||||||
if (ctx.user.role === "member") {
|
await checkProjectAccess(ctx, "create");
|
||||||
await checkProjectAccess(
|
|
||||||
ctx.user.id,
|
|
||||||
"create",
|
|
||||||
ctx.session.activeOrganizationId,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get source project
|
|
||||||
const sourceEnvironment = input.duplicateInSameProject
|
const sourceEnvironment = input.duplicateInSameProject
|
||||||
? await findEnvironmentById(input.sourceEnvironmentId)
|
? await findEnvironmentById(input.sourceEnvironmentId)
|
||||||
: null;
|
: null;
|
||||||
@@ -626,7 +639,24 @@ export const projectRouter = createTRPCRouter({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create new project or use existing one
|
if (
|
||||||
|
input.duplicateInSameProject &&
|
||||||
|
sourceEnvironment &&
|
||||||
|
ctx.user.role !== "owner" &&
|
||||||
|
ctx.user.role !== "admin"
|
||||||
|
) {
|
||||||
|
const { accessedProjects } = await findMemberByUserId(
|
||||||
|
ctx.user.id,
|
||||||
|
ctx.session.activeOrganizationId,
|
||||||
|
);
|
||||||
|
if (!accessedProjects.includes(sourceEnvironment.project.projectId)) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "UNAUTHORIZED",
|
||||||
|
message: "You don't have access to this project",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const targetProject = input.duplicateInSameProject
|
const targetProject = input.duplicateInSameProject
|
||||||
? sourceEnvironment
|
? sourceEnvironment
|
||||||
: await createProject(
|
: await createProject(
|
||||||
@@ -643,7 +673,6 @@ export const projectRouter = createTRPCRouter({
|
|||||||
if (input.includeServices) {
|
if (input.includeServices) {
|
||||||
const servicesToDuplicate = input.selectedServices || [];
|
const servicesToDuplicate = input.selectedServices || [];
|
||||||
|
|
||||||
// Helper function to duplicate a service
|
|
||||||
const duplicateService = async (id: string, type: string) => {
|
const duplicateService = async (id: string, type: string) => {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case "application": {
|
case "application": {
|
||||||
@@ -947,20 +976,22 @@ export const projectRouter = createTRPCRouter({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Duplicate selected services
|
|
||||||
for (const service of servicesToDuplicate) {
|
for (const service of servicesToDuplicate) {
|
||||||
await duplicateService(service.id, service.type);
|
await duplicateService(service.id, service.type);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!input.duplicateInSameProject && ctx.user.role === "member") {
|
if (!input.duplicateInSameProject) {
|
||||||
await addNewProject(
|
await addNewProject(ctx, targetProject?.projectId || "");
|
||||||
ctx.user.id,
|
|
||||||
targetProject?.projectId || "",
|
|
||||||
ctx.session.activeOrganizationId,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await audit(ctx, {
|
||||||
|
action: "create",
|
||||||
|
resourceType: "project",
|
||||||
|
resourceId: targetProject?.projectId || "",
|
||||||
|
resourceName: input.name,
|
||||||
|
metadata: { duplicatedFrom: input.sourceEnvironmentId },
|
||||||
|
});
|
||||||
return targetProject;
|
return targetProject;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
|
|||||||
67
apps/dokploy/server/api/routers/proprietary/audit-log.ts
Normal file
67
apps/dokploy/server/api/routers/proprietary/audit-log.ts
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
import { getAuditLogs } from "@dokploy/server/services/proprietary/audit-log";
|
||||||
|
import { hasValidLicense } from "@dokploy/server/services/proprietary/license-key";
|
||||||
|
import { TRPCError } from "@trpc/server";
|
||||||
|
import { z } from "zod";
|
||||||
|
import { createTRPCRouter, withPermission } from "../../trpc";
|
||||||
|
|
||||||
|
export const auditLogRouter = createTRPCRouter({
|
||||||
|
all: withPermission("auditLog", "read")
|
||||||
|
.use(async ({ ctx, next }) => {
|
||||||
|
const licensed = await hasValidLicense(ctx.session.activeOrganizationId);
|
||||||
|
if (!licensed) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "FORBIDDEN",
|
||||||
|
message: "Valid enterprise license required",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return next();
|
||||||
|
})
|
||||||
|
.input(
|
||||||
|
z.object({
|
||||||
|
userId: z.string().optional(),
|
||||||
|
userEmail: z.string().optional(),
|
||||||
|
resourceName: z.string().optional(),
|
||||||
|
action: z
|
||||||
|
.enum([
|
||||||
|
"create",
|
||||||
|
"update",
|
||||||
|
"delete",
|
||||||
|
"deploy",
|
||||||
|
"cancel",
|
||||||
|
"redeploy",
|
||||||
|
"login",
|
||||||
|
"logout",
|
||||||
|
])
|
||||||
|
.optional(),
|
||||||
|
resourceType: z
|
||||||
|
.enum([
|
||||||
|
"project",
|
||||||
|
"service",
|
||||||
|
"environment",
|
||||||
|
"deployment",
|
||||||
|
"user",
|
||||||
|
"customRole",
|
||||||
|
"domain",
|
||||||
|
"certificate",
|
||||||
|
"registry",
|
||||||
|
"server",
|
||||||
|
"sshKey",
|
||||||
|
"gitProvider",
|
||||||
|
"notification",
|
||||||
|
"settings",
|
||||||
|
"session",
|
||||||
|
])
|
||||||
|
.optional(),
|
||||||
|
from: z.date().optional(),
|
||||||
|
to: z.date().optional(),
|
||||||
|
limit: z.number().min(1).max(500).default(50),
|
||||||
|
offset: z.number().min(0).default(0),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.query(async ({ ctx, input }) => {
|
||||||
|
return getAuditLogs({
|
||||||
|
organizationId: ctx.session.activeOrganizationId,
|
||||||
|
...input,
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
});
|
||||||
321
apps/dokploy/server/api/routers/proprietary/custom-role.ts
Normal file
321
apps/dokploy/server/api/routers/proprietary/custom-role.ts
Normal file
@@ -0,0 +1,321 @@
|
|||||||
|
import { db } from "@dokploy/server/db";
|
||||||
|
import { member, organizationRole, user } from "@dokploy/server/db/schema";
|
||||||
|
import { statements } from "@dokploy/server/lib/access-control";
|
||||||
|
import { TRPCError } from "@trpc/server";
|
||||||
|
import { and, count, eq } from "drizzle-orm";
|
||||||
|
import { z } from "zod";
|
||||||
|
import {
|
||||||
|
createTRPCRouter,
|
||||||
|
enterpriseProcedure,
|
||||||
|
protectedProcedure,
|
||||||
|
} from "../../trpc";
|
||||||
|
import { audit } from "../../utils/audit";
|
||||||
|
|
||||||
|
const permissionsSchema = z.record(z.string(), z.array(z.string()));
|
||||||
|
|
||||||
|
export const customRoleRouter = createTRPCRouter({
|
||||||
|
all: protectedProcedure.query(async ({ ctx }) => {
|
||||||
|
const [roles, memberCounts] = await Promise.all([
|
||||||
|
db.query.organizationRole.findMany({
|
||||||
|
where: eq(
|
||||||
|
organizationRole.organizationId,
|
||||||
|
ctx.session.activeOrganizationId,
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
db
|
||||||
|
.select({ role: member.role, count: count() })
|
||||||
|
.from(member)
|
||||||
|
.where(eq(member.organizationId, ctx.session.activeOrganizationId))
|
||||||
|
.groupBy(member.role),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const memberCountByRole = new Map(
|
||||||
|
memberCounts.map((r) => [r.role, r.count]),
|
||||||
|
);
|
||||||
|
|
||||||
|
const roleMap = new Map<
|
||||||
|
string,
|
||||||
|
{
|
||||||
|
role: string;
|
||||||
|
permissions: Record<string, string[]>;
|
||||||
|
createdAt: Date;
|
||||||
|
ids: string[];
|
||||||
|
memberCount: number;
|
||||||
|
}
|
||||||
|
>();
|
||||||
|
|
||||||
|
for (const entry of roles) {
|
||||||
|
const existing = roleMap.get(entry.role);
|
||||||
|
const parsed = JSON.parse(entry.permission) as Record<string, string[]>;
|
||||||
|
|
||||||
|
if (existing) {
|
||||||
|
for (const [resource, actions] of Object.entries(parsed)) {
|
||||||
|
existing.permissions[resource] = [
|
||||||
|
...new Set([...(existing.permissions[resource] ?? []), ...actions]),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
existing.ids.push(entry.id);
|
||||||
|
} else {
|
||||||
|
roleMap.set(entry.role, {
|
||||||
|
role: entry.role,
|
||||||
|
permissions: parsed,
|
||||||
|
createdAt: entry.createdAt,
|
||||||
|
ids: [entry.id],
|
||||||
|
memberCount: memberCountByRole.get(entry.role) ?? 0,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Array.from(roleMap.values());
|
||||||
|
}),
|
||||||
|
|
||||||
|
create: enterpriseProcedure
|
||||||
|
.input(
|
||||||
|
z.object({
|
||||||
|
roleName: z
|
||||||
|
.string()
|
||||||
|
.min(1)
|
||||||
|
.max(50)
|
||||||
|
.refine(
|
||||||
|
(name) => !["owner", "admin", "member"].includes(name),
|
||||||
|
"Cannot use reserved role names (owner, admin, member)",
|
||||||
|
),
|
||||||
|
permissions: permissionsSchema,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.mutation(async ({ input, ctx }) => {
|
||||||
|
const existingRoles = await db.query.organizationRole.findMany({
|
||||||
|
where: eq(
|
||||||
|
organizationRole.organizationId,
|
||||||
|
ctx.session.activeOrganizationId,
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
const uniqueRoleNames = new Set(existingRoles.map((r) => r.role));
|
||||||
|
|
||||||
|
if (uniqueRoleNames.size >= 10) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "BAD_REQUEST",
|
||||||
|
message: "Maximum of 10 custom roles per organization reached",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (uniqueRoleNames.has(input.roleName)) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "CONFLICT",
|
||||||
|
message: `Role "${input.roleName}" already exists`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
validatePermissions(input.permissions);
|
||||||
|
|
||||||
|
const [created] = await db
|
||||||
|
.insert(organizationRole)
|
||||||
|
.values({
|
||||||
|
organizationId: ctx.session.activeOrganizationId,
|
||||||
|
role: input.roleName,
|
||||||
|
permission: JSON.stringify(input.permissions),
|
||||||
|
})
|
||||||
|
.returning();
|
||||||
|
|
||||||
|
await audit(ctx, {
|
||||||
|
action: "create",
|
||||||
|
resourceType: "customRole",
|
||||||
|
resourceName: input.roleName,
|
||||||
|
});
|
||||||
|
return created;
|
||||||
|
}),
|
||||||
|
|
||||||
|
update: enterpriseProcedure
|
||||||
|
.input(
|
||||||
|
z.object({
|
||||||
|
roleName: z.string().min(1),
|
||||||
|
newRoleName: z
|
||||||
|
.string()
|
||||||
|
.min(1)
|
||||||
|
.max(50)
|
||||||
|
.refine(
|
||||||
|
(name) => !["owner", "admin", "member"].includes(name),
|
||||||
|
"Cannot use reserved role names (owner, admin, member)",
|
||||||
|
)
|
||||||
|
.optional(),
|
||||||
|
permissions: permissionsSchema,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.mutation(async ({ input, ctx }) => {
|
||||||
|
if (["owner", "admin", "member"].includes(input.roleName)) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "BAD_REQUEST",
|
||||||
|
message: "Cannot modify built-in roles",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const effectiveRoleName = input.newRoleName ?? input.roleName;
|
||||||
|
|
||||||
|
if (input.newRoleName && input.newRoleName !== input.roleName) {
|
||||||
|
const existing = await db.query.organizationRole.findFirst({
|
||||||
|
where: and(
|
||||||
|
eq(
|
||||||
|
organizationRole.organizationId,
|
||||||
|
ctx.session.activeOrganizationId,
|
||||||
|
),
|
||||||
|
eq(organizationRole.role, input.newRoleName),
|
||||||
|
),
|
||||||
|
});
|
||||||
|
if (existing) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "CONFLICT",
|
||||||
|
message: `Role "${input.newRoleName}" already exists`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await db
|
||||||
|
.update(member)
|
||||||
|
.set({ role: input.newRoleName })
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(member.organizationId, ctx.session.activeOrganizationId),
|
||||||
|
eq(member.role, input.roleName),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
validatePermissions(input.permissions);
|
||||||
|
|
||||||
|
const [updated] = await db
|
||||||
|
.update(organizationRole)
|
||||||
|
.set({
|
||||||
|
role: effectiveRoleName,
|
||||||
|
permission: JSON.stringify(input.permissions),
|
||||||
|
})
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(
|
||||||
|
organizationRole.organizationId,
|
||||||
|
ctx.session.activeOrganizationId,
|
||||||
|
),
|
||||||
|
eq(organizationRole.role, input.roleName),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.returning();
|
||||||
|
|
||||||
|
await audit(ctx, {
|
||||||
|
action: "update",
|
||||||
|
resourceType: "customRole",
|
||||||
|
resourceName: effectiveRoleName,
|
||||||
|
});
|
||||||
|
return updated;
|
||||||
|
}),
|
||||||
|
|
||||||
|
remove: enterpriseProcedure
|
||||||
|
.input(
|
||||||
|
z.object({
|
||||||
|
roleName: z.string().min(1),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.mutation(async ({ input, ctx }) => {
|
||||||
|
if (["owner", "admin", "member"].includes(input.roleName)) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "BAD_REQUEST",
|
||||||
|
message: "Cannot delete built-in roles",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const assignedMembers = await db.query.member.findMany({
|
||||||
|
where: and(
|
||||||
|
eq(member.organizationId, ctx.session.activeOrganizationId),
|
||||||
|
eq(member.role, input.roleName),
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (assignedMembers.length > 0) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "BAD_REQUEST",
|
||||||
|
message: `Cannot delete role "${input.roleName}": ${assignedMembers.length} member(s) are currently assigned to it. Reassign them first.`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleted = await db
|
||||||
|
.delete(organizationRole)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(
|
||||||
|
organizationRole.organizationId,
|
||||||
|
ctx.session.activeOrganizationId,
|
||||||
|
),
|
||||||
|
eq(organizationRole.role, input.roleName),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.returning();
|
||||||
|
|
||||||
|
if (deleted.length === 0) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "NOT_FOUND",
|
||||||
|
message: `Role "${input.roleName}" not found`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await audit(ctx, {
|
||||||
|
action: "delete",
|
||||||
|
resourceType: "customRole",
|
||||||
|
resourceName: input.roleName,
|
||||||
|
});
|
||||||
|
return { deleted: deleted.length };
|
||||||
|
}),
|
||||||
|
|
||||||
|
membersByRole: protectedProcedure
|
||||||
|
.input(z.object({ roleName: z.string().min(1) }))
|
||||||
|
.query(async ({ input, ctx }) => {
|
||||||
|
const members = await db
|
||||||
|
.select({
|
||||||
|
id: member.id,
|
||||||
|
userId: member.userId,
|
||||||
|
email: user.email,
|
||||||
|
firstName: user.firstName,
|
||||||
|
lastName: user.lastName,
|
||||||
|
})
|
||||||
|
.from(member)
|
||||||
|
.innerJoin(user, eq(member.userId, user.id))
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(member.organizationId, ctx.session.activeOrganizationId),
|
||||||
|
eq(member.role, input.roleName),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return members;
|
||||||
|
}),
|
||||||
|
|
||||||
|
getStatements: protectedProcedure.query(() => {
|
||||||
|
return statements;
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const INTERNAL_RESOURCES = ["organization", "invitation", "team", "ac"];
|
||||||
|
|
||||||
|
function validatePermissions(permissions: Record<string, string[]>) {
|
||||||
|
for (const [resource, actions] of Object.entries(permissions)) {
|
||||||
|
if (INTERNAL_RESOURCES.includes(resource)) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "BAD_REQUEST",
|
||||||
|
message: `Resource "${resource}" is managed internally and cannot be assigned to custom roles`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!(resource in statements)) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "BAD_REQUEST",
|
||||||
|
message: `Unknown resource: ${resource}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const validActions = statements[resource as keyof typeof statements];
|
||||||
|
for (const action of actions) {
|
||||||
|
if (!validActions.includes(action as never)) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "BAD_REQUEST",
|
||||||
|
message: `Invalid action "${action}" for resource "${resource}". Valid actions: ${validActions.join(", ")}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user