mirror of
https://github.com/Dokploy/dokploy.git
synced 2026-06-16 12:45:21 +02:00
Compare commits
6 Commits
feat/add-a
...
1365-creat
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
80d5313dd8 | ||
|
|
3f3ff9670b | ||
|
|
7fb902551e | ||
|
|
a201b3f979 | ||
|
|
01d78e50fc | ||
|
|
da0e726326 |
@@ -5,8 +5,7 @@ vi.mock("node:fs", () => ({
|
||||
default: fs,
|
||||
}));
|
||||
|
||||
import type { FileConfig } from "@dokploy/server";
|
||||
import type { WebServer } from "@dokploy/server/db/schema";
|
||||
import type { FileConfig, User } from "@dokploy/server";
|
||||
import {
|
||||
createDefaultServerTraefikConfig,
|
||||
loadOrCreateConfig,
|
||||
@@ -14,8 +13,11 @@ import {
|
||||
} from "@dokploy/server";
|
||||
import { beforeEach, expect, test, vi } from "vitest";
|
||||
|
||||
const baseAdmin: WebServer = {
|
||||
const baseAdmin: User = {
|
||||
https: false,
|
||||
enablePaidFeatures: false,
|
||||
allowImpersonation: false,
|
||||
role: "user",
|
||||
metricsConfig: {
|
||||
containers: {
|
||||
refreshRate: 20,
|
||||
@@ -38,6 +40,10 @@ const baseAdmin: WebServer = {
|
||||
urlCallback: "",
|
||||
},
|
||||
},
|
||||
cleanupCacheApplications: false,
|
||||
cleanupCacheOnCompose: false,
|
||||
cleanupCacheOnPreviews: false,
|
||||
createdAt: new Date(),
|
||||
serverIp: null,
|
||||
certificateType: "none",
|
||||
host: null,
|
||||
@@ -45,7 +51,22 @@ const baseAdmin: WebServer = {
|
||||
sshPrivateKey: null,
|
||||
enableDockerCleanup: false,
|
||||
logCleanupCron: null,
|
||||
webServerId: "1",
|
||||
serversQuantity: 0,
|
||||
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(() => {
|
||||
@@ -64,6 +85,8 @@ test("Should apply redirect-to-https", () => {
|
||||
updateServerTraefik(
|
||||
{
|
||||
...baseAdmin,
|
||||
https: true,
|
||||
certificateType: "letsencrypt",
|
||||
},
|
||||
"example.com",
|
||||
);
|
||||
|
||||
@@ -48,7 +48,7 @@ export const ShowTraefikConfig = ({ applicationId }: Props) => {
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col pt-2 relative">
|
||||
<div className="flex flex-col gap-6 max-h-[35rem] min-h-[10rem]">
|
||||
<div className="flex flex-col gap-6 max-h-[35rem] min-h-[10rem] overflow-y-auto">
|
||||
<CodeEditor
|
||||
lineWrapping
|
||||
value={data || "Empty"}
|
||||
|
||||
@@ -158,7 +158,7 @@ export const ShowDeployment = ({
|
||||
<div
|
||||
ref={scrollRef}
|
||||
onScroll={handleScroll}
|
||||
className="h-[720px] space-y-0 border p-4 bg-[#fafafa] dark:bg-[#050506] rounded custom-logs-scrollbar"
|
||||
className="h-[720px] overflow-y-auto space-y-0 border p-4 bg-[#fafafa] dark:bg-[#050506] rounded custom-logs-scrollbar"
|
||||
>
|
||||
{" "}
|
||||
{filteredLogs.length > 0 ? (
|
||||
|
||||
@@ -71,7 +71,7 @@ export const ShowDomains = ({ id, type }: Props) => {
|
||||
const [validationStates, setValidationStates] = useState<ValidationStates>(
|
||||
{},
|
||||
);
|
||||
const { data: webServer } = api.webServer.get.useQuery();
|
||||
const { data: ip } = api.settings.getIp.useQuery();
|
||||
|
||||
const {
|
||||
data,
|
||||
@@ -110,9 +110,7 @@ export const ShowDomains = ({ id, type }: Props) => {
|
||||
const result = await validateDomain({
|
||||
domain: host,
|
||||
serverIp:
|
||||
application?.server?.ipAddress?.toString() ||
|
||||
webServer?.serverIp?.toString() ||
|
||||
"",
|
||||
application?.server?.ipAddress?.toString() || ip?.toString() || "",
|
||||
});
|
||||
|
||||
setValidationStates((prev) => ({
|
||||
@@ -212,7 +210,7 @@ export const ShowDomains = ({ id, type }: Props) => {
|
||||
}}
|
||||
serverIp={
|
||||
application?.server?.ipAddress?.toString() ||
|
||||
webServer?.serverIp?.toString()
|
||||
ip?.toString()
|
||||
}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -42,7 +42,7 @@ export const ShowContainerConfig = ({ containerId, serverId }: Props) => {
|
||||
See in detail the config of this container
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="text-wrap rounded-lg border p-4 text-sm bg-card max-h-[80vh]">
|
||||
<div className="text-wrap rounded-lg border p-4 overflow-y-auto text-sm bg-card max-h-[80vh]">
|
||||
<code>
|
||||
<pre className="whitespace-pre-wrap break-words">
|
||||
<CodeEditor
|
||||
|
||||
@@ -274,7 +274,7 @@ export const DockerLogsId: React.FC<Props> = ({
|
||||
<div
|
||||
ref={scrollRef}
|
||||
onScroll={handleScroll}
|
||||
className="h-[720px] space-y-0 border p-4 bg-[#fafafa] dark:bg-[#050506] rounded custom-logs-scrollbar"
|
||||
className="h-[720px] overflow-y-auto space-y-0 border p-4 bg-[#fafafa] dark:bg-[#050506] rounded custom-logs-scrollbar"
|
||||
>
|
||||
{filteredLogs.length > 0 ? (
|
||||
filteredLogs.map((filteredLog: LogLine, index: number) => (
|
||||
|
||||
@@ -138,7 +138,7 @@ export function LineCountFilter({
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<CommandPrimitive.List className="max-h-[300px] overflow-x-hidden">
|
||||
<CommandPrimitive.List className="max-h-[300px] overflow-y-auto overflow-x-hidden">
|
||||
<CommandPrimitive.Group className="px-2 py-1.5">
|
||||
{lineCountOptions.map((option) => {
|
||||
const isSelected = value === option.value;
|
||||
|
||||
@@ -46,11 +46,11 @@ interface Props {
|
||||
mariadbId: string;
|
||||
}
|
||||
export const ShowExternalMariadbCredentials = ({ mariadbId }: Props) => {
|
||||
const { data: webServer } = api.webServer.get.useQuery();
|
||||
const { data: ip } = api.settings.getIp.useQuery();
|
||||
const { data, refetch } = api.mariadb.one.useQuery({ mariadbId });
|
||||
const { mutateAsync, isLoading } = api.mariadb.saveExternalPort.useMutation();
|
||||
const [connectionUrl, setConnectionUrl] = useState("");
|
||||
const getIp = data?.server?.ipAddress || webServer?.serverIp;
|
||||
const getIp = data?.server?.ipAddress || ip;
|
||||
const form = useForm<DockerProvider>({
|
||||
defaultValues: {},
|
||||
resolver: zodResolver(DockerProviderSchema),
|
||||
|
||||
@@ -46,11 +46,11 @@ interface Props {
|
||||
mongoId: string;
|
||||
}
|
||||
export const ShowExternalMongoCredentials = ({ mongoId }: Props) => {
|
||||
const { data: webServer } = api.webServer.get.useQuery();
|
||||
const { data: ip } = api.settings.getIp.useQuery();
|
||||
const { data, refetch } = api.mongo.one.useQuery({ mongoId });
|
||||
const { mutateAsync, isLoading } = api.mongo.saveExternalPort.useMutation();
|
||||
const [connectionUrl, setConnectionUrl] = useState("");
|
||||
const getIp = data?.server?.ipAddress || webServer?.serverIp;
|
||||
const getIp = data?.server?.ipAddress || ip;
|
||||
const form = useForm<DockerProvider>({
|
||||
defaultValues: {},
|
||||
resolver: zodResolver(DockerProviderSchema),
|
||||
|
||||
@@ -46,11 +46,11 @@ interface Props {
|
||||
mysqlId: string;
|
||||
}
|
||||
export const ShowExternalMysqlCredentials = ({ mysqlId }: Props) => {
|
||||
const { data: webServer } = api.webServer.get.useQuery();
|
||||
const { data: ip } = api.settings.getIp.useQuery();
|
||||
const { data, refetch } = api.mysql.one.useQuery({ mysqlId });
|
||||
const { mutateAsync, isLoading } = api.mysql.saveExternalPort.useMutation();
|
||||
const [connectionUrl, setConnectionUrl] = useState("");
|
||||
const getIp = data?.server?.ipAddress || webServer?.serverIp;
|
||||
const getIp = data?.server?.ipAddress || ip;
|
||||
const form = useForm<DockerProvider>({
|
||||
defaultValues: {},
|
||||
resolver: zodResolver(DockerProviderSchema),
|
||||
|
||||
@@ -46,11 +46,11 @@ interface Props {
|
||||
postgresId: string;
|
||||
}
|
||||
export const ShowExternalPostgresCredentials = ({ postgresId }: Props) => {
|
||||
const { data: webServer } = api.webServer.get.useQuery();
|
||||
const { data: ip } = api.settings.getIp.useQuery();
|
||||
const { data, refetch } = api.postgres.one.useQuery({ postgresId });
|
||||
const { mutateAsync, isLoading } =
|
||||
api.postgres.saveExternalPort.useMutation();
|
||||
const getIp = data?.server?.ipAddress || webServer?.serverIp;
|
||||
const getIp = data?.server?.ipAddress || ip;
|
||||
const [connectionUrl, setConnectionUrl] = useState("");
|
||||
|
||||
const form = useForm<DockerProvider>({
|
||||
|
||||
@@ -167,7 +167,7 @@ export const DuplicateProject = ({
|
||||
|
||||
<div className="grid gap-2">
|
||||
<Label>Selected services to duplicate</Label>
|
||||
<div className="space-y-2 max-h-[200px] border rounded-md p-4">
|
||||
<div className="space-y-2 max-h-[200px] overflow-y-auto border rounded-md p-4">
|
||||
{selectedServices.map((service) => (
|
||||
<div key={service.id} className="flex items-center space-x-2">
|
||||
<span className="text-sm">
|
||||
|
||||
@@ -47,12 +47,11 @@ import { useMemo, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { HandleProject } from "./handle-project";
|
||||
import { ProjectEnvironment } from "./project-environment";
|
||||
import { PERMISSIONS } from "@dokploy/server/lib/permissions";
|
||||
import { Permissions } from "../shared/Permissions";
|
||||
|
||||
export const ShowProjects = () => {
|
||||
const utils = api.useUtils();
|
||||
const { data, isLoading } = api.project.all.useQuery();
|
||||
const { data: auth } = api.user.get.useQuery();
|
||||
const { mutateAsync } = api.project.remove.useMutation();
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
|
||||
@@ -84,11 +83,11 @@ export const ShowProjects = () => {
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
|
||||
<Permissions permissions={[PERMISSIONS.PROJECT.CREATE.name]}>
|
||||
{(auth?.role === "owner" || auth?.canCreateProjects) && (
|
||||
<div className="">
|
||||
<HandleProject />
|
||||
</div>
|
||||
</Permissions>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<CardContent className="space-y-2 py-8 border-t gap-4 flex flex-col min-h-[60vh]">
|
||||
@@ -158,7 +157,7 @@ export const ShowProjects = () => {
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
className="w-[200px] space-y-2 max-h-[400px]"
|
||||
className="w-[200px] space-y-2 overflow-y-auto max-h-[400px]"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{project.applications.length > 0 && (
|
||||
@@ -266,7 +265,7 @@ export const ShowProjects = () => {
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
className="w-[200px] space-y-2 max-h-[280px]"
|
||||
className="w-[200px] space-y-2 overflow-y-auto max-h-[280px]"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<DropdownMenuLabel className="font-normal">
|
||||
@@ -290,11 +289,8 @@ export const ShowProjects = () => {
|
||||
<div
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<Permissions
|
||||
permissions={[
|
||||
PERMISSIONS.PROJECT.DELETE.name,
|
||||
]}
|
||||
>
|
||||
{(auth?.role === "owner" ||
|
||||
auth?.canDeleteProjects) && (
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger className="w-full">
|
||||
<DropdownMenuItem
|
||||
@@ -360,7 +356,7 @@ export const ShowProjects = () => {
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</Permissions>
|
||||
)}
|
||||
</div>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
||||
@@ -46,11 +46,11 @@ interface Props {
|
||||
redisId: string;
|
||||
}
|
||||
export const ShowExternalRedisCredentials = ({ redisId }: Props) => {
|
||||
const { data: webServer } = api.webServer.get.useQuery();
|
||||
const { data: ip } = api.settings.getIp.useQuery();
|
||||
const { data, refetch } = api.redis.one.useQuery({ redisId });
|
||||
const { mutateAsync, isLoading } = api.redis.saveExternalPort.useMutation();
|
||||
const [connectionUrl, setConnectionUrl] = useState("");
|
||||
const getIp = data?.server?.ipAddress || webServer?.serverIp;
|
||||
const getIp = data?.server?.ipAddress || ip;
|
||||
|
||||
const form = useForm<DockerProvider>({
|
||||
defaultValues: {},
|
||||
|
||||
@@ -14,7 +14,7 @@ export const ShowWelcomeDokploy = () => {
|
||||
|
||||
const { data: isCloud, isLoading } = api.settings.isCloud.useQuery();
|
||||
|
||||
if (!isCloud || data?.role?.name !== "admin") {
|
||||
if (!isCloud || data?.role !== "admin") {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -23,14 +23,14 @@ export const ShowWelcomeDokploy = () => {
|
||||
!isLoading &&
|
||||
isCloud &&
|
||||
!localStorage.getItem("hasSeenCloudWelcomeModal") &&
|
||||
data?.role?.name === "owner"
|
||||
data?.role === "owner"
|
||||
) {
|
||||
setOpen(true);
|
||||
}
|
||||
}, [isCloud, isLoading]);
|
||||
|
||||
const handleClose = (isOpen: boolean) => {
|
||||
if (data?.role?.name === "owner") {
|
||||
if (data?.role === "owner") {
|
||||
setOpen(isOpen);
|
||||
if (!isOpen) {
|
||||
localStorage.setItem("hasSeenCloudWelcomeModal", "true"); // Establece el flag al cerrar el modal
|
||||
|
||||
@@ -32,7 +32,6 @@ import { Disable2FA } from "./disable-2fa";
|
||||
import { Enable2FA } from "./enable-2fa";
|
||||
|
||||
const profileSchema = z.object({
|
||||
name: z.string(),
|
||||
email: z.string(),
|
||||
password: z.string().nullable(),
|
||||
currentPassword: z.string().nullable(),
|
||||
@@ -80,7 +79,6 @@ export const ProfileForm = () => {
|
||||
|
||||
const form = useForm<Profile>({
|
||||
defaultValues: {
|
||||
name: data?.user?.name || "",
|
||||
email: data?.user?.email || "",
|
||||
password: "",
|
||||
image: data?.user?.image || "",
|
||||
@@ -94,7 +92,6 @@ export const ProfileForm = () => {
|
||||
if (data) {
|
||||
form.reset(
|
||||
{
|
||||
name: data?.user?.name || "",
|
||||
email: data?.user?.email || "",
|
||||
password: form.getValues("password") || "",
|
||||
image: data?.user?.image || "",
|
||||
@@ -117,7 +114,6 @@ export const ProfileForm = () => {
|
||||
|
||||
const onSubmit = async (values: Profile) => {
|
||||
await mutateAsync({
|
||||
name: values.name,
|
||||
email: values.email.toLowerCase(),
|
||||
password: values.password || undefined,
|
||||
image: values.image,
|
||||
@@ -128,7 +124,6 @@ export const ProfileForm = () => {
|
||||
await refetch();
|
||||
toast.success("Profile Updated");
|
||||
form.reset({
|
||||
name: values.name,
|
||||
email: values.email,
|
||||
password: "",
|
||||
image: values.image,
|
||||
@@ -172,19 +167,6 @@ export const ProfileForm = () => {
|
||||
className="grid gap-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
|
||||
control={form.control}
|
||||
name="email"
|
||||
|
||||
@@ -7,7 +7,7 @@ interface Props {
|
||||
serverId?: string;
|
||||
}
|
||||
export const ToggleDockerCleanup = ({ serverId }: Props) => {
|
||||
const { data, refetch } = api.webServer.get.useQuery(undefined, {
|
||||
const { data, refetch } = api.user.get.useQuery(undefined, {
|
||||
enabled: !serverId,
|
||||
});
|
||||
|
||||
@@ -20,9 +20,9 @@ export const ToggleDockerCleanup = ({ serverId }: Props) => {
|
||||
},
|
||||
);
|
||||
|
||||
const enabled = data?.enableDockerCleanup || server?.enableDockerCleanup;
|
||||
const enabled = data?.user.enableDockerCleanup || server?.enableDockerCleanup;
|
||||
|
||||
const { mutateAsync } = api.webServer.updateDockerCleanup.useMutation();
|
||||
const { mutateAsync } = api.settings.updateDockerCleanup.useMutation();
|
||||
|
||||
const handleToggle = async (checked: boolean) => {
|
||||
try {
|
||||
|
||||
@@ -89,7 +89,7 @@ export const SetupMonitoring = ({ serverId }: Props) => {
|
||||
enabled: !!serverId,
|
||||
},
|
||||
)
|
||||
: api.webServer.get.useQuery();
|
||||
: api.user.getServerMetrics.useQuery();
|
||||
|
||||
const url = useUrl();
|
||||
|
||||
|
||||
@@ -147,7 +147,7 @@ export const SetupServer = ({ serverId }: Props) => {
|
||||
<li>2. Add The SSH Key to Server Manually</li>
|
||||
</ul>
|
||||
<div className="flex flex-col gap-4 w-full overflow-auto">
|
||||
<div className="flex relative flex-col gap-2">
|
||||
<div className="flex relative flex-col gap-2 overflow-y-auto">
|
||||
<div className="text-sm text-primary flex flex-row gap-2 items-center">
|
||||
Copy Public Key ({server?.sshKey?.name})
|
||||
<button
|
||||
|
||||
@@ -117,7 +117,7 @@ export const CreateSSHKey = () => {
|
||||
Option 2
|
||||
</span>
|
||||
<div className="flex flex-col gap-4 w-full overflow-auto">
|
||||
<div className="flex relative flex-col gap-2">
|
||||
<div className="flex relative flex-col gap-2 overflow-y-auto">
|
||||
<div className="text-sm text-primary flex flex-row gap-2 items-center">
|
||||
Copy Public Key
|
||||
<button
|
||||
|
||||
@@ -49,15 +49,12 @@ type AddInvitation = z.infer<typeof addInvitation>;
|
||||
export const AddInvitation = () => {
|
||||
const [open, setOpen] = useState(false);
|
||||
const utils = api.useUtils();
|
||||
const { data: roles } = api.role.all.useQuery();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||
const { data: emailProviders } =
|
||||
api.notification.getEmailProviders.useQuery();
|
||||
const {
|
||||
mutateAsync: createInvitation,
|
||||
isLoading,
|
||||
error,
|
||||
} = api.user.createInvitation.useMutation();
|
||||
const { mutateAsync: sendInvitation } = api.user.sendInvitation.useMutation();
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const { data: activeOrganization } = authClient.useActiveOrganization();
|
||||
|
||||
const form = useForm<AddInvitation>({
|
||||
@@ -73,20 +70,36 @@ export const AddInvitation = () => {
|
||||
}, [form, form.formState.isSubmitSuccessful, form.reset]);
|
||||
|
||||
const onSubmit = async (data: AddInvitation) => {
|
||||
await createInvitation({
|
||||
setIsLoading(true);
|
||||
const result = await authClient.organization.inviteMember({
|
||||
email: data.email.toLowerCase(),
|
||||
role: data.role,
|
||||
organizationId: activeOrganization?.id || "",
|
||||
notificationId: data.notificationId || "",
|
||||
})
|
||||
.then(() => {
|
||||
organizationId: activeOrganization?.id,
|
||||
});
|
||||
|
||||
if (result.error) {
|
||||
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");
|
||||
})
|
||||
.catch((error: any) => {
|
||||
toast.error(error.message);
|
||||
});
|
||||
}
|
||||
setError(null);
|
||||
setOpen(false);
|
||||
}
|
||||
|
||||
utils.organization.allInvitations.invalidate();
|
||||
setIsLoading(false);
|
||||
};
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
@@ -100,7 +113,7 @@ export const AddInvitation = () => {
|
||||
<DialogTitle>Add Invitation</DialogTitle>
|
||||
<DialogDescription>Invite a new user</DialogDescription>
|
||||
</DialogHeader>
|
||||
{error && <AlertBlock type="error">{error.message}</AlertBlock>}
|
||||
{error && <AlertBlock type="error">{error}</AlertBlock>}
|
||||
|
||||
<Form {...form}>
|
||||
<form
|
||||
@@ -145,12 +158,6 @@ export const AddInvitation = () => {
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
<SelectItem value="member">Member</SelectItem>
|
||||
<SelectItem value="admin">Admin</SelectItem>
|
||||
{roles?.map((role) => (
|
||||
<SelectItem key={role.name} value={role.name}>
|
||||
{role.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormDescription>
|
||||
|
||||
@@ -1,758 +0,0 @@
|
||||
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>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,444 @@
|
||||
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,10 +30,9 @@ import { format } from "date-fns";
|
||||
import { MoreHorizontal, Users } from "lucide-react";
|
||||
import { Loader2 } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
import { AddUserPermissionsV2 } from "./add-permissions-v2";
|
||||
import { AddUserPermissions } from "./add-permissions";
|
||||
|
||||
export const ShowUsers = () => {
|
||||
const { data: user } = api.user.get.useQuery();
|
||||
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||
const { data, isLoading, refetch } = api.user.all.useQuery();
|
||||
const { mutateAsync } = api.user.remove.useMutation();
|
||||
@@ -85,22 +84,20 @@ export const ShowUsers = () => {
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{data?.map((member) => {
|
||||
const isSameUser = member.user.id === user?.user.id;
|
||||
|
||||
return (
|
||||
<TableRow key={member.id}>
|
||||
<TableCell className="w-[250px]">
|
||||
{member.user.email} {isSameUser && "(You)"}
|
||||
<TableCell className="w-[100px]">
|
||||
{member.user.email}
|
||||
</TableCell>
|
||||
<TableCell className="text-center">
|
||||
<Badge
|
||||
variant={
|
||||
member?.role?.name === "owner"
|
||||
member.role === "owner"
|
||||
? "default"
|
||||
: "secondary"
|
||||
}
|
||||
>
|
||||
{member?.role?.name}
|
||||
{member.role}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell className="text-center">
|
||||
@@ -115,77 +112,35 @@ export const ShowUsers = () => {
|
||||
</TableCell>
|
||||
|
||||
<TableCell className="text-right flex justify-end">
|
||||
{member.role !== "owner" && !isSameUser && (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="h-8 w-8 p-0"
|
||||
>
|
||||
<span className="sr-only">
|
||||
Open menu
|
||||
</span>
|
||||
<MoreHorizontal className="h-4 w-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuLabel>
|
||||
Actions
|
||||
</DropdownMenuLabel>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="h-8 w-8 p-0"
|
||||
>
|
||||
<span className="sr-only">Open menu</span>
|
||||
<MoreHorizontal className="h-4 w-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuLabel>
|
||||
Actions
|
||||
</DropdownMenuLabel>
|
||||
|
||||
{member.role !== "owner" && (
|
||||
<AddUserPermissions
|
||||
userId={member.user.id}
|
||||
/>
|
||||
)}
|
||||
|
||||
{member.role !== "owner" && (
|
||||
<>
|
||||
<AddUserPermissionsV2
|
||||
userId={member.user.id}
|
||||
/>
|
||||
</>
|
||||
|
||||
{!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) {
|
||||
{!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,
|
||||
})
|
||||
@@ -197,40 +152,86 @@ export const ShowUsers = () => {
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error(
|
||||
"Error deleting user",
|
||||
"Error deleting destination",
|
||||
);
|
||||
});
|
||||
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 } =
|
||||
await authClient.organization.removeMember(
|
||||
{
|
||||
memberIdOrEmail: member.id,
|
||||
},
|
||||
);
|
||||
const { error } =
|
||||
await authClient.organization.removeMember(
|
||||
{
|
||||
memberIdOrEmail: member.id,
|
||||
},
|
||||
);
|
||||
|
||||
if (!error) {
|
||||
toast.success(
|
||||
"User unlinked successfully",
|
||||
);
|
||||
refetch();
|
||||
} else {
|
||||
toast.error("Error unlinking user");
|
||||
}
|
||||
}}
|
||||
>
|
||||
<DropdownMenuItem
|
||||
className="w-full cursor-pointer text-red-500 hover:!text-red-600"
|
||||
onSelect={(e) => e.preventDefault()}
|
||||
if (!error) {
|
||||
toast.success(
|
||||
"User unlinked successfully",
|
||||
);
|
||||
refetch();
|
||||
} else {
|
||||
toast.error(
|
||||
"Error unlinking user",
|
||||
);
|
||||
}
|
||||
}}
|
||||
>
|
||||
Unlink User
|
||||
</DropdownMenuItem>
|
||||
</DialogAction>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)}
|
||||
<DropdownMenuItem
|
||||
className="w-full cursor-pointer text-red-500 hover:!text-red-600"
|
||||
onSelect={(e) => e.preventDefault()}
|
||||
>
|
||||
Unlink User
|
||||
</DropdownMenuItem>
|
||||
</DialogAction>
|
||||
</>
|
||||
)}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
|
||||
@@ -62,9 +62,9 @@ type AddServerDomain = z.infer<typeof addServerDomain>;
|
||||
|
||||
export const WebDomain = () => {
|
||||
const { t } = useTranslation("settings");
|
||||
const { data, refetch } = api.webServer.get.useQuery();
|
||||
const { data, refetch } = api.user.get.useQuery();
|
||||
const { mutateAsync, isLoading } =
|
||||
api.webServer.assignDomainServer.useMutation();
|
||||
api.settings.assignDomainServer.useMutation();
|
||||
|
||||
const form = useForm<AddServerDomain>({
|
||||
defaultValues: {
|
||||
@@ -79,10 +79,10 @@ export const WebDomain = () => {
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
form.reset({
|
||||
domain: data?.host || "",
|
||||
certificateType: data?.certificateType,
|
||||
letsEncryptEmail: data?.letsEncryptEmail || "",
|
||||
https: data?.https || false,
|
||||
domain: data?.user?.host || "",
|
||||
certificateType: data?.user?.certificateType,
|
||||
letsEncryptEmail: data?.user?.letsEncryptEmail || "",
|
||||
https: data?.user?.https || false,
|
||||
});
|
||||
}
|
||||
}, [form, form.reset, data]);
|
||||
|
||||
@@ -16,12 +16,13 @@ import { UpdateServer } from "./web-server/update-server";
|
||||
|
||||
export const WebServer = () => {
|
||||
const { t } = useTranslation("settings");
|
||||
const { data } = api.webServer.get.useQuery();
|
||||
const { data } = api.user.get.useQuery();
|
||||
|
||||
const { data: dokployVersion } = api.settings.getDokployVersion.useQuery();
|
||||
|
||||
return (
|
||||
<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">
|
||||
<div className="rounded-xl bg-background shadow-md ">
|
||||
<CardHeader className="">
|
||||
@@ -33,6 +34,14 @@ export const WebServer = () => {
|
||||
{t("settings.server.webServer.description")}
|
||||
</CardDescription>
|
||||
</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">
|
||||
<div className="grid md:grid-cols-2 gap-4">
|
||||
<ShowDokployActions />
|
||||
@@ -44,7 +53,7 @@ export const WebServer = () => {
|
||||
|
||||
<div className="flex items-center flex-wrap justify-between gap-4">
|
||||
<span className="text-sm text-muted-foreground">
|
||||
Server IP: {data?.serverIp}
|
||||
Server IP: {data?.user.serverIp}
|
||||
</span>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
Version: {dokployVersion}
|
||||
|
||||
@@ -46,15 +46,15 @@ interface Props {
|
||||
export const UpdateServerIp = ({ children }: Props) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
const { data } = api.webServer.get.useQuery();
|
||||
const { data } = api.user.get.useQuery();
|
||||
const { data: ip } = api.server.publicIp.useQuery();
|
||||
|
||||
const { mutateAsync, isLoading, error, isError } =
|
||||
api.webServer.update.useMutation();
|
||||
api.user.update.useMutation();
|
||||
|
||||
const form = useForm<Schema>({
|
||||
defaultValues: {
|
||||
serverIp: data?.serverIp || "",
|
||||
serverIp: data?.user.serverIp || "",
|
||||
},
|
||||
resolver: zodResolver(schema),
|
||||
});
|
||||
@@ -62,7 +62,7 @@ export const UpdateServerIp = ({ children }: Props) => {
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
form.reset({
|
||||
serverIp: data.serverIp || "",
|
||||
serverIp: data.user.serverIp || "",
|
||||
});
|
||||
}
|
||||
}, [form, form.reset, data]);
|
||||
@@ -80,7 +80,7 @@ export const UpdateServerIp = ({ children }: Props) => {
|
||||
})
|
||||
.then(async () => {
|
||||
toast.success("Server IP Updated");
|
||||
await utils.webServer.get.invalidate();
|
||||
await utils.user.get.invalidate();
|
||||
setIsOpen(false);
|
||||
})
|
||||
.catch(() => {
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
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>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<div className="max-h-48">
|
||||
<div className="max-h-48 overflow-y-auto">
|
||||
{activeNodes.map((node) => (
|
||||
<div key={node.ID} className="flex items-center gap-2">
|
||||
{node.Hostname}
|
||||
@@ -162,7 +162,7 @@ export default function SwarmMonitorCard({ serverId }: Props) {
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<div className="max-h-48">
|
||||
<div className="max-h-48 overflow-y-auto">
|
||||
{managerNodes.map((node) => (
|
||||
<div key={node.ID} className="flex items-center gap-2">
|
||||
{node.Hostname}
|
||||
|
||||
@@ -87,8 +87,8 @@ import { Logo } from "../shared/logo";
|
||||
import { Button } from "../ui/button";
|
||||
import { UpdateServerButton } from "./update-server";
|
||||
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 SingleNavItem = {
|
||||
@@ -102,6 +102,10 @@ type SingleNavItem = {
|
||||
}) => 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 =
|
||||
| SingleNavItem
|
||||
| {
|
||||
@@ -115,6 +119,8 @@ type NavItem =
|
||||
}) => boolean;
|
||||
};
|
||||
|
||||
// ExternalLink type
|
||||
// Represents an external link item (used for the help section)
|
||||
type ExternalLink = {
|
||||
name: string;
|
||||
url: string;
|
||||
@@ -125,12 +131,18 @@ type ExternalLink = {
|
||||
}) => boolean;
|
||||
};
|
||||
|
||||
// Menu type
|
||||
// Consists of home, settings, and help items
|
||||
type Menu = {
|
||||
home: NavItem[];
|
||||
settings: NavItem[];
|
||||
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 = {
|
||||
home: [
|
||||
{
|
||||
@@ -153,8 +165,7 @@ const MENU: Menu = {
|
||||
url: "/dashboard/schedules",
|
||||
icon: Clock,
|
||||
// Only enabled in non-cloud environments
|
||||
isEnabled: ({ isCloud, auth }) =>
|
||||
!isCloud && auth?.role?.name === "owner",
|
||||
isEnabled: ({ isCloud, auth }) => !isCloud && auth?.role === "owner",
|
||||
},
|
||||
{
|
||||
isSingle: true,
|
||||
@@ -164,10 +175,7 @@ const MENU: Menu = {
|
||||
// Only enabled for admins and users with access to Traefik files in non-cloud environments
|
||||
isEnabled: ({ auth, isCloud }) =>
|
||||
!!(
|
||||
(auth?.role?.name === "owner" ||
|
||||
auth?.role?.permissions?.includes(
|
||||
PERMISSIONS.TRAEFIK.ACCESS.name,
|
||||
)) &&
|
||||
(auth?.role === "owner" || auth?.canAccessToTraefikFiles) &&
|
||||
!isCloud
|
||||
),
|
||||
},
|
||||
@@ -178,11 +186,7 @@ const MENU: Menu = {
|
||||
icon: BlocksIcon,
|
||||
// Only enabled for admins and users with access to Docker in non-cloud environments
|
||||
isEnabled: ({ auth, isCloud }) =>
|
||||
!!(
|
||||
(auth?.role?.name === "owner" ||
|
||||
auth?.role?.permissions?.includes(PERMISSIONS.DOCKER.VIEW.name)) &&
|
||||
!isCloud
|
||||
),
|
||||
!!((auth?.role === "owner" || auth?.canAccessToDocker) && !isCloud),
|
||||
},
|
||||
{
|
||||
isSingle: true,
|
||||
@@ -191,11 +195,7 @@ const MENU: Menu = {
|
||||
icon: PieChart,
|
||||
// Only enabled for admins and users with access to Docker in non-cloud environments
|
||||
isEnabled: ({ auth, isCloud }) =>
|
||||
!!(
|
||||
(auth?.role?.name === "owner" ||
|
||||
auth?.role?.permissions?.includes(PERMISSIONS.DOCKER.VIEW.name)) &&
|
||||
!isCloud
|
||||
),
|
||||
!!((auth?.role === "owner" || auth?.canAccessToDocker) && !isCloud),
|
||||
},
|
||||
{
|
||||
isSingle: true,
|
||||
@@ -204,12 +204,64 @@ const MENU: Menu = {
|
||||
icon: Forward,
|
||||
// Only enabled for admins and users with access to Docker in non-cloud environments
|
||||
isEnabled: ({ auth, isCloud }) =>
|
||||
!!(
|
||||
(auth?.role?.name === "owner" ||
|
||||
auth?.role?.permissions?.includes(PERMISSIONS.DOCKER.VIEW.name)) &&
|
||||
!isCloud
|
||||
),
|
||||
!!((auth?.role === "owner" || auth?.canAccessToDocker) && !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: [
|
||||
@@ -219,8 +271,7 @@ const MENU: Menu = {
|
||||
url: "/dashboard/settings/server",
|
||||
icon: Activity,
|
||||
// Only enabled for admins in non-cloud environments
|
||||
isEnabled: ({ auth, isCloud }) =>
|
||||
!!(auth?.role?.name === "owner" && !isCloud),
|
||||
isEnabled: ({ auth, isCloud }) => !!(auth?.role === "owner" && !isCloud),
|
||||
},
|
||||
{
|
||||
isSingle: true,
|
||||
@@ -234,7 +285,7 @@ const MENU: Menu = {
|
||||
url: "/dashboard/settings/servers",
|
||||
icon: Server,
|
||||
// Only enabled for admins
|
||||
isEnabled: ({ auth }) => !!(auth?.role?.name === "owner"),
|
||||
isEnabled: ({ auth }) => !!(auth?.role === "owner"),
|
||||
},
|
||||
{
|
||||
isSingle: true,
|
||||
@@ -242,7 +293,7 @@ const MENU: Menu = {
|
||||
icon: Users,
|
||||
url: "/dashboard/settings/users",
|
||||
// Only enabled for admins
|
||||
isEnabled: ({ auth }) => !!(auth?.role?.name === "owner"),
|
||||
isEnabled: ({ auth }) => !!(auth?.role === "owner"),
|
||||
},
|
||||
{
|
||||
isSingle: true,
|
||||
@@ -251,17 +302,14 @@ const MENU: Menu = {
|
||||
url: "/dashboard/settings/ssh-keys",
|
||||
// Only enabled for admins and users with access to SSH keys
|
||||
isEnabled: ({ auth }) =>
|
||||
!!(
|
||||
auth?.role?.name === "owner" ||
|
||||
auth?.role?.permissions?.includes(PERMISSIONS.SSH_KEYS.ACCESS.name)
|
||||
),
|
||||
!!(auth?.role === "owner" || auth?.canAccessToSSHKeys),
|
||||
},
|
||||
{
|
||||
title: "AI",
|
||||
icon: BotIcon,
|
||||
url: "/dashboard/settings/ai",
|
||||
isSingle: true,
|
||||
isEnabled: ({ auth }) => !!(auth?.role?.name === "owner"),
|
||||
isEnabled: ({ auth }) => !!(auth?.role === "owner"),
|
||||
},
|
||||
{
|
||||
isSingle: true,
|
||||
@@ -270,12 +318,7 @@ const MENU: Menu = {
|
||||
icon: GitBranch,
|
||||
// Only enabled for admins and users with access to Git providers
|
||||
isEnabled: ({ auth }) =>
|
||||
!!(
|
||||
auth?.role?.name === "owner" ||
|
||||
auth?.role?.permissions?.includes(
|
||||
PERMISSIONS.GIT_PROVIDERS.ACCESS.name,
|
||||
)
|
||||
),
|
||||
!!(auth?.role === "owner" || auth?.canAccessToGitProviders),
|
||||
},
|
||||
{
|
||||
isSingle: true,
|
||||
@@ -283,7 +326,7 @@ const MENU: Menu = {
|
||||
url: "/dashboard/settings/registry",
|
||||
icon: Package,
|
||||
// Only enabled for admins
|
||||
isEnabled: ({ auth }) => !!(auth?.role?.name === "owner"),
|
||||
isEnabled: ({ auth }) => !!(auth?.role === "owner"),
|
||||
},
|
||||
{
|
||||
isSingle: true,
|
||||
@@ -291,7 +334,7 @@ const MENU: Menu = {
|
||||
url: "/dashboard/settings/destinations",
|
||||
icon: Database,
|
||||
// Only enabled for admins
|
||||
isEnabled: ({ auth }) => !!(auth?.role?.name === "owner"),
|
||||
isEnabled: ({ auth }) => !!(auth?.role === "owner"),
|
||||
},
|
||||
|
||||
{
|
||||
@@ -300,7 +343,7 @@ const MENU: Menu = {
|
||||
url: "/dashboard/settings/certificates",
|
||||
icon: ShieldCheck,
|
||||
// Only enabled for admins
|
||||
isEnabled: ({ auth }) => !!(auth?.role?.name === "owner"),
|
||||
isEnabled: ({ auth }) => !!(auth?.role === "owner"),
|
||||
},
|
||||
{
|
||||
isSingle: true,
|
||||
@@ -308,8 +351,7 @@ const MENU: Menu = {
|
||||
url: "/dashboard/settings/cluster",
|
||||
icon: Boxes,
|
||||
// Only enabled for admins in non-cloud environments
|
||||
isEnabled: ({ auth, isCloud }) =>
|
||||
!!(auth?.role?.name === "owner" && !isCloud),
|
||||
isEnabled: ({ auth, isCloud }) => !!(auth?.role === "owner" && !isCloud),
|
||||
},
|
||||
{
|
||||
isSingle: true,
|
||||
@@ -317,7 +359,7 @@ const MENU: Menu = {
|
||||
url: "/dashboard/settings/notifications",
|
||||
icon: Bell,
|
||||
// Only enabled for admins
|
||||
isEnabled: ({ auth }) => !!(auth?.role?.name === "owner"),
|
||||
isEnabled: ({ auth }) => !!(auth?.role === "owner"),
|
||||
},
|
||||
{
|
||||
isSingle: true,
|
||||
@@ -325,8 +367,7 @@ const MENU: Menu = {
|
||||
url: "/dashboard/settings/billing",
|
||||
icon: CreditCard,
|
||||
// Only enabled for admins in cloud environments
|
||||
isEnabled: ({ auth, isCloud }) =>
|
||||
!!(auth?.role?.name === "owner" && isCloud),
|
||||
isEnabled: ({ auth, isCloud }) => !!(auth?.role === "owner" && isCloud),
|
||||
},
|
||||
],
|
||||
|
||||
@@ -464,7 +505,6 @@ function SidebarLogo() {
|
||||
const { state } = useSidebar();
|
||||
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||
const { data: user } = api.user.get.useQuery();
|
||||
console.log(user);
|
||||
const { data: session } = authClient.useSession();
|
||||
|
||||
const {
|
||||
@@ -623,7 +663,7 @@ function SidebarLogo() {
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
{(user?.role?.name === "owner" || isCloud) && (
|
||||
{(user?.role === "owner" || isCloud) && (
|
||||
<>
|
||||
<DropdownMenuSeparator />
|
||||
<AddOrganization />
|
||||
@@ -989,7 +1029,7 @@ export default function Page({ children }: Props) {
|
||||
</SidebarContent>
|
||||
<SidebarFooter>
|
||||
<SidebarMenu className="flex flex-col gap-2">
|
||||
{!isCloud && auth?.role?.name === "owner" && (
|
||||
{!isCloud && auth?.role === "owner" && (
|
||||
<SidebarMenuItem>
|
||||
<UpdateServerButton />
|
||||
</SidebarMenuItem>
|
||||
|
||||
@@ -23,8 +23,6 @@ import { ChevronsUpDown } from "lucide-react";
|
||||
import { useRouter } from "next/router";
|
||||
import { ModeToggle } from "../ui/modeToggle";
|
||||
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;
|
||||
|
||||
@@ -100,7 +98,7 @@ export const UserNav = () => {
|
||||
>
|
||||
Monitoring
|
||||
</DropdownMenuItem>
|
||||
<Permissions permissions={[PERMISSIONS.TRAEFIK.ACCESS.name]}>
|
||||
{(data?.role === "owner" || data?.canAccessToTraefikFiles) && (
|
||||
<DropdownMenuItem
|
||||
className="cursor-pointer"
|
||||
onClick={() => {
|
||||
@@ -109,9 +107,8 @@ export const UserNav = () => {
|
||||
>
|
||||
Traefik
|
||||
</DropdownMenuItem>
|
||||
</Permissions>
|
||||
|
||||
<Permissions permissions={[PERMISSIONS.DOCKER.VIEW.name]}>
|
||||
)}
|
||||
{(data?.role === "owner" || data?.canAccessToDocker) && (
|
||||
<DropdownMenuItem
|
||||
className="cursor-pointer"
|
||||
onClick={() => {
|
||||
@@ -122,11 +119,11 @@ export const UserNav = () => {
|
||||
>
|
||||
Docker
|
||||
</DropdownMenuItem>
|
||||
</Permissions>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{data?.role?.name === "owner" && (
|
||||
{data?.role === "owner" && (
|
||||
<DropdownMenuItem
|
||||
className="cursor-pointer"
|
||||
onClick={() => {
|
||||
@@ -139,7 +136,7 @@ export const UserNav = () => {
|
||||
</>
|
||||
)}
|
||||
</DropdownMenuGroup>
|
||||
{isCloud && data?.role?.name === "owner" && (
|
||||
{isCloud && data?.role === "owner" && (
|
||||
<DropdownMenuItem
|
||||
className="cursor-pointer"
|
||||
onClick={() => {
|
||||
@@ -157,6 +154,9 @@ export const UserNav = () => {
|
||||
await authClient.signOut().then(() => {
|
||||
router.push("/");
|
||||
});
|
||||
// await mutateAsync().then(() => {
|
||||
// router.push("/");
|
||||
// });
|
||||
}}
|
||||
>
|
||||
Log out
|
||||
|
||||
@@ -1,187 +0,0 @@
|
||||
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");
|
||||
File diff suppressed because it is too large
Load Diff
@@ -722,13 +722,6 @@
|
||||
"when": 1751848685503,
|
||||
"tag": "0102_opposite_grandmaster",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 103,
|
||||
"version": "7",
|
||||
"when": 1752428260850,
|
||||
"tag": "0103_brainy_nehzno",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -24,7 +24,7 @@
|
||||
// });
|
||||
// for (const admin of admins) {
|
||||
// const user = await db
|
||||
// .insert(schema.users)
|
||||
// .insert(schema.users_temp)
|
||||
// .values({
|
||||
// id: admin.adminId,
|
||||
// email: admin.auth.email,
|
||||
@@ -74,7 +74,7 @@
|
||||
|
||||
// for (const member of admin.users) {
|
||||
// const userTemp = await db
|
||||
// .insert(schema.users)
|
||||
// .insert(schema.users_temp)
|
||||
// .values({
|
||||
// id: member.userId,
|
||||
// email: member.auth.email,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "dokploy",
|
||||
"version": "v0.24.1",
|
||||
"version": "v0.24.2",
|
||||
"private": true,
|
||||
"license": "Apache-2.0",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { buffer } from "node:stream/consumers";
|
||||
import { db } from "@/server/db";
|
||||
import { organization, server, users } from "@/server/db/schema";
|
||||
import { organization, server, users_temp } from "@/server/db/schema";
|
||||
import { type Server, findUserById } from "@dokploy/server";
|
||||
import { asc, eq } from "drizzle-orm";
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
@@ -64,13 +64,13 @@ export default async function handler(
|
||||
session.subscription as string,
|
||||
);
|
||||
await db
|
||||
.update(users)
|
||||
.update(users_temp)
|
||||
.set({
|
||||
stripeCustomerId: session.customer as string,
|
||||
stripeSubscriptionId: session.subscription as string,
|
||||
serversQuantity: subscription?.items?.data?.[0]?.quantity ?? 0,
|
||||
})
|
||||
.where(eq(users.id, adminId))
|
||||
.where(eq(users_temp.id, adminId))
|
||||
.returning();
|
||||
|
||||
const admin = await findUserById(adminId);
|
||||
@@ -85,12 +85,14 @@ export default async function handler(
|
||||
const newSubscription = event.data.object as Stripe.Subscription;
|
||||
|
||||
await db
|
||||
.update(users)
|
||||
.update(users_temp)
|
||||
.set({
|
||||
stripeSubscriptionId: newSubscription.id,
|
||||
stripeCustomerId: newSubscription.customer as string,
|
||||
})
|
||||
.where(eq(users.stripeCustomerId, newSubscription.customer as string))
|
||||
.where(
|
||||
eq(users_temp.stripeCustomerId, newSubscription.customer as string),
|
||||
)
|
||||
.returning();
|
||||
|
||||
break;
|
||||
@@ -100,12 +102,14 @@ export default async function handler(
|
||||
const newSubscription = event.data.object as Stripe.Subscription;
|
||||
|
||||
await db
|
||||
.update(users)
|
||||
.update(users_temp)
|
||||
.set({
|
||||
stripeSubscriptionId: null,
|
||||
serversQuantity: 0,
|
||||
})
|
||||
.where(eq(users.stripeCustomerId, newSubscription.customer as string));
|
||||
.where(
|
||||
eq(users_temp.stripeCustomerId, newSubscription.customer as string),
|
||||
);
|
||||
|
||||
const admin = await findUserByStripeCustomerId(
|
||||
newSubscription.customer as string,
|
||||
@@ -131,12 +135,12 @@ export default async function handler(
|
||||
|
||||
if (newSubscription.status === "active") {
|
||||
await db
|
||||
.update(users)
|
||||
.update(users_temp)
|
||||
.set({
|
||||
serversQuantity: newSubscription?.items?.data?.[0]?.quantity ?? 0,
|
||||
})
|
||||
.where(
|
||||
eq(users.stripeCustomerId, newSubscription.customer as string),
|
||||
eq(users_temp.stripeCustomerId, newSubscription.customer as string),
|
||||
);
|
||||
|
||||
const newServersQuantity = admin.serversQuantity;
|
||||
@@ -144,10 +148,10 @@ export default async function handler(
|
||||
} else {
|
||||
await disableServers(admin.id);
|
||||
await db
|
||||
.update(users)
|
||||
.update(users_temp)
|
||||
.set({ serversQuantity: 0 })
|
||||
.where(
|
||||
eq(users.stripeCustomerId, newSubscription.customer as string),
|
||||
eq(users_temp.stripeCustomerId, newSubscription.customer as string),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -168,11 +172,11 @@ export default async function handler(
|
||||
}
|
||||
|
||||
await db
|
||||
.update(users)
|
||||
.update(users_temp)
|
||||
.set({
|
||||
serversQuantity: suscription?.items?.data?.[0]?.quantity ?? 0,
|
||||
})
|
||||
.where(eq(users.stripeCustomerId, suscription.customer as string));
|
||||
.where(eq(users_temp.stripeCustomerId, suscription.customer as string));
|
||||
|
||||
const admin = await findUserByStripeCustomerId(
|
||||
suscription.customer as string,
|
||||
@@ -201,11 +205,13 @@ export default async function handler(
|
||||
return res.status(400).send("Webhook Error: Admin not found");
|
||||
}
|
||||
await db
|
||||
.update(users)
|
||||
.update(users_temp)
|
||||
.set({
|
||||
serversQuantity: 0,
|
||||
})
|
||||
.where(eq(users.stripeCustomerId, newInvoice.customer as string));
|
||||
.where(
|
||||
eq(users_temp.stripeCustomerId, newInvoice.customer as string),
|
||||
);
|
||||
|
||||
await disableServers(admin.id);
|
||||
}
|
||||
@@ -223,13 +229,13 @@ export default async function handler(
|
||||
|
||||
await disableServers(admin.id);
|
||||
await db
|
||||
.update(users)
|
||||
.update(users_temp)
|
||||
.set({
|
||||
stripeCustomerId: null,
|
||||
stripeSubscriptionId: null,
|
||||
serversQuantity: 0,
|
||||
})
|
||||
.where(eq(users.stripeCustomerId, customer.id));
|
||||
.where(eq(users_temp.stripeCustomerId, customer.id));
|
||||
|
||||
break;
|
||||
}
|
||||
@@ -256,8 +262,8 @@ const disableServers = async (userId: string) => {
|
||||
};
|
||||
|
||||
const findUserByStripeCustomerId = async (stripeCustomerId: string) => {
|
||||
const user = db.query.users.findFirst({
|
||||
where: eq(users.stripeCustomerId, stripeCustomerId),
|
||||
const user = db.query.users_temp.findFirst({
|
||||
where: eq(users_temp.stripeCustomerId, stripeCustomerId),
|
||||
});
|
||||
return user;
|
||||
};
|
||||
|
||||
@@ -3,7 +3,6 @@ import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
||||
import { appRouter } from "@/server/api/root";
|
||||
import { IS_CLOUD } from "@dokploy/server/constants";
|
||||
import { validateRequest } from "@dokploy/server/lib/auth";
|
||||
import { PERMISSIONS } from "@dokploy/server/lib/permissions";
|
||||
import { createServerSideHelpers } from "@trpc/react-query/server";
|
||||
import type { GetServerSidePropsContext } from "next";
|
||||
import type { ReactElement } from "react";
|
||||
@@ -54,8 +53,12 @@ export async function getServerSideProps(
|
||||
try {
|
||||
await helpers.project.all.prefetch();
|
||||
|
||||
if (user.role?.name === "member" || !user?.role?.isSystem) {
|
||||
if (!user?.role?.permissions?.includes(PERMISSIONS.DOCKER.VIEW.name)) {
|
||||
if (user.role === "member") {
|
||||
const userR = await helpers.user.one.fetch({
|
||||
userId: user.id,
|
||||
});
|
||||
|
||||
if (!userR?.canAccessToDocker) {
|
||||
return {
|
||||
redirect: {
|
||||
permanent: true,
|
||||
|
||||
@@ -20,7 +20,7 @@ const Dashboard = () => {
|
||||
false,
|
||||
);
|
||||
|
||||
const { data: webServer, isLoading } = api.webServer.get.useQuery();
|
||||
const { data: monitoring, isLoading } = api.user.getMetricsToken.useQuery();
|
||||
return (
|
||||
<div className="space-y-4 pb-10">
|
||||
{/* <AlertBlock>
|
||||
@@ -59,12 +59,12 @@ const Dashboard = () => {
|
||||
<ShowPaidMonitoring
|
||||
BASE_URL={
|
||||
process.env.NODE_ENV === "production"
|
||||
? `http://${webServer?.serverIp}:${webServer?.metricsConfig?.server?.port}/metrics`
|
||||
? `http://${monitoring?.serverIp}:${monitoring?.metricsConfig?.server?.port}/metrics`
|
||||
: BASE_URL
|
||||
}
|
||||
token={
|
||||
process.env.NODE_ENV === "production"
|
||||
? webServer?.metricsConfig?.server?.token
|
||||
? monitoring?.metricsConfig?.server?.token
|
||||
: DEFAULT_TOKEN
|
||||
}
|
||||
/>
|
||||
|
||||
@@ -94,7 +94,6 @@ import { useRouter } from "next/router";
|
||||
import { type ReactElement, useEffect, useMemo, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import superjson from "superjson";
|
||||
import { Permissions } from "@/components/dashboard/shared/Permissions";
|
||||
|
||||
export type Services = {
|
||||
appName: string;
|
||||
@@ -222,6 +221,7 @@ const Project = (
|
||||
) => {
|
||||
const [isBulkActionLoading, setIsBulkActionLoading] = useState(false);
|
||||
const { projectId } = props;
|
||||
const { data: auth } = api.user.get.useQuery();
|
||||
const [sortBy, setSortBy] = useState<string>(() => {
|
||||
if (typeof window !== "undefined") {
|
||||
return localStorage.getItem("servicesSort") || "createdAt-desc";
|
||||
@@ -736,27 +736,30 @@ const Project = (
|
||||
Stop
|
||||
</Button>
|
||||
</DialogAction>
|
||||
<Permissions permissions={["project:delete"]}>
|
||||
<DialogAction
|
||||
title="Delete Services"
|
||||
description={`Are you sure you want to delete ${selectedServices.length} services? This action cannot be undone.`}
|
||||
type="destructive"
|
||||
onClick={handleBulkDelete}
|
||||
>
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="w-full justify-start text-destructive"
|
||||
{(auth?.role === "owner" ||
|
||||
auth?.canDeleteServices) && (
|
||||
<>
|
||||
<DialogAction
|
||||
title="Delete Services"
|
||||
description={`Are you sure you want to delete ${selectedServices.length} services? This action cannot be undone.`}
|
||||
type="destructive"
|
||||
onClick={handleBulkDelete}
|
||||
>
|
||||
<Trash2 className="mr-2 h-4 w-4" />
|
||||
Delete
|
||||
</Button>
|
||||
</DialogAction>
|
||||
<DuplicateProject
|
||||
projectId={projectId}
|
||||
services={applications}
|
||||
selectedServiceIds={selectedServices}
|
||||
/>
|
||||
</Permissions>
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="w-full justify-start text-destructive"
|
||||
>
|
||||
<Trash2 className="mr-2 h-4 w-4" />
|
||||
Delete
|
||||
</Button>
|
||||
</DialogAction>
|
||||
<DuplicateProject
|
||||
projectId={projectId}
|
||||
services={applications}
|
||||
selectedServiceIds={selectedServices}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
<Dialog
|
||||
open={isMoveDialogOpen}
|
||||
|
||||
@@ -37,6 +37,7 @@ import {
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { appRouter } from "@/server/api/root";
|
||||
import { api } from "@/utils/api";
|
||||
import { validateRequest } from "@dokploy/server/lib/auth";
|
||||
@@ -53,7 +54,6 @@ import { useRouter } from "next/router";
|
||||
import { type ReactElement, useEffect, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import superjson from "superjson";
|
||||
import { Permissions } from "@/components/dashboard/shared/Permissions";
|
||||
|
||||
type TabState =
|
||||
| "projects"
|
||||
@@ -88,6 +88,7 @@ const Service = (
|
||||
);
|
||||
|
||||
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||
const { data: auth } = api.user.get.useQuery();
|
||||
|
||||
return (
|
||||
<div className="pb-10">
|
||||
@@ -178,9 +179,9 @@ const Service = (
|
||||
|
||||
<div className="flex flex-row gap-2 justify-end">
|
||||
<UpdateApplication applicationId={applicationId} />
|
||||
<Permissions permissions={["service:delete"]}>
|
||||
{(auth?.role === "owner" || auth?.canDeleteServices) && (
|
||||
<DeleteService id={applicationId} type="application" />
|
||||
</Permissions>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
|
||||
@@ -33,6 +33,7 @@ import {
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { appRouter } from "@/server/api/root";
|
||||
import { api } from "@/utils/api";
|
||||
import { validateRequest } from "@dokploy/server/lib/auth";
|
||||
@@ -50,7 +51,6 @@ import { useRouter } from "next/router";
|
||||
import { type ReactElement, useEffect, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import superjson from "superjson";
|
||||
import { Permissions } from "@/components/dashboard/shared/Permissions";
|
||||
|
||||
type TabState =
|
||||
| "projects"
|
||||
@@ -78,6 +78,7 @@ const Service = (
|
||||
|
||||
const { data } = api.compose.one.useQuery({ composeId });
|
||||
|
||||
const { data: auth } = api.user.get.useQuery();
|
||||
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||
|
||||
return (
|
||||
@@ -170,9 +171,9 @@ const Service = (
|
||||
<div className="flex flex-row gap-2 justify-end">
|
||||
<UpdateCompose composeId={composeId} />
|
||||
|
||||
<Permissions permissions={["service:delete"]}>
|
||||
{(auth?.role === "owner" || auth?.canDeleteServices) && (
|
||||
<DeleteService id={composeId} type="compose" />
|
||||
</Permissions>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
|
||||
@@ -44,7 +44,6 @@ import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import { type ReactElement, useState } from "react";
|
||||
import superjson from "superjson";
|
||||
import { Permissions } from "@/components/dashboard/shared/Permissions";
|
||||
|
||||
type TabState = "projects" | "monitoring" | "settings" | "backups" | "advanced";
|
||||
|
||||
@@ -58,6 +57,7 @@ const Mariadb = (
|
||||
const { projectId } = router.query;
|
||||
const [tab, setSab] = useState<TabState>(activeTab);
|
||||
const { data } = api.mariadb.one.useQuery({ mariadbId });
|
||||
const { data: auth } = api.user.get.useQuery();
|
||||
|
||||
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||
|
||||
@@ -142,9 +142,9 @@ const Mariadb = (
|
||||
</div>
|
||||
<div className="flex flex-row gap-2 justify-end">
|
||||
<UpdateMariadb mariadbId={mariadbId} />
|
||||
<Permissions permissions={["service:delete"]}>
|
||||
{(auth?.role === "owner" || auth?.canDeleteServices) && (
|
||||
<DeleteService id={mariadbId} type="mariadb" />
|
||||
</Permissions>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
|
||||
@@ -44,7 +44,6 @@ import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import { type ReactElement, useState } from "react";
|
||||
import superjson from "superjson";
|
||||
import { Permissions } from "@/components/dashboard/shared/Permissions";
|
||||
|
||||
type TabState = "projects" | "monitoring" | "settings" | "backups" | "advanced";
|
||||
|
||||
@@ -58,6 +57,8 @@ const Mongo = (
|
||||
const [tab, setSab] = useState<TabState>(activeTab);
|
||||
const { data } = api.mongo.one.useQuery({ mongoId });
|
||||
|
||||
const { data: auth } = api.user.get.useQuery();
|
||||
|
||||
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||
|
||||
return (
|
||||
@@ -142,9 +143,9 @@ const Mongo = (
|
||||
|
||||
<div className="flex flex-row gap-2 justify-end">
|
||||
<UpdateMongo mongoId={mongoId} />
|
||||
<Permissions permissions={["service:delete"]}>
|
||||
{(auth?.role === "owner" || auth?.canDeleteServices) && (
|
||||
<DeleteService id={mongoId} type="mongo" />
|
||||
</Permissions>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
|
||||
@@ -29,6 +29,7 @@ import {
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { appRouter } from "@/server/api/root";
|
||||
import { api } from "@/utils/api";
|
||||
import { validateRequest } from "@dokploy/server/lib/auth";
|
||||
@@ -43,8 +44,6 @@ import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import { type ReactElement, useState } from "react";
|
||||
import superjson from "superjson";
|
||||
import { Permissions } from "@/components/dashboard/shared/Permissions";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
type TabState = "projects" | "monitoring" | "settings" | "backups" | "advanced";
|
||||
|
||||
@@ -57,6 +56,7 @@ const MySql = (
|
||||
const { projectId } = router.query;
|
||||
const [tab, setSab] = useState<TabState>(activeTab);
|
||||
const { data } = api.mysql.one.useQuery({ mysqlId });
|
||||
const { data: auth } = api.user.get.useQuery();
|
||||
|
||||
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||
|
||||
@@ -143,9 +143,9 @@ const MySql = (
|
||||
|
||||
<div className="flex flex-row gap-2 justify-end">
|
||||
<UpdateMysql mysqlId={mysqlId} />
|
||||
<Permissions permissions={["service:delete"]}>
|
||||
{(auth?.role === "owner" || auth?.canDeleteServices) && (
|
||||
<DeleteService id={mysqlId} type="mysql" />
|
||||
</Permissions>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
|
||||
@@ -29,6 +29,7 @@ import {
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { appRouter } from "@/server/api/root";
|
||||
import { api } from "@/utils/api";
|
||||
import { validateRequest } from "@dokploy/server/lib/auth";
|
||||
@@ -43,8 +44,6 @@ import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import { type ReactElement, useState } from "react";
|
||||
import superjson from "superjson";
|
||||
import { Permissions } from "@/components/dashboard/shared/Permissions";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
type TabState = "projects" | "monitoring" | "settings" | "backups" | "advanced";
|
||||
|
||||
@@ -57,6 +56,7 @@ const Postgresql = (
|
||||
const { projectId } = router.query;
|
||||
const [tab, setSab] = useState<TabState>(activeTab);
|
||||
const { data } = api.postgres.one.useQuery({ postgresId });
|
||||
const { data: auth } = api.user.get.useQuery();
|
||||
|
||||
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||
|
||||
@@ -142,9 +142,9 @@ const Postgresql = (
|
||||
|
||||
<div className="flex flex-row gap-2 justify-end">
|
||||
<UpdatePostgres postgresId={postgresId} />
|
||||
<Permissions permissions={["service:delete"]}>
|
||||
{(auth?.role === "owner" || auth?.canDeleteServices) && (
|
||||
<DeleteService id={postgresId} type="postgres" />
|
||||
</Permissions>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
|
||||
@@ -43,7 +43,6 @@ import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import { type ReactElement, useState } from "react";
|
||||
import superjson from "superjson";
|
||||
import { Permissions } from "@/components/dashboard/shared/Permissions";
|
||||
|
||||
type TabState = "projects" | "monitoring" | "settings" | "advanced";
|
||||
|
||||
@@ -57,6 +56,8 @@ const Redis = (
|
||||
const [tab, setSab] = useState<TabState>(activeTab);
|
||||
const { data } = api.redis.one.useQuery({ redisId });
|
||||
|
||||
const { data: auth } = api.user.get.useQuery();
|
||||
|
||||
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||
|
||||
return (
|
||||
@@ -141,9 +142,9 @@ const Redis = (
|
||||
|
||||
<div className="flex flex-row gap-2 justify-end">
|
||||
<UpdateRedis redisId={redisId} />
|
||||
<Permissions permissions={["service:delete"]}>
|
||||
{(auth?.role === "owner" || auth?.canDeleteServices) && (
|
||||
<DeleteService id={redisId} type="redis" />
|
||||
</Permissions>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
|
||||
@@ -39,7 +39,7 @@ export async function getServerSideProps(
|
||||
};
|
||||
}
|
||||
const { user } = await validateRequest(ctx.req);
|
||||
if (!user || (user.role?.name !== "owner" && user.role?.name !== "admin")) {
|
||||
if (!user || user.role !== "owner") {
|
||||
return {
|
||||
redirect: {
|
||||
permanent: true,
|
||||
|
||||
@@ -44,7 +44,7 @@ export async function getServerSideProps(
|
||||
|
||||
await helpers.user.get.prefetch();
|
||||
|
||||
if (!user || (user.role?.name !== "owner" && user.role?.name !== "admin")) {
|
||||
if (!user || user.role === "member") {
|
||||
return {
|
||||
redirect: {
|
||||
permanent: true,
|
||||
|
||||
@@ -31,7 +31,7 @@ export async function getServerSideProps(
|
||||
}
|
||||
const { req, res } = ctx;
|
||||
const { user, session } = await validateRequest(req);
|
||||
if (!user || (user.role?.name !== "owner" && user.role?.name !== "admin")) {
|
||||
if (!user || user.role === "member") {
|
||||
return {
|
||||
redirect: {
|
||||
permanent: true,
|
||||
|
||||
@@ -25,7 +25,7 @@ export async function getServerSideProps(
|
||||
) {
|
||||
const { req, res } = ctx;
|
||||
const { user, session } = await validateRequest(req);
|
||||
if (!user || (user.role?.name !== "owner" && user.role?.name !== "admin")) {
|
||||
if (!user || user.role === "member") {
|
||||
return {
|
||||
redirect: {
|
||||
permanent: true,
|
||||
|
||||
@@ -34,7 +34,7 @@ export async function getServerSideProps(
|
||||
};
|
||||
}
|
||||
const { user, session } = await validateRequest(ctx.req);
|
||||
if (!user || (user.role?.name !== "owner" && user.role?.name !== "admin")) {
|
||||
if (!user || user.role === "member") {
|
||||
return {
|
||||
redirect: {
|
||||
permanent: true,
|
||||
|
||||
@@ -26,7 +26,7 @@ export async function getServerSideProps(
|
||||
) {
|
||||
const { req, res } = ctx;
|
||||
const { user, session } = await validateRequest(req);
|
||||
if (!user || (user.role?.name !== "owner" && user.role?.name !== "admin")) {
|
||||
if (!user || user.role === "member") {
|
||||
return {
|
||||
redirect: {
|
||||
permanent: true,
|
||||
|
||||
@@ -3,7 +3,6 @@ import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
||||
|
||||
import { appRouter } from "@/server/api/root";
|
||||
import { validateRequest } from "@dokploy/server";
|
||||
import { PERMISSIONS } from "@dokploy/server/lib/permissions";
|
||||
import { createServerSideHelpers } from "@trpc/react-query/server";
|
||||
import type { GetServerSidePropsContext } from "next";
|
||||
import type { ReactElement } from "react";
|
||||
@@ -50,12 +49,12 @@ export async function getServerSideProps(
|
||||
try {
|
||||
await helpers.project.all.prefetch();
|
||||
await helpers.settings.isCloud.prefetch();
|
||||
if (user.role?.name === "member" || !user?.role?.isSystem) {
|
||||
if (
|
||||
!user?.role?.permissions?.includes(
|
||||
PERMISSIONS.GIT_PROVIDERS.ACCESS.name,
|
||||
)
|
||||
) {
|
||||
if (user.role === "member") {
|
||||
const userR = await helpers.user.one.fetch({
|
||||
userId: user.id,
|
||||
});
|
||||
|
||||
if (!userR?.canAccessToGitProviders) {
|
||||
return {
|
||||
redirect: {
|
||||
permanent: true,
|
||||
|
||||
@@ -26,7 +26,7 @@ export async function getServerSideProps(
|
||||
) {
|
||||
const { req, res } = ctx;
|
||||
const { user, session } = await validateRequest(req);
|
||||
if (!user || (user.role?.name !== "owner" && user.role?.name !== "admin")) {
|
||||
if (!user || user.role === "member") {
|
||||
return {
|
||||
redirect: {
|
||||
permanent: true,
|
||||
|
||||
@@ -1,23 +1,27 @@
|
||||
import { ShowApiKeys } from "@/components/dashboard/settings/api/show-api-keys";
|
||||
import { ProfileForm } from "@/components/dashboard/settings/profile/profile-form";
|
||||
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
||||
|
||||
import { appRouter } from "@/server/api/root";
|
||||
import { api } from "@/utils/api";
|
||||
import { getLocale, serverSideTranslations } from "@/utils/i18n";
|
||||
import { validateRequest } from "@dokploy/server";
|
||||
import { createServerSideHelpers } from "@trpc/react-query/server";
|
||||
import type { GetServerSidePropsContext } from "next";
|
||||
import type { ReactElement } from "react";
|
||||
import superjson from "superjson";
|
||||
import { Permissions } from "@/components/dashboard/shared/Permissions";
|
||||
|
||||
const Page = () => {
|
||||
const { data } = api.user.get.useQuery();
|
||||
|
||||
// const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||
return (
|
||||
<div className="w-full">
|
||||
<div className="h-full rounded-xl max-w-5xl mx-auto flex flex-col gap-4">
|
||||
<ProfileForm />
|
||||
<Permissions permissions={["api:access"]}>
|
||||
<ShowApiKeys />
|
||||
</Permissions>
|
||||
{(data?.canAccessToAPI || data?.role === "owner") && <ShowApiKeys />}
|
||||
|
||||
{/* {isCloud && <RemoveSelfAccount />} */}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -26,7 +26,7 @@ export async function getServerSideProps(
|
||||
) {
|
||||
const { req, res } = ctx;
|
||||
const { user, session } = await validateRequest(req);
|
||||
if (!user || (user.role?.name !== "owner" && user.role?.name !== "admin")) {
|
||||
if (!user || user.role === "member") {
|
||||
return {
|
||||
redirect: {
|
||||
permanent: true,
|
||||
|
||||
@@ -59,7 +59,7 @@ export async function getServerSideProps(
|
||||
},
|
||||
};
|
||||
}
|
||||
if (user.role?.name === "member" || !user?.role?.isSystem) {
|
||||
if (user.role === "member") {
|
||||
return {
|
||||
redirect: {
|
||||
permanent: true,
|
||||
|
||||
@@ -36,7 +36,7 @@ export async function getServerSideProps(
|
||||
},
|
||||
};
|
||||
}
|
||||
if (user.role?.name === "member" || !user?.role?.isSystem) {
|
||||
if (user.role === "member") {
|
||||
return {
|
||||
redirect: {
|
||||
permanent: true,
|
||||
|
||||
@@ -3,7 +3,6 @@ import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
||||
|
||||
import { appRouter } from "@/server/api/root";
|
||||
import { validateRequest } from "@dokploy/server";
|
||||
import { PERMISSIONS } from "@dokploy/server/lib/permissions";
|
||||
import { createServerSideHelpers } from "@trpc/react-query/server";
|
||||
import type { GetServerSidePropsContext } from "next";
|
||||
import type { ReactElement } from "react";
|
||||
@@ -51,10 +50,12 @@ export async function getServerSideProps(
|
||||
await helpers.project.all.prefetch();
|
||||
await helpers.settings.isCloud.prefetch();
|
||||
|
||||
if (user.role?.name === "member" || !user?.role?.isSystem) {
|
||||
if (
|
||||
!user?.role?.permissions?.includes(PERMISSIONS.SSH_KEYS.ACCESS.name)
|
||||
) {
|
||||
if (user.role === "member") {
|
||||
const userR = await helpers.user.one.fetch({
|
||||
userId: user.id,
|
||||
});
|
||||
|
||||
if (!userR?.canAccessToSSHKeys) {
|
||||
return {
|
||||
redirect: {
|
||||
permanent: true,
|
||||
|
||||
@@ -29,7 +29,7 @@ export async function getServerSideProps(
|
||||
const { req, res } = ctx;
|
||||
const { user, session } = await validateRequest(req);
|
||||
|
||||
if (!user || (user.role?.name !== "owner" && user.role?.name !== "admin")) {
|
||||
if (!user || user.role === "member") {
|
||||
return {
|
||||
redirect: {
|
||||
permanent: true,
|
||||
|
||||
@@ -3,7 +3,6 @@ import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
||||
import { appRouter } from "@/server/api/root";
|
||||
import { IS_CLOUD } from "@dokploy/server/constants";
|
||||
import { validateRequest } from "@dokploy/server/lib/auth";
|
||||
import { PERMISSIONS } from "@dokploy/server/lib/permissions";
|
||||
import { createServerSideHelpers } from "@trpc/react-query/server";
|
||||
import type { GetServerSidePropsContext } from "next";
|
||||
import type { ReactElement } from "react";
|
||||
@@ -54,8 +53,12 @@ export async function getServerSideProps(
|
||||
try {
|
||||
await helpers.project.all.prefetch();
|
||||
|
||||
if (user.role?.name === "member" || !user?.role?.isSystem) {
|
||||
if (!user?.role?.permissions?.includes(PERMISSIONS.DOCKER.VIEW.name)) {
|
||||
if (user.role === "member") {
|
||||
const userR = await helpers.user.one.fetch({
|
||||
userId: user.id,
|
||||
});
|
||||
|
||||
if (!userR?.canAccessToDocker) {
|
||||
return {
|
||||
redirect: {
|
||||
permanent: true,
|
||||
|
||||
@@ -3,7 +3,6 @@ import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
||||
import { appRouter } from "@/server/api/root";
|
||||
import { IS_CLOUD } from "@dokploy/server/constants";
|
||||
import { validateRequest } from "@dokploy/server/lib/auth";
|
||||
import { PERMISSIONS } from "@dokploy/server/lib/permissions";
|
||||
import { createServerSideHelpers } from "@trpc/react-query/server";
|
||||
import type { GetServerSidePropsContext } from "next";
|
||||
import type { ReactElement } from "react";
|
||||
@@ -54,8 +53,12 @@ export async function getServerSideProps(
|
||||
try {
|
||||
await helpers.project.all.prefetch();
|
||||
|
||||
if (user.role?.name === "member" || !user?.role?.isSystem) {
|
||||
if (!user?.role?.permissions?.includes(PERMISSIONS.TRAEFIK.ACCESS.name)) {
|
||||
if (user.role === "member") {
|
||||
const userR = await helpers.user.one.fetch({
|
||||
userId: user.id,
|
||||
});
|
||||
|
||||
if (!userR?.canAccessToTraefikFiles) {
|
||||
return {
|
||||
redirect: {
|
||||
permanent: true,
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { appRouter } from "@/server/api/root";
|
||||
import { api } from "@/utils/api";
|
||||
import { validateRequest } from "@dokploy/server";
|
||||
import { createServerSideHelpers } from "@trpc/react-query/server";
|
||||
import type { GetServerSidePropsContext, NextPage } from "next";
|
||||
import dynamic from "next/dynamic";
|
||||
import "swagger-ui-react/swagger-ui.css";
|
||||
import { useEffect, useState } from "react";
|
||||
import { PERMISSIONS } from "@dokploy/server/lib/permissions";
|
||||
import superjson from "superjson";
|
||||
|
||||
const SwaggerUI = dynamic(() => import("swagger-ui-react"), { ssr: false });
|
||||
|
||||
@@ -69,7 +71,8 @@ const Home: NextPage = () => {
|
||||
|
||||
export default Home;
|
||||
export async function getServerSideProps(context: GetServerSidePropsContext) {
|
||||
const { user } = await validateRequest(context.req);
|
||||
const { req, res } = context;
|
||||
const { user, session } = await validateRequest(context.req);
|
||||
if (!user) {
|
||||
return {
|
||||
redirect: {
|
||||
@@ -78,9 +81,23 @@ 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 (user.role?.name === "member" || !user?.role?.isSystem) {
|
||||
if (!user?.role?.permissions?.includes(PERMISSIONS.API.ACCESS.name)) {
|
||||
if (!userR?.canAccessToAPI) {
|
||||
return {
|
||||
redirect: {
|
||||
permanent: true,
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
import { findOwner } from "@dokploy/server";
|
||||
import { findAdmin } from "@dokploy/server";
|
||||
import { db } from "@dokploy/server/db";
|
||||
import { users } from "@dokploy/server/db/schema";
|
||||
import { users_temp } from "@dokploy/server/db/schema";
|
||||
import { eq } from "drizzle-orm";
|
||||
|
||||
(async () => {
|
||||
try {
|
||||
const result = await findOwner();
|
||||
const result = await findAdmin();
|
||||
|
||||
const update = await db
|
||||
.update(users)
|
||||
.update(users_temp)
|
||||
.set({
|
||||
twoFactorEnabled: false,
|
||||
})
|
||||
.where(eq(users.id, result.userId));
|
||||
.where(eq(users_temp.id, result.userId));
|
||||
|
||||
if (update) {
|
||||
console.log("2FA reset successful");
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { findOwner } from "@dokploy/server";
|
||||
import { findAdmin } from "@dokploy/server";
|
||||
import { generateRandomPassword } from "@dokploy/server";
|
||||
import { db } from "@dokploy/server/db";
|
||||
import { account } from "@dokploy/server/db/schema";
|
||||
@@ -8,7 +8,7 @@ import { eq } from "drizzle-orm";
|
||||
try {
|
||||
const randomPassword = await generateRandomPassword();
|
||||
|
||||
const result = await findOwner();
|
||||
const result = await findAdmin();
|
||||
|
||||
const update = await db
|
||||
.update(account)
|
||||
|
||||
@@ -38,8 +38,6 @@ import { userRouter } from "./routers/user";
|
||||
import { scheduleRouter } from "./routers/schedule";
|
||||
import { rollbackRouter } from "./routers/rollbacks";
|
||||
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.
|
||||
*
|
||||
@@ -86,8 +84,6 @@ export const appRouter = createTRPCRouter({
|
||||
schedule: scheduleRouter,
|
||||
rollback: rollbackRouter,
|
||||
volumeBackups: volumeBackupsRouter,
|
||||
role: roleRouter,
|
||||
webServer: webServerRouter,
|
||||
});
|
||||
|
||||
// export type definition of API
|
||||
|
||||
@@ -3,7 +3,7 @@ import {
|
||||
IS_CLOUD,
|
||||
findUserById,
|
||||
setupWebMonitoring,
|
||||
updateWebServer,
|
||||
updateUser,
|
||||
} from "@dokploy/server";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { adminProcedure, createTRPCRouter } from "../trpc";
|
||||
@@ -27,8 +27,7 @@ export const adminRouter = createTRPCRouter({
|
||||
});
|
||||
}
|
||||
|
||||
await updateWebServer({
|
||||
// @ts-expect-error - TODO: fix this
|
||||
await updateUser(user.id, {
|
||||
metricsConfig: {
|
||||
server: {
|
||||
type: "Dokploy",
|
||||
@@ -53,7 +52,7 @@ export const adminRouter = createTRPCRouter({
|
||||
},
|
||||
});
|
||||
|
||||
const currentServer = await setupWebMonitoring();
|
||||
const currentServer = await setupWebMonitoring(user.id);
|
||||
return currentServer;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
|
||||
@@ -147,10 +147,11 @@ export const aiRouter = createTRPCRouter({
|
||||
serverId: z.string().optional(),
|
||||
}),
|
||||
)
|
||||
.mutation(async ({ input }) => {
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
try {
|
||||
return await suggestVariants({
|
||||
...input,
|
||||
organizationId: ctx.session.activeOrganizationId,
|
||||
});
|
||||
} catch (error) {
|
||||
throw new TRPCError({
|
||||
@@ -162,7 +163,7 @@ export const aiRouter = createTRPCRouter({
|
||||
deploy: protectedProcedure
|
||||
.input(deploySuggestionSchema)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
if (ctx.user.role.name === "member" || !ctx.user.role.isSystem) {
|
||||
if (ctx.user.role === "member") {
|
||||
await checkServiceAccess(
|
||||
ctx.session.activeOrganizationId,
|
||||
input.projectId,
|
||||
@@ -215,7 +216,7 @@ export const aiRouter = createTRPCRouter({
|
||||
}
|
||||
}
|
||||
|
||||
if (ctx.user.role.name === "member" || !ctx.user.role.isSystem) {
|
||||
if (ctx.user.role === "member") {
|
||||
await addNewService(
|
||||
ctx.session.activeOrganizationId,
|
||||
ctx.user.ownerId,
|
||||
|
||||
@@ -63,7 +63,7 @@ export const applicationRouter = createTRPCRouter({
|
||||
.input(apiCreateApplication)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
if (ctx.user.role.name === "member" || !ctx.user.role.isSystem) {
|
||||
if (ctx.user.role === "member") {
|
||||
await checkServiceAccess(
|
||||
ctx.user.id,
|
||||
input.projectId,
|
||||
@@ -88,7 +88,7 @@ export const applicationRouter = createTRPCRouter({
|
||||
}
|
||||
const newApplication = await createApplication(input);
|
||||
|
||||
if (ctx.user.role.name === "member" || !ctx.user.role.isSystem) {
|
||||
if (ctx.user.role === "member") {
|
||||
await addNewService(
|
||||
ctx.user.id,
|
||||
newApplication.applicationId,
|
||||
@@ -110,7 +110,7 @@ export const applicationRouter = createTRPCRouter({
|
||||
one: protectedProcedure
|
||||
.input(apiFindOneApplication)
|
||||
.query(async ({ input, ctx }) => {
|
||||
if (ctx.user.role.name === "member" || !ctx.user.role.isSystem) {
|
||||
if (ctx.user.role === "member") {
|
||||
await checkServiceAccess(
|
||||
ctx.user.id,
|
||||
input.applicationId,
|
||||
@@ -201,7 +201,7 @@ export const applicationRouter = createTRPCRouter({
|
||||
delete: protectedProcedure
|
||||
.input(apiFindOneApplication)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
if (ctx.user.role.name === "member" || !ctx.user.role.isSystem) {
|
||||
if (ctx.user.role === "member") {
|
||||
await checkServiceAccess(
|
||||
ctx.user.id,
|
||||
input.applicationId,
|
||||
|
||||
@@ -31,7 +31,7 @@ import {
|
||||
findGitProviderById,
|
||||
findProjectById,
|
||||
findServerById,
|
||||
findWebServer,
|
||||
findUserById,
|
||||
getComposeContainer,
|
||||
loadServices,
|
||||
randomizeComposeFile,
|
||||
@@ -64,7 +64,7 @@ export const composeRouter = createTRPCRouter({
|
||||
.input(apiCreateCompose)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
try {
|
||||
if (ctx.user.role.name === "member" || !ctx.user.role.isSystem) {
|
||||
if (ctx.user.role === "member") {
|
||||
await checkServiceAccess(
|
||||
ctx.user.id,
|
||||
input.projectId,
|
||||
@@ -88,7 +88,7 @@ export const composeRouter = createTRPCRouter({
|
||||
}
|
||||
const newService = await createCompose(input);
|
||||
|
||||
if (ctx.user.role.name === "member" || !ctx.user.role.isSystem) {
|
||||
if (ctx.user.role === "member") {
|
||||
await addNewService(
|
||||
ctx.user.id,
|
||||
newService.composeId,
|
||||
@@ -105,7 +105,7 @@ export const composeRouter = createTRPCRouter({
|
||||
one: protectedProcedure
|
||||
.input(apiFindCompose)
|
||||
.query(async ({ input, ctx }) => {
|
||||
if (ctx.user.role.name === "member" || !ctx.user.role.isSystem) {
|
||||
if (ctx.user.role === "member") {
|
||||
await checkServiceAccess(
|
||||
ctx.user.id,
|
||||
input.composeId,
|
||||
@@ -177,7 +177,7 @@ export const composeRouter = createTRPCRouter({
|
||||
delete: protectedProcedure
|
||||
.input(apiDeleteCompose)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
if (ctx.user.role.name === "member" || !ctx.user.role.isSystem) {
|
||||
if (ctx.user.role === "member") {
|
||||
await checkServiceAccess(
|
||||
ctx.user.id,
|
||||
input.composeId,
|
||||
@@ -469,7 +469,7 @@ export const composeRouter = createTRPCRouter({
|
||||
}),
|
||||
)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
if (ctx.user.role.name === "member" || !ctx.user.role.isSystem) {
|
||||
if (ctx.user.role === "member") {
|
||||
await checkServiceAccess(
|
||||
ctx.user.id,
|
||||
input.projectId,
|
||||
@@ -487,8 +487,8 @@ export const composeRouter = createTRPCRouter({
|
||||
|
||||
const template = await fetchTemplateFiles(input.id, input.baseUrl);
|
||||
|
||||
const webServer = await findWebServer();
|
||||
let serverIp = webServer.serverIp || "127.0.0.1";
|
||||
const admin = await findUserById(ctx.user.ownerId);
|
||||
let serverIp = admin.serverIp || "127.0.0.1";
|
||||
|
||||
const project = await findProjectById(input.projectId);
|
||||
|
||||
@@ -524,7 +524,7 @@ export const composeRouter = createTRPCRouter({
|
||||
isolatedDeployment: true,
|
||||
});
|
||||
|
||||
if (ctx.user.role.name === "member" || !ctx.user.role.isSystem) {
|
||||
if (ctx.user.role === "member") {
|
||||
await addNewService(
|
||||
ctx.user.id,
|
||||
compose.composeId,
|
||||
@@ -709,8 +709,8 @@ export const composeRouter = createTRPCRouter({
|
||||
const decodedData = Buffer.from(input.base64, "base64").toString(
|
||||
"utf-8",
|
||||
);
|
||||
const webServer = await findWebServer();
|
||||
let serverIp = webServer.serverIp || "127.0.0.1";
|
||||
const admin = await findUserById(ctx.user.ownerId);
|
||||
let serverIp = admin.serverIp || "127.0.0.1";
|
||||
|
||||
if (compose.serverId) {
|
||||
const server = await findServerById(compose.serverId);
|
||||
@@ -785,8 +785,8 @@ export const composeRouter = createTRPCRouter({
|
||||
await removeDomainById(domain.domainId);
|
||||
}
|
||||
|
||||
const webServer = await findWebServer();
|
||||
let serverIp = webServer.serverIp || "127.0.0.1";
|
||||
const admin = await findUserById(ctx.user.ownerId);
|
||||
let serverIp = admin.serverIp || "127.0.0.1";
|
||||
|
||||
if (compose.serverId) {
|
||||
const server = await findServerById(compose.serverId);
|
||||
|
||||
@@ -13,9 +13,9 @@ import {
|
||||
findDomainById,
|
||||
findDomainsByApplicationId,
|
||||
findDomainsByComposeId,
|
||||
findOrganizationById,
|
||||
findPreviewDeploymentById,
|
||||
findServerById,
|
||||
findWebServer,
|
||||
generateTraefikMeDomain,
|
||||
manageDomain,
|
||||
removeDomain,
|
||||
@@ -93,19 +93,25 @@ export const domainRouter = createTRPCRouter({
|
||||
}),
|
||||
generateDomain: protectedProcedure
|
||||
.input(z.object({ appName: z.string(), serverId: z.string().optional() }))
|
||||
.mutation(async ({ input }) => {
|
||||
return generateTraefikMeDomain(input.appName, input.serverId);
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
return generateTraefikMeDomain(
|
||||
input.appName,
|
||||
ctx.user.ownerId,
|
||||
input.serverId,
|
||||
);
|
||||
}),
|
||||
canGenerateTraefikMeDomains: protectedProcedure
|
||||
.input(z.object({ serverId: z.string() }))
|
||||
.query(async ({ input }) => {
|
||||
const webServer = await findWebServer();
|
||||
.query(async ({ input, ctx }) => {
|
||||
const organization = await findOrganizationById(
|
||||
ctx.session.activeOrganizationId,
|
||||
);
|
||||
|
||||
if (input.serverId) {
|
||||
const server = await findServerById(input.serverId);
|
||||
return server.ipAddress;
|
||||
}
|
||||
return webServer?.serverIp;
|
||||
return organization?.owner.serverIp;
|
||||
}),
|
||||
|
||||
update: protectedProcedure
|
||||
|
||||
@@ -41,7 +41,7 @@ export const mariadbRouter = createTRPCRouter({
|
||||
.input(apiCreateMariaDB)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
if (ctx.user.role.name === "member" || !ctx.user.role.isSystem) {
|
||||
if (ctx.user.role === "member") {
|
||||
await checkServiceAccess(
|
||||
ctx.user.id,
|
||||
input.projectId,
|
||||
@@ -65,7 +65,7 @@ export const mariadbRouter = createTRPCRouter({
|
||||
});
|
||||
}
|
||||
const newMariadb = await createMariadb(input);
|
||||
if (ctx.user.role.name === "member" || !ctx.user.role.isSystem) {
|
||||
if (ctx.user.role === "member") {
|
||||
await addNewService(
|
||||
ctx.user.id,
|
||||
newMariadb.mariadbId,
|
||||
@@ -92,7 +92,7 @@ export const mariadbRouter = createTRPCRouter({
|
||||
one: protectedProcedure
|
||||
.input(apiFindOneMariaDB)
|
||||
.query(async ({ input, ctx }) => {
|
||||
if (ctx.user.role.name === "member" || !ctx.user.role.isSystem) {
|
||||
if (ctx.user.role === "member") {
|
||||
await checkServiceAccess(
|
||||
ctx.user.id,
|
||||
input.mariadbId,
|
||||
@@ -219,7 +219,7 @@ export const mariadbRouter = createTRPCRouter({
|
||||
remove: protectedProcedure
|
||||
.input(apiFindOneMariaDB)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
if (ctx.user.role.name === "member" || !ctx.user.role.isSystem) {
|
||||
if (ctx.user.role === "member") {
|
||||
await checkServiceAccess(
|
||||
ctx.user.id,
|
||||
input.mariadbId,
|
||||
|
||||
@@ -41,7 +41,7 @@ export const mongoRouter = createTRPCRouter({
|
||||
.input(apiCreateMongo)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
if (ctx.user.role.name === "member" || !ctx.user.role.isSystem) {
|
||||
if (ctx.user.role === "member") {
|
||||
await checkServiceAccess(
|
||||
ctx.user.id,
|
||||
input.projectId,
|
||||
@@ -65,7 +65,7 @@ export const mongoRouter = createTRPCRouter({
|
||||
});
|
||||
}
|
||||
const newMongo = await createMongo(input);
|
||||
if (ctx.user.role.name === "member" || !ctx.user.role.isSystem) {
|
||||
if (ctx.user.role === "member") {
|
||||
await addNewService(
|
||||
ctx.user.id,
|
||||
newMongo.mongoId,
|
||||
@@ -96,7 +96,7 @@ export const mongoRouter = createTRPCRouter({
|
||||
one: protectedProcedure
|
||||
.input(apiFindOneMongo)
|
||||
.query(async ({ input, ctx }) => {
|
||||
if (ctx.user.role.name === "member" || !ctx.user.role.isSystem) {
|
||||
if (ctx.user.role === "member") {
|
||||
await checkServiceAccess(
|
||||
ctx.user.id,
|
||||
input.mongoId,
|
||||
@@ -261,7 +261,7 @@ export const mongoRouter = createTRPCRouter({
|
||||
remove: protectedProcedure
|
||||
.input(apiFindOneMongo)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
if (ctx.user.role.name === "member" || !ctx.user.role.isSystem) {
|
||||
if (ctx.user.role === "member") {
|
||||
await checkServiceAccess(
|
||||
ctx.user.id,
|
||||
input.mongoId,
|
||||
|
||||
@@ -44,7 +44,7 @@ export const mysqlRouter = createTRPCRouter({
|
||||
.input(apiCreateMySql)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
if (ctx.user.role.name === "member" || !ctx.user.role.isSystem) {
|
||||
if (ctx.user.role === "member") {
|
||||
await checkServiceAccess(
|
||||
ctx.user.id,
|
||||
input.projectId,
|
||||
@@ -69,7 +69,7 @@ export const mysqlRouter = createTRPCRouter({
|
||||
}
|
||||
|
||||
const newMysql = await createMysql(input);
|
||||
if (ctx.user.role.name === "member" || !ctx.user.role.isSystem) {
|
||||
if (ctx.user.role === "member") {
|
||||
await addNewService(
|
||||
ctx.user.id,
|
||||
newMysql.mysqlId,
|
||||
@@ -100,7 +100,7 @@ export const mysqlRouter = createTRPCRouter({
|
||||
one: protectedProcedure
|
||||
.input(apiFindOneMySql)
|
||||
.query(async ({ input, ctx }) => {
|
||||
if (ctx.user.role.name === "member" || !ctx.user.role.isSystem) {
|
||||
if (ctx.user.role === "member") {
|
||||
await checkServiceAccess(
|
||||
ctx.user.id,
|
||||
input.mysqlId,
|
||||
@@ -260,7 +260,7 @@ export const mysqlRouter = createTRPCRouter({
|
||||
remove: protectedProcedure
|
||||
.input(apiFindOneMySql)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
if (ctx.user.role.name === "member" || !ctx.user.role.isSystem) {
|
||||
if (ctx.user.role === "member") {
|
||||
await checkServiceAccess(
|
||||
ctx.user.id,
|
||||
input.mysqlId,
|
||||
|
||||
@@ -24,7 +24,7 @@ import {
|
||||
apiUpdateTelegram,
|
||||
notifications,
|
||||
server,
|
||||
webServer,
|
||||
users_temp,
|
||||
} from "@/server/db/schema";
|
||||
import {
|
||||
IS_CLOUD,
|
||||
@@ -345,19 +345,19 @@ export const notificationRouter = createTRPCRouter({
|
||||
if (input.ServerType === "Dokploy") {
|
||||
const result = await db
|
||||
.select()
|
||||
.from(webServer)
|
||||
.from(users_temp)
|
||||
.where(
|
||||
sql`${webServer.metricsConfig}::jsonb -> 'server' ->> 'token' = ${input.Token}`,
|
||||
sql`${users_temp.metricsConfig}::jsonb -> 'server' ->> 'token' = ${input.Token}`,
|
||||
);
|
||||
|
||||
if (!result?.[0]?.webServerId) {
|
||||
if (!result?.[0]?.id) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Token not found",
|
||||
});
|
||||
}
|
||||
|
||||
organizationId = result?.[0]?.webServerId;
|
||||
organizationId = result?.[0]?.id;
|
||||
ServerName = "Dokploy";
|
||||
} else {
|
||||
const result = await db
|
||||
|
||||
@@ -32,6 +32,8 @@ export const organizationRouter = createTRPCRouter({
|
||||
.returning()
|
||||
.then((res) => res[0]);
|
||||
|
||||
console.log("result", result);
|
||||
|
||||
if (!result) {
|
||||
throw new TRPCError({
|
||||
code: "INTERNAL_SERVER_ERROR",
|
||||
|
||||
@@ -41,7 +41,7 @@ export const postgresRouter = createTRPCRouter({
|
||||
.input(apiCreatePostgres)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
if (ctx.user.role.name === "member" || !ctx.user.role.isSystem) {
|
||||
if (ctx.user.role === "member") {
|
||||
await checkServiceAccess(
|
||||
ctx.user.id,
|
||||
input.projectId,
|
||||
@@ -65,7 +65,7 @@ export const postgresRouter = createTRPCRouter({
|
||||
});
|
||||
}
|
||||
const newPostgres = await createPostgres(input);
|
||||
if (ctx.user.role.name === "member" || !ctx.user.role.isSystem) {
|
||||
if (ctx.user.role === "member") {
|
||||
await addNewService(
|
||||
ctx.user.id,
|
||||
newPostgres.postgresId,
|
||||
@@ -96,7 +96,7 @@ export const postgresRouter = createTRPCRouter({
|
||||
one: protectedProcedure
|
||||
.input(apiFindOnePostgres)
|
||||
.query(async ({ input, ctx }) => {
|
||||
if (ctx.user.role.name === "member" || !ctx.user.role.isSystem) {
|
||||
if (ctx.user.role === "member") {
|
||||
await checkServiceAccess(
|
||||
ctx.user.id,
|
||||
input.postgresId,
|
||||
@@ -244,7 +244,7 @@ export const postgresRouter = createTRPCRouter({
|
||||
remove: protectedProcedure
|
||||
.input(apiFindOnePostgres)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
if (ctx.user.role.name === "member" || !ctx.user.role.isSystem) {
|
||||
if (ctx.user.role === "member") {
|
||||
await checkServiceAccess(
|
||||
ctx.user.id,
|
||||
input.postgresId,
|
||||
|
||||
@@ -1,13 +1,23 @@
|
||||
import { apiFindAllByApplication } from "@/server/db/schema";
|
||||
import { db } from "@/server/db";
|
||||
import { apiFindAllByApplication, applications } from "@/server/db/schema";
|
||||
import {
|
||||
createPreviewDeployment,
|
||||
findApplicationById,
|
||||
findPreviewDeploymentByApplicationId,
|
||||
findPreviewDeploymentById,
|
||||
findPreviewDeploymentsByApplicationId,
|
||||
findPreviewDeploymentsByPullRequestId,
|
||||
IS_CLOUD,
|
||||
removePreviewDeployment,
|
||||
} from "@dokploy/server";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { z } from "zod";
|
||||
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({
|
||||
all: protectedProcedure
|
||||
@@ -59,4 +69,142 @@ export const previewDeploymentRouter = createTRPCRouter({
|
||||
}
|
||||
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)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
try {
|
||||
if (ctx.user.role.name === "member" || !ctx.user.role.isSystem) {
|
||||
if (ctx.user.role === "member") {
|
||||
await checkProjectAccess(
|
||||
ctx.user.id,
|
||||
"create",
|
||||
@@ -78,7 +78,7 @@ export const projectRouter = createTRPCRouter({
|
||||
input,
|
||||
ctx.session.activeOrganizationId,
|
||||
);
|
||||
if (ctx.user.role.name === "member" || !ctx.user.role.isSystem) {
|
||||
if (ctx.user.role === "member") {
|
||||
await addNewProject(
|
||||
ctx.user.id,
|
||||
project.projectId,
|
||||
@@ -99,7 +99,7 @@ export const projectRouter = createTRPCRouter({
|
||||
one: protectedProcedure
|
||||
.input(apiFindOneProject)
|
||||
.query(async ({ input, ctx }) => {
|
||||
if (ctx.user.role.name === "member" || !ctx.user.role.isSystem) {
|
||||
if (ctx.user.role === "member") {
|
||||
const { accessedServices } = await findMemberById(
|
||||
ctx.user.id,
|
||||
ctx.session.activeOrganizationId,
|
||||
@@ -164,7 +164,7 @@ export const projectRouter = createTRPCRouter({
|
||||
return project;
|
||||
}),
|
||||
all: protectedProcedure.query(async ({ ctx }) => {
|
||||
if (ctx.user.role.name === "member" || !ctx.user.role.isSystem) {
|
||||
if (ctx.user.role === "member") {
|
||||
const { accessedProjects, accessedServices } = await findMemberById(
|
||||
ctx.user.id,
|
||||
ctx.session.activeOrganizationId,
|
||||
@@ -241,7 +241,7 @@ export const projectRouter = createTRPCRouter({
|
||||
.input(apiRemoveProject)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
if (ctx.user.role.name === "member" || !ctx.user.role.isSystem) {
|
||||
if (ctx.user.role === "member") {
|
||||
await checkProjectAccess(
|
||||
ctx.user.id,
|
||||
"delete",
|
||||
@@ -314,7 +314,7 @@ export const projectRouter = createTRPCRouter({
|
||||
)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
try {
|
||||
if (ctx.user.role.name === "member" || !ctx.user.role.isSystem) {
|
||||
if (ctx.user.role === "member") {
|
||||
await checkProjectAccess(
|
||||
ctx.user.id,
|
||||
"create",
|
||||
@@ -649,10 +649,7 @@ export const projectRouter = createTRPCRouter({
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
!input.duplicateInSameProject &&
|
||||
(ctx.user.role.name === "member" || !ctx.user.role.isSystem)
|
||||
) {
|
||||
if (!input.duplicateInSameProject && ctx.user.role === "member") {
|
||||
await addNewProject(
|
||||
ctx.user.id,
|
||||
targetProject.projectId,
|
||||
|
||||
@@ -41,7 +41,7 @@ export const redisRouter = createTRPCRouter({
|
||||
.input(apiCreateRedis)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
if (ctx.user.role.name === "member" || !ctx.user.role.isSystem) {
|
||||
if (ctx.user.role === "member") {
|
||||
await checkServiceAccess(
|
||||
ctx.user.id,
|
||||
input.projectId,
|
||||
@@ -65,7 +65,7 @@ export const redisRouter = createTRPCRouter({
|
||||
});
|
||||
}
|
||||
const newRedis = await createRedis(input);
|
||||
if (ctx.user.role.name === "member" || !ctx.user.role.isSystem) {
|
||||
if (ctx.user.role === "member") {
|
||||
await addNewService(
|
||||
ctx.user.id,
|
||||
newRedis.redisId,
|
||||
@@ -89,7 +89,7 @@ export const redisRouter = createTRPCRouter({
|
||||
one: protectedProcedure
|
||||
.input(apiFindOneRedis)
|
||||
.query(async ({ input, ctx }) => {
|
||||
if (ctx.user.role.name === "member" || !ctx.user.role.isSystem) {
|
||||
if (ctx.user.role === "member") {
|
||||
await checkServiceAccess(
|
||||
ctx.user.id,
|
||||
input.redisId,
|
||||
@@ -251,7 +251,7 @@ export const redisRouter = createTRPCRouter({
|
||||
remove: protectedProcedure
|
||||
.input(apiFindOneRedis)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
if (ctx.user.role.name === "member" || !ctx.user.role.isSystem) {
|
||||
if (ctx.user.role === "member") {
|
||||
await checkServiceAccess(
|
||||
ctx.user.id,
|
||||
input.redisId,
|
||||
|
||||
@@ -1,75 +0,0 @@
|
||||
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,12 +1,16 @@
|
||||
import { db } from "@/server/db";
|
||||
import {
|
||||
apiAssignDomain,
|
||||
apiEnableDashboard,
|
||||
apiModifyTraefikConfig,
|
||||
apiReadStatsLogs,
|
||||
apiReadTraefikConfig,
|
||||
apiSaveSSHKey,
|
||||
apiServerSchema,
|
||||
apiTraefikConfig,
|
||||
apiUpdateDockerCleanup,
|
||||
} from "@/server/db/schema";
|
||||
import { removeJob, schedule } from "@/server/utils/backup";
|
||||
import {
|
||||
DEFAULT_UPDATE_DATA,
|
||||
IS_CLOUD,
|
||||
@@ -19,6 +23,7 @@ import {
|
||||
execAsync,
|
||||
execAsyncRemote,
|
||||
findServerById,
|
||||
findUserById,
|
||||
getDokployImage,
|
||||
getDokployImageTag,
|
||||
getLogCleanupStatus,
|
||||
@@ -35,9 +40,14 @@ import {
|
||||
readMainConfig,
|
||||
readMonitoringConfig,
|
||||
recreateDirectory,
|
||||
sendDockerCleanupNotifications,
|
||||
spawnAsync,
|
||||
startLogCleanup,
|
||||
stopLogCleanup,
|
||||
updateLetsEncryptEmail,
|
||||
updateServerById,
|
||||
updateServerTraefik,
|
||||
updateUser,
|
||||
writeConfig,
|
||||
writeMainConfig,
|
||||
writeTraefikConfigInPath,
|
||||
@@ -47,6 +57,7 @@ import { generateOpenApiDocument } from "@dokploy/trpc-openapi";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { sql } from "drizzle-orm";
|
||||
import { dump, load } from "js-yaml";
|
||||
import { scheduleJob, scheduledJobs } from "node-schedule";
|
||||
import { z } from "zod";
|
||||
import packageInfo from "../../../package.json";
|
||||
import { appRouter } from "../root";
|
||||
@@ -176,6 +187,135 @@ export const settingsRouter = createTRPCRouter({
|
||||
await recreateDirectory(MONITORING_PATH);
|
||||
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(() => {
|
||||
if (IS_CLOUD) {
|
||||
@@ -330,6 +470,13 @@ export const settingsRouter = createTRPCRouter({
|
||||
|
||||
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(
|
||||
async ({ ctx }): Promise<unknown> => {
|
||||
@@ -494,16 +641,10 @@ export const settingsRouter = createTRPCRouter({
|
||||
},
|
||||
})
|
||||
.input(
|
||||
z
|
||||
.object({
|
||||
dateRange: z
|
||||
.object({
|
||||
start: z.string().optional(),
|
||||
end: z.string().optional(),
|
||||
})
|
||||
.optional(),
|
||||
})
|
||||
.optional(),
|
||||
z.object({
|
||||
start: z.string().optional(),
|
||||
end: z.string().optional(),
|
||||
}),
|
||||
)
|
||||
.query(async ({ input }) => {
|
||||
if (IS_CLOUD) {
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import {
|
||||
IS_CLOUD,
|
||||
createApiKey,
|
||||
findAdmin,
|
||||
findNotificationById,
|
||||
findOrganizationById,
|
||||
findUserById,
|
||||
getUserByToken,
|
||||
removeUserById,
|
||||
sendEmailNotification,
|
||||
updateUser,
|
||||
findWebServer,
|
||||
} from "@dokploy/server";
|
||||
import { db } from "@dokploy/server/db";
|
||||
import {
|
||||
@@ -29,7 +30,6 @@ import {
|
||||
protectedProcedure,
|
||||
publicProcedure,
|
||||
} from "../trpc";
|
||||
import { sendEmail } from "@dokploy/server/verification/send-verification-email";
|
||||
|
||||
const apiCreateApiKey = z.object({
|
||||
name: z.string().min(1),
|
||||
@@ -54,7 +54,6 @@ export const userRouter = createTRPCRouter({
|
||||
where: eq(member.organizationId, ctx.session.activeOrganizationId),
|
||||
with: {
|
||||
user: true,
|
||||
role: true,
|
||||
},
|
||||
orderBy: [asc(member.createdAt)],
|
||||
});
|
||||
@@ -87,10 +86,7 @@ export const userRouter = createTRPCRouter({
|
||||
// Allow access if:
|
||||
// 1. User is requesting their own information
|
||||
// 2. User has owner role (admin permissions) AND user is in the same organization
|
||||
if (
|
||||
memberResult.userId !== ctx.user.id &&
|
||||
ctx.user.role?.name !== "owner"
|
||||
) {
|
||||
if (memberResult.userId !== ctx.user.id && ctx.user.role !== "owner") {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to access this user",
|
||||
@@ -106,7 +102,6 @@ export const userRouter = createTRPCRouter({
|
||||
eq(member.organizationId, ctx.session?.activeOrganizationId || ""),
|
||||
),
|
||||
with: {
|
||||
role: true,
|
||||
user: {
|
||||
with: {
|
||||
apiKeys: true,
|
||||
@@ -152,6 +147,19 @@ export const userRouter = createTRPCRouter({
|
||||
|
||||
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
|
||||
.input(apiUpdateUser)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
@@ -191,6 +199,14 @@ export const userRouter = createTRPCRouter({
|
||||
.query(async ({ input }) => {
|
||||
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
|
||||
.input(
|
||||
z.object({
|
||||
@@ -367,83 +383,6 @@ export const userRouter = createTRPCRouter({
|
||||
|
||||
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
|
||||
.input(
|
||||
z.object({
|
||||
@@ -471,11 +410,11 @@ export const userRouter = createTRPCRouter({
|
||||
});
|
||||
}
|
||||
|
||||
const webServer = await findWebServer();
|
||||
const admin = await findAdmin();
|
||||
const host =
|
||||
process.env.NODE_ENV === "development"
|
||||
? "http://localhost:3000"
|
||||
: webServer.host;
|
||||
: admin.user.host;
|
||||
const inviteLink = `${host}/invitation?token=${input.invitationId}`;
|
||||
|
||||
const organization = await findOrganizationById(
|
||||
@@ -499,52 +438,4 @@ export const userRouter = createTRPCRouter({
|
||||
}
|
||||
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;
|
||||
}
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -1,169 +0,0 @@
|
||||
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,6 +98,7 @@ export const deploymentWorker = new Worker(
|
||||
titleLog: job.data.titleLog,
|
||||
descriptionLog: job.data.descriptionLog,
|
||||
previewDeploymentId: job.data.previewDeploymentId,
|
||||
isExternal: job.data.isExternal,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
@@ -107,6 +108,7 @@ export const deploymentWorker = new Worker(
|
||||
titleLog: job.data.titleLog,
|
||||
descriptionLog: job.data.descriptionLog,
|
||||
previewDeploymentId: job.data.previewDeploymentId,
|
||||
isExternal: job.data.isExternal,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ type DeployJob =
|
||||
applicationType: "application-preview";
|
||||
previewDeploymentId: string;
|
||||
serverId?: string;
|
||||
isExternal?: boolean;
|
||||
};
|
||||
|
||||
export type DeploymentJob = DeployJob;
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
// boolean,
|
||||
// } from "drizzle-orm/pg-core";
|
||||
|
||||
// export const users = pgTable("users", {
|
||||
// export const users_temp = pgTable("users_temp", {
|
||||
// id: text("id").primaryKey(),
|
||||
// name: text("name").notNull(),
|
||||
// email: text("email").notNull().unique(),
|
||||
@@ -29,7 +29,7 @@
|
||||
// userAgent: text("user_agent"),
|
||||
// userId: text("user_id")
|
||||
// .notNull()
|
||||
// .references(() => users.id, { onDelete: "cascade" }),
|
||||
// .references(() => users_temp.id, { onDelete: "cascade" }),
|
||||
// activeOrganizationId: text("active_organization_id"),
|
||||
// });
|
||||
|
||||
@@ -39,7 +39,7 @@
|
||||
// providerId: text("provider_id").notNull(),
|
||||
// userId: text("user_id")
|
||||
// .notNull()
|
||||
// .references(() => users.id, { onDelete: "cascade" }),
|
||||
// .references(() => users_temp.id, { onDelete: "cascade" }),
|
||||
// accessToken: text("access_token"),
|
||||
// refreshToken: text("refresh_token"),
|
||||
// idToken: text("id_token"),
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
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,8 +9,7 @@ import {
|
||||
import { nanoid } from "nanoid";
|
||||
import { projects } from "./project";
|
||||
import { server } from "./server";
|
||||
import { users } from "./user";
|
||||
// import { role } from "./rbac";
|
||||
import { users_temp } from "./user";
|
||||
|
||||
export const account = pgTable("account", {
|
||||
id: text("id")
|
||||
@@ -22,7 +21,7 @@ export const account = pgTable("account", {
|
||||
providerId: text("provider_id").notNull(),
|
||||
userId: text("user_id")
|
||||
.notNull()
|
||||
.references(() => users.id, { onDelete: "cascade" }),
|
||||
.references(() => users_temp.id, { onDelete: "cascade" }),
|
||||
accessToken: text("access_token"),
|
||||
refreshToken: text("refresh_token"),
|
||||
idToken: text("id_token"),
|
||||
@@ -40,9 +39,9 @@ export const account = pgTable("account", {
|
||||
});
|
||||
|
||||
export const accountRelations = relations(account, ({ one }) => ({
|
||||
user: one(users, {
|
||||
user: one(users_temp, {
|
||||
fields: [account.userId],
|
||||
references: [users.id],
|
||||
references: [users_temp.id],
|
||||
}),
|
||||
}));
|
||||
|
||||
@@ -66,15 +65,15 @@ export const organization = pgTable("organization", {
|
||||
metadata: text("metadata"),
|
||||
ownerId: text("owner_id")
|
||||
.notNull()
|
||||
.references(() => users.id, { onDelete: "cascade" }),
|
||||
.references(() => users_temp.id, { onDelete: "cascade" }),
|
||||
});
|
||||
|
||||
export const organizationRelations = relations(
|
||||
organization,
|
||||
({ one, many }) => ({
|
||||
owner: one(users, {
|
||||
owner: one(users_temp, {
|
||||
fields: [organization.ownerId],
|
||||
references: [users.id],
|
||||
references: [users_temp.id],
|
||||
}),
|
||||
servers: many(server),
|
||||
projects: many(projects),
|
||||
@@ -91,12 +90,24 @@ export const member = pgTable("member", {
|
||||
.references(() => organization.id, { onDelete: "cascade" }),
|
||||
userId: text("user_id")
|
||||
.notNull()
|
||||
.references(() => users.id, { onDelete: "cascade" }),
|
||||
role: text("role").$type<"owner" | "member" | "admin">(),
|
||||
// roleId: text("roleId").references(() => role.roleId, { onDelete: "cascade" }),
|
||||
.references(() => users_temp.id, { onDelete: "cascade" }),
|
||||
role: text("role").notNull().$type<"owner" | "member" | "admin">(),
|
||||
createdAt: timestamp("created_at").notNull(),
|
||||
teamId: text("team_id"),
|
||||
// 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")
|
||||
.array()
|
||||
.notNull()
|
||||
@@ -112,30 +123,24 @@ export const memberRelations = relations(member, ({ one }) => ({
|
||||
fields: [member.organizationId],
|
||||
references: [organization.id],
|
||||
}),
|
||||
user: one(users, {
|
||||
user: one(users_temp, {
|
||||
fields: [member.userId],
|
||||
references: [users.id],
|
||||
references: [users_temp.id],
|
||||
}),
|
||||
// role: one(role, {
|
||||
// fields: [member.roleId],
|
||||
// references: [role.roleId],
|
||||
// }),
|
||||
}));
|
||||
|
||||
export const invitation = pgTable("invitation", {
|
||||
id: text("id")
|
||||
.primaryKey()
|
||||
.$defaultFn(() => nanoid()),
|
||||
id: text("id").primaryKey(),
|
||||
organizationId: text("organization_id")
|
||||
.notNull()
|
||||
.references(() => organization.id, { onDelete: "cascade" }),
|
||||
email: text("email").notNull(),
|
||||
role: text("role"),
|
||||
role: text("role").$type<"owner" | "member" | "admin">(),
|
||||
status: text("status").notNull(),
|
||||
expiresAt: timestamp("expires_at").notNull(),
|
||||
inviterId: text("inviter_id")
|
||||
.notNull()
|
||||
.references(() => users.id, { onDelete: "cascade" }),
|
||||
.references(() => users_temp.id, { onDelete: "cascade" }),
|
||||
teamId: text("team_id"),
|
||||
});
|
||||
|
||||
@@ -152,7 +157,7 @@ export const twoFactor = pgTable("two_factor", {
|
||||
backupCodes: text("backup_codes").notNull(),
|
||||
userId: text("user_id")
|
||||
.notNull()
|
||||
.references(() => users.id, { onDelete: "cascade" }),
|
||||
.references(() => users_temp.id, { onDelete: "cascade" }),
|
||||
});
|
||||
|
||||
export const apikey = pgTable("apikey", {
|
||||
@@ -163,7 +168,7 @@ export const apikey = pgTable("apikey", {
|
||||
key: text("key").notNull(),
|
||||
userId: text("user_id")
|
||||
.notNull()
|
||||
.references(() => users.id, { onDelete: "cascade" }),
|
||||
.references(() => users_temp.id, { onDelete: "cascade" }),
|
||||
refillInterval: integer("refill_interval"),
|
||||
refillAmount: integer("refill_amount"),
|
||||
lastRefillAt: timestamp("last_refill_at"),
|
||||
@@ -182,8 +187,8 @@ export const apikey = pgTable("apikey", {
|
||||
});
|
||||
|
||||
export const apikeyRelations = relations(apikey, ({ one }) => ({
|
||||
user: one(users, {
|
||||
user: one(users_temp, {
|
||||
fields: [apikey.userId],
|
||||
references: [users.id],
|
||||
references: [users_temp.id],
|
||||
}),
|
||||
}));
|
||||
|
||||
@@ -19,7 +19,7 @@ import { mariadb } from "./mariadb";
|
||||
import { mongo } from "./mongo";
|
||||
import { mysql } from "./mysql";
|
||||
import { postgres } from "./postgres";
|
||||
import { users } from "./user";
|
||||
import { users_temp } from "./user";
|
||||
export const databaseType = pgEnum("databaseType", [
|
||||
"postgres",
|
||||
"mariadb",
|
||||
@@ -74,7 +74,7 @@ export const backups = pgTable("backup", {
|
||||
mongoId: text("mongoId").references((): AnyPgColumn => mongo.mongoId, {
|
||||
onDelete: "cascade",
|
||||
}),
|
||||
userId: text("userId").references(() => users.id),
|
||||
userId: text("userId").references(() => users_temp.id),
|
||||
// Only for compose backups
|
||||
metadata: jsonb("metadata").$type<
|
||||
| {
|
||||
@@ -118,9 +118,9 @@ export const backupsRelations = relations(backups, ({ one, many }) => ({
|
||||
fields: [backups.mongoId],
|
||||
references: [mongo.mongoId],
|
||||
}),
|
||||
user: one(users, {
|
||||
user: one(users_temp, {
|
||||
fields: [backups.userId],
|
||||
references: [users.id],
|
||||
references: [users_temp.id],
|
||||
}),
|
||||
compose: one(compose, {
|
||||
fields: [backups.composeId],
|
||||
|
||||
@@ -8,7 +8,7 @@ import { bitbucket } from "./bitbucket";
|
||||
import { gitea } from "./gitea";
|
||||
import { github } from "./github";
|
||||
import { gitlab } from "./gitlab";
|
||||
import { users } from "./user";
|
||||
import { users_temp } from "./user";
|
||||
|
||||
export const gitProviderType = pgEnum("gitProviderType", [
|
||||
"github",
|
||||
@@ -32,7 +32,7 @@ export const gitProvider = pgTable("git_provider", {
|
||||
.references(() => organization.id, { onDelete: "cascade" }),
|
||||
userId: text("userId")
|
||||
.notNull()
|
||||
.references(() => users.id, { onDelete: "cascade" }),
|
||||
.references(() => users_temp.id, { onDelete: "cascade" }),
|
||||
});
|
||||
|
||||
export const gitProviderRelations = relations(gitProvider, ({ one }) => ({
|
||||
@@ -56,9 +56,9 @@ export const gitProviderRelations = relations(gitProvider, ({ one }) => ({
|
||||
fields: [gitProvider.organizationId],
|
||||
references: [organization.id],
|
||||
}),
|
||||
user: one(users, {
|
||||
user: one(users_temp, {
|
||||
fields: [gitProvider.userId],
|
||||
references: [users.id],
|
||||
references: [users_temp.id],
|
||||
}),
|
||||
}));
|
||||
|
||||
|
||||
@@ -30,9 +30,7 @@ export * from "./server";
|
||||
export * from "./utils";
|
||||
export * from "./preview-deployments";
|
||||
export * from "./ai";
|
||||
// export * from "./rbac";
|
||||
export * from "./account";
|
||||
export * from "./schedule";
|
||||
export * from "./rollbacks";
|
||||
export * from "./volume-backups";
|
||||
export * from "./web-server";
|
||||
|
||||
@@ -1,58 +0,0 @@
|
||||
// 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 { deployments } from "./deployment";
|
||||
import { server } from "./server";
|
||||
import { users } from "./user";
|
||||
import { users_temp } from "./user";
|
||||
import { generateAppName } from "./utils";
|
||||
export const shellTypes = pgEnum("shellType", ["bash", "sh"]);
|
||||
|
||||
@@ -45,7 +45,7 @@ export const schedules = pgTable("schedule", {
|
||||
serverId: text("serverId").references(() => server.serverId, {
|
||||
onDelete: "cascade",
|
||||
}),
|
||||
userId: text("userId").references(() => users.id, {
|
||||
userId: text("userId").references(() => users_temp.id, {
|
||||
onDelete: "cascade",
|
||||
}),
|
||||
enabled: boolean("enabled").notNull().default(true),
|
||||
@@ -69,9 +69,9 @@ export const schedulesRelations = relations(schedules, ({ one, many }) => ({
|
||||
fields: [schedules.serverId],
|
||||
references: [server.serverId],
|
||||
}),
|
||||
user: one(users, {
|
||||
user: one(users_temp, {
|
||||
fields: [schedules.userId],
|
||||
references: [users.id],
|
||||
references: [users_temp.id],
|
||||
}),
|
||||
deployments: many(deployments),
|
||||
}));
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { pgTable, text, timestamp } from "drizzle-orm/pg-core";
|
||||
import { users } from "./user";
|
||||
import { users_temp } from "./user";
|
||||
|
||||
// OLD TABLE
|
||||
export const session = pgTable("session_temp", {
|
||||
@@ -12,7 +12,7 @@ export const session = pgTable("session_temp", {
|
||||
userAgent: text("user_agent"),
|
||||
userId: text("user_id")
|
||||
.notNull()
|
||||
.references(() => users.id, { onDelete: "cascade" }),
|
||||
.references(() => users_temp.id, { onDelete: "cascade" }),
|
||||
impersonatedBy: text("impersonated_by"),
|
||||
activeOrganizationId: text("active_organization_id"),
|
||||
});
|
||||
|
||||
@@ -2,6 +2,7 @@ import { relations } from "drizzle-orm";
|
||||
import {
|
||||
boolean,
|
||||
integer,
|
||||
jsonb,
|
||||
pgTable,
|
||||
text,
|
||||
timestamp,
|
||||
@@ -13,6 +14,7 @@ import { account, apikey, organization } from "./account";
|
||||
import { backups } from "./backups";
|
||||
import { projects } from "./project";
|
||||
import { schedules } from "./schedule";
|
||||
import { certificateType } from "./shared";
|
||||
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
|
||||
@@ -21,8 +23,10 @@ import { paths } from "@dokploy/server/constants";
|
||||
* @see https://orm.drizzle.team/docs/goodies#multi-project-schema
|
||||
*/
|
||||
|
||||
// OLD TABLE
|
||||
|
||||
// TEMP
|
||||
export const users = pgTable("users", {
|
||||
export const users_temp = pgTable("user_temp", {
|
||||
id: text("id")
|
||||
.notNull()
|
||||
.primaryKey()
|
||||
@@ -32,7 +36,10 @@ export const users = pgTable("users", {
|
||||
expirationDate: text("expirationDate")
|
||||
.notNull()
|
||||
.$defaultFn(() => new Date().toISOString()),
|
||||
createdAt: timestamp("created_at").notNull().defaultNow(),
|
||||
createdAt2: text("createdAt")
|
||||
.notNull()
|
||||
.$defaultFn(() => new Date().toISOString()),
|
||||
createdAt: timestamp("created_at").defaultNow(),
|
||||
// Auth
|
||||
twoFactorEnabled: boolean("two_factor_enabled"),
|
||||
email: text("email").notNull().unique(),
|
||||
@@ -41,19 +48,83 @@ export const users = pgTable("users", {
|
||||
banned: boolean("banned"),
|
||||
banReason: text("ban_reason"),
|
||||
banExpires: timestamp("ban_expires"),
|
||||
updatedAt: timestamp("updated_at").notNull().defaultNow(),
|
||||
updatedAt: timestamp("updated_at").notNull(),
|
||||
// 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"),
|
||||
// Metrics
|
||||
enablePaidFeatures: boolean("enablePaidFeatures").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"),
|
||||
stripeSubscriptionId: text("stripeSubscriptionId"),
|
||||
serversQuantity: integer("serversQuantity").notNull().default(0),
|
||||
});
|
||||
|
||||
export const usersRelations = relations(users, ({ one, many }) => ({
|
||||
export const usersRelations = relations(users_temp, ({ one, many }) => ({
|
||||
account: one(account, {
|
||||
fields: [users.id],
|
||||
fields: [users_temp.id],
|
||||
references: [account.userId],
|
||||
}),
|
||||
organizations: many(organization),
|
||||
@@ -63,7 +134,7 @@ export const usersRelations = relations(users, ({ one, many }) => ({
|
||||
schedules: many(schedules),
|
||||
}));
|
||||
|
||||
const createSchema = createInsertSchema(users, {
|
||||
const createSchema = createInsertSchema(users_temp, {
|
||||
id: z.string().min(1),
|
||||
isRegistered: z.boolean().optional(),
|
||||
}).omit({
|
||||
@@ -128,6 +199,33 @@ export const apiFindOneUserByAuth = createSchema
|
||||
// authId: true,
|
||||
})
|
||||
.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({
|
||||
traefikConfig: z.string().min(1),
|
||||
|
||||
@@ -1,104 +0,0 @@
|
||||
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(),
|
||||
});
|
||||
@@ -1,13 +0,0 @@
|
||||
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,7 +13,6 @@ export * from "./services/settings";
|
||||
export * from "./services/volume-backups";
|
||||
export * from "./services/docker";
|
||||
export * from "./services/destination";
|
||||
export * from "./services/role";
|
||||
export * from "./services/deployment";
|
||||
export * from "./services/mount";
|
||||
export * from "./services/certificate";
|
||||
@@ -35,7 +34,6 @@ export * from "./services/server";
|
||||
export * from "./services/schedule";
|
||||
export * from "./services/application";
|
||||
export * from "./services/rollbacks";
|
||||
export * from "./services/web-server";
|
||||
export * from "./utils/databases/rebuild";
|
||||
export * from "./setup/config-paths";
|
||||
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