Replaced TOTP with cap key
- Replaced TOTP with an implementation of a cap key to better align with Tahoe's way of handling user / data.
This commit is contained in:
parent
70a92cb0e6
commit
22e8576cab
10 changed files with 161 additions and 116 deletions
|
@ -22,9 +22,5 @@
|
|||
"svelte": "^5.0.0",
|
||||
"vite": "^5.0.3",
|
||||
"vitest": "^2.0.4"
|
||||
},
|
||||
"dependencies": {
|
||||
"@paulmillr/qr": "^0.3.0",
|
||||
"otpauth": "^9.3.5"
|
||||
}
|
||||
}
|
||||
|
|
25
packages/pnpm-lock.yaml
generated
25
packages/pnpm-lock.yaml
generated
|
@ -7,13 +7,6 @@ settings:
|
|||
importers:
|
||||
|
||||
.:
|
||||
dependencies:
|
||||
'@paulmillr/qr':
|
||||
specifier: ^0.3.0
|
||||
version: 0.3.0
|
||||
otpauth:
|
||||
specifier: ^9.3.5
|
||||
version: 9.3.5
|
||||
devDependencies:
|
||||
'@sveltejs/adapter-auto':
|
||||
specifier: ^3.0.0
|
||||
|
@ -208,13 +201,6 @@ packages:
|
|||
'@jridgewell/trace-mapping@0.3.25':
|
||||
resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==}
|
||||
|
||||
'@noble/hashes@1.5.0':
|
||||
resolution: {integrity: sha512-1j6kQFb7QRru7eKN3ZDvRcP13rugwdxZqCjbiAVZfIJwgj2A65UmT4TgARXGlXgnRkORLTDTrO19ZErt7+QXgA==}
|
||||
engines: {node: ^14.21.3 || >=16}
|
||||
|
||||
'@paulmillr/qr@0.3.0':
|
||||
resolution: {integrity: sha512-3s/cagXuoXTA2gWSfSfJNanNgm2ifmqgoX8WLOs5//3qrIJ3WWHFjqFqCxvYGf46Afwv6PctT9eAOXLDGwp96Q==}
|
||||
|
||||
'@polka/url@1.0.0-next.28':
|
||||
resolution: {integrity: sha512-8LduaNlMZGwdZ6qWrKlfa+2M4gahzFkprZiAt2TF8uS0qQgBizKXpXURqvTJ4WtmupWxaLqjRb2UCTe72mu+Aw==}
|
||||
|
||||
|
@ -506,9 +492,6 @@ packages:
|
|||
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
|
||||
hasBin: true
|
||||
|
||||
otpauth@9.3.5:
|
||||
resolution: {integrity: sha512-jQyqOuQExeIl4YIiOUz4TdEcamgAgPX6UYeeS9Iit4lkvs7bwHb0JNDqchGRccbRfvWHV6oRwH36tOsVmc+7hQ==}
|
||||
|
||||
pathe@1.1.2:
|
||||
resolution: {integrity: sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==}
|
||||
|
||||
|
@ -775,10 +758,6 @@ snapshots:
|
|||
'@jridgewell/resolve-uri': 3.1.2
|
||||
'@jridgewell/sourcemap-codec': 1.5.0
|
||||
|
||||
'@noble/hashes@1.5.0': {}
|
||||
|
||||
'@paulmillr/qr@0.3.0': {}
|
||||
|
||||
'@polka/url@1.0.0-next.28': {}
|
||||
|
||||
'@rollup/rollup-android-arm-eabi@4.25.0':
|
||||
|
@ -1042,10 +1021,6 @@ snapshots:
|
|||
|
||||
nanoid@3.3.7: {}
|
||||
|
||||
otpauth@9.3.5:
|
||||
dependencies:
|
||||
'@noble/hashes': 1.5.0
|
||||
|
||||
pathe@1.1.2: {}
|
||||
|
||||
pathval@2.0.0: {}
|
||||
|
|
|
@ -11,7 +11,7 @@
|
|||
|
||||
<div class='cluster nav-right'>
|
||||
<ul>
|
||||
<li><button onclick={() => goto('/auth/login')}>Login</button></li>
|
||||
<li><button onclick={() => goto('/dashboard')}>Dashboard</button></li>
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
|
|
|
@ -1,20 +0,0 @@
|
|||
import * as OTPAuth from 'otpauth'
|
||||
|
||||
let secret = new OTPAuth.Secret({ size: 20 })
|
||||
|
||||
export const totp = () => {
|
||||
let totp = new OTPAuth.TOTP({
|
||||
issuer: 'Private data',
|
||||
label: 'otp',
|
||||
algorithm: 'SHA256',
|
||||
digits: 6,
|
||||
period: 300,
|
||||
secret
|
||||
})
|
||||
|
||||
let token = totp.generate()
|
||||
let seconds = totp.period - (Math.floor(Date.now() / 1000) % totp.period)
|
||||
let uri = totp.toString()
|
||||
|
||||
return { token, seconds, uri }
|
||||
}
|
|
@ -13,6 +13,7 @@
|
|||
--border-color: #3b3b3b;
|
||||
--font-family: -apple-system, BlinkMacSystemFont, avenir next, avenir, segoe ui, helvetica neue, helvetica, Cantarell, Ubuntu, roboto, noto, arial, sans-serif;
|
||||
--navbar-height: 3em;
|
||||
--shade-color: #ccc;
|
||||
--theme-background: white;
|
||||
--theme-color: #444;
|
||||
}
|
||||
|
@ -54,4 +55,14 @@
|
|||
color: light-dark(rgba(16, 16, 16, 0.3), rgba(255, 255, 255, 0.3));
|
||||
border-color: light-dark(rgba(118, 118, 118, 0.3), rgba(195, 195, 195, 0.3));
|
||||
}
|
||||
|
||||
:global(input) {
|
||||
font-size: 0.9em;
|
||||
font-weight: 400;
|
||||
padding: 0 1em;
|
||||
height: 3em;
|
||||
border: none;
|
||||
border-radius: 2px;
|
||||
outline: solid thin var(--border-color);
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -10,5 +10,5 @@
|
|||
<div class='landing-page'>
|
||||
<h1>Private facts</h1>
|
||||
|
||||
<button onclick={() => goto('/auth/login')}>Login</button>
|
||||
<button onclick={() => goto('/dashboard')}>Dashboard</button>
|
||||
</div>
|
||||
|
|
|
@ -1,15 +0,0 @@
|
|||
import { totp } from '$lib/utils'
|
||||
|
||||
export const load = () => {
|
||||
const { token, seconds, uri } = totp()
|
||||
|
||||
return { token, seconds, uri }
|
||||
}
|
||||
|
||||
export const actions = {
|
||||
getTotp: async ({ request }) => {
|
||||
const { token, seconds, uri } = totp()
|
||||
|
||||
return { token, seconds, uri }
|
||||
}
|
||||
}
|
|
@ -1,50 +0,0 @@
|
|||
<script>
|
||||
import { onDestroy } from 'svelte'
|
||||
import encodeQR from '@paulmillr/qr'
|
||||
|
||||
let { data, form } = $props()
|
||||
|
||||
let totpForm
|
||||
let count = $state(form?.seconds ?? data.seconds)
|
||||
|
||||
const timer = setInterval(() => count--, 1000)
|
||||
const qrCode = encodeQR(data.uri, 'svg')
|
||||
|
||||
onDestroy(() => {
|
||||
clearInterval(timer)
|
||||
})
|
||||
|
||||
$effect(() => {
|
||||
if (count === 0) {
|
||||
totpForm.requestSubmit()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Login page</title>
|
||||
<meta name='description' content='Login page'/>
|
||||
</svelte:head>
|
||||
|
||||
<div class='login-content'>
|
||||
<h1>Login</h1>
|
||||
|
||||
<p>Token: {form?.token ?? data.token}.</p>
|
||||
<p>Valid for {count}s.</p>
|
||||
|
||||
{#if qrCode}
|
||||
<div class='qr-code'>
|
||||
{@html qrCode}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<form action='?/getTotp' method='post' enctype='form-data' bind:this={totpForm}>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.qr-code {
|
||||
width: 15em;
|
||||
margin: 0 auto;
|
||||
}
|
||||
</style>
|
12
packages/src/routes/dashboard/+page.server.js
Normal file
12
packages/src/routes/dashboard/+page.server.js
Normal file
|
@ -0,0 +1,12 @@
|
|||
import { fail } from '@sveltejs/kit'
|
||||
|
||||
export const actions = {
|
||||
createCapKey: async ({ request }) => {
|
||||
try {
|
||||
return { success: true, endpoint: 'createCapKey', capKey: 123 }
|
||||
} catch (err) {
|
||||
console.log({ err })
|
||||
return fail(500, { endpoint: 'createCapKey', error: err })
|
||||
}
|
||||
}
|
||||
}
|
136
packages/src/routes/dashboard/+page.svelte
Normal file
136
packages/src/routes/dashboard/+page.svelte
Normal file
|
@ -0,0 +1,136 @@
|
|||
<script>
|
||||
import { enhance } from '$app/forms'
|
||||
import { beforeNavigate } from '$app/navigation'
|
||||
|
||||
let capKey = $state()
|
||||
let capKeyInput = $state()
|
||||
let newCapKey = $state()
|
||||
let checked = $state(false)
|
||||
|
||||
const submitKey = () => {
|
||||
capKey = capKeyInput
|
||||
}
|
||||
|
||||
const persistCapKey = () => {
|
||||
capKey = newCapKey
|
||||
newCapKey = undefined
|
||||
}
|
||||
|
||||
const enhanceForm = () => {
|
||||
return async ({ result, update }) => {
|
||||
await update()
|
||||
newCapKey = result.data.capKey
|
||||
}
|
||||
}
|
||||
|
||||
beforeNavigate(({ cancel }) => {
|
||||
if (newCapKey && !capKey) {
|
||||
cancel()
|
||||
alert('Please confirm you copied and saved the cap key.')
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Private Facts dashboard</title>
|
||||
<meta name='description' content='Private Facts dashboard.'>
|
||||
</svelte:head>
|
||||
<h1>Dashboard</h1>
|
||||
|
||||
{#if !capKey && !newCapKey}
|
||||
<div class='cap-key-div add'>
|
||||
<div class='cap-key'>
|
||||
<label for='capKeyInput'>Cap key:</label>
|
||||
<input type='text' name='capKeyInput' bind:value={capKeyInput} />
|
||||
<button onclick={submitKey}>Submit</button>
|
||||
</div>
|
||||
|
||||
<form action='?/createCapKey' method='post' enctype='form-data' use:enhance={enhanceForm}>
|
||||
<button type='submit'>New cap key</button>
|
||||
</form>
|
||||
</div>
|
||||
{:else if !capKey && newCapKey}
|
||||
<div class='cap-key-div get'>
|
||||
<label for='newCapKey'><h2>New cap key</h2></label>
|
||||
<input type='text' id='newCapKey' name='newCapKey' value={newCapKey} />
|
||||
|
||||
<div class='warning'>
|
||||
<h3>Attention!</h3>
|
||||
|
||||
<p>This is your new cap key. You need it to access your folders and files. It will not be saved nor ever shown again.</p>
|
||||
|
||||
<p><strong>Copy it now and save it somewhere safe.</strong></p>
|
||||
|
||||
<p>If you lose your cap key you will not be able to access your files anymore. You will have to create a new cap key and upload new files and folders.</p>
|
||||
|
||||
<div class='confirm-copy'>
|
||||
<input type='checkbox' id='checkbox' name='checkbox' bind:checked />
|
||||
<label for='copyCheckbox'>Copied and saved.</label>
|
||||
</div>
|
||||
|
||||
<button disabled={!checked} onclick={persistCapKey}>Ok</button>
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<h2>File tree</h2>
|
||||
<p>{capKey}</p>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
form, .cap-key {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
form button {
|
||||
margin: 0 0 0 auto;
|
||||
}
|
||||
|
||||
.cap-key {
|
||||
margin: auto 0 2em 0;
|
||||
}
|
||||
|
||||
.cap-key label {
|
||||
min-width: 5em;
|
||||
margin: auto 0;
|
||||
}
|
||||
|
||||
.cap-key input {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.cap-key button {
|
||||
min-width: 10em;
|
||||
margin: auto 0 auto 1em;
|
||||
}
|
||||
|
||||
.cap-key-div {
|
||||
max-width: 40em;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.cap-key-div.get {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.confirm-copy {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.confirm-copy input {
|
||||
height: unset;
|
||||
margin-right: 1em;
|
||||
border: solid thin var(--border-color);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.warning {
|
||||
border: solid thin var(--border-color);
|
||||
border-radius: 10px;
|
||||
background-color: var(--shade-color);
|
||||
padding: 0.5em;
|
||||
margin: 2em auto;
|
||||
}
|
||||
</style>
|
Loading…
Add table
Add a link
Reference in a new issue