使用 React JS、Crypto JS 和 Fauna 构建密码管理器
使用 React JS 和 Fauna 构建 Google 密码管理器克隆版
介绍
动物入门
设置应用程序
运行我们的样板应用程序
加密
结论
由 Mux 主办的 DEV 全球展示挑战赛:展示你的项目!
使用 React JS 和 Fauna 构建 Google 密码管理器克隆版
本文是为“与动物一起写作”项目而作。
介绍
本文将带您了解我如何使用 React JS 和 Fauna 构建一个密码管理器。密码管理器至关重要。当我们拥有多个账户和多个密码时,我们需要管理它们。如果没有密码管理器的帮助,管理密码将非常困难。
先决条件
- 具备React和 JSX的基础知识。
- npm和npx已安装。
- 如何创建一个 React 应用。
- React Bootstrap已安装。
- 加密和密码学的基础知识。
动物入门
首先,在Fauna创建一个帐户。
创建动物数据库
要创建动物数据库,请前往动物控制面板。
接下来,点击按钮New Database,输入数据库名称,然后按回车键。
创建动物群落收藏
集合是将具有相同或相似用途的文档(行)分组在一起。集合的作用类似于传统 SQL 数据库中的表。
在我们正在创建的应用程序中,我们将有两个集合,分别是用户集合users和密码passwords集合。用户集合用于存储用户数据,而密码passwords集合用于保存所有密码数据。
要创建这些集合,请单击您创建的数据库,然后单击New Collection。仅输入集合名称(users),然后单击保存,并对第二个集合(passwords)执行相同的操作。
创建动物群索引
使用索引可以快速查找数据,无需每次访问数据库集合时都搜索数据库集合中的每个文档。可以使用数据库集合中的一个或多个字段创建索引。要创建动物群索引,请单击indexes仪表板左侧的相应部分。
在本应用中,我们将创建以下索引:
user_passwords:用于检索特定用户创建的所有密码的索引。user_by_email用于通过用户邮箱检索特定用户数据的索引。此索引必须唯一。
设置应用程序
接下来,我们将使用下面的入门项目。首先,请在 GitHub 上克隆该项目。
git clone <https://github.com/Babatunde13/password-manager-started-code.git>
cd password-manager-starter-code
npm install
克隆仓库后,将下载以下文件/文件夹:
/src/assets/此文件夹包含应用程序中将使用的所有图像。/src/App.css这是我们应用程序的基础 CSS 文件。/src/models.js我们将通过这个文件与我们的动物数据库进行通信。.env.sample此文件显示了成功运行应用程序所需的环境变量。- 服务工作线程文件用于 PWA 功能。
index.js:此文件用于将文件div中的内容挂载public/index.html到我们的应用程序组件。-
src/screens此文件夹定义了应用程序中的所有页面(屏幕)。以下屏幕已在此screen文件夹中定义: -
Home.js这是首页。 -
Signin.js这是登录页面。 -
Signup.js这是注册页面。 -
App.js这是仪表盘页面。 -
src/components这是创建应用程序中所有组件的文件夹。以下组件均创建于此components文件夹中: -
Flash此文件夹包含一个文件flash.js和一个flash.css文件。文件中导出的组件flash.js用于在应用程序中闪烁消息。 -
createPassword.modal.js这是在尝试创建新密码时显示的模态框。 -
editPassword.modal.js当用户尝试更新密码时,将显示此模态框。 -
Navbar.js这是导航栏组件。 -
Passwords.js该组件用于渲染密码,并导入到应用程序仪表板中。 -
previewPassword.modal.js当用户预览密码时,将显示此模态框。
环境变量
env我们的应用程序有两个环境变量,如示例文件中所示,分别是REACT_APP_FAUNA_KEY`<environment_variable>` 和 `<environment_variable> REACT_APP_SECRET_KEY`。在使用 React 和 `<environment_variable>` 创建环境变量时create_react_app,我们需要在环境变量前加上 `<environment_variable>` 前缀REACT_APP_。
生成你的动物群秘密密钥
Fauna 密钥用于将应用程序或脚本连接到数据库,每个数据库的密钥都是唯一的。要生成密钥,请转到控制面板的安全部分并点击“生成密钥” 。输入密钥名称,系统将为您生成一个新密钥。请按以下格式New Key将密钥粘贴到您的文件中。.envREACT_APP_FAUNA_KEY={{ API key}}
应用程序密钥
您的应用程序密钥必须保密,任何人都不得访问。我们将使用应用程序密钥对密码进行加密,然后再将其存储到数据库中。请.env按以下格式将您的密钥添加到文件中:REACT_APP_SECRET_KEY={{ secret key}}
运行我们的样板应用程序
到目前为止,我们已经了解了应用程序的结构,现在是时候运行我们的样板应用程序了。要运行应用程序,我们npm start在根目录中输入命令。服务器启动后,我们应该看到以下内容:
您可以通过手动编辑文件中定义的端点来测试其他端点src/App.js。下图显示了/login端点:
我们来讨论一下这个组件内部发生了什么。首先,它导入了文件夹中的几个文件screens以及一些库。
- 我们从 React导入了 `<Route>
BrowserRouter`、Switch`Route<Route>` 和 `<Route>` ;这个库用于定义组件的端点。`<Route>`组件可以用来路由多个组件,我们还可以设置一些组件在整个应用程序中都存在。` <Route>` 组件用于告诉 React 一次只渲染一个组件。`<Route>` 组件接收路径和组件,我们还传递一个参数,告诉它匹配同一个端点。Redirectreact-router-domBrowserRouterswitchexact - 我们还导入了一个
events库,用于监听应用中向用户闪烁的事件。具体做法是创建一个 flash 函数,并将其附加到 window 对象上,以便在应用的任何位置使用它。该函数接收消息及其类型作为参数,然后触发一个事件。之后,我们可以使用flash组件监听此事件,并在应用中渲染一些闪烁消息。
首页
让我们来构建应用程序的首页。将内容更改src/screens/Home.js为以下内容:
import NavbarComponent from "../components/Navbar";
import Container from 'react-bootstrap/Container';
import { Link } from 'react-router-dom'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faHeart } from '@fortawesome/free-solid-svg-icons'
import {Flash} from '../components/Flash/flash'
import hero1 from '../assets/illus8.jpg';
import hero from '../assets/illus4.png';
const Home = () => {
return (
<div>
<NavbarComponent />
<Flash />
<Container style={{height : "70vh", display : "flex", alignItems : "center", justifyContent : "center", overflow : "hidden"}}>
<img src={hero1} alt="" className="h-25 shadow-lg mx-2" style={{border : "none", borderRadius : "15px"}} />
<img src={hero} alt="" className="shadow-lg" style={{border : "none", borderRadius : "15px", maxWidth : "90%", maxHeight : "75%"}} />
<img src={hero1} alt="" className="h-25 shadow-lg mx-2" style={{border : "none", borderRadius : "15px"}} />
</Container>
<p className="navbar fixed-bottom d-block w-100 m-0 text-center" style={{backgroundColor : "#d1e1f0e7"}} >Built with <FontAwesomeIcon icon={faHeart} className="text-danger" /> by <Link target="_blank" to={{ pathname: "https://twitter.com/bkoiki950"}}>Babatunde Koiki</Link> and <Link target="_blank" to={{ pathname: "https://twitter.com/AdewolzJ"}}>wolz-CODElife</Link></p>
</div>
)
}
export default Home
这里没什么特别的,只有JSX代码。返回浏览器查看应用程序内容;你应该会看到以下内容:
导航栏组件
请将您的内容更改src/components/Navbar.js为以下内容:
import {useState} from 'react'
import Navbar from 'react-bootstrap/Navbar'
import Nav from 'react-bootstrap/Nav'
import NavDropdown from 'react-bootstrap/NavDropdown'
import { Link } from 'react-router-dom'
import CreatePasswordModal from '../components/createPassword.modal'
import favicon from '../assets/favicon.png'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faUserCircle, faCog } from '@fortawesome/free-solid-svg-icons'
const NavbarComponent = (props) => {
const [createModalShow, setCreateModalShow] = useState(false);
const handleHide = (url, password, email, name) => {
let n = true
if (url || password || email || name) {n = window.confirm("Your changes won't be saved...")}
if (n) setCreateModalShow(false)
}
const handleCreate = payload => {
props.handleCreate(payload)
setCreateModalShow(false)
}
return (
<Navbar expand="lg" className="navbar-fixed-top"
style={{position : "sticky", top : "0", zIndex: "10000", backgroundColor : "#d1e1f0e7"}}>
<Navbar.Brand as={Link} to="/" style={{cursor : 'pointer'}}>
<img src={favicon} alt="" style={{width : '40px', height : '40px'}} className="mr-2" />
Password Manager
</Navbar.Brand>
<Navbar.Toggle aria-controls="basic-navbar-nav" />
<Navbar.Collapse id="basic-navbar-nav">
<Nav className="ml-auto">
<Link to="/" className="mt-2" style={{textDecoration : "none"}}>Home</Link>
{!localStorage.getItem('userId') ?
<>
<NavDropdown title={<FontAwesomeIcon icon={faUserCircle} size="2x" className="text-primary" />} alignRight id="basic-nav-dropdown">
<NavDropdown.Item as={Link} to="/login" className="text-primary">Sign in</NavDropdown.Item>
<NavDropdown.Item as={Link} to="/register" className="text-primary">Register</NavDropdown.Item>
</NavDropdown>
</>:
<>
<NavDropdown title={<FontAwesomeIcon icon={faCog} size="2x" className="text-primary" />} alignRight id="basic-nav-dropdown">
<NavDropdown.Item as={Link} to="/dashboard" className="text-primary" >Dashboard</NavDropdown.Item>
<CreatePasswordModal show={createModalShow} onHide={handleHide} handleCreate={ handleCreate } />
<NavDropdown.Item to="#" onClick={() => setCreateModalShow(true)} className="text-primary" >Create New Password</NavDropdown.Item>
<NavDropdown.Divider />
<NavDropdown.Item as={Link} to="/logout" className="text-primary" >Logout</NavDropdown.Item>
</NavDropdown>
</>
}
</Nav>
</Navbar.Collapse>
</Navbar>
)
}
export default NavbarComponent
应用程序主页现在应该如下所示:
这Navbar是一个动态组件。下拉菜单中显示的内容取决于用户是否已通过身份验证。如果用户未登录,则会显示登录和注册按钮;如果用户已登录,则会显示创建密码按钮、仪表盘按钮和注销按钮。此组件有一个名为 `is_create_password` 的本地状态createModal,其默认值为 false,用于判断是否点击了创建密码按钮。如果点击了该按钮,则会显示创建密码模态框。该handleCreate函数作为 prop 传递给CreatePasswordModal组件以创建新密码。handleHide当用户点击模态框外部区域或取消按钮时,该函数用于隐藏模态框。我们还会检查是否没有传递任何数据,并且需要确认用户是否真的想要关闭模态框。检查 `user` 对象是否存在于 `user_id` 中localStorage,我们会在用户登录时设置该对象。您会注意到,该Flash组件在应用程序中以纯文本形式显示。我们需要更新该组件。
闪存组件
请将你的内容替换src/components/Flash/flash.js为以下内容:
import React, { useEffect, useState } from 'react';
import {event} from '../../App';
import './flash.css';
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faTimesCircle } from '@fortawesome/free-solid-svg-icons'
export const Flash = () => {
let [visibility, setVisibility] = useState(false);
let [message, setMessage] = useState('');
let [type, setType] = useState('');
useEffect(() => {
event.addListener('flash', ({message, type}) => {
setVisibility(true);
setMessage(message);
setType(type);
});
}, []);
useEffect(() => {
setTimeout(() => {
setVisibility(false);
}, 10000)
})
return (
visibility &&
<div className={`alert alert-${type}`}>
<br />
<p>{message}</p>
<span className="close">
<FontAwesomeIcon icon={faTimesCircle} onClick={() => setVisibility(false)} />
</span>
<br />
</div>
)
}
当应用的任何部分触发事件时,都会渲染此组件。我们需要从根App.js组件导出事件类。我们将要触发的就是这个事件对象。我们会监听一个事件,该事件会返回触发的消息和类型(回想一下:这就是我们在App.js文件中定义要监听的内容)。我们创建了三个状态:`<state>`、message` type<state>` 和 ` visibility<state>`。监听事件时,我们会将 ` <state> message` 和type`<state>` 的状态更新为事件返回的值,并将 `<state>` 的可见性设置为 `true`。如果用户没有手动移除,Flash 组件应该只显示很短的时间(10 秒)。我们还创建了另一个 `useEffect`,用于在 10 秒后将 `<state>` 的可见性设置为 `false`。如果 `<state>` 的可见性为 `true`,我们会返回一些内容。如果您现在检查应用,应该看不到任何 Flash 组件的内容,因为其可见性为 `false`。`<state>`type状态用于动态样式设置,就像Bootstrap 中的 ` <state>` warning、 `<state> success` 和`<alert>` 一样。接下来error我们将创建我们的Signin组件Signup,但在此之前,我们需要在我们的组件中创建两个函数models.js,我们将使用它们来创建用户和登录用户。
用户模型
在文件末尾src/models.js输入以下内容:
export const createUser = async (firstName, lastName, email, password) => {
password = await bcrypt.hash(password, bcrypt.genSaltSync(10))
try {
let newUser = await client.query(
q.Create(
q.Collection('users'),
{
data: {
firstName,
email,
lastName,
password
}
}
)
)
if (newUser.name === 'BadRequest') return
newUser.data.id = newUser.ref.value.id
return newUser.data
} catch (error) {
return
}
}
export const getUser = async (userId) => {
const userData = await client.query(
q.Get(
q.Ref(q.Collection('users'), userId)
)
)
if (userData.name === "NotFound") return
if (userData.name === "BadRequest") return "Something went wrong"
userData.data.id = userData.ref.value.id
return userData.data
}
export const loginUser = async (email, password) => {
let userData = await client.query(
q.Get(
q.Match(q.Index('user_by_email'), email)
)
)
if (userData.name === "NotFound") return
if (userData.name === "BadRequest") return "Something went wrong"
userData.data.id = userData.ref.value.id
if (bcrypt.compareSync(password, userData.data.password)) return userData.data
else return
}
- 第一个函数
createUser接收要创建的用户的数据:名字、姓氏、邮箱和密码(明文),并创建用户数据。在创建文档之前,我们会先对密码进行哈希处理。 - 第二个函数
getUser用于根据其唯一 ID 获取用户数据。 - 该函数
loginUser接收电子邮件和密码,并查找具有该电子邮件的 userData;如果存在,则比较密码,userData如果密码相同则返回该对象;否则,将返回 null。
注册页面
请将文件内容更改src/screens/Signup.js为以下内容:
import { useState } from 'react'
import { createUser } from '../models';
import {useHistory} from 'react-router-dom'
import Form from "react-bootstrap/Form";
import { Link } from 'react-router-dom'
import Col from "react-bootstrap/Col";
import Button from "react-bootstrap/Button";
import Container from "react-bootstrap/Container";
import NavbarComponent from '../components/Navbar';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faUserCircle } from '@fortawesome/free-solid-svg-icons'
import { Flash } from '../components/Flash/flash';
export default function SignIn() {
const history = useHistory()
if (localStorage.getItem('userId')) {
setTimeout(() => {
window.flash('You are logged in', 'warning')
}, 100)
history.push('/')
}
const [validated, setValidated] = useState(false);
const handleSubmit = async (e) => {
e.preventDefault()
const body = {
firstName: e.target.firstName.value,
lastName: e.target.lastName.value,
email: e.target.email.value,
password: e.target.password.value
}
try {
if (body.firstName && body.lastName && body.password && body.email && body.password === e.target.confirm_password.value) {
const user = await createUser(body.firstName, body.lastName, body.email, body.password)
if (!user) {
window.flash('Email has been chosen', 'error')
} else {
localStorage.setItem('userId', user.id)
localStorage.setItem('email', user.email)
history.push('/')
window.flash('Account created successfully, signed in', 'success')
}
} else if (!body.firstName || !body.email || !body.lastName || !e.target.confirm_password.value) {
setValidated(true)
} else {
setValidated(true)
}
} catch (error) {
console.log(error)
window.flash('Something went wrong', 'error')
}
}
return (
<>
<NavbarComponent />
<Flash /> <br/><br/>
<Container className='d-flex flex-column align-items-center justify-content-center pt-5' style={{height : '80vh'}}>
<p className="h3 display-4 mt-5"><FontAwesomeIcon icon={faUserCircle} size="1x" /></p>
<p className="h2 display-5">Register</p>
<Form noValidate validated={validated} onSubmit={handleSubmit} style={{minWidth : '300px' }}>
<Form.Row>
<Form.Group as={Col} md="6" controlId="validationCustom01">
<Form.Label>First name</Form.Label>
<Form.Control required name='firstName' type="text" placeholder="First name" />
<Form.Control.Feedback type="invalid">Please provide your first name.</Form.Control.Feedback>
<Form.Control.Feedback>Great name!</Form.Control.Feedback>
</Form.Group>
<Form.Group as={Col} md="6" controlId="validationCustom02">
<Form.Label>Last Name</Form.Label>
<Form.Control required name='lastName' type="text" placeholder="Last name" />
<Form.Control.Feedback type="invalid">Please provide your last name.</Form.Control.Feedback>
<Form.Control.Feedback>Looks good!</Form.Control.Feedback>
</Form.Group>
<Form.Group as={Col} md="12" controlId="validationCustomUsername">
<Form.Label>Email</Form.Label>
<Form.Control type="email" placeholder="Email" aria-describedby="inputGroupPrepend" required name='email' />
<Form.Control.Feedback type="invalid">Please choose a valid and unique email.</Form.Control.Feedback>
<Form.Control.Feedback>Looks good!</Form.Control.Feedback>
</Form.Group>
</Form.Row>
<Form.Row>
<Form.Group as={Col} md="6" controlId="validationCustom04">
<Form.Label>Password</Form.Label>
<Form.Control type="password" placeholder="Password" required name='password' />
<Form.Control.Feedback type="invalid">Please provide a password between 8 and 20.</Form.Control.Feedback>
<Form.Control.Feedback>Looks good!</Form.Control.Feedback>
</Form.Group>
<Form.Group as={Col} md="6" controlId="validationCustom04">
<Form.Label>Confirm Password</Form.Label>
<Form.Control type="password" placeholder="Confirm Password" required name='confirm_password' />
<Form.Control.Feedback type="invalid">Fields do not match.</Form.Control.Feedback>
<Form.Control.Feedback>Looks good!</Form.Control.Feedback>
</Form.Group>
</Form.Row>
<Button type="submit">Register</Button>
<p className="text-center"><Link to="/login">Sign in</Link> if already registered!</p>
</Form>
</Container>
</>
)
}
- 在函数开始时,我们验证用户是否未通过身份验证。如果用户已通过身份验证,则调用
window.flash之前创建的函数,并传递消息和警告信息;然后,重定向回首页。 - 接下来,我们创建了一个
validated用于数据验证的状态。 - 该
handleSubmit函数作为表单的处理程序传递onSubmit。我们还使用了命名表单,因此无需定义多个变量。
验证后的数据被发送到该createUser函数,如果该函数返回用户对象,则创建该用户;否则,该用户已存在。
立即前往注册页面并创建账户。
登录页面
请将文件内容更改src/screens/Signin.js为以下内容:
import { useState} from 'react'
import { useHistory } from 'react-router-dom';
import {loginUser} from '../models'
import Form from "react-bootstrap/Form";
import Col from "react-bootstrap/Col";
import Button from "react-bootstrap/Button";
import { Link } from 'react-router-dom'
import Container from "react-bootstrap/Container";
import NavbarComponent from '../components/Navbar';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faUserCircle } from '@fortawesome/free-solid-svg-icons'
import { Flash } from '../components/Flash/flash';
export default function SignIn() {
const history = useHistory()
if (localStorage.getItem('userId')) {
setTimeout(() => {
window.flash('You are logged in', 'warning')
}, 100)
history.push('/')
}
const [validated, setValidated] = useState(false)
const handleSubmit = async (event) => {
event.preventDefault();
const body = {
email: event.target.email.value,
password: event.target.password.value
}
// Handle login logic
if (!body.email || !body.password) {
setValidated(true)
} else {
const user = await loginUser(body.email, body.password)
if (user) {
localStorage.setItem('userId', user.id)
localStorage.setItem('email', user.email)
history.push('/')
window.flash('Logged in successfully!', 'success')
} else {
window.flash('Invalid email or password', 'error')
}
}
}
return (
<>
<NavbarComponent />
<Flash />
<Container className='d-flex flex-column align-items-center justify-content-center' style={{height : '80vh'}}>
<p className="h3 display-4"><FontAwesomeIcon icon={faUserCircle} size="1x" /></p>
<p className="h2 display-5">Sign in</p>
<Form noValidate validated={validated} onSubmit={handleSubmit} style={{minWidth : '300px' }}>
<Form.Row>
<Form.Group as={Col} md="12" controlId="validationCustom01">
<Form.Label>Email</Form.Label>
<Form.Control required name='email' type="email" placeholder="Email" />
<Form.Control.Feedback type="invalid">Please provide a valid email.</Form.Control.Feedback>
<Form.Control.Feedback>Looks Good!</Form.Control.Feedback>
</Form.Group>
</Form.Row>
<Form.Row>
<Form.Group as={Col} md="12" controlId="validationCustom02">
<Form.Label>Password</Form.Label>
<Form.Control required name='password' type="password" placeholder="Password" />
<Form.Control.Feedback type="invalid">Please provide a password.</Form.Control.Feedback>
<Form.Control.Feedback>Looks good!</Form.Control.Feedback>
</Form.Group>
</Form.Row>
<Button type="submit">Sign in</Button>
<p className="text-center"><Link to="/register">Register</Link> to create account!</p>
</Form>
</Container>
</>
)
}
此组件与注册组件类似。
密码模型
更新该models.js文件,添加用于创建、编辑、删除和获取应用程序密码的功能。将以下内容添加到文件末尾src/models.js:
export const createPassword = async (accountName, accountUrl, email, encryptedPassword, userId) => {
let user = await getUser(userId)
const date = new Date()
const months = [
"January", "February", "March", "April", "May", "June",
"July", "August", "September", "October", "November", "December"
]
let newPassword = await client.query(
q.Create(
q.Collection('passwords'),
{
data: {
accountName,
accountUrl,
email,
encryptedPassword,
created__at: `${months[date.getMonth()]} ${date.getDate()}, ${date.getFullYear()}`,
user: {
email: user.email,
id: user.id
}
}
}
)
)
if (newPassword.name === 'BadRequest') return
newPassword.data.id = newPassword.ref.value.id
return newPassword.data
}
export const getPasswordsByUserID = async id => {
let passwords = []
try {
let userPasswords = await client.query(
q.Paginate(
q.Match(q.Index('user_passwords'), id)
)
)
if (userPasswords.name === "NotFound") return
if (userPasswords.name === "BadRequest") return "Something went wrong"
for (let passwordId of userPasswords.data) {
let password = await getPassword(passwordId.value.id)
passwords.push(password)
}
return passwords
} catch (error) {
return
}
}
export const getPassword = async id => {
let password = await client.query(
q.Get(q.Ref(q.Collection('passwords'), id))
)
if (password.name === "NotFound") return
if (password.name === "BadRequest") return "Something went wrong"
password.data.id = password.ref.value.id
return password.data
}
export const updatePassword = async (payload, id) => {
let password = await client.query(
q.Update(
q.Ref(q.Collection('passwords'), id),
{data: payload}
)
)
if (password.name === "NotFound") return
if (password.name === "BadRequest") return "Something went wrong"
password.data.id = password.ref.value.id
return password.data
}
export const deletePassword = async id => {
let password = await client.query(
q.Delete(
q.Ref(q.Collection('passwords'), id)
)
)
if (password.name === "NotFound") return
if (password.name === "BadRequest") return "Something went wrong"
password.data.id = password.ref.value.id
return password.data
}
该getPasswordsByUserID函数使用user_passwords我们之前创建的索引来筛选集合并返回结果。它会在集合中搜索,并返回一个数组,其中包含所有data.user.id与给定 ID 相同的密码。
仪表盘页面
请更新src/screens/App.js以下内容:
import { useState, useEffect } from 'react'
import {
getPasswordsByUserID,
createPassword,
deletePassword,
updatePassword
} from "../models";
import 'bootstrap/dist/css/bootstrap.min.css';
import Passwords from '../components/Passwords';
import NavbarComponent from '../components/Navbar';
import { useHistory } from 'react-router';
import { Flash } from '../components/Flash/flash';
const AppDashboard = () => {
const history = useHistory()
if (!localStorage.getItem('userId')) {
setTimeout(() => {
window.flash('You need to be logged in', 'warning')
}, 100)
history.push('/login')
}
const [passwords, setPasswords] = useState([])
const [isPending, setIsPending] = useState(false)
const handleCreate = async password => {
// save to dB
password.userId = localStorage.getItem('userId')
const newPassword = await createPassword(
password.accountName,
password.accountUrl,
password.email,
password.encryptedPassword,
password.userId
)
setPasswords([newPassword, ...passwords])
window.flash('New contact created successfully', 'success')
}
useEffect(() => {
setIsPending(true)
const getContacts = async () => {
let passwordData = await getPasswordsByUserID(localStorage.getItem('userId'))
setPasswords(passwordData)
}
getContacts()
setIsPending(false)
}, [])
return (
<>
<NavbarComponent passwords={ passwords} handleCreate={ handleCreate }/>
<Flash />
<Passwords isPending={isPending} passwords={passwords}
handleEdit={async payload => {
await updatePassword({
accountName: payload.accountName,
accountUrl: payload.accountUrl,
email: payload.email,
encryptedPassword: payload.password
}, payload.id)
setPasswords(passwords.map( password => password.id === payload.id? payload : password))
}}
handleDelete={async id => {
await deletePassword(id)
setPasswords(passwords.filter( ele => ele.id !== id))
}}
/>
</>
);
}
export default AppDashboard;
您可能已经知道,此页面受到保护,未经身份验证的用户无法访问。因此,我们首先检查用户对象是否存在localStorage,如果用户未登录,则将其重定向回登录页面。
仪表盘渲染密码组件,该组件会将密码显示在 DOM 中。此组件有两种状态:passwords 和 isPending。从数据库获取数据时,组件isPending状态设置为 passwords true。成功从数据库获取密码数据后,passwordsisPending状态重置为 false,isPendingpasswords状态设置为已获取的数据。从passwords数据库获取数据时,DOM 上会显示一个加载指示器(spinner)。我们通过检查isPendingisPending 状态是否为true 来实现这一点true,如果为 true,则在仪表盘中显示加载指示器。
该passwords组件接受以下属性:
isPending从数据库获取密码时,会显示一个加载指示器。passwords这是从获取已认证用户创建的密码中收到的数据。handleEdit当点击密码的编辑按钮时,将调用此函数。handleDelete当点击密码的删除按钮时,将调用此函数。
密码组件
请将文件内容替换src/components/Passwords.js为以下内容:
import Button from 'react-bootstrap/Button'
import Container from 'react-bootstrap/Container'
import Form from "react-bootstrap/Form";
import Row from "react-bootstrap/Row";
import CryptoJS from "crypto-js";
import dotenv from 'dotenv'
import { useState } from 'react'
import PreviewPasswordModal from './previewPassword.modal'
import web from '../assets/web.png';
import { Col } from 'react-bootstrap';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faSpinner } from '@fortawesome/free-solid-svg-icons'
dotenv.config()
const Password = ({
id,
accountName,
accountUrl,
email,
password,
handleDelete,
handleEdit
}) => {
const [editModal, setEditModal] = useState(false)
const [previewModal, setpreviewModal] = useState(false)
const title_ = accountName || accountUrl
const previewPassword = () => {
setpreviewModal(true)
}
const editPassword = (payload) => {
handleEdit(payload)
setEditModal(false)
window.flash('Password edited successfully', 'success')
}
const deletePassword = () => {
handleDelete(id)
window.flash('Password deleted successfully', 'success')
}
return (
<Col sm="12">
<Button style={{backgroundColor: "white", color: 'black', margin: '5px 0px', width: "100%"}} onClick={previewPassword}>
<Row>
<Col sm={1}><img src={web} alt="" /></Col>
<Col className="text-left mt-1">{accountName}</Col>
</Row>
</Button>
<PreviewPasswordModal
id={id}
show={previewModal}
edit={editModal}
onHideEdit={()=>{setEditModal(false)}}
onEdit={()=>{setEditModal(true)}}
onDelete={() => {deletePassword(); setpreviewModal(false)}}
accountName={accountName}
accountUrl={accountUrl}
email={email}
password={password}
editPassword={editPassword}
title={"Preview Password for "+title_}
onHide={() => {setpreviewModal(false)}}
/>
</Col>
)
}
const Passwords = ({passwords, handleEdit, handleDelete, isPending}) => {
return (
<Container className="p-3 my-5 bordered">
{isPending ?
<p className="my-5 py-5 h2 display-4 w-100" style={{textAlign : "center"}}>
<FontAwesomeIcon icon={faSpinner} spin />
</p>
:
<>
<Row className="p-2 text-white" style={{backgroundColor : "dodgerblue"}}>
<Col xs={12} sm={6} className="pt-2">{passwords ? passwords.length: 0} Sites and Apps</Col>
<Col xs={12} sm={6}>
<Form inline onSubmit={(e) => {e.preventDefault()}}>
<input type="text" placeholder="Search Passwords" className="form-control ml-md-auto" onChange={(e)=> {e.preventDefault()}} />
</Form>
</Col>
</Row>
<br/><br/>
<Row>
{passwords.length > 0?
passwords.map(ele => {
const bytes = CryptoJS.AES.decrypt(ele.encryptedPassword, process.env.REACT_APP_SECRET_KEY);
const password = bytes.toString(CryptoJS.enc.Utf8)
const passwordData = {...ele, password}
return <Password {...passwordData} key={ele.id} handleEdit={handleEdit} handleDelete={handleDelete} />
}) :
<p className="my-5 py-5 h2 display-5 w-100" style={{textAlign : "center"}}>You have not created any passwords</p>
}
</Row>
</>
}
</Container>
)
}
export default Passwords
此文件包含两个组件:Password和Passwords组件。我们的仪表盘将以相同的样式显示密码列表,因此需要一个可以用于Passwords其他组件的组件来显示单个密码。我们先来看Password组件。
组件中正在发生以下情况Password:
-
该组件接收以下属性:
-
id:从数据库(Fauna)生成的密码的 ID -
accountName:我们要保存密码的应用程序名称 -
accountUrl:我们保存密码的应用程序的 URL -
email可以是电子邮件地址或用户名,具体取决于您用于登录的方式。 -
password用于登录应用程序的密码。 -
handleDelete点击删除按钮时调用的函数 -
handleEdit编辑密码时调用的函数 -
该组件有两种状态:
-
editModal:组件中使用的状态editPassword。它用于设置show模态框的属性。 -
previewModal:组件中用于PreviewPassword设置show模态框属性的状态。 -
该组件创建了三个函数:
-
previewPassword用于将状态设置PreviewModal为 true -
当我们在控制面板中点击密码时,就会调用此函数。
-
editPassword此函数调用handleEdit来自src/screens/App.js.的 props。props 与我们文件中的函数handleEdit通信。此函数调用此函数,然后将状态值设置回 false,最后显示成功消息。editPasswordmodels.jseditPasswordhandleEditsetEditModal -
deletePassword调用handleDeleteprops 并显示成功消息 -
该组件的返回语句是一个 `
Colfrom` 元素react-bootstrap;它包含一个带有`of` 属性的Col按钮,点击该按钮会显示密码预览模态框。该组件返回的第二个内容是模态框本身。您可以点击此链接查看如何使用它。该组件还有一些额外的属性,例如 `<property>` 和 ` <property> `,我已将它们显示在模态框中。onClickpreviewPasswordPreviewPasswordModalmodalsreact-bootstrapaccountNameaccountUrl
现在我们来看看组件内部发生了什么Passwords:这个组件是无状态的;它接收以下属性:
passwords用户创建的密码数组handleEdit和handleDelete:传递给Password组件的函数。isPending用于判断应用程序是否仍在从数据库获取数据
加密
加密是将文本转换成代码,防止未经授权的用户访问的过程。加密和解密信息的科学称为密码学。您可以阅读这篇文章以更好地了解加密。加密分为两种类型:单向加密symmetric和asymmetric双向加密。
- 对称加密:在对称加密中,加密和解密使用同一个密钥。因此,采用安全的密钥传输方式在发送方和接收方之间传递至关重要。
- 非对称加密:非对称加密使用密钥对的概念:加密和解密过程使用不同的密钥。其中一个密钥通常称为私钥,另一个称为公钥。
您可以查看这篇文章,以便更好地了解这些类型的加密。
我们为什么需要加密?
如果我们把原始密码存储在数据库中,而授权用户获得了数据库访问权限,那么所有用户数据都会泄露。因此,我们需要一种安全的方式来存储用户数据,防止管理员获取原始密码。你可能会问,为什么不呢?因为即使我们想要存储加密数据,我们仍然需要在应用程序中查看原始密码,这就需要对这些密码进行加密和解密。如果我们对密码进行哈希处理,就无法解密,因为哈希是单向加密,而加密是双向加密。
为了简化起见,本应用将使用对称加密。加密算法有很多种,但我使用的是高级加密标准(AES)。我们将使用相应的crypto-js软件包。正如您在组件中看到的Passwords,由于数据库中存储的是加密后的密码,因此我们需要对密码进行解密。
这是我们数据库中的一个示例数据。
如果您选择使用控制面板,您应该会看到以下内容:
创建密码组件
它只createPasswordModal返回导航栏下拉菜单中显示的文本create password。我们来处理这个组件。在你的src/components/createPassword.modal.js文件中,输入以下内容:
import Modal from 'react-bootstrap/Modal'
import Container from "react-bootstrap/Container";
import Button from "react-bootstrap/Button";
import Form from "react-bootstrap/Form";
import Row from "react-bootstrap/Row";
import Col from "react-bootstrap/Col";
import CryptoJS from "crypto-js";
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faPlus } from '@fortawesome/free-solid-svg-icons'
import { useState } from 'react'
import dotenv from 'dotenv'
dotenv.config()
const CreatePasswordModal = props => {
const [accountName, setAccountName] = useState('')
const [accountUrl, setAccountUrl] = useState('')
const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
const handleCreate = async () => {
const encryptedPassword = CryptoJS.AES.encrypt(password, process.env.REACT_APP_SECRET_KEY).toString()
const payload = {
accountName,
accountUrl,
email,
encryptedPassword
}
props.handleCreate(payload)
setAccountName('')
setAccountUrl('')
setEmail('')
setPassword('')
window.flash('Password created successfully', 'success')
}
const onHide = () => {
props.onHide(accountUrl, password, email, accountName)
}
return (
<Modal
{...props} size="xlg" aria-labelledby="contained-modal-title-vcenter" centered onHide={onHide}
>
<Modal.Header style={{backgroundColor : "#d1e1f0"}} closeButton>
<Modal.Title id="contained-modal-title-vcenter">Create New Password</Modal.Title>
</Modal.Header>
<Modal.Body className="show-grid">
<Container>
<Form>
<Row>
<Form.Group as={Col}>
<Form.Control placeholder="Account Name" value={accountName} onChange={(e) => setAccountName(e.target.value)}/>
</Form.Group>
<Form.Group as={Col}>
<Form.Control placeholder="Account URL" defaultValue={`https://${accountUrl}`} onChange={(e) => setAccountUrl(e.target.value)}/>
</Form.Group>
</Row>
<Row>
<Form.Group as={Col}>
<Form.Control type="email" value={email} placeholder="Email" onChange={(e) => setEmail(e.target.value)}/>
</Form.Group>
</Row>
<Row>
<Form.Group as={Col}>
<Form.Control type="password" value={password} placeholder="Password" onChange={(e) => setPassword(e.target.value)}/>
</Form.Group>
</Row>
</Form>
</Container>
</Modal.Body>
<Modal.Footer>
<Button variant="success" onClick={handleCreate} disabled={(!accountUrl || !accountName || !email) ? true : false}>
<FontAwesomeIcon icon={faPlus} size="1x" className="" />
</Button>
</Modal.Footer>
</Modal>
);
}
export default CreatePasswordModal
该组件有四种状态,分别对应输入字段中的值。它还有两个函数:handleCreate一个是在点击加号图标时调用的,另一个onHide是在关闭模态框时调用的。
点击create new password按钮后,应用程序应该看起来像这样。
创建一些密码,它们将显示在您的仪表板中。
点击按钮即可看到文本preview password。之所以能看到密码预览文本,是因为它在previewPasswordModal组件中已渲染。
预览密码组件
在您的src/components/previewPassword.modal.js文件中,输入以下内容:
import { useState } from "react";
import Modal from 'react-bootstrap/Modal'
import FormControl from 'react-bootstrap/FormControl'
import Container from "react-bootstrap/Container";
import Button from "react-bootstrap/Button";
import Row from "react-bootstrap/Row";
import Col from "react-bootstrap/Col";
import EditPasswordModal from "./editPassword.modal";
import web from '../assets/web.png';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faLink, faEye, faEyeSlash, faCopy, faEdit, faTrashAlt } from '@fortawesome/free-solid-svg-icons'
const PreviewPasswordModal = props => {
const [passwordType, setPasswordType] = useState('password')
return <Modal
{...props} size="xlg"aria-labelledby="contained-modal-title-vcenter" centered>
<Modal.Header style={{backgroundColor : "#d1e1f0"}} closeButton>
<Modal.Title id="contained-modal-title-vcenter">
<img src={web} alt=""/> {props.accountName}
</Modal.Title>
</Modal.Header>
<Modal.Body className="show-grid">
<Container>
<Row>
<Col>
<p><FontAwesomeIcon icon={faLink} size="sm" /> <a href={props.accountUrl} rel="noreferrer" target="_blank"><small>{props.accountName}</small></a></p>
<div><FormControl type="text" value={props.email} className="my-1" readOnly/></div>
<Row className="my-1">
<Col xs={8} md={9}>
<FormControl type={passwordType} value={props.password} readOnly/>
</Col>
<Col xs={2} md={1} className="text-left">
<span style={{cursor : 'pointer'}} onClick={() => {setPasswordType(passwordType === "password"? "text" : "password")}}>
{passwordType === "password"?
<FontAwesomeIcon icon={faEye} size="1x" className="align-bottom" />
:
<FontAwesomeIcon icon={faEyeSlash} size="1x" className="align-bottom" /> }
</span>
</Col>
<Col xs={2} md={1} className="text-right">
<span style={{cursor : 'pointer'}}
onClick={() => {
let passwordText = document.createElement('textarea')
passwordText.innerText = props.password
document.body.appendChild(passwordText)
passwordText.select()
document.execCommand('copy')
passwordText.remove()
}}>
<FontAwesomeIcon icon={faCopy} size="1x" className="align-bottom" />
</span>
</Col>
</Row>
</Col>
</Row>
</Container>
</Modal.Body>
<Modal.Footer>
<Button onClick={props.onEdit}>
<FontAwesomeIcon icon={faEdit} size="md" className="" />
</Button>
<Button variant="danger" onClick={props.onDelete}>
<FontAwesomeIcon icon={faTrashAlt} size="1x" className="" />
</Button>
</Modal.Footer>
<EditPasswordModal
closePreview={() => {props.onHide()}}
id={props.id}
show={props.edit}
editPassword={props.editPassword}
onEdit={props.onEdit}
accountName={props.accountName}
accountUrl={props.accountUrl}
email={props.email}
password={props.password}
title={"Edit Password for "+props.accountName}
onHide={props.onHideEdit}
/>
</Modal>
}
export default PreviewPasswordModal
此组件渲染模态框和EditPasswordModal组件。我们会向组件传递一些属性。如果您点击仪表盘中的任何密码,您应该会看到以下内容:
请查看Edit Password模态框底部的文本;该文本由EditPasswordModal组件渲染。此组件包含用于复制和预览密码的功能。
编辑密码模态框
在您的editPasswordModal.js文件中,输入以下内容:
import Modal from 'react-bootstrap/Modal'
import Container from "react-bootstrap/Container";
import Button from "react-bootstrap/Button";
import Form from "react-bootstrap/Form";
import Row from "react-bootstrap/Row";
import Col from "react-bootstrap/Col";
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faEye, faEyeSlash, faEdit} from '@fortawesome/free-solid-svg-icons'
import { useState } from 'react'
import CryptoJS from "crypto-js";
import dotenv from 'dotenv'
dotenv.config()
const EditPasswordModal = props => {
const [accountName, setAccountName] = useState(props.accountName)
const [accountUrl, setAccountUrl] = useState(props.accountUrl)
const [email, setEmail] = useState(props.email)
const [password, setPassword] = useState(props.password)
const [passwordType, setPasswordType] = useState('password')
const onEdit = () => {
const encryptedPassword = CryptoJS.AES.encrypt(password, process.env.REACT_APP_SECRET_KEY).toString()
const payload = {
accountName,
accountUrl,
email,
encryptedPassword,
id: props.id
}
props.editPassword(payload)
props.closePreview()
}
return (
<Modal {...props} size="xlg" aria-labelledby="contained-modal-title-vcenter" centered>
<Modal.Header style={{backgroundColor : "#d1e1f0"}} closeButton>
<Modal.Title id="contained-modal-title-vcenter">
{props.title}
</Modal.Title>
</Modal.Header>
<Modal.Body className="show-grid">
<Container>
<Form>
<Row>
<Form.Group as={Col}>
<Form.Control placeholder="Account Name" value={accountName} onChange={(e) => setAccountName(e.target.value)}/>
</Form.Group>
<Form.Group as={Col}>
<Form.Control placeholder="Account URL" value={accountUrl} onChange={(e) => setAccountUrl(e.target.value)}/>
</Form.Group>
</Row>
<Row>
<Form.Group as={Col}>
<Form.Control type="email" value={email} placeholder="Email" onChange={(e) => setEmail(e.target.value)}/>
</Form.Group>
</Row>
<Row className="my-1">
<Col>
<Form.Control type={passwordType} value={password} onChange={(e) => setPassword(e.target.value)}/>
</Col>
<Col xs={2} className="text-center">
<span style={{cursor : 'pointer'}}
onClick={() => {setPasswordType(passwordType === "password"? "text" : "password")}}>
{passwordType === "password"?
<FontAwesomeIcon icon={faEye} size="1x" className="align-bottom" />
:
<FontAwesomeIcon icon={faEyeSlash} size="1x" className="align-bottom" /> }
</span>
</Col>
</Row>
</Form>
</Container>
</Modal.Body>
<Modal.Footer>
<Button variant="success" onClick={onEdit} disabled={(!accountUrl || !accountName || !email) ? true : false}>
<FontAwesomeIcon icon={faEdit} size="1x" className="" /> Edit
</Button>
</Modal.Footer>
</Modal>
);
}
export default EditPasswordModal
现在点击该edit图标,我们应该会看到以下内容:
您还可以将密码输入字段的类型从密码切换到文本,以便预览和尝试编辑密码。
结论
本文介绍了如何使用React JS、Fauna、React Bootstrap和Crypto JS构建密码管理器应用。您可以在这里查看该应用的代码片段,部署后的应用版本在这里。如有任何问题,您可以通过Twitter联系我。此外,您还可以为该应用创建一个 404 页面,因为它目前还没有。
文章来源:https://dev.to/bkoiki950/building-a-password-manager-with-react-js-crypto-js-and-fauna-29g9

























