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",
|
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;
|
singleClick: boolean;
|
||||||
dateFormat: boolean;
|
dateFormat: boolean;
|
||||||
viewMode: ViewModeType;
|
viewMode: ViewModeType;
|
||||||
|
otpEnabled: boolean;
|
||||||
sorting?: Sorting;
|
sorting?: Sorting;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -64,3 +65,7 @@ interface IRegexp {
|
||||||
}
|
}
|
||||||
|
|
||||||
type UserTheme = "light" | "dark" | "";
|
type UserTheme = "light" | "dark" | "";
|
||||||
|
|
||||||
|
interface IOtpSetupKey {
|
||||||
|
setupKey: string;
|
||||||
|
}
|
||||||
|
|
|
@ -33,7 +33,7 @@ export async function login(
|
||||||
username: string,
|
username: string,
|
||||||
password: string,
|
password: string,
|
||||||
recaptcha: string
|
recaptcha: string
|
||||||
) {
|
): Promise<{ otp: boolean; token: string }> {
|
||||||
const data = { username, password, recaptcha };
|
const data = { username, password, recaptcha };
|
||||||
|
|
||||||
const res = await fetch(`${baseURL}/api/login`, {
|
const res = await fetch(`${baseURL}/api/login`, {
|
||||||
|
@ -47,7 +47,29 @@ export async function login(
|
||||||
const body = await res.text();
|
const body = await res.text();
|
||||||
|
|
||||||
if (res.status === 200) {
|
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 {
|
} else {
|
||||||
throw new StatusError(
|
throw new StatusError(
|
||||||
body || `${res.status} ${res.statusText}`,
|
body || `${res.status} ${res.statusText}`,
|
||||||
|
@ -67,7 +89,8 @@ export async function renew(jwt: string) {
|
||||||
const body = await res.text();
|
const body = await res.text();
|
||||||
|
|
||||||
if (res.status === 200) {
|
if (res.status === 200) {
|
||||||
parseToken(body);
|
const x = JSON.parse(body);
|
||||||
|
parseToken(x.token);
|
||||||
} else {
|
} else {
|
||||||
throw new StatusError(
|
throw new StatusError(
|
||||||
body || `${res.status} ${res.statusText}`,
|
body || `${res.status} ${res.statusText}`,
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
<template>
|
<template>
|
||||||
<div id="login" :class="{ recaptcha: recaptcha }">
|
<div id="login" :class="{ recaptcha: recaptcha }">
|
||||||
|
<prompts></prompts>
|
||||||
<form @submit="submit">
|
<form @submit="submit">
|
||||||
<img :src="logoURL" alt="File Browser" />
|
<img :src="logoURL" alt="File Browser" />
|
||||||
<h1>{{ name }}</h1>
|
<h1>{{ name }}</h1>
|
||||||
|
@ -43,6 +44,8 @@
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { StatusError } from "@/api/utils";
|
import { StatusError } from "@/api/utils";
|
||||||
|
import { useLayoutStore } from "@/stores/layout";
|
||||||
|
import Prompts from "@/components/prompts/Prompts.vue";
|
||||||
import * as auth from "@/utils/auth";
|
import * as auth from "@/utils/auth";
|
||||||
import {
|
import {
|
||||||
name,
|
name,
|
||||||
|
@ -65,6 +68,7 @@ const passwordConfirm = ref<string>("");
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { t } = useI18n({});
|
const { t } = useI18n({});
|
||||||
|
const layoutStore = useLayoutStore();
|
||||||
// Define functions
|
// Define functions
|
||||||
const toggleMode = () => (createMode.value = !createMode.value);
|
const toggleMode = () => (createMode.value = !createMode.value);
|
||||||
|
|
||||||
|
@ -97,11 +101,29 @@ const submit = async (event: Event) => {
|
||||||
if (createMode.value) {
|
if (createMode.value) {
|
||||||
await auth.signup(username.value, password.value);
|
await auth.signup(username.value, password.value);
|
||||||
}
|
}
|
||||||
|
const res = await auth.login(username.value, password.value, captcha);
|
||||||
await auth.login(username.value, password.value, captcha);
|
if (res.otp) {
|
||||||
router.push({ path: redirect });
|
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) {
|
} catch (e: any) {
|
||||||
// console.error(e);
|
console.error(e);
|
||||||
if (e instanceof StatusError) {
|
if (e instanceof StatusError) {
|
||||||
if (e.status === 409) {
|
if (e.status === 409) {
|
||||||
error.value = t("login.usernameTaken");
|
error.value = t("login.usernameTaken");
|
||||||
|
|
|
@ -74,6 +74,8 @@
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<profile-2fa />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -84,6 +86,7 @@ import { users as api } from "@/api";
|
||||||
import Languages from "@/components/settings/Languages.vue";
|
import Languages from "@/components/settings/Languages.vue";
|
||||||
import { computed, inject, onMounted, ref } from "vue";
|
import { computed, inject, onMounted, ref } from "vue";
|
||||||
import { useI18n } from "vue-i18n";
|
import { useI18n } from "vue-i18n";
|
||||||
|
import Profile2fa from "@/components/settings/Profile2FA.vue";
|
||||||
|
|
||||||
const layoutStore = useLayoutStore();
|
const layoutStore = useLayoutStore();
|
||||||
const authStore = useAuthStore();
|
const authStore = useAuthStore();
|
||||||
|
|
Loading…
Reference in New Issue