diff --git a/src/apps/components/firstrun/FirstRun/Page/TOTPFailed.svelte b/src/apps/components/firstrun/FirstRun/Page/TOTPFailed.svelte new file mode 100755 index 000000000..8d2c1ff0a --- /dev/null +++ b/src/apps/components/firstrun/FirstRun/Page/TOTPFailed.svelte @@ -0,0 +1,9 @@ + + + +

2FA Setup Failed

+

Something went wrong when trying to spawn the 2FA setup overlay.

+
+

You can always try again later in the settings app under 'Your ArcOS Identity'.

diff --git a/src/apps/components/firstrun/FirstRun/Page/TOTPSetup.svelte b/src/apps/components/firstrun/FirstRun/Page/TOTPSetup.svelte new file mode 100755 index 000000000..1590f2f70 --- /dev/null +++ b/src/apps/components/firstrun/FirstRun/Page/TOTPSetup.svelte @@ -0,0 +1,7 @@ + + + +

Set-up 2FA?

+

Do you want to secure your ArcOS account by setting up 2FA using your authenticator app?

diff --git a/src/apps/components/firstrun/store.ts b/src/apps/components/firstrun/store.ts index 675ee6de6..56c2bcba6 100755 --- a/src/apps/components/firstrun/store.ts +++ b/src/apps/components/firstrun/store.ts @@ -1,12 +1,18 @@ -import { Daemon } from "$ts/env"; +import type { ITotpConnector } from "$interfaces/modules/server/ITotpConnector"; +import { Daemon, SoundBus } from "$ts/env"; import { UserPaths } from "$ts/user/store"; +import { MessageBox } from "$ts/util/dialog"; import { join } from "$ts/util/fs"; +import { ElevationLevel } from "$types/system/elevation"; import type { ArcShortcut } from "$types/system/shortcut"; +import type { TotpSetupGuiRuntime } from "../totpsetupgui/runtime"; import DisplayName from "./FirstRun/Page/DisplayName.svelte"; import Finish from "./FirstRun/Page/Finish.svelte"; import ProfilePicture from "./FirstRun/Page/ProfilePicture.svelte"; import Style from "./FirstRun/Page/Style.svelte"; import ThirdParty from "./FirstRun/Page/ThirdParty.svelte"; +import TOTPFailed from "./FirstRun/Page/TOTPFailed.svelte"; +import TOTPSetup from "./FirstRun/Page/TOTPSetup.svelte"; import Welcome from "./FirstRun/Page/Welcome.svelte"; import { DarkModeGraphic, LightModeGraphic } from "./images"; import type { FirstRunPage, FirstRunTheme } from "./types"; @@ -127,7 +133,7 @@ export const FirstRunPages = new Map([ right: [ { caption: "Not now", - action: (process) => process.switchPage("finish"), + action: (process) => process.switchPage("totpSetup"), }, { caption: "Enable", @@ -136,7 +142,46 @@ export const FirstRunPages = new Map([ v.security.enableThirdParty = true; return v; }); - process.switchPage("finish"); + process.switchPage("totpSetup"); + }, + suggested: true, + }, + ], + }, + }, + ], + [ + "totpSetup", + { + name: "Set-up 2FA?", + hero: true, + component: TOTPSetup, + actions: { + left: [ + { + caption: "Go back", + action: (process) => process.switchPage("thirdParty"), + }, + ], + right: [ + { + caption: "Skip", + action: (process) => process.switchPage("finish"), + }, + { + caption: "Set-up 2FA", + action: async (process) => { + const totpSetupProc = await process.spawnOverlayApp("TotpSetupGui", process.pid, true); + if (!totpSetupProc) { + process.switchPage("totpFailed"); + SoundBus.playSound("arcos.dialog.error"); + } + + totpSetupProc?.setupState.subscribe((v) => { + if (v === "successful") { + process.switchPage("finish"); + } + }); }, suggested: true, }, @@ -144,6 +189,30 @@ export const FirstRunPages = new Map([ }, }, ], + [ + "totpFailed", + { + name: "2FA Setup Failed", + hero: true, + component: TOTPFailed, + actions: { + left: [ + { + caption: "Go back", + action: (process) => process.switchPage("thirdParty"), + }, + ], + right: [ + { + caption: "Try again", + action: (process) => process.switchPage("totpSetup"), + }, + + { caption: "Finish later", action: (process) => process.switchPage("finish"), suggested: true }, + ], + }, + }, + ], [ "finish", { diff --git a/src/apps/components/totpsetupgui/TotpSetupGui.svelte b/src/apps/components/totpsetupgui/TotpSetupGui.svelte index a3b2b9992..4a428ac55 100755 --- a/src/apps/components/totpsetupgui/TotpSetupGui.svelte +++ b/src/apps/components/totpsetupgui/TotpSetupGui.svelte @@ -7,7 +7,7 @@ import Input from "./TotpSetupGui/Input.svelte"; const { process }: { process: ITotpSetupGuiRuntime } = $props(); - const { code, url } = process; + const { code, url, setupState, firstTimeSetup } = process; let errored = $state(false); let locked = $state(false); @@ -51,6 +51,28 @@ ); $code = ""; } else { + if (firstTimeSetup) { + await MessageBox( + { + title: "ArcOS Security", + message: "Two-factor authentication has now been enabled on your account.", + buttons: [ + { + caption: "Okay", + action: () => { + setupState.set("successful"); + }, + }, + ], + sound: "arcos.dialog.info", + image: "GoodStatusIcon", + }, + process.parentPid, + true + ); + return; + } + MessageBox( { title: "ArcOS Security", diff --git a/src/apps/components/totpsetupgui/runtime.ts b/src/apps/components/totpsetupgui/runtime.ts index 9a10dcb72..2fec658de 100755 --- a/src/apps/components/totpsetupgui/runtime.ts +++ b/src/apps/components/totpsetupgui/runtime.ts @@ -4,16 +4,21 @@ import { AppProcess } from "$ts/apps/process"; import { Daemon } from "$ts/env"; import { Store } from "$ts/writable"; import type { AppProcessData } from "$types/apps/app"; +import type { SetupState } from "./types"; export class TotpSetupGuiRuntime extends AppProcess implements ITotpSetupGuiRuntime { public code = Store(""); public url = Store(""); + public setupState = Store("unfinished"); + public firstTimeSetup: boolean; //#region LIFECYCLE - constructor(pid: number, parentPid: number, app: AppProcessData) { + constructor(pid: number, parentPid: number, app: AppProcessData, firstTimeSetup: boolean = false) { super(pid, parentPid, app); + this.firstTimeSetup = firstTimeSetup; + this.setSource(__SOURCE__); } @@ -27,6 +32,12 @@ export class TotpSetupGuiRuntime extends AppProcess implements ITotpSetupGuiRunt this.url.set(result.result!.url); } + async onClose(): Promise { + if (this.setupState() === "unfinished") this.setupState.set("canceled"); + + return true; + } + //#endregion //#region ACCESS diff --git a/src/apps/components/totpsetupgui/types.ts b/src/apps/components/totpsetupgui/types.ts new file mode 100644 index 000000000..a81af96ad --- /dev/null +++ b/src/apps/components/totpsetupgui/types.ts @@ -0,0 +1 @@ +export type SetupState = "unfinished" | "canceled" | "successful"; diff --git a/src/apps/core/newloginapp/runtime.ts b/src/apps/core/newloginapp/runtime.ts index 1ba59437c..25df0ec0e 100644 --- a/src/apps/core/newloginapp/runtime.ts +++ b/src/apps/core/newloginapp/runtime.ts @@ -132,7 +132,7 @@ export class NewLoginAppRuntime extends AppProcess implements INewLoginAppRuntim this.Persistence.update((v) => { const userConnector = Server.GetConn("UserConnector", ""); const persistence: LoginPersistenceUser = { - displayName: info.preferences.account.displayName ?? info.username, + displayName: info.preferences?.account?.displayName ?? info.username, username: info.username, userId: info._id, profilePictureUrl: userConnector.PictureUrl(info._id), diff --git a/src/interfaces/runtimes/ITotpSetupGuiRuntime.ts b/src/interfaces/runtimes/ITotpSetupGuiRuntime.ts index 0efafa313..61405eee0 100644 --- a/src/interfaces/runtimes/ITotpSetupGuiRuntime.ts +++ b/src/interfaces/runtimes/ITotpSetupGuiRuntime.ts @@ -1,3 +1,4 @@ +import type { SetupState } from "$apps/components/totpsetupgui/types"; import type { IAppProcess } from "$interfaces/IAppProcess"; import type { ReadableStore } from "$types/shared/writable"; @@ -5,6 +6,8 @@ import type { ReadableStore } from "$types/shared/writable"; export interface ITotpSetupGuiRuntime extends IAppProcess { code: ReadableStore; url: ReadableStore; + setupState: ReadableStore; + firstTimeSetup: boolean; validate(): boolean; activateTotp(): Promise;