aboutsummaryrefslogtreecommitdiffstats
path: root/code/app
diff options
context:
space:
mode:
Diffstat (limited to 'code/app')
-rw-r--r--code/app/src/lib/components/button.svelte4
-rw-r--r--code/app/src/lib/services/abstractions/IAccountService.ts53
-rw-r--r--code/app/src/lib/services/account-service.ts54
-rw-r--r--code/app/src/routes/(api)/delete-cookie/+server.ts8
-rw-r--r--code/app/src/routes/(main)/(public)/reset-password/+page.svelte26
-rw-r--r--code/app/src/routes/(main)/(public)/reset-password/[id]/+page.svelte30
-rw-r--r--code/app/src/routes/(main)/(public)/sign-in/+page.svelte123
7 files changed, 213 insertions, 85 deletions
diff --git a/code/app/src/lib/components/button.svelte b/code/app/src/lib/components/button.svelte
index abe62ae..49a9354 100644
--- a/code/app/src/lib/components/button.svelte
+++ b/code/app/src/lib/components/button.svelte
@@ -5,7 +5,6 @@
<script lang="ts">
import pwKey from "$actions/pwKey";
-
import { SpinnerIcon } from "./icons";
export let kind = "primary" as ButtonKind;
@@ -93,7 +92,7 @@
use:pwKey={_pwKey}
{...shared_props}
on:click
- class="{sizeClasses} {kindClasses} {$$restProps.class ?? ''}
+ 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"
@@ -109,7 +108,6 @@
.reset {
border: 0px;
outline: none;
-
}
.reset:focus {
outline: none;
diff --git a/code/app/src/lib/services/abstractions/IAccountService.ts b/code/app/src/lib/services/abstractions/IAccountService.ts
new file mode 100644
index 0000000..736c3ae
--- /dev/null
+++ b/code/app/src/lib/services/abstractions/IAccountService.ts
@@ -0,0 +1,53 @@
+import type { KnownProblem } from "$lib/models/internal/KnownProblem"
+
+export interface IAccountService {
+ session: Session,
+ login_async(payload: LoginPayload): Promise<LoginResponse>,
+ logout_async(): Promise<void>,
+ create_account_async(payload: CreateAccountPayload): Promise<CreateAccountResponse>,
+ delete_current_async(): Promise<DeleteAccountResponse>,
+ update_current_async(payload: UpdateAccountPayload): Promise<UpdateAccountResponse>,
+}
+
+export type Session = {
+ profile: {
+ username: string,
+ displayName: string,
+ id: string,
+ },
+ lastChecked: number,
+}
+
+export type LoginPayload = {
+ username: string,
+ password: string,
+ persist: boolean
+}
+
+export type LoginResponse = {
+ isLoggedIn: boolean
+}
+
+export type CreateAccountPayload = {
+ username: string,
+ password: string,
+}
+
+export type CreateAccountResponse = {
+ isCreated: boolean,
+ knownProblem?: KnownProblem
+}
+
+export type DeleteAccountResponse = {
+ isDeleted: boolean
+}
+
+export type UpdateAccountPayload = {
+ username: string,
+ password: string
+}
+
+export type UpdateAccountResponse = {
+ isUpdated: boolean,
+ knownProblem?: KnownProblem
+} \ No newline at end of file
diff --git a/code/app/src/lib/services/account-service.ts b/code/app/src/lib/services/account-service.ts
new file mode 100644
index 0000000..90af163
--- /dev/null
+++ b/code/app/src/lib/services/account-service.ts
@@ -0,0 +1,54 @@
+import { http_delete_async, http_get_async, http_post_async } from "$lib/api/_fetch";
+import { api_base, CookieNames } from "$lib/configuration";
+import { is_known_problem } from "$lib/models/internal/KnownProblem";
+import type { CreateAccountPayload, CreateAccountResponse, DeleteAccountResponse, IAccountService, LoginPayload, LoginResponse, Session, UpdateAccountPayload, UpdateAccountResponse } from "./abstractions/IAccountService";
+
+export class AccountService implements IAccountService {
+ session: Session;
+ async login_async(payload: LoginPayload): Promise<LoginResponse> {
+ const response = await http_post_async(api_base("_/account/login"), payload);
+ return { isLoggedIn: response.ok };
+ }
+ 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;
+ }
+ 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 (is_known_problem(response)) return {
+ isCreated: false,
+ knownProblem: await response.json()
+ }
+
+ return {
+ isCreated: false
+ }
+ }
+ async delete_current_async(): Promise<DeleteAccountResponse> {
+ const response = await http_delete_async(api_base("_/account/delete"));
+ return {
+ isDeleted: response.ok
+ }
+ }
+ 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 (is_known_problem(response)) return {
+ isUpdated: false,
+ knownProblem: await response.json()
+ }
+
+ return {
+ isUpdated: false
+ }
+ }
+} \ No newline at end of file
diff --git a/code/app/src/routes/(api)/delete-cookie/+server.ts b/code/app/src/routes/(api)/delete-cookie/+server.ts
new file mode 100644
index 0000000..ee5e1dc
--- /dev/null
+++ b/code/app/src/routes/(api)/delete-cookie/+server.ts
@@ -0,0 +1,8 @@
+import type { RequestHandler } from './$types';
+
+export const GET: RequestHandler = async ({ cookies, url }) => {
+ const cookieToDelete = url.searchParams.get("key");
+ if (!cookieToDelete || cookies.get(cookieToDelete) === undefined) return;
+ cookies.delete(cookieToDelete)
+ return new Response();
+}; \ 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 4d0288f..8bda3dc 100644
--- a/code/app/src/routes/(main)/(public)/reset-password/+page.svelte
+++ b/code/app/src/routes/(main)/(public)/reset-password/+page.svelte
@@ -1,13 +1,12 @@
<script lang="ts">
import { Alert, Input, Button } from "$lib/components";
- import X from "$lib/components/icons/x.svelte";
import LL from "$lib/i18n/i18n-svelte";
import { PasswordResetService } from "$lib/services/password-reset-service";
const formData = {
email: {
value: "",
- error: "",
+ errors: [],
},
};
@@ -20,27 +19,31 @@
},
};
- const service = new PasswordResetService();
+ const resetRequests = new PasswordResetService();
let loading = false;
let showSuccessAlert = false;
- $: showErrorAlert = formError.title || (formError.subtitle && !showSuccessAlert);
+ $: showErrorAlert = (formError.title !== "" || formError.subtitle !== "") && !showSuccessAlert;
async function submitFormAsync() {
formError.set();
showSuccessAlert = false;
loading = true;
- const response = await service.create_request_async(formData.email.value);
+ const response = await resetRequests.create_request_async(formData.email.value);
loading = false;
if (response.isCreated) {
showSuccessAlert = true;
- return;
- }
- if (response.knownProblem) {
+ } else if (response.knownProblem) {
if (response.knownProblem.title) formError.title = response.knownProblem.title;
if (response.knownProblem.subtitle) formError.subtitle = response.knownProblem.subtitle;
- for (const error of response.knownProblem.errors) {
+ for (const error of Object.entries(response.knownProblem.errors)) {
+ if (error[0] === "email") {
+ error[1].forEach(formData.email.errors.push);
+ }
}
+ } else {
+ formError.title = $LL.unexpectedError();
+ formError.subtitle = $LL.tryAgainSoon();
}
}
</script>
@@ -61,7 +64,7 @@
<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">
<form class="space-y-6" on:submit|preventDefault={submitFormAsync}>
- <Alert title={errorData.title} message={errorData.text} type="error" visible={showErrorAlert} />
+ <Alert title={formError.title} message={formError.subtitle} type="error" visible={showErrorAlert} />
<Alert
type="success"
@@ -75,8 +78,9 @@
name="email"
type="email"
autocomplete="email"
+ errors={formData.email.errors}
required
- bind:value={formData.email}
+ bind:value={formData.email.value}
label={$LL.emailAddress()}
/>
<Button text={$LL.submit()} type="submit" {loading} fullWidth />
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 3710290..2026764 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,26 +1,22 @@
<script lang="ts">
- import { check_forgot_password_request, fulfill_forgot_password_request } from "$lib/api/account";
import { onMount } from "svelte";
import LL from "$lib/i18n/i18n-svelte";
import { Alert, Input, Button } from "$lib/components";
import type { PageServerData } from "./$types";
- import type { ErrorResult } from "$lib/models/internal/ErrorResult";
import { goto } from "$app/navigation";
- import { Message, messageQueryKey } from "$routes/(main)/(public)/sign-in";
+ import { SignInPageMessage, signInPageMessageQueryKey } from "$routes/(main)/(public)/sign-in";
+ import { PasswordResetService } from "$lib/services/password-reset-service";
export let data: PageServerData;
-
+ const service = new PasswordResetService();
const formData = {
- newPassword: "",
+ newPassword: {
+ value: "",
+ errors: [],
+ },
};
- const errorData = {
- text: "",
- title: "",
- } as ErrorResult;
-
let errorState: undefined | "expired" | "404" | "unknown";
-
let finishedPreliminaryLoading = false;
let loading = false;
let canSubmit = true;
@@ -28,18 +24,18 @@
async function submitFormAsync() {
if (!canSubmit) return;
loading = true;
- const request = await fulfill_forgot_password_request(data.resetRequestId, formData.newPassword);
- if (request.ok) {
- goto("/sign-in?" + messageQueryKey + "=" + Message.AFTER_PASSWORD_RESET);
+ const request = await service.fulfill_request_async(data.resetRequestId, formData.newPassword.value);
+ if (request.isFulfilled) {
+ goto("/sign-in?" + signInPageMessageQueryKey + "=" + SignInPageMessage.AFTER_PASSWORD_RESET);
+ } else if (request.knownProblem) {
}
-
loading = false;
}
onMount(async () => {
errorState = undefined;
- const isValidRequest = await check_forgot_password_request(data.resetRequestId);
- if (!isValidRequest.ok && isValidRequest.status !== 404) {
+ const isValidRequest = await service.request_is_valid_async(data.resetRequestId);
+ if (!isValidRequest.isValid) {
errorState = "unknown";
canSubmit = false;
}
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 5e2cb56..01c65dd 100644
--- a/code/app/src/routes/(main)/(public)/sign-in/+page.svelte
+++ b/code/app/src/routes/(main)/(public)/sign-in/+page.svelte
@@ -1,26 +1,44 @@
<script lang="ts">
- import {goto} from "$app/navigation";
- import {http_account} from "$lib/api/account";
- import {Button, Checkbox, Input, Alert} from "$lib/components";
+ import { goto } from "$app/navigation";
+ import { http_account } from "$lib/api/account";
+ import { Button, Checkbox, Input, Alert } from "$lib/components";
import LL from "$lib/i18n/i18n-svelte";
import pwKey from "$actions/pwKey";
- import {isOk} from "rustic";
- import {onMount} from "svelte";
- import {signInPageMessageQueryKey, signInPageTestKeys, type SignInPageMessage} from ".";
+ import { isOk } from "rustic";
+ import { onMount } from "svelte";
+ import { signInPageMessageQueryKey, signInPageTestKeys, type SignInPageMessage } from ".";
+ import { AccountService } from "$lib/services/account-service";
+ import type { LoginPayload } from "$lib/services/abstractions/IAccountService";
let loading = false;
let messageType: SignInPageMessage | undefined = undefined;
- const data = {
- username: "",
- password: "",
- persist: true,
- } as LoginPayload;
+ const accountService = new AccountService();
- let errorData = {
- text: "",
+ const formData = {
+ username: {
+ value: "",
+ errors: [],
+ },
+ password: {
+ value: "",
+ errors: [],
+ },
+ persist: {
+ value: false,
+ errors: [],
+ },
+ };
+
+ const formError = {
title: "",
- } as ErrorResult;
+ subtitle: "",
+ set(title = "", subtitle = "") {
+ formError.title = title;
+ formError.subtitle = subtitle;
+ },
+ };
+
$: showErrorAlert = (errorData.text?.length ?? 0 + errorData.title?.length ?? 0) > 0;
onMount(() => {
@@ -33,7 +51,7 @@
});
async function submitFormAsync() {
- errorData = {text: "", title: ""};
+ errorData = { text: "", title: "" };
loading = true;
data.persist = !data.persist;
const loginResponse = await http_account.login_async(data);
@@ -56,24 +74,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>
@@ -84,46 +102,43 @@
</h2>
<p class="mt-2 text-sm text-gray-600">
{$LL.or().toLowerCase()}
- <a href="/sign-up" use:pwKey={signInPageTestKeys.signUpAnchor}
- class="link">{$LL.createANewAccount().toLowerCase()}</a>
+ <a href="/sign-up" use:pwKey={signInPageTestKeys.signUpAnchor} class="link">{$LL.createANewAccount().toLowerCase()}</a>
</p>
</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={errorData.title} message={errorData.text} type="error"
- _pwKey={signInPageTestKeys.formErrorAlert}/>
+ <Alert title={errorData.title} message={errorData.text} type="error" _pwKey={signInPageTestKeys.formErrorAlert} />
{/if}
- <form class="space-y-6 mt-2" use:pwKey={signInPageTestKeys.signInForm}
- on:submit|preventDefault={submitFormAsync}>
+ <form class="space-y-6 mt-2" use:pwKey={signInPageTestKeys.signInForm} on:submit|preventDefault={submitFormAsync}>
<Input
- id="username"
- _pwKey={signInPageTestKeys.usernameInput}
- name="username"
- type="email"
- label={$LL.emailAddress()}
- required
- bind:value={data.username}
+ id="username"
+ _pwKey={signInPageTestKeys.usernameInput}
+ name="username"
+ type="email"
+ label={$LL.emailAddress()}
+ required
+ bind:value={data.username}
/>
<Input
- id="password"
- name="password"
- type="password"
- label={$LL.password()}
- _pwKey={signInPageTestKeys.passwordInput}
- autocomplete="current-password"
- required
- bind:value={data.password}
+ id="password"
+ name="password"
+ type="password"
+ label={$LL.password()}
+ _pwKey={signInPageTestKeys.passwordInput}
+ autocomplete="current-password"
+ required
+ bind:value={data.password}
/>
<div class="flex items-center justify-between">
<Checkbox
- id="remember-me"
- _pwKey={signInPageTestKeys.rememberMeCheckbox}
- name="remember-me"
- bind:checked={data.persist}
- label={$LL.signInPage.notMyComputer()}
+ id="remember-me"
+ _pwKey={signInPageTestKeys.rememberMeCheckbox}
+ name="remember-me"
+ bind:checked={data.persist}
+ label={$LL.signInPage.notMyComputer()}
/>
<div class="text-sm">
<a href="/reset-password" class="link" use:pwKey={signInPageTestKeys.resetPasswordAnchor}>
@@ -132,7 +147,7 @@
</div>
</div>
- <Button text={$LL.submit()} fullWidth type="submit" {loading}/>
+ <Button text={$LL.submit()} fullWidth type="submit" {loading} />
</form>
</div>
</div>