mirror of
https://github.com/shishantbiswas/bknd.git
synced 2026-03-16 04:27:21 +00:00
rename to email otp to make it more explicit
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
import { afterAll, beforeAll, describe, expect, mock, test } from "bun:test";
|
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 { createApp } from "core/test/utils";
|
||||||
import { disableConsoleLog, enableConsoleLog } from "core/utils/test";
|
import { disableConsoleLog, enableConsoleLog } from "core/utils/test";
|
||||||
|
|
||||||
@@ -10,7 +10,7 @@ describe("otp plugin", () => {
|
|||||||
test("should not work if auth is not enabled", async () => {
|
test("should not work if auth is not enabled", async () => {
|
||||||
const app = createApp({
|
const app = createApp({
|
||||||
options: {
|
options: {
|
||||||
plugins: [otp({ showActualErrors: true })],
|
plugins: [emailOTP({ showActualErrors: true })],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
await app.build();
|
await app.build();
|
||||||
@@ -37,7 +37,7 @@ describe("otp plugin", () => {
|
|||||||
send: async () => {},
|
send: async () => {},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
plugins: [otp({ showActualErrors: true })],
|
plugins: [emailOTP({ showActualErrors: true })],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
await app.build();
|
await app.build();
|
||||||
@@ -69,7 +69,7 @@ describe("otp plugin", () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
options: {
|
options: {
|
||||||
plugins: [otp({ showActualErrors: true })],
|
plugins: [emailOTP({ showActualErrors: true })],
|
||||||
drivers: {
|
drivers: {
|
||||||
email: {
|
email: {
|
||||||
send: async (to) => {
|
send: async (to) => {
|
||||||
@@ -121,7 +121,7 @@ describe("otp plugin", () => {
|
|||||||
},
|
},
|
||||||
options: {
|
options: {
|
||||||
plugins: [
|
plugins: [
|
||||||
otp({
|
emailOTP({
|
||||||
showActualErrors: true,
|
showActualErrors: true,
|
||||||
generateEmail: (otp) => ({ subject: "test", body: otp.code }),
|
generateEmail: (otp) => ({ subject: "test", body: otp.code }),
|
||||||
}),
|
}),
|
||||||
@@ -179,7 +179,7 @@ describe("otp plugin", () => {
|
|||||||
},
|
},
|
||||||
options: {
|
options: {
|
||||||
plugins: [
|
plugins: [
|
||||||
otp({
|
emailOTP({
|
||||||
showActualErrors: true,
|
showActualErrors: true,
|
||||||
generateEmail: (otp) => ({ subject: "test", body: otp.code }),
|
generateEmail: (otp) => ({ subject: "test", body: otp.code }),
|
||||||
}),
|
}),
|
||||||
@@ -16,7 +16,7 @@ import {
|
|||||||
import { invariant, s, jsc, HttpStatus, threwAsync, randomString } from "bknd/utils";
|
import { invariant, s, jsc, HttpStatus, threwAsync, randomString } from "bknd/utils";
|
||||||
import { Hono } from "hono";
|
import { Hono } from "hono";
|
||||||
|
|
||||||
export type OtpPluginOptions = {
|
export type EmailOTPPluginOptions = {
|
||||||
/**
|
/**
|
||||||
* Customize code generation. If not provided, a random 6-digit code will be generated.
|
* 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.
|
* Customize email content. If not provided, a default email will be sent.
|
||||||
*/
|
*/
|
||||||
generateEmail?: (
|
generateEmail?: (
|
||||||
otp: OtpFieldSchema,
|
otp: EmailOTPFieldSchema,
|
||||||
) => MaybePromise<{ subject: string; body: string | { text: string; html: string } }>;
|
) => MaybePromise<{ subject: string; body: string | { text: string; html: string } }>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -57,6 +57,13 @@ export type OtpPluginOptions = {
|
|||||||
* @default false
|
* @default false
|
||||||
*/
|
*/
|
||||||
showActualErrors?: boolean;
|
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 = {
|
const otpFields = {
|
||||||
@@ -70,9 +77,14 @@ const otpFields = {
|
|||||||
used_at: datetime(),
|
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,
|
generateCode: _generateCode,
|
||||||
apiBasePath = "/api/auth/otp",
|
apiBasePath = "/api/auth/otp",
|
||||||
ttl = 600,
|
ttl = 600,
|
||||||
@@ -80,10 +92,11 @@ export function otp({
|
|||||||
entityConfig,
|
entityConfig,
|
||||||
generateEmail: _generateEmail,
|
generateEmail: _generateEmail,
|
||||||
showActualErrors = false,
|
showActualErrors = false,
|
||||||
}: OtpPluginOptions = {}): AppPlugin {
|
allowExternalMutations = false,
|
||||||
|
}: EmailOTPPluginOptions = {}): AppPlugin {
|
||||||
return (app: App) => {
|
return (app: App) => {
|
||||||
return {
|
return {
|
||||||
name: "bknd-otp",
|
name: "bknd-email-otp",
|
||||||
schema: () =>
|
schema: () =>
|
||||||
em(
|
em(
|
||||||
{
|
{
|
||||||
@@ -112,7 +125,7 @@ export function otp({
|
|||||||
_generateCode ?? (() => Math.floor(100000 + Math.random() * 900000).toString());
|
_generateCode ?? (() => Math.floor(100000 + Math.random() * 900000).toString());
|
||||||
const generateEmail =
|
const generateEmail =
|
||||||
_generateEmail ??
|
_generateEmail ??
|
||||||
((otp: OtpFieldSchema) => ({
|
((otp: EmailOTPFieldSchema) => ({
|
||||||
subject: "OTP Code",
|
subject: "OTP Code",
|
||||||
body: `Your OTP code is: ${otp.code}`,
|
body: `Your OTP code is: ${otp.code}`,
|
||||||
}));
|
}));
|
||||||
@@ -205,7 +218,7 @@ export function otp({
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
.onError((err) => {
|
.onError((err) => {
|
||||||
if (showActualErrors) {
|
if (showActualErrors || err instanceof OTPError) {
|
||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -214,8 +227,9 @@ export function otp({
|
|||||||
|
|
||||||
app.server.route(apiBasePath, hono);
|
app.server.route(apiBasePath, hono);
|
||||||
|
|
||||||
// just for now, prevent mutations of the OTP entity
|
if (allowExternalMutations !== true) {
|
||||||
registerListeners(app, entityName);
|
registerListeners(app, entityName);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -233,14 +247,14 @@ async function findUser(app: App, email: string) {
|
|||||||
|
|
||||||
async function generateAndSendCode(
|
async function generateAndSendCode(
|
||||||
app: App,
|
app: App,
|
||||||
opts: Required<Pick<OtpPluginOptions, "generateCode" | "generateEmail" | "ttl" | "entity">>,
|
opts: Required<Pick<EmailOTPPluginOptions, "generateCode" | "generateEmail" | "ttl" | "entity">>,
|
||||||
user: Pick<DB["users"], "email">,
|
user: Pick<DB["users"], "email">,
|
||||||
action: OtpFieldSchema["action"],
|
action: EmailOTPFieldSchema["action"],
|
||||||
) {
|
) {
|
||||||
const { generateCode, generateEmail, ttl, entity: entityName } = opts;
|
const { generateCode, generateEmail, ttl, entity: entityName } = opts;
|
||||||
const newCode = generateCode?.(user);
|
const newCode = generateCode?.(user);
|
||||||
if (!newCode) {
|
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);
|
await invalidateAllUserCodes(app, entityName, user.email, ttl);
|
||||||
@@ -266,18 +280,18 @@ async function getValidatedCode(
|
|||||||
entityName: string,
|
entityName: string,
|
||||||
email: string,
|
email: string,
|
||||||
code: string,
|
code: string,
|
||||||
action: OtpFieldSchema["action"],
|
action: EmailOTPFieldSchema["action"],
|
||||||
) {
|
) {
|
||||||
invariant(email, "[OTP Plugin]: Email is required");
|
invariant(email, "[OTP Plugin]: Email is required");
|
||||||
invariant(code, "[OTP Plugin]: Code is required");
|
invariant(code, "[OTP Plugin]: Code is required");
|
||||||
const em = app.em.fork();
|
const em = app.em.fork();
|
||||||
const { data: otpData } = await em.repo(entityName).findOne({ email, code, action });
|
const { data: otpData } = await em.repo(entityName).findOne({ email, code, action });
|
||||||
if (!otpData) {
|
if (!otpData) {
|
||||||
throw new Exception("Invalid code", HttpStatus.BAD_REQUEST);
|
throw new OTPError("Invalid code");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (otpData.expires_at < new Date()) {
|
if (otpData.expires_at < new Date()) {
|
||||||
throw new Exception("Code expired", HttpStatus.GONE);
|
throw new OTPError("Code expired");
|
||||||
}
|
}
|
||||||
|
|
||||||
return otpData;
|
return otpData;
|
||||||
@@ -298,27 +312,15 @@ async function invalidateAllUserCodes(app: App, entityName: string, email: strin
|
|||||||
function registerListeners(app: App, entityName: string) {
|
function registerListeners(app: App, entityName: string) {
|
||||||
app.emgr.onAny(
|
app.emgr.onAny(
|
||||||
(event) => {
|
(event) => {
|
||||||
let allowed = true;
|
if (event instanceof DatabaseEvents.MutatorInsertBefore || event instanceof DatabaseEvents.MutatorUpdateBefore) {
|
||||||
let action = "";
|
|
||||||
if (event instanceof DatabaseEvents.MutatorInsertBefore) {
|
|
||||||
if (event.params.entity.name === entityName) {
|
if (event.params.entity.name === entityName) {
|
||||||
allowed = false;
|
throw new OTPError("Mutations of the OTP entity are not allowed");
|
||||||
action = "create";
|
|
||||||
}
|
}
|
||||||
} 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",
|
mode: "sync",
|
||||||
id: "bknd-otp",
|
id: "bknd-email-otp",
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -8,4 +8,4 @@ export { syncConfig, type SyncConfigOptions } from "./dev/sync-config.plugin";
|
|||||||
export { syncTypes, type SyncTypesOptions } from "./dev/sync-types.plugin";
|
export { syncTypes, type SyncTypesOptions } from "./dev/sync-types.plugin";
|
||||||
export { syncSecrets, type SyncSecretsOptions } from "./dev/sync-secrets.plugin";
|
export { syncSecrets, type SyncSecretsOptions } from "./dev/sync-secrets.plugin";
|
||||||
export { timestamps, type TimestampsPluginOptions } from "./data/timestamps.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";
|
||||||
|
|||||||
Reference in New Issue
Block a user