Created Profile page in frontend
This commit is contained in:
		@@ -27,9 +27,13 @@ provide<TokenInjectType>('jwt', {
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
	<nav>
 | 
			
		||||
		<router-link to="/login" v-if="jwt != null" @click="logout()">
 | 
			
		||||
			Logout
 | 
			
		||||
		</router-link>
 | 
			
		||||
		<template v-if="jwt != null">
 | 
			
		||||
			<router-link to="/">Files</router-link>
 | 
			
		||||
			<span style="margin-left: 2em" />
 | 
			
		||||
			<router-link to="/profile">Profile</router-link>
 | 
			
		||||
			<span style="margin-left: 2em" />
 | 
			
		||||
			<router-link to="/login" @click="logout()">Logout</router-link>
 | 
			
		||||
		</template>
 | 
			
		||||
	</nav>
 | 
			
		||||
	<router-view />
 | 
			
		||||
</template>
 | 
			
		||||
 
 | 
			
		||||
@@ -28,3 +28,66 @@ export const refresh_token = (
 | 
			
		||||
	token: string
 | 
			
		||||
): Promise<Responses.Auth.RefreshResponse | Responses.ErrorResponse> =>
 | 
			
		||||
	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 * as Auth from './auth';
 | 
			
		||||
export * as FS from './fs';
 | 
			
		||||
export * as User from './user';
 | 
			
		||||
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 FSView from '@/views/FSView.vue';
 | 
			
		||||
import SetTokenView from '@/views/SetTokenView.vue';
 | 
			
		||||
import ProfileView from '@/views/ProfileView.vue';
 | 
			
		||||
import TFAView from '@/views/TFAView.vue';
 | 
			
		||||
 | 
			
		||||
const routes: Array<RouteRecordRaw> = [
 | 
			
		||||
	{
 | 
			
		||||
@@ -12,9 +14,18 @@ const routes: Array<RouteRecordRaw> = [
 | 
			
		||||
		name: 'home',
 | 
			
		||||
		component: HomeView
 | 
			
		||||
	},
 | 
			
		||||
	{
 | 
			
		||||
		path: '/profile',
 | 
			
		||||
		name: 'profile',
 | 
			
		||||
		component: ProfileView
 | 
			
		||||
	},
 | 
			
		||||
	{
 | 
			
		||||
		path: '/profile/2fa-enable',
 | 
			
		||||
		name: '2fa',
 | 
			
		||||
		component: TFAView
 | 
			
		||||
	},
 | 
			
		||||
	{
 | 
			
		||||
		path: '/about',
 | 
			
		||||
		name: 'about',
 | 
			
		||||
		component: AboutView
 | 
			
		||||
	},
 | 
			
		||||
	{
 | 
			
		||||
@@ -27,14 +38,15 @@ const routes: Array<RouteRecordRaw> = [
 | 
			
		||||
		name: 'signup',
 | 
			
		||||
		component: SignupView
 | 
			
		||||
	},
 | 
			
		||||
	{
 | 
			
		||||
		path: '/set_token',
 | 
			
		||||
		component: SetTokenView
 | 
			
		||||
	},
 | 
			
		||||
	{
 | 
			
		||||
		path: '/fs/:node_id',
 | 
			
		||||
		name: 'fs',
 | 
			
		||||
		component: FSView
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	{
 | 
			
		||||
		path: '/set_token',
 | 
			
		||||
		component: SetTokenView
 | 
			
		||||
	}
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -13,7 +13,7 @@ async function start_redirect() {
 | 
			
		||||
	if (!token) return;
 | 
			
		||||
	const root = await FS.get_root(token);
 | 
			
		||||
	if (isErrorResponse(root)) return jwt.logout();
 | 
			
		||||
	await router.push({
 | 
			
		||||
	await router.replace({
 | 
			
		||||
		name: 'fs',
 | 
			
		||||
		params: { node_id: root.rootId }
 | 
			
		||||
	});
 | 
			
		||||
 
 | 
			
		||||
@@ -48,14 +48,14 @@ async function login() {
 | 
			
		||||
	<template v-if="!requestOtp">
 | 
			
		||||
		<input type="email" placeholder="Email" v-model="username" />
 | 
			
		||||
		<input type="password" placeholder="Password" v-model="password" />
 | 
			
		||||
		<a href="/api/auth/gitlab">Login with gitlab</a>
 | 
			
		||||
		<router-link to="signup">Signup instead?</router-link>
 | 
			
		||||
	</template>
 | 
			
		||||
	<template v-else>
 | 
			
		||||
		<div>Please input your 2 factor authentication code</div>
 | 
			
		||||
		<input type="text" placeholder="Code" v-model="otp" />
 | 
			
		||||
	</template>
 | 
			
		||||
	<button @click="login()">Login</button>
 | 
			
		||||
	<a href="/api/auth/gitlab">Login with gitlab</a>
 | 
			
		||||
	<router-link to="signup">Signup instead?</router-link>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<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 { Auth, isErrorResponse } from '@/api';
 | 
			
		||||
 | 
			
		||||
let username = ref('');
 | 
			
		||||
let password = ref('');
 | 
			
		||||
let password2 = ref('');
 | 
			
		||||
const username = ref('');
 | 
			
		||||
const password = ref('');
 | 
			
		||||
const password2 = ref('');
 | 
			
		||||
const error = ref('');
 | 
			
		||||
 | 
			
		||||
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>
 | 
			
		||||
		Reference in New Issue
	
	Block a user