Merge branch 'user-info' into 'main'
Add user info/profile page See merge request root/fileserver!5
This commit is contained in:
commit
762c1c84c9
@ -23,14 +23,28 @@ export class LoginRequest extends SignUpRequest {
|
|||||||
otp?: string;
|
otp?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class TfaComplete extends BaseRequest {
|
|
||||||
@IsNotEmpty()
|
|
||||||
@IsString()
|
|
||||||
code: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class TfaSetup extends BaseRequest {
|
export class TfaSetup extends BaseRequest {
|
||||||
@IsNotEmpty()
|
@IsNotEmpty()
|
||||||
@IsBoolean()
|
@IsBoolean()
|
||||||
mail: boolean;
|
mail: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class TfaComplete extends BaseRequest {
|
||||||
|
@IsNotEmpty()
|
||||||
|
@IsBoolean()
|
||||||
|
mail: boolean;
|
||||||
|
|
||||||
|
@IsNotEmpty()
|
||||||
|
@IsString()
|
||||||
|
code: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ChangePasswordRequest extends BaseRequest {
|
||||||
|
@IsNotEmpty()
|
||||||
|
@IsString()
|
||||||
|
oldPassword: string;
|
||||||
|
|
||||||
|
@IsNotEmpty()
|
||||||
|
@IsString()
|
||||||
|
newPassword: string;
|
||||||
|
}
|
||||||
|
@ -35,4 +35,6 @@ export class RemoveTfaResponse extends SuccessResponse {}
|
|||||||
export class RequestEmailTfaResponse extends SuccessResponse {}
|
export class RequestEmailTfaResponse extends SuccessResponse {}
|
||||||
export class TfaCompletedResponse extends SuccessResponse {}
|
export class TfaCompletedResponse extends SuccessResponse {}
|
||||||
export class SignupResponse extends SuccessResponse {}
|
export class SignupResponse extends SuccessResponse {}
|
||||||
|
export class ChangePasswordResponse extends SuccessResponse {}
|
||||||
|
export class LogoutAllResponse extends SuccessResponse {}
|
||||||
export class RefreshResponse extends LoginResponse {}
|
export class RefreshResponse extends LoginResponse {}
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
export * from './base';
|
export * from './base';
|
||||||
export * as Auth from './auth';
|
export * as Auth from './auth';
|
||||||
export * as FS from './fs';
|
export * as FS from './fs';
|
||||||
|
export * as User from './user';
|
||||||
|
27
dto/src/responses/user.ts
Normal file
27
dto/src/responses/user.ts
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import { SuccessResponse } from './base';
|
||||||
|
import { ValidateConstructor } from '../utils';
|
||||||
|
import { IsBoolean, IsNotEmpty, IsString } from 'class-validator';
|
||||||
|
|
||||||
|
@ValidateConstructor
|
||||||
|
export class UserInfoResponse extends SuccessResponse {
|
||||||
|
constructor(name: string, gitlab: boolean, tfaEnabled: boolean) {
|
||||||
|
super();
|
||||||
|
this.name = name;
|
||||||
|
this.gitlab = gitlab;
|
||||||
|
this.tfaEnabled = tfaEnabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
@IsNotEmpty()
|
||||||
|
@IsString()
|
||||||
|
name: string;
|
||||||
|
|
||||||
|
@IsBoolean()
|
||||||
|
gitlab: boolean;
|
||||||
|
|
||||||
|
@IsBoolean()
|
||||||
|
tfaEnabled: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class DeleteUserResponse extends SuccessResponse {}
|
||||||
|
export class ChangePasswordResponse extends SuccessResponse {}
|
||||||
|
export class LogoutAllResponse extends SuccessResponse {}
|
@ -27,9 +27,13 @@ provide<TokenInjectType>('jwt', {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<nav>
|
<nav>
|
||||||
<router-link to="/login" v-if="jwt != null" @click="logout()">
|
<template v-if="jwt != null">
|
||||||
Logout
|
<router-link to="/">Files</router-link>
|
||||||
</router-link>
|
<span style="margin-left: 2em" />
|
||||||
|
<router-link to="/profile">Profile</router-link>
|
||||||
|
<span style="margin-left: 2em" />
|
||||||
|
<router-link to="/login" @click="logout()">Logout</router-link>
|
||||||
|
</template>
|
||||||
</nav>
|
</nav>
|
||||||
<router-view />
|
<router-view />
|
||||||
</template>
|
</template>
|
||||||
|
@ -28,3 +28,66 @@ export const refresh_token = (
|
|||||||
token: string
|
token: string
|
||||||
): Promise<Responses.Auth.RefreshResponse | Responses.ErrorResponse> =>
|
): Promise<Responses.Auth.RefreshResponse | Responses.ErrorResponse> =>
|
||||||
post_token('/api/auth/refresh', {}, token);
|
post_token('/api/auth/refresh', {}, token);
|
||||||
|
|
||||||
|
export const change_password = (
|
||||||
|
oldPw: string,
|
||||||
|
newPw: string,
|
||||||
|
token: string
|
||||||
|
): Promise<Responses.Auth.ChangePasswordResponse | Responses.ErrorResponse> =>
|
||||||
|
post_token<Requests.Auth.ChangePasswordRequest>(
|
||||||
|
'/api/auth/change_password',
|
||||||
|
{
|
||||||
|
oldPassword: oldPw,
|
||||||
|
newPassword: newPw
|
||||||
|
},
|
||||||
|
token
|
||||||
|
);
|
||||||
|
|
||||||
|
export const logout_all = (
|
||||||
|
token: string
|
||||||
|
): Promise<Responses.Auth.LogoutAllResponse | Responses.ErrorResponse> =>
|
||||||
|
post_token('/api/auth/logout_all', {}, token);
|
||||||
|
|
||||||
|
export function tfa_setup(
|
||||||
|
mail: false,
|
||||||
|
token: string
|
||||||
|
): Promise<Responses.Auth.RequestTotpTfaResponse | Responses.ErrorResponse>;
|
||||||
|
export function tfa_setup(
|
||||||
|
mail: true,
|
||||||
|
token: string
|
||||||
|
): Promise<Responses.Auth.RequestEmailTfaResponse | Responses.ErrorResponse>;
|
||||||
|
export function tfa_setup(
|
||||||
|
mail: boolean,
|
||||||
|
token: string
|
||||||
|
): Promise<
|
||||||
|
| Responses.Auth.RequestEmailTfaResponse
|
||||||
|
| Responses.Auth.RequestTotpTfaResponse
|
||||||
|
| Responses.ErrorResponse
|
||||||
|
> {
|
||||||
|
return post_token<Requests.Auth.TfaSetup>(
|
||||||
|
'/api/auth/2fa/setup',
|
||||||
|
{
|
||||||
|
mail
|
||||||
|
},
|
||||||
|
token
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const tfa_complete = (
|
||||||
|
mail: boolean,
|
||||||
|
code: string,
|
||||||
|
token: string
|
||||||
|
): Promise<Responses.Auth.TfaCompletedResponse | Responses.ErrorResponse> =>
|
||||||
|
post_token<Requests.Auth.TfaComplete>(
|
||||||
|
'/api/auth/2fa/complete',
|
||||||
|
{
|
||||||
|
mail,
|
||||||
|
code
|
||||||
|
},
|
||||||
|
token
|
||||||
|
);
|
||||||
|
|
||||||
|
export const tfa_disable = (
|
||||||
|
token: string
|
||||||
|
): Promise<Responses.Auth.RemoveTfaResponse | Responses.ErrorResponse> =>
|
||||||
|
post_token('/api/auth/2fa/disable', {}, token);
|
||||||
|
@ -3,4 +3,5 @@ export { Requests, Responses } from 'dto';
|
|||||||
export { isErrorResponse } from './base';
|
export { isErrorResponse } from './base';
|
||||||
export * as Auth from './auth';
|
export * as Auth from './auth';
|
||||||
export * as FS from './fs';
|
export * as FS from './fs';
|
||||||
|
export * as User from './user';
|
||||||
export * from './util';
|
export * from './util';
|
||||||
|
12
frontend/src/api/user.ts
Normal file
12
frontend/src/api/user.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import { get_token, post_token } from '@/api/base';
|
||||||
|
import { Responses } from 'dto';
|
||||||
|
|
||||||
|
export const get_user_info = (
|
||||||
|
token: string
|
||||||
|
): Promise<Responses.User.UserInfoResponse | Responses.ErrorResponse> =>
|
||||||
|
get_token('/api/user/info', token);
|
||||||
|
|
||||||
|
export const delete_user = (
|
||||||
|
token: string
|
||||||
|
): Promise<Responses.User.DeleteUserResponse | Responses.ErrorResponse> =>
|
||||||
|
post_token('/api/user/delete', {}, token);
|
@ -5,6 +5,8 @@ import HomeView from '@/views/HomeView.vue';
|
|||||||
import AboutView from '@/views/AboutView.vue';
|
import AboutView from '@/views/AboutView.vue';
|
||||||
import FSView from '@/views/FSView.vue';
|
import FSView from '@/views/FSView.vue';
|
||||||
import SetTokenView from '@/views/SetTokenView.vue';
|
import SetTokenView from '@/views/SetTokenView.vue';
|
||||||
|
import ProfileView from '@/views/ProfileView.vue';
|
||||||
|
import TFAView from '@/views/TFAView.vue';
|
||||||
|
|
||||||
const routes: Array<RouteRecordRaw> = [
|
const routes: Array<RouteRecordRaw> = [
|
||||||
{
|
{
|
||||||
@ -12,9 +14,18 @@ const routes: Array<RouteRecordRaw> = [
|
|||||||
name: 'home',
|
name: 'home',
|
||||||
component: HomeView
|
component: HomeView
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/profile',
|
||||||
|
name: 'profile',
|
||||||
|
component: ProfileView
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/profile/2fa-enable',
|
||||||
|
name: '2fa',
|
||||||
|
component: TFAView
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: '/about',
|
path: '/about',
|
||||||
name: 'about',
|
|
||||||
component: AboutView
|
component: AboutView
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -27,14 +38,15 @@ const routes: Array<RouteRecordRaw> = [
|
|||||||
name: 'signup',
|
name: 'signup',
|
||||||
component: SignupView
|
component: SignupView
|
||||||
},
|
},
|
||||||
{
|
|
||||||
path: '/set_token',
|
|
||||||
component: SetTokenView
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
path: '/fs/:node_id',
|
path: '/fs/:node_id',
|
||||||
name: 'fs',
|
name: 'fs',
|
||||||
component: FSView
|
component: FSView
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
path: '/set_token',
|
||||||
|
component: SetTokenView
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
@ -13,7 +13,7 @@ async function start_redirect() {
|
|||||||
if (!token) return;
|
if (!token) return;
|
||||||
const root = await FS.get_root(token);
|
const root = await FS.get_root(token);
|
||||||
if (isErrorResponse(root)) return jwt.logout();
|
if (isErrorResponse(root)) return jwt.logout();
|
||||||
await router.push({
|
await router.replace({
|
||||||
name: 'fs',
|
name: 'fs',
|
||||||
params: { node_id: root.rootId }
|
params: { node_id: root.rootId }
|
||||||
});
|
});
|
||||||
|
@ -48,14 +48,14 @@ async function login() {
|
|||||||
<template v-if="!requestOtp">
|
<template v-if="!requestOtp">
|
||||||
<input type="email" placeholder="Email" v-model="username" />
|
<input type="email" placeholder="Email" v-model="username" />
|
||||||
<input type="password" placeholder="Password" v-model="password" />
|
<input type="password" placeholder="Password" v-model="password" />
|
||||||
|
<a href="/api/auth/gitlab">Login with gitlab</a>
|
||||||
|
<router-link to="signup">Signup instead?</router-link>
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<div>Please input your 2 factor authentication code</div>
|
<div>Please input your 2 factor authentication code</div>
|
||||||
<input type="text" placeholder="Code" v-model="otp" />
|
<input type="text" placeholder="Code" v-model="otp" />
|
||||||
</template>
|
</template>
|
||||||
<button @click="login()">Login</button>
|
<button @click="login()">Login</button>
|
||||||
<a href="/api/auth/gitlab">Login with gitlab</a>
|
|
||||||
<router-link to="signup">Signup instead?</router-link>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped></style>
|
<style scoped></style>
|
||||||
|
124
frontend/src/views/ProfileView.vue
Normal file
124
frontend/src/views/ProfileView.vue
Normal file
@ -0,0 +1,124 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, inject, onBeforeMount } from 'vue';
|
||||||
|
import {
|
||||||
|
Auth,
|
||||||
|
User,
|
||||||
|
check_token,
|
||||||
|
isErrorResponse,
|
||||||
|
TokenInjectType,
|
||||||
|
Responses
|
||||||
|
} from '@/api';
|
||||||
|
import { onBeforeRouteUpdate } from 'vue-router';
|
||||||
|
|
||||||
|
const error = ref('');
|
||||||
|
const oldPw = ref('');
|
||||||
|
const newPw = ref('');
|
||||||
|
const newPw2 = ref('');
|
||||||
|
const user = ref<Responses.User.UserInfoResponse | null>(null);
|
||||||
|
|
||||||
|
const jwt = inject<TokenInjectType>('jwt') as TokenInjectType;
|
||||||
|
|
||||||
|
onBeforeRouteUpdate(async () => {
|
||||||
|
await updateProfile();
|
||||||
|
});
|
||||||
|
onBeforeMount(async () => {
|
||||||
|
await updateProfile();
|
||||||
|
});
|
||||||
|
async function updateProfile() {
|
||||||
|
const token = await check_token(jwt);
|
||||||
|
if (!token) return;
|
||||||
|
|
||||||
|
const res = await User.get_user_info(token);
|
||||||
|
if (isErrorResponse(res)) return jwt.logout();
|
||||||
|
user.value = res;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteUser() {
|
||||||
|
const token = await check_token(jwt);
|
||||||
|
if (!token) return;
|
||||||
|
await User.delete_user(token);
|
||||||
|
jwt.logout();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function logoutAll() {
|
||||||
|
const token = await check_token(jwt);
|
||||||
|
if (!token) return;
|
||||||
|
await Auth.logout_all(token);
|
||||||
|
jwt.logout();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function changePw() {
|
||||||
|
if (oldPw.value === '' || newPw.value === '' || newPw2.value === '') {
|
||||||
|
error.value = 'Password missing';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (newPw.value !== newPw2.value) {
|
||||||
|
error.value = "Passwords don't match";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const token = await check_token(jwt);
|
||||||
|
if (!token) return;
|
||||||
|
const res = await Auth.change_password(oldPw.value, newPw.value, token);
|
||||||
|
if (isErrorResponse(res))
|
||||||
|
error.value = 'Password change failed: ' + res.message;
|
||||||
|
else jwt.logout();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function tfaDisable() {
|
||||||
|
const token = await check_token(jwt);
|
||||||
|
if (!token) return;
|
||||||
|
await Auth.tfa_disable(token);
|
||||||
|
jwt.logout();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<template v-if="user">
|
||||||
|
<div v-if="error !== ''" v-text="error"></div>
|
||||||
|
<div>User: {{ user.name }}</div>
|
||||||
|
<div>Signed in with {{ user.gitlab ? 'gitlab' : 'password' }}</div>
|
||||||
|
<template v-if="!user.gitlab">
|
||||||
|
<div>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
placeholder="Old password"
|
||||||
|
v-model="oldPw"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
placeholder="New password"
|
||||||
|
v-model="newPw"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
placeholder="Repeat new password"
|
||||||
|
v-model="newPw2"
|
||||||
|
/>
|
||||||
|
<button @click="changePw">Change</button>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div>
|
||||||
|
2 Factor authentication:
|
||||||
|
{{ user.tfaEnabled ? 'Enabled' : 'Disabled' }}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<a href="#" v-if="user.tfaEnabled" @click="tfaDisable">
|
||||||
|
Disable
|
||||||
|
</a>
|
||||||
|
<router-link to="/profile/2fa-enable" v-else>
|
||||||
|
Enable
|
||||||
|
</router-link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<div>
|
||||||
|
<a href="#" @click="logoutAll">Logout everywhere</a>
|
||||||
|
<a href="#" @click="deleteUser">Delete Account</a>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<div>Loading...</div>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped></style>
|
@ -2,9 +2,9 @@
|
|||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
import { Auth, isErrorResponse } from '@/api';
|
import { Auth, isErrorResponse } from '@/api';
|
||||||
|
|
||||||
let username = ref('');
|
const username = ref('');
|
||||||
let password = ref('');
|
const password = ref('');
|
||||||
let password2 = ref('');
|
const password2 = ref('');
|
||||||
const error = ref('');
|
const error = ref('');
|
||||||
|
|
||||||
async function signup() {
|
async function signup() {
|
||||||
|
89
frontend/src/views/TFAView.vue
Normal file
89
frontend/src/views/TFAView.vue
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, inject } from 'vue';
|
||||||
|
import { Auth, check_token, isErrorResponse, TokenInjectType } from '@/api';
|
||||||
|
|
||||||
|
enum state {
|
||||||
|
SELECT,
|
||||||
|
MAIL,
|
||||||
|
TOTP
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentState = ref<state>(state.SELECT);
|
||||||
|
|
||||||
|
const error = ref('');
|
||||||
|
const qrImage = ref('');
|
||||||
|
const secret = ref('');
|
||||||
|
const code = ref('');
|
||||||
|
|
||||||
|
const jwt = inject<TokenInjectType>('jwt') as TokenInjectType;
|
||||||
|
|
||||||
|
async function selectMail() {
|
||||||
|
const token = await check_token(jwt);
|
||||||
|
if (!token) return;
|
||||||
|
error.value = 'Working...';
|
||||||
|
const res = await Auth.tfa_setup(true, token);
|
||||||
|
if (isErrorResponse(res))
|
||||||
|
error.value = 'Failed to select 2fa type: ' + res.message;
|
||||||
|
else {
|
||||||
|
error.value = '';
|
||||||
|
currentState.value = state.MAIL;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function selectTotp() {
|
||||||
|
const token = await check_token(jwt);
|
||||||
|
if (!token) return;
|
||||||
|
error.value = 'Working...';
|
||||||
|
const res = await Auth.tfa_setup(false, token);
|
||||||
|
if (isErrorResponse(res))
|
||||||
|
error.value = 'Failed to select 2fa type: ' + res.message;
|
||||||
|
else {
|
||||||
|
qrImage.value = res.qrCode;
|
||||||
|
secret.value = res.secret;
|
||||||
|
error.value = '';
|
||||||
|
currentState.value = state.TOTP;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function submit() {
|
||||||
|
const token = await check_token(jwt);
|
||||||
|
if (!token) return;
|
||||||
|
error.value = 'Working...';
|
||||||
|
const res = await Auth.tfa_complete(
|
||||||
|
currentState.value === state.MAIL,
|
||||||
|
code.value,
|
||||||
|
token
|
||||||
|
);
|
||||||
|
if (isErrorResponse(res))
|
||||||
|
error.value = 'Failed to submit code: ' + res.message;
|
||||||
|
else jwt.logout();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div v-if="error !== ''" v-text="error"></div>
|
||||||
|
<template v-if="currentState === state.SELECT">
|
||||||
|
<div>Select 2 Factor authentication type:</div>
|
||||||
|
<div>
|
||||||
|
<button @click="selectMail">Mail</button>
|
||||||
|
<button @click="selectTotp">Google Authenticator</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template v-else-if="currentState === state.MAIL">
|
||||||
|
<div>Please enter the code you got by mail</div>
|
||||||
|
<input type="text" placeholder="Code" v-model="code" />
|
||||||
|
<button @click="submit()">Submit</button>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<img :src="qrImage" alt="QrCode" />
|
||||||
|
<details>
|
||||||
|
<summary>Show manual input code</summary>
|
||||||
|
{{ secret }}
|
||||||
|
</details>
|
||||||
|
<div>Please enter the current code</div>
|
||||||
|
<input type="text" placeholder="Code" v-model="code" />
|
||||||
|
<button @click="submit()">Submit</button>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped></style>
|
@ -18,7 +18,9 @@
|
|||||||
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
|
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
|
||||||
"test:e2e": "jest --config ./test/jest-e2e.json",
|
"test:e2e": "jest --config ./test/jest-e2e.json",
|
||||||
"webpack": "webpack --config webpack.config.ts",
|
"webpack": "webpack --config webpack.config.ts",
|
||||||
"genapi": "ts-node tools/apigen.ts"
|
"genapi": "ts-node tools/apigen.ts",
|
||||||
|
"updateDto": "cd dto && yarn build && cd .. && yarn add ./dto && cd frontend && yarn add ../dto",
|
||||||
|
"lint-fix-all": "yarn lint-fix && cd dto && yarn lint-fix && cd ../frontend && yarn lint --fix"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fastify/multipart": "^7.1.0",
|
"@fastify/multipart": "^7.1.0",
|
||||||
|
@ -15,7 +15,7 @@ import {
|
|||||||
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 { Responses, Requests } from 'dto';
|
import { Requests, Responses } from 'dto';
|
||||||
import { tfaTypes } from '../entities';
|
import { tfaTypes } from '../entities';
|
||||||
import { toDataURL } from 'qrcode';
|
import { toDataURL } from 'qrcode';
|
||||||
import * as base32 from 'thirty-two';
|
import * as base32 from 'thirty-two';
|
||||||
@ -48,12 +48,22 @@ export default class AuthController {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async tfa(
|
@Post('2fa/disable')
|
||||||
req,
|
async tfaDisable(
|
||||||
code: string,
|
@Request() req
|
||||||
type: tfaTypes
|
): Promise<Responses.Auth.RemoveTfaResponse> {
|
||||||
|
await this.authService.setTfaType(req.user, tfaTypes.NONE);
|
||||||
|
await this.authService.revokeAll(req.user);
|
||||||
|
return new Responses.Auth.RemoveTfaResponse();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post('2fa/complete')
|
||||||
|
async tfaMail(
|
||||||
|
@Request() req,
|
||||||
|
@Body(new ValidationPipe()) data: Requests.Auth.TfaComplete
|
||||||
): Promise<Responses.Auth.TfaCompletedResponse> {
|
): Promise<Responses.Auth.TfaCompletedResponse> {
|
||||||
if (!(await this.authService.verifyTfa(req.user, code, type))) {
|
const type = data.mail ? tfaTypes.EMAIL : tfaTypes.TOTP;
|
||||||
|
if (!(await this.authService.verifyTfa(req.user, data.code, type))) {
|
||||||
throw new UnauthorizedException('Incorrect 2fa');
|
throw new UnauthorizedException('Incorrect 2fa');
|
||||||
}
|
}
|
||||||
await this.authService.setTfaType(req.user, type);
|
await this.authService.setTfaType(req.user, type);
|
||||||
@ -61,23 +71,7 @@ export default class AuthController {
|
|||||||
return new Responses.Auth.TfaCompletedResponse();
|
return new Responses.Auth.TfaCompletedResponse();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post('2fa/complete/mail')
|
@Post('2fa/setup')
|
||||||
async tfaMail(
|
|
||||||
@Request() req,
|
|
||||||
@Body(new ValidationPipe()) data: Requests.Auth.TfaComplete
|
|
||||||
): Promise<Responses.Auth.TfaCompletedResponse> {
|
|
||||||
return await this.tfa(req, data.code, tfaTypes.EMAIL);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Post('2fa/complete/totp')
|
|
||||||
async tfaTotp(
|
|
||||||
@Request() req,
|
|
||||||
@Body(new ValidationPipe()) data: Requests.Auth.TfaComplete
|
|
||||||
): Promise<Responses.Auth.TfaCompletedResponse> {
|
|
||||||
return await this.tfa(req, data.code, tfaTypes.TOTP);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Get('2fa/setup')
|
|
||||||
async setupTotp(
|
async setupTotp(
|
||||||
@Request() req,
|
@Request() req,
|
||||||
@Body(new ValidationPipe()) data: Requests.Auth.TfaSetup
|
@Body(new ValidationPipe()) data: Requests.Auth.TfaSetup
|
||||||
@ -93,7 +87,7 @@ export default class AuthController {
|
|||||||
.encode(secret)
|
.encode(secret)
|
||||||
.toString()}&issuer=MFileserver`
|
.toString()}&issuer=MFileserver`
|
||||||
),
|
),
|
||||||
secret
|
base32.encode(secret).toString()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -134,4 +128,23 @@ export default class AuthController {
|
|||||||
url: `/set_token?token=${token}`
|
url: `/set_token?token=${token}`
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Post('change_password')
|
||||||
|
async changePassword(
|
||||||
|
@Request() req,
|
||||||
|
@Body(new ValidationPipe()) data: Requests.Auth.ChangePasswordRequest
|
||||||
|
): Promise<Responses.Auth.ChangePasswordResponse> {
|
||||||
|
await this.authService.changePassword(
|
||||||
|
req.user,
|
||||||
|
data.oldPassword,
|
||||||
|
data.newPassword
|
||||||
|
);
|
||||||
|
return new Responses.Auth.ChangePasswordResponse();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post('logout_all')
|
||||||
|
async logoutAll(@Request() req): Promise<Responses.Auth.LogoutAllResponse> {
|
||||||
|
await this.authService.revokeAll(req.user);
|
||||||
|
return new Responses.Auth.LogoutAllResponse();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
27
src/controller/user.ts
Normal file
27
src/controller/user.ts
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import { Controller, Get, Post, Request } from '@nestjs/common';
|
||||||
|
import { AuthService } from '../services/auth';
|
||||||
|
import { Responses } from 'dto';
|
||||||
|
|
||||||
|
@Controller('api/user')
|
||||||
|
export default class UserController {
|
||||||
|
constructor(private authService: AuthService) {}
|
||||||
|
|
||||||
|
@Get('info')
|
||||||
|
async getUserInfo(
|
||||||
|
@Request() req
|
||||||
|
): Promise<Responses.User.UserInfoResponse> {
|
||||||
|
return new Responses.User.UserInfoResponse(
|
||||||
|
req.user.name,
|
||||||
|
req.user.isGitlabUser,
|
||||||
|
this.authService.requiresTfa(req.user)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post('delete')
|
||||||
|
async deleteUser(
|
||||||
|
@Request() req
|
||||||
|
): Promise<Responses.User.DeleteUserResponse> {
|
||||||
|
await this.authService.deleteUser(req.user);
|
||||||
|
return new Responses.User.DeleteUserResponse();
|
||||||
|
}
|
||||||
|
}
|
@ -6,8 +6,9 @@ import {
|
|||||||
AuthLocalService,
|
AuthLocalService,
|
||||||
AuthJwtService
|
AuthJwtService
|
||||||
} from '../services/auth';
|
} from '../services/auth';
|
||||||
import AuthController from '../controller/auth';
|
|
||||||
import FileSystemService from '../services/filesystem';
|
import FileSystemService from '../services/filesystem';
|
||||||
|
import AuthController from '../controller/auth';
|
||||||
|
import UserController from '../controller/user';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [TypeOrmModule.forFeature([User, INode, JWTToken])],
|
imports: [TypeOrmModule.forFeature([User, INode, JWTToken])],
|
||||||
@ -17,6 +18,6 @@ import FileSystemService from '../services/filesystem';
|
|||||||
AuthJwtService,
|
AuthJwtService,
|
||||||
FileSystemService
|
FileSystemService
|
||||||
],
|
],
|
||||||
controllers: [AuthController]
|
controllers: [AuthController, UserController]
|
||||||
})
|
})
|
||||||
export default class AuthModule {}
|
export default class AuthModule {}
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import {
|
import {
|
||||||
BadRequestException,
|
BadRequestException,
|
||||||
|
ForbiddenException,
|
||||||
Injectable,
|
Injectable,
|
||||||
UnauthorizedException
|
UnauthorizedException
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
@ -312,6 +313,23 @@ export class AuthService {
|
|||||||
}
|
}
|
||||||
return info && info.username == user.name;
|
return info && info.username == user.name;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async deleteUser(user: User): Promise<void> {
|
||||||
|
await this.revokeAll(user);
|
||||||
|
await this.fsService.delete(await user.root, true);
|
||||||
|
await this.userRepo.remove(user);
|
||||||
|
}
|
||||||
|
|
||||||
|
async changePassword(
|
||||||
|
user: User,
|
||||||
|
oldPW: string,
|
||||||
|
newPw: string
|
||||||
|
): Promise<void> {
|
||||||
|
if (!(await argon2.verify(user.password, oldPW)))
|
||||||
|
throw new ForbiddenException('Old password is wrong');
|
||||||
|
user.password = await argon2.hash(newPw);
|
||||||
|
await this.revokeAll(await this.userRepo.save(user));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
|
@ -51,12 +51,12 @@ export default class FileSystemService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async delete(node: INode): Promise<void> {
|
async delete(node: INode, force = false): Promise<void> {
|
||||||
if (node.parentId == null)
|
if (node.parentId == null || force)
|
||||||
throw new BadRequestException("Can't delete root");
|
throw new BadRequestException("Can't delete root");
|
||||||
if (!node.isFile)
|
if (!node.isFile)
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
(await node.children).map((child) => this.delete(child))
|
(await node.children).map((child) => this.delete(child, force))
|
||||||
);
|
);
|
||||||
else
|
else
|
||||||
unlink(`files/${node.id}`, (err) => {
|
unlink(`files/${node.id}`, (err) => {
|
||||||
|
Loading…
Reference in New Issue
Block a user