Implemented routing instead of internal state
All checks were successful
/ Build the server (push) Successful in 3m5s

This commit is contained in:
Mutzi 2023-10-24 12:50:04 +02:00
parent 7334bd8e71
commit c8b2ae30c8
Signed by: root
GPG Key ID: 2437494E09F13876
18 changed files with 135 additions and 121 deletions

View File

@ -11,6 +11,7 @@
"@microsoft/fetch-event-source": "^2.0.1",
"filesize": "^10.1.0",
"qrcode-svg": "^1.1.0",
"svelte-spa-router": "^3.3.0",
"tailwind-merge": "^1.14.0"
},
"devDependencies": {
@ -2619,6 +2620,14 @@
"node": ">=8.10.0"
}
},
"node_modules/regexparam": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/regexparam/-/regexparam-2.0.1.tgz",
"integrity": "sha512-zRgSaYemnNYxUv+/5SeoHI0eJIgTL/A2pUtXUPLHQxUldagouJ9p+K6IbIZ/JiQuCEv2E2B1O11SjVQy3aMCkw==",
"engines": {
"node": ">=8"
}
},
"node_modules/relateurl": {
"version": "0.2.7",
"resolved": "https://registry.npmjs.org/relateurl/-/relateurl-0.2.7.tgz",
@ -3037,6 +3046,17 @@
"node": ">=12"
}
},
"node_modules/svelte-spa-router": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/svelte-spa-router/-/svelte-spa-router-3.3.0.tgz",
"integrity": "sha512-cwRNe7cxD43sCvSfEeaKiNZg3FCizGxeMcf7CPiWRP3jKXjEma3vxyyuDtPOam6nWbVxl9TNM3hlE/i87ZlqcQ==",
"dependencies": {
"regexparam": "2.0.1"
},
"funding": {
"url": "https://github.com/sponsors/ItalyPaleAle"
}
},
"node_modules/svg.draggable.js": {
"version": "2.2.2",
"resolved": "https://registry.npmjs.org/svg.draggable.js/-/svg.draggable.js-2.2.2.tgz",

View File

@ -35,6 +35,7 @@
"@microsoft/fetch-event-source": "^2.0.1",
"filesize": "^10.1.0",
"qrcode-svg": "^1.1.0",
"svelte-spa-router": "^3.3.0",
"tailwind-merge": "^1.14.0"
}
}

View File

@ -1,37 +1,24 @@
<script lang="ts">
import {changeStateFunction, error_banner, info_banner, rpc, session, show_working, state, StateE, token, workingWrapperO} from './store';
import {Banner, Navbar, Spinner} from 'flowbite-svelte';
import {error_banner, info_banner, rpc, session, show_working, token, workingWrapperO} from './store';
import {Banner, Navbar, NavBrand, Spinner} from 'flowbite-svelte';
import Router, {replace} from 'svelte-spa-router';
import {routes} from './routes';
import {FileStorage} from './icons';
import LinkButton from './components/LinkButton.svelte';
import Login from './pages/Login.svelte';
import Signup from './pages/Signup.svelte';
import ResetPassword from './pages/ResetPassword.svelte';
import Profile from './pages/Profile.svelte';
import TfaSetup from './pages/TfaSetup.svelte';
import Admin from './pages/Admin.svelte';
import View from './pages/View.svelte';
import A from './components/A.svelte';
const s = session.s;
function homeClick() {
if ($token == null)
$state.s = StateE.LOGIN;
else
$state = { s: StateE.VIEW, view_node: 0 };
}
async function leaveSudo() {
await workingWrapperO(() => rpc.Admin_unsudo($token ?? ''));
await session.update($token);
state.set({s: StateE.ADMIN, view_node: 0});
await replace('/admin');
}
function logout() {
rpc.Auth_logout($token ?? '');
token.set(null);
}
homeClick();
</script>
<main class="h-screen w-screen p-4 flex flex-col">
@ -49,43 +36,22 @@
<Banner position="absolute" dismissable={false}><Spinner size="5" class="mr-2" />Working</Banner>
{/if}
<Navbar class="flex-grow-0">
<button on:click={homeClick} id="home-button" class="flex items-center">
<NavBrand href={$token == null ? '#/login' : '#/view/0'}>
<FileStorage width="1.5em" height="1.5em"/>
<span id="navbar-text">MFileserver</span>
</button>
<span id="navbar-text" class="ml-2">MFileserver</span>
</NavBrand>
{#if $token != null}
<div class="flex md:order-2">
<div class="flex md:order-2 gap-x-2">
{#if $s?.sudo} <LinkButton on:click={leaveSudo}>Leave sudo</LinkButton> {/if}
{#if $s?.admin} <LinkButton on:click={changeStateFunction(StateE.ADMIN)}>Admin</LinkButton> {/if}
<LinkButton on:click={changeStateFunction(StateE.VIEW, 0)}>Files</LinkButton>
<LinkButton on:click={changeStateFunction(StateE.PROFILE)}>Profile</LinkButton>
{#if $s?.admin} <A href="#/admin">Admin</A> {/if}
<A href="#/view/0">Files</A>
<A href="#/profile">Profile</A>
<LinkButton on:click={logout}>Logout</LinkButton>
</div>
{/if}
</Navbar>
<span class="grid justify-items-center mt-10">
{#if $state.s === StateE.LOGIN } <Login/>
{:else if $state.s === StateE.SIGNUP} <Signup/>
{:else if $state.s === StateE.RESET_PASSWORD} <ResetPassword/>
{:else if $state.s === StateE.PROFILE} <Profile/>
{:else if $state.s === StateE.TFA_SETUP} <TfaSetup/>
{:else if $state.s === StateE.ADMIN} <Admin/>
{:else if $state.s === StateE.VIEW} <View/>
{:else} <span>You are in state {$state.s}, which should not be possible, please report this.</span>
{/if}
<Router {routes} />
</span>
</main>
<style>
#navbar-text {
margin-left: 0.5em;
font-weight: 500;
}
#home-button {
background: none;
border: none;
cursor: pointer;
}
</style>

View File

@ -0,0 +1,8 @@
<script lang="ts">
import {A} from 'flowbite-svelte';
export let href: string;
</script>
<A {href} aClass="hover:text-primary-400 transition-colors">
<slot></slot>
</A>

View File

@ -1,7 +1,7 @@
<script lang="ts">
import {rpc, show_working, state, token} from '../store';
import {rpc, show_working, token} from '../store';
import {Button, ButtonGroup, Modal} from 'flowbite-svelte';
import {afterUpdate} from 'svelte';
import {afterUpdate, createEventDispatcher} from 'svelte';
let show_confirm = false;
let show_modal = false;
@ -9,6 +9,8 @@
let text = '';
let nodes: number[] = [];
const dispatch = createEventDispatcher<{reload_node: null}>();
async function real_delete() {
show_confirm = false;
show_modal = true;
@ -27,7 +29,7 @@
show_working.set(false);
show_modal = false;
state.update(v => v);
dispatch('reload_node');
}
export const del = async (n: number[]) => {

View File

@ -4,11 +4,12 @@
</script>
<script lang="ts">
import {Checkbox, Dropdown, DropdownItem, Spinner, Table, TableBody, TableBodyCell, TableBodyRow, TableHead, TableHeadCell, Tooltip} from 'flowbite-svelte';
import {Folder, FolderParent, DocumentBlank, CaretLeft} from '../icons';
import {filesize} from 'filesize';
import {api, changeStateFunction, download, StateE, token, rpc} from '../store';
import {Folder, FolderParent, DocumentBlank, CaretLeft} from '../icons';
import {api, download, token, rpc} from '../store';
import LinkButton from './LinkButton.svelte';
import DeleteModal from './DeleteModal.svelte';
import A from './A.svelte';
export let node: api.Node;
@ -43,11 +44,12 @@
$: ctx_style = `top: ${ctx_y}px; left: ${ctx_x}px; position: fixed;`;
function onCtxMenu(node: api.Node, e: MouseEvent) {
console.log(e);
e.preventDefault();
if (!ctx_hidden)
return ctx_hidden = true;
ctx_x = e.pageX;
ctx_y = e.pageY;
ctx_x = e.clientX;
ctx_y = e.clientY;
ctx_node = node;
ctx_hidden = false;
}
@ -70,7 +72,7 @@
<svelte:body on:click={() => (ctx_hidden = true)} />
<DeleteModal bind:del={del} />
<DeleteModal bind:del={del} on:reload_node />
<Table hoverable>
<TableHead theadClass="text-xs">
@ -86,7 +88,7 @@
<TableBodyRow>
<TableBodyCell class="!p-4"></TableBodyCell>
<TableBodyCell class="px-2 w-0"><FolderParent /></TableBodyCell>
<TableBodyCell class="pl-0"><LinkButton on:click={changeStateFunction(StateE.VIEW, node.parent ?? 0)}>..</LinkButton></TableBodyCell>
<TableBodyCell class="pl-0"><A href={'#/view/' + node.parent}>..</A></TableBodyCell>
<TableBodyCell></TableBodyCell>
</TableBodyRow>
{/if}
@ -94,7 +96,7 @@
<TableBodyRow on:contextmenu={onCtxMenu.bind(null, node)}>
<TableBodyCell class="p-2 pl-4 w-0 h-0"><Checkbox bind:group={selected} value={node.id}/></TableBodyCell>
<TableBodyCell class="px-2 w-0"><Folder /></TableBodyCell>
<TableBodyCell class="pl-0"><LinkButton on:click={changeStateFunction(StateE.VIEW, node.id)}>{node.name}</LinkButton></TableBodyCell>
<TableBodyCell class="pl-0"><A href={'#/view/' + node.id}>{node.name}</A></TableBodyCell>
<TableBodyCell></TableBodyCell>
</TableBodyRow>
{/each}
@ -112,7 +114,7 @@
<DocumentBlank />
{/if}
</TableBodyCell>
<TableBodyCell class="pl-0"><LinkButton on:click={changeStateFunction(StateE.VIEW, node.id)}>{node.name}</LinkButton></TableBodyCell>
<TableBodyCell class="pl-0"><A href={'#/view/' + node.id}>{node.name}</A></TableBodyCell>
<TableBodyCell>{filesize(node.size ?? 0, {base: 2, standard: 'jedec'})}</TableBodyCell>
</TableBodyRow>
{/each}

View File

@ -16,7 +16,7 @@
.link-button {
background: none;
border: none;
padding: 0 0.25em;
padding: 0;
cursor: pointer;
}
</style>

View File

@ -1,7 +1,11 @@
<script lang="ts">
import {state, token, type UploadFile} from '../store';
import {token, type UploadFile} from '../store';
import {Button, Modal, Progressbar} from 'flowbite-svelte';
import {filesize} from 'filesize';
import {createEventDispatcher} from 'svelte';
const dispatch = createEventDispatcher<{reload_node: null}>();
interface MyFile extends UploadFile {
waiting: boolean,
@ -24,7 +28,7 @@
function close() {
show_modal = false;
state.update(v => v);
dispatch('reload_node');
}
async function realUpload(file: MyFile) {

View File

@ -1,9 +1,10 @@
import "./app.pcss";
import App from "./App.svelte";
import {state, StateE, token} from './store';
import {token} from './store';
import {replace} from 'svelte-spa-router';
token.subscribe(v => {
if (v == null) state.set({s: StateE.LOGIN, view_node: 0});
if (v == null) replace('/login').then()
});
const app = new App({

View File

@ -1,8 +1,9 @@
<script lang="ts">
import {api, rpc, session, state, StateE, token, workingWrapperO, workingWrapperR} from '../store';
import {api, rpc, session, token, workingWrapperO, workingWrapperR} from '../store';
import {Checkbox, Table, TableBody, TableBodyCell, TableBodyRow, TableHead, TableHeadCell} from 'flowbite-svelte';
import {Checkmark, Error} from '../icons';
import LinkButton from '../components/LinkButton.svelte';
import {replace} from 'svelte-spa-router';
let users: api.UserInfo[] = [];
@ -25,7 +26,7 @@
async function sudo(user: number) {
if (await workingWrapperO(() => rpc.Admin_sudo($token ?? '', user))) {
await session.update($token);
state.set({s: StateE.VIEW, view_node: 0});
await replace('/view/0');
}
}

View File

@ -1,7 +1,8 @@
<script lang="ts">
import {Button, ButtonGroup, Card, Input, InputAddon} from 'flowbite-svelte';
import {Email, OTP, Password} from '../icons';
import {changeStateFunction, rpc, state, StateE, token, workingWrapperR, api} from '../store';
import {rpc, token, workingWrapperR, api} from '../store';
import {replace} from 'svelte-spa-router';
let ask_tfa = false;
let username = '', password = '', tfa = '';
@ -14,7 +15,7 @@
return;
}
token.set(resp.token);
state.set({s: StateE.VIEW, view_node: 0});
await replace('/view/0');
}
function keyUp(e: KeyboardEvent) {
@ -41,9 +42,9 @@
<Input type="password" placeholder="Password" bind:value={password} on:keyup={keyUp}></Input>
</ButtonGroup>
<ButtonGroup class="w-full flex flex-nowrap">
<Button class="flex-1 flex-grow" color="primary" outline on:click={changeStateFunction(StateE.SIGNUP)}>Signup</Button>
<Button class="flex-1 flex-grow" color="primary" outline href="#/signup">Signup</Button>
<Button class="flex-1 flex-grow" color="primary" on:click={login}>Login</Button>
<Button class="flex-1 flex-grow" color="primary" outline on:click={changeStateFunction(StateE.RESET_PASSWORD)}>Forget password</Button>
<Button class="flex-1 flex-grow" color="primary" outline href="#/reset_pw">Forget password</Button>
</ButtonGroup>
{/if}
</Card>

View File

@ -1,5 +1,5 @@
<script lang="ts">
import {changeStateFunction, error_banner, rpc, session, StateE, token, workingWrapperO} from '../store';
import {error_banner, rpc, session, token, workingWrapperO} from '../store';
import {Accordion, AccordionItem, Button, ButtonGroup, Input, InputAddon} from 'flowbite-svelte';
import {Password} from '../icons';
import {info_banner} from '../store.js';
@ -69,7 +69,7 @@
{#if tfa_enabled}
<Button class="w-full" color="red" on:click={disableTfa}>Disable</Button>
{:else}
<Button class="w-full" color="green" on:click={changeStateFunction(StateE.TFA_SETUP)}>Enable</Button>
<Button class="w-full" color="green" href="#/tfa">Enable</Button>
{/if}
</AccordionItem>
<AccordionItem>

View File

@ -1,7 +1,8 @@
<script lang="ts">
import {Button, ButtonGroup, Card, Input, InputAddon} from 'flowbite-svelte';
import {Email, EmailNew, Password} from '../icons';
import {changeStateFunction, error_banner, info_banner, rpc, state, StateE, workingWrapper, workingWrapperO} from '../store';
import {error_banner, info_banner, rpc, workingWrapper, workingWrapperO} from '../store';
import {replace} from 'svelte-spa-router';
let enter_key = false;
let username = '', key = '', password = '', password2 = '';
@ -20,7 +21,7 @@
}
if (await workingWrapperO(() => rpc.Auth_reset_password(key, password)))
$state.s = StateE.LOGIN;
await replace('/login');
}
function keyUp(e: KeyboardEvent) {
@ -49,7 +50,7 @@
<Input type="password" placeholder="Repeat password" bind:value={password2} on:keyup={keyUp}></Input>
</ButtonGroup>
<ButtonGroup class="w-full flex flex-nowrap">
<Button class="flex-1 flex-grow" color="primary" outline on:click={changeStateFunction(StateE.LOGIN)}>Login</Button>
<Button class="flex-1 flex-grow" color="primary" outline href="#/login">Login</Button>
<Button class="flex-1 flex-grow" color="primary" on:click={changePw}>Change password</Button>
</ButtonGroup>
{:else}
@ -58,7 +59,7 @@
<Input type="email" placeholder="Email" bind:value={username} on:keyup={keyUp}></Input>
</ButtonGroup>
<ButtonGroup class="w-full flex flex-nowrap">
<Button class="flex-1 flex-grow" color="primary" outline on:click={changeStateFunction(StateE.LOGIN)}>Login</Button>
<Button class="flex-1 flex-grow" color="primary" outline href="#/login">Login</Button>
<Button class="flex-1 flex-grow" color="primary" on:click={sendKey}>Send recovery key</Button>
<Button class="flex-1 flex-grow" color="primary" outline on:click={() => (enter_key = true)}>Enter key</Button>
</ButtonGroup>

View File

@ -1,7 +1,8 @@
<script lang="ts">
import {Button, ButtonGroup, Card, Input, InputAddon} from 'flowbite-svelte';
import {Email, Password} from '../icons';
import {changeStateFunction, error_banner, info_banner, rpc, state, StateE, workingWrapperO} from '../store';
import {error_banner, info_banner, rpc, workingWrapperO} from '../store';
import {replace} from 'svelte-spa-router';
let username = '', username2 = '', password = '', password2 = '';
@ -20,7 +21,7 @@
if (resp) {
info_banner.set('Account created, please wait till an administrator approves it');
$state.s = StateE.LOGIN;
await replace('/login');
}
}
@ -48,7 +49,7 @@
<Input type="password" placeholder="Repeat password" bind:value={password2} on:keyup={keyUp}></Input>
</ButtonGroup>
<ButtonGroup class="w-full flex flex-nowrap">
<Button class="flex-1 flex-grow" color="primary" outline on:click={changeStateFunction(StateE.LOGIN)}>Login</Button>
<Button class="flex-1 flex-grow" color="primary" outline href="#/login">Login</Button>
<Button class="flex-1 flex-grow" color="primary" on:click={signup}>Singup</Button>
</ButtonGroup>
</Card>

View File

@ -1,8 +1,9 @@
<script lang="ts">
import {Button, ButtonGroup, Card, Input, InputAddon, StepIndicator, Tooltip} from 'flowbite-svelte';
import {OTP} from '../icons';
import {info_banner, rpc, session, state, StateE, token, workingWrapperO, workingWrapperR} from '../store';
import {info_banner, rpc, session, token, workingWrapperO, workingWrapperR} from '../store';
import QRCode from 'qrcode-svg';
import {replace} from 'svelte-spa-router';
const s = session.s;
@ -35,7 +36,6 @@
async function completeSetup() {
if (await workingWrapperO(() => rpc.Auth_tfa_complete($token ?? '', code))) {
info_banner.set("Successfully set up two factor authentication");
$state.s = StateE.LOGIN;
token.set(null);
}
}

View File

@ -1,33 +1,38 @@
<script lang="ts">
import {Breadcrumb, Dropzone, Modal, Progressbar} from 'flowbite-svelte';
import {derived} from 'svelte/store';
import {writable} from 'svelte/store';
import {CloudUpload} from '../icons';
import {api, changeStateFunction, rpc, state, StateE, token, type UploadFile, workingWrapperR} from '../store';
import LinkButton from '../components/LinkButton.svelte';
import {api, rpc, token, type UploadFile, workingWrapperR} from '../store';
import DirViewer from '../components/DirViewer.svelte';
import UploadModal from '../components/UploadModal.svelte';
import FileViewer from '../components/FileViewer.svelte';
import A from '../components/A.svelte';
interface Data {
node: api.Node|null,
segments: api.PathSegment[]
}
let data = derived<typeof state, Data>(
state,
($state, set) => {
(async () => {
let node = await workingWrapperR<api.Node>(() => rpc.FS_get_node($token ?? '', $state.view_node));
if (!node)
return $state.view_node = 0;
let segments = await workingWrapperR<api.PathSegment[]>(() => rpc.FS_get_path($token ?? '', node!.id));
if (!segments)
return $state.view_node = 0;
set({node: node as Data['node'], segments });
})();
},
{ node: null, segments: [] }
);
export let params: {id?: string}|undefined = {};
$: {
let id = 0;
if (params && params.id) {
id = parseInt(params.id);
if (id >= 0)
updateData(id);
}
}
const data = writable<Data>({node: null, segments: []});
async function updateData(id: number) {
let node = await workingWrapperR<api.Node>(() => rpc.FS_get_node($token ?? '', id));
if (!node)
return;
let segments = await workingWrapperR<api.PathSegment[]>(() => rpc.FS_get_path($token ?? '', id));
if (!segments)
return;
data.set({node: node as Data['node'], segments });
}
const getFile = async (entry: FileSystemEntry) => new Promise<File>((o, e) => (entry as FileSystemFileEntry).file(o, e));
@ -70,7 +75,7 @@
const files: UploadFile[] = [];
for (const f of input.files)
files.push({
id: $state.view_node,
id: $data.node?.id ?? 0,
name: f.name,
full_name: f.name,
file: f,
@ -89,7 +94,7 @@
if (!entry)
console.error("Failed to get entry for: ", i);
else
files.push(...await handleEntry($state.view_node, '', entry));
files.push(...await handleEntry($data.node?.id ?? 0, '', entry));
}
await upload(files);
}
@ -120,7 +125,7 @@
{#if i > 0}<li class="inline-flex items-center">/</li>{/if}
<li class="inline-flex items-center">
{#if segment.id !== null}
<LinkButton on:click={changeStateFunction(StateE.VIEW, segment.id)}>{segment.id === 0 ? 'Files' : segment.name}</LinkButton>
<A href={'#/view/' + segment.id}>{segment.id === 0 ? 'Files' : segment.name}</A>
{:else}
<span style="padding: 0 0.25em;">{segment.name}</span>
{/if}
@ -142,10 +147,10 @@
{:else if $data.node.file}
<FileViewer node={$data.node} />
{:else}
<DirViewer node={$data.node} />
<DirViewer node={$data.node} on:reload_node={() => updateData($data.node?.id ?? 0)} />
{/if}
</div>
<UploadModal bind:upload={real_upload}/>
<UploadModal bind:upload={real_upload} on:reload_node={() => updateData($data.node?.id ?? 0)} />
{#if upload_progress_data.current !== upload_progress_data.total}
<Modal open dismissable={false} title="Creating files">
<div class="mb-1 flex justify-between">

17
frontend/src/routes.ts Normal file
View File

@ -0,0 +1,17 @@
import Login from './pages/Login.svelte';
import Signup from './pages/Signup.svelte';
import ResetPassword from './pages/ResetPassword.svelte';
import Profile from './pages/Profile.svelte';
import TfaSetup from './pages/TfaSetup.svelte';
import Admin from './pages/Admin.svelte';
import View from './pages/View.svelte';
export const routes = {
'/login': Login,
'/signup': Signup,
'/reset_pw': ResetPassword,
'/profile': Profile,
'/tfa': TfaSetup,
'/admin': Admin,
'/view/:id': View
}

View File

@ -1,14 +1,9 @@
import {MRPCConnector, type Session, type Response} from './api';
import {get, type Writable, writable} from 'svelte/store';
import {type Writable, writable} from 'svelte/store';
import {filesize} from 'filesize';
export * as api from './api';
export enum StateE { LOGIN, SIGNUP, RESET_PASSWORD, PROFILE, TFA_SETUP, ADMIN, VIEW }
export interface State {
s: StateE,
view_node: number
}
export interface UploadFile {
id: number,
name: string,
@ -21,15 +16,12 @@ export const show_working = writable<boolean>(false);
export const info_banner = writable<string>('');
export const error_banner = writable<string>('');
export const state = writable<State>({s: StateE.LOGIN, view_node: 0});
export const rpc = new MRPCConnector('/mrpc');
export const token = writable<string|null>(localStorage.getItem('token'));
export const session: { s: Writable<Session|null>, update: (token: string|null) => Promise<void> } = {
s: writable(null),
update: async (t: string|null) => {
console.log('S');
if (t == null) {
session.s.set(null);
return;
@ -50,14 +42,6 @@ token.subscribe(v => {
localStorage.setItem('token', v);
})
export function changeStateFunction(target: StateE, node?: number): () => void {
return () => {
const new_node = node ?? get(state).view_node;
state.set({s: target, view_node: new_node});
}
}
export async function workingWrapper<T>(fn: () => Promise<T>): Promise<T|null> {
let r = null;
error_banner.set('');