diff --git a/packages/core/api/index.js b/packages/core/api/index.js index 6b06d5d31..f9d62d295 100644 --- a/packages/core/api/index.js +++ b/packages/core/api/index.js @@ -24,6 +24,7 @@ import Attachments from "../collections/attachments"; import Debug from "./debug"; import { Mutex } from "async-mutex"; import NoteHistory from "../collections/note-history"; +import MFAManager from "./mfa-manager"; /** * @type {EventSource} @@ -89,6 +90,7 @@ class Database { await this._validate(); this.user = new UserManager(this.storage, this); + this.mfa = new MFAManager(this.storage, this); this.syncer = new Sync(this); this.vault = new Vault(this); this.conflicts = new Conflicts(this); diff --git a/packages/core/api/mfa-manager.js b/packages/core/api/mfa-manager.js new file mode 100644 index 000000000..f3d26368b --- /dev/null +++ b/packages/core/api/mfa-manager.js @@ -0,0 +1,61 @@ +import http from "../utils/http"; +import constants from "../utils/constants"; +import TokenManager from "./token-manager"; + +const ENDPOINTS = { + setup: "/mfa/setup", + enable: "/mfa/enable", + disable: "/mfa/disable", + reset: "/mfa/reset", + recoveryCodes: "/mfa/recovery_codes", +}; + +class MFAManager { + /** + * + * @param {import("../database/storage").default} storage + * @param {import("../api/index").default} db + */ + constructor(storage, db) { + this._storage = storage; + this._db = db; + this.tokenManager = new TokenManager(storage); + } + + /** + * + * @param {"app" | "sms" | "email"} type + * @param {string} phoneNumber + * @returns + */ + async setup(type, phoneNumber = undefined) { + const token = await this.tokenManager.getAccessToken(); + if (!token) return; + return await http.post( + `${constants.AUTH_HOST}${ENDPOINTS.setup}`, + { + type, + phoneNumber, + }, + token + ); + } + + /** + * + * @param {"app" | "sms" | "email"} type + * @param {string} code + * @returns + */ + async enable(type, code) { + const token = await this.tokenManager.getAccessToken(); + if (!token) return; + return await http.post( + `${constants.AUTH_HOST}${ENDPOINTS.enable}`, + { type, code }, + token + ); + } +} + +export default MFAManager; diff --git a/packages/core/api/user-manager.js b/packages/core/api/user-manager.js index 653dba6cb..2d836c73c 100644 --- a/packages/core/api/user-manager.js +++ b/packages/core/api/user-manager.js @@ -61,7 +61,7 @@ class UserManager { return await this.login(email, password, hashedPassword); } - async login(email, password, hashedPassword) { + async login(email, password, code, hashedPassword = undefined) { if (!hashedPassword) { hashedPassword = await this._storage.hash(password, email); } @@ -70,6 +70,7 @@ class UserManager { await http.post(`${constants.AUTH_HOST}${ENDPOINTS.token}`, { username: email, password: hashedPassword, + code, grant_type: "password", scope: "notesnook.sync offline_access openid IdentityServerApi", client_id: "notesnook", diff --git a/packages/core/utils/http.js b/packages/core/utils/http.js index aca3b1e8f..d0421ecc3 100644 --- a/packages/core/utils/http.js +++ b/packages/core/utils/http.js @@ -38,9 +38,8 @@ function transformer(data, type) { if (type === "application/json") return JSON.stringify(data); else { return Object.entries(data) - .map( - ([key, value]) => - `${encodeURIComponent(key)}=${encodeURIComponent(value)}` + .map(([key, value]) => + value ? `${encodeURIComponent(key)}=${encodeURIComponent(value)}` : "" ) .join("&"); } @@ -58,7 +57,7 @@ async function handleResponse(response) { if (response.ok) { return json; } - throw new Error(errorTransformer(json)); + throw new RequestError(errorTransformer(json)); } else { if (response.status === 429) throw new Error("You are being rate limited."); @@ -106,26 +105,41 @@ function getAuthorizationHeader(token) { } function errorTransformer(errorJson) { + let errorMessage = "Unknown error."; + let errorCode = "unknown"; + if (!errorJson.error && !errorJson.errors && !errorJson.error_description) - return "Unknown error."; - const { error, error_description, errors } = errorJson; + return { description: errorMessage, code: errorCode, data: {} }; + const { error, error_description, errors, data } = errorJson; if (errors) { - return errors.join("\n"); + errorMessage = errors.join("\n"); } switch (error) { case "invalid_grant": { switch (error_description) { case "invalid_username_or_password": - return "Username or password incorrect."; + errorMessage = "Username or password incorrect."; + errorCode = error_description; + break; default: - return error_description || error; + errorMessage = error_description || error; + errorCode = error || "invalid_grant"; + break; } } default: - return error_description || error; + errorMessage = error_description || "An unknown error occured."; + errorCode = error; + break; } + + return { + description: errorMessage, + code: errorCode, + data: data ? JSON.parse(data) : undefined, + }; } /** @@ -149,26 +163,10 @@ async function fetchWrapped(input, init) { } } -// /** -// * -// * @param {RequestInfo} resource -// * @param {RequestInit} options -// * @returns -// */ -// async function fetchWithTimeout(resource, options = {}) { -// try { -// const { timeout = 8000 } = options; - -// const controller = new AbortController(); -// const id = setTimeout(() => controller.abort(), timeout); -// const response = await fetch(resource, { -// ...options, -// signal: controller.signal, -// }); -// clearTimeout(id); -// return response; -// } catch (e) { -// if (e.name === "AbortError") throw new Error("Request timed out."); -// throw e; -// } -// } +class RequestError extends Error { + constructor(error) { + super(error.description); + this.code = error.code; + this.data = error.data; + } +}