add password to frontend

This commit is contained in:
Niccolo Borgioli 2023-05-23 09:39:19 +02:00
parent 6da28a701e
commit fdc2722fb9
No known key found for this signature in database
GPG Key ID: D93C615F75EE4F0B
8 changed files with 124 additions and 70 deletions

View File

@ -92,7 +92,7 @@ button {
} }
*:disabled, *:disabled,
*[disabled='true'] { .disabled {
opacity: 0.5; opacity: 0.5;
} }
@ -126,3 +126,13 @@ fieldset {
.tr { .tr {
text-align: right; text-align: right;
} }
hr {
border: none;
border-bottom: 2px solid var(--ui-bg-1);
margin: 1rem 0;
}
p {
margin: 0;
}

View File

@ -8,48 +8,69 @@
export let note: Note export let note: Note
export let timeExpiration = false export let timeExpiration = false
let customPassword = false
$: if (!customPassword) note.password = undefined
</script> </script>
<div class="fields"> <div class="flex col">
<TextInput <div class="flex">
data-testid="field-views" <TextInput
type="number" data-testid="field-views"
label={$t('common.views', { values: { n: 0 } })} type="number"
bind:value={note.views} label={$t('common.views', { values: { n: 0 } })}
disabled={timeExpiration} bind:value={note.views}
max={$status?.max_views} disabled={timeExpiration}
min={1} max={$status?.max_views}
validate={(v) => min={1}
($status && v <= $status?.max_views && v > 0) || validate={(v) =>
$t('home.errors.max', { values: { n: $status?.max_views ?? 0 } })} ($status && v <= $status?.max_views && v > 0) ||
/> $t('home.errors.max', { values: { n: $status?.max_views ?? 0 } })}
<div class="middle-switch"> />
<Switch <Switch
data-testid="switch-advanced-toggle" data-testid="switch-advanced-toggle"
label={$t('common.mode')} label={$t('common.mode')}
bind:value={timeExpiration} bind:value={timeExpiration}
color={false} color={false}
/> />
<TextInput
data-testid="field-expiration"
type="number"
label={$t('common.minutes', { values: { n: 0 } })}
bind:value={note.expiration}
disabled={!timeExpiration}
max={$status?.max_expiration}
validate={(v) =>
($status && v < $status?.max_expiration) ||
$t('home.errors.max', { values: { n: $status?.max_expiration ?? 0 } })}
/>
</div>
<div class="flex">
<Switch bind:value={customPassword} label={$t('home.advanced.custom_password')} />
<TextInput
type="password"
bind:value={note.password}
label={$t('common.password')}
disabled={!customPassword}
random
/>
</div>
<div>
{$t('home.advanced.explanation')}
</div> </div>
<TextInput
data-testid="field-expiration"
type="number"
label={$t('common.minutes', { values: { n: 0 } })}
bind:value={note.expiration}
disabled={!timeExpiration}
max={$status?.max_expiration}
validate={(v) =>
($status && v < $status?.max_expiration) ||
$t('home.errors.max', { values: { n: $status?.max_expiration ?? 0 } })}
/>
</div> </div>
<style> <style>
.middle-switch { .flex {
margin: 0 1rem; display: flex;
align-items: flex-end;
gap: 1rem;
width: 100%;
} }
.fields { .col {
display: flex; gap: 1.5rem;
flex-direction: column;
} }
</style> </style>

View File

@ -1,7 +1,7 @@
<script lang="ts" context="module"> <script lang="ts" context="module">
export type NoteResult = { export type NoteResult = {
password: string
id: string id: string
password?: string
} }
</script> </script>
@ -14,7 +14,8 @@
export let result: NoteResult export let result: NoteResult
$: url = `${window.location.origin}/note/${result.id}#${result.password}` let url = `${window.location.origin}/note/${result.id}`
if (result.password) url += `#${result.password}`
function reset() { function reset() {
window.location.reload() window.location.reload()

View File

@ -4,43 +4,35 @@
export let color = true export let color = true
</script> </script>
<div {...$$restProps}> <label {...$$restProps}>
<label class="switch"> <small>{label}</small>
<small>{label}</small> <input type="checkbox" bind:checked={value} />
<input type="checkbox" bind:checked={value} /> <span class:color class="slider" />
<span class:color class="slider" /> </label>
</label>
</div>
<style> <style>
div { label {
height: 3.75rem;
}
.switch {
position: relative; position: relative;
display: inline-block; display: inline-block;
width: 4rem;
height: 2.5rem;
} }
.switch input { label input {
opacity: 0; display: none;
width: 0; }
height: 0;
small {
display: block;
width: max-content;
} }
.slider { .slider {
position: absolute; display: block;
width: 4rem;
height: 2.5rem;
position: relative;
cursor: pointer; cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
border: 2px solid var(--ui-bg-1); border: 2px solid var(--ui-bg-1);
background-color: var(--ui-bg-0); background-color: var(--ui-bg-0);
transition: var(--ui-anim);
transform: translateY(1.2rem);
} }
.slider:before { .slider:before {

View File

@ -30,7 +30,7 @@
</script> </script>
<label> <label>
<small disabled={$$restProps.disabled}> <small class:disabled={$$restProps.disabled}>
{label} {label}
{#if valid !== true} {#if valid !== true}
<span class="error-text">{valid}</span> <span class="error-text">{valid}</span>
@ -54,6 +54,7 @@
label { label {
position: relative; position: relative;
display: block; display: block;
width: 100%;
} }
label > small { label > small {

View File

@ -1,5 +1,5 @@
<script lang="ts"> <script lang="ts">
import { AES, Hex } from 'occulto' import { AES, Hex, Bytes } from 'occulto'
import { t } from 'svelte-intl-precompile' import { t } from 'svelte-intl-precompile'
import { blur } from 'svelte/transition' import { blur } from 'svelte/transition'
@ -57,13 +57,14 @@
try { try {
loading = $t('common.encrypting') loading = $t('common.encrypting')
const key = await AES.generateKey() const derived = note.password && (await AES.derive(note.password))
const password = Hex.encode(key) const key = derived ? derived[0] : await AES.generateKey()
const data: Note = { const data: Note = {
contents: '', contents: '',
meta: note.meta, meta: note.meta,
} }
if (derived) data.meta.derivation = derived[1]
if (isFile) { if (isFile) {
if (files.length === 0) throw new EmptyContentError() if (files.length === 0) throw new EmptyContentError()
data.contents = await Adapters.Files.encrypt(files, key) data.contents = await Adapters.Files.encrypt(files, key)
@ -77,8 +78,8 @@
loading = $t('common.uploading') loading = $t('common.uploading')
const response = await create(data) const response = await create(data)
result = { result = {
password: password,
id: response.id, id: response.id,
password: note.password ? undefined : Hex.encode(key),
} }
notify.success($t('home.messages.note_created')) notify.success($t('home.messages.note_created'))
} catch (e) { } catch (e) {
@ -148,7 +149,7 @@
{#if advanced} {#if advanced}
<div transition:blur={{ duration: 250 }}> <div transition:blur={{ duration: 250 }}>
<br /> <hr />
<AdvancedParameters bind:note bind:timeExpiration /> <AdvancedParameters bind:note bind:timeExpiration />
</div> </div>
{/if} {/if}

View File

@ -23,6 +23,7 @@
right: 0; right: 0;
width: 100%; width: 100%;
background-color: var(--ui-bg-0-85); background-color: var(--ui-bg-0-85);
backdrop-filter: blur(2px);
} }
a { a {

View File

@ -1,30 +1,35 @@
<script lang="ts"> <script lang="ts">
import { Hex } from 'occulto' import { AES, Hex } from 'occulto'
import { onMount } from 'svelte' import { onMount } from 'svelte'
import { t } from 'svelte-intl-precompile' import { t } from 'svelte-intl-precompile'
import Button from '$lib/ui/Button.svelte' import Button from '$lib/ui/Button.svelte'
import Loader from '$lib/ui/Loader.svelte' import Loader from '$lib/ui/Loader.svelte'
import ShowNote, { type DecryptedNote } from '$lib/ui/ShowNote.svelte' import ShowNote, { type DecryptedNote } from '$lib/ui/ShowNote.svelte'
import { Adapters, get, info } from '@cryptgeon/shared' import TextInput from '$lib/ui/TextInput.svelte'
import { Adapters, get, info, type NoteMeta } from '@cryptgeon/shared'
import type { PageData } from './$types' import type { PageData } from './$types'
export let data: PageData export let data: PageData
let id = data.id let id = data.id
let password: string let password: string | null = null
let note: DecryptedNote | null = null let note: DecryptedNote | null = null
let exists = false let exists = false
let meta: NoteMeta | null = null
let loading: string | null = null let loading: string | null = null
let error: string | null = null let error: string | null = null
$: valid = !!password?.length
onMount(async () => { onMount(async () => {
// Check if note exists // Check if note exists
try { try {
loading = $t('common.loading') loading = $t('common.loading')
password = window.location.hash.slice(1) password = window.location.hash.slice(1)
await info(id) const note = await info(id)
meta = note.meta
exists = true exists = true
} catch { } catch {
exists = false exists = false
@ -38,11 +43,18 @@
*/ */
async function show() { async function show() {
try { try {
if (!valid) {
error = $t('show.errors.no_password')
return
}
// Load note
error = null error = null
loading = $t('common.downloading') loading = $t('common.downloading')
const data = await get(id) const data = await get(id)
loading = $t('common.decrypting') loading = $t('common.decrypting')
const key = Hex.decode(password) const derived = meta?.derivation && (await AES.derive(password!, meta.derivation))
const key = derived ? derived[0] : Hex.decode(password!)
switch (data.meta.type) { switch (data.meta.type) {
case 'text': case 'text':
note = { note = {
@ -77,9 +89,18 @@
<form on:submit|preventDefault={show}> <form on:submit|preventDefault={show}>
<fieldset> <fieldset>
<p>{$t('show.explanation')}</p> <p>{$t('show.explanation')}</p>
<Button data-testid="show-note-button" type="submit">{$t('show.show_note')}</Button> {#if meta?.derivation}
<TextInput
data-testid="show-note-password"
type="password"
bind:value={password}
label={$t('common.password')}
/>
{/if}
<Button disabled={!valid} data-testid="show-note-button" type="submit"
>{$t('show.show_note')}</Button
>
{#if error} {#if error}
<br />
<p class="error-text"> <p class="error-text">
{error} {error}
<br /> <br />
@ -97,4 +118,10 @@
.loader { .loader {
text-align: center; text-align: center;
} }
fieldset {
display: flex;
flex-direction: column;
gap: 1rem;
}
</style> </style>