From 0439b2074049eab7a933b6cfa35389b54b5d30d4 Mon Sep 17 00:00:00 2001 From: KhashayarKhm Date: Tue, 29 Apr 2025 14:02:40 +0330 Subject: [PATCH] feat(frontend): include OTP components to login and settings pages - add OTP APIs - add OTP prompt to Login page - add Profile2FA to Profile page --- frontend/src/api/users.ts | 42 +++++++++++++++++++++++++ frontend/src/types/user.d.ts | 5 +++ frontend/src/utils/auth.ts | 29 +++++++++++++++-- frontend/src/views/Login.vue | 30 +++++++++++++++--- frontend/src/views/settings/Profile.vue | 3 ++ 5 files changed, 102 insertions(+), 7 deletions(-) diff --git a/frontend/src/api/users.ts b/frontend/src/api/users.ts index 78096b49..e863bd0a 100644 --- a/frontend/src/api/users.ts +++ b/frontend/src/api/users.ts @@ -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, + }, + }); +} diff --git a/frontend/src/types/user.d.ts b/frontend/src/types/user.d.ts index b81806fc..64e10225 100644 --- a/frontend/src/types/user.d.ts +++ b/frontend/src/types/user.d.ts @@ -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; +} diff --git a/frontend/src/utils/auth.ts b/frontend/src/utils/auth.ts index b868d90f..72e3ad3a 100644 --- a/frontend/src/utils/auth.ts +++ b/frontend/src/utils/auth.ts @@ -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 { + 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}`, diff --git a/frontend/src/views/Login.vue b/frontend/src/views/Login.vue index 5804789a..cf62bfad 100644 --- a/frontend/src/views/Login.vue +++ b/frontend/src/views/Login.vue @@ -1,5 +1,6 @@