From a8219611cbebbd27501d9f30c804979048b98107 Mon Sep 17 00:00:00 2001 From: ivarlovlie Date: Tue, 13 Dec 2022 14:48:11 +0100 Subject: 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 --- code/app/src/components/button.svelte | 24 +-- code/app/src/configuration/index.ts | 4 +- code/app/src/help/cache.ts | 38 +++++ code/app/src/help/global-state.ts | 22 +++ code/app/src/help/md5.ts | 48 ++++++ code/app/src/help/persistent-store.ts | 9 +- code/app/src/models/internal/IForm.ts | 15 ++ code/app/src/routes/(main)/(app)/+layout.svelte | 27 +++- .../src/routes/(main)/(app)/projects/+page.svelte | 171 +++++++++------------ .../src/routes/(main)/(public)/portal/+page.svelte | 26 ++++ .../app/src/routes/(main)/(public)/portal/+page.ts | 10 ++ .../(main)/(public)/reset-password/+page.svelte | 11 +- .../(public)/reset-password/[id]/+page.svelte | 41 ++--- .../routes/(main)/(public)/sign-in/+page.svelte | 106 ++++++------- code/app/src/routes/(main)/+layout.server.ts | 76 ++++----- .../src/services/abstractions/IAccountService.ts | 10 +- code/app/src/services/account-service.ts | 28 ++-- code/app/src/services/password-reset-service.ts | 17 +- code/app/src/services/settings-service.ts | 5 +- 19 files changed, 416 insertions(+), 272 deletions(-) create mode 100644 code/app/src/help/cache.ts create mode 100644 code/app/src/help/global-state.ts create mode 100644 code/app/src/help/md5.ts create mode 100644 code/app/src/models/internal/IForm.ts create mode 100644 code/app/src/routes/(main)/(public)/portal/+page.svelte create mode 100644 code/app/src/routes/(main)/(public)/portal/+page.ts (limited to 'code/app/src') 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 @@ +{#if showEmailValidatedNotif} + + + +{/if} +
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 @@
@@ -66,78 +39,80 @@

A list of all the projects in your organsation.

- -
- {#each $headerRows as headerRow (headerRow.id)} - - - {#each headerRow.cells as cell (cell.id)} - - + {#each headerRow.cells as cell (cell.id)} + + - - {/each} - - - {/each} + > + {#if props.sort.order === "asc"} + + {:else if props.sort.order === "desc"} + + {:else if !props.sort.disabled} + + {/if} + + {#if cell.id === "status"} + + {/if} + + + + {/each} + + + {/each} - {#each $rows as row (row.id)} - - - {#each row.cells as cell (cell.id)} - {@const materialisedCell = cell.render()} - - - - {/each} - - - {/each} + {#each $rows as row (row.id)} + + + {#each row.cells as cell (cell.id)} + {@const materialisedCell = cell.render()} + + + + {/each} + + + {/each}
+
-
- - +
+ + - {#if props.sort.order === "asc"} - - {:else if props.sort.order === "desc"} - - {:else if !props.sort.disabled} - - {/if} - - {#if cell.id === "status"} - - {/if} -
-
- {#if cell.id === "name"} - - - - {:else if cell.id === "status"} - - {:else} - - {/if} -
+ {#if cell.id === "name"} + + + + {:else if cell.id === "status"} + + {:else} + + {/if} +
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 @@ + + +
+

Warping...

+
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; } - - Reset password - Greatoffice - -

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 @@
@@ -106,10 +106,10 @@
- {#if showErrorAlert} - + {#if form.showError} + {/if} -
+ form.submit_async()}>
@@ -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()} />
@@ -146,7 +148,7 @@
-
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"; - -export const load: LayoutServerLoad = async ({url, request, route, cookies, locals, fetch}) => { - console.log(url.toString()); +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 ({ 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("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(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(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, - login_async(payload: LoginPayload): Promise, - logout_async(): Promise, - + end_session(callback: Function): Promise, create_account_async(payload: CreateAccountPayload): Promise, - delete_current_async(): Promise, - update_current_async(payload: UpdateAccountPayload): Promise, } 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 { if (!this.session) return; const currentValue = get(this.session); @@ -67,7 +71,7 @@ export class AccountService implements IAccountService { async login_async(payload: LoginPayload): Promise { 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 { 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 { 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 { - 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 { - 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 { throw new Error("Method not implemented."); } -- cgit v1.3