feat(frontend): include OTP components to login and settings pages
- add OTP APIs - add OTP prompt to Login page - add Profile2FA to Profile pagepull/3885/head
parent
a18583640c
commit
0439b20740
|
@ -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,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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}`,
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -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();
|
||||
|
|
Loading…
Reference in New Issue