Skip to content

Commit

Permalink
feat: improve error handling for login and sync process (#51)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
IZUMI-Zu authored Jan 28, 2025
1 parent 398eb5e commit 722e1fe
Show file tree
Hide file tree
Showing 3 changed files with 125 additions and 100 deletions.
8 changes: 7 additions & 1 deletion CasdoorLoginPage.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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"),
Expand Down
130 changes: 75 additions & 55 deletions api.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,86 +13,52 @@
// limitations under the License.

import i18next from "i18next";
import AsyncStorage from "@react-native-async-storage/async-storage";

const TIMEOUT_MS = 5000;

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"));
Expand All @@ -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);
};
87 changes: 43 additions & 44 deletions syncLogic.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();

}

0 comments on commit 722e1fe

Please sign in to comment.