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 { otp } from "./otp.plugin";
|
||||||
import { createApp } from "core/test/utils";
|
import { createApp } from "core/test/utils";
|
||||||
|
import { disableConsoleLog, enableConsoleLog } from "core/utils/test";
|
||||||
|
|
||||||
|
beforeAll(disableConsoleLog);
|
||||||
|
afterAll(enableConsoleLog);
|
||||||
|
|
||||||
describe("otp plugin", () => {
|
describe("otp plugin", () => {
|
||||||
test("should not work if auth is not enabled", async () => {
|
test("should not work if auth is not enabled", async () => {
|
||||||
@@ -20,6 +24,42 @@ describe("otp plugin", () => {
|
|||||||
expect(res.status).toBe(404);
|
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 () => {
|
test("should generate a token", async () => {
|
||||||
const called = mock(() => null);
|
const called = mock(() => null);
|
||||||
const app = createApp({
|
const app = createApp({
|
||||||
@@ -53,17 +93,23 @@ describe("otp plugin", () => {
|
|||||||
body: JSON.stringify({ email: "test@test.com" }),
|
body: JSON.stringify({ email: "test@test.com" }),
|
||||||
});
|
});
|
||||||
expect(res.status).toBe(201);
|
expect(res.status).toBe(201);
|
||||||
const data = (await res.json()) as any;
|
expect(await res.json()).toEqual({ sent: true, action: "login" } as any);
|
||||||
expect(data.data.code).toBeDefined();
|
|
||||||
expect(data.data.code.length).toBe(6);
|
const { data } = await app
|
||||||
expect(data.data.code.split("").every((char: string) => Number.isInteger(Number(char)))).toBe(
|
.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,
|
true,
|
||||||
);
|
);
|
||||||
expect(data.data.email).toBe("test@test.com");
|
expect(data?.email).toBe("test@test.com");
|
||||||
expect(called).toHaveBeenCalled();
|
expect(called).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("should login with a code", async () => {
|
test("should login with a code", async () => {
|
||||||
|
let code = "";
|
||||||
|
|
||||||
const app = createApp({
|
const app = createApp({
|
||||||
config: {
|
config: {
|
||||||
auth: {
|
auth: {
|
||||||
@@ -74,10 +120,18 @@ describe("otp plugin", () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
options: {
|
options: {
|
||||||
plugins: [otp({ showActualErrors: true })],
|
plugins: [
|
||||||
|
otp({
|
||||||
|
showActualErrors: true,
|
||||||
|
generateEmail: (otp) => ({ subject: "test", body: otp.code }),
|
||||||
|
}),
|
||||||
|
],
|
||||||
drivers: {
|
drivers: {
|
||||||
email: {
|
email: {
|
||||||
send: async () => {},
|
send: async (to, subject, body) => {
|
||||||
|
expect(to).toBe("test@test.com");
|
||||||
|
code = String(body);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
seed: async (ctx) => {
|
seed: async (ctx) => {
|
||||||
@@ -87,14 +141,13 @@ describe("otp plugin", () => {
|
|||||||
});
|
});
|
||||||
await app.build();
|
await app.build();
|
||||||
|
|
||||||
const res = await app.server.request("/api/auth/otp/login", {
|
await app.server.request("/api/auth/otp/login", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
},
|
},
|
||||||
body: JSON.stringify({ email: "test@test.com" }),
|
body: JSON.stringify({ email: "test@test.com" }),
|
||||||
});
|
});
|
||||||
const data = (await res.json()) as any;
|
|
||||||
|
|
||||||
{
|
{
|
||||||
const res = await app.server.request("/api/auth/otp/login", {
|
const res = await app.server.request("/api/auth/otp/login", {
|
||||||
@@ -102,7 +155,7 @@ describe("otp plugin", () => {
|
|||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"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.status).toBe(200);
|
||||||
expect(res.headers.get("set-cookie")).toBeDefined();
|
expect(res.headers.get("set-cookie")).toBeDefined();
|
||||||
@@ -113,6 +166,8 @@ describe("otp plugin", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("should register with a code", async () => {
|
test("should register with a code", async () => {
|
||||||
|
let code = "";
|
||||||
|
|
||||||
const app = createApp({
|
const app = createApp({
|
||||||
config: {
|
config: {
|
||||||
auth: {
|
auth: {
|
||||||
@@ -123,10 +178,18 @@ describe("otp plugin", () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
options: {
|
options: {
|
||||||
plugins: [otp({ showActualErrors: true })],
|
plugins: [
|
||||||
|
otp({
|
||||||
|
showActualErrors: true,
|
||||||
|
generateEmail: (otp) => ({ subject: "test", body: otp.code }),
|
||||||
|
}),
|
||||||
|
],
|
||||||
drivers: {
|
drivers: {
|
||||||
email: {
|
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" }),
|
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", {
|
const res = await app.server.request("/api/auth/otp/register", {
|
||||||
@@ -148,7 +211,7 @@ describe("otp plugin", () => {
|
|||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"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.status).toBe(200);
|
||||||
expect(res.headers.get("set-cookie")).toBeDefined();
|
expect(res.headers.get("set-cookie")).toBeDefined();
|
||||||
@@ -157,4 +220,8 @@ describe("otp plugin", () => {
|
|||||||
expect(userData.token).toBeDefined();
|
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 DB,
|
||||||
type FieldSchema,
|
type FieldSchema,
|
||||||
type MaybePromise,
|
type MaybePromise,
|
||||||
|
type EntityConfig,
|
||||||
} from "bknd";
|
} from "bknd";
|
||||||
import { invariant, s, jsc, HttpStatus, threwAsync, randomString } from "bknd/utils";
|
import { invariant, s, jsc, HttpStatus, threwAsync, randomString } from "bknd/utils";
|
||||||
import { MutatorDeleteBefore, MutatorInsertBefore, MutatorUpdateBefore } from "data/events";
|
import { MutatorDeleteBefore, MutatorInsertBefore, MutatorUpdateBefore } from "data/events";
|
||||||
@@ -39,6 +40,11 @@ export type OtpPluginOptions = {
|
|||||||
*/
|
*/
|
||||||
entity?: string;
|
entity?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The config for the OTP entity.
|
||||||
|
*/
|
||||||
|
entityConfig?: EntityConfig;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Customize email content. If not provided, a default email will be sent.
|
* Customize email content. If not provided, a default email will be sent.
|
||||||
*/
|
*/
|
||||||
@@ -71,6 +77,7 @@ export function otp({
|
|||||||
apiBasePath = "/api/auth/otp",
|
apiBasePath = "/api/auth/otp",
|
||||||
ttl = 600,
|
ttl = 600,
|
||||||
entity: entityName = "users_otp",
|
entity: entityName = "users_otp",
|
||||||
|
entityConfig,
|
||||||
generateEmail: _generateEmail,
|
generateEmail: _generateEmail,
|
||||||
showActualErrors = false,
|
showActualErrors = false,
|
||||||
}: OtpPluginOptions = {}): AppPlugin {
|
}: OtpPluginOptions = {}): AppPlugin {
|
||||||
@@ -80,7 +87,15 @@ export function otp({
|
|||||||
schema: () =>
|
schema: () =>
|
||||||
em(
|
em(
|
||||||
{
|
{
|
||||||
[entityName]: entity(entityName, otpFields),
|
[entityName]: entity(
|
||||||
|
entityName,
|
||||||
|
otpFields,
|
||||||
|
entityConfig ?? {
|
||||||
|
name: "Users OTP",
|
||||||
|
sort_dir: "desc",
|
||||||
|
},
|
||||||
|
"generated",
|
||||||
|
),
|
||||||
},
|
},
|
||||||
({ index }, schema) => {
|
({ index }, schema) => {
|
||||||
const otp = schema[entityName]!;
|
const otp = schema[entityName]!;
|
||||||
@@ -131,14 +146,14 @@ export function otp({
|
|||||||
// @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 });
|
||||||
} else {
|
} else {
|
||||||
const otpData = await generateAndSendCode(
|
await generateAndSendCode(
|
||||||
app,
|
app,
|
||||||
{ generateCode, generateEmail, ttl, entity: entityName },
|
{ generateCode, generateEmail, ttl, entity: entityName },
|
||||||
user,
|
user,
|
||||||
"login",
|
"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
|
// @ts-expect-error private method
|
||||||
return auth.authenticator.respondWithUser(c, { user, token: jwt });
|
return auth.authenticator.respondWithUser(c, { user, token: jwt });
|
||||||
} else {
|
} else {
|
||||||
const otpData = await generateAndSendCode(
|
await generateAndSendCode(
|
||||||
app,
|
app,
|
||||||
{ generateCode, generateEmail, ttl, entity: entityName },
|
{ generateCode, generateEmail, ttl, entity: entityName },
|
||||||
{ email },
|
{ email },
|
||||||
"register",
|
"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 err;
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new Exception("Invalid code", HttpStatus.BAD_REQUEST);
|
throw new Exception("Invalid credentials", HttpStatus.BAD_REQUEST);
|
||||||
});
|
});
|
||||||
|
|
||||||
app.server.route(apiBasePath, hono);
|
app.server.route(apiBasePath, hono);
|
||||||
@@ -282,7 +297,7 @@ 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(
|
||||||
async (event) => {
|
(event) => {
|
||||||
let allowed = true;
|
let allowed = true;
|
||||||
let action = "";
|
let action = "";
|
||||||
if (event instanceof MutatorInsertBefore) {
|
if (event instanceof MutatorInsertBefore) {
|
||||||
|
|||||||
Reference in New Issue
Block a user