mirror of
https://github.com/shishantbiswas/bknd.git
synced 2026-03-16 04:27:21 +00:00
otp: add sendEmail option to disable sending for debugging
This commit is contained in:
@@ -221,6 +221,38 @@ describe("otp plugin", () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("should not send email if sendEmail is false", async () => {
|
||||||
|
const called = mock(() => null);
|
||||||
|
const app = createApp({
|
||||||
|
config: {
|
||||||
|
auth: {
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
plugins: [emailOTP({ sendEmail: false })],
|
||||||
|
drivers: {
|
||||||
|
email: {
|
||||||
|
send: async () => {
|
||||||
|
called();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await app.build();
|
||||||
|
|
||||||
|
const res = await app.server.request("/api/auth/otp/register", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ email: "test@test.com" }),
|
||||||
|
});
|
||||||
|
expect(res.status).toBe(201);
|
||||||
|
expect(called).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
// @todo: test invalid codes
|
// @todo: test invalid codes
|
||||||
// @todo: test codes with different actions
|
// @todo: test codes with different actions
|
||||||
// @todo: test code expiration
|
// @todo: test code expiration
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import {
|
|||||||
type MaybePromise,
|
type MaybePromise,
|
||||||
type EntityConfig,
|
type EntityConfig,
|
||||||
} from "bknd";
|
} from "bknd";
|
||||||
import { invariant, s, jsc, HttpStatus, threwAsync, randomString } from "bknd/utils";
|
import { invariant, s, jsc, HttpStatus, threwAsync, randomString, $console } from "bknd/utils";
|
||||||
import { Hono } from "hono";
|
import { Hono } from "hono";
|
||||||
|
|
||||||
export type EmailOTPPluginOptions = {
|
export type EmailOTPPluginOptions = {
|
||||||
@@ -64,6 +64,12 @@ export type EmailOTPPluginOptions = {
|
|||||||
* @default false
|
* @default false
|
||||||
*/
|
*/
|
||||||
allowExternalMutations?: boolean;
|
allowExternalMutations?: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether to send the email with the OTP code.
|
||||||
|
* @default true
|
||||||
|
*/
|
||||||
|
sendEmail?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const otpFields = {
|
const otpFields = {
|
||||||
@@ -93,6 +99,7 @@ export function emailOTP({
|
|||||||
generateEmail: _generateEmail,
|
generateEmail: _generateEmail,
|
||||||
showActualErrors = false,
|
showActualErrors = false,
|
||||||
allowExternalMutations = false,
|
allowExternalMutations = false,
|
||||||
|
sendEmail = true,
|
||||||
}: EmailOTPPluginOptions = {}): AppPlugin {
|
}: EmailOTPPluginOptions = {}): AppPlugin {
|
||||||
return (app: App) => {
|
return (app: App) => {
|
||||||
return {
|
return {
|
||||||
@@ -138,11 +145,13 @@ export function emailOTP({
|
|||||||
"json",
|
"json",
|
||||||
s.object({
|
s.object({
|
||||||
email: s.string({ format: "email" }),
|
email: s.string({ format: "email" }),
|
||||||
code: s.string().optional(),
|
code: s.string({ minLength: 1 }).optional(),
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
|
jsc("query", s.object({ redirect: s.string().optional() })),
|
||||||
async (c) => {
|
async (c) => {
|
||||||
const { email, code } = c.req.valid("json");
|
const { email, code } = c.req.valid("json");
|
||||||
|
const { redirect } = c.req.valid("query");
|
||||||
const user = await findUser(app, email);
|
const user = await findUser(app, email);
|
||||||
|
|
||||||
if (code) {
|
if (code) {
|
||||||
@@ -157,14 +166,21 @@ export function emailOTP({
|
|||||||
|
|
||||||
const jwt = await auth.authenticator.jwt(user);
|
const jwt = await auth.authenticator.jwt(user);
|
||||||
// @ts-expect-error private method
|
// @ts-expect-error private method
|
||||||
return auth.authenticator.respondWithUser(c, { user, token: jwt });
|
return auth.authenticator.respondWithUser(
|
||||||
|
c,
|
||||||
|
{ user, token: jwt },
|
||||||
|
{ redirect },
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
await generateAndSendCode(
|
const otpData = await invalidateAndGenerateCode(
|
||||||
app,
|
app,
|
||||||
{ generateCode, generateEmail, ttl, entity: entityName },
|
{ generateCode, ttl, entity: entityName },
|
||||||
user,
|
user,
|
||||||
"login",
|
"login",
|
||||||
);
|
);
|
||||||
|
if (sendEmail) {
|
||||||
|
await sendCode(app, otpData, { generateEmail });
|
||||||
|
}
|
||||||
|
|
||||||
return c.json({ sent: true, action: "login" }, HttpStatus.CREATED);
|
return c.json({ sent: true, action: "login" }, HttpStatus.CREATED);
|
||||||
}
|
}
|
||||||
@@ -176,11 +192,13 @@ export function emailOTP({
|
|||||||
"json",
|
"json",
|
||||||
s.object({
|
s.object({
|
||||||
email: s.string({ format: "email" }),
|
email: s.string({ format: "email" }),
|
||||||
code: s.string().optional(),
|
code: s.string({ minLength: 1 }).optional(),
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
|
jsc("query", s.object({ redirect: s.string().optional() })),
|
||||||
async (c) => {
|
async (c) => {
|
||||||
const { email, code } = c.req.valid("json");
|
const { email, code } = c.req.valid("json");
|
||||||
|
const { redirect } = c.req.valid("query");
|
||||||
|
|
||||||
// throw if user exists
|
// throw if user exists
|
||||||
if (!(await threwAsync(findUser(app, email)))) {
|
if (!(await threwAsync(findUser(app, email)))) {
|
||||||
@@ -204,14 +222,21 @@ export function emailOTP({
|
|||||||
|
|
||||||
const jwt = await auth.authenticator.jwt(user);
|
const jwt = await auth.authenticator.jwt(user);
|
||||||
// @ts-expect-error private method
|
// @ts-expect-error private method
|
||||||
return auth.authenticator.respondWithUser(c, { user, token: jwt });
|
return auth.authenticator.respondWithUser(
|
||||||
|
c,
|
||||||
|
{ user, token: jwt },
|
||||||
|
{ redirect },
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
await generateAndSendCode(
|
const otpData = await invalidateAndGenerateCode(
|
||||||
app,
|
app,
|
||||||
{ generateCode, generateEmail, ttl, entity: entityName },
|
{ generateCode, ttl, entity: entityName },
|
||||||
{ email },
|
{ email },
|
||||||
"register",
|
"register",
|
||||||
);
|
);
|
||||||
|
if (sendEmail) {
|
||||||
|
await sendCode(app, otpData, { generateEmail });
|
||||||
|
}
|
||||||
|
|
||||||
return c.json({ sent: true, action: "register" }, HttpStatus.CREATED);
|
return c.json({ sent: true, action: "register" }, HttpStatus.CREATED);
|
||||||
}
|
}
|
||||||
@@ -245,13 +270,13 @@ async function findUser(app: App, email: string) {
|
|||||||
return user;
|
return user;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function generateAndSendCode(
|
async function invalidateAndGenerateCode(
|
||||||
app: App,
|
app: App,
|
||||||
opts: Required<Pick<EmailOTPPluginOptions, "generateCode" | "generateEmail" | "ttl" | "entity">>,
|
opts: Required<Pick<EmailOTPPluginOptions, "generateCode" | "ttl" | "entity">>,
|
||||||
user: Pick<DB["users"], "email">,
|
user: Pick<DB["users"], "email">,
|
||||||
action: EmailOTPFieldSchema["action"],
|
action: EmailOTPFieldSchema["action"],
|
||||||
) {
|
) {
|
||||||
const { generateCode, generateEmail, ttl, entity: entityName } = opts;
|
const { generateCode, ttl, entity: entityName } = opts;
|
||||||
const newCode = generateCode?.(user);
|
const newCode = generateCode?.(user);
|
||||||
if (!newCode) {
|
if (!newCode) {
|
||||||
throw new OTPError("Failed to generate code");
|
throw new OTPError("Failed to generate code");
|
||||||
@@ -269,12 +294,21 @@ async function generateAndSendCode(
|
|||||||
expires_at: new Date(Date.now() + ttl * 1000),
|
expires_at: new Date(Date.now() + ttl * 1000),
|
||||||
});
|
});
|
||||||
|
|
||||||
const { subject, body } = await generateEmail(otpData);
|
$console.log("[OTP Code]", newCode);
|
||||||
await app.drivers?.email?.send(user.email, subject, body);
|
|
||||||
|
|
||||||
return otpData;
|
return otpData;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function sendCode(
|
||||||
|
app: App,
|
||||||
|
otpData: EmailOTPFieldSchema,
|
||||||
|
opts: Required<Pick<EmailOTPPluginOptions, "generateEmail">>,
|
||||||
|
) {
|
||||||
|
const { generateEmail } = opts;
|
||||||
|
const { subject, body } = await generateEmail(otpData);
|
||||||
|
await app.drivers?.email?.send(otpData.email, subject, body);
|
||||||
|
}
|
||||||
|
|
||||||
async function getValidatedCode(
|
async function getValidatedCode(
|
||||||
app: App,
|
app: App,
|
||||||
entityName: string,
|
entityName: string,
|
||||||
|
|||||||
Reference in New Issue
Block a user