import { randomBytes } from "crypto";
import { and, eq } from "drizzle-orm";
import { createTransport } from "nodemailer";
import { db } from "@karakeep/db";
import { verificationTokens } from "@karakeep/db/schema";
import serverConfig from "@karakeep/shared/config";
export async function sendVerificationEmail(email: string, name: string) {
if (!serverConfig.email.smtp) {
throw new Error("SMTP is not configured");
}
const token = randomBytes(10).toString("hex");
const expires = new Date(Date.now() + 24 * 60 * 60 * 1000); // 24 hours
// Store verification token
await db.insert(verificationTokens).values({
identifier: email,
token,
expires,
});
const transporter = createTransport({
host: serverConfig.email.smtp.host,
port: serverConfig.email.smtp.port,
secure: serverConfig.email.smtp.secure,
auth:
serverConfig.email.smtp.user && serverConfig.email.smtp.password
? {
user: serverConfig.email.smtp.user,
pass: serverConfig.email.smtp.password,
}
: undefined,
});
const verificationUrl = `${serverConfig.publicUrl}/verify-email?token=${encodeURIComponent(token)}&email=${encodeURIComponent(email)}`;
const mailOptions = {
from: serverConfig.email.smtp.from,
to: email,
subject: "Verify your email address",
html: `
Welcome to Karakeep, ${name}!
Please verify your email address by clicking the link below:
Verify Email Address
If the button doesn't work, you can copy and paste this link into your browser:
${verificationUrl}
This link will expire in 24 hours.
If you didn't create an account with us, please ignore this email.
`,
text: `
Welcome to Karakeep, ${name}!
Please verify your email address by visiting this link:
${verificationUrl}
This link will expire in 24 hours.
If you didn't create an account with us, please ignore this email.
`,
};
await transporter.sendMail(mailOptions);
}
export async function verifyEmailToken(
email: string,
token: string,
): Promise {
const verificationToken = await db.query.verificationTokens.findFirst({
where: (vt, { and, eq }) =>
and(eq(vt.identifier, email), eq(vt.token, token)),
});
if (!verificationToken) {
return false;
}
if (verificationToken.expires < new Date()) {
// Clean up expired token
await db
.delete(verificationTokens)
.where(
and(
eq(verificationTokens.identifier, email),
eq(verificationTokens.token, token),
),
);
return false;
}
// Clean up used token
await db
.delete(verificationTokens)
.where(
and(
eq(verificationTokens.identifier, email),
eq(verificationTokens.token, token),
),
);
return true;
}
export async function sendInviteEmail(
email: string,
token: string,
inviterName: string,
) {
if (!serverConfig.email.smtp) {
throw new Error("SMTP is not configured");
}
const transporter = createTransport({
host: serverConfig.email.smtp.host,
port: serverConfig.email.smtp.port,
secure: serverConfig.email.smtp.secure,
auth:
serverConfig.email.smtp.user && serverConfig.email.smtp.password
? {
user: serverConfig.email.smtp.user,
pass: serverConfig.email.smtp.password,
}
: undefined,
});
const inviteUrl = `${serverConfig.publicUrl}/invite/${encodeURIComponent(token)}`;
const mailOptions = {
from: serverConfig.email.smtp.from,
to: email,
subject: "You've been invited to join Karakeep",
html: `
You've been invited to join Karakeep!
${inviterName} has invited you to join Karakeep, the bookmark everything app.
Click the link below to accept your invitation and create your account:
Accept Invitation
If the button doesn't work, you can copy and paste this link into your browser:
${inviteUrl}
This invitation will expire in 7 days.
If you weren't expecting this invitation, you can safely ignore this email.
`,
text: `
You've been invited to join Karakeep!
${inviterName} has invited you to join Karakeep, a powerful bookmarking and content organization platform.
Accept your invitation by visiting this link:
${inviteUrl}
This invitation will expire in 7 days.
If you weren't expecting this invitation, you can safely ignore this email.
`,
};
await transporter.sendMail(mailOptions);
}