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 {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>
);

View File

@ -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();}
};

View File

@ -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;

View File

@ -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;

View File

@ -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}

View File

@ -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>

View File

@ -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>
);

View File

@ -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>
);

View File

@ -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) {

View File

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