mirror of
https://github.com/shishantbiswas/bknd.git
synced 2026-03-15 20:17:22 +00:00
otp: add entity config, reduce result
This commit is contained in:
@@ -1,6 +1,10 @@
|
||||
import { describe, expect, mock, test } from "bun:test";
|
||||
import { afterAll, beforeAll, describe, expect, mock, test } from "bun:test";
|
||||
import { otp } from "./otp.plugin";
|
||||
import { createApp } from "core/test/utils";
|
||||
import { disableConsoleLog, enableConsoleLog } from "core/utils/test";
|
||||
|
||||
beforeAll(disableConsoleLog);
|
||||
afterAll(enableConsoleLog);
|
||||
|
||||
describe("otp plugin", () => {
|
||||
test("should not work if auth is not enabled", async () => {
|
||||
@@ -20,6 +24,42 @@ describe("otp plugin", () => {
|
||||
expect(res.status).toBe(404);
|
||||
});
|
||||
|
||||
test("should prevent mutations of the OTP entity", async () => {
|
||||
const app = createApp({
|
||||
config: {
|
||||
auth: {
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
options: {
|
||||
drivers: {
|
||||
email: {
|
||||
send: async () => {},
|
||||
},
|
||||
},
|
||||
plugins: [otp({ showActualErrors: true })],
|
||||
},
|
||||
});
|
||||
await app.build();
|
||||
|
||||
const payload = {
|
||||
email: "test@test.com",
|
||||
code: "123456",
|
||||
action: "login",
|
||||
created_at: new Date(),
|
||||
expires_at: new Date(Date.now() + 1000 * 60 * 60 * 24),
|
||||
used_at: null,
|
||||
};
|
||||
|
||||
expect(app.em.mutator("users_otp").insertOne(payload)).rejects.toThrow();
|
||||
expect(
|
||||
await app
|
||||
.getApi()
|
||||
.data.createOne("users_otp", payload)
|
||||
.then((r) => r.ok),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
test("should generate a token", async () => {
|
||||
const called = mock(() => null);
|
||||
const app = createApp({
|
||||
@@ -53,17 +93,23 @@ describe("otp plugin", () => {
|
||||
body: JSON.stringify({ email: "test@test.com" }),
|
||||
});
|
||||
expect(res.status).toBe(201);
|
||||
const data = (await res.json()) as any;
|
||||
expect(data.data.code).toBeDefined();
|
||||
expect(data.data.code.length).toBe(6);
|
||||
expect(data.data.code.split("").every((char: string) => Number.isInteger(Number(char)))).toBe(
|
||||
expect(await res.json()).toEqual({ sent: true, action: "login" } as any);
|
||||
|
||||
const { data } = await app
|
||||
.getApi()
|
||||
.data.readOneBy("users_otp", { where: { email: "test@test.com" } });
|
||||
expect(data?.code).toBeDefined();
|
||||
expect(data?.code?.length).toBe(6);
|
||||
expect(data?.code?.split("").every((char: string) => Number.isInteger(Number(char)))).toBe(
|
||||
true,
|
||||
);
|
||||
expect(data.data.email).toBe("test@test.com");
|
||||
expect(data?.email).toBe("test@test.com");
|
||||
expect(called).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("should login with a code", async () => {
|
||||
let code = "";
|
||||
|
||||
const app = createApp({
|
||||
config: {
|
||||
auth: {
|
||||
@@ -74,10 +120,18 @@ describe("otp plugin", () => {
|
||||
},
|
||||
},
|
||||
options: {
|
||||
plugins: [otp({ showActualErrors: true })],
|
||||
plugins: [
|
||||
otp({
|
||||
showActualErrors: true,
|
||||
generateEmail: (otp) => ({ subject: "test", body: otp.code }),
|
||||
}),
|
||||
],
|
||||
drivers: {
|
||||
email: {
|
||||
send: async () => {},
|
||||
send: async (to, subject, body) => {
|
||||
expect(to).toBe("test@test.com");
|
||||
code = String(body);
|
||||
},
|
||||
},
|
||||
},
|
||||
seed: async (ctx) => {
|
||||
@@ -87,14 +141,13 @@ describe("otp plugin", () => {
|
||||
});
|
||||
await app.build();
|
||||
|
||||
const res = await app.server.request("/api/auth/otp/login", {
|
||||
await app.server.request("/api/auth/otp/login", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({ email: "test@test.com" }),
|
||||
});
|
||||
const data = (await res.json()) as any;
|
||||
|
||||
{
|
||||
const res = await app.server.request("/api/auth/otp/login", {
|
||||
@@ -102,7 +155,7 @@ describe("otp plugin", () => {
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({ email: "test@test.com", code: data.data.code }),
|
||||
body: JSON.stringify({ email: "test@test.com", code }),
|
||||
});
|
||||
expect(res.status).toBe(200);
|
||||
expect(res.headers.get("set-cookie")).toBeDefined();
|
||||
@@ -113,6 +166,8 @@ describe("otp plugin", () => {
|
||||
});
|
||||
|
||||
test("should register with a code", async () => {
|
||||
let code = "";
|
||||
|
||||
const app = createApp({
|
||||
config: {
|
||||
auth: {
|
||||
@@ -123,10 +178,18 @@ describe("otp plugin", () => {
|
||||
},
|
||||
},
|
||||
options: {
|
||||
plugins: [otp({ showActualErrors: true })],
|
||||
plugins: [
|
||||
otp({
|
||||
showActualErrors: true,
|
||||
generateEmail: (otp) => ({ subject: "test", body: otp.code }),
|
||||
}),
|
||||
],
|
||||
drivers: {
|
||||
email: {
|
||||
send: async () => {},
|
||||
send: async (to, subject, body) => {
|
||||
expect(to).toBe("test@test.com");
|
||||
code = String(body);
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -140,7 +203,7 @@ describe("otp plugin", () => {
|
||||
},
|
||||
body: JSON.stringify({ email: "test@test.com" }),
|
||||
});
|
||||
const data = (await res.json()) as any;
|
||||
expect(await res.json()).toEqual({ sent: true, action: "register" } as any);
|
||||
|
||||
{
|
||||
const res = await app.server.request("/api/auth/otp/register", {
|
||||
@@ -148,7 +211,7 @@ describe("otp plugin", () => {
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({ email: "test@test.com", code: data.data.code }),
|
||||
body: JSON.stringify({ email: "test@test.com", code }),
|
||||
});
|
||||
expect(res.status).toBe(200);
|
||||
expect(res.headers.get("set-cookie")).toBeDefined();
|
||||
@@ -157,4 +220,8 @@ describe("otp plugin", () => {
|
||||
expect(userData.token).toBeDefined();
|
||||
}
|
||||
});
|
||||
|
||||
// @todo: test invalid codes
|
||||
// @todo: test codes with different actions
|
||||
// @todo: test code expiration
|
||||
});
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
type DB,
|
||||
type FieldSchema,
|
||||
type MaybePromise,
|
||||
type EntityConfig,
|
||||
} from "bknd";
|
||||
import { invariant, s, jsc, HttpStatus, threwAsync, randomString } from "bknd/utils";
|
||||
import { MutatorDeleteBefore, MutatorInsertBefore, MutatorUpdateBefore } from "data/events";
|
||||
@@ -39,6 +40,11 @@ export type OtpPluginOptions = {
|
||||
*/
|
||||
entity?: string;
|
||||
|
||||
/**
|
||||
* The config for the OTP entity.
|
||||
*/
|
||||
entityConfig?: EntityConfig;
|
||||
|
||||
/**
|
||||
* Customize email content. If not provided, a default email will be sent.
|
||||
*/
|
||||
@@ -71,6 +77,7 @@ export function otp({
|
||||
apiBasePath = "/api/auth/otp",
|
||||
ttl = 600,
|
||||
entity: entityName = "users_otp",
|
||||
entityConfig,
|
||||
generateEmail: _generateEmail,
|
||||
showActualErrors = false,
|
||||
}: OtpPluginOptions = {}): AppPlugin {
|
||||
@@ -80,7 +87,15 @@ export function otp({
|
||||
schema: () =>
|
||||
em(
|
||||
{
|
||||
[entityName]: entity(entityName, otpFields),
|
||||
[entityName]: entity(
|
||||
entityName,
|
||||
otpFields,
|
||||
entityConfig ?? {
|
||||
name: "Users OTP",
|
||||
sort_dir: "desc",
|
||||
},
|
||||
"generated",
|
||||
),
|
||||
},
|
||||
({ index }, schema) => {
|
||||
const otp = schema[entityName]!;
|
||||
@@ -131,14 +146,14 @@ export function otp({
|
||||
// @ts-expect-error private method
|
||||
return auth.authenticator.respondWithUser(c, { user, token: jwt });
|
||||
} else {
|
||||
const otpData = await generateAndSendCode(
|
||||
await generateAndSendCode(
|
||||
app,
|
||||
{ generateCode, generateEmail, ttl, entity: entityName },
|
||||
user,
|
||||
"login",
|
||||
);
|
||||
|
||||
return c.json({ data: otpData }, HttpStatus.CREATED);
|
||||
return c.json({ sent: true, action: "login" }, HttpStatus.CREATED);
|
||||
}
|
||||
},
|
||||
)
|
||||
@@ -178,14 +193,14 @@ export function otp({
|
||||
// @ts-expect-error private method
|
||||
return auth.authenticator.respondWithUser(c, { user, token: jwt });
|
||||
} else {
|
||||
const otpData = await generateAndSendCode(
|
||||
await generateAndSendCode(
|
||||
app,
|
||||
{ generateCode, generateEmail, ttl, entity: entityName },
|
||||
{ email },
|
||||
"register",
|
||||
);
|
||||
|
||||
return c.json({ data: otpData }, HttpStatus.CREATED);
|
||||
return c.json({ sent: true, action: "register" }, HttpStatus.CREATED);
|
||||
}
|
||||
},
|
||||
)
|
||||
@@ -194,7 +209,7 @@ export function otp({
|
||||
throw err;
|
||||
}
|
||||
|
||||
throw new Exception("Invalid code", HttpStatus.BAD_REQUEST);
|
||||
throw new Exception("Invalid credentials", HttpStatus.BAD_REQUEST);
|
||||
});
|
||||
|
||||
app.server.route(apiBasePath, hono);
|
||||
@@ -282,7 +297,7 @@ async function invalidateAllUserCodes(app: App, entityName: string, email: strin
|
||||
|
||||
function registerListeners(app: App, entityName: string) {
|
||||
app.emgr.onAny(
|
||||
async (event) => {
|
||||
(event) => {
|
||||
let allowed = true;
|
||||
let action = "";
|
||||
if (event instanceof MutatorInsertBefore) {
|
||||
|
||||
Reference in New Issue
Block a user