Fixed frontend

This commit is contained in:
Mutzi 2022-08-31 14:28:35 +02:00
parent df93c5e091
commit c290863e26
56 changed files with 1823 additions and 6370 deletions

15
frontend/.eslintrc.cjs Normal file
View File

@ -0,0 +1,15 @@
/* eslint-env node */
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",
},
};

27
frontend/.gitignore vendored
View File

@ -1,21 +1,26 @@
.DS_Store # Logs
node_modules logs
/dist *.log
# local env files
.env.local
.env.*.local
# Log files
npm-debug.log* npm-debug.log*
yarn-debug.log* yarn-debug.log*
yarn-error.log* yarn-error.log*
pnpm-debug.log* pnpm-debug.log*
lerna-debug.log*
node_modules
.DS_Store
dist
dist-ssr
coverage
*.local
/cypress/videos/
/cypress/screenshots/
# Editor directories and files # Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea .idea
.vscode
*.suo *.suo
*.ntvs* *.ntvs*
*.njsproj *.njsproj

View File

@ -1,7 +0,0 @@
{
"tabWidth": 4,
"useTabs": true,
"singleQuote": true,
"trailingComma": "none",
"endOfLine": "lf"
}

46
frontend/README.md Normal file
View File

@ -0,0 +1,46 @@
# frontend
This template should help get you started developing with Vue 3 in Vite.
## Recommended IDE Setup
[VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur) + [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin).
## Type Support for `.vue` Imports in TS
TypeScript cannot handle type information for `.vue` imports by default, so we replace the `tsc` CLI with `vue-tsc` for type checking. In editors, we need [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin) to make the TypeScript language service aware of `.vue` types.
If the standalone TypeScript plugin doesn't feel fast enough to you, Volar has also implemented a [Take Over Mode](https://github.com/johnsoncodehk/volar/discussions/471#discussioncomment-1361669) that is more performant. You can enable it by the following steps:
1. Disable the built-in TypeScript Extension
1) Run `Extensions: Show Built-in Extensions` from VSCode's command palette
2) Find `TypeScript and JavaScript Language Features`, right click and select `Disable (Workspace)`
2. Reload the VSCode window by running `Developer: Reload Window` from the command palette.
## Customize configuration
See [Vite Configuration Reference](https://vitejs.dev/config/).
## Project Setup
```sh
npm install
```
### Compile and Hot-Reload for Development
```sh
npm run dev
```
### Type-Check, Compile and Minify for Production
```sh
npm run build
```
### Lint with [ESLint](https://eslint.org/)
```sh
npm run lint
```

View File

@ -1,3 +0,0 @@
module.exports = {
presets: ['@vue/cli-plugin-babel/preset']
};

1
frontend/env.d.ts vendored Normal file
View File

@ -0,0 +1 @@
/// <reference types="vite/client" />

View File

@ -1,29 +0,0 @@
module.exports = {
parser: '@typescript-eslint/parser',
parserOptions: {
project: 'tsconfig.json',
tsconfigRootDir: __dirname,
sourceType: 'module'
},
plugins: ['@typescript-eslint/eslint-plugin', 'no-relative-import-paths'],
extends: [
'plugin:@typescript-eslint/recommended',
'plugin:prettier/recommended'
],
root: true,
env: {
node: true,
jest: true
},
ignorePatterns: ['.eslintrc.js'],
rules: {
'@typescript-eslint/interface-name-prefix': 'off',
'@typescript-eslint/explicit-function-return-type': 'off',
'@typescript-eslint/explicit-module-boundary-types': 'off',
'@typescript-eslint/no-explicit-any': 'off',
'no-relative-import-paths/no-relative-import-paths': [
'error',
{ allowSameFolder: true, rootDir: 'src' }
]
}
};

13
frontend/index.html Normal file
View File

@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite App</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

View File

@ -1,64 +1,41 @@
{ {
"name": "frontend", "name": "frontend",
"version": "0.1.0", "version": "0.0.0",
"private": true, "private": true,
"scripts": { "license": "suck my dick",
"serve": "vue-cli-service build --watch --dest ../run/static", "scripts": {
"build": "vue-cli-service build", "dev": "vite build --outDir ../run/static --watch",
"lint": "vue-cli-service lint" "build": "run-p type-check build-only",
}, "build-only": "vite build",
"dependencies": { "type-check": "vue-tsc --noEmit",
"axios": "^0.27.2", "lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore"
"class-transformer": "^0.5.1", },
"class-validator": "^0.13.2", "dependencies": {
"core-js": "^3.8.3", "axios": "^0.27.2",
"filesize": "^9.0.11", "class-transformer": "^0.5.1",
"jwt-decode": "^3.1.2", "class-validator": "^0.13.2",
"naive-ui": "^2.32.1", "filesize": "^9.0.11",
"stream-browserify": "^3.0.0", "jwt-decode": "^3.1.2",
"util": "^0.12.4", "naive-ui": "^2.32.1",
"vue": "^3.2.13", "stream-browserify": "^3.0.0",
"vue-router": "^4.0.3" "util": "^0.12.4",
}, "vue": "^3.2.37",
"devDependencies": { "vue-router": "^4.1.3"
"@typescript-eslint/eslint-plugin": "^5.4.0", },
"@typescript-eslint/parser": "^5.4.0", "devDependencies": {
"@vue/cli-plugin-babel": "~5.0.0", "@rushstack/eslint-patch": "^1.1.4",
"@vue/cli-plugin-eslint": "~5.0.0", "@types/node": "^18.7.14",
"@vue/cli-plugin-router": "~5.0.0", "@vitejs/plugin-vue": "^3.0.1",
"@vue/cli-plugin-typescript": "~5.0.0", "@vue/eslint-config-prettier": "^7.0.0",
"@vue/cli-service": "~5.0.0", "@vue/eslint-config-typescript": "^11.0.0",
"@vue/eslint-config-typescript": "^9.1.0", "@vue/tsconfig": "^0.1.3",
"eslint": "^7.32.0", "eslint": "8.22.0",
"eslint-config-prettier": "^8.3.0", "eslint-plugin-vue": "^9.3.0",
"eslint-plugin-prettier": "^4.0.0", "npm-run-all": "^4.1.5",
"eslint-plugin-vue": "^8.0.3", "prettier": "^2.7.1",
"prettier": "^2.4.1", "sass": "^1.32.7",
"sass": "^1.32.7", "typescript": "~4.7.4",
"sass-loader": "^12.0.0", "vite": "^3.0.4",
"typescript": "~4.5.5", "vue-tsc": "^0.39.5"
"vfonts": "^0.0.3" }
},
"eslintConfig": {
"root": true,
"env": {
"node": true
},
"extends": [
"plugin:vue/vue3-essential",
"eslint:recommended",
"@vue/typescript/recommended",
"plugin:prettier/recommended"
],
"parserOptions": {
"ecmaVersion": 2020
},
"rules": {}
},
"browserslist": [
"> 1%",
"last 2 versions",
"not dead",
"not ie 11"
]
} }

View File

@ -1,22 +0,0 @@
<!DOCTYPE html>
<html lang="">
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
<!--suppress HtmlUnknownTarget -->
<link rel="icon" href="<%= BASE_URL %>favicon.ico" />
<title><%= htmlWebpackPlugin.options.title %></title>
</head>
<body>
<noscript>
<strong
>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't
work properly without JavaScript enabled. Please enable it to
continue.</strong
>
</noscript>
<div id="app"></div>
<!-- built files will be auto injected -->
</body>
</html>

View File

@ -1,62 +1,62 @@
<script setup async lang="ts"> <script setup async lang="ts">
import { provide, ref } from 'vue'; import { provide, ref } from "vue";
import { useRouter } from 'vue-router'; import { useRouter } from "vue-router";
import { TokenInjectType } from '@/api'; import type { TokenInjectType } from "@/api";
const router = useRouter(); const router = useRouter();
const jwt = ref<string | null>(localStorage.getItem('token')); const jwt = ref<string | null>(localStorage.getItem("token"));
function setToken(token: string) { function setToken(token: string) {
jwt.value = token; jwt.value = token;
localStorage.setItem('token', token); localStorage.setItem("token", token);
} }
function logout() { function logout() {
jwt.value = null; jwt.value = null;
localStorage.removeItem('token'); localStorage.removeItem("token");
router.push({ name: 'login' }); router.push({ name: "login" });
} }
provide<TokenInjectType>('jwt', { provide<TokenInjectType>("jwt", {
jwt, jwt,
setToken, setToken,
logout logout,
}); });
</script> </script>
<template> <template>
<nav> <nav>
<template v-if="jwt != null"> <template v-if="jwt != null">
<router-link to="/">Files</router-link> <router-link to="/">Files</router-link>
<span style="margin-left: 2em" /> <span style="margin-left: 2em" />
<router-link to="/profile">Profile</router-link> <router-link to="/profile">Profile</router-link>
<span style="margin-left: 2em" /> <span style="margin-left: 2em" />
<router-link to="/login" @click="logout()">Logout</router-link> <router-link to="/login" @click="logout()">Logout</router-link>
</template> </template>
</nav> </nav>
<router-view /> <router-view />
</template> </template>
<style lang="scss"> <style lang="scss">
#app { #app {
font-family: Avenir, Helvetica, Arial, sans-serif; font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;
text-align: center; text-align: center;
color: #2c3e50; color: #2c3e50;
} }
nav { nav {
padding: 30px; padding: 30px;
a { a {
font-weight: bold; font-weight: bold;
color: #2c3e50; color: #2c3e50;
&.router-link-exact-active { &.router-link-exact-active {
color: #42b983; color: #42b983;
} }
} }
} }
</style> </style>

View File

@ -1,12 +1,12 @@
<script setup lang="ts"> <script setup lang="ts">
import App from './App'; import App from "./App.vue";
</script> </script>
<template> <template>
<Suspense> <Suspense>
<App></App> <App></App>
<template #fallback> <template #fallback>
<div>Loading...</div> <div>Loading...</div>
</template> </template>
</Suspense> </Suspense>
</template> </template>

View File

@ -1,54 +1,54 @@
import { Requests, Responses, UserRole, get_token, post_token } from './base'; import { Requests, Responses, UserRole, get_token, post_token } from "./base";
export const get_users = (token: string): Promise<Responses.Admin.GetUsers> => export const get_users = (token: string): Promise<Responses.Admin.GetUsers> =>
get_token('/api/admin/users', token); get_token("/api/admin/users", token);
export const set_role = ( export const set_role = (
user: number, user: number,
role: UserRole, role: UserRole,
token: string token: string
): Promise<Responses.Admin.SetUserRole | Responses.ErrorResponse> => ): Promise<Responses.Admin.SetUserRole | Responses.ErrorResponse> =>
post_token<Requests.Admin.SetUserRole>( post_token<Requests.Admin.SetUserRole>(
'/api/admin/set_role', "/api/admin/set_role",
{ {
user, user,
role role,
}, },
token token
); );
export const logout = ( export const logout = (
user: number, user: number,
token: string token: string
): Promise<Responses.Admin.LogoutAllUser | Responses.ErrorResponse> => ): Promise<Responses.Admin.LogoutAllUser | Responses.ErrorResponse> =>
post_token<Requests.Admin.LogoutAll>( post_token<Requests.Admin.LogoutAll>(
'/api/admin/logout', "/api/admin/logout",
{ {
user user,
}, },
token token
); );
export const delete_user = ( export const delete_user = (
user: number, user: number,
token: string token: string
): Promise<Responses.Admin.DeleteUser | Responses.ErrorResponse> => ): Promise<Responses.Admin.DeleteUser | Responses.ErrorResponse> =>
post_token<Requests.Admin.DeleteUser>( post_token<Requests.Admin.DeleteUser>(
'/api/admin/delete', "/api/admin/delete",
{ {
user user,
}, },
token token
); );
export const disable_tfa = ( export const disable_tfa = (
user: number, user: number,
token: string token: string
): Promise<Responses.Admin.DisableTfa | Responses.ErrorResponse> => ): Promise<Responses.Admin.DisableTfa | Responses.ErrorResponse> =>
post_token<Requests.Admin.DisableTfa>( post_token<Requests.Admin.DisableTfa>(
'/api/admin/disable_2fa', "/api/admin/disable_2fa",
{ {
user user,
}, },
token token
); );

View File

@ -1,93 +1,93 @@
import { Responses, Requests, post, post_token } from './base'; import { Responses, Requests, post, post_token } from "./base";
export const auth_login = ( export const auth_login = (
username: string, username: string,
password: string, password: string,
otp?: string otp?: string
): Promise< ): Promise<
| Responses.Auth.LoginResponse | Responses.Auth.LoginResponse
| Responses.Auth.TfaRequiredResponse | Responses.Auth.TfaRequiredResponse
| Responses.ErrorResponse | Responses.ErrorResponse
> => > =>
post<Requests.Auth.LoginRequest>('/api/auth/login', { post<Requests.Auth.LoginRequest>("/api/auth/login", {
username: username, username: username,
password: password, password: password,
otp: otp otp: otp,
}); });
export const auth_signup = ( export const auth_signup = (
username: string, username: string,
password: string password: string
): Promise<Responses.Auth.SignupResponse | Responses.ErrorResponse> => ): Promise<Responses.Auth.SignupResponse | Responses.ErrorResponse> =>
post<Requests.Auth.SignUpRequest>('/api/auth/signup', { post<Requests.Auth.SignUpRequest>("/api/auth/signup", {
username: username, username: username,
password: password password: password,
}); });
export const refresh_token = ( export const refresh_token = (
token: string token: string
): Promise<Responses.Auth.RefreshResponse | Responses.ErrorResponse> => ): Promise<Responses.Auth.RefreshResponse | Responses.ErrorResponse> =>
post_token('/api/auth/refresh', {}, token); post_token("/api/auth/refresh", {}, token);
export const change_password = ( export const change_password = (
oldPw: string, oldPw: string,
newPw: string, newPw: string,
token: string token: string
): Promise<Responses.Auth.ChangePasswordResponse | Responses.ErrorResponse> => ): Promise<Responses.Auth.ChangePasswordResponse | Responses.ErrorResponse> =>
post_token<Requests.Auth.ChangePasswordRequest>( post_token<Requests.Auth.ChangePasswordRequest>(
'/api/auth/change_password', "/api/auth/change_password",
{ {
oldPassword: oldPw, oldPassword: oldPw,
newPassword: newPw newPassword: newPw,
}, },
token token
); );
export const logout_all = ( export const logout_all = (
token: string token: string
): Promise<Responses.Auth.LogoutAllResponse | Responses.ErrorResponse> => ): Promise<Responses.Auth.LogoutAllResponse | Responses.ErrorResponse> =>
post_token('/api/auth/logout_all', {}, token); post_token("/api/auth/logout_all", {}, token);
export function tfa_setup( export function tfa_setup(
mail: false, mail: false,
token: string token: string
): Promise<Responses.Auth.RequestTotpTfaResponse | Responses.ErrorResponse>; ): Promise<Responses.Auth.RequestTotpTfaResponse | Responses.ErrorResponse>;
export function tfa_setup( export function tfa_setup(
mail: true, mail: true,
token: string token: string
): Promise<Responses.Auth.RequestEmailTfaResponse | Responses.ErrorResponse>; ): Promise<Responses.Auth.RequestEmailTfaResponse | Responses.ErrorResponse>;
export function tfa_setup( export function tfa_setup(
mail: boolean, mail: boolean,
token: string token: string
): Promise< ): Promise<
| Responses.Auth.RequestEmailTfaResponse | Responses.Auth.RequestEmailTfaResponse
| Responses.Auth.RequestTotpTfaResponse | Responses.Auth.RequestTotpTfaResponse
| Responses.ErrorResponse | Responses.ErrorResponse
> { > {
return post_token<Requests.Auth.TfaSetup>( return post_token<Requests.Auth.TfaSetup>(
'/api/auth/2fa/setup', "/api/auth/2fa/setup",
{ {
mail mail,
}, },
token token
); );
} }
export const tfa_complete = ( export const tfa_complete = (
mail: boolean, mail: boolean,
code: string, code: string,
token: string token: string
): Promise<Responses.Auth.TfaCompletedResponse | Responses.ErrorResponse> => ): Promise<Responses.Auth.TfaCompletedResponse | Responses.ErrorResponse> =>
post_token<Requests.Auth.TfaComplete>( post_token<Requests.Auth.TfaComplete>(
'/api/auth/2fa/complete', "/api/auth/2fa/complete",
{ {
mail, mail,
code code,
}, },
token token
); );
export const tfa_disable = ( export const tfa_disable = (
token: string token: string
): Promise<Responses.Auth.RemoveTfaResponse | Responses.ErrorResponse> => ): Promise<Responses.Auth.RemoveTfaResponse | Responses.ErrorResponse> =>
post_token('/api/auth/2fa/disable', {}, token); post_token("/api/auth/2fa/disable", {}, token);

View File

@ -1,62 +1,62 @@
import axios from 'axios'; import axios from "axios";
import { Requests, Responses, UserRole } from '../dto'; import { Requests, Responses, UserRole } from "../dto";
export { Requests, Responses, UserRole }; export { Requests, Responses, UserRole };
export const post = <T extends Requests.BaseRequest>(url: string, data: T) => export const post = <T extends Requests.BaseRequest>(url: string, data: T) =>
axios axios
.post(url, data, { .post(url, data, {
headers: { 'Content-type': 'application/json' } headers: { "Content-type": "application/json" },
}) })
.then((res) => res.data) .then((res) => res.data)
.catch((err) => err.response.data); .catch((err) => err.response.data);
export const post_token = <T extends Requests.BaseRequest>( export const post_token = <T extends Requests.BaseRequest>(
url: string, url: string,
data: T, data: T,
token: string token: string
) => ) =>
axios axios
.post(url, data, { .post(url, data, {
headers: { headers: {
Authorization: 'Bearer ' + token, Authorization: "Bearer " + token,
'Content-type': 'application/json' "Content-type": "application/json",
} },
}) })
.then((res) => res.data) .then((res) => res.data)
.catch((err) => err.response.data); .catch((err) => err.response.data);
export const post_token_form = ( export const post_token_form = (
url: string, url: string,
data: FormData, data: FormData,
token: string, token: string,
onProgress: (progressEvent: ProgressEvent) => void onProgress: (progressEvent: ProgressEvent) => void
) => ) =>
axios axios
.post(url, data, { .post(url, data, {
headers: { headers: {
Authorization: 'Bearer ' + token, Authorization: "Bearer " + token,
'Content-type': 'multipart/form-data' "Content-type": "multipart/form-data",
}, },
onUploadProgress: onProgress onUploadProgress: onProgress,
}) })
.then((res) => res.data) .then((res) => res.data)
.catch((err) => err.response.data); .catch((err) => err.response.data);
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
export const get = (url: string) => export const get = (url: string) =>
axios axios
.get(url) .get(url)
.then((res) => res.data) .then((res) => res.data)
.catch((err) => err.response.data); .catch((err) => err.response.data);
export const get_token = (url: string, token: string) => export const get_token = (url: string, token: string) =>
axios axios
.get(url, { .get(url, {
headers: { Authorization: 'Bearer ' + token } headers: { Authorization: "Bearer " + token },
}) })
.then((res) => res.data) .then((res) => res.data)
.catch((err) => err.response.data); .catch((err) => err.response.data);
export const isErrorResponse = ( export const isErrorResponse = (
res: Responses.BaseResponse res: Responses.BaseResponse
): res is Responses.ErrorResponse => res.statusCode != 200; ): res is Responses.ErrorResponse => res.statusCode != 200;

View File

@ -1,89 +1,84 @@
import { import {
Responses, Responses,
Requests, Requests,
get_token, get_token,
post_token, post_token,
post_token_form, post_token_form,
isErrorResponse isErrorResponse,
} from './base'; } from "./base";
export const get_root = ( export const get_root = (
token: string token: string
): Promise<Responses.FS.GetRootResponse | Responses.ErrorResponse> => ): Promise<Responses.FS.GetRootResponse | Responses.ErrorResponse> =>
get_token('/api/fs/root', token); get_token("/api/fs/root", token);
export const get_node = ( export const get_node = (
token: string, token: string,
node: number node: number
): Promise<Responses.FS.GetNodeResponse | Responses.ErrorResponse> => ): Promise<Responses.FS.GetNodeResponse | Responses.ErrorResponse> =>
get_token(`/api/fs/node/${node}`, token); get_token(`/api/fs/node/${node}`, token);
export const get_path = ( export const get_path = (
token: string, token: string,
node: number node: number
): Promise<Responses.FS.GetPathResponse | Responses.ErrorResponse> => ): Promise<Responses.FS.GetPathResponse | Responses.ErrorResponse> =>
get_token(`/api/fs/path/${node}`, token); get_token(`/api/fs/path/${node}`, token);
export const create_folder = ( export const create_folder = (
token: string, token: string,
parent: number, parent: number,
name: string name: string
): Promise<Responses.FS.CreateFolderResponse | Responses.ErrorResponse> => ): Promise<Responses.FS.CreateFolderResponse | Responses.ErrorResponse> =>
post_token<Requests.FS.CreateFolderRequest>( post_token<Requests.FS.CreateFolderRequest>(
'/api/fs/createFolder', "/api/fs/createFolder",
{ {
parent: parent, parent: parent,
name: name name: name,
}, },
token token
); );
export const create_file = ( export const create_file = (
token: string, token: string,
parent: number, parent: number,
name: string name: string
): Promise<Responses.FS.CreateFileResponse | Responses.ErrorResponse> => ): Promise<Responses.FS.CreateFileResponse | Responses.ErrorResponse> =>
post_token<Requests.FS.CreateFileRequest>( post_token<Requests.FS.CreateFileRequest>(
'/api/fs/createFile', "/api/fs/createFile",
{ {
parent: parent, parent: parent,
name: name name: name,
}, },
token token
); );
export const delete_node = ( export const delete_node = (
token: string, token: string,
node: number node: number
): Promise<Responses.FS.DeleteResponse | Responses.ErrorResponse> => ): Promise<Responses.FS.DeleteResponse | Responses.ErrorResponse> =>
post_token(`/api/fs/delete/${node}`, {}, token); post_token(`/api/fs/delete/${node}`, {}, token);
export const upload_file = async ( export const upload_file = async (
token: string, token: string,
parent: number, parent: number,
file: File, file: File,
onProgress: (progressEvent: ProgressEvent) => void onProgress: (progressEvent: ProgressEvent) => void
): Promise<Responses.FS.UploadFileResponse | Responses.ErrorResponse> => { ): Promise<Responses.FS.UploadFileResponse | Responses.ErrorResponse> => {
const node = await create_file(token, parent, file.name); const node = await create_file(token, parent, file.name);
if (isErrorResponse(node)) return node; if (isErrorResponse(node)) return node;
const form = new FormData(); const form = new FormData();
form.set('file', file); form.set("file", file);
return post_token_form( return post_token_form(`/api/fs/upload/${node.id}`, form, token, onProgress);
`/api/fs/upload/${node.id}`,
form,
token,
onProgress
);
}; };
export function download_file(token: string, id: number) { export function download_file(token: string, id: number) {
const form = document.createElement('form'); const form = document.createElement("form");
form.method = 'post'; form.method = "post";
form.target = '_blank'; form.target = "_blank";
form.action = '/api/fs/download'; form.action = "/api/fs/download";
form.innerHTML = `<input type="hidden" name="jwtToken" value="${token}"><input type="hidden" name="id" value="${id}">`; form.innerHTML = `<input type="hidden" name="jwtToken" value="${token}"><input type="hidden" name="id" value="${id}">`;
document.body.appendChild(form); document.body.appendChild(form);
form.submit(); form.submit();
document.body.removeChild(form); document.body.removeChild(form);
} }

View File

@ -1,6 +1,6 @@
export { Requests, Responses, UserRole, isErrorResponse } from './base'; export { Requests, Responses, UserRole, isErrorResponse } from "./base";
export * as Auth from './auth'; export * as Auth from "./auth";
export * as FS from './fs'; export * as FS from "./fs";
export * as User from './user'; export * as User from "./user";
export * as Admin from './admin'; export * as Admin from "./admin";
export * from './util'; export * from "./util";

View File

@ -1,11 +1,11 @@
import { Responses, get_token, post_token } from '@/api/base'; import { Responses, get_token, post_token } from "@/api/base";
export const get_user_info = ( export const get_user_info = (
token: string token: string
): Promise<Responses.User.UserInfoResponse | Responses.ErrorResponse> => ): Promise<Responses.User.UserInfoResponse | Responses.ErrorResponse> =>
get_token('/api/user/info', token); get_token("/api/user/info", token);
export const delete_user = ( export const delete_user = (
token: string token: string
): Promise<Responses.User.DeleteUserResponse | Responses.ErrorResponse> => ): Promise<Responses.User.DeleteUserResponse | Responses.ErrorResponse> =>
post_token('/api/user/delete', {}, token); post_token("/api/user/delete", {}, token);

View File

@ -1,25 +1,26 @@
import jwtDecode, { JwtPayload } from 'jwt-decode'; import type { JwtPayload } from "jwt-decode";
import { Ref, UnwrapRef } from 'vue'; import type { Ref, UnwrapRef } from "vue";
import { isErrorResponse } from './base'; import jwtDecode from "jwt-decode";
import { refresh_token } from './auth'; import { isErrorResponse } from "./base";
import { refresh_token } from "./auth";
export async function check_token( export async function check_token(
token: TokenInjectType token: TokenInjectType
): Promise<string | void> { ): Promise<string | void> {
if (!token.jwt.value) return token.logout(); if (!token.jwt.value) return token.logout();
const payload = jwtDecode<JwtPayload>(token.jwt.value); const payload = jwtDecode<JwtPayload>(token.jwt.value);
if (!payload) return token.logout(); if (!payload) return token.logout();
// Expires in more than 60 Minute // Expires in more than 60 Minute
if (payload.exp && payload.exp > Math.floor(Date.now() / 1000 + 60 * 60)) if (payload.exp && payload.exp > Math.floor(Date.now() / 1000 + 60 * 60))
return token.jwt.value; return token.jwt.value;
const new_token = await refresh_token(token.jwt.value); const new_token = await refresh_token(token.jwt.value);
if (isErrorResponse(new_token)) return token.logout(); if (isErrorResponse(new_token)) return token.logout();
token.setToken(new_token.jwt); token.setToken(new_token.jwt);
return new_token.jwt; return new_token.jwt;
} }
export type TokenInjectType = { export type TokenInjectType = {
jwt: Ref<UnwrapRef<string | null>>; jwt: Ref<UnwrapRef<string | null>>;
setToken: (token: string) => void; setToken: (token: string) => void;
logout: () => void; logout: () => void;
}; };

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.7 KiB

View File

@ -0,0 +1 @@
<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>

After

Width:  |  Height:  |  Size: 308 B

View File

@ -1,40 +1,41 @@
<script setup lang="ts"> <script setup lang="ts">
import { defineEmits, defineProps, inject } from 'vue'; import type { TokenInjectType } from "@/api";
import { check_token, FS, Responses, TokenInjectType } from '@/api'; import { defineEmits, defineProps, inject } from "vue";
import { check_token, FS, Responses } from "@/api";
const jwt = inject<TokenInjectType>('jwt') as TokenInjectType; const jwt = inject<TokenInjectType>("jwt") as TokenInjectType;
const props = defineProps<{ const props = defineProps<{
node: Responses.FS.GetNodeResponse; node: Responses.FS.GetNodeResponse;
}>(); }>();
const emit = defineEmits<{ const emit = defineEmits<{
(e: 'reloadNode'): void; (e: "reloadNode"): void;
}>(); }>();
async function del() { async function del() {
const token = await check_token(jwt); const token = await check_token(jwt);
if (!token) return; if (!token) return;
await FS.delete_node(token, props.node.id); await FS.delete_node(token, props.node.id);
emit('reloadNode'); emit("reloadNode");
} }
async function download() { async function download() {
const token = await check_token(jwt); const token = await check_token(jwt);
if (!token) return; if (!token) return;
FS.download_file(token, props.node.id); FS.download_file(token, props.node.id);
} }
</script> </script>
<template> <template>
<td> <td>
<router-link :to="'/fs/' + props.node.id">{{ node.name }}</router-link> <router-link :to="'/fs/' + props.node.id">{{ node.name }}</router-link>
</td> </td>
<td> <td>
<a href="#" @click="download()" v-if="props.node.isFile">Download</a> <a href="#" @click="download()" v-if="props.node.isFile">Download</a>
</td> </td>
<td> <td>
<a href="#" @click="del()" v-if="props.node.name !== '..'">delete</a> <a href="#" @click="del()" v-if="props.node.name !== '..'">delete</a>
</td> </td>
</template> </template>
<style scoped></style> <style scoped></style>

View File

@ -1,108 +1,102 @@
<script setup lang="ts"> <script setup lang="ts">
import { defineEmits, defineProps, inject, reactive, ref, watch } from 'vue'; import type { TokenInjectType } from "@/api";
import { FS, Responses, check_token, TokenInjectType } from '@/api'; import { defineEmits, defineProps, inject, reactive, ref, watch } from "vue";
import DirEntry from '@/components/FSView/DirEntry.vue'; import { FS, Responses, check_token } from "@/api";
import UploadFileDialog from '@/components/UploadDialog/UploadFileDialog.vue'; import DirEntry from "@/components/FSView/DirEntry.vue";
import { NModal } from 'naive-ui'; import UploadFileDialog from "@/components/UploadDialog/UploadFileDialog.vue";
import { NModal } from "naive-ui";
const props = defineProps<{ const props = defineProps<{
node: Responses.FS.GetNodeResponse; node: Responses.FS.GetNodeResponse;
}>(); }>();
const jwt = inject<TokenInjectType>('jwt') as TokenInjectType; const jwt = inject<TokenInjectType>("jwt") as TokenInjectType;
const emit = defineEmits<{ const emit = defineEmits<{
(e: 'reloadNode'): void; (e: "reloadNode"): void;
(e: 'gotoRoot'): void; (e: "gotoRoot"): void;
}>(); }>();
const fileInput = ref<HTMLInputElement>(); const fileInput = ref<HTMLInputElement>();
const uploadDialog = ref(); const uploadDialog = ref();
const uploadDialogShow = ref(false); const uploadDialogShow = ref(false);
const new_folder_name = ref(''); const new_folder_name = ref("");
const files = ref<File[]>([]); const files = ref<File[]>([]);
const nodes = ref<Responses.FS.GetNodeResponse[]>([]); const nodes = ref<Responses.FS.GetNodeResponse[]>([]);
const hasParent = ref(false); const hasParent = ref(false);
const parentNode = reactive<Responses.FS.GetNodeResponse>({ const parentNode = reactive<Responses.FS.GetNodeResponse>({
id: 0, id: 0,
statusCode: 200, statusCode: 200,
isFile: false, isFile: false,
parent: null, parent: null,
name: '..' name: "..",
}); });
watch( watch(
() => props.node, () => props.node,
async (to) => { async (to) => {
parentNode.id = to.parent ?? 0; parentNode.id = to.parent ?? 0;
hasParent.value = to.parent != null; hasParent.value = to.parent != null;
nodes.value = []; nodes.value = [];
const token = await check_token(jwt); const token = await check_token(jwt);
if (!token) return; if (!token) return;
await Promise.all( await Promise.all(
to.children?.map(async (child) => { to.children?.map(async (child) => {
nodes.value.push( nodes.value.push(
(await FS.get_node( (await FS.get_node(token, child)) as Responses.FS.GetNodeResponse
token, );
child }) ?? []
)) as Responses.FS.GetNodeResponse );
); },
}) ?? [] { immediate: true }
);
},
{ immediate: true }
); );
async function newFolder() { async function newFolder() {
const token = await check_token(jwt); const token = await check_token(jwt);
if (!token) return; if (!token) return;
await FS.create_folder(token, props.node.id, new_folder_name.value); await FS.create_folder(token, props.node.id, new_folder_name.value);
emit('reloadNode'); emit("reloadNode");
} }
async function uploadFiles() { async function uploadFiles() {
files.value = Array.from(fileInput.value?.files ?? []); files.value = Array.from(fileInput.value?.files ?? []);
if (files.value.length == 0) return; if (files.value.length == 0) return;
uploadDialogShow.value = true; uploadDialogShow.value = true;
} }
async function uploadFilesDialogOpen() { async function uploadFilesDialogOpen() {
await uploadDialog.value?.startUpload(props.node.id); await uploadDialog.value?.startUpload(props.node.id);
uploadDialogShow.value = false; uploadDialogShow.value = false;
if (fileInput.value) fileInput.value.value = ''; if (fileInput.value) fileInput.value.value = "";
emit('reloadNode'); emit("reloadNode");
} }
</script> </script>
<template> <template>
<div> <div>
<input <input type="text" placeholder="Folder name" v-model="new_folder_name" />
type="text" <a href="#" @click="newFolder()">create folder</a>
placeholder="Folder name" </div>
v-model="new_folder_name" <div>
/> <input type="file" ref="fileInput" multiple />
<a href="#" @click="newFolder()">create folder</a> <a href="#" @click="uploadFiles()">upload files</a>
</div> </div>
<div> <table>
<input type="file" ref="fileInput" multiple /> <tr v-if="hasParent">
<a href="#" @click="uploadFiles()">upload files</a> <DirEntry :node="parentNode" @reloadNode="emit('reloadNode')" />
</div> </tr>
<table> <tr v-for="n in nodes" :key="n.id">
<tr v-if="hasParent"> <DirEntry :node="n" @reloadNode="emit('reloadNode')" />
<DirEntry :node="parentNode" @reloadNode="emit('reloadNode')" /> </tr>
</tr> </table>
<tr v-for="n in nodes" :key="n.id"> <n-modal
<DirEntry :node="n" @reloadNode="emit('reloadNode')" /> v-model:show="uploadDialogShow"
</tr> :close-on-esc="false"
</table> :mask-closable="false"
<n-modal :on-after-enter="uploadFilesDialogOpen"
v-model:show="uploadDialogShow" >
:close-on-esc="false" <UploadFileDialog ref="uploadDialog" :files="files" />
:mask-closable="false" </n-modal>
:on-after-enter="uploadFilesDialogOpen"
>
<UploadFileDialog ref="uploadDialog" :files="files" />
</n-modal>
</template> </template>
<style scoped></style> <style scoped></style>

View File

@ -1,38 +1,39 @@
<script setup lang="ts"> <script setup lang="ts">
import { defineProps, inject } from 'vue'; import type { TokenInjectType } from "@/api";
import { check_token, FS, Responses, TokenInjectType } from '@/api'; import { defineProps, inject } from "vue";
import { check_token, FS, Responses } from "@/api";
const props = defineProps<{ const props = defineProps<{
node: Responses.FS.GetNodeResponse; node: Responses.FS.GetNodeResponse;
}>(); }>();
const jwt = inject<TokenInjectType>('jwt') as TokenInjectType; const jwt = inject<TokenInjectType>("jwt") as TokenInjectType;
async function del() { async function del() {
const token = await check_token(jwt); const token = await check_token(jwt);
if (!token) return; if (!token) return;
await FS.delete_node(token, props.node.id); await FS.delete_node(token, props.node.id);
} }
async function download() { async function download() {
const token = await check_token(jwt); const token = await check_token(jwt);
if (!token) return; if (!token) return;
FS.download_file(token, props.node.id); FS.download_file(token, props.node.id);
} }
</script> </script>
<template> <template>
<div> <div>
<router-link :to="'/fs/' + props.node.parent ?? 0">..</router-link> <router-link :to="'/fs/' + props.node.parent ?? 0">..</router-link>
</div> </div>
<div> <div>
<a href="#" @click="download()" v-if="props.node.isFile">Download</a> <a href="#" @click="download()" v-if="props.node.isFile">Download</a>
</div> </div>
<div> <div>
<router-link :to="'/fs/' + props.node.parent ?? 0" @click="del()"> <router-link :to="'/fs/' + props.node.parent ?? 0" @click="del()">
delete delete
</router-link> </router-link>
</div> </div>
</template> </template>
<style scoped></style> <style scoped></style>

View File

@ -1,156 +1,140 @@
<template> <template>
<div class="hello"> <div class="hello">
<h1>{{ msg }}</h1> <h1>{{ msg }}</h1>
<p> <p>
For a guide and recipes on how to configure / customize this For a guide and recipes on how to configure / customize this project,<br />
project,<br /> check out the
check out the <a href="https://cli.vuejs.org" target="_blank" rel="noopener"
<a href="https://cli.vuejs.org" target="_blank" rel="noopener" >vue-cli documentation</a
>vue-cli documentation</a >.
>. </p>
</p> <h3>Installed CLI Plugins</h3>
<h3>Installed CLI Plugins</h3> <ul>
<ul> <li>
<li> <a
<a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-babel"
href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-babel" target="_blank"
target="_blank" rel="noopener"
rel="noopener" >babel</a
>babel</a >
> </li>
</li> <li>
<li> <a
<a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-router"
href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-router" target="_blank"
target="_blank" rel="noopener"
rel="noopener" >router</a
>router</a >
> </li>
</li> <li>
<li> <a
<a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-vuex"
href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-vuex" target="_blank"
target="_blank" rel="noopener"
rel="noopener" >vuex</a
>vuex</a >
> </li>
</li> <li>
<li> <a
<a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-eslint"
href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-eslint" target="_blank"
target="_blank" rel="noopener"
rel="noopener" >eslint</a
>eslint</a >
> </li>
</li> <li>
<li> <a
<a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-typescript"
href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-typescript" target="_blank"
target="_blank" rel="noopener"
rel="noopener" >typescript</a
>typescript</a >
> </li>
</li> </ul>
</ul> <h3>Essential Links</h3>
<h3>Essential Links</h3> <ul>
<ul> <li>
<li> <a href="https://vuejs.org" target="_blank" rel="noopener">Core Docs</a>
<a href="https://vuejs.org" target="_blank" rel="noopener" </li>
>Core Docs</a <li>
> <a href="https://forum.vuejs.org" target="_blank" rel="noopener"
</li> >Forum</a
<li> >
<a href="https://forum.vuejs.org" target="_blank" rel="noopener" </li>
>Forum</a <li>
> <a href="https://chat.vuejs.org" target="_blank" rel="noopener"
</li> >Community Chat</a
<li> >
<a href="https://chat.vuejs.org" target="_blank" rel="noopener" </li>
>Community Chat</a <li>
> <a href="https://twitter.com/vuejs" target="_blank" rel="noopener"
</li> >Twitter</a
<li> >
<a </li>
href="https://twitter.com/vuejs" <li>
target="_blank" <a href="https://news.vuejs.org" target="_blank" rel="noopener">News</a>
rel="noopener" </li>
>Twitter</a </ul>
> <h3>Ecosystem</h3>
</li> <ul>
<li> <li>
<a href="https://news.vuejs.org" target="_blank" rel="noopener" <a href="https://router.vuejs.org" target="_blank" rel="noopener"
>News</a >vue-router</a
> >
</li> </li>
</ul> <li>
<h3>Ecosystem</h3> <a href="https://vuex.vuejs.org" target="_blank" rel="noopener">vuex</a>
<ul> </li>
<li> <li>
<a <a
href="https://router.vuejs.org" href="https://github.com/vuejs/vue-devtools#vue-devtools"
target="_blank" target="_blank"
rel="noopener" rel="noopener"
>vue-router</a >vue-devtools</a
> >
</li> </li>
<li> <li>
<a href="https://vuex.vuejs.org" target="_blank" rel="noopener" <a href="https://vue-loader.vuejs.org" target="_blank" rel="noopener"
>vuex</a >vue-loader</a
> >
</li> </li>
<li> <li>
<a <a
href="https://github.com/vuejs/vue-devtools#vue-devtools" href="https://github.com/vuejs/awesome-vue"
target="_blank" target="_blank"
rel="noopener" rel="noopener"
>vue-devtools</a >awesome-vue</a
> >
</li> </li>
<li> </ul>
<a </div>
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> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent } from 'vue'; import { defineComponent } from "vue";
export default defineComponent({ export default defineComponent({
name: 'HelloWorld', name: "HelloWorld",
props: { props: {
msg: String msg: String,
} },
}); });
</script> </script>
<!-- Add "scoped" attribute to limit CSS to this component only --> <!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped lang="scss"> <style scoped lang="scss">
h3 { h3 {
margin: 40px 0 0; margin: 40px 0 0;
} }
ul { ul {
list-style-type: none; list-style-type: none;
padding: 0; padding: 0;
} }
li { li {
display: inline-block; display: inline-block;
margin: 0 10px; margin: 0 10px;
} }
a { a {
color: #42b983; color: #42b983;
} }
</style> </style>

View File

@ -1,51 +1,52 @@
<script setup lang="ts"> <script setup lang="ts">
import { defineProps, defineExpose, ref } from 'vue'; import type { Status } from "naive-ui/es/progress/src/interface";
import { isErrorResponse, FS } from '@/api'; import { defineProps, defineExpose, ref } from "vue";
import { NProgress } from 'naive-ui'; import { isErrorResponse, FS } from "@/api";
import filesize from 'filesize'; import { NProgress } from "naive-ui";
import filesize from "filesize";
const props = defineProps<{ const props = defineProps<{
file: File; file: File;
}>(); }>();
const progress = ref(0); const progress = ref(0);
const percentage = ref(0); const percentage = ref(0);
const err = ref(''); const err = ref("");
const status = ref('info'); const status = ref<Status>("info");
async function startUpload(parent: number, token: string) { async function startUpload(parent: number, token: string) {
const resp = await FS.upload_file(token, parent, props.file, (e) => { const resp = await FS.upload_file(token, parent, props.file, (e) => {
progress.value = e.loaded; progress.value = e.loaded;
percentage.value = (e.loaded / e.total) * 100; percentage.value = (e.loaded / e.total) * 100;
}); });
percentage.value = 100; percentage.value = 100;
if (isErrorResponse(resp)) { if (isErrorResponse(resp)) {
err.value = resp.message ?? 'Error'; err.value = resp.message ?? "Error";
status.value = 'error'; status.value = "error";
} else status.value = 'success'; } else status.value = "success";
} }
defineExpose({ defineExpose({
startUpload startUpload,
}); });
</script> </script>
<template> <template>
<div v-if="percentage < 100"> <div v-if="percentage < 100">
{{ file.name }} - {{ filesize(progress) }} / {{ filesize(file.size) }} - {{ file.name }} - {{ filesize(progress) }} / {{ filesize(file.size) }} -
{{ Math.floor(percentage * 1000) / 1000 }}% {{ Math.floor(percentage * 1000) / 1000 }}%
</div> </div>
<div v-else-if="err !== ''">{{ file.name }} - Error: {{ err }}</div> <div v-else-if="err !== ''">{{ file.name }} - Error: {{ err }}</div>
<div v-else>{{ file.name }} - Completed</div> <div v-else>{{ file.name }} - Completed</div>
<n-progress <n-progress
type="line" type="line"
:percentage="percentage" :percentage="percentage"
:height="20" :height="20"
:status="status" :status="status"
border-radius="10px 0" border-radius="10px 0"
fill-border-radius="10px 0" fill-border-radius="10px 0"
:show-indicator="false" :show-indicator="false"
/> />
</template> </template>
<style scoped></style> <style scoped></style>

View File

@ -1,48 +1,44 @@
<script setup lang="ts"> <script setup lang="ts">
import { defineProps, defineExpose, ref, inject } from 'vue'; import type { TokenInjectType } from "@/api";
import { check_token, TokenInjectType } from '@/api'; import { defineProps, defineExpose, ref, inject } from "vue";
import UploadEntry from '@/components/UploadDialog/UploadEntry.vue'; import { check_token } from "@/api";
import { NCard } from 'naive-ui'; 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 entries = ref<typeof UploadEntry[]>([]);
const done = ref(false); const done = ref(false);
let canCloseResolve = null; let canCloseResolve: (value: unknown) => void = () => null;
const canClose = new Promise((r) => (canCloseResolve = r)); const canClose = new Promise((r) => (canCloseResolve = r));
async function startUpload(parent: number) { async function startUpload(parent: number) {
const token = await check_token(jwt); const token = await check_token(jwt);
if (!token) return; if (!token) return;
await Promise.all( await Promise.all(
entries.value.map((entry) => entry.startUpload(parent, token)) entries.value.map((entry) => entry.startUpload(parent, token))
); );
done.value = true; done.value = true;
await canClose; await canClose;
} }
defineExpose({ defineExpose({
startUpload startUpload,
}); });
defineProps<{ defineProps<{
files: File[]; files: File[];
}>(); }>();
</script> </script>
<template> <template>
<n-card title="Upload Files"> <n-card title="Upload Files">
<div> <div>
<UploadEntry <UploadEntry v-for="f in files" :key="f.name" ref="entries" :file="f" />
v-for="f in files" </div>
:key="f.name" <div>
ref="entries" <button v-if="done" @click="canCloseResolve(null)">Close</button>
:file="f" </div>
/> </n-card>
</div>
<div>
<button v-if="done" @click="canCloseResolve()">Close</button>
</div>
</n-card>
</template> </template>
<style scoped></style> <style scoped></style>

View File

@ -1,8 +1,8 @@
export * as Requests from './requests'; export * as Requests from "./requests";
export * as Responses from './responses'; export * as Responses from "./responses";
export { export {
UserRole, UserRole,
validateSync, validateSync,
validateAsync, validateAsync,
validateAsyncInline validateAsyncInline,
} from './utils'; } from "./utils";

View File

@ -1,15 +1,15 @@
import { BaseRequest } from './base'; import { BaseRequest } from "./base";
import { IsEnum, IsNumber } from 'class-validator'; import { IsEnum, IsNumber } from "class-validator";
import { UserRole } from '@/dto'; import { UserRole } from "@/dto";
export class AdminRequest extends BaseRequest { export class AdminRequest extends BaseRequest {
@IsNumber() @IsNumber()
user: number; user: number;
} }
export class SetUserRole extends AdminRequest { export class SetUserRole extends AdminRequest {
@IsEnum(UserRole) @IsEnum(UserRole)
role: UserRole; role: UserRole;
} }
export class LogoutAll extends AdminRequest {} export class LogoutAll extends AdminRequest {}

View File

@ -1,50 +1,50 @@
import { BaseRequest } from './base'; import { BaseRequest } from "./base";
import { import {
IsBoolean, IsBoolean,
IsEmail, IsEmail,
IsNotEmpty, IsNotEmpty,
IsOptional, IsOptional,
IsString IsString,
} from 'class-validator'; } from "class-validator";
export class SignUpRequest extends BaseRequest { export class SignUpRequest extends BaseRequest {
@IsEmail() @IsEmail()
username: string; username: string;
@IsNotEmpty() @IsNotEmpty()
@IsString() @IsString()
password: string; password: string;
} }
export class LoginRequest extends SignUpRequest { export class LoginRequest extends SignUpRequest {
@IsOptional() @IsOptional()
@IsNotEmpty() @IsNotEmpty()
@IsString() @IsString()
otp?: string; otp?: string;
} }
export class TfaSetup extends BaseRequest { export class TfaSetup extends BaseRequest {
@IsNotEmpty() @IsNotEmpty()
@IsBoolean() @IsBoolean()
mail: boolean; mail: boolean;
} }
export class TfaComplete extends BaseRequest { export class TfaComplete extends BaseRequest {
@IsNotEmpty() @IsNotEmpty()
@IsBoolean() @IsBoolean()
mail: boolean; mail: boolean;
@IsNotEmpty() @IsNotEmpty()
@IsString() @IsString()
code: string; code: string;
} }
export class ChangePasswordRequest extends BaseRequest { export class ChangePasswordRequest extends BaseRequest {
@IsNotEmpty() @IsNotEmpty()
@IsString() @IsString()
oldPassword: string; oldPassword: string;
@IsNotEmpty() @IsNotEmpty()
@IsString() @IsString()
newPassword: string; newPassword: string;
} }

View File

@ -1,14 +1,14 @@
import { BaseRequest } from './base'; import { BaseRequest } from "./base";
import { IsInt, IsNotEmpty, IsString, Min } from 'class-validator'; import { IsInt, IsNotEmpty, IsString, Min } from "class-validator";
export class CreateFolderRequest extends BaseRequest { export class CreateFolderRequest extends BaseRequest {
@IsInt() @IsInt()
@Min(1) @Min(1)
parent: number; parent: number;
@IsNotEmpty() @IsNotEmpty()
@IsString() @IsString()
name: string; name: string;
} }
export class CreateFileRequest extends CreateFolderRequest {} export class CreateFileRequest extends CreateFolderRequest {}

View File

@ -1,4 +1,4 @@
export * from './base'; export * from "./base";
export * as Auth from './auth'; export * as Auth from "./auth";
export * as FS from './fs'; export * as FS from "./fs";
export * as Admin from './admin'; export * as Admin from "./admin";

View File

@ -1,58 +1,58 @@
import { SuccessResponse } from './base'; import { SuccessResponse } from "./base";
import { import {
IsArray, IsArray,
IsBoolean, IsBoolean,
IsEnum, IsEnum,
IsNotEmpty, IsNotEmpty,
IsNumber, IsNumber,
IsString, IsString,
ValidateNested ValidateNested,
} from 'class-validator'; } from "class-validator";
import { UserRole, ValidateConstructor } from '../utils'; import { UserRole, ValidateConstructor } from "../utils";
@ValidateConstructor @ValidateConstructor
export class GetUsersEntry { export class GetUsersEntry {
constructor( constructor(
id: number, id: number,
gitlab: boolean, gitlab: boolean,
name: string, name: string,
role: UserRole, role: UserRole,
tfaEnabled: boolean tfaEnabled: boolean
) { ) {
this.id = id; this.id = id;
this.gitlab = gitlab; this.gitlab = gitlab;
this.name = name; this.name = name;
this.role = role; this.role = role;
this.tfaEnabled = tfaEnabled; this.tfaEnabled = tfaEnabled;
} }
@IsNumber() @IsNumber()
id: number; id: number;
@IsBoolean() @IsBoolean()
gitlab: boolean; gitlab: boolean;
@IsString() @IsString()
@IsNotEmpty() @IsNotEmpty()
name: string; name: string;
@IsEnum(UserRole) @IsEnum(UserRole)
role: UserRole; role: UserRole;
@IsBoolean() @IsBoolean()
tfaEnabled: boolean; tfaEnabled: boolean;
} }
@ValidateConstructor @ValidateConstructor
export class GetUsers extends SuccessResponse { export class GetUsers extends SuccessResponse {
constructor(users: GetUsersEntry[]) { constructor(users: GetUsersEntry[]) {
super(); super();
this.users = users; this.users = users;
} }
@IsArray() @IsArray()
@ValidateNested({ each: true }) @ValidateNested({ each: true })
users: GetUsersEntry[]; users: GetUsersEntry[];
} }
export class LogoutAllUser extends SuccessResponse {} export class LogoutAllUser extends SuccessResponse {}

View File

@ -1,33 +1,33 @@
import { SuccessResponse } from './base'; import { SuccessResponse } from "./base";
import { IsBase32, IsJWT, IsNotEmpty } from 'class-validator'; import { IsBase32, IsJWT, IsNotEmpty } from "class-validator";
import { ValidateConstructor } from '../utils'; import { ValidateConstructor } from "../utils";
@ValidateConstructor @ValidateConstructor
export class LoginResponse extends SuccessResponse { export class LoginResponse extends SuccessResponse {
constructor(jwt: string) { constructor(jwt: string) {
super(); super();
this.jwt = jwt; this.jwt = jwt;
} }
@IsNotEmpty() @IsNotEmpty()
@IsJWT() @IsJWT()
jwt: string; jwt: string;
} }
@ValidateConstructor @ValidateConstructor
export class RequestTotpTfaResponse extends SuccessResponse { export class RequestTotpTfaResponse extends SuccessResponse {
constructor(qrCode: string, secret: string) { constructor(qrCode: string, secret: string) {
super(); super();
this.qrCode = qrCode; this.qrCode = qrCode;
this.secret = secret; this.secret = secret;
} }
@IsNotEmpty() @IsNotEmpty()
qrCode: string; qrCode: string;
@IsNotEmpty() @IsNotEmpty()
@IsBase32() @IsBase32()
secret: string; secret: string;
} }
export class TfaRequiredResponse extends SuccessResponse {} export class TfaRequiredResponse extends SuccessResponse {}

View File

@ -1,25 +1,25 @@
import { IsNumber, Max, Min } from 'class-validator'; import { IsNumber, Max, Min } from "class-validator";
export class BaseResponse { export class BaseResponse {
constructor(statusCode: number) { constructor(statusCode: number) {
this.statusCode = statusCode; this.statusCode = statusCode;
} }
@IsNumber() @IsNumber()
@Min(100) @Min(100)
@Max(599) @Max(599)
statusCode: number; statusCode: number;
} }
export class SuccessResponse extends BaseResponse { export class SuccessResponse extends BaseResponse {
constructor() { constructor() {
super(200); super(200);
} }
declare statusCode: 200; declare statusCode: 200;
} }
export class ErrorResponse extends BaseResponse { export class ErrorResponse extends BaseResponse {
declare statusCode: 400 | 401 | 403; declare statusCode: 400 | 401 | 403;
message?: string; message?: string;
} }

View File

@ -1,87 +1,87 @@
import { SuccessResponse } from './base'; import { SuccessResponse } from "./base";
import { import {
IsBoolean, IsBoolean,
IsInt, IsInt,
IsNotEmpty, IsNotEmpty,
IsOptional, IsOptional,
IsString, IsString,
Min Min,
} from 'class-validator'; } from "class-validator";
import { ValidateConstructor } from '../utils'; import { ValidateConstructor } from "../utils";
@ValidateConstructor @ValidateConstructor
export class GetRootResponse extends SuccessResponse { export class GetRootResponse extends SuccessResponse {
constructor(rootId: number) { constructor(rootId: number) {
super(); super();
this.rootId = rootId; this.rootId = rootId;
} }
@IsInt() @IsInt()
@Min(1) @Min(1)
rootId: number; rootId: number;
} }
export class GetNodeResponse extends SuccessResponse { export class GetNodeResponse extends SuccessResponse {
constructor( constructor(
id: number, id: number,
name: string, name: string,
isFile: boolean, isFile: boolean,
parent: number | null parent: number | null
) { ) {
super(); super();
this.id = id; this.id = id;
this.name = name; this.name = name;
this.isFile = isFile; this.isFile = isFile;
this.parent = parent; this.parent = parent;
} }
@IsInt() @IsInt()
@Min(1) @Min(1)
id: number; id: number;
@IsString() @IsString()
name: string; name: string;
@IsBoolean() @IsBoolean()
isFile: boolean; isFile: boolean;
@IsOptional() @IsOptional()
@IsInt() @IsInt()
@Min(1) @Min(1)
parent: number | null; parent: number | null;
@IsOptional() @IsOptional()
@IsInt({ each: true }) @IsInt({ each: true })
@Min(1, { each: true }) @Min(1, { each: true })
children?: number[]; children?: number[];
@IsOptional() @IsOptional()
@IsInt() @IsInt()
@Min(0) @Min(0)
size?: number; size?: number;
} }
@ValidateConstructor @ValidateConstructor
export class GetPathResponse extends SuccessResponse { export class GetPathResponse extends SuccessResponse {
constructor(path: string) { constructor(path: string) {
super(); super();
this.path = path; this.path = path;
} }
@IsNotEmpty() @IsNotEmpty()
@IsString() @IsString()
path: string; path: string;
} }
@ValidateConstructor @ValidateConstructor
export class CreateFolderResponse extends SuccessResponse { export class CreateFolderResponse extends SuccessResponse {
constructor(id: number) { constructor(id: number) {
super(); super();
this.id = id; this.id = id;
} }
@IsInt() @IsInt()
@Min(1) @Min(1)
id: number; id: number;
} }
export class UploadFileResponse extends SuccessResponse {} export class UploadFileResponse extends SuccessResponse {}

View File

@ -1,5 +1,5 @@
export * from './base'; export * from "./base";
export * as Auth from './auth'; export * as Auth from "./auth";
export * as FS from './fs'; export * as FS from "./fs";
export * as User from './user'; export * as User from "./user";
export * as Admin from './admin'; export * as Admin from "./admin";

View File

@ -1,25 +1,25 @@
import { SuccessResponse } from './base'; import { SuccessResponse } from "./base";
import { ValidateConstructor } from '../utils'; import { ValidateConstructor } from "../utils";
import { IsBoolean, IsNotEmpty, IsString } from 'class-validator'; import { IsBoolean, IsNotEmpty, IsString } from "class-validator";
@ValidateConstructor @ValidateConstructor
export class UserInfoResponse extends SuccessResponse { export class UserInfoResponse extends SuccessResponse {
constructor(name: string, gitlab: boolean, tfaEnabled: boolean) { constructor(name: string, gitlab: boolean, tfaEnabled: boolean) {
super(); super();
this.name = name; this.name = name;
this.gitlab = gitlab; this.gitlab = gitlab;
this.tfaEnabled = tfaEnabled; this.tfaEnabled = tfaEnabled;
} }
@IsNotEmpty() @IsNotEmpty()
@IsString() @IsString()
name: string; name: string;
@IsBoolean() @IsBoolean()
gitlab: boolean; gitlab: boolean;
@IsBoolean() @IsBoolean()
tfaEnabled: boolean; tfaEnabled: boolean;
} }
export class DeleteUserResponse extends SuccessResponse {} export class DeleteUserResponse extends SuccessResponse {}

View File

@ -1,43 +1,43 @@
import { validate, validateSync as _validateSync } from 'class-validator'; import { validate, validateSync as _validateSync } from "class-validator";
export enum UserRole { export enum UserRole {
ADMIN = 2, ADMIN = 2,
USER = 1, USER = 1,
DISABLED = 0 DISABLED = 0,
} }
export function validateSync<T extends object>(data: T): void { export function validateSync<T extends object>(data: T): void {
const errors = _validateSync(data); const errors = _validateSync(data);
if (errors.length > 0) { if (errors.length > 0) {
console.error('Validation failed, errors: ', errors); console.error("Validation failed, errors: ", errors);
throw new Error('Validation failed'); throw new Error("Validation failed");
} }
} }
export async function validateAsync<T extends object>(data: T): Promise<void> { export async function validateAsync<T extends object>(data: T): Promise<void> {
const errors = await validate(data); const errors = await validate(data);
if (errors.length > 0) { if (errors.length > 0) {
console.error('Validation failed, errors: ', errors); console.error("Validation failed, errors: ", errors);
throw new Error('Validation failed'); throw new Error("Validation failed");
} }
} }
export async function validateAsyncInline<T extends object>( export async function validateAsyncInline<T extends object>(
data: T data: T
): Promise<T> { ): Promise<T> {
await validateAsync(data); await validateAsync(data);
return data; return data;
} }
export function ValidateConstructor< export function ValidateConstructor<
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
T extends { new (...args: any[]): any } T extends { new (...args: any[]): any }
>(constr: T) { >(constr: T) {
return class extends constr { return class extends constr {
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
constructor(...args: any[]) { constructor(...args: any[]) {
super(...args); super(...args);
validateSync(this); validateSync(this);
} }
}; };
} }

View File

@ -1,8 +1,8 @@
import { createApp } from 'vue'; import { createApp } from "vue";
import router from './router'; import AppAsyncWrapper from "./AppAsyncWrapper.vue";
import AppAsyncWrapper from './AppAsyncWrapper.vue'; import router from "./router";
const app = createApp(AppAsyncWrapper); const app = createApp(AppAsyncWrapper);
app.use(router); app.use(router);
app.config.unwrapInjectedRef = true; app.config.unwrapInjectedRef = true;
app.mount('#app'); app.mount("#app");

View File

@ -1,63 +1,64 @@
import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router'; import type { RouteRecordRaw } from "vue-router";
import LoginView from '@/views/LoginView.vue'; import { createRouter, createWebHistory } from "vue-router";
import SignupView from '@/views/SignupView.vue'; import LoginView from "@/views/LoginView.vue";
import HomeView from '@/views/HomeView.vue'; import SignupView from "@/views/SignupView.vue";
import AboutView from '@/views/AboutView.vue'; import HomeView from "@/views/HomeView.vue";
import FSView from '@/views/FSView.vue'; import AboutView from "@/views/AboutView.vue";
import SetTokenView from '@/views/SetTokenView.vue'; import FSView from "@/views/FSView.vue";
import ProfileView from '@/views/ProfileView.vue'; import SetTokenView from "@/views/SetTokenView.vue";
import TFAView from '@/views/TFAView.vue'; import ProfileView from "@/views/ProfileView.vue";
import AdminView from '@/views/AdminView.vue'; import TFAView from "@/views/TFAView.vue";
import AdminView from "@/views/AdminView.vue";
const routes: Array<RouteRecordRaw> = [ const routes: Array<RouteRecordRaw> = [
{ {
path: '/', path: "/",
name: 'home', name: "home",
component: HomeView component: HomeView,
}, },
{ {
path: '/profile', path: "/profile",
name: 'profile', name: "profile",
component: ProfileView component: ProfileView,
}, },
{ {
path: '/profile/2fa-enable', path: "/profile/2fa-enable",
name: '2fa', name: "2fa",
component: TFAView component: TFAView,
}, },
{ {
path: '/admin', path: "/admin",
component: AdminView component: AdminView,
}, },
{ {
path: '/about', path: "/about",
component: AboutView component: AboutView,
}, },
{ {
path: '/login', path: "/login",
name: 'login', name: "login",
component: LoginView component: LoginView,
}, },
{ {
path: '/signup', path: "/signup",
name: 'signup', name: "signup",
component: SignupView component: SignupView,
}, },
{ {
path: '/fs/:node_id', path: "/fs/:node_id",
name: 'fs', name: "fs",
component: FSView component: FSView,
}, },
{ {
path: '/set_token', path: "/set_token",
component: SetTokenView component: SetTokenView,
} },
]; ];
const router = createRouter({ const router = createRouter({
history: createWebHistory(process.env.BASE_URL), history: createWebHistory(import.meta.env.BASE_URL),
routes routes,
}); });
export default router; export default router;

View File

@ -1,6 +0,0 @@
/* eslint-disable */
declare module '*.vue' {
import type { DefineComponent } from 'vue';
const component: DefineComponent<{}, {}, any>;
export default component;
}

View File

@ -1,5 +1,5 @@
<template> <template>
<div class="about"> <div class="about">
<h1>This is an about page</h1> <h1>This is an about page</h1>
</div> </div>
</template> </template>

View File

@ -1,109 +1,104 @@
<script setup lang="ts"> <script setup lang="ts">
import { inject, onBeforeMount, ref } from 'vue'; import type { TokenInjectType } from "@/api";
import { import { inject, onBeforeMount, ref } from "vue";
Responses, import { Responses, check_token, Admin, isErrorResponse } from "@/api";
check_token, import { onBeforeRouteUpdate } from "vue-router";
TokenInjectType, import router from "@/router";
Admin,
isErrorResponse
} from '@/api';
import { onBeforeRouteUpdate } from 'vue-router';
import router from '@/router';
const jwt = inject<TokenInjectType>('jwt') as TokenInjectType; const jwt = inject<TokenInjectType>("jwt") as TokenInjectType;
const users = ref<Responses.Admin.GetUsersEntry[]>([]); const users = ref<Responses.Admin.GetUsersEntry[]>([]);
onBeforeRouteUpdate(async () => { onBeforeRouteUpdate(async () => {
await updatePanel(); await updatePanel();
}); });
onBeforeMount(async () => { onBeforeMount(async () => {
await updatePanel(); await updatePanel();
}); });
async function updatePanel() { async function updatePanel() {
const token = await check_token(jwt); const token = await check_token(jwt);
if (!token) return; if (!token) return;
const res = await Admin.get_users(token); const res = await Admin.get_users(token);
if (isErrorResponse(res)) return router.replace({ path: '/' }); if (isErrorResponse(res)) return router.replace({ path: "/" });
users.value = res.users; users.value = res.users;
} }
async function setRole(user: number, roleStr: string) { async function setRole(user: number, roleStr: string) {
const token = await check_token(jwt); const token = await check_token(jwt);
if (!token) return; if (!token) return;
const res = await Admin.set_role(user, parseInt(roleStr, 10), token); const res = await Admin.set_role(user, parseInt(roleStr, 10), token);
if (isErrorResponse(res)) console.error(res.message); if (isErrorResponse(res)) console.error(res.message);
await updatePanel(); await updatePanel();
} }
async function disableTfa(user: number) { async function disableTfa(user: number) {
const token = await check_token(jwt); const token = await check_token(jwt);
if (!token) return; if (!token) return;
const res = await Admin.disable_tfa(user, token); const res = await Admin.disable_tfa(user, token);
if (isErrorResponse(res)) console.error(res.message); if (isErrorResponse(res)) console.error(res.message);
await updatePanel(); await updatePanel();
} }
async function logoutUser(user: number) { async function logoutUser(user: number) {
const token = await check_token(jwt); const token = await check_token(jwt);
if (!token) return; if (!token) return;
const res = await Admin.logout(user, token); const res = await Admin.logout(user, token);
if (isErrorResponse(res)) console.error(res.message); if (isErrorResponse(res)) console.error(res.message);
await updatePanel(); await updatePanel();
} }
async function deleteUser(user: number) { async function deleteUser(user: number) {
const token = await check_token(jwt); const token = await check_token(jwt);
if (!token) return; if (!token) return;
const res = await Admin.delete_user(user, token); const res = await Admin.delete_user(user, token);
if (isErrorResponse(res)) console.error(res.message); if (isErrorResponse(res)) console.error(res.message);
await updatePanel(); await updatePanel();
} }
</script> </script>
<template> <template>
<table> <table>
<tr> <tr>
<th>Name</th> <th>Name</th>
<th>Type</th> <th>Type</th>
<th>Role</th> <th>Role</th>
<th>Tfa Status</th> <th>Tfa Status</th>
<th>Actions</th> <th>Actions</th>
</tr> </tr>
<tr v-for="user in users" :key="user.id"> <tr v-for="user in users" :key="user.id">
<td>{{ user.name }}</td> <td>{{ user.name }}</td>
<td>{{ user.gitlab ? 'Gitlab' : 'Password' }}</td> <td>{{ user.gitlab ? "Gitlab" : "Password" }}</td>
<td> <td>
<select @change="setRole(user.id, $event.target.value)"> <select @change="setRole(user.id, ($event.target as HTMLSelectElement).value)">
<option value="0" :selected="user.role === 0 ? true : null"> <option value="0" :selected="user.role === 0 ? true : undefined">
Disabled Disabled
</option> </option>
<option value="1" :selected="user.role === 1 ? true : null"> <option value="1" :selected="user.role === 1 ? true : undefined">
User User
</option> </option>
<option value="2" :selected="user.role === 2 ? true : null"> <option value="2" :selected="user.role === 2 ? true : undefined">
Admin Admin
</option> </option>
</select> </select>
</td> </td>
<td v-if="user.gitlab"></td> <td v-if="user.gitlab"></td>
<td v-else> <td v-else>
{{ user.tfaEnabled ? 'Enabled' : 'Disabled' }} {{ user.tfaEnabled ? "Enabled" : "Disabled" }}
</td> </td>
<td> <td>
<button v-if="user.tfaEnabled" @click="disableTfa(user.id)"> <button v-if="user.tfaEnabled" @click="disableTfa(user.id)">
Disable Tfa Disable Tfa
</button> </button>
<button @click="logoutUser(user.id)">Logout all</button> <button @click="logoutUser(user.id)">Logout all</button>
<button @click="deleteUser(user.id)">Delete</button> <button @click="deleteUser(user.id)">Delete</button>
</td> </td>
</tr> </tr>
</table> </table>
</template> </template>
<style scoped></style> <style scoped></style>

View File

@ -1,70 +1,65 @@
<script setup lang="ts"> <script setup lang="ts">
import { onBeforeRouteUpdate, useRoute, useRouter } from 'vue-router'; import type { TokenInjectType } from "@/api";
import { inject, onBeforeMount, ref } from 'vue'; import { onBeforeRouteUpdate, useRoute, useRouter } from "vue-router";
import { import { inject, onBeforeMount, ref } from "vue";
check_token, import { check_token, FS, Responses, isErrorResponse } from "@/api";
FS, import DirViewer from "@/components/FSView/DirViewer.vue";
Responses, import FileViewer from "@/components/FSView/FileViewer.vue";
isErrorResponse,
TokenInjectType
} from '@/api';
import DirViewer from '@/components/FSView/DirViewer.vue';
import FileViewer from '@/components/FSView/FileViewer.vue';
const router = useRouter(); const router = useRouter();
const route = useRoute(); const route = useRoute();
const jwt = inject<TokenInjectType>('jwt') as TokenInjectType; const jwt = inject<TokenInjectType>("jwt") as TokenInjectType;
const path = ref(''); const path = ref("");
const node = ref<Responses.FS.GetNodeResponse | null>(null); const node = ref<Responses.FS.GetNodeResponse | null>(null);
async function fetch_node(node_id: number) { async function fetch_node(node_id: number) {
const token = await check_token(jwt); const token = await check_token(jwt);
if (!token) return; if (!token) return;
let [p, n] = [ const [p, n] = [
await FS.get_path(token, node_id), await FS.get_path(token, node_id),
await FS.get_node(token, node_id) await FS.get_node(token, node_id),
]; ];
if (isErrorResponse(p)) return gotoRoot(); if (isErrorResponse(p)) return gotoRoot();
if (isErrorResponse(n)) return gotoRoot(); if (isErrorResponse(n)) return gotoRoot();
[path.value, node.value] = [p.path, n]; [path.value, node.value] = [p.path, n];
} }
onBeforeRouteUpdate(async (to) => { onBeforeRouteUpdate(async (to) => {
await fetch_node(Number(to.params.node_id)); await fetch_node(Number(to.params.node_id));
}); });
async function reloadNode() { async function reloadNode() {
await fetch_node(Number(route.params.node_id)); await fetch_node(Number(route.params.node_id));
} }
onBeforeMount(async () => { onBeforeMount(async () => {
await reloadNode(); await reloadNode();
}); });
async function gotoRoot() { async function gotoRoot() {
const token = await check_token(jwt); const token = await check_token(jwt);
if (!token) return; if (!token) return;
const rootRes = await FS.get_root(token); const rootRes = await FS.get_root(token);
if (isErrorResponse(rootRes)) return jwt.logout(); if (isErrorResponse(rootRes)) return jwt.logout();
const root = rootRes.rootId; const root = rootRes.rootId;
await router.replace({ await router.replace({
name: 'fs', name: "fs",
params: { node_id: root } params: { node_id: root },
}); });
} }
</script> </script>
<template> <template>
<div v-if="node"> <div v-if="node">
<div>Path: {{ path }}</div> <div>Path: {{ path }}</div>
<DirViewer <DirViewer
v-if="!node.isFile" v-if="!node.isFile"
:node="node" :node="node"
@reloadNode="reloadNode" @reloadNode="reloadNode"
@gotoRoot="gotoRoot" @gotoRoot="gotoRoot"
/> />
<FileViewer v-else :node="node" /> <FileViewer v-else :node="node" />
</div> </div>
</template> </template>
<style scoped></style> <style scoped></style>

View File

@ -1,28 +1,29 @@
<template><p></p></template> <template><p></p></template>
<script setup lang="ts"> <script setup lang="ts">
import { onBeforeRouteUpdate, useRouter } from 'vue-router'; import type { TokenInjectType } from "@/api";
import { inject, onBeforeMount } from 'vue'; import { onBeforeRouteUpdate, useRouter } from "vue-router";
import { FS, check_token, isErrorResponse, TokenInjectType } from '@/api'; import { inject, onBeforeMount } from "vue";
import { FS, check_token, isErrorResponse } from "@/api";
const router = useRouter(); const router = useRouter();
const jwt = inject<TokenInjectType>('jwt') as TokenInjectType; const jwt = inject<TokenInjectType>("jwt") as TokenInjectType;
async function start_redirect() { async function start_redirect() {
const token = await check_token(jwt); const token = await check_token(jwt);
if (!token) return; if (!token) return;
const root = await FS.get_root(token); const root = await FS.get_root(token);
if (isErrorResponse(root)) return jwt.logout(); if (isErrorResponse(root)) return jwt.logout();
await router.replace({ await router.replace({
name: 'fs', name: "fs",
params: { node_id: root.rootId } params: { node_id: root.rootId },
}); });
} }
onBeforeRouteUpdate(async () => { onBeforeRouteUpdate(async () => {
await start_redirect(); await start_redirect();
}); });
onBeforeMount(async () => { onBeforeMount(async () => {
await start_redirect(); await start_redirect();
}); });
</script> </script>

View File

@ -1,61 +1,62 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, inject } from 'vue'; import type { TokenInjectType } from "@/api";
import { Auth, FS, isErrorResponse, TokenInjectType } from '@/api'; import { ref, inject } from "vue";
import { useRouter } from 'vue-router'; import { Auth, FS, isErrorResponse } from "@/api";
import { useRouter } from "vue-router";
const router = useRouter(); const router = useRouter();
const username = ref(''); const username = ref("");
const password = ref(''); const password = ref("");
const otp = ref(''); const otp = ref("");
const error = ref(''); const error = ref("");
const requestOtp = ref(false); const requestOtp = ref(false);
const jwt = inject<TokenInjectType>('jwt') as TokenInjectType; const jwt = inject<TokenInjectType>("jwt") as TokenInjectType;
async function login() { async function login() {
error.value = ''; error.value = "";
if (username.value === '' || password.value === '') { if (username.value === "" || password.value === "") {
error.value = 'Email and/or Password missing'; error.value = "Email and/or Password missing";
return; return;
} }
const res = await (requestOtp.value const res = await (requestOtp.value
? Auth.auth_login(username.value, password.value, otp.value) ? Auth.auth_login(username.value, password.value, otp.value)
: Auth.auth_login(username.value, password.value)); : Auth.auth_login(username.value, password.value));
if (isErrorResponse(res)) error.value = 'Login failed: ' + res.message; if (isErrorResponse(res)) error.value = "Login failed: " + res.message;
else if ('jwt' in res) { else if ("jwt" in res) {
const root = await FS.get_root(res.jwt); const root = await FS.get_root(res.jwt);
if (isErrorResponse(root)) { if (isErrorResponse(root)) {
error.value = 'Get root failed: ' + root.message; error.value = "Get root failed: " + root.message;
return; return;
} }
jwt.setToken(res.jwt); jwt.setToken(res.jwt);
await router.push({ await router.push({
name: 'fs', name: "fs",
params: { node_id: root.rootId } params: { node_id: root.rootId },
}); });
} else { } else {
error.value = ''; error.value = "";
requestOtp.value = true; requestOtp.value = true;
} }
} }
</script> </script>
<template> <template>
<div v-if="error !== ''" v-text="error"></div> <div v-if="error !== ''" v-text="error"></div>
<template v-if="!requestOtp"> <template v-if="!requestOtp">
<input type="email" placeholder="Email" v-model="username" /> <input type="email" placeholder="Email" v-model="username" />
<input type="password" placeholder="Password" v-model="password" /> <input type="password" placeholder="Password" v-model="password" />
<a href="/api/auth/gitlab">Login with gitlab</a> <a href="/api/auth/gitlab">Login with gitlab</a>
<router-link to="signup">Signup instead?</router-link> <router-link to="signup">Signup instead?</router-link>
</template> </template>
<template v-else> <template v-else>
<div>Please input your 2 factor authentication code</div> <div>Please input your 2 factor authentication code</div>
<input type="text" placeholder="Code" v-model="otp" /> <input type="text" placeholder="Code" v-model="otp" />
</template> </template>
<button @click="login()">Login</button> <button @click="login()">Login</button>
</template> </template>
<style scoped></style> <style scoped></style>

View File

@ -1,124 +1,106 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, inject, onBeforeMount } from 'vue'; import type { TokenInjectType } from "@/api";
import { import { ref, inject, onBeforeMount } from "vue";
Auth, import { Auth, User, check_token, isErrorResponse, Responses } from "@/api";
User, import { onBeforeRouteUpdate } from "vue-router";
check_token,
isErrorResponse,
TokenInjectType,
Responses
} from '@/api';
import { onBeforeRouteUpdate } from 'vue-router';
const error = ref(''); const error = ref("");
const oldPw = ref(''); const oldPw = ref("");
const newPw = ref(''); const newPw = ref("");
const newPw2 = ref(''); const newPw2 = ref("");
const user = ref<Responses.User.UserInfoResponse | null>(null); const user = ref<Responses.User.UserInfoResponse | null>(null);
const jwt = inject<TokenInjectType>('jwt') as TokenInjectType; const jwt = inject<TokenInjectType>("jwt") as TokenInjectType;
onBeforeRouteUpdate(async () => { onBeforeRouteUpdate(async () => {
await updateProfile(); await updateProfile();
}); });
onBeforeMount(async () => { onBeforeMount(async () => {
await updateProfile(); await updateProfile();
}); });
async function updateProfile() { async function updateProfile() {
const token = await check_token(jwt); const token = await check_token(jwt);
if (!token) return; if (!token) return;
const res = await User.get_user_info(token); const res = await User.get_user_info(token);
if (isErrorResponse(res)) return jwt.logout(); if (isErrorResponse(res)) return jwt.logout();
user.value = res; user.value = res;
} }
async function deleteUser() { async function deleteUser() {
const token = await check_token(jwt); const token = await check_token(jwt);
if (!token) return; if (!token) return;
await User.delete_user(token); await User.delete_user(token);
jwt.logout(); jwt.logout();
} }
async function logoutAll() { async function logoutAll() {
const token = await check_token(jwt); const token = await check_token(jwt);
if (!token) return; if (!token) return;
await Auth.logout_all(token); await Auth.logout_all(token);
jwt.logout(); jwt.logout();
} }
async function changePw() { async function changePw() {
if (oldPw.value === '' || newPw.value === '' || newPw2.value === '') { if (oldPw.value === "" || newPw.value === "" || newPw2.value === "") {
error.value = 'Password missing'; error.value = "Password missing";
return; return;
} }
if (newPw.value !== newPw2.value) { if (newPw.value !== newPw2.value) {
error.value = "Passwords don't match"; error.value = "Passwords don't match";
return; return;
} }
const token = await check_token(jwt); const token = await check_token(jwt);
if (!token) return; if (!token) return;
const res = await Auth.change_password(oldPw.value, newPw.value, token); const res = await Auth.change_password(oldPw.value, newPw.value, token);
if (isErrorResponse(res)) if (isErrorResponse(res))
error.value = 'Password change failed: ' + res.message; error.value = "Password change failed: " + res.message;
else jwt.logout(); else jwt.logout();
} }
async function tfaDisable() { async function tfaDisable() {
const token = await check_token(jwt); const token = await check_token(jwt);
if (!token) return; if (!token) return;
await Auth.tfa_disable(token); await Auth.tfa_disable(token);
jwt.logout(); jwt.logout();
} }
</script> </script>
<template> <template>
<template v-if="user"> <template v-if="user">
<div v-if="error !== ''" v-text="error"></div> <div v-if="error !== ''" v-text="error"></div>
<div>User: {{ user.name }}</div> <div>User: {{ user.name }}</div>
<div>Signed in with {{ user.gitlab ? 'gitlab' : 'password' }}</div> <div>Signed in with {{ user.gitlab ? "gitlab" : "password" }}</div>
<template v-if="!user.gitlab"> <template v-if="!user.gitlab">
<div> <div>
<input <input type="password" placeholder="Old password" v-model="oldPw" />
type="password" <input type="password" placeholder="New password" v-model="newPw" />
placeholder="Old password" <input
v-model="oldPw" type="password"
/> placeholder="Repeat new password"
<input v-model="newPw2"
type="password" />
placeholder="New password" <button @click="changePw">Change</button>
v-model="newPw" </div>
/> <div>
<input <div>
type="password" 2 Factor authentication:
placeholder="Repeat new password" {{ user.tfaEnabled ? "Enabled" : "Disabled" }}
v-model="newPw2" </div>
/> <div>
<button @click="changePw">Change</button> <a href="#" v-if="user.tfaEnabled" @click="tfaDisable"> Disable </a>
</div> <router-link to="/profile/2fa-enable" v-else> Enable </router-link>
<div> </div>
<div> </div>
2 Factor authentication: </template>
{{ user.tfaEnabled ? 'Enabled' : 'Disabled' }} <div>
</div> <a href="#" @click="logoutAll">Logout everywhere</a>
<div> <a href="#" @click="deleteUser">Delete Account</a>
<a href="#" v-if="user.tfaEnabled" @click="tfaDisable"> </div>
Disable </template>
</a> <template v-else>
<router-link to="/profile/2fa-enable" v-else> <div>Loading...</div>
Enable </template>
</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> </template>
<style scoped></style> <style scoped></style>

View File

@ -1,19 +1,19 @@
<script setup lang="ts"> <script setup lang="ts">
import { inject } from 'vue'; import type { TokenInjectType } from "@/api";
import { TokenInjectType } from '@/api'; import { inject } from "vue";
import { useRoute, useRouter } from 'vue-router'; import { useRoute, useRouter } from "vue-router";
const router = useRouter(); const router = useRouter();
const route = useRoute(); 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); if ("token" in route.query) jwt.setToken(route.query["token"] as string);
router.replace({ path: '/' }); router.replace({ path: "/" });
</script> </script>
<template> <template>
<router-link to="/">Click here to go home</router-link> <router-link to="/">Click here to go home</router-link>
</template> </template>
<style scoped></style> <style scoped></style>

View File

@ -1,35 +1,35 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref } from 'vue'; import { ref } from "vue";
import { Auth, isErrorResponse } from '@/api'; import { Auth, isErrorResponse } from "@/api";
const username = ref(''); const username = ref("");
const password = ref(''); const password = ref("");
const password2 = ref(''); const password2 = ref("");
const error = ref(''); const error = ref("");
async function signup() { async function signup() {
if (username.value === '' || password.value === '') { if (username.value === "" || password.value === "") {
error.value = 'Email and/or Password missing'; error.value = "Email and/or Password missing";
return; return;
} }
if (password.value !== password2.value) { if (password.value !== password2.value) {
error.value = "Passwords don't match"; error.value = "Passwords don't match";
return; return;
} }
const res = await Auth.auth_signup(username.value, password.value); const res = await Auth.auth_signup(username.value, password.value);
error.value = isErrorResponse(res) error.value = isErrorResponse(res)
? 'Signup failed: ' + res.message ? "Signup failed: " + res.message
: 'Signup successful, please wait till an admin unlocks your account.'; : "Signup successful, please wait till an admin unlocks your account.";
} }
</script> </script>
<template> <template>
<div v-if="error !== ''" v-text="error"></div> <div v-if="error !== ''" v-text="error"></div>
<input type="email" placeholder="Email" v-model="username" /> <input type="email" placeholder="Email" v-model="username" />
<input type="password" placeholder="Password" v-model="password" /> <input type="password" placeholder="Password" v-model="password" />
<input type="password" placeholder="Repeat password" v-model="password2" /> <input type="password" placeholder="Repeat password" v-model="password2" />
<button @click="signup()">Signup</button> <button @click="signup()">Signup</button>
<router-link to="login">Login instead?</router-link> <router-link to="login">Login instead?</router-link>
</template> </template>
<style scoped></style> <style scoped></style>

View File

@ -1,89 +1,90 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, inject } from 'vue'; import type { TokenInjectType } from "@/api";
import { Auth, check_token, isErrorResponse, TokenInjectType } from '@/api'; import { ref, inject } from "vue";
import { Auth, check_token, isErrorResponse } from "@/api";
enum state { enum state {
SELECT, SELECT,
MAIL, MAIL,
TOTP TOTP,
} }
const currentState = ref<state>(state.SELECT); const currentState = ref<state>(state.SELECT);
const error = ref(''); const error = ref("");
const qrImage = ref(''); const qrImage = ref("");
const secret = ref(''); const secret = ref("");
const code = ref(''); const code = ref("");
const jwt = inject<TokenInjectType>('jwt') as TokenInjectType; const jwt = inject<TokenInjectType>("jwt") as TokenInjectType;
async function selectMail() { async function selectMail() {
const token = await check_token(jwt); const token = await check_token(jwt);
if (!token) return; if (!token) return;
error.value = 'Working...'; error.value = "Working...";
const res = await Auth.tfa_setup(true, token); const res = await Auth.tfa_setup(true, token);
if (isErrorResponse(res)) if (isErrorResponse(res))
error.value = 'Failed to select 2fa type: ' + res.message; error.value = "Failed to select 2fa type: " + res.message;
else { else {
error.value = ''; error.value = "";
currentState.value = state.MAIL; currentState.value = state.MAIL;
} }
} }
async function selectTotp() { async function selectTotp() {
const token = await check_token(jwt); const token = await check_token(jwt);
if (!token) return; if (!token) return;
error.value = 'Working...'; error.value = "Working...";
const res = await Auth.tfa_setup(false, token); const res = await Auth.tfa_setup(false, token);
if (isErrorResponse(res)) if (isErrorResponse(res))
error.value = 'Failed to select 2fa type: ' + res.message; error.value = "Failed to select 2fa type: " + res.message;
else { else {
qrImage.value = res.qrCode; qrImage.value = res.qrCode;
secret.value = res.secret; secret.value = res.secret;
error.value = ''; error.value = "";
currentState.value = state.TOTP; currentState.value = state.TOTP;
} }
} }
async function submit() { async function submit() {
const token = await check_token(jwt); const token = await check_token(jwt);
if (!token) return; if (!token) return;
error.value = 'Working...'; error.value = "Working...";
const res = await Auth.tfa_complete( const res = await Auth.tfa_complete(
currentState.value === state.MAIL, currentState.value === state.MAIL,
code.value, code.value,
token token
); );
if (isErrorResponse(res)) if (isErrorResponse(res))
error.value = 'Failed to submit code: ' + res.message; error.value = "Failed to submit code: " + res.message;
else jwt.logout(); else jwt.logout();
} }
</script> </script>
<template> <template>
<div v-if="error !== ''" v-text="error"></div> <div v-if="error !== ''" v-text="error"></div>
<template v-if="currentState === state.SELECT"> <template v-if="currentState === state.SELECT">
<div>Select 2 Factor authentication type:</div> <div>Select 2 Factor authentication type:</div>
<div> <div>
<button @click="selectMail">Mail</button> <button @click="selectMail">Mail</button>
<button @click="selectTotp">Google Authenticator</button> <button @click="selectTotp">Google Authenticator</button>
</div> </div>
</template> </template>
<template v-else-if="currentState === state.MAIL"> <template v-else-if="currentState === state.MAIL">
<div>Please enter the code you got by mail</div> <div>Please enter the code you got by mail</div>
<input type="text" placeholder="Code" v-model="code" /> <input type="text" placeholder="Code" v-model="code" />
<button @click="submit()">Submit</button> <button @click="submit()">Submit</button>
</template> </template>
<template v-else> <template v-else>
<img :src="qrImage" alt="QrCode" /> <img :src="qrImage" alt="QrCode" />
<details> <details>
<summary>Show manual input code</summary> <summary>Show manual input code</summary>
{{ secret }} {{ secret }}
</details> </details>
<div>Please enter the current code</div> <div>Please enter the current code</div>
<input type="text" placeholder="Code" v-model="code" /> <input type="text" placeholder="Code" v-model="code" />
<button @click="submit()">Submit</button> <button @click="submit()">Submit</button>
</template> </template>
</template> </template>
<style scoped></style> <style scoped></style>

View File

@ -0,0 +1,8 @@
{
"extends": "@vue/tsconfig/tsconfig.node.json",
"include": ["vite.config.*", "vitest.config.*", "cypress.config.*"],
"compilerOptions": {
"composite": true,
"types": ["node"]
}
}

View File

@ -1,32 +1,19 @@
{ {
"compilerOptions": { "extends": "@vue/tsconfig/tsconfig.web.json",
"target": "esnext", "include": ["env.d.ts", "src/**/*", "src/**/*.vue"],
"module": "esnext", "compilerOptions": {
"strict": true, "baseUrl": ".",
"jsx": "preserve", "experimentalDecorators": true,
"moduleResolution": "node", "emitDecoratorMetadata": true,
"skipLibCheck": true, "strictPropertyInitialization": false,
"esModuleInterop": true, "paths": {
"allowSyntheticDefaultImports": true, "@/*": ["./src/*"]
"forceConsistentCasingInFileNames": true, }
"useDefineForClassFields": true, },
"sourceMap": true,
"emitDecoratorMetadata": true, "references": [
"experimentalDecorators": true, {
"strictPropertyInitialization": false, "path": "./tsconfig.config.json"
"baseUrl": ".", }
"types": ["webpack-env"], ]
"paths": {
"@/*": ["src/*"]
},
"lib": ["esnext", "dom", "dom.iterable", "scripthost"]
},
"include": [
"src/**/*.ts",
"src/**/*.tsx",
"src/**/*.vue",
"tests/**/*.ts",
"tests/**/*.tsx"
],
"exclude": ["node_modules"]
} }

14
frontend/vite.config.ts Normal file
View File

@ -0,0 +1,14 @@
import { fileURLToPath, URL } from "node:url";
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
// https://vitejs.dev/config/
export default defineConfig({
plugins: [vue()],
resolve: {
alias: {
"@": fileURLToPath(new URL("./src", import.meta.url)),
},
},
});

View File

@ -1,12 +0,0 @@
const { defineConfig } = require('@vue/cli-service');
module.exports = defineConfig({
transpileDependencies: true,
configureWebpack: {
resolve: {
fallback: {
crypto: false,
stream: require.resolve('stream-browserify')
}
}
}
});

File diff suppressed because it is too large Load Diff