Implemented share link generation and viewing
All checks were successful
/ Build the server (push) Successful in 2m35s
All checks were successful
/ Build the server (push) Successful in 2m35s
Closes #60
This commit is contained in:
parent
0306a98936
commit
6c2b73dbd0
@ -12,32 +12,32 @@
|
|||||||
"fetch-api": "openapi-typescript http://127.0.0.1:2121/openapi.json -o ./src/api/schema.d.ts"
|
"fetch-api": "openapi-typescript http://127.0.0.1:2121/openapi.json -o ./src/api/schema.d.ts"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@iconify/json": "^2.2.132",
|
"@iconify/json": "^2.2.324",
|
||||||
"@sveltejs/vite-plugin-svelte": "^2.4.2",
|
"@sveltejs/vite-plugin-svelte": "^2.5.3",
|
||||||
"@tsconfig/svelte": "^5.0.0",
|
"@tsconfig/svelte": "^5.0.4",
|
||||||
"@types/node": "^20.8.6",
|
"@types/node": "^20.17.30",
|
||||||
"@types/qrcode-svg": "^1.1.2",
|
"@types/qrcode-svg": "^1.1.5",
|
||||||
"autoprefixer": "^10.4.14",
|
"autoprefixer": "^10.4.21",
|
||||||
"flowbite": "^1.8.1",
|
"flowbite": "^1.8.1",
|
||||||
"flowbite-svelte": "^0.44.18",
|
"flowbite-svelte": "^0.44.24",
|
||||||
"openapi-typescript": "^7.3.3",
|
"openapi-typescript": "^7.6.1",
|
||||||
"postcss": "^8.4.24",
|
"postcss": "^8.5.3",
|
||||||
"postcss-load-config": "^4.0.1",
|
"postcss-load-config": "^4.0.2",
|
||||||
"svelte": "^4.0.5",
|
"svelte": "^4.2.19",
|
||||||
"svelte-check": "^3.4.6",
|
"svelte-check": "^3.8.6",
|
||||||
"tailwindcss": "^3.3.2",
|
"tailwindcss": "^3.4.17",
|
||||||
"tslib": "^2.6.0",
|
"tslib": "^2.8.1",
|
||||||
"typescript": "^5.0.2",
|
"typescript": "^5.8.3",
|
||||||
"unplugin-icons": "^0.17.1",
|
"unplugin-icons": "^0.17.4",
|
||||||
"vite": "^4.4.5",
|
"vite": "^4.5.12",
|
||||||
"vite-plugin-html": "^3.2.0",
|
"vite-plugin-html": "^3.2.2",
|
||||||
"vite-plugin-singlefile": "^0.13.5",
|
"vite-plugin-singlefile": "^0.13.5",
|
||||||
"vite-plugin-tailwind-purgecss": "^0.1.3"
|
"vite-plugin-tailwind-purgecss": "^0.1.4"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@microsoft/fetch-event-source": "^2.0.1",
|
"@microsoft/fetch-event-source": "^2.0.1",
|
||||||
"filesize": "^10.1.0",
|
"filesize": "^10.1.6",
|
||||||
"openapi-fetch": "^0.12.0",
|
"openapi-fetch": "^0.12.5",
|
||||||
"qrcode-svg": "^1.1.0",
|
"qrcode-svg": "^1.1.0",
|
||||||
"svelte-spa-router": "^3.3.0",
|
"svelte-spa-router": "^3.3.0",
|
||||||
"tailwind-merge": "^1.14.0"
|
"tailwind-merge": "^1.14.0"
|
||||||
|
1242
frontend/pnpm-lock.yaml
generated
1242
frontend/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@ -1,7 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {error_banner, info_banner, rpc, session, show_working, token, workingWrapper} from './store';
|
import {error_banner, info_banner, globalRpc, session, show_working, token, workingWrapper} from './store';
|
||||||
import {Banner, Navbar, NavBrand, Spinner} from 'flowbite-svelte';
|
import {Banner, Navbar, NavBrand, Spinner} from 'flowbite-svelte';
|
||||||
import Router, {replace} from 'svelte-spa-router';
|
import Router, {push, replace} from 'svelte-spa-router';
|
||||||
import {routes} from './routes';
|
import {routes} from './routes';
|
||||||
import {FileStorage} from './icons';
|
import {FileStorage} from './icons';
|
||||||
import LinkButton from './components/LinkButton.svelte';
|
import LinkButton from './components/LinkButton.svelte';
|
||||||
@ -10,14 +10,15 @@
|
|||||||
const s = session.s;
|
const s = session.s;
|
||||||
|
|
||||||
async function leaveSudo() {
|
async function leaveSudo() {
|
||||||
await workingWrapper(() => rpc.admin.unSudo());
|
await workingWrapper(() => globalRpc.admin.unSudo());
|
||||||
await session.update($token);
|
await session.update($token);
|
||||||
await replace('/admin');
|
await replace('/admin');
|
||||||
}
|
}
|
||||||
|
|
||||||
async function logout() {
|
async function logout() {
|
||||||
await rpc.logout();
|
await globalRpc.logout();
|
||||||
token.set(null);
|
token.set(null);
|
||||||
|
await push('/login');
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@ -49,6 +50,10 @@
|
|||||||
<A href="#/profile">Profile</A>
|
<A href="#/profile">Profile</A>
|
||||||
<LinkButton on:click={logout}>Logout</LinkButton>
|
<LinkButton on:click={logout}>Logout</LinkButton>
|
||||||
</div>
|
</div>
|
||||||
|
{:else}
|
||||||
|
<div class="flex md:order-2 gap-x-2">
|
||||||
|
<A href="#/login">Login</A>
|
||||||
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</Navbar>
|
</Navbar>
|
||||||
<span class="grid justify-items-center mt-10">
|
<span class="grid justify-items-center mt-10">
|
||||||
|
@ -1,17 +1,8 @@
|
|||||||
import type {paths, components} from './schema';
|
import type {paths, components} from './schema';
|
||||||
import createClient from 'openapi-fetch';
|
import createClient from 'openapi-fetch';
|
||||||
import {fetchEventSource} from '@microsoft/fetch-event-source';
|
import {fetchEventSource} from '@microsoft/fetch-event-source';
|
||||||
|
import {token} from '../store';
|
||||||
const client = createClient<paths>();
|
import { replace } from 'svelte-spa-router';
|
||||||
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 Session = components['schemas']['de.mattv.fileserver.Response$Session'];
|
||||||
export type Node = components['schemas']['de.mattv.fileserver.Response$Node'];
|
export type Node = components['schemas']['de.mattv.fileserver.Response$Node'];
|
||||||
@ -19,48 +10,51 @@ export type PathSegment = components['schemas']['de.mattv.fileserver.Response$Pa
|
|||||||
export type CreateNodeInfo = components['schemas']['de.mattv.fileserver.Response$CreateNodeInfo'];
|
export type CreateNodeInfo = components['schemas']['de.mattv.fileserver.Response$CreateNodeInfo'];
|
||||||
export type UserInfo = components['schemas']['de.mattv.fileserver.Response$UserInfo'];
|
export type UserInfo = components['schemas']['de.mattv.fileserver.Response$UserInfo'];
|
||||||
|
|
||||||
export const rpc = {
|
const _getRpcClient = () => {
|
||||||
|
let obj = {
|
||||||
token: '',
|
token: '',
|
||||||
|
client: createClient<paths>(),
|
||||||
|
share_root: null,
|
||||||
|
|
||||||
signup: (username: string, password: string) =>
|
signup: (username: string, password: string) =>
|
||||||
client.POST('/api/public/auth/signup', { body: { username, password } }).then(v => v.data),
|
obj.client.POST('/api/public/auth/signup', { body: { username, password } }).then(v => v.data),
|
||||||
login: (username: string, password: string, otp?: string) =>
|
login: (username: string, password: string, otp?: string) =>
|
||||||
client.POST('/api/public/auth/login', { body: { username, password, otp } }).then(v => v.data),
|
obj.client.POST('/api/public/auth/login', { body: { username, password, otp } }).then(v => v.data),
|
||||||
|
|
||||||
send_recovery_key: (username: string) =>
|
send_recovery_key: (username: string) =>
|
||||||
client.POST('/api/public/auth/send_recovery_key', { body: username }).then(v => v.data),
|
obj.client.POST('/api/public/auth/send_recovery_key', { body: username }).then(v => v.data),
|
||||||
reset_password: (key: string, password: string) =>
|
reset_password: (key: string, password: string) =>
|
||||||
client.POST('/api/public/auth/reset_password', { body: { key, password } }).then(v => v.data),
|
obj.client.POST('/api/public/auth/reset_password', { body: { key, password } }).then(v => v.data),
|
||||||
|
|
||||||
|
|
||||||
change_password: (oldPassword: string, newPassword: string) =>
|
change_password: (oldPassword: string, newPassword: string) =>
|
||||||
client.POST('/api/user/auth/change_password', { body: { oldPassword, newPassword } }).then(v => v.data),
|
obj.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),
|
logout: () => obj.client.POST('/api/user/auth/logout').then(v => v.data),
|
||||||
logoutAll: () => client.POST('/api/user/auth/logout_all').then(v => v.data),
|
logoutAll: () => obj.client.POST('/api/user/auth/logout_all').then(v => v.data),
|
||||||
deleteAccount: () => client.POST('/api/user/auth/delete').then(v => v.data),
|
deleteAccount: () => obj.client.POST('/api/user/auth/delete').then(v => v.data),
|
||||||
sessionInfo: () => client.POST('/api/user/session').then(v => v.data),
|
sessionInfo: () => obj.client.POST('/api/user_share/session').then(v => v.data),
|
||||||
|
|
||||||
tfaSetupMail: () => client.POST('/api/user/tfa/setup_mail').then(v => v.data),
|
tfaSetupMail: () => obj.client.POST('/api/user/tfa/setup_mail').then(v => v.data),
|
||||||
tfaSetupTotp: () => client.POST('/api/user/tfa/setup_totp').then(v => v.data),
|
tfaSetupTotp: () => obj.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),
|
tfaComplete: (code: string) => obj.client.POST('/api/user/tfa/complete', { body: code }).then(v => v.data),
|
||||||
tfaDisable: () => client.POST('/api/user/tfa/disable').then(v => v.data),
|
tfaDisable: () => obj.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),
|
getNode: (node: number) => obj.client.POST('/api/user_share/fs/node', { body: node }).then(v => v.data),
|
||||||
getPath: (node: number) => client.POST('/api/user/fs/path', { body: node }).then(v => v.data),
|
getPath: (node: number) => obj.client.POST('/api/user_share/fs/path', { body: node }).then(v => v.data),
|
||||||
getNodesSize: (nodes: number[]) => client.POST('/api/user/fs/size', { body: nodes }).then(v => v.data),
|
getNodesSize: (nodes: number[]) => obj.client.POST('/api/user_share/fs/size', { body: nodes }).then(v => v.data),
|
||||||
getMime: (node: number) => client.POST('/api/user/fs/mime', { body: node }).then(v => v.data),
|
getMime: (node: number) => obj.client.POST('/api/user_share/fs/mime', { body: node }).then(v => v.data),
|
||||||
downloadPreview: (node: number) => client.POST('/api/user/fs/preview', { body: node }).then(v => v.data),
|
downloadPreview: (node: number) => obj.client.POST('/api/user_share/fs/preview', { body: node }).then(v => v.data),
|
||||||
|
|
||||||
createNode: (name: string, parent: number, file: boolean) =>
|
createNode: (name: string, parent: number, file: boolean) =>
|
||||||
client.POST('/api/user/fs/create', { body: { name, parent, file } }).then(v => v.data),
|
obj.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', {
|
deleteNodes: (nodes: number[], cbk: (v: string|null) => void) => fetchEventSource('/api/user/fs/delete', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: JSON.stringify(nodes),
|
body: JSON.stringify(nodes),
|
||||||
headers: {
|
headers: {
|
||||||
'Authorization': 'Bearer ' + rpc.token,
|
'Authorization': 'Bearer ' + obj.token,
|
||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json'
|
||||||
},
|
},
|
||||||
onmessage: v => cbk(v.data),
|
onmessage: v => cbk(v.data),
|
||||||
@ -68,15 +62,47 @@ export const rpc = {
|
|||||||
onclose: () => cbk(null)
|
onclose: () => cbk(null)
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
shareNode: (node: number) => obj.client.POST('/api/user/fs/share', { body: node }).then(v => v.data),
|
||||||
|
unshareNode: (node: number) => obj.client.POST('/api/user/fs/unshare', { body: node }).then(v => v.data),
|
||||||
|
|
||||||
admin: {
|
admin: {
|
||||||
listUsers: () => client.POST('/api/admin/users').then(v => v.data),
|
listUsers: () => obj.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),
|
setEnabled: (id: number, state: boolean) => obj.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),
|
setAdmin: (id: number, state: boolean) => obj.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),
|
sudo: (id: number) => obj.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),
|
logout: (id: number) => obj.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),
|
disableTfa: (id: number) => obj.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),
|
deleteUser: (id: number) => obj.client.POST('/api/admin/user/delete', { body: id }).then(v => v.data),
|
||||||
unSudo: () => client.POST('/api/admin/un_sudo').then(v => v.data),
|
unSudo: () => obj.client.POST('/api/admin/un_sudo').then(v => v.data),
|
||||||
shutdown: () => client.POST('/api/admin/shutdown').then(v => v.data),
|
shutdown: () => obj.client.POST('/api/admin/shutdown').then(v => v.data),
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
obj.client.use({
|
||||||
|
onRequest({ schemaPath, request }) {
|
||||||
|
if (schemaPath.startsWith('/api/public') || obj.token == '')
|
||||||
|
return;
|
||||||
|
|
||||||
|
request.headers.set('Authorization', `Bearer ${obj.token}`);
|
||||||
|
return request;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return obj;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type RpcClient = Omit<ReturnType<typeof _getRpcClient>, 'share_root'> & { share_root: null | number };
|
||||||
|
export const getRpcClient: () => RpcClient = () => _getRpcClient();
|
||||||
|
export const globalRpc = getRpcClient();
|
||||||
|
|
||||||
|
globalRpc.client.use({
|
||||||
|
onResponse({ schemaPath, response }) {
|
||||||
|
if (schemaPath.startsWith('/api/public') || schemaPath == '/api/user/session')
|
||||||
|
return response;
|
||||||
|
if (response.status >= 400 && response.status != 404) {
|
||||||
|
token.set(null);
|
||||||
|
replace('/login');
|
||||||
|
}
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
399
frontend/src/api/schema.d.ts
vendored
399
frontend/src/api/schema.d.ts
vendored
@ -4,6 +4,102 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
export interface paths {
|
export interface paths {
|
||||||
|
"/api/user_share/session": {
|
||||||
|
parameters: {
|
||||||
|
query?: never;
|
||||||
|
header?: never;
|
||||||
|
path?: never;
|
||||||
|
cookie?: never;
|
||||||
|
};
|
||||||
|
get?: never;
|
||||||
|
put?: never;
|
||||||
|
post: operations["session"];
|
||||||
|
delete?: never;
|
||||||
|
options?: never;
|
||||||
|
head?: never;
|
||||||
|
patch?: never;
|
||||||
|
trace?: never;
|
||||||
|
};
|
||||||
|
"/api/user_share/fs/size": {
|
||||||
|
parameters: {
|
||||||
|
query?: never;
|
||||||
|
header?: never;
|
||||||
|
path?: never;
|
||||||
|
cookie?: never;
|
||||||
|
};
|
||||||
|
get?: never;
|
||||||
|
put?: never;
|
||||||
|
post: operations["size"];
|
||||||
|
delete?: never;
|
||||||
|
options?: never;
|
||||||
|
head?: never;
|
||||||
|
patch?: never;
|
||||||
|
trace?: never;
|
||||||
|
};
|
||||||
|
"/api/user_share/fs/preview": {
|
||||||
|
parameters: {
|
||||||
|
query?: never;
|
||||||
|
header?: never;
|
||||||
|
path?: never;
|
||||||
|
cookie?: never;
|
||||||
|
};
|
||||||
|
get?: never;
|
||||||
|
put?: never;
|
||||||
|
post: operations["preview"];
|
||||||
|
delete?: never;
|
||||||
|
options?: never;
|
||||||
|
head?: never;
|
||||||
|
patch?: never;
|
||||||
|
trace?: never;
|
||||||
|
};
|
||||||
|
"/api/user_share/fs/path": {
|
||||||
|
parameters: {
|
||||||
|
query?: never;
|
||||||
|
header?: never;
|
||||||
|
path?: never;
|
||||||
|
cookie?: never;
|
||||||
|
};
|
||||||
|
get?: never;
|
||||||
|
put?: never;
|
||||||
|
post: operations["path"];
|
||||||
|
delete?: never;
|
||||||
|
options?: never;
|
||||||
|
head?: never;
|
||||||
|
patch?: never;
|
||||||
|
trace?: never;
|
||||||
|
};
|
||||||
|
"/api/user_share/fs/node": {
|
||||||
|
parameters: {
|
||||||
|
query?: never;
|
||||||
|
header?: never;
|
||||||
|
path?: never;
|
||||||
|
cookie?: never;
|
||||||
|
};
|
||||||
|
get?: never;
|
||||||
|
put?: never;
|
||||||
|
post: operations["nodeInfo"];
|
||||||
|
delete?: never;
|
||||||
|
options?: never;
|
||||||
|
head?: never;
|
||||||
|
patch?: never;
|
||||||
|
trace?: never;
|
||||||
|
};
|
||||||
|
"/api/user_share/fs/mime": {
|
||||||
|
parameters: {
|
||||||
|
query?: never;
|
||||||
|
header?: never;
|
||||||
|
path?: never;
|
||||||
|
cookie?: never;
|
||||||
|
};
|
||||||
|
get?: never;
|
||||||
|
put?: never;
|
||||||
|
post: operations["mime"];
|
||||||
|
delete?: never;
|
||||||
|
options?: never;
|
||||||
|
head?: never;
|
||||||
|
patch?: never;
|
||||||
|
trace?: never;
|
||||||
|
};
|
||||||
"/api/user/tfa/setup_totp": {
|
"/api/user/tfa/setup_totp": {
|
||||||
parameters: {
|
parameters: {
|
||||||
query?: never;
|
query?: never;
|
||||||
@ -68,7 +164,7 @@ export interface paths {
|
|||||||
patch?: never;
|
patch?: never;
|
||||||
trace?: never;
|
trace?: never;
|
||||||
};
|
};
|
||||||
"/api/user/session": {
|
"/api/user/fs/unshare": {
|
||||||
parameters: {
|
parameters: {
|
||||||
query?: never;
|
query?: never;
|
||||||
header?: never;
|
header?: never;
|
||||||
@ -77,14 +173,14 @@ export interface paths {
|
|||||||
};
|
};
|
||||||
get?: never;
|
get?: never;
|
||||||
put?: never;
|
put?: never;
|
||||||
post: operations["session"];
|
post: operations["path_1"];
|
||||||
delete?: never;
|
delete?: never;
|
||||||
options?: never;
|
options?: never;
|
||||||
head?: never;
|
head?: never;
|
||||||
patch?: never;
|
patch?: never;
|
||||||
trace?: never;
|
trace?: never;
|
||||||
};
|
};
|
||||||
"/api/user/fs/size": {
|
"/api/user/fs/share": {
|
||||||
parameters: {
|
parameters: {
|
||||||
query?: never;
|
query?: never;
|
||||||
header?: never;
|
header?: never;
|
||||||
@ -93,71 +189,7 @@ export interface paths {
|
|||||||
};
|
};
|
||||||
get?: never;
|
get?: never;
|
||||||
put?: never;
|
put?: never;
|
||||||
post: operations["size"];
|
post: operations["share"];
|
||||||
delete?: never;
|
|
||||||
options?: never;
|
|
||||||
head?: never;
|
|
||||||
patch?: never;
|
|
||||||
trace?: never;
|
|
||||||
};
|
|
||||||
"/api/user/fs/preview": {
|
|
||||||
parameters: {
|
|
||||||
query?: never;
|
|
||||||
header?: never;
|
|
||||||
path?: never;
|
|
||||||
cookie?: never;
|
|
||||||
};
|
|
||||||
get?: never;
|
|
||||||
put?: never;
|
|
||||||
post: operations["preview"];
|
|
||||||
delete?: never;
|
|
||||||
options?: never;
|
|
||||||
head?: never;
|
|
||||||
patch?: never;
|
|
||||||
trace?: never;
|
|
||||||
};
|
|
||||||
"/api/user/fs/path": {
|
|
||||||
parameters: {
|
|
||||||
query?: never;
|
|
||||||
header?: never;
|
|
||||||
path?: never;
|
|
||||||
cookie?: never;
|
|
||||||
};
|
|
||||||
get?: never;
|
|
||||||
put?: never;
|
|
||||||
post: operations["path"];
|
|
||||||
delete?: never;
|
|
||||||
options?: never;
|
|
||||||
head?: never;
|
|
||||||
patch?: never;
|
|
||||||
trace?: never;
|
|
||||||
};
|
|
||||||
"/api/user/fs/node": {
|
|
||||||
parameters: {
|
|
||||||
query?: never;
|
|
||||||
header?: never;
|
|
||||||
path?: never;
|
|
||||||
cookie?: never;
|
|
||||||
};
|
|
||||||
get?: never;
|
|
||||||
put?: never;
|
|
||||||
post: operations["nodeInfo"];
|
|
||||||
delete?: never;
|
|
||||||
options?: never;
|
|
||||||
head?: never;
|
|
||||||
patch?: never;
|
|
||||||
trace?: never;
|
|
||||||
};
|
|
||||||
"/api/user/fs/mime": {
|
|
||||||
parameters: {
|
|
||||||
query?: never;
|
|
||||||
header?: never;
|
|
||||||
path?: never;
|
|
||||||
cookie?: never;
|
|
||||||
};
|
|
||||||
get?: never;
|
|
||||||
put?: never;
|
|
||||||
post: operations["mime"];
|
|
||||||
delete?: never;
|
delete?: never;
|
||||||
options?: never;
|
options?: never;
|
||||||
head?: never;
|
head?: never;
|
||||||
@ -472,15 +504,13 @@ export interface paths {
|
|||||||
export type webhooks = Record<string, never>;
|
export type webhooks = Record<string, never>;
|
||||||
export interface components {
|
export interface components {
|
||||||
schemas: {
|
schemas: {
|
||||||
"de.mattv.fileserver.ResponseJava.lang.String": {
|
|
||||||
e?: string;
|
|
||||||
o?: string;
|
|
||||||
};
|
|
||||||
"de.mattv.fileserver.Response$Session": {
|
"de.mattv.fileserver.Response$Session": {
|
||||||
name: string;
|
name: string;
|
||||||
tfaEnabled: boolean;
|
tfaEnabled: boolean;
|
||||||
admin: boolean;
|
admin: boolean;
|
||||||
sudo: boolean;
|
sudo: boolean;
|
||||||
|
/** Format: int64 */
|
||||||
|
shareRoot?: number;
|
||||||
};
|
};
|
||||||
"de.mattv.fileserver.Response$PathSegment": {
|
"de.mattv.fileserver.Response$PathSegment": {
|
||||||
name: string;
|
name: string;
|
||||||
@ -493,12 +523,17 @@ export interface components {
|
|||||||
name: string;
|
name: string;
|
||||||
file: boolean;
|
file: boolean;
|
||||||
preview: boolean;
|
preview: boolean;
|
||||||
|
shareName?: string;
|
||||||
/** Format: int64 */
|
/** Format: int64 */
|
||||||
size?: number;
|
size?: number;
|
||||||
/** Format: int64 */
|
/** Format: int64 */
|
||||||
parent?: number;
|
parent?: number;
|
||||||
children?: components["schemas"]["de.mattv.fileserver.Response$Node"][];
|
children?: components["schemas"]["de.mattv.fileserver.Response$Node"][];
|
||||||
};
|
};
|
||||||
|
"de.mattv.fileserver.ResponseJava.lang.String": {
|
||||||
|
e?: string;
|
||||||
|
o?: string;
|
||||||
|
};
|
||||||
"org.springframework.web.servlet.mvc.method.annotation.SseEmitter": {
|
"org.springframework.web.servlet.mvc.method.annotation.SseEmitter": {
|
||||||
/** Format: int64 */
|
/** Format: int64 */
|
||||||
timeout?: number;
|
timeout?: number;
|
||||||
@ -566,88 +601,6 @@ export interface components {
|
|||||||
}
|
}
|
||||||
export type $defs = Record<string, never>;
|
export type $defs = Record<string, never>;
|
||||||
export interface operations {
|
export interface operations {
|
||||||
setupTfaTotp: {
|
|
||||||
parameters: {
|
|
||||||
query?: never;
|
|
||||||
header?: never;
|
|
||||||
path?: never;
|
|
||||||
cookie?: never;
|
|
||||||
};
|
|
||||||
requestBody?: never;
|
|
||||||
responses: {
|
|
||||||
/** @description OK */
|
|
||||||
200: {
|
|
||||||
headers: {
|
|
||||||
[name: string]: unknown;
|
|
||||||
};
|
|
||||||
content: {
|
|
||||||
"*/*": components["schemas"]["de.mattv.fileserver.ResponseJava.lang.String"];
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
setupTfaMail: {
|
|
||||||
parameters: {
|
|
||||||
query?: never;
|
|
||||||
header?: never;
|
|
||||||
path?: never;
|
|
||||||
cookie?: never;
|
|
||||||
};
|
|
||||||
requestBody?: never;
|
|
||||||
responses: {
|
|
||||||
/** @description OK */
|
|
||||||
200: {
|
|
||||||
headers: {
|
|
||||||
[name: string]: unknown;
|
|
||||||
};
|
|
||||||
content: {
|
|
||||||
"*/*": string;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
disableTfa: {
|
|
||||||
parameters: {
|
|
||||||
query?: never;
|
|
||||||
header?: never;
|
|
||||||
path?: never;
|
|
||||||
cookie?: never;
|
|
||||||
};
|
|
||||||
requestBody?: never;
|
|
||||||
responses: {
|
|
||||||
/** @description OK */
|
|
||||||
200: {
|
|
||||||
headers: {
|
|
||||||
[name: string]: unknown;
|
|
||||||
};
|
|
||||||
content?: never;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
setupComplete: {
|
|
||||||
parameters: {
|
|
||||||
query?: never;
|
|
||||||
header?: never;
|
|
||||||
path?: never;
|
|
||||||
cookie?: never;
|
|
||||||
};
|
|
||||||
requestBody: {
|
|
||||||
content: {
|
|
||||||
"application/json": string;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
responses: {
|
|
||||||
/** @description OK */
|
|
||||||
200: {
|
|
||||||
headers: {
|
|
||||||
[name: string]: unknown;
|
|
||||||
};
|
|
||||||
content: {
|
|
||||||
"*/*": string;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
session: {
|
session: {
|
||||||
parameters: {
|
parameters: {
|
||||||
query?: never;
|
query?: never;
|
||||||
@ -788,16 +741,146 @@ export interface operations {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
delete: {
|
setupTfaTotp: {
|
||||||
parameters: {
|
parameters: {
|
||||||
query: {
|
query?: never;
|
||||||
ids: number[];
|
|
||||||
};
|
|
||||||
header?: never;
|
header?: never;
|
||||||
path?: never;
|
path?: never;
|
||||||
cookie?: never;
|
cookie?: never;
|
||||||
};
|
};
|
||||||
requestBody?: never;
|
requestBody?: never;
|
||||||
|
responses: {
|
||||||
|
/** @description OK */
|
||||||
|
200: {
|
||||||
|
headers: {
|
||||||
|
[name: string]: unknown;
|
||||||
|
};
|
||||||
|
content: {
|
||||||
|
"*/*": components["schemas"]["de.mattv.fileserver.ResponseJava.lang.String"];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
setupTfaMail: {
|
||||||
|
parameters: {
|
||||||
|
query?: never;
|
||||||
|
header?: never;
|
||||||
|
path?: never;
|
||||||
|
cookie?: never;
|
||||||
|
};
|
||||||
|
requestBody?: never;
|
||||||
|
responses: {
|
||||||
|
/** @description OK */
|
||||||
|
200: {
|
||||||
|
headers: {
|
||||||
|
[name: string]: unknown;
|
||||||
|
};
|
||||||
|
content: {
|
||||||
|
"*/*": string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
disableTfa: {
|
||||||
|
parameters: {
|
||||||
|
query?: never;
|
||||||
|
header?: never;
|
||||||
|
path?: never;
|
||||||
|
cookie?: never;
|
||||||
|
};
|
||||||
|
requestBody?: never;
|
||||||
|
responses: {
|
||||||
|
/** @description OK */
|
||||||
|
200: {
|
||||||
|
headers: {
|
||||||
|
[name: string]: unknown;
|
||||||
|
};
|
||||||
|
content?: never;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
setupComplete: {
|
||||||
|
parameters: {
|
||||||
|
query?: never;
|
||||||
|
header?: never;
|
||||||
|
path?: never;
|
||||||
|
cookie?: never;
|
||||||
|
};
|
||||||
|
requestBody: {
|
||||||
|
content: {
|
||||||
|
"application/json": string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
responses: {
|
||||||
|
/** @description OK */
|
||||||
|
200: {
|
||||||
|
headers: {
|
||||||
|
[name: string]: unknown;
|
||||||
|
};
|
||||||
|
content: {
|
||||||
|
"*/*": string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
path_1: {
|
||||||
|
parameters: {
|
||||||
|
query?: never;
|
||||||
|
header?: never;
|
||||||
|
path?: never;
|
||||||
|
cookie?: never;
|
||||||
|
};
|
||||||
|
requestBody: {
|
||||||
|
content: {
|
||||||
|
"application/json": number;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
responses: {
|
||||||
|
/** @description OK */
|
||||||
|
200: {
|
||||||
|
headers: {
|
||||||
|
[name: string]: unknown;
|
||||||
|
};
|
||||||
|
content?: never;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
share: {
|
||||||
|
parameters: {
|
||||||
|
query?: never;
|
||||||
|
header?: never;
|
||||||
|
path?: never;
|
||||||
|
cookie?: never;
|
||||||
|
};
|
||||||
|
requestBody: {
|
||||||
|
content: {
|
||||||
|
"application/json": number;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
responses: {
|
||||||
|
/** @description OK */
|
||||||
|
200: {
|
||||||
|
headers: {
|
||||||
|
[name: string]: unknown;
|
||||||
|
};
|
||||||
|
content: {
|
||||||
|
"*/*": string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
delete: {
|
||||||
|
parameters: {
|
||||||
|
query?: never;
|
||||||
|
header?: never;
|
||||||
|
path?: never;
|
||||||
|
cookie?: never;
|
||||||
|
};
|
||||||
|
requestBody: {
|
||||||
|
content: {
|
||||||
|
"application/json": number[];
|
||||||
|
};
|
||||||
|
};
|
||||||
responses: {
|
responses: {
|
||||||
/** @description OK */
|
/** @description OK */
|
||||||
200: {
|
200: {
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {rpc, show_working} from '../store';
|
import {globalRpc, show_working} from '../store';
|
||||||
import {Button, ButtonGroup, Modal} from 'flowbite-svelte';
|
import {Button, ButtonGroup, Modal} from 'flowbite-svelte';
|
||||||
import {afterUpdate, createEventDispatcher} from 'svelte';
|
import {afterUpdate, createEventDispatcher} from 'svelte';
|
||||||
|
|
||||||
@ -18,7 +18,8 @@
|
|||||||
show_working.set(true);
|
show_working.set(true);
|
||||||
|
|
||||||
await new Promise<void>((resolve) => {
|
await new Promise<void>((resolve) => {
|
||||||
rpc.deleteNodes(nodes, v => {
|
|
||||||
|
globalRpc.deleteNodes(nodes, v => {
|
||||||
if (v == null)
|
if (v == null)
|
||||||
resolve();
|
resolve();
|
||||||
else {
|
else {
|
||||||
|
@ -22,14 +22,18 @@
|
|||||||
Tooltip
|
Tooltip
|
||||||
} from 'flowbite-svelte';
|
} from 'flowbite-svelte';
|
||||||
import {filesize} from 'filesize';
|
import {filesize} from 'filesize';
|
||||||
import {Folder, FolderParent, DocumentBlank, CaretLeft, FolderAdd} from '../icons';
|
import {Folder, FolderParent, DocumentBlank, CaretLeft, FolderAdd, Share} from '../icons';
|
||||||
import {api, download, rpc, workingWrapperR, error_banner} from '../store';
|
import {api, download, workingWrapperR, error_banner, type RpcClient, info_banner} from '../store';
|
||||||
import LinkButton from './LinkButton.svelte';
|
import LinkButton from './LinkButton.svelte';
|
||||||
import DeleteModal from './DeleteModal.svelte';
|
import DeleteModal from './DeleteModal.svelte';
|
||||||
import A from './A.svelte';
|
import A from './A.svelte';
|
||||||
import {createEventDispatcher} from 'svelte';
|
import {createEventDispatcher} from 'svelte';
|
||||||
|
|
||||||
export let node: api.Node;
|
export let node: api.Node;
|
||||||
|
export let rpc: RpcClient
|
||||||
|
export let path_prefix: string;
|
||||||
|
|
||||||
|
const not_share = rpc.share_root == null;
|
||||||
|
|
||||||
const dispatch = createEventDispatcher<{reload_node: null}>();
|
const dispatch = createEventDispatcher<{reload_node: null}>();
|
||||||
|
|
||||||
@ -79,7 +83,6 @@
|
|||||||
$: ctx_style = `top: ${ctx_y}px; left: ${ctx_x}px; position: fixed;`;
|
$: ctx_style = `top: ${ctx_y}px; left: ${ctx_x}px; position: fixed;`;
|
||||||
|
|
||||||
function onCtxMenu(node: api.Node, e: MouseEvent) {
|
function onCtxMenu(node: api.Node, e: MouseEvent) {
|
||||||
console.log(e);
|
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (!ctx_hidden)
|
if (!ctx_hidden)
|
||||||
return ctx_hidden = true;
|
return ctx_hidden = true;
|
||||||
@ -93,15 +96,31 @@
|
|||||||
const selectFolders = () => selected = dirs.map(v => v.id);
|
const selectFolders = () => selected = dirs.map(v => v.id);
|
||||||
const selectFiles = () => selected = files.map(v => v.id);
|
const selectFiles = () => selected = files.map(v => v.id);
|
||||||
const selectNone = () => selected = [];
|
const selectNone = () => selected = [];
|
||||||
const downloadSelected = () => download(nodes.filter(v => selected.includes(v.id)));
|
const downloadSelected = () => download(rpc, nodes.filter(v => selected.includes(v.id)));
|
||||||
|
|
||||||
|
let del: (nodes: number[]) => Promise<void>; // bound to DeleteModal
|
||||||
const deleteSelected = () => del(selected);
|
const deleteSelected = () => del(selected);
|
||||||
|
|
||||||
|
|
||||||
const onCtxDownload = () => download([ctx_node]);
|
|
||||||
|
|
||||||
let del: (nodes: number[]) => Promise<void>;
|
|
||||||
const onCtxDelete = () => del([ctx_node.id]);
|
const onCtxDelete = () => del([ctx_node.id]);
|
||||||
|
|
||||||
|
const onCtxDownload = () => download(rpc, [ctx_node]);
|
||||||
|
|
||||||
|
const shareCopyLink = (id: string) => {
|
||||||
|
let link = `${location.origin}/#/share/${id}`;
|
||||||
|
navigator.clipboard.writeText(link);
|
||||||
|
info_banner.set('Copied link to clipboard');
|
||||||
|
};
|
||||||
|
|
||||||
|
const onCtxShare = async () => {
|
||||||
|
let id = await rpc.shareNode(ctx_node.id);
|
||||||
|
setTimeout(() => shareCopyLink(id!), 75);
|
||||||
|
dispatch('reload_node');
|
||||||
|
};
|
||||||
|
const onCtxCopyShare = () => shareCopyLink(ctx_node.shareName!);
|
||||||
|
const onCtxUnshare = async () => {
|
||||||
|
await rpc.unshareNode(ctx_node.id);
|
||||||
|
dispatch('reload_node');
|
||||||
|
}
|
||||||
|
|
||||||
const onShowPreview = (e: Event) => { show_preview.set((e.target as HTMLInputElement).checked); }
|
const onShowPreview = (e: Event) => { show_preview.set((e.target as HTMLInputElement).checked); }
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@ -123,7 +142,7 @@
|
|||||||
<TableBodyRow>
|
<TableBodyRow>
|
||||||
<TableBodyCell class="!p-4"></TableBodyCell>
|
<TableBodyCell class="!p-4"></TableBodyCell>
|
||||||
<TableBodyCell class="px-2 w-0"><FolderParent /></TableBodyCell>
|
<TableBodyCell class="px-2 w-0"><FolderParent /></TableBodyCell>
|
||||||
<TableBodyCell class="pl-0"><A href={'#/view/' + node.parent}>..</A></TableBodyCell>
|
<TableBodyCell class="pl-0"><A href={path_prefix + node.parent}>..</A></TableBodyCell>
|
||||||
<TableBodyCell></TableBodyCell>
|
<TableBodyCell></TableBodyCell>
|
||||||
</TableBodyRow>
|
</TableBodyRow>
|
||||||
{/if}
|
{/if}
|
||||||
@ -131,7 +150,10 @@
|
|||||||
<TableBodyRow on:contextmenu={onCtxMenu.bind(null, node)}>
|
<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="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="px-2 w-0"><Folder /></TableBodyCell>
|
||||||
<TableBodyCell class="pl-0"><A href={'#/view/' + node.id}>{node.name}</A></TableBodyCell>
|
<TableBodyCell class="pl-0 flex flex-row gap-2">
|
||||||
|
{#if node.shareName && not_share}<Share/>{/if}
|
||||||
|
<A href={path_prefix + node.id}>{node.name}</A>
|
||||||
|
</TableBodyCell>
|
||||||
<TableBodyCell></TableBodyCell>
|
<TableBodyCell></TableBodyCell>
|
||||||
</TableBodyRow>
|
</TableBodyRow>
|
||||||
{/each}
|
{/each}
|
||||||
@ -149,7 +171,10 @@
|
|||||||
<DocumentBlank />
|
<DocumentBlank />
|
||||||
{/if}
|
{/if}
|
||||||
</TableBodyCell>
|
</TableBodyCell>
|
||||||
<TableBodyCell class="pl-0"><A href={'#/view/' + node.id}>{node.name}</A></TableBodyCell>
|
<TableBodyCell class="pl-0 flex flex-row gap-2">
|
||||||
|
{#if node.shareName && not_share}<Share/>{/if}
|
||||||
|
<A href={path_prefix + node.id}>{node.name}</A>
|
||||||
|
</TableBodyCell>
|
||||||
<TableBodyCell>{filesize(node.size ?? 0, {base: 2, standard: 'jedec'})}</TableBodyCell>
|
<TableBodyCell>{filesize(node.size ?? 0, {base: 2, standard: 'jedec'})}</TableBodyCell>
|
||||||
</TableBodyRow>
|
</TableBodyRow>
|
||||||
{/each}
|
{/each}
|
||||||
@ -157,10 +182,10 @@
|
|||||||
<tfoot class="text-gray-700 bg-gray-50">
|
<tfoot class="text-gray-700 bg-gray-50">
|
||||||
<tr>
|
<tr>
|
||||||
<td class="px-6 py-3" colspan="3">
|
<td class="px-6 py-3" colspan="3">
|
||||||
<LinkButton on:click={() => (show_new_folder = true)} class="mr-3">New folder</LinkButton>
|
{#if not_share}<LinkButton on:click={() => (show_new_folder = true)} class="mr-3">New folder</LinkButton>{/if}
|
||||||
{#if selected.length > 0}
|
{#if selected.length > 0}
|
||||||
<LinkButton on:click={downloadSelected}>Download</LinkButton>
|
<LinkButton on:click={downloadSelected}>Download</LinkButton>
|
||||||
<LinkButton color="red" on:click={deleteSelected}>Delete</LinkButton>
|
{#if not_share}<LinkButton color="red" on:click={deleteSelected}>Delete</LinkButton>{/if}
|
||||||
{/if}
|
{/if}
|
||||||
</td>
|
</td>
|
||||||
<td class="px-6 py-3">{filesize(total_size, {base: 2, standard: 'jedec'})}</td>
|
<td class="px-6 py-3">{filesize(total_size, {base: 2, standard: 'jedec'})}</td>
|
||||||
@ -189,7 +214,17 @@
|
|||||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||||
<div style={ctx_style} hidden={ctx_hidden} class="z-50 shadow-md rounded-lg border-gray-100 bg-white" on:contextmenu={() => (ctx_hidden = true)}>
|
<div style={ctx_style} hidden={ctx_hidden} class="z-50 shadow-md rounded-lg border-gray-100 bg-white" on:contextmenu={() => (ctx_hidden = true)}>
|
||||||
<ul class="py-1">
|
<ul class="py-1">
|
||||||
|
{#if not_share}
|
||||||
|
{#if ctx_node?.shareName}
|
||||||
|
<li><button class="font-medium py-2 px-4 text-sm hover:bg-gray-100 w-full text-left" on:click={onCtxCopyShare}>Copy share link</button></li>
|
||||||
|
<li><button class="font-medium py-2 px-4 text-sm hover:bg-gray-100 w-full text-left" on:click={onCtxUnshare}>Unshare</button></li>
|
||||||
|
{:else}
|
||||||
|
<li><button class="font-medium py-2 px-4 text-sm hover:bg-gray-100 w-full text-left" on:click={onCtxShare}>Share</button></li>
|
||||||
|
{/if}
|
||||||
|
{/if}
|
||||||
<li><button class="font-medium py-2 px-4 text-sm hover:bg-gray-100 w-full text-left" on:click={onCtxDownload}>Download</button></li>
|
<li><button class="font-medium py-2 px-4 text-sm hover:bg-gray-100 w-full text-left" on:click={onCtxDownload}>Download</button></li>
|
||||||
|
{#if not_share}
|
||||||
<li><button class="font-medium py-2 px-4 text-sm hover:bg-gray-100 text-red-400 w-full text-left" on:click={onCtxDelete}>Delete</button></li>
|
<li><button class="font-medium py-2 px-4 text-sm hover:bg-gray-100 text-red-400 w-full text-left" on:click={onCtxDelete}>Delete</button></li>
|
||||||
|
{/if}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,10 +1,11 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {Button, Spinner} from 'flowbite-svelte';
|
import {Button, Spinner} from 'flowbite-svelte';
|
||||||
import {Download} from '../icons';
|
import {Download} from '../icons';
|
||||||
import {api, download, rpc, token, workingWrapper} from '../store';
|
import {api, download, workingWrapper, type RpcClient} from '../store';
|
||||||
import {onDestroy} from 'svelte';
|
import {onDestroy} from 'svelte';
|
||||||
|
|
||||||
export let node: api.Node;
|
export let node: api.Node;
|
||||||
|
export let rpc: RpcClient;
|
||||||
|
|
||||||
let src = '';
|
let src = '';
|
||||||
let loading = false;
|
let loading = false;
|
||||||
@ -25,7 +26,7 @@
|
|||||||
const resp = await fetch('/api/public/download', {
|
const resp = await fetch('/api/public/download', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||||
body: `token=${$token ?? ''}&node=${node.id}`
|
body: `token=${rpc.token}&node=${node.id}`
|
||||||
});
|
});
|
||||||
if (resp.status != 200)
|
if (resp.status != 200)
|
||||||
return;
|
return;
|
||||||
@ -38,7 +39,7 @@
|
|||||||
onDestroy(() => { if (src.startsWith('blob')) URL.revokeObjectURL(src); });
|
onDestroy(() => { if (src.startsWith('blob')) URL.revokeObjectURL(src); });
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Button class="w-full mb-6" on:click={() => download([node])}><Download />Download</Button>
|
<Button class="w-full mb-6" on:click={() => download(rpc, [node])}><Download />Download</Button>
|
||||||
{#if can_display && !loading && src === ''}
|
{#if can_display && !loading && src === ''}
|
||||||
<Button class="w-full" outline on:click={load}>Load</Button>
|
<Button class="w-full" outline on:click={load}>Load</Button>
|
||||||
{:else if loading}
|
{:else if loading}
|
||||||
|
@ -10,6 +10,7 @@ export {default as Password} from '~icons/carbon/Password';
|
|||||||
export {default as CloudUpload} from '~icons/carbon/CloudUpload';
|
export {default as CloudUpload} from '~icons/carbon/CloudUpload';
|
||||||
export {default as Checkmark} from '~icons/carbon/Checkmark';
|
export {default as Checkmark} from '~icons/carbon/Checkmark';
|
||||||
export {default as Error} from '~icons/carbon/Error';
|
export {default as Error} from '~icons/carbon/Error';
|
||||||
|
export {default as Share} from '~icons/carbon/Share';
|
||||||
|
|
||||||
export {default as CaretLeft} from '~icons/ph/CaretLeft';
|
export {default as CaretLeft} from '~icons/ph/CaretLeft';
|
||||||
export {default as OTP} from '~icons/ph/Password';
|
export {default as OTP} from '~icons/ph/Password';
|
||||||
|
@ -1,11 +1,5 @@
|
|||||||
import "./app.pcss";
|
import "./app.pcss";
|
||||||
import App from "./App.svelte";
|
import App from "./App.svelte";
|
||||||
import {token} from './store';
|
|
||||||
import {replace} from 'svelte-spa-router';
|
|
||||||
|
|
||||||
token.subscribe(v => {
|
|
||||||
if (v == null) replace('/login').then()
|
|
||||||
});
|
|
||||||
|
|
||||||
const app = new App({
|
const app = new App({
|
||||||
target: document.getElementById("app") as any,
|
target: document.getElementById("app") as any,
|
||||||
|
12
frontend/src/pages/Home.svelte
Normal file
12
frontend/src/pages/Home.svelte
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { replace } from 'svelte-spa-router';
|
||||||
|
import { globalRpc } from '../api';
|
||||||
|
|
||||||
|
globalRpc.sessionInfo()
|
||||||
|
.then(v => {
|
||||||
|
if (v)
|
||||||
|
replace('/view/0');
|
||||||
|
else
|
||||||
|
replace('/login');
|
||||||
|
});
|
||||||
|
</script>
|
@ -2,7 +2,7 @@
|
|||||||
import {Breadcrumb, Dropzone, Modal, Progressbar} from 'flowbite-svelte';
|
import {Breadcrumb, Dropzone, Modal, Progressbar} from 'flowbite-svelte';
|
||||||
import {writable} from 'svelte/store';
|
import {writable} from 'svelte/store';
|
||||||
import {CloudUpload} from '../icons';
|
import {CloudUpload} from '../icons';
|
||||||
import {api, rpc, token, type UploadFile, workingWrapper, workingWrapperR} from '../store';
|
import {api, globalRpc, token, type UploadFile, workingWrapper, workingWrapperR} from '../store';
|
||||||
import DirViewer from '../components/DirViewer.svelte';
|
import DirViewer from '../components/DirViewer.svelte';
|
||||||
import UploadModal from '../components/UploadModal.svelte';
|
import UploadModal from '../components/UploadModal.svelte';
|
||||||
import FileViewer from '../components/FileViewer.svelte';
|
import FileViewer from '../components/FileViewer.svelte';
|
||||||
@ -25,10 +25,10 @@
|
|||||||
|
|
||||||
const data = writable<Data>({node: null, segments: []});
|
const data = writable<Data>({node: null, segments: []});
|
||||||
async function updateData(id: number) {
|
async function updateData(id: number) {
|
||||||
let node = await workingWrapper(() => rpc.getNode(id));
|
let node = await workingWrapper(() => globalRpc.getNode(id));
|
||||||
if (!node)
|
if (!node)
|
||||||
return;
|
return;
|
||||||
let segments = await workingWrapper(() => rpc.getPath(id));
|
let segments = await workingWrapper(() => globalRpc.getPath(id));
|
||||||
if (!segments)
|
if (!segments)
|
||||||
return;
|
return;
|
||||||
data.set({node: node as Data['node'], segments });
|
data.set({node: node as Data['node'], segments });
|
||||||
@ -50,7 +50,7 @@
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const resp = await workingWrapperR<api.CreateNodeInfo>(() => rpc.createNode(entry.name, parent, false));
|
const resp = await workingWrapperR<api.CreateNodeInfo>(() => globalRpc.createNode(entry.name, parent, false));
|
||||||
if (!resp) return [];
|
if (!resp) return [];
|
||||||
if (resp.isFile) return [];
|
if (resp.isFile) return [];
|
||||||
const reader = (entry as FileSystemDirectoryEntry).createReader();
|
const reader = (entry as FileSystemDirectoryEntry).createReader();
|
||||||
@ -109,7 +109,7 @@
|
|||||||
upload_progress_data.current = 0;
|
upload_progress_data.current = 0;
|
||||||
const upload_files: UploadFile[] = [];
|
const upload_files: UploadFile[] = [];
|
||||||
for (const file of files) {
|
for (const file of files) {
|
||||||
const resp = await workingWrapperR<api.CreateNodeInfo>(() => rpc.createNode(file.name, file.id, true));
|
const resp = await workingWrapperR<api.CreateNodeInfo>(() => globalRpc.createNode(file.name, file.id, true));
|
||||||
if (resp && resp.isFile)
|
if (resp && resp.isFile)
|
||||||
upload_files.push({ ...file, id: resp.id, overwrite: resp.exists });
|
upload_files.push({ ...file, id: resp.id, overwrite: resp.exists });
|
||||||
upload_progress_data.current++;
|
upload_progress_data.current++;
|
||||||
@ -145,9 +145,9 @@
|
|||||||
{#if $data.node === null}
|
{#if $data.node === null}
|
||||||
<!-- Waiting for data -->
|
<!-- Waiting for data -->
|
||||||
{:else if $data.node.file}
|
{:else if $data.node.file}
|
||||||
<FileViewer node={$data.node} />
|
<FileViewer rpc={globalRpc} node={$data.node} />
|
||||||
{:else}
|
{:else}
|
||||||
<DirViewer node={$data.node} on:reload_node={() => updateData($data.node?.id ?? 0)} />
|
<DirViewer rpc={globalRpc} path_prefix="#/view/" node={$data.node} on:reload_node={() => updateData($data.node?.id ?? 0)} />
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<UploadModal bind:upload={real_upload} on:reload_node={() => updateData($data.node?.id ?? 0)} />
|
<UploadModal bind:upload={real_upload} on:reload_node={() => updateData($data.node?.id ?? 0)} />
|
||||||
|
@ -1,14 +1,14 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {Button, ButtonGroup, Card, Input, InputAddon} from 'flowbite-svelte';
|
import {Button, ButtonGroup, Card, Input, InputAddon} from 'flowbite-svelte';
|
||||||
import {Email, OTP, Password} from '../icons';
|
import {Email, OTP, Password} from '../../icons';
|
||||||
import {rpc, token, workingWrapperR} from '../store';
|
import {globalRpc, token, workingWrapperR} from '../../store';
|
||||||
import {replace} from 'svelte-spa-router';
|
import {replace} from 'svelte-spa-router';
|
||||||
|
|
||||||
let ask_tfa = false;
|
let ask_tfa = false;
|
||||||
let username = '', password = '', tfa = '';
|
let username = '', password = '', tfa = '';
|
||||||
|
|
||||||
async function login() {
|
async function login() {
|
||||||
const resp = await workingWrapperR(() => rpc.login(username, password, ask_tfa ? tfa : undefined));
|
const resp = await workingWrapperR(() => globalRpc.login(username, password, ask_tfa ? tfa : undefined));
|
||||||
if (!resp) return;
|
if (!resp) return;
|
||||||
if (resp.otpNeeded) {
|
if (resp.otpNeeded) {
|
||||||
ask_tfa = true;
|
ask_tfa = true;
|
||||||
@ -45,7 +45,7 @@
|
|||||||
<ButtonGroup class="w-full flex flex-nowrap">
|
<ButtonGroup class="w-full flex flex-nowrap">
|
||||||
<Button class="flex-1 flex-grow" color="primary" outline href="#/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" on:click={login}>Login</Button>
|
||||||
<Button class="flex-1 flex-grow" color="primary" outline href="#/reset_pw">Forget password</Button>
|
<Button class="flex-1 flex-grow" color="primary" outline href="#/reset_pw">Forgot password</Button>
|
||||||
</ButtonGroup>
|
</ButtonGroup>
|
||||||
{/if}
|
{/if}
|
||||||
</Card>
|
</Card>
|
@ -1,14 +1,14 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {Button, ButtonGroup, Card, Input, InputAddon} from 'flowbite-svelte';
|
import {Button, ButtonGroup, Card, Input, InputAddon} from 'flowbite-svelte';
|
||||||
import {Email, EmailNew, Password} from '../icons';
|
import {Email, EmailNew, Password} from '../../icons';
|
||||||
import {error_banner, info_banner, rpc, workingWrapper, workingWrapperO} from '../store';
|
import {error_banner, info_banner, globalRpc, workingWrapper, workingWrapperO} from '../../store';
|
||||||
import {replace} from 'svelte-spa-router';
|
import {replace} from 'svelte-spa-router';
|
||||||
|
|
||||||
let enter_key = false;
|
let enter_key = false;
|
||||||
let username = '', key = '', password = '', password2 = '';
|
let username = '', key = '', password = '', password2 = '';
|
||||||
|
|
||||||
async function sendKey() {
|
async function sendKey() {
|
||||||
await workingWrapper(() => rpc.send_recovery_key(username));
|
await workingWrapper(() => globalRpc.send_recovery_key(username));
|
||||||
info_banner.set('A message has been sent');
|
info_banner.set('A message has been sent');
|
||||||
enter_key = true;
|
enter_key = true;
|
||||||
}
|
}
|
||||||
@ -19,7 +19,7 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (await workingWrapperO(() => rpc.reset_password(key, password)))
|
if (await workingWrapperO(() => globalRpc.reset_password(key, password)))
|
||||||
await replace('/login');
|
await replace('/login');
|
||||||
}
|
}
|
||||||
|
|
@ -1,7 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {Button, ButtonGroup, Card, Input, InputAddon} from 'flowbite-svelte';
|
import {Button, ButtonGroup, Card, Input, InputAddon} from 'flowbite-svelte';
|
||||||
import {Email, Password} from '../icons';
|
import {Email, Password} from '../../icons';
|
||||||
import {error_banner, info_banner, rpc, workingWrapperO} from '../store';
|
import {error_banner, info_banner, globalRpc, workingWrapperO} from '../../store';
|
||||||
import {replace} from 'svelte-spa-router';
|
import {replace} from 'svelte-spa-router';
|
||||||
|
|
||||||
let username = '', username2 = '', password = '', password2 = '';
|
let username = '', username2 = '', password = '', password2 = '';
|
||||||
@ -17,7 +17,7 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const resp = await workingWrapperO(() => rpc.signup(username, password));
|
const resp = await workingWrapperO(() => globalRpc.signup(username, password));
|
||||||
|
|
||||||
if (resp) {
|
if (resp) {
|
||||||
info_banner.set('Account created, please wait till an administrator approves it');
|
info_banner.set('Account created, please wait till an administrator approves it');
|
@ -1,51 +1,51 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {api, rpc, session, token, workingWrapper} from '../store';
|
import {api, globalRpc, session, token, workingWrapper} from '../../store';
|
||||||
import {Checkbox, Table, TableBody, TableBodyCell, TableBodyRow, TableHead, TableHeadCell} from 'flowbite-svelte';
|
import {Checkbox, Table, TableBody, TableBodyCell, TableBodyRow, TableHead, TableHeadCell} from 'flowbite-svelte';
|
||||||
import {Checkmark, Error} from '../icons';
|
import {Checkmark, Error} from '../../icons';
|
||||||
import LinkButton from '../components/LinkButton.svelte';
|
import LinkButton from '../../components/LinkButton.svelte';
|
||||||
import {replace} from 'svelte-spa-router';
|
import {replace} from 'svelte-spa-router';
|
||||||
|
|
||||||
let users: api.UserInfo[] = [];
|
let users: api.UserInfo[] = [];
|
||||||
|
|
||||||
async function fetchUsers() {
|
async function fetchUsers() {
|
||||||
const resp = await workingWrapper(() => rpc.admin.listUsers());
|
const resp = await workingWrapper(() => globalRpc.admin.listUsers());
|
||||||
users = resp || [];
|
users = resp || [];
|
||||||
}
|
}
|
||||||
|
|
||||||
async function changeEnabled(user: number, target: boolean) {
|
async function changeEnabled(user: number, target: boolean) {
|
||||||
await workingWrapper(() => rpc.admin.setEnabled(user, target));
|
await workingWrapper(() => globalRpc.admin.setEnabled(user, target));
|
||||||
await fetchUsers();
|
await fetchUsers();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function changeAdmin(user: number, target: boolean) {
|
async function changeAdmin(user: number, target: boolean) {
|
||||||
await workingWrapper(() => rpc.admin.setAdmin(user, target));
|
await workingWrapper(() => globalRpc.admin.setAdmin(user, target));
|
||||||
await fetchUsers();
|
await fetchUsers();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function sudo(user: number) {
|
async function sudo(user: number) {
|
||||||
await workingWrapper(() => rpc.admin.sudo(user))
|
await workingWrapper(() => globalRpc.admin.sudo(user))
|
||||||
await session.update('');
|
await session.update('');
|
||||||
await replace('/view/0');
|
await replace('/view/0');
|
||||||
}
|
}
|
||||||
|
|
||||||
async function logout(user: number) {
|
async function logout(user: number) {
|
||||||
await workingWrapper(() => rpc.admin.logout(user));
|
await workingWrapper(() => globalRpc.admin.logout(user));
|
||||||
await fetchUsers();
|
await fetchUsers();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function removeTfa(user: number) {
|
async function removeTfa(user: number) {
|
||||||
await workingWrapper(() => rpc.admin.disableTfa(user));
|
await workingWrapper(() => globalRpc.admin.disableTfa(user));
|
||||||
await fetchUsers();
|
await fetchUsers();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function deleteUser(user: number) {
|
async function deleteUser(user: number) {
|
||||||
await workingWrapper(() => rpc.admin.deleteUser(user));
|
await workingWrapper(() => globalRpc.admin.deleteUser(user));
|
||||||
await fetchUsers();
|
await fetchUsers();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function shutdown() {
|
async function shutdown() {
|
||||||
if (confirm('Do you really want to shutdown the server?')) {
|
if (confirm('Do you really want to shutdown the server?')) {
|
||||||
await rpc.admin.shutdown();
|
await globalRpc.admin.shutdown();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,8 +1,8 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {error_banner, rpc, session, token, workingWrapper, workingWrapperO} from '../store';
|
import {error_banner, globalRpc, session, token, workingWrapper, workingWrapperO} from '../../store';
|
||||||
import {Accordion, AccordionItem, Button, ButtonGroup, Input, InputAddon} from 'flowbite-svelte';
|
import {Accordion, AccordionItem, Button, ButtonGroup, Input, InputAddon} from 'flowbite-svelte';
|
||||||
import {Password} from '../icons';
|
import {Password} from '../../icons';
|
||||||
import {info_banner} from '../store.js';
|
import {info_banner} from '../../store.js';
|
||||||
|
|
||||||
const s = session.s;
|
const s = session.s;
|
||||||
const tfa_enabled: boolean = $s?.tfaEnabled ?? false;
|
const tfa_enabled: boolean = $s?.tfaEnabled ?? false;
|
||||||
@ -15,7 +15,7 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const resp = await workingWrapperO(() => rpc.change_password(old, password));
|
const resp = await workingWrapperO(() => globalRpc.change_password(old, password));
|
||||||
if (resp) {
|
if (resp) {
|
||||||
info_banner.set('Changed password');
|
info_banner.set('Changed password');
|
||||||
change_pw_data.o = '';
|
change_pw_data.o = '';
|
||||||
@ -26,18 +26,18 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function disableTfa() {
|
async function disableTfa() {
|
||||||
await workingWrapper(() => rpc.tfaDisable());
|
await workingWrapper(() => globalRpc.tfaDisable());
|
||||||
token.set(null);
|
token.set(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function logoutAll() {
|
async function logoutAll() {
|
||||||
await workingWrapper(() => rpc.logoutAll());
|
await workingWrapper(() => globalRpc.logoutAll());
|
||||||
token.set(null);
|
token.set(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function deleteAccount() {
|
async function deleteAccount() {
|
||||||
if (confirm("Do your really want to delete your account?")) {
|
if (confirm("Do your really want to delete your account?")) {
|
||||||
await workingWrapper(() => rpc.deleteAccount());
|
await workingWrapper(() => globalRpc.deleteAccount());
|
||||||
token.set(null);
|
token.set(null);
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,7 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {Button, ButtonGroup, Card, Input, InputAddon, StepIndicator, Tooltip} from 'flowbite-svelte';
|
import {Button, ButtonGroup, Card, Input, InputAddon, StepIndicator, Tooltip} from 'flowbite-svelte';
|
||||||
import {OTP} from '../icons';
|
import {OTP} from '../../icons';
|
||||||
import {info_banner, rpc, session, token, workingWrapperO, workingWrapperR} from '../store';
|
import {info_banner, globalRpc, session, token, workingWrapperO, workingWrapperR} from '../../store';
|
||||||
import QRCode from 'qrcode-svg';
|
import QRCode from 'qrcode-svg';
|
||||||
|
|
||||||
const s = session.s;
|
const s = session.s;
|
||||||
@ -13,13 +13,13 @@
|
|||||||
|
|
||||||
async function startSetup(mail: boolean) {
|
async function startSetup(mail: boolean) {
|
||||||
if (mail) {
|
if (mail) {
|
||||||
const resp = await workingWrapperO(() => rpc.tfaSetupMail());
|
const resp = await workingWrapperO(() => globalRpc.tfaSetupMail());
|
||||||
if (resp) {
|
if (resp) {
|
||||||
secret = null;
|
secret = null;
|
||||||
step = 2;
|
step = 2;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const resp = await workingWrapperR<string>(() => rpc.tfaSetupTotp());
|
const resp = await workingWrapperR<string>(() => globalRpc.tfaSetupTotp());
|
||||||
if (resp != null) {
|
if (resp != null) {
|
||||||
secret = resp.replaceAll('=', '');
|
secret = resp.replaceAll('=', '');
|
||||||
secret_qr_code = new QRCode({
|
secret_qr_code = new QRCode({
|
||||||
@ -33,7 +33,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function completeSetup() {
|
async function completeSetup() {
|
||||||
if (await workingWrapperO(() => rpc.tfaComplete(code))) {
|
if (await workingWrapperO(() => globalRpc.tfaComplete(code))) {
|
||||||
info_banner.set("Successfully set up two factor authentication");
|
info_banner.set("Successfully set up two factor authentication");
|
||||||
token.set(null);
|
token.set(null);
|
||||||
}
|
}
|
19
frontend/src/pages/share/ShareHome.svelte
Normal file
19
frontend/src/pages/share/ShareHome.svelte
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { replace } from 'svelte-spa-router';
|
||||||
|
import { getRpcClient } from '../../api';
|
||||||
|
|
||||||
|
export let params: {sid?: string} | undefined = {};
|
||||||
|
|
||||||
|
let sid = params?.sid ?? '';
|
||||||
|
if (sid.length != 30) {
|
||||||
|
replace('/');
|
||||||
|
}
|
||||||
|
|
||||||
|
let rpc = getRpcClient();
|
||||||
|
rpc.token = sid;
|
||||||
|
rpc.sessionInfo().then(v => {
|
||||||
|
let root = v?.shareRoot;
|
||||||
|
if (!root) replace('/');
|
||||||
|
else replace(`/share/${sid}/${root}`);
|
||||||
|
})
|
||||||
|
</script>
|
77
frontend/src/pages/share/ShareView.svelte
Normal file
77
frontend/src/pages/share/ShareView.svelte
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { replace } from 'svelte-spa-router';
|
||||||
|
import { getRpcClient } from '../../api';
|
||||||
|
import { writable } from 'svelte/store';
|
||||||
|
import { api, workingWrapper } from '../../store';
|
||||||
|
import { Breadcrumb } from 'flowbite-svelte';
|
||||||
|
import A from '../../components/A.svelte';
|
||||||
|
import FileViewer from '../../components/FileViewer.svelte';
|
||||||
|
import DirViewer from '../../components/DirViewer.svelte';
|
||||||
|
|
||||||
|
export let params: {sid?: string, id?: string} | undefined = {};
|
||||||
|
|
||||||
|
let sid = params?.sid ?? '';
|
||||||
|
if (sid.length != 30) {
|
||||||
|
replace('/');
|
||||||
|
}
|
||||||
|
|
||||||
|
let base_link = `#/share/${sid}/`;
|
||||||
|
|
||||||
|
let rpc = getRpcClient();
|
||||||
|
rpc.token = sid;
|
||||||
|
|
||||||
|
$: {
|
||||||
|
let id = 0;
|
||||||
|
if (params && params.id) {
|
||||||
|
id = parseInt(params.id);
|
||||||
|
if (id >= 0)
|
||||||
|
updateData(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Data {
|
||||||
|
node: api.Node | null,
|
||||||
|
segments: api.PathSegment[],
|
||||||
|
root: number,
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = writable<Data>({node: null, segments: [], root: 0});
|
||||||
|
async function updateData(id: number) {
|
||||||
|
let root = await workingWrapper(() => rpc.sessionInfo().then(v => v?.shareRoot));
|
||||||
|
if (!root)
|
||||||
|
return replace('/');
|
||||||
|
let node = await workingWrapper(() => rpc.getNode(id));
|
||||||
|
if (!node)
|
||||||
|
return;
|
||||||
|
let segments = await workingWrapper(() => rpc.getPath(id));
|
||||||
|
if (!segments)
|
||||||
|
return;
|
||||||
|
rpc.share_root = root;
|
||||||
|
data.set({node: node as Data['node'], segments, root });
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="w-full max-w-4xl">
|
||||||
|
<div class="w-full flex mb-2 h-16">
|
||||||
|
<Breadcrumb>
|
||||||
|
{#each $data.segments as segment, i}
|
||||||
|
{#if i > 0}<li class="inline-flex items-center">/</li>{/if}
|
||||||
|
<li class="inline-flex items-center">
|
||||||
|
{#if segment.id !== null}
|
||||||
|
<A href={base_link + segment.id}>{segment.id === $data.root ? 'Share' : segment.name}</A>
|
||||||
|
{:else}
|
||||||
|
<span style="padding: 0 0.25em;">{segment.name}</span>
|
||||||
|
{/if}
|
||||||
|
</li>
|
||||||
|
{/each}
|
||||||
|
</Breadcrumb>
|
||||||
|
<span class="flex-1"></span>
|
||||||
|
</div>
|
||||||
|
{#if $data.node === null}
|
||||||
|
<!-- Waiting for data -->
|
||||||
|
{:else if $data.node.file}
|
||||||
|
<FileViewer rpc={rpc} node={$data.node} />
|
||||||
|
{:else}
|
||||||
|
<DirViewer rpc={rpc} path_prefix={base_link} node={$data.node} on:reload_node={() => updateData($data.node?.id ?? $data.root)} />
|
||||||
|
{/if}
|
||||||
|
</div>
|
@ -1,17 +1,27 @@
|
|||||||
import Login from './pages/Login.svelte';
|
import Home from './pages/Home.svelte';
|
||||||
import Signup from './pages/Signup.svelte';
|
import Login from './pages/login/Login.svelte';
|
||||||
import ResetPassword from './pages/ResetPassword.svelte';
|
import Signup from './pages/login/Signup.svelte';
|
||||||
import Profile from './pages/Profile.svelte';
|
import ResetPassword from './pages/login/ResetPassword.svelte';
|
||||||
import TfaSetup from './pages/TfaSetup.svelte';
|
import Profile from './pages/profile/Profile.svelte';
|
||||||
import Admin from './pages/Admin.svelte';
|
import TfaSetup from './pages/profile/TfaSetup.svelte';
|
||||||
|
import Admin from './pages/profile/Admin.svelte';
|
||||||
import View from './pages/View.svelte';
|
import View from './pages/View.svelte';
|
||||||
|
import ShareHome from './pages/share/ShareHome.svelte';
|
||||||
|
import ShareView from './pages/share/ShareView.svelte';
|
||||||
|
|
||||||
export const routes = {
|
export const routes = {
|
||||||
|
'/': Home,
|
||||||
|
|
||||||
'/login': Login,
|
'/login': Login,
|
||||||
'/signup': Signup,
|
'/signup': Signup,
|
||||||
'/reset_pw': ResetPassword,
|
'/reset_pw': ResetPassword,
|
||||||
|
|
||||||
'/profile': Profile,
|
'/profile': Profile,
|
||||||
'/tfa': TfaSetup,
|
'/tfa': TfaSetup,
|
||||||
'/admin': Admin,
|
'/admin': Admin,
|
||||||
'/view/:id': View
|
|
||||||
|
'/view/:id': View,
|
||||||
|
|
||||||
|
'/share/:sid': ShareHome,
|
||||||
|
'/share/:sid/:id': ShareView
|
||||||
}
|
}
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
import {type Session, rpc} from './api';
|
import {type Session, type RpcClient, globalRpc} from './api';
|
||||||
import {type Writable, writable} from 'svelte/store';
|
import {type Writable, writable} from 'svelte/store';
|
||||||
import {filesize} from 'filesize';
|
import {filesize} from 'filesize';
|
||||||
|
|
||||||
export * as api from './api';
|
export * as api from './api';
|
||||||
export {rpc} from './api';
|
export {globalRpc, type RpcClient} from './api';
|
||||||
|
|
||||||
export interface UploadFile {
|
export interface UploadFile {
|
||||||
id: number,
|
id: number,
|
||||||
@ -25,14 +25,11 @@ export const session: { s: Writable<Session|null>, update: (token: string|null)
|
|||||||
session.s.set(null);
|
session.s.set(null);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const s = await rpc.sessionInfo();
|
const s = await globalRpc.sessionInfo();
|
||||||
if (!s)
|
session.s.set(s || null);
|
||||||
token.set(null);
|
|
||||||
else
|
|
||||||
session.s.set(s);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
token.subscribe(t => rpc.token = t ?? '');
|
token.subscribe(t => globalRpc.token = t ?? '');
|
||||||
token.subscribe(t => session.update(t));
|
token.subscribe(t => session.update(t));
|
||||||
|
|
||||||
token.subscribe(v => {
|
token.subscribe(v => {
|
||||||
@ -76,7 +73,7 @@ export async function workingWrapperR<T>(fn: () => Promise<{
|
|||||||
return resp.o as unknown as T;
|
return resp.o as unknown as T;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function download<T extends {id:number, file:boolean}>(nodes: T[]) {
|
export async function download<T extends {id:number, file:boolean}>(rpc: RpcClient, nodes: T[]) {
|
||||||
const form = document.createElement('form');
|
const form = document.createElement('form');
|
||||||
form.method = 'POST';
|
form.method = 'POST';
|
||||||
form.target = '_blank';
|
form.target = '_blank';
|
||||||
|
@ -24,7 +24,8 @@ public class Response<T> {
|
|||||||
@NonNull String name,
|
@NonNull String name,
|
||||||
@NonNull boolean tfaEnabled,
|
@NonNull boolean tfaEnabled,
|
||||||
@NonNull boolean admin,
|
@NonNull boolean admin,
|
||||||
@NonNull boolean sudo
|
@NonNull boolean sudo,
|
||||||
|
@Nullable Long shareRoot
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public record UserInfo(
|
public record UserInfo(
|
||||||
@ -51,18 +52,20 @@ public class Response<T> {
|
|||||||
@NonNull String name,
|
@NonNull String name,
|
||||||
@NonNull boolean file,
|
@NonNull boolean file,
|
||||||
@NonNull boolean preview,
|
@NonNull boolean preview,
|
||||||
|
@Nullable String shareName,
|
||||||
@Nullable Long size,
|
@Nullable Long size,
|
||||||
@Nullable Long parent,
|
@Nullable Long parent,
|
||||||
@Nullable Node[] children
|
@Nullable Node[] children
|
||||||
) {
|
) {
|
||||||
public static Node from(de.mattv.fileserver.data.Node node, Node[] children) {
|
public static Node from(de.mattv.fileserver.data.Node node, boolean hasParent, Node[] children) {
|
||||||
return new Node(
|
return new Node(
|
||||||
node.id,
|
node.id,
|
||||||
node.name,
|
node.name,
|
||||||
node.isFile,
|
node.isFile,
|
||||||
node.hasPreview,
|
node.hasPreview,
|
||||||
|
node.shareString,
|
||||||
node.size,
|
node.size,
|
||||||
node.parent == null ? null : node.parent.id,
|
(node.parent == null || !hasParent) ? null : node.parent.id,
|
||||||
children
|
children
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -5,6 +5,7 @@ import com.thoughtworks.xstream.io.xml.XppDriver;
|
|||||||
import de.mattv.fileserver.Utils;
|
import de.mattv.fileserver.Utils;
|
||||||
import de.mattv.fileserver.data.converter.ConverterUtils;
|
import de.mattv.fileserver.data.converter.ConverterUtils;
|
||||||
import de.mattv.fileserver.data.converter.DataConverter;
|
import de.mattv.fileserver.data.converter.DataConverter;
|
||||||
|
import de.mattv.fileserver.security.ShareService;
|
||||||
import jakarta.annotation.Nullable;
|
import jakarta.annotation.Nullable;
|
||||||
import jakarta.annotation.PostConstruct;
|
import jakarta.annotation.PostConstruct;
|
||||||
import jakarta.annotation.PreDestroy;
|
import jakarta.annotation.PreDestroy;
|
||||||
@ -40,7 +41,6 @@ public class Data {
|
|||||||
|
|
||||||
public static final ConcurrentHashMap<Long, User> USERS = new ConcurrentHashMap<>();
|
public static final ConcurrentHashMap<Long, User> USERS = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
|
||||||
public static final ReentrantReadWriteLock USER_LOCK = new ReentrantReadWriteLock();
|
public static final ReentrantReadWriteLock USER_LOCK = new ReentrantReadWriteLock();
|
||||||
|
|
||||||
private static void saveData() {
|
private static void saveData() {
|
||||||
@ -89,6 +89,7 @@ public class Data {
|
|||||||
} catch (RuntimeException e) {
|
} catch (RuntimeException e) {
|
||||||
Utils.crash(log, "Failed to load data", e);
|
Utils.crash(log, "Failed to load data", e);
|
||||||
}
|
}
|
||||||
|
ShareService.init();
|
||||||
log.info("Finished loading data");
|
log.info("Finished loading data");
|
||||||
saveThread = new Thread(() -> {
|
saveThread = new Thread(() -> {
|
||||||
while (!SHUTDOWN_FLAG.get()) {
|
while (!SHUTDOWN_FLAG.get()) {
|
||||||
|
@ -20,6 +20,7 @@ public class Node {
|
|||||||
public boolean hasPreview = false;
|
public boolean hasPreview = false;
|
||||||
public long size = 0;
|
public long size = 0;
|
||||||
public Node parent = null;
|
public Node parent = null;
|
||||||
|
public String shareString = null;
|
||||||
public final ArrayList<Node> children = new ArrayList<>();
|
public final ArrayList<Node> children = new ArrayList<>();
|
||||||
|
|
||||||
public final File file;
|
public final File file;
|
||||||
|
@ -13,15 +13,20 @@ import java.util.Collection;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
public class Token implements Authentication {
|
public class Token implements Authentication {
|
||||||
private static final Duration LIFETIME = Duration.ofMinutes(60);
|
private static final Duration LIFETIME = Duration.ofHours(24);
|
||||||
private static Instant nowPlusLifetime() { return Instant.now().plus(LIFETIME); }
|
private static Instant nowPlusLifetime() { return Instant.now().plus(LIFETIME); }
|
||||||
|
|
||||||
|
private static final SimpleGrantedAuthority AUTHORITY_USER = new SimpleGrantedAuthority("ROLE_USER");
|
||||||
|
private static final SimpleGrantedAuthority AUTHORITY_ADMIN = new SimpleGrantedAuthority("ROLE_ADMIN");
|
||||||
|
|
||||||
private @NonNull Instant expiresAt = nowPlusLifetime();
|
private @NonNull Instant expiresAt = nowPlusLifetime();
|
||||||
@Getter private final @NonNull String token;
|
@Getter private final @NonNull String token;
|
||||||
|
@Getter private final Long shareRoot;
|
||||||
@Getter private @NonNull User user;
|
@Getter private @NonNull User user;
|
||||||
@Getter private User sudoRealUser = null;
|
@Getter private User sudoRealUser = null;
|
||||||
|
|
||||||
public void refresh() { expiresAt = nowPlusLifetime(); }
|
public void refresh() { expiresAt = nowPlusLifetime(); }
|
||||||
|
public boolean isShare() { return shareRoot != null; }
|
||||||
public boolean expired() { return Utils.instantExpired(expiresAt); }
|
public boolean expired() { return Utils.instantExpired(expiresAt); }
|
||||||
public boolean inSudo() { return sudoRealUser != null; }
|
public boolean inSudo() { return sudoRealUser != null; }
|
||||||
public boolean isAdmin() { return (sudoRealUser != null) || user.admin; }
|
public boolean isAdmin() { return (sudoRealUser != null) || user.admin; }
|
||||||
@ -34,25 +39,29 @@ public class Token implements Authentication {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void sudo(@NonNull User newUser) {
|
public void sudo(@NonNull User newUser) {
|
||||||
if (this.sudoRealUser == null) sudoRealUser = user;
|
if (this.sudoRealUser == null)
|
||||||
|
sudoRealUser = user;
|
||||||
user = newUser;
|
user = newUser;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Token(@NonNull String token, @NonNull User user) {
|
public Token(@NonNull String token, @NonNull User user, Long shareRoot) {
|
||||||
this.token = token;
|
this.token = token;
|
||||||
this.user = user;
|
this.user = user;
|
||||||
|
this.shareRoot = shareRoot;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@Override public Object getDetails() { return null; }
|
@Override public Object getDetails() { return null; }
|
||||||
@Override public String getName() { return String.valueOf(user.id); }
|
@Override public String getName() { return String.valueOf(user.id); }
|
||||||
@Override public Object getPrincipal() { return user; }
|
@Override public Object getPrincipal() { return this; }
|
||||||
@Override public Object getCredentials() { return this; }
|
@Override public Object getCredentials() { return this; }
|
||||||
@Override public boolean isAuthenticated() { return true; }
|
@Override public boolean isAuthenticated() { return true; }
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Collection<? extends GrantedAuthority> getAuthorities() {
|
public Collection<? extends GrantedAuthority> getAuthorities() {
|
||||||
return isAdmin() ? List.of(new SimpleGrantedAuthority("ROLE_ADMIN")) : List.of();
|
if (isShare()) return List.of();
|
||||||
|
return isAdmin()
|
||||||
|
? List.of(AUTHORITY_USER, AUTHORITY_ADMIN)
|
||||||
|
: List.of(AUTHORITY_USER);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -62,9 +71,7 @@ public class Token implements Authentication {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String toString() {
|
public String toString() {
|
||||||
return "Token{" +
|
if (shareRoot == null) return "Token{user=" + user + ", real_user=" + sudoRealUser + '}';
|
||||||
"user=" + user +
|
else return "Share(" + token + ")";
|
||||||
", real_user=" + sudoRealUser +
|
|
||||||
'}';
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -14,6 +14,7 @@ public class NodeConverter {
|
|||||||
private static final String ATTR_NAME_NAME = "name";
|
private static final String ATTR_NAME_NAME = "name";
|
||||||
private static final String ATTR_FLAGS_NAME = "flags";
|
private static final String ATTR_FLAGS_NAME = "flags";
|
||||||
private static final String ATTR_SIZE_NAME = "size";
|
private static final String ATTR_SIZE_NAME = "size";
|
||||||
|
private static final String ATTR_SHARE_NAME = "share";
|
||||||
|
|
||||||
private static class Flags {
|
private static class Flags {
|
||||||
public static final long FILE = 1;
|
public static final long FILE = 1;
|
||||||
@ -31,6 +32,7 @@ public class NodeConverter {
|
|||||||
writer.addAttribute(ATTR_FLAGS_NAME, String.valueOf(flags));
|
writer.addAttribute(ATTR_FLAGS_NAME, String.valueOf(flags));
|
||||||
|
|
||||||
if (node.isFile) writer.addAttribute(ATTR_SIZE_NAME, String.valueOf(node.size));
|
if (node.isFile) writer.addAttribute(ATTR_SIZE_NAME, String.valueOf(node.size));
|
||||||
|
if (node.shareString != null) writer.addAttribute(ATTR_SHARE_NAME, node.shareString);
|
||||||
|
|
||||||
node.children.forEach(child -> toXml(child, writer));
|
node.children.forEach(child -> toXml(child, writer));
|
||||||
|
|
||||||
@ -62,6 +64,10 @@ public class NodeConverter {
|
|||||||
node.size = ConverterUtils.getLong(reader, ATTR_SIZE_NAME);
|
node.size = ConverterUtils.getLong(reader, ATTR_SIZE_NAME);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String share = reader.getAttribute(ATTR_SHARE_NAME);
|
||||||
|
if (share != null)
|
||||||
|
node.shareString = share;
|
||||||
|
|
||||||
while (reader.hasMoreChildren()) {
|
while (reader.hasMoreChildren()) {
|
||||||
Node child = fromXml(reader, user);
|
Node child = fromXml(reader, user);
|
||||||
child.parent = node;
|
child.parent = node;
|
||||||
|
@ -3,6 +3,7 @@ package de.mattv.fileserver.routes;
|
|||||||
import de.mattv.fileserver.data.Node;
|
import de.mattv.fileserver.data.Node;
|
||||||
import de.mattv.fileserver.data.Token;
|
import de.mattv.fileserver.data.Token;
|
||||||
import de.mattv.fileserver.data.User;
|
import de.mattv.fileserver.data.User;
|
||||||
|
import de.mattv.fileserver.routes.fs.FsUtils;
|
||||||
import de.mattv.fileserver.security.TokenService;
|
import de.mattv.fileserver.security.TokenService;
|
||||||
import de.mattv.fileserver.util.AutoCloseLock;
|
import de.mattv.fileserver.util.AutoCloseLock;
|
||||||
import de.mattv.fileserver.util.PublicRestController;
|
import de.mattv.fileserver.util.PublicRestController;
|
||||||
@ -34,7 +35,7 @@ public class Download {
|
|||||||
User user = token.getUser();
|
User user = token.getUser();
|
||||||
try (AutoCloseLock ignored = new AutoCloseLock(user.nodeLock.readLock())) {
|
try (AutoCloseLock ignored = new AutoCloseLock(user.nodeLock.readLock())) {
|
||||||
Node node = user.nodes.get(body.node);
|
Node node = user.nodes.get(body.node);
|
||||||
if (node == null || !node.isFile) {
|
if (node == null || !node.isFile || !FsUtils.shareHasAccess(token, node)) {
|
||||||
response.sendError(401, "Invalid node");
|
response.sendError(401, "Invalid node");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -3,6 +3,7 @@ package de.mattv.fileserver.routes;
|
|||||||
import de.mattv.fileserver.data.Node;
|
import de.mattv.fileserver.data.Node;
|
||||||
import de.mattv.fileserver.data.Token;
|
import de.mattv.fileserver.data.Token;
|
||||||
import de.mattv.fileserver.data.User;
|
import de.mattv.fileserver.data.User;
|
||||||
|
import de.mattv.fileserver.routes.fs.FsUtils;
|
||||||
import de.mattv.fileserver.security.TokenService;
|
import de.mattv.fileserver.security.TokenService;
|
||||||
import de.mattv.fileserver.util.AutoCloseLock;
|
import de.mattv.fileserver.util.AutoCloseLock;
|
||||||
import de.mattv.fileserver.util.PublicRestController;
|
import de.mattv.fileserver.util.PublicRestController;
|
||||||
@ -51,7 +52,7 @@ public class DownloadMulti {
|
|||||||
|
|
||||||
for (long id : body.nodes) {
|
for (long id : body.nodes) {
|
||||||
Node node = user.nodes.get(id);
|
Node node = user.nodes.get(id);
|
||||||
if (node != null)
|
if (node != null && FsUtils.shareHasAccess(token, node))
|
||||||
todo.add(Pair.of(node, ""));
|
todo.add(Pair.of(node, ""));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2,6 +2,7 @@ package de.mattv.fileserver.routes;
|
|||||||
|
|
||||||
import de.mattv.fileserver.Utils;
|
import de.mattv.fileserver.Utils;
|
||||||
import de.mattv.fileserver.data.Node;
|
import de.mattv.fileserver.data.Node;
|
||||||
|
import de.mattv.fileserver.data.Token;
|
||||||
import de.mattv.fileserver.data.User;
|
import de.mattv.fileserver.data.User;
|
||||||
import de.mattv.fileserver.util.AuthUser;
|
import de.mattv.fileserver.util.AuthUser;
|
||||||
import de.mattv.fileserver.util.AutoCloseLock;
|
import de.mattv.fileserver.util.AutoCloseLock;
|
||||||
@ -46,7 +47,8 @@ public class Upload {
|
|||||||
|
|
||||||
@PostMapping("/upload/{id}")
|
@PostMapping("/upload/{id}")
|
||||||
@Operation(hidden = true)
|
@Operation(hidden = true)
|
||||||
private void upload(@AuthUser User user, @PathVariable long id, HttpServletRequest request, HttpServletResponse response) throws IOException {
|
private void upload(@AuthUser Token token, @PathVariable long id, HttpServletRequest request, HttpServletResponse response) throws IOException {
|
||||||
|
User user = token.getUser();
|
||||||
try (AutoCloseLock ignored = new AutoCloseLock(user.nodeLock.readLock())) {
|
try (AutoCloseLock ignored = new AutoCloseLock(user.nodeLock.readLock())) {
|
||||||
Node node = user.nodes.get(id);
|
Node node = user.nodes.get(id);
|
||||||
if (node == null || !node.isFile) {
|
if (node == null || !node.isFile) {
|
||||||
|
@ -8,9 +8,14 @@ import org.springframework.boot.SpringApplication;
|
|||||||
import org.springframework.context.ApplicationContext;
|
import org.springframework.context.ApplicationContext;
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
|
||||||
|
import java.util.concurrent.ExecutorService;
|
||||||
|
import java.util.concurrent.Executors;
|
||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@AdminRestController
|
@AdminRestController
|
||||||
public class General {
|
public class General {
|
||||||
|
private static final ExecutorService SHUTDOWN_EXECUTOR = Executors.newSingleThreadExecutor();
|
||||||
|
|
||||||
private final ApplicationContext ctx;
|
private final ApplicationContext ctx;
|
||||||
|
|
||||||
public General(ApplicationContext ctx) {
|
public General(ApplicationContext ctx) {
|
||||||
@ -25,6 +30,6 @@ public class General {
|
|||||||
@PostMapping("/shutdown")
|
@PostMapping("/shutdown")
|
||||||
private void shutdown(@Parameter(hidden = true) Token token) {
|
private void shutdown(@Parameter(hidden = true) Token token) {
|
||||||
log.warn("Shutdown started by {}", token);
|
log.warn("Shutdown started by {}", token);
|
||||||
SpringApplication.exit(ctx);
|
SHUTDOWN_EXECUTOR.execute(() -> SpringApplication.exit(ctx));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
package de.mattv.fileserver.routes.auth;
|
package de.mattv.fileserver.routes.auth;
|
||||||
|
|
||||||
import de.mattv.fileserver.data.Data;
|
import de.mattv.fileserver.data.Data;
|
||||||
|
import de.mattv.fileserver.data.Token;
|
||||||
import de.mattv.fileserver.data.User;
|
import de.mattv.fileserver.data.User;
|
||||||
import de.mattv.fileserver.security.TokenService;
|
import de.mattv.fileserver.security.TokenService;
|
||||||
import de.mattv.fileserver.util.AuthUser;
|
import de.mattv.fileserver.util.AuthUser;
|
||||||
@ -17,7 +18,8 @@ public class ChangePW {
|
|||||||
private record Body(@NonNull String oldPassword, @NonNull String newPassword) {}
|
private record Body(@NonNull String oldPassword, @NonNull String newPassword) {}
|
||||||
|
|
||||||
@PostMapping("/auth/change_password")
|
@PostMapping("/auth/change_password")
|
||||||
private @NonNull Optional<String> change(@AuthUser User user, @RequestBody Body body) {
|
private @NonNull Optional<String> change(@AuthUser Token token, @RequestBody Body body) {
|
||||||
|
User user = token.getUser();
|
||||||
if (body.newPassword.length() < 6)
|
if (body.newPassword.length() < 6)
|
||||||
return Optional.of("Password must be at least 6 characters");
|
return Optional.of("Password must be at least 6 characters");
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
package de.mattv.fileserver.routes.auth;
|
package de.mattv.fileserver.routes.auth;
|
||||||
|
|
||||||
import de.mattv.fileserver.data.User;
|
import de.mattv.fileserver.data.Token;
|
||||||
import de.mattv.fileserver.util.AuthUser;
|
import de.mattv.fileserver.util.AuthUser;
|
||||||
import de.mattv.fileserver.util.UserRestController;
|
import de.mattv.fileserver.util.UserRestController;
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
@ -8,5 +8,5 @@ import org.springframework.web.bind.annotation.PostMapping;
|
|||||||
@UserRestController
|
@UserRestController
|
||||||
public class Delete {
|
public class Delete {
|
||||||
@PostMapping("/auth/delete")
|
@PostMapping("/auth/delete")
|
||||||
private void delete(@AuthUser User user) { user.delete(); }
|
private void delete(@AuthUser Token token) { token.getUser().delete(); }
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
package de.mattv.fileserver.routes.auth;
|
package de.mattv.fileserver.routes.auth;
|
||||||
|
|
||||||
import de.mattv.fileserver.data.Token;
|
import de.mattv.fileserver.data.Token;
|
||||||
import de.mattv.fileserver.data.User;
|
|
||||||
import de.mattv.fileserver.security.TokenService;
|
import de.mattv.fileserver.security.TokenService;
|
||||||
import de.mattv.fileserver.util.AuthUser;
|
import de.mattv.fileserver.util.AuthUser;
|
||||||
import de.mattv.fileserver.util.UserRestController;
|
import de.mattv.fileserver.util.UserRestController;
|
||||||
@ -16,7 +15,7 @@ public class Logout {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/auth/logout_all")
|
@PostMapping("/auth/logout_all")
|
||||||
private void logoutAll(@AuthUser User user) {
|
private void logoutAll(@AuthUser Token token) {
|
||||||
TokenService.logoutAll(user.id);
|
TokenService.logoutAll(token.getUser().id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,20 +4,30 @@ import de.mattv.fileserver.Response;
|
|||||||
import de.mattv.fileserver.data.Token;
|
import de.mattv.fileserver.data.Token;
|
||||||
import de.mattv.fileserver.data.User;
|
import de.mattv.fileserver.data.User;
|
||||||
import de.mattv.fileserver.util.AuthUser;
|
import de.mattv.fileserver.util.AuthUser;
|
||||||
import de.mattv.fileserver.util.UserRestController;
|
import de.mattv.fileserver.util.UserOrShareRestController;
|
||||||
import io.swagger.v3.oas.annotations.Parameter;
|
|
||||||
import de.mattv.fileserver.util.NonNull;
|
import de.mattv.fileserver.util.NonNull;
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
|
||||||
@UserRestController
|
@UserOrShareRestController
|
||||||
public class SessionInfo {
|
public class SessionInfo {
|
||||||
@PostMapping("/session")
|
@PostMapping("/session")
|
||||||
private @NonNull Response.Session session(@AuthUser User user, @Parameter(hidden = true) Token token) {
|
private @NonNull Response.Session session(@AuthUser Token token) {
|
||||||
|
if (token.isShare()) {
|
||||||
|
return new Response.Session(
|
||||||
|
"[REDACTED]",
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
token.getShareRoot()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
User user = token.getUser();
|
||||||
return new Response.Session(
|
return new Response.Session(
|
||||||
user.name,
|
user.name,
|
||||||
user.tfaSecret != null,
|
user.tfaSecret != null,
|
||||||
token.isAdmin(),
|
token.isAdmin(),
|
||||||
token.inSudo()
|
token.inSudo(),
|
||||||
|
null
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,6 +3,7 @@ package de.mattv.fileserver.routes.auth;
|
|||||||
|
|
||||||
import de.mattv.fileserver.Response;
|
import de.mattv.fileserver.Response;
|
||||||
import de.mattv.fileserver.data.Data;
|
import de.mattv.fileserver.data.Data;
|
||||||
|
import de.mattv.fileserver.data.Token;
|
||||||
import de.mattv.fileserver.data.User;
|
import de.mattv.fileserver.data.User;
|
||||||
import de.mattv.fileserver.security.TokenService;
|
import de.mattv.fileserver.security.TokenService;
|
||||||
import de.mattv.fileserver.services.TFAMailer;
|
import de.mattv.fileserver.services.TFAMailer;
|
||||||
@ -27,7 +28,8 @@ public class TFA {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/tfa/setup_mail")
|
@PostMapping("/tfa/setup_mail")
|
||||||
private @NonNull Optional<String> setupTfaMail(@AuthUser User user) {
|
private @NonNull Optional<String> setupTfaMail(@AuthUser Token token) {
|
||||||
|
User user = token.getUser();
|
||||||
if (user.tfaSecret != null)
|
if (user.tfaSecret != null)
|
||||||
return Optional.of("Tfa is already enabled");
|
return Optional.of("Tfa is already enabled");
|
||||||
|
|
||||||
@ -37,7 +39,8 @@ public class TFA {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/tfa/setup_totp")
|
@PostMapping("/tfa/setup_totp")
|
||||||
private @NonNull Response<String> setupTfaTotp(@AuthUser User user) {
|
private @NonNull Response<String> setupTfaTotp(@AuthUser Token token) {
|
||||||
|
User user = token.getUser();
|
||||||
if (user.tfaSecret != null)
|
if (user.tfaSecret != null)
|
||||||
return Response.e("Tfa is already enabled");
|
return Response.e("Tfa is already enabled");
|
||||||
|
|
||||||
@ -46,7 +49,8 @@ public class TFA {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/tfa/complete")
|
@PostMapping("/tfa/complete")
|
||||||
private @NonNull Optional<String> setupComplete(@AuthUser User user, @RequestBody @NonNull String otp) {
|
private @NonNull Optional<String> setupComplete(@AuthUser Token token, @RequestBody @NonNull String otp) {
|
||||||
|
User user = token.getUser();
|
||||||
if (user.tempTfaSecret == null)
|
if (user.tempTfaSecret == null)
|
||||||
return Optional.of("You never started tfa setup");
|
return Optional.of("You never started tfa setup");
|
||||||
|
|
||||||
@ -64,7 +68,8 @@ public class TFA {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/tfa/disable")
|
@PostMapping("/tfa/disable")
|
||||||
private void disableTfa(@AuthUser User user) {
|
private void disableTfa(@AuthUser Token token) {
|
||||||
|
User user = token.getUser();
|
||||||
user.tfaSecret = null;
|
user.tfaSecret = null;
|
||||||
Data.save();
|
Data.save();
|
||||||
TokenService.logoutAll(user.id);
|
TokenService.logoutAll(user.id);
|
||||||
|
@ -2,6 +2,7 @@ package de.mattv.fileserver.routes.fs;
|
|||||||
|
|
||||||
import de.mattv.fileserver.Response;
|
import de.mattv.fileserver.Response;
|
||||||
import de.mattv.fileserver.data.Node;
|
import de.mattv.fileserver.data.Node;
|
||||||
|
import de.mattv.fileserver.data.Token;
|
||||||
import de.mattv.fileserver.data.User;
|
import de.mattv.fileserver.data.User;
|
||||||
import de.mattv.fileserver.util.AuthUser;
|
import de.mattv.fileserver.util.AuthUser;
|
||||||
import de.mattv.fileserver.util.AutoCloseLock;
|
import de.mattv.fileserver.util.AutoCloseLock;
|
||||||
@ -20,7 +21,8 @@ public class Create {
|
|||||||
private record Body(@NonNull String name, @NonNull long parent, @NonNull boolean file) {}
|
private record Body(@NonNull String name, @NonNull long parent, @NonNull boolean file) {}
|
||||||
|
|
||||||
@PostMapping("/fs/create")
|
@PostMapping("/fs/create")
|
||||||
private @NonNull Response<Response.CreateNodeInfo> create(@AuthUser User user, @RequestBody Body body) {
|
private @NonNull Response<Response.CreateNodeInfo> create(@AuthUser Token token, @RequestBody Body body) {
|
||||||
|
User user = token.getUser();
|
||||||
try (AutoCloseLock ignored = new AutoCloseLock(user.nodeLock.readLock())) {
|
try (AutoCloseLock ignored = new AutoCloseLock(user.nodeLock.readLock())) {
|
||||||
Node parent = user.nodes.get(body.parent);
|
Node parent = user.nodes.get(body.parent);
|
||||||
if (parent == null) return Response.e("Invalid parent node");
|
if (parent == null) return Response.e("Invalid parent node");
|
||||||
|
@ -2,7 +2,9 @@ package de.mattv.fileserver.routes.fs;
|
|||||||
|
|
||||||
import de.mattv.fileserver.data.Data;
|
import de.mattv.fileserver.data.Data;
|
||||||
import de.mattv.fileserver.data.Node;
|
import de.mattv.fileserver.data.Node;
|
||||||
|
import de.mattv.fileserver.data.Token;
|
||||||
import de.mattv.fileserver.data.User;
|
import de.mattv.fileserver.data.User;
|
||||||
|
import de.mattv.fileserver.security.ShareService;
|
||||||
import de.mattv.fileserver.util.AuthUser;
|
import de.mattv.fileserver.util.AuthUser;
|
||||||
import de.mattv.fileserver.util.AutoCloseLock;
|
import de.mattv.fileserver.util.AutoCloseLock;
|
||||||
import de.mattv.fileserver.util.UserRestController;
|
import de.mattv.fileserver.util.UserRestController;
|
||||||
@ -31,7 +33,8 @@ public class FsDelete {
|
|||||||
private static final ExecutorService DELETE_POOL = Executors.newSingleThreadExecutor();
|
private static final ExecutorService DELETE_POOL = Executors.newSingleThreadExecutor();
|
||||||
|
|
||||||
@PostMapping("/fs/delete")
|
@PostMapping("/fs/delete")
|
||||||
private @NonNull SseEmitter delete(@AuthUser User user, @RequestBody @NonNull long[] ids) {
|
private @NonNull SseEmitter delete(@AuthUser Token token, @RequestBody @NonNull long[] ids) {
|
||||||
|
User user = token.getUser();
|
||||||
MyEmitter emitter = new MyEmitter(new SseEmitter());
|
MyEmitter emitter = new MyEmitter(new SseEmitter());
|
||||||
DELETE_POOL.execute(() -> {
|
DELETE_POOL.execute(() -> {
|
||||||
emitter.send("Waiting for lock...");
|
emitter.send("Waiting for lock...");
|
||||||
@ -56,6 +59,10 @@ public class FsDelete {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (node.shareString != null) {
|
||||||
|
ShareService.removeShare(node);
|
||||||
|
}
|
||||||
|
|
||||||
if (node.isFile) {
|
if (node.isFile) {
|
||||||
emitter.send("Deleting " + path + "...");
|
emitter.send("Deleting " + path + "...");
|
||||||
if (!node.file.delete()) log.warn("Failed to delete file {}", node.file);
|
if (!node.file.delete()) log.warn("Failed to delete file {}", node.file);
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
package de.mattv.fileserver.routes.fs;
|
package de.mattv.fileserver.routes.fs;
|
||||||
|
|
||||||
import de.mattv.fileserver.data.Node;
|
import de.mattv.fileserver.data.Node;
|
||||||
|
import de.mattv.fileserver.data.Token;
|
||||||
import de.mattv.fileserver.data.User;
|
import de.mattv.fileserver.data.User;
|
||||||
import de.mattv.fileserver.util.NonNull;
|
import de.mattv.fileserver.util.NonNull;
|
||||||
|
|
||||||
@ -35,4 +36,14 @@ public class FsUtils {
|
|||||||
}
|
}
|
||||||
return path.isEmpty() ? "/" : path.toString();
|
return path.isEmpty() ? "/" : path.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static boolean shareHasAccess(Token token, Node node) {
|
||||||
|
Long rootId = token.getShareRoot();
|
||||||
|
if (rootId == null) return true;
|
||||||
|
do {
|
||||||
|
if (node.id == rootId) return true;
|
||||||
|
node = node.parent;
|
||||||
|
} while (node != null);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,10 +2,11 @@ package de.mattv.fileserver.routes.fs;
|
|||||||
|
|
||||||
import de.mattv.fileserver.Response;
|
import de.mattv.fileserver.Response;
|
||||||
import de.mattv.fileserver.data.Node;
|
import de.mattv.fileserver.data.Node;
|
||||||
|
import de.mattv.fileserver.data.Token;
|
||||||
import de.mattv.fileserver.data.User;
|
import de.mattv.fileserver.data.User;
|
||||||
import de.mattv.fileserver.util.AuthUser;
|
import de.mattv.fileserver.util.AuthUser;
|
||||||
import de.mattv.fileserver.util.AutoCloseLock;
|
import de.mattv.fileserver.util.AutoCloseLock;
|
||||||
import de.mattv.fileserver.util.UserRestController;
|
import de.mattv.fileserver.util.UserOrShareRestController;
|
||||||
import de.mattv.fileserver.util.NonNull;
|
import de.mattv.fileserver.util.NonNull;
|
||||||
import org.springframework.http.MediaTypeFactory;
|
import org.springframework.http.MediaTypeFactory;
|
||||||
import org.springframework.util.MimeType;
|
import org.springframework.util.MimeType;
|
||||||
@ -13,21 +14,26 @@ import org.springframework.web.bind.annotation.PostMapping;
|
|||||||
import org.springframework.web.bind.annotation.RequestBody;
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
|
||||||
@UserRestController
|
@UserOrShareRestController
|
||||||
public class Info {
|
public class Info {
|
||||||
@PostMapping("/fs/node")
|
@PostMapping("/fs/node")
|
||||||
private @NonNull Optional<Response.Node> nodeInfo(@AuthUser User user, @RequestBody @NonNull long id) {
|
private @NonNull Optional<Response.Node> nodeInfo(@AuthUser Token token, @RequestBody @NonNull long id) {
|
||||||
|
User user = token.getUser();
|
||||||
try (AutoCloseLock ignored = new AutoCloseLock(user.nodeLock.readLock())) {
|
try (AutoCloseLock ignored = new AutoCloseLock(user.nodeLock.readLock())) {
|
||||||
Node node = user.nodes.get(id);
|
Node node = user.nodes.get(id);
|
||||||
if (node == null) return Optional.empty();
|
if (node == null || !FsUtils.shareHasAccess(token, node)) return Optional.empty();
|
||||||
|
|
||||||
|
Long shareRoot = token.getShareRoot();
|
||||||
|
|
||||||
Response.Node me = Response.Node.from(
|
Response.Node me = Response.Node.from(
|
||||||
node,
|
node,
|
||||||
|
(shareRoot == null) || (shareRoot != node.id),
|
||||||
node.children.stream()
|
node.children.stream()
|
||||||
.map(n -> Response.Node.from(n, new Response.Node[0]))
|
.map(n -> Response.Node.from(n, true, new Response.Node[0]))
|
||||||
.toArray(Response.Node[]::new)
|
.toArray(Response.Node[]::new)
|
||||||
);
|
);
|
||||||
return Optional.of(me);
|
return Optional.of(me);
|
||||||
@ -35,32 +41,53 @@ public class Info {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/fs/path")
|
@PostMapping("/fs/path")
|
||||||
private @NonNull Optional<List<Response.PathSegment>> path(@AuthUser User user, @RequestBody @NonNull long id) {
|
private @NonNull Optional<List<Response.PathSegment>> path(@AuthUser Token token, @RequestBody @NonNull long id) {
|
||||||
|
User user = token.getUser();
|
||||||
try (AutoCloseLock ignored = new AutoCloseLock(user.nodeLock.readLock())) {
|
try (AutoCloseLock ignored = new AutoCloseLock(user.nodeLock.readLock())) {
|
||||||
Node node = user.nodes.get(id);
|
Node node = user.nodes.get(id);
|
||||||
if (node == null) return Optional.empty();
|
if (node == null || !FsUtils.shareHasAccess(token, node)) return Optional.empty();
|
||||||
|
|
||||||
ArrayList<Response.PathSegment> segments = new ArrayList<>();
|
ArrayList<Node> segments = new ArrayList<>();
|
||||||
while (node != null) {
|
while (node != null) {
|
||||||
segments.add(new Response.PathSegment(node.name, node.isFile ? null : node.id));
|
segments.add(node);
|
||||||
node = node.parent;
|
node = node.parent;
|
||||||
}
|
}
|
||||||
return Optional.of(segments.reversed());
|
Long shareRoot = token.getShareRoot();
|
||||||
|
if (shareRoot != null) {
|
||||||
|
while (!segments.isEmpty() && segments.getLast().id != shareRoot) {
|
||||||
|
segments.removeLast();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Optional.of(
|
||||||
|
segments.stream()
|
||||||
|
.map(n -> new Response.PathSegment(n.name, n.isFile ? null : n.id))
|
||||||
|
.toList()
|
||||||
|
.reversed()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/fs/size")
|
@PostMapping("/fs/size")
|
||||||
private long size(@AuthUser User user, @RequestBody @NonNull long[] ids) {
|
private long size(@AuthUser Token token, @RequestBody @NonNull long[] ids) {
|
||||||
|
User user = token.getUser();
|
||||||
try (AutoCloseLock ignored = new AutoCloseLock(user.nodeLock.readLock())) {
|
try (AutoCloseLock ignored = new AutoCloseLock(user.nodeLock.readLock())) {
|
||||||
|
if (token.isShare()) {
|
||||||
|
ids = Arrays.stream(ids)
|
||||||
|
.mapToObj(user.nodes::get)
|
||||||
|
.filter(n -> (n != null) && (FsUtils.shareHasAccess(token, n)))
|
||||||
|
.mapToLong(n -> n.id)
|
||||||
|
.toArray();
|
||||||
|
}
|
||||||
return FsUtils.nodesSize(user, ids);
|
return FsUtils.nodesSize(user, ids);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/fs/mime")
|
@PostMapping("/fs/mime")
|
||||||
private @NonNull Optional<String> mime(@AuthUser User user, @RequestBody @NonNull long id) {
|
private @NonNull Optional<String> mime(@AuthUser Token token, @RequestBody @NonNull long id) {
|
||||||
|
User user = token.getUser();
|
||||||
try (AutoCloseLock ignored = new AutoCloseLock(user.nodeLock.readLock())) {
|
try (AutoCloseLock ignored = new AutoCloseLock(user.nodeLock.readLock())) {
|
||||||
Node node = user.nodes.get(id);
|
Node node = user.nodes.get(id);
|
||||||
if (node == null || !node.isFile) return Optional.empty();
|
if (node == null || !node.isFile || !FsUtils.shareHasAccess(token, node)) return Optional.empty();
|
||||||
return MediaTypeFactory.getMediaType(node.name).map(MimeType::toString);
|
return MediaTypeFactory.getMediaType(node.name).map(MimeType::toString);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,10 +1,11 @@
|
|||||||
package de.mattv.fileserver.routes.fs;
|
package de.mattv.fileserver.routes.fs;
|
||||||
|
|
||||||
import de.mattv.fileserver.data.Node;
|
import de.mattv.fileserver.data.Node;
|
||||||
|
import de.mattv.fileserver.data.Token;
|
||||||
import de.mattv.fileserver.data.User;
|
import de.mattv.fileserver.data.User;
|
||||||
import de.mattv.fileserver.util.AuthUser;
|
import de.mattv.fileserver.util.AuthUser;
|
||||||
import de.mattv.fileserver.util.AutoCloseLock;
|
import de.mattv.fileserver.util.AutoCloseLock;
|
||||||
import de.mattv.fileserver.util.UserRestController;
|
import de.mattv.fileserver.util.UserOrShareRestController;
|
||||||
import de.mattv.fileserver.util.NonNull;
|
import de.mattv.fileserver.util.NonNull;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.apache.commons.codec.binary.Base64;
|
import org.apache.commons.codec.binary.Base64;
|
||||||
@ -16,13 +17,14 @@ import java.nio.file.Files;
|
|||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@UserRestController
|
@UserOrShareRestController
|
||||||
public class Preview {
|
public class Preview {
|
||||||
@PostMapping("/fs/preview")
|
@PostMapping("/fs/preview")
|
||||||
private @NonNull Optional<String> preview(@AuthUser User user, @RequestBody long id) {
|
private @NonNull Optional<String> preview(@AuthUser Token token, @RequestBody long id) {
|
||||||
|
User user = token.getUser();
|
||||||
try (AutoCloseLock ignored = new AutoCloseLock(user.nodeLock.readLock())) {
|
try (AutoCloseLock ignored = new AutoCloseLock(user.nodeLock.readLock())) {
|
||||||
Node node = user.nodes.get(id);
|
Node node = user.nodes.get(id);
|
||||||
if (node == null || !node.hasPreview) return Optional.empty();
|
if (node == null || !node.hasPreview || !FsUtils.shareHasAccess(token, node)) return Optional.empty();
|
||||||
byte[] content = Files.readAllBytes(node.previewFile.toPath());
|
byte[] content = Files.readAllBytes(node.previewFile.toPath());
|
||||||
return Optional.of(Base64.encodeBase64String(content));
|
return Optional.of(Base64.encodeBase64String(content));
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
|
37
src/main/java/de/mattv/fileserver/routes/fs/Share.java
Normal file
37
src/main/java/de/mattv/fileserver/routes/fs/Share.java
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
package de.mattv.fileserver.routes.fs;
|
||||||
|
|
||||||
|
import de.mattv.fileserver.data.Node;
|
||||||
|
import de.mattv.fileserver.data.Token;
|
||||||
|
import de.mattv.fileserver.data.User;
|
||||||
|
import de.mattv.fileserver.security.ShareService;
|
||||||
|
import de.mattv.fileserver.util.AuthUser;
|
||||||
|
import de.mattv.fileserver.util.AutoCloseLock;
|
||||||
|
import de.mattv.fileserver.util.NonNull;
|
||||||
|
import de.mattv.fileserver.util.UserRestController;
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
@UserRestController
|
||||||
|
public class Share {
|
||||||
|
@PostMapping("/fs/share")
|
||||||
|
private @NonNull Optional<String> share(@AuthUser Token token, @RequestBody @NonNull long id) {
|
||||||
|
User user = token.getUser();
|
||||||
|
try (AutoCloseLock ignored = new AutoCloseLock(user.nodeLock.readLock())) {
|
||||||
|
Node node = user.nodes.get(id);
|
||||||
|
if (node == null) return Optional.empty();
|
||||||
|
return Optional.of(ShareService.createShare(user, node));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/fs/unshare")
|
||||||
|
private void path(@AuthUser Token token, @RequestBody @NonNull long id) {
|
||||||
|
User user = token.getUser();
|
||||||
|
try (AutoCloseLock ignored = new AutoCloseLock(user.nodeLock.readLock())) {
|
||||||
|
Node node = user.nodes.get(id);
|
||||||
|
if (node != null)
|
||||||
|
ShareService.removeShare(node);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -30,9 +30,10 @@ public class Config {
|
|||||||
return http
|
return http
|
||||||
.addFilter(filter)
|
.addFilter(filter)
|
||||||
.authenticationManager(manager)
|
.authenticationManager(manager)
|
||||||
.securityMatcher("/api/user/**", "/api/admin/**")
|
.securityMatcher("/api/user_share/**", "/api/user/**", "/api/admin/**")
|
||||||
.authorizeHttpRequests(auth -> {
|
.authorizeHttpRequests(auth -> {
|
||||||
auth.requestMatchers("/api/admin/**").hasRole("ADMIN");
|
auth.requestMatchers("/api/admin/**").hasRole("ADMIN");
|
||||||
|
auth.requestMatchers("/api/user/**").hasRole("USER");
|
||||||
auth.anyRequest().authenticated();
|
auth.anyRequest().authenticated();
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
@ -12,8 +12,11 @@ public class Filter extends AbstractPreAuthenticatedProcessingFilter {
|
|||||||
@Override
|
@Override
|
||||||
protected Object getPreAuthenticatedCredentials(HttpServletRequest request) {
|
protected Object getPreAuthenticatedCredentials(HttpServletRequest request) {
|
||||||
String auth = request.getHeader("Authorization");
|
String auth = request.getHeader("Authorization");
|
||||||
if (auth == null || !auth.startsWith("Bearer "))
|
if (auth == null)
|
||||||
return null;
|
return null;
|
||||||
|
if (auth.startsWith("Bearer "))
|
||||||
return TokenService.getToken(auth.substring(7));
|
return TokenService.getToken(auth.substring(7));
|
||||||
|
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
45
src/main/java/de/mattv/fileserver/security/ShareService.java
Normal file
45
src/main/java/de/mattv/fileserver/security/ShareService.java
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
package de.mattv.fileserver.security;
|
||||||
|
|
||||||
|
import de.mattv.fileserver.data.Data;
|
||||||
|
import de.mattv.fileserver.data.Node;
|
||||||
|
import de.mattv.fileserver.data.User;
|
||||||
|
import de.mattv.fileserver.util.NonNull;
|
||||||
|
import org.apache.commons.lang3.RandomStringUtils;
|
||||||
|
import org.apache.commons.lang3.tuple.Pair;
|
||||||
|
import org.springframework.lang.Nullable;
|
||||||
|
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
|
public class ShareService {
|
||||||
|
private static final ConcurrentHashMap<String, Pair<User, Node>> SHARES = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
public static void init() {
|
||||||
|
for (User user : Data.USERS.values()) {
|
||||||
|
for (Node node : user.nodes.values()) {
|
||||||
|
if (node.shareString != null) {
|
||||||
|
SHARES.put(node.shareString, Pair.of(user, node));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static @Nullable Pair<User, Node> getShare(@NonNull String token) {
|
||||||
|
return SHARES.get(token);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String createShare(User user, Node node) {
|
||||||
|
if (node.shareString != null)
|
||||||
|
return node.shareString;
|
||||||
|
|
||||||
|
String name = RandomStringUtils.random(TokenService.SHARE_TOKEN_LENGTH, true, true);
|
||||||
|
SHARES.put(name, Pair.of(user, node));
|
||||||
|
node.shareString = name;
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void removeShare(Node node) {
|
||||||
|
if (node.shareString == null) return;
|
||||||
|
SHARES.remove(node.shareString);
|
||||||
|
node.shareString = null;
|
||||||
|
}
|
||||||
|
}
|
@ -12,6 +12,8 @@ import java.util.concurrent.TimeUnit;
|
|||||||
|
|
||||||
public class TokenService {
|
public class TokenService {
|
||||||
private static final ConcurrentHashMap<String, Token> TOKENS = new ConcurrentHashMap<>();
|
private static final ConcurrentHashMap<String, Token> TOKENS = new ConcurrentHashMap<>();
|
||||||
|
public static final int TOKEN_LENGTH = 32;
|
||||||
|
public static final int SHARE_TOKEN_LENGTH = 30;
|
||||||
|
|
||||||
@Scheduled(fixedRate = 2, timeUnit = TimeUnit.HOURS)
|
@Scheduled(fixedRate = 2, timeUnit = TimeUnit.HOURS)
|
||||||
private static void cleanExpiredTokens() {
|
private static void cleanExpiredTokens() {
|
||||||
@ -19,20 +21,26 @@ public class TokenService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static String createToken(@NonNull User user) {
|
public static String createToken(@NonNull User user) {
|
||||||
String token = RandomStringUtils.random(32, true, true);
|
String token = RandomStringUtils.random(TOKEN_LENGTH, true, true);
|
||||||
TOKENS.put(token, new Token(token, user));
|
TOKENS.put(token, new Token(token, user, null));
|
||||||
return token;
|
return token;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static @Nullable Token getToken(@NonNull String token) {
|
public static @Nullable Token getToken(@NonNull String token) {
|
||||||
|
if (token.length() == TOKEN_LENGTH) {
|
||||||
Token found = TOKENS.get(token);
|
Token found = TOKENS.get(token);
|
||||||
if (found != null && found.expired()) {
|
if (found == null) return null;
|
||||||
|
if (found.expired()) {
|
||||||
TOKENS.remove(token);
|
TOKENS.remove(token);
|
||||||
found = null;
|
return null;
|
||||||
} else if (found != null) {
|
|
||||||
found.refresh();
|
|
||||||
}
|
}
|
||||||
|
found.refresh();
|
||||||
return found;
|
return found;
|
||||||
|
} else if (token.length() == SHARE_TOKEN_LENGTH) {
|
||||||
|
var share = ShareService.getShare(token);
|
||||||
|
if (share == null) return null;
|
||||||
|
return new Token(token, share.getLeft(), share.getRight().id);
|
||||||
|
} else return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void logout(@NonNull Token token) {
|
public static void logout(@NonNull Token token) {
|
||||||
|
@ -0,0 +1,14 @@
|
|||||||
|
package de.mattv.fileserver.util;
|
||||||
|
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
|
||||||
|
import java.lang.annotation.ElementType;
|
||||||
|
import java.lang.annotation.Retention;
|
||||||
|
import java.lang.annotation.RetentionPolicy;
|
||||||
|
import java.lang.annotation.Target;
|
||||||
|
|
||||||
|
@Target(ElementType.TYPE)
|
||||||
|
@Retention(RetentionPolicy.RUNTIME)
|
||||||
|
@SecuredRestController
|
||||||
|
@RequestMapping("/api/user_share")
|
||||||
|
public @interface UserOrShareRestController {}
|
Loading…
x
Reference in New Issue
Block a user