Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions src/apps/components/pswdresetwizard/Pages/EnterUsername.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<script lang="ts">
import type { IPswdResetWizardRuntime } from "$interfaces/runtimes/IPswdResetWizardRuntime";

let { process }: { process: IPswdResetWizardRuntime } = $props();
const { RecoveryUsername, ErrorMessage } = process;
</script>

<p class="instruction">
First, we need your username. To verify that it’s you, we’ll send an email with a verification code to the email address of your
account.
</p>

<div class="field-wrapper">
<h2>Enter your username:</h2>
<div class="field">
<span class="lucide icon-user"></span>
<input type="text" placeholder="Username" bind:value={$RecoveryUsername} />
</div>
{#if $ErrorMessage}
<div class="error-message">
<span class="lucide icon-circle-alert"></span>
<span>{$ErrorMessage}</span>
</div>
{/if}
</div>

<p class="help">
If you forgot your username, please talk to an Administrator in the ArcOS Discord server, and we'll help you recover your
account.
</p>
25 changes: 25 additions & 0 deletions src/apps/components/pswdresetwizard/Pages/NewPassword.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<script lang="ts">
import type { IPswdResetWizardRuntime } from "$interfaces/runtimes/IPswdResetWizardRuntime";

let { process }: { process: IPswdResetWizardRuntime } = $props();
const { RecoveryUsername, NewPassword, ErrorMessage } = process;
</script>

<p class="instruction">
We’re almost there, {$RecoveryUsername}! To finish off, think of a strong password for your ArcOS account. Save it in your password
manager so that you don’t lose it.
</p>

<div class="field-wrapper">
<h2>Enter a new password:</h2>
<div class="field">
<span class="lucide icon-key-round"></span>
<input type="password" placeholder="Password" bind:value={$NewPassword} />
</div>
{#if $ErrorMessage}
<div class="error-message">
<span class="lucide icon-circle-alert"></span>
<span>{$ErrorMessage}</span>
</div>
{/if}
</div>
11 changes: 11 additions & 0 deletions src/apps/components/pswdresetwizard/Pages/RestartFirst.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<div class="warning-message">
<span class="lucide icon-triangle-alert"></span>
<span>You have to restart ArcOS first.</span>
</div>

<p class="instruction">
An ArcOS session was active before requesting a password reset. For your safety, please restart ArcOS before proceeding. This is
to prevent residual third-party code from that session from potentially stealing your account information.
</p>

<p class="help">After restarting, click the <b>Forgot password?</b> link again to continue.</p>
28 changes: 28 additions & 0 deletions src/apps/components/pswdresetwizard/Pages/Verification.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<script lang="ts">
import type { IPswdResetWizardRuntime } from "$interfaces/runtimes/IPswdResetWizardRuntime";

let { process }: { process: IPswdResetWizardRuntime } = $props();
const { RecoveryCode, ErrorMessage } = process;
</script>

<p class="instruction">
We’ve sent a verification code to the email address associated with your account. Enter the code in the field below to continue:
</p>

<div class="field-wrapper">
<h2>Verification code:</h2>
<div class="field verification-code">
<input type="text" placeholder="XXXXXX" bind:value={$RecoveryCode} />
</div>
{#if $ErrorMessage}
<div class="error-message">
<span class="lucide icon-circle-alert"></span>
<span>{$ErrorMessage}</span>
</div>
{/if}
</div>

<p class="help">
The verification code is valid for 15 minutes. After this period has elapsed, you can request another verification code. Only
one code may be requested at a time.
</p>
39 changes: 39 additions & 0 deletions src/apps/components/pswdresetwizard/PswdResetWizard.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<script lang="ts">
import type { IPswdResetWizardRuntime } from "$interfaces/runtimes/IPswdResetWizardRuntime";
import Spinner from "$lib/Spinner.svelte";
import ActionBar from "$lib/Window/ActionBar.svelte";
import ActionButton from "$lib/Window/ActionBar/ActionButton.svelte";

let { process }: { process: IPswdResetWizardRuntime } = $props();

const { CurrentPage, Loading } = process;
const Component = $derived($CurrentPage?.component);
</script>

<div class="top">
<div class="header">
<h1>Account Recovery</h1>
<p>Follow these steps to reset your password.</p>
</div>
<div class="page">
{#if Component}
<Component {process} />
{/if}
</div>
{#if $Loading}
<div class="loading-overlay">
<Spinner height={32} />
</div>
{/if}
</div>
<ActionBar>
{#snippet rightContent()}
{#if $CurrentPage}
{#each $CurrentPage.buttons as button (button.caption)}
<ActionButton suggested={button.suggested} disabled={button.disabled || $Loading} onclick={() => button.action()}>
{button.caption}
</ActionButton>
{/each}
{/if}
{/snippet}
</ActionBar>
45 changes: 45 additions & 0 deletions src/apps/components/pswdresetwizard/PswdResetWizardApp.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import "$css/apps/components/pswdresetwizard.css";
import type { App } from "$types/apps/app";
import PswdResetWizard from "./PswdResetWizard.svelte";
import { PswdResetWizardRuntime } from "./runtime";

const PswdResetWizardApp: App = {
metadata: {
name: "Password reset wizard",
author: "Izaak Kuipers",
version: "1.0.0",
icon: "ComponentIcon",
},
position: { centered: true },
size: {
w: 480,
h: 400,
},
minSize: {
w: 480,
h: 400,
},
maxSize: {
w: 480,
h: 400,
},
controls: {
minimize: false,
maximize: false,
close: true,
},
state: {
minimized: false,
maximized: false,
resizable: false,
headless: true,
fullscreen: false,
},
assets: {
runtime: PswdResetWizardRuntime,
component: PswdResetWizard as any,
},
id: "PswdResetWizardApp",
};

export default PswdResetWizardApp;
141 changes: 141 additions & 0 deletions src/apps/components/pswdresetwizard/runtime.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
import type { IUserConnector } from "$interfaces/modules/server/IUserConnector";
import type { INewLoginAppRuntime } from "$interfaces/runtimes/INewLoginAppRuntime";
import type { IPswdResetWizardRuntime } from "$interfaces/runtimes/IPswdResetWizardRuntime";
import { AppProcess } from "$ts/apps/process";
import { Daemon, Env, GetConnector, Kernel, Stack } from "$ts/env";
import { GoodStatusIcon } from "$ts/images/status";
import { checkPasswordStrength } from "$ts/util";
import { MessageBox } from "$ts/util/dialog";
import { Store } from "$ts/writable";
import type { AppProcessData } from "$types/apps/app";
import { PasswordStrengthCaptions, type PasswordStrength } from "$types/user";
import { PswdResetPages } from "./store";
import type { PswdResetPage } from "./types";

export class PswdResetWizardRuntime extends AppProcess implements IPswdResetWizardRuntime {
public RecoveryUsername = Store<string>("");
public RecoveryCode = Store<string>("");
public NewPassword = Store<string>("");

public Loading = Store<boolean>(false);
public CurrentPage = Store<PswdResetPage | undefined>();
public ErrorMessage = Store<string>("");

// for callback to login
public Finished = Store<boolean>(false);

public get LoginRuntime(): INewLoginAppRuntime {
const proc = Stack.getProcess<INewLoginAppRuntime>(+Env.get("LOGINAPP_PID"));
if (!proc) throw new Error("Reference to NewLoginAppRuntime could not be obtained");

return proc;
}

async onClose(): Promise<boolean> {
this.Finished.set(true);
return true;
}

private RecoveryUserId?: string;
private ResetToken?: string;
private Pages = PswdResetPages(this);
private get UserConnector(): IUserConnector {
return GetConnector<IUserConnector>("UserConnector");
}

constructor(pid: number, parentPid: number, app: AppProcessData) {
super(pid, parentPid, app);

if (Daemon && !Daemon?._disposed) throw new Error("PswdResetWizard illegal invocation");
}

protected async start(): Promise<any> {
const daemonHasRun = !!Kernel.Logs.find((log) => log.source === "UserDaemon.Hello" && log.message === "HELLO!");

this.SwitchPage(daemonHasRun ? "restartFirst" : "enterUsername");

this.Loading.subscribe((v) => {
if (v) this.ErrorMessage.set("");
});
}

public async DoSendEmail() {
if (!this.RecoveryUsername()) return;

this.Loading.set(true);
const result = await this.UserConnector.CreatePswdResetRequest(this.RecoveryUsername());
this.Loading.set(false);

if (!result.success) {
this.ErrorMessage.set(result.errorMessage ?? "An unknown error has occurred. Please talk to an administrator.");
return;
}

this.RecoveryUserId = result.result?.userId;
this.SwitchPage("verification");
}

public async DoVerify() {
if (!this.RecoveryCode()) return;

this.Loading.set(true);
const result = await this.UserConnector.VerifyPswdResetRequest(this.RecoveryUserId!, this.RecoveryCode());
this.Loading.set(false);

if (!result.success) {
this.ErrorMessage.set(result.errorMessage ?? "An unknown error has occurred. Please talk to an administrator.");
return;
}

this.RecoveryUserId = result.result?.userId;
this.ResetToken = result.result?.resetToken;
this.SwitchPage("newPassword");
}

public async DoChangePassword() {
const password = this.NewPassword();
if (!password) return;

const passwordStrength = checkPasswordStrength(password).value as PasswordStrength;
if (passwordStrength === "tooWeak") {
this.ErrorMessage.set(`Password is ${PasswordStrengthCaptions[passwordStrength]}!`);
return;
}

this.Loading.set(true);
const result = await this.UserConnector.ConsumePswdResetRequest(this.RecoveryUserId!, this.ResetToken!, this.NewPassword());
this.Loading.set(false);

if (!result.success) {
this.ErrorMessage.set(result.errorMessage ?? "An unknown error has occurred. Please talk to an administrator.");
return;
}

MessageBox(
{
title: "Password changed",
message: "Good news! Your ArcOS account password has been changed. Click Okay to return to the login screen.",
buttons: [
{
caption: "Okay",
action: () => {
this.closeWindow();
},
suggested: true,
},
],
sound: "arcos.dialog.info",
image: GoodStatusIcon, // no icon service here
},
this.pid,
true
);
}

public SwitchPage(pageId: string) {
if (!this.Pages[pageId]) return;

this.ErrorMessage.set("");
this.CurrentPage.set(this.Pages[pageId]);
}
}
Loading
Loading