diff --git a/apps/dokploy/components/dashboard/impersonation/impersonation-bar.tsx b/apps/dokploy/components/dashboard/impersonation/impersonation-bar.tsx new file mode 100644 index 00000000..9d3de57b --- /dev/null +++ b/apps/dokploy/components/dashboard/impersonation/impersonation-bar.tsx @@ -0,0 +1,472 @@ +"use client"; + +import { authClient } from "@/lib/auth-client"; +import { useEffect, useState } from "react"; +import { Button } from "@/components/ui/button"; +import { + CheckIcon, + ChevronsUpDown, + Settings2, + UserIcon, + XIcon, + Shield, + Calendar, + Key, + Copy, + Fingerprint, + Building2, + CreditCard, + Server, +} from "lucide-react"; +import { toast } from "sonner"; +import { + Command, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList, + CommandSeparator, +} from "@/components/ui/command"; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover"; +import { cn } from "@/lib/utils"; +import { Logo } from "@/components/shared/logo"; +import { Badge } from "@/components/ui/badge"; +import { + Tooltip, + TooltipContent, + TooltipTrigger, + TooltipProvider, +} from "@/components/ui/tooltip"; +import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; +import { format } from "date-fns"; +import copy from "copy-to-clipboard"; +import { api } from "@/utils/api"; +import type { RouterOutputs } from "@/utils/api"; + +type User = RouterOutputs["user"]["listUsers"]["users"][number]; + +export const ImpersonationBar = () => { + const [selectedUser, setSelectedUser] = useState(null); + const [isImpersonating, setIsImpersonating] = useState(false); + const [open, setOpen] = useState(false); + const [showBar, setShowBar] = useState(false); + const [searchTerm, setSearchTerm] = useState(""); + const [recentlyAccessed, setRecentlyAccessed] = useState([]); + const { data } = api.user.get.useQuery(); + const { data: users, isLoading } = api.user.listUsers.useQuery( + { + search: searchTerm, + }, + { + enabled: open && !isImpersonating, + }, + ); + + const handleImpersonate = async () => { + if (!selectedUser) return; + + try { + await authClient.admin.impersonateUser({ + userId: selectedUser.id, + }); + setIsImpersonating(true); + setOpen(false); + + setRecentlyAccessed((prev) => { + const filtered = prev.filter((u) => u.id !== selectedUser.id); + return [selectedUser, ...filtered].slice(0, 5); + }); + + toast.success("Successfully impersonating user", { + description: `You are now viewing as ${selectedUser.name || selectedUser.email}`, + }); + window.location.reload(); + } catch (error) { + console.error("Error impersonating user:", error); + toast.error("Error impersonating user"); + } + }; + + const handleStopImpersonating = async () => { + try { + await authClient.admin.stopImpersonating(); + setIsImpersonating(false); + setSelectedUser(null); + setShowBar(false); + toast.success("Stopped impersonating user"); + window.location.reload(); + } catch (error) { + console.error("Error stopping impersonation:", error); + toast.error("Error stopping impersonation"); + } + }; + + useEffect(() => { + const checkImpersonation = async () => { + try { + const session = await authClient.getSession(); + if (session?.data?.session?.impersonatedBy) { + setIsImpersonating(true); + setShowBar(true); + // setSelectedUser(data); + } + } catch (error) { + console.error("Error checking impersonation status:", error); + } + }; + + checkImpersonation(); + // fetchUsers(); + }, []); + + return ( + + <> + + + + + + {isImpersonating ? "Impersonation Controls" : "User Impersonation"} + + + +
+
+ + {!isImpersonating ? ( +
+ + + + + + + { + setSearchTerm(search); + }} + className="h-9" + /> + {isLoading ? ( +
+ Loading users... +
+ ) : ( + <> + No users found. + + {recentlyAccessed.length > 0 && !searchTerm && ( + <> + + {recentlyAccessed.map((user) => ( + { + setSelectedUser(user); + setOpen(false); + }} + > + + + + + {user.name || ""} + + + {user.email} • {user.role} + + + + + + ))} + + + + )} + + + {users?.users.map((user) => ( + { + setSelectedUser(user); + setOpen(false); + }} + > + + + + + {user.name || ""} + + + {user.email} • {user.role} + + + + + + ))} + + + + )} +
+
+
+ +
+ ) : ( +
+
+ + + + {data?.user?.name?.slice(0, 2).toUpperCase() || "U"} + + +
+
+ + + Impersonating + + + {data?.user?.name || ""} + +
+
+ + + {data?.user?.email} • {data?.role} + + + + + ID: {data?.user?.id?.slice(0, 8)} + + + + + + + Org: {data?.organizationId?.slice(0, 8)} + + + + {data?.user?.stripeCustomerId && ( + + + + Customer: + {data?.user?.stripeCustomerId?.slice(0, 8)} + + + + )} + {data?.user?.stripeSubscriptionId && ( + + + + Sub: {data?.user?.stripeSubscriptionId?.slice(0, 8)} + + + + )} + {data?.user?.serversQuantity !== undefined && ( + + + Servers: {data.user.serversQuantity} + + )} + {data?.createdAt && ( + + + Created:{" "} + {format(new Date(data.createdAt), "MMM d, yyyy")} + + )} + + + + + + 2FA{" "} + {data?.user?.twoFactorEnabled + ? "Enabled" + : "Disabled"} + + + + + Two-Factor Authentication Status + + +
+
+
+ +
+ )} +
+
+ +
+ ); +}; diff --git a/apps/dokploy/components/dashboard/settings/profile/profile-form.tsx b/apps/dokploy/components/dashboard/settings/profile/profile-form.tsx index 9532b7d6..e4af7b9e 100644 --- a/apps/dokploy/components/dashboard/settings/profile/profile-form.tsx +++ b/apps/dokploy/components/dashboard/settings/profile/profile-form.tsx @@ -10,6 +10,7 @@ import { import { Form, FormControl, + FormDescription, FormField, FormItem, FormLabel, @@ -28,12 +29,14 @@ import { toast } from "sonner"; import { z } from "zod"; import { Disable2FA } from "./disable-2fa"; import { Enable2FA } from "./enable-2fa"; +import { Switch } from "@/components/ui/switch"; const profileSchema = z.object({ email: z.string(), password: z.string().nullable(), currentPassword: z.string().nullable(), image: z.string().optional(), + allowImpersonation: z.boolean().optional().default(false), }); type Profile = z.infer; @@ -56,6 +59,7 @@ const randomImages = [ export const ProfileForm = () => { const _utils = api.useUtils(); const { data, refetch, isLoading } = api.user.get.useQuery(); + const { data: isCloud } = api.settings.isCloud.useQuery(); const { mutateAsync, @@ -79,6 +83,7 @@ export const ProfileForm = () => { password: "", image: data?.user?.image || "", currentPassword: "", + allowImpersonation: data?.user?.allowImpersonation || false, }, resolver: zodResolver(profileSchema), }); @@ -91,12 +96,17 @@ export const ProfileForm = () => { password: form.getValues("password") || "", image: data?.user?.image || "", currentPassword: form.getValues("currentPassword") || "", + allowImpersonation: data?.user?.allowImpersonation, }, { keepValues: true, }, ); + form.reset({ + allowImpersonation: data?.user?.allowImpersonation, + }); + if (data.user.email) { generateSHA256Hash(data.user.email).then((hash) => { setGravatarHash(hash); @@ -111,6 +121,7 @@ export const ProfileForm = () => { password: values.password || undefined, image: values.image, currentPassword: values.currentPassword || undefined, + allowImpersonation: values.allowImpersonation, }) .then(async () => { await refetch(); @@ -256,7 +267,34 @@ export const ProfileForm = () => { )} /> + {isCloud && ( + ( + +
+ Allow Impersonation + + Enable this option to allow Dokploy Cloud + administrators to temporarily access your + account for troubleshooting and support + purposes. This helps them quickly identify and + resolve any issues you may encounter. + +
+ + + +
+ )} + /> + )} +