feat: add secret check (#20)
This commit is contained in:
parent
e96d171cf8
commit
a46b4a25c8
2
App.js
2
App.js
|
@ -17,6 +17,7 @@ import {PaperProvider} from "react-native-paper";
|
|||
import {NavigationContainer} from "@react-navigation/native";
|
||||
import {BulletList} from "react-content-loader/native";
|
||||
import {SQLiteProvider} from "expo-sqlite";
|
||||
import Toast from "react-native-toast-message";
|
||||
import Header from "./Header";
|
||||
import NavigationBar from "./NavigationBar";
|
||||
import {migrateDb} from "./TotpDatabase";
|
||||
|
@ -31,6 +32,7 @@ const App = () => {
|
|||
<NavigationBar />
|
||||
</PaperProvider>
|
||||
</NavigationContainer>
|
||||
<Toast />
|
||||
</SQLiteProvider>
|
||||
</React.Suspense>
|
||||
);
|
||||
|
|
|
@ -14,10 +14,12 @@
|
|||
|
||||
import React, {useEffect, useState} from "react";
|
||||
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 SDK from "casdoor-react-native-sdk";
|
||||
import PropTypes from "prop-types";
|
||||
import Toast from "react-native-toast-message";
|
||||
|
||||
import EnterCasdoorSdkConfig from "./EnterCasdoorSdkConfig";
|
||||
import useStore from "./useStorage";
|
||||
// import {LogBox} from "react-native";
|
||||
|
@ -28,6 +30,7 @@ const CasdoorLoginPage = ({onWebviewClose}) => {
|
|||
CasdoorLoginPage.propTypes = {
|
||||
onWebviewClose: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
const [casdoorLoginURL, setCasdoorLoginURL] = useState("");
|
||||
const [showConfigPage, setShowConfigPage] = useState(true);
|
||||
|
||||
|
@ -45,6 +48,11 @@ const CasdoorLoginPage = ({onWebviewClose}) => {
|
|||
const handleHideConfigPage = () => {
|
||||
setShowConfigPage(false);
|
||||
};
|
||||
|
||||
const handleShowConfigPage = () => {
|
||||
setShowConfigPage(true);
|
||||
};
|
||||
|
||||
const getCasdoorSignInUrl = async() => {
|
||||
const signinUrl = await sdk.getSigninUrl();
|
||||
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 (
|
||||
<Portal>
|
||||
<View style={{flex: 1}}>
|
||||
{showConfigPage && <EnterCasdoorSdkConfig onClose={handleHideConfigPage} onWebviewClose={onWebviewClose} />}
|
||||
{!showConfigPage && casdoorLoginURL !== "" && (
|
||||
<WebView
|
||||
source={{uri: casdoorLoginURL}}
|
||||
onNavigationStateChange={onNavigationStateChange}
|
||||
style={{flex: 1}}
|
||||
mixedContentMode="always"
|
||||
javaScriptEnabled={true}
|
||||
<SafeAreaView style={styles.container}>
|
||||
{showConfigPage && (
|
||||
<EnterCasdoorSdkConfig
|
||||
onClose={handleHideConfigPage}
|
||||
onWebviewClose={onWebviewClose}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
{!showConfigPage && casdoorLoginURL !== "" && (
|
||||
<>
|
||||
<TouchableOpacity
|
||||
style={styles.backButton}
|
||||
onPress={handleShowConfigPage}
|
||||
>
|
||||
<Text style={styles.backButtonText}>Back to Config</Text>
|
||||
</TouchableOpacity>
|
||||
<WebView
|
||||
source={{uri: casdoorLoginURL}}
|
||||
onNavigationStateChange={onNavigationStateChange}
|
||||
onError={(syntheticEvent) => {
|
||||
const {nativeEvent} = syntheticEvent;
|
||||
handleErrorResponse(nativeEvent);
|
||||
}}
|
||||
style={styles.webview}
|
||||
mixedContentMode="always"
|
||||
javaScriptEnabled={true}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</SafeAreaView>
|
||||
</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 = () => {
|
||||
if (sdk) {sdk.clearState();}
|
||||
};
|
||||
|
|
|
@ -12,104 +12,230 @@
|
|||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import React, {useState} from "react";
|
||||
import {Text, TextInput, View} from "react-native";
|
||||
import {Button, Divider, IconButton, Menu} from "react-native-paper";
|
||||
import React, {useCallback, useState} from "react";
|
||||
import {View} from "react-native";
|
||||
import {Button, IconButton, Menu, Text, TextInput} from "react-native-paper";
|
||||
import Toast from "react-native-toast-message";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
export default function EnterAccountDetails({onClose, onAdd}) {
|
||||
const EnterAccountDetails = ({onClose, onAdd, validateSecret}) => {
|
||||
EnterAccountDetails.propTypes = {
|
||||
onClose: PropTypes.func.isRequired,
|
||||
onAdd: PropTypes.func.isRequired,
|
||||
validateSecret: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
const [accountName, setAccountName] = 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 closeMenu = () => setVisible(false);
|
||||
const [selectedItem, setSelectedItem] = useState("Time based");
|
||||
|
||||
const handleMenuItemPress = (item) => {
|
||||
const handleMenuItemPress = useCallback((item) => {
|
||||
setSelectedItem(item);
|
||||
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});
|
||||
setAccountName("");
|
||||
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 (
|
||||
<View style={{flex: 1, justifyContent: "center", alignItems: "center"}}>
|
||||
<Text style={{fontSize: 24, marginBottom: 5}}>Add new 2FA account</Text>
|
||||
<View style={{flexDirection: "row", alignItems: "center"}}>
|
||||
<IconButton icon="account-details" size={35} />
|
||||
<View style={styles.container}>
|
||||
<View style={styles.content}>
|
||||
<View style={styles.header}>
|
||||
<Text style={styles.title}>Add Account</Text>
|
||||
<IconButton
|
||||
icon="close"
|
||||
size={24}
|
||||
onPress={onClose}
|
||||
style={styles.closeButton}
|
||||
/>
|
||||
</View>
|
||||
<TextInput
|
||||
label="Account Name"
|
||||
placeholder="Account Name"
|
||||
value={accountName}
|
||||
autoCapitalize="none"
|
||||
onChangeText={(text) => setAccountName(text)}
|
||||
style={{borderWidth: 3, borderColor: "white", margin: 10, width: 230, height: 50, borderRadius: 5, fontSize: 18, color: "gray", paddingLeft: 10}}
|
||||
onChangeText={handleAccountNameChange}
|
||||
error={!!accountNameError}
|
||||
style={styles.input}
|
||||
mode="outlined"
|
||||
/>
|
||||
</View>
|
||||
|
||||
<View style={{flexDirection: "row", alignItems: "center"}}>
|
||||
<IconButton icon="account-key" size={35} />
|
||||
<TextInput
|
||||
label="Secret Key"
|
||||
placeholder="Secret Key"
|
||||
value={secretKey}
|
||||
autoCapitalize="none"
|
||||
onChangeText={(text) => setSecretKey(text)}
|
||||
secureTextEntry
|
||||
style={{borderWidth: 3, borderColor: "white", margin: 10, width: 230, height: 50, borderRadius: 5, fontSize: 18, color: "gray", paddingLeft: 10}}
|
||||
/>
|
||||
</View>
|
||||
<Button
|
||||
icon="account-plus"
|
||||
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
|
||||
visible={visible}
|
||||
onDismiss={closeMenu}
|
||||
anchor={
|
||||
<Button style={{alignItems: "left"}} icon={"chevron-down"} onPress={openMenu}>
|
||||
{selectedItem}
|
||||
</Button>
|
||||
onChangeText={handleSecretKeyChange}
|
||||
secureTextEntry={!showPassword}
|
||||
error={!!secretError}
|
||||
style={styles.input}
|
||||
mode="outlined"
|
||||
right={
|
||||
<TextInput.Icon
|
||||
icon={showPassword ? "eye-off" : "eye"}
|
||||
onPress={() => setShowPassword(!showPassword)}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Menu.Item onPress={() => handleMenuItemPress("Time based")} title="Time based" />
|
||||
<Divider />
|
||||
<Menu.Item onPress={() => handleMenuItemPress("Counter based")} title="Counter based" />
|
||||
</Menu>
|
||||
/>
|
||||
<View style={styles.buttonContainer}>
|
||||
<Menu
|
||||
visible={visible}
|
||||
onDismiss={closeMenu}
|
||||
anchor={
|
||||
<Button
|
||||
onPress={openMenu}
|
||||
mode="outlined"
|
||||
icon="chevron-down"
|
||||
contentStyle={styles.menuButtonContent}
|
||||
style={styles.menuButton}
|
||||
>
|
||||
{selectedItem}
|
||||
</Button>
|
||||
}
|
||||
contentStyle={styles.menuContent}
|
||||
>
|
||||
<Menu.Item onPress={() => handleMenuItemPress("Time based")} title="Time based" />
|
||||
<Menu.Item onPress={() => handleMenuItemPress("Counter based")} title="Counter based" />
|
||||
</Menu>
|
||||
<Button
|
||||
mode="contained"
|
||||
onPress={handleAddAccount}
|
||||
style={styles.addButton}
|
||||
labelStyle={styles.buttonLabel}
|
||||
>
|
||||
Add Account
|
||||
</Button>
|
||||
</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;
|
||||
|
|
|
@ -13,8 +13,9 @@
|
|||
// limitations under the License.
|
||||
|
||||
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 Toast from "react-native-toast-message";
|
||||
import DefaultCasdoorSdkConfig from "./DefaultCasdoorSdkConfig";
|
||||
import PropTypes from "prop-types";
|
||||
import useStore from "./useStorage";
|
||||
|
@ -22,6 +23,7 @@ import useStore from "./useStorage";
|
|||
const EnterCasdoorSdkConfig = ({onClose, onWebviewClose}) => {
|
||||
EnterCasdoorSdkConfig.propTypes = {
|
||||
onClose: PropTypes.func.isRequired,
|
||||
onWebviewClose: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
const {
|
||||
|
@ -44,12 +46,26 @@ const EnterCasdoorSdkConfig = ({onClose, onWebviewClose}) => {
|
|||
|
||||
const handleSave = () => {
|
||||
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;
|
||||
}
|
||||
onClose();
|
||||
};
|
||||
|
||||
const handleScanToLogin = () => {
|
||||
Toast.show({
|
||||
type: "info",
|
||||
text1: "Info",
|
||||
text2: "Scan to Login functionality not implemented yet.",
|
||||
autoHide: true,
|
||||
});
|
||||
};
|
||||
|
||||
const handleUseDefault = () => {
|
||||
setCasdoorConfig(DefaultCasdoorSdkConfig);
|
||||
onClose();
|
||||
|
@ -57,115 +73,155 @@ const EnterCasdoorSdkConfig = ({onClose, onWebviewClose}) => {
|
|||
|
||||
return (
|
||||
<Portal>
|
||||
<View style={{flex: 1, justifyContent: "center", alignItems: "center", backgroundColor: "white"}}>
|
||||
<View style={{top: -60, flex: 1, justifyContent: "center", alignItems: "center", backgroundColor: "white"}}>
|
||||
<Text style={{fontSize: 24, marginBottom: 5}}>Casdoor server</Text>
|
||||
<ScrollView contentContainerStyle={styles.scrollContainer}>
|
||||
<View style={styles.content}>
|
||||
<View style={styles.header}>
|
||||
<Text style={styles.title}>Casdoor server</Text>
|
||||
<IconButton
|
||||
icon="close"
|
||||
size={24}
|
||||
onPress={closeConfigPage}
|
||||
style={styles.closeButton}
|
||||
/>
|
||||
</View>
|
||||
<TextInput
|
||||
label="Endpoint"
|
||||
value={serverUrl}
|
||||
onChangeText={setServerUrl}
|
||||
autoCapitalize="none"
|
||||
style={{
|
||||
borderWidth: 3,
|
||||
borderColor: "white",
|
||||
margin: 10,
|
||||
width: 300,
|
||||
height: 50,
|
||||
borderRadius: 5,
|
||||
fontSize: 18,
|
||||
color: "gray",
|
||||
paddingLeft: 10,
|
||||
}}
|
||||
style={styles.input}
|
||||
mode="outlined"
|
||||
/>
|
||||
<TextInput
|
||||
label="ClientID"
|
||||
label="Client ID"
|
||||
value={clientId}
|
||||
onChangeText={setClientId}
|
||||
autoCapitalize="none"
|
||||
style={{
|
||||
borderWidth: 3,
|
||||
borderColor: "white",
|
||||
margin: 10,
|
||||
width: 300,
|
||||
height: 50,
|
||||
borderRadius: 5,
|
||||
fontSize: 18,
|
||||
color: "gray",
|
||||
paddingLeft: 10,
|
||||
}}
|
||||
style={styles.input}
|
||||
mode="outlined"
|
||||
/>
|
||||
<TextInput
|
||||
label="appName"
|
||||
label="App Name"
|
||||
value={appName}
|
||||
onChangeText={setAppName}
|
||||
autoCapitalize="none"
|
||||
style={{
|
||||
borderWidth: 3,
|
||||
borderColor: "white",
|
||||
margin: 10,
|
||||
width: 300,
|
||||
height: 50,
|
||||
borderRadius: 5,
|
||||
fontSize: 18,
|
||||
color: "gray",
|
||||
paddingLeft: 10,
|
||||
}}
|
||||
style={styles.input}
|
||||
mode="outlined"
|
||||
/>
|
||||
<TextInput
|
||||
label="organizationName"
|
||||
label="Organization Name"
|
||||
value={organizationName}
|
||||
onChangeText={setOrganizationName}
|
||||
autoCapitalize="none"
|
||||
style={{
|
||||
borderWidth: 3,
|
||||
borderColor: "white",
|
||||
margin: 10,
|
||||
width: 300,
|
||||
height: 50,
|
||||
borderRadius: 5,
|
||||
fontSize: 18,
|
||||
color: "gray",
|
||||
paddingLeft: 10,
|
||||
}}
|
||||
style={styles.input}
|
||||
mode="outlined"
|
||||
/>
|
||||
<View style={styles.buttonRow}>
|
||||
<Button
|
||||
mode="contained"
|
||||
onPress={handleSave}
|
||||
style={[styles.button, styles.confirmButton]}
|
||||
labelStyle={styles.buttonLabel}
|
||||
>
|
||||
Confirm
|
||||
</Button>
|
||||
<Button
|
||||
mode="contained"
|
||||
onPress={handleScanToLogin}
|
||||
style={[styles.button, styles.scanButton]}
|
||||
labelStyle={styles.buttonLabel}
|
||||
>
|
||||
Scan to Login
|
||||
</Button>
|
||||
</View>
|
||||
<Button
|
||||
mode="contained"
|
||||
onPress={handleSave}
|
||||
style={{
|
||||
backgroundColor: "#E6DFF3",
|
||||
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>
|
||||
</Button>
|
||||
<Button
|
||||
mode="contained"
|
||||
mode="outlined"
|
||||
onPress={handleUseDefault}
|
||||
style={{
|
||||
backgroundColor: "#E6DFF3",
|
||||
borderRadius: 5,
|
||||
margin: 10,
|
||||
alignItems: "center",
|
||||
position: "absolute",
|
||||
top: 660,
|
||||
width: 300,
|
||||
}}
|
||||
style={[styles.button, styles.outlinedButton]}
|
||||
labelStyle={styles.outlinedButtonLabel}
|
||||
>
|
||||
<Text style={{fontSize: 18, width: 300, color: "black"}}>Use Casdoor Demo Site</Text>
|
||||
Use Casdoor Demo Site
|
||||
</Button>
|
||||
<IconButton icon={"close"} size={30} onPress={closeConfigPage} style={{position: "absolute", top: 120, right: -30}} />
|
||||
</View>
|
||||
</View>
|
||||
</ScrollView>
|
||||
</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;
|
||||
|
|
24
Header.js
24
Header.js
|
@ -15,13 +15,16 @@
|
|||
import * as React from "react";
|
||||
import {Dimensions, StyleSheet, View} from "react-native";
|
||||
import {Appbar, Avatar, Menu, Text, TouchableRipple} from "react-native-paper";
|
||||
import Toast from "react-native-toast-message";
|
||||
import CasdoorLoginPage, {CasdoorLogout} from "./CasdoorLoginPage";
|
||||
import useStore from "./useStorage";
|
||||
import useSyncStore from "./useSyncStore";
|
||||
|
||||
const {width} = Dimensions.get("window");
|
||||
|
||||
const Header = () => {
|
||||
const {userInfo, clearAll} = useStore();
|
||||
const syncError = useSyncStore(state => state.syncError);
|
||||
const [showLoginPage, setShowLoginPage] = React.useState(false);
|
||||
const [menuVisible, setMenuVisible] = React.useState(false);
|
||||
|
||||
|
@ -41,8 +44,27 @@ const Header = () => {
|
|||
clearAll();
|
||||
};
|
||||
|
||||
const handleSyncErrorPress = () => {
|
||||
Toast.show({
|
||||
type: "error",
|
||||
text1: "Sync Error",
|
||||
text2: syncError || "An unknown error occurred during synchronization.",
|
||||
autoHide: true,
|
||||
});
|
||||
};
|
||||
|
||||
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
|
||||
title="Casdoor"
|
||||
titleStyle={styles.titleText}
|
||||
|
|
34
HomePage.js
34
HomePage.js
|
@ -19,6 +19,7 @@ import {GestureHandlerRootView, Swipeable} from "react-native-gesture-handler";
|
|||
import {CountdownCircleTimer} from "react-native-countdown-circle-timer";
|
||||
import {useNetInfo} from "@react-native-community/netinfo";
|
||||
import {FlashList} from "@shopify/flash-list";
|
||||
import Toast from "react-native-toast-message";
|
||||
import * as SQLite from "expo-sqlite/next";
|
||||
|
||||
import SearchBar from "./SearchBar";
|
||||
|
@ -49,11 +50,13 @@ export default function HomePage() {
|
|||
const [refreshing, setRefreshing] = useState(false);
|
||||
const {isConnected} = useNetInfo();
|
||||
const [canSync, setCanSync] = useState(false);
|
||||
const [key, setKey] = useState(0);
|
||||
|
||||
const swipeableRef = useRef(null);
|
||||
const db = SQLite.useSQLiteContext();
|
||||
const {userInfo, serverUrl, token} = useStore();
|
||||
const {startSync} = useSyncStore();
|
||||
const db = SQLite.useSQLiteContext();
|
||||
const syncError = useSyncStore(state => state.syncError);
|
||||
|
||||
useEffect(() => {
|
||||
if (db) {
|
||||
|
@ -89,11 +92,29 @@ export default function HomePage() {
|
|||
|
||||
const onRefresh = async() => {
|
||||
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);
|
||||
};
|
||||
|
||||
const handleAddAccount = async(accountData) => {
|
||||
setKey(prevKey => prevKey + 1);
|
||||
await TotpDatabase.insertAccount(db, accountData);
|
||||
closeEnterAccountModal();
|
||||
};
|
||||
|
@ -218,6 +239,7 @@ export default function HomePage() {
|
|||
right={() => (
|
||||
<View style={{justifyContent: "center", alignItems: "center"}}>
|
||||
<CountdownCircleTimer
|
||||
key={key}
|
||||
isPlaying={true}
|
||||
duration={30}
|
||||
initialRemainingTime={TotpDatabase.calculateCountdown()}
|
||||
|
@ -226,7 +248,11 @@ export default function HomePage() {
|
|||
size={60}
|
||||
onComplete={() => {
|
||||
TotpDatabase.updateToken(db, item.id);
|
||||
return {shouldRepeat: true, delay: 0};
|
||||
return {
|
||||
shouldRepeat: true,
|
||||
delay: 0,
|
||||
newInitialRemainingTime: TotpDatabase.calculateCountdown(),
|
||||
};
|
||||
}}
|
||||
strokeWidth={5}
|
||||
>
|
||||
|
@ -292,7 +318,7 @@ export default function HomePage() {
|
|||
transform: [{translateX: -OFFSET_X}, {translateY: -OFFSET_Y}],
|
||||
}}
|
||||
>
|
||||
<EnterAccountDetails onClose={closeEnterAccountModal} onAdd={handleAddAccount} />
|
||||
<EnterAccountDetails onClose={closeEnterAccountModal} onAdd={handleAddAccount} validateSecret={TotpDatabase.validateSecret} />
|
||||
</Modal>
|
||||
</Portal>
|
||||
|
||||
|
|
|
@ -13,8 +13,8 @@
|
|||
// limitations under the License.
|
||||
|
||||
import React, {useEffect, useState} from "react";
|
||||
import {Dimensions, Text, View} from "react-native";
|
||||
import {IconButton, Modal, Portal} from "react-native-paper";
|
||||
import {Text, View} from "react-native";
|
||||
import {IconButton, Portal} from "react-native-paper";
|
||||
import {Camera, CameraView} from "expo-camera";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
|
@ -54,41 +54,23 @@ const ScanQRCode = ({onClose, showScanner, onAdd}) => {
|
|||
closeOptions();
|
||||
};
|
||||
|
||||
const {width, height} = Dimensions.get("window");
|
||||
const offsetX = width * 0.5;
|
||||
const offsetY = height * 0.5;
|
||||
|
||||
return (
|
||||
<View style={{marginTop: "50%", flex: 1}} >
|
||||
<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 ? (
|
||||
<Text style={{marginLeft: "20%", marginRight: "20%"}}>Requesting for camera permission</Text>
|
||||
) : hasPermission === false ? (
|
||||
<Text style={{marginLeft: "20%", marginRight: "20%"}}>No access to camera</Text>
|
||||
) : (
|
||||
<CameraView
|
||||
onBarcodeScanned={handleBarCodeScanned}
|
||||
barcodeScannerSettings={{
|
||||
barcodeTypes: ["qr", "pdf417"],
|
||||
}}
|
||||
style={{flex: 1}}
|
||||
/>
|
||||
)}
|
||||
<IconButton icon={"close"} size={40} onPress={onClose} style={{position: "absolute", top: 30, right: 5}} />
|
||||
</Modal>
|
||||
{hasPermission === null ? (
|
||||
<Text style={{marginLeft: "20%", marginRight: "20%"}}>Requesting for camera permission</Text>
|
||||
) : hasPermission === false ? (
|
||||
<Text style={{marginLeft: "20%", marginRight: "20%"}}>No access to camera</Text>
|
||||
) : (
|
||||
<CameraView
|
||||
onBarcodeScanned={handleBarCodeScanned}
|
||||
barcodeScannerSettings={{
|
||||
barcodeTypes: ["qr", "pdf417"],
|
||||
}}
|
||||
style={{flex: 1}}
|
||||
/>
|
||||
)}
|
||||
<IconButton icon={"close"} size={40} onPress={onClose} style={{position: "absolute", top: 30, right: 5}} />
|
||||
</Portal>
|
||||
</View>
|
||||
);
|
||||
|
|
|
@ -13,14 +13,15 @@
|
|||
// limitations under the License.
|
||||
|
||||
import * as React from "react";
|
||||
import {Button} from "react-native-paper";
|
||||
import {View} from "react-native";
|
||||
import {StyleSheet, View, useWindowDimensions} from "react-native";
|
||||
import {Button, Surface, Text} from "react-native-paper";
|
||||
import CasdoorLoginPage, {CasdoorLogout} from "./CasdoorLoginPage";
|
||||
import useStore from "./useStorage";
|
||||
|
||||
const SettingPage = () => {
|
||||
const [showLoginPage, setShowLoginPage] = React.useState(false);
|
||||
const {userInfo, clearAll} = useStore();
|
||||
const {width} = useWindowDimensions();
|
||||
|
||||
const handleCasdoorLogin = () => setShowLoginPage(true);
|
||||
const handleHideLoginPage = () => setShowLoginPage(false);
|
||||
|
@ -30,16 +31,42 @@ const SettingPage = () => {
|
|||
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 (
|
||||
<View>
|
||||
<Button
|
||||
style={{marginTop: "50%", marginLeft: "20%", marginRight: "20%"}}
|
||||
icon={userInfo === null ? "login" : "logout"}
|
||||
mode="contained"
|
||||
onPress={userInfo === null ? handleCasdoorLogin : handleCasdoorLogout}
|
||||
>
|
||||
{userInfo === null ? "Login with Casdoor" : "Logout"}
|
||||
</Button>
|
||||
<View style={styles.container}>
|
||||
<Surface style={styles.surface} elevation={4}>
|
||||
<Text style={styles.title}>Account Settings</Text>
|
||||
<Button
|
||||
style={styles.button}
|
||||
icon={userInfo === null ? "login" : "logout"}
|
||||
mode="contained"
|
||||
onPress={userInfo === null ? handleCasdoorLogin : handleCasdoorLogout}
|
||||
>
|
||||
{userInfo === null ? "Login with Casdoor" : "Logout"}
|
||||
</Button>
|
||||
</Surface>
|
||||
{showLoginPage && <CasdoorLoginPage onWebviewClose={handleHideLoginPage} />}
|
||||
</View>
|
||||
);
|
||||
|
|
|
@ -45,6 +45,17 @@ CREATE TABLE accounts (
|
|||
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) => {
|
||||
if (secretKey !== null && secretKey !== undefined && secretKey !== "") {
|
||||
try {
|
||||
|
@ -147,7 +158,6 @@ export async function getAllAccounts(db) {
|
|||
const mappedAccount = {
|
||||
...account,
|
||||
accountName: account.account_name,
|
||||
secretKey: account.secret,
|
||||
};
|
||||
return mappedAccount;
|
||||
});
|
||||
|
@ -173,10 +183,18 @@ async function updateSyncTimeForAll(db) {
|
|||
}
|
||||
|
||||
export function calculateCountdown() {
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
const now = Math.round(new Date().getTime() / 1000.0);
|
||||
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) {
|
||||
for (const account of mergedAccounts) {
|
||||
if (account.id) {
|
||||
|
|
Loading…
Reference in New Issue