feat: add support for persist storage (#19)
This commit is contained in:
parent
4ece6cab45
commit
3b46a8ab50
64
Account.js
64
Account.js
|
@ -1,64 +0,0 @@
|
||||||
// Copyright 2023 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 totp from "totp-generator";
|
|
||||||
|
|
||||||
class Account {
|
|
||||||
constructor(accountName, issuer, secretKey, onUpdate) {
|
|
||||||
this.accountName = accountName;
|
|
||||||
this.secretKey = secretKey;
|
|
||||||
this.onUpdate = onUpdate;
|
|
||||||
this.token = this.generateToken();
|
|
||||||
this.isEditing = false;
|
|
||||||
this.issuer = issuer;
|
|
||||||
}
|
|
||||||
|
|
||||||
calculateCountdown() {
|
|
||||||
const currentTime = Math.floor(Date.now() / 1000);
|
|
||||||
return 30 - (currentTime % 30);
|
|
||||||
}
|
|
||||||
|
|
||||||
generateToken() {
|
|
||||||
if (this.secretKey !== null && this.secretKey !== undefined && this.secretKey !== "") {
|
|
||||||
const token = totp(this.secretKey);
|
|
||||||
const tokenWithSpace = token.slice(0, 3) + " " + token.slice(3);
|
|
||||||
return tokenWithSpace;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
generateAndSetToken() {
|
|
||||||
this.token = this.generateToken();
|
|
||||||
this.onUpdate();
|
|
||||||
}
|
|
||||||
|
|
||||||
setAccountName(accountName) {
|
|
||||||
this.accountName = accountName;
|
|
||||||
this.setEditingStatus(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
setEditingStatus(status) {
|
|
||||||
this.isEditing = status;
|
|
||||||
this.onUpdate();
|
|
||||||
}
|
|
||||||
|
|
||||||
getEditStatus() {
|
|
||||||
return this.isEditing;
|
|
||||||
}
|
|
||||||
|
|
||||||
deleteAccount() {
|
|
||||||
this.onUpdate();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Account;
|
|
18
App.js
18
App.js
|
@ -15,28 +15,24 @@
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import {PaperProvider} from "react-native-paper";
|
import {PaperProvider} from "react-native-paper";
|
||||||
import {NavigationContainer} from "@react-navigation/native";
|
import {NavigationContainer} from "@react-navigation/native";
|
||||||
|
import {BulletList} from "react-content-loader/native";
|
||||||
|
import {SQLiteProvider} from "expo-sqlite";
|
||||||
import Header from "./Header";
|
import Header from "./Header";
|
||||||
import NavigationBar from "./NavigationBar";
|
import NavigationBar from "./NavigationBar";
|
||||||
import {UserProvider} from "./UserContext";
|
import {migrateDb} from "./TotpDatabase";
|
||||||
import {CasdoorServerProvider} from "./CasdoorServerContext";
|
|
||||||
|
|
||||||
const App = () => {
|
const App = () => {
|
||||||
|
|
||||||
const [userInfo, setUserInfo] = React.useState(null);
|
|
||||||
const [token, setToken] = React.useState(null);
|
|
||||||
const [casdoorServer, setCasdoorServer] = React.useState(null);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CasdoorServerProvider value={{casdoorServer, setCasdoorServer}} >
|
<React.Suspense fallback={<BulletList />}>
|
||||||
<UserProvider value={{userInfo, setUserInfo, token, setToken}} >
|
<SQLiteProvider databaseName="totp.db" onInit={migrateDb} options={{enableChangeListener: true}}>
|
||||||
<NavigationContainer>
|
<NavigationContainer>
|
||||||
<PaperProvider>
|
<PaperProvider>
|
||||||
<Header />
|
<Header />
|
||||||
<NavigationBar />
|
<NavigationBar />
|
||||||
</PaperProvider>
|
</PaperProvider>
|
||||||
</NavigationContainer>
|
</NavigationContainer>
|
||||||
</UserProvider>
|
</SQLiteProvider>
|
||||||
</CasdoorServerProvider>
|
</React.Suspense>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
export default App;
|
export default App;
|
||||||
|
|
|
@ -12,15 +12,14 @@
|
||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
import React, {useEffect} from "react";
|
import React, {useEffect, useState} from "react";
|
||||||
import {WebView} from "react-native-webview";
|
import {WebView} from "react-native-webview";
|
||||||
import {View} from "react-native";
|
import {View} from "react-native";
|
||||||
import {Portal} from "react-native-paper";
|
import {Portal} from "react-native-paper";
|
||||||
import SDK from "casdoor-react-native-sdk";
|
import SDK from "casdoor-react-native-sdk";
|
||||||
import UserContext from "./UserContext";
|
|
||||||
import PropTypes from "prop-types";
|
import PropTypes from "prop-types";
|
||||||
import EnterCasdoorSdkConfig from "./EnterCasdoorSdkConfig";
|
import EnterCasdoorSdkConfig from "./EnterCasdoorSdkConfig";
|
||||||
import CasdoorServerContext from "./CasdoorServerContext";
|
import useStore from "./useStorage";
|
||||||
// import {LogBox} from "react-native";
|
// import {LogBox} from "react-native";
|
||||||
// LogBox.ignoreAllLogs();
|
// LogBox.ignoreAllLogs();
|
||||||
|
|
||||||
|
@ -29,10 +28,20 @@ const CasdoorLoginPage = ({onWebviewClose}) => {
|
||||||
CasdoorLoginPage.propTypes = {
|
CasdoorLoginPage.propTypes = {
|
||||||
onWebviewClose: PropTypes.func.isRequired,
|
onWebviewClose: PropTypes.func.isRequired,
|
||||||
};
|
};
|
||||||
const [casdoorLoginURL, setCasdoorLoginURL] = React.useState("");
|
const [casdoorLoginURL, setCasdoorLoginURL] = useState("");
|
||||||
const {setUserInfo, setToken} = React.useContext(UserContext);
|
const [showConfigPage, setShowConfigPage] = useState(true);
|
||||||
const [showConfigPage, setShowConfigPage] = React.useState(true);
|
|
||||||
const {casdoorServer} = React.useContext(CasdoorServerContext);
|
const {
|
||||||
|
serverUrl,
|
||||||
|
clientId,
|
||||||
|
redirectPath,
|
||||||
|
appName,
|
||||||
|
organizationName,
|
||||||
|
getCasdoorConfig,
|
||||||
|
setUserInfo,
|
||||||
|
setToken,
|
||||||
|
} = useStore();
|
||||||
|
|
||||||
const handleHideConfigPage = () => {
|
const handleHideConfigPage = () => {
|
||||||
setShowConfigPage(false);
|
setShowConfigPage(false);
|
||||||
};
|
};
|
||||||
|
@ -42,14 +51,15 @@ const CasdoorLoginPage = ({onWebviewClose}) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (casdoorServer) {
|
if (serverUrl && clientId && redirectPath && appName && organizationName) {
|
||||||
sdk = new SDK(casdoorServer);
|
sdk = new SDK(getCasdoorConfig());
|
||||||
getCasdoorSignInUrl();
|
getCasdoorSignInUrl();
|
||||||
}
|
}
|
||||||
}, [casdoorServer]);
|
}, [serverUrl, clientId, redirectPath, appName, organizationName]);
|
||||||
|
|
||||||
const onNavigationStateChange = async(navState) => {
|
const onNavigationStateChange = async(navState) => {
|
||||||
if (navState.url.startsWith(casdoorServer.redirectPath)) {
|
const {redirectPath} = getCasdoorConfig();
|
||||||
|
if (navState.url.startsWith(redirectPath)) {
|
||||||
onWebviewClose();
|
onWebviewClose();
|
||||||
const token = await sdk.getAccessToken(navState.url);
|
const token = await sdk.getAccessToken(navState.url);
|
||||||
const userInfo = sdk.JwtDecode(token);
|
const userInfo = sdk.JwtDecode(token);
|
||||||
|
@ -75,7 +85,9 @@ const CasdoorLoginPage = ({onWebviewClose}) => {
|
||||||
</Portal>
|
</Portal>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const CasdoorLogout = () => {
|
export const CasdoorLogout = () => {
|
||||||
sdk.clearState();
|
if (sdk) {sdk.clearState();}
|
||||||
};
|
};
|
||||||
|
|
||||||
export default CasdoorLoginPage;
|
export default CasdoorLoginPage;
|
||||||
|
|
|
@ -1,20 +0,0 @@
|
||||||
// Copyright 2023 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 React from "react";
|
|
||||||
|
|
||||||
const CasdoorServerContext = React.createContext();
|
|
||||||
export const CasdoorServerProvider = CasdoorServerContext.Provider;
|
|
||||||
export const CasdoorServerConsumer = CasdoorServerContext.Consumer;
|
|
||||||
export default CasdoorServerContext;
|
|
|
@ -29,6 +29,7 @@ export default function EnterAccountDetails({onClose, onEdit, placeholder}) {
|
||||||
const handleConfirm = () => {
|
const handleConfirm = () => {
|
||||||
onEdit(accountName);
|
onEdit(accountName);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={{flex: 1, justifyContent: "center", alignItems: "center"}}>
|
<View style={{flex: 1, justifyContent: "center", alignItems: "center"}}>
|
||||||
<Text style={{fontSize: 24, marginBottom: 5}}>Enter new account name</Text>
|
<Text style={{fontSize: 24, marginBottom: 5}}>Enter new account name</Text>
|
||||||
|
|
|
@ -12,52 +12,46 @@
|
||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
import React, {useState} from "react";
|
import React from "react";
|
||||||
import {Alert, Text, View} from "react-native";
|
import {Alert, Text, View} from "react-native";
|
||||||
import {Button, IconButton, Portal, TextInput} from "react-native-paper";
|
import {Button, IconButton, Portal, TextInput} from "react-native-paper";
|
||||||
import DefaultCasdoorSdkConfig from "./DefaultCasdoorSdkConfig";
|
import DefaultCasdoorSdkConfig from "./DefaultCasdoorSdkConfig";
|
||||||
import CasdoorServerContext from "./CasdoorServerContext";
|
|
||||||
import PropTypes from "prop-types";
|
import PropTypes from "prop-types";
|
||||||
|
import useStore from "./useStorage";
|
||||||
|
|
||||||
const EnterCasdoorSdkConfig = ({onClose, onWebviewClose}) => {
|
const EnterCasdoorSdkConfig = ({onClose, onWebviewClose}) => {
|
||||||
EnterCasdoorSdkConfig.propTypes = {
|
EnterCasdoorSdkConfig.propTypes = {
|
||||||
onClose: PropTypes.func.isRequired,
|
onClose: PropTypes.func.isRequired,
|
||||||
};
|
};
|
||||||
const {setCasdoorServer} = React.useContext(CasdoorServerContext);
|
|
||||||
|
|
||||||
const [CasdoorSdkConfig, setCasdoorSdkConfig] = useState({
|
const {
|
||||||
serverUrl: "",
|
serverUrl,
|
||||||
clientId: "",
|
clientId,
|
||||||
appName: "",
|
redirectPath,
|
||||||
organizationName: "",
|
appName,
|
||||||
redirectPath: "http://casdoor-app",
|
organizationName,
|
||||||
signinPath: "/api/signin",
|
setServerUrl,
|
||||||
});
|
setClientId,
|
||||||
|
setAppName,
|
||||||
const handleInputChange = (key, value) => {
|
setOrganizationName,
|
||||||
setCasdoorSdkConfig({...CasdoorSdkConfig, [key]: value});
|
setCasdoorConfig,
|
||||||
};
|
} = useStore();
|
||||||
|
|
||||||
const closeConfigPage = () => {
|
const closeConfigPage = () => {
|
||||||
onClose();
|
onClose();
|
||||||
onWebviewClose();
|
onWebviewClose();
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSave = () => {
|
const handleSave = () => {
|
||||||
if (
|
if (!serverUrl || !clientId || !appName || !organizationName || !redirectPath) {
|
||||||
!CasdoorSdkConfig.serverUrl ||
|
|
||||||
!CasdoorSdkConfig.clientId ||
|
|
||||||
!CasdoorSdkConfig.appName ||
|
|
||||||
!CasdoorSdkConfig.organizationName ||
|
|
||||||
!CasdoorSdkConfig.redirectPath
|
|
||||||
) {
|
|
||||||
Alert.alert("Please fill in all the fields!");
|
Alert.alert("Please fill in all the fields!");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setCasdoorServer(CasdoorSdkConfig);
|
|
||||||
onClose();
|
onClose();
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleUseDefault = () => {
|
const handleUseDefault = () => {
|
||||||
setCasdoorServer(DefaultCasdoorSdkConfig);
|
setCasdoorConfig(DefaultCasdoorSdkConfig);
|
||||||
onClose();
|
onClose();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -68,8 +62,8 @@ const EnterCasdoorSdkConfig = ({onClose, onWebviewClose}) => {
|
||||||
<Text style={{fontSize: 24, marginBottom: 5}}>Casdoor server</Text>
|
<Text style={{fontSize: 24, marginBottom: 5}}>Casdoor server</Text>
|
||||||
<TextInput
|
<TextInput
|
||||||
label="Endpoint"
|
label="Endpoint"
|
||||||
value={CasdoorSdkConfig.serverUrl}
|
value={serverUrl}
|
||||||
onChangeText={(text) => handleInputChange("serverUrl", text)}
|
onChangeText={setServerUrl}
|
||||||
autoCapitalize="none"
|
autoCapitalize="none"
|
||||||
style={{
|
style={{
|
||||||
borderWidth: 3,
|
borderWidth: 3,
|
||||||
|
@ -85,8 +79,8 @@ const EnterCasdoorSdkConfig = ({onClose, onWebviewClose}) => {
|
||||||
/>
|
/>
|
||||||
<TextInput
|
<TextInput
|
||||||
label="ClientID"
|
label="ClientID"
|
||||||
value={CasdoorSdkConfig.clientId}
|
value={clientId}
|
||||||
onChangeText={(text) => handleInputChange("clientId", text)}
|
onChangeText={setClientId}
|
||||||
autoCapitalize="none"
|
autoCapitalize="none"
|
||||||
style={{
|
style={{
|
||||||
borderWidth: 3,
|
borderWidth: 3,
|
||||||
|
@ -102,8 +96,8 @@ const EnterCasdoorSdkConfig = ({onClose, onWebviewClose}) => {
|
||||||
/>
|
/>
|
||||||
<TextInput
|
<TextInput
|
||||||
label="appName"
|
label="appName"
|
||||||
value={CasdoorSdkConfig.appName}
|
value={appName}
|
||||||
onChangeText={(text) => handleInputChange("appName", text)}
|
onChangeText={setAppName}
|
||||||
autoCapitalize="none"
|
autoCapitalize="none"
|
||||||
style={{
|
style={{
|
||||||
borderWidth: 3,
|
borderWidth: 3,
|
||||||
|
@ -119,8 +113,8 @@ const EnterCasdoorSdkConfig = ({onClose, onWebviewClose}) => {
|
||||||
/>
|
/>
|
||||||
<TextInput
|
<TextInput
|
||||||
label="organizationName"
|
label="organizationName"
|
||||||
value={CasdoorSdkConfig.organizationName}
|
value={organizationName}
|
||||||
onChangeText={(text) => handleInputChange("organizationName", text)}
|
onChangeText={setOrganizationName}
|
||||||
autoCapitalize="none"
|
autoCapitalize="none"
|
||||||
style={{
|
style={{
|
||||||
borderWidth: 3,
|
borderWidth: 3,
|
||||||
|
@ -173,4 +167,5 @@ const EnterCasdoorSdkConfig = ({onClose, onWebviewClose}) => {
|
||||||
</Portal>
|
</Portal>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default EnterCasdoorSdkConfig;
|
export default EnterCasdoorSdkConfig;
|
||||||
|
|
143
Header.js
143
Header.js
|
@ -13,82 +13,139 @@
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import {Appbar, Avatar, Button, Menu, Text} from "react-native-paper";
|
import {Dimensions, StyleSheet, View} from "react-native";
|
||||||
import UserContext from "./UserContext";
|
import {Appbar, Avatar, Menu, Text, TouchableRipple} from "react-native-paper";
|
||||||
import {StyleSheet, View} from "react-native";
|
|
||||||
import CasdoorLoginPage, {CasdoorLogout} from "./CasdoorLoginPage";
|
import CasdoorLoginPage, {CasdoorLogout} from "./CasdoorLoginPage";
|
||||||
|
import useStore from "./useStorage";
|
||||||
|
|
||||||
|
const {width} = Dimensions.get("window");
|
||||||
|
|
||||||
const Header = () => {
|
const Header = () => {
|
||||||
const {userInfo, setUserInfo, setToken} = React.useContext(UserContext);
|
const {userInfo, clearAll} = useStore();
|
||||||
const [showLoginPage, setShowLoginPage] = React.useState(false);
|
const [showLoginPage, setShowLoginPage] = React.useState(false);
|
||||||
const [menuVisible, setMenuVisible] = React.useState(false);
|
const [menuVisible, setMenuVisible] = React.useState(false);
|
||||||
|
|
||||||
const openMenu = () => setMenuVisible(true);
|
const openMenu = () => setMenuVisible(true);
|
||||||
const closeMenu = () => setMenuVisible(false);
|
const closeMenu = () => setMenuVisible(false);
|
||||||
|
|
||||||
const handleMenuLogoutClicked = () => {
|
const handleMenuLogoutClicked = () => {
|
||||||
handleCasdoorLogout();
|
handleCasdoorLogout();
|
||||||
closeMenu();
|
closeMenu();
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCasdoorLogin = () => {
|
const handleCasdoorLogin = () => setShowLoginPage(true);
|
||||||
setShowLoginPage(true);
|
const handleHideLoginPage = () => setShowLoginPage(false);
|
||||||
};
|
|
||||||
const handleCasdoorLogout = () => {
|
const handleCasdoorLogout = () => {
|
||||||
CasdoorLogout();
|
CasdoorLogout();
|
||||||
setUserInfo(null);
|
clearAll();
|
||||||
setToken(null);
|
|
||||||
};
|
|
||||||
const handleHideLoginPage = () => {
|
|
||||||
setShowLoginPage(false);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View>
|
<Appbar.Header>
|
||||||
<Appbar.Header style={{height: 40}}>
|
<Appbar.Content
|
||||||
<View style={[StyleSheet.absoluteFill, {alignItems: "center", justifyContent: "center"}]} pointerEvents="box-none">
|
title="Casdoor"
|
||||||
<Appbar.Content title="Casdoor" style={{
|
titleStyle={styles.titleText}
|
||||||
alignItems: "center",
|
style={styles.titleContainer}
|
||||||
justifyContent: "center",
|
/>
|
||||||
}} />
|
<View style={styles.rightContainer}>
|
||||||
</View>
|
|
||||||
<View style={{flex: 1}} />
|
|
||||||
<Menu
|
<Menu
|
||||||
visible={menuVisible}
|
visible={menuVisible}
|
||||||
|
onDismiss={closeMenu}
|
||||||
|
contentStyle={styles.menuContent}
|
||||||
|
anchorPosition="bottom"
|
||||||
|
mode="elevated"
|
||||||
anchor={
|
anchor={
|
||||||
<Button
|
<TouchableRipple
|
||||||
style={{
|
|
||||||
marginRight: 2,
|
|
||||||
backgroundColor: "transparent",
|
|
||||||
height: 40,
|
|
||||||
flexDirection: "row",
|
|
||||||
alignItems: "center",
|
|
||||||
justifyContent: "center",
|
|
||||||
}}
|
|
||||||
onPress={userInfo === null ? handleCasdoorLogin : openMenu}
|
onPress={userInfo === null ? handleCasdoorLogin : openMenu}
|
||||||
|
style={styles.buttonContainer}
|
||||||
>
|
>
|
||||||
<View style={{flexDirection: "row", alignItems: "center"}}>
|
<View style={styles.buttonContent}>
|
||||||
<View style={{position: "relative", height: 32, justifyContent: "flex-end", marginRight: 8}}>
|
<Text
|
||||||
<Text variant="titleMedium">
|
style={[
|
||||||
{userInfo === null ? "Login" : userInfo.name}
|
styles.buttonText,
|
||||||
</Text>
|
userInfo !== null && {marginRight: 8},
|
||||||
</View>
|
]}
|
||||||
|
>
|
||||||
|
{userInfo === null ? "Login" : userInfo.name}
|
||||||
|
</Text>
|
||||||
{userInfo !== null && (
|
{userInfo !== null && (
|
||||||
<Avatar.Image
|
<Avatar.Image
|
||||||
size={32}
|
size={24}
|
||||||
source={{uri: userInfo.avatar}}
|
source={{uri: userInfo.avatar}}
|
||||||
style={{backgroundColor: "transparent"}}
|
style={styles.avatar}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
</Button>
|
</TouchableRipple>
|
||||||
}
|
}
|
||||||
onDismiss={closeMenu}
|
|
||||||
>
|
>
|
||||||
<Menu.Item onPress={() => handleMenuLogoutClicked()} title="Logout" />
|
<Menu.Item onPress={handleMenuLogoutClicked} title="Logout" />
|
||||||
</Menu>
|
</Menu>
|
||||||
</Appbar.Header>
|
</View>
|
||||||
{showLoginPage && <CasdoorLoginPage onWebviewClose={handleHideLoginPage} />}
|
{showLoginPage && <CasdoorLoginPage onWebviewClose={handleHideLoginPage} />}
|
||||||
</View>
|
</Appbar.Header>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
leftContainer: {
|
||||||
|
position: "absolute",
|
||||||
|
left: 0,
|
||||||
|
top: 0,
|
||||||
|
bottom: 0,
|
||||||
|
justifyContent: "center",
|
||||||
|
paddingLeft: width * 0.03,
|
||||||
|
},
|
||||||
|
rightContainer: {
|
||||||
|
position: "absolute",
|
||||||
|
right: 0,
|
||||||
|
top: 0,
|
||||||
|
bottom: 0,
|
||||||
|
justifyContent: "center",
|
||||||
|
paddingRight: width * 0.03,
|
||||||
|
},
|
||||||
|
titleContainer: {
|
||||||
|
position: "absolute",
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
top: 0,
|
||||||
|
bottom: 0,
|
||||||
|
justifyContent: "center",
|
||||||
|
alignItems: "center",
|
||||||
|
},
|
||||||
|
titleText: {
|
||||||
|
fontSize: Math.max(20, width * 0.045),
|
||||||
|
fontWeight: "bold",
|
||||||
|
textAlign: "center",
|
||||||
|
},
|
||||||
|
buttonContainer: {
|
||||||
|
borderRadius: 20,
|
||||||
|
overflow: "hidden",
|
||||||
|
},
|
||||||
|
buttonContent: {
|
||||||
|
flexDirection: "row",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
paddingVertical: 8,
|
||||||
|
paddingHorizontal: 16,
|
||||||
|
},
|
||||||
|
buttonText: {
|
||||||
|
fontSize: Math.max(14, width * 0.035),
|
||||||
|
fontWeight: "bold",
|
||||||
|
},
|
||||||
|
menuContent: {
|
||||||
|
backgroundColor: "#FFFFFF",
|
||||||
|
borderRadius: 8,
|
||||||
|
elevation: 3,
|
||||||
|
shadowColor: "#000000",
|
||||||
|
shadowOffset: {width: 0, height: 2},
|
||||||
|
shadowOpacity: 0.2,
|
||||||
|
shadowRadius: 3,
|
||||||
|
},
|
||||||
|
avatar: {
|
||||||
|
backgroundColor: "transparent",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
export default Header;
|
export default Header;
|
||||||
|
|
231
HomePage.js
231
HomePage.js
|
@ -12,21 +12,23 @@
|
||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
import React, {useContext, useEffect, useRef, useState} from "react";
|
import React, {useEffect, useRef, useState} from "react";
|
||||||
import {Dimensions, FlatList, RefreshControl, Text, TouchableOpacity, View} from "react-native";
|
import {Dimensions, RefreshControl, TouchableOpacity, View} from "react-native";
|
||||||
import {Divider, IconButton, List, Modal, Portal} from "react-native-paper";
|
import {Divider, IconButton, List, Modal, Portal, Text} from "react-native-paper";
|
||||||
import {GestureHandlerRootView, Swipeable} from "react-native-gesture-handler";
|
import {GestureHandlerRootView, Swipeable} from "react-native-gesture-handler";
|
||||||
import {CountdownCircleTimer} from "react-native-countdown-circle-timer";
|
import {CountdownCircleTimer} from "react-native-countdown-circle-timer";
|
||||||
|
import {useNetInfo} from "@react-native-community/netinfo";
|
||||||
|
import {FlashList} from "@shopify/flash-list";
|
||||||
|
import * as SQLite from "expo-sqlite/next";
|
||||||
|
|
||||||
import SearchBar from "./SearchBar";
|
import SearchBar from "./SearchBar";
|
||||||
import EnterAccountDetails from "./EnterAccountDetails";
|
import EnterAccountDetails from "./EnterAccountDetails";
|
||||||
import ScanQRCode from "./ScanQRCode";
|
import ScanQRCode from "./ScanQRCode";
|
||||||
import EditAccountDetails from "./EditAccountDetails";
|
import EditAccountDetails from "./EditAccountDetails";
|
||||||
import AvatarWithFallback from "./AvatarWithFallback";
|
import AvatarWithFallback from "./AvatarWithFallback";
|
||||||
import Account from "./Account";
|
import * as TotpDatabase from "./TotpDatabase";
|
||||||
import UserContext from "./UserContext";
|
import useStore from "./useStorage";
|
||||||
import CasdoorServerContext from "./CasdoorServerContext";
|
import useSyncStore from "./useSyncStore";
|
||||||
import useSync, {SYNC_STATUS} from "./useSync";
|
|
||||||
|
|
||||||
const {width, height} = Dimensions.get("window");
|
const {width, height} = Dimensions.get("window");
|
||||||
const REFRESH_INTERVAL = 10000;
|
const REFRESH_INTERVAL = 10000;
|
||||||
|
@ -37,56 +39,83 @@ export default function HomePage() {
|
||||||
const [isPlusButton, setIsPlusButton] = useState(true);
|
const [isPlusButton, setIsPlusButton] = useState(true);
|
||||||
const [showOptions, setShowOptions] = useState(false);
|
const [showOptions, setShowOptions] = useState(false);
|
||||||
const [showEnterAccountModal, setShowEnterAccountModal] = useState(false);
|
const [showEnterAccountModal, setShowEnterAccountModal] = useState(false);
|
||||||
const [accountList, setAccountList] = useState([]);
|
|
||||||
const [searchQuery, setSearchQuery] = useState("");
|
const [searchQuery, setSearchQuery] = useState("");
|
||||||
const [filteredData, setFilteredData] = useState(accountList);
|
const [accounts, setAccounts] = useState([]);
|
||||||
|
const [filteredData, setFilteredData] = useState(accounts);
|
||||||
const [showScanner, setShowScanner] = useState(false);
|
const [showScanner, setShowScanner] = useState(false);
|
||||||
const [showEditAccountModal, setShowEditAccountModal] = useState(false);
|
const [showEditAccountModal, setShowEditAccountModal] = useState(false);
|
||||||
|
const [editingAccount, setEditingAccount] = useState(null);
|
||||||
const [placeholder, setPlaceholder] = useState("");
|
const [placeholder, setPlaceholder] = useState("");
|
||||||
const [refreshing, setRefreshing] = useState(false);
|
const [refreshing, setRefreshing] = useState(false);
|
||||||
|
const {isConnected} = useNetInfo();
|
||||||
|
const [canSync, setCanSync] = useState(false);
|
||||||
|
|
||||||
const swipeableRef = useRef(null);
|
const swipeableRef = useRef(null);
|
||||||
const isSyncing = useRef(false);
|
const {userInfo, serverUrl, token} = useStore();
|
||||||
|
const {startSync} = useSyncStore();
|
||||||
|
const db = SQLite.useSQLiteContext();
|
||||||
|
|
||||||
const {userInfo, token} = useContext(UserContext);
|
useEffect(() => {
|
||||||
const {casdoorServer} = useContext(CasdoorServerContext);
|
if (db) {
|
||||||
const {syncAccounts, syncSignal, resetSyncSignal, addToSyncData} = useSync(userInfo, token, casdoorServer);
|
const subscription = SQLite.addDatabaseChangeListener((event) => {loadAccounts();});
|
||||||
|
return () => {if (subscription) {subscription.remove();}};
|
||||||
const handleSync = async() => {
|
|
||||||
if (isSyncing.current) {return;}
|
|
||||||
isSyncing.current = true;
|
|
||||||
try {
|
|
||||||
const syncedAccounts = await syncAccounts();
|
|
||||||
if (syncedAccounts.success && syncedAccounts.accountList) {
|
|
||||||
accountList.forEach(account => account.deleteAccount());
|
|
||||||
const newAccountList = syncedAccounts.accountList.map(account => new Account(
|
|
||||||
account.accountName,
|
|
||||||
account.issuer,
|
|
||||||
account.secretKey,
|
|
||||||
onUpdate
|
|
||||||
));
|
|
||||||
setAccountList(newAccountList);
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
isSyncing.current = false;
|
|
||||||
setRefreshing(false);
|
|
||||||
resetSyncSignal();
|
|
||||||
}
|
}
|
||||||
|
}, [db]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setCanSync(Boolean(isConnected && userInfo && serverUrl));
|
||||||
|
}, [isConnected, userInfo, serverUrl]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setFilteredData(accounts);
|
||||||
|
}, [accounts]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
loadAccounts();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const timer = setInterval(() => {
|
||||||
|
if (canSync) {startSync(db, userInfo, serverUrl, token);}
|
||||||
|
}, REFRESH_INTERVAL);
|
||||||
|
return () => clearInterval(timer);
|
||||||
|
}, [startSync]);
|
||||||
|
|
||||||
|
const loadAccounts = async() => {
|
||||||
|
const loadedAccounts = await TotpDatabase.getAllAccounts(db);
|
||||||
|
setAccounts(loadedAccounts);
|
||||||
|
setFilteredData(loadedAccounts);
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
const onRefresh = async() => {
|
||||||
if ((syncSignal || refreshing) && !isSyncing.current) {
|
|
||||||
handleSync();
|
|
||||||
}
|
|
||||||
}, [syncSignal, refreshing]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const timer = setInterval(handleSync, REFRESH_INTERVAL);
|
|
||||||
return () => clearInterval(timer);
|
|
||||||
}, [handleSync]);
|
|
||||||
|
|
||||||
const onRefresh = () => {
|
|
||||||
setRefreshing(true);
|
setRefreshing(true);
|
||||||
|
if (canSync) {await startSync(db, userInfo, serverUrl, token);}
|
||||||
|
setRefreshing(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAddAccount = async(accountData) => {
|
||||||
|
await TotpDatabase.insertAccount(db, accountData);
|
||||||
|
closeEnterAccountModal();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDeleteAccount = async(id) => {
|
||||||
|
await TotpDatabase.deleteAccount(db, id);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleEditAccount = (account) => {
|
||||||
|
closeSwipeableMenu();
|
||||||
|
setEditingAccount(account);
|
||||||
|
setPlaceholder(account.accountName);
|
||||||
|
setShowEditAccountModal(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onAccountEdit = async(newAccountName) => {
|
||||||
|
if (editingAccount) {
|
||||||
|
await TotpDatabase.updateAccountName(db, editingAccount.id, newAccountName);
|
||||||
|
setPlaceholder("");
|
||||||
|
setEditingAccount(null);
|
||||||
|
closeEditAccountModal();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const closeEditAccountModal = () => setShowEditAccountModal(false);
|
const closeEditAccountModal = () => setShowEditAccountModal(false);
|
||||||
|
@ -117,46 +146,6 @@ export default function HomePage() {
|
||||||
|
|
||||||
const closeEnterAccountModal = () => setShowEnterAccountModal(false);
|
const closeEnterAccountModal = () => setShowEnterAccountModal(false);
|
||||||
|
|
||||||
const onUpdate = () => setAccountList(prev => [...prev]);
|
|
||||||
|
|
||||||
const handleAddAccount = (accountData) => {
|
|
||||||
const newAccount = new Account(accountData.accountName, accountData.issuer, accountData.secretKey, onUpdate);
|
|
||||||
addToSyncData(newAccount, SYNC_STATUS.ADD);
|
|
||||||
newAccount.token = newAccount.generateToken();
|
|
||||||
|
|
||||||
setAccountList(prev => [...prev, newAccount]);
|
|
||||||
closeEnterAccountModal();
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDeleteAccount = (accountName) => {
|
|
||||||
const accountToDelete = accountList.find(account => account.accountName === accountName);
|
|
||||||
if (accountToDelete) {
|
|
||||||
accountToDelete.deleteAccount();
|
|
||||||
addToSyncData(accountToDelete, SYNC_STATUS.DELETE);
|
|
||||||
}
|
|
||||||
setAccountList(prevList => prevList.filter(account => account.accountName !== accountName));
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleEditAccount = (accountName) => {
|
|
||||||
closeSwipeableMenu();
|
|
||||||
const accountToEdit = accountList.find(account => account.accountName === accountName);
|
|
||||||
if (accountToEdit) {
|
|
||||||
setPlaceholder(accountToEdit.accountName);
|
|
||||||
setShowEditAccountModal(true);
|
|
||||||
accountToEdit.setEditingStatus(true);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const onAccountEdit = (newAccountName) => {
|
|
||||||
const accountToEdit = accountList.find(account => account.getEditStatus() === true);
|
|
||||||
if (accountToEdit) {
|
|
||||||
addToSyncData(accountToEdit, SYNC_STATUS.EDIT, newAccountName);
|
|
||||||
accountToEdit.setAccountName(newAccountName);
|
|
||||||
}
|
|
||||||
setPlaceholder("");
|
|
||||||
closeEditAccountModal();
|
|
||||||
};
|
|
||||||
|
|
||||||
const closeSwipeableMenu = () => {
|
const closeSwipeableMenu = () => {
|
||||||
if (swipeableRef.current) {
|
if (swipeableRef.current) {
|
||||||
swipeableRef.current.close();
|
swipeableRef.current.close();
|
||||||
|
@ -166,17 +155,18 @@ export default function HomePage() {
|
||||||
const handleSearch = (query) => {
|
const handleSearch = (query) => {
|
||||||
setSearchQuery(query);
|
setSearchQuery(query);
|
||||||
setFilteredData(query.trim() !== ""
|
setFilteredData(query.trim() !== ""
|
||||||
? accountList.filter(item => item.accountName.toLowerCase().includes(query.toLowerCase()))
|
? accounts.filter(item => item.accountName.toLowerCase().includes(query.toLowerCase()))
|
||||||
: accountList
|
: accounts
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={{flex: 1}}>
|
<View style={{flex: 1}}>
|
||||||
<SearchBar onSearch={handleSearch} />
|
<SearchBar onSearch={handleSearch} />
|
||||||
<FlatList
|
<FlashList
|
||||||
data={searchQuery.trim() !== "" ? filteredData : accountList}
|
data={searchQuery.trim() !== "" ? filteredData : accounts}
|
||||||
keyExtractor={(item, index) => index.toString()}
|
keyExtractor={(item) => `${item.id}`}
|
||||||
|
estimatedItemSize={10}
|
||||||
refreshControl={
|
refreshControl={
|
||||||
<RefreshControl refreshing={refreshing} onRefresh={onRefresh} />
|
<RefreshControl refreshing={refreshing} onRefresh={onRefresh} />
|
||||||
}
|
}
|
||||||
|
@ -184,17 +174,17 @@ export default function HomePage() {
|
||||||
<GestureHandlerRootView>
|
<GestureHandlerRootView>
|
||||||
<Swipeable
|
<Swipeable
|
||||||
ref={swipeableRef}
|
ref={swipeableRef}
|
||||||
renderRightActions={(progress, dragX) => (
|
renderRightActions={() => (
|
||||||
<View style={{flexDirection: "row", alignItems: "center"}}>
|
<View style={{flexDirection: "row", alignItems: "center"}}>
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={{height: 70, width: 80, backgroundColor: "#E6DFF3", alignItems: "center", justifyContent: "center"}}
|
style={{height: 70, width: 80, backgroundColor: "#E6DFF3", alignItems: "center", justifyContent: "center"}}
|
||||||
onPress={handleEditAccount.bind(this, item.accountName)}
|
onPress={() => handleEditAccount(item)}
|
||||||
>
|
>
|
||||||
<Text>Edit</Text>
|
<Text>Edit</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={{height: 70, width: 80, backgroundColor: "#FFC0CB", alignItems: "center", justifyContent: "center"}}
|
style={{height: 70, width: 80, backgroundColor: "#FFC0CB", alignItems: "center", justifyContent: "center"}}
|
||||||
onPress={handleDeleteAccount.bind(this, item.accountName)}
|
onPress={() => handleDeleteAccount(item.id)}
|
||||||
>
|
>
|
||||||
<Text>Delete</Text>
|
<Text>Delete</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
|
@ -202,36 +192,49 @@ export default function HomePage() {
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<List.Item
|
<List.Item
|
||||||
style={{height: 80, alignItems: "center", justifyContent: "center", marginLeft: 10}}
|
style={{
|
||||||
|
height: 80,
|
||||||
|
paddingVertical: 5,
|
||||||
|
paddingHorizontal: 25,
|
||||||
|
}}
|
||||||
title={
|
title={
|
||||||
<View>
|
<View style={{flex: 1, justifyContent: "center"}}>
|
||||||
<Text style={{fontSize: 20}}>{item.accountName}</Text>
|
<Text variant="titleMedium">{item.accountName}</Text>
|
||||||
<Text style={{fontSize: 35, width: 180}}>{item.token}</Text>
|
<Text variant="headlineSmall" style={{fontWeight: "bold"}}>{item.token}</Text>
|
||||||
</View>
|
</View>
|
||||||
}
|
}
|
||||||
left={(props) => (
|
left={() => (
|
||||||
<AvatarWithFallback
|
<AvatarWithFallback
|
||||||
source={{uri: item.issuer ? `https://cdn.casbin.org/img/social_${item.issuer.toLowerCase()}.png` : "https://cdn.casbin.org/img/social_default.png"}}
|
source={{uri: item.issuer ? `https://cdn.casbin.org/img/social_${item.issuer.toLowerCase()}.png` : "https://cdn.casbin.org/img/social_default.png"}}
|
||||||
fallbackSource={{uri: "https://cdn.casbin.org/img/social_default.png"}}
|
fallbackSource={{uri: "https://cdn.casbin.org/img/social_default.png"}}
|
||||||
size={60}
|
size={60}
|
||||||
style={{marginLeft: 10, marginRight: 10, borderRadius: 10, backgroundColor: "transparent"}}
|
style={{
|
||||||
|
marginRight: 15,
|
||||||
|
borderRadius: 10,
|
||||||
|
backgroundColor: "transparent",
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
right={(props) => (
|
right={() => (
|
||||||
<CountdownCircleTimer
|
<View style={{justifyContent: "center", alignItems: "center"}}>
|
||||||
isPlaying={true}
|
<CountdownCircleTimer
|
||||||
duration={30}
|
isPlaying={true}
|
||||||
initialRemainingTime={item.calculateCountdown()}
|
duration={30}
|
||||||
colors={["#004777", "#0072A0", "#0099CC", "#FF6600", "#CC3300", "#A30000"]}
|
initialRemainingTime={TotpDatabase.calculateCountdown()}
|
||||||
colorsTime={[30, 24, 18, 12, 6, 0]}
|
colors={["#004777", "#0072A0", "#0099CC", "#FF6600", "#CC3300", "#A30000"]}
|
||||||
size={60}
|
colorsTime={[30, 24, 18, 12, 6, 0]}
|
||||||
onComplete={() => {item.generateAndSetToken(); return {shouldRepeat: true};}}
|
size={60}
|
||||||
strokeWidth={5}
|
onComplete={() => {
|
||||||
>
|
TotpDatabase.updateToken(db, item.id);
|
||||||
{({remainingTime}) => (
|
return {shouldRepeat: true, delay: 0};
|
||||||
<Text style={{fontSize: 20}}>{remainingTime}s</Text>
|
}}
|
||||||
)}
|
strokeWidth={5}
|
||||||
</CountdownCircleTimer>
|
>
|
||||||
|
{({remainingTime}) => (
|
||||||
|
<Text style={{fontSize: 18, fontWeight: "bold"}}>{remainingTime}s</Text>
|
||||||
|
)}
|
||||||
|
</CountdownCircleTimer>
|
||||||
|
</View>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</Swipeable>
|
</Swipeable>
|
||||||
|
|
|
@ -22,6 +22,7 @@ const SearchBar = ({onSearch}) => {
|
||||||
setSearchQuery(query);
|
setSearchQuery(query);
|
||||||
onSearch(query);
|
onSearch(query);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Searchbar
|
<Searchbar
|
||||||
placeholder="Search"
|
placeholder="Search"
|
||||||
|
|
|
@ -16,22 +16,18 @@ import * as React from "react";
|
||||||
import {Button} from "react-native-paper";
|
import {Button} from "react-native-paper";
|
||||||
import {View} from "react-native";
|
import {View} from "react-native";
|
||||||
import CasdoorLoginPage, {CasdoorLogout} from "./CasdoorLoginPage";
|
import CasdoorLoginPage, {CasdoorLogout} from "./CasdoorLoginPage";
|
||||||
import UserContext from "./UserContext";
|
import useStore from "./useStorage";
|
||||||
|
|
||||||
const SettingPage = () => {
|
const SettingPage = () => {
|
||||||
const [showLoginPage, setShowLoginPage] = React.useState(false);
|
const [showLoginPage, setShowLoginPage] = React.useState(false);
|
||||||
const {userInfo, setUserInfo, setToken} = React.useContext(UserContext);
|
const {userInfo, clearAll} = useStore();
|
||||||
const handleCasdoorLogin = () => {
|
|
||||||
setShowLoginPage(true);
|
const handleCasdoorLogin = () => setShowLoginPage(true);
|
||||||
};
|
const handleHideLoginPage = () => setShowLoginPage(false);
|
||||||
|
|
||||||
const handleCasdoorLogout = () => {
|
const handleCasdoorLogout = () => {
|
||||||
CasdoorLogout();
|
CasdoorLogout();
|
||||||
setUserInfo(null);
|
clearAll();
|
||||||
setToken(null);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleHideLoginPage = () => {
|
|
||||||
setShowLoginPage(false);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -0,0 +1,298 @@
|
||||||
|
// 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 totp from "totp-generator";
|
||||||
|
import * as api from "./api";
|
||||||
|
|
||||||
|
export async function migrateDb(db) {
|
||||||
|
const DATABASE_VERSION = 1;
|
||||||
|
const result = await db.getFirstAsync("PRAGMA user_version");
|
||||||
|
let currentVersion = result?.user_version ?? 0;
|
||||||
|
if (currentVersion === DATABASE_VERSION) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentVersion === 0) {
|
||||||
|
await db.execAsync(`
|
||||||
|
PRAGMA journal_mode = 'wal';
|
||||||
|
CREATE TABLE accounts (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
issuer TEXT,
|
||||||
|
account_name TEXT NOT NULL,
|
||||||
|
old_account_name TEXT DEFAULT NULL,
|
||||||
|
secret TEXT NOT NULL,
|
||||||
|
token TEXT,
|
||||||
|
is_deleted INTEGER DEFAULT 0,
|
||||||
|
last_change_time INTEGER DEFAULT (strftime('%s', 'now')),
|
||||||
|
last_sync_time INTEGER DEFAULT NULL
|
||||||
|
);
|
||||||
|
`);
|
||||||
|
await db.execAsync(`PRAGMA user_version = ${DATABASE_VERSION}`);
|
||||||
|
currentVersion = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
await db.execAsync(`PRAGMA user_version = ${DATABASE_VERSION}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const generateToken = (secretKey) => {
|
||||||
|
if (secretKey !== null && secretKey !== undefined && secretKey !== "") {
|
||||||
|
try {
|
||||||
|
const token = totp(secretKey);
|
||||||
|
const tokenWithSpace = token.slice(0, 3) + " " + token.slice(3);
|
||||||
|
return tokenWithSpace;
|
||||||
|
} catch (error) {
|
||||||
|
return "Secret Invalid";
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return "Secret Empty";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function insertAccount(db, account) {
|
||||||
|
const token = generateToken(account.secretKey);
|
||||||
|
const currentTime = Math.floor(Date.now() / 1000);
|
||||||
|
return await db.runAsync(
|
||||||
|
"INSERT INTO accounts (issuer, account_name, secret, token, last_change_time) VALUES (?, ?, ?, ?, ?)",
|
||||||
|
account.issuer ?? "",
|
||||||
|
account.accountName,
|
||||||
|
account.secretKey,
|
||||||
|
token ?? "",
|
||||||
|
currentTime
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updateAccountName(db, id, newAccountName) {
|
||||||
|
const account = await db.getFirstAsync("SELECT * FROM accounts WHERE id = ?", id);
|
||||||
|
const currentTime = Math.floor(Date.now() / 1000);
|
||||||
|
|
||||||
|
// Only update old_account_name if it's null or if last_sync_time is more recent than last_change_time
|
||||||
|
if (account.old_account_name === null || (account.last_sync_time && account.last_sync_time > account.last_change_time)) {
|
||||||
|
await db.runAsync(`
|
||||||
|
UPDATE accounts
|
||||||
|
SET account_name = ?,
|
||||||
|
old_account_name = ?,
|
||||||
|
last_change_time = ?
|
||||||
|
WHERE id = ?
|
||||||
|
`, newAccountName, account.account_name, currentTime, id);
|
||||||
|
} else {
|
||||||
|
await db.runAsync(`
|
||||||
|
UPDATE accounts
|
||||||
|
SET account_name = ?,
|
||||||
|
last_change_time = ?
|
||||||
|
WHERE id = ?
|
||||||
|
`, newAccountName, currentTime, id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updateAccount(db, account, id) {
|
||||||
|
const token = generateToken(account.secretKey);
|
||||||
|
const currentTime = Math.floor(Date.now() / 1000);
|
||||||
|
const result = await db.runAsync(
|
||||||
|
"UPDATE accounts SET issuer = ?, account_name = ?, old_account_name = ?, secret = ?, token = ?, last_change_time = ? WHERE id = ?",
|
||||||
|
account.issuer,
|
||||||
|
account.accountName,
|
||||||
|
account.oldAccountName ?? null,
|
||||||
|
account.secretKey,
|
||||||
|
token ?? "",
|
||||||
|
currentTime,
|
||||||
|
id
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result.changes === 0) {
|
||||||
|
throw new Error(`No account updated for id: ${id}`);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deleteAccount(db, id) {
|
||||||
|
const currentTime = Math.floor(Date.now() / 1000);
|
||||||
|
await db.runAsync("UPDATE accounts SET is_deleted = 1, last_change_time = ? WHERE id = ?", currentTime, id);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function trueDeleteAccount(db, id) {
|
||||||
|
return await db.runAsync("DELETE FROM accounts WHERE id = ?", id);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updateToken(db, id) {
|
||||||
|
const result = db.getFirstSync("SELECT secret FROM accounts WHERE id = ?", id);
|
||||||
|
if (result.secret === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const token = generateToken(result.secret);
|
||||||
|
return db.runSync("UPDATE accounts SET token = ? WHERE id = ?", token, id);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updateTokenForAll(db) {
|
||||||
|
const accounts = await db.getAllAsync("SELECT * FROM accounts WHERE is_deleted = 0");
|
||||||
|
for (const account of accounts) {
|
||||||
|
const token = generateToken(account.secret);
|
||||||
|
await db.runAsync("UPDATE accounts SET token = ? WHERE id = ?", token, account.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getAllAccounts(db) {
|
||||||
|
const accounts = await db.getAllAsync("SELECT * FROM accounts WHERE is_deleted = 0");
|
||||||
|
return accounts.map(account => {
|
||||||
|
const mappedAccount = {
|
||||||
|
...account,
|
||||||
|
accountName: account.account_name,
|
||||||
|
secretKey: account.secret,
|
||||||
|
};
|
||||||
|
return mappedAccount;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getLocalAccounts(db) {
|
||||||
|
const accounts = await db.getAllAsync("SELECT * FROM accounts");
|
||||||
|
return accounts.map(account => ({
|
||||||
|
id: account.id,
|
||||||
|
issuer: account.issuer,
|
||||||
|
accountName: account.account_name,
|
||||||
|
oldAccountName: account.old_account_name,
|
||||||
|
secretKey: account.secret,
|
||||||
|
isDeleted: account.is_deleted === 1,
|
||||||
|
lastChangeTime: account.last_change_time,
|
||||||
|
lastSyncTime: account.last_sync_time,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateSyncTimeForAll(db) {
|
||||||
|
const currentTime = Math.floor(Date.now() / 1000);
|
||||||
|
await db.runAsync("UPDATE accounts SET last_sync_time = ?", currentTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function calculateCountdown() {
|
||||||
|
const now = Math.floor(Date.now() / 1000);
|
||||||
|
return 30 - (now % 30);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateLocalDatabase(db, mergedAccounts) {
|
||||||
|
for (const account of mergedAccounts) {
|
||||||
|
if (account.id) {
|
||||||
|
if (account.isDeleted) {
|
||||||
|
await db.runAsync("DELETE FROM accounts WHERE id = ?", account.id);
|
||||||
|
} else {
|
||||||
|
await updateAccount(db, account, account.id);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
await insertAccount(db, account);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getAccountKey(account) {
|
||||||
|
return `${account.issuer}:${account.accountName}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function mergeAccounts(localAccounts, serverAccounts, serverTimestamp) {
|
||||||
|
const mergedAccounts = new Map();
|
||||||
|
const localAccountKeys = new Map();
|
||||||
|
|
||||||
|
// Process local accounts
|
||||||
|
for (const local of localAccounts) {
|
||||||
|
const key = getAccountKey(local);
|
||||||
|
mergedAccounts.set(key, {
|
||||||
|
...local,
|
||||||
|
synced: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Store both current and old account keys for local accounts
|
||||||
|
localAccountKeys.set(key, local);
|
||||||
|
if (local.oldAccountName) {
|
||||||
|
const oldKey = getAccountKey({...local, accountName: local.oldAccountName});
|
||||||
|
localAccountKeys.set(oldKey, local);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const processedLocalKeys = new Set();
|
||||||
|
|
||||||
|
// Merge with server accounts
|
||||||
|
for (const server of serverAccounts) {
|
||||||
|
const serverKey = getAccountKey(server);
|
||||||
|
const localAccount = localAccountKeys.get(serverKey);
|
||||||
|
|
||||||
|
if (!localAccount) {
|
||||||
|
// New account from server
|
||||||
|
mergedAccounts.set(serverKey, {...server, synced: true});
|
||||||
|
} else {
|
||||||
|
const localKey = getAccountKey(localAccount);
|
||||||
|
const local = mergedAccounts.get(localKey);
|
||||||
|
|
||||||
|
if (serverTimestamp > local.lastChangeTime) {
|
||||||
|
// Server has newer changes
|
||||||
|
mergedAccounts.set(localKey, {
|
||||||
|
...server,
|
||||||
|
id: local.id,
|
||||||
|
oldAccountName: local.accountName !== server.accountName ? local.accountName : local.oldAccountName,
|
||||||
|
synced: true,
|
||||||
|
});
|
||||||
|
} else if (local.accountName !== server.accountName) {
|
||||||
|
// Local name change is newer, update the server account name
|
||||||
|
mergedAccounts.set(localKey, {
|
||||||
|
...local,
|
||||||
|
oldAccountName: server.accountName,
|
||||||
|
synced: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// If local is newer or deleted, keep the local version (already in mergedAccounts)
|
||||||
|
processedLocalKeys.add(localKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Handle server-side deletions
|
||||||
|
for (const [key, local] of mergedAccounts) {
|
||||||
|
if (!processedLocalKeys.has(key) && local.lastSyncTime && local.lastSyncTime < serverTimestamp) {
|
||||||
|
// This account was not found on the server and was previously synced
|
||||||
|
// Mark it as deleted
|
||||||
|
mergedAccounts.set(key, {...local, isDeleted: true, synced: true});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Array.from(mergedAccounts.values());
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function syncWithCloud(db, userInfo, serverUrl, token) {
|
||||||
|
const localAccounts = await getLocalAccounts(db);
|
||||||
|
const {updatedTime, mfaAccounts: serverAccounts} = await api.getMfaAccounts(
|
||||||
|
serverUrl,
|
||||||
|
userInfo.owner,
|
||||||
|
userInfo.name,
|
||||||
|
token
|
||||||
|
);
|
||||||
|
|
||||||
|
const serverTimestamp = Math.floor(new Date(updatedTime).getTime() / 1000);
|
||||||
|
|
||||||
|
const mergedAccounts = mergeAccounts(localAccounts, serverAccounts, serverTimestamp);
|
||||||
|
await updateLocalDatabase(db, mergedAccounts);
|
||||||
|
|
||||||
|
const accountsToSync = mergedAccounts.filter(account => !account.isDeleted).map(account => ({
|
||||||
|
issuer: account.issuer,
|
||||||
|
accountName: account.accountName,
|
||||||
|
secretKey: account.secretKey,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const {status} = await api.updateMfaAccounts(
|
||||||
|
serverUrl,
|
||||||
|
userInfo.owner,
|
||||||
|
userInfo.name,
|
||||||
|
accountsToSync,
|
||||||
|
token
|
||||||
|
);
|
||||||
|
|
||||||
|
if (status !== "ok") {
|
||||||
|
throw new Error("Sync failed");
|
||||||
|
}
|
||||||
|
|
||||||
|
await updateSyncTimeForAll(db);
|
||||||
|
await updateTokenForAll(db);
|
||||||
|
}
|
|
@ -1,20 +0,0 @@
|
||||||
// Copyright 2023 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 React from "react";
|
|
||||||
|
|
||||||
const UserContext = React.createContext();
|
|
||||||
export const UserProvider = UserContext.Provider;
|
|
||||||
export const UserConsumer = UserContext.Consumer;
|
|
||||||
export default UserContext;
|
|
File diff suppressed because it is too large
Load Diff
20
package.json
20
package.json
|
@ -3,7 +3,7 @@
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"main": "node_modules/expo/AppEntry.js",
|
"main": "node_modules/expo/AppEntry.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "expo start",
|
"start": "expo start --tunnel",
|
||||||
"android": "expo start --android",
|
"android": "expo start --android",
|
||||||
"ios": "expo start --ios",
|
"ios": "expo start --ios",
|
||||||
"web": "expo start --web",
|
"web": "expo start --web",
|
||||||
|
@ -15,31 +15,35 @@
|
||||||
"@react-native-community/netinfo": "11.3.1",
|
"@react-native-community/netinfo": "11.3.1",
|
||||||
"@react-navigation/bottom-tabs": "^6.5.8",
|
"@react-navigation/bottom-tabs": "^6.5.8",
|
||||||
"@react-navigation/native": "^6.1.7",
|
"@react-navigation/native": "^6.1.7",
|
||||||
|
"@shopify/flash-list": "1.6.4",
|
||||||
"casdoor-react-native-sdk": "1.1.0",
|
"casdoor-react-native-sdk": "1.1.0",
|
||||||
"eslint-plugin-import": "^2.28.1",
|
"eslint-plugin-import": "^2.28.1",
|
||||||
"expo": "~51.0.24",
|
"expo": "~51.0.26",
|
||||||
"expo-camera": "~15.0.14",
|
"expo-camera": "~15.0.14",
|
||||||
"expo-dev-client": "^4.0.21",
|
"expo-dev-client": "~4.0.22",
|
||||||
"expo-image": "^1.12.13",
|
"expo-image": "^1.12.13",
|
||||||
|
"expo-sqlite": "~14.0.6",
|
||||||
"expo-status-bar": "~1.12.1",
|
"expo-status-bar": "~1.12.1",
|
||||||
"expo-system-ui": "~3.0.7",
|
"expo-system-ui": "~3.0.7",
|
||||||
"expo-updates": "~0.25.21",
|
"expo-updates": "~0.25.22",
|
||||||
"hotp-totp": "^1.0.6",
|
"hotp-totp": "^1.0.6",
|
||||||
"prop-types": "^15.8.1",
|
"prop-types": "^15.8.1",
|
||||||
"react": "18.2.0",
|
"react": "18.2.0",
|
||||||
|
"react-content-loader": "^7.0.2",
|
||||||
"react-dom": "18.2.0",
|
"react-dom": "18.2.0",
|
||||||
"react-native": "0.74.3",
|
"react-native": "0.74.5",
|
||||||
"react-native-countdown-circle-timer": "^3.2.1",
|
"react-native-countdown-circle-timer": "^3.2.1",
|
||||||
"react-native-gesture-handler": "~2.16.1",
|
"react-native-gesture-handler": "~2.16.1",
|
||||||
"react-native-paper": "^5.10.3",
|
"react-native-paper": "^5.10.3",
|
||||||
"react-native-reanimated": "~3.10.1",
|
"react-native-reanimated": "~3.10.1",
|
||||||
"react-native-root-toast": "^3.6.0",
|
|
||||||
"react-native-safe-area-context": "4.10.5",
|
"react-native-safe-area-context": "4.10.5",
|
||||||
"react-native-screens": "^3.31.1",
|
"react-native-screens": "3.31.1",
|
||||||
"react-native-svg": "15.2.0",
|
"react-native-svg": "15.2.0",
|
||||||
|
"react-native-toast-message": "^2.2.0",
|
||||||
"react-native-web": "~0.19.6",
|
"react-native-web": "~0.19.6",
|
||||||
"react-native-webview": "13.8.6",
|
"react-native-webview": "13.8.6",
|
||||||
"totp-generator": "^0.0.14"
|
"totp-generator": "^0.0.14",
|
||||||
|
"zustand": "^4.5.4"
|
||||||
},
|
},
|
||||||
"verifyConditions": [
|
"verifyConditions": [
|
||||||
"semantic-release-expo",
|
"semantic-release-expo",
|
||||||
|
|
|
@ -0,0 +1,70 @@
|
||||||
|
// 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 {create} from "zustand";
|
||||||
|
import {createJSONStorage, persist} from "zustand/middleware";
|
||||||
|
import AsyncStorage from "@react-native-async-storage/async-storage";
|
||||||
|
|
||||||
|
const asyncStoragePersistConfig = {
|
||||||
|
setItem: async(key, value) => await AsyncStorage.setItem(key, value),
|
||||||
|
getItem: async(key) => await AsyncStorage.getItem(key),
|
||||||
|
removeItem: async(key) => await AsyncStorage.removeItem(key),
|
||||||
|
};
|
||||||
|
|
||||||
|
const useStore = create(
|
||||||
|
persist(
|
||||||
|
(set, get) => ({
|
||||||
|
serverUrl: "",
|
||||||
|
clientId: "",
|
||||||
|
redirectPath: "http://casdoor-app",
|
||||||
|
appName: "",
|
||||||
|
organizationName: "",
|
||||||
|
signinPath: "/api/signin",
|
||||||
|
userInfo: null,
|
||||||
|
token: null,
|
||||||
|
setServerUrl: (url) => set({serverUrl: url}),
|
||||||
|
setClientId: (id) => set({clientId: id}),
|
||||||
|
setRedirectPath: (path) => set({redirectPath: path}),
|
||||||
|
setAppName: (name) => set({appName: name}),
|
||||||
|
setOrganizationName: (name) => set({organizationName: name}),
|
||||||
|
setSigninPath: (path) => set({signinPath: path}),
|
||||||
|
setUserInfo: (info) => set({userInfo: info}),
|
||||||
|
setToken: (token) => set({token: token}),
|
||||||
|
clearAll: () => set({userInfo: null, token: null}),
|
||||||
|
|
||||||
|
getCasdoorConfig: () => ({
|
||||||
|
serverUrl: get().serverUrl,
|
||||||
|
clientId: get().clientId,
|
||||||
|
appName: get().appName,
|
||||||
|
organizationName: get().organizationName,
|
||||||
|
redirectPath: get().redirectPath,
|
||||||
|
signinPath: get().signinPath,
|
||||||
|
}),
|
||||||
|
setCasdoorConfig: (config) => set({
|
||||||
|
serverUrl: config.serverUrl || get().serverUrl,
|
||||||
|
clientId: config.clientId || get().clientId,
|
||||||
|
appName: config.appName || get().appName,
|
||||||
|
organizationName: config.organizationName || get().organizationName,
|
||||||
|
redirectPath: config.redirectPath || get().redirectPath,
|
||||||
|
signinPath: config.signinPath || get().signinPath,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
name: "casdoor-storage",
|
||||||
|
storage: createJSONStorage(() => asyncStoragePersistConfig),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
export default useStore;
|
129
useSync.js
129
useSync.js
|
@ -1,129 +0,0 @@
|
||||||
// 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 {useCallback, useEffect, useState} from "react";
|
|
||||||
import * as api from "./api";
|
|
||||||
import {useNetInfo} from "@react-native-community/netinfo";
|
|
||||||
|
|
||||||
export const SYNC_STATUS = {
|
|
||||||
ADD: "add",
|
|
||||||
EDIT: "edit",
|
|
||||||
DELETE: "delete",
|
|
||||||
};
|
|
||||||
|
|
||||||
const applySync = (serverAccountList, toSyncData) => {
|
|
||||||
return toSyncData.reduce((acc, syncItem) => {
|
|
||||||
switch (syncItem.status) {
|
|
||||||
case SYNC_STATUS.ADD:
|
|
||||||
if (!acc.some(account => account.accountName === syncItem.data.accountName && account.secretKey === syncItem.data.secretKey)) {
|
|
||||||
acc.push(syncItem.data);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case SYNC_STATUS.EDIT:
|
|
||||||
const indexToEdit = acc.findIndex(account => account.accountName === syncItem.data.accountName && account.secretKey === syncItem.data.secretKey);
|
|
||||||
if (indexToEdit !== -1) {
|
|
||||||
acc[indexToEdit] = {...acc[indexToEdit], ...syncItem.data, accountName: syncItem.newAccountName};
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case SYNC_STATUS.DELETE:
|
|
||||||
return acc.filter(account => !(account.accountName === syncItem.data.accountName && account.secretKey === syncItem.data.secretKey));
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
return acc;
|
|
||||||
}, [...serverAccountList]);
|
|
||||||
};
|
|
||||||
|
|
||||||
const useSync = (userInfo, token, casdoorServer) => {
|
|
||||||
const [toSyncData, setToSyncData] = useState([]);
|
|
||||||
const [syncSignal, setSyncSignal] = useState(false);
|
|
||||||
const {isConnected} = useNetInfo();
|
|
||||||
const [canSync, setCanSync] = useState(false);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setCanSync(userInfo && casdoorServer && isConnected);
|
|
||||||
}, [userInfo, casdoorServer, isConnected]);
|
|
||||||
|
|
||||||
const triggerSync = useCallback(() => {
|
|
||||||
if (canSync) {
|
|
||||||
setSyncSignal(true);
|
|
||||||
}
|
|
||||||
}, [canSync]);
|
|
||||||
|
|
||||||
const resetSyncSignal = useCallback(() => {
|
|
||||||
setSyncSignal(false);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const addToSyncData = useCallback((toSyncAccount, status, newAccountName = null) => {
|
|
||||||
setToSyncData([...toSyncData, {
|
|
||||||
data: {
|
|
||||||
accountName: toSyncAccount.accountName,
|
|
||||||
issuer: toSyncAccount.issuer,
|
|
||||||
secretKey: toSyncAccount.secretKey,
|
|
||||||
},
|
|
||||||
status,
|
|
||||||
newAccountName: newAccountName || "",
|
|
||||||
}]);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const syncAccounts = useCallback(async() => {
|
|
||||||
if (!canSync) {return {success: false, error: "Cannot sync"};}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const {mfaAccounts: serverAccountList} = await api.getMfaAccounts(
|
|
||||||
casdoorServer.serverUrl,
|
|
||||||
userInfo.owner,
|
|
||||||
userInfo.name,
|
|
||||||
token
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!serverAccountList) {
|
|
||||||
return {success: false, error: "Failed to get accounts"};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (toSyncData.length === 0) {
|
|
||||||
return {success: true, accountList: serverAccountList};
|
|
||||||
}
|
|
||||||
|
|
||||||
const updatedServerAccountList = applySync(serverAccountList, toSyncData);
|
|
||||||
|
|
||||||
const {status} = await api.updateMfaAccounts(
|
|
||||||
casdoorServer.serverUrl,
|
|
||||||
userInfo.owner,
|
|
||||||
userInfo.name,
|
|
||||||
updatedServerAccountList,
|
|
||||||
token
|
|
||||||
);
|
|
||||||
|
|
||||||
if (status === "ok") {setToSyncData([]);}
|
|
||||||
return {success: status === "ok", accountList: updatedServerAccountList};
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
return {success: false, error: error.message};
|
|
||||||
}
|
|
||||||
}, [canSync, casdoorServer, userInfo, token, toSyncData]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (canSync) {triggerSync();}
|
|
||||||
}, [canSync, toSyncData]);
|
|
||||||
|
|
||||||
return {
|
|
||||||
syncSignal,
|
|
||||||
resetSyncSignal,
|
|
||||||
syncAccounts,
|
|
||||||
addToSyncData,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export default useSync;
|
|
|
@ -0,0 +1,38 @@
|
||||||
|
// 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 {create} from "zustand";
|
||||||
|
import * as TotpDatabase from "./TotpDatabase";
|
||||||
|
|
||||||
|
const useSyncStore = create((set, get) => ({
|
||||||
|
isSyncing: false,
|
||||||
|
syncError: null,
|
||||||
|
|
||||||
|
startSync: async(db, userInfo, casdoorServer, token) => {
|
||||||
|
if (!get().isSyncing) {
|
||||||
|
set({isSyncing: true, syncError: null});
|
||||||
|
try {
|
||||||
|
await TotpDatabase.syncWithCloud(db, userInfo, casdoorServer, token);
|
||||||
|
} catch (error) {
|
||||||
|
set({syncError: error.message});
|
||||||
|
} finally {
|
||||||
|
set({isSyncing: false});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
clearSyncError: () => set({syncError: null}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
export default useSyncStore;
|
Loading…
Reference in New Issue