mirror of
https://github.com/Dokploy/dokploy
synced 2025-06-26 18:27:59 +00:00
refactor: update database foreign key constraints and user management
This commit is contained in:
parent
8c28223343
commit
c7d47a6003
@ -1,129 +0,0 @@
|
||||
import { AlertBlock } from "@/components/shared/alert-block";
|
||||
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 { authClient } from "@/lib/auth-client";
|
||||
import { api } from "@/utils/api";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { PlusIcon } from "lucide-react";
|
||||
import { useEffect, useState } 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 [open, setOpen] = useState(false);
|
||||
const utils = api.useUtils();
|
||||
|
||||
const { data: activeOrganization } = authClient.useActiveOrganization();
|
||||
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) => {
|
||||
const result = await authClient.organization.inviteMember({
|
||||
email: data.email.toLowerCase(),
|
||||
role: "member",
|
||||
organizationId: activeOrganization?.id,
|
||||
});
|
||||
console.log(result);
|
||||
// await mutateAsync({
|
||||
// email: data.email.toLowerCase(),
|
||||
// })
|
||||
// .then(async () => {
|
||||
// toast.success("Invitation created");
|
||||
// await utils.user.all.invalidate();
|
||||
// setOpen(false);
|
||||
// })
|
||||
// .catch(() => {
|
||||
// toast.error("Error creating the invitation");
|
||||
// });
|
||||
};
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogTrigger className="" asChild>
|
||||
<Button>
|
||||
<PlusIcon className="h-4 w-4" /> 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 && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
||||
|
||||
<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>
|
||||
);
|
||||
};
|
@ -27,18 +27,16 @@ import { api } from "@/utils/api";
|
||||
import copy from "copy-to-clipboard";
|
||||
import { format } from "date-fns";
|
||||
import { MoreHorizontal, Users } from "lucide-react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { AddUserPermissions } from "./add-permissions";
|
||||
import { AddUser } from "./add-user";
|
||||
|
||||
import { DialogAction } from "@/components/shared/dialog-action";
|
||||
import { Loader2 } from "lucide-react";
|
||||
import { authClient } from "@/lib/auth-client";
|
||||
|
||||
export const ShowUsers = () => {
|
||||
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||
const { data, isLoading, refetch } = api.user.all.useQuery();
|
||||
const { mutateAsync, isLoading: isRemoving } =
|
||||
api.admin.removeUser.useMutation();
|
||||
const { mutateAsync, isLoading: isRemoving } = api.user.remove.useMutation();
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
@ -67,7 +65,6 @@ export const ShowUsers = () => {
|
||||
<span className="text-base text-muted-foreground">
|
||||
Invite users to your Dokploy account
|
||||
</span>
|
||||
<AddUser />
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col gap-4 min-h-[25vh]">
|
||||
@ -88,36 +85,37 @@ export const ShowUsers = () => {
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{data?.map((user) => {
|
||||
{data?.map((member) => {
|
||||
return (
|
||||
<TableRow key={user.userId}>
|
||||
<TableRow key={member.id}>
|
||||
<TableCell className="w-[100px]">
|
||||
{user.user.email}
|
||||
{member.user.email}
|
||||
</TableCell>
|
||||
<TableCell className="text-center">
|
||||
<Badge
|
||||
variant={
|
||||
user.role === "owner"
|
||||
member.role === "owner"
|
||||
? "default"
|
||||
: "secondary"
|
||||
}
|
||||
>
|
||||
{user.role}
|
||||
{member.role}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell className="text-center">
|
||||
{user.user.twoFactorEnabled
|
||||
{member.user.twoFactorEnabled
|
||||
? "Enabled"
|
||||
: "Disabled"}
|
||||
</TableCell>
|
||||
<TableCell className="text-center">
|
||||
{user.user.isRegistered || user.role === "owner"
|
||||
{member.user.isRegistered ||
|
||||
member.role === "owner"
|
||||
? "Registered"
|
||||
: "Not Registered"}
|
||||
</TableCell>
|
||||
<TableCell className="text-right">
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{format(new Date(user.createdAt), "PPpp")}
|
||||
{format(new Date(member.createdAt), "PPpp")}
|
||||
</span>
|
||||
</TableCell>
|
||||
|
||||
@ -136,13 +134,13 @@ export const ShowUsers = () => {
|
||||
<DropdownMenuLabel>
|
||||
Actions
|
||||
</DropdownMenuLabel>
|
||||
{!user.user.isRegistered &&
|
||||
user.role !== "owner" && (
|
||||
{!member.user.isRegistered &&
|
||||
member.role !== "owner" && (
|
||||
<DropdownMenuItem
|
||||
className="w-full cursor-pointer"
|
||||
onSelect={(e) => {
|
||||
copy(
|
||||
`${origin}/invitation?token=${user.user.token}`,
|
||||
`${origin}/invitation?token=${member.user.token}`,
|
||||
);
|
||||
toast.success(
|
||||
"Invitation Copied to clipboard",
|
||||
@ -153,32 +151,53 @@ export const ShowUsers = () => {
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
|
||||
{user.role !== "owner" && (
|
||||
{member.role !== "owner" && (
|
||||
<AddUserPermissions
|
||||
userId={user.userId}
|
||||
userId={member.user.id}
|
||||
/>
|
||||
)}
|
||||
|
||||
{user.role !== "owner" && (
|
||||
{member.role !== "owner" && (
|
||||
<DialogAction
|
||||
title="Delete User"
|
||||
description="Are you sure you want to delete this user?"
|
||||
type="destructive"
|
||||
onClick={async () => {
|
||||
await mutateAsync({
|
||||
userId: user.userId,
|
||||
})
|
||||
.then(() => {
|
||||
if (isCloud) {
|
||||
const { error } =
|
||||
await authClient.organization.removeMember(
|
||||
{
|
||||
memberIdOrEmail:
|
||||
member.user.id,
|
||||
},
|
||||
);
|
||||
|
||||
if (!error) {
|
||||
toast.success(
|
||||
"User deleted successfully",
|
||||
);
|
||||
refetch();
|
||||
})
|
||||
.catch(() => {
|
||||
} else {
|
||||
toast.error(
|
||||
"Error deleting destination",
|
||||
"Error deleting user",
|
||||
);
|
||||
});
|
||||
}
|
||||
} else {
|
||||
await mutateAsync({
|
||||
userId: member.user.id,
|
||||
})
|
||||
.then(() => {
|
||||
toast.success(
|
||||
"User deleted successfully",
|
||||
);
|
||||
refetch();
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error(
|
||||
"Error deleting destination",
|
||||
);
|
||||
});
|
||||
}
|
||||
}}
|
||||
>
|
||||
<DropdownMenuItem
|
||||
@ -197,10 +216,6 @@ export const ShowUsers = () => {
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
|
||||
<div className="flex flex-row gap-2 flex-wrap w-full justify-end mr-4">
|
||||
<AddUser />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
|
18
apps/dokploy/drizzle/0074_lowly_jack_power.sql
Normal file
18
apps/dokploy/drizzle/0074_lowly_jack_power.sql
Normal file
@ -0,0 +1,18 @@
|
||||
ALTER TABLE "account" DROP CONSTRAINT "account_user_id_user_temp_id_fk";
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "invitation" DROP CONSTRAINT "invitation_organization_id_organization_id_fk";
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "invitation" DROP CONSTRAINT "invitation_inviter_id_user_temp_id_fk";
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "member" DROP CONSTRAINT "member_organization_id_organization_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 "account" ADD CONSTRAINT "account_user_id_user_temp_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user_temp"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "invitation" ADD CONSTRAINT "invitation_organization_id_organization_id_fk" FOREIGN KEY ("organization_id") REFERENCES "public"."organization"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "invitation" ADD CONSTRAINT "invitation_inviter_id_user_temp_id_fk" FOREIGN KEY ("inviter_id") REFERENCES "public"."user_temp"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "member" ADD CONSTRAINT "member_organization_id_organization_id_fk" FOREIGN KEY ("organization_id") REFERENCES "public"."organization"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "member" ADD CONSTRAINT "member_user_id_user_temp_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user_temp"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "organization" ADD CONSTRAINT "organization_owner_id_user_temp_id_fk" FOREIGN KEY ("owner_id") REFERENCES "public"."user_temp"("id") ON DELETE cascade ON UPDATE no action;
|
4878
apps/dokploy/drizzle/meta/0074_snapshot.json
Normal file
4878
apps/dokploy/drizzle/meta/0074_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
@ -519,6 +519,13 @@
|
||||
"when": 1739740193879,
|
||||
"tag": "0073_polite_miss_america",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 74,
|
||||
"version": "7",
|
||||
"when": 1739773539709,
|
||||
"tag": "0074_lowly_jack_power",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
@ -2,11 +2,13 @@ import { apiFindOneUser, apiFindOneUserByAuth } from "@/server/db/schema";
|
||||
import {
|
||||
findUserByAuthId,
|
||||
findUserById,
|
||||
IS_CLOUD,
|
||||
removeUserById,
|
||||
updateUser,
|
||||
verify2FA,
|
||||
} from "@dokploy/server";
|
||||
import { db } from "@dokploy/server/db";
|
||||
import { apiUpdateUser, member } from "@dokploy/server/db/schema";
|
||||
import { account, apiUpdateUser, member } from "@dokploy/server/db/schema";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { z } from "zod";
|
||||
@ -44,19 +46,17 @@ export const userRouter = createTRPCRouter({
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
return await updateUser(ctx.user.id, input);
|
||||
}),
|
||||
verify2FASetup: protectedProcedure
|
||||
|
||||
remove: protectedProcedure
|
||||
.input(
|
||||
z.object({
|
||||
secret: z.string(),
|
||||
pin: z.string(),
|
||||
userId: z.string(),
|
||||
}),
|
||||
)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const user = await findUserById(ctx.user.id);
|
||||
await verify2FA(user, input.secret, input.pin);
|
||||
await updateUser(user.id, {
|
||||
secret: input.secret,
|
||||
});
|
||||
return user;
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
if (IS_CLOUD) {
|
||||
return true;
|
||||
}
|
||||
return await removeUserById(input.userId);
|
||||
}),
|
||||
});
|
||||
|
@ -15,7 +15,7 @@ export const account = pgTable("account", {
|
||||
providerId: text("provider_id").notNull(),
|
||||
userId: text("user_id")
|
||||
.notNull()
|
||||
.references(() => users_temp.id),
|
||||
.references(() => users_temp.id, { onDelete: "cascade" }),
|
||||
accessToken: text("access_token"),
|
||||
refreshToken: text("refresh_token"),
|
||||
idToken: text("id_token"),
|
||||
@ -59,7 +59,7 @@ export const organization = pgTable("organization", {
|
||||
metadata: text("metadata"),
|
||||
ownerId: text("owner_id")
|
||||
.notNull()
|
||||
.references(() => users_temp.id),
|
||||
.references(() => users_temp.id, { onDelete: "cascade" }),
|
||||
});
|
||||
|
||||
export const organizationRelations = relations(
|
||||
@ -80,10 +80,10 @@ export const member = pgTable("member", {
|
||||
.$defaultFn(() => nanoid()),
|
||||
organizationId: text("organization_id")
|
||||
.notNull()
|
||||
.references(() => organization.id),
|
||||
.references(() => organization.id, { onDelete: "cascade" }),
|
||||
userId: text("user_id")
|
||||
.notNull()
|
||||
.references(() => users_temp.id),
|
||||
.references(() => users_temp.id, { onDelete: "cascade" }),
|
||||
role: text("role").notNull().$type<"owner" | "member" | "admin">(),
|
||||
createdAt: timestamp("created_at").notNull(),
|
||||
});
|
||||
@ -103,14 +103,14 @@ export const invitation = pgTable("invitation", {
|
||||
id: text("id").primaryKey(),
|
||||
organizationId: text("organization_id")
|
||||
.notNull()
|
||||
.references(() => organization.id),
|
||||
.references(() => organization.id, { onDelete: "cascade" }),
|
||||
email: text("email").notNull(),
|
||||
role: text("role").$type<"owner" | "member" | "admin">(),
|
||||
status: text("status").notNull(),
|
||||
expiresAt: timestamp("expires_at").notNull(),
|
||||
inviterId: text("inviter_id")
|
||||
.notNull()
|
||||
.references(() => users_temp.id),
|
||||
.references(() => users_temp.id, { onDelete: "cascade" }),
|
||||
});
|
||||
|
||||
export const invitationRelations = relations(invitation, ({ one }) => ({
|
||||
|
Loading…
Reference in New Issue
Block a user