[web] General code improvements (#6222)
This commit is contained in:
@@ -54,6 +54,7 @@ import type {
|
||||
import { type CollectionUser } from "ente-media/collection";
|
||||
import { PublicLinkCreated } from "ente-new/photos/components/share/PublicLinkCreated";
|
||||
import { avatarTextColor } from "ente-new/photos/services/avatar";
|
||||
import { deleteShareURL } from "ente-new/photos/services/collection";
|
||||
import type { CollectionSummary } from "ente-new/photos/services/collection/ui";
|
||||
import { usePhotosAppContext } from "ente-new/photos/types/context";
|
||||
import { CustomError, parseSharingErrorCodes } from "ente-shared/error";
|
||||
@@ -65,7 +66,6 @@ import React, { useContext, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { Trans } from "react-i18next";
|
||||
import {
|
||||
createShareableURL,
|
||||
deleteShareableURL,
|
||||
shareCollection,
|
||||
unshareCollection,
|
||||
updateShareableURL,
|
||||
@@ -1222,16 +1222,16 @@ const ManagePublicShareOptions: React.FC<ManagePublicShareOptionsProps> = ({
|
||||
galleryContext.setBlockingLoad(false);
|
||||
}
|
||||
};
|
||||
const disablePublicSharing = async () => {
|
||||
const handleRemovePublicLink = async () => {
|
||||
try {
|
||||
galleryContext.setBlockingLoad(true);
|
||||
await deleteShareableURL(collection);
|
||||
await deleteShareURL(collection.id);
|
||||
setPublicShareProp(null);
|
||||
galleryContext.syncWithRemote(false, true);
|
||||
onClose();
|
||||
} catch (e) {
|
||||
const errorMessage = handleSharingErrors(e);
|
||||
setSharableLinkError(errorMessage);
|
||||
log.error("Failed to remove public link", e);
|
||||
setSharableLinkError(t("generic_error"));
|
||||
} finally {
|
||||
galleryContext.setBlockingLoad(false);
|
||||
}
|
||||
@@ -1293,7 +1293,7 @@ const ManagePublicShareOptions: React.FC<ManagePublicShareOptionsProps> = ({
|
||||
<RowButton
|
||||
color="critical"
|
||||
startIcon={<RemoveCircleOutlineIcon />}
|
||||
onClick={disablePublicSharing}
|
||||
onClick={handleRemovePublicLink}
|
||||
label={t("remove_link")}
|
||||
/>
|
||||
</RowButtonGroup>
|
||||
|
||||
@@ -571,24 +571,6 @@ export const createShareableURL = async (collection: Collection) => {
|
||||
}
|
||||
};
|
||||
|
||||
export const deleteShareableURL = async (collection: Collection) => {
|
||||
try {
|
||||
const token = getToken();
|
||||
if (!token) {
|
||||
return null;
|
||||
}
|
||||
await HTTPService.delete(
|
||||
await apiURL(`/collections/share-url/${collection.id}`),
|
||||
null,
|
||||
null,
|
||||
{ "X-Auth-Token": token },
|
||||
);
|
||||
} catch (e) {
|
||||
log.error("deleteShareableURL failed ", e);
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
|
||||
export const updateShareableURL = async (
|
||||
request: UpdatePublicURL,
|
||||
): Promise<PublicURL> => {
|
||||
|
||||
@@ -34,11 +34,11 @@ export interface RecoverPageProps {
|
||||
const Page: React.FC<RecoverPageProps> = ({ twoFactorType }) => {
|
||||
const { logout, showMiniDialog } = useBaseContext();
|
||||
|
||||
const [sessionID, setSessionID] = useState<string | null>(null);
|
||||
const [encryptedTwoFactorSecret, setEncryptedTwoFactorSecret] = useState<{
|
||||
encryptedData: string;
|
||||
nonce: string;
|
||||
} | null>(null);
|
||||
const [sessionID, setSessionID] = useState<string | null>(null);
|
||||
const [doesHaveEncryptedRecoveryKey, setDoesHaveEncryptedRecoveryKey] =
|
||||
useState(false);
|
||||
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import { Paper, Stack, styled, Typography } from "@mui/material";
|
||||
import { CodeBlock } from "ente-accounts/components/CodeBlock";
|
||||
import { Verify2FACodeForm } from "ente-accounts/components/Verify2FACodeForm";
|
||||
import { getUserRecoveryKey } from "ente-accounts/services/recovery-key";
|
||||
import { appHomeRoute } from "ente-accounts/services/redirect";
|
||||
import type { TwoFactorSecret } from "ente-accounts/services/user";
|
||||
import { enableTwoFactor, setupTwoFactor } from "ente-accounts/services/user";
|
||||
import {
|
||||
setupTwoFactor,
|
||||
setupTwoFactorFinish,
|
||||
} from "ente-accounts/services/user";
|
||||
import { CenteredFill } from "ente-base/components/containers";
|
||||
import { LinkButton } from "ente-base/components/LinkButton";
|
||||
import { ActivityIndicator } from "ente-base/components/mui/ActivityIndicator";
|
||||
import { FocusVisibleButton } from "ente-base/components/mui/FocusVisibleButton";
|
||||
import { encryptBox } from "ente-base/crypto";
|
||||
import { getData, setLSUser } from "ente-shared/storage/localStorage";
|
||||
import { t } from "i18next";
|
||||
import { useRouter } from "next/router";
|
||||
import React, { useEffect, useState } from "react";
|
||||
@@ -27,16 +27,7 @@ const Page: React.FC = () => {
|
||||
}, []);
|
||||
|
||||
const handleSubmit = async (otp: string) => {
|
||||
const box = await encryptBox(
|
||||
twoFactorSecret!.secretCode,
|
||||
await getUserRecoveryKey(),
|
||||
);
|
||||
await enableTwoFactor({
|
||||
code: otp,
|
||||
encryptedTwoFactorSecret: box.encryptedData,
|
||||
twoFactorSecretDecryptionNonce: box.nonce,
|
||||
});
|
||||
await setLSUser({ ...getData("user"), isTwoFactorEnabled: true });
|
||||
await setupTwoFactorFinish(twoFactorSecret!.secretCode, otp);
|
||||
await router.push(appHomeRoute);
|
||||
};
|
||||
|
||||
|
||||
@@ -38,10 +38,20 @@ const Page: React.FC = () => {
|
||||
|
||||
const handleSubmit = async (otp: string) => {
|
||||
try {
|
||||
const resp = await verifyTwoFactor(otp, sessionID);
|
||||
const { keyAttributes, encryptedToken, token, id } = resp;
|
||||
await setLSUser({ ...getData("user"), token, encryptedToken, id });
|
||||
setData("keyAttributes", keyAttributes!);
|
||||
const { keyAttributes, encryptedToken, id } = await verifyTwoFactor(
|
||||
otp,
|
||||
sessionID,
|
||||
);
|
||||
await setLSUser({
|
||||
...getData("user"),
|
||||
id,
|
||||
// The original code was parsing an token which is never going
|
||||
// to be present in the response, so effectively was always
|
||||
// setting token to undefined. So this works, but is it needed?
|
||||
token: undefined,
|
||||
encryptedToken,
|
||||
});
|
||||
setData("keyAttributes", keyAttributes);
|
||||
await router.push(unstashRedirect() ?? "/credentials");
|
||||
} catch (e) {
|
||||
if (e instanceof HTTPError && e.res.status == 404) {
|
||||
|
||||
@@ -252,7 +252,7 @@ export const saveCredentialsAndNavigateTo = async (
|
||||
const { id, encryptedToken, keyAttributes } = response;
|
||||
|
||||
await setLSUser({ ...getData("user"), encryptedToken, id });
|
||||
setData("keyAttributes", keyAttributes!);
|
||||
setData("keyAttributes", keyAttributes);
|
||||
|
||||
return unstashRedirect() ?? "/credentials";
|
||||
};
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { encryptBox } from "ente-base/crypto";
|
||||
import {
|
||||
authenticatedRequestHeaders,
|
||||
ensureOk,
|
||||
@@ -5,9 +6,10 @@ import {
|
||||
} from "ente-base/http";
|
||||
import { apiURL } from "ente-base/origins";
|
||||
import HTTPService from "ente-shared/network/HTTPService";
|
||||
import { getToken } from "ente-shared/storage/localStorage/helpers";
|
||||
import { getData, setLSUser } from "ente-shared/storage/localStorage";
|
||||
import { nullToUndefined } from "ente-utils/transform";
|
||||
import { z } from "zod/v4";
|
||||
import { getUserRecoveryKey } from "./recovery-key";
|
||||
|
||||
export interface User {
|
||||
id: number;
|
||||
@@ -18,18 +20,25 @@ export interface User {
|
||||
twoFactorSessionID: string;
|
||||
}
|
||||
|
||||
// TODO: During login the only field present is email. Which makes this
|
||||
// optionality indicated by these types incorrect.
|
||||
/**
|
||||
* The local storage data about the user after they've logged in.
|
||||
*/
|
||||
const LocalUser = z.object({
|
||||
/** The user's ID. */
|
||||
/**
|
||||
* The user's ID.
|
||||
*/
|
||||
id: z.number(),
|
||||
/** The user's email. */
|
||||
/**
|
||||
* The user's email.
|
||||
*/
|
||||
email: z.string(),
|
||||
/**
|
||||
* The user's (plaintext) auth token.
|
||||
*
|
||||
* It is used for making API calls on their behalf, by passing this token as
|
||||
* the value of the X-Auth-Token header in the HTTP request.
|
||||
*
|
||||
* Deprecated, use `getAuthToken()` instead (which fetches it from IDB).
|
||||
*/
|
||||
token: z.string(),
|
||||
});
|
||||
@@ -37,9 +46,30 @@ const LocalUser = z.object({
|
||||
/** Locally available data for the logged in user */
|
||||
export type LocalUser = z.infer<typeof LocalUser>;
|
||||
|
||||
/**
|
||||
* The local storage data about the user before login or signup is complete.
|
||||
*
|
||||
* During login or signup, the user object exists in various partial states in
|
||||
* local storage.
|
||||
*
|
||||
* - Initially, there is no user object in local storage.
|
||||
*
|
||||
* - When the user enters their email, the email property of the stored object
|
||||
* is set, but nothing else.
|
||||
*
|
||||
* - If they have second factor verification set, then after entering their
|
||||
* password {@link isTwoFactorEnabled} and {@link twoFactorSessionID} will
|
||||
* also get filled in.
|
||||
*
|
||||
* - Once they verify their TOTP based second factor, their {@link id} and
|
||||
* {@link encryptedToken} will also get filled in.
|
||||
*/
|
||||
// TODO: Start using me.
|
||||
export const PreLoginLocalUser = LocalUser.partial();
|
||||
|
||||
/**
|
||||
* Return the logged-in user, if someone is indeed logged in. Otherwise return
|
||||
* `undefined`.
|
||||
* `undefined` (TODO: That's not what it is doing...).
|
||||
*
|
||||
* The user's data is stored in the browser's localStorage. Thus, this function
|
||||
* only works from the main thread, not from web workers (local storage is not
|
||||
@@ -250,6 +280,44 @@ export const savedKeyAttributes = (): KeyAttributes | undefined => {
|
||||
export const ensureSavedKeyAttributes = (): KeyAttributes =>
|
||||
ensureExpectedLoggedInValue(savedKeyAttributes());
|
||||
|
||||
/**
|
||||
* Update or set the user's {@link KeyAttributes} on remote.
|
||||
*/
|
||||
export const putUserKeyAttributes = async (keyAttributes: KeyAttributes) =>
|
||||
ensureOk(
|
||||
await fetch(await apiURL("/users/attributes"), {
|
||||
method: "PUT",
|
||||
headers: await authenticatedRequestHeaders(),
|
||||
body: JSON.stringify({ keyAttributes }),
|
||||
}),
|
||||
);
|
||||
|
||||
export interface RecoveryKeyAttributes {
|
||||
masterKeyEncryptedWithRecoveryKey: string;
|
||||
masterKeyDecryptionNonce: string;
|
||||
recoveryKeyEncryptedWithMasterKey: string;
|
||||
recoveryKeyDecryptionNonce: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the encrypted recovery key attributes for the logged in user.
|
||||
*
|
||||
* In practice, this is not expected to be called and is meant as a rare
|
||||
* fallback for very old accounts created prior to recovery key related
|
||||
* attributes being assigned on account setup. Even for these, it'll be called
|
||||
* only once.
|
||||
*/
|
||||
export const putUserRecoveryKeyAttributes = async (
|
||||
recoveryKeyAttributes: RecoveryKeyAttributes,
|
||||
) =>
|
||||
ensureOk(
|
||||
await fetch(await apiURL("/users/recovery-key"), {
|
||||
method: "PUT",
|
||||
headers: await authenticatedRequestHeaders(),
|
||||
body: JSON.stringify(recoveryKeyAttributes),
|
||||
}),
|
||||
);
|
||||
|
||||
export interface UserVerificationResponse {
|
||||
id: number;
|
||||
keyAttributes?: KeyAttributes | undefined;
|
||||
@@ -274,33 +342,6 @@ export interface UserVerificationResponse {
|
||||
srpM2?: string | undefined;
|
||||
}
|
||||
|
||||
export interface TwoFactorVerificationResponse {
|
||||
id: number;
|
||||
keyAttributes: KeyAttributes;
|
||||
encryptedToken?: string;
|
||||
token?: string;
|
||||
}
|
||||
|
||||
const TwoFactorSecret = z.object({
|
||||
secretCode: z.string(),
|
||||
qrCode: z.string(),
|
||||
});
|
||||
|
||||
export type TwoFactorSecret = z.infer<typeof TwoFactorSecret>;
|
||||
|
||||
export interface TwoFactorRecoveryResponse {
|
||||
encryptedSecret: string;
|
||||
secretDecryptionNonce: string;
|
||||
}
|
||||
|
||||
export interface UpdatedKey {
|
||||
kekSalt: string;
|
||||
encryptedKey: string;
|
||||
keyDecryptionNonce: string;
|
||||
memLimit: number;
|
||||
opsLimit: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ask remote to send a OTP / OTT to the given email to verify that the user has
|
||||
* access to it. Subsequent the app will pass this OTT back via the
|
||||
@@ -387,34 +428,6 @@ export const EmailOrSRPAuthorizationResponse = z.object({
|
||||
srpM2: z.string().nullish().transform(nullToUndefined),
|
||||
});
|
||||
|
||||
/**
|
||||
* The result of a successful two factor verification (totp or passkey).
|
||||
*/
|
||||
export const TwoFactorAuthorizationResponse = z.object({
|
||||
id: z.number(),
|
||||
/** TODO: keyAttributes is guaranteed to be returned by museum, update the
|
||||
* types to reflect that. */
|
||||
keyAttributes: RemoteKeyAttributes.nullish().transform(nullToUndefined),
|
||||
/** TODO: encryptedToken is guaranteed to be returned by museum, update the
|
||||
* types to reflect that. */
|
||||
encryptedToken: z.string().nullish().transform(nullToUndefined),
|
||||
});
|
||||
|
||||
export type TwoFactorAuthorizationResponse = z.infer<
|
||||
typeof TwoFactorAuthorizationResponse
|
||||
>;
|
||||
|
||||
/**
|
||||
* Update or set the user's {@link KeyAttributes} on remote.
|
||||
*/
|
||||
export const putUserKeyAttributes = async (keyAttributes: KeyAttributes) =>
|
||||
ensureOk(
|
||||
await fetch(await apiURL("/users/attributes"), {
|
||||
method: "PUT",
|
||||
headers: await authenticatedRequestHeaders(),
|
||||
body: JSON.stringify({ keyAttributes }),
|
||||
}),
|
||||
);
|
||||
/**
|
||||
* Log the user out on remote, if possible and needed.
|
||||
*/
|
||||
@@ -441,32 +454,216 @@ export const remoteLogoutIfNeeded = async () => {
|
||||
ensureOk(res);
|
||||
};
|
||||
|
||||
export const verifyTwoFactor = async (code: string, sessionID: string) => {
|
||||
export interface UpdatedKey {
|
||||
kekSalt: string;
|
||||
encryptedKey: string;
|
||||
keyDecryptionNonce: string;
|
||||
memLimit: number;
|
||||
opsLimit: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Change the email associated with the user's account on remote.
|
||||
*
|
||||
* @param email The new email.
|
||||
*
|
||||
* @param ott The verification code that was sent to the new email.
|
||||
*/
|
||||
export const changeEmail = async (email: string, ott: string) =>
|
||||
ensureOk(
|
||||
await fetch(await apiURL("/users/change-email"), {
|
||||
method: "POST",
|
||||
headers: await authenticatedRequestHeaders(),
|
||||
body: JSON.stringify({ email, ott }),
|
||||
}),
|
||||
);
|
||||
|
||||
const TwoFactorSecret = z.object({
|
||||
/**
|
||||
* The 2FA secret code.
|
||||
*/
|
||||
secretCode: z.string(),
|
||||
/**
|
||||
* A base64 encoded "image/png".
|
||||
*/
|
||||
qrCode: z.string(),
|
||||
});
|
||||
|
||||
export type TwoFactorSecret = z.infer<typeof TwoFactorSecret>;
|
||||
|
||||
/**
|
||||
* Start a TOTP based two factor setup process by fetching a secret code (and
|
||||
* the corresponding QR code) from remote.
|
||||
*
|
||||
* Once the user provides us with a TOTP generated using the provided secret, we
|
||||
* can finish the setup with {@link setupTwoFactorFinish}.
|
||||
*/
|
||||
export const setupTwoFactor = async (): Promise<TwoFactorSecret> => {
|
||||
const res = await fetch(await apiURL("/users/two-factor/setup"), {
|
||||
method: "POST",
|
||||
headers: await authenticatedRequestHeaders(),
|
||||
});
|
||||
ensureOk(res);
|
||||
return TwoFactorSecret.parse(await res.json());
|
||||
};
|
||||
|
||||
/**
|
||||
* Finish the TOTP based two factor setup by provided a previously obtained
|
||||
* secret (using {@link setupTwoFactor}) and the current TOTP generated using
|
||||
* that secret.
|
||||
*
|
||||
* This updates both the state both locally and on remote.
|
||||
*
|
||||
* @param secretCode The value of {@link secretCode} from the
|
||||
* {@link TwoFactorSecret} obtained by {@link setupTwoFactor}.
|
||||
*
|
||||
* @param totp The current TOTP corresponding to {@link secretCode}.
|
||||
*/
|
||||
export const setupTwoFactorFinish = async (
|
||||
secretCode: string,
|
||||
totp: string,
|
||||
) => {
|
||||
const box = await encryptBox(secretCode, await getUserRecoveryKey());
|
||||
await enableTwoFactor({
|
||||
code: totp,
|
||||
encryptedTwoFactorSecret: box.encryptedData,
|
||||
twoFactorSecretDecryptionNonce: box.nonce,
|
||||
});
|
||||
await setLSUser({ ...getData("user"), isTwoFactorEnabled: true });
|
||||
};
|
||||
|
||||
interface EnableTwoFactorRequest {
|
||||
/**
|
||||
* The current value of the TOTP corresponding to the two factor {@link
|
||||
* secretCode} obtained from a previous call to {@link setupTwoFactor}.
|
||||
*/
|
||||
code: string;
|
||||
/**
|
||||
* The {@link secretCode} encrypted with the user's recovery key.
|
||||
*
|
||||
* This is used in the case of second factor recovery.
|
||||
*/
|
||||
encryptedTwoFactorSecret: string;
|
||||
/**
|
||||
* The nonce that was used when encrypting {@link encryptedTwoFactorSecret}.
|
||||
*/
|
||||
twoFactorSecretDecryptionNonce: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable the TOTP based two factor for the user by providing the current 2FA
|
||||
* code corresponding the two factor secret, and encrypted secrets for future
|
||||
* recovery (if needed).
|
||||
*/
|
||||
const enableTwoFactor = async (req: EnableTwoFactorRequest) =>
|
||||
ensureOk(
|
||||
await fetch(await apiURL("/users/two-factor/enable"), {
|
||||
method: "POST",
|
||||
headers: await authenticatedRequestHeaders(),
|
||||
body: JSON.stringify(req),
|
||||
}),
|
||||
);
|
||||
|
||||
/**
|
||||
* The result of a successful two factor verification (TOTP or passkey),
|
||||
* recovery removal (TOTP) or recovery bypass (passkey).
|
||||
*/
|
||||
export const TwoFactorAuthorizationResponse = z.object({
|
||||
/**
|
||||
* The user's ID.
|
||||
*/
|
||||
id: z.number(),
|
||||
/**
|
||||
* The user's key attributes.
|
||||
*/
|
||||
keyAttributes: RemoteKeyAttributes,
|
||||
/**
|
||||
* A encrypted auth token.
|
||||
*/
|
||||
encryptedToken: z.string(),
|
||||
});
|
||||
|
||||
export type TwoFactorAuthorizationResponse = z.infer<
|
||||
typeof TwoFactorAuthorizationResponse
|
||||
>;
|
||||
|
||||
export const verifyTwoFactor = async (
|
||||
code: string,
|
||||
sessionID: string,
|
||||
): Promise<TwoFactorAuthorizationResponse> => {
|
||||
const res = await fetch(await apiURL("/users/two-factor/verify"), {
|
||||
method: "POST",
|
||||
headers: publicRequestHeaders(),
|
||||
body: JSON.stringify({ code, sessionID }),
|
||||
});
|
||||
ensureOk(res);
|
||||
const json = await res.json();
|
||||
// TODO: Use zod here
|
||||
return json as UserVerificationResponse;
|
||||
return TwoFactorAuthorizationResponse.parse(await res.json());
|
||||
};
|
||||
|
||||
/** The type of the second factor we're trying to act on */
|
||||
export type TwoFactorType = "totp" | "passkey";
|
||||
|
||||
const TwoFactorRecoveryResponse = z.object({
|
||||
/**
|
||||
* The recovery secret, encrypted using the user's recovery key.
|
||||
*/
|
||||
encryptedSecret: z.string(),
|
||||
/**
|
||||
* The nonce used during encryption of {@link encryptedSecret}.
|
||||
*/
|
||||
secretDecryptionNonce: z.string(),
|
||||
});
|
||||
|
||||
type TwoFactorRecoveryResponse = z.infer<typeof TwoFactorRecoveryResponse>;
|
||||
|
||||
/**
|
||||
* Initiate second factor reset or bypass by requesting the encrypted second
|
||||
* factor recovery secret (and nonce) from remote. The user can then decrypt
|
||||
* these using their recovery key to reset or bypass their second factor.
|
||||
*
|
||||
* @param sessionID A two factor session ID ({@link twoFactorSessionID} or
|
||||
* {@link passkeySessionID}) for the user.
|
||||
*
|
||||
* @param twoFactorType The type of second factor to reset or bypass.
|
||||
*
|
||||
* [Note: Second factor recovery]
|
||||
*
|
||||
* 1. When setting up a TOTP based second factor, client sends a (encrypted 2fa
|
||||
* recovery secret, nonce) pair to remote. This is a randomly generated
|
||||
* secret (and nonce) encrypted using the user's recovery key.
|
||||
*
|
||||
* 2. Similarly, when setting up a passkey as the second factor, the client
|
||||
* sends a encrypted recovery secret (see {@link configurePasskeyRecovery}).
|
||||
*
|
||||
* 3. When the user wishes to reset or bypass their second factor, the client
|
||||
* asks remote for these encrypted secrets (using {@link recoverTwoFactor}).
|
||||
*
|
||||
* 4. User then enters their recovery key, which the client uses to decrypt the
|
||||
* recovery secret and provide it back to remote for verification (using
|
||||
* {@link removeTwoFactor}).
|
||||
*
|
||||
* 5. If the recovery secret matches, then remote resets (TOTP based) or bypass
|
||||
* (passkey based) the user's second factor.
|
||||
*/
|
||||
export const recoverTwoFactor = async (
|
||||
sessionID: string,
|
||||
twoFactorType: TwoFactorType,
|
||||
) => {
|
||||
const resp = await HTTPService.get(
|
||||
await apiURL("/users/two-factor/recover"),
|
||||
{ sessionID, twoFactorType },
|
||||
): Promise<TwoFactorRecoveryResponse> => {
|
||||
const res = await fetch(
|
||||
await apiURL("/users/two-factor/recover", { sessionID, twoFactorType }),
|
||||
{ headers: publicRequestHeaders() },
|
||||
);
|
||||
return resp.data as TwoFactorRecoveryResponse;
|
||||
ensureOk(res);
|
||||
return TwoFactorRecoveryResponse.parse(await res.json());
|
||||
};
|
||||
|
||||
export interface TwoFactorVerificationResponse {
|
||||
id: number;
|
||||
keyAttributes: KeyAttributes;
|
||||
encryptedToken?: string;
|
||||
token?: string;
|
||||
}
|
||||
|
||||
export const removeTwoFactor = async (
|
||||
sessionID: string,
|
||||
secret: string,
|
||||
@@ -478,70 +675,3 @@ export const removeTwoFactor = async (
|
||||
);
|
||||
return resp.data as TwoFactorVerificationResponse;
|
||||
};
|
||||
|
||||
export const changeEmail = async (email: string, ott: string) => {
|
||||
await HTTPService.post(
|
||||
await apiURL("/users/change-email"),
|
||||
{ email, ott },
|
||||
undefined,
|
||||
{ "X-Auth-Token": getToken() },
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Start the two factor setup process by fetching a secret code (and the
|
||||
* corresponding QR code) from remote.
|
||||
*/
|
||||
export const setupTwoFactor = async () => {
|
||||
const res = await fetch(await apiURL("/users/two-factor/setup"), {
|
||||
method: "POST",
|
||||
headers: await authenticatedRequestHeaders(),
|
||||
});
|
||||
ensureOk(res);
|
||||
return TwoFactorSecret.parse(await res.json());
|
||||
};
|
||||
|
||||
interface EnableTwoFactorRequest {
|
||||
code: string;
|
||||
encryptedTwoFactorSecret: string;
|
||||
twoFactorSecretDecryptionNonce: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable two factor for the user by providing the 2FA code and the encrypted
|
||||
* secret from a previous call to {@link setupTwoFactor}.
|
||||
*/
|
||||
export const enableTwoFactor = async (req: EnableTwoFactorRequest) =>
|
||||
ensureOk(
|
||||
await fetch(await apiURL("/users/two-factor/enable"), {
|
||||
method: "POST",
|
||||
headers: await authenticatedRequestHeaders(),
|
||||
body: JSON.stringify(req),
|
||||
}),
|
||||
);
|
||||
|
||||
export interface RecoveryKeyAttributes {
|
||||
masterKeyEncryptedWithRecoveryKey: string;
|
||||
masterKeyDecryptionNonce: string;
|
||||
recoveryKeyEncryptedWithMasterKey: string;
|
||||
recoveryKeyDecryptionNonce: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the encrypted recovery key attributes for the logged in user.
|
||||
*
|
||||
* In practice, this is not expected to be called and is meant as a rare
|
||||
* fallback for very old accounts created prior to recovery key related
|
||||
* attributes being assigned on account setup. Even for these, it'll be called
|
||||
* only once.
|
||||
*/
|
||||
export const putUserRecoveryKeyAttributes = async (
|
||||
recoveryKeyAttributes: RecoveryKeyAttributes,
|
||||
) =>
|
||||
ensureOk(
|
||||
await fetch(await apiURL("/users/recovery-key"), {
|
||||
method: "PUT",
|
||||
headers: await authenticatedRequestHeaders(),
|
||||
body: JSON.stringify(recoveryKeyAttributes),
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -263,3 +263,16 @@ export const deleteFromTrash = async (fileIDs: number[]) => {
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Delete the public link for the collection with given {@link collectionID}.
|
||||
*
|
||||
* Does not modify local state.
|
||||
*/
|
||||
export const deleteShareURL = async (collectionID: number) =>
|
||||
ensureOk(
|
||||
await fetch(await apiURL(`/collections/share-url/${collectionID}`), {
|
||||
method: "DELETE",
|
||||
headers: await authenticatedRequestHeaders(),
|
||||
}),
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user