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:
@@ -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 copy from "copy-to-clipboard";
|
||||||
import { format } from "date-fns";
|
import { format } from "date-fns";
|
||||||
import { MoreHorizontal, Users } from "lucide-react";
|
import { MoreHorizontal, Users } from "lucide-react";
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { AddUserPermissions } from "./add-permissions";
|
import { AddUserPermissions } from "./add-permissions";
|
||||||
import { AddUser } from "./add-user";
|
|
||||||
|
|
||||||
import { DialogAction } from "@/components/shared/dialog-action";
|
import { DialogAction } from "@/components/shared/dialog-action";
|
||||||
import { Loader2 } from "lucide-react";
|
import { Loader2 } from "lucide-react";
|
||||||
|
import { authClient } from "@/lib/auth-client";
|
||||||
|
|
||||||
export const ShowUsers = () => {
|
export const ShowUsers = () => {
|
||||||
|
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||||
const { data, isLoading, refetch } = api.user.all.useQuery();
|
const { data, isLoading, refetch } = api.user.all.useQuery();
|
||||||
const { mutateAsync, isLoading: isRemoving } =
|
const { mutateAsync, isLoading: isRemoving } = api.user.remove.useMutation();
|
||||||
api.admin.removeUser.useMutation();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
@@ -67,7 +65,6 @@ export const ShowUsers = () => {
|
|||||||
<span className="text-base text-muted-foreground">
|
<span className="text-base text-muted-foreground">
|
||||||
Invite users to your Dokploy account
|
Invite users to your Dokploy account
|
||||||
</span>
|
</span>
|
||||||
<AddUser />
|
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex flex-col gap-4 min-h-[25vh]">
|
<div className="flex flex-col gap-4 min-h-[25vh]">
|
||||||
@@ -88,36 +85,37 @@ export const ShowUsers = () => {
|
|||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{data?.map((user) => {
|
{data?.map((member) => {
|
||||||
return (
|
return (
|
||||||
<TableRow key={user.userId}>
|
<TableRow key={member.id}>
|
||||||
<TableCell className="w-[100px]">
|
<TableCell className="w-[100px]">
|
||||||
{user.user.email}
|
{member.user.email}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="text-center">
|
<TableCell className="text-center">
|
||||||
<Badge
|
<Badge
|
||||||
variant={
|
variant={
|
||||||
user.role === "owner"
|
member.role === "owner"
|
||||||
? "default"
|
? "default"
|
||||||
: "secondary"
|
: "secondary"
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{user.role}
|
{member.role}
|
||||||
</Badge>
|
</Badge>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="text-center">
|
<TableCell className="text-center">
|
||||||
{user.user.twoFactorEnabled
|
{member.user.twoFactorEnabled
|
||||||
? "Enabled"
|
? "Enabled"
|
||||||
: "Disabled"}
|
: "Disabled"}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="text-center">
|
<TableCell className="text-center">
|
||||||
{user.user.isRegistered || user.role === "owner"
|
{member.user.isRegistered ||
|
||||||
|
member.role === "owner"
|
||||||
? "Registered"
|
? "Registered"
|
||||||
: "Not Registered"}
|
: "Not Registered"}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="text-right">
|
<TableCell className="text-right">
|
||||||
<span className="text-sm text-muted-foreground">
|
<span className="text-sm text-muted-foreground">
|
||||||
{format(new Date(user.createdAt), "PPpp")}
|
{format(new Date(member.createdAt), "PPpp")}
|
||||||
</span>
|
</span>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
|
|
||||||
@@ -136,13 +134,13 @@ export const ShowUsers = () => {
|
|||||||
<DropdownMenuLabel>
|
<DropdownMenuLabel>
|
||||||
Actions
|
Actions
|
||||||
</DropdownMenuLabel>
|
</DropdownMenuLabel>
|
||||||
{!user.user.isRegistered &&
|
{!member.user.isRegistered &&
|
||||||
user.role !== "owner" && (
|
member.role !== "owner" && (
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
className="w-full cursor-pointer"
|
className="w-full cursor-pointer"
|
||||||
onSelect={(e) => {
|
onSelect={(e) => {
|
||||||
copy(
|
copy(
|
||||||
`${origin}/invitation?token=${user.user.token}`,
|
`${origin}/invitation?token=${member.user.token}`,
|
||||||
);
|
);
|
||||||
toast.success(
|
toast.success(
|
||||||
"Invitation Copied to clipboard",
|
"Invitation Copied to clipboard",
|
||||||
@@ -153,32 +151,53 @@ export const ShowUsers = () => {
|
|||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{user.role !== "owner" && (
|
{member.role !== "owner" && (
|
||||||
<AddUserPermissions
|
<AddUserPermissions
|
||||||
userId={user.userId}
|
userId={member.user.id}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{user.role !== "owner" && (
|
{member.role !== "owner" && (
|
||||||
<DialogAction
|
<DialogAction
|
||||||
title="Delete User"
|
title="Delete User"
|
||||||
description="Are you sure you want to delete this user?"
|
description="Are you sure you want to delete this user?"
|
||||||
type="destructive"
|
type="destructive"
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
await mutateAsync({
|
if (isCloud) {
|
||||||
userId: user.userId,
|
const { error } =
|
||||||
})
|
await authClient.organization.removeMember(
|
||||||
.then(() => {
|
{
|
||||||
|
memberIdOrEmail:
|
||||||
|
member.user.id,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!error) {
|
||||||
toast.success(
|
toast.success(
|
||||||
"User deleted successfully",
|
"User deleted successfully",
|
||||||
);
|
);
|
||||||
refetch();
|
refetch();
|
||||||
})
|
} else {
|
||||||
.catch(() => {
|
|
||||||
toast.error(
|
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
|
<DropdownMenuItem
|
||||||
@@ -197,10 +216,6 @@ export const ShowUsers = () => {
|
|||||||
})}
|
})}
|
||||||
</TableBody>
|
</TableBody>
|
||||||
</Table>
|
</Table>
|
||||||
|
|
||||||
<div className="flex flex-row gap-2 flex-wrap w-full justify-end mr-4">
|
|
||||||
<AddUser />
|
|
||||||
</div>
|
|
||||||
</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,
|
"when": 1739740193879,
|
||||||
"tag": "0073_polite_miss_america",
|
"tag": "0073_polite_miss_america",
|
||||||
"breakpoints": true
|
"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 {
|
import {
|
||||||
findUserByAuthId,
|
findUserByAuthId,
|
||||||
findUserById,
|
findUserById,
|
||||||
|
IS_CLOUD,
|
||||||
|
removeUserById,
|
||||||
updateUser,
|
updateUser,
|
||||||
verify2FA,
|
verify2FA,
|
||||||
} from "@dokploy/server";
|
} from "@dokploy/server";
|
||||||
import { db } from "@dokploy/server/db";
|
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 { TRPCError } from "@trpc/server";
|
||||||
import { eq } from "drizzle-orm";
|
import { eq } from "drizzle-orm";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
@@ -44,19 +46,17 @@ export const userRouter = createTRPCRouter({
|
|||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
return await updateUser(ctx.user.id, input);
|
return await updateUser(ctx.user.id, input);
|
||||||
}),
|
}),
|
||||||
verify2FASetup: protectedProcedure
|
|
||||||
|
remove: protectedProcedure
|
||||||
.input(
|
.input(
|
||||||
z.object({
|
z.object({
|
||||||
secret: z.string(),
|
userId: z.string(),
|
||||||
pin: z.string(),
|
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.mutation(async ({ ctx, input }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
const user = await findUserById(ctx.user.id);
|
if (IS_CLOUD) {
|
||||||
await verify2FA(user, input.secret, input.pin);
|
return true;
|
||||||
await updateUser(user.id, {
|
}
|
||||||
secret: input.secret,
|
return await removeUserById(input.userId);
|
||||||
});
|
|
||||||
return user;
|
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ export const account = pgTable("account", {
|
|||||||
providerId: text("provider_id").notNull(),
|
providerId: text("provider_id").notNull(),
|
||||||
userId: text("user_id")
|
userId: text("user_id")
|
||||||
.notNull()
|
.notNull()
|
||||||
.references(() => users_temp.id),
|
.references(() => users_temp.id, { onDelete: "cascade" }),
|
||||||
accessToken: text("access_token"),
|
accessToken: text("access_token"),
|
||||||
refreshToken: text("refresh_token"),
|
refreshToken: text("refresh_token"),
|
||||||
idToken: text("id_token"),
|
idToken: text("id_token"),
|
||||||
@@ -59,7 +59,7 @@ export const organization = pgTable("organization", {
|
|||||||
metadata: text("metadata"),
|
metadata: text("metadata"),
|
||||||
ownerId: text("owner_id")
|
ownerId: text("owner_id")
|
||||||
.notNull()
|
.notNull()
|
||||||
.references(() => users_temp.id),
|
.references(() => users_temp.id, { onDelete: "cascade" }),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const organizationRelations = relations(
|
export const organizationRelations = relations(
|
||||||
@@ -80,10 +80,10 @@ export const member = pgTable("member", {
|
|||||||
.$defaultFn(() => nanoid()),
|
.$defaultFn(() => nanoid()),
|
||||||
organizationId: text("organization_id")
|
organizationId: text("organization_id")
|
||||||
.notNull()
|
.notNull()
|
||||||
.references(() => organization.id),
|
.references(() => organization.id, { onDelete: "cascade" }),
|
||||||
userId: text("user_id")
|
userId: text("user_id")
|
||||||
.notNull()
|
.notNull()
|
||||||
.references(() => users_temp.id),
|
.references(() => users_temp.id, { onDelete: "cascade" }),
|
||||||
role: text("role").notNull().$type<"owner" | "member" | "admin">(),
|
role: text("role").notNull().$type<"owner" | "member" | "admin">(),
|
||||||
createdAt: timestamp("created_at").notNull(),
|
createdAt: timestamp("created_at").notNull(),
|
||||||
});
|
});
|
||||||
@@ -103,14 +103,14 @@ export const invitation = pgTable("invitation", {
|
|||||||
id: text("id").primaryKey(),
|
id: text("id").primaryKey(),
|
||||||
organizationId: text("organization_id")
|
organizationId: text("organization_id")
|
||||||
.notNull()
|
.notNull()
|
||||||
.references(() => organization.id),
|
.references(() => organization.id, { onDelete: "cascade" }),
|
||||||
email: text("email").notNull(),
|
email: text("email").notNull(),
|
||||||
role: text("role").$type<"owner" | "member" | "admin">(),
|
role: text("role").$type<"owner" | "member" | "admin">(),
|
||||||
status: text("status").notNull(),
|
status: text("status").notNull(),
|
||||||
expiresAt: timestamp("expires_at").notNull(),
|
expiresAt: timestamp("expires_at").notNull(),
|
||||||
inviterId: text("inviter_id")
|
inviterId: text("inviter_id")
|
||||||
.notNull()
|
.notNull()
|
||||||
.references(() => users_temp.id),
|
.references(() => users_temp.id, { onDelete: "cascade" }),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const invitationRelations = relations(invitation, ({ one }) => ({
|
export const invitationRelations = relations(invitation, ({ one }) => ({
|
||||||
|
|||||||
Reference in New Issue
Block a user