发布于 2026-01-06 0 阅读
0

使用 React JS、Crypto JS 和 Fauna 构建密码管理器 使用 React JS 和 Fauna 构建 Google 密码管理器克隆版 简介 Fauna 入门 设置应用程序 运行我们的样板应用程序 加密 结论 DEV 的全球展示挑战赛 由 Mux 呈现:展示你的项目!

使用 React JS、Crypto JS 和 Fauna 构建密码管理器

使用 React JS 和 Fauna 构建 Google 密码管理器克隆版

介绍

动物入门

设置应用程序

运行我们的样板应用程序

加密

结论

由 Mux 主办的 DEV 全球展示挑战赛:展示你的项目!

使用 React JS 和 Fauna 构建 Google 密码管理器克隆版

本文是为“与动物一起写作”项目而作。

介绍

本文将带您了解我如何使用 React JS 和 Fauna 构建一个密码管理器。密码管理器至关重要。当我们拥有多个账户和多个密码时,我们需要管理它们。如果没有密码管理器的帮助,管理密码将非常困难。

先决条件

  1. 具备React和 JSX的基础知识
  2. npmnpx已安装。
  3. 如何创建一个 React 应用
  4. React Bootstrap已安装。
  5. 加密密码学的基础知识

替代文字

动物入门

首先,在Fauna创建一个帐户

替代文字

创建动物数据库

要创建动物数据库,请前往动物控制面板

替代文字

接下来,点击按钮New Database,输入数据库名称,然后按回车键。

创建动物群落收藏

集合是将具有相同或相似用途的文档(行)分组在一起。集合的作用类似于传统 SQL 数据库中的表。

在我们正在创建的应用程序中,我们将有两个集合,分别是用户集合users和密码passwords集合。用户集合用于存储用户数据,而密码passwords集合用于保存所有密码数据。

要创建这些集合,请单击您创建的数据库,然后单击New Collection。仅输入集合名称(users),然后单击保存,并对第二个集合(passwords)执行相同的操作。

替代文字

创建动物群索引

使用索引可以快速查找数据,无需每次访问数据库集合时都搜索数据库集合中的每个文档。可以使用数据库集合中的一个或多个字段创建索引。要创建动物群索引,请单击indexes仪表板左侧的相应部分。

替代文字

在本应用中,我们将创建以下索引:

  1. user_passwords:用于检索特定用户创建的所有密码的索引。
  2. user_by_email用于通过用户邮箱检索特定用户数据的索引。此索引必须唯一。

设置应用程序

接下来,我们将使用下面的入门项目。首先,请在 GitHub 上克隆该项目。

git clone <https://github.com/Babatunde13/password-manager-started-code.git>
cd password-manager-starter-code
npm install
Enter fullscreen mode Exit fullscreen mode

克隆仓库后,将下载以下文件/文件夹:

  1. /src/assets/此文件夹包含应用程序中将使用的所有图像。
  2. /src/App.css这是我们应用程序的基础 CSS 文件。
  3. /src/models.js我们将通过这个文件与我们的动物数据库进行通信。
  4. .env.sample此文件显示了成功运行应用程序所需的环境变量。
  5. 服务工作线程文件用于 PWA 功能。
  6. index.js:此文件用于将文件div中的内容挂载public/index.html到我们的应用程序组件。
  7. src/screens此文件夹定义了应用程序中的所有页面(屏幕)。以下屏幕已在此screen文件夹中定义:

  8. Home.js这是首页。

  9. Signin.js这是登录页面。

  10. Signup.js这是注册页面。

  11. App.js这是仪表盘页面。

  12. src/components这是创建应用程序中所有组件的文件夹。以下组件均创建于此components文件夹中:

  13. Flash此文件夹包含一个文件flash.js和一个flash.css文件。文件中导出的组件flash.js用于在应用程序中闪烁消息。

  14. createPassword.modal.js这是在尝试创建新密码时显示的模态框。

  15. editPassword.modal.js当用户尝试更新密码时,将显示此模态框。

  16. Navbar.js这是导航栏组件。

  17. Passwords.js该组件用于渲染密码,并导入到应用程序仪表板中。

  18. 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以及一些库。

  1. 我们从 React导入了 `<Route> BrowserRouter`、Switch` Route<Route>` 和 `<Route>` ;这个库用于定义组件的端点。`<Route>`组件可以用来路由多个组件,我们还可以设置一些组件在整个应用程序中都存在。` <Route>` 组件用于告诉 React 一次只渲染一个组件。`<Route>` 组件接收路径和组件,我们还传递一个参数,告诉它匹配同一个端点。Redirectreact-router-domBrowserRouterswitchexact
  2. 我们还导入了一个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
Enter fullscreen mode Exit fullscreen mode

这里没什么特别的,只有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
Enter fullscreen mode Exit fullscreen mode

应用程序主页现在应该如下所示:

替代文字

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

Enter fullscreen mode Exit fullscreen mode

当应用的任何部分触发事件时,都会渲染此组件。我们需要从根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
}
Enter fullscreen mode Exit fullscreen mode
  1. 第一个函数createUser接收要创建的用户的数据:名字、姓氏、邮箱和密码(明文),并创建用户数据。在创建文档之前,我们会先对密码进行哈希处理。
  2. 第二个函数getUser用于根据其唯一 ID 获取用户数据。
  3. 该函数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>
    </>
  )
}

Enter fullscreen mode Exit fullscreen mode
  1. 在函数开始时,我们验证用户是否未通过身份验证。如果用户已通过身份验证,则调用window.flash之前创建的函数,并传递消息和警告信息;然后,重定向回首页。
  2. 接下来,我们创建了一个validated用于数据验证的状态。
  3. 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>
      </>
    )
  }

Enter fullscreen mode Exit fullscreen mode

此组件与注册组件类似。

替代文字

替代文字

密码模型

更新该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
}

Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

您可能已经知道,此页面受到保护,未经身份验证的用户无法访问。因此,我们首先检查用户对象是否存在localStorage,如果用户未登录,则将其重定向回登录页面。

替代文字

替代文字

仪表盘渲染密码组件,该组件会将密码显示在 DOM 中。此组件有两种状态:passwords 和 isPending。从数据库获取数据时,组件isPending状态设置为 passwords true。成功从数据库获取密码数据后,passwordsisPending状态重置为 false,isPendingpasswords状态设置为已获取的数据。从passwords数据库获取数据时,DOM 上会显示一个加载指示器(spinner)。我们通过检查isPendingisPending 状态是否为true 来实现这一点true,如果为 true,则在仪表盘中显示加载指示器。

passwords组件接受以下属性:

  1. isPending从数据库获取密码时,会显示一个加载指示器。
  2. passwords这是从获取已认证用户创建的密码中收到的数据。
  3. handleEdit当点击密码的编辑按钮时,将调用此函数。
  4. 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
Enter fullscreen mode Exit fullscreen mode

此文件包含两个组件:PasswordPasswords组件。我们的仪表盘将以相同的样式显示密码列表,因此需要一个可以用于Passwords其他组件的组件来显示单个密码。我们先来看Password组件。

组件中正在发生以下情况Password

  1. 该组件接收以下属性:

  2. id:从数据库(Fauna)生成的密码的 ID

  3. accountName:我们要保存密码的应用程序名称

  4. accountUrl:我们保存密码的应用程序的 URL

  5. email可以是电子邮件地址或用户名,具体取决于您用于登录的方式。

  6. password用于登录应用程序的密码。

  7. handleDelete点击删除按钮时调用的函数

  8. handleEdit编辑密码时调用的函数

  9. 该组件有两种状态:

  10. editModal:组件中使用的状态editPassword。它用于设置show模态框的属性。

  11. previewModal:组件中用于PreviewPassword设置show模态框属性的状态。

  12. 该组件创建了三个函数:

  13. previewPassword用于将状态设置PreviewModal为 true

  14. 当我们在控制面板中点击密码时,就会调用此函数。

  15. editPassword此函数调用handleEdit来自src/screens/App.js.的 props。props 与我们文件中的函数handleEdit通信。此函数调用此函数,然后将状态值设置回 false,最后显示成功消息。editPasswordmodels.jseditPasswordhandleEditsetEditModal

  16. deletePassword调用handleDeleteprops 并显示成功消息

  17. 该组件的返回语句是一个 ` Colfrom` 元素react-bootstrap;它包含一个带有`of` 属性的Col按钮,点击该按钮会显示密码预览模态框。该组件返回的第二个内容是模态框本身。您可以点击链接查看如何使用它该组件还有一些额外的属性,例如 `<property>` 和 ` <property> `,我已将它们显示在模态框中。onClickpreviewPasswordPreviewPasswordModalmodalsreact-bootstrapaccountNameaccountUrl

现在我们来看看组件内部发生了什么Passwords:这个组件是无状态的;它接收以下属性:

  1. passwords用户创建的密码数组
  2. handleEdithandleDelete:传递给Password组件的函数。
  3. isPending用于判断应用程序是否仍在从数据库获取数据

加密

加密是将文本转换成代码,防止未经授权的用户访问的过程。加密和解密信息的科学称为密码学。您可以阅读这篇文章以更好地了解加密。加密分为两种类型:单向加密symmetricasymmetric双向加密。

  1. 对称加密:在对称加密中,加密和解密使用同一个密钥。因此,采用安全的密钥传输方式在发送方和接收方之间传递至关重要。

替代文字

  1. 非对称加密:非对称加密使用密钥对的概念:加密和解密过程使用不同的密钥。其中一个密钥通常称为私钥,另一个称为公钥。

替代文字

您可以查看这篇文章,以便更好地了解这些类型的加密。

我们为什么需要加密?

如果我们把原始密码存储在数据库中,而授权用户获得了数据库访问权限,那么所有用户数据都会泄露。因此,我们需要一种安全的方式来存储用户数据,防止管理员获取原始密码。你可能会问,为什么不呢?因为即使我们想要存储加密数据,我们仍然需要在应用程序中查看原始密码,这就需要对这些密码进行加密和解密。如果我们对密码进行哈希处理,就无法解密,因为哈希是单向加密,而加密是双向加密。

为了简化起见,本应用将使用对称加密。加密算法有很多种,但我使用的是高级加密标准(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
Enter fullscreen mode Exit fullscreen mode

该组件有四种状态,分别对应输入字段中的值。它还有两个函数: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
Enter fullscreen mode Exit fullscreen mode

此组件渲染模态框和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
Enter fullscreen mode Exit fullscreen mode

现在点击该edit图标,我们应该会看到以下内容:

替代文字

您还可以将密码输入字段的类型从密码切换到文本,以便预览和尝试编辑密码。

结论

本文介绍了如何使用React JSFaunaReact BootstrapCrypto JS构建密码管理器应用。您可以在这里查看该应用的代码片段,部署后的应用版本在这里。如有任何问题,您可以通过Twitter联系我。此外,您还可以为该应用创建一个 404 页面,因为它目前还没有。

文章来源:https://dev.to/bkoiki950/building-a-password-manager-with-react-js-crypto-js-and-fauna-29g9