feat(frontend): include OTP components to login and settings pages

- add OTP APIs
- add OTP prompt to Login page
- add Profile2FA to Profile page
pull/3885/head
KhashayarKhm 2025-04-29 14:02:40 +03:30
parent a18583640c
commit 0439b20740
5 changed files with 102 additions and 7 deletions

View File

@ -41,3 +41,45 @@ export async function remove(id: number) {
method: "DELETE",
});
}
export async function enableOTP(id: number, password: string) {
const res = await fetchURL(`/api/users/${id}/otp`, {
method: "POST",
body: JSON.stringify({
password,
}),
});
const payload: IOtpSetupKey = await res.json();
return payload;
}
export async function checkOtp(id: number, code: string) {
return fetchURL(`/api/users/${id}/otp/check`, {
method: "POST",
body: JSON.stringify({
code,
}),
});
}
export async function getOtpInfo(id: number, code: string) {
const res = await fetchURL(`/api/users/${id}/otp`, {
method: "GET",
headers: {
"X-TOTP-CODE": code,
},
});
const payload: IOtpSetupKey = await res.json();
return payload;
}
export async function disableOtp(id: number, code: string) {
return fetchURL(`/api/users/${id}/otp`, {
method: "DELETE",
headers: {
"X-TOTP-CODE": code,
},
});
}

View File

@ -12,6 +12,7 @@ interface IUser {
singleClick: boolean;
dateFormat: boolean;
viewMode: ViewModeType;
otpEnabled: boolean;
sorting?: Sorting;
}
@ -64,3 +65,7 @@ interface IRegexp {
}
type UserTheme = "light" | "dark" | "";
interface IOtpSetupKey {
setupKey: string;
}

View File

@ -33,7 +33,7 @@ export async function login(
username: string,
password: string,
recaptcha: string
) {
): Promise<{ otp: boolean; token: string }> {
const data = { username, password, recaptcha };
const res = await fetch(`${baseURL}/api/login`, {
@ -47,7 +47,29 @@ export async function login(
const body = await res.text();
if (res.status === 200) {
parseToken(body);
const payload = JSON.parse(body);
return payload;
} else {
throw new StatusError(
body || `${res.status} ${res.statusText}`,
res.status
);
}
}
export async function verifyTOTP(code: string, token: string): Promise<void> {
const res = await fetch(`${baseURL}/api/login/otp`, {
method: "POST",
headers: {
"X-TOTP-CODE": code,
"X-TOTP-Auth": token,
},
});
const body = await res.text();
if (res.status === 200) {
const payload = JSON.parse(body);
parseToken(payload.token);
} else {
throw new StatusError(
body || `${res.status} ${res.statusText}`,
@ -67,7 +89,8 @@ export async function renew(jwt: string) {
const body = await res.text();
if (res.status === 200) {
parseToken(body);
const x = JSON.parse(body);
parseToken(x.token);
} else {
throw new StatusError(
body || `${res.status} ${res.statusText}`,

View File

@ -1,5 +1,6 @@
<template>
<div id="login" :class="{ recaptcha: recaptcha }">
<prompts></prompts>
<form @submit="submit">
<img :src="logoURL" alt="File Browser" />
<h1>{{ name }}</h1>
@ -43,6 +44,8 @@
<script setup lang="ts">
import { StatusError } from "@/api/utils";
import { useLayoutStore } from "@/stores/layout";
import Prompts from "@/components/prompts/Prompts.vue";
import * as auth from "@/utils/auth";
import {
name,
@ -65,6 +68,7 @@ const passwordConfirm = ref<string>("");
const route = useRoute();
const router = useRouter();
const { t } = useI18n({});
const layoutStore = useLayoutStore();
// Define functions
const toggleMode = () => (createMode.value = !createMode.value);
@ -97,11 +101,29 @@ const submit = async (event: Event) => {
if (createMode.value) {
await auth.signup(username.value, password.value);
}
await auth.login(username.value, password.value, captcha);
router.push({ path: redirect });
const res = await auth.login(username.value, password.value, captcha);
if (res.otp) {
layoutStore.showHover({
prompt: "otp",
confirm: async (code: string) => {
try {
await auth.verifyTOTP(code, res.token);
router.push({ path: redirect });
} catch (e: any) {
if (e instanceof StatusError) {
error.value = t("otp.verificationFailed");
} else {
$showError(e);
}
}
},
});
} else {
auth.parseToken(res.token);
router.push({ path: redirect });
}
} catch (e: any) {
// console.error(e);
console.error(e);
if (e instanceof StatusError) {
if (e.status === 409) {
error.value = t("login.usernameTaken");

View File

@ -74,6 +74,8 @@
</div>
</form>
</div>
<profile-2fa />
</div>
</template>
@ -84,6 +86,7 @@ import { users as api } from "@/api";
import Languages from "@/components/settings/Languages.vue";
import { computed, inject, onMounted, ref } from "vue";
import { useI18n } from "vue-i18n";
import Profile2fa from "@/components/settings/Profile2FA.vue";
const layoutStore = useLayoutStore();
const authStore = useAuthStore();