mirror of
https://github.com/dokploy/dokploy.git
synced 2026-06-14 03:19:49 +00:00
Compare commits
22 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d84099108a | |||
| cee426dcf5 | |||
| 1074e9b08e | |||
| a5911e2bac | |||
| a43b8ee2d2 | |||
| 982a1d5d31 | |||
| 30d45bf2e5 | |||
| db221e5cc4 | |||
| e1773a8f8b | |||
| e8475730fa | |||
| d78e634cb0 | |||
| 509d95fbf2 | |||
| b928e94e51 | |||
| 3052979bdd | |||
| 2ec4868a09 | |||
| 733777eeb1 | |||
| 521330682d | |||
| 7cc048450b | |||
| 427674dd64 | |||
| 8b8dc8c94f | |||
| d6e8653839 | |||
| d0b7ce3a50 |
@@ -5,7 +5,8 @@ vi.mock("node:fs", () => ({
|
||||
default: fs,
|
||||
}));
|
||||
|
||||
import type { FileConfig, User } from "@dokploy/server";
|
||||
import type { FileConfig } from "@dokploy/server";
|
||||
import type { WebServer } from "@dokploy/server/db/schema";
|
||||
import {
|
||||
createDefaultServerTraefikConfig,
|
||||
loadOrCreateConfig,
|
||||
@@ -13,11 +14,8 @@ import {
|
||||
} from "@dokploy/server";
|
||||
import { beforeEach, expect, test, vi } from "vitest";
|
||||
|
||||
const baseAdmin: User = {
|
||||
const baseAdmin: WebServer = {
|
||||
https: false,
|
||||
enablePaidFeatures: false,
|
||||
allowImpersonation: false,
|
||||
role: "user",
|
||||
metricsConfig: {
|
||||
containers: {
|
||||
refreshRate: 20,
|
||||
@@ -40,10 +38,6 @@ const baseAdmin: User = {
|
||||
urlCallback: "",
|
||||
},
|
||||
},
|
||||
cleanupCacheApplications: false,
|
||||
cleanupCacheOnCompose: false,
|
||||
cleanupCacheOnPreviews: false,
|
||||
createdAt: new Date(),
|
||||
serverIp: null,
|
||||
certificateType: "none",
|
||||
host: null,
|
||||
@@ -51,22 +45,7 @@ const baseAdmin: User = {
|
||||
sshPrivateKey: null,
|
||||
enableDockerCleanup: false,
|
||||
logCleanupCron: null,
|
||||
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,
|
||||
webServerId: "1",
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -85,8 +64,6 @@ test("Should apply redirect-to-https", () => {
|
||||
updateServerTraefik(
|
||||
{
|
||||
...baseAdmin,
|
||||
https: true,
|
||||
certificateType: "letsencrypt",
|
||||
},
|
||||
"example.com",
|
||||
);
|
||||
|
||||
@@ -71,7 +71,7 @@ export const ShowDomains = ({ id, type }: Props) => {
|
||||
const [validationStates, setValidationStates] = useState<ValidationStates>(
|
||||
{},
|
||||
);
|
||||
const { data: ip } = api.settings.getIp.useQuery();
|
||||
const { data: webServer } = api.webServer.get.useQuery();
|
||||
|
||||
const {
|
||||
data,
|
||||
@@ -110,7 +110,9 @@ export const ShowDomains = ({ id, type }: Props) => {
|
||||
const result = await validateDomain({
|
||||
domain: host,
|
||||
serverIp:
|
||||
application?.server?.ipAddress?.toString() || ip?.toString() || "",
|
||||
application?.server?.ipAddress?.toString() ||
|
||||
webServer?.serverIp?.toString() ||
|
||||
"",
|
||||
});
|
||||
|
||||
setValidationStates((prev) => ({
|
||||
@@ -210,7 +212,7 @@ export const ShowDomains = ({ id, type }: Props) => {
|
||||
}}
|
||||
serverIp={
|
||||
application?.server?.ipAddress?.toString() ||
|
||||
ip?.toString()
|
||||
webServer?.serverIp?.toString()
|
||||
}
|
||||
/>
|
||||
)}
|
||||
|
||||
+2
-2
@@ -46,11 +46,11 @@ interface Props {
|
||||
mariadbId: string;
|
||||
}
|
||||
export const ShowExternalMariadbCredentials = ({ mariadbId }: Props) => {
|
||||
const { data: ip } = api.settings.getIp.useQuery();
|
||||
const { data: webServer } = api.webServer.get.useQuery();
|
||||
const { data, refetch } = api.mariadb.one.useQuery({ mariadbId });
|
||||
const { mutateAsync, isLoading } = api.mariadb.saveExternalPort.useMutation();
|
||||
const [connectionUrl, setConnectionUrl] = useState("");
|
||||
const getIp = data?.server?.ipAddress || ip;
|
||||
const getIp = data?.server?.ipAddress || webServer?.serverIp;
|
||||
const form = useForm<DockerProvider>({
|
||||
defaultValues: {},
|
||||
resolver: zodResolver(DockerProviderSchema),
|
||||
|
||||
@@ -46,11 +46,11 @@ interface Props {
|
||||
mongoId: string;
|
||||
}
|
||||
export const ShowExternalMongoCredentials = ({ mongoId }: Props) => {
|
||||
const { data: ip } = api.settings.getIp.useQuery();
|
||||
const { data: webServer } = api.webServer.get.useQuery();
|
||||
const { data, refetch } = api.mongo.one.useQuery({ mongoId });
|
||||
const { mutateAsync, isLoading } = api.mongo.saveExternalPort.useMutation();
|
||||
const [connectionUrl, setConnectionUrl] = useState("");
|
||||
const getIp = data?.server?.ipAddress || ip;
|
||||
const getIp = data?.server?.ipAddress || webServer?.serverIp;
|
||||
const form = useForm<DockerProvider>({
|
||||
defaultValues: {},
|
||||
resolver: zodResolver(DockerProviderSchema),
|
||||
|
||||
@@ -46,11 +46,11 @@ interface Props {
|
||||
mysqlId: string;
|
||||
}
|
||||
export const ShowExternalMysqlCredentials = ({ mysqlId }: Props) => {
|
||||
const { data: ip } = api.settings.getIp.useQuery();
|
||||
const { data: webServer } = api.webServer.get.useQuery();
|
||||
const { data, refetch } = api.mysql.one.useQuery({ mysqlId });
|
||||
const { mutateAsync, isLoading } = api.mysql.saveExternalPort.useMutation();
|
||||
const [connectionUrl, setConnectionUrl] = useState("");
|
||||
const getIp = data?.server?.ipAddress || ip;
|
||||
const getIp = data?.server?.ipAddress || webServer?.serverIp;
|
||||
const form = useForm<DockerProvider>({
|
||||
defaultValues: {},
|
||||
resolver: zodResolver(DockerProviderSchema),
|
||||
|
||||
+2
-2
@@ -46,11 +46,11 @@ interface Props {
|
||||
postgresId: string;
|
||||
}
|
||||
export const ShowExternalPostgresCredentials = ({ postgresId }: Props) => {
|
||||
const { data: ip } = api.settings.getIp.useQuery();
|
||||
const { data: webServer } = api.webServer.get.useQuery();
|
||||
const { data, refetch } = api.postgres.one.useQuery({ postgresId });
|
||||
const { mutateAsync, isLoading } =
|
||||
api.postgres.saveExternalPort.useMutation();
|
||||
const getIp = data?.server?.ipAddress || ip;
|
||||
const getIp = data?.server?.ipAddress || webServer?.serverIp;
|
||||
const [connectionUrl, setConnectionUrl] = useState("");
|
||||
|
||||
const form = useForm<DockerProvider>({
|
||||
|
||||
@@ -47,11 +47,12 @@ 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("");
|
||||
|
||||
@@ -83,11 +84,11 @@ export const ShowProjects = () => {
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
|
||||
{(auth?.role === "owner" || auth?.canCreateProjects) && (
|
||||
<Permissions permissions={[PERMISSIONS.PROJECT.CREATE.name]}>
|
||||
<div className="">
|
||||
<HandleProject />
|
||||
</div>
|
||||
)}
|
||||
</Permissions>
|
||||
</div>
|
||||
|
||||
<CardContent className="space-y-2 py-8 border-t gap-4 flex flex-col min-h-[60vh]">
|
||||
@@ -289,8 +290,11 @@ export const ShowProjects = () => {
|
||||
<div
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{(auth?.role === "owner" ||
|
||||
auth?.canDeleteProjects) && (
|
||||
<Permissions
|
||||
permissions={[
|
||||
PERMISSIONS.PROJECT.DELETE.name,
|
||||
]}
|
||||
>
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger className="w-full">
|
||||
<DropdownMenuItem
|
||||
@@ -356,7 +360,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: ip } = api.settings.getIp.useQuery();
|
||||
const { data: webServer } = api.webServer.get.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 || ip;
|
||||
const getIp = data?.server?.ipAddress || webServer?.serverIp;
|
||||
|
||||
const form = useForm<DockerProvider>({
|
||||
defaultValues: {},
|
||||
|
||||
@@ -14,7 +14,7 @@ export const ShowWelcomeDokploy = () => {
|
||||
|
||||
const { data: isCloud, isLoading } = api.settings.isCloud.useQuery();
|
||||
|
||||
if (!isCloud || data?.role !== "admin") {
|
||||
if (!isCloud || data?.role?.name !== "admin") {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -23,14 +23,14 @@ export const ShowWelcomeDokploy = () => {
|
||||
!isLoading &&
|
||||
isCloud &&
|
||||
!localStorage.getItem("hasSeenCloudWelcomeModal") &&
|
||||
data?.role === "owner"
|
||||
data?.role?.name === "owner"
|
||||
) {
|
||||
setOpen(true);
|
||||
}
|
||||
}, [isCloud, isLoading]);
|
||||
|
||||
const handleClose = (isOpen: boolean) => {
|
||||
if (data?.role === "owner") {
|
||||
if (data?.role?.name === "owner") {
|
||||
setOpen(isOpen);
|
||||
if (!isOpen) {
|
||||
localStorage.setItem("hasSeenCloudWelcomeModal", "true"); // Establece el flag al cerrar el modal
|
||||
|
||||
@@ -32,6 +32,7 @@ import { Disable2FA } from "./disable-2fa";
|
||||
import { Enable2FA } from "./enable-2fa";
|
||||
|
||||
const profileSchema = z.object({
|
||||
name: z.string(),
|
||||
email: z.string(),
|
||||
password: z.string().nullable(),
|
||||
currentPassword: z.string().nullable(),
|
||||
@@ -79,6 +80,7 @@ export const ProfileForm = () => {
|
||||
|
||||
const form = useForm<Profile>({
|
||||
defaultValues: {
|
||||
name: data?.user?.name || "",
|
||||
email: data?.user?.email || "",
|
||||
password: "",
|
||||
image: data?.user?.image || "",
|
||||
@@ -92,6 +94,7 @@ export const ProfileForm = () => {
|
||||
if (data) {
|
||||
form.reset(
|
||||
{
|
||||
name: data?.user?.name || "",
|
||||
email: data?.user?.email || "",
|
||||
password: form.getValues("password") || "",
|
||||
image: data?.user?.image || "",
|
||||
@@ -114,6 +117,7 @@ export const ProfileForm = () => {
|
||||
|
||||
const onSubmit = async (values: Profile) => {
|
||||
await mutateAsync({
|
||||
name: values.name,
|
||||
email: values.email.toLowerCase(),
|
||||
password: values.password || undefined,
|
||||
image: values.image,
|
||||
@@ -124,6 +128,7 @@ export const ProfileForm = () => {
|
||||
await refetch();
|
||||
toast.success("Profile Updated");
|
||||
form.reset({
|
||||
name: values.name,
|
||||
email: values.email,
|
||||
password: "",
|
||||
image: values.image,
|
||||
@@ -167,6 +172,19 @@ 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"
|
||||
|
||||
+3
-3
@@ -7,7 +7,7 @@ interface Props {
|
||||
serverId?: string;
|
||||
}
|
||||
export const ToggleDockerCleanup = ({ serverId }: Props) => {
|
||||
const { data, refetch } = api.user.get.useQuery(undefined, {
|
||||
const { data, refetch } = api.webServer.get.useQuery(undefined, {
|
||||
enabled: !serverId,
|
||||
});
|
||||
|
||||
@@ -20,9 +20,9 @@ export const ToggleDockerCleanup = ({ serverId }: Props) => {
|
||||
},
|
||||
);
|
||||
|
||||
const enabled = data?.user.enableDockerCleanup || server?.enableDockerCleanup;
|
||||
const enabled = data?.enableDockerCleanup || server?.enableDockerCleanup;
|
||||
|
||||
const { mutateAsync } = api.settings.updateDockerCleanup.useMutation();
|
||||
const { mutateAsync } = api.webServer.updateDockerCleanup.useMutation();
|
||||
|
||||
const handleToggle = async (checked: boolean) => {
|
||||
try {
|
||||
|
||||
@@ -89,7 +89,7 @@ export const SetupMonitoring = ({ serverId }: Props) => {
|
||||
enabled: !!serverId,
|
||||
},
|
||||
)
|
||||
: api.user.getServerMetrics.useQuery();
|
||||
: api.webServer.get.useQuery();
|
||||
|
||||
const url = useUrl();
|
||||
|
||||
|
||||
@@ -49,12 +49,15 @@ type AddInvitation = z.infer<typeof addInvitation>;
|
||||
export const AddInvitation = () => {
|
||||
const [open, setOpen] = useState(false);
|
||||
const utils = api.useUtils();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const { data: roles } = api.role.all.useQuery();
|
||||
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||
const { data: emailProviders } =
|
||||
api.notification.getEmailProviders.useQuery();
|
||||
const { mutateAsync: sendInvitation } = api.user.sendInvitation.useMutation();
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const {
|
||||
mutateAsync: createInvitation,
|
||||
isLoading,
|
||||
error,
|
||||
} = api.user.createInvitation.useMutation();
|
||||
const { data: activeOrganization } = authClient.useActiveOrganization();
|
||||
|
||||
const form = useForm<AddInvitation>({
|
||||
@@ -70,36 +73,20 @@ export const AddInvitation = () => {
|
||||
}, [form, form.formState.isSubmitSuccessful, form.reset]);
|
||||
|
||||
const onSubmit = async (data: AddInvitation) => {
|
||||
setIsLoading(true);
|
||||
const result = await authClient.organization.inviteMember({
|
||||
await createInvitation({
|
||||
email: data.email.toLowerCase(),
|
||||
role: data.role,
|
||||
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 {
|
||||
organizationId: activeOrganization?.id || "",
|
||||
notificationId: data.notificationId || "",
|
||||
})
|
||||
.then(() => {
|
||||
toast.success("Invitation created");
|
||||
}
|
||||
setError(null);
|
||||
setOpen(false);
|
||||
}
|
||||
})
|
||||
.catch((error: any) => {
|
||||
toast.error(error.message);
|
||||
});
|
||||
|
||||
utils.organization.allInvitations.invalidate();
|
||||
setIsLoading(false);
|
||||
};
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
@@ -113,7 +100,7 @@ export const AddInvitation = () => {
|
||||
<DialogTitle>Add Invitation</DialogTitle>
|
||||
<DialogDescription>Invite a new user</DialogDescription>
|
||||
</DialogHeader>
|
||||
{error && <AlertBlock type="error">{error}</AlertBlock>}
|
||||
{error && <AlertBlock type="error">{error.message}</AlertBlock>}
|
||||
|
||||
<Form {...form}>
|
||||
<form
|
||||
@@ -158,6 +145,12 @@ 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>
|
||||
|
||||
@@ -0,0 +1,758 @@
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import { DropdownMenuItem } from "@/components/ui/dropdown-menu";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
FormDescription,
|
||||
} from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { api } from "@/utils/api";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import { extractServices } from "@/pages/dashboard/project/[projectId]";
|
||||
import { PenBoxIcon, Trash2 } from "lucide-react";
|
||||
import { format } from "date-fns";
|
||||
import { DialogAction } from "@/components/shared/dialog-action";
|
||||
|
||||
const assignRoleSchema = z.object({
|
||||
roleId: z.string(),
|
||||
accessedProjects: z.array(z.string()).optional(),
|
||||
accessedServices: z.array(z.string()).optional(),
|
||||
});
|
||||
|
||||
const createRoleSchema = z.object({
|
||||
name: z.string().min(1, "Name is required"),
|
||||
description: z.string().optional(),
|
||||
permissions: z.array(z.string()).min(1, "Select at least one permission"),
|
||||
});
|
||||
|
||||
type AssignRoleForm = z.infer<typeof assignRoleSchema>;
|
||||
type CreateRoleForm = z.infer<typeof createRoleSchema>;
|
||||
|
||||
interface Props {
|
||||
userId: string;
|
||||
}
|
||||
|
||||
export const AddUserPermissionsV2 = ({ userId }: Props) => {
|
||||
const utils = api.useUtils();
|
||||
const { data: projects } = api.project.all.useQuery();
|
||||
const [activeTab, setActiveTab] = useState<"assign" | "create">("assign");
|
||||
const [editingRole, setEditingRole] = useState<{
|
||||
roleId: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
permissions: string[];
|
||||
} | null>(null);
|
||||
const { data: roles, refetch: refetchRoles } = api.role.all.useQuery();
|
||||
const { data: defaultRoles } = api.role.getDefaultRoles.useQuery();
|
||||
|
||||
const { data: userData, refetch: refetchUser } = api.user.one.useQuery(
|
||||
{
|
||||
userId,
|
||||
},
|
||||
{
|
||||
enabled: !!userId,
|
||||
},
|
||||
);
|
||||
|
||||
const { mutateAsync: createRole, isLoading: isCreatingRole } =
|
||||
api.role.create.useMutation();
|
||||
const { mutateAsync: updateRole, isLoading: isUpdatingRole } =
|
||||
api.role.update.useMutation();
|
||||
const { mutateAsync: deleteRole, isLoading: isDeletingRole } =
|
||||
api.role.delete.useMutation();
|
||||
const { mutateAsync: updateMemberRole, isLoading: isAssigningRole } =
|
||||
api.user.assignRole.useMutation();
|
||||
|
||||
const assignForm = useForm<AssignRoleForm>({
|
||||
resolver: zodResolver(assignRoleSchema),
|
||||
defaultValues: {
|
||||
accessedProjects: [],
|
||||
accessedServices: [],
|
||||
},
|
||||
});
|
||||
|
||||
const createForm = useForm<CreateRoleForm>({
|
||||
resolver: zodResolver(createRoleSchema),
|
||||
defaultValues: {
|
||||
permissions: [],
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (userData) {
|
||||
assignForm.reset({
|
||||
roleId: userData.roleId || "",
|
||||
accessedProjects: userData.accessedProjects || [],
|
||||
accessedServices: userData.accessedServices || [],
|
||||
});
|
||||
}
|
||||
}, [userData, assignForm]);
|
||||
|
||||
// Reset form when switching between create and edit modes
|
||||
useEffect(() => {
|
||||
if (editingRole) {
|
||||
createForm.reset({
|
||||
name: editingRole.name,
|
||||
description: editingRole.description || "",
|
||||
permissions: editingRole.permissions,
|
||||
});
|
||||
} else {
|
||||
createForm.reset({
|
||||
name: "",
|
||||
description: "",
|
||||
permissions: [],
|
||||
});
|
||||
}
|
||||
}, [editingRole, createForm]);
|
||||
|
||||
// Check if the selected role is owner or admin (has full access)
|
||||
const selectedRoleId = assignForm.watch("roleId");
|
||||
const selectedRole = defaultRoles?.roles?.find(
|
||||
(role) => role.roleId === selectedRoleId,
|
||||
);
|
||||
|
||||
const isFullAccessRole =
|
||||
selectedRole &&
|
||||
(selectedRole.name === "owner" || selectedRole.name === "admin");
|
||||
|
||||
const onAssignRole = async (data: AssignRoleForm) => {
|
||||
try {
|
||||
await updateMemberRole({
|
||||
userId,
|
||||
roleId: data.roleId,
|
||||
accessedProjects: isFullAccessRole ? [] : data.accessedProjects || [],
|
||||
accessedServices: isFullAccessRole ? [] : data.accessedServices || [],
|
||||
});
|
||||
toast.success("Role assigned successfully");
|
||||
await refetchUser();
|
||||
await utils.user.all.invalidate();
|
||||
} catch (error) {
|
||||
const message =
|
||||
error instanceof Error ? error.message : "Failed to assign role";
|
||||
toast.error(message);
|
||||
}
|
||||
};
|
||||
|
||||
const onCreateRole = async (data: CreateRoleForm) => {
|
||||
try {
|
||||
if (editingRole) {
|
||||
// Update existing role
|
||||
await updateRole({
|
||||
roleId: editingRole.roleId,
|
||||
...data,
|
||||
permissions: data.permissions,
|
||||
});
|
||||
toast.success("Role updated successfully");
|
||||
} else {
|
||||
// Create new role
|
||||
await createRole({
|
||||
...data,
|
||||
permissions: data.permissions,
|
||||
});
|
||||
toast.success("Role created successfully");
|
||||
}
|
||||
refetchRoles();
|
||||
setActiveTab("assign");
|
||||
setEditingRole(null);
|
||||
createForm.reset();
|
||||
} catch (error) {
|
||||
const message =
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: editingRole
|
||||
? "Failed to update role"
|
||||
: "Failed to create role";
|
||||
toast.error(message);
|
||||
}
|
||||
};
|
||||
|
||||
const onEditRole = (role: {
|
||||
roleId: string;
|
||||
name: string;
|
||||
description?: string | null;
|
||||
permissions: string[] | null;
|
||||
}) => {
|
||||
setEditingRole({
|
||||
roleId: role.roleId,
|
||||
name: role.name,
|
||||
description: role.description || "",
|
||||
permissions: role.permissions || [],
|
||||
});
|
||||
setActiveTab("create");
|
||||
};
|
||||
|
||||
const cancelEdit = () => {
|
||||
setEditingRole(null);
|
||||
setActiveTab("assign");
|
||||
createForm.reset();
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<DropdownMenuItem
|
||||
className="w-full cursor-pointer"
|
||||
onSelect={(e) => e.preventDefault()}
|
||||
>
|
||||
Manage Roles
|
||||
</DropdownMenuItem>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-h-[85vh] overflow-y-auto sm:max-w-4xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Role Management</DialogTitle>
|
||||
<DialogDescription>
|
||||
Assign existing roles or create new ones. The Owner role has full
|
||||
access to all features.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<Tabs
|
||||
value={activeTab}
|
||||
onValueChange={(v) => setActiveTab(v as "assign" | "create")}
|
||||
>
|
||||
<TabsList className="grid w-full grid-cols-2">
|
||||
<TabsTrigger value="assign">Assign Role</TabsTrigger>
|
||||
<TabsTrigger value="create">
|
||||
{editingRole ? "Edit Role" : "Create Role"}
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="assign">
|
||||
<Form {...assignForm}>
|
||||
<form onSubmit={assignForm.handleSubmit(onAssignRole)}>
|
||||
<div className="space-y-4 py-4">
|
||||
<FormField
|
||||
control={assignForm.control}
|
||||
name="roleId"
|
||||
render={({ field }) => (
|
||||
<FormItem className="space-y-3">
|
||||
<FormLabel>Select Role</FormLabel>
|
||||
<FormControl>
|
||||
<RadioGroup
|
||||
onValueChange={field.onChange}
|
||||
defaultValue={field.value}
|
||||
className="space-y-4"
|
||||
>
|
||||
<div className="space-y-4">
|
||||
<h4 className="text-sm font-medium">
|
||||
Default Roles
|
||||
</h4>
|
||||
{defaultRoles?.roles?.map((role) => {
|
||||
const isOwner = role.name === "owner";
|
||||
const isAdmin = role.name === "admin";
|
||||
if (isOwner) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<FormItem
|
||||
key={role.roleId}
|
||||
className="flex items-center space-x-3 space-y-0"
|
||||
>
|
||||
<FormControl>
|
||||
<RadioGroupItem
|
||||
value={role.roleId || ""}
|
||||
disabled={isOwner}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormLabel className="font-normal">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-medium capitalize">
|
||||
{role.name}
|
||||
</span>
|
||||
{isAdmin && (
|
||||
<Badge
|
||||
variant="default"
|
||||
className="text-xs"
|
||||
>
|
||||
Full Access
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
{role.description}
|
||||
</div>
|
||||
{!isOwner && (
|
||||
<div className="flex flex-wrap gap-1 mt-1">
|
||||
{role.permissions?.map(
|
||||
(permission) => (
|
||||
<Badge
|
||||
key={permission.name}
|
||||
variant={
|
||||
isOwner
|
||||
? "default"
|
||||
: "secondary"
|
||||
}
|
||||
className="text-xs"
|
||||
>
|
||||
{permission.description}
|
||||
</Badge>
|
||||
),
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</FormLabel>
|
||||
</FormItem>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
{/* Custom Roles Section */}
|
||||
{roles &&
|
||||
roles.filter((r) => !r.isSystem).length > 0 && (
|
||||
<div className="space-y-4">
|
||||
<h4 className="text-sm font-medium">
|
||||
Custom Roles
|
||||
</h4>
|
||||
{roles
|
||||
?.filter((r) => !r.isSystem)
|
||||
.map((role) => (
|
||||
<FormItem
|
||||
key={role.roleId}
|
||||
className="flex items-center justify-between space-x-3 space-y-0"
|
||||
>
|
||||
<div className="flex items-center space-x-3">
|
||||
<FormControl>
|
||||
<RadioGroupItem
|
||||
value={role.roleId}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormLabel className="font-normal">
|
||||
<span className="font-medium">
|
||||
{role.name}
|
||||
</span>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
{role.description}
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{format(
|
||||
role.createdAt,
|
||||
"MMM d, yyyy",
|
||||
)}
|
||||
</p>
|
||||
<div className="flex flex-wrap gap-1 mt-1">
|
||||
{role.permissions?.map(
|
||||
(permission) => {
|
||||
const permissionInfo =
|
||||
defaultRoles?.permissions?.find(
|
||||
(p) =>
|
||||
p.name === permission,
|
||||
);
|
||||
return (
|
||||
<Badge
|
||||
key={permission}
|
||||
variant="secondary"
|
||||
className="text-xs"
|
||||
>
|
||||
{
|
||||
permissionInfo?.description
|
||||
}
|
||||
</Badge>
|
||||
);
|
||||
},
|
||||
)}
|
||||
</div>
|
||||
</FormLabel>
|
||||
</div>
|
||||
<div className="flex space-x-2">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => onEditRole(role)}
|
||||
title="Edit role"
|
||||
>
|
||||
<PenBoxIcon className="h-4 w-4" />
|
||||
</Button>
|
||||
<DialogAction
|
||||
title="Delete Role"
|
||||
description="Are you sure you want to delete this role?"
|
||||
type="destructive"
|
||||
onClick={async () => {
|
||||
await deleteRole({
|
||||
roleId: role.roleId,
|
||||
})
|
||||
.then(() => {
|
||||
refetchRoles();
|
||||
toast.success(
|
||||
"Role deleted successfully",
|
||||
);
|
||||
})
|
||||
.catch((error) => {
|
||||
const message =
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: "Error deleting role";
|
||||
toast.error(message);
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="text-destructive hover:text-destructive"
|
||||
isLoading={isDeletingRole}
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
</DialogAction>
|
||||
</div>
|
||||
</FormItem>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
{/* Project Access Section - Only show if not full access role */}
|
||||
{!isFullAccessRole && selectedRoleId && (
|
||||
<>
|
||||
<Separator />
|
||||
<FormField
|
||||
control={assignForm.control}
|
||||
name="accessedProjects"
|
||||
render={() => (
|
||||
<FormItem className="space-y-4">
|
||||
<div>
|
||||
<FormLabel className="text-base">
|
||||
Projects Access
|
||||
</FormLabel>
|
||||
<FormDescription>
|
||||
Select the projects that the user can access
|
||||
</FormDescription>
|
||||
</div>
|
||||
{projects?.length === 0 && (
|
||||
<p className="text-sm text-muted-foreground">
|
||||
No projects found
|
||||
</p>
|
||||
)}
|
||||
<div className="grid md:grid-cols-2 gap-4">
|
||||
{projects?.map((project, index) => {
|
||||
const services = extractServices(project);
|
||||
return (
|
||||
<FormField
|
||||
key={`project-${index}`}
|
||||
control={assignForm.control}
|
||||
name="accessedProjects"
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<FormItem
|
||||
key={project.projectId}
|
||||
className="flex flex-col items-start space-x-4 rounded-lg p-4 border"
|
||||
>
|
||||
<div className="flex flex-row gap-4">
|
||||
<FormControl>
|
||||
<Checkbox
|
||||
checked={field.value?.includes(
|
||||
project.projectId,
|
||||
)}
|
||||
onCheckedChange={(checked) => {
|
||||
return checked
|
||||
? field.onChange([
|
||||
...(field.value || []),
|
||||
project.projectId,
|
||||
])
|
||||
: field.onChange(
|
||||
field.value?.filter(
|
||||
(value) =>
|
||||
value !==
|
||||
project.projectId,
|
||||
),
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormLabel className="text-sm font-medium text-primary">
|
||||
{project.name}
|
||||
</FormLabel>
|
||||
</div>
|
||||
{services.length === 0 && (
|
||||
<p className="text-sm text-muted-foreground ml-6">
|
||||
No services found
|
||||
</p>
|
||||
)}
|
||||
{services?.map(
|
||||
(service, serviceIndex) => (
|
||||
<FormField
|
||||
key={`service-${serviceIndex}`}
|
||||
control={assignForm.control}
|
||||
name="accessedServices"
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<FormItem
|
||||
key={service.id}
|
||||
className="flex flex-row items-start space-x-3 space-y-0 ml-6"
|
||||
>
|
||||
<FormControl>
|
||||
<Checkbox
|
||||
checked={field.value?.includes(
|
||||
service.id,
|
||||
)}
|
||||
onCheckedChange={(
|
||||
checked,
|
||||
) => {
|
||||
const currentProjects =
|
||||
assignForm.getValues(
|
||||
"accessedProjects",
|
||||
) || [];
|
||||
const currentServices =
|
||||
field.value || [];
|
||||
|
||||
if (checked) {
|
||||
// Add service
|
||||
const newServices =
|
||||
[
|
||||
...currentServices,
|
||||
service.id,
|
||||
];
|
||||
field.onChange(
|
||||
newServices,
|
||||
);
|
||||
|
||||
// Auto-select project if not already selected
|
||||
if (
|
||||
!currentProjects.includes(
|
||||
project.projectId,
|
||||
)
|
||||
) {
|
||||
assignForm.setValue(
|
||||
"accessedProjects",
|
||||
[
|
||||
...currentProjects,
|
||||
project.projectId,
|
||||
],
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// Remove service
|
||||
const newServices =
|
||||
currentServices.filter(
|
||||
(value) =>
|
||||
value !==
|
||||
service.id,
|
||||
);
|
||||
field.onChange(
|
||||
newServices,
|
||||
);
|
||||
|
||||
// Check if any other services from this project are still selected
|
||||
const otherServicesFromProject =
|
||||
services.filter(
|
||||
(s) =>
|
||||
s.id !==
|
||||
service.id &&
|
||||
newServices.includes(
|
||||
s.id,
|
||||
),
|
||||
);
|
||||
|
||||
// If no other services from this project, unselect the project
|
||||
if (
|
||||
otherServicesFromProject.length ===
|
||||
0
|
||||
) {
|
||||
assignForm.setValue(
|
||||
"accessedProjects",
|
||||
currentProjects.filter(
|
||||
(p) =>
|
||||
p !==
|
||||
project.projectId,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormLabel className="text-sm text-muted-foreground">
|
||||
{service.name}
|
||||
</FormLabel>
|
||||
</FormItem>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
),
|
||||
)}
|
||||
</FormItem>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button type="submit" disabled={isAssigningRole}>
|
||||
{isAssigningRole ? "Assigning..." : "Save Role"}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
</Form>
|
||||
</TabsContent>
|
||||
|
||||
{/* Create Role Tab Content */}
|
||||
<TabsContent value="create">
|
||||
<Form {...createForm}>
|
||||
<form onSubmit={createForm.handleSubmit(onCreateRole)}>
|
||||
<div className="space-y-4 py-4">
|
||||
<FormField
|
||||
control={createForm.control}
|
||||
name="name"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Role Name</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="e.g. Developer" {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
Role name must be unique
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={createForm.control}
|
||||
name="description"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Description</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="e.g. Role for development team members"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={createForm.control}
|
||||
name="permissions"
|
||||
render={() => (
|
||||
<FormItem>
|
||||
<FormLabel>Permissions</FormLabel>
|
||||
<Card className=" bg-transparent">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-sm">
|
||||
Available Permissions
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Select the permissions for this role
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="grid grid-cols-2 gap-4">
|
||||
{defaultRoles?.permissions?.map((permission) => (
|
||||
<FormField
|
||||
key={permission.name}
|
||||
control={createForm.control}
|
||||
name="permissions"
|
||||
render={({ field }) => (
|
||||
<FormItem
|
||||
key={permission.name}
|
||||
className="flex flex-row items-start space-x-3 space-y-0"
|
||||
>
|
||||
<FormControl>
|
||||
<Checkbox
|
||||
checked={field.value?.includes(
|
||||
permission.name,
|
||||
)}
|
||||
onCheckedChange={(checked) => {
|
||||
return checked
|
||||
? field.onChange([
|
||||
...(field.value || []),
|
||||
permission.name,
|
||||
])
|
||||
: field.onChange(
|
||||
field.value?.filter(
|
||||
(value) =>
|
||||
value !== permission.name,
|
||||
),
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormLabel className="text-sm font-normal">
|
||||
{permission.description}
|
||||
</FormLabel>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
))}
|
||||
</CardContent>
|
||||
</Card>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={isCreatingRole || isUpdatingRole}
|
||||
>
|
||||
{isCreatingRole || isUpdatingRole
|
||||
? "Saving..."
|
||||
: "Save Role"}
|
||||
</Button>
|
||||
{editingRole && (
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={cancelEdit}
|
||||
disabled={isUpdatingRole}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
)}
|
||||
</DialogFooter>
|
||||
</form>
|
||||
</Form>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
@@ -1,444 +0,0 @@
|
||||
import { AlertBlock } from "@/components/shared/alert-block";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import { DropdownMenuItem } from "@/components/ui/dropdown-menu";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { extractServices } from "@/pages/dashboard/project/[projectId]";
|
||||
import { api } from "@/utils/api";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useEffect } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
|
||||
const addPermissions = z.object({
|
||||
accessedProjects: z.array(z.string()).optional(),
|
||||
accessedServices: z.array(z.string()).optional(),
|
||||
canCreateProjects: z.boolean().optional().default(false),
|
||||
canCreateServices: z.boolean().optional().default(false),
|
||||
canDeleteProjects: z.boolean().optional().default(false),
|
||||
canDeleteServices: z.boolean().optional().default(false),
|
||||
canAccessToTraefikFiles: z.boolean().optional().default(false),
|
||||
canAccessToDocker: z.boolean().optional().default(false),
|
||||
canAccessToAPI: z.boolean().optional().default(false),
|
||||
canAccessToSSHKeys: z.boolean().optional().default(false),
|
||||
canAccessToGitProviders: z.boolean().optional().default(false),
|
||||
});
|
||||
|
||||
type AddPermissions = z.infer<typeof addPermissions>;
|
||||
|
||||
interface Props {
|
||||
userId: string;
|
||||
}
|
||||
|
||||
export const AddUserPermissions = ({ userId }: Props) => {
|
||||
const { data: projects } = api.project.all.useQuery();
|
||||
|
||||
const { data, refetch } = api.user.one.useQuery(
|
||||
{
|
||||
userId,
|
||||
},
|
||||
{
|
||||
enabled: !!userId,
|
||||
},
|
||||
);
|
||||
|
||||
const { mutateAsync, isError, error, isLoading } =
|
||||
api.user.assignPermissions.useMutation();
|
||||
|
||||
const form = useForm<AddPermissions>({
|
||||
defaultValues: {
|
||||
accessedProjects: [],
|
||||
accessedServices: [],
|
||||
},
|
||||
resolver: zodResolver(addPermissions),
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
form.reset({
|
||||
accessedProjects: data.accessedProjects || [],
|
||||
accessedServices: data.accessedServices || [],
|
||||
canCreateProjects: data.canCreateProjects,
|
||||
canCreateServices: data.canCreateServices,
|
||||
canDeleteProjects: data.canDeleteProjects,
|
||||
canDeleteServices: data.canDeleteServices,
|
||||
canAccessToTraefikFiles: data.canAccessToTraefikFiles,
|
||||
canAccessToDocker: data.canAccessToDocker,
|
||||
canAccessToAPI: data.canAccessToAPI,
|
||||
canAccessToSSHKeys: data.canAccessToSSHKeys,
|
||||
canAccessToGitProviders: data.canAccessToGitProviders,
|
||||
});
|
||||
}
|
||||
}, [form, form.formState.isSubmitSuccessful, form.reset, data]);
|
||||
|
||||
const onSubmit = async (data: AddPermissions) => {
|
||||
await mutateAsync({
|
||||
id: userId,
|
||||
canCreateServices: data.canCreateServices,
|
||||
canCreateProjects: data.canCreateProjects,
|
||||
canDeleteServices: data.canDeleteServices,
|
||||
canDeleteProjects: data.canDeleteProjects,
|
||||
canAccessToTraefikFiles: data.canAccessToTraefikFiles,
|
||||
accessedProjects: data.accessedProjects || [],
|
||||
accessedServices: data.accessedServices || [],
|
||||
canAccessToDocker: data.canAccessToDocker,
|
||||
canAccessToAPI: data.canAccessToAPI,
|
||||
canAccessToSSHKeys: data.canAccessToSSHKeys,
|
||||
canAccessToGitProviders: data.canAccessToGitProviders,
|
||||
})
|
||||
.then(async () => {
|
||||
toast.success("Permissions updated");
|
||||
refetch();
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error updating the permissions");
|
||||
});
|
||||
};
|
||||
return (
|
||||
<Dialog>
|
||||
<DialogTrigger className="" asChild>
|
||||
<DropdownMenuItem
|
||||
className="w-full cursor-pointer"
|
||||
onSelect={(e) => e.preventDefault()}
|
||||
>
|
||||
Add Permissions
|
||||
</DropdownMenuItem>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-h-[85vh] sm:max-w-4xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Permissions</DialogTitle>
|
||||
<DialogDescription>Add or remove permissions</DialogDescription>
|
||||
</DialogHeader>
|
||||
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
||||
|
||||
<Form {...form}>
|
||||
<form
|
||||
id="hook-form-add-permissions"
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="grid grid-cols-1 md:grid-cols-2 w-full gap-4"
|
||||
>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="canCreateProjects"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm">
|
||||
<div className="space-y-0.5">
|
||||
<FormLabel>Create Projects</FormLabel>
|
||||
<FormDescription>
|
||||
Allow the user to create projects
|
||||
</FormDescription>
|
||||
</div>
|
||||
<FormControl>
|
||||
<Switch
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="canDeleteProjects"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm">
|
||||
<div className="space-y-0.5">
|
||||
<FormLabel>Delete Projects</FormLabel>
|
||||
<FormDescription>
|
||||
Allow the user to delete projects
|
||||
</FormDescription>
|
||||
</div>
|
||||
<FormControl>
|
||||
<Switch
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="canCreateServices"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm">
|
||||
<div className="space-y-0.5">
|
||||
<FormLabel>Create Services</FormLabel>
|
||||
<FormDescription>
|
||||
Allow the user to create services
|
||||
</FormDescription>
|
||||
</div>
|
||||
<FormControl>
|
||||
<Switch
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="canDeleteServices"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm">
|
||||
<div className="space-y-0.5">
|
||||
<FormLabel>Delete Services</FormLabel>
|
||||
<FormDescription>
|
||||
Allow the user to delete services
|
||||
</FormDescription>
|
||||
</div>
|
||||
<FormControl>
|
||||
<Switch
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="canAccessToTraefikFiles"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm">
|
||||
<div className="space-y-0.5">
|
||||
<FormLabel>Access to Traefik Files</FormLabel>
|
||||
<FormDescription>
|
||||
Allow the user to access to the Traefik Tab Files
|
||||
</FormDescription>
|
||||
</div>
|
||||
<FormControl>
|
||||
<Switch
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="canAccessToDocker"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm">
|
||||
<div className="space-y-0.5">
|
||||
<FormLabel>Access to Docker</FormLabel>
|
||||
<FormDescription>
|
||||
Allow the user to access to the Docker Tab
|
||||
</FormDescription>
|
||||
</div>
|
||||
<FormControl>
|
||||
<Switch
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="canAccessToAPI"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm">
|
||||
<div className="space-y-0.5">
|
||||
<FormLabel>Access to API/CLI</FormLabel>
|
||||
<FormDescription>
|
||||
Allow the user to access to the API/CLI
|
||||
</FormDescription>
|
||||
</div>
|
||||
<FormControl>
|
||||
<Switch
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="canAccessToSSHKeys"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm">
|
||||
<div className="space-y-0.5">
|
||||
<FormLabel>Access to SSH Keys</FormLabel>
|
||||
<FormDescription>
|
||||
Allow to users to access to the SSH Keys section
|
||||
</FormDescription>
|
||||
</div>
|
||||
<FormControl>
|
||||
<Switch
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="canAccessToGitProviders"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm">
|
||||
<div className="space-y-0.5">
|
||||
<FormLabel>Access to Git Providers</FormLabel>
|
||||
<FormDescription>
|
||||
Allow to users to access to the Git Providers section
|
||||
</FormDescription>
|
||||
</div>
|
||||
<FormControl>
|
||||
<Switch
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="accessedProjects"
|
||||
render={() => (
|
||||
<FormItem className="md:col-span-2">
|
||||
<div className="mb-4">
|
||||
<FormLabel className="text-base">Projects</FormLabel>
|
||||
<FormDescription>
|
||||
Select the Projects that the user can access
|
||||
</FormDescription>
|
||||
</div>
|
||||
{projects?.length === 0 && (
|
||||
<p className="text-sm text-muted-foreground">
|
||||
No projects found
|
||||
</p>
|
||||
)}
|
||||
<div className="grid md:grid-cols-2 gap-4">
|
||||
{projects?.map((item, index) => {
|
||||
const applications = extractServices(item);
|
||||
return (
|
||||
<FormField
|
||||
key={`project-${index}`}
|
||||
control={form.control}
|
||||
name="accessedProjects"
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<FormItem
|
||||
key={item.projectId}
|
||||
className="flex flex-col items-start space-x-4 rounded-lg p-4 border"
|
||||
>
|
||||
<div className="flex flex-row gap-4">
|
||||
<FormControl>
|
||||
<Checkbox
|
||||
checked={field.value?.includes(
|
||||
item.projectId,
|
||||
)}
|
||||
onCheckedChange={(checked) => {
|
||||
return checked
|
||||
? field.onChange([
|
||||
...(field.value || []),
|
||||
item.projectId,
|
||||
])
|
||||
: field.onChange(
|
||||
field.value?.filter(
|
||||
(value) =>
|
||||
value !== item.projectId,
|
||||
),
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormLabel className="text-sm font-medium text-primary">
|
||||
{item.name}
|
||||
</FormLabel>
|
||||
</div>
|
||||
{applications.length === 0 && (
|
||||
<p className="text-sm text-muted-foreground">
|
||||
No services found
|
||||
</p>
|
||||
)}
|
||||
{applications?.map((item, index) => (
|
||||
<FormField
|
||||
key={`project-${index}`}
|
||||
control={form.control}
|
||||
name="accessedServices"
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<FormItem
|
||||
key={item.id}
|
||||
className="flex flex-row items-start space-x-3 space-y-0"
|
||||
>
|
||||
<FormControl>
|
||||
<Checkbox
|
||||
checked={field.value?.includes(
|
||||
item.id,
|
||||
)}
|
||||
onCheckedChange={(checked) => {
|
||||
return checked
|
||||
? field.onChange([
|
||||
...(field.value || []),
|
||||
item.id,
|
||||
])
|
||||
: field.onChange(
|
||||
field.value?.filter(
|
||||
(value) =>
|
||||
value !== item.id,
|
||||
),
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormLabel className="text-sm text-muted-foreground">
|
||||
{item.name}
|
||||
</FormLabel>
|
||||
</FormItem>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</FormItem>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<DialogFooter className="flex w-full flex-row justify-end md:col-span-2">
|
||||
<Button
|
||||
isLoading={isLoading}
|
||||
form="hook-form-add-permissions"
|
||||
type="submit"
|
||||
>
|
||||
Update
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
</Form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
@@ -30,9 +30,10 @@ import { format } from "date-fns";
|
||||
import { MoreHorizontal, Users } from "lucide-react";
|
||||
import { Loader2 } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
import { AddUserPermissions } from "./add-permissions";
|
||||
import { AddUserPermissionsV2 } from "./add-permissions-v2";
|
||||
|
||||
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();
|
||||
@@ -84,20 +85,22 @@ export const ShowUsers = () => {
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{data?.map((member) => {
|
||||
const isSameUser = member.user.id === user?.user.id;
|
||||
|
||||
return (
|
||||
<TableRow key={member.id}>
|
||||
<TableCell className="w-[100px]">
|
||||
{member.user.email}
|
||||
<TableCell className="w-[250px]">
|
||||
{member.user.email} {isSameUser && "(You)"}
|
||||
</TableCell>
|
||||
<TableCell className="text-center">
|
||||
<Badge
|
||||
variant={
|
||||
member.role === "owner"
|
||||
member?.role?.name === "owner"
|
||||
? "default"
|
||||
: "secondary"
|
||||
}
|
||||
>
|
||||
{member.role}
|
||||
{member?.role?.name}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell className="text-center">
|
||||
@@ -112,35 +115,77 @@ export const ShowUsers = () => {
|
||||
</TableCell>
|
||||
|
||||
<TableCell className="text-right flex justify-end">
|
||||
<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" && !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>
|
||||
|
||||
{member.role !== "owner" && (
|
||||
<AddUserPermissions
|
||||
userId={member.user.id}
|
||||
/>
|
||||
)}
|
||||
|
||||
{member.role !== "owner" && (
|
||||
<>
|
||||
{!isCloud && (
|
||||
<DialogAction
|
||||
title="Delete User"
|
||||
description="Are you sure you want to delete this user?"
|
||||
type="destructive"
|
||||
onClick={async () => {
|
||||
<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) {
|
||||
await mutateAsync({
|
||||
userId: member.user.id,
|
||||
})
|
||||
@@ -152,86 +197,40 @@ export const ShowUsers = () => {
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error(
|
||||
"Error deleting destination",
|
||||
"Error deleting user",
|
||||
);
|
||||
});
|
||||
}}
|
||||
>
|
||||
<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;
|
||||
}
|
||||
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",
|
||||
);
|
||||
}
|
||||
}}
|
||||
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()}
|
||||
>
|
||||
<DropdownMenuItem
|
||||
className="w-full cursor-pointer text-red-500 hover:!text-red-600"
|
||||
onSelect={(e) => e.preventDefault()}
|
||||
>
|
||||
Unlink User
|
||||
</DropdownMenuItem>
|
||||
</DialogAction>
|
||||
</>
|
||||
)}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
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.user.get.useQuery();
|
||||
const { data, refetch } = api.webServer.get.useQuery();
|
||||
const { mutateAsync, isLoading } =
|
||||
api.settings.assignDomainServer.useMutation();
|
||||
api.webServer.assignDomainServer.useMutation();
|
||||
|
||||
const form = useForm<AddServerDomain>({
|
||||
defaultValues: {
|
||||
@@ -79,10 +79,10 @@ export const WebDomain = () => {
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
form.reset({
|
||||
domain: data?.user?.host || "",
|
||||
certificateType: data?.user?.certificateType,
|
||||
letsEncryptEmail: data?.user?.letsEncryptEmail || "",
|
||||
https: data?.user?.https || false,
|
||||
domain: data?.host || "",
|
||||
certificateType: data?.certificateType,
|
||||
letsEncryptEmail: data?.letsEncryptEmail || "",
|
||||
https: data?.https || false,
|
||||
});
|
||||
}
|
||||
}, [form, form.reset, data]);
|
||||
|
||||
@@ -16,13 +16,12 @@ import { UpdateServer } from "./web-server/update-server";
|
||||
|
||||
export const WebServer = () => {
|
||||
const { t } = useTranslation("settings");
|
||||
const { data } = api.user.get.useQuery();
|
||||
const { data } = api.webServer.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="">
|
||||
@@ -34,14 +33,6 @@ 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 />
|
||||
@@ -53,7 +44,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?.user.serverIp}
|
||||
Server IP: {data?.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.user.get.useQuery();
|
||||
const { data } = api.webServer.get.useQuery();
|
||||
const { data: ip } = api.server.publicIp.useQuery();
|
||||
|
||||
const { mutateAsync, isLoading, error, isError } =
|
||||
api.user.update.useMutation();
|
||||
api.webServer.update.useMutation();
|
||||
|
||||
const form = useForm<Schema>({
|
||||
defaultValues: {
|
||||
serverIp: data?.user.serverIp || "",
|
||||
serverIp: data?.serverIp || "",
|
||||
},
|
||||
resolver: zodResolver(schema),
|
||||
});
|
||||
@@ -62,7 +62,7 @@ export const UpdateServerIp = ({ children }: Props) => {
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
form.reset({
|
||||
serverIp: data.user.serverIp || "",
|
||||
serverIp: data.serverIp || "",
|
||||
});
|
||||
}
|
||||
}, [form, form.reset, data]);
|
||||
@@ -80,7 +80,7 @@ export const UpdateServerIp = ({ children }: Props) => {
|
||||
})
|
||||
.then(async () => {
|
||||
toast.success("Server IP Updated");
|
||||
await utils.user.get.invalidate();
|
||||
await utils.webServer.get.invalidate();
|
||||
setIsOpen(false);
|
||||
})
|
||||
.catch(() => {
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import { api } from "@/utils/api";
|
||||
import type { PermissionName } from "@dokploy/server/lib/permissions";
|
||||
import { useMemo } from "react";
|
||||
|
||||
interface Props {
|
||||
permissions: PermissionName[];
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export const Permissions = ({ permissions, children }: Props) => {
|
||||
const { data: auth } = api.user.get.useQuery();
|
||||
|
||||
const hasPermission = useMemo(() => {
|
||||
if (auth?.role?.name === "owner" || auth?.role?.name === "admin") {
|
||||
return true;
|
||||
}
|
||||
|
||||
return permissions.some((permission) =>
|
||||
auth?.role?.permissions?.includes(permission),
|
||||
);
|
||||
}, [permissions, auth]);
|
||||
|
||||
if (!hasPermission) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <>{children}</>;
|
||||
};
|
||||
@@ -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,10 +102,6 @@ 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
|
||||
| {
|
||||
@@ -119,8 +115,6 @@ type NavItem =
|
||||
}) => boolean;
|
||||
};
|
||||
|
||||
// ExternalLink type
|
||||
// Represents an external link item (used for the help section)
|
||||
type ExternalLink = {
|
||||
name: string;
|
||||
url: string;
|
||||
@@ -131,18 +125,12 @@ 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: [
|
||||
{
|
||||
@@ -165,7 +153,8 @@ const MENU: Menu = {
|
||||
url: "/dashboard/schedules",
|
||||
icon: Clock,
|
||||
// Only enabled in non-cloud environments
|
||||
isEnabled: ({ isCloud, auth }) => !isCloud && auth?.role === "owner",
|
||||
isEnabled: ({ isCloud, auth }) =>
|
||||
!isCloud && auth?.role?.name === "owner",
|
||||
},
|
||||
{
|
||||
isSingle: true,
|
||||
@@ -175,7 +164,10 @@ const MENU: Menu = {
|
||||
// Only enabled for admins and users with access to Traefik files in non-cloud environments
|
||||
isEnabled: ({ auth, isCloud }) =>
|
||||
!!(
|
||||
(auth?.role === "owner" || auth?.canAccessToTraefikFiles) &&
|
||||
(auth?.role?.name === "owner" ||
|
||||
auth?.role?.permissions?.includes(
|
||||
PERMISSIONS.TRAEFIK.ACCESS.name,
|
||||
)) &&
|
||||
!isCloud
|
||||
),
|
||||
},
|
||||
@@ -186,7 +178,11 @@ const MENU: Menu = {
|
||||
icon: BlocksIcon,
|
||||
// Only enabled for admins and users with access to Docker in non-cloud environments
|
||||
isEnabled: ({ auth, isCloud }) =>
|
||||
!!((auth?.role === "owner" || auth?.canAccessToDocker) && !isCloud),
|
||||
!!(
|
||||
(auth?.role?.name === "owner" ||
|
||||
auth?.role?.permissions?.includes(PERMISSIONS.DOCKER.VIEW.name)) &&
|
||||
!isCloud
|
||||
),
|
||||
},
|
||||
{
|
||||
isSingle: true,
|
||||
@@ -195,7 +191,11 @@ const MENU: Menu = {
|
||||
icon: PieChart,
|
||||
// Only enabled for admins and users with access to Docker in non-cloud environments
|
||||
isEnabled: ({ auth, isCloud }) =>
|
||||
!!((auth?.role === "owner" || auth?.canAccessToDocker) && !isCloud),
|
||||
!!(
|
||||
(auth?.role?.name === "owner" ||
|
||||
auth?.role?.permissions?.includes(PERMISSIONS.DOCKER.VIEW.name)) &&
|
||||
!isCloud
|
||||
),
|
||||
},
|
||||
{
|
||||
isSingle: true,
|
||||
@@ -204,64 +204,12 @@ const MENU: Menu = {
|
||||
icon: Forward,
|
||||
// Only enabled for admins and users with access to Docker in non-cloud environments
|
||||
isEnabled: ({ auth, isCloud }) =>
|
||||
!!((auth?.role === "owner" || auth?.canAccessToDocker) && !isCloud),
|
||||
!!(
|
||||
(auth?.role?.name === "owner" ||
|
||||
auth?.role?.permissions?.includes(PERMISSIONS.DOCKER.VIEW.name)) &&
|
||||
!isCloud
|
||||
),
|
||||
},
|
||||
|
||||
// Legacy unused menu, adjusted to the new structure
|
||||
// {
|
||||
// isSingle: true,
|
||||
// title: "Projects",
|
||||
// url: "/dashboard/projects",
|
||||
// icon: Folder,
|
||||
// },
|
||||
// {
|
||||
// isSingle: true,
|
||||
// title: "Monitoring",
|
||||
// icon: BarChartHorizontalBigIcon,
|
||||
// url: "/dashboard/settings/monitoring",
|
||||
// },
|
||||
// {
|
||||
// isSingle: false,
|
||||
// title: "Settings",
|
||||
// icon: Settings2,
|
||||
// items: [
|
||||
// {
|
||||
// title: "Profile",
|
||||
// url: "/dashboard/settings/profile",
|
||||
// },
|
||||
// {
|
||||
// title: "Users",
|
||||
// url: "/dashboard/settings/users",
|
||||
// },
|
||||
// {
|
||||
// title: "SSH Key",
|
||||
// url: "/dashboard/settings/ssh-keys",
|
||||
// },
|
||||
// {
|
||||
// title: "Git",
|
||||
// url: "/dashboard/settings/git-providers",
|
||||
// },
|
||||
// ],
|
||||
// },
|
||||
// {
|
||||
// isSingle: false,
|
||||
// title: "Integrations",
|
||||
// icon: BlocksIcon,
|
||||
// items: [
|
||||
// {
|
||||
// title: "S3 Destinations",
|
||||
// url: "/dashboard/settings/destinations",
|
||||
// },
|
||||
// {
|
||||
// title: "Registry",
|
||||
// url: "/dashboard/settings/registry",
|
||||
// },
|
||||
// {
|
||||
// title: "Notifications",
|
||||
// url: "/dashboard/settings/notifications",
|
||||
// },
|
||||
// ],
|
||||
// },
|
||||
],
|
||||
|
||||
settings: [
|
||||
@@ -271,7 +219,8 @@ const MENU: Menu = {
|
||||
url: "/dashboard/settings/server",
|
||||
icon: Activity,
|
||||
// Only enabled for admins in non-cloud environments
|
||||
isEnabled: ({ auth, isCloud }) => !!(auth?.role === "owner" && !isCloud),
|
||||
isEnabled: ({ auth, isCloud }) =>
|
||||
!!(auth?.role?.name === "owner" && !isCloud),
|
||||
},
|
||||
{
|
||||
isSingle: true,
|
||||
@@ -285,7 +234,7 @@ const MENU: Menu = {
|
||||
url: "/dashboard/settings/servers",
|
||||
icon: Server,
|
||||
// Only enabled for admins
|
||||
isEnabled: ({ auth }) => !!(auth?.role === "owner"),
|
||||
isEnabled: ({ auth }) => !!(auth?.role?.name === "owner"),
|
||||
},
|
||||
{
|
||||
isSingle: true,
|
||||
@@ -293,7 +242,7 @@ const MENU: Menu = {
|
||||
icon: Users,
|
||||
url: "/dashboard/settings/users",
|
||||
// Only enabled for admins
|
||||
isEnabled: ({ auth }) => !!(auth?.role === "owner"),
|
||||
isEnabled: ({ auth }) => !!(auth?.role?.name === "owner"),
|
||||
},
|
||||
{
|
||||
isSingle: true,
|
||||
@@ -302,14 +251,17 @@ const MENU: Menu = {
|
||||
url: "/dashboard/settings/ssh-keys",
|
||||
// Only enabled for admins and users with access to SSH keys
|
||||
isEnabled: ({ auth }) =>
|
||||
!!(auth?.role === "owner" || auth?.canAccessToSSHKeys),
|
||||
!!(
|
||||
auth?.role?.name === "owner" ||
|
||||
auth?.role?.permissions?.includes(PERMISSIONS.SSH_KEYS.ACCESS.name)
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "AI",
|
||||
icon: BotIcon,
|
||||
url: "/dashboard/settings/ai",
|
||||
isSingle: true,
|
||||
isEnabled: ({ auth }) => !!(auth?.role === "owner"),
|
||||
isEnabled: ({ auth }) => !!(auth?.role?.name === "owner"),
|
||||
},
|
||||
{
|
||||
isSingle: true,
|
||||
@@ -318,7 +270,12 @@ const MENU: Menu = {
|
||||
icon: GitBranch,
|
||||
// Only enabled for admins and users with access to Git providers
|
||||
isEnabled: ({ auth }) =>
|
||||
!!(auth?.role === "owner" || auth?.canAccessToGitProviders),
|
||||
!!(
|
||||
auth?.role?.name === "owner" ||
|
||||
auth?.role?.permissions?.includes(
|
||||
PERMISSIONS.GIT_PROVIDERS.ACCESS.name,
|
||||
)
|
||||
),
|
||||
},
|
||||
{
|
||||
isSingle: true,
|
||||
@@ -326,7 +283,7 @@ const MENU: Menu = {
|
||||
url: "/dashboard/settings/registry",
|
||||
icon: Package,
|
||||
// Only enabled for admins
|
||||
isEnabled: ({ auth }) => !!(auth?.role === "owner"),
|
||||
isEnabled: ({ auth }) => !!(auth?.role?.name === "owner"),
|
||||
},
|
||||
{
|
||||
isSingle: true,
|
||||
@@ -334,7 +291,7 @@ const MENU: Menu = {
|
||||
url: "/dashboard/settings/destinations",
|
||||
icon: Database,
|
||||
// Only enabled for admins
|
||||
isEnabled: ({ auth }) => !!(auth?.role === "owner"),
|
||||
isEnabled: ({ auth }) => !!(auth?.role?.name === "owner"),
|
||||
},
|
||||
|
||||
{
|
||||
@@ -343,7 +300,7 @@ const MENU: Menu = {
|
||||
url: "/dashboard/settings/certificates",
|
||||
icon: ShieldCheck,
|
||||
// Only enabled for admins
|
||||
isEnabled: ({ auth }) => !!(auth?.role === "owner"),
|
||||
isEnabled: ({ auth }) => !!(auth?.role?.name === "owner"),
|
||||
},
|
||||
{
|
||||
isSingle: true,
|
||||
@@ -351,7 +308,8 @@ const MENU: Menu = {
|
||||
url: "/dashboard/settings/cluster",
|
||||
icon: Boxes,
|
||||
// Only enabled for admins in non-cloud environments
|
||||
isEnabled: ({ auth, isCloud }) => !!(auth?.role === "owner" && !isCloud),
|
||||
isEnabled: ({ auth, isCloud }) =>
|
||||
!!(auth?.role?.name === "owner" && !isCloud),
|
||||
},
|
||||
{
|
||||
isSingle: true,
|
||||
@@ -359,7 +317,7 @@ const MENU: Menu = {
|
||||
url: "/dashboard/settings/notifications",
|
||||
icon: Bell,
|
||||
// Only enabled for admins
|
||||
isEnabled: ({ auth }) => !!(auth?.role === "owner"),
|
||||
isEnabled: ({ auth }) => !!(auth?.role?.name === "owner"),
|
||||
},
|
||||
{
|
||||
isSingle: true,
|
||||
@@ -367,7 +325,8 @@ const MENU: Menu = {
|
||||
url: "/dashboard/settings/billing",
|
||||
icon: CreditCard,
|
||||
// Only enabled for admins in cloud environments
|
||||
isEnabled: ({ auth, isCloud }) => !!(auth?.role === "owner" && isCloud),
|
||||
isEnabled: ({ auth, isCloud }) =>
|
||||
!!(auth?.role?.name === "owner" && isCloud),
|
||||
},
|
||||
],
|
||||
|
||||
@@ -505,6 +464,7 @@ function SidebarLogo() {
|
||||
const { state } = useSidebar();
|
||||
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||
const { data: user } = api.user.get.useQuery();
|
||||
console.log(user);
|
||||
const { data: session } = authClient.useSession();
|
||||
|
||||
const {
|
||||
@@ -663,7 +623,7 @@ function SidebarLogo() {
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
{(user?.role === "owner" || isCloud) && (
|
||||
{(user?.role?.name === "owner" || isCloud) && (
|
||||
<>
|
||||
<DropdownMenuSeparator />
|
||||
<AddOrganization />
|
||||
@@ -1029,7 +989,7 @@ export default function Page({ children }: Props) {
|
||||
</SidebarContent>
|
||||
<SidebarFooter>
|
||||
<SidebarMenu className="flex flex-col gap-2">
|
||||
{!isCloud && auth?.role === "owner" && (
|
||||
{!isCloud && auth?.role?.name === "owner" && (
|
||||
<SidebarMenuItem>
|
||||
<UpdateServerButton />
|
||||
</SidebarMenuItem>
|
||||
|
||||
@@ -23,6 +23,8 @@ 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;
|
||||
|
||||
@@ -98,7 +100,7 @@ export const UserNav = () => {
|
||||
>
|
||||
Monitoring
|
||||
</DropdownMenuItem>
|
||||
{(data?.role === "owner" || data?.canAccessToTraefikFiles) && (
|
||||
<Permissions permissions={[PERMISSIONS.TRAEFIK.ACCESS.name]}>
|
||||
<DropdownMenuItem
|
||||
className="cursor-pointer"
|
||||
onClick={() => {
|
||||
@@ -107,8 +109,9 @@ export const UserNav = () => {
|
||||
>
|
||||
Traefik
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
{(data?.role === "owner" || data?.canAccessToDocker) && (
|
||||
</Permissions>
|
||||
|
||||
<Permissions permissions={[PERMISSIONS.DOCKER.VIEW.name]}>
|
||||
<DropdownMenuItem
|
||||
className="cursor-pointer"
|
||||
onClick={() => {
|
||||
@@ -119,11 +122,11 @@ export const UserNav = () => {
|
||||
>
|
||||
Docker
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</Permissions>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{data?.role === "owner" && (
|
||||
{data?.role?.name === "owner" && (
|
||||
<DropdownMenuItem
|
||||
className="cursor-pointer"
|
||||
onClick={() => {
|
||||
@@ -136,7 +139,7 @@ export const UserNav = () => {
|
||||
</>
|
||||
)}
|
||||
</DropdownMenuGroup>
|
||||
{isCloud && data?.role === "owner" && (
|
||||
{isCloud && data?.role?.name === "owner" && (
|
||||
<DropdownMenuItem
|
||||
className="cursor-pointer"
|
||||
onClick={() => {
|
||||
@@ -154,9 +157,6 @@ export const UserNav = () => {
|
||||
await authClient.signOut().then(() => {
|
||||
router.push("/");
|
||||
});
|
||||
// await mutateAsync().then(() => {
|
||||
// router.push("/");
|
||||
// });
|
||||
}}
|
||||
>
|
||||
Log out
|
||||
|
||||
@@ -0,0 +1,187 @@
|
||||
CREATE TABLE "member_role" (
|
||||
"roleId" text PRIMARY KEY NOT NULL,
|
||||
"name" text NOT NULL,
|
||||
"description" text,
|
||||
"canDelete" boolean DEFAULT true NOT NULL,
|
||||
"is_system" boolean DEFAULT false,
|
||||
"permissions" text[],
|
||||
"created_at" timestamp DEFAULT now() NOT NULL,
|
||||
"updated_at" timestamp DEFAULT now() NOT NULL,
|
||||
"organizationId" text NOT NULL,
|
||||
CONSTRAINT "member_role_name_unique" UNIQUE("name"),
|
||||
CONSTRAINT "role_name_unique" UNIQUE("name","organizationId")
|
||||
);
|
||||
|
||||
-- Create default roles for each organization
|
||||
DO $$
|
||||
DECLARE
|
||||
org RECORD;
|
||||
BEGIN
|
||||
FOR org IN SELECT id FROM "organization"
|
||||
LOOP
|
||||
-- Insert owner role
|
||||
INSERT INTO "member_role" ("roleId", "name", "description", "canDelete", "is_system", "permissions", "created_at", "updated_at", "organizationId")
|
||||
VALUES (
|
||||
org.id || '_owner',
|
||||
'owner',
|
||||
'Owner role with full access',
|
||||
false,
|
||||
true,
|
||||
'{"project:create", "project:delete", "service:create", "service:delete", "traefik_files:access", "docker:view", "api:access", "ssh_keys:access", "git_providers:access", "schedules:access"}',
|
||||
NOW(),
|
||||
NOW(),
|
||||
org.id
|
||||
);
|
||||
|
||||
-- Insert admin role
|
||||
INSERT INTO "member_role" ("roleId", "name", "description", "canDelete", "is_system", "permissions", "created_at", "updated_at", "organizationId")
|
||||
VALUES (
|
||||
org.id || '_admin',
|
||||
'admin',
|
||||
'Administrator role with elevated access',
|
||||
false,
|
||||
true,
|
||||
'{"project:create", "project:delete", "service:create", "service:delete", "traefik_files:access", "docker:view", "api:access", "ssh_keys:access", "schedules:access"}',
|
||||
NOW(),
|
||||
NOW(),
|
||||
org.id
|
||||
);
|
||||
|
||||
-- Insert member role
|
||||
INSERT INTO "member_role" ("roleId", "name", "description", "canDelete", "is_system", "permissions", "created_at", "updated_at", "organizationId")
|
||||
VALUES (
|
||||
org.id || '_member',
|
||||
'member',
|
||||
'Standard member role',
|
||||
false,
|
||||
true,
|
||||
'{"project:create", "service:create", "docker:view"}',
|
||||
NOW(),
|
||||
NOW(),
|
||||
org.id
|
||||
);
|
||||
END LOOP;
|
||||
END $$;
|
||||
|
||||
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "user_temp" RENAME TO "users";--> statement-breakpoint
|
||||
ALTER TABLE "users" DROP CONSTRAINT "user_temp_email_unique";--> statement-breakpoint
|
||||
ALTER TABLE "backup" DROP CONSTRAINT "backup_userId_user_temp_id_fk";
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "session_temp" DROP CONSTRAINT "session_temp_user_id_user_temp_id_fk";
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "git_provider" DROP CONSTRAINT "git_provider_userId_user_temp_id_fk";
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "account" DROP CONSTRAINT "account_user_id_user_temp_id_fk";
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "apikey" DROP CONSTRAINT "apikey_user_id_user_temp_id_fk";
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "invitation" DROP CONSTRAINT "invitation_inviter_id_user_temp_id_fk";
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "member" DROP CONSTRAINT "member_user_id_user_temp_id_fk";
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "organization" DROP CONSTRAINT "organization_owner_id_user_temp_id_fk";
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "two_factor" DROP CONSTRAINT "two_factor_user_id_user_temp_id_fk";
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "schedule" DROP CONSTRAINT "schedule_userId_user_temp_id_fk";
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "users" ALTER COLUMN "created_at" SET NOT NULL;--> statement-breakpoint
|
||||
ALTER TABLE "users" ALTER COLUMN "updated_at" SET DEFAULT now();--> statement-breakpoint
|
||||
ALTER TABLE "member" ALTER COLUMN "role" DROP NOT NULL;--> statement-breakpoint
|
||||
ALTER TABLE "member" ADD COLUMN "roleId" text;--> statement-breakpoint
|
||||
ALTER TABLE "member_role" ADD CONSTRAINT "member_role_organizationId_organization_id_fk" FOREIGN KEY ("organizationId") REFERENCES "public"."organization"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "backup" ADD CONSTRAINT "backup_userId_users_id_fk" FOREIGN KEY ("userId") REFERENCES "public"."users"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "session_temp" ADD CONSTRAINT "session_temp_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "git_provider" ADD CONSTRAINT "git_provider_userId_users_id_fk" FOREIGN KEY ("userId") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "account" ADD CONSTRAINT "account_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "apikey" ADD CONSTRAINT "apikey_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "invitation" ADD CONSTRAINT "invitation_inviter_id_users_id_fk" FOREIGN KEY ("inviter_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "member" ADD CONSTRAINT "member_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "member" ADD CONSTRAINT "member_roleId_member_role_roleId_fk" FOREIGN KEY ("roleId") REFERENCES "public"."member_role"("roleId") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||
|
||||
-- Update existing members with corresponding roles based on their current role type
|
||||
DO $$
|
||||
DECLARE
|
||||
mem RECORD;
|
||||
BEGIN
|
||||
FOR mem IN SELECT m.id, m.organization_id, m.role as role_type FROM "member" m
|
||||
LOOP
|
||||
UPDATE "member"
|
||||
SET "roleId" = mem.organization_id || '_' || mem.role_type
|
||||
WHERE id = mem.id;
|
||||
END LOOP;
|
||||
END $$;
|
||||
|
||||
|
||||
ALTER TABLE "organization" ADD CONSTRAINT "organization_owner_id_users_id_fk" FOREIGN KEY ("owner_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "two_factor" ADD CONSTRAINT "two_factor_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "schedule" ADD CONSTRAINT "schedule_userId_users_id_fk" FOREIGN KEY ("userId") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||
|
||||
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "web_server" (
|
||||
"webServerId" text PRIMARY KEY NOT NULL,
|
||||
"serverIp" text,
|
||||
"certificateType" "certificateType" DEFAULT 'none' NOT NULL,
|
||||
"https" boolean DEFAULT false NOT NULL,
|
||||
"host" text,
|
||||
"letsEncryptEmail" text,
|
||||
"sshPrivateKey" text,
|
||||
"enableDockerCleanup" boolean DEFAULT false NOT NULL,
|
||||
"logCleanupCron" text DEFAULT '0 0 * * *',
|
||||
"metricsConfig" jsonb DEFAULT '{"server":{"type":"Dokploy","refreshRate":60,"port":4500,"token":"","retentionDays":2,"cronJob":"","urlCallback":"","thresholds":{"cpu":0,"memory":0}},"containers":{"refreshRate":60,"services":{"include":[],"exclude":[]}}}'::jsonb NOT NULL
|
||||
);
|
||||
|
||||
INSERT INTO "web_server" (
|
||||
"webServerId",
|
||||
"serverIp",
|
||||
"certificateType",
|
||||
"https",
|
||||
"host",
|
||||
"letsEncryptEmail",
|
||||
"sshPrivateKey",
|
||||
"enableDockerCleanup",
|
||||
"logCleanupCron",
|
||||
"metricsConfig"
|
||||
)
|
||||
SELECT
|
||||
gen_random_uuid() as "webServerId",
|
||||
u."serverIp",
|
||||
COALESCE(u."certificateType", 'none') as "certificateType",
|
||||
COALESCE(u."https", false) as "https",
|
||||
u."host",
|
||||
u."letsEncryptEmail",
|
||||
u."sshPrivateKey",
|
||||
COALESCE(u."enableDockerCleanup", false) as "enableDockerCleanup",
|
||||
COALESCE(u."logCleanupCron", '0 0 * * *') as "logCleanupCron",
|
||||
COALESCE(u."metricsConfig", '{"server":{"type":"Dokploy","refreshRate":60,"port":4500,"token":"","retentionDays":2,"cronJob":"","urlCallback":"","thresholds":{"cpu":0,"memory":0}},"containers":{"refreshRate":60,"services":{"include":[],"exclude":[]}}}') as "metricsConfig"
|
||||
FROM "users" u
|
||||
INNER JOIN "organization" o ON u.id = o.owner_id
|
||||
LIMIT 1;
|
||||
|
||||
|
||||
ALTER TABLE "users" DROP COLUMN "createdAt";--> statement-breakpoint
|
||||
ALTER TABLE "users" DROP COLUMN "serverIp";--> statement-breakpoint
|
||||
ALTER TABLE "users" DROP COLUMN "certificateType";--> statement-breakpoint
|
||||
ALTER TABLE "users" DROP COLUMN "https";--> statement-breakpoint
|
||||
ALTER TABLE "users" DROP COLUMN "host";--> statement-breakpoint
|
||||
ALTER TABLE "users" DROP COLUMN "letsEncryptEmail";--> statement-breakpoint
|
||||
ALTER TABLE "users" DROP COLUMN "sshPrivateKey";--> statement-breakpoint
|
||||
ALTER TABLE "users" DROP COLUMN "enableDockerCleanup";--> statement-breakpoint
|
||||
ALTER TABLE "users" DROP COLUMN "logCleanupCron";--> statement-breakpoint
|
||||
ALTER TABLE "users" DROP COLUMN "metricsConfig";--> statement-breakpoint
|
||||
ALTER TABLE "users" DROP COLUMN "cleanupCacheApplications";--> statement-breakpoint
|
||||
ALTER TABLE "users" DROP COLUMN "cleanupCacheOnPreviews";--> statement-breakpoint
|
||||
ALTER TABLE "users" DROP COLUMN "cleanupCacheOnCompose";--> statement-breakpoint
|
||||
ALTER TABLE "member" DROP COLUMN "canCreateProjects";--> statement-breakpoint
|
||||
ALTER TABLE "member" DROP COLUMN "canAccessToSSHKeys";--> statement-breakpoint
|
||||
ALTER TABLE "member" DROP COLUMN "canCreateServices";--> statement-breakpoint
|
||||
ALTER TABLE "member" DROP COLUMN "canDeleteProjects";--> statement-breakpoint
|
||||
ALTER TABLE "member" DROP COLUMN "canDeleteServices";--> statement-breakpoint
|
||||
ALTER TABLE "member" DROP COLUMN "canAccessToDocker";--> statement-breakpoint
|
||||
ALTER TABLE "member" DROP COLUMN "canAccessToAPI";--> statement-breakpoint
|
||||
ALTER TABLE "member" DROP COLUMN "canAccessToGitProviders";--> statement-breakpoint
|
||||
ALTER TABLE "member" DROP COLUMN "canAccessToTraefikFiles";--> statement-breakpoint
|
||||
ALTER TABLE "users" ADD CONSTRAINT "users_email_unique" UNIQUE("email");
|
||||
File diff suppressed because it is too large
Load Diff
@@ -722,6 +722,13 @@
|
||||
"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_temp)
|
||||
// .insert(schema.users)
|
||||
// .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_temp)
|
||||
// .insert(schema.users)
|
||||
// .values({
|
||||
// id: member.userId,
|
||||
// email: member.auth.email,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { buffer } from "node:stream/consumers";
|
||||
import { db } from "@/server/db";
|
||||
import { organization, server, users_temp } from "@/server/db/schema";
|
||||
import { organization, server, users } from "@/server/db/schema";
|
||||
import { type Server, findUserById } from "@dokploy/server";
|
||||
import { 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_temp)
|
||||
.update(users)
|
||||
.set({
|
||||
stripeCustomerId: session.customer as string,
|
||||
stripeSubscriptionId: session.subscription as string,
|
||||
serversQuantity: subscription?.items?.data?.[0]?.quantity ?? 0,
|
||||
})
|
||||
.where(eq(users_temp.id, adminId))
|
||||
.where(eq(users.id, adminId))
|
||||
.returning();
|
||||
|
||||
const admin = await findUserById(adminId);
|
||||
@@ -85,14 +85,12 @@ export default async function handler(
|
||||
const newSubscription = event.data.object as Stripe.Subscription;
|
||||
|
||||
await db
|
||||
.update(users_temp)
|
||||
.update(users)
|
||||
.set({
|
||||
stripeSubscriptionId: newSubscription.id,
|
||||
stripeCustomerId: newSubscription.customer as string,
|
||||
})
|
||||
.where(
|
||||
eq(users_temp.stripeCustomerId, newSubscription.customer as string),
|
||||
)
|
||||
.where(eq(users.stripeCustomerId, newSubscription.customer as string))
|
||||
.returning();
|
||||
|
||||
break;
|
||||
@@ -102,14 +100,12 @@ export default async function handler(
|
||||
const newSubscription = event.data.object as Stripe.Subscription;
|
||||
|
||||
await db
|
||||
.update(users_temp)
|
||||
.update(users)
|
||||
.set({
|
||||
stripeSubscriptionId: null,
|
||||
serversQuantity: 0,
|
||||
})
|
||||
.where(
|
||||
eq(users_temp.stripeCustomerId, newSubscription.customer as string),
|
||||
);
|
||||
.where(eq(users.stripeCustomerId, newSubscription.customer as string));
|
||||
|
||||
const admin = await findUserByStripeCustomerId(
|
||||
newSubscription.customer as string,
|
||||
@@ -135,12 +131,12 @@ export default async function handler(
|
||||
|
||||
if (newSubscription.status === "active") {
|
||||
await db
|
||||
.update(users_temp)
|
||||
.update(users)
|
||||
.set({
|
||||
serversQuantity: newSubscription?.items?.data?.[0]?.quantity ?? 0,
|
||||
})
|
||||
.where(
|
||||
eq(users_temp.stripeCustomerId, newSubscription.customer as string),
|
||||
eq(users.stripeCustomerId, newSubscription.customer as string),
|
||||
);
|
||||
|
||||
const newServersQuantity = admin.serversQuantity;
|
||||
@@ -148,10 +144,10 @@ export default async function handler(
|
||||
} else {
|
||||
await disableServers(admin.id);
|
||||
await db
|
||||
.update(users_temp)
|
||||
.update(users)
|
||||
.set({ serversQuantity: 0 })
|
||||
.where(
|
||||
eq(users_temp.stripeCustomerId, newSubscription.customer as string),
|
||||
eq(users.stripeCustomerId, newSubscription.customer as string),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -172,11 +168,11 @@ export default async function handler(
|
||||
}
|
||||
|
||||
await db
|
||||
.update(users_temp)
|
||||
.update(users)
|
||||
.set({
|
||||
serversQuantity: suscription?.items?.data?.[0]?.quantity ?? 0,
|
||||
})
|
||||
.where(eq(users_temp.stripeCustomerId, suscription.customer as string));
|
||||
.where(eq(users.stripeCustomerId, suscription.customer as string));
|
||||
|
||||
const admin = await findUserByStripeCustomerId(
|
||||
suscription.customer as string,
|
||||
@@ -205,13 +201,11 @@ export default async function handler(
|
||||
return res.status(400).send("Webhook Error: Admin not found");
|
||||
}
|
||||
await db
|
||||
.update(users_temp)
|
||||
.update(users)
|
||||
.set({
|
||||
serversQuantity: 0,
|
||||
})
|
||||
.where(
|
||||
eq(users_temp.stripeCustomerId, newInvoice.customer as string),
|
||||
);
|
||||
.where(eq(users.stripeCustomerId, newInvoice.customer as string));
|
||||
|
||||
await disableServers(admin.id);
|
||||
}
|
||||
@@ -229,13 +223,13 @@ export default async function handler(
|
||||
|
||||
await disableServers(admin.id);
|
||||
await db
|
||||
.update(users_temp)
|
||||
.update(users)
|
||||
.set({
|
||||
stripeCustomerId: null,
|
||||
stripeSubscriptionId: null,
|
||||
serversQuantity: 0,
|
||||
})
|
||||
.where(eq(users_temp.stripeCustomerId, customer.id));
|
||||
.where(eq(users.stripeCustomerId, customer.id));
|
||||
|
||||
break;
|
||||
}
|
||||
@@ -262,8 +256,8 @@ const disableServers = async (userId: string) => {
|
||||
};
|
||||
|
||||
const findUserByStripeCustomerId = async (stripeCustomerId: string) => {
|
||||
const user = db.query.users_temp.findFirst({
|
||||
where: eq(users_temp.stripeCustomerId, stripeCustomerId),
|
||||
const user = db.query.users.findFirst({
|
||||
where: eq(users.stripeCustomerId, stripeCustomerId),
|
||||
});
|
||||
return user;
|
||||
};
|
||||
|
||||
@@ -3,6 +3,7 @@ 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";
|
||||
@@ -53,12 +54,8 @@ export async function getServerSideProps(
|
||||
try {
|
||||
await helpers.project.all.prefetch();
|
||||
|
||||
if (user.role === "member") {
|
||||
const userR = await helpers.user.one.fetch({
|
||||
userId: user.id,
|
||||
});
|
||||
|
||||
if (!userR?.canAccessToDocker) {
|
||||
if (user.role?.name === "member" || !user?.role?.isSystem) {
|
||||
if (!user?.role?.permissions?.includes(PERMISSIONS.DOCKER.VIEW.name)) {
|
||||
return {
|
||||
redirect: {
|
||||
permanent: true,
|
||||
|
||||
@@ -20,7 +20,7 @@ const Dashboard = () => {
|
||||
false,
|
||||
);
|
||||
|
||||
const { data: monitoring, isLoading } = api.user.getMetricsToken.useQuery();
|
||||
const { data: webServer, isLoading } = api.webServer.get.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://${monitoring?.serverIp}:${monitoring?.metricsConfig?.server?.port}/metrics`
|
||||
? `http://${webServer?.serverIp}:${webServer?.metricsConfig?.server?.port}/metrics`
|
||||
: BASE_URL
|
||||
}
|
||||
token={
|
||||
process.env.NODE_ENV === "production"
|
||||
? monitoring?.metricsConfig?.server?.token
|
||||
? webServer?.metricsConfig?.server?.token
|
||||
: DEFAULT_TOKEN
|
||||
}
|
||||
/>
|
||||
|
||||
@@ -94,6 +94,7 @@ 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;
|
||||
@@ -221,7 +222,6 @@ 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,30 +736,27 @@ const Project = (
|
||||
Stop
|
||||
</Button>
|
||||
</DialogAction>
|
||||
{(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}
|
||||
<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"
|
||||
>
|
||||
<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}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<Trash2 className="mr-2 h-4 w-4" />
|
||||
Delete
|
||||
</Button>
|
||||
</DialogAction>
|
||||
<DuplicateProject
|
||||
projectId={projectId}
|
||||
services={applications}
|
||||
selectedServiceIds={selectedServices}
|
||||
/>
|
||||
</Permissions>
|
||||
|
||||
<Dialog
|
||||
open={isMoveDialogOpen}
|
||||
|
||||
+3
-4
@@ -37,7 +37,6 @@ 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";
|
||||
@@ -54,6 +53,7 @@ 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,7 +88,6 @@ const Service = (
|
||||
);
|
||||
|
||||
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||
const { data: auth } = api.user.get.useQuery();
|
||||
|
||||
return (
|
||||
<div className="pb-10">
|
||||
@@ -179,9 +178,9 @@ const Service = (
|
||||
|
||||
<div className="flex flex-row gap-2 justify-end">
|
||||
<UpdateApplication applicationId={applicationId} />
|
||||
{(auth?.role === "owner" || auth?.canDeleteServices) && (
|
||||
<Permissions permissions={["service:delete"]}>
|
||||
<DeleteService id={applicationId} type="application" />
|
||||
)}
|
||||
</Permissions>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
|
||||
@@ -33,7 +33,6 @@ 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";
|
||||
@@ -51,6 +50,7 @@ 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,7 +78,6 @@ const Service = (
|
||||
|
||||
const { data } = api.compose.one.useQuery({ composeId });
|
||||
|
||||
const { data: auth } = api.user.get.useQuery();
|
||||
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||
|
||||
return (
|
||||
@@ -171,9 +170,9 @@ const Service = (
|
||||
<div className="flex flex-row gap-2 justify-end">
|
||||
<UpdateCompose composeId={composeId} />
|
||||
|
||||
{(auth?.role === "owner" || auth?.canDeleteServices) && (
|
||||
<Permissions permissions={["service:delete"]}>
|
||||
<DeleteService id={composeId} type="compose" />
|
||||
)}
|
||||
</Permissions>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
|
||||
@@ -44,6 +44,7 @@ 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";
|
||||
|
||||
@@ -57,7 +58,6 @@ 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} />
|
||||
{(auth?.role === "owner" || auth?.canDeleteServices) && (
|
||||
<Permissions permissions={["service:delete"]}>
|
||||
<DeleteService id={mariadbId} type="mariadb" />
|
||||
)}
|
||||
</Permissions>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
|
||||
@@ -44,6 +44,7 @@ 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";
|
||||
|
||||
@@ -57,8 +58,6 @@ 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 (
|
||||
@@ -143,9 +142,9 @@ const Mongo = (
|
||||
|
||||
<div className="flex flex-row gap-2 justify-end">
|
||||
<UpdateMongo mongoId={mongoId} />
|
||||
{(auth?.role === "owner" || auth?.canDeleteServices) && (
|
||||
<Permissions permissions={["service:delete"]}>
|
||||
<DeleteService id={mongoId} type="mongo" />
|
||||
)}
|
||||
</Permissions>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
|
||||
@@ -29,7 +29,6 @@ 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";
|
||||
@@ -44,6 +43,8 @@ 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";
|
||||
|
||||
@@ -56,7 +57,6 @@ 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} />
|
||||
{(auth?.role === "owner" || auth?.canDeleteServices) && (
|
||||
<Permissions permissions={["service:delete"]}>
|
||||
<DeleteService id={mysqlId} type="mysql" />
|
||||
)}
|
||||
</Permissions>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
|
||||
@@ -29,7 +29,6 @@ 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";
|
||||
@@ -44,6 +43,8 @@ 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";
|
||||
|
||||
@@ -56,7 +57,6 @@ 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} />
|
||||
{(auth?.role === "owner" || auth?.canDeleteServices) && (
|
||||
<Permissions permissions={["service:delete"]}>
|
||||
<DeleteService id={postgresId} type="postgres" />
|
||||
)}
|
||||
</Permissions>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
|
||||
@@ -43,6 +43,7 @@ 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";
|
||||
|
||||
@@ -56,8 +57,6 @@ 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 (
|
||||
@@ -142,9 +141,9 @@ const Redis = (
|
||||
|
||||
<div className="flex flex-row gap-2 justify-end">
|
||||
<UpdateRedis redisId={redisId} />
|
||||
{(auth?.role === "owner" || auth?.canDeleteServices) && (
|
||||
<Permissions permissions={["service:delete"]}>
|
||||
<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 !== "owner") {
|
||||
if (!user || (user.role?.name !== "owner" && user.role?.name !== "admin")) {
|
||||
return {
|
||||
redirect: {
|
||||
permanent: true,
|
||||
|
||||
@@ -44,7 +44,7 @@ export async function getServerSideProps(
|
||||
|
||||
await helpers.user.get.prefetch();
|
||||
|
||||
if (!user || user.role === "member") {
|
||||
if (!user || (user.role?.name !== "owner" && user.role?.name !== "admin")) {
|
||||
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 === "member") {
|
||||
if (!user || (user.role?.name !== "owner" && user.role?.name !== "admin")) {
|
||||
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 === "member") {
|
||||
if (!user || (user.role?.name !== "owner" && user.role?.name !== "admin")) {
|
||||
return {
|
||||
redirect: {
|
||||
permanent: true,
|
||||
|
||||
@@ -34,7 +34,7 @@ export async function getServerSideProps(
|
||||
};
|
||||
}
|
||||
const { user, session } = await validateRequest(ctx.req);
|
||||
if (!user || user.role === "member") {
|
||||
if (!user || (user.role?.name !== "owner" && user.role?.name !== "admin")) {
|
||||
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 === "member") {
|
||||
if (!user || (user.role?.name !== "owner" && user.role?.name !== "admin")) {
|
||||
return {
|
||||
redirect: {
|
||||
permanent: true,
|
||||
|
||||
@@ -3,6 +3,7 @@ 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";
|
||||
@@ -49,12 +50,12 @@ export async function getServerSideProps(
|
||||
try {
|
||||
await helpers.project.all.prefetch();
|
||||
await helpers.settings.isCloud.prefetch();
|
||||
if (user.role === "member") {
|
||||
const userR = await helpers.user.one.fetch({
|
||||
userId: user.id,
|
||||
});
|
||||
|
||||
if (!userR?.canAccessToGitProviders) {
|
||||
if (user.role?.name === "member" || !user?.role?.isSystem) {
|
||||
if (
|
||||
!user?.role?.permissions?.includes(
|
||||
PERMISSIONS.GIT_PROVIDERS.ACCESS.name,
|
||||
)
|
||||
) {
|
||||
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 === "member") {
|
||||
if (!user || (user.role?.name !== "owner" && user.role?.name !== "admin")) {
|
||||
return {
|
||||
redirect: {
|
||||
permanent: true,
|
||||
|
||||
@@ -1,27 +1,23 @@
|
||||
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 />
|
||||
{(data?.canAccessToAPI || data?.role === "owner") && <ShowApiKeys />}
|
||||
|
||||
{/* {isCloud && <RemoveSelfAccount />} */}
|
||||
<Permissions permissions={["api:access"]}>
|
||||
<ShowApiKeys />
|
||||
</Permissions>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -26,7 +26,7 @@ export async function getServerSideProps(
|
||||
) {
|
||||
const { req, res } = ctx;
|
||||
const { user, session } = await validateRequest(req);
|
||||
if (!user || user.role === "member") {
|
||||
if (!user || (user.role?.name !== "owner" && user.role?.name !== "admin")) {
|
||||
return {
|
||||
redirect: {
|
||||
permanent: true,
|
||||
|
||||
@@ -59,7 +59,7 @@ export async function getServerSideProps(
|
||||
},
|
||||
};
|
||||
}
|
||||
if (user.role === "member") {
|
||||
if (user.role?.name === "member" || !user?.role?.isSystem) {
|
||||
return {
|
||||
redirect: {
|
||||
permanent: true,
|
||||
|
||||
@@ -36,7 +36,7 @@ export async function getServerSideProps(
|
||||
},
|
||||
};
|
||||
}
|
||||
if (user.role === "member") {
|
||||
if (user.role?.name === "member" || !user?.role?.isSystem) {
|
||||
return {
|
||||
redirect: {
|
||||
permanent: true,
|
||||
|
||||
@@ -3,6 +3,7 @@ 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 +51,10 @@ export async function getServerSideProps(
|
||||
await helpers.project.all.prefetch();
|
||||
await helpers.settings.isCloud.prefetch();
|
||||
|
||||
if (user.role === "member") {
|
||||
const userR = await helpers.user.one.fetch({
|
||||
userId: user.id,
|
||||
});
|
||||
|
||||
if (!userR?.canAccessToSSHKeys) {
|
||||
if (user.role?.name === "member" || !user?.role?.isSystem) {
|
||||
if (
|
||||
!user?.role?.permissions?.includes(PERMISSIONS.SSH_KEYS.ACCESS.name)
|
||||
) {
|
||||
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 === "member") {
|
||||
if (!user || (user.role?.name !== "owner" && user.role?.name !== "admin")) {
|
||||
return {
|
||||
redirect: {
|
||||
permanent: true,
|
||||
|
||||
@@ -3,6 +3,7 @@ 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";
|
||||
@@ -53,12 +54,8 @@ export async function getServerSideProps(
|
||||
try {
|
||||
await helpers.project.all.prefetch();
|
||||
|
||||
if (user.role === "member") {
|
||||
const userR = await helpers.user.one.fetch({
|
||||
userId: user.id,
|
||||
});
|
||||
|
||||
if (!userR?.canAccessToDocker) {
|
||||
if (user.role?.name === "member" || !user?.role?.isSystem) {
|
||||
if (!user?.role?.permissions?.includes(PERMISSIONS.DOCKER.VIEW.name)) {
|
||||
return {
|
||||
redirect: {
|
||||
permanent: true,
|
||||
|
||||
@@ -3,6 +3,7 @@ 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";
|
||||
@@ -53,12 +54,8 @@ export async function getServerSideProps(
|
||||
try {
|
||||
await helpers.project.all.prefetch();
|
||||
|
||||
if (user.role === "member") {
|
||||
const userR = await helpers.user.one.fetch({
|
||||
userId: user.id,
|
||||
});
|
||||
|
||||
if (!userR?.canAccessToTraefikFiles) {
|
||||
if (user.role?.name === "member" || !user?.role?.isSystem) {
|
||||
if (!user?.role?.permissions?.includes(PERMISSIONS.TRAEFIK.ACCESS.name)) {
|
||||
return {
|
||||
redirect: {
|
||||
permanent: true,
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
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 superjson from "superjson";
|
||||
import { PERMISSIONS } from "@dokploy/server/lib/permissions";
|
||||
|
||||
const SwaggerUI = dynamic(() => import("swagger-ui-react"), { ssr: false });
|
||||
|
||||
@@ -71,8 +69,7 @@ const Home: NextPage = () => {
|
||||
|
||||
export default Home;
|
||||
export async function getServerSideProps(context: GetServerSidePropsContext) {
|
||||
const { req, res } = context;
|
||||
const { user, session } = await validateRequest(context.req);
|
||||
const { user } = await validateRequest(context.req);
|
||||
if (!user) {
|
||||
return {
|
||||
redirect: {
|
||||
@@ -81,23 +78,9 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
||||
},
|
||||
};
|
||||
}
|
||||
const helpers = createServerSideHelpers({
|
||||
router: appRouter,
|
||||
ctx: {
|
||||
req: req as any,
|
||||
res: res as any,
|
||||
db: null as any,
|
||||
session: session as any,
|
||||
user: user as any,
|
||||
},
|
||||
transformer: superjson,
|
||||
});
|
||||
if (user.role === "member") {
|
||||
const userR = await helpers.user.one.fetch({
|
||||
userId: user.id,
|
||||
});
|
||||
|
||||
if (!userR?.canAccessToAPI) {
|
||||
if (user.role?.name === "member" || !user?.role?.isSystem) {
|
||||
if (!user?.role?.permissions?.includes(PERMISSIONS.API.ACCESS.name)) {
|
||||
return {
|
||||
redirect: {
|
||||
permanent: true,
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
import { findAdmin } from "@dokploy/server";
|
||||
import { findOwner } from "@dokploy/server";
|
||||
import { db } from "@dokploy/server/db";
|
||||
import { users_temp } from "@dokploy/server/db/schema";
|
||||
import { users } from "@dokploy/server/db/schema";
|
||||
import { eq } from "drizzle-orm";
|
||||
|
||||
(async () => {
|
||||
try {
|
||||
const result = await findAdmin();
|
||||
const result = await findOwner();
|
||||
|
||||
const update = await db
|
||||
.update(users_temp)
|
||||
.update(users)
|
||||
.set({
|
||||
twoFactorEnabled: false,
|
||||
})
|
||||
.where(eq(users_temp.id, result.userId));
|
||||
.where(eq(users.id, result.userId));
|
||||
|
||||
if (update) {
|
||||
console.log("2FA reset successful");
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { findAdmin } from "@dokploy/server";
|
||||
import { findOwner } from "@dokploy/server";
|
||||
import { generateRandomPassword } from "@dokploy/server";
|
||||
import { 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 findAdmin();
|
||||
const result = await findOwner();
|
||||
|
||||
const update = await db
|
||||
.update(account)
|
||||
|
||||
@@ -38,6 +38,8 @@ 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.
|
||||
*
|
||||
@@ -84,6 +86,8 @@ 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,
|
||||
updateUser,
|
||||
updateWebServer,
|
||||
} from "@dokploy/server";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { adminProcedure, createTRPCRouter } from "../trpc";
|
||||
@@ -27,7 +27,8 @@ export const adminRouter = createTRPCRouter({
|
||||
});
|
||||
}
|
||||
|
||||
await updateUser(user.id, {
|
||||
await updateWebServer({
|
||||
// @ts-expect-error - TODO: fix this
|
||||
metricsConfig: {
|
||||
server: {
|
||||
type: "Dokploy",
|
||||
@@ -52,7 +53,7 @@ export const adminRouter = createTRPCRouter({
|
||||
},
|
||||
});
|
||||
|
||||
const currentServer = await setupWebMonitoring(user.id);
|
||||
const currentServer = await setupWebMonitoring();
|
||||
return currentServer;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
|
||||
@@ -147,11 +147,10 @@ export const aiRouter = createTRPCRouter({
|
||||
serverId: z.string().optional(),
|
||||
}),
|
||||
)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
.mutation(async ({ input }) => {
|
||||
try {
|
||||
return await suggestVariants({
|
||||
...input,
|
||||
organizationId: ctx.session.activeOrganizationId,
|
||||
});
|
||||
} catch (error) {
|
||||
throw new TRPCError({
|
||||
@@ -163,7 +162,7 @@ export const aiRouter = createTRPCRouter({
|
||||
deploy: protectedProcedure
|
||||
.input(deploySuggestionSchema)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
if (ctx.user.role === "member") {
|
||||
if (ctx.user.role.name === "member" || !ctx.user.role.isSystem) {
|
||||
await checkServiceAccess(
|
||||
ctx.session.activeOrganizationId,
|
||||
input.projectId,
|
||||
@@ -216,7 +215,7 @@ export const aiRouter = createTRPCRouter({
|
||||
}
|
||||
}
|
||||
|
||||
if (ctx.user.role === "member") {
|
||||
if (ctx.user.role.name === "member" || !ctx.user.role.isSystem) {
|
||||
await addNewService(
|
||||
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 === "member") {
|
||||
if (ctx.user.role.name === "member" || !ctx.user.role.isSystem) {
|
||||
await checkServiceAccess(
|
||||
ctx.user.id,
|
||||
input.projectId,
|
||||
@@ -88,7 +88,7 @@ export const applicationRouter = createTRPCRouter({
|
||||
}
|
||||
const newApplication = await createApplication(input);
|
||||
|
||||
if (ctx.user.role === "member") {
|
||||
if (ctx.user.role.name === "member" || !ctx.user.role.isSystem) {
|
||||
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 === "member") {
|
||||
if (ctx.user.role.name === "member" || !ctx.user.role.isSystem) {
|
||||
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 === "member") {
|
||||
if (ctx.user.role.name === "member" || !ctx.user.role.isSystem) {
|
||||
await checkServiceAccess(
|
||||
ctx.user.id,
|
||||
input.applicationId,
|
||||
|
||||
@@ -31,7 +31,7 @@ import {
|
||||
findGitProviderById,
|
||||
findProjectById,
|
||||
findServerById,
|
||||
findUserById,
|
||||
findWebServer,
|
||||
getComposeContainer,
|
||||
loadServices,
|
||||
randomizeComposeFile,
|
||||
@@ -64,7 +64,7 @@ export const composeRouter = createTRPCRouter({
|
||||
.input(apiCreateCompose)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
try {
|
||||
if (ctx.user.role === "member") {
|
||||
if (ctx.user.role.name === "member" || !ctx.user.role.isSystem) {
|
||||
await checkServiceAccess(
|
||||
ctx.user.id,
|
||||
input.projectId,
|
||||
@@ -88,7 +88,7 @@ export const composeRouter = createTRPCRouter({
|
||||
}
|
||||
const newService = await createCompose(input);
|
||||
|
||||
if (ctx.user.role === "member") {
|
||||
if (ctx.user.role.name === "member" || !ctx.user.role.isSystem) {
|
||||
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 === "member") {
|
||||
if (ctx.user.role.name === "member" || !ctx.user.role.isSystem) {
|
||||
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 === "member") {
|
||||
if (ctx.user.role.name === "member" || !ctx.user.role.isSystem) {
|
||||
await checkServiceAccess(
|
||||
ctx.user.id,
|
||||
input.composeId,
|
||||
@@ -469,7 +469,7 @@ export const composeRouter = createTRPCRouter({
|
||||
}),
|
||||
)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
if (ctx.user.role === "member") {
|
||||
if (ctx.user.role.name === "member" || !ctx.user.role.isSystem) {
|
||||
await checkServiceAccess(
|
||||
ctx.user.id,
|
||||
input.projectId,
|
||||
@@ -487,8 +487,8 @@ export const composeRouter = createTRPCRouter({
|
||||
|
||||
const template = await fetchTemplateFiles(input.id, input.baseUrl);
|
||||
|
||||
const admin = await findUserById(ctx.user.ownerId);
|
||||
let serverIp = admin.serverIp || "127.0.0.1";
|
||||
const webServer = await findWebServer();
|
||||
let serverIp = webServer.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 === "member") {
|
||||
if (ctx.user.role.name === "member" || !ctx.user.role.isSystem) {
|
||||
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 admin = await findUserById(ctx.user.ownerId);
|
||||
let serverIp = admin.serverIp || "127.0.0.1";
|
||||
const webServer = await findWebServer();
|
||||
let serverIp = webServer.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 admin = await findUserById(ctx.user.ownerId);
|
||||
let serverIp = admin.serverIp || "127.0.0.1";
|
||||
const webServer = await findWebServer();
|
||||
let serverIp = webServer.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,25 +93,19 @@ export const domainRouter = createTRPCRouter({
|
||||
}),
|
||||
generateDomain: protectedProcedure
|
||||
.input(z.object({ appName: z.string(), serverId: z.string().optional() }))
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
return generateTraefikMeDomain(
|
||||
input.appName,
|
||||
ctx.user.ownerId,
|
||||
input.serverId,
|
||||
);
|
||||
.mutation(async ({ input }) => {
|
||||
return generateTraefikMeDomain(input.appName, input.serverId);
|
||||
}),
|
||||
canGenerateTraefikMeDomains: protectedProcedure
|
||||
.input(z.object({ serverId: z.string() }))
|
||||
.query(async ({ input, ctx }) => {
|
||||
const organization = await findOrganizationById(
|
||||
ctx.session.activeOrganizationId,
|
||||
);
|
||||
.query(async ({ input }) => {
|
||||
const webServer = await findWebServer();
|
||||
|
||||
if (input.serverId) {
|
||||
const server = await findServerById(input.serverId);
|
||||
return server.ipAddress;
|
||||
}
|
||||
return organization?.owner.serverIp;
|
||||
return webServer?.serverIp;
|
||||
}),
|
||||
|
||||
update: protectedProcedure
|
||||
|
||||
@@ -41,7 +41,7 @@ export const mariadbRouter = createTRPCRouter({
|
||||
.input(apiCreateMariaDB)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
if (ctx.user.role === "member") {
|
||||
if (ctx.user.role.name === "member" || !ctx.user.role.isSystem) {
|
||||
await checkServiceAccess(
|
||||
ctx.user.id,
|
||||
input.projectId,
|
||||
@@ -65,7 +65,7 @@ export const mariadbRouter = createTRPCRouter({
|
||||
});
|
||||
}
|
||||
const newMariadb = await createMariadb(input);
|
||||
if (ctx.user.role === "member") {
|
||||
if (ctx.user.role.name === "member" || !ctx.user.role.isSystem) {
|
||||
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 === "member") {
|
||||
if (ctx.user.role.name === "member" || !ctx.user.role.isSystem) {
|
||||
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 === "member") {
|
||||
if (ctx.user.role.name === "member" || !ctx.user.role.isSystem) {
|
||||
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 === "member") {
|
||||
if (ctx.user.role.name === "member" || !ctx.user.role.isSystem) {
|
||||
await checkServiceAccess(
|
||||
ctx.user.id,
|
||||
input.projectId,
|
||||
@@ -65,7 +65,7 @@ export const mongoRouter = createTRPCRouter({
|
||||
});
|
||||
}
|
||||
const newMongo = await createMongo(input);
|
||||
if (ctx.user.role === "member") {
|
||||
if (ctx.user.role.name === "member" || !ctx.user.role.isSystem) {
|
||||
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 === "member") {
|
||||
if (ctx.user.role.name === "member" || !ctx.user.role.isSystem) {
|
||||
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 === "member") {
|
||||
if (ctx.user.role.name === "member" || !ctx.user.role.isSystem) {
|
||||
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 === "member") {
|
||||
if (ctx.user.role.name === "member" || !ctx.user.role.isSystem) {
|
||||
await checkServiceAccess(
|
||||
ctx.user.id,
|
||||
input.projectId,
|
||||
@@ -69,7 +69,7 @@ export const mysqlRouter = createTRPCRouter({
|
||||
}
|
||||
|
||||
const newMysql = await createMysql(input);
|
||||
if (ctx.user.role === "member") {
|
||||
if (ctx.user.role.name === "member" || !ctx.user.role.isSystem) {
|
||||
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 === "member") {
|
||||
if (ctx.user.role.name === "member" || !ctx.user.role.isSystem) {
|
||||
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 === "member") {
|
||||
if (ctx.user.role.name === "member" || !ctx.user.role.isSystem) {
|
||||
await checkServiceAccess(
|
||||
ctx.user.id,
|
||||
input.mysqlId,
|
||||
|
||||
@@ -24,7 +24,7 @@ import {
|
||||
apiUpdateTelegram,
|
||||
notifications,
|
||||
server,
|
||||
users_temp,
|
||||
webServer,
|
||||
} 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(users_temp)
|
||||
.from(webServer)
|
||||
.where(
|
||||
sql`${users_temp.metricsConfig}::jsonb -> 'server' ->> 'token' = ${input.Token}`,
|
||||
sql`${webServer.metricsConfig}::jsonb -> 'server' ->> 'token' = ${input.Token}`,
|
||||
);
|
||||
|
||||
if (!result?.[0]?.id) {
|
||||
if (!result?.[0]?.webServerId) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Token not found",
|
||||
});
|
||||
}
|
||||
|
||||
organizationId = result?.[0]?.id;
|
||||
organizationId = result?.[0]?.webServerId;
|
||||
ServerName = "Dokploy";
|
||||
} else {
|
||||
const result = await db
|
||||
|
||||
@@ -32,8 +32,6 @@ 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 === "member") {
|
||||
if (ctx.user.role.name === "member" || !ctx.user.role.isSystem) {
|
||||
await checkServiceAccess(
|
||||
ctx.user.id,
|
||||
input.projectId,
|
||||
@@ -65,7 +65,7 @@ export const postgresRouter = createTRPCRouter({
|
||||
});
|
||||
}
|
||||
const newPostgres = await createPostgres(input);
|
||||
if (ctx.user.role === "member") {
|
||||
if (ctx.user.role.name === "member" || !ctx.user.role.isSystem) {
|
||||
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 === "member") {
|
||||
if (ctx.user.role.name === "member" || !ctx.user.role.isSystem) {
|
||||
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 === "member") {
|
||||
if (ctx.user.role.name === "member" || !ctx.user.role.isSystem) {
|
||||
await checkServiceAccess(
|
||||
ctx.user.id,
|
||||
input.postgresId,
|
||||
|
||||
@@ -57,7 +57,7 @@ export const projectRouter = createTRPCRouter({
|
||||
.input(apiCreateProject)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
try {
|
||||
if (ctx.user.role === "member") {
|
||||
if (ctx.user.role.name === "member" || !ctx.user.role.isSystem) {
|
||||
await checkProjectAccess(
|
||||
ctx.user.id,
|
||||
"create",
|
||||
@@ -78,7 +78,7 @@ export const projectRouter = createTRPCRouter({
|
||||
input,
|
||||
ctx.session.activeOrganizationId,
|
||||
);
|
||||
if (ctx.user.role === "member") {
|
||||
if (ctx.user.role.name === "member" || !ctx.user.role.isSystem) {
|
||||
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 === "member") {
|
||||
if (ctx.user.role.name === "member" || !ctx.user.role.isSystem) {
|
||||
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 === "member") {
|
||||
if (ctx.user.role.name === "member" || !ctx.user.role.isSystem) {
|
||||
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 === "member") {
|
||||
if (ctx.user.role.name === "member" || !ctx.user.role.isSystem) {
|
||||
await checkProjectAccess(
|
||||
ctx.user.id,
|
||||
"delete",
|
||||
@@ -314,7 +314,7 @@ export const projectRouter = createTRPCRouter({
|
||||
)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
try {
|
||||
if (ctx.user.role === "member") {
|
||||
if (ctx.user.role.name === "member" || !ctx.user.role.isSystem) {
|
||||
await checkProjectAccess(
|
||||
ctx.user.id,
|
||||
"create",
|
||||
@@ -649,7 +649,10 @@ export const projectRouter = createTRPCRouter({
|
||||
}
|
||||
}
|
||||
|
||||
if (!input.duplicateInSameProject && ctx.user.role === "member") {
|
||||
if (
|
||||
!input.duplicateInSameProject &&
|
||||
(ctx.user.role.name === "member" || !ctx.user.role.isSystem)
|
||||
) {
|
||||
await addNewProject(
|
||||
ctx.user.id,
|
||||
targetProject.projectId,
|
||||
|
||||
@@ -41,7 +41,7 @@ export const redisRouter = createTRPCRouter({
|
||||
.input(apiCreateRedis)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
if (ctx.user.role === "member") {
|
||||
if (ctx.user.role.name === "member" || !ctx.user.role.isSystem) {
|
||||
await checkServiceAccess(
|
||||
ctx.user.id,
|
||||
input.projectId,
|
||||
@@ -65,7 +65,7 @@ export const redisRouter = createTRPCRouter({
|
||||
});
|
||||
}
|
||||
const newRedis = await createRedis(input);
|
||||
if (ctx.user.role === "member") {
|
||||
if (ctx.user.role.name === "member" || !ctx.user.role.isSystem) {
|
||||
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 === "member") {
|
||||
if (ctx.user.role.name === "member" || !ctx.user.role.isSystem) {
|
||||
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 === "member") {
|
||||
if (ctx.user.role.name === "member" || !ctx.user.role.isSystem) {
|
||||
await checkServiceAccess(
|
||||
ctx.user.id,
|
||||
input.redisId,
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
import { createTRPCRouter } from "@/server/api/trpc";
|
||||
// import { createRole, removeRoleById, updateRoleById } from "@dokploy/server";
|
||||
// import { defaultPermissions } from "@dokploy/server/lib/permissions";
|
||||
|
||||
export const roleRouter = createTRPCRouter({
|
||||
// all: protectedProcedure.query(async ({ ctx }) => {
|
||||
// const roles = await db.query.role.findMany({
|
||||
// where: and(
|
||||
// eq(role.organizationId, ctx.session.activeOrganizationId),
|
||||
// eq(role.isSystem, false),
|
||||
// ),
|
||||
// orderBy: [asc(role.createdAt)],
|
||||
// });
|
||||
// return roles;
|
||||
// }),
|
||||
// delete: protectedProcedure
|
||||
// .input(apiFindOneRole)
|
||||
// .mutation(async ({ input }) => {
|
||||
// try {
|
||||
// return removeRoleById(input.roleId);
|
||||
// } catch (error) {
|
||||
// const message =
|
||||
// error instanceof Error ? error.message : "Error input: Deleting role";
|
||||
// throw new TRPCError({
|
||||
// code: "BAD_REQUEST",
|
||||
// message,
|
||||
// });
|
||||
// }
|
||||
// }),
|
||||
// create: protectedProcedure
|
||||
// .input(createRoleSchema)
|
||||
// .mutation(async ({ input, ctx }) => {
|
||||
// try {
|
||||
// return await createRole(
|
||||
// {
|
||||
// ...input,
|
||||
// },
|
||||
// ctx.session.activeOrganizationId,
|
||||
// );
|
||||
// } catch (error) {
|
||||
// console.error(error);
|
||||
// throw new TRPCError({
|
||||
// code: "BAD_REQUEST",
|
||||
// message: "Error input: Creating role",
|
||||
// cause: error,
|
||||
// });
|
||||
// }
|
||||
// }),
|
||||
// update: protectedProcedure
|
||||
// .input(updateRoleSchema)
|
||||
// .mutation(async ({ input }) => {
|
||||
// return await updateRoleById(input.roleId, input);
|
||||
// }),
|
||||
// getDefaultRoles: protectedProcedure.query(async ({ ctx }) => {
|
||||
// const roles = await db.query.role.findMany({
|
||||
// where: and(
|
||||
// eq(role.organizationId, ctx.session.activeOrganizationId),
|
||||
// eq(role.isSystem, true),
|
||||
// ),
|
||||
// });
|
||||
// // add the description from the constants roles to the roles
|
||||
// const rolesWithDescription = defaultPermissions.map((role) => {
|
||||
// const roleInfo = roles.find((r) => r.name === role.name);
|
||||
// return {
|
||||
// ...roleInfo,
|
||||
// ...role,
|
||||
// };
|
||||
// });
|
||||
// const set = new Set(rolesWithDescription.flatMap((r) => r.permissions));
|
||||
// return {
|
||||
// roles: rolesWithDescription,
|
||||
// permissions: Array.from(set),
|
||||
// };
|
||||
// }),
|
||||
});
|
||||
@@ -1,16 +1,12 @@
|
||||
import { db } from "@/server/db";
|
||||
import {
|
||||
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,
|
||||
@@ -23,7 +19,6 @@ import {
|
||||
execAsync,
|
||||
execAsyncRemote,
|
||||
findServerById,
|
||||
findUserById,
|
||||
getDokployImage,
|
||||
getDokployImageTag,
|
||||
getLogCleanupStatus,
|
||||
@@ -40,14 +35,9 @@ import {
|
||||
readMainConfig,
|
||||
readMonitoringConfig,
|
||||
recreateDirectory,
|
||||
sendDockerCleanupNotifications,
|
||||
spawnAsync,
|
||||
startLogCleanup,
|
||||
stopLogCleanup,
|
||||
updateLetsEncryptEmail,
|
||||
updateServerById,
|
||||
updateServerTraefik,
|
||||
updateUser,
|
||||
writeConfig,
|
||||
writeMainConfig,
|
||||
writeTraefikConfigInPath,
|
||||
@@ -57,7 +47,6 @@ 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";
|
||||
@@ -187,135 +176,6 @@ 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) {
|
||||
@@ -470,13 +330,6 @@ 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> => {
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
import {
|
||||
IS_CLOUD,
|
||||
createApiKey,
|
||||
findAdmin,
|
||||
findNotificationById,
|
||||
findOrganizationById,
|
||||
findUserById,
|
||||
getUserByToken,
|
||||
removeUserById,
|
||||
sendEmailNotification,
|
||||
updateUser,
|
||||
findWebServer,
|
||||
} from "@dokploy/server";
|
||||
import { db } from "@dokploy/server/db";
|
||||
import {
|
||||
@@ -30,6 +29,7 @@ import {
|
||||
protectedProcedure,
|
||||
publicProcedure,
|
||||
} from "../trpc";
|
||||
import { sendEmail } from "@dokploy/server/verification/send-verification-email";
|
||||
|
||||
const apiCreateApiKey = z.object({
|
||||
name: z.string().min(1),
|
||||
@@ -54,6 +54,7 @@ export const userRouter = createTRPCRouter({
|
||||
where: eq(member.organizationId, ctx.session.activeOrganizationId),
|
||||
with: {
|
||||
user: true,
|
||||
role: true,
|
||||
},
|
||||
orderBy: [asc(member.createdAt)],
|
||||
});
|
||||
@@ -86,7 +87,10 @@ 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 !== "owner") {
|
||||
if (
|
||||
memberResult.userId !== ctx.user.id &&
|
||||
ctx.user.role?.name !== "owner"
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to access this user",
|
||||
@@ -102,6 +106,7 @@ export const userRouter = createTRPCRouter({
|
||||
eq(member.organizationId, ctx.session?.activeOrganizationId || ""),
|
||||
),
|
||||
with: {
|
||||
role: true,
|
||||
user: {
|
||||
with: {
|
||||
apiKeys: true,
|
||||
@@ -147,19 +152,6 @@ 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 }) => {
|
||||
@@ -199,14 +191,6 @@ 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({
|
||||
@@ -383,6 +367,83 @@ 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({
|
||||
@@ -410,11 +471,11 @@ export const userRouter = createTRPCRouter({
|
||||
});
|
||||
}
|
||||
|
||||
const admin = await findAdmin();
|
||||
const webServer = await findWebServer();
|
||||
const host =
|
||||
process.env.NODE_ENV === "development"
|
||||
? "http://localhost:3000"
|
||||
: admin.user.host;
|
||||
: webServer.host;
|
||||
const inviteLink = `${host}/invitation?token=${input.invitationId}`;
|
||||
|
||||
const organization = await findOrganizationById(
|
||||
@@ -438,4 +499,52 @@ 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;
|
||||
}
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -0,0 +1,169 @@
|
||||
import {
|
||||
apiAssignDomain,
|
||||
apiSaveSSHKey,
|
||||
apiUpdateDockerCleanup,
|
||||
updateWebServerSchema,
|
||||
} from "@/server/db/schema";
|
||||
import { removeJob, schedule } from "@/server/utils/backup";
|
||||
import {
|
||||
IS_CLOUD,
|
||||
cleanUpDockerBuilder,
|
||||
cleanUpSystemPrune,
|
||||
cleanUpUnusedImages,
|
||||
findServerById,
|
||||
findWebServer,
|
||||
sendDockerCleanupNotifications,
|
||||
updateLetsEncryptEmail,
|
||||
updateServerById,
|
||||
updateServerTraefik,
|
||||
updateWebServer,
|
||||
} from "@dokploy/server";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { scheduleJob, scheduledJobs } from "node-schedule";
|
||||
import { adminProcedure, createTRPCRouter } from "../trpc";
|
||||
|
||||
export const webServerRouter = createTRPCRouter({
|
||||
get: adminProcedure.query(async () => {
|
||||
if (IS_CLOUD) {
|
||||
return null;
|
||||
}
|
||||
return await findWebServer();
|
||||
}),
|
||||
update: adminProcedure
|
||||
.input(updateWebServerSchema)
|
||||
.mutation(async ({ input }) => {
|
||||
if (IS_CLOUD) {
|
||||
return null;
|
||||
}
|
||||
return await updateWebServer(input);
|
||||
}),
|
||||
saveSSHPrivateKey: adminProcedure
|
||||
.input(apiSaveSSHKey)
|
||||
.mutation(async ({ input }) => {
|
||||
if (IS_CLOUD) {
|
||||
return null;
|
||||
}
|
||||
await updateWebServer({
|
||||
sshPrivateKey: input.sshPrivateKey,
|
||||
});
|
||||
|
||||
return true;
|
||||
}),
|
||||
assignDomainServer: adminProcedure
|
||||
.input(apiAssignDomain)
|
||||
.mutation(async ({ input }) => {
|
||||
if (IS_CLOUD) {
|
||||
return true;
|
||||
}
|
||||
const webServer = await updateWebServer({
|
||||
host: input.host,
|
||||
...(input.letsEncryptEmail && {
|
||||
letsEncryptEmail: input.letsEncryptEmail,
|
||||
}),
|
||||
certificateType: input.certificateType,
|
||||
https: input.https,
|
||||
});
|
||||
|
||||
if (!webServer) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "User not found",
|
||||
});
|
||||
}
|
||||
|
||||
updateServerTraefik(webServer, input.host);
|
||||
if (input.letsEncryptEmail) {
|
||||
updateLetsEncryptEmail(input.letsEncryptEmail);
|
||||
}
|
||||
|
||||
return webServer;
|
||||
}),
|
||||
cleanSSHPrivateKey: adminProcedure.mutation(async () => {
|
||||
if (IS_CLOUD) {
|
||||
return true;
|
||||
}
|
||||
await updateWebServer({
|
||||
sshPrivateKey: null,
|
||||
});
|
||||
return true;
|
||||
}),
|
||||
updateDockerCleanup: adminProcedure
|
||||
.input(apiUpdateDockerCleanup)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
if (input.serverId) {
|
||||
await updateServerById(input.serverId, {
|
||||
enableDockerCleanup: input.enableDockerCleanup,
|
||||
});
|
||||
|
||||
const server = await findServerById(input.serverId);
|
||||
|
||||
if (server.organizationId !== ctx.session?.activeOrganizationId) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to access this server",
|
||||
});
|
||||
}
|
||||
|
||||
if (server.enableDockerCleanup) {
|
||||
const server = await findServerById(input.serverId);
|
||||
if (server.serverStatus === "inactive") {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Server is inactive",
|
||||
});
|
||||
}
|
||||
if (IS_CLOUD) {
|
||||
await schedule({
|
||||
cronSchedule: "0 0 * * *",
|
||||
serverId: input.serverId,
|
||||
type: "server",
|
||||
});
|
||||
} else {
|
||||
scheduleJob(server.serverId, "0 0 * * *", async () => {
|
||||
console.log(
|
||||
`Docker Cleanup ${new Date().toLocaleString()}] Running...`,
|
||||
);
|
||||
await cleanUpUnusedImages(server.serverId);
|
||||
await cleanUpDockerBuilder(server.serverId);
|
||||
await cleanUpSystemPrune(server.serverId);
|
||||
await sendDockerCleanupNotifications(server.organizationId);
|
||||
});
|
||||
}
|
||||
} else {
|
||||
if (IS_CLOUD) {
|
||||
await removeJob({
|
||||
cronSchedule: "0 0 * * *",
|
||||
serverId: input.serverId,
|
||||
type: "server",
|
||||
});
|
||||
} else {
|
||||
const currentJob = scheduledJobs[server.serverId];
|
||||
currentJob?.cancel();
|
||||
}
|
||||
}
|
||||
} else if (!IS_CLOUD) {
|
||||
const userUpdated = await updateWebServer({
|
||||
enableDockerCleanup: input.enableDockerCleanup,
|
||||
});
|
||||
|
||||
if (userUpdated?.enableDockerCleanup) {
|
||||
scheduleJob("docker-cleanup", "0 0 * * *", async () => {
|
||||
console.log(
|
||||
`Docker Cleanup ${new Date().toLocaleString()}] Running...`,
|
||||
);
|
||||
await cleanUpUnusedImages();
|
||||
await cleanUpDockerBuilder();
|
||||
await cleanUpSystemPrune();
|
||||
await sendDockerCleanupNotifications(
|
||||
ctx.session.activeOrganizationId,
|
||||
);
|
||||
});
|
||||
} else {
|
||||
const currentJob = scheduledJobs["docker-cleanup"];
|
||||
currentJob?.cancel();
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}),
|
||||
});
|
||||
@@ -6,7 +6,7 @@
|
||||
// boolean,
|
||||
// } from "drizzle-orm/pg-core";
|
||||
|
||||
// export const users_temp = pgTable("users_temp", {
|
||||
// export const users = pgTable("users", {
|
||||
// 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_temp.id, { onDelete: "cascade" }),
|
||||
// .references(() => users.id, { onDelete: "cascade" }),
|
||||
// activeOrganizationId: text("active_organization_id"),
|
||||
// });
|
||||
|
||||
@@ -39,7 +39,7 @@
|
||||
// providerId: text("provider_id").notNull(),
|
||||
// userId: text("user_id")
|
||||
// .notNull()
|
||||
// .references(() => users_temp.id, { onDelete: "cascade" }),
|
||||
// .references(() => users.id, { onDelete: "cascade" }),
|
||||
// accessToken: text("access_token"),
|
||||
// refreshToken: text("refresh_token"),
|
||||
// idToken: text("id_token"),
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import { sql } from "drizzle-orm";
|
||||
import { db } from "..";
|
||||
import { organization } from "../schema/account";
|
||||
import { getDefaultRolesSQL } from "../schema/rbac";
|
||||
|
||||
export async function createDefaultRoles() {
|
||||
try {
|
||||
// Get all organizations
|
||||
const organizations = await db.select().from(organization);
|
||||
|
||||
// Create default roles for each organization
|
||||
for (const org of organizations) {
|
||||
const rolesSQL = getDefaultRolesSQL(org.id);
|
||||
await db.execute(sql.raw(rolesSQL));
|
||||
|
||||
console.log(
|
||||
`Created default roles for organization: ${org.name} (${org.id})`,
|
||||
);
|
||||
}
|
||||
|
||||
console.log("Successfully created default roles for all organizations");
|
||||
} catch (error) {
|
||||
console.error("Error creating default roles:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,8 @@ import {
|
||||
import { nanoid } from "nanoid";
|
||||
import { projects } from "./project";
|
||||
import { server } from "./server";
|
||||
import { users_temp } from "./user";
|
||||
import { users } from "./user";
|
||||
// import { role } from "./rbac";
|
||||
|
||||
export const account = pgTable("account", {
|
||||
id: text("id")
|
||||
@@ -21,7 +22,7 @@ export const account = pgTable("account", {
|
||||
providerId: text("provider_id").notNull(),
|
||||
userId: text("user_id")
|
||||
.notNull()
|
||||
.references(() => users_temp.id, { onDelete: "cascade" }),
|
||||
.references(() => users.id, { onDelete: "cascade" }),
|
||||
accessToken: text("access_token"),
|
||||
refreshToken: text("refresh_token"),
|
||||
idToken: text("id_token"),
|
||||
@@ -39,9 +40,9 @@ export const account = pgTable("account", {
|
||||
});
|
||||
|
||||
export const accountRelations = relations(account, ({ one }) => ({
|
||||
user: one(users_temp, {
|
||||
user: one(users, {
|
||||
fields: [account.userId],
|
||||
references: [users_temp.id],
|
||||
references: [users.id],
|
||||
}),
|
||||
}));
|
||||
|
||||
@@ -65,15 +66,15 @@ export const organization = pgTable("organization", {
|
||||
metadata: text("metadata"),
|
||||
ownerId: text("owner_id")
|
||||
.notNull()
|
||||
.references(() => users_temp.id, { onDelete: "cascade" }),
|
||||
.references(() => users.id, { onDelete: "cascade" }),
|
||||
});
|
||||
|
||||
export const organizationRelations = relations(
|
||||
organization,
|
||||
({ one, many }) => ({
|
||||
owner: one(users_temp, {
|
||||
owner: one(users, {
|
||||
fields: [organization.ownerId],
|
||||
references: [users_temp.id],
|
||||
references: [users.id],
|
||||
}),
|
||||
servers: many(server),
|
||||
projects: many(projects),
|
||||
@@ -90,24 +91,12 @@ export const member = pgTable("member", {
|
||||
.references(() => organization.id, { onDelete: "cascade" }),
|
||||
userId: text("user_id")
|
||||
.notNull()
|
||||
.references(() => users_temp.id, { onDelete: "cascade" }),
|
||||
role: text("role").notNull().$type<"owner" | "member" | "admin">(),
|
||||
.references(() => users.id, { onDelete: "cascade" }),
|
||||
role: text("role").$type<"owner" | "member" | "admin">(),
|
||||
// roleId: text("roleId").references(() => role.roleId, { onDelete: "cascade" }),
|
||||
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()
|
||||
@@ -123,24 +112,30 @@ export const memberRelations = relations(member, ({ one }) => ({
|
||||
fields: [member.organizationId],
|
||||
references: [organization.id],
|
||||
}),
|
||||
user: one(users_temp, {
|
||||
user: one(users, {
|
||||
fields: [member.userId],
|
||||
references: [users_temp.id],
|
||||
references: [users.id],
|
||||
}),
|
||||
// role: one(role, {
|
||||
// fields: [member.roleId],
|
||||
// references: [role.roleId],
|
||||
// }),
|
||||
}));
|
||||
|
||||
export const invitation = pgTable("invitation", {
|
||||
id: text("id").primaryKey(),
|
||||
id: text("id")
|
||||
.primaryKey()
|
||||
.$defaultFn(() => nanoid()),
|
||||
organizationId: text("organization_id")
|
||||
.notNull()
|
||||
.references(() => organization.id, { onDelete: "cascade" }),
|
||||
email: text("email").notNull(),
|
||||
role: text("role").$type<"owner" | "member" | "admin">(),
|
||||
role: text("role"),
|
||||
status: text("status").notNull(),
|
||||
expiresAt: timestamp("expires_at").notNull(),
|
||||
inviterId: text("inviter_id")
|
||||
.notNull()
|
||||
.references(() => users_temp.id, { onDelete: "cascade" }),
|
||||
.references(() => users.id, { onDelete: "cascade" }),
|
||||
teamId: text("team_id"),
|
||||
});
|
||||
|
||||
@@ -157,7 +152,7 @@ export const twoFactor = pgTable("two_factor", {
|
||||
backupCodes: text("backup_codes").notNull(),
|
||||
userId: text("user_id")
|
||||
.notNull()
|
||||
.references(() => users_temp.id, { onDelete: "cascade" }),
|
||||
.references(() => users.id, { onDelete: "cascade" }),
|
||||
});
|
||||
|
||||
export const apikey = pgTable("apikey", {
|
||||
@@ -168,7 +163,7 @@ export const apikey = pgTable("apikey", {
|
||||
key: text("key").notNull(),
|
||||
userId: text("user_id")
|
||||
.notNull()
|
||||
.references(() => users_temp.id, { onDelete: "cascade" }),
|
||||
.references(() => users.id, { onDelete: "cascade" }),
|
||||
refillInterval: integer("refill_interval"),
|
||||
refillAmount: integer("refill_amount"),
|
||||
lastRefillAt: timestamp("last_refill_at"),
|
||||
@@ -187,8 +182,8 @@ export const apikey = pgTable("apikey", {
|
||||
});
|
||||
|
||||
export const apikeyRelations = relations(apikey, ({ one }) => ({
|
||||
user: one(users_temp, {
|
||||
user: one(users, {
|
||||
fields: [apikey.userId],
|
||||
references: [users_temp.id],
|
||||
references: [users.id],
|
||||
}),
|
||||
}));
|
||||
|
||||
@@ -19,7 +19,7 @@ import { mariadb } from "./mariadb";
|
||||
import { mongo } from "./mongo";
|
||||
import { mysql } from "./mysql";
|
||||
import { postgres } from "./postgres";
|
||||
import { users_temp } from "./user";
|
||||
import { users } 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_temp.id),
|
||||
userId: text("userId").references(() => users.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_temp, {
|
||||
user: one(users, {
|
||||
fields: [backups.userId],
|
||||
references: [users_temp.id],
|
||||
references: [users.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_temp } from "./user";
|
||||
import { users } 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_temp.id, { onDelete: "cascade" }),
|
||||
.references(() => users.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_temp, {
|
||||
user: one(users, {
|
||||
fields: [gitProvider.userId],
|
||||
references: [users_temp.id],
|
||||
references: [users.id],
|
||||
}),
|
||||
}));
|
||||
|
||||
|
||||
@@ -30,7 +30,9 @@ 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";
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
// import { relations } from "drizzle-orm";
|
||||
// import { pgTable, text, timestamp, boolean, unique } from "drizzle-orm/pg-core";
|
||||
// import { nanoid } from "nanoid";
|
||||
// import { organization, member } from "./account";
|
||||
// import { createInsertSchema } from "drizzle-zod";
|
||||
// import { z } from "zod";
|
||||
|
||||
// export const role = pgTable(
|
||||
// "member_role",
|
||||
// {
|
||||
// roleId: text("roleId")
|
||||
// .primaryKey()
|
||||
// .$defaultFn(() => nanoid()),
|
||||
// name: text("name").notNull().unique(),
|
||||
// description: text("description"),
|
||||
// canDelete: boolean("canDelete").notNull().default(true),
|
||||
// isSystem: boolean("is_system").default(false),
|
||||
// permissions: text("permissions").array(),
|
||||
// createdAt: timestamp("created_at").notNull().defaultNow(),
|
||||
// updatedAt: timestamp("updated_at").notNull().defaultNow(),
|
||||
// organizationId: text("organizationId")
|
||||
// .notNull()
|
||||
// .references(() => organization.id, { onDelete: "cascade" }),
|
||||
// },
|
||||
// (table) => ({
|
||||
// roleName: unique("role_name_unique").on(table.name, table.organizationId),
|
||||
// }),
|
||||
// );
|
||||
|
||||
// export const roleRelations = relations(role, ({ one, many }) => ({
|
||||
// organization: one(organization, {
|
||||
// fields: [role.organizationId],
|
||||
// references: [organization.id],
|
||||
// }),
|
||||
// members: many(member),
|
||||
// }));
|
||||
|
||||
// export type Role = typeof role.$inferSelect;
|
||||
|
||||
// export const createRoleSchema = createInsertSchema(role)
|
||||
// .omit({
|
||||
// roleId: true,
|
||||
// createdAt: true,
|
||||
// updatedAt: true,
|
||||
// isSystem: true,
|
||||
// organizationId: true,
|
||||
// })
|
||||
// .extend({
|
||||
// permissions: z.array(z.string()),
|
||||
// });
|
||||
|
||||
// export const updateRoleSchema = createRoleSchema.extend({
|
||||
// roleId: z.string().min(1),
|
||||
// });
|
||||
|
||||
// export const apiFindOneRole = z.object({
|
||||
// roleId: z.string().min(1),
|
||||
// });
|
||||
@@ -7,7 +7,7 @@ import { applications } from "./application";
|
||||
import { compose } from "./compose";
|
||||
import { deployments } from "./deployment";
|
||||
import { server } from "./server";
|
||||
import { users_temp } from "./user";
|
||||
import { users } 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_temp.id, {
|
||||
userId: text("userId").references(() => users.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_temp, {
|
||||
user: one(users, {
|
||||
fields: [schedules.userId],
|
||||
references: [users_temp.id],
|
||||
references: [users.id],
|
||||
}),
|
||||
deployments: many(deployments),
|
||||
}));
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { pgTable, text, timestamp } from "drizzle-orm/pg-core";
|
||||
import { users_temp } from "./user";
|
||||
import { users } 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_temp.id, { onDelete: "cascade" }),
|
||||
.references(() => users.id, { onDelete: "cascade" }),
|
||||
impersonatedBy: text("impersonated_by"),
|
||||
activeOrganizationId: text("active_organization_id"),
|
||||
});
|
||||
|
||||
@@ -2,7 +2,6 @@ import { relations } from "drizzle-orm";
|
||||
import {
|
||||
boolean,
|
||||
integer,
|
||||
jsonb,
|
||||
pgTable,
|
||||
text,
|
||||
timestamp,
|
||||
@@ -14,7 +13,6 @@ 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
|
||||
@@ -23,10 +21,8 @@ import { paths } from "@dokploy/server/constants";
|
||||
* @see https://orm.drizzle.team/docs/goodies#multi-project-schema
|
||||
*/
|
||||
|
||||
// OLD TABLE
|
||||
|
||||
// TEMP
|
||||
export const users_temp = pgTable("user_temp", {
|
||||
export const users = pgTable("users", {
|
||||
id: text("id")
|
||||
.notNull()
|
||||
.primaryKey()
|
||||
@@ -36,10 +32,7 @@ export const users_temp = pgTable("user_temp", {
|
||||
expirationDate: text("expirationDate")
|
||||
.notNull()
|
||||
.$defaultFn(() => new Date().toISOString()),
|
||||
createdAt2: text("createdAt")
|
||||
.notNull()
|
||||
.$defaultFn(() => new Date().toISOString()),
|
||||
createdAt: timestamp("created_at").defaultNow(),
|
||||
createdAt: timestamp("created_at").notNull().defaultNow(),
|
||||
// Auth
|
||||
twoFactorEnabled: boolean("two_factor_enabled"),
|
||||
email: text("email").notNull().unique(),
|
||||
@@ -48,83 +41,19 @@ export const users_temp = pgTable("user_temp", {
|
||||
banned: boolean("banned"),
|
||||
banReason: text("ban_reason"),
|
||||
banExpires: timestamp("ban_expires"),
|
||||
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 * * *"),
|
||||
updatedAt: timestamp("updated_at").notNull().defaultNow(),
|
||||
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_temp, ({ one, many }) => ({
|
||||
export const usersRelations = relations(users, ({ one, many }) => ({
|
||||
account: one(account, {
|
||||
fields: [users_temp.id],
|
||||
fields: [users.id],
|
||||
references: [account.userId],
|
||||
}),
|
||||
organizations: many(organization),
|
||||
@@ -134,7 +63,7 @@ export const usersRelations = relations(users_temp, ({ one, many }) => ({
|
||||
schedules: many(schedules),
|
||||
}));
|
||||
|
||||
const createSchema = createInsertSchema(users_temp, {
|
||||
const createSchema = createInsertSchema(users, {
|
||||
id: z.string().min(1),
|
||||
isRegistered: z.boolean().optional(),
|
||||
}).omit({
|
||||
@@ -199,33 +128,6 @@ 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),
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
import { boolean, jsonb, pgTable, text } from "drizzle-orm/pg-core";
|
||||
import { nanoid } from "nanoid";
|
||||
import { certificateType } from "./shared";
|
||||
import { z } from "zod";
|
||||
import { createInsertSchema } from "drizzle-zod";
|
||||
|
||||
export const webServer = pgTable("web_server", {
|
||||
webServerId: text("webServerId")
|
||||
.notNull()
|
||||
.primaryKey()
|
||||
.$defaultFn(() => nanoid()),
|
||||
// Admin
|
||||
serverIp: text("serverIp"),
|
||||
certificateType: certificateType("certificateType").notNull().default("none"),
|
||||
https: boolean("https").notNull().default(false),
|
||||
host: text("host"),
|
||||
letsEncryptEmail: text("letsEncryptEmail"),
|
||||
sshPrivateKey: text("sshPrivateKey"),
|
||||
enableDockerCleanup: boolean("enableDockerCleanup").notNull().default(false),
|
||||
logCleanupCron: text("logCleanupCron").default("0 0 * * *"),
|
||||
metricsConfig: jsonb("metricsConfig")
|
||||
.$type<{
|
||||
server: {
|
||||
type: "Dokploy" | "Remote";
|
||||
refreshRate: number;
|
||||
port: number;
|
||||
token: string;
|
||||
urlCallback: string;
|
||||
retentionDays: number;
|
||||
cronJob: string;
|
||||
thresholds: {
|
||||
cpu: number;
|
||||
memory: number;
|
||||
};
|
||||
};
|
||||
containers: {
|
||||
refreshRate: number;
|
||||
services: {
|
||||
include: string[];
|
||||
exclude: string[];
|
||||
};
|
||||
};
|
||||
}>()
|
||||
.notNull()
|
||||
.default({
|
||||
server: {
|
||||
type: "Dokploy",
|
||||
refreshRate: 60,
|
||||
port: 4500,
|
||||
token: "",
|
||||
retentionDays: 2,
|
||||
cronJob: "",
|
||||
urlCallback: "",
|
||||
thresholds: {
|
||||
cpu: 0,
|
||||
memory: 0,
|
||||
},
|
||||
},
|
||||
containers: {
|
||||
refreshRate: 60,
|
||||
services: {
|
||||
include: [],
|
||||
exclude: [],
|
||||
},
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
export type WebServer = typeof webServer.$inferSelect;
|
||||
|
||||
const createSchema = createInsertSchema(webServer);
|
||||
|
||||
export const updateWebServerSchema = createSchema.omit({
|
||||
webServerId: true,
|
||||
metricsConfig: true,
|
||||
});
|
||||
|
||||
export const apiSaveSSHKey = createSchema
|
||||
.pick({
|
||||
sshPrivateKey: true,
|
||||
})
|
||||
.required();
|
||||
|
||||
export const apiAssignDomain = createSchema
|
||||
.pick({
|
||||
host: true,
|
||||
certificateType: true,
|
||||
letsEncryptEmail: true,
|
||||
https: true,
|
||||
})
|
||||
.required()
|
||||
.partial({
|
||||
letsEncryptEmail: true,
|
||||
https: true,
|
||||
});
|
||||
|
||||
export const apiUpdateDockerCleanup = createSchema
|
||||
.pick({
|
||||
enableDockerCleanup: true,
|
||||
})
|
||||
.required()
|
||||
.extend({
|
||||
serverId: z.string().optional(),
|
||||
});
|
||||
@@ -0,0 +1,13 @@
|
||||
import { createDefaultRoles } from "../migrations/create-default-roles";
|
||||
|
||||
async function main() {
|
||||
try {
|
||||
await createDefaultRoles();
|
||||
process.exit(0);
|
||||
} catch (error) {
|
||||
console.error("Failed to create default roles:", error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
@@ -13,6 +13,7 @@ export * from "./services/settings";
|
||||
export * from "./services/volume-backups";
|
||||
export * from "./services/docker";
|
||||
export * from "./services/destination";
|
||||
export * from "./services/role";
|
||||
export * from "./services/deployment";
|
||||
export * from "./services/mount";
|
||||
export * from "./services/certificate";
|
||||
@@ -34,6 +35,7 @@ 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";
|
||||
|
||||
@@ -9,9 +9,9 @@ import { IS_CLOUD } from "../constants";
|
||||
import { db } from "../db";
|
||||
import * as schema from "../db/schema";
|
||||
import { getUserByToken } from "../services/admin";
|
||||
import { updateUser } from "../services/user";
|
||||
import { sendEmail } from "../verification/send-verification-email";
|
||||
import { getPublicIpWithFallback } from "../wss/utils";
|
||||
import { findWebServer, updateWebServer } from "../services/web-server";
|
||||
|
||||
const { handler, api } = betterAuth({
|
||||
database: drizzleAdapter(db, {
|
||||
@@ -31,19 +31,12 @@ const { handler, api } = betterAuth({
|
||||
},
|
||||
...(!IS_CLOUD && {
|
||||
async trustedOrigins() {
|
||||
const admin = await db.query.member.findFirst({
|
||||
where: eq(schema.member.role, "owner"),
|
||||
with: {
|
||||
user: true,
|
||||
},
|
||||
});
|
||||
const admin = await findWebServer();
|
||||
|
||||
if (admin) {
|
||||
return [
|
||||
...(admin.user.serverIp
|
||||
? [`http://${admin.user.serverIp}:3000`]
|
||||
: []),
|
||||
...(admin.user.host ? [`https://${admin.user.host}`] : []),
|
||||
...(admin.serverIp ? [`http://${admin.serverIp}:3000`] : []),
|
||||
...(admin.host ? [`https://${admin.host}`] : []),
|
||||
];
|
||||
}
|
||||
return [];
|
||||
@@ -118,7 +111,7 @@ const { handler, api } = betterAuth({
|
||||
});
|
||||
|
||||
if (!IS_CLOUD) {
|
||||
await updateUser(user.id, {
|
||||
await updateWebServer({
|
||||
serverIp: await getPublicIpWithFallback(),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,187 @@
|
||||
import {
|
||||
defaultStatements,
|
||||
memberAc,
|
||||
ownerAc,
|
||||
adminAc,
|
||||
} from "better-auth/plugins/organization/access";
|
||||
import { createAccessControl } from "better-auth/plugins/access";
|
||||
|
||||
/**
|
||||
* make sure to use `as const` so typescript can infer the type correctly
|
||||
*/
|
||||
const statement = {
|
||||
...defaultStatements,
|
||||
project: ["view", "create", "delete"],
|
||||
service: ["view", "create", "delete"],
|
||||
traefik_files: ["access"],
|
||||
docker: ["access"],
|
||||
api: ["access"],
|
||||
schedules: ["access"],
|
||||
git_providers: ["access"],
|
||||
ssh_keys: ["access"],
|
||||
} as const;
|
||||
|
||||
export const ac = createAccessControl(statement);
|
||||
|
||||
export const owner = ac.newRole({
|
||||
...ownerAc.statements,
|
||||
// inherit all the statements from the statements object
|
||||
project: ["create", "view", "delete"],
|
||||
service: ["create", "view", "delete"],
|
||||
traefik_files: ["access"],
|
||||
docker: ["access"],
|
||||
api: ["access"],
|
||||
schedules: ["access"],
|
||||
git_providers: ["access"],
|
||||
ssh_keys: ["access"],
|
||||
});
|
||||
|
||||
export const admin = ac.newRole({
|
||||
...adminAc.statements,
|
||||
project: ["create", "view", "delete"],
|
||||
service: ["create", "view", "delete"],
|
||||
traefik_files: ["access"],
|
||||
docker: ["access"],
|
||||
api: ["access"],
|
||||
schedules: ["access"],
|
||||
git_providers: ["access"],
|
||||
ssh_keys: ["access"],
|
||||
});
|
||||
|
||||
export const member = ac.newRole({
|
||||
...memberAc.statements,
|
||||
project: ["create", "view", "delete"],
|
||||
service: ["create", "view", "delete"],
|
||||
});
|
||||
|
||||
export const PERMISSIONS = {
|
||||
PROJECT: {
|
||||
VIEW: {
|
||||
name: "project:view",
|
||||
description: "View projects",
|
||||
},
|
||||
CREATE: {
|
||||
name: "project:create",
|
||||
description: "Create projects",
|
||||
},
|
||||
DELETE: {
|
||||
name: "project:delete",
|
||||
description: "Delete projects",
|
||||
},
|
||||
},
|
||||
SERVICE: {
|
||||
VIEW: {
|
||||
name: "service:view",
|
||||
description: "View services",
|
||||
},
|
||||
CREATE: {
|
||||
name: "service:create",
|
||||
description: "Create services",
|
||||
},
|
||||
DELETE: {
|
||||
name: "service:delete",
|
||||
description: "Delete services",
|
||||
},
|
||||
},
|
||||
TRAEFIK: {
|
||||
ACCESS: {
|
||||
name: "traefik_files:access",
|
||||
description: "Access traefik files",
|
||||
},
|
||||
},
|
||||
DOCKER: {
|
||||
VIEW: {
|
||||
name: "docker:view",
|
||||
description: "View docker",
|
||||
},
|
||||
},
|
||||
API: {
|
||||
ACCESS: {
|
||||
name: "api:access",
|
||||
description: "Access API",
|
||||
},
|
||||
},
|
||||
SCHEDULES: {
|
||||
ACCESS: {
|
||||
name: "schedules:access",
|
||||
description: "Access schedules",
|
||||
},
|
||||
},
|
||||
GIT_PROVIDERS: {
|
||||
ACCESS: {
|
||||
name: "git_providers:access",
|
||||
description: "Access git providers",
|
||||
},
|
||||
},
|
||||
SSH_KEYS: {
|
||||
ACCESS: {
|
||||
name: "ssh_keys:access",
|
||||
description: "Access ssh keys",
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const ownerPermissions = [
|
||||
PERMISSIONS.PROJECT.VIEW,
|
||||
PERMISSIONS.PROJECT.CREATE,
|
||||
PERMISSIONS.PROJECT.DELETE,
|
||||
PERMISSIONS.SERVICE.VIEW,
|
||||
PERMISSIONS.SERVICE.CREATE,
|
||||
PERMISSIONS.SERVICE.DELETE,
|
||||
PERMISSIONS.TRAEFIK.ACCESS,
|
||||
PERMISSIONS.SCHEDULES.ACCESS,
|
||||
PERMISSIONS.GIT_PROVIDERS.ACCESS,
|
||||
PERMISSIONS.SSH_KEYS.ACCESS,
|
||||
] as const;
|
||||
|
||||
export const adminPermissions = [
|
||||
PERMISSIONS.PROJECT.VIEW,
|
||||
PERMISSIONS.PROJECT.CREATE,
|
||||
PERMISSIONS.PROJECT.DELETE,
|
||||
PERMISSIONS.SERVICE.VIEW,
|
||||
PERMISSIONS.SERVICE.CREATE,
|
||||
PERMISSIONS.SERVICE.DELETE,
|
||||
PERMISSIONS.TRAEFIK.ACCESS,
|
||||
PERMISSIONS.DOCKER.VIEW,
|
||||
PERMISSIONS.API.ACCESS,
|
||||
PERMISSIONS.SCHEDULES.ACCESS,
|
||||
PERMISSIONS.GIT_PROVIDERS.ACCESS,
|
||||
PERMISSIONS.SSH_KEYS.ACCESS,
|
||||
] as const;
|
||||
|
||||
export const memberPermissions = [
|
||||
PERMISSIONS.PROJECT.CREATE,
|
||||
PERMISSIONS.SERVICE.CREATE,
|
||||
PERMISSIONS.TRAEFIK.ACCESS,
|
||||
] as const;
|
||||
|
||||
export const defaultPermissions = [
|
||||
{
|
||||
name: "owner",
|
||||
description: "Owner of the organization with full access to all features",
|
||||
permissions: ownerPermissions,
|
||||
},
|
||||
{
|
||||
name: "admin",
|
||||
description:
|
||||
"Administrator with access to manage projects, services and configurations",
|
||||
permissions: adminPermissions,
|
||||
},
|
||||
{
|
||||
name: "member",
|
||||
description:
|
||||
"Regular member with access to create projects and manage services",
|
||||
permissions: memberPermissions,
|
||||
},
|
||||
] as const;
|
||||
|
||||
// Utility type to extract all permission names
|
||||
type ExtractPermissionNames<T> = T extends { name: infer U }
|
||||
? U
|
||||
: T extends object
|
||||
? {
|
||||
[K in keyof T]: ExtractPermissionNames<T[K]>;
|
||||
}[keyof T]
|
||||
: never;
|
||||
|
||||
export type PermissionName = ExtractPermissionNames<typeof PERMISSIONS>;
|
||||
@@ -3,18 +3,16 @@ import {
|
||||
invitation,
|
||||
member,
|
||||
organization,
|
||||
users_temp,
|
||||
users,
|
||||
} from "@dokploy/server/db/schema";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { IS_CLOUD } from "../constants";
|
||||
import { findWebServer } from "./web-server";
|
||||
|
||||
export const findUserById = async (userId: string) => {
|
||||
const user = await db.query.users_temp.findFirst({
|
||||
where: eq(users_temp.id, userId),
|
||||
// with: {
|
||||
// account: true,
|
||||
// },
|
||||
const user = await db.query.users.findFirst({
|
||||
where: eq(users.id, userId),
|
||||
});
|
||||
if (!user) {
|
||||
throw new TRPCError({
|
||||
@@ -46,21 +44,21 @@ export const isAdminPresent = async () => {
|
||||
return true;
|
||||
};
|
||||
|
||||
export const findAdmin = async () => {
|
||||
const admin = await db.query.member.findFirst({
|
||||
export const findOwner = async () => {
|
||||
const owner = await db.query.member.findFirst({
|
||||
where: eq(member.role, "owner"),
|
||||
with: {
|
||||
user: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!admin) {
|
||||
if (!owner) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Admin not found",
|
||||
message: "Owner not found",
|
||||
});
|
||||
}
|
||||
return admin;
|
||||
return owner;
|
||||
};
|
||||
|
||||
export const getUserByToken = async (token: string) => {
|
||||
@@ -73,6 +71,7 @@ export const getUserByToken = async (token: string) => {
|
||||
expiresAt: true,
|
||||
role: true,
|
||||
inviterId: true,
|
||||
organizationId: true,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -83,8 +82,8 @@ export const getUserByToken = async (token: string) => {
|
||||
});
|
||||
}
|
||||
|
||||
const userAlreadyExists = await db.query.users_temp.findFirst({
|
||||
where: eq(users_temp.email, user?.email || ""),
|
||||
const userAlreadyExists = await db.query.users.findFirst({
|
||||
where: eq(users.email, user?.email || ""),
|
||||
});
|
||||
|
||||
const { expiresAt, ...rest } = user;
|
||||
@@ -97,8 +96,8 @@ export const getUserByToken = async (token: string) => {
|
||||
|
||||
export const removeUserById = async (userId: string) => {
|
||||
await db
|
||||
.delete(users_temp)
|
||||
.where(eq(users_temp.id, userId))
|
||||
.delete(users)
|
||||
.where(eq(users.id, userId))
|
||||
.returning()
|
||||
.then((res) => res[0]);
|
||||
};
|
||||
@@ -107,10 +106,10 @@ export const getDokployUrl = async () => {
|
||||
if (IS_CLOUD) {
|
||||
return "https://app.dokploy.com";
|
||||
}
|
||||
const admin = await findAdmin();
|
||||
const webServer = await findWebServer();
|
||||
|
||||
if (admin.user.host) {
|
||||
return `https://${admin.user.host}`;
|
||||
if (webServer.host) {
|
||||
return `https://${webServer.host}`;
|
||||
}
|
||||
return `http://${admin.user.serverIp}:${process.env.PORT}`;
|
||||
return `http://${webServer.serverIp}:${process.env.PORT}`;
|
||||
};
|
||||
|
||||
@@ -6,8 +6,8 @@ import { generateObject } from "ai";
|
||||
import { desc, eq } from "drizzle-orm";
|
||||
import { z } from "zod";
|
||||
import { IS_CLOUD } from "../constants";
|
||||
import { findOrganizationById } from "./admin";
|
||||
import { findServerById } from "./server";
|
||||
import { findWebServer } from "./web-server";
|
||||
|
||||
export const getAiSettingsByOrganizationId = async (organizationId: string) => {
|
||||
const aiSettings = await db.query.ai.findMany({
|
||||
@@ -53,18 +53,12 @@ export const deleteAiSettings = async (aiId: string) => {
|
||||
};
|
||||
|
||||
interface Props {
|
||||
organizationId: string;
|
||||
aiId: string;
|
||||
input: string;
|
||||
serverId?: string | undefined;
|
||||
}
|
||||
|
||||
export const suggestVariants = async ({
|
||||
organizationId,
|
||||
aiId,
|
||||
input,
|
||||
serverId,
|
||||
}: Props) => {
|
||||
export const suggestVariants = async ({ aiId, input, serverId }: Props) => {
|
||||
try {
|
||||
const aiSettings = await getAiSettingById(aiId);
|
||||
if (!aiSettings || !aiSettings.isEnabled) {
|
||||
@@ -79,8 +73,8 @@ export const suggestVariants = async ({
|
||||
|
||||
let ip = "";
|
||||
if (!IS_CLOUD) {
|
||||
const organization = await findOrganizationById(organizationId);
|
||||
ip = organization?.owner.serverIp || "";
|
||||
const webServer = await findWebServer();
|
||||
ip = webServer?.serverIp || "";
|
||||
}
|
||||
|
||||
if (serverId) {
|
||||
|
||||
@@ -6,10 +6,10 @@ import { manageDomain } from "@dokploy/server/utils/traefik/domain";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { type apiCreateDomain, domains } from "../db/schema";
|
||||
import { findUserById } from "./admin";
|
||||
import { findApplicationById } from "./application";
|
||||
import { detectCDNProvider } from "./cdn";
|
||||
import { findServerById } from "./server";
|
||||
import { findWebServer } from "./web-server";
|
||||
|
||||
export type Domain = typeof domains.$inferSelect;
|
||||
|
||||
@@ -43,7 +43,6 @@ export const createDomain = async (input: typeof apiCreateDomain._type) => {
|
||||
|
||||
export const generateTraefikMeDomain = async (
|
||||
appName: string,
|
||||
userId: string,
|
||||
serverId?: string,
|
||||
) => {
|
||||
if (serverId) {
|
||||
@@ -60,9 +59,9 @@ export const generateTraefikMeDomain = async (
|
||||
projectName: appName,
|
||||
});
|
||||
}
|
||||
const admin = await findUserById(userId);
|
||||
const webServer = await findWebServer();
|
||||
return generateRandomDomain({
|
||||
serverIp: admin?.serverIp || "",
|
||||
serverIp: webServer?.serverIp || "",
|
||||
projectName: appName,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -2,7 +2,6 @@ import { db } from "@dokploy/server/db";
|
||||
import {
|
||||
type apiCreatePreviewDeployment,
|
||||
deployments,
|
||||
organization,
|
||||
previewDeployments,
|
||||
} from "@dokploy/server/db/schema";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
@@ -13,11 +12,11 @@ import { removeDirectoryCode } from "../utils/filesystem/directory";
|
||||
import { authGithub } from "../utils/providers/github";
|
||||
import { removeTraefikConfig } from "../utils/traefik/application";
|
||||
import { manageDomain } from "../utils/traefik/domain";
|
||||
import { findUserById } from "./admin";
|
||||
import { findApplicationById } from "./application";
|
||||
import { removeDeploymentsByPreviewDeploymentId } from "./deployment";
|
||||
import { createDomain } from "./domain";
|
||||
import { type Github, getIssueComment } from "./github";
|
||||
import { findWebServer } from "./web-server";
|
||||
|
||||
export type PreviewDeployment = typeof previewDeployments.$inferSelect;
|
||||
|
||||
@@ -156,14 +155,10 @@ export const createPreviewDeployment = async (
|
||||
const application = await findApplicationById(schema.applicationId);
|
||||
const appName = `preview-${application.appName}-${generatePassword(6)}`;
|
||||
|
||||
const org = await db.query.organization.findFirst({
|
||||
where: eq(organization.id, application.project.organizationId),
|
||||
});
|
||||
const generateDomain = await generateWildcardDomain(
|
||||
application.previewWildcard || "*.traefik.me",
|
||||
appName,
|
||||
application.server?.ipAddress || "",
|
||||
org?.ownerId || "",
|
||||
);
|
||||
|
||||
const octokit = authGithub(application?.github as Github);
|
||||
@@ -256,7 +251,6 @@ const generateWildcardDomain = async (
|
||||
baseDomain: string,
|
||||
appName: string,
|
||||
serverIp: string,
|
||||
userId: string,
|
||||
): Promise<string> => {
|
||||
if (!baseDomain.startsWith("*.")) {
|
||||
throw new Error('The base domain must start with "*."');
|
||||
@@ -274,8 +268,8 @@ const generateWildcardDomain = async (
|
||||
}
|
||||
|
||||
if (!ip) {
|
||||
const admin = await findUserById(userId);
|
||||
ip = admin?.serverIp || "";
|
||||
const webServer = await findWebServer();
|
||||
ip = webServer?.serverIp || "";
|
||||
}
|
||||
|
||||
const slugIp = ip.replaceAll(".", "-");
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
// import { eq } from "drizzle-orm";
|
||||
// import { db } from "../db";
|
||||
// import {
|
||||
// type createRoleSchema,
|
||||
// member,
|
||||
// role,
|
||||
// type updateRoleSchema,
|
||||
// } from "../db/schema";
|
||||
// import type { z } from "zod";
|
||||
// import {
|
||||
// adminPermissions,
|
||||
// memberPermissions,
|
||||
// ownerPermissions,
|
||||
// } from "../lib/permissions";
|
||||
|
||||
// export const createRole = async (
|
||||
// input: z.infer<typeof createRoleSchema>,
|
||||
// organizationId: string,
|
||||
// ) => {
|
||||
// await db.transaction(async (tx) => {
|
||||
// const { ...other } = input;
|
||||
// const newRole = await tx
|
||||
// .insert(role)
|
||||
// .values({ ...other, organizationId })
|
||||
// .returning()
|
||||
// .then((res) => res[0]);
|
||||
|
||||
// if (!newRole) {
|
||||
// throw new Error("Failed to create role");
|
||||
// }
|
||||
|
||||
// return role;
|
||||
// });
|
||||
// };
|
||||
|
||||
// const findRoleById = async (roleId: string) => {
|
||||
// const result = await db.query.role.findFirst({
|
||||
// where: eq(role.roleId, roleId),
|
||||
// });
|
||||
|
||||
// if (!result) {
|
||||
// throw new Error("Role not found");
|
||||
// }
|
||||
|
||||
// return result;
|
||||
// };
|
||||
|
||||
// export const removeRoleById = async (roleId: string) => {
|
||||
// const currentRole = await findRoleById(roleId);
|
||||
|
||||
// if (!currentRole) {
|
||||
// throw new Error("Role not found");
|
||||
// }
|
||||
|
||||
// if (currentRole.isSystem) {
|
||||
// throw new Error("Cannot delete system role");
|
||||
// }
|
||||
|
||||
// const members = await db.query.member.findMany({
|
||||
// where: eq(member.roleId, roleId),
|
||||
// });
|
||||
|
||||
// if (members.length > 0) {
|
||||
// throw new Error("Cannot delete role with assigned members");
|
||||
// }
|
||||
|
||||
// await db.delete(role).where(eq(role.roleId, roleId));
|
||||
|
||||
// return currentRole;
|
||||
// };
|
||||
|
||||
// export const updateRoleById = async (
|
||||
// roleId: string,
|
||||
// input: z.infer<typeof updateRoleSchema>,
|
||||
// ) => {
|
||||
// const currentRole = await findRoleById(roleId);
|
||||
|
||||
// if (!currentRole) {
|
||||
// throw new Error("Role not found");
|
||||
// }
|
||||
|
||||
// if (currentRole.isSystem) {
|
||||
// throw new Error("Cannot update system role");
|
||||
// }
|
||||
|
||||
// await db.update(role).set(input).where(eq(role.roleId, roleId));
|
||||
|
||||
// return currentRole;
|
||||
// };
|
||||
|
||||
// export const createDefaultRoles = async (organizationId: string) => {
|
||||
// await db.transaction(async (tx) => {
|
||||
// await tx.insert(role).values({
|
||||
// name: "owner",
|
||||
// description: "Owner of the organization with full access to all features",
|
||||
// organizationId,
|
||||
// isSystem: true,
|
||||
// permissions: ownerPermissions.map((permission) => permission.name),
|
||||
// });
|
||||
|
||||
// await tx.insert(role).values({
|
||||
// name: "admin",
|
||||
// description:
|
||||
// "Administrator with access to manage projects, services and configurations",
|
||||
// organizationId,
|
||||
// isSystem: true,
|
||||
// permissions: adminPermissions.map((permission) => permission.name),
|
||||
// });
|
||||
|
||||
// await tx.insert(role).values({
|
||||
// name: "member",
|
||||
// description:
|
||||
// "Regular member with access to create projects and manage services",
|
||||
// organizationId,
|
||||
// isSystem: true,
|
||||
// permissions: memberPermissions.map((permission) => permission.name),
|
||||
// });
|
||||
// });
|
||||
// };
|
||||
@@ -1,10 +1,11 @@
|
||||
import { db } from "@dokploy/server/db";
|
||||
import { apikey, member, users_temp } from "@dokploy/server/db/schema";
|
||||
import { apikey, member, users } from "@dokploy/server/db/schema";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { and, eq } from "drizzle-orm";
|
||||
import { auth } from "../lib/auth";
|
||||
import { PERMISSIONS } from "../lib/permissions";
|
||||
|
||||
export type User = typeof users_temp.$inferSelect;
|
||||
export type User = typeof users.$inferSelect;
|
||||
|
||||
export const addNewProject = async (
|
||||
userId: string,
|
||||
@@ -44,13 +45,16 @@ export const canPerformCreationService = async (
|
||||
projectId: string,
|
||||
organizationId: string,
|
||||
) => {
|
||||
const { accessedProjects, canCreateServices } = await findMemberById(
|
||||
const { accessedProjects, role } = await findMemberById(
|
||||
userId,
|
||||
organizationId,
|
||||
);
|
||||
const haveAccessToProject = accessedProjects.includes(projectId);
|
||||
|
||||
if (canCreateServices && haveAccessToProject) {
|
||||
if (
|
||||
role?.permissions?.includes(PERMISSIONS.SERVICE.CREATE.name) &&
|
||||
haveAccessToProject
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -77,13 +81,16 @@ export const canPeformDeleteService = async (
|
||||
serviceId: string,
|
||||
organizationId: string,
|
||||
) => {
|
||||
const { accessedServices, canDeleteServices } = await findMemberById(
|
||||
const { accessedServices, role } = await findMemberById(
|
||||
userId,
|
||||
organizationId,
|
||||
);
|
||||
const haveAccessToService = accessedServices.includes(serviceId);
|
||||
|
||||
if (canDeleteServices && haveAccessToService) {
|
||||
if (
|
||||
role?.permissions?.includes(PERMISSIONS.SERVICE.DELETE.name) &&
|
||||
haveAccessToService
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -94,9 +101,9 @@ export const canPerformCreationProject = async (
|
||||
userId: string,
|
||||
organizationId: string,
|
||||
) => {
|
||||
const { canCreateProjects } = await findMemberById(userId, organizationId);
|
||||
const { role } = await findMemberById(userId, organizationId);
|
||||
|
||||
if (canCreateProjects) {
|
||||
if (role?.permissions?.includes(PERMISSIONS.PROJECT.CREATE.name)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -107,9 +114,9 @@ export const canPerformDeleteProject = async (
|
||||
userId: string,
|
||||
organizationId: string,
|
||||
) => {
|
||||
const { canDeleteProjects } = await findMemberById(userId, organizationId);
|
||||
const { role } = await findMemberById(userId, organizationId);
|
||||
|
||||
if (canDeleteProjects) {
|
||||
if (role?.permissions?.includes(PERMISSIONS.PROJECT.DELETE.name)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -135,11 +142,8 @@ export const canAccessToTraefikFiles = async (
|
||||
userId: string,
|
||||
organizationId: string,
|
||||
) => {
|
||||
const { canAccessToTraefikFiles } = await findMemberById(
|
||||
userId,
|
||||
organizationId,
|
||||
);
|
||||
return canAccessToTraefikFiles;
|
||||
const { role } = await findMemberById(userId, organizationId);
|
||||
return role?.permissions?.includes(PERMISSIONS.TRAEFIK.ACCESS.name);
|
||||
};
|
||||
|
||||
export const checkServiceAccess = async (
|
||||
@@ -183,7 +187,7 @@ export const checkServiceAccess = async (
|
||||
};
|
||||
|
||||
export const checkProjectAccess = async (
|
||||
authId: string,
|
||||
userId: string,
|
||||
action: "create" | "delete" | "access",
|
||||
organizationId: string,
|
||||
projectId?: string,
|
||||
@@ -192,16 +196,16 @@ export const checkProjectAccess = async (
|
||||
switch (action) {
|
||||
case "access":
|
||||
hasPermission = await canPerformAccessProject(
|
||||
authId,
|
||||
userId,
|
||||
projectId as string,
|
||||
organizationId,
|
||||
);
|
||||
break;
|
||||
case "create":
|
||||
hasPermission = await canPerformCreationProject(authId, organizationId);
|
||||
hasPermission = await canPerformCreationProject(userId, organizationId);
|
||||
break;
|
||||
case "delete":
|
||||
hasPermission = await canPerformDeleteProject(authId, organizationId);
|
||||
hasPermission = await canPerformDeleteProject(userId, organizationId);
|
||||
break;
|
||||
default:
|
||||
hasPermission = false;
|
||||
@@ -225,6 +229,7 @@ export const findMemberById = async (
|
||||
),
|
||||
with: {
|
||||
user: true,
|
||||
role: true,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -239,11 +244,11 @@ export const findMemberById = async (
|
||||
|
||||
export const updateUser = async (userId: string, userData: Partial<User>) => {
|
||||
const user = await db
|
||||
.update(users_temp)
|
||||
.update(users)
|
||||
.set({
|
||||
...userData,
|
||||
})
|
||||
.where(eq(users_temp.id, userId))
|
||||
.where(eq(users.id, userId))
|
||||
.returning()
|
||||
.then((res) => res[0]);
|
||||
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
import { webServer, type updateWebServerSchema } from "../db/schema";
|
||||
import { db } from "../db";
|
||||
import type { z } from "zod";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
|
||||
export const createWebServer = async () => {
|
||||
const exists = await findWebServer();
|
||||
if (exists) {
|
||||
return exists;
|
||||
}
|
||||
const server = await db?.insert(webServer).values({});
|
||||
return server;
|
||||
};
|
||||
|
||||
export const findWebServer = async () => {
|
||||
const server = await db?.query.webServer.findFirst();
|
||||
|
||||
if (!server) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Web server not found",
|
||||
});
|
||||
}
|
||||
return server;
|
||||
};
|
||||
|
||||
export const updateWebServer = async (
|
||||
input: z.infer<typeof updateWebServerSchema>,
|
||||
) => {
|
||||
const server = await findWebServer();
|
||||
if (!server) {
|
||||
await createWebServer();
|
||||
}
|
||||
const updated = await db
|
||||
.update(webServer)
|
||||
.set({
|
||||
...input,
|
||||
})
|
||||
.returning()
|
||||
.then(([updated]) => updated);
|
||||
return updated;
|
||||
};
|
||||
@@ -1,11 +1,11 @@
|
||||
import { findServerById } from "@dokploy/server/services/server";
|
||||
import type { ContainerCreateOptions } from "dockerode";
|
||||
import { IS_CLOUD } from "../constants";
|
||||
import { findUserById } from "../services/admin";
|
||||
import { getDokployImageTag } from "../services/settings";
|
||||
import { pullImage, pullRemoteImage } from "../utils/docker/utils";
|
||||
import { execAsync, execAsyncRemote } from "../utils/process/execAsync";
|
||||
import { getRemoteDocker } from "../utils/servers/remote-docker";
|
||||
import { findWebServer } from "../services/web-server";
|
||||
|
||||
export const setupMonitoring = async (serverId: string) => {
|
||||
const server = await findServerById(serverId);
|
||||
@@ -83,8 +83,8 @@ export const setupMonitoring = async (serverId: string) => {
|
||||
}
|
||||
};
|
||||
|
||||
export const setupWebMonitoring = async (userId: string) => {
|
||||
const user = await findUserById(userId);
|
||||
export const setupWebMonitoring = async () => {
|
||||
const webServer = await findWebServer();
|
||||
|
||||
const containerName = "dokploy-monitoring";
|
||||
let imageName = "dokploy/monitoring:latest";
|
||||
@@ -99,7 +99,7 @@ export const setupWebMonitoring = async (userId: string) => {
|
||||
|
||||
const settings: ContainerCreateOptions = {
|
||||
name: containerName,
|
||||
Env: [`METRICS_CONFIG=${JSON.stringify(user?.metricsConfig)}`],
|
||||
Env: [`METRICS_CONFIG=${JSON.stringify(webServer?.metricsConfig)}`],
|
||||
Image: imageName,
|
||||
HostConfig: {
|
||||
// Memory: 100 * 1024 * 1024, // 100MB en bytes
|
||||
@@ -110,9 +110,9 @@ export const setupWebMonitoring = async (userId: string) => {
|
||||
Name: "always",
|
||||
},
|
||||
PortBindings: {
|
||||
[`${user?.metricsConfig?.server?.port}/tcp`]: [
|
||||
[`${webServer?.metricsConfig?.server?.port}/tcp`]: [
|
||||
{
|
||||
HostPort: user?.metricsConfig?.server?.port.toString(),
|
||||
HostPort: webServer?.metricsConfig?.server?.port.toString(),
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -126,7 +126,7 @@ export const setupWebMonitoring = async (userId: string) => {
|
||||
// NetworkMode: "host",
|
||||
},
|
||||
ExposedPorts: {
|
||||
[`${user?.metricsConfig?.server?.port}/tcp`]: {},
|
||||
[`${webServer?.metricsConfig?.server?.port}/tcp`]: {},
|
||||
},
|
||||
};
|
||||
const docker = await getRemoteDocker();
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import { paths } from "@dokploy/server/constants";
|
||||
import { findAdmin } from "@dokploy/server/services/admin";
|
||||
import { updateUser } from "@dokploy/server/services/user";
|
||||
import { findOwner } from "@dokploy/server/services/admin";
|
||||
import { scheduleJob, scheduledJobs } from "node-schedule";
|
||||
import { execAsync } from "../process/execAsync";
|
||||
import {
|
||||
findWebServer,
|
||||
updateWebServer,
|
||||
} from "@dokploy/server/services/web-server";
|
||||
|
||||
const LOG_CLEANUP_JOB_NAME = "access-log-cleanup";
|
||||
|
||||
@@ -29,9 +32,9 @@ export const startLogCleanup = async (
|
||||
}
|
||||
});
|
||||
|
||||
const admin = await findAdmin();
|
||||
if (admin) {
|
||||
await updateUser(admin.user.id, {
|
||||
const owner = await findOwner();
|
||||
if (owner) {
|
||||
await updateWebServer({
|
||||
logCleanupCron: cronExpression,
|
||||
});
|
||||
}
|
||||
@@ -51,9 +54,9 @@ export const stopLogCleanup = async (): Promise<boolean> => {
|
||||
}
|
||||
|
||||
// Update database
|
||||
const admin = await findAdmin();
|
||||
if (admin) {
|
||||
await updateUser(admin.user.id, {
|
||||
const owner = await findOwner();
|
||||
if (owner) {
|
||||
await updateWebServer({
|
||||
logCleanupCron: null,
|
||||
});
|
||||
}
|
||||
@@ -69,8 +72,8 @@ export const getLogCleanupStatus = async (): Promise<{
|
||||
enabled: boolean;
|
||||
cronExpression: string | null;
|
||||
}> => {
|
||||
const admin = await findAdmin();
|
||||
const cronExpression = admin?.user.logCleanupCron ?? null;
|
||||
const webServer = await findWebServer();
|
||||
const cronExpression = webServer?.logCleanupCron ?? null;
|
||||
return {
|
||||
enabled: cronExpression !== null,
|
||||
cronExpression,
|
||||
|
||||
@@ -15,6 +15,7 @@ import { member } from "@dokploy/server/db/schema";
|
||||
import type { BackupSchedule } from "@dokploy/server/services/backup";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { startLogCleanup } from "../access-log/handler";
|
||||
import { findWebServer } from "@dokploy/server/services/web-server";
|
||||
|
||||
export const initCronJobs = async () => {
|
||||
console.log("Setting up cron jobs....");
|
||||
@@ -26,11 +27,13 @@ export const initCronJobs = async () => {
|
||||
},
|
||||
});
|
||||
|
||||
if (!admin) {
|
||||
const webServer = await findWebServer();
|
||||
|
||||
if (!webServer || !admin) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (admin.user.enableDockerCleanup) {
|
||||
if (webServer.enableDockerCleanup) {
|
||||
scheduleJob("docker-cleanup", "0 0 * * *", async () => {
|
||||
console.log(
|
||||
`Docker Cleanup ${new Date().toLocaleString()}] Running docker cleanup`,
|
||||
@@ -87,9 +90,9 @@ export const initCronJobs = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
if (admin?.user.logCleanupCron) {
|
||||
console.log("Starting log requests cleanup", admin.user.logCleanupCron);
|
||||
await startLogCleanup(admin.user.logCleanupCron);
|
||||
if (webServer.logCleanupCron) {
|
||||
console.log("Starting log requests cleanup", webServer.logCleanupCron);
|
||||
await startLogCleanup(webServer.logCleanupCron);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { paths } from "@dokploy/server/constants";
|
||||
import type { User } from "@dokploy/server/services/user";
|
||||
import type { WebServer } from "@dokploy/server/db/schema";
|
||||
import { dump, load } from "js-yaml";
|
||||
import {
|
||||
loadOrCreateConfig,
|
||||
@@ -12,10 +12,10 @@ import type { FileConfig } from "./file-types";
|
||||
import type { MainTraefikConfig } from "./types";
|
||||
|
||||
export const updateServerTraefik = (
|
||||
user: User | null,
|
||||
webServer: WebServer | null,
|
||||
newHost: string | null,
|
||||
) => {
|
||||
const { https, certificateType } = user || {};
|
||||
const { https, certificateType } = webServer || {};
|
||||
const appName = "dokploy";
|
||||
const config: FileConfig = loadOrCreateConfig(appName);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user