Rewrote Frontend
This commit is contained in:
		@@ -1,15 +1,15 @@
 | 
			
		||||
/* eslint-env node */
 | 
			
		||||
require("@rushstack/eslint-patch/modern-module-resolution");
 | 
			
		||||
require('@rushstack/eslint-patch/modern-module-resolution');
 | 
			
		||||
 | 
			
		||||
module.exports = {
 | 
			
		||||
  root: true,
 | 
			
		||||
  extends: [
 | 
			
		||||
    "plugin:vue/vue3-essential",
 | 
			
		||||
    "eslint:recommended",
 | 
			
		||||
    "@vue/eslint-config-typescript/recommended",
 | 
			
		||||
    "@vue/eslint-config-prettier",
 | 
			
		||||
  ],
 | 
			
		||||
  parserOptions: {
 | 
			
		||||
    ecmaVersion: "latest",
 | 
			
		||||
  },
 | 
			
		||||
	root: true,
 | 
			
		||||
	extends: [
 | 
			
		||||
		'plugin:vue/vue3-essential',
 | 
			
		||||
		'eslint:recommended',
 | 
			
		||||
		'@vue/eslint-config-typescript/recommended',
 | 
			
		||||
		'@vue/eslint-config-prettier'
 | 
			
		||||
	],
 | 
			
		||||
	parserOptions: {
 | 
			
		||||
		ecmaVersion: 'latest'
 | 
			
		||||
	}
 | 
			
		||||
};
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										7
									
								
								frontend/.prettierrc
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								frontend/.prettierrc
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,7 @@
 | 
			
		||||
{
 | 
			
		||||
    "tabWidth": 4,
 | 
			
		||||
    "useTabs": true,
 | 
			
		||||
    "singleQuote": true,
 | 
			
		||||
    "trailingComma": "none",
 | 
			
		||||
    "endOfLine": "lf"
 | 
			
		||||
}
 | 
			
		||||
@@ -2,9 +2,9 @@
 | 
			
		||||
<html lang="en">
 | 
			
		||||
  <head>
 | 
			
		||||
    <meta charset="UTF-8" />
 | 
			
		||||
    <link rel="icon" href="/favicon.ico" />
 | 
			
		||||
    <link rel="icon" href="/favicon.svg" />
 | 
			
		||||
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
 | 
			
		||||
    <title>Vite App</title>
 | 
			
		||||
    <title>MFileserver</title>
 | 
			
		||||
  </head>
 | 
			
		||||
  <body>
 | 
			
		||||
    <div id="app"></div>
 | 
			
		||||
 
 | 
			
		||||
@@ -4,16 +4,16 @@
 | 
			
		||||
  "private": true,
 | 
			
		||||
  "license": "suck my dick",
 | 
			
		||||
  "scripts": {
 | 
			
		||||
    "dev": "vite build --outDir ../run/static --watch",
 | 
			
		||||
    "dev": "vite build -c vite.dev.config.ts --mode development",
 | 
			
		||||
    "build": "run-p type-check build-only",
 | 
			
		||||
    "build-only": "vite build",
 | 
			
		||||
    "type-check": "vue-tsc --noEmit",
 | 
			
		||||
    "lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore"
 | 
			
		||||
  },
 | 
			
		||||
  "dependencies": {
 | 
			
		||||
    "@vicons/carbon": "^0.12.0",
 | 
			
		||||
    "@vicons/ionicons5": "^0.12.0",
 | 
			
		||||
    "axios": "^0.27.2",
 | 
			
		||||
    "class-transformer": "^0.5.1",
 | 
			
		||||
    "class-validator": "^0.13.2",
 | 
			
		||||
    "filesize": "^9.0.11",
 | 
			
		||||
    "jwt-decode": "^3.1.2",
 | 
			
		||||
    "naive-ui": "^2.32.1",
 | 
			
		||||
@@ -26,6 +26,7 @@
 | 
			
		||||
    "@rushstack/eslint-patch": "^1.1.4",
 | 
			
		||||
    "@types/node": "^18.7.14",
 | 
			
		||||
    "@vitejs/plugin-vue": "^3.0.1",
 | 
			
		||||
    "@vitejs/plugin-vue-jsx": "^2.0.0",
 | 
			
		||||
    "@vue/eslint-config-prettier": "^7.0.0",
 | 
			
		||||
    "@vue/eslint-config-typescript": "^11.0.0",
 | 
			
		||||
    "@vue/tsconfig": "^0.1.3",
 | 
			
		||||
 
 | 
			
		||||
										
											Binary file not shown.
										
									
								
							| 
		 Before Width: | Height: | Size: 4.2 KiB  | 
							
								
								
									
										1
									
								
								frontend/public/favicon.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								frontend/public/favicon.svg
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1 @@
 | 
			
		||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 32 32"><path d="M28 20h-2v2h2v6H4v-6h2v-2H4a2.002 2.002 0 0 0-2 2v6a2.002 2.002 0 0 0 2 2h24a2.002 2.002 0 0 0 2-2v-6a2.002 2.002 0 0 0-2-2z" fill="currentColor"></path><circle cx="7" cy="25" r="1" fill="currentColor"></circle><path d="M22.707 7.293l-5-5A1 1 0 0 0 17 2h-6a2.002 2.002 0 0 0-2 2v16a2.002 2.002 0 0 0 2 2h10a2.002 2.002 0 0 0 2-2V8a1 1 0 0 0-.293-.707zM20.586 8H17V4.414zM11 20V4h4v4a2.002 2.002 0 0 0 2 2h4v10z" fill="currentColor"></path></svg>
 | 
			
		||||
| 
		 After Width: | Height: | Size: 557 B  | 
@@ -1,62 +1,113 @@
 | 
			
		||||
<script setup async lang="ts">
 | 
			
		||||
import { provide, ref } from "vue";
 | 
			
		||||
import { useRouter } from "vue-router";
 | 
			
		||||
import type { TokenInjectType } from "@/api";
 | 
			
		||||
import type { MenuOption } from 'naive-ui';
 | 
			
		||||
import { provide, ref, h } from 'vue';
 | 
			
		||||
import { useRouter, RouterLink } from 'vue-router';
 | 
			
		||||
import type { TokenInjectType } from '@/api';
 | 
			
		||||
import { useMessage, NMenu, NPageHeader, NIcon } from 'naive-ui';
 | 
			
		||||
import { BareMetalServer02 } from '@vicons/carbon';
 | 
			
		||||
 | 
			
		||||
const router = useRouter();
 | 
			
		||||
const message = useMessage();
 | 
			
		||||
 | 
			
		||||
const jwt = ref<string | null>(localStorage.getItem("token"));
 | 
			
		||||
const jwt = ref<string | null>(localStorage.getItem('token'));
 | 
			
		||||
 | 
			
		||||
function setToken(token: string) {
 | 
			
		||||
  jwt.value = token;
 | 
			
		||||
  localStorage.setItem("token", token);
 | 
			
		||||
	jwt.value = token;
 | 
			
		||||
	localStorage.setItem('token', token);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function logout() {
 | 
			
		||||
  jwt.value = null;
 | 
			
		||||
  localStorage.removeItem("token");
 | 
			
		||||
  router.push({ name: "login" });
 | 
			
		||||
	jwt.value = null;
 | 
			
		||||
	localStorage.removeItem('token');
 | 
			
		||||
	router.push({ name: 'login' });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
provide<TokenInjectType>("jwt", {
 | 
			
		||||
  jwt,
 | 
			
		||||
  setToken,
 | 
			
		||||
  logout,
 | 
			
		||||
provide<TokenInjectType>('jwt', {
 | 
			
		||||
	jwt,
 | 
			
		||||
	setToken,
 | 
			
		||||
	logout
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
router.afterEach(() => message.destroyAll());
 | 
			
		||||
 | 
			
		||||
function handleUpdateValue(key: string) {
 | 
			
		||||
	if (key === 'login') logout();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const menuOptions: MenuOption[] = [
 | 
			
		||||
	{
 | 
			
		||||
		label: () =>
 | 
			
		||||
			h(
 | 
			
		||||
				RouterLink,
 | 
			
		||||
				{
 | 
			
		||||
					to: '/'
 | 
			
		||||
				},
 | 
			
		||||
				{ default: () => 'Files' }
 | 
			
		||||
			),
 | 
			
		||||
		key: 'fs'
 | 
			
		||||
	},
 | 
			
		||||
	{
 | 
			
		||||
		label: () =>
 | 
			
		||||
			h(
 | 
			
		||||
				RouterLink,
 | 
			
		||||
				{
 | 
			
		||||
					to: '/profile'
 | 
			
		||||
				},
 | 
			
		||||
				{ default: () => 'Profile' }
 | 
			
		||||
			),
 | 
			
		||||
		key: 'profile'
 | 
			
		||||
	},
 | 
			
		||||
	{
 | 
			
		||||
		label: () =>
 | 
			
		||||
			h(
 | 
			
		||||
				RouterLink,
 | 
			
		||||
				{
 | 
			
		||||
					to: '/login'
 | 
			
		||||
				},
 | 
			
		||||
				{ default: () => 'Logout' }
 | 
			
		||||
			),
 | 
			
		||||
		key: 'login'
 | 
			
		||||
	}
 | 
			
		||||
];
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
  <nav>
 | 
			
		||||
    <template v-if="jwt != null">
 | 
			
		||||
      <router-link to="/">Files</router-link>
 | 
			
		||||
      <span style="margin-left: 2em" />
 | 
			
		||||
      <router-link to="/profile">Profile</router-link>
 | 
			
		||||
      <span style="margin-left: 2em" />
 | 
			
		||||
      <router-link to="/login" @click="logout()">Logout</router-link>
 | 
			
		||||
    </template>
 | 
			
		||||
  </nav>
 | 
			
		||||
  <router-view />
 | 
			
		||||
	<n-page-header style="margin-bottom: 3em">
 | 
			
		||||
		<template #title>
 | 
			
		||||
			<n-icon class="nav-icon" size="1.5em">
 | 
			
		||||
				<BareMetalServer02 />
 | 
			
		||||
			</n-icon>
 | 
			
		||||
			MFileserver
 | 
			
		||||
		</template>
 | 
			
		||||
		<template #extra>
 | 
			
		||||
			<n-menu
 | 
			
		||||
				v-if="jwt != null"
 | 
			
		||||
				mode="horizontal"
 | 
			
		||||
				:options="menuOptions"
 | 
			
		||||
				@update:value="handleUpdateValue"
 | 
			
		||||
			/>
 | 
			
		||||
		</template>
 | 
			
		||||
	</n-page-header>
 | 
			
		||||
	<router-view />
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<style lang="scss">
 | 
			
		||||
#app {
 | 
			
		||||
  font-family: Avenir, Helvetica, Arial, sans-serif;
 | 
			
		||||
  -webkit-font-smoothing: antialiased;
 | 
			
		||||
  -moz-osx-font-smoothing: grayscale;
 | 
			
		||||
  text-align: center;
 | 
			
		||||
  color: #2c3e50;
 | 
			
		||||
body {
 | 
			
		||||
	height: 100%;
 | 
			
		||||
	padding: 2em;
 | 
			
		||||
	display: flex;
 | 
			
		||||
	justify-content: center;
 | 
			
		||||
	align-content: center;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
nav {
 | 
			
		||||
  padding: 30px;
 | 
			
		||||
#app {
 | 
			
		||||
	font-family: Avenir, Helvetica, Arial, sans-serif;
 | 
			
		||||
	-webkit-font-smoothing: antialiased;
 | 
			
		||||
	-moz-osx-font-smoothing: grayscale;
 | 
			
		||||
	color: #2c3e50;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
  a {
 | 
			
		||||
    font-weight: bold;
 | 
			
		||||
    color: #2c3e50;
 | 
			
		||||
 | 
			
		||||
    &.router-link-exact-active {
 | 
			
		||||
      color: #42b983;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
.nav-icon {
 | 
			
		||||
	top: 0.25em;
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
 
 | 
			
		||||
@@ -1,12 +1,17 @@
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
import App from "./App.vue";
 | 
			
		||||
import App from './App.vue';
 | 
			
		||||
import { NSpin, NMessageProvider, NDialogProvider } from 'naive-ui';
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
  <Suspense>
 | 
			
		||||
    <App></App>
 | 
			
		||||
    <template #fallback>
 | 
			
		||||
      <div>Loading...</div>
 | 
			
		||||
    </template>
 | 
			
		||||
  </Suspense>
 | 
			
		||||
	<Suspense>
 | 
			
		||||
		<n-message-provider :closable="true" :duration="5000">
 | 
			
		||||
			<n-dialog-provider>
 | 
			
		||||
				<App />
 | 
			
		||||
			</n-dialog-provider>
 | 
			
		||||
		</n-message-provider>
 | 
			
		||||
		<template #fallback>
 | 
			
		||||
			<div><n-spin size="small" />Loading...</div>
 | 
			
		||||
		</template>
 | 
			
		||||
	</Suspense>
 | 
			
		||||
</template>
 | 
			
		||||
 
 | 
			
		||||
@@ -1,54 +1,55 @@
 | 
			
		||||
import { Requests, Responses, UserRole, get_token, post_token } from "./base";
 | 
			
		||||
import type { Requests, Responses } from '@/dto';
 | 
			
		||||
import { UserRole, get_token, post_token } from './base';
 | 
			
		||||
 | 
			
		||||
export const get_users = (token: string): Promise<Responses.Admin.GetUsers> =>
 | 
			
		||||
  get_token("/api/admin/users", token);
 | 
			
		||||
export const get_users = (token: string): Promise<Responses.GetUsers> =>
 | 
			
		||||
	get_token('/api/admin/users', token);
 | 
			
		||||
 | 
			
		||||
export const set_role = (
 | 
			
		||||
  user: number,
 | 
			
		||||
  role: UserRole,
 | 
			
		||||
  token: string
 | 
			
		||||
): Promise<Responses.Admin.SetUserRole | Responses.ErrorResponse> =>
 | 
			
		||||
  post_token<Requests.Admin.SetUserRole>(
 | 
			
		||||
    "/api/admin/set_role",
 | 
			
		||||
    {
 | 
			
		||||
      user,
 | 
			
		||||
      role,
 | 
			
		||||
    },
 | 
			
		||||
    token
 | 
			
		||||
  );
 | 
			
		||||
	user: number,
 | 
			
		||||
	role: UserRole,
 | 
			
		||||
	token: string
 | 
			
		||||
): Promise<Responses.Success | Responses.Error> =>
 | 
			
		||||
	post_token<Requests.SetUserRole>(
 | 
			
		||||
		'/api/admin/set_role',
 | 
			
		||||
		{
 | 
			
		||||
			user,
 | 
			
		||||
			role
 | 
			
		||||
		},
 | 
			
		||||
		token
 | 
			
		||||
	);
 | 
			
		||||
 | 
			
		||||
export const logout = (
 | 
			
		||||
  user: number,
 | 
			
		||||
  token: string
 | 
			
		||||
): Promise<Responses.Admin.LogoutAllUser | Responses.ErrorResponse> =>
 | 
			
		||||
  post_token<Requests.Admin.LogoutAll>(
 | 
			
		||||
    "/api/admin/logout",
 | 
			
		||||
    {
 | 
			
		||||
      user,
 | 
			
		||||
    },
 | 
			
		||||
    token
 | 
			
		||||
  );
 | 
			
		||||
	user: number,
 | 
			
		||||
	token: string
 | 
			
		||||
): Promise<Responses.Success | Responses.Error> =>
 | 
			
		||||
	post_token<Requests.Admin>(
 | 
			
		||||
		'/api/admin/logout',
 | 
			
		||||
		{
 | 
			
		||||
			user
 | 
			
		||||
		},
 | 
			
		||||
		token
 | 
			
		||||
	);
 | 
			
		||||
 | 
			
		||||
export const delete_user = (
 | 
			
		||||
  user: number,
 | 
			
		||||
  token: string
 | 
			
		||||
): Promise<Responses.Admin.DeleteUser | Responses.ErrorResponse> =>
 | 
			
		||||
  post_token<Requests.Admin.DeleteUser>(
 | 
			
		||||
    "/api/admin/delete",
 | 
			
		||||
    {
 | 
			
		||||
      user,
 | 
			
		||||
    },
 | 
			
		||||
    token
 | 
			
		||||
  );
 | 
			
		||||
	user: number,
 | 
			
		||||
	token: string
 | 
			
		||||
): Promise<Responses.Success | Responses.Error> =>
 | 
			
		||||
	post_token<Requests.Admin>(
 | 
			
		||||
		'/api/admin/delete',
 | 
			
		||||
		{
 | 
			
		||||
			user
 | 
			
		||||
		},
 | 
			
		||||
		token
 | 
			
		||||
	);
 | 
			
		||||
 | 
			
		||||
export const disable_tfa = (
 | 
			
		||||
  user: number,
 | 
			
		||||
  token: string
 | 
			
		||||
): Promise<Responses.Admin.DisableTfa | Responses.ErrorResponse> =>
 | 
			
		||||
  post_token<Requests.Admin.DisableTfa>(
 | 
			
		||||
    "/api/admin/disable_2fa",
 | 
			
		||||
    {
 | 
			
		||||
      user,
 | 
			
		||||
    },
 | 
			
		||||
    token
 | 
			
		||||
  );
 | 
			
		||||
	user: number,
 | 
			
		||||
	token: string
 | 
			
		||||
): Promise<Responses.Success | Responses.Error> =>
 | 
			
		||||
	post_token<Requests.Admin>(
 | 
			
		||||
		'/api/admin/disable_2fa',
 | 
			
		||||
		{
 | 
			
		||||
			user
 | 
			
		||||
		},
 | 
			
		||||
		token
 | 
			
		||||
	);
 | 
			
		||||
 
 | 
			
		||||
@@ -1,93 +1,86 @@
 | 
			
		||||
import { Responses, Requests, post, post_token } from "./base";
 | 
			
		||||
import type { Requests, Responses } from '@/dto';
 | 
			
		||||
import { post, post_token } from './base';
 | 
			
		||||
 | 
			
		||||
export const auth_login = (
 | 
			
		||||
  username: string,
 | 
			
		||||
  password: string,
 | 
			
		||||
  otp?: string
 | 
			
		||||
): Promise<
 | 
			
		||||
  | Responses.Auth.LoginResponse
 | 
			
		||||
  | Responses.Auth.TfaRequiredResponse
 | 
			
		||||
  | Responses.ErrorResponse
 | 
			
		||||
> =>
 | 
			
		||||
  post<Requests.Auth.LoginRequest>("/api/auth/login", {
 | 
			
		||||
    username: username,
 | 
			
		||||
    password: password,
 | 
			
		||||
    otp: otp,
 | 
			
		||||
  });
 | 
			
		||||
	username: string,
 | 
			
		||||
	password: string,
 | 
			
		||||
	otp?: string
 | 
			
		||||
): Promise<Responses.Login | Responses.Success | Responses.Error> =>
 | 
			
		||||
	post<Requests.Login>('/api/auth/login', {
 | 
			
		||||
		username: username,
 | 
			
		||||
		password: password,
 | 
			
		||||
		otp: otp
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
export const auth_signup = (
 | 
			
		||||
  username: string,
 | 
			
		||||
  password: string
 | 
			
		||||
): Promise<Responses.Auth.SignupResponse | Responses.ErrorResponse> =>
 | 
			
		||||
  post<Requests.Auth.SignUpRequest>("/api/auth/signup", {
 | 
			
		||||
    username: username,
 | 
			
		||||
    password: password,
 | 
			
		||||
  });
 | 
			
		||||
	username: string,
 | 
			
		||||
	password: string
 | 
			
		||||
): Promise<Responses.Success | Responses.Error> =>
 | 
			
		||||
	post<Requests.SignUp>('/api/auth/signup', {
 | 
			
		||||
		username: username,
 | 
			
		||||
		password: password
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
export const refresh_token = (
 | 
			
		||||
  token: string
 | 
			
		||||
): Promise<Responses.Auth.RefreshResponse | Responses.ErrorResponse> =>
 | 
			
		||||
  post_token("/api/auth/refresh", {}, token);
 | 
			
		||||
	token: string
 | 
			
		||||
): Promise<Responses.Login | Responses.Error> =>
 | 
			
		||||
	post_token('/api/auth/refresh', {}, token);
 | 
			
		||||
 | 
			
		||||
export const change_password = (
 | 
			
		||||
  oldPw: string,
 | 
			
		||||
  newPw: string,
 | 
			
		||||
  token: string
 | 
			
		||||
): Promise<Responses.Auth.ChangePasswordResponse | Responses.ErrorResponse> =>
 | 
			
		||||
  post_token<Requests.Auth.ChangePasswordRequest>(
 | 
			
		||||
    "/api/auth/change_password",
 | 
			
		||||
    {
 | 
			
		||||
      oldPassword: oldPw,
 | 
			
		||||
      newPassword: newPw,
 | 
			
		||||
    },
 | 
			
		||||
    token
 | 
			
		||||
  );
 | 
			
		||||
	oldPw: string,
 | 
			
		||||
	newPw: string,
 | 
			
		||||
	token: string
 | 
			
		||||
): Promise<Responses.Success | Responses.Error> =>
 | 
			
		||||
	post_token<Requests.ChangePassword>(
 | 
			
		||||
		'/api/auth/change_password',
 | 
			
		||||
		{
 | 
			
		||||
			oldPassword: oldPw,
 | 
			
		||||
			newPassword: newPw
 | 
			
		||||
		},
 | 
			
		||||
		token
 | 
			
		||||
	);
 | 
			
		||||
 | 
			
		||||
export const logout_all = (
 | 
			
		||||
  token: string
 | 
			
		||||
): Promise<Responses.Auth.LogoutAllResponse | Responses.ErrorResponse> =>
 | 
			
		||||
  post_token("/api/auth/logout_all", {}, token);
 | 
			
		||||
	token: string
 | 
			
		||||
): Promise<Responses.Success | Responses.Error> =>
 | 
			
		||||
	post_token('/api/auth/logout_all', {}, token);
 | 
			
		||||
 | 
			
		||||
export function tfa_setup(
 | 
			
		||||
  mail: false,
 | 
			
		||||
  token: string
 | 
			
		||||
): Promise<Responses.Auth.RequestTotpTfaResponse | Responses.ErrorResponse>;
 | 
			
		||||
	mail: false,
 | 
			
		||||
	token: string
 | 
			
		||||
): Promise<Responses.RequestsTotpTfa | Responses.Error>;
 | 
			
		||||
export function tfa_setup(
 | 
			
		||||
  mail: true,
 | 
			
		||||
  token: string
 | 
			
		||||
): Promise<Responses.Auth.RequestEmailTfaResponse | Responses.ErrorResponse>;
 | 
			
		||||
	mail: true,
 | 
			
		||||
	token: string
 | 
			
		||||
): Promise<Responses.Success | Responses.Error>;
 | 
			
		||||
export function tfa_setup(
 | 
			
		||||
  mail: boolean,
 | 
			
		||||
  token: string
 | 
			
		||||
): Promise<
 | 
			
		||||
  | Responses.Auth.RequestEmailTfaResponse
 | 
			
		||||
  | Responses.Auth.RequestTotpTfaResponse
 | 
			
		||||
  | Responses.ErrorResponse
 | 
			
		||||
> {
 | 
			
		||||
  return post_token<Requests.Auth.TfaSetup>(
 | 
			
		||||
    "/api/auth/2fa/setup",
 | 
			
		||||
    {
 | 
			
		||||
      mail,
 | 
			
		||||
    },
 | 
			
		||||
    token
 | 
			
		||||
  );
 | 
			
		||||
	mail: boolean,
 | 
			
		||||
	token: string
 | 
			
		||||
): Promise<Responses.Success | Responses.RequestsTotpTfa | Responses.Error> {
 | 
			
		||||
	return post_token<Requests.TfaSetup>(
 | 
			
		||||
		'/api/auth/2fa/setup',
 | 
			
		||||
		{
 | 
			
		||||
			mail
 | 
			
		||||
		},
 | 
			
		||||
		token
 | 
			
		||||
	);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const tfa_complete = (
 | 
			
		||||
  mail: boolean,
 | 
			
		||||
  code: string,
 | 
			
		||||
  token: string
 | 
			
		||||
): Promise<Responses.Auth.TfaCompletedResponse | Responses.ErrorResponse> =>
 | 
			
		||||
  post_token<Requests.Auth.TfaComplete>(
 | 
			
		||||
    "/api/auth/2fa/complete",
 | 
			
		||||
    {
 | 
			
		||||
      mail,
 | 
			
		||||
      code,
 | 
			
		||||
    },
 | 
			
		||||
    token
 | 
			
		||||
  );
 | 
			
		||||
	mail: boolean,
 | 
			
		||||
	code: string,
 | 
			
		||||
	token: string
 | 
			
		||||
): Promise<Responses.Success | Responses.Error> =>
 | 
			
		||||
	post_token<Requests.TfaComplete>(
 | 
			
		||||
		'/api/auth/2fa/complete',
 | 
			
		||||
		{
 | 
			
		||||
			mail,
 | 
			
		||||
			code
 | 
			
		||||
		},
 | 
			
		||||
		token
 | 
			
		||||
	);
 | 
			
		||||
 | 
			
		||||
export const tfa_disable = (
 | 
			
		||||
  token: string
 | 
			
		||||
): Promise<Responses.Auth.RemoveTfaResponse | Responses.ErrorResponse> =>
 | 
			
		||||
  post_token("/api/auth/2fa/disable", {}, token);
 | 
			
		||||
	token: string
 | 
			
		||||
): Promise<Responses.Success | Responses.Error> =>
 | 
			
		||||
	post_token('/api/auth/2fa/disable', {}, token);
 | 
			
		||||
 
 | 
			
		||||
@@ -1,62 +1,62 @@
 | 
			
		||||
import axios from "axios";
 | 
			
		||||
import { Requests, Responses, UserRole } from "../dto";
 | 
			
		||||
export { Requests, Responses, UserRole };
 | 
			
		||||
import axios from 'axios';
 | 
			
		||||
import type { Requests, Responses, UploadFile } from '@/dto';
 | 
			
		||||
import { UserRole } from '@/dto';
 | 
			
		||||
export { Requests, Responses, UserRole, UploadFile };
 | 
			
		||||
 | 
			
		||||
export const post = <T extends Requests.BaseRequest>(url: string, data: T) =>
 | 
			
		||||
  axios
 | 
			
		||||
    .post(url, data, {
 | 
			
		||||
      headers: { "Content-type": "application/json" },
 | 
			
		||||
    })
 | 
			
		||||
    .then((res) => res.data)
 | 
			
		||||
    .catch((err) => err.response.data);
 | 
			
		||||
export const post = <T extends Requests.Base>(url: string, data: T) =>
 | 
			
		||||
	axios
 | 
			
		||||
		.post(url, data, {
 | 
			
		||||
			headers: { 'Content-type': 'application/json' }
 | 
			
		||||
		})
 | 
			
		||||
		.then((res) => res.data)
 | 
			
		||||
		.catch((err) => err.response.data);
 | 
			
		||||
 | 
			
		||||
export const post_token = <T extends Requests.BaseRequest>(
 | 
			
		||||
  url: string,
 | 
			
		||||
  data: T,
 | 
			
		||||
  token: string
 | 
			
		||||
export const post_token = <T extends Requests.Base>(
 | 
			
		||||
	url: string,
 | 
			
		||||
	data: T,
 | 
			
		||||
	token: string
 | 
			
		||||
) =>
 | 
			
		||||
  axios
 | 
			
		||||
    .post(url, data, {
 | 
			
		||||
      headers: {
 | 
			
		||||
        Authorization: "Bearer " + token,
 | 
			
		||||
        "Content-type": "application/json",
 | 
			
		||||
      },
 | 
			
		||||
    })
 | 
			
		||||
    .then((res) => res.data)
 | 
			
		||||
    .catch((err) => err.response.data);
 | 
			
		||||
	axios
 | 
			
		||||
		.post(url, data, {
 | 
			
		||||
			headers: {
 | 
			
		||||
				Authorization: 'Bearer ' + token,
 | 
			
		||||
				'Content-type': 'application/json'
 | 
			
		||||
			}
 | 
			
		||||
		})
 | 
			
		||||
		.then((res) => res.data)
 | 
			
		||||
		.catch((err) => err.response.data);
 | 
			
		||||
 | 
			
		||||
export const post_token_form = (
 | 
			
		||||
  url: string,
 | 
			
		||||
  data: FormData,
 | 
			
		||||
  token: string,
 | 
			
		||||
  onProgress: (progressEvent: ProgressEvent) => void
 | 
			
		||||
	url: string,
 | 
			
		||||
	data: FormData,
 | 
			
		||||
	token: string,
 | 
			
		||||
	onProgress: (progressEvent: ProgressEvent) => void
 | 
			
		||||
) =>
 | 
			
		||||
  axios
 | 
			
		||||
    .post(url, data, {
 | 
			
		||||
      headers: {
 | 
			
		||||
        Authorization: "Bearer " + token,
 | 
			
		||||
        "Content-type": "multipart/form-data",
 | 
			
		||||
      },
 | 
			
		||||
      onUploadProgress: onProgress,
 | 
			
		||||
    })
 | 
			
		||||
    .then((res) => res.data)
 | 
			
		||||
    .catch((err) => err.response.data);
 | 
			
		||||
	axios
 | 
			
		||||
		.post(url, data, {
 | 
			
		||||
			headers: {
 | 
			
		||||
				Authorization: 'Bearer ' + token,
 | 
			
		||||
				'Content-type': 'multipart/form-data'
 | 
			
		||||
			},
 | 
			
		||||
			onUploadProgress: onProgress
 | 
			
		||||
		})
 | 
			
		||||
		.then((res) => res.data)
 | 
			
		||||
		.catch((err) => err.response.data);
 | 
			
		||||
 | 
			
		||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
 | 
			
		||||
export const get = (url: string) =>
 | 
			
		||||
  axios
 | 
			
		||||
    .get(url)
 | 
			
		||||
    .then((res) => res.data)
 | 
			
		||||
    .catch((err) => err.response.data);
 | 
			
		||||
	axios
 | 
			
		||||
		.get(url)
 | 
			
		||||
		.then((res) => res.data)
 | 
			
		||||
		.catch((err) => err.response.data);
 | 
			
		||||
 | 
			
		||||
export const get_token = (url: string, token: string) =>
 | 
			
		||||
  axios
 | 
			
		||||
    .get(url, {
 | 
			
		||||
      headers: { Authorization: "Bearer " + token },
 | 
			
		||||
    })
 | 
			
		||||
    .then((res) => res.data)
 | 
			
		||||
    .catch((err) => err.response.data);
 | 
			
		||||
	axios
 | 
			
		||||
		.get(url, {
 | 
			
		||||
			headers: { Authorization: 'Bearer ' + token }
 | 
			
		||||
		})
 | 
			
		||||
		.then((res) => res.data)
 | 
			
		||||
		.catch((err) => err.response.data);
 | 
			
		||||
 | 
			
		||||
export const isErrorResponse = (
 | 
			
		||||
  res: Responses.BaseResponse
 | 
			
		||||
): res is Responses.ErrorResponse => res.statusCode != 200;
 | 
			
		||||
export const isErrorResponse = (res: Responses.Base): res is Responses.Error =>
 | 
			
		||||
	res.statusCode > 299;
 | 
			
		||||
 
 | 
			
		||||
@@ -1,84 +1,130 @@
 | 
			
		||||
import type { Requests, Responses, UploadFile } from '@/dto';
 | 
			
		||||
import {
 | 
			
		||||
  Responses,
 | 
			
		||||
  Requests,
 | 
			
		||||
  get_token,
 | 
			
		||||
  post_token,
 | 
			
		||||
  post_token_form,
 | 
			
		||||
  isErrorResponse,
 | 
			
		||||
} from "./base";
 | 
			
		||||
	get_token,
 | 
			
		||||
	post_token,
 | 
			
		||||
	post_token_form,
 | 
			
		||||
	isErrorResponse
 | 
			
		||||
} from './base';
 | 
			
		||||
 | 
			
		||||
export const get_root = (
 | 
			
		||||
  token: string
 | 
			
		||||
): Promise<Responses.FS.GetRootResponse | Responses.ErrorResponse> =>
 | 
			
		||||
  get_token("/api/fs/root", token);
 | 
			
		||||
	token: string
 | 
			
		||||
): Promise<Responses.GetRoot | Responses.Error> =>
 | 
			
		||||
	get_token('/api/fs/root', token);
 | 
			
		||||
 | 
			
		||||
export const get_node = (
 | 
			
		||||
  token: string,
 | 
			
		||||
  node: number
 | 
			
		||||
): Promise<Responses.FS.GetNodeResponse | Responses.ErrorResponse> =>
 | 
			
		||||
  get_token(`/api/fs/node/${node}`, token);
 | 
			
		||||
	token: string,
 | 
			
		||||
	node: number
 | 
			
		||||
): Promise<Responses.GetNode | Responses.Error> =>
 | 
			
		||||
	get_token(`/api/fs/node/${node}`, token);
 | 
			
		||||
 | 
			
		||||
export const get_path = (
 | 
			
		||||
  token: string,
 | 
			
		||||
  node: number
 | 
			
		||||
): Promise<Responses.FS.GetPathResponse | Responses.ErrorResponse> =>
 | 
			
		||||
  get_token(`/api/fs/path/${node}`, token);
 | 
			
		||||
	token: string,
 | 
			
		||||
	node: number
 | 
			
		||||
): Promise<Responses.GetPath | Responses.Error> =>
 | 
			
		||||
	get_token(`/api/fs/path/${node}`, token);
 | 
			
		||||
 | 
			
		||||
export const create_folder = (
 | 
			
		||||
  token: string,
 | 
			
		||||
  parent: number,
 | 
			
		||||
  name: string
 | 
			
		||||
): Promise<Responses.FS.CreateFolderResponse | Responses.ErrorResponse> =>
 | 
			
		||||
  post_token<Requests.FS.CreateFolderRequest>(
 | 
			
		||||
    "/api/fs/createFolder",
 | 
			
		||||
    {
 | 
			
		||||
      parent: parent,
 | 
			
		||||
      name: name,
 | 
			
		||||
    },
 | 
			
		||||
    token
 | 
			
		||||
  );
 | 
			
		||||
	token: string,
 | 
			
		||||
	parent: number,
 | 
			
		||||
	name: string
 | 
			
		||||
): Promise<
 | 
			
		||||
	Responses.CreateFolder | Responses.CreateFolderExists | Responses.Error
 | 
			
		||||
> =>
 | 
			
		||||
	post_token<Requests.CreateFolder>(
 | 
			
		||||
		'/api/fs/createFolder',
 | 
			
		||||
		{
 | 
			
		||||
			parent: parent,
 | 
			
		||||
			name: name
 | 
			
		||||
		},
 | 
			
		||||
		token
 | 
			
		||||
	);
 | 
			
		||||
 | 
			
		||||
export const create_file = (
 | 
			
		||||
  token: string,
 | 
			
		||||
  parent: number,
 | 
			
		||||
  name: string
 | 
			
		||||
): Promise<Responses.FS.CreateFileResponse | Responses.ErrorResponse> =>
 | 
			
		||||
  post_token<Requests.FS.CreateFileRequest>(
 | 
			
		||||
    "/api/fs/createFile",
 | 
			
		||||
    {
 | 
			
		||||
      parent: parent,
 | 
			
		||||
      name: name,
 | 
			
		||||
    },
 | 
			
		||||
    token
 | 
			
		||||
  );
 | 
			
		||||
	token: string,
 | 
			
		||||
	parent: number,
 | 
			
		||||
	name: string
 | 
			
		||||
): Promise<
 | 
			
		||||
	Responses.CreateFolder | Responses.CreateFolderExists | Responses.Error
 | 
			
		||||
> =>
 | 
			
		||||
	post_token<Requests.CreateFolder>(
 | 
			
		||||
		'/api/fs/createFile',
 | 
			
		||||
		{
 | 
			
		||||
			parent: parent,
 | 
			
		||||
			name: name
 | 
			
		||||
		},
 | 
			
		||||
		token
 | 
			
		||||
	);
 | 
			
		||||
 | 
			
		||||
export const delete_node = (
 | 
			
		||||
  token: string,
 | 
			
		||||
  node: number
 | 
			
		||||
): Promise<Responses.FS.DeleteResponse | Responses.ErrorResponse> =>
 | 
			
		||||
  post_token(`/api/fs/delete/${node}`, {}, token);
 | 
			
		||||
export const create_zip = (
 | 
			
		||||
	token: string,
 | 
			
		||||
	nodes: number[]
 | 
			
		||||
): Promise<Responses.CreateZip | Responses.Error> =>
 | 
			
		||||
	post_token<Requests.CreateZip>(
 | 
			
		||||
		'/api/fs/create_zip',
 | 
			
		||||
		{
 | 
			
		||||
			nodes: nodes
 | 
			
		||||
		},
 | 
			
		||||
		token
 | 
			
		||||
	);
 | 
			
		||||
 | 
			
		||||
export const upload_file = async (
 | 
			
		||||
  token: string,
 | 
			
		||||
  parent: number,
 | 
			
		||||
  file: File,
 | 
			
		||||
  onProgress: (progressEvent: ProgressEvent) => void
 | 
			
		||||
): Promise<Responses.FS.UploadFileResponse | Responses.ErrorResponse> => {
 | 
			
		||||
  const node = await create_file(token, parent, file.name);
 | 
			
		||||
  if (isErrorResponse(node)) return node;
 | 
			
		||||
export const download_preview = (
 | 
			
		||||
	token: string,
 | 
			
		||||
	node: number
 | 
			
		||||
): Promise<Responses.DownloadBase64 | Responses.Error> =>
 | 
			
		||||
	get_token(`/api/fs/download_preview/${node}`, token);
 | 
			
		||||
 | 
			
		||||
  const form = new FormData();
 | 
			
		||||
  form.set("file", file);
 | 
			
		||||
  return post_token_form(`/api/fs/upload/${node.id}`, form, token, onProgress);
 | 
			
		||||
};
 | 
			
		||||
export const download_base64 = (
 | 
			
		||||
	token: string,
 | 
			
		||||
	node: number
 | 
			
		||||
): Promise<Responses.DownloadBase64 | Responses.Error> =>
 | 
			
		||||
	get_token(`/api/fs/download_base64/${node}`, token);
 | 
			
		||||
 | 
			
		||||
export const get_type = (
 | 
			
		||||
	token: string,
 | 
			
		||||
	node: number
 | 
			
		||||
): Promise<Responses.GetType | Responses.Error> =>
 | 
			
		||||
	get_token(`/api/fs/get_type/${node}`, token);
 | 
			
		||||
 | 
			
		||||
export async function upload_file(
 | 
			
		||||
	token: string,
 | 
			
		||||
	file: UploadFile,
 | 
			
		||||
	onProgress: (progressEvent: ProgressEvent) => void
 | 
			
		||||
): Promise<Responses.Success | Responses.Error> {
 | 
			
		||||
	const node = await create_file(token, file.parent, file.file.name);
 | 
			
		||||
	if (isErrorResponse(node)) return node;
 | 
			
		||||
	if ('exists' in node && !node.isFile)
 | 
			
		||||
		return { statusCode: 400, message: 'File exists as folder' };
 | 
			
		||||
 | 
			
		||||
	const form = new FormData();
 | 
			
		||||
	form.set('file', file.file);
 | 
			
		||||
	return post_token_form(
 | 
			
		||||
		`/api/fs/upload/${node.id}`,
 | 
			
		||||
		form,
 | 
			
		||||
		token,
 | 
			
		||||
		onProgress
 | 
			
		||||
	);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function download_file(token: string, id: number) {
 | 
			
		||||
  const form = document.createElement("form");
 | 
			
		||||
  form.method = "post";
 | 
			
		||||
  form.target = "_blank";
 | 
			
		||||
  form.action = "/api/fs/download";
 | 
			
		||||
  form.innerHTML = `<input type="hidden" name="jwtToken" value="${token}"><input type="hidden" name="id" value="${id}">`;
 | 
			
		||||
  document.body.appendChild(form);
 | 
			
		||||
  form.submit();
 | 
			
		||||
  document.body.removeChild(form);
 | 
			
		||||
	const form = document.createElement('form');
 | 
			
		||||
	form.method = 'post';
 | 
			
		||||
	form.target = '_blank';
 | 
			
		||||
	form.action = '/api/fs/download';
 | 
			
		||||
	form.innerHTML = `<input type="hidden" name="jwtToken" value="${token}"><input type="hidden" name="id" value="${id}">`;
 | 
			
		||||
	document.body.appendChild(form);
 | 
			
		||||
	form.submit();
 | 
			
		||||
	document.body.removeChild(form);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function download_multi_file(token: string, ids: number[]) {
 | 
			
		||||
	const form = document.createElement('form');
 | 
			
		||||
	form.method = 'post';
 | 
			
		||||
	form.target = '_blank';
 | 
			
		||||
	form.action = '/api/fs/download_multi';
 | 
			
		||||
	form.innerHTML = `<input type="hidden" name="jwtToken" value="${token}"><input type="hidden" name="id" value="${ids.join(
 | 
			
		||||
		','
 | 
			
		||||
	)}">`;
 | 
			
		||||
	document.body.appendChild(form);
 | 
			
		||||
	form.submit();
 | 
			
		||||
	document.body.removeChild(form);
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,7 @@
 | 
			
		||||
export { Requests, Responses, UserRole, isErrorResponse } from "./base";
 | 
			
		||||
export * as Auth from "./auth";
 | 
			
		||||
export * as FS from "./fs";
 | 
			
		||||
export * as User from "./user";
 | 
			
		||||
export * as Admin from "./admin";
 | 
			
		||||
export * from "./util";
 | 
			
		||||
export type { Requests, Responses, UploadFile } from './base';
 | 
			
		||||
export { UserRole, isErrorResponse } from './base';
 | 
			
		||||
export * as Auth from './auth';
 | 
			
		||||
export * as FS from './fs';
 | 
			
		||||
export * as User from './user';
 | 
			
		||||
export * as Admin from './admin';
 | 
			
		||||
export * from './util';
 | 
			
		||||
 
 | 
			
		||||
@@ -1,11 +1,12 @@
 | 
			
		||||
import { Responses, get_token, post_token } from "@/api/base";
 | 
			
		||||
import type { Responses } from '@/api/base';
 | 
			
		||||
import { get_token, post_token } from '@/api/base';
 | 
			
		||||
 | 
			
		||||
export const get_user_info = (
 | 
			
		||||
  token: string
 | 
			
		||||
): Promise<Responses.User.UserInfoResponse | Responses.ErrorResponse> =>
 | 
			
		||||
  get_token("/api/user/info", token);
 | 
			
		||||
	token: string
 | 
			
		||||
): Promise<Responses.UserInfo | Responses.Error> =>
 | 
			
		||||
	get_token('/api/user/info', token);
 | 
			
		||||
 | 
			
		||||
export const delete_user = (
 | 
			
		||||
  token: string
 | 
			
		||||
): Promise<Responses.User.DeleteUserResponse | Responses.ErrorResponse> =>
 | 
			
		||||
  post_token("/api/user/delete", {}, token);
 | 
			
		||||
	token: string
 | 
			
		||||
): Promise<Responses.Success | Responses.Error> =>
 | 
			
		||||
	post_token('/api/user/delete', {}, token);
 | 
			
		||||
 
 | 
			
		||||
@@ -1,26 +1,26 @@
 | 
			
		||||
import type { JwtPayload } from "jwt-decode";
 | 
			
		||||
import type { Ref, UnwrapRef } from "vue";
 | 
			
		||||
import jwtDecode from "jwt-decode";
 | 
			
		||||
import { isErrorResponse } from "./base";
 | 
			
		||||
import { refresh_token } from "./auth";
 | 
			
		||||
import type { JwtPayload } from 'jwt-decode';
 | 
			
		||||
import type { Ref, UnwrapRef } from 'vue';
 | 
			
		||||
import jwtDecode from 'jwt-decode';
 | 
			
		||||
import { isErrorResponse } from './base';
 | 
			
		||||
import { refresh_token } from './auth';
 | 
			
		||||
 | 
			
		||||
export async function check_token(
 | 
			
		||||
  token: TokenInjectType
 | 
			
		||||
	token: TokenInjectType
 | 
			
		||||
): Promise<string | void> {
 | 
			
		||||
  if (!token.jwt.value) return token.logout();
 | 
			
		||||
  const payload = jwtDecode<JwtPayload>(token.jwt.value);
 | 
			
		||||
  if (!payload) return token.logout();
 | 
			
		||||
  // Expires in more than 60 Minute
 | 
			
		||||
  if (payload.exp && payload.exp > Math.floor(Date.now() / 1000 + 60 * 60))
 | 
			
		||||
    return token.jwt.value;
 | 
			
		||||
  const new_token = await refresh_token(token.jwt.value);
 | 
			
		||||
  if (isErrorResponse(new_token)) return token.logout();
 | 
			
		||||
  token.setToken(new_token.jwt);
 | 
			
		||||
  return new_token.jwt;
 | 
			
		||||
	if (!token.jwt.value) return token.logout();
 | 
			
		||||
	const payload = jwtDecode<JwtPayload>(token.jwt.value);
 | 
			
		||||
	if (!payload) return token.logout();
 | 
			
		||||
	// Expires in more than 60 Minute
 | 
			
		||||
	if (payload.exp && payload.exp > Math.floor(Date.now() / 1000 + 60 * 60))
 | 
			
		||||
		return token.jwt.value;
 | 
			
		||||
	const new_token = await refresh_token(token.jwt.value);
 | 
			
		||||
	if (isErrorResponse(new_token)) return token.logout();
 | 
			
		||||
	token.setToken(new_token.jwt);
 | 
			
		||||
	return new_token.jwt;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export type TokenInjectType = {
 | 
			
		||||
  jwt: Ref<UnwrapRef<string | null>>;
 | 
			
		||||
  setToken: (token: string) => void;
 | 
			
		||||
  logout: () => void;
 | 
			
		||||
	jwt: Ref<UnwrapRef<string | null>>;
 | 
			
		||||
	setToken: (token: string) => void;
 | 
			
		||||
	logout: () => void;
 | 
			
		||||
};
 | 
			
		||||
 
 | 
			
		||||
@@ -1 +0,0 @@
 | 
			
		||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 261.76 226.69"  xmlns:v="https://vecta.io/nano"><path d="M161.096.001l-30.225 52.351L100.647.001H-.005l130.877 226.688L261.749.001z" fill="#41b883"/><path d="M161.096.001l-30.225 52.351L100.647.001H52.346l78.526 136.01L209.398.001z" fill="#34495e"/></svg>
 | 
			
		||||
| 
		 Before Width: | Height: | Size: 308 B  | 
							
								
								
									
										31
									
								
								frontend/src/components/AsyncImage.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								frontend/src/components/AsyncImage.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,31 @@
 | 
			
		||||
<script setup async lang="ts">
 | 
			
		||||
import type { TokenInjectType } from '@/api';
 | 
			
		||||
import { inject, ref } from 'vue';
 | 
			
		||||
import { NImage } from 'naive-ui';
 | 
			
		||||
import { check_token, FS, isErrorResponse } from '@/api';
 | 
			
		||||
 | 
			
		||||
const props = defineProps<{
 | 
			
		||||
	alt: string;
 | 
			
		||||
	id: number;
 | 
			
		||||
}>();
 | 
			
		||||
 | 
			
		||||
const jwt = inject<TokenInjectType>('jwt') as TokenInjectType;
 | 
			
		||||
 | 
			
		||||
const success = ref(false);
 | 
			
		||||
const data = ref('');
 | 
			
		||||
 | 
			
		||||
const token = await check_token(jwt);
 | 
			
		||||
if (token) {
 | 
			
		||||
	const resp = await FS.download_preview(jwt.jwt.value ?? '', props.id);
 | 
			
		||||
	if (!isErrorResponse(resp)) {
 | 
			
		||||
		data.value = resp.data;
 | 
			
		||||
		success.value = true;
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
	<NImage v-if="success" :alt="alt" :src="data" />
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<style lang="scss"></style>
 | 
			
		||||
							
								
								
									
										90
									
								
								frontend/src/components/DirViewer/CreateZipDialog.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										90
									
								
								frontend/src/components/DirViewer/CreateZipDialog.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,90 @@
 | 
			
		||||
import type { TokenInjectType } from '@/api';
 | 
			
		||||
import { ref } from 'vue';
 | 
			
		||||
import { NProgress, NButton, NIcon } from 'naive-ui';
 | 
			
		||||
import filesize from 'filesize';
 | 
			
		||||
import { Archive, Download } from '@vicons/carbon';
 | 
			
		||||
import { FS, check_token, isErrorResponse } from '@/api';
 | 
			
		||||
import type { DialogApiInjection } from 'naive-ui/es/dialog/src/DialogProvider';
 | 
			
		||||
 | 
			
		||||
export default function createZipDialog(
 | 
			
		||||
	nodes: number[],
 | 
			
		||||
	dialog: DialogApiInjection,
 | 
			
		||||
	jwt: TokenInjectType
 | 
			
		||||
) {
 | 
			
		||||
	const progress = ref(0);
 | 
			
		||||
	const total = ref(1);
 | 
			
		||||
	const percentage = ref(0);
 | 
			
		||||
	const done = ref(false);
 | 
			
		||||
	const dia = dialog.create({
 | 
			
		||||
		title: 'Create Archive...',
 | 
			
		||||
		closable: false,
 | 
			
		||||
		closeOnEsc: false,
 | 
			
		||||
		maskClosable: false,
 | 
			
		||||
		icon: () => <Archive />,
 | 
			
		||||
		content: () => (
 | 
			
		||||
			<NProgress
 | 
			
		||||
				type="line"
 | 
			
		||||
				percentage={percentage.value}
 | 
			
		||||
				height={20}
 | 
			
		||||
				status="info"
 | 
			
		||||
				showIndicator={false}
 | 
			
		||||
			/>
 | 
			
		||||
		),
 | 
			
		||||
		action: () =>
 | 
			
		||||
			done.value ? (
 | 
			
		||||
				<NButton
 | 
			
		||||
					onClick={async () => {
 | 
			
		||||
						const token = await check_token(jwt);
 | 
			
		||||
						if (!token) return;
 | 
			
		||||
						if (nodes.length == 1)
 | 
			
		||||
							FS.download_file(token, nodes[0]);
 | 
			
		||||
						else FS.download_multi_file(token, nodes);
 | 
			
		||||
						dia.destroy();
 | 
			
		||||
					}}
 | 
			
		||||
				>
 | 
			
		||||
					{{
 | 
			
		||||
						icon: () => (
 | 
			
		||||
							<NIcon>
 | 
			
		||||
								<Download />
 | 
			
		||||
							</NIcon>
 | 
			
		||||
						),
 | 
			
		||||
						default: () => 'Download archive'
 | 
			
		||||
					}}
 | 
			
		||||
				</NButton>
 | 
			
		||||
			) : (
 | 
			
		||||
				<div>
 | 
			
		||||
					{filesize(progress.value, {
 | 
			
		||||
						base: 2,
 | 
			
		||||
						standard: 'jedec'
 | 
			
		||||
					})}
 | 
			
		||||
					/
 | 
			
		||||
					{filesize(total.value, {
 | 
			
		||||
						base: 2,
 | 
			
		||||
						standard: 'jedec'
 | 
			
		||||
					})}
 | 
			
		||||
					- {Math.floor(percentage.value * 1000) / 1000}%
 | 
			
		||||
				</div>
 | 
			
		||||
			)
 | 
			
		||||
	});
 | 
			
		||||
	let updateRunning = false;
 | 
			
		||||
	const updateInterval = setInterval(async () => {
 | 
			
		||||
		if (updateRunning) return;
 | 
			
		||||
		updateRunning = true;
 | 
			
		||||
		const token = await check_token(jwt);
 | 
			
		||||
		if (!token) return;
 | 
			
		||||
		const resp = await FS.create_zip(token, nodes);
 | 
			
		||||
		if (isErrorResponse(resp)) return;
 | 
			
		||||
		if (resp.done) {
 | 
			
		||||
			percentage.value = 100;
 | 
			
		||||
			clearInterval(updateInterval);
 | 
			
		||||
			done.value = true;
 | 
			
		||||
		} else {
 | 
			
		||||
			progress.value = resp.progress ?? 0;
 | 
			
		||||
			total.value = resp.total ?? 1;
 | 
			
		||||
			if (total.value == 0) total.value = 1;
 | 
			
		||||
			percentage.value = (progress.value / total.value) * 100;
 | 
			
		||||
		}
 | 
			
		||||
		updateRunning = false;
 | 
			
		||||
	}, 500);
 | 
			
		||||
	return dia;
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										74
									
								
								frontend/src/components/DirViewer/DeleteModal.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										74
									
								
								frontend/src/components/DirViewer/DeleteModal.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,74 @@
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
import type { TokenInjectType } from '@/api';
 | 
			
		||||
import type { LogInst } from 'naive-ui';
 | 
			
		||||
import { ref, inject } from 'vue';
 | 
			
		||||
import { check_token } from '@/api';
 | 
			
		||||
import { NCard, NLog } from 'naive-ui';
 | 
			
		||||
 | 
			
		||||
const jwt = inject<TokenInjectType>('jwt') as TokenInjectType;
 | 
			
		||||
const log = ref('');
 | 
			
		||||
const logInst = ref<LogInst>();
 | 
			
		||||
 | 
			
		||||
function getLogWriter() {
 | 
			
		||||
	const decoder = new TextDecoder();
 | 
			
		||||
	return new WritableStream<Uint8Array>({
 | 
			
		||||
		write(chunk) {
 | 
			
		||||
			log.value += decoder.decode(chunk, { stream: true });
 | 
			
		||||
			logInst.value?.scrollTo({ position: 'top' });
 | 
			
		||||
		},
 | 
			
		||||
		close() {
 | 
			
		||||
			log.value += decoder.decode(new Uint8Array(0), { stream: false });
 | 
			
		||||
			logInst.value?.scrollTo({ position: 'top' });
 | 
			
		||||
		},
 | 
			
		||||
		abort(err) {
 | 
			
		||||
			log.value += `Error: ${err}\n`;
 | 
			
		||||
			logInst.value?.scrollTo({ position: 'top' });
 | 
			
		||||
		}
 | 
			
		||||
	});
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const props = defineProps<{
 | 
			
		||||
	nodes: number[];
 | 
			
		||||
}>();
 | 
			
		||||
 | 
			
		||||
async function startDelete() {
 | 
			
		||||
	const token = await check_token(jwt);
 | 
			
		||||
	if (!token) return;
 | 
			
		||||
	for (const node of props.nodes) {
 | 
			
		||||
		try {
 | 
			
		||||
			const logWriter = getLogWriter();
 | 
			
		||||
			const resp = await fetch(`/api/fs/delete/${node}`, {
 | 
			
		||||
				method: 'post',
 | 
			
		||||
				headers: {
 | 
			
		||||
					Authorization: 'Bearer ' + token
 | 
			
		||||
				}
 | 
			
		||||
			});
 | 
			
		||||
			if (!resp.ok) continue;
 | 
			
		||||
			if (!resp.body) continue;
 | 
			
		||||
			await resp.body.pipeTo(logWriter);
 | 
			
		||||
		} catch (err) {
 | 
			
		||||
			log.value += `Error: ${err}\n`;
 | 
			
		||||
			logInst.value?.scrollTo({ position: 'top' });
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
defineExpose({
 | 
			
		||||
	startDelete
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
	<n-card title="Deleting..." style="margin: 20px">
 | 
			
		||||
		<n-log ref="logInst" class="log-code" :log="log" :rows="50"></n-log>
 | 
			
		||||
		<!--<n-code class="log-code">
 | 
			
		||||
		</n-code>-->
 | 
			
		||||
	</n-card>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<style scoped lang="scss">
 | 
			
		||||
.log-code {
 | 
			
		||||
	margin: 8px;
 | 
			
		||||
	background-color: rgb(250, 250, 252);
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
							
								
								
									
										124
									
								
								frontend/src/components/DirViewer/DirViewer.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										124
									
								
								frontend/src/components/DirViewer/DirViewer.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,124 @@
 | 
			
		||||
<script setup lang="tsx">
 | 
			
		||||
import type { TokenInjectType, Responses } from '@/api';
 | 
			
		||||
import type { CSSProperties } from 'vue';
 | 
			
		||||
import { inject, ref, watch } from 'vue';
 | 
			
		||||
import {
 | 
			
		||||
	useMessage,
 | 
			
		||||
	useDialog,
 | 
			
		||||
	NSwitch,
 | 
			
		||||
	NGrid,
 | 
			
		||||
	NGi,
 | 
			
		||||
	NButton,
 | 
			
		||||
	NIcon,
 | 
			
		||||
	NInput
 | 
			
		||||
} from 'naive-ui';
 | 
			
		||||
import { FolderAdd } from '@vicons/carbon';
 | 
			
		||||
import { FS, check_token } from '@/api';
 | 
			
		||||
import DirViewerTable from '@/components/DirViewer/DirViewerTable.vue';
 | 
			
		||||
import { loadingMsgWrapper } from '@/utils';
 | 
			
		||||
 | 
			
		||||
const message = useMessage();
 | 
			
		||||
const dialog = useDialog();
 | 
			
		||||
 | 
			
		||||
const props = defineProps<{
 | 
			
		||||
	node: Responses.GetNode;
 | 
			
		||||
}>();
 | 
			
		||||
 | 
			
		||||
const jwt = inject<TokenInjectType>('jwt') as TokenInjectType;
 | 
			
		||||
 | 
			
		||||
const emit = defineEmits<{
 | 
			
		||||
	(e: 'reloadNode'): void;
 | 
			
		||||
}>();
 | 
			
		||||
 | 
			
		||||
const showPreview = ref(false);
 | 
			
		||||
const nodes = ref<Responses.GetNodeEntry[]>([]);
 | 
			
		||||
 | 
			
		||||
watch(
 | 
			
		||||
	() => props.node,
 | 
			
		||||
	async (to) => {
 | 
			
		||||
		nodes.value = [];
 | 
			
		||||
		if (to.parent != null)
 | 
			
		||||
			nodes.value.push({
 | 
			
		||||
				id: to.parent,
 | 
			
		||||
				isFile: false,
 | 
			
		||||
				parent: null,
 | 
			
		||||
				name: '..',
 | 
			
		||||
				preview: false
 | 
			
		||||
			});
 | 
			
		||||
		if (to.children) nodes.value.push(...to.children);
 | 
			
		||||
	},
 | 
			
		||||
	{ immediate: true }
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
const newFolder = loadingMsgWrapper(message, async (name: string) => {
 | 
			
		||||
	const token = await check_token(jwt);
 | 
			
		||||
	if (!token) return;
 | 
			
		||||
	await FS.create_folder(token, props.node.id, name);
 | 
			
		||||
	emit('reloadNode');
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
function previewSwitchRailStyle(state: { focused: boolean; checked: boolean }) {
 | 
			
		||||
	const style: CSSProperties = {};
 | 
			
		||||
	style.background = state.checked ? '#0b0' : '#d00';
 | 
			
		||||
	if (state.focused)
 | 
			
		||||
		style.boxShadow = `0 0 0 2px ${
 | 
			
		||||
			state.checked ? '#00bb0040' : '#dd000040'
 | 
			
		||||
		}`;
 | 
			
		||||
	return style;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function createNewFolderDialog() {
 | 
			
		||||
	let newFolderName = '';
 | 
			
		||||
	const dia = dialog.create({
 | 
			
		||||
		title: 'New Folder',
 | 
			
		||||
		icon: () => <FolderAdd />,
 | 
			
		||||
		content: () => (
 | 
			
		||||
			<NInput
 | 
			
		||||
				type="text"
 | 
			
		||||
				placeholder="Folder name"
 | 
			
		||||
				onInput={(e) => (newFolderName = e)}
 | 
			
		||||
				onKeyup={(e) => {
 | 
			
		||||
					if (e.key === 'Enter')
 | 
			
		||||
						newFolder(newFolderName).then(() => dia.destroy());
 | 
			
		||||
				}}
 | 
			
		||||
			/>
 | 
			
		||||
		),
 | 
			
		||||
		negativeText: 'Cancel',
 | 
			
		||||
		positiveText: 'Create',
 | 
			
		||||
		positiveButtonProps: { type: 'success' },
 | 
			
		||||
		onPositiveClick: () => newFolder(newFolderName)
 | 
			
		||||
	});
 | 
			
		||||
	return dia;
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
	<n-grid cols="2" x-gap="16" y-gap="16">
 | 
			
		||||
		<n-gi>
 | 
			
		||||
			<n-button @click="createNewFolderDialog">
 | 
			
		||||
				<template #icon>
 | 
			
		||||
					<n-icon><FolderAdd /></n-icon>
 | 
			
		||||
				</template>
 | 
			
		||||
				Create folder
 | 
			
		||||
			</n-button>
 | 
			
		||||
		</n-gi>
 | 
			
		||||
		<n-gi style="text-align: right">
 | 
			
		||||
			<n-switch
 | 
			
		||||
				:rail-style="previewSwitchRailStyle"
 | 
			
		||||
				v-model:value="showPreview"
 | 
			
		||||
			>
 | 
			
		||||
				<template #checked>Show preview</template>
 | 
			
		||||
				<template #unchecked>Hide preview</template>
 | 
			
		||||
			</n-switch>
 | 
			
		||||
		</n-gi>
 | 
			
		||||
		<n-gi span="2">
 | 
			
		||||
			<DirViewerTable
 | 
			
		||||
				:nodes="nodes"
 | 
			
		||||
				:show-preview="showPreview"
 | 
			
		||||
				@reloadNode="emit('reloadNode')"
 | 
			
		||||
			/>
 | 
			
		||||
		</n-gi>
 | 
			
		||||
	</n-grid>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<style scoped></style>
 | 
			
		||||
							
								
								
									
										379
									
								
								frontend/src/components/DirViewer/DirViewerTable.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										379
									
								
								frontend/src/components/DirViewer/DirViewerTable.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,379 @@
 | 
			
		||||
<script setup lang="tsx">
 | 
			
		||||
import type { TokenInjectType, Responses } from '@/api';
 | 
			
		||||
import type {
 | 
			
		||||
	DropdownOption,
 | 
			
		||||
	DropdownGroupOption,
 | 
			
		||||
	DropdownDividerOption,
 | 
			
		||||
	DropdownRenderOption,
 | 
			
		||||
	DataTableColumn
 | 
			
		||||
} from 'naive-ui';
 | 
			
		||||
import type { SummaryCell } from 'naive-ui/es/data-table/src/interface';
 | 
			
		||||
import { inject, ref, nextTick, Suspense } from 'vue';
 | 
			
		||||
import filesize from 'filesize';
 | 
			
		||||
import { check_token, FS } from '@/api';
 | 
			
		||||
import { loadingMsgWrapper } from '@/utils';
 | 
			
		||||
import {
 | 
			
		||||
	useMessage,
 | 
			
		||||
	useDialog,
 | 
			
		||||
	NDataTable,
 | 
			
		||||
	NText,
 | 
			
		||||
	NIcon,
 | 
			
		||||
	NDropdown,
 | 
			
		||||
	NPopover,
 | 
			
		||||
	NSpin,
 | 
			
		||||
	NImageGroup,
 | 
			
		||||
	NButtonGroup,
 | 
			
		||||
	NButton,
 | 
			
		||||
	NModal
 | 
			
		||||
} from 'naive-ui';
 | 
			
		||||
import {
 | 
			
		||||
	Folder,
 | 
			
		||||
	FolderParent,
 | 
			
		||||
	DocumentBlank,
 | 
			
		||||
	Delete,
 | 
			
		||||
	Download
 | 
			
		||||
} from '@vicons/carbon';
 | 
			
		||||
import NLink from '@/components/NLink.vue';
 | 
			
		||||
import AsyncImage from '@/components/AsyncImage.vue';
 | 
			
		||||
import createZipDialog from '@/components/DirViewer/CreateZipDialog';
 | 
			
		||||
import DeleteModal from '@/components/DirViewer/DeleteModal.vue';
 | 
			
		||||
 | 
			
		||||
const message = useMessage();
 | 
			
		||||
const dialog = useDialog();
 | 
			
		||||
 | 
			
		||||
const jwt = inject<TokenInjectType>('jwt') as TokenInjectType;
 | 
			
		||||
 | 
			
		||||
const emit = defineEmits<{
 | 
			
		||||
	(e: 'reloadNode'): void;
 | 
			
		||||
}>();
 | 
			
		||||
 | 
			
		||||
type DropdownOptionsType = Array<
 | 
			
		||||
	| DropdownOption
 | 
			
		||||
	| DropdownGroupOption
 | 
			
		||||
	| DropdownDividerOption
 | 
			
		||||
	| DropdownRenderOption
 | 
			
		||||
>;
 | 
			
		||||
 | 
			
		||||
const props = defineProps<{
 | 
			
		||||
	nodes: Responses.GetNodeEntry[];
 | 
			
		||||
	showPreview: boolean;
 | 
			
		||||
}>();
 | 
			
		||||
 | 
			
		||||
const checkedRows = ref<number[]>([]);
 | 
			
		||||
const deleteNodes = ref<number[]>([]);
 | 
			
		||||
const deleteDialog = ref();
 | 
			
		||||
const deleteDialogShow = ref(false);
 | 
			
		||||
 | 
			
		||||
const dropdownX = ref(0);
 | 
			
		||||
const dropdownY = ref(0);
 | 
			
		||||
const dropdownShow = ref(false);
 | 
			
		||||
let dropdownCurrentNode: Responses.GetNodeEntry | null = null;
 | 
			
		||||
 | 
			
		||||
const dropdownOptions = ref<DropdownOptionsType>();
 | 
			
		||||
const dropdownOptionsFolder: DropdownOptionsType = [
 | 
			
		||||
	{
 | 
			
		||||
		label: () => <NText>Download</NText>,
 | 
			
		||||
		key: 'download',
 | 
			
		||||
		icon: () => (
 | 
			
		||||
			<NIcon>
 | 
			
		||||
				<Download />
 | 
			
		||||
			</NIcon>
 | 
			
		||||
		)
 | 
			
		||||
	},
 | 
			
		||||
	{
 | 
			
		||||
		label: () => <NText type="error">Delete</NText>,
 | 
			
		||||
		key: 'delete',
 | 
			
		||||
		icon: () => (
 | 
			
		||||
			<NIcon>
 | 
			
		||||
				<Delete />
 | 
			
		||||
			</NIcon>
 | 
			
		||||
		)
 | 
			
		||||
	}
 | 
			
		||||
];
 | 
			
		||||
const dropdownOptionsFile: DropdownOptionsType = [
 | 
			
		||||
	{
 | 
			
		||||
		label: () => <NText>Download</NText>,
 | 
			
		||||
		key: 'download',
 | 
			
		||||
		icon: () => (
 | 
			
		||||
			<NIcon>
 | 
			
		||||
				<Download />
 | 
			
		||||
			</NIcon>
 | 
			
		||||
		)
 | 
			
		||||
	},
 | 
			
		||||
	{
 | 
			
		||||
		label: () => <NText type="error">Delete</NText>,
 | 
			
		||||
		key: 'delete',
 | 
			
		||||
		icon: () => (
 | 
			
		||||
			<NIcon>
 | 
			
		||||
				<Delete />
 | 
			
		||||
			</NIcon>
 | 
			
		||||
		)
 | 
			
		||||
	}
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
const dropdownSelect = loadingMsgWrapper(message, async (key: string) => {
 | 
			
		||||
	dropdownShow.value = false;
 | 
			
		||||
	const token = await check_token(jwt);
 | 
			
		||||
	if (!token) return;
 | 
			
		||||
	if (!dropdownCurrentNode) return;
 | 
			
		||||
	switch (key) {
 | 
			
		||||
		case 'download':
 | 
			
		||||
			if (dropdownCurrentNode.isFile)
 | 
			
		||||
				await FS.download_file(token, dropdownCurrentNode.id);
 | 
			
		||||
			else createZipDialog([dropdownCurrentNode.id], dialog, jwt);
 | 
			
		||||
			break;
 | 
			
		||||
		case 'delete':
 | 
			
		||||
			dialog.warning({
 | 
			
		||||
				title: 'Really delete?',
 | 
			
		||||
				content: `Are you sure you want to delete "${dropdownCurrentNode.name}"`,
 | 
			
		||||
				positiveText: 'Yes',
 | 
			
		||||
				negativeText: 'No',
 | 
			
		||||
				onPositiveClick: () => {
 | 
			
		||||
					if (!dropdownCurrentNode) return;
 | 
			
		||||
					deleteNodes.value = [dropdownCurrentNode.id];
 | 
			
		||||
					showDeleteDialog();
 | 
			
		||||
				}
 | 
			
		||||
			});
 | 
			
		||||
			break;
 | 
			
		||||
	}
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const columns: DataTableColumn<Responses.GetNodeEntry>[] = [
 | 
			
		||||
	{
 | 
			
		||||
		type: 'selection',
 | 
			
		||||
		options: [
 | 
			
		||||
			{
 | 
			
		||||
				label: 'Select all folders',
 | 
			
		||||
				key: 'folders',
 | 
			
		||||
				onSelect(data) {
 | 
			
		||||
					checkedRows.value = data
 | 
			
		||||
						.filter((node) => !node.isFile)
 | 
			
		||||
						.map((node) => node.id);
 | 
			
		||||
				}
 | 
			
		||||
			},
 | 
			
		||||
			{
 | 
			
		||||
				label: 'Select all files',
 | 
			
		||||
				key: 'files',
 | 
			
		||||
				onSelect(data) {
 | 
			
		||||
					checkedRows.value = data
 | 
			
		||||
						.filter((node) => node.isFile)
 | 
			
		||||
						.map((node) => node.id);
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		],
 | 
			
		||||
		disabled(node) {
 | 
			
		||||
			return node.parent == null;
 | 
			
		||||
		}
 | 
			
		||||
	},
 | 
			
		||||
	{
 | 
			
		||||
		title: 'Name',
 | 
			
		||||
		key: 'name',
 | 
			
		||||
		minWidth: 720,
 | 
			
		||||
		render(node) {
 | 
			
		||||
			return (
 | 
			
		||||
				<NLink to={`/fs/${node.id}`}>
 | 
			
		||||
					<div>
 | 
			
		||||
						<NIcon
 | 
			
		||||
							size="1.2em"
 | 
			
		||||
							color="#111"
 | 
			
		||||
							component={
 | 
			
		||||
								node.isFile
 | 
			
		||||
									? DocumentBlank
 | 
			
		||||
									: node.name == '..'
 | 
			
		||||
									? FolderParent
 | 
			
		||||
									: Folder
 | 
			
		||||
							}
 | 
			
		||||
							style="top: 0.25em; margin-right: 0.5em"
 | 
			
		||||
						/>
 | 
			
		||||
						{node.name}
 | 
			
		||||
					</div>
 | 
			
		||||
				</NLink>
 | 
			
		||||
			);
 | 
			
		||||
		}
 | 
			
		||||
	},
 | 
			
		||||
	{
 | 
			
		||||
		title: 'Size',
 | 
			
		||||
		key: 'size',
 | 
			
		||||
		minWidth: 100,
 | 
			
		||||
		render(node) {
 | 
			
		||||
			return !node.isFile ? (
 | 
			
		||||
				''
 | 
			
		||||
			) : (
 | 
			
		||||
				<NPopover trigger="hover">
 | 
			
		||||
					{{
 | 
			
		||||
						default: () => `${node.size?.toLocaleString()} bytes`,
 | 
			
		||||
						trigger: () =>
 | 
			
		||||
							filesize(node.size ?? 0, {
 | 
			
		||||
								base: 2,
 | 
			
		||||
								standard: 'jedec'
 | 
			
		||||
							})
 | 
			
		||||
					}}
 | 
			
		||||
				</NPopover>
 | 
			
		||||
			);
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
];
 | 
			
		||||
const previewColumns: DataTableColumn<Responses.GetNodeEntry>[] = [
 | 
			
		||||
	columns[0],
 | 
			
		||||
	{
 | 
			
		||||
		title: 'Preview',
 | 
			
		||||
		key: 'preview',
 | 
			
		||||
		render(node) {
 | 
			
		||||
			return node.preview ? (
 | 
			
		||||
				<Suspense>
 | 
			
		||||
					{{
 | 
			
		||||
						default: () => (
 | 
			
		||||
							<AsyncImage alt={node.name} id={node.id} />
 | 
			
		||||
						),
 | 
			
		||||
						fallback: () => <NSpin size="small" />
 | 
			
		||||
					}}
 | 
			
		||||
				</Suspense>
 | 
			
		||||
			) : (
 | 
			
		||||
				''
 | 
			
		||||
			);
 | 
			
		||||
		}
 | 
			
		||||
	},
 | 
			
		||||
	...columns.slice(1)
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
const massDownload = loadingMsgWrapper(message, async () => {
 | 
			
		||||
	const token = await check_token(jwt);
 | 
			
		||||
	if (!token) return;
 | 
			
		||||
	const nodes = checkedRows.value;
 | 
			
		||||
	if (nodes.length == 1) {
 | 
			
		||||
		const node = props.nodes.find((n) => n.id == nodes[0]);
 | 
			
		||||
		if (!node) return;
 | 
			
		||||
		if (node.isFile) await FS.download_file(token, nodes[0]);
 | 
			
		||||
		else createZipDialog(nodes, dialog, jwt);
 | 
			
		||||
	} else createZipDialog(nodes, dialog, jwt);
 | 
			
		||||
	checkedRows.value = [];
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const massDelete = loadingMsgWrapper(message, async () => {
 | 
			
		||||
	const token = await check_token(jwt);
 | 
			
		||||
	if (!token) return;
 | 
			
		||||
	dialog.warning({
 | 
			
		||||
		title: 'Really delete?',
 | 
			
		||||
		content: `Are you sure you want to delete "${checkedRows.value.length} folders/files"`,
 | 
			
		||||
		positiveText: 'Yes',
 | 
			
		||||
		negativeText: 'No',
 | 
			
		||||
		onPositiveClick: loadingMsgWrapper(message, async () => {
 | 
			
		||||
			deleteNodes.value = checkedRows.value;
 | 
			
		||||
			showDeleteDialog();
 | 
			
		||||
		})
 | 
			
		||||
	});
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const selectionCell = (): SummaryCell => {
 | 
			
		||||
	return {
 | 
			
		||||
		value:
 | 
			
		||||
			checkedRows.value.length != 0 ? (
 | 
			
		||||
				<NButtonGroup>
 | 
			
		||||
					<NButton onClick={massDownload}>Download</NButton>
 | 
			
		||||
					<NButton onClick={massDelete} type="error">
 | 
			
		||||
						Delete
 | 
			
		||||
					</NButton>
 | 
			
		||||
				</NButtonGroup>
 | 
			
		||||
			) : (
 | 
			
		||||
				''
 | 
			
		||||
			),
 | 
			
		||||
		colSpan: props.showPreview ? 2 : 1
 | 
			
		||||
	};
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const sizeCell = (data: Responses.GetNodeEntry[]): SummaryCell => {
 | 
			
		||||
	return {
 | 
			
		||||
		value: (
 | 
			
		||||
			<span>
 | 
			
		||||
				{filesize(
 | 
			
		||||
					data.reduce((cur, node) => cur + (node.size ?? 0), 0),
 | 
			
		||||
					{
 | 
			
		||||
						base: 2,
 | 
			
		||||
						standard: 'jedec'
 | 
			
		||||
					}
 | 
			
		||||
				)}
 | 
			
		||||
			</span>
 | 
			
		||||
		)
 | 
			
		||||
	};
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
function createPreviewSummary(data: Responses.GetNodeEntry[]) {
 | 
			
		||||
	return {
 | 
			
		||||
		preview: selectionCell(),
 | 
			
		||||
		size: sizeCell(data)
 | 
			
		||||
	};
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function createSummary(data: Responses.GetNodeEntry[]) {
 | 
			
		||||
	return {
 | 
			
		||||
		name: selectionCell(),
 | 
			
		||||
		size: sizeCell(data)
 | 
			
		||||
	};
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function rowProps(node: Responses.GetNodeEntry) {
 | 
			
		||||
	if (!('isFile' in node)) return {};
 | 
			
		||||
	return {
 | 
			
		||||
		onContextmenu: (e: MouseEvent) => {
 | 
			
		||||
			e.preventDefault();
 | 
			
		||||
			dropdownShow.value = false;
 | 
			
		||||
			dropdownCurrentNode = node;
 | 
			
		||||
			dropdownOptions.value = node.isFile
 | 
			
		||||
				? dropdownOptionsFile
 | 
			
		||||
				: dropdownOptionsFolder;
 | 
			
		||||
			nextTick().then(() => {
 | 
			
		||||
				dropdownShow.value = true;
 | 
			
		||||
				dropdownX.value = e.clientX;
 | 
			
		||||
				dropdownY.value = e.clientY;
 | 
			
		||||
			});
 | 
			
		||||
		}
 | 
			
		||||
	};
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const rowKey = (node: Responses.GetNodeEntry): number => node.id;
 | 
			
		||||
 | 
			
		||||
function showDeleteDialog() {
 | 
			
		||||
	if (deleteNodes.value.length == 0) return;
 | 
			
		||||
	deleteDialogShow.value = true;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function onShowDeleteDialog() {
 | 
			
		||||
	await deleteDialog.value?.startDelete();
 | 
			
		||||
	deleteDialogShow.value = false;
 | 
			
		||||
	emit('reloadNode');
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
	<n-image-group>
 | 
			
		||||
		<n-data-table
 | 
			
		||||
			:columns="showPreview ? previewColumns : columns"
 | 
			
		||||
			:data="nodes"
 | 
			
		||||
			:row-key="rowKey"
 | 
			
		||||
			:row-props="rowProps"
 | 
			
		||||
			:summary="showPreview ? createPreviewSummary : createSummary"
 | 
			
		||||
			v-model:checked-row-keys="checkedRows"
 | 
			
		||||
		/>
 | 
			
		||||
	</n-image-group>
 | 
			
		||||
	<n-dropdown
 | 
			
		||||
		placement="bottom-start"
 | 
			
		||||
		trigger="manual"
 | 
			
		||||
		:x="dropdownX"
 | 
			
		||||
		:y="dropdownY"
 | 
			
		||||
		:show="dropdownShow"
 | 
			
		||||
		:show-arrow="true"
 | 
			
		||||
		:options="dropdownOptions"
 | 
			
		||||
		:on-clickoutside="() => (dropdownShow = false)"
 | 
			
		||||
		@select="dropdownSelect"
 | 
			
		||||
	/>
 | 
			
		||||
	<n-modal
 | 
			
		||||
		v-model:show="deleteDialogShow"
 | 
			
		||||
		:close-on-esc="false"
 | 
			
		||||
		:mask-closable="false"
 | 
			
		||||
		:on-after-enter="onShowDeleteDialog"
 | 
			
		||||
	>
 | 
			
		||||
		<DeleteModal ref="deleteDialog" :nodes="deleteNodes" />
 | 
			
		||||
	</n-modal>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<style scoped></style>
 | 
			
		||||
@@ -1,41 +0,0 @@
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
import type { TokenInjectType } from "@/api";
 | 
			
		||||
import { defineEmits, defineProps, inject } from "vue";
 | 
			
		||||
import { check_token, FS, Responses } from "@/api";
 | 
			
		||||
 | 
			
		||||
const jwt = inject<TokenInjectType>("jwt") as TokenInjectType;
 | 
			
		||||
const props = defineProps<{
 | 
			
		||||
  node: Responses.FS.GetNodeResponse;
 | 
			
		||||
}>();
 | 
			
		||||
 | 
			
		||||
const emit = defineEmits<{
 | 
			
		||||
  (e: "reloadNode"): void;
 | 
			
		||||
}>();
 | 
			
		||||
 | 
			
		||||
async function del() {
 | 
			
		||||
  const token = await check_token(jwt);
 | 
			
		||||
  if (!token) return;
 | 
			
		||||
  await FS.delete_node(token, props.node.id);
 | 
			
		||||
  emit("reloadNode");
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function download() {
 | 
			
		||||
  const token = await check_token(jwt);
 | 
			
		||||
  if (!token) return;
 | 
			
		||||
  FS.download_file(token, props.node.id);
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
  <td>
 | 
			
		||||
    <router-link :to="'/fs/' + props.node.id">{{ node.name }}</router-link>
 | 
			
		||||
  </td>
 | 
			
		||||
  <td>
 | 
			
		||||
    <a href="#" @click="download()" v-if="props.node.isFile">Download</a>
 | 
			
		||||
  </td>
 | 
			
		||||
  <td>
 | 
			
		||||
    <a href="#" @click="del()" v-if="props.node.name !== '..'">delete</a>
 | 
			
		||||
  </td>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<style scoped></style>
 | 
			
		||||
@@ -1,102 +0,0 @@
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
import type { TokenInjectType } from "@/api";
 | 
			
		||||
import { defineEmits, defineProps, inject, reactive, ref, watch } from "vue";
 | 
			
		||||
import { FS, Responses, check_token } from "@/api";
 | 
			
		||||
import DirEntry from "@/components/FSView/DirEntry.vue";
 | 
			
		||||
import UploadFileDialog from "@/components/UploadDialog/UploadFileDialog.vue";
 | 
			
		||||
import { NModal } from "naive-ui";
 | 
			
		||||
 | 
			
		||||
const props = defineProps<{
 | 
			
		||||
  node: Responses.FS.GetNodeResponse;
 | 
			
		||||
}>();
 | 
			
		||||
 | 
			
		||||
const jwt = inject<TokenInjectType>("jwt") as TokenInjectType;
 | 
			
		||||
 | 
			
		||||
const emit = defineEmits<{
 | 
			
		||||
  (e: "reloadNode"): void;
 | 
			
		||||
  (e: "gotoRoot"): void;
 | 
			
		||||
}>();
 | 
			
		||||
 | 
			
		||||
const fileInput = ref<HTMLInputElement>();
 | 
			
		||||
const uploadDialog = ref();
 | 
			
		||||
const uploadDialogShow = ref(false);
 | 
			
		||||
 | 
			
		||||
const new_folder_name = ref("");
 | 
			
		||||
const files = ref<File[]>([]);
 | 
			
		||||
const nodes = ref<Responses.FS.GetNodeResponse[]>([]);
 | 
			
		||||
const hasParent = ref(false);
 | 
			
		||||
const parentNode = reactive<Responses.FS.GetNodeResponse>({
 | 
			
		||||
  id: 0,
 | 
			
		||||
  statusCode: 200,
 | 
			
		||||
  isFile: false,
 | 
			
		||||
  parent: null,
 | 
			
		||||
  name: "..",
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
watch(
 | 
			
		||||
  () => props.node,
 | 
			
		||||
  async (to) => {
 | 
			
		||||
    parentNode.id = to.parent ?? 0;
 | 
			
		||||
    hasParent.value = to.parent != null;
 | 
			
		||||
    nodes.value = [];
 | 
			
		||||
    const token = await check_token(jwt);
 | 
			
		||||
    if (!token) return;
 | 
			
		||||
    await Promise.all(
 | 
			
		||||
      to.children?.map(async (child) => {
 | 
			
		||||
        nodes.value.push(
 | 
			
		||||
          (await FS.get_node(token, child)) as Responses.FS.GetNodeResponse
 | 
			
		||||
        );
 | 
			
		||||
      }) ?? []
 | 
			
		||||
    );
 | 
			
		||||
  },
 | 
			
		||||
  { immediate: true }
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
async function newFolder() {
 | 
			
		||||
  const token = await check_token(jwt);
 | 
			
		||||
  if (!token) return;
 | 
			
		||||
  await FS.create_folder(token, props.node.id, new_folder_name.value);
 | 
			
		||||
  emit("reloadNode");
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function uploadFiles() {
 | 
			
		||||
  files.value = Array.from(fileInput.value?.files ?? []);
 | 
			
		||||
  if (files.value.length == 0) return;
 | 
			
		||||
  uploadDialogShow.value = true;
 | 
			
		||||
}
 | 
			
		||||
async function uploadFilesDialogOpen() {
 | 
			
		||||
  await uploadDialog.value?.startUpload(props.node.id);
 | 
			
		||||
  uploadDialogShow.value = false;
 | 
			
		||||
  if (fileInput.value) fileInput.value.value = "";
 | 
			
		||||
  emit("reloadNode");
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
  <div>
 | 
			
		||||
    <input type="text" placeholder="Folder name" v-model="new_folder_name" />
 | 
			
		||||
    <a href="#" @click="newFolder()">create folder</a>
 | 
			
		||||
  </div>
 | 
			
		||||
  <div>
 | 
			
		||||
    <input type="file" ref="fileInput" multiple />
 | 
			
		||||
    <a href="#" @click="uploadFiles()">upload files</a>
 | 
			
		||||
  </div>
 | 
			
		||||
  <table>
 | 
			
		||||
    <tr v-if="hasParent">
 | 
			
		||||
      <DirEntry :node="parentNode" @reloadNode="emit('reloadNode')" />
 | 
			
		||||
    </tr>
 | 
			
		||||
    <tr v-for="n in nodes" :key="n.id">
 | 
			
		||||
      <DirEntry :node="n" @reloadNode="emit('reloadNode')" />
 | 
			
		||||
    </tr>
 | 
			
		||||
  </table>
 | 
			
		||||
  <n-modal
 | 
			
		||||
    v-model:show="uploadDialogShow"
 | 
			
		||||
    :close-on-esc="false"
 | 
			
		||||
    :mask-closable="false"
 | 
			
		||||
    :on-after-enter="uploadFilesDialogOpen"
 | 
			
		||||
  >
 | 
			
		||||
    <UploadFileDialog ref="uploadDialog" :files="files" />
 | 
			
		||||
  </n-modal>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<style scoped></style>
 | 
			
		||||
@@ -1,39 +0,0 @@
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
import type { TokenInjectType } from "@/api";
 | 
			
		||||
import { defineProps, inject } from "vue";
 | 
			
		||||
import { check_token, FS, Responses } from "@/api";
 | 
			
		||||
 | 
			
		||||
const props = defineProps<{
 | 
			
		||||
  node: Responses.FS.GetNodeResponse;
 | 
			
		||||
}>();
 | 
			
		||||
 | 
			
		||||
const jwt = inject<TokenInjectType>("jwt") as TokenInjectType;
 | 
			
		||||
 | 
			
		||||
async function del() {
 | 
			
		||||
  const token = await check_token(jwt);
 | 
			
		||||
  if (!token) return;
 | 
			
		||||
  await FS.delete_node(token, props.node.id);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function download() {
 | 
			
		||||
  const token = await check_token(jwt);
 | 
			
		||||
  if (!token) return;
 | 
			
		||||
  FS.download_file(token, props.node.id);
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
  <div>
 | 
			
		||||
    <router-link :to="'/fs/' + props.node.parent ?? 0">..</router-link>
 | 
			
		||||
  </div>
 | 
			
		||||
  <div>
 | 
			
		||||
    <a href="#" @click="download()" v-if="props.node.isFile">Download</a>
 | 
			
		||||
  </div>
 | 
			
		||||
  <div>
 | 
			
		||||
    <router-link :to="'/fs/' + props.node.parent ?? 0" @click="del()">
 | 
			
		||||
      delete
 | 
			
		||||
    </router-link>
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<style scoped></style>
 | 
			
		||||
							
								
								
									
										45
									
								
								frontend/src/components/FileViewer/AudioVideoDownload.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										45
									
								
								frontend/src/components/FileViewer/AudioVideoDownload.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,45 @@
 | 
			
		||||
import { ref } from 'vue';
 | 
			
		||||
import { NProgress } from 'naive-ui';
 | 
			
		||||
import filesize from 'filesize';
 | 
			
		||||
import { Music, Video } from '@vicons/carbon';
 | 
			
		||||
import type { DialogApiInjection } from 'naive-ui/es/dialog/src/DialogProvider';
 | 
			
		||||
 | 
			
		||||
export default function createAudioVideoDialog(
 | 
			
		||||
	dialog: DialogApiInjection,
 | 
			
		||||
	video: boolean
 | 
			
		||||
) {
 | 
			
		||||
	const progress = ref(0);
 | 
			
		||||
	const total = ref(1);
 | 
			
		||||
	const percentage = ref(0);
 | 
			
		||||
	const dia = dialog.create({
 | 
			
		||||
		title: video ? 'Loading video...' : 'Loading audio...',
 | 
			
		||||
		closable: false,
 | 
			
		||||
		closeOnEsc: false,
 | 
			
		||||
		maskClosable: false,
 | 
			
		||||
		icon: () => (video ? <Video /> : <Music />),
 | 
			
		||||
		content: () => (
 | 
			
		||||
			<NProgress
 | 
			
		||||
				type="line"
 | 
			
		||||
				percentage={percentage.value}
 | 
			
		||||
				height={20}
 | 
			
		||||
				status="info"
 | 
			
		||||
				showIndicator={false}
 | 
			
		||||
			/>
 | 
			
		||||
		),
 | 
			
		||||
		action: () => (
 | 
			
		||||
			<div>
 | 
			
		||||
				{filesize(progress.value, {
 | 
			
		||||
					base: 2,
 | 
			
		||||
					standard: 'jedec'
 | 
			
		||||
				})}
 | 
			
		||||
				/
 | 
			
		||||
				{filesize(total.value, {
 | 
			
		||||
					base: 2,
 | 
			
		||||
					standard: 'jedec'
 | 
			
		||||
				})}
 | 
			
		||||
				- {Math.floor(percentage.value * 1000) / 1000}%
 | 
			
		||||
			</div>
 | 
			
		||||
		)
 | 
			
		||||
	});
 | 
			
		||||
	return { progress, total, percentage, dia };
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										129
									
								
								frontend/src/components/FileViewer/FileViewer.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										129
									
								
								frontend/src/components/FileViewer/FileViewer.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,129 @@
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
import type { TokenInjectType, Responses } from '@/api';
 | 
			
		||||
import { inject, ref, watch } from 'vue';
 | 
			
		||||
import { Download, Play } from '@vicons/carbon';
 | 
			
		||||
import { useDialog, NGrid, NGi, NButton, NImage, NSpin, NIcon } from 'naive-ui';
 | 
			
		||||
import { check_token, FS, isErrorResponse } from '@/api';
 | 
			
		||||
import createAudioVideoDialog from '@/components/FileViewer/AudioVideoDownload';
 | 
			
		||||
import axios from 'axios';
 | 
			
		||||
 | 
			
		||||
const props = defineProps<{
 | 
			
		||||
	node: Responses.GetNode;
 | 
			
		||||
}>();
 | 
			
		||||
 | 
			
		||||
const dialog = useDialog();
 | 
			
		||||
const jwt = inject<TokenInjectType>('jwt') as TokenInjectType;
 | 
			
		||||
 | 
			
		||||
enum fileTypes {
 | 
			
		||||
	UNKNOWN,
 | 
			
		||||
	LOADING,
 | 
			
		||||
	IMAGE,
 | 
			
		||||
	AUDIO,
 | 
			
		||||
	VIDEO
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const fileType = ref<fileTypes>(fileTypes.UNKNOWN);
 | 
			
		||||
const src = ref('');
 | 
			
		||||
 | 
			
		||||
async function download() {
 | 
			
		||||
	const token = await check_token(jwt);
 | 
			
		||||
	if (!token) return;
 | 
			
		||||
	FS.download_file(token, props.node.id);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function loadAudioOrVideo() {
 | 
			
		||||
	const token = await check_token(jwt);
 | 
			
		||||
	if (!token) return;
 | 
			
		||||
	const { progress, total, percentage, dia } = createAudioVideoDialog(
 | 
			
		||||
		dialog,
 | 
			
		||||
		fileType.value === fileTypes.VIDEO
 | 
			
		||||
	);
 | 
			
		||||
	total.value = props.node.size ?? 1;
 | 
			
		||||
	const params = new URLSearchParams();
 | 
			
		||||
	params.append('jwtToken', token);
 | 
			
		||||
	params.append('id', props.node.id.toString());
 | 
			
		||||
	const resp = await axios.post('/api/fs/download', params, {
 | 
			
		||||
		responseType: 'blob',
 | 
			
		||||
		onDownloadProgress: (e: ProgressEvent) => {
 | 
			
		||||
			progress.value = e.loaded;
 | 
			
		||||
			percentage.value = (e.loaded / e.total) * 100;
 | 
			
		||||
		}
 | 
			
		||||
	});
 | 
			
		||||
	dia.destroy();
 | 
			
		||||
	if (resp.status != 200) return;
 | 
			
		||||
	src.value = URL.createObjectURL(resp.data as Blob);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function getType(node: Responses.GetNode) {
 | 
			
		||||
	fileType.value = fileTypes.LOADING;
 | 
			
		||||
	if (src.value.startsWith('blob')) URL.revokeObjectURL(src.value);
 | 
			
		||||
	src.value = '';
 | 
			
		||||
	const token = await check_token(jwt);
 | 
			
		||||
	if (!token) return;
 | 
			
		||||
	const resp = await FS.get_type(token, node.id);
 | 
			
		||||
	if (isErrorResponse(resp)) return;
 | 
			
		||||
	if (resp.type.startsWith('image')) {
 | 
			
		||||
		const dataResp = await FS.download_base64(token, node.id);
 | 
			
		||||
		if (isErrorResponse(dataResp)) return;
 | 
			
		||||
		src.value = dataResp.data;
 | 
			
		||||
		fileType.value = fileTypes.IMAGE;
 | 
			
		||||
	}
 | 
			
		||||
	if (resp.type.startsWith('audio')) fileType.value = fileTypes.AUDIO;
 | 
			
		||||
	if (resp.type.startsWith('video')) fileType.value = fileTypes.VIDEO;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
watch(
 | 
			
		||||
	() => props.node,
 | 
			
		||||
	async (to) => {
 | 
			
		||||
		await getType(to);
 | 
			
		||||
		if (fileType.value === fileTypes.LOADING)
 | 
			
		||||
			fileType.value = fileTypes.UNKNOWN;
 | 
			
		||||
	},
 | 
			
		||||
	{ immediate: true }
 | 
			
		||||
);
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
	<n-grid cols="1" x-gap="16" y-gap="16">
 | 
			
		||||
		<n-gi style="text-align: right">
 | 
			
		||||
			<n-button @click="download()">
 | 
			
		||||
				<template #icon>
 | 
			
		||||
					<n-icon><Download /></n-icon>
 | 
			
		||||
				</template>
 | 
			
		||||
				Download
 | 
			
		||||
			</n-button>
 | 
			
		||||
		</n-gi>
 | 
			
		||||
		<n-gi style="text-align: center">
 | 
			
		||||
			<n-spin v-if="fileType === fileTypes.LOADING" size="large" />
 | 
			
		||||
			<n-image
 | 
			
		||||
				v-else-if="fileType === fileTypes.IMAGE"
 | 
			
		||||
				:src="src"
 | 
			
		||||
				:alt="node.name"
 | 
			
		||||
			/>
 | 
			
		||||
			<template
 | 
			
		||||
				v-else-if="
 | 
			
		||||
					fileType === fileTypes.VIDEO || fileType === fileTypes.AUDIO
 | 
			
		||||
				"
 | 
			
		||||
			>
 | 
			
		||||
				<video
 | 
			
		||||
					v-if="fileType === fileTypes.VIDEO && src !== ''"
 | 
			
		||||
					:src="src"
 | 
			
		||||
					controls
 | 
			
		||||
				/>
 | 
			
		||||
				<audio
 | 
			
		||||
					v-else-if="fileType === fileTypes.AUDIO && src !== ''"
 | 
			
		||||
					:src="src"
 | 
			
		||||
					controls
 | 
			
		||||
				/>
 | 
			
		||||
				<n-button v-else @click="loadAudioOrVideo">
 | 
			
		||||
					<template #icon>
 | 
			
		||||
						<n-icon><Play /></n-icon>
 | 
			
		||||
					</template>
 | 
			
		||||
					Load and play
 | 
			
		||||
				</n-button>
 | 
			
		||||
			</template>
 | 
			
		||||
		</n-gi>
 | 
			
		||||
	</n-grid>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<style scoped></style>
 | 
			
		||||
@@ -1,140 +0,0 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <div class="hello">
 | 
			
		||||
    <h1>{{ msg }}</h1>
 | 
			
		||||
    <p>
 | 
			
		||||
      For a guide and recipes on how to configure / customize this project,<br />
 | 
			
		||||
      check out the
 | 
			
		||||
      <a href="https://cli.vuejs.org" target="_blank" rel="noopener"
 | 
			
		||||
        >vue-cli documentation</a
 | 
			
		||||
      >.
 | 
			
		||||
    </p>
 | 
			
		||||
    <h3>Installed CLI Plugins</h3>
 | 
			
		||||
    <ul>
 | 
			
		||||
      <li>
 | 
			
		||||
        <a
 | 
			
		||||
          href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-babel"
 | 
			
		||||
          target="_blank"
 | 
			
		||||
          rel="noopener"
 | 
			
		||||
          >babel</a
 | 
			
		||||
        >
 | 
			
		||||
      </li>
 | 
			
		||||
      <li>
 | 
			
		||||
        <a
 | 
			
		||||
          href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-router"
 | 
			
		||||
          target="_blank"
 | 
			
		||||
          rel="noopener"
 | 
			
		||||
          >router</a
 | 
			
		||||
        >
 | 
			
		||||
      </li>
 | 
			
		||||
      <li>
 | 
			
		||||
        <a
 | 
			
		||||
          href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-vuex"
 | 
			
		||||
          target="_blank"
 | 
			
		||||
          rel="noopener"
 | 
			
		||||
          >vuex</a
 | 
			
		||||
        >
 | 
			
		||||
      </li>
 | 
			
		||||
      <li>
 | 
			
		||||
        <a
 | 
			
		||||
          href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-eslint"
 | 
			
		||||
          target="_blank"
 | 
			
		||||
          rel="noopener"
 | 
			
		||||
          >eslint</a
 | 
			
		||||
        >
 | 
			
		||||
      </li>
 | 
			
		||||
      <li>
 | 
			
		||||
        <a
 | 
			
		||||
          href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-typescript"
 | 
			
		||||
          target="_blank"
 | 
			
		||||
          rel="noopener"
 | 
			
		||||
          >typescript</a
 | 
			
		||||
        >
 | 
			
		||||
      </li>
 | 
			
		||||
    </ul>
 | 
			
		||||
    <h3>Essential Links</h3>
 | 
			
		||||
    <ul>
 | 
			
		||||
      <li>
 | 
			
		||||
        <a href="https://vuejs.org" target="_blank" rel="noopener">Core Docs</a>
 | 
			
		||||
      </li>
 | 
			
		||||
      <li>
 | 
			
		||||
        <a href="https://forum.vuejs.org" target="_blank" rel="noopener"
 | 
			
		||||
          >Forum</a
 | 
			
		||||
        >
 | 
			
		||||
      </li>
 | 
			
		||||
      <li>
 | 
			
		||||
        <a href="https://chat.vuejs.org" target="_blank" rel="noopener"
 | 
			
		||||
          >Community Chat</a
 | 
			
		||||
        >
 | 
			
		||||
      </li>
 | 
			
		||||
      <li>
 | 
			
		||||
        <a href="https://twitter.com/vuejs" target="_blank" rel="noopener"
 | 
			
		||||
          >Twitter</a
 | 
			
		||||
        >
 | 
			
		||||
      </li>
 | 
			
		||||
      <li>
 | 
			
		||||
        <a href="https://news.vuejs.org" target="_blank" rel="noopener">News</a>
 | 
			
		||||
      </li>
 | 
			
		||||
    </ul>
 | 
			
		||||
    <h3>Ecosystem</h3>
 | 
			
		||||
    <ul>
 | 
			
		||||
      <li>
 | 
			
		||||
        <a href="https://router.vuejs.org" target="_blank" rel="noopener"
 | 
			
		||||
          >vue-router</a
 | 
			
		||||
        >
 | 
			
		||||
      </li>
 | 
			
		||||
      <li>
 | 
			
		||||
        <a href="https://vuex.vuejs.org" target="_blank" rel="noopener">vuex</a>
 | 
			
		||||
      </li>
 | 
			
		||||
      <li>
 | 
			
		||||
        <a
 | 
			
		||||
          href="https://github.com/vuejs/vue-devtools#vue-devtools"
 | 
			
		||||
          target="_blank"
 | 
			
		||||
          rel="noopener"
 | 
			
		||||
          >vue-devtools</a
 | 
			
		||||
        >
 | 
			
		||||
      </li>
 | 
			
		||||
      <li>
 | 
			
		||||
        <a href="https://vue-loader.vuejs.org" target="_blank" rel="noopener"
 | 
			
		||||
          >vue-loader</a
 | 
			
		||||
        >
 | 
			
		||||
      </li>
 | 
			
		||||
      <li>
 | 
			
		||||
        <a
 | 
			
		||||
          href="https://github.com/vuejs/awesome-vue"
 | 
			
		||||
          target="_blank"
 | 
			
		||||
          rel="noopener"
 | 
			
		||||
          >awesome-vue</a
 | 
			
		||||
        >
 | 
			
		||||
      </li>
 | 
			
		||||
    </ul>
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
import { defineComponent } from "vue";
 | 
			
		||||
 | 
			
		||||
export default defineComponent({
 | 
			
		||||
  name: "HelloWorld",
 | 
			
		||||
  props: {
 | 
			
		||||
    msg: String,
 | 
			
		||||
  },
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<!-- Add "scoped" attribute to limit CSS to this component only -->
 | 
			
		||||
<style scoped lang="scss">
 | 
			
		||||
h3 {
 | 
			
		||||
  margin: 40px 0 0;
 | 
			
		||||
}
 | 
			
		||||
ul {
 | 
			
		||||
  list-style-type: none;
 | 
			
		||||
  padding: 0;
 | 
			
		||||
}
 | 
			
		||||
li {
 | 
			
		||||
  display: inline-block;
 | 
			
		||||
  margin: 0 10px;
 | 
			
		||||
}
 | 
			
		||||
a {
 | 
			
		||||
  color: #42b983;
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
							
								
								
									
										13
									
								
								frontend/src/components/NLink.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								frontend/src/components/NLink.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,13 @@
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
import { NA } from 'naive-ui';
 | 
			
		||||
 | 
			
		||||
defineProps<{
 | 
			
		||||
	to: string;
 | 
			
		||||
}>();
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
	<router-link :to="to" #="{ navigate, href }" custom>
 | 
			
		||||
		<n-a :href="href" @click="navigate"><slot /></n-a>
 | 
			
		||||
	</router-link>
 | 
			
		||||
</template>
 | 
			
		||||
@@ -1,52 +1,93 @@
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
import type { Status } from "naive-ui/es/progress/src/interface";
 | 
			
		||||
import { defineProps, defineExpose, ref } from "vue";
 | 
			
		||||
import { isErrorResponse, FS } from "@/api";
 | 
			
		||||
import { NProgress } from "naive-ui";
 | 
			
		||||
import filesize from "filesize";
 | 
			
		||||
import type { Status } from 'naive-ui/es/progress/src/interface';
 | 
			
		||||
import type { UploadFile } from '@/api';
 | 
			
		||||
import { ref } from 'vue';
 | 
			
		||||
import { isErrorResponse, FS } from '@/api';
 | 
			
		||||
import { NProgress } from 'naive-ui';
 | 
			
		||||
import filesize from 'filesize';
 | 
			
		||||
 | 
			
		||||
const props = defineProps<{
 | 
			
		||||
  file: File;
 | 
			
		||||
	file: UploadFile;
 | 
			
		||||
}>();
 | 
			
		||||
 | 
			
		||||
const progress = ref(0);
 | 
			
		||||
const percentage = ref(0);
 | 
			
		||||
const err = ref("");
 | 
			
		||||
const status = ref<Status>("info");
 | 
			
		||||
const err = ref('');
 | 
			
		||||
const status = ref<Status>('info');
 | 
			
		||||
const shown = ref(true);
 | 
			
		||||
 | 
			
		||||
async function startUpload(parent: number, token: string) {
 | 
			
		||||
  const resp = await FS.upload_file(token, parent, props.file, (e) => {
 | 
			
		||||
    progress.value = e.loaded;
 | 
			
		||||
    percentage.value = (e.loaded / e.total) * 100;
 | 
			
		||||
  });
 | 
			
		||||
  percentage.value = 100;
 | 
			
		||||
  if (isErrorResponse(resp)) {
 | 
			
		||||
    err.value = resp.message ?? "Error";
 | 
			
		||||
    status.value = "error";
 | 
			
		||||
  } else status.value = "success";
 | 
			
		||||
async function startUpload(token: string, done: () => void) {
 | 
			
		||||
	const resp = await FS.upload_file(token, props.file, (e) => {
 | 
			
		||||
		progress.value = e.loaded;
 | 
			
		||||
		percentage.value = (e.loaded / e.total) * 100;
 | 
			
		||||
		if (e.loaded == e.total) done();
 | 
			
		||||
	});
 | 
			
		||||
	percentage.value = 100;
 | 
			
		||||
	if (isErrorResponse(resp)) {
 | 
			
		||||
		err.value = resp.message ?? 'Error';
 | 
			
		||||
		status.value = 'error';
 | 
			
		||||
	} else {
 | 
			
		||||
		status.value = 'success';
 | 
			
		||||
		shown.value = false;
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
defineExpose({
 | 
			
		||||
  startUpload,
 | 
			
		||||
	startUpload
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
  <div v-if="percentage < 100">
 | 
			
		||||
    {{ file.name }} - {{ filesize(progress) }} / {{ filesize(file.size) }} -
 | 
			
		||||
    {{ Math.floor(percentage * 1000) / 1000 }}%
 | 
			
		||||
  </div>
 | 
			
		||||
  <div v-else-if="err !== ''">{{ file.name }} - Error: {{ err }}</div>
 | 
			
		||||
  <div v-else>{{ file.name }} - Completed</div>
 | 
			
		||||
  <n-progress
 | 
			
		||||
    type="line"
 | 
			
		||||
    :percentage="percentage"
 | 
			
		||||
    :height="20"
 | 
			
		||||
    :status="status"
 | 
			
		||||
    border-radius="10px 0"
 | 
			
		||||
    fill-border-radius="10px 0"
 | 
			
		||||
    :show-indicator="false"
 | 
			
		||||
  />
 | 
			
		||||
	<Transition name="slide-up">
 | 
			
		||||
		<div class="container" v-show="shown">
 | 
			
		||||
			<div v-if="percentage < 100">
 | 
			
		||||
				{{ file.fullName }} -
 | 
			
		||||
				{{
 | 
			
		||||
					filesize(progress, {
 | 
			
		||||
						base: 2,
 | 
			
		||||
						standard: 'jedec'
 | 
			
		||||
					})
 | 
			
		||||
				}}
 | 
			
		||||
				/
 | 
			
		||||
				{{
 | 
			
		||||
					filesize(file.file.size, {
 | 
			
		||||
						base: 2,
 | 
			
		||||
						standard: 'jedec'
 | 
			
		||||
					})
 | 
			
		||||
				}}
 | 
			
		||||
				- {{ Math.floor(percentage * 1000) / 1000 }}%
 | 
			
		||||
			</div>
 | 
			
		||||
			<div v-else-if="err !== ''">
 | 
			
		||||
				{{ file.fullName }} - Error: {{ err }}
 | 
			
		||||
			</div>
 | 
			
		||||
			<div v-else>{{ file.fullName }} - Completed</div>
 | 
			
		||||
			<n-progress
 | 
			
		||||
				type="line"
 | 
			
		||||
				:percentage="percentage"
 | 
			
		||||
				:height="20"
 | 
			
		||||
				:status="status"
 | 
			
		||||
				border-radius="10px 0"
 | 
			
		||||
				fill-border-radius="10px 0"
 | 
			
		||||
				:show-indicator="false"
 | 
			
		||||
			/>
 | 
			
		||||
		</div>
 | 
			
		||||
	</Transition>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<style scoped></style>
 | 
			
		||||
<style scoped lang="scss">
 | 
			
		||||
.container {
 | 
			
		||||
	height: 60px;
 | 
			
		||||
	padding: 8px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.slide-up-leave-active {
 | 
			
		||||
	transition: all 2s ease-out;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.slide-up-leave-to {
 | 
			
		||||
	height: 0;
 | 
			
		||||
	padding: 0 8px;
 | 
			
		||||
	opacity: 0;
 | 
			
		||||
	transform: translateY(-60px);
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										203
									
								
								frontend/src/components/UploadDialog/UploadField.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										203
									
								
								frontend/src/components/UploadDialog/UploadField.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,203 @@
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
import type { TokenInjectType, Responses, UploadFile } from '@/api';
 | 
			
		||||
import { inject, ref } from 'vue';
 | 
			
		||||
import { useMessage, NModal, NText, NIcon } from 'naive-ui';
 | 
			
		||||
import { CloudUpload } from '@vicons/carbon';
 | 
			
		||||
import { FS, check_token, isErrorResponse } from '@/api';
 | 
			
		||||
import UploadFileDialog from '@/components/UploadDialog/UploadFileDialog.vue';
 | 
			
		||||
import { loadingMsgWrapper } from '@/utils';
 | 
			
		||||
 | 
			
		||||
const message = useMessage();
 | 
			
		||||
 | 
			
		||||
const props = defineProps<{
 | 
			
		||||
	node: Responses.GetNode;
 | 
			
		||||
}>();
 | 
			
		||||
 | 
			
		||||
const jwt = inject<TokenInjectType>('jwt') as TokenInjectType;
 | 
			
		||||
 | 
			
		||||
const emit = defineEmits<{
 | 
			
		||||
	(e: 'reloadNode'): void;
 | 
			
		||||
}>();
 | 
			
		||||
 | 
			
		||||
const uploadArea = ref<HTMLDivElement>();
 | 
			
		||||
const fileInput = ref<HTMLInputElement>();
 | 
			
		||||
const uploadDialog = ref();
 | 
			
		||||
const uploadDialogShow = ref(false);
 | 
			
		||||
 | 
			
		||||
const files = ref<UploadFile[]>([]);
 | 
			
		||||
 | 
			
		||||
function startDrag() {
 | 
			
		||||
	uploadArea.value?.classList.add('uploadActive');
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function stopDrag() {
 | 
			
		||||
	uploadArea.value?.classList.remove('uploadActive');
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function openBrowser() {
 | 
			
		||||
	fileInput.value?.click();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function browserChanged(event: InputEvent) {
 | 
			
		||||
	files.value = Array.from(
 | 
			
		||||
		(event.target as HTMLInputElement).files ?? []
 | 
			
		||||
	).map((file) => {
 | 
			
		||||
		return {
 | 
			
		||||
			parent: props.node.id,
 | 
			
		||||
			fullName: file.name,
 | 
			
		||||
			file
 | 
			
		||||
		};
 | 
			
		||||
	});
 | 
			
		||||
	uploadFiles();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
interface FileSystemDirectoryReader {
 | 
			
		||||
	readEntries(
 | 
			
		||||
		successCallback: (entries: FileSystemEntry[]) => void,
 | 
			
		||||
		errorCallback?: (err: DOMException) => void
 | 
			
		||||
	): void;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
interface FileSystemEntry {
 | 
			
		||||
	readonly fullPath: string;
 | 
			
		||||
	readonly isDirectory: boolean;
 | 
			
		||||
	readonly isFile: boolean;
 | 
			
		||||
	readonly name: string;
 | 
			
		||||
	file(
 | 
			
		||||
		successCallback: (file: File) => void,
 | 
			
		||||
		errorCallback?: (err: DOMException) => void
 | 
			
		||||
	): void;
 | 
			
		||||
	createReader(): FileSystemDirectoryReader;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const asyncReadEntries = async (
 | 
			
		||||
	reader: FileSystemDirectoryReader
 | 
			
		||||
): Promise<FileSystemEntry[]> =>
 | 
			
		||||
	new Promise((resolve, reject) => reader.readEntries(resolve, reject));
 | 
			
		||||
 | 
			
		||||
const getFile = async (entry: FileSystemEntry): Promise<File> =>
 | 
			
		||||
	new Promise((resolve, reject) => entry.file(resolve, reject));
 | 
			
		||||
 | 
			
		||||
async function processDirOrFile(
 | 
			
		||||
	entry: FileSystemEntry,
 | 
			
		||||
	parent: number,
 | 
			
		||||
	token: string
 | 
			
		||||
) {
 | 
			
		||||
	if (entry.isDirectory) {
 | 
			
		||||
		const resp = await FS.create_folder(token, parent, entry.name);
 | 
			
		||||
		if (isErrorResponse(resp)) return;
 | 
			
		||||
		if ('exists' in resp && resp.isFile) return;
 | 
			
		||||
		const reader = entry.createReader();
 | 
			
		||||
		let entries = [];
 | 
			
		||||
		do {
 | 
			
		||||
			try {
 | 
			
		||||
				entries = await asyncReadEntries(reader);
 | 
			
		||||
				entries.forEach((e) => processDirOrFile(e, resp.id, token));
 | 
			
		||||
			} catch {
 | 
			
		||||
				break;
 | 
			
		||||
			}
 | 
			
		||||
		} while (entries.length != 0);
 | 
			
		||||
	} else
 | 
			
		||||
		files.value.push({
 | 
			
		||||
			parent: parent,
 | 
			
		||||
			fullName: entry.fullPath.slice(1),
 | 
			
		||||
			file: await getFile(entry)
 | 
			
		||||
		});
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const filesDropped = loadingMsgWrapper(message, async (event: DragEvent) => {
 | 
			
		||||
	stopDrag();
 | 
			
		||||
	if (!event.dataTransfer) return;
 | 
			
		||||
	const token = await check_token(jwt);
 | 
			
		||||
	if (!token) return;
 | 
			
		||||
	files.value = [];
 | 
			
		||||
	for (const file of event.dataTransfer.items) {
 | 
			
		||||
		const entry = file.webkitGetAsEntry();
 | 
			
		||||
		if (entry)
 | 
			
		||||
			await processDirOrFile(
 | 
			
		||||
				entry as unknown as FileSystemEntry,
 | 
			
		||||
				props.node.id,
 | 
			
		||||
				token
 | 
			
		||||
			);
 | 
			
		||||
	}
 | 
			
		||||
	uploadFiles();
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
function uploadFiles() {
 | 
			
		||||
	if (files.value.length == 0) return;
 | 
			
		||||
	uploadDialogShow.value = true;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function uploadFilesDialogOpen() {
 | 
			
		||||
	await uploadDialog.value?.startUpload();
 | 
			
		||||
	uploadDialogShow.value = false;
 | 
			
		||||
	if (fileInput.value) fileInput.value.value = '';
 | 
			
		||||
	emit('reloadNode');
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
	<div
 | 
			
		||||
		class="uploadArea"
 | 
			
		||||
		ref="uploadArea"
 | 
			
		||||
		@drop.prevent
 | 
			
		||||
		@dragenter.prevent
 | 
			
		||||
		@dragover.prevent
 | 
			
		||||
		@dragleave.prevent
 | 
			
		||||
		@dragend.prevent
 | 
			
		||||
		@click="openBrowser"
 | 
			
		||||
		@drop="filesDropped"
 | 
			
		||||
		@dragenter="startDrag"
 | 
			
		||||
		@dragover="startDrag"
 | 
			
		||||
		@dragleave="stopDrag"
 | 
			
		||||
		@dragend="stopDrag"
 | 
			
		||||
	>
 | 
			
		||||
		<input type="file" ref="fileInput" multiple @input="browserChanged" />
 | 
			
		||||
		<div>
 | 
			
		||||
			<n-icon size="2em">
 | 
			
		||||
				<CloudUpload />
 | 
			
		||||
			</n-icon>
 | 
			
		||||
		</div>
 | 
			
		||||
		<n-text>
 | 
			
		||||
			Click or drag here to upload files
 | 
			
		||||
		</n-text>
 | 
			
		||||
	</div>
 | 
			
		||||
	<n-modal
 | 
			
		||||
		v-model:show="uploadDialogShow"
 | 
			
		||||
		:close-on-esc="false"
 | 
			
		||||
		:mask-closable="false"
 | 
			
		||||
		:on-after-enter="uploadFilesDialogOpen"
 | 
			
		||||
	>
 | 
			
		||||
		<UploadFileDialog ref="uploadDialog" :files="files" />
 | 
			
		||||
	</n-modal>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<style scoped lang="scss">
 | 
			
		||||
.uploadArea {
 | 
			
		||||
	border: 1px dashed #ddd;
 | 
			
		||||
	border-radius: 3px;
 | 
			
		||||
	cursor: pointer;
 | 
			
		||||
	background-color: rgb(250, 250, 252);
 | 
			
		||||
 | 
			
		||||
	text-align: center;
 | 
			
		||||
 | 
			
		||||
	transition: border-color 250ms ease-out, background-color 250ms ease-out;
 | 
			
		||||
 | 
			
		||||
	padding: 20px;
 | 
			
		||||
 | 
			
		||||
	input {
 | 
			
		||||
		display: block;
 | 
			
		||||
		width: 0;
 | 
			
		||||
		height: 0;
 | 
			
		||||
		opacity: 0;
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.uploadArea:hover {
 | 
			
		||||
	border-color: #888;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.uploadActive {
 | 
			
		||||
	background-color: rgb(240, 252, 240);
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
@@ -1,44 +1,44 @@
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
import type { TokenInjectType } from "@/api";
 | 
			
		||||
import { defineProps, defineExpose, ref, inject } from "vue";
 | 
			
		||||
import { check_token } from "@/api";
 | 
			
		||||
import UploadEntry from "@/components/UploadDialog/UploadEntry.vue";
 | 
			
		||||
import { NCard } from "naive-ui";
 | 
			
		||||
import type { TokenInjectType, UploadFile } from '@/api';
 | 
			
		||||
import { ref, inject } from 'vue';
 | 
			
		||||
import { check_token } from '@/api';
 | 
			
		||||
import UploadEntry from '@/components/UploadDialog/UploadEntry.vue';
 | 
			
		||||
import { NCard } from 'naive-ui';
 | 
			
		||||
 | 
			
		||||
const jwt = inject<TokenInjectType>("jwt") as TokenInjectType;
 | 
			
		||||
const jwt = inject<TokenInjectType>('jwt') as TokenInjectType;
 | 
			
		||||
 | 
			
		||||
const entries = ref<typeof UploadEntry[]>([]);
 | 
			
		||||
const done = ref(false);
 | 
			
		||||
let canCloseResolve: (value: unknown) => void = () => null;
 | 
			
		||||
const canClose = new Promise((r) => (canCloseResolve = r));
 | 
			
		||||
 | 
			
		||||
async function startUpload(parent: number) {
 | 
			
		||||
  const token = await check_token(jwt);
 | 
			
		||||
  if (!token) return;
 | 
			
		||||
  await Promise.all(
 | 
			
		||||
    entries.value.map((entry) => entry.startUpload(parent, token))
 | 
			
		||||
  );
 | 
			
		||||
  done.value = true;
 | 
			
		||||
  await canClose;
 | 
			
		||||
async function startUpload() {
 | 
			
		||||
	const token = await check_token(jwt);
 | 
			
		||||
	if (!token) return;
 | 
			
		||||
	const ents: typeof UploadEntry[] = entries.value;
 | 
			
		||||
	const allProms: Promise<void>[] = [];
 | 
			
		||||
	for (const entry of ents) {
 | 
			
		||||
		await new Promise<void>((resolve) =>
 | 
			
		||||
			allProms.push(entry.startUpload(token, resolve))
 | 
			
		||||
		);
 | 
			
		||||
	}
 | 
			
		||||
	await Promise.all(allProms);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
defineExpose({
 | 
			
		||||
  startUpload,
 | 
			
		||||
	startUpload
 | 
			
		||||
});
 | 
			
		||||
defineProps<{
 | 
			
		||||
  files: File[];
 | 
			
		||||
	files: UploadFile[];
 | 
			
		||||
}>();
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
  <n-card title="Upload Files">
 | 
			
		||||
    <div>
 | 
			
		||||
      <UploadEntry v-for="f in files" :key="f.name" ref="entries" :file="f" />
 | 
			
		||||
    </div>
 | 
			
		||||
    <div>
 | 
			
		||||
      <button v-if="done" @click="canCloseResolve(null)">Close</button>
 | 
			
		||||
    </div>
 | 
			
		||||
  </n-card>
 | 
			
		||||
	<n-card title="Uploading files" style="margin: 20px">
 | 
			
		||||
		<UploadEntry
 | 
			
		||||
			v-for="f in files"
 | 
			
		||||
			:key="f.file.name"
 | 
			
		||||
			ref="entries"
 | 
			
		||||
			:file="f"
 | 
			
		||||
		/>
 | 
			
		||||
	</n-card>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<style scoped></style>
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										73
									
								
								frontend/src/components/UserChangePw.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										73
									
								
								frontend/src/components/UserChangePw.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,73 @@
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
import type { TokenInjectType } from '@/api';
 | 
			
		||||
import { inject, ref } from 'vue';
 | 
			
		||||
import { Auth, check_token, isErrorResponse } from '@/api';
 | 
			
		||||
import { useMessage, NInput, NGrid, NGi, NButton, NCard } from 'naive-ui';
 | 
			
		||||
import { loadingMsgWrapper } from '@/utils';
 | 
			
		||||
 | 
			
		||||
const message = useMessage();
 | 
			
		||||
 | 
			
		||||
const oldPw = ref('');
 | 
			
		||||
const newPw = ref('');
 | 
			
		||||
const newPw2 = ref('');
 | 
			
		||||
const jwt = inject<TokenInjectType>('jwt') as TokenInjectType;
 | 
			
		||||
 | 
			
		||||
const changePw = loadingMsgWrapper(message, async () => {
 | 
			
		||||
	if (oldPw.value === '' || newPw.value === '' || newPw2.value === '') {
 | 
			
		||||
		message.error('Password missing');
 | 
			
		||||
		return;
 | 
			
		||||
	}
 | 
			
		||||
	if (newPw.value !== newPw2.value) {
 | 
			
		||||
		message.error("Passwords don't match");
 | 
			
		||||
		return;
 | 
			
		||||
	}
 | 
			
		||||
	const token = await check_token(jwt);
 | 
			
		||||
	if (!token) return;
 | 
			
		||||
	const res = await Auth.change_password(oldPw.value, newPw.value, token);
 | 
			
		||||
	if (isErrorResponse(res))
 | 
			
		||||
		message.error(`Password change failed: ${res.message}`);
 | 
			
		||||
	else jwt.logout();
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
function onKey(event: KeyboardEvent) {
 | 
			
		||||
	if (event.key == 'Enter') changePw();
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
	<n-card title="Change password" embedded>
 | 
			
		||||
		<n-grid cols="1" x-gap="16" y-gap="16">
 | 
			
		||||
			<n-gi>
 | 
			
		||||
				<n-input
 | 
			
		||||
					type="password"
 | 
			
		||||
					placeholder="Old password"
 | 
			
		||||
					v-model:value="oldPw"
 | 
			
		||||
					@keyup="onKey"
 | 
			
		||||
				/>
 | 
			
		||||
			</n-gi>
 | 
			
		||||
			<n-gi>
 | 
			
		||||
				<n-input
 | 
			
		||||
					type="password"
 | 
			
		||||
					placeholder="New password"
 | 
			
		||||
					v-model:value="newPw"
 | 
			
		||||
					@keyup="onKey"
 | 
			
		||||
				/>
 | 
			
		||||
			</n-gi>
 | 
			
		||||
			<n-gi>
 | 
			
		||||
				<n-input
 | 
			
		||||
					type="password"
 | 
			
		||||
					placeholder="Repeat new password"
 | 
			
		||||
					v-model:value="newPw2"
 | 
			
		||||
					@keyup="onKey"
 | 
			
		||||
				/>
 | 
			
		||||
			</n-gi>
 | 
			
		||||
			<n-gi>
 | 
			
		||||
				<n-button type="info" @click="changePw">
 | 
			
		||||
					Change password
 | 
			
		||||
				</n-button>
 | 
			
		||||
			</n-gi>
 | 
			
		||||
		</n-grid>
 | 
			
		||||
	</n-card>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<style scoped></style>
 | 
			
		||||
@@ -1,8 +1,150 @@
 | 
			
		||||
export * as Requests from "./requests";
 | 
			
		||||
export * as Responses from "./responses";
 | 
			
		||||
export {
 | 
			
		||||
  UserRole,
 | 
			
		||||
  validateSync,
 | 
			
		||||
  validateAsync,
 | 
			
		||||
  validateAsyncInline,
 | 
			
		||||
} from "./utils";
 | 
			
		||||
export enum UserRole {
 | 
			
		||||
	ADMIN = 2,
 | 
			
		||||
	USER = 1,
 | 
			
		||||
	DISABLED = 0
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface UploadFile {
 | 
			
		||||
	parent: number;
 | 
			
		||||
	fullName: string;
 | 
			
		||||
	file: File;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// eslint-disable-next-line @typescript-eslint/no-namespace
 | 
			
		||||
export namespace Requests {
 | 
			
		||||
	// eslint-disable-next-line @typescript-eslint/no-empty-interface
 | 
			
		||||
	export interface Base {}
 | 
			
		||||
 | 
			
		||||
	export interface Admin extends Base {
 | 
			
		||||
		user: number;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	export interface SetUserRole extends Admin {
 | 
			
		||||
		role: UserRole;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	export interface SignUp extends Base {
 | 
			
		||||
		username: string;
 | 
			
		||||
		password: string;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	export interface Login extends SignUp {
 | 
			
		||||
		otp?: string;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	export interface TfaSetup extends Base {
 | 
			
		||||
		mail: boolean;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	export interface TfaComplete extends Base {
 | 
			
		||||
		mail: boolean;
 | 
			
		||||
		code: string;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	export interface ChangePassword extends Base {
 | 
			
		||||
		oldPassword: string;
 | 
			
		||||
		newPassword: string;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	export interface CreateFolder extends Base {
 | 
			
		||||
		parent: number;
 | 
			
		||||
		name: string;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	export interface CreateZip extends Base {
 | 
			
		||||
		nodes: number[];
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// eslint-disable-next-line @typescript-eslint/no-namespace
 | 
			
		||||
export namespace Responses {
 | 
			
		||||
	export interface Base {
 | 
			
		||||
		statusCode: number;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	export interface Success extends Base {
 | 
			
		||||
		statusCode: 200;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	export interface Error extends Base {
 | 
			
		||||
		statusCode: 400 | 401 | 403;
 | 
			
		||||
		message?: string;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	export interface Login extends Success {
 | 
			
		||||
		jwt: string;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	export interface RequestsTotpTfa extends Success {
 | 
			
		||||
		qrCode: string;
 | 
			
		||||
		secret: string;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	export interface GetRoot extends Success {
 | 
			
		||||
		rootId: number;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	export interface GetNodeEntry {
 | 
			
		||||
		id: number;
 | 
			
		||||
		name: string;
 | 
			
		||||
		isFile: boolean;
 | 
			
		||||
		preview: boolean;
 | 
			
		||||
		parent: number | null;
 | 
			
		||||
		size?: number;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	export interface GetNode extends Success, GetNodeEntry {
 | 
			
		||||
		children?: GetNodeEntry[];
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	export interface PathSegment {
 | 
			
		||||
		path: string;
 | 
			
		||||
		node?: number;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	export interface GetPath extends Success {
 | 
			
		||||
		segments: PathSegment[];
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	export interface CreateFolder extends Success {
 | 
			
		||||
		id: number;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	export interface CreateFolderExists extends Success {
 | 
			
		||||
		exists: true;
 | 
			
		||||
		id: number;
 | 
			
		||||
		isFile: boolean;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	export interface CreateZip extends Success {
 | 
			
		||||
		done: boolean;
 | 
			
		||||
		progress?: number;
 | 
			
		||||
		total?: number;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	export interface DownloadBase64 extends Success {
 | 
			
		||||
		data: string;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	export interface GetType extends Success {
 | 
			
		||||
		type: string;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	export interface UserInfo extends Success {
 | 
			
		||||
		name: string;
 | 
			
		||||
		gitlab: boolean;
 | 
			
		||||
		tfaEnabled: boolean;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	export interface GetUsersEntry {
 | 
			
		||||
		id: number;
 | 
			
		||||
		gitlab: boolean;
 | 
			
		||||
		name: string;
 | 
			
		||||
		role: UserRole;
 | 
			
		||||
		tfaEnabled: boolean;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	export interface GetUsers extends Success {
 | 
			
		||||
		users: GetUsersEntry[];
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,17 +0,0 @@
 | 
			
		||||
import { BaseRequest } from "./base";
 | 
			
		||||
import { IsEnum, IsNumber } from "class-validator";
 | 
			
		||||
import { UserRole } from "@/dto";
 | 
			
		||||
 | 
			
		||||
export class AdminRequest extends BaseRequest {
 | 
			
		||||
  @IsNumber()
 | 
			
		||||
  user: number;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export class SetUserRole extends AdminRequest {
 | 
			
		||||
  @IsEnum(UserRole)
 | 
			
		||||
  role: UserRole;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export class LogoutAll extends AdminRequest {}
 | 
			
		||||
export class DeleteUser extends AdminRequest {}
 | 
			
		||||
export class DisableTfa extends AdminRequest {}
 | 
			
		||||
@@ -1,50 +0,0 @@
 | 
			
		||||
import { BaseRequest } from "./base";
 | 
			
		||||
import {
 | 
			
		||||
  IsBoolean,
 | 
			
		||||
  IsEmail,
 | 
			
		||||
  IsNotEmpty,
 | 
			
		||||
  IsOptional,
 | 
			
		||||
  IsString,
 | 
			
		||||
} from "class-validator";
 | 
			
		||||
 | 
			
		||||
export class SignUpRequest extends BaseRequest {
 | 
			
		||||
  @IsEmail()
 | 
			
		||||
  username: string;
 | 
			
		||||
 | 
			
		||||
  @IsNotEmpty()
 | 
			
		||||
  @IsString()
 | 
			
		||||
  password: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export class LoginRequest extends SignUpRequest {
 | 
			
		||||
  @IsOptional()
 | 
			
		||||
  @IsNotEmpty()
 | 
			
		||||
  @IsString()
 | 
			
		||||
  otp?: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export class TfaSetup extends BaseRequest {
 | 
			
		||||
  @IsNotEmpty()
 | 
			
		||||
  @IsBoolean()
 | 
			
		||||
  mail: boolean;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export class TfaComplete extends BaseRequest {
 | 
			
		||||
  @IsNotEmpty()
 | 
			
		||||
  @IsBoolean()
 | 
			
		||||
  mail: boolean;
 | 
			
		||||
 | 
			
		||||
  @IsNotEmpty()
 | 
			
		||||
  @IsString()
 | 
			
		||||
  code: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export class ChangePasswordRequest extends BaseRequest {
 | 
			
		||||
  @IsNotEmpty()
 | 
			
		||||
  @IsString()
 | 
			
		||||
  oldPassword: string;
 | 
			
		||||
 | 
			
		||||
  @IsNotEmpty()
 | 
			
		||||
  @IsString()
 | 
			
		||||
  newPassword: string;
 | 
			
		||||
}
 | 
			
		||||
@@ -1 +0,0 @@
 | 
			
		||||
export class BaseRequest {}
 | 
			
		||||
@@ -1,14 +0,0 @@
 | 
			
		||||
import { BaseRequest } from "./base";
 | 
			
		||||
import { IsInt, IsNotEmpty, IsString, Min } from "class-validator";
 | 
			
		||||
 | 
			
		||||
export class CreateFolderRequest extends BaseRequest {
 | 
			
		||||
  @IsInt()
 | 
			
		||||
  @Min(1)
 | 
			
		||||
  parent: number;
 | 
			
		||||
 | 
			
		||||
  @IsNotEmpty()
 | 
			
		||||
  @IsString()
 | 
			
		||||
  name: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export class CreateFileRequest extends CreateFolderRequest {}
 | 
			
		||||
@@ -1,4 +0,0 @@
 | 
			
		||||
export * from "./base";
 | 
			
		||||
export * as Auth from "./auth";
 | 
			
		||||
export * as FS from "./fs";
 | 
			
		||||
export * as Admin from "./admin";
 | 
			
		||||
@@ -1,61 +0,0 @@
 | 
			
		||||
import { SuccessResponse } from "./base";
 | 
			
		||||
import {
 | 
			
		||||
  IsArray,
 | 
			
		||||
  IsBoolean,
 | 
			
		||||
  IsEnum,
 | 
			
		||||
  IsNotEmpty,
 | 
			
		||||
  IsNumber,
 | 
			
		||||
  IsString,
 | 
			
		||||
  ValidateNested,
 | 
			
		||||
} from "class-validator";
 | 
			
		||||
import { UserRole, ValidateConstructor } from "../utils";
 | 
			
		||||
 | 
			
		||||
@ValidateConstructor
 | 
			
		||||
export class GetUsersEntry {
 | 
			
		||||
  constructor(
 | 
			
		||||
    id: number,
 | 
			
		||||
    gitlab: boolean,
 | 
			
		||||
    name: string,
 | 
			
		||||
    role: UserRole,
 | 
			
		||||
    tfaEnabled: boolean
 | 
			
		||||
  ) {
 | 
			
		||||
    this.id = id;
 | 
			
		||||
    this.gitlab = gitlab;
 | 
			
		||||
    this.name = name;
 | 
			
		||||
    this.role = role;
 | 
			
		||||
    this.tfaEnabled = tfaEnabled;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @IsNumber()
 | 
			
		||||
  id: number;
 | 
			
		||||
 | 
			
		||||
  @IsBoolean()
 | 
			
		||||
  gitlab: boolean;
 | 
			
		||||
 | 
			
		||||
  @IsString()
 | 
			
		||||
  @IsNotEmpty()
 | 
			
		||||
  name: string;
 | 
			
		||||
 | 
			
		||||
  @IsEnum(UserRole)
 | 
			
		||||
  role: UserRole;
 | 
			
		||||
 | 
			
		||||
  @IsBoolean()
 | 
			
		||||
  tfaEnabled: boolean;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ValidateConstructor
 | 
			
		||||
export class GetUsers extends SuccessResponse {
 | 
			
		||||
  constructor(users: GetUsersEntry[]) {
 | 
			
		||||
    super();
 | 
			
		||||
    this.users = users;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @IsArray()
 | 
			
		||||
  @ValidateNested({ each: true })
 | 
			
		||||
  users: GetUsersEntry[];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export class LogoutAllUser extends SuccessResponse {}
 | 
			
		||||
export class DeleteUser extends SuccessResponse {}
 | 
			
		||||
export class SetUserRole extends SuccessResponse {}
 | 
			
		||||
export class DisableTfa extends SuccessResponse {}
 | 
			
		||||
@@ -1,40 +0,0 @@
 | 
			
		||||
import { SuccessResponse } from "./base";
 | 
			
		||||
import { IsBase32, IsJWT, IsNotEmpty } from "class-validator";
 | 
			
		||||
import { ValidateConstructor } from "../utils";
 | 
			
		||||
 | 
			
		||||
@ValidateConstructor
 | 
			
		||||
export class LoginResponse extends SuccessResponse {
 | 
			
		||||
  constructor(jwt: string) {
 | 
			
		||||
    super();
 | 
			
		||||
    this.jwt = jwt;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @IsNotEmpty()
 | 
			
		||||
  @IsJWT()
 | 
			
		||||
  jwt: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ValidateConstructor
 | 
			
		||||
export class RequestTotpTfaResponse extends SuccessResponse {
 | 
			
		||||
  constructor(qrCode: string, secret: string) {
 | 
			
		||||
    super();
 | 
			
		||||
    this.qrCode = qrCode;
 | 
			
		||||
    this.secret = secret;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @IsNotEmpty()
 | 
			
		||||
  qrCode: string;
 | 
			
		||||
 | 
			
		||||
  @IsNotEmpty()
 | 
			
		||||
  @IsBase32()
 | 
			
		||||
  secret: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export class TfaRequiredResponse extends SuccessResponse {}
 | 
			
		||||
export class RemoveTfaResponse extends SuccessResponse {}
 | 
			
		||||
export class RequestEmailTfaResponse extends SuccessResponse {}
 | 
			
		||||
export class TfaCompletedResponse extends SuccessResponse {}
 | 
			
		||||
export class SignupResponse extends SuccessResponse {}
 | 
			
		||||
export class ChangePasswordResponse extends SuccessResponse {}
 | 
			
		||||
export class LogoutAllResponse extends SuccessResponse {}
 | 
			
		||||
export class RefreshResponse extends LoginResponse {}
 | 
			
		||||
@@ -1,25 +0,0 @@
 | 
			
		||||
import { IsNumber, Max, Min } from "class-validator";
 | 
			
		||||
 | 
			
		||||
export class BaseResponse {
 | 
			
		||||
  constructor(statusCode: number) {
 | 
			
		||||
    this.statusCode = statusCode;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @IsNumber()
 | 
			
		||||
  @Min(100)
 | 
			
		||||
  @Max(599)
 | 
			
		||||
  statusCode: number;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export class SuccessResponse extends BaseResponse {
 | 
			
		||||
  constructor() {
 | 
			
		||||
    super(200);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  declare statusCode: 200;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export class ErrorResponse extends BaseResponse {
 | 
			
		||||
  declare statusCode: 400 | 401 | 403;
 | 
			
		||||
  message?: string;
 | 
			
		||||
}
 | 
			
		||||
@@ -1,89 +0,0 @@
 | 
			
		||||
import { SuccessResponse } from "./base";
 | 
			
		||||
import {
 | 
			
		||||
  IsBoolean,
 | 
			
		||||
  IsInt,
 | 
			
		||||
  IsNotEmpty,
 | 
			
		||||
  IsOptional,
 | 
			
		||||
  IsString,
 | 
			
		||||
  Min,
 | 
			
		||||
} from "class-validator";
 | 
			
		||||
import { ValidateConstructor } from "../utils";
 | 
			
		||||
 | 
			
		||||
@ValidateConstructor
 | 
			
		||||
export class GetRootResponse extends SuccessResponse {
 | 
			
		||||
  constructor(rootId: number) {
 | 
			
		||||
    super();
 | 
			
		||||
    this.rootId = rootId;
 | 
			
		||||
  }
 | 
			
		||||
  @IsInt()
 | 
			
		||||
  @Min(1)
 | 
			
		||||
  rootId: number;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export class GetNodeResponse extends SuccessResponse {
 | 
			
		||||
  constructor(
 | 
			
		||||
    id: number,
 | 
			
		||||
    name: string,
 | 
			
		||||
    isFile: boolean,
 | 
			
		||||
    parent: number | null
 | 
			
		||||
  ) {
 | 
			
		||||
    super();
 | 
			
		||||
    this.id = id;
 | 
			
		||||
    this.name = name;
 | 
			
		||||
    this.isFile = isFile;
 | 
			
		||||
    this.parent = parent;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @IsInt()
 | 
			
		||||
  @Min(1)
 | 
			
		||||
  id: number;
 | 
			
		||||
 | 
			
		||||
  @IsString()
 | 
			
		||||
  name: string;
 | 
			
		||||
 | 
			
		||||
  @IsBoolean()
 | 
			
		||||
  isFile: boolean;
 | 
			
		||||
 | 
			
		||||
  @IsOptional()
 | 
			
		||||
  @IsInt()
 | 
			
		||||
  @Min(1)
 | 
			
		||||
  parent: number | null;
 | 
			
		||||
 | 
			
		||||
  @IsOptional()
 | 
			
		||||
  @IsInt({ each: true })
 | 
			
		||||
  @Min(1, { each: true })
 | 
			
		||||
  children?: number[];
 | 
			
		||||
 | 
			
		||||
  @IsOptional()
 | 
			
		||||
  @IsInt()
 | 
			
		||||
  @Min(0)
 | 
			
		||||
  size?: number;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ValidateConstructor
 | 
			
		||||
export class GetPathResponse extends SuccessResponse {
 | 
			
		||||
  constructor(path: string) {
 | 
			
		||||
    super();
 | 
			
		||||
    this.path = path;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @IsNotEmpty()
 | 
			
		||||
  @IsString()
 | 
			
		||||
  path: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ValidateConstructor
 | 
			
		||||
export class CreateFolderResponse extends SuccessResponse {
 | 
			
		||||
  constructor(id: number) {
 | 
			
		||||
    super();
 | 
			
		||||
    this.id = id;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @IsInt()
 | 
			
		||||
  @Min(1)
 | 
			
		||||
  id: number;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export class UploadFileResponse extends SuccessResponse {}
 | 
			
		||||
export class DeleteResponse extends SuccessResponse {}
 | 
			
		||||
export class CreateFileResponse extends CreateFolderResponse {}
 | 
			
		||||
@@ -1,5 +0,0 @@
 | 
			
		||||
export * from "./base";
 | 
			
		||||
export * as Auth from "./auth";
 | 
			
		||||
export * as FS from "./fs";
 | 
			
		||||
export * as User from "./user";
 | 
			
		||||
export * as Admin from "./admin";
 | 
			
		||||
@@ -1,27 +0,0 @@
 | 
			
		||||
import { SuccessResponse } from "./base";
 | 
			
		||||
import { ValidateConstructor } from "../utils";
 | 
			
		||||
import { IsBoolean, IsNotEmpty, IsString } from "class-validator";
 | 
			
		||||
 | 
			
		||||
@ValidateConstructor
 | 
			
		||||
export class UserInfoResponse extends SuccessResponse {
 | 
			
		||||
  constructor(name: string, gitlab: boolean, tfaEnabled: boolean) {
 | 
			
		||||
    super();
 | 
			
		||||
    this.name = name;
 | 
			
		||||
    this.gitlab = gitlab;
 | 
			
		||||
    this.tfaEnabled = tfaEnabled;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @IsNotEmpty()
 | 
			
		||||
  @IsString()
 | 
			
		||||
  name: string;
 | 
			
		||||
 | 
			
		||||
  @IsBoolean()
 | 
			
		||||
  gitlab: boolean;
 | 
			
		||||
 | 
			
		||||
  @IsBoolean()
 | 
			
		||||
  tfaEnabled: boolean;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export class DeleteUserResponse extends SuccessResponse {}
 | 
			
		||||
export class ChangePasswordResponse extends SuccessResponse {}
 | 
			
		||||
export class LogoutAllResponse extends SuccessResponse {}
 | 
			
		||||
@@ -1,43 +0,0 @@
 | 
			
		||||
import { validate, validateSync as _validateSync } from "class-validator";
 | 
			
		||||
 | 
			
		||||
export enum UserRole {
 | 
			
		||||
  ADMIN = 2,
 | 
			
		||||
  USER = 1,
 | 
			
		||||
  DISABLED = 0,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function validateSync<T extends object>(data: T): void {
 | 
			
		||||
  const errors = _validateSync(data);
 | 
			
		||||
  if (errors.length > 0) {
 | 
			
		||||
    console.error("Validation failed, errors: ", errors);
 | 
			
		||||
    throw new Error("Validation failed");
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export async function validateAsync<T extends object>(data: T): Promise<void> {
 | 
			
		||||
  const errors = await validate(data);
 | 
			
		||||
  if (errors.length > 0) {
 | 
			
		||||
    console.error("Validation failed, errors: ", errors);
 | 
			
		||||
    throw new Error("Validation failed");
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export async function validateAsyncInline<T extends object>(
 | 
			
		||||
  data: T
 | 
			
		||||
): Promise<T> {
 | 
			
		||||
  await validateAsync(data);
 | 
			
		||||
  return data;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function ValidateConstructor<
 | 
			
		||||
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
 | 
			
		||||
  T extends { new (...args: any[]): any }
 | 
			
		||||
>(constr: T) {
 | 
			
		||||
  return class extends constr {
 | 
			
		||||
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
 | 
			
		||||
    constructor(...args: any[]) {
 | 
			
		||||
      super(...args);
 | 
			
		||||
      validateSync(this);
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
@@ -1,8 +1,8 @@
 | 
			
		||||
import { createApp } from "vue";
 | 
			
		||||
import AppAsyncWrapper from "./AppAsyncWrapper.vue";
 | 
			
		||||
import router from "./router";
 | 
			
		||||
import { createApp } from 'vue';
 | 
			
		||||
import AppAsyncWrapper from './AppAsyncWrapper.vue';
 | 
			
		||||
import router from './router';
 | 
			
		||||
 | 
			
		||||
const app = createApp(AppAsyncWrapper);
 | 
			
		||||
app.use(router);
 | 
			
		||||
app.config.unwrapInjectedRef = true;
 | 
			
		||||
app.mount("#app");
 | 
			
		||||
app.mount('#app');
 | 
			
		||||
 
 | 
			
		||||
@@ -1,64 +1,58 @@
 | 
			
		||||
import type { RouteRecordRaw } from "vue-router";
 | 
			
		||||
import { createRouter, createWebHistory } from "vue-router";
 | 
			
		||||
import LoginView from "@/views/LoginView.vue";
 | 
			
		||||
import SignupView from "@/views/SignupView.vue";
 | 
			
		||||
import HomeView from "@/views/HomeView.vue";
 | 
			
		||||
import AboutView from "@/views/AboutView.vue";
 | 
			
		||||
import FSView from "@/views/FSView.vue";
 | 
			
		||||
import SetTokenView from "@/views/SetTokenView.vue";
 | 
			
		||||
import ProfileView from "@/views/ProfileView.vue";
 | 
			
		||||
import TFAView from "@/views/TFAView.vue";
 | 
			
		||||
import AdminView from "@/views/AdminView.vue";
 | 
			
		||||
import type { RouteRecordRaw } from 'vue-router';
 | 
			
		||||
import { createRouter, createWebHistory } from 'vue-router';
 | 
			
		||||
import LoginView from '@/views/LoginView.vue';
 | 
			
		||||
import SignupView from '@/views/SignupView.vue';
 | 
			
		||||
import HomeView from '@/views/HomeView.vue';
 | 
			
		||||
import FSView from '@/views/FSView.vue';
 | 
			
		||||
import SetTokenView from '@/views/SetTokenView.vue';
 | 
			
		||||
import ProfileView from '@/views/ProfileView.vue';
 | 
			
		||||
import TFAView from '@/views/TFAView.vue';
 | 
			
		||||
import AdminView from '@/views/AdminView.vue';
 | 
			
		||||
 | 
			
		||||
const routes: Array<RouteRecordRaw> = [
 | 
			
		||||
  {
 | 
			
		||||
    path: "/",
 | 
			
		||||
    name: "home",
 | 
			
		||||
    component: HomeView,
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    path: "/profile",
 | 
			
		||||
    name: "profile",
 | 
			
		||||
    component: ProfileView,
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    path: "/profile/2fa-enable",
 | 
			
		||||
    name: "2fa",
 | 
			
		||||
    component: TFAView,
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    path: "/admin",
 | 
			
		||||
    component: AdminView,
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    path: "/about",
 | 
			
		||||
    component: AboutView,
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    path: "/login",
 | 
			
		||||
    name: "login",
 | 
			
		||||
    component: LoginView,
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    path: "/signup",
 | 
			
		||||
    name: "signup",
 | 
			
		||||
    component: SignupView,
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    path: "/fs/:node_id",
 | 
			
		||||
    name: "fs",
 | 
			
		||||
    component: FSView,
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  {
 | 
			
		||||
    path: "/set_token",
 | 
			
		||||
    component: SetTokenView,
 | 
			
		||||
  },
 | 
			
		||||
	{
 | 
			
		||||
		path: '/',
 | 
			
		||||
		name: 'home',
 | 
			
		||||
		component: HomeView
 | 
			
		||||
	},
 | 
			
		||||
	{
 | 
			
		||||
		path: '/profile',
 | 
			
		||||
		name: 'profile',
 | 
			
		||||
		component: ProfileView
 | 
			
		||||
	},
 | 
			
		||||
	{
 | 
			
		||||
		path: '/profile/2fa-enable',
 | 
			
		||||
		name: '2fa',
 | 
			
		||||
		component: TFAView
 | 
			
		||||
	},
 | 
			
		||||
	{
 | 
			
		||||
		path: '/admin',
 | 
			
		||||
		component: AdminView
 | 
			
		||||
	},
 | 
			
		||||
	{
 | 
			
		||||
		path: '/login',
 | 
			
		||||
		name: 'login',
 | 
			
		||||
		component: LoginView
 | 
			
		||||
	},
 | 
			
		||||
	{
 | 
			
		||||
		path: '/signup',
 | 
			
		||||
		name: 'signup',
 | 
			
		||||
		component: SignupView
 | 
			
		||||
	},
 | 
			
		||||
	{
 | 
			
		||||
		path: '/fs/:node_id',
 | 
			
		||||
		name: 'fs',
 | 
			
		||||
		component: FSView
 | 
			
		||||
	},
 | 
			
		||||
	{
 | 
			
		||||
		path: '/set_token',
 | 
			
		||||
		component: SetTokenView
 | 
			
		||||
	}
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
const router = createRouter({
 | 
			
		||||
  history: createWebHistory(import.meta.env.BASE_URL),
 | 
			
		||||
  routes,
 | 
			
		||||
	history: createWebHistory(import.meta.env.BASE_URL),
 | 
			
		||||
	routes
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export default router;
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										19
									
								
								frontend/src/utils.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								frontend/src/utils.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,19 @@
 | 
			
		||||
import type { MessageApiInjection } from 'naive-ui/es/message/src/MessageProvider';
 | 
			
		||||
 | 
			
		||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
 | 
			
		||||
export function loadingMsgWrapper<T extends (...args: any[]) => Promise<any>>(
 | 
			
		||||
	msg: MessageApiInjection,
 | 
			
		||||
	func: T
 | 
			
		||||
): T {
 | 
			
		||||
	return <T>(async (...args: never[]) => {
 | 
			
		||||
		const loadMsg = msg.loading('Working', {
 | 
			
		||||
			duration: 0,
 | 
			
		||||
			closable: false
 | 
			
		||||
		});
 | 
			
		||||
		try {
 | 
			
		||||
			return await func(...args);
 | 
			
		||||
		} finally {
 | 
			
		||||
			loadMsg.destroy();
 | 
			
		||||
		}
 | 
			
		||||
	});
 | 
			
		||||
}
 | 
			
		||||
@@ -1,5 +0,0 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <div class="about">
 | 
			
		||||
    <h1>This is an about page</h1>
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
@@ -1,104 +1,152 @@
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
import type { TokenInjectType } from "@/api";
 | 
			
		||||
import { inject, onBeforeMount, ref } from "vue";
 | 
			
		||||
import { Responses, check_token, Admin, isErrorResponse } from "@/api";
 | 
			
		||||
import { onBeforeRouteUpdate } from "vue-router";
 | 
			
		||||
import router from "@/router";
 | 
			
		||||
<script setup lang="tsx">
 | 
			
		||||
import type { TokenInjectType, Responses } from '@/api';
 | 
			
		||||
import type { SelectOption, DataTableColumn } from 'naive-ui';
 | 
			
		||||
import { inject, onBeforeMount, ref } from 'vue';
 | 
			
		||||
import { check_token, Admin, isErrorResponse } from '@/api';
 | 
			
		||||
import { onBeforeRouteUpdate } from 'vue-router';
 | 
			
		||||
import router from '@/router';
 | 
			
		||||
import { loadingMsgWrapper } from '@/utils';
 | 
			
		||||
import {
 | 
			
		||||
	useMessage,
 | 
			
		||||
	NDataTable,
 | 
			
		||||
	NSelect,
 | 
			
		||||
	NButton,
 | 
			
		||||
	NButtonGroup
 | 
			
		||||
} from 'naive-ui';
 | 
			
		||||
 | 
			
		||||
const jwt = inject<TokenInjectType>("jwt") as TokenInjectType;
 | 
			
		||||
const message = useMessage();
 | 
			
		||||
 | 
			
		||||
const users = ref<Responses.Admin.GetUsersEntry[]>([]);
 | 
			
		||||
const jwt = inject<TokenInjectType>('jwt') as TokenInjectType;
 | 
			
		||||
 | 
			
		||||
const users = ref<Responses.GetUsersEntry[]>([]);
 | 
			
		||||
 | 
			
		||||
onBeforeRouteUpdate(async () => {
 | 
			
		||||
  await updatePanel();
 | 
			
		||||
	await updatePanel();
 | 
			
		||||
});
 | 
			
		||||
onBeforeMount(async () => {
 | 
			
		||||
  await updatePanel();
 | 
			
		||||
	await updatePanel();
 | 
			
		||||
});
 | 
			
		||||
async function updatePanel() {
 | 
			
		||||
  const token = await check_token(jwt);
 | 
			
		||||
  if (!token) return;
 | 
			
		||||
 | 
			
		||||
  const res = await Admin.get_users(token);
 | 
			
		||||
  if (isErrorResponse(res)) return router.replace({ path: "/" });
 | 
			
		||||
  users.value = res.users;
 | 
			
		||||
}
 | 
			
		||||
const updatePanel = loadingMsgWrapper(message, async () => {
 | 
			
		||||
	const token = await check_token(jwt);
 | 
			
		||||
	if (!token) return;
 | 
			
		||||
 | 
			
		||||
async function setRole(user: number, roleStr: string) {
 | 
			
		||||
  const token = await check_token(jwt);
 | 
			
		||||
  if (!token) return;
 | 
			
		||||
	const res = await Admin.get_users(token);
 | 
			
		||||
	if (isErrorResponse(res)) return router.replace({ path: '/' });
 | 
			
		||||
	users.value = res.users;
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
  const res = await Admin.set_role(user, parseInt(roleStr, 10), token);
 | 
			
		||||
  if (isErrorResponse(res)) console.error(res.message);
 | 
			
		||||
  await updatePanel();
 | 
			
		||||
}
 | 
			
		||||
const setRole = loadingMsgWrapper(
 | 
			
		||||
	message,
 | 
			
		||||
	async (user: number, role: number) => {
 | 
			
		||||
		const token = await check_token(jwt);
 | 
			
		||||
		if (!token) return;
 | 
			
		||||
 | 
			
		||||
async function disableTfa(user: number) {
 | 
			
		||||
  const token = await check_token(jwt);
 | 
			
		||||
  if (!token) return;
 | 
			
		||||
		const res = await Admin.set_role(user, role, token);
 | 
			
		||||
		if (isErrorResponse(res)) console.error(res.message);
 | 
			
		||||
		await updatePanel();
 | 
			
		||||
	}
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
  const res = await Admin.disable_tfa(user, token);
 | 
			
		||||
  if (isErrorResponse(res)) console.error(res.message);
 | 
			
		||||
  await updatePanel();
 | 
			
		||||
}
 | 
			
		||||
const action = (
 | 
			
		||||
	func: (
 | 
			
		||||
		user: number,
 | 
			
		||||
		token: string
 | 
			
		||||
	) => Promise<Responses.Success | Responses.Error>
 | 
			
		||||
) => {
 | 
			
		||||
	return loadingMsgWrapper(message, async (user: number) => {
 | 
			
		||||
		const token = await check_token(jwt);
 | 
			
		||||
		if (!token) return;
 | 
			
		||||
 | 
			
		||||
async function logoutUser(user: number) {
 | 
			
		||||
  const token = await check_token(jwt);
 | 
			
		||||
  if (!token) return;
 | 
			
		||||
		const res = await func(user, token);
 | 
			
		||||
		if (isErrorResponse(res)) console.error(res.message);
 | 
			
		||||
		await updatePanel();
 | 
			
		||||
	});
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
  const res = await Admin.logout(user, token);
 | 
			
		||||
  if (isErrorResponse(res)) console.error(res.message);
 | 
			
		||||
  await updatePanel();
 | 
			
		||||
}
 | 
			
		||||
const logoutUser = action(Admin.logout);
 | 
			
		||||
const disableTfa = action(Admin.disable_tfa);
 | 
			
		||||
const deleteUser = action(Admin.delete_user);
 | 
			
		||||
 | 
			
		||||
async function deleteUser(user: number) {
 | 
			
		||||
  const token = await check_token(jwt);
 | 
			
		||||
  if (!token) return;
 | 
			
		||||
const selectOptions: SelectOption[] = [
 | 
			
		||||
	{
 | 
			
		||||
		label: 'Disabled',
 | 
			
		||||
		value: 0
 | 
			
		||||
	},
 | 
			
		||||
	{
 | 
			
		||||
		label: 'User',
 | 
			
		||||
		value: 1
 | 
			
		||||
	},
 | 
			
		||||
	{
 | 
			
		||||
		label: 'Admin',
 | 
			
		||||
		value: 2
 | 
			
		||||
	}
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
  const res = await Admin.delete_user(user, token);
 | 
			
		||||
  if (isErrorResponse(res)) console.error(res.message);
 | 
			
		||||
  await updatePanel();
 | 
			
		||||
}
 | 
			
		||||
const columns: DataTableColumn<Responses.GetUsersEntry>[] = [
 | 
			
		||||
	{
 | 
			
		||||
		title: 'Name',
 | 
			
		||||
		key: 'name'
 | 
			
		||||
	},
 | 
			
		||||
	{
 | 
			
		||||
		title: 'Type',
 | 
			
		||||
		key: 'gitlab',
 | 
			
		||||
		render(user) {
 | 
			
		||||
			return user.gitlab ? 'Gitlab' : 'Password';
 | 
			
		||||
		}
 | 
			
		||||
	},
 | 
			
		||||
	{
 | 
			
		||||
		title: 'Role',
 | 
			
		||||
		key: 'role',
 | 
			
		||||
		minWidth: 120,
 | 
			
		||||
		render(user) {
 | 
			
		||||
			return (
 | 
			
		||||
				<NSelect
 | 
			
		||||
					value={user.role}
 | 
			
		||||
					options={selectOptions}
 | 
			
		||||
					onUpdateValue={(value: number) => setRole(user.id, value)}
 | 
			
		||||
				/>
 | 
			
		||||
			);
 | 
			
		||||
		}
 | 
			
		||||
	},
 | 
			
		||||
	{
 | 
			
		||||
		title: 'Tfa Status',
 | 
			
		||||
		key: 'tfaEnabled',
 | 
			
		||||
		render(user) {
 | 
			
		||||
			return user.gitlab ? '' : user.tfaEnabled ? 'Enabled' : 'Disabled';
 | 
			
		||||
		}
 | 
			
		||||
	},
 | 
			
		||||
	{
 | 
			
		||||
		title: 'Actions',
 | 
			
		||||
		key: 'actions',
 | 
			
		||||
		render(user) {
 | 
			
		||||
			return (
 | 
			
		||||
				<NButtonGroup>
 | 
			
		||||
					<NButton onClick={() => logoutUser(user.id)}>
 | 
			
		||||
						Logout all
 | 
			
		||||
					</NButton>
 | 
			
		||||
					{user.tfaEnabled ? (
 | 
			
		||||
						<NButton
 | 
			
		||||
							type="warning"
 | 
			
		||||
							onClick={() => disableTfa(user.id)}
 | 
			
		||||
						>
 | 
			
		||||
							Disable Tfa
 | 
			
		||||
						</NButton>
 | 
			
		||||
					) : (
 | 
			
		||||
						''
 | 
			
		||||
					)}
 | 
			
		||||
					<NButton onClick={() => deleteUser(user.id)} type="error">
 | 
			
		||||
						Delete
 | 
			
		||||
					</NButton>
 | 
			
		||||
				</NButtonGroup>
 | 
			
		||||
			);
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
];
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
  <table>
 | 
			
		||||
    <tr>
 | 
			
		||||
      <th>Name</th>
 | 
			
		||||
      <th>Type</th>
 | 
			
		||||
      <th>Role</th>
 | 
			
		||||
      <th>Tfa Status</th>
 | 
			
		||||
      <th>Actions</th>
 | 
			
		||||
    </tr>
 | 
			
		||||
    <tr v-for="user in users" :key="user.id">
 | 
			
		||||
      <td>{{ user.name }}</td>
 | 
			
		||||
      <td>{{ user.gitlab ? "Gitlab" : "Password" }}</td>
 | 
			
		||||
      <td>
 | 
			
		||||
        <select @change="setRole(user.id, ($event.target as HTMLSelectElement).value)">
 | 
			
		||||
          <option value="0" :selected="user.role === 0 ? true : undefined">
 | 
			
		||||
            Disabled
 | 
			
		||||
          </option>
 | 
			
		||||
          <option value="1" :selected="user.role === 1 ? true : undefined">
 | 
			
		||||
            User
 | 
			
		||||
          </option>
 | 
			
		||||
          <option value="2" :selected="user.role === 2 ? true : undefined">
 | 
			
		||||
            Admin
 | 
			
		||||
          </option>
 | 
			
		||||
        </select>
 | 
			
		||||
      </td>
 | 
			
		||||
      <td v-if="user.gitlab"></td>
 | 
			
		||||
      <td v-else>
 | 
			
		||||
        {{ user.tfaEnabled ? "Enabled" : "Disabled" }}
 | 
			
		||||
      </td>
 | 
			
		||||
      <td>
 | 
			
		||||
        <button v-if="user.tfaEnabled" @click="disableTfa(user.id)">
 | 
			
		||||
          Disable Tfa
 | 
			
		||||
        </button>
 | 
			
		||||
        <button @click="logoutUser(user.id)">Logout all</button>
 | 
			
		||||
        <button @click="deleteUser(user.id)">Delete</button>
 | 
			
		||||
      </td>
 | 
			
		||||
    </tr>
 | 
			
		||||
  </table>
 | 
			
		||||
	<n-data-table :columns="columns" :data="users" />
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<style scoped></style>
 | 
			
		||||
 
 | 
			
		||||
@@ -1,65 +1,92 @@
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
import type { TokenInjectType } from "@/api";
 | 
			
		||||
import { onBeforeRouteUpdate, useRoute, useRouter } from "vue-router";
 | 
			
		||||
import { inject, onBeforeMount, ref } from "vue";
 | 
			
		||||
import { check_token, FS, Responses, isErrorResponse } from "@/api";
 | 
			
		||||
import DirViewer from "@/components/FSView/DirViewer.vue";
 | 
			
		||||
import FileViewer from "@/components/FSView/FileViewer.vue";
 | 
			
		||||
import type { TokenInjectType, Responses } from '@/api';
 | 
			
		||||
import { onBeforeRouteUpdate, useRoute, useRouter } from 'vue-router';
 | 
			
		||||
import { inject, onBeforeMount, ref } from 'vue';
 | 
			
		||||
import { NCard } from 'naive-ui';
 | 
			
		||||
import { check_token, FS, isErrorResponse } from '@/api';
 | 
			
		||||
import UploadField from '@/components/UploadDialog/UploadField.vue';
 | 
			
		||||
import DirViewer from '@/components/DirViewer/DirViewer.vue';
 | 
			
		||||
import FileViewer from '@/components/FileViewer/FileViewer.vue';
 | 
			
		||||
import NLink from '@/components/NLink.vue';
 | 
			
		||||
 | 
			
		||||
const router = useRouter();
 | 
			
		||||
const route = useRoute();
 | 
			
		||||
const jwt = inject<TokenInjectType>("jwt") as TokenInjectType;
 | 
			
		||||
const jwt = inject<TokenInjectType>('jwt') as TokenInjectType;
 | 
			
		||||
 | 
			
		||||
const path = ref("");
 | 
			
		||||
const node = ref<Responses.FS.GetNodeResponse | null>(null);
 | 
			
		||||
const path = ref<Responses.GetPath | null>(null);
 | 
			
		||||
const node = ref<Responses.GetNode | null>(null);
 | 
			
		||||
 | 
			
		||||
function nameCompare(a: Responses.GetNodeEntry, b: Responses.GetNodeEntry) {
 | 
			
		||||
	const aStr = a.name.toLowerCase();
 | 
			
		||||
	const bStr = b.name.toLowerCase();
 | 
			
		||||
	return aStr.localeCompare(bStr);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function fetch_node(node_id: number) {
 | 
			
		||||
  const token = await check_token(jwt);
 | 
			
		||||
  if (!token) return;
 | 
			
		||||
  const [p, n] = [
 | 
			
		||||
    await FS.get_path(token, node_id),
 | 
			
		||||
    await FS.get_node(token, node_id),
 | 
			
		||||
  ];
 | 
			
		||||
  if (isErrorResponse(p)) return gotoRoot();
 | 
			
		||||
  if (isErrorResponse(n)) return gotoRoot();
 | 
			
		||||
  [path.value, node.value] = [p.path, n];
 | 
			
		||||
	const token = await check_token(jwt);
 | 
			
		||||
	if (!token) return;
 | 
			
		||||
	const [p, n] = [
 | 
			
		||||
		await FS.get_path(token, node_id),
 | 
			
		||||
		await FS.get_node(token, node_id)
 | 
			
		||||
	];
 | 
			
		||||
	if (isErrorResponse(p)) return gotoRoot();
 | 
			
		||||
	if (isErrorResponse(n)) return gotoRoot();
 | 
			
		||||
	if (n.children) {
 | 
			
		||||
		const folders = n.children
 | 
			
		||||
			.filter((node) => !node.isFile)
 | 
			
		||||
			.sort(nameCompare);
 | 
			
		||||
		const files = n.children
 | 
			
		||||
			.filter((node) => node.isFile)
 | 
			
		||||
			.sort(nameCompare);
 | 
			
		||||
		n.children = [...folders, ...files];
 | 
			
		||||
	}
 | 
			
		||||
	[path.value, node.value] = [p, n];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
onBeforeRouteUpdate(async (to) => {
 | 
			
		||||
  await fetch_node(Number(to.params.node_id));
 | 
			
		||||
	await fetch_node(Number(to.params.node_id));
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
async function reloadNode() {
 | 
			
		||||
  await fetch_node(Number(route.params.node_id));
 | 
			
		||||
	await fetch_node(Number(route.params.node_id));
 | 
			
		||||
}
 | 
			
		||||
onBeforeMount(async () => {
 | 
			
		||||
  await reloadNode();
 | 
			
		||||
	await reloadNode();
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
async function gotoRoot() {
 | 
			
		||||
  const token = await check_token(jwt);
 | 
			
		||||
  if (!token) return;
 | 
			
		||||
  const rootRes = await FS.get_root(token);
 | 
			
		||||
  if (isErrorResponse(rootRes)) return jwt.logout();
 | 
			
		||||
  const root = rootRes.rootId;
 | 
			
		||||
  await router.replace({
 | 
			
		||||
    name: "fs",
 | 
			
		||||
    params: { node_id: root },
 | 
			
		||||
  });
 | 
			
		||||
	const token = await check_token(jwt);
 | 
			
		||||
	if (!token) return;
 | 
			
		||||
	const rootRes = await FS.get_root(token);
 | 
			
		||||
	if (isErrorResponse(rootRes)) return jwt.logout();
 | 
			
		||||
	const root = rootRes.rootId;
 | 
			
		||||
	await router.replace({
 | 
			
		||||
		name: 'fs',
 | 
			
		||||
		params: { node_id: root }
 | 
			
		||||
	});
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
  <div v-if="node">
 | 
			
		||||
    <div>Path: {{ path }}</div>
 | 
			
		||||
    <DirViewer
 | 
			
		||||
      v-if="!node.isFile"
 | 
			
		||||
      :node="node"
 | 
			
		||||
      @reloadNode="reloadNode"
 | 
			
		||||
      @gotoRoot="gotoRoot"
 | 
			
		||||
    />
 | 
			
		||||
    <FileViewer v-else :node="node" />
 | 
			
		||||
  </div>
 | 
			
		||||
	<n-card v-if="node" header-style="font-size: 1.5em">
 | 
			
		||||
		<template #header>
 | 
			
		||||
			<span
 | 
			
		||||
				v-for="seg in path?.segments ?? []"
 | 
			
		||||
				:key="seg.path"
 | 
			
		||||
				style="margin-left: 0.25em"
 | 
			
		||||
			>
 | 
			
		||||
				<NLink v-if="seg.node" :to="`/fs/${seg.node}`">
 | 
			
		||||
					{{ seg.path }}
 | 
			
		||||
				</NLink>
 | 
			
		||||
				<template v-else>{{ seg.path }}</template>
 | 
			
		||||
			</span>
 | 
			
		||||
		</template>
 | 
			
		||||
		<template v-if="!node.isFile" #header-extra>
 | 
			
		||||
			<UploadField :node="node" @reloadNode="reloadNode" />
 | 
			
		||||
		</template>
 | 
			
		||||
		<DirViewer v-if="!node.isFile" :node="node" @reloadNode="reloadNode" />
 | 
			
		||||
		<FileViewer v-else :node="node" />
 | 
			
		||||
	</n-card>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<style scoped></style>
 | 
			
		||||
 
 | 
			
		||||
@@ -1,29 +1,29 @@
 | 
			
		||||
<template><p></p></template>
 | 
			
		||||
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
import type { TokenInjectType } from "@/api";
 | 
			
		||||
import { onBeforeRouteUpdate, useRouter } from "vue-router";
 | 
			
		||||
import { inject, onBeforeMount } from "vue";
 | 
			
		||||
import { FS, check_token, isErrorResponse } from "@/api";
 | 
			
		||||
import type { TokenInjectType } from '@/api';
 | 
			
		||||
import { onBeforeRouteUpdate, useRouter } from 'vue-router';
 | 
			
		||||
import { inject, onBeforeMount } from 'vue';
 | 
			
		||||
import { FS, check_token, isErrorResponse } from '@/api';
 | 
			
		||||
 | 
			
		||||
const router = useRouter();
 | 
			
		||||
const jwt = inject<TokenInjectType>("jwt") as TokenInjectType;
 | 
			
		||||
const jwt = inject<TokenInjectType>('jwt') as TokenInjectType;
 | 
			
		||||
 | 
			
		||||
async function start_redirect() {
 | 
			
		||||
  const token = await check_token(jwt);
 | 
			
		||||
  if (!token) return;
 | 
			
		||||
  const root = await FS.get_root(token);
 | 
			
		||||
  if (isErrorResponse(root)) return jwt.logout();
 | 
			
		||||
  await router.replace({
 | 
			
		||||
    name: "fs",
 | 
			
		||||
    params: { node_id: root.rootId },
 | 
			
		||||
  });
 | 
			
		||||
	const token = await check_token(jwt);
 | 
			
		||||
	if (!token) return;
 | 
			
		||||
	const root = await FS.get_root(token);
 | 
			
		||||
	if (isErrorResponse(root)) return jwt.logout();
 | 
			
		||||
	await router.replace({
 | 
			
		||||
		name: 'fs',
 | 
			
		||||
		params: { node_id: root.rootId }
 | 
			
		||||
	});
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
onBeforeRouteUpdate(async () => {
 | 
			
		||||
  await start_redirect();
 | 
			
		||||
	await start_redirect();
 | 
			
		||||
});
 | 
			
		||||
onBeforeMount(async () => {
 | 
			
		||||
  await start_redirect();
 | 
			
		||||
	await start_redirect();
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
 
 | 
			
		||||
@@ -1,62 +1,142 @@
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
import type { TokenInjectType } from "@/api";
 | 
			
		||||
import { ref, inject } from "vue";
 | 
			
		||||
import { Auth, FS, isErrorResponse } from "@/api";
 | 
			
		||||
import { useRouter } from "vue-router";
 | 
			
		||||
import type { TokenInjectType } from '@/api';
 | 
			
		||||
import { ref, inject } from 'vue';
 | 
			
		||||
import { Auth, FS, isErrorResponse } from '@/api';
 | 
			
		||||
import { useRouter } from 'vue-router';
 | 
			
		||||
import {
 | 
			
		||||
	useMessage,
 | 
			
		||||
	NInput,
 | 
			
		||||
	NGrid,
 | 
			
		||||
	NGi,
 | 
			
		||||
	NButton,
 | 
			
		||||
	NIcon,
 | 
			
		||||
	NH4,
 | 
			
		||||
	NCard
 | 
			
		||||
} from 'naive-ui';
 | 
			
		||||
import { LogoGitlab } from '@vicons/ionicons5';
 | 
			
		||||
import { loadingMsgWrapper } from '@/utils';
 | 
			
		||||
 | 
			
		||||
const router = useRouter();
 | 
			
		||||
const message = useMessage();
 | 
			
		||||
 | 
			
		||||
const username = ref("");
 | 
			
		||||
const password = ref("");
 | 
			
		||||
const otp = ref("");
 | 
			
		||||
 | 
			
		||||
const error = ref("");
 | 
			
		||||
const username = ref('');
 | 
			
		||||
const password = ref('');
 | 
			
		||||
const otp = ref('');
 | 
			
		||||
 | 
			
		||||
const requestOtp = ref(false);
 | 
			
		||||
 | 
			
		||||
const jwt = inject<TokenInjectType>("jwt") as TokenInjectType;
 | 
			
		||||
const jwt = inject<TokenInjectType>('jwt') as TokenInjectType;
 | 
			
		||||
 | 
			
		||||
async function login() {
 | 
			
		||||
  error.value = "";
 | 
			
		||||
  if (username.value === "" || password.value === "") {
 | 
			
		||||
    error.value = "Email and/or Password missing";
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  const res = await (requestOtp.value
 | 
			
		||||
    ? Auth.auth_login(username.value, password.value, otp.value)
 | 
			
		||||
    : Auth.auth_login(username.value, password.value));
 | 
			
		||||
  if (isErrorResponse(res)) error.value = "Login failed: " + res.message;
 | 
			
		||||
  else if ("jwt" in res) {
 | 
			
		||||
    const root = await FS.get_root(res.jwt);
 | 
			
		||||
    if (isErrorResponse(root)) {
 | 
			
		||||
      error.value = "Get root failed: " + root.message;
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    jwt.setToken(res.jwt);
 | 
			
		||||
    await router.push({
 | 
			
		||||
      name: "fs",
 | 
			
		||||
      params: { node_id: root.rootId },
 | 
			
		||||
    });
 | 
			
		||||
  } else {
 | 
			
		||||
    error.value = "";
 | 
			
		||||
    requestOtp.value = true;
 | 
			
		||||
  }
 | 
			
		||||
const login = loadingMsgWrapper(message, async () => {
 | 
			
		||||
	if (username.value === '' || password.value === '') {
 | 
			
		||||
		message.error('Email and/or Password missing', {
 | 
			
		||||
			closable: true,
 | 
			
		||||
			duration: 5000
 | 
			
		||||
		});
 | 
			
		||||
		return;
 | 
			
		||||
	}
 | 
			
		||||
	const res = await (requestOtp.value
 | 
			
		||||
		? Auth.auth_login(username.value, password.value, otp.value)
 | 
			
		||||
		: Auth.auth_login(username.value, password.value));
 | 
			
		||||
	if (isErrorResponse(res)) {
 | 
			
		||||
		message.error(`Login failed: ${res.message}`, {
 | 
			
		||||
			closable: true,
 | 
			
		||||
			duration: 5000
 | 
			
		||||
		});
 | 
			
		||||
	} else if ('jwt' in res) {
 | 
			
		||||
		const root = await FS.get_root(res.jwt);
 | 
			
		||||
		if (isErrorResponse(root)) {
 | 
			
		||||
			message.error(`Get root failed: ${root.message}`, {
 | 
			
		||||
				closable: true,
 | 
			
		||||
				duration: 5000
 | 
			
		||||
			});
 | 
			
		||||
			return;
 | 
			
		||||
		}
 | 
			
		||||
		jwt.setToken(res.jwt);
 | 
			
		||||
		await router.push({
 | 
			
		||||
			name: 'fs',
 | 
			
		||||
			params: { node_id: root.rootId }
 | 
			
		||||
		});
 | 
			
		||||
	} else requestOtp.value = true;
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
function loginGitlab() {
 | 
			
		||||
	window.location.pathname = '/api/auth/gitlab';
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function signup() {
 | 
			
		||||
	router.replace('signup');
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function onKey(event: KeyboardEvent) {
 | 
			
		||||
	if (event.key == 'Enter') login();
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
  <div v-if="error !== ''" v-text="error"></div>
 | 
			
		||||
  <template v-if="!requestOtp">
 | 
			
		||||
    <input type="email" placeholder="Email" v-model="username" />
 | 
			
		||||
    <input type="password" placeholder="Password" v-model="password" />
 | 
			
		||||
    <a href="/api/auth/gitlab">Login with gitlab</a>
 | 
			
		||||
    <router-link to="signup">Signup instead?</router-link>
 | 
			
		||||
  </template>
 | 
			
		||||
  <template v-else>
 | 
			
		||||
    <div>Please input your 2 factor authentication code</div>
 | 
			
		||||
    <input type="text" placeholder="Code" v-model="otp" />
 | 
			
		||||
  </template>
 | 
			
		||||
  <button @click="login()">Login</button>
 | 
			
		||||
	<n-card>
 | 
			
		||||
		<template v-if="!requestOtp">
 | 
			
		||||
			<n-grid cols="2" x-gap="16" y-gap="16">
 | 
			
		||||
				<n-gi span="2">
 | 
			
		||||
					<n-input
 | 
			
		||||
						type="text"
 | 
			
		||||
						placeholder="Email"
 | 
			
		||||
						v-model:value="username"
 | 
			
		||||
						autofocus
 | 
			
		||||
						:input-props="{ type: 'email' }"
 | 
			
		||||
						@keyup="onKey"
 | 
			
		||||
					/>
 | 
			
		||||
				</n-gi>
 | 
			
		||||
				<n-gi span="2">
 | 
			
		||||
					<n-input
 | 
			
		||||
						type="password"
 | 
			
		||||
						placeholder="Password"
 | 
			
		||||
						v-model:value="password"
 | 
			
		||||
						@keyup="onKey"
 | 
			
		||||
					/>
 | 
			
		||||
				</n-gi>
 | 
			
		||||
				<n-gi span="2" style="text-align: center">
 | 
			
		||||
					<n-button type="info" @click="login">Login</n-button>
 | 
			
		||||
				</n-gi>
 | 
			
		||||
				<n-gi>
 | 
			
		||||
					<n-button
 | 
			
		||||
						ghost
 | 
			
		||||
						color="#fc6d27"
 | 
			
		||||
						text-color="#000"
 | 
			
		||||
						@click="loginGitlab"
 | 
			
		||||
					>
 | 
			
		||||
						<template #icon>
 | 
			
		||||
							<n-icon color="#fc6d27"><LogoGitlab /></n-icon>
 | 
			
		||||
						</template>
 | 
			
		||||
						Login with gitlab
 | 
			
		||||
					</n-button>
 | 
			
		||||
				</n-gi>
 | 
			
		||||
				<n-gi style="text-align: right">
 | 
			
		||||
					<n-button ghost @click="signup">Signup</n-button>
 | 
			
		||||
				</n-gi>
 | 
			
		||||
			</n-grid>
 | 
			
		||||
		</template>
 | 
			
		||||
		<template v-else>
 | 
			
		||||
			<n-grid cols="2" x-gap="16" y-gap="16">
 | 
			
		||||
				<n-gi span="2" style="text-align: center">
 | 
			
		||||
					<n-h4>Please input your 2 factor authentication code</n-h4>
 | 
			
		||||
				</n-gi>
 | 
			
		||||
				<n-gi span="1">
 | 
			
		||||
					<n-input
 | 
			
		||||
						type="text"
 | 
			
		||||
						placeholder="Code"
 | 
			
		||||
						maxlength="6"
 | 
			
		||||
						v-model:value="otp"
 | 
			
		||||
						autofocus
 | 
			
		||||
						@keyup="onKey"
 | 
			
		||||
					/>
 | 
			
		||||
				</n-gi>
 | 
			
		||||
				<n-gi span="1" style="text-align: right">
 | 
			
		||||
					<n-button type="info" @click="login">Login</n-button>
 | 
			
		||||
				</n-gi>
 | 
			
		||||
			</n-grid>
 | 
			
		||||
		</template>
 | 
			
		||||
	</n-card>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<style scoped></style>
 | 
			
		||||
 
 | 
			
		||||
@@ -1,106 +1,108 @@
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
import type { TokenInjectType } from "@/api";
 | 
			
		||||
import { ref, inject, onBeforeMount } from "vue";
 | 
			
		||||
import { Auth, User, check_token, isErrorResponse, Responses } from "@/api";
 | 
			
		||||
import { onBeforeRouteUpdate } from "vue-router";
 | 
			
		||||
import type { TokenInjectType, Responses } from '@/api';
 | 
			
		||||
import { ref, inject, onBeforeMount } from 'vue';
 | 
			
		||||
import { Auth, User, check_token, isErrorResponse } from '@/api';
 | 
			
		||||
import { onBeforeRouteUpdate, useRouter } from 'vue-router';
 | 
			
		||||
import { NSpin, NGrid, NGi, NButton, NCard, useMessage } from 'naive-ui';
 | 
			
		||||
import UserChangePw from '@/components/UserChangePw.vue';
 | 
			
		||||
import { loadingMsgWrapper } from '@/utils';
 | 
			
		||||
 | 
			
		||||
const error = ref("");
 | 
			
		||||
const oldPw = ref("");
 | 
			
		||||
const newPw = ref("");
 | 
			
		||||
const newPw2 = ref("");
 | 
			
		||||
const user = ref<Responses.User.UserInfoResponse | null>(null);
 | 
			
		||||
const router = useRouter();
 | 
			
		||||
const message = useMessage();
 | 
			
		||||
 | 
			
		||||
const jwt = inject<TokenInjectType>("jwt") as TokenInjectType;
 | 
			
		||||
const user = ref<Responses.UserInfo | null>(null);
 | 
			
		||||
 | 
			
		||||
const jwt = inject<TokenInjectType>('jwt') as TokenInjectType;
 | 
			
		||||
 | 
			
		||||
onBeforeRouteUpdate(async () => {
 | 
			
		||||
  await updateProfile();
 | 
			
		||||
	await updateProfile();
 | 
			
		||||
});
 | 
			
		||||
onBeforeMount(async () => {
 | 
			
		||||
  await updateProfile();
 | 
			
		||||
	await updateProfile();
 | 
			
		||||
});
 | 
			
		||||
async function updateProfile() {
 | 
			
		||||
  const token = await check_token(jwt);
 | 
			
		||||
  if (!token) return;
 | 
			
		||||
const updateProfile = loadingMsgWrapper(message, async () => {
 | 
			
		||||
	const token = await check_token(jwt);
 | 
			
		||||
	if (!token) return;
 | 
			
		||||
 | 
			
		||||
  const res = await User.get_user_info(token);
 | 
			
		||||
  if (isErrorResponse(res)) return jwt.logout();
 | 
			
		||||
  user.value = res;
 | 
			
		||||
}
 | 
			
		||||
	const res = await User.get_user_info(token);
 | 
			
		||||
	if (isErrorResponse(res)) return jwt.logout();
 | 
			
		||||
	user.value = res;
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
async function deleteUser() {
 | 
			
		||||
  const token = await check_token(jwt);
 | 
			
		||||
  if (!token) return;
 | 
			
		||||
  await User.delete_user(token);
 | 
			
		||||
  jwt.logout();
 | 
			
		||||
}
 | 
			
		||||
const deleteUser = loadingMsgWrapper(message, async () => {
 | 
			
		||||
	const token = await check_token(jwt);
 | 
			
		||||
	if (!token) return;
 | 
			
		||||
	await User.delete_user(token);
 | 
			
		||||
	jwt.logout();
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
async function logoutAll() {
 | 
			
		||||
  const token = await check_token(jwt);
 | 
			
		||||
  if (!token) return;
 | 
			
		||||
  await Auth.logout_all(token);
 | 
			
		||||
  jwt.logout();
 | 
			
		||||
}
 | 
			
		||||
const logoutAll = loadingMsgWrapper(message, async () => {
 | 
			
		||||
	const token = await check_token(jwt);
 | 
			
		||||
	if (!token) return;
 | 
			
		||||
	await Auth.logout_all(token);
 | 
			
		||||
	jwt.logout();
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
async function changePw() {
 | 
			
		||||
  if (oldPw.value === "" || newPw.value === "" || newPw2.value === "") {
 | 
			
		||||
    error.value = "Password missing";
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  if (newPw.value !== newPw2.value) {
 | 
			
		||||
    error.value = "Passwords don't match";
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  const token = await check_token(jwt);
 | 
			
		||||
  if (!token) return;
 | 
			
		||||
  const res = await Auth.change_password(oldPw.value, newPw.value, token);
 | 
			
		||||
  if (isErrorResponse(res))
 | 
			
		||||
    error.value = "Password change failed: " + res.message;
 | 
			
		||||
  else jwt.logout();
 | 
			
		||||
}
 | 
			
		||||
const tfaDisable = loadingMsgWrapper(message, async () => {
 | 
			
		||||
	const token = await check_token(jwt);
 | 
			
		||||
	if (!token) return;
 | 
			
		||||
	await Auth.tfa_disable(token);
 | 
			
		||||
	jwt.logout();
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
async function tfaDisable() {
 | 
			
		||||
  const token = await check_token(jwt);
 | 
			
		||||
  if (!token) return;
 | 
			
		||||
  await Auth.tfa_disable(token);
 | 
			
		||||
  jwt.logout();
 | 
			
		||||
async function tfaEnable() {
 | 
			
		||||
	await router.push('/profile/2fa-enable');
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
  <template v-if="user">
 | 
			
		||||
    <div v-if="error !== ''" v-text="error"></div>
 | 
			
		||||
    <div>User: {{ user.name }}</div>
 | 
			
		||||
    <div>Signed in with {{ user.gitlab ? "gitlab" : "password" }}</div>
 | 
			
		||||
    <template v-if="!user.gitlab">
 | 
			
		||||
      <div>
 | 
			
		||||
        <input type="password" placeholder="Old password" v-model="oldPw" />
 | 
			
		||||
        <input type="password" placeholder="New password" v-model="newPw" />
 | 
			
		||||
        <input
 | 
			
		||||
          type="password"
 | 
			
		||||
          placeholder="Repeat new password"
 | 
			
		||||
          v-model="newPw2"
 | 
			
		||||
        />
 | 
			
		||||
        <button @click="changePw">Change</button>
 | 
			
		||||
      </div>
 | 
			
		||||
      <div>
 | 
			
		||||
        <div>
 | 
			
		||||
          2 Factor authentication:
 | 
			
		||||
          {{ user.tfaEnabled ? "Enabled" : "Disabled" }}
 | 
			
		||||
        </div>
 | 
			
		||||
        <div>
 | 
			
		||||
          <a href="#" v-if="user.tfaEnabled" @click="tfaDisable"> Disable </a>
 | 
			
		||||
          <router-link to="/profile/2fa-enable" v-else> Enable </router-link>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
    </template>
 | 
			
		||||
    <div>
 | 
			
		||||
      <a href="#" @click="logoutAll">Logout everywhere</a>
 | 
			
		||||
      <a href="#" @click="deleteUser">Delete Account</a>
 | 
			
		||||
    </div>
 | 
			
		||||
  </template>
 | 
			
		||||
  <template v-else>
 | 
			
		||||
    <div>Loading...</div>
 | 
			
		||||
  </template>
 | 
			
		||||
	<template v-if="user">
 | 
			
		||||
		<n-card :title="user.name">
 | 
			
		||||
			<n-grid cols="2" x-gap="16" y-gap="16">
 | 
			
		||||
				<template v-if="!user.gitlab">
 | 
			
		||||
					<n-gi span="2">
 | 
			
		||||
						<n-grid cols="2" x-gap="16">
 | 
			
		||||
							<n-gi><UserChangePw /></n-gi>
 | 
			
		||||
							<n-gi>
 | 
			
		||||
								<n-card
 | 
			
		||||
									title="2 Factor authentication"
 | 
			
		||||
									embedded
 | 
			
		||||
								>
 | 
			
		||||
									<n-button
 | 
			
		||||
										v-if="user.tfaEnabled"
 | 
			
		||||
										type="error"
 | 
			
		||||
										@click="tfaDisable"
 | 
			
		||||
									>
 | 
			
		||||
										Disable
 | 
			
		||||
									</n-button>
 | 
			
		||||
									<n-button
 | 
			
		||||
										v-else
 | 
			
		||||
										type="success"
 | 
			
		||||
										@click="tfaEnable"
 | 
			
		||||
									>
 | 
			
		||||
										Enable
 | 
			
		||||
									</n-button>
 | 
			
		||||
								</n-card>
 | 
			
		||||
							</n-gi>
 | 
			
		||||
						</n-grid>
 | 
			
		||||
					</n-gi>
 | 
			
		||||
				</template>
 | 
			
		||||
				<n-gi>
 | 
			
		||||
					<n-button type="error" @click="logoutAll">
 | 
			
		||||
						Logout everywhere
 | 
			
		||||
					</n-button>
 | 
			
		||||
				</n-gi>
 | 
			
		||||
				<n-gi>
 | 
			
		||||
					<n-button type="error" @click="deleteUser">
 | 
			
		||||
						Delete Account
 | 
			
		||||
					</n-button>
 | 
			
		||||
				</n-gi>
 | 
			
		||||
			</n-grid>
 | 
			
		||||
		</n-card>
 | 
			
		||||
	</template>
 | 
			
		||||
	<template v-else>
 | 
			
		||||
		<div><n-spin size="small" />Loading...</div>
 | 
			
		||||
	</template>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<style scoped></style>
 | 
			
		||||
 
 | 
			
		||||
@@ -1,19 +1,19 @@
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
import type { TokenInjectType } from "@/api";
 | 
			
		||||
import { inject } from "vue";
 | 
			
		||||
import { useRoute, useRouter } from "vue-router";
 | 
			
		||||
import type { TokenInjectType } from '@/api';
 | 
			
		||||
import { inject } from 'vue';
 | 
			
		||||
import { useRoute, useRouter } from 'vue-router';
 | 
			
		||||
 | 
			
		||||
const router = useRouter();
 | 
			
		||||
const route = useRoute();
 | 
			
		||||
 | 
			
		||||
const jwt = inject<TokenInjectType>("jwt") as TokenInjectType;
 | 
			
		||||
const jwt = inject<TokenInjectType>('jwt') as TokenInjectType;
 | 
			
		||||
 | 
			
		||||
if ("token" in route.query) jwt.setToken(route.query["token"] as string);
 | 
			
		||||
router.replace({ path: "/" });
 | 
			
		||||
if ('token' in route.query) jwt.setToken(route.query['token'] as string);
 | 
			
		||||
router.replace({ path: '/' });
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
  <router-link to="/">Click here to go home</router-link>
 | 
			
		||||
	<router-link to="/" replace>Click here to go home</router-link>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<style scoped></style>
 | 
			
		||||
 
 | 
			
		||||
@@ -1,35 +1,85 @@
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
import { ref } from "vue";
 | 
			
		||||
import { Auth, isErrorResponse } from "@/api";
 | 
			
		||||
import { ref } from 'vue';
 | 
			
		||||
import { Auth, isErrorResponse } from '@/api';
 | 
			
		||||
import { useMessage, NInput, NGrid, NGi, NButton, NCard } from 'naive-ui';
 | 
			
		||||
import { useRouter } from 'vue-router';
 | 
			
		||||
import { loadingMsgWrapper } from '@/utils';
 | 
			
		||||
 | 
			
		||||
const username = ref("");
 | 
			
		||||
const password = ref("");
 | 
			
		||||
const password2 = ref("");
 | 
			
		||||
const error = ref("");
 | 
			
		||||
const router = useRouter();
 | 
			
		||||
const message = useMessage();
 | 
			
		||||
 | 
			
		||||
async function signup() {
 | 
			
		||||
  if (username.value === "" || password.value === "") {
 | 
			
		||||
    error.value = "Email and/or Password missing";
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  if (password.value !== password2.value) {
 | 
			
		||||
    error.value = "Passwords don't match";
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  const res = await Auth.auth_signup(username.value, password.value);
 | 
			
		||||
  error.value = isErrorResponse(res)
 | 
			
		||||
    ? "Signup failed: " + res.message
 | 
			
		||||
    : "Signup successful, please wait till an admin unlocks your account.";
 | 
			
		||||
const username = ref('');
 | 
			
		||||
const password = ref('');
 | 
			
		||||
const password2 = ref('');
 | 
			
		||||
 | 
			
		||||
const signup = loadingMsgWrapper(message, async () => {
 | 
			
		||||
	if (username.value === '' || password.value === '') {
 | 
			
		||||
		message.error('Email and/or Password missing');
 | 
			
		||||
		return;
 | 
			
		||||
	}
 | 
			
		||||
	if (password.value !== password2.value) {
 | 
			
		||||
		message.error("Passwords don't match");
 | 
			
		||||
		return;
 | 
			
		||||
	}
 | 
			
		||||
	const res = await Auth.auth_signup(username.value, password.value);
 | 
			
		||||
	if (isErrorResponse(res)) {
 | 
			
		||||
		message.error(`Signup failed: ${res.message}`);
 | 
			
		||||
	} else {
 | 
			
		||||
		message.success(
 | 
			
		||||
			'Signup successful, please wait till an admin unlocks your account.',
 | 
			
		||||
			{
 | 
			
		||||
				duration: 10000
 | 
			
		||||
			}
 | 
			
		||||
		);
 | 
			
		||||
	}
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
function login() {
 | 
			
		||||
	router.replace('login');
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function onKey(event: KeyboardEvent) {
 | 
			
		||||
	if (event.key == 'Enter') signup();
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
  <div v-if="error !== ''" v-text="error"></div>
 | 
			
		||||
  <input type="email" placeholder="Email" v-model="username" />
 | 
			
		||||
  <input type="password" placeholder="Password" v-model="password" />
 | 
			
		||||
  <input type="password" placeholder="Repeat password" v-model="password2" />
 | 
			
		||||
  <button @click="signup()">Signup</button>
 | 
			
		||||
  <router-link to="login">Login instead?</router-link>
 | 
			
		||||
	<n-card>
 | 
			
		||||
		<n-grid cols="2" x-gap="16" y-gap="16">
 | 
			
		||||
			<n-gi span="2">
 | 
			
		||||
				<n-input
 | 
			
		||||
					type="text"
 | 
			
		||||
					placeholder="Email"
 | 
			
		||||
					v-model:value="username"
 | 
			
		||||
					autofocus
 | 
			
		||||
					:input-props="{ type: 'email' }"
 | 
			
		||||
					@keyup="onKey"
 | 
			
		||||
				/>
 | 
			
		||||
			</n-gi>
 | 
			
		||||
			<n-gi span="2">
 | 
			
		||||
				<n-input
 | 
			
		||||
					type="password"
 | 
			
		||||
					placeholder="Password"
 | 
			
		||||
					v-model:value="password"
 | 
			
		||||
					@keyup="onKey"
 | 
			
		||||
				/>
 | 
			
		||||
			</n-gi>
 | 
			
		||||
			<n-gi span="2">
 | 
			
		||||
				<n-input
 | 
			
		||||
					type="password"
 | 
			
		||||
					placeholder="Repeat password"
 | 
			
		||||
					v-model:value="password2"
 | 
			
		||||
					@keyup="onKey"
 | 
			
		||||
				/>
 | 
			
		||||
			</n-gi>
 | 
			
		||||
			<n-gi>
 | 
			
		||||
				<n-button type="info" @click="signup">Signup</n-button>
 | 
			
		||||
			</n-gi>
 | 
			
		||||
			<n-gi>
 | 
			
		||||
				<n-button ghost @click="login">Login instead?</n-button>
 | 
			
		||||
			</n-gi>
 | 
			
		||||
		</n-grid>
 | 
			
		||||
	</n-card>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<style scoped></style>
 | 
			
		||||
 
 | 
			
		||||
@@ -1,90 +1,138 @@
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
import type { TokenInjectType } from "@/api";
 | 
			
		||||
import { ref, inject } from "vue";
 | 
			
		||||
import { Auth, check_token, isErrorResponse } from "@/api";
 | 
			
		||||
import type { TokenInjectType } from '@/api';
 | 
			
		||||
import { ref, inject } from 'vue';
 | 
			
		||||
import { Auth, check_token, isErrorResponse } from '@/api';
 | 
			
		||||
import {
 | 
			
		||||
	useMessage,
 | 
			
		||||
	NInput,
 | 
			
		||||
	NGrid,
 | 
			
		||||
	NGi,
 | 
			
		||||
	NButton,
 | 
			
		||||
	NImage,
 | 
			
		||||
	NPopover,
 | 
			
		||||
	NCard
 | 
			
		||||
} from 'naive-ui';
 | 
			
		||||
import { loadingMsgWrapper } from '@/utils';
 | 
			
		||||
 | 
			
		||||
const message = useMessage();
 | 
			
		||||
 | 
			
		||||
enum state {
 | 
			
		||||
  SELECT,
 | 
			
		||||
  MAIL,
 | 
			
		||||
  TOTP,
 | 
			
		||||
	SELECT,
 | 
			
		||||
	MAIL,
 | 
			
		||||
	TOTP
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const currentState = ref<state>(state.SELECT);
 | 
			
		||||
 | 
			
		||||
const error = ref("");
 | 
			
		||||
const qrImage = ref("");
 | 
			
		||||
const secret = ref("");
 | 
			
		||||
const code = ref("");
 | 
			
		||||
const qrImage = ref('');
 | 
			
		||||
const secret = ref('');
 | 
			
		||||
const code = ref('');
 | 
			
		||||
 | 
			
		||||
const jwt = inject<TokenInjectType>("jwt") as TokenInjectType;
 | 
			
		||||
const jwt = inject<TokenInjectType>('jwt') as TokenInjectType;
 | 
			
		||||
 | 
			
		||||
async function selectMail() {
 | 
			
		||||
  const token = await check_token(jwt);
 | 
			
		||||
  if (!token) return;
 | 
			
		||||
  error.value = "Working...";
 | 
			
		||||
  const res = await Auth.tfa_setup(true, token);
 | 
			
		||||
  if (isErrorResponse(res))
 | 
			
		||||
    error.value = "Failed to select 2fa type: " + res.message;
 | 
			
		||||
  else {
 | 
			
		||||
    error.value = "";
 | 
			
		||||
    currentState.value = state.MAIL;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
const selectMail = loadingMsgWrapper(message, async () => {
 | 
			
		||||
	const token = await check_token(jwt);
 | 
			
		||||
	if (!token) return;
 | 
			
		||||
	const res = await Auth.tfa_setup(true, token);
 | 
			
		||||
	if (isErrorResponse(res))
 | 
			
		||||
		message.error(`Failed to select 2fa type: ${res.message}`);
 | 
			
		||||
	else currentState.value = state.MAIL;
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
async function selectTotp() {
 | 
			
		||||
  const token = await check_token(jwt);
 | 
			
		||||
  if (!token) return;
 | 
			
		||||
  error.value = "Working...";
 | 
			
		||||
  const res = await Auth.tfa_setup(false, token);
 | 
			
		||||
  if (isErrorResponse(res))
 | 
			
		||||
    error.value = "Failed to select 2fa type: " + res.message;
 | 
			
		||||
  else {
 | 
			
		||||
    qrImage.value = res.qrCode;
 | 
			
		||||
    secret.value = res.secret;
 | 
			
		||||
    error.value = "";
 | 
			
		||||
    currentState.value = state.TOTP;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
const selectTotp = loadingMsgWrapper(message, async () => {
 | 
			
		||||
	const token = await check_token(jwt);
 | 
			
		||||
	if (!token) return;
 | 
			
		||||
	const res = await Auth.tfa_setup(false, token);
 | 
			
		||||
	if (isErrorResponse(res))
 | 
			
		||||
		message.error(`Failed to select 2fa type: ${res.message}`);
 | 
			
		||||
	else {
 | 
			
		||||
		qrImage.value = res.qrCode;
 | 
			
		||||
		secret.value = res.secret;
 | 
			
		||||
		currentState.value = state.TOTP;
 | 
			
		||||
	}
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
async function submit() {
 | 
			
		||||
  const token = await check_token(jwt);
 | 
			
		||||
  if (!token) return;
 | 
			
		||||
  error.value = "Working...";
 | 
			
		||||
  const res = await Auth.tfa_complete(
 | 
			
		||||
    currentState.value === state.MAIL,
 | 
			
		||||
    code.value,
 | 
			
		||||
    token
 | 
			
		||||
  );
 | 
			
		||||
  if (isErrorResponse(res))
 | 
			
		||||
    error.value = "Failed to submit code: " + res.message;
 | 
			
		||||
  else jwt.logout();
 | 
			
		||||
const submit = loadingMsgWrapper(message, async () => {
 | 
			
		||||
	const token = await check_token(jwt);
 | 
			
		||||
	if (!token) return;
 | 
			
		||||
	const res = await Auth.tfa_complete(
 | 
			
		||||
		currentState.value === state.MAIL,
 | 
			
		||||
		code.value,
 | 
			
		||||
		token
 | 
			
		||||
	);
 | 
			
		||||
	if (isErrorResponse(res))
 | 
			
		||||
		message.error(`Failed to submit code: ${res.message}`);
 | 
			
		||||
	else jwt.logout();
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
function onKey(event: KeyboardEvent) {
 | 
			
		||||
	if (event.key == 'Enter') submit();
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
  <div v-if="error !== ''" v-text="error"></div>
 | 
			
		||||
  <template v-if="currentState === state.SELECT">
 | 
			
		||||
    <div>Select 2 Factor authentication type:</div>
 | 
			
		||||
    <div>
 | 
			
		||||
      <button @click="selectMail">Mail</button>
 | 
			
		||||
      <button @click="selectTotp">Google Authenticator</button>
 | 
			
		||||
    </div>
 | 
			
		||||
  </template>
 | 
			
		||||
  <template v-else-if="currentState === state.MAIL">
 | 
			
		||||
    <div>Please enter the code you got by mail</div>
 | 
			
		||||
    <input type="text" placeholder="Code" v-model="code" />
 | 
			
		||||
    <button @click="submit()">Submit</button>
 | 
			
		||||
  </template>
 | 
			
		||||
  <template v-else>
 | 
			
		||||
    <img :src="qrImage" alt="QrCode" />
 | 
			
		||||
    <details>
 | 
			
		||||
      <summary>Show manual input code</summary>
 | 
			
		||||
      {{ secret }}
 | 
			
		||||
    </details>
 | 
			
		||||
    <div>Please enter the current code</div>
 | 
			
		||||
    <input type="text" placeholder="Code" v-model="code" />
 | 
			
		||||
    <button @click="submit()">Submit</button>
 | 
			
		||||
  </template>
 | 
			
		||||
	<n-card>
 | 
			
		||||
		<n-grid cols="2" x-gap="16" y-gap="16">
 | 
			
		||||
			<template v-if="currentState === state.SELECT">
 | 
			
		||||
				<n-gi span="2" style="text-align: center">
 | 
			
		||||
					Select 2 Factor authentication type
 | 
			
		||||
				</n-gi>
 | 
			
		||||
				<n-gi>
 | 
			
		||||
					<n-button @click="selectMail">Mail</n-button>
 | 
			
		||||
				</n-gi>
 | 
			
		||||
				<n-gi style="text-align: right">
 | 
			
		||||
					<n-button @click="selectTotp"
 | 
			
		||||
						>Google Authenticator</n-button
 | 
			
		||||
					>
 | 
			
		||||
				</n-gi>
 | 
			
		||||
			</template>
 | 
			
		||||
			<template v-else-if="currentState === state.MAIL">
 | 
			
		||||
				<n-gi span="2" style="text-align: center">
 | 
			
		||||
					Please enter the code you got by mail
 | 
			
		||||
				</n-gi>
 | 
			
		||||
				<n-gi>
 | 
			
		||||
					<n-input
 | 
			
		||||
						type="text"
 | 
			
		||||
						placeholder="Code"
 | 
			
		||||
						maxlength="6"
 | 
			
		||||
						v-model:value="code"
 | 
			
		||||
						@keyup="onKey"
 | 
			
		||||
					/>
 | 
			
		||||
				</n-gi>
 | 
			
		||||
				<n-gi style="text-align: right">
 | 
			
		||||
					<n-button @click="submit">Submit</n-button>
 | 
			
		||||
				</n-gi>
 | 
			
		||||
			</template>
 | 
			
		||||
			<template v-else>
 | 
			
		||||
				<n-gi span="2" style="text-align: center">
 | 
			
		||||
					<n-image :src="qrImage" alt="QrCode" />
 | 
			
		||||
				</n-gi>
 | 
			
		||||
				<n-gi span="2" style="text-align: center">
 | 
			
		||||
					<n-popover placement="bottom" trigger="click">
 | 
			
		||||
						<template #trigger>
 | 
			
		||||
							<n-button>Show manual input code</n-button>
 | 
			
		||||
						</template>
 | 
			
		||||
						{{ secret }}
 | 
			
		||||
					</n-popover>
 | 
			
		||||
				</n-gi>
 | 
			
		||||
				<n-gi span="2" style="text-align: center">
 | 
			
		||||
					Please enter the current code
 | 
			
		||||
				</n-gi>
 | 
			
		||||
				<n-gi>
 | 
			
		||||
					<n-input
 | 
			
		||||
						type="text"
 | 
			
		||||
						placeholder="Code"
 | 
			
		||||
						maxlength="6"
 | 
			
		||||
						v-model:value="code"
 | 
			
		||||
						@keyup="onKey"
 | 
			
		||||
					/>
 | 
			
		||||
				</n-gi>
 | 
			
		||||
				<n-gi style="text-align: right">
 | 
			
		||||
					<n-button @click="submit">Submit</n-button>
 | 
			
		||||
				</n-gi>
 | 
			
		||||
			</template>
 | 
			
		||||
		</n-grid>
 | 
			
		||||
	</n-card>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<style scoped></style>
 | 
			
		||||
 
 | 
			
		||||
@@ -1,14 +1,18 @@
 | 
			
		||||
import { fileURLToPath, URL } from "node:url";
 | 
			
		||||
import { fileURLToPath, URL } from 'node:url';
 | 
			
		||||
 | 
			
		||||
import { defineConfig } from "vite";
 | 
			
		||||
import vue from "@vitejs/plugin-vue";
 | 
			
		||||
import { defineConfig } from 'vite';
 | 
			
		||||
import vue from '@vitejs/plugin-vue';
 | 
			
		||||
import vueJsx from '@vitejs/plugin-vue-jsx';
 | 
			
		||||
 | 
			
		||||
// https://vitejs.dev/config/
 | 
			
		||||
export default defineConfig({
 | 
			
		||||
  plugins: [vue()],
 | 
			
		||||
  resolve: {
 | 
			
		||||
    alias: {
 | 
			
		||||
      "@": fileURLToPath(new URL("./src", import.meta.url)),
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
	plugins: [vue(), vueJsx()],
 | 
			
		||||
	resolve: {
 | 
			
		||||
		alias: {
 | 
			
		||||
			'@': fileURLToPath(new URL('./src', import.meta.url))
 | 
			
		||||
		}
 | 
			
		||||
	},
 | 
			
		||||
	build: {
 | 
			
		||||
		chunkSizeWarningLimit: 1024 * 1024
 | 
			
		||||
	}
 | 
			
		||||
});
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										32
									
								
								frontend/vite.dev.config.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								frontend/vite.dev.config.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,32 @@
 | 
			
		||||
import { fileURLToPath, URL } from 'node:url';
 | 
			
		||||
 | 
			
		||||
import { defineConfig } from 'vite';
 | 
			
		||||
import vue from '@vitejs/plugin-vue';
 | 
			
		||||
import vueJsx from '@vitejs/plugin-vue-jsx';
 | 
			
		||||
 | 
			
		||||
// https://vitejs.dev/config/
 | 
			
		||||
export default defineConfig({
 | 
			
		||||
	plugins: [vue(), vueJsx()],
 | 
			
		||||
	resolve: {
 | 
			
		||||
		alias: {
 | 
			
		||||
			'@': fileURLToPath(new URL('./src', import.meta.url))
 | 
			
		||||
		}
 | 
			
		||||
	},
 | 
			
		||||
	build: {
 | 
			
		||||
		watch: {},
 | 
			
		||||
		sourcemap: false,
 | 
			
		||||
		minify: false,
 | 
			
		||||
		outDir: '../run/static',
 | 
			
		||||
		emptyOutDir: true,
 | 
			
		||||
		reportCompressedSize: false,
 | 
			
		||||
		rollupOptions: {
 | 
			
		||||
			output: {
 | 
			
		||||
				manualChunks(id) {
 | 
			
		||||
					if (id.includes('node_modules')) {
 | 
			
		||||
						return 'vendor';
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
});
 | 
			
		||||
@@ -2,11 +2,273 @@
 | 
			
		||||
# yarn lockfile v1
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
"@babel/parser@^7.16.4":
 | 
			
		||||
"@ampproject/remapping@^2.1.0":
 | 
			
		||||
  version "2.2.0"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@ampproject/remapping/-/remapping-2.2.0.tgz#56c133824780de3174aed5ab6834f3026790154d"
 | 
			
		||||
  integrity sha512-qRmjj8nj9qmLTQXXmaR1cck3UXSRMPrbsLJAasZpF+t3riI71BXed5ebIOYwQntykeZuhjsdweEc9BxH5Jc26w==
 | 
			
		||||
  dependencies:
 | 
			
		||||
    "@jridgewell/gen-mapping" "^0.1.0"
 | 
			
		||||
    "@jridgewell/trace-mapping" "^0.3.9"
 | 
			
		||||
 | 
			
		||||
"@babel/code-frame@^7.18.6":
 | 
			
		||||
  version "7.18.6"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.18.6.tgz#3b25d38c89600baa2dcc219edfa88a74eb2c427a"
 | 
			
		||||
  integrity sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q==
 | 
			
		||||
  dependencies:
 | 
			
		||||
    "@babel/highlight" "^7.18.6"
 | 
			
		||||
 | 
			
		||||
"@babel/compat-data@^7.18.8":
 | 
			
		||||
  version "7.18.13"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.18.13.tgz#6aff7b350a1e8c3e40b029e46cbe78e24a913483"
 | 
			
		||||
  integrity sha512-5yUzC5LqyTFp2HLmDoxGQelcdYgSpP9xsnMWBphAscOdFrHSAVbLNzWiy32sVNDqJRDiJK6klfDnAgu6PAGSHw==
 | 
			
		||||
 | 
			
		||||
"@babel/core@^7.18.13":
 | 
			
		||||
  version "7.18.13"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.18.13.tgz#9be8c44512751b05094a4d3ab05fc53a47ce00ac"
 | 
			
		||||
  integrity sha512-ZisbOvRRusFktksHSG6pjj1CSvkPkcZq/KHD45LAkVP/oiHJkNBZWfpvlLmX8OtHDG8IuzsFlVRWo08w7Qxn0A==
 | 
			
		||||
  dependencies:
 | 
			
		||||
    "@ampproject/remapping" "^2.1.0"
 | 
			
		||||
    "@babel/code-frame" "^7.18.6"
 | 
			
		||||
    "@babel/generator" "^7.18.13"
 | 
			
		||||
    "@babel/helper-compilation-targets" "^7.18.9"
 | 
			
		||||
    "@babel/helper-module-transforms" "^7.18.9"
 | 
			
		||||
    "@babel/helpers" "^7.18.9"
 | 
			
		||||
    "@babel/parser" "^7.18.13"
 | 
			
		||||
    "@babel/template" "^7.18.10"
 | 
			
		||||
    "@babel/traverse" "^7.18.13"
 | 
			
		||||
    "@babel/types" "^7.18.13"
 | 
			
		||||
    convert-source-map "^1.7.0"
 | 
			
		||||
    debug "^4.1.0"
 | 
			
		||||
    gensync "^1.0.0-beta.2"
 | 
			
		||||
    json5 "^2.2.1"
 | 
			
		||||
    semver "^6.3.0"
 | 
			
		||||
 | 
			
		||||
"@babel/generator@^7.18.13":
 | 
			
		||||
  version "7.18.13"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.18.13.tgz#59550cbb9ae79b8def15587bdfbaa388c4abf212"
 | 
			
		||||
  integrity sha512-CkPg8ySSPuHTYPJYo7IRALdqyjM9HCbt/3uOBEFbzyGVP6Mn8bwFPB0jX6982JVNBlYzM1nnPkfjuXSOPtQeEQ==
 | 
			
		||||
  dependencies:
 | 
			
		||||
    "@babel/types" "^7.18.13"
 | 
			
		||||
    "@jridgewell/gen-mapping" "^0.3.2"
 | 
			
		||||
    jsesc "^2.5.1"
 | 
			
		||||
 | 
			
		||||
"@babel/helper-annotate-as-pure@^7.18.6":
 | 
			
		||||
  version "7.18.6"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.18.6.tgz#eaa49f6f80d5a33f9a5dd2276e6d6e451be0a6bb"
 | 
			
		||||
  integrity sha512-duORpUiYrEpzKIop6iNbjnwKLAKnJ47csTyRACyEmWj0QdUrm5aqNJGHSSEQSUAvNW0ojX0dOmK9dZduvkfeXA==
 | 
			
		||||
  dependencies:
 | 
			
		||||
    "@babel/types" "^7.18.6"
 | 
			
		||||
 | 
			
		||||
"@babel/helper-compilation-targets@^7.18.9":
 | 
			
		||||
  version "7.18.9"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.18.9.tgz#69e64f57b524cde3e5ff6cc5a9f4a387ee5563bf"
 | 
			
		||||
  integrity sha512-tzLCyVmqUiFlcFoAPLA/gL9TeYrF61VLNtb+hvkuVaB5SUjW7jcfrglBIX1vUIoT7CLP3bBlIMeyEsIl2eFQNg==
 | 
			
		||||
  dependencies:
 | 
			
		||||
    "@babel/compat-data" "^7.18.8"
 | 
			
		||||
    "@babel/helper-validator-option" "^7.18.6"
 | 
			
		||||
    browserslist "^4.20.2"
 | 
			
		||||
    semver "^6.3.0"
 | 
			
		||||
 | 
			
		||||
"@babel/helper-create-class-features-plugin@^7.18.9":
 | 
			
		||||
  version "7.18.13"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.18.13.tgz#63e771187bd06d234f95fdf8bd5f8b6429de6298"
 | 
			
		||||
  integrity sha512-hDvXp+QYxSRL+23mpAlSGxHMDyIGChm0/AwTfTAAK5Ufe40nCsyNdaYCGuK91phn/fVu9kqayImRDkvNAgdrsA==
 | 
			
		||||
  dependencies:
 | 
			
		||||
    "@babel/helper-annotate-as-pure" "^7.18.6"
 | 
			
		||||
    "@babel/helper-environment-visitor" "^7.18.9"
 | 
			
		||||
    "@babel/helper-function-name" "^7.18.9"
 | 
			
		||||
    "@babel/helper-member-expression-to-functions" "^7.18.9"
 | 
			
		||||
    "@babel/helper-optimise-call-expression" "^7.18.6"
 | 
			
		||||
    "@babel/helper-replace-supers" "^7.18.9"
 | 
			
		||||
    "@babel/helper-split-export-declaration" "^7.18.6"
 | 
			
		||||
 | 
			
		||||
"@babel/helper-environment-visitor@^7.18.9":
 | 
			
		||||
  version "7.18.9"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@babel/helper-environment-visitor/-/helper-environment-visitor-7.18.9.tgz#0c0cee9b35d2ca190478756865bb3528422f51be"
 | 
			
		||||
  integrity sha512-3r/aACDJ3fhQ/EVgFy0hpj8oHyHpQc+LPtJoY9SzTThAsStm4Ptegq92vqKoE3vD706ZVFWITnMnxucw+S9Ipg==
 | 
			
		||||
 | 
			
		||||
"@babel/helper-function-name@^7.18.9":
 | 
			
		||||
  version "7.18.9"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.18.9.tgz#940e6084a55dee867d33b4e487da2676365e86b0"
 | 
			
		||||
  integrity sha512-fJgWlZt7nxGksJS9a0XdSaI4XvpExnNIgRP+rVefWh5U7BL8pPuir6SJUmFKRfjWQ51OtWSzwOxhaH/EBWWc0A==
 | 
			
		||||
  dependencies:
 | 
			
		||||
    "@babel/template" "^7.18.6"
 | 
			
		||||
    "@babel/types" "^7.18.9"
 | 
			
		||||
 | 
			
		||||
"@babel/helper-hoist-variables@^7.18.6":
 | 
			
		||||
  version "7.18.6"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.18.6.tgz#d4d2c8fb4baeaa5c68b99cc8245c56554f926678"
 | 
			
		||||
  integrity sha512-UlJQPkFqFULIcyW5sbzgbkxn2FKRgwWiRexcuaR8RNJRy8+LLveqPjwZV/bwrLZCN0eUHD/x8D0heK1ozuoo6Q==
 | 
			
		||||
  dependencies:
 | 
			
		||||
    "@babel/types" "^7.18.6"
 | 
			
		||||
 | 
			
		||||
"@babel/helper-member-expression-to-functions@^7.18.9":
 | 
			
		||||
  version "7.18.9"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.18.9.tgz#1531661e8375af843ad37ac692c132841e2fd815"
 | 
			
		||||
  integrity sha512-RxifAh2ZoVU67PyKIO4AMi1wTenGfMR/O/ae0CCRqwgBAt5v7xjdtRw7UoSbsreKrQn5t7r89eruK/9JjYHuDg==
 | 
			
		||||
  dependencies:
 | 
			
		||||
    "@babel/types" "^7.18.9"
 | 
			
		||||
 | 
			
		||||
"@babel/helper-module-imports@^7.0.0", "@babel/helper-module-imports@^7.18.6":
 | 
			
		||||
  version "7.18.6"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.18.6.tgz#1e3ebdbbd08aad1437b428c50204db13c5a3ca6e"
 | 
			
		||||
  integrity sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA==
 | 
			
		||||
  dependencies:
 | 
			
		||||
    "@babel/types" "^7.18.6"
 | 
			
		||||
 | 
			
		||||
"@babel/helper-module-transforms@^7.18.9":
 | 
			
		||||
  version "7.18.9"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.18.9.tgz#5a1079c005135ed627442df31a42887e80fcb712"
 | 
			
		||||
  integrity sha512-KYNqY0ICwfv19b31XzvmI/mfcylOzbLtowkw+mfvGPAQ3kfCnMLYbED3YecL5tPd8nAYFQFAd6JHp2LxZk/J1g==
 | 
			
		||||
  dependencies:
 | 
			
		||||
    "@babel/helper-environment-visitor" "^7.18.9"
 | 
			
		||||
    "@babel/helper-module-imports" "^7.18.6"
 | 
			
		||||
    "@babel/helper-simple-access" "^7.18.6"
 | 
			
		||||
    "@babel/helper-split-export-declaration" "^7.18.6"
 | 
			
		||||
    "@babel/helper-validator-identifier" "^7.18.6"
 | 
			
		||||
    "@babel/template" "^7.18.6"
 | 
			
		||||
    "@babel/traverse" "^7.18.9"
 | 
			
		||||
    "@babel/types" "^7.18.9"
 | 
			
		||||
 | 
			
		||||
"@babel/helper-optimise-call-expression@^7.18.6":
 | 
			
		||||
  version "7.18.6"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.18.6.tgz#9369aa943ee7da47edab2cb4e838acf09d290ffe"
 | 
			
		||||
  integrity sha512-HP59oD9/fEHQkdcbgFCnbmgH5vIQTJbxh2yf+CdM89/glUNnuzr87Q8GIjGEnOktTROemO0Pe0iPAYbqZuOUiA==
 | 
			
		||||
  dependencies:
 | 
			
		||||
    "@babel/types" "^7.18.6"
 | 
			
		||||
 | 
			
		||||
"@babel/helper-plugin-utils@^7.10.4", "@babel/helper-plugin-utils@^7.18.6", "@babel/helper-plugin-utils@^7.18.9":
 | 
			
		||||
  version "7.18.9"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.18.9.tgz#4b8aea3b069d8cb8a72cdfe28ddf5ceca695ef2f"
 | 
			
		||||
  integrity sha512-aBXPT3bmtLryXaoJLyYPXPlSD4p1ld9aYeR+sJNOZjJJGiOpb+fKfh3NkcCu7J54nUJwCERPBExCCpyCOHnu/w==
 | 
			
		||||
 | 
			
		||||
"@babel/helper-replace-supers@^7.18.9":
 | 
			
		||||
  version "7.18.9"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.18.9.tgz#1092e002feca980fbbb0bd4d51b74a65c6a500e6"
 | 
			
		||||
  integrity sha512-dNsWibVI4lNT6HiuOIBr1oyxo40HvIVmbwPUm3XZ7wMh4k2WxrxTqZwSqw/eEmXDS9np0ey5M2bz9tBmO9c+YQ==
 | 
			
		||||
  dependencies:
 | 
			
		||||
    "@babel/helper-environment-visitor" "^7.18.9"
 | 
			
		||||
    "@babel/helper-member-expression-to-functions" "^7.18.9"
 | 
			
		||||
    "@babel/helper-optimise-call-expression" "^7.18.6"
 | 
			
		||||
    "@babel/traverse" "^7.18.9"
 | 
			
		||||
    "@babel/types" "^7.18.9"
 | 
			
		||||
 | 
			
		||||
"@babel/helper-simple-access@^7.18.6":
 | 
			
		||||
  version "7.18.6"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.18.6.tgz#d6d8f51f4ac2978068df934b569f08f29788c7ea"
 | 
			
		||||
  integrity sha512-iNpIgTgyAvDQpDj76POqg+YEt8fPxx3yaNBg3S30dxNKm2SWfYhD0TGrK/Eu9wHpUW63VQU894TsTg+GLbUa1g==
 | 
			
		||||
  dependencies:
 | 
			
		||||
    "@babel/types" "^7.18.6"
 | 
			
		||||
 | 
			
		||||
"@babel/helper-split-export-declaration@^7.18.6":
 | 
			
		||||
  version "7.18.6"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.18.6.tgz#7367949bc75b20c6d5a5d4a97bba2824ae8ef075"
 | 
			
		||||
  integrity sha512-bde1etTx6ZyTmobl9LLMMQsaizFVZrquTEHOqKeQESMKo4PlObf+8+JA25ZsIpZhT/WEd39+vOdLXAFG/nELpA==
 | 
			
		||||
  dependencies:
 | 
			
		||||
    "@babel/types" "^7.18.6"
 | 
			
		||||
 | 
			
		||||
"@babel/helper-string-parser@^7.18.10":
 | 
			
		||||
  version "7.18.10"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.18.10.tgz#181f22d28ebe1b3857fa575f5c290b1aaf659b56"
 | 
			
		||||
  integrity sha512-XtIfWmeNY3i4t7t4D2t02q50HvqHybPqW2ki1kosnvWCwuCMeo81Jf0gwr85jy/neUdg5XDdeFE/80DXiO+njw==
 | 
			
		||||
 | 
			
		||||
"@babel/helper-validator-identifier@^7.18.6":
 | 
			
		||||
  version "7.18.6"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.18.6.tgz#9c97e30d31b2b8c72a1d08984f2ca9b574d7a076"
 | 
			
		||||
  integrity sha512-MmetCkz9ej86nJQV+sFCxoGGrUbU3q02kgLciwkrt9QqEB7cP39oKEY0PakknEO0Gu20SskMRi+AYZ3b1TpN9g==
 | 
			
		||||
 | 
			
		||||
"@babel/helper-validator-option@^7.18.6":
 | 
			
		||||
  version "7.18.6"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.18.6.tgz#bf0d2b5a509b1f336099e4ff36e1a63aa5db4db8"
 | 
			
		||||
  integrity sha512-XO7gESt5ouv/LRJdrVjkShckw6STTaB7l9BrpBaAHDeF5YZT+01PCwmR0SJHnkW6i8OwW/EVWRShfi4j2x+KQw==
 | 
			
		||||
 | 
			
		||||
"@babel/helpers@^7.18.9":
 | 
			
		||||
  version "7.18.9"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.18.9.tgz#4bef3b893f253a1eced04516824ede94dcfe7ff9"
 | 
			
		||||
  integrity sha512-Jf5a+rbrLoR4eNdUmnFu8cN5eNJT6qdTdOg5IHIzq87WwyRw9PwguLFOWYgktN/60IP4fgDUawJvs7PjQIzELQ==
 | 
			
		||||
  dependencies:
 | 
			
		||||
    "@babel/template" "^7.18.6"
 | 
			
		||||
    "@babel/traverse" "^7.18.9"
 | 
			
		||||
    "@babel/types" "^7.18.9"
 | 
			
		||||
 | 
			
		||||
"@babel/highlight@^7.18.6":
 | 
			
		||||
  version "7.18.6"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.18.6.tgz#81158601e93e2563795adcbfbdf5d64be3f2ecdf"
 | 
			
		||||
  integrity sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g==
 | 
			
		||||
  dependencies:
 | 
			
		||||
    "@babel/helper-validator-identifier" "^7.18.6"
 | 
			
		||||
    chalk "^2.0.0"
 | 
			
		||||
    js-tokens "^4.0.0"
 | 
			
		||||
 | 
			
		||||
"@babel/parser@^7.16.4", "@babel/parser@^7.18.10", "@babel/parser@^7.18.13":
 | 
			
		||||
  version "7.18.13"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.18.13.tgz#5b2dd21cae4a2c5145f1fbd8ca103f9313d3b7e4"
 | 
			
		||||
  integrity sha512-dgXcIfMuQ0kgzLB2b9tRZs7TTFFaGM2AbtA4fJgUUYukzGH4jwsS7hzQHEGs67jdehpm22vkgKwvbU+aEflgwg==
 | 
			
		||||
 | 
			
		||||
"@babel/plugin-syntax-import-meta@^7.10.4":
 | 
			
		||||
  version "7.10.4"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz#ee601348c370fa334d2207be158777496521fd51"
 | 
			
		||||
  integrity sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==
 | 
			
		||||
  dependencies:
 | 
			
		||||
    "@babel/helper-plugin-utils" "^7.10.4"
 | 
			
		||||
 | 
			
		||||
"@babel/plugin-syntax-jsx@^7.0.0":
 | 
			
		||||
  version "7.18.6"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.18.6.tgz#a8feef63b010150abd97f1649ec296e849943ca0"
 | 
			
		||||
  integrity sha512-6mmljtAedFGTWu2p/8WIORGwy+61PLgOMPOdazc7YoJ9ZCWUyFy3A6CpPkRKLKD1ToAesxX8KGEViAiLo9N+7Q==
 | 
			
		||||
  dependencies:
 | 
			
		||||
    "@babel/helper-plugin-utils" "^7.18.6"
 | 
			
		||||
 | 
			
		||||
"@babel/plugin-syntax-typescript@^7.18.6":
 | 
			
		||||
  version "7.18.6"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.18.6.tgz#1c09cd25795c7c2b8a4ba9ae49394576d4133285"
 | 
			
		||||
  integrity sha512-mAWAuq4rvOepWCBid55JuRNvpTNf2UGVgoz4JV0fXEKolsVZDzsa4NqCef758WZJj/GDu0gVGItjKFiClTAmZA==
 | 
			
		||||
  dependencies:
 | 
			
		||||
    "@babel/helper-plugin-utils" "^7.18.6"
 | 
			
		||||
 | 
			
		||||
"@babel/plugin-transform-typescript@^7.18.12":
 | 
			
		||||
  version "7.18.12"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.18.12.tgz#712e9a71b9e00fde9f8c0238e0cceee86ab2f8fd"
 | 
			
		||||
  integrity sha512-2vjjam0cum0miPkenUbQswKowuxs/NjMwIKEq0zwegRxXk12C9YOF9STXnaUptITOtOJHKHpzvvWYOjbm6tc0w==
 | 
			
		||||
  dependencies:
 | 
			
		||||
    "@babel/helper-create-class-features-plugin" "^7.18.9"
 | 
			
		||||
    "@babel/helper-plugin-utils" "^7.18.9"
 | 
			
		||||
    "@babel/plugin-syntax-typescript" "^7.18.6"
 | 
			
		||||
 | 
			
		||||
"@babel/template@^7.0.0", "@babel/template@^7.18.10", "@babel/template@^7.18.6":
 | 
			
		||||
  version "7.18.10"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.18.10.tgz#6f9134835970d1dbf0835c0d100c9f38de0c5e71"
 | 
			
		||||
  integrity sha512-TI+rCtooWHr3QJ27kJxfjutghu44DLnasDMwpDqCXVTal9RLp3RSYNh4NdBrRP2cQAoG9A8juOQl6P6oZG4JxA==
 | 
			
		||||
  dependencies:
 | 
			
		||||
    "@babel/code-frame" "^7.18.6"
 | 
			
		||||
    "@babel/parser" "^7.18.10"
 | 
			
		||||
    "@babel/types" "^7.18.10"
 | 
			
		||||
 | 
			
		||||
"@babel/traverse@^7.0.0", "@babel/traverse@^7.18.13", "@babel/traverse@^7.18.9":
 | 
			
		||||
  version "7.18.13"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.18.13.tgz#5ab59ef51a997b3f10c4587d648b9696b6cb1a68"
 | 
			
		||||
  integrity sha512-N6kt9X1jRMLPxxxPYWi7tgvJRH/rtoU+dbKAPDM44RFHiMH8igdsaSBgFeskhSl/kLWLDUvIh1RXCrTmg0/zvA==
 | 
			
		||||
  dependencies:
 | 
			
		||||
    "@babel/code-frame" "^7.18.6"
 | 
			
		||||
    "@babel/generator" "^7.18.13"
 | 
			
		||||
    "@babel/helper-environment-visitor" "^7.18.9"
 | 
			
		||||
    "@babel/helper-function-name" "^7.18.9"
 | 
			
		||||
    "@babel/helper-hoist-variables" "^7.18.6"
 | 
			
		||||
    "@babel/helper-split-export-declaration" "^7.18.6"
 | 
			
		||||
    "@babel/parser" "^7.18.13"
 | 
			
		||||
    "@babel/types" "^7.18.13"
 | 
			
		||||
    debug "^4.1.0"
 | 
			
		||||
    globals "^11.1.0"
 | 
			
		||||
 | 
			
		||||
"@babel/types@^7.0.0", "@babel/types@^7.18.10", "@babel/types@^7.18.13", "@babel/types@^7.18.6", "@babel/types@^7.18.9":
 | 
			
		||||
  version "7.18.13"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.18.13.tgz#30aeb9e514f4100f7c1cb6e5ba472b30e48f519a"
 | 
			
		||||
  integrity sha512-ePqfTihzW0W6XAU+aMw2ykilisStJfDnsejDCXRchCcMJ4O0+8DhPXf2YUbZ6wjBlsEmZwLK/sPweWtu8hcJYQ==
 | 
			
		||||
  dependencies:
 | 
			
		||||
    "@babel/helper-string-parser" "^7.18.10"
 | 
			
		||||
    "@babel/helper-validator-identifier" "^7.18.6"
 | 
			
		||||
    to-fast-properties "^2.0.0"
 | 
			
		||||
 | 
			
		||||
"@css-render/plugin-bem@^0.15.10":
 | 
			
		||||
  version "0.15.11"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@css-render/plugin-bem/-/plugin-bem-0.15.11.tgz#250b853704af1fbb935b8fcd987839dcc9c95ce2"
 | 
			
		||||
@@ -61,6 +323,46 @@
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz#b520529ec21d8e5945a1851dfd1c32e94e39ff45"
 | 
			
		||||
  integrity sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==
 | 
			
		||||
 | 
			
		||||
"@jridgewell/gen-mapping@^0.1.0":
 | 
			
		||||
  version "0.1.1"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.1.1.tgz#e5d2e450306a9491e3bd77e323e38d7aff315996"
 | 
			
		||||
  integrity sha512-sQXCasFk+U8lWYEe66WxRDOE9PjVz4vSM51fTu3Hw+ClTpUSQb718772vH3pyS5pShp6lvQM7SxgIDXXXmOX7w==
 | 
			
		||||
  dependencies:
 | 
			
		||||
    "@jridgewell/set-array" "^1.0.0"
 | 
			
		||||
    "@jridgewell/sourcemap-codec" "^1.4.10"
 | 
			
		||||
 | 
			
		||||
"@jridgewell/gen-mapping@^0.3.2":
 | 
			
		||||
  version "0.3.2"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz#c1aedc61e853f2bb9f5dfe6d4442d3b565b253b9"
 | 
			
		||||
  integrity sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A==
 | 
			
		||||
  dependencies:
 | 
			
		||||
    "@jridgewell/set-array" "^1.0.1"
 | 
			
		||||
    "@jridgewell/sourcemap-codec" "^1.4.10"
 | 
			
		||||
    "@jridgewell/trace-mapping" "^0.3.9"
 | 
			
		||||
 | 
			
		||||
"@jridgewell/resolve-uri@^3.0.3":
 | 
			
		||||
  version "3.1.0"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz#2203b118c157721addfe69d47b70465463066d78"
 | 
			
		||||
  integrity sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==
 | 
			
		||||
 | 
			
		||||
"@jridgewell/set-array@^1.0.0", "@jridgewell/set-array@^1.0.1":
 | 
			
		||||
  version "1.1.2"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@jridgewell/set-array/-/set-array-1.1.2.tgz#7c6cf998d6d20b914c0a55a91ae928ff25965e72"
 | 
			
		||||
  integrity sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==
 | 
			
		||||
 | 
			
		||||
"@jridgewell/sourcemap-codec@^1.4.10":
 | 
			
		||||
  version "1.4.14"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz#add4c98d341472a289190b424efbdb096991bb24"
 | 
			
		||||
  integrity sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==
 | 
			
		||||
 | 
			
		||||
"@jridgewell/trace-mapping@^0.3.9":
 | 
			
		||||
  version "0.3.15"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.15.tgz#aba35c48a38d3fd84b37e66c9c0423f9744f9774"
 | 
			
		||||
  integrity sha512-oWZNOULl+UbhsgB51uuZzglikfIKSUBO/M9W2OfEjn7cmqoAiCgmv9lyACTUacZwBz0ITnJ2NqjU8Tx0DHL88g==
 | 
			
		||||
  dependencies:
 | 
			
		||||
    "@jridgewell/resolve-uri" "^3.0.3"
 | 
			
		||||
    "@jridgewell/sourcemap-codec" "^1.4.10"
 | 
			
		||||
 | 
			
		||||
"@juggle/resize-observer@^3.3.1":
 | 
			
		||||
  version "3.4.0"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@juggle/resize-observer/-/resize-observer-3.4.0.tgz#08d6c5e20cf7e4cc02fd181c4b0c225cd31dbb60"
 | 
			
		||||
@@ -200,6 +502,26 @@
 | 
			
		||||
    "@typescript-eslint/types" "5.36.1"
 | 
			
		||||
    eslint-visitor-keys "^3.3.0"
 | 
			
		||||
 | 
			
		||||
"@vicons/carbon@^0.12.0":
 | 
			
		||||
  version "0.12.0"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@vicons/carbon/-/carbon-0.12.0.tgz#dfcc5d6283662eccee55700b2d5c29e688a70f5a"
 | 
			
		||||
  integrity sha512-kCOgr/ZOhZzoiFLJ8pwxMa2TMxrkCUOA22qExPabus35F4+USqzcsxaPoYtqRd9ROOYiHrSqwapak/ywF0D9bg==
 | 
			
		||||
 | 
			
		||||
"@vicons/ionicons5@^0.12.0":
 | 
			
		||||
  version "0.12.0"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@vicons/ionicons5/-/ionicons5-0.12.0.tgz#c39fda04420dfae3b58053faf8aaf3555253299d"
 | 
			
		||||
  integrity sha512-Iy1EUVRpX0WWxeu1VIReR1zsZLMc4fqpt223czR+Rpnrwu7pt46nbnC2ycO7ItI/uqDLJxnbcMC7FujKs9IfFA==
 | 
			
		||||
 | 
			
		||||
"@vitejs/plugin-vue-jsx@^2.0.0":
 | 
			
		||||
  version "2.0.1"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@vitejs/plugin-vue-jsx/-/plugin-vue-jsx-2.0.1.tgz#563a844964f5b025c828b452d6a9882df7194f9a"
 | 
			
		||||
  integrity sha512-lmiR1k9+lrF7LMczO0pxtQ8mOn6XeppJDHxnpxkJQpT5SiKz4SKhKdeNstXaTNuR8qZhUo5X0pJlcocn72Y4Jg==
 | 
			
		||||
  dependencies:
 | 
			
		||||
    "@babel/core" "^7.18.13"
 | 
			
		||||
    "@babel/plugin-syntax-import-meta" "^7.10.4"
 | 
			
		||||
    "@babel/plugin-transform-typescript" "^7.18.12"
 | 
			
		||||
    "@vue/babel-plugin-jsx" "^1.1.1"
 | 
			
		||||
 | 
			
		||||
"@vitejs/plugin-vue@^3.0.1":
 | 
			
		||||
  version "3.0.3"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@vitejs/plugin-vue/-/plugin-vue-3.0.3.tgz#7e3e401ccb30b4380d2279d9849281413f1791ef"
 | 
			
		||||
@@ -255,6 +577,26 @@
 | 
			
		||||
    "@volar/typescript-faster" "0.39.5"
 | 
			
		||||
    "@volar/vue-language-core" "0.39.5"
 | 
			
		||||
 | 
			
		||||
"@vue/babel-helper-vue-transform-on@^1.0.2":
 | 
			
		||||
  version "1.0.2"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@vue/babel-helper-vue-transform-on/-/babel-helper-vue-transform-on-1.0.2.tgz#9b9c691cd06fc855221a2475c3cc831d774bc7dc"
 | 
			
		||||
  integrity sha512-hz4R8tS5jMn8lDq6iD+yWL6XNB699pGIVLk7WSJnn1dbpjaazsjZQkieJoRX6gW5zpYSCFqQ7jUquPNY65tQYA==
 | 
			
		||||
 | 
			
		||||
"@vue/babel-plugin-jsx@^1.1.1":
 | 
			
		||||
  version "1.1.1"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@vue/babel-plugin-jsx/-/babel-plugin-jsx-1.1.1.tgz#0c5bac27880d23f89894cd036a37b55ef61ddfc1"
 | 
			
		||||
  integrity sha512-j2uVfZjnB5+zkcbc/zsOc0fSNGCMMjaEXP52wdwdIfn0qjFfEYpYZBFKFg+HHnQeJCVrjOeO0YxgaL7DMrym9w==
 | 
			
		||||
  dependencies:
 | 
			
		||||
    "@babel/helper-module-imports" "^7.0.0"
 | 
			
		||||
    "@babel/plugin-syntax-jsx" "^7.0.0"
 | 
			
		||||
    "@babel/template" "^7.0.0"
 | 
			
		||||
    "@babel/traverse" "^7.0.0"
 | 
			
		||||
    "@babel/types" "^7.0.0"
 | 
			
		||||
    "@vue/babel-helper-vue-transform-on" "^1.0.2"
 | 
			
		||||
    camelcase "^6.0.0"
 | 
			
		||||
    html-tags "^3.1.0"
 | 
			
		||||
    svg-tags "^1.0.0"
 | 
			
		||||
 | 
			
		||||
"@vue/compiler-core@3.2.38", "@vue/compiler-core@^3.2.37":
 | 
			
		||||
  version "3.2.38"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@vue/compiler-core/-/compiler-core-3.2.38.tgz#0a2a7bffd2280ac19a96baf5301838a2cf1964d7"
 | 
			
		||||
@@ -482,6 +824,16 @@ braces@^3.0.2, braces@~3.0.2:
 | 
			
		||||
  dependencies:
 | 
			
		||||
    fill-range "^7.0.1"
 | 
			
		||||
 | 
			
		||||
browserslist@^4.20.2:
 | 
			
		||||
  version "4.21.3"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.21.3.tgz#5df277694eb3c48bc5c4b05af3e8b7e09c5a6d1a"
 | 
			
		||||
  integrity sha512-898rgRXLAyRkM1GryrrBHGkqA5hlpkV5MhtZwg9QXeiyLUYs2k00Un05aX5l2/yJIOObYKOpS2JNo8nJDE7fWQ==
 | 
			
		||||
  dependencies:
 | 
			
		||||
    caniuse-lite "^1.0.30001370"
 | 
			
		||||
    electron-to-chromium "^1.4.202"
 | 
			
		||||
    node-releases "^2.0.6"
 | 
			
		||||
    update-browserslist-db "^1.0.5"
 | 
			
		||||
 | 
			
		||||
call-bind@^1.0.0, call-bind@^1.0.2:
 | 
			
		||||
  version "1.0.2"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.2.tgz#b1d4e89e688119c3c9a903ad30abb2f6a919be3c"
 | 
			
		||||
@@ -495,7 +847,17 @@ callsites@^3.0.0:
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73"
 | 
			
		||||
  integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==
 | 
			
		||||
 | 
			
		||||
chalk@^2.4.1:
 | 
			
		||||
camelcase@^6.0.0:
 | 
			
		||||
  version "6.3.0"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.3.0.tgz#5685b95eb209ac9c0c177467778c9c84df58ba9a"
 | 
			
		||||
  integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==
 | 
			
		||||
 | 
			
		||||
caniuse-lite@^1.0.30001370:
 | 
			
		||||
  version "1.0.30001388"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001388.tgz#88e01f4591cbd81f9f665f3f078c66b509fbe55d"
 | 
			
		||||
  integrity sha512-znVbq4OUjqgLxMxoNX2ZeeLR0d7lcDiE5uJ4eUiWdml1J1EkxbnQq6opT9jb9SMfJxB0XA16/ziHwni4u1I3GQ==
 | 
			
		||||
 | 
			
		||||
chalk@^2.0.0, chalk@^2.4.1:
 | 
			
		||||
  version "2.4.2"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424"
 | 
			
		||||
  integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==
 | 
			
		||||
@@ -527,19 +889,6 @@ chalk@^4.0.0:
 | 
			
		||||
  optionalDependencies:
 | 
			
		||||
    fsevents "~2.3.2"
 | 
			
		||||
 | 
			
		||||
class-transformer@^0.5.1:
 | 
			
		||||
  version "0.5.1"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/class-transformer/-/class-transformer-0.5.1.tgz#24147d5dffd2a6cea930a3250a677addf96ab336"
 | 
			
		||||
  integrity sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw==
 | 
			
		||||
 | 
			
		||||
class-validator@^0.13.2:
 | 
			
		||||
  version "0.13.2"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/class-validator/-/class-validator-0.13.2.tgz#64b031e9f3f81a1e1dcd04a5d604734608b24143"
 | 
			
		||||
  integrity sha512-yBUcQy07FPlGzUjoLuUfIOXzgynnQPPruyK1Ge2B74k9ROwnle1E+NxLWnUv5OLU8hA/qL5leAE9XnXq3byaBw==
 | 
			
		||||
  dependencies:
 | 
			
		||||
    libphonenumber-js "^1.9.43"
 | 
			
		||||
    validator "^13.7.0"
 | 
			
		||||
 | 
			
		||||
color-convert@^1.9.0:
 | 
			
		||||
  version "1.9.3"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8"
 | 
			
		||||
@@ -576,6 +925,13 @@ concat-map@0.0.1:
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
 | 
			
		||||
  integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==
 | 
			
		||||
 | 
			
		||||
convert-source-map@^1.7.0:
 | 
			
		||||
  version "1.8.0"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.8.0.tgz#f3373c32d21b4d780dd8004514684fb791ca4369"
 | 
			
		||||
  integrity sha512-+OQdjP49zViI/6i7nIJpA8rAl4sV/JdPfU9nZs3VqOwGIgizICvuN2ru6fMd+4llL0tar18UYJXfZ/TWtmhUjA==
 | 
			
		||||
  dependencies:
 | 
			
		||||
    safe-buffer "~5.1.1"
 | 
			
		||||
 | 
			
		||||
cross-spawn@^6.0.5:
 | 
			
		||||
  version "6.0.5"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4"
 | 
			
		||||
@@ -621,16 +977,16 @@ csstype@~3.0.5:
 | 
			
		||||
  integrity sha512-sa6P2wJ+CAbgyy4KFssIb/JNMLxFvKF1pCYCSXS8ZMuqZnMsrxqI2E5sPyoTpxoPU/gVZMzr2zjOfg8GIZOMsw==
 | 
			
		||||
 | 
			
		||||
date-fns-tz@^1.3.3:
 | 
			
		||||
  version "1.3.6"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/date-fns-tz/-/date-fns-tz-1.3.6.tgz#4195a58a2f86eda55ea69fb477f3ed8a6e2188ac"
 | 
			
		||||
  integrity sha512-C8q7mErvG4INw1ZwAFmPlGjEo5Sv4udjKVbTc03zpP9cu6cp5AemFzKhz0V68LGcWEtX5mJudzzg3G04emIxLA==
 | 
			
		||||
  version "1.3.7"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/date-fns-tz/-/date-fns-tz-1.3.7.tgz#e8e9d2aaceba5f1cc0e677631563081fdcb0e69a"
 | 
			
		||||
  integrity sha512-1t1b8zyJo+UI8aR+g3iqr5fkUHWpd58VBx8J/ZSQ+w7YrGlw80Ag4sA86qkfCXRBLmMc4I2US+aPMd4uKvwj5g==
 | 
			
		||||
 | 
			
		||||
date-fns@^2.28.0:
 | 
			
		||||
  version "2.29.2"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.29.2.tgz#0d4b3d0f3dff0f920820a070920f0d9662c51931"
 | 
			
		||||
  integrity sha512-0VNbwmWJDS/G3ySwFSJA3ayhbURMTJLtwM2DTxf9CWondCnh6DTNlO9JgRSq6ibf4eD0lfMJNBxUdEAHHix+bA==
 | 
			
		||||
 | 
			
		||||
debug@^4.1.1, debug@^4.3.2, debug@^4.3.4:
 | 
			
		||||
debug@^4.1.0, debug@^4.1.1, debug@^4.3.2, debug@^4.3.4:
 | 
			
		||||
  version "4.3.4"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865"
 | 
			
		||||
  integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==
 | 
			
		||||
@@ -669,6 +1025,11 @@ doctrine@^3.0.0:
 | 
			
		||||
  dependencies:
 | 
			
		||||
    esutils "^2.0.2"
 | 
			
		||||
 | 
			
		||||
electron-to-chromium@^1.4.202:
 | 
			
		||||
  version "1.4.240"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.240.tgz#b11fb838f2e79f34fbe8b57eec55e7e5d81ee6ea"
 | 
			
		||||
  integrity sha512-r20dUOtZ4vUPTqAajDGonIM1uas5tf85Up+wPdtNBNvBSqGCfkpvMVvQ1T8YJzPV9/Y9g3FbUDcXb94Rafycow==
 | 
			
		||||
 | 
			
		||||
error-ex@^1.3.1:
 | 
			
		||||
  version "1.3.2"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf"
 | 
			
		||||
@@ -841,6 +1202,11 @@ esbuild@^0.14.47:
 | 
			
		||||
    esbuild-windows-64 "0.14.54"
 | 
			
		||||
    esbuild-windows-arm64 "0.14.54"
 | 
			
		||||
 | 
			
		||||
escalade@^3.1.1:
 | 
			
		||||
  version "3.1.1"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40"
 | 
			
		||||
  integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==
 | 
			
		||||
 | 
			
		||||
escape-string-regexp@^1.0.5:
 | 
			
		||||
  version "1.0.5"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4"
 | 
			
		||||
@@ -1136,6 +1502,11 @@ functions-have-names@^1.2.2:
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/functions-have-names/-/functions-have-names-1.2.3.tgz#0404fe4ee2ba2f607f0e0ec3c80bae994133b834"
 | 
			
		||||
  integrity sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==
 | 
			
		||||
 | 
			
		||||
gensync@^1.0.0-beta.2:
 | 
			
		||||
  version "1.0.0-beta.2"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0"
 | 
			
		||||
  integrity sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==
 | 
			
		||||
 | 
			
		||||
get-intrinsic@^1.0.2, get-intrinsic@^1.1.0, get-intrinsic@^1.1.1:
 | 
			
		||||
  version "1.1.2"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.1.2.tgz#336975123e05ad0b7ba41f152ee4aadbea6cf598"
 | 
			
		||||
@@ -1179,6 +1550,11 @@ glob@^7.1.3:
 | 
			
		||||
    once "^1.3.0"
 | 
			
		||||
    path-is-absolute "^1.0.0"
 | 
			
		||||
 | 
			
		||||
globals@^11.1.0:
 | 
			
		||||
  version "11.12.0"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e"
 | 
			
		||||
  integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==
 | 
			
		||||
 | 
			
		||||
globals@^13.15.0:
 | 
			
		||||
  version "13.17.0"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/globals/-/globals-13.17.0.tgz#902eb1e680a41da93945adbdcb5a9f361ba69bd4"
 | 
			
		||||
@@ -1259,6 +1635,11 @@ hosted-git-info@^2.1.4:
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.9.tgz#dffc0bf9a21c02209090f2aa69429e1414daf3f9"
 | 
			
		||||
  integrity sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==
 | 
			
		||||
 | 
			
		||||
html-tags@^3.1.0:
 | 
			
		||||
  version "3.2.0"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/html-tags/-/html-tags-3.2.0.tgz#dbb3518d20b726524e4dd43de397eb0a95726961"
 | 
			
		||||
  integrity sha512-vy7ClnArOZwCnqZgvv+ddgHgJiAFXe3Ge9ML5/mBctVJoUoYPCdxVucOywjDARn6CVoh3dRSFdPHy2sX80L0Wg==
 | 
			
		||||
 | 
			
		||||
ignore@^5.2.0:
 | 
			
		||||
  version "5.2.0"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.0.tgz#6d3bac8fa7fe0d45d9f9be7bac2fc279577e345a"
 | 
			
		||||
@@ -1446,6 +1827,11 @@ isexe@^2.0.0:
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10"
 | 
			
		||||
  integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==
 | 
			
		||||
 | 
			
		||||
js-tokens@^4.0.0:
 | 
			
		||||
  version "4.0.0"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
 | 
			
		||||
  integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==
 | 
			
		||||
 | 
			
		||||
js-yaml@^4.1.0:
 | 
			
		||||
  version "4.1.0"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602"
 | 
			
		||||
@@ -1453,6 +1839,11 @@ js-yaml@^4.1.0:
 | 
			
		||||
  dependencies:
 | 
			
		||||
    argparse "^2.0.1"
 | 
			
		||||
 | 
			
		||||
jsesc@^2.5.1:
 | 
			
		||||
  version "2.5.2"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-2.5.2.tgz#80564d2e483dacf6e8ef209650a67df3f0c283a4"
 | 
			
		||||
  integrity sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==
 | 
			
		||||
 | 
			
		||||
json-parse-better-errors@^1.0.1:
 | 
			
		||||
  version "1.0.2"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz#bb867cfb3450e69107c131d1c514bab3dc8bcaa9"
 | 
			
		||||
@@ -1468,6 +1859,11 @@ json-stable-stringify-without-jsonify@^1.0.1:
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651"
 | 
			
		||||
  integrity sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==
 | 
			
		||||
 | 
			
		||||
json5@^2.2.1:
 | 
			
		||||
  version "2.2.1"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.1.tgz#655d50ed1e6f95ad1a3caababd2b0efda10b395c"
 | 
			
		||||
  integrity sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==
 | 
			
		||||
 | 
			
		||||
jwt-decode@^3.1.2:
 | 
			
		||||
  version "3.1.2"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/jwt-decode/-/jwt-decode-3.1.2.tgz#3fb319f3675a2df0c2895c8f5e9fa4b67b04ed59"
 | 
			
		||||
@@ -1481,11 +1877,6 @@ levn@^0.4.1:
 | 
			
		||||
    prelude-ls "^1.2.1"
 | 
			
		||||
    type-check "~0.4.0"
 | 
			
		||||
 | 
			
		||||
libphonenumber-js@^1.9.43:
 | 
			
		||||
  version "1.10.13"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/libphonenumber-js/-/libphonenumber-js-1.10.13.tgz#0b5833c7fdbf671140530d83531c6753f7e0ea3c"
 | 
			
		||||
  integrity sha512-b74iyWmwb4GprAUPjPkJ11GTC7KX4Pd3onpJfKxYyY8y9Rbb4ERY47LvCMEDM09WD3thiLDMXtkfDK/AX+zT7Q==
 | 
			
		||||
 | 
			
		||||
load-json-file@^4.0.0:
 | 
			
		||||
  version "4.0.0"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-4.0.0.tgz#2f5f45ab91e33216234fd53adab668eb4ec0993b"
 | 
			
		||||
@@ -1575,9 +1966,9 @@ ms@2.1.2:
 | 
			
		||||
  integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==
 | 
			
		||||
 | 
			
		||||
naive-ui@^2.32.1:
 | 
			
		||||
  version "2.33.1"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/naive-ui/-/naive-ui-2.33.1.tgz#ef1046b727145e868c4be32686fd6073219f07ac"
 | 
			
		||||
  integrity sha512-S8iS5TsnJ5PAbUCCC+IGjW7H6fYJF5s0HTzuUjqRLS8C1tFxmWhKkBZU1db/vg/4O5GKEyjaoq4ZSzRHOwRTcQ==
 | 
			
		||||
  version "2.33.2"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/naive-ui/-/naive-ui-2.33.2.tgz#c74e8b7c944f6af18cd850bd640f6d3485a47f05"
 | 
			
		||||
  integrity sha512-XT18dOE7dK15xedO9MlrPsD3AXBKncr0lqlsxakHl/DckqOaAbdA7yxDl/qtVTBC+1Rlf29cFP/th7P7DSy5zg==
 | 
			
		||||
  dependencies:
 | 
			
		||||
    "@css-render/plugin-bem" "^0.15.10"
 | 
			
		||||
    "@css-render/vue3-ssr" "^0.15.10"
 | 
			
		||||
@@ -1612,6 +2003,11 @@ nice-try@^1.0.4:
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366"
 | 
			
		||||
  integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==
 | 
			
		||||
 | 
			
		||||
node-releases@^2.0.6:
 | 
			
		||||
  version "2.0.6"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.6.tgz#8a7088c63a55e493845683ebf3c828d8c51c5503"
 | 
			
		||||
  integrity sha512-PiVXnNuFm5+iYkLBNeq5211hvO38y63T0i2KKh2KnUs3RpzJ+JtODFjkD8yjLwnDkTYF1eKXheUwdssR+NRZdg==
 | 
			
		||||
 | 
			
		||||
normalize-package-data@^2.3.2:
 | 
			
		||||
  version "2.5.0"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.5.0.tgz#e66db1838b200c1dfc233225d12cb36520e234a8"
 | 
			
		||||
@@ -1902,10 +2298,15 @@ safe-buffer@^5.1.2, safe-buffer@~5.2.0:
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6"
 | 
			
		||||
  integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==
 | 
			
		||||
 | 
			
		||||
safe-buffer@~5.1.1:
 | 
			
		||||
  version "5.1.2"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d"
 | 
			
		||||
  integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==
 | 
			
		||||
 | 
			
		||||
sass@^1.32.7:
 | 
			
		||||
  version "1.54.6"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/sass/-/sass-1.54.6.tgz#5a12c268db26555c335028e355d6b7b1a5b9b4c8"
 | 
			
		||||
  integrity sha512-DUqJjR2WxXBcZjRSZX5gCVyU+9fuC2qDfFzoKX9rV4rCOcec5mPtEafTcfsyL3YJuLONjWylBne+uXVh5rrmFw==
 | 
			
		||||
  version "1.54.8"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/sass/-/sass-1.54.8.tgz#4adef0dd86ea2b1e4074f551eeda4fc5f812a996"
 | 
			
		||||
  integrity sha512-ib4JhLRRgbg6QVy6bsv5uJxnJMTS2soVcCp9Y88Extyy13A8vV0G1fAwujOzmNkFQbR3LvedudAMbtuNRPbQww==
 | 
			
		||||
  dependencies:
 | 
			
		||||
    chokidar ">=3.0.0 <4.0.0"
 | 
			
		||||
    immutable "^4.0.0"
 | 
			
		||||
@@ -1921,6 +2322,11 @@ seemly@^0.3.6:
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7"
 | 
			
		||||
  integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==
 | 
			
		||||
 | 
			
		||||
semver@^6.3.0:
 | 
			
		||||
  version "6.3.0"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d"
 | 
			
		||||
  integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==
 | 
			
		||||
 | 
			
		||||
semver@^7.3.5, semver@^7.3.6, semver@^7.3.7:
 | 
			
		||||
  version "7.3.7"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.7.tgz#12c5b649afdbf9049707796e22a4028814ce523f"
 | 
			
		||||
@@ -2090,11 +2496,21 @@ supports-preserve-symlinks-flag@^1.0.0:
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09"
 | 
			
		||||
  integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==
 | 
			
		||||
 | 
			
		||||
svg-tags@^1.0.0:
 | 
			
		||||
  version "1.0.0"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/svg-tags/-/svg-tags-1.0.0.tgz#58f71cee3bd519b59d4b2a843b6c7de64ac04764"
 | 
			
		||||
  integrity sha512-ovssysQTa+luh7A5Weu3Rta6FJlFBBbInjOh722LIt6klpU2/HtdUbszju/G4devcvk8PGt7FCLv5wftu3THUA==
 | 
			
		||||
 | 
			
		||||
text-table@^0.2.0:
 | 
			
		||||
  version "0.2.0"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4"
 | 
			
		||||
  integrity sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==
 | 
			
		||||
 | 
			
		||||
to-fast-properties@^2.0.0:
 | 
			
		||||
  version "2.0.0"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz#dc5e698cbd079265bc73e0377681a4e4e83f616e"
 | 
			
		||||
  integrity sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==
 | 
			
		||||
 | 
			
		||||
to-regex-range@^5.0.1:
 | 
			
		||||
  version "5.0.1"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4"
 | 
			
		||||
@@ -2146,6 +2562,14 @@ unbox-primitive@^1.0.2:
 | 
			
		||||
    has-symbols "^1.0.3"
 | 
			
		||||
    which-boxed-primitive "^1.0.2"
 | 
			
		||||
 | 
			
		||||
update-browserslist-db@^1.0.5:
 | 
			
		||||
  version "1.0.7"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.0.7.tgz#16279639cff1d0f800b14792de43d97df2d11b7d"
 | 
			
		||||
  integrity sha512-iN/XYesmZ2RmmWAiI4Z5rq0YqSiv0brj9Ce9CfhNE4xIW2h+MFxcgkxIzZ+ShkFPUkjU3gQ+3oypadD3RAMtrg==
 | 
			
		||||
  dependencies:
 | 
			
		||||
    escalade "^3.1.1"
 | 
			
		||||
    picocolors "^1.0.0"
 | 
			
		||||
 | 
			
		||||
uri-js@^4.2.2:
 | 
			
		||||
  version "4.4.1"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e"
 | 
			
		||||
@@ -2183,11 +2607,6 @@ validate-npm-package-license@^3.0.1:
 | 
			
		||||
    spdx-correct "^3.0.0"
 | 
			
		||||
    spdx-expression-parse "^3.0.0"
 | 
			
		||||
 | 
			
		||||
validator@^13.7.0:
 | 
			
		||||
  version "13.7.0"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/validator/-/validator-13.7.0.tgz#4f9658ba13ba8f3d82ee881d3516489ea85c0857"
 | 
			
		||||
  integrity sha512-nYXQLCBkpJ8X6ltALua9dRrZDHVYxjJ1wgskNt1lH9fzGjs3tgojGSCBjmEPwkWS1y29+DrizMTW19Pr9uB2nw==
 | 
			
		||||
 | 
			
		||||
vdirs@^0.1.4, vdirs@^0.1.8:
 | 
			
		||||
  version "0.1.8"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/vdirs/-/vdirs-0.1.8.tgz#a103bc43baca738f8dea912a7e9737154a19dbc2"
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user