This commit is contained in:
		
							
								
								
									
										1
									
								
								frontend/favicon.svg.base64
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								frontend/favicon.svg.base64
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1 @@
 | 
			
		||||
PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB2aWV3Qm94PSIwIDAgMzIgMzIiPjxwYXRoIGQ9Ik0yOCAyMGgtMnYyaDJ2Nkg0di02aDJ2LTJINGEyLjAwMiAyLjAwMiAwIDAgMC0yIDJ2NmEyLjAwMiAyLjAwMiAwIDAgMCAyIDJoMjRhMi4wMDIgMi4wMDIgMCAwIDAgMi0ydi02YTIuMDAyIDIuMDAyIDAgMCAwLTItMnoiIGZpbGw9ImN1cnJlbnRDb2xvciI+PC9wYXRoPjxjaXJjbGUgY3g9IjciIGN5PSIyNSIgcj0iMSIgZmlsbD0iY3VycmVudENvbG9yIj48L2NpcmNsZT48cGF0aCBkPSJNMjIuNzA3IDcuMjkzbC01LTVBMSAxIDAgMCAwIDE3IDJoLTZhMi4wMDIgMi4wMDIgMCAwIDAtMiAydjE2YTIuMDAyIDIuMDAyIDAgMCAwIDIgMmgxMGEyLjAwMiAyLjAwMiAwIDAgMCAyLTJWOGExIDEgMCAwIDAtLjI5My0uNzA3ek0yMC41ODYgOEgxN1Y0LjQxNHpNMTEgMjBWNGg0djRhMi4wMDIgMi4wMDIgMCAwIDAgMiAyaDR2MTB6IiBmaWxsPSJjdXJyZW50Q29sb3IiPjwvcGF0aD48L3N2Zz4=
 | 
			
		||||
@@ -2,7 +2,7 @@
 | 
			
		||||
<html lang="en">
 | 
			
		||||
<head>
 | 
			
		||||
    <meta charset="UTF-8"/>
 | 
			
		||||
     <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
 | 
			
		||||
     <link rel="icon" type="image/svg+xml" href="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB2aWV3Qm94PSIwIDAgMzIgMzIiPjxwYXRoIGQ9Ik0yOCAyMGgtMnYyaDJ2Nkg0di02aDJ2LTJINGEyLjAwMiAyLjAwMiAwIDAgMC0yIDJ2NmEyLjAwMiAyLjAwMiAwIDAgMCAyIDJoMjRhMi4wMDIgMi4wMDIgMCAwIDAgMi0ydi02YTIuMDAyIDIuMDAyIDAgMCAwLTItMnoiIGZpbGw9ImN1cnJlbnRDb2xvciI+PC9wYXRoPjxjaXJjbGUgY3g9IjciIGN5PSIyNSIgcj0iMSIgZmlsbD0iY3VycmVudENvbG9yIj48L2NpcmNsZT48cGF0aCBkPSJNMjIuNzA3IDcuMjkzbC01LTVBMSAxIDAgMCAwIDE3IDJoLTZhMi4wMDIgMi4wMDIgMCAwIDAtMiAydjE2YTIuMDAyIDIuMDAyIDAgMCAwIDIgMmgxMGEyLjAwMiAyLjAwMiAwIDAgMCAyLTJWOGExIDEgMCAwIDAtLjI5My0uNzA3ek0yMC41ODYgOEgxN1Y0LjQxNHpNMTEgMjBWNGg0djRhMi4wMDIgMi4wMDIgMCAwIDAgMiAyaDR2MTB6IiBmaWxsPSJjdXJyZW50Q29sb3IiPjwvcGF0aD48L3N2Zz4=" />
 | 
			
		||||
    <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
 | 
			
		||||
    <title>MFileserver</title>
 | 
			
		||||
</head>
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										3581
									
								
								frontend/package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										3581
									
								
								frontend/package-lock.json
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							@@ -1,13 +1,15 @@
 | 
			
		||||
{
 | 
			
		||||
  "name": "frontend",
 | 
			
		||||
  "private": true,
 | 
			
		||||
  "packageManager": "pnpm@9.0.0",
 | 
			
		||||
  "version": "0.0.0",
 | 
			
		||||
  "type": "module",
 | 
			
		||||
  "scripts": {
 | 
			
		||||
    "dev": "vite",
 | 
			
		||||
    "build": "vite build",
 | 
			
		||||
    "preview": "vite preview",
 | 
			
		||||
    "check": "svelte-check --tsconfig ./tsconfig.json"
 | 
			
		||||
    "check": "svelte-check --tsconfig ./tsconfig.json",
 | 
			
		||||
    "fetch-api": "openapi-typescript http://127.0.0.1:2121/openapi.json -o ./src/api/schema.d.ts"
 | 
			
		||||
  },
 | 
			
		||||
  "devDependencies": {
 | 
			
		||||
    "@iconify/json": "^2.2.132",
 | 
			
		||||
@@ -18,6 +20,7 @@
 | 
			
		||||
    "autoprefixer": "^10.4.14",
 | 
			
		||||
    "flowbite": "^1.8.1",
 | 
			
		||||
    "flowbite-svelte": "^0.44.18",
 | 
			
		||||
    "openapi-typescript": "^7.3.3",
 | 
			
		||||
    "postcss": "^8.4.24",
 | 
			
		||||
    "postcss-load-config": "^4.0.1",
 | 
			
		||||
    "svelte": "^4.0.5",
 | 
			
		||||
@@ -34,6 +37,7 @@
 | 
			
		||||
  "dependencies": {
 | 
			
		||||
    "@microsoft/fetch-event-source": "^2.0.1",
 | 
			
		||||
    "filesize": "^10.1.0",
 | 
			
		||||
    "openapi-fetch": "^0.12.0",
 | 
			
		||||
    "qrcode-svg": "^1.1.0",
 | 
			
		||||
    "svelte-spa-router": "^3.3.0",
 | 
			
		||||
    "tailwind-merge": "^1.14.0"
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										3018
									
								
								frontend/pnpm-lock.yaml
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										3018
									
								
								frontend/pnpm-lock.yaml
									
									
									
										generated
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							@@ -1 +0,0 @@
 | 
			
		||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 32 32"><path d="M28 20h-2v2h2v6H4v-6h2v-2H4a2.002 2.002 0 0 0-2 2v6a2.002 2.002 0 0 0 2 2h24a2.002 2.002 0 0 0 2-2v-6a2.002 2.002 0 0 0-2-2z" fill="currentColor"></path><circle cx="7" cy="25" r="1" fill="currentColor"></circle><path d="M22.707 7.293l-5-5A1 1 0 0 0 17 2h-6a2.002 2.002 0 0 0-2 2v16a2.002 2.002 0 0 0 2 2h10a2.002 2.002 0 0 0 2-2V8a1 1 0 0 0-.293-.707zM20.586 8H17V4.414zM11 20V4h4v4a2.002 2.002 0 0 0 2 2h4v10z" fill="currentColor"></path></svg>
 | 
			
		||||
| 
		 Before Width: | Height: | Size: 557 B  | 
@@ -1,5 +1,5 @@
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
    import {error_banner, info_banner, rpc, session, show_working, token, workingWrapperO} from './store';
 | 
			
		||||
    import {error_banner, info_banner, rpc, session, show_working, token, workingWrapper} from './store';
 | 
			
		||||
    import {Banner, Navbar, NavBrand, Spinner} from 'flowbite-svelte';
 | 
			
		||||
    import Router, {replace} from 'svelte-spa-router';
 | 
			
		||||
    import {routes} from './routes';
 | 
			
		||||
@@ -10,13 +10,13 @@
 | 
			
		||||
    const s = session.s;
 | 
			
		||||
 | 
			
		||||
    async function leaveSudo() {
 | 
			
		||||
        await workingWrapperO(() => rpc.Admin_unsudo($token ?? ''));
 | 
			
		||||
        await workingWrapper(() => rpc.admin.unSudo());
 | 
			
		||||
        await session.update($token);
 | 
			
		||||
        await replace('/admin');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function logout() {
 | 
			
		||||
        rpc.Auth_logout($token ?? '');
 | 
			
		||||
    async function logout() {
 | 
			
		||||
        await rpc.logout();
 | 
			
		||||
        token.set(null);
 | 
			
		||||
    }
 | 
			
		||||
</script>
 | 
			
		||||
 
 | 
			
		||||
@@ -1,311 +0,0 @@
 | 
			
		||||
import { fetchEventSource } from '@microsoft/fetch-event-source';
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
export interface ResponseE {
 | 
			
		||||
    e: string,
 | 
			
		||||
    o: null
 | 
			
		||||
}
 | 
			
		||||
interface ResponseO<T> {
 | 
			
		||||
    e: null,
 | 
			
		||||
    o: T
 | 
			
		||||
}
 | 
			
		||||
export type Response<T> = ResponseE | ResponseO<T>;
 | 
			
		||||
 | 
			
		||||
export interface LoginResponse {
 | 
			
		||||
    otp_needed: boolean;
 | 
			
		||||
    token: (string|null);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface Session {
 | 
			
		||||
    name: string;
 | 
			
		||||
    tfa_enabled: boolean;
 | 
			
		||||
    admin: boolean;
 | 
			
		||||
    sudo: boolean;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface UserInfo {
 | 
			
		||||
    id: number;
 | 
			
		||||
    name: string;
 | 
			
		||||
    tfa: boolean;
 | 
			
		||||
    admin: boolean;
 | 
			
		||||
    enabled: boolean;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface Node {
 | 
			
		||||
    id: number;
 | 
			
		||||
    name: string;
 | 
			
		||||
    file: boolean;
 | 
			
		||||
    preview: boolean;
 | 
			
		||||
    parent: (number|null);
 | 
			
		||||
    size: (number|null);
 | 
			
		||||
    children: (Node[]|null);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface CreateNodeInfo {
 | 
			
		||||
    id: number;
 | 
			
		||||
    exists: boolean;
 | 
			
		||||
    file: boolean;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface ZipInfo {
 | 
			
		||||
    done: boolean;
 | 
			
		||||
    progress: number;
 | 
			
		||||
    total: number;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface PathSegment {
 | 
			
		||||
    name: string;
 | 
			
		||||
    id: (number|null);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
export class MRPCConnector {
 | 
			
		||||
    url: string;
 | 
			
		||||
 | 
			
		||||
    private __create_msg(service: string, method: string, data: any) {
 | 
			
		||||
        return {service, method, data};
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public constructor(url: string) {
 | 
			
		||||
        this.url = url;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
    public Auth_signup(username: string, password: string): Promise<(string|null)> {
 | 
			
		||||
        const __msg = this.__create_msg('Auth', 'signup', {username,password});
 | 
			
		||||
        return fetch(this.url, {
 | 
			
		||||
            method: 'POST',
 | 
			
		||||
            body: JSON.stringify(__msg)
 | 
			
		||||
        }).then((__r) => __r.json());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public Auth_login(username: string, password: string, otp: (string|null)): Promise<Response<LoginResponse>> {
 | 
			
		||||
        const __msg = this.__create_msg('Auth', 'login', {username,password,otp});
 | 
			
		||||
        return fetch(this.url, {
 | 
			
		||||
            method: 'POST',
 | 
			
		||||
            body: JSON.stringify(__msg)
 | 
			
		||||
        }).then((__r) => __r.json());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public Auth_send_recovery_key(username: string): Promise<void> {
 | 
			
		||||
        const __msg = this.__create_msg('Auth', 'send_recovery_key', {username});
 | 
			
		||||
        return fetch(this.url, {method: 'POST', body: JSON.stringify(__msg)}).then(__r => {});
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public Auth_reset_password(key: string, password: string): Promise<(string|null)> {
 | 
			
		||||
        const __msg = this.__create_msg('Auth', 'reset_password', {key,password});
 | 
			
		||||
        return fetch(this.url, {
 | 
			
		||||
            method: 'POST',
 | 
			
		||||
            body: JSON.stringify(__msg)
 | 
			
		||||
        }).then((__r) => __r.json());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public Auth_change_password(token: string, old_pw: string, new_pw: string): Promise<(string|null)> {
 | 
			
		||||
        const __msg = this.__create_msg('Auth', 'change_password', {token,old_pw,new_pw});
 | 
			
		||||
        return fetch(this.url, {
 | 
			
		||||
            method: 'POST',
 | 
			
		||||
            body: JSON.stringify(__msg)
 | 
			
		||||
        }).then((__r) => __r.json());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public Auth_logout(token: string): Promise<void> {
 | 
			
		||||
        const __msg = this.__create_msg('Auth', 'logout', {token});
 | 
			
		||||
        return fetch(this.url, {method: 'POST', body: JSON.stringify(__msg)}).then(__r => {});
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public Auth_logout_all(token: string): Promise<(string|null)> {
 | 
			
		||||
        const __msg = this.__create_msg('Auth', 'logout_all', {token});
 | 
			
		||||
        return fetch(this.url, {
 | 
			
		||||
            method: 'POST',
 | 
			
		||||
            body: JSON.stringify(__msg)
 | 
			
		||||
        }).then((__r) => __r.json());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public Auth_tfa_setup_mail(token: string): Promise<(string|null)> {
 | 
			
		||||
        const __msg = this.__create_msg('Auth', 'tfa_setup_mail', {token});
 | 
			
		||||
        return fetch(this.url, {
 | 
			
		||||
            method: 'POST',
 | 
			
		||||
            body: JSON.stringify(__msg)
 | 
			
		||||
        }).then((__r) => __r.json());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public Auth_tfa_setup_totp(token: string): Promise<Response<string>> {
 | 
			
		||||
        const __msg = this.__create_msg('Auth', 'tfa_setup_totp', {token});
 | 
			
		||||
        return fetch(this.url, {
 | 
			
		||||
            method: 'POST',
 | 
			
		||||
            body: JSON.stringify(__msg)
 | 
			
		||||
        }).then((__r) => __r.json());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public Auth_tfa_complete(token: string, otp: string): Promise<(string|null)> {
 | 
			
		||||
        const __msg = this.__create_msg('Auth', 'tfa_complete', {token,otp});
 | 
			
		||||
        return fetch(this.url, {
 | 
			
		||||
            method: 'POST',
 | 
			
		||||
            body: JSON.stringify(__msg)
 | 
			
		||||
        }).then((__r) => __r.json());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public Auth_tfa_disable(token: string): Promise<(string|null)> {
 | 
			
		||||
        const __msg = this.__create_msg('Auth', 'tfa_disable', {token});
 | 
			
		||||
        return fetch(this.url, {
 | 
			
		||||
            method: 'POST',
 | 
			
		||||
            body: JSON.stringify(__msg)
 | 
			
		||||
        }).then((__r) => __r.json());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public Auth_delete_user(token: string): Promise<(string|null)> {
 | 
			
		||||
        const __msg = this.__create_msg('Auth', 'delete_user', {token});
 | 
			
		||||
        return fetch(this.url, {
 | 
			
		||||
            method: 'POST',
 | 
			
		||||
            body: JSON.stringify(__msg)
 | 
			
		||||
        }).then((__r) => __r.json());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public Auth_session_info(token: string): Promise<Response<Session>> {
 | 
			
		||||
        const __msg = this.__create_msg('Auth', 'session_info', {token});
 | 
			
		||||
        return fetch(this.url, {
 | 
			
		||||
            method: 'POST',
 | 
			
		||||
            body: JSON.stringify(__msg)
 | 
			
		||||
        }).then((__r) => __r.json());
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
    public Admin_list_users(token: string): Promise<Response<UserInfo[]>> {
 | 
			
		||||
        const __msg = this.__create_msg('Admin', 'list_users', {token});
 | 
			
		||||
        return fetch(this.url, {
 | 
			
		||||
            method: 'POST',
 | 
			
		||||
            body: JSON.stringify(__msg)
 | 
			
		||||
        }).then((__r) => __r.json());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public Admin_delete_user(token: string, user: number): Promise<(string|null)> {
 | 
			
		||||
        const __msg = this.__create_msg('Admin', 'delete_user', {token,user});
 | 
			
		||||
        return fetch(this.url, {
 | 
			
		||||
            method: 'POST',
 | 
			
		||||
            body: JSON.stringify(__msg)
 | 
			
		||||
        }).then((__r) => __r.json());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public Admin_logout(token: string, user: number): Promise<(string|null)> {
 | 
			
		||||
        const __msg = this.__create_msg('Admin', 'logout', {token,user});
 | 
			
		||||
        return fetch(this.url, {
 | 
			
		||||
            method: 'POST',
 | 
			
		||||
            body: JSON.stringify(__msg)
 | 
			
		||||
        }).then((__r) => __r.json());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public Admin_disable_tfa(token: string, user: number): Promise<(string|null)> {
 | 
			
		||||
        const __msg = this.__create_msg('Admin', 'disable_tfa', {token,user});
 | 
			
		||||
        return fetch(this.url, {
 | 
			
		||||
            method: 'POST',
 | 
			
		||||
            body: JSON.stringify(__msg)
 | 
			
		||||
        }).then((__r) => __r.json());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public Admin_set_admin(token: string, user: number, admin: boolean): Promise<(string|null)> {
 | 
			
		||||
        const __msg = this.__create_msg('Admin', 'set_admin', {token,user,admin});
 | 
			
		||||
        return fetch(this.url, {
 | 
			
		||||
            method: 'POST',
 | 
			
		||||
            body: JSON.stringify(__msg)
 | 
			
		||||
        }).then((__r) => __r.json());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public Admin_set_enabled(token: string, user: number, enabled: boolean): Promise<(string|null)> {
 | 
			
		||||
        const __msg = this.__create_msg('Admin', 'set_enabled', {token,user,enabled});
 | 
			
		||||
        return fetch(this.url, {
 | 
			
		||||
            method: 'POST',
 | 
			
		||||
            body: JSON.stringify(__msg)
 | 
			
		||||
        }).then((__r) => __r.json());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public Admin_sudo(token: string, user: number): Promise<(string|null)> {
 | 
			
		||||
        const __msg = this.__create_msg('Admin', 'sudo', {token,user});
 | 
			
		||||
        return fetch(this.url, {
 | 
			
		||||
            method: 'POST',
 | 
			
		||||
            body: JSON.stringify(__msg)
 | 
			
		||||
        }).then((__r) => __r.json());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public Admin_unsudo(token: string): Promise<(string|null)> {
 | 
			
		||||
        const __msg = this.__create_msg('Admin', 'unsudo', {token});
 | 
			
		||||
        return fetch(this.url, {
 | 
			
		||||
            method: 'POST',
 | 
			
		||||
            body: JSON.stringify(__msg)
 | 
			
		||||
        }).then((__r) => __r.json());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public Admin_shutdown(token: string): Promise<(string|null)> {
 | 
			
		||||
        const __msg = this.__create_msg('Admin', 'shutdown', {token});
 | 
			
		||||
        return fetch(this.url, {
 | 
			
		||||
            method: 'POST',
 | 
			
		||||
            body: JSON.stringify(__msg)
 | 
			
		||||
        }).then((__r) => __r.json());
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
    public FS_get_node(token: string, node: number): Promise<Response<Node>> {
 | 
			
		||||
        const __msg = this.__create_msg('FS', 'get_node', {token,node});
 | 
			
		||||
        return fetch(this.url, {
 | 
			
		||||
            method: 'POST',
 | 
			
		||||
            body: JSON.stringify(__msg)
 | 
			
		||||
        }).then((__r) => __r.json());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public FS_get_path(token: string, node: number): Promise<Response<PathSegment[]>> {
 | 
			
		||||
        const __msg = this.__create_msg('FS', 'get_path', {token,node});
 | 
			
		||||
        return fetch(this.url, {
 | 
			
		||||
            method: 'POST',
 | 
			
		||||
            body: JSON.stringify(__msg)
 | 
			
		||||
        }).then((__r) => __r.json());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public FS_get_nodes_size(token: string, nodes: number[]): Promise<Response<number>> {
 | 
			
		||||
        const __msg = this.__create_msg('FS', 'get_nodes_size', {token,nodes});
 | 
			
		||||
        return fetch(this.url, {
 | 
			
		||||
            method: 'POST',
 | 
			
		||||
            body: JSON.stringify(__msg)
 | 
			
		||||
        }).then((__r) => __r.json());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public FS_create_node(token: string, file: boolean, parent: number, name: string): Promise<Response<CreateNodeInfo>> {
 | 
			
		||||
        const __msg = this.__create_msg('FS', 'create_node', {token,file,parent,name});
 | 
			
		||||
        return fetch(this.url, {
 | 
			
		||||
            method: 'POST',
 | 
			
		||||
            body: JSON.stringify(__msg)
 | 
			
		||||
        }).then((__r) => __r.json());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public FS_move_nodes(token: string, nodes: number[], parent: number): Promise<(string|null)> {
 | 
			
		||||
        const __msg = this.__create_msg('FS', 'move_nodes', {token,nodes,parent});
 | 
			
		||||
        return fetch(this.url, {
 | 
			
		||||
            method: 'POST',
 | 
			
		||||
            body: JSON.stringify(__msg)
 | 
			
		||||
        }).then((__r) => __r.json());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public FS_delete_nodes(token: string, nodes: number[], __cbk: (v: string|null) => void) {
 | 
			
		||||
        const __msg = this.__create_msg('FS', 'delete_nodes', {token,nodes});
 | 
			
		||||
        fetchEventSource(this.url, {
 | 
			
		||||
            method: 'POST',
 | 
			
		||||
            body: JSON.stringify(__msg),
 | 
			
		||||
            onmessage: __e => __cbk(JSON.parse(__e.data)),
 | 
			
		||||
            onerror: __e => {throw __e;},
 | 
			
		||||
            onclose: () => __cbk(null)
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public FS_download_preview(token: string, node: number): Promise<Response<string>> {
 | 
			
		||||
        const __msg = this.__create_msg('FS', 'download_preview', {token,node});
 | 
			
		||||
        return fetch(this.url, {
 | 
			
		||||
            method: 'POST',
 | 
			
		||||
            body: JSON.stringify(__msg)
 | 
			
		||||
        }).then((__r) => __r.json());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public FS_get_mime(token: string, node: number): Promise<Response<string>> {
 | 
			
		||||
        const __msg = this.__create_msg('FS', 'get_mime', {token,node});
 | 
			
		||||
        return fetch(this.url, {
 | 
			
		||||
            method: 'POST',
 | 
			
		||||
            body: JSON.stringify(__msg)
 | 
			
		||||
        }).then((__r) => __r.json());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										82
									
								
								frontend/src/api/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										82
									
								
								frontend/src/api/index.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,82 @@
 | 
			
		||||
import type {paths, components} from './schema';
 | 
			
		||||
import createClient from 'openapi-fetch';
 | 
			
		||||
import {fetchEventSource} from '@microsoft/fetch-event-source';
 | 
			
		||||
 | 
			
		||||
const client = createClient<paths>();
 | 
			
		||||
client.use({
 | 
			
		||||
    onRequest({ schemaPath, request }) {
 | 
			
		||||
        if (schemaPath.startsWith('/api/public') || rpc.token == '')
 | 
			
		||||
            return;
 | 
			
		||||
 | 
			
		||||
        request.headers.set('Authorization', `Bearer ${rpc.token}`);
 | 
			
		||||
        return request;
 | 
			
		||||
    }
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
export type Session = components['schemas']['de.mattv.fileserver.Response$Session'];
 | 
			
		||||
export type Node = components['schemas']['de.mattv.fileserver.Response$Node'];
 | 
			
		||||
export type PathSegment = components['schemas']['de.mattv.fileserver.Response$PathSegment'];
 | 
			
		||||
export type CreateNodeInfo = components['schemas']['de.mattv.fileserver.Response$CreateNodeInfo'];
 | 
			
		||||
export type UserInfo = components['schemas']['de.mattv.fileserver.Response$UserInfo'];
 | 
			
		||||
 | 
			
		||||
export const rpc = {
 | 
			
		||||
    token: '',
 | 
			
		||||
 | 
			
		||||
    signup: (username: string, password: string) =>
 | 
			
		||||
        client.POST('/api/public/auth/signup', { body: { username, password } }).then(v => v.data),
 | 
			
		||||
    login: (username: string, password: string, otp?: string) =>
 | 
			
		||||
        client.POST('/api/public/auth/login', { body: { username, password, otp } }).then(v => v.data),
 | 
			
		||||
 | 
			
		||||
    send_recovery_key: (username: string) =>
 | 
			
		||||
        client.POST('/api/public/auth/send_recovery_key', { body: username }).then(v => v.data),
 | 
			
		||||
    reset_password: (key: string, password: string) =>
 | 
			
		||||
        client.POST('/api/public/auth/reset_password', { body: { key, password } }).then(v => v.data),
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    change_password: (oldPassword: string, newPassword: string) =>
 | 
			
		||||
        client.POST('/api/user/auth/change_password', { body: { oldPassword, newPassword } }).then(v => v.data),
 | 
			
		||||
 | 
			
		||||
    logout: () => client.POST('/api/user/auth/logout').then(v => v.data),
 | 
			
		||||
    logoutAll: () => client.POST('/api/user/auth/logout_all').then(v => v.data),
 | 
			
		||||
    deleteAccount: () => client.POST('/api/user/auth/delete').then(v => v.data),
 | 
			
		||||
    sessionInfo: () => client.POST('/api/user/session').then(v => v.data),
 | 
			
		||||
 | 
			
		||||
    tfaSetupMail: () => client.POST('/api/user/tfa/setup_mail').then(v => v.data),
 | 
			
		||||
    tfaSetupTotp: () => client.POST('/api/user/tfa/setup_totp').then(v => v.data),
 | 
			
		||||
    tfaComplete: (code: string) => client.POST('/api/user/tfa/complete', { body: code }).then(v => v.data),
 | 
			
		||||
    tfaDisable: () => client.POST('/api/user/tfa/disable').then(v => v.data),
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    getNode: (node: number) => client.POST('/api/user/fs/node', { body: node }).then(v => v.data),
 | 
			
		||||
    getPath: (node: number) => client.POST('/api/user/fs/path', { body: node }).then(v => v.data),
 | 
			
		||||
    getNodesSize: (nodes: number[]) => client.POST('/api/user/fs/size', { body: nodes }).then(v => v.data),
 | 
			
		||||
    getMime: (node: number) => client.POST('/api/user/fs/mime', { body: node }).then(v => v.data),
 | 
			
		||||
    downloadPreview: (node: number) => client.POST('/api/user/fs/preview', { body: node }).then(v => v.data),
 | 
			
		||||
 | 
			
		||||
    createNode: (name: string, parent: number, file: boolean) =>
 | 
			
		||||
        client.POST('/api/user/fs/create', { body: { name, parent, file } }).then(v => v.data),
 | 
			
		||||
 | 
			
		||||
    deleteNodes: (nodes: number[], cbk: (v: string|null) => void) => fetchEventSource('/api/user/fs/delete', {
 | 
			
		||||
        method: 'POST',
 | 
			
		||||
        body: JSON.stringify(nodes),
 | 
			
		||||
        headers: {
 | 
			
		||||
            'Authorization': 'Bearer ' + rpc.token,
 | 
			
		||||
            'Content-Type': 'application/json'
 | 
			
		||||
        },
 | 
			
		||||
        onmessage: v => cbk(v.data),
 | 
			
		||||
        onerror: e => { throw e; },
 | 
			
		||||
        onclose: () => cbk(null)
 | 
			
		||||
    }),
 | 
			
		||||
 | 
			
		||||
    admin: {
 | 
			
		||||
        listUsers: () => client.POST('/api/admin/users').then(v => v.data),
 | 
			
		||||
        setEnabled: (id: number, state: boolean) => client.POST('/api/admin/user/set_enabled', { body: { id, state } }).then(v => v.data),
 | 
			
		||||
        setAdmin: (id: number, state: boolean) => client.POST('/api/admin/user/set_admin', { body: { id, state } }).then(v => v.data),
 | 
			
		||||
        sudo: (id: number) => client.POST('/api/admin/user/sudo', { body: id }).then(v => v.data),
 | 
			
		||||
        logout: (id: number) => client.POST('/api/admin/user/logout', { body: id }).then(v => v.data),
 | 
			
		||||
        disableTfa: (id: number) => client.POST('/api/admin/user/disable_tfa', { body: id }).then(v => v.data),
 | 
			
		||||
        deleteUser: (id: number) => client.POST('/api/admin/user/delete', { body: id }).then(v => v.data),
 | 
			
		||||
        unSudo: () => client.POST('/api/admin/un_sudo').then(v => v.data),
 | 
			
		||||
        shutdown: () => client.POST('/api/admin/shutdown').then(v => v.data),
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										1197
									
								
								frontend/src/api/schema.d.ts
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										1197
									
								
								frontend/src/api/schema.d.ts
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							@@ -1,5 +1,5 @@
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
    import {rpc, show_working, token} from '../store';
 | 
			
		||||
    import {rpc, show_working} from '../store';
 | 
			
		||||
    import {Button, ButtonGroup, Modal} from 'flowbite-svelte';
 | 
			
		||||
    import {afterUpdate, createEventDispatcher} from 'svelte';
 | 
			
		||||
 | 
			
		||||
@@ -18,7 +18,7 @@
 | 
			
		||||
        show_working.set(true);
 | 
			
		||||
 | 
			
		||||
        await new Promise<void>((resolve) => {
 | 
			
		||||
            rpc.FS_delete_nodes($token ?? '', nodes, (v) => {
 | 
			
		||||
            rpc.deleteNodes(nodes, v => {
 | 
			
		||||
                if (v == null)
 | 
			
		||||
                    resolve();
 | 
			
		||||
                else {
 | 
			
		||||
 
 | 
			
		||||
@@ -23,7 +23,7 @@
 | 
			
		||||
    } from 'flowbite-svelte';
 | 
			
		||||
    import {filesize} from 'filesize';
 | 
			
		||||
    import {Folder, FolderParent, DocumentBlank, CaretLeft, FolderAdd} from '../icons';
 | 
			
		||||
    import {api, download, token, rpc, workingWrapperR, error_banner} from '../store';
 | 
			
		||||
    import {api, download, rpc, workingWrapperR, error_banner} from '../store';
 | 
			
		||||
    import LinkButton from './LinkButton.svelte';
 | 
			
		||||
    import DeleteModal from './DeleteModal.svelte';
 | 
			
		||||
    import A from './A.svelte';
 | 
			
		||||
@@ -57,18 +57,18 @@
 | 
			
		||||
            return error_banner.set('Folder name can\'t be empty');
 | 
			
		||||
 | 
			
		||||
        show_new_folder = false;
 | 
			
		||||
        const resp = await workingWrapperR<api.CreateNodeInfo>(() => rpc.FS_create_node($token ?? '', false, node.id, new_folder_name));
 | 
			
		||||
        if (resp && resp.file)
 | 
			
		||||
        const resp = await workingWrapperR<api.CreateNodeInfo>(() => rpc.createNode(new_folder_name, node.id, false));
 | 
			
		||||
        if (resp && resp.isFile)
 | 
			
		||||
            return error_banner.set('Folder already exists as file');
 | 
			
		||||
 | 
			
		||||
        dispatch('reload_node');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async function getPreview(node: number) {
 | 
			
		||||
        const resp = await rpc.FS_download_preview($token ?? '', node);
 | 
			
		||||
        if (resp.o == null)
 | 
			
		||||
        const resp = await rpc.downloadPreview(node);
 | 
			
		||||
        if (!resp)
 | 
			
		||||
            return;
 | 
			
		||||
        previews[node] = 'data:image/png;base64,' + resp.o;
 | 
			
		||||
        previews[node] = 'data:image/png;base64,' + resp;
 | 
			
		||||
        previews = previews;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -93,11 +93,11 @@
 | 
			
		||||
    const selectFolders = () => selected = dirs.map(v => v.id);
 | 
			
		||||
    const selectFiles = () => selected = files.map(v => v.id);
 | 
			
		||||
    const selectNone = () => selected = [];
 | 
			
		||||
    const downloadSelected = () => download($token ?? '', nodes.filter(v => selected.includes(v.id)));
 | 
			
		||||
    const downloadSelected = () => download(nodes.filter(v => selected.includes(v.id)));
 | 
			
		||||
    const deleteSelected = () => del(selected);
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    const onCtxDownload = () => download($token ?? '', [ctx_node]);
 | 
			
		||||
    const onCtxDownload = () => download([ctx_node]);
 | 
			
		||||
 | 
			
		||||
    let del: (nodes: number[]) => Promise<void>;
 | 
			
		||||
    const onCtxDelete = () => del([ctx_node.id]);
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,7 @@
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
    import {Button, Spinner} from 'flowbite-svelte';
 | 
			
		||||
    import {Download} from '../icons';
 | 
			
		||||
    import {api, download, rpc, token, workingWrapperR} from '../store';
 | 
			
		||||
    import {api, download, rpc, token, workingWrapper} from '../store';
 | 
			
		||||
    import {onDestroy} from 'svelte';
 | 
			
		||||
 | 
			
		||||
    export let node: api.Node;
 | 
			
		||||
@@ -11,7 +11,7 @@
 | 
			
		||||
 | 
			
		||||
    let mime: string|null;
 | 
			
		||||
    let image: boolean, video: boolean, audio: boolean, pdf: boolean, can_display: boolean;
 | 
			
		||||
    $: workingWrapperR<string>(() => rpc.FS_get_mime($token ?? '', node.id)).then(v => mime = v);
 | 
			
		||||
    $: workingWrapper(() => rpc.getMime(node.id)).then(v => mime = v || null);
 | 
			
		||||
    $: image = mime?.startsWith('image/') ?? false;
 | 
			
		||||
    $: video = mime?.startsWith('video/') ?? false;
 | 
			
		||||
    $: audio = mime?.startsWith('audio/') ?? false;
 | 
			
		||||
@@ -22,7 +22,7 @@
 | 
			
		||||
        loading = true;
 | 
			
		||||
        if (src.startsWith('blob'))
 | 
			
		||||
            URL.revokeObjectURL(src);
 | 
			
		||||
        const resp = await fetch('/download', {
 | 
			
		||||
        const resp = await fetch('/api/public/download', {
 | 
			
		||||
            method: 'POST',
 | 
			
		||||
            headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
 | 
			
		||||
            body: `token=${$token ?? ''}&node=${node.id}`
 | 
			
		||||
@@ -38,7 +38,7 @@
 | 
			
		||||
    onDestroy(() => { if (src.startsWith('blob')) URL.revokeObjectURL(src); });
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<Button class="w-full mb-6" on:click={() => download($token ?? '', [node])}><Download />Download</Button>
 | 
			
		||||
<Button class="w-full mb-6" on:click={() => download([node])}><Download />Download</Button>
 | 
			
		||||
{#if can_display && !loading && src === ''}
 | 
			
		||||
    <Button class="w-full" outline on:click={load}>Load</Button>
 | 
			
		||||
{:else if loading}
 | 
			
		||||
 
 | 
			
		||||
@@ -49,9 +49,8 @@
 | 
			
		||||
                if (file.current == file.total)
 | 
			
		||||
                    resolve(null);
 | 
			
		||||
            };
 | 
			
		||||
            xhr.open('POST', '/upload', true);
 | 
			
		||||
            xhr.setRequestHeader('X-Node', file.id.toString());
 | 
			
		||||
            xhr.setRequestHeader('X-Token', $token ?? '');
 | 
			
		||||
            xhr.open('POST', `/api/user/upload/${file.id}`, true);
 | 
			
		||||
            xhr.setRequestHeader('Authorization', 'Bearer ' + ($token ?? ''));
 | 
			
		||||
            xhr.send(file.file);
 | 
			
		||||
        });
 | 
			
		||||
        current += file.total - load_progress;
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,5 @@
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
    import {api, rpc, session, token, workingWrapperO, workingWrapperR} from '../store';
 | 
			
		||||
    import {api, rpc, session, token, workingWrapper} from '../store';
 | 
			
		||||
    import {Checkbox, Table, TableBody, TableBodyCell, TableBodyRow, TableHead, TableHeadCell} from 'flowbite-svelte';
 | 
			
		||||
    import {Checkmark, Error} from '../icons';
 | 
			
		||||
    import LinkButton from '../components/LinkButton.svelte';
 | 
			
		||||
@@ -8,46 +8,44 @@
 | 
			
		||||
    let users: api.UserInfo[] = [];
 | 
			
		||||
 | 
			
		||||
    async function fetchUsers() {
 | 
			
		||||
        const resp = await workingWrapperR<api.UserInfo[]>(() => rpc.Admin_list_users($token ?? ''));
 | 
			
		||||
        if (resp != null)
 | 
			
		||||
            users = resp;
 | 
			
		||||
        const resp = await workingWrapper(() => rpc.admin.listUsers());
 | 
			
		||||
        users = resp || [];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async function changeEnabled(user: number, target: boolean) {
 | 
			
		||||
        await workingWrapperO(() => rpc.Admin_set_enabled($token ?? '', user, target));
 | 
			
		||||
        await workingWrapper(() => rpc.admin.setEnabled(user, target));
 | 
			
		||||
        await fetchUsers();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async function changeAdmin(user: number, target: boolean) {
 | 
			
		||||
        await workingWrapperO(() => rpc.Admin_set_admin($token ?? '', user, target));
 | 
			
		||||
        await workingWrapper(() => rpc.admin.setAdmin(user, target));
 | 
			
		||||
        await fetchUsers();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async function sudo(user: number) {
 | 
			
		||||
        if (await workingWrapperO(() => rpc.Admin_sudo($token ?? '', user))) {
 | 
			
		||||
            await session.update($token);
 | 
			
		||||
            await replace('/view/0');
 | 
			
		||||
        }
 | 
			
		||||
        await workingWrapper(() => rpc.admin.sudo(user))
 | 
			
		||||
        await session.update('');
 | 
			
		||||
        await replace('/view/0');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async function logout(user: number) {
 | 
			
		||||
        await workingWrapperO(() => rpc.Admin_logout($token ?? '', user));
 | 
			
		||||
        await workingWrapper(() => rpc.admin.logout(user));
 | 
			
		||||
        await fetchUsers();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async function removeTfa(user: number) {
 | 
			
		||||
        await workingWrapperO(() => rpc.Admin_disable_tfa($token ?? '', user));
 | 
			
		||||
        await workingWrapper(() => rpc.admin.disableTfa(user));
 | 
			
		||||
        await fetchUsers();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async function deleteUser(user: number) {
 | 
			
		||||
        await workingWrapperO(() => rpc.Admin_delete_user($token ?? '', user));
 | 
			
		||||
        await workingWrapper(() => rpc.admin.deleteUser(user));
 | 
			
		||||
        await fetchUsers();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async function shutdown() {
 | 
			
		||||
        if (confirm('Do you really want to shutdown the server?')) {
 | 
			
		||||
            await rpc.Admin_shutdown($token ?? '');
 | 
			
		||||
            await rpc.admin.shutdown();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -66,7 +64,7 @@
 | 
			
		||||
        {#each users as user (user.id)}
 | 
			
		||||
            <TableBodyRow>
 | 
			
		||||
                <TableBodyCell>{user.name}</TableBodyCell>
 | 
			
		||||
                <TableBodyCell>{#if user.tfa}<Checkmark/>{:else}<Error/>{/if}</TableBodyCell>
 | 
			
		||||
                <TableBodyCell>{#if user.tfaEnabled}<Checkmark/>{:else}<Error/>{/if}</TableBodyCell>
 | 
			
		||||
                <TableBodyCell>
 | 
			
		||||
                    <Checkbox checked={user.enabled} on:change={changeEnabled.bind(null, user.id, !user.enabled)}></Checkbox>
 | 
			
		||||
                </TableBodyCell>
 | 
			
		||||
@@ -76,7 +74,7 @@
 | 
			
		||||
                <TableBodyCell class="flex">
 | 
			
		||||
                    <LinkButton class="flex-auto" on:click={sudo.bind(null, user.id)}>Sudo</LinkButton>
 | 
			
		||||
                    <LinkButton class="flex-auto" on:click={logout.bind(null, user.id)}>Logout</LinkButton>
 | 
			
		||||
                    {#if user.tfa}<LinkButton class="flex-auto" color="amber" on:click={removeTfa.bind(null, user.id)}>Remove tfa</LinkButton>{/if}
 | 
			
		||||
                    {#if user.tfaEnabled}<LinkButton class="flex-auto" color="amber" on:click={removeTfa.bind(null, user.id)}>Remove tfa</LinkButton>{/if}
 | 
			
		||||
                    <LinkButton class="flex-auto" color="red" on:click={deleteUser.bind(null, user.id)}>Delete</LinkButton>
 | 
			
		||||
                </TableBodyCell>
 | 
			
		||||
            </TableBodyRow>
 | 
			
		||||
 
 | 
			
		||||
@@ -1,21 +1,22 @@
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
    import {Button, ButtonGroup, Card, Input, InputAddon} from 'flowbite-svelte';
 | 
			
		||||
    import {Email, OTP, Password} from '../icons';
 | 
			
		||||
    import {rpc, token, workingWrapperR, api} from '../store';
 | 
			
		||||
    import {rpc, token, workingWrapperR} from '../store';
 | 
			
		||||
    import {replace} from 'svelte-spa-router';
 | 
			
		||||
 | 
			
		||||
    let ask_tfa = false;
 | 
			
		||||
    let username = '', password = '', tfa = '';
 | 
			
		||||
 | 
			
		||||
    async function login() {
 | 
			
		||||
        const resp = await workingWrapperR<api.LoginResponse>(() => rpc.Auth_login(username, password, ask_tfa ? tfa : null));
 | 
			
		||||
        const resp = await workingWrapperR(() => rpc.login(username, password, ask_tfa ? tfa : undefined));
 | 
			
		||||
        if (!resp) return;
 | 
			
		||||
        if (resp.otp_needed) {
 | 
			
		||||
        if (resp.otpNeeded) {
 | 
			
		||||
            ask_tfa = true;
 | 
			
		||||
            return;
 | 
			
		||||
        } else if (resp.token) {
 | 
			
		||||
            token.set(resp.token);
 | 
			
		||||
            await replace('/view/0');
 | 
			
		||||
        }
 | 
			
		||||
        token.set(resp.token);
 | 
			
		||||
        await replace('/view/0');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function keyUp(e: KeyboardEvent) {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,11 +1,11 @@
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
    import {error_banner, rpc, session, token, workingWrapperO} from '../store';
 | 
			
		||||
    import {error_banner, rpc, session, token, workingWrapper, workingWrapperO} from '../store';
 | 
			
		||||
    import {Accordion, AccordionItem, Button, ButtonGroup, Input, InputAddon} from 'flowbite-svelte';
 | 
			
		||||
    import {Password} from '../icons';
 | 
			
		||||
    import {info_banner} from '../store.js';
 | 
			
		||||
 | 
			
		||||
    const s = session.s;
 | 
			
		||||
    const tfa_enabled: boolean = $s?.tfa_enabled ?? false;
 | 
			
		||||
    const tfa_enabled: boolean = $s?.tfaEnabled ?? false;
 | 
			
		||||
    const change_pw_data = {o: '', n: '', n2: ''}
 | 
			
		||||
 | 
			
		||||
    async function changePw() {
 | 
			
		||||
@@ -15,28 +15,29 @@
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const resp = await workingWrapperO(() => rpc.Auth_change_password($token ?? '', old, password));
 | 
			
		||||
        const resp = await workingWrapperO(() => rpc.change_password(old, password));
 | 
			
		||||
        if (resp) {
 | 
			
		||||
            info_banner.set('Changed password');
 | 
			
		||||
            change_pw_data.o = '';
 | 
			
		||||
            change_pw_data.n = '';
 | 
			
		||||
            change_pw_data.n2 = '';
 | 
			
		||||
            token.set(null);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async function disableTfa() {
 | 
			
		||||
        await workingWrapperO(() => rpc.Auth_tfa_disable($token ?? ''));
 | 
			
		||||
        await workingWrapper(() => rpc.tfaDisable());
 | 
			
		||||
        token.set(null);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async function logoutAll() {
 | 
			
		||||
        await workingWrapperO(() => rpc.Auth_logout_all($token ?? ''));
 | 
			
		||||
        await workingWrapper(() => rpc.logoutAll());
 | 
			
		||||
        token.set(null);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async function deleteAccount() {
 | 
			
		||||
        if (confirm("Do your really want to delete your account?")) {
 | 
			
		||||
            await workingWrapperO(() => rpc.Auth_delete_user($token ?? ''));
 | 
			
		||||
            await workingWrapper(() => rpc.deleteAccount());
 | 
			
		||||
            token.set(null);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -8,7 +8,7 @@
 | 
			
		||||
    let username = '', key = '', password = '', password2 = '';
 | 
			
		||||
 | 
			
		||||
    async function sendKey() {
 | 
			
		||||
        await workingWrapper(() => rpc.Auth_send_recovery_key(username));
 | 
			
		||||
        await workingWrapper(() => rpc.send_recovery_key(username));
 | 
			
		||||
        info_banner.set('A message has been sent');
 | 
			
		||||
        enter_key = true;
 | 
			
		||||
    }
 | 
			
		||||
@@ -19,7 +19,7 @@
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (await workingWrapperO(() => rpc.Auth_reset_password(key, password)))
 | 
			
		||||
        if (await workingWrapperO(() => rpc.reset_password(key, password)))
 | 
			
		||||
            await replace('/login');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -17,7 +17,7 @@
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const resp = await workingWrapperO(() => rpc.Auth_signup(username, password));
 | 
			
		||||
        const resp = await workingWrapperO(() => rpc.signup(username, password));
 | 
			
		||||
 | 
			
		||||
        if (resp) {
 | 
			
		||||
            info_banner.set('Account created, please wait till an administrator approves it');
 | 
			
		||||
 
 | 
			
		||||
@@ -3,7 +3,6 @@
 | 
			
		||||
    import {OTP} from '../icons';
 | 
			
		||||
    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;
 | 
			
		||||
 | 
			
		||||
@@ -14,13 +13,13 @@
 | 
			
		||||
 | 
			
		||||
    async function startSetup(mail: boolean) {
 | 
			
		||||
        if (mail) {
 | 
			
		||||
            const resp = await workingWrapperO(() => rpc.Auth_tfa_setup_mail($token ?? ''));
 | 
			
		||||
            const resp = await workingWrapperO(() => rpc.tfaSetupMail());
 | 
			
		||||
            if (resp) {
 | 
			
		||||
                secret = null;
 | 
			
		||||
                step = 2;
 | 
			
		||||
            }
 | 
			
		||||
        } else {
 | 
			
		||||
            const resp = await workingWrapperR<string>(() => rpc.Auth_tfa_setup_totp($token ?? ''));
 | 
			
		||||
            const resp = await workingWrapperR<string>(() => rpc.tfaSetupTotp());
 | 
			
		||||
            if (resp != null) {
 | 
			
		||||
                secret = resp.replaceAll('=', '');
 | 
			
		||||
                secret_qr_code = new QRCode({
 | 
			
		||||
@@ -34,7 +33,7 @@
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async function completeSetup() {
 | 
			
		||||
        if (await workingWrapperO(() => rpc.Auth_tfa_complete($token ?? '', code))) {
 | 
			
		||||
        if (await workingWrapperO(() => rpc.tfaComplete(code))) {
 | 
			
		||||
            info_banner.set("Successfully set up two factor authentication");
 | 
			
		||||
            token.set(null);
 | 
			
		||||
        }
 | 
			
		||||
 
 | 
			
		||||
@@ -2,7 +2,7 @@
 | 
			
		||||
    import {Breadcrumb, Dropzone, Modal, Progressbar} from 'flowbite-svelte';
 | 
			
		||||
    import {writable} from 'svelte/store';
 | 
			
		||||
    import {CloudUpload} from '../icons';
 | 
			
		||||
    import {api, rpc, token, type UploadFile, workingWrapperR} from '../store';
 | 
			
		||||
    import {api, rpc, token, type UploadFile, workingWrapper, workingWrapperR} from '../store';
 | 
			
		||||
    import DirViewer from '../components/DirViewer.svelte';
 | 
			
		||||
    import UploadModal from '../components/UploadModal.svelte';
 | 
			
		||||
    import FileViewer from '../components/FileViewer.svelte';
 | 
			
		||||
@@ -25,10 +25,10 @@
 | 
			
		||||
 | 
			
		||||
    const data = writable<Data>({node: null, segments: []});
 | 
			
		||||
    async function updateData(id: number) {
 | 
			
		||||
        let node = await workingWrapperR<api.Node>(() => rpc.FS_get_node($token ?? '', id));
 | 
			
		||||
        let node = await workingWrapper(() => rpc.getNode(id));
 | 
			
		||||
        if (!node)
 | 
			
		||||
            return;
 | 
			
		||||
        let segments = await workingWrapperR<api.PathSegment[]>(() => rpc.FS_get_path($token ?? '', id));
 | 
			
		||||
        let segments = await workingWrapper(() => rpc.getPath(id));
 | 
			
		||||
        if (!segments)
 | 
			
		||||
            return;
 | 
			
		||||
        data.set({node: node as Data['node'], segments });
 | 
			
		||||
@@ -50,9 +50,9 @@
 | 
			
		||||
                return [];
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        const resp = await workingWrapperR<api.CreateNodeInfo>(() => rpc.FS_create_node($token ?? '', false, parent, entry.name));
 | 
			
		||||
        const resp = await workingWrapperR<api.CreateNodeInfo>(() => rpc.createNode(entry.name, parent, false));
 | 
			
		||||
        if (!resp) return [];
 | 
			
		||||
        if (resp.file) return [];
 | 
			
		||||
        if (resp.isFile) return [];
 | 
			
		||||
        const reader = (entry as FileSystemDirectoryEntry).createReader();
 | 
			
		||||
        const files: UploadFile[] = [];
 | 
			
		||||
        const name = parent_name + entry.name + '/';
 | 
			
		||||
@@ -109,8 +109,8 @@
 | 
			
		||||
        upload_progress_data.current = 0;
 | 
			
		||||
        const upload_files: UploadFile[] = [];
 | 
			
		||||
        for (const file of files) {
 | 
			
		||||
            const resp = await workingWrapperR<api.CreateNodeInfo>(() => rpc.FS_create_node($token ?? '', true, file.id, file.name));
 | 
			
		||||
            if (resp && resp.file)
 | 
			
		||||
            const resp = await workingWrapperR<api.CreateNodeInfo>(() => rpc.createNode(file.name, file.id, true));
 | 
			
		||||
            if (resp && resp.isFile)
 | 
			
		||||
                upload_files.push({ ...file, id: resp.id, overwrite: resp.exists });
 | 
			
		||||
            upload_progress_data.current++;
 | 
			
		||||
        }
 | 
			
		||||
 
 | 
			
		||||
@@ -1,8 +1,9 @@
 | 
			
		||||
import {MRPCConnector, type Session, type Response} from './api';
 | 
			
		||||
import {type Session, rpc} from './api';
 | 
			
		||||
import {type Writable, writable} from 'svelte/store';
 | 
			
		||||
import {filesize} from 'filesize';
 | 
			
		||||
 | 
			
		||||
export * as api from './api';
 | 
			
		||||
export {rpc} from './api';
 | 
			
		||||
 | 
			
		||||
export interface UploadFile {
 | 
			
		||||
    id: number,
 | 
			
		||||
@@ -16,8 +17,6 @@ export const show_working = writable<boolean>(false);
 | 
			
		||||
export const info_banner = writable<string>('');
 | 
			
		||||
export const error_banner = writable<string>('');
 | 
			
		||||
 | 
			
		||||
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),
 | 
			
		||||
@@ -26,14 +25,15 @@ export const session: { s: Writable<Session|null>, update: (token: string|null)
 | 
			
		||||
            session.s.set(null);
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        const s = await rpc.Auth_session_info(t)
 | 
			
		||||
        if (s.e)
 | 
			
		||||
        const s = await rpc.sessionInfo();
 | 
			
		||||
        if (!s)
 | 
			
		||||
            token.set(null);
 | 
			
		||||
        else
 | 
			
		||||
            session.s.set(s.o);
 | 
			
		||||
            session.s.set(s);
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
token.subscribe((t) => session.update(t));
 | 
			
		||||
token.subscribe(t => rpc.token = t ?? '');
 | 
			
		||||
token.subscribe(t => session.update(t));
 | 
			
		||||
 | 
			
		||||
token.subscribe(v => {
 | 
			
		||||
    if (v == null)
 | 
			
		||||
@@ -44,49 +44,50 @@ token.subscribe(v => {
 | 
			
		||||
 | 
			
		||||
export async function workingWrapper<T>(fn: () => Promise<T>): Promise<T|null> {
 | 
			
		||||
    let r = null;
 | 
			
		||||
    info_banner.set('');
 | 
			
		||||
    error_banner.set('');
 | 
			
		||||
    show_working.set(true);
 | 
			
		||||
    try {
 | 
			
		||||
        r = await fn();
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
        error_banner.set(`Error while making request: ${e}`);
 | 
			
		||||
        token.set(null);
 | 
			
		||||
    }
 | 
			
		||||
    show_working.set(false);
 | 
			
		||||
    return r;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export async function workingWrapperO(fn: () => Promise<string|null>): Promise<boolean> {
 | 
			
		||||
export async function workingWrapperO(fn: () => Promise<string|undefined>): Promise<boolean> {
 | 
			
		||||
    const resp = await workingWrapper(fn);
 | 
			
		||||
    if (resp)
 | 
			
		||||
        error_banner.set(resp);
 | 
			
		||||
    if (resp === 'Unauthorized')
 | 
			
		||||
        token.set(null);
 | 
			
		||||
    return resp == null;
 | 
			
		||||
    return resp == undefined;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export async function workingWrapperR<T>(fn: () => Promise<Response<T>>): Promise<T|null> {
 | 
			
		||||
export async function workingWrapperR<T>(fn: () => Promise<{
 | 
			
		||||
    e?: string,
 | 
			
		||||
    o?: T
 | 
			
		||||
} | undefined>): Promise<T|null> {
 | 
			
		||||
    const resp = await workingWrapper(fn);
 | 
			
		||||
    if (!resp)
 | 
			
		||||
        return null;
 | 
			
		||||
    if (resp.e === 'Unauthorized')
 | 
			
		||||
        token.set(null);
 | 
			
		||||
    else if (resp.e != null)
 | 
			
		||||
    if (resp.e != null)
 | 
			
		||||
        error_banner.set(resp.e);
 | 
			
		||||
    return resp.o;
 | 
			
		||||
    return resp.o as unknown as T;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export async function download<T extends {id:number, file:boolean}>(token: string, nodes: T[]) {
 | 
			
		||||
export async function download<T extends {id:number, file:boolean}>(nodes: T[]) {
 | 
			
		||||
    const form = document.createElement('form');
 | 
			
		||||
    form.method = 'POST';
 | 
			
		||||
    form.target = '_blank';
 | 
			
		||||
    form.innerHTML = `<input type="hidden" name="token" value="${token}">`;
 | 
			
		||||
    if (nodes.length == 1 && nodes[0].file) {
 | 
			
		||||
        form.action = '/download';
 | 
			
		||||
        form.innerHTML += `<input type="hidden" name="node" value="${nodes[0].id}}">`;
 | 
			
		||||
    form.innerHTML = `<input type="hidden" name="token" value="${rpc.token}">`;
 | 
			
		||||
    if (nodes.length == 1 && nodes[0] && nodes[0].file) {
 | 
			
		||||
        form.action = '/api/public/download';
 | 
			
		||||
        form.innerHTML += `<input type="hidden" name="node" value="${nodes[0].id}">`;
 | 
			
		||||
    } else {
 | 
			
		||||
        form.action = '/download_multi';
 | 
			
		||||
        form.action = '/api/public/download_multi';
 | 
			
		||||
        form.innerHTML += `<input type="hidden" name="nodes" value="${nodes.map(n => n.id).join('.')}">`;
 | 
			
		||||
        const resp = await workingWrapperR<number>(() => rpc.FS_get_nodes_size(token, nodes.map(v => v.id)));
 | 
			
		||||
        const resp = await workingWrapper(() => rpc.getNodesSize(nodes.map(v => v.id)));
 | 
			
		||||
        if (!resp)
 | 
			
		||||
            return;
 | 
			
		||||
        info_banner.set(`Estimated size: ${filesize(resp, {base: 2, standard: 'jedec'})}`);
 | 
			
		||||
 
 | 
			
		||||
@@ -13,7 +13,8 @@
 | 
			
		||||
     */
 | 
			
		||||
    "allowJs": true,
 | 
			
		||||
    "checkJs": true,
 | 
			
		||||
    "isolatedModules": true
 | 
			
		||||
    "isolatedModules": true,
 | 
			
		||||
    "noUncheckedIndexedAccess": true
 | 
			
		||||
  },
 | 
			
		||||
  "include": ["src/**/*.d.ts", "src/**/*.ts", "src/**/*.js", "src/**/*.svelte"],
 | 
			
		||||
  "references": [{ "path": "./tsconfig.node.json" }]
 | 
			
		||||
 
 | 
			
		||||
@@ -4,42 +4,9 @@ import {viteSingleFile} from 'vite-plugin-singlefile';
 | 
			
		||||
import {createHtmlPlugin} from 'vite-plugin-html';
 | 
			
		||||
import purgeCss from 'vite-plugin-tailwind-purgecss';
 | 
			
		||||
import Icons from 'unplugin-icons/vite';
 | 
			
		||||
import {NormalizedInputOptions, PluginContext} from 'rollup';
 | 
			
		||||
import * as fs from 'fs';
 | 
			
		||||
import * as child_process from 'child_process';
 | 
			
		||||
 | 
			
		||||
const response_replacement =
 | 
			
		||||
    'interface ResponseE {\n' +
 | 
			
		||||
    '    e: string,\n' +
 | 
			
		||||
    '    o: null\n' +
 | 
			
		||||
    '}\n' +
 | 
			
		||||
    'interface ResponseO<T> {\n' +
 | 
			
		||||
    '    e: null,\n' +
 | 
			
		||||
    '    o: T\n' +
 | 
			
		||||
    '}\n' +
 | 
			
		||||
    'export type Response<T> = ResponseE | ResponseO<T>;'
 | 
			
		||||
 | 
			
		||||
function checkMrpc(this: PluginContext, _options: NormalizedInputOptions) {
 | 
			
		||||
    const src_ts = fs.statSync('../fileserver.rs').mtimeMs;
 | 
			
		||||
    const update = !fs.existsSync('src/api.ts') || fs.statSync('src/api.ts').mtimeMs <= src_ts;
 | 
			
		||||
    if (!update)
 | 
			
		||||
        return;
 | 
			
		||||
    child_process.spawnSync(
 | 
			
		||||
        '../mrpc',
 | 
			
		||||
        ['-n', 'src/api', '-c', 'ts', '../fileserver.rs'],
 | 
			
		||||
        { stdio: 'inherit' }
 | 
			
		||||
    );
 | 
			
		||||
    let api_content = fs.readFileSync('src/api.ts', 'utf8');
 | 
			
		||||
    api_content = api_content.replace(
 | 
			
		||||
        'interface Response<T> {\n    e: (string|null);\n    o: (T|null);\n}',
 | 
			
		||||
        response_replacement
 | 
			
		||||
    );
 | 
			
		||||
    fs.writeFileSync('src/api.ts', api_content, 'utf8');
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default defineConfig({
 | 
			
		||||
    plugins: [
 | 
			
		||||
        { name: 'mrpc', buildStart: checkMrpc },
 | 
			
		||||
        svelte(),
 | 
			
		||||
        Icons({ compiler: 'svelte' }),
 | 
			
		||||
        purgeCss(),
 | 
			
		||||
@@ -51,11 +18,9 @@ export default defineConfig({
 | 
			
		||||
    },
 | 
			
		||||
    server: {
 | 
			
		||||
        host: '0.0.0.0',
 | 
			
		||||
        port: 2345,
 | 
			
		||||
        proxy: {
 | 
			
		||||
            '/mrpc': 'http://127.0.0.1:2121',
 | 
			
		||||
            '/download': 'http://127.0.0.1:2121',
 | 
			
		||||
            '/download_multi': 'http://127.0.0.1:2121',
 | 
			
		||||
            '/upload': 'http://127.0.0.1:2121'
 | 
			
		||||
            '/api': 'http://127.0.0.1:2121'
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
})
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user