mirror of
https://github.com/Dokploy/dokploy
synced 2025-06-26 18:27:59 +00:00
Merge pull request #2056 from Dokploy/1834-user-invite-email-not-sending
feat(invitation): add email provider selection and notification handl…
This commit is contained in:
@@ -41,6 +41,7 @@ const addInvitation = z.object({
|
|||||||
.min(1, "Email is required")
|
.min(1, "Email is required")
|
||||||
.email({ message: "Invalid email" }),
|
.email({ message: "Invalid email" }),
|
||||||
role: z.enum(["member", "admin"]),
|
role: z.enum(["member", "admin"]),
|
||||||
|
notificationId: z.string().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
type AddInvitation = z.infer<typeof addInvitation>;
|
type AddInvitation = z.infer<typeof addInvitation>;
|
||||||
@@ -49,6 +50,10 @@ export const AddInvitation = () => {
|
|||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const utils = api.useUtils();
|
const utils = api.useUtils();
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
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 [error, setError] = useState<string | null>(null);
|
||||||
const { data: activeOrganization } = authClient.useActiveOrganization();
|
const { data: activeOrganization } = authClient.useActiveOrganization();
|
||||||
|
|
||||||
@@ -56,6 +61,7 @@ export const AddInvitation = () => {
|
|||||||
defaultValues: {
|
defaultValues: {
|
||||||
email: "",
|
email: "",
|
||||||
role: "member",
|
role: "member",
|
||||||
|
notificationId: "",
|
||||||
},
|
},
|
||||||
resolver: zodResolver(addInvitation),
|
resolver: zodResolver(addInvitation),
|
||||||
});
|
});
|
||||||
@@ -74,7 +80,20 @@ export const AddInvitation = () => {
|
|||||||
if (result.error) {
|
if (result.error) {
|
||||||
setError(result.error.message || "");
|
setError(result.error.message || "");
|
||||||
} else {
|
} 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);
|
setError(null);
|
||||||
setOpen(false);
|
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">
|
<DialogFooter className="flex w-full flex-row">
|
||||||
<Button
|
<Button
|
||||||
isLoading={isLoading}
|
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 {
|
import {
|
||||||
IS_CLOUD,
|
IS_CLOUD,
|
||||||
createApiKey,
|
createApiKey,
|
||||||
|
findAdmin,
|
||||||
|
findNotificationById,
|
||||||
findOrganizationById,
|
findOrganizationById,
|
||||||
findUserById,
|
findUserById,
|
||||||
getUserByToken,
|
getUserByToken,
|
||||||
removeUserById,
|
removeUserById,
|
||||||
|
sendEmailNotification,
|
||||||
updateUser,
|
updateUser,
|
||||||
} from "@dokploy/server";
|
} from "@dokploy/server";
|
||||||
import { db } from "@dokploy/server/db";
|
import { db } from "@dokploy/server/db";
|
||||||
@@ -362,4 +365,59 @@ export const userRouter = createTRPCRouter({
|
|||||||
|
|
||||||
return organizations.length;
|
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;
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user