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