feat: add some 2FA TOTP code (#2)

* feat: add 2FA TOTP.

* fix: use npmjs.
This commit is contained in:
ChenWenpeng 2023-09-08 18:30:06 +08:00 committed by GitHub
parent b575ab96f1
commit 39674fa9c7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 322 additions and 86 deletions

50
Account.js Normal file
View File

@ -0,0 +1,50 @@
// 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 {default as hotptotp} from 'hotp-totp';
const {totp} = hotptotp;
window.Buffer = window.Buffer || require("buffer").Buffer;
class Account {
constructor(description, secretCode, onUpdate) {
this.title = description;
this.secretCode = secretCode;
this.countdowns = 30;
this.timer = setInterval(this.updateCountdown.bind(this), 1000);
this.token = '';
this.tokenInterval = setInterval(this.generateAndSetToken.bind(this), 30000);
this.onUpdate = onUpdate;
}
generateToken = async () => {
let token = await totp(this.secretCode);
return token;
}
generateAndSetToken = async () => {
this.token = await this.generateToken();
this.onUpdate();
}
updateCountdown() {
this.countdowns = Math.max(0, this.countdowns - 1);
if (this.countdowns === 0) {
this.countdowns = 30;
this.onUpdate();
}
}
}
export default Account;

72
EnterAccountDetails.js Normal file
View File

@ -0,0 +1,72 @@
// 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, { useState } from 'react';
import { View, Text, TextInput } from 'react-native';
import { Button, IconButton } from 'react-native-paper';
export default function EnterAccountDetails({ onClose, onAdd }) {
const [description, setDescription] = useState('');
const [secretCode, setSecretCode] = useState('');
const handleAddAccount = () => {
onAdd({ description, secretCode });
setDescription('');
setSecretCode('');
};
return (
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
<Text style={{fontSize: 24, marginBottom: 5}}>Add new 2FA account</Text>
<div style={{display: 'flex', marginTop: 10}}>
<IconButton icon='account-details' size={35}></IconButton>
<TextInput
placeholder='Description'
value={description}
onChangeText={(text) => setDescription(text)}
style={{ borderWidth: 3, borderColor: 'white', margin: 10, width: 230, height: 50, borderRadius: 5, fontSize: 18,
color: 'gray', paddingLeft: 10}}
/>
</div>
<div style={{display: 'flex'}}>
<IconButton icon='account-key' size={35}></IconButton>
<TextInput
placeholder='Secret code'
value={secretCode}
onChangeText={(text) => setSecretCode(text)}
secureTextEntry
style={{ borderWidth: 3, borderColor: 'white', margin: 10, width: 230, height: 50, borderRadius: 5, fontSize: 18,
color: 'gray', paddingLeft: 10 }}
/>
</div>
<Button
icon='account-plus'
style={{
backgroundColor: '#393544',
borderRadius: 5,
margin: 10,
alignItems: 'center',
position: 'absolute',
top: 260,
width: 300,
// height: 50
}}
onPress={handleAddAccount}
>
<Text style={{fontSize: 18}}>Add</Text>
</Button>
<IconButton icon={'close'} size={30} onPress={onClose} style={{position: 'absolute', top: 5, right: 5}} />
</View>
);
}

View File

@ -13,73 +13,169 @@
// limitations under the License.
import * as React from 'react';
import {Avatar, List} from "react-native-paper";
import SearchBar from "./SearchBar";
import { View, TouchableOpacity, Text, FlatList } from 'react-native';
import { Avatar, List, Portal, Modal, IconButton } from 'react-native-paper';
import SearchBar from './SearchBar';
import EnterAccountDetails from './EnterAccountDetails';
import Account from "./Account";
export default function HomePage() {
const [isPlusButton, setIsPlusButton] = React.useState(true);
const [showOptions, setShowOptions] = React.useState(false);
const [showEnterAccountModal, setShowEnterAccountModal] = React.useState(false);
const [accountList, setAccountList] = React.useState([]);
const [searchQuery, setSearchQuery] = React.useState('');
const [filteredData, setFilteredData] = React.useState(accountList);
const togglePlusButton = () => {
setIsPlusButton(!isPlusButton);
setShowOptions(!showOptions);
};
const closeOptions = () => {
setIsPlusButton(true);
setShowOptions(false);
};
const openEnterAccountModal = () => {
setShowEnterAccountModal(true);
closeOptions();
};
const closeEnterAccountModal = () => {
setShowEnterAccountModal(false);
};
const handleAddAccount = async (accountData) => {
const onUpdate = () => {
setAccountList(prevList => [...prevList]);
};
const newAccount = new Account(accountData.description, accountData.secretCode, onUpdate);
const token = await newAccount.generateToken();
newAccount.token = token;
await setAccountList(prevList => [...prevList, newAccount]);
closeEnterAccountModal();
};
React.useEffect(() => {
setAccountList(prevList => [...prevList]);
}, [accountList]);
const handleSearch = (query) => {
setSearchQuery(query);
if (query.trim() !== '') {
const filteredResults = accountList.filter(item =>
item.title.toLowerCase().includes(query.toLowerCase())
);
setFilteredData(filteredResults);
} else {
setFilteredData(accountList);
}
};
return (
<div>
<SearchBar />
<List.Item
title="Casdoor"
description="admin"
left={props => <Avatar.Image size={24} style={{marginLeft: '20px', backgroundColor: 'rgb(242,242,242)'}} source={'https://cdn.casbin.org/img/social_casdoor.png'} />}
/>
<List.Item
title="GitHub"
description="Linus"
left={props => <Avatar.Image size={24} style={{marginLeft: '20px', backgroundColor: 'rgb(242,242,242)'}} source={'https://cdn.casbin.org/img/social_github.png'} />}
/>
<List.Item
title="Google"
description="James Greenson"
left={props => <Avatar.Image size={24} style={{marginLeft: '20px', backgroundColor: 'rgb(242,242,242)'}} source={'https://cdn.casbin.org/img/social_google.png'} />}
/>
<List.Item
title="Casdoor"
description="admin"
left={props => <Avatar.Image size={24} style={{marginLeft: '20px', backgroundColor: 'rgb(242,242,242)'}} source={'https://cdn.casbin.org/img/social_casdoor.png'} />}
/>
<List.Item
title="GitHub"
description="Linus"
left={props => <Avatar.Image size={24} style={{marginLeft: '20px', backgroundColor: 'rgb(242,242,242)'}} source={'https://cdn.casbin.org/img/social_github.png'} />}
/>
<List.Item
title="Google"
description="James Greenson"
left={props => <Avatar.Image size={24} style={{marginLeft: '20px', backgroundColor: 'rgb(242,242,242)'}} source={'https://cdn.casbin.org/img/social_google.png'} />}
/>
<List.Item
title="Casdoor"
description="admin"
left={props => <Avatar.Image size={24} style={{marginLeft: '20px', backgroundColor: 'rgb(242,242,242)'}} source={'https://cdn.casbin.org/img/social_casdoor.png'} />}
/>
<List.Item
title="GitHub"
description="Linus"
left={props => <Avatar.Image size={24} style={{marginLeft: '20px', backgroundColor: 'rgb(242,242,242)'}} source={'https://cdn.casbin.org/img/social_github.png'} />}
/>
<List.Item
title="Google"
description="James Greenson"
left={props => <Avatar.Image size={24} style={{marginLeft: '20px', backgroundColor: 'rgb(242,242,242)'}} source={'https://cdn.casbin.org/img/social_google.png'} />}
/>
<List.Item
title="Casdoor"
description="admin"
left={props => <Avatar.Image size={24} style={{marginLeft: '20px', backgroundColor: 'rgb(242,242,242)'}} source={'https://cdn.casbin.org/img/social_casdoor.png'} />}
/>
<List.Item
title="GitHub"
description="Linus"
left={props => <Avatar.Image size={24} style={{marginLeft: '20px', backgroundColor: 'rgb(242,242,242)'}} source={'https://cdn.casbin.org/img/social_github.png'} />}
/>
<List.Item
title="Google"
description="James Greenson"
left={props => <Avatar.Image size={24} style={{marginLeft: '20px', backgroundColor: 'rgb(242,242,242)'}} source={'https://cdn.casbin.org/img/social_google.png'} />}
/>
</div>
<View style={{ flex: 1 }}>
<SearchBar onSearch={ handleSearch } />
<FlatList
// data={accountList}
data={searchQuery.trim() !== '' ? filteredData : accountList}
keyExtractor={(item, index) => index.toString()}
renderItem={({ item }) => (
<List.Item
title={
<View style={{ flexDirection: 'row', alignItems: 'center' }}>
<Text style={{ fontSize: 20, width: 80 }}>{item.title}</Text>
<Text style={{ marginLeft: 20, fontSize: 30 }}>{item.token}</Text>
<Text style={{ marginLeft: 20, fontSize: 20, width: 20 }}>{item.countdowns}s</Text>
</View>
}
left={(props) => (
<Avatar.Image
size={60}
style={{ marginLeft: '20px', backgroundColor: 'rgb(242,242,242)' }}
source={'https://cdn.casbin.org/img/social_casdoor.png'}
/>
)}
/>
)}
/>
<Portal>
<Modal
visible={showOptions}
onDismiss={closeOptions}
contentContainerStyle={{
backgroundColor: 'white',
padding: 20,
borderRadius: 10,
width: 300,
height: 150,
position: 'absolute',
top: '50%',
left: '50%',
transform: [{ translateX: '-50%' }, { translateY: '-50%' }],
}}
>
<TouchableOpacity
style={{ flexDirection: 'row', alignItems: 'center'}}
onPress={() => {
// Handle scanning QR code operation...
// closeOptions();
}}
>
<IconButton icon={'camera'} size={35} />
<Text style={{fontSize: 18}} >Scan QR code</Text>
</TouchableOpacity>
<TouchableOpacity
style={{ flexDirection: 'row', alignItems: 'center', marginTop: 10 }}
onPress={openEnterAccountModal}
>
<IconButton icon={'keyboard'} size={35} />
<Text style={{fontSize: 18}}>Enter Secret code</Text>
</TouchableOpacity>
</Modal>
</Portal>
<Portal>
<Modal
visible={showEnterAccountModal}
onDismiss={closeEnterAccountModal}
contentContainerStyle={{
backgroundColor: 'white',
padding: 1,
borderRadius: 10,
width: '90%',
height: '40%',
position: 'absolute',
top: '50%',
left: '50%',
transform: [{ translateX: '-50%' }, { translateY: '-50%' }],
}}
>
<EnterAccountDetails onClose={closeEnterAccountModal} onAdd={handleAddAccount} />
</Modal>
</Portal>
<TouchableOpacity
style={{
position: 'absolute',
bottom: 30,
right: 30,
width: 70,
height: 70,
borderRadius: 35,
backgroundColor: '#393544',
alignItems: 'center',
justifyContent: 'center',
}}
onPress={togglePlusButton}
>
<IconButton icon={isPlusButton ? 'plus' : 'close'} size={40} color={'white'} />
</TouchableOpacity>
</View>
);
}

View File

@ -102,4 +102,4 @@ const styles = StyleSheet.create({
justifyContent: 'center',
alignItems: 'center',
},
});
});

View File

@ -15,10 +15,13 @@
import * as React from 'react';
import { Searchbar } from 'react-native-paper';
const SearchBar = () => {
const SearchBar = ({ onSearch }) => {
const [searchQuery, setSearchQuery] = React.useState('');
const onChangeSearch = query => setSearchQuery(query);
const onChangeSearch = query => {
setSearchQuery(query);
onSearch(query);
}
return (
<Searchbar

49
package-lock.json generated
View File

@ -11,8 +11,10 @@
"@expo/webpack-config": "^19.0.0",
"@react-navigation/bottom-tabs": "^6.5.8",
"@react-navigation/native": "^6.1.7",
"buffer": "^6.0.3",
"expo": "~49.0.8",
"expo-status-bar": "~1.6.0",
"hotp-totp": "^1.0.6",
"react": "18.2.0",
"react-dom": "18.2.0",
"react-native": "0.72.4",
@ -7182,6 +7184,15 @@
"readable-stream": "^3.4.0"
}
},
"node_modules/bl/node_modules/buffer": {
"version": "5.7.1",
"resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
"integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
"dependencies": {
"base64-js": "^1.3.1",
"ieee754": "^1.1.13"
}
},
"node_modules/bl/node_modules/readable-stream": {
"version": "3.6.2",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
@ -7331,26 +7342,12 @@
}
},
"node_modules/buffer": {
"version": "5.7.1",
"resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
"integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
"integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==",
"dependencies": {
"base64-js": "^1.3.1",
"ieee754": "^1.1.13"
"ieee754": "^1.2.1"
}
},
"node_modules/buffer-alloc": {
@ -10258,6 +10255,14 @@
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
},
"node_modules/hotp-totp": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/hotp-totp/-/hotp-totp-1.0.6.tgz",
"integrity": "sha512-+5nXaNkFF9YHuAjGo2VjLxPDVTf0QXZTVXBTGoS18eNb64ofQ25qnUINBPlDnmgriryM4UkPAGD2ATNCV9ivsg==",
"dependencies": {
"thirty-two": "^1.0.2"
}
},
"node_modules/hpack.js": {
"version": "2.1.6",
"resolved": "https://registry.npmjs.org/hpack.js/-/hpack.js-2.1.6.tgz",
@ -16734,6 +16739,14 @@
"node": ">=0.8"
}
},
"node_modules/thirty-two": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/thirty-two/-/thirty-two-1.0.2.tgz",
"integrity": "sha512-OEI0IWCe+Dw46019YLl6V10Us5bi574EvlJEOcAkB29IzQ/mYD1A6RyNHLjZPiHCmuodxvgF6U+vZO1L15lxVA==",
"engines": {
"node": ">=0.2.6"
}
},
"node_modules/throat": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/throat/-/throat-5.0.0.tgz",

View File

@ -12,8 +12,10 @@
"@expo/webpack-config": "^19.0.0",
"@react-navigation/bottom-tabs": "^6.5.8",
"@react-navigation/native": "^6.1.7",
"buffer": "^6.0.3",
"expo": "~49.0.8",
"expo-status-bar": "~1.6.0",
"hotp-totp": "^1.0.6",
"react": "18.2.0",
"react-dom": "18.2.0",
"react-native": "0.72.4",