feat(webui): users can change their passwords
This commit is contained in:
parent
858815985c
commit
e6847b05e2
7 changed files with 236 additions and 87 deletions
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@upnd/upend",
|
"name": "@upnd/upend",
|
||||||
"version": "0.5.1",
|
"version": "0.5.2",
|
||||||
"description": "Client library to interact with the UpEnd system.",
|
"description": "Client library to interact with the UpEnd system.",
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
"types": "dist/index.d.ts",
|
"types": "dist/index.d.ts",
|
||||||
|
|
|
@ -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(
|
public async authStatus(
|
||||||
options?: ApiFetchOptions,
|
options?: ApiFetchOptions,
|
||||||
): Promise<{ user: string } | undefined> {
|
): Promise<{ user: string } | undefined> {
|
||||||
|
|
|
@ -17,6 +17,7 @@
|
||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
import { i18n } from '$lib/i18n';
|
import { i18n } from '$lib/i18n';
|
||||||
import { selected } from '$lib/components/EntitySelect.svelte';
|
import { selected } from '$lib/components/EntitySelect.svelte';
|
||||||
|
import Modal from '$lib/components/layout/Modal.svelte';
|
||||||
|
|
||||||
let files: File[] = [];
|
let files: File[] = [];
|
||||||
let URLs: string[] = [];
|
let URLs: string[] = [];
|
||||||
|
@ -114,9 +115,8 @@
|
||||||
<svelte:window on:beforeunload={onBeforeUnload} />
|
<svelte:window on:beforeunload={onBeforeUnload} />
|
||||||
<svelte:body on:keydown={onKeydown} />
|
<svelte:body on:keydown={onKeydown} />
|
||||||
|
|
||||||
<!-- svelte-ignore a11y-click-events-have-key-events a11y-no-static-element-interactions -->
|
{#if visible}
|
||||||
<div class="addmodal-container" class:visible class:uploading on:click={reset}>
|
<Modal on:close={reset}>
|
||||||
<div class="addmodal" on:click|stopPropagation>
|
|
||||||
<div class="files" bind:this={filesElement}>
|
<div class="files" bind:this={filesElement}>
|
||||||
{#each files as file}
|
{#each files as file}
|
||||||
<div class="entry">
|
<div class="entry">
|
||||||
|
@ -163,8 +163,8 @@
|
||||||
<ProgressBar value={totalProgress} />
|
<ProgressBar value={totalProgress} />
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</Modal>
|
||||||
</div>
|
{/if}
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
.addmodal-container {
|
.addmodal-container {
|
||||||
|
|
|
@ -2,6 +2,8 @@
|
||||||
import { i18n } from '$lib/i18n';
|
import { i18n } from '$lib/i18n';
|
||||||
import Icon from '$lib/components/utils/Icon.svelte';
|
import Icon from '$lib/components/utils/Icon.svelte';
|
||||||
import { login } from '$lib/api';
|
import { login } from '$lib/api';
|
||||||
|
import Modal from '$lib/components/layout/Modal.svelte';
|
||||||
|
import { type UpendApiError } from '@upnd/upend/api';
|
||||||
|
|
||||||
let username = '';
|
let username = '';
|
||||||
let password = '';
|
let password = '';
|
||||||
|
@ -14,15 +16,14 @@
|
||||||
authenticating = true;
|
authenticating = true;
|
||||||
await login({ username, password });
|
await login({ username, password });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
error = (e as object).toString();
|
error = (e as UpendApiError).message || (e as UpendApiError).kind;
|
||||||
} finally {
|
} finally {
|
||||||
authenticating = false;
|
authenticating = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="modal-container">
|
<Modal disabled={authenticating}>
|
||||||
<div class="modal" class:authenticating>
|
|
||||||
<h2>
|
<h2>
|
||||||
<Icon name="lock" />
|
<Icon name="lock" />
|
||||||
{$i18n.t('Authorization required')}
|
{$i18n.t('Authorization required')}
|
||||||
|
@ -35,45 +36,11 @@
|
||||||
{#if error}
|
{#if error}
|
||||||
<div class="error">{error}</div>
|
<div class="error">{error}</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</Modal>
|
||||||
</div>
|
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
@use '$lib/styles/colors';
|
@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 {
|
h2 {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
margin: 0 0 1rem 0;
|
margin: 0 0 1rem 0;
|
||||||
|
|
|
@ -2,13 +2,13 @@
|
||||||
import { addEmitter } from '../AddModal.svelte';
|
import { addEmitter } from '../AddModal.svelte';
|
||||||
import Icon from '../utils/Icon.svelte';
|
import Icon from '../utils/Icon.svelte';
|
||||||
import { jobsEmitter } from './Jobs.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 Selector, { type SelectorValue } from '../utils/Selector.svelte';
|
||||||
import { i18n } from '$lib/i18n';
|
import { i18n } from '$lib/i18n';
|
||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
import { vaultInfo } from '$lib/util/info';
|
import { vaultInfo } from '$lib/util/info';
|
||||||
import { slide } from 'svelte/transition';
|
import HeaderUserDropdown from '$lib/components/layout/HeaderUserDropdown.svelte';
|
||||||
|
|
||||||
let selector: Selector;
|
let selector: Selector;
|
||||||
let userDropdown = false;
|
let userDropdown = false;
|
||||||
|
@ -77,12 +77,6 @@
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:body
|
|
||||||
on:click={() => {
|
|
||||||
userDropdown = false;
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div class="header">
|
<div class="header">
|
||||||
<h1>
|
<h1>
|
||||||
<a href="/">
|
<a href="/">
|
||||||
|
@ -115,17 +109,7 @@
|
||||||
>
|
>
|
||||||
<Icon name="user" />
|
<Icon name="user" />
|
||||||
</button>
|
</button>
|
||||||
{#if userDropdown}
|
<HeaderUserDropdown bind:open={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}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style lang="scss">
|
<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) {
|
@media screen and (max-width: 600px) {
|
||||||
.name {
|
.name {
|
||||||
display: none;
|
display: none;
|
||||||
|
|
140
webui/src/lib/components/layout/HeaderUserDropdown.svelte
Normal file
140
webui/src/lib/components/layout/HeaderUserDropdown.svelte
Normal 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>
|
59
webui/src/lib/components/layout/Modal.svelte
Normal file
59
webui/src/lib/components/layout/Modal.svelte
Normal 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>
|
Loading…
Reference in a new issue