feat: add secret check (#20)

This commit is contained in:
IZUMI-Zu 2024-08-17 23:22:44 +08:00 committed by GitHub
parent e96d171cf8
commit a46b4a25c8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 530 additions and 216 deletions

2
App.js
View File

@ -17,6 +17,7 @@ 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 {BulletList} from "react-content-loader/native";
import {SQLiteProvider} from "expo-sqlite"; import {SQLiteProvider} from "expo-sqlite";
import Toast from "react-native-toast-message";
import Header from "./Header"; import Header from "./Header";
import NavigationBar from "./NavigationBar"; import NavigationBar from "./NavigationBar";
import {migrateDb} from "./TotpDatabase"; import {migrateDb} from "./TotpDatabase";
@ -31,6 +32,7 @@ const App = () => {
<NavigationBar /> <NavigationBar />
</PaperProvider> </PaperProvider>
</NavigationContainer> </NavigationContainer>
<Toast />
</SQLiteProvider> </SQLiteProvider>
</React.Suspense> </React.Suspense>
); );

View File

@ -14,10 +14,12 @@
import React, {useEffect, useState} 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 {Platform, SafeAreaView, StatusBar, StyleSheet, Text, TouchableOpacity} 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 PropTypes from "prop-types"; import PropTypes from "prop-types";
import Toast from "react-native-toast-message";
import EnterCasdoorSdkConfig from "./EnterCasdoorSdkConfig"; import EnterCasdoorSdkConfig from "./EnterCasdoorSdkConfig";
import useStore from "./useStorage"; import useStore from "./useStorage";
// import {LogBox} from "react-native"; // import {LogBox} from "react-native";
@ -28,6 +30,7 @@ const CasdoorLoginPage = ({onWebviewClose}) => {
CasdoorLoginPage.propTypes = { CasdoorLoginPage.propTypes = {
onWebviewClose: PropTypes.func.isRequired, onWebviewClose: PropTypes.func.isRequired,
}; };
const [casdoorLoginURL, setCasdoorLoginURL] = useState(""); const [casdoorLoginURL, setCasdoorLoginURL] = useState("");
const [showConfigPage, setShowConfigPage] = useState(true); const [showConfigPage, setShowConfigPage] = useState(true);
@ -45,6 +48,11 @@ const CasdoorLoginPage = ({onWebviewClose}) => {
const handleHideConfigPage = () => { const handleHideConfigPage = () => {
setShowConfigPage(false); setShowConfigPage(false);
}; };
const handleShowConfigPage = () => {
setShowConfigPage(true);
};
const getCasdoorSignInUrl = async() => { const getCasdoorSignInUrl = async() => {
const signinUrl = await sdk.getSigninUrl(); const signinUrl = await sdk.getSigninUrl();
setCasdoorLoginURL(signinUrl); setCasdoorLoginURL(signinUrl);
@ -68,24 +76,71 @@ const CasdoorLoginPage = ({onWebviewClose}) => {
} }
}; };
const handleErrorResponse = (error) => {
Toast.show({
type: "error",
text1: "Error",
text2: error.description,
autoHide: true,
});
setShowConfigPage(true);
};
return ( return (
<Portal> <Portal>
<View style={{flex: 1}}> <SafeAreaView style={styles.container}>
{showConfigPage && <EnterCasdoorSdkConfig onClose={handleHideConfigPage} onWebviewClose={onWebviewClose} />} {showConfigPage && (
<EnterCasdoorSdkConfig
onClose={handleHideConfigPage}
onWebviewClose={onWebviewClose}
/>
)}
{!showConfigPage && casdoorLoginURL !== "" && ( {!showConfigPage && casdoorLoginURL !== "" && (
<>
<TouchableOpacity
style={styles.backButton}
onPress={handleShowConfigPage}
>
<Text style={styles.backButtonText}>Back to Config</Text>
</TouchableOpacity>
<WebView <WebView
source={{uri: casdoorLoginURL}} source={{uri: casdoorLoginURL}}
onNavigationStateChange={onNavigationStateChange} onNavigationStateChange={onNavigationStateChange}
style={{flex: 1}} onError={(syntheticEvent) => {
const {nativeEvent} = syntheticEvent;
handleErrorResponse(nativeEvent);
}}
style={styles.webview}
mixedContentMode="always" mixedContentMode="always"
javaScriptEnabled={true} javaScriptEnabled={true}
/> />
</>
)} )}
</View> </SafeAreaView>
</Portal> </Portal>
); );
}; };
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: "white",
paddingTop: Platform.OS === "android" ? StatusBar.currentHeight : 0,
},
webview: {
flex: 1,
},
backButton: {
padding: 10,
backgroundColor: "#007AFF",
alignItems: "center",
},
backButtonText: {
color: "white",
fontWeight: "bold",
},
});
export const CasdoorLogout = () => { export const CasdoorLogout = () => {
if (sdk) {sdk.clearState();} if (sdk) {sdk.clearState();}
}; };

View File

@ -12,104 +12,230 @@
// 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, {useCallback, useState} from "react";
import {Text, TextInput, View} from "react-native"; import {View} from "react-native";
import {Button, Divider, IconButton, Menu} from "react-native-paper"; import {Button, IconButton, Menu, Text, TextInput} from "react-native-paper";
import Toast from "react-native-toast-message";
import PropTypes from "prop-types"; import PropTypes from "prop-types";
export default function EnterAccountDetails({onClose, onAdd}) { const EnterAccountDetails = ({onClose, onAdd, validateSecret}) => {
EnterAccountDetails.propTypes = { EnterAccountDetails.propTypes = {
onClose: PropTypes.func.isRequired, onClose: PropTypes.func.isRequired,
onAdd: PropTypes.func.isRequired, onAdd: PropTypes.func.isRequired,
validateSecret: PropTypes.func.isRequired,
}; };
const [accountName, setAccountName] = useState(""); const [accountName, setAccountName] = useState("");
const [secretKey, setSecretKey] = useState(""); const [secretKey, setSecretKey] = useState("");
const [secretError, setSecretError] = useState("");
const [accountNameError, setAccountNameError] = useState("");
const [visible, setVisible] = useState(false);
const [selectedItem, setSelectedItem] = useState("Time based");
const [showPassword, setShowPassword] = useState(false);
const [visible, setVisible] = React.useState(false);
const openMenu = () => setVisible(true); const openMenu = () => setVisible(true);
const closeMenu = () => setVisible(false); const closeMenu = () => setVisible(false);
const [selectedItem, setSelectedItem] = useState("Time based");
const handleMenuItemPress = (item) => { const handleMenuItemPress = useCallback((item) => {
setSelectedItem(item); setSelectedItem(item);
closeMenu(); closeMenu();
}; }, []);
const handleAddAccount = useCallback(() => {
if (accountName.trim() === "") {
setAccountNameError("Account Name is required");
}
if (secretKey.trim() === "") {
setSecretError("Secret Key is required");
}
if (accountName.trim() === "" || secretKey.trim() === "") {
Toast.show({
type: "error",
text1: "Error",
text2: "Please fill in all the fields!",
autoHide: true,
});
return;
}
if (secretError) {
Toast.show({
type: "error",
text1: "Invalid Secret Key",
text2: "Please check your secret key and try again.",
autoHide: true,
});
return;
}
const handleAddAccount = () => {
onAdd({accountName, secretKey}); onAdd({accountName, secretKey});
setAccountName(""); setAccountName("");
setSecretKey(""); setSecretKey("");
}; setAccountNameError("");
setSecretError("");
}, [accountName, secretKey, secretError, onAdd]);
const handleSecretKeyChange = useCallback((text) => {
setSecretKey(text);
if (validateSecret) {
const isValid = validateSecret(text);
setSecretError(isValid || text.trim() === "" ? "" : "Invalid Secret Key");
}
}, [validateSecret]);
const handleAccountNameChange = useCallback((text) => {
setAccountName(text);
if (accountNameError) {
setAccountNameError("");
}
}, [accountNameError]);
return ( return (
<View style={{flex: 1, justifyContent: "center", alignItems: "center"}}> <View style={styles.container}>
<Text style={{fontSize: 24, marginBottom: 5}}>Add new 2FA account</Text> <View style={styles.content}>
<View style={{flexDirection: "row", alignItems: "center"}}> <View style={styles.header}>
<IconButton icon="account-details" size={35} /> <Text style={styles.title}>Add Account</Text>
<IconButton
icon="close"
size={24}
onPress={onClose}
style={styles.closeButton}
/>
</View>
<TextInput <TextInput
label="Account Name" label="Account Name"
placeholder="Account Name"
value={accountName} value={accountName}
autoCapitalize="none" onChangeText={handleAccountNameChange}
onChangeText={(text) => setAccountName(text)} error={!!accountNameError}
style={{borderWidth: 3, borderColor: "white", margin: 10, width: 230, height: 50, borderRadius: 5, fontSize: 18, color: "gray", paddingLeft: 10}} style={styles.input}
mode="outlined"
/> />
</View>
<View style={{flexDirection: "row", alignItems: "center"}}>
<IconButton icon="account-key" size={35} />
<TextInput <TextInput
label="Secret Key" label="Secret Key"
placeholder="Secret Key"
value={secretKey} value={secretKey}
autoCapitalize="none" onChangeText={handleSecretKeyChange}
onChangeText={(text) => setSecretKey(text)} secureTextEntry={!showPassword}
secureTextEntry error={!!secretError}
style={{borderWidth: 3, borderColor: "white", margin: 10, width: 230, height: 50, borderRadius: 5, fontSize: 18, color: "gray", paddingLeft: 10}} style={styles.input}
mode="outlined"
right={
<TextInput.Icon
icon={showPassword ? "eye-off" : "eye"}
onPress={() => setShowPassword(!showPassword)}
/> />
</View> }
<Button />
icon="account-plus" <View style={styles.buttonContainer}>
style={{
backgroundColor: "#E6DFF3",
borderRadius: 5,
margin: 10,
alignItems: "center",
position: "absolute",
top: 230,
right: 30,
width: 90,
}}
onPress={handleAddAccount}
>
<Text style={{fontSize: 18}}>Add</Text>
</Button>
<IconButton icon={"close"} size={30} onPress={onClose} style={{position: "absolute", top: 5, right: 5}} />
<View
style={{
backgroundColor: "#E6DFF3",
borderRadius: 5,
position: "absolute",
left: 30,
top: 240,
width: 140,
}}
>
<Menu <Menu
visible={visible} visible={visible}
onDismiss={closeMenu} onDismiss={closeMenu}
anchor={ anchor={
<Button style={{alignItems: "left"}} icon={"chevron-down"} onPress={openMenu}> <Button
onPress={openMenu}
mode="outlined"
icon="chevron-down"
contentStyle={styles.menuButtonContent}
style={styles.menuButton}
>
{selectedItem} {selectedItem}
</Button> </Button>
} }
contentStyle={styles.menuContent}
> >
<Menu.Item onPress={() => handleMenuItemPress("Time based")} title="Time based" /> <Menu.Item onPress={() => handleMenuItemPress("Time based")} title="Time based" />
<Divider />
<Menu.Item onPress={() => handleMenuItemPress("Counter based")} title="Counter based" /> <Menu.Item onPress={() => handleMenuItemPress("Counter based")} title="Counter based" />
</Menu> </Menu>
<Button
mode="contained"
onPress={handleAddAccount}
style={styles.addButton}
labelStyle={styles.buttonLabel}
>
Add Account
</Button>
</View>
</View> </View>
</View> </View>
); );
} };
const styles = {
container: {
flex: 1,
justifyContent: "center",
alignItems: "center",
},
content: {
width: "100%",
borderRadius: 10,
padding: 20,
backgroundColor: "#F5F5F5",
shadowColor: "#000",
shadowOffset: {width: 0, height: 2},
shadowOpacity: 0.1,
shadowRadius: 10,
elevation: 5,
},
header: {
position: "relative",
alignItems: "center",
marginBottom: 20,
},
title: {
fontSize: 24,
fontWeight: "bold",
color: "#333",
textAlign: "center",
},
closeButton: {
position: "absolute",
right: 0,
top: -8,
},
input: {
marginVertical: 10,
fontSize: 16,
backgroundColor: "white",
},
buttonContainer: {
flexDirection: "row",
justifyContent: "space-between",
marginTop: 10,
},
menuButton: {
flex: 1,
marginRight: 10,
height: 50,
justifyContent: "center",
fontSize: 12,
},
menuButtonContent: {
height: 50,
justifyContent: "center",
},
menuContent: {
backgroundColor: "#FFFFFF",
borderRadius: 8,
elevation: 3,
shadowColor: "#000000",
shadowOffset: {width: 0, height: 2},
shadowOpacity: 0.2,
shadowRadius: 3,
},
addButton: {
flex: 1,
backgroundColor: "#8A7DF7",
height: 50,
justifyContent: "center",
paddingHorizontal: 5,
},
buttonLabel: {
fontSize: 14,
color: "white",
textAlign: "center",
},
};
export default EnterAccountDetails;

View File

@ -13,8 +13,9 @@
// limitations under the License. // limitations under the License.
import React from "react"; import React from "react";
import {Alert, Text, View} from "react-native"; import {ScrollView, Text, View} from "react-native";
import {Button, IconButton, Portal, TextInput} from "react-native-paper"; import {Button, IconButton, Portal, TextInput} from "react-native-paper";
import Toast from "react-native-toast-message";
import DefaultCasdoorSdkConfig from "./DefaultCasdoorSdkConfig"; import DefaultCasdoorSdkConfig from "./DefaultCasdoorSdkConfig";
import PropTypes from "prop-types"; import PropTypes from "prop-types";
import useStore from "./useStorage"; import useStore from "./useStorage";
@ -22,6 +23,7 @@ import useStore from "./useStorage";
const EnterCasdoorSdkConfig = ({onClose, onWebviewClose}) => { const EnterCasdoorSdkConfig = ({onClose, onWebviewClose}) => {
EnterCasdoorSdkConfig.propTypes = { EnterCasdoorSdkConfig.propTypes = {
onClose: PropTypes.func.isRequired, onClose: PropTypes.func.isRequired,
onWebviewClose: PropTypes.func.isRequired,
}; };
const { const {
@ -44,12 +46,26 @@ const EnterCasdoorSdkConfig = ({onClose, onWebviewClose}) => {
const handleSave = () => { const handleSave = () => {
if (!serverUrl || !clientId || !appName || !organizationName || !redirectPath) { if (!serverUrl || !clientId || !appName || !organizationName || !redirectPath) {
Alert.alert("Please fill in all the fields!"); Toast.show({
type: "error",
text1: "Error",
text2: "Please fill in all the fields!",
autoHide: true,
});
return; return;
} }
onClose(); onClose();
}; };
const handleScanToLogin = () => {
Toast.show({
type: "info",
text1: "Info",
text2: "Scan to Login functionality not implemented yet.",
autoHide: true,
});
};
const handleUseDefault = () => { const handleUseDefault = () => {
setCasdoorConfig(DefaultCasdoorSdkConfig); setCasdoorConfig(DefaultCasdoorSdkConfig);
onClose(); onClose();
@ -57,115 +73,155 @@ const EnterCasdoorSdkConfig = ({onClose, onWebviewClose}) => {
return ( return (
<Portal> <Portal>
<View style={{flex: 1, justifyContent: "center", alignItems: "center", backgroundColor: "white"}}> <ScrollView contentContainerStyle={styles.scrollContainer}>
<View style={{top: -60, flex: 1, justifyContent: "center", alignItems: "center", backgroundColor: "white"}}> <View style={styles.content}>
<Text style={{fontSize: 24, marginBottom: 5}}>Casdoor server</Text> <View style={styles.header}>
<Text style={styles.title}>Casdoor server</Text>
<IconButton
icon="close"
size={24}
onPress={closeConfigPage}
style={styles.closeButton}
/>
</View>
<TextInput <TextInput
label="Endpoint" label="Endpoint"
value={serverUrl} value={serverUrl}
onChangeText={setServerUrl} onChangeText={setServerUrl}
autoCapitalize="none" autoCapitalize="none"
style={{ style={styles.input}
borderWidth: 3, mode="outlined"
borderColor: "white",
margin: 10,
width: 300,
height: 50,
borderRadius: 5,
fontSize: 18,
color: "gray",
paddingLeft: 10,
}}
/> />
<TextInput <TextInput
label="ClientID" label="Client ID"
value={clientId} value={clientId}
onChangeText={setClientId} onChangeText={setClientId}
autoCapitalize="none" autoCapitalize="none"
style={{ style={styles.input}
borderWidth: 3, mode="outlined"
borderColor: "white",
margin: 10,
width: 300,
height: 50,
borderRadius: 5,
fontSize: 18,
color: "gray",
paddingLeft: 10,
}}
/> />
<TextInput <TextInput
label="appName" label="App Name"
value={appName} value={appName}
onChangeText={setAppName} onChangeText={setAppName}
autoCapitalize="none" autoCapitalize="none"
style={{ style={styles.input}
borderWidth: 3, mode="outlined"
borderColor: "white",
margin: 10,
width: 300,
height: 50,
borderRadius: 5,
fontSize: 18,
color: "gray",
paddingLeft: 10,
}}
/> />
<TextInput <TextInput
label="organizationName" label="Organization Name"
value={organizationName} value={organizationName}
onChangeText={setOrganizationName} onChangeText={setOrganizationName}
autoCapitalize="none" autoCapitalize="none"
style={{ style={styles.input}
borderWidth: 3, mode="outlined"
borderColor: "white",
margin: 10,
width: 300,
height: 50,
borderRadius: 5,
fontSize: 18,
color: "gray",
paddingLeft: 10,
}}
/> />
<View style={styles.buttonRow}>
<Button <Button
mode="contained" mode="contained"
onPress={handleSave} onPress={handleSave}
style={{ style={[styles.button, styles.confirmButton]}
backgroundColor: "#E6DFF3", labelStyle={styles.buttonLabel}
borderRadius: 5,
margin: 10,
alignItems: "center",
position: "absolute",
top: 600,
width: 300,
height: 50,
display: "flex",
justifyContent: "center",
}}
> >
<Text style={{fontSize: 21, width: 300, color: "black"}}>Confirm</Text> Confirm
</Button> </Button>
<Button <Button
mode="contained" mode="contained"
onPress={handleUseDefault} onPress={handleScanToLogin}
style={{ style={[styles.button, styles.scanButton]}
backgroundColor: "#E6DFF3", labelStyle={styles.buttonLabel}
borderRadius: 5,
margin: 10,
alignItems: "center",
position: "absolute",
top: 660,
width: 300,
}}
> >
<Text style={{fontSize: 18, width: 300, color: "black"}}>Use Casdoor Demo Site</Text> Scan to Login
</Button> </Button>
<IconButton icon={"close"} size={30} onPress={closeConfigPage} style={{position: "absolute", top: 120, right: -30}} />
</View> </View>
<Button
mode="outlined"
onPress={handleUseDefault}
style={[styles.button, styles.outlinedButton]}
labelStyle={styles.outlinedButtonLabel}
>
Use Casdoor Demo Site
</Button>
</View> </View>
</ScrollView>
</Portal> </Portal>
); );
}; };
const styles = {
scrollContainer: {
flexGrow: 1,
width: "100%",
justifyContent: "center",
alignItems: "center",
backgroundColor: "rgba(255, 255, 255, 0.5)",
},
content: {
width: "95%",
borderRadius: 10,
padding: 20,
backgroundColor: "#F5F5F5",
shadowColor: "#000",
shadowOffset: {width: 0, height: 2},
shadowOpacity: 0.1,
shadowRadius: 10,
elevation: 5,
},
input: {
marginVertical: 10,
fontSize: 16,
backgroundColor: "white",
},
buttonRow: {
flexDirection: "row",
justifyContent: "space-between",
marginTop: 14,
marginBottom: 12,
},
button: {
borderRadius: 5,
paddingVertical: 8,
},
confirmButton: {
backgroundColor: "#6200EE",
flex: 1,
marginRight: 5,
},
scanButton: {
backgroundColor: "#03DAC6",
flex: 1,
marginLeft: 5,
},
buttonLabel: {
fontSize: 16,
color: "white",
},
outlinedButton: {
borderColor: "#6200EE",
borderWidth: 1,
width: "100%",
},
outlinedButtonLabel: {
color: "#6200EE",
fontSize: 16,
textAlign: "center",
},
header: {
position: "relative",
alignItems: "center",
marginBottom: 20,
},
title: {
fontSize: 24,
fontWeight: "bold",
color: "#333",
textAlign: "center",
},
closeButton: {
position: "absolute",
right: 0,
top: -8,
},
};
export default EnterCasdoorSdkConfig; export default EnterCasdoorSdkConfig;

View File

@ -15,13 +15,16 @@
import * as React from "react"; import * as React from "react";
import {Dimensions, StyleSheet, View} from "react-native"; import {Dimensions, StyleSheet, View} from "react-native";
import {Appbar, Avatar, Menu, Text, TouchableRipple} from "react-native-paper"; import {Appbar, Avatar, Menu, Text, TouchableRipple} from "react-native-paper";
import Toast from "react-native-toast-message";
import CasdoorLoginPage, {CasdoorLogout} from "./CasdoorLoginPage"; import CasdoorLoginPage, {CasdoorLogout} from "./CasdoorLoginPage";
import useStore from "./useStorage"; import useStore from "./useStorage";
import useSyncStore from "./useSyncStore";
const {width} = Dimensions.get("window"); const {width} = Dimensions.get("window");
const Header = () => { const Header = () => {
const {userInfo, clearAll} = useStore(); const {userInfo, clearAll} = useStore();
const syncError = useSyncStore(state => state.syncError);
const [showLoginPage, setShowLoginPage] = React.useState(false); const [showLoginPage, setShowLoginPage] = React.useState(false);
const [menuVisible, setMenuVisible] = React.useState(false); const [menuVisible, setMenuVisible] = React.useState(false);
@ -41,8 +44,27 @@ const Header = () => {
clearAll(); clearAll();
}; };
const handleSyncErrorPress = () => {
Toast.show({
type: "error",
text1: "Sync Error",
text2: syncError || "An unknown error occurred during synchronization.",
autoHide: true,
});
};
return ( return (
<Appbar.Header> <Appbar.Header mode="center-aligned">
<View style={styles.leftContainer}>
{true && syncError && (
<Appbar.Action
icon="sync-alert"
color="#E53935"
size={24}
onPress={handleSyncErrorPress}
/>
)}
</View>
<Appbar.Content <Appbar.Content
title="Casdoor" title="Casdoor"
titleStyle={styles.titleText} titleStyle={styles.titleText}

View File

@ -19,6 +19,7 @@ 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 {useNetInfo} from "@react-native-community/netinfo";
import {FlashList} from "@shopify/flash-list"; import {FlashList} from "@shopify/flash-list";
import Toast from "react-native-toast-message";
import * as SQLite from "expo-sqlite/next"; import * as SQLite from "expo-sqlite/next";
import SearchBar from "./SearchBar"; import SearchBar from "./SearchBar";
@ -49,11 +50,13 @@ export default function HomePage() {
const [refreshing, setRefreshing] = useState(false); const [refreshing, setRefreshing] = useState(false);
const {isConnected} = useNetInfo(); const {isConnected} = useNetInfo();
const [canSync, setCanSync] = useState(false); const [canSync, setCanSync] = useState(false);
const [key, setKey] = useState(0);
const swipeableRef = useRef(null); const swipeableRef = useRef(null);
const db = SQLite.useSQLiteContext();
const {userInfo, serverUrl, token} = useStore(); const {userInfo, serverUrl, token} = useStore();
const {startSync} = useSyncStore(); const {startSync} = useSyncStore();
const db = SQLite.useSQLiteContext(); const syncError = useSyncStore(state => state.syncError);
useEffect(() => { useEffect(() => {
if (db) { if (db) {
@ -89,11 +92,29 @@ export default function HomePage() {
const onRefresh = async() => { const onRefresh = async() => {
setRefreshing(true); setRefreshing(true);
if (canSync) {await startSync(db, userInfo, serverUrl, token);} if (canSync) {
await startSync(db, userInfo, serverUrl, token);
if (syncError) {
Toast.show({
type: "error",
text1: "Sync error",
text2: syncError,
autoHide: true,
});
} else {
Toast.show({
type: "success",
text1: "Sync success",
text2: "All your accounts are up to date.",
autoHide: true,
});
}
}
setRefreshing(false); setRefreshing(false);
}; };
const handleAddAccount = async(accountData) => { const handleAddAccount = async(accountData) => {
setKey(prevKey => prevKey + 1);
await TotpDatabase.insertAccount(db, accountData); await TotpDatabase.insertAccount(db, accountData);
closeEnterAccountModal(); closeEnterAccountModal();
}; };
@ -218,6 +239,7 @@ export default function HomePage() {
right={() => ( right={() => (
<View style={{justifyContent: "center", alignItems: "center"}}> <View style={{justifyContent: "center", alignItems: "center"}}>
<CountdownCircleTimer <CountdownCircleTimer
key={key}
isPlaying={true} isPlaying={true}
duration={30} duration={30}
initialRemainingTime={TotpDatabase.calculateCountdown()} initialRemainingTime={TotpDatabase.calculateCountdown()}
@ -226,7 +248,11 @@ export default function HomePage() {
size={60} size={60}
onComplete={() => { onComplete={() => {
TotpDatabase.updateToken(db, item.id); TotpDatabase.updateToken(db, item.id);
return {shouldRepeat: true, delay: 0}; return {
shouldRepeat: true,
delay: 0,
newInitialRemainingTime: TotpDatabase.calculateCountdown(),
};
}} }}
strokeWidth={5} strokeWidth={5}
> >
@ -292,7 +318,7 @@ export default function HomePage() {
transform: [{translateX: -OFFSET_X}, {translateY: -OFFSET_Y}], transform: [{translateX: -OFFSET_X}, {translateY: -OFFSET_Y}],
}} }}
> >
<EnterAccountDetails onClose={closeEnterAccountModal} onAdd={handleAddAccount} /> <EnterAccountDetails onClose={closeEnterAccountModal} onAdd={handleAddAccount} validateSecret={TotpDatabase.validateSecret} />
</Modal> </Modal>
</Portal> </Portal>

View File

@ -13,8 +13,8 @@
// limitations under the License. // limitations under the License.
import React, {useEffect, useState} from "react"; import React, {useEffect, useState} from "react";
import {Dimensions, Text, View} from "react-native"; import {Text, View} from "react-native";
import {IconButton, Modal, Portal} from "react-native-paper"; import {IconButton, Portal} from "react-native-paper";
import {Camera, CameraView} from "expo-camera"; import {Camera, CameraView} from "expo-camera";
import PropTypes from "prop-types"; import PropTypes from "prop-types";
@ -54,26 +54,9 @@ const ScanQRCode = ({onClose, showScanner, onAdd}) => {
closeOptions(); closeOptions();
}; };
const {width, height} = Dimensions.get("window");
const offsetX = width * 0.5;
const offsetY = height * 0.5;
return ( return (
<View style={{marginTop: "50%", flex: 1}} > <View style={{marginTop: "50%", flex: 1}} >
<Portal> <Portal>
<Modal
visible={showScanner}
onDismiss={closeOptions}
contentContainerStyle={{
backgroundColor: "white",
width: width,
height: height,
position: "absolute",
top: "50%",
left: "50%",
transform: [{translateX: -offsetX}, {translateY: -offsetY}],
}}
>
{hasPermission === null ? ( {hasPermission === null ? (
<Text style={{marginLeft: "20%", marginRight: "20%"}}>Requesting for camera permission</Text> <Text style={{marginLeft: "20%", marginRight: "20%"}}>Requesting for camera permission</Text>
) : hasPermission === false ? ( ) : hasPermission === false ? (
@ -88,7 +71,6 @@ const ScanQRCode = ({onClose, showScanner, onAdd}) => {
/> />
)} )}
<IconButton icon={"close"} size={40} onPress={onClose} style={{position: "absolute", top: 30, right: 5}} /> <IconButton icon={"close"} size={40} onPress={onClose} style={{position: "absolute", top: 30, right: 5}} />
</Modal>
</Portal> </Portal>
</View> </View>
); );

View File

@ -13,14 +13,15 @@
// limitations under the License. // limitations under the License.
import * as React from "react"; import * as React from "react";
import {Button} from "react-native-paper"; import {StyleSheet, View, useWindowDimensions} from "react-native";
import {View} from "react-native"; import {Button, Surface, Text} from "react-native-paper";
import CasdoorLoginPage, {CasdoorLogout} from "./CasdoorLoginPage"; import CasdoorLoginPage, {CasdoorLogout} from "./CasdoorLoginPage";
import useStore from "./useStorage"; import useStore from "./useStorage";
const SettingPage = () => { const SettingPage = () => {
const [showLoginPage, setShowLoginPage] = React.useState(false); const [showLoginPage, setShowLoginPage] = React.useState(false);
const {userInfo, clearAll} = useStore(); const {userInfo, clearAll} = useStore();
const {width} = useWindowDimensions();
const handleCasdoorLogin = () => setShowLoginPage(true); const handleCasdoorLogin = () => setShowLoginPage(true);
const handleHideLoginPage = () => setShowLoginPage(false); const handleHideLoginPage = () => setShowLoginPage(false);
@ -30,16 +31,42 @@ const SettingPage = () => {
clearAll(); clearAll();
}; };
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: "center",
alignItems: "center",
padding: 16,
},
surface: {
padding: 16,
width: width > 600 ? 400 : "100%",
maxWidth: 400,
alignItems: "center",
},
title: {
fontSize: 24,
marginBottom: 24,
},
button: {
marginTop: 16,
width: "100%",
},
});
return ( return (
<View> <View style={styles.container}>
<Surface style={styles.surface} elevation={4}>
<Text style={styles.title}>Account Settings</Text>
<Button <Button
style={{marginTop: "50%", marginLeft: "20%", marginRight: "20%"}} style={styles.button}
icon={userInfo === null ? "login" : "logout"} icon={userInfo === null ? "login" : "logout"}
mode="contained" mode="contained"
onPress={userInfo === null ? handleCasdoorLogin : handleCasdoorLogout} onPress={userInfo === null ? handleCasdoorLogin : handleCasdoorLogout}
> >
{userInfo === null ? "Login with Casdoor" : "Logout"} {userInfo === null ? "Login with Casdoor" : "Logout"}
</Button> </Button>
</Surface>
{showLoginPage && <CasdoorLoginPage onWebviewClose={handleHideLoginPage} />} {showLoginPage && <CasdoorLoginPage onWebviewClose={handleHideLoginPage} />}
</View> </View>
); );

View File

@ -45,6 +45,17 @@ CREATE TABLE accounts (
await db.execAsync(`PRAGMA user_version = ${DATABASE_VERSION}`); await db.execAsync(`PRAGMA user_version = ${DATABASE_VERSION}`);
} }
export async function clearDatabase(db) {
try {
await db.execAsync("DELETE FROM accounts");
await db.execAsync("DELETE FROM sqlite_sequence WHERE name='accounts'");
await db.execAsync("PRAGMA user_version = 0");
return true;
} catch (error) {
return false;
}
}
const generateToken = (secretKey) => { const generateToken = (secretKey) => {
if (secretKey !== null && secretKey !== undefined && secretKey !== "") { if (secretKey !== null && secretKey !== undefined && secretKey !== "") {
try { try {
@ -147,7 +158,6 @@ export async function getAllAccounts(db) {
const mappedAccount = { const mappedAccount = {
...account, ...account,
accountName: account.account_name, accountName: account.account_name,
secretKey: account.secret,
}; };
return mappedAccount; return mappedAccount;
}); });
@ -173,10 +183,18 @@ async function updateSyncTimeForAll(db) {
} }
export function calculateCountdown() { export function calculateCountdown() {
const now = Math.floor(Date.now() / 1000); const now = Math.round(new Date().getTime() / 1000.0);
return 30 - (now % 30); return 30 - (now % 30);
} }
export function validateSecret(secret) {
const base32Regex = /^[A-Z2-7]+=*$/i;
if (!secret || secret.length % 8 !== 0) {
return false;
}
return base32Regex.test(secret);
}
async function updateLocalDatabase(db, mergedAccounts) { async function updateLocalDatabase(db, mergedAccounts) {
for (const account of mergedAccounts) { for (const account of mergedAccounts) {
if (account.id) { if (account.id) {

View File

@ -1,7 +1,7 @@
{ {
"expo": { "expo": {
"name": "Casdoor", "name": "Casdoor",
"slug": "Casdoor", "slug": "casdoor-app",
"version": "1.2.0", "version": "1.2.0",
"orientation": "portrait", "orientation": "portrait",
"icon": "./assets/icon.png", "icon": "./assets/icon.png",