Added totp/mail otp, split up dto and api into multiple files

This commit is contained in:
2022-08-24 16:15:33 +02:00
parent af1df3e508
commit cd0d25ba4f
30 changed files with 535 additions and 379 deletions

View File

@@ -1,7 +1,7 @@
<script setup async lang="ts">
import { provide, ref } from 'vue';
import { useRouter } from 'vue-router';
import { refresh_token, TokenInjectType, isErrorResponse } from '@/api';
import { Auth, TokenInjectType, isErrorResponse } from '@/api';
const router = useRouter();
@@ -21,7 +21,7 @@ function logout() {
jwt.value = localStorage.getItem('token');
if (jwt.value == null) await router.push({ name: 'login' });
else {
const new_token = await refresh_token(jwt.value ?? '');
const new_token = await Auth.refresh_token(jwt.value ?? '');
if (isErrorResponse(new_token)) logout();
else setToken(new_token.jwt);
}

View File

@@ -1,221 +0,0 @@
import axios from 'axios';
import {
AuthLoginRequest,
AuthSignUpRequest,
BaseRequest,
BaseResponse,
CreateFileRequest,
CreateFileResponse,
CreateFolderRequest,
CreateFolderResponse,
DeleteRequest,
DeleteResponse,
ErrorResponse,
GetNodeResponse,
GetPathResponse,
GetRootResponse,
LoginResponse,
RefreshResponse,
SignupResponse,
UploadFileResponse
} from '../../dto';
import jwtDecode, { JwtPayload } from 'jwt-decode';
import { Ref, UnwrapRef } from 'vue';
export * from '../../dto';
const post = <T extends BaseRequest>(url: string, data: T) =>
axios
.post(url, data, {
headers: { 'Content-type': 'application/json' }
})
.then((res) => res.data)
.catch((err) => err.response.data);
const post_token = <T extends BaseRequest>(
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);
const post_token_form = (
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);
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const get = (url: string) =>
axios
.get(url)
.then((res) => res.data)
.catch((err) => err.response.data);
const get_token = (url: string, token: string) =>
axios
.get(url, {
headers: { Authorization: 'Bearer ' + token }
})
.then((res) => res.data)
.catch((err) => err.response.data);
//
// Api Requests
//
export const auth_login = (
username: string,
password: string
): Promise<LoginResponse | ErrorResponse> =>
post<AuthLoginRequest>('/api/auth/login', {
username: username,
password: password
});
export const auth_signup = (
username: string,
password: string
): Promise<SignupResponse | ErrorResponse> =>
post<AuthSignUpRequest>('/api/auth/signup', {
username: username,
password: password
});
export const get_root = (
token: string
): Promise<GetRootResponse | ErrorResponse> => get_token('/api/fs/root', token);
export const get_node = (
token: string,
node: number
): Promise<GetNodeResponse | ErrorResponse> =>
get_token(`/api/fs/node/${node}`, token);
export const get_path = (
token: string,
node: number
): Promise<GetPathResponse | ErrorResponse> =>
get_token(`/api/fs/path/${node}`, token);
export const create_folder = (
token: string,
parent: number,
name: string
): Promise<CreateFolderResponse | ErrorResponse> =>
post_token<CreateFolderRequest>(
'/api/fs/createFolder',
{
parent: parent,
name: name
},
token
);
export const create_file = (
token: string,
parent: number,
name: string
): Promise<CreateFileResponse | ErrorResponse> =>
post_token<CreateFileRequest>(
'/api/fs/createFile',
{
parent: parent,
name: name
},
token
);
export const delete_node = (
token: string,
node: number
): Promise<DeleteResponse | ErrorResponse> =>
post_token<DeleteRequest>(
'/api/fs/delete',
{
node: node
},
token
);
export const upload_file = async (
token: string,
parent: number,
file: File,
onProgress: (progressEvent: ProgressEvent) => void
): Promise<UploadFileResponse | ErrorResponse> => {
const node = await create_file(token, parent, file.name);
if (isErrorResponse(node)) return node;
const form = new FormData();
form.set('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);
}
export const refresh_token = (
token: string
): Promise<RefreshResponse | ErrorResponse> =>
post_token('/api/auth/refresh', '', token);
//
// Utilities
//
export async function check_token(
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;
}
export const isErrorResponse = (res: BaseResponse): res is ErrorResponse =>
res.statusCode != 200;
export type TokenInjectType = {
jwt: Ref<UnwrapRef<string | null>>;
setToken: (token: string) => void;
logout: () => void;
};

30
frontend/src/api/auth.ts Normal file
View File

@@ -0,0 +1,30 @@
import { Responses, Requests, 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.AuthLoginRequest>('/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.AuthSignUpRequest>('/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);

63
frontend/src/api/base.ts Normal file
View File

@@ -0,0 +1,63 @@
import axios from 'axios';
import { Requests, Responses } from '../../../dto';
export * from '../../../dto';
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_token = <T extends Requests.BaseRequest>(
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);
export const post_token_form = (
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);
// 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);
export const get_token = (url: string, token: string) =>
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;

95
frontend/src/api/fs.ts Normal file
View File

@@ -0,0 +1,95 @@
import {
Responses,
Requests,
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);
export const get_node = (
token: string,
node: number
): Promise<Responses.FS.GetNodeResponse | Responses.ErrorResponse> =>
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);
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
);
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
);
export const delete_node = (
token: string,
node: number
): Promise<Responses.FS.DeleteResponse | Responses.ErrorResponse> =>
post_token<Requests.FS.DeleteRequest>(
'/api/fs/delete',
{
node: node
},
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;
const form = new FormData();
form.set('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);
}

View File

@@ -0,0 +1,4 @@
export { Requests, Responses, isErrorResponse } from './base';
export * as Auth from './auth';
export * as FS from './fs';
export * from './util';

25
frontend/src/api/util.ts Normal file
View File

@@ -0,0 +1,25 @@
import jwtDecode, { JwtPayload } from 'jwt-decode';
import { Ref, UnwrapRef } from 'vue';
import { isErrorResponse } from './base';
import { refresh_token } from './auth';
export async function check_token(
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;
}
export type TokenInjectType = {
jwt: Ref<UnwrapRef<string | null>>;
setToken: (token: string) => void;
logout: () => void;
};

View File

@@ -1,16 +1,10 @@
<script setup lang="ts">
import { defineEmits, defineProps, inject } from 'vue';
import {
check_token,
delete_node,
download_file,
GetNodeResponse,
TokenInjectType
} from '@/api';
import { check_token, FS, Responses, TokenInjectType } from '@/api';
const jwt = inject<TokenInjectType>('jwt') as TokenInjectType;
const props = defineProps<{
node: GetNodeResponse;
node: Responses.FS.GetNodeResponse;
}>();
const emit = defineEmits<{
@@ -20,14 +14,14 @@ const emit = defineEmits<{
async function del() {
const token = await check_token(jwt);
if (!token) return;
await delete_node(token, props.node.id);
await FS.delete_node(token, props.node.id);
emit('reloadNode');
}
async function download() {
const token = await check_token(jwt);
if (!token) return;
download_file(token, props.node.id);
FS.download_file(token, props.node.id);
}
</script>

View File

@@ -1,18 +1,12 @@
<script setup lang="ts">
import { defineEmits, defineProps, inject, reactive, ref, watch } from 'vue';
import {
GetNodeResponse,
create_folder,
get_node,
check_token,
TokenInjectType
} from '@/api';
import { FS, Responses, check_token, TokenInjectType } 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: GetNodeResponse;
node: Responses.FS.GetNodeResponse;
}>();
const jwt = inject<TokenInjectType>('jwt') as TokenInjectType;
@@ -28,9 +22,9 @@ const uploadDialogShow = ref(false);
const new_folder_name = ref('');
const files = ref<File[]>([]);
const nodes = ref<GetNodeResponse[]>([]);
const nodes = ref<Responses.FS.GetNodeResponse[]>([]);
const hasParent = ref(false);
const parentNode = reactive<GetNodeResponse>({
const parentNode = reactive<Responses.FS.GetNodeResponse>({
id: 0,
statusCode: 200,
isFile: false,
@@ -49,7 +43,10 @@ watch(
await Promise.all(
to.children?.map(async (child) => {
nodes.value.push(
(await get_node(token, child)) as GetNodeResponse
(await FS.get_node(
token,
child
)) as Responses.FS.GetNodeResponse
);
}) ?? []
);
@@ -60,7 +57,7 @@ watch(
async function newFolder() {
const token = await check_token(jwt);
if (!token) return;
await create_folder(token, props.node.id, new_folder_name.value);
await FS.create_folder(token, props.node.id, new_folder_name.value);
emit('reloadNode');
}

View File

@@ -1,15 +1,9 @@
<script setup lang="ts">
import { defineProps, inject } from 'vue';
import {
check_token,
delete_node,
download_file,
GetNodeResponse,
TokenInjectType
} from '@/api';
import { check_token, FS, Responses, TokenInjectType } from '@/api';
const props = defineProps<{
node: GetNodeResponse;
node: Responses.FS.GetNodeResponse;
}>();
const jwt = inject<TokenInjectType>('jwt') as TokenInjectType;
@@ -17,13 +11,13 @@ const jwt = inject<TokenInjectType>('jwt') as TokenInjectType;
async function del() {
const token = await check_token(jwt);
if (!token) return;
await delete_node(token, props.node.id);
await FS.delete_node(token, props.node.id);
}
async function download() {
const token = await check_token(jwt);
if (!token) return;
download_file(token, props.node.id);
FS.download_file(token, props.node.id);
}
</script>

View File

@@ -1,6 +1,6 @@
<script setup lang="ts">
import { defineProps, defineExpose, ref } from 'vue';
import { isErrorResponse, upload_file } from '@/api';
import { isErrorResponse, FS } from '@/api';
import { NProgress } from 'naive-ui';
import filesize from 'filesize';
@@ -14,7 +14,7 @@ const err = ref('');
const status = ref('info');
async function startUpload(parent: number, token: string) {
const resp = await upload_file(token, parent, props.file, (e) => {
const resp = await FS.upload_file(token, parent, props.file, (e) => {
progress.value = e.loaded;
percentage.value = (e.loaded / e.total) * 100;
});

View File

@@ -3,10 +3,8 @@ import { onBeforeRouteUpdate, useRoute, useRouter } from 'vue-router';
import { inject, onBeforeMount, ref } from 'vue';
import {
check_token,
get_node,
get_path,
get_root,
GetNodeResponse,
FS,
Responses,
isErrorResponse,
TokenInjectType
} from '@/api';
@@ -18,14 +16,14 @@ const route = useRoute();
const jwt = inject<TokenInjectType>('jwt') as TokenInjectType;
const path = ref('');
const node = ref<GetNodeResponse | null>(null);
const node = ref<Responses.FS.GetNodeResponse | null>(null);
async function fetch_node(node_id: number) {
const token = await check_token(jwt);
if (!token) return;
let [p, n] = [
await get_path(token, node_id),
await get_node(token, node_id)
await FS.get_path(token, node_id),
await FS.get_node(token, node_id)
];
if (isErrorResponse(p)) return gotoRoot();
if (isErrorResponse(n)) return gotoRoot();
@@ -46,7 +44,7 @@ onBeforeMount(async () => {
async function gotoRoot() {
const token = await check_token(jwt);
if (!token) return;
const rootRes = await get_root(token);
const rootRes = await FS.get_root(token);
if (isErrorResponse(rootRes)) return jwt.logout();
const root = rootRes.rootId;
await router.replace({

View File

@@ -3,7 +3,7 @@
<script setup lang="ts">
import { onBeforeRouteUpdate, useRouter } from 'vue-router';
import { inject, onBeforeMount } from 'vue';
import { check_token, get_root, isErrorResponse, TokenInjectType } from '@/api';
import { FS, check_token, isErrorResponse, TokenInjectType } from '@/api';
const router = useRouter();
const jwt = inject<TokenInjectType>('jwt') as TokenInjectType;
@@ -11,7 +11,7 @@ const jwt = inject<TokenInjectType>('jwt') as TokenInjectType;
async function start_redirect() {
const token = await check_token(jwt);
if (!token) return;
const root = await get_root(token);
const root = await FS.get_root(token);
if (isErrorResponse(root)) return jwt.logout();
await router.push({
name: 'fs',

View File

@@ -1,25 +1,32 @@
<script setup lang="ts">
import { ref, inject } from 'vue';
import { auth_login, get_root, isErrorResponse, TokenInjectType } from '@/api';
import { Auth, FS, isErrorResponse, TokenInjectType } from '@/api';
import { useRouter } from 'vue-router';
const router = useRouter();
let username = ref('');
let password = ref('');
const username = ref('');
const password = ref('');
const otp = ref('');
const error = ref('');
const requestOtp = ref(false);
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 auth_login(username.value, password.value);
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 {
const root = await get_root(res.jwt);
else if ('jwt' in res) {
const root = await FS.get_root(res.jwt);
if (isErrorResponse(root)) {
error.value = 'Get root failed: ' + root.message;
return;
@@ -29,14 +36,23 @@ async function login() {
name: 'fs',
params: { node_id: root.rootId }
});
} else {
error.value = '';
requestOtp.value = true;
}
}
</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" />
<template v-if="!requestOtp">
<input type="email" placeholder="Email" v-model="username" />
<input type="password" placeholder="Password" v-model="password" />
</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>
<router-link to="signup">Signup instead?</router-link>
</template>

View File

@@ -1,6 +1,6 @@
<script setup lang="ts">
import { ref } from 'vue';
import { auth_signup, isErrorResponse } from '@/api';
import { Auth, isErrorResponse } from '@/api';
let username = ref('');
let password = ref('');
@@ -16,7 +16,7 @@ async function signup() {
error.value = "Passwords don't match";
return;
}
const res = await auth_signup(username.value, password.value);
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.';