mirror of
https://github.com/Dokploy/dokploy
synced 2025-06-26 18:27:59 +00:00
feat: initial commit
This commit is contained in:
378
components/dashboard/settings/users/add-permissions.tsx
Normal file
378
components/dashboard/settings/users/add-permissions.tsx
Normal file
@@ -0,0 +1,378 @@
|
||||
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 {
|
||||
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 { AlertTriangle, ListTodo } from "lucide-react";
|
||||
import { useEffect } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
|
||||
const addPermissions = z.object({
|
||||
accesedProjects: z.array(z.string()).optional(),
|
||||
accesedServices: 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),
|
||||
});
|
||||
|
||||
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.byUserId.useQuery(
|
||||
{
|
||||
userId,
|
||||
},
|
||||
{
|
||||
enabled: !!userId,
|
||||
},
|
||||
);
|
||||
|
||||
const { mutateAsync, isError, error, isLoading } =
|
||||
api.admin.assignPermissions.useMutation();
|
||||
|
||||
const form = useForm<AddPermissions>({
|
||||
defaultValues: {
|
||||
accesedProjects: [],
|
||||
accesedServices: [],
|
||||
},
|
||||
resolver: zodResolver(addPermissions),
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
form.reset({
|
||||
accesedProjects: data.accesedProjects || [],
|
||||
accesedServices: data.accesedServices || [],
|
||||
canCreateProjects: data.canCreateProjects,
|
||||
canCreateServices: data.canCreateServices,
|
||||
canDeleteProjects: data.canDeleteProjects,
|
||||
canDeleteServices: data.canDeleteServices,
|
||||
canAccessToTraefikFiles: data.canAccessToTraefikFiles,
|
||||
canAccessToDocker: data.canAccessToDocker,
|
||||
});
|
||||
}
|
||||
}, [form, form.formState.isSubmitSuccessful, form.reset, data]);
|
||||
|
||||
const onSubmit = async (data: AddPermissions) => {
|
||||
await mutateAsync({
|
||||
userId,
|
||||
canCreateServices: data.canCreateServices,
|
||||
canCreateProjects: data.canCreateProjects,
|
||||
canDeleteServices: data.canDeleteServices,
|
||||
canDeleteProjects: data.canDeleteProjects,
|
||||
canAccessToTraefikFiles: data.canAccessToTraefikFiles,
|
||||
accesedProjects: data.accesedProjects || [],
|
||||
accesedServices: data.accesedServices || [],
|
||||
canAccessToDocker: data.canAccessToDocker,
|
||||
})
|
||||
.then(async () => {
|
||||
toast.success("Permissions updated");
|
||||
refetch();
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to update the permissions");
|
||||
});
|
||||
};
|
||||
return (
|
||||
<Dialog>
|
||||
<DialogTrigger className="" asChild>
|
||||
<Button variant="ghost">
|
||||
<ListTodo className="size-4 text-muted-foreground " />
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-h-[85vh] overflow-y-auto sm:max-w-4xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Permissions</DialogTitle>
|
||||
<DialogDescription>Add or remove permissions</DialogDescription>
|
||||
</DialogHeader>
|
||||
{isError && (
|
||||
<div className="flex flex-row gap-4 rounded-lg bg-red-50 p-2 dark:bg-red-950">
|
||||
<AlertTriangle className="text-red-600 dark:text-red-400" />
|
||||
<span className="text-sm text-red-600 dark:text-red-400">
|
||||
{error?.message}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<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="accesedProjects"
|
||||
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="accesedProjects"
|
||||
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="accesedServices"
|
||||
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>
|
||||
);
|
||||
};
|
||||
123
components/dashboard/settings/users/add-user.tsx
Normal file
123
components/dashboard/settings/users/add-user.tsx
Normal file
@@ -0,0 +1,123 @@
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { api } from "@/utils/api";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { AlertTriangle } from "lucide-react";
|
||||
import { useEffect } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
|
||||
const addUser = z.object({
|
||||
email: z
|
||||
.string()
|
||||
.min(1, "Email is required")
|
||||
.email({ message: "Invalid email" }),
|
||||
});
|
||||
|
||||
type AddUser = z.infer<typeof addUser>;
|
||||
|
||||
export const AddUser = () => {
|
||||
const utils = api.useUtils();
|
||||
|
||||
const { mutateAsync, isError, error, isLoading } =
|
||||
api.admin.createUserInvitation.useMutation();
|
||||
|
||||
const form = useForm<AddUser>({
|
||||
defaultValues: {
|
||||
email: "",
|
||||
},
|
||||
resolver: zodResolver(addUser),
|
||||
});
|
||||
useEffect(() => {
|
||||
form.reset();
|
||||
}, [form, form.formState.isSubmitSuccessful, form.reset]);
|
||||
|
||||
const onSubmit = async (data: AddUser) => {
|
||||
await mutateAsync({
|
||||
email: data.email,
|
||||
})
|
||||
.then(async () => {
|
||||
toast.success("Invitation created");
|
||||
await utils.user.all.invalidate();
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to create the invitation");
|
||||
});
|
||||
};
|
||||
return (
|
||||
<Dialog>
|
||||
<DialogTrigger className="" asChild>
|
||||
<Button>Add User</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-2xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Add User</DialogTitle>
|
||||
<DialogDescription>Invite a new user</DialogDescription>
|
||||
</DialogHeader>
|
||||
{isError && (
|
||||
<div className="flex flex-row gap-4 rounded-lg bg-red-50 p-2 dark:bg-red-950">
|
||||
<AlertTriangle className="text-red-600 dark:text-red-400" />
|
||||
<span className="text-sm text-red-600 dark:text-red-400">
|
||||
{error?.message}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Form {...form}>
|
||||
<form
|
||||
id="hook-form-add-user"
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="grid w-full gap-4 "
|
||||
>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="email"
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<FormItem>
|
||||
<FormLabel>Email</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder={"email@dokploy.com"} {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
This will be the email of the new user
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<DialogFooter className="flex w-full flex-row">
|
||||
<Button
|
||||
isLoading={isLoading}
|
||||
form="hook-form-add-user"
|
||||
type="submit"
|
||||
>
|
||||
Create
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
</Form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
60
components/dashboard/settings/users/delete-user.tsx
Normal file
60
components/dashboard/settings/users/delete-user.tsx
Normal file
@@ -0,0 +1,60 @@
|
||||
import React from "react";
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
AlertDialogTrigger,
|
||||
} from "@/components/ui/alert-dialog";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { api } from "@/utils/api";
|
||||
import { TrashIcon } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
interface Props {
|
||||
authId: string;
|
||||
}
|
||||
export const DeleteUser = ({ authId }: Props) => {
|
||||
const { mutateAsync, isLoading } = api.admin.removeUser.useMutation();
|
||||
const utils = api.useUtils();
|
||||
return (
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger asChild>
|
||||
<Button variant="ghost" isLoading={isLoading}>
|
||||
<TrashIcon className="size-4 text-muted-foreground " />
|
||||
</Button>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
This action cannot be undone. This will permanently delete the user.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={async () => {
|
||||
await mutateAsync({
|
||||
authId,
|
||||
})
|
||||
.then(async () => {
|
||||
utils.user.all.invalidate();
|
||||
toast.success("User delete succesfully");
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to delete the user");
|
||||
});
|
||||
}}
|
||||
>
|
||||
Confirm
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
);
|
||||
};
|
||||
110
components/dashboard/settings/users/show-users.tsx
Normal file
110
components/dashboard/settings/users/show-users.tsx
Normal file
@@ -0,0 +1,110 @@
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import { api } from "@/utils/api";
|
||||
import { CopyIcon, Users } from "lucide-react";
|
||||
import { AddUser } from "./add-user";
|
||||
import { DeleteUser } from "./delete-user";
|
||||
import { format } from "date-fns";
|
||||
import { useEffect, useState } from "react";
|
||||
import { AddUserPermissions } from "./add-permissions";
|
||||
import copy from "copy-to-clipboard";
|
||||
import { toast } from "sonner";
|
||||
import { UpdateUser } from "./update-user";
|
||||
|
||||
export const ShowUsers = () => {
|
||||
const { data } = api.user.all.useQuery();
|
||||
const [url, setUrl] = useState("");
|
||||
useEffect(() => {
|
||||
setUrl(document.location.origin);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="h-full col-span-2">
|
||||
<Card className="bg-transparent h-full border-none">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-xl">Users</CardTitle>
|
||||
<CardDescription>Add, manage and delete users.</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-2 h-full">
|
||||
{data?.length === 0 ? (
|
||||
<div className="flex flex-col items-center gap-3">
|
||||
<Users className="size-8 self-center text-muted-foreground" />
|
||||
<span className="text-base text-muted-foreground">
|
||||
To create a user is required to add
|
||||
</span>
|
||||
<AddUser />
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col gap-6">
|
||||
{data?.map((user) => {
|
||||
return (
|
||||
<div
|
||||
key={user.userId}
|
||||
className="flex gap-2 flex-col justify-start border p-4 rounded-lg"
|
||||
>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{user.auth.email}
|
||||
</span>
|
||||
{!user.isRegistered && (
|
||||
<span className="text-sm text-muted-foreground">
|
||||
Expire In{" "}
|
||||
{format(new Date(user.expirationDate), "PPpp")}
|
||||
</span>
|
||||
)}
|
||||
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{user.isRegistered ? "Registered" : "Not Registered"}
|
||||
</span>
|
||||
{user.auth.is2FAEnabled && (
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{user.auth.is2FAEnabled
|
||||
? "2FA Enabled"
|
||||
: "2FA Not Enabled"}
|
||||
</span>
|
||||
)}
|
||||
|
||||
<div className="flex flex-wrap flex-row gap-3">
|
||||
{!user.isRegistered && (
|
||||
<div className="overflow-x-auto flex flex-row gap-4 items-center">
|
||||
<div className="overflow-x-auto">
|
||||
<span className="text-sm text-muted-foreground ">
|
||||
{`${url}/invitation?token=${user.token}`}
|
||||
</span>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
// className="absolute right-2 top-2"
|
||||
onClick={() => {
|
||||
copy(`${url}/invitation?token=${user.token}`);
|
||||
toast.success("Invitation Copied to clipboard");
|
||||
}}
|
||||
>
|
||||
<CopyIcon className="size-4 text-muted-foreground" />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{user.isRegistered && (
|
||||
<AddUserPermissions userId={user.userId} />
|
||||
)}
|
||||
{user.isRegistered && <UpdateUser authId={user.authId} />}
|
||||
<DeleteUser authId={user.authId} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
<div className="flex flex-col justify-end gap-3 w-full items-end">
|
||||
<AddUser />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
168
components/dashboard/settings/users/update-user.tsx
Normal file
168
components/dashboard/settings/users/update-user.tsx
Normal file
@@ -0,0 +1,168 @@
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { z } from "zod";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useEffect } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { AlertTriangle, SquarePen } from "lucide-react";
|
||||
import { api } from "@/utils/api";
|
||||
|
||||
const updateUserSchema = z.object({
|
||||
email: z
|
||||
.string()
|
||||
.min(1, "Email is required")
|
||||
.email({ message: "Invalid email" }),
|
||||
password: z.string(),
|
||||
});
|
||||
|
||||
type UpdateUser = z.infer<typeof updateUserSchema>;
|
||||
|
||||
interface Props {
|
||||
authId: string;
|
||||
}
|
||||
|
||||
export const UpdateUser = ({ authId }: Props) => {
|
||||
const utils = api.useUtils();
|
||||
const { mutateAsync, error, isError, isLoading } =
|
||||
api.auth.updateByAdmin.useMutation();
|
||||
const { data } = api.auth.one.useQuery(
|
||||
{
|
||||
id: authId,
|
||||
},
|
||||
{
|
||||
enabled: !!authId,
|
||||
},
|
||||
);
|
||||
|
||||
const form = useForm<UpdateUser>({
|
||||
defaultValues: {
|
||||
email: "",
|
||||
password: "",
|
||||
},
|
||||
resolver: zodResolver(updateUserSchema),
|
||||
});
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
form.reset({
|
||||
email: data.email || "",
|
||||
password: "",
|
||||
});
|
||||
}
|
||||
}, [data, form, form.reset]);
|
||||
|
||||
const onSubmit = async (formData: UpdateUser) => {
|
||||
await mutateAsync({
|
||||
email: formData.email === data?.email ? null : formData.email,
|
||||
password: formData.password,
|
||||
id: authId,
|
||||
})
|
||||
.then(() => {
|
||||
toast.success("User updated succesfully");
|
||||
utils.user.all.invalidate();
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to update the user");
|
||||
})
|
||||
.finally(() => {});
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog>
|
||||
<DialogTrigger asChild className="w-fit">
|
||||
<Button
|
||||
variant="ghost"
|
||||
className=" cursor-pointer space-x-3 w-fit"
|
||||
onSelect={(e) => e.preventDefault()}
|
||||
>
|
||||
<SquarePen className="size-4 text-muted-foreground" />
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-lg">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Update User</DialogTitle>
|
||||
<DialogDescription>Update the user</DialogDescription>
|
||||
</DialogHeader>
|
||||
{isError && (
|
||||
<div className="flex flex-row gap-4 rounded-lg bg-red-50 p-2 dark:bg-red-950">
|
||||
<AlertTriangle className="text-red-600 dark:text-red-400" />
|
||||
<span className="text-sm text-red-600 dark:text-red-400">
|
||||
{error.message}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="grid gap-4">
|
||||
<div className="grid items-center gap-4">
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
id="hook-form-update-user"
|
||||
className="grid w-full gap-4 "
|
||||
>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="email"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Email</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="XNl5C@example.com" {...field} />
|
||||
</FormControl>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="password"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Password</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="password"
|
||||
placeholder="*******"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<DialogFooter>
|
||||
<Button
|
||||
form="hook-form-update-user"
|
||||
type="submit"
|
||||
isLoading={isLoading}
|
||||
>
|
||||
Update
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
</Form>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user