Merge branch '2fa' into 'main'
Add 2 Factor authentication Closes #1 See merge request root/fileserver!2
This commit is contained in:
commit
4dd852a3cb
75
dto/index.ts
75
dto/index.ts
@ -1,73 +1,2 @@
|
|||||||
//
|
export * as Requests from './requests';
|
||||||
// Responses
|
export * as Responses from './responses';
|
||||||
//
|
|
||||||
|
|
||||||
export interface BaseResponse {
|
|
||||||
statusCode: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ErrorResponse extends BaseResponse {
|
|
||||||
statusCode: 400 | 401 | 403;
|
|
||||||
message?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type RefreshResponse = LoginResponse;
|
|
||||||
export interface LoginResponse extends BaseResponse {
|
|
||||||
statusCode: 200;
|
|
||||||
jwt: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface GetRootResponse extends BaseResponse {
|
|
||||||
statusCode: 200;
|
|
||||||
rootId: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface GetNodeResponse extends BaseResponse {
|
|
||||||
statusCode: 200;
|
|
||||||
id: number;
|
|
||||||
name: string;
|
|
||||||
isFile: boolean;
|
|
||||||
parent: number | null;
|
|
||||||
children?: number[];
|
|
||||||
size?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface GetPathResponse extends BaseResponse {
|
|
||||||
statusCode: 200;
|
|
||||||
path: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type CreateFileResponse = CreateFolderResponse;
|
|
||||||
export interface CreateFolderResponse extends BaseResponse {
|
|
||||||
statusCode: 200;
|
|
||||||
id: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type SignupResponse = DeleteResponse;
|
|
||||||
export type UploadFileResponse = DeleteResponse;
|
|
||||||
export interface DeleteResponse extends BaseResponse {
|
|
||||||
statusCode: 200;
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Requests
|
|
||||||
//
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-empty-interface
|
|
||||||
export interface BaseRequest {}
|
|
||||||
|
|
||||||
export type AuthSignUpRequest = AuthLoginRequest;
|
|
||||||
export interface AuthLoginRequest extends BaseRequest {
|
|
||||||
username: string;
|
|
||||||
password: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type CreateFileRequest = CreateFolderRequest;
|
|
||||||
export interface CreateFolderRequest extends BaseRequest {
|
|
||||||
parent: number;
|
|
||||||
name: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface DeleteRequest extends BaseRequest {
|
|
||||||
node: number;
|
|
||||||
}
|
|
||||||
|
10
dto/requests/auth.ts
Normal file
10
dto/requests/auth.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import { BaseRequest } from './base';
|
||||||
|
|
||||||
|
export interface AuthSignUpRequest extends BaseRequest {
|
||||||
|
username: string;
|
||||||
|
password: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AuthLoginRequest extends AuthSignUpRequest {
|
||||||
|
otp?: string;
|
||||||
|
}
|
2
dto/requests/base.ts
Normal file
2
dto/requests/base.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
// eslint-disable-next-line @typescript-eslint/no-empty-interface
|
||||||
|
export interface BaseRequest {}
|
12
dto/requests/fs.ts
Normal file
12
dto/requests/fs.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import { BaseRequest } from './base';
|
||||||
|
|
||||||
|
export type CreateFileRequest = CreateFolderRequest;
|
||||||
|
|
||||||
|
export interface CreateFolderRequest extends BaseRequest {
|
||||||
|
parent: number;
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DeleteRequest extends BaseRequest {
|
||||||
|
node: number;
|
||||||
|
}
|
3
dto/requests/index.ts
Normal file
3
dto/requests/index.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export * from './base';
|
||||||
|
export * as Auth from './auth';
|
||||||
|
export * as FS from './fs';
|
19
dto/responses/auth.ts
Normal file
19
dto/responses/auth.ts
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import { BaseResponse, SuccessResponse } from './base';
|
||||||
|
|
||||||
|
export type TfaRequiredResponse = SuccessResponse;
|
||||||
|
export type RemoveTfaResponse = SuccessResponse;
|
||||||
|
export type RequestEmailTfaResponse = SuccessResponse;
|
||||||
|
export type TfaCompletedResponse = SuccessResponse;
|
||||||
|
export type SignupResponse = SuccessResponse;
|
||||||
|
export type RefreshResponse = LoginResponse;
|
||||||
|
|
||||||
|
export interface LoginResponse extends BaseResponse {
|
||||||
|
statusCode: 200;
|
||||||
|
jwt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RequestTotpTfaResponse extends BaseResponse {
|
||||||
|
statusCode: 200;
|
||||||
|
qrCode: string;
|
||||||
|
secret: string;
|
||||||
|
}
|
12
dto/responses/base.ts
Normal file
12
dto/responses/base.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
export interface BaseResponse {
|
||||||
|
statusCode: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SuccessResponse extends BaseResponse {
|
||||||
|
statusCode: 200;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ErrorResponse extends BaseResponse {
|
||||||
|
statusCode: 400 | 401 | 403;
|
||||||
|
message?: string;
|
||||||
|
}
|
30
dto/responses/fs.ts
Normal file
30
dto/responses/fs.ts
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import { BaseResponse, SuccessResponse } from './base';
|
||||||
|
|
||||||
|
export type UploadFileResponse = SuccessResponse;
|
||||||
|
export type DeleteResponse = SuccessResponse;
|
||||||
|
export type CreateFileResponse = CreateFolderResponse;
|
||||||
|
|
||||||
|
export interface GetRootResponse extends BaseResponse {
|
||||||
|
statusCode: 200;
|
||||||
|
rootId: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GetNodeResponse extends BaseResponse {
|
||||||
|
statusCode: 200;
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
isFile: boolean;
|
||||||
|
parent: number | null;
|
||||||
|
children?: number[];
|
||||||
|
size?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GetPathResponse extends BaseResponse {
|
||||||
|
statusCode: 200;
|
||||||
|
path: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreateFolderResponse extends BaseResponse {
|
||||||
|
statusCode: 200;
|
||||||
|
id: number;
|
||||||
|
}
|
3
dto/responses/index.ts
Normal file
3
dto/responses/index.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export * from './base';
|
||||||
|
export * as Auth from './auth';
|
||||||
|
export * as FS from './fs';
|
@ -1,7 +1,7 @@
|
|||||||
<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 { refresh_token, TokenInjectType, isErrorResponse } from '@/api';
|
import { Auth, TokenInjectType, isErrorResponse } from '@/api';
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
@ -21,7 +21,7 @@ function logout() {
|
|||||||
jwt.value = localStorage.getItem('token');
|
jwt.value = localStorage.getItem('token');
|
||||||
if (jwt.value == null) await router.push({ name: 'login' });
|
if (jwt.value == null) await router.push({ name: 'login' });
|
||||||
else {
|
else {
|
||||||
const new_token = await refresh_token(jwt.value ?? '');
|
const new_token = await Auth.refresh_token(jwt.value ?? '');
|
||||||
if (isErrorResponse(new_token)) logout();
|
if (isErrorResponse(new_token)) logout();
|
||||||
else setToken(new_token.jwt);
|
else setToken(new_token.jwt);
|
||||||
}
|
}
|
||||||
|
@ -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
30
frontend/src/api/auth.ts
Normal 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
63
frontend/src/api/base.ts
Normal 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
95
frontend/src/api/fs.ts
Normal 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);
|
||||||
|
}
|
4
frontend/src/api/index.ts
Normal file
4
frontend/src/api/index.ts
Normal 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
25
frontend/src/api/util.ts
Normal 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;
|
||||||
|
};
|
@ -1,16 +1,10 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { defineEmits, defineProps, inject } from 'vue';
|
import { defineEmits, defineProps, inject } from 'vue';
|
||||||
import {
|
import { check_token, FS, Responses, TokenInjectType } from '@/api';
|
||||||
check_token,
|
|
||||||
delete_node,
|
|
||||||
download_file,
|
|
||||||
GetNodeResponse,
|
|
||||||
TokenInjectType
|
|
||||||
} from '@/api';
|
|
||||||
|
|
||||||
const jwt = inject<TokenInjectType>('jwt') as TokenInjectType;
|
const jwt = inject<TokenInjectType>('jwt') as TokenInjectType;
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
node: GetNodeResponse;
|
node: Responses.FS.GetNodeResponse;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
@ -20,14 +14,14 @@ const emit = defineEmits<{
|
|||||||
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 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;
|
||||||
download_file(token, props.node.id);
|
FS.download_file(token, props.node.id);
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -1,18 +1,12 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { defineEmits, defineProps, inject, reactive, ref, watch } from 'vue';
|
import { defineEmits, defineProps, inject, reactive, ref, watch } from 'vue';
|
||||||
import {
|
import { FS, Responses, check_token, TokenInjectType } from '@/api';
|
||||||
GetNodeResponse,
|
|
||||||
create_folder,
|
|
||||||
get_node,
|
|
||||||
check_token,
|
|
||||||
TokenInjectType
|
|
||||||
} from '@/api';
|
|
||||||
import DirEntry from '@/components/FSView/DirEntry.vue';
|
import DirEntry from '@/components/FSView/DirEntry.vue';
|
||||||
import UploadFileDialog from '@/components/UploadDialog/UploadFileDialog.vue';
|
import UploadFileDialog from '@/components/UploadDialog/UploadFileDialog.vue';
|
||||||
import { NModal } from 'naive-ui';
|
import { NModal } from 'naive-ui';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
node: GetNodeResponse;
|
node: Responses.FS.GetNodeResponse;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const jwt = inject<TokenInjectType>('jwt') as TokenInjectType;
|
const jwt = inject<TokenInjectType>('jwt') as TokenInjectType;
|
||||||
@ -28,9 +22,9 @@ 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<GetNodeResponse[]>([]);
|
const nodes = ref<Responses.FS.GetNodeResponse[]>([]);
|
||||||
const hasParent = ref(false);
|
const hasParent = ref(false);
|
||||||
const parentNode = reactive<GetNodeResponse>({
|
const parentNode = reactive<Responses.FS.GetNodeResponse>({
|
||||||
id: 0,
|
id: 0,
|
||||||
statusCode: 200,
|
statusCode: 200,
|
||||||
isFile: false,
|
isFile: false,
|
||||||
@ -49,7 +43,10 @@ watch(
|
|||||||
await Promise.all(
|
await Promise.all(
|
||||||
to.children?.map(async (child) => {
|
to.children?.map(async (child) => {
|
||||||
nodes.value.push(
|
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() {
|
async function newFolder() {
|
||||||
const token = await check_token(jwt);
|
const token = await check_token(jwt);
|
||||||
if (!token) return;
|
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');
|
emit('reloadNode');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,15 +1,9 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { defineProps, inject } from 'vue';
|
import { defineProps, inject } from 'vue';
|
||||||
import {
|
import { check_token, FS, Responses, TokenInjectType } from '@/api';
|
||||||
check_token,
|
|
||||||
delete_node,
|
|
||||||
download_file,
|
|
||||||
GetNodeResponse,
|
|
||||||
TokenInjectType
|
|
||||||
} from '@/api';
|
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
node: GetNodeResponse;
|
node: Responses.FS.GetNodeResponse;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const jwt = inject<TokenInjectType>('jwt') as TokenInjectType;
|
const jwt = inject<TokenInjectType>('jwt') as TokenInjectType;
|
||||||
@ -17,13 +11,13 @@ 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 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;
|
||||||
download_file(token, props.node.id);
|
FS.download_file(token, props.node.id);
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { defineProps, defineExpose, ref } from 'vue';
|
import { defineProps, defineExpose, ref } from 'vue';
|
||||||
import { isErrorResponse, upload_file } from '@/api';
|
import { isErrorResponse, FS } from '@/api';
|
||||||
import { NProgress } from 'naive-ui';
|
import { NProgress } from 'naive-ui';
|
||||||
import filesize from 'filesize';
|
import filesize from 'filesize';
|
||||||
|
|
||||||
@ -14,7 +14,7 @@ const err = ref('');
|
|||||||
const status = ref('info');
|
const status = ref('info');
|
||||||
|
|
||||||
async function startUpload(parent: number, token: string) {
|
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;
|
progress.value = e.loaded;
|
||||||
percentage.value = (e.loaded / e.total) * 100;
|
percentage.value = (e.loaded / e.total) * 100;
|
||||||
});
|
});
|
||||||
|
@ -3,10 +3,8 @@ import { onBeforeRouteUpdate, useRoute, useRouter } from 'vue-router';
|
|||||||
import { inject, onBeforeMount, ref } from 'vue';
|
import { inject, onBeforeMount, ref } from 'vue';
|
||||||
import {
|
import {
|
||||||
check_token,
|
check_token,
|
||||||
get_node,
|
FS,
|
||||||
get_path,
|
Responses,
|
||||||
get_root,
|
|
||||||
GetNodeResponse,
|
|
||||||
isErrorResponse,
|
isErrorResponse,
|
||||||
TokenInjectType
|
TokenInjectType
|
||||||
} from '@/api';
|
} from '@/api';
|
||||||
@ -18,14 +16,14 @@ 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<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] = [
|
let [p, n] = [
|
||||||
await get_path(token, node_id),
|
await FS.get_path(token, node_id),
|
||||||
await 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();
|
||||||
@ -46,7 +44,7 @@ onBeforeMount(async () => {
|
|||||||
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 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({
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { onBeforeRouteUpdate, useRouter } from 'vue-router';
|
import { onBeforeRouteUpdate, useRouter } from 'vue-router';
|
||||||
import { inject, onBeforeMount } from 'vue';
|
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 router = useRouter();
|
||||||
const jwt = inject<TokenInjectType>('jwt') as TokenInjectType;
|
const jwt = inject<TokenInjectType>('jwt') as TokenInjectType;
|
||||||
@ -11,7 +11,7 @@ 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 get_root(token);
|
const root = await FS.get_root(token);
|
||||||
if (isErrorResponse(root)) return jwt.logout();
|
if (isErrorResponse(root)) return jwt.logout();
|
||||||
await router.push({
|
await router.push({
|
||||||
name: 'fs',
|
name: 'fs',
|
||||||
|
@ -1,25 +1,32 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, inject } from 'vue';
|
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';
|
import { useRouter } from 'vue-router';
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
let username = ref('');
|
const username = ref('');
|
||||||
let password = ref('');
|
const password = ref('');
|
||||||
|
const otp = ref('');
|
||||||
|
|
||||||
const error = ref('');
|
const error = ref('');
|
||||||
|
|
||||||
|
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 = '';
|
||||||
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 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;
|
if (isErrorResponse(res)) error.value = 'Login failed: ' + res.message;
|
||||||
else {
|
else if ('jwt' in res) {
|
||||||
const root = await 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;
|
||||||
@ -29,14 +36,23 @@ async function login() {
|
|||||||
name: 'fs',
|
name: 'fs',
|
||||||
params: { node_id: root.rootId }
|
params: { node_id: root.rootId }
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
error.value = '';
|
||||||
|
requestOtp.value = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</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" />
|
<template v-if="!requestOtp">
|
||||||
<input type="password" placeholder="Password" v-model="password" />
|
<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>
|
<button @click="login()">Login</button>
|
||||||
<router-link to="signup">Signup instead?</router-link>
|
<router-link to="signup">Signup instead?</router-link>
|
||||||
</template>
|
</template>
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
import { auth_signup, isErrorResponse } from '@/api';
|
import { Auth, isErrorResponse } from '@/api';
|
||||||
|
|
||||||
let username = ref('');
|
let username = ref('');
|
||||||
let password = ref('');
|
let password = ref('');
|
||||||
@ -16,7 +16,7 @@ async function signup() {
|
|||||||
error.value = "Passwords don't match";
|
error.value = "Passwords don't match";
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const res = await 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.';
|
||||||
|
@ -32,12 +32,16 @@
|
|||||||
"@nestjs/typeorm": "^9.0.0",
|
"@nestjs/typeorm": "^9.0.0",
|
||||||
"argon2": "^0.28.7",
|
"argon2": "^0.28.7",
|
||||||
"jsonwebtoken": "^8.5.1",
|
"jsonwebtoken": "^8.5.1",
|
||||||
|
"nodemailer": "^6.7.8",
|
||||||
|
"notp": "^2.0.3",
|
||||||
"passport": "^0.6.0",
|
"passport": "^0.6.0",
|
||||||
"passport-jwt": "^4.0.0",
|
"passport-jwt": "^4.0.0",
|
||||||
"passport-local": "^1.0.0",
|
"passport-local": "^1.0.0",
|
||||||
|
"qrcode": "^1.5.1",
|
||||||
"reflect-metadata": "^0.1.13",
|
"reflect-metadata": "^0.1.13",
|
||||||
"rxjs": "^7.5.6",
|
"rxjs": "^7.5.6",
|
||||||
"sqlite3": "^5.0.11",
|
"sqlite3": "^5.0.11",
|
||||||
|
"thirty-two": "^1.0.2",
|
||||||
"typeorm": "^0.3.7"
|
"typeorm": "^0.3.7"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@ -48,8 +52,11 @@
|
|||||||
"@types/jest": "^28.1.6",
|
"@types/jest": "^28.1.6",
|
||||||
"@types/jsonwebtoken": "^8.5.8",
|
"@types/jsonwebtoken": "^8.5.8",
|
||||||
"@types/node": "^18.6.5",
|
"@types/node": "^18.6.5",
|
||||||
|
"@types/nodemailer": "^6.4.5",
|
||||||
|
"@types/notp": "^2.0.2",
|
||||||
"@types/passport-jwt": "^3.0.6",
|
"@types/passport-jwt": "^3.0.6",
|
||||||
"@types/passport-local": "^1.0.34",
|
"@types/passport-local": "^1.0.34",
|
||||||
|
"@types/qrcode": "^1.5.0",
|
||||||
"@types/supertest": "^2.0.12",
|
"@types/supertest": "^2.0.12",
|
||||||
"@types/webpack": "^5.28.0",
|
"@types/webpack": "^5.28.0",
|
||||||
"@types/webpack-node-externals": "^2.5.3",
|
"@types/webpack-node-externals": "^2.5.3",
|
||||||
|
@ -2,20 +2,21 @@ import {
|
|||||||
BadRequestException,
|
BadRequestException,
|
||||||
Body,
|
Body,
|
||||||
Controller,
|
Controller,
|
||||||
|
Get,
|
||||||
HttpCode,
|
HttpCode,
|
||||||
|
ParseBoolPipe,
|
||||||
Post,
|
Post,
|
||||||
Request,
|
Request,
|
||||||
|
UnauthorizedException,
|
||||||
UseGuards
|
UseGuards
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { AuthService } from '../services/auth';
|
import { AuthService } from '../services/auth';
|
||||||
import { AuthGuard } from '@nestjs/passport';
|
import { AuthGuard } from '@nestjs/passport';
|
||||||
import { Public } from '../authguards';
|
import { Public } from '../authguards';
|
||||||
import {
|
import { Responses } from 'dto';
|
||||||
ErrorResponse,
|
import { tfaTypes } from '../entities';
|
||||||
LoginResponse,
|
import { toDataURL } from 'qrcode';
|
||||||
RefreshResponse,
|
import * as base32 from 'thirty-two';
|
||||||
SignupResponse
|
|
||||||
} from 'dto';
|
|
||||||
|
|
||||||
@Controller('api/auth')
|
@Controller('api/auth')
|
||||||
export default class AuthController {
|
export default class AuthController {
|
||||||
@ -25,19 +26,90 @@ export default class AuthController {
|
|||||||
@UseGuards(AuthGuard('local'))
|
@UseGuards(AuthGuard('local'))
|
||||||
@Post('login')
|
@Post('login')
|
||||||
@HttpCode(200)
|
@HttpCode(200)
|
||||||
async login(@Request() req): Promise<LoginResponse> {
|
async login(
|
||||||
|
@Request() req,
|
||||||
|
@Body('otp') otp?: string
|
||||||
|
): Promise<
|
||||||
|
Responses.Auth.LoginResponse | Responses.Auth.TfaRequiredResponse
|
||||||
|
> {
|
||||||
|
if (this.authService.requiresTfa(req.user)) {
|
||||||
|
if (!otp) {
|
||||||
|
if (req.user.tfaType == tfaTypes.EMAIL)
|
||||||
|
await this.authService.sendTfaMail(req.user);
|
||||||
|
return {
|
||||||
|
statusCode: 200
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (!(await this.authService.verifyTfa(req.user, otp)))
|
||||||
|
throw new UnauthorizedException('Incorrect 2fa');
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
statusCode: 200,
|
statusCode: 200,
|
||||||
jwt: await this.authService.login(req.user)
|
jwt: await this.authService.login(req.user)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async tfa(
|
||||||
|
req,
|
||||||
|
code: string,
|
||||||
|
type: tfaTypes
|
||||||
|
): Promise<Responses.Auth.TfaCompletedResponse> {
|
||||||
|
if (!(await this.authService.verifyTfa(req.user, code, type))) {
|
||||||
|
throw new UnauthorizedException('Incorrect 2fa');
|
||||||
|
}
|
||||||
|
await this.authService.setTfaType(req.user, type);
|
||||||
|
await this.authService.revokeAll(req.user);
|
||||||
|
return {
|
||||||
|
statusCode: 200
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post('2fa/complete/mail')
|
||||||
|
async tfaMail(
|
||||||
|
@Request() req,
|
||||||
|
@Body('code') code: string
|
||||||
|
): Promise<Responses.Auth.TfaCompletedResponse> {
|
||||||
|
return await this.tfa(req, code, tfaTypes.EMAIL);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post('2fa/complete/totp')
|
||||||
|
async tfaTotp(
|
||||||
|
@Request() req,
|
||||||
|
@Body('code') code: string
|
||||||
|
): Promise<Responses.Auth.TfaCompletedResponse> {
|
||||||
|
return await this.tfa(req, code, tfaTypes.TOTP);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('2fa/setup')
|
||||||
|
async setupTotp(
|
||||||
|
@Request() req,
|
||||||
|
@Body('mail', ParseBoolPipe) mail: boolean
|
||||||
|
): Promise<
|
||||||
|
| Responses.Auth.RequestTotpTfaResponse
|
||||||
|
| Responses.Auth.RequestEmailTfaResponse
|
||||||
|
> {
|
||||||
|
const secret = await this.authService.setupTfa(req.user);
|
||||||
|
if (mail)
|
||||||
|
return {
|
||||||
|
statusCode: 200
|
||||||
|
};
|
||||||
|
return {
|
||||||
|
statusCode: 200,
|
||||||
|
qrCode: await toDataURL(
|
||||||
|
`otpauth://totp/MFileserver:${req.user.name}?secret=${base32
|
||||||
|
.encode(secret)
|
||||||
|
.toString()}&issuer=MFileserver`
|
||||||
|
),
|
||||||
|
secret
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
@Public()
|
@Public()
|
||||||
@Post('signup')
|
@Post('signup')
|
||||||
async signup(
|
async signup(
|
||||||
@Body('username') username,
|
@Body('username') username,
|
||||||
@Body('password') password
|
@Body('password') password
|
||||||
): Promise<SignupResponse | ErrorResponse> {
|
): Promise<Responses.Auth.SignupResponse> {
|
||||||
if ((await this.authService.findUser(username)) != null)
|
if ((await this.authService.findUser(username)) != null)
|
||||||
throw new BadRequestException('Username already taken');
|
throw new BadRequestException('Username already taken');
|
||||||
await this.authService.signup(username, password);
|
await this.authService.signup(username, password);
|
||||||
@ -47,7 +119,7 @@ export default class AuthController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Post('refresh')
|
@Post('refresh')
|
||||||
async refresh(@Request() req): Promise<RefreshResponse | ErrorResponse> {
|
async refresh(@Request() req): Promise<Responses.Auth.RefreshResponse> {
|
||||||
const token = await this.authService.login(req.user);
|
const token = await this.authService.login(req.user);
|
||||||
await this.authService.revoke(req.token);
|
await this.authService.revoke(req.token);
|
||||||
return {
|
return {
|
||||||
|
@ -8,15 +8,7 @@ import {
|
|||||||
Request,
|
Request,
|
||||||
StreamableFile
|
StreamableFile
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import {
|
import { Responses } from 'dto';
|
||||||
CreateFileResponse,
|
|
||||||
CreateFolderResponse,
|
|
||||||
DeleteResponse,
|
|
||||||
GetNodeResponse,
|
|
||||||
GetPathResponse,
|
|
||||||
GetRootResponse,
|
|
||||||
UploadFileResponse
|
|
||||||
} from 'dto/index';
|
|
||||||
import FileSystemService from '../services/filesystem';
|
import FileSystemService from '../services/filesystem';
|
||||||
import { UserRole } from '../entities';
|
import { UserRole } from '../entities';
|
||||||
import { Role } from '../authguards';
|
import { Role } from '../authguards';
|
||||||
@ -27,7 +19,7 @@ export default class FileSystemController {
|
|||||||
|
|
||||||
@Get('root')
|
@Get('root')
|
||||||
@Role(UserRole.USER)
|
@Role(UserRole.USER)
|
||||||
async getRoot(@Request() req): Promise<GetRootResponse> {
|
async getRoot(@Request() req): Promise<Responses.FS.GetRootResponse> {
|
||||||
return {
|
return {
|
||||||
statusCode: 200,
|
statusCode: 200,
|
||||||
rootId: req.user.rootId
|
rootId: req.user.rootId
|
||||||
@ -39,9 +31,9 @@ export default class FileSystemController {
|
|||||||
async getNode(
|
async getNode(
|
||||||
@Request() req,
|
@Request() req,
|
||||||
@Param('node', ParseIntPipe) nodeId
|
@Param('node', ParseIntPipe) nodeId
|
||||||
): Promise<GetNodeResponse> {
|
): Promise<Responses.FS.GetNodeResponse> {
|
||||||
const node = await this.fsService.getNodeAndValidate(nodeId, req.user);
|
const node = await this.fsService.getNodeAndValidate(nodeId, req.user);
|
||||||
const data: GetNodeResponse = {
|
const data: Responses.FS.GetNodeResponse = {
|
||||||
id: nodeId,
|
id: nodeId,
|
||||||
statusCode: 200,
|
statusCode: 200,
|
||||||
name: node.name,
|
name: node.name,
|
||||||
@ -61,7 +53,7 @@ export default class FileSystemController {
|
|||||||
async getPath(
|
async getPath(
|
||||||
@Request() req,
|
@Request() req,
|
||||||
@Param('node', ParseIntPipe) nodeId
|
@Param('node', ParseIntPipe) nodeId
|
||||||
): Promise<GetPathResponse> {
|
): Promise<Responses.FS.GetPathResponse> {
|
||||||
return {
|
return {
|
||||||
statusCode: 200,
|
statusCode: 200,
|
||||||
path: await this.fsService.generatePath(
|
path: await this.fsService.generatePath(
|
||||||
@ -76,7 +68,7 @@ export default class FileSystemController {
|
|||||||
@Request() req,
|
@Request() req,
|
||||||
@Body('parent', ParseIntPipe) parent,
|
@Body('parent', ParseIntPipe) parent,
|
||||||
@Body('name') name
|
@Body('name') name
|
||||||
): Promise<CreateFolderResponse> {
|
): Promise<Responses.FS.CreateFolderResponse> {
|
||||||
const newChild = await this.fsService.create(
|
const newChild = await this.fsService.create(
|
||||||
await this.fsService.getNodeAndValidate(parent, req.user),
|
await this.fsService.getNodeAndValidate(parent, req.user),
|
||||||
name,
|
name,
|
||||||
@ -95,7 +87,7 @@ export default class FileSystemController {
|
|||||||
@Request() req,
|
@Request() req,
|
||||||
@Body('parent', ParseIntPipe) parent,
|
@Body('parent', ParseIntPipe) parent,
|
||||||
@Body('name') name
|
@Body('name') name
|
||||||
): Promise<CreateFileResponse> {
|
): Promise<Responses.FS.CreateFileResponse> {
|
||||||
const newChild = await this.fsService.create(
|
const newChild = await this.fsService.create(
|
||||||
await this.fsService.getNodeAndValidate(parent, req.user),
|
await this.fsService.getNodeAndValidate(parent, req.user),
|
||||||
name,
|
name,
|
||||||
@ -113,7 +105,7 @@ export default class FileSystemController {
|
|||||||
async delete(
|
async delete(
|
||||||
@Request() req,
|
@Request() req,
|
||||||
@Body('node', ParseIntPipe) node_id
|
@Body('node', ParseIntPipe) node_id
|
||||||
): Promise<DeleteResponse> {
|
): Promise<Responses.FS.DeleteResponse> {
|
||||||
await this.fsService.delete(
|
await this.fsService.delete(
|
||||||
await this.fsService.getNodeAndValidate(node_id, req.user)
|
await this.fsService.getNodeAndValidate(node_id, req.user)
|
||||||
);
|
);
|
||||||
@ -125,7 +117,7 @@ export default class FileSystemController {
|
|||||||
async upload(
|
async upload(
|
||||||
@Request() req,
|
@Request() req,
|
||||||
@Param('node', ParseIntPipe) nodeId
|
@Param('node', ParseIntPipe) nodeId
|
||||||
): Promise<UploadFileResponse> {
|
): Promise<Responses.FS.UploadFileResponse> {
|
||||||
await this.fsService.uploadFile(await req.file(), nodeId, req.user);
|
await this.fsService.uploadFile(await req.file(), nodeId, req.user);
|
||||||
return { statusCode: 200 };
|
return { statusCode: 200 };
|
||||||
}
|
}
|
||||||
|
@ -13,6 +13,12 @@ export enum UserRole {
|
|||||||
DISABLED = 0
|
DISABLED = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum tfaTypes {
|
||||||
|
NONE = 0,
|
||||||
|
EMAIL = 1,
|
||||||
|
TOTP = 2
|
||||||
|
}
|
||||||
|
|
||||||
@Entity()
|
@Entity()
|
||||||
export class INode {
|
export class INode {
|
||||||
@PrimaryGeneratedColumn()
|
@PrimaryGeneratedColumn()
|
||||||
@ -59,6 +65,19 @@ export class User {
|
|||||||
rootId: number;
|
rootId: number;
|
||||||
@OneToOne(() => INode)
|
@OneToOne(() => INode)
|
||||||
root: Promise<INode>;
|
root: Promise<INode>;
|
||||||
|
|
||||||
|
@Column({
|
||||||
|
type: 'int',
|
||||||
|
default: tfaTypes.NONE,
|
||||||
|
transformer: {
|
||||||
|
from: (db: number): tfaTypes => db,
|
||||||
|
to: (type: tfaTypes): number => type
|
||||||
|
}
|
||||||
|
})
|
||||||
|
tfaType: tfaTypes;
|
||||||
|
|
||||||
|
@Column({ nullable: true })
|
||||||
|
tfaSecret: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Entity()
|
@Entity()
|
||||||
|
@ -4,17 +4,30 @@ import {
|
|||||||
UnauthorizedException
|
UnauthorizedException
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { InjectRepository } from '@nestjs/typeorm';
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
import { JWTToken, User, UserRole } from '../entities';
|
import { JWTToken, tfaTypes, User, UserRole } from '../entities';
|
||||||
import { Repository, LessThanOrEqual } from 'typeorm';
|
import { LessThanOrEqual, Repository } from 'typeorm';
|
||||||
import * as argon2 from 'argon2';
|
import * as argon2 from 'argon2';
|
||||||
import { PassportStrategy } from '@nestjs/passport';
|
import { PassportStrategy } from '@nestjs/passport';
|
||||||
import { Strategy as LocalStrategy } from 'passport-local';
|
import { Strategy as LocalStrategy } from 'passport-local';
|
||||||
import { ExtractJwt, Strategy as JWTStrategy } from 'passport-jwt';
|
import { ExtractJwt, Strategy as JWTStrategy } from 'passport-jwt';
|
||||||
import FileSystemService from './filesystem';
|
import FileSystemService from './filesystem';
|
||||||
import * as jwt from 'jsonwebtoken';
|
import * as jwt from 'jsonwebtoken';
|
||||||
|
import { createTransport } from 'nodemailer';
|
||||||
|
import * as notp from 'notp';
|
||||||
|
import { randomBytes } from 'crypto';
|
||||||
|
|
||||||
const jwtSecret = 'CUM';
|
const jwtSecret = 'CUM';
|
||||||
|
|
||||||
|
const mailAccount = createTransport({
|
||||||
|
host: 'mail.mattv.de',
|
||||||
|
port: 587,
|
||||||
|
secure: false,
|
||||||
|
auth: {
|
||||||
|
user: 'no-reply@mattv.de',
|
||||||
|
pass: 'noreplyLONGPASS123'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
interface jwtPayload {
|
interface jwtPayload {
|
||||||
sub: number;
|
sub: number;
|
||||||
jti: number;
|
jti: number;
|
||||||
@ -32,6 +45,16 @@ export class AuthService {
|
|||||||
private fsService: FileSystemService
|
private fsService: FileSystemService
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
|
generateTfaSecret(): string {
|
||||||
|
const set =
|
||||||
|
'0123456789ABCDEFGHIJKLMNOPQRSTUVWXTZabcdefghiklmnopqrstuvwxyz!@#$%^&*()<>?/[]{},.:;';
|
||||||
|
return randomBytes(32)
|
||||||
|
.map((b) =>
|
||||||
|
set.charCodeAt(Math.floor((b / 255.0) * (set.length - 1)))
|
||||||
|
)
|
||||||
|
.toString();
|
||||||
|
}
|
||||||
|
|
||||||
async getUser(userId: number): Promise<User | null> {
|
async getUser(userId: number): Promise<User | null> {
|
||||||
return this.userRepo.findOneBy({
|
return this.userRepo.findOneBy({
|
||||||
id: userId
|
id: userId
|
||||||
@ -67,6 +90,52 @@ export class AuthService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
requiresTfa(user: User): boolean {
|
||||||
|
return user.tfaType != tfaTypes.NONE;
|
||||||
|
}
|
||||||
|
|
||||||
|
async verifyTfa(
|
||||||
|
user: User,
|
||||||
|
token: string,
|
||||||
|
type?: tfaTypes
|
||||||
|
): Promise<boolean> {
|
||||||
|
if (!type) type = user.tfaType;
|
||||||
|
const delta = notp.totp.verify(token, user.tfaSecret, {
|
||||||
|
window: 10
|
||||||
|
});
|
||||||
|
return (
|
||||||
|
delta &&
|
||||||
|
(type == tfaTypes.EMAIL ? delta.delta <= 0 : delta.delta == 0)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async sendTfaMail(user: User) {
|
||||||
|
await mailAccount.sendMail({
|
||||||
|
from: 'fileserver@mattv.de',
|
||||||
|
to: user.name,
|
||||||
|
subject: 'Fileserver - EMail 2fa code',
|
||||||
|
text: `Your code is: ${notp.totp.gen(
|
||||||
|
user.tfaSecret
|
||||||
|
)}\nIt is valid for 5 Minutes`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async setupTfa(user: User): Promise<string> {
|
||||||
|
if (user.tfaType != tfaTypes.NONE)
|
||||||
|
throw new BadRequestException(
|
||||||
|
'2 Factor authentication is already setup'
|
||||||
|
);
|
||||||
|
const secret = this.generateTfaSecret();
|
||||||
|
user.tfaSecret = secret;
|
||||||
|
await this.userRepo.save(user);
|
||||||
|
return secret;
|
||||||
|
}
|
||||||
|
|
||||||
|
async setTfaType(user: User, type: tfaTypes) {
|
||||||
|
user.tfaType = type;
|
||||||
|
await this.userRepo.save(user);
|
||||||
|
}
|
||||||
|
|
||||||
async login(user: User) {
|
async login(user: User) {
|
||||||
const token = new JWTToken();
|
const token = new JWTToken();
|
||||||
token.ownerId = user.id;
|
token.ownerId = user.id;
|
||||||
@ -101,6 +170,12 @@ export class AuthService {
|
|||||||
id: token.id
|
id: token.id
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async revokeAll(user: User) {
|
||||||
|
await this.tokenRepo.delete({
|
||||||
|
ownerId: user.id
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
|
128
yarn.lock
128
yarn.lock
@ -1105,6 +1105,20 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-18.7.11.tgz#486e72cfccde88da24e1f23ff1b7d8bfb64e6250"
|
resolved "https://registry.yarnpkg.com/@types/node/-/node-18.7.11.tgz#486e72cfccde88da24e1f23ff1b7d8bfb64e6250"
|
||||||
integrity sha512-KZhFpSLlmK/sdocfSAjqPETTMd0ug6HIMIAwkwUpU79olnZdQtMxpQP+G1wDzCH7na+FltSIhbaZuKdwZ8RDrw==
|
integrity sha512-KZhFpSLlmK/sdocfSAjqPETTMd0ug6HIMIAwkwUpU79olnZdQtMxpQP+G1wDzCH7na+FltSIhbaZuKdwZ8RDrw==
|
||||||
|
|
||||||
|
"@types/nodemailer@^6.4.5":
|
||||||
|
version "6.4.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/nodemailer/-/nodemailer-6.4.5.tgz#09011ac73259245475d1688e4ba101860567dc39"
|
||||||
|
integrity sha512-zuP3nBRQHI6M2PkXnGGy1Ww4VB+MyYHGgnfV2T+JR9KLkeWqPJuyVUgLpKXuFnA/b7pZaIDFh2sV4759B7jK1g==
|
||||||
|
dependencies:
|
||||||
|
"@types/node" "*"
|
||||||
|
|
||||||
|
"@types/notp@^2.0.2":
|
||||||
|
version "2.0.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/notp/-/notp-2.0.2.tgz#7283f4918b2770555e0f2df72acc9f46ebd41ae9"
|
||||||
|
integrity sha512-JUcVYN9Tmw0AjoAfvjslS4hbv39fPBbZdftBK3b50g5z/DmhLsu6cd0UOEBiQuMwy2FirshF2Gk9gAvfWjshMw==
|
||||||
|
dependencies:
|
||||||
|
"@types/node" "*"
|
||||||
|
|
||||||
"@types/parse-json@^4.0.0":
|
"@types/parse-json@^4.0.0":
|
||||||
version "4.0.0"
|
version "4.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0"
|
resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0"
|
||||||
@ -1148,6 +1162,13 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@types/prettier/-/prettier-2.7.0.tgz#ea03e9f0376a4446f44797ca19d9c46c36e352dc"
|
resolved "https://registry.yarnpkg.com/@types/prettier/-/prettier-2.7.0.tgz#ea03e9f0376a4446f44797ca19d9c46c36e352dc"
|
||||||
integrity sha512-RI1L7N4JnW5gQw2spvL7Sllfuf1SaHdrZpCHiBlCXjIlufi1SMNnbu2teze3/QE67Fg2tBlH7W+mi4hVNk4p0A==
|
integrity sha512-RI1L7N4JnW5gQw2spvL7Sllfuf1SaHdrZpCHiBlCXjIlufi1SMNnbu2teze3/QE67Fg2tBlH7W+mi4hVNk4p0A==
|
||||||
|
|
||||||
|
"@types/qrcode@^1.5.0":
|
||||||
|
version "1.5.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/qrcode/-/qrcode-1.5.0.tgz#6a98fe9a9a7b2a9a3167b6dde17eff999eabe40b"
|
||||||
|
integrity sha512-x5ilHXRxUPIMfjtM+1vf/GPTRWZ81nqscursm5gMznJeK9M0YnZ1c3bEvRLQ0zSSgedLx1J6MGL231ObQGGhaA==
|
||||||
|
dependencies:
|
||||||
|
"@types/node" "*"
|
||||||
|
|
||||||
"@types/qs@*":
|
"@types/qs@*":
|
||||||
version "6.9.6"
|
version "6.9.6"
|
||||||
resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.6.tgz#df9c3c8b31a247ec315e6996566be3171df4b3b1"
|
resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.6.tgz#df9c3c8b31a247ec315e6996566be3171df4b3b1"
|
||||||
@ -1871,7 +1892,7 @@ callsites@^3.0.0:
|
|||||||
resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73"
|
resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73"
|
||||||
integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==
|
integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==
|
||||||
|
|
||||||
camelcase@^5.3.1:
|
camelcase@^5.0.0, camelcase@^5.3.1:
|
||||||
version "5.3.1"
|
version "5.3.1"
|
||||||
resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320"
|
resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320"
|
||||||
integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==
|
integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==
|
||||||
@ -1999,6 +2020,15 @@ cli-width@^3.0.0:
|
|||||||
resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-3.0.0.tgz#a2f48437a2caa9a22436e794bf071ec9e61cedf6"
|
resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-3.0.0.tgz#a2f48437a2caa9a22436e794bf071ec9e61cedf6"
|
||||||
integrity sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==
|
integrity sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==
|
||||||
|
|
||||||
|
cliui@^6.0.0:
|
||||||
|
version "6.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/cliui/-/cliui-6.0.0.tgz#511d702c0c4e41ca156d7d0e96021f23e13225b1"
|
||||||
|
integrity sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==
|
||||||
|
dependencies:
|
||||||
|
string-width "^4.2.0"
|
||||||
|
strip-ansi "^6.0.0"
|
||||||
|
wrap-ansi "^6.2.0"
|
||||||
|
|
||||||
cliui@^7.0.2:
|
cliui@^7.0.2:
|
||||||
version "7.0.4"
|
version "7.0.4"
|
||||||
resolved "https://registry.yarnpkg.com/cliui/-/cliui-7.0.4.tgz#a0265ee655476fc807aea9df3df8df7783808b4f"
|
resolved "https://registry.yarnpkg.com/cliui/-/cliui-7.0.4.tgz#a0265ee655476fc807aea9df3df8df7783808b4f"
|
||||||
@ -2188,6 +2218,11 @@ debug@4, debug@^4.0.0, debug@^4.1.0, debug@^4.1.1, debug@^4.3.2, debug@^4.3.3, d
|
|||||||
dependencies:
|
dependencies:
|
||||||
ms "2.1.2"
|
ms "2.1.2"
|
||||||
|
|
||||||
|
decamelize@^1.2.0:
|
||||||
|
version "1.2.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290"
|
||||||
|
integrity sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==
|
||||||
|
|
||||||
dedent@^0.7.0:
|
dedent@^0.7.0:
|
||||||
version "0.7.0"
|
version "0.7.0"
|
||||||
resolved "https://registry.yarnpkg.com/dedent/-/dedent-0.7.0.tgz#2495ddbaf6eb874abb0e1be9df22d2e5a544326c"
|
resolved "https://registry.yarnpkg.com/dedent/-/dedent-0.7.0.tgz#2495ddbaf6eb874abb0e1be9df22d2e5a544326c"
|
||||||
@ -2263,6 +2298,11 @@ diff@^4.0.1:
|
|||||||
resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d"
|
resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d"
|
||||||
integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==
|
integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==
|
||||||
|
|
||||||
|
dijkstrajs@^1.0.1:
|
||||||
|
version "1.0.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/dijkstrajs/-/dijkstrajs-1.0.2.tgz#2e48c0d3b825462afe75ab4ad5e829c8ece36257"
|
||||||
|
integrity sha512-QV6PMaHTCNmKSeP6QoXhVTw9snc9VD8MulTT0Bd99Pacp4SS1cjcrYPgBPmibqKVtMJJfqC6XvOXgPMEEPH/fg==
|
||||||
|
|
||||||
dir-glob@^3.0.1:
|
dir-glob@^3.0.1:
|
||||||
version "3.0.1"
|
version "3.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-3.0.1.tgz#56dbf73d992a4a93ba1584f4534063fd2e41717f"
|
resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-3.0.1.tgz#56dbf73d992a4a93ba1584f4534063fd2e41717f"
|
||||||
@ -2309,6 +2349,11 @@ emoji-regex@^8.0.0:
|
|||||||
resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37"
|
resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37"
|
||||||
integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==
|
integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==
|
||||||
|
|
||||||
|
encode-utf8@^1.0.3:
|
||||||
|
version "1.0.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/encode-utf8/-/encode-utf8-1.0.3.tgz#f30fdd31da07fb596f281beb2f6b027851994cda"
|
||||||
|
integrity sha512-ucAnuBEhUK4boH2HjVYG5Q2mQyPorvv0u/ocS+zhdw0S8AlHYY+GOFhP1Gio5z4icpP2ivFSvhtFjQi8+T9ppw==
|
||||||
|
|
||||||
encodeurl@~1.0.2:
|
encodeurl@~1.0.2:
|
||||||
version "1.0.2"
|
version "1.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59"
|
resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59"
|
||||||
@ -2876,7 +2921,7 @@ gensync@^1.0.0-beta.2:
|
|||||||
resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0"
|
resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0"
|
||||||
integrity sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==
|
integrity sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==
|
||||||
|
|
||||||
get-caller-file@^2.0.5:
|
get-caller-file@^2.0.1, get-caller-file@^2.0.5:
|
||||||
version "2.0.5"
|
version "2.0.5"
|
||||||
resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e"
|
resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e"
|
||||||
integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==
|
integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==
|
||||||
@ -4239,6 +4284,11 @@ node-releases@^2.0.5:
|
|||||||
resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.5.tgz#280ed5bc3eba0d96ce44897d8aee478bfb3d9666"
|
resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.5.tgz#280ed5bc3eba0d96ce44897d8aee478bfb3d9666"
|
||||||
integrity sha512-U9h1NLROZTq9uE1SNffn6WuPDg8icmi3ns4rEl/oTfIle4iLjTliCzgTsbaIFMq/Xn078/lfY/BL0GWZ+psK4Q==
|
integrity sha512-U9h1NLROZTq9uE1SNffn6WuPDg8icmi3ns4rEl/oTfIle4iLjTliCzgTsbaIFMq/Xn078/lfY/BL0GWZ+psK4Q==
|
||||||
|
|
||||||
|
nodemailer@^6.7.8:
|
||||||
|
version "6.7.8"
|
||||||
|
resolved "https://registry.yarnpkg.com/nodemailer/-/nodemailer-6.7.8.tgz#9f1af9911314960c0b889079e1754e8d9e3f740a"
|
||||||
|
integrity sha512-2zaTFGqZixVmTxpJRCFC+Vk5eGRd/fYtvIR+dl5u9QXLTQWGIf48x/JXvo58g9sa0bU6To04XUv554Paykum3g==
|
||||||
|
|
||||||
nopt@^5.0.0:
|
nopt@^5.0.0:
|
||||||
version "5.0.0"
|
version "5.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/nopt/-/nopt-5.0.0.tgz#530942bb58a512fccafe53fe210f13a25355dc88"
|
resolved "https://registry.yarnpkg.com/nopt/-/nopt-5.0.0.tgz#530942bb58a512fccafe53fe210f13a25355dc88"
|
||||||
@ -4251,6 +4301,11 @@ normalize-path@^3.0.0, normalize-path@~3.0.0:
|
|||||||
resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65"
|
resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65"
|
||||||
integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==
|
integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==
|
||||||
|
|
||||||
|
notp@^2.0.3:
|
||||||
|
version "2.0.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/notp/-/notp-2.0.3.tgz#a9fd11e25cfe1ccb39fc6689544ee4c10ef9a577"
|
||||||
|
integrity sha512-oBig/2uqkjQ5AkBuw4QJYwkEWa/q+zHxI5/I5z6IeP2NT0alpJFsP/trrfCC+9xOAgQSZXssNi962kp5KBmypQ==
|
||||||
|
|
||||||
npm-run-path@^4.0.0, npm-run-path@^4.0.1:
|
npm-run-path@^4.0.0, npm-run-path@^4.0.1:
|
||||||
version "4.0.1"
|
version "4.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-4.0.1.tgz#b7ecd1e5ed53da8e37a55e1c2269e0b97ed748ea"
|
resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-4.0.1.tgz#b7ecd1e5ed53da8e37a55e1c2269e0b97ed748ea"
|
||||||
@ -4569,6 +4624,11 @@ pluralize@8.0.0:
|
|||||||
resolved "https://registry.yarnpkg.com/pluralize/-/pluralize-8.0.0.tgz#1a6fa16a38d12a1901e0320fa017051c539ce3b1"
|
resolved "https://registry.yarnpkg.com/pluralize/-/pluralize-8.0.0.tgz#1a6fa16a38d12a1901e0320fa017051c539ce3b1"
|
||||||
integrity sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==
|
integrity sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==
|
||||||
|
|
||||||
|
pngjs@^5.0.0:
|
||||||
|
version "5.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/pngjs/-/pngjs-5.0.0.tgz#e79dd2b215767fd9c04561c01236df960bce7fbb"
|
||||||
|
integrity sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==
|
||||||
|
|
||||||
prelude-ls@^1.2.1:
|
prelude-ls@^1.2.1:
|
||||||
version "1.2.1"
|
version "1.2.1"
|
||||||
resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396"
|
resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396"
|
||||||
@ -4643,6 +4703,16 @@ punycode@^2.1.0:
|
|||||||
resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec"
|
resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec"
|
||||||
integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==
|
integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==
|
||||||
|
|
||||||
|
qrcode@^1.5.1:
|
||||||
|
version "1.5.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/qrcode/-/qrcode-1.5.1.tgz#0103f97317409f7bc91772ef30793a54cd59f0cb"
|
||||||
|
integrity sha512-nS8NJ1Z3md8uTjKtP+SGGhfqmTCs5flU/xR623oI0JX+Wepz9R8UrRVCTBTJm3qGw3rH6jJ6MUHjkDx15cxSSg==
|
||||||
|
dependencies:
|
||||||
|
dijkstrajs "^1.0.1"
|
||||||
|
encode-utf8 "^1.0.3"
|
||||||
|
pngjs "^5.0.0"
|
||||||
|
yargs "^15.3.1"
|
||||||
|
|
||||||
qs@6.9.3:
|
qs@6.9.3:
|
||||||
version "6.9.3"
|
version "6.9.3"
|
||||||
resolved "https://registry.yarnpkg.com/qs/-/qs-6.9.3.tgz#bfadcd296c2d549f1dffa560619132c977f5008e"
|
resolved "https://registry.yarnpkg.com/qs/-/qs-6.9.3.tgz#bfadcd296c2d549f1dffa560619132c977f5008e"
|
||||||
@ -4744,6 +4814,11 @@ require-from-string@^2.0.2:
|
|||||||
resolved "https://registry.yarnpkg.com/require-from-string/-/require-from-string-2.0.2.tgz#89a7fdd938261267318eafe14f9c32e598c36909"
|
resolved "https://registry.yarnpkg.com/require-from-string/-/require-from-string-2.0.2.tgz#89a7fdd938261267318eafe14f9c32e598c36909"
|
||||||
integrity sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==
|
integrity sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==
|
||||||
|
|
||||||
|
require-main-filename@^2.0.0:
|
||||||
|
version "2.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-2.0.0.tgz#d0b329ecc7cc0f61649f62215be69af54aa8989b"
|
||||||
|
integrity sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==
|
||||||
|
|
||||||
resolve-cwd@^3.0.0:
|
resolve-cwd@^3.0.0:
|
||||||
version "3.0.0"
|
version "3.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-3.0.0.tgz#0f0075f1bb2544766cf73ba6a6e2adfebcb13f2d"
|
resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-3.0.0.tgz#0f0075f1bb2544766cf73ba6a6e2adfebcb13f2d"
|
||||||
@ -5314,6 +5389,11 @@ thenify-all@^1.0.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
any-promise "^1.0.0"
|
any-promise "^1.0.0"
|
||||||
|
|
||||||
|
thirty-two@^1.0.2:
|
||||||
|
version "1.0.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/thirty-two/-/thirty-two-1.0.2.tgz#4ca2fffc02a51290d2744b9e3f557693ca6b627a"
|
||||||
|
integrity sha512-OEI0IWCe+Dw46019YLl6V10Us5bi574EvlJEOcAkB29IzQ/mYD1A6RyNHLjZPiHCmuodxvgF6U+vZO1L15lxVA==
|
||||||
|
|
||||||
thread-stream@^2.0.0:
|
thread-stream@^2.0.0:
|
||||||
version "2.1.0"
|
version "2.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/thread-stream/-/thread-stream-2.1.0.tgz#d560dd8b9d09482b0e2e876a96c229c374870836"
|
resolved "https://registry.yarnpkg.com/thread-stream/-/thread-stream-2.1.0.tgz#d560dd8b9d09482b0e2e876a96c229c374870836"
|
||||||
@ -5707,6 +5787,11 @@ whatwg-url@^5.0.0:
|
|||||||
tr46 "~0.0.3"
|
tr46 "~0.0.3"
|
||||||
webidl-conversions "^3.0.0"
|
webidl-conversions "^3.0.0"
|
||||||
|
|
||||||
|
which-module@^2.0.0:
|
||||||
|
version "2.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a"
|
||||||
|
integrity sha512-B+enWhmw6cjfVC7kS8Pj9pCrKSc5txArRyaYGe088shv/FGWH+0Rjx/xPgtsWfsUtS27FkP697E4DDhgrgoc0Q==
|
||||||
|
|
||||||
which@^2.0.1, which@^2.0.2:
|
which@^2.0.1, which@^2.0.2:
|
||||||
version "2.0.2"
|
version "2.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1"
|
resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1"
|
||||||
@ -5738,6 +5823,15 @@ word-wrap@^1.2.3:
|
|||||||
resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c"
|
resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c"
|
||||||
integrity sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==
|
integrity sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==
|
||||||
|
|
||||||
|
wrap-ansi@^6.2.0:
|
||||||
|
version "6.2.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-6.2.0.tgz#e9393ba07102e6c91a3b221478f0257cd2856e53"
|
||||||
|
integrity sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==
|
||||||
|
dependencies:
|
||||||
|
ansi-styles "^4.0.0"
|
||||||
|
string-width "^4.1.0"
|
||||||
|
strip-ansi "^6.0.0"
|
||||||
|
|
||||||
wrap-ansi@^7.0.0:
|
wrap-ansi@^7.0.0:
|
||||||
version "7.0.0"
|
version "7.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
|
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
|
||||||
@ -5773,6 +5867,11 @@ xmlbuilder@~11.0.0:
|
|||||||
resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-11.0.1.tgz#be9bae1c8a046e76b31127726347d0ad7002beb3"
|
resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-11.0.1.tgz#be9bae1c8a046e76b31127726347d0ad7002beb3"
|
||||||
integrity sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==
|
integrity sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==
|
||||||
|
|
||||||
|
y18n@^4.0.0:
|
||||||
|
version "4.0.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.3.tgz#b5f259c82cd6e336921efd7bfd8bf560de9eeedf"
|
||||||
|
integrity sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==
|
||||||
|
|
||||||
y18n@^5.0.5:
|
y18n@^5.0.5:
|
||||||
version "5.0.8"
|
version "5.0.8"
|
||||||
resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55"
|
resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55"
|
||||||
@ -5793,6 +5892,14 @@ yargs-parser@21.0.1, yargs-parser@^21.0.0:
|
|||||||
resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-21.0.1.tgz#0267f286c877a4f0f728fceb6f8a3e4cb95c6e35"
|
resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-21.0.1.tgz#0267f286c877a4f0f728fceb6f8a3e4cb95c6e35"
|
||||||
integrity sha512-9BK1jFpLzJROCI5TzwZL/TU4gqjK5xiHV/RfWLOahrjAko/e4DJkRDZQXfvqAsiZzzYhgAzbgz6lg48jcm4GLg==
|
integrity sha512-9BK1jFpLzJROCI5TzwZL/TU4gqjK5xiHV/RfWLOahrjAko/e4DJkRDZQXfvqAsiZzzYhgAzbgz6lg48jcm4GLg==
|
||||||
|
|
||||||
|
yargs-parser@^18.1.2:
|
||||||
|
version "18.1.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-18.1.3.tgz#be68c4975c6b2abf469236b0c870362fab09a7b0"
|
||||||
|
integrity sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==
|
||||||
|
dependencies:
|
||||||
|
camelcase "^5.0.0"
|
||||||
|
decamelize "^1.2.0"
|
||||||
|
|
||||||
yargs-parser@^20.2.2:
|
yargs-parser@^20.2.2:
|
||||||
version "20.2.9"
|
version "20.2.9"
|
||||||
resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.9.tgz#2eb7dc3b0289718fc295f362753845c41a0c94ee"
|
resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.9.tgz#2eb7dc3b0289718fc295f362753845c41a0c94ee"
|
||||||
@ -5803,6 +5910,23 @@ yargs-parser@^21.0.1:
|
|||||||
resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-21.1.1.tgz#9096bceebf990d21bb31fa9516e0ede294a77d35"
|
resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-21.1.1.tgz#9096bceebf990d21bb31fa9516e0ede294a77d35"
|
||||||
integrity sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==
|
integrity sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==
|
||||||
|
|
||||||
|
yargs@^15.3.1:
|
||||||
|
version "15.4.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/yargs/-/yargs-15.4.1.tgz#0d87a16de01aee9d8bec2bfbf74f67851730f4f8"
|
||||||
|
integrity sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==
|
||||||
|
dependencies:
|
||||||
|
cliui "^6.0.0"
|
||||||
|
decamelize "^1.2.0"
|
||||||
|
find-up "^4.1.0"
|
||||||
|
get-caller-file "^2.0.1"
|
||||||
|
require-directory "^2.1.1"
|
||||||
|
require-main-filename "^2.0.0"
|
||||||
|
set-blocking "^2.0.0"
|
||||||
|
string-width "^4.2.0"
|
||||||
|
which-module "^2.0.0"
|
||||||
|
y18n "^4.0.0"
|
||||||
|
yargs-parser "^18.1.2"
|
||||||
|
|
||||||
yargs@^16.0.0:
|
yargs@^16.0.0:
|
||||||
version "16.2.0"
|
version "16.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/yargs/-/yargs-16.2.0.tgz#1c82bf0f6b6a66eafce7ef30e376f49a12477f66"
|
resolved "https://registry.yarnpkg.com/yargs/-/yargs-16.2.0.tgz#1c82bf0f6b6a66eafce7ef30e376f49a12477f66"
|
||||||
|
Loading…
Reference in New Issue
Block a user