rename to email otp to make it more explicit

This commit is contained in:
dswbx
2025-11-07 08:48:02 +01:00
parent 68fbb6e933
commit 341eb13425
3 changed files with 40 additions and 38 deletions

View File

@@ -1,5 +1,5 @@
import { afterAll, beforeAll, describe, expect, mock, test } from "bun:test";
import { otp } from "./otp.plugin";
import { emailOTP } from "./email-otp.plugin";
import { createApp } from "core/test/utils";
import { disableConsoleLog, enableConsoleLog } from "core/utils/test";
@@ -10,7 +10,7 @@ describe("otp plugin", () => {
test("should not work if auth is not enabled", async () => {
const app = createApp({
options: {
plugins: [otp({ showActualErrors: true })],
plugins: [emailOTP({ showActualErrors: true })],
},
});
await app.build();
@@ -37,7 +37,7 @@ describe("otp plugin", () => {
send: async () => {},
},
},
plugins: [otp({ showActualErrors: true })],
plugins: [emailOTP({ showActualErrors: true })],
},
});
await app.build();
@@ -69,7 +69,7 @@ describe("otp plugin", () => {
},
},
options: {
plugins: [otp({ showActualErrors: true })],
plugins: [emailOTP({ showActualErrors: true })],
drivers: {
email: {
send: async (to) => {
@@ -121,7 +121,7 @@ describe("otp plugin", () => {
},
options: {
plugins: [
otp({
emailOTP({
showActualErrors: true,
generateEmail: (otp) => ({ subject: "test", body: otp.code }),
}),
@@ -179,7 +179,7 @@ describe("otp plugin", () => {
},
options: {
plugins: [
otp({
emailOTP({
showActualErrors: true,
generateEmail: (otp) => ({ subject: "test", body: otp.code }),
}),

View File

@@ -16,7 +16,7 @@ import {
import { invariant, s, jsc, HttpStatus, threwAsync, randomString } from "bknd/utils";
import { Hono } from "hono";
export type OtpPluginOptions = {
export type EmailOTPPluginOptions = {
/**
* Customize code generation. If not provided, a random 6-digit code will be generated.
*/
@@ -49,7 +49,7 @@ export type OtpPluginOptions = {
* Customize email content. If not provided, a default email will be sent.
*/
generateEmail?: (
otp: OtpFieldSchema,
otp: EmailOTPFieldSchema,
) => MaybePromise<{ subject: string; body: string | { text: string; html: string } }>;
/**
@@ -57,6 +57,13 @@ export type OtpPluginOptions = {
* @default false
*/
showActualErrors?: boolean;
/**
* Allow direct mutations (create/update) of OTP codes outside of this plugin,
* e.g. via API or admin UI. If false, mutations are only allowed via the plugin's flows.
* @default false
*/
allowExternalMutations?: boolean;
};
const otpFields = {
@@ -70,9 +77,14 @@ const otpFields = {
used_at: datetime(),
};
export type OtpFieldSchema = FieldSchema<typeof otpFields>;
export type EmailOTPFieldSchema = FieldSchema<typeof otpFields>;
export function otp({
class OTPError extends Exception {
override name = "OTPError";
override code = HttpStatus.BAD_REQUEST;
}
export function emailOTP({
generateCode: _generateCode,
apiBasePath = "/api/auth/otp",
ttl = 600,
@@ -80,10 +92,11 @@ export function otp({
entityConfig,
generateEmail: _generateEmail,
showActualErrors = false,
}: OtpPluginOptions = {}): AppPlugin {
allowExternalMutations = false,
}: EmailOTPPluginOptions = {}): AppPlugin {
return (app: App) => {
return {
name: "bknd-otp",
name: "bknd-email-otp",
schema: () =>
em(
{
@@ -112,7 +125,7 @@ export function otp({
_generateCode ?? (() => Math.floor(100000 + Math.random() * 900000).toString());
const generateEmail =
_generateEmail ??
((otp: OtpFieldSchema) => ({
((otp: EmailOTPFieldSchema) => ({
subject: "OTP Code",
body: `Your OTP code is: ${otp.code}`,
}));
@@ -205,7 +218,7 @@ export function otp({
},
)
.onError((err) => {
if (showActualErrors) {
if (showActualErrors || err instanceof OTPError) {
throw err;
}
@@ -214,8 +227,9 @@ export function otp({
app.server.route(apiBasePath, hono);
// just for now, prevent mutations of the OTP entity
registerListeners(app, entityName);
if (allowExternalMutations !== true) {
registerListeners(app, entityName);
}
},
};
};
@@ -233,14 +247,14 @@ async function findUser(app: App, email: string) {
async function generateAndSendCode(
app: App,
opts: Required<Pick<OtpPluginOptions, "generateCode" | "generateEmail" | "ttl" | "entity">>,
opts: Required<Pick<EmailOTPPluginOptions, "generateCode" | "generateEmail" | "ttl" | "entity">>,
user: Pick<DB["users"], "email">,
action: OtpFieldSchema["action"],
action: EmailOTPFieldSchema["action"],
) {
const { generateCode, generateEmail, ttl, entity: entityName } = opts;
const newCode = generateCode?.(user);
if (!newCode) {
throw new Exception("[OTP Plugin]: Failed to generate code");
throw new OTPError("Failed to generate code");
}
await invalidateAllUserCodes(app, entityName, user.email, ttl);
@@ -266,18 +280,18 @@ async function getValidatedCode(
entityName: string,
email: string,
code: string,
action: OtpFieldSchema["action"],
action: EmailOTPFieldSchema["action"],
) {
invariant(email, "[OTP Plugin]: Email is required");
invariant(code, "[OTP Plugin]: Code is required");
const em = app.em.fork();
const { data: otpData } = await em.repo(entityName).findOne({ email, code, action });
if (!otpData) {
throw new Exception("Invalid code", HttpStatus.BAD_REQUEST);
throw new OTPError("Invalid code");
}
if (otpData.expires_at < new Date()) {
throw new Exception("Code expired", HttpStatus.GONE);
throw new OTPError("Code expired");
}
return otpData;
@@ -298,27 +312,15 @@ async function invalidateAllUserCodes(app: App, entityName: string, email: strin
function registerListeners(app: App, entityName: string) {
app.emgr.onAny(
(event) => {
let allowed = true;
let action = "";
if (event instanceof DatabaseEvents.MutatorInsertBefore) {
if (event instanceof DatabaseEvents.MutatorInsertBefore || event instanceof DatabaseEvents.MutatorUpdateBefore) {
if (event.params.entity.name === entityName) {
allowed = false;
action = "create";
throw new OTPError("Mutations of the OTP entity are not allowed");
}
} else if (event instanceof DatabaseEvents.MutatorUpdateBefore) {
if (event.params.entity.name === entityName) {
allowed = false;
action = "update";
}
}
if (!allowed) {
throw new Exception(`[OTP Plugin]: Not allowed to ${action} OTP codes manually`);
}
},
{
mode: "sync",
id: "bknd-otp",
id: "bknd-email-otp",
},
);
}

View File

@@ -8,4 +8,4 @@ export { syncConfig, type SyncConfigOptions } from "./dev/sync-config.plugin";
export { syncTypes, type SyncTypesOptions } from "./dev/sync-types.plugin";
export { syncSecrets, type SyncSecretsOptions } from "./dev/sync-secrets.plugin";
export { timestamps, type TimestampsPluginOptions } from "./data/timestamps.plugin";
export { otp, type OtpPluginOptions } from "./auth/otp.plugin";
export { emailOTP, type EmailOTPPluginOptions } from "./auth/email-otp.plugin";