feat: support importing entried from other authenticators (#30)
This commit is contained in:
parent
e3f7ab5203
commit
308b546395
29
HomePage.js
29
HomePage.js
|
@ -26,6 +26,7 @@ import EnterAccountDetails from "./EnterAccountDetails";
|
|||
import ScanQRCode from "./ScanQRCode";
|
||||
import EditAccountDetails from "./EditAccountDetails";
|
||||
import AvatarWithFallback from "./AvatarWithFallback";
|
||||
import {useImportManager} from "./ImportManager";
|
||||
import useStore from "./useStorage";
|
||||
import {calculateCountdown} from "./totpUtil";
|
||||
import {generateToken, validateSecret} from "./totpUtil";
|
||||
|
@ -57,6 +58,16 @@ export default function HomePage() {
|
|||
const {setAccount, updateAccount, insertAccount, insertAccounts, deleteAccount} = useEditAccount();
|
||||
const {notify} = useNotifications();
|
||||
|
||||
const {showImportOptions} = useImportManager((data) => {
|
||||
handleAddAccount(data);
|
||||
}, (err) => {
|
||||
notify("error", {
|
||||
params: {title: "Import error", description: err.message},
|
||||
});
|
||||
}, () => {
|
||||
setShowScanner(true);
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
refreshAccounts();
|
||||
}, []);
|
||||
|
@ -107,7 +118,7 @@ export default function HomePage() {
|
|||
|
||||
const handleAddAccount = async(accountDataInput) => {
|
||||
if (Array.isArray(accountDataInput)) {
|
||||
insertAccounts(accountDataInput);
|
||||
await insertAccounts(accountDataInput);
|
||||
} else {
|
||||
await setAccount(accountDataInput);
|
||||
await insertAccount();
|
||||
|
@ -175,6 +186,11 @@ export default function HomePage() {
|
|||
closeOptions();
|
||||
};
|
||||
|
||||
const openImportAccountModal = () => {
|
||||
showImportOptions();
|
||||
closeOptions();
|
||||
};
|
||||
|
||||
const closeEnterAccountModal = () => setShowEnterAccountModal(false);
|
||||
|
||||
const closeSwipeableMenu = () => {
|
||||
|
@ -291,11 +307,11 @@ export default function HomePage() {
|
|||
padding: 20,
|
||||
borderRadius: 10,
|
||||
width: 300,
|
||||
height: 150,
|
||||
height: 225,
|
||||
position: "absolute",
|
||||
top: "50%",
|
||||
left: "50%",
|
||||
transform: [{translateX: -150}, {translateY: -75}],
|
||||
transform: [{translateX: -150}, {translateY: -112.5}],
|
||||
}}
|
||||
>
|
||||
<TouchableOpacity
|
||||
|
@ -312,6 +328,13 @@ export default function HomePage() {
|
|||
<IconButton icon={"keyboard"} size={35} />
|
||||
<Text style={{fontSize: 18}}>Enter Secret code</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
style={{flexDirection: "row", alignItems: "center", marginTop: 10}}
|
||||
onPress={openImportAccountModal}
|
||||
>
|
||||
<IconButton icon={"import"} size={35} />
|
||||
<Text style={{fontSize: 18}}>Import from other app</Text>
|
||||
</TouchableOpacity>
|
||||
</Modal>
|
||||
</Portal>
|
||||
|
||||
|
|
|
@ -0,0 +1,56 @@
|
|||
// Copyright 2024 The Casdoor Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import {useActionSheet} from "@expo/react-native-action-sheet";
|
||||
import {importFromMSAuth} from "./MSAuthImportLogic";
|
||||
|
||||
const importApps = [
|
||||
{name: "Google Authenticator", useScanner: true},
|
||||
{name: "Microsoft Authenticator", importFunction: importFromMSAuth},
|
||||
];
|
||||
|
||||
export const useImportManager = (onImportComplete, onError, onOpenScanner) => {
|
||||
const {showActionSheetWithOptions} = useActionSheet();
|
||||
|
||||
const showImportOptions = () => {
|
||||
const options = [...importApps.map(app => app.name), "Cancel"];
|
||||
const cancelButtonIndex = options.length - 1;
|
||||
|
||||
showActionSheetWithOptions(
|
||||
{
|
||||
options,
|
||||
cancelButtonIndex,
|
||||
title: "Select app to import from",
|
||||
},
|
||||
(selectedIndex) => {
|
||||
if (selectedIndex !== cancelButtonIndex) {
|
||||
const selectedApp = importApps[selectedIndex];
|
||||
if (selectedApp.useScanner) {
|
||||
onOpenScanner();
|
||||
} else if (selectedApp.importFunction) {
|
||||
selectedApp.importFunction()
|
||||
.then(result => {
|
||||
if (result) {onImportComplete(result);}
|
||||
})
|
||||
.catch(onError);
|
||||
} else {
|
||||
onError(new Error(`Import function not implemented for ${selectedApp.name}`));
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
return {showImportOptions};
|
||||
};
|
|
@ -0,0 +1,84 @@
|
|||
// Copyright 2024 The Casdoor Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import * as DocumentPicker from "expo-document-picker";
|
||||
import * as FileSystem from "expo-file-system";
|
||||
import {openDatabaseSync} from "expo-sqlite/next";
|
||||
|
||||
const SQLITE_DIR = `${FileSystem.documentDirectory}SQLite`;
|
||||
|
||||
const getRandomDBName = () => {
|
||||
const randomString = Math.random().toString(36).substring(2, 15);
|
||||
const timestamp = Date.now();
|
||||
return `${randomString}_${timestamp}.db`;
|
||||
};
|
||||
|
||||
const createDirectory = async(dir) => {
|
||||
try {
|
||||
if (!(await FileSystem.getInfoAsync(dir)).exists) {
|
||||
await FileSystem.makeDirectoryAsync(dir, {intermediates: true});
|
||||
}
|
||||
} catch (error) {
|
||||
throw new Error(`Error creating directory: ${error.message}`);
|
||||
}
|
||||
};
|
||||
|
||||
const queryMicrosoftAuthenticatorDatabase = async(db) => {
|
||||
return await db.getAllAsync("SELECT name, username, oath_secret_key FROM accounts WHERE account_type = 0");
|
||||
};
|
||||
|
||||
const formatMicrosoftAuthenticatorData = (rows) => {
|
||||
return rows.map(row => {
|
||||
const {name, username, oath_secret_key} = row;
|
||||
return {issuer: name, accountName: username, secretKey: oath_secret_key};
|
||||
});
|
||||
};
|
||||
|
||||
export const importFromMSAuth = async() => {
|
||||
const dbName = getRandomDBName();
|
||||
const internalDbName = `${SQLITE_DIR}/${dbName}`;
|
||||
try {
|
||||
const result = await DocumentPicker.getDocumentAsync({
|
||||
multiple: false,
|
||||
copyToCacheDirectory: false,
|
||||
});
|
||||
|
||||
if (!result.canceled) {
|
||||
const file = result.assets[0];
|
||||
if ((await FileSystem.getInfoAsync(file.uri)).exists) {
|
||||
await createDirectory(SQLITE_DIR);
|
||||
await FileSystem.copyAsync({from: file.uri, to: internalDbName});
|
||||
|
||||
try {
|
||||
const db = openDatabaseSync(dbName);
|
||||
|
||||
const rows = await queryMicrosoftAuthenticatorDatabase(db);
|
||||
if (rows.length === 0) {
|
||||
throw new Error("No data found in Microsoft Authenticator database");
|
||||
}
|
||||
return formatMicrosoftAuthenticatorData(rows);
|
||||
} catch (dbError) {
|
||||
if (dbError.message.includes("file is not a database")) {
|
||||
throw new Error("file is not a database");
|
||||
}
|
||||
throw new Error(dbError.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
throw new Error(`Error importing from Microsoft Authenticator: ${error.message}`);
|
||||
} finally {
|
||||
await FileSystem.deleteAsync(internalDbName, {idempotent: true});
|
||||
}
|
||||
};
|
File diff suppressed because it is too large
Load Diff
|
@ -21,18 +21,19 @@
|
|||
"casdoor-react-native-sdk": "1.1.0",
|
||||
"drizzle-orm": "^0.33.0",
|
||||
"eslint-plugin-import": "^2.28.1",
|
||||
"expo": "~51.0.34",
|
||||
"expo": "~51.0.37",
|
||||
"expo-asset": "~10.0.10",
|
||||
"expo-camera": "~15.0.16",
|
||||
"expo-crypto": "~13.0.2",
|
||||
"expo-dev-client": "~4.0.27",
|
||||
"expo-dev-client": "~4.0.28",
|
||||
"expo-document-picker": "~12.0.2",
|
||||
"expo-drizzle-studio-plugin": "^0.0.2",
|
||||
"expo-image": "~1.13.0",
|
||||
"expo-image-picker": "~15.0.7",
|
||||
"expo-sqlite": "^14.0.6",
|
||||
"expo-status-bar": "~1.12.1",
|
||||
"expo-system-ui": "~3.0.7",
|
||||
"expo-updates": "~0.25.25",
|
||||
"expo-updates": "~0.25.27",
|
||||
"hi-base32": "^0.5.1",
|
||||
"hotp-totp": "^1.0.6",
|
||||
"prop-types": "^15.8.1",
|
||||
|
|
|
@ -129,7 +129,7 @@ const useEditAccountStore = create((set, get) => ({
|
|||
}
|
||||
},
|
||||
|
||||
insertAccounts: (accounts) => {
|
||||
insertAccounts: async(accounts) => {
|
||||
try {
|
||||
db.transaction((tx) => {
|
||||
const insertWithDuplicateCheck = (baseAccName, issuer, secretKey) => {
|
||||
|
|
Loading…
Reference in New Issue