diff options
Diffstat (limited to 'code')
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> |
