2022-08-31 06:33:37 +05:00
|
|
|
/*
|
|
|
|
|
This file is part of the Notesnook project (https://notesnook.com/)
|
|
|
|
|
|
2023-01-16 13:44:52 +05:00
|
|
|
Copyright (C) 2023 Streetwriters (Private) Limited
|
2022-08-31 06:33:37 +05:00
|
|
|
|
|
|
|
|
This program is free software: you can redistribute it and/or modify
|
|
|
|
|
it under the terms of the GNU General Public License as published by
|
|
|
|
|
the Free Software Foundation, either version 3 of the License, or
|
|
|
|
|
(at your option) any later version.
|
|
|
|
|
|
|
|
|
|
This program is distributed in the hope that it will be useful,
|
|
|
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
|
GNU General Public License for more details.
|
|
|
|
|
|
|
|
|
|
You should have received a copy of the GNU General Public License
|
|
|
|
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
|
*/
|
2022-08-30 16:13:11 +05:00
|
|
|
|
2024-09-23 15:08:57 +05:00
|
|
|
import { User } from "../types.js";
|
|
|
|
|
import http from "../utils/http.js";
|
|
|
|
|
import constants from "../utils/constants.js";
|
|
|
|
|
import TokenManager from "./token-manager.js";
|
|
|
|
|
import { EV, EVENTS } from "../common.js";
|
|
|
|
|
import { HealthCheck } from "./healthcheck.js";
|
|
|
|
|
import Database from "./index.js";
|
2024-02-28 23:21:22 +05:00
|
|
|
import { SerializedKey } from "@notesnook/crypto";
|
2024-09-23 15:08:57 +05:00
|
|
|
import { logger } from "../logger.js";
|
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-04-01 18:41:33 +05:00
|
|
|
resetUser: "/users/reset",
|
2022-08-31 06:33:37 +05:00
|
|
|
activateTrial: "/subscriptions/trial"
|
2020-12-16 12:06:25 +05:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
class UserManager {
|
2023-08-21 13:32:06 +05:00
|
|
|
private tokenManager: TokenManager;
|
2024-08-13 14:57:52 +05:00
|
|
|
private cachedAttachmentKey?: SerializedKey;
|
2023-08-21 13:32:06 +05:00
|
|
|
constructor(private readonly db: Database) {
|
2024-02-05 22:05:55 +05:00
|
|
|
this.tokenManager = new TokenManager(this.db.kv);
|
2023-08-21 13:32:06 +05:00
|
|
|
|
|
|
|
|
EV.subscribe(EVENTS.userUnauthorized, async (url: string) => {
|
|
|
|
|
if (url.includes("/connect/token") || !(await HealthCheck.auth())) return;
|
2022-01-07 13:58:06 +05:00
|
|
|
try {
|
|
|
|
|
await this.tokenManager._refreshToken(true);
|
|
|
|
|
} catch (e) {
|
2023-08-21 13:32:06 +05:00
|
|
|
if (
|
|
|
|
|
e instanceof Error &&
|
|
|
|
|
(e.message === "invalid_grant" || e.message === "invalid_client")
|
|
|
|
|
) {
|
2023-03-17 17:00:15 +05:00
|
|
|
await this.logout(
|
|
|
|
|
false,
|
|
|
|
|
`Your token has been revoked. Error: ${e.message}.`
|
|
|
|
|
);
|
|
|
|
|
}
|
2022-01-07 13:58:06 +05:00
|
|
|
}
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
2023-08-21 13:32:06 +05:00
|
|
|
async signup(email: string, password: string) {
|
2022-09-16 11:16:16 +05:00
|
|
|
email = email.toLowerCase();
|
|
|
|
|
|
2023-08-21 13:32:06 +05:00
|
|
|
const hashedPassword = await this.db.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,
|
2022-08-31 06:33:37 +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
|
|
|
}
|
|
|
|
|
|
2023-08-21 13:32:06 +05:00
|
|
|
async authenticateEmail(email: string) {
|
2022-12-09 12:14:12 +05:00
|
|
|
if (!email) throw new Error("Email is required.");
|
|
|
|
|
|
|
|
|
|
email = email.toLowerCase();
|
|
|
|
|
|
2022-12-12 12:55:15 +05:00
|
|
|
const result = await http.post(`${constants.AUTH_HOST}${ENDPOINTS.token}`, {
|
2022-12-09 12:14:12 +05:00
|
|
|
email,
|
|
|
|
|
grant_type: "email",
|
|
|
|
|
client_id: "notesnook"
|
|
|
|
|
});
|
2022-12-12 12:55:15 +05:00
|
|
|
|
|
|
|
|
await this.tokenManager.saveToken(result);
|
|
|
|
|
return result.additional_data;
|
2022-12-09 12:14:12 +05:00
|
|
|
}
|
|
|
|
|
|
2023-08-21 13:32:06 +05:00
|
|
|
async authenticateMultiFactorCode(code: string, method: string) {
|
2022-12-12 12:55:15 +05:00
|
|
|
if (!code || !method) throw new Error("code & method are required.");
|
|
|
|
|
|
2024-05-09 10:30:25 +05:00
|
|
|
const token = await this.tokenManager.getToken();
|
|
|
|
|
if (!token || token.scope !== "auth:grant_types:mfa")
|
|
|
|
|
throw new Error("No token found.");
|
2022-12-12 12:55:15 +05:00
|
|
|
|
|
|
|
|
await this.tokenManager.saveToken(
|
|
|
|
|
await http.post(
|
|
|
|
|
`${constants.AUTH_HOST}${ENDPOINTS.token}`,
|
|
|
|
|
{
|
|
|
|
|
grant_type: "mfa",
|
|
|
|
|
client_id: "notesnook",
|
|
|
|
|
"mfa:code": code,
|
|
|
|
|
"mfa:method": method
|
|
|
|
|
},
|
2024-05-09 10:30:25 +05:00
|
|
|
token.access_token
|
2022-12-12 12:55:15 +05:00
|
|
|
)
|
2022-12-09 12:14:12 +05:00
|
|
|
);
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
2023-10-30 11:55:43 +05:00
|
|
|
async authenticatePassword(
|
2023-08-21 13:32:06 +05:00
|
|
|
email: string,
|
|
|
|
|
password: string,
|
|
|
|
|
hashedPassword?: string,
|
|
|
|
|
sessionExpired?: boolean
|
2023-10-30 11:55:43 +05:00
|
|
|
) {
|
2022-12-12 12:55:15 +05:00
|
|
|
if (!email || !password) throw new Error("email & password are required.");
|
|
|
|
|
|
2024-03-06 11:28:45 +05:00
|
|
|
const token = await this.tokenManager.getToken();
|
2024-05-09 10:30:25 +05:00
|
|
|
if (!token || token.scope !== "auth:grant_types:mfa_password")
|
|
|
|
|
throw new Error("No token found.");
|
2022-12-09 12:14:12 +05:00
|
|
|
|
|
|
|
|
email = email.toLowerCase();
|
|
|
|
|
if (!hashedPassword) {
|
2023-08-21 13:32:06 +05:00
|
|
|
hashedPassword = await this.db.storage().hash(password, email);
|
2022-12-09 12:14:12 +05:00
|
|
|
}
|
2024-03-06 11:28:45 +05:00
|
|
|
try {
|
2024-12-11 14:49:26 +05:00
|
|
|
let usesFallback = false;
|
2024-03-06 11:28:45 +05:00
|
|
|
await this.tokenManager.saveToken(
|
2024-12-11 14:49:26 +05:00
|
|
|
await http
|
|
|
|
|
.post(
|
|
|
|
|
`${constants.AUTH_HOST}${ENDPOINTS.token}`,
|
|
|
|
|
{
|
|
|
|
|
grant_type: "mfa_password",
|
|
|
|
|
client_id: "notesnook",
|
|
|
|
|
scope: "notesnook.sync offline_access IdentityServerApi",
|
|
|
|
|
password: hashedPassword
|
|
|
|
|
},
|
|
|
|
|
token.access_token
|
|
|
|
|
)
|
|
|
|
|
.catch(async (e) => {
|
2024-12-13 14:06:17 +05:00
|
|
|
if (e instanceof Error && e.message === "Password is incorrect.") {
|
2024-12-11 14:49:26 +05:00
|
|
|
hashedPassword = await this.db
|
|
|
|
|
.storage()
|
|
|
|
|
.hash(password, email, { usesFallback: true });
|
|
|
|
|
if (hashedPassword === null) return Promise.reject(e);
|
|
|
|
|
usesFallback = true;
|
|
|
|
|
return await http.post(
|
|
|
|
|
`${constants.AUTH_HOST}${ENDPOINTS.token}`,
|
|
|
|
|
{
|
|
|
|
|
grant_type: "mfa_password",
|
|
|
|
|
client_id: "notesnook",
|
|
|
|
|
scope: "notesnook.sync offline_access IdentityServerApi",
|
|
|
|
|
password: hashedPassword
|
|
|
|
|
},
|
|
|
|
|
token.access_token
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
return Promise.reject(e);
|
|
|
|
|
})
|
2024-03-06 11:28:45 +05:00
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const user = await this.fetchUser();
|
2024-02-12 12:45:16 +05:00
|
|
|
if (!user) throw new Error("Failed to fetch user.");
|
2024-03-06 11:28:45 +05:00
|
|
|
|
|
|
|
|
if (!sessionExpired) {
|
2024-02-05 22:05:55 +05:00
|
|
|
await this.db.setLastSynced(0);
|
2023-12-08 11:51:51 +05:00
|
|
|
await this.db.syncer.devices.register();
|
2024-03-06 11:28:45 +05:00
|
|
|
}
|
2022-12-09 12:14:12 +05:00
|
|
|
|
2024-12-11 14:49:26 +05:00
|
|
|
if (usesFallback) {
|
|
|
|
|
await this.db.storage().deriveCryptoKeyFallback({
|
|
|
|
|
password,
|
|
|
|
|
salt: user.salt
|
|
|
|
|
});
|
|
|
|
|
} else {
|
|
|
|
|
await this.db.storage().deriveCryptoKey({
|
|
|
|
|
password,
|
|
|
|
|
salt: user.salt
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
await this.db.kv().write("usesFallbackPWHash", usesFallback);
|
2024-03-06 11:28:45 +05:00
|
|
|
EV.publish(EVENTS.userLoggedIn, user);
|
|
|
|
|
} catch (e) {
|
|
|
|
|
await this.tokenManager.saveToken(token);
|
|
|
|
|
throw e;
|
2023-10-30 11:55:43 +05:00
|
|
|
}
|
2022-12-09 12:14:12 +05:00
|
|
|
}
|
|
|
|
|
|
2023-08-21 13:32:06 +05:00
|
|
|
async _login({
|
|
|
|
|
email,
|
|
|
|
|
password,
|
|
|
|
|
hashedPassword,
|
|
|
|
|
code,
|
|
|
|
|
method
|
|
|
|
|
}: {
|
|
|
|
|
email: string;
|
|
|
|
|
password: string;
|
|
|
|
|
hashedPassword?: string;
|
|
|
|
|
code?: string;
|
|
|
|
|
method?: string;
|
|
|
|
|
}) {
|
2023-01-09 18:58:30 +05:00
|
|
|
email = email && email.toLowerCase();
|
|
|
|
|
|
|
|
|
|
if (!hashedPassword && password) {
|
2023-08-21 13:32:06 +05:00
|
|
|
hashedPassword = await this.db.storage().hash(password, email);
|
2023-01-09 18:58:30 +05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
await this.tokenManager.saveToken(
|
|
|
|
|
await http.post(`${constants.AUTH_HOST}${ENDPOINTS.token}`, {
|
|
|
|
|
username: email,
|
|
|
|
|
password: hashedPassword,
|
|
|
|
|
grant_type: code ? "mfa" : "password",
|
2024-05-11 10:29:02 +05:00
|
|
|
scope: "notesnook.sync offline_access IdentityServerApi",
|
2023-01-09 18:58:30 +05:00
|
|
|
client_id: "notesnook",
|
|
|
|
|
"mfa:code": code,
|
|
|
|
|
"mfa:method": method
|
|
|
|
|
})
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const user = await this.fetchUser();
|
2023-08-21 13:32:06 +05:00
|
|
|
if (!user) return;
|
|
|
|
|
|
2024-01-12 15:41:33 +05:00
|
|
|
await this.db.storage().deriveCryptoKey({
|
2023-01-09 18:58:30 +05:00
|
|
|
password,
|
|
|
|
|
salt: user.salt
|
|
|
|
|
});
|
2024-02-05 22:05:55 +05:00
|
|
|
await this.db.setLastSynced(0);
|
2023-12-08 11:51:51 +05:00
|
|
|
await this.db.syncer.devices.register();
|
2023-01-09 18:58:30 +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;
|
|
|
|
|
}
|
|
|
|
|
|
2023-08-21 13:32:06 +05:00
|
|
|
async logout(revoke = true, reason?: string) {
|
2020-12-22 16:13:01 +05:00
|
|
|
try {
|
2023-12-08 11:51:51 +05:00
|
|
|
await this.db.syncer.devices.unregister();
|
2020-12-22 16:13:01 +05:00
|
|
|
if (revoke) await this.tokenManager.revokeToken();
|
|
|
|
|
} catch (e) {
|
2024-04-18 09:37:21 +05:00
|
|
|
logger.error(e, "Error logging out user.", { revoke, reason });
|
2020-12-22 16:13:01 +05:00
|
|
|
} finally {
|
2024-08-13 14:57:52 +05:00
|
|
|
this.cachedAttachmentKey = undefined;
|
2023-12-12 10:02:26 +05:00
|
|
|
await this.db.reset();
|
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
|
|
|
}
|
|
|
|
|
|
2023-08-21 13:32:06 +05:00
|
|
|
setUser(user: User) {
|
2024-02-05 22:05:55 +05:00
|
|
|
return this.db.kv().write("user", user);
|
2020-12-16 12:06:25 +05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
getUser() {
|
2024-02-05 22:05:55 +05:00
|
|
|
return this.db.kv().read("user");
|
2020-12-16 12:06:25 +05:00
|
|
|
}
|
|
|
|
|
|
2024-02-12 11:38:35 +05:00
|
|
|
/**
|
|
|
|
|
* @deprecated
|
|
|
|
|
*/
|
|
|
|
|
getLegacyUser() {
|
|
|
|
|
return this.db.storage().read<User>("user");
|
|
|
|
|
}
|
|
|
|
|
|
2022-04-01 18:41:33 +05:00
|
|
|
async resetUser(removeAttachments = true) {
|
2023-08-21 13:32:06 +05:00
|
|
|
const token = await this.tokenManager.getAccessToken();
|
2022-04-01 18:41:33 +05:00
|
|
|
if (!token) return;
|
|
|
|
|
await http.post(
|
|
|
|
|
`${constants.API_HOST}${ENDPOINTS.resetUser}`,
|
|
|
|
|
{ removeAttachments },
|
|
|
|
|
token
|
|
|
|
|
);
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
2024-03-21 13:16:21 +05:00
|
|
|
private async updateUser(partial: Partial<User>) {
|
|
|
|
|
const user = await this.getUser();
|
|
|
|
|
if (!user) return;
|
|
|
|
|
|
2023-08-21 13:32:06 +05:00
|
|
|
const token = await this.tokenManager.getAccessToken();
|
2021-12-31 09:05:45 +05:00
|
|
|
await http.patch.json(
|
|
|
|
|
`${constants.API_HOST}${ENDPOINTS.user}`,
|
2024-03-21 13:16:21 +05:00
|
|
|
partial,
|
2021-12-31 09:05:45 +05:00
|
|
|
token
|
|
|
|
|
);
|
|
|
|
|
|
2024-03-21 13:16:21 +05:00
|
|
|
await this.setUser({ ...user, ...partial });
|
2024-02-28 23:21:22 +05:00
|
|
|
}
|
|
|
|
|
|
2023-08-21 13:32:06 +05:00
|
|
|
async deleteUser(password: string) {
|
|
|
|
|
const token = await this.tokenManager.getAccessToken();
|
2021-01-19 09:48:42 +05:00
|
|
|
const user = await this.getUser();
|
2023-08-21 13:32:06 +05:00
|
|
|
if (!token || !user) return;
|
|
|
|
|
|
2020-12-16 12:06:25 +05:00
|
|
|
await http.post(
|
|
|
|
|
`${constants.API_HOST}${ENDPOINTS.deleteUser}`,
|
2024-12-11 14:49:26 +05:00
|
|
|
{
|
|
|
|
|
password: await this.db.storage().hash(password, user.email, {
|
|
|
|
|
usesFallback: await this.db.kv().read("usesFallbackPWHash")
|
|
|
|
|
})
|
|
|
|
|
},
|
2020-12-16 12:06:25 +05:00
|
|
|
token
|
|
|
|
|
);
|
|
|
|
|
await this.logout(false, "Account deleted.");
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
2023-08-21 13:32:06 +05:00
|
|
|
async fetchUser(): Promise<User | undefined> {
|
2020-12-16 12:06:25 +05:00
|
|
|
try {
|
2023-08-21 13:32:06 +05:00
|
|
|
const token = await this.tokenManager.getAccessToken();
|
2020-12-16 12:06:25 +05:00
|
|
|
if (!token) return;
|
|
|
|
|
const user = await http.get(
|
|
|
|
|
`${constants.API_HOST}${ENDPOINTS.user}`,
|
|
|
|
|
token
|
|
|
|
|
);
|
|
|
|
|
if (user) {
|
2024-07-22 13:08:12 +05:00
|
|
|
const oldUser = await this.getUser();
|
|
|
|
|
if (
|
|
|
|
|
oldUser &&
|
|
|
|
|
(oldUser.subscription.type !== user.subscription.type ||
|
|
|
|
|
oldUser.subscription.provider !== user.subscription.provider)
|
|
|
|
|
) {
|
|
|
|
|
await this.tokenManager._refreshToken(true);
|
|
|
|
|
EV.publish(EVENTS.userSubscriptionUpdated, user.subscription);
|
|
|
|
|
}
|
|
|
|
|
if (oldUser && !oldUser.isEmailConfirmed && user.isEmailConfirmed)
|
|
|
|
|
EV.publish(EVENTS.userEmailConfirmed);
|
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) {
|
2024-04-18 09:37:21 +05:00
|
|
|
logger.error(e, "Error fetching user");
|
2021-01-22 22:08:56 +05:00
|
|
|
return await this.getUser();
|
2020-12-16 12:06:25 +05:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-08-21 13:32:06 +05:00
|
|
|
changePassword(oldPassword: string, newPassword: string) {
|
2020-12-22 13:13:18 +05:00
|
|
|
return this._updatePassword("change_password", {
|
|
|
|
|
old_password: oldPassword,
|
2022-08-31 06:33:37 +05:00
|
|
|
new_password: newPassword
|
2020-12-22 13:13:18 +05:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2023-08-21 13:32:06 +05:00
|
|
|
async changeMarketingConsent(enabled: boolean) {
|
|
|
|
|
const token = await this.tokenManager.getAccessToken();
|
2023-05-31 19:47:08 +05:00
|
|
|
if (!token) return;
|
|
|
|
|
|
|
|
|
|
await http.patch(
|
|
|
|
|
`${constants.AUTH_HOST}${ENDPOINTS.patchUser}`,
|
|
|
|
|
{
|
|
|
|
|
type: "change_marketing_consent",
|
|
|
|
|
enabled: enabled
|
|
|
|
|
},
|
|
|
|
|
token
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2023-08-21 13:32:06 +05:00
|
|
|
resetPassword(newPassword: string) {
|
2020-12-22 13:13:18 +05:00
|
|
|
return this._updatePassword("reset_password", {
|
2022-08-31 06:33:37 +05:00
|
|
|
new_password: newPassword
|
2020-12-22 13:13:18 +05:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2023-08-21 13:32:06 +05:00
|
|
|
async getEncryptionKey(): Promise<SerializedKey | undefined> {
|
2020-12-16 12:06:25 +05:00
|
|
|
const user = await this.getUser();
|
|
|
|
|
if (!user) return;
|
2024-01-12 15:41:33 +05:00
|
|
|
const key = await this.db.storage().getCryptoKey();
|
2022-04-06 03:12:05 +05:00
|
|
|
if (!key) return;
|
2020-12-16 12:06:25 +05:00
|
|
|
return { key, salt: user.salt };
|
|
|
|
|
}
|
|
|
|
|
|
2024-02-12 11:38:35 +05:00
|
|
|
/**
|
|
|
|
|
* @deprecated
|
|
|
|
|
*/
|
|
|
|
|
async getLegacyEncryptionKey(): Promise<SerializedKey | undefined> {
|
|
|
|
|
const user = await this.getLegacyUser();
|
|
|
|
|
if (!user) return;
|
|
|
|
|
const key = await this.db.storage().getCryptoKey();
|
|
|
|
|
if (!key) return;
|
|
|
|
|
return { key, salt: user.salt };
|
|
|
|
|
}
|
|
|
|
|
|
2021-10-25 11:35:00 +05:00
|
|
|
async getAttachmentsKey() {
|
2024-08-13 14:57:52 +05:00
|
|
|
if (this.cachedAttachmentKey) return this.cachedAttachmentKey;
|
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) {
|
2023-08-21 13:32:06 +05:00
|
|
|
const token = await this.tokenManager.getAccessToken();
|
2021-10-26 09:13:18 +05:00
|
|
|
user = await http.get(`${constants.API_HOST}${ENDPOINTS.user}`, token);
|
|
|
|
|
}
|
2023-08-21 13:32:06 +05:00
|
|
|
if (!user) return;
|
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) {
|
2023-08-21 13:32:06 +05:00
|
|
|
const key = await this.db.crypto().generateRandomKey();
|
|
|
|
|
user.attachmentsKey = await this.db
|
|
|
|
|
.storage()
|
|
|
|
|
.encrypt(userEncryptionKey, JSON.stringify(key));
|
2021-10-25 11:35:00 +05:00
|
|
|
|
2024-03-21 13:16:21 +05:00
|
|
|
await this.updateUser({ attachmentsKey: user.attachmentsKey });
|
2021-10-26 09:13:18 +05:00
|
|
|
return key;
|
|
|
|
|
}
|
2021-10-25 11:35:00 +05:00
|
|
|
|
2023-08-21 13:32:06 +05:00
|
|
|
const plainData = await this.db
|
|
|
|
|
.storage()
|
|
|
|
|
.decrypt(userEncryptionKey, user.attachmentsKey);
|
|
|
|
|
if (!plainData) return;
|
2024-08-13 14:57:52 +05:00
|
|
|
this.cachedAttachmentKey = JSON.parse(plainData) as SerializedKey;
|
|
|
|
|
return this.cachedAttachmentKey;
|
2021-10-26 09:13:18 +05:00
|
|
|
} catch (e) {
|
2024-04-18 09:37:21 +05:00
|
|
|
logger.error(e, "Could not get attachments encryption key.");
|
2023-08-21 13:32:06 +05:00
|
|
|
if (e instanceof Error)
|
|
|
|
|
throw new Error(
|
2024-04-18 09:37:21 +05:00
|
|
|
`Could not get attachments encryption key. Error: ${e.message}`
|
2023-08-21 13:32:06 +05:00
|
|
|
);
|
2021-10-26 09:13:18 +05:00
|
|
|
}
|
2021-10-25 11:35:00 +05:00
|
|
|
}
|
|
|
|
|
|
2024-07-16 14:26:19 +05:00
|
|
|
async sendVerificationEmail(newEmail?: string) {
|
2023-08-21 13:32:06 +05:00
|
|
|
const token = await this.tokenManager.getAccessToken();
|
2020-12-22 13:13:18 +05:00
|
|
|
if (!token) return;
|
|
|
|
|
await http.post(
|
|
|
|
|
`${constants.AUTH_HOST}${ENDPOINTS.verifyUser}`,
|
2023-01-09 15:04:44 +05:00
|
|
|
{ newEmail },
|
|
|
|
|
token
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2023-08-21 13:32:06 +05:00
|
|
|
async changeEmail(newEmail: string, password: string, code: string) {
|
2023-01-09 15:04:44 +05:00
|
|
|
const token = await this.tokenManager.getAccessToken();
|
|
|
|
|
if (!token) return;
|
|
|
|
|
|
|
|
|
|
const user = await this.getUser();
|
|
|
|
|
if (!user) return;
|
|
|
|
|
|
|
|
|
|
const email = newEmail.toLowerCase();
|
|
|
|
|
|
|
|
|
|
await http.patch(
|
|
|
|
|
`${constants.AUTH_HOST}${ENDPOINTS.patchUser}`,
|
|
|
|
|
{
|
|
|
|
|
type: "change_email",
|
|
|
|
|
new_email: newEmail,
|
2024-12-11 14:49:26 +05:00
|
|
|
password: await this.db.storage().hash(password, email, {
|
|
|
|
|
usesFallback: await this.db.kv().read("usesFallbackPWHash")
|
|
|
|
|
}),
|
2023-01-09 15:04:44 +05:00
|
|
|
verification_code: code
|
|
|
|
|
},
|
2020-12-22 13:13:18 +05:00
|
|
|
token
|
|
|
|
|
);
|
2023-01-09 15:04:44 +05:00
|
|
|
|
2024-01-12 15:41:33 +05:00
|
|
|
await this.db.storage().deriveCryptoKey({
|
2023-01-09 15:04:44 +05:00
|
|
|
password,
|
|
|
|
|
salt: user.salt
|
|
|
|
|
});
|
2020-12-22 13:13:18 +05:00
|
|
|
}
|
|
|
|
|
|
2023-08-21 13:32:06 +05:00
|
|
|
recoverAccount(email: string) {
|
2020-12-22 13:13:18 +05:00
|
|
|
return http.post(`${constants.AUTH_HOST}${ENDPOINTS.recoverAccount}`, {
|
|
|
|
|
email,
|
2022-08-31 06:33:37 +05:00
|
|
|
client_id: "notesnook"
|
2020-12-22 13:13:18 +05:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2023-08-21 13:32:06 +05:00
|
|
|
async verifyPassword(password: string) {
|
2021-06-01 09:46:50 +05:00
|
|
|
try {
|
|
|
|
|
const user = await this.getUser();
|
|
|
|
|
const key = await this.getEncryptionKey();
|
2023-08-21 13:32:06 +05:00
|
|
|
if (!user || !key) return false;
|
|
|
|
|
|
|
|
|
|
const cipher = await this.db.storage().encrypt(key, "notesnook");
|
|
|
|
|
const plainText = await this.db.storage().decrypt({ password }, cipher);
|
2021-06-01 09:46:50 +05:00
|
|
|
return plainText === "notesnook";
|
|
|
|
|
} catch (e) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-08-21 13:32:06 +05:00
|
|
|
async _updatePassword(
|
|
|
|
|
type: "change_password" | "reset_password",
|
|
|
|
|
data: {
|
|
|
|
|
new_password: string;
|
|
|
|
|
old_password?: string;
|
|
|
|
|
encryptionKey?: SerializedKey;
|
|
|
|
|
}
|
|
|
|
|
) {
|
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
|
|
|
|
2023-11-02 12:21:01 +05:00
|
|
|
await this.clearSessions();
|
2021-12-31 09:05:45 +05:00
|
|
|
|
2023-08-21 13:32:06 +05:00
|
|
|
if (data.encryptionKey) await this.db.sync({ type: "fetch", force: true });
|
2022-04-01 18:41:33 +05:00
|
|
|
|
2024-01-12 15:41:33 +05:00
|
|
|
await this.db.storage().deriveCryptoKey({
|
2023-11-02 12:21:01 +05:00
|
|
|
password: new_password,
|
|
|
|
|
salt
|
|
|
|
|
});
|
2021-12-31 09:05:45 +05:00
|
|
|
|
2023-11-02 12:21:01 +05:00
|
|
|
if (!(await this.resetUser(false))) return;
|
2021-12-31 09:05:45 +05:00
|
|
|
|
2023-08-21 13:32:06 +05:00
|
|
|
await this.db.sync({ type: "send", force: true });
|
2022-01-01 16:11:55 +05:00
|
|
|
|
2023-11-02 12:21:01 +05:00
|
|
|
if (attachmentsKey) {
|
|
|
|
|
const userEncryptionKey = await this.getEncryptionKey();
|
|
|
|
|
if (!userEncryptionKey) return;
|
2023-08-21 13:32:06 +05:00
|
|
|
user.attachmentsKey = await this.db
|
|
|
|
|
.storage()
|
|
|
|
|
.encrypt(userEncryptionKey, JSON.stringify(attachmentsKey));
|
2024-03-21 13:16:21 +05:00
|
|
|
await this.updateUser({ attachmentsKey: user.attachmentsKey });
|
2023-11-02 12:21:01 +05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (old_password)
|
2024-12-11 14:49:26 +05:00
|
|
|
old_password = await this.db.storage().hash(old_password, email, {
|
|
|
|
|
usesFallback: await this.db.kv().read("usesFallbackPWHash")
|
|
|
|
|
});
|
2023-11-02 12:21:01 +05:00
|
|
|
if (new_password)
|
2023-08-21 13:32:06 +05:00
|
|
|
new_password = await this.db.storage().hash(new_password, email);
|
2023-11-02 12:21:01 +05:00
|
|
|
|
|
|
|
|
await http.patch(
|
|
|
|
|
`${constants.AUTH_HOST}${ENDPOINTS.patchUser}`,
|
|
|
|
|
{
|
|
|
|
|
type,
|
|
|
|
|
old_password,
|
|
|
|
|
new_password
|
|
|
|
|
},
|
|
|
|
|
token
|
|
|
|
|
);
|
2024-12-13 14:06:17 +05:00
|
|
|
await this.db.kv().write("usesFallbackPWHash", false);
|
2021-12-31 09:05:45 +05:00
|
|
|
|
2020-12-16 12:06:25 +05:00
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export default UserManager;
|