Add support for OTPs and enhance sensitive apis with OTP verification (#191)
One-Time Password (OTP) package added with comprehensive token management, including OTP verification for team account deletion and ownership transfer.
This commit is contained in:
committed by
GitHub
parent
20f7fd2c22
commit
d31f3eb993
@@ -8,6 +8,7 @@ export class Mailbox {
|
||||
email: string,
|
||||
params: {
|
||||
deleteAfter: boolean;
|
||||
subject?: string;
|
||||
},
|
||||
) {
|
||||
const mailbox = email.split('@')[0];
|
||||
@@ -18,13 +19,17 @@ export class Mailbox {
|
||||
throw new Error('Invalid email');
|
||||
}
|
||||
|
||||
const json = await this.getInviteEmail(mailbox, params);
|
||||
const json = await this.getEmail(mailbox, params);
|
||||
|
||||
if (!json?.body) {
|
||||
throw new Error('Email body was not found');
|
||||
}
|
||||
|
||||
console.log('Email found');
|
||||
console.log(`Email found for ${email}`, {
|
||||
id: json.id,
|
||||
subject: json.subject,
|
||||
date: json.date,
|
||||
});
|
||||
|
||||
const html = (json.body as { html: string }).html;
|
||||
const el = parse(html);
|
||||
@@ -40,10 +45,49 @@ export class Mailbox {
|
||||
return this.page.goto(linkHref);
|
||||
}
|
||||
|
||||
async getInviteEmail(
|
||||
/**
|
||||
* Retrieves an OTP code from an email
|
||||
* @param email The email address to check for the OTP
|
||||
* @param deleteAfter Whether to delete the email after retrieving the OTP
|
||||
* @returns The OTP code
|
||||
*/
|
||||
async getOtpFromEmail(email: string, deleteAfter: boolean = true) {
|
||||
const mailbox = email.split('@')[0];
|
||||
|
||||
console.log(`Retrieving OTP from mailbox ${email} ...`);
|
||||
|
||||
if (!mailbox) {
|
||||
throw new Error('Invalid email');
|
||||
}
|
||||
|
||||
const json = await this.getEmail(mailbox, {
|
||||
deleteAfter,
|
||||
subject: `One-time password for Makerkit`,
|
||||
});
|
||||
|
||||
if (!json?.body) {
|
||||
throw new Error('Email body was not found');
|
||||
}
|
||||
|
||||
const html = (json.body as { html: string }).html;
|
||||
|
||||
const text = html.match(
|
||||
new RegExp(`Your one-time password is: (\\d{6})`),
|
||||
)?.[1];
|
||||
|
||||
if (text) {
|
||||
console.log(`OTP code found in text: ${text}`);
|
||||
return text;
|
||||
}
|
||||
|
||||
throw new Error('Could not find OTP code in email');
|
||||
}
|
||||
|
||||
async getEmail(
|
||||
mailbox: string,
|
||||
params: {
|
||||
deleteAfter: boolean;
|
||||
subject?: string;
|
||||
},
|
||||
) {
|
||||
const url = `http://127.0.0.1:54324/api/v1/mailbox/${mailbox}`;
|
||||
@@ -54,13 +98,34 @@ export class Mailbox {
|
||||
throw new Error(`Failed to fetch emails: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const json = (await response.json()) as Array<{ id: string }>;
|
||||
const json = (await response.json()) as Array<{
|
||||
id: string;
|
||||
subject: string;
|
||||
}>;
|
||||
|
||||
if (!json || !json.length) {
|
||||
console.log(`No emails found for mailbox ${mailbox}`);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const messageId = json[0]?.id;
|
||||
const message = params.subject
|
||||
? (() => {
|
||||
const filtered = json.filter(
|
||||
(item) => item.subject === params.subject,
|
||||
);
|
||||
|
||||
console.log(
|
||||
`Found ${filtered.length} emails with subject ${params.subject}`,
|
||||
);
|
||||
|
||||
return filtered[filtered.length - 1];
|
||||
})()
|
||||
: json[0];
|
||||
|
||||
console.log(`Message: ${JSON.stringify(message)}`);
|
||||
|
||||
const messageId = message?.id;
|
||||
const messageUrl = `${url}/${messageId}`;
|
||||
|
||||
const messageResponse = await fetch(messageUrl);
|
||||
|
||||
63
apps/e2e/tests/utils/otp.po.ts
Normal file
63
apps/e2e/tests/utils/otp.po.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import { Page, expect } from '@playwright/test';
|
||||
|
||||
import { Mailbox } from './mailbox';
|
||||
|
||||
export class OtpPo {
|
||||
private readonly page: Page;
|
||||
private readonly mailbox: Mailbox;
|
||||
|
||||
constructor(page: Page) {
|
||||
this.page = page;
|
||||
this.mailbox = new Mailbox(page);
|
||||
}
|
||||
|
||||
/**
|
||||
* Completes the OTP verification process
|
||||
* @param email The email address to send the OTP to
|
||||
*/
|
||||
async completeOtpVerification(email: string) {
|
||||
// Click the "Send Verification Code" button
|
||||
await this.page.click('[data-test="otp-send-verification-button"]');
|
||||
|
||||
// wait for the OTP to be sent
|
||||
await this.page.waitForTimeout(500);
|
||||
|
||||
await expect(async () => {
|
||||
// Get the OTP code from the email
|
||||
const otpCode = await this.getOtpCodeFromEmail(email);
|
||||
|
||||
expect(otpCode).not.toBeNull();
|
||||
|
||||
// Enter the OTP code
|
||||
await this.enterOtpCode(otpCode);
|
||||
}).toPass();
|
||||
|
||||
// Click the "Verify Code" button
|
||||
await this.page.click('[data-test="otp-verify-button"]');
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the OTP code from an email
|
||||
* @param email The email address to check for the OTP
|
||||
* @returns The OTP code
|
||||
*/
|
||||
async getOtpCodeFromEmail(email: string) {
|
||||
// Get the OTP from the email
|
||||
const otpCode = await this.mailbox.getOtpFromEmail(email);
|
||||
|
||||
if (!otpCode) {
|
||||
throw new Error('Failed to retrieve OTP code from email');
|
||||
}
|
||||
|
||||
return otpCode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enters the OTP code into the input fields
|
||||
* @param otpCode The 6-digit OTP code
|
||||
*/
|
||||
async enterOtpCode(otpCode: string) {
|
||||
console.log(`Entering OTP code: ${otpCode}`);
|
||||
await this.page.fill('[data-input-otp]', otpCode);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user