aboutsummaryrefslogtreecommitdiffstats
path: root/code
diff options
context:
space:
mode:
authorivarlovlie <git@ivarlovlie.no>2022-12-13 14:48:11 +0100
committerivarlovlie <git@ivarlovlie.no>2022-12-13 14:48:21 +0100
commita8219611cbebbd27501d9f30c804979048b98107 (patch)
tree632dbab8b724f0148b06525771d85ad6ddb55e35 /code
parentdb2d937a38d5c309e3c6aa14f2eaf3cfe58f415e (diff)
downloadgreatoffice-a8219611cbebbd27501d9f30c804979048b98107.tar.xz
greatoffice-a8219611cbebbd27501d9f30c804979048b98107.zip
feat: A whole slew of things
- Use a md5 hash of the session cookie value as key for session validity check - Introduce global state - Introduce a common interface for form logic, and implement it on the sign-in form - Introduce static resolve() on all services instead of new-upping all over. - Implement /portal on the frontend to support giving the frontend a inital context from server or anywhere. - Show a notification when users sign in for the first time after validating their email
Diffstat (limited to 'code')
-rw-r--r--code/api/src/Endpoints/Internal/Root/ValidateRoute.cs2
-rw-r--r--code/app/src/components/button.svelte24
-rw-r--r--code/app/src/configuration/index.ts4
-rw-r--r--code/app/src/help/cache.ts38
-rw-r--r--code/app/src/help/global-state.ts22
-rw-r--r--code/app/src/help/md5.ts48
-rw-r--r--code/app/src/help/persistent-store.ts9
-rw-r--r--code/app/src/models/internal/IForm.ts15
-rw-r--r--code/app/src/routes/(main)/(app)/+layout.svelte27
-rw-r--r--code/app/src/routes/(main)/(app)/projects/+page.svelte171
-rw-r--r--code/app/src/routes/(main)/(public)/portal/+page.svelte26
-rw-r--r--code/app/src/routes/(main)/(public)/portal/+page.ts10
-rw-r--r--code/app/src/routes/(main)/(public)/reset-password/+page.svelte11
-rw-r--r--code/app/src/routes/(main)/(public)/reset-password/[id]/+page.svelte41
-rw-r--r--code/app/src/routes/(main)/(public)/sign-in/+page.svelte106
-rw-r--r--code/app/src/routes/(main)/+layout.server.ts74
-rw-r--r--code/app/src/services/abstractions/IAccountService.ts10
-rw-r--r--code/app/src/services/account-service.ts28
-rw-r--r--code/app/src/services/password-reset-service.ts17
-rw-r--r--code/app/src/services/settings-service.ts5
20 files changed, 416 insertions, 272 deletions
diff --git a/code/api/src/Endpoints/Internal/Root/ValidateRoute.cs b/code/api/src/Endpoints/Internal/Root/ValidateRoute.cs
index 682a869..428a1a2 100644
--- a/code/api/src/Endpoints/Internal/Root/ValidateRoute.cs
+++ b/code/api/src/Endpoints/Internal/Root/ValidateRoute.cs
@@ -8,7 +8,7 @@ public class ValidateRoute : RouteBaseSync.WithRequest<ValidateRoute.QueryParams
public ValidateRoute(UserService userService, VaultService vaultService) {
_userService = userService;
var c = vaultService.GetCurrentAppConfiguration();
- _continueTo = c.CANONICAL_FRONTEND_URL + "/?act=email-validated";
+ _continueTo = c.CANONICAL_FRONTEND_URL + "/portal?msg=emailValidated";
}
public class QueryParams
diff --git a/code/app/src/components/button.svelte b/code/app/src/components/button.svelte
index d573d01..f92be97 100644
--- a/code/app/src/components/button.svelte
+++ b/code/app/src/components/button.svelte
@@ -5,7 +5,7 @@
<script lang="ts">
import pwKey from "$actions/pwKey";
- import {SpinnerIcon} from "./icons";
+ import { SpinnerIcon } from "./icons";
export let kind = "primary" as ButtonKind;
export let size = "md" as ButtonSize;
@@ -75,30 +75,30 @@
{#if href}
<a
- use:pwKey={_pwKey}
- {...shared_props}
- {href}
- class="{sizeClasses} {kindClasses} {loading ? 'disabled:' : ''} {$$restProps.class ?? ''} {fullWidth
+ use:pwKey={_pwKey}
+ {...shared_props}
+ {href}
+ class="{sizeClasses} {kindClasses} {loading ? 'disabled:' : ''} {$$restProps.class ?? ''} {fullWidth
? 'w-full justify-center'
- : ''} inline-flex items-center border font-medium rounded shadow-sm focus:outline-none focus:ring-2"
+ : ''} disabled:cursor-not-allowed inline-flex items-center border font-medium rounded shadow-sm focus:outline-none focus:ring-2"
>
{#if loading}
- <SpinnerIcon class={spinnerTextClasses + " " + spinnerMarginClasses}/>
+ <SpinnerIcon class={spinnerTextClasses + " " + spinnerMarginClasses} />
{/if}
{text}
</a>
{:else}
<button
- use:pwKey={_pwKey}
- {...shared_props}
- on:click
- class="btn {sizeClasses} {kindClasses} {$$restProps.class ?? ''}
+ use:pwKey={_pwKey}
+ {...shared_props}
+ on:click
+ class="btn {sizeClasses} {kindClasses} {$$restProps.class ?? ''}
{fullWidth
? 'w-full justify-center'
: ''} inline-flex items-center border font-medium rounded shadow-sm focus:outline-none focus:ring-2"
>
{#if loading}
- <SpinnerIcon class={spinnerTextClasses + " " + spinnerMarginClasses}/>
+ <SpinnerIcon class={spinnerTextClasses + " " + spinnerMarginClasses} />
{/if}
{text}
</button>
diff --git a/code/app/src/configuration/index.ts b/code/app/src/configuration/index.ts
index 8e4334f..f4fb87b 100644
--- a/code/app/src/configuration/index.ts
+++ b/code/app/src/configuration/index.ts
@@ -57,4 +57,6 @@ export const StorageKeys = {
entries: "entries",
stopwatch: "stopwatchState",
logLevel: "logLevel",
-}; \ No newline at end of file
+};
+
+export type PortalMessage = "emailValidated"; \ No newline at end of file
diff --git a/code/app/src/help/cache.ts b/code/app/src/help/cache.ts
new file mode 100644
index 0000000..e253399
--- /dev/null
+++ b/code/app/src/help/cache.ts
@@ -0,0 +1,38 @@
+import { Temporal } from "temporal-polyfill";
+import { log_debug } from "$help/logger";
+
+let cache = {};
+
+export const CacheKeys = {
+ isAuthenticated: "isAuthenticated"
+}
+
+export async function cached_result_async<T>(key: string, staleAfterSeconds: number, get_result: any, forceRefresh: boolean = false) {
+ if (!cache[key]) {
+ cache[key] = {
+ l: 0,
+ c: undefined as T,
+ };
+ }
+ const staleEpoch = ((cache[key]?.l ?? 0) + staleAfterSeconds);
+ const isStale = forceRefresh || (staleEpoch < Temporal.Now.instant().epochSeconds);
+ if (isStale || !cache[key]?.c) {
+ cache[key].c = await get_result();
+ cache[key].l = Temporal.Now.instant().epochSeconds;
+ }
+
+ log_debug("Ran cached_result_async", {
+ cacheKey: key,
+ isStale,
+ cache: cache[key],
+ staleEpoch,
+ });
+
+ return cache[key].c as T;
+}
+
+export function clear_cache(key: string) {
+ if (!key) throw new Error("No key was specified");
+ cache[key].c = undefined;
+ log_debug("Cleared cache with key: " + key);
+} \ No newline at end of file
diff --git a/code/app/src/help/global-state.ts b/code/app/src/help/global-state.ts
new file mode 100644
index 0000000..a253ae9
--- /dev/null
+++ b/code/app/src/help/global-state.ts
@@ -0,0 +1,22 @@
+import { get } from "svelte/store";
+import { writable_persistent } from "./persistent-store";
+
+const state = writable_persistent<any>({
+ initialState: {},
+ name: "global-state"
+});
+
+export type GlobalStateKeys = "isLoggedIn" | "showEmailValidatedAlertWhenLoggedIn" | "all";
+
+export function fgs(key: GlobalStateKeys): any {
+ const value = get(state);
+ if (key === "all") return value;
+ return value[key];
+}
+
+export function sgs(key: GlobalStateKeys, value: any) {
+ if (key === "all") throw new Error("Not allowed to set global state key: all");
+ const stateValue = get(state);
+ stateValue[key] = JSON.stringify(value)
+ state.set(stateValue);
+} \ No newline at end of file
diff --git a/code/app/src/help/md5.ts b/code/app/src/help/md5.ts
new file mode 100644
index 0000000..0265194
--- /dev/null
+++ b/code/app/src/help/md5.ts
@@ -0,0 +1,48 @@
+// A formatted version of a popular md5 implementation.
+// Original copyright (c) Paul Johnston & Greg Holt.
+// The function itself is now 42 lines long.
+// https://stackoverflow.com/a/60467595 "Don't deny."
+
+export function md5(inputString: string): string {
+ const hc = "0123456789abcdef";
+ function rh(n) { var j, s = ""; for (j = 0; j <= 3; j++) s += hc.charAt((n >> (j * 8 + 4)) & 0x0F) + hc.charAt((n >> (j * 8)) & 0x0F); return s; }
+ function ad(x, y) { var l = (x & 0xFFFF) + (y & 0xFFFF); var m = (x >> 16) + (y >> 16) + (l >> 16); return (m << 16) | (l & 0xFFFF); }
+ function rl(n, c) { return (n << c) | (n >>> (32 - c)); }
+ function cm(q, a, b, x, s, t) { return ad(rl(ad(ad(a, q), ad(x, t)), s), b); }
+ function ff(a, b, c, d, x, s, t) { return cm((b & c) | ((~b) & d), a, b, x, s, t); }
+ function gg(a, b, c, d, x, s, t) { return cm((b & d) | (c & (~d)), a, b, x, s, t); }
+ function hh(a, b, c, d, x, s, t) { return cm(b ^ c ^ d, a, b, x, s, t); }
+ function ii(a, b, c, d, x, s, t) { return cm(c ^ (b | (~d)), a, b, x, s, t); }
+ function sb(x) {
+ var i; var nblk = ((x.length + 8) >> 6) + 1; var blks = new Array(nblk * 16); for (i = 0; i < nblk * 16; i++) blks[i] = 0;
+ for (i = 0; i < x.length; i++) blks[i >> 2] |= x.charCodeAt(i) << ((i % 4) * 8);
+ blks[i >> 2] |= 0x80 << ((i % 4) * 8); blks[nblk * 16 - 2] = x.length * 8; return blks;
+ }
+ var i, x = sb(inputString), a = 1732584193, b = -271733879, c = -1732584194, d = 271733878, olda, oldb, oldc, oldd;
+ for (i = 0; i < x.length; i += 16) {
+ olda = a; oldb = b; oldc = c; oldd = d;
+ a = ff(a, b, c, d, x[i + 0], 7, -680876936); d = ff(d, a, b, c, x[i + 1], 12, -389564586); c = ff(c, d, a, b, x[i + 2], 17, 606105819);
+ b = ff(b, c, d, a, x[i + 3], 22, -1044525330); a = ff(a, b, c, d, x[i + 4], 7, -176418897); d = ff(d, a, b, c, x[i + 5], 12, 1200080426);
+ c = ff(c, d, a, b, x[i + 6], 17, -1473231341); b = ff(b, c, d, a, x[i + 7], 22, -45705983); a = ff(a, b, c, d, x[i + 8], 7, 1770035416);
+ d = ff(d, a, b, c, x[i + 9], 12, -1958414417); c = ff(c, d, a, b, x[i + 10], 17, -42063); b = ff(b, c, d, a, x[i + 11], 22, -1990404162);
+ a = ff(a, b, c, d, x[i + 12], 7, 1804603682); d = ff(d, a, b, c, x[i + 13], 12, -40341101); c = ff(c, d, a, b, x[i + 14], 17, -1502002290);
+ b = ff(b, c, d, a, x[i + 15], 22, 1236535329); a = gg(a, b, c, d, x[i + 1], 5, -165796510); d = gg(d, a, b, c, x[i + 6], 9, -1069501632);
+ c = gg(c, d, a, b, x[i + 11], 14, 643717713); b = gg(b, c, d, a, x[i + 0], 20, -373897302); a = gg(a, b, c, d, x[i + 5], 5, -701558691);
+ d = gg(d, a, b, c, x[i + 10], 9, 38016083); c = gg(c, d, a, b, x[i + 15], 14, -660478335); b = gg(b, c, d, a, x[i + 4], 20, -405537848);
+ a = gg(a, b, c, d, x[i + 9], 5, 568446438); d = gg(d, a, b, c, x[i + 14], 9, -1019803690); c = gg(c, d, a, b, x[i + 3], 14, -187363961);
+ b = gg(b, c, d, a, x[i + 8], 20, 1163531501); a = gg(a, b, c, d, x[i + 13], 5, -1444681467); d = gg(d, a, b, c, x[i + 2], 9, -51403784);
+ c = gg(c, d, a, b, x[i + 7], 14, 1735328473); b = gg(b, c, d, a, x[i + 12], 20, -1926607734); a = hh(a, b, c, d, x[i + 5], 4, -378558);
+ d = hh(d, a, b, c, x[i + 8], 11, -2022574463); c = hh(c, d, a, b, x[i + 11], 16, 1839030562); b = hh(b, c, d, a, x[i + 14], 23, -35309556);
+ a = hh(a, b, c, d, x[i + 1], 4, -1530992060); d = hh(d, a, b, c, x[i + 4], 11, 1272893353); c = hh(c, d, a, b, x[i + 7], 16, -155497632);
+ b = hh(b, c, d, a, x[i + 10], 23, -1094730640); a = hh(a, b, c, d, x[i + 13], 4, 681279174); d = hh(d, a, b, c, x[i + 0], 11, -358537222);
+ c = hh(c, d, a, b, x[i + 3], 16, -722521979); b = hh(b, c, d, a, x[i + 6], 23, 76029189); a = hh(a, b, c, d, x[i + 9], 4, -640364487);
+ d = hh(d, a, b, c, x[i + 12], 11, -421815835); c = hh(c, d, a, b, x[i + 15], 16, 530742520); b = hh(b, c, d, a, x[i + 2], 23, -995338651);
+ a = ii(a, b, c, d, x[i + 0], 6, -198630844); d = ii(d, a, b, c, x[i + 7], 10, 1126891415); c = ii(c, d, a, b, x[i + 14], 15, -1416354905);
+ b = ii(b, c, d, a, x[i + 5], 21, -57434055); a = ii(a, b, c, d, x[i + 12], 6, 1700485571); d = ii(d, a, b, c, x[i + 3], 10, -1894986606);
+ c = ii(c, d, a, b, x[i + 10], 15, -1051523); b = ii(b, c, d, a, x[i + 1], 21, -2054922799); a = ii(a, b, c, d, x[i + 8], 6, 1873313359);
+ d = ii(d, a, b, c, x[i + 15], 10, -30611744); c = ii(c, d, a, b, x[i + 6], 15, -1560198380); b = ii(b, c, d, a, x[i + 13], 21, 1309151649);
+ a = ii(a, b, c, d, x[i + 4], 6, -145523070); d = ii(d, a, b, c, x[i + 11], 10, -1120210379); c = ii(c, d, a, b, x[i + 2], 15, 718787259);
+ b = ii(b, c, d, a, x[i + 9], 21, -343485551); a = ad(a, olda); b = ad(b, oldb); c = ad(c, oldc); d = ad(d, oldd);
+ }
+ return rh(a) + rh(b) + rh(c) + rh(d);
+} \ No newline at end of file
diff --git a/code/app/src/help/persistent-store.ts b/code/app/src/help/persistent-store.ts
index 6a54282..cb12547 100644
--- a/code/app/src/help/persistent-store.ts
+++ b/code/app/src/help/persistent-store.ts
@@ -1,6 +1,7 @@
import {browser} from "$app/environment";
import {writable as _writable, readable as _readable} from "svelte/store";
import type {Writable, Readable, StartStopNotifier} from "svelte/store";
+import { log_debug, log_info } from "./logger";
enum StoreType {
SESSION = 0,
@@ -76,11 +77,11 @@ function subscribe<T>(store: Writable<T> | Readable<T>, options: WritableStore<T
function writable_persistent<T>(options: WritableStore<T>): Writable<T> {
if (!browser) {
- console.warn("Persistent store is only available in the browser");
+ log_info("WARN: Persistent store is only available in the browser");
return;
}
if (options.options === undefined) options.options = default_store_options;
- console.log("Creating writable store with options: ", options);
+ log_debug("creating writable store with options: ", options);
const store = _writable<T>(options.initialState);
hydrate(store, options);
subscribe(store, options);
@@ -89,11 +90,11 @@ function writable_persistent<T>(options: WritableStore<T>): Writable<T> {
function readable_persistent<T>(options: ReadableStore<T>): Readable<T> {
if (!browser) {
- console.warn("Persistent store is only available in the browser");
+ log_info("WARN: Persistent store is only available in the browser");
return;
}
if (options.options === undefined) options.options = default_store_options;
- console.log("Creating readable store with options: ", options);
+ log_debug("Creating readable store with options: ", options);
const store = _readable<T>(options.initialState, options.callback);
// hydrate(store, options);
subscribe(store, options);
diff --git a/code/app/src/models/internal/IForm.ts b/code/app/src/models/internal/IForm.ts
new file mode 100644
index 0000000..c14b770
--- /dev/null
+++ b/code/app/src/models/internal/IForm.ts
@@ -0,0 +1,15 @@
+import type { FormError } from "./FormError";
+
+export interface IForm {
+ fields: Record<string, IFormField>;
+ error: FormError;
+ get_payload: Function;
+ submit_async: Function;
+ isLoading: boolean;
+ showError: boolean;
+}
+
+export interface IFormField {
+ value: any;
+ errors: Array<string>;
+}
diff --git a/code/app/src/routes/(main)/(app)/+layout.svelte b/code/app/src/routes/(main)/(app)/+layout.svelte
index e57bc3b..936e0a7 100644
--- a/code/app/src/routes/(main)/(app)/+layout.svelte
+++ b/code/app/src/routes/(main)/(app)/+layout.svelte
@@ -22,12 +22,13 @@
TransitionRoot,
} from "@rgossiaux/svelte-headlessui";
import { DialogPanel } from "@developermuch/dev-svelte-headlessui";
- import { Input } from "$components";
+ import { Input, Notification } from "$components";
import { goto } from "$app/navigation";
import { page } from "$app/stores";
+ import { onMount } from "svelte";
+ import { fgs, sgs } from "$help/global-state";
- const accountService = new AccountService();
-
+ const accountService = AccountService.resolve();
const session = {
profile: {
username: "Brukernavn",
@@ -37,6 +38,12 @@
let sidebarOpen = false;
let sidebarSearchValue: string | undefined;
+ let showEmailValidatedNotif = false;
+
+ onMount(() => {
+ showEmailValidatedNotif = fgs("showEmailValidatedAlertWhenLoggedIn") === "true";
+ if (showEmailValidatedNotif) sgs("showEmailValidatedAlertWhenLoggedIn", false);
+ });
function sign_out() {
accountService.end_session(() => goto("/sign-in"));
@@ -71,6 +78,20 @@
];
</script>
+{#if showEmailValidatedNotif}
+ <Notification
+ title="Email successfully validated"
+ subtitle="Because of this, you now have gained access to more functionality"
+ show={true}
+ />
+
+ <Notification
+ title="Email successfully validated"
+ subtitle="Because of this, you now have gained access to more functionality"
+ show={true}
+ />
+{/if}
+
<div class="min-h-full">
<!-- Mobile sidebar -->
<TransitionRoot show={sidebarOpen}>
diff --git a/code/app/src/routes/(main)/(app)/projects/+page.svelte b/code/app/src/routes/(main)/(app)/projects/+page.svelte
index 1508118..2585331 100644
--- a/code/app/src/routes/(main)/(app)/projects/+page.svelte
+++ b/code/app/src/routes/(main)/(app)/projects/+page.svelte
@@ -1,41 +1,14 @@
<script lang="ts">
- import {Button, ProjectStatusBadge, Input} from "$components";
- import type {Project} from "$models/projects/Project";
- import {onMount} from "svelte";
- import {faker} from "@faker-js/faker";
- import {Temporal} from "temporal-polyfill";
- import {createTable, Subscribe, Render} from "svelte-headless-table";
- import {addSortBy, addTableFilter} from "svelte-headless-table/plugins";
- import {ProjectStatus} from "$models/projects/ProjectStatus";
- import {writable, type Writable} from "svelte/store";
- import {
- ChevronDownIcon,
- ChevronUpIcon,
- ChevronUpDownIcon,
- MagnifyingGlassIcon,
- FunnelIcon,
- } from "$components/icons";
+ import { Button, ProjectStatusBadge, Input } from "$components";
+ import type { Project } from "$models/projects/Project";
+ import { createTable, Subscribe, Render } from "svelte-headless-table";
+ import { addSortBy, addTableFilter } from "svelte-headless-table/plugins";
+ import { writable, type Writable } from "svelte/store";
+ import { ChevronDownIcon, ChevronUpIcon, ChevronUpDownIcon, MagnifyingGlassIcon, FunnelIcon } from "$components/icons";
import LL from "$i18n/i18n-svelte";
- import {goto} from "$app/navigation";
+ import { goto } from "$app/navigation";
- let projects: Writable<Array<Project>> = writable([]);
-
- onMount(() => {
- let i = 0;
- const tempProjects = [];
- while (i < 101) {
- tempProjects.push({
- id: crypto.randomUUID(),
- name: faker.lorem.word(),
- start: Temporal.Now.plainDateISO().toLocaleString(),
- description: faker.lorem.words(3),
- members: [],
- status: ProjectStatus.IDLE,
- });
- i++;
- }
- projects.set(tempProjects);
- });
+ const projects: Writable<Array<Project>> = writable([]);
function on_open_project(event) {
if (event.code && (event.code !== "Enter" || event.code !== "Space")) return;
@@ -50,14 +23,14 @@
});
const columns = table.createColumns([
- table.column({header: $LL.name(), accessor: "name"}),
- table.column({header: "Status", accessor: "status"}),
- table.column({header: "Start", accessor: "start"}),
- table.column({header: "Description", accessor: "description", plugins: {sort: {disable: true}}}),
+ table.column({ header: $LL.name(), accessor: "name" }),
+ table.column({ header: "Status", accessor: "status" }),
+ table.column({ header: "Start", accessor: "start" }),
+ table.column({ header: "Description", accessor: "description", plugins: { sort: { disable: true } } }),
]);
- const {headerRows, rows, tableAttrs, tableBodyAttrs, pluginStates} = table.createViewModel(columns);
- const {filterValue} = pluginStates.filter;
+ const { headerRows, rows, tableAttrs, tableBodyAttrs, pluginStates } = table.createViewModel(columns);
+ const { filterValue } = pluginStates.filter;
</script>
<div class="sm:flex sm:items-center">
@@ -66,78 +39,80 @@
<p class="mt-2 text-sm text-gray-700">A list of all the projects in your organsation.</p>
</div>
<div class="mt-4 sm:mt-0 sm:ml-16 inline-flex gap-1 sm:flex-none">
- <Input icon={MagnifyingGlassIcon} placeholder="Search" bind:value={$filterValue}/>
- <Button text="Create project" href="/projects/create"/>
+ <Input icon={MagnifyingGlassIcon} placeholder="Search" bind:value={$filterValue} />
+ <Button text="Create project" href="/projects/create" />
</div>
</div>
<div class="-mx-2 mt-6 rounded-md shadow overflow-auto max-h-[80vh] sm:-mx-6 md:mx-0">
<table {...$tableAttrs} class="min-w-full divide-y divide-gray-300">
<thead class="bg-gray-50">
- {#each $headerRows as headerRow (headerRow.id)}
- <Subscribe rowAttrs={headerRow.attrs()} let:rowAttrs>
- <tr {...rowAttrs} class="shadow-sm">
- {#each headerRow.cells as cell (cell.id)}
- <Subscribe attrs={cell.attrs()} let:attrs props={cell.props()} let:props>
- <th
+ {#each $headerRows as headerRow (headerRow.id)}
+ <Subscribe rowAttrs={headerRow.attrs()} let:rowAttrs>
+ <tr {...rowAttrs} class="shadow-sm">
+ {#each headerRow.cells as cell (cell.id)}
+ <Subscribe attrs={cell.attrs()} let:attrs props={cell.props()} let:props>
+ <th
{...attrs}
scope="col"
class="sticky top-0 bg-gray-50 bg-opacity-100 whitespace-nowrap px-2 py-3.5 text-left text-sm font-semibold text-gray-900"
- >
- <div class="group inline-flex">
- <Render of={cell.render()}/>
- <span
+ >
+ <div class="group inline-flex">
+ <Render of={cell.render()} />
+ <span
on:click={props.sort.toggle}
on:keypress={props.sort.toggle}
class="{props.sort.disabled
- ? 'bg-gray-200 text-gray-900 group-hover:bg-gray-300'
- : 'invisible text-gray-400 group-hover:visible group-focus:visible'}
+ ? 'bg-gray-200 text-gray-900 group-hover:bg-gray-300'
+ : 'invisible text-gray-400 group-hover:visible group-focus:visible'}
{props.sort.disabled ? '' : 'cursor-pointer'}
ml-2 flex-none rounded"
- >
- {#if props.sort.order === "asc"}
- <ChevronUpIcon/>
- {:else if props.sort.order === "desc"}
- <ChevronDownIcon/>
- {:else if !props.sort.disabled}
- <ChevronUpDownIcon/>
- {/if}
- </span>
- {#if cell.id === "status"}
- <span class="invisible text-gray-400 cursor-pointer group-hover:visible group-focus:visible ml-2 flex-none rounded">
- <FunnelIcon/>
- </span>
- {/if}
- </div>
- </th>
- </Subscribe>
- {/each}
- </tr>
- </Subscribe>
- {/each}
+ >
+ {#if props.sort.order === "asc"}
+ <ChevronUpIcon />
+ {:else if props.sort.order === "desc"}
+ <ChevronDownIcon />
+ {:else if !props.sort.disabled}
+ <ChevronUpDownIcon />
+ {/if}
+ </span>
+ {#if cell.id === "status"}
+ <span
+ class="invisible text-gray-400 cursor-pointer group-hover:visible group-focus:visible ml-2 flex-none rounded"
+ >
+ <FunnelIcon />
+ </span>
+ {/if}
+ </div>
+ </th>
+ </Subscribe>
+ {/each}
+ </tr>
+ </Subscribe>
+ {/each}
</thead>
<tbody {...$tableBodyAttrs} class="divide-y divide-gray-200 bg-white">
- {#each $rows as row (row.id)}
- <Subscribe rowAttrs={row.attrs()} let:rowAttrs>
- <tr {...rowAttrs}>
- {#each row.cells as cell (cell.id)}
- {@const materialisedCell = cell.render()}
- <Subscribe attrs={cell.attrs()} let:attrs>
- <td {...attrs} class="whitespace-nowrap px-2 py-2 text-sm">
- {#if cell.id === "name"}
- <span class="link" title="Open project" on:click={on_open_project} on:keypress={on_open_project}>
- <Render of={materialisedCell}/>
- </span>
- {:else if cell.id === "status"}
- <ProjectStatusBadge status={materialisedCell.toString()}/>
- {:else}
- <Render of={materialisedCell}/>
- {/if}
- </td>
- </Subscribe>
- {/each}
- </tr>
- </Subscribe>
- {/each}
+ {#each $rows as row (row.id)}
+ <Subscribe rowAttrs={row.attrs()} let:rowAttrs>
+ <tr {...rowAttrs}>
+ {#each row.cells as cell (cell.id)}
+ {@const materialisedCell = cell.render()}
+ <Subscribe attrs={cell.attrs()} let:attrs>
+ <td {...attrs} class="whitespace-nowrap px-2 py-2 text-sm">
+ {#if cell.id === "name"}
+ <span class="link" title="Open project" on:click={on_open_project} on:keypress={on_open_project}>
+ <Render of={materialisedCell} />
+ </span>
+ {:else if cell.id === "status"}
+ <ProjectStatusBadge status={materialisedCell.toString()} />
+ {:else}
+ <Render of={materialisedCell} />
+ {/if}
+ </td>
+ </Subscribe>
+ {/each}
+ </tr>
+ </Subscribe>
+ {/each}
</tbody>
</table>
</div>
diff --git a/code/app/src/routes/(main)/(public)/portal/+page.svelte b/code/app/src/routes/(main)/(public)/portal/+page.svelte
new file mode 100644
index 0000000..bd6aa15
--- /dev/null
+++ b/code/app/src/routes/(main)/(public)/portal/+page.svelte
@@ -0,0 +1,26 @@
+<script lang="ts">
+ import { onMount } from "svelte";
+ import type { PageData } from "./$types";
+ import type { PortalMessage } from "$configuration";
+ import { goto } from "$app/navigation";
+ import { sgs } from "$help/global-state";
+
+ export let data: PageData;
+
+ onMount(async () => {
+ switch (data.message as PortalMessage) {
+ case "emailValidated": {
+ sgs("showEmailValidatedAlertWhenLoggedIn", true);
+ await goto("/home");
+ break;
+ }
+ default: {
+ await goto("/home");
+ }
+ }
+ });
+</script>
+
+<div class="p-3">
+ <h1>Warping...</h1>
+</div>
diff --git a/code/app/src/routes/(main)/(public)/portal/+page.ts b/code/app/src/routes/(main)/(public)/portal/+page.ts
new file mode 100644
index 0000000..49bf3db
--- /dev/null
+++ b/code/app/src/routes/(main)/(public)/portal/+page.ts
@@ -0,0 +1,10 @@
+import type { PortalMessage } from '$configuration';
+import { redirect } from '@sveltejs/kit';
+import type { PageLoad } from './$types';
+
+export const load: PageLoad = async ({ url }) => {
+ const queryParams = new URLSearchParams(url.search);
+ const message = queryParams.get("msg") as PortalMessage;
+ if (!message) throw redirect(302, "/");
+ return { message };
+}; \ No newline at end of file
diff --git a/code/app/src/routes/(main)/(public)/reset-password/+page.svelte b/code/app/src/routes/(main)/(public)/reset-password/+page.svelte
index 55859f6..a45ccdd 100644
--- a/code/app/src/routes/(main)/(public)/reset-password/+page.svelte
+++ b/code/app/src/routes/(main)/(public)/reset-password/+page.svelte
@@ -12,7 +12,7 @@
};
const formError = new FormError();
- const resetRequests = new PasswordResetService();
+ const passwordResetService = PasswordResetService.resolve();
let loading = false;
let showSuccessAlert = false;
@@ -23,7 +23,7 @@
showSuccessAlert = false;
showErrorAlert = false;
loading = true;
- const response = await resetRequests.create_request_async(formData.email.value);
+ const response = await passwordResetService.create_request_async(formData.email.value);
loading = false;
if (response.isCreated) {
showSuccessAlert = true;
@@ -37,17 +37,12 @@
}
}
} else {
- formError.title = $LL.unexpectedError();
- formError.subtitle = $LL.tryAgainSoon();
+ formError.set($LL.unexpectedError(), $LL.tryAgainSoon());
}
showErrorAlert = formError.has_error() && !showSuccessAlert;
}
</script>
-<svlete:head>
- <title>Reset password - Greatoffice</title>
-</svlete:head>
-
<div class="min-h-full flex flex-col justify-center py-12 sm:px-6 lg:px-8">
<div class="sm:mx-auto sm:w-full p-2 sm:p-0 sm:max-w-md">
<h2 class="mt-6 text-3xl tracking-tight font-bold text-gray-900">
diff --git a/code/app/src/routes/(main)/(public)/reset-password/[id]/+page.svelte b/code/app/src/routes/(main)/(public)/reset-password/[id]/+page.svelte
index 8f817bf..27a1af5 100644
--- a/code/app/src/routes/(main)/(public)/reset-password/[id]/+page.svelte
+++ b/code/app/src/routes/(main)/(public)/reset-password/[id]/+page.svelte
@@ -1,14 +1,15 @@
<script lang="ts">
- import {onMount} from "svelte";
+ import { onMount } from "svelte";
import LL from "$i18n/i18n-svelte";
- import {Alert, Input, Button} from "$components";
- import type {PageServerData} from "./$types";
- import {goto} from "$app/navigation";
- import {SignInPageMessage, signInPageMessageQueryKey} from "$routes/(main)/(public)/sign-in";
- import {PasswordResetService} from "$services/password-reset-service";
+ import { Alert, Input, Button } from "$components";
+ import type { PageServerData } from "./$types";
+ import { goto } from "$app/navigation";
+ import { SignInPageMessage, signInPageMessageQueryKey } from "$routes/(main)/(public)/sign-in";
+ import { PasswordResetService } from "$services/password-reset-service";
export let data: PageServerData;
- const passwordResets = new PasswordResetService();
+ const passwordResetService = PasswordResetService.resolve();
+
const formData = {
newPassword: {
value: "",
@@ -24,7 +25,7 @@
async function submitFormAsync() {
if (!canSubmit) return;
loading = true;
- const request = await passwordResets.fulfill_request_async(data.resetRequestId, formData.newPassword.value);
+ const request = await passwordResetService.fulfill_request_async(data.resetRequestId, formData.newPassword.value);
if (request.isFulfilled) {
goto("/sign-in?" + signInPageMessageQueryKey + "=" + SignInPageMessage.AFTER_PASSWORD_RESET);
} else if (request.knownProblem) {
@@ -33,7 +34,7 @@
}
onMount(async () => {
- const response = await passwordResets.request_is_valid_async(data.resetRequestId);
+ const response = await passwordResetService.request_is_valid_async(data.resetRequestId);
requestIsInvalid = !response.isValid;
finishedPreliminaryLoading = true;
});
@@ -57,19 +58,21 @@
<div class="bg-white py-8 px-4 shadow sm:rounded-lg sm:px-10">
<form class="space-y-6" on:submit|preventDefault={submitFormAsync}>
{#if requestIsInvalid}
- <Alert title={$LL.resetPasswordPage.invalidRequestTitle()}
- message={$LL.resetPasswordPage.invalidRequestMessage()}/>
+ <Alert
+ title={$LL.resetPasswordPage.invalidRequestTitle()}
+ message={$LL.resetPasswordPage.invalidRequestMessage()}
+ />
{/if}
<Input
- id="password"
- name="password"
- type="password"
- autocomplete="new-password"
- required
- bind:value={formData.newPassword.value}
- label={$LL.resetPasswordPage.newPassword()}
+ id="password"
+ name="password"
+ type="password"
+ autocomplete="new-password"
+ required
+ bind:value={formData.newPassword.value}
+ label={$LL.resetPasswordPage.newPassword()}
/>
- <Button text={$LL.submit()} type="submit" {loading} fullWidth/>
+ <Button text={$LL.submit()} type="submit" {loading} fullWidth />
</form>
</div>
</div>
diff --git a/code/app/src/routes/(main)/(public)/sign-in/+page.svelte b/code/app/src/routes/(main)/(public)/sign-in/+page.svelte
index c4ecb1a..66d4575 100644
--- a/code/app/src/routes/(main)/(public)/sign-in/+page.svelte
+++ b/code/app/src/routes/(main)/(public)/sign-in/+page.svelte
@@ -8,62 +8,62 @@
import { AccountService } from "$services/account-service";
import type { LoginPayload } from "$services/abstractions/IAccountService";
import { FormError } from "$models/internal/FormError";
+ import type { IForm } from "$models/internal/IForm";
- let loading = false;
- let showErrorAlert = false;
let messageType: SignInPageMessage | undefined = undefined;
- const accountService = new AccountService();
-
- const formData = {
- username: {
- value: "",
- errors: [],
- },
- password: {
- value: "",
- errors: [],
+ const accountService = AccountService.resolve();
+ const form = {
+ fields: {
+ username: {
+ value: "",
+ errors: [],
+ },
+ password: {
+ value: "",
+ errors: [],
+ },
+ persist: {
+ value: false,
+ errors: [],
+ },
},
- persist: {
- value: false,
- errors: [],
- },
- as_payload(): LoginPayload {
+ error: new FormError(),
+ isLoading: false,
+ showError: false,
+ get_payload(): LoginPayload {
return {
- password: formData.password.value,
- username: formData.username.value,
- persist: !formData.persist.value,
+ password: form.fields.password.value,
+ username: form.fields.username.value,
+ persist: !form.fields.persist.value,
};
},
- };
-
- const formError = new FormError();
+ async submit_async() {
+ console.log("sadf");
+ form.error.set();
+ form.showError = form.error.has_error();
+ form.isLoading = true;
+ const loginResponse = await accountService.login_async(form.get_payload());
+ if (loginResponse.isLoggedIn) {
+ await goto("/home");
+ } else if (loginResponse.knownProblem) {
+ form.error.set_from_known_problem(loginResponse.knownProblem);
+ } else {
+ form.error.set($LL.unexpectedError(), $LL.tryAgainSoon());
+ }
+ form.isLoading = false;
+ form.showError = form.error.has_error();
+ },
+ } as IForm;
onMount(() => {
- const searcher = new URLSearchParams(window.location.search);
- if (searcher.get(signInPageMessageQueryKey)) {
- messageType = searcher.get(signInPageMessageQueryKey) as SignInPageMessage;
- searcher.delete(signInPageMessageQueryKey);
- history.replaceState(null, "", window.location.origin + window.location.pathname);
+ const queryParams = new URLSearchParams(window.location.search);
+ if (queryParams.get(signInPageMessageQueryKey)) {
+ messageType = queryParams.get(signInPageMessageQueryKey) as SignInPageMessage;
+ queryParams.delete(signInPageMessageQueryKey);
+ window.history.replaceState(null, "", window.location.origin + window.location.pathname);
}
});
-
- async function submit_form_async() {
- formError.set();
- showErrorAlert = formError.has_error();
- loading = true;
- const loginResponse = await accountService.login_async(formData.as_payload());
- if (loginResponse.isLoggedIn) {
- await goto("/home");
- } else if (loginResponse.knownProblem) {
- formError.set_from_known_problem(loginResponse.knownProblem);
- } else {
- formError.title = $LL.unexpectedError();
- formError.subtitle = $LL.tryAgainSoon();
- }
- loading = false;
- showErrorAlert = formError.has_error();
- }
</script>
<div class="min-h-full flex flex-col justify-center py-12 sm:px-6 lg:px-8">
@@ -106,10 +106,10 @@
</div>
<div class="mt-8 sm:mx-auto sm:w-full sm:max-w-md">
<div class="bg-white py-8 px-4 shadow sm:rounded-lg sm:px-10">
- {#if showErrorAlert}
- <Alert title={formError.title} message={formError.subtitle} type="error" _pwKey={signInPageTestKeys.formErrorAlert} />
+ {#if form.showError}
+ <Alert title={form.error.title} message={form.error.subtitle} type="error" _pwKey={signInPageTestKeys.formErrorAlert} />
{/if}
- <form class="space-y-6 mt-2" use:pwKey={signInPageTestKeys.signInForm} on:submit|preventDefault={submit_form_async}>
+ <form class="space-y-6 mt-2" use:pwKey={signInPageTestKeys.signInForm} on:submit|preventDefault={() => form.submit_async()}>
<Input
id="username"
_pwKey={signInPageTestKeys.usernameInput}
@@ -117,7 +117,8 @@
type="email"
label={$LL.emailAddress()}
required
- bind:value={formData.username.value}
+ errors={form.fields.username.errors}
+ bind:value={form.fields.username.value}
/>
<Input
@@ -128,7 +129,8 @@
_pwKey={signInPageTestKeys.passwordInput}
autocomplete="current-password"
required
- bind:value={formData.password.value}
+ errors={form.fields.password.errors}
+ bind:value={form.fields.password.value}
/>
<div class="flex items-center justify-between">
@@ -136,7 +138,7 @@
id="remember-me"
_pwKey={signInPageTestKeys.rememberMeCheckbox}
name="remember-me"
- bind:checked={formData.persist.value}
+ bind:checked={form.fields.persist.value}
label={$LL.signInPage.notMyComputer()}
/>
<div class="text-sm">
@@ -146,7 +148,7 @@
</div>
</div>
- <Button text={$LL.submit()} fullWidth type="submit" {loading} />
+ <Button text={$LL.submit()} fullWidth type="submit" loading={form.isLoading} />
</form>
</div>
</div>
diff --git a/code/app/src/routes/(main)/+layout.server.ts b/code/app/src/routes/(main)/+layout.server.ts
index 4199d7f..b040b8f 100644
--- a/code/app/src/routes/(main)/+layout.server.ts
+++ b/code/app/src/routes/(main)/+layout.server.ts
@@ -1,35 +1,41 @@
-import {api_base, CookieNames} from "$configuration";
-import {log_debug, log_error} from "$help/logger";
-import {error, redirect} from "@sveltejs/kit";
-import {Temporal} from "temporal-polyfill";
-import type {LayoutServerLoad} from "./$types";
+import { api_base, CookieNames } from "$configuration";
+import { cached_result_async, CacheKeys } from "$help/cache";
+import { log_debug, log_error } from "$help/logger";
+import { md5 } from "$help/md5";
+import { error, redirect } from "@sveltejs/kit";
+import type { LayoutServerLoad } from "./$types";
-export const load: LayoutServerLoad = async ({url, request, route, cookies, locals, fetch}) => {
- console.log(url.toString());
+export const load: LayoutServerLoad = async ({ route, cookies, locals, fetch }) => {
const isBaseRoute = route.id === "/(main)";
- const isPublicRoute = (route.id?.startsWith("/(main)/(public)") || isBaseRoute) ?? true;
+ const isPortalRoute = route.id === "/(main)/(public)/portal";
+ const isPublicRoute = (isBaseRoute || (route.id?.startsWith("/(main)/(public)") ?? false)) ?? true;
const sessionCookieValue = cookies.get(CookieNames.session);
- const hasSessionCookie = (sessionCookieValue?.length > 0 ?? false);
- const sessionIsValid = hasSessionCookie && (await cached_result_async<Response>("sessionCheck", 120, () => fetch(api_base("_/is-authenticated"), {
- headers: {
- Cookie: CookieNames.session + "=" + sessionCookieValue,
- },
- }).catch((e) => {
- log_error(e);
- throw error(503, {
- message: "We are experiencing a service disruption! Have patience while we resolve the issue.",
- });
- }))).ok;
+ let sessionIsValid = false;
+ if ((sessionCookieValue?.length > 0 ?? false)) {
+ const sessionHash = md5(sessionCookieValue);
+ sessionIsValid = (await cached_result_async<Response>(sessionHash + "_" + CacheKeys.isAuthenticated, 120, () => fetch(api_base("_/is-authenticated"), {
+ headers: {
+ Cookie: CookieNames.session + "=" + sessionCookieValue,
+ },
+ }).catch((e) => {
+ log_error(e);
+ throw error(503, {
+ message: "We are experiencing a service disruption! Have patience while we resolve the issue.",
+ });
+ }))).ok;
+ }
log_debug("Base Layout loaded", {
sessionIsValid,
isPublicRoute,
+ isBaseRoute,
+ isPortalRoute,
routeId: route.id,
});
- if (sessionIsValid && isPublicRoute) {
+ if (sessionIsValid && isPublicRoute && !isPortalRoute) {
throw redirect(302, "/home");
- } else if (isBaseRoute || !sessionIsValid && !isPublicRoute) {
+ } else if (!isPortalRoute && (isBaseRoute || !sessionIsValid && !isPublicRoute)) {
throw redirect(302, "/sign-in");
}
@@ -37,29 +43,3 @@ export const load: LayoutServerLoad = async ({url, request, route, cookies, loca
locale: locals.locale,
};
};
-
-let resultCache = {};
-
-async function cached_result_async<T>(key: string, staleAfterSeconds: number, get_result: any, forceRefresh: boolean = false) {
- if (!resultCache[key]) {
- resultCache[key] = {
- l: 0,
- c: undefined as T,
- };
- }
- const staleEpoch = ((resultCache[key]?.l ?? 0) + staleAfterSeconds);
- const isStale = forceRefresh || (staleEpoch < Temporal.Now.instant().epochSeconds);
- if (isStale || !resultCache[key]?.c) {
- resultCache[key].c = await get_result();
- resultCache[key].l = Temporal.Now.instant().epochSeconds;
- }
-
- log_debug("Ran cached_result_async", {
- cacheKey: key,
- isStale,
- cache: resultCache[key],
- staleEpoch,
- });
-
- return resultCache[key].c as T;
-} \ No newline at end of file
diff --git a/code/app/src/services/abstractions/IAccountService.ts b/code/app/src/services/abstractions/IAccountService.ts
index 0645eb6..ad0c45b 100644
--- a/code/app/src/services/abstractions/IAccountService.ts
+++ b/code/app/src/services/abstractions/IAccountService.ts
@@ -1,17 +1,13 @@
-import type {KnownProblem} from "$models/internal/KnownProblem";
-import type {Writable} from "svelte/store";
+import type { KnownProblem } from "$models/internal/KnownProblem";
+import type { Writable } from "svelte/store";
export interface IAccountService {
session: Writable<Session>,
-
login_async(payload: LoginPayload): Promise<LoginResponse>,
-
logout_async(): Promise<void>,
-
+ end_session(callback: Function): Promise<void>,
create_account_async(payload: CreateAccountPayload): Promise<CreateAccountResponse>,
-
delete_current_async(): Promise<DeleteAccountResponse>,
-
update_current_async(payload: UpdateAccountPayload): Promise<UpdateAccountResponse>,
}
diff --git a/code/app/src/services/account-service.ts b/code/app/src/services/account-service.ts
index 92c6126..330e588 100644
--- a/code/app/src/services/account-service.ts
+++ b/code/app/src/services/account-service.ts
@@ -1,12 +1,12 @@
-import {http_delete_async, http_get_async, http_post_async} from "$api/_fetch";
-import {browser} from "$app/environment";
-import {api_base, CookieNames, StorageKeys} from "$configuration";
-import {is_known_problem} from "$models/internal/KnownProblem";
-import {log_debug} from "$help/logger";
-import {StoreType, writable_persistent} from "$help/persistent-store";
-import {get} from "svelte/store";
-import type {Writable} from "svelte/store";
-import {Temporal} from "temporal-polyfill";
+import { http_delete_async, http_get_async, http_post_async } from "$api/_fetch";
+import { browser } from "$app/environment";
+import { api_base, CookieNames, StorageKeys } from "$configuration";
+import { is_known_problem } from "$models/internal/KnownProblem";
+import { log_debug } from "$help/logger";
+import { StoreType, writable_persistent } from "$help/persistent-store";
+import { get } from "svelte/store";
+import type { Writable } from "svelte/store";
+import { Temporal } from "temporal-polyfill";
import type {
CreateAccountPayload,
CreateAccountResponse,
@@ -38,6 +38,10 @@ export class AccountService implements IAccountService {
}
}
+ static resolve(): IAccountService {
+ return new AccountService();
+ }
+
async refresh_session(forceRefresh: boolean = false): Promise<void> {
if (!this.session) return;
const currentValue = get(this.session);
@@ -67,7 +71,7 @@ export class AccountService implements IAccountService {
async login_async(payload: LoginPayload): Promise<LoginResponse> {
const response = await http_post_async(api_base("_/account/login"), payload);
- if (response.ok) return {isLoggedIn: true};
+ if (response.ok) return { isLoggedIn: true };
if (is_known_problem(response)) return {
isLoggedIn: false,
knownProblem: await response.json(),
@@ -90,7 +94,7 @@ export class AccountService implements IAccountService {
async create_account_async(payload: CreateAccountPayload): Promise<CreateAccountResponse> {
const response = await http_post_async(api_base("_/account/create"), payload);
- if (response.ok) return {isCreated: true};
+ if (response.ok) return { isCreated: true };
if (is_known_problem(response)) return {
isCreated: false,
knownProblem: await response.json(),
@@ -109,7 +113,7 @@ export class AccountService implements IAccountService {
async update_current_async(payload: UpdateAccountPayload): Promise<UpdateAccountResponse> {
const response = await http_post_async(api_base("_/account/update"), payload);
- if (response.ok) return {isUpdated: true};
+ if (response.ok) return { isUpdated: true };
if (is_known_problem(response)) return {
isUpdated: false,
knownProblem: await response.json(),
diff --git a/code/app/src/services/password-reset-service.ts b/code/app/src/services/password-reset-service.ts
index ab3a953..d7700b3 100644
--- a/code/app/src/services/password-reset-service.ts
+++ b/code/app/src/services/password-reset-service.ts
@@ -1,6 +1,6 @@
-import {http_get_async, http_post_async} from "$api/_fetch";
-import {api_base} from "$configuration";
-import {is_known_problem} from "$models/internal/KnownProblem";
+import { http_get_async, http_post_async } from "$api/_fetch";
+import { api_base } from "$configuration";
+import { is_known_problem } from "$models/internal/KnownProblem";
import type {
CreateRequestResponse,
FulfillRequestResponse,
@@ -9,9 +9,12 @@ import type {
} from "./abstractions/IPasswordResetService";
export class PasswordResetService implements IPasswordResetService {
+ static resolve(): IPasswordResetService {
+ return new PasswordResetService();
+ }
async create_request_async(email: string): Promise<CreateRequestResponse> {
- const response = await http_post_async(api_base("_/password-reset-request/create"), {email});
- if (response.ok) return {isCreated: true};
+ const response = await http_post_async(api_base("_/password-reset-request/create"), { email });
+ if (response.ok) return { isCreated: true };
if (is_known_problem(response)) return {
isCreated: false,
knownProblem: await response.json(),
@@ -23,8 +26,8 @@ export class PasswordResetService implements IPasswordResetService {
}
async fulfill_request_async(id: string, newPassword: string): Promise<FulfillRequestResponse> {
- const response = await http_post_async(api_base("_/password-reset-request/fulfill"), {id: id, newPassword});
- if (response.ok) return {isFulfilled: true};
+ const response = await http_post_async(api_base("_/password-reset-request/fulfill"), { id: id, newPassword });
+ if (response.ok) return { isFulfilled: true };
if (is_known_problem(response)) return {
isFulfilled: false,
knownProblem: await response.json(),
diff --git a/code/app/src/services/settings-service.ts b/code/app/src/services/settings-service.ts
index 20053a9..7f8cb2b 100644
--- a/code/app/src/services/settings-service.ts
+++ b/code/app/src/services/settings-service.ts
@@ -1,6 +1,9 @@
-import type {ISettingsService} from "./abstractions/ISettingsService";
+import type { ISettingsService } from "./abstractions/ISettingsService";
export class SettingsService implements ISettingsService {
+ static resolve(): ISettingsService {
+ return new SettingsService();
+ }
get_user_settings(): Promise<void> {
throw new Error("Method not implemented.");
}