From 722e1fe362061ab53f6fa56b560ca7e9177b3c62 Mon Sep 17 00:00:00 2001 From: IZUMI-Zu <274620705z@gmail.com> Date: Tue, 28 Jan 2025 08:50:12 +0800 Subject: [PATCH] feat: improve error handling for login and sync process (#51) * feat: improve error handling for login and sync process * feat: enhance token validation and localization support * refactor: extract common fetch logic with timeout and improve API methods --- CasdoorLoginPage.js | 8 ++- api.js | 130 +++++++++++++++++++++++++------------------- syncLogic.js | 87 +++++++++++++++-------------- 3 files changed, 125 insertions(+), 100 deletions(-) diff --git a/CasdoorLoginPage.js b/CasdoorLoginPage.js index c2a7ce5..24bc916 100644 --- a/CasdoorLoginPage.js +++ b/CasdoorLoginPage.js @@ -27,6 +27,7 @@ import DefaultCasdoorSdkConfig from "./DefaultCasdoorSdkConfig"; import {useTranslation} from "react-i18next"; import {useLanguageSync} from "./useLanguageSync"; import {useEditAccount} from "./useAccountStore"; +import * as api from "./api"; let sdk = null; @@ -108,17 +109,22 @@ function CasdoorLoginPage({onWebviewClose, initialMethod}) { } }; - const handleQRLogin = (loginInfo) => { + const handleQRLogin = async(loginInfo) => { setServerUrl(loginInfo.serverUrl); setClientId(""); setAppName(""); setOrganizationName(""); initSdk(); + try { const accessToken = loginInfo.accessToken; const userInfo = sdk.JwtDecode(accessToken); + + await api.validateToken(loginInfo.serverUrl, accessToken); + setToken(accessToken); setUserInfo(userInfo); + notify("success", { params: { title: t("common.success"), diff --git a/api.js b/api.js index d8b9fcb..e57622d 100644 --- a/api.js +++ b/api.js @@ -13,6 +13,7 @@ // limitations under the License. import i18next from "i18next"; +import AsyncStorage from "@react-native-async-storage/async-storage"; const TIMEOUT_MS = 5000; @@ -20,79 +21,44 @@ const timeout = (ms) => { return new Promise((_, reject) => setTimeout(() => reject(new Error("Request timed out")), ms)); }; -export const getMfaAccounts = async(serverUrl, owner, name, token, timeoutMs = TIMEOUT_MS) => { +const fetchWithTimeout = async(url, options = {}, timeoutMs = TIMEOUT_MS) => { const controller = new AbortController(); const {signal} = controller; try { - const result = await Promise.race([ - fetch(`${serverUrl}/api/get-user?id=${owner}/${encodeURIComponent(name)}&access_token=${token}`, { - method: "GET", - signal, - }), - timeout(timeoutMs), - ]); + // default headers + const defaultHeaders = { + "Accept-Language": await AsyncStorage.getItem("language"), + "Content-Type": "application/json", + }; - const res = await result.json(); + const {token, ...fetchOptions} = options; - // Check the response status and message - if (res.status === "error") { - throw new Error(res.msg); + if (token) { + defaultHeaders.Authorization = `Bearer ${token}`; } - return { - updatedTime: res.data.updatedTime, - mfaAccounts: res.data.mfaAccounts || [], + const finalOptions = { + ...fetchOptions, + headers: { + ...defaultHeaders, + ...fetchOptions.headers, + }, + signal, }; - } catch (error) { - if (error.name === "AbortError") { - throw new Error(i18next.t("api.Request timed out")); - } - throw error; - } finally { - controller.abort(); - } -}; -export const updateMfaAccounts = async(serverUrl, owner, name, newMfaAccounts, token, timeoutMs = TIMEOUT_MS) => { - const controller = new AbortController(); - const {signal} = controller; - - try { - const getUserResult = await Promise.race([ - fetch(`${serverUrl}/api/get-user?id=${owner}/${encodeURIComponent(name)}&access_token=${token}`, { - method: "GET", - Authorization: `Bearer ${token}`, - signal, - }), - timeout(timeoutMs), - ]); - - const userData = await getUserResult.json(); - - userData.data.mfaAccounts = newMfaAccounts; - - const updateResult = await Promise.race([ - fetch(`${serverUrl}/api/update-user?id=${owner}/${encodeURIComponent(name)}&access_token=${token}`, { - method: "POST", - Authorization: `Bearer ${token}`, - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify(userData.data), - signal, - }), + const result = await Promise.race([ + fetch(url, finalOptions), timeout(timeoutMs), ]); - const res = await updateResult.json(); + const res = await result.json(); - // Check the response status and message if (res.status === "error") { throw new Error(res.msg); } - return {status: res.status, data: res.data}; + return res; } catch (error) { if (error.name === "AbortError") { throw new Error(i18next.t("api.Request timed out")); @@ -102,3 +68,57 @@ export const updateMfaAccounts = async(serverUrl, owner, name, newMfaAccounts, t controller.abort(); } }; + +export const getMfaAccounts = async(serverUrl, owner, name, token, timeoutMs = TIMEOUT_MS) => { + const res = await fetchWithTimeout( + `${serverUrl}/api/get-user?id=${owner}/${encodeURIComponent(name)}`, + { + method: "GET", + token, + }, + timeoutMs + ); + + return { + updatedTime: res.data.updatedTime, + mfaAccounts: res.data.mfaAccounts || [], + }; +}; + +export const updateMfaAccounts = async(serverUrl, owner, name, newMfaAccounts, token, timeoutMs = TIMEOUT_MS) => { + const userData = await fetchWithTimeout( + `${serverUrl}/api/get-user?id=${owner}/${encodeURIComponent(name)}`, + { + method: "GET", + token, + }, + timeoutMs + ); + + userData.data.mfaAccounts = newMfaAccounts; + + const res = await fetchWithTimeout( + `${serverUrl}/api/update-user?id=${owner}/${encodeURIComponent(name)}`, + { + method: "POST", + token, + body: JSON.stringify(userData.data), + }, + timeoutMs + ); + + return {status: res.status, data: res.data}; +}; + +export const validateToken = async(serverUrl, token, timeoutMs = TIMEOUT_MS) => { + const res = await fetchWithTimeout( + `${serverUrl}/api/userinfo`, + { + method: "GET", + token, + }, + timeoutMs + ); + + return !!(res.sub && res.name && res.preferred_username); +}; diff --git a/syncLogic.js b/syncLogic.js index d6b3f2c..0f69df5 100644 --- a/syncLogic.js +++ b/syncLogic.js @@ -141,63 +141,62 @@ function mergeAccounts(localAccounts, serverAccounts, serverTimestamp) { } export async function syncWithCloud(db, userInfo, serverUrl, token) { - try { - const localAccounts = await getLocalAccounts(db); + const localAccounts = await getLocalAccounts(db); - const {updatedTime, mfaAccounts: serverAccounts} = await api.getMfaAccounts( - serverUrl, - userInfo.owner, - userInfo.name, - token - ); + try { + await api.validateToken(serverUrl, token); + } catch (error) { + handleTokenExpiration(); + throw error; + } - const mergedAccounts = mergeAccounts(localAccounts, serverAccounts, updatedTime); + const {updatedTime, mfaAccounts: serverAccounts} = await api.getMfaAccounts( + serverUrl, + userInfo.owner, + userInfo.name, + token + ); - await updateLocalDatabase(db, mergedAccounts); + const mergedAccounts = mergeAccounts(localAccounts, serverAccounts, updatedTime); - const accountsToSync = mergedAccounts - .filter(account => account.deletedAt === null || account.deletedAt === undefined) - .map(account => { - const {issuer, accountName, secretKey, origin} = account; - const accountToSync = {issuer, accountName, secretKey}; - if (origin !== null) { - accountToSync.origin = origin; - } - return accountToSync; - }); + await updateLocalDatabase(db, mergedAccounts); - const serverAccountsStringified = serverAccounts.map(account => { + const accountsToSync = mergedAccounts + .filter(account => account.deletedAt === null || account.deletedAt === undefined) + .map(account => { const {issuer, accountName, secretKey, origin} = account; - const accountStringified = {issuer, accountName, secretKey}; + const accountToSync = {issuer, accountName, secretKey}; if (origin !== null) { - accountStringified.origin = origin; + accountToSync.origin = origin; } - return JSON.stringify(accountStringified); + return accountToSync; }); - const accountsToSyncStringified = accountsToSync.map(account => JSON.stringify(account)); - - if (JSON.stringify(accountsToSyncStringified.sort()) !== JSON.stringify(serverAccountsStringified.sort())) { - const {status} = await api.updateMfaAccounts( - serverUrl, - userInfo.owner, - userInfo.name, - accountsToSync, - token - ); - - if (status !== "ok") { - throw new Error(i18next.t("syncLogic.Sync failed")); - } + const serverAccountsStringified = serverAccounts.map(account => { + const {issuer, accountName, secretKey, origin} = account; + const accountStringified = {issuer, accountName, secretKey}; + if (origin !== null) { + accountStringified.origin = origin; } + return JSON.stringify(accountStringified); + }); - await db.update(schema.accounts).set({syncAt: new Date()}).run(); + const accountsToSyncStringified = accountsToSync.map(account => JSON.stringify(account)); - } catch (error) { - if (error.message.includes("Access token has expired")) { - handleTokenExpiration(); - throw new Error(i18next.t("syncLogic.Access token has expired, please login again")); + if (JSON.stringify(accountsToSyncStringified.sort()) !== JSON.stringify(serverAccountsStringified.sort())) { + const {status} = await api.updateMfaAccounts( + serverUrl, + userInfo.owner, + userInfo.name, + accountsToSync, + token + ); + + if (status !== "ok") { + throw new Error(i18next.t("syncLogic.Sync failed")); } - throw error; } + + await db.update(schema.accounts).set({syncAt: new Date()}).run(); + }