feat(webui): users can change their passwords

feat/plugins-backend
Tomáš Mládek 2024-04-04 20:50:30 +02:00
parent 17bc53a6fe
commit 60a8b15164
7 changed files with 236 additions and 87 deletions

View File

@ -1,6 +1,6 @@
{
"name": "@upnd/upend",
"version": "0.5.1",
"version": "0.5.2",
"description": "Client library to interact with the UpEnd system.",
"main": "dist/index.js",
"types": "dist/index.d.ts",

View File

@ -434,6 +434,17 @@ export class UpEndApi {
}
}
public async register(credentials: {
username: string;
password: string;
}): Promise<void> {
await this.fetch(`${this.apiUrl}/auth/register`, undefined, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(credentials),
});
}
public async authStatus(
options?: ApiFetchOptions,
): Promise<{ user: string } | undefined> {

View File

@ -17,6 +17,7 @@
import { goto } from '$app/navigation';
import { i18n } from '$lib/i18n';
import { selected } from '$lib/components/EntitySelect.svelte';
import Modal from '$lib/components/layout/Modal.svelte';
let files: File[] = [];
let URLs: string[] = [];
@ -114,9 +115,8 @@
<svelte:window on:beforeunload={onBeforeUnload} />
<svelte:body on:keydown={onKeydown} />
<!-- svelte-ignore a11y-click-events-have-key-events a11y-no-static-element-interactions -->
<div class="addmodal-container" class:visible class:uploading on:click={reset}>
<div class="addmodal" on:click|stopPropagation>
{#if visible}
<Modal on:close={reset}>
<div class="files" bind:this={filesElement}>
{#each files as file}
<div class="entry">
@ -163,8 +163,8 @@
<ProgressBar value={totalProgress} />
</div>
{/if}
</div>
</div>
</Modal>
{/if}
<style lang="scss">
.addmodal-container {

View File

@ -2,6 +2,8 @@
import { i18n } from '$lib/i18n';
import Icon from '$lib/components/utils/Icon.svelte';
import { login } from '$lib/api';
import Modal from '$lib/components/layout/Modal.svelte';
import { type UpendApiError } from '@upnd/upend/api';
let username = '';
let password = '';
@ -14,66 +16,31 @@
authenticating = true;
await login({ username, password });
} catch (e) {
error = (e as object).toString();
error = (e as UpendApiError).message || (e as UpendApiError).kind;
} finally {
authenticating = false;
}
}
</script>
<div class="modal-container">
<div class="modal" class:authenticating>
<h2>
<Icon name="lock" />
{$i18n.t('Authorization required')}
</h2>
<form on:submit|preventDefault={submit}>
<input placeholder={$i18n.t('Username')} type="text" bind:value={username} required />
<input placeholder={$i18n.t('Password')} type="password" bind:value={password} required />
<button type="submit"> <Icon plain name="log-in" /> {$i18n.t('Login')}</button>
</form>
{#if error}
<div class="error">{error}</div>
{/if}
</div>
</div>
<Modal disabled={authenticating}>
<h2>
<Icon name="lock" />
{$i18n.t('Authorization required')}
</h2>
<form on:submit|preventDefault={submit}>
<input placeholder={$i18n.t('Username')} type="text" bind:value={username} required />
<input placeholder={$i18n.t('Password')} type="password" bind:value={password} required />
<button type="submit"> <Icon plain name="log-in" /> {$i18n.t('Login')}</button>
</form>
{#if error}
<div class="error">{error}</div>
{/if}
</Modal>
<style lang="scss">
@use '$lib/styles/colors';
.modal-container {
position: fixed;
left: 0;
top: 0;
width: 100vw;
height: 100vh;
background: rgba(0, 0, 0, 0.66);
color: var(--foreground);
z-index: 9;
}
.modal {
position: fixed;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
background: var(--background);
color: var(--foreground);
border-radius: 5px;
border: 1px solid var(--foreground);
padding: 2rem;
display: flex;
flex-direction: column;
gap: 1rem;
&.authenticating {
filter: brightness(0.66);
pointer-events: none;
}
}
h2 {
text-align: center;
margin: 0 0 1rem 0;

View File

@ -2,13 +2,13 @@
import { addEmitter } from '../AddModal.svelte';
import Icon from '../utils/Icon.svelte';
import { jobsEmitter } from './Jobs.svelte';
import api, { currentUser, logout } from '$lib/api';
import api from '$lib/api';
import Selector, { type SelectorValue } from '../utils/Selector.svelte';
import { i18n } from '$lib/i18n';
import { goto } from '$app/navigation';
import { onMount } from 'svelte';
import { vaultInfo } from '$lib/util/info';
import { slide } from 'svelte/transition';
import HeaderUserDropdown from '$lib/components/layout/HeaderUserDropdown.svelte';
let selector: Selector;
let userDropdown = false;
@ -77,12 +77,6 @@
}
</script>
<svelte:body
on:click={() => {
userDropdown = false;
}}
/>
<div class="header">
<h1>
<a href="/">
@ -115,17 +109,7 @@
>
<Icon name="user" />
</button>
{#if userDropdown}
<!-- svelte-ignore a11y-no-static-element-interactions a11y-click-events-have-key-events -->
<div class="user-dropdown" transition:slide on:click|stopPropagation={() => {}}>
<div class="user">
<Icon plain name="user" />
{$currentUser || '???'}
</div>
<hr />
<button on:click={() => logout()}> <Icon name="log-out" />{$i18n.t('Log out')}</button>
</div>
{/if}
<HeaderUserDropdown bind:open={userDropdown} />
</div>
<style lang="scss">
@ -169,18 +153,6 @@
}
}
.user-dropdown {
background: var(--background);
border-radius: 4px;
border: 1px solid var(--foreground);
padding: 0.5em;
position: absolute;
top: 3.5rem;
right: 0.5rem;
box-shadow: 0 0 0.5rem rgba(0, 0, 0, 0.5);
z-index: 99;
}
@media screen and (max-width: 600px) {
.name {
display: none;

View File

@ -0,0 +1,140 @@
<script lang="ts">
import api, { currentUser, logout } from '$lib/api';
import { type UpendApiError } from '@upnd/upend/api';
import { i18n } from '$lib/i18n';
import Icon from '$lib/components/utils/Icon.svelte';
import { slide } from 'svelte/transition';
import Modal from '$lib/components/layout/Modal.svelte';
export let open = false;
let changePasswordModal = false;
let currentPassword = '';
let newPassword = '';
let newPasswordConfirm = '';
async function changePassword() {
try {
const result = await api.authenticate(
{ username: $currentUser!, password: currentPassword },
'key'
);
if (result) {
await api.register({ username: $currentUser!, password: newPassword });
alert($i18n.t('Password changed successfully').toString());
changePasswordModal = false;
}
} catch (e: unknown) {
alert(
$i18n.t('Error authenticating: {error}', { error: (e as UpendApiError).message }).toString()
);
}
}
</script>
<svelte:body
on:click={() => {
open = false;
}}
/>
{#if open}
<!-- svelte-ignore a11y-no-static-element-interactions a11y-click-events-have-key-events -->
<div class="user-dropdown" transition:slide on:click|stopPropagation>
<div class="user">
<Icon plain name="user" />
{$currentUser || '???'}
</div>
<button on:click={() => (changePasswordModal = true)}>
<Icon name="lock" />{$i18n.t('Change password')}</button
>
<button on:click={() => logout()}> <Icon name="log-out" />{$i18n.t('Log out')}</button>
</div>
{/if}
{#if changePasswordModal}
<Modal on:close={() => (changePasswordModal = false)}>
<h2>
<Icon name="lock" />
{$i18n.t('Change password')}
</h2>
<form class="change-password">
<label>
<span>{$i18n.t('Current password')}</span>
<input type="password" bind:value={currentPassword} required />
</label>
<label>
<span>{$i18n.t('New password')}</span>
<input type="password" bind:value={newPassword} required />
</label>
<label>
<span>{$i18n.t('Confirm new password')}</span>
<input type="password" bind:value={newPasswordConfirm} required />
</label>
<button
type="submit"
on:click={() => changePassword()}
disabled={newPassword !== newPasswordConfirm || !newPassword}
>{$i18n.t('Change password')}</button
>
</form>
</Modal>
{/if}
<style>
.user-dropdown {
display: flex;
flex-direction: column;
gap: 0.5rem;
background: var(--background);
border-radius: 4px;
border: 1px solid var(--foreground);
padding: 0.5em;
position: absolute;
top: 3.5rem;
right: 0.5rem;
box-shadow: 0 0 0.5rem rgba(0, 0, 0, 0.5);
z-index: 99;
}
.user {
font-weight: 500;
margin: 0 1rem;
}
button,
.user {
display: flex;
align-items: center;
gap: 0.5rem;
}
h2 {
text-align: center;
margin: 0 0 1rem 0;
}
.change-password {
display: grid;
grid-template-columns: 1fr 1fr;
align-items: center;
gap: 0.5rem 1rem;
& label {
display: contents;
& span {
text-align: right;
}
}
& button {
grid-column: span 2;
justify-content: center;
justify-self: center;
font-weight: 500;
margin-top: 1rem;
}
}
</style>

View File

@ -0,0 +1,59 @@
<script lang="ts">
import { fade } from 'svelte/transition';
import { createEventDispatcher } from 'svelte';
const dispatch = createEventDispatcher();
export let disabled = false;
export let dismissable = true;
function close() {
if (!dismissable) return;
dispatch('close');
}
function onKeydown(event: KeyboardEvent) {
if (event.key === 'Escape') close();
}
</script>
<svelte:body on:keydown={onKeydown} />
<!-- svelte-ignore a11y-click-events-have-key-events a11y-no-static-element-interactions -->
<div class="modal-container" on:click={close} transition:fade={{ duration: 200 }}>
<div class="modal" class:disabled on:click|stopPropagation>
<slot />
</div>
</div>
<style>
.modal-container {
position: fixed;
left: 0;
top: 0;
width: 100vw;
height: 100vh;
background: rgba(0, 0, 0, 0.66);
z-index: 999;
}
.modal {
position: fixed;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
background: var(--background);
color: var(--foreground);
border-radius: 5px;
border: 1px solid var(--foreground);
padding: 2rem;
display: flex;
flex-direction: column;
gap: 1rem;
&.disabled {
filter: brightness(0.66);
pointer-events: none;
}
}
</style>