mirror of
https://github.com/Dokploy/dokploy
synced 2025-06-26 18:27:59 +00:00
feat(invitation): add email provider selection and notification handling for user invitations
- Introduced a new optional field for notificationId in the invitation form. - Implemented fetching of email providers based on the active organization. - Enhanced invitation sending logic to include email notifications when applicable. - Updated UI to conditionally display email provider selection based on cloud status.
This commit is contained in:
parent
be91b53c86
commit
c84b271511
@ -41,6 +41,7 @@ const addInvitation = z.object({
|
||||
.min(1, "Email is required")
|
||||
.email({ message: "Invalid email" }),
|
||||
role: z.enum(["member", "admin"]),
|
||||
notificationId: z.string().optional(),
|
||||
});
|
||||
|
||||
type AddInvitation = z.infer<typeof addInvitation>;
|
||||
@ -49,6 +50,10 @@ export const AddInvitation = () => {
|
||||
const [open, setOpen] = useState(false);
|
||||
const utils = api.useUtils();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||
const { data: emailProviders } =
|
||||
api.notification.getEmailProviders.useQuery();
|
||||
const { mutateAsync: sendInvitation } = api.user.sendInvitation.useMutation();
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const { data: activeOrganization } = authClient.useActiveOrganization();
|
||||
|
||||
@ -56,6 +61,7 @@ export const AddInvitation = () => {
|
||||
defaultValues: {
|
||||
email: "",
|
||||
role: "member",
|
||||
notificationId: "",
|
||||
},
|
||||
resolver: zodResolver(addInvitation),
|
||||
});
|
||||
@ -74,7 +80,20 @@ export const AddInvitation = () => {
|
||||
if (result.error) {
|
||||
setError(result.error.message || "");
|
||||
} else {
|
||||
toast.success("Invitation created");
|
||||
if (!isCloud && data.notificationId) {
|
||||
await sendInvitation({
|
||||
invitationId: result.data.id,
|
||||
notificationId: data.notificationId || "",
|
||||
})
|
||||
.then(() => {
|
||||
toast.success("Invitation created and email sent");
|
||||
})
|
||||
.catch((error: any) => {
|
||||
toast.error(error.message);
|
||||
});
|
||||
} else {
|
||||
toast.success("Invitation created");
|
||||
}
|
||||
setError(null);
|
||||
setOpen(false);
|
||||
}
|
||||
@ -149,6 +168,47 @@ export const AddInvitation = () => {
|
||||
);
|
||||
}}
|
||||
/>
|
||||
|
||||
{!isCloud && (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="notificationId"
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<FormItem>
|
||||
<FormLabel>Email Provider</FormLabel>
|
||||
<Select
|
||||
onValueChange={field.onChange}
|
||||
defaultValue={field.value}
|
||||
>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select an email provider" />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
{emailProviders?.map((provider) => (
|
||||
<SelectItem
|
||||
key={provider.notificationId}
|
||||
value={provider.notificationId}
|
||||
>
|
||||
{provider.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
<SelectItem value="none" disabled>
|
||||
None
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormDescription>
|
||||
Select the email provider to send the invitation
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<DialogFooter className="flex w-full flex-row">
|
||||
<Button
|
||||
isLoading={isLoading}
|
||||
|
@ -446,4 +446,12 @@ export const notificationRouter = createTRPCRouter({
|
||||
});
|
||||
}
|
||||
}),
|
||||
getEmailProviders: adminProcedure.query(async ({ ctx }) => {
|
||||
return await db.query.notifications.findMany({
|
||||
where: eq(notifications.organizationId, ctx.session.activeOrganizationId),
|
||||
with: {
|
||||
email: true,
|
||||
},
|
||||
});
|
||||
}),
|
||||
});
|
||||
|
@ -1,10 +1,13 @@
|
||||
import {
|
||||
IS_CLOUD,
|
||||
createApiKey,
|
||||
findAdmin,
|
||||
findNotificationById,
|
||||
findOrganizationById,
|
||||
findUserById,
|
||||
getUserByToken,
|
||||
removeUserById,
|
||||
sendEmailNotification,
|
||||
updateUser,
|
||||
} from "@dokploy/server";
|
||||
import { db } from "@dokploy/server/db";
|
||||
@ -362,4 +365,59 @@ export const userRouter = createTRPCRouter({
|
||||
|
||||
return organizations.length;
|
||||
}),
|
||||
sendInvitation: adminProcedure
|
||||
.input(
|
||||
z.object({
|
||||
invitationId: z.string().min(1),
|
||||
notificationId: z.string().min(1),
|
||||
}),
|
||||
)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
if (IS_CLOUD) {
|
||||
return;
|
||||
}
|
||||
|
||||
const notification = await findNotificationById(input.notificationId);
|
||||
|
||||
const email = notification.email;
|
||||
|
||||
const currentInvitation = await db.query.invitation.findFirst({
|
||||
where: eq(invitation.id, input.invitationId),
|
||||
});
|
||||
|
||||
if (!email) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Email notification not found",
|
||||
});
|
||||
}
|
||||
|
||||
const admin = await findAdmin();
|
||||
const host =
|
||||
process.env.NODE_ENV === "development"
|
||||
? "http://localhost:3000"
|
||||
: admin.user.host;
|
||||
const inviteLink = `${host}/invitation?token=${input.invitationId}`;
|
||||
|
||||
const organization = await findOrganizationById(
|
||||
ctx.session.activeOrganizationId,
|
||||
);
|
||||
|
||||
try {
|
||||
await sendEmailNotification(
|
||||
{
|
||||
...email,
|
||||
toAddresses: [currentInvitation?.email || ""],
|
||||
},
|
||||
"Invitation to join organization",
|
||||
`
|
||||
<p>You are invited to join ${organization?.name || "organization"} on Dokploy. Click the link to accept the invitation: <a href="${inviteLink}">Accept Invitation</a></p>
|
||||
`,
|
||||
);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
throw error;
|
||||
}
|
||||
return inviteLink;
|
||||
}),
|
||||
});
|
||||
|
Loading…
Reference in New Issue
Block a user