diff options
Diffstat (limited to 'code/app')
18 files changed, 334 insertions, 224 deletions
diff --git a/code/app/.typesafe-i18n.json b/code/app/.typesafe-i18n.json index a856d24..42cea32 100644 --- a/code/app/.typesafe-i18n.json +++ b/code/app/.typesafe-i18n.json @@ -1,5 +1,5 @@ { "adapter": "svelte", "$schema": "https://unpkg.com/typesafe-i18n@5.17.1/schema/typesafe-i18n.json", - "outputPath": "src/lib/i18n" + "outputPath": "src/i18n" }
\ No newline at end of file diff --git a/code/app/src/configuration/index.ts b/code/app/src/configuration/index.ts index 9b03b66..a0ec66d 100644 --- a/code/app/src/configuration/index.ts +++ b/code/app/src/configuration/index.ts @@ -1,4 +1,4 @@ -export const BASE_DOMAIN = "dev.greatoffice.life"; +export const BASE_DOMAIN = "stage.greatoffice.app"; export const DEV_BASE_DOMAIN = "http://localhost"; export const API_ADDRESS = "https://api." + BASE_DOMAIN; export const DEV_API_ADDRESS = "http://localhost:5000"; diff --git a/code/app/src/help/persistent-store.ts b/code/app/src/help/persistent-store.ts index f2c14c9..6a54282 100644 --- a/code/app/src/help/persistent-store.ts +++ b/code/app/src/help/persistent-store.ts @@ -1,3 +1,4 @@ +import {browser} from "$app/environment"; import {writable as _writable, readable as _readable} from "svelte/store"; import type {Writable, Readable, StartStopNotifier} from "svelte/store"; @@ -28,6 +29,7 @@ interface ReadableStore<T> { } function get_store(type: StoreType): Storage { + if (!browser) return undefined; switch (type) { case StoreType.SESSION: return window.sessionStorage; @@ -48,6 +50,7 @@ function prepared_store_value(value: any): string { function get_store_value<T>(options: WritableStore<T> | ReadableStore<T>): any { try { const storage = get_store(options.options.store); + if (!storage) return; const value = storage.getItem(options.name); if (!value) return false; return JSON.parse(value); @@ -64,6 +67,7 @@ function hydrate<T>(store: Writable<T>, options: WritableStore<T> | ReadableStor function subscribe<T>(store: Writable<T> | Readable<T>, options: WritableStore<T> | ReadableStore<T>): void { const storage = get_store(options.options.store); + if (!storage) return; if (!store.subscribe) return; store.subscribe((state: any) => { storage.setItem(options.name, prepared_store_value(state)); @@ -71,6 +75,10 @@ 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"); + return; + } if (options.options === undefined) options.options = default_store_options; console.log("Creating writable store with options: ", options); const store = _writable<T>(options.initialState); @@ -80,6 +88,10 @@ 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"); + return; + } if (options.options === undefined) options.options = default_store_options; console.log("Creating readable store with options: ", options); const store = _readable<T>(options.initialState, options.callback); diff --git a/code/app/src/i18n/en/index.ts b/code/app/src/i18n/en/index.ts index fbf5423..b38eb48 100644 --- a/code/app/src/i18n/en/index.ts +++ b/code/app/src/i18n/en/index.ts @@ -27,6 +27,7 @@ const en: BaseTranslation = { createRecordButtonText: "Press enter or click here to create {0}" }, signInPage: { + title: "Sign in", notMyComputer: "This is not my computer", resetPassword: "Reset password", yourPasswordIsUpdated: "Your password is updated", @@ -39,9 +40,12 @@ const en: BaseTranslation = { feelFreeToSignInAgain: "Feel free to sign in again" }, signUpPage: { + title: "Sign up", createYourNewAccount: "Create your new account", }, resetPasswordPage: { + title: "Reset password", + fulfillTitle: "Set new password", setANewPassword: "Set a new password", expired: "Expired", requestHasExpired: "Your request has expired", diff --git a/code/app/src/i18n/i18n-types.ts b/code/app/src/i18n/i18n-types.ts index cf968d7..ef1d664 100644 --- a/code/app/src/i18n/i18n-types.ts +++ b/code/app/src/i18n/i18n-types.ts @@ -117,6 +117,10 @@ type RootTranslation = { } signInPage: { /** + * Sign in + */ + title: string + /** * This is not my computer */ notMyComputer: string @@ -159,12 +163,24 @@ type RootTranslation = { } signUpPage: { /** + * Sign up + */ + title: string + /** * Create your new account */ createYourNewAccount: string } resetPasswordPage: { /** + * Reset password + */ + title: string + /** + * Set new password + */ + fulfillTitle: string + /** * Set a new password */ setANewPassword: string @@ -326,6 +342,10 @@ export type TranslationFunctions = { } signInPage: { /** + * Sign in + */ + title: () => LocalizedString + /** * This is not my computer */ notMyComputer: () => LocalizedString @@ -368,12 +388,24 @@ export type TranslationFunctions = { } signUpPage: { /** + * Sign up + */ + title: () => LocalizedString + /** * Create your new account */ createYourNewAccount: () => LocalizedString } resetPasswordPage: { /** + * Reset password + */ + title: () => LocalizedString + /** + * Set new password + */ + fulfillTitle: () => LocalizedString + /** * Set a new password */ setANewPassword: () => LocalizedString diff --git a/code/app/src/routes/(main)/(app)/+layout.svelte b/code/app/src/routes/(main)/(app)/+layout.svelte index 6cb70ef..e57bc3b 100644 --- a/code/app/src/routes/(main)/(app)/+layout.svelte +++ b/code/app/src/routes/(main)/(app)/+layout.svelte @@ -10,7 +10,7 @@ QueueListIcon, CalendarIcon, } from "$components/icons"; - import {AccountService} from "$services/account-service"; + import { AccountService } from "$services/account-service"; import { Dialog, Menu, @@ -21,11 +21,11 @@ TransitionChild, TransitionRoot, } from "@rgossiaux/svelte-headlessui"; - import {DialogPanel} from "@developermuch/dev-svelte-headlessui"; - import {Input} from "$components"; - import {goto} from "$app/navigation"; - import {page} from "$app/stores"; - + import { DialogPanel } from "@developermuch/dev-svelte-headlessui"; + import { Input } from "$components"; + import { goto } from "$app/navigation"; + import { page } from "$app/stores"; + const accountService = new AccountService(); const session = { @@ -76,45 +76,45 @@ <TransitionRoot show={sidebarOpen}> <Dialog as="div" class="relative z-40 lg:hidden" on:close={() => (sidebarOpen = false)}> <TransitionChild - as="div" - enter="transition-opacity ease-linear duration-300" - enterFrom="opacity-0" - enterTo="opacity-100" - leave="transition-opacity ease-linear duration-300" - leaveFrom="opacity-100" - leaveTo="opacity-0" + as="div" + enter="transition-opacity ease-linear duration-300" + enterFrom="opacity-0" + enterTo="opacity-100" + leave="transition-opacity ease-linear duration-300" + leaveFrom="opacity-100" + leaveTo="opacity-0" > - <div class="fixed inset-0 bg-gray-600 bg-opacity-75"/> + <div class="fixed inset-0 bg-gray-600 bg-opacity-75" /> </TransitionChild> <div class="fixed inset-0 z-40 flex"> <TransitionChild - as="div" - enter="transition ease-in-out duration-300 transform" - enterFrom="-translate-x-full" - enterTo="translate-x-0" - leave="transition ease-in-out duration-300 transform" - leaveFrom="translate-x-0" - leaveTo="-translate-x-full" + as="div" + enter="transition ease-in-out duration-300 transform" + enterFrom="-translate-x-full" + enterTo="translate-x-0" + leave="transition ease-in-out duration-300 transform" + leaveFrom="translate-x-0" + leaveTo="-translate-x-full" > <DialogPanel class="relative flex w-full max-w-xs flex-1 flex-col bg-white pt-5 pb-4"> <TransitionChild - as="div" - enter="ease-in-out duration-300" - enterFrom="opacity-0" - enterTo="opacity-100" - leave="ease-in-out duration-300" - leaveFrom="opacity-100" - leaveTo="opacity-0" + as="div" + enter="ease-in-out duration-300" + enterFrom="opacity-0" + enterTo="opacity-100" + leave="ease-in-out duration-300" + leaveFrom="opacity-100" + leaveTo="opacity-0" > <div class="absolute top-0 right-0 -mr-12 pt-2"> <button - type="button" - class="ml-1 flex h-10 w-10 items-center justify-center rounded-full focus:outline-none focus:ring-2 focus:ring-inset focus:ring-white" - on:click={() => (sidebarOpen = false)} + type="button" + class="ml-1 flex h-10 w-10 items-center justify-center rounded-full focus:outline-none focus:ring-2 focus:ring-inset focus:ring-white" + on:click={() => (sidebarOpen = false)} > <span class="sr-only">Close sidebar</span> - <XMarkIcon class="text-white" aria-hidden="true"/> + <XMarkIcon class="text-white" aria-hidden="true" /> </button> </div> </TransitionChild> @@ -124,15 +124,17 @@ {#each navigationItems as item} {@const current = $page.url.pathname.startsWith(item.href)} <a - href={item.href} - aria-current={current ? "page" : undefined} - class="group flex items-center px-2 py-2 text-base leading-5 font-medium rounded-md + href={item.href} + aria-current={current ? "page" : undefined} + class="group flex items-center px-2 py-2 text-base leading-5 font-medium rounded-md {current ? 'bg-gray-100 text-gray-900' : 'text-gray-600 hover:text-gray-900 hover:bg-gray-50'}" > <svelte:component - this={item.icon} - class="mr-3 flex-shrink-0 h-6 w-6 {current ? 'text-gray-500' : 'text-gray-400 group-hover:text-gray-500'}" - aria-hidden="true" + this={item.icon} + class="mr-3 flex-shrink-0 h-6 w-6 {current + ? 'text-gray-500' + : 'text-gray-400 group-hover:text-gray-500'}" + aria-hidden="true" /> {item.name} </a> @@ -155,52 +157,52 @@ <!-- User account dropdown --> <Menu class="relative inline-block text-left"> <MenuButton - class="group w-full rounded-md bg-gray-100 px-3.5 py-2 text-left text-sm font-medium text-gray-700 hover:bg-gray-200 focus:outline-none focus:ring-2 focus:ring-teal-500 focus:ring-offset-2 focus:ring-offset-gray-100" + class="group w-full rounded-md bg-gray-100 px-3.5 py-2 text-left text-sm font-medium text-gray-700 hover:bg-gray-200 focus:outline-none focus:ring-2 focus:ring-teal-500 focus:ring-offset-2 focus:ring-offset-gray-100" > - <span class="flex w-full items-center justify-between"> - <span class="flex min-w-0 items-center justify-between space-x-3"> - <span class="flex min-w-0 flex-1 flex-col"> - <span class="truncate text-sm font-medium text-gray-900"> - {session.profile.username} - </span> - <span class="truncate text-sm text-gray-500">{session.profile.displayName}</span> - </span> - </span> - <ChevronUpDownIcon class="flex-shrink-0 text-gray-400 group-hover:text-gray-500" aria-hidden="true"/> - </span> + <span class="flex w-full items-center justify-between"> + <span class="flex min-w-0 items-center justify-between space-x-3"> + <span class="flex min-w-0 flex-1 flex-col"> + <span class="truncate text-sm font-medium text-gray-900"> + {session.profile.username} + </span> + <span class="truncate text-sm text-gray-500">{session.profile.displayName}</span> + </span> + </span> + <ChevronUpDownIcon class="flex-shrink-0 text-gray-400 group-hover:text-gray-500" aria-hidden="true" /> + </span> </MenuButton> <Transition - leave="transition ease-in duration-75" - enter="transition ease-out duration-100" - enterFrom="transform opacity-0 scale-95" - enterTo="transform opacity-100 scale-100" - leaveFrom="transform opacity-100 scale-100" - leaveTo="transform opacity-0 scale-95" - as="div" + leave="transition ease-in duration-75" + enter="transition ease-out duration-100" + enterFrom="transform opacity-0 scale-95" + enterTo="transform opacity-100 scale-100" + leaveFrom="transform opacity-100 scale-100" + leaveTo="transform opacity-0 scale-95" + as="div" > <MenuItems - class="absolute right-0 left-0 z-10 mt-1 origin-top divide-y divide-gray-200 rounded-md bg-white shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none" + class="absolute right-0 left-0 z-10 mt-1 origin-top divide-y divide-gray-200 rounded-md bg-white shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none" > <div class="py-1"> <MenuItem> - <a href="/profile" - class="text-gray-700 block px-4 py-2 text-sm hover:text-gray-900 hover:bg-gray-100"> - View profile </a> + <a href="/profile" class="text-gray-700 block px-4 py-2 text-sm hover:text-gray-900 hover:bg-gray-100"> + View profile + </a> </MenuItem> <MenuItem> - <a href="/settings" - class="text-gray-700 block px-4 py-2 text-sm hover:text-gray-900 hover:bg-gray-100"> - Settings </a> + <a href="/settings" class="text-gray-700 block px-4 py-2 text-sm hover:text-gray-900 hover:bg-gray-100"> + Settings + </a> </MenuItem> </div> <div class="py-1"> <MenuItem> - <span - on:click={() => sign_out()} - class="text-gray-700 block px-4 py-2 text-sm hover:bg-red-200 hover:text-red-900 cursor-pointer" - > - Sign out - </span> + <span + on:click={() => sign_out()} + class="text-gray-700 block px-4 py-2 text-sm hover:bg-red-200 hover:text-red-900 cursor-pointer" + > + Sign out + </span> </MenuItem> </div> </MenuItems> @@ -210,8 +212,7 @@ <div class="mt-3 hidden"> <label for="search" class="sr-only">Search</label> <div class="relative mt-1 rounded-md shadow-sm"> - <Input type="search" name="search" icon={MagnifyingGlassIcon} placeholder="Search" - bind:value={sidebarSearchValue}/> + <Input type="search" name="search" icon={MagnifyingGlassIcon} placeholder="Search" bind:value={sidebarSearchValue} /> </div> </div> <!-- Navigation --> @@ -220,15 +221,15 @@ {#each navigationItems as item} {@const current = $page.url.pathname.startsWith(item.href)} <a - href={item.href} - aria-current={current ? "page" : undefined} - class="group flex items-center px-2 py-2 text-base leading-5 font-medium rounded-md + href={item.href} + aria-current={current ? "page" : undefined} + class="group flex items-center px-2 py-2 text-base leading-5 font-medium rounded-md {current ? 'bg-gray-200 text-gray-900' : 'text-gray-700 hover:text-gray-900 hover:bg-gray-50'}" > <svelte:component - this={item.icon} - class="mr-3 flex-shrink-0 h-6 w-6 {current ? 'text-gray-500' : 'text-gray-400 group-hover:text-gray-500'}" - aria-hidden="true" + this={item.icon} + class="mr-3 flex-shrink-0 h-6 w-6 {current ? 'text-gray-500' : 'text-gray-400 group-hover:text-gray-500'}" + aria-hidden="true" /> {item.name} </a> @@ -243,12 +244,12 @@ <!-- Search header --> <div class="sticky top-0 z-10 flex h-16 flex-shrink-0 border-b border-gray-200 bg-white lg:hidden"> <button - type="button" - class="border-r border-gray-200 px-4 text-gray-500 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-teal-500 lg:hidden" - on:click={() => (sidebarOpen = true)} + type="button" + class="border-r border-gray-200 px-4 text-gray-500 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-teal-500 lg:hidden" + on:click={() => (sidebarOpen = true)} > <span class="sr-only">Open sidebar</span> - <Bars3CenterLeftIcon aria-hidden="true"/> + <Bars3CenterLeftIcon aria-hidden="true" /> </button> <div class="flex flex-1 justify-between px-4 sm:px-6 lg:px-8"> <div class="flex flex-1"> @@ -256,12 +257,12 @@ <label for="search-field" class="sr-only">Search</label> <div class="relative w-full text-gray-400 focus-within:text-gray-600"> <Input - bind:value={sidebarSearchValue} - icon={MagnifyingGlassIcon} - id="search-field" - name="search-field" - placeholder="Search" - type="search" + bind:value={sidebarSearchValue} + icon={MagnifyingGlassIcon} + id="search-field" + name="search-field" + placeholder="Search" + type="search" /> </div> </form> @@ -271,35 +272,38 @@ <Menu as="div" class="relative ml-3"> <div> <MenuButton - class="flex max-w-xs items-center rounded-full bg-white text-sm focus:outline-none focus:ring-2 focus:ring-teal-500 focus:ring-offset-2" + class="flex max-w-xs items-center rounded-full bg-white text-sm focus:outline-none focus:ring-2 focus:ring-teal-500 focus:ring-offset-2" > <span class="sr-only">Open user menu</span> </MenuButton> </div> <Transition - enterFrom="transform opacity-0 scale-95" - enterTo="transform opacity-100 scale-100" - leaveFrom="transform opacity-100 scale-100" - leaveTo="transform opacity-0 scale-95" - as="div" + enterFrom="transform opacity-0 scale-95" + enterTo="transform opacity-100 scale-100" + leaveFrom="transform opacity-100 scale-100" + leaveTo="transform opacity-0 scale-95" + as="div" > <MenuItems - class="absolute right-0 z-10 mt-2 w-48 origin-top-right divide-y divide-gray-200 rounded-md bg-white shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none" + class="absolute right-0 z-10 mt-2 w-48 origin-top-right divide-y divide-gray-200 rounded-md bg-white shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none" > <div class="py-1"> <MenuItem> - <a href="/profile" class="text-gray-700 block px-4 py-2 text-sm"> View - profile </a> + <a href="/profile" class="text-gray-700 block px-4 py-2 text-sm"> View profile </a> </MenuItem> <MenuItem> - <a href="/settings" - class="text-gray-700 block px-4 py-2 text-sm hover:text-gray-900 hover:bg-gray-100"> - Settings </a> + <a + href="/settings" + class="text-gray-700 block px-4 py-2 text-sm hover:text-gray-900 hover:bg-gray-100" + > + Settings + </a> </MenuItem> <div class="py-1"> <MenuItem> - <span on:click={() => sign_out()} - class="text-gray-700 block px-4 py-2 text-sm"> Sign out </span> + <span on:click={() => sign_out()} class="text-gray-700 block px-4 py-2 text-sm"> + Sign out + </span> </MenuItem> </div> </div> @@ -310,7 +314,7 @@ </div> </div> <main class="flex-1 p-3"> - <slot/> + <slot /> </main> </div> </div> diff --git a/code/app/src/routes/(main)/(public)/+layout.svelte b/code/app/src/routes/(main)/(public)/+layout.svelte index 0d84f9a..6da653c 100644 --- a/code/app/src/routes/(main)/(public)/+layout.svelte +++ b/code/app/src/routes/(main)/(public)/+layout.svelte @@ -1,18 +1,18 @@ <script> - import {LocaleSwitcher} from "$components"; + import { LocaleSwitcher } from "$components"; import LL from "$i18n/i18n-svelte"; </script> -<LocaleSwitcher tabindex={-1}/> -<slot/> +<LocaleSwitcher tabindex={-1} /> +<slot /> <footer class="grid sm:gap-5 grid-flow-row sm:justify-center px-2 sm:grid-flow-col"> <a href="https://greatoffice.life/privacy" class="link"> {$LL.privacyPolicy()} </a> - <a href="https://greatoffice.life/tos" class="link"> + <a href="https://greatoffice.life/terms" class="link"> {$LL.tos()} </a> - <a href="https://greatoffice.life/documentation" class="link"> + <a href="https://greatoffice.life/docs" class="link"> {$LL.documentation()} </a> </footer> 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 34dabae..55859f6 100644 --- a/code/app/src/routes/(main)/(public)/reset-password/+page.svelte +++ b/code/app/src/routes/(main)/(public)/reset-password/+page.svelte @@ -1,8 +1,8 @@ <script lang="ts"> - import {Alert, Input, Button} from "$components"; + import { Alert, Input, Button } from "$components"; import LL from "$i18n/i18n-svelte"; - import {FormError} from "$models/internal/FormError"; - import {PasswordResetService} from "$services/password-reset-service"; + import { FormError } from "$models/internal/FormError"; + import { PasswordResetService } from "$services/password-reset-service"; const formData = { email: { @@ -44,6 +44,10 @@ } </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"> @@ -61,21 +65,21 @@ <div class="bg-white py-8 px-4 shadow sm:rounded-lg sm:px-10"> <form class="space-y-6" on:submit|preventDefault={submit_form_async}> {#if showErrorAlert} - <Alert title={formError.title} message={formError.subtitle} type="error"/> + <Alert title={formError.title} message={formError.subtitle} type="error" /> {:else if showSuccessAlert} - <Alert type="success" title={$LL.success()} message={$LL.resetPasswordPage.requestSentMessage()}/> + <Alert type="success" title={$LL.success()} message={$LL.resetPasswordPage.requestSentMessage()} /> {/if} <Input - id="email" - name="email" - type="email" - autocomplete="email" - errors={formData.email.errors} - bind:value={formData.email.value} - required - label={$LL.emailAddress()} + id="email" + name="email" + type="email" + autocomplete="email" + errors={formData.email.errors} + bind:value={formData.email.value} + required + label={$LL.emailAddress()} /> - <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)/reset-password/+page.ts b/code/app/src/routes/(main)/(public)/reset-password/+page.ts new file mode 100644 index 0000000..c0859e0 --- /dev/null +++ b/code/app/src/routes/(main)/(public)/reset-password/+page.ts @@ -0,0 +1,11 @@ +import LL from '$i18n/i18n-svelte'; +import { get } from 'svelte/store'; +import type { PageLoad } from './$types'; + +const l = get(LL); + +export const load: PageLoad = async () => { + return { + title: l.resetPasswordPage.title(), + }; +};
\ No newline at end of file diff --git a/code/app/src/routes/(main)/(public)/reset-password/[id]/+page.ts b/code/app/src/routes/(main)/(public)/reset-password/[id]/+page.ts new file mode 100644 index 0000000..3252b7a --- /dev/null +++ b/code/app/src/routes/(main)/(public)/reset-password/[id]/+page.ts @@ -0,0 +1,11 @@ +import LL from '$i18n/i18n-svelte'; +import { get } from 'svelte/store'; +import type { PageLoad } from './$types'; + +const l = get(LL); + +export const load: PageLoad = async () => { + return { + title: l.resetPasswordPage.fulfillTitle(), + }; +};
\ No newline at end of file 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 e862050..c4ecb1a 100644 --- a/code/app/src/routes/(main)/(public)/sign-in/+page.svelte +++ b/code/app/src/routes/(main)/(public)/sign-in/+page.svelte @@ -1,13 +1,13 @@ <script lang="ts"> - import {goto} from "$app/navigation"; - import {Button, Checkbox, Input, Alert} from "$components"; + import { goto } from "$app/navigation"; + import { Button, Checkbox, Input, Alert } from "$components"; import LL from "$i18n/i18n-svelte"; import pwKey from "$actions/pwKey"; - import {onMount} from "svelte"; - import {signInPageMessageQueryKey, signInPageTestKeys, type SignInPageMessage} from "."; - import {AccountService} from "$services/account-service"; - import type {LoginPayload} from "$services/abstractions/IAccountService"; - import {FormError} from "$models/internal/FormError"; + import { onMount } from "svelte"; + import { signInPageMessageQueryKey, signInPageTestKeys, type SignInPageMessage } from "."; + import { AccountService } from "$services/account-service"; + import type { LoginPayload } from "$services/abstractions/IAccountService"; + import { FormError } from "$models/internal/FormError"; let loading = false; let showErrorAlert = false; @@ -71,24 +71,24 @@ <div class="sm:max-w-md sm:mx-auto sm:w-full"> {#if messageType === "after-password-reset"} <Alert - title={$LL.signInPage.yourNewPasswordIsApplied()} - _pwKey={signInPageTestKeys.afterPasswordResetAlert} - message={$LL.signInPage.signInBelow()} - closeable + title={$LL.signInPage.yourNewPasswordIsApplied()} + _pwKey={signInPageTestKeys.afterPasswordResetAlert} + message={$LL.signInPage.signInBelow()} + closeable /> {:else if messageType === "user-disabled"} <Alert - title={$LL.signInPage.yourAccountIsDisabled()} - _pwKey={signInPageTestKeys.userDisabledAlert} - message={$LL.signInPage.contactYourAdminIfDisabled()} - closeable + title={$LL.signInPage.yourAccountIsDisabled()} + _pwKey={signInPageTestKeys.userDisabledAlert} + message={$LL.signInPage.contactYourAdminIfDisabled()} + closeable /> {:else if messageType === "user-inactivity"} <Alert - title={$LL.signInPage.youHaveReachedInactivityLimit()} - _pwKey={signInPageTestKeys.userInactivityAlert} - message={$LL.signInPage.feelFreeToSignInAgain()} - closeable + title={$LL.signInPage.youHaveReachedInactivityLimit()} + _pwKey={signInPageTestKeys.userInactivityAlert} + message={$LL.signInPage.feelFreeToSignInAgain()} + closeable /> {/if} </div> @@ -107,39 +107,37 @@ <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}/> + <Alert title={formError.title} message={formError.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={submit_form_async}> <Input - id="username" - _pwKey={signInPageTestKeys.usernameInput} - name="username" - type="email" - label={$LL.emailAddress()} - required - bind:value={formData.username.value} + id="username" + _pwKey={signInPageTestKeys.usernameInput} + name="username" + type="email" + label={$LL.emailAddress()} + required + bind:value={formData.username.value} /> <Input - id="password" - name="password" - type="password" - label={$LL.password()} - _pwKey={signInPageTestKeys.passwordInput} - autocomplete="current-password" - required - bind:value={formData.password.value} + id="password" + name="password" + type="password" + label={$LL.password()} + _pwKey={signInPageTestKeys.passwordInput} + autocomplete="current-password" + required + bind:value={formData.password.value} /> <div class="flex items-center justify-between"> <Checkbox - id="remember-me" - _pwKey={signInPageTestKeys.rememberMeCheckbox} - name="remember-me" - bind:checked={formData.persist.value} - label={$LL.signInPage.notMyComputer()} + id="remember-me" + _pwKey={signInPageTestKeys.rememberMeCheckbox} + name="remember-me" + bind:checked={formData.persist.value} + label={$LL.signInPage.notMyComputer()} /> <div class="text-sm"> <a href="/reset-password" class="link" use:pwKey={signInPageTestKeys.resetPasswordAnchor}> @@ -148,7 +146,7 @@ </div> </div> - <Button text={$LL.submit()} fullWidth type="submit" {loading}/> + <Button text={$LL.submit()} fullWidth type="submit" {loading} /> </form> </div> </div> diff --git a/code/app/src/routes/(main)/(public)/sign-in/+page.ts b/code/app/src/routes/(main)/(public)/sign-in/+page.ts new file mode 100644 index 0000000..bebc459 --- /dev/null +++ b/code/app/src/routes/(main)/(public)/sign-in/+page.ts @@ -0,0 +1,11 @@ +import LL from '$i18n/i18n-svelte'; +import { get } from 'svelte/store'; +import type { PageLoad } from './$types'; + +const l = get(LL); + +export const load: PageLoad = async () => { + return { + title: l.signInPage.title(), + }; +};
\ No newline at end of file diff --git a/code/app/src/routes/(main)/(public)/sign-up/+page.svelte b/code/app/src/routes/(main)/(public)/sign-up/+page.svelte index 58940ea..d111042 100644 --- a/code/app/src/routes/(main)/(public)/sign-up/+page.svelte +++ b/code/app/src/routes/(main)/(public)/sign-up/+page.svelte @@ -1,10 +1,10 @@ <script lang="ts"> - import {goto} from "$app/navigation"; - import type {CreateAccountPayload} from "$api/account"; - import {Button, Input, Alert} from "$components"; + import { goto } from "$app/navigation"; + import type { CreateAccountPayload } from "$api/account"; + import { Button, Input, Alert } from "$components"; import LL from "$i18n/i18n-svelte"; - import {FormError} from "$models/internal/FormError"; - import {AccountService} from "$services/account-service"; + import { FormError } from "$models/internal/FormError"; + import { AccountService } from "$services/account-service"; const formData = { username: { @@ -76,30 +76,30 @@ <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" class="mb-2"/> + <Alert title={formError.title} message={formError.subtitle} type="error" class="mb-2" /> {/if} <form class="space-y-6" on:submit|preventDefault={submit_form_async}> <Input - label={$LL.emailAddress()} - id="email" - name="email" - autocomplete="email" - required - type="email" - bind:value={formData.username.value} - errors={formData.username.errors} + label={$LL.emailAddress()} + id="email" + name="email" + autocomplete="email" + required + type="email" + bind:value={formData.username.value} + errors={formData.username.errors} /> <Input - label={$LL.password()} - id="password" - name="password" - required - type="password" - bind:value={formData.password.value} - errors={formData.password.errors} + label={$LL.password()} + id="password" + name="password" + required + type="password" + bind:value={formData.password.value} + errors={formData.password.errors} /> - <Button type="submit" text={$LL.submit()} {loading} fullWidth/> + <Button type="submit" text={$LL.submit()} {loading} fullWidth /> </form> </div> </div> diff --git a/code/app/src/routes/(main)/(public)/sign-up/+page.ts b/code/app/src/routes/(main)/(public)/sign-up/+page.ts new file mode 100644 index 0000000..8c86f55 --- /dev/null +++ b/code/app/src/routes/(main)/(public)/sign-up/+page.ts @@ -0,0 +1,11 @@ +import LL from '$i18n/i18n-svelte'; +import { get } from 'svelte/store'; +import type { PageLoad } from './$types'; + +const l = get(LL); + +export const load: PageLoad = async () => { + return { + title: l.signUpPage.title(), + }; +};
\ No newline at end of file diff --git a/code/app/src/routes/(main)/+layout.server.ts b/code/app/src/routes/(main)/+layout.server.ts index 086d1c0..4199d7f 100644 --- a/code/app/src/routes/(main)/+layout.server.ts +++ b/code/app/src/routes/(main)/+layout.server.ts @@ -4,12 +4,15 @@ import {error, redirect} from "@sveltejs/kit"; import {Temporal} from "temporal-polyfill"; import type {LayoutServerLoad} from "./$types"; -export const load: LayoutServerLoad = async ({route, cookies, locals}) => { +export const load: LayoutServerLoad = async ({url, request, route, cookies, locals, fetch}) => { + console.log(url.toString()); const isBaseRoute = route.id === "/(main)"; const isPublicRoute = (route.id?.startsWith("/(main)/(public)") || isBaseRoute) ?? true; - const sessionIsValid = (await cached_result<Response>("sessionCheck", 120, () => fetch(api_base("_/valid-session"), { + 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 + "=" + cookies.get(CookieNames.session), + Cookie: CookieNames.session + "=" + sessionCookieValue, }, }).catch((e) => { log_error(e); @@ -37,7 +40,7 @@ export const load: LayoutServerLoad = async ({route, cookies, locals}) => { let resultCache = {}; -async function cached_result<T>(key: string, staleAfterSeconds: number, code: any) { +async function cached_result_async<T>(key: string, staleAfterSeconds: number, get_result: any, forceRefresh: boolean = false) { if (!resultCache[key]) { resultCache[key] = { l: 0, @@ -45,13 +48,13 @@ async function cached_result<T>(key: string, staleAfterSeconds: number, code: an }; } const staleEpoch = ((resultCache[key]?.l ?? 0) + staleAfterSeconds); - const isStale = staleEpoch < Temporal.Now.instant().epochSeconds; + const isStale = forceRefresh || (staleEpoch < Temporal.Now.instant().epochSeconds); if (isStale || !resultCache[key]?.c) { - resultCache[key].c = await code(); + resultCache[key].c = await get_result(); resultCache[key].l = Temporal.Now.instant().epochSeconds; } - log_debug("Ran cached_result", { + log_debug("Ran cached_result_async", { cacheKey: key, isStale, cache: resultCache[key], diff --git a/code/app/src/routes/(main)/+layout.svelte b/code/app/src/routes/(main)/+layout.svelte index 2b96527..7662d6a 100644 --- a/code/app/src/routes/(main)/+layout.svelte +++ b/code/app/src/routes/(main)/+layout.svelte @@ -1,21 +1,25 @@ <script lang="ts"> import "../../app.pcss"; - import {setLocale} from "$i18n/i18n-svelte"; - import {ExclamationTriangleIcon} from "$components/icons"; - import type {LayoutData} from "./$types"; + import { setLocale } from "$i18n/i18n-svelte"; + import { ExclamationTriangleIcon } from "$components/icons"; + import { page } from "$app/stores"; + import type { LayoutData } from "./$types"; let online = true; export let data: LayoutData; setLocale(data.locale); </script> -<svelte:window bind:online/> +<svelte:window bind:online /> +<svelte:head> + <title>{$page.data.title ? $page.data.title + " - Greatoffice" : "Greatoffice"}</title> +</svelte:head> {#if !online} <div class="bg-yellow-50 relative z-50 p-4"> <div class="flex"> <div class="flex-shrink-0"> - <ExclamationTriangleIcon class="bg-yellow-50 text-yellow-500"/> + <ExclamationTriangleIcon class="bg-yellow-50 text-yellow-500" /> </div> <div class="ml-3"> <p class="text-sm text-yellow-700">You seem to be offline, please check your internet connection.</p> @@ -24,4 +28,4 @@ </div> {/if} -<slot/> +<slot /> diff --git a/code/app/src/routes/(main)/+layout.ts b/code/app/src/routes/(main)/+layout.ts index 0509aaf..3893260 100644 --- a/code/app/src/routes/(main)/+layout.ts +++ b/code/app/src/routes/(main)/+layout.ts @@ -1,10 +1,10 @@ -import type {LayoutLoad} from "./$types"; -import type {Locales} from "$i18n/i18n-types"; -import {loadLocaleAsync} from "$i18n/i18n-util.async"; -import {setLocale} from "$i18n/i18n-svelte"; +import type { LayoutLoad } from "./$types"; +import type { Locales } from "$i18n/i18n-types"; +import { loadLocaleAsync } from "$i18n/i18n-util.async"; +import { setLocale } from "$i18n/i18n-svelte"; -export const load: LayoutLoad<{ locale: Locales }> = async ({data: {locale}}) => { +export const load: LayoutLoad<{ locale: Locales }> = async ({ data: { locale } }) => { await loadLocaleAsync(locale); setLocale(locale); - return {locale}; + return { locale }; };
\ No newline at end of file diff --git a/code/app/src/services/account-service.ts b/code/app/src/services/account-service.ts index 9d45950..92c6126 100644 --- a/code/app/src/services/account-service.ts +++ b/code/app/src/services/account-service.ts @@ -1,4 +1,5 @@ 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"; @@ -19,24 +20,29 @@ import type { } from "./abstractions/IAccountService"; export class AccountService implements IAccountService { - session: Writable<Session>; + session: Writable<Session> | undefined; private sessionCooldown = 3600; constructor() { - this.session = writable_persistent({ - name: StorageKeys.session, - initialState: {} as Session, - options: { - store: StoreType.LOCAL, - }, - }); - this.refresh_session(); + if (browser) { + this.session = writable_persistent({ + name: StorageKeys.session, + initialState: {} as Session, + options: { + store: StoreType.LOCAL, + }, + }); + this.refresh_session(); + } else { + this.session = undefined; + } } async refresh_session(forceRefresh: boolean = false): Promise<void> { + if (!this.session) return; const currentValue = get(this.session); const currentEpoch = Temporal.Now.instant().epochSeconds; - if (currentValue?._lastUpdated + this.sessionCooldown < currentEpoch) { + if (!forceRefresh && ((currentValue?._lastUpdated ?? 0) + this.sessionCooldown) > currentEpoch) { log_debug("Session is not stale yet", { currentEpoch, staleEpoch: currentValue?._lastUpdated + this.sessionCooldown, @@ -45,11 +51,14 @@ export class AccountService implements IAccountService { } const sessionResponse = await http_get_async(api_base("_/account/session")); if (sessionResponse.ok) { - + this.session.set(await sessionResponse.json()); + } else { + this.session.set(null); } } async end_session(callback: Function): Promise<void> { + if (!this.session) return; await this.logout_async(); this.session.set(null); if (typeof callback === "function") callback(); @@ -70,14 +79,12 @@ export class AccountService implements IAccountService { async logout_async(): Promise<void> { const response = await http_get_async(api_base("_/account/logout")); - if (!response.ok) { const deleteCookieResponse = await fetch("/delete-cookie?key=" + CookieNames.session); if (!deleteCookieResponse.ok) { throw new Error("Could neither logout nor delete session cookie."); } } - return; } @@ -88,7 +95,6 @@ export class AccountService implements IAccountService { isCreated: false, knownProblem: await response.json(), }; - return { isCreated: false, }; @@ -108,7 +114,6 @@ export class AccountService implements IAccountService { isUpdated: false, knownProblem: await response.json(), }; - return { isUpdated: false, }; |
