aboutsummaryrefslogtreecommitdiffstats
path: root/code/app
diff options
context:
space:
mode:
Diffstat (limited to 'code/app')
-rw-r--r--code/app/.typesafe-i18n.json2
-rw-r--r--code/app/src/configuration/index.ts2
-rw-r--r--code/app/src/help/persistent-store.ts12
-rw-r--r--code/app/src/i18n/en/index.ts4
-rw-r--r--code/app/src/i18n/i18n-types.ts32
-rw-r--r--code/app/src/routes/(main)/(app)/+layout.svelte210
-rw-r--r--code/app/src/routes/(main)/(public)/+layout.svelte10
-rw-r--r--code/app/src/routes/(main)/(public)/reset-password/+page.svelte32
-rw-r--r--code/app/src/routes/(main)/(public)/reset-password/+page.ts11
-rw-r--r--code/app/src/routes/(main)/(public)/reset-password/[id]/+page.ts11
-rw-r--r--code/app/src/routes/(main)/(public)/sign-in/+page.svelte86
-rw-r--r--code/app/src/routes/(main)/(public)/sign-in/+page.ts11
-rw-r--r--code/app/src/routes/(main)/(public)/sign-up/+page.svelte44
-rw-r--r--code/app/src/routes/(main)/(public)/sign-up/+page.ts11
-rw-r--r--code/app/src/routes/(main)/+layout.server.ts17
-rw-r--r--code/app/src/routes/(main)/+layout.svelte16
-rw-r--r--code/app/src/routes/(main)/+layout.ts12
-rw-r--r--code/app/src/services/account-service.ts35
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: {
/**
+ * S​i​g​n​ ​i​n
+ */
+ title: string
+ /**
* T​h​i​s​ ​i​s​ ​n​o​t​ ​m​y​ ​c​o​m​p​u​t​e​r
*/
notMyComputer: string
@@ -159,12 +163,24 @@ type RootTranslation = {
}
signUpPage: {
/**
+ * S​i​g​n​ ​u​p
+ */
+ title: string
+ /**
* C​r​e​a​t​e​ ​y​o​u​r​ ​n​e​w​ ​a​c​c​o​u​n​t
*/
createYourNewAccount: string
}
resetPasswordPage: {
/**
+ * R​e​s​e​t​ ​p​a​s​s​w​o​r​d
+ */
+ title: string
+ /**
+ * S​e​t​ ​n​e​w​ ​p​a​s​s​w​o​r​d
+ */
+ fulfillTitle: string
+ /**
* S​e​t​ ​a​ ​n​e​w​ ​p​a​s​s​w​o​r​d
*/
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,
};