diff --git a/src/frontend/src/flows/manage/index.ts b/src/frontend/src/flows/manage/index.ts index 6466d3bda2..d90265b765 100644 --- a/src/frontend/src/flows/manage/index.ts +++ b/src/frontend/src/flows/manage/index.ts @@ -2,7 +2,7 @@ import { TemplateResult, render, html } from "lit-html"; import { LEGACY_II_URL } from "../../config"; import { Connection, AuthenticatedConnection } from "../../utils/iiConnection"; import { withLoader } from "../../components/loader"; -import { unreachable } from "../../utils/utils"; +import { unreachable, unknownToString } from "../../utils/utils"; import { logoutSection } from "../../components/logout"; import { deviceSettings } from "./deviceSettings"; import { showWarning } from "../../banner"; @@ -28,6 +28,16 @@ import { recoveryDeviceToLabel, } from "../../utils/recoveryDevice"; +// A simple representation of "device"s used on the manage page. +export type Device = { + // Open the settings screen for that particular device + openSettings: () => Promise; + // The displayed name of a device (not exactly the "alias") because + // recovery devices handle aliases differently. + label: string; + isRecovery: boolean; +}; + /* Template for the authbox when authenticating to II */ export const authnTemplateManage = (): AuthnTemplates => { const wrap = ({ @@ -94,8 +104,6 @@ const displayFailedToListDevices = (error: Error) => // and we (the frontend) only allow user one recovery device per type (phrase, fob), // which leaves room for 8 authenticator devices. const MAX_AUTHENTICATORS = 8; -const numAuthenticators = (devices: DeviceData[]) => - devices.filter((device) => "authentication" in device.purpose).length; // Actual page content. We display the Identity Anchor and the list of // (non-recovery) devices. Additionally, if the user does _not_ have any @@ -105,12 +113,14 @@ const numAuthenticators = (devices: DeviceData[]) => // recovery devices. const pageContent = ({ userNumber, - devices, + authenticators, + recoveries, onAddDevice, onAddRecovery, }: { userNumber: bigint; - devices: DeviceData[]; + authenticators: Device[]; + recoveries: Device[]; onAddDevice: (next: "canceled" | "local" | "remote") => void; onAddRecovery: () => void; }): TemplateResult => { @@ -121,9 +131,10 @@ const pageContent = ({ Add devices and recovery methods to make your anchor more secure.

- ${anchorSection(userNumber)} ${devicesSection(devices, onAddDevice)} - ${!hasRecoveryDevice(devices) ? recoveryNag({ onAddRecovery }) : undefined} - ${recoverySection(devices, onAddRecovery)} ${logoutSection()} + ${anchorSection(userNumber)} + ${devicesSection({ authenticators, onAddDevice })} + ${recoveries.length === 0 ? recoveryNag({ onAddRecovery }) : undefined} + ${recoverySection({ recoveries, onAddRecovery })} ${logoutSection()} `; return mainWindow({ @@ -143,12 +154,16 @@ const anchorSection = (userNumber: bigint): TemplateResult => html` `; -const devicesSection = ( - devices: DeviceData[], - onAddDevice: (next: "canceled" | "local" | "remote") => void -): TemplateResult => { +// The regular, "authenticator" devices +const devicesSection = ({ + authenticators, + onAddDevice, +}: { + authenticators: Device[]; + onAddDevice: (next: "canceled" | "local" | "remote") => void; +}): TemplateResult => { const wrapClasses = ["l-stack"]; - const isWarning = devices.length < 2; + const isWarning = authenticators.length < 2; if (isWarning === true) { wrapClasses.push("c-card", "c-card--narrow", "c-card--warning"); @@ -170,7 +185,7 @@ const devicesSection = ( You can register up to ${MAX_AUTHENTICATORS} authenticator devices (recovery devices excluded) - (${numAuthenticators(devices)}/${MAX_AUTHENTICATORS}) + (${authenticators.length}/${MAX_AUTHENTICATORS}) @@ -184,10 +199,21 @@ const devicesSection = ( }
-
+
+
    + ${authenticators.map((device) => { + return html` +
  • + ${deviceListItem({ + device, + })} +
  • + `; + })}
+
- @@ -293,9 +332,33 @@ export const displayManage = ( devices: DeviceData[] ): void => { const container = document.getElementById("pageContent") as HTMLElement; + const hasSingleDevice = devices.length <= 1; + + const _devices = devices.map((device) => ({ + openSettings: async () => { + try { + await deviceSettings(userNumber, connection, device, hasSingleDevice); + } catch (e: unknown) { + await displayError({ + title: "Could not edit device", + message: "An error happened on the settings page.", + detail: unknownToString(e, "unknown error"), + primaryButton: "Ok", + }); + } + + await renderManage(userNumber, connection); + }, + label: isRecoveryDevice(device) + ? recoveryDeviceToLabel(device) + : device.alias, + isRecovery: isRecoveryDevice(device), + })); + const template = pageContent({ userNumber, - devices, + authenticators: _devices.filter((device) => !device.isRecovery), + recoveries: _devices.filter((device) => device.isRecovery), onAddDevice: async (nextAction) => { switch (nextAction) { case "canceled": { @@ -340,66 +403,8 @@ export const displayManage = ( } render(template, container); - renderDevices(userNumber, connection, devices); -}; - -const renderDevices = async ( - userNumber: bigint, - connection: AuthenticatedConnection, - devices: DeviceData[] -) => { - const list = document.createElement("ul"); - const recoveryList = document.createElement("ul"); - const isOnlyDevice = devices.length < 2; - - devices.forEach((device) => { - const identityElement = document.createElement("li"); - identityElement.className = "c-action-list__item"; - - render(deviceListItem(device), identityElement); - const buttonSettings = identityElement.querySelector( - "button[data-action=settings]" - ) as HTMLButtonElement; - if (buttonSettings !== null) { - buttonSettings.onclick = async () => { - await deviceSettings( - userNumber, - connection, - device, - isOnlyDevice - ).catch((e) => - displayError({ - title: "Could not edit device", - message: "An error happened on the settings page.", - detail: e.toString(), - primaryButton: "Ok", - }) - ); - await renderManage(userNumber, connection); - }; - } - "recovery" in device.purpose - ? recoveryList.appendChild(identityElement) - : list.appendChild(identityElement); - }); - const deviceList = document.getElementById("deviceList") as HTMLElement; - deviceList.innerHTML = ``; - deviceList.appendChild(list); - - const recoveryDevices = document.getElementById( - "recoveryList" - ) as HTMLElement; - - if (recoveryDevices !== null) { - recoveryDevices.innerHTML = ``; - recoveryDevices.appendChild(recoveryList); - } }; -// Whether or the user has registered a device as recovery -const hasRecoveryDevice = (devices: DeviceData[]): boolean => - devices.some((device) => "recovery" in device.purpose); - // Whether the user has a recovery phrase or not const hasRecoveryPhrase = (devices: DeviceData[]): boolean => devices.some((device) => device.alias === "Recovery phrase");