feat: add some 2FA TOTP code (#2)
* feat: add 2FA TOTP. * fix: use npmjs.
This commit is contained in:
parent
b575ab96f1
commit
39674fa9c7
|
@ -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;
|
|
@ -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>
|
||||
);
|
||||
}
|
218
HomePage.js
218
HomePage.js
|
@ -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 />
|
||||
<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="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'} />}
|
||||
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'}
|
||||
/>
|
||||
<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>
|
||||
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
Loading…
Reference in New Issue