refactor: update database foreign key constraints and user management

This commit is contained in:
Mauricio Siu 2025-02-17 00:30:15 -06:00
parent 8c28223343
commit c7d47a6003
7 changed files with 4967 additions and 178 deletions

View File

@ -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>
);
};

View File

@ -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>
)}
</>

View 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;

File diff suppressed because it is too large Load Diff

View File

@ -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
}
]
}

View File

@ -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);
}),
});

View File

@ -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 }) => ({