diff --git a/package.json b/package.json index 68c820a12..a9e2ac4c2 100755 --- a/package.json +++ b/package.json @@ -47,6 +47,7 @@ "@types/node": "^22.10.5", "@types/sql.js": "^1.4.9", "@types/string-width": "^4.0.1", + "@types/ua-parser-js": "^0.7.39", "@types/validator": "^13.12.2", "@xterm/addon-clipboard": "^0.1.0", "@xterm/addon-fit": "^0.10.0", @@ -84,7 +85,7 @@ "svelte-image-viewer": "^7.1.0", "svelte-markdown": "^0.4.1", "svelte-splitpanes": "^8.0.9", - "ua-parser-js": "^2.0.3", + "ua-parser-js": "1.0.41", "validator": "^13.12.0", "vite-plugin-no-bundle": "^4.0.0", "xterm": "^5.3.0", diff --git a/src/apps/admin/mailbrokermanagerapp/MailbrokerManager.svelte b/src/apps/admin/mailbrokermanagerapp/MailbrokerManager.svelte new file mode 100644 index 000000000..9ce1a8209 --- /dev/null +++ b/src/apps/admin/mailbrokermanagerapp/MailbrokerManager.svelte @@ -0,0 +1,57 @@ + + + +
+ +
+ {#if loading} + + {:else if Page && data?.success} + {@const result = data.result!} + + {:else} +
+
+ +
+

The page failed to load

+

{data?.errorMessage ?? "An unknown error occurred."}

+
+
+
+ {/if} +
+
diff --git a/src/apps/admin/mailbrokermanagerapp/MailbrokerManager/SentRecordPills.svelte b/src/apps/admin/mailbrokermanagerapp/MailbrokerManager/SentRecordPills.svelte new file mode 100644 index 000000000..ed9f06918 --- /dev/null +++ b/src/apps/admin/mailbrokermanagerapp/MailbrokerManager/SentRecordPills.svelte @@ -0,0 +1,23 @@ + + +
+
+ + {formatBytes(sentRecord.htmlContent.length)} +
+
+ + {formatBytes(sentRecord.textContent.length)} +
+ {#if sentRecord.to.username && sentRecord.to.serverName} +
+ + To a user +
+ {/if} +
diff --git a/src/apps/admin/mailbrokermanagerapp/MailbrokerManager/ServerKeyPills.svelte b/src/apps/admin/mailbrokermanagerapp/MailbrokerManager/ServerKeyPills.svelte new file mode 100644 index 000000000..1f3128d23 --- /dev/null +++ b/src/apps/admin/mailbrokermanagerapp/MailbrokerManager/ServerKeyPills.svelte @@ -0,0 +1,24 @@ + + +
+ {#if disabled} +
+ Disabled +
+ {:else} +
+ Enabled +
+ {/if} + {#if serverKey.serverName === "arcapi.nl"} +
Master key
+ {/if} + {#if Server.hostname === serverKey.serverName} +
This server
+ {/if} +
diff --git a/src/apps/admin/mailbrokermanagerapp/MailbrokerManager/Sidebar.svelte b/src/apps/admin/mailbrokermanagerapp/MailbrokerManager/Sidebar.svelte new file mode 100644 index 000000000..0f37a2d60 --- /dev/null +++ b/src/apps/admin/mailbrokermanagerapp/MailbrokerManager/Sidebar.svelte @@ -0,0 +1,24 @@ + + +
+ +
diff --git a/src/apps/admin/mailbrokermanagerapp/MailbrokerManager/TemplateListing.svelte b/src/apps/admin/mailbrokermanagerapp/MailbrokerManager/TemplateListing.svelte new file mode 100644 index 000000000..191f2e724 --- /dev/null +++ b/src/apps/admin/mailbrokermanagerapp/MailbrokerManager/TemplateListing.svelte @@ -0,0 +1,76 @@ + + +
+
+

{filteredTemplates.length} {Plural("template", filteredTemplates.length)}

+ + {#if !hideNewButton && process.admin.canAccess(AdminScopes.adminMailbrokerTemplatesWrite)} + + {/if} +
+ +
+ {#each filteredTemplates as template (template._id)} + + {/each} + + {#if !filteredTemplates.length} +

+ There are no templates that match the criteria. +

+ {/if} +
+
diff --git a/src/apps/admin/mailbrokermanagerapp/MailbrokerManager/TemplateListing/ListingOption.svelte b/src/apps/admin/mailbrokermanagerapp/MailbrokerManager/TemplateListing/ListingOption.svelte new file mode 100644 index 000000000..2dccedcbb --- /dev/null +++ b/src/apps/admin/mailbrokermanagerapp/MailbrokerManager/TemplateListing/ListingOption.svelte @@ -0,0 +1,20 @@ + + + diff --git a/src/apps/admin/mailbrokermanagerapp/MailbrokerManager/TemplatePills.svelte b/src/apps/admin/mailbrokermanagerapp/MailbrokerManager/TemplatePills.svelte new file mode 100644 index 000000000..47109e684 --- /dev/null +++ b/src/apps/admin/mailbrokermanagerapp/MailbrokerManager/TemplatePills.svelte @@ -0,0 +1,20 @@ + + +
+
+ + {formatBytes(template.htmlContent.length)} +
+
+ + {formatBytes(template.textContent.length)} +
+ {#if deprecated} +
Deprecated
+ {/if} +
diff --git a/src/apps/admin/mailbrokermanagerapp/MailbrokerManagerApp.ts b/src/apps/admin/mailbrokermanagerapp/MailbrokerManagerApp.ts new file mode 100644 index 000000000..781fb6a91 --- /dev/null +++ b/src/apps/admin/mailbrokermanagerapp/MailbrokerManagerApp.ts @@ -0,0 +1,44 @@ +import "$css/apps/admin/mailbroker.css"; +import type { App } from "$types/apps/app"; +import MailbrokerManager from "./MailbrokerManager.svelte"; +import { MailbrokerManagerRuntime } from "./runtime"; + +const MailbrokerManagerApp: App = { + metadata: { + name: "Mailbroker Manager", + version: "1.0.0", + author: "Izaak Kuipers", + appGroup: "adminTools", + icon: "MailbrokerAdminIcon", + }, + position: { centered: true }, + size: { w: 800, h: 480 }, + minSize: { w: 800, h: 480 }, + maxSize: { w: NaN, h: NaN }, + state: { + maximized: false, + minimized: false, + resizable: true, + fullscreen: false, + headless: true, + }, + controls: { + minimize: true, + maximize: true, + close: true, + }, + assets: { + runtime: MailbrokerManagerRuntime, + component: MailbrokerManager as any, + }, + glass: true, + elevated: true, + hidden: false, + core: false, + overlay: false, + noSafeMode: false, + vital: true, + id: "MailbrokerManagerApp", +}; + +export default MailbrokerManagerApp; diff --git a/src/apps/admin/mailbrokermanagerapp/Overlays/NewKeyOverlay/NewKey.svelte b/src/apps/admin/mailbrokermanagerapp/Overlays/NewKeyOverlay/NewKey.svelte new file mode 100644 index 000000000..ae97c6570 --- /dev/null +++ b/src/apps/admin/mailbrokermanagerapp/Overlays/NewKeyOverlay/NewKey.svelte @@ -0,0 +1,49 @@ + + +
+ +
+

New server key

+

Enter the name of the server for which you want to create the server key:

+ +
+
+ + + {#snippet rightContent()} + process.closeWindow()}>Cancel + Create + {/snippet} + diff --git a/src/apps/admin/mailbrokermanagerapp/Overlays/NewKeyOverlay/NewKeyOverlay.ts b/src/apps/admin/mailbrokermanagerapp/Overlays/NewKeyOverlay/NewKeyOverlay.ts new file mode 100644 index 000000000..9198073ff --- /dev/null +++ b/src/apps/admin/mailbrokermanagerapp/Overlays/NewKeyOverlay/NewKeyOverlay.ts @@ -0,0 +1,45 @@ +import type { App } from "$types/apps/app"; +import NewKey from "./NewKey.svelte"; +import { MailbrokerNewKeyOverlayRuntime } from "./runtime"; + +const NewKeyOverlay: App = { + metadata: { + name: "New mailbroker key", + author: "Izaak Kuipers", + version: "1.0.0", + icon: "MailbrokerAdminIcon", + }, + position: { centered: true }, + size: { + w: 380, + h: 200, + }, + minSize: { + w: 380, + h: 200, + }, + maxSize: { + w: 380, + h: 200, + }, + controls: { + minimize: false, + maximize: false, + close: true, + }, + state: { + resizable: false, + headless: true, + fullscreen: false, + maximized: false, + minimized: false, + }, + assets: { + runtime: MailbrokerNewKeyOverlayRuntime, + component: NewKey as any, + }, + hidden: true, + id: "MailbrokerManagerApp_NewKeyOverlay", +}; + +export default NewKeyOverlay; diff --git a/src/apps/admin/mailbrokermanagerapp/Overlays/NewKeyOverlay/runtime.ts b/src/apps/admin/mailbrokermanagerapp/Overlays/NewKeyOverlay/runtime.ts new file mode 100644 index 000000000..4c958dfbc --- /dev/null +++ b/src/apps/admin/mailbrokermanagerapp/Overlays/NewKeyOverlay/runtime.ts @@ -0,0 +1,19 @@ +import type { IMailbrokerManagerRuntime, IMailbrokerNewKeyOverlayRuntime } from "$interfaces/runtimes/IMailbrokerRuntime"; +import type { IAdminBootstrapper } from "$interfaces/services/IAdminBootstrapper"; +import { AppProcess } from "$ts/apps/process"; +import { Daemon, Stack } from "$ts/env"; +import type { AppProcessData } from "$types/apps/app"; + +export class MailbrokerNewKeyOverlayRuntime extends AppProcess implements IMailbrokerNewKeyOverlayRuntime { + get admin() { + return Daemon.serviceHost?.getService("AdminBootstrapper")!; + } + + get parent() { + return Stack.getProcess(this.parentPid)!; + } + + constructor(pid: number, parentPid: number, app: AppProcessData) { + super(pid, parentPid, app); + } +} diff --git a/src/apps/admin/mailbrokermanagerapp/Overlays/SendOverlay/SendOverlay.svelte b/src/apps/admin/mailbrokermanagerapp/Overlays/SendOverlay/SendOverlay.svelte new file mode 100644 index 000000000..a3e2955bb --- /dev/null +++ b/src/apps/admin/mailbrokermanagerapp/Overlays/SendOverlay/SendOverlay.svelte @@ -0,0 +1,103 @@ + + +
+ + +
+ +
+ {#each process.properties as property (property.name)} +
+

{property.name}

+
+ {#if property.isUser} + {#if property.variant === MailbrokerSendOverlay.PropertyVariant.UserId} + + + {:else if property.variant === MailbrokerSendOverlay.PropertyVariant.Username} + + + {/if} + {:else if property.variant === MailbrokerSendOverlay.PropertyVariant.Url} + + + {:else} + + + {/if} +
+
+ {/each} +
+ + + {#snippet leftContent()} + {#if process.template?.deprecated} + Deprecated template + {/if} + {/snippet} + {#snippet rightContent()} + process.closeWindow()}>Cancel + Send + {/snippet} + diff --git a/src/apps/admin/mailbrokermanagerapp/Overlays/SendOverlay/SendOverlay.ts b/src/apps/admin/mailbrokermanagerapp/Overlays/SendOverlay/SendOverlay.ts new file mode 100644 index 000000000..553239f52 --- /dev/null +++ b/src/apps/admin/mailbrokermanagerapp/Overlays/SendOverlay/SendOverlay.ts @@ -0,0 +1,45 @@ +import type { App } from "$types/apps/app"; +import { MailbrokerSendOverlayRuntime } from "./runtime"; +import SendOverlaySvelte from "./SendOverlay.svelte"; + +const SendOverlay: App = { + metadata: { + name: "Send mailbroker template", + author: "Izaak Kuipers", + version: "1.0.0", + icon: "MailbrokerAdminIcon", + }, + position: { centered: true }, + size: { + w: 450, + h: 300, + }, + minSize: { + w: 450, + h: 300, + }, + maxSize: { + w: 450, + h: 300, + }, + controls: { + minimize: false, + maximize: false, + close: true, + }, + state: { + resizable: false, + headless: true, + fullscreen: false, + maximized: false, + minimized: false, + }, + assets: { + runtime: MailbrokerSendOverlayRuntime, + component: SendOverlaySvelte as any, + }, + hidden: true, + id: "MailbrokerManagerApp_SendOverlay", +}; + +export default SendOverlay; \ No newline at end of file diff --git a/src/apps/admin/mailbrokermanagerapp/Overlays/SendOverlay/runtime.ts b/src/apps/admin/mailbrokermanagerapp/Overlays/SendOverlay/runtime.ts new file mode 100644 index 000000000..e9d0c08be --- /dev/null +++ b/src/apps/admin/mailbrokermanagerapp/Overlays/SendOverlay/runtime.ts @@ -0,0 +1,92 @@ +import type { IMailbrokerSendOverlayRuntime } from "$interfaces/runtimes/IMailbrokerRuntime"; +import type { IAdminBootstrapper } from "$interfaces/services/IAdminBootstrapper"; +import { AppProcess } from "$ts/apps/process"; +import { Daemon } from "$ts/env"; +import { BTN_OKAY_SUG, MessageBox } from "$ts/util/dialog"; +import type { AppProcessData } from "$types/apps/app"; +import type { User } from "$types/server/admin"; +import type { Mailbroker } from "$types/server/mailbroker"; +import type { ExpandedUserInfo } from "$types/user"; +import { MailbrokerSendOverlay } from "./types"; + +export class MailbrokerSendOverlayRuntime extends AppProcess implements IMailbrokerSendOverlayRuntime { + public template?: Mailbroker.MailTemplate; + public templateId: string; + public properties: MailbrokerSendOverlay.Property[] = []; + public users: ExpandedUserInfo[] = []; + + get admin() { + return Daemon.serviceHost?.getService("AdminBootstrapper")!; + } + + constructor(pid: number, parentPid: number, app: AppProcessData, templateId: string) { + super(pid, parentPid, app); + + this.templateId = templateId; + } + + protected async start(): Promise { + const result = await this.admin.getMailbrokerTemplate(this.templateId); + + if (!result.success) { + MessageBox( + { + title: "Not found", + message: `The mailbroker template specified could not be found on the server. Please try again. ${result.errorMessage ?? "Unknown failure"}`, + image: "WarningIcon", + sound: "arcos.dialog.warning", + buttons: [BTN_OKAY_SUG], + }, + Daemon.getShell()?.pid!, + true + ); + + return; + } + + this.template = result.result; + this.users = await this.admin.getAllUsers(); + this.parseProperties(); + } + + private parseProperties() { + if (!this.template) return; + + const result: MailbrokerSendOverlay.Property[] = []; + const testStr = `${this.template.htmlContent}${this.template.subjectContent}${this.template.textContent}`; + const regex = /%(?[a-zA-Z_-]+)%/g; + const matches = testStr.matchAll(regex); + + for (const match of matches ?? []) { + const name = match?.groups?.name; + if (!name || result.find((r) => r.name === name)) continue; + + const property: MailbrokerSendOverlay.Property = { + variant: MailbrokerSendOverlay.PropertyVariant.Unknown, + isUser: false, + name, + }; + + switch (name.toLowerCase()) { + case "url": + property.variant = MailbrokerSendOverlay.PropertyVariant.Url; + break; + case "username": + property.variant = MailbrokerSendOverlay.PropertyVariant.Username; + property.isUser = true; + break; + case "userid": + property.variant = MailbrokerSendOverlay.PropertyVariant.UserId; + property.isUser = true; + break; + default: + property.variant = MailbrokerSendOverlay.PropertyVariant.Unknown; + break; + } + + result.push(property); + } + + this.properties = result; + } +} diff --git a/src/apps/admin/mailbrokermanagerapp/Overlays/SendOverlay/types.ts b/src/apps/admin/mailbrokermanagerapp/Overlays/SendOverlay/types.ts new file mode 100644 index 000000000..5f3c467eb --- /dev/null +++ b/src/apps/admin/mailbrokermanagerapp/Overlays/SendOverlay/types.ts @@ -0,0 +1,14 @@ +export namespace MailbrokerSendOverlay { + export interface Property { + name: string; + variant: PropertyVariant; + isUser: boolean; + } + + export enum PropertyVariant { + Unknown, + Username, + UserId, + Url + } +} \ No newline at end of file diff --git a/src/apps/admin/mailbrokermanagerapp/Overlays/ViewKeyOverlay/KeyOverlay.svelte b/src/apps/admin/mailbrokermanagerapp/Overlays/ViewKeyOverlay/KeyOverlay.svelte new file mode 100644 index 000000000..dc07c8639 --- /dev/null +++ b/src/apps/admin/mailbrokermanagerapp/Overlays/ViewKeyOverlay/KeyOverlay.svelte @@ -0,0 +1,145 @@ + + +{#if process.key} +
+ +
+

Key for {process.key?.serverName}

+

{process.key?.value}

+ +
+
+ +
+
+

Created at

+

{dayjs(process.key.createdAt).format("D MMMM YYYY, HH:mm:ss")}

+
+ +
+

Updated at

+

{dayjs(process.key.updatedAt).format("D MMMM YYYY, HH:mm:ss")}

+
+ +
+

Sent records

+

{process.sentRecords.length} {Plural("record", process.sentRecords.length)} sent by this server

+
+
+ + + {#snippet leftContent()} + Copy... + {/snippet} + {#snippet rightContent()} + + {disabled ? "Enable" : "Disable"} + + + Delete + + process.closeWindow()}>Close + {/snippet} + +{/if} diff --git a/src/apps/admin/mailbrokermanagerapp/Overlays/ViewKeyOverlay/KeyOverlay.ts b/src/apps/admin/mailbrokermanagerapp/Overlays/ViewKeyOverlay/KeyOverlay.ts new file mode 100644 index 000000000..e7f470f50 --- /dev/null +++ b/src/apps/admin/mailbrokermanagerapp/Overlays/ViewKeyOverlay/KeyOverlay.ts @@ -0,0 +1,45 @@ +import type { App } from "$types/apps/app"; +import KeyOverlaySvelte from "./KeyOverlay.svelte"; +import { MailbrokerViewKeyOverlayRuntime } from "./runtime"; + +const KeyOverlay: App = { + metadata: { + name: "View mailbroker key", + author: "Izaak Kuipers", + version: "1.0.0", + icon: "MailbrokerAdminIcon", + }, + position: { centered: true }, + size: { + w: 450, + h: 300, + }, + minSize: { + w: 450, + h: 300, + }, + maxSize: { + w: 450, + h: 300, + }, + controls: { + minimize: false, + maximize: false, + close: true, + }, + state: { + resizable: false, + headless: true, + fullscreen: false, + maximized: false, + minimized: false, + }, + assets: { + runtime: MailbrokerViewKeyOverlayRuntime, + component: KeyOverlaySvelte as any, + }, + hidden: true, + id: "MailbrokerManagerApp_KeyOverlay", +}; + +export default KeyOverlay; \ No newline at end of file diff --git a/src/apps/admin/mailbrokermanagerapp/Overlays/ViewKeyOverlay/runtime.ts b/src/apps/admin/mailbrokermanagerapp/Overlays/ViewKeyOverlay/runtime.ts new file mode 100644 index 000000000..48fc6b45b --- /dev/null +++ b/src/apps/admin/mailbrokermanagerapp/Overlays/ViewKeyOverlay/runtime.ts @@ -0,0 +1,52 @@ +import type { IMailbrokerViewKeyOverlayRuntime } from "$interfaces/runtimes/IMailbrokerRuntime"; +import type { IAdminBootstrapper } from "$interfaces/services/IAdminBootstrapper"; +import { AppProcess } from "$ts/apps/process"; +import { Daemon } from "$ts/env"; +import { AdminScopes } from "$ts/servicehost/services/AdminBootstrapper/store"; +import { BTN_OKAY_SUG, MessageBox } from "$ts/util/dialog"; +import type { AppProcessData } from "$types/apps/app"; +import type { Mailbroker } from "$types/server/mailbroker"; + +export class MailbrokerViewKeyOverlayRuntime extends AppProcess implements IMailbrokerViewKeyOverlayRuntime { + public key?: Mailbroker.MailKey; + public keyId: string; + public sentRecords: Mailbroker.SentMail[] = []; + + get admin() { + return Daemon.serviceHost?.getService("AdminBootstrapper")!; + } + + constructor(pid: number, parentPid: number, app: AppProcessData, key: string) { + super(pid, parentPid, app); + + this.keyId = key; + } + + protected async start(): Promise { + const result = await this.admin.getMailbrokerKey(this.keyId); + + if (!result.success) { + MessageBox( + { + title: "Not found", + message: `The mailbroker key specified could not be found on the server. Please try again. ${result.errorMessage ?? "Unknown failure"}`, + image: "WarningIcon", + sound: "arcos.dialog.warning", + buttons: [BTN_OKAY_SUG], + }, + Daemon.getShell()?.pid!, + true + ); + + return; + } + + this.key = result.result; + + if (!this.admin.canAccess(AdminScopes.adminMailbrokerSentRead)) return; + + this.sentRecords = + (await this.admin.getMailbrokerSentRecords()).result?.filter((record) => record.to?.serverName === this.key!.serverName) ?? + []; + } +} diff --git a/src/apps/admin/mailbrokermanagerapp/Pages/GenericList.svelte b/src/apps/admin/mailbrokermanagerapp/Pages/GenericList.svelte new file mode 100644 index 000000000..701ea478f --- /dev/null +++ b/src/apps/admin/mailbrokermanagerapp/Pages/GenericList.svelte @@ -0,0 +1,18 @@ + + + diff --git a/src/apps/admin/mailbrokermanagerapp/Pages/Logs.svelte b/src/apps/admin/mailbrokermanagerapp/Pages/Logs.svelte new file mode 100644 index 000000000..8ac8f18ed --- /dev/null +++ b/src/apps/admin/mailbrokermanagerapp/Pages/Logs.svelte @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + {#each data.toReversed() as log (log._id)} + + + + + + + + {/each} + +
CodeMessageDetailsWhen?
+ + {log.code}{log.message}{log.details}{dayjs(log.createdAt).format("D MMM YYYY, HH:mm:ss")}
diff --git a/src/apps/admin/mailbrokermanagerapp/Pages/NewTemplate.svelte b/src/apps/admin/mailbrokermanagerapp/Pages/NewTemplate.svelte new file mode 100644 index 000000000..f6265d6be --- /dev/null +++ b/src/apps/admin/mailbrokermanagerapp/Pages/NewTemplate.svelte @@ -0,0 +1,52 @@ + + + + + + {#snippet rightContent()} + + Save + + {/snippet} + diff --git a/src/apps/admin/mailbrokermanagerapp/Pages/Sent.svelte b/src/apps/admin/mailbrokermanagerapp/Pages/Sent.svelte new file mode 100644 index 000000000..ac4dc1763 --- /dev/null +++ b/src/apps/admin/mailbrokermanagerapp/Pages/Sent.svelte @@ -0,0 +1,61 @@ + + +
+

{data.length} {Plural("sent email", data.length)}

+ +
+ +
+ {#each filteredSent.toReversed() as sentRecord (sentRecord._id)} + + {/each} + + {#if !filteredSent.length} +

+ There are no sent records that match the criteria. +

+ {/if} +
diff --git a/src/apps/admin/mailbrokermanagerapp/Pages/Sent/SentListingOption.svelte b/src/apps/admin/mailbrokermanagerapp/Pages/Sent/SentListingOption.svelte new file mode 100644 index 000000000..b8aaedcee --- /dev/null +++ b/src/apps/admin/mailbrokermanagerapp/Pages/Sent/SentListingOption.svelte @@ -0,0 +1,20 @@ + + + diff --git a/src/apps/admin/mailbrokermanagerapp/Pages/ServerKeys.svelte b/src/apps/admin/mailbrokermanagerapp/Pages/ServerKeys.svelte new file mode 100644 index 000000000..14cf3b723 --- /dev/null +++ b/src/apps/admin/mailbrokermanagerapp/Pages/ServerKeys.svelte @@ -0,0 +1,65 @@ + + +
+

{data.length} {Plural("server key", data.length)}

+ + +
+ +
+ {#each filteredKeys as serverKey (serverKey._id)} + + {/each} + + {#if !filteredKeys.length} +

+ There are no server keys that match the criteria. +

+ {/if} +
diff --git a/src/apps/admin/mailbrokermanagerapp/Pages/ServerKeys/ServerKeyOption.svelte b/src/apps/admin/mailbrokermanagerapp/Pages/ServerKeys/ServerKeyOption.svelte new file mode 100644 index 000000000..715395929 --- /dev/null +++ b/src/apps/admin/mailbrokermanagerapp/Pages/ServerKeys/ServerKeyOption.svelte @@ -0,0 +1,16 @@ + + + diff --git a/src/apps/admin/mailbrokermanagerapp/Pages/ViewSentRecord.svelte b/src/apps/admin/mailbrokermanagerapp/Pages/ViewSentRecord.svelte new file mode 100644 index 000000000..3606faad7 --- /dev/null +++ b/src/apps/admin/mailbrokermanagerapp/Pages/ViewSentRecord.svelte @@ -0,0 +1,88 @@ + + + + +
+ +
+ {#if showCode} + + {:else} + + {/if} +
+
+ + + {#snippet leftContent()} + (showCode = false)} icon="eye" /> + (showCode = true)} icon="code" /> + {/snippet} + {#snippet rightContent()} + + Delete... + + {/snippet} + diff --git a/src/apps/admin/mailbrokermanagerapp/Pages/ViewSentRecord/SentRecordHeader.svelte b/src/apps/admin/mailbrokermanagerapp/Pages/ViewSentRecord/SentRecordHeader.svelte new file mode 100644 index 000000000..13da95971 --- /dev/null +++ b/src/apps/admin/mailbrokermanagerapp/Pages/ViewSentRecord/SentRecordHeader.svelte @@ -0,0 +1,35 @@ + + +
+ +
+

{sentRecord.subject}

+ {#if sentRecord.to.serverName && sentRecord.to.username} +

From {sentRecord.to.serverName}, to {sentRecord.to.email} ({sentRecord.to.username})

+ {:else} +

To {sentRecord.to.email}

+ {/if} + +
+ +
+ {#if sentRecord.createdAt} +
+

Created at:

+

{dayjs(sentRecord.createdAt).format("D MMMM YYYY, HH:mm:ss")}

+
+ {/if} + {#if sentRecord.updatedAt} +
+

Updated at:

+

{dayjs(sentRecord.updatedAt).format("D MMMM YYYY, HH:mm:ss")}

+
+ {/if} +
+
diff --git a/src/apps/admin/mailbrokermanagerapp/Pages/ViewTemplate.svelte b/src/apps/admin/mailbrokermanagerapp/Pages/ViewTemplate.svelte new file mode 100644 index 000000000..fd3f486b2 --- /dev/null +++ b/src/apps/admin/mailbrokermanagerapp/Pages/ViewTemplate.svelte @@ -0,0 +1,140 @@ + + + + + + {#snippet leftContent()} + + {#if deprecated} + Undeprecate + {:else} + Deprecate + {/if} + + Delete + {/snippet} + {#snippet rightContent()} + process.spawnOverlay("SendOverlay", pageProps.templateId)}>Send... + Discard + Save + {/snippet} + diff --git a/src/apps/admin/mailbrokermanagerapp/Pages/ViewTemplate/TemplateEditor.svelte b/src/apps/admin/mailbrokermanagerapp/Pages/ViewTemplate/TemplateEditor.svelte new file mode 100644 index 000000000..e6d89e7e8 --- /dev/null +++ b/src/apps/admin/mailbrokermanagerapp/Pages/ViewTemplate/TemplateEditor.svelte @@ -0,0 +1,32 @@ + + +
+
+ {#each Object.entries(tabs) as [tab]} + + {/each} +
+
+ {#if Component} + + {/if} +
+
diff --git a/src/apps/admin/mailbrokermanagerapp/Pages/ViewTemplate/TemplateEditor/GeneralTab.svelte b/src/apps/admin/mailbrokermanagerapp/Pages/ViewTemplate/TemplateEditor/GeneralTab.svelte new file mode 100644 index 000000000..c6126f7dd --- /dev/null +++ b/src/apps/admin/mailbrokermanagerapp/Pages/ViewTemplate/TemplateEditor/GeneralTab.svelte @@ -0,0 +1,36 @@ + + +
+

Template name

+ +
+ +
+

Subject content

+ +
+ +
+

From suffix

+ +
diff --git a/src/apps/admin/mailbrokermanagerapp/Pages/ViewTemplate/TemplateEditor/HtmlContentTab.svelte b/src/apps/admin/mailbrokermanagerapp/Pages/ViewTemplate/TemplateEditor/HtmlContentTab.svelte new file mode 100644 index 000000000..d75534c26 --- /dev/null +++ b/src/apps/admin/mailbrokermanagerapp/Pages/ViewTemplate/TemplateEditor/HtmlContentTab.svelte @@ -0,0 +1,34 @@ + + + + +
+ +
diff --git a/src/apps/admin/mailbrokermanagerapp/Pages/ViewTemplate/TemplateEditor/TextContentTab.svelte b/src/apps/admin/mailbrokermanagerapp/Pages/ViewTemplate/TemplateEditor/TextContentTab.svelte new file mode 100644 index 000000000..515c2c969 --- /dev/null +++ b/src/apps/admin/mailbrokermanagerapp/Pages/ViewTemplate/TemplateEditor/TextContentTab.svelte @@ -0,0 +1,11 @@ + + + \ No newline at end of file diff --git a/src/apps/admin/mailbrokermanagerapp/Pages/ViewTemplate/TemplateHeader.svelte b/src/apps/admin/mailbrokermanagerapp/Pages/ViewTemplate/TemplateHeader.svelte new file mode 100644 index 000000000..cc35df3a8 --- /dev/null +++ b/src/apps/admin/mailbrokermanagerapp/Pages/ViewTemplate/TemplateHeader.svelte @@ -0,0 +1,42 @@ + + +
+ +
+

{$updateData.name} - ArcOS {$updateData.fromSuffix}

+

{$updateData.subjectContent}

+ +
+
+ {#if template.createdAt} +
+

Created at:

+

{dayjs(template.createdAt).format("D MMMM YYYY, HH:mm:ss")}

+
+ {/if} + {#if template.updatedAt} +
+

Updated at:

+

{dayjs(template.updatedAt).format("D MMMM YYYY, HH:mm:ss")}

+
+ {/if} +
+
diff --git a/src/apps/admin/mailbrokermanagerapp/runtime.ts b/src/apps/admin/mailbrokermanagerapp/runtime.ts new file mode 100644 index 000000000..70bddc897 --- /dev/null +++ b/src/apps/admin/mailbrokermanagerapp/runtime.ts @@ -0,0 +1,61 @@ +import type { IMailbrokerManagerRuntime } from "$interfaces/runtimes/IMailbrokerRuntime"; +import type { IAdminBootstrapper } from "$interfaces/services/IAdminBootstrapper"; +import { AppProcess } from "$ts/apps/process"; +import { Daemon } from "$ts/env"; +import { Store } from "$ts/writable"; +import type { App, AppProcessData } from "$types/apps/app"; +import NewKeyOverlay from "./Overlays/NewKeyOverlay/NewKeyOverlay"; +import SendOverlay from "./Overlays/SendOverlay/SendOverlay"; +import KeyOverlay from "./Overlays/ViewKeyOverlay/KeyOverlay"; +import { mailbrokerPages } from "./store"; + +export class MailbrokerManagerRuntime extends AppProcess implements IMailbrokerManagerRuntime { + currentPage = Store(""); + pageProps = Store>({}); + + get admin() { + return Daemon.serviceHost?.getService("AdminBootstrapper")!; + } + + protected overlayStore: Record = { + SendOverlay, + KeyOverlay, + NewKeyOverlay, + }; + + //#region LIFECYCLE + + constructor(pid: number, parentPid: number, app: AppProcessData) { + super(pid, parentPid, app); + + this.switchPage("activeTemplates"); + + this.setSource(__SOURCE__); + } + + switchPage(pageId: string, props?: Record, force = false) { + this.Log(`Loading page '${pageId}'`); + + if (!mailbrokerPages.has(pageId)) return; + + if (force) { + this.currentPage.set(""); + } + + this.pageProps.set({}); + + const page = mailbrokerPages.get(pageId); + + this.pageProps.set(props ?? {}); + this.currentPage.set(pageId); + this.windowTitle.set(`${page?.name}`); + } + + async start() {} + + async stop() {} + + async render() {} + + //#endregion LIFECYCLE +} diff --git a/src/apps/admin/mailbrokermanagerapp/store.ts b/src/apps/admin/mailbrokermanagerapp/store.ts new file mode 100644 index 000000000..948219f62 --- /dev/null +++ b/src/apps/admin/mailbrokermanagerapp/store.ts @@ -0,0 +1,95 @@ +import { AdminScopes } from "$ts/servicehost/services/AdminBootstrapper/store"; +import GenericList from "./Pages/GenericList.svelte"; +import Logs from "./Pages/Logs.svelte"; +import NewTemplate from "./Pages/NewTemplate.svelte"; +import Sent from "./Pages/Sent.svelte"; +import ServerKeys from "./Pages/ServerKeys.svelte"; +import ViewSentRecord from "./Pages/ViewSentRecord.svelte"; +import ViewTemplate from "./Pages/ViewTemplate.svelte"; +import type { MailbrokerPage, MailbrokerPages } from "./types"; + +export const mailbrokerPages: MailbrokerPages = new Map([ + [ + "activeTemplates", + { + name: "Active templates", + icon: "mails", + content: GenericList, + data: async (process) => await process.admin.getMailbrokerTemplates((template) => !template.deprecated), + scopes: [AdminScopes.adminMailbrokerTemplatesRead], + }, + ], + [ + "deprecatedTemplates", + { + name: "Deprecated templates", + icon: "mails", + content: GenericList, + data: async (process) => await process.admin.getMailbrokerTemplates((template) => template.deprecated), + scopes: [AdminScopes.adminMailbrokerTemplatesRead], + }, + ], + [ + "viewTemplate", + { + name: "View template", + icon: "eye", + hidden: true, + content: ViewTemplate, + data: async (process, props: { templateId: string }) => await process.admin.getMailbrokerTemplate(props.templateId), + scopes: [AdminScopes.adminMailbrokerTemplatesRead], + }, + ], + [ + "newTemplate", + { + name: "Create template", + icon: "plus", + hidden: true, + content: NewTemplate, + scopes: [AdminScopes.adminMailbrokerTemplatesWrite], + }, + ], + [ + "sent", + { + name: "Sent emails", + icon: "send", + content: Sent, + separator: true, + data: async (process) => await process.admin.getMailbrokerSentRecords(), + scopes: [AdminScopes.adminMailbrokerSentRead], + }, + ], + [ + "viewSentRecord", + { + name: "View sent record", + icon: "eye", + hidden: true, + content: ViewSentRecord, + data: async (process, props: { sentRecordId: string }) => await process.admin.getMailbrokerSentRecord(props.sentRecordId), + scopes: [AdminScopes.adminMailbrokerSentRead], + }, + ], + [ + "keys", + { + name: "Server keys", + icon: "key", + content: ServerKeys, + data: async (process) => await process.admin.getAllMailbrokerKeys(), + scopes: [AdminScopes.adminMailbrokerKeysRead], + }, + ], + [ + "logs", + { + name: "Logs", + icon: "list", + content: Logs, + data: async (process) => await process.admin.getMailbrokerLogs(), + scopes: [AdminScopes.adminMailbrokerLogsRead], + }, + ], +]); diff --git a/src/apps/admin/mailbrokermanagerapp/types.ts b/src/apps/admin/mailbrokermanagerapp/types.ts new file mode 100644 index 000000000..057cd861d --- /dev/null +++ b/src/apps/admin/mailbrokermanagerapp/types.ts @@ -0,0 +1,16 @@ +import type { ICommandResult } from "$interfaces/ICommandResult"; +import type { IMailbrokerManagerRuntime } from "$interfaces/runtimes/IMailbrokerRuntime"; +import type { MaybePromise } from "$types/shared/common"; +import type { Component } from "svelte"; + +export interface MailbrokerPage { + name: string; + icon: string; + content: Component; + hidden?: boolean; + separator?: boolean; + data?: (process: IMailbrokerManagerRuntime, pageProps: any) => MaybePromise>>; + scopes?: string[]; +} + +export type MailbrokerPages = Map; diff --git a/src/apps/components/messagebox/messageBox.ts b/src/apps/components/messagebox/messageBox.ts index 8849a4d25..10b1e811b 100644 --- a/src/apps/components/messagebox/messageBox.ts +++ b/src/apps/components/messagebox/messageBox.ts @@ -25,7 +25,7 @@ export const MessageBoxApp: App = { controls: { minimize: false, maximize: false, - close: true, + close: false, }, assets: { runtime: MessageBoxRuntime, diff --git a/src/apps/components/messagecomposer/MessageComposer.svelte b/src/apps/components/messagecomposer/MessageComposer.svelte index 13c5b6764..ef1cbdc16 100755 --- a/src/apps/components/messagecomposer/MessageComposer.svelte +++ b/src/apps/components/messagecomposer/MessageComposer.svelte @@ -1,20 +1,29 @@ +
- + {#if $showPreview} +
+ +
+ {:else} + + {/if}
- + diff --git a/src/apps/components/messagecomposer/MessageComposer.ts b/src/apps/components/messagecomposer/MessageComposer.ts index a4b8d29db..9109b8013 100644 --- a/src/apps/components/messagecomposer/MessageComposer.ts +++ b/src/apps/components/messagecomposer/MessageComposer.ts @@ -12,8 +12,8 @@ export const MessageComposerApp: App = { appGroup: "multimedia", }, position: { centered: true }, - size: { w: 550, h: 450 }, - minSize: { w: 550, h: 450 }, + size: { w: 800, h: 650 }, + minSize: { w: 800, h: 650 }, maxSize: { w: NaN, h: NaN }, state: { maximized: false, diff --git a/src/apps/components/messagecomposer/MessageComposer/MessageActions.svelte b/src/apps/components/messagecomposer/MessageComposer/MessageActions.svelte new file mode 100755 index 000000000..3016607df --- /dev/null +++ b/src/apps/components/messagecomposer/MessageComposer/MessageActions.svelte @@ -0,0 +1,27 @@ + + + + {#snippet leftContent()} + process.addAttachment()}> + Attach... + + + {/snippet} + {#snippet rightContent()} + + ($showPreview = true)} icon="eye"> + ($showPreview = false)} icon="code-xml"> + + {/snippet} + diff --git a/src/apps/components/messagecomposer/MessageComposer/ActionBar.svelte b/src/apps/components/messagecomposer/MessageComposer/StatusBar.svelte old mode 100755 new mode 100644 similarity index 58% rename from src/apps/components/messagecomposer/MessageComposer/ActionBar.svelte rename to src/apps/components/messagecomposer/MessageComposer/StatusBar.svelte index 6beb47617..bb6b0b511 --- a/src/apps/components/messagecomposer/MessageComposer/ActionBar.svelte +++ b/src/apps/components/messagecomposer/MessageComposer/StatusBar.svelte @@ -3,22 +3,19 @@ import ActionBar from "$lib/Window/ActionBar.svelte"; import ActionButton from "$lib/Window/ActionBar/ActionButton.svelte"; import IconActionButton from "$lib/Window/ActionBar/ActionIconButton.svelte"; - import Pill from "$lib/Window/ActionBar/ActionPill.svelte"; - import Separator from "$lib/Window/ActionBar/ActionSeparator.svelte"; + import ActionPill from "$lib/Window/ActionBar/ActionPill.svelte"; import { formatBytes } from "$ts/util/fs"; - const { process }: { process: IMessageComposerRuntime } = $props(); - const { title, body, recipients, sending, attachments } = process; + let { process }: { process: IMessageComposerRuntime } = $props(); + const { body, attachments, sending, title, recipients } = process; {#snippet leftContent()} - {formatBytes($body.length)} - {formatBytes($attachments.map((a) => a.data.size).reduce((a, b) => a + b, 0))} + {formatBytes($body.length)} + {formatBytes($attachments.map((a) => a.data.size).reduce((a, b) => a + b, 0))} {/snippet} {#snippet rightContent()} - process.addAttachment()} /> - (false); recipients = Store([]); attachments = Store([]); + showPreview = Store(false); title = Store(""); body = Store(""); replyId: string | undefined; @@ -201,6 +202,13 @@ export class MessageComposerRuntime extends AppProcess implements IMessageCompos this.attachments.update((v) => v.filter((a) => a.uuid !== uuid)); } + //#endregion + //#region SIGNATURES + + addSignature() { + // stub + } + //#endregion //#region MISC diff --git a/src/apps/components/processinfo/ProcessInfo.svelte b/src/apps/components/processinfo/ProcessInfo.svelte index f02359eb6..d670014db 100644 --- a/src/apps/components/processinfo/ProcessInfo.svelte +++ b/src/apps/components/processinfo/ProcessInfo.svelte @@ -59,6 +59,9 @@ {proc.sourceUrl || import.meta.url} {proc.sourceUrl ? "Original" : "Interpreted"} + {#if !ProcessesHelper.IsAnyAppProcess(proc)} + {proc.constructor.name || "-"} + {/if} {#if ProcessesHelper.IsAnyAppProcess(proc)} @@ -66,7 +69,7 @@ {proc.app.id} {proc.app.data.originId} - {proc.app.desktop || "-"} + {proc.constructor.name || "-"} {/if} diff --git a/src/apps/components/pswdresetwizard/Pages/EnterUsername.svelte b/src/apps/components/pswdresetwizard/Pages/EnterUsername.svelte new file mode 100644 index 000000000..e30b9be73 --- /dev/null +++ b/src/apps/components/pswdresetwizard/Pages/EnterUsername.svelte @@ -0,0 +1,30 @@ + + +

+ 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. +

+ +
+

Enter your username:

+
+ + +
+ {#if $ErrorMessage} +
+ + {$ErrorMessage} +
+ {/if} +
+ +

+ If you forgot your username, please talk to an Administrator in the ArcOS Discord server, and we'll help you recover your + account. +

diff --git a/src/apps/components/pswdresetwizard/Pages/NewPassword.svelte b/src/apps/components/pswdresetwizard/Pages/NewPassword.svelte new file mode 100644 index 000000000..13ea3437e --- /dev/null +++ b/src/apps/components/pswdresetwizard/Pages/NewPassword.svelte @@ -0,0 +1,25 @@ + + +

+ 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. +

+ +
+

Enter a new password:

+
+ + +
+ {#if $ErrorMessage} +
+ + {$ErrorMessage} +
+ {/if} +
diff --git a/src/apps/components/pswdresetwizard/Pages/RestartFirst.svelte b/src/apps/components/pswdresetwizard/Pages/RestartFirst.svelte new file mode 100644 index 000000000..15af0306e --- /dev/null +++ b/src/apps/components/pswdresetwizard/Pages/RestartFirst.svelte @@ -0,0 +1,11 @@ +
+ + You have to restart ArcOS first. +
+ +

+ 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. +

+ +

After restarting, click the Forgot password? link again to continue.

diff --git a/src/apps/components/pswdresetwizard/Pages/Verification.svelte b/src/apps/components/pswdresetwizard/Pages/Verification.svelte new file mode 100644 index 000000000..6ea7cc56b --- /dev/null +++ b/src/apps/components/pswdresetwizard/Pages/Verification.svelte @@ -0,0 +1,28 @@ + + +

+ We’ve sent a verification code to the email address associated with your account. Enter the code in the field below to continue: +

+ +
+

Verification code:

+
+ +
+ {#if $ErrorMessage} +
+ + {$ErrorMessage} +
+ {/if} +
+ +

+ 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. +

diff --git a/src/apps/components/pswdresetwizard/PswdResetWizard.svelte b/src/apps/components/pswdresetwizard/PswdResetWizard.svelte new file mode 100644 index 000000000..691bb78e4 --- /dev/null +++ b/src/apps/components/pswdresetwizard/PswdResetWizard.svelte @@ -0,0 +1,39 @@ + + +
+
+

Account Recovery

+

Follow these steps to reset your password.

+
+
+ {#if Component} + + {/if} +
+ {#if $Loading} +
+ +
+ {/if} +
+ + {#snippet rightContent()} + {#if $CurrentPage} + {#each $CurrentPage.buttons as button (button.caption)} + button.action()}> + {button.caption} + + {/each} + {/if} + {/snippet} + diff --git a/src/apps/components/pswdresetwizard/PswdResetWizardApp.ts b/src/apps/components/pswdresetwizard/PswdResetWizardApp.ts new file mode 100644 index 000000000..2e25b9d4a --- /dev/null +++ b/src/apps/components/pswdresetwizard/PswdResetWizardApp.ts @@ -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; diff --git a/src/apps/components/pswdresetwizard/runtime.ts b/src/apps/components/pswdresetwizard/runtime.ts new file mode 100644 index 000000000..f1e410369 --- /dev/null +++ b/src/apps/components/pswdresetwizard/runtime.ts @@ -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(""); + public RecoveryCode = Store(""); + public NewPassword = Store(""); + + public Loading = Store(false); + public CurrentPage = Store(); + public ErrorMessage = Store(""); + + // for callback to login + public Finished = Store(false); + + public get LoginRuntime(): INewLoginAppRuntime { + const proc = Stack.getProcess(+Env.get("LOGINAPP_PID")); + if (!proc) throw new Error("Reference to NewLoginAppRuntime could not be obtained"); + + return proc; + } + + async onClose(): Promise { + this.Finished.set(true); + return true; + } + + private RecoveryUserId?: string; + private ResetToken?: string; + private Pages = PswdResetPages(this); + private get UserConnector(): IUserConnector { + return GetConnector("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 { + 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]); + } +} diff --git a/src/apps/components/pswdresetwizard/store.ts b/src/apps/components/pswdresetwizard/store.ts new file mode 100644 index 000000000..48c331582 --- /dev/null +++ b/src/apps/components/pswdresetwizard/store.ts @@ -0,0 +1,81 @@ +import type { IPswdResetWizardRuntime } from "$interfaces/runtimes/IPswdResetWizardRuntime"; +import EnterUsername from "./Pages/EnterUsername.svelte"; +import NewPassword from "./Pages/NewPassword.svelte"; +import RestartFirst from "./Pages/RestartFirst.svelte"; +import Verification from "./Pages/Verification.svelte"; +import type { PswdResetPage } from "./types"; + +export function PswdResetPages(runtime: IPswdResetWizardRuntime): Record { + return { + restartFirst: { + component: RestartFirst as any, + buttons: [ + { + caption: "Cancel", + action: () => { + runtime.closeWindow(); + }, + }, + { + caption: "Restart ArcOS", + action: () => { + runtime.closeWindow(); + runtime.LoginRuntime.PerformRestart(); + }, + suggested: true, + }, + ], + }, + enterUsername: { + component: EnterUsername as any, + buttons: [ + { + caption: "Cancel", + action: () => { + runtime.closeWindow(); + }, + }, + { + caption: "Send email", + action: () => { + runtime.DoSendEmail(); + }, + suggested: true, + }, + ], + }, + verification: { + component: Verification as any, + buttons: [ + { + caption: "Cancel", + disabled: true as any, + action: () => {}, + }, + { + caption: "Verify", + suggested: true, + action: () => runtime.DoVerify(), + }, + ], + }, + newPassword: { + component: NewPassword as any, + buttons: [ + { + caption: "Cancel", + action: () => { + runtime.closeWindow(); + }, + }, + { + caption: "Change password", + action: () => { + runtime.DoChangePassword(); + }, + suggested: true, + }, + ], + }, + }; +} diff --git a/src/apps/components/pswdresetwizard/types.ts b/src/apps/components/pswdresetwizard/types.ts new file mode 100644 index 000000000..50407d6fe --- /dev/null +++ b/src/apps/components/pswdresetwizard/types.ts @@ -0,0 +1,7 @@ +import type { MessageBoxButton } from "$types/shared/messagebox"; +import type { Component } from "svelte"; + +export interface PswdResetPage { + component: Component; + buttons: (MessageBoxButton & { disabled?: boolean })[]; +} diff --git a/src/apps/components/sharecreategui/runtime.ts b/src/apps/components/sharecreategui/runtime.ts index 45419c073..664ba329c 100755 --- a/src/apps/components/sharecreategui/runtime.ts +++ b/src/apps/components/sharecreategui/runtime.ts @@ -32,15 +32,14 @@ export class ShareCreateGuiRuntime extends AppProcess implements IShareCreateGui if (!name || !password) return; - const result = await this.shares.createShare(name, password); // Let's create the share + const createResult = await this.shares.createShare(name, password); // Let's create the share - if (!result) { + if (!createResult.success) { // create failed or smth MessageBox( { title: "Failed to create share", - message: - "ArcOS was unable to create the share you requested. You might already have the maximum amount of shares in your account.", + message: `ArcOS was unable to create the share you requested. ${createResult.errorMessage ?? "An unknown error occurred while attempting to create the share. Please contact support."}`, buttons: [BTN_OKAY_SUG], image: "ErrorIcon", sound: "arcos.dialog.error", @@ -52,7 +51,7 @@ export class ShareCreateGuiRuntime extends AppProcess implements IShareCreateGui return; } - const drive = await this.shares.mountShareById(result._id); // Mount the created share + const drive = await this.shares.mountShareById(createResult.result!._id); // Mount the created share // NOTE: The user daemon automatically mounts owned shares upon login, so we don't have to add it to startup diff --git a/src/apps/components/sharemgmtgui/ShareMgmtGui/Overlays/ChangePassword.svelte b/src/apps/components/sharemgmtgui/ShareMgmtGui/Overlays/ChangePassword.svelte index ddeaee3b2..3f4239a40 100755 --- a/src/apps/components/sharemgmtgui/ShareMgmtGui/Overlays/ChangePassword.svelte +++ b/src/apps/components/sharemgmtgui/ShareMgmtGui/Overlays/ChangePassword.svelte @@ -28,15 +28,15 @@ } const shares = Daemon?.serviceHost?.getService("ShareMgmt")!; - const result = shares?.changeSharePassword(process.parentProcess.shareId, newPassword); + const result = await shares?.changeSharePassword(process.parentProcess.shareId, newPassword); process.closeWindow(); - if (!result) { + if (!result.success) { MessageBox( { title: "Change password", - message: "Failed to change your password! Something might have gone wrong on our end. Please try again later.", + message: `Failed to change the password of the share. ${result.errorMessage ?? "An unknown error occurred. Please contact support."}`, buttons: [BTN_OKAY_SUG], image: "WarningIcon", sound: "arcos.dialog.warning", diff --git a/src/apps/components/shell/Shell.svelte b/src/apps/components/shell/Shell.svelte index c148db790..11985c07e 100755 --- a/src/apps/components/shell/Shell.svelte +++ b/src/apps/components/shell/Shell.svelte @@ -14,6 +14,7 @@ const { process }: AppComponentProps = $props(); const { userPreferences, startMenuOpened, actionCenterOpened, username, FullscreenCount } = process; + const { InitComplete } = Daemon; let currentDesktop = $state(); @@ -28,27 +29,29 @@ }); -
0} -> -
- - - -
- - - {#if Daemon.userInfo.isSystem} -
- ArcOS v{ArcOSVersion}-{ArcMode()}_{ArcBuild()}
- {Daemon.username}@{Server.hostname}
- THIS IS A SYSTEM ACCOUNT. WATCH OUT. -
- {/if} +{#if $InitComplete} +
0} + > +
+ + + +
+ + + {#if Daemon.userInfo.isSystem} +
+ ArcOS v{ArcOSVersion}-{ArcMode()}_{ArcBuild()}
+ {Daemon.username}@{Server.hostname}
+ THIS IS A SYSTEM ACCOUNT. WATCH OUT. +
+ {/if} +
+
+ +
-
- -
-
+{/if} diff --git a/src/apps/components/shell/Shell/ActionCenter.svelte b/src/apps/components/shell/Shell/ActionCenter.svelte index 61f288b08..3acd66d19 100755 --- a/src/apps/components/shell/Shell/ActionCenter.svelte +++ b/src/apps/components/shell/Shell/ActionCenter.svelte @@ -3,7 +3,6 @@ import type { BooleanStore } from "$types/shared/writable"; import type { UserPreferencesStore } from "$types/user"; import { QuickSettings } from "../store"; - import CardStack from "./ActionCenter/CardStack.svelte"; import Clock from "./ActionCenter/Clock.svelte"; import Notifications from "./ActionCenter/Notifications.svelte"; import QuickSetting from "./ActionCenter/QuickSetting.svelte"; @@ -22,9 +21,6 @@
- {#if !process.safeMode} - - {/if}
{#if !process.safeMode} diff --git a/src/apps/components/shell/Shell/ActionCenter/CardStack.svelte b/src/apps/components/shell/Shell/ActionCenter/CardStack.svelte deleted file mode 100755 index 27d0a6c74..000000000 --- a/src/apps/components/shell/Shell/ActionCenter/CardStack.svelte +++ /dev/null @@ -1,67 +0,0 @@ - - -
- -
- {#if userDaemon} - - - - {:else} -
- ERR_NO_DAEMON -
-
- ERR_NO_DAEMON -
-
- ERR_NO_DAEMON -
- {/if} -
-
diff --git a/src/apps/components/shell/Shell/ActionCenter/CardStack/Cards/Gallery.svelte b/src/apps/components/shell/Shell/ActionCenter/CardStack/Cards/Gallery.svelte deleted file mode 100755 index 08509a383..000000000 --- a/src/apps/components/shell/Shell/ActionCenter/CardStack/Cards/Gallery.svelte +++ /dev/null @@ -1,106 +0,0 @@ - - - diff --git a/src/apps/components/shell/Shell/ActionCenter/CardStack/Cards/Notes.svelte b/src/apps/components/shell/Shell/ActionCenter/CardStack/Cards/Notes.svelte deleted file mode 100755 index 83f1957ac..000000000 --- a/src/apps/components/shell/Shell/ActionCenter/CardStack/Cards/Notes.svelte +++ /dev/null @@ -1,9 +0,0 @@ - - -
- -
diff --git a/src/apps/components/shell/Shell/ActionCenter/CardStack/Cards/Weather.svelte b/src/apps/components/shell/Shell/ActionCenter/CardStack/Cards/Weather.svelte deleted file mode 100755 index b8f2b317d..000000000 --- a/src/apps/components/shell/Shell/ActionCenter/CardStack/Cards/Weather.svelte +++ /dev/null @@ -1,95 +0,0 @@ - - -
{ - refresh(true); - }, - icon: "rotate-cw", - }, - { - caption: "Change location...", - icon: "map-pin", - action: () => { - changeLocation(); - }, - }, - ], - process, - ]} -> - {#if $userPreferences.shell.actionCenter.weatherLocation.name === "unset"} -

Set-up weather

- - {:else if !loading} - {#if data} - {#if data.icon && data.iconColor} - - {/if} -

- {data.temperature.toFixed(1)} °C -

-

- - {#if data.condition} - - {data.condition} - - {/if} -

- {:else} -

Failed to get weather info

- - {/if} - {:else} - - {/if} -
diff --git a/src/apps/components/shell/Shell/ActionCenter/CardStack/Indicator.svelte b/src/apps/components/shell/Shell/ActionCenter/CardStack/Indicator.svelte deleted file mode 100755 index a119ce3e2..000000000 --- a/src/apps/components/shell/Shell/ActionCenter/CardStack/Indicator.svelte +++ /dev/null @@ -1,9 +0,0 @@ - - -
- {#each new Array(max).fill(0) as _, i} -
- {/each} -
diff --git a/src/apps/components/shell/Shell/Taskbar.svelte b/src/apps/components/shell/Shell/Taskbar.svelte index ec1b118f3..8b10b6a80 100755 --- a/src/apps/components/shell/Shell/Taskbar.svelte +++ b/src/apps/components/shell/Shell/Taskbar.svelte @@ -9,6 +9,7 @@ import StartButton from "./Taskbar/StartButton.svelte"; import Clock from "./Taskbar/SystemArea/Clock.svelte"; import SystemTray from "./Taskbar/SystemTray.svelte"; + import TaskbarWeatherApplet from "./Taskbar/TaskbarWeatherApplet.svelte"; import WorkspaceManagerButton from "./Taskbar/WorkspaceManagerButton.svelte"; const { process }: { process: IShellRuntime } = $props(); @@ -26,6 +27,7 @@ + {#snippet ifActive(service: ITrayHostService)} diff --git a/src/apps/components/shell/Shell/Taskbar/ActionCenterButton.svelte b/src/apps/components/shell/Shell/Taskbar/ActionCenterButton.svelte index ceacb54bb..badc90d85 100755 --- a/src/apps/components/shell/Shell/Taskbar/ActionCenterButton.svelte +++ b/src/apps/components/shell/Shell/Taskbar/ActionCenterButton.svelte @@ -10,7 +10,7 @@ + {:else if data} + {#if data.icon && data.iconColor} + + {/if} + {#if $userPreferences.weatherSettings.displayMode !== "onlyTemperature"} +
+

+ {data.temperature.toFixed(1)} °C +

+

+ + {#if $userPreferences.weatherSettings.displayMode === "withCity" && city} + + {city} + + {:else if $userPreferences.weatherSettings.displayMode === "withCondition" && data.condition} + + {data.condition} + + {/if} +

+
+ {:else} +

+ {data.temperature.toFixed(1)} °C + +

+ {/if} + {:else} +

Failed to get weather info

+ + {/if} +
+{/if} diff --git a/src/apps/components/shell/Shell/Taskbar/WorkspaceManagerButton.svelte b/src/apps/components/shell/Shell/Taskbar/WorkspaceManagerButton.svelte index 8c0a572ab..39ed649c5 100755 --- a/src/apps/components/shell/Shell/Taskbar/WorkspaceManagerButton.svelte +++ b/src/apps/components/shell/Shell/Taskbar/WorkspaceManagerButton.svelte @@ -38,7 +38,7 @@ + {:else if $Status.variant === LoginStatusVariant.Loading} +

+ + {@html $Status.content} +

+ {:else} + + {/if} + + {#if $Status.variant === LoginStatusVariant.None} + + +
+ + +
+ {/if} + + {#if ServerInfo?.loginBottomText} +

+ {[ServerInfo?.loginBottomText, import.meta.env.DW_PREVIEW_DEP_BRANCH].filter(Boolean).join(" -- ")} +

+ {/if} +
diff --git a/src/apps/core/newloginapp/NewLogin/Field.svelte b/src/apps/core/newloginapp/NewLogin/Field.svelte new file mode 100755 index 000000000..0b9d27ede --- /dev/null +++ b/src/apps/core/newloginapp/NewLogin/Field.svelte @@ -0,0 +1,21 @@ + + +
+ + +
diff --git a/src/apps/core/newloginapp/NewLogin/NewLoginForm.svelte b/src/apps/core/newloginapp/NewLogin/NewLoginForm.svelte new file mode 100644 index 000000000..638b9fab9 --- /dev/null +++ b/src/apps/core/newloginapp/NewLogin/NewLoginForm.svelte @@ -0,0 +1,50 @@ + + + + + + +{#if !$State.selectedUser && !ServerInfo?.disableRegistration} + +{/if} diff --git a/src/apps/core/newloginapp/NewLogin/UserSelector.svelte b/src/apps/core/newloginapp/NewLogin/UserSelector.svelte new file mode 100644 index 000000000..11c21b0d9 --- /dev/null +++ b/src/apps/core/newloginapp/NewLogin/UserSelector.svelte @@ -0,0 +1,37 @@ + + +{#if $Persistence.users?.length} +
+ {#each $Persistence.users as user (user.userId)} +
+ + +
+ {/each} +
+ +
+
+{/if} diff --git a/src/apps/core/newloginapp/NewLoginApp.ts b/src/apps/core/newloginapp/NewLoginApp.ts new file mode 100644 index 000000000..f57868dae --- /dev/null +++ b/src/apps/core/newloginapp/NewLoginApp.ts @@ -0,0 +1,40 @@ +import "$css/apps/core/newloginapp.css"; +import type { App } from "$types/apps/app"; +import NewLogin from "./NewLogin.svelte"; +import { NewLoginAppRuntime } from "./runtime"; + +const NewLoginApp: App = { + metadata: { + name: "LogonUI", + author: "Izaak Kuipers", + version: "10.0.0", + icon: "PasswordIcon", + appGroup: "coreApps", + }, + size: { w: NaN, h: NaN }, + minSize: { w: NaN, h: NaN }, + maxSize: { w: NaN, h: NaN }, + position: {}, + state: { + minimized: false, + maximized: false, + resizable: false, + fullscreen: true, + headless: true, + }, + controls: { + minimize: false, + maximize: false, + close: false, + }, + assets: { + component: NewLogin as any, + runtime: NewLoginAppRuntime, + }, + core: true, + hidden: true, + vital: true, + id: "NewLoginApp", +}; + +export default NewLoginApp; diff --git a/src/apps/core/newloginapp/runtime.ts b/src/apps/core/newloginapp/runtime.ts new file mode 100644 index 000000000..1ba59437c --- /dev/null +++ b/src/apps/core/newloginapp/runtime.ts @@ -0,0 +1,554 @@ +import PswdResetWizardApp from "$apps/components/pswdresetwizard/PswdResetWizardApp"; +import TotpAuthGuiApp from "$apps/components/totpauthgui/TotpAuthGui"; +import type { Constructs } from "$interfaces/common"; +import type { IAppProcess } from "$interfaces/IAppProcess"; +import type { ICommandResult } from "$interfaces/ICommandResult"; +import type { IUserDaemon } from "$interfaces/IUserDaemon"; +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 { ConfigurationBuilder } from "$ts/config"; +import { UserDaemon } from "$ts/daemon"; +import { Env, GetConnector, Server, Stack, State, SysDispatch } from "$ts/env"; +import { ProfilePictures } from "$ts/images/pfp"; +import { Sleep } from "$ts/sleep"; +import { LoginUser } from "$ts/user/auth"; +import { Wallpapers } from "$ts/user/wallpaper/store"; +import { authcode } from "$ts/util"; +import { UUID } from "$ts/util/uuid"; +import { Store } from "$ts/writable"; +import type { AppProcessData } from "$types/apps/app"; +import type { RenderArgs } from "$types/system/process"; +import type { UserInfo } from "$types/user"; +import dayjs from "dayjs"; +import Cookies from "js-cookie"; +import { NewLoginUserDaemonStartOptions } from "./store"; +import { + LoginStatusVariant, + type LoginPersistence, + type LoginPersistenceUser, + type LoginScreenOptions, + type LoginState, + type LoginStatus, +} from "./types"; + +export class NewLoginAppRuntime extends AppProcess implements INewLoginAppRuntime { + private readonly PERSISTENCE_LS_ID = "arcNewLoginPersistence"; + private readonly DEFAULT_WALLPAPER_ID = "img39"; + private options?: LoginScreenOptions; + public State = Store({}); + public Status = Store({ + variant: LoginStatusVariant.None, + }); + public Persistence = Store(); + public Config = new ConfigurationBuilder() + .WithLocalStorage(this.PERSISTENCE_LS_ID) + .ReadsFrom(this.Persistence) + .WithDefaults({ users: [] }) + .ForProcess(this) + .Build(); + private onErrorDismissed?(): void; + + public get ServerInfo() { + return Server.serverInfo; + } + + public get WelcomeString() { + const hour = dayjs().hour(); + + if (hour < 6) return "Hi, go to sleep"; + if (hour < 12) return "Good morning"; + if (hour < 18) return "Good afternoon"; + return "Good evening"; + } + + //#region LIFECYCLE + + constructor(pid: number, parentPid: number, app: AppProcessData, options?: LoginScreenOptions) { + super(pid, parentPid, app); + + this.options = options; + this.safeMode = !!(options?.safeMode || Env.get("safemode")); + + if (this.safeMode) Env.set("safemode", true); + } + + async start() { + Env.set("loginapp_pid", this.pid); + + await this.Config.initialize(); + this.State.set(this.GetDefaultLoginState()); + + const lastUserId = this.Persistence().lastUserId; + if (lastUserId) this.LoadLoginStateUsingPersistenceFrom(lastUserId); + } + + async stop() { + Env.delete("loginapp_pid"); + } + + async render(args: RenderArgs) { + if (this.ServerInfo?.freshBackend) { + await State.loadState("initialSetup"); + return false; + } + + if (!this.options?.type) { + State.getStateLoaders().main.removeAttribute("style"); + + const tokenResult = await this.LoadToken(); + const loginNotice = this.ServerInfo?.loginNotice; + + if (!tokenResult && loginNotice) { + this.ShowError(loginNotice); + } + + return; + } + + switch (this.options.type) { + case "logoff": + await this.PerformLogoff(this.options.userDaemon!); + return; + case "shutdown": + await this.PerformShutdown(this.options.userDaemon); + return; + case "restart": + await this.PerformRestart(this.options.userDaemon); + return; + + default: + throw new Error(`NewLoginAppRuntime.render: invalid login type '${this.options.type}'`); + } + } + + //#endregion + //#region PERSISTENCE + + SaveUserPersistence(info: UserInfo) { + this.Log(`SaveUserPersistence: ${info._id}`); + + this.Persistence.update((v) => { + const userConnector = Server.GetConn("UserConnector", ""); + const persistence: LoginPersistenceUser = { + displayName: info.preferences.account.displayName ?? info.username, + username: info.username, + userId: info._id, + profilePictureUrl: userConnector.PictureUrl(info._id), + wallpaperUrl: userConnector.LoginBgUrl(info._id), + administrator: info.admin, + }; + + const index = v.users.findIndex((u) => u.userId === info._id); + if (index < 0) v.users.push(persistence); + else v.users[index] = persistence; + + v.lastUserId = info._id; + + return v; + }); + } + + SetLastUsed(userId: string) { + this.Log(`SetLastUsed: ${userId}`); + this.Persistence.update((v) => { + v.lastUserId = userId; + + return v; + }); + } + + RemoveUser(userId: string) { + this.Log(`RemoveUser: ${userId}`); + this.Persistence.update((v) => { + v.users = v.users.filter((user) => user.userId !== userId); + + if (v.lastUserId === userId) { + v.lastUserId = v.users[0]?.userId ?? ""; + } + + return v; + }); + + this.LoadLoginStateUsingPersistenceFrom(this.Persistence().lastUserId ?? ""); + } + + SelectUser(userId: string) { + this.Persistence.update((v) => { + v.lastUserId = userId; + return v; + }); + this.LoadLoginStateUsingPersistenceFrom(userId); + } + + LoadLoginStateUsingPersistenceFrom(userId: string) { + this.Log(`LoadLoginStateUsingPersistenceFrom: ${userId}`); + + const userPersistence = this.Persistence().users.find((u) => u.userId === userId); + if (!userPersistence) { + this.State.set(this.GetDefaultLoginState()); + return; + } + + this.State.set({ + wallpaperUrl: userPersistence.wallpaperUrl, + profilePictureUrl: userPersistence.profilePictureUrl, + displayName: userPersistence.displayName, + selectedUser: userPersistence, + }); + } + + //#endregion + //#region LOGIN LOGIC + + async JumpstartUserDaemon(token: string, userInfo: UserInfo) { + this.Log(`Staring user daemon for '${userInfo.username}' (v10)`); + + this.ShowLoading(this.WelcomeString); + + const { success, result: userDaemon, errorMessage } = await UserDaemon.Hello(token, userInfo.username, userInfo); + + if (!success) { + this.ShowError(errorMessage ?? "Unknown error. Please contact support."); + return; + } + + const broadcast = (message: string) => { + if (!userDaemon?.preferences()?.enableVerboseLogin || !message) return; + this.ShowLoading(message); + }; + + this.ShowLoading("Saving token"); + this.SaveToken(userDaemon!); + + const result = await userDaemon?.startUserDaemon(NewLoginUserDaemonStartOptions(this), broadcast.bind(this)); + + this.SaveUserPersistence(userDaemon?.userInfo!); + + if (!result?.success) { + this.ShowError( + result?.errorMessage ?? "Unknown failure while attempting to initialize the User Daemon. Please contact support." + ); + } + } + + async PerformLogin(identity: string, password: string): Promise { + this.Log(`Trying login of '${identity}' using login v10`); + + this.ShowLoading(this.WelcomeString); + + const tokenResult = await LoginUser(identity, password); + + if (!tokenResult.success) { + await this.ShowErrorAndWait(tokenResult.errorMessage ?? "The credentials you entered are incorrect."); + + return; + } + + const userInfoResult = await this.GetUserInfo(tokenResult.result!); + if (!userInfoResult.success) { + this.ResetCookies(); + this.ShowErrorAndWait(userInfoResult.errorMessage ?? "Session token is invalid."); + + return; + } + + const userInfo = userInfoResult!.result; + this.ShowLoading(`Hi, ${userInfo!.preferences?.account?.displayName ?? userInfo!.username}!`); + + await this.JumpstartUserDaemon(tokenResult.result!, userInfo!); + } + + private async GetUserInfo(token: string): Promise> { + this.Log(`GetUserInfo`); + + return await GetConnector("UserConnector", token).Self(); + } + + public CreateUser(): void { + State?.loadState("initialSetup"); + } + + public async ForgotPassword(): Promise { + await new Promise(async (resolve) => { + const proc = await Stack.spawn( + PswdResetWizardApp.assets.runtime as Constructs, + undefined, + "", + this.pid, + { + data: { ...PswdResetWizardApp, overlay: true }, + id: PswdResetWizardApp.id, + } + ); + + if (!proc) { + // todo + resolve(); + return; + } + + const unsub = proc.Finished.subscribe((v) => { + if (!v) return; + + resolve(); + unsub?.(); + }); + }); + } + + //#endregion + //#region 2FA + + public async AskFor2FA(userId?: string): Promise { + this.Log(`AskFor2FA`); + + const returnId = UUID(); + + return new Promise(async (r) => { + SysDispatch.subscribe("totp-unlock-success", ([id]) => { + if (id === returnId) r(true); + }); + + SysDispatch.subscribe("totp-unlock-cancel", ([id]) => { + if (id === returnId) r(false); + }); + + await Stack.spawn( + TotpAuthGuiApp.assets.runtime, + undefined, + userId, + this.pid, + { data: { ...TotpAuthGuiApp, overlay: true }, id: "TotpAuthGuiApp" }, + returnId + ); + }); + } + + //#endregion + //#region POWER + + private async PerformLogoff(userDaemon: IUserDaemon) { + this.Log(`Logging off user '${userDaemon.username}'`); + + this.ShowLoading(`Goodbye, ${userDaemon.preferences().account.displayName ?? userDaemon.username}!`); + + const verbose = userDaemon.preferences().enableVerboseLogin; + const broadcast = (message: string) => { + if (!verbose) return; + this.ShowLoading(message); + }; + + this.LoadLoginStateUsingPersistenceFrom(userDaemon.userInfo._id); + + broadcast("Stopping service host"); + await userDaemon.serviceHost?.spinDown(broadcast.bind(this)); + + broadcast("Stopping processes"); + for (const proc of Stack.renderer?.currentState.map((pid) => Stack.getProcess(pid) as IAppProcess) ?? []) { + if (!proc._disposed && proc.pid !== this.pid) { + await proc.killSelf(); + } + } + + broadcast("Notifying activity"); + await userDaemon.activity!.logActivity("logout"); + this.ResetCookies(); + + // mandatory timeout because of user daemon <> state handler timing mismatch + await Sleep(2000); + + broadcast("Stopping user contexts"); + await userDaemon.stopUserContexts(); + + broadcast("Discontinuing token"); + await userDaemon.account!.discontinueToken(); + + broadcast("Stopping User Daemon"); + await userDaemon.killSelf(); + + this.NoStatus(); + State?.getStateLoaders()?.main?.removeAttribute("style"); + } + + public async PerformShutdown(userDaemon?: IUserDaemon) { + this.Log(`Handling shutdown`); + + this.ShowLoading("Shutting down..."); + + const verbose = userDaemon?.preferences().enableVerboseLogin; + const broadcast = (message: string) => { + if (!verbose) return; + this.ShowLoading(message); + }; + + if (userDaemon) { + this.SaveUserPersistence(userDaemon.userInfo); + this.LoadLoginStateUsingPersistenceFrom(userDaemon.userInfo._id); + + broadcast("Stopping Service Host"); + await userDaemon.serviceHost?.spinDown(broadcast); + + broadcast("Stopping User Contexts"); + await userDaemon?.stopUserContexts(); + } + + // mandatory timeout because of user daemon <> state handler timing mismatch + await Sleep(2000); + + if (userDaemon) { + broadcast("Stopping User Daemon"); + await userDaemon.killSelf(); + } + + State?.loadState("turnedOff"); + } + + public async PerformRestart(userDaemon?: IUserDaemon) { + this.Log(`Handling restart`); + + this.ShowLoading("Restarting..."); + + const verbose = userDaemon?.preferences().enableVerboseLogin; + const broadcast = (message: string) => { + if (!verbose) return; + this.ShowLoading(message); + }; + + if (userDaemon) { + this.SaveUserPersistence(userDaemon.userInfo); + this.LoadLoginStateUsingPersistenceFrom(userDaemon.userInfo._id); + + broadcast("Stopping Service Host"); + await userDaemon.serviceHost?.spinDown(broadcast); + + broadcast("Stopping User Contexts"); + await userDaemon?.stopUserContexts(); + } + + await Sleep(2000); + + if (userDaemon) { + broadcast("Stopping User Daemon"); + await userDaemon.killSelf(); + } + + location.reload(); + } + + //#endregion + //#region COOKIES + + private SaveToken(userDaemon: IUserDaemon) { + const token = userDaemon.token; + const username = userDaemon.username; + + this.Log(`Saving token of '${userDaemon.username}' to cookies`); + + const cookieOptions: Cookies.CookieAttributes = { + path: "/", + secure: false, + expires: 30, + domain: location.hostname, + }; + + Cookies.set("arcToken", token, cookieOptions); + Cookies.set("arcUsername", username, cookieOptions); + } + + private async LoadToken() { + this.Log("Loading token from cookies"); + + const token = Cookies.get("arcToken"); + if (!token) return false; + + const userInfoResult = await this.GetUserInfo(token); + if (!userInfoResult.success) { + this.ResetCookies(); + + return false; + } + + this.LoadLoginStateUsingPersistenceFrom(userInfoResult.result!._id); + await this.JumpstartUserDaemon(token, userInfoResult.result!); + + return true; + } + + public ResetCookies() { + this.Log(`Resetting stored cookie state`); + + Cookies.remove("arcToken"); + Cookies.remove("arcUsername"); + } + + //#region UTILS + + private GetDefaultLoginState(): LoginState { + const wallpaperUrl = Server.serverInfo?.loginWallpaper + ? `${Server.url}/loginbg${authcode()}` + : Wallpapers[this.DEFAULT_WALLPAPER_ID].url; + + const profilePictureUrl = ProfilePictures.def; + + return { + displayName: "", + wallpaperUrl, + profilePictureUrl, + }; + } + + public DismissError(): void { + this.Log("DismissError"); + + if (this.Status().variant !== LoginStatusVariant.Error) return; + + this.Status.update((v) => { + v.variant = LoginStatusVariant.None; + return v; + }); + + this.onErrorDismissed?.(); + this.onErrorDismissed = undefined; + } + + public ShowError(message: string) { + this.Log(`ShowError: ${message}`); + if (this.Status().variant === LoginStatusVariant.Error) return; + + this.Status.update((v) => { + v.variant = LoginStatusVariant.Error; + v.content = message; + + return v; + }); + } + + public async ShowErrorAndWait(message: string): Promise { + this.Log(`ShowErrorAndWait: ${message}`); + return new Promise((resolve) => { + this.onErrorDismissed = () => resolve(); + this.ShowError(message); + }); + } + + public ShowLoading(message: string) { + this.Log(`ShowLoading: ${message}`); + this.Status.update((v) => { + v.variant = LoginStatusVariant.Loading; + v.content = message; + return v; + }); + } + + private NoStatus() { + this.Log(`NoStatus`); + this.Status.set({ + variant: LoginStatusVariant.None, + content: "", + }); + } + + //#endregion +} diff --git a/src/apps/core/newloginapp/store.ts b/src/apps/core/newloginapp/store.ts new file mode 100644 index 000000000..3c53282d3 --- /dev/null +++ b/src/apps/core/newloginapp/store.ts @@ -0,0 +1,60 @@ +import type { INewLoginAppRuntime } from "$interfaces/runtimes/INewLoginAppRuntime"; +import { Daemon, SoundBus, State } from "$ts/env"; +import { CommandResult } from "$ts/result"; +import type { UserDaemonStartOptions } from "$types/daemon"; + +export function NewLoginUserDaemonStartOptions(runtime: INewLoginAppRuntime): UserDaemonStartOptions { + return { + onUserInfo: async (info) => { + runtime.SaveUserPersistence(info); + runtime.LoadLoginStateUsingPersistenceFrom(info._id); + + if (info.hasTotp && info.restricted) { + runtime.ShowLoading("Requesting 2FA"); + + const unlocked = await runtime.AskFor2FA(info._id); + + if (!unlocked) { + await Daemon.account!.discontinueToken(); + await Daemon.killSelf(); + runtime.ResetCookies(); + return CommandResult.Error("You didn't enter a valid 2FA code!"); + } + } + + if (info.isSystem) { + await runtime.ShowErrorAndWait( + "This is a system account. Changes you make here can cause external ArcOS services to stop working. To continue, click Okay." + ); + } + + runtime.ShowLoading(runtime.WelcomeString); + + return CommandResult.Ok(); + }, + startStages: [ + "filesystem", + "preferencesSync", + "notifyLogin", + "serviceHost", + "firstRun", + "driveNotifierWatcher", + "indexing", + "statusRefresh", + "letsGo", + "workspaces", + "autorun", + ], + stageCallbacks: { + preferencesSync: async (_daemon, broadcast) => { + broadcast("Reading profile customization"); + }, + letsGo: async (daemon) => { + await State?.loadState("desktop", { userDaemon: daemon }, true); + SoundBus?.playSound("arcos.system.logon"); + daemon.renderer!.setAppRendererClasses(daemon.preferences()); + daemon.checks!.checkNightly(); + }, + }, + }; +} diff --git a/src/apps/core/newloginapp/types.ts b/src/apps/core/newloginapp/types.ts new file mode 100644 index 000000000..d2ac76392 --- /dev/null +++ b/src/apps/core/newloginapp/types.ts @@ -0,0 +1,40 @@ +import type { IUserDaemon } from "$interfaces/IUserDaemon"; + +export interface LoginState { + wallpaperUrl?: string; + profilePictureUrl?: string; + displayName?: string; + selectedUser?: LoginPersistenceUser; + hideProfileImage?: boolean; +} + +export interface LoginStatus { + variant: LoginStatusVariant; + content?: string; +} + +export enum LoginStatusVariant { + None, + Error, + Loading, +} + +export interface LoginPersistence { + lastUserId?: string; + users: LoginPersistenceUser[]; +} + +export interface LoginPersistenceUser { + userId: string; + username: string; + displayName: string; + wallpaperUrl: string; + profilePictureUrl: string; + administrator: boolean; +} + +export interface LoginScreenOptions { + userDaemon?: IUserDaemon; + type?: string; + safeMode?: boolean; +} diff --git a/src/apps/core/switchserver/runtime.ts b/src/apps/core/switchserver/runtime.ts index 54283b87b..5b558f13d 100644 --- a/src/apps/core/switchserver/runtime.ts +++ b/src/apps/core/switchserver/runtime.ts @@ -59,6 +59,8 @@ export class SwitchServerRuntime extends AppProcess implements ISwitchServerRunt Cookies.remove("arcToken"); Cookies.remove("arcUsername"); localStorage.removeItem("arcLoginPersistence"); + localStorage.removeItem("arcNewLoginPersistence"); + localStorage.removeItem("arcUsername"); } location.reload(); diff --git a/src/apps/user/filemanager/FileManager/Sidebar.svelte b/src/apps/user/filemanager/FileManager/Sidebar.svelte index 7099d6d6d..6e9fa46cd 100755 --- a/src/apps/user/filemanager/FileManager/Sidebar.svelte +++ b/src/apps/user/filemanager/FileManager/Sidebar.svelte @@ -10,11 +10,11 @@ const { path, rootFolders, drives } = process; -