mirror of
https://github.com/Dokploy/dokploy.git
synced 2026-06-25 00:55:30 +02:00
Compare commits
22 Commits
1365-creat
...
feat/add-a
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d84099108a | ||
|
|
cee426dcf5 | ||
|
|
1074e9b08e | ||
|
|
a5911e2bac | ||
|
|
a43b8ee2d2 | ||
|
|
982a1d5d31 | ||
|
|
30d45bf2e5 | ||
|
|
db221e5cc4 | ||
|
|
e1773a8f8b | ||
|
|
e8475730fa | ||
|
|
d78e634cb0 | ||
|
|
509d95fbf2 | ||
|
|
b928e94e51 | ||
|
|
3052979bdd | ||
|
|
2ec4868a09 | ||
|
|
733777eeb1 | ||
|
|
521330682d | ||
|
|
7cc048450b | ||
|
|
427674dd64 | ||
|
|
8b8dc8c94f | ||
|
|
d6e8653839 | ||
|
|
d0b7ce3a50 |
@@ -5,7 +5,8 @@ vi.mock("node:fs", () => ({
|
|||||||
default: fs,
|
default: fs,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
import type { FileConfig, User } from "@dokploy/server";
|
import type { FileConfig } from "@dokploy/server";
|
||||||
|
import type { WebServer } from "@dokploy/server/db/schema";
|
||||||
import {
|
import {
|
||||||
createDefaultServerTraefikConfig,
|
createDefaultServerTraefikConfig,
|
||||||
loadOrCreateConfig,
|
loadOrCreateConfig,
|
||||||
@@ -13,11 +14,8 @@ import {
|
|||||||
} from "@dokploy/server";
|
} from "@dokploy/server";
|
||||||
import { beforeEach, expect, test, vi } from "vitest";
|
import { beforeEach, expect, test, vi } from "vitest";
|
||||||
|
|
||||||
const baseAdmin: User = {
|
const baseAdmin: WebServer = {
|
||||||
https: false,
|
https: false,
|
||||||
enablePaidFeatures: false,
|
|
||||||
allowImpersonation: false,
|
|
||||||
role: "user",
|
|
||||||
metricsConfig: {
|
metricsConfig: {
|
||||||
containers: {
|
containers: {
|
||||||
refreshRate: 20,
|
refreshRate: 20,
|
||||||
@@ -40,10 +38,6 @@ const baseAdmin: User = {
|
|||||||
urlCallback: "",
|
urlCallback: "",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
cleanupCacheApplications: false,
|
|
||||||
cleanupCacheOnCompose: false,
|
|
||||||
cleanupCacheOnPreviews: false,
|
|
||||||
createdAt: new Date(),
|
|
||||||
serverIp: null,
|
serverIp: null,
|
||||||
certificateType: "none",
|
certificateType: "none",
|
||||||
host: null,
|
host: null,
|
||||||
@@ -51,22 +45,7 @@ const baseAdmin: User = {
|
|||||||
sshPrivateKey: null,
|
sshPrivateKey: null,
|
||||||
enableDockerCleanup: false,
|
enableDockerCleanup: false,
|
||||||
logCleanupCron: null,
|
logCleanupCron: null,
|
||||||
serversQuantity: 0,
|
webServerId: "1",
|
||||||
stripeCustomerId: "",
|
|
||||||
stripeSubscriptionId: "",
|
|
||||||
banExpires: new Date(),
|
|
||||||
banned: true,
|
|
||||||
banReason: "",
|
|
||||||
email: "",
|
|
||||||
expirationDate: "",
|
|
||||||
id: "",
|
|
||||||
isRegistered: false,
|
|
||||||
name: "",
|
|
||||||
createdAt2: new Date().toISOString(),
|
|
||||||
emailVerified: false,
|
|
||||||
image: "",
|
|
||||||
updatedAt: new Date(),
|
|
||||||
twoFactorEnabled: false,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@@ -85,8 +64,6 @@ test("Should apply redirect-to-https", () => {
|
|||||||
updateServerTraefik(
|
updateServerTraefik(
|
||||||
{
|
{
|
||||||
...baseAdmin,
|
...baseAdmin,
|
||||||
https: true,
|
|
||||||
certificateType: "letsencrypt",
|
|
||||||
},
|
},
|
||||||
"example.com",
|
"example.com",
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ export const ShowTraefikConfig = ({ applicationId }: Props) => {
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex flex-col pt-2 relative">
|
<div className="flex flex-col pt-2 relative">
|
||||||
<div className="flex flex-col gap-6 max-h-[35rem] min-h-[10rem] overflow-y-auto">
|
<div className="flex flex-col gap-6 max-h-[35rem] min-h-[10rem]">
|
||||||
<CodeEditor
|
<CodeEditor
|
||||||
lineWrapping
|
lineWrapping
|
||||||
value={data || "Empty"}
|
value={data || "Empty"}
|
||||||
|
|||||||
@@ -158,7 +158,7 @@ export const ShowDeployment = ({
|
|||||||
<div
|
<div
|
||||||
ref={scrollRef}
|
ref={scrollRef}
|
||||||
onScroll={handleScroll}
|
onScroll={handleScroll}
|
||||||
className="h-[720px] overflow-y-auto space-y-0 border p-4 bg-[#fafafa] dark:bg-[#050506] rounded custom-logs-scrollbar"
|
className="h-[720px] space-y-0 border p-4 bg-[#fafafa] dark:bg-[#050506] rounded custom-logs-scrollbar"
|
||||||
>
|
>
|
||||||
{" "}
|
{" "}
|
||||||
{filteredLogs.length > 0 ? (
|
{filteredLogs.length > 0 ? (
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ export const ShowDomains = ({ id, type }: Props) => {
|
|||||||
const [validationStates, setValidationStates] = useState<ValidationStates>(
|
const [validationStates, setValidationStates] = useState<ValidationStates>(
|
||||||
{},
|
{},
|
||||||
);
|
);
|
||||||
const { data: ip } = api.settings.getIp.useQuery();
|
const { data: webServer } = api.webServer.get.useQuery();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data,
|
data,
|
||||||
@@ -110,7 +110,9 @@ export const ShowDomains = ({ id, type }: Props) => {
|
|||||||
const result = await validateDomain({
|
const result = await validateDomain({
|
||||||
domain: host,
|
domain: host,
|
||||||
serverIp:
|
serverIp:
|
||||||
application?.server?.ipAddress?.toString() || ip?.toString() || "",
|
application?.server?.ipAddress?.toString() ||
|
||||||
|
webServer?.serverIp?.toString() ||
|
||||||
|
"",
|
||||||
});
|
});
|
||||||
|
|
||||||
setValidationStates((prev) => ({
|
setValidationStates((prev) => ({
|
||||||
@@ -210,7 +212,7 @@ export const ShowDomains = ({ id, type }: Props) => {
|
|||||||
}}
|
}}
|
||||||
serverIp={
|
serverIp={
|
||||||
application?.server?.ipAddress?.toString() ||
|
application?.server?.ipAddress?.toString() ||
|
||||||
ip?.toString()
|
webServer?.serverIp?.toString()
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ export const ShowContainerConfig = ({ containerId, serverId }: Props) => {
|
|||||||
See in detail the config of this container
|
See in detail the config of this container
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<div className="text-wrap rounded-lg border p-4 overflow-y-auto text-sm bg-card max-h-[80vh]">
|
<div className="text-wrap rounded-lg border p-4 text-sm bg-card max-h-[80vh]">
|
||||||
<code>
|
<code>
|
||||||
<pre className="whitespace-pre-wrap break-words">
|
<pre className="whitespace-pre-wrap break-words">
|
||||||
<CodeEditor
|
<CodeEditor
|
||||||
|
|||||||
@@ -274,7 +274,7 @@ export const DockerLogsId: React.FC<Props> = ({
|
|||||||
<div
|
<div
|
||||||
ref={scrollRef}
|
ref={scrollRef}
|
||||||
onScroll={handleScroll}
|
onScroll={handleScroll}
|
||||||
className="h-[720px] overflow-y-auto space-y-0 border p-4 bg-[#fafafa] dark:bg-[#050506] rounded custom-logs-scrollbar"
|
className="h-[720px] space-y-0 border p-4 bg-[#fafafa] dark:bg-[#050506] rounded custom-logs-scrollbar"
|
||||||
>
|
>
|
||||||
{filteredLogs.length > 0 ? (
|
{filteredLogs.length > 0 ? (
|
||||||
filteredLogs.map((filteredLog: LogLine, index: number) => (
|
filteredLogs.map((filteredLog: LogLine, index: number) => (
|
||||||
|
|||||||
@@ -138,7 +138,7 @@ export function LineCountFilter({
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<CommandPrimitive.List className="max-h-[300px] overflow-y-auto overflow-x-hidden">
|
<CommandPrimitive.List className="max-h-[300px] overflow-x-hidden">
|
||||||
<CommandPrimitive.Group className="px-2 py-1.5">
|
<CommandPrimitive.Group className="px-2 py-1.5">
|
||||||
{lineCountOptions.map((option) => {
|
{lineCountOptions.map((option) => {
|
||||||
const isSelected = value === option.value;
|
const isSelected = value === option.value;
|
||||||
|
|||||||
@@ -46,11 +46,11 @@ interface Props {
|
|||||||
mariadbId: string;
|
mariadbId: string;
|
||||||
}
|
}
|
||||||
export const ShowExternalMariadbCredentials = ({ mariadbId }: Props) => {
|
export const ShowExternalMariadbCredentials = ({ mariadbId }: Props) => {
|
||||||
const { data: ip } = api.settings.getIp.useQuery();
|
const { data: webServer } = api.webServer.get.useQuery();
|
||||||
const { data, refetch } = api.mariadb.one.useQuery({ mariadbId });
|
const { data, refetch } = api.mariadb.one.useQuery({ mariadbId });
|
||||||
const { mutateAsync, isLoading } = api.mariadb.saveExternalPort.useMutation();
|
const { mutateAsync, isLoading } = api.mariadb.saveExternalPort.useMutation();
|
||||||
const [connectionUrl, setConnectionUrl] = useState("");
|
const [connectionUrl, setConnectionUrl] = useState("");
|
||||||
const getIp = data?.server?.ipAddress || ip;
|
const getIp = data?.server?.ipAddress || webServer?.serverIp;
|
||||||
const form = useForm<DockerProvider>({
|
const form = useForm<DockerProvider>({
|
||||||
defaultValues: {},
|
defaultValues: {},
|
||||||
resolver: zodResolver(DockerProviderSchema),
|
resolver: zodResolver(DockerProviderSchema),
|
||||||
|
|||||||
@@ -46,11 +46,11 @@ interface Props {
|
|||||||
mongoId: string;
|
mongoId: string;
|
||||||
}
|
}
|
||||||
export const ShowExternalMongoCredentials = ({ mongoId }: Props) => {
|
export const ShowExternalMongoCredentials = ({ mongoId }: Props) => {
|
||||||
const { data: ip } = api.settings.getIp.useQuery();
|
const { data: webServer } = api.webServer.get.useQuery();
|
||||||
const { data, refetch } = api.mongo.one.useQuery({ mongoId });
|
const { data, refetch } = api.mongo.one.useQuery({ mongoId });
|
||||||
const { mutateAsync, isLoading } = api.mongo.saveExternalPort.useMutation();
|
const { mutateAsync, isLoading } = api.mongo.saveExternalPort.useMutation();
|
||||||
const [connectionUrl, setConnectionUrl] = useState("");
|
const [connectionUrl, setConnectionUrl] = useState("");
|
||||||
const getIp = data?.server?.ipAddress || ip;
|
const getIp = data?.server?.ipAddress || webServer?.serverIp;
|
||||||
const form = useForm<DockerProvider>({
|
const form = useForm<DockerProvider>({
|
||||||
defaultValues: {},
|
defaultValues: {},
|
||||||
resolver: zodResolver(DockerProviderSchema),
|
resolver: zodResolver(DockerProviderSchema),
|
||||||
|
|||||||
@@ -46,11 +46,11 @@ interface Props {
|
|||||||
mysqlId: string;
|
mysqlId: string;
|
||||||
}
|
}
|
||||||
export const ShowExternalMysqlCredentials = ({ mysqlId }: Props) => {
|
export const ShowExternalMysqlCredentials = ({ mysqlId }: Props) => {
|
||||||
const { data: ip } = api.settings.getIp.useQuery();
|
const { data: webServer } = api.webServer.get.useQuery();
|
||||||
const { data, refetch } = api.mysql.one.useQuery({ mysqlId });
|
const { data, refetch } = api.mysql.one.useQuery({ mysqlId });
|
||||||
const { mutateAsync, isLoading } = api.mysql.saveExternalPort.useMutation();
|
const { mutateAsync, isLoading } = api.mysql.saveExternalPort.useMutation();
|
||||||
const [connectionUrl, setConnectionUrl] = useState("");
|
const [connectionUrl, setConnectionUrl] = useState("");
|
||||||
const getIp = data?.server?.ipAddress || ip;
|
const getIp = data?.server?.ipAddress || webServer?.serverIp;
|
||||||
const form = useForm<DockerProvider>({
|
const form = useForm<DockerProvider>({
|
||||||
defaultValues: {},
|
defaultValues: {},
|
||||||
resolver: zodResolver(DockerProviderSchema),
|
resolver: zodResolver(DockerProviderSchema),
|
||||||
|
|||||||
@@ -46,11 +46,11 @@ interface Props {
|
|||||||
postgresId: string;
|
postgresId: string;
|
||||||
}
|
}
|
||||||
export const ShowExternalPostgresCredentials = ({ postgresId }: Props) => {
|
export const ShowExternalPostgresCredentials = ({ postgresId }: Props) => {
|
||||||
const { data: ip } = api.settings.getIp.useQuery();
|
const { data: webServer } = api.webServer.get.useQuery();
|
||||||
const { data, refetch } = api.postgres.one.useQuery({ postgresId });
|
const { data, refetch } = api.postgres.one.useQuery({ postgresId });
|
||||||
const { mutateAsync, isLoading } =
|
const { mutateAsync, isLoading } =
|
||||||
api.postgres.saveExternalPort.useMutation();
|
api.postgres.saveExternalPort.useMutation();
|
||||||
const getIp = data?.server?.ipAddress || ip;
|
const getIp = data?.server?.ipAddress || webServer?.serverIp;
|
||||||
const [connectionUrl, setConnectionUrl] = useState("");
|
const [connectionUrl, setConnectionUrl] = useState("");
|
||||||
|
|
||||||
const form = useForm<DockerProvider>({
|
const form = useForm<DockerProvider>({
|
||||||
|
|||||||
@@ -167,7 +167,7 @@ export const DuplicateProject = ({
|
|||||||
|
|
||||||
<div className="grid gap-2">
|
<div className="grid gap-2">
|
||||||
<Label>Selected services to duplicate</Label>
|
<Label>Selected services to duplicate</Label>
|
||||||
<div className="space-y-2 max-h-[200px] overflow-y-auto border rounded-md p-4">
|
<div className="space-y-2 max-h-[200px] border rounded-md p-4">
|
||||||
{selectedServices.map((service) => (
|
{selectedServices.map((service) => (
|
||||||
<div key={service.id} className="flex items-center space-x-2">
|
<div key={service.id} className="flex items-center space-x-2">
|
||||||
<span className="text-sm">
|
<span className="text-sm">
|
||||||
|
|||||||
@@ -47,11 +47,12 @@ import { useMemo, useState } from "react";
|
|||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { HandleProject } from "./handle-project";
|
import { HandleProject } from "./handle-project";
|
||||||
import { ProjectEnvironment } from "./project-environment";
|
import { ProjectEnvironment } from "./project-environment";
|
||||||
|
import { PERMISSIONS } from "@dokploy/server/lib/permissions";
|
||||||
|
import { Permissions } from "../shared/Permissions";
|
||||||
|
|
||||||
export const ShowProjects = () => {
|
export const ShowProjects = () => {
|
||||||
const utils = api.useUtils();
|
const utils = api.useUtils();
|
||||||
const { data, isLoading } = api.project.all.useQuery();
|
const { data, isLoading } = api.project.all.useQuery();
|
||||||
const { data: auth } = api.user.get.useQuery();
|
|
||||||
const { mutateAsync } = api.project.remove.useMutation();
|
const { mutateAsync } = api.project.remove.useMutation();
|
||||||
const [searchQuery, setSearchQuery] = useState("");
|
const [searchQuery, setSearchQuery] = useState("");
|
||||||
|
|
||||||
@@ -83,11 +84,11 @@ export const ShowProjects = () => {
|
|||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
|
|
||||||
{(auth?.role === "owner" || auth?.canCreateProjects) && (
|
<Permissions permissions={[PERMISSIONS.PROJECT.CREATE.name]}>
|
||||||
<div className="">
|
<div className="">
|
||||||
<HandleProject />
|
<HandleProject />
|
||||||
</div>
|
</div>
|
||||||
)}
|
</Permissions>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<CardContent className="space-y-2 py-8 border-t gap-4 flex flex-col min-h-[60vh]">
|
<CardContent className="space-y-2 py-8 border-t gap-4 flex flex-col min-h-[60vh]">
|
||||||
@@ -157,7 +158,7 @@ export const ShowProjects = () => {
|
|||||||
</Button>
|
</Button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent
|
<DropdownMenuContent
|
||||||
className="w-[200px] space-y-2 overflow-y-auto max-h-[400px]"
|
className="w-[200px] space-y-2 max-h-[400px]"
|
||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
>
|
>
|
||||||
{project.applications.length > 0 && (
|
{project.applications.length > 0 && (
|
||||||
@@ -265,7 +266,7 @@ export const ShowProjects = () => {
|
|||||||
</Button>
|
</Button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent
|
<DropdownMenuContent
|
||||||
className="w-[200px] space-y-2 overflow-y-auto max-h-[280px]"
|
className="w-[200px] space-y-2 max-h-[280px]"
|
||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
>
|
>
|
||||||
<DropdownMenuLabel className="font-normal">
|
<DropdownMenuLabel className="font-normal">
|
||||||
@@ -289,8 +290,11 @@ export const ShowProjects = () => {
|
|||||||
<div
|
<div
|
||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
>
|
>
|
||||||
{(auth?.role === "owner" ||
|
<Permissions
|
||||||
auth?.canDeleteProjects) && (
|
permissions={[
|
||||||
|
PERMISSIONS.PROJECT.DELETE.name,
|
||||||
|
]}
|
||||||
|
>
|
||||||
<AlertDialog>
|
<AlertDialog>
|
||||||
<AlertDialogTrigger className="w-full">
|
<AlertDialogTrigger className="w-full">
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
@@ -356,7 +360,7 @@ export const ShowProjects = () => {
|
|||||||
</AlertDialogFooter>
|
</AlertDialogFooter>
|
||||||
</AlertDialogContent>
|
</AlertDialogContent>
|
||||||
</AlertDialog>
|
</AlertDialog>
|
||||||
)}
|
</Permissions>
|
||||||
</div>
|
</div>
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
|
|||||||
@@ -46,11 +46,11 @@ interface Props {
|
|||||||
redisId: string;
|
redisId: string;
|
||||||
}
|
}
|
||||||
export const ShowExternalRedisCredentials = ({ redisId }: Props) => {
|
export const ShowExternalRedisCredentials = ({ redisId }: Props) => {
|
||||||
const { data: ip } = api.settings.getIp.useQuery();
|
const { data: webServer } = api.webServer.get.useQuery();
|
||||||
const { data, refetch } = api.redis.one.useQuery({ redisId });
|
const { data, refetch } = api.redis.one.useQuery({ redisId });
|
||||||
const { mutateAsync, isLoading } = api.redis.saveExternalPort.useMutation();
|
const { mutateAsync, isLoading } = api.redis.saveExternalPort.useMutation();
|
||||||
const [connectionUrl, setConnectionUrl] = useState("");
|
const [connectionUrl, setConnectionUrl] = useState("");
|
||||||
const getIp = data?.server?.ipAddress || ip;
|
const getIp = data?.server?.ipAddress || webServer?.serverIp;
|
||||||
|
|
||||||
const form = useForm<DockerProvider>({
|
const form = useForm<DockerProvider>({
|
||||||
defaultValues: {},
|
defaultValues: {},
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ export const ShowWelcomeDokploy = () => {
|
|||||||
|
|
||||||
const { data: isCloud, isLoading } = api.settings.isCloud.useQuery();
|
const { data: isCloud, isLoading } = api.settings.isCloud.useQuery();
|
||||||
|
|
||||||
if (!isCloud || data?.role !== "admin") {
|
if (!isCloud || data?.role?.name !== "admin") {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -23,14 +23,14 @@ export const ShowWelcomeDokploy = () => {
|
|||||||
!isLoading &&
|
!isLoading &&
|
||||||
isCloud &&
|
isCloud &&
|
||||||
!localStorage.getItem("hasSeenCloudWelcomeModal") &&
|
!localStorage.getItem("hasSeenCloudWelcomeModal") &&
|
||||||
data?.role === "owner"
|
data?.role?.name === "owner"
|
||||||
) {
|
) {
|
||||||
setOpen(true);
|
setOpen(true);
|
||||||
}
|
}
|
||||||
}, [isCloud, isLoading]);
|
}, [isCloud, isLoading]);
|
||||||
|
|
||||||
const handleClose = (isOpen: boolean) => {
|
const handleClose = (isOpen: boolean) => {
|
||||||
if (data?.role === "owner") {
|
if (data?.role?.name === "owner") {
|
||||||
setOpen(isOpen);
|
setOpen(isOpen);
|
||||||
if (!isOpen) {
|
if (!isOpen) {
|
||||||
localStorage.setItem("hasSeenCloudWelcomeModal", "true"); // Establece el flag al cerrar el modal
|
localStorage.setItem("hasSeenCloudWelcomeModal", "true"); // Establece el flag al cerrar el modal
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ import { Disable2FA } from "./disable-2fa";
|
|||||||
import { Enable2FA } from "./enable-2fa";
|
import { Enable2FA } from "./enable-2fa";
|
||||||
|
|
||||||
const profileSchema = z.object({
|
const profileSchema = z.object({
|
||||||
|
name: z.string(),
|
||||||
email: z.string(),
|
email: z.string(),
|
||||||
password: z.string().nullable(),
|
password: z.string().nullable(),
|
||||||
currentPassword: z.string().nullable(),
|
currentPassword: z.string().nullable(),
|
||||||
@@ -79,6 +80,7 @@ export const ProfileForm = () => {
|
|||||||
|
|
||||||
const form = useForm<Profile>({
|
const form = useForm<Profile>({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
|
name: data?.user?.name || "",
|
||||||
email: data?.user?.email || "",
|
email: data?.user?.email || "",
|
||||||
password: "",
|
password: "",
|
||||||
image: data?.user?.image || "",
|
image: data?.user?.image || "",
|
||||||
@@ -92,6 +94,7 @@ export const ProfileForm = () => {
|
|||||||
if (data) {
|
if (data) {
|
||||||
form.reset(
|
form.reset(
|
||||||
{
|
{
|
||||||
|
name: data?.user?.name || "",
|
||||||
email: data?.user?.email || "",
|
email: data?.user?.email || "",
|
||||||
password: form.getValues("password") || "",
|
password: form.getValues("password") || "",
|
||||||
image: data?.user?.image || "",
|
image: data?.user?.image || "",
|
||||||
@@ -114,6 +117,7 @@ export const ProfileForm = () => {
|
|||||||
|
|
||||||
const onSubmit = async (values: Profile) => {
|
const onSubmit = async (values: Profile) => {
|
||||||
await mutateAsync({
|
await mutateAsync({
|
||||||
|
name: values.name,
|
||||||
email: values.email.toLowerCase(),
|
email: values.email.toLowerCase(),
|
||||||
password: values.password || undefined,
|
password: values.password || undefined,
|
||||||
image: values.image,
|
image: values.image,
|
||||||
@@ -124,6 +128,7 @@ export const ProfileForm = () => {
|
|||||||
await refetch();
|
await refetch();
|
||||||
toast.success("Profile Updated");
|
toast.success("Profile Updated");
|
||||||
form.reset({
|
form.reset({
|
||||||
|
name: values.name,
|
||||||
email: values.email,
|
email: values.email,
|
||||||
password: "",
|
password: "",
|
||||||
image: values.image,
|
image: values.image,
|
||||||
@@ -167,6 +172,19 @@ export const ProfileForm = () => {
|
|||||||
className="grid gap-4"
|
className="grid gap-4"
|
||||||
>
|
>
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="name"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Name</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="email"
|
name="email"
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ interface Props {
|
|||||||
serverId?: string;
|
serverId?: string;
|
||||||
}
|
}
|
||||||
export const ToggleDockerCleanup = ({ serverId }: Props) => {
|
export const ToggleDockerCleanup = ({ serverId }: Props) => {
|
||||||
const { data, refetch } = api.user.get.useQuery(undefined, {
|
const { data, refetch } = api.webServer.get.useQuery(undefined, {
|
||||||
enabled: !serverId,
|
enabled: !serverId,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -20,9 +20,9 @@ export const ToggleDockerCleanup = ({ serverId }: Props) => {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
const enabled = data?.user.enableDockerCleanup || server?.enableDockerCleanup;
|
const enabled = data?.enableDockerCleanup || server?.enableDockerCleanup;
|
||||||
|
|
||||||
const { mutateAsync } = api.settings.updateDockerCleanup.useMutation();
|
const { mutateAsync } = api.webServer.updateDockerCleanup.useMutation();
|
||||||
|
|
||||||
const handleToggle = async (checked: boolean) => {
|
const handleToggle = async (checked: boolean) => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -89,7 +89,7 @@ export const SetupMonitoring = ({ serverId }: Props) => {
|
|||||||
enabled: !!serverId,
|
enabled: !!serverId,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
: api.user.getServerMetrics.useQuery();
|
: api.webServer.get.useQuery();
|
||||||
|
|
||||||
const url = useUrl();
|
const url = useUrl();
|
||||||
|
|
||||||
|
|||||||
@@ -147,7 +147,7 @@ export const SetupServer = ({ serverId }: Props) => {
|
|||||||
<li>2. Add The SSH Key to Server Manually</li>
|
<li>2. Add The SSH Key to Server Manually</li>
|
||||||
</ul>
|
</ul>
|
||||||
<div className="flex flex-col gap-4 w-full overflow-auto">
|
<div className="flex flex-col gap-4 w-full overflow-auto">
|
||||||
<div className="flex relative flex-col gap-2 overflow-y-auto">
|
<div className="flex relative flex-col gap-2">
|
||||||
<div className="text-sm text-primary flex flex-row gap-2 items-center">
|
<div className="text-sm text-primary flex flex-row gap-2 items-center">
|
||||||
Copy Public Key ({server?.sshKey?.name})
|
Copy Public Key ({server?.sshKey?.name})
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -117,7 +117,7 @@ export const CreateSSHKey = () => {
|
|||||||
Option 2
|
Option 2
|
||||||
</span>
|
</span>
|
||||||
<div className="flex flex-col gap-4 w-full overflow-auto">
|
<div className="flex flex-col gap-4 w-full overflow-auto">
|
||||||
<div className="flex relative flex-col gap-2 overflow-y-auto">
|
<div className="flex relative flex-col gap-2">
|
||||||
<div className="text-sm text-primary flex flex-row gap-2 items-center">
|
<div className="text-sm text-primary flex flex-row gap-2 items-center">
|
||||||
Copy Public Key
|
Copy Public Key
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -49,12 +49,15 @@ 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: roles } = api.role.all.useQuery();
|
||||||
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: sendInvitation } = api.user.sendInvitation.useMutation();
|
const {
|
||||||
const [error, setError] = useState<string | null>(null);
|
mutateAsync: createInvitation,
|
||||||
|
isLoading,
|
||||||
|
error,
|
||||||
|
} = api.user.createInvitation.useMutation();
|
||||||
const { data: activeOrganization } = authClient.useActiveOrganization();
|
const { data: activeOrganization } = authClient.useActiveOrganization();
|
||||||
|
|
||||||
const form = useForm<AddInvitation>({
|
const form = useForm<AddInvitation>({
|
||||||
@@ -70,36 +73,20 @@ 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);
|
await createInvitation({
|
||||||
const result = await authClient.organization.inviteMember({
|
|
||||||
email: data.email.toLowerCase(),
|
email: data.email.toLowerCase(),
|
||||||
role: data.role,
|
role: data.role,
|
||||||
organizationId: activeOrganization?.id,
|
organizationId: activeOrganization?.id || "",
|
||||||
});
|
notificationId: data.notificationId || "",
|
||||||
|
})
|
||||||
if (result.error) {
|
.then(() => {
|
||||||
setError(result.error.message || "");
|
|
||||||
} else {
|
|
||||||
if (!isCloud && data.notificationId) {
|
|
||||||
await sendInvitation({
|
|
||||||
invitationId: result.data.id,
|
|
||||||
notificationId: data.notificationId || "",
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
toast.success("Invitation created and email sent");
|
|
||||||
})
|
|
||||||
.catch((error: any) => {
|
|
||||||
toast.error(error.message);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
toast.success("Invitation created");
|
toast.success("Invitation created");
|
||||||
}
|
})
|
||||||
setError(null);
|
.catch((error: any) => {
|
||||||
setOpen(false);
|
toast.error(error.message);
|
||||||
}
|
});
|
||||||
|
|
||||||
utils.organization.allInvitations.invalidate();
|
utils.organization.allInvitations.invalidate();
|
||||||
setIsLoading(false);
|
|
||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={setOpen}>
|
<Dialog open={open} onOpenChange={setOpen}>
|
||||||
@@ -113,7 +100,7 @@ export const AddInvitation = () => {
|
|||||||
<DialogTitle>Add Invitation</DialogTitle>
|
<DialogTitle>Add Invitation</DialogTitle>
|
||||||
<DialogDescription>Invite a new user</DialogDescription>
|
<DialogDescription>Invite a new user</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
{error && <AlertBlock type="error">{error}</AlertBlock>}
|
{error && <AlertBlock type="error">{error.message}</AlertBlock>}
|
||||||
|
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
<form
|
<form
|
||||||
@@ -158,6 +145,12 @@ export const AddInvitation = () => {
|
|||||||
</FormControl>
|
</FormControl>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="member">Member</SelectItem>
|
<SelectItem value="member">Member</SelectItem>
|
||||||
|
<SelectItem value="admin">Admin</SelectItem>
|
||||||
|
{roles?.map((role) => (
|
||||||
|
<SelectItem key={role.name} value={role.name}>
|
||||||
|
{role.name}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
<FormDescription>
|
<FormDescription>
|
||||||
|
|||||||
@@ -0,0 +1,758 @@
|
|||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogTrigger,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
import { DropdownMenuItem } from "@/components/ui/dropdown-menu";
|
||||||
|
import {
|
||||||
|
Form,
|
||||||
|
FormControl,
|
||||||
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
FormMessage,
|
||||||
|
FormDescription,
|
||||||
|
} from "@/components/ui/form";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
|
||||||
|
import { Separator } from "@/components/ui/separator";
|
||||||
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||||
|
import { api } from "@/utils/api";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import { z } from "zod";
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
import { Checkbox } from "@/components/ui/checkbox";
|
||||||
|
import {
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
CardDescription,
|
||||||
|
CardHeader,
|
||||||
|
CardTitle,
|
||||||
|
} from "@/components/ui/card";
|
||||||
|
import { extractServices } from "@/pages/dashboard/project/[projectId]";
|
||||||
|
import { PenBoxIcon, Trash2 } from "lucide-react";
|
||||||
|
import { format } from "date-fns";
|
||||||
|
import { DialogAction } from "@/components/shared/dialog-action";
|
||||||
|
|
||||||
|
const assignRoleSchema = z.object({
|
||||||
|
roleId: z.string(),
|
||||||
|
accessedProjects: z.array(z.string()).optional(),
|
||||||
|
accessedServices: z.array(z.string()).optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const createRoleSchema = z.object({
|
||||||
|
name: z.string().min(1, "Name is required"),
|
||||||
|
description: z.string().optional(),
|
||||||
|
permissions: z.array(z.string()).min(1, "Select at least one permission"),
|
||||||
|
});
|
||||||
|
|
||||||
|
type AssignRoleForm = z.infer<typeof assignRoleSchema>;
|
||||||
|
type CreateRoleForm = z.infer<typeof createRoleSchema>;
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
userId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AddUserPermissionsV2 = ({ userId }: Props) => {
|
||||||
|
const utils = api.useUtils();
|
||||||
|
const { data: projects } = api.project.all.useQuery();
|
||||||
|
const [activeTab, setActiveTab] = useState<"assign" | "create">("assign");
|
||||||
|
const [editingRole, setEditingRole] = useState<{
|
||||||
|
roleId: string;
|
||||||
|
name: string;
|
||||||
|
description?: string;
|
||||||
|
permissions: string[];
|
||||||
|
} | null>(null);
|
||||||
|
const { data: roles, refetch: refetchRoles } = api.role.all.useQuery();
|
||||||
|
const { data: defaultRoles } = api.role.getDefaultRoles.useQuery();
|
||||||
|
|
||||||
|
const { data: userData, refetch: refetchUser } = api.user.one.useQuery(
|
||||||
|
{
|
||||||
|
userId,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
enabled: !!userId,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const { mutateAsync: createRole, isLoading: isCreatingRole } =
|
||||||
|
api.role.create.useMutation();
|
||||||
|
const { mutateAsync: updateRole, isLoading: isUpdatingRole } =
|
||||||
|
api.role.update.useMutation();
|
||||||
|
const { mutateAsync: deleteRole, isLoading: isDeletingRole } =
|
||||||
|
api.role.delete.useMutation();
|
||||||
|
const { mutateAsync: updateMemberRole, isLoading: isAssigningRole } =
|
||||||
|
api.user.assignRole.useMutation();
|
||||||
|
|
||||||
|
const assignForm = useForm<AssignRoleForm>({
|
||||||
|
resolver: zodResolver(assignRoleSchema),
|
||||||
|
defaultValues: {
|
||||||
|
accessedProjects: [],
|
||||||
|
accessedServices: [],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const createForm = useForm<CreateRoleForm>({
|
||||||
|
resolver: zodResolver(createRoleSchema),
|
||||||
|
defaultValues: {
|
||||||
|
permissions: [],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (userData) {
|
||||||
|
assignForm.reset({
|
||||||
|
roleId: userData.roleId || "",
|
||||||
|
accessedProjects: userData.accessedProjects || [],
|
||||||
|
accessedServices: userData.accessedServices || [],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [userData, assignForm]);
|
||||||
|
|
||||||
|
// Reset form when switching between create and edit modes
|
||||||
|
useEffect(() => {
|
||||||
|
if (editingRole) {
|
||||||
|
createForm.reset({
|
||||||
|
name: editingRole.name,
|
||||||
|
description: editingRole.description || "",
|
||||||
|
permissions: editingRole.permissions,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
createForm.reset({
|
||||||
|
name: "",
|
||||||
|
description: "",
|
||||||
|
permissions: [],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [editingRole, createForm]);
|
||||||
|
|
||||||
|
// Check if the selected role is owner or admin (has full access)
|
||||||
|
const selectedRoleId = assignForm.watch("roleId");
|
||||||
|
const selectedRole = defaultRoles?.roles?.find(
|
||||||
|
(role) => role.roleId === selectedRoleId,
|
||||||
|
);
|
||||||
|
|
||||||
|
const isFullAccessRole =
|
||||||
|
selectedRole &&
|
||||||
|
(selectedRole.name === "owner" || selectedRole.name === "admin");
|
||||||
|
|
||||||
|
const onAssignRole = async (data: AssignRoleForm) => {
|
||||||
|
try {
|
||||||
|
await updateMemberRole({
|
||||||
|
userId,
|
||||||
|
roleId: data.roleId,
|
||||||
|
accessedProjects: isFullAccessRole ? [] : data.accessedProjects || [],
|
||||||
|
accessedServices: isFullAccessRole ? [] : data.accessedServices || [],
|
||||||
|
});
|
||||||
|
toast.success("Role assigned successfully");
|
||||||
|
await refetchUser();
|
||||||
|
await utils.user.all.invalidate();
|
||||||
|
} catch (error) {
|
||||||
|
const message =
|
||||||
|
error instanceof Error ? error.message : "Failed to assign role";
|
||||||
|
toast.error(message);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onCreateRole = async (data: CreateRoleForm) => {
|
||||||
|
try {
|
||||||
|
if (editingRole) {
|
||||||
|
// Update existing role
|
||||||
|
await updateRole({
|
||||||
|
roleId: editingRole.roleId,
|
||||||
|
...data,
|
||||||
|
permissions: data.permissions,
|
||||||
|
});
|
||||||
|
toast.success("Role updated successfully");
|
||||||
|
} else {
|
||||||
|
// Create new role
|
||||||
|
await createRole({
|
||||||
|
...data,
|
||||||
|
permissions: data.permissions,
|
||||||
|
});
|
||||||
|
toast.success("Role created successfully");
|
||||||
|
}
|
||||||
|
refetchRoles();
|
||||||
|
setActiveTab("assign");
|
||||||
|
setEditingRole(null);
|
||||||
|
createForm.reset();
|
||||||
|
} catch (error) {
|
||||||
|
const message =
|
||||||
|
error instanceof Error
|
||||||
|
? error.message
|
||||||
|
: editingRole
|
||||||
|
? "Failed to update role"
|
||||||
|
: "Failed to create role";
|
||||||
|
toast.error(message);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onEditRole = (role: {
|
||||||
|
roleId: string;
|
||||||
|
name: string;
|
||||||
|
description?: string | null;
|
||||||
|
permissions: string[] | null;
|
||||||
|
}) => {
|
||||||
|
setEditingRole({
|
||||||
|
roleId: role.roleId,
|
||||||
|
name: role.name,
|
||||||
|
description: role.description || "",
|
||||||
|
permissions: role.permissions || [],
|
||||||
|
});
|
||||||
|
setActiveTab("create");
|
||||||
|
};
|
||||||
|
|
||||||
|
const cancelEdit = () => {
|
||||||
|
setEditingRole(null);
|
||||||
|
setActiveTab("assign");
|
||||||
|
createForm.reset();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog>
|
||||||
|
<DialogTrigger asChild>
|
||||||
|
<DropdownMenuItem
|
||||||
|
className="w-full cursor-pointer"
|
||||||
|
onSelect={(e) => e.preventDefault()}
|
||||||
|
>
|
||||||
|
Manage Roles
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DialogTrigger>
|
||||||
|
<DialogContent className="max-h-[85vh] overflow-y-auto sm:max-w-4xl">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Role Management</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
Assign existing roles or create new ones. The Owner role has full
|
||||||
|
access to all features.
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<Tabs
|
||||||
|
value={activeTab}
|
||||||
|
onValueChange={(v) => setActiveTab(v as "assign" | "create")}
|
||||||
|
>
|
||||||
|
<TabsList className="grid w-full grid-cols-2">
|
||||||
|
<TabsTrigger value="assign">Assign Role</TabsTrigger>
|
||||||
|
<TabsTrigger value="create">
|
||||||
|
{editingRole ? "Edit Role" : "Create Role"}
|
||||||
|
</TabsTrigger>
|
||||||
|
</TabsList>
|
||||||
|
|
||||||
|
<TabsContent value="assign">
|
||||||
|
<Form {...assignForm}>
|
||||||
|
<form onSubmit={assignForm.handleSubmit(onAssignRole)}>
|
||||||
|
<div className="space-y-4 py-4">
|
||||||
|
<FormField
|
||||||
|
control={assignForm.control}
|
||||||
|
name="roleId"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="space-y-3">
|
||||||
|
<FormLabel>Select Role</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<RadioGroup
|
||||||
|
onValueChange={field.onChange}
|
||||||
|
defaultValue={field.value}
|
||||||
|
className="space-y-4"
|
||||||
|
>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<h4 className="text-sm font-medium">
|
||||||
|
Default Roles
|
||||||
|
</h4>
|
||||||
|
{defaultRoles?.roles?.map((role) => {
|
||||||
|
const isOwner = role.name === "owner";
|
||||||
|
const isAdmin = role.name === "admin";
|
||||||
|
if (isOwner) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<FormItem
|
||||||
|
key={role.roleId}
|
||||||
|
className="flex items-center space-x-3 space-y-0"
|
||||||
|
>
|
||||||
|
<FormControl>
|
||||||
|
<RadioGroupItem
|
||||||
|
value={role.roleId || ""}
|
||||||
|
disabled={isOwner}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormLabel className="font-normal">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="font-medium capitalize">
|
||||||
|
{role.name}
|
||||||
|
</span>
|
||||||
|
{isAdmin && (
|
||||||
|
<Badge
|
||||||
|
variant="default"
|
||||||
|
className="text-xs"
|
||||||
|
>
|
||||||
|
Full Access
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-muted-foreground">
|
||||||
|
{role.description}
|
||||||
|
</div>
|
||||||
|
{!isOwner && (
|
||||||
|
<div className="flex flex-wrap gap-1 mt-1">
|
||||||
|
{role.permissions?.map(
|
||||||
|
(permission) => (
|
||||||
|
<Badge
|
||||||
|
key={permission.name}
|
||||||
|
variant={
|
||||||
|
isOwner
|
||||||
|
? "default"
|
||||||
|
: "secondary"
|
||||||
|
}
|
||||||
|
className="text-xs"
|
||||||
|
>
|
||||||
|
{permission.description}
|
||||||
|
</Badge>
|
||||||
|
),
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</FormLabel>
|
||||||
|
</FormItem>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Separator />
|
||||||
|
|
||||||
|
{/* Custom Roles Section */}
|
||||||
|
{roles &&
|
||||||
|
roles.filter((r) => !r.isSystem).length > 0 && (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<h4 className="text-sm font-medium">
|
||||||
|
Custom Roles
|
||||||
|
</h4>
|
||||||
|
{roles
|
||||||
|
?.filter((r) => !r.isSystem)
|
||||||
|
.map((role) => (
|
||||||
|
<FormItem
|
||||||
|
key={role.roleId}
|
||||||
|
className="flex items-center justify-between space-x-3 space-y-0"
|
||||||
|
>
|
||||||
|
<div className="flex items-center space-x-3">
|
||||||
|
<FormControl>
|
||||||
|
<RadioGroupItem
|
||||||
|
value={role.roleId}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormLabel className="font-normal">
|
||||||
|
<span className="font-medium">
|
||||||
|
{role.name}
|
||||||
|
</span>
|
||||||
|
<div className="text-xs text-muted-foreground">
|
||||||
|
{role.description}
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
{format(
|
||||||
|
role.createdAt,
|
||||||
|
"MMM d, yyyy",
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
<div className="flex flex-wrap gap-1 mt-1">
|
||||||
|
{role.permissions?.map(
|
||||||
|
(permission) => {
|
||||||
|
const permissionInfo =
|
||||||
|
defaultRoles?.permissions?.find(
|
||||||
|
(p) =>
|
||||||
|
p.name === permission,
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<Badge
|
||||||
|
key={permission}
|
||||||
|
variant="secondary"
|
||||||
|
className="text-xs"
|
||||||
|
>
|
||||||
|
{
|
||||||
|
permissionInfo?.description
|
||||||
|
}
|
||||||
|
</Badge>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</FormLabel>
|
||||||
|
</div>
|
||||||
|
<div className="flex space-x-2">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
onClick={() => onEditRole(role)}
|
||||||
|
title="Edit role"
|
||||||
|
>
|
||||||
|
<PenBoxIcon className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
<DialogAction
|
||||||
|
title="Delete Role"
|
||||||
|
description="Are you sure you want to delete this role?"
|
||||||
|
type="destructive"
|
||||||
|
onClick={async () => {
|
||||||
|
await deleteRole({
|
||||||
|
roleId: role.roleId,
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
refetchRoles();
|
||||||
|
toast.success(
|
||||||
|
"Role deleted successfully",
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
const message =
|
||||||
|
error instanceof Error
|
||||||
|
? error.message
|
||||||
|
: "Error deleting role";
|
||||||
|
toast.error(message);
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="text-destructive hover:text-destructive"
|
||||||
|
isLoading={isDeletingRole}
|
||||||
|
>
|
||||||
|
<Trash2 className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</DialogAction>
|
||||||
|
</div>
|
||||||
|
</FormItem>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</RadioGroup>
|
||||||
|
</FormControl>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Project Access Section - Only show if not full access role */}
|
||||||
|
{!isFullAccessRole && selectedRoleId && (
|
||||||
|
<>
|
||||||
|
<Separator />
|
||||||
|
<FormField
|
||||||
|
control={assignForm.control}
|
||||||
|
name="accessedProjects"
|
||||||
|
render={() => (
|
||||||
|
<FormItem className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<FormLabel className="text-base">
|
||||||
|
Projects Access
|
||||||
|
</FormLabel>
|
||||||
|
<FormDescription>
|
||||||
|
Select the projects that the user can access
|
||||||
|
</FormDescription>
|
||||||
|
</div>
|
||||||
|
{projects?.length === 0 && (
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
No projects found
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
<div className="grid md:grid-cols-2 gap-4">
|
||||||
|
{projects?.map((project, index) => {
|
||||||
|
const services = extractServices(project);
|
||||||
|
return (
|
||||||
|
<FormField
|
||||||
|
key={`project-${index}`}
|
||||||
|
control={assignForm.control}
|
||||||
|
name="accessedProjects"
|
||||||
|
render={({ field }) => {
|
||||||
|
return (
|
||||||
|
<FormItem
|
||||||
|
key={project.projectId}
|
||||||
|
className="flex flex-col items-start space-x-4 rounded-lg p-4 border"
|
||||||
|
>
|
||||||
|
<div className="flex flex-row gap-4">
|
||||||
|
<FormControl>
|
||||||
|
<Checkbox
|
||||||
|
checked={field.value?.includes(
|
||||||
|
project.projectId,
|
||||||
|
)}
|
||||||
|
onCheckedChange={(checked) => {
|
||||||
|
return checked
|
||||||
|
? field.onChange([
|
||||||
|
...(field.value || []),
|
||||||
|
project.projectId,
|
||||||
|
])
|
||||||
|
: field.onChange(
|
||||||
|
field.value?.filter(
|
||||||
|
(value) =>
|
||||||
|
value !==
|
||||||
|
project.projectId,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormLabel className="text-sm font-medium text-primary">
|
||||||
|
{project.name}
|
||||||
|
</FormLabel>
|
||||||
|
</div>
|
||||||
|
{services.length === 0 && (
|
||||||
|
<p className="text-sm text-muted-foreground ml-6">
|
||||||
|
No services found
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
{services?.map(
|
||||||
|
(service, serviceIndex) => (
|
||||||
|
<FormField
|
||||||
|
key={`service-${serviceIndex}`}
|
||||||
|
control={assignForm.control}
|
||||||
|
name="accessedServices"
|
||||||
|
render={({ field }) => {
|
||||||
|
return (
|
||||||
|
<FormItem
|
||||||
|
key={service.id}
|
||||||
|
className="flex flex-row items-start space-x-3 space-y-0 ml-6"
|
||||||
|
>
|
||||||
|
<FormControl>
|
||||||
|
<Checkbox
|
||||||
|
checked={field.value?.includes(
|
||||||
|
service.id,
|
||||||
|
)}
|
||||||
|
onCheckedChange={(
|
||||||
|
checked,
|
||||||
|
) => {
|
||||||
|
const currentProjects =
|
||||||
|
assignForm.getValues(
|
||||||
|
"accessedProjects",
|
||||||
|
) || [];
|
||||||
|
const currentServices =
|
||||||
|
field.value || [];
|
||||||
|
|
||||||
|
if (checked) {
|
||||||
|
// Add service
|
||||||
|
const newServices =
|
||||||
|
[
|
||||||
|
...currentServices,
|
||||||
|
service.id,
|
||||||
|
];
|
||||||
|
field.onChange(
|
||||||
|
newServices,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Auto-select project if not already selected
|
||||||
|
if (
|
||||||
|
!currentProjects.includes(
|
||||||
|
project.projectId,
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
assignForm.setValue(
|
||||||
|
"accessedProjects",
|
||||||
|
[
|
||||||
|
...currentProjects,
|
||||||
|
project.projectId,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Remove service
|
||||||
|
const newServices =
|
||||||
|
currentServices.filter(
|
||||||
|
(value) =>
|
||||||
|
value !==
|
||||||
|
service.id,
|
||||||
|
);
|
||||||
|
field.onChange(
|
||||||
|
newServices,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Check if any other services from this project are still selected
|
||||||
|
const otherServicesFromProject =
|
||||||
|
services.filter(
|
||||||
|
(s) =>
|
||||||
|
s.id !==
|
||||||
|
service.id &&
|
||||||
|
newServices.includes(
|
||||||
|
s.id,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
// If no other services from this project, unselect the project
|
||||||
|
if (
|
||||||
|
otherServicesFromProject.length ===
|
||||||
|
0
|
||||||
|
) {
|
||||||
|
assignForm.setValue(
|
||||||
|
"accessedProjects",
|
||||||
|
currentProjects.filter(
|
||||||
|
(p) =>
|
||||||
|
p !==
|
||||||
|
project.projectId,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormLabel className="text-sm text-muted-foreground">
|
||||||
|
{service.name}
|
||||||
|
</FormLabel>
|
||||||
|
</FormItem>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
)}
|
||||||
|
</FormItem>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DialogFooter>
|
||||||
|
<Button type="submit" disabled={isAssigningRole}>
|
||||||
|
{isAssigningRole ? "Assigning..." : "Save Role"}
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
{/* Create Role Tab Content */}
|
||||||
|
<TabsContent value="create">
|
||||||
|
<Form {...createForm}>
|
||||||
|
<form onSubmit={createForm.handleSubmit(onCreateRole)}>
|
||||||
|
<div className="space-y-4 py-4">
|
||||||
|
<FormField
|
||||||
|
control={createForm.control}
|
||||||
|
name="name"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Role Name</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder="e.g. Developer" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>
|
||||||
|
Role name must be unique
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={createForm.control}
|
||||||
|
name="description"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Description</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
placeholder="e.g. Role for development team members"
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={createForm.control}
|
||||||
|
name="permissions"
|
||||||
|
render={() => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Permissions</FormLabel>
|
||||||
|
<Card className=" bg-transparent">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-sm">
|
||||||
|
Available Permissions
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Select the permissions for this role
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="grid grid-cols-2 gap-4">
|
||||||
|
{defaultRoles?.permissions?.map((permission) => (
|
||||||
|
<FormField
|
||||||
|
key={permission.name}
|
||||||
|
control={createForm.control}
|
||||||
|
name="permissions"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem
|
||||||
|
key={permission.name}
|
||||||
|
className="flex flex-row items-start space-x-3 space-y-0"
|
||||||
|
>
|
||||||
|
<FormControl>
|
||||||
|
<Checkbox
|
||||||
|
checked={field.value?.includes(
|
||||||
|
permission.name,
|
||||||
|
)}
|
||||||
|
onCheckedChange={(checked) => {
|
||||||
|
return checked
|
||||||
|
? field.onChange([
|
||||||
|
...(field.value || []),
|
||||||
|
permission.name,
|
||||||
|
])
|
||||||
|
: field.onChange(
|
||||||
|
field.value?.filter(
|
||||||
|
(value) =>
|
||||||
|
value !== permission.name,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormLabel className="text-sm font-normal">
|
||||||
|
{permission.description}
|
||||||
|
</FormLabel>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<DialogFooter>
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
disabled={isCreatingRole || isUpdatingRole}
|
||||||
|
>
|
||||||
|
{isCreatingRole || isUpdatingRole
|
||||||
|
? "Saving..."
|
||||||
|
: "Save Role"}
|
||||||
|
</Button>
|
||||||
|
{editingRole && (
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={cancelEdit}
|
||||||
|
disabled={isUpdatingRole}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</DialogFooter>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
</TabsContent>
|
||||||
|
</Tabs>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,444 +0,0 @@
|
|||||||
import { AlertBlock } from "@/components/shared/alert-block";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import { Checkbox } from "@/components/ui/checkbox";
|
|
||||||
import {
|
|
||||||
Dialog,
|
|
||||||
DialogContent,
|
|
||||||
DialogDescription,
|
|
||||||
DialogFooter,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
DialogTrigger,
|
|
||||||
} from "@/components/ui/dialog";
|
|
||||||
import { DropdownMenuItem } from "@/components/ui/dropdown-menu";
|
|
||||||
import {
|
|
||||||
Form,
|
|
||||||
FormControl,
|
|
||||||
FormDescription,
|
|
||||||
FormField,
|
|
||||||
FormItem,
|
|
||||||
FormLabel,
|
|
||||||
FormMessage,
|
|
||||||
} from "@/components/ui/form";
|
|
||||||
import { Switch } from "@/components/ui/switch";
|
|
||||||
import { extractServices } from "@/pages/dashboard/project/[projectId]";
|
|
||||||
import { api } from "@/utils/api";
|
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
|
||||||
import { useEffect } from "react";
|
|
||||||
import { useForm } from "react-hook-form";
|
|
||||||
import { toast } from "sonner";
|
|
||||||
import { z } from "zod";
|
|
||||||
|
|
||||||
const addPermissions = z.object({
|
|
||||||
accessedProjects: z.array(z.string()).optional(),
|
|
||||||
accessedServices: z.array(z.string()).optional(),
|
|
||||||
canCreateProjects: z.boolean().optional().default(false),
|
|
||||||
canCreateServices: z.boolean().optional().default(false),
|
|
||||||
canDeleteProjects: z.boolean().optional().default(false),
|
|
||||||
canDeleteServices: z.boolean().optional().default(false),
|
|
||||||
canAccessToTraefikFiles: z.boolean().optional().default(false),
|
|
||||||
canAccessToDocker: z.boolean().optional().default(false),
|
|
||||||
canAccessToAPI: z.boolean().optional().default(false),
|
|
||||||
canAccessToSSHKeys: z.boolean().optional().default(false),
|
|
||||||
canAccessToGitProviders: z.boolean().optional().default(false),
|
|
||||||
});
|
|
||||||
|
|
||||||
type AddPermissions = z.infer<typeof addPermissions>;
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
userId: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const AddUserPermissions = ({ userId }: Props) => {
|
|
||||||
const { data: projects } = api.project.all.useQuery();
|
|
||||||
|
|
||||||
const { data, refetch } = api.user.one.useQuery(
|
|
||||||
{
|
|
||||||
userId,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
enabled: !!userId,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const { mutateAsync, isError, error, isLoading } =
|
|
||||||
api.user.assignPermissions.useMutation();
|
|
||||||
|
|
||||||
const form = useForm<AddPermissions>({
|
|
||||||
defaultValues: {
|
|
||||||
accessedProjects: [],
|
|
||||||
accessedServices: [],
|
|
||||||
},
|
|
||||||
resolver: zodResolver(addPermissions),
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (data) {
|
|
||||||
form.reset({
|
|
||||||
accessedProjects: data.accessedProjects || [],
|
|
||||||
accessedServices: data.accessedServices || [],
|
|
||||||
canCreateProjects: data.canCreateProjects,
|
|
||||||
canCreateServices: data.canCreateServices,
|
|
||||||
canDeleteProjects: data.canDeleteProjects,
|
|
||||||
canDeleteServices: data.canDeleteServices,
|
|
||||||
canAccessToTraefikFiles: data.canAccessToTraefikFiles,
|
|
||||||
canAccessToDocker: data.canAccessToDocker,
|
|
||||||
canAccessToAPI: data.canAccessToAPI,
|
|
||||||
canAccessToSSHKeys: data.canAccessToSSHKeys,
|
|
||||||
canAccessToGitProviders: data.canAccessToGitProviders,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, [form, form.formState.isSubmitSuccessful, form.reset, data]);
|
|
||||||
|
|
||||||
const onSubmit = async (data: AddPermissions) => {
|
|
||||||
await mutateAsync({
|
|
||||||
id: userId,
|
|
||||||
canCreateServices: data.canCreateServices,
|
|
||||||
canCreateProjects: data.canCreateProjects,
|
|
||||||
canDeleteServices: data.canDeleteServices,
|
|
||||||
canDeleteProjects: data.canDeleteProjects,
|
|
||||||
canAccessToTraefikFiles: data.canAccessToTraefikFiles,
|
|
||||||
accessedProjects: data.accessedProjects || [],
|
|
||||||
accessedServices: data.accessedServices || [],
|
|
||||||
canAccessToDocker: data.canAccessToDocker,
|
|
||||||
canAccessToAPI: data.canAccessToAPI,
|
|
||||||
canAccessToSSHKeys: data.canAccessToSSHKeys,
|
|
||||||
canAccessToGitProviders: data.canAccessToGitProviders,
|
|
||||||
})
|
|
||||||
.then(async () => {
|
|
||||||
toast.success("Permissions updated");
|
|
||||||
refetch();
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
toast.error("Error updating the permissions");
|
|
||||||
});
|
|
||||||
};
|
|
||||||
return (
|
|
||||||
<Dialog>
|
|
||||||
<DialogTrigger className="" asChild>
|
|
||||||
<DropdownMenuItem
|
|
||||||
className="w-full cursor-pointer"
|
|
||||||
onSelect={(e) => e.preventDefault()}
|
|
||||||
>
|
|
||||||
Add Permissions
|
|
||||||
</DropdownMenuItem>
|
|
||||||
</DialogTrigger>
|
|
||||||
<DialogContent className="max-h-[85vh] sm:max-w-4xl">
|
|
||||||
<DialogHeader>
|
|
||||||
<DialogTitle>Permissions</DialogTitle>
|
|
||||||
<DialogDescription>Add or remove permissions</DialogDescription>
|
|
||||||
</DialogHeader>
|
|
||||||
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
|
||||||
|
|
||||||
<Form {...form}>
|
|
||||||
<form
|
|
||||||
id="hook-form-add-permissions"
|
|
||||||
onSubmit={form.handleSubmit(onSubmit)}
|
|
||||||
className="grid grid-cols-1 md:grid-cols-2 w-full gap-4"
|
|
||||||
>
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="canCreateProjects"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm">
|
|
||||||
<div className="space-y-0.5">
|
|
||||||
<FormLabel>Create Projects</FormLabel>
|
|
||||||
<FormDescription>
|
|
||||||
Allow the user to create projects
|
|
||||||
</FormDescription>
|
|
||||||
</div>
|
|
||||||
<FormControl>
|
|
||||||
<Switch
|
|
||||||
checked={field.value}
|
|
||||||
onCheckedChange={field.onChange}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="canDeleteProjects"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm">
|
|
||||||
<div className="space-y-0.5">
|
|
||||||
<FormLabel>Delete Projects</FormLabel>
|
|
||||||
<FormDescription>
|
|
||||||
Allow the user to delete projects
|
|
||||||
</FormDescription>
|
|
||||||
</div>
|
|
||||||
<FormControl>
|
|
||||||
<Switch
|
|
||||||
checked={field.value}
|
|
||||||
onCheckedChange={field.onChange}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="canCreateServices"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm">
|
|
||||||
<div className="space-y-0.5">
|
|
||||||
<FormLabel>Create Services</FormLabel>
|
|
||||||
<FormDescription>
|
|
||||||
Allow the user to create services
|
|
||||||
</FormDescription>
|
|
||||||
</div>
|
|
||||||
<FormControl>
|
|
||||||
<Switch
|
|
||||||
checked={field.value}
|
|
||||||
onCheckedChange={field.onChange}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="canDeleteServices"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm">
|
|
||||||
<div className="space-y-0.5">
|
|
||||||
<FormLabel>Delete Services</FormLabel>
|
|
||||||
<FormDescription>
|
|
||||||
Allow the user to delete services
|
|
||||||
</FormDescription>
|
|
||||||
</div>
|
|
||||||
<FormControl>
|
|
||||||
<Switch
|
|
||||||
checked={field.value}
|
|
||||||
onCheckedChange={field.onChange}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="canAccessToTraefikFiles"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm">
|
|
||||||
<div className="space-y-0.5">
|
|
||||||
<FormLabel>Access to Traefik Files</FormLabel>
|
|
||||||
<FormDescription>
|
|
||||||
Allow the user to access to the Traefik Tab Files
|
|
||||||
</FormDescription>
|
|
||||||
</div>
|
|
||||||
<FormControl>
|
|
||||||
<Switch
|
|
||||||
checked={field.value}
|
|
||||||
onCheckedChange={field.onChange}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="canAccessToDocker"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm">
|
|
||||||
<div className="space-y-0.5">
|
|
||||||
<FormLabel>Access to Docker</FormLabel>
|
|
||||||
<FormDescription>
|
|
||||||
Allow the user to access to the Docker Tab
|
|
||||||
</FormDescription>
|
|
||||||
</div>
|
|
||||||
<FormControl>
|
|
||||||
<Switch
|
|
||||||
checked={field.value}
|
|
||||||
onCheckedChange={field.onChange}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="canAccessToAPI"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm">
|
|
||||||
<div className="space-y-0.5">
|
|
||||||
<FormLabel>Access to API/CLI</FormLabel>
|
|
||||||
<FormDescription>
|
|
||||||
Allow the user to access to the API/CLI
|
|
||||||
</FormDescription>
|
|
||||||
</div>
|
|
||||||
<FormControl>
|
|
||||||
<Switch
|
|
||||||
checked={field.value}
|
|
||||||
onCheckedChange={field.onChange}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="canAccessToSSHKeys"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm">
|
|
||||||
<div className="space-y-0.5">
|
|
||||||
<FormLabel>Access to SSH Keys</FormLabel>
|
|
||||||
<FormDescription>
|
|
||||||
Allow to users to access to the SSH Keys section
|
|
||||||
</FormDescription>
|
|
||||||
</div>
|
|
||||||
<FormControl>
|
|
||||||
<Switch
|
|
||||||
checked={field.value}
|
|
||||||
onCheckedChange={field.onChange}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="canAccessToGitProviders"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm">
|
|
||||||
<div className="space-y-0.5">
|
|
||||||
<FormLabel>Access to Git Providers</FormLabel>
|
|
||||||
<FormDescription>
|
|
||||||
Allow to users to access to the Git Providers section
|
|
||||||
</FormDescription>
|
|
||||||
</div>
|
|
||||||
<FormControl>
|
|
||||||
<Switch
|
|
||||||
checked={field.value}
|
|
||||||
onCheckedChange={field.onChange}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="accessedProjects"
|
|
||||||
render={() => (
|
|
||||||
<FormItem className="md:col-span-2">
|
|
||||||
<div className="mb-4">
|
|
||||||
<FormLabel className="text-base">Projects</FormLabel>
|
|
||||||
<FormDescription>
|
|
||||||
Select the Projects that the user can access
|
|
||||||
</FormDescription>
|
|
||||||
</div>
|
|
||||||
{projects?.length === 0 && (
|
|
||||||
<p className="text-sm text-muted-foreground">
|
|
||||||
No projects found
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
<div className="grid md:grid-cols-2 gap-4">
|
|
||||||
{projects?.map((item, index) => {
|
|
||||||
const applications = extractServices(item);
|
|
||||||
return (
|
|
||||||
<FormField
|
|
||||||
key={`project-${index}`}
|
|
||||||
control={form.control}
|
|
||||||
name="accessedProjects"
|
|
||||||
render={({ field }) => {
|
|
||||||
return (
|
|
||||||
<FormItem
|
|
||||||
key={item.projectId}
|
|
||||||
className="flex flex-col items-start space-x-4 rounded-lg p-4 border"
|
|
||||||
>
|
|
||||||
<div className="flex flex-row gap-4">
|
|
||||||
<FormControl>
|
|
||||||
<Checkbox
|
|
||||||
checked={field.value?.includes(
|
|
||||||
item.projectId,
|
|
||||||
)}
|
|
||||||
onCheckedChange={(checked) => {
|
|
||||||
return checked
|
|
||||||
? field.onChange([
|
|
||||||
...(field.value || []),
|
|
||||||
item.projectId,
|
|
||||||
])
|
|
||||||
: field.onChange(
|
|
||||||
field.value?.filter(
|
|
||||||
(value) =>
|
|
||||||
value !== item.projectId,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
<FormLabel className="text-sm font-medium text-primary">
|
|
||||||
{item.name}
|
|
||||||
</FormLabel>
|
|
||||||
</div>
|
|
||||||
{applications.length === 0 && (
|
|
||||||
<p className="text-sm text-muted-foreground">
|
|
||||||
No services found
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
{applications?.map((item, index) => (
|
|
||||||
<FormField
|
|
||||||
key={`project-${index}`}
|
|
||||||
control={form.control}
|
|
||||||
name="accessedServices"
|
|
||||||
render={({ field }) => {
|
|
||||||
return (
|
|
||||||
<FormItem
|
|
||||||
key={item.id}
|
|
||||||
className="flex flex-row items-start space-x-3 space-y-0"
|
|
||||||
>
|
|
||||||
<FormControl>
|
|
||||||
<Checkbox
|
|
||||||
checked={field.value?.includes(
|
|
||||||
item.id,
|
|
||||||
)}
|
|
||||||
onCheckedChange={(checked) => {
|
|
||||||
return checked
|
|
||||||
? field.onChange([
|
|
||||||
...(field.value || []),
|
|
||||||
item.id,
|
|
||||||
])
|
|
||||||
: field.onChange(
|
|
||||||
field.value?.filter(
|
|
||||||
(value) =>
|
|
||||||
value !== item.id,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
<FormLabel className="text-sm text-muted-foreground">
|
|
||||||
{item.name}
|
|
||||||
</FormLabel>
|
|
||||||
</FormItem>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</FormItem>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<DialogFooter className="flex w-full flex-row justify-end md:col-span-2">
|
|
||||||
<Button
|
|
||||||
isLoading={isLoading}
|
|
||||||
form="hook-form-add-permissions"
|
|
||||||
type="submit"
|
|
||||||
>
|
|
||||||
Update
|
|
||||||
</Button>
|
|
||||||
</DialogFooter>
|
|
||||||
</form>
|
|
||||||
</Form>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -30,9 +30,10 @@ import { format } from "date-fns";
|
|||||||
import { MoreHorizontal, Users } from "lucide-react";
|
import { MoreHorizontal, Users } from "lucide-react";
|
||||||
import { Loader2 } from "lucide-react";
|
import { Loader2 } from "lucide-react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { AddUserPermissions } from "./add-permissions";
|
import { AddUserPermissionsV2 } from "./add-permissions-v2";
|
||||||
|
|
||||||
export const ShowUsers = () => {
|
export const ShowUsers = () => {
|
||||||
|
const { data: user } = api.user.get.useQuery();
|
||||||
const { data: isCloud } = api.settings.isCloud.useQuery();
|
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||||
const { data, isLoading, refetch } = api.user.all.useQuery();
|
const { data, isLoading, refetch } = api.user.all.useQuery();
|
||||||
const { mutateAsync } = api.user.remove.useMutation();
|
const { mutateAsync } = api.user.remove.useMutation();
|
||||||
@@ -84,20 +85,22 @@ export const ShowUsers = () => {
|
|||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{data?.map((member) => {
|
{data?.map((member) => {
|
||||||
|
const isSameUser = member.user.id === user?.user.id;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TableRow key={member.id}>
|
<TableRow key={member.id}>
|
||||||
<TableCell className="w-[100px]">
|
<TableCell className="w-[250px]">
|
||||||
{member.user.email}
|
{member.user.email} {isSameUser && "(You)"}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="text-center">
|
<TableCell className="text-center">
|
||||||
<Badge
|
<Badge
|
||||||
variant={
|
variant={
|
||||||
member.role === "owner"
|
member?.role?.name === "owner"
|
||||||
? "default"
|
? "default"
|
||||||
: "secondary"
|
: "secondary"
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{member.role}
|
{member?.role?.name}
|
||||||
</Badge>
|
</Badge>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="text-center">
|
<TableCell className="text-center">
|
||||||
@@ -112,35 +115,77 @@ export const ShowUsers = () => {
|
|||||||
</TableCell>
|
</TableCell>
|
||||||
|
|
||||||
<TableCell className="text-right flex justify-end">
|
<TableCell className="text-right flex justify-end">
|
||||||
<DropdownMenu>
|
{member.role !== "owner" && !isSameUser && (
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenu>
|
||||||
<Button
|
<DropdownMenuTrigger asChild>
|
||||||
variant="ghost"
|
<Button
|
||||||
className="h-8 w-8 p-0"
|
variant="ghost"
|
||||||
>
|
className="h-8 w-8 p-0"
|
||||||
<span className="sr-only">Open menu</span>
|
>
|
||||||
<MoreHorizontal className="h-4 w-4" />
|
<span className="sr-only">
|
||||||
</Button>
|
Open menu
|
||||||
</DropdownMenuTrigger>
|
</span>
|
||||||
<DropdownMenuContent align="end">
|
<MoreHorizontal className="h-4 w-4" />
|
||||||
<DropdownMenuLabel>
|
</Button>
|
||||||
Actions
|
</DropdownMenuTrigger>
|
||||||
</DropdownMenuLabel>
|
<DropdownMenuContent align="end">
|
||||||
|
<DropdownMenuLabel>
|
||||||
|
Actions
|
||||||
|
</DropdownMenuLabel>
|
||||||
|
|
||||||
{member.role !== "owner" && (
|
|
||||||
<AddUserPermissions
|
|
||||||
userId={member.user.id}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{member.role !== "owner" && (
|
|
||||||
<>
|
<>
|
||||||
{!isCloud && (
|
<AddUserPermissionsV2
|
||||||
<DialogAction
|
userId={member.user.id}
|
||||||
title="Delete User"
|
/>
|
||||||
description="Are you sure you want to delete this user?"
|
</>
|
||||||
type="destructive"
|
|
||||||
onClick={async () => {
|
{!isCloud && (
|
||||||
|
<DialogAction
|
||||||
|
title="Delete User"
|
||||||
|
description="Are you sure you want to delete this user?"
|
||||||
|
type="destructive"
|
||||||
|
onClick={async () => {
|
||||||
|
await mutateAsync({
|
||||||
|
userId: member.user.id,
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
toast.success(
|
||||||
|
"User deleted successfully",
|
||||||
|
);
|
||||||
|
refetch();
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
toast.error(
|
||||||
|
"Error deleting destination",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DropdownMenuItem
|
||||||
|
className="w-full cursor-pointer text-red-500 hover:!text-red-600"
|
||||||
|
onSelect={(e) => e.preventDefault()}
|
||||||
|
>
|
||||||
|
Delete User
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DialogAction>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<DialogAction
|
||||||
|
title="Unlink User"
|
||||||
|
description="Are you sure you want to unlink this user?"
|
||||||
|
type="destructive"
|
||||||
|
onClick={async () => {
|
||||||
|
if (!isCloud) {
|
||||||
|
const orgCount =
|
||||||
|
await utils.user.checkUserOrganizations.fetch(
|
||||||
|
{
|
||||||
|
userId: member.user.id,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log(orgCount);
|
||||||
|
|
||||||
|
if (orgCount === 1) {
|
||||||
await mutateAsync({
|
await mutateAsync({
|
||||||
userId: member.user.id,
|
userId: member.user.id,
|
||||||
})
|
})
|
||||||
@@ -152,86 +197,40 @@ export const ShowUsers = () => {
|
|||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error(
|
toast.error(
|
||||||
"Error deleting destination",
|
"Error deleting user",
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}}
|
return;
|
||||||
>
|
|
||||||
<DropdownMenuItem
|
|
||||||
className="w-full cursor-pointer text-red-500 hover:!text-red-600"
|
|
||||||
onSelect={(e) =>
|
|
||||||
e.preventDefault()
|
|
||||||
}
|
|
||||||
>
|
|
||||||
Delete User
|
|
||||||
</DropdownMenuItem>
|
|
||||||
</DialogAction>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<DialogAction
|
|
||||||
title="Unlink User"
|
|
||||||
description="Are you sure you want to unlink this user?"
|
|
||||||
type="destructive"
|
|
||||||
onClick={async () => {
|
|
||||||
if (!isCloud) {
|
|
||||||
const orgCount =
|
|
||||||
await utils.user.checkUserOrganizations.fetch(
|
|
||||||
{
|
|
||||||
userId: member.user.id,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
console.log(orgCount);
|
|
||||||
|
|
||||||
if (orgCount === 1) {
|
|
||||||
await mutateAsync({
|
|
||||||
userId: member.user.id,
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
toast.success(
|
|
||||||
"User deleted successfully",
|
|
||||||
);
|
|
||||||
refetch();
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
toast.error(
|
|
||||||
"Error deleting user",
|
|
||||||
);
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const { error } =
|
const { error } =
|
||||||
await authClient.organization.removeMember(
|
await authClient.organization.removeMember(
|
||||||
{
|
{
|
||||||
memberIdOrEmail: member.id,
|
memberIdOrEmail: member.id,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!error) {
|
if (!error) {
|
||||||
toast.success(
|
toast.success(
|
||||||
"User unlinked successfully",
|
"User unlinked successfully",
|
||||||
);
|
);
|
||||||
refetch();
|
refetch();
|
||||||
} else {
|
} else {
|
||||||
toast.error(
|
toast.error("Error unlinking user");
|
||||||
"Error unlinking user",
|
}
|
||||||
);
|
}}
|
||||||
}
|
>
|
||||||
}}
|
<DropdownMenuItem
|
||||||
|
className="w-full cursor-pointer text-red-500 hover:!text-red-600"
|
||||||
|
onSelect={(e) => e.preventDefault()}
|
||||||
>
|
>
|
||||||
<DropdownMenuItem
|
Unlink User
|
||||||
className="w-full cursor-pointer text-red-500 hover:!text-red-600"
|
</DropdownMenuItem>
|
||||||
onSelect={(e) => e.preventDefault()}
|
</DialogAction>
|
||||||
>
|
</DropdownMenuContent>
|
||||||
Unlink User
|
</DropdownMenu>
|
||||||
</DropdownMenuItem>
|
)}
|
||||||
</DialogAction>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</DropdownMenuContent>
|
|
||||||
</DropdownMenu>
|
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -62,9 +62,9 @@ type AddServerDomain = z.infer<typeof addServerDomain>;
|
|||||||
|
|
||||||
export const WebDomain = () => {
|
export const WebDomain = () => {
|
||||||
const { t } = useTranslation("settings");
|
const { t } = useTranslation("settings");
|
||||||
const { data, refetch } = api.user.get.useQuery();
|
const { data, refetch } = api.webServer.get.useQuery();
|
||||||
const { mutateAsync, isLoading } =
|
const { mutateAsync, isLoading } =
|
||||||
api.settings.assignDomainServer.useMutation();
|
api.webServer.assignDomainServer.useMutation();
|
||||||
|
|
||||||
const form = useForm<AddServerDomain>({
|
const form = useForm<AddServerDomain>({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
@@ -79,10 +79,10 @@ export const WebDomain = () => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (data) {
|
if (data) {
|
||||||
form.reset({
|
form.reset({
|
||||||
domain: data?.user?.host || "",
|
domain: data?.host || "",
|
||||||
certificateType: data?.user?.certificateType,
|
certificateType: data?.certificateType,
|
||||||
letsEncryptEmail: data?.user?.letsEncryptEmail || "",
|
letsEncryptEmail: data?.letsEncryptEmail || "",
|
||||||
https: data?.user?.https || false,
|
https: data?.https || false,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [form, form.reset, data]);
|
}, [form, form.reset, data]);
|
||||||
|
|||||||
@@ -16,13 +16,12 @@ import { UpdateServer } from "./web-server/update-server";
|
|||||||
|
|
||||||
export const WebServer = () => {
|
export const WebServer = () => {
|
||||||
const { t } = useTranslation("settings");
|
const { t } = useTranslation("settings");
|
||||||
const { data } = api.user.get.useQuery();
|
const { data } = api.webServer.get.useQuery();
|
||||||
|
|
||||||
const { data: dokployVersion } = api.settings.getDokployVersion.useQuery();
|
const { data: dokployVersion } = api.settings.getDokployVersion.useQuery();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
{/* <Card className={cn("rounded-lg w-full bg-transparent p-0", className)}></Card> */}
|
|
||||||
<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">
|
||||||
<div className="rounded-xl bg-background shadow-md ">
|
<div className="rounded-xl bg-background shadow-md ">
|
||||||
<CardHeader className="">
|
<CardHeader className="">
|
||||||
@@ -34,14 +33,6 @@ export const WebServer = () => {
|
|||||||
{t("settings.server.webServer.description")}
|
{t("settings.server.webServer.description")}
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
{/* <CardHeader>
|
|
||||||
<CardTitle className="text-xl">
|
|
||||||
{t("settings.server.webServer.title")}
|
|
||||||
</CardTitle>
|
|
||||||
<CardDescription>
|
|
||||||
{t("settings.server.webServer.description")}
|
|
||||||
</CardDescription>
|
|
||||||
</CardHeader> */}
|
|
||||||
<CardContent className="space-y-6 py-6 border-t">
|
<CardContent className="space-y-6 py-6 border-t">
|
||||||
<div className="grid md:grid-cols-2 gap-4">
|
<div className="grid md:grid-cols-2 gap-4">
|
||||||
<ShowDokployActions />
|
<ShowDokployActions />
|
||||||
@@ -53,7 +44,7 @@ export const WebServer = () => {
|
|||||||
|
|
||||||
<div className="flex items-center flex-wrap justify-between gap-4">
|
<div className="flex items-center flex-wrap justify-between gap-4">
|
||||||
<span className="text-sm text-muted-foreground">
|
<span className="text-sm text-muted-foreground">
|
||||||
Server IP: {data?.user.serverIp}
|
Server IP: {data?.serverIp}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-sm text-muted-foreground">
|
<span className="text-sm text-muted-foreground">
|
||||||
Version: {dokployVersion}
|
Version: {dokployVersion}
|
||||||
|
|||||||
@@ -46,15 +46,15 @@ interface Props {
|
|||||||
export const UpdateServerIp = ({ children }: Props) => {
|
export const UpdateServerIp = ({ children }: Props) => {
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
|
||||||
const { data } = api.user.get.useQuery();
|
const { data } = api.webServer.get.useQuery();
|
||||||
const { data: ip } = api.server.publicIp.useQuery();
|
const { data: ip } = api.server.publicIp.useQuery();
|
||||||
|
|
||||||
const { mutateAsync, isLoading, error, isError } =
|
const { mutateAsync, isLoading, error, isError } =
|
||||||
api.user.update.useMutation();
|
api.webServer.update.useMutation();
|
||||||
|
|
||||||
const form = useForm<Schema>({
|
const form = useForm<Schema>({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
serverIp: data?.user.serverIp || "",
|
serverIp: data?.serverIp || "",
|
||||||
},
|
},
|
||||||
resolver: zodResolver(schema),
|
resolver: zodResolver(schema),
|
||||||
});
|
});
|
||||||
@@ -62,7 +62,7 @@ export const UpdateServerIp = ({ children }: Props) => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (data) {
|
if (data) {
|
||||||
form.reset({
|
form.reset({
|
||||||
serverIp: data.user.serverIp || "",
|
serverIp: data.serverIp || "",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [form, form.reset, data]);
|
}, [form, form.reset, data]);
|
||||||
@@ -80,7 +80,7 @@ export const UpdateServerIp = ({ children }: Props) => {
|
|||||||
})
|
})
|
||||||
.then(async () => {
|
.then(async () => {
|
||||||
toast.success("Server IP Updated");
|
toast.success("Server IP Updated");
|
||||||
await utils.user.get.invalidate();
|
await utils.webServer.get.invalidate();
|
||||||
setIsOpen(false);
|
setIsOpen(false);
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
|
|||||||
28
apps/dokploy/components/dashboard/shared/Permissions.tsx
Normal file
28
apps/dokploy/components/dashboard/shared/Permissions.tsx
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import { api } from "@/utils/api";
|
||||||
|
import type { PermissionName } from "@dokploy/server/lib/permissions";
|
||||||
|
import { useMemo } from "react";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
permissions: PermissionName[];
|
||||||
|
children: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Permissions = ({ permissions, children }: Props) => {
|
||||||
|
const { data: auth } = api.user.get.useQuery();
|
||||||
|
|
||||||
|
const hasPermission = useMemo(() => {
|
||||||
|
if (auth?.role?.name === "owner" || auth?.role?.name === "admin") {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return permissions.some((permission) =>
|
||||||
|
auth?.role?.permissions?.includes(permission),
|
||||||
|
);
|
||||||
|
}, [permissions, auth]);
|
||||||
|
|
||||||
|
if (!hasPermission) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <>{children}</>;
|
||||||
|
};
|
||||||
@@ -128,7 +128,7 @@ export default function SwarmMonitorCard({ serverId }: Props) {
|
|||||||
</div>
|
</div>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent>
|
<TooltipContent>
|
||||||
<div className="max-h-48 overflow-y-auto">
|
<div className="max-h-48">
|
||||||
{activeNodes.map((node) => (
|
{activeNodes.map((node) => (
|
||||||
<div key={node.ID} className="flex items-center gap-2">
|
<div key={node.ID} className="flex items-center gap-2">
|
||||||
{node.Hostname}
|
{node.Hostname}
|
||||||
@@ -162,7 +162,7 @@ export default function SwarmMonitorCard({ serverId }: Props) {
|
|||||||
</div>
|
</div>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent>
|
<TooltipContent>
|
||||||
<div className="max-h-48 overflow-y-auto">
|
<div className="max-h-48">
|
||||||
{managerNodes.map((node) => (
|
{managerNodes.map((node) => (
|
||||||
<div key={node.ID} className="flex items-center gap-2">
|
<div key={node.ID} className="flex items-center gap-2">
|
||||||
{node.Hostname}
|
{node.Hostname}
|
||||||
|
|||||||
@@ -87,8 +87,8 @@ import { Logo } from "../shared/logo";
|
|||||||
import { Button } from "../ui/button";
|
import { Button } from "../ui/button";
|
||||||
import { UpdateServerButton } from "./update-server";
|
import { UpdateServerButton } from "./update-server";
|
||||||
import { UserNav } from "./user-nav";
|
import { UserNav } from "./user-nav";
|
||||||
|
import { PERMISSIONS } from "@dokploy/server/lib/permissions";
|
||||||
|
|
||||||
// The types of the queries we are going to use
|
|
||||||
type AuthQueryOutput = inferRouterOutputs<AppRouter>["user"]["get"];
|
type AuthQueryOutput = inferRouterOutputs<AppRouter>["user"]["get"];
|
||||||
|
|
||||||
type SingleNavItem = {
|
type SingleNavItem = {
|
||||||
@@ -102,10 +102,6 @@ type SingleNavItem = {
|
|||||||
}) => boolean;
|
}) => boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
// NavItem type
|
|
||||||
// Consists of a single item or a group of items
|
|
||||||
// If `isSingle` is true or undefined, the item is a single item
|
|
||||||
// If `isSingle` is false, the item is a group of items
|
|
||||||
type NavItem =
|
type NavItem =
|
||||||
| SingleNavItem
|
| SingleNavItem
|
||||||
| {
|
| {
|
||||||
@@ -119,8 +115,6 @@ type NavItem =
|
|||||||
}) => boolean;
|
}) => boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
// ExternalLink type
|
|
||||||
// Represents an external link item (used for the help section)
|
|
||||||
type ExternalLink = {
|
type ExternalLink = {
|
||||||
name: string;
|
name: string;
|
||||||
url: string;
|
url: string;
|
||||||
@@ -131,18 +125,12 @@ type ExternalLink = {
|
|||||||
}) => boolean;
|
}) => boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Menu type
|
|
||||||
// Consists of home, settings, and help items
|
|
||||||
type Menu = {
|
type Menu = {
|
||||||
home: NavItem[];
|
home: NavItem[];
|
||||||
settings: NavItem[];
|
settings: NavItem[];
|
||||||
help: ExternalLink[];
|
help: ExternalLink[];
|
||||||
};
|
};
|
||||||
|
|
||||||
// Menu items
|
|
||||||
// Consists of unfiltered home, settings, and help items
|
|
||||||
// The items are filtered based on the user's role and permissions
|
|
||||||
// The `isEnabled` function is called to determine if the item should be displayed
|
|
||||||
const MENU: Menu = {
|
const MENU: Menu = {
|
||||||
home: [
|
home: [
|
||||||
{
|
{
|
||||||
@@ -165,7 +153,8 @@ 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 }) => !isCloud && auth?.role === "owner",
|
isEnabled: ({ isCloud, auth }) =>
|
||||||
|
!isCloud && auth?.role?.name === "owner",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
isSingle: true,
|
isSingle: true,
|
||||||
@@ -175,7 +164,10 @@ const MENU: Menu = {
|
|||||||
// Only enabled for admins and users with access to Traefik files in non-cloud environments
|
// Only enabled for admins and users with access to Traefik files in non-cloud environments
|
||||||
isEnabled: ({ auth, isCloud }) =>
|
isEnabled: ({ auth, isCloud }) =>
|
||||||
!!(
|
!!(
|
||||||
(auth?.role === "owner" || auth?.canAccessToTraefikFiles) &&
|
(auth?.role?.name === "owner" ||
|
||||||
|
auth?.role?.permissions?.includes(
|
||||||
|
PERMISSIONS.TRAEFIK.ACCESS.name,
|
||||||
|
)) &&
|
||||||
!isCloud
|
!isCloud
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
@@ -186,7 +178,11 @@ const MENU: Menu = {
|
|||||||
icon: BlocksIcon,
|
icon: BlocksIcon,
|
||||||
// Only enabled for admins and users with access to Docker in non-cloud environments
|
// Only enabled for admins and users with access to Docker in non-cloud environments
|
||||||
isEnabled: ({ auth, isCloud }) =>
|
isEnabled: ({ auth, isCloud }) =>
|
||||||
!!((auth?.role === "owner" || auth?.canAccessToDocker) && !isCloud),
|
!!(
|
||||||
|
(auth?.role?.name === "owner" ||
|
||||||
|
auth?.role?.permissions?.includes(PERMISSIONS.DOCKER.VIEW.name)) &&
|
||||||
|
!isCloud
|
||||||
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
isSingle: true,
|
isSingle: true,
|
||||||
@@ -195,7 +191,11 @@ const MENU: Menu = {
|
|||||||
icon: PieChart,
|
icon: PieChart,
|
||||||
// Only enabled for admins and users with access to Docker in non-cloud environments
|
// Only enabled for admins and users with access to Docker in non-cloud environments
|
||||||
isEnabled: ({ auth, isCloud }) =>
|
isEnabled: ({ auth, isCloud }) =>
|
||||||
!!((auth?.role === "owner" || auth?.canAccessToDocker) && !isCloud),
|
!!(
|
||||||
|
(auth?.role?.name === "owner" ||
|
||||||
|
auth?.role?.permissions?.includes(PERMISSIONS.DOCKER.VIEW.name)) &&
|
||||||
|
!isCloud
|
||||||
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
isSingle: true,
|
isSingle: true,
|
||||||
@@ -204,64 +204,12 @@ const MENU: Menu = {
|
|||||||
icon: Forward,
|
icon: Forward,
|
||||||
// Only enabled for admins and users with access to Docker in non-cloud environments
|
// Only enabled for admins and users with access to Docker in non-cloud environments
|
||||||
isEnabled: ({ auth, isCloud }) =>
|
isEnabled: ({ auth, isCloud }) =>
|
||||||
!!((auth?.role === "owner" || auth?.canAccessToDocker) && !isCloud),
|
!!(
|
||||||
|
(auth?.role?.name === "owner" ||
|
||||||
|
auth?.role?.permissions?.includes(PERMISSIONS.DOCKER.VIEW.name)) &&
|
||||||
|
!isCloud
|
||||||
|
),
|
||||||
},
|
},
|
||||||
|
|
||||||
// Legacy unused menu, adjusted to the new structure
|
|
||||||
// {
|
|
||||||
// isSingle: true,
|
|
||||||
// title: "Projects",
|
|
||||||
// url: "/dashboard/projects",
|
|
||||||
// icon: Folder,
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// isSingle: true,
|
|
||||||
// title: "Monitoring",
|
|
||||||
// icon: BarChartHorizontalBigIcon,
|
|
||||||
// url: "/dashboard/settings/monitoring",
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// isSingle: false,
|
|
||||||
// title: "Settings",
|
|
||||||
// icon: Settings2,
|
|
||||||
// items: [
|
|
||||||
// {
|
|
||||||
// title: "Profile",
|
|
||||||
// url: "/dashboard/settings/profile",
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// title: "Users",
|
|
||||||
// url: "/dashboard/settings/users",
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// title: "SSH Key",
|
|
||||||
// url: "/dashboard/settings/ssh-keys",
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// title: "Git",
|
|
||||||
// url: "/dashboard/settings/git-providers",
|
|
||||||
// },
|
|
||||||
// ],
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// isSingle: false,
|
|
||||||
// title: "Integrations",
|
|
||||||
// icon: BlocksIcon,
|
|
||||||
// items: [
|
|
||||||
// {
|
|
||||||
// title: "S3 Destinations",
|
|
||||||
// url: "/dashboard/settings/destinations",
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// title: "Registry",
|
|
||||||
// url: "/dashboard/settings/registry",
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// title: "Notifications",
|
|
||||||
// url: "/dashboard/settings/notifications",
|
|
||||||
// },
|
|
||||||
// ],
|
|
||||||
// },
|
|
||||||
],
|
],
|
||||||
|
|
||||||
settings: [
|
settings: [
|
||||||
@@ -271,7 +219,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 }) => !!(auth?.role === "owner" && !isCloud),
|
isEnabled: ({ auth, isCloud }) =>
|
||||||
|
!!(auth?.role?.name === "owner" && !isCloud),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
isSingle: true,
|
isSingle: true,
|
||||||
@@ -285,7 +234,7 @@ const MENU: Menu = {
|
|||||||
url: "/dashboard/settings/servers",
|
url: "/dashboard/settings/servers",
|
||||||
icon: Server,
|
icon: Server,
|
||||||
// Only enabled for admins
|
// Only enabled for admins
|
||||||
isEnabled: ({ auth }) => !!(auth?.role === "owner"),
|
isEnabled: ({ auth }) => !!(auth?.role?.name === "owner"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
isSingle: true,
|
isSingle: true,
|
||||||
@@ -293,7 +242,7 @@ const MENU: Menu = {
|
|||||||
icon: Users,
|
icon: Users,
|
||||||
url: "/dashboard/settings/users",
|
url: "/dashboard/settings/users",
|
||||||
// Only enabled for admins
|
// Only enabled for admins
|
||||||
isEnabled: ({ auth }) => !!(auth?.role === "owner"),
|
isEnabled: ({ auth }) => !!(auth?.role?.name === "owner"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
isSingle: true,
|
isSingle: true,
|
||||||
@@ -302,14 +251,17 @@ const MENU: Menu = {
|
|||||||
url: "/dashboard/settings/ssh-keys",
|
url: "/dashboard/settings/ssh-keys",
|
||||||
// Only enabled for admins and users with access to SSH keys
|
// Only enabled for admins and users with access to SSH keys
|
||||||
isEnabled: ({ auth }) =>
|
isEnabled: ({ auth }) =>
|
||||||
!!(auth?.role === "owner" || auth?.canAccessToSSHKeys),
|
!!(
|
||||||
|
auth?.role?.name === "owner" ||
|
||||||
|
auth?.role?.permissions?.includes(PERMISSIONS.SSH_KEYS.ACCESS.name)
|
||||||
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "AI",
|
title: "AI",
|
||||||
icon: BotIcon,
|
icon: BotIcon,
|
||||||
url: "/dashboard/settings/ai",
|
url: "/dashboard/settings/ai",
|
||||||
isSingle: true,
|
isSingle: true,
|
||||||
isEnabled: ({ auth }) => !!(auth?.role === "owner"),
|
isEnabled: ({ auth }) => !!(auth?.role?.name === "owner"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
isSingle: true,
|
isSingle: true,
|
||||||
@@ -318,7 +270,12 @@ const MENU: Menu = {
|
|||||||
icon: GitBranch,
|
icon: GitBranch,
|
||||||
// Only enabled for admins and users with access to Git providers
|
// Only enabled for admins and users with access to Git providers
|
||||||
isEnabled: ({ auth }) =>
|
isEnabled: ({ auth }) =>
|
||||||
!!(auth?.role === "owner" || auth?.canAccessToGitProviders),
|
!!(
|
||||||
|
auth?.role?.name === "owner" ||
|
||||||
|
auth?.role?.permissions?.includes(
|
||||||
|
PERMISSIONS.GIT_PROVIDERS.ACCESS.name,
|
||||||
|
)
|
||||||
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
isSingle: true,
|
isSingle: true,
|
||||||
@@ -326,7 +283,7 @@ const MENU: Menu = {
|
|||||||
url: "/dashboard/settings/registry",
|
url: "/dashboard/settings/registry",
|
||||||
icon: Package,
|
icon: Package,
|
||||||
// Only enabled for admins
|
// Only enabled for admins
|
||||||
isEnabled: ({ auth }) => !!(auth?.role === "owner"),
|
isEnabled: ({ auth }) => !!(auth?.role?.name === "owner"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
isSingle: true,
|
isSingle: true,
|
||||||
@@ -334,7 +291,7 @@ const MENU: Menu = {
|
|||||||
url: "/dashboard/settings/destinations",
|
url: "/dashboard/settings/destinations",
|
||||||
icon: Database,
|
icon: Database,
|
||||||
// Only enabled for admins
|
// Only enabled for admins
|
||||||
isEnabled: ({ auth }) => !!(auth?.role === "owner"),
|
isEnabled: ({ auth }) => !!(auth?.role?.name === "owner"),
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
@@ -343,7 +300,7 @@ const MENU: Menu = {
|
|||||||
url: "/dashboard/settings/certificates",
|
url: "/dashboard/settings/certificates",
|
||||||
icon: ShieldCheck,
|
icon: ShieldCheck,
|
||||||
// Only enabled for admins
|
// Only enabled for admins
|
||||||
isEnabled: ({ auth }) => !!(auth?.role === "owner"),
|
isEnabled: ({ auth }) => !!(auth?.role?.name === "owner"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
isSingle: true,
|
isSingle: true,
|
||||||
@@ -351,7 +308,8 @@ 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 }) => !!(auth?.role === "owner" && !isCloud),
|
isEnabled: ({ auth, isCloud }) =>
|
||||||
|
!!(auth?.role?.name === "owner" && !isCloud),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
isSingle: true,
|
isSingle: true,
|
||||||
@@ -359,7 +317,7 @@ const MENU: Menu = {
|
|||||||
url: "/dashboard/settings/notifications",
|
url: "/dashboard/settings/notifications",
|
||||||
icon: Bell,
|
icon: Bell,
|
||||||
// Only enabled for admins
|
// Only enabled for admins
|
||||||
isEnabled: ({ auth }) => !!(auth?.role === "owner"),
|
isEnabled: ({ auth }) => !!(auth?.role?.name === "owner"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
isSingle: true,
|
isSingle: true,
|
||||||
@@ -367,7 +325,8 @@ const MENU: Menu = {
|
|||||||
url: "/dashboard/settings/billing",
|
url: "/dashboard/settings/billing",
|
||||||
icon: CreditCard,
|
icon: CreditCard,
|
||||||
// Only enabled for admins in cloud environments
|
// Only enabled for admins in cloud environments
|
||||||
isEnabled: ({ auth, isCloud }) => !!(auth?.role === "owner" && isCloud),
|
isEnabled: ({ auth, isCloud }) =>
|
||||||
|
!!(auth?.role?.name === "owner" && isCloud),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|
||||||
@@ -505,6 +464,7 @@ function SidebarLogo() {
|
|||||||
const { state } = useSidebar();
|
const { state } = useSidebar();
|
||||||
const { data: isCloud } = api.settings.isCloud.useQuery();
|
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||||
const { data: user } = api.user.get.useQuery();
|
const { data: user } = api.user.get.useQuery();
|
||||||
|
console.log(user);
|
||||||
const { data: session } = authClient.useSession();
|
const { data: session } = authClient.useSession();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@@ -663,7 +623,7 @@ function SidebarLogo() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
{(user?.role === "owner" || isCloud) && (
|
{(user?.role?.name === "owner" || isCloud) && (
|
||||||
<>
|
<>
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
<AddOrganization />
|
<AddOrganization />
|
||||||
@@ -1029,7 +989,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" && (
|
{!isCloud && auth?.role?.name === "owner" && (
|
||||||
<SidebarMenuItem>
|
<SidebarMenuItem>
|
||||||
<UpdateServerButton />
|
<UpdateServerButton />
|
||||||
</SidebarMenuItem>
|
</SidebarMenuItem>
|
||||||
|
|||||||
@@ -23,6 +23,8 @@ import { ChevronsUpDown } from "lucide-react";
|
|||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { ModeToggle } from "../ui/modeToggle";
|
import { ModeToggle } from "../ui/modeToggle";
|
||||||
import { SidebarMenuButton } from "../ui/sidebar";
|
import { SidebarMenuButton } from "../ui/sidebar";
|
||||||
|
import { Permissions } from "../dashboard/shared/Permissions";
|
||||||
|
import { PERMISSIONS } from "@dokploy/server/lib/permissions";
|
||||||
|
|
||||||
const _AUTO_CHECK_UPDATES_INTERVAL_MINUTES = 7;
|
const _AUTO_CHECK_UPDATES_INTERVAL_MINUTES = 7;
|
||||||
|
|
||||||
@@ -98,7 +100,7 @@ export const UserNav = () => {
|
|||||||
>
|
>
|
||||||
Monitoring
|
Monitoring
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
{(data?.role === "owner" || data?.canAccessToTraefikFiles) && (
|
<Permissions permissions={[PERMISSIONS.TRAEFIK.ACCESS.name]}>
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
className="cursor-pointer"
|
className="cursor-pointer"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@@ -107,8 +109,9 @@ export const UserNav = () => {
|
|||||||
>
|
>
|
||||||
Traefik
|
Traefik
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
)}
|
</Permissions>
|
||||||
{(data?.role === "owner" || data?.canAccessToDocker) && (
|
|
||||||
|
<Permissions permissions={[PERMISSIONS.DOCKER.VIEW.name]}>
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
className="cursor-pointer"
|
className="cursor-pointer"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@@ -119,11 +122,11 @@ export const UserNav = () => {
|
|||||||
>
|
>
|
||||||
Docker
|
Docker
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
)}
|
</Permissions>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
{data?.role === "owner" && (
|
{data?.role?.name === "owner" && (
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
className="cursor-pointer"
|
className="cursor-pointer"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@@ -136,7 +139,7 @@ export const UserNav = () => {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</DropdownMenuGroup>
|
</DropdownMenuGroup>
|
||||||
{isCloud && data?.role === "owner" && (
|
{isCloud && data?.role?.name === "owner" && (
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
className="cursor-pointer"
|
className="cursor-pointer"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@@ -154,9 +157,6 @@ export const UserNav = () => {
|
|||||||
await authClient.signOut().then(() => {
|
await authClient.signOut().then(() => {
|
||||||
router.push("/");
|
router.push("/");
|
||||||
});
|
});
|
||||||
// await mutateAsync().then(() => {
|
|
||||||
// router.push("/");
|
|
||||||
// });
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Log out
|
Log out
|
||||||
|
|||||||
187
apps/dokploy/drizzle/0103_brainy_nehzno.sql
Normal file
187
apps/dokploy/drizzle/0103_brainy_nehzno.sql
Normal file
@@ -0,0 +1,187 @@
|
|||||||
|
CREATE TABLE "member_role" (
|
||||||
|
"roleId" text PRIMARY KEY NOT NULL,
|
||||||
|
"name" text NOT NULL,
|
||||||
|
"description" text,
|
||||||
|
"canDelete" boolean DEFAULT true NOT NULL,
|
||||||
|
"is_system" boolean DEFAULT false,
|
||||||
|
"permissions" text[],
|
||||||
|
"created_at" timestamp DEFAULT now() NOT NULL,
|
||||||
|
"updated_at" timestamp DEFAULT now() NOT NULL,
|
||||||
|
"organizationId" text NOT NULL,
|
||||||
|
CONSTRAINT "member_role_name_unique" UNIQUE("name"),
|
||||||
|
CONSTRAINT "role_name_unique" UNIQUE("name","organizationId")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Create default roles for each organization
|
||||||
|
DO $$
|
||||||
|
DECLARE
|
||||||
|
org RECORD;
|
||||||
|
BEGIN
|
||||||
|
FOR org IN SELECT id FROM "organization"
|
||||||
|
LOOP
|
||||||
|
-- Insert owner role
|
||||||
|
INSERT INTO "member_role" ("roleId", "name", "description", "canDelete", "is_system", "permissions", "created_at", "updated_at", "organizationId")
|
||||||
|
VALUES (
|
||||||
|
org.id || '_owner',
|
||||||
|
'owner',
|
||||||
|
'Owner role with full access',
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
'{"project:create", "project:delete", "service:create", "service:delete", "traefik_files:access", "docker:view", "api:access", "ssh_keys:access", "git_providers:access", "schedules:access"}',
|
||||||
|
NOW(),
|
||||||
|
NOW(),
|
||||||
|
org.id
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Insert admin role
|
||||||
|
INSERT INTO "member_role" ("roleId", "name", "description", "canDelete", "is_system", "permissions", "created_at", "updated_at", "organizationId")
|
||||||
|
VALUES (
|
||||||
|
org.id || '_admin',
|
||||||
|
'admin',
|
||||||
|
'Administrator role with elevated access',
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
'{"project:create", "project:delete", "service:create", "service:delete", "traefik_files:access", "docker:view", "api:access", "ssh_keys:access", "schedules:access"}',
|
||||||
|
NOW(),
|
||||||
|
NOW(),
|
||||||
|
org.id
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Insert member role
|
||||||
|
INSERT INTO "member_role" ("roleId", "name", "description", "canDelete", "is_system", "permissions", "created_at", "updated_at", "organizationId")
|
||||||
|
VALUES (
|
||||||
|
org.id || '_member',
|
||||||
|
'member',
|
||||||
|
'Standard member role',
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
'{"project:create", "service:create", "docker:view"}',
|
||||||
|
NOW(),
|
||||||
|
NOW(),
|
||||||
|
org.id
|
||||||
|
);
|
||||||
|
END LOOP;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
|
||||||
|
--> statement-breakpoint
|
||||||
|
ALTER TABLE "user_temp" RENAME TO "users";--> statement-breakpoint
|
||||||
|
ALTER TABLE "users" DROP CONSTRAINT "user_temp_email_unique";--> statement-breakpoint
|
||||||
|
ALTER TABLE "backup" DROP CONSTRAINT "backup_userId_user_temp_id_fk";
|
||||||
|
--> statement-breakpoint
|
||||||
|
ALTER TABLE "session_temp" DROP CONSTRAINT "session_temp_user_id_user_temp_id_fk";
|
||||||
|
--> statement-breakpoint
|
||||||
|
ALTER TABLE "git_provider" DROP CONSTRAINT "git_provider_userId_user_temp_id_fk";
|
||||||
|
--> statement-breakpoint
|
||||||
|
ALTER TABLE "account" DROP CONSTRAINT "account_user_id_user_temp_id_fk";
|
||||||
|
--> statement-breakpoint
|
||||||
|
ALTER TABLE "apikey" DROP CONSTRAINT "apikey_user_id_user_temp_id_fk";
|
||||||
|
--> statement-breakpoint
|
||||||
|
ALTER TABLE "invitation" DROP CONSTRAINT "invitation_inviter_id_user_temp_id_fk";
|
||||||
|
--> statement-breakpoint
|
||||||
|
ALTER TABLE "member" DROP CONSTRAINT "member_user_id_user_temp_id_fk";
|
||||||
|
--> statement-breakpoint
|
||||||
|
ALTER TABLE "organization" DROP CONSTRAINT "organization_owner_id_user_temp_id_fk";
|
||||||
|
--> statement-breakpoint
|
||||||
|
ALTER TABLE "two_factor" DROP CONSTRAINT "two_factor_user_id_user_temp_id_fk";
|
||||||
|
--> statement-breakpoint
|
||||||
|
ALTER TABLE "schedule" DROP CONSTRAINT "schedule_userId_user_temp_id_fk";
|
||||||
|
--> statement-breakpoint
|
||||||
|
ALTER TABLE "users" ALTER COLUMN "created_at" SET NOT NULL;--> statement-breakpoint
|
||||||
|
ALTER TABLE "users" ALTER COLUMN "updated_at" SET DEFAULT now();--> statement-breakpoint
|
||||||
|
ALTER TABLE "member" ALTER COLUMN "role" DROP NOT NULL;--> statement-breakpoint
|
||||||
|
ALTER TABLE "member" ADD COLUMN "roleId" text;--> statement-breakpoint
|
||||||
|
ALTER TABLE "member_role" ADD CONSTRAINT "member_role_organizationId_organization_id_fk" FOREIGN KEY ("organizationId") REFERENCES "public"."organization"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||||
|
ALTER TABLE "backup" ADD CONSTRAINT "backup_userId_users_id_fk" FOREIGN KEY ("userId") REFERENCES "public"."users"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
||||||
|
ALTER TABLE "session_temp" ADD CONSTRAINT "session_temp_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||||
|
ALTER TABLE "git_provider" ADD CONSTRAINT "git_provider_userId_users_id_fk" FOREIGN KEY ("userId") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||||
|
ALTER TABLE "account" ADD CONSTRAINT "account_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||||
|
ALTER TABLE "apikey" ADD CONSTRAINT "apikey_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||||
|
ALTER TABLE "invitation" ADD CONSTRAINT "invitation_inviter_id_users_id_fk" FOREIGN KEY ("inviter_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||||
|
ALTER TABLE "member" ADD CONSTRAINT "member_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||||
|
ALTER TABLE "member" ADD CONSTRAINT "member_roleId_member_role_roleId_fk" FOREIGN KEY ("roleId") REFERENCES "public"."member_role"("roleId") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||||
|
|
||||||
|
-- Update existing members with corresponding roles based on their current role type
|
||||||
|
DO $$
|
||||||
|
DECLARE
|
||||||
|
mem RECORD;
|
||||||
|
BEGIN
|
||||||
|
FOR mem IN SELECT m.id, m.organization_id, m.role as role_type FROM "member" m
|
||||||
|
LOOP
|
||||||
|
UPDATE "member"
|
||||||
|
SET "roleId" = mem.organization_id || '_' || mem.role_type
|
||||||
|
WHERE id = mem.id;
|
||||||
|
END LOOP;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
|
||||||
|
ALTER TABLE "organization" ADD CONSTRAINT "organization_owner_id_users_id_fk" FOREIGN KEY ("owner_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||||
|
ALTER TABLE "two_factor" ADD CONSTRAINT "two_factor_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||||
|
ALTER TABLE "schedule" ADD CONSTRAINT "schedule_userId_users_id_fk" FOREIGN KEY ("userId") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||||
|
|
||||||
|
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "web_server" (
|
||||||
|
"webServerId" text PRIMARY KEY NOT NULL,
|
||||||
|
"serverIp" text,
|
||||||
|
"certificateType" "certificateType" DEFAULT 'none' NOT NULL,
|
||||||
|
"https" boolean DEFAULT false NOT NULL,
|
||||||
|
"host" text,
|
||||||
|
"letsEncryptEmail" text,
|
||||||
|
"sshPrivateKey" text,
|
||||||
|
"enableDockerCleanup" boolean DEFAULT false NOT NULL,
|
||||||
|
"logCleanupCron" text DEFAULT '0 0 * * *',
|
||||||
|
"metricsConfig" jsonb DEFAULT '{"server":{"type":"Dokploy","refreshRate":60,"port":4500,"token":"","retentionDays":2,"cronJob":"","urlCallback":"","thresholds":{"cpu":0,"memory":0}},"containers":{"refreshRate":60,"services":{"include":[],"exclude":[]}}}'::jsonb NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
INSERT INTO "web_server" (
|
||||||
|
"webServerId",
|
||||||
|
"serverIp",
|
||||||
|
"certificateType",
|
||||||
|
"https",
|
||||||
|
"host",
|
||||||
|
"letsEncryptEmail",
|
||||||
|
"sshPrivateKey",
|
||||||
|
"enableDockerCleanup",
|
||||||
|
"logCleanupCron",
|
||||||
|
"metricsConfig"
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
gen_random_uuid() as "webServerId",
|
||||||
|
u."serverIp",
|
||||||
|
COALESCE(u."certificateType", 'none') as "certificateType",
|
||||||
|
COALESCE(u."https", false) as "https",
|
||||||
|
u."host",
|
||||||
|
u."letsEncryptEmail",
|
||||||
|
u."sshPrivateKey",
|
||||||
|
COALESCE(u."enableDockerCleanup", false) as "enableDockerCleanup",
|
||||||
|
COALESCE(u."logCleanupCron", '0 0 * * *') as "logCleanupCron",
|
||||||
|
COALESCE(u."metricsConfig", '{"server":{"type":"Dokploy","refreshRate":60,"port":4500,"token":"","retentionDays":2,"cronJob":"","urlCallback":"","thresholds":{"cpu":0,"memory":0}},"containers":{"refreshRate":60,"services":{"include":[],"exclude":[]}}}') as "metricsConfig"
|
||||||
|
FROM "users" u
|
||||||
|
INNER JOIN "organization" o ON u.id = o.owner_id
|
||||||
|
LIMIT 1;
|
||||||
|
|
||||||
|
|
||||||
|
ALTER TABLE "users" DROP COLUMN "createdAt";--> statement-breakpoint
|
||||||
|
ALTER TABLE "users" DROP COLUMN "serverIp";--> statement-breakpoint
|
||||||
|
ALTER TABLE "users" DROP COLUMN "certificateType";--> statement-breakpoint
|
||||||
|
ALTER TABLE "users" DROP COLUMN "https";--> statement-breakpoint
|
||||||
|
ALTER TABLE "users" DROP COLUMN "host";--> statement-breakpoint
|
||||||
|
ALTER TABLE "users" DROP COLUMN "letsEncryptEmail";--> statement-breakpoint
|
||||||
|
ALTER TABLE "users" DROP COLUMN "sshPrivateKey";--> statement-breakpoint
|
||||||
|
ALTER TABLE "users" DROP COLUMN "enableDockerCleanup";--> statement-breakpoint
|
||||||
|
ALTER TABLE "users" DROP COLUMN "logCleanupCron";--> statement-breakpoint
|
||||||
|
ALTER TABLE "users" DROP COLUMN "metricsConfig";--> statement-breakpoint
|
||||||
|
ALTER TABLE "users" DROP COLUMN "cleanupCacheApplications";--> statement-breakpoint
|
||||||
|
ALTER TABLE "users" DROP COLUMN "cleanupCacheOnPreviews";--> statement-breakpoint
|
||||||
|
ALTER TABLE "users" DROP COLUMN "cleanupCacheOnCompose";--> statement-breakpoint
|
||||||
|
ALTER TABLE "member" DROP COLUMN "canCreateProjects";--> statement-breakpoint
|
||||||
|
ALTER TABLE "member" DROP COLUMN "canAccessToSSHKeys";--> statement-breakpoint
|
||||||
|
ALTER TABLE "member" DROP COLUMN "canCreateServices";--> statement-breakpoint
|
||||||
|
ALTER TABLE "member" DROP COLUMN "canDeleteProjects";--> statement-breakpoint
|
||||||
|
ALTER TABLE "member" DROP COLUMN "canDeleteServices";--> statement-breakpoint
|
||||||
|
ALTER TABLE "member" DROP COLUMN "canAccessToDocker";--> statement-breakpoint
|
||||||
|
ALTER TABLE "member" DROP COLUMN "canAccessToAPI";--> statement-breakpoint
|
||||||
|
ALTER TABLE "member" DROP COLUMN "canAccessToGitProviders";--> statement-breakpoint
|
||||||
|
ALTER TABLE "member" DROP COLUMN "canAccessToTraefikFiles";--> statement-breakpoint
|
||||||
|
ALTER TABLE "users" ADD CONSTRAINT "users_email_unique" UNIQUE("email");
|
||||||
6179
apps/dokploy/drizzle/meta/0103_snapshot.json
Normal file
6179
apps/dokploy/drizzle/meta/0103_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -722,6 +722,13 @@
|
|||||||
"when": 1751848685503,
|
"when": 1751848685503,
|
||||||
"tag": "0102_opposite_grandmaster",
|
"tag": "0102_opposite_grandmaster",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 103,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1752428260850,
|
||||||
|
"tag": "0103_brainy_nehzno",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -24,7 +24,7 @@
|
|||||||
// });
|
// });
|
||||||
// for (const admin of admins) {
|
// for (const admin of admins) {
|
||||||
// const user = await db
|
// const user = await db
|
||||||
// .insert(schema.users_temp)
|
// .insert(schema.users)
|
||||||
// .values({
|
// .values({
|
||||||
// id: admin.adminId,
|
// id: admin.adminId,
|
||||||
// email: admin.auth.email,
|
// email: admin.auth.email,
|
||||||
@@ -74,7 +74,7 @@
|
|||||||
|
|
||||||
// for (const member of admin.users) {
|
// for (const member of admin.users) {
|
||||||
// const userTemp = await db
|
// const userTemp = await db
|
||||||
// .insert(schema.users_temp)
|
// .insert(schema.users)
|
||||||
// .values({
|
// .values({
|
||||||
// id: member.userId,
|
// id: member.userId,
|
||||||
// email: member.auth.email,
|
// email: member.auth.email,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "dokploy",
|
"name": "dokploy",
|
||||||
"version": "v0.24.2",
|
"version": "v0.24.1",
|
||||||
"private": true,
|
"private": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { buffer } from "node:stream/consumers";
|
import { buffer } from "node:stream/consumers";
|
||||||
import { db } from "@/server/db";
|
import { db } from "@/server/db";
|
||||||
import { organization, server, users_temp } from "@/server/db/schema";
|
import { organization, server, users } from "@/server/db/schema";
|
||||||
import { type Server, findUserById } from "@dokploy/server";
|
import { type Server, findUserById } from "@dokploy/server";
|
||||||
import { asc, eq } from "drizzle-orm";
|
import { asc, eq } from "drizzle-orm";
|
||||||
import type { NextApiRequest, NextApiResponse } from "next";
|
import type { NextApiRequest, NextApiResponse } from "next";
|
||||||
@@ -64,13 +64,13 @@ export default async function handler(
|
|||||||
session.subscription as string,
|
session.subscription as string,
|
||||||
);
|
);
|
||||||
await db
|
await db
|
||||||
.update(users_temp)
|
.update(users)
|
||||||
.set({
|
.set({
|
||||||
stripeCustomerId: session.customer as string,
|
stripeCustomerId: session.customer as string,
|
||||||
stripeSubscriptionId: session.subscription as string,
|
stripeSubscriptionId: session.subscription as string,
|
||||||
serversQuantity: subscription?.items?.data?.[0]?.quantity ?? 0,
|
serversQuantity: subscription?.items?.data?.[0]?.quantity ?? 0,
|
||||||
})
|
})
|
||||||
.where(eq(users_temp.id, adminId))
|
.where(eq(users.id, adminId))
|
||||||
.returning();
|
.returning();
|
||||||
|
|
||||||
const admin = await findUserById(adminId);
|
const admin = await findUserById(adminId);
|
||||||
@@ -85,14 +85,12 @@ export default async function handler(
|
|||||||
const newSubscription = event.data.object as Stripe.Subscription;
|
const newSubscription = event.data.object as Stripe.Subscription;
|
||||||
|
|
||||||
await db
|
await db
|
||||||
.update(users_temp)
|
.update(users)
|
||||||
.set({
|
.set({
|
||||||
stripeSubscriptionId: newSubscription.id,
|
stripeSubscriptionId: newSubscription.id,
|
||||||
stripeCustomerId: newSubscription.customer as string,
|
stripeCustomerId: newSubscription.customer as string,
|
||||||
})
|
})
|
||||||
.where(
|
.where(eq(users.stripeCustomerId, newSubscription.customer as string))
|
||||||
eq(users_temp.stripeCustomerId, newSubscription.customer as string),
|
|
||||||
)
|
|
||||||
.returning();
|
.returning();
|
||||||
|
|
||||||
break;
|
break;
|
||||||
@@ -102,14 +100,12 @@ export default async function handler(
|
|||||||
const newSubscription = event.data.object as Stripe.Subscription;
|
const newSubscription = event.data.object as Stripe.Subscription;
|
||||||
|
|
||||||
await db
|
await db
|
||||||
.update(users_temp)
|
.update(users)
|
||||||
.set({
|
.set({
|
||||||
stripeSubscriptionId: null,
|
stripeSubscriptionId: null,
|
||||||
serversQuantity: 0,
|
serversQuantity: 0,
|
||||||
})
|
})
|
||||||
.where(
|
.where(eq(users.stripeCustomerId, newSubscription.customer as string));
|
||||||
eq(users_temp.stripeCustomerId, newSubscription.customer as string),
|
|
||||||
);
|
|
||||||
|
|
||||||
const admin = await findUserByStripeCustomerId(
|
const admin = await findUserByStripeCustomerId(
|
||||||
newSubscription.customer as string,
|
newSubscription.customer as string,
|
||||||
@@ -135,12 +131,12 @@ export default async function handler(
|
|||||||
|
|
||||||
if (newSubscription.status === "active") {
|
if (newSubscription.status === "active") {
|
||||||
await db
|
await db
|
||||||
.update(users_temp)
|
.update(users)
|
||||||
.set({
|
.set({
|
||||||
serversQuantity: newSubscription?.items?.data?.[0]?.quantity ?? 0,
|
serversQuantity: newSubscription?.items?.data?.[0]?.quantity ?? 0,
|
||||||
})
|
})
|
||||||
.where(
|
.where(
|
||||||
eq(users_temp.stripeCustomerId, newSubscription.customer as string),
|
eq(users.stripeCustomerId, newSubscription.customer as string),
|
||||||
);
|
);
|
||||||
|
|
||||||
const newServersQuantity = admin.serversQuantity;
|
const newServersQuantity = admin.serversQuantity;
|
||||||
@@ -148,10 +144,10 @@ export default async function handler(
|
|||||||
} else {
|
} else {
|
||||||
await disableServers(admin.id);
|
await disableServers(admin.id);
|
||||||
await db
|
await db
|
||||||
.update(users_temp)
|
.update(users)
|
||||||
.set({ serversQuantity: 0 })
|
.set({ serversQuantity: 0 })
|
||||||
.where(
|
.where(
|
||||||
eq(users_temp.stripeCustomerId, newSubscription.customer as string),
|
eq(users.stripeCustomerId, newSubscription.customer as string),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -172,11 +168,11 @@ export default async function handler(
|
|||||||
}
|
}
|
||||||
|
|
||||||
await db
|
await db
|
||||||
.update(users_temp)
|
.update(users)
|
||||||
.set({
|
.set({
|
||||||
serversQuantity: suscription?.items?.data?.[0]?.quantity ?? 0,
|
serversQuantity: suscription?.items?.data?.[0]?.quantity ?? 0,
|
||||||
})
|
})
|
||||||
.where(eq(users_temp.stripeCustomerId, suscription.customer as string));
|
.where(eq(users.stripeCustomerId, suscription.customer as string));
|
||||||
|
|
||||||
const admin = await findUserByStripeCustomerId(
|
const admin = await findUserByStripeCustomerId(
|
||||||
suscription.customer as string,
|
suscription.customer as string,
|
||||||
@@ -205,13 +201,11 @@ export default async function handler(
|
|||||||
return res.status(400).send("Webhook Error: Admin not found");
|
return res.status(400).send("Webhook Error: Admin not found");
|
||||||
}
|
}
|
||||||
await db
|
await db
|
||||||
.update(users_temp)
|
.update(users)
|
||||||
.set({
|
.set({
|
||||||
serversQuantity: 0,
|
serversQuantity: 0,
|
||||||
})
|
})
|
||||||
.where(
|
.where(eq(users.stripeCustomerId, newInvoice.customer as string));
|
||||||
eq(users_temp.stripeCustomerId, newInvoice.customer as string),
|
|
||||||
);
|
|
||||||
|
|
||||||
await disableServers(admin.id);
|
await disableServers(admin.id);
|
||||||
}
|
}
|
||||||
@@ -229,13 +223,13 @@ export default async function handler(
|
|||||||
|
|
||||||
await disableServers(admin.id);
|
await disableServers(admin.id);
|
||||||
await db
|
await db
|
||||||
.update(users_temp)
|
.update(users)
|
||||||
.set({
|
.set({
|
||||||
stripeCustomerId: null,
|
stripeCustomerId: null,
|
||||||
stripeSubscriptionId: null,
|
stripeSubscriptionId: null,
|
||||||
serversQuantity: 0,
|
serversQuantity: 0,
|
||||||
})
|
})
|
||||||
.where(eq(users_temp.stripeCustomerId, customer.id));
|
.where(eq(users.stripeCustomerId, customer.id));
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -262,8 +256,8 @@ const disableServers = async (userId: string) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const findUserByStripeCustomerId = async (stripeCustomerId: string) => {
|
const findUserByStripeCustomerId = async (stripeCustomerId: string) => {
|
||||||
const user = db.query.users_temp.findFirst({
|
const user = db.query.users.findFirst({
|
||||||
where: eq(users_temp.stripeCustomerId, stripeCustomerId),
|
where: eq(users.stripeCustomerId, stripeCustomerId),
|
||||||
});
|
});
|
||||||
return user;
|
return user;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
|||||||
import { appRouter } from "@/server/api/root";
|
import { appRouter } from "@/server/api/root";
|
||||||
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 { PERMISSIONS } from "@dokploy/server/lib/permissions";
|
||||||
import { createServerSideHelpers } from "@trpc/react-query/server";
|
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";
|
||||||
@@ -53,12 +54,8 @@ export async function getServerSideProps(
|
|||||||
try {
|
try {
|
||||||
await helpers.project.all.prefetch();
|
await helpers.project.all.prefetch();
|
||||||
|
|
||||||
if (user.role === "member") {
|
if (user.role?.name === "member" || !user?.role?.isSystem) {
|
||||||
const userR = await helpers.user.one.fetch({
|
if (!user?.role?.permissions?.includes(PERMISSIONS.DOCKER.VIEW.name)) {
|
||||||
userId: user.id,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!userR?.canAccessToDocker) {
|
|
||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
permanent: true,
|
permanent: true,
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ const Dashboard = () => {
|
|||||||
false,
|
false,
|
||||||
);
|
);
|
||||||
|
|
||||||
const { data: monitoring, isLoading } = api.user.getMetricsToken.useQuery();
|
const { data: webServer, isLoading } = api.webServer.get.useQuery();
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4 pb-10">
|
<div className="space-y-4 pb-10">
|
||||||
{/* <AlertBlock>
|
{/* <AlertBlock>
|
||||||
@@ -59,12 +59,12 @@ const Dashboard = () => {
|
|||||||
<ShowPaidMonitoring
|
<ShowPaidMonitoring
|
||||||
BASE_URL={
|
BASE_URL={
|
||||||
process.env.NODE_ENV === "production"
|
process.env.NODE_ENV === "production"
|
||||||
? `http://${monitoring?.serverIp}:${monitoring?.metricsConfig?.server?.port}/metrics`
|
? `http://${webServer?.serverIp}:${webServer?.metricsConfig?.server?.port}/metrics`
|
||||||
: BASE_URL
|
: BASE_URL
|
||||||
}
|
}
|
||||||
token={
|
token={
|
||||||
process.env.NODE_ENV === "production"
|
process.env.NODE_ENV === "production"
|
||||||
? monitoring?.metricsConfig?.server?.token
|
? webServer?.metricsConfig?.server?.token
|
||||||
: DEFAULT_TOKEN
|
: DEFAULT_TOKEN
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -94,6 +94,7 @@ import { useRouter } from "next/router";
|
|||||||
import { type ReactElement, useEffect, useMemo, useState } from "react";
|
import { type ReactElement, useEffect, useMemo, useState } from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import superjson from "superjson";
|
import superjson from "superjson";
|
||||||
|
import { Permissions } from "@/components/dashboard/shared/Permissions";
|
||||||
|
|
||||||
export type Services = {
|
export type Services = {
|
||||||
appName: string;
|
appName: string;
|
||||||
@@ -221,7 +222,6 @@ const Project = (
|
|||||||
) => {
|
) => {
|
||||||
const [isBulkActionLoading, setIsBulkActionLoading] = useState(false);
|
const [isBulkActionLoading, setIsBulkActionLoading] = useState(false);
|
||||||
const { projectId } = props;
|
const { projectId } = props;
|
||||||
const { data: auth } = api.user.get.useQuery();
|
|
||||||
const [sortBy, setSortBy] = useState<string>(() => {
|
const [sortBy, setSortBy] = useState<string>(() => {
|
||||||
if (typeof window !== "undefined") {
|
if (typeof window !== "undefined") {
|
||||||
return localStorage.getItem("servicesSort") || "createdAt-desc";
|
return localStorage.getItem("servicesSort") || "createdAt-desc";
|
||||||
@@ -736,30 +736,27 @@ const Project = (
|
|||||||
Stop
|
Stop
|
||||||
</Button>
|
</Button>
|
||||||
</DialogAction>
|
</DialogAction>
|
||||||
{(auth?.role === "owner" ||
|
<Permissions permissions={["project:delete"]}>
|
||||||
auth?.canDeleteServices) && (
|
<DialogAction
|
||||||
<>
|
title="Delete Services"
|
||||||
<DialogAction
|
description={`Are you sure you want to delete ${selectedServices.length} services? This action cannot be undone.`}
|
||||||
title="Delete Services"
|
type="destructive"
|
||||||
description={`Are you sure you want to delete ${selectedServices.length} services? This action cannot be undone.`}
|
onClick={handleBulkDelete}
|
||||||
type="destructive"
|
>
|
||||||
onClick={handleBulkDelete}
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
className="w-full justify-start text-destructive"
|
||||||
>
|
>
|
||||||
<Button
|
<Trash2 className="mr-2 h-4 w-4" />
|
||||||
variant="ghost"
|
Delete
|
||||||
className="w-full justify-start text-destructive"
|
</Button>
|
||||||
>
|
</DialogAction>
|
||||||
<Trash2 className="mr-2 h-4 w-4" />
|
<DuplicateProject
|
||||||
Delete
|
projectId={projectId}
|
||||||
</Button>
|
services={applications}
|
||||||
</DialogAction>
|
selectedServiceIds={selectedServices}
|
||||||
<DuplicateProject
|
/>
|
||||||
projectId={projectId}
|
</Permissions>
|
||||||
services={applications}
|
|
||||||
selectedServiceIds={selectedServices}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Dialog
|
<Dialog
|
||||||
open={isMoveDialogOpen}
|
open={isMoveDialogOpen}
|
||||||
|
|||||||
@@ -37,7 +37,6 @@ import {
|
|||||||
TooltipProvider,
|
TooltipProvider,
|
||||||
TooltipTrigger,
|
TooltipTrigger,
|
||||||
} from "@/components/ui/tooltip";
|
} from "@/components/ui/tooltip";
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
import { appRouter } from "@/server/api/root";
|
import { appRouter } from "@/server/api/root";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { validateRequest } from "@dokploy/server/lib/auth";
|
import { validateRequest } from "@dokploy/server/lib/auth";
|
||||||
@@ -54,6 +53,7 @@ import { useRouter } from "next/router";
|
|||||||
import { type ReactElement, useEffect, useState } from "react";
|
import { type ReactElement, useEffect, useState } from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import superjson from "superjson";
|
import superjson from "superjson";
|
||||||
|
import { Permissions } from "@/components/dashboard/shared/Permissions";
|
||||||
|
|
||||||
type TabState =
|
type TabState =
|
||||||
| "projects"
|
| "projects"
|
||||||
@@ -88,7 +88,6 @@ const Service = (
|
|||||||
);
|
);
|
||||||
|
|
||||||
const { data: isCloud } = api.settings.isCloud.useQuery();
|
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||||
const { data: auth } = api.user.get.useQuery();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="pb-10">
|
<div className="pb-10">
|
||||||
@@ -179,9 +178,9 @@ const Service = (
|
|||||||
|
|
||||||
<div className="flex flex-row gap-2 justify-end">
|
<div className="flex flex-row gap-2 justify-end">
|
||||||
<UpdateApplication applicationId={applicationId} />
|
<UpdateApplication applicationId={applicationId} />
|
||||||
{(auth?.role === "owner" || auth?.canDeleteServices) && (
|
<Permissions permissions={["service:delete"]}>
|
||||||
<DeleteService id={applicationId} type="application" />
|
<DeleteService id={applicationId} type="application" />
|
||||||
)}
|
</Permissions>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
|
|||||||
@@ -33,7 +33,6 @@ import {
|
|||||||
TooltipProvider,
|
TooltipProvider,
|
||||||
TooltipTrigger,
|
TooltipTrigger,
|
||||||
} from "@/components/ui/tooltip";
|
} from "@/components/ui/tooltip";
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
import { appRouter } from "@/server/api/root";
|
import { appRouter } from "@/server/api/root";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { validateRequest } from "@dokploy/server/lib/auth";
|
import { validateRequest } from "@dokploy/server/lib/auth";
|
||||||
@@ -51,6 +50,7 @@ import { useRouter } from "next/router";
|
|||||||
import { type ReactElement, useEffect, useState } from "react";
|
import { type ReactElement, useEffect, useState } from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import superjson from "superjson";
|
import superjson from "superjson";
|
||||||
|
import { Permissions } from "@/components/dashboard/shared/Permissions";
|
||||||
|
|
||||||
type TabState =
|
type TabState =
|
||||||
| "projects"
|
| "projects"
|
||||||
@@ -78,7 +78,6 @@ 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: isCloud } = api.settings.isCloud.useQuery();
|
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -171,9 +170,9 @@ const Service = (
|
|||||||
<div className="flex flex-row gap-2 justify-end">
|
<div className="flex flex-row gap-2 justify-end">
|
||||||
<UpdateCompose composeId={composeId} />
|
<UpdateCompose composeId={composeId} />
|
||||||
|
|
||||||
{(auth?.role === "owner" || auth?.canDeleteServices) && (
|
<Permissions permissions={["service:delete"]}>
|
||||||
<DeleteService id={composeId} type="compose" />
|
<DeleteService id={composeId} type="compose" />
|
||||||
)}
|
</Permissions>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ import Link from "next/link";
|
|||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { type ReactElement, useState } from "react";
|
import { type ReactElement, useState } from "react";
|
||||||
import superjson from "superjson";
|
import superjson from "superjson";
|
||||||
|
import { Permissions } from "@/components/dashboard/shared/Permissions";
|
||||||
|
|
||||||
type TabState = "projects" | "monitoring" | "settings" | "backups" | "advanced";
|
type TabState = "projects" | "monitoring" | "settings" | "backups" | "advanced";
|
||||||
|
|
||||||
@@ -57,7 +58,6 @@ const Mariadb = (
|
|||||||
const { projectId } = router.query;
|
const { projectId } = router.query;
|
||||||
const [tab, setSab] = useState<TabState>(activeTab);
|
const [tab, setSab] = useState<TabState>(activeTab);
|
||||||
const { data } = api.mariadb.one.useQuery({ mariadbId });
|
const { data } = api.mariadb.one.useQuery({ mariadbId });
|
||||||
const { data: auth } = api.user.get.useQuery();
|
|
||||||
|
|
||||||
const { data: isCloud } = api.settings.isCloud.useQuery();
|
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||||
|
|
||||||
@@ -142,9 +142,9 @@ 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} />
|
<UpdateMariadb mariadbId={mariadbId} />
|
||||||
{(auth?.role === "owner" || auth?.canDeleteServices) && (
|
<Permissions permissions={["service:delete"]}>
|
||||||
<DeleteService id={mariadbId} type="mariadb" />
|
<DeleteService id={mariadbId} type="mariadb" />
|
||||||
)}
|
</Permissions>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ import Link from "next/link";
|
|||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { type ReactElement, useState } from "react";
|
import { type ReactElement, useState } from "react";
|
||||||
import superjson from "superjson";
|
import superjson from "superjson";
|
||||||
|
import { Permissions } from "@/components/dashboard/shared/Permissions";
|
||||||
|
|
||||||
type TabState = "projects" | "monitoring" | "settings" | "backups" | "advanced";
|
type TabState = "projects" | "monitoring" | "settings" | "backups" | "advanced";
|
||||||
|
|
||||||
@@ -57,8 +58,6 @@ const Mongo = (
|
|||||||
const [tab, setSab] = useState<TabState>(activeTab);
|
const [tab, setSab] = useState<TabState>(activeTab);
|
||||||
const { data } = api.mongo.one.useQuery({ mongoId });
|
const { data } = api.mongo.one.useQuery({ mongoId });
|
||||||
|
|
||||||
const { data: auth } = api.user.get.useQuery();
|
|
||||||
|
|
||||||
const { data: isCloud } = api.settings.isCloud.useQuery();
|
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -143,9 +142,9 @@ const Mongo = (
|
|||||||
|
|
||||||
<div className="flex flex-row gap-2 justify-end">
|
<div className="flex flex-row gap-2 justify-end">
|
||||||
<UpdateMongo mongoId={mongoId} />
|
<UpdateMongo mongoId={mongoId} />
|
||||||
{(auth?.role === "owner" || auth?.canDeleteServices) && (
|
<Permissions permissions={["service:delete"]}>
|
||||||
<DeleteService id={mongoId} type="mongo" />
|
<DeleteService id={mongoId} type="mongo" />
|
||||||
)}
|
</Permissions>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
|
|||||||
@@ -29,7 +29,6 @@ import {
|
|||||||
TooltipProvider,
|
TooltipProvider,
|
||||||
TooltipTrigger,
|
TooltipTrigger,
|
||||||
} from "@/components/ui/tooltip";
|
} from "@/components/ui/tooltip";
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
import { appRouter } from "@/server/api/root";
|
import { appRouter } from "@/server/api/root";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { validateRequest } from "@dokploy/server/lib/auth";
|
import { validateRequest } from "@dokploy/server/lib/auth";
|
||||||
@@ -44,6 +43,8 @@ import Link from "next/link";
|
|||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { type ReactElement, useState } from "react";
|
import { type ReactElement, useState } from "react";
|
||||||
import superjson from "superjson";
|
import superjson from "superjson";
|
||||||
|
import { Permissions } from "@/components/dashboard/shared/Permissions";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
type TabState = "projects" | "monitoring" | "settings" | "backups" | "advanced";
|
type TabState = "projects" | "monitoring" | "settings" | "backups" | "advanced";
|
||||||
|
|
||||||
@@ -56,7 +57,6 @@ const MySql = (
|
|||||||
const { projectId } = router.query;
|
const { projectId } = router.query;
|
||||||
const [tab, setSab] = useState<TabState>(activeTab);
|
const [tab, setSab] = useState<TabState>(activeTab);
|
||||||
const { data } = api.mysql.one.useQuery({ mysqlId });
|
const { data } = api.mysql.one.useQuery({ mysqlId });
|
||||||
const { data: auth } = api.user.get.useQuery();
|
|
||||||
|
|
||||||
const { data: isCloud } = api.settings.isCloud.useQuery();
|
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||||
|
|
||||||
@@ -143,9 +143,9 @@ const MySql = (
|
|||||||
|
|
||||||
<div className="flex flex-row gap-2 justify-end">
|
<div className="flex flex-row gap-2 justify-end">
|
||||||
<UpdateMysql mysqlId={mysqlId} />
|
<UpdateMysql mysqlId={mysqlId} />
|
||||||
{(auth?.role === "owner" || auth?.canDeleteServices) && (
|
<Permissions permissions={["service:delete"]}>
|
||||||
<DeleteService id={mysqlId} type="mysql" />
|
<DeleteService id={mysqlId} type="mysql" />
|
||||||
)}
|
</Permissions>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
|
|||||||
@@ -29,7 +29,6 @@ import {
|
|||||||
TooltipProvider,
|
TooltipProvider,
|
||||||
TooltipTrigger,
|
TooltipTrigger,
|
||||||
} from "@/components/ui/tooltip";
|
} from "@/components/ui/tooltip";
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
import { appRouter } from "@/server/api/root";
|
import { appRouter } from "@/server/api/root";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { validateRequest } from "@dokploy/server/lib/auth";
|
import { validateRequest } from "@dokploy/server/lib/auth";
|
||||||
@@ -44,6 +43,8 @@ import Link from "next/link";
|
|||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { type ReactElement, useState } from "react";
|
import { type ReactElement, useState } from "react";
|
||||||
import superjson from "superjson";
|
import superjson from "superjson";
|
||||||
|
import { Permissions } from "@/components/dashboard/shared/Permissions";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
type TabState = "projects" | "monitoring" | "settings" | "backups" | "advanced";
|
type TabState = "projects" | "monitoring" | "settings" | "backups" | "advanced";
|
||||||
|
|
||||||
@@ -56,7 +57,6 @@ const Postgresql = (
|
|||||||
const { projectId } = router.query;
|
const { projectId } = router.query;
|
||||||
const [tab, setSab] = useState<TabState>(activeTab);
|
const [tab, setSab] = useState<TabState>(activeTab);
|
||||||
const { data } = api.postgres.one.useQuery({ postgresId });
|
const { data } = api.postgres.one.useQuery({ postgresId });
|
||||||
const { data: auth } = api.user.get.useQuery();
|
|
||||||
|
|
||||||
const { data: isCloud } = api.settings.isCloud.useQuery();
|
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||||
|
|
||||||
@@ -142,9 +142,9 @@ const Postgresql = (
|
|||||||
|
|
||||||
<div className="flex flex-row gap-2 justify-end">
|
<div className="flex flex-row gap-2 justify-end">
|
||||||
<UpdatePostgres postgresId={postgresId} />
|
<UpdatePostgres postgresId={postgresId} />
|
||||||
{(auth?.role === "owner" || auth?.canDeleteServices) && (
|
<Permissions permissions={["service:delete"]}>
|
||||||
<DeleteService id={postgresId} type="postgres" />
|
<DeleteService id={postgresId} type="postgres" />
|
||||||
)}
|
</Permissions>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ import Link from "next/link";
|
|||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { type ReactElement, useState } from "react";
|
import { type ReactElement, useState } from "react";
|
||||||
import superjson from "superjson";
|
import superjson from "superjson";
|
||||||
|
import { Permissions } from "@/components/dashboard/shared/Permissions";
|
||||||
|
|
||||||
type TabState = "projects" | "monitoring" | "settings" | "advanced";
|
type TabState = "projects" | "monitoring" | "settings" | "advanced";
|
||||||
|
|
||||||
@@ -56,8 +57,6 @@ const Redis = (
|
|||||||
const [tab, setSab] = useState<TabState>(activeTab);
|
const [tab, setSab] = useState<TabState>(activeTab);
|
||||||
const { data } = api.redis.one.useQuery({ redisId });
|
const { data } = api.redis.one.useQuery({ redisId });
|
||||||
|
|
||||||
const { data: auth } = api.user.get.useQuery();
|
|
||||||
|
|
||||||
const { data: isCloud } = api.settings.isCloud.useQuery();
|
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -142,9 +141,9 @@ const Redis = (
|
|||||||
|
|
||||||
<div className="flex flex-row gap-2 justify-end">
|
<div className="flex flex-row gap-2 justify-end">
|
||||||
<UpdateRedis redisId={redisId} />
|
<UpdateRedis redisId={redisId} />
|
||||||
{(auth?.role === "owner" || auth?.canDeleteServices) && (
|
<Permissions permissions={["service:delete"]}>
|
||||||
<DeleteService id={redisId} type="redis" />
|
<DeleteService id={redisId} type="redis" />
|
||||||
)}
|
</Permissions>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ export async function getServerSideProps(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
const { user } = await validateRequest(ctx.req);
|
const { user } = await validateRequest(ctx.req);
|
||||||
if (!user || user.role !== "owner") {
|
if (!user || (user.role?.name !== "owner" && user.role?.name !== "admin")) {
|
||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
permanent: true,
|
permanent: true,
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ export async function getServerSideProps(
|
|||||||
|
|
||||||
await helpers.user.get.prefetch();
|
await helpers.user.get.prefetch();
|
||||||
|
|
||||||
if (!user || user.role === "member") {
|
if (!user || (user.role?.name !== "owner" && user.role?.name !== "admin")) {
|
||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
permanent: true,
|
permanent: true,
|
||||||
|
|||||||
@@ -31,7 +31,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 || (user.role?.name !== "owner" && user.role?.name !== "admin")) {
|
||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
permanent: true,
|
permanent: true,
|
||||||
|
|||||||
@@ -25,7 +25,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 || (user.role?.name !== "owner" && user.role?.name !== "admin")) {
|
||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
permanent: true,
|
permanent: true,
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ export async function getServerSideProps(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
const { user, session } = await validateRequest(ctx.req);
|
const { user, session } = await validateRequest(ctx.req);
|
||||||
if (!user || user.role === "member") {
|
if (!user || (user.role?.name !== "owner" && user.role?.name !== "admin")) {
|
||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
permanent: true,
|
permanent: true,
|
||||||
|
|||||||
@@ -26,7 +26,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 || (user.role?.name !== "owner" && user.role?.name !== "admin")) {
|
||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
permanent: true,
|
permanent: true,
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
|||||||
|
|
||||||
import { appRouter } from "@/server/api/root";
|
import { appRouter } from "@/server/api/root";
|
||||||
import { validateRequest } from "@dokploy/server";
|
import { validateRequest } from "@dokploy/server";
|
||||||
|
import { PERMISSIONS } from "@dokploy/server/lib/permissions";
|
||||||
import { createServerSideHelpers } from "@trpc/react-query/server";
|
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";
|
||||||
@@ -49,12 +50,12 @@ 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") {
|
if (user.role?.name === "member" || !user?.role?.isSystem) {
|
||||||
const userR = await helpers.user.one.fetch({
|
if (
|
||||||
userId: user.id,
|
!user?.role?.permissions?.includes(
|
||||||
});
|
PERMISSIONS.GIT_PROVIDERS.ACCESS.name,
|
||||||
|
)
|
||||||
if (!userR?.canAccessToGitProviders) {
|
) {
|
||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
permanent: true,
|
permanent: true,
|
||||||
|
|||||||
@@ -26,7 +26,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 || (user.role?.name !== "owner" && user.role?.name !== "admin")) {
|
||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
permanent: true,
|
permanent: true,
|
||||||
|
|||||||
@@ -1,27 +1,23 @@
|
|||||||
import { ShowApiKeys } from "@/components/dashboard/settings/api/show-api-keys";
|
import { ShowApiKeys } from "@/components/dashboard/settings/api/show-api-keys";
|
||||||
import { ProfileForm } from "@/components/dashboard/settings/profile/profile-form";
|
import { ProfileForm } from "@/components/dashboard/settings/profile/profile-form";
|
||||||
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
||||||
|
|
||||||
import { appRouter } from "@/server/api/root";
|
import { appRouter } from "@/server/api/root";
|
||||||
import { api } from "@/utils/api";
|
|
||||||
import { getLocale, serverSideTranslations } from "@/utils/i18n";
|
import { getLocale, serverSideTranslations } from "@/utils/i18n";
|
||||||
import { validateRequest } from "@dokploy/server";
|
import { validateRequest } from "@dokploy/server";
|
||||||
import { createServerSideHelpers } from "@trpc/react-query/server";
|
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 { Permissions } from "@/components/dashboard/shared/Permissions";
|
||||||
|
|
||||||
const Page = () => {
|
const Page = () => {
|
||||||
const { data } = api.user.get.useQuery();
|
|
||||||
|
|
||||||
// const { data: isCloud } = api.settings.isCloud.useQuery();
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
<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 />
|
||||||
{(data?.canAccessToAPI || data?.role === "owner") && <ShowApiKeys />}
|
<Permissions permissions={["api:access"]}>
|
||||||
|
<ShowApiKeys />
|
||||||
{/* {isCloud && <RemoveSelfAccount />} */}
|
</Permissions>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -26,7 +26,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 || (user.role?.name !== "owner" && user.role?.name !== "admin")) {
|
||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
permanent: true,
|
permanent: true,
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ export async function getServerSideProps(
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (user.role === "member") {
|
if (user.role?.name === "member" || !user?.role?.isSystem) {
|
||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
permanent: true,
|
permanent: true,
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ export async function getServerSideProps(
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (user.role === "member") {
|
if (user.role?.name === "member" || !user?.role?.isSystem) {
|
||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
permanent: true,
|
permanent: true,
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
|||||||
|
|
||||||
import { appRouter } from "@/server/api/root";
|
import { appRouter } from "@/server/api/root";
|
||||||
import { validateRequest } from "@dokploy/server";
|
import { validateRequest } from "@dokploy/server";
|
||||||
|
import { PERMISSIONS } from "@dokploy/server/lib/permissions";
|
||||||
import { createServerSideHelpers } from "@trpc/react-query/server";
|
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";
|
||||||
@@ -50,12 +51,10 @@ 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") {
|
if (user.role?.name === "member" || !user?.role?.isSystem) {
|
||||||
const userR = await helpers.user.one.fetch({
|
if (
|
||||||
userId: user.id,
|
!user?.role?.permissions?.includes(PERMISSIONS.SSH_KEYS.ACCESS.name)
|
||||||
});
|
) {
|
||||||
|
|
||||||
if (!userR?.canAccessToSSHKeys) {
|
|
||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
permanent: true,
|
permanent: true,
|
||||||
|
|||||||
@@ -29,7 +29,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 || (user.role?.name !== "owner" && user.role?.name !== "admin")) {
|
||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
permanent: true,
|
permanent: true,
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
|||||||
import { appRouter } from "@/server/api/root";
|
import { appRouter } from "@/server/api/root";
|
||||||
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 { PERMISSIONS } from "@dokploy/server/lib/permissions";
|
||||||
import { createServerSideHelpers } from "@trpc/react-query/server";
|
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";
|
||||||
@@ -53,12 +54,8 @@ export async function getServerSideProps(
|
|||||||
try {
|
try {
|
||||||
await helpers.project.all.prefetch();
|
await helpers.project.all.prefetch();
|
||||||
|
|
||||||
if (user.role === "member") {
|
if (user.role?.name === "member" || !user?.role?.isSystem) {
|
||||||
const userR = await helpers.user.one.fetch({
|
if (!user?.role?.permissions?.includes(PERMISSIONS.DOCKER.VIEW.name)) {
|
||||||
userId: user.id,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!userR?.canAccessToDocker) {
|
|
||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
permanent: true,
|
permanent: true,
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
|||||||
import { appRouter } from "@/server/api/root";
|
import { appRouter } from "@/server/api/root";
|
||||||
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 { PERMISSIONS } from "@dokploy/server/lib/permissions";
|
||||||
import { createServerSideHelpers } from "@trpc/react-query/server";
|
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";
|
||||||
@@ -53,12 +54,8 @@ export async function getServerSideProps(
|
|||||||
try {
|
try {
|
||||||
await helpers.project.all.prefetch();
|
await helpers.project.all.prefetch();
|
||||||
|
|
||||||
if (user.role === "member") {
|
if (user.role?.name === "member" || !user?.role?.isSystem) {
|
||||||
const userR = await helpers.user.one.fetch({
|
if (!user?.role?.permissions?.includes(PERMISSIONS.TRAEFIK.ACCESS.name)) {
|
||||||
userId: user.id,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!userR?.canAccessToTraefikFiles) {
|
|
||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
permanent: true,
|
permanent: true,
|
||||||
|
|||||||
@@ -1,12 +1,10 @@
|
|||||||
import { appRouter } from "@/server/api/root";
|
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { validateRequest } from "@dokploy/server";
|
import { validateRequest } from "@dokploy/server";
|
||||||
import { createServerSideHelpers } from "@trpc/react-query/server";
|
|
||||||
import type { GetServerSidePropsContext, NextPage } from "next";
|
import type { GetServerSidePropsContext, NextPage } from "next";
|
||||||
import dynamic from "next/dynamic";
|
import dynamic from "next/dynamic";
|
||||||
import "swagger-ui-react/swagger-ui.css";
|
import "swagger-ui-react/swagger-ui.css";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import superjson from "superjson";
|
import { PERMISSIONS } from "@dokploy/server/lib/permissions";
|
||||||
|
|
||||||
const SwaggerUI = dynamic(() => import("swagger-ui-react"), { ssr: false });
|
const SwaggerUI = dynamic(() => import("swagger-ui-react"), { ssr: false });
|
||||||
|
|
||||||
@@ -71,8 +69,7 @@ const Home: NextPage = () => {
|
|||||||
|
|
||||||
export default Home;
|
export default Home;
|
||||||
export async function getServerSideProps(context: GetServerSidePropsContext) {
|
export async function getServerSideProps(context: GetServerSidePropsContext) {
|
||||||
const { req, res } = context;
|
const { user } = await validateRequest(context.req);
|
||||||
const { user, session } = await validateRequest(context.req);
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
@@ -81,23 +78,9 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
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,
|
|
||||||
});
|
|
||||||
if (user.role === "member") {
|
|
||||||
const userR = await helpers.user.one.fetch({
|
|
||||||
userId: user.id,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!userR?.canAccessToAPI) {
|
if (user.role?.name === "member" || !user?.role?.isSystem) {
|
||||||
|
if (!user?.role?.permissions?.includes(PERMISSIONS.API.ACCESS.name)) {
|
||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
permanent: true,
|
permanent: true,
|
||||||
|
|||||||
@@ -1,18 +1,18 @@
|
|||||||
import { findAdmin } from "@dokploy/server";
|
import { findOwner } from "@dokploy/server";
|
||||||
import { db } from "@dokploy/server/db";
|
import { db } from "@dokploy/server/db";
|
||||||
import { users_temp } from "@dokploy/server/db/schema";
|
import { users } from "@dokploy/server/db/schema";
|
||||||
import { eq } from "drizzle-orm";
|
import { eq } from "drizzle-orm";
|
||||||
|
|
||||||
(async () => {
|
(async () => {
|
||||||
try {
|
try {
|
||||||
const result = await findAdmin();
|
const result = await findOwner();
|
||||||
|
|
||||||
const update = await db
|
const update = await db
|
||||||
.update(users_temp)
|
.update(users)
|
||||||
.set({
|
.set({
|
||||||
twoFactorEnabled: false,
|
twoFactorEnabled: false,
|
||||||
})
|
})
|
||||||
.where(eq(users_temp.id, result.userId));
|
.where(eq(users.id, result.userId));
|
||||||
|
|
||||||
if (update) {
|
if (update) {
|
||||||
console.log("2FA reset successful");
|
console.log("2FA reset successful");
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { findAdmin } from "@dokploy/server";
|
import { findOwner } from "@dokploy/server";
|
||||||
import { generateRandomPassword } from "@dokploy/server";
|
import { generateRandomPassword } from "@dokploy/server";
|
||||||
import { db } from "@dokploy/server/db";
|
import { db } from "@dokploy/server/db";
|
||||||
import { account } from "@dokploy/server/db/schema";
|
import { account } from "@dokploy/server/db/schema";
|
||||||
@@ -8,7 +8,7 @@ import { eq } from "drizzle-orm";
|
|||||||
try {
|
try {
|
||||||
const randomPassword = await generateRandomPassword();
|
const randomPassword = await generateRandomPassword();
|
||||||
|
|
||||||
const result = await findAdmin();
|
const result = await findOwner();
|
||||||
|
|
||||||
const update = await db
|
const update = await db
|
||||||
.update(account)
|
.update(account)
|
||||||
|
|||||||
@@ -38,6 +38,8 @@ import { userRouter } from "./routers/user";
|
|||||||
import { scheduleRouter } from "./routers/schedule";
|
import { scheduleRouter } from "./routers/schedule";
|
||||||
import { rollbackRouter } from "./routers/rollbacks";
|
import { rollbackRouter } from "./routers/rollbacks";
|
||||||
import { volumeBackupsRouter } from "./routers/volume-backups";
|
import { volumeBackupsRouter } from "./routers/volume-backups";
|
||||||
|
import { roleRouter } from "./routers/role";
|
||||||
|
import { webServerRouter } from "./routers/web-server";
|
||||||
/**
|
/**
|
||||||
* This is the primary router for your server.
|
* This is the primary router for your server.
|
||||||
*
|
*
|
||||||
@@ -84,6 +86,8 @@ export const appRouter = createTRPCRouter({
|
|||||||
schedule: scheduleRouter,
|
schedule: scheduleRouter,
|
||||||
rollback: rollbackRouter,
|
rollback: rollbackRouter,
|
||||||
volumeBackups: volumeBackupsRouter,
|
volumeBackups: volumeBackupsRouter,
|
||||||
|
role: roleRouter,
|
||||||
|
webServer: webServerRouter,
|
||||||
});
|
});
|
||||||
|
|
||||||
// export type definition of API
|
// export type definition of API
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import {
|
|||||||
IS_CLOUD,
|
IS_CLOUD,
|
||||||
findUserById,
|
findUserById,
|
||||||
setupWebMonitoring,
|
setupWebMonitoring,
|
||||||
updateUser,
|
updateWebServer,
|
||||||
} from "@dokploy/server";
|
} from "@dokploy/server";
|
||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
import { adminProcedure, createTRPCRouter } from "../trpc";
|
import { adminProcedure, createTRPCRouter } from "../trpc";
|
||||||
@@ -27,7 +27,8 @@ export const adminRouter = createTRPCRouter({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
await updateUser(user.id, {
|
await updateWebServer({
|
||||||
|
// @ts-expect-error - TODO: fix this
|
||||||
metricsConfig: {
|
metricsConfig: {
|
||||||
server: {
|
server: {
|
||||||
type: "Dokploy",
|
type: "Dokploy",
|
||||||
@@ -52,7 +53,7 @@ export const adminRouter = createTRPCRouter({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const currentServer = await setupWebMonitoring(user.id);
|
const currentServer = await setupWebMonitoring();
|
||||||
return currentServer;
|
return currentServer;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw error;
|
throw error;
|
||||||
|
|||||||
@@ -147,11 +147,10 @@ export const aiRouter = createTRPCRouter({
|
|||||||
serverId: z.string().optional(),
|
serverId: z.string().optional(),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.mutation(async ({ ctx, input }) => {
|
.mutation(async ({ input }) => {
|
||||||
try {
|
try {
|
||||||
return await suggestVariants({
|
return await suggestVariants({
|
||||||
...input,
|
...input,
|
||||||
organizationId: ctx.session.activeOrganizationId,
|
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
@@ -163,7 +162,7 @@ export const aiRouter = createTRPCRouter({
|
|||||||
deploy: protectedProcedure
|
deploy: protectedProcedure
|
||||||
.input(deploySuggestionSchema)
|
.input(deploySuggestionSchema)
|
||||||
.mutation(async ({ ctx, input }) => {
|
.mutation(async ({ ctx, input }) => {
|
||||||
if (ctx.user.role === "member") {
|
if (ctx.user.role.name === "member" || !ctx.user.role.isSystem) {
|
||||||
await checkServiceAccess(
|
await checkServiceAccess(
|
||||||
ctx.session.activeOrganizationId,
|
ctx.session.activeOrganizationId,
|
||||||
input.projectId,
|
input.projectId,
|
||||||
@@ -216,7 +215,7 @@ export const aiRouter = createTRPCRouter({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ctx.user.role === "member") {
|
if (ctx.user.role.name === "member" || !ctx.user.role.isSystem) {
|
||||||
await addNewService(
|
await addNewService(
|
||||||
ctx.session.activeOrganizationId,
|
ctx.session.activeOrganizationId,
|
||||||
ctx.user.ownerId,
|
ctx.user.ownerId,
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ export const applicationRouter = createTRPCRouter({
|
|||||||
.input(apiCreateApplication)
|
.input(apiCreateApplication)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
try {
|
try {
|
||||||
if (ctx.user.role === "member") {
|
if (ctx.user.role.name === "member" || !ctx.user.role.isSystem) {
|
||||||
await checkServiceAccess(
|
await checkServiceAccess(
|
||||||
ctx.user.id,
|
ctx.user.id,
|
||||||
input.projectId,
|
input.projectId,
|
||||||
@@ -88,7 +88,7 @@ export const applicationRouter = createTRPCRouter({
|
|||||||
}
|
}
|
||||||
const newApplication = await createApplication(input);
|
const newApplication = await createApplication(input);
|
||||||
|
|
||||||
if (ctx.user.role === "member") {
|
if (ctx.user.role.name === "member" || !ctx.user.role.isSystem) {
|
||||||
await addNewService(
|
await addNewService(
|
||||||
ctx.user.id,
|
ctx.user.id,
|
||||||
newApplication.applicationId,
|
newApplication.applicationId,
|
||||||
@@ -110,7 +110,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") {
|
if (ctx.user.role.name === "member" || !ctx.user.role.isSystem) {
|
||||||
await checkServiceAccess(
|
await checkServiceAccess(
|
||||||
ctx.user.id,
|
ctx.user.id,
|
||||||
input.applicationId,
|
input.applicationId,
|
||||||
@@ -201,7 +201,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") {
|
if (ctx.user.role.name === "member" || !ctx.user.role.isSystem) {
|
||||||
await checkServiceAccess(
|
await checkServiceAccess(
|
||||||
ctx.user.id,
|
ctx.user.id,
|
||||||
input.applicationId,
|
input.applicationId,
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ import {
|
|||||||
findGitProviderById,
|
findGitProviderById,
|
||||||
findProjectById,
|
findProjectById,
|
||||||
findServerById,
|
findServerById,
|
||||||
findUserById,
|
findWebServer,
|
||||||
getComposeContainer,
|
getComposeContainer,
|
||||||
loadServices,
|
loadServices,
|
||||||
randomizeComposeFile,
|
randomizeComposeFile,
|
||||||
@@ -64,7 +64,7 @@ export const composeRouter = createTRPCRouter({
|
|||||||
.input(apiCreateCompose)
|
.input(apiCreateCompose)
|
||||||
.mutation(async ({ ctx, input }) => {
|
.mutation(async ({ ctx, input }) => {
|
||||||
try {
|
try {
|
||||||
if (ctx.user.role === "member") {
|
if (ctx.user.role.name === "member" || !ctx.user.role.isSystem) {
|
||||||
await checkServiceAccess(
|
await checkServiceAccess(
|
||||||
ctx.user.id,
|
ctx.user.id,
|
||||||
input.projectId,
|
input.projectId,
|
||||||
@@ -88,7 +88,7 @@ export const composeRouter = createTRPCRouter({
|
|||||||
}
|
}
|
||||||
const newService = await createCompose(input);
|
const newService = await createCompose(input);
|
||||||
|
|
||||||
if (ctx.user.role === "member") {
|
if (ctx.user.role.name === "member" || !ctx.user.role.isSystem) {
|
||||||
await addNewService(
|
await addNewService(
|
||||||
ctx.user.id,
|
ctx.user.id,
|
||||||
newService.composeId,
|
newService.composeId,
|
||||||
@@ -105,7 +105,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") {
|
if (ctx.user.role.name === "member" || !ctx.user.role.isSystem) {
|
||||||
await checkServiceAccess(
|
await checkServiceAccess(
|
||||||
ctx.user.id,
|
ctx.user.id,
|
||||||
input.composeId,
|
input.composeId,
|
||||||
@@ -177,7 +177,7 @@ export const composeRouter = createTRPCRouter({
|
|||||||
delete: protectedProcedure
|
delete: protectedProcedure
|
||||||
.input(apiDeleteCompose)
|
.input(apiDeleteCompose)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
if (ctx.user.role === "member") {
|
if (ctx.user.role.name === "member" || !ctx.user.role.isSystem) {
|
||||||
await checkServiceAccess(
|
await checkServiceAccess(
|
||||||
ctx.user.id,
|
ctx.user.id,
|
||||||
input.composeId,
|
input.composeId,
|
||||||
@@ -469,7 +469,7 @@ export const composeRouter = createTRPCRouter({
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.mutation(async ({ ctx, input }) => {
|
.mutation(async ({ ctx, input }) => {
|
||||||
if (ctx.user.role === "member") {
|
if (ctx.user.role.name === "member" || !ctx.user.role.isSystem) {
|
||||||
await checkServiceAccess(
|
await checkServiceAccess(
|
||||||
ctx.user.id,
|
ctx.user.id,
|
||||||
input.projectId,
|
input.projectId,
|
||||||
@@ -487,8 +487,8 @@ export const composeRouter = createTRPCRouter({
|
|||||||
|
|
||||||
const template = await fetchTemplateFiles(input.id, input.baseUrl);
|
const template = await fetchTemplateFiles(input.id, input.baseUrl);
|
||||||
|
|
||||||
const admin = await findUserById(ctx.user.ownerId);
|
const webServer = await findWebServer();
|
||||||
let serverIp = admin.serverIp || "127.0.0.1";
|
let serverIp = webServer.serverIp || "127.0.0.1";
|
||||||
|
|
||||||
const project = await findProjectById(input.projectId);
|
const project = await findProjectById(input.projectId);
|
||||||
|
|
||||||
@@ -524,7 +524,7 @@ export const composeRouter = createTRPCRouter({
|
|||||||
isolatedDeployment: true,
|
isolatedDeployment: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (ctx.user.role === "member") {
|
if (ctx.user.role.name === "member" || !ctx.user.role.isSystem) {
|
||||||
await addNewService(
|
await addNewService(
|
||||||
ctx.user.id,
|
ctx.user.id,
|
||||||
compose.composeId,
|
compose.composeId,
|
||||||
@@ -709,8 +709,8 @@ export const composeRouter = createTRPCRouter({
|
|||||||
const decodedData = Buffer.from(input.base64, "base64").toString(
|
const decodedData = Buffer.from(input.base64, "base64").toString(
|
||||||
"utf-8",
|
"utf-8",
|
||||||
);
|
);
|
||||||
const admin = await findUserById(ctx.user.ownerId);
|
const webServer = await findWebServer();
|
||||||
let serverIp = admin.serverIp || "127.0.0.1";
|
let serverIp = webServer.serverIp || "127.0.0.1";
|
||||||
|
|
||||||
if (compose.serverId) {
|
if (compose.serverId) {
|
||||||
const server = await findServerById(compose.serverId);
|
const server = await findServerById(compose.serverId);
|
||||||
@@ -785,8 +785,8 @@ export const composeRouter = createTRPCRouter({
|
|||||||
await removeDomainById(domain.domainId);
|
await removeDomainById(domain.domainId);
|
||||||
}
|
}
|
||||||
|
|
||||||
const admin = await findUserById(ctx.user.ownerId);
|
const webServer = await findWebServer();
|
||||||
let serverIp = admin.serverIp || "127.0.0.1";
|
let serverIp = webServer.serverIp || "127.0.0.1";
|
||||||
|
|
||||||
if (compose.serverId) {
|
if (compose.serverId) {
|
||||||
const server = await findServerById(compose.serverId);
|
const server = await findServerById(compose.serverId);
|
||||||
|
|||||||
@@ -13,9 +13,9 @@ import {
|
|||||||
findDomainById,
|
findDomainById,
|
||||||
findDomainsByApplicationId,
|
findDomainsByApplicationId,
|
||||||
findDomainsByComposeId,
|
findDomainsByComposeId,
|
||||||
findOrganizationById,
|
|
||||||
findPreviewDeploymentById,
|
findPreviewDeploymentById,
|
||||||
findServerById,
|
findServerById,
|
||||||
|
findWebServer,
|
||||||
generateTraefikMeDomain,
|
generateTraefikMeDomain,
|
||||||
manageDomain,
|
manageDomain,
|
||||||
removeDomain,
|
removeDomain,
|
||||||
@@ -93,25 +93,19 @@ export const domainRouter = createTRPCRouter({
|
|||||||
}),
|
}),
|
||||||
generateDomain: protectedProcedure
|
generateDomain: protectedProcedure
|
||||||
.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 }) => {
|
||||||
return generateTraefikMeDomain(
|
return generateTraefikMeDomain(input.appName, input.serverId);
|
||||||
input.appName,
|
|
||||||
ctx.user.ownerId,
|
|
||||||
input.serverId,
|
|
||||||
);
|
|
||||||
}),
|
}),
|
||||||
canGenerateTraefikMeDomains: protectedProcedure
|
canGenerateTraefikMeDomains: protectedProcedure
|
||||||
.input(z.object({ serverId: z.string() }))
|
.input(z.object({ serverId: z.string() }))
|
||||||
.query(async ({ input, ctx }) => {
|
.query(async ({ input }) => {
|
||||||
const organization = await findOrganizationById(
|
const webServer = await findWebServer();
|
||||||
ctx.session.activeOrganizationId,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (input.serverId) {
|
if (input.serverId) {
|
||||||
const server = await findServerById(input.serverId);
|
const server = await findServerById(input.serverId);
|
||||||
return server.ipAddress;
|
return server.ipAddress;
|
||||||
}
|
}
|
||||||
return organization?.owner.serverIp;
|
return webServer?.serverIp;
|
||||||
}),
|
}),
|
||||||
|
|
||||||
update: protectedProcedure
|
update: protectedProcedure
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ export const mariadbRouter = createTRPCRouter({
|
|||||||
.input(apiCreateMariaDB)
|
.input(apiCreateMariaDB)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
try {
|
try {
|
||||||
if (ctx.user.role === "member") {
|
if (ctx.user.role.name === "member" || !ctx.user.role.isSystem) {
|
||||||
await checkServiceAccess(
|
await checkServiceAccess(
|
||||||
ctx.user.id,
|
ctx.user.id,
|
||||||
input.projectId,
|
input.projectId,
|
||||||
@@ -65,7 +65,7 @@ export const mariadbRouter = createTRPCRouter({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
const newMariadb = await createMariadb(input);
|
const newMariadb = await createMariadb(input);
|
||||||
if (ctx.user.role === "member") {
|
if (ctx.user.role.name === "member" || !ctx.user.role.isSystem) {
|
||||||
await addNewService(
|
await addNewService(
|
||||||
ctx.user.id,
|
ctx.user.id,
|
||||||
newMariadb.mariadbId,
|
newMariadb.mariadbId,
|
||||||
@@ -92,7 +92,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") {
|
if (ctx.user.role.name === "member" || !ctx.user.role.isSystem) {
|
||||||
await checkServiceAccess(
|
await checkServiceAccess(
|
||||||
ctx.user.id,
|
ctx.user.id,
|
||||||
input.mariadbId,
|
input.mariadbId,
|
||||||
@@ -219,7 +219,7 @@ export const mariadbRouter = createTRPCRouter({
|
|||||||
remove: protectedProcedure
|
remove: protectedProcedure
|
||||||
.input(apiFindOneMariaDB)
|
.input(apiFindOneMariaDB)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
if (ctx.user.role === "member") {
|
if (ctx.user.role.name === "member" || !ctx.user.role.isSystem) {
|
||||||
await checkServiceAccess(
|
await checkServiceAccess(
|
||||||
ctx.user.id,
|
ctx.user.id,
|
||||||
input.mariadbId,
|
input.mariadbId,
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ export const mongoRouter = createTRPCRouter({
|
|||||||
.input(apiCreateMongo)
|
.input(apiCreateMongo)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
try {
|
try {
|
||||||
if (ctx.user.role === "member") {
|
if (ctx.user.role.name === "member" || !ctx.user.role.isSystem) {
|
||||||
await checkServiceAccess(
|
await checkServiceAccess(
|
||||||
ctx.user.id,
|
ctx.user.id,
|
||||||
input.projectId,
|
input.projectId,
|
||||||
@@ -65,7 +65,7 @@ export const mongoRouter = createTRPCRouter({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
const newMongo = await createMongo(input);
|
const newMongo = await createMongo(input);
|
||||||
if (ctx.user.role === "member") {
|
if (ctx.user.role.name === "member" || !ctx.user.role.isSystem) {
|
||||||
await addNewService(
|
await addNewService(
|
||||||
ctx.user.id,
|
ctx.user.id,
|
||||||
newMongo.mongoId,
|
newMongo.mongoId,
|
||||||
@@ -96,7 +96,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") {
|
if (ctx.user.role.name === "member" || !ctx.user.role.isSystem) {
|
||||||
await checkServiceAccess(
|
await checkServiceAccess(
|
||||||
ctx.user.id,
|
ctx.user.id,
|
||||||
input.mongoId,
|
input.mongoId,
|
||||||
@@ -261,7 +261,7 @@ export const mongoRouter = createTRPCRouter({
|
|||||||
remove: protectedProcedure
|
remove: protectedProcedure
|
||||||
.input(apiFindOneMongo)
|
.input(apiFindOneMongo)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
if (ctx.user.role === "member") {
|
if (ctx.user.role.name === "member" || !ctx.user.role.isSystem) {
|
||||||
await checkServiceAccess(
|
await checkServiceAccess(
|
||||||
ctx.user.id,
|
ctx.user.id,
|
||||||
input.mongoId,
|
input.mongoId,
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ export const mysqlRouter = createTRPCRouter({
|
|||||||
.input(apiCreateMySql)
|
.input(apiCreateMySql)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
try {
|
try {
|
||||||
if (ctx.user.role === "member") {
|
if (ctx.user.role.name === "member" || !ctx.user.role.isSystem) {
|
||||||
await checkServiceAccess(
|
await checkServiceAccess(
|
||||||
ctx.user.id,
|
ctx.user.id,
|
||||||
input.projectId,
|
input.projectId,
|
||||||
@@ -69,7 +69,7 @@ export const mysqlRouter = createTRPCRouter({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const newMysql = await createMysql(input);
|
const newMysql = await createMysql(input);
|
||||||
if (ctx.user.role === "member") {
|
if (ctx.user.role.name === "member" || !ctx.user.role.isSystem) {
|
||||||
await addNewService(
|
await addNewService(
|
||||||
ctx.user.id,
|
ctx.user.id,
|
||||||
newMysql.mysqlId,
|
newMysql.mysqlId,
|
||||||
@@ -100,7 +100,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") {
|
if (ctx.user.role.name === "member" || !ctx.user.role.isSystem) {
|
||||||
await checkServiceAccess(
|
await checkServiceAccess(
|
||||||
ctx.user.id,
|
ctx.user.id,
|
||||||
input.mysqlId,
|
input.mysqlId,
|
||||||
@@ -260,7 +260,7 @@ export const mysqlRouter = createTRPCRouter({
|
|||||||
remove: protectedProcedure
|
remove: protectedProcedure
|
||||||
.input(apiFindOneMySql)
|
.input(apiFindOneMySql)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
if (ctx.user.role === "member") {
|
if (ctx.user.role.name === "member" || !ctx.user.role.isSystem) {
|
||||||
await checkServiceAccess(
|
await checkServiceAccess(
|
||||||
ctx.user.id,
|
ctx.user.id,
|
||||||
input.mysqlId,
|
input.mysqlId,
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ import {
|
|||||||
apiUpdateTelegram,
|
apiUpdateTelegram,
|
||||||
notifications,
|
notifications,
|
||||||
server,
|
server,
|
||||||
users_temp,
|
webServer,
|
||||||
} from "@/server/db/schema";
|
} from "@/server/db/schema";
|
||||||
import {
|
import {
|
||||||
IS_CLOUD,
|
IS_CLOUD,
|
||||||
@@ -345,19 +345,19 @@ export const notificationRouter = createTRPCRouter({
|
|||||||
if (input.ServerType === "Dokploy") {
|
if (input.ServerType === "Dokploy") {
|
||||||
const result = await db
|
const result = await db
|
||||||
.select()
|
.select()
|
||||||
.from(users_temp)
|
.from(webServer)
|
||||||
.where(
|
.where(
|
||||||
sql`${users_temp.metricsConfig}::jsonb -> 'server' ->> 'token' = ${input.Token}`,
|
sql`${webServer.metricsConfig}::jsonb -> 'server' ->> 'token' = ${input.Token}`,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!result?.[0]?.id) {
|
if (!result?.[0]?.webServerId) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "BAD_REQUEST",
|
code: "BAD_REQUEST",
|
||||||
message: "Token not found",
|
message: "Token not found",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
organizationId = result?.[0]?.id;
|
organizationId = result?.[0]?.webServerId;
|
||||||
ServerName = "Dokploy";
|
ServerName = "Dokploy";
|
||||||
} else {
|
} else {
|
||||||
const result = await db
|
const result = await db
|
||||||
|
|||||||
@@ -32,8 +32,6 @@ export const organizationRouter = createTRPCRouter({
|
|||||||
.returning()
|
.returning()
|
||||||
.then((res) => res[0]);
|
.then((res) => res[0]);
|
||||||
|
|
||||||
console.log("result", result);
|
|
||||||
|
|
||||||
if (!result) {
|
if (!result) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "INTERNAL_SERVER_ERROR",
|
code: "INTERNAL_SERVER_ERROR",
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ export const postgresRouter = createTRPCRouter({
|
|||||||
.input(apiCreatePostgres)
|
.input(apiCreatePostgres)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
try {
|
try {
|
||||||
if (ctx.user.role === "member") {
|
if (ctx.user.role.name === "member" || !ctx.user.role.isSystem) {
|
||||||
await checkServiceAccess(
|
await checkServiceAccess(
|
||||||
ctx.user.id,
|
ctx.user.id,
|
||||||
input.projectId,
|
input.projectId,
|
||||||
@@ -65,7 +65,7 @@ export const postgresRouter = createTRPCRouter({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
const newPostgres = await createPostgres(input);
|
const newPostgres = await createPostgres(input);
|
||||||
if (ctx.user.role === "member") {
|
if (ctx.user.role.name === "member" || !ctx.user.role.isSystem) {
|
||||||
await addNewService(
|
await addNewService(
|
||||||
ctx.user.id,
|
ctx.user.id,
|
||||||
newPostgres.postgresId,
|
newPostgres.postgresId,
|
||||||
@@ -96,7 +96,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") {
|
if (ctx.user.role.name === "member" || !ctx.user.role.isSystem) {
|
||||||
await checkServiceAccess(
|
await checkServiceAccess(
|
||||||
ctx.user.id,
|
ctx.user.id,
|
||||||
input.postgresId,
|
input.postgresId,
|
||||||
@@ -244,7 +244,7 @@ export const postgresRouter = createTRPCRouter({
|
|||||||
remove: protectedProcedure
|
remove: protectedProcedure
|
||||||
.input(apiFindOnePostgres)
|
.input(apiFindOnePostgres)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
if (ctx.user.role === "member") {
|
if (ctx.user.role.name === "member" || !ctx.user.role.isSystem) {
|
||||||
await checkServiceAccess(
|
await checkServiceAccess(
|
||||||
ctx.user.id,
|
ctx.user.id,
|
||||||
input.postgresId,
|
input.postgresId,
|
||||||
|
|||||||
@@ -1,23 +1,13 @@
|
|||||||
import { db } from "@/server/db";
|
import { apiFindAllByApplication } from "@/server/db/schema";
|
||||||
import { apiFindAllByApplication, applications } from "@/server/db/schema";
|
|
||||||
import {
|
import {
|
||||||
createPreviewDeployment,
|
|
||||||
findApplicationById,
|
findApplicationById,
|
||||||
findPreviewDeploymentByApplicationId,
|
|
||||||
findPreviewDeploymentById,
|
findPreviewDeploymentById,
|
||||||
findPreviewDeploymentsByApplicationId,
|
findPreviewDeploymentsByApplicationId,
|
||||||
findPreviewDeploymentsByPullRequestId,
|
|
||||||
IS_CLOUD,
|
|
||||||
removePreviewDeployment,
|
removePreviewDeployment,
|
||||||
} 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 { createTRPCRouter, protectedProcedure } from "../trpc";
|
||||||
import { eq } from "drizzle-orm";
|
|
||||||
import { and } from "drizzle-orm";
|
|
||||||
import { myQueue } from "@/server/queues/queueSetup";
|
|
||||||
import { deploy } from "@/server/utils/deploy";
|
|
||||||
import type { DeploymentJob } from "@/server/queues/queue-types";
|
|
||||||
|
|
||||||
export const previewDeploymentRouter = createTRPCRouter({
|
export const previewDeploymentRouter = createTRPCRouter({
|
||||||
all: protectedProcedure
|
all: protectedProcedure
|
||||||
@@ -69,142 +59,4 @@ export const previewDeploymentRouter = createTRPCRouter({
|
|||||||
}
|
}
|
||||||
return previewDeployment;
|
return previewDeployment;
|
||||||
}),
|
}),
|
||||||
create: protectedProcedure
|
|
||||||
.input(
|
|
||||||
z.object({
|
|
||||||
action: z.enum(["opened", "synchronize", "reopened", "closed"]),
|
|
||||||
pullRequestId: z.string(),
|
|
||||||
repository: z.string(),
|
|
||||||
owner: z.string(),
|
|
||||||
branch: z.string(),
|
|
||||||
deploymentHash: z.string(),
|
|
||||||
prBranch: z.string(),
|
|
||||||
prNumber: z.any(),
|
|
||||||
prTitle: z.string(),
|
|
||||||
prURL: z.string(),
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.mutation(async ({ input, ctx }) => {
|
|
||||||
const organizationId = ctx.session.activeOrganizationId;
|
|
||||||
const action = input.action;
|
|
||||||
const prId = input.pullRequestId;
|
|
||||||
|
|
||||||
if (action === "closed") {
|
|
||||||
const previewDeploymentResult =
|
|
||||||
await findPreviewDeploymentsByPullRequestId(prId);
|
|
||||||
|
|
||||||
const filteredPreviewDeploymentResult = previewDeploymentResult.filter(
|
|
||||||
(previewDeployment) =>
|
|
||||||
previewDeployment.application.project.organizationId ===
|
|
||||||
organizationId,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (filteredPreviewDeploymentResult.length > 0) {
|
|
||||||
for (const previewDeployment of filteredPreviewDeploymentResult) {
|
|
||||||
try {
|
|
||||||
await removePreviewDeployment(
|
|
||||||
previewDeployment.previewDeploymentId,
|
|
||||||
);
|
|
||||||
} catch (error) {
|
|
||||||
console.log(error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
message: "Preview Deployments Closed",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
action === "opened" ||
|
|
||||||
action === "synchronize" ||
|
|
||||||
action === "reopened"
|
|
||||||
) {
|
|
||||||
const deploymentHash = input.deploymentHash;
|
|
||||||
|
|
||||||
const prBranch = input.prBranch;
|
|
||||||
const prNumber = input.prNumber;
|
|
||||||
const prTitle = input.prTitle;
|
|
||||||
const prURL = input.prURL;
|
|
||||||
const apps = await db.query.applications.findMany({
|
|
||||||
where: and(
|
|
||||||
eq(applications.sourceType, "github"),
|
|
||||||
eq(applications.repository, input.repository),
|
|
||||||
eq(applications.branch, input.branch),
|
|
||||||
eq(applications.isPreviewDeploymentsActive, true),
|
|
||||||
eq(applications.owner, input.owner),
|
|
||||||
),
|
|
||||||
with: {
|
|
||||||
previewDeployments: true,
|
|
||||||
project: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const filteredApps = apps.filter(
|
|
||||||
(app) => app.project.organizationId === organizationId,
|
|
||||||
);
|
|
||||||
|
|
||||||
console.log(filteredApps);
|
|
||||||
|
|
||||||
for (const app of filteredApps) {
|
|
||||||
const previewLimit = app?.previewLimit || 0;
|
|
||||||
if (app?.previewDeployments?.length > previewLimit) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const previewDeploymentResult =
|
|
||||||
await findPreviewDeploymentByApplicationId(app.applicationId, prId);
|
|
||||||
|
|
||||||
let previewDeploymentId =
|
|
||||||
previewDeploymentResult?.previewDeploymentId || "";
|
|
||||||
|
|
||||||
if (!previewDeploymentResult) {
|
|
||||||
try {
|
|
||||||
const previewDeployment = await createPreviewDeployment({
|
|
||||||
applicationId: app.applicationId as string,
|
|
||||||
branch: prBranch,
|
|
||||||
pullRequestId: prId,
|
|
||||||
pullRequestNumber: prNumber,
|
|
||||||
pullRequestTitle: prTitle,
|
|
||||||
pullRequestURL: prURL,
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log(previewDeployment);
|
|
||||||
previewDeploymentId = previewDeployment.previewDeploymentId;
|
|
||||||
} catch (error) {
|
|
||||||
console.log(error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const jobData: DeploymentJob = {
|
|
||||||
applicationId: app.applicationId as string,
|
|
||||||
titleLog: "Preview Deployment",
|
|
||||||
descriptionLog: `Hash: ${deploymentHash}`,
|
|
||||||
type: "deploy",
|
|
||||||
applicationType: "application-preview",
|
|
||||||
server: !!app.serverId,
|
|
||||||
previewDeploymentId,
|
|
||||||
isExternal: true,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (IS_CLOUD && app.serverId) {
|
|
||||||
jobData.serverId = app.serverId;
|
|
||||||
await deploy(jobData);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
await myQueue.add(
|
|
||||||
"deployments",
|
|
||||||
{ ...jobData },
|
|
||||||
{
|
|
||||||
removeOnComplete: true,
|
|
||||||
removeOnFail: true,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
message: "Preview Deployments Created",
|
|
||||||
};
|
|
||||||
}),
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ export const projectRouter = createTRPCRouter({
|
|||||||
.input(apiCreateProject)
|
.input(apiCreateProject)
|
||||||
.mutation(async ({ ctx, input }) => {
|
.mutation(async ({ ctx, input }) => {
|
||||||
try {
|
try {
|
||||||
if (ctx.user.role === "member") {
|
if (ctx.user.role.name === "member" || !ctx.user.role.isSystem) {
|
||||||
await checkProjectAccess(
|
await checkProjectAccess(
|
||||||
ctx.user.id,
|
ctx.user.id,
|
||||||
"create",
|
"create",
|
||||||
@@ -78,7 +78,7 @@ export const projectRouter = createTRPCRouter({
|
|||||||
input,
|
input,
|
||||||
ctx.session.activeOrganizationId,
|
ctx.session.activeOrganizationId,
|
||||||
);
|
);
|
||||||
if (ctx.user.role === "member") {
|
if (ctx.user.role.name === "member" || !ctx.user.role.isSystem) {
|
||||||
await addNewProject(
|
await addNewProject(
|
||||||
ctx.user.id,
|
ctx.user.id,
|
||||||
project.projectId,
|
project.projectId,
|
||||||
@@ -99,7 +99,7 @@ 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.name === "member" || !ctx.user.role.isSystem) {
|
||||||
const { accessedServices } = await findMemberById(
|
const { accessedServices } = await findMemberById(
|
||||||
ctx.user.id,
|
ctx.user.id,
|
||||||
ctx.session.activeOrganizationId,
|
ctx.session.activeOrganizationId,
|
||||||
@@ -164,7 +164,7 @@ 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.name === "member" || !ctx.user.role.isSystem) {
|
||||||
const { accessedProjects, accessedServices } = await findMemberById(
|
const { accessedProjects, accessedServices } = await findMemberById(
|
||||||
ctx.user.id,
|
ctx.user.id,
|
||||||
ctx.session.activeOrganizationId,
|
ctx.session.activeOrganizationId,
|
||||||
@@ -241,7 +241,7 @@ export const projectRouter = createTRPCRouter({
|
|||||||
.input(apiRemoveProject)
|
.input(apiRemoveProject)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
try {
|
try {
|
||||||
if (ctx.user.role === "member") {
|
if (ctx.user.role.name === "member" || !ctx.user.role.isSystem) {
|
||||||
await checkProjectAccess(
|
await checkProjectAccess(
|
||||||
ctx.user.id,
|
ctx.user.id,
|
||||||
"delete",
|
"delete",
|
||||||
@@ -314,7 +314,7 @@ export const projectRouter = createTRPCRouter({
|
|||||||
)
|
)
|
||||||
.mutation(async ({ ctx, input }) => {
|
.mutation(async ({ ctx, input }) => {
|
||||||
try {
|
try {
|
||||||
if (ctx.user.role === "member") {
|
if (ctx.user.role.name === "member" || !ctx.user.role.isSystem) {
|
||||||
await checkProjectAccess(
|
await checkProjectAccess(
|
||||||
ctx.user.id,
|
ctx.user.id,
|
||||||
"create",
|
"create",
|
||||||
@@ -649,7 +649,10 @@ export const projectRouter = createTRPCRouter({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!input.duplicateInSameProject && ctx.user.role === "member") {
|
if (
|
||||||
|
!input.duplicateInSameProject &&
|
||||||
|
(ctx.user.role.name === "member" || !ctx.user.role.isSystem)
|
||||||
|
) {
|
||||||
await addNewProject(
|
await addNewProject(
|
||||||
ctx.user.id,
|
ctx.user.id,
|
||||||
targetProject.projectId,
|
targetProject.projectId,
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ export const redisRouter = createTRPCRouter({
|
|||||||
.input(apiCreateRedis)
|
.input(apiCreateRedis)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
try {
|
try {
|
||||||
if (ctx.user.role === "member") {
|
if (ctx.user.role.name === "member" || !ctx.user.role.isSystem) {
|
||||||
await checkServiceAccess(
|
await checkServiceAccess(
|
||||||
ctx.user.id,
|
ctx.user.id,
|
||||||
input.projectId,
|
input.projectId,
|
||||||
@@ -65,7 +65,7 @@ export const redisRouter = createTRPCRouter({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
const newRedis = await createRedis(input);
|
const newRedis = await createRedis(input);
|
||||||
if (ctx.user.role === "member") {
|
if (ctx.user.role.name === "member" || !ctx.user.role.isSystem) {
|
||||||
await addNewService(
|
await addNewService(
|
||||||
ctx.user.id,
|
ctx.user.id,
|
||||||
newRedis.redisId,
|
newRedis.redisId,
|
||||||
@@ -89,7 +89,7 @@ export const redisRouter = createTRPCRouter({
|
|||||||
one: protectedProcedure
|
one: protectedProcedure
|
||||||
.input(apiFindOneRedis)
|
.input(apiFindOneRedis)
|
||||||
.query(async ({ input, ctx }) => {
|
.query(async ({ input, ctx }) => {
|
||||||
if (ctx.user.role === "member") {
|
if (ctx.user.role.name === "member" || !ctx.user.role.isSystem) {
|
||||||
await checkServiceAccess(
|
await checkServiceAccess(
|
||||||
ctx.user.id,
|
ctx.user.id,
|
||||||
input.redisId,
|
input.redisId,
|
||||||
@@ -251,7 +251,7 @@ export const redisRouter = createTRPCRouter({
|
|||||||
remove: protectedProcedure
|
remove: protectedProcedure
|
||||||
.input(apiFindOneRedis)
|
.input(apiFindOneRedis)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
if (ctx.user.role === "member") {
|
if (ctx.user.role.name === "member" || !ctx.user.role.isSystem) {
|
||||||
await checkServiceAccess(
|
await checkServiceAccess(
|
||||||
ctx.user.id,
|
ctx.user.id,
|
||||||
input.redisId,
|
input.redisId,
|
||||||
|
|||||||
75
apps/dokploy/server/api/routers/role.ts
Normal file
75
apps/dokploy/server/api/routers/role.ts
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
import { createTRPCRouter } from "@/server/api/trpc";
|
||||||
|
// import { createRole, removeRoleById, updateRoleById } from "@dokploy/server";
|
||||||
|
// import { defaultPermissions } from "@dokploy/server/lib/permissions";
|
||||||
|
|
||||||
|
export const roleRouter = createTRPCRouter({
|
||||||
|
// all: protectedProcedure.query(async ({ ctx }) => {
|
||||||
|
// const roles = await db.query.role.findMany({
|
||||||
|
// where: and(
|
||||||
|
// eq(role.organizationId, ctx.session.activeOrganizationId),
|
||||||
|
// eq(role.isSystem, false),
|
||||||
|
// ),
|
||||||
|
// orderBy: [asc(role.createdAt)],
|
||||||
|
// });
|
||||||
|
// return roles;
|
||||||
|
// }),
|
||||||
|
// delete: protectedProcedure
|
||||||
|
// .input(apiFindOneRole)
|
||||||
|
// .mutation(async ({ input }) => {
|
||||||
|
// try {
|
||||||
|
// return removeRoleById(input.roleId);
|
||||||
|
// } catch (error) {
|
||||||
|
// const message =
|
||||||
|
// error instanceof Error ? error.message : "Error input: Deleting role";
|
||||||
|
// throw new TRPCError({
|
||||||
|
// code: "BAD_REQUEST",
|
||||||
|
// message,
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
// }),
|
||||||
|
// create: protectedProcedure
|
||||||
|
// .input(createRoleSchema)
|
||||||
|
// .mutation(async ({ input, ctx }) => {
|
||||||
|
// try {
|
||||||
|
// return await createRole(
|
||||||
|
// {
|
||||||
|
// ...input,
|
||||||
|
// },
|
||||||
|
// ctx.session.activeOrganizationId,
|
||||||
|
// );
|
||||||
|
// } catch (error) {
|
||||||
|
// console.error(error);
|
||||||
|
// throw new TRPCError({
|
||||||
|
// code: "BAD_REQUEST",
|
||||||
|
// message: "Error input: Creating role",
|
||||||
|
// cause: error,
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
// }),
|
||||||
|
// update: protectedProcedure
|
||||||
|
// .input(updateRoleSchema)
|
||||||
|
// .mutation(async ({ input }) => {
|
||||||
|
// return await updateRoleById(input.roleId, input);
|
||||||
|
// }),
|
||||||
|
// getDefaultRoles: protectedProcedure.query(async ({ ctx }) => {
|
||||||
|
// const roles = await db.query.role.findMany({
|
||||||
|
// where: and(
|
||||||
|
// eq(role.organizationId, ctx.session.activeOrganizationId),
|
||||||
|
// eq(role.isSystem, true),
|
||||||
|
// ),
|
||||||
|
// });
|
||||||
|
// // add the description from the constants roles to the roles
|
||||||
|
// const rolesWithDescription = defaultPermissions.map((role) => {
|
||||||
|
// const roleInfo = roles.find((r) => r.name === role.name);
|
||||||
|
// return {
|
||||||
|
// ...roleInfo,
|
||||||
|
// ...role,
|
||||||
|
// };
|
||||||
|
// });
|
||||||
|
// const set = new Set(rolesWithDescription.flatMap((r) => r.permissions));
|
||||||
|
// return {
|
||||||
|
// roles: rolesWithDescription,
|
||||||
|
// permissions: Array.from(set),
|
||||||
|
// };
|
||||||
|
// }),
|
||||||
|
});
|
||||||
@@ -1,16 +1,12 @@
|
|||||||
import { db } from "@/server/db";
|
import { db } from "@/server/db";
|
||||||
import {
|
import {
|
||||||
apiAssignDomain,
|
|
||||||
apiEnableDashboard,
|
apiEnableDashboard,
|
||||||
apiModifyTraefikConfig,
|
apiModifyTraefikConfig,
|
||||||
apiReadStatsLogs,
|
apiReadStatsLogs,
|
||||||
apiReadTraefikConfig,
|
apiReadTraefikConfig,
|
||||||
apiSaveSSHKey,
|
|
||||||
apiServerSchema,
|
apiServerSchema,
|
||||||
apiTraefikConfig,
|
apiTraefikConfig,
|
||||||
apiUpdateDockerCleanup,
|
|
||||||
} from "@/server/db/schema";
|
} from "@/server/db/schema";
|
||||||
import { removeJob, schedule } from "@/server/utils/backup";
|
|
||||||
import {
|
import {
|
||||||
DEFAULT_UPDATE_DATA,
|
DEFAULT_UPDATE_DATA,
|
||||||
IS_CLOUD,
|
IS_CLOUD,
|
||||||
@@ -23,7 +19,6 @@ import {
|
|||||||
execAsync,
|
execAsync,
|
||||||
execAsyncRemote,
|
execAsyncRemote,
|
||||||
findServerById,
|
findServerById,
|
||||||
findUserById,
|
|
||||||
getDokployImage,
|
getDokployImage,
|
||||||
getDokployImageTag,
|
getDokployImageTag,
|
||||||
getLogCleanupStatus,
|
getLogCleanupStatus,
|
||||||
@@ -40,14 +35,9 @@ import {
|
|||||||
readMainConfig,
|
readMainConfig,
|
||||||
readMonitoringConfig,
|
readMonitoringConfig,
|
||||||
recreateDirectory,
|
recreateDirectory,
|
||||||
sendDockerCleanupNotifications,
|
|
||||||
spawnAsync,
|
spawnAsync,
|
||||||
startLogCleanup,
|
startLogCleanup,
|
||||||
stopLogCleanup,
|
stopLogCleanup,
|
||||||
updateLetsEncryptEmail,
|
|
||||||
updateServerById,
|
|
||||||
updateServerTraefik,
|
|
||||||
updateUser,
|
|
||||||
writeConfig,
|
writeConfig,
|
||||||
writeMainConfig,
|
writeMainConfig,
|
||||||
writeTraefikConfigInPath,
|
writeTraefikConfigInPath,
|
||||||
@@ -57,7 +47,6 @@ import { generateOpenApiDocument } from "@dokploy/trpc-openapi";
|
|||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
import { sql } from "drizzle-orm";
|
import { sql } from "drizzle-orm";
|
||||||
import { dump, load } from "js-yaml";
|
import { dump, load } from "js-yaml";
|
||||||
import { scheduleJob, scheduledJobs } from "node-schedule";
|
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import packageInfo from "../../../package.json";
|
import packageInfo from "../../../package.json";
|
||||||
import { appRouter } from "../root";
|
import { appRouter } from "../root";
|
||||||
@@ -187,135 +176,6 @@ export const settingsRouter = createTRPCRouter({
|
|||||||
await recreateDirectory(MONITORING_PATH);
|
await recreateDirectory(MONITORING_PATH);
|
||||||
return true;
|
return true;
|
||||||
}),
|
}),
|
||||||
saveSSHPrivateKey: adminProcedure
|
|
||||||
.input(apiSaveSSHKey)
|
|
||||||
.mutation(async ({ input, ctx }) => {
|
|
||||||
if (IS_CLOUD) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
await updateUser(ctx.user.id, {
|
|
||||||
sshPrivateKey: input.sshPrivateKey,
|
|
||||||
});
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}),
|
|
||||||
assignDomainServer: adminProcedure
|
|
||||||
.input(apiAssignDomain)
|
|
||||||
.mutation(async ({ ctx, input }) => {
|
|
||||||
if (IS_CLOUD) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
const user = await updateUser(ctx.user.id, {
|
|
||||||
host: input.host,
|
|
||||||
...(input.letsEncryptEmail && {
|
|
||||||
letsEncryptEmail: input.letsEncryptEmail,
|
|
||||||
}),
|
|
||||||
certificateType: input.certificateType,
|
|
||||||
https: input.https,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!user) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "NOT_FOUND",
|
|
||||||
message: "User not found",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
updateServerTraefik(user, input.host);
|
|
||||||
if (input.letsEncryptEmail) {
|
|
||||||
updateLetsEncryptEmail(input.letsEncryptEmail);
|
|
||||||
}
|
|
||||||
|
|
||||||
return user;
|
|
||||||
}),
|
|
||||||
cleanSSHPrivateKey: adminProcedure.mutation(async ({ ctx }) => {
|
|
||||||
if (IS_CLOUD) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
await updateUser(ctx.user.id, {
|
|
||||||
sshPrivateKey: null,
|
|
||||||
});
|
|
||||||
return true;
|
|
||||||
}),
|
|
||||||
updateDockerCleanup: adminProcedure
|
|
||||||
.input(apiUpdateDockerCleanup)
|
|
||||||
.mutation(async ({ input, ctx }) => {
|
|
||||||
if (input.serverId) {
|
|
||||||
await updateServerById(input.serverId, {
|
|
||||||
enableDockerCleanup: input.enableDockerCleanup,
|
|
||||||
});
|
|
||||||
|
|
||||||
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",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (server.enableDockerCleanup) {
|
|
||||||
const server = await findServerById(input.serverId);
|
|
||||||
if (server.serverStatus === "inactive") {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "NOT_FOUND",
|
|
||||||
message: "Server is inactive",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (IS_CLOUD) {
|
|
||||||
await schedule({
|
|
||||||
cronSchedule: "0 0 * * *",
|
|
||||||
serverId: input.serverId,
|
|
||||||
type: "server",
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
scheduleJob(server.serverId, "0 0 * * *", async () => {
|
|
||||||
console.log(
|
|
||||||
`Docker Cleanup ${new Date().toLocaleString()}] Running...`,
|
|
||||||
);
|
|
||||||
await cleanUpUnusedImages(server.serverId);
|
|
||||||
await cleanUpDockerBuilder(server.serverId);
|
|
||||||
await cleanUpSystemPrune(server.serverId);
|
|
||||||
await sendDockerCleanupNotifications(server.organizationId);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (IS_CLOUD) {
|
|
||||||
await removeJob({
|
|
||||||
cronSchedule: "0 0 * * *",
|
|
||||||
serverId: input.serverId,
|
|
||||||
type: "server",
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
const currentJob = scheduledJobs[server.serverId];
|
|
||||||
currentJob?.cancel();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (!IS_CLOUD) {
|
|
||||||
const userUpdated = await updateUser(ctx.user.id, {
|
|
||||||
enableDockerCleanup: input.enableDockerCleanup,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (userUpdated?.enableDockerCleanup) {
|
|
||||||
scheduleJob("docker-cleanup", "0 0 * * *", async () => {
|
|
||||||
console.log(
|
|
||||||
`Docker Cleanup ${new Date().toLocaleString()}] Running...`,
|
|
||||||
);
|
|
||||||
await cleanUpUnusedImages();
|
|
||||||
await cleanUpDockerBuilder();
|
|
||||||
await cleanUpSystemPrune();
|
|
||||||
await sendDockerCleanupNotifications(
|
|
||||||
ctx.session.activeOrganizationId,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
const currentJob = scheduledJobs["docker-cleanup"];
|
|
||||||
currentJob?.cancel();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}),
|
|
||||||
|
|
||||||
readTraefikConfig: adminProcedure.query(() => {
|
readTraefikConfig: adminProcedure.query(() => {
|
||||||
if (IS_CLOUD) {
|
if (IS_CLOUD) {
|
||||||
@@ -470,13 +330,6 @@ export const settingsRouter = createTRPCRouter({
|
|||||||
|
|
||||||
return readConfigInPath(input.path, input.serverId);
|
return readConfigInPath(input.path, input.serverId);
|
||||||
}),
|
}),
|
||||||
getIp: protectedProcedure.query(async ({ ctx }) => {
|
|
||||||
if (IS_CLOUD) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
const user = await findUserById(ctx.user.ownerId);
|
|
||||||
return user.serverIp;
|
|
||||||
}),
|
|
||||||
|
|
||||||
getOpenApiDocument: protectedProcedure.query(
|
getOpenApiDocument: protectedProcedure.query(
|
||||||
async ({ ctx }): Promise<unknown> => {
|
async ({ ctx }): Promise<unknown> => {
|
||||||
@@ -641,10 +494,16 @@ export const settingsRouter = createTRPCRouter({
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
.input(
|
.input(
|
||||||
z.object({
|
z
|
||||||
start: z.string().optional(),
|
.object({
|
||||||
end: z.string().optional(),
|
dateRange: z
|
||||||
}),
|
.object({
|
||||||
|
start: z.string().optional(),
|
||||||
|
end: z.string().optional(),
|
||||||
|
})
|
||||||
|
.optional(),
|
||||||
|
})
|
||||||
|
.optional(),
|
||||||
)
|
)
|
||||||
.query(async ({ input }) => {
|
.query(async ({ input }) => {
|
||||||
if (IS_CLOUD) {
|
if (IS_CLOUD) {
|
||||||
|
|||||||
@@ -1,14 +1,13 @@
|
|||||||
import {
|
import {
|
||||||
IS_CLOUD,
|
IS_CLOUD,
|
||||||
createApiKey,
|
createApiKey,
|
||||||
findAdmin,
|
|
||||||
findNotificationById,
|
findNotificationById,
|
||||||
findOrganizationById,
|
findOrganizationById,
|
||||||
findUserById,
|
|
||||||
getUserByToken,
|
getUserByToken,
|
||||||
removeUserById,
|
removeUserById,
|
||||||
sendEmailNotification,
|
sendEmailNotification,
|
||||||
updateUser,
|
updateUser,
|
||||||
|
findWebServer,
|
||||||
} from "@dokploy/server";
|
} from "@dokploy/server";
|
||||||
import { db } from "@dokploy/server/db";
|
import { db } from "@dokploy/server/db";
|
||||||
import {
|
import {
|
||||||
@@ -30,6 +29,7 @@ import {
|
|||||||
protectedProcedure,
|
protectedProcedure,
|
||||||
publicProcedure,
|
publicProcedure,
|
||||||
} from "../trpc";
|
} from "../trpc";
|
||||||
|
import { sendEmail } from "@dokploy/server/verification/send-verification-email";
|
||||||
|
|
||||||
const apiCreateApiKey = z.object({
|
const apiCreateApiKey = z.object({
|
||||||
name: z.string().min(1),
|
name: z.string().min(1),
|
||||||
@@ -54,6 +54,7 @@ export const userRouter = createTRPCRouter({
|
|||||||
where: eq(member.organizationId, ctx.session.activeOrganizationId),
|
where: eq(member.organizationId, ctx.session.activeOrganizationId),
|
||||||
with: {
|
with: {
|
||||||
user: true,
|
user: true,
|
||||||
|
role: true,
|
||||||
},
|
},
|
||||||
orderBy: [asc(member.createdAt)],
|
orderBy: [asc(member.createdAt)],
|
||||||
});
|
});
|
||||||
@@ -86,7 +87,10 @@ export const userRouter = createTRPCRouter({
|
|||||||
// Allow access if:
|
// Allow access if:
|
||||||
// 1. User is requesting their own information
|
// 1. User is requesting their own information
|
||||||
// 2. User has owner role (admin permissions) AND user is in the same organization
|
// 2. User has owner role (admin permissions) AND user is in the same organization
|
||||||
if (memberResult.userId !== ctx.user.id && ctx.user.role !== "owner") {
|
if (
|
||||||
|
memberResult.userId !== ctx.user.id &&
|
||||||
|
ctx.user.role?.name !== "owner"
|
||||||
|
) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
message: "You are not authorized to access this user",
|
message: "You are not authorized to access this user",
|
||||||
@@ -102,6 +106,7 @@ export const userRouter = createTRPCRouter({
|
|||||||
eq(member.organizationId, ctx.session?.activeOrganizationId || ""),
|
eq(member.organizationId, ctx.session?.activeOrganizationId || ""),
|
||||||
),
|
),
|
||||||
with: {
|
with: {
|
||||||
|
role: true,
|
||||||
user: {
|
user: {
|
||||||
with: {
|
with: {
|
||||||
apiKeys: true,
|
apiKeys: true,
|
||||||
@@ -147,19 +152,6 @@ export const userRouter = createTRPCRouter({
|
|||||||
|
|
||||||
return memberResult?.user;
|
return memberResult?.user;
|
||||||
}),
|
}),
|
||||||
getServerMetrics: protectedProcedure.query(async ({ ctx }) => {
|
|
||||||
const memberResult = await db.query.member.findFirst({
|
|
||||||
where: and(
|
|
||||||
eq(member.userId, ctx.user.id),
|
|
||||||
eq(member.organizationId, ctx.session?.activeOrganizationId || ""),
|
|
||||||
),
|
|
||||||
with: {
|
|
||||||
user: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return memberResult?.user;
|
|
||||||
}),
|
|
||||||
update: protectedProcedure
|
update: protectedProcedure
|
||||||
.input(apiUpdateUser)
|
.input(apiUpdateUser)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
@@ -199,14 +191,6 @@ export const userRouter = createTRPCRouter({
|
|||||||
.query(async ({ input }) => {
|
.query(async ({ input }) => {
|
||||||
return await getUserByToken(input.token);
|
return await getUserByToken(input.token);
|
||||||
}),
|
}),
|
||||||
getMetricsToken: protectedProcedure.query(async ({ ctx }) => {
|
|
||||||
const user = await findUserById(ctx.user.ownerId);
|
|
||||||
return {
|
|
||||||
serverIp: user.serverIp,
|
|
||||||
enabledFeatures: user.enablePaidFeatures,
|
|
||||||
metricsConfig: user?.metricsConfig,
|
|
||||||
};
|
|
||||||
}),
|
|
||||||
remove: protectedProcedure
|
remove: protectedProcedure
|
||||||
.input(
|
.input(
|
||||||
z.object({
|
z.object({
|
||||||
@@ -383,6 +367,83 @@ export const userRouter = createTRPCRouter({
|
|||||||
|
|
||||||
return organizations.length;
|
return organizations.length;
|
||||||
}),
|
}),
|
||||||
|
createInvitation: adminProcedure
|
||||||
|
.input(
|
||||||
|
z.object({
|
||||||
|
email: z.string().email(),
|
||||||
|
role: z.string(),
|
||||||
|
organizationId: z.string(),
|
||||||
|
notificationId: z.string(),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.mutation(async ({ input, ctx }) => {
|
||||||
|
const organization = await findOrganizationById(input.organizationId);
|
||||||
|
if (organization?.ownerId !== ctx.user.ownerId) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "UNAUTHORIZED",
|
||||||
|
message: "You are not allowed to create invitations",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const invitationResult = await db
|
||||||
|
.insert(invitation)
|
||||||
|
.values({
|
||||||
|
email: input.email,
|
||||||
|
role: input.role,
|
||||||
|
organizationId: input.organizationId,
|
||||||
|
status: "pending",
|
||||||
|
// 24 hours
|
||||||
|
expiresAt: new Date(Date.now() + 1000 * 60 * 60 * 24),
|
||||||
|
inviterId: ctx.user.id,
|
||||||
|
})
|
||||||
|
.returning()
|
||||||
|
.then(([invitation]) => invitation);
|
||||||
|
|
||||||
|
const webServer = await findWebServer();
|
||||||
|
|
||||||
|
let host = "";
|
||||||
|
|
||||||
|
if (process.env.NODE_ENV === "development") {
|
||||||
|
host = "http://localhost:3000";
|
||||||
|
} else {
|
||||||
|
host = webServer.host || "";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (IS_CLOUD) {
|
||||||
|
host = "https://app.dokploy.com";
|
||||||
|
}
|
||||||
|
|
||||||
|
const inviteLink = `${host}/invitation?token=${invitationResult?.id}`;
|
||||||
|
if (IS_CLOUD) {
|
||||||
|
await sendEmail({
|
||||||
|
email: invitationResult?.email || "",
|
||||||
|
subject: "Invitation to join organization",
|
||||||
|
text: `
|
||||||
|
<p>You are invited to join ${organization?.name || "organization"} on Dokploy. Click the link to accept the invitation: <a href="${inviteLink}">Accept Invitation</a></p>
|
||||||
|
`,
|
||||||
|
});
|
||||||
|
} else if (input.notificationId) {
|
||||||
|
const notification = await findNotificationById(input.notificationId);
|
||||||
|
|
||||||
|
const email = notification.email;
|
||||||
|
|
||||||
|
if (!email) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "NOT_FOUND",
|
||||||
|
message: "Email notification not found",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
await sendEmailNotification(
|
||||||
|
{
|
||||||
|
...email,
|
||||||
|
toAddresses: [invitationResult?.email || ""],
|
||||||
|
},
|
||||||
|
"Invitation to join organization",
|
||||||
|
`
|
||||||
|
<p>You are invited to join ${organization?.name || "organization"} on Dokploy. Click the link to accept the invitation: <a href="${inviteLink}">Accept Invitation</a></p>
|
||||||
|
`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}),
|
||||||
sendInvitation: adminProcedure
|
sendInvitation: adminProcedure
|
||||||
.input(
|
.input(
|
||||||
z.object({
|
z.object({
|
||||||
@@ -410,11 +471,11 @@ export const userRouter = createTRPCRouter({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const admin = await findAdmin();
|
const webServer = await findWebServer();
|
||||||
const host =
|
const host =
|
||||||
process.env.NODE_ENV === "development"
|
process.env.NODE_ENV === "development"
|
||||||
? "http://localhost:3000"
|
? "http://localhost:3000"
|
||||||
: admin.user.host;
|
: webServer.host;
|
||||||
const inviteLink = `${host}/invitation?token=${input.invitationId}`;
|
const inviteLink = `${host}/invitation?token=${input.invitationId}`;
|
||||||
|
|
||||||
const organization = await findOrganizationById(
|
const organization = await findOrganizationById(
|
||||||
@@ -438,4 +499,52 @@ export const userRouter = createTRPCRouter({
|
|||||||
}
|
}
|
||||||
return inviteLink;
|
return inviteLink;
|
||||||
}),
|
}),
|
||||||
|
assignRole: adminProcedure
|
||||||
|
.input(
|
||||||
|
z.object({
|
||||||
|
userId: z.string(),
|
||||||
|
roleId: z.string(),
|
||||||
|
accessedProjects: z.array(z.string()).optional(),
|
||||||
|
accessedServices: z.array(z.string()).optional(),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.mutation(async ({ input, ctx }) => {
|
||||||
|
try {
|
||||||
|
const organization = await findOrganizationById(
|
||||||
|
ctx.session.activeOrganizationId,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (organization?.ownerId !== ctx.user.ownerId) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "UNAUTHORIZED",
|
||||||
|
message: "You are not allowed to assign roles",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const memberResult = await db.query.member.findFirst({
|
||||||
|
where: and(
|
||||||
|
eq(member.userId, input.userId),
|
||||||
|
eq(member.organizationId, ctx.session.activeOrganizationId),
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!memberResult) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "NOT_FOUND",
|
||||||
|
message: "Member not found",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await db
|
||||||
|
.update(member)
|
||||||
|
.set({
|
||||||
|
roleId: input.roleId,
|
||||||
|
accessedProjects: input.accessedProjects || [],
|
||||||
|
accessedServices: input.accessedServices || [],
|
||||||
|
})
|
||||||
|
.where(eq(member.id, memberResult.id));
|
||||||
|
} catch (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
|
|||||||
169
apps/dokploy/server/api/routers/web-server.ts
Normal file
169
apps/dokploy/server/api/routers/web-server.ts
Normal file
@@ -0,0 +1,169 @@
|
|||||||
|
import {
|
||||||
|
apiAssignDomain,
|
||||||
|
apiSaveSSHKey,
|
||||||
|
apiUpdateDockerCleanup,
|
||||||
|
updateWebServerSchema,
|
||||||
|
} from "@/server/db/schema";
|
||||||
|
import { removeJob, schedule } from "@/server/utils/backup";
|
||||||
|
import {
|
||||||
|
IS_CLOUD,
|
||||||
|
cleanUpDockerBuilder,
|
||||||
|
cleanUpSystemPrune,
|
||||||
|
cleanUpUnusedImages,
|
||||||
|
findServerById,
|
||||||
|
findWebServer,
|
||||||
|
sendDockerCleanupNotifications,
|
||||||
|
updateLetsEncryptEmail,
|
||||||
|
updateServerById,
|
||||||
|
updateServerTraefik,
|
||||||
|
updateWebServer,
|
||||||
|
} from "@dokploy/server";
|
||||||
|
import { TRPCError } from "@trpc/server";
|
||||||
|
import { scheduleJob, scheduledJobs } from "node-schedule";
|
||||||
|
import { adminProcedure, createTRPCRouter } from "../trpc";
|
||||||
|
|
||||||
|
export const webServerRouter = createTRPCRouter({
|
||||||
|
get: adminProcedure.query(async () => {
|
||||||
|
if (IS_CLOUD) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return await findWebServer();
|
||||||
|
}),
|
||||||
|
update: adminProcedure
|
||||||
|
.input(updateWebServerSchema)
|
||||||
|
.mutation(async ({ input }) => {
|
||||||
|
if (IS_CLOUD) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return await updateWebServer(input);
|
||||||
|
}),
|
||||||
|
saveSSHPrivateKey: adminProcedure
|
||||||
|
.input(apiSaveSSHKey)
|
||||||
|
.mutation(async ({ input }) => {
|
||||||
|
if (IS_CLOUD) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
await updateWebServer({
|
||||||
|
sshPrivateKey: input.sshPrivateKey,
|
||||||
|
});
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}),
|
||||||
|
assignDomainServer: adminProcedure
|
||||||
|
.input(apiAssignDomain)
|
||||||
|
.mutation(async ({ input }) => {
|
||||||
|
if (IS_CLOUD) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const webServer = await updateWebServer({
|
||||||
|
host: input.host,
|
||||||
|
...(input.letsEncryptEmail && {
|
||||||
|
letsEncryptEmail: input.letsEncryptEmail,
|
||||||
|
}),
|
||||||
|
certificateType: input.certificateType,
|
||||||
|
https: input.https,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!webServer) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "NOT_FOUND",
|
||||||
|
message: "User not found",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
updateServerTraefik(webServer, input.host);
|
||||||
|
if (input.letsEncryptEmail) {
|
||||||
|
updateLetsEncryptEmail(input.letsEncryptEmail);
|
||||||
|
}
|
||||||
|
|
||||||
|
return webServer;
|
||||||
|
}),
|
||||||
|
cleanSSHPrivateKey: adminProcedure.mutation(async () => {
|
||||||
|
if (IS_CLOUD) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
await updateWebServer({
|
||||||
|
sshPrivateKey: null,
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
}),
|
||||||
|
updateDockerCleanup: adminProcedure
|
||||||
|
.input(apiUpdateDockerCleanup)
|
||||||
|
.mutation(async ({ input, ctx }) => {
|
||||||
|
if (input.serverId) {
|
||||||
|
await updateServerById(input.serverId, {
|
||||||
|
enableDockerCleanup: input.enableDockerCleanup,
|
||||||
|
});
|
||||||
|
|
||||||
|
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",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (server.enableDockerCleanup) {
|
||||||
|
const server = await findServerById(input.serverId);
|
||||||
|
if (server.serverStatus === "inactive") {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "NOT_FOUND",
|
||||||
|
message: "Server is inactive",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (IS_CLOUD) {
|
||||||
|
await schedule({
|
||||||
|
cronSchedule: "0 0 * * *",
|
||||||
|
serverId: input.serverId,
|
||||||
|
type: "server",
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
scheduleJob(server.serverId, "0 0 * * *", async () => {
|
||||||
|
console.log(
|
||||||
|
`Docker Cleanup ${new Date().toLocaleString()}] Running...`,
|
||||||
|
);
|
||||||
|
await cleanUpUnusedImages(server.serverId);
|
||||||
|
await cleanUpDockerBuilder(server.serverId);
|
||||||
|
await cleanUpSystemPrune(server.serverId);
|
||||||
|
await sendDockerCleanupNotifications(server.organizationId);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (IS_CLOUD) {
|
||||||
|
await removeJob({
|
||||||
|
cronSchedule: "0 0 * * *",
|
||||||
|
serverId: input.serverId,
|
||||||
|
type: "server",
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
const currentJob = scheduledJobs[server.serverId];
|
||||||
|
currentJob?.cancel();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (!IS_CLOUD) {
|
||||||
|
const userUpdated = await updateWebServer({
|
||||||
|
enableDockerCleanup: input.enableDockerCleanup,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (userUpdated?.enableDockerCleanup) {
|
||||||
|
scheduleJob("docker-cleanup", "0 0 * * *", async () => {
|
||||||
|
console.log(
|
||||||
|
`Docker Cleanup ${new Date().toLocaleString()}] Running...`,
|
||||||
|
);
|
||||||
|
await cleanUpUnusedImages();
|
||||||
|
await cleanUpDockerBuilder();
|
||||||
|
await cleanUpSystemPrune();
|
||||||
|
await sendDockerCleanupNotifications(
|
||||||
|
ctx.session.activeOrganizationId,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
const currentJob = scheduledJobs["docker-cleanup"];
|
||||||
|
currentJob?.cancel();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}),
|
||||||
|
});
|
||||||
@@ -98,7 +98,6 @@ export const deploymentWorker = new Worker(
|
|||||||
titleLog: job.data.titleLog,
|
titleLog: job.data.titleLog,
|
||||||
descriptionLog: job.data.descriptionLog,
|
descriptionLog: job.data.descriptionLog,
|
||||||
previewDeploymentId: job.data.previewDeploymentId,
|
previewDeploymentId: job.data.previewDeploymentId,
|
||||||
isExternal: job.data.isExternal,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -108,7 +107,6 @@ export const deploymentWorker = new Worker(
|
|||||||
titleLog: job.data.titleLog,
|
titleLog: job.data.titleLog,
|
||||||
descriptionLog: job.data.descriptionLog,
|
descriptionLog: job.data.descriptionLog,
|
||||||
previewDeploymentId: job.data.previewDeploymentId,
|
previewDeploymentId: job.data.previewDeploymentId,
|
||||||
isExternal: job.data.isExternal,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,7 +26,6 @@ type DeployJob =
|
|||||||
applicationType: "application-preview";
|
applicationType: "application-preview";
|
||||||
previewDeploymentId: string;
|
previewDeploymentId: string;
|
||||||
serverId?: string;
|
serverId?: string;
|
||||||
isExternal?: boolean;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type DeploymentJob = DeployJob;
|
export type DeploymentJob = DeployJob;
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
// boolean,
|
// boolean,
|
||||||
// } from "drizzle-orm/pg-core";
|
// } from "drizzle-orm/pg-core";
|
||||||
|
|
||||||
// export const users_temp = pgTable("users_temp", {
|
// export const users = pgTable("users", {
|
||||||
// id: text("id").primaryKey(),
|
// id: text("id").primaryKey(),
|
||||||
// name: text("name").notNull(),
|
// name: text("name").notNull(),
|
||||||
// email: text("email").notNull().unique(),
|
// email: text("email").notNull().unique(),
|
||||||
@@ -29,7 +29,7 @@
|
|||||||
// userAgent: text("user_agent"),
|
// userAgent: text("user_agent"),
|
||||||
// userId: text("user_id")
|
// userId: text("user_id")
|
||||||
// .notNull()
|
// .notNull()
|
||||||
// .references(() => users_temp.id, { onDelete: "cascade" }),
|
// .references(() => users.id, { onDelete: "cascade" }),
|
||||||
// activeOrganizationId: text("active_organization_id"),
|
// activeOrganizationId: text("active_organization_id"),
|
||||||
// });
|
// });
|
||||||
|
|
||||||
@@ -39,7 +39,7 @@
|
|||||||
// providerId: text("provider_id").notNull(),
|
// providerId: text("provider_id").notNull(),
|
||||||
// userId: text("user_id")
|
// userId: text("user_id")
|
||||||
// .notNull()
|
// .notNull()
|
||||||
// .references(() => users_temp.id, { onDelete: "cascade" }),
|
// .references(() => users.id, { onDelete: "cascade" }),
|
||||||
// accessToken: text("access_token"),
|
// accessToken: text("access_token"),
|
||||||
// refreshToken: text("refresh_token"),
|
// refreshToken: text("refresh_token"),
|
||||||
// idToken: text("id_token"),
|
// idToken: text("id_token"),
|
||||||
|
|||||||
26
packages/server/src/db/migrations/create-default-roles.ts
Normal file
26
packages/server/src/db/migrations/create-default-roles.ts
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import { sql } from "drizzle-orm";
|
||||||
|
import { db } from "..";
|
||||||
|
import { organization } from "../schema/account";
|
||||||
|
import { getDefaultRolesSQL } from "../schema/rbac";
|
||||||
|
|
||||||
|
export async function createDefaultRoles() {
|
||||||
|
try {
|
||||||
|
// Get all organizations
|
||||||
|
const organizations = await db.select().from(organization);
|
||||||
|
|
||||||
|
// Create default roles for each organization
|
||||||
|
for (const org of organizations) {
|
||||||
|
const rolesSQL = getDefaultRolesSQL(org.id);
|
||||||
|
await db.execute(sql.raw(rolesSQL));
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`Created default roles for organization: ${org.name} (${org.id})`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("Successfully created default roles for all organizations");
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error creating default roles:", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,7 +9,8 @@ import {
|
|||||||
import { nanoid } from "nanoid";
|
import { nanoid } from "nanoid";
|
||||||
import { projects } from "./project";
|
import { projects } from "./project";
|
||||||
import { server } from "./server";
|
import { server } from "./server";
|
||||||
import { users_temp } from "./user";
|
import { users } from "./user";
|
||||||
|
// import { role } from "./rbac";
|
||||||
|
|
||||||
export const account = pgTable("account", {
|
export const account = pgTable("account", {
|
||||||
id: text("id")
|
id: text("id")
|
||||||
@@ -21,7 +22,7 @@ export const account = pgTable("account", {
|
|||||||
providerId: text("provider_id").notNull(),
|
providerId: text("provider_id").notNull(),
|
||||||
userId: text("user_id")
|
userId: text("user_id")
|
||||||
.notNull()
|
.notNull()
|
||||||
.references(() => users_temp.id, { onDelete: "cascade" }),
|
.references(() => users.id, { onDelete: "cascade" }),
|
||||||
accessToken: text("access_token"),
|
accessToken: text("access_token"),
|
||||||
refreshToken: text("refresh_token"),
|
refreshToken: text("refresh_token"),
|
||||||
idToken: text("id_token"),
|
idToken: text("id_token"),
|
||||||
@@ -39,9 +40,9 @@ export const account = pgTable("account", {
|
|||||||
});
|
});
|
||||||
|
|
||||||
export const accountRelations = relations(account, ({ one }) => ({
|
export const accountRelations = relations(account, ({ one }) => ({
|
||||||
user: one(users_temp, {
|
user: one(users, {
|
||||||
fields: [account.userId],
|
fields: [account.userId],
|
||||||
references: [users_temp.id],
|
references: [users.id],
|
||||||
}),
|
}),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@@ -65,15 +66,15 @@ export const organization = pgTable("organization", {
|
|||||||
metadata: text("metadata"),
|
metadata: text("metadata"),
|
||||||
ownerId: text("owner_id")
|
ownerId: text("owner_id")
|
||||||
.notNull()
|
.notNull()
|
||||||
.references(() => users_temp.id, { onDelete: "cascade" }),
|
.references(() => users.id, { onDelete: "cascade" }),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const organizationRelations = relations(
|
export const organizationRelations = relations(
|
||||||
organization,
|
organization,
|
||||||
({ one, many }) => ({
|
({ one, many }) => ({
|
||||||
owner: one(users_temp, {
|
owner: one(users, {
|
||||||
fields: [organization.ownerId],
|
fields: [organization.ownerId],
|
||||||
references: [users_temp.id],
|
references: [users.id],
|
||||||
}),
|
}),
|
||||||
servers: many(server),
|
servers: many(server),
|
||||||
projects: many(projects),
|
projects: many(projects),
|
||||||
@@ -90,24 +91,12 @@ export const member = pgTable("member", {
|
|||||||
.references(() => organization.id, { onDelete: "cascade" }),
|
.references(() => organization.id, { onDelete: "cascade" }),
|
||||||
userId: text("user_id")
|
userId: text("user_id")
|
||||||
.notNull()
|
.notNull()
|
||||||
.references(() => users_temp.id, { onDelete: "cascade" }),
|
.references(() => users.id, { onDelete: "cascade" }),
|
||||||
role: text("role").notNull().$type<"owner" | "member" | "admin">(),
|
role: text("role").$type<"owner" | "member" | "admin">(),
|
||||||
|
// roleId: text("roleId").references(() => role.roleId, { onDelete: "cascade" }),
|
||||||
createdAt: timestamp("created_at").notNull(),
|
createdAt: timestamp("created_at").notNull(),
|
||||||
teamId: text("team_id"),
|
teamId: text("team_id"),
|
||||||
// Permissions
|
// Permissions
|
||||||
canCreateProjects: boolean("canCreateProjects").notNull().default(false),
|
|
||||||
canAccessToSSHKeys: boolean("canAccessToSSHKeys").notNull().default(false),
|
|
||||||
canCreateServices: boolean("canCreateServices").notNull().default(false),
|
|
||||||
canDeleteProjects: boolean("canDeleteProjects").notNull().default(false),
|
|
||||||
canDeleteServices: boolean("canDeleteServices").notNull().default(false),
|
|
||||||
canAccessToDocker: boolean("canAccessToDocker").notNull().default(false),
|
|
||||||
canAccessToAPI: boolean("canAccessToAPI").notNull().default(false),
|
|
||||||
canAccessToGitProviders: boolean("canAccessToGitProviders")
|
|
||||||
.notNull()
|
|
||||||
.default(false),
|
|
||||||
canAccessToTraefikFiles: boolean("canAccessToTraefikFiles")
|
|
||||||
.notNull()
|
|
||||||
.default(false),
|
|
||||||
accessedProjects: text("accesedProjects")
|
accessedProjects: text("accesedProjects")
|
||||||
.array()
|
.array()
|
||||||
.notNull()
|
.notNull()
|
||||||
@@ -123,24 +112,30 @@ export const memberRelations = relations(member, ({ one }) => ({
|
|||||||
fields: [member.organizationId],
|
fields: [member.organizationId],
|
||||||
references: [organization.id],
|
references: [organization.id],
|
||||||
}),
|
}),
|
||||||
user: one(users_temp, {
|
user: one(users, {
|
||||||
fields: [member.userId],
|
fields: [member.userId],
|
||||||
references: [users_temp.id],
|
references: [users.id],
|
||||||
}),
|
}),
|
||||||
|
// role: one(role, {
|
||||||
|
// fields: [member.roleId],
|
||||||
|
// references: [role.roleId],
|
||||||
|
// }),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
export const invitation = pgTable("invitation", {
|
export const invitation = pgTable("invitation", {
|
||||||
id: text("id").primaryKey(),
|
id: text("id")
|
||||||
|
.primaryKey()
|
||||||
|
.$defaultFn(() => nanoid()),
|
||||||
organizationId: text("organization_id")
|
organizationId: text("organization_id")
|
||||||
.notNull()
|
.notNull()
|
||||||
.references(() => organization.id, { onDelete: "cascade" }),
|
.references(() => organization.id, { onDelete: "cascade" }),
|
||||||
email: text("email").notNull(),
|
email: text("email").notNull(),
|
||||||
role: text("role").$type<"owner" | "member" | "admin">(),
|
role: text("role"),
|
||||||
status: text("status").notNull(),
|
status: text("status").notNull(),
|
||||||
expiresAt: timestamp("expires_at").notNull(),
|
expiresAt: timestamp("expires_at").notNull(),
|
||||||
inviterId: text("inviter_id")
|
inviterId: text("inviter_id")
|
||||||
.notNull()
|
.notNull()
|
||||||
.references(() => users_temp.id, { onDelete: "cascade" }),
|
.references(() => users.id, { onDelete: "cascade" }),
|
||||||
teamId: text("team_id"),
|
teamId: text("team_id"),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -157,7 +152,7 @@ export const twoFactor = pgTable("two_factor", {
|
|||||||
backupCodes: text("backup_codes").notNull(),
|
backupCodes: text("backup_codes").notNull(),
|
||||||
userId: text("user_id")
|
userId: text("user_id")
|
||||||
.notNull()
|
.notNull()
|
||||||
.references(() => users_temp.id, { onDelete: "cascade" }),
|
.references(() => users.id, { onDelete: "cascade" }),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const apikey = pgTable("apikey", {
|
export const apikey = pgTable("apikey", {
|
||||||
@@ -168,7 +163,7 @@ export const apikey = pgTable("apikey", {
|
|||||||
key: text("key").notNull(),
|
key: text("key").notNull(),
|
||||||
userId: text("user_id")
|
userId: text("user_id")
|
||||||
.notNull()
|
.notNull()
|
||||||
.references(() => users_temp.id, { onDelete: "cascade" }),
|
.references(() => users.id, { onDelete: "cascade" }),
|
||||||
refillInterval: integer("refill_interval"),
|
refillInterval: integer("refill_interval"),
|
||||||
refillAmount: integer("refill_amount"),
|
refillAmount: integer("refill_amount"),
|
||||||
lastRefillAt: timestamp("last_refill_at"),
|
lastRefillAt: timestamp("last_refill_at"),
|
||||||
@@ -187,8 +182,8 @@ export const apikey = pgTable("apikey", {
|
|||||||
});
|
});
|
||||||
|
|
||||||
export const apikeyRelations = relations(apikey, ({ one }) => ({
|
export const apikeyRelations = relations(apikey, ({ one }) => ({
|
||||||
user: one(users_temp, {
|
user: one(users, {
|
||||||
fields: [apikey.userId],
|
fields: [apikey.userId],
|
||||||
references: [users_temp.id],
|
references: [users.id],
|
||||||
}),
|
}),
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import { mariadb } from "./mariadb";
|
|||||||
import { mongo } from "./mongo";
|
import { mongo } from "./mongo";
|
||||||
import { mysql } from "./mysql";
|
import { mysql } from "./mysql";
|
||||||
import { postgres } from "./postgres";
|
import { postgres } from "./postgres";
|
||||||
import { users_temp } from "./user";
|
import { users } from "./user";
|
||||||
export const databaseType = pgEnum("databaseType", [
|
export const databaseType = pgEnum("databaseType", [
|
||||||
"postgres",
|
"postgres",
|
||||||
"mariadb",
|
"mariadb",
|
||||||
@@ -74,7 +74,7 @@ export const backups = pgTable("backup", {
|
|||||||
mongoId: text("mongoId").references((): AnyPgColumn => mongo.mongoId, {
|
mongoId: text("mongoId").references((): AnyPgColumn => mongo.mongoId, {
|
||||||
onDelete: "cascade",
|
onDelete: "cascade",
|
||||||
}),
|
}),
|
||||||
userId: text("userId").references(() => users_temp.id),
|
userId: text("userId").references(() => users.id),
|
||||||
// Only for compose backups
|
// Only for compose backups
|
||||||
metadata: jsonb("metadata").$type<
|
metadata: jsonb("metadata").$type<
|
||||||
| {
|
| {
|
||||||
@@ -118,9 +118,9 @@ export const backupsRelations = relations(backups, ({ one, many }) => ({
|
|||||||
fields: [backups.mongoId],
|
fields: [backups.mongoId],
|
||||||
references: [mongo.mongoId],
|
references: [mongo.mongoId],
|
||||||
}),
|
}),
|
||||||
user: one(users_temp, {
|
user: one(users, {
|
||||||
fields: [backups.userId],
|
fields: [backups.userId],
|
||||||
references: [users_temp.id],
|
references: [users.id],
|
||||||
}),
|
}),
|
||||||
compose: one(compose, {
|
compose: one(compose, {
|
||||||
fields: [backups.composeId],
|
fields: [backups.composeId],
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { bitbucket } from "./bitbucket";
|
|||||||
import { gitea } from "./gitea";
|
import { gitea } from "./gitea";
|
||||||
import { github } from "./github";
|
import { github } from "./github";
|
||||||
import { gitlab } from "./gitlab";
|
import { gitlab } from "./gitlab";
|
||||||
import { users_temp } from "./user";
|
import { users } from "./user";
|
||||||
|
|
||||||
export const gitProviderType = pgEnum("gitProviderType", [
|
export const gitProviderType = pgEnum("gitProviderType", [
|
||||||
"github",
|
"github",
|
||||||
@@ -32,7 +32,7 @@ export const gitProvider = pgTable("git_provider", {
|
|||||||
.references(() => organization.id, { onDelete: "cascade" }),
|
.references(() => organization.id, { onDelete: "cascade" }),
|
||||||
userId: text("userId")
|
userId: text("userId")
|
||||||
.notNull()
|
.notNull()
|
||||||
.references(() => users_temp.id, { onDelete: "cascade" }),
|
.references(() => users.id, { onDelete: "cascade" }),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const gitProviderRelations = relations(gitProvider, ({ one }) => ({
|
export const gitProviderRelations = relations(gitProvider, ({ one }) => ({
|
||||||
@@ -56,9 +56,9 @@ export const gitProviderRelations = relations(gitProvider, ({ one }) => ({
|
|||||||
fields: [gitProvider.organizationId],
|
fields: [gitProvider.organizationId],
|
||||||
references: [organization.id],
|
references: [organization.id],
|
||||||
}),
|
}),
|
||||||
user: one(users_temp, {
|
user: one(users, {
|
||||||
fields: [gitProvider.userId],
|
fields: [gitProvider.userId],
|
||||||
references: [users_temp.id],
|
references: [users.id],
|
||||||
}),
|
}),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|||||||
@@ -30,7 +30,9 @@ export * from "./server";
|
|||||||
export * from "./utils";
|
export * from "./utils";
|
||||||
export * from "./preview-deployments";
|
export * from "./preview-deployments";
|
||||||
export * from "./ai";
|
export * from "./ai";
|
||||||
|
// export * from "./rbac";
|
||||||
export * from "./account";
|
export * from "./account";
|
||||||
export * from "./schedule";
|
export * from "./schedule";
|
||||||
export * from "./rollbacks";
|
export * from "./rollbacks";
|
||||||
export * from "./volume-backups";
|
export * from "./volume-backups";
|
||||||
|
export * from "./web-server";
|
||||||
|
|||||||
58
packages/server/src/db/schema/rbac.ts
Normal file
58
packages/server/src/db/schema/rbac.ts
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
// import { relations } from "drizzle-orm";
|
||||||
|
// import { pgTable, text, timestamp, boolean, unique } from "drizzle-orm/pg-core";
|
||||||
|
// import { nanoid } from "nanoid";
|
||||||
|
// import { organization, member } from "./account";
|
||||||
|
// import { createInsertSchema } from "drizzle-zod";
|
||||||
|
// import { z } from "zod";
|
||||||
|
|
||||||
|
// export const role = pgTable(
|
||||||
|
// "member_role",
|
||||||
|
// {
|
||||||
|
// roleId: text("roleId")
|
||||||
|
// .primaryKey()
|
||||||
|
// .$defaultFn(() => nanoid()),
|
||||||
|
// name: text("name").notNull().unique(),
|
||||||
|
// description: text("description"),
|
||||||
|
// canDelete: boolean("canDelete").notNull().default(true),
|
||||||
|
// isSystem: boolean("is_system").default(false),
|
||||||
|
// permissions: text("permissions").array(),
|
||||||
|
// createdAt: timestamp("created_at").notNull().defaultNow(),
|
||||||
|
// updatedAt: timestamp("updated_at").notNull().defaultNow(),
|
||||||
|
// organizationId: text("organizationId")
|
||||||
|
// .notNull()
|
||||||
|
// .references(() => organization.id, { onDelete: "cascade" }),
|
||||||
|
// },
|
||||||
|
// (table) => ({
|
||||||
|
// roleName: unique("role_name_unique").on(table.name, table.organizationId),
|
||||||
|
// }),
|
||||||
|
// );
|
||||||
|
|
||||||
|
// export const roleRelations = relations(role, ({ one, many }) => ({
|
||||||
|
// organization: one(organization, {
|
||||||
|
// fields: [role.organizationId],
|
||||||
|
// references: [organization.id],
|
||||||
|
// }),
|
||||||
|
// members: many(member),
|
||||||
|
// }));
|
||||||
|
|
||||||
|
// export type Role = typeof role.$inferSelect;
|
||||||
|
|
||||||
|
// export const createRoleSchema = createInsertSchema(role)
|
||||||
|
// .omit({
|
||||||
|
// roleId: true,
|
||||||
|
// createdAt: true,
|
||||||
|
// updatedAt: true,
|
||||||
|
// isSystem: true,
|
||||||
|
// organizationId: true,
|
||||||
|
// })
|
||||||
|
// .extend({
|
||||||
|
// permissions: z.array(z.string()),
|
||||||
|
// });
|
||||||
|
|
||||||
|
// export const updateRoleSchema = createRoleSchema.extend({
|
||||||
|
// roleId: z.string().min(1),
|
||||||
|
// });
|
||||||
|
|
||||||
|
// export const apiFindOneRole = z.object({
|
||||||
|
// roleId: z.string().min(1),
|
||||||
|
// });
|
||||||
@@ -7,7 +7,7 @@ import { applications } from "./application";
|
|||||||
import { compose } from "./compose";
|
import { compose } from "./compose";
|
||||||
import { deployments } from "./deployment";
|
import { deployments } from "./deployment";
|
||||||
import { server } from "./server";
|
import { server } from "./server";
|
||||||
import { users_temp } from "./user";
|
import { users } from "./user";
|
||||||
import { generateAppName } from "./utils";
|
import { generateAppName } from "./utils";
|
||||||
export const shellTypes = pgEnum("shellType", ["bash", "sh"]);
|
export const shellTypes = pgEnum("shellType", ["bash", "sh"]);
|
||||||
|
|
||||||
@@ -45,7 +45,7 @@ export const schedules = pgTable("schedule", {
|
|||||||
serverId: text("serverId").references(() => server.serverId, {
|
serverId: text("serverId").references(() => server.serverId, {
|
||||||
onDelete: "cascade",
|
onDelete: "cascade",
|
||||||
}),
|
}),
|
||||||
userId: text("userId").references(() => users_temp.id, {
|
userId: text("userId").references(() => users.id, {
|
||||||
onDelete: "cascade",
|
onDelete: "cascade",
|
||||||
}),
|
}),
|
||||||
enabled: boolean("enabled").notNull().default(true),
|
enabled: boolean("enabled").notNull().default(true),
|
||||||
@@ -69,9 +69,9 @@ export const schedulesRelations = relations(schedules, ({ one, many }) => ({
|
|||||||
fields: [schedules.serverId],
|
fields: [schedules.serverId],
|
||||||
references: [server.serverId],
|
references: [server.serverId],
|
||||||
}),
|
}),
|
||||||
user: one(users_temp, {
|
user: one(users, {
|
||||||
fields: [schedules.userId],
|
fields: [schedules.userId],
|
||||||
references: [users_temp.id],
|
references: [users.id],
|
||||||
}),
|
}),
|
||||||
deployments: many(deployments),
|
deployments: many(deployments),
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { pgTable, text, timestamp } from "drizzle-orm/pg-core";
|
import { pgTable, text, timestamp } from "drizzle-orm/pg-core";
|
||||||
import { users_temp } from "./user";
|
import { users } from "./user";
|
||||||
|
|
||||||
// OLD TABLE
|
// OLD TABLE
|
||||||
export const session = pgTable("session_temp", {
|
export const session = pgTable("session_temp", {
|
||||||
@@ -12,7 +12,7 @@ export const session = pgTable("session_temp", {
|
|||||||
userAgent: text("user_agent"),
|
userAgent: text("user_agent"),
|
||||||
userId: text("user_id")
|
userId: text("user_id")
|
||||||
.notNull()
|
.notNull()
|
||||||
.references(() => users_temp.id, { onDelete: "cascade" }),
|
.references(() => users.id, { onDelete: "cascade" }),
|
||||||
impersonatedBy: text("impersonated_by"),
|
impersonatedBy: text("impersonated_by"),
|
||||||
activeOrganizationId: text("active_organization_id"),
|
activeOrganizationId: text("active_organization_id"),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import { relations } from "drizzle-orm";
|
|||||||
import {
|
import {
|
||||||
boolean,
|
boolean,
|
||||||
integer,
|
integer,
|
||||||
jsonb,
|
|
||||||
pgTable,
|
pgTable,
|
||||||
text,
|
text,
|
||||||
timestamp,
|
timestamp,
|
||||||
@@ -14,7 +13,6 @@ import { account, apikey, organization } from "./account";
|
|||||||
import { backups } from "./backups";
|
import { backups } from "./backups";
|
||||||
import { projects } from "./project";
|
import { projects } from "./project";
|
||||||
import { schedules } from "./schedule";
|
import { schedules } from "./schedule";
|
||||||
import { certificateType } from "./shared";
|
|
||||||
import { paths } from "@dokploy/server/constants";
|
import { paths } from "@dokploy/server/constants";
|
||||||
/**
|
/**
|
||||||
* This is an example of how to use the multi-project schema feature of Drizzle ORM. Use the same
|
* This is an example of how to use the multi-project schema feature of Drizzle ORM. Use the same
|
||||||
@@ -23,10 +21,8 @@ import { paths } from "@dokploy/server/constants";
|
|||||||
* @see https://orm.drizzle.team/docs/goodies#multi-project-schema
|
* @see https://orm.drizzle.team/docs/goodies#multi-project-schema
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// OLD TABLE
|
|
||||||
|
|
||||||
// TEMP
|
// TEMP
|
||||||
export const users_temp = pgTable("user_temp", {
|
export const users = pgTable("users", {
|
||||||
id: text("id")
|
id: text("id")
|
||||||
.notNull()
|
.notNull()
|
||||||
.primaryKey()
|
.primaryKey()
|
||||||
@@ -36,10 +32,7 @@ export const users_temp = pgTable("user_temp", {
|
|||||||
expirationDate: text("expirationDate")
|
expirationDate: text("expirationDate")
|
||||||
.notNull()
|
.notNull()
|
||||||
.$defaultFn(() => new Date().toISOString()),
|
.$defaultFn(() => new Date().toISOString()),
|
||||||
createdAt2: text("createdAt")
|
createdAt: timestamp("created_at").notNull().defaultNow(),
|
||||||
.notNull()
|
|
||||||
.$defaultFn(() => new Date().toISOString()),
|
|
||||||
createdAt: timestamp("created_at").defaultNow(),
|
|
||||||
// Auth
|
// Auth
|
||||||
twoFactorEnabled: boolean("two_factor_enabled"),
|
twoFactorEnabled: boolean("two_factor_enabled"),
|
||||||
email: text("email").notNull().unique(),
|
email: text("email").notNull().unique(),
|
||||||
@@ -48,83 +41,19 @@ export const users_temp = pgTable("user_temp", {
|
|||||||
banned: boolean("banned"),
|
banned: boolean("banned"),
|
||||||
banReason: text("ban_reason"),
|
banReason: text("ban_reason"),
|
||||||
banExpires: timestamp("ban_expires"),
|
banExpires: timestamp("ban_expires"),
|
||||||
updatedAt: timestamp("updated_at").notNull(),
|
updatedAt: timestamp("updated_at").notNull().defaultNow(),
|
||||||
// Admin
|
|
||||||
serverIp: text("serverIp"),
|
|
||||||
certificateType: certificateType("certificateType").notNull().default("none"),
|
|
||||||
https: boolean("https").notNull().default(false),
|
|
||||||
host: text("host"),
|
|
||||||
letsEncryptEmail: text("letsEncryptEmail"),
|
|
||||||
sshPrivateKey: text("sshPrivateKey"),
|
|
||||||
enableDockerCleanup: boolean("enableDockerCleanup").notNull().default(false),
|
|
||||||
logCleanupCron: text("logCleanupCron").default("0 0 * * *"),
|
|
||||||
role: text("role").notNull().default("user"),
|
role: text("role").notNull().default("user"),
|
||||||
// Metrics
|
// Metrics
|
||||||
enablePaidFeatures: boolean("enablePaidFeatures").notNull().default(false),
|
enablePaidFeatures: boolean("enablePaidFeatures").notNull().default(false),
|
||||||
allowImpersonation: boolean("allowImpersonation").notNull().default(false),
|
allowImpersonation: boolean("allowImpersonation").notNull().default(false),
|
||||||
metricsConfig: jsonb("metricsConfig")
|
|
||||||
.$type<{
|
|
||||||
server: {
|
|
||||||
type: "Dokploy" | "Remote";
|
|
||||||
refreshRate: number;
|
|
||||||
port: number;
|
|
||||||
token: string;
|
|
||||||
urlCallback: string;
|
|
||||||
retentionDays: number;
|
|
||||||
cronJob: string;
|
|
||||||
thresholds: {
|
|
||||||
cpu: number;
|
|
||||||
memory: number;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
containers: {
|
|
||||||
refreshRate: number;
|
|
||||||
services: {
|
|
||||||
include: string[];
|
|
||||||
exclude: string[];
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}>()
|
|
||||||
.notNull()
|
|
||||||
.default({
|
|
||||||
server: {
|
|
||||||
type: "Dokploy",
|
|
||||||
refreshRate: 60,
|
|
||||||
port: 4500,
|
|
||||||
token: "",
|
|
||||||
retentionDays: 2,
|
|
||||||
cronJob: "",
|
|
||||||
urlCallback: "",
|
|
||||||
thresholds: {
|
|
||||||
cpu: 0,
|
|
||||||
memory: 0,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
containers: {
|
|
||||||
refreshRate: 60,
|
|
||||||
services: {
|
|
||||||
include: [],
|
|
||||||
exclude: [],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
cleanupCacheApplications: boolean("cleanupCacheApplications")
|
|
||||||
.notNull()
|
|
||||||
.default(false),
|
|
||||||
cleanupCacheOnPreviews: boolean("cleanupCacheOnPreviews")
|
|
||||||
.notNull()
|
|
||||||
.default(false),
|
|
||||||
cleanupCacheOnCompose: boolean("cleanupCacheOnCompose")
|
|
||||||
.notNull()
|
|
||||||
.default(false),
|
|
||||||
stripeCustomerId: text("stripeCustomerId"),
|
stripeCustomerId: text("stripeCustomerId"),
|
||||||
stripeSubscriptionId: text("stripeSubscriptionId"),
|
stripeSubscriptionId: text("stripeSubscriptionId"),
|
||||||
serversQuantity: integer("serversQuantity").notNull().default(0),
|
serversQuantity: integer("serversQuantity").notNull().default(0),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const usersRelations = relations(users_temp, ({ one, many }) => ({
|
export const usersRelations = relations(users, ({ one, many }) => ({
|
||||||
account: one(account, {
|
account: one(account, {
|
||||||
fields: [users_temp.id],
|
fields: [users.id],
|
||||||
references: [account.userId],
|
references: [account.userId],
|
||||||
}),
|
}),
|
||||||
organizations: many(organization),
|
organizations: many(organization),
|
||||||
@@ -134,7 +63,7 @@ export const usersRelations = relations(users_temp, ({ one, many }) => ({
|
|||||||
schedules: many(schedules),
|
schedules: many(schedules),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const createSchema = createInsertSchema(users_temp, {
|
const createSchema = createInsertSchema(users, {
|
||||||
id: z.string().min(1),
|
id: z.string().min(1),
|
||||||
isRegistered: z.boolean().optional(),
|
isRegistered: z.boolean().optional(),
|
||||||
}).omit({
|
}).omit({
|
||||||
@@ -199,33 +128,6 @@ export const apiFindOneUserByAuth = createSchema
|
|||||||
// authId: true,
|
// authId: true,
|
||||||
})
|
})
|
||||||
.required();
|
.required();
|
||||||
export const apiSaveSSHKey = createSchema
|
|
||||||
.pick({
|
|
||||||
sshPrivateKey: true,
|
|
||||||
})
|
|
||||||
.required();
|
|
||||||
|
|
||||||
export const apiAssignDomain = createSchema
|
|
||||||
.pick({
|
|
||||||
host: true,
|
|
||||||
certificateType: true,
|
|
||||||
letsEncryptEmail: true,
|
|
||||||
https: true,
|
|
||||||
})
|
|
||||||
.required()
|
|
||||||
.partial({
|
|
||||||
letsEncryptEmail: true,
|
|
||||||
https: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const apiUpdateDockerCleanup = createSchema
|
|
||||||
.pick({
|
|
||||||
enableDockerCleanup: true,
|
|
||||||
})
|
|
||||||
.required()
|
|
||||||
.extend({
|
|
||||||
serverId: z.string().optional(),
|
|
||||||
});
|
|
||||||
|
|
||||||
export const apiTraefikConfig = z.object({
|
export const apiTraefikConfig = z.object({
|
||||||
traefikConfig: z.string().min(1),
|
traefikConfig: z.string().min(1),
|
||||||
|
|||||||
104
packages/server/src/db/schema/web-server.ts
Normal file
104
packages/server/src/db/schema/web-server.ts
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
import { boolean, jsonb, pgTable, text } from "drizzle-orm/pg-core";
|
||||||
|
import { nanoid } from "nanoid";
|
||||||
|
import { certificateType } from "./shared";
|
||||||
|
import { z } from "zod";
|
||||||
|
import { createInsertSchema } from "drizzle-zod";
|
||||||
|
|
||||||
|
export const webServer = pgTable("web_server", {
|
||||||
|
webServerId: text("webServerId")
|
||||||
|
.notNull()
|
||||||
|
.primaryKey()
|
||||||
|
.$defaultFn(() => nanoid()),
|
||||||
|
// Admin
|
||||||
|
serverIp: text("serverIp"),
|
||||||
|
certificateType: certificateType("certificateType").notNull().default("none"),
|
||||||
|
https: boolean("https").notNull().default(false),
|
||||||
|
host: text("host"),
|
||||||
|
letsEncryptEmail: text("letsEncryptEmail"),
|
||||||
|
sshPrivateKey: text("sshPrivateKey"),
|
||||||
|
enableDockerCleanup: boolean("enableDockerCleanup").notNull().default(false),
|
||||||
|
logCleanupCron: text("logCleanupCron").default("0 0 * * *"),
|
||||||
|
metricsConfig: jsonb("metricsConfig")
|
||||||
|
.$type<{
|
||||||
|
server: {
|
||||||
|
type: "Dokploy" | "Remote";
|
||||||
|
refreshRate: number;
|
||||||
|
port: number;
|
||||||
|
token: string;
|
||||||
|
urlCallback: string;
|
||||||
|
retentionDays: number;
|
||||||
|
cronJob: string;
|
||||||
|
thresholds: {
|
||||||
|
cpu: number;
|
||||||
|
memory: number;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
containers: {
|
||||||
|
refreshRate: number;
|
||||||
|
services: {
|
||||||
|
include: string[];
|
||||||
|
exclude: string[];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}>()
|
||||||
|
.notNull()
|
||||||
|
.default({
|
||||||
|
server: {
|
||||||
|
type: "Dokploy",
|
||||||
|
refreshRate: 60,
|
||||||
|
port: 4500,
|
||||||
|
token: "",
|
||||||
|
retentionDays: 2,
|
||||||
|
cronJob: "",
|
||||||
|
urlCallback: "",
|
||||||
|
thresholds: {
|
||||||
|
cpu: 0,
|
||||||
|
memory: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
containers: {
|
||||||
|
refreshRate: 60,
|
||||||
|
services: {
|
||||||
|
include: [],
|
||||||
|
exclude: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type WebServer = typeof webServer.$inferSelect;
|
||||||
|
|
||||||
|
const createSchema = createInsertSchema(webServer);
|
||||||
|
|
||||||
|
export const updateWebServerSchema = createSchema.omit({
|
||||||
|
webServerId: true,
|
||||||
|
metricsConfig: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const apiSaveSSHKey = createSchema
|
||||||
|
.pick({
|
||||||
|
sshPrivateKey: true,
|
||||||
|
})
|
||||||
|
.required();
|
||||||
|
|
||||||
|
export const apiAssignDomain = createSchema
|
||||||
|
.pick({
|
||||||
|
host: true,
|
||||||
|
certificateType: true,
|
||||||
|
letsEncryptEmail: true,
|
||||||
|
https: true,
|
||||||
|
})
|
||||||
|
.required()
|
||||||
|
.partial({
|
||||||
|
letsEncryptEmail: true,
|
||||||
|
https: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const apiUpdateDockerCleanup = createSchema
|
||||||
|
.pick({
|
||||||
|
enableDockerCleanup: true,
|
||||||
|
})
|
||||||
|
.required()
|
||||||
|
.extend({
|
||||||
|
serverId: z.string().optional(),
|
||||||
|
});
|
||||||
13
packages/server/src/db/scripts/create-default-roles.ts
Normal file
13
packages/server/src/db/scripts/create-default-roles.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import { createDefaultRoles } from "../migrations/create-default-roles";
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
try {
|
||||||
|
await createDefaultRoles();
|
||||||
|
process.exit(0);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to create default roles:", error);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
main();
|
||||||
@@ -13,6 +13,7 @@ export * from "./services/settings";
|
|||||||
export * from "./services/volume-backups";
|
export * from "./services/volume-backups";
|
||||||
export * from "./services/docker";
|
export * from "./services/docker";
|
||||||
export * from "./services/destination";
|
export * from "./services/destination";
|
||||||
|
export * from "./services/role";
|
||||||
export * from "./services/deployment";
|
export * from "./services/deployment";
|
||||||
export * from "./services/mount";
|
export * from "./services/mount";
|
||||||
export * from "./services/certificate";
|
export * from "./services/certificate";
|
||||||
@@ -34,6 +35,7 @@ export * from "./services/server";
|
|||||||
export * from "./services/schedule";
|
export * from "./services/schedule";
|
||||||
export * from "./services/application";
|
export * from "./services/application";
|
||||||
export * from "./services/rollbacks";
|
export * from "./services/rollbacks";
|
||||||
|
export * from "./services/web-server";
|
||||||
export * from "./utils/databases/rebuild";
|
export * from "./utils/databases/rebuild";
|
||||||
export * from "./setup/config-paths";
|
export * from "./setup/config-paths";
|
||||||
export * from "./setup/postgres-setup";
|
export * from "./setup/postgres-setup";
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user