2022-03-17 22:58:08 +05:00
|
|
|
import "../types";
|
2020-12-16 12:06:25 +05:00
|
|
|
import http from "../utils/http";
|
|
|
|
|
import constants from "../utils/constants";
|
|
|
|
|
import TokenManager from "./token-manager";
|
2022-03-30 15:52:48 +05:00
|
|
|
import { EV, EVENTS, setUserPersonalizationBytes } from "../common";
|
2020-12-16 12:06:25 +05:00
|
|
|
|
|
|
|
|
const ENDPOINTS = {
|
|
|
|
|
signup: "/users",
|
|
|
|
|
token: "/connect/token",
|
|
|
|
|
user: "/users",
|
|
|
|
|
deleteUser: "/users/delete",
|
|
|
|
|
patchUser: "/account",
|
2020-12-16 14:56:09 +05:00
|
|
|
verifyUser: "/account/verify",
|
2020-12-16 12:06:25 +05:00
|
|
|
revoke: "/connect/revocation",
|
2020-12-22 09:36:35 +05:00
|
|
|
recoverAccount: "/account/recover",
|
2022-02-17 21:22:22 +05:00
|
|
|
activateTrial: "/subscriptions/trial",
|
2020-12-16 12:06:25 +05:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
class UserManager {
|
|
|
|
|
/**
|
|
|
|
|
*
|
2021-09-26 11:47:13 +05:00
|
|
|
* @param {import("../database/storage").default} storage
|
2021-12-31 09:05:45 +05:00
|
|
|
* @param {import("../api/index").default} db
|
2020-12-16 12:06:25 +05:00
|
|
|
*/
|
2021-12-31 09:05:45 +05:00
|
|
|
constructor(storage, db) {
|
2021-09-26 11:47:13 +05:00
|
|
|
this._storage = storage;
|
2021-12-31 09:05:45 +05:00
|
|
|
this._db = db;
|
2021-09-26 11:47:13 +05:00
|
|
|
this.tokenManager = new TokenManager(storage);
|
2022-01-03 13:26:57 +05:00
|
|
|
|
2022-01-07 13:58:06 +05:00
|
|
|
EV.subscribe(EVENTS.userUnauthorized, async (url) => {
|
|
|
|
|
if (url.includes("/connect/token")) return;
|
|
|
|
|
try {
|
|
|
|
|
await this.tokenManager._refreshToken(true);
|
|
|
|
|
} catch (e) {
|
|
|
|
|
await this.logout(
|
|
|
|
|
false,
|
|
|
|
|
`Your token has been revoked. Error: ${e.message}.`
|
|
|
|
|
);
|
|
|
|
|
}
|
2022-01-03 13:26:57 +05:00
|
|
|
});
|
2020-12-16 12:06:25 +05:00
|
|
|
}
|
|
|
|
|
|
2020-12-22 16:13:01 +05:00
|
|
|
async init() {
|
|
|
|
|
const user = await this.getUser();
|
|
|
|
|
if (!user) return;
|
2021-06-04 09:32:13 +05:00
|
|
|
setUserPersonalizationBytes(user.salt);
|
2020-12-22 16:13:01 +05:00
|
|
|
}
|
|
|
|
|
|
2020-12-16 12:06:25 +05:00
|
|
|
async signup(email, password) {
|
2021-09-26 11:47:13 +05:00
|
|
|
const hashedPassword = await this._storage.hash(password, email);
|
2020-12-16 12:06:25 +05:00
|
|
|
await http.post(`${constants.API_HOST}${ENDPOINTS.signup}`, {
|
|
|
|
|
email,
|
2021-01-18 23:29:49 +05:00
|
|
|
password: hashedPassword,
|
2020-12-22 09:55:31 +05:00
|
|
|
client_id: "notesnook",
|
2020-12-16 12:06:25 +05:00
|
|
|
});
|
2021-02-26 17:33:46 +05:00
|
|
|
EV.publish(EVENTS.userSignedUp);
|
2022-03-17 22:58:08 +05:00
|
|
|
return await this._login({ email, password, hashedPassword });
|
2021-01-18 23:29:49 +05:00
|
|
|
}
|
|
|
|
|
|
2022-03-17 22:58:08 +05:00
|
|
|
async login(email, password) {
|
|
|
|
|
return this._login({ email, password });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async mfaLogin(email, password, { code, method }) {
|
|
|
|
|
return this._login({ email, password, code, method });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @private
|
|
|
|
|
*/
|
|
|
|
|
async _login({ email, password, hashedPassword, code, method }) {
|
2021-04-09 12:55:32 +05:00
|
|
|
if (!hashedPassword) {
|
2021-09-26 11:47:13 +05:00
|
|
|
hashedPassword = await this._storage.hash(password, email);
|
2021-04-09 12:55:32 +05:00
|
|
|
}
|
|
|
|
|
|
2020-12-16 12:06:25 +05:00
|
|
|
await this.tokenManager.saveToken(
|
|
|
|
|
await http.post(`${constants.AUTH_HOST}${ENDPOINTS.token}`, {
|
|
|
|
|
username: email,
|
2021-01-19 09:48:42 +05:00
|
|
|
password: hashedPassword,
|
2020-12-16 12:06:25 +05:00
|
|
|
grant_type: "password",
|
2021-06-15 10:17:42 +05:00
|
|
|
scope: "notesnook.sync offline_access openid IdentityServerApi",
|
2020-12-16 12:06:25 +05:00
|
|
|
client_id: "notesnook",
|
2022-03-17 22:58:08 +05:00
|
|
|
"mfa:code": code,
|
|
|
|
|
"mfa:method": method,
|
2020-12-16 12:06:25 +05:00
|
|
|
})
|
|
|
|
|
);
|
|
|
|
|
|
2021-05-25 16:19:58 +05:00
|
|
|
const user = await this.fetchUser();
|
2021-06-04 09:32:13 +05:00
|
|
|
setUserPersonalizationBytes(user.salt);
|
2021-09-26 11:47:13 +05:00
|
|
|
await this._storage.deriveCryptoKey(`_uk_@${user.email}`, {
|
2020-12-16 12:06:25 +05:00
|
|
|
password,
|
|
|
|
|
salt: user.salt,
|
|
|
|
|
});
|
|
|
|
|
|
2021-01-23 10:48:21 +05:00
|
|
|
EV.publish(EVENTS.userLoggedIn, user);
|
2020-12-16 12:06:25 +05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async getSessions() {
|
|
|
|
|
const token = await this.tokenManager.getAccessToken();
|
|
|
|
|
if (!token) return;
|
|
|
|
|
await http.get(`${constants.AUTH_HOST}/account/sessions`, token);
|
|
|
|
|
}
|
|
|
|
|
|
2021-12-31 09:06:12 +05:00
|
|
|
async clearSessions(all = false) {
|
|
|
|
|
const token = await this.tokenManager.getToken();
|
|
|
|
|
if (!token) return;
|
|
|
|
|
const { access_token, refresh_token } = token;
|
|
|
|
|
await http.post(
|
|
|
|
|
`${constants.AUTH_HOST}/account/sessions/clear?all=${all}`,
|
|
|
|
|
{ refresh_token },
|
|
|
|
|
access_token
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2022-02-17 21:22:22 +05:00
|
|
|
async activateTrial() {
|
|
|
|
|
const token = await this.tokenManager.getAccessToken();
|
|
|
|
|
if (!token) return false;
|
|
|
|
|
await http.post(
|
|
|
|
|
`${constants.SUBSCRIPTIONS_HOST}${ENDPOINTS.activateTrial}`,
|
|
|
|
|
null,
|
|
|
|
|
token
|
|
|
|
|
);
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
2020-12-16 12:06:25 +05:00
|
|
|
async logout(revoke = true, reason) {
|
2020-12-22 16:13:01 +05:00
|
|
|
try {
|
|
|
|
|
if (revoke) await this.tokenManager.revokeToken();
|
|
|
|
|
} catch (e) {
|
|
|
|
|
console.error(e);
|
|
|
|
|
} finally {
|
2021-09-26 11:47:13 +05:00
|
|
|
await this._storage.clear();
|
2021-01-23 10:48:21 +05:00
|
|
|
EV.publish(EVENTS.userLoggedOut, reason);
|
2021-07-03 23:07:54 +05:00
|
|
|
EV.publish(EVENTS.appRefreshRequested);
|
2020-12-22 16:13:01 +05:00
|
|
|
}
|
2020-12-16 12:06:25 +05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
setUser(user) {
|
|
|
|
|
if (!user) return;
|
2021-09-26 11:47:13 +05:00
|
|
|
return this._storage.write("user", user);
|
2020-12-16 12:06:25 +05:00
|
|
|
}
|
|
|
|
|
|
2022-03-17 22:58:08 +05:00
|
|
|
/**
|
|
|
|
|
*
|
|
|
|
|
* @returns {Promise<User>}
|
|
|
|
|
*/
|
2020-12-16 12:06:25 +05:00
|
|
|
getUser() {
|
2021-09-26 11:47:13 +05:00
|
|
|
return this._storage.read("user");
|
2020-12-16 12:06:25 +05:00
|
|
|
}
|
|
|
|
|
|
2021-12-31 09:05:45 +05:00
|
|
|
async updateUser(user) {
|
|
|
|
|
if (!user) return;
|
|
|
|
|
|
|
|
|
|
let token = await this.tokenManager.getAccessToken();
|
|
|
|
|
await http.patch.json(
|
|
|
|
|
`${constants.API_HOST}${ENDPOINTS.user}`,
|
|
|
|
|
user,
|
|
|
|
|
token
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
await this.setUser(user);
|
|
|
|
|
}
|
|
|
|
|
|
2020-12-16 12:06:25 +05:00
|
|
|
async deleteUser(password) {
|
|
|
|
|
let token = await this.tokenManager.getAccessToken();
|
|
|
|
|
if (!token) return;
|
2021-01-19 09:48:42 +05:00
|
|
|
const user = await this.getUser();
|
2020-12-16 12:06:25 +05:00
|
|
|
await http.post(
|
|
|
|
|
`${constants.API_HOST}${ENDPOINTS.deleteUser}`,
|
2021-09-26 11:47:13 +05:00
|
|
|
{ password: await this._storage.hash(password, user.email) },
|
2020-12-16 12:06:25 +05:00
|
|
|
token
|
|
|
|
|
);
|
|
|
|
|
await this.logout(false, "Account deleted.");
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
2022-03-17 22:58:08 +05:00
|
|
|
/**
|
|
|
|
|
*
|
|
|
|
|
* @returns {Promise<User>}
|
|
|
|
|
*/
|
2021-05-25 16:19:58 +05:00
|
|
|
async fetchUser() {
|
2020-12-16 12:06:25 +05:00
|
|
|
try {
|
|
|
|
|
let token = await this.tokenManager.getAccessToken();
|
|
|
|
|
if (!token) return;
|
|
|
|
|
const user = await http.get(
|
|
|
|
|
`${constants.API_HOST}${ENDPOINTS.user}`,
|
|
|
|
|
token
|
|
|
|
|
);
|
|
|
|
|
if (user) {
|
2021-05-25 16:19:58 +05:00
|
|
|
await this.setUser(user);
|
2021-01-23 10:48:21 +05:00
|
|
|
EV.publish(EVENTS.userFetched, user);
|
2020-12-16 12:06:25 +05:00
|
|
|
return user;
|
|
|
|
|
} else {
|
2020-12-16 14:41:28 +05:00
|
|
|
return await this.getUser();
|
2020-12-16 12:06:25 +05:00
|
|
|
}
|
2021-01-22 22:08:56 +05:00
|
|
|
} catch (e) {
|
|
|
|
|
console.error(e);
|
|
|
|
|
return await this.getUser();
|
2020-12-16 12:06:25 +05:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2020-12-22 13:13:18 +05:00
|
|
|
changePassword(oldPassword, newPassword) {
|
|
|
|
|
return this._updatePassword("change_password", {
|
|
|
|
|
old_password: oldPassword,
|
|
|
|
|
new_password: newPassword,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
resetPassword(newPassword) {
|
|
|
|
|
return this._updatePassword("reset_password", {
|
|
|
|
|
new_password: newPassword,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2020-12-16 12:06:25 +05:00
|
|
|
async getEncryptionKey() {
|
|
|
|
|
const user = await this.getUser();
|
|
|
|
|
if (!user) return;
|
2021-09-26 11:47:13 +05:00
|
|
|
const key = await this._storage.getCryptoKey(`_uk_@${user.email}`);
|
2020-12-16 12:06:25 +05:00
|
|
|
return { key, salt: user.salt };
|
|
|
|
|
}
|
|
|
|
|
|
2021-10-25 11:35:00 +05:00
|
|
|
async getAttachmentsKey() {
|
2021-10-26 09:13:18 +05:00
|
|
|
try {
|
|
|
|
|
let user = await this.getUser();
|
|
|
|
|
if (!user) return;
|
2021-10-25 11:35:00 +05:00
|
|
|
|
2021-10-26 09:13:18 +05:00
|
|
|
if (!user.attachmentsKey) {
|
|
|
|
|
let token = await this.tokenManager.getAccessToken();
|
|
|
|
|
user = await http.get(`${constants.API_HOST}${ENDPOINTS.user}`, token);
|
|
|
|
|
}
|
2021-10-25 11:35:00 +05:00
|
|
|
|
2021-10-26 09:13:18 +05:00
|
|
|
const userEncryptionKey = await this.getEncryptionKey();
|
|
|
|
|
if (!userEncryptionKey) return;
|
2021-10-25 11:35:00 +05:00
|
|
|
|
2021-10-26 09:13:18 +05:00
|
|
|
if (!user.attachmentsKey) {
|
|
|
|
|
const key = await this._storage.generateRandomKey();
|
2021-12-31 09:05:45 +05:00
|
|
|
user.attachmentsKey = await this._storage.encrypt(
|
2021-10-26 09:13:18 +05:00
|
|
|
userEncryptionKey,
|
|
|
|
|
JSON.stringify(key)
|
|
|
|
|
);
|
2021-10-25 11:35:00 +05:00
|
|
|
|
2021-12-31 09:05:45 +05:00
|
|
|
await this.updateUser(user);
|
2021-10-26 09:13:18 +05:00
|
|
|
return key;
|
|
|
|
|
}
|
2021-10-25 11:35:00 +05:00
|
|
|
|
2021-10-26 09:13:18 +05:00
|
|
|
const plainData = await this._storage.decrypt(
|
|
|
|
|
userEncryptionKey,
|
|
|
|
|
user.attachmentsKey
|
|
|
|
|
);
|
|
|
|
|
return JSON.parse(plainData);
|
|
|
|
|
} catch (e) {
|
2021-10-28 22:03:06 +05:00
|
|
|
throw new Error(
|
|
|
|
|
`Could not get attachments encryption key. Please make sure you have Internet access. Error: ${e.message}`
|
|
|
|
|
);
|
2021-10-26 09:13:18 +05:00
|
|
|
}
|
2021-10-25 11:35:00 +05:00
|
|
|
}
|
|
|
|
|
|
2020-12-22 13:13:18 +05:00
|
|
|
async sendVerificationEmail() {
|
|
|
|
|
let token = await this.tokenManager.getAccessToken();
|
|
|
|
|
if (!token) return;
|
|
|
|
|
await http.post(
|
|
|
|
|
`${constants.AUTH_HOST}${ENDPOINTS.verifyUser}`,
|
|
|
|
|
null,
|
|
|
|
|
token
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
recoverAccount(email) {
|
|
|
|
|
return http.post(`${constants.AUTH_HOST}${ENDPOINTS.recoverAccount}`, {
|
|
|
|
|
email,
|
|
|
|
|
client_id: "notesnook",
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2021-06-01 09:46:50 +05:00
|
|
|
async verifyPassword(password) {
|
|
|
|
|
try {
|
|
|
|
|
const user = await this.getUser();
|
|
|
|
|
if (!user) return false;
|
|
|
|
|
const key = await this.getEncryptionKey();
|
2021-09-26 11:47:13 +05:00
|
|
|
const cipher = await this._storage.encrypt(key, "notesnook");
|
|
|
|
|
const plainText = await this._storage.decrypt({ password }, cipher);
|
2021-06-01 09:46:50 +05:00
|
|
|
return plainText === "notesnook";
|
|
|
|
|
} catch (e) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2020-12-22 13:13:18 +05:00
|
|
|
async _updatePassword(type, data) {
|
2022-01-01 16:11:55 +05:00
|
|
|
const token = await this.tokenManager.getAccessToken();
|
|
|
|
|
const user = await this.getUser();
|
|
|
|
|
if (!token || !user) throw new Error("You are not logged in.");
|
2021-01-19 09:48:42 +05:00
|
|
|
|
2022-01-01 16:11:55 +05:00
|
|
|
const { email, salt } = user;
|
2021-12-31 09:05:45 +05:00
|
|
|
|
2022-01-01 16:11:55 +05:00
|
|
|
let { new_password, old_password } = data;
|
2021-12-31 09:05:45 +05:00
|
|
|
if (old_password && !(await this.verifyPassword(old_password)))
|
|
|
|
|
throw new Error("Incorrect old password.");
|
|
|
|
|
|
2022-01-01 16:11:55 +05:00
|
|
|
if (!new_password) throw new Error("New password is required.");
|
|
|
|
|
|
2021-12-31 09:05:45 +05:00
|
|
|
const attachmentsKey = await this.getAttachmentsKey();
|
2022-01-03 13:26:57 +05:00
|
|
|
data.encryptionKey = data.encryptionKey || (await this.getEncryptionKey());
|
2021-12-31 09:05:45 +05:00
|
|
|
|
|
|
|
|
await this._db.outbox.add(type, data, async () => {
|
|
|
|
|
await this._db.sync(true, true);
|
|
|
|
|
|
|
|
|
|
await this._storage.deriveCryptoKey(`_uk_@${email}`, {
|
2022-01-01 16:11:55 +05:00
|
|
|
password: new_password,
|
2021-12-31 09:05:45 +05:00
|
|
|
salt,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (attachmentsKey) {
|
|
|
|
|
const userEncryptionKey = await this.getEncryptionKey();
|
|
|
|
|
if (!userEncryptionKey) return;
|
|
|
|
|
user.attachmentsKey = await this._storage.encrypt(
|
|
|
|
|
userEncryptionKey,
|
|
|
|
|
JSON.stringify(attachmentsKey)
|
|
|
|
|
);
|
|
|
|
|
await this.updateUser(user);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
await this._db.sync(false, true);
|
|
|
|
|
|
2022-01-01 16:11:55 +05:00
|
|
|
if (old_password)
|
|
|
|
|
old_password = await this._storage.hash(old_password, email);
|
|
|
|
|
if (new_password)
|
|
|
|
|
new_password = await this._storage.hash(new_password, email);
|
|
|
|
|
|
2021-12-31 09:05:45 +05:00
|
|
|
await http.patch(
|
|
|
|
|
`${constants.AUTH_HOST}${ENDPOINTS.patchUser}`,
|
|
|
|
|
{
|
|
|
|
|
type,
|
2022-01-01 16:11:55 +05:00
|
|
|
old_password,
|
|
|
|
|
new_password,
|
2021-12-31 09:05:45 +05:00
|
|
|
},
|
|
|
|
|
token
|
2021-01-19 09:48:42 +05:00
|
|
|
);
|
2021-12-31 09:05:45 +05:00
|
|
|
});
|
|
|
|
|
|
2020-12-16 12:06:25 +05:00
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export default UserManager;
|